Conversation
…n, and update Supabase client initialization for SSR.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughBump to 2.0.x with Ethereum/Gemini/Sarvam integrations, WalletConnect v2 auth and token persistence, interactive paginated book reader and StoryEngagement (votes/comments/saves), Supabase client/server async and noop build-safe clients, same-origin TTS/AI routing, Redis/Upstash support, Docker/Cloudflare export adjustments, and various UI/hydration tweaks. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Client as Web Client
participant WalletApp as Wallet (mobile/extension)
participant Backend as Server
participant Supabase as Supabase
User->>Client: Click WalletConnect
Client->>Client: Init WalletConnect v2 (EthereumProvider)
Client->>WalletApp: Open QR / trigger modal
WalletApp->>Client: Return accounts
Client->>Backend: GET /api/v1/auth/nonce?address={addr}
Backend->>Client: Return nonce
Client->>WalletApp: personal_sign(nonce)
WalletApp->>Client: Return signature
Client->>Backend: POST /api/v1/auth/wallet-login {address, signature}
Backend->>Supabase: Verify & create session
Supabase->>Backend: Return tokens
Backend->>Client: Return accessToken, refreshToken
Client->>Client: Store tokens in localStorage & dispatch StorageEvent
Client->>User: Redirect to /dashboard
sequenceDiagram
participant User
participant Client as StoryEngagement
participant Backend as Supabase API
participant DB as Database
User->>Client: Open story page (mount)
Client->>Backend: Fetch engagement (votes, comments, saved)
Backend->>DB: Query story_votes, story_comments, saved_stories
DB->>Backend: Return current state
Backend->>Client: Return engagement data
Client->>User: Render votes/comments/save UI
User->>Client: Click upvote / submit comment
Client->>Client: Optimistic UI update
Client->>Backend: Insert/Update vote or comment
Backend->>DB: Persist change
DB->>Backend: Confirm
Backend->>Client: Return updated state
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly Related PRs
Suggested Labels
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Deploying groqtales with
|
| Latest commit: |
6137b70
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://9d8c4862.groqtales.pages.dev |
| Branch Preview URL: | https://drago.groqtales.pages.dev |
There was a problem hiding this comment.
Actionable comments posted: 13
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
README.md (2)
282-286:⚠️ Potential issue | 🟡 MinorSmart Contracts section still references Monad Testnet.
The contract workflow section mentions Monad Testnet (ChainID: 10143) and includes a warning about not sending real funds to Monad addresses. This conflicts with the updated tech stack (Ethereum Mainnet via Alchemy). Update to reflect the current blockchain target.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@README.md` around lines 282 - 286, Update the "Smart Contracts" / "contract workflow" section to replace references to Monad Testnet (ChainID: 10143) with Ethereum Mainnet (via Alchemy), update any network label/chain ID to Mainnet (chain ID 1), and revise the safety warning to advise against sending real funds to test/deployed addresses while mentioning that development uses Alchemy endpoints and that testing should use a dedicated developer or faucet-funded testnet wallet; ensure the heading and any inline references (e.g., "Network:", the warning paragraph) are updated for consistency.
181-181:⚠️ Potential issue | 🟡 MinorInconsistent environment variable:
MONAD_RPC_URLstill marked as required.The README now documents Ethereum Mainnet via Alchemy (lines 74, 96, 122), but the environment configuration table still lists
MONAD_RPC_URLas required. This should be updated to reference Alchemy/Ethereum configuration instead.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@README.md` at line 181, Update the README table row that currently lists MONAD_RPC_URL: replace the environment variable name MONAD_RPC_URL with ALCHEMY_RPC_URL and change its description to "The RPC endpoint for interacting with Ethereum Mainnet via Alchemy." Ensure the table entry reflects the correct requirement status (mark as Required if the app needs an Alchemy RPC) and keep the rest of the table formatting intact so references elsewhere (README mentions of Alchemy/Ethereum) remain consistent.
🧹 Nitpick comments (6)
lib/feeds-client.ts (1)
69-76: Redundant token check in headers.After the early return on line 69,
tokenis guaranteed to be truthy, making the conditional spread...(token && { ... })unnecessary.♻️ Suggested simplification
const token = getAuthToken(); if (!token) return []; const res = await fetch( `${feedsBase()}/api/feeds/notifications/me?unread=${unreadOnly ? 50 : 0}&limit=${limit}`, { headers: { - ...(token && { Authorization: `Bearer ${token}` }), + Authorization: `Bearer ${token}`, }, } );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/feeds-client.ts` around lines 69 - 76, The Authorization header spread inside the fetch call is redundant because token is already guaranteed truthy by the early return; update the fetch options in the function that calls fetch with feedsBase()/api/feeds/notifications/me (using unreadOnly and limit) to set Authorization: `Bearer ${token}` directly in headers instead of using the conditional spread `...(token && { Authorization: ... })`, leaving the early `if (!token) return []` in place.app/auth/callback/page.tsx (1)
34-42: Consider extracting token persistence to a shared utility.This token persistence pattern is duplicated across
sign-in-form.tsx,sign-up-form.tsx, and this callback page. A shared helper would improve maintainability.♻️ Example shared utility
// lib/auth-utils.ts export function persistAuthTokens(accessToken: string, refreshToken?: string) { if (typeof window === 'undefined') return; localStorage.setItem('accessToken', accessToken); if (refreshToken) { localStorage.setItem('refreshToken', refreshToken); } window.dispatchEvent(new StorageEvent('storage', { key: 'accessToken' })); }Then in each auth flow:
import { persistAuthTokens } from '@/lib/auth-utils'; // ... if (session?.access_token) { persistAuthTokens(session.access_token, session.refresh_token); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/auth/callback/page.tsx` around lines 34 - 42, Extract the duplicated localStorage token persistence into a shared helper named persistAuthTokens (e.g., export function persistAuthTokens(accessToken: string, refreshToken?: string)) and place it in a new module like lib/auth-utils.ts; then replace the inline logic in the callback page (the block using supabase.auth.getSession() that sets localStorage and dispatches the StorageEvent) and the same code in sign-in-form.tsx and sign-up-form.tsx with a call to persistAuthTokens(session.access_token, session.refresh_token) after checking session?.access_token, and import the helper where needed.app/globals.css (1)
1534-1552: Addprefers-reduced-motionsupport for page flip animations.The new animations should respect user motion preferences for accessibility, consistent with other animations in this file (e.g., lines 757-774, 896-913).
♿ Add reduced motion support
.animate-page-flip-right { animation: page-flip-right 0.35s ease-in-out; } .animate-page-flip-left { animation: page-flip-left 0.35s ease-in-out; } +@media (prefers-reduced-motion: reduce) { + .animate-page-flip-right, + .animate-page-flip-left { + animation: none !important; + } +}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/globals.css` around lines 1534 - 1552, The page-flip animations (`@keyframes` page-flip-right, page-flip-left and classes .animate-page-flip-right, .animate-page-flip-left) lack prefers-reduced-motion handling; add a media query for prefers-reduced-motion: reduce that disables or drastically shortens these animations (e.g., set animation: none or minimal duration) for both classes so the flip effect is removed for users who opt out of motion, matching the pattern used elsewhere in this file for other animations.components/book-view.tsx (1)
284-294: Magic number9999for "last page" navigation is fragile.Setting
currentPageto9999and relying on clamping (line 294) works but is unclear. Consider computing the last page index explicitly or usingInfinitywith a comment, or set it after the chapter state updates.♻️ Suggested improvement
setTimeout(() => { setActiveChapter(c => c - 1); - // We'll set page to last page of previous chapter after state updates - setCurrentPage(9999); // Will be clamped in render + // Set to max value; will be clamped to actual last page during render + setCurrentPage(Number.MAX_SAFE_INTEGER); setIsFlipping(false); }, 350);Or restructure to compute the previous chapter's page count and set the exact value.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/book-view.tsx` around lines 284 - 294, Replace the fragile magic number 9999 used in the timeout block by computing and setting the actual last-page index for the previous chapter instead of relying on clamping: after calling setActiveChapter(c => c - 1) inside the setTimeout, compute the previous chapter's page count (or derive it from the same source used to compute totalPages) and call setCurrentPage(lastPageIndex) (or move setCurrentPage into a callback/effect that runs after setActiveChapter completes). Update references to setCurrentPage and ensure safeCurrentPage remains consistent with the new explicit last-page assignment.components/story-engagement.tsx (1)
44-48: Missingsupabasein useEffect dependency array.The
supabaseclient is stable (created once at component mount viacreateClient()), so this is technically safe. However, the ESLintreact-hooks/exhaustive-depsrule will flag this. Consider addingsupabaseto the dependency array or using// eslint-disable-next-line react-hooks/exhaustive-depswith a comment explaining why it's safe.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/story-engagement.tsx` around lines 44 - 48, The useEffect that calls supabase.auth.getSession() is missing the supabase dependency (useEffect(() => { ... }, [])), which will be flagged by react-hooks/exhaustive-deps; update the useEffect in components/story-engagement.tsx to either include supabase in the dependency array or, if supabase is guaranteed stable (created once via createClient), add a one-line eslint disable comment (// eslint-disable-next-line react-hooks/exhaustive-deps) immediately above the useEffect with a short explanation why it's safe; keep the existing setUserId(data.session?.user?.id ?? null) logic intact.app/api/tts/generate/route.ts (1)
70-96: Supabase client created twice.A new Supabase client is created at line 72 and again at line 96. Consider reusing the same client instance for both storage upload and DB upsert operations.
♻️ Proposed refactor
if (SUPABASE_SERVICE_KEY) { + const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_KEY); try { - const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_KEY); const fileName = `tts/${storyId}/ch${chapterIndex}_${speaker}_${languageCode}.wav`; // ... storage upload code ... } catch (uploadErr) { console.warn('Failed to upload audio to storage, using base64 fallback:', uploadErr); } - } - // Also store record in story_audio table if available - if (SUPABASE_SERVICE_KEY) { try { - const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_KEY); await supabase.from('story_audio').upsert({ // ... }); } catch { // Non-critical — continue even if DB insert fails } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/tts/generate/route.ts` around lines 70 - 96, The code creates a Supabase client twice (createClient called in two separate try blocks) — consolidate by creating one supabase instance when SUPABASE_SERVICE_KEY is truthy (e.g., const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_KEY)) and reuse it for both the storage upload (the block using supabase.storage.upload and fileName) and the DB upsert/insert into story_audio, removing the second createClient call; ensure error handling wraps the combined operations (or handle upload and DB insert separately but using the same supabase variable) so you still catch uploadErr/DB errors while avoiding duplicate client construction.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/api/tts/audio/route.ts`:
- Around line 20-21: The current code selects SUPABASE_SERVICE_KEY when
available and passes it to createClient, which exposes a service-role key on a
public GET endpoint; change the logic in route.ts so you never use
SUPABASE_SERVICE_KEY/SUPABASE_SERVICE_ROLE_KEY for this handler: either (A)
always initialize the client with SUPABASE_ANON_KEY (use SUPABASE_ANON_KEY
directly in createClient) if the story_audio data is public, or (B) enforce
authentication and authorization in the handler (validate a session/token, check
permissions) and only then initialize or use a server-side client for privileged
reads; update references to SUPABASE_SERVICE_KEY/SUPABASE_SERVICE_ROLE_KEY and
createClient accordingly so the service role is not used in unauthenticated
requests.
- Around line 4-6: The file leaks hardcoded Supabase credentials: remove the
literal fallback values for SUPABASE_URL, SUPABASE_SERVICE_KEY, and
SUPABASE_ANON_KEY in app/api/tts/audio/route.ts and instead read them directly
from process.env; if any required value is missing, fail fast by throwing a
descriptive error (e.g., in module init or a helper like getSupabaseConfig) so
the app won’t silently point at a shared project—update any usages of
SUPABASE_URL, SUPABASE_SERVICE_KEY, and SUPABASE_ANON_KEY to rely on this
validated config.
In `@app/api/tts/generate/route.ts`:
- Around line 93-107: The POST route in route.ts attempts to upsert into
story_audio (via createClient(...).from('story_audio').upsert(...)) but the
table is missing from supabase/schema.sql and failures are silently swallowed;
either add a story_audio table definition to supabase/schema.sql with columns
story_id (matching your stories PK type), chapter_index (integer), speaker
(text), language_code (text), audio_url (text) and an appropriate unique
constraint on (story_id, chapter_index, speaker, language_code), or remove the
upsert entirely if caching isn’t intended; also replace the empty catch in
route.ts so it logs the caught error (include the error/exception and context)
instead of swallowing it.
- Around line 75-86: The code uses the wrong Supabase bucket name 'story-audio'
causing mismatch with the backend's 'tts-audio'; update both occurrences of
supabase.storage.from('story-audio') in this route (the upload call that uses
fileName and audioBuffer, and the subsequent getPublicUrl call that sets
audioUrl) to use 'tts-audio' so uploaded files and public URL lookups align with
server/routes/tts.js.
In `@app/gallery/page.tsx`:
- Around line 33-35: The card currently wraps interactive controls (the vote
controls and the "Read File" button) inside the full-card <Link
href={`/stories/${post.id}`}> which creates nested interactive content and
causes the unguarded "Read File" click to navigate to /stories/${post.id}; fix
this by moving the vote controls and the "Read File" button out of the <Link>
(only keep non-interactive hero content—image/title—inside the Link), or
alternatively replace the outer <Link> with a non-interactive wrapper and create
a distinct accessible <Link> for the title/read action; ensure the "Read File"
button is a standalone <button> or <Link> (not nested), remove any click
handlers that let it bubble into the outer Link (or call event.stopPropagation
if you must keep it adjacent), and update components referenced (the Link
wrapper around the card, the vote control components, and the "Read File" button
that references post.id) so keyboard and screen-reader behavior is correct.
In `@components/book-view.tsx`:
- Around line 254-305: The keyboard navigation useEffect is placed after the
early return for compact mode, causing hooks to be called conditionally; move
the useEffect (and any related state/logic that defines goNext, goPrev,
isFlipping, setIsFlipping, currentPage, activeChapter, totalPages, pages,
safeCurrentPage and pageContent) so that the hook is defined before the early
return for compact, ensuring useEffect is called on every render while keeping
the compact return intact; update any references so goNext/goPrev still work
when compact is true (they can be no-ops or used only when needed).
In `@components/story-engagement.tsx`:
- Around line 114-117: The loadComments effect is overwriting the accurate
initial commentCount with the paginated data length; stop setting commentCount
to data.length in the block that calls setComments. Update the code around
setComments and setCommentCount so that setCommentCount is not assigned using
(data as any)?.length (or only updated from a total/count field returned by the
API), e.g. keep the initial exact count from the earlier fetch or use a returned
totalCount value instead of data.length; reference the setComments and
setCommentCount calls inside the loadComments handler to locate and
remove/replace the length-based overwrite.
- Around line 128-147: The optimistic UI updates in the vote handler are applied
immediately but not reverted on failure; capture the previous state (e.g., const
prevVote = userVote and const prevScore = voteScore) before performing the
optimistic update (using setVoteScore and setUserVote), then perform the DB call
via supabase.from('story_votes').upsert or .delete inside the try; in the catch
block revert state using setVoteScore(prevScore) and setUserVote(prevVote) and
call setVoteLoading(false) and surface an error to the user (toast or alert)
instead of only console.error so the UI stays consistent with the server.
In `@components/wallet-connect.tsx`:
- Line 134: The signature message currently reads "Sign this message to
authenticate with Comicraft..." which is inconsistent; update the string
assigned to the message constant in components/wallet-connect.tsx (the const
message declaration) to use the correct product name "GroqTales" (or update to
the agreed branding), making sure any other occurrences of "Comicraft" in the
same file or related auth code (e.g., functions handling signature/nonce) are
updated for consistency.
- Around line 89-165: The WalletConnect flow in handleWalletConnect stores
tokens but never updates Web3 context, so call the Web3 context updater after
successful auth: import/use the connectWallet (or equivalent) from useWeb3 and
invoke it once authRes.ok and tokens are stored (e.g., connectWallet({ provider,
account: selectedAccount, chainId: provider.chainId || <derive from provider> })
or call a provided setAccount/setConnected/setChainId), or emit the same storage
event handler that Web3Provider listens for; update Web3Provider to accept
WalletConnect provider instances if needed so account, connected and chainId are
set and the UI reflects the connected state.
In `@docker-compose.yml`:
- Around line 17-67: The compose file removed the local MongoDB service but
server/backend.js still requires mongoose and a MongoDB connection (MONGO_URI)
for vector search; either re-add a mongo service to docker-compose.yml (e.g.,
service name "mongo" using mongo:6, expose 27017, add a volume like
mongo_data:/data/db, and a healthcheck) and add depends_on: mongo: condition:
service_healthy to the "app" service, or explicitly document and wire an
external MONGO_URI in the env_file/.env.local and remove any hard dependency by
updating server/backend.js connection logic to read process.env.MONGO_URI;
reference the symbols "mongo" (service), "MONGO_URI" (env var) and "mongoose" /
"server/backend.js" when making the change.
In `@lib/supabase/client.ts`:
- Around line 5-6: The code currently falls back to hardcoded Supabase URL and
anon key when process.env.NEXT_PUBLIC_SUPABASE_URL or
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY are missing; change this to fail fast
by removing the default literals and validating those env vars at startup (e.g.,
throw an Error or assert) so the app exits with a clear message if
NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY are undefined; if you
need defaults only for tests, gate them behind a specific test flag (not the
production/staging code path) and reference the same env names
(NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY) when adding the check.
In `@lib/supabase/server.ts`:
- Around line 7-9: Replace the hardcoded fallback values used when calling
createServerClient: stop using the literal Supabase URL and anon key and instead
require process.env.NEXT_PUBLIC_SUPABASE_URL and
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY to be present; if either is missing,
throw/exit immediately with a clear error so the app fails fast (do this before
invoking createServerClient) and use the environment values when calling
createServerClient.
---
Outside diff comments:
In `@README.md`:
- Around line 282-286: Update the "Smart Contracts" / "contract workflow"
section to replace references to Monad Testnet (ChainID: 10143) with Ethereum
Mainnet (via Alchemy), update any network label/chain ID to Mainnet (chain ID
1), and revise the safety warning to advise against sending real funds to
test/deployed addresses while mentioning that development uses Alchemy endpoints
and that testing should use a dedicated developer or faucet-funded testnet
wallet; ensure the heading and any inline references (e.g., "Network:", the
warning paragraph) are updated for consistency.
- Line 181: Update the README table row that currently lists MONAD_RPC_URL:
replace the environment variable name MONAD_RPC_URL with ALCHEMY_RPC_URL and
change its description to "The RPC endpoint for interacting with Ethereum
Mainnet via Alchemy." Ensure the table entry reflects the correct requirement
status (mark as Required if the app needs an Alchemy RPC) and keep the rest of
the table formatting intact so references elsewhere (README mentions of
Alchemy/Ethereum) remain consistent.
---
Nitpick comments:
In `@app/api/tts/generate/route.ts`:
- Around line 70-96: The code creates a Supabase client twice (createClient
called in two separate try blocks) — consolidate by creating one supabase
instance when SUPABASE_SERVICE_KEY is truthy (e.g., const supabase =
createClient(SUPABASE_URL, SUPABASE_SERVICE_KEY)) and reuse it for both the
storage upload (the block using supabase.storage.upload and fileName) and the DB
upsert/insert into story_audio, removing the second createClient call; ensure
error handling wraps the combined operations (or handle upload and DB insert
separately but using the same supabase variable) so you still catch uploadErr/DB
errors while avoiding duplicate client construction.
In `@app/auth/callback/page.tsx`:
- Around line 34-42: Extract the duplicated localStorage token persistence into
a shared helper named persistAuthTokens (e.g., export function
persistAuthTokens(accessToken: string, refreshToken?: string)) and place it in a
new module like lib/auth-utils.ts; then replace the inline logic in the callback
page (the block using supabase.auth.getSession() that sets localStorage and
dispatches the StorageEvent) and the same code in sign-in-form.tsx and
sign-up-form.tsx with a call to persistAuthTokens(session.access_token,
session.refresh_token) after checking session?.access_token, and import the
helper where needed.
In `@app/globals.css`:
- Around line 1534-1552: The page-flip animations (`@keyframes` page-flip-right,
page-flip-left and classes .animate-page-flip-right, .animate-page-flip-left)
lack prefers-reduced-motion handling; add a media query for
prefers-reduced-motion: reduce that disables or drastically shortens these
animations (e.g., set animation: none or minimal duration) for both classes so
the flip effect is removed for users who opt out of motion, matching the pattern
used elsewhere in this file for other animations.
In `@components/book-view.tsx`:
- Around line 284-294: Replace the fragile magic number 9999 used in the timeout
block by computing and setting the actual last-page index for the previous
chapter instead of relying on clamping: after calling setActiveChapter(c => c -
1) inside the setTimeout, compute the previous chapter's page count (or derive
it from the same source used to compute totalPages) and call
setCurrentPage(lastPageIndex) (or move setCurrentPage into a callback/effect
that runs after setActiveChapter completes). Update references to setCurrentPage
and ensure safeCurrentPage remains consistent with the new explicit last-page
assignment.
In `@components/story-engagement.tsx`:
- Around line 44-48: The useEffect that calls supabase.auth.getSession() is
missing the supabase dependency (useEffect(() => { ... }, [])), which will be
flagged by react-hooks/exhaustive-deps; update the useEffect in
components/story-engagement.tsx to either include supabase in the dependency
array or, if supabase is guaranteed stable (created once via createClient), add
a one-line eslint disable comment (// eslint-disable-next-line
react-hooks/exhaustive-deps) immediately above the useEffect with a short
explanation why it's safe; keep the existing setUserId(data.session?.user?.id ??
null) logic intact.
In `@lib/feeds-client.ts`:
- Around line 69-76: The Authorization header spread inside the fetch call is
redundant because token is already guaranteed truthy by the early return; update
the fetch options in the function that calls fetch with
feedsBase()/api/feeds/notifications/me (using unreadOnly and limit) to set
Authorization: `Bearer ${token}` directly in headers instead of using the
conditional spread `...(token && { Authorization: ... })`, leaving the early `if
(!token) return []` in place.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 3034b023-8e55-499b-a379-716453d08766
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (31)
CHANGELOG.mdCONTRIBUTING.mdDockerfileREADME.mdSECURITY.mdTESTING_DOUBLE_MINT_PREVENTION.mdVERSIONapp/api/tts/audio/route.tsapp/api/tts/generate/route.tsapp/auth/callback/page.tsxapp/gallery/page.tsxapp/globals.cssapp/layout.tsxapp/stories/[id]/client.tsxapp/stories/[id]/page.tsxcomponents/auth/sign-in-form.tsxcomponents/auth/sign-up-form.tsxcomponents/book-view.tsxcomponents/footer.tsxcomponents/story-engagement.tsxcomponents/wallet-connect.tsxdocker-compose.ymlhooks/use-system-health.tshooks/use-tts.tslib/api-client.tslib/feedService.tslib/feeds-client.tslib/gemini-service.tslib/royalty-service.tslib/supabase/client.tslib/supabase/server.ts
💤 Files with no reviewable changes (1)
- TESTING_DOUBLE_MINT_PREVENTION.md
| const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL || 'https://nipmqxecwnzwsmfrrkpl.supabase.co'; | ||
| const SUPABASE_SERVICE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY || ''; | ||
| const SUPABASE_ANON_KEY = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im5pcG1xeGVjd256d3NtZnJya3BsIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDEwNDY2MDIsImV4cCI6MjA1NjYyMjYwMn0.pBBXvtjEHrMTFD3GGpoK0oLxHHZEigVksm7ZBYUIzJg'; |
There was a problem hiding this comment.
Remove the hardcoded Supabase fallback values.
Committing a real project URL/anon JWT here means missing env vars will silently point this route at the shared Supabase project. It also keeps a credential-like token in source, which is exactly what Gitleaks is flagging.
Suggested fix
-const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL || 'https://nipmqxecwnzwsmfrrkpl.supabase.co';
-const SUPABASE_SERVICE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY || '';
-const SUPABASE_ANON_KEY = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || '...';
+const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL;
+const SUPABASE_SERVICE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY;
+const SUPABASE_ANON_KEY = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;- const key = SUPABASE_SERVICE_KEY || SUPABASE_ANON_KEY;
+ if (!SUPABASE_URL || !SUPABASE_ANON_KEY) {
+ return NextResponse.json({ error: 'Supabase is not configured' }, { status: 500 });
+ }🧰 Tools
🪛 Gitleaks (8.30.0)
[high] 6-6: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.
(jwt)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/api/tts/audio/route.ts` around lines 4 - 6, The file leaks hardcoded
Supabase credentials: remove the literal fallback values for SUPABASE_URL,
SUPABASE_SERVICE_KEY, and SUPABASE_ANON_KEY in app/api/tts/audio/route.ts and
instead read them directly from process.env; if any required value is missing,
fail fast by throwing a descriptive error (e.g., in module init or a helper like
getSupabaseConfig) so the app won’t silently point at a shared project—update
any usages of SUPABASE_URL, SUPABASE_SERVICE_KEY, and SUPABASE_ANON_KEY to rely
on this validated config.
| const key = SUPABASE_SERVICE_KEY || SUPABASE_ANON_KEY; | ||
| const supabase = createClient(SUPABASE_URL, key); |
There was a problem hiding this comment.
Do not use the service-role key on this public GET endpoint.
This handler has no auth check, so preferring SUPABASE_SERVICE_ROLE_KEY bypasses RLS and makes any matching story_audio row readable by anyone who can guess the query parameters. If this data is public, use the anon client; if it is not, require a session and authorize explicitly before querying.
Suggested fix
- const key = SUPABASE_SERVICE_KEY || SUPABASE_ANON_KEY;
- const supabase = createClient(SUPABASE_URL, key);
+ const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const key = SUPABASE_SERVICE_KEY || SUPABASE_ANON_KEY; | |
| const supabase = createClient(SUPABASE_URL, key); | |
| const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/api/tts/audio/route.ts` around lines 20 - 21, The current code selects
SUPABASE_SERVICE_KEY when available and passes it to createClient, which exposes
a service-role key on a public GET endpoint; change the logic in route.ts so you
never use SUPABASE_SERVICE_KEY/SUPABASE_SERVICE_ROLE_KEY for this handler:
either (A) always initialize the client with SUPABASE_ANON_KEY (use
SUPABASE_ANON_KEY directly in createClient) if the story_audio data is public,
or (B) enforce authentication and authorization in the handler (validate a
session/token, check permissions) and only then initialize or use a server-side
client for privileged reads; update references to
SUPABASE_SERVICE_KEY/SUPABASE_SERVICE_ROLE_KEY and createClient accordingly so
the service role is not used in unauthenticated requests.
| const { data, error } = await supabase.storage | ||
| .from('story-audio') | ||
| .upload(fileName, audioBuffer, { | ||
| contentType: 'audio/wav', | ||
| upsert: true, | ||
| }); | ||
|
|
||
| if (!error && data) { | ||
| const { data: urlData } = supabase.storage | ||
| .from('story-audio') | ||
| .getPublicUrl(fileName); | ||
| audioUrl = urlData.publicUrl; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify bucket names used across the codebase
echo "=== Checking bucket names in tts-related files ==="
rg -n "from\(['\"]" --type ts --type js -g '*tts*' -g '*audio*' | grep -E "(story-audio|tts-audio)"
echo ""
echo "=== All storage bucket references ==="
rg -n "\.from\(['\"]" --type ts --type js | grep -v node_modules | head -30Repository: IndieHub25/GroqTales
Length of output: 1982
🏁 Script executed:
# Check if the file exists and read the relevant lines
if [ -f "app/api/tts/generate/route.ts" ]; then
echo "=== File exists: app/api/tts/generate/route.ts ==="
wc -l app/api/tts/generate/route.ts
echo ""
echo "=== Lines 70-90 ==="
sed -n '70,90p' app/api/tts/generate/route.ts
else
echo "File not found: app/api/tts/generate/route.ts"
fi
echo ""
echo "=== Checking for tts routes in server ==="
find server -name "*tts*" -type fRepository: IndieHub25/GroqTales
Length of output: 994
🏁 Script executed:
# Search specifically for bucket references with context
echo "=== Searching for 'story-audio' ==="
rg "story-audio" -n
echo ""
echo "=== Searching for 'tts-audio' ==="
rg "tts-audio" -n
echo ""
echo "=== Searching for 'story_audio' ==="
rg "story_audio" -nRepository: IndieHub25/GroqTales
Length of output: 2327
Storage bucket name mismatch with backend.
This route uploads to bucket story-audio (lines 76, 84), but the backend server/routes/tts.js uses bucket tts-audio. Audio files uploaded via this endpoint will be stored in a different bucket than those from the backend, causing inconsistent storage organization and retrieval failures.
🐛 Proposed fix - align with backend bucket name
const fileName = `tts/${storyId}/ch${chapterIndex}_${speaker}_${languageCode}.wav`;
const { data, error } = await supabase.storage
- .from('story-audio')
+ .from('tts-audio')
.upload(fileName, audioBuffer, {
contentType: 'audio/wav',
upsert: true,
});
if (!error && data) {
const { data: urlData } = supabase.storage
- .from('story-audio')
+ .from('tts-audio')
.getPublicUrl(fileName);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const { data, error } = await supabase.storage | |
| .from('story-audio') | |
| .upload(fileName, audioBuffer, { | |
| contentType: 'audio/wav', | |
| upsert: true, | |
| }); | |
| if (!error && data) { | |
| const { data: urlData } = supabase.storage | |
| .from('story-audio') | |
| .getPublicUrl(fileName); | |
| audioUrl = urlData.publicUrl; | |
| const { data, error } = await supabase.storage | |
| .from('tts-audio') | |
| .upload(fileName, audioBuffer, { | |
| contentType: 'audio/wav', | |
| upsert: true, | |
| }); | |
| if (!error && data) { | |
| const { data: urlData } = supabase.storage | |
| .from('tts-audio') | |
| .getPublicUrl(fileName); | |
| audioUrl = urlData.publicUrl; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/api/tts/generate/route.ts` around lines 75 - 86, The code uses the wrong
Supabase bucket name 'story-audio' causing mismatch with the backend's
'tts-audio'; update both occurrences of supabase.storage.from('story-audio') in
this route (the upload call that uses fileName and audioBuffer, and the
subsequent getPublicUrl call that sets audioUrl) to use 'tts-audio' so uploaded
files and public URL lookups align with server/routes/tts.js.
| // Also store record in story_audio table if available | ||
| if (SUPABASE_SERVICE_KEY) { | ||
| try { | ||
| const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_KEY); | ||
| await supabase.from('story_audio').upsert({ | ||
| story_id: storyId, | ||
| chapter_index: chapterIndex, | ||
| speaker, | ||
| language_code: languageCode, | ||
| audio_url: audioUrl, | ||
| }, { onConflict: 'story_id,chapter_index,speaker,language_code' }).select(); | ||
| } catch { | ||
| // Non-critical — continue even if DB insert fails | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if story_audio table is defined anywhere
echo "=== Searching for story_audio table definition ==="
rg -n "story_audio" --type sql
echo ""
echo "=== Searching for CREATE TABLE story_audio ==="
rg -n "CREATE TABLE.*story_audio" --type sql
echo ""
echo "=== Checking the GET route that reads from this table ==="
cat app/api/tts/audio/route.ts 2>/dev/null || echo "File not found"Repository: IndieHub25/GroqTales
Length of output: 2348
🏁 Script executed:
#!/bin/bash
# Search for all SQL files and schema-related files
echo "=== All SQL files in repo ==="
fd -e sql
echo ""
echo "=== Search for any migration files ==="
fd -e sql -o -e migration -o -e migrations | head -20
echo ""
echo "=== Check supabase directory structure ==="
ls -la supabase/ 2>/dev/null || echo "supabase/ directory not found"
echo ""
echo "=== Check for any reference to story_audio in POST route ==="
grep -n "story_audio" app/api/tts/generate/route.ts 2>/dev/null || echo "No matches"Repository: IndieHub25/GroqTales
Length of output: 664
🏁 Script executed:
#!/bin/bash
# Read the schema.sql file to see what tables exist
echo "=== Contents of supabase/schema.sql ==="
cat supabase/schema.sqlRepository: IndieHub25/GroqTales
Length of output: 7975
Add story_audio table definition to schema.
The route upserts to story_audio (lines 97–103), but this table is not defined in supabase/schema.sql. The silent catch block masks the upsert failure, leaving the audio caching feature non-functional. The companion GET route gracefully handles the missing table by returning null, but the POST route should either upsert successfully or log the issue.
Add the table to supabase/schema.sql with appropriate columns (story_id, chapter_index, speaker, language_code, audio_url), or remove the upsert if caching is not yet planned.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/api/tts/generate/route.ts` around lines 93 - 107, The POST route in
route.ts attempts to upsert into story_audio (via
createClient(...).from('story_audio').upsert(...)) but the table is missing from
supabase/schema.sql and failures are silently swallowed; either add a
story_audio table definition to supabase/schema.sql with columns story_id
(matching your stories PK type), chapter_index (integer), speaker (text),
language_code (text), audio_url (text) and an appropriate unique constraint on
(story_id, chapter_index, speaker, language_code), or remove the upsert entirely
if caching isn’t intended; also replace the empty catch in route.ts so it logs
the caught error (include the error/exception and context) instead of swallowing
it.
| const { nonce } = await nonceRes.json(); | ||
|
|
||
| // Step 2: Sign the message | ||
| const message = `Sign this message to authenticate with Comicraft. Nonce: ${nonce}`; |
There was a problem hiding this comment.
Inconsistent branding in signature message.
The signature message references "Comicraft" while the PR is for "GroqTales". Verify this is intentional branding or update for consistency.
- const message = `Sign this message to authenticate with Comicraft. Nonce: ${nonce}`;
+ const message = `Sign this message to authenticate with GroqTales. Nonce: ${nonce}`;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const message = `Sign this message to authenticate with Comicraft. Nonce: ${nonce}`; | |
| const message = `Sign this message to authenticate with GroqTales. Nonce: ${nonce}`; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/wallet-connect.tsx` at line 134, The signature message currently
reads "Sign this message to authenticate with Comicraft..." which is
inconsistent; update the string assigned to the message constant in
components/wallet-connect.tsx (the const message declaration) to use the correct
product name "GroqTales" (or update to the agreed branding), making sure any
other occurrences of "Comicraft" in the same file or related auth code (e.g.,
functions handling signature/nonce) are updated for consistency.
| # --------------------------------------------------------------------------- | ||
| # Redis — local cache (optional; production uses Render Redis) | ||
| # --------------------------------------------------------------------------- | ||
| redis: | ||
| image: redis:7-alpine | ||
| restart: unless-stopped | ||
| ports: | ||
| - "27017:27017" | ||
| - "6379:6379" | ||
| volumes: | ||
| - mongo_data:/data/db | ||
|
|
||
| anvil: | ||
| image: ghcr.io/foundry-rs/foundry:v1.0.0 | ||
| command: anvil --host 0.0.0.0 --port 8545 | ||
| ports: | ||
| - "8545:8545" | ||
| - redis_data:/data | ||
| healthcheck: | ||
| test: ["CMD", "redis-cli", "ping"] | ||
| interval: 10s | ||
| timeout: 5s | ||
| retries: 3 | ||
|
|
||
| server: | ||
| # --------------------------------------------------------------------------- | ||
| # GroqTales App — Next.js frontend + Express backend | ||
| # --------------------------------------------------------------------------- | ||
| app: | ||
| build: | ||
| context: . | ||
| dockerfile: Dockerfile | ||
| depends_on: | ||
| - mongo | ||
| - anvil | ||
| redis: | ||
| condition: service_healthy | ||
| env_file: | ||
| - .env.local | ||
| environment: | ||
| NODE_ENV: development | ||
| MONGODB_URI: mongodb://mongo:27017/groqtales | ||
| NEXT_PUBLIC_RPC_URL: http://anvil:8545 | ||
| command: sh -c "node scripts/seed.js && npm start" | ||
| NODE_ENV: production | ||
| REDIS_URL: redis://redis:6379 | ||
|
|
||
| # ── Supabase (required) ── | ||
| # NEXT_PUBLIC_SUPABASE_URL: (set in .env.local) | ||
| # NEXT_PUBLIC_SUPABASE_ANON_KEY: (set in .env.local) | ||
| # SUPABASE_SERVICE_ROLE_KEY: (set in .env.local) | ||
|
|
||
| # ── AI Services (required) ── | ||
| # GEMINI_API_KEY: (set in .env.local) | ||
| # GROQ_API_KEY: (set in .env.local) | ||
|
|
||
| # ── Blockchain (required for NFT minting) ── | ||
| # ALCHEMY_ETH_MAINNET_HTTP_URL: (set in .env.local) | ||
| # PLATFORM_SIGNER_KEY: (set in .env.local) | ||
|
|
||
| # ── WalletConnect (optional) ── | ||
| # NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: (set in .env.local) | ||
|
|
||
| # ── TTS (optional) ── | ||
| # SARVAM_API_KEY: (set in .env.local) | ||
| ports: |
There was a problem hiding this comment.
Compose still needs a MongoDB source.
This file removes the only local MongoDB service and no longer documents a Mongo connection variable, but server/backend.js still requires mongoose and explicitly says MongoDB is needed again for vector search. As-is, docker compose up will either fail to boot the backend or leave Mongo-backed routes unusable unless you re-add Mongo here or wire an external MONGO_URI.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docker-compose.yml` around lines 17 - 67, The compose file removed the local
MongoDB service but server/backend.js still requires mongoose and a MongoDB
connection (MONGO_URI) for vector search; either re-add a mongo service to
docker-compose.yml (e.g., service name "mongo" using mongo:6, expose 27017, add
a volume like mongo_data:/data/db, and a healthcheck) and add depends_on: mongo:
condition: service_healthy to the "app" service, or explicitly document and wire
an external MONGO_URI in the env_file/.env.local and remove any hard dependency
by updating server/backend.js connection logic to read process.env.MONGO_URI;
reference the symbols "mongo" (service), "MONGO_URI" (env var) and "mongoose" /
"server/backend.js" when making the change.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@package.json`:
- Line 3: Update the repository VERSION file to match the new package.json
version by changing its contents from 2.0.2 to 2.0.3 so next.config.js resolves
NEXT_PUBLIC_VERSION correctly; locate the top-level file named VERSION (used by
next.config.js) and replace its value with "2.0.3" in this same PR to keep
package.json, VERSION, and CHANGELOG.md consistent.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: ce25dc5f-02a4-45df-b037-fd631c94bbee
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (4)
CHANGELOG.mdapp/genres/[slug]/page.tsxapp/stories/[id]/page.tsxpackage.json
| { | ||
| "name": "groqtales", | ||
| "version": "1.9.7", | ||
| "version": "2.0.3", |
There was a problem hiding this comment.
Keep VERSION in sync with this bump.
next.config.js resolves NEXT_PUBLIC_VERSION from the root VERSION file before package.json, so changing only this field will still bake 2.0.2 into the UI/footer on the next build. Please update VERSION to 2.0.3 in the same PR or the app will advertise a different release than package.json and CHANGELOG.md.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@package.json` at line 3, Update the repository VERSION file to match the new
package.json version by changing its contents from 2.0.2 to 2.0.3 so
next.config.js resolves NEXT_PUBLIC_VERSION correctly; locate the top-level file
named VERSION (used by next.config.js) and replace its value with "2.0.3" in
this same PR to keep package.json, VERSION, and CHANGELOG.md consistent.
…nre and story pages, and remove TTS API endpoints.
There was a problem hiding this comment.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
README.md (1)
253-258:⚠️ Potential issue | 🟠 MajorThe Docker env table no longer matches
docker-compose.yml.The compose file now expects external Supabase credentials from
.env.local; it does not provide a localsupabaseservice or auto-populateNEXT_PUBLIC_SUPABASE_URL=http://supabase:54321. Following this table will send contributors to a host that does not exist in the current stack.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@README.md` around lines 253 - 258, The Docker env table is outdated: update the entries for NEXT_PUBLIC_SUPABASE_URL (and related Supabase vars like NEXT_PUBLIC_SUPABASE_ANON_KEY) to reflect that docker-compose no longer provides a local `supabase` service and instead reads credentials from .env.local; either remove the `http://supabase:54321` default, mark these variables as "must be set in .env.local / external Supabase" or provide the new expected default values, and add a short note telling contributors to copy .env.local.example and populate NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY before running docker-compose.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/gallery/page.tsx`:
- Around line 102-105: The onClick handler on the Button uses
window.open(post.file_url, '_blank') which leaves the opened page access to
window.opener; update the handler in the Button (the component rendering Read
File) so the external tab is opened safely either by using
window.open(post.file_url, '_blank', 'noopener,noreferrer') or by capturing the
returned window (const w = window.open(...)) and immediately setting w.opener =
null; ensure the change is applied where post.file_url and the Button's onClick
are defined so external links are opened with noopener/noreferrer protection.
In `@components/book-view.tsx`:
- Around line 295-304: The keyboard handler in the useEffect (handler used with
goNext/goPrev and compact) should skip handling when focus is on interactive
elements; modify handler to early-return if event.target (or
document.activeElement) is an input, textarea, select, [contentEditable=true], a
button or anchor, or any element with role="button" (and also when any ancestor
is contentEditable or a form control) so Arrow keys and Space aren’t intercepted
while typing or when buttons/links are focused; keep the existing compact check
and only call preventDefault() and goNext/goPrev when the focus is not on these
interactive elements.
- Around line 352-359: Add keyframe definitions and animation utilities for the
missing classes used in the component: define keyframes for "page-flip-right"
and "page-flip-left" under theme.extend.keyframes in tailwind.config.js, then
add corresponding animation entries "page-flip-right" and "page-flip-left" under
theme.extend.animation that reference those keyframes (set duration,
timing-function, and fill-mode as needed). Ensure the names match the classes
used in the JSX: animate-page-flip-right and animate-page-flip-left so Tailwind
generates those utilities.
In `@components/story-engagement.tsx`:
- Around line 59-98: The UI queries reference missing Supabase tables
story_votes, story_comments, and saved_stories (used in functions that call
supabase.from('story_votes'), supabase.from('story_comments'), and
supabase.from('saved_stories') and rely on fields like vote, story_id, user_id),
so add DDL to supabase/schema.sql to create these tables: story_votes with
columns story_id (fk), user_id (fk), vote (integer) and appropriate
primary/unique key or index on (story_id,user_id); story_comments with id,
story_id (fk), user_id, body, created_at (for counting and listing); and
saved_stories with story_id, user_id and a primary/unique constraint on
(story_id,user_id); include foreign key constraints to the stories and users
tables and indexes needed for .eq queries and exact counts so fresh environments
don’t error on first render.
- Around line 44-49: Replace the one-time supabase.auth.getSession() in
story-engagement.tsx with the same auth bootstrap used elsewhere: on mount check
localStorage for the persisted access token key (e.g. "accessToken") and, if
present, initialize or restore the Supabase session so the user session is
available, and also subscribe to Supabase auth state changes via
supabase.auth.onAuthStateChange to update setUserId whenever the session
changes; ensure useEffect cleans up the subscription. This will make useEffect,
supabase.auth.getSession, setUserId, and the auth subscription behave
consistently with components like sign-in-form.tsx and wallet-connect.tsx.
- Around line 171-177: After inserting a comment you reload comments but never
update the commentCount state, so the badge stays stale; fix by updating
commentCount immediately after the insert — e.g., in the same block where you
call setCommentText('') and await loadComments(), call setCommentCount(count =>
count + 1) (or re-fetch the total via your existing loadData/loadComments flow
and setCommentCount(returnedTotal)) so the UI badge reflects the new total;
adjust in the function that performs the insert (the block calling
supabase.from('story_comments').insert, setCommentText, and loadComments).
- Around line 170-199: The Supabase mutations in handle comment and handleSave
(the supabase.from(...).insert(...) call in the comment handler and the
.delete()/ .insert() calls in handleSave) can fail silently because Supabase v2
returns {data,error} by default; add .throwOnError() to each mutation so
failures throw and are caught by the existing try/catch, and move UI state
changes (setCommentText(''), loadComments(), and setIsSaved(...)) so they only
run after the awaited mutation succeeds; keep setCommentLoading(false) in
finally or after the catch so loading state is always cleared.
In `@components/wallet-connect.tsx`:
- Around line 105-145: The code hardcodes chain id 1 when calling
setWalletConnection, causing incorrect network display and explorer links; use
the provider.chainId value returned by the EthereumProvider instead. Update the
flow after successful auth to read provider.chainId (or
provider.chain?.chainId/parse it to a number if needed) and pass that into
setWalletConnection(selectedAccount, <connectedChainId>) instead of 1, and
ensure viewOnExplorer and any chain-to-explorer URL logic consume the same
provider.chainId so the UI and links reflect the actual connected network.
In `@docker-compose.yml`:
- Around line 21-48: lib/redis.ts currently always exports the in-memory
MockRedis which ignores REDIS_URL; update it so that when process.env.REDIS_URL
(or equivalent env var) is set it creates and exports a real Redis client
(matching the client type used by the app) and only falls back to MockRedis when
REDIS_URL is absent or the Redis client fails to connect; locate the file and
the exported symbol (e.g., MockRedis, redisClient, or getRedisClient) and modify
the initialization logic to read process.env.REDIS_URL, instantiate a real Redis
connection when present, attach proper error/ready handlers, and export that
instance so server/backend.js reports and uses the actual Redis provided by
docker-compose.
---
Outside diff comments:
In `@README.md`:
- Around line 253-258: The Docker env table is outdated: update the entries for
NEXT_PUBLIC_SUPABASE_URL (and related Supabase vars like
NEXT_PUBLIC_SUPABASE_ANON_KEY) to reflect that docker-compose no longer provides
a local `supabase` service and instead reads credentials from .env.local; either
remove the `http://supabase:54321` default, mark these variables as "must be set
in .env.local / external Supabase" or provide the new expected default values,
and add a short note telling contributors to copy .env.local.example and
populate NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY before
running docker-compose.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: a801fcc6-9179-403d-a40a-b3f86b851090
📒 Files selected for processing (10)
README.mdapp/gallery/page.tsxcomponents/book-view.tsxcomponents/providers/web3-provider.tsxcomponents/story-engagement.tsxcomponents/wallet-connect.tsxdocker-compose.ymllib/supabase/client.tslib/supabase/server.tspackage.json
| useEffect(() => { | ||
| const handler = (e: KeyboardEvent) => { | ||
| // Don't handle keys if in compact mode to avoid side-effects | ||
| if (compact) return; | ||
| if (e.key === 'ArrowRight' || e.key === ' ') { e.preventDefault(); goNext(); } | ||
| if (e.key === 'ArrowLeft') { e.preventDefault(); goPrev(); } | ||
| }; | ||
| window.addEventListener('keydown', handler); | ||
| return () => window.removeEventListener('keydown', handler); | ||
| }, [goNext, goPrev, compact]); |
There was a problem hiding this comment.
Keyboard navigation may interfere with form inputs and buttons.
The handler unconditionally captures ArrowRight, ArrowLeft, and Space keys with preventDefault(). This will:
- Prevent typing arrow keys in text inputs on the page
- Block Space from activating focused buttons/links (accessibility issue)
- Interfere with any other interactive elements expecting these keys
Add a guard to skip handling when focus is on an interactive element.
🛡️ Proposed fix to exclude interactive elements
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (compact) return;
+ const tag = (e.target as HTMLElement)?.tagName;
+ if (tag === 'INPUT' || tag === 'TEXTAREA' || (e.target as HTMLElement)?.isContentEditable) {
+ return;
+ }
if (e.key === 'ArrowRight' || e.key === ' ') { e.preventDefault(); goNext(); }
if (e.key === 'ArrowLeft') { e.preventDefault(); goPrev(); }
};
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, [goNext, goPrev, compact]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/book-view.tsx` around lines 295 - 304, The keyboard handler in the
useEffect (handler used with goNext/goPrev and compact) should skip handling
when focus is on interactive elements; modify handler to early-return if
event.target (or document.activeElement) is an input, textarea, select,
[contentEditable=true], a button or anchor, or any element with role="button"
(and also when any ancestor is contentEditable or a form control) so Arrow keys
and Space aren’t intercepted while typing or when buttons/links are focused;
keep the existing compact check and only call preventDefault() and goNext/goPrev
when the focus is not on these interactive elements.
…e runtime from profile and genre routes, and update build scripts for compatibility
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
__tests__/bugfix/cloudflare-edge-runtime-profile-route.test.ts (1)
46-84:⚠️ Potential issue | 🔴 CriticalCritical: Missing closing brace causes syntax error.
The test starting at line 46 (
test('Property 1: Cloudflare build succeeds...') is missing its closing brace and parenthesis. Line 72 ends withexpect(routeContent).not.toContain("runtime = 'edge'");but the test block is never closed before the next test starts on line 77.🐛 Proposed fix to close the test block
// Confirm the profile page source no longer declares the Edge runtime const routeContent = fs.readFileSync(profileRoutePath, 'utf-8'); expect(routeContent).not.toContain("runtime = 'edge'"); + }); /** * Additional verification: Check that the profile route file exists * and is a valid dynamic route */ test('Profile route file exists and is a dynamic route', () => {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@__tests__/bugfix/cloudflare-edge-runtime-profile-route.test.ts` around lines 46 - 84, The first test block test('Property 1: Cloudflare build succeeds and profile route has no Edge runtime') is missing its closing brace/parenthesis, which causes a syntax error and nests the subsequent test; close that test immediately after the expect(routeContent).not.toContain("runtime = 'edge'"); line by adding the missing "});" so the following test('Profile route file exists and is a dynamic route') is a separate top-level test; verify references to profileRoutePath and routeContent remain unchanged.
🧹 Nitpick comments (2)
package.json (1)
119-119: Consider removing unusedcross-envdependency.
cross-envis still listed in dependencies (line 119) but is no longer used in any npm scripts after this change. Consider removing it to reduce bundle size and dependency footprint.♻️ Proposed change
- "cross-env": "^10.1.0",🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@package.json` at line 119, Remove the unused cross-env dependency from package.json: locate the "cross-env" entry in the dependencies array and delete that key-value pair, then run the package manager (npm install or yarn install) to update lockfile; also scan package.json scripts to confirm no remaining references to "cross-env" (and update any scripts to use plain env vars or platform-safe alternatives if needed).app/profile/[slug]/page.tsx (1)
6-19: Clarify comment about 404 behavior withdynamicParams = true.The comment on line 10 states that "all other slugs will simply 404 in the static export," but
dynamicParams = trueon line 6 typically allows non-pre-rendered params to be handled at runtime. In a static export (output: 'export'), the behavior depends on deployment configuration:
- Direct URL access to
/profile/some-usernamemay 404 on Cloudflare Pages unless a fallback is configured- Client-side navigation from within the app should still work via the
StoryClientfetching data dynamicallyConsider updating the comment to be more precise about when 404s occur (direct access vs. client-side navigation), or verify this matches the intended behavior with Cloudflare Pages' SPA fallback configuration.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/profile/`[slug]/page.tsx around lines 6 - 19, The comment above generateStaticParams is inaccurate about 404 behavior when dynamicParams = true; update the comment next to dynamicParams and generateStaticParams to clarify that with output: 'export' non-pre-rendered slugs may 404 on direct URL access (depending on hosting/SPA-fallback like Cloudflare Pages) while client-side navigation can still resolve dynamic usernames via the frontend router/StoryClient; adjust the wording to state the difference (direct access vs client-side navigation) and add a note to verify the deployment’s SPA fallback configuration if you expect direct-access handling for dynamic slugs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/stories/`[id]/page.tsx:
- Around line 7-11: The current export dynamicParams = false (in the same module
that defines generateStaticParams) prevents any non-prerendered story IDs from
resolving and causes hard 404s; to fix, either allow dynamic routing by changing
export const dynamicParams = true so pages like /stories/{id} behave like the
profile route and resolve at request time, or keep dynamicParams = false but add
an SPA fallback rule in your static redirects configuration (public/_redirects)
that routes non-pre-rendered /stories/* requests to your client entry so the
client router can handle them; update whichever approach you choose and ensure
generateStaticParams remains as-is for the pre-rendered default.
---
Outside diff comments:
In `@__tests__/bugfix/cloudflare-edge-runtime-profile-route.test.ts`:
- Around line 46-84: The first test block test('Property 1: Cloudflare build
succeeds and profile route has no Edge runtime') is missing its closing
brace/parenthesis, which causes a syntax error and nests the subsequent test;
close that test immediately after the
expect(routeContent).not.toContain("runtime = 'edge'"); line by adding the
missing "});" so the following test('Profile route file exists and is a dynamic
route') is a separate top-level test; verify references to profileRoutePath and
routeContent remain unchanged.
---
Nitpick comments:
In `@app/profile/`[slug]/page.tsx:
- Around line 6-19: The comment above generateStaticParams is inaccurate about
404 behavior when dynamicParams = true; update the comment next to dynamicParams
and generateStaticParams to clarify that with output: 'export' non-pre-rendered
slugs may 404 on direct URL access (depending on hosting/SPA-fallback like
Cloudflare Pages) while client-side navigation can still resolve dynamic
usernames via the frontend router/StoryClient; adjust the wording to state the
difference (direct access vs client-side navigation) and add a note to verify
the deployment’s SPA fallback configuration if you expect direct-access handling
for dynamic slugs.
In `@package.json`:
- Line 119: Remove the unused cross-env dependency from package.json: locate the
"cross-env" entry in the dependencies array and delete that key-value pair, then
run the package manager (npm install or yarn install) to update lockfile; also
scan package.json scripts to confirm no remaining references to "cross-env" (and
update any scripts to use plain env vars or platform-safe alternatives if
needed).
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: d710f0d6-a282-499d-b29f-48553155c726
📒 Files selected for processing (7)
__tests__/bugfix/cloudflare-edge-runtime-profile-route.test.tsapp/genres/[slug]/page.tsxapp/profile/[slug]/page.tsxapp/stories/[id]/page.tsxnext.config.jspackage.jsonwrangler.toml
💤 Files with no reviewable changes (1)
- app/genres/[slug]/page.tsx
…nnection logic, and implement noop Supabase clients for static builds. Add story_votes, story_comments, and saved_stories tables to Supabase schema, and update Tailwind configuration for new animations.
…, update dynamicParams for SPA fallback, and remove unused dependencies from package.json and package-lock.json.
Implement local TTS generation endpoints, enhance book view navigation, and update Supabase client initialization for SSR.
Summary by CodeRabbit
New Features
Bug Fixes
Documentation