[pro-web] feat: implement logo generation wizard with multi-step creation flow#601
[pro-web] feat: implement logo generation wizard with multi-step creation flow#601derianrddev wants to merge 35 commits intodevelopfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
1 Skipped Deployment
|
Reviewer's GuideImplements a new multi-step Logo Generation wizard integrated with the existing Brand Kit flow, adds a full wordmark/logo-typography system (presets, casing, weights, auto-scaling preview), extends workspace media state/actions for logo data, and introduces precomputed Google Fonts and reference-asset helpers for AI logo generation. Sequence diagram for Brand Kit checkpoint to Logo Generation wizard transitionsequenceDiagram
actor User
participant BrandKitDialog
participant BrandKitWizard
participant CheckpointStep
participant WorkspaceMedia
participant LogoGenerationWizard
User->>BrandKitDialog: open Brand_Kit_dialog
BrandKitDialog->>BrandKitWizard: render view brandKit
User->>BrandKitWizard: complete previous_steps
BrandKitWizard->>CheckpointStep: render CheckpointStep
User->>CheckpointStep: click Generate_Logo
CheckpointStep->>WorkspaceMedia: buildBrandKitData
alt activeOrganization_exists
CheckpointStep->>WorkspaceMedia: saveBrandKit
WorkspaceMedia-->>CheckpointStep: save_success
CheckpointStep->>WorkspaceMedia: clearBrandKitStep
else no_activeOrganization
Note over CheckpointStep,WorkspaceMedia: Skip_saving_brand_kit
end
CheckpointStep->>BrandKitWizard: onGenerateLogo_callback
BrandKitWizard->>BrandKitDialog: onGenerateLogo
BrandKitDialog->>BrandKitDialog: setCurrentView logoGeneration
BrandKitDialog->>LogoGenerationWizard: render LogoGenerationWizard
LogoGenerationWizard->>LogoGenerationWizard: initialize steps Logo_Style,Direction,Refine,Confirm
Sequence diagram for Refine Logo step AI generation with reference assetssequenceDiagram
actor User
participant RefineLogoStep
participant WorkspaceMedia
participant LogoPromptHelper as logo-prompt
participant LogoRefAssetsHelper as logo-reference-assets
participant ImageGen as useImageGeneration
RefineLogoStep->>WorkspaceMedia: get selectedLogoStyle
RefineLogoStep->>WorkspaceMedia: get selectedSymbolStyle
RefineLogoStep->>WorkspaceMedia: get selectedWordmarkStyle
RefineLogoStep->>WorkspaceMedia: get wordmarkName
RefineLogoStep->>WorkspaceMedia: get selectedVibes
RefineLogoStep->>WorkspaceMedia: get selectedPalette
RefineLogoStep->>WorkspaceMedia: get selectedFonts
RefineLogoStep->>LogoRefAssetsHelper: preloadLogoReferenceAssets selectedFonts,selectedPalette,selectedLogoStyle,selectedWordmarkStyle,selectedSymbolStyle
LogoRefAssetsHelper-->>RefineLogoStep: logoReferenceAssets[4]
RefineLogoStep->>WorkspaceMedia: setLogoReferenceAssets logoReferenceAssets
RefineLogoStep->>LogoPromptHelper: buildLogoPrompt selectedLogoStyle,selectedSymbolStyle,selectedWordmarkStyle,wordmarkName,selectedVibes,selectedPalette,selectedFonts
LogoPromptHelper-->>RefineLogoStep: generatedPrompt
RefineLogoStep->>RefineLogoStep: set prompt_if_not_dirty
User->>RefineLogoStep: edit_prompt_optional
User->>RefineLogoStep: click Generate_logo
RefineLogoStep->>LogoRefAssetsHelper: getLogoReferenceChecksums selectedFonts,selectedPalette,selectedLogoStyle,selectedWordmarkStyle,selectedSymbolStyle
LogoRefAssetsHelper-->>RefineLogoStep: referenceImageChecksums[]
RefineLogoStep->>ImageGen: generateImage prompt,gemini-3-pro-image-preview,referenceImageChecksums,aspectRatio_1_1
ImageGen-->>RefineLogoStep: generatedImage_or_error
RefineLogoStep->>WorkspaceMedia: handlePromptSubmit prompt
RefineLogoStep-->>User: show generated_logo_preview_or_error_state
Updated class diagram for workspace media state and brand kit logo typesclassDiagram
class WorkspaceMediaProvider {
+generatedImage
+selectedVibes
+selectedFonts
+selectedPalette
+brandKitCurrentStep
+isSavingBrandKit
+selectedLogoStyle LogoStyle
+selectedSymbolStyle SymbolStyle
+selectedWordmarkStyle WordmarkStyle
+wordmarkName string
+logoReferenceAssets LogoReferenceAsset[]
+wordmarkPreset WordmarkStyle
+wordmarkCasing WordmarkCasing
+wordmarkTracking number
+wordmarkFontWeight WordmarkFontWeight
+setSelectedLogoStyle(style)
+setSelectedSymbolStyle(style)
+setSelectedWordmarkStyle(style)
+setWordmarkName(name)
+setLogoReferenceAssets(assets)
+addLogoReferenceAsset(asset,index)
+removeLogoReferenceAsset(index)
+setWordmarkPreset(preset)
+setWordmarkCasing(casing)
+setWordmarkTracking(tracking)
+setWordmarkFontWeight(weight)
}
class LogoStyle {
<<enumeration>>
+wordmark
+symbol
}
class LogoReferenceAsset {
+type
+previewUrl string
+label string
+metadata Record
}
class SymbolStyle {
<<enumeration>>
+monogram
+geometric-badge
+soft-blob
+sharp-crest
+abstract-symbol
+literal-pictogram
+negative-space
+line-grid
+emblem-seal
+motion-arrow
+mascot-head
+pattern-tile
}
class WordmarkStyle {
<<enumeration>>
+all-caps-tight
+all-caps-wide
+title-case-neutral
+lowercase-soft
+heavy-weight
+light-weight
+condensed
+extended-wide
+small-caps
+custom-letter-tweak
+accent-touch
+color-system-set
}
class WordmarkCasing {
<<enumeration>>
+all-caps
+title-case
+lowercase
+small-caps
}
class WordmarkFontWeight {
<<enumeration>>
+extra_light
+light
+regular
+medium
+semibold
+bold
+black
}
class WordmarkPreset {
+id WordmarkStyle
+title string
+subtitle string
+casingMode
+fixedCasing WordmarkCasing
+trackingMin number
+trackingMax number
+defaultTracking number
+allowedWeights WordmarkFontWeight[]
+defaultWeight WordmarkFontWeight
+notes string
}
class CasingOption {
+id WordmarkCasing
+label string
+icon LucideIcon
}
WorkspaceMediaProvider "1" --> "*" LogoReferenceAsset : manages
WorkspaceMediaProvider --> LogoStyle
WorkspaceMediaProvider --> SymbolStyle
WorkspaceMediaProvider --> WordmarkStyle
WorkspaceMediaProvider --> WordmarkCasing
WorkspaceMediaProvider --> WordmarkFontWeight
WordmarkPreset --> WordmarkStyle
WordmarkPreset --> WordmarkCasing
WordmarkPreset --> WordmarkFontWeight
CasingOption --> WordmarkCasing
Flow diagram for logo reference assets and AI generationflowchart LR
subgraph Brand_Kit_state
A_selectedFonts["Selected fonts in WorkspaceMedia"]
B_selectedPalette["Selected palette in WorkspaceMedia"]
C_logoStyle["selectedLogoStyle"]
D_wordmarkStyle["selectedWordmarkStyle"]
E_symbolStyle["selectedSymbolStyle"]
end
subgraph Refine_Logo_step
F_preload["preloadLogoReferenceAssets"]
G_assets["logoReferenceAssets[4]"]
H_slot["LogoReferenceSlot components"]
I_checksums["getLogoReferenceChecksums"]
end
subgraph Image_generation
J_generate["useImageGeneration.generateImage"]
K_model["Gemini image model"]
end
A_selectedFonts --> F_preload
B_selectedPalette --> F_preload
C_logoStyle --> F_preload
D_wordmarkStyle --> F_preload
E_symbolStyle --> F_preload
F_preload --> G_assets
G_assets --> H_slot
H_slot --> I_checksums
A_selectedFonts --> I_checksums
B_selectedPalette --> I_checksums
C_logoStyle --> I_checksums
D_wordmarkStyle --> I_checksums
E_symbolStyle --> I_checksums
I_checksums --> J_generate
J_generate --> K_model
subgraph Checksum_lookup
L_fonts["BRAND_KIT_CHECKSUMS.fonts"]
M_palettes["BRAND_KIT_CHECKSUMS.palettes"]
N_wordmark["BRAND_KIT_CHECKSUMS.wordmark"]
O_symbol["BRAND_KIT_CHECKSUMS.symbol"]
end
I_checksums --> L_fonts
I_checksums --> M_palettes
I_checksums --> N_wordmark
I_checksums --> O_symbol
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a full brand‑kit and multi‑scope logo generation system: new UI wizards and steps, per‑scope image generation, GCS upload/deduplication for logo assets, checksum-backed reference image mapping, many brand‑kit constants/types, image/base64/ZIP helpers, and refactors to media/workspace hooks and components. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant API as Org API
participant Uploader as uploadLogoAssetToGCS
participant GCS as GoogleCloudStorage
participant GraphQL as GraphQL
participant DB as OrgDataStore
Client->>API: PATCH /api/organizations/:id (brandKit + logoAssets)
API->>API: validate & merge incoming brandKit
API->>Uploader: uploadLogoAssetToGCS(orgId, type, base64)
Uploader->>GCS: compute checksum, check object existence
alt object missing
Uploader->>GCS: upload object
end
GCS-->>Uploader: bucketKey + signedUrl
Uploader-->>API: LogoAsset metadata (url, checksum, bucketKey)
API->>GraphQL: mutate organization { brandKit: updatedBrandKit }
GraphQL->>DB: persist updatedBrandKit
DB-->>GraphQL: mutation result
GraphQL-->>API: updated organization
API-->>Client: 200 OK with transformed org data
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
10b5242 to
363afe8
Compare
There was a problem hiding this comment.
Sorry @derianrddev, your pull request is larger than the review limit of 150000 diff characters
There was a problem hiding this comment.
Actionable comments posted: 11
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
apps/pro-web/components/routes/workspace/media-tab/wizards/media-wizards.tsx (1)
78-87:⚠️ Potential issue | 🟡 MinorLibrary image selection is a no-op — the selected image is never added.
The
onImageSelectcallback checks reference image count but never callsaddReferenceImage. The dialog closes without any visible feedback to the user that nothing happened. Consider at minimum showing a toast/warning so users aren't confused.apps/pro-web/components/routes/pro/chat-panel-pro.tsx (1)
13-37:⚠️ Potential issue | 🟡 MinorRemove unused props from
ChatPanelProPropsinterface and eliminate unused destructuring.The interface declares 10 props that are never used:
onConvertToDocument(destructured but never referenced), plus the dialog/conversion props (convertDialogOpen,setConvertDialogOpen,selectedMessageId,convertedText,setConvertedText,targetProject,setTargetProject,targetDocument,setTargetDocument) that are neither destructured nor forwarded to children. These appear to be remnants from the previous architecture before logic was moved toChatPanelProControls. Remove them to avoid misleading future consumers about the component's actual API.apps/pro-web/lib/hooks/use-image-generation.tsx (1)
67-72:⚠️ Potential issue | 🟠 MajorNo guard against concurrent
generateImagecalls.
isLoadingis set inside the try-block but never checked at entry. If the caller invokesgenerateImagetwice (e.g. double-click), two parallel fetches run, competing for the same state. Add an early return when already loading.Proposed guard
const generateImage = useCallback( async ( prompt: string, modelId: ImageModel, options?: GenerateImageOptions, ) => { + if (isLoading) return + const { previousImageData,
🤖 Fix all issues with AI agents
In `@apps/pro-web/app/api/organizations/`[id]/route.ts:
- Around line 111-142: The logoAssets validation lacks checks that the symbol
and/or wordmark include a non-empty base64 string, causing decodeBase64ToBuffer
(called by uploadLogoAssetToGCS) to receive undefined and throw; update the
validation in the handler that inspects body.logoAssets to verify that if
body.logoAssets.symbol exists then body.logoAssets.symbol.base64 is a non-empty
string, and likewise if body.logoAssets.wordmark exists then
body.logoAssets.wordmark.base64 is a non-empty string (in addition to the
existing wordmark.name check), returning a 422 JSON error with a clear message
when base64 is missing or invalid.
In `@apps/pro-web/app/globals.css`:
- Around line 1109-1114: The CSS rule targets a custom .scrollarea-viewport
class but only colors-step.tsx and wordmark-style-step.tsx add that class, so
other ScrollArea usages (symbol-style-step, refine-logo-step, vibe-step,
fonts-step, checkpoint-step, logo-confirm-step) miss the overflow fix; either
add the scrollarea-viewport class to the ScrollArea/ScrollAreaViewport instance
in each of those components (look for usages of ScrollArea, ScrollArea.Viewport
or ScrollAreaViewport in those files) or move the selector to target Radix's
attribute directly (e.g., replace .scrollarea-viewport
[data-radix-scroll-area-viewport] with [data-radix-scroll-area-viewport] > div)
in globals.css so all ScrollArea components receive the fix without per-file
changes.
In `@apps/pro-web/components/routes/chat/prompt-form/index.tsx`:
- Around line 518-537: The code clears the user's input immediately via
setInput('') before building and submitting the wordmark prompt, which loses the
value if buildRefineLogoPrompt returns falsy or if handleWordmarkPromptSubmit
fails; change the flow in the block guarded by isWordmarkScope so you stash
trimmedInput into a local variable (trimmedInput already exists), defer calling
setInput('') until after refinePrompt is confirmed truthy and after await
handleWordmarkPromptSubmit(refinePrompt) resolves successfully, and in
error/falsy cases leave the input intact (or restore it from the stashed
trimmedInput) so the user can recover their text; update logic around
buildRefineLogoPrompt and handleWordmarkPromptSubmit to only clear input
post-success and to restore input on failure.
In `@apps/pro-web/components/routes/pro/chat-panel-pro-controls.tsx`:
- Around line 122-131: The current useMemo for chatId mistakenly groups the OR
with the ternary, causing params.threadId to be ignored; update the chatId
computation in the useMemo so it implements the three-way fallback explicitly:
return params.threadId if truthy, else if mbChatState.isNewChat return
mbChatState.newChatThreadId, otherwise return activeThread?.threadId; locate the
chatId useMemo and replace the single expression using params.threadId ||
mbChatState.isNewChat ? ... with an explicit conditional sequence (checking
params.threadId first, then mbChatState.isNewChat, then activeThread?.threadId)
to preserve the route param.
In
`@apps/pro-web/components/routes/workspace/media-tab/brand-kit/logo-generator/logo-generator-wizard.tsx`:
- Around line 217-281: The effect starts async loads with Promise.allSettled but
has no cleanup, so stale callbacks can mutate refs and call
getGenerationInstance().addInitialImage after unmount or deps change; fix by
adding a per-run abort token (e.g., const abort = { cancelled: false } or an
AbortController) scoped to the useEffect run, check abort before mutating
isLoadingLogosRef, loadedLogosRef, and before calling instance.addInitialImage
inside loadLogo, and in the effect cleanup set abort.cancelled = true (or call
controller.abort()) so pending promises short-circuit; also ensure you only flip
isLoadingLogosRef.current = false if the run is not aborted (use the same token)
to avoid blocking subsequent runs — update symbols: useEffect,
isLoadingLogosRef, loadedLogosRef, loadLogo, getGenerationInstance,
addInitialImage, and the Promise.allSettled finally handler accordingly.
In
`@apps/pro-web/components/routes/workspace/media-tab/brand-kit/logo-generator/steps/logo-confirm-step.tsx`:
- Around line 32-84: The handleSave function currently closes the wizard
silently on three failure paths (no org found from initialState.organizationData
by activeOrganization, no images from symbolGen/wordmarkGen, and saveLogoAssets
throwing) so update handleSave to display a user-facing toast/notification for
each case before (or instead of) calling close(); specifically, when currentOrg
is missing show a "Organization not found" toast referencing
initialState.organizationData and activeOrganization, when both
symbolGen.generatedImage and wordmarkGen.generatedImage are absent show a
"Please generate at least one image" toast, and in the catch block show a
failure toast that includes the caught error message from saveLogoAssets; keep
setIsSaving(false) in the finally block and still call next() on success.
In
`@apps/pro-web/components/routes/workspace/media-tab/brand-kit/logo-generator/steps/refine-logo-step.tsx`:
- Around line 91-146: The effect can loop indefinitely because the catch resets
hasGeneratedRef.current to false and isGeneratingImage will toggle, retriggering
the effect; add a retry limit: introduce a maxRetries constant and a retry
counter ref (e.g., retryCountRef) and in the catch increment
retryCountRef.current and only reset hasGeneratedRef.current to false if
retryCountRef.current < maxRetries; once maxRetries is reached, leave
hasGeneratedRef.current true (or set a failed flag ref) and log the permanent
failure so the effect won't reattempt; update references to hasGeneratedRef,
retryCountRef, isGeneratingImage, generatedImage, handleWordmarkPromptSubmit and
scope in the logic so the effect stops retrying after the max attempts.
In `@apps/pro-web/lib/constants/brand-kit/google-fonts.ts`:
- Around line 77-149: The FONT_WEIGHTS_BY_FAMILY entry for "Fraunces" includes
'200' but the Fraunces font instantiation's weight array is missing '200',
causing the UI to list an unavailable weight; update the Fraunces font
instantiation weight array to include '200' (making it
['200','300','400','500','600','700','900']) so it matches
FONT_WEIGHTS_BY_FAMILY, or alternatively remove '200' from
FONT_WEIGHTS_BY_FAMILY—ensure both the FONT_WEIGHTS_BY_FAMILY "Fraunces" entry
and the Fraunces font instantiation weight list are identical.
In `@apps/pro-web/lib/contexts/chat-panel-context.tsx`:
- Around line 26-37: ChatPanelProvider is passing a new inline object to
ChatPanelContext.Provider every render which forces all consumers to re-render;
wrap the value object ({ chatbot, isAtBottom, scrollToBottom }) in React.useMemo
inside the ChatPanelProvider function and pass that memoized value to
ChatPanelContext.Provider, using [chatbot, isAtBottom, scrollToBottom] as the
dependency array so the object is only recreated when one of those values
actually changes.
In `@apps/pro-web/lib/helpers/color-utils.ts`:
- Around line 105-124: gentlyAdjustForAA currently always darkens the text color
toward black, which worsens contrast on dark backgrounds; change it to first
decide direction by comparing background and text luminance (or contrast with
black vs white) and then either darken (as now) or lighten the text toward white
by incrementing r,g,b up to the same loop/step limits, clamping 0–255 and
recomputing hex and contrastRatio each iteration; update the loop that
manipulates r,g,b (and the hex construction) to add when lightening and subtract
when darkening, and return adjusted: true when a successful change reaches
contrast >= 4.5.
In `@apps/pro-web/lib/hooks/use-image-generation.tsx`:
- Around line 164-172: The current update uses setHistory((prev) => { ... cursor
... }) while setCursor uses a functional updater, causing stale closure of
cursor when generateImage is called rapidly; fix by making history+cursor update
atomic—either use a ref (e.g., cursorRef) read/updated inside your generateImage
flow so setHistory always slices using the up-to-date cursorRef before pushing
imageObject and then incrementing cursorRef and calling setCursor, or replace
history and cursor state with a single useReducer that handles the "add image"
action (updating both history and cursor together) so setHistory and setCursor
are not out of sync.
🟡 Minor comments (20)
apps/pro-web/lib/helpers/ai-image.ts-224-246 (1)
224-246:⚠️ Potential issue | 🟡 MinorSilent no-op on empty files array may confuse callers.
When all files are filtered out (line 231-233), the function silently returns without downloading anything. Callers (e.g., the "Download All" button in
LogoConfirmStep) would have no indication that the operation was a no-op. Consider either throwing an error or returning a boolean so callers can show appropriate feedback.💡 Option: throw on empty input
if (validFiles.length === 0) { - return + throw new Error('No valid files to download') }apps/pro-web/lib/helpers/color-utils.ts-20-28 (1)
20-28:⚠️ Potential issue | 🟡 Minor
hexToRgbdoesn't validate input — may produceNaNon malformed hex strings.If
hexis empty, has fewer than 6 characters, or contains non-hex characters,Number.parseIntreturnsNaNand the bit-shifting will propagate it. Consider adding a guard or validation.apps/pro-web/components/routes/workspace/media-tab/brand-kit/logo-generator/steps/logo-confirm-step.tsx-87-108 (1)
87-108:⚠️ Potential issue | 🟡 Minor
handleDownloadAllhas no error handling.
imageHelpers.downloadZipcan throw (e.g., ZIP generation failure), but this call is unguarded. Wrap in try/catch with user feedback.Proposed fix
const handleDownloadAll = async () => { const symbolImage = symbolGen.generatedImage?.base64 const wordmarkImage = wordmarkGen.generatedImage?.base64 if (!symbolImage && !wordmarkImage) { return } const files: Array<{ name: string; base64: string }> = [] if (symbolImage) { files.push({ name: 'logo-symbol.png', base64: symbolImage }) } if (wordmarkImage) { files.push({ name: 'wordmark.png', base64: wordmarkImage }) } - await imageHelpers.downloadZip(files, 'logo-assets.zip') + try { + await imageHelpers.downloadZip(files, 'logo-assets.zip') + } catch (error) { + console.error('Failed to download logo assets:', error) + } }apps/pro-web/lib/helpers/workspace/media-tab/logo-prompt.ts-91-98 (1)
91-98:⚠️ Potential issue | 🟡 Minor
'small-caps'casing variant is silently passed through without a human-friendly label.
WordmarkCasingincludes'small-caps'but thecasingMaponly covers three of the four variants. The|| wordmarkCasingfallback will emit the raw slugsmall-capsinto the prompt, unlike the other variants which get clean labels. Either add the mapping or leave an explicit comment that the raw value is intentional.Proposed fix
if (wordmarkCasing) { const casingMap: Record<string, string> = { 'all-caps': 'ALL CAPS', 'title-case': 'Title Case', lowercase: 'all lowercase', + 'small-caps': 'Small Caps', } prompt += `- Casing: ${casingMap[wordmarkCasing] || wordmarkCasing}\n` }apps/pro-web/lib/hooks/use-workspace-media.tsx-674-675 (1)
674-675:⚠️ Potential issue | 🟡 MinorRemove debug
console.logstatements before merging.These log the full server response and extracted brand kit data, which could leak sensitive data in production and clutter the console.
Proposed fix
- console.log('Server response after saving logo assets:', result) - console.log('Extracted updatedBrandKit:', updatedBrandKit)apps/pro-web/components/routes/workspace/media-tab/brand-kit/logo-generator/steps/symbol-style-step.tsx-37-40 (1)
37-40:⚠️ Potential issue | 🟡 MinorMinor grammar issue in user-facing text.
Line 39: "Pick the style that fit the most" should be "Pick the style that fits the most."
📝 Fix
- Pick the style that fit the most (you can change it later). + Pick the style that fits the most (you can change it later).apps/pro-web/components/routes/workspace/media-tab/brand-kit/brand-kit-generator/steps/colors-step.tsx-222-253 (1)
222-253:⚠️ Potential issue | 🟡 MinorHardcoded white text on the primary-colored button may fail contrast.
Line 234 sets
color: '#FFFFFF'on the primary-colored preview button. If a palette's primary color is light (e.g.,#F97316from "Peach Charcoal"), the contrast ratio against white could dip below AA thresholds. Since you already havegentlyAdjustForAAavailable, consider applying it to button text as well.apps/pro-web/lib/constants/brand-kit/brand-kit-checksums.ts-147-170 (1)
147-170:⚠️ Potential issue | 🟡 MinorVerify source PNG files for
all-caps-tightandsmall-capswordmark styles.Both entries currently resolve to checksum
9b2ba40829dcedf65b9bbd99f85e382d78a68854534795ade60691cd979dbc31, indicating they reference the same uploaded asset. Since these styles have distinct descriptions, rendering logic, and preset parameters, they should have different preview images. Checkpublic/brand-kit/wordmark/to confirm whethersmall-caps.pngis missing, identical toall-caps-tight.png, or corrupted. Runbun run upload-brand-kit-assetsto regenerate checksums after correcting the source files.apps/pro-web/lib/constants/brand-kit/wordmark-options.ts-4-151 (1)
4-151:⚠️ Potential issue | 🟡 MinorAdd a comment explaining why
'color-system-set'is excluded fromWORDMARK_PRESETS.
'color-system-set'is defined inWordmarkStyleand included inWORDMARK_PROMPTS(which usesRecord<WordmarkStyle, ...>), but omitted fromWORDMARK_PRESETS. The prompt describes it as "a standard deliverable set" rather than a user-selectable layout, so the exclusion is intentional. However, without explicit documentation, future developers may assume this is an oversight. Add a comment above the constant explaining that'color-system-set'is intentionally excluded because it's an output variant, not a preset choice presented to users.apps/pro-web/components/routes/workspace/media-tab/brand-kit/logo-generator/wordmark-preview.tsx-138-142 (1)
138-142:⚠️ Potential issue | 🟡 MinorComment inaccuracy: "10% margin on each side" vs actual 10% total.
Line 139 says "Leave 10% margin on each side" but the check
estimatedWidth <= containerWidth * 0.9reserves 10% total (≈5% per side). Either update the comment or change the multiplier to0.8if 10% per side was intended.apps/pro-web/components/routes/workspace/media-tab/brand-kit/logo-generator/steps/wordmark-style-step.tsx-164-182 (1)
164-182:⚠️ Potential issue | 🟡 MinorDead guard:
resolvedNamecan never be falsy.
resolvedNamealways falls back to'Your Brand'(line 166), so theif (!resolvedName) returnon line 168 never triggers. Similarly, thedisabled={!effectiveName}on line 490 is always false sinceeffectiveNamehas the same fallback (line 71).🧹 Proposed cleanup
const handleContinue = () => { const resolvedName = wordmarkName.trim() || activeOrganization || 'Your Brand' - if (!resolvedName) return - if (!wordmarkName.trim()) { setWordmarkName(resolvedName) }apps/pro-web/components/routes/workspace/media-tab/brand-kit/logo-generator/steps/wordmark-style-step.tsx-191-198 (1)
191-198:⚠️ Potential issue | 🟡 MinorHeader text appears copied from the Colors step.
"It is time for some color!" and the readability subtitle don't match the Wordmark Style step. This should describe wordmark/typography selection instead.
apps/pro-web/components/routes/workspace/media-tab/brand-kit/logo-generator/snap-slider.tsx-25-35 (1)
25-35:⚠️ Potential issue | 🟡 MinorEmpty
optionsarray causes label rendering errors and invalid ARIA state.When
optionsis empty:options[0]andoptions[options.length - 1]areundefined,formatLabelreceivesundefined, andselectedIndexis -1 (invalid foraria-valuenow). WhileisDisabledguards interactions, the labels and ARIA attributes are still computed unconditionally.🛡️ Proposed defensive guard
export function SnapSlider<T extends string | number>({ value, options, onChange, formatLabel = (v) => String(v), ... }: SnapSliderProps<T>) { + if (options.length === 0) return null + const selectedIndex = options.indexOf(value)apps/pro-web/components/routes/workspace/media-tab/brand-kit/logo-generator/steps/wordmark-style-step.tsx-143-149 (1)
143-149:⚠️ Potential issue | 🟡 MinorAdd defensive check for empty
allowedWeightsarrays.Both locations (lines 143-149 and 467-471) access
preset.allowedWeights[0]without validating the array is non-empty. While all current presets have at least one weight option, the type definition allows empty arrays, creating a risk of passingundefinedtosetWordmarkFontWeightorWordmarkPreview.weightif a preset is added with an emptyallowedWeightsarray. Add a guard check before accessing the index (e.g.,allowedWeights.length > 0 ? allowedWeights[0] : fallback).apps/pro-web/components/routes/chat/prompt-form/index.tsx-288-340 (1)
288-340:⚠️ Potential issue | 🟡 MinorSymbol draft auto-population reinserts on every return to symbol create mode.
When the user navigates away from symbol scope (line 293 resets the ref), then returns, the draft overwrites whatever was previously in the input. This may be intentional, but if the user had typed something before navigating away, it's silently lost. Consider checking whether the input already has content before overwriting.
apps/pro-web/components/routes/workspace/media-tab/brand-kit/brand-kit-generator/steps/checkpoint-step.tsx-294-297 (1)
294-297:⚠️ Potential issue | 🟡 MinorTypo: "The primary font are" → "The primary font is".
- *The primary font are for headlines and bold messages + *The primary font is for headlines and bold messagesapps/pro-web/scripts/upload-brand-kit-assets.ts-51-62 (1)
51-62:⚠️ Potential issue | 🟡 MinorPartial upload failures produce an incomplete checksums file with no indication of missing entries.
If a file upload fails (line 60-62), the script logs an error but continues. The generated
brand-kit-checksums.tswill silently omit that file's checksum. Downstream code usinggetBrandKitChecksumwill getundefinedwith no way to distinguish "not uploaded yet" from "upload failed."Consider either failing the script on any error or emitting a comment/warning in the generated file listing skipped assets.
apps/pro-web/components/routes/workspace/media-tab/brand-kit/brand-kit-generator/steps/checkpoint-step.tsx-139-148 (1)
139-148:⚠️ Potential issue | 🟡 Minor
vibeDescriptionTextproduces awkward phrasing for a single vibe part.When there's only one part after splitting, the
.mapon line 145 always prefixes the last (and only) element with"and ", yielding text like "It is and minimal." instead of "It is minimal."Proposed fix
const vibeDescriptionText = selectedVibes .map((name) => VIBES.find((v) => v.name === name)?.description) .filter((d): d is string => Boolean(d)) .flatMap((desc) => desc.split(' & ').map((part) => part.trim().toLowerCase()), ) - .map((vibeDesc, index, array) => - index === array.length - 1 ? `and ${vibeDesc}` : `${vibeDesc}, `, - ) - .join(' ') + .reduce((acc, vibeDesc, index, array) => { + if (array.length === 1) return vibeDesc + if (index === array.length - 1) return `${acc}and ${vibeDesc}` + return `${acc}${vibeDesc}, ` + }, '')apps/pro-web/components/routes/workspace/media-tab/brand-kit/logo-generator/logo-generator-wizard.tsx-324-342 (1)
324-342:⚠️ Potential issue | 🟡 MinorWrap the motion.div with
<AnimatePresence>to enable theexitanimation.The
exit={{ opacity: 0, x: -40 }}prop on line 328 has no effect without<AnimatePresence>wrapping this element. Framer Motion only runs exit animations when the component is a direct child ofAnimatePresenceand is removed from the React tree. Currently, theexitprop is dead code. ImportAnimatePresencefromframer-motionand wrap the motion.div (lines 324–342) accordingly.apps/pro-web/lib/constants/brand-kit/google-fonts.ts-647-669 (1)
647-669:⚠️ Potential issue | 🟡 Minor
getFallbackWeightterminal fallback'regular'may not be available for the font.Line 669:
availableWeights[0] || 'regular'— ifavailableWeightsis empty (no intersection between the font's supported weights and preset'sallowedWeights), the fallback is'regular'. But this font might not support 400 either (e.g., display-only fonts). The caller should be aware that the return value is not guaranteed to be renderable.Consider adding a
console.warnwhen the empty-array fallback path is hit, to aid debugging.
🧹 Nitpick comments (37)
apps/pro-web/lib/helpers/workspace/index.ts (1)
28-43: Add a runtime guard for array length, not just a doc comment.The JSDoc warns about >8 items, but nothing stops a caller from passing a larger array. In a browser environment,
generatePermutationson 10+ elements will produce millions of arrays and can freeze or crash the tab. A defensive runtime check is cheap and prevents subtle production issues.🛡️ Proposed fix
+const MAX_PERMUTATION_LENGTH = 8 + export function generatePermutations<T>(arr: T[]): T[][] { + if (arr.length > MAX_PERMUTATION_LENGTH) { + throw new RangeError( + `generatePermutations: input length ${arr.length} exceeds safe limit of ${MAX_PERMUTATION_LENGTH}` + ) + } if (arr.length <= 1) return [arr]apps/pro-web/components/ui/radio-group.tsx (1)
1-44: Consider placing this reusable UI component inpackages/mb-ui/.Per project guidelines,
packages/mb-ui/is the designated home for all new reusable UI components, especially shadcn/ui primitives. This RadioGroup is a generic, app-agnostic wrapper around Radix UI and would be better shared from there rather than scoped toapps/pro-web/components/ui/.Also, since the project uses React 19.2,
forwardRefis no longer necessary —refcan be passed as a regular prop. This isn't urgent (it still works), but worth aligning with the newer pattern when convenient.♻️ Example: React 19 ref-as-prop style (for when you move to mb-ui)
-const RadioGroup = React.forwardRef< - React.ElementRef<typeof RadioGroupPrimitive.Root>, - React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root> ->(({ className, ...props }, ref) => { - return ( - <RadioGroupPrimitive.Root - className={cn('grid gap-2', className)} - {...props} - ref={ref} - /> - ) -}) -RadioGroup.displayName = RadioGroupPrimitive.Root.displayName +function RadioGroup({ + className, + ref, + ...props +}: React.ComponentPropsWithRef<typeof RadioGroupPrimitive.Root>) { + return ( + <RadioGroupPrimitive.Root + className={cn('grid gap-2', className)} + {...props} + ref={ref} + /> + ) +} +RadioGroup.displayName = RadioGroupPrimitive.Root.displayNameAs per coding guidelines, "Use packages/mb-ui/ as the designated home for all new reusable UI components, preferring shadcn/ui components." Based on learnings, "UI components should follow Radix UI patterns and use shadcn/ui as the designated home for reusable components."
apps/pro-web/lib/helpers/ai-image.ts (3)
135-159: Consider adding a fetch timeout to prevent indefinite hangs.If the image URL points to a slow or unresponsive server, this
fetchwill hang indefinitely. Since this is used in a user-facing logo generation flow, a stalled request would degrade UX.♻️ Suggested improvement
- const response = await fetch(url) + const response = await fetch(url, { signal: AbortSignal.timeout(15_000) })
184-198: DRY:base64ToUint8Arrayduplicates the core logic ofbase64ToBlob.Lines 184-197 are almost identical to lines 13-23 in
base64ToBlob. Consider refactoringbase64ToBlobto delegate tobase64ToUint8Array:♻️ Proposed refactor
base64ToBlob: (base64Data: string, type = 'image/png'): Blob => { - const actualBase64 = base64Data.includes('base64,') - ? base64Data.split('base64,')[1] - : base64Data - - const byteString = atob(actualBase64) - const arrayBuffer = new ArrayBuffer(byteString.length) - const uint8Array = new Uint8Array(arrayBuffer) - - for (let i = 0; i < byteString.length; i++) { - uint8Array[i] = byteString.charCodeAt(i) - } - - return new Blob([uint8Array], { type }) + return new Blob([imageHelpers.base64ToUint8Array(base64Data)], { type }) },
200-217: DRY:downloadBlobduplicates the download path inshareOrDownload(lines 79-96).The download logic in
shareOrDownloadis essentially the same asdownloadBlob. Consider refactoringshareOrDownloadto delegate todownloadBlobfor the download fallback. Also,downloadBloblacks thetry/catchthatshareOrDownloadhas — any DOM error (e.g., in a non-browser environment) will throw unhandled.♻️ Proposed refactor for shareOrDownload
// Download the image (desktop or fallback) - try { - const blobUrl = URL.createObjectURL(blob) - const link = document.createElement('a') - link.href = blobUrl - link.download = `${fileName}.png` - link.style.display = 'none' - document.body.appendChild(link) - link.click() - - // Cleanup - setTimeout(() => { - document.body.removeChild(link) - URL.revokeObjectURL(blobUrl) - }, 100) - } catch (error) { - console.error('Failed to download image:', error) - throw error - } + try { + imageHelpers.downloadBlob(blob, `${fileName}.png`) + } catch (error) { + console.error('Failed to download image:', error) + throw error + }apps/pro-web/lib/helpers/workspace/media-tab/logo-assets.client.ts (1)
11-29: Consider adding a fetch timeout to prevent indefinite hangs.
fetch(url)has no timeout orAbortController. If a signed URL endpoint is slow or unresponsive, this will hang indefinitely, which could block preloading flows in the logo generator wizard.⏱️ Proposed timeout addition
export async function fetchSignedUrlAsBase64(url: string): Promise<string> { - const response = await fetch(url) + const controller = new AbortController() + const timeoutId = setTimeout(() => controller.abort(), 30_000) + + const response = await fetch(url, { signal: controller.signal }).finally(() => + clearTimeout(timeoutId) + ) if (!response.ok) { throw new Error(`Failed to fetch image: ${response.statusText}`) }apps/pro-web/components/routes/workspace/media-tab/brand-kit/shared/selectable-carousel.tsx (1)
68-77: Selection state not communicated to assistive technology.The
<button>wrapping each carousel item has noaria-pressedoraria-selectedattribute. Screen readers won't convey the selected state.Proposed fix
<button type="button" onClick={() => onSelect(item)} className="w-full h-full" + aria-pressed={selected} > {children(item, selected)} </button>apps/pro-web/components/ui/workspace-sheet/index.tsx (2)
38-47: DOM element variables should be prefixed with$per codebase convention.
workspaceContainerandzoomOverlay(Line 66) hold DOM element references and should follow the$prefix convention.Proposed fix
useLayoutEffect(() => { - const workspaceContainer = document.getElementById(containerId) - if (workspaceContainer) { - setContainer(workspaceContainer) + const $workspaceContainer = document.getElementById(containerId) + if ($workspaceContainer) { + setContainer($workspaceContainer) } else {- const zoomOverlay = document.querySelector('[data-image-zoom="true"]') - if (zoomOverlay) { + const $zoomOverlay = document.querySelector('[data-image-zoom="true"]') + if ($zoomOverlay) {Based on learnings: "Enforce a DOM element naming convention in the pro-web app: in TSX files, prefix variables that hold DOM element references with a leading '$'."
14-24:maxWidthis declared in the interface but never used.The
maxWidthprop is defined inWorkspaceDialogPropsbut not consumed in the component body or JSX.Proposed fix
interface WorkspaceDialogProps { open: boolean onOpenChange: (open: boolean) => void title: string subtitle?: string children: ReactNode footer?: ReactNode - maxWidth?: string containerId?: string panelSlot?: ReactNode }apps/pro-web/lib/helpers/workspace/media-tab/logo-prompt.ts (3)
56-60:logoStyleMapis an identity mapping — the values are the same as the keys.You could simplify by using
selectedLogoStyledirectly, removing the unnecessary map and thelogoStyleTextvariable.Proposed simplification
- // Map logo style to display text - const logoStyleMap: Record<LogoStyle, string> = { - wordmark: 'wordmark', - symbol: 'symbol', - } - const logoStyleText = logoStyleMap[selectedLogoStyle] + const logoStyleText = selectedLogoStyle
201-204: Defensive checkc.color !== 'undefined'guards against a stringifiedundefined, which hints at an upstream bug.If
c.colorcan end up as the literal string'undefined', that likely means somewhere upstreamString(undefined)or template-literal interpolation is occurring. This filter is a reasonable safeguard, but consider also checkingc.role !== 'undefined'for consistency, and investigate the root cause.Tighter filter
const validColors = allowedColors.filter( - (c) => c.color && c.role && c.color !== 'undefined', + (c) => c.color && c.role && c.color !== 'undefined' && c.role !== 'undefined', )
138-146:stylesThatAllowInitialis re-allocated on every call.This array is static data. Hoisting it to module scope avoids the allocation and makes the intent clearer.
Hoist to module level
+const STYLES_THAT_ALLOW_INITIAL: SymbolStyle[] = [ + 'monogram-letterform', + 'geometric-badge', + 'soft-blob-pill', + 'sharp-crest-angle', + 'abstract-symbol', + 'literal-pictogram', + 'pattern-tile-motif', +] + export function buildLogoBasePrompt({Then replace the local declaration with
STYLES_THAT_ALLOW_INITIAL.apps/pro-web/lib/hooks/use-workspace-media.tsx (5)
303-310:useCallbackforgetGenerationInstanceis ineffective — the hook objects in the dependency array are new references every render.
useImageGeneration()returns a new object on each render, so[wordmarkGeneration, symbolGeneration, mediaGeneration]always differs, makinguseCallbacka no-op. Consider usinguseRefto hold the generation instances, or restructure so the callback reads from refs.Sketch using refs
+const mediaRef = useRef(mediaGeneration) +const wordmarkRef = useRef(wordmarkGeneration) +const symbolRef = useRef(symbolGeneration) + +// Keep refs in sync +mediaRef.current = mediaGeneration +wordmarkRef.current = wordmarkGeneration +symbolRef.current = symbolGeneration + const getGenerationInstance = useCallback( (scope: GenerationScope) => { - if (scope === 'wordmark') return wordmarkGeneration - if (scope === 'symbol') return symbolGeneration - return mediaGeneration + if (scope === 'wordmark') return wordmarkRef.current + if (scope === 'symbol') return symbolRef.current + return mediaRef.current }, - [wordmarkGeneration, symbolGeneration, mediaGeneration], + [], )
628-705:saveLogoAssetsMutationduplicates the IDB update logic fromsaveBrandKitMutation.Lines 664–691 are nearly identical to lines 588–611 (fetch workspace data from IDB, extract
brandKitfrom response, map organizations, write back). Extract a shared helper to reduce duplication and keep the logic consistent.Helper sketch
async function updateBrandKitInIDB( result: { data?: { brandKit?: BrandKitData } }, organizationId: string, getItem: (...args: unknown[]) => Promise<unknown>, updateItem: (...args: unknown[]) => Promise<void>, ) { const workspaceData = await getItem(WORKSPACE_STRUCTURE_KEY) if (!workspaceData || typeof workspaceData !== 'object') return const workspace = workspaceData as unknown as WorkspaceStructure const updatedBrandKit = result.data?.brandKit as BrandKitData | undefined if (!updatedBrandKit) return const updatedWorkspace: WorkspaceStructure = { ...workspace, organizationData: workspace.organizationData.map( (org: OrganizationData) => org.id === organizationId ? { ...org, brandKit: updatedBrandKit } : org, ), } const payload = structureToIDBPayload(updatedWorkspace) await updateItem(WORKSPACE_STRUCTURE_KEY, payload) }
379-423:handleWordmarkPromptSubmitandhandleSymbolPromptSubmitare nearly identical.Both follow the same pattern: get scope-specific reference images → resolve checksums → check for existing image → call
generateImagewith'gemini-3-pro-image-preview'and'1:1'aspect ratio. Consider extracting a shared helper parameterized by scope and generation instance to reduce duplication.
762-820:generatePermutationsproducesn!results — safe for 4 items (24 permutations) but fragile if palette size grows.For 4 colors this is fine, but if the palette structure ever gains more color slots, the factorial growth could cause performance issues. A comment documenting the assumption (exactly 4 colors) would help prevent future regressions.
Also,
shuffleSelectedPaletteOrderreadsselectedPaletteandshuffleStatefrom closure but usessetShuffleState/setSelectedPalette/setColorRoleItemsimperatively. The deps[selectedPalette, shuffleState]are correct, but consider using functional updaters forsetSelectedPaletteif there's any risk of stale closures during rapid shuffles.
48-208: TheMediaWorkspaceState+MediaWorkspaceActionsinterfaces and provider are growing very large — consider decomposing.The provider now manages ~25 state fields and ~30+ actions spanning templates, brand-kit, logo generation, color roles, shuffle state, and three generation instances. This makes the context a "god object" that triggers re-renders for all consumers on any state change.
This isn't blocking, but as this feature set matures, splitting into focused sub-providers (e.g.,
LogoGenerationProvider,BrandKitProvider) would improve maintainability and render performance. As per coding guidelines, "keep providers focused with hierarchical composition."apps/pro-web/components/routes/workspace/media-tab/brand-kit/logo-generator/steps/logo-symbol-or-done-step.tsx (1)
14-17:goTo(5)is a magic number — fragile if wizard steps are reordered.If the confirm step moves to a different index, this will silently navigate to the wrong step. Consider using a named constant (e.g.,
CONFIRM_STEP_INDEX) or deriving the index from the step configuration to make this resilient to reordering.Example
+// Defined alongside step config or imported from wizard constants +const CONFIRM_STEP_INDEX = 5 + const handleDone = () => { - goTo(5) + goTo(CONFIRM_STEP_INDEX) }apps/pro-web/components/routes/workspace/workspace-content.tsx (1)
3-3:useChatPanelis imported but never used in this file.The hook is imported on line 3 but not invoked anywhere in the component. If this was added in preparation for upcoming work, consider removing it until it's needed to keep the import list clean.
apps/pro-web/components/routes/workspace/media-tab/media-workspace.tsx (1)
44-50: Consider stabilizing handler references to avoid re-renders.The inline arrow functions on
onAdd,onAddMultiple, andonRemovecreate new references every render, which can cause unnecessary re-renders ofReferenceImagesPanel. Since they only partially apply the'media'scope, consider extracting them withuseCallback.As per coding guidelines, "When attaching event handlers to components, use regular function calls instead of arrow functions to prevent unnecessary re-renders and potential build errors from hoisting."
♻️ Extract stable callbacks
+import { useCallback, useEffect } from 'react' -import { useEffect } from 'react' export function MediaWorkspace() { const { generatedImage, resetImageGeneration, selectedTemplate, getReferenceImages, addReferenceImage, addMultipleReferenceImages, removeReferenceImage, } = useWorkspaceMedia() + const handleAddReference = useCallback( + (img: Parameters<typeof addReferenceImage>[1]) => addReferenceImage('media', img), + [addReferenceImage], + ) + const handleAddMultipleReferences = useCallback( + (imgs: Parameters<typeof addMultipleReferenceImages>[1]) => addMultipleReferenceImages('media', imgs), + [addMultipleReferenceImages], + ) + const handleRemoveReference = useCallback( + (idx: number) => removeReferenceImage('media', idx), + [removeReferenceImage], + ) <ReferenceImagesPanel images={getReferenceImages('media')} - onAdd={(img) => addReferenceImage('media', img)} - onAddMultiple={(imgs) => addMultipleReferenceImages('media', imgs)} - onRemove={(idx) => removeReferenceImage('media', idx)} + onAdd={handleAddReference} + onAddMultiple={handleAddMultipleReferences} + onRemove={handleRemoveReference} className="w-full lg:w-80 border-t lg:border-t-0 lg:border-l border-gray-200 px-3 sm:px-4 lg:px-7 pt-3 sm:pt-4 lg:pt-12 pb-4 sm:pb-6 lg:pb-8" />apps/pro-web/components/routes/workspace/media-tab/brand-kit/brand-kit-generator/steps/colors-step.tsx (1)
28-33: Unsafeas FontFamilytype assertion.
selectedFonts[0].familyis typed asstring, and casting it toFontFamilybypasses compile-time checking. If a font family stored in context doesn't match aGOOGLE_FONTSkey, the fallback (?? '') saves you at runtime, but the assertion masks the mismatch.Consider using a type guard or validation helper instead:
♻️ Safer font class resolution
+import { GOOGLE_FONTS } from '@/lib/constants/brand-kit/google-fonts' + +function isFontFamily(family: string): family is FontFamily { + return family in GOOGLE_FONTS +} + -const primaryFontClass = selectedFonts?.[0] - ? getGoogleFontClass(selectedFonts[0].family as FontFamily) - : '' -const secondaryFontClass = selectedFonts?.[1] - ? getGoogleFontClass(selectedFonts[1].family as FontFamily) - : primaryFontClass +const primaryFontClass = selectedFonts?.[0]?.family && isFontFamily(selectedFonts[0].family) + ? getGoogleFontClass(selectedFonts[0].family) + : '' +const secondaryFontClass = selectedFonts?.[1]?.family && isFontFamily(selectedFonts[1].family) + ? getGoogleFontClass(selectedFonts[1].family) + : primaryFontClassapps/pro-web/components/routes/workspace/media-tab/brand-kit/logo-generator/steps/symbol-style-step.tsx (1)
19-23: Removeconsole.logdebug artifact.Line 21 logs the selected symbol style to the console. This should be removed before merging to avoid noisy console output in production.
🧹 Remove debug log
const handleContinue = () => { if (!selectedSymbolStyle) return - console.log('Selected symbol style:', selectedSymbolStyle) next() }apps/pro-web/components/routes/workspace/media-tab/brand-kit/logo-generator/wordmark-preview.tsx (2)
32-40: MoveweightClassMapoutside the component to avoid re-creation on every render.This is a static mapping that never changes. Declaring it inside the component body causes a new object allocation on each render.
♻️ Proposed refactor
+const WEIGHT_CLASS_MAP: Record<WordmarkFontWeight, string> = { + 'extra light': 'font-extralight', + light: 'font-light', + regular: 'font-normal', + medium: 'font-medium', + semibold: 'font-semibold', + bold: 'font-bold', + black: 'font-black', +} + export function WordmarkPreview({ ... }: WordmarkPreviewProps) { ... - const weightClassMap: Record<WordmarkFontWeight, string> = { - 'extra light': 'font-extralight', - light: 'font-light', - regular: 'font-normal', - medium: 'font-medium', - semibold: 'font-semibold', - bold: 'font-bold', - black: 'font-black', - }Then reference
WEIGHT_CLASS_MAP[weight]at line 80.
43-65: Stale ref risk in ResizeObserver cleanup.
containerRef.currentis captured at effect-run time (line 62) but may benullby cleanup time if the component unmounts mid-cycle. WhileresizeObserver.disconnect()itself is safe, the common best practice is to capture the node in a local variable. This also suppresses the React Strict Mode double-invoke warning.♻️ Proposed refactor
useEffect(() => { - if (!containerRef.current || !displayText) return + const node = containerRef.current + if (!node || !displayText) return const updateSize = () => { - if (!containerRef.current) return - const containerWidth = containerRef.current.offsetWidth + if (!node) return + const containerWidth = node.offsetWidth ... } updateSize() const resizeObserver = new ResizeObserver(updateSize) - resizeObserver.observe(containerRef.current) + resizeObserver.observe(node) return () => resizeObserver.disconnect() }, [displayText, tracking, weight, fontFamily])apps/pro-web/components/routes/workspace/media-tab/brand-kit/brand-kit-generator/brand-kit-generator-wizard.tsx (1)
61-66: Consider gating verbose console logs behind a dev/debug flag.Multiple
console.logstatements (lines 61, 66, 92–94, 125) with emoji prefixes are useful during development but will be noisy in production. A lightweight debug utility orprocess.env.NODE_ENVguard would keep observability without cluttering prod console output.Also applies to: 92-94, 125-125
apps/pro-web/components/shared/image-display.tsx (1)
55-67: Custom eventclose-image-zoomlacks type safety.The
close-image-zoomevent is wired viawindow.addEventListener/removeEventListenerusing a plain string. Consider declaring a typed custom event or at minimum a shared constant for the event name to avoid typo-driven bugs across dispatchers and listeners.apps/pro-web/components/routes/workspace/media-tab/brand-kit/logo-generator/snap-slider.tsx (1)
61-88: No touch event support — drag interaction is mouse-only.
handleMouseDown/mousemove/mouseuphandle desktop dragging, but there are no correspondingtouchstart/touchmove/touchendhandlers. Mobile users can still use keyboard navigation, but the drag experience is desktop-exclusive.apps/pro-web/scripts/upload-brand-kit-assets.ts (1)
76-97: Generated file has inconsistent indentation.The function body (lines 90-95) uses single-space indentation while the rest of the codebase appears to use tabs. Since this file is auto-generated and consumed by the TypeScript compiler, this is cosmetic but may trigger linter warnings.
apps/pro-web/components/routes/workspace/media-tab/brand-kit/logo-generator/logo-generator-wizard.tsx (1)
248-255: HardcodedmodelId: 'gemini-3-pro-image-preview'for restored logos.This fabricates a
GeneratedImagewith a model ID that may not correspond to the actual model used to generate the saved logo. If downstream code usesmodelIdfor routing or display, this could produce incorrect behavior. Consider storing the original model ID alongside the logo asset, or using a sentinel value that clearly indicates "restored from storage."apps/pro-web/lib/helpers/workspace/media-tab/logo-assets.server.ts (1)
23-32:initBucket()creates a newStorageclient on every invocation.For the current low-frequency logo upload use case this is fine, but if reuse grows, consider lazily initializing a module-level singleton to avoid repeated credential parsing and connection setup.
Lazy singleton pattern
+let _bucket: Bucket | null = null + function initBucket(): Bucket { + if (_bucket) return _bucket const storage = new Storage({ projectId: appConfig.features.storageProjectId, credentials: { client_email: appConfig.features.storageClientEmail, private_key: appConfig.features.storageSecretAccessKey, }, }) - return storage.bucket(appConfig.features.storageBucketName) + _bucket = storage.bucket(appConfig.features.storageBucketName) + return _bucket }apps/pro-web/app/api/organizations/[id]/route.ts (1)
242-319: Consolidated brandKit + logoAssets update avoids race conditions.Fetching existing data once, merging both brandKit and logo assets, then persisting in a single mutation is the right approach. This prevents partial writes and data loss from concurrent requests.
One architectural note: the persisted
urlfields in logo assets are signed URLs with a 7-day expiry. Consumers loading logos after expiry will receive 403s. Consider storing the bucket key alongside the URL and re-signing on read, or using a longer-lived access pattern.apps/pro-web/components/routes/chat/prompt-form/index.tsx (1)
322-333: DOM element variable should use the$prefix per codebase convention.Line 325 assigns a DOM element to
el. Per the established convention, this should be$el.- const el = inputRef.current - el.focus() - el.setSelectionRange(draft.length, draft.length) - el.scrollTop = el.scrollHeight + const $el = inputRef.current + $el.focus() + $el.setSelectionRange(draft.length, draft.length) + $el.scrollTop = $el.scrollHeightBased on learnings: "In TSX files, adopt a convention where variables that reference DOM elements are prefixed with '$'."
apps/pro-web/components/routes/pro/chat-panel-pro-controls.tsx (1)
296-344: Consider removing dead commented-out code.Lines 296, 310–344 contain multiple blocks of commented-out feature toggles and UI elements. These are either tracked by TODOs or obsolete. Keeping them adds noise; prefer tracking these in issues instead.
apps/pro-web/components/routes/workspace/media-tab/brand-kit/brand-kit-flow.tsx (1)
64-72: Magic number3for brand-kit generator checkpoint step.The hardcoded step index
3is fragile — if steps are reordered or added, this silently breaks. Extract it as a named constant or import it from the wizard definition.Proposed fix
+const BRAND_KIT_GENERATOR_CHECKPOINT_STEP = 3 + const handleCloseLogoGenerator = () => { if (currentOrg?.id) { - const brandKitGeneratorCheckpointStep = 3 - saveBrandKitStep(brandKitGeneratorCheckpointStep, currentOrg.id) + saveBrandKitStep(BRAND_KIT_GENERATOR_CHECKPOINT_STEP, currentOrg.id) } setCurrentView('brandKitGenerator') }apps/pro-web/types/media.types.ts (1)
84-95:casingtyped asstringinstead ofWordmarkCasing.Both
WordmarkLogoAsset.casingandWordmarkLogoAssetInput.casingare typed asstring, but aWordmarkCasingunion type exists inbrand-kit.types.ts. Using the specific type would catch invalid values at compile time.Proposed fix (both interfaces)
+import type { WordmarkCasing, WordmarkFontWeight } from './brand-kit.types' + export interface WordmarkLogoAsset extends LogoAsset { name: string fontFamily?: string - casing?: string + casing?: WordmarkCasing tracking?: number - fontWeight?: string + fontWeight?: WordmarkFontWeight } // ... export interface WordmarkLogoAssetInput extends LogoAssetInputBase { name: string fontFamily?: string - casing?: string + casing?: WordmarkCasing tracking?: number - fontWeight?: string + fontWeight?: WordmarkFontWeight }Also applies to: 115-126
apps/pro-web/lib/helpers/workspace/media-tab/logo-reference-image-mapper.ts (2)
16-32:normalizeFontFileNameandnormalizePaletteFileNameare identical — DRY violation.Both functions perform the same transformation: lowercase → replace spaces with hyphens → strip non-alphanumeric chars. Consolidate into a single helper.
Proposed fix
-function normalizeFontFileName(fontFamily: string): string { - return fontFamily - .toLowerCase() - .replace(/\s+/g, '-') - .replace(/[^a-z0-9-]/g, '') -} - -function normalizePaletteFileName(paletteName: string): string { - return paletteName - .toLowerCase() - .replace(/\s+/g, '-') - .replace(/[^a-z0-9-]/g, '') -} +function normalizeAssetKey(name: string): string { + return name + .toLowerCase() + .replace(/\s+/g, '-') + .replace(/[^a-z0-9-]/g, '') +}Then replace usages:
- const fontKey = normalizeFontFileName(fontFamily) + const fontKey = normalizeAssetKey(fontFamily) ... - const paletteKey = normalizePaletteFileName(selectedPalette.name) + const paletteKey = normalizeAssetKey(selectedPalette.name)
44-102: Consider extracting the repeated parameter shape into a named interface.
resolveLogoReferenceSpecs,getLogoReferenceImages, andgetLogoReferenceChecksumsall take the same inline object type. Extracting it reduces duplication and makes the API easier to maintain.Example
interface LogoReferenceInput { selectedFonts: SelectedFont[] selectedPalette: Palette | null selectedLogoStyle: LogoStyle | null selectedWordmarkStyle: WordmarkStyle | null selectedSymbolStyle: SymbolStyle | null wordmarkFontFamily?: string | null } export function resolveLogoReferenceSpecs(input: LogoReferenceInput): LogoReferenceSpec[] { ... } export function getLogoReferenceImages(input: LogoReferenceInput): ReferenceImagePayload[] { ... } export function getLogoReferenceChecksums(input: LogoReferenceInput): string[] { ... }Also applies to: 133-192
| // Validate logoAssets if provided | ||
| if (body.logoAssets !== undefined) { | ||
| if (typeof body.logoAssets !== 'object' || body.logoAssets === null) { | ||
| return NextResponse.json( | ||
| { error: 'Logo assets must be an object' }, | ||
| { status: 422 }, | ||
| ) | ||
| } | ||
|
|
||
| if (!body.logoAssets.symbol && !body.logoAssets.wordmark) { | ||
| return NextResponse.json( | ||
| { | ||
| error: | ||
| 'At least one logo asset (symbol or wordmark) must be provided', | ||
| }, | ||
| { status: 422 }, | ||
| ) | ||
| } | ||
|
|
||
| if (body.logoAssets.wordmark) { | ||
| if ( | ||
| !body.logoAssets.wordmark.name || | ||
| typeof body.logoAssets.wordmark.name !== 'string' || | ||
| !body.logoAssets.wordmark.name.trim() | ||
| ) { | ||
| return NextResponse.json( | ||
| { error: 'Wordmark name is required and must be non-empty' }, | ||
| { status: 422 }, | ||
| ) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Missing base64 field validation for logo asset inputs.
The validation checks that at least one asset exists and that wordmark has a name, but never verifies that base64 is present on symbol or wordmark inputs. If a caller omits base64, uploadLogoAssetToGCS → decodeBase64ToBuffer will throw on undefined, surfacing as an opaque 500 instead of a descriptive 422.
Add base64 validation
if (body.logoAssets.wordmark) {
if (
!body.logoAssets.wordmark.name ||
typeof body.logoAssets.wordmark.name !== 'string' ||
!body.logoAssets.wordmark.name.trim()
) {
return NextResponse.json(
{ error: 'Wordmark name is required and must be non-empty' },
{ status: 422 },
)
}
+ if (!body.logoAssets.wordmark.base64 || typeof body.logoAssets.wordmark.base64 !== 'string') {
+ return NextResponse.json(
+ { error: 'Wordmark base64 image data is required' },
+ { status: 422 },
+ )
+ }
}
+
+ if (body.logoAssets.symbol) {
+ if (!body.logoAssets.symbol.base64 || typeof body.logoAssets.symbol.base64 !== 'string') {
+ return NextResponse.json(
+ { error: 'Symbol base64 image data is required' },
+ { status: 422 },
+ )
+ }
+ }🤖 Prompt for AI Agents
In `@apps/pro-web/app/api/organizations/`[id]/route.ts around lines 111 - 142, The
logoAssets validation lacks checks that the symbol and/or wordmark include a
non-empty base64 string, causing decodeBase64ToBuffer (called by
uploadLogoAssetToGCS) to receive undefined and throw; update the validation in
the handler that inspects body.logoAssets to verify that if
body.logoAssets.symbol exists then body.logoAssets.symbol.base64 is a non-empty
string, and likewise if body.logoAssets.wordmark exists then
body.logoAssets.wordmark.base64 is a non-empty string (in addition to the
existing wordmark.name check), returning a 422 JSON error with a clear message
when base64 is missing or invalid.
|
|
||
| const trimmedInput = input.trim() | ||
| setInput('') | ||
|
|
||
| // Media Tab mode: use Media-specific handler | ||
| if (isMediaMode) { | ||
| await handlePromptSubmit(input) | ||
| // Wordmark mode: generate/refine wordmark | ||
| if (isWordmarkScope) { | ||
| // Build refine prompt with selected colors | ||
| const allowedColors = getSelectedColors() | ||
| const refinePrompt = buildRefineLogoPrompt({ | ||
| allowedColors, | ||
| userUpdate: trimmedInput, | ||
| }) | ||
|
|
||
| if (!refinePrompt) { | ||
| return | ||
| } | ||
|
|
||
| await handleWordmarkPromptSubmit(refinePrompt) | ||
| return | ||
| } |
There was a problem hiding this comment.
setInput('') clears user input before the async submission completes.
On line 520, the input is cleared unconditionally. If buildRefineLogoPrompt returns falsy (line 531) or the async handler fails, the user's text is already gone with no way to recover it. Consider clearing input only after a successful submission, or stashing the value for recovery.
Proposed fix
const trimmedInput = input.trim()
- setInput('')
-
// Wordmark mode: generate/refine wordmark
if (isWordmarkScope) {
const allowedColors = getSelectedColors()
const refinePrompt = buildRefineLogoPrompt({
allowedColors,
userUpdate: trimmedInput,
})
if (!refinePrompt) {
return
}
await handleWordmarkPromptSubmit(refinePrompt)
+ setInput('')
return
}
// ... similar for other scopes📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const trimmedInput = input.trim() | |
| setInput('') | |
| // Media Tab mode: use Media-specific handler | |
| if (isMediaMode) { | |
| await handlePromptSubmit(input) | |
| // Wordmark mode: generate/refine wordmark | |
| if (isWordmarkScope) { | |
| // Build refine prompt with selected colors | |
| const allowedColors = getSelectedColors() | |
| const refinePrompt = buildRefineLogoPrompt({ | |
| allowedColors, | |
| userUpdate: trimmedInput, | |
| }) | |
| if (!refinePrompt) { | |
| return | |
| } | |
| await handleWordmarkPromptSubmit(refinePrompt) | |
| return | |
| } | |
| const trimmedInput = input.trim() | |
| // Wordmark mode: generate/refine wordmark | |
| if (isWordmarkScope) { | |
| // Build refine prompt with selected colors | |
| const allowedColors = getSelectedColors() | |
| const refinePrompt = buildRefineLogoPrompt({ | |
| allowedColors, | |
| userUpdate: trimmedInput, | |
| }) | |
| if (!refinePrompt) { | |
| return | |
| } | |
| await handleWordmarkPromptSubmit(refinePrompt) | |
| setInput('') | |
| return | |
| } |
🤖 Prompt for AI Agents
In `@apps/pro-web/components/routes/chat/prompt-form/index.tsx` around lines 518 -
537, The code clears the user's input immediately via setInput('') before
building and submitting the wordmark prompt, which loses the value if
buildRefineLogoPrompt returns falsy or if handleWordmarkPromptSubmit fails;
change the flow in the block guarded by isWordmarkScope so you stash
trimmedInput into a local variable (trimmedInput already exists), defer calling
setInput('') until after refinePrompt is confirmed truthy and after await
handleWordmarkPromptSubmit(refinePrompt) resolves successfully, and in
error/falsy cases leave the input intact (or restore it from the stashed
trimmedInput) so the user can recover their text; update logic around
buildRefineLogoPrompt and handleWordmarkPromptSubmit to only clear input
post-success and to restore input on failure.
| const chatId = useMemo(() => { | ||
| return params.threadId || mbChatState.isNewChat | ||
| ? mbChatState.newChatThreadId | ||
| : activeThread?.threadId | ||
| }, [ | ||
| activeThread?.threadId, | ||
| params, | ||
| mbChatState.isNewChat, | ||
| mbChatState.newChatThreadId, | ||
| ]) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check how chatId is used downstream in the component
rg -nP --type=ts --type=tsx -C3 '\bchatId\b' apps/pro-web/components/routes/pro/chat-panel-pro-controls.tsx | head -100Repository: bitcashorg/masterbots
Length of output: 92
🏁 Script executed:
#!/bin/bash
# Also check the file size and structure to understand context better
wc -l apps/pro-web/components/routes/pro/chat-panel-pro-controls.tsxRepository: bitcashorg/masterbots
Length of output: 131
🏁 Script executed:
#!/bin/bash
# Look at the useMBChat hook to understand state structure
rg -nP --type=ts --type=tsx 'export.*useMBChat|function useMBChat|const useMBChat' apps/pro-web/lib/hooks/use-mb-chat.tsx | head -20Repository: bitcashorg/masterbots
Length of output: 92
🏁 Script executed:
#!/bin/bash
# Search for chatId usage without type constraint
rg -nP -C3 'chatId' apps/pro-web/components/routes/pro/chat-panel-pro-controls.tsxRepository: bitcashorg/masterbots
Length of output: 624
🏁 Script executed:
#!/bin/bash
# Read the specific lines around the useMemo and where chatId is used
sed -n '120,180p' apps/pro-web/components/routes/pro/chat-panel-pro-controls.tsxRepository: bitcashorg/masterbots
Length of output: 1495
🏁 Script executed:
#!/bin/bash
# Also read lines where chatId is referenced later (likely in handleContinueGeneration)
sed -n '180,290p' apps/pro-web/components/routes/pro/chat-panel-pro-controls.tsxRepository: bitcashorg/masterbots
Length of output: 3120
🏁 Script executed:
#!/bin/bash
# Double-check operator precedence by looking at MDN or creating a test
# Verify the ternary vs OR precedence claim
cat > /tmp/test-precedence.js << 'EOF'
// Test operator precedence: does || bind tighter than ? :
const params_threadId = "thread-123";
const isNewChat = true;
const newChatId = "new-chat-456";
const activeThreadId = "active-789";
// Current code
const result1 = params_threadId || isNewChat ? newChatId : activeThreadId;
// What happens: (params_threadId || isNewChat) ? newChatId : activeThreadId
console.log("Current expression result:", result1);
console.log("Expected if logic was correct: params_threadId would be used");
console.log("Actual: discarded, newChatId used instead");
// Proposed fix
const result2 = params_threadId || (isNewChat ? newChatId : null) || activeThreadId;
console.log("\nProposed fix result:", result2);
EOF
node /tmp/test-precedence.jsRepository: bitcashorg/masterbots
Length of output: 240
🏁 Script executed:
#!/bin/bash
# Check if there are any tests or other usages that hint at expected behavior
rg -n 'isNewChat|newChatThreadId' apps/pro-web/components/routes/pro/chat-panel-pro-controls.tsx | head -20Repository: bitcashorg/masterbots
Length of output: 219
Critical operator precedence bug in chatId — params.threadId is discarded when truthy.
Due to JavaScript operator precedence, this evaluates as (params.threadId || mbChatState.isNewChat) ? mbChatState.newChatThreadId : activeThread?.threadId. When params.threadId is set (navigating to an existing thread), the condition is true and returns mbChatState.newChatThreadId, losing the route parameter. This causes messages to be sent to a new chat instead of the intended thread.
Fix with explicit grouping to implement the three-way fallback:
Proposed fix
const chatId = useMemo(() => {
- return params.threadId || mbChatState.isNewChat
- ? mbChatState.newChatThreadId
- : activeThread?.threadId
+ return params.threadId
+ || (mbChatState.isNewChat ? mbChatState.newChatThreadId : null)
+ || activeThread?.threadId
}, [
activeThread?.threadId,
params,
mbChatState.isNewChat,
mbChatState.newChatThreadId,
])🤖 Prompt for AI Agents
In `@apps/pro-web/components/routes/pro/chat-panel-pro-controls.tsx` around lines
122 - 131, The current useMemo for chatId mistakenly groups the OR with the
ternary, causing params.threadId to be ignored; update the chatId computation in
the useMemo so it implements the three-way fallback explicitly: return
params.threadId if truthy, else if mbChatState.isNewChat return
mbChatState.newChatThreadId, otherwise return activeThread?.threadId; locate the
chatId useMemo and replace the single expression using params.threadId ||
mbChatState.isNewChat ? ... with an explicit conditional sequence (checking
params.threadId first, then mbChatState.isNewChat, then activeThread?.threadId)
to preserve the route param.
| useEffect(() => { | ||
| if (!brandKit?.logos) return | ||
| if (isLoadingLogosRef.current) return | ||
|
|
||
| // Check if we need to load any logos | ||
| const needsWordmark = | ||
| brandKit.logos.wordmark?.url && !loadedLogosRef.current.wordmark | ||
| const needsSymbol = | ||
| brandKit.logos.symbol?.url && !loadedLogosRef.current.symbol | ||
|
|
||
| if (!needsWordmark && !needsSymbol) return | ||
|
|
||
| // Start loading | ||
| isLoadingLogosRef.current = true | ||
|
|
||
| const loadLogo = async ( | ||
| scope: 'wordmark' | 'symbol', | ||
| url: string, | ||
| ): Promise<void> => { | ||
| try { | ||
| // Skip if generatedImage already exists (user has already generated a new one) | ||
| const instance = getGenerationInstance(scope) | ||
| if (instance.generatedImage) { | ||
| loadedLogosRef.current[scope] = true | ||
| return | ||
| } | ||
|
|
||
| console.log(`🔄 Preloading ${scope} logo from GCS...`) | ||
|
|
||
| const base64 = await fetchSignedUrlAsBase64(url) | ||
|
|
||
| const savedImage: GeneratedImage = { | ||
| id: nanoid(), | ||
| prompt: `Saved ${scope} logo`, | ||
| base64, | ||
| modelId: 'gemini-3-pro-image-preview', | ||
| timestamp: Date.now(), | ||
| provider: 'google', | ||
| } | ||
|
|
||
| instance.addInitialImage(savedImage) | ||
| loadedLogosRef.current[scope] = true | ||
|
|
||
| console.log(`✅ ${scope} logo preloaded from GCS`) | ||
| } catch (error) { | ||
| console.error(`❌ Failed to preload ${scope} logo:`, error) | ||
| // Fail silently - auto-generation will happen | ||
| } | ||
| } | ||
|
|
||
| // Load both logos concurrently | ||
| const loadPromises: Promise<void>[] = [] | ||
|
|
||
| if (needsWordmark && brandKit.logos.wordmark?.url) { | ||
| loadPromises.push(loadLogo('wordmark', brandKit.logos.wordmark.url)) | ||
| } | ||
|
|
||
| if (needsSymbol && brandKit.logos.symbol?.url) { | ||
| loadPromises.push(loadLogo('symbol', brandKit.logos.symbol.url)) | ||
| } | ||
|
|
||
| Promise.allSettled(loadPromises).finally(() => { | ||
| isLoadingLogosRef.current = false | ||
| }) | ||
| }, [brandKit?.logos, getGenerationInstance]) |
There was a problem hiding this comment.
Fire-and-forget promise in useEffect — no cleanup on unmount or dependency change.
Promise.allSettled(...).finally(...) on line 278 is not awaited and has no abort mechanism. If the component unmounts or brandKit.logos changes mid-flight, the old promise chain still mutates refs and calls addInitialImage on potentially stale generation instances. This also means isLoadingLogosRef stays true until the first load completes, blocking any legitimate re-trigger.
Consider using an AbortController or a mounted flag to skip stale updates:
Sketch
useEffect(() => {
if (!brandKit?.logos) return
if (isLoadingLogosRef.current) return
// ...
+ let cancelled = false
isLoadingLogosRef.current = true
const loadLogo = async (scope, url) => {
try {
// ...
const base64 = await fetchSignedUrlAsBase64(url)
+ if (cancelled) return
// ...
instance.addInitialImage(savedImage)
} catch (error) { /* ... */ }
}
// ...
Promise.allSettled(loadPromises).finally(() => {
isLoadingLogosRef.current = false
})
+ return () => { cancelled = true }
}, [brandKit?.logos, getGenerationInstance])🤖 Prompt for AI Agents
In
`@apps/pro-web/components/routes/workspace/media-tab/brand-kit/logo-generator/logo-generator-wizard.tsx`
around lines 217 - 281, The effect starts async loads with Promise.allSettled
but has no cleanup, so stale callbacks can mutate refs and call
getGenerationInstance().addInitialImage after unmount or deps change; fix by
adding a per-run abort token (e.g., const abort = { cancelled: false } or an
AbortController) scoped to the useEffect run, check abort before mutating
isLoadingLogosRef, loadedLogosRef, and before calling instance.addInitialImage
inside loadLogo, and in the effect cleanup set abort.cancelled = true (or call
controller.abort()) so pending promises short-circuit; also ensure you only flip
isLoadingLogosRef.current = false if the run is not aborted (use the same token)
to avoid blocking subsequent runs — update symbols: useEffect,
isLoadingLogosRef, loadedLogosRef, loadLogo, getGenerationInstance,
addInitialImage, and the Promise.allSettled finally handler accordingly.
| useEffect(() => { | ||
| // Skip auto-generation for symbol mode, as it requires the user to specify the symbol they want to generate | ||
| if (scope === 'symbol') { | ||
| return | ||
| } | ||
|
|
||
| if (hasGeneratedRef.current || isGeneratingImage || generatedImage) { | ||
| return | ||
| } | ||
|
|
||
| // Build prompt | ||
| const prompt = buildLogoBasePrompt({ | ||
| selectedLogoStyle, | ||
| selectedSymbolStyle, | ||
| selectedWordmarkStyle, | ||
| wordmarkName, | ||
| wordmarkFontFamily, | ||
| wordmarkCasing, | ||
| wordmarkTracking, | ||
| wordmarkFontWeight, | ||
| selectedVibes, | ||
| selectedPalette, | ||
| selectedFonts, | ||
| }) | ||
|
|
||
| if (!prompt) { | ||
| console.warn( | ||
| '[RefineLogoStep] Cannot generate wordmark: invalid configuration', | ||
| ) | ||
| return | ||
| } | ||
|
|
||
| // Mark as generated | ||
| hasGeneratedRef.current = true | ||
|
|
||
| handleWordmarkPromptSubmit(prompt).catch((err) => { | ||
| console.error('[RefineLogoStep] Auto-generation failed:', err) | ||
| hasGeneratedRef.current = false | ||
| }) | ||
| }, [ | ||
| isGeneratingImage, | ||
| generatedImage, | ||
| selectedLogoStyle, | ||
| selectedSymbolStyle, | ||
| selectedWordmarkStyle, | ||
| wordmarkName, | ||
| wordmarkFontFamily, | ||
| wordmarkCasing, | ||
| wordmarkTracking, | ||
| wordmarkFontWeight, | ||
| selectedVibes, | ||
| selectedPalette, | ||
| selectedFonts, | ||
| scope, | ||
| handleWordmarkPromptSubmit, | ||
| ]) |
There was a problem hiding this comment.
Potential infinite retry loop on auto-generation failure.
When handleWordmarkPromptSubmit rejects (Line 128), hasGeneratedRef.current is reset to false. Since isGeneratingImage is a dependency and will change back to false after the failed attempt, this effect will re-fire and attempt generation again — indefinitely on persistent errors.
Consider adding a retry counter or not resetting the ref on failure:
Proposed fix
+ const retryCountRef = useRef(0)
+ const MAX_RETRIES = 2
useEffect(() => {
if (scope === 'symbol') return
- if (hasGeneratedRef.current || isGeneratingImage || generatedImage) return
+ if (hasGeneratedRef.current || isGeneratingImage || generatedImage) return
+ if (retryCountRef.current >= MAX_RETRIES) return
const prompt = buildLogoBasePrompt({ /* ... */ })
if (!prompt) { /* ... */ return }
hasGeneratedRef.current = true
handleWordmarkPromptSubmit(prompt).catch((err) => {
console.error('[RefineLogoStep] Auto-generation failed:', err)
- hasGeneratedRef.current = false
+ retryCountRef.current += 1
+ hasGeneratedRef.current = false
})
}, [/* ... */])🤖 Prompt for AI Agents
In
`@apps/pro-web/components/routes/workspace/media-tab/brand-kit/logo-generator/steps/refine-logo-step.tsx`
around lines 91 - 146, The effect can loop indefinitely because the catch resets
hasGeneratedRef.current to false and isGeneratingImage will toggle, retriggering
the effect; add a retry limit: introduce a maxRetries constant and a retry
counter ref (e.g., retryCountRef) and in the catch increment
retryCountRef.current and only reset hasGeneratedRef.current to false if
retryCountRef.current < maxRetries; once maxRetries is reached, leave
hasGeneratedRef.current true (or set a failed flag ref) and log the permanent
failure so the effect won't reattempt; update references to hasGeneratedRef,
retryCountRef, isGeneratingImage, generatedImage, handleWordmarkPromptSubmit and
scope in the logic so the effect stops retrying after the max attempts.
| export function gentlyAdjustForAA( | ||
| bg: string, | ||
| text: string, | ||
| ): { bg: string; text: string; adjusted: boolean } { | ||
| let ratio = contrastRatio(bg, text) | ||
| if (ratio >= 4.5) return { bg, text, adjusted: false } | ||
|
|
||
| // try darkening text toward black | ||
| let [r, g, b] = hexToRgb(text).map((c) => Math.round(c * 255)) | ||
| for (let i = 0; i < 10 && ratio < 4.5; i++) { | ||
| r = Math.max(0, r - 8) | ||
| g = Math.max(0, g - 8) | ||
| b = Math.max(0, b - 8) | ||
| const hex = `#${[r, g, b].map((v) => v.toString(16).padStart(2, '0')).join('')}` | ||
| ratio = contrastRatio(bg, hex) | ||
| if (ratio >= 4.5) return { bg, text: hex, adjusted: true } | ||
| } | ||
|
|
||
| return { bg, text, adjusted: false } | ||
| } |
There was a problem hiding this comment.
gentlyAdjustForAA only darkens text — fails on dark backgrounds.
The function unconditionally darkens the text color toward black. On a dark background with light text, this reduces contrast instead of improving it. The function should lighten text (toward white) when the background is darker than the text.
Proposed fix
export function gentlyAdjustForAA(
bg: string,
text: string,
): { bg: string; text: string; adjusted: boolean } {
let ratio = contrastRatio(bg, text)
if (ratio >= 4.5) return { bg, text, adjusted: false }
- // try darkening text toward black
+ // Determine direction: darken text if bg is light, lighten text if bg is dark
+ const bgLum = relLum(hexToRgb(bg))
+ const shouldLighten = bgLum < 0.5
+
let [r, g, b] = hexToRgb(text).map((c) => Math.round(c * 255))
for (let i = 0; i < 10 && ratio < 4.5; i++) {
- r = Math.max(0, r - 8)
- g = Math.max(0, g - 8)
- b = Math.max(0, b - 8)
+ if (shouldLighten) {
+ r = Math.min(255, r + 8)
+ g = Math.min(255, g + 8)
+ b = Math.min(255, b + 8)
+ } else {
+ r = Math.max(0, r - 8)
+ g = Math.max(0, g - 8)
+ b = Math.max(0, b - 8)
+ }
const hex = `#${[r, g, b].map((v) => v.toString(16).padStart(2, '0')).join('')}`
ratio = contrastRatio(bg, hex)
if (ratio >= 4.5) return { bg, text: hex, adjusted: true }
}
return { bg, text, adjusted: false }
}🤖 Prompt for AI Agents
In `@apps/pro-web/lib/helpers/color-utils.ts` around lines 105 - 124,
gentlyAdjustForAA currently always darkens the text color toward black, which
worsens contrast on dark backgrounds; change it to first decide direction by
comparing background and text luminance (or contrast with black vs white) and
then either darken (as now) or lighten the text toward white by incrementing
r,g,b up to the same loop/step limits, clamping 0–255 and recomputing hex and
contrastRatio each iteration; update the loop that manipulates r,g,b (and the
hex construction) to add when lightening and subtract when darkening, and return
adjusted: true when a successful change reaches contrast >= 4.5.
| //* Update history and cursor | ||
| setHistory((prev) => { | ||
| // Remove all items after current cursor (future history) | ||
| const newHistory = prev.slice(0, cursor + 1) | ||
| // Add new image | ||
| newHistory.push(imageObject) | ||
| return newHistory | ||
| }) | ||
| setCursor((prev) => prev + 1) |
There was a problem hiding this comment.
Stale cursor in setHistory can discard history entries on rapid calls.
setHistory on line 167 reads cursor from the closure, but setCursor on line 172 uses a functional updater. If generateImage fires twice before React commits, both calls see the same cursor value and the first image is silently dropped. Use a ref or combine both into a single state object to keep them synchronized.
Proposed fix using a ref to stay in sync
+ const cursorRef = useRef(-1)
const [history, setHistory] = useState<GeneratedImage[]>([])
const [cursor, setCursor] = useState(-1)
// Keep ref in sync
+ useEffect(() => { cursorRef.current = cursor }, [cursor])
// Inside generateImage success path:
- setHistory((prev) => {
- const newHistory = prev.slice(0, cursor + 1)
- newHistory.push(imageObject)
- return newHistory
- })
- setCursor((prev) => prev + 1)
+ setHistory((prev) => {
+ const newHistory = prev.slice(0, cursorRef.current + 1)
+ newHistory.push(imageObject)
+ cursorRef.current = newHistory.length - 1
+ return newHistory
+ })
+ setCursor(cursorRef.current)Alternatively, consolidate history + cursor into a single useReducer so both update atomically.
🤖 Prompt for AI Agents
In `@apps/pro-web/lib/hooks/use-image-generation.tsx` around lines 164 - 172, The
current update uses setHistory((prev) => { ... cursor ... }) while setCursor
uses a functional updater, causing stale closure of cursor when generateImage is
called rapidly; fix by making history+cursor update atomic—either use a ref
(e.g., cursorRef) read/updated inside your generateImage flow so setHistory
always slices using the up-to-date cursorRef before pushing imageObject and then
incrementing cursorRef and calling setCursor, or replace history and cursor
state with a single useReducer that handles the "add image" action (updating
both history and cursor together) so setHistory and setCursor are not out of
sync.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@apps/pro-web/lib/hooks/use-workspace-media.tsx`:
- Around line 759-817: shuffleSelectedPaletteOrder currently builds permutations
from the live selectedPalette colors so once you swap them the next call
permutes the wrong input; fix by storing a canonical color array for each
palette (e.g., a map keyed by selectedPalette.name to the original
[primary,accent,bg,text] created when a palette is first selected) and have
shuffleSelectedPaletteOrder generate permutations from that canonical array
instead of selectedPalette; keep using shuffleState[paletteName] to track the
cursor, update setShuffleState and setSelectedPalette as before (using
nextPerm), and ensure setColorRoleItems still maps positions consistently to
roles (primary->0, accent->1, bg->2, text->3) so selection state remains per
position.
- Around line 297-310: The getGenerationInstance useCallback is being recreated
every render because useImageGeneration() returns fresh objects; fix by
memoizing each generation instance (the results of useImageGeneration()) with
useMemo so wordmarkGeneration, symbolGeneration, and mediaGeneration have stable
references, then keep getGenerationInstance as a useCallback that depends on
those memoized values (or remove useCallback if consumer identity isn't
required); update consumers like logo-generator-wizard.tsx to rely on the
now-stable getGenerationInstance reference.
🧹 Nitpick comments (9)
apps/pro-web/components/routes/pro/chat-panel-pro.tsx (1)
13-29: Several declared props are unused.
onConvertToDocumentis destructured (Line 36) but never referenced. The dialog-state props on Lines 20–28 (convertDialogOpen,setConvertDialogOpen,selectedMessageId,convertedText,setConvertedText,targetProject,setTargetProject,targetDocument,setTargetDocument) are declared in the interface but neither destructured nor passed anywhere. If these are planned forChatPanelProControls, they should be wired through the context or passed as props; otherwise, trim the interface to avoid a misleading public API.apps/pro-web/components/routes/pro/chat-panel-pro-controls.tsx (4)
138-145: AvoidanyforchatOptionsparameter.The
biome-ignoresuppresses the lint warning, butChatRequestOptions(already imported on Line 22) is the expected type here.Proposed fix
const prepareMessageOptions = useCallback( - // biome-ignore lint/suspicious/noExplicitAny: <explanation> - (chatOptions: any) => ({ + (chatOptions: ChatRequestOptions) => ({ ...chatOptions, webSearch: webSearch, }), [webSearch], )
147-178:handleContinueGenerationis recreated every render — consideruseCallback.This async handler is passed to a
Button'sonClick. WithoutuseCallback, a new function reference is created each render, which can cause unnecessary re-renders of child components. The dependencies are stable (getContinuationPrompt,append,chatId,continueGeneration,chatbot,setIsCutOff,setIsContinuing).
309-344: Remove commented-out code blocks.These large commented-out sections (active action indicator, Deep Expertise toggle, web search toggle) add visual noise and reduce readability. If they represent planned features, track them in issues instead.
446-453: Simplify!Boolean(chatbot)to!chatbot.The double negation with
Boolean()is unnecessary for a truthiness check on an optional object, hence the biome lint warning.!chatbotis equivalent and idiomatic.Proposed fix
disabled={ - // biome-ignore lint/complexity/noExtraBooleanCast: <explanation> - !Boolean(chatbot) || + !chatbot || isLoading || isPreProcessing || processingState !== 'idle' }apps/pro-web/lib/hooks/use-workspace-media.tsx (4)
158-175: Extract the logo assets payload type to avoid triple duplication.This exact inline type is repeated in
saveLogoAssetsMutation(lines 634–648) andsaveLogoAssets(lines 713–727). Extract it once and reuse.♻️ Suggested refactor
Define the type once near the other interfaces (or in
brand-kit.types.ts):interface LogoAssetsPayload { symbol?: { base64: string style?: string } wordmark?: { base64: string style?: string name: string fontFamily?: string casing?: string tracking?: number fontWeight?: string } }Then reference
LogoAssetsPayloadin the interface, mutation, and callback.
629-702: Near-identical mutation logic duplicated fromsaveBrandKitMutation.
saveLogoAssetsMutation(lines 629–702) mirrorssaveBrandKitMutation(lines 567–626) almost line-for-line — same endpoint, same IDB sync, same invalidation. Only the request body key differs (brandKitvslogoAssets).Extract the shared PATCH → IDB-sync → invalidate pattern into a helper to reduce ~70 lines of duplication.
♻️ Sketch
async function patchOrganizationAndSyncIDB( organizationId: string, body: Record<string, unknown>, getItem: typeof getItem, updateItem: typeof updateItem, ) { const response = await fetch(`/api/organizations/${organizationId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) if (!response.ok) throw new Error(`Failed to save to server`) const result = await response.json() const workspaceData = await getItem(WORKSPACE_STRUCTURE_KEY) if (workspaceData && typeof workspaceData === 'object') { const workspace = workspaceData as unknown as WorkspaceStructure const updatedBrandKit = result.data?.brandKit as BrandKitData | undefined if (updatedBrandKit) { const updatedWorkspace: WorkspaceStructure = { ...workspace, organizationData: workspace.organizationData.map( (org: OrganizationData) => org.id === organizationId ? { ...org, brandKit: updatedBrandKit } : org, ), } await updateItem(WORKSPACE_STRUCTURE_KEY, structureToIDBPayload(updatedWorkspace)) } } return result }Then both mutations become one-liners in
mutationFn.
379-423:handleWordmarkPromptSubmitandhandleSymbolPromptSubmitare near-duplicates — consider a shared helper.The two functions differ only in the scope (
'wordmark'/'symbol') and the generation instance. A single parameterized function would eliminate ~40 lines of duplication:♻️ Sketch
const handleScopedPromptSubmit = async ( scope: 'wordmark' | 'symbol', ) => async (prompt: string) => { const refs = referenceImagesByScope[scope] const checksums = refs.length > 0 ? refs.map(r => r.checksum) : undefined const gen = scope === 'wordmark' ? wordmarkGeneration : symbolGeneration await gen.generateImage(prompt, 'gemini-3-pro-image-preview', { previousImageData: gen.generatedImage || undefined, referenceImageChecksums: checksums, aspectRatio: '1:1', }) }
826-927: Consider splitting this provider — 50+ context values will re-render all consumers on any change.This single context carries image generation state (3 scopes), brand kit state, logo generation state, UI toggles, palette state, and all associated actions. Any state change triggers a re-render of every consumer.
During active image generation (where
isLoading/generatedImageupdate frequently), unrelated consumers (e.g., brand kit step UI) will re-render needlessly. Consider splitting into focused contexts (e.g.,LogoGenerationContext,BrandKitContext) or memoizing sub-objects withuseMemo.
| // Three separate image generation instances | ||
| const mediaGeneration = useImageGeneration() | ||
| const wordmarkGeneration = useImageGeneration() | ||
| const symbolGeneration = useImageGeneration() | ||
|
|
||
| // Helper to get the correct instance based on scope | ||
| const getGenerationInstance = useCallback( | ||
| (scope: GenerationScope) => { | ||
| if (scope === 'wordmark') return wordmarkGeneration | ||
| if (scope === 'symbol') return symbolGeneration | ||
| return mediaGeneration | ||
| }, | ||
| [wordmarkGeneration, symbolGeneration, mediaGeneration], | ||
| ) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find the useImageGeneration hook implementation
fd "use-image-generation" --type f -e ts -e tsxRepository: bitcashorg/masterbots
Length of output: 156
🏁 Script executed:
#!/bin/bash
# Read the useImageGeneration hook implementation
cat -n apps/pro-web/lib/hooks/use-image-generation.tsxRepository: bitcashorg/masterbots
Length of output: 9453
🏁 Script executed:
#!/bin/bash
# Check how getGenerationInstance is used in the file
cat -n apps/pro-web/lib/hooks/use-workspace-media.tsx | grep -A 5 -B 5 "getGenerationInstance"Repository: bitcashorg/masterbots
Length of output: 1268
🏁 Script executed:
#!/bin/bash
# Check the full context of how getGenerationInstance is used and what context it's part of
sed -n '290,330p' apps/pro-web/lib/hooks/use-workspace-media.tsxRepository: bitcashorg/masterbots
Length of output: 1649
🏁 Script executed:
#!/bin/bash
# Check where getGenerationInstance is consumed - look for useEffect/useCallback dependencies
rg "getGenerationInstance" apps/pro-web/lib/hooks/use-workspace-media.tsx -A 3 -B 1Repository: bitcashorg/masterbots
Length of output: 614
🏁 Script executed:
#!/bin/bash
# Check the full return value of this hook to see the context object structure
sed -n '860,950p' apps/pro-web/lib/hooks/use-workspace-media.tsxRepository: bitcashorg/masterbots
Length of output: 1914
🏁 Script executed:
#!/bin/bash
# Look for where getGenerationInstance is actually consumed/used
# Check if it's used in useEffect dependencies or passed to children
rg "getGenerationInstance" apps/pro-web --type tsx --type ts -B 2 -A 2Repository: bitcashorg/masterbots
Length of output: 92
🏁 Script executed:
#!/bin/bash
# Look for where getGenerationInstance is actually consumed/used
rg "getGenerationInstance" apps/pro-web -A 2 -B 2Repository: bitcashorg/masterbots
Length of output: 8949
useCallback for getGenerationInstance is ineffective due to unstable dependencies and causes downstream effects to re-run unnecessarily.
useImageGeneration() returns a new object reference on every render and is not memoized. This means the dependency array [wordmarkGeneration, symbolGeneration, mediaGeneration] always contains new references, making the useCallback recreate the function on every render. Additionally, consumers like logo-generator-wizard.tsx include getGenerationInstance in useEffect dependencies, causing those effects to re-run every render instead of only when actual logic changes.
Memoize the return value of useImageGeneration() using useMemo to provide stable references, or alternatively remove the useCallback if function identity is not critical for consumers.
🤖 Prompt for AI Agents
In `@apps/pro-web/lib/hooks/use-workspace-media.tsx` around lines 297 - 310, The
getGenerationInstance useCallback is being recreated every render because
useImageGeneration() returns fresh objects; fix by memoizing each generation
instance (the results of useImageGeneration()) with useMemo so
wordmarkGeneration, symbolGeneration, and mediaGeneration have stable
references, then keep getGenerationInstance as a useCallback that depends on
those memoized values (or remove useCallback if consumer identity isn't
required); update consumers like logo-generator-wizard.tsx to rely on the
now-stable getGenerationInstance reference.
| // Shuffle selected palette order deterministically | ||
| const shuffleSelectedPaletteOrder = useCallback(() => { | ||
| if (!selectedPalette) return | ||
|
|
||
| const paletteName = selectedPalette.name | ||
| const baseColors = [ | ||
| selectedPalette.primary, | ||
| selectedPalette.accent, | ||
| selectedPalette.bg, | ||
| selectedPalette.text, | ||
| ] | ||
|
|
||
| // Generate all valid permutations | ||
| const allPermutations = generatePermutations(baseColors) | ||
|
|
||
| // Filter to valid combinations only | ||
| const validPermutations = allPermutations.filter((perm) => { | ||
| const [, , bg, text] = perm | ||
| // bg and text must have good contrast (WCAG AA) | ||
| return isValidTextCombination(bg, text) | ||
| }) | ||
|
|
||
| if (validPermutations.length === 0) { | ||
| console.warn('[shuffleSelectedPaletteOrder] No valid permutations found') | ||
| return | ||
| } | ||
|
|
||
| // Get current cursor for this palette (default to 0) | ||
| const currentCursor = shuffleState[paletteName] ?? 0 | ||
|
|
||
| // Advance to next valid permutation (cycle) | ||
| const nextCursor = (currentCursor + 1) % validPermutations.length | ||
| const nextPerm = validPermutations[nextCursor] | ||
|
|
||
| // Update shuffle state | ||
| setShuffleState((prev) => ({ ...prev, [paletteName]: nextCursor })) | ||
|
|
||
| // Update selectedPalette with new color assignments | ||
| setSelectedPalette({ | ||
| name: selectedPalette.name, | ||
| category: selectedPalette.category, | ||
| primary: nextPerm[0], | ||
| accent: nextPerm[1], | ||
| bg: nextPerm[2], | ||
| text: nextPerm[3], | ||
| }) | ||
|
|
||
| // Also update colorRoleItems to match the new palette order | ||
| // Keep selection state per position | ||
| setColorRoleItems((prev) => { | ||
| const selections = prev.map((item) => item.selected) | ||
| return [ | ||
| { role: 'primary', color: nextPerm[0], selected: selections[0] }, | ||
| { role: 'secondary', color: nextPerm[2], selected: selections[1] }, // bg maps to secondary | ||
| { role: 'accent', color: nextPerm[1], selected: selections[2] }, | ||
| { role: 'muted', color: nextPerm[3], selected: selections[3] }, // text maps to muted | ||
| ] | ||
| }) | ||
| }, [selectedPalette, shuffleState]) |
There was a problem hiding this comment.
Bug: Cursor-based shuffle breaks after the first invocation because input colors change.
shuffleSelectedPaletteOrder generates permutations from selectedPalette's current colors (line 764–769), then updates selectedPalette with the chosen permutation (line 797–804). On the next call, baseColors reflects the already-shuffled order, producing a completely different permutation set — the stored cursor (shuffleState[paletteName]) now indexes into the wrong list.
Effect: After the first shuffle, subsequent shuffles will not cycle through all valid permutations deterministically; they may repeat, skip, or land on unexpected orderings.
Fix: Derive permutations from the palette's original/canonical color ordering (e.g., store the initial [primary, accent, bg, text] once per palette) rather than the live selectedPalette state.
🤖 Prompt for AI Agents
In `@apps/pro-web/lib/hooks/use-workspace-media.tsx` around lines 759 - 817,
shuffleSelectedPaletteOrder currently builds permutations from the live
selectedPalette colors so once you swap them the next call permutes the wrong
input; fix by storing a canonical color array for each palette (e.g., a map
keyed by selectedPalette.name to the original [primary,accent,bg,text] created
when a palette is first selected) and have shuffleSelectedPaletteOrder generate
permutations from that canonical array instead of selectedPalette; keep using
shuffleState[paletteName] to track the cursor, update setShuffleState and
setSelectedPalette as before (using nextPerm), and ensure setColorRoleItems
still maps positions consistently to roles (primary->0, accent->1, bg->2,
text->3) so selection state remains per position.
0534dc6 to
80a113e
Compare
| "format:write": "prettier --write \"{app,lib,components}/**/*.{ts,tsx,mdx}\" --cache", | ||
| "format:check": "prettier --check \"{app,lib,components}**/*.{ts,tsx,mdx}\" --cache" | ||
| "format:check": "prettier --check \"{app,lib,components}**/*.{ts,tsx,mdx}\" --cache", | ||
| "upload-brand-kit-assets": "bun run scripts/upload-brand-kit-assets.ts" |
There was a problem hiding this comment.
Note: With the current approach, if the GCS environment variables change or the bucket is cleared/removed, this script needs to be run again to re-upload the base Brand Kit assets (logo styles, fonts, and palettes).
TODO: Investigate whether there is a better “seed” or automatic preload strategy for assets in GCS, or move this step to CI/CD in the future to avoid manual dependency.
CC: @AndlerRL
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/pro-web/components/routes/chat/prompt-form/index.tsx (1)
509-616:⚠️ Potential issue | 🟠 MajorAsync submission handlers lack error handling — unhandled rejections possible.
handleWordmarkPromptSubmit,handleSymbolPromptSubmit,handleMediaPromptSubmit, andonSubmitare allawaited but none are wrapped intry/catch. If any of these reject, the promise rejection goes unhandled. Combined with the prematuresetInput(''), the user loses their input with no feedback.Wrap the async calls (or the entire function body after validation) in a
try/catchthat shows an error viacustomSonnerand restores the input on failure.As per coding guidelines: "Handle GraphQL operation failures at network, parsing, and business logic levels with meaningful fallbacks for degraded functionality" and "Test assumptions about API calls and DOM operations - don't assume success, always verify results and provide meaningful fallbacks."
🤖 Fix all issues with AI agents
In `@apps/pro-web/components/routes/chat/prompt-form/index.tsx`:
- Around line 334-340: getGenerationInstance is being recreated every render
because mediaGeneration, wordmarkGeneration, and symbolGeneration are new
objects each time (they come from direct useImageGeneration() calls), so memoize
those three instances with useMemo and then keep getGenerationInstance wrapped
with useCallback depending on the memoized values; specifically, replace direct
useImageGeneration() invocations with memoized variables (e.g., const
mediaGeneration = useMemo(() => useImageGeneration(...), [...deps]) or memoize
the returned objects), update getGenerationInstance's dependency array to
reference those memoized symbols (mediaGeneration, wordmarkGeneration,
symbolGeneration), and this will stabilize getGenerationInstance so the effect
that depends on it (the useEffect at the block ending with setInput) no longer
re-runs every render.
- Around line 178-179: The current logic allows a fragile state where
isBrandKitOpen can be true while activeGenerationScope remains 'media', making
the media submission path unreachable; update state coordination by either (A)
when toggling brand kit open (where isBrandKitOpen is set), also set
activeGenerationScope to a valid input scope like 'wordmark' or 'symbol' (update
the brand kit open/close handler that mutates isBrandKitOpen and
activeGenerationScope), or (B) add an explicit guard/reset where
isBrandKitInputDisabled is computed or in the submit handlers (e.g., the media
submission handler) to detect the invalid combination (isBrandKitOpen &&
activeGenerationScope === 'media') and reset activeGenerationScope to a safe
default before proceeding; reference isBrandKitOpen, activeGenerationScope,
isBrandKitInputDisabled, and the media submission handler to locate where to
apply the change.
🧹 Nitpick comments (1)
apps/pro-web/components/routes/chat/prompt-form/index.tsx (1)
322-333: DOM element variable should use$prefix per codebase convention.Line 325:
elshould be$elto follow the pro-web convention of prefixing DOM element references with$.Proposed fix
setTimeout(() => { if (!inputRef.current) return - const el = inputRef.current - el.focus() + const $el = inputRef.current + $el.focus() // Move caret to end - el.setSelectionRange(draft.length, draft.length) + $el.setSelectionRange(draft.length, draft.length) // Scroll to bottom - el.scrollTop = el.scrollHeight + $el.scrollTop = $el.scrollHeight }, 0)Based on learnings: "In TSX files, adopt a convention where variables that reference DOM elements are prefixed with '$' (e.g., $questionElement, $el) to quickly identify DOM refs."
| const isBrandKitInputDisabled = | ||
| isBrandKitOpen && !isWordmarkScope && !isSymbolScope |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's look at the file in question to understand the context better
fd 'prompt-form' --type f -e tsx | head -5Repository: bitcashorg/masterbots
Length of output: 47
🏁 Script executed:
# Search for where activeGenerationScope is defined/assigned
rg -n 'activeGenerationScope\s*=' --type=ts --type=tsx -B 2 -A 2Repository: bitcashorg/masterbots
Length of output: 92
🏁 Script executed:
# Search for isBrandKitOpen and its assignments
rg -n 'isBrandKitOpen' --type=ts --type=tsx -B 2 -A 2 | head -100Repository: bitcashorg/masterbots
Length of output: 92
🏁 Script executed:
# Look for media scope handling
rg -n "media" --type=ts --type=tsx -i -B 2 -A 2 | grep -A 5 -B 5 "scope\|media" | head -80Repository: bitcashorg/masterbots
Length of output: 92
🏁 Script executed:
# Find the file directly
find . -path "*prompt-form/index.tsx" -type fRepository: bitcashorg/masterbots
Length of output: 180
🏁 Script executed:
# Search for activeGenerationScope without file type specification
rg -n 'activeGenerationScope' -B 2 -A 2Repository: bitcashorg/masterbots
Length of output: 3486
🏁 Script executed:
# Search for isBrandKitOpen assignments
rg -n 'isBrandKitOpen.*=' -B 1 -A 1 | head -100Repository: bitcashorg/masterbots
Length of output: 1339
🏁 Script executed:
# Look at the prompt-form component structure
find . -path "*prompt-form*" -type f | grep -E "\.(ts|tsx|js|jsx)$"Repository: bitcashorg/masterbots
Length of output: 536
🏁 Script executed:
# Search for handleMediaPromptSubmit to understand media scope handling
rg -n 'handleMediaPromptSubmit' -B 3 -A 5Repository: bitcashorg/masterbots
Length of output: 3892
🏁 Script executed:
# Search for setActiveGenerationScope calls to understand when scope changes
rg -n 'setActiveGenerationScope' -B 3 -A 3Repository: bitcashorg/masterbots
Length of output: 4088
🏁 Script executed:
# Look at the BrandKitFlow and how it manages state
rg -n 'BrandKitFlow' -B 2 -A 10Repository: bitcashorg/masterbots
Length of output: 4973
🏁 Script executed:
# Check lines around 178-179 in the actual prompt-form file to see full context
sed -n '165,200p' ./apps/pro-web/components/routes/chat/prompt-form/index.tsxRepository: bitcashorg/masterbots
Length of output: 1538
🏁 Script executed:
# Check lines 595-650 to see the submitPrompt logic and media scope handling
sed -n '590,650p' ./apps/pro-web/components/routes/chat/prompt-form/index.tsxRepository: bitcashorg/masterbots
Length of output: 1948
🏁 Script executed:
# Check if activeGenerationScope changes based on isBrandKitOpen
rg -n 'isBrandKitOpen' -B 5 -A 5Repository: bitcashorg/masterbots
Length of output: 10744
🏁 Script executed:
# Search for where brand kit state might affect activeGenerationScope
rg -n 'setIsBrandKitOpen' -B 5 -A 5Repository: bitcashorg/masterbots
Length of output: 5817
🏁 Script executed:
# Check if there's any effect that manages scope when brand kit changes
sed -n '1,100p' ./apps/pro-web/lib/hooks/use-workspace-media.tsx | head -50Repository: bitcashorg/masterbots
Length of output: 1479
🏁 Script executed:
# Look for any effects that coordinate isBrandKitOpen with activeGenerationScope
rg -n 'useEffect' ./apps/pro-web/lib/hooks/use-workspace-media.tsx -A 10 | head -80Repository: bitcashorg/masterbots
Length of output: 47
🏁 Script executed:
# Check the textarea disabled prop to confirm the condition
sed -n '795,820p' ./apps/pro-web/components/routes/chat/prompt-form/index.tsxRepository: bitcashorg/masterbots
Length of output: 951
🏁 Script executed:
# Check if there's any logic that prevents media scope when brand kit opens, or verifies the UI flow
sed -n '35,120p' ./apps/pro-web/components/routes/pro/chat-panel-pro.tsxRepository: bitcashorg/masterbots
Length of output: 2282
🏁 Script executed:
# Look at BrandKitFlow to understand how it manages scope
sed -n '20,80p' ./apps/pro-web/components/routes/workspace/media-tab/brand-kit/brand-kit-flow.tsxRepository: bitcashorg/masterbots
Length of output: 1687
🏁 Script executed:
# Check if there's documentation or comments explaining the intended relationship
rg -n 'Brand.*Kit.*scope|media.*Brand.*Kit' -B 2 -A 2Repository: bitcashorg/masterbots
Length of output: 47
Clarify state coordination between isBrandKitOpen and activeGenerationScope to prevent unreachable media submission path.
The input disables when isBrandKitOpen && !isWordmarkScope && !isSymbolScope (i.e., when scope is 'media'). While the prompt form is currently hidden entirely when brand kit opens (chat-panel-pro.tsx line 97), there's no explicit guarantee that activeGenerationScope changes from its default 'media' value. This creates a fragile dependency where the media submission handler (lines 595–597) could become unreachable if the UI architecture changes. Consider either: (1) explicitly setting activeGenerationScope to 'wordmark' or 'symbol' when brand kit opens, or (2) adding an explicit guard/reset mechanism to prevent this state combination.
🤖 Prompt for AI Agents
In `@apps/pro-web/components/routes/chat/prompt-form/index.tsx` around lines 178 -
179, The current logic allows a fragile state where isBrandKitOpen can be true
while activeGenerationScope remains 'media', making the media submission path
unreachable; update state coordination by either (A) when toggling brand kit
open (where isBrandKitOpen is set), also set activeGenerationScope to a valid
input scope like 'wordmark' or 'symbol' (update the brand kit open/close handler
that mutates isBrandKitOpen and activeGenerationScope), or (B) add an explicit
guard/reset where isBrandKitInputDisabled is computed or in the submit handlers
(e.g., the media submission handler) to detect the invalid combination
(isBrandKitOpen && activeGenerationScope === 'media') and reset
activeGenerationScope to a safe default before proceeding; reference
isBrandKitOpen, activeGenerationScope, isBrandKitInputDisabled, and the media
submission handler to locate where to apply the change.
| }, [ | ||
| isWorkspaceActive, | ||
| activeGenerationScope, | ||
| selectedSymbolStyle, | ||
| getGenerationInstance, | ||
| setInput, | ||
| ]) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find the useWorkspaceMedia hook definition
fd 'use-workspace-media' --type f -e ts -e tsxRepository: bitcashorg/masterbots
Length of output: 111
🏁 Script executed:
# Also search for useWorkspaceMedia in the codebase
rg 'useWorkspaceMedia' --type ts --type tsx -lRepository: bitcashorg/masterbots
Length of output: 92
🏁 Script executed:
# Search for getGenerationInstance declarations and usage
rg 'getGenerationInstance' --type ts --type tsx -B2 -A2 | head -150Repository: bitcashorg/masterbots
Length of output: 92
🏁 Script executed:
# Check the specific file and lines mentioned in the review
cat -n apps/pro-web/components/routes/chat/prompt-form/index.tsx | sed -n '170,180p;330,345p'Repository: bitcashorg/masterbots
Length of output: 1029
🏁 Script executed:
# Read the useWorkspaceMedia hook to check if getGenerationInstance is wrapped in useCallback
cat -n apps/pro-web/lib/hooks/use-workspace-media.tsxRepository: bitcashorg/masterbots
Length of output: 34237
🏁 Script executed:
# Also search for where getGenerationInstance is derived from or defined
rg 'getGenerationInstance\s*=' --type ts --type js -B5 -A5Repository: bitcashorg/masterbots
Length of output: 991
🏁 Script executed:
# Check the exact hook definition and its return
rg 'export.*useWorkspaceMedia' --type ts --type js -A30Repository: bitcashorg/masterbots
Length of output: 719
🏁 Script executed:
# Find useImageGeneration hook
fd 'use-image-generation' --type f -e ts -e tsxRepository: bitcashorg/masterbots
Length of output: 156
🏁 Script executed:
# Search for useImageGeneration definition
rg 'export.*useImageGeneration|function useImageGeneration' --type ts --type js -A 20Repository: bitcashorg/masterbots
Length of output: 3504
🏁 Script executed:
# Read the full useImageGeneration hook from pro-web
cat -n apps/pro-web/lib/hooks/use-image-generation.tsx | head -200Repository: bitcashorg/masterbots
Length of output: 6865
🏁 Script executed:
# Look for the return statement of useImageGeneration
rg 'return \{' apps/pro-web/lib/hooks/use-image-generation.tsx -A 30Repository: bitcashorg/masterbots
Length of output: 286
🏁 Script executed:
# Check if the generation instances are wrapped in useMemo in useWorkspaceMedia
rg -n 'mediaGeneration|wordmarkGeneration|symbolGeneration' apps/pro-web/lib/hooks/use-workspace-media.tsx | head -20Repository: bitcashorg/masterbots
Length of output: 1083
Memoize the generation instances to prevent getGenerationInstance from being recreated on every render.
getGenerationInstance is wrapped in useCallback, but its dependency array references mediaGeneration, wordmarkGeneration, and symbolGeneration—three new object instances created on every render via direct useImageGeneration() calls. This causes getGenerationInstance itself to be recreated every render despite useCallback, making the effect at line 334 re-run wastefully. Wrap the three generation instances in useMemo to stabilize the dependency chain.
🤖 Prompt for AI Agents
In `@apps/pro-web/components/routes/chat/prompt-form/index.tsx` around lines 334 -
340, getGenerationInstance is being recreated every render because
mediaGeneration, wordmarkGeneration, and symbolGeneration are new objects each
time (they come from direct useImageGeneration() calls), so memoize those three
instances with useMemo and then keep getGenerationInstance wrapped with
useCallback depending on the memoized values; specifically, replace direct
useImageGeneration() invocations with memoized variables (e.g., const
mediaGeneration = useMemo(() => useImageGeneration(...), [...deps]) or memoize
the returned objects), update getGenerationInstance's dependency array to
reference those memoized symbols (mediaGeneration, wordmarkGeneration,
symbolGeneration), and this will stabilize getGenerationInstance so the effect
that depends on it (the useEffect at the block ending with setInput) no longer
re-runs every render.
310dcdd to
849422a
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@apps/pro-web/components/routes/chat/prompt-form/index.tsx`:
- Around line 593-597: The media submission guard currently checks activeTab ===
'media' and can call handleMediaPromptSubmit even when the workspace is
inactive; change the condition to use the existing boolean isWorkspaceMediaTab
(which is defined as isWorkspaceActive && activeTab === 'media') so media
submissions only run when both the workspace is active and the media tab is
selected, i.e., replace the activeTab === 'media' check with isWorkspaceMediaTab
where handleMediaPromptSubmit(trimmedInput) is invoked.
🧹 Nitpick comments (1)
apps/pro-web/components/routes/chat/prompt-form/index.tsx (1)
320-332: Nit: Use$elprefix for DOM element references.Per the project's DOM naming convention, variables holding DOM element references should be prefixed with
$.Proposed fix
setTimeout(() => { if (!inputRef.current) return - const el = inputRef.current - el.focus() + const $el = inputRef.current + $el.focus() // Move caret to end - el.setSelectionRange(draft.length, draft.length) + $el.setSelectionRange(draft.length, draft.length) // Scroll to bottom - el.scrollTop = el.scrollHeight + $el.scrollTop = $el.scrollHeight }, 0)Based on learnings: "In TSX files, adopt a convention where variables that reference DOM elements are prefixed with '$' (e.g., $questionElement, $el) to quickly identify DOM refs."
| // Media scope: generate or refine image | ||
| if (activeTab === 'media') { | ||
| await handleMediaPromptSubmit(trimmedInput) | ||
| return | ||
| } |
There was a problem hiding this comment.
Use isWorkspaceMediaTab instead of activeTab === 'media' to guard media submission.
Line 169 correctly defines isWorkspaceMediaTab = isWorkspaceActive && activeTab === 'media', but the submission guard on line 594 only checks activeTab === 'media'. If activeTab retains a stale 'media' value while the workspace is inactive, the regular chat submission path is bypassed in favor of handleMediaPromptSubmit.
Proposed fix
// Media scope: generate or refine image
- if (activeTab === 'media') {
+ if (isWorkspaceMediaTab) {
await handleMediaPromptSubmit(trimmedInput)
return
}📝 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.
| // Media scope: generate or refine image | |
| if (activeTab === 'media') { | |
| await handleMediaPromptSubmit(trimmedInput) | |
| return | |
| } | |
| // Media scope: generate or refine image | |
| if (isWorkspaceMediaTab) { | |
| await handleMediaPromptSubmit(trimmedInput) | |
| return | |
| } |
🤖 Prompt for AI Agents
In `@apps/pro-web/components/routes/chat/prompt-form/index.tsx` around lines 593 -
597, The media submission guard currently checks activeTab === 'media' and can
call handleMediaPromptSubmit even when the workspace is inactive; change the
condition to use the existing boolean isWorkspaceMediaTab (which is defined as
isWorkspaceActive && activeTab === 'media') so media submissions only run when
both the workspace is active and the media tab is selected, i.e., replace the
activeTab === 'media' check with isWorkspaceMediaTab where
handleMediaPromptSubmit(trimmedInput) is invoked.
🎬 Video Demo
https://www.loom.com/share/ec78833eff904f8c9a440c49a08d475f
Summary by Sourcery
Introduce a multi-step AI-assisted logo generation flow integrated with the Brand Kit, including a completed wordmark style step, foundational direction/refinement/confirmation steps, and supporting typography and asset infrastructure.
New Features:
Enhancements:
Build:
Summary by CodeRabbit
New Features
Improvements