Skip to content

feat: public feed enhancement, responsive styling optimizations#13

Merged
Simplereally merged 17 commits intomainfrom
feat/introduce-truly-public-feed
Jan 16, 2026
Merged

feat: public feed enhancement, responsive styling optimizations#13
Simplereally merged 17 commits intomainfrom
feat/introduce-truly-public-feed

Conversation

@Simplereally
Copy link
Copy Markdown
Owner

@Simplereally Simplereally commented Jan 15, 2026

Summary by CodeRabbit

  • New Features

    • Public feed for guests with Community showcase, Living Strip marquee, and a floating feed CTA; auth-aware tabs (Following hidden for guests).
    • Model Value Showcase on Pricing and batch-generation controls in studio.
    • Pollen balance display added to the header.
  • Improvements

    • Sensitive-content controls (block/blur/allow), click-to-reveal overlay, and progressive thumbnail→full‑res crossfade.
    • Video preview support, improved image loading priority/quality, feed analytics, and new animations (marquee, sheen, collapsibles).
  • Documentation

    • Expanded skills, model specs, and server-side caching guides.

✏️ Tip: You can customize this high-level summary in your review settings.

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Jan 15, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
bloomstudio Ready Ready Preview, Comment Jan 16, 2026 8:20am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 15, 2026

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Summary
Agent Skills Docs
.agent/skills/*/SKILL.md
Dozens of new SKILL.md docs added (authoring, Next.js, Convex, testing, UI, image/video generation). Documentation-only additions.
Public Feed & Client
app/feed/[type]/page.tsx, components/gallery/feed-client.tsx, components/gallery/feed-tabs.tsx, components/gallery/feed-cta.tsx, components/gallery/*, app/sitemap.ts
Server-rendered metadata, JSON-LD, sitemap entry for /feed/public; FeedClient/FeedTabs become auth-aware and accept server-cached initialPage + initialPreference; FeedCta added; client loadMore uses server actions.
Server Actions & Caching
app/_server/actions/*.ts, app/_server/cache/*.ts, app/_server/cache/config.ts
New server actions for feed/history/favorites and unstable_cache wrappers for paginated pages; centralized TTLs, tags, PAGE_SIZES and cache tag helpers.
Convex Schema & Backend
convex/schema.ts, convex/generatedImages.ts, convex/users.ts, convex/*
Schema fields for sensitivity (isSensitive, sensitiveConfidence, contentAnalysis, promptInference), users.contentFilterPreference; PublicFeedImage mapping; internal queries/mutations for analysis workflow; getPublicFeed accepts filterPreference.
Vision Providers & Moderation
convex/lib/groq.ts, convex/lib/openrouter.ts, convex/lib/visionAnalysis.ts, convex/lib/nsfwDetection.ts, providerHealth*
Groq + OpenRouter integration with fallbacks; OpenRouter client, Groq client, sensitivity scoring, provider health parsing, rate-limit handling, and tests.
Prompt Inference & Scheduling
convex/lib/promptInference.ts, convex/promptInference.ts, convex/contentAnalysis.ts, convex/crons.ts
Cerebras-based prompt inference, analyzePromptImage internal action, contentAnalysis actions and cron to process unanalyzed images and schedule vision analysis when needed.
Media & Previews
convex/lib/videoPreview.ts, hooks/use-media-player.ts, components/ui/media-player.tsx
FFmpeg-based preview generator, new useMediaPlayer hook to centralize media lifecycle and robust play/pause handling; media player refactor to use hook and Next/Image.
Image Loading & Sensitivity UI
components/ui/image-card.tsx, components/images/image-lightbox.tsx, hooks/use-image-lightbox.ts, components/gallery/paginated-image-grid.tsx, components/ui/sensitive-content-overlay.tsx
Progressive thumbnail→full-res crossfade, priority loading prop, SensitiveContentOverlay component, auth-gated actions (save/copy/favorite) and propagation of userShowsSensitive through grids/cards.
Analytics
lib/analytics.ts
New feed analytics types and tracking helpers (feed views, CTA views/clicks/dismiss, prompt copies, video play, image click/lightbox open).
Studio / Gallery Enhancements
components/studio/*, components/studio/batch/*, components/studio/layout/*
BatchConfigButton, hybrid Convex + server-cached gallery initialPage support, studio layout callbacks (onSidebarOpenChange/onGalleryOpenChange), gallery initialPage wiring, generation path changes (video params, batch).
Invalidation & Convex Server Auth
app/_server/actions/invalidation.ts, app/_server/convex/client.ts
Centralized invalidation helpers (history/favorites/feed/following), server-side Convex auth helpers (getConvexClerkToken, getCurrentUserId, requireUserId).
Hooks & Utilities
hooks/use-mobile.ts, hooks/use-aspect-ratio-dimensions.ts, hooks/use-prompt-input.ts, hooks/use-media-player.ts
useSyncExternalStore for mobile detection, fixed/multi-tier aspect-ratio logic (Nano Banana), negative-prompt subscriptions, new useMediaPlayer hook and tests.
Model Registry & Specs
lib/config/models.ts, lib/config/models.test.ts, nano-banana-pro-spec.md, nanobana-specs.md
Added supportsReferenceImage, Nano Banana / Nano Banana Pro presets/tiers, tests and spec docs; model metadata updated.
Landing / Marketing UI
components/landing/*, app/page.tsx, app/layout.tsx, components/pricing/*, app/pricing/*
New CommunitySection and LivingStrip, hero redesign, ModelValueShowcase, multiple copy/styling updates, minor removals (LivingStrip commented out).
Routing & Deployment
proxy.ts, vercel.json, app/sitemap.ts
Added /feed/following to protected routes; cache-control headers for static/assets; sitemap includes feed pages.
Tests & Test Mocks
many *.test.tsx / *.test.ts
Large set of new/updated tests: feed, CTA, media hooks, vision clients, prompt inference, server actions, provider health, UI components, and strengthened mock typings.
Docs / Planning / BYOP cleanup
todo/*, CEREBRAS_INTEGRATION_SPIKE.md
Many design/spec docs added; several BYOP planning docs removed.
Packages
package.json
Added dependencies: @ai-sdk/cerebras, @ai-sdk/groq.

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
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

🐇 I hopped through feeds at dawn's light,

thumbnails bloomed then high-res took flight,
prompts were parsed with careful sight,
caches hummed and analytics chimed bright,
"Explore the community!" — a rabbit's delight.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.95% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: public feed enhancements and responsive styling improvements, which are central themes throughout the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings


📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d554c01 and 5e9cc5b.

📒 Files selected for processing (10)
  • .agent/rules/coding-standards.md
  • .gitignore
  • components/images/image-lightbox.test.tsx
  • components/studio/canvas/canvas-wave.tsx
  • components/studio/canvas/image-canvas.tsx
  • components/studio/features/canvas/canvas-feature.test.tsx
  • components/studio/features/prompt-library/prompt-detail.test.tsx
  • components/studio/layout/studio-shell.tsx
  • components/ui/scroll-area.tsx
  • components/ui/sensitive-content-overlay.test.tsx

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: Avoid any type - use proper union type instead.

The params variable is typed as any, 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 satisfies or creating a shared base type. As per coding guidelines, avoid as casts and prefer satisfies, generics, or runtime validation.

components/ui/button.tsx (1)

50-50: Potential issue: Decorative overlays may break asChild functionality.

When asChild=true, Radix's Slot component expects exactly one child element to merge props onto. The overlay spans (Lines 61-73) add extra children, which could cause Slot to 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:

  1. Removing them entirely if no external consumers depend on them
  2. Adding a @deprecated JSDoc tag to signal intent to remove

This 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.current retains stale IDs. If steps changes 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/static caching 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 text or plaintext would 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.ts but the listByStatus and listMine queries (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: The typeof window check in getSnapshot is unnecessary.

useSyncExternalStore only calls getSnapshot on the client; it uses getServerSnapshot during 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 redundant resize listener.

MediaQueryList already fires when the viewport crosses the breakpoint threshold. The resize listener 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 using useSyncExternalStore for reduced motion detection.

This PR introduces useSyncExternalStore in hooks/use-mobile.ts for 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 useEffect approach is valid for browser-only side effects, but extracting this into a reusable usePrefersReducedMotion hook using useSyncExternalStore would align with the pattern established in this PR.

components/pricing/model-value-showcase.tsx (1)

353-365: Use Next.js Link for internal navigation.

The anchor tags for #comparison-table and /#models should use Next.js Link component 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 unused Sparkles import.

The Sparkles icon 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.

fullResUrl is already computed as displayImage?.originalUrl || displayImage?.url on line 94, so the fallback || displayImage.url on 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 + B to toggle, but standard modal/drawer UX also expects plain Escape to 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 .mp4 extension 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 mimeType or extension parameter:

 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 60 flag 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. Since veryfast preset 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 using onLoadedData instead of onCanPlay for more reliable loading detection.

onCanPlay fires when enough data is available to start playback, but the video may still show a black frame briefly. onLoadedData ensures 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: CommunitySection is imported but commented out in JSX.

The CommunitySection import 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: Move afterEach inside the describe block for consistency.

The afterEach hook is defined at the module level while beforeEach with vi.useFakeTimers() is inside the describe block. For better scoping and consistency, move afterEach inside the describe block alongside beforeEach.

♻️ 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 from json() instead of wrapping it in Promise.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 isSignedIn is undefined (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 isLoaded from useAuth() 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: Avoid as cast; the type already satisfies EnrichedImage.

The mapped object structurally matches EnrichedImage, so the cast is unnecessary. Per coding guidelines, prefer type-safe alternatives over as casts.

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 for generationParams instead of v.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.Mock cast and implicit any in the mock return value work, but for consistency with coding guidelines preferring no any, 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 uses any type.

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, and activeSection varying. 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

📥 Commits

Reviewing files that changed from the base of the PR and between e4ea943 and ce2f4ea.

⛔ Files ignored due to path filters (9)
  • convex/_generated/api.d.ts is excluded by !**/_generated/**
  • public/community/abstract.png is excluded by !**/*.png
  • public/community/cat.png is excluded by !**/*.png
  • public/community/cyberpunk.png is excluded by !**/*.png
  • public/community/fantasy.png is excluded by !**/*.png
  • public/community/fashion.png is excluded by !**/*.png
  • public/community/interior.png is excluded by !**/*.png
  • public/community/portrait.png is excluded by !**/*.png
  • public/community/product.png is 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.md
  • DIMENSION_CONSTRAINTS_RESEARCH.md
  • app/feed/[type]/page.tsx
  • app/globals.css
  • app/page.tsx
  • app/pricing/page.tsx
  • app/pricing/pricing-data.tsx
  • app/sitemap.ts
  • components/gallery/feed-client.tsx
  • components/gallery/feed-cta.test.tsx
  • components/gallery/feed-cta.tsx
  • components/gallery/feed-tabs.test.tsx
  • components/gallery/feed-tabs.tsx
  • components/gallery/paginated-image-grid.tsx
  • components/gl/gl.tsx
  • components/images/image-lightbox.test.tsx
  • components/images/image-lightbox.tsx
  • components/landing/community-section.tsx
  • components/landing/competitor-comparison.tsx
  • components/landing/cta-section.tsx
  • components/landing/feature-card.tsx
  • components/landing/features-section.tsx
  • components/landing/hero-section.tsx
  • components/landing/landing-header.tsx
  • components/landing/living-strip.tsx
  • components/landing/model-badge.tsx
  • components/landing/models-section.tsx
  • components/landing/showcase-section.tsx
  • components/landing/value-prop-section.tsx
  • components/layout/footer.tsx
  • components/layout/header.tsx
  • components/pricing/model-value-showcase.tsx
  • components/solutions/solution-steps.tsx
  • components/studio/controls/model-selector.tsx
  • components/studio/layout/studio-header.tsx
  • components/studio/layout/studio-layout.tsx
  • components/studio/layout/studio-shell.tsx
  • components/ui/button.tsx
  • components/ui/image-card.tsx
  • components/ui/sheet.tsx
  • components/ui/sidebar.tsx
  • convex/generatedImages.ts
  • convex/lib/videoPreview.ts
  • hooks/use-image-lightbox.ts
  • hooks/use-mobile.ts
  • lib/analytics.ts
  • lib/config/models.ts
  • lib/seo-config.ts
  • proxy.ts
  • todo/byop-issue-analysis.md
  • todo/byop-pollinations-auth-refactor.md
  • todo/byop-story-1-core-infrastructure.md
  • todo/byop-story-2-ui-integration.md
  • todo/byop-story-3-generation-refactor.md
  • todo/byop-story-4-cleanup-security.md
  • todo/byop-story-5-testing-polish.md
  • todo/living-strip-story.md
  • todo/product-hunt-launch-plan.md
  • todo/public-feed-phase2-story1-landing-integration.md
  • todo/public-feed-phase2-story2-image-quality.md
  • todo/public-feed-unauthenticated-access.md
  • vercel.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 of any which defeats the point of using TypeScript.
