Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
✅ Deploy Preview for frolicking-blancmange-ef6637 canceled.
|
Deploying with
|
| 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 |
WalkthroughAdds 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
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)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Areas to focus during review:
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🧰 Additional context used📓 Path-based instructions (1)**/*.{ts,tsx,js,jsx,json}📄 CodeRabbit inference engine (CLAUDE.md)
Files:
🧠 Learnings (1)📓 Common learnings🧬 Code graph analysis (1)packages/shared/src/utils/validation.test.ts (1)
⏰ 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)
🔇 Additional comments (1)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 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 acrossgetConfig()andgetCorsOriginsFromBindings()+ fix Biome formatting.
Right now bindings-defaults omithttp://localhost:5174(and:4173) even thoughgetConfig()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
memouses shallow prop comparison. SinceConfigDatahas 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 undefinedonClearSelection.The clear button's
onClickdirectly referencesonClearSelection, 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
"图片生成模式", whileFlowInput.tsxusest('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:breakonly exits inner loop; outer loop continues unnecessarily.When a matching resolution is found,
breakexits only the innerjloop, but the outeriloop 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 forceen-UStimestamps.
Hardcoded strings (You,AI,Copy, etc.) andtoLocaleTimeString('en-US', ...)bypass the new i18n system.Also applies to: 67-90, 110-138
apps/web/src/pages/FlowPage.tsx (2)
46-65: Unused_nodesparameter incalculatePathToNode.The
_nodesparameter 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 indownloadImage.Line 98 creates an object URL for
img.srcthat is never revoked after the image loads. While this is pre-existing code, consider fixing it alongside the newconvertToPngBlobfunction 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
generateMockAIResponsefunction 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-inapplyNodeChangesfor better performance.The current implementation manually handles position changes. React Flow provides
applyNodeChangesutility 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
useTranslationfromreact-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.login production can clutter browser console. Consider usingconsole.debugor 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
getLayoutedElementsfromapps/web/src/components/flow/layout.tswith 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 emptyinitializeFlowfunction 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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis 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.tsapps/web/src/main.tsxapps/web/src/components/ui/LanguageSwitcher.tsxapps/web/src/components/flow/UserPromptNode.tsxapps/web/src/components/flow/Lightbox.tsxapps/web/src/components/flow/ImageNode.tsxapps/web/src/pages/FlowPageV2.tsxapps/web/package.jsonapps/web/src/components/flow/MessageNode.tsxpackage.jsonapps/web/src/locales/en.jsonpackages/shared/src/utils/validation.tsapps/web/src/stores/flowStore.tsapps/web/src/components/flow/AIResultNode.tsxapps/web/src/lib/utils.tsapps/web/src/components/flow/FlowInput.tsxapps/api/package.jsonapps/web/src/components/flow/StorageLimitModal.tsxapps/web/src/components/flow/ConfigNode.tsxapps/web/src/pages/ConversationFlowPage.tsxapps/web/src/lib/imageBlobStore.tsapps/web/src/stores/conversationFlowStore.tsapps/web/src/pages/FlowPage.tsxapps/api/src/config.tsapps/web/src/locales/zh.jsonapps/web/src/components/flow/FloatingInput.tsxapps/web/src/App.tsxapps/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.tsapps/web/src/main.tsxapps/web/src/components/ui/LanguageSwitcher.tsxapps/web/src/components/flow/UserPromptNode.tsxapps/web/src/components/flow/Lightbox.tsxapps/web/src/components/flow/ImageNode.tsxapps/web/src/pages/FlowPageV2.tsxapps/web/src/components/flow/MessageNode.tsxapps/web/src/stores/flowStore.tsapps/web/src/components/flow/AIResultNode.tsxapps/web/src/lib/utils.tsapps/web/src/components/flow/FlowInput.tsxapps/web/src/components/flow/StorageLimitModal.tsxapps/web/src/components/flow/ConfigNode.tsxapps/web/src/pages/ConversationFlowPage.tsxapps/web/src/lib/imageBlobStore.tsapps/web/src/stores/conversationFlowStore.tsapps/web/src/pages/FlowPage.tsxapps/web/src/components/flow/FloatingInput.tsxapps/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.tsapps/web/src/main.tsxapps/web/src/components/ui/LanguageSwitcher.tsxapps/web/src/components/flow/UserPromptNode.tsxapps/web/src/components/flow/Lightbox.tsxapps/web/src/components/flow/ImageNode.tsxapps/web/src/pages/FlowPageV2.tsxapps/web/src/components/flow/MessageNode.tsxapps/web/src/stores/flowStore.tsapps/web/src/components/flow/AIResultNode.tsxapps/web/src/lib/utils.tsapps/web/src/components/flow/FlowInput.tsxapps/web/src/components/flow/StorageLimitModal.tsxapps/web/src/components/flow/ConfigNode.tsxapps/web/src/pages/ConversationFlowPage.tsxapps/web/src/lib/imageBlobStore.tsapps/web/src/stores/conversationFlowStore.tsapps/web/src/pages/FlowPage.tsxapps/web/src/components/flow/FloatingInput.tsxapps/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.tsapps/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.ymlapps/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.tsapps/web/src/main.tsxapps/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.tsxapps/web/package.jsonapps/web/src/components/flow/AIResultNode.tsxapps/web/src/lib/utils.tsapps/api/package.jsonapps/web/src/components/flow/FloatingInput.tsxapps/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.tsxapps/web/src/components/flow/ImageNode.tsxapps/web/src/pages/FlowPageV2.tsxapps/web/src/components/flow/MessageNode.tsxapps/web/src/stores/flowStore.tsapps/web/src/components/flow/AIResultNode.tsxapps/web/src/components/flow/FlowInput.tsxapps/web/src/components/flow/StorageLimitModal.tsxapps/web/src/components/flow/ConfigNode.tsxapps/web/src/pages/ConversationFlowPage.tsxapps/web/src/lib/imageBlobStore.tsapps/web/src/stores/conversationFlowStore.tsapps/web/src/pages/FlowPage.tsxREADME.mdapps/web/src/components/flow/FloatingInput.tsxapps/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.tsapps/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.jsonapps/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
packageManagerandwranglerVersionconfiguration 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 forapps/api/package.jsonto 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 withen.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
cancelledis true (e.g., component unmounted or image changed during fetch), the object URL created at line 47 won't be stored inobjectUrlRefand 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
cancelledbefore 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
downloadImagesAsZipfunction 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:imageNodemay have stale data after cleanup.When
handleConfirmCleanupruns, it fetchesimageNodefromimageNodesat line 244, butcleanupForNewBlobat line 238 may have already removed some images from storage. TheimageNode.data.imageUrlcould reference a deleted blob.Additionally, if the cleanup removes the pending image's node from the store,
imageNodewill 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
handleDownloadAllfunction 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
generatingRefto 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
confirmConfigaction 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
addMessageaction 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.
| const defaultOrigins = ['http://localhost:5173', 'http://localhost:5174', 'http://localhost:3000'] | ||
| const origins = config.corsOrigins || defaultOrigins | ||
|
|
There was a problem hiding this comment.
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.
| // 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) | ||
| } | ||
| }) |
There was a problem hiding this comment.
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) citeturn0search0 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.
| // 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.
| // import ConversationFlowPage from './pages/ConversationFlowPage' | ||
| // import FlowPage from './pages/FlowPage' | ||
| import FlowPageV2 from './pages/FlowPageV2' | ||
| import ImageGenerator from './pages/ImageGenerator' |
There was a problem hiding this comment.
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.
| <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' | ||
| }`} | ||
| /> |
There was a problem hiding this comment.
Fix formatting to resolve CI failure, and review Handle styling.
-
The pipeline indicates Biome formatting issues around lines 182-186. Run
pnpm formatto fix. -
The top Handle styling (lines 193-195) uses identical styles for both
selectedand 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.
| 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() | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| // 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) | ||
| }) | ||
| } |
There was a problem hiding this comment.
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.
| // 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.
| function ConversationFlowCanvas() { | ||
| const { setCenter } = useReactFlow() | ||
|
|
||
| const { | ||
| nodes, | ||
| edges, | ||
| activeNodeId, | ||
| addMessage, | ||
| setActiveNode, | ||
| clearFlow, | ||
| setNodes, | ||
| } = useConversationFlowStore() | ||
|
|
There was a problem hiding this comment.
🧩 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.tsRepository: 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.tsRepository: 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 -20Repository: 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.tsRepository: 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.tsRepository: 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
persistmiddleware 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)
- 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>
Deploying zenith-image-generator with
|
| 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 |
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>
There was a problem hiding this comment.
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: Handlereason: nullexplicitly.When
reasonisnull, 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 inconvertToPngBlob.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,
conversationFlowStorelacks 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 --fixorpnpm check --fixto 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 formatto 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
trueeven 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.okbefore callingresponse.blob().apps/web/src/stores/conversationFlowStore.ts (1)
91-105: Formatting issue flagged by CI.This was flagged in a previous review. Run
pnpm formatto 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-nonecorrectly prevents interaction conflicts, but screen reader users won't receive this editing hint. Consider adding anaria-labelto 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
onNodeDragandonNodeDragStopperform the same operation. SinceonNodeDragStopfires when dragging ends, you could removeonNodeDragto reduce re-renders during drag operations, or keep onlyonNodeDragStopfor 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
onClickon the backdrop div to close would improve UX. If intentional, consideraria-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_CONFIGSis 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
setNodeson every position change during drag, which could be expensive. Consider debouncing or handling onlydragging: falsechanges. 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: MoveloadInputConfig()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.
cycleResolutionhardcodes% 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
returnafter 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
storeBlobconcern,deleteBlobperforms 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.
clearAllBlobsperforms 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: DuplicategetLayoutedElementsimplementation.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_WIDTHandNODE_HEIGHTconstants, or you can keep local constants if dimensions differ between flow types.
356-362:initializeFlowis 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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis 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.tsxapps/web/src/components/flow/MessageNode.tsxapps/web/src/pages/FlowPage.tsxapps/web/src/pages/FlowPageV2.tsxapps/web/src/pages/ConversationFlowPage.tsxapps/web/src/stores/conversationFlowStore.tsapps/web/src/components/flow/ImageNode.tsxapps/web/src/lib/imageBlobStore.tsapps/web/src/components/flow/UserPromptNode.tsxpackage.jsonapps/web/src/components/ui/LanguageSwitcher.tsxapps/web/src/components/flow/AIResultNode.tsxapps/web/src/components/flow/ConfigNode.tsxapps/web/src/stores/flowStore.tsapps/web/src/components/flow/FlowInput.tsxapps/web/src/components/flow/Lightbox.tsxapps/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.tsxapps/web/src/components/flow/MessageNode.tsxapps/web/src/pages/FlowPage.tsxapps/web/src/pages/FlowPageV2.tsxapps/web/src/pages/ConversationFlowPage.tsxapps/web/src/stores/conversationFlowStore.tsapps/web/src/components/flow/ImageNode.tsxapps/web/src/lib/imageBlobStore.tsapps/web/src/components/flow/UserPromptNode.tsxapps/web/src/components/ui/LanguageSwitcher.tsxapps/web/src/components/flow/AIResultNode.tsxapps/web/src/components/flow/ConfigNode.tsxapps/web/src/stores/flowStore.tsapps/web/src/components/flow/FlowInput.tsxapps/web/src/components/flow/Lightbox.tsxapps/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.tsxapps/web/src/components/flow/MessageNode.tsxapps/web/src/pages/FlowPage.tsxapps/web/src/pages/FlowPageV2.tsxapps/web/src/pages/ConversationFlowPage.tsxapps/web/src/stores/conversationFlowStore.tsapps/web/src/components/flow/ImageNode.tsxapps/web/src/lib/imageBlobStore.tsapps/web/src/components/flow/UserPromptNode.tsxapps/web/src/components/ui/LanguageSwitcher.tsxapps/web/src/components/flow/AIResultNode.tsxapps/web/src/components/flow/ConfigNode.tsxapps/web/src/stores/flowStore.tsapps/web/src/components/flow/FlowInput.tsxapps/web/src/components/flow/Lightbox.tsxapps/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.tsxapps/web/src/components/flow/MessageNode.tsxapps/web/src/pages/FlowPage.tsxapps/web/src/pages/FlowPageV2.tsxapps/web/src/pages/ConversationFlowPage.tsxapps/web/src/stores/conversationFlowStore.tsapps/web/src/components/flow/ImageNode.tsxapps/web/src/lib/imageBlobStore.tsapps/web/src/components/flow/AIResultNode.tsxapps/web/src/components/flow/ConfigNode.tsxapps/web/src/stores/flowStore.tsapps/web/src/components/flow/FlowInput.tsxapps/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.tsxapps/web/src/stores/conversationFlowStore.tsapps/web/src/components/flow/ImageNode.tsxapps/web/src/components/ui/LanguageSwitcher.tsxapps/web/src/components/flow/ConfigNode.tsxapps/web/src/components/flow/FlowInput.tsxapps/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.tsxapps/web/src/components/flow/AIResultNode.tsxapps/web/src/components/flow/FlowInput.tsxapps/web/src/components/flow/Lightbox.tsxapps/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. Thewrangler.tomlfiles use v4-compatible settings (e.g.,nodejs_compatinstead of deprecatednode_compat), CI workflows explicitly specifywranglerVersion: "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. TheConfigNodePropsinterface correctly extendsNodePropsand specifies thedatatype.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. ThenodeTypesobject 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
ImageNodePropsandGenerateApiResponseinterfaces 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
calculatePathToNodefunction 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
activePathandstyledEdgescomputations 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
selectedNodeIdfor branching, falling back to the last node. This enables the tree-branching workflow.
445-450: Verify type compatibility forselectedNodeIdprop.The state
selectedNodeIdis typed asstring | null, but based on the relevant code snippets,FloatingInputexpectsselectedNodeId: string | undefined. While this often works in practice, consider usingselectedNodeId ?? undefinedfor 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 constfor 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
getAllNodesandgetAllEdgesare 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
_hasHydratedflag after rehydration for components to know when data is ready
345-361: Fire-and-forget blob deletion could leave orphan state on failure.
deleteBlobsis 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:
- Blob IDs are guaranteed unique (they are, via counter)
- 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=tsapps/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
deleteBlobin a separate transaction. Between these operations, another process could delete the same blob.This is low-risk since:
- Double-delete is safe (delete on non-existent key is a no-op)
- 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:
blobToObjectUrlincludes a reminder comment to revoke the URLblobToDataUrlproperly wraps FileReader in a Promise with error handlingapps/web/src/stores/conversationFlowStore.ts (3)
122-212: LGTM!The
addMessageaction is well-implemented:
- Creates the new node with proper data structure
- Updates parent's
childIdscorrectly- Creates edges when parent exists
- Applies layout after changes
- Updates active path and node/edge styling
224-297: LGTM!The
deleteNodeaction correctly handles:
- Recursive descendant finding
- Updating parent's
childIdsto 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.
| <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' | ||
| }`} | ||
| /> |
There was a problem hiding this comment.
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.
| <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.
| 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]) |
There was a problem hiding this comment.
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.
Summary
Changes
Flow Mode V2
Storage Architecture
zenith-image-blobs)lastAccessedAttimestampsStorage Limits
Security & CI
Test plan
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Improvements
Chores
✏️ Tip: You can customize this high-level summary in your review settings.