Skip to content

feat: Flow Mode V2 with blob storage and storage limit management#50

Merged
WuMingDao merged 10 commits intomainfrom
dev
Dec 13, 2025
Merged

feat: Flow Mode V2 with blob storage and storage limit management#50
WuMingDao merged 10 commits intomainfrom
dev

Conversation

@WuMingDao
Copy link
Copy Markdown
Owner

@WuMingDao WuMingDao commented Dec 13, 2025

Summary

  • FlowPageV2: New visual canvas for batch image generation with i18n support
  • Blob Storage: Separate IndexedDB storage for images (33% smaller than base64)
  • Storage Limits: Dual limits (500 images / 4GB) with LRU cleanup
  • User Confirmation: Modal for downloading images before cleanup
  • Security: Upgrade wrangler to v4, fix esbuild vulnerability
  • Documentation: Add security section and providers reference

Changes

Flow Mode V2

  • Visual node-based canvas using React Flow
  • Batch generation with preview
  • Lightbox for image viewing
  • Download all images as ZIP

Storage Architecture

  • Separate IndexedDB blob store (zenith-image-blobs)
  • Raw Blob storage instead of base64 (33% smaller)
  • LRU tracking with lastAccessedAt timestamps
  • Image proxy endpoint for external URLs (CORS bypass)

Storage Limits

  • Max 500 images OR 4GB storage (whichever triggers first)
  • Warning at 80% capacity
  • User confirmation modal before cleanup
  • Download all option before cleanup

Security & CI

  • Upgrade wrangler to v4
  • Specify wrangler version in GitHub Actions
  • Add security documentation

Test plan

  • Generate images in Flow Mode
  • Verify images are stored in IndexedDB blob store
  • Test download all as ZIP
  • Test lightbox navigation
  • Verify storage limit modal appears when limit is reached
  • Test cleanup after user confirmation

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Local image caching with IndexedDB, LRU cleanup, storage limits, Storage Limit modal, bulk download, and lightbox
    • Multi-language support (English/Chinese) with language switcher and i18n initialization
    • New flow UI: FlowPageV2, conversation flow editor, FlowInput, and new node types (config, image, message)
  • Improvements

    • Branching and active-path highlighting, selection indicators, preview/edit workflows, model validation/fallback, ZIP download of images
  • Chores

    • CI deployment configured to use pnpm and Wrangler v4

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

WuMingDao and others added 8 commits December 13, 2025 21:07
Fixes CVE in esbuild <= 0.24.2 (development server request exposure).
Wrangler 4.x removes the old @esbuild-plugins/* dependencies that
required the vulnerable esbuild version.

- Upgrade wrangler from 3.x to 4.x
- All esbuild versions now >= 0.25.0

Resolves: https://github.com/WuMingDao/zenith-image-generator/security/dependabot/1

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Add node selection for branching from any node in the flow
- Implement active path highlighting from root to selected node
- Add MiniMap with colored nodes based on selection state
- Improve node styling with glassmorphism effects and glow on selection
- Add branch indicator badge on selected nodes
- Update FloatingInput to show branching source indicator
- Auto-pan viewport to new nodes after creation
- Add model validation to ensure correct model for provider
- Update CORS config to include localhost:5174
- Add zustand and react-markdown dependencies

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Add wranglerVersion: "4" to deploy workflows
- Add packageManager: pnpm for consistency

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Add FlowPageV2 with node-based workflow canvas (ConfigNode, ImageNode, FlowInput, Lightbox)
- Add i18n support for Chinese/English with auto-detection via i18next
- Add IndexedDB persistence for flow state with hydration tracking to prevent race conditions
- Add localStorage persistence for input configuration (aspect ratio, resolution, batch count, seed)
- Add batch download as ZIP with webp-to-png conversion using JSZip
- Add LanguageSwitcher component for language toggle

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Create imageBlobStore.ts with dedicated IndexedDB for image blobs
- Store raw Blob instead of base64 (33% smaller)
- Add 100 image limit with LRU eviction policy
- Update flowStore to use blobId reference instead of inline data
- Update ImageNode/Lightbox to load blobs via Object URL
- Fix CORS issues by downloading from local blob storage
- Add image proxy endpoint for external URLs
- Update allowed hosts whitelist from provider configs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Increase MAX_IMAGES from 100 to 500
- Add MAX_STORAGE_MB limit of 4GB
- Implement dual-limit LRU cleanup (count OR size triggers eviction)
- Optimize for 2K/4K image workflows

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Show modal when storage limit (500 images / 4GB) is reached
- User can download all images before cleanup
- User must confirm cleanup before old images are removed
- Add i18n support for modal texts (en/zh)
- Update README with Flow Mode storage limits
- Fix lint warning in FlowInput.tsx

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented Dec 13, 2025

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

Project Deployment Review Updated (UTC)
zenith-image-generator-web Ready Ready Preview, Comment Dec 13, 2025 5:24pm

@netlify
Copy link
Copy Markdown

netlify Bot commented Dec 13, 2025

Deploy Preview for frolicking-blancmange-ef6637 canceled.

Name Link
🔨 Latest commit 0ce7434
🔍 Latest deploy log https://app.netlify.com/projects/frolicking-blancmange-ef6637/deploys/693da130ac55f70008b92360

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Dec 13, 2025

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
zenith-image-generator-api 0ce7434 Commit Preview URL

Branch Preview URL
Dec 13 2025, 05:24 PM

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Dec 13, 2025

Walkthrough

Adds a React Flow-based Flow Mode with local IndexedDB image blob storage and LRU cleanup, i18n support (en/zh) and language switcher, new flow & conversation Zustand stores, many Flow UI components (nodes, lightbox, storage modal), an API image-proxy endpoint, and CI updates to use pnpm + Wrangler v4.

Changes

Cohort / File(s) Summary
CI & Deploy
​.github/workflows/deploy-cf-pages.yml, ​.github/workflows/deploy-cf-workers.yml, ​.github/workflows/preview.yml, package.json, apps/api/package.json
Add packageManager: pnpm and wranglerVersion: "4" inputs to Wrangler actions; bump wrangler devDependency to v4.x.
API / Config
apps/api/src/app.ts, apps/api/src/config.ts
Add GET /proxy-image endpoint with URL validation and proxied fetch; extend default dev CORS origins to include http://localhost:5174.
Shared Validation
packages/shared/src/utils/validation.ts, packages/shared/src/utils/validation.test.ts
Extend allowed image host whitelist (additional HF/OSS and cloud storage suffixes); update test URL.
i18n & Locales
apps/web/src/lib/i18n.ts, apps/web/src/locales/en.json, apps/web/src/locales/zh.json, apps/web/src/components/ui/LanguageSwitcher.tsx
Initialize i18next with browser detection and localStorage cache; add English/Chinese locale files and a LanguageSwitcher component.
Image Blob Store
apps/web/src/lib/imageBlobStore.ts
New IndexedDB-backed blob store with BLOBS/META stores, STORAGE_LIMITS (500 images / 4GB), LRU eviction, CRUD, cleanup helpers, and blob/URL conversion utilities.
Utilities
apps/web/src/lib/utils.ts
Add proxy helpers (getApiUrl, needsProxy, getProxiedUrl), canvas PNG conversion utilities, and downloadImagesAsZip (JSZip) with per-image progress and HF URL handling.
Web deps & entry
apps/web/package.json, apps/web/src/main.tsx, apps/web/src/App.tsx
Add i18n, jszip, react-markdown, zustand deps; import i18n on startup; switch /flow route to FlowPageV2.
Flow Store & Conversation Store
apps/web/src/stores/flowStore.ts, apps/web/src/stores/conversationFlowStore.ts
New Zustand stores: useFlowStore (config/image nodes, preview, persistence, blob cleanup integration) and useConversationFlowStore (message graph with dagre layout and active path). Export related types/constants.
Flow UI Components
apps/web/src/components/flow/*
ConfigNode.tsx, ImageNode.tsx, AIResultNode.tsx, UserPromptNode.tsx, MessageNode.tsx, FloatingInput.tsx, FlowInput.tsx, Lightbox.tsx, StorageLimitModal.tsx
Add many Flow Mode components: config and message nodes, image generation/display node with blob integration, selection-aware UI, floating input with selected-node indicator, FlowInput (stateful/persistent controls), Lightbox with navigation/download, and StorageLimitModal. Several components add/modify exported props/types.
Flow Pages
apps/web/src/pages/FlowPageV2.tsx, apps/web/src/pages/FlowPage.tsx, apps/web/src/pages/ConversationFlowPage.tsx
Add FlowPageV2 (Flow UI with API settings, bulk download, storage management), enhance FlowPage with path/branch selection and highlighting, and add ConversationFlowPage for message-graph editing.
Docs
README.md
Document Flow Mode features: local IndexedDB caching, 500-image/4GB limits, LRU cleanup with confirmation, and download-before-cleanup guidance.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant FlowPageV2
    participant FlowInput
    participant useFlowStore
    participant ImageNode
    participant API as API/proxy-image
    participant imageBlobStore as IndexedDB

    User->>FlowPageV2: Open Flow Mode
    FlowPageV2->>useFlowStore: initialize / hydrate persisted state
    User->>FlowInput: Enter prompt & submit
    FlowInput->>useFlowStore: confirmConfig (create config + image nodes)
    useFlowStore-->>FlowPageV2: update nodes/edges
    FlowPageV2->>ImageNode: render and start generation
    ImageNode->>API: generateImageApi / fetch image
    API-->>ImageNode: return image URL / blob
    ImageNode->>imageBlobStore: checkStorageLimit(newBlobSize)
    alt storage OK
        ImageNode->>imageBlobStore: storeBlob(id, blob)
        imageBlobStore-->>ImageNode: saved (blobId/object URL)
    else near limit
        ImageNode->>useFlowStore: setStorageLimitState
        FlowPageV2->>User: show StorageLimitModal
        User->>FlowPageV2: choose Download All or Cleanup
        FlowPageV2->>imageBlobStore: cleanup oldest blobs (LRU)
        imageBlobStore-->>FlowPageV2: blobs removed
        ImageNode->>imageBlobStore: storeBlob(retry)
    end
    ImageNode->>useFlowStore: updateImageGenerated (url/blobId)
    User->>ImageNode: double-click
    ImageNode->>useFlowStore: setLightboxImage
    FlowPageV2->>FlowPageV2: open Lightbox (reads blobs via imageBlobStore)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Areas to focus during review:

  • apps/web/src/lib/imageBlobStore.ts: IndexedDB schema, LRU eviction correctness, race conditions during concurrent store/cleanup.
  • apps/web/src/stores/flowStore.ts and flow <> components coordination: hydration, persistence boundaries, preview/edit flows.
  • apps/web/src/components/flow/ImageNode.tsx and Lightbox.tsx: object URL lifecycle, blob vs URL handling, download fallbacks.
  • apps/api/src/app.ts: /proxy-image URL validation and CORS defaults.
  • CI/workflows and devDeps: Wrangler v4 inputs and devDependency bump compatibility.

Possibly related PRs

Poem

🐰 I hop through nodes with nimble feet,

Blobs tucked safe where IndexedDB and LRU meet,
Languages flip with a cheerful click,
Branches bloom and images stack quick,
The rabbit hums, "Zip ’em up—what a treat!" 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 51.92% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ 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 and specifically describes the main change: introducing Flow Mode V2 with blob storage and storage limit management features.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dev

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0b89f4f and 0ce7434.

📒 Files selected for processing (1)
  • packages/shared/src/utils/validation.test.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Biome for all linting and formatting operations (pnpm lint, pnpm format, pnpm check)

Files:

  • packages/shared/src/utils/validation.test.ts
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: WuMingDao/zenith-image-generator PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-12T02:11:01.015Z
Learning: Applies to apps/web/src/**/*.{ts,tsx} : Persist Flow mode state using IndexedDB storage (via src/lib/flow-storage.ts) for nodes, edges, and images
🧬 Code graph analysis (1)
packages/shared/src/utils/validation.test.ts (1)
packages/shared/src/utils/validation.ts (1)
  • isAllowedImageUrl (75-82)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Workers Builds: zenith-image-generator-api
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (1)
packages/shared/src/utils/validation.test.ts (1)

88-88: No action needed. The test URL mrfakename-z-image-turbo.hf.space is correctly present in ALLOWED_IMAGE_HOSTS, and the old URL luca115-z-image-turbo.hf.space has been removed from the allow list. The test change aligns properly with the updated configuration.


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

@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: 19

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/api/src/config.ts (1)

23-31: Align CORS defaults across getConfig() and getCorsOriginsFromBindings() + fix Biome formatting.
Right now bindings-defaults omit http://localhost:5174 (and :4173) even though getConfig() includes them, which can cause confusing dev-only CORS failures.

   const defaultOrigins = isDev