Let TypeScript inference work; export explicit prop types for shared/public components.
No any type in TypeScript. Use unknown and narrow instead.
Avoid as casts in TypeScript; prefer satisfies, generics, and runtime validation.
@ts-expect-error allowed only with a comment explaining why + link/TODO to remove.

Files:

  • components/landing/competitor-comparison.tsx
  • components/landing/models-section.tsx
  • components/studio/layout/studio-header.tsx
  • components/images/image-lightbox.test.tsx
  • components/landing/community-section.tsx
  • components/gl/gl.tsx
  • components/landing/feature-card.tsx
  • components/gallery/feed-cta.tsx
  • components/landing/model-badge.tsx
  • components/gallery/feed-client.tsx
  • components/gallery/paginated-image-grid.tsx
  • components/landing/living-strip.tsx
  • lib/config/models.ts
  • hooks/use-image-lightbox.ts
  • components/gallery/feed-tabs.test.tsx
  • app/pricing/pricing-data.tsx
  • components/pricing/model-value-showcase.tsx
  • components/studio/layout/studio-layout.tsx
  • components/ui/sheet.tsx
  • components/gallery/feed-cta.test.tsx
  • components/ui/button.tsx
  • proxy.ts
  • components/landing/value-prop-section.tsx
  • components/layout/header.tsx
  • components/landing/hero-section.tsx
  • components/studio/controls/model-selector.tsx
  • components/landing/landing-header.tsx
  • components/layout/footer.tsx
  • lib/seo-config.ts
  • components/images/image-lightbox.tsx
  • components/landing/cta-section.tsx
  • app/pricing/page.tsx
  • app/feed/[type]/page.tsx
  • components/gallery/feed-tabs.tsx
  • components/landing/features-section.tsx
  • components/landing/showcase-section.tsx
  • components/studio/layout/studio-shell.tsx
  • convex/lib/videoPreview.ts
  • hooks/use-mobile.ts
  • convex/generatedImages.ts
  • app/sitemap.ts
  • app/page.tsx
  • components/solutions/solution-steps.tsx
  • components/ui/sidebar.tsx
  • components/ui/image-card.tsx
  • lib/analytics.ts
**/*.{tsx,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Avoid useEffect for app data fetching. Use it for browser-only side effects (subscriptions, observers, localStorage).

Files:

  • components/landing/competitor-comparison.tsx
  • components/landing/models-section.tsx
  • components/studio/layout/studio-header.tsx
  • components/images/image-lightbox.test.tsx
  • components/landing/community-section.tsx
  • components/gl/gl.tsx
  • components/landing/feature-card.tsx
  • components/gallery/feed-cta.tsx
  • components/landing/model-badge.tsx
  • components/gallery/feed-client.tsx
  • components/gallery/paginated-image-grid.tsx
  • components/landing/living-strip.tsx
  • components/gallery/feed-tabs.test.tsx
  • app/pricing/pricing-data.tsx
  • components/pricing/model-value-showcase.tsx
  • components/studio/layout/studio-layout.tsx
  • components/ui/sheet.tsx
  • components/gallery/feed-cta.test.tsx
  • components/ui/button.tsx
  • components/landing/value-prop-section.tsx
  • components/layout/header.tsx
  • components/landing/hero-section.tsx
  • components/studio/controls/model-selector.tsx
  • components/landing/landing-header.tsx
  • components/layout/footer.tsx
  • components/images/image-lightbox.tsx
  • components/landing/cta-section.tsx
  • app/pricing/page.tsx
  • app/feed/[type]/page.tsx
  • components/gallery/feed-tabs.tsx
  • components/landing/features-section.tsx
  • components/landing/showcase-section.tsx
  • components/studio/layout/studio-shell.tsx
  • app/page.tsx
  • components/solutions/solution-steps.tsx
  • components/ui/sidebar.tsx
  • components/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.
Use useQuery / useMutation in Client Components for client reactivity with Convex.
In Server Components, use preloadQuery(...) and pass the payload to a Client Component using usePreloadedQuery(...) for SSR + reactivity.
Use fetchQuery(...) 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 + useActionState for form submission state when using React 19 form actions.
Use useOptimistic in React 19 for instant UI feedback when appropriate.
Use useFormStatus inside design-system components that need form pending state in React 19.
Use use to 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.
Use next/image for images in Next.js.
Use shadcn/ui as the base component library; extend via composition + variants.

Files:

  • app/pricing/pricing-data.tsx
  • app/pricing/page.tsx
  • app/feed/[type]/page.tsx
  • app/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.tsx
  • app/pricing/page.tsx
  • app/feed/[type]/page.tsx
  • app/globals.css
  • app/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.md
  • components/landing/landing-header.tsx
  • todo/public-feed-unauthenticated-access.md
  • .agent/skills/testing-code/SKILL.md
  • hooks/use-mobile.ts
  • components/solutions/solution-steps.tsx
  • 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 `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.md
  • todo/public-feed-unauthenticated-access.md
  • app/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.md
  • components/ui/button.tsx
  • components/landing/landing-header.tsx
  • components/layout/footer.tsx
  • components/landing/showcase-section.tsx
  • app/page.tsx
  • components/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.md
  • components/landing/landing-header.tsx
  • hooks/use-mobile.ts
  • app/page.tsx
  • components/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.md
  • app/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.md
  • components/images/image-lightbox.tsx
  • hooks/use-mobile.ts
  • app/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.md
  • app/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.md
  • hooks/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.md
  • components/landing/landing-header.tsx
  • app/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.md
  • app/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.tsx
  • components/images/image-lightbox.tsx
  • components/ui/image-card.tsx
  • todo/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.tsx
  • components/images/image-lightbox.tsx
  • hooks/use-mobile.ts
  • components/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.tsx
  • components/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.ts
  • convex/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.ts
  • 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 **/*.{tsx,jsx} : Avoid `useEffect` for app data fetching. Use it for browser-only side effects (subscriptions, observers, localStorage).

Applied to files:

  • hooks/use-mobile.ts
  • components/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.

Comment thread .agent/skills/adding-route/SKILL.md
Comment thread .agent/skills/brainstorming-ui-and-design/SKILL.md
Comment thread .agent/skills/developing-nextjs/SKILL.md Outdated
Comment thread components/gallery/feed-cta.test.tsx Outdated
Comment thread components/gallery/feed-cta.tsx
Comment thread convex/lib/videoPreview.ts
Comment thread lib/config/models.ts Outdated
Comment thread lib/seo-config.ts Outdated
Comment thread lib/seo-config.ts Outdated
Comment thread todo/product-hunt-launch-plan.md Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: Fix aspectRatioOptions tier 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, but aspectRatioOptions returns dimensions from availableRatios without applying the same scaling. This creates an inconsistency where the two functions return different dimensions for the same ratio when tier is "2k" or "4k".

Currently, aspectRatioOptions is exported but unused in the codebase (the selector uses getDimensionsForRatio directly). To prevent future bugs if this is used:

  • Option 1: Remove the unused export
  • Option 2: Apply tier scaling in aspectRatioOptions to match getDimensionsForRatio behavior
  • Option 3: Add a comment clarifying that aspectRatioOptions intentionally 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_CONFIG is imported but never referenced directly in the test code. The mock at lines 31-38 defines its own STRIPE_CONFIG object, 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/10 styling 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 for Icon.

Since getIconForTag always returns an icon (never null), 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 unused isVideo parameter.

The isVideo parameter 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>) => void
convex/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), ffmpegPromise will 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), compressionRatio will be negative. This isn't a bug, but consumers should be aware of this possibility.

Consider documenting this in the VideoPreviewResult interface 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 accent prop supports "ember" | "neutral" | "muted", but all three TierSection usages pass "ember". If the other variants are for future use, this is fine. Otherwise, simplify by removing unused variants.


417-431: Consider using next/link for internal navigation.

The /#models link navigates to another page. Using Link from next/link enables 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: Consider decoding="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 onLoad callback 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 isVideoContent helper 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, as casts should be avoided, but this pattern is common with Convex when the data model uses string for IDs that need to be narrowed. Consider typing ImageCardData._id as Id<"generatedImages"> if feasible to eliminate these casts.


223-224: Redundant loading attribute when priority is true.

When priority={true}, Next.js Image automatically handles eager loading and adds preload hints. The explicit loading="eager" is redundant in this case.

Suggested simplification
-                    loading={priority ? "eager" : "lazy"}
-                    priority={priority}
+                    priority={priority}
+                    {...(!priority && { loading: "lazy" })}

Or simply remove the loading prop entirely since Next.js defaults to lazy when priority is 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

📥 Commits

Reviewing files that changed from the base of the PR and between ce2f4ea and a5ec894.

📒 Files selected for processing (35)
  • .agent/skills/adding-route/SKILL.md
  • .agent/skills/developing-nextjs/SKILL.md
  • README.md
  • app/layout.tsx
  • app/pricing/pricing-data.tsx
  • components/gallery/feed-cta.test.tsx
  • components/gallery/feed-cta.tsx
  • components/gallery/image-history.tsx
  • components/images/image-lightbox.tsx
  • components/landing/competitor-comparison.tsx
  • components/landing/faq-section.tsx
  • components/landing/features-section.tsx
  • components/landing/models-section.tsx
  • components/landing/value-prop-section.tsx
  • components/pricing/model-value-showcase.tsx
  • components/studio/controls/aspect-ratio-selector.tsx
  • components/studio/controls/model-selector.tsx
  • components/studio/features/generation/controls-view.tsx
  • components/studio/upgrade-modal.test.tsx
  • components/studio/upgrade-modal.tsx
  • components/ui/image-card.test.tsx
  • components/ui/image-card.tsx
  • components/ui/media-player.test.tsx
  • components/ui/media-player.tsx
  • convex/lib/pollinations.test.ts
  • convex/lib/videoPreview.ts
  • hooks/use-aspect-ratio-dimensions.test.ts
  • hooks/use-aspect-ratio-dimensions.ts
  • hooks/use-media-player.test.ts
  • hooks/use-media-player.ts
  • lib/config/models.test.ts
  • lib/config/models.ts
  • lib/seo-config.ts
  • nano-banana-pro-spec.md
  • nanobana-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 of any which defeats the point of using TypeScript.
Let TypeScript inference work; export explicit prop types for shared/public components.
No any type in TypeScript. Use unknown and narrow instead.
Avoid as casts in TypeScript; prefer satisfies, generics, and runtime validation.
@ts-expect-error allowed only with a comment explaining why + link/TODO to remove.

Files:

  • components/gallery/image-history.tsx
  • components/studio/features/generation/controls-view.tsx
  • app/layout.tsx
  • components/ui/media-player.tsx
  • components/ui/media-player.test.tsx
  • components/gallery/feed-cta.test.tsx
  • components/studio/upgrade-modal.test.tsx
  • hooks/use-aspect-ratio-dimensions.test.ts
  • components/images/image-lightbox.tsx
  • components/ui/image-card.tsx
  • components/landing/models-section.tsx
  • convex/lib/pollinations.test.ts
  • hooks/use-media-player.ts
  • components/gallery/feed-cta.tsx
  • app/pricing/pricing-data.tsx
  • lib/config/models.test.ts
  • components/studio/upgrade-modal.tsx
  • components/pricing/model-value-showcase.tsx
  • components/landing/faq-section.tsx
  • components/landing/value-prop-section.tsx
  • components/studio/controls/aspect-ratio-selector.tsx
  • components/ui/image-card.test.tsx
  • lib/config/models.ts
  • hooks/use-aspect-ratio-dimensions.ts
  • hooks/use-media-player.test.ts
  • convex/lib/videoPreview.ts
**/*.{tsx,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Avoid useEffect for app data fetching. Use it for browser-only side effects (subscriptions, observers, localStorage).

Files:

  • components/gallery/image-history.tsx
  • components/studio/features/generation/controls-view.tsx
  • app/layout.tsx
  • components/ui/media-player.tsx
  • components/ui/media-player.test.tsx
  • components/gallery/feed-cta.test.tsx
  • components/studio/upgrade-modal.test.tsx
  • components/images/image-lightbox.tsx
  • components/ui/image-card.tsx
  • components/landing/models-section.tsx
  • components/gallery/feed-cta.tsx
  • app/pricing/pricing-data.tsx
  • components/studio/upgrade-modal.tsx
  • components/pricing/model-value-showcase.tsx
  • components/landing/faq-section.tsx
  • components/landing/value-prop-section.tsx
  • components/studio/controls/aspect-ratio-selector.tsx
  • components/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.
Use useQuery / useMutation in Client Components for client reactivity with Convex.
In Server Components, use preloadQuery(...) and pass the payload to a Client Component using usePreloadedQuery(...) for SSR + reactivity.
Use fetchQuery(...) 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 + useActionState for form submission state when using React 19 form actions.
Use useOptimistic in React 19 for instant UI feedback when appropriate.
Use useFormStatus inside design-system components that need form pending state in React 19.
Use use to 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.
Use next/image for images in Next.js.
Use shadcn/ui as the base component library; extend via composition + variants.

Files:

  • app/layout.tsx
  • app/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.tsx
  • app/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.tsx
  • 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 `useOptimistic` in React 19 for instant UI feedback when appropriate.

Applied to files:

  • components/images/image-lightbox.tsx
  • components/ui/image-card.tsx
  • 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 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.ts
  • convex/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.ts
  • convex/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 $1344$ becomes $1376$ is because $1376$ is the Absolute Max Buffer f...

(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 index as the key is acceptable here since the faqs array 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 beforeEach to ensure test isolation. This prevents state leakage between tests when isStripeConfigured is set to false in 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 findByText instead of getByText for 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:

  1. The checkout action is not called when Stripe is unconfigured
  2. Appropriate error logging and user feedback occur
components/landing/models-section.tsx (4)

11-11: LGTM!

The border color change to border-white/10 aligns with the design system used throughout this component (matching lines 64, 72, 93, 112).


19-21: LGTM!

Good use of min-w-0 combined with truncate and block to ensure proper text truncation within the flex container. The structural simplification is clean.


28-29: LGTM!

Good use of localeCompare for locale-aware sorting. Since MODEL_REGISTRY is static configuration data, the filter/sort operations on each render are acceptable. If the registry grows significantly, consider memoizing with useMemo.


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 LucideIcon type 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.tsx and error.tsx for 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 waitFor import 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/await with waitFor for assertions
  • Properly mocking play() to return a resolved Promise
  • Simulating state transitions via dataset.paused

This aligns well with the useMediaPlayer hook'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 useEffect for synchronizing with an external value (the URL prop), not data fetching.


119-162: Excellent race-condition-safe play/pause implementation.

The handleVideoClick handler properly:

  1. Tracks pending play() Promises via ref
  2. Awaits pending play before calling pause()
  3. Gracefully handles AbortError from rapid interactions
  4. Logs non-abort errors for debugging
  5. 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 handleVideoClick for the click handler to leverage the race-condition-safe play/pause logic from the hook.


141-163: LGTM!

Image rendering appropriately uses the direct onClick prop 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 createMockVideo helper cleanly encapsulates mock video setup with configurable paused state.


243-275: Excellent AbortError handling test.

This test correctly verifies that AbortError from interrupted play() 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:

  1. Starting a controlled play Promise
  2. Clicking to pause while play is pending
  3. 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:

  1. Memoized callbacks maintain referential stability across renders
  2. 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 preset property 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 veryfast for 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 finally block 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 * 1024 is 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 rafId tracking and cleanup via cancelAnimationFrame addresses the memory leak concern. The first animate() 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/image with fill prop per guidelines. The empty alt="" 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-110 keeps the hover interaction focused on the shadow/overlay effects without the zoom, which aligns with the broader UI refinement direction in this PR. The transition-transform duration-500 class 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 HTMLVideoElement for type-safe property access
  • Correctly checking video.muted as a boolean property rather than an attribute (since muted is a reflected property in the DOM)
  • Test assertions align with the ImageCard component's video rendering behavior (autoPlay, loop, muted)
components/gallery/feed-cta.tsx (2)

21-54: Well-implemented CTA with proper stale closure fix.

The isVisibleRef pattern correctly prevents the stale closure issue where isVisible state 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: Verify mix-blend-difference accessibility across theme backgrounds.

The mix-blend-difference technique 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() in beforeEach and vi.useRealTimers() in afterEach. This prevents timer state from leaking between tests.

Note: afterEach is 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 isVisibleRef pattern in the component correctly prevents duplicate trackCtaView calls 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 priority for immediate display
  • Applies subtle blur-[2px] while full-res loads
  • Fades out with opacity-0 pointer-events-none absolute inset-0 when 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 stopPropagation on the link to prevent lightbox close

Based 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 → isThumbnailLoaded stays false forever
  • Full-res hasn't loaded yet → isFullResLoaded is false

This is correct behavior, but worth confirming that the spinner correctly disappears once the single full-res image loads (when isFullResLoaded becomes true).

components/ui/image-card.tsx (5)

21-44: LGTM on type definitions.

The ImageCardData and ImageCardProps interfaces are well-typed with appropriate JSDoc comments. The originalUrl optional field and priority prop additions are cleanly documented.

Also applies to: 46-61


99-106: Prompt copy tracking integration is correct.

The trackPromptCopy("feed", !!isSignedIn) call properly converts isSignedIn (which could be undefined initially) to a boolean, matching the expected signature from lib/analytics.ts.


144-154: LGTM on event handlers.

The useCallback wrappers 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, and autoPlay is 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 supportsReferenceImage property follows the established pattern for optional boolean flags in the interface (similar to supportsAudio, supportsInterpolation).


228-263: LGTM!

The NANOBANANA_ASPECT_RATIOS preset is well-documented and correctly excludes 9:21 (unsupported per spec) and custom (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:21 and custom is correct for this fixed-dimension model.


400-422: LGTM!

The nanobanana-pro model configuration correctly implements the spec with:

  • ~17.2MP pixel budget
  • Dimension limits from the 1K/4K tier extremes (672-6336)
  • dimensionsEnabled: false for fixed dimensions
  • Three supported tiers mapping to the spec's 1K/2K/4K system

631-653: LGTM!

The nanobanana model configuration correctly reflects the spec:

  • Fixed dimensions with dimensionsEnabled: false and outputCertainty: "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?.supportsReferenceImage correctly 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: false and outputCertainty: "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 availableRatios are returned without recalculation when dimensionsEnabled === 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.

Comment thread app/layout.tsx
Comment thread components/landing/value-prop-section.tsx Outdated
Comment thread components/studio/controls/aspect-ratio-selector.tsx
Comment thread components/ui/image-card.tsx Outdated
Comment thread convex/lib/pollinations.test.ts Outdated
Comment thread README.md Outdated
Comment thread README.md Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: Avoid any type; use the existing type union instead.

Using any defeats TypeScript's type safety. The params object 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 | VideoGenerationParams

As per coding guidelines, any should be avoided in TypeScript; use unknown and narrow, or proper type unions.

components/studio/controls/collapsible-section.test.tsx (1)

4-4: Missing vi import from vitest.

The test uses vi.fn() on line 212, but vi is 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 | null but function never returns null.

This was previously flagged. The function always returns Frame as a fallback (line 81), making the | null in 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 the asChild pattern 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-lg or shadow-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-2 without 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-effect is for light mode and glass-effect-home is 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 for Icon.

Since getIconForTag always returns an icon (falls back to Frame), 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 1 and 1000 are hardcoded here but already defined as MIN_BATCH_SIZE and MAX_BATCH_SIZE constants in batch-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 = 1000

Then 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 !isExpanded block, so isExpanded is always false here, 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: Eye and EyeOff.

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> with onClick but 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 adding preload attribute 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: Avoid as any cast; use proper type narrowing.

Per coding guidelines, avoid any which 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: Same as any issue 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 any type 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 analyzeRecentImages indefinitely.

Consider either:

  1. Marking failed images with an analysisError field to skip them in future batches
  2. Implementing a retry counter with backoff
convex/lib/openrouter.ts (1)

70-74: Validate API response against expected schema.

JSON.parse returns any and 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: Avoid as cast; the manual mapping already satisfies the type.

The as EnrichedImage[] cast on line 622 violates the coding guideline to avoid as casts. Since you're manually constructing objects with all required EnrichedImage fields, 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 as casts.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a5ec894 and f69fd2d.

⛔ Files ignored due to path filters (1)
  • convex/_generated/api.d.ts is excluded by !**/_generated/**
📒 Files selected for processing (32)
  • app/globals.css
  • components/gallery/paginated-image-grid.tsx
  • components/studio/batch/batch-config-button.tsx
  • components/studio/batch/batch-mode-panel.tsx
  • components/studio/batch/index.ts
  • components/studio/controls/aspect-ratio-selector.tsx
  • components/studio/controls/collapsible-section.test.tsx
  • components/studio/controls/collapsible-section.tsx
  • components/studio/controls/model-selector.tsx
  • components/studio/controls/options-panel.tsx
  • components/studio/controls/prompt-section.tsx
  • components/studio/controls/resolution-tier-selector.tsx
  • components/studio/features/canvas/canvas-feature.tsx
  • components/studio/features/canvas/canvas-view.tsx
  • components/studio/features/generation/controls-view.tsx
  • components/studio/index.ts
  • components/studio/layout/studio-header.tsx
  • components/studio/layout/studio-layout.tsx
  • components/studio/layout/studio-shell.tsx
  • components/ui/image-card.tsx
  • components/ui/sensitive-content-overlay.tsx
  • convex/contentAnalysis.test.ts
  • convex/contentAnalysis.ts
  • convex/crons.ts
  • convex/generatedImages.ts
  • convex/lib/nsfwDetection.test.ts
  • convex/lib/nsfwDetection.ts
  • convex/lib/openrouter.ts
  • convex/schema.ts
  • convex/users.ts
  • hooks/queries/use-image-history.ts
  • todo/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 of any which defeats the point of using TypeScript.
Let TypeScript inference work; export explicit prop types for shared/public components.
No any type in TypeScript. Use unknown and narrow instead.
Avoid as casts in TypeScript; prefer satisfies, generics, and runtime validation.
@ts-expect-error allowed only with a comment explaining why + link/TODO to remove.

Files:

  • components/studio/index.ts
  • components/studio/batch/batch-mode-panel.tsx
  • components/studio/features/canvas/canvas-feature.tsx
  • components/ui/sensitive-content-overlay.tsx
  • components/studio/batch/batch-config-button.tsx
  • hooks/queries/use-image-history.ts
  • convex/lib/nsfwDetection.ts
  • components/studio/controls/options-panel.tsx
  • components/studio/controls/resolution-tier-selector.tsx
  • convex/contentAnalysis.ts
  • components/studio/features/canvas/canvas-view.tsx
  • components/studio/controls/collapsible-section.tsx
  • components/studio/batch/index.ts
  • convex/contentAnalysis.test.ts
  • convex/generatedImages.ts
  • components/studio/controls/collapsible-section.test.tsx
  • components/studio/layout/studio-header.tsx
  • convex/schema.ts
  • components/studio/layout/studio-shell.tsx
  • convex/crons.ts
  • components/ui/image-card.tsx
  • convex/lib/nsfwDetection.test.ts
  • components/studio/controls/prompt-section.tsx
  • components/gallery/paginated-image-grid.tsx
  • convex/lib/openrouter.ts
  • components/studio/controls/aspect-ratio-selector.tsx
  • convex/users.ts
  • components/studio/controls/model-selector.tsx
**/*.{tsx,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Avoid useEffect for app data fetching. Use it for browser-only side effects (subscriptions, observers, localStorage).

Files:

  • components/studio/batch/batch-mode-panel.tsx
  • components/studio/features/canvas/canvas-feature.tsx
  • components/ui/sensitive-content-overlay.tsx
  • components/studio/batch/batch-config-button.tsx
  • components/studio/controls/options-panel.tsx
  • components/studio/controls/resolution-tier-selector.tsx
  • components/studio/features/canvas/canvas-view.tsx
  • components/studio/controls/collapsible-section.tsx
  • components/studio/controls/collapsible-section.test.tsx
  • components/studio/layout/studio-header.tsx
  • components/studio/layout/studio-shell.tsx
  • components/ui/image-card.tsx
  • components/studio/controls/prompt-section.tsx
  • components/gallery/paginated-image-grid.tsx
  • components/studio/controls/aspect-ratio-selector.tsx
  • components/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.tsx
  • components/ui/image-card.tsx
  • components/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.tsx
  • 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 `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.ts
  • convex/contentAnalysis.ts
  • convex/contentAnalysis.test.ts
  • convex/generatedImages.ts
  • convex/schema.ts
  • convex/crons.ts
  • convex/lib/nsfwDetection.test.ts
  • convex/lib/openrouter.ts
  • convex/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.ts
  • convex/contentAnalysis.ts
  • convex/contentAnalysis.test.ts
  • convex/generatedImages.ts
  • convex/schema.ts
  • convex/crons.ts
  • convex/lib/nsfwDetection.test.ts
  • convex/lib/openrouter.ts
  • convex/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.tsx
  • components/studio/layout/studio-header.tsx
  • components/ui/image-card.tsx
  • components/studio/controls/aspect-ratio-selector.tsx
  • components/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.ts
  • components/gallery/paginated-image-grid.tsx
  • convex/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.ts
  • components/gallery/paginated-image-grid.tsx
  • todo/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.

Comment thread components/studio/layout/studio-shell.tsx
Comment thread convex/crons.ts
Comment thread convex/generatedImages.ts
Comment thread convex/lib/nsfwDetection.ts Outdated
Comment thread convex/lib/openrouter.ts
Comment thread hooks/queries/use-image-history.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 disabled prop references hasContent, 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: Avoid any type; use proper typing for generation parameters.

Line 243 uses const params: any, which violates the coding guideline requiring strict TypeScript without any. The type should match the expected parameter type for generate().

🔧 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 | VideoGenerationParams

If the union type doesn't fit cleanly, consider creating a dedicated type or using satisfies with a more specific type. As per coding guidelines, avoid as casts; prefer satisfies, 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 for BatchConfigButton.

The test file needs to be updated to include BatchConfigButton in 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 redundant mockImage redeclaration.

This mockImage constant shadows and duplicates the outer mockImage defined 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 like getByRole('button', { name: /copy/i }) or adding a data-testid to the copy button in the component.

components/images/image-lightbox.tsx (2)

218-241: Redundant fallback in src prop.

Line 220: fullResUrl || displayImage.url is redundant. Since fullResUrl is defined as displayImage?.originalUrl || displayImage?.url (line 94), and this code only executes when displayImage is truthy (line 118 conditional), fullResUrl will 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 || isFullResLoaded ensures the zoom indicator appears once either layer is ready. However, when hasSeparateThumbnail is false, isThumbnailLoaded will never become true (only isFullResLoaded will). 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 the PaginatedGalleryResult type.

This type appears to be duplicated across multiple files (also seen in app/_server/cache/history.ts context). 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: HistoryFilters is 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.ts file 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, since token is already string | 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 sharing PaginatedGalleryResult type.

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, and feed-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 onSuccess and onError side effects. The server-side cache invalidation via invalidateUserFavoritesCache() is correctly placed in onSuccess.

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 updated useToggleFavorite hook.

The useToggleFavorite hook was significantly refactored to use TanStack Query's useMutation with onSuccess and onError callbacks, but no corresponding tests were added. Consider adding tests to verify:

  • The mutation calls the Convex toggle function
  • onSuccess triggers invalidateUserFavoritesCache
  • onError displays a toast notification
todo/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 text or plaintext.

📝 Suggested fix
-```
+```text
 app/
   _server/
     convexAuth.ts          # Clerk -> Convex JWT helper (server-only)
components/gallery/history-client.test.tsx (1)

61-69: Avoid as any casts in test fixtures.

Per coding guidelines, avoid any type 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: Avoid as type assertion.

Per coding guidelines, prefer generics or type narrowing over as casts. Since items comes 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 selectAll requires a specific type signature, consider updating the useImageSelection hook's type definitions instead of casting at the call site.

convex/lib/visionAnalysis.ts (3)

14-14: Remove unused imports.

GroqApiError and GroqTimeoutError are imported but never used in this file. If they're intended for re-export, add an explicit export { ... } 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: Avoid as T casts; use a type-safe approach instead.

The current implementation uses as T casts 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 via process.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 to null on line 145, then line 148 sets it again.

This isn't a bug, but you could simplify by using a finally block 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 null which falls back to server state - this might actually be the intended behavior if you want the server state to take over.


171-176: Consider memoizing showSensitiveOverlay or moving state declaration.

The isRevealed state is declared after its dependent condition showSensitiveOverlay. 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 session
components/ui/image-card.test.tsx (3)

73-73: Avoid as any cast; use a typed mock return instead.

Per coding guidelines, any defeats TypeScript's purpose. Consider creating a minimal typed mock object or using unknown with 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: Same as any issue in the unauthenticated user mock.

Consistent with the earlier comment, avoid as any casts.


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 exporting getDefaultDeps for testing consistency.

The function analyzeImageWithGroqDeps is documented as "@internal Exported for testing" (line 95), but getDefaultDeps is 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: Avoid v.any() for generationParams.

Using v.any() defeats TypeScript's type safety. Define a proper schema for generationParams or use v.record(v.string(), v.any()) as a minimum, though a fully typed schema is preferred.

As per coding guidelines: "No any type in TypeScript. Use unknown and 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: Avoid as 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 as casts in TypeScript; prefer satisfies, 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 conditional sensitiveSource update in updateImageSensitivity.

The mutation always sets sensitiveSource: "vision_analysis" regardless of the isSensitive result. 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 isSensitive is 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 retryCount mechanism 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 retryCount in 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 &apos; or the Unicode character directly.

♻️ Suggested change
           <p className="text-muted-foreground">
-            Images {"you've"} loved from across the community.
+            Images you&apos;ve loved from across the community.
           </p>
hooks/mutations/use-set-visibility.ts (1)

29-39: Verify that awaiting async onSuccess doesn't block mutation resolution.

The onSuccess handler is async and awaits invalidateVisibilityChange(). TanStack Query's useMutation does support async onSuccess, but be aware that:

  1. The mutation will appear "successful" to the caller before the server cache invalidation completes
  2. Any error in invalidateVisibilityChange() will be silently swallowed since there's no error handling here

Consider 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 using vi.fn(), you could simplify by setting the mock implementation in beforeEach:

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 awaiting invalidateQueries for consistency.

The async onSuccess pattern 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 while invalidateImageDeletion() is awaited.

For consistency and to ensure all cache invalidations complete before onSuccess returns, 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's onSuccess.

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 when initialPage changes.

Unlike FeedClient which resets its state when feedType or initialPage changes, this component initializes state only once via lazy initializers. If the parent re-renders with a different initialPage (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: mockCerebrasInstance appears unused.

The mockCerebrasInstance mock is created and configured in beforeEach, 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, generateWithCerebras throws a CerebrasError with code TOKEN_LIMIT_EXCEEDED when 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 useMutation mock relies on string equality (mutationRef === "follow"), which works because the API mock returns "follow" for api.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 a data-testid to 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 feedType or initialPage changes. If initialPage is 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.log statements will execute in production, which could be noisy for a frequently-called suggestion generator. Consider wrapping them in a development check similar to VERBOSE_LOGGING in 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 http or text for 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 isRetryableError function 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 a status property on the error object if available from the AI SDK.


293-307: async keyword is unnecessary.

The function doesn't use await, so the async keyword can be removed. The streamText call 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 using JSON.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.

memoizedLoadMore simply calls handleLoadMore which is already a useCallback. This wrapper can be removed and handleLoadMore used 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}
             // ...
         />
     )

Comment on lines +25 to +28
export async function invalidateUserFavoritesCache() {
const userId = await requireUserId()
revalidateTag(CACHE_TAGS.FAVORITES_USER(userId), "max")
}
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

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.

Suggested change
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.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

Comment on lines +36 to +38
export async function invalidatePublicFeedCache() {
revalidateTag(CACHE_TAGS.FEED_PUBLIC, "max")
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

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.

Suggested change
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).

Comment on lines +46 to +49
export async function invalidateUserFollowingFeedCache() {
const userId = await requireUserId()
revalidateTag(CACHE_TAGS.FEED_FOLLOWING_USER(userId), "max")
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

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.

Suggested change
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.

Comment thread app/_server/cache/favorites.ts
Comment thread app/_server/cache/history.ts Outdated
Comment thread components/studio/gallery/persistent-image-gallery.tsx
Comment thread convex/tempTagStats.ts
Comment thread hooks/mutations/use-delete-image.test.tsx
Comment thread lib/prompt-enhancement/prompt-enhancer.ts Outdated
Comment on lines +6 to 9
*
* Uses unified AI provider with Groq as primary (14,400 RPD free tier)
* and OpenRouter as fallback.
*/
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
*
* 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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 separate negativeHasContent based on subscribeToNegativePrompt and 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 state
app/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> with Button asChild pattern to fix invalid nested interactive elements.

<a><button></a> is invalid HTML and breaks keyboard/focus behavior. The Button component supports the asChild prop 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., when requireUserId() 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 with isSensitive === null through toPublicFeedImages, which coerces null to false. 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 double as casts for Convex IDs.

The pattern value as unknown as Id<"referenceImages"> violates the TypeScript guideline to avoid as casts. 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 single as cast with @ts-expect-error and 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: Remove as unknown as casts 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 using Object.assign to 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 when steps changes.
If steps updates while the component stays mounted, visibleStepIndex can 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.log statement 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, useEffect should 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-pointer vs cursor-not-allowed conflict 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-pointer class uses !important, which will override cursor-not-allowed when 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 parseResult in promptInference.ts which validates the schema, this function directly returns the parsed JSON without validating that it conforms to ContentAnalysisResult. 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 flag dangerouslySetInnerHTML.

Even though the JSON-LD is static, Biome’s lint/security/noDangerouslySetInnerHtml will 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 be undefined even when userId is present. Passing undefined to fetchQuery will 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 === null

Apply the same fix to getFavoritesPageCached() and both functions in history.ts (getMyImagesPageCached() and getMyImagesWithDisplayDataCached()).

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 filterPreference in the loadMore call (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 a useEffect to reset items, cursor, and isDone when preference changes 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.ts

Also applies to: 292-320, 380-390

Comment thread components/gallery/feed-client.tsx Outdated
Comment thread components/gallery/history-client.tsx Outdated
Comment thread components/pollen-balance/pollen-balance-display-view.tsx
Comment thread components/pollen-balance/pollen-balance-display.test.tsx
Comment thread components/ui/sidebar.tsx
Comment thread convex/batchGeneration.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 _id property needed for toggle favorite to work.

The implementation's handleToggleFavorite checks convexImageId = currentImage?._id and returns early if undefined (line 73). Since mockImage is created without _id, the function never calls mutateAsync, but the test expects it to be called.

Either provide _id when creating mockImage:

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: Missing onRefresh/isRefreshing props 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?: () => void and isRefreshing?: boolean to 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 test
components/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">
                     <Image
components/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 new userShowsSensitive prop.

Based on the summary, PaginatedImageGrid now forwards a userShowsSensitive prop to ImageCard. 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: Extract activeClasses to a shared constant.

This exact styling pattern is duplicated across multiple control components: model-selector.tsx, aspect-ratio-selector.tsx, and resolution-tier-selector.tsx. Create a shared constant (e.g., in a constants.ts or styles.ts file 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 as double cast is necessary here because Convex's Id<"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 "avoid as casts" 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 activeCount is 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 a useMemo.

♻️ 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 findByText is 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 consoleSpy created on line 127 (and similarly on line 141) suppresses console.error but is never restored. While vi.clearAllMocks() clears call history, it doesn't restore original implementations. Consider using mockRestore() in cleanup or use vi.restoreAllMocks() in afterEach.

♻️ 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 useSyncExternalStore pattern 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 CollapsibleTrigger only renders when isExpanded is true (line 131), the ternary isExpanded ? "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: Avoid as 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 as casts in TypeScript.

components/studio/features/canvas/canvas-view.test.tsx (1)

9-22: Consider adding test coverage for the progress prop.

The ImageCanvas mock doesn't capture the progress prop, and there's no test verifying that progress is correctly passed through from CanvasView to ImageCanvas. 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 isGeneratedImagesId function duplicates the isConvexId logic already present in hooks/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-label or data-testid in 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 a data-testid or role="button" on the overlay and using getByTestId/getByRole instead 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: Avoid as unknown as by using satisfies for mock objects.
This keeps type safety without weakening checks. As per coding guidelines, avoid as casts.

♻️ Proposed refactor using satisfies
   beforeEach(() => {
     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 setCopied after 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: Avoid as const; use typed literals/satisfies.

This aligns with the TS guideline to avoid casts while preserving literal types.

♻️ 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 }
As per coding guidelines, avoid `as` casts.
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 avoids useEffect for app data fetching; consider moving empty-page skipping into the server cache/page preparation so the client receives a non-empty initialPage, or otherwise gate this behavior outside useEffect.

As per coding guidelines, avoid useEffect for app data fetching.

components/gallery/history-client.test.tsx (1)

29-31: Reduce unsafe as unknown as casts 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 in createGeneratedImageId).

♻️ 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 as casts in TypeScript.

Also applies to: 105-126

components/gallery/history-client.tsx (2)

168-201: Consider moving filter-driven fetch out of useEffect.

Guideline asks to avoid useEffect for data fetching in TSX. A cleaner alternative is a dedicated handleFiltersChange(next) that updates localStorage state and triggers loadMyHistoryWithDisplayPage directly from the filter-change handler (both HistoryFiltersDropdown and ActiveFilterBadges).

As per coding guidelines, avoid useEffect for app data fetching in TSX.


224-227: Drop the as { _id: string } cast in handleSelectAll.

Id<"generatedImages"> is already a string subtype; the cast shouldn’t be needed if selectAll accepts the ID type. If it does not, consider widening selectAll’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 as casts in TypeScript.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a3164e6 and 3121775.

📒 Files selected for processing (52)
  • .agent/rules/coding-standards.md
  • .github/workflows/test.yml
  • app/_server/cache/favorites.ts
  • app/_server/cache/history.ts
  • app/globals.css
  • components/gallery/favorites-client.tsx
  • components/gallery/feed-client.test.tsx
  • components/gallery/feed-client.tsx
  • components/gallery/history-client.test.tsx
  • components/gallery/history-client.tsx
  • components/gallery/paginated-image-grid.test.tsx
  • components/gallery/visibility-toggle.test.tsx
  • components/images/image-lightbox.test.tsx
  • components/layout/header.test.tsx
  • components/layout/header.tsx
  • components/pollen-balance/pollen-balance-display-view.tsx
  • components/pollen-balance/pollen-balance-display.test.tsx
  • components/pricing/model-value-showcase.tsx
  • components/settings/star-repo-card.tsx
  • components/studio/canvas/canvas-wave.tsx
  • components/studio/canvas/image-canvas.test.tsx
  • components/studio/canvas/image-canvas.tsx
  • components/studio/canvas/image-toolbar.tsx
  • components/studio/canvas/luminous-tide-effect.tsx
  • components/studio/controls/aspect-ratio-selector.tsx
  • components/studio/controls/collapsible-section.tsx
  • components/studio/controls/debug.test.tsx
  • components/studio/controls/model-selector.tsx
  • components/studio/controls/reference-image-picker.test.tsx
  • components/studio/controls/reference-images-browser-modal.test.tsx
  • components/studio/controls/video-reference-image-picker.test.tsx
  • components/studio/controls/video-reference-image-picker.tsx
  • components/studio/features/canvas/canvas-feature.test.tsx
  • components/studio/features/canvas/canvas-feature.tsx
  • components/studio/features/canvas/canvas-view.test.tsx
  • components/studio/features/canvas/canvas-view.tsx
  • components/studio/features/generation/controls-view.tsx
  • components/studio/features/prompt-library/prompt-detail.test.tsx
  • components/studio/features/prompt-library/prompt-library.test.tsx
  • components/studio/upgrade-modal.test.tsx
  • components/subscription/subscription-badge.test.tsx
  • components/ui/media-player.test.tsx
  • components/ui/rich-tooltip.tsx
  • components/ui/sensitive-content-overlay.test.tsx
  • convex/tempTagStats.test.ts
  • convex/tempTagStats.ts
  • hooks/mutations/use-delete-image.test.tsx
  • lib/prompt-enhancement/prompt-enhancer.ts
  • lib/prompt-enhancement/suggestion-generator.ts
  • package.json
  • scripts/generate-lint-docs.js
  • todo/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 of any which defeats the point of using TypeScript.
Let TypeScript inference work; export explicit prop types for shared/public components.
No any type in TypeScript. Use unknown and narrow instead.
Avoid as casts in TypeScript; prefer satisfies, generics, and runtime validation.
@ts-expect-error allowed only with a comment explaining why + link/TODO to remove.

Files:

  • components/gallery/paginated-image-grid.test.tsx
  • components/ui/media-player.test.tsx
  • components/studio/controls/debug.test.tsx
  • components/studio/canvas/image-canvas.test.tsx
  • components/subscription/subscription-badge.test.tsx
  • components/studio/controls/collapsible-section.tsx
  • components/studio/features/generation/controls-view.tsx
  • components/studio/features/canvas/canvas-view.test.tsx
  • components/studio/controls/reference-image-picker.test.tsx
  • components/gallery/favorites-client.tsx
  • components/studio/canvas/luminous-tide-effect.tsx
  • components/images/image-lightbox.test.tsx
  • components/gallery/feed-client.tsx
  • components/studio/features/canvas/canvas-feature.test.tsx
  • components/ui/sensitive-content-overlay.test.tsx
  • components/studio/canvas/canvas-wave.tsx
  • components/pricing/model-value-showcase.tsx
  • components/gallery/history-client.tsx
  • components/ui/rich-tooltip.tsx
  • components/studio/features/prompt-library/prompt-library.test.tsx
  • components/studio/features/canvas/canvas-feature.tsx
  • components/studio/features/prompt-library/prompt-detail.test.tsx
  • components/studio/canvas/image-canvas.tsx
  • components/gallery/history-client.test.tsx
  • components/gallery/visibility-toggle.test.tsx
  • components/studio/controls/aspect-ratio-selector.tsx
  • components/studio/controls/reference-images-browser-modal.test.tsx
  • components/studio/upgrade-modal.test.tsx
  • components/studio/features/canvas/canvas-view.tsx
  • components/pollen-balance/pollen-balance-display-view.tsx
  • components/studio/canvas/image-toolbar.tsx
  • components/settings/star-repo-card.tsx
  • components/gallery/feed-client.test.tsx
  • components/layout/header.tsx
**/*.{tsx,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Avoid useEffect for app data fetching. Use it for browser-only side effects (subscriptions, observers, localStorage).

Files:

  • components/gallery/paginated-image-grid.test.tsx
  • components/ui/media-player.test.tsx
  • components/studio/controls/debug.test.tsx
  • components/studio/canvas/image-canvas.test.tsx
  • components/subscription/subscription-badge.test.tsx
  • components/studio/controls/collapsible-section.tsx
  • components/studio/features/generation/controls-view.tsx
  • components/studio/features/canvas/canvas-view.test.tsx
  • components/studio/controls/reference-image-picker.test.tsx
  • components/gallery/favorites-client.tsx
  • components/studio/canvas/luminous-tide-effect.tsx
  • components/images/image-lightbox.test.tsx
  • components/gallery/feed-client.tsx
  • components/studio/features/canvas/canvas-feature.test.tsx
  • components/ui/sensitive-content-overlay.test.tsx
  • components/studio/canvas/canvas-wave.tsx
  • components/pricing/model-value-showcase.tsx
  • components/gallery/history-client.tsx
  • components/ui/rich-tooltip.tsx
  • components/studio/features/prompt-library/prompt-library.test.tsx
  • components/studio/features/canvas/canvas-feature.tsx
  • components/studio/features/prompt-library/prompt-detail.test.tsx
  • components/studio/canvas/image-canvas.tsx
  • components/gallery/history-client.test.tsx
  • components/gallery/visibility-toggle.test.tsx
  • components/studio/controls/aspect-ratio-selector.tsx
  • components/studio/controls/reference-images-browser-modal.test.tsx
  • components/studio/upgrade-modal.test.tsx
  • components/studio/features/canvas/canvas-view.tsx
  • components/pollen-balance/pollen-balance-display-view.tsx
  • components/studio/canvas/image-toolbar.tsx
  • components/settings/star-repo-card.tsx
  • components/gallery/feed-client.test.tsx
  • components/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.tsx
  • components/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.md
  • components/ui/rich-tooltip.tsx
  • components/studio/features/prompt-library/prompt-library.test.tsx
  • components/studio/controls/aspect-ratio-selector.tsx
  • components/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.md
  • components/gallery/favorites-client.tsx
  • components/gallery/feed-client.tsx
  • components/gallery/history-client.tsx
  • components/gallery/history-client.test.tsx
  • components/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.md
  • components/gallery/feed-client.tsx
  • components/gallery/history-client.tsx
  • components/gallery/visibility-toggle.test.tsx
  • components/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.md
  • components/gallery/favorites-client.tsx
  • components/studio/features/canvas/canvas-feature.test.tsx
  • components/gallery/history-client.tsx
  • components/studio/features/canvas/canvas-feature.tsx
  • components/gallery/history-client.test.tsx
  • components/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.md
  • components/gallery/feed-client.tsx
  • components/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.md
  • 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: 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.md
  • components/gallery/favorites-client.tsx
  • components/gallery/feed-client.tsx
  • components/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.md
  • 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 `next/image` for images in Next.js.

Applied to files:

  • components/studio/controls/reference-image-picker.test.tsx
  • components/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.tsx
  • components/gallery/history-client.test.tsx
  • components/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.

Comment thread .agent/rules/coding-standards.md Outdated
Comment on lines +93 to +99
{isLowBalance ? (
<AlertTriangle className="w-3 h-3 mr-1" />
) : (
<Coins className="w-3 h-3 mr-1" />
)}
{formattedBalance}
</Badge>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
{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.

Comment on lines +53 to +60
export interface LuminousTideEffectProps {
/** Whether the effect is active */
isActive?: boolean
/** Optional progress (0-100) to influence wave behavior */
progress?: number
/** Additional className */
className?: string
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.div

Please 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.

Comment on lines +88 to +91
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" : ""
)}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

!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.

Suggested 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" : ""
)}
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.

Comment on lines +13 to +15
function createPromptId(value: string): Id<"prompts"> {
return value as unknown as Id<"prompts">;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Check the file and surrounding context
head -20 components/studio/features/prompt-library/prompt-detail.test.tsx

Repository: 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:

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.

Comment thread components/studio/features/prompt-library/prompt-detail.test.tsx
Comment thread components/ui/sensitive-content-overlay.test.tsx Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 3121775 and d554c01.

📒 Files selected for processing (2)
  • components/studio/canvas/image-canvas.tsx
  • components/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 of any which defeats the point of using TypeScript.
Let TypeScript inference work; export explicit prop types for shared/public components.
No any type in TypeScript. Use unknown and narrow instead.
Avoid as casts in TypeScript; prefer satisfies, generics, and runtime validation.
@ts-expect-error allowed 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 useEffect for 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.

Comment thread components/studio/canvas/image-canvas.tsx
Comment on lines +160 to +193
// 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>
)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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>
@Simplereally Simplereally merged commit 3c05fdd into main Jan 16, 2026
3 of 4 checks passed
@Simplereally Simplereally deleted the feat/introduce-truly-public-feed branch January 16, 2026 08:20
@coderabbitai coderabbitai Bot mentioned this pull request Jan 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants