feat: public feed enhancement, responsive styling optimizations#13
feat: public feed enhancement, responsive styling optimizations#13Simplereally merged 17 commits intomainfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Caution Review failedThe pull request is closed. 📝 WalkthroughWalkthroughAdds a public community feed with server-cached pagination and server actions; implements prompt- and vision-based sensitive-content analysis and moderation fields; integrates Groq/OpenRouter/Cerebras for vision and prompt inference; adds server cache/invalidation, media preview tooling, many UI updates, tests, and docs. Changes
Sequence Diagram(s)sequenceDiagram
participant Browser
participant AppServer as App Server (app/feed/[type]/page.tsx)
participant NextCache as Next Cache (app/_server/cache)
participant ServerActions as Server Actions (app/_server/actions)
participant Convex as Convex Backend
participant Analytics as Analytics (lib/analytics)
Browser->>AppServer: GET /feed/public
AppServer->>NextCache: getPublicFeedPageCached(cursor=null, filterPreference)
NextCache->>Convex: api.generatedImages.getPublicFeed (if cache miss)
Convex-->>NextCache: paginated PublicFeedImage page
NextCache-->>AppServer: initialPage
AppServer-->>Browser: render HTML + initialPage + metadata (JSON-LD)
Browser->>ServerActions: Hydrated FeedClient loadMore / user actions
ServerActions->>NextCache: loadPublicFeedPage(cursor)
NextCache->>Convex: fetch page (if miss)
Convex-->>NextCache: page
NextCache-->>ServerActions: page
ServerActions-->>Browser: page (server action response)
Browser->>Analytics: trackFeedView / trackCtaView / trackImageClick
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
📜 Recent review detailsConfiguration used: defaults Review profile: CHILL Plan: Pro 📒 Files selected for processing (10)
✏️ Tip: You can disable this entire section by setting 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 |
There was a problem hiding this comment.
Actionable comments posted: 14
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
components/studio/layout/studio-shell.tsx (1)
220-236: Avoidanytype - use proper union type instead.The
paramsvariable is typed asany, which violates the TypeScript coding guidelines. Since this supports both image and video generation, use a union type or the existing types.Proposed fix
- const params: any = { + const params: ImageGenerationParams | VideoGenerationParams = { prompt, negativePrompt: negativePrompt || undefined, model: generationSettings.model, width: generationSettings.width, height: generationSettings.height, seed: effectiveSeed, enhance: generationSettings.options.enhance, private: generationSettings.options.private, safe: generationSettings.options.safe, image: generationSettings.referenceImage, // Video-specific parameters duration: generationSettings.videoSettings.duration, audio: generationSettings.videoSettings.audio, aspectRatio: generationSettings.aspectRatio, lastFrameImage: generationSettings.videoReferenceImages.lastFrame, }If the union type causes issues due to incompatible properties, consider using
satisfiesor creating a shared base type. As per coding guidelines, avoidascasts and prefersatisfies, generics, or runtime validation.components/ui/button.tsx (1)
50-50: Potential issue: Decorative overlays may breakasChildfunctionality.When
asChild=true, Radix'sSlotcomponent expects exactly one child element to merge props onto. The overlay spans (Lines 61-73) add extra children, which could causeSlotto throw or behave unexpectedly.🔧 Proposed fix: Conditionally render overlays only when not using asChild
<Comp data-slot="button" data-variant={variant} data-size={size} className={cn(buttonVariants({ variant, size, className }))} {...props} > - {/* Layer 1: Persistent hover glow - fades in and stays while hovering */} - {variant === "default" && ( + {/* Layer 1: Persistent hover glow - fades in and stays while hovering */} + {variant === "default" && !asChild && ( <span className="pointer-events-none absolute inset-0 bg-gradient-to-b from-white/15 via-white/5 to-transparent opacity-0 group-hover/btn:opacity-100 transition-opacity duration-300" aria-hidden="true" /> )} {/* Layer 2: Sweep sheen - rapid diagonal animation on hover entry */} - {variant === "default" && ( + {variant === "default" && !asChild && ( <span className="pointer-events-none absolute inset-0 bg-gradient-to-r from-transparent via-white/25 to-transparent -translate-x-full skew-x-[-15deg] group-hover/btn:animate-button-sheen" aria-hidden="true" /> )} {children} </Comp>Also applies to: 59-75
🤖 Fix all issues with AI agents
In @.agent/skills/adding-route/SKILL.md:
- Around line 43-66: Update the metadata.title value in the page template to use
the correct brand name instead of "Pixelstream": locate the exported const
metadata and replace the title string " [Page Title] | Pixelstream" with the
correct brand (e.g., " [Page Title] | bloomstudio") so generated pages use the
proper site name; ensure you update any instances in the template for
consistency (the metadata constant and any example title usage in the file where
[RouteName]Page is defined).
In @.agent/skills/brainstorming-ui-and-design/SKILL.md:
- Line 10: In the line that reads "Refer to `.agent/skills/styling-ui/SKILL.md`
for repo specific tailwind/css rules.", change "repo specific" to the hyphenated
compound adjective "repo-specific" so the sentence reads "Refer to
`.agent/skills/styling-ui/SKILL.md` for repo-specific tailwind/css rules."
In @.agent/skills/developing-nextjs/SKILL.md:
- Around line 9-11: The document inconsistently references the Next.js version;
ensure both occurrences use the same version string by updating the second
instance to match the intended version (e.g., change the "Next.js 16 App Router"
mention on line 11 to the canonical phrase used elsewhere), i.e., find the
phrase "Next.js 16 App Router" and make all references uniform across the file
(update line 11 to match).
In `@components/gallery/feed-cta.test.tsx`:
- Around line 86-96: The test title for the FeedCta test is misleading: update
the it() description that currently reads "shows CTA after scrolling 600px" to
reflect the actual threshold used in the test and component (scrollY > 1200);
locate the test for FeedCta (the it block that sets window.scrollY = 1201 and
fires a scroll event) and change its description to something like "shows CTA
after scrolling past 1200px" so the name matches the assertion.
In `@components/gallery/feed-cta.tsx`:
- Around line 30-32: Update the inline comment inside the handleScroll function
so it matches the actual threshold used: change the "Show after scrolling 600px
(roughly 4-6 images viewed)" comment to reference 1200px, e.g., "Show after
scrolling 1200px", and keep any explanatory parenthetical text accurate relative
to the new value; the condition using window.scrollY > 1200 and the isVisible
variable should remain unchanged.
- Around line 27-51: The effect has a stale closure over isVisible used in
handleScroll and the timer, causing duplicate trackCtaView calls; fix by
introducing a mutable ref (e.g., isVisibleRef) that mirrors isVisible and update
it whenever you call setIsVisible, then read isVisibleRef.current inside
handleScroll and the timer callback instead of the closed-over isVisible,
keeping the effect dependency array as [isLoaded, isSignedIn] so it doesn't
re-run unnecessarily; ensure you still clear the timer and remove the scroll
listener in the cleanup.
In `@components/landing/competitor-comparison.tsx`:
- Line 22: Fix the typo in the user-facing string by changing the value of the
waitTimes property ("No artifical wait times") to the correct spelling "No
artificial wait times" wherever waitTimes is defined or exported (e.g., the
waitTimes property in the competitor comparison component).
In `@components/pricing/model-value-showcase.tsx`:
- Line 369: Remove the stray space in the closing section tag in
components/pricing/model-value-showcase.tsx by changing the malformed "</section
>" to the correct "</section>" (locate the closing tag near the end of the
ModelValueShowcase JSX block) so the JSX is syntactically clean.
In `@convex/lib/videoPreview.ts`:
- Around line 202-208: The returned width/height in the VideoPreviewResult are
hardcoded approximations (1280 × min(config.maxHeight, 720)) instead of the
actual ffmpeg output; update the implementation that returns { buffer:
previewBuffer, compressionRatio, width, height } to either (A) probe the encoded
previewBuffer with ffprobe/ffprobeSync to extract exact width/height and return
those real values, or (B) rename the fields to
approximateWidth/approximateHeight and update the VideoPreviewResult interface
and all consumers to reflect they are estimates (or remove the fields entirely
if unused). Ensure references to previewBuffer, compressionRatio and the
VideoPreviewResult interface are updated accordingly.
In `@lib/config/models.ts`:
- Line 474: The description string in models.ts ("description: \"GPT Image 1,
powered image generation with fixed sizes\"") has an awkward comma; update the
description value for the relevant model entry (the description field) to remove
the comma and produce a cleaner phrase such as "GPT Image 1 powered image
generation with fixed sizes" (or another concise rewording) so the description
reads smoothly.
In `@lib/seo-config.ts`:
- Line 722: Replace the inconsistent product name in the SEO copy by changing
"NanoBanana Pro" to "Nano Banana Pro" in the string found in lib/seo-config.ts
(the sentence referencing "Our pro-tier models like ...") so it matches the
naming used in pricing-data.tsx (line 121) across the codebase.
- Line 121: The model name is inconsistent: change all occurrences of
"NanoBanana Pro" to the canonical "Nano Banana Pro" so names match
app/pricing/pricing-data.tsx; update the sentence in seo-config.ts that
currently contains "NanoBanana Pro" (the image-generation description string)
and also the other occurrences mentioned (the entries referenced at lines 167
and 722) to "Nano Banana Pro", verifying any other usages in related strings or
constants are updated to the same form.
In `@todo/product-hunt-launch-plan.md`:
- Line 64: Fix the malformed checklist item by correcting the bold syntax and
spacing: remove the extra space after the opening bold marker so "** engage:**"
becomes "**engage:**" (or use "**engage**:"), and ensure the checklist text
reads "Log in to PH daily. Upvote, comment on other launches. (New accounts with
0 activity get deprioritized)." so the list item is a valid markdown checklist
(e.g., "* [ ] **engage:** Log in to PH daily...").
🧹 Nitpick comments (27)
components/studio/layout/studio-layout.tsx (1)
35-40: Consider removing legacy props or adding a deprecation notice.These props are marked as "kept for compatibility but unused." If they're truly unused, consider:
- Removing them entirely if no external consumers depend on them
- Adding a
@deprecatedJSDoc tag to signal intent to removeThis prevents confusion and reduces API surface area.
components/solutions/solution-steps.tsx (1)
40-43: Consider clearing the timer array after cleanup.The cleanup clears all timers, but
timersRef.currentretains stale IDs. Ifstepschanges and the effect re-runs, new timer IDs are appended to an array still containing old (cleared) IDs. This is a minor inefficiency—not a bug—but clearing the array ensures clean state.♻️ Suggested improvement
return () => { observer.disconnect() timersRef.current.forEach(clearTimeout) + timersRef.current = [] }vercel.json (1)
21-29: The/_next/staticcaching rule is redundant.Vercel and Next.js automatically apply aggressive caching to
/_next/static/assets since these files have content hashes in their filenames. This explicit rule won't cause harm but adds unnecessary configuration..agent/skills/developing-nextjs/SKILL.md (1)
32-45: Consider adding a language identifier to the code block.The decision tree diagram doesn't have a language specified. While this is a minor documentation lint issue, adding
textorplaintextwould satisfy markdownlint.📝 Suggested fix
-``` +```text Need data from Convex?.agent/skills/creating-client-component/SKILL.md (1)
20-28: Add language specifier to the fenced code block.The decision tree code block is missing a language specifier, which triggers a markdown lint warning. Consider adding a language identifier for consistency.
📝 Proposed fix
-``` +```text Need any of these? ├─ useState, useEffect, useRef → Client ├─ onClick, onChange, onSubmit → Client ├─ useRouter, usePathname → Client ├─ useQuery, useMutation (Convex) → Client ├─ Browser APIs (localStorage, window) → Client └─ None of above → Server Component (no 'use client') -``` +```.agent/skills/writing-convex-functions/SKILL.md (1)
48-89: Good query templates with minor comment inconsistency.The query templates are well-structured and demonstrate proper patterns. However, the file comment on line 51 says
// convex/users.tsbut thelistByStatusandlistMinequeries (lines 64-88) operate on the"generatedImages"table, which would typically be in a different file.Consider updating the comment or splitting examples to match the expected file locations.
.agent/skills/writing-server-actions/SKILL.md (1)
114-133: Inconsistent error handling pattern between templates.The Convex template throws an error on auth failure (line 116:
throw new Error('Unauthorized')), while the Basic template returns a typed result (line 75-76:return { success: false, error: 'Unauthorized' }). For consistency with the guardrails section ("Return typed results — not void or any") and to provide a uniform pattern, consider using the return-based approach in both templates.♻️ Suggested fix for consistency
export async function createPrompt(formData: FormData) { const { userId } = await auth(); - if (!userId) throw new Error('Unauthorized'); + if (!userId) { + return { success: false, error: 'Unauthorized' }; + }hooks/use-mobile.ts (2)
13-15: Thetypeof windowcheck ingetSnapshotis unnecessary.
useSyncExternalStoreonly callsgetSnapshoton the client; it usesgetServerSnapshotduring SSR. The check is harmless but adds dead code.♻️ Suggested simplification
const getSnapshot = React.useCallback(() => { - return typeof window !== "undefined" ? window.innerWidth < MOBILE_BREAKPOINT : false + return window.innerWidth < MOBILE_BREAKPOINT }, [])
21-29: Consider removing the redundantresizelistener.
MediaQueryListalready fires when the viewport crosses the breakpoint threshold. Theresizelistener adds extra callbacks on every pixel change without providing additional value for boolean breakpoint detection.♻️ Leaner subscription using only MediaQueryList
const subscribe = React.useCallback((callback: () => void) => { const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) mql.addEventListener("change", callback) - window.addEventListener("resize", callback) - return () => { - mql.removeEventListener("change", callback) - window.removeEventListener("resize", callback) - } + return () => mql.removeEventListener("change", callback) }, [])components/landing/living-strip.tsx (1)
73-80: Consider usinguseSyncExternalStorefor reduced motion detection.This PR introduces
useSyncExternalStoreinhooks/use-mobile.tsfor hydration-safe media query detection. The same pattern could be applied here for consistency and to avoid the initial render with default state.The current
useEffectapproach is valid for browser-only side effects, but extracting this into a reusableusePrefersReducedMotionhook usinguseSyncExternalStorewould align with the pattern established in this PR.components/pricing/model-value-showcase.tsx (1)
353-365: Use Next.jsLinkfor internal navigation.The anchor tags for
#comparison-tableand/#modelsshould use Next.jsLinkcomponent for client-side navigation consistency. Hash links still benefit from Link's prefetching and routing integration.♻️ Suggested fix
+import Link from "next/link" // ... <div className="mt-6 flex flex-wrap justify-center gap-6 text-xs text-muted-foreground"> - <a + <Link href="#comparison-table" className="hover:text-foreground transition-colors underline underline-offset-4 decoration-border hover:decoration-foreground" > Compare features - </a> + </Link> <span className="text-border">·</span> - <a + <Link href="/#models" className="hover:text-foreground transition-colors underline underline-offset-4 decoration-border hover:decoration-foreground" > Explore all models - </a> + </Link> </div>components/layout/footer.tsx (1)
3-3: Remove unusedSparklesimport.The
Sparklesicon is imported but not used anywhere in the component. This should be cleaned up.🧹 Proposed fix
-import { ArrowRight, Sparkles } from "lucide-react" +import { ArrowRight } from "lucide-react"components/images/image-lightbox.tsx (2)
219-240: Minor: Redundant fallback in image src.
fullResUrlis already computed asdisplayImage?.originalUrl || displayImage?.urlon line 94, so the fallback|| displayImage.urlon line 220 is redundant. However, this is defensive coding and harmless.🧹 Optional simplification
<NextImage - src={fullResUrl || displayImage.url} + src={fullResUrl!} alt={displayImage.prompt || "Generated image"}Note: If you prefer keeping the defensive fallback for clarity, that's fine too.
361-381: Consider consistent disabled state for unauthenticated users.The signed-in button (line 352) is disabled when
isLoadingDetails || !displayImage.prompt, but the unauthenticated version doesn't have this check. While the button redirects to sign-in anyway, showing a disabled-looking button during loading would be more consistent.💅 Optional: Add loading/disabled state
<Link href="/sign-in" onClick={(e) => e.stopPropagation()}> <Button variant="ghost" size="icon" - className="h-10 w-10 mb-1 rounded-full bg-white/10 hover:bg-white/20 text-white/60 hover:text-white border border-white/10 backdrop-blur-md transition-all shrink-0 hover:scale-105 active:scale-95 shadow-lg" + className={cn( + "h-10 w-10 mb-1 rounded-full bg-white/10 hover:bg-white/20 text-white/60 hover:text-white border border-white/10 backdrop-blur-md transition-all shrink-0 hover:scale-105 active:scale-95 shadow-lg", + (isLoadingDetails || !displayImage.prompt) && "opacity-50 pointer-events-none" + )} >components/ui/sidebar.tsx (1)
215-251: Consider adding Escape key handling for mobile sidebar.The previous Sheet-based implementation likely included keyboard handling (Escape to close). The new custom implementation has the overlay click-to-close but may be missing the Escape key listener for accessibility.
The existing keyboard shortcut handler (lines 129-142) uses
Ctrl/Cmd + Bto toggle, but standard modal/drawer UX also expects plainEscapeto close when open. Consider adding:♿ Suggested Escape key handler
// Adds a keyboard shortcut to toggle the sidebar. React.useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { + // Close mobile sidebar on Escape + if (event.key === "Escape" && isMobile && openMobile) { + event.preventDefault() + setOpenMobile(false) + return + } if ( event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey) ) { event.preventDefault() toggleSidebar() } } window.addEventListener("keydown", handleKeyDown) return () => window.removeEventListener("keydown", handleKeyDown) - }, [toggleSidebar]) + }, [toggleSidebar, isMobile, openMobile, setOpenMobile])convex/lib/videoPreview.ts (2)
135-136: Input file extension hardcoded to.mp4.The input file is always named with
.mp4extension regardless of the actual video format. While ffmpeg typically handles this via content detection, some edge cases with container-specific features might behave unexpectedly.If this becomes an issue, consider accepting a
mimeTypeorextensionparameter:export async function generateVideoPreview( videoBuffer: Buffer, - config: VideoPreviewConfig = PREVIEW_CONFIG + config: VideoPreviewConfig = PREVIEW_CONFIG, + inputExtension: string = ".mp4" ): Promise<VideoPreviewResult | null> { ... - const inputPath = join(tempDir, `input-${randomUUID()}.mp4`) + const inputPath = join(tempDir, `input-${randomUUID()}${inputExtension}`)
148-188: Consider adding an explicit timeout for the ffmpeg encoding process.The
-t 60flag limits output duration, but encoding time itself has no protection. With Convex's 10-minute action timeout as a fallback, this is less critical—but an explicit timeout (e.g., 5 minutes) would improve predictability and fail faster if ffmpeg stalls rather than hanging until Convex's limit. Sinceveryfastpreset is already in use, most videos should encode quickly, making an additional timeout layer a safety improvement.components/ui/image-card.tsx (1)
210-244: Consider usingonLoadedDatainstead ofonCanPlayfor more reliable loading detection.
onCanPlayfires when enough data is available to start playback, but the video may still show a black frame briefly.onLoadedDataensures at least the first frame is available, providing a smoother visual transition.♻️ Optional improvement
<video ref={videoRef} src={image.url} muted loop playsInline preload="metadata" - onCanPlay={handleLoad} + onLoadedData={handleLoad} className={cn(app/page.tsx (1)
1-1: Unused import:CommunitySectionis imported but commented out in JSX.The
CommunitySectionimport on line 1 is not used since line 68 has it commented out. If this is intentional staging for a future enable, consider adding a comment explaining the intent, or remove both until ready.♻️ If not needed yet, consider commenting the import too
-import { CommunitySection } from "@/components/landing/community-section"; +// import { CommunitySection } from "@/components/landing/community-section";Also applies to: 68-68
components/gallery/feed-cta.test.tsx (1)
40-42: MoveafterEachinside thedescribeblock for consistency.The
afterEachhook is defined at the module level whilebeforeEachwithvi.useFakeTimers()is inside thedescribeblock. For better scoping and consistency, moveafterEachinside thedescribeblock alongsidebeforeEach.♻️ Suggested refactor
describe("FeedCta", () => { beforeEach(() => { vi.clearAllMocks() vi.useFakeTimers() }) + afterEach(() => { + vi.useRealTimers() + }) + describe("authenticated user", () => {And remove lines 40-42 from the module level.
.agent/skills/testing-functions/SKILL.md (1)
143-153: Minor inconsistency in mock implementation example.Line 147's mock for the retry success case returns
{ data: "test" }directly fromjson()instead of wrapping it inPromise.resolve()like line 136 does. While this works in most cases due to promise flattening, it's inconsistent with the pattern shown earlier.📝 Suggested fix for consistency
.mockResolvedValueOnce({ ok: false, status: 500 }) - .mockResolvedValueOnce({ ok: true, json: () => ({ data: "test" }) }); + .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ data: "test" }) });components/gallery/feed-tabs.tsx (1)
18-28: Consider handling the loading state for authentication.When
isSignedInisundefined(during auth loading), the filter at line 21 will treat it as falsy, showing only the "public" tab. This may cause a brief flash where authenticated users momentarily see only one tab before auth loads.Consider using
isLoadedfromuseAuth()to either show a loading state or default to showing all tabs until auth is confirmed:♻️ Suggested refinement
export function FeedTabs({ activeType }: FeedTabsProps) { - const { isSignedIn } = useAuth() + const { isSignedIn, isLoaded } = useAuth() // Filter tabs: show all for authenticated, only "public" for guests - const visibleTabs = isSignedIn + // Show all tabs while loading to prevent flash for authenticated users + const visibleTabs = !isLoaded || isSignedIn ? FEED_TYPES : FEED_TYPES.filter((type) => type === "public")convex/generatedImages.ts (2)
551-556: Avoidascast; the type already satisfiesEnrichedImage.The mapped object structurally matches
EnrichedImage, so the cast is unnecessary. Per coding guidelines, prefer type-safe alternatives overascasts.Suggested fix
- const enrichedPage = paginatedResult.page.map((image) => ({ + const enrichedPage: EnrichedImage[] = paginatedResult.page.map((image) => ({ ...image, ownerName: user.username ?? "Anonymous", ownerPictureUrl: user.pictureUrl ?? null, - })) as EnrichedImage[] + }))
141-141: Consider defining a schema forgenerationParamsinstead ofv.any().While
v.any()is a valid Convex validator for dynamic data, it bypasses runtime validation entirely. If the structure of generation parameters is known (even partially), defining a typed validator would improve data integrity and catch malformed inputs early..agent/skills/testing-components/SKILL.md (2)
100-106: Consider using stricter typing in mock examples.The
vi.Mockcast and implicitanyin the mock return value work, but for consistency with coding guidelines preferring noany, consider showing a typed pattern:// Alternative with stricter typing: vi.mocked(useQuery).mockReturnValue([ { _id: "1", name: "Test Image" }, { _id: "2", name: "Another Image" }, ]);
126-132: Mock usesanytype.Per coding guidelines, avoid
any. For documentation examples, consider using a more explicit type:vi.mock("next/image", () => ({ default: ({ src, alt, ...props }: { src: string; alt: string; [key: string]: unknown }) => ( <img src={src} alt={alt} {...props} /> ), }));components/landing/landing-header.tsx (1)
89-167: Consider extracting a reusable NavItem component.The navigation items share identical structure with only
href,label, andactiveSectionvarying. This could be extracted into a component to reduce duplication.♻️ Optional: Extract NavItem component
// Could be defined above LandingHeader or in a separate file function NavItem({ href, label, isActive, onClick }: { href: string label: string isActive: boolean onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void }) { return ( <Link href={href} onClick={onClick} className="group"> <span className={cn( "relative inline-flex items-center text-sm font-medium transition-colors cursor-pointer py-2", isActive ? "text-primary" : "text-foreground/70 hover:text-foreground" )}> {label} <span className={cn( "absolute bottom-1 left-0 h-px transition-all duration-300 ease-out", isActive ? "w-full bg-primary/60" : "w-0 bg-foreground/40 group-hover:w-full" )} /> </span> </Link> ) }
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (9)
convex/_generated/api.d.tsis excluded by!**/_generated/**public/community/abstract.pngis excluded by!**/*.pngpublic/community/cat.pngis excluded by!**/*.pngpublic/community/cyberpunk.pngis excluded by!**/*.pngpublic/community/fantasy.pngis excluded by!**/*.pngpublic/community/fashion.pngis excluded by!**/*.pngpublic/community/interior.pngis excluded by!**/*.pngpublic/community/portrait.pngis excluded by!**/*.pngpublic/community/product.pngis excluded by!**/*.png
📒 Files selected for processing (77)
.agent/skills/adding-convex-table/SKILL.md.agent/skills/adding-route/SKILL.md.agent/skills/brainstorming-ui-and-design/SKILL.md.agent/skills/creating-client-component/SKILL.md.agent/skills/developing-nextjs/SKILL.md.agent/skills/generating-images/SKILL.md.agent/skills/generating-videos/SKILL.md.agent/skills/managing-convex/SKILL.md.agent/skills/preloading-convex-data/SKILL.md.agent/skills/styling-ui/SKILL.md.agent/skills/testing-code/SKILL.md.agent/skills/testing-components/SKILL.md.agent/skills/testing-functions/SKILL.md.agent/skills/writing-and-authoring-skills/SKILL.md.agent/skills/writing-convex-functions/SKILL.md.agent/skills/writing-server-actions/SKILL.mdDIMENSION_CONSTRAINTS_RESEARCH.mdapp/feed/[type]/page.tsxapp/globals.cssapp/page.tsxapp/pricing/page.tsxapp/pricing/pricing-data.tsxapp/sitemap.tscomponents/gallery/feed-client.tsxcomponents/gallery/feed-cta.test.tsxcomponents/gallery/feed-cta.tsxcomponents/gallery/feed-tabs.test.tsxcomponents/gallery/feed-tabs.tsxcomponents/gallery/paginated-image-grid.tsxcomponents/gl/gl.tsxcomponents/images/image-lightbox.test.tsxcomponents/images/image-lightbox.tsxcomponents/landing/community-section.tsxcomponents/landing/competitor-comparison.tsxcomponents/landing/cta-section.tsxcomponents/landing/feature-card.tsxcomponents/landing/features-section.tsxcomponents/landing/hero-section.tsxcomponents/landing/landing-header.tsxcomponents/landing/living-strip.tsxcomponents/landing/model-badge.tsxcomponents/landing/models-section.tsxcomponents/landing/showcase-section.tsxcomponents/landing/value-prop-section.tsxcomponents/layout/footer.tsxcomponents/layout/header.tsxcomponents/pricing/model-value-showcase.tsxcomponents/solutions/solution-steps.tsxcomponents/studio/controls/model-selector.tsxcomponents/studio/layout/studio-header.tsxcomponents/studio/layout/studio-layout.tsxcomponents/studio/layout/studio-shell.tsxcomponents/ui/button.tsxcomponents/ui/image-card.tsxcomponents/ui/sheet.tsxcomponents/ui/sidebar.tsxconvex/generatedImages.tsconvex/lib/videoPreview.tshooks/use-image-lightbox.tshooks/use-mobile.tslib/analytics.tslib/config/models.tslib/seo-config.tsproxy.tstodo/byop-issue-analysis.mdtodo/byop-pollinations-auth-refactor.mdtodo/byop-story-1-core-infrastructure.mdtodo/byop-story-2-ui-integration.mdtodo/byop-story-3-generation-refactor.mdtodo/byop-story-4-cleanup-security.mdtodo/byop-story-5-testing-polish.mdtodo/living-strip-story.mdtodo/product-hunt-launch-plan.mdtodo/public-feed-phase2-story1-landing-integration.mdtodo/public-feed-phase2-story2-image-quality.mdtodo/public-feed-unauthenticated-access.mdvercel.json
💤 Files with no reviewable changes (7)
- todo/byop-pollinations-auth-refactor.md
- todo/byop-story-3-generation-refactor.md
- todo/byop-issue-analysis.md
- todo/byop-story-1-core-infrastructure.md
- todo/byop-story-2-ui-integration.md
- todo/byop-story-4-cleanup-security.md
- todo/byop-story-5-testing-polish.md
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: Always leverage TypeScript (strict mode); avoid usage ofanywhich defeats the point of using TypeScript.
Let TypeScript inference work; export explicit prop types for shared/public components.
Noanytype in TypeScript. Useunknownand narrow instead.
Avoidascasts in TypeScript; prefersatisfies, generics, and runtime validation.
@ts-expect-errorallowed only with a comment explaining why + link/TODO to remove.
Files:
components/landing/competitor-comparison.tsxcomponents/landing/models-section.tsxcomponents/studio/layout/studio-header.tsxcomponents/images/image-lightbox.test.tsxcomponents/landing/community-section.tsxcomponents/gl/gl.tsxcomponents/landing/feature-card.tsxcomponents/gallery/feed-cta.tsxcomponents/landing/model-badge.tsxcomponents/gallery/feed-client.tsxcomponents/gallery/paginated-image-grid.tsxcomponents/landing/living-strip.tsxlib/config/models.tshooks/use-image-lightbox.tscomponents/gallery/feed-tabs.test.tsxapp/pricing/pricing-data.tsxcomponents/pricing/model-value-showcase.tsxcomponents/studio/layout/studio-layout.tsxcomponents/ui/sheet.tsxcomponents/gallery/feed-cta.test.tsxcomponents/ui/button.tsxproxy.tscomponents/landing/value-prop-section.tsxcomponents/layout/header.tsxcomponents/landing/hero-section.tsxcomponents/studio/controls/model-selector.tsxcomponents/landing/landing-header.tsxcomponents/layout/footer.tsxlib/seo-config.tscomponents/images/image-lightbox.tsxcomponents/landing/cta-section.tsxapp/pricing/page.tsxapp/feed/[type]/page.tsxcomponents/gallery/feed-tabs.tsxcomponents/landing/features-section.tsxcomponents/landing/showcase-section.tsxcomponents/studio/layout/studio-shell.tsxconvex/lib/videoPreview.tshooks/use-mobile.tsconvex/generatedImages.tsapp/sitemap.tsapp/page.tsxcomponents/solutions/solution-steps.tsxcomponents/ui/sidebar.tsxcomponents/ui/image-card.tsxlib/analytics.ts
**/*.{tsx,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Avoid
useEffectfor app data fetching. Use it for browser-only side effects (subscriptions, observers, localStorage).
Files:
components/landing/competitor-comparison.tsxcomponents/landing/models-section.tsxcomponents/studio/layout/studio-header.tsxcomponents/images/image-lightbox.test.tsxcomponents/landing/community-section.tsxcomponents/gl/gl.tsxcomponents/landing/feature-card.tsxcomponents/gallery/feed-cta.tsxcomponents/landing/model-badge.tsxcomponents/gallery/feed-client.tsxcomponents/gallery/paginated-image-grid.tsxcomponents/landing/living-strip.tsxcomponents/gallery/feed-tabs.test.tsxapp/pricing/pricing-data.tsxcomponents/pricing/model-value-showcase.tsxcomponents/studio/layout/studio-layout.tsxcomponents/ui/sheet.tsxcomponents/gallery/feed-cta.test.tsxcomponents/ui/button.tsxcomponents/landing/value-prop-section.tsxcomponents/layout/header.tsxcomponents/landing/hero-section.tsxcomponents/studio/controls/model-selector.tsxcomponents/landing/landing-header.tsxcomponents/layout/footer.tsxcomponents/images/image-lightbox.tsxcomponents/landing/cta-section.tsxapp/pricing/page.tsxapp/feed/[type]/page.tsxcomponents/gallery/feed-tabs.tsxcomponents/landing/features-section.tsxcomponents/landing/showcase-section.tsxcomponents/studio/layout/studio-shell.tsxapp/page.tsxcomponents/solutions/solution-steps.tsxcomponents/ui/sidebar.tsxcomponents/ui/image-card.tsx
app/**/*.{tsx,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
app/**/*.{tsx,jsx}: Use Server Components by default in Next.js App Router. Add'use client'only at the leaves that need interactivity/hooks.
UseuseQuery/useMutationin Client Components for client reactivity with Convex.
In Server Components, usepreloadQuery(...)and pass the payload to a Client Component usingusePreloadedQuery(...)for SSR + reactivity.
UsefetchQuery(...)in Server Components for server-only (non-reactive) rendering with Convex.
Prefer Convex optimistic updates (useMutation(...).withOptimisticUpdate(...)) over duplicating server logic in ad-hoc local state.
Prefer Actions +useActionStatefor form submission state when using React 19 form actions.
UseuseOptimisticin React 19 for instant UI feedback when appropriate.
UseuseFormStatusinside design-system components that need form pending state in React 19.
Useuseto read a Promise/Context during render only where Suspense semantics make sense in React 19.
Use dynamic imports for non-critical client UI in Next.js.
Usenext/imagefor images in Next.js.
Use shadcn/ui as the base component library; extend via composition + variants.
Files:
app/pricing/pricing-data.tsxapp/pricing/page.tsxapp/feed/[type]/page.tsxapp/page.tsx
{app/**/*.{tsx,jsx},**/*.css}
📄 CodeRabbit inference engine (CLAUDE.md)
Use Tailwind CSS utilities in JSX; extract repetition into components, not
@apply.
Files:
app/pricing/pricing-data.tsxapp/pricing/page.tsxapp/feed/[type]/page.tsxapp/globals.cssapp/page.tsx
**/*.css
📄 CodeRabbit inference engine (CLAUDE.md)
Theme via CSS variables; keep design tokens centralized.
Files:
app/globals.css
🧠 Learnings (24)
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/{actions,route}.{ts,tsx} : Call Convex via `fetchMutation` / `fetchAction` in Server Actions / Route Handlers when you must mutate from the server boundary.
Applied to files:
.agent/skills/writing-convex-functions/SKILL.md.agent/skills/managing-convex/SKILL.md.agent/skills/developing-nextjs/SKILL.md.agent/skills/preloading-convex-data/SKILL.md.agent/skills/writing-server-actions/SKILL.md
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/*.{tsx,jsx} : Use `useQuery` / `useMutation` in Client Components for client reactivity with Convex.
Applied to files:
.agent/skills/writing-convex-functions/SKILL.md.agent/skills/creating-client-component/SKILL.md.agent/skills/managing-convex/SKILL.md.agent/skills/developing-nextjs/SKILL.md.agent/skills/preloading-convex-data/SKILL.mdcomponents/landing/landing-header.tsxtodo/public-feed-unauthenticated-access.md.agent/skills/testing-code/SKILL.mdhooks/use-mobile.tscomponents/solutions/solution-steps.tsxcomponents/ui/image-card.tsx
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/*.{tsx,jsx} : Use `fetchQuery(...)` in Server Components for server-only (non-reactive) rendering with Convex.
Applied to files:
.agent/skills/writing-convex-functions/SKILL.md.agent/skills/creating-client-component/SKILL.md.agent/skills/managing-convex/SKILL.md.agent/skills/developing-nextjs/SKILL.md.agent/skills/preloading-convex-data/SKILL.mdtodo/public-feed-unauthenticated-access.mdapp/page.tsx
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/*.{tsx,jsx} : Use shadcn/ui as the base component library; extend via composition + variants.
Applied to files:
components/studio/layout/studio-header.tsx.agent/skills/styling-ui/SKILL.mdcomponents/ui/button.tsxcomponents/landing/landing-header.tsxcomponents/layout/footer.tsxcomponents/landing/showcase-section.tsxapp/page.tsxcomponents/ui/image-card.tsx.agent/skills/testing-components/SKILL.md
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/*.{tsx,jsx} : Use Server Components by default in Next.js App Router. Add `'use client'` only at the leaves that need interactivity/hooks.
Applied to files:
.agent/skills/creating-client-component/SKILL.md.agent/skills/adding-route/SKILL.md.agent/skills/developing-nextjs/SKILL.md.agent/skills/preloading-convex-data/SKILL.mdcomponents/landing/landing-header.tsxhooks/use-mobile.tsapp/page.tsxcomponents/solutions/solution-steps.tsx
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/*.{tsx,jsx} : In Server Components, use `preloadQuery(...)` and pass the payload to a Client Component using `usePreloadedQuery(...)` for SSR + reactivity.
Applied to files:
.agent/skills/creating-client-component/SKILL.md.agent/skills/developing-nextjs/SKILL.md.agent/skills/preloading-convex-data/SKILL.mdapp/page.tsx
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/*.{tsx,jsx} : Use dynamic imports for non-critical client UI in Next.js.
Applied to files:
.agent/skills/creating-client-component/SKILL.md.agent/skills/developing-nextjs/SKILL.md.agent/skills/preloading-convex-data/SKILL.mdcomponents/images/image-lightbox.tsxhooks/use-mobile.tsapp/page.tsx
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Presentational components should take props in and output JSX with no data fetching and minimal/no app logic.
Applied to files:
.agent/skills/creating-client-component/SKILL.mdapp/page.tsx
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Convex is the source of truth for server state; local state is for UI-only concerns (selection, filters, draft text).
Applied to files:
.agent/skills/managing-convex/SKILL.md
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/*.{tsx,jsx} : Prefer Convex optimistic updates (`useMutation(...).withOptimisticUpdate(...)`) over duplicating server logic in ad-hoc local state.
Applied to files:
.agent/skills/managing-convex/SKILL.md.agent/skills/developing-nextjs/SKILL.md.agent/skills/preloading-convex-data/SKILL.mdhooks/use-mobile.ts
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/loading.tsx : Use `loading.tsx` and `error.tsx` for route-segment UX in Next.js App Router.
Applied to files:
.agent/skills/adding-route/SKILL.md.agent/skills/developing-nextjs/SKILL.md.agent/skills/preloading-convex-data/SKILL.mdcomponents/landing/landing-header.tsxapp/page.tsx
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/not-found.tsx : Use `not-found.tsx` for 404 states in Next.js App Router.
Applied to files:
.agent/skills/adding-route/SKILL.md.agent/skills/developing-nextjs/SKILL.md
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/(page|layout).{tsx,jsx} : Avoid multiple `preloadQuery` calls on the same page; consolidate queries or design around a single preload.
Applied to files:
.agent/skills/developing-nextjs/SKILL.md.agent/skills/preloading-convex-data/SKILL.md
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to {app/**/*.{tsx,jsx},**/*.css} : Use Tailwind CSS utilities in JSX; extract repetition into components, not `apply`.
Applied to files:
.agent/skills/styling-ui/SKILL.md
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to **/*.css : Theme via CSS variables; keep design tokens centralized.
Applied to files:
.agent/skills/styling-ui/SKILL.mdapp/globals.css
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/*.{tsx,jsx} : Use `useFormStatus` inside design-system components that need form pending state in React 19.
Applied to files:
.agent/skills/styling-ui/SKILL.md
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/*.{tsx,jsx} : Use `next/image` for images in Next.js.
Applied to files:
components/gallery/feed-client.tsxcomponents/images/image-lightbox.tsxcomponents/ui/image-card.tsxtodo/public-feed-phase2-story2-image-quality.md
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/*.{tsx,jsx} : Use `useOptimistic` in React 19 for instant UI feedback when appropriate.
Applied to files:
components/landing/landing-header.tsxcomponents/images/image-lightbox.tsxhooks/use-mobile.tscomponents/solutions/solution-steps.tsx
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/*.{tsx,jsx} : Use `use` to read a Promise/Context during render only where Suspense semantics make sense in React 19.
Applied to files:
components/landing/landing-header.tsxcomponents/solutions/solution-steps.tsx
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/*.{tsx,jsx} : Prefer Actions + `useActionState` for form submission state when using React 19 form actions.
Applied to files:
.agent/skills/writing-server-actions/SKILL.md
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to **/*.{ts,tsx} : `ts-expect-error` allowed only with a comment explaining why + link/TODO to remove.
Applied to files:
.agent/skills/testing-code/SKILL.md
📚 Learning: 2025-12-26T08:12:21.228Z
Learnt from: Simplereally
Repo: Simplereally/pixelstream PR: 1
File: convex/users.ts:24-46
Timestamp: 2025-12-26T08:12:21.228Z
Learning: Convex schemas do not support schema-level unique constraints (no .unique() on index definitions). Enforce uniqueness in mutation logic using a check-then-insert pattern, which is safe because Convex mutations are serialized with ACID guarantees. Apply this guidance to all Convex schema and mutation code (not just convex/schema.ts) to ensure data integrity.
Applied to files:
convex/lib/videoPreview.tsconvex/generatedImages.ts
📚 Learning: 2025-12-26T08:12:21.228Z
Learnt from: Simplereally
Repo: Simplereally/pixelstream PR: 1
File: convex/users.ts:24-46
Timestamp: 2025-12-26T08:12:21.228Z
Learning: Convex mutations provide serializable isolation with optimistic concurrency control (OCC). The check-then-insert pattern (query for existing record, then conditionally insert) is safe from race conditions in Convex because concurrent mutations that conflict are retried and execute as if they ran in serial order. Apply this pattern for lazy initializations (e.g., getOrCreateUser patterns) in convex/* files, avoiding additional unique constraints. Ensure the mutation is idempotent and retriable.
Applied to files:
convex/lib/videoPreview.tsconvex/generatedImages.ts
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to **/*.{tsx,jsx} : Avoid `useEffect` for app data fetching. Use it for browser-only side effects (subscriptions, observers, localStorage).
Applied to files:
hooks/use-mobile.tscomponents/solutions/solution-steps.tsx
🧬 Code graph analysis (17)
components/landing/community-section.tsx (1)
components/landing/scroll-reveal.tsx (1)
ScrollReveal(28-68)
components/landing/model-badge.tsx (1)
lib/utils.ts (1)
cn(11-13)
components/gallery/feed-client.tsx (1)
lib/analytics.ts (1)
trackFeedView(69-77)
components/landing/living-strip.tsx (2)
lib/utils.ts (1)
cn(11-13)components/ui/button.tsx (1)
Button(79-79)
components/gallery/feed-tabs.test.tsx (1)
components/gallery/feed-tabs.tsx (1)
FeedTabs(17-50)
components/pricing/model-value-showcase.tsx (1)
lib/utils.ts (1)
cn(11-13)
components/gallery/feed-cta.test.tsx (1)
components/gallery/feed-cta.tsx (1)
FeedCta(21-102)
components/landing/value-prop-section.tsx (1)
components/landing/scroll-reveal.tsx (1)
ScrollReveal(28-68)
components/landing/hero-section.tsx (2)
components/landing/scroll-reveal.tsx (1)
ScrollReveal(28-68)components/ui/button.tsx (1)
Button(79-79)
components/landing/landing-header.tsx (2)
lib/utils.ts (1)
cn(11-13)components/ui/button.tsx (1)
Button(79-79)
components/layout/footer.tsx (2)
components/ui/button.tsx (1)
Button(79-79)lib/utils.ts (1)
cn(11-13)
components/images/image-lightbox.tsx (2)
lib/utils.ts (1)
cn(11-13)components/ui/button.tsx (1)
Button(79-79)
app/pricing/page.tsx (2)
components/pricing/model-value-showcase.tsx (1)
ModelValueShowcase(231-371)components/pricing/feature-detail-dialog.tsx (1)
FeatureDetailDialog(22-71)
app/feed/[type]/page.tsx (1)
components/gallery/feed-cta.tsx (1)
FeedCta(21-102)
components/landing/features-section.tsx (1)
components/landing/scroll-reveal.tsx (1)
ScrollReveal(28-68)
components/solutions/solution-steps.tsx (1)
components/landing/scroll-reveal.tsx (1)
ScrollReveal(28-68)
components/ui/sidebar.tsx (1)
lib/utils.ts (1)
cn(11-13)
🪛 ast-grep (0.40.5)
app/feed/[type]/page.tsx
[warning] 87-87: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html
(react-unsafe-html-injection)
🪛 Biome (2.1.2)
app/feed/[type]/page.tsx
[error] 88-88: Avoid passing content using the dangerouslySetInnerHTML prop.
Setting content using code can expose users to cross-site scripting (XSS) attacks
(lint/security/noDangerouslySetInnerHtml)
🪛 LanguageTool
.agent/skills/adding-route/SKILL.md
[style] ~124-~124: ‘absolutely required’ might be wordy. Consider a shorter alternative.
Context: ...add 'use client'** to page.tsx unless absolutely required - If route already exists, stop and...
(EN_WORDINESS_PREMIUM_ABSOLUTELY_REQUIRED)
.agent/skills/writing-and-authoring-skills/SKILL.md
[style] ~67-~67: The adverb ‘often’ is usually put before the verb ‘do’.
Context: ...you already do well Pick something you do often and consistently: * “Add a new feature...
(ADVERB_WORD_ORDER)
.agent/skills/brainstorming-ui-and-design/SKILL.md
[grammar] ~10-~10: Use a hyphen to join words.
Context: ...ent/skills/styling-ui/SKILL.md` for repo specific tailwind/css rules. ## Design ...
(QB_NEW_EN_HYPHEN)
[style] ~43-~43: “thinking outside the box” can be a clichéd phrase in professional communication. Consider an alternative expression to make your writing more engaging and original.
Context: ...ck, show what can truly be created when thinking outside the box and committing fully to a distinctive v...
(THINK_OUTSIDE_THE_BOX)
todo/product-hunt-launch-plan.md
[style] ~48-~48: Consider using a more formal and expressive alternative to ‘amazing’.
Context: ...t Bloom because we were tired of seeing amazing AI tools gated behind $30/month subscri...
(AWESOME)
[style] ~58-~58: Consider using polite language here.
Context: ...th it, and tell us what you think! > > Let me know if you have any feature requests!" ## ...
(INSERT_PLEASE)
todo/public-feed-phase2-story2-image-quality.md
[grammar] ~28-~28: Ensure spelling is correct
Context: ... to Original Image URLs ### 1.1 Update toPublicFeedImages Helper **Current State (convex/generat...
(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)
🪛 markdownlint-cli2 (0.18.1)
.agent/skills/creating-client-component/SKILL.md
20-20: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
.agent/skills/developing-nextjs/SKILL.md
32-32: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
todo/product-hunt-launch-plan.md
67-67: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
hooks/use-aspect-ratio-dimensions.ts (1)
222-255: FixaspectRatioOptionstier scaling for multi-tier models or document its intended use.The code correctly applies tier-based scaling in
getDimensionsForRatio(HD=1×, 2K=2×, 4K=4×) for multi-tier models, butaspectRatioOptionsreturns dimensions fromavailableRatioswithout applying the same scaling. This creates an inconsistency where the two functions return different dimensions for the same ratio whentieris "2k" or "4k".Currently,
aspectRatioOptionsis exported but unused in the codebase (the selector usesgetDimensionsForRatiodirectly). To prevent future bugs if this is used:
- Option 1: Remove the unused export
- Option 2: Apply tier scaling in
aspectRatioOptionsto matchgetDimensionsForRatiobehavior- Option 3: Add a comment clarifying that
aspectRatioOptionsintentionally returns base dimensions (not tier-scaled) for reference purposes only
🤖 Fix all issues with AI agents
In `@app/layout.tsx`:
- Line 43: Update the inconsistent branding strings: replace the "NanoBanana"
mentions in openGraph.description and twitter.description so they match the main
description's "Nano Banana Pro" text; locate the openGraph.description and
twitter.description properties in app/layout.tsx and update their description
strings to the exact phrasing used in the main description.
In `@components/landing/value-prop-section.tsx`:
- Line 44: The sentence in the ValuePropSection JSX ("All premium image and
video models are included. Nano Banana Pro, Veo3.1, Seedance, Seedream Pro -
everything is included.") is redundant; replace it with a single concise line
that lists the models without repeating "included" (for example: "All premium
image and video models — Nano Banana Pro, Veo3.1, Seedance, Seedream Pro — are
included."), updating the string literal inside the component rendering in
value-prop-section.tsx.
In `@components/studio/controls/aspect-ratio-selector.tsx`:
- Around line 47-82: The function getIconForTag declares a return type of
LucideIcon | null but always returns a LucideIcon (falls back to Frame), so
update the signature to return LucideIcon (remove | null) and adjust any callers
that currently handle a null (remove or simplify unnecessary null checks) —
specifically change the function declaration getIconForTag = (tag: string):
LucideIcon | null => to return LucideIcon and ensure usages expecting null are
updated to accept a guaranteed LucideIcon.
In `@components/ui/image-card.tsx`:
- Around line 347-355: The Link wrapping a Button produces invalid HTML
(interactive <button> inside <a>) for the JSX in the image-card component:
replace the nested interactive elements by using a single interactive
element—either use your Button's "asChild" pattern (render Button as an anchor
so Link/anchor is the outer interactive element) or have Link render an anchor
via legacyBehavior/passHref and move the Button styles onto that anchor (or
convert the Button to a styled <a>), ensuring you keep the onClick
stopPropagation behavior and preserve the Heart icon and Button props (variant,
size, className).
In `@convex/lib/pollinations.test.ts`:
- Line 226: The test uses the display name "Nano Banana" for the model, but
buildPollinationsUrl sends the model ID to the Pollinations API; change the
model value to the ID "nanobanana" in the test (the object with model: "Nano
Banana") so it matches lib/config/models.ts and other tests (e.g., "zimage",
"veo", "seedance-pro") and ensures buildPollinationsUrl sends the correct model
identifier.
In `@README.md`:
- Line 26: Replace the incorrect model display name "GPT1.5" with the official
"GPT 1.5" in the README models list (the line containing "🎨 **13+ AI Models** —
Choose from Nano Banana Pro, GPT1.5, ...") so the user-facing documentation
matches the model registry's official display name.
♻️ Duplicate comments (3)
.agent/skills/developing-nextjs/SKILL.md (1)
4-4: Update all Next.js version references for consistency.The documentation references "Next.js 16" in multiple locations (lines 4, 9, and 11), but the provided library documentation focuses on Next.js 15. Ensure all version references are consistent with the actual Next.js version used in the project.
📝 Suggested fix
- Creates pages, layouts, and server/client components in Next.js 16 App Router. + Creates pages, layouts, and server/client components in Next.js 15 App Router.-# Developing with Next.js 16 +# Developing with Next.js 15-Next.js 16 App Router overview. **Use the specific skills below for actual tasks.** +Next.js 15 App Router overview. **Use the specific skills below for actual tasks.**Also applies to: 9-9, 11-11
components/pricing/model-value-showcase.tsx (1)
434-434: Fix: Extra whitespace in closing tag.Remove the stray space before
>.- </section > + </section>components/gallery/feed-cta.tsx (1)
31-37: Comment/code mismatch: comment says 600px, code checks 1200px.The comment on line 32 references "600px" but the condition on line 33 checks
> 1200. This was flagged in a previous review and marked as addressed, but the comment still doesn't match the actual threshold.📝 Suggested fix
const handleScroll = () => { - // Show after scrolling 600px (roughly 4-6 images viewed) + // Show after scrolling 1200px (engagement threshold) if (window.scrollY > 1200 && !isVisibleRef.current) {
🧹 Nitpick comments (15)
components/studio/upgrade-modal.test.tsx (1)
7-8: Unused import:STRIPE_CONFIG.
STRIPE_CONFIGis imported but never referenced directly in the test code. The mock at lines 31-38 defines its ownSTRIPE_CONFIGobject, making this import unnecessary.🔧 Suggested fix
-import { isStripeConfigured, STRIPE_CONFIG } from "@/lib/config/stripe"; +import { isStripeConfigured } from "@/lib/config/stripe";components/landing/value-prop-section.tsx (1)
25-25: Consistent gradient styling applied across all feature blocks — looks good.The uniform
bg-gradient-to-br from-white/5 via-card/80 to-purple-500/5 border border-white/10styling across all four blocks creates visual consistency.Optionally, you could extract this repeated class string to a constant to reduce duplication and make future style updates easier:
const featureCardClasses = "h-full p-6 rounded-2xl bg-gradient-to-br from-white/5 via-card/80 to-purple-500/5 border border-white/10";Also applies to: 33-33, 41-41, 49-49
components/studio/controls/aspect-ratio-selector.tsx (1)
253-264: Redundant null check forIcon.Since
getIconForTagalways returns an icon (nevernull), the conditional check{Icon && ...}on line 260 is unnecessary. If you apply the return type fix, this can be simplified.Suggested simplification
{tags.map((tag) => { const Icon = getIconForTag(tag); return ( <div key={tag} className="flex items-center gap-2 text-xs text-muted-foreground" > - {Icon && <Icon className="h-3.5 w-3.5 text-muted-foreground/70" />} + <Icon className="h-3.5 w-3.5 text-muted-foreground/70" /> <span>{tag}</span> </div> ); })}.agent/skills/developing-nextjs/SKILL.md (1)
32-45: Add language specifier to fenced code block.The decision tree uses a fenced code block without a language specifier. Consider adding a language identifier for better syntax highlighting and rendering.
📝 Suggested fix
-``` +```text Need data from Convex? ├─ Server Component → preloadQuery() → see `preloading-convex-data`hooks/use-media-player.ts (1)
69-75: Consider removing unusedisVideoparameter.The
isVideoparameter is accepted but never used within the hook's logic. If it's only needed by the component to determine rendering, it may be cleaner to keep this determination outside the hook.Suggested change
export function useMediaPlayer({ url, - isVideo, onLoad, onError, onClick, }: UseMediaPlayerProps): UseMediaPlayerReturn {And update the interface accordingly:
export interface UseMediaPlayerProps { /** Media URL - used to reset state when it changes */ url: string - /** Whether the content is video (vs image) */ - isVideo: boolean /** Callback when media successfully loads */ onLoad?: (e: React.SyntheticEvent<HTMLImageElement | HTMLVideoElement>) => voidconvex/lib/videoPreview.ts (2)
82-104: Rejected promise remains cached, preventing retry on transient failures.If the lazy import fails (e.g., due to a transient issue),
ffmpegPromisewill hold the rejected promise indefinitely. Subsequent calls will receive the same rejected promise instead of retrying.♻️ Suggested fix: Reset cache on rejection
let ffmpegPromise: Promise<typeof Ffmpeg> | null = null function getFfmpeg(): Promise<typeof Ffmpeg> { if (!ffmpegPromise) { ffmpegPromise = (async () => { const importStart = performance.now() const ffmpegStatic = (await import("ffmpeg-static")).default console.log(`[VideoPreview] [PERF] import ffmpeg-static took ${(performance.now() - importStart).toFixed(1)}ms`) const moduleStart = performance.now() const ffmpegModule = await import("fluent-ffmpeg") console.log(`[VideoPreview] [PERF] import fluent-ffmpeg took ${(performance.now() - moduleStart).toFixed(1)}ms`) if (!ffmpegStatic) { throw new Error("ffmpeg-static binary not found") } ffmpegModule.default.setFfmpegPath(ffmpegStatic) return ffmpegModule.default - })() + })().catch((err) => { + ffmpegPromise = null // Reset cache to allow retry + throw err + }) } return ffmpegPromise }
194-202: Edge case: compression ratio can be negative for small files.If the output file is larger than the input (possible with very small or already-compressed files),
compressionRatiowill be negative. This isn't a bug, but consumers should be aware of this possibility.Consider documenting this in the
VideoPreviewResultinterface or clamping to zero if negative values are confusing:📝 Option: Add JSDoc clarification
/** Result of video preview generation */ export interface VideoPreviewResult { /** Preview video buffer */ buffer: Buffer - /** Size reduction percentage */ + /** Size reduction percentage (can be negative if preview is larger than original) */ compressionRatio: number }components/pricing/model-value-showcase.tsx (2)
251-263: Consider: Unused accent variants.The
accentprop supports"ember" | "neutral" | "muted", but all threeTierSectionusages pass"ember". If the other variants are for future use, this is fine. Otherwise, simplify by removing unused variants.
417-431: Consider usingnext/linkfor internal navigation.The
/#modelslink navigates to another page. UsingLinkfromnext/linkenables client-side navigation for better performance.♻️ Suggested improvement
+import Link from "next/link" ... <div className="mt-6 flex flex-wrap justify-center gap-6 text-xs text-muted-foreground"> <a href="#comparison-table" className="hover:text-foreground transition-colors underline underline-offset-4 decoration-border hover:decoration-foreground" > Compare features </a> <span className="text-border">·</span> - <a + <Link href="/#models" className="hover:text-foreground transition-colors underline underline-offset-4 decoration-border hover:decoration-foreground" > Explore all models - </a> + </Link> </div>components/images/image-lightbox.tsx (1)
226-231: Considerdecoding="async"instead of"sync"for large images.Using
decoding="sync"forces the browser to decode the image synchronously, which can cause UI jank/frame drops on large high-resolution images. Since this is the full-resolution layer that loads after the thumbnail,decoding="async"(or omitting the attribute entirely) would allow the browser to schedule decoding off the main thread.The crossfade effect should still work smoothly since the
onLoadcallback triggers after decoding completes.📝 Suggested change
<NextImage src={fullResUrl || displayImage.url} alt={displayImage.prompt || "Generated image"} onLoad={(e) => { handleImageLoad(e as unknown as React.SyntheticEvent<HTMLImageElement>) setIsFullResLoaded(true) }} draggable={false} - decoding="sync" + decoding="async" width={displayImage.width || displayImage.params?.width || 1000}components/ui/image-card.tsx (2)
86-94: Video detection and query setup look correct.The
isVideoContenthelper is used appropriately. The favorites query correctly skips when the user is not signed in.Note: The
as Id<"generatedImages">cast is repeated here and at line 135. Per coding guidelines,ascasts should be avoided, but this pattern is common with Convex when the data model usesstringfor IDs that need to be narrowed. Consider typingImageCardData._idasId<"generatedImages">if feasible to eliminate these casts.
223-224: Redundantloadingattribute whenpriorityis true.When
priority={true}, Next.js Image automatically handles eager loading and adds preload hints. The explicitloading="eager"is redundant in this case.Suggested simplification
- loading={priority ? "eager" : "lazy"} - priority={priority} + priority={priority} + {...(!priority && { loading: "lazy" })}Or simply remove the
loadingprop entirely since Next.js defaults tolazywhenpriorityis false:- loading={priority ? "eager" : "lazy"} priority={priority}nano-banana-pro-spec.md (1)
60-60: Minor grammatical redundancy."The reason... is because" contains redundancy. Consider simplifying to "...becomes 1376 because..." or "The reason... is that...".
✏️ Suggested fix
-The reason $1344$ becomes $1376$ is because $1376$ is the **Absolute Max Buffer** for the 1K hardware instance. +$1344$ becomes $1376$ because $1376$ is the **Absolute Max Buffer** for the 1K hardware instance.lib/config/models.ts (1)
506-506: Consider using straight quotes for consistency.The description uses curly/smart quotes (
"ingredients") while other model descriptions in this file use straight quotes or no quotes. Smart quotes can cause issues with text search and copy-paste operations.✏️ Suggested fix
- description: "Reference-guided generation, character/product consistency across shots, native audio, "ingredients" style control, social-ready storytelling", + description: "Reference-guided generation, character/product consistency across shots, native audio, 'ingredients' style control, social-ready storytelling",hooks/use-aspect-ratio-dimensions.ts (1)
157-192: Consider using a tier-to-scale mapping for clarity and extensibility.The scaling logic is correct, but using a mapping object would be cleaner than the if-else chain and easier to extend if new tiers are added.
♻️ Suggested refactor
- // Multi-tier model (e.g., Nano Banana Pro) - // availableRatios contains base (HD/1K) dimensions - // Scale based on selected tier: HD=1×, 2K=2×, 4K=4× - let scale = 1 - if (tier === "2k") { - scale = 2 - } else if (tier === "4k") { - scale = 4 - } + // Multi-tier model (e.g., Nano Banana Pro) + // availableRatios contains base (HD/1K) dimensions + // Scale based on selected tier: SD/HD=1×, 2K=2×, 4K=4× + const tierScaleMap: Record<ResolutionTier, number> = { + sd: 1, + hd: 1, + "2k": 2, + "4k": 4, + max: 4, // Fallback to 4K scale for max tier + } + const scale = tierScaleMap[tier]
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (35)
.agent/skills/adding-route/SKILL.md.agent/skills/developing-nextjs/SKILL.mdREADME.mdapp/layout.tsxapp/pricing/pricing-data.tsxcomponents/gallery/feed-cta.test.tsxcomponents/gallery/feed-cta.tsxcomponents/gallery/image-history.tsxcomponents/images/image-lightbox.tsxcomponents/landing/competitor-comparison.tsxcomponents/landing/faq-section.tsxcomponents/landing/features-section.tsxcomponents/landing/models-section.tsxcomponents/landing/value-prop-section.tsxcomponents/pricing/model-value-showcase.tsxcomponents/studio/controls/aspect-ratio-selector.tsxcomponents/studio/controls/model-selector.tsxcomponents/studio/features/generation/controls-view.tsxcomponents/studio/upgrade-modal.test.tsxcomponents/studio/upgrade-modal.tsxcomponents/ui/image-card.test.tsxcomponents/ui/image-card.tsxcomponents/ui/media-player.test.tsxcomponents/ui/media-player.tsxconvex/lib/pollinations.test.tsconvex/lib/videoPreview.tshooks/use-aspect-ratio-dimensions.test.tshooks/use-aspect-ratio-dimensions.tshooks/use-media-player.test.tshooks/use-media-player.tslib/config/models.test.tslib/config/models.tslib/seo-config.tsnano-banana-pro-spec.mdnanobana-specs.md
✅ Files skipped from review due to trivial changes (1)
- nanobana-specs.md
🚧 Files skipped from review as they are similar to previous changes (4)
- lib/seo-config.ts
- components/studio/controls/model-selector.tsx
- components/landing/competitor-comparison.tsx
- components/landing/features-section.tsx
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: Always leverage TypeScript (strict mode); avoid usage ofanywhich defeats the point of using TypeScript.
Let TypeScript inference work; export explicit prop types for shared/public components.
Noanytype in TypeScript. Useunknownand narrow instead.
Avoidascasts in TypeScript; prefersatisfies, generics, and runtime validation.
@ts-expect-errorallowed only with a comment explaining why + link/TODO to remove.
Files:
components/gallery/image-history.tsxcomponents/studio/features/generation/controls-view.tsxapp/layout.tsxcomponents/ui/media-player.tsxcomponents/ui/media-player.test.tsxcomponents/gallery/feed-cta.test.tsxcomponents/studio/upgrade-modal.test.tsxhooks/use-aspect-ratio-dimensions.test.tscomponents/images/image-lightbox.tsxcomponents/ui/image-card.tsxcomponents/landing/models-section.tsxconvex/lib/pollinations.test.tshooks/use-media-player.tscomponents/gallery/feed-cta.tsxapp/pricing/pricing-data.tsxlib/config/models.test.tscomponents/studio/upgrade-modal.tsxcomponents/pricing/model-value-showcase.tsxcomponents/landing/faq-section.tsxcomponents/landing/value-prop-section.tsxcomponents/studio/controls/aspect-ratio-selector.tsxcomponents/ui/image-card.test.tsxlib/config/models.tshooks/use-aspect-ratio-dimensions.tshooks/use-media-player.test.tsconvex/lib/videoPreview.ts
**/*.{tsx,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Avoid
useEffectfor app data fetching. Use it for browser-only side effects (subscriptions, observers, localStorage).
Files:
components/gallery/image-history.tsxcomponents/studio/features/generation/controls-view.tsxapp/layout.tsxcomponents/ui/media-player.tsxcomponents/ui/media-player.test.tsxcomponents/gallery/feed-cta.test.tsxcomponents/studio/upgrade-modal.test.tsxcomponents/images/image-lightbox.tsxcomponents/ui/image-card.tsxcomponents/landing/models-section.tsxcomponents/gallery/feed-cta.tsxapp/pricing/pricing-data.tsxcomponents/studio/upgrade-modal.tsxcomponents/pricing/model-value-showcase.tsxcomponents/landing/faq-section.tsxcomponents/landing/value-prop-section.tsxcomponents/studio/controls/aspect-ratio-selector.tsxcomponents/ui/image-card.test.tsx
app/**/*.{tsx,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
app/**/*.{tsx,jsx}: Use Server Components by default in Next.js App Router. Add'use client'only at the leaves that need interactivity/hooks.
UseuseQuery/useMutationin Client Components for client reactivity with Convex.
In Server Components, usepreloadQuery(...)and pass the payload to a Client Component usingusePreloadedQuery(...)for SSR + reactivity.
UsefetchQuery(...)in Server Components for server-only (non-reactive) rendering with Convex.
Prefer Convex optimistic updates (useMutation(...).withOptimisticUpdate(...)) over duplicating server logic in ad-hoc local state.
Prefer Actions +useActionStatefor form submission state when using React 19 form actions.
UseuseOptimisticin React 19 for instant UI feedback when appropriate.
UseuseFormStatusinside design-system components that need form pending state in React 19.
Useuseto read a Promise/Context during render only where Suspense semantics make sense in React 19.
Use dynamic imports for non-critical client UI in Next.js.
Usenext/imagefor images in Next.js.
Use shadcn/ui as the base component library; extend via composition + variants.
Files:
app/layout.tsxapp/pricing/pricing-data.tsx
{app/**/*.{tsx,jsx},**/*.css}
📄 CodeRabbit inference engine (CLAUDE.md)
Use Tailwind CSS utilities in JSX; extract repetition into components, not
@apply.
Files:
app/layout.tsxapp/pricing/pricing-data.tsx
🧠 Learnings (17)
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/*.{tsx,jsx} : Use `next/image` for images in Next.js.
Applied to files:
components/images/image-lightbox.tsxcomponents/ui/image-card.tsx
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/*.{tsx,jsx} : Use `useOptimistic` in React 19 for instant UI feedback when appropriate.
Applied to files:
components/images/image-lightbox.tsxcomponents/ui/image-card.tsxcomponents/pricing/model-value-showcase.tsx
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/*.{tsx,jsx} : Use dynamic imports for non-critical client UI in Next.js.
Applied to files:
components/images/image-lightbox.tsx.agent/skills/developing-nextjs/SKILL.md
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/*.{tsx,jsx} : Use `useQuery` / `useMutation` in Client Components for client reactivity with Convex.
Applied to files:
components/ui/image-card.tsx.agent/skills/developing-nextjs/SKILL.md
📚 Learning: 2025-12-26T08:12:21.228Z
Learnt from: Simplereally
Repo: Simplereally/pixelstream PR: 1
File: convex/users.ts:24-46
Timestamp: 2025-12-26T08:12:21.228Z
Learning: Convex schemas do not support schema-level unique constraints (no .unique() on index definitions). Enforce uniqueness in mutation logic using a check-then-insert pattern, which is safe because Convex mutations are serialized with ACID guarantees. Apply this guidance to all Convex schema and mutation code (not just convex/schema.ts) to ensure data integrity.
Applied to files:
convex/lib/pollinations.test.tsconvex/lib/videoPreview.ts
📚 Learning: 2025-12-26T08:12:21.228Z
Learnt from: Simplereally
Repo: Simplereally/pixelstream PR: 1
File: convex/users.ts:24-46
Timestamp: 2025-12-26T08:12:21.228Z
Learning: Convex mutations provide serializable isolation with optimistic concurrency control (OCC). The check-then-insert pattern (query for existing record, then conditionally insert) is safe from race conditions in Convex because concurrent mutations that conflict are retried and execute as if they ran in serial order. Apply this pattern for lazy initializations (e.g., getOrCreateUser patterns) in convex/* files, avoiding additional unique constraints. Ensure the mutation is idempotent and retriable.
Applied to files:
convex/lib/pollinations.test.tsconvex/lib/videoPreview.ts
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/*.{tsx,jsx} : Use `use` to read a Promise/Context during render only where Suspense semantics make sense in React 19.
Applied to files:
hooks/use-media-player.ts
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to **/*.{tsx,jsx} : Avoid `useEffect` for app data fetching. Use it for browser-only side effects (subscriptions, observers, localStorage).
Applied to files:
components/pricing/model-value-showcase.tsx
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/*.{tsx,jsx} : Use shadcn/ui as the base component library; extend via composition + variants.
Applied to files:
components/studio/controls/aspect-ratio-selector.tsx
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/loading.tsx : Use `loading.tsx` and `error.tsx` for route-segment UX in Next.js App Router.
Applied to files:
.agent/skills/adding-route/SKILL.md.agent/skills/developing-nextjs/SKILL.md
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/not-found.tsx : Use `not-found.tsx` for 404 states in Next.js App Router.
Applied to files:
.agent/skills/adding-route/SKILL.md.agent/skills/developing-nextjs/SKILL.md
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/*.{tsx,jsx} : Use Server Components by default in Next.js App Router. Add `'use client'` only at the leaves that need interactivity/hooks.
Applied to files:
.agent/skills/adding-route/SKILL.md.agent/skills/developing-nextjs/SKILL.md
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/*.{tsx,jsx} : In Server Components, use `preloadQuery(...)` and pass the payload to a Client Component using `usePreloadedQuery(...)` for SSR + reactivity.
Applied to files:
.agent/skills/developing-nextjs/SKILL.md
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/*.{tsx,jsx} : Use `fetchQuery(...)` in Server Components for server-only (non-reactive) rendering with Convex.
Applied to files:
.agent/skills/developing-nextjs/SKILL.md
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/{actions,route}.{ts,tsx} : Call Convex via `fetchMutation` / `fetchAction` in Server Actions / Route Handlers when you must mutate from the server boundary.
Applied to files:
.agent/skills/developing-nextjs/SKILL.md
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/*.{tsx,jsx} : Prefer Convex optimistic updates (`useMutation(...).withOptimisticUpdate(...)`) over duplicating server logic in ad-hoc local state.
Applied to files:
.agent/skills/developing-nextjs/SKILL.md
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/(page|layout).{tsx,jsx} : Avoid multiple `preloadQuery` calls on the same page; consolidate queries or design around a single preload.
Applied to files:
.agent/skills/developing-nextjs/SKILL.md
🧬 Code graph analysis (14)
components/ui/media-player.tsx (1)
hooks/use-media-player.ts (1)
useMediaPlayer(69-173)
components/ui/media-player.test.tsx (1)
components/ui/media-player.tsx (1)
MediaPlayer(60-164)
hooks/use-aspect-ratio-dimensions.test.ts (2)
types/pollinations.ts (2)
AspectRatioOption(95-103)ModelConstraints(65-90)hooks/use-aspect-ratio-dimensions.ts (1)
useAspectRatioDimensions(77-263)
components/images/image-lightbox.tsx (2)
lib/utils.ts (1)
cn(11-13)components/ui/button.tsx (1)
Button(79-79)
components/ui/image-card.tsx (3)
components/ui/media-player.tsx (1)
isVideoContent(25-29)lib/analytics.ts (1)
trackPromptCopy(149-157)lib/utils.ts (1)
cn(11-13)
components/landing/models-section.tsx (1)
lib/config/models.ts (1)
MODEL_REGISTRY(345-681)
components/gallery/feed-cta.tsx (2)
lib/analytics.ts (2)
trackCtaView(116-121)trackCtaClick(126-131)lib/utils.ts (1)
cn(11-13)
lib/config/models.test.ts (1)
lib/config/models.ts (3)
MODEL_REGISTRY(345-681)getModel(690-692)getModelAspectRatios(704-706)
components/pricing/model-value-showcase.tsx (1)
lib/utils.ts (1)
cn(11-13)
components/landing/value-prop-section.tsx (1)
components/landing/scroll-reveal.tsx (1)
ScrollReveal(28-68)
components/ui/image-card.test.tsx (1)
components/ui/image-card.tsx (1)
ImageCard(69-366)
lib/config/models.ts (1)
types/pollinations.ts (1)
AspectRatioOption(95-103)
hooks/use-aspect-ratio-dimensions.ts (2)
types/pollinations.ts (2)
AspectRatio(17-29)AspectRatioOption(95-103)lib/config/standard-resolutions.ts (2)
StandardDimensions(15-18)getAllAspectRatioDefinitions(181-183)
hooks/use-media-player.test.ts (1)
hooks/use-media-player.ts (1)
useMediaPlayer(69-173)
🪛 LanguageTool
nano-banana-pro-spec.md
[style] ~60-~60: The word “because” means “for the reason that” and thus introduces redundancy.
Context: ...nap"** The reason
(REASON_IS_BECAUSE)
.agent/skills/adding-route/SKILL.md
[style] ~124-~124: ‘absolutely required’ might be wordy. Consider a shorter alternative.
Context: ...add 'use client'** to page.tsx unless absolutely required - If route already exists, stop and...
(EN_WORDINESS_PREMIUM_ABSOLUTELY_REQUIRED)
🪛 markdownlint-cli2 (0.18.1)
.agent/skills/developing-nextjs/SKILL.md
32-32: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🔇 Additional comments (74)
components/landing/faq-section.tsx (2)
24-24: LGTM!The copy update accurately reflects the product offering. No issues with this text change.
38-67: LGTM!The component is well-structured. Using
indexas the key is acceptable here since thefaqsarray is static and won't be dynamically reordered or filtered.components/studio/upgrade-modal.tsx (1)
38-38: LGTM!The branding text update to "Nano Banana Pro images/month" is consistent with the corresponding test file changes.
components/studio/upgrade-modal.test.tsx (4)
44-53: LGTM!Good practice resetting the mock in
beforeEachto ensure test isolation. This prevents state leakage between tests whenisStripeConfiguredis set tofalsein specific test cases.
67-74: LGTM!Test assertion correctly updated to match the new branding text in the component.
111-122: LGTM!Good improvement using
findByTextinstead ofgetByTextfor the async assertion. This properly handles the timing of the loading state transition and avoids potential flaky tests.
139-151: LGTM!Good test coverage for the Stripe configuration guard. The test properly verifies that:
- The checkout action is not called when Stripe is unconfigured
- Appropriate error logging and user feedback occur
components/landing/models-section.tsx (4)
11-11: LGTM!The border color change to
border-white/10aligns with the design system used throughout this component (matching lines 64, 72, 93, 112).
19-21: LGTM!Good use of
min-w-0combined withtruncateandblockto ensure proper text truncation within the flex container. The structural simplification is clean.
28-29: LGTM!Good use of
localeComparefor locale-aware sorting. SinceMODEL_REGISTRYis static configuration data, the filter/sort operations on each render are acceptable. If the registry grows significantly, consider memoizing withuseMemo.
61-61: LGTM!The responsive padding adjustments appropriately accommodate the floating header badge at different breakpoints. The border color change maintains consistency with the component's design system.
components/landing/value-prop-section.tsx (1)
34-34: Title update improves clarity.The change from "1,000+ Batch Queue" to "1,000 Image Batch Queue" is more descriptive and helps users understand the feature better.
components/studio/controls/aspect-ratio-selector.tsx (2)
18-44: LGTM!Icon imports are properly structured for tree-shaking, and the
LucideIcontype import is correctly used for typing the helper function.
239-275: LGTM!The enriched tooltip content with header, icon-tagged use cases, and custom state messaging is well-structured. The component correctly extends shadcn/ui's Tooltip composition pattern. Based on learnings, this aligns with the guideline to extend shadcn/ui via composition and variants.
.agent/skills/developing-nextjs/SKILL.md (1)
13-62: Well-structured skill documentation.The skill routing, core principles, and guardrails effectively capture Next.js App Router best practices and align well with the retrieved learnings about Server Components, client boundaries, and Convex integration patterns.
Based on learnings, this correctly emphasizes Server Components by default and client leaves for interactivity.
.agent/skills/adding-route/SKILL.md (3)
18-41: Clear and comprehensive route creation algorithm.The step-by-step checklist properly covers all essential files for a Next.js App Router route, including loading and error states. The verification step ensures build integrity before proceeding.
Based on learnings, this correctly uses
loading.tsxanderror.tsxfor route-segment UX.
43-107: Excellent route templates following Next.js best practices.All three templates (page.tsx, loading.tsx, error.tsx) correctly demonstrate:
- Server Component default for page.tsx
- Proper metadata export for SEO
- Client Component only where needed (error.tsx)
- Semantic HTML structure
Note: The past review comment about the brand name "Pixelstream" has been successfully addressed—line 50 now correctly shows "Bloom Studio".
Based on learnings, these templates properly use Server Components by default and add
'use client'only for the error boundary that needs interactivity.
122-127: Appropriate guardrails reinforce best practices.The guardrails correctly emphasize avoiding unnecessary
'use client'directives and include proper error handling for edge cases (route conflicts, build failures).components/ui/media-player.test.tsx (2)
2-2: LGTM!The
waitForimport is correctly added to support async assertions in the updated test.
92-122: Good adaptation for async video playback handling.The test correctly handles the async nature of
video.play()by:
- Using
async/awaitwithwaitForfor assertions- Properly mocking
play()to return a resolved Promise- Simulating state transitions via
dataset.pausedThis aligns well with the
useMediaPlayerhook's race-condition-safe implementation.hooks/use-media-player.ts (4)
1-4: LGTM!Appropriate "use client" directive for a hook that manages browser-specific state (video element refs, DOM events).
8-19: Well-documented interfaces.The props and return type interfaces are clearly documented with JSDoc comments explaining each property's purpose.
Also applies to: 24-44
84-90: Appropriate useEffect usage.This effect correctly handles URL changes by resetting state and clearing pending operations. This is a valid use of
useEffectfor synchronizing with an external value (the URL prop), not data fetching.
119-162: Excellent race-condition-safe play/pause implementation.The
handleVideoClickhandler properly:
- Tracks pending
play()Promises via ref- Awaits pending play before calling
pause()- Gracefully handles
AbortErrorfrom rapid interactions- Logs non-abort errors for debugging
- Clears the Promise ref correctly in both success and error paths
This effectively prevents the common "The play() request was interrupted by a call to pause()" browser error.
components/ui/media-player.tsx (4)
9-11: LGTM!Good documentation update explaining the hook-based architecture, and proper import of the new hook.
Also applies to: 17-17
77-91: Clean refactor to hook-based state management.The component now correctly delegates all media lifecycle state and handlers to
useMediaPlayer, keeping the component focused on rendering concerns only. This separation improves testability and reusability.
107-139: LGTM!Video rendering correctly uses
handleVideoClickfor the click handler to leverage the race-condition-safe play/pause logic from the hook.
141-163: LGTM!Image rendering appropriately uses the direct
onClickprop since images don't require the play/pause handling logic.hooks/use-media-player.test.ts (5)
1-8: LGTM!Proper imports and test setup with mock clearing before each test.
143-150: Good helper function for mock video creation.The
createMockVideohelper cleanly encapsulates mock video setup with configurablepausedstate.
243-275: Excellent AbortError handling test.This test correctly verifies that
AbortErrorfrom interruptedplay()calls is silently ignored, which is the expected behavior for rapid user interactions.
311-363: Critical race condition test.This test validates the core race-condition handling by:
- Starting a controlled play Promise
- Clicking to pause while play is pending
- Resolving play and verifying pause is called afterward
This ensures the "play() interrupted by pause()" error is properly prevented.
366-412: Good callback stability tests.These tests verify that:
- Memoized callbacks maintain referential stability across renders
- Callbacks correctly update when their dependencies change
This ensures proper integration with React's reconciliation and prevents unnecessary re-renders in consumer components.
convex/lib/videoPreview.ts (6)
32-52: Well-defined types with proper documentation.The interfaces are clearly typed with appropriate JSDoc comments. The
presetproperty correctly uses a string literal union for type safety.
58-74: Reasonable configuration defaults with clear documentation.The preset values are well-suited for their use cases. Using
veryfastfor both configs is a sensible choice given Convex action time constraints.
145-185: Well-implemented settlement pattern for Promise handling.The settlement guard (lines 146-153) correctly prevents double-resolve/reject scenarios, and killing the ffmpeg process on error ensures proper cleanup.
207-215: Proper resource cleanup with error suppression.The
finallyblock ensures temp files are cleaned up regardless of success or failure. Using.catch(() => {})to suppress cleanup errors is appropriate here since failing to delete temp files shouldn't cause the operation to fail.
224-228: Clean delegation to the main function.Simple and correct wrapper that delegates to the parameterized implementation.
239-243: Simple and clear threshold check.The 5MB threshold is reasonable, and the inline calculation
5 * 1024 * 1024is clearer than using a magic number.app/pricing/pricing-data.tsx (1)
51-54: LGTM! Data updates are consistent across the file.The model count update to "13+" is consistently reflected in Pro features (line 51), FAQ content (line 93), and the feature comparison table (line 131). The new "Custom prompt library" feature is appropriately added.
Also applies to: 93-93, 131-131
components/pricing/model-value-showcase.tsx (3)
137-165: Animation cleanup is adequate.The
rafIdtracking and cleanup viacancelAnimationFrameaddresses the memory leak concern. The firstanimate()call is synchronous, so it completes atomically before any unmount could interrupt. Subsequent frames are properly tracked and cancelled on cleanup.
174-249: LGTM!Good use of
next/imagewithfillprop per guidelines. The emptyalt=""is appropriate for decorative logos. The responsive styling and hover states are well implemented.
124-130: LGTM! Tier categorization is sound.The filtering logic correctly partitions all 13 models into non-overlapping tiers with clear boundaries. The alphabetical sort before filtering ensures consistent ordering within each tier.
components/gallery/image-history.tsx (1)
64-70: LGTM! Hover scale effect removed for cleaner UX.The removal of
group-hover:scale-110keeps the hover interaction focused on the shadow/overlay effects without the zoom, which aligns with the broader UI refinement direction in this PR. Thetransition-transform duration-500class on line 69 is now unused for this image but doesn't cause any issues.components/ui/image-card.test.tsx (1)
116-127: LGTM! Test correctly validates video element attributes and properties.Good approach:
- Casting to
HTMLVideoElementfor type-safe property access- Correctly checking
video.mutedas a boolean property rather than an attribute (sincemutedis a reflected property in the DOM)- Test assertions align with the
ImageCardcomponent's video rendering behavior (autoPlay, loop, muted)components/gallery/feed-cta.tsx (2)
21-54: Well-implemented CTA with proper stale closure fix.The
isVisibleRefpattern correctly prevents the stale closure issue whereisVisiblestate would be captured at effect creation time. The effect properly:
- Gates on auth state
- Uses ref for mutable tracking across scroll events and timer
- Cleans up both the event listener and timeout
- Tracks analytics only once via the ref guard
80-88: Verifymix-blend-differenceaccessibility across theme backgrounds.The
mix-blend-differencetechnique for dynamic text contrast is clever, but it may produce low-contrast or unexpected colors on certain background combinations (e.g., mid-gray backgrounds can result in near-invisible text).Consider testing this across both light and dark themes with various scroll positions to ensure WCAG contrast compliance.
components/gallery/feed-cta.test.tsx (2)
41-49: LGTM! Proper timer setup and teardown.Good test hygiene with
vi.useFakeTimers()inbeforeEachandvi.useRealTimers()inafterEach. This prevents timer state from leaking between tests.Note:
afterEachis used but not explicitly imported on line 4. This works because Vitest provides it globally, but for consistency you could add it to the import.
116-132: Good test for the single-invocation guard.This test validates that the
isVisibleRefpattern in the component correctly prevents duplicatetrackCtaViewcalls when multiple scroll events fire after the threshold is crossed.components/images/image-lightbox.tsx (4)
87-101: LGTM! Progressive loading state management is well-structured.The thumbnail/full-res URL resolution logic correctly handles:
- Images with separate thumbnail (originalUrl differs from url)
- Images without separate thumbnail (fallback to url for both layers)
- State reset when the image changes via the effect dependency array
197-216: LGTM! Thumbnail layer with blur-up effect.Good implementation of the progressive loading pattern:
- Thumbnail loads with
priorityfor immediate display- Applies subtle
blur-[2px]while full-res loads- Fades out with
opacity-0 pointer-events-none absolute inset-0when full-res is ready- Correctly guarded by
hasSeparateThumbnail && thumbnailUrl
338-382: LGTM! Clean auth-gated UI with appropriate sign-in prompt.The conditional rendering provides:
- Full functionality for authenticated users
- Clear sign-in call-to-action for unauthenticated users with helpful tooltip
- Proper
stopPropagationon the link to prevent lightbox closeBased on learnings, this follows the pattern of using auth hooks appropriately in client components.
259-264: Verify spinner visibility for images without separate thumbnails.The condition
(!isVideo && !isThumbnailLoaded && !isFullResLoaded)will show the spinner when:
- No separate thumbnail exists →
isThumbnailLoadedstaysfalseforever- Full-res hasn't loaded yet →
isFullResLoadedisfalseThis is correct behavior, but worth confirming that the spinner correctly disappears once the single full-res image loads (when
isFullResLoadedbecomestrue).components/ui/image-card.tsx (5)
21-44: LGTM on type definitions.The
ImageCardDataandImageCardPropsinterfaces are well-typed with appropriate JSDoc comments. TheoriginalUrloptional field andpriorityprop additions are cleanly documented.Also applies to: 46-61
99-106: Prompt copy tracking integration is correct.The
trackPromptCopy("feed", !!isSignedIn)call properly convertsisSignedIn(which could beundefinedinitially) to a boolean, matching the expected signature fromlib/analytics.ts.
144-154: LGTM on event handlers.The
useCallbackwrappers with empty dependencies are appropriate here since the state setters are stable.
191-211: Good loading state pattern with Skeleton and video support.The Skeleton placeholder with absolute positioning and opacity transitions provides smooth loading UX. Video handling with
muted,playsInline, andautoPlayis appropriate for preview cards, and the behavior is documented in the component's JSDoc.
316-343: Auth-gated favorites with tooltip feedback is well implemented.The conditional rendering pattern for showing different UI based on authentication state is clean, and the tooltip messaging ("Sign in to save favorites" vs "Add to favorites") provides clear user guidance.
nano-banana-pro-spec.md (1)
19-25: Verify duplicate input dimensions for different ratios.Lines 19 (4:3) and 25 (5:4) both show the same input dimensions
$1152 \times 896$, but they're different aspect ratios. While the outputs differ, having identical inputs for both 4:3 and 5:4 seems unusual.Additionally, line 24 (4:5) also shows input
$896 \times 1152$which is the portrait version of the same dimensions.Please verify these are the intended values, as 4:3 ≈ 1.33, 5:4 = 1.25, and 4:5 = 0.8 are distinct ratios that would typically have different input dimensions.
lib/config/models.ts (6)
56-57: LGTM!The new
supportsReferenceImageproperty follows the established pattern for optional boolean flags in the interface (similar tosupportsAudio,supportsInterpolation).
228-263: LGTM!The
NANOBANANA_ASPECT_RATIOSpreset is well-documented and correctly excludes9:21(unsupported per spec) andcustom(since the model uses fixed output dimensions). The pixel calculations in the documentation table are accurate.
265-333: LGTM!Excellent documentation for the tiered dimension system. The HD (1K) tier default dimensions match the spec, and the exclusion of
9:21andcustomis correct for this fixed-dimension model.
400-422: LGTM!The
nanobanana-promodel configuration correctly implements the spec with:
- ~17.2MP pixel budget
- Dimension limits from the 1K/4K tier extremes (672-6336)
dimensionsEnabled: falsefor fixed dimensions- Three supported tiers mapping to the spec's 1K/2K/4K system
631-653: LGTM!The
nanobananamodel configuration correctly reflects the spec:
- Fixed dimensions with
dimensionsEnabled: falseandoutputCertainty: "exact"- Single
"hd"tier for the fixed output system- Proper dimension constraints from the 21:9 ratio extremes
722-727: LGTM!The new accessor follows the established pattern used by
getModelSupportsNegativePrompt, with proper null-safe handling via??.components/studio/features/generation/controls-view.tsx (1)
365-388: LGTM!Clean implementation of conditional rendering for the Reference section based on model capability. The optional chaining on
selectedModelData?.supportsReferenceImagecorrectly handles the case where the model might not be found. This improves UX by hiding irrelevant controls for models that don't support reference images.lib/config/models.test.ts (4)
58-60: LGTM!Display name tests correctly updated to match the new naming convention ("Nano Banana" and "Nano Banana Pro").
255-269: LGTM!Comprehensive tests for Nano Banana Pro constraints that accurately reflect the spec and model configuration. Good use of descriptive comments linking expectations to spec documentation.
271-287: LGTM!Tests correctly validate Nano Banana's fixed-dimension constraints including the important
outputCertainty: "exact"property that distinguishes it from other models.
355-400: LGTM!Thorough tests for both Nano Banana preset variants:
- Correctly validates exclusion of unsupported ratios (
9:21,custom)- Verifies specific dimension values from the spec
- Covers key aspect ratios (1:1, 16:9, 9:16, 21:9)
hooks/use-aspect-ratio-dimensions.test.ts (3)
238-264: LGTM! Well-structured test fixtures for fixed-dimension models.The test data accurately represents the Nano Banana model constraints with
dimensionsEnabled: falseandoutputCertainty: "exact", which correctly triggers the fixed-dimension code path in the hook.
265-361: LGTM! Comprehensive coverage for single-tier fixed-dimension behavior.The tests correctly verify that exact dimensions from
availableRatiosare returned without recalculation whendimensionsEnabled === false. Good assertions with specific expected values and clear comments explaining what incorrect behavior would look like.
363-500: LGTM! Thorough parameterized tests for multi-tier dimension scaling.The test matrix covers all 10 aspect ratios × 3 tiers (30 dimension checks) with explicit expected values. The scaling relationship (HD=1×, 2K=2×, 4K=4×) is clearly documented and verified.
hooks/use-aspect-ratio-dimensions.ts (1)
147-154: LGTM! Clear documentation of the fixed vs dynamic dimension behavior.The JSDoc accurately describes the two code paths: fixed-dimension models using
availableRatios(with tier scaling for multi-tier) and dynamic-dimension models using constraint-based calculations.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
components/studio/layout/studio-shell.tsx (1)
221-237: Avoidanytype; use the existing type union instead.Using
anydefeats TypeScript's type safety. Theparamsobject should be typed with the existing types already imported.Suggested fix
- const params: any = { + const params: ImageGenerationParams | VideoGenerationParams = { prompt, negativePrompt: negativePrompt || undefined, model: generationSettings.model, width: generationSettings.width, height: generationSettings.height, seed: effectiveSeed, enhance: generationSettings.options.enhance, private: generationSettings.options.private, safe: generationSettings.options.safe, image: generationSettings.referenceImage, // Video-specific parameters duration: generationSettings.videoSettings.duration, audio: generationSettings.videoSettings.audio, aspectRatio: generationSettings.aspectRatio, lastFrameImage: generationSettings.videoReferenceImages.lastFrame, - } + } satisfies ImageGenerationParams | VideoGenerationParamsAs per coding guidelines,
anyshould be avoided in TypeScript; useunknownand narrow, or proper type unions.components/studio/controls/collapsible-section.test.tsx (1)
4-4: Missingviimport from vitest.The test uses
vi.fn()on line 212, butviis not included in the import statement.Proposed fix
-import { describe, expect, it } from "vitest" +import { describe, expect, it, vi } from "vitest"todo/nsfw-sensitive-content-detection.md (1)
397-411: Plan-to-implementation discrepancy: contextual score and thresholds differ.The plan specifies:
- Contextual body part + modifier score:
+0.5(line 398)- Vision analysis scheduling range:
> 0.3 && < 0.8(line 440)But the implementation uses:
- Contextual score:
+0.7(nsfwDetection.ts line 73)- Scheduling range:
>= 0.3 && < 0.9(generatedImages.ts line 207)Consider updating the documentation to reflect the actual implementation, or vice versa if the plan values were intentional.
🤖 Fix all issues with AI agents
In `@components/studio/layout/studio-shell.tsx`:
- Around line 352-356: Tests are failing because the test mock for
"@/components/studio" does not export BatchConfigButton; update the mock in
components/studio/layout/studio-shell.test.tsx to include BatchConfigButton
(e.g., add BatchConfigButton: vi.fn(() => <div data-testid="batch-config-button"
/>)) or switch to a partial mock using importOriginal/import to spread the real
exports and only override what you need so BatchConfigButton is available to the
StudioShell tests.
In `@convex/crons.ts`:
- Around line 20-25: Wrap the body of analyzeRecentImages in a try-catch to
prevent thrown errors from ctx.runQuery or ctx.scheduler.runAfter from crashing
the cron; catch and log failures from the initial ctx.runQuery call (including
the error) and inside the loop catch and log failures per image when calling
ctx.scheduler.runAfter for internal.contentAnalysis.analyzeImage (include the
image._id in the log) so a single failure won’t block subsequent scheduling or
future cron runs.
In `@convex/generatedImages.ts`:
- Around line 865-874: The current filter in getUnanalyzedImages only matches
documents where isTagged is missing (undefined) and therefore misses documents
where isTagged is explicitly false; update the handler's query on
"generatedImages" to include both cases (isTagged === undefined OR isTagged ===
false) — e.g., replace the single eq check with a logical OR combining
q.eq(q.field("isTagged"), undefined) and q.eq(q.field("isTagged"), false) so
unanalyzed images explicitly marked false are returned.
In `@convex/lib/nsfwDetection.ts`:
- Around line 86-94: The explanatory comment above the return (near the object
with isSensitive, confidence, matchedTerms, detectionMethod) has the wrong
contextual score; update that comment to use the actual contextual score (0.7)
so the example reads "suggestive (0.4) + contextual (0.7) = 1.1" (or rephrase to
avoid specific arithmetic) to match the value assigned earlier for contextual
and avoid confusion.
In `@convex/lib/openrouter.ts`:
- Around line 27-57: The fetch call that posts to
"https://openrouter.ai/api/v1/chat/completions" (using OPENROUTER_API_KEY,
ANALYSIS_MODEL and imageUrl) has no timeout and can hang; wrap the request with
an AbortController, pass its signal to fetch, start a timer (e.g., const
timeoutId = setTimeout(() => controller.abort(), <ms>)) before calling fetch,
clear the timeout after fetch completes, and handle the abort error path so the
surrounding function returns a controlled timeout error instead of hanging.
In `@hooks/queries/use-image-history.ts`:
- Around line 93-100: Tests for usePublicFeed fail because the test mock for
"convex/react" doesn't export useQuery; update the test's mock setup to include
useQuery alongside usePaginatedQuery so the hook can call the mocked function.
Specifically, in the vi.mock(import("convex/react"), ...) block add an exported
useQuery mock (e.g., useQuery: vi.fn()) and ensure usePaginatedQuery remains
mocked so usePublicFeed can run without importing the real convex/react
implementation.
♻️ Duplicate comments (2)
components/studio/controls/aspect-ratio-selector.tsx (1)
47-82: Return type still includes| nullbut function never returnsnull.This was previously flagged. The function always returns
Frameas a fallback (line 81), making the| nullin the return type incorrect. This causes an unnecessary null check downstream at line 276 ({Icon && <Icon .../>}).components/ui/image-card.tsx (1)
365-373: Invalid HTML:<button>nested inside<a>remains unaddressed.This was flagged in a previous review. The
<Link>wrapping<Button>produces invalid HTML (<a><button>...</button></a>). Use theasChildpattern or convert to anchor styling as suggested previously.
🧹 Nitpick comments (18)
components/studio/controls/model-selector.tsx (2)
177-185: Consider using a design system shadow token instead of hardcoded value.The shadow
shadow-[0_20px_60px_0px_rgba(0,0,0,0.8)]is quite aggressive (80% black opacity with 60px blur) and may appear too heavy in light mode. Consider using one of Tailwind's built-in shadow utilities (e.g.,shadow-lgorshadow-xl) or a design token for consistency across the UI.💡 Suggested alternative
<TooltipContent side="bottom" - className="p-3 max-w-[240px] bg-popover border border-border dark:border-white/15 text-popover-foreground shadow-[0_20px_60px_0px_rgba(0,0,0,0.8)] animate-in fade-in-0 zoom-in-95 duration-200" + className="p-3 max-w-[240px] bg-popover border border-border dark:border-white/15 text-popover-foreground shadow-xl animate-in fade-in-0 zoom-in-95 duration-200" >
227-238: Comment mismatch: grid is fixed, not responsive.The comment on line 227 states "2-column responsive grid" but the implementation uses a fixed
grid-cols-2without responsive breakpoints. If this component is used in narrow viewports, the fixed 2-column layout might cause buttons to be cramped. If the fixed layout is intentional (e.g., for a controlled-width sidebar), consider updating the comment to match.💡 Option A: Update comment to match implementation
- // Compact variant using shadcn buttons with 2-column responsive grid + // Compact variant using shadcn buttons with fixed 2-column grid💡 Option B: Restore responsive behavior if needed on mobile
- className="grid grid-cols-2 gap-1.5" + className="grid grid-cols-1 sm:grid-cols-2 gap-1.5"app/globals.css (1)
310-338: Consider clarifying utility naming to indicate mode.The glass-effect utilities work well, but the naming could be clearer.
glass-effectis for light mode andglass-effect-homeis for dark mode, which isn't immediately obvious from the names.💡 Suggestion: Rename for clarity
Consider renaming to make the mode explicit, or use a single utility with dark mode variants:
Option 1 - Explicit naming:
`@utility` glass-effect-light { /* current glass-effect */ } `@utility` glass-effect-dark { /* current glass-effect-home */ }Option 2 - Single utility with dark variant (preferred if both are used contextually):
`@utility` glass-effect { /* Light mode styles */ background: linear-gradient(...); /* ... */ .dark & { /* Dark mode styles */ background: linear-gradient(...); } }Based on learnings, design tokens should be kept centralized - the utilities themselves are well-structured, just the naming could better communicate intent.
components/studio/controls/aspect-ratio-selector.tsx (2)
155-157: Indentation inconsistency.Lines 155-158 have extra indentation (4 spaces instead of 2) compared to the rest of the component body. This doesn't affect functionality but creates visual inconsistency.
🧹 Fix indentation
- // Active selection styling - matching the emerald green from other controls - const activeClasses = "bg-emerald-500/15 text-emerald-700 border border-emerald-500 dark:bg-emerald-500/20 dark:text-emerald-400 dark:border-emerald-500 ring-1 ring-emerald-500/20" - - return ( + // Active selection styling - matching the emerald green from other controls + const activeClasses = "bg-emerald-500/15 text-emerald-700 border border-emerald-500 dark:bg-emerald-500/20 dark:text-emerald-400 dark:border-emerald-500 ring-1 ring-emerald-500/20"; + + return (
269-280: Unnecessary null check forIcon.Since
getIconForTagalways returns an icon (falls back toFrame), the conditional{Icon && <Icon .../>}on line 276 is redundant. Once the return type is corrected, simplify to just render the icon directly.♻️ Simplify icon rendering (after fixing return type)
{tags.map((tag) => { const Icon = getIconForTag(tag); return ( <div key={tag} className="flex items-center gap-2 text-xs text-muted-foreground" > - {Icon && <Icon className="h-3.5 w-3.5 text-muted-foreground/70" />} + <Icon className="h-3.5 w-3.5 text-muted-foreground/70" /> <span>{tag}</span> </div> ); })}components/studio/batch/batch-config-button.tsx (1)
44-50: Extract shared constants to avoid duplication.The values
1and1000are hardcoded here but already defined asMIN_BATCH_SIZEandMAX_BATCH_SIZEconstants inbatch-mode-panel.tsx. Consider exporting and reusing those constants to maintain consistency.♻️ Suggested refactor
In
batch-mode-panel.tsx, export the constants:-/** Minimum batch size */ -const MIN_BATCH_SIZE = 1 -/** Maximum batch size */ -const MAX_BATCH_SIZE = 1000 +/** Minimum batch size */ +export const MIN_BATCH_SIZE = 1 +/** Maximum batch size */ +export const MAX_BATCH_SIZE = 1000Then in this file:
-import { type BatchModeSettings } from "./batch-mode-panel" +import { type BatchModeSettings, MIN_BATCH_SIZE, MAX_BATCH_SIZE } from "./batch-mode-panel"const handleCountChange = (e: React.ChangeEvent<HTMLInputElement>) => { const value = parseInt(e.target.value, 10) if (!isNaN(value)) { - const clampedValue = Math.min(Math.max(value, 1), 1000) + const clampedValue = Math.min(Math.max(value, MIN_BATCH_SIZE), MAX_BATCH_SIZE) onSettingsChange({ ...settings, count: clampedValue }) } }Also update the Input constraints on lines 129-130 to use the constants.
components/studio/layout/studio-shell.tsx (1)
369-370: Consider extracting complex progress calculation for readability.The nested ternary on line 370 is hard to parse at a glance. Extracting it to a variable would improve clarity.
Suggested refactor
+ const batchProgress = batchMode.isBatchActive && batchMode.batchProgress.totalCount > 0 + ? (batchMode.batchProgress.completedCount / batchMode.batchProgress.totalCount) * 100 + : undefined + const canvasContent = ( <CanvasFeature currentImage={galleryState.currentImage} isGenerating={isGenerating || batchMode.isBatchActive} - progress={batchMode.isBatchActive ? (batchMode.batchProgress.totalCount > 0 ? (batchMode.batchProgress.completedCount / batchMode.batchProgress.totalCount) * 100 : 0) : undefined} + progress={batchProgress} onOpenLightbox={studioUI.openLightbox} onRegenerate={handleRegenerate} /> )components/studio/controls/collapsible-section.tsx (1)
104-113: Redundant conditional in collapsed chevron.The
isExpanded ? "rotate-90" : ""condition on line 109 is inside the!isExpandedblock, soisExpandedis alwaysfalsehere, making this condition always evaluate to"". Consider simplifying.Suggested simplification
<ChevronRight data-testid={testId ? `${testId}-chevron` : undefined} className={cn( - "h-4 w-4 text-muted-foreground/50 transition-transform shrink-0", - isExpanded ? "rotate-90" : "", + "h-4 w-4 text-muted-foreground/50 shrink-0", !isExpanded && (collapsedContent || rightContent) ? "ml-2" : "ml-auto" )} />components/ui/sensitive-content-overlay.tsx (2)
4-4: Unused imports:EyeandEyeOff.These icons are imported but never used in the component.
Remove unused imports
-import { Eye, EyeOff, AlertTriangle } from "lucide-react" +import { AlertTriangle } from "lucide-react"
20-34: Accessibility: clickable overlay is not keyboard accessible.The overlay uses a
<div>withonClickbut lacks keyboard support (onKeyDown,role="button",tabIndex). Users navigating via keyboard cannot reveal the content.Add keyboard accessibility
<div className={cn( "absolute inset-0 z-10 flex flex-col items-center justify-center", "bg-black/80 backdrop-blur-md", "rounded-lg cursor-pointer group hover:bg-black/70 transition-all", className )} + role="button" + tabIndex={0} onClick={(e) => { e.preventDefault() e.stopPropagation() setRevealed(true) onReveal?.() }} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault() + e.stopPropagation() + setRevealed(true) + onReveal?.() + } + }} >components/ui/image-card.tsx (1)
216-229: Consider addingpreloadattribute for video performance.The video autoplay implementation is correct (muted for browser compliance, playsInline for iOS). Consider adding
preload="metadata"to reduce initial bandwidth while still allowing quick playback.Optional: Add preload attribute
<video src={image.url} muted loop playsInline autoPlay + preload="metadata" onCanPlay={handleLoad}convex/contentAnalysis.test.ts (3)
35-35: Avoidas anycast; use proper type narrowing.Per coding guidelines, avoid
anywhich defeats TypeScript's purpose. Consider defining a type for the mocked action config:♻️ Suggested fix
+// At top of file or in a shared test utils +interface MockedAction { + handler: (...args: unknown[]) => unknown; + args?: Record<string, unknown>; +} + it("should export analyzeImage action", async () => { const { analyzeImage } = await import("./contentAnalysis"); expect(analyzeImage).toBeDefined(); - // Since we mocked internalAction to return config, we can check it has handler - expect((analyzeImage as any).handler).toBeInstanceOf(Function); + const action = analyzeImage as MockedAction; + expect(action.handler).toBeInstanceOf(Function); });
41-41: Sameas anyissue here.Apply the same typed approach for consistency.
♻️ Suggested fix
it("should export analyzeRecentImages action", async () => { const { analyzeRecentImages } = await import("./contentAnalysis"); expect(analyzeRecentImages).toBeDefined(); - expect((analyzeRecentImages as any).handler).toBeInstanceOf(Function); + const action = analyzeRecentImages as MockedAction; + expect(action.handler).toBeInstanceOf(Function); });
6-6: Consider typing the mock config parameter.The
anytype in the mock functions also bypasses type safety.♻️ Optional improvement
+interface ActionConfig { + args?: Record<string, unknown>; + handler: (...args: unknown[]) => unknown; +} + vi.mock("./_generated/server", () => ({ - internalAction: (config: any) => config, - internalMutation: (config: any) => config, - query: (config: any) => config, + internalAction: (config: ActionConfig) => config, + internalMutation: (config: ActionConfig) => config, + query: (config: ActionConfig) => config, }));convex/contentAnalysis.ts (1)
47-49: Silent error swallowing may hide persistent failures.The catch block logs but doesn't track failed analyses. If the same image repeatedly fails (e.g., invalid URL, API issues), it will be re-queued by
analyzeRecentImagesindefinitely.Consider either:
- Marking failed images with an
analysisErrorfield to skip them in future batches- Implementing a retry counter with backoff
convex/lib/openrouter.ts (1)
70-74: Validate API response against expected schema.
JSON.parsereturnsanyand the result isn't validated. If the LLM returns unexpected values (e.g.,"moderate"instead of"partial"), they'll propagate through the system.♻️ Suggested validation helper
function validateAnalysisResult(data: unknown): ContentAnalysisResult { if (typeof data !== 'object' || data === null) { throw new Error('Invalid analysis response format'); } const obj = data as Record<string, unknown>; const nudityValues = ['none', 'partial', 'full'] as const; const sexualValues = ['none', 'suggestive', 'explicit'] as const; const violenceValues = ['none', 'mild', 'graphic'] as const; const nudity = nudityValues.find(v => v === obj.nudity); const sexual_content = sexualValues.find(v => v === obj.sexual_content); const violence = violenceValues.find(v => v === obj.violence); if (!nudity || !sexual_content || !violence) { throw new Error(`Invalid classification values: ${JSON.stringify(obj)}`); } return { nudity, sexual_content, violence, confidence: typeof obj.confidence === 'number' ? obj.confidence : 0, reasoning: typeof obj.reasoning === 'string' ? obj.reasoning : '', }; }Then use it:
- return JSON.parse(jsonContent); + return validateAnalysisResult(JSON.parse(jsonContent));convex/generatedImages.ts (2)
177-199: Consider extracting policy comments to a doc comment or constant.The inline comments explaining the tagging policy (lines 178-196) are valuable but verbose. Consider consolidating them into a doc comment on the function or a named constant for the thresholds to improve readability.
♻️ Suggested approach
/** * Tagging policy thresholds: * - confidence < 0.3: Safe pass (isTagged=true, isSensitive=false) * - confidence 0.3-0.9: Borderline, schedule vision analysis (isTagged=false) * - confidence >= 0.9: Explicit (isTagged=true, isSensitive=true) */ const SAFE_THRESHOLD = 0.3; const EXPLICIT_THRESHOLD = 0.9; // Then in the handler: isTagged: promptAnalysis.confidence < SAFE_THRESHOLD || promptAnalysis.confidence >= EXPLICIT_THRESHOLD,
617-628: Avoidascast; the manual mapping already satisfies the type.The
as EnrichedImage[]cast on line 622 violates the coding guideline to avoidascasts. Since you're manually constructing objects with all requiredEnrichedImagefields, the cast is unnecessary if TypeScript can infer the type.♻️ Suggested fix
- // Enrich with user info (we already have the user), cast to EnrichedImage for helper - const enrichedPage = paginatedResult.page.map((image) => ({ + // Enrich with user info (we already have the user) + const enrichedPage: EnrichedImage[] = paginatedResult.page.map((image) => ({ ...image, ownerName: user.username ?? "Anonymous", ownerPictureUrl: user.pictureUrl ?? null, - })) as EnrichedImage[] + }))As per coding guidelines, prefer explicit type annotations over
ascasts.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
convex/_generated/api.d.tsis excluded by!**/_generated/**
📒 Files selected for processing (32)
app/globals.csscomponents/gallery/paginated-image-grid.tsxcomponents/studio/batch/batch-config-button.tsxcomponents/studio/batch/batch-mode-panel.tsxcomponents/studio/batch/index.tscomponents/studio/controls/aspect-ratio-selector.tsxcomponents/studio/controls/collapsible-section.test.tsxcomponents/studio/controls/collapsible-section.tsxcomponents/studio/controls/model-selector.tsxcomponents/studio/controls/options-panel.tsxcomponents/studio/controls/prompt-section.tsxcomponents/studio/controls/resolution-tier-selector.tsxcomponents/studio/features/canvas/canvas-feature.tsxcomponents/studio/features/canvas/canvas-view.tsxcomponents/studio/features/generation/controls-view.tsxcomponents/studio/index.tscomponents/studio/layout/studio-header.tsxcomponents/studio/layout/studio-layout.tsxcomponents/studio/layout/studio-shell.tsxcomponents/ui/image-card.tsxcomponents/ui/sensitive-content-overlay.tsxconvex/contentAnalysis.test.tsconvex/contentAnalysis.tsconvex/crons.tsconvex/generatedImages.tsconvex/lib/nsfwDetection.test.tsconvex/lib/nsfwDetection.tsconvex/lib/openrouter.tsconvex/schema.tsconvex/users.tshooks/queries/use-image-history.tstodo/nsfw-sensitive-content-detection.md
🚧 Files skipped from review as they are similar to previous changes (2)
- components/studio/features/generation/controls-view.tsx
- components/studio/layout/studio-layout.tsx
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: Always leverage TypeScript (strict mode); avoid usage ofanywhich defeats the point of using TypeScript.
Let TypeScript inference work; export explicit prop types for shared/public components.
Noanytype in TypeScript. Useunknownand narrow instead.
Avoidascasts in TypeScript; prefersatisfies, generics, and runtime validation.
@ts-expect-errorallowed only with a comment explaining why + link/TODO to remove.
Files:
components/studio/index.tscomponents/studio/batch/batch-mode-panel.tsxcomponents/studio/features/canvas/canvas-feature.tsxcomponents/ui/sensitive-content-overlay.tsxcomponents/studio/batch/batch-config-button.tsxhooks/queries/use-image-history.tsconvex/lib/nsfwDetection.tscomponents/studio/controls/options-panel.tsxcomponents/studio/controls/resolution-tier-selector.tsxconvex/contentAnalysis.tscomponents/studio/features/canvas/canvas-view.tsxcomponents/studio/controls/collapsible-section.tsxcomponents/studio/batch/index.tsconvex/contentAnalysis.test.tsconvex/generatedImages.tscomponents/studio/controls/collapsible-section.test.tsxcomponents/studio/layout/studio-header.tsxconvex/schema.tscomponents/studio/layout/studio-shell.tsxconvex/crons.tscomponents/ui/image-card.tsxconvex/lib/nsfwDetection.test.tscomponents/studio/controls/prompt-section.tsxcomponents/gallery/paginated-image-grid.tsxconvex/lib/openrouter.tscomponents/studio/controls/aspect-ratio-selector.tsxconvex/users.tscomponents/studio/controls/model-selector.tsx
**/*.{tsx,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Avoid
useEffectfor app data fetching. Use it for browser-only side effects (subscriptions, observers, localStorage).
Files:
components/studio/batch/batch-mode-panel.tsxcomponents/studio/features/canvas/canvas-feature.tsxcomponents/ui/sensitive-content-overlay.tsxcomponents/studio/batch/batch-config-button.tsxcomponents/studio/controls/options-panel.tsxcomponents/studio/controls/resolution-tier-selector.tsxcomponents/studio/features/canvas/canvas-view.tsxcomponents/studio/controls/collapsible-section.tsxcomponents/studio/controls/collapsible-section.test.tsxcomponents/studio/layout/studio-header.tsxcomponents/studio/layout/studio-shell.tsxcomponents/ui/image-card.tsxcomponents/studio/controls/prompt-section.tsxcomponents/gallery/paginated-image-grid.tsxcomponents/studio/controls/aspect-ratio-selector.tsxcomponents/studio/controls/model-selector.tsx
{app/**/*.{tsx,jsx},**/*.css}
📄 CodeRabbit inference engine (CLAUDE.md)
Use Tailwind CSS utilities in JSX; extract repetition into components, not
@apply.
Files:
app/globals.css
**/*.css
📄 CodeRabbit inference engine (CLAUDE.md)
Theme via CSS variables; keep design tokens centralized.
Files:
app/globals.css
🧠 Learnings (12)
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/*.{tsx,jsx} : Use `useQuery` / `useMutation` in Client Components for client reactivity with Convex.
Applied to files:
components/ui/sensitive-content-overlay.tsxcomponents/ui/image-card.tsxcomponents/gallery/paginated-image-grid.tsx
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/*.{tsx,jsx} : Use `useOptimistic` in React 19 for instant UI feedback when appropriate.
Applied to files:
components/ui/sensitive-content-overlay.tsxcomponents/ui/image-card.tsx
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/*.{tsx,jsx} : Use `use` to read a Promise/Context during render only where Suspense semantics make sense in React 19.
Applied to files:
components/ui/sensitive-content-overlay.tsx
📚 Learning: 2025-12-26T08:12:21.228Z
Learnt from: Simplereally
Repo: Simplereally/pixelstream PR: 1
File: convex/users.ts:24-46
Timestamp: 2025-12-26T08:12:21.228Z
Learning: Convex schemas do not support schema-level unique constraints (no .unique() on index definitions). Enforce uniqueness in mutation logic using a check-then-insert pattern, which is safe because Convex mutations are serialized with ACID guarantees. Apply this guidance to all Convex schema and mutation code (not just convex/schema.ts) to ensure data integrity.
Applied to files:
convex/lib/nsfwDetection.tsconvex/contentAnalysis.tsconvex/contentAnalysis.test.tsconvex/generatedImages.tsconvex/schema.tsconvex/crons.tsconvex/lib/nsfwDetection.test.tsconvex/lib/openrouter.tsconvex/users.ts
📚 Learning: 2025-12-26T08:12:21.228Z
Learnt from: Simplereally
Repo: Simplereally/pixelstream PR: 1
File: convex/users.ts:24-46
Timestamp: 2025-12-26T08:12:21.228Z
Learning: Convex mutations provide serializable isolation with optimistic concurrency control (OCC). The check-then-insert pattern (query for existing record, then conditionally insert) is safe from race conditions in Convex because concurrent mutations that conflict are retried and execute as if they ran in serial order. Apply this pattern for lazy initializations (e.g., getOrCreateUser patterns) in convex/* files, avoiding additional unique constraints. Ensure the mutation is idempotent and retriable.
Applied to files:
convex/lib/nsfwDetection.tsconvex/contentAnalysis.tsconvex/contentAnalysis.test.tsconvex/generatedImages.tsconvex/schema.tsconvex/crons.tsconvex/lib/nsfwDetection.test.tsconvex/lib/openrouter.tsconvex/users.ts
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/*.{tsx,jsx} : Use shadcn/ui as the base component library; extend via composition + variants.
Applied to files:
components/studio/controls/collapsible-section.tsxcomponents/studio/layout/studio-header.tsxcomponents/ui/image-card.tsxcomponents/studio/controls/aspect-ratio-selector.tsxcomponents/studio/controls/model-selector.tsx
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/{actions,route}.{ts,tsx} : Call Convex via `fetchMutation` / `fetchAction` in Server Actions / Route Handlers when you must mutate from the server boundary.
Applied to files:
convex/generatedImages.ts
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/*.{tsx,jsx} : Prefer Convex optimistic updates (`useMutation(...).withOptimisticUpdate(...)`) over duplicating server logic in ad-hoc local state.
Applied to files:
convex/generatedImages.tscomponents/gallery/paginated-image-grid.tsxconvex/users.ts
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/*.{tsx,jsx} : Use `fetchQuery(...)` in Server Components for server-only (non-reactive) rendering with Convex.
Applied to files:
convex/generatedImages.tscomponents/gallery/paginated-image-grid.tsxtodo/nsfw-sensitive-content-detection.md
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to **/*.{ts,tsx} : `ts-expect-error` allowed only with a comment explaining why + link/TODO to remove.
Applied to files:
components/ui/image-card.tsx
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/*.{tsx,jsx} : Use `next/image` for images in Next.js.
Applied to files:
components/ui/image-card.tsx
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to **/*.css : Theme via CSS variables; keep design tokens centralized.
Applied to files:
app/globals.css
🧬 Code graph analysis (10)
components/ui/sensitive-content-overlay.tsx (1)
lib/utils.ts (1)
cn(11-13)
components/studio/batch/batch-config-button.tsx (2)
components/studio/batch/batch-mode-panel.tsx (1)
BatchModeSettings(13-16)lib/utils.ts (1)
cn(11-13)
convex/contentAnalysis.ts (1)
convex/lib/openrouter.ts (2)
analyzeImageContent(14-79)calculateSensitivityScore(81-94)
components/studio/controls/collapsible-section.tsx (1)
lib/utils.ts (1)
cn(11-13)
convex/contentAnalysis.test.ts (1)
convex/contentAnalysis.ts (2)
analyzeImage(12-51)analyzeRecentImages(53-67)
convex/generatedImages.ts (1)
convex/lib/nsfwDetection.ts (1)
analyzePromptForNSFW(37-95)
components/studio/controls/collapsible-section.test.tsx (1)
components/studio/controls/collapsible-section.tsx (1)
CollapsibleSection(42-154)
components/ui/image-card.tsx (5)
components/ui/media-player.tsx (1)
isVideoContent(25-29)lib/analytics.ts (1)
trackPromptCopy(149-157)components/ui/sensitive-content-overlay.tsx (1)
SensitiveContentOverlay(12-44)lib/utils.ts (1)
cn(11-13)components/ui/button.tsx (1)
Button(79-79)
convex/lib/nsfwDetection.test.ts (1)
convex/lib/nsfwDetection.ts (1)
analyzePromptForNSFW(37-95)
components/studio/controls/aspect-ratio-selector.tsx (1)
lib/utils.ts (1)
cn(11-13)
🪛 GitHub Check: Run Tests
hooks/queries/use-image-history.ts
[failure] 94-94: hooks/queries/use-image-history.test.ts > image history hooks > usePublicFeed calls getPublicFeed
Error: [vitest] No "useQuery" export is defined on the "convex/react" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:
vi.mock(import("convex/react"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})
❯ Module.usePublicFeed hooks/queries/use-image-history.ts:94:30
❯ hooks/queries/use-image-history.test.ts:34:45
❯ TestComponent node_modules/@testing-library/react/dist/pure.js:330:27
❯ Object.react_stack_bottom_frame node_modules/react-dom/cjs/react-dom-client.development.js:25904:20
❯ renderWithHooks node_modules/react-dom/cjs/react-dom-client.development.js:7662:22
❯ updateFunctionComponent node_modules/react-dom/cjs/react-dom-client.development.js:10166:19
❯ beginWork node_modules/react-dom/cjs/react-dom-client.development.js:11778:18
❯ runWithFiberInDEV node_modules/react-dom/cjs/react-dom-client.development.js:874:13
components/studio/layout/studio-shell.tsx
[failure] 352-352: components/studio/layout/studio-shell.test.tsx > StudioShell > passes sidebar open state to header
Error: [vitest] No "BatchConfigButton" export is defined on the "@/components/studio" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:
vi.mock(import("@/components/studio"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})
❯ StudioShell components/studio/layout/studio-shell.tsx:352:26
❯ Object.react_stack_bottom_frame node_modules/react-dom/cjs/react-dom-client.development.js:25904:20
❯ renderWithHooks node_modules/react-dom/cjs/react-dom-client.development.js:7662:22
❯ updateFunctionComponent node_modules/react-dom/cjs/react-dom-client.development.js:10166:19
❯ beginWork node_modules/react-dom/cjs/react-dom-client.development.js:11778:18
❯ runWithFiberInDEV node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ performUnitOfWork node_modules/react-dom/cjs/react-dom-client.development.js:17641:22
❯ workLoopSync node_modules/react-dom/cjs/react-dom-client.development.js:17469:41
[failure] 352-352: components/studio/layout/studio-shell.test.tsx > StudioShell > passes gallery visibility state to layout
Error: [vitest] No "BatchConfigButton" export is defined on the "@/components/studio" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:
vi.mock(import("@/components/studio"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})
❯ StudioShell components/studio/layout/studio-shell.tsx:352:26
❯ Object.react_stack_bottom_frame node_modules/react-dom/cjs/react-dom-client.development.js:25904:20
❯ renderWithHooks node_modules/react-dom/cjs/react-dom-client.development.js:7662:22
❯ updateFunctionComponent node_modules/react-dom/cjs/react-dom-client.development.js:10166:19
❯ beginWork node_modules/react-dom/cjs/react-dom-client.development.js:11778:18
❯ runWithFiberInDEV node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ performUnitOfWork node_modules/react-dom/cjs/react-dom-client.development.js:17641:22
❯ workLoopSync node_modules/react-dom/cjs/react-dom-client.development.js:17469:41
[failure] 352-352: components/studio/layout/studio-shell.test.tsx > StudioShell > passes sidebar visibility state to layout
Error: [vitest] No "BatchConfigButton" export is defined on the "@/components/studio" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:
vi.mock(import("@/components/studio"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})
❯ StudioShell components/studio/layout/studio-shell.tsx:352:26
❯ Object.react_stack_bottom_frame node_modules/react-dom/cjs/react-dom-client.development.js:25904:20
❯ renderWithHooks node_modules/react-dom/cjs/react-dom-client.development.js:7662:22
❯ updateFunctionComponent node_modules/react-dom/cjs/react-dom-client.development.js:10166:19
❯ beginWork node_modules/react-dom/cjs/react-dom-client.development.js:11778:18
❯ runWithFiberInDEV node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ performUnitOfWork node_modules/react-dom/cjs/react-dom-client.development.js:17641:22
❯ workLoopSync node_modules/react-dom/cjs/react-dom-client.development.js:17469:41
[failure] 352-352: components/studio/layout/studio-shell.test.tsx > StudioShell > renders generate button
Error: [vitest] No "BatchConfigButton" export is defined on the "@/components/studio" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:
vi.mock(import("@/components/studio"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})
❯ StudioShell components/studio/layout/studio-shell.tsx:352:26
❯ Object.react_stack_bottom_frame node_modules/react-dom/cjs/react-dom-client.development.js:25904:20
❯ renderWithHooks node_modules/react-dom/cjs/react-dom-client.development.js:7662:22
❯ updateFunctionComponent node_modules/react-dom/cjs/react-dom-client.development.js:10166:19
❯ beginWork node_modules/react-dom/cjs/react-dom-client.development.js:11778:18
❯ runWithFiberInDEV node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ performUnitOfWork node_modules/react-dom/cjs/react-dom-client.development.js:17641:22
❯ workLoopSync node_modules/react-dom/cjs/react-dom-client.development.js:17469:41
[failure] 352-352: components/studio/layout/studio-shell.test.tsx > StudioShell > renders gallery feature
Error: [vitest] No "BatchConfigButton" export is defined on the "@/components/studio" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:
vi.mock(import("@/components/studio"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})
❯ StudioShell components/studio/layout/studio-shell.tsx:352:26
❯ Object.react_stack_bottom_frame node_modules/react-dom/cjs/react-dom-client.development.js:25904:20
❯ renderWithHooks node_modules/react-dom/cjs/react-dom-client.development.js:7662:22
❯ updateFunctionComponent node_modules/react-dom/cjs/react-dom-client.development.js:10166:19
❯ beginWork node_modules/react-dom/cjs/react-dom-client.development.js:11778:18
❯ runWithFiberInDEV node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ performUnitOfWork node_modules/react-dom/cjs/react-dom-client.development.js:17641:22
❯ workLoopSync node_modules/react-dom/cjs/react-dom-client.development.js:17469:41
[failure] 352-352: components/studio/layout/studio-shell.test.tsx > StudioShell > renders canvas feature
Error: [vitest] No "BatchConfigButton" export is defined on the "@/components/studio" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:
vi.mock(import("@/components/studio"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})
❯ StudioShell components/studio/layout/studio-shell.tsx:352:26
❯ Object.react_stack_bottom_frame node_modules/react-dom/cjs/react-dom-client.development.js:25904:20
❯ renderWithHooks node_modules/react-dom/cjs/react-dom-client.development.js:7662:22
❯ updateFunctionComponent node_modules/react-dom/cjs/react-dom-client.development.js:10166:19
❯ beginWork node_modules/react-dom/cjs/react-dom-client.development.js:11778:18
❯ runWithFiberInDEV node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ performUnitOfWork node_modules/react-dom/cjs/react-dom-client.development.js:17641:22
❯ workLoopSync node_modules/react-dom/cjs/react-dom-client.development.js:17469:41
[failure] 352-352: components/studio/layout/studio-shell.test.tsx > StudioShell > renders controls feature in sidebar
Error: [vitest] No "BatchConfigButton" export is defined on the "@/components/studio" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:
vi.mock(import("@/components/studio"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})
❯ StudioShell components/studio/layout/studio-shell.tsx:352:26
❯ Object.react_stack_bottom_frame node_modules/react-dom/cjs/react-dom-client.development.js:25904:20
❯ renderWithHooks node_modules/react-dom/cjs/react-dom-client.development.js:7662:22
❯ updateFunctionComponent node_modules/react-dom/cjs/react-dom-client.development.js:10166:19
❯ beginWork node_modules/react-dom/cjs/react-dom-client.development.js:11778:18
❯ runWithFiberInDEV node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ performUnitOfWork node_modules/react-dom/cjs/react-dom-client.development.js:17641:22
❯ workLoopSync node_modules/react-dom/cjs/react-dom-client.development.js:17469:41
[failure] 352-352: components/studio/layout/studio-shell.test.tsx > StudioShell > renders prompt feature in sidebar
Error: [vitest] No "BatchConfigButton" export is defined on the "@/components/studio" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:
vi.mock(import("@/components/studio"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})
❯ StudioShell components/studio/layout/studio-shell.tsx:352:26
❯ Object.react_stack_bottom_frame node_modules/react-dom/cjs/react-dom-client.development.js:25904:20
❯ renderWithHooks node_modules/react-dom/cjs/react-dom-client.development.js:7662:22
❯ updateFunctionComponent node_modules/react-dom/cjs/react-dom-client.development.js:10166:19
❯ beginWork node_modules/react-dom/cjs/react-dom-client.development.js:11778:18
❯ runWithFiberInDEV node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ performUnitOfWork node_modules/react-dom/cjs/react-dom-client.development.js:17641:22
❯ workLoopSync node_modules/react-dom/cjs/react-dom-client.development.js:17469:41
[failure] 352-352: components/studio/layout/studio-shell.test.tsx > StudioShell > renders all main components
Error: [vitest] No "BatchConfigButton" export is defined on the "@/components/studio" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:
vi.mock(import("@/components/studio"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})
❯ StudioShell components/studio/layout/studio-shell.tsx:352:26
❯ Object.react_stack_bottom_frame node_modules/react-dom/cjs/react-dom-client.development.js:25904:20
❯ renderWithHooks node_modules/react-dom/cjs/react-dom-client.development.js:7662:22
❯ updateFunctionComponent node_modules/react-dom/cjs/react-dom-client.development.js:10166:19
❯ beginWork node_modules/react-dom/cjs/react-dom-client.development.js:11778:18
❯ runWithFiberInDEV node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ performUnitOfWork node_modules/react-dom/cjs/react-dom-client.development.js:17641:22
❯ workLoopSync node_modules/react-dom/cjs/react-dom-client.development.js:17469:41
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
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)
components/studio/controls/prompt-section.tsx (1)
461-469: Bug: EnhanceButton for negative prompt uses wrong condition.The
disabledprop referenceshasContent, which tracks the positive prompt's content state. This means the enhance button for negative prompts will be disabled when the positive prompt is empty, even if the negative prompt has content.Suggested fix: Track negative prompt content separately
Add state for negative prompt content:
// Display state - updated via RAF batching to prevent lag const [characterCount, setCharacterCount] = React.useState(0); const [hasContent, setHasContent] = React.useState(false); + const [hasNegativeContent, setHasNegativeContent] = React.useState(false); const isNearLimit = characterCount > maxLength * 0.9;Update the negative prompt subscription:
React.useEffect(() => { - // Currently no UI display for negative prompt character count, - // but subscription ensures setNegativePrompt flows through hook properly - const unsubscribe = subscribeToNegativePrompt(() => { - // Could add negative prompt character count here if needed + const unsubscribe = subscribeToNegativePrompt((value) => { + setHasNegativeContent(value.length > 0); }); + // Initialize with current value + const initialValue = getNegativePrompt(); + if (initialValue) { + setHasNegativeContent(initialValue.length > 0); + } return unsubscribe; - }, [subscribeToNegativePrompt]); + }, [subscribeToNegativePrompt, getNegativePrompt]);Fix the disabled condition:
<EnhanceButton isEnhancing={isEnhancingNegativePrompt} - disabled={!hasContent || isGenerating} + disabled={!hasNegativeContent || isGenerating} onEnhance={onEnhanceNegativePrompt}components/studio/layout/studio-shell.tsx (1)
243-259: Avoidanytype; use proper typing for generation parameters.Line 243 uses
const params: any, which violates the coding guideline requiring strict TypeScript withoutany. The type should match the expected parameter type forgenerate().🔧 Suggested fix
- const params: any = { + const params: ImageGenerationParams | VideoGenerationParams = { prompt, negativePrompt: negativePrompt || undefined, model: generationSettings.model, width: generationSettings.width, height: generationSettings.height, seed: effectiveSeed, enhance: generationSettings.options.enhance, private: generationSettings.options.private, safe: generationSettings.options.safe, image: generationSettings.referenceImage, // Video-specific parameters duration: generationSettings.videoSettings.duration, audio: generationSettings.videoSettings.audio, aspectRatio: generationSettings.aspectRatio, lastFrameImage: generationSettings.videoReferenceImages.lastFrame, - } + } satisfies ImageGenerationParams | VideoGenerationParamsIf the union type doesn't fit cleanly, consider creating a dedicated type or using
satisfieswith a more specific type. As per coding guidelines, avoidascasts; prefersatisfies, generics, or runtime validation.
🤖 Fix all issues with AI agents
In `@app/_server/actions/invalidation.ts`:
- Around line 25-28: The revalidateTag call in invalidateUserFavoritesCache uses
an invalid second argument ("max"); remove the extra argument so revalidateTag
is called with only the tag. Update the function invalidateUserFavoritesCache to
call revalidateTag(CACHE_TAGS.FAVORITES_USER(userId)) (i.e., drop the "max"
parameter) to match the correct revalidateTag signature.
- Around line 36-38: The call to revalidateTag in invalidatePublicFeedCache uses
an invalid second argument ("max"); remove that extra argument so the function
simply calls revalidateTag(CACHE_TAGS.FEED_PUBLIC). Update the revalidateTag
invocation inside export async function invalidatePublicFeedCache() to pass only
the CACHE_TAGS.FEED_PUBLIC identifier (leave the function async/export as-is).
- Around line 46-49: In invalidateUserFollowingFeedCache, remove the erroneous
second argument ("max") passed to revalidateTag so the call uses the correct
single-argument signature; update the revalidateTag invocation inside
invalidateUserFollowingFeedCache (which constructs the tag via
CACHE_TAGS.FEED_FOLLOWING_USER(userId)) to pass only that tag.
In `@app/_server/cache/favorites.ts`:
- Around line 14-35: The token for Convex is fetched outside the unstable_cache
closure in getFavoritesPageCached, causing the cached closure to hold a stale
token for the cache TTL; move the await getConvexClerkToken() call inside the
async closure passed to unstable_cache (so token is acquired on each cache
miss/execution) and remove the external token variable, keeping the same cache
key and revalidate/tags logic around unstable_cache and fetchQuery
(api.favorites.list).
In `@app/_server/cache/history.ts`:
- Around line 31-34: The code builds a cache key using filters.models?.sort()
which mutates the caller's array; update the normalization to avoid in-place
mutation by cloning before sorting (e.g., use [...filters.models].sort() or
Array.from(filters.models).sort()) and ensure the fallback preserves the
previous "none" behavior (e.g., JSON.stringify({ v: filters.visibility, m:
filters.models ? [...filters.models].sort() : undefined })). Target the
filterKey expression that references filters.models to make this change.
- Around line 68-70: The code mutates filters.models by calling sort() when
building filterKey; change it to sort a copy instead (e.g., clone filters.models
before sorting) so filters.models is not modified. Update the construction of
filterKey (the filters.models usage) to sort a shallow copy (like using spread
or slice) before JSON.stringify to preserve the original filters object.
In `@app/feed/`[type]/page.tsx:
- Around line 64-78: When building the following feed, detect unauthenticated
visitors and surface auth-aware UI: in the server block where you check
feedType/isPublicFeed and call getCurrentUserId(), set a flag (e.g.,
isUnauthenticatedFollowing) when feedType === "following" and getCurrentUserId()
returns null, or otherwise set initialPage to a sentinel indicating "requires
sign-in"; then pass that flag or sentinel through to the client FeedClient
(which already has access to isSignedIn) and render a different empty state (or
show a sign-in prompt) instead of the generic FollowingEmptyState for
unauthenticated users; update uses of initialPage / FollowingEmptyState in the
feed rendering to branch on that new flag/sentinel so authenticated users with
no posts still see the normal empty messaging while unauthenticated users get
the sign-in prompt.
In `@components/gallery/favorites-client.tsx`:
- Line 55: Replace the unsafe "as any" on images={items.map((r) => r as any)} by
providing a proper type conversion: either make PaginatedFavoritesResult generic
so items is typed as ImageCardData[] (or items:
PaginatedFavoritesResult<ImageCardData>) or map items to ImageCardData with an
explicit typed transformation (e.g., items.map((r): ImageCardData => ({ ... }))
after narrowing from unknown), removing the any cast and preserving type safety
for PaginatedImageGrid.
In `@components/studio/gallery/persistent-image-gallery.tsx`:
- Around line 209-214: The auto-load effect can loop infinitely if
handleLoadMore keeps failing; add a loadErrorCount state (e.g.,
useState<number>(0)), increment it in handleLoadMore's catch block and reset it
to 0 on successful load, then update the React.useEffect condition to include
loadErrorCount < MAX_RETRIES (e.g., 3) so the effect only calls handleLoadMore
while errors are below the threshold; reference handleLoadMore, the effect that
checks canLoadMore/results.length/isLoading/isLoadingMore, and ensure you update
state consistency when a load succeeds or permanently fails.
In `@hooks/mutations/use-delete-image.test.tsx`:
- Around line 41-44: The test sets up a mock for invalidateImageDeletion but
never asserts it was called; update the successful-deletion test case for the
useDeleteImage hook to assert that invalidateImageDeletion was invoked (and
optionally with expected args) after a successful delete; locate the test that
simulates a successful deletion and add an assertion using the mocked
invalidateImageDeletion (from the vi.mock) to ensure cache invalidation is
triggered by the hook.
In `@lib/prompt-enhancement/prompt-enhancer.ts`:
- Around line 6-8: The file-level doc comment in prompt-enhancer.ts incorrectly
states "Groq as primary"; update that comment to reflect the actual primary
provider used by the code (Cerebras) or make it generic to match generateText in
ai-provider.ts; specifically mention that generateText in
lib/ai-provider/ai-provider.ts uses Cerebras as the primary provider with
OpenRouter as a fallback (or reword to avoid naming a specific provider) so the
comment matches the implementation.
In `@lib/prompt-enhancement/suggestion-generator.ts`:
- Around line 6-9: The docstring in suggestion-generator.ts incorrectly states
"Groq as primary"; update that comment to reflect the actual provider order used
by the unified AI provider in lib/ai-provider/ai-provider.ts (Cerebras as
primary and OpenRouter as fallback) and note that Groq is reserved only for
vision/NSFW detection; locate the docstring in suggestion-generator.ts and
replace the provider wording so it matches the provider selection implemented in
the AI provider module.
♻️ Duplicate comments (1)
components/studio/layout/studio-shell.tsx (1)
374-378: Test mock needs update forBatchConfigButton.The test file needs to be updated to include
BatchConfigButtonin the mock, as flagged in the pipeline failures.
🧹 Nitpick comments (52)
components/studio/controls/prompt-section.tsx (1)
190-198: Consider removing or documenting the no-op subscription.The subscription to negative prompt changes currently does nothing. If future functionality is planned, add a TODO comment. Otherwise, this adds unnecessary overhead (subscription lifecycle management) with no benefit.
Option 1: Remove if not needed
- // Subscribe to negative prompt changes (no display state needed, just RAF-batch any UI updates if added later) - React.useEffect(() => { - // Currently no UI display for negative prompt character count, - // but subscription ensures setNegativePrompt flows through hook properly - const unsubscribe = subscribeToNegativePrompt(() => { - // Could add negative prompt character count here if needed - }); - return unsubscribe; - }, [subscribeToNegativePrompt]);Option 2: Add TODO if planned
// Subscribe to negative prompt changes (no display state needed, just RAF-batch any UI updates if added later) React.useEffect(() => { - // Currently no UI display for negative prompt character count, - // but subscription ensures setNegativePrompt flows through hook properly + // TODO: Add negative prompt character count display when UI design is finalized const unsubscribe = subscribeToNegativePrompt(() => { - // Could add negative prompt character count here if needed }); return unsubscribe; }, [subscribeToNegativePrompt]);components/images/image-lightbox.test.tsx (2)
379-386: Remove redundantmockImageredeclaration.This
mockImageconstant shadows and duplicates the outermockImagedefined at line 190-196 with identical properties. Consider removing it to reduce duplication.♻️ Suggested fix
describe('copy prompt auth-gating', () => { - const mockImage = { - url: 'https://example.com/test-image.jpg', - prompt: 'A beautiful landscape', - model: 'test-model', - width: 1024, - height: 1024, - } - it('shows copy prompt button when authenticated', async () => {
388-395: Fragile button selection by index.Using
getAllByRole('button')[1]relies on button order, which can break if the UI changes. Consider using a more specific selector likegetByRole('button', { name: /copy/i })or adding adata-testidto the copy button in the component.components/images/image-lightbox.tsx (2)
218-241: Redundant fallback insrcprop.Line 220:
fullResUrl || displayImage.urlis redundant. SincefullResUrlis defined asdisplayImage?.originalUrl || displayImage?.url(line 94), and this code only executes whendisplayImageis truthy (line 118 conditional),fullResUrlwill always have a value.♻️ Suggested simplification
<NextImage - src={fullResUrl || displayImage.url} + src={fullResUrl} alt={displayImage.prompt || "Generated image"}And similarly for line 231:
- unoptimized={(fullResUrl || displayImage.url).startsWith('http')} + unoptimized={fullResUrl.startsWith('http')}
245-245: Verify zoom indicator visibility logic.The condition
isThumbnailLoaded || isFullResLoadedensures the zoom indicator appears once either layer is ready. However, whenhasSeparateThumbnailis false,isThumbnailLoadedwill never become true (onlyisFullResLoadedwill). This works correctly, but consider adding a comment explaining this behavior for maintainability.components/studio/features/history/gallery-view.tsx (1)
17-29: Consider centralizing thePaginatedGalleryResulttype.This type appears to be duplicated across multiple files (also seen in
app/_server/cache/history.tscontext). Consider extracting it to a shared types file to maintain consistency and avoid drift.♻️ Suggested approach
Create a shared types file, e.g.,
types/gallery.ts:export type PaginatedGalleryResult = { page: Array<{ _id: string _creationTime: number url: string visibility?: "public" | "unlisted" model?: string contentType?: string }> isDone: boolean continueCursor: string }Then import from the shared location in all consuming files.
app/_server/actions/history.ts (1)
6-9: Type duplication:HistoryFiltersis defined in multiple places.This interface is also defined in
hooks/queries/use-image-history.ts(lines 8-13 in relevant snippets). Consider importing from a shared location to avoid drift.♻️ Suggested fix
"use server" import { requireUserId } from "../convex/client" import { getMyImagesPageCached, getMyImagesWithDisplayDataCached } from "../cache/history" +import type { HistoryFilters } from "@/hooks/queries/use-image-history" -interface HistoryFilters { - visibility?: "public" | "unlisted" - models?: string[] -}Alternatively, create a shared
types/history.tsfile and import from there in both locations.app/_server/convex/client.ts (1)
8-12: Simplify the return statement.The Clerk
getToken({ template: "convex" })API usage is correct. However, sincetokenis alreadystring | undefined, the nullish coalescing is redundant. Simplify to:export async function getConvexClerkToken(): Promise<string | undefined> { const { getToken } = await auth() const token = await getToken({ template: "convex" }) return token }components/studio/features/history/gallery-feature.tsx (1)
17-29: Consider exporting and sharingPaginatedGalleryResulttype.This type appears to be a common structure for server-cached pagination results. Based on the AI summary, similar types are used in
gallery-view.tsx,persistent-image-gallery.tsx, andfeed-client.tsx. Consider defining this type once in a shared location (e.g.,@/types/pagination.ts) and importing it to avoid duplication and ensure consistency across components.hooks/queries/use-favorites.ts (1)
46-67: Good pattern for adding side effects to Convex mutations.The TanStack Query wrapper around the Convex mutation provides a clean way to add
onSuccessandonErrorside effects. The server-side cache invalidation viainvalidateUserFavoritesCache()is correctly placed inonSuccess.However,
queryClient(line 48) is imported but unused. The comment on lines 58-60 explains it's reserved for potential TanStack Query cache invalidation, but consider removing it until actually needed to keep the code clean.🧹 Remove unused queryClient (optional)
export function useToggleFavorite() { const toggleFavorite = useConvexMutation(api.favorites.toggle) - const queryClient = useQueryClient() return useMutation({Also remove the import if not used elsewhere:
-import { useMutation, useQueryClient } from "@tanstack/react-query" +import { useMutation } from "@tanstack/react-query"hooks/queries/use-favorites.test.ts (1)
22-26: Consider adding tests for the updateduseToggleFavoritehook.The
useToggleFavoritehook was significantly refactored to use TanStack Query'suseMutationwithonSuccessandonErrorcallbacks, but no corresponding tests were added. Consider adding tests to verify:
- The mutation calls the Convex toggle function
onSuccesstriggersinvalidateUserFavoritesCacheonErrordisplays a toast notificationtodo/reference-next-cache-and-actions-implementation.md (1)
77-99: Add language specifier to the fenced code block.The file layout diagram should have a language specifier for consistent rendering. Since this is a directory tree structure, use
textorplaintext.📝 Suggested fix
-``` +```text app/ _server/ convexAuth.ts # Clerk -> Convex JWT helper (server-only)components/gallery/history-client.test.tsx (1)
61-69: Avoidas anycasts in test fixtures.Per coding guidelines, avoid
anytype usage. Consider defining a minimal type or using partial mocks that satisfy the required shape.♻️ Suggested fix
// Mock initial page data that simulates server-side cached response const mockInitialPage = { page: [ - { _id: "img1" as any }, - { _id: "img2" as any }, - ] as any[], + { _id: "img1" }, + { _id: "img2" }, + ] as Array<{ _id: string }>, continueCursor: "cursor123", isDone: false, }components/gallery/history-client.tsx (2)
112-145: Filter change effect has a potential race condition with async state updates.If the component unmounts while
fetchFiltered()is in progress, the state setters will be called on an unmounted component. Consider adding an abort mechanism or mounted check.♻️ Suggested fix using a mounted flag
React.useEffect(() => { // Skip the initial mount if (prevFilterKeyRef.current === currentFilterKey) return prevFilterKeyRef.current = currentFilterKey // Check if filters are now empty (reset to initial state) const hasFilters = filterState.selectedVisibility.length > 0 || filterState.selectedModels.length > 0 + let isMounted = true + async function fetchFiltered() { setIsLoadingFilters(true) try { const result = await loadMyHistoryWithDisplayPage({ cursor: null, // Start from the beginning filters: hasFilters ? queryFilters : undefined, }) + if (!isMounted) return setItems(result.page) setCursor(result.continueCursor) setIsDone(result.isDone) } catch (error) { console.error("Failed to fetch filtered history:", error) } finally { - setIsLoadingFilters(false) + if (isMounted) setIsLoadingFilters(false) } } void fetchFiltered() // Exit selection mode and clear selection when filters change if (selectionMode) { setSelectionMode(false) deselectAll() } + + return () => { isMounted = false } }, [currentFilterKey, filterState, queryFilters, selectionMode, setSelectionMode, deselectAll])
167-169: Avoidastype assertion.Per coding guidelines, prefer generics or type narrowing over
ascasts. Sinceitemscomes from a typed source, the cast shouldn't be necessary.♻️ Suggested fix
const handleSelectAll = React.useCallback(() => { - selectAll(items.map((r) => ({ _id: r._id } as { _id: string }))) + selectAll(items.map((r) => ({ _id: r._id }))) }, [selectAll, items])If
selectAllrequires a specific type signature, consider updating theuseImageSelectionhook's type definitions instead of casting at the call site.convex/lib/visionAnalysis.ts (3)
14-14: Remove unused imports.
GroqApiErrorandGroqTimeoutErrorare imported but never used in this file. If they're intended for re-export, add an explicitexport { ... }statement.Proposed fix
-import { analyzeImageWithGroq, hasGroqApiKey, GroqApiError, GroqTimeoutError } from "./groq" +import { analyzeImageWithGroq, hasGroqApiKey } from "./groq"Or if re-export is intended:
+// Re-export Groq error types for consumers +export { GroqApiError, GroqTimeoutError } from "./groq"
75-80: Avoidas Tcasts; use a type-safe approach instead.The current implementation uses
as Tcasts which can mask type errors. Per coding guidelines, prefer generics with proper constraints.Type-safe alternative
-function normalizeLevel<T extends string>(value: unknown, allowed: T[]): T { - if (typeof value === "string" && allowed.includes(value.toLowerCase() as T)) { - return value.toLowerCase() as T - } - return allowed[0] // Default to first (safest) option -} +function normalizeLevel<T extends string>(value: unknown, allowed: readonly T[]): T { + if (typeof value === "string") { + const lowered = value.toLowerCase() + const found = allowed.find((v) => v === lowered) + if (found !== undefined) { + return found + } + } + return allowed[0] // Default to first (safest) option +}
141-143: Inconsistent environment variable access pattern.
hasGroqApiKey()is used for Groq, but OpenRouter availability is checked directly viaprocess.env.OPENROUTER_API_KEY. Consider using a consistent helper pattern.Suggested improvement
Either import/create a
hasOpenRouterApiKey()helper from./openrouter, or inline both checks:export function hasAnyVisionProvider(): boolean { - return hasGroqApiKey() || !!process.env.OPENROUTER_API_KEY + // Prefer consistent helper pattern if available in openrouter module + return hasGroqApiKey() || hasOpenRouterApiKey() }components/ui/image-card.tsx (2)
141-149: Minor: Redundant state reset after success.
setOptimisticFavorited(null)is called unconditionally after the try/catch, which means it runs on both success and after the catch block already handled the error case. The catch block sets it tonullon line 145, then line 148 sets it again.This isn't a bug, but you could simplify by using a
finallyblock or removing the redundant call.Optional cleanup using finally
try { await toggleFavorite.mutateAsync({ imageId: image._id as Id<"generatedImages"> }) } catch { // Revert on error - setOptimisticFavorited(null) + setOptimisticFavorited(displayFavorited) // Revert to previous value + } finally { + // Clear optimistic state after completion + setOptimisticFavorited(null) } - // Clear optimistic state after server confirms - setOptimisticFavorited(null)Note: The current revert logic sets to
nullwhich falls back to server state - this might actually be the intended behavior if you want the server state to take over.
171-176: Consider memoizingshowSensitiveOverlayor moving state declaration.The
isRevealedstate is declared after its dependent conditionshowSensitiveOverlay. While this works correctly, declaring related state closer to the top of the component (with other state declarations) improves readability.Suggested reorganization
const [copied, setCopied] = React.useState(false) const [isHovered, setIsHovered] = React.useState(false) const [optimisticFavorited, setOptimisticFavorited] = React.useState<boolean | null>(null) const [isLoaded, setIsLoaded] = React.useState(false) + const [isRevealed, setIsRevealed] = React.useState(false) // Local reveal state for sensitive content // ... later in component ... const showSensitiveOverlay = image.isSensitive && !userShowsSensitive - const [isRevealed, setIsRevealed] = React.useState(false) // Local reveal state for this sessioncomponents/ui/image-card.test.tsx (3)
73-73: Avoidas anycast; use a typed mock return instead.Per coding guidelines,
anydefeats TypeScript's purpose. Consider creating a minimal typed mock object or usingunknownwith narrowing.Suggested approach
- vi.mocked(useUser).mockReturnValue({ isSignedIn: true, user: { id: "user1" } } as any) + vi.mocked(useUser).mockReturnValue({ isSignedIn: true, user: { id: "user1" }, isLoaded: true } as ReturnType<typeof useUser>)Alternatively, create a minimal mock type that satisfies the component's actual usage.
152-152: Sameas anyissue in the unauthenticated user mock.Consistent with the earlier comment, avoid
as anycasts.
155-173: These two tests are effectively identical.Both tests perform the same assertion (
signInLinks.length >= 2) with different names. Consider consolidating into a single test or making each test assert something distinct (e.g., one verifies the copy button's sign-in link specifically, the other verifies the favorite button's sign-in link).Suggested consolidation
- it("renders sign-in link for copy button", () => { - render(<ImageCard {...defaultProps} />) - // There are two links to sign-in: one for favorite, one for copy. - // We can find them by href. - const links = screen.getAllByRole("link", { hidden: true }) - const signInLinks = links.filter(link => link.getAttribute("href") === "/sign-in") - - // Should be at least 2 (Copy and Favorite) - expect(signInLinks.length).toBeGreaterThanOrEqual(2) - }) - - it("renders sign-in link for favorite button", () => { - render(<ImageCard {...defaultProps} />) - const links = screen.getAllByRole("link", { hidden: true }) - const signInLinks = links.filter(link => link.getAttribute("href") === "/sign-in") - - // Should be at least 2 (Copy and Favorite) - expect(signInLinks.length).toBeGreaterThanOrEqual(2) + it("renders sign-in links for copy and favorite actions", () => { + render(<ImageCard {...defaultProps} />) + const links = screen.getAllByRole("link", { hidden: true }) + const signInLinks = links.filter(link => link.getAttribute("href") === "/sign-in") + + // Should have at least 2 sign-in links (Copy and Favorite) + expect(signInLinks.length).toBeGreaterThanOrEqual(2) })convex/lib/groq.ts (1)
79-86: Consider exportinggetDefaultDepsfor testing consistency.The function
analyzeImageWithGroqDepsis documented as "@internal Exported for testing" (line 95), butgetDefaultDepsis private. If tests need to verify default configuration values, exporting this helper would be useful.Optional export
-const getDefaultDeps = (): GroqVisionDeps => ({ +export const getDefaultDeps = (): GroqVisionDeps => ({convex/generatedImages.ts (3)
146-146: Avoidv.any()forgenerationParams.Using
v.any()defeats TypeScript's type safety. Define a proper schema forgenerationParamsor usev.record(v.string(), v.any())as a minimum, though a fully typed schema is preferred.As per coding guidelines: "No
anytype in TypeScript. Useunknownand narrow instead."♻️ Suggested approach
If the structure is known, define it explicitly:
generationParams: v.object({ steps: v.optional(v.number()), cfgScale: v.optional(v.number()), sampler: v.optional(v.string()), // ... other known fields }),Or at minimum use a record type:
- generationParams: v.any(), + generationParams: v.record(v.string(), v.union(v.string(), v.number(), v.boolean(), v.null())),
609-618: Avoidas EnrichedImage[]cast; consider type-safe mapping.The
as EnrichedImage[]cast bypasses type checking. Since you already have the user data, you can ensure type safety by explicitly constructing the enriched objects.As per coding guidelines: "Avoid
ascasts in TypeScript; prefersatisfies, generics, and runtime validation."♻️ Suggested fix
- const enrichedPage = paginatedResult.page.map((image) => ({ - ...image, - ownerName: user.username ?? "Anonymous", - ownerPictureUrl: user.pictureUrl ?? null, - })) as EnrichedImage[] + const enrichedPage: EnrichedImage[] = paginatedResult.page.map((image) => ({ + ...image, + ownerName: user.username ?? "Anonymous", + ownerPictureUrl: user.pictureUrl ?? null, + }))
829-850: Consider conditionalsensitiveSourceupdate inupdateImageSensitivity.The mutation always sets
sensitiveSource: "vision_analysis"regardless of theisSensitiveresult. If vision analysis determines the image is safe (isSensitive: false), you may want to preserve the original source or use a different value like"vision_analysis_safe"to distinguish from prompt-based safe classifications.♻️ Suggested adjustment
handler: async (ctx, args) => { await ctx.db.patch(args.imageId, { isSensitive: args.isSensitive, - // If vision found it sensitive, update source - sensitiveSource: "vision_analysis", + // Update source to reflect vision analysis result + sensitiveSource: args.isSensitive ? "vision_analysis" : "vision_analysis_safe", sensitiveConfidence: args.confidence, contentAnalysis: args.contentAnalysis, }); },convex/contentAnalysis.ts (2)
43-51: Confidence value of 0 for safe images may lose useful metadata.When
isSensitiveis false, the confidence is set to 0 (line 44). This discards the actual confidence score that vision analysis determined the image is safe. Consider preserving the score for debugging/auditing purposes.♻️ Suggested change
await ctx.runMutation(internal.generatedImages.updateImageSensitivity, { imageId: args.imageId, isSensitive, - confidence: isSensitive ? sensitivityScore * (analysis.confidence || 1) : 0, + // Preserve actual confidence score regardless of sensitivity result + confidence: sensitivityScore * (analysis.confidence || 1), contentAnalysis: { nudity: analysis.nudity, sexual: analysis.sexual_content, violence: analysis.violence, analyzedAt: Date.now(), }, });
52-55: Implement failure tracking or backoff mechanism for persistent analysis failures.Images that fail analysis are silently logged and remain in the unanalyzed state, causing them to be retried indefinitely by the cron job. Unlike other processors in the codebase (singleGeneration, batchProcessor), there is no
retryCountmechanism to track failed attempts or implement exponential backoff. This can lead to wasted processing cycles on persistently failing images (e.g., invalid URLs, corrupted images).Consider adding failure tracking (similar to
retryCountin other modules) or implementing a skip mechanism for images that exceed a retry threshold.hooks/mutations/use-delete-image.test.tsx (1)
76-77: Unusual semicolon placement for ASI handling.The
;(useConvexMutation as ...)pattern is used throughout to prevent ASI issues. While valid, this is unconventional. A more readable approach would be to use a separate statement or explicit line continuation.This is a minor style preference and the current approach works correctly.
app/favorites/page.tsx (1)
35-37: Consider using HTML entity for apostrophe.The
{"you've"}JSX escape works but is slightly awkward. Consider using the HTML entity'or the Unicode character directly.♻️ Suggested change
<p className="text-muted-foreground"> - Images {"you've"} loved from across the community. + Images you've loved from across the community. </p>hooks/mutations/use-set-visibility.ts (1)
29-39: Verify that awaiting asynconSuccessdoesn't block mutation resolution.The
onSuccesshandler is async and awaitsinvalidateVisibilityChange(). TanStack Query'suseMutationdoes support asynconSuccess, but be aware that:
- The mutation will appear "successful" to the caller before the server cache invalidation completes
- Any error in
invalidateVisibilityChange()will be silently swallowed since there's no error handling hereConsider wrapping the server invalidation in a try-catch if cache invalidation failures should be logged:
// Invalidate server-side Next.js cache try { await invalidateVisibilityChange() } catch (e) { console.error("Failed to invalidate server cache:", e) }app/_server/actions/feed.test.ts (1)
12-15: Consider simplifying mock setup.The arrow function wrapper
(...args: unknown[]) => mockGetPublicFeedPageCached(...args)is verbose. Since you're already usingvi.fn(), you could simplify by setting the mock implementation inbeforeEach:vi.mock("@/app/_server/cache/feed", () => ({ getPublicFeedPageCached: vi.fn(), getFollowingFeedPageCached: vi.fn(), })) // Then in beforeEach or tests: import { getPublicFeedPageCached } from "@/app/_server/cache/feed" vi.mocked(getPublicFeedPageCached).mockResolvedValue(mockPaginatedResult)However, the current approach works correctly and may be preferred for explicit control over mock references.
hooks/mutations/use-delete-image.ts (2)
80-89: Consider awaitinginvalidateQueriesfor consistency.The async
onSuccesspattern is correct for calling the server action. However,queryClient.invalidateQueries()returns a Promise that resolves when the refetch completes. Currently these are fire-and-forget whileinvalidateImageDeletion()is awaited.For consistency and to ensure all cache invalidations complete before
onSuccessreturns, consider:♻️ Optional: Await all invalidations
onSuccess: async () => { toast.success("Image deleted") // Invalidate client-side TanStack Query caches - queryClient.invalidateQueries({ queryKey: ["image-history"] }) - queryClient.invalidateQueries({ queryKey: ["public-feed"] }) + await Promise.all([ + queryClient.invalidateQueries({ queryKey: ["image-history"] }), + queryClient.invalidateQueries({ queryKey: ["public-feed"] }), + ]) // Invalidate server-side Next.js cache await invalidateImageDeletion() },
160-169: Same consideration for bulk delete'sonSuccess.Same optional improvement applies here for consistency with the single delete hook.
♻️ Optional: Await all invalidations
onSuccess: async (result) => { const count = result.successCount toast.success(`Deleted ${count} image${count !== 1 ? "s" : ""}`) // Invalidate client-side TanStack Query caches - queryClient.invalidateQueries({ queryKey: ["image-history"] }) - queryClient.invalidateQueries({ queryKey: ["public-feed"] }) + await Promise.all([ + queryClient.invalidateQueries({ queryKey: ["image-history"] }), + queryClient.invalidateQueries({ queryKey: ["public-feed"] }), + ]) // Invalidate server-side Next.js cache await invalidateImageDeletion() },components/gallery/favorites-client.tsx (2)
26-31: Consider resetting state wheninitialPagechanges.Unlike
FeedClientwhich resets its state whenfeedTypeorinitialPagechanges, this component initializes state only once via lazy initializers. If the parent re-renders with a differentinitialPage(e.g., after cache invalidation), the component will show stale data.Add a useEffect to sync state with prop changes
+ // Reset state when initialPage changes (e.g., after invalidation) + React.useEffect(() => { + setItems(initialPage.page) + setCursor(initialPage.continueCursor) + setIsDone(initialPage.isDone) + }, [initialPage])
43-45: Consider surfacing errors to the user.Currently, errors are only logged to console. Users won't know why loading failed. Consider showing a toast or inline error message.
lib/cerebras/cerebras-client.test.ts (2)
41-51:mockCerebrasInstanceappears unused.The
mockCerebrasInstancemock is created and configured inbeforeEach, but it's never used in any test assertions or logic. Consider removing it to reduce test noise.Remove unused mock
const mockApiKey = "test-cerebras-api-key" - const mockCerebrasInstance = vi.fn() const mockModelFn = vi.fn().mockReturnValue("model-instance") beforeEach(() => { vi.clearAllMocks() clearCerebrasClientCache() ;(getCerebrasApiKey as Mock).mockReturnValue(mockApiKey) ;(createCerebras as Mock).mockReturnValue(mockModelFn) - mockCerebrasInstance.mockReturnValue("model-instance") })
106-215: Good coverage of core scenarios; consider adding TOKEN_LIMIT_EXCEEDED test.The tests comprehensively cover the happy path, answer extraction, abort handling, and retry logic. Based on the implementation notes,
generateWithCerebrasthrows aCerebrasErrorwith codeTOKEN_LIMIT_EXCEEDEDwhen reasoning output is truncated. Consider adding a test for this edge case.app/profile/[username]/page.test.tsx (2)
22-30: Mock strategy works but is fragile.The
useMutationmock relies on string equality (mutationRef === "follow"), which works because the API mock returns"follow"forapi.follows.follow. However, this coupling is implicit and could silently break if the mock values change.Consider a more explicit approach:
Alternative: Use imported mock references
vi.mock("convex/react", () => ({ useQuery: (...args: unknown[]) => mockUseQuery(...args), useMutation: (mutationRef: unknown) => { const mutationMap: Record<string, ReturnType<typeof vi.fn>> = { "follow": mockFollow, "unfollow": mockUnfollow, } return mutationMap[mutationRef as string] ?? mockUseMutation(mutationRef) }, }))
300-308: Skeleton detection is implementation-coupled.Querying by CSS class patterns (
animate-pulse,skeleton) may break if styling changes. Consider adding adata-testidto the skeleton component for more resilient testing.components/gallery/feed-client.tsx (2)
51-55: Consider memoizing or comparing initialPage contents.This effect resets state whenever
feedTypeorinitialPagechanges. IfinitialPageis a new object reference on each render (even with same content), this could cause unnecessary state resets. Consider comparing the page content or cursor instead of the entire object reference.♻️ Suggested improvement using stable comparison
// Reset state when feedType or initialPage changes React.useEffect(() => { setItems(initialPage?.page ?? []) setCursor(initialPage?.continueCursor ?? null) setIsDone(initialPage?.isDone ?? false) - }, [feedType, initialPage]) + // Use continueCursor as a stable identifier for the page + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [feedType, initialPage?.continueCursor])
69-73: Consider surfacing load errors to users.Currently, errors during "load more" are only logged to console. Users won't know if additional content failed to load. Consider adding error state and UI feedback.
♻️ Optional: Add error state for better UX
const [isLoadingMore, setIsLoadingMore] = React.useState(false) + const [loadError, setLoadError] = React.useState<string | null>(null) // ... in loadMore callback: + setLoadError(null) try { // ... existing code } catch (error) { console.error("Failed to load more:", error) + setLoadError("Failed to load more images. Tap to retry.") } finally {lib/prompt-enhancement/suggestion-generator.ts (1)
70-79: Consider gating debug logs for production.These
console.logstatements will execute in production, which could be noisy for a frequently-called suggestion generator. Consider wrapping them in a development check similar toVERBOSE_LOGGINGin the AI provider.♻️ Suggested improvement
+const DEBUG = process.env.NODE_ENV === "development" + // ... in generateSuggestions: - console.log("[Suggestions] Raw AI response:", JSON.stringify(result)) + if (DEBUG) { + console.log("[Suggestions] Raw AI response:", JSON.stringify(result)) + } // Parse the comma-separated response const suggestions = result.text .split(",") // ... - console.log("[Suggestions] Parsed:", suggestions) + if (DEBUG) { + console.log("[Suggestions] Parsed:", suggestions) + }CEREBRAS_INTEGRATION_SPIKE.md (3)
27-30: Add language specifier to code block.The static analysis tool flagged this code block as missing a language identifier. Consider using
httportextfor the API endpoint example.📝 Suggested fix
-``` +```http POST https://api.cerebras.ai/v1/chat/completions Authorization: Bearer $CEREBRAS_API_KEY</details> --- `43-49`: **Add language specifier to directory structure code block.** Consider using `text` or `plaintext` for the directory tree structure to satisfy the linter. <details> <summary>📝 Suggested fix</summary> ```diff -``` +```text lib/cerebras/ ├── cerebras-config.ts # Models, API key getter ├── cerebras-client.ts # Client with retry logic ├── cerebras-client.test.ts └── index.ts</details> --- `71-77`: **Wrap bare URLs in angle brackets.** The URLs in the reference table are bare. Wrapping them in angle brackets improves markdown compatibility. <details> <summary>📝 Suggested fix</summary> ```diff | Resource | URL | |----------|-----| -| Cerebras Docs | https://docs.cerebras.ai | -| API Reference | https://docs.cerebras.ai/api-reference | -| AI SDK Provider | https://www.npmjs.com/package/@ai-sdk/cerebras | -| Rate Limits | https://docs.cerebras.ai/rate-limits | +| Cerebras Docs | <https://docs.cerebras.ai> | +| API Reference | <https://docs.cerebras.ai/api-reference> | +| AI SDK Provider | <https://www.npmjs.com/package/@ai-sdk/cerebras> | +| Rate Limits | <https://docs.cerebras.ai/rate-limits> |lib/cerebras/cerebras-client.ts (2)
112-136: String-based error detection is fragile but acceptable.The
isRetryableErrorfunction relies on string matching in error messages. This works but could produce false positives if status codes appear in unrelated error text. Consider checking for astatusproperty on the error object if available from the AI SDK.
293-307:asynckeyword is unnecessary.The function doesn't use
await, so theasynckeyword can be removed. ThestreamTextcall returns a stream result that doesn't need to be awaited here.♻️ Suggested simplification
-export async function streamWithCerebras(options: CerebrasGenerateOptions) { +export function streamWithCerebras(options: CerebrasGenerateOptions) { const cerebras = createCerebrasClient() const model = options.model ?? CEREBRAS_MODELS.TEXT_PRIMARY const result = streamText({ model: cerebras(model), system: options.system, prompt: options.prompt, maxOutputTokens: options.maxTokens ?? 1024, temperature: options.temperature ?? 0.7, abortSignal: options.abortSignal, }) return result }components/studio/gallery/persistent-image-gallery.tsx (3)
136-141: Array dependency serialization is acceptable but fragile.The
queryFilters.models?.join(",")pattern for dependency tracking works but could misbehave if model names contain commas. Consider usingJSON.stringify(queryFilters)or a custom comparison hook for more robust dependency tracking.
201-203: Consider user feedback for load errors.Errors are logged but not surfaced to users. If pagination fails, users might think there are no more images. Consider showing a toast or retry option.
356-359: Redundant wrapper callback.
memoizedLoadMoresimply callshandleLoadMorewhich is already auseCallback. This wrapper can be removed andhandleLoadMoreused directly.♻️ Suggested simplification
- // Memoize load more handler - const memoizedLoadMore = React.useCallback(() => { - handleLoadMore() - }, [handleLoadMore]) - return ( <ImageGallery {...restProps} // ... - onLoadMore={canLoadMore || isLoadingMore ? memoizedLoadMore : undefined} + onLoadMore={canLoadMore || isLoadingMore ? handleLoadMore : undefined} // ... /> )
| export async function invalidateUserFavoritesCache() { | ||
| const userId = await requireUserId() | ||
| revalidateTag(CACHE_TAGS.FAVORITES_USER(userId), "max") | ||
| } |
There was a problem hiding this comment.
Same invalid revalidateTag signature.
Remove the "max" argument here as well.
🐛 Proposed fix
export async function invalidateUserFavoritesCache() {
const userId = await requireUserId()
- revalidateTag(CACHE_TAGS.FAVORITES_USER(userId), "max")
+ revalidateTag(CACHE_TAGS.FAVORITES_USER(userId))
}📝 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.
| export async function invalidateUserFavoritesCache() { | |
| const userId = await requireUserId() | |
| revalidateTag(CACHE_TAGS.FAVORITES_USER(userId), "max") | |
| } | |
| export async function invalidateUserFavoritesCache() { | |
| const userId = await requireUserId() | |
| revalidateTag(CACHE_TAGS.FAVORITES_USER(userId)) | |
| } |
🤖 Prompt for AI Agents
In `@app/_server/actions/invalidation.ts` around lines 25 - 28, The revalidateTag
call in invalidateUserFavoritesCache uses an invalid second argument ("max");
remove the extra argument so revalidateTag is called with only the tag. Update
the function invalidateUserFavoritesCache to call
revalidateTag(CACHE_TAGS.FAVORITES_USER(userId)) (i.e., drop the "max"
parameter) to match the correct revalidateTag signature.
There was a problem hiding this comment.
@coderabbitai re-check that file please
/**
- This function allows you to purge cached data on-demand for a specific cache tag.
- Read more: Next.js Docs:
revalidateTag
*/
export declare function revalidateTag(tag: string, profile: string | CacheLifeConfig): undefined;
There was a problem hiding this comment.
@Simplereally, you're absolutely right, and I apologize for the incorrect review!
Looking at the official Next.js type declaration you've provided, revalidateTag does accept a second parameter profile: string | CacheLifeConfig. The current implementation in the file is correct:
revalidateTag(CACHE_TAGS.HISTORY_USER(userId), "max")
revalidateTag(CACHE_TAGS.FAVORITES_USER(userId), "max")The "max" profile parameter enables stale-while-revalidate behavior, which is exactly what the code comments indicate is intended. This allows serving cached content immediately while fetching fresh data in the background.
My previous suggestion to remove the "max" argument was based on an outdated understanding of the API. Thank you for catching this and providing the correct signature!
🐰✨
(\/)
(••)
/>📚 Learned something new today!
✏️ Learnings added
Learnt from: Simplereally
Repo: Simplereally/bloomstudio PR: 13
File: app/_server/actions/invalidation.ts:25-28
Timestamp: 2026-01-16T04:33:52.104Z
Learning: In Next.js, `revalidateTag(tag: string, profile: string | CacheLifeConfig)` accepts a second parameter for cache profile. Using `revalidateTag(tag, "max")` is valid and enables stale-while-revalidate behavior.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
🧠 Learnings used
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/{actions,route}.{ts,tsx} : Call Convex via `fetchMutation` / `fetchAction` in Server Actions / Route Handlers when you must mutate from the server boundary.
| export async function invalidatePublicFeedCache() { | ||
| revalidateTag(CACHE_TAGS.FEED_PUBLIC, "max") | ||
| } |
There was a problem hiding this comment.
Same invalid revalidateTag signature.
Remove the "max" argument.
🐛 Proposed fix
export async function invalidatePublicFeedCache() {
- revalidateTag(CACHE_TAGS.FEED_PUBLIC, "max")
+ revalidateTag(CACHE_TAGS.FEED_PUBLIC)
}📝 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.
| export async function invalidatePublicFeedCache() { | |
| revalidateTag(CACHE_TAGS.FEED_PUBLIC, "max") | |
| } | |
| export async function invalidatePublicFeedCache() { | |
| revalidateTag(CACHE_TAGS.FEED_PUBLIC) | |
| } |
🤖 Prompt for AI Agents
In `@app/_server/actions/invalidation.ts` around lines 36 - 38, The call to
revalidateTag in invalidatePublicFeedCache uses an invalid second argument
("max"); remove that extra argument so the function simply calls
revalidateTag(CACHE_TAGS.FEED_PUBLIC). Update the revalidateTag invocation
inside export async function invalidatePublicFeedCache() to pass only the
CACHE_TAGS.FEED_PUBLIC identifier (leave the function async/export as-is).
| export async function invalidateUserFollowingFeedCache() { | ||
| const userId = await requireUserId() | ||
| revalidateTag(CACHE_TAGS.FEED_FOLLOWING_USER(userId), "max") | ||
| } |
There was a problem hiding this comment.
Same invalid revalidateTag signature.
Remove the "max" argument.
🐛 Proposed fix
export async function invalidateUserFollowingFeedCache() {
const userId = await requireUserId()
- revalidateTag(CACHE_TAGS.FEED_FOLLOWING_USER(userId), "max")
+ revalidateTag(CACHE_TAGS.FEED_FOLLOWING_USER(userId))
}📝 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.
| export async function invalidateUserFollowingFeedCache() { | |
| const userId = await requireUserId() | |
| revalidateTag(CACHE_TAGS.FEED_FOLLOWING_USER(userId), "max") | |
| } | |
| export async function invalidateUserFollowingFeedCache() { | |
| const userId = await requireUserId() | |
| revalidateTag(CACHE_TAGS.FEED_FOLLOWING_USER(userId)) | |
| } |
🤖 Prompt for AI Agents
In `@app/_server/actions/invalidation.ts` around lines 46 - 49, In
invalidateUserFollowingFeedCache, remove the erroneous second argument ("max")
passed to revalidateTag so the call uses the correct single-argument signature;
update the revalidateTag invocation inside invalidateUserFollowingFeedCache
(which constructs the tag via CACHE_TAGS.FEED_FOLLOWING_USER(userId)) to pass
only that tag.
| * | ||
| * Uses unified AI provider with Groq as primary (14,400 RPD free tier) | ||
| * and OpenRouter as fallback. | ||
| */ |
There was a problem hiding this comment.
Documentation inconsistency: mentions Groq but uses Cerebras.
The docstring states "Uses unified AI provider with Groq as primary" but the unified AI provider (lib/ai-provider/ai-provider.ts) uses Cerebras as primary and OpenRouter as fallback. Groq is reserved for vision/NSFW detection as noted in the AI provider file.
📝 Suggested fix
/**
* Prompt Suggestions Generator
*
* Service layer for generating contextual prompt suggestions using LLM.
* Provides real-time suggestions based on user input.
- *
- * Uses unified AI provider with Groq as primary (14,400 RPD free tier)
- * and OpenRouter as fallback.
+ *
+ * Uses unified AI provider with Cerebras as primary (ultra-fast inference)
+ * and OpenRouter as fallback.
*/📝 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.
| * | |
| * Uses unified AI provider with Groq as primary (14,400 RPD free tier) | |
| * and OpenRouter as fallback. | |
| */ | |
| /** | |
| * Prompt Suggestions Generator | |
| * | |
| * Service layer for generating contextual prompt suggestions using LLM. | |
| * Provides real-time suggestions based on user input. | |
| * | |
| * Uses unified AI provider with Cerebras as primary (ultra-fast inference) | |
| * and OpenRouter as fallback. | |
| */ |
🤖 Prompt for AI Agents
In `@lib/prompt-enhancement/suggestion-generator.ts` around lines 6 - 9, The
docstring in suggestion-generator.ts incorrectly states "Groq as primary";
update that comment to reflect the actual provider order used by the unified AI
provider in lib/ai-provider/ai-provider.ts (Cerebras as primary and OpenRouter
as fallback) and note that Groq is reserved only for vision/NSFW detection;
locate the docstring in suggestion-generator.ts and replace the provider wording
so it matches the provider selection implemented in the AI provider module.
There was a problem hiding this comment.
Actionable comments posted: 6
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (7)
components/studio/controls/prompt-section.tsx (1)
461-466: Use negative-prompt content for negative-prompt enhancement gating.
disabled={!hasContent || isGenerating}is derived from the main prompt only, so negative prompt enhancement can be disabled when the negative prompt has text (or enabled when it's empty). Track a separatenegativeHasContentbased onsubscribeToNegativePromptand use it here.💡 Suggested fix
+ const [negativeHasContent, setNegativeHasContent] = React.useState(false); React.useEffect(() => { - const unsubscribe = subscribeToNegativePrompt(() => { - // Could add negative prompt character count here if needed - }); + const unsubscribe = subscribeToNegativePrompt((value) => { + setNegativeHasContent(value.length > 0); + }); return unsubscribe; }, [subscribeToNegativePrompt]);- {onEnhanceNegativePrompt && onCancelEnhanceNegativePrompt && ( + {onEnhanceNegativePrompt && onCancelEnhanceNegativePrompt && ( <EnhanceButton isEnhancing={isEnhancingNegativePrompt} - disabled={!hasContent || isGenerating} + disabled={!negativeHasContent || isGenerating} onEnhance={onEnhanceNegativePrompt} onCancel={onCancelEnhanceNegativePrompt} className="relative right-auto bottom-auto" /> )}components/studio/gallery/selection-context.tsx (1)
10-13: Doc comment no longer matches implementation.The header still says “Uses refs internally,” but refs were removed. Update the comment to avoid misleading readers.
📝 Suggested doc fix
- * - Uses refs internally to provide stable callbacks + * - Uses memoized callbacks derived from selection stateapp/profile/loading.tsx (1)
1-5: Update the comment to match the new export.It still says BrandedLoading, but the file now re-exports ProfileSkeleton.
📝 Suggested fix
- * Re-exports the shared BrandedLoading component for consistency. + * Re-exports the shared ProfileSkeleton component for consistency.components/landing/landing-header.tsx (1)
169-233: Replace<Link>wrapping<Button>withButton asChildpattern to fix invalid nested interactive elements.
<a><button></a>is invalid HTML and breaks keyboard/focus behavior. The Button component supports theasChildprop via Radix UI Slot — use it to render the Button as a Link instead:Example fix
-<Link href="/feed/public" className="hidden lg:block group/feed"> - <Button variant="ghost" className={cn(...)} > +<Button asChild variant="ghost" className={cn("hidden lg:block group/feed", ...)} > + <Link href="/feed/public"> ... - </Button> -</Link> + </Link> +</Button>Apply to all instances: lines 169–177, 189–202, 211–216, 331–343, 352–356, 360–368.
app/profile/[username]/page.tsx (1)
122-139: Isolate cache invalidation errors to avoid misleading follow status messages.If
invalidateFollowChange()throws (e.g., whenrequireUserId()fails), the error is caught by the outer handler and displays "Failed to update follow status" even though the follow/unfollow operation succeeded. Wrap cache invalidation in its own try-catch to prevent confusing the user.Suggested fix
try { if (isFollowing) { await unfollow({ followeeId }) toast.success("Unfollowed") } else { await follow({ followeeId }) toast.success("Followed") } - // Invalidate following feed cache so it reflects the change - await invalidateFollowChange() + // Invalidate following feed cache so it reflects the change + try { + await invalidateFollowChange() + } catch (err) { + console.warn("Failed to invalidate following feed cache", err) + } } catch (error) { toast.error("Failed to update follow status") console.error(error) } finally {convex/generatedImages.ts (2)
148-190: Avoid logging sensitive prompt terms in production logs.
Line 150 logs explicit terms and confidence; this can surface sensitive content in retained logs. Consider gating behind a debug flag or removing.🔧 Example guard
- console.log(`[Prompt Analysis] Score: ${promptAnalysis.confidence}, Sensitive: ${promptAnalysis.isSensitive}, Terms: ${promptAnalysis.matchedTerms.join(", ")}`) + if (process.env.NODE_ENV === "development") { + console.log( + `[Prompt Analysis] Score: ${promptAnalysis.confidence}, Sensitive: ${promptAnalysis.isSensitive}, Terms: ${promptAnalysis.matchedTerms.join(", ")}` + ) + }
674-700: Pending sensitive state is treated as safe in following feed.
Line 699 passes images withisSensitive === nullthroughtoPublicFeedImages, which coerces null tofalse. This can surface unanalyzed content as safe. Consider filtering out nulls (or widening the public type to allow null).🐛 Possible fix (filter out pending)
const paginatedResult = await ctx.db .query("generatedImages") .withIndex("by_visibility", (q) => q.eq("visibility", "public")) .filter((q) => { // Owner filter: ownerId must be in followedIds const ownerConditions = followedIds.map(id => q.eq(q.field("ownerId"), id)) const ownerFilter = ownerConditions.length === 1 ? ownerConditions[0] : q.or(...ownerConditions) // Aspect ratio filter: not extreme const aspectRatioFilter = q.not(q.gt(q.field("aspectRatio"), 4)) + const analyzedFilter = q.neq(q.field("isSensitive"), null) - return q.and(ownerFilter, aspectRatioFilter) + return q.and(ownerFilter, aspectRatioFilter, analyzedFilter) })
🤖 Fix all issues with AI agents
In `@components/gallery/feed-client.tsx`:
- Around line 68-85: The early return in loadMore() uses "!cursor" which
prevents the initial fetch when initialPage is undefined (cursor === null);
update the guard in loadMore (in components/gallery/feed-client.tsx) to allow a
null cursor for the first fetch — e.g., only short-circuit if isDone or
isLoadingMore or (cursor === null && items.length > 0) — so the handler will run
when there are no items yet but cursor is null; ensure you reference and update
the loadMore function and its use of cursor, items, isDone, and isLoadingMore
accordingly.
In `@components/gallery/history-client.tsx`:
- Around line 89-101: The optimistic delete currently closes over the outer
items variable causing stale rollbacks; change handleOptimisticDelete so it uses
a functional update to capture the exact current list at deletion time (use
setItems(prev => { const snapshot = prev; const newList = prev.filter(...);
return newList; }) pattern), store that snapshot in the closure returned, and
have the rollback call setItems(() => snapshot) so rollback restores the precise
state; update references to handleOptimisticDelete, setItems, and the
deletedIdSet/previousItems usage accordingly.
In `@components/pollen-balance/pollen-balance-display-view.tsx`:
- Around line 3-12: Add an interactive refresh affordance to the
PollenBalanceDisplayView by extending its props to accept onRefresh (a callback)
and isRefreshing (boolean), update the component signature
(PollenBalanceDisplayView props/interface) to include these, make the balance
Badge (or surrounding control) clickable and call onRefresh when clicked, and
show a visible loading indicator (spinner/Skeleton/disabled state) on the Badge
when isRefreshing is true; also update the container that renders
PollenBalanceDisplayView to pass onRefresh={refetch} and
isRefreshing={isRefreshing} from the hook so manual refresh is wired end-to-end.
In `@components/pollen-balance/pollen-balance-display.test.tsx`:
- Around line 6-30: The test references the React type React.ReactElement in the
renderWithProviders function but never imports the React namespace; import the
React type (e.g., add "import React from 'react'" or "import type React from
'react'" / "import type { ReactElement } from 'react'") and update
renderWithProviders to use that imported type (or use ReactElement directly) so
TypeScript can resolve the type used by renderWithProviders.
In `@components/ui/sidebar.tsx`:
- Around line 209-242: The mobile sidebar currently renders as a plain div
(data-sidebar="sidebar") which loses modal semantics; update the component so
when openMobile is true the sidebar container has role="dialog" and
aria-modal="true", implement Escape-key handling to call setOpenMobile(false)
(e.g., on keydown at the top-level wrapper), trap focus within the sidebar while
open (use a focus trap utility or add focus sentinels and manage focus
programmatically on open/close using openMobile and refs), and restore
background inert/scroll-lock behavior (set document.body.style.overflow =
"hidden" when openMobile and revert on close, or apply inert/aria-hidden to the
page content outside the sidebar). Ensure the existing overlay onClick still
closes via setOpenMobile(false) and use the same data-sidebar="sidebar"
container and openMobile state to locate where to add these changes.
In `@convex/batchGeneration.ts`:
- Around line 159-162: The console.log at the prompt analysis step is printing
user-derived matchedTerms which can contain sensitive content; update the
logging in batchGeneration.ts where analyzePromptForNSFW and promptAnalysis are
used so you do not output matchedTerms directly (e.g., log only
promptAnalysis.confidence, promptAnalysis.isSensitive, and either
promptAnalysis.matchedTerms.length or a redacted flag), or gate full-term output
behind an explicit debug flag; specifically modify the console.log that
references promptAnalysis.matchedTerms.join to remove or replace the joined
terms and preserve only non-sensitive metadata.
🟡 Minor comments (15)
components/studio/controls/aspect-ratio-selector.tsx-44-68 (1)
44-68: Tighten the “ad” match to avoid false positives.Line 64 uses
t.includes("ad"), which will incorrectly classify unrelated tags like “gradient” or “shadow” as marketing. Use token/word matching for “ad/ads” and keep explicit matches for “advertisement/marketing.”💡 Suggested adjustment
- const t = tag.toLowerCase(); + const t = tag.toLowerCase(); + const tokens = t.split(/[^a-z0-9]+/).filter(Boolean); // Business & Marketing - if (t.includes("ad") || t.includes("advertisement") || t.includes("marketing")) return Megaphone; + if (tokens.includes("ad") || tokens.includes("ads") || t.includes("advertisement") || t.includes("marketing")) + return Megaphone;components/studio/controls/reference-image-picker.test.tsx-49-51 (1)
49-51: Avoid doubleascasts for Convex IDs.The pattern
value as unknown as Id<"referenceImages">violates the TypeScript guideline to avoidascasts. While Convex doesn't expose a branded type factory, consider extracting this into a test helper with a comment explaining the limitation, or use a singleascast with@ts-expect-errorand a TODO linking to a Convex feature request if branded type creation becomes available.components/studio/controls/reference-image-picker.test.tsx-84-92 (1)
84-92: Removeas unknown ascasts and use a typed mock factory instead.The current mock bypasses TypeScript's type checking, making it impossible to catch missing properties if the component's usage expands. Follow the pattern from
content-preference-card.test.tsx: create a properly typed mock helper usingObject.assignto combine the function with its mutation interface properties (mutateAsync,isPending, etc.) without casts.Example:
function createMockUploadMutation(): ReturnType<typeof useUploadReference> { const mutation = Object.assign( vi.fn().mockResolvedValue({ url: "test", r2Key: "key", contentType: "image/png", sizeBytes: 100 }), { isPending: false } ); return mutation as ReturnType<typeof useUploadReference>; } beforeEach(() => { vi.mocked(useUploadReference).mockReturnValue(createMockUploadMutation()); vi.mocked(useDeleteReferenceImage).mockReturnValue(createMockDeleteMutation()); });components/solutions/solution-steps.tsx-13-44 (1)
13-44: Reset reveal state whenstepschanges.
Ifstepsupdates while the component stays mounted,visibleStepIndexcan retain the previous value and skip the reveal sequence. Consider resetting before starting the observer sequence.🔧 Suggested fix
useEffect(() => { + setVisibleStepIndex(-1) const timers: Array<ReturnType<typeof setTimeout>> = [] const observer = new IntersectionObserver(convex/singleGeneration.ts-228-230 (1)
228-230: Consider removing sensitive term logging in production.The
console.logstatement logs matched NSFW terms to the console. In production, this could expose sensitive content in logs, potentially creating compliance/privacy concerns or making logs harder to review.Suggested fix
// Analyze prompt for NSFW content const promptAnalysis = analyzePromptForNSFW(args.prompt) - console.log(`[Prompt Analysis] Score: ${promptAnalysis.confidence}, Sensitive: ${promptAnalysis.isSensitive}, Terms: ${promptAnalysis.matchedTerms.join(", ")}`) + // Log only non-sensitive details; avoid logging matched terms in production + console.log(`[Prompt Analysis] Score: ${promptAnalysis.confidence}, Sensitive: ${promptAnalysis.isSensitive}, TermCount: ${promptAnalysis.matchedTerms.length}`)components/gallery/history-client.tsx-161-193 (1)
161-193: Race condition risk on rapid filter changes.When filters change quickly, multiple
fetchFiltered()calls may be in flight simultaneously, and responses could arrive out of order, leading to displaying stale data. As per coding guidelines,useEffectshould be used cautiously for data fetching.🔧 Suggested fix with ignore flag for stale responses
React.useEffect(() => { // Skip the initial mount if (prevFilterKeyRef.current === currentFilterKey) return prevFilterKeyRef.current = currentFilterKey // Check if filters are now empty (reset to initial state) const hasFilters = filterState.selectedVisibility.length > 0 || filterState.selectedModels.length > 0 + let ignore = false + async function fetchFiltered() { setIsLoadingFilters(true) try { const result = await loadMyHistoryWithDisplayPage({ cursor: null, filters: hasFilters ? queryFilters : undefined, }) + if (ignore) return setItems(result.page) setCursor(result.continueCursor) setIsDone(result.isDone) } catch (error) { + if (ignore) return console.error("Failed to fetch filtered history:", error) } finally { + if (!ignore) setIsLoadingFilters(false) - setIsLoadingFilters(false) } } void fetchFiltered() // Exit selection mode... if (selectionMode) { setSelectionMode(false) deselectAll() } + + return () => { ignore = true } }, [currentFilterKey, filterState, queryFilters, selectionMode, setSelectionMode, deselectAll])components/studio/controls/collapsible-section.tsx-132-146 (1)
132-146: Same cursor conflict in expanded-state chevron trigger.Apply the same fix here for consistency.
🐛 Apply consistent fix
<CollapsibleTrigger className={cn( - "flex items-center justify-center p-2 rounded-none transition-colors outline-none focus-visible:ring-2 focus-visible:ring-sidebar-ring select-none !cursor-pointer", - disabled ? "cursor-not-allowed" : "" + "flex items-center justify-center p-2 rounded-none transition-colors outline-none focus-visible:ring-2 focus-visible:ring-sidebar-ring select-none", + disabled ? "!cursor-not-allowed" : "cursor-pointer" )}components/studio/controls/collapsible-section.tsx-109-120 (1)
109-120: Same cursor conflict in collapsed-state chevron trigger.The same
!cursor-pointervscursor-not-allowedconflict exists here.🐛 Apply consistent fix
<CollapsibleTrigger className={cn( - "flex items-center justify-center p-2 rounded-none transition-colors outline-none focus-visible:ring-2 focus-visible:ring-sidebar-ring select-none !cursor-pointer", - disabled ? "cursor-not-allowed" : "" + "flex items-center justify-center p-2 rounded-none transition-colors outline-none focus-visible:ring-2 focus-visible:ring-sidebar-ring select-none", + disabled ? "!cursor-not-allowed" : "cursor-pointer" )}components/studio/controls/collapsible-section.tsx-88-91 (1)
88-91: Cursor class conflict when disabled.The
!cursor-pointerclass uses!important, which will overridecursor-not-allowedwhen the component is disabled. This means disabled triggers will still show a pointer cursor.🐛 Suggested fix
<CollapsibleTrigger className={cn( - "flex items-center gap-3 flex-1 min-w-0 py-2 px-3 rounded-none transition-colors text-left outline-none focus-visible:ring-2 focus-visible:ring-sidebar-ring select-none !cursor-pointer", - disabled ? "cursor-not-allowed" : "" + "flex items-center gap-3 flex-1 min-w-0 py-2 px-3 rounded-none transition-colors text-left outline-none focus-visible:ring-2 focus-visible:ring-sidebar-ring select-none", + disabled ? "!cursor-not-allowed" : "cursor-pointer" )}convex/lib/openrouter.ts-102-106 (1)
102-106: Add validation for parsed JSON response.Unlike
parseResultinpromptInference.tswhich validates the schema, this function directly returns the parsed JSON without validating that it conforms toContentAnalysisResult. Malformed API responses could cause unexpected runtime behavior.🛠️ Suggested validation
// Remove any markdown code block formatting if present const jsonContent = content.replace(/^```json\n?/, '').replace(/\n?```$/, ''); // Parse JSON response - return JSON.parse(jsonContent); + const parsed = JSON.parse(jsonContent); + + // Validate required fields + const validNudity = ["none", "partial", "full"]; + const validSexual = ["none", "suggestive", "explicit"]; + const validViolence = ["none", "mild", "graphic"]; + + if (!validNudity.includes(parsed.nudity) || + !validSexual.includes(parsed.sexual_content) || + !validViolence.includes(parsed.violence) || + typeof parsed.confidence !== "number" || + typeof parsed.reasoning !== "string") { + throw new Error("Invalid response format from OpenRouter"); + } + + return parsed as ContentAnalysisResult;app/feed/[type]/page.tsx-114-131 (1)
114-131: Lint/security rule will flagdangerouslySetInnerHTML.Even though the JSON-LD is static, Biome’s
lint/security/noDangerouslySetInnerHtmlwill still fail. Add an explicit ignore with rationale to keep lint clean.🧹 Biome ignore with rationale
{isPublicFeed && ( + // biome-ignore lint/security/noDangerouslySetInnerHtml -- JSON-LD is static, not user-provided <script type="application/ld+json" dangerouslySetInnerHTML={{app/_server/cache/feed.ts-52-75 (1)
52-75: Add token validation before caching the following feed.The token returned by
getConvexClerkToken()can beundefinedeven whenuserIdis present. PassingundefinedtofetchQuerywill fail, and the cached result will store an error or empty state under the user-scoped key for up to 1 hour. Add a guard to fail fast:export async function getFollowingFeedPageCached( userId: string, cursor: Cursor, numItems: number = PAGE_SIZES.FEED ) { const token = await getConvexClerkToken() + if (!token) { + throw new Error("Missing Convex auth token for following feed") + } const isFirstPage = cursor === nullApply the same fix to
getFavoritesPageCached()and both functions inhistory.ts(getMyImagesPageCached()andgetMyImagesWithDisplayDataCached()).components/gallery/feed-client.tsx-45-51 (1)
45-51: Reset pagination when sensitivity preference changes on public feed.The initial page loads with the server's preference, but subsequent pages use the current preference via
filterPreferencein theloadMorecall (line 75). If a user changes their preference while viewing, results mix filters—some items filtered by the old preference, others by the new one. Add auseEffectto reset items, cursor, andisDonewhenpreferencechanges on the public feed only.💡 Reset sketch
+ React.useEffect(() => { + if (!isPublicFeed) return + setItems([]) + setCursor(null) + setIsDone(false) + }, [preference, isPublicFeed])convex/generatedImages.ts-93-116 (1)
93-116: Doc mismatch in helper comment.
Line 94-96 says thumbnails are used, but Line 101-105 intentionally use full-size URLs. Update the comment to avoid confusion.📝 Suggested fix
- * Helper to map enriched images to optimized public feed format. - * Reduces bandwidth by using thumbnails and excluding unused fields. + * Helper to map enriched images to optimized public feed format. + * Excludes heavy fields; uses full-size URLs for feed card quality..kiro/specs/pollen-balance-display/design.md-88-104 (1)
88-104: Address markdownlint MD040/MD036 for fenced blocks and emphasis headings.The doc has unlabeled fenced blocks and bold/emphasis lines used like headings. If markdownlint runs in CI, it will fail. Consider adding language identifiers and converting the emphasized “Validates” lines into proper headings or plain text.
✍️ Example fixes
-``` +```text components/ ├── pollen-balance/ │ ├── index.ts │ ├── pollen-balance-display.tsx │ └── pollen-balance-display-view.tsx-**Validates: Requirements 2.1** +#### Validates: Requirements 2.1-``` +```text hooks/ ├── use-pollen-balance.test.tsAlso applies to: 292-320, 380-390
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
components/layout/header.tsx (1)
257-269: Fix inconsistent indentation.Line 258 has excessive indentation compared to the surrounding code, breaking consistency.
🧹 Suggested fix
{/* Dev-only Modals */} - {isLocalDev && ( + {isLocalDev && ( <> <UpgradeModal isOpen={upgradeModalOpen} onClose={() => setUpgradeModalOpen(false)} /> <ApiKeyOnboardingModal forceOpen={onboardingModalOpen} onClose={() => setOnboardingModalOpen(false)} /> </> )}components/studio/features/canvas/canvas-feature.test.tsx (1)
200-207: Test will fail because mockImage lacks the_idproperty needed for toggle favorite to work.The implementation's
handleToggleFavoritechecksconvexImageId = currentImage?._idand returns early if undefined (line 73). SincemockImageis created without_id, the function never callsmutateAsync, but the test expects it to be called.Either provide
_idwhen creatingmockImage:const mockImage = createMockImage({ id: "test-image-1", _id: "test-image-1", // Add Convex ID url: "https://example.com/image.jpg", ... })And update the assertion to expect the correct field:
expect(mockMutateAsync).toHaveBeenCalledWith({ imageId: mockImage._id })
🤖 Fix all issues with AI agents
In @.agent/rules/coding-standards.md:
- Line 21: Fix the markdown bold formatting by removing the stray space inside
the emphasis markers: change the text that currently reads "Ensure " (shown as
"**Ensure **") to "**Ensure**" so the line becomes "**Ensure** any new or
updated code have had their test files created or updated idiomatically using
RTL gold standards and vite test usage." Locate the offending text in the file
where the string "**Ensure ** any new or updated code..." appears and update it
to "**Ensure**" to resolve the linter warning.
In `@components/pollen-balance/pollen-balance-display-view.tsx`:
- Around line 93-99: The Badge rendering currently assumes formattedBalance is
non-null; when formattedBalance is null the Badge shows only the icon. Update
the render in the pollen-balance-display-view component to provide a fallback
when formattedBalance is null (e.g., show a placeholder like "—" or "0") so the
Badge always has visible text; modify the JSX around formattedBalance and keep
the existing isLowBalance icon logic (Badge, isLowBalance, formattedBalance) to
use the fallback value when formattedBalance === null.
In `@components/studio/canvas/luminous-tide-effect.tsx`:
- Around line 53-60: LuminousTideEffectProps declares isActive and progress but
the LuminousTideEffect component never uses them so the API is misleading;
inspect call sites (e.g., run ripgrep for "LuminousTideEffect") to confirm
whether consumers pass isActive/progress, then either (A) wire isActive into the
component render/animation by gating the returned JSX and starting/stopping
animations when isActive is false, and apply progress to the animation (for
example expose a CSS variable or prop-derived value used by the animation loop),
or (B) if no callers use these props, remove isActive and progress from
LuminousTideEffectProps and any related prop handling to simplify the API;
reference LuminousTideEffectProps and the LuminousTideEffect component when
making the change.
In `@components/studio/controls/collapsible-section.tsx`:
- Around line 88-91: The class string for the CollapsibleTrigger contains an
"!cursor-pointer" which uses Tailwind's important modifier and overrides the
conditional "cursor-not-allowed" when the component is disabled; update the
className in the CollapsibleTrigger usages (the className passed into the
CollapsibleTrigger and any other identical CollapsibleTrigger instances) to
remove the "!" prefix from "cursor-pointer" so the conditional disabled class
"cursor-not-allowed" can take effect, leaving the pointer-events-none behavior
intact.
In `@components/studio/features/prompt-library/prompt-detail.test.tsx`:
- Around line 71-82: The assertion that "Copied!" appears can race because the
click triggers an async state update; in the test for PromptDetail replace the
synchronous check using screen.getByText("Copied!") with an async query such as
await screen.findByText("Copied!") (or wrap it in waitFor) after invoking
userEvent.click(copyBtn) and verifying onCopy was called, so the test waits for
the DOM update; reference PromptDetail, onCopy, copyBtn, userEvent, getByRole
and getByText in the change.
- Around line 13-15: The test helper createPromptId uses a double-cast to force
a branded Id<"prompts"> which breaks typing rules; replace it with a proper Id
construction (e.g., use new Id("prompts", value) or a helper from convex-test)
so the returned value is a correctly branded Id<"prompts"> without using as
casts; update the createPromptId function to call the Id constructor or convex
test helper and return that typed Id instead of performing an as unknown as
cast.
In `@components/ui/sensitive-content-overlay.test.tsx`:
- Around line 5-22: Tests leak calls from the mocked toast and mockPush across
examples; add a beforeEach hook in the SensitiveContentOverlay test suite to
clear mocks before each test (e.g., call vi.clearAllMocks() or
vi.resetAllMocks()) so that toast and mockPush call histories are reset; update
the describe("SensitiveContentOverlay", ...) block to include this beforeEach to
ensure toast and mockPush are clean for every test.
♻️ Duplicate comments (1)
components/pollen-balance/pollen-balance-display-view.tsx (1)
14-25: MissingonRefresh/isRefreshingprops for manual refresh capability.The interface doesn't include props for manual refresh, which was already flagged in a previous review. Please refer to the existing suggestion to add
onRefresh?: () => voidandisRefreshing?: booleanto meet the spec requirement for clickable refresh.
🧹 Nitpick comments (30)
.github/workflows/test.yml (1)
12-19: Consider adding dependency caching for faster CI runs.The workflow could benefit from caching bun dependencies to speed up subsequent runs.
⚡ Optional: Add dependency caching
steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 with: bun-version: 1.3.6 + - uses: actions/cache@v4 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }} + restore-keys: | + ${{ runner.os }}-bun- - run: bun install --frozen-lockfile - run: bun run lint - run: bun run testcomponents/settings/star-repo-card.tsx (1)
24-38: Remove extraneous empty lines inside JSX.Lines 25-26 contain empty whitespace that appears to be leftover from editing. Consider removing these for cleaner code.
🧹 Suggested cleanup
- <div className="flex flex-col sm:flex-row items-center gap-6 p-6 rounded-2xl bg-gradient-to-br from-gray-900 to-black text-white shadow-xl border border-white/10 relative overflow-hidden"> - - + <div className="flex flex-col sm:flex-row items-center gap-6 p-6 rounded-2xl bg-gradient-to-br from-gray-900 to-black text-white shadow-xl border border-white/10 relative overflow-hidden"> <Imagecomponents/gallery/visibility-toggle.test.tsx (1)
54-55: Minor: Remove leftover blank lines.Lines 54 and 71 appear to be artifacts from removing
userEvent.setup(). Consider removing these extra blank lines for cleaner formatting.Also applies to: 71-72
components/gallery/paginated-image-grid.test.tsx (1)
38-45: Consider adding test coverage for the newuserShowsSensitiveprop.Based on the summary,
PaginatedImageGridnow forwards auserShowsSensitiveprop toImageCard. The current mock doesn't capture this prop, and there are no tests verifying this forwarding behavior. Consider adding a test case to ensure the prop is passed correctly.🧪 Optional: Add test for userShowsSensitive prop forwarding
Update the mock to capture the prop:
vi.mock("@/components/ui/image-card", () => ({ - ImageCard: vi.fn(({ image, selectionMode, isSelected, onSelectionChange }) => ( - <div data-testid="image-card" data-id={image._id} data-selected={isSelected}> + ImageCard: vi.fn(({ image, selectionMode, isSelected, onSelectionChange, userShowsSensitive }) => ( + <div data-testid="image-card" data-id={image._id} data-selected={isSelected} data-shows-sensitive={userShowsSensitive}> {selectionMode && <span>Selection Mode On</span>} <button onClick={() => onSelectionChange?.(image._id, !isSelected)}>Toggle</button> </div> )), }))Then add a test case:
it("passes userShowsSensitive prop to ImageCard", () => { render(<PaginatedImageGrid {...defaultProps} userShowsSensitive={true} />) const cards = screen.getAllByTestId("image-card") expect(cards[0]).toHaveAttribute("data-shows-sensitive", "true") })components/ui/rich-tooltip.tsx (1)
1-5: Consider reordering imports for consistency.By convention, third-party imports (like
react) typically precede local imports. This is a minor style point.+import * as React from "react" import { TooltipContent as BaseTooltipContent } from "@/components/ui/tooltip" import { cn } from "@/lib/utils" -import * as React from "react"components/studio/controls/aspect-ratio-selector.tsx (1)
152-154: ExtractactiveClassesto a shared constant.This exact styling pattern is duplicated across multiple control components:
model-selector.tsx,aspect-ratio-selector.tsx, andresolution-tier-selector.tsx. Create a shared constant (e.g., in aconstants.tsorstyles.tsfile within the controls directory) to eliminate the duplication and maintain consistency.components/studio/features/prompt-library/prompt-library.test.tsx (1)
110-112: Consider adding a brief comment explaining the cast.The
as unknown asdouble cast is necessary here because Convex'sId<"prompts">is a branded type that can't be constructed directly outside the Convex runtime. For test mocks, this is a pragmatic approach. A brief comment would help future maintainers understand why this exception to the "avoidascasts" guideline is intentional.📝 Suggested comment
function createPromptId(value: string): Id<"prompts"> { + // Convex IDs are branded types; cast required for test mocks return value as unknown as Id<"prompts"> }components/gallery/favorites-client.tsx (1)
43-44: Consider providing user feedback on load failure.Currently, errors are only logged to console. Users won't know if loading failed. Consider adding an error state to display a retry option or toast notification.
Example approach
const [isLoadingMore, setIsLoadingMore] = React.useState(false) +const [loadError, setLoadError] = React.useState<string | null>(null) const loadMore = React.useCallback(async () => { if (isDone || isLoadingMore || !cursor) return setIsLoadingMore(true) + setLoadError(null) try { const result = await loadFavoritesPage({ cursor }) setItems(prev => [...prev, ...result.page]) setCursor(result.continueCursor) setIsDone(result.isDone) } catch (error) { console.error("Failed to load more favorites:", error) + setLoadError("Failed to load more favorites. Tap to retry.") } finally { setIsLoadingMore(false) } }, [cursor, isDone, isLoadingMore])Then conditionally render an error/retry UI in the component.
components/pollen-balance/pollen-balance-display-view.tsx (1)
59-61: Consider extracting duplicated TooltipContent className.The same lengthy className string appears in both the error and success tooltip content. Extracting to a constant improves maintainability.
♻️ Suggested refactor
+const tooltipContentClassName = + "p-3 min-w-[200px] bg-popover border border-border dark:border-white/15 text-popover-foreground shadow-[0_20px_60px_0px_rgba(0,0,0,0.8)] animate-in fade-in-0 zoom-in-95 duration-200" + export const PollenBalanceDisplayView = memo(function PollenBalanceDisplayView({Then use
className={tooltipContentClassName}in both TooltipContent instances.Also applies to: 101-103
components/studio/features/generation/controls-view.tsx (1)
400-417: Consider memoizing the IIFE in collapsedContent.The immediately invoked function expression (IIFE) that calculates
activeCountis re-created on every render. While the performance impact is minimal for this simple calculation, for consistency with the component's optimization approach (React.memo), you could extract this to auseMemo.♻️ Optional refactor
+ const activeOptionsCount = React.useMemo( + () => Object.values(options).filter(Boolean).length, + [options] + ); + // In JSX: collapsedContent={ - (() => { - const activeCount = Object.values(options).filter(Boolean).length; - return activeCount > 0 ? ( - <span className={cn(badgeClassName, "tabular-nums")}> - {activeCount} active - </span> - ) : undefined; - })() + activeOptionsCount > 0 ? ( + <span className={cn(badgeClassName, "tabular-nums")}> + {activeOptionsCount} active + </span> + ) : undefined }components/studio/upgrade-modal.test.tsx (2)
111-122: LGTM!Using
findByTextis the correct async-aware approach for testing the loading state. The test properly verifies both the visual feedback ("Redirecting...") and the button disabled state.Minor readability nit: Line 113 is long. Consider extracting the delay logic for clarity:
♻️ Optional: Extract mock implementation for readability
it("handles checkout loading state", async () => { const user = userEvent.setup(); - mockCreateCheckout.mockImplementation(() => new Promise((resolve) => setTimeout(() => resolve({ url: "https://stripe.com" }), 100))); + mockCreateCheckout.mockImplementation( + () => new Promise((resolve) => setTimeout(() => resolve({ url: "https://stripe.com" }), 100)) + ); render(<UpgradeModal isOpen={true} onClose={vi.fn()} />);
124-137: Consider restoring console spy after test.The
consoleSpycreated on line 127 (and similarly on line 141) suppressesconsole.errorbut is never restored. Whilevi.clearAllMocks()clears call history, it doesn't restore original implementations. Consider usingmockRestore()in cleanup or usevi.restoreAllMocks()inafterEach.♻️ Optional: Restore spy to prevent leaking
await waitFor(() => { expect(consoleSpy).toHaveBeenCalledWith("Checkout error:", expect.any(Error)); expect(toast.error).toHaveBeenCalledWith("Checkout failed", expect.any(Object)); }); + consoleSpy.mockRestore(); });Or add a global
afterEach:afterEach(() => { vi.restoreAllMocks(); });components/layout/header.tsx (1)
42-46: Consider adding a brief comment explaining this client-detection idiom.The
useSyncExternalStorepattern for client detection is valid and SSR-safe, but non-obvious to readers unfamiliar with it.📝 Suggested improvement
+ // SSR-safe client detection: returns false on server, true on client after hydration const isClient = useSyncExternalStore( () => () => {}, () => true, () => false )components/studio/controls/collapsible-section.tsx (1)
139-145: Redundant conditional for chevron rotation.Since this
CollapsibleTriggeronly renders whenisExpandedistrue(line 131), the ternaryisExpanded ? "rotate-90" : ""will always evaluate to"rotate-90".Simplification
<ChevronRight data-testid={testId ? `${testId}-chevron` : undefined} - className={cn( - "h-4 w-4 text-muted-foreground/50 transition-transform shrink-0", - isExpanded ? "rotate-90" : "" - )} + className="h-4 w-4 text-muted-foreground/50 transition-transform shrink-0 rotate-90" />components/ui/media-player.test.tsx (1)
102-132: Avoidas HTMLVideoElement; use a runtime type guard instead.Keeps strict typing without assertions and aligns with the TS guideline to avoid
as.♻️ Proposed adjustment
- const video = screen.getByTestId("media-video") as HTMLVideoElement + const video = screen.getByTestId("media-video") + if (!(video instanceof HTMLVideoElement)) { + throw new Error("Expected media-video to be a <video> element") + }As per coding guidelines, avoid
ascasts in TypeScript.components/studio/features/canvas/canvas-view.test.tsx (1)
9-22: Consider adding test coverage for theprogressprop.The
ImageCanvasmock doesn't capture theprogressprop, and there's no test verifying thatprogressis correctly passed through fromCanvasViewtoImageCanvas. This prop was added in the implementation but isn't validated in tests.Suggested mock update
vi.mock("@/components/studio", () => ({ - ImageCanvas: ({ image, isGenerating, onImageClick, children }: { + ImageCanvas: ({ image, isGenerating, progress, onImageClick, children }: { image: GeneratedImage | null; isGenerating: boolean; + progress?: number; onImageClick?: () => void; children?: React.ReactNode; }) => ( <div data-testid="image-canvas" onClick={onImageClick}> <span data-testid="canvas-has-image">{String(!!image)}</span> <span data-testid="canvas-is-generating">{String(isGenerating)}</span> + {typeof progress === "number" && <span data-testid="canvas-progress">{progress}</span>} {image && <span data-testid="canvas-image-id">{image.id}</span>} {children} </div> ),Then add a test case:
it("passes progress to ImageCanvas", () => { render(<CanvasView {...defaultProps} isGenerating={true} progress={50} />) expect(screen.getByTestId("canvas-progress")).toHaveTextContent("50") })components/studio/features/canvas/canvas-feature.tsx (1)
36-38: Consider extracting the shared ID validation logic.The
isGeneratedImagesIdfunction duplicates theisConvexIdlogic already present inhooks/queries/use-favorites.ts(lines 24-31). Consider exporting a shared utility to avoid divergence.Option: Export from use-favorites or create a shared utility
// In a shared location like lib/convex-utils.ts or exported from use-favorites.ts export function isGeneratedImagesId(id?: string): id is Id<"generatedImages"> { return !!id && !id.startsWith("img_") }Then import in both locations to maintain consistency.
components/studio/controls/reference-images-browser-modal.test.tsx (1)
194-210: Redundant empty‑state test can be removed.The empty-state scenario is already covered in the search block (see Line 139), so this block is duplicate coverage and can be dropped to reduce maintenance.
♻️ Suggested cleanup
-describe("ReferenceImagesBrowserModal - search empty state", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it("shows empty state message when search yields no results", async () => { - const mockOnSelect = vi.fn(); - const mockOnOpenChange = vi.fn(); - - render(<ReferenceImagesBrowserModal open={true} onOpenChange={mockOnOpenChange} onSelect={mockOnSelect} />); - - // Type something that won't match - const searchInput = screen.getByTestId("reference-images-search"); - fireEvent.change(searchInput, { target: { value: "zzzznonexistent" } }); - - expect(screen.getByText("No images match your search")).toBeInTheDocument(); - }); -});components/studio/features/prompt-library/prompt-detail.test.tsx (1)
1-2: Scope the confirm “Remove” click to the alert dialog.Picking the last “Remove” button is order-dependent. Scoping to the dialog makes the test resilient.
♻️ More robust dialog-scoped query
-import { render, screen } from "@testing-library/react"; +import { render, screen, within } from "@testing-library/react"; @@ - const removeButtons = screen.getAllByRole("button", { name: "Remove" }); - const confirmButton = removeButtons[removeButtons.length - 1]; + const dialog = screen.getByRole("alertdialog"); + const confirmButton = within(dialog).getByRole("button", { name: "Remove" });Also applies to: 123-126
components/images/image-lightbox.test.tsx (1)
417-424: Avoid index-based button selection.The order-dependent selector is brittle if button ordering changes. Prefer an accessible name or a stable test id.
♻️ Suggested refactor
- const copyButton = screen.getAllByRole("button")[1]; // Second button after save to library + const copyButton = screen.getByRole("button", { name: /copy prompt/i });If the button doesn’t expose an accessible name, consider adding an
aria-labelordata-testidin the component and targeting that here.components/pricing/model-value-showcase.tsx (2)
4-7: Consider honoring reduced-motion for the counter animation.For users who prefer reduced motion, skip RAF animation and set the value immediately.
♿ Proposed update
-import { motion, useInView } from "framer-motion" +import { motion, useInView, useReducedMotion } from "framer-motion" @@ function AnimatedNumber({ value, delay = 0 }: { value: number; delay?: number }) { const [displayValue, setDisplayValue] = useState(0) const ref = useRef<HTMLSpanElement>(null) const isInView = useInView(ref, { once: true, margin: "-100px" }) + const prefersReducedMotion = useReducedMotion() useEffect(() => { if (!isInView) return + if (prefersReducedMotion) { + setDisplayValue(value) + return + } @@ - }, [isInView, value, delay]) + }, [isInView, value, delay, prefersReducedMotion])Also applies to: 133-166
206-215: Add descriptive alt text for model logos (or mark as decorative).
alt=""hides the logo from screen readers; using the model name improves accessibility.📝 Suggested change
- alt="" + alt={`${model.displayName} logo`}components/ui/sensitive-content-overlay.test.tsx (1)
33-35: Prefer a stable target for overlay clicks.DOM traversal via
closest(...).parentElement+ non‑null assertions is brittle. Consider adding adata-testidorrole="button"on the overlay and usinggetByTestId/getByRoleinstead for robustness (and easier future refactors).♻️ Example test change (requires adding data-testid on the overlay)
-const overlay = screen.getByText("Sensitive Content").closest("div")?.parentElement -fireEvent.click(overlay!) +const overlay = screen.getByTestId("sensitive-content-overlay") +fireEvent.click(overlay)Also applies to: 50-51, 73-74
components/studio/controls/reference-image-picker.test.tsx (1)
72-83: Avoidas unknown asby usingsatisfiesfor mock objects.
This keeps type safety without weakening checks. As per coding guidelines, avoidascasts.♻️ Proposed refactor using
satisfiesbeforeEach(() => { vi.clearAllMocks(); vi.mocked(useReferenceImages).mockReturnValue(mockRecentImages); - vi.mocked(useUploadReference).mockReturnValue({ - mutateAsync: vi.fn(), - isPending: false, - } as unknown as ReturnType<typeof useUploadReference>); - vi.mocked(useDeleteReferenceImage).mockReturnValue({ - mutateAsync: vi.fn(), - isPending: false, - } as unknown as ReturnType<typeof useDeleteReferenceImage>); + const uploadMock = { + mutateAsync: vi.fn(), + isPending: false, + } satisfies ReturnType<typeof useUploadReference>; + const deleteMock = { + mutateAsync: vi.fn(), + isPending: false, + } satisfies ReturnType<typeof useDeleteReferenceImage>; + vi.mocked(useUploadReference).mockReturnValue(uploadMock); + vi.mocked(useDeleteReferenceImage).mockReturnValue(deleteMock); });components/studio/canvas/image-toolbar.tsx (1)
63-67: Clear the copy-feedback timeout on unmount.Without cleanup, rapid clicks or unmounts can leave pending timers that call
setCopiedafter unmount.♻️ Suggested fix
- const [copied, setCopied] = React.useState(false) + const [copied, setCopied] = React.useState(false) + const copyResetRef = React.useRef<number | null>(null) @@ - const handleCopyUrl = async () => { - await onCopyUrl?.() - setCopied(true) - setTimeout(() => setCopied(false), 2000) - } + const handleCopyUrl = async () => { + await onCopyUrl?.() + setCopied(true) + if (copyResetRef.current !== null) { + window.clearTimeout(copyResetRef.current) + } + copyResetRef.current = window.setTimeout(() => setCopied(false), 2000) + } + + React.useEffect(() => { + return () => { + if (copyResetRef.current !== null) { + window.clearTimeout(copyResetRef.current) + } + } + }, [])components/studio/canvas/image-canvas.tsx (1)
26-41: Avoidas const; use typed literals/satisfies.This aligns with the TS guideline to avoid casts while preserving literal types.
As per coding guidelines, avoid `as` casts.♻️ Suggested refactor
-const EXPO_OUT = [0.22, 1, 0.36, 1] as const +const EXPO_OUT: [number, number, number, number] = [0.22, 1, 0.36, 1] @@ -const TIMING = { +const TIMING = { iconMorph: 250, // Icon transition duration textStagger: 30, // Per-letter delay for text reveal -} as const +} satisfies { iconMorph: number; textStagger: number }components/gallery/feed-client.tsx (1)
92-98: Avoid useEffect-driven data fetching for auto-load.This effect triggers
loadMore()(network fetch) when items are empty. The TSX guideline explicitly avoidsuseEffectfor app data fetching; consider moving empty-page skipping into the server cache/page preparation so the client receives a non-emptyinitialPage, or otherwise gate this behavior outsideuseEffect.As per coding guidelines, avoid
useEffectfor app data fetching.components/gallery/history-client.test.tsx (1)
29-31: Reduce unsafeas unknown ascasts in test fixtures.These casts bypass type checking; you can keep safety using
satisfies/typed mock helpers (and if Convex exposes an ID helper, prefer that increateGeneratedImageId).♻️ Suggested refactor to avoid `as` casts
- const mockSelection = { + const mockSelection = { selectionMode: false, setSelectionMode: vi.fn(), selectedIds: new Set<string>(), toggleSelection: vi.fn(), selectAll: vi.fn(), deselectAll: vi.fn(), handleDeleteSelected: vi.fn(), handleSetSelectedVisibility: vi.fn(), isDeleting: false, isUpdatingVisibility: false, - } + } satisfies ReturnType<typeof useImageSelection> beforeEach(() => { vi.clearAllMocks() - vi.mocked(useUser).mockReturnValue({ user: { id: "user1" } } as unknown as ReturnType<typeof useUser>) - vi.mocked(useImageSelection).mockReturnValue(mockSelection as unknown as ReturnType<typeof useImageSelection>) - vi.mocked(useLocalStorage).mockReturnValue([ - { selectedVisibility: [], selectedModels: [] }, - vi.fn(), - ] as unknown as ReturnType<typeof useLocalStorage>) + const mockUser = { user: { id: "user1" } } satisfies ReturnType<typeof useUser> + const mockStorage = [ + { selectedVisibility: [], selectedModels: [] }, + vi.fn(), + ] satisfies ReturnType<typeof useLocalStorage> + vi.mocked(useUser).mockReturnValue(mockUser) + vi.mocked(useImageSelection).mockReturnValue(mockSelection) + vi.mocked(useLocalStorage).mockReturnValue(mockStorage) })As per coding guidelines, avoid
ascasts in TypeScript.Also applies to: 105-126
components/gallery/history-client.tsx (2)
168-201: Consider moving filter-driven fetch out ofuseEffect.Guideline asks to avoid
useEffectfor data fetching in TSX. A cleaner alternative is a dedicatedhandleFiltersChange(next)that updates localStorage state and triggersloadMyHistoryWithDisplayPagedirectly from the filter-change handler (bothHistoryFiltersDropdownandActiveFilterBadges).As per coding guidelines, avoid
useEffectfor app data fetching in TSX.
224-227: Drop theas { _id: string }cast inhandleSelectAll.
Id<"generatedImages">is already a string subtype; the cast shouldn’t be needed ifselectAllaccepts the ID type. If it does not, consider wideningselectAll’s input type instead of casting here.♻️ Suggested tweak
- selectAll(items.map((r) => ({ _id: r._id } as { _id: string }))) + selectAll(items.map((r) => ({ _id: r._id })))As per coding guidelines, avoid
ascasts in TypeScript.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (52)
.agent/rules/coding-standards.md.github/workflows/test.ymlapp/_server/cache/favorites.tsapp/_server/cache/history.tsapp/globals.csscomponents/gallery/favorites-client.tsxcomponents/gallery/feed-client.test.tsxcomponents/gallery/feed-client.tsxcomponents/gallery/history-client.test.tsxcomponents/gallery/history-client.tsxcomponents/gallery/paginated-image-grid.test.tsxcomponents/gallery/visibility-toggle.test.tsxcomponents/images/image-lightbox.test.tsxcomponents/layout/header.test.tsxcomponents/layout/header.tsxcomponents/pollen-balance/pollen-balance-display-view.tsxcomponents/pollen-balance/pollen-balance-display.test.tsxcomponents/pricing/model-value-showcase.tsxcomponents/settings/star-repo-card.tsxcomponents/studio/canvas/canvas-wave.tsxcomponents/studio/canvas/image-canvas.test.tsxcomponents/studio/canvas/image-canvas.tsxcomponents/studio/canvas/image-toolbar.tsxcomponents/studio/canvas/luminous-tide-effect.tsxcomponents/studio/controls/aspect-ratio-selector.tsxcomponents/studio/controls/collapsible-section.tsxcomponents/studio/controls/debug.test.tsxcomponents/studio/controls/model-selector.tsxcomponents/studio/controls/reference-image-picker.test.tsxcomponents/studio/controls/reference-images-browser-modal.test.tsxcomponents/studio/controls/video-reference-image-picker.test.tsxcomponents/studio/controls/video-reference-image-picker.tsxcomponents/studio/features/canvas/canvas-feature.test.tsxcomponents/studio/features/canvas/canvas-feature.tsxcomponents/studio/features/canvas/canvas-view.test.tsxcomponents/studio/features/canvas/canvas-view.tsxcomponents/studio/features/generation/controls-view.tsxcomponents/studio/features/prompt-library/prompt-detail.test.tsxcomponents/studio/features/prompt-library/prompt-library.test.tsxcomponents/studio/upgrade-modal.test.tsxcomponents/subscription/subscription-badge.test.tsxcomponents/ui/media-player.test.tsxcomponents/ui/rich-tooltip.tsxcomponents/ui/sensitive-content-overlay.test.tsxconvex/tempTagStats.test.tsconvex/tempTagStats.tshooks/mutations/use-delete-image.test.tsxlib/prompt-enhancement/prompt-enhancer.tslib/prompt-enhancement/suggestion-generator.tspackage.jsonscripts/generate-lint-docs.jstodo/generating-state-design.md
✅ Files skipped from review due to trivial changes (1)
- components/studio/controls/video-reference-image-picker.tsx
🚧 Files skipped from review as they are similar to previous changes (7)
- app/_server/cache/favorites.ts
- components/studio/controls/model-selector.tsx
- components/layout/header.test.tsx
- components/pollen-balance/pollen-balance-display.test.tsx
- app/globals.css
- components/studio/controls/video-reference-image-picker.test.tsx
- app/_server/cache/history.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: Always leverage TypeScript (strict mode); avoid usage ofanywhich defeats the point of using TypeScript.
Let TypeScript inference work; export explicit prop types for shared/public components.
Noanytype in TypeScript. Useunknownand narrow instead.
Avoidascasts in TypeScript; prefersatisfies, generics, and runtime validation.
@ts-expect-errorallowed only with a comment explaining why + link/TODO to remove.
Files:
components/gallery/paginated-image-grid.test.tsxcomponents/ui/media-player.test.tsxcomponents/studio/controls/debug.test.tsxcomponents/studio/canvas/image-canvas.test.tsxcomponents/subscription/subscription-badge.test.tsxcomponents/studio/controls/collapsible-section.tsxcomponents/studio/features/generation/controls-view.tsxcomponents/studio/features/canvas/canvas-view.test.tsxcomponents/studio/controls/reference-image-picker.test.tsxcomponents/gallery/favorites-client.tsxcomponents/studio/canvas/luminous-tide-effect.tsxcomponents/images/image-lightbox.test.tsxcomponents/gallery/feed-client.tsxcomponents/studio/features/canvas/canvas-feature.test.tsxcomponents/ui/sensitive-content-overlay.test.tsxcomponents/studio/canvas/canvas-wave.tsxcomponents/pricing/model-value-showcase.tsxcomponents/gallery/history-client.tsxcomponents/ui/rich-tooltip.tsxcomponents/studio/features/prompt-library/prompt-library.test.tsxcomponents/studio/features/canvas/canvas-feature.tsxcomponents/studio/features/prompt-library/prompt-detail.test.tsxcomponents/studio/canvas/image-canvas.tsxcomponents/gallery/history-client.test.tsxcomponents/gallery/visibility-toggle.test.tsxcomponents/studio/controls/aspect-ratio-selector.tsxcomponents/studio/controls/reference-images-browser-modal.test.tsxcomponents/studio/upgrade-modal.test.tsxcomponents/studio/features/canvas/canvas-view.tsxcomponents/pollen-balance/pollen-balance-display-view.tsxcomponents/studio/canvas/image-toolbar.tsxcomponents/settings/star-repo-card.tsxcomponents/gallery/feed-client.test.tsxcomponents/layout/header.tsx
**/*.{tsx,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Avoid
useEffectfor app data fetching. Use it for browser-only side effects (subscriptions, observers, localStorage).
Files:
components/gallery/paginated-image-grid.test.tsxcomponents/ui/media-player.test.tsxcomponents/studio/controls/debug.test.tsxcomponents/studio/canvas/image-canvas.test.tsxcomponents/subscription/subscription-badge.test.tsxcomponents/studio/controls/collapsible-section.tsxcomponents/studio/features/generation/controls-view.tsxcomponents/studio/features/canvas/canvas-view.test.tsxcomponents/studio/controls/reference-image-picker.test.tsxcomponents/gallery/favorites-client.tsxcomponents/studio/canvas/luminous-tide-effect.tsxcomponents/images/image-lightbox.test.tsxcomponents/gallery/feed-client.tsxcomponents/studio/features/canvas/canvas-feature.test.tsxcomponents/ui/sensitive-content-overlay.test.tsxcomponents/studio/canvas/canvas-wave.tsxcomponents/pricing/model-value-showcase.tsxcomponents/gallery/history-client.tsxcomponents/ui/rich-tooltip.tsxcomponents/studio/features/prompt-library/prompt-library.test.tsxcomponents/studio/features/canvas/canvas-feature.tsxcomponents/studio/features/prompt-library/prompt-detail.test.tsxcomponents/studio/canvas/image-canvas.tsxcomponents/gallery/history-client.test.tsxcomponents/gallery/visibility-toggle.test.tsxcomponents/studio/controls/aspect-ratio-selector.tsxcomponents/studio/controls/reference-images-browser-modal.test.tsxcomponents/studio/upgrade-modal.test.tsxcomponents/studio/features/canvas/canvas-view.tsxcomponents/pollen-balance/pollen-balance-display-view.tsxcomponents/studio/canvas/image-toolbar.tsxcomponents/settings/star-repo-card.tsxcomponents/gallery/feed-client.test.tsxcomponents/layout/header.tsx
🧠 Learnings (17)
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to **/*.{ts,tsx} : No `any` type in TypeScript. Use `unknown` and narrow instead.
Applied to files:
components/gallery/paginated-image-grid.test.tsxcomponents/gallery/favorites-client.tsx
📚 Learning: 2026-01-16T04:49:14.822Z
Learnt from: Simplereally
Repo: Simplereally/bloomstudio PR: 13
File: convex/batchGeneration.ts:159-162
Timestamp: 2026-01-16T04:49:14.822Z
Learning: In convex/batchGeneration.ts, inside storeGeneratedImage (internalMutation), the team intentionally logs analyzePromptForNSFW results including matchedTerms (joined string) to server logs. This is considered a feature by the maintainer (Simplereally) and should not be flagged as an issue in future reviews unless requirements change.
Applied to files:
components/studio/canvas/image-canvas.test.tsx
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/*.{tsx,jsx} : Use shadcn/ui as the base component library; extend via composition + variants.
Applied to files:
components/studio/controls/collapsible-section.tsx.agent/rules/coding-standards.mdcomponents/ui/rich-tooltip.tsxcomponents/studio/features/prompt-library/prompt-library.test.tsxcomponents/studio/controls/aspect-ratio-selector.tsxcomponents/layout/header.tsx
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/*.{tsx,jsx} : Use Server Components by default in Next.js App Router. Add `'use client'` only at the leaves that need interactivity/hooks.
Applied to files:
.agent/rules/coding-standards.mdcomponents/gallery/favorites-client.tsxcomponents/gallery/feed-client.tsxcomponents/gallery/history-client.tsxcomponents/gallery/history-client.test.tsxcomponents/layout/header.tsx
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/*.{tsx,jsx} : Use `useQuery` / `useMutation` in Client Components for client reactivity with Convex.
Applied to files:
.agent/rules/coding-standards.mdcomponents/gallery/feed-client.tsxcomponents/gallery/history-client.tsxcomponents/gallery/visibility-toggle.test.tsxcomponents/layout/header.tsx
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/*.{tsx,jsx} : Prefer Convex optimistic updates (`useMutation(...).withOptimisticUpdate(...)`) over duplicating server logic in ad-hoc local state.
Applied to files:
.agent/rules/coding-standards.mdcomponents/gallery/favorites-client.tsxcomponents/studio/features/canvas/canvas-feature.test.tsxcomponents/gallery/history-client.tsxcomponents/studio/features/canvas/canvas-feature.tsxcomponents/gallery/history-client.test.tsxcomponents/gallery/visibility-toggle.test.tsx
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/*.{tsx,jsx} : Use `fetchQuery(...)` in Server Components for server-only (non-reactive) rendering with Convex.
Applied to files:
.agent/rules/coding-standards.mdcomponents/gallery/feed-client.tsxcomponents/gallery/history-client.tsx
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to **/*.{ts,tsx} : Always leverage TypeScript (strict mode); avoid usage of `any` which defeats the point of using TypeScript.
Applied to files:
.agent/rules/coding-standards.mdcomponents/gallery/favorites-client.tsx
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Convex is the source of truth for server state; local state is for UI-only concerns (selection, filters, draft text).
Applied to files:
.agent/rules/coding-standards.md
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/{actions,route}.{ts,tsx} : Call Convex via `fetchMutation` / `fetchAction` in Server Actions / Route Handlers when you must mutate from the server boundary.
Applied to files:
.agent/rules/coding-standards.md
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/*.{tsx,jsx} : In Server Components, use `preloadQuery(...)` and pass the payload to a Client Component using `usePreloadedQuery(...)` for SSR + reactivity.
Applied to files:
.agent/rules/coding-standards.mdcomponents/gallery/favorites-client.tsxcomponents/gallery/feed-client.tsxcomponents/gallery/history-client.tsx
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to **/*.{ts,tsx} : Avoid `as` casts in TypeScript; prefer `satisfies`, generics, and runtime validation.
Applied to files:
.agent/rules/coding-standards.mdcomponents/gallery/favorites-client.tsx
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/*.{tsx,jsx} : Use `next/image` for images in Next.js.
Applied to files:
components/studio/controls/reference-image-picker.test.tsxcomponents/studio/canvas/image-canvas.tsx
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/*.{tsx,jsx} : Prefer Actions + `useActionState` for form submission state when using React 19 form actions.
Applied to files:
components/gallery/favorites-client.tsx
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/*.{tsx,jsx} : Use `useOptimistic` in React 19 for instant UI feedback when appropriate.
Applied to files:
components/gallery/history-client.tsxcomponents/gallery/history-client.test.tsxcomponents/gallery/visibility-toggle.test.tsx
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to **/*.{ts,tsx} : Let TypeScript inference work; export explicit prop types for shared/public components.
Applied to files:
components/studio/features/prompt-library/prompt-library.test.tsx
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Presentational components should take props in and output JSX with no data fetching and minimal/no app logic.
Applied to files:
components/pollen-balance/pollen-balance-display-view.tsx
🧬 Code graph analysis (23)
components/ui/media-player.test.tsx (1)
components/ui/media-player.tsx (1)
MediaPlayer(61-167)
components/studio/controls/collapsible-section.tsx (1)
lib/utils.ts (1)
cn(11-13)
components/studio/features/generation/controls-view.tsx (3)
components/studio/controls/collapsible-section.tsx (1)
CollapsibleSection(44-171)components/studio/index.ts (2)
CollapsibleSection(15-15)ReferenceImagePicker(22-22)components/ui/button.tsx (1)
Button(79-79)
components/gallery/favorites-client.tsx (2)
app/_server/actions/favorites.ts (1)
loadFavoritesPage(9-15)components/gallery/paginated-image-grid.tsx (1)
PaginatedImageGrid(65-255)
components/studio/canvas/luminous-tide-effect.tsx (1)
lib/utils.ts (1)
cn(11-13)
components/gallery/feed-client.tsx (2)
app/_server/actions/feed.ts (2)
loadPublicFeedPage(10-16)loadFollowingFeedPage(23-33)lib/analytics.ts (1)
trackFeedView(69-77)
components/studio/features/canvas/canvas-feature.test.tsx (1)
components/studio/features/canvas/canvas-feature.tsx (1)
CanvasFeature(53-120)
components/ui/sensitive-content-overlay.test.tsx (1)
components/ui/sensitive-content-overlay.tsx (1)
SensitiveContentOverlay(15-73)
components/pricing/model-value-showcase.tsx (1)
lib/utils.ts (1)
cn(11-13)
components/gallery/history-client.tsx (3)
app/_server/actions/history.ts (1)
loadMyHistoryWithDisplayPage(26-33)components/gallery/history-filters.tsx (1)
HistoryFilterState(29-34)hooks/use-local-storage.ts (1)
useLocalStorage(13-78)
components/ui/rich-tooltip.tsx (1)
lib/utils.ts (1)
cn(11-13)
components/studio/features/prompt-library/prompt-library.test.tsx (1)
components/studio/features/prompt-library/save-prompt-form.tsx (1)
SavePromptFormProps(26-31)
components/studio/features/canvas/canvas-feature.tsx (1)
hooks/queries/use-favorites.ts (2)
useIsFavorited(24-40)useToggleFavorite(46-67)
components/studio/canvas/image-canvas.tsx (2)
lib/utils.ts (1)
cn(11-13)components/studio/canvas/canvas-wave.tsx (1)
CanvasWave(48-325)
components/gallery/history-client.test.tsx (3)
app/_server/actions/history.ts (1)
loadMyHistoryWithDisplayPage(26-33)hooks/use-local-storage.ts (1)
useLocalStorage(13-78)components/gallery/history-client.tsx (1)
HistoryClient(60-290)
components/studio/controls/aspect-ratio-selector.tsx (2)
lib/utils.ts (1)
cn(11-13)components/ui/rich-tooltip.tsx (1)
RichTooltipContent(7-31)
components/studio/controls/reference-images-browser-modal.test.tsx (1)
components/studio/index.ts (1)
ReferenceImagesBrowserModal(28-28)
components/studio/upgrade-modal.test.tsx (1)
components/studio/upgrade-modal.tsx (1)
UpgradeModal(47-193)
components/studio/features/canvas/canvas-view.tsx (1)
components/studio/canvas/image-canvas.tsx (1)
ImageCanvas(197-399)
components/pollen-balance/pollen-balance-display-view.tsx (4)
components/ui/rich-tooltip.tsx (2)
Tooltip(5-5)TooltipTrigger(5-5)components/ui/tooltip.tsx (1)
TooltipContent(62-62)lib/utils.ts (1)
cn(11-13)hooks/use-pollen-balance.ts (1)
isLowBalance(92-100)
components/settings/star-repo-card.tsx (1)
components/ui/button.tsx (1)
Button(79-79)
components/gallery/feed-client.test.tsx (2)
app/_server/actions/feed.ts (1)
loadPublicFeedPage(10-16)components/gallery/feed-client.tsx (1)
FeedClient(36-118)
components/layout/header.tsx (1)
lib/utils.ts (2)
isLocalhost(19-23)cn(11-13)
🪛 GitHub Actions: Test
components/studio/canvas/canvas-wave.tsx
[error] 157-157: eslint: @typescript-eslint/no-explicit-any - Unexpected any. Specify a different type.
components/studio/canvas/image-canvas.tsx
[warning] 29-29: eslint: @typescript-eslint/no-unused-vars - 'SPRING_REFINED' is assigned a value but never used.
[warning] 57-57: eslint: @typescript-eslint/no-unused-vars - 'hubVariants' is assigned a value but never used.
[warning] 71-71: eslint: @typescript-eslint/no-unused-vars - 'iconVariants' is assigned a value but never used.
🪛 markdownlint-cli2 (0.18.1)
.agent/rules/coding-standards.md
21-21: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| {isLowBalance ? ( | ||
| <AlertTriangle className="w-3 h-3 mr-1" /> | ||
| ) : ( | ||
| <Coins className="w-3 h-3 mr-1" /> | ||
| )} | ||
| {formattedBalance} | ||
| </Badge> |
There was a problem hiding this comment.
Consider handling null formattedBalance in success state.
When formattedBalance is null (which the interface allows), the badge will render with only the icon and no text. This could occur if the balance fetch succeeds but returns no data.
Consider adding a fallback display value:
💡 Suggested fix
{isLowBalance ? (
<AlertTriangle className="w-3 h-3 mr-1" />
) : (
<Coins className="w-3 h-3 mr-1" />
)}
- {formattedBalance}
+ {formattedBalance ?? "—"}
</Badge>📝 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.
| {isLowBalance ? ( | |
| <AlertTriangle className="w-3 h-3 mr-1" /> | |
| ) : ( | |
| <Coins className="w-3 h-3 mr-1" /> | |
| )} | |
| {formattedBalance} | |
| </Badge> | |
| {isLowBalance ? ( | |
| <AlertTriangle className="w-3 h-3 mr-1" /> | |
| ) : ( | |
| <Coins className="w-3 h-3 mr-1" /> | |
| )} | |
| {formattedBalance ?? "—"} | |
| </Badge> |
🤖 Prompt for AI Agents
In `@components/pollen-balance/pollen-balance-display-view.tsx` around lines 93 -
99, The Badge rendering currently assumes formattedBalance is non-null; when
formattedBalance is null the Badge shows only the icon. Update the render in the
pollen-balance-display-view component to provide a fallback when
formattedBalance is null (e.g., show a placeholder like "—" or "0") so the Badge
always has visible text; modify the JSX around formattedBalance and keep the
existing isLowBalance icon logic (Badge, isLowBalance, formattedBalance) to use
the fallback value when formattedBalance === null.
| export interface LuminousTideEffectProps { | ||
| /** Whether the effect is active */ | ||
| isActive?: boolean | ||
| /** Optional progress (0-100) to influence wave behavior */ | ||
| progress?: number | ||
| /** Additional className */ | ||
| className?: string | ||
| } |
There was a problem hiding this comment.
Wire or remove isActive / progress to avoid a misleading public API.
Both props are documented but unused, so the effect always runs and ignores progress. Either hook them up (e.g., gate rendering/animation) or remove them until supported.
🐛 One possible fix (gate on isActive and drop unused progress)
export interface LuminousTideEffectProps {
/** Whether the effect is active */
isActive?: boolean
- /** Optional progress (0-100) to influence wave behavior */
- progress?: number
/** Additional className */
className?: string
}
export const LuminousTideEffect = React.memo(function LuminousTideEffect({
className,
+ isActive = true,
}: LuminousTideEffectProps) {
+ if (!isActive) return null
return (
<motion.divPlease verify usage before removing/altering props. Expected result: confirm whether callers pass isActive/progress, then decide to wire or remove.
#!/bin/bash
# Find all usages of the component and inspect prop usage.
rg -n --type=tsx -C2 '\bLuminousTideEffect\b'Also applies to: 413-451
🤖 Prompt for AI Agents
In `@components/studio/canvas/luminous-tide-effect.tsx` around lines 53 - 60,
LuminousTideEffectProps declares isActive and progress but the
LuminousTideEffect component never uses them so the API is misleading; inspect
call sites (e.g., run ripgrep for "LuminousTideEffect") to confirm whether
consumers pass isActive/progress, then either (A) wire isActive into the
component render/animation by gating the returned JSX and starting/stopping
animations when isActive is false, and apply progress to the animation (for
example expose a CSS variable or prop-derived value used by the animation loop),
or (B) if no callers use these props, remove isActive and progress from
LuminousTideEffectProps and any related prop handling to simplify the API;
reference LuminousTideEffectProps and the LuminousTideEffect component when
making the change.
| className={cn( | ||
| "flex items-center gap-3 flex-1 min-w-0 py-2 px-3 rounded-none transition-colors text-left outline-none focus-visible:ring-2 focus-visible:ring-sidebar-ring select-none !cursor-pointer", | ||
| disabled ? "cursor-not-allowed" : "" | ||
| )} |
There was a problem hiding this comment.
!cursor-pointer overrides the disabled cursor-not-allowed style.
The Tailwind ! modifier applies !important, which will override the conditional cursor-not-allowed class. While the parent's pointer-events-none prevents interaction when disabled, removing the ! prefix maintains visual consistency.
Suggested fix
<CollapsibleTrigger
className={cn(
- "flex items-center gap-3 flex-1 min-w-0 py-2 px-3 rounded-none transition-colors text-left outline-none focus-visible:ring-2 focus-visible:ring-sidebar-ring select-none !cursor-pointer",
+ "flex items-center gap-3 flex-1 min-w-0 py-2 px-3 rounded-none transition-colors text-left outline-none focus-visible:ring-2 focus-visible:ring-sidebar-ring select-none cursor-pointer",
disabled ? "cursor-not-allowed" : ""
)}Apply the same change to the other CollapsibleTrigger elements on lines 110-113 and 133-136.
📝 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.
| className={cn( | |
| "flex items-center gap-3 flex-1 min-w-0 py-2 px-3 rounded-none transition-colors text-left outline-none focus-visible:ring-2 focus-visible:ring-sidebar-ring select-none !cursor-pointer", | |
| disabled ? "cursor-not-allowed" : "" | |
| )} | |
| className={cn( | |
| "flex items-center gap-3 flex-1 min-w-0 py-2 px-3 rounded-none transition-colors text-left outline-none focus-visible:ring-2 focus-visible:ring-sidebar-ring select-none cursor-pointer", | |
| disabled ? "cursor-not-allowed" : "" | |
| )} |
🤖 Prompt for AI Agents
In `@components/studio/controls/collapsible-section.tsx` around lines 88 - 91, The
class string for the CollapsibleTrigger contains an "!cursor-pointer" which uses
Tailwind's important modifier and overrides the conditional "cursor-not-allowed"
when the component is disabled; update the className in the CollapsibleTrigger
usages (the className passed into the CollapsibleTrigger and any other identical
CollapsibleTrigger instances) to remove the "!" prefix from "cursor-pointer" so
the conditional disabled class "cursor-not-allowed" can take effect, leaving the
pointer-events-none behavior intact.
| function createPromptId(value: string): Id<"prompts"> { | ||
| return value as unknown as Id<"prompts">; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Check the file and surrounding context
head -20 components/studio/features/prompt-library/prompt-detail.test.tsxRepository: Simplereally/bloomstudio
Length of output: 803
🌐 Web query:
Convex create mock ID test helper documentation
💡 Result:
Here are the official Convex testing docs and the community mock-backend (convex-test) that explain how to create test data and how IDs are handled:
- Convex testing overview (automated tests + convex-test library). Explains testing approaches and points to convex-test. [1]
- convex-test docs (mock Convex backend; examples of inserting data via t.mutation / t.run and limitations — including “ID format: your code should not depend on the document or storage ID format”). Use convex-test to create test documents and get their IDs from the returned value. [2]
- convex-test (GitHub / npm) — package and source examples. [3]
Useful links:
- Convex testing docs: https://docs.convex.dev/testing. [1]
- convex-test docs: https://docs.convex.dev/testing/convex-test. [2]
- convex-test repository / npm: get-convex/convex-test (GitHub / npm). [3]
If you want, I can:
- Extract the exact example code for creating test documents (and showing the returned ID) from convex-test and paste it here, or
- Provide a small ready-to-run Vitest example that creates mock IDs and asserts against them.
Which would you prefer?
🌐 Web query:
Convex testing best practices branded ID type
💡 Result:
Here are concise best practices for testing Convex projects that use branded/Id types (TypeScript):
-
Use Convex’s generated Id types everywhere in code and tests to get compile-time distinction between table IDs. Create test IDs with the generated constructor API (new Id("table", idString)) or use ctx.db.insert in test fixtures. [1][2]
-
Keep schema and validators explicit: declare Id fields in your schema with s.id("tableName") and use Convex validators (v.string() / branded helpers) to document branded string types. For richer safety, use branded-string helpers (e.g., convex-helpers.brandedString) and Infer for types used in validators. Validate at runtime in tests when needed. [2][3]
-
When writing tests, construct properly-typed IDs (cast only at creation points). Prefer factory/helper functions that return Id<"table"> rather than raw strings; this avoids accidental mixing of different Id brands in assertions. Avoid unions of multiple Id types in code paths you want the tooling/codemods to analyze. [1][4]
-
Keep tooling and test libs up to date: update convex-test and convex-helpers to versions compatible with newer Convex APIs (see required versions in Convex release notes) so tests and codemods correctly understand table-specific Ids. Run npx convex codegen / push during CI to keep generated types current. [4][5]
-
Test patterns:
- Use integration-style tests that insert via ctx.db.insert and then call functions to preserve runtime ID generation semantics.
- For unit tests, use factories that produce valid Id instances (or use new Id(...) where available) and add simple runtime checks (isUUID / validator) when your logic depends on format.
- Assert types by importing generated Doc/Id types in test files to ensure TypeScript catches mismatches at compile time. [1][2][3]
-
CI/maintenance: run Convex codegen in CI, keep TypeScript >= supported version, and run linters/codemods if you refactor ID APIs to avoid silent type regressions. [2][5]
Sources: Convex docs on Document IDs & TypeScript best practices, guidance on branded types/validators, and recent Convex release notes. [1][2][3][4][5]
References
[1] Convex — Document IDs and Id usage.
[2] Convex — TypeScript best practices / Type support.
[3] Stack Convex — Using branded types in validators (convex-helpers).
[4] Convex news — ctx.db API changes and convex-test/convex-helpers version notes.
[5] Convex release notes — Id API & schema s.id(...) requirement.
Avoid double-cast for Convex IDs; use the Id constructor or test helpers instead.
Lines 13–15 use as unknown as to force a branded Id<"prompts">, which violates the no-as cast guideline and defeats strict typing. Convex provides a constructor API (new Id("prompts", value)) or test libraries (convex-test) to create typed IDs without casts. Create a proper factory function that uses one of these approaches instead.
🤖 Prompt for AI Agents
In `@components/studio/features/prompt-library/prompt-detail.test.tsx` around
lines 13 - 15, The test helper createPromptId uses a double-cast to force a
branded Id<"prompts"> which breaks typing rules; replace it with a proper Id
construction (e.g., use new Id("prompts", value) or a helper from convex-test)
so the returned value is a correctly branded Id<"prompts"> without using as
casts; update the createPromptId function to call the Id constructor or convex
test helper and return that typed Id instead of performing an as unknown as
cast.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@components/studio/canvas/image-canvas.tsx`:
- Around line 160-193: Clamp the incoming progress prop to the 0–100 range at
the start of CapillaryProgress (e.g., const safeProgress = Math.max(0,
Math.min(100, progress))) and use safeProgress everywhere instead of progress:
in the motion.div animate width (`animate={{ width: `${safeProgress}%` }}`), in
the meniscus left calculation (`style={{ left: `calc(${Math.max(0, safeProgress
- 3)}%)` }}`), and in the displayed percentage (`{Math.round(safeProgress)}%`)
so negative widths or overshoot are prevented.
- Around line 24-118: The review points out unused animation symbols
SPRING_REFINED, hubVariants, and iconVariants which cause lint/CI failures;
either remove these definitions or wire them into the motion components that
should use them (e.g., apply hubVariants to the central hub motion element,
iconVariants to the icon motion element, and SPRING_REFINED where a spring
transition is intended), ensuring the symbol names (SPRING_REFINED, hubVariants,
iconVariants) appear as the variants/transition props on the appropriate
motion.* components so the definitions are consumed; if you choose removal,
delete the SPRING_REFINED constant and the hubVariants and iconVariants objects.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
components/studio/canvas/image-canvas.tsxcomponents/studio/layout/studio-shell.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- components/studio/layout/studio-shell.tsx
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: Always leverage TypeScript (strict mode); avoid usage ofanywhich defeats the point of using TypeScript.
Let TypeScript inference work; export explicit prop types for shared/public components.
Noanytype in TypeScript. Useunknownand narrow instead.
Avoidascasts in TypeScript; prefersatisfies, generics, and runtime validation.
@ts-expect-errorallowed only with a comment explaining why + link/TODO to remove.
Files:
components/studio/canvas/image-canvas.tsx
**/*.{tsx,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Avoid
useEffectfor app data fetching. Use it for browser-only side effects (subscriptions, observers, localStorage).
Files:
components/studio/canvas/image-canvas.tsx
🧠 Learnings (1)
📚 Learning: 2026-01-07T07:55:42.930Z
Learnt from: CR
Repo: Simplereally/bloomstudio PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:55:42.930Z
Learning: Applies to app/**/*.{tsx,jsx} : Use `next/image` for images in Next.js.
Applied to files:
components/studio/canvas/image-canvas.tsx
🪛 GitHub Actions: Test
components/studio/canvas/image-canvas.tsx
[warning] 29-29: 'SPRING_REFINED' is assigned a value but never used @typescript-eslint/no-unused-vars
[warning] 57-57: 'hubVariants' is assigned a value but never used @typescript-eslint/no-unused-vars
[warning] 71-71: 'iconVariants' is assigned a value but never used @typescript-eslint/no-unused-vars
🔇 Additional comments (7)
components/studio/canvas/image-canvas.tsx (7)
3-12: Nice expanded header docs.
Clear animation intent and state framing.
19-22: No review notes for these imports.
122-129: Props extension looks good.
131-158: AnimatedText looks solid.
220-235: CanvasWave integration looks clean.
241-334: Placeholder/generating layout updates look good.
336-391: Image display transition tweaks are solid.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| // Refined capillary progress bar - liquid-like precision | ||
| function CapillaryProgress({ progress }: { progress: number }) { | ||
| return ( | ||
| <div className="w-56 space-y-3"> | ||
| {/* Progress track */} | ||
| <div className="relative h-0.5 w-full bg-foreground/5 rounded-full overflow-hidden"> | ||
| {/* Fill */} | ||
| <motion.div | ||
| className="absolute inset-y-0 left-0 bg-gradient-to-r from-primary/80 to-primary rounded-full" | ||
| initial={{ width: 0 }} | ||
| animate={{ width: `${progress}%` }} | ||
| transition={{ duration: 0.5, ease: EXPO_OUT }} | ||
| /> | ||
| {/* Meniscus highlight at leading edge */} | ||
| <motion.div | ||
| className="absolute inset-y-0 w-6 bg-gradient-to-r from-transparent via-white/40 to-transparent rounded-full" | ||
| style={{ left: `calc(${Math.max(0, progress - 3)}%)` }} | ||
| animate={{ opacity: [0.2, 0.5, 0.2] }} | ||
| transition={{ duration: 1.2, repeat: Infinity }} | ||
| /> | ||
| </div> | ||
|
|
||
| {/* Percentage */} | ||
| <div className="flex justify-between items-center"> | ||
| <span className="text-[10px] font-mono text-muted-foreground/70 uppercase tracking-[0.15em]"> | ||
| Progress | ||
| </span> | ||
| <span className="text-[10px] font-mono text-primary/90 tabular-nums font-medium"> | ||
| {Math.round(progress)}% | ||
| </span> | ||
| </div> | ||
| </div> | ||
| ) | ||
| } |
There was a problem hiding this comment.
Clamp progress to a safe [0–100] range before using it in styles.
Prevents negative widths or overshoot if upstream sends out-of-range values.
✅ Suggested fix
function CapillaryProgress({ progress }: { progress: number }) {
+ const clampedProgress = Math.min(100, Math.max(0, progress))
return (
<div className="w-56 space-y-3">
{/* Progress track */}
<div className="relative h-0.5 w-full bg-foreground/5 rounded-full overflow-hidden">
{/* Fill */}
<motion.div
className="absolute inset-y-0 left-0 bg-gradient-to-r from-primary/80 to-primary rounded-full"
initial={{ width: 0 }}
- animate={{ width: `${progress}%` }}
+ animate={{ width: `${clampedProgress}%` }}
transition={{ duration: 0.5, ease: EXPO_OUT }}
/>
{/* Meniscus highlight at leading edge */}
<motion.div
className="absolute inset-y-0 w-6 bg-gradient-to-r from-transparent via-white/40 to-transparent rounded-full"
- style={{ left: `calc(${Math.max(0, progress - 3)}%)` }}
+ style={{ left: `calc(${Math.max(0, clampedProgress - 3)}%)` }}
animate={{ opacity: [0.2, 0.5, 0.2] }}
transition={{ duration: 1.2, repeat: Infinity }}
/>
</div>
{/* Percentage */}
<div className="flex justify-between items-center">
<span className="text-[10px] font-mono text-muted-foreground/70 uppercase tracking-[0.15em]">
Progress
</span>
<span className="text-[10px] font-mono text-primary/90 tabular-nums font-medium">
- {Math.round(progress)}%
+ {Math.round(clampedProgress)}%
</span>
</div>
</div>
)
}🤖 Prompt for AI Agents
In `@components/studio/canvas/image-canvas.tsx` around lines 160 - 193, Clamp the
incoming progress prop to the 0–100 range at the start of CapillaryProgress
(e.g., const safeProgress = Math.max(0, Math.min(100, progress))) and use
safeProgress everywhere instead of progress: in the motion.div animate width
(`animate={{ width: `${safeProgress}%` }}`), in the meniscus left calculation
(`style={{ left: `calc(${Math.max(0, safeProgress - 3)}%)` }}`), and in the
displayed percentage (`{Math.round(safeProgress)}%`) so negative widths or
overshoot are prevented.
* Initial plan * Initial plan for fixing ESLint errors Co-authored-by: Simplereally <120893410+Simplereally@users.noreply.github.com> * Fix ESLint errors and warnings Co-authored-by: Simplereally <120893410+Simplereally@users.noreply.github.com> * Remove package-lock.json and add to .gitignore (use bun) Co-authored-by: Simplereally <120893410+Simplereally@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Simplereally <120893410+Simplereally@users.noreply.github.com>
Summary by CodeRabbit
New Features
Improvements
Documentation
✏️ Tip: You can customize this high-level summary in your review settings.