-    ? ['http://localhost:5173', 'http://localhost:5174', 'http://localhost:3000', 'http://localhost:4173']
+    ? [
+        'http://localhost:5173',
+        'http://localhost:5174',
+        'http://localhost:3000',
+        'http://localhost:4173',
+      ]
     : []

 export function getCorsOriginsFromBindings(env?: { CORS_ORIGINS?: string }): string[] {
-  const defaultOrigins = ['http://localhost:5173', 'http://localhost:3000']
+  const defaultOrigins = [
+    'http://localhost:5173',
+    'http://localhost:5174',
+    'http://localhost:3000',
+    'http://localhost:4173',
+  ]

Also applies to: 43-51

🧹 Nitpick comments (19)
apps/web/src/components/flow/ConfigNode.tsx (1)

87-87: Consider custom memo comparison for ConfigData fields.

The default memo uses shallow prop comparison. Since ConfigData has multiple fields, you could optionally provide a custom comparison function to prevent unnecessary re-renders when only non-visual fields change.

Example:

-export const ConfigNode = memo(ConfigNodeComponent)
+export const ConfigNode = memo(
+  ConfigNodeComponent,
+  (prev, next) =>
+    prev.data.prompt === next.data.prompt &&
+    prev.data.width === next.data.width &&
+    prev.data.height === next.data.height &&
+    prev.data.batchCount === next.data.batchCount &&
+    prev.data.seed === next.data.seed &&
+    prev.data.isPreview === next.data.isPreview &&
+    prev.selected === next.selected
+)

However, the default behavior is perfectly acceptable for most use cases.

apps/web/src/components/flow/UserPromptNode.tsx (1)

66-72: Redundant ternary: both branches are identical.

The source Handle's className uses a ternary that returns the same value regardless of selected:

selected ? '!bg-orange-500 !border-orange-400' : '!bg-orange-500 !border-orange-400'

This appears to be a copy-paste oversight. Either remove the ternary or differentiate the non-selected state like the target Handle.

       <Handle
         type="source"
         position={Position.Bottom}
         className={`!w-3 !h-3 !border-2 transition-colors ${
-          selected ? '!bg-orange-500 !border-orange-400' : '!bg-orange-500 !border-orange-400'
+          selected ? '!bg-orange-500 !border-orange-400' : '!bg-zinc-600 !border-zinc-500'
         }`}
       />

Or if the source Handle should always be orange:

       <Handle
         type="source"
         position={Position.Bottom}
-        className={`!w-3 !h-3 !border-2 transition-colors ${
-          selected ? '!bg-orange-500 !border-orange-400' : '!bg-orange-500 !border-orange-400'
-        }`}
+        className="!w-3 !h-3 !border-2 !bg-orange-500 !border-orange-400 transition-colors"
       />
apps/web/src/components/flow/FloatingInput.tsx (2)

106-112: Consider guarding against undefined onClearSelection.

The clear button's onClick directly references onClearSelection, which is an optional prop. While clicking won't crash (just a no-op), consider an explicit guard for clarity:

             <button
               type="button"
-              onClick={onClearSelection}
+              onClick={() => onClearSelection?.()}
               className="ml-1 p-0.5 rounded hover:bg-orange-500/20 text-orange-400 hover:text-orange-300 transition-colors"
             >

159-167: Hardcoded Chinese text inconsistent with i18n approach.

Line 162 uses hardcoded Chinese text "图片生成模式", while FlowInput.tsx uses t('flow.imageGenMode') for the same string. Consider using i18n here for consistency across the codebase.

+import { useTranslation } from 'react-i18next'
 // ... in component:
+  const { t } = useTranslation()
 // ...
-              <span className="text-zinc-500 font-medium">图片生成模式</span>
+              <span className="text-zinc-500 font-medium">{t('flow.imageGenMode')}</span>
apps/web/src/components/flow/FlowInput.tsx (2)

74-94: Remove commented-out code.

Lines 76-80 contain commented-out code. If this logic is intentionally disabled, consider removing it entirely or adding a TODO/FIXME comment explaining why it's preserved for future reference.

   // Update preview when config changes
   useEffect(() => {
-    // // If editing mode and not modified, don't show preview
-    // if (editingConfigId && !isEditingModified) {
-    //   setPreviewConfig(null)
-    //   return
-    // }
-
     // If there's content, show preview
     if (prompt.trim()) {

113-125: break only exits inner loop; outer loop continues unnecessarily.

When a matching resolution is found, break exits only the inner j loop, but the outer i loop continues iterating. Consider using a labeled break or restructuring with a flag for efficiency.

   const loadConfig = useCallback((config: ConfigData) => {
     setPrompt(config.prompt)
     setSeed(config.seed)
     setBatchCount(config.batchCount)
 
     // Find matching aspect ratio and resolution
+    outer:
     for (let i = 0; i < ASPECT_RATIOS.length; i++) {
       const ratio = ASPECT_RATIOS[i]
       for (let j = 0; j < ratio.presets.length; j++) {
         if (ratio.presets[j].w === config.width && ratio.presets[j].h === config.height) {
           setAspectRatioIndex(i)
           setResolutionIndex(j)
-          break
+          break outer
         }
       }
     }
 
     textareaRef.current?.focus()
   }, [])
apps/web/src/lib/i18n.ts (1)

1-28: Configure supported languages (en/zh) to avoid region-code mismatches + organize imports.

 import i18n from 'i18next'
-import { initReactI18next } from 'react-i18next'
 import LanguageDetector from 'i18next-browser-languagedetector'
+import { initReactI18next } from 'react-i18next'

 import en from '@/locales/en.json'
 import zh from '@/locales/zh.json'

 i18n
   .use(LanguageDetector)
   .use(initReactI18next)
   .init({
     resources,
     fallbackLng: 'en',
+    supportedLngs: ['en', 'zh'],
+    nonExplicitSupportedLngs: true,
     interpolation: {
       escapeValue: false, // React already escapes values
     },
     detection: {
       order: ['localStorage', 'navigator'],
       caches: ['localStorage'],
     },
   })
apps/web/src/components/flow/MessageNode.tsx (1)

12-18: Localize labels/tooltips and don’t force en-US timestamps.
Hardcoded strings (You, AI, Copy, etc.) and toLocaleTimeString('en-US', ...) bypass the new i18n system.

Also applies to: 67-90, 110-138

apps/web/src/pages/FlowPage.tsx (2)

46-65: Unused _nodes parameter in calculatePathToNode.

The _nodes parameter is declared but never used in this function. It's correctly prefixed with underscore to indicate intentional unused status, but consider removing it entirely if not needed for future use.

 function calculatePathToNode(
   nodeId: string,
-  _nodes: Node[],
   edges: Edge[]
 ): string[] {

Then update the call site at line 91:

-    return calculatePathToNode(selectedNodeId, nodes, edges)
+    return calculatePathToNode(selectedNodeId, edges)

261-262: Branch parent fallback relies on array order.

The fallback nodes[nodes.length - 1] assumes the last node in the array is the most recently added. This works for linear flows but may behave unexpectedly if nodes are reordered or filtered elsewhere. Consider using a timestamp or explicit tracking of the "last added" node if this becomes an issue.

apps/web/src/lib/utils.ts (1)

96-99: Pre-existing object URL leak in downloadImage.

Line 98 creates an object URL for img.src that is never revoked after the image loads. While this is pre-existing code, consider fixing it alongside the new convertToPngBlob function for consistency.

         img.onload = () => {
+          URL.revokeObjectURL(img.src)
           const canvas = document.createElement('canvas')
           // ... rest of handler
         }
-        img.onerror = () => reject(new Error('Failed to load image'))
+        img.onerror = () => {
+          URL.revokeObjectURL(img.src)
+          reject(new Error('Failed to load image'))
+        }
         img.src = URL.createObjectURL(blob)
apps/web/src/components/flow/ImageNode.tsx (1)

34-49: Consider adding a timeout to prevent hanging requests.

The fetch call has no timeout, which could cause the UI to hang indefinitely if the server doesn't respond.

-  const res = await fetch(`${baseUrl}/api/generate`, {
+  const controller = new AbortController()
+  const timeoutId = setTimeout(() => controller.abort(), 120000) // 2 min timeout for image generation
+  
+  const res = await fetch(`${baseUrl}/api/generate`, {
     method: 'POST',
     headers: {
       'Content-Type': 'application/json',
       ...(token && { [providerConfig.authHeader]: token }),
     },
     body: JSON.stringify({
       provider,
       prompt,
       model,
       width,
       height,
       steps: 9,
       seed,
     }),
+    signal: controller.signal,
   })
+  
+  clearTimeout(timeoutId)
apps/web/src/pages/ConversationFlowPage.tsx (4)

85-99: Mock AI response should be replaced with real API integration.

The generateMockAIResponse function is a placeholder. Consider adding a TODO comment or tracking issue for real API integration.

Would you like me to open an issue to track the real API integration for conversation flow?


172-188: Consider using React Flow's built-in applyNodeChanges for better performance.

The current implementation manually handles position changes. React Flow provides applyNodeChanges utility that handles this more efficiently.

+import { applyNodeChanges } from '@xyflow/react'
+
 // In the component:
-onNodesChange={(changes) => {
-  // Handle node position changes from dragging
-  const state = useConversationFlowStore.getState()
-  const updatedNodes = [...state.nodes]
-  for (const change of changes) {
-    if (change.type === 'position' && change.position) {
-      const nodeIndex = updatedNodes.findIndex(n => n.id === change.id)
-      if (nodeIndex !== -1) {
-        updatedNodes[nodeIndex] = {
-          ...updatedNodes[nodeIndex],
-          position: change.position,
-        }
-      }
-    }
-  }
-  setNodes(updatedNodes)
-}}
+onNodesChange={(changes) => {
+  const positionChanges = changes.filter(c => c.type === 'position')
+  if (positionChanges.length > 0) {
+    const state = useConversationFlowStore.getState()
+    setNodes(applyNodeChanges(positionChanges, state.nodes))
+  }
+}}

145-148: Missing i18n integration for UI strings.

This component has hardcoded English strings while other components in the PR use useTranslation from react-i18next. Consider adding i18n support for consistency.

Example strings that should be translated:

  • "Conversation Flow" (line 146)
  • "Clear" (line 159)
  • "Start a conversation" (line 234)
  • "Replying to" (line 256)
  • "Press Enter to send..." (line 309)

125-130: Remove empty useEffect or implement the intended functionality.

This effect has a comment suggesting optional auto-fit but does nothing. Either implement it or remove the dead code.

-  // Fit view when nodes change significantly
-  useEffect(() => {
-    if (nodes.length > 0) {
-      // Optional: auto-fit on first load
-    }
-  }, [nodes.length])
apps/web/src/lib/imageBlobStore.ts (1)

283-283: Consider removing or reducing log level for cleanup message.

console.log in production can clutter browser console. Consider using console.debug or removing if not needed for debugging.

-      console.log(`Removed oldest blob: ${oldestId}`)
+      // Uncomment for debugging: console.debug(`Removed oldest blob: ${oldestId}`)
apps/web/src/stores/conversationFlowStore.ts (2)

55-89: Consider consolidating with existing layout helper.

This function duplicates getLayoutedElements from apps/web/src/components/flow/layout.ts with slightly different spacing (nodesep: 80 vs 50, ranksep: 100 vs 80). Consider using a shared helper with configurable spacing.

+// In apps/web/src/components/flow/layout.ts, add options parameter:
+export function getLayoutedElements(
+  nodes: Node[],
+  edges: Edge[],
+  options?: { direction?: 'TB' | 'LR'; nodesep?: number; ranksep?: number }
+) {
+  const { direction = 'TB', nodesep = 50, ranksep = 80 } = options ?? {}
+  // ... existing implementation with configurable spacing
+}

366-372: Remove empty initializeFlow function or implement it.

This function does nothing. Either implement the intended initialization logic or remove the dead code.

-    // Initialize with a root node
-    initializeFlow: () => {
-      const state = get()
-      if (state.nodes.length === 0) {
-        // Flow is empty, ready for first message
-      }
-    },
+    // Initialize flow - no-op as first message creates the root
+    initializeFlow: () => {},
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f2ca44c and 9a93a98.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (32)
  • .github/workflows/deploy-cf-pages.yml (1 hunks)
  • .github/workflows/deploy-cf-workers.yml (1 hunks)
  • .github/workflows/preview.yml (1 hunks)
  • README.md (1 hunks)
  • apps/api/package.json (1 hunks)
  • apps/api/src/app.ts (2 hunks)
  • apps/api/src/config.ts (1 hunks)
  • apps/web/package.json (1 hunks)
  • apps/web/src/App.tsx (2 hunks)
  • apps/web/src/components/flow/AIResultNode.tsx (5 hunks)
  • apps/web/src/components/flow/ConfigNode.tsx (1 hunks)
  • apps/web/src/components/flow/FloatingInput.tsx (3 hunks)
  • apps/web/src/components/flow/FlowInput.tsx (1 hunks)
  • apps/web/src/components/flow/ImageNode.tsx (1 hunks)
  • apps/web/src/components/flow/Lightbox.tsx (1 hunks)
  • apps/web/src/components/flow/MessageNode.tsx (1 hunks)
  • apps/web/src/components/flow/StorageLimitModal.tsx (1 hunks)
  • apps/web/src/components/flow/UserPromptNode.tsx (3 hunks)
  • apps/web/src/components/ui/LanguageSwitcher.tsx (1 hunks)
  • apps/web/src/lib/i18n.ts (1 hunks)
  • apps/web/src/lib/imageBlobStore.ts (1 hunks)
  • apps/web/src/lib/utils.ts (2 hunks)
  • apps/web/src/locales/en.json (1 hunks)
  • apps/web/src/locales/zh.json (1 hunks)
  • apps/web/src/main.tsx (1 hunks)
  • apps/web/src/pages/ConversationFlowPage.tsx (1 hunks)
  • apps/web/src/pages/FlowPage.tsx (7 hunks)
  • apps/web/src/pages/FlowPageV2.tsx (1 hunks)
  • apps/web/src/stores/conversationFlowStore.ts (1 hunks)
  • apps/web/src/stores/flowStore.ts (1 hunks)
  • package.json (1 hunks)
  • packages/shared/src/utils/validation.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Biome for all linting and formatting operations (pnpm lint, pnpm format, pnpm check)

Files:

  • apps/web/src/lib/i18n.ts
  • apps/web/src/main.tsx
  • apps/web/src/components/ui/LanguageSwitcher.tsx
  • apps/web/src/components/flow/UserPromptNode.tsx
  • apps/web/src/components/flow/Lightbox.tsx
  • apps/web/src/components/flow/ImageNode.tsx
  • apps/web/src/pages/FlowPageV2.tsx
  • apps/web/package.json
  • apps/web/src/components/flow/MessageNode.tsx
  • package.json
  • apps/web/src/locales/en.json
  • packages/shared/src/utils/validation.ts
  • apps/web/src/stores/flowStore.ts
  • apps/web/src/components/flow/AIResultNode.tsx
  • apps/web/src/lib/utils.ts
  • apps/web/src/components/flow/FlowInput.tsx
  • apps/api/package.json
  • apps/web/src/components/flow/StorageLimitModal.tsx
  • apps/web/src/components/flow/ConfigNode.tsx
  • apps/web/src/pages/ConversationFlowPage.tsx
  • apps/web/src/lib/imageBlobStore.ts
  • apps/web/src/stores/conversationFlowStore.ts
  • apps/web/src/pages/FlowPage.tsx
  • apps/api/src/config.ts
  • apps/web/src/locales/zh.json
  • apps/web/src/components/flow/FloatingInput.tsx
  • apps/web/src/App.tsx
  • apps/api/src/app.ts
apps/web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/web/src/**/*.{ts,tsx}: Use @/ path alias for imports in frontend code (maps to src/)
Persist settings and API keys to localStorage with AES-256-GCM encryption (use src/lib/crypto.ts)
Persist Flow mode state using IndexedDB storage (via src/lib/flow-storage.ts) for nodes, edges, and images
Configure API URL via VITE_API_URL environment variable, defaulting to relative path for same-origin deployment

Files:

  • apps/web/src/lib/i18n.ts
  • apps/web/src/main.tsx
  • apps/web/src/components/ui/LanguageSwitcher.tsx
  • apps/web/src/components/flow/UserPromptNode.tsx
  • apps/web/src/components/flow/Lightbox.tsx
  • apps/web/src/components/flow/ImageNode.tsx
  • apps/web/src/pages/FlowPageV2.tsx
  • apps/web/src/components/flow/MessageNode.tsx
  • apps/web/src/stores/flowStore.ts
  • apps/web/src/components/flow/AIResultNode.tsx
  • apps/web/src/lib/utils.ts
  • apps/web/src/components/flow/FlowInput.tsx
  • apps/web/src/components/flow/StorageLimitModal.tsx
  • apps/web/src/components/flow/ConfigNode.tsx
  • apps/web/src/pages/ConversationFlowPage.tsx
  • apps/web/src/lib/imageBlobStore.ts
  • apps/web/src/stores/conversationFlowStore.ts
  • apps/web/src/pages/FlowPage.tsx
  • apps/web/src/components/flow/FloatingInput.tsx
  • apps/web/src/App.tsx
apps/web/src/**/*.{tsx,ts}

📄 CodeRabbit inference engine (CLAUDE.md)

Use React 19 with Vite, Tailwind CSS, and shadcn/ui components in the frontend (apps/web)

Files:

  • apps/web/src/lib/i18n.ts
  • apps/web/src/main.tsx
  • apps/web/src/components/ui/LanguageSwitcher.tsx
  • apps/web/src/components/flow/UserPromptNode.tsx
  • apps/web/src/components/flow/Lightbox.tsx
  • apps/web/src/components/flow/ImageNode.tsx
  • apps/web/src/pages/FlowPageV2.tsx
  • apps/web/src/components/flow/MessageNode.tsx
  • apps/web/src/stores/flowStore.ts
  • apps/web/src/components/flow/AIResultNode.tsx
  • apps/web/src/lib/utils.ts
  • apps/web/src/components/flow/FlowInput.tsx
  • apps/web/src/components/flow/StorageLimitModal.tsx
  • apps/web/src/components/flow/ConfigNode.tsx
  • apps/web/src/pages/ConversationFlowPage.tsx
  • apps/web/src/lib/imageBlobStore.ts
  • apps/web/src/stores/conversationFlowStore.ts
  • apps/web/src/pages/FlowPage.tsx
  • apps/web/src/components/flow/FloatingInput.tsx
  • apps/web/src/App.tsx
apps/api/src/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

apps/api/src/**/*.ts: POST /api/generate endpoint must support 'gitee' and 'huggingface' as provider parameters, require X-API-Key header for Gitee AI, and support optional X-HF-Token header for HuggingFace
Use Hono API framework for Cloudflare Workers implementation in the API app (apps/api)

Files:

  • apps/api/src/config.ts
  • apps/api/src/app.ts
🧠 Learnings (9)
📓 Common learnings
Learnt from: CR
Repo: WuMingDao/zenith-image-generator PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-12T02:11:01.015Z
Learning: Applies to apps/web/src/**/*.{ts,tsx} : Persist Flow mode state using IndexedDB storage (via src/lib/flow-storage.ts) for nodes, edges, and images
📚 Learning: 2025-12-12T02:11:01.015Z
Learnt from: CR
Repo: WuMingDao/zenith-image-generator PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-12T02:11:01.015Z
Learning: Deploy API to Cloudflare Workers using 'wrangler deploy --minify src/index.ts' from apps/api directory

Applied to files:

  • .github/workflows/preview.yml
  • .github/workflows/deploy-cf-workers.yml
  • apps/api/package.json
  • .github/workflows/deploy-cf-pages.yml
📚 Learning: 2025-12-12T02:11:01.015Z
Learnt from: CR
Repo: WuMingDao/zenith-image-generator PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-12T02:11:01.015Z
Learning: Applies to apps/web/src/**/*.{ts,tsx} : Use @/ path alias for imports in frontend code (maps to src/)

Applied to files:

  • apps/web/src/lib/i18n.ts
  • apps/web/src/main.tsx
  • apps/web/src/App.tsx
📚 Learning: 2025-12-12T02:11:01.015Z
Learnt from: CR
Repo: WuMingDao/zenith-image-generator PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-12T02:11:01.015Z
Learning: Applies to apps/web/src/**/*.{ts,tsx} : Persist settings and API keys to localStorage with AES-256-GCM encryption (use src/lib/crypto.ts)

Applied to files:

  • apps/web/src/lib/i18n.ts
📚 Learning: 2025-12-12T02:11:01.015Z
Learnt from: CR
Repo: WuMingDao/zenith-image-generator PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-12T02:11:01.015Z
Learning: Applies to apps/web/src/**/*.{tsx,ts} : Use React 19 with Vite, Tailwind CSS, and shadcn/ui components in the frontend (apps/web)

Applied to files:

  • apps/web/src/main.tsx
  • apps/web/package.json
  • apps/web/src/components/flow/AIResultNode.tsx
  • apps/web/src/lib/utils.ts
  • apps/api/package.json
  • apps/web/src/components/flow/FloatingInput.tsx
  • apps/web/src/App.tsx
📚 Learning: 2025-12-12T02:11:01.015Z
Learnt from: CR
Repo: WuMingDao/zenith-image-generator PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-12T02:11:01.015Z
Learning: Run development with 'pnpm dev' for all apps or use app-specific commands: pnpm dev:web for frontend (port 5173) or pnpm dev:api for API (port 8787)

Applied to files:

  • .github/workflows/deploy-cf-workers.yml
📚 Learning: 2025-12-12T02:11:01.015Z
Learnt from: CR
Repo: WuMingDao/zenith-image-generator PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-12T02:11:01.015Z
Learning: Applies to apps/web/src/**/*.{ts,tsx} : Persist Flow mode state using IndexedDB storage (via src/lib/flow-storage.ts) for nodes, edges, and images

Applied to files:

  • apps/web/src/components/flow/Lightbox.tsx
  • apps/web/src/components/flow/ImageNode.tsx
  • apps/web/src/pages/FlowPageV2.tsx
  • apps/web/src/components/flow/MessageNode.tsx
  • apps/web/src/stores/flowStore.ts
  • apps/web/src/components/flow/AIResultNode.tsx
  • apps/web/src/components/flow/FlowInput.tsx
  • apps/web/src/components/flow/StorageLimitModal.tsx
  • apps/web/src/components/flow/ConfigNode.tsx
  • apps/web/src/pages/ConversationFlowPage.tsx
  • apps/web/src/lib/imageBlobStore.ts
  • apps/web/src/stores/conversationFlowStore.ts
  • apps/web/src/pages/FlowPage.tsx
  • README.md
  • apps/web/src/components/flow/FloatingInput.tsx
  • apps/web/src/App.tsx
📚 Learning: 2025-12-12T02:11:01.015Z
Learnt from: CR
Repo: WuMingDao/zenith-image-generator PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-12T02:11:01.015Z
Learning: Applies to apps/web/src/**/*.{ts,tsx} : Configure API URL via VITE_API_URL environment variable, defaulting to relative path for same-origin deployment

Applied to files:

  • apps/web/src/lib/utils.ts
  • apps/api/src/config.ts
📚 Learning: 2025-12-12T02:11:01.015Z
Learnt from: CR
Repo: WuMingDao/zenith-image-generator PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-12T02:11:01.015Z
Learning: Applies to apps/api/src/**/*.ts : Use Hono API framework for Cloudflare Workers implementation in the API app (apps/api)

Applied to files:

  • apps/api/package.json
  • apps/api/src/app.ts
🧬 Code graph analysis (13)
apps/web/src/components/flow/Lightbox.tsx (3)
apps/web/src/stores/flowStore.ts (1)
  • useFlowStore (160-433)
apps/web/src/lib/imageBlobStore.ts (3)
  • getBlob (161-180)
  • blobToObjectUrl (322-324)
  • blobToDataUrl (329-336)
apps/web/src/lib/utils.ts (1)
  • downloadImage (49-115)
apps/web/src/pages/FlowPageV2.tsx (8)
apps/web/src/components/flow/ConfigNode.tsx (1)
  • ConfigNode (87-87)
apps/web/src/components/flow/ImageNode.tsx (1)
  • ImageNode (306-306)
apps/web/src/lib/constants.ts (3)
  • loadSettings (49-56)
  • getDefaultModel (70-73)
  • saveSettings (58-60)
apps/web/src/lib/crypto.ts (2)
  • loadAllTokens (74-84)
  • encryptAndStoreToken (35-53)
apps/web/src/lib/imageBlobStore.ts (2)
  • getBlob (161-180)
  • blobToDataUrl (329-336)
apps/web/src/lib/utils.ts (1)
  • downloadImagesAsZip (178-230)
apps/web/src/components/ui/LanguageSwitcher.tsx (1)
  • LanguageSwitcher (4-23)
apps/web/src/components/flow/Lightbox.tsx (1)
  • Lightbox (8-246)
apps/web/src/components/flow/MessageNode.tsx (1)
apps/web/src/stores/conversationFlowStore.ts (1)
  • MessageNodeData (12-21)
apps/web/src/stores/flowStore.ts (2)
apps/web/api/[[...route]].ts (1)
  • config (12-14)
apps/web/src/lib/imageBlobStore.ts (2)
  • deleteBlobs (198-212)
  • clearAllBlobs (296-304)
apps/web/src/lib/utils.ts (1)
apps/api/src/index.ts (1)
  • fetch (17-21)
apps/web/src/components/flow/FlowInput.tsx (1)
apps/web/src/stores/flowStore.ts (2)
  • useFlowStore (160-433)
  • ConfigData (16-25)
apps/web/src/components/flow/StorageLimitModal.tsx (1)
apps/web/src/lib/imageBlobStore.ts (1)
  • STORAGE_LIMITS (19-23)
apps/web/src/components/flow/ConfigNode.tsx (1)
apps/web/src/stores/flowStore.ts (1)
  • ConfigData (16-25)
apps/web/src/pages/ConversationFlowPage.tsx (2)
apps/web/src/components/flow/MessageNode.tsx (1)
  • MessageNode (155-155)
apps/web/src/stores/conversationFlowStore.ts (1)
  • useConversationFlowStore (110-374)
apps/web/src/stores/conversationFlowStore.ts (2)
apps/web/src/components/flow/MessageNode.tsx (1)
  • MessageRole (157-157)
apps/web/src/components/flow/layout.ts (1)
  • getLayoutedElements (7-34)
apps/web/src/pages/FlowPage.tsx (1)
apps/web/src/components/flow/FloatingInput.tsx (1)
  • FloatingInput (20-190)
apps/web/src/App.tsx (1)
apps/web/src/pages/FlowPageV2.tsx (1)
  • FlowPageV2 (403-409)
apps/api/src/app.ts (4)
apps/api/src/middleware/error-handler.ts (1)
  • sendError (28-32)
packages/shared/src/types/error.ts (1)
  • Errors (95-156)
packages/shared/src/utils/validation.ts (1)
  • isAllowedImageUrl (75-82)
apps/api/src/index.ts (1)
  • fetch (17-21)
🪛 GitHub Actions: CI
apps/web/src/lib/i18n.ts

[error] 1-4: The imports and exports are not sorted. Safe fix suggested by Biome: Organize Imports.

apps/web/src/components/ui/LanguageSwitcher.tsx

[error] 1-3: The imports and exports are not sorted. Safe fix suggested by Biome: Organize Imports.

apps/web/src/components/flow/Lightbox.tsx

[error] 5-7: The imports and exports are not sorted. Safe fix suggested by Biome: Organize Imports.

apps/web/src/components/flow/ImageNode.tsx

[error] 7-12: The imports and exports are not sorted. Safe fix suggested by Biome: Organize Imports.

apps/web/src/components/flow/MessageNode.tsx

[error] 1-3: The imports and exports are not sorted. Safe fix suggested by Biome: Organize Imports.

apps/web/src/stores/flowStore.ts

[error] 308-346: Biome formatter would have printed different content. Various object spread and function call formatting adjustments detected.


[error] 321-329: Biome: formatting change suggested for node update logic in image handling.

apps/web/src/components/flow/AIResultNode.tsx

[error] 182-186: Biome formatter would have printed different content. Minor formatting in AIResultNode.tsx around selected styling.

apps/web/src/components/flow/FlowInput.tsx

[error] 1-4: The imports and exports are not sorted. Safe fix suggested by Biome: Organize Imports.

apps/web/src/components/flow/ConfigNode.tsx

[error] 1-3: The imports and exports are not sorted. Safe fix suggested by Biome: Organize Imports.

apps/web/src/stores/conversationFlowStore.ts

[error] 90-95: Biome formatter would have printed different content. Detected path calculation and update blocks formatting changes.

apps/api/src/config.ts

[error] 23-32: Biome formatter would have printed different content. Detected formatting issues in apps/api/src/config.ts (cors origins formatting).

apps/api/src/app.ts

[error] 346-351: Biome formatter would have printed different content. Detected formatting issues in apps/api/src/app.ts (image proxy error handling).

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Workers Builds: zenith-image-generator-api
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (35)
apps/web/src/components/flow/ConfigNode.tsx (1)

11-85: LGTM! Well-structured component with proper conditional rendering.

The component implementation is clean and follows React best practices:

  • Clear separation of preview vs confirmed node states
  • Good UX with prompt truncation, hover hints, and visual badges
  • Proper i18n integration for multi-language support
  • Appropriate use of React Flow's Handle component
.github/workflows/deploy-cf-workers.yml (1)

47-48: LGTM! Wrangler v4 configuration added.

The explicit packageManager and wranglerVersion configuration aligns with the PR's upgrade to Wrangler v4 and ensures consistent deployment behavior.

.github/workflows/deploy-cf-pages.yml (1)

50-51: LGTM! Wrangler v4 configuration added.

The configuration matches the upgrade pattern across all Cloudflare deployment workflows.

package.json (1)

39-39: Wrangler v4 upgrade aligns with API package.

The version update is consistent with apps/api/package.json. Please refer to the verification request for apps/api/package.json to confirm version validity and security status.

.github/workflows/preview.yml (1)

61-62: LGTM! Wrangler v4 configuration added.

The configuration is consistent with other Cloudflare deployment workflows in this PR.

apps/web/src/locales/en.json (1)

1-130: LGTM! Well-structured locale file.

The English locale file is comprehensive and properly formatted:

  • Clear namespace organization
  • Consistent placeholder syntax ({{variable}})
  • Professional translations
  • Appropriate coverage for all UI features
packages/shared/src/utils/validation.ts (1)

61-72: LGTM! Image host whitelist appropriately expanded.

The updated whitelist adds support for additional HuggingFace Spaces and cloud storage providers (Aliyun OSS, Baidu Cloud), which aligns with the PR's image proxy functionality. The added comments improve clarity.

apps/web/package.json (1)

28-42: All dependency versions verified and compatible with React 19.2.0.

All 12 package versions exist in npm registry. React peer dependency requirements are satisfied: react-i18next@16.5.0 requires React ≥ 16.8.0 and react-markdown@10.1.0 requires React ≥ 18, both compatible with React 19.2.0. No known security vulnerabilities detected in any of the specified versions.

apps/api/package.json (1)

26-26: Wrangler v4.54.0 is a valid and secure version.

The specified version exists on npm and has no known security vulnerabilities. Earlier CVEs (CVE-2023-3348, CVE-2023-7079, CVE-2023-7080) only affected Wrangler v2/v3; v4 is the actively maintained line with security fixes applied.

apps/web/src/main.tsx (1)

4-5: LGTM!

The i18n initialization import is correctly placed before the App import, ensuring translations are ready before the application renders. This follows the recommended pattern for i18next setup.

README.md (1)

29-32: LGTM!

Good documentation of the new Flow Mode storage features. The bullet points clearly communicate the storage limits, cleanup behavior, and user safeguards.

apps/web/src/locales/zh.json (1)

1-130: Looks consistent; verify placeholder parity with en.json.
Main thing to double-check is that every key using {{...}} matches the English resource’s placeholder names (same spelling/casing), otherwise i18next will silently fail interpolation.

apps/web/src/components/flow/Lightbox.tsx (2)

91-119: LGTM - Download implementation with blob preference.

The download logic correctly prioritizes blob storage over URL fallback, and the data URL approach for download doesn't require cleanup.


40-61: Potential object URL leak when effect is cancelled.

If the async operation completes but cancelled is true (e.g., component unmounted or image changed during fetch), the object URL created at line 47 won't be stored in objectUrlRef and thus won't be revoked.

Consider revoking the URL if cancelled:

       if (cancelled) return
+      // If cancelled after blob fetch, still need to track/revoke
 
       if (blob) {
         const url = blobToObjectUrl(blob)
+        if (cancelled) {
+          URL.revokeObjectURL(url)
+          return
+        }
         objectUrlRef.current = url
         setDisplayUrl(url)

Or restructure to check cancelled before creating the URL.

Likely an incorrect or invalid review comment.

apps/web/src/lib/utils.ts (1)

178-220: LGTM - ZIP download with robust error handling.

The downloadImagesAsZip function has good error handling with fallback to original images when PNG conversion fails, and properly tracks progress. The object URL for the final ZIP blob is correctly revoked at line 229.

apps/web/src/pages/FlowPageV2.tsx (2)

229-255: Edge case: imageNode may have stale data after cleanup.

When handleConfirmCleanup runs, it fetches imageNode from imageNodes at line 244, but cleanupForNewBlob at line 238 may have already removed some images from storage. The imageNode.data.imageUrl could reference a deleted blob.

Additionally, if the cleanup removes the pending image's node from the store, imageNode will be undefined and the update will be skipped silently.

Consider adding defensive checks or ensuring the cleanup doesn't affect the pending image:

     // Perform cleanup
     await cleanupForNewBlob(pendingBlob.size)
 
     // Store the pending blob
     const blobId = await storeBlob(pendingImageId, pendingBlob)
+    if (!blobId) {
+      console.error('Failed to store pending blob after cleanup')
+      clearStorageLimitState()
+      return
+    }
 
     // Update the image with the new blobId
     const imageNode = imageNodes.find((n) => n.id === pendingImageId)
-    if (imageNode && blobId) {
+    if (imageNode) {

163-221: LGTM - Download implementation with progress tracking.

The handleDownloadAll function properly:

  • Filters images with data available
  • Prioritizes blob storage over URLs (avoiding CORS issues)
  • Provides user feedback via progress state
  • Handles errors gracefully
apps/web/src/components/flow/AIResultNode.tsx (2)

118-125: LGTM - Model validation with provider fallback.

Good defensive coding to validate the saved model against the provider's available models and fall back to the default if invalid. This prevents errors when switching providers.


270-276: Bottom Handle correctly differentiates selected state.

Unlike the top Handle, the bottom Handle properly shows different styling for selected vs non-selected states (orange when selected, zinc when not).

apps/web/src/components/flow/ImageNode.tsx (4)

91-125: LGTM!

The blob loading effect has proper cancellation handling, correctly revokes previous object URLs before creating new ones, and includes appropriate fallback logic.


137-210: LGTM - well-designed generation flow with proper safeguards.

The effect properly:

  • Waits for store hydration before generating
  • Uses generatingRef to prevent duplicate generation attempts
  • Handles storage limits gracefully by updating UI even when blob storage fails
  • Has comprehensive error handling

212-237: LGTM!

The download handler properly prioritizes blob storage and falls back to URL-based download when needed.


245-303: LGTM!

The component rendering is well-structured with clear loading, error, and success states. The hover overlay provides good UX with download functionality.

apps/web/src/pages/ConversationFlowPage.tsx (1)

324-345: LGTM - inline styles are acceptable for component-specific edge styling.

The inline CSS is scoped to this component's canvas class and handles React Flow edge customization that would be difficult to achieve with Tailwind alone.

apps/web/src/lib/imageBlobStore.ts (3)

32-53: LGTM - Database initialization with proper upgrade handling.

The database setup correctly handles the upgrade path with separate stores for blobs and metadata, including appropriate indexes for LRU tracking.


55-89: LGTM!

The storage limit check properly evaluates both count and size constraints, returning structured data that the UI can use to display appropriate messaging.


157-180: LGTM!

The blob retrieval correctly updates the LRU timestamp while ensuring the update failure doesn't prevent blob access. Good defensive design.

apps/web/src/stores/flowStore.ts (5)

1-5: LGTM - Clean imports with proper path aliases.

Imports correctly use the @/ path alias as per coding guidelines, and import necessary utilities from the imageBlobStore.


131-158: LGTM!

The IndexedDB storage adapter is well-implemented with proper error handling for each operation.


196-254: LGTM!

The confirmConfig action correctly creates the config node and batch image nodes with proper positioning, seed incrementing, and ID counter management.


338-369: LGTM - Async blob cleanup with sync state update is a good pattern.

The state updates immediately for responsive UI while blob cleanup happens asynchronously. The .catch(console.error) ensures failures don't crash but are logged for debugging.


417-430: LGTM - Persistence configuration correctly partializes state.

The persist configuration properly:

  • Excludes transient UI state (preview, editing, lightbox, storage limit)
  • Uses IndexedDB storage as per coding guidelines
  • Sets hydration flag for components to await

Based on learnings, this correctly implements Flow mode state persistence using IndexedDB.

apps/web/src/stores/conversationFlowStore.ts (3)

125-216: LGTM - Comprehensive message addition with proper state management.

The addMessage action correctly:

  • Creates nodes with proper metadata
  • Maintains parent-child relationships
  • Applies layout after changes
  • Updates active path and styling for all nodes/edges

229-303: LGTM - Robust node deletion with descendant cleanup.

The deletion logic correctly handles:

  • Recursive descendant discovery
  • Parent relationship updates
  • Edge filtering
  • Active node fallback when deleted
  • Path recalculation

1-4: This comment incorrectly applies the Flow mode persistence guideline to conversationFlowStore. The codebase distinguishes between Flow mode (image generation, which uses flowStore with IndexedDB persistence) and Conversation mode (conversation tree, which uses conversationFlowStore as a session-based store). The coding guideline "Persist Flow mode state using IndexedDB storage" applies specifically to the image generation workflow, not conversation state. Conversation state starting empty on page load and having a clearFlow() button indicates intentional session-only behavior.

Likely an incorrect or invalid review comment.

Comment thread apps/api/src/app.ts
Comment on lines +48 to 50
const defaultOrigins = ['http://localhost:5173', 'http://localhost:5174', 'http://localhost:3000']
const origins = config.corsOrigins || defaultOrigins

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Keep CORS defaults in sync with config.ts and bindings defaults.
You added http://localhost:5174 here, but getCorsOriginsFromBindings() (apps/api/src/config.ts) still omits it, so Wrangler/Workers dev may still fail CORS unless env is set.

🤖 Prompt for AI Agents
In apps/api/src/app.ts around lines 48-50, defaultOrigins includes
'http://localhost:5174' but getCorsOriginsFromBindings() in
apps/api/src/config.ts and the bindings/defaults do not — causing a CORS
mismatch for Wrangler/Workers dev. Fix by adding 'http://localhost:5174' to the
CORS defaults in apps/api/src/config.ts (update the getCorsOriginsFromBindings()
default list) and update any related bindings/defaults (e.g., dev wrangler
bindings or shared defaults) so both places use the same origin list or, better,
centralize the default origins into a single exported constant and reference it
from app.ts and config.ts.

Comment thread apps/api/src/app.ts
Comment on lines +333 to +363
// Image proxy endpoint (for CORS bypass when downloading external images)
app.get('/proxy-image', async (c) => {
const url = c.req.query('url')

if (!url || typeof url !== 'string') {
return sendError(c, Errors.invalidParams('url', 'url query parameter is required'))
}

if (!isAllowedImageUrl(url)) {
return sendError(c, Errors.invalidParams('url', 'URL not allowed'))
}

try {
const response = await fetch(url)
if (!response.ok) {
return sendError(c, Errors.generationFailed('Image Proxy', `Failed to fetch image: ${response.status}`))
}

const contentType = response.headers.get('content-type') || 'image/png'
const blob = await response.blob()

return new Response(blob, {
headers: {
'Content-Type': contentType,
'Cache-Control': 'public, max-age=86400',
},
})
} catch (err) {
return sendError(c, err)
}
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Harden /proxy-image: prevent allowlist bypass (redirects), non-image proxying, and unbounded buffering.
As implemented, a permitted URL can 302/307 to a disallowed host (TOCTOU), and you’ll proxy arbitrary content-types while buffering full bodies into memory.

Proposed diff (keeps behavior but adds guardrails + fixes Biome formatting complaint nearby):

   // Image proxy endpoint (for CORS bypass when downloading external images)
   app.get('/proxy-image', async (c) => {
     const url = c.req.query('url')

     if (!url || typeof url !== 'string') {
       return sendError(c, Errors.invalidParams('url', 'url query parameter is required'))
     }

     if (!isAllowedImageUrl(url)) {
       return sendError(c, Errors.invalidParams('url', 'URL not allowed'))
     }

     try {
-      const response = await fetch(url)
+      const controller = new AbortController()
+      const timeoutId = setTimeout(() => controller.abort(), 15_000)
+
+      const response = await fetch(url, {
+        redirect: 'manual',
+        signal: controller.signal,
+      }).finally(() => clearTimeout(timeoutId))
+
       if (!response.ok) {
-        return sendError(c, Errors.generationFailed('Image Proxy', `Failed to fetch image: ${response.status}`))
+        return sendError(
+          c,
+          Errors.generationFailed('Image Proxy', `Failed to fetch image: ${response.status}`)
+        )
       }
 
-      const contentType = response.headers.get('content-type') || 'image/png'
+      // Block redirect-based allowlist bypass (re-validate the Location URL).
+      if (response.status >= 300 && response.status < 400) {
+        const location = response.headers.get('location')
+        if (!location) {
+          return sendError(c, Errors.generationFailed('Image Proxy', 'Redirect missing location header'))
+        }
+        const redirectedUrl = new URL(location, url).toString()
+        if (!isAllowedImageUrl(redirectedUrl)) {
+          return sendError(c, Errors.invalidParams('url', 'Redirect URL not allowed'))
+        }
+        return sendError(c, Errors.generationFailed('Image Proxy', 'Redirects are not supported'))
+      }
+
+      const contentType = response.headers.get('content-type') || ''
+      if (!contentType.toLowerCase().startsWith('image/')) {
+        return sendError(c, Errors.generationFailed('Image Proxy', 'Upstream is not an image'))
+      }
+
+      // Optional: avoid huge buffers (best-effort; header may be absent).
+      const contentLength = Number(response.headers.get('content-length') ?? '0')
+      if (contentLength && contentLength > 25 * 1024 * 1024) {
+        return sendError(c, Errors.generationFailed('Image Proxy', 'Image too large'))
+      }
+
       const blob = await response.blob()

       return new Response(blob, {
         headers: {
           'Content-Type': contentType,
           'Cache-Control': 'public, max-age=86400',
         },
       })
     } catch (err) {
       return sendError(c, err)
     }
   })

Also: isAllowedImageUrl() currently uses hostname.endsWith(host) citeturn0search0 which typically needs a “dot-boundary” check (hostname === host || hostname.endsWith(\.${host}`)) to avoid evilallowed.commatchingallowed.com. Consider tightening it in packages/shared/src/utils/validation.ts`.

📝 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
// Image proxy endpoint (for CORS bypass when downloading external images)
app.get('/proxy-image', async (c) => {
const url = c.req.query('url')
if (!url || typeof url !== 'string') {
return sendError(c, Errors.invalidParams('url', 'url query parameter is required'))
}
if (!isAllowedImageUrl(url)) {
return sendError(c, Errors.invalidParams('url', 'URL not allowed'))
}
try {
const response = await fetch(url)
if (!response.ok) {
return sendError(c, Errors.generationFailed('Image Proxy', `Failed to fetch image: ${response.status}`))
}
const contentType = response.headers.get('content-type') || 'image/png'
const blob = await response.blob()
return new Response(blob, {
headers: {
'Content-Type': contentType,
'Cache-Control': 'public, max-age=86400',
},
})
} catch (err) {
return sendError(c, err)
}
})
// Image proxy endpoint (for CORS bypass when downloading external images)
app.get('/proxy-image', async (c) => {
const url = c.req.query('url')
if (!url || typeof url !== 'string') {
return sendError(c, Errors.invalidParams('url', 'url query parameter is required'))
}
if (!isAllowedImageUrl(url)) {
return sendError(c, Errors.invalidParams('url', 'URL not allowed'))
}
try {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 15_000)
const response = await fetch(url, {
redirect: 'manual',
signal: controller.signal,
}).finally(() => clearTimeout(timeoutId))
if (!response.ok) {
return sendError(
c,
Errors.generationFailed('Image Proxy', `Failed to fetch image: ${response.status}`)
)
}
// Block redirect-based allowlist bypass (re-validate the Location URL).
if (response.status >= 300 && response.status < 400) {
const location = response.headers.get('location')
if (!location) {
return sendError(c, Errors.generationFailed('Image Proxy', 'Redirect missing location header'))
}
const redirectedUrl = new URL(location, url).toString()
if (!isAllowedImageUrl(redirectedUrl)) {
return sendError(c, Errors.invalidParams('url', 'Redirect URL not allowed'))
}
return sendError(c, Errors.generationFailed('Image Proxy', 'Redirects are not supported'))
}
const contentType = response.headers.get('content-type') || ''
if (!contentType.toLowerCase().startsWith('image/')) {
return sendError(c, Errors.generationFailed('Image Proxy', 'Upstream is not an image'))
}
// Optional: avoid huge buffers (best-effort; header may be absent).
const contentLength = Number(response.headers.get('content-length') ?? '0')
if (contentLength && contentLength > 25 * 1024 * 1024) {
return sendError(c, Errors.generationFailed('Image Proxy', 'Image too large'))
}
const blob = await response.blob()
return new Response(blob, {
headers: {
'Content-Type': contentType,
'Cache-Control': 'public, max-age=86400',
},
})
} catch (err) {
return sendError(c, err)
}
})
🧰 Tools
🪛 GitHub Actions: CI

[error] 346-351: Biome formatter would have printed different content. Detected formatting issues in apps/api/src/app.ts (image proxy error handling).

🤖 Prompt for AI Agents
In apps/api/src/app.ts around lines 333 to 363, the proxy currently follows
redirects (allowlist can be bypassed), buffers the entire body into memory and
doesn't ensure the resource is an image; change fetch to use redirect: 'manual'
and if the response is a 3xx reject the request (deny redirects), validate the
Content-Type header exists and startsWith('image/') and respond with an error if
not, enforce a maximum allowed size by checking Content-Length and rejecting if
it exceeds a safe limit (e.g. 5MB), and return the upstream response body as a
streamed Response (use response.body) instead of awaiting response.blob() to
avoid unbounded buffering; additionally tighten isAllowedImageUrl in
packages/shared/src/utils/validation.ts to use a dot-boundary check (hostname
=== host || hostname.endsWith(`.${host}`)) to prevent evilallowed.com matching
allowed.com.

Comment thread apps/web/src/App.tsx
Comment on lines +3 to 6
// import ConversationFlowPage from './pages/ConversationFlowPage'
// import FlowPage from './pages/FlowPage'
import FlowPageV2 from './pages/FlowPageV2'
import ImageGenerator from './pages/ImageGenerator'
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use @/ alias for FlowPageV2 import (project convention).

-// import ConversationFlowPage from './pages/ConversationFlowPage'
-// import FlowPage from './pages/FlowPage'
-import FlowPageV2 from './pages/FlowPageV2'
+import FlowPageV2 from '@/pages/FlowPageV2'

(Optionally drop the commented imports/routes if they’re not intentionally kept as scaffolding.)

Also applies to: 14-16

🤖 Prompt for AI Agents
In apps/web/src/App.tsx around lines 3 to 6 (and similarly apply to lines 14 to
16), replace the relative imports with the project alias: use
"@/pages/FlowPageV2" for FlowPageV2 (and "@/pages/ImageGenerator" for
ImageGenerator if applicable) to follow the project's import convention;
optionally remove the commented-out imports/routes if they are not intentionally
kept as scaffolding.

Comment on lines +180 to +196
<div
className={`
relative backdrop-blur-md border rounded-xl p-4 min-w-[280px] shadow-2xl
transition-all duration-300 ease-out
${selected
? 'bg-zinc-900/80 border-orange-500/50 ring-2 ring-orange-500/40 shadow-[0_0_30px_rgba(249,115,22,0.2)]'
: 'bg-zinc-900/60 border-zinc-700 hover:border-zinc-600'
}
`}
>
<Handle
type="target"
position={Position.Top}
className={`!w-3 !h-3 !border-2 transition-colors ${
selected ? '!bg-orange-500 !border-orange-400' : '!bg-orange-500 !border-orange-400'
}`}
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix formatting to resolve CI failure, and review Handle styling.

  1. The pipeline indicates Biome formatting issues around lines 182-186. Run pnpm format to fix.

  2. The top Handle styling (lines 193-195) uses identical styles for both selected and non-selected states:

selected ? '!bg-orange-500 !border-orange-400' : '!bg-orange-500 !border-orange-400'

This ternary has no effect. If intentional (always orange), simplify to a single class string. If the top handle should differ when selected, update the non-selected style.

       <Handle
         type="target"
         position={Position.Top}
-        className={`!w-3 !h-3 !border-2 transition-colors ${
-          selected ? '!bg-orange-500 !border-orange-400' : '!bg-orange-500 !border-orange-400'
-        }`}
+        className="!w-3 !h-3 !border-2 !bg-orange-500 !border-orange-400"
       />
🧰 Tools
🪛 GitHub Actions: CI

[error] 182-186: Biome formatter would have printed different content. Minor formatting in AIResultNode.tsx around selected styling.

🤖 Prompt for AI Agents
In apps/web/src/components/flow/AIResultNode.tsx around lines 180 to 196, Biome
formatting errors are present (run pnpm format to fix) and the top Handle uses a
redundant ternary that returns the same classes for selected and non-selected
states; remove the unnecessary ternary and replace it with a single class string
if the orange styling should always apply, or adjust the second branch to the
intended non-selected classes so the selection state actually differs.

Comment thread apps/web/src/components/flow/ConfigNode.tsx
Comment on lines +309 to +316
export async function urlToBlob(url: string): Promise<Blob> {
const apiUrl = import.meta.env.VITE_API_URL || ''
const isExternal = url.startsWith('http') && !url.includes(window.location.host)
const fetchUrl = isExternal ? `${apiUrl}/api/proxy-image?url=${encodeURIComponent(url)}` : url

const response = await fetch(fetchUrl)
return await response.blob()
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add error handling for fetch failure in urlToBlob.

If the fetch fails (network error, 404, etc.), response.blob() will throw. The caller should know why it failed.

 export async function urlToBlob(url: string): Promise<Blob> {
   const apiUrl = import.meta.env.VITE_API_URL || ''
   const isExternal = url.startsWith('http') && !url.includes(window.location.host)
   const fetchUrl = isExternal ? `${apiUrl}/api/proxy-image?url=${encodeURIComponent(url)}` : url

   const response = await fetch(fetchUrl)
+  if (!response.ok) {
+    throw new Error(`Failed to fetch image: ${response.status} ${response.statusText}`)
+  }
   return await response.blob()
 }
📝 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 urlToBlob(url: string): Promise<Blob> {
const apiUrl = import.meta.env.VITE_API_URL || ''
const isExternal = url.startsWith('http') && !url.includes(window.location.host)
const fetchUrl = isExternal ? `${apiUrl}/api/proxy-image?url=${encodeURIComponent(url)}` : url
const response = await fetch(fetchUrl)
return await response.blob()
}
export async function urlToBlob(url: string): Promise<Blob> {
const apiUrl = import.meta.env.VITE_API_URL || ''
const isExternal = url.startsWith('http') && !url.includes(window.location.host)
const fetchUrl = isExternal ? `${apiUrl}/api/proxy-image?url=${encodeURIComponent(url)}` : url
const response = await fetch(fetchUrl)
if (!response.ok) {
throw new Error(`Failed to fetch image: ${response.status} ${response.statusText}`)
}
return await response.blob()
}
🤖 Prompt for AI Agents
In apps/web/src/lib/imageBlobStore.ts around lines 309 to 316, the urlToBlob
function lacks error handling for fetch failures; wrap the fetch in a try/catch,
check response.ok after the fetch and if not ok throw a new Error with the HTTP
status, statusText and the attempted URL (or proxy URL), and in the catch
rethrow a descriptive error that includes the original error message and the URL
so callers can distinguish network errors from HTTP errors.

Comment thread apps/web/src/lib/utils.ts
Comment on lines +140 to +172
// Convert to PNG using canvas
return new Promise((resolve, reject) => {
const img = new Image()
img.crossOrigin = 'anonymous'

img.onload = () => {
const canvas = document.createElement('canvas')
canvas.width = img.naturalWidth
canvas.height = img.naturalHeight
const ctx = canvas.getContext('2d')
if (!ctx) {
reject(new Error('Failed to get canvas context'))
return
}
ctx.drawImage(img, 0, 0)

canvas.toBlob(
(pngBlob) => {
if (!pngBlob) {
reject(new Error('Failed to convert to PNG'))
return
}
resolve(pngBlob)
},
'image/png',
1.0
)
}

img.onerror = () => reject(new Error('Failed to load image'))
img.src = URL.createObjectURL(blob)
})
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Object URL not revoked in convertToPngBlob.

Line 170 creates an object URL for img.src but it's never revoked after the image loads. This causes a memory leak, especially when processing many images in a ZIP download.

     img.onload = () => {
       const canvas = document.createElement('canvas')
       canvas.width = img.naturalWidth
       canvas.height = img.naturalHeight
       const ctx = canvas.getContext('2d')
       if (!ctx) {
+        URL.revokeObjectURL(img.src)
         reject(new Error('Failed to get canvas context'))
         return
       }
       ctx.drawImage(img, 0, 0)
 
       canvas.toBlob(
         (pngBlob) => {
+          URL.revokeObjectURL(img.src)
           if (!pngBlob) {
             reject(new Error('Failed to convert to PNG'))
             return
           }
           resolve(pngBlob)
         },
         'image/png',
         1.0
       )
     }
 
-    img.onerror = () => reject(new Error('Failed to load image'))
+    img.onerror = () => {
+      URL.revokeObjectURL(img.src)
+      reject(new Error('Failed to load image'))
+    }
     img.src = URL.createObjectURL(blob)
📝 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
// Convert to PNG using canvas
return new Promise((resolve, reject) => {
const img = new Image()
img.crossOrigin = 'anonymous'
img.onload = () => {
const canvas = document.createElement('canvas')
canvas.width = img.naturalWidth
canvas.height = img.naturalHeight
const ctx = canvas.getContext('2d')
if (!ctx) {
reject(new Error('Failed to get canvas context'))
return
}
ctx.drawImage(img, 0, 0)
canvas.toBlob(
(pngBlob) => {
if (!pngBlob) {
reject(new Error('Failed to convert to PNG'))
return
}
resolve(pngBlob)
},
'image/png',
1.0
)
}
img.onerror = () => reject(new Error('Failed to load image'))
img.src = URL.createObjectURL(blob)
})
}
// Convert to PNG using canvas
return new Promise((resolve, reject) => {
const img = new Image()
img.crossOrigin = 'anonymous'
img.onload = () => {
const canvas = document.createElement('canvas')
canvas.width = img.naturalWidth
canvas.height = img.naturalHeight
const ctx = canvas.getContext('2d')
if (!ctx) {
URL.revokeObjectURL(img.src)
reject(new Error('Failed to get canvas context'))
return
}
ctx.drawImage(img, 0, 0)
canvas.toBlob(
(pngBlob) => {
URL.revokeObjectURL(img.src)
if (!pngBlob) {
reject(new Error('Failed to convert to PNG'))
return
}
resolve(pngBlob)
},
'image/png',
1.0
)
}
img.onerror = () => {
URL.revokeObjectURL(img.src)
reject(new Error('Failed to load image'))
}
img.src = URL.createObjectURL(blob)
})
}
🤖 Prompt for AI Agents
In apps/web/src/lib/utils.ts around lines 140 to 172, the object URL created
with URL.createObjectURL(blob) is never revoked causing a memory leak; change
the function to store the returned URL in a const, call URL.revokeObjectURL(url)
in every exit path (before resolving or rejecting) — i.e., revoke in img.onload
(both on success after resolve and on failures like missing ctx or pngBlob) and
in img.onerror — so the blob URL is always revoked regardless of success or
error.

Comment on lines +34 to +46
function ConversationFlowCanvas() {
const { setCenter } = useReactFlow()

const {
nodes,
edges,
activeNodeId,
addMessage,
setActiveNode,
clearFlow,
setNodes,
} = useConversationFlowStore()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check if conversationFlowStore has persistence configured
rg -n "persist|IndexedDB|openDB|flow-storage" apps/web/src/stores/conversationFlowStore.ts

# Compare with flowStore persistence pattern
rg -n "persist|IndexedDB|openDB|flow-storage" apps/web/src/stores/flowStore.ts

# List store files to understand structure
fd "Store\.ts$" apps/web/src/stores/

Repository: WuMingDao/zenith-image-generator

Length of output: 908


🏁 Script executed:

# Check the size and content of conversationFlowStore.ts
wc -l apps/web/src/stores/conversationFlowStore.ts

# Read the conversationFlowStore to see how it's currently structured
head -80 apps/web/src/stores/conversationFlowStore.ts

Repository: WuMingDao/zenith-image-generator

Length of output: 2180


🏁 Script executed:

# Check if there are any persist or storage references in the full conversationFlowStore
rg -n "persist|storage|IndexedDB|flow-storage" apps/web/src/stores/conversationFlowStore.ts

# Check if flow-storage.ts exists and what it exports
ls -la apps/web/src/lib/flow-storage.ts 2>/dev/null && head -50 apps/web/src/lib/flow-storage.ts

# Verify flowStore uses flow-storage
rg -n "flow-storage" apps/web/src/stores/flowStore.ts

Repository: WuMingDao/zenith-image-generator

Length of output: 1312


🏁 Script executed:

# Check how flowStore uses flow-storage
rg -A5 -B5 "flow-storage" apps/web/src/stores/flowStore.ts | head -50

# Check the ConversationFlowPage.tsx to understand the context
sed -n '1,50p' apps/web/src/pages/ConversationFlowPage.tsx

# Check if conversationFlowStore is persisted anywhere else
rg -n "conversationFlowStore" apps/web/src/ | head -20

Repository: WuMingDao/zenith-image-generator

Length of output: 1478


🏁 Script executed:

# Get full flowStore to understand persistence implementation
cat -n apps/web/src/stores/flowStore.ts | head -200

# Check if flowStore uses flow-storage.ts
grep -n "flow-storage" apps/web/src/stores/flowStore.ts

Repository: WuMingDao/zenith-image-generator

Length of output: 6686


🏁 Script executed:

# Check if there are any comments or issues about conversationFlowStore persistence
rg -i "conversation.*persist|persist.*conversation" apps/web/src/

# Check the rest of conversationFlowStore to see full store implementation
tail -100 apps/web/src/stores/conversationFlowStore.ts

Repository: WuMingDao/zenith-image-generator

Length of output: 2663


Add IndexedDB persistence to conversationFlowStore following the flowStore pattern.

The conversationFlowStore lacks any persistence mechanism. Currently, conversation flow state (nodes, edges, messages) is lost on page refresh. Implement persistence using Zustand's persist middleware with IndexedDB storage adapter, matching the pattern used in flowStore.ts (lines 110-162). This requires:

  • Adding persist middleware to the store creation
  • Implementing custom IndexedDB storage adapter
  • Persisting at least: nodes, edges, activeNodeId, nodeIdCounter
  • Excluding non-persistent fields (UI state like previews/editing modes)

Comment thread apps/web/src/stores/conversationFlowStore.ts
Comment thread apps/web/src/stores/flowStore.ts
- Resolve merge conflicts in apps/web/package.json and pnpm-lock.yaml
- Fix Biome import organization errors
- Fix Biome formatting issues

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Dec 13, 2025

Deploying zenith-image-generator with  Cloudflare Pages  Cloudflare Pages

Latest commit: 0ce7434
Status: ✅  Deploy successful!
Preview URL: https://649de979.zenith-image-generator.pages.dev
Branch Preview URL: https://dev.zenith-image-generator.pages.dev

View logs

Update isAllowedImageUrl test to use mrfakename-z-image-turbo.hf.space
instead of luca115-z-image-turbo.hf.space to match the whitelist.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Copy link
Copy Markdown

@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

♻️ Duplicate comments (13)
apps/web/src/components/flow/StorageLimitModal.tsx (2)

30-108: Consider using shadcn/ui Dialog for accessibility.

The custom modal lacks proper accessibility features (role="dialog", aria-modal, focus trap, ESC-to-close). Past review comments recommended refactoring to use shadcn/ui Dialog primitives for WCAG compliance.


51-58: Handle reason: null explicitly.

When reason is null, the ternary falls through to the 'size' branch, which may be unintentional. This issue was previously flagged in past review comments.

           <p className="text-zinc-300">
-            {reason === 'count'
-              ? t('storage.limitReachedCount', { count: STORAGE_LIMITS.MAX_IMAGES })
-              : t('storage.limitReachedSize', {
-                  size: currentSizeMB.toFixed(1),
-                  max: STORAGE_LIMITS.MAX_STORAGE_MB,
-                })}
+            {reason === 'count' &&
+              t('storage.limitReachedCount', { count: STORAGE_LIMITS.MAX_IMAGES })}
+            {reason === 'size' &&
+              t('storage.limitReachedSize', {
+                size: currentSizeMB.toFixed(1),
+                max: STORAGE_LIMITS.MAX_STORAGE_MB,
+              })}
           </p>
apps/web/src/components/flow/AIResultNode.tsx (1)

191-197: Remove redundant ternary on target Handle.

The top Handle uses identical styling for both selected and non-selected states, rendering the ternary ineffective. This issue was previously flagged in past review comments.

       <Handle
         type="target"
         position={Position.Top}
-        className={`!w-3 !h-3 !border-2 transition-colors ${
-          selected ? '!bg-orange-500 !border-orange-400' : '!bg-orange-500 !border-orange-400'
-        }`}
+        className="!w-3 !h-3 !border-2 transition-colors !bg-orange-500 !border-orange-400"
       />
apps/web/src/components/ui/LanguageSwitcher.tsx (1)

7-10: Normalize language codes to handle region variants.

The direct equality check i18n.language === 'zh' will fail to match region-specific codes like 'zh-CN'. This issue was previously flagged in past review comments.

   const toggleLanguage = async () => {
-    const newLang = i18n.language === 'zh' ? 'en' : 'zh'
-    i18n.changeLanguage(newLang)
+    const current = i18n.resolvedLanguage ?? i18n.language
+    const isZh = current?.toLowerCase().startsWith('zh')
+    const newLang = isZh ? 'en' : 'zh'
+    await i18n.changeLanguage(newLang)
   }
apps/web/src/components/flow/MessageNode.tsx (1)

112-133: Wire or remove non-functional action buttons.

Only the Copy button has behavior. Branch, Reply, and Delete buttons render UI affordances without functionality. This was previously flagged in past review comments.

apps/web/src/lib/utils.ts (1)

140-172: Object URL not revoked in convertToPngBlob.

Line 170 creates an object URL that's never revoked, causing memory leaks during batch processing. This was previously flagged in past review comments.

     img.onload = () => {
       const canvas = document.createElement('canvas')
       canvas.width = img.naturalWidth
       canvas.height = img.naturalHeight
       const ctx = canvas.getContext('2d')
       if (!ctx) {
+        URL.revokeObjectURL(img.src)
         reject(new Error('Failed to get canvas context'))
         return
       }
       ctx.drawImage(img, 0, 0)
 
       canvas.toBlob(
         (pngBlob) => {
+          URL.revokeObjectURL(img.src)
           if (!pngBlob) {
             reject(new Error('Failed to convert to PNG'))
             return
           }
           resolve(pngBlob)
         },
         'image/png',
         1.0
       )
     }
 
-    img.onerror = () => reject(new Error('Failed to load image'))
+    img.onerror = () => {
+      URL.revokeObjectURL(img.src)
+      reject(new Error('Failed to load image'))
+    }
     img.src = URL.createObjectURL(blob)
apps/web/src/pages/ConversationFlowPage.tsx (1)

34-39: Missing IndexedDB persistence for conversation flow state.

As noted in the past review, conversationFlowStore lacks persistence. Conversation state will be lost on page refresh. This is inconsistent with the coding guidelines requiring IndexedDB persistence for Flow mode state.

apps/web/src/components/flow/FlowInput.tsx (1)

1-6: Import sorting issue flagged by CI.

This issue was already flagged in a previous review. The imports need to be sorted according to Biome rules. Run pnpm lint --fix or pnpm check --fix to auto-fix.

apps/web/src/stores/flowStore.ts (1)

306-323: Formatting issue flagged by CI.

This was already flagged in a previous review. Run pnpm format to auto-fix.

apps/web/src/lib/imageBlobStore.ts (3)

95-114: Return value doesn't reflect actual cleanup success.

This was flagged in a previous review. The function returns true even when the loop exits because no more blobs could be deleted, which may mean cleanup failed to free enough space.


120-141: Use a transaction for atomic blob + metadata storage.

This was flagged in a previous review. Storing blob and metadata in separate operations without a transaction can leave orphan blobs if the second write fails.


313-320: Add error handling for fetch failure in urlToBlob.

This was flagged in a previous review. The function should check response.ok before calling response.blob().

apps/web/src/stores/conversationFlowStore.ts (1)

91-105: Formatting issue flagged by CI.

This was flagged in a previous review. Run pnpm format to auto-fix.

🧹 Nitpick comments (16)
apps/web/src/components/flow/ConfigNode.tsx (1)

78-83: Consider accessibility for the double-click hint.

The hover overlay with pointer-events-none correctly prevents interaction conflicts, but screen reader users won't receive this editing hint. Consider adding an aria-label to the parent div or a visually-hidden text alternative for accessibility.

apps/web/src/pages/FlowPageV2.tsx (2)

126-144: Redundant drag handlers can be consolidated.

Both onNodeDrag and onNodeDragStop perform the same operation. Since onNodeDragStop fires when dragging ends, you could remove onNodeDrag to reduce re-renders during drag operations, or keep only onNodeDragStop for the final position update.

-  // Handle node drag - group dragging for config nodes (real-time)
-  const onNodeDrag = useCallback(
-    (_: React.MouseEvent, node: Node) => {
-      if (node.type === 'configNode' && !node.data.isPreview) {
-        updateConfigPosition(node.id, node.position.x, node.position.y)
-      }
-    },
-    [updateConfigPosition]
-  )
-
   // Handle node drag stop - finalize position
   const onNodeDragStop = useCallback(

Then remove onNodeDrag={onNodeDrag} from ReactFlow props (line 259).


345-376: Consider adding backdrop click to close the settings modal.

The settings modal currently only closes via the X button. Adding onClick on the backdrop div to close would improve UX. If intentional, consider aria-modal="true" and focus trapping for accessibility.

       {showSettings && (
-        <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
+        <div
+          className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
+          onClick={() => setShowSettings(false)}
+        >
-          <div className="bg-zinc-900 border border-zinc-800 rounded-xl p-6 w-full max-w-md mx-4">
+          <div
+            className="bg-zinc-900 border border-zinc-800 rounded-xl p-6 w-full max-w-md mx-4"
+            onClick={(e) => e.stopPropagation()}
+          >
apps/web/src/components/flow/ImageNode.tsx (2)

33-76: Consider adding request timeout and avoiding duplicate dynamic imports.

The fetch call lacks a timeout, which could leave requests hanging indefinitely. Also, PROVIDER_CONFIGS is dynamically imported both here and in the effect at line 170, which could be consolidated.

 async function generateImageApi(
   prompt: string,
   width: number,
   height: number,
   provider: ProviderType,
   token: string,
   model: string,
   seed: number
 ): Promise<ImageDetails> {
   const baseUrl = import.meta.env.VITE_API_URL || ''
   const { PROVIDER_CONFIGS } = await import('@/lib/constants')
   const providerConfig = PROVIDER_CONFIGS[provider]

+  const controller = new AbortController()
+  const timeoutId = setTimeout(() => controller.abort(), 120000) // 2 min timeout
+
-  const res = await fetch(`${baseUrl}/api/generate`, {
+  const res = await fetch(`${baseUrl}/api/generate`, {
     method: 'POST',
     headers: {
       'Content-Type': 'application/json',
       ...(token && { [providerConfig.authHeader]: token }),
     },
     body: JSON.stringify({
       provider,
       prompt,
       model,
       width,
       height,
       steps: 9,
       seed,
     }),
+    signal: controller.signal,
   })
+
+  clearTimeout(timeoutId)

149-234: Generation effect has correct guards but consider extracting for readability.

The effect correctly waits for hydration, checks existing URL, and uses a ref to prevent double-generation. The dependency array is comprehensive. However, the effect is quite long (85+ lines) and could benefit from extracting the generation logic into a separate async function for maintainability.

apps/web/src/pages/ConversationFlowPage.tsx (3)

57-93: Direct store access pattern is correct but mock AI should be clearly marked for replacement.

Using useConversationFlowStore.getState() inside setTimeout is the correct pattern to avoid stale closures. However, the mock AI with hardcoded 1500ms delay should have a more prominent TODO comment indicating it needs API integration.

-    // Simulate AI response (replace with actual API call)
+    // TODO: Replace mock AI with actual API integration
+    // This mock delay and response generator are placeholders
     setTimeout(() => {
       const aiResponse = generateMockAIResponse(userContent)

163-179: Node change handler only processes position updates and may cause performance issues.

The handler creates a new array and calls setNodes on every position change during drag, which could be expensive. Consider debouncing or handling only dragging: false changes. Also, other change types (selection, removal) are silently ignored.

           onNodesChange={(changes) => {
-            // Handle node position changes from dragging
             const state = useConversationFlowStore.getState()
-            const updatedNodes = [...state.nodes]
+            let hasPositionChange = false
+            const updatedNodes = state.nodes.map((node) => {
+              const change = changes.find(
+                (c) => c.type === 'position' && c.id === node.id && c.position && !c.dragging
+              )
+              if (change && change.type === 'position' && change.position) {
+                hasPositionChange = true
+                return { ...node, position: change.position }
+              }
+              return node
+            })
-            for (const change of changes) {
-              if (change.type === 'position' && change.position) {
-                const nodeIndex = updatedNodes.findIndex((n) => n.id === change.id)
-                if (nodeIndex !== -1) {
-                  updatedNodes[nodeIndex] = {
-                    ...updatedNodes[nodeIndex],
-                    position: change.position,
-                  }
-                }
-              }
-            }
-            setNodes(updatedNodes)
+            if (hasPositionChange) {
+              setNodes(updatedNodes)
+            }
           }}

309-331: Consider moving inline styles to a CSS module.

The inline <style> tag works but mixes concerns. For better maintainability, consider extracting these styles to a CSS module (ConversationFlowPage.module.css) or using a CSS-in-JS solution consistent with the rest of the codebase.

apps/web/src/components/flow/FlowInput.tsx (4)

46-56: Move loadInputConfig() call outside the component or use lazy initialization.

loadInputConfig() is called on every render because it's invoked directly in the component body. This reads from localStorage synchronously on each render, which is inefficient.

Use lazy initialization for useState to ensure it's called only once:

-  // Load initial config from localStorage
-  const initialConfig = loadInputConfig()
-  const [aspectRatioIndex, setAspectRatioIndex] = useState(initialConfig.aspectRatioIndex)
-  const [resolutionIndex, setResolutionIndex] = useState(initialConfig.resolutionIndex)
-  const [prompt, setPrompt] = useState('')
-  const [batchCount, setBatchCount] = useState(initialConfig.batchCount)
-  const [seed, setSeed] = useState(initialConfig.seed)
+  const [aspectRatioIndex, setAspectRatioIndex] = useState(() => loadInputConfig().aspectRatioIndex)
+  const [resolutionIndex, setResolutionIndex] = useState(() => loadInputConfig().resolutionIndex)
+  const [prompt, setPrompt] = useState('')
+  const [batchCount, setBatchCount] = useState(() => loadInputConfig().batchCount)
+  const [seed, setSeed] = useState(() => loadInputConfig().seed)

Or load once and destructure:

-  const initialConfig = loadInputConfig()
-  const [aspectRatioIndex, setAspectRatioIndex] = useState(initialConfig.aspectRatioIndex)
+  const [aspectRatioIndex, setAspectRatioIndex] = useState(() => {
+    const config = loadInputConfig()
+    return config.aspectRatioIndex
+  })

177-179: Hardcoded preset count may cause issues if aspect ratios have varying preset counts.

cycleResolution hardcodes % 2, but different aspect ratios could have different numbers of presets.

   const cycleResolution = () => {
-    setResolutionIndex((prev) => (prev + 1) % 2)
+    setResolutionIndex((prev) => (prev + 1) % currentAspectRatio.presets.length)
   }

112-122: Nested loop doesn't break out of outer loop when match is found.

When a matching preset is found, only the inner loop breaks, but the outer loop continues iterating unnecessarily.

     // Find matching aspect ratio and resolution
+    outer:
     for (let i = 0; i < ASPECT_RATIOS.length; i++) {
       const ratio = ASPECT_RATIOS[i]
       for (let j = 0; j < ratio.presets.length; j++) {
         if (ratio.presets[j].w === config.width && ratio.presets[j].h === config.height) {
           setAspectRatioIndex(i)
           setResolutionIndex(j)
-          break
+          break outer
         }
       }
     }

Or use return after setting the values if this is the end of the function logic, or extract to a helper.


230-237: "Seed:" label is hardcoded instead of using i18n.

Other UI strings use t() for internationalization, but "Seed:" is hardcoded.

           <Badge
             variant="secondary"
             className="bg-zinc-800 text-zinc-400 rounded-full px-3 py-0.5 text-xs font-normal cursor-pointer hover:bg-zinc-700 flex items-center gap-1"
             onClick={randomizeSeed}
           >
-            Seed: {seed}
+            {t('flow.seed')}: {seed}
             <RefreshCw size={10} className="ml-1" />
           </Badge>

Remember to add the translation key to your locale files.

apps/web/src/lib/imageBlobStore.ts (2)

188-196: Consider using a transaction for atomic deletion from both stores.

Similar to the storeBlob concern, deleteBlob performs two separate delete operations. If the first succeeds but the second fails, metadata becomes orphaned.

 export async function deleteBlob(id: string): Promise<void> {
   try {
     const db = await getDB()
-    await db.delete(BLOBS_STORE, id)
-    await db.delete(META_STORE, id)
+    const tx = db.transaction([BLOBS_STORE, META_STORE], 'readwrite')
+    tx.objectStore(BLOBS_STORE).delete(id)
+    tx.objectStore(META_STORE).delete(id)
+    await tx.done
   } catch (e) {
     console.error('Failed to delete blob:', e)
   }
 }

Note: deleteBlobs (lines 201-215) already uses a transaction correctly.


300-308: Consider using a transaction for atomic clearing of both stores.

clearAllBlobs performs two separate clear operations. For consistency with the PR's emphasis on data integrity, consider wrapping in a transaction.

 export async function clearAllBlobs(): Promise<void> {
   try {
     const db = await getDB()
-    await db.clear(BLOBS_STORE)
-    await db.clear(META_STORE)
+    const tx = db.transaction([BLOBS_STORE, META_STORE], 'readwrite')
+    tx.objectStore(BLOBS_STORE).clear()
+    tx.objectStore(META_STORE).clear()
+    await tx.done
   } catch (e) {
     console.error('Failed to clear all blobs:', e)
   }
 }
apps/web/src/stores/conversationFlowStore.ts (2)

56-89: Duplicate getLayoutedElements implementation.

This function is nearly identical to apps/web/src/components/flow/layout.ts:6-33. Consider importing from the shared module to avoid duplication.

-// Node dimensions for layout calculation
-const NODE_WIDTH = 320
-const NODE_HEIGHT = 180
+import { getLayoutedElements, NODE_WIDTH, NODE_HEIGHT } from '@/components/flow/layout'

-// Helper function to get dagre-layouted elements
-function getLayoutedElements(
-  nodes: Node<MessageNodeData>[],
-  edges: Edge[],
-  direction: 'TB' | 'LR' = 'TB'
-) {
-  // ... entire function body
-}

Note: The shared module may need to export NODE_WIDTH and NODE_HEIGHT constants, or you can keep local constants if dimensions differ between flow types.


356-362: initializeFlow is essentially a no-op.

The function only checks if nodes are empty but doesn't do anything with that information. Either implement initialization logic or remove if unnecessary.

     // Initialize with a root node
     initializeFlow: () => {
-      const state = get()
-      if (state.nodes.length === 0) {
-        // Flow is empty, ready for first message
-      }
+      // Flow is empty and ready for first message
+      // No initialization needed - first addMessage will create the root
     },

Or if you want to create a welcome/system message:

initializeFlow: () => {
  const state = get()
  if (state.nodes.length === 0) {
    state.addMessage(null, 'Welcome! How can I help you?', 'ai')
  }
},
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9a93a98 and 0b89f4f.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (25)
  • .github/workflows/deploy-cf-pages.yml (1 hunks)
  • .github/workflows/deploy-cf-workers.yml (1 hunks)
  • .github/workflows/preview.yml (1 hunks)
  • apps/api/package.json (1 hunks)
  • apps/api/src/app.ts (2 hunks)
  • apps/api/src/config.ts (1 hunks)
  • apps/web/package.json (1 hunks)
  • apps/web/src/components/flow/AIResultNode.tsx (5 hunks)
  • apps/web/src/components/flow/ConfigNode.tsx (1 hunks)
  • apps/web/src/components/flow/FlowInput.tsx (1 hunks)
  • apps/web/src/components/flow/ImageNode.tsx (1 hunks)
  • apps/web/src/components/flow/Lightbox.tsx (1 hunks)
  • apps/web/src/components/flow/MessageNode.tsx (1 hunks)
  • apps/web/src/components/flow/StorageLimitModal.tsx (1 hunks)
  • apps/web/src/components/flow/UserPromptNode.tsx (3 hunks)
  • apps/web/src/components/ui/LanguageSwitcher.tsx (1 hunks)
  • apps/web/src/lib/i18n.ts (1 hunks)
  • apps/web/src/lib/imageBlobStore.ts (1 hunks)
  • apps/web/src/lib/utils.ts (2 hunks)
  • apps/web/src/pages/ConversationFlowPage.tsx (1 hunks)
  • apps/web/src/pages/FlowPage.tsx (7 hunks)
  • apps/web/src/pages/FlowPageV2.tsx (1 hunks)
  • apps/web/src/stores/conversationFlowStore.ts (1 hunks)
  • apps/web/src/stores/flowStore.ts (1 hunks)
  • package.json (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (8)
  • .github/workflows/deploy-cf-workers.yml
  • .github/workflows/preview.yml
  • apps/web/package.json
  • apps/api/package.json
  • apps/web/src/lib/i18n.ts
  • apps/api/src/config.ts
  • .github/workflows/deploy-cf-pages.yml
  • apps/api/src/app.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Biome for all linting and formatting operations (pnpm lint, pnpm format, pnpm check)

Files:

  • apps/web/src/components/flow/StorageLimitModal.tsx
  • apps/web/src/components/flow/MessageNode.tsx
  • apps/web/src/pages/FlowPage.tsx
  • apps/web/src/pages/FlowPageV2.tsx
  • apps/web/src/pages/ConversationFlowPage.tsx
  • apps/web/src/stores/conversationFlowStore.ts
  • apps/web/src/components/flow/ImageNode.tsx
  • apps/web/src/lib/imageBlobStore.ts
  • apps/web/src/components/flow/UserPromptNode.tsx
  • package.json
  • apps/web/src/components/ui/LanguageSwitcher.tsx
  • apps/web/src/components/flow/AIResultNode.tsx
  • apps/web/src/components/flow/ConfigNode.tsx
  • apps/web/src/stores/flowStore.ts
  • apps/web/src/components/flow/FlowInput.tsx
  • apps/web/src/components/flow/Lightbox.tsx
  • apps/web/src/lib/utils.ts
apps/web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/web/src/**/*.{ts,tsx}: Use @/ path alias for imports in frontend code (maps to src/)
Persist settings and API keys to localStorage with AES-256-GCM encryption (use src/lib/crypto.ts)
Persist Flow mode state using IndexedDB storage (via src/lib/flow-storage.ts) for nodes, edges, and images
Configure API URL via VITE_API_URL environment variable, defaulting to relative path for same-origin deployment

Files:

  • apps/web/src/components/flow/StorageLimitModal.tsx
  • apps/web/src/components/flow/MessageNode.tsx
  • apps/web/src/pages/FlowPage.tsx
  • apps/web/src/pages/FlowPageV2.tsx
  • apps/web/src/pages/ConversationFlowPage.tsx
  • apps/web/src/stores/conversationFlowStore.ts
  • apps/web/src/components/flow/ImageNode.tsx
  • apps/web/src/lib/imageBlobStore.ts
  • apps/web/src/components/flow/UserPromptNode.tsx
  • apps/web/src/components/ui/LanguageSwitcher.tsx
  • apps/web/src/components/flow/AIResultNode.tsx
  • apps/web/src/components/flow/ConfigNode.tsx
  • apps/web/src/stores/flowStore.ts
  • apps/web/src/components/flow/FlowInput.tsx
  • apps/web/src/components/flow/Lightbox.tsx
  • apps/web/src/lib/utils.ts
apps/web/src/**/*.{tsx,ts}

📄 CodeRabbit inference engine (CLAUDE.md)

Use React 19 with Vite, Tailwind CSS, and shadcn/ui components in the frontend (apps/web)

Files:

  • apps/web/src/components/flow/StorageLimitModal.tsx
  • apps/web/src/components/flow/MessageNode.tsx
  • apps/web/src/pages/FlowPage.tsx
  • apps/web/src/pages/FlowPageV2.tsx
  • apps/web/src/pages/ConversationFlowPage.tsx
  • apps/web/src/stores/conversationFlowStore.ts
  • apps/web/src/components/flow/ImageNode.tsx
  • apps/web/src/lib/imageBlobStore.ts
  • apps/web/src/components/flow/UserPromptNode.tsx
  • apps/web/src/components/ui/LanguageSwitcher.tsx
  • apps/web/src/components/flow/AIResultNode.tsx
  • apps/web/src/components/flow/ConfigNode.tsx
  • apps/web/src/stores/flowStore.ts
  • apps/web/src/components/flow/FlowInput.tsx
  • apps/web/src/components/flow/Lightbox.tsx
  • apps/web/src/lib/utils.ts
🧠 Learnings (5)
📓 Common learnings
Learnt from: CR
Repo: WuMingDao/zenith-image-generator PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-12T02:11:01.015Z
Learning: Applies to apps/web/src/**/*.{ts,tsx} : Persist Flow mode state using IndexedDB storage (via src/lib/flow-storage.ts) for nodes, edges, and images
📚 Learning: 2025-12-12T02:11:01.015Z
Learnt from: CR
Repo: WuMingDao/zenith-image-generator PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-12T02:11:01.015Z
Learning: Applies to apps/web/src/**/*.{ts,tsx} : Persist Flow mode state using IndexedDB storage (via src/lib/flow-storage.ts) for nodes, edges, and images

Applied to files:

  • apps/web/src/components/flow/StorageLimitModal.tsx
  • apps/web/src/components/flow/MessageNode.tsx
  • apps/web/src/pages/FlowPage.tsx
  • apps/web/src/pages/FlowPageV2.tsx
  • apps/web/src/pages/ConversationFlowPage.tsx
  • apps/web/src/stores/conversationFlowStore.ts
  • apps/web/src/components/flow/ImageNode.tsx
  • apps/web/src/lib/imageBlobStore.ts
  • apps/web/src/components/flow/AIResultNode.tsx
  • apps/web/src/components/flow/ConfigNode.tsx
  • apps/web/src/stores/flowStore.ts
  • apps/web/src/components/flow/FlowInput.tsx
  • apps/web/src/components/flow/Lightbox.tsx
📚 Learning: 2025-12-12T02:11:01.015Z
Learnt from: CR
Repo: WuMingDao/zenith-image-generator PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-12T02:11:01.015Z
Learning: Applies to **/*.{ts,tsx,js,jsx,json} : Use Biome for all linting and formatting operations (pnpm lint, pnpm format, pnpm check)

Applied to files:

  • apps/web/src/components/flow/MessageNode.tsx
  • apps/web/src/stores/conversationFlowStore.ts
  • apps/web/src/components/flow/ImageNode.tsx
  • apps/web/src/components/ui/LanguageSwitcher.tsx
  • apps/web/src/components/flow/ConfigNode.tsx
  • apps/web/src/components/flow/FlowInput.tsx
  • apps/web/src/components/flow/Lightbox.tsx
📚 Learning: 2025-12-12T02:11:01.015Z
Learnt from: CR
Repo: WuMingDao/zenith-image-generator PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-12T02:11:01.015Z
Learning: Applies to apps/web/src/**/*.{tsx,ts} : Use React 19 with Vite, Tailwind CSS, and shadcn/ui components in the frontend (apps/web)

Applied to files:

  • apps/web/src/components/ui/LanguageSwitcher.tsx
  • apps/web/src/components/flow/AIResultNode.tsx
  • apps/web/src/components/flow/FlowInput.tsx
  • apps/web/src/components/flow/Lightbox.tsx
  • apps/web/src/lib/utils.ts
📚 Learning: 2025-12-12T02:11:01.015Z
Learnt from: CR
Repo: WuMingDao/zenith-image-generator PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-12T02:11:01.015Z
Learning: Applies to apps/web/src/**/*.{ts,tsx} : Configure API URL via VITE_API_URL environment variable, defaulting to relative path for same-origin deployment

Applied to files:

  • apps/web/src/lib/utils.ts
🧬 Code graph analysis (12)
apps/web/src/components/flow/StorageLimitModal.tsx (1)
apps/web/src/lib/imageBlobStore.ts (1)
  • STORAGE_LIMITS (19-23)
apps/web/src/components/flow/MessageNode.tsx (1)
apps/web/src/stores/conversationFlowStore.ts (1)
  • MessageNodeData (12-21)
apps/web/src/pages/FlowPage.tsx (1)
apps/web/src/components/flow/FloatingInput.tsx (1)
  • FloatingInput (20-190)
apps/web/src/pages/ConversationFlowPage.tsx (2)
apps/web/src/components/flow/MessageNode.tsx (1)
  • MessageNode (147-147)
apps/web/src/stores/conversationFlowStore.ts (1)
  • useConversationFlowStore (107-364)
apps/web/src/stores/conversationFlowStore.ts (1)
apps/web/src/components/flow/layout.ts (1)
  • getLayoutedElements (7-34)
apps/web/src/components/flow/ImageNode.tsx (6)
apps/web/src/stores/flowStore.ts (2)
  • ImageData (36-48)
  • useFlowStore (160-440)
packages/shared/src/types/api.ts (1)
  • ImageDetails (30-49)
apps/web/src/lib/imageBlobStore.ts (3)
  • getBlob (164-183)
  • blobToObjectUrl (326-328)
  • urlToBlob (313-320)
apps/web/src/lib/constants.ts (2)
  • loadSettings (49-56)
  • getDefaultModel (70-73)
apps/web/src/lib/crypto.ts (1)
  • loadAllTokens (74-84)
apps/web/src/lib/utils.ts (1)
  • downloadImage (49-115)
apps/web/src/lib/imageBlobStore.ts (1)
apps/api/src/index.ts (1)
  • fetch (17-21)
apps/web/src/components/flow/AIResultNode.tsx (2)
apps/web/src/lib/constants.ts (2)
  • getModelsByProvider (20-20)
  • getDefaultModel (70-73)
packages/shared/src/constants/models.ts (1)
  • getModelsByProvider (151-153)
apps/web/src/components/flow/ConfigNode.tsx (1)
apps/web/src/stores/flowStore.ts (1)
  • ConfigData (16-25)
apps/web/src/stores/flowStore.ts (1)
apps/web/src/lib/imageBlobStore.ts (2)
  • deleteBlobs (201-215)
  • clearAllBlobs (300-308)
apps/web/src/components/flow/Lightbox.tsx (3)
apps/web/src/stores/flowStore.ts (1)
  • useFlowStore (160-440)
apps/web/src/lib/imageBlobStore.ts (3)
  • getBlob (164-183)
  • blobToObjectUrl (326-328)
  • blobToDataUrl (333-340)
apps/web/src/lib/utils.ts (1)
  • downloadImage (49-115)
apps/web/src/lib/utils.ts (1)
apps/api/src/index.ts (1)
  • fetch (17-21)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Workers Builds: zenith-image-generator-api
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (26)
apps/web/src/components/flow/AIResultNode.tsx (1)

118-125: LGTM on model validation logic.

The dynamic model validation correctly ensures the selected model is supported by the current provider, with a safe fallback to the provider's default model.

apps/web/src/components/flow/Lightbox.tsx (1)

21-71: LGTM on object URL lifecycle management.

The blob loading logic properly revokes previous object URLs before creating new ones (lines 27-31) and cleans up on unmount (lines 64-71), preventing memory leaks.

apps/web/src/lib/utils.ts (1)

9-37: LGTM on proxy URL utilities.

The helper functions correctly implement cross-origin detection and proxy URL construction, using VITE_API_URL as per coding guidelines.

Based on coding guidelines, Configure API URL via VITE_API_URL environment variable, defaulting to relative path for same-origin deployment.

package.json (1)

39-39: No action needed. Wrangler v4 is already in use with compatible configurations. The wrangler.toml files use v4-compatible settings (e.g., nodejs_compat instead of deprecated node_compat), CI workflows explicitly specify wranglerVersion: "4", and Node.js 20 exceeds v4's minimum requirement (18+). No deprecated or removed Wrangler features are present in the codebase.

apps/web/src/components/flow/ConfigNode.tsx (1)

1-9: LGTM! Clean component interface and imports.

The imports are properly organized with external libraries first, followed by internal imports using the @/ path alias as per coding guidelines. The ConfigNodeProps interface correctly extends NodeProps and specifies the data type.

apps/web/src/pages/FlowPageV2.tsx (2)

1-33: LGTM! Well-organized imports and node type registration.

Imports are properly structured with external libraries first, then internal modules using the @/ path alias. The nodeTypes object is correctly defined outside the component to prevent unnecessary re-renders.


91-106: LGTM! Token and settings management.

The token loading effect and model validation logic are correctly implemented. The model fallback to default when provider changes ensures valid state.

apps/web/src/components/flow/ImageNode.tsx (3)

1-26: LGTM! Imports and type definitions are well-structured.

Imports are properly organized. The ImageNodeProps and GenerateApiResponse interfaces provide clear typing for the component's data contract.


236-261: LGTM! Download handler with proper fallback.

The download logic correctly prioritizes local blob storage over URL-based downloads, with appropriate error handling and dynamic import for the utility function.


269-324: LGTM! Clean conditional rendering for node states.

The component handles loading, error, and success states cleanly. The biome-ignore comment appropriately explains the accessibility exception for the React Flow node interaction pattern.

apps/web/src/pages/ConversationFlowPage.tsx (1)

336-345: Mock AI generator is acceptable as placeholder.

The function is clearly marked as mock. Ensure this gets replaced with actual API integration before production use.

apps/web/src/pages/FlowPage.tsx (4)

45-61: LGTM! Path calculation helper function.

The calculatePathToNode function correctly builds the path from root to the target node by traversing edges backwards. The edge map construction and while loop are efficient.


81-103: LGTM! Well-implemented branching state with proper memoization.

The activePath and styledEdges computations are correctly memoized with appropriate dependencies. The edge styling logic correctly identifies edges on the active path.


253-255: LGTM! Branching logic correctly implemented.

The parent node selection logic correctly prioritizes selectedNodeId for branching, falling back to the last node. This enables the tree-branching workflow.


445-450: Verify type compatibility for selectedNodeId prop.

The state selectedNodeId is typed as string | null, but based on the relevant code snippets, FloatingInput expects selectedNodeId: string | undefined. While this often works in practice, consider using selectedNodeId ?? undefined for explicit type alignment.

       <FloatingInput
         onSubmit={addNode}
         providerLabel={`${provider} / ${model}`}
-        selectedNodeId={selectedNodeId}
+        selectedNodeId={selectedNodeId ?? undefined}
         onClearSelection={() => setSelectedNodeId(null)}
       />
apps/web/src/stores/flowStore.ts (6)

1-6: LGTM!

Imports are well-organized with proper use of type imports and the @/ path alias as per coding guidelines.


8-14: LGTM!

Layout constants are well-defined with as const for type safety and immutability.


110-158: LGTM!

The IndexedDB storage adapter is well-implemented with proper error handling, singleton pattern for the database instance, and correct async storage interface for Zustand persist.


378-412: LGTM!

The computed methods getAllNodes and getAllEdges are well-implemented, deriving renderable data from the store state. The preview node is correctly included when present with appropriate styling.


424-439: LGTM!

The persistence configuration correctly:

  • Uses custom IndexedDB storage per coding guidelines
  • Partializes state to only persist necessary fields (configNodes, imageNodes, nodeIdCounter)
  • Sets _hasHydrated flag after rehydration for components to know when data is ready

345-361: Fire-and-forget blob deletion could leave orphan state on failure.

deleteBlobs is called without awaiting, and the state is updated synchronously. If blob deletion fails, the image nodes are removed but blobs remain in IndexedDB as orphans.

Consider this acceptable if:

  1. Blob IDs are guaranteed unique (they are, via counter)
  2. Orphan cleanup is handled elsewhere (e.g., startup reconciliation)

Otherwise, consider awaiting or implementing a cleanup mechanism.

#!/bin/bash
# Check if there's any orphan blob cleanup logic elsewhere in the codebase
rg -n "orphan|reconcil|cleanup.*blob|stale.*blob" --type=ts
apps/web/src/lib/imageBlobStore.ts (2)

276-295: Potential TOCTOU in removeOldestBlob.

The function opens a readonly transaction to find the oldest blob, then calls deleteBlob in a separate transaction. Between these operations, another process could delete the same blob.

This is low-risk since:

  1. Double-delete is safe (delete on non-existent key is a no-op)
  2. This is only used internally for LRU eviction

No action needed if the above assumptions hold.


322-340: LGTM!

The blob utility functions are well-implemented:

  • blobToObjectUrl includes a reminder comment to revoke the URL
  • blobToDataUrl properly wraps FileReader in a Promise with error handling
apps/web/src/stores/conversationFlowStore.ts (3)

122-212: LGTM!

The addMessage action is well-implemented:

  • Creates the new node with proper data structure
  • Updates parent's childIds correctly
  • Creates edges when parent exists
  • Applies layout after changes
  • Updates active path and node/edge styling

224-297: LGTM!

The deleteNode action correctly handles:

  • Recursive descendant finding
  • Updating parent's childIds to remove deleted node
  • Filtering edges that reference deleted nodes
  • Re-applying layout
  • Updating active node if it was deleted (falls back to parent)
  • Recalculating active path

107-116: ConversationFlowPage is currently disabled and not integrated into the application.

The route for ConversationFlowPage is commented out in App.tsx, so this store is not actively used. If this feature is enabled in the future, persistence should be added using IndexedDB storage following the same pattern as flowStore.ts.

Comment on lines +67 to +73
<Handle
type="source"
position={Position.Bottom}
className={`!w-3 !h-3 !border-2 transition-colors ${
selected ? '!bg-orange-500 !border-orange-400' : '!bg-orange-500 !border-orange-400'
}`}
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove redundant ternary on source Handle.

The bottom Handle uses identical styling for both selected and non-selected states. If the orange styling should always apply, simplify to a single class string. Otherwise, update the non-selected branch to use different colors (e.g., zinc like the top Handle's non-selected state).

       <Handle
         type="source"
         position={Position.Bottom}
-        className={`!w-3 !h-3 !border-2 transition-colors ${
-          selected ? '!bg-orange-500 !border-orange-400' : '!bg-orange-500 !border-orange-400'
-        }`}
+        className="!w-3 !h-3 !border-2 transition-colors !bg-orange-500 !border-orange-400"
       />
📝 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
<Handle
type="source"
position={Position.Bottom}
className={`!w-3 !h-3 !border-2 transition-colors ${
selected ? '!bg-orange-500 !border-orange-400' : '!bg-orange-500 !border-orange-400'
}`}
/>
<Handle
type="source"
position={Position.Bottom}
className="!w-3 !h-3 !border-2 transition-colors !bg-orange-500 !border-orange-400"
/>
🤖 Prompt for AI Agents
In apps/web/src/components/flow/UserPromptNode.tsx around lines 67 to 73, the
source Handle uses a redundant ternary that applies identical orange styles for
both branches; replace the ternary with a single class string (e.g.,
className="!w-3 !h-3 !border-2 transition-colors !bg-orange-500
!border-orange-400") so the Handle always uses the orange styling, or if you
intended a different non-selected look, update the non-selected branch to match
the top Handle's zinc styling instead of duplicating the orange classes.

Comment on lines +225 to +251
const handleConfirmCleanup = useCallback(async () => {
if (!storageLimitState?.pendingBlob || !storageLimitState?.pendingImageId) {
clearStorageLimitState()
return
}

const { pendingBlob, pendingImageId } = storageLimitState

// Perform cleanup
await cleanupForNewBlob(pendingBlob.size)

// Store the pending blob
const blobId = await storeBlob(pendingImageId, pendingBlob)

// Update the image with the new blobId
const imageNode = imageNodes.find((n) => n.id === pendingImageId)
if (imageNode && blobId) {
updateImageGenerated(
pendingImageId,
imageNode.data.imageUrl || '',
imageNode.data.duration || '',
blobId
)
}

clearStorageLimitState()
}, [storageLimitState, imageNodes, updateImageGenerated, clearStorageLimitState])
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Handle potential stale closure and failed blob storage.

The imageNodes reference in the dependency array could be stale if the cleanup is triggered after new images are added. Also, if storeBlob returns null, the update proceeds without proper handling.

   const handleConfirmCleanup = useCallback(async () => {
     if (!storageLimitState?.pendingBlob || !storageLimitState?.pendingImageId) {
       clearStorageLimitState()
       return
     }

     const { pendingBlob, pendingImageId } = storageLimitState

     // Perform cleanup
     await cleanupForNewBlob(pendingBlob.size)

     // Store the pending blob
     const blobId = await storeBlob(pendingImageId, pendingBlob)
+    if (!blobId) {
+      console.warn('Failed to store blob after cleanup')
+      clearStorageLimitState()
+      return
+    }

-    // Update the image with the new blobId
-    const imageNode = imageNodes.find((n) => n.id === pendingImageId)
+    // Get fresh state to avoid stale closure
+    const currentImageNodes = useFlowStore.getState().imageNodes
+    const imageNode = currentImageNodes.find((n) => n.id === pendingImageId)
     if (imageNode && blobId) {
🤖 Prompt for AI Agents
In apps/web/src/pages/FlowPageV2.tsx around lines 225 to 251, the callback may
close over a stale imageNodes and proceeds when storeBlob returns null; update
it to read the latest image node at execution time (e.g., use a ref or retrieve
image node via a state getter/useCallback with imageNodes in scope) and wrap the
async work in try/catch/finally so cleanup always runs; specifically, after
awaiting storeBlob check for a falsy blobId and bail with an error/log (do not
call updateImageGenerated), and in finally call clearStorageLimitState so state
is cleared even on error.

@WuMingDao WuMingDao merged commit 0d5f8f6 into main Dec 13, 2025
16 of 18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api ci dependencies documentation Improvements or additions to documentation enhancement New feature or request shared testing web

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant