Skip to content

[pro-web] feat: implement logo generation wizard with multi-step creation flow#601

Open
derianrddev wants to merge 35 commits intodevelopfrom
feat/logo-generator
Open

[pro-web] feat: implement logo generation wizard with multi-step creation flow#601
derianrddev wants to merge 35 commits intodevelopfrom
feat/logo-generator

Conversation

@derianrddev
Copy link
Contributor

@derianrddev derianrddev commented Jan 16, 2026

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

  • Add a dedicated Logo Generation wizard with a four-step flow (style, direction, refine, confirm) accessible from the Brand Kit dialog.
  • Implement a wordmark logo style designer with live preview, typography presets, casing/tracking/weight controls, and font role selection.
  • Enable logo refinement using preloaded brand kit reference assets, prompt generation helpers, and image generation wiring.
  • Add symbol and wordmark style selection screens backed by typed style enums and preset configurations.
  • Provide an image preview modal and logo reference slots for managing and previewing logo reference imagery.

Enhancements:

  • Extend the shared wizard stepper with a logo-generation variant, decorative checkpoint step, and improved responsive layout.
  • Augment workspace media state and actions to track logo generation choices, wordmark presets, and reference assets, and re-export font weight helpers for consumers.
  • Refactor Google Fonts configuration into a brand-kit–specific module with expanded weight support and utilities for mapping semantic wordmark weights.
  • Define strong brand-kit types for symbol/wordmark styles, casing, font weights, and presets, plus wordmark preset and casing option constants.
  • Improve Brand Kit dialog navigation to support switching between the Brand Kit wizard and the Logo Generation wizard entry point.

Build:

  • Add @radix-ui/react-radio-group as a UI dependency for font selection controls.

Summary by CodeRabbit

  • New Features

    • Brand Kit flow: multi-step generator (vibes, fonts, palettes) and logo generator with previews, refinement, download (ZIP) and save.
    • Per-scope image generation: separate media/wordmark/symbol instances with undo/redo, retry, initial-image and scope-aware prompts.
    • New workspace sheet modal and Pro chat controls for improved workspace interactions.
  • Improvements

    • Reference image management, selectable carousels, and snap slider controls.
    • Wordmark live preview (auto-scaling) and improved color-contrast/readability handling.
    • Consistent responsive sizing and UI refinements.

@derianrddev derianrddev self-assigned this Jan 16, 2026
@vercel
Copy link

vercel bot commented Jan 16, 2026

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

Project Deployment Actions Updated (UTC)
masterbots-pro Ready Ready Preview, Comment Feb 13, 2026 6:52pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
masterbots Skipped Skipped Feb 13, 2026 6:52pm

Request Review

@sourcery-ai
Copy link

sourcery-ai bot commented Jan 16, 2026

Reviewer's Guide

Implements 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 transition

sequenceDiagram
  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
Loading

Sequence diagram for Refine Logo step AI generation with reference assets

sequenceDiagram
  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
Loading

Updated class diagram for workspace media state and brand kit logo types

classDiagram
  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
Loading

Flow diagram for logo reference assets and AI generation

flowchart 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
Loading

File-Level Changes

Change Details Files
Extend wizard stepper to support logo generation variant with decorative checkpoint and goal icons plus improved responsive layout.
  • Add a variant prop to distinguish brand kit vs logo generation stepper behavior and visuals
  • Insert a decorative non-clickable "Checkpoint" step for logo generation flows while preserving index mapping
  • Use Goal icon for the final logo-generation step and FlagTriangleRight for the checkpoint/brand kit final step
  • Rework layout with fixed-width step containers, connector lines, and better spacing and accessibility
apps/pro-web/components/routes/workspace/media-tab/ui/brand-kit/wizard/wizard-stepper.tsx
Add logo generation state and actions to workspace media context, including wordmark preset configuration and reference asset management.
  • Introduce LogoStyle and LogoReferenceAsset types and wire them into MediaWorkspaceState
  • Track selected logo style, symbol/wordmark styles, wordmark name, reference assets, and wordmark typography parameters (preset, casing, tracking, weight)
  • Expose setters plus helpers to add/remove reference assets and update wordmark configuration
  • Initialize sensible defaults for logo-related state and re-export font weight helpers for use in UI components
apps/pro-web/lib/hooks/use-workspace-media.tsx
Refactor BrandKit dialog into separate Brand Kit and Logo Generation wizards with navigation between entry, checkpoint, and logo flows.
  • Expand BrandKitDialog view state to include distinct brandKit and logoGeneration views and adjust subtitle logic
  • Route "New Brand" into the BrandKitWizard and "Generate Logo" into the LogoGenerationWizard
  • Change checkpoint step to call an onGenerateLogo callback instead of advancing the generic wizard, saving brand kit first when possible
  • Simplify BrandKitWizard to always advance internally while delegating logo-generation transition to its parent
apps/pro-web/components/routes/workspace/media-tab/ui/brand-kit/brand-kit-dialog.tsx
apps/pro-web/components/routes/workspace/media-tab/ui/brand-kit/wizard/steps/checkpoint-step.tsx
apps/pro-web/components/routes/workspace/media-tab/ui/brand-kit/wizard/brand-kit-wizard.tsx
apps/pro-web/components/ui/wizard/index.tsx
Introduce a Logo Generation wizard with four steps and shared stepper integration.
  • Create LogoGenerationWizard wrapper that uses useWizard, plugs into the shared WizardStepper with logoGeneration variant, and handles finish/cancel behavior
  • Wire steps for style selection, direction, refinement, and confirmation into a single flow
apps/pro-web/components/routes/workspace/media-tab/ui/brand-kit/logo-generation/logo-generation-wizard.tsx
Implement a full-featured Logo Style step with wordmark presets, live preview, typography controls, and font-weight availability logic.
  • Build LogoStyleStep UI with main preview card, name input, font-role radio selection, casing controls, tracking slider, and weight slider
  • Integrate WORDMARK_PRESETS and CASING_OPTIONS to drive presets, enforce fixed/free casing rules, and apply preset-specific tracking/weight defaults
  • Use getAvailableWordmarkWeights and getFallbackWeight to constrain weights to those supported by the chosen Google font family
  • Persist selections into workspace media state (logo style, name, preset, casing, tracking, weight) and advance the wizard once a valid name is set
apps/pro-web/components/routes/workspace/media-tab/ui/brand-kit/logo-generation/steps/logo-style-step.tsx
apps/pro-web/lib/constants/brand-kit/wordmark.ts
apps/pro-web/types/brand-kit.types.ts
Add reusable wordmark preview, snap-slider, and radio-group UI primitives to support interactive logo-typography editing.
  • Create WordmarkPreview component that applies casing (including small-caps), tracking, weight, and auto-scales font size using canvas measurement and ResizeObserver
  • Add SnapSlider component for discrete option selection with keyboard support, drag interaction, and labeled extremes/icons
  • Introduce a Radix-based RadioGroup and RadioGroupItem wrapper for choosing between primary and secondary fonts
apps/pro-web/components/routes/workspace/media-tab/ui/brand-kit/logo-generation/wordmark-preview.tsx
apps/pro-web/components/routes/workspace/media-tab/ui/brand-kit/logo-generation/snap-slider.tsx
apps/pro-web/components/ui/radio-group.tsx
Add Google Fonts constants and helpers for wordmark typography, including per-family weight availability and semantic-to-numeric weight mapping.
  • Move and significantly expand Google Fonts definitions into a brand-kit–scoped module with 80+ families and full weight ranges
  • Expose FONT_WEIGHTS_BY_FAMILY, a GOOGLE_FONTS map, and type aliases for font family/weights
  • Map semantic WordmarkFontWeight values to numeric CSS weights and back
  • Provide getAvailableWordmarkWeights and getFallbackWeight helpers for filtering and choosing valid weights per family
  • Remove the old google-fonts constants file in favor of the new brand-kit namespace
apps/pro-web/lib/constants/brand-kit/google-fonts.ts
apps/pro-web/lib/constants/google-fonts.ts
Add reference-asset infrastructure (checksums, preload helpers, and UI slots) to feed logo generation with precomputed image references.
  • Introduce BRAND_KIT_CHECKSUMS and getBrandKitChecksum to map static font/palette/wordmark/symbol images to precomputed checksums
  • Add helpers to preload LogoReferenceAsset instances from current brand-kit selections and to derive reference checksums without uploads
  • Create LogoReferenceSlot UI for dropzone/file-input based logo reference tiles with thumbnail preview/removal and drag-and-drop handling
  • Implement an ImagePreviewModal that overlays a large preview on top of Radix dialogs while carefully intercepting events
apps/pro-web/lib/constants/brand-kit/brand-kit-checksums.ts
apps/pro-web/lib/helpers/logo-reference-assets.ts
apps/pro-web/components/routes/workspace/media-tab/ui/brand-kit/logo-generation/logo-reference-slot.tsx
apps/pro-web/components/routes/workspace/media-tab/ui/brand-kit/logo-generation/image-preview-modal.tsx
Implement a Refine Logo step that builds an AI prompt from brand-kit selections, wires in reference assets/checksums, and triggers image generation.
  • Preload logo reference assets into workspace media when the step mounts based on selected fonts, palette, logo style, and style variants
  • Auto-generate a prompt via buildLogoPrompt while allowing users to edit it (tracked via a dirty flag)
  • Use getLogoReferenceChecksums and useImageGeneration to generate a new logo image with reference checksums and a fixed 1:1 aspect ratio, handling loading and error states
  • Render four LogoReferenceSlot tiles, a preview pane for the generated image, and a prompt textarea with submit button
apps/pro-web/components/routes/workspace/media-tab/ui/brand-kit/logo-generation/steps/refine-logo-step.tsx
apps/pro-web/lib/helpers/logo-prompt.ts
Implement a Logo Direction step and a stub Confirm step for selecting high-level logo layout and finalizing generation.
  • Add LogoDirectionStep that conditionally displays symbol or wordmark style options backed by static preview images and persists the selected style in workspace media
  • Include LogoConfirmStep placeholder that provides basic confirmation UI and a stubbed generate handler
  • Define SymbolStyle and WordmarkStyle enumerations consistent with available preview assets
apps/pro-web/components/routes/workspace/media-tab/ui/brand-kit/logo-generation/steps/logo-direction-step.tsx
apps/pro-web/components/routes/workspace/media-tab/ui/brand-kit/logo-generation/steps/logo-confirm-step.tsx
apps/pro-web/types/brand-kit.types.ts
Wire Radix radio-group dependency for new typography controls.
  • Add @radix-ui/react-radio-group to the pro-web app dependencies
apps/pro-web/package.json

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 16, 2026

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Summary
Organization API
apps/pro-web/app/api/organizations/[id]/route.ts
PATCH accepts logoAssets?: LogoAssetsInput and brandKit?: Partial<BrandKitData, validates/merges brandKit, uploads assets via uploadLogoAssetToGCS, persists merged brandKit, and returns transformed org data.
Media templates route
apps/pro-web/app/api/media/templates/route.ts
Updated import path for parseTemplateFilename to .../media-tab/template.
Brand‑kit UI & wizards
apps/pro-web/components/routes/workspace/media-tab/brand-kit/...
Adds BrandKitFlow, BrandKitGenerator/LogoGenerator wizards, many wizard steps (vibe, fonts, colors, checkpoint, refine, confirm, etc.), and shared UI primitives (DualActionCard, SnapSlider, SelectableCarousel, WizardStepper, WordmarkPreview).
Chat & prompt UI
apps/pro-web/components/routes/chat/prompt-form/index.tsx, .../pro/chat-panel-pro-controls.tsx, .../pro/chat-panel-pro.tsx
PromptForm extended for multi‑scope generation (media/wordmark/symbol) with per‑scope handlers; adds ChatPanelProControls and refactors ChatPanelPro to provider pattern.
Workspace media hook & context
apps/pro-web/lib/hooks/use-workspace-media.tsx, apps/pro-web/lib/contexts/chat-panel-pro-context.tsx
Major API expansion: per‑scope reference images, multi‑instance generation (media/wordmark/symbol) with getGenerationInstance, scope-aware prompt handlers, saveBrandKit/saveLogoAssets mutations, palette shuffling, and reference image management APIs.
Logo asset helpers (server & client)
apps/pro-web/lib/helpers/workspace/media-tab/logo-assets.server.ts, .../logo-assets.client.ts
Server: uploadLogoAssetToGCS (base64 decode, SHA‑256 dedupe, GCS upload, signed URL); Client: fetchSignedUrlAsBase64.
Logo prompts & reference mapping
apps/pro-web/lib/helpers/workspace/media-tab/logo-prompt.ts, .../logo-reference-image-mapper.ts
Adds buildLogoBasePrompt, buildRefineLogoPrompt, and utilities to resolve logo reference specs to ReferenceImagePayloads via precomputed checksums.
Brand‑kit constants & types
apps/pro-web/types/brand-kit.types.ts, apps/pro-web/types/media.types.ts, apps/pro-web/lib/constants/brand-kit/*
Adds typed models, constants (PALETTES, VIBES, SYMBOL_OPTIONS, font options, WORDMARK_PRESETS, prompts), font-weight helpers, and generated BRAND_KIT_CHECKSUMS.
Image helpers & generation
apps/pro-web/lib/helpers/ai-image.ts, apps/pro-web/lib/helpers/color-utils.ts, apps/pro-web/lib/hooks/use-image-generation.tsx
Adds base64/URL/ZIP helpers, WCAG color utilities, permutations helper, and extends image-generation hook with history (undo/redo), retry, addInitialImage.
Reference images & sidebar
apps/pro-web/components/routes/workspace/media-tab/reference-images/*, .../media-sidebar.tsx, media-canvas.tsx, media-workspace.tsx, wizards/media-wizards.tsx
ReferenceImagesPanel and ReferenceUploadSlot converted to prop-driven components; media workspace wired to scope-aware handlers; minor sizing/class token refactors.
UI primitives & layout
apps/pro-web/components/ui/radio-group.tsx, apps/pro-web/components/ui/sheet.tsx, apps/pro-web/components/ui/workspace-sheet/index.tsx, apps/pro-web/components/ui/workspace-dialog/*
Adds Radix-based RadioGroup, SheetContent gains optional container prop, WorkspaceSheet replaces removed WorkspaceDialog and provides panel slot and image-zoom escape handling.
ImageDisplay & types
apps/pro-web/components/shared/image-display.tsx, apps/pro-web/types/index.ts
ImageDisplay props: showModelLabel, showDownloadButton, labelText; types updated to expose image-generation history/retry APIs (undo/redo/retry/addInitialImage).
Styles & build
apps/pro-web/app/globals.css, apps/pro-web/package.json, apps/pro-web/scripts/upload-brand-kit-assets.ts
Radix ScrollArea overflow fix; package additions (@radix-ui/react-radio-group, jszip) and upload-brand-kit-assets.ts script that uploads assets and emits brand-kit-checksums.ts.

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
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested labels

difficulty:hard

Suggested reviewers

  • AndlerRL

"I’m a rabbit who paints with font and hue,
I hop through wizards, prompts, checksums too.
I stash logos in clouds with a tiny cheer,
Zip colors and prompts — brand magic’s here! 🐇✨"

🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely summarizes the main feature: implementing a logo generation wizard with a multi-step creation flow, which aligns with the extensive changes throughout the codebase.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into develop

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/logo-generator

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@vercel vercel bot temporarily deployed to Preview – masterbots January 16, 2026 16:56 Inactive
@vercel vercel bot temporarily deployed to Preview – masterbots January 20, 2026 19:15 Inactive
@vercel vercel bot temporarily deployed to Preview – masterbots January 21, 2026 22:34 Inactive
@vercel vercel bot temporarily deployed to Preview – masterbots January 22, 2026 21:34 Inactive
@vercel vercel bot temporarily deployed to Preview – masterbots January 24, 2026 02:29 Inactive
@vercel vercel bot temporarily deployed to Preview – masterbots January 26, 2026 22:45 Inactive
@vercel vercel bot temporarily deployed to Preview – masterbots January 28, 2026 22:08 Inactive
@vercel vercel bot temporarily deployed to Preview – masterbots January 29, 2026 16:52 Inactive
@vercel vercel bot temporarily deployed to Preview – masterbots January 29, 2026 20:15 Inactive
@vercel vercel bot temporarily deployed to Preview – masterbots January 29, 2026 22:12 Inactive
@vercel vercel bot temporarily deployed to Preview – masterbots February 3, 2026 22:31 Inactive
@vercel vercel bot temporarily deployed to Preview – masterbots February 5, 2026 01:11 Inactive
Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Sorry @derianrddev, your pull request is larger than the review limit of 150000 diff characters

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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 | 🟡 Minor

Library image selection is a no-op — the selected image is never added.

The onImageSelect callback checks reference image count but never calls addReferenceImage. 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 | 🟡 Minor

Remove unused props from ChatPanelProProps interface 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 to ChatPanelProControls. 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 | 🟠 Major

No guard against concurrent generateImage calls.

isLoading is set inside the try-block but never checked at entry. If the caller invokes generateImage twice (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 | 🟡 Minor

Silent 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

hexToRgb doesn't validate input — may produce NaN on malformed hex strings.

If hex is empty, has fewer than 6 characters, or contains non-hex characters, Number.parseInt returns NaN and 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

handleDownloadAll has no error handling.

imageHelpers.downloadZip can 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.

WordmarkCasing includes 'small-caps' but the casingMap only covers three of the four variants. The || wordmarkCasing fallback will emit the raw slug small-caps into 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 | 🟡 Minor

Remove debug console.log statements 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 | 🟡 Minor

Minor 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 | 🟡 Minor

Hardcoded 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., #F97316 from "Peach Charcoal"), the contrast ratio against white could dip below AA thresholds. Since you already have gentlyAdjustForAA available, 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 | 🟡 Minor

Verify source PNG files for all-caps-tight and small-caps wordmark 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. Check public/brand-kit/wordmark/ to confirm whether small-caps.png is missing, identical to all-caps-tight.png, or corrupted. Run bun run upload-brand-kit-assets to regenerate checksums after correcting the source files.

apps/pro-web/lib/constants/brand-kit/wordmark-options.ts-4-151 (1)

4-151: ⚠️ Potential issue | 🟡 Minor

Add a comment explaining why 'color-system-set' is excluded from WORDMARK_PRESETS.

'color-system-set' is defined in WordmarkStyle and included in WORDMARK_PROMPTS (which uses Record<WordmarkStyle, ...>), but omitted from WORDMARK_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 | 🟡 Minor

Comment 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.9 reserves 10% total (≈5% per side). Either update the comment or change the multiplier to 0.8 if 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 | 🟡 Minor

Dead guard: resolvedName can never be falsy.

resolvedName always falls back to 'Your Brand' (line 166), so the if (!resolvedName) return on line 168 never triggers. Similarly, the disabled={!effectiveName} on line 490 is always false since effectiveName has 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 | 🟡 Minor

Header 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 | 🟡 Minor

Empty options array causes label rendering errors and invalid ARIA state.

When options is empty: options[0] and options[options.length - 1] are undefined, formatLabel receives undefined, and selectedIndex is -1 (invalid for aria-valuenow). While isDisabled guards 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 | 🟡 Minor

Add defensive check for empty allowedWeights arrays.

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 passing undefined to setWordmarkFontWeight or WordmarkPreview.weight if a preset is added with an empty allowedWeights array. 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 | 🟡 Minor

Symbol 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 | 🟡 Minor

Typo: "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 messages
apps/pro-web/scripts/upload-brand-kit-assets.ts-51-62 (1)

51-62: ⚠️ Potential issue | 🟡 Minor

Partial 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.ts will silently omit that file's checksum. Downstream code using getBrandKitChecksum will get undefined with 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

vibeDescriptionText produces awkward phrasing for a single vibe part.

When there's only one part after splitting, the .map on 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 | 🟡 Minor

Wrap the motion.div with <AnimatePresence> to enable the exit animation.

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 of AnimatePresence and is removed from the React tree. Currently, the exit prop is dead code. Import AnimatePresence from framer-motion and 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

getFallbackWeight terminal fallback 'regular' may not be available for the font.

Line 669: availableWeights[0] || 'regular' — if availableWeights is empty (no intersection between the font's supported weights and preset's allowedWeights), 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.warn when 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, generatePermutations on 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 in packages/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 to apps/pro-web/components/ui/.

Also, since the project uses React 19.2, forwardRef is no longer necessary — ref can 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.displayName

As 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 fetch will 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: base64ToUint8Array duplicates the core logic of base64ToBlob.

Lines 184-197 are almost identical to lines 13-23 in base64ToBlob. Consider refactoring base64ToBlob to delegate to base64ToUint8Array:

♻️ 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: downloadBlob duplicates the download path in shareOrDownload (lines 79-96).

The download logic in shareOrDownload is essentially the same as downloadBlob. Consider refactoring shareOrDownload to delegate to downloadBlob for the download fallback. Also, downloadBlob lacks the try/catch that shareOrDownload has — 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 or AbortController. 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 no aria-pressed or aria-selected attribute. 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.

workspaceContainer and zoomOverlay (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: maxWidth is declared in the interface but never used.

The maxWidth prop is defined in WorkspaceDialogProps but 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: logoStyleMap is an identity mapping — the values are the same as the keys.

You could simplify by using selectedLogoStyle directly, removing the unnecessary map and the logoStyleText variable.

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 check c.color !== 'undefined' guards against a stringified undefined, which hints at an upstream bug.

If c.color can end up as the literal string 'undefined', that likely means somewhere upstream String(undefined) or template-literal interpolation is occurring. This filter is a reasonable safeguard, but consider also checking c.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: stylesThatAllowInitial is 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: useCallback for getGenerationInstance is 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, making useCallback a no-op. Consider using useRef to 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: saveLogoAssetsMutation duplicates the IDB update logic from saveBrandKitMutation.

Lines 664–691 are nearly identical to lines 588–611 (fetch workspace data from IDB, extract brandKit from 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: handleWordmarkPromptSubmit and handleSymbolPromptSubmit are nearly identical.

Both follow the same pattern: get scope-specific reference images → resolve checksums → check for existing image → call generateImage with '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: generatePermutations produces n! 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, shuffleSelectedPaletteOrder reads selectedPalette and shuffleState from closure but uses setShuffleState/setSelectedPalette/setColorRoleItems imperatively. The deps [selectedPalette, shuffleState] are correct, but consider using functional updaters for setSelectedPalette if there's any risk of stale closures during rapid shuffles.


48-208: The MediaWorkspaceState + MediaWorkspaceActions interfaces 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: useChatPanel is 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, and onRemove create new references every render, which can cause unnecessary re-renders of ReferenceImagesPanel. Since they only partially apply the 'media' scope, consider extracting them with useCallback.

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: Unsafe as FontFamily type assertion.

selectedFonts[0].family is typed as string, and casting it to FontFamily bypasses compile-time checking. If a font family stored in context doesn't match a GOOGLE_FONTS key, 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)
+	: primaryFontClass
apps/pro-web/components/routes/workspace/media-tab/brand-kit/logo-generator/steps/symbol-style-step.tsx (1)

19-23: Remove console.log debug 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: Move weightClassMap outside 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.current is captured at effect-run time (line 62) but may be null by cleanup time if the component unmounts mid-cycle. While resizeObserver.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.log statements (lines 61, 66, 92–94, 125) with emoji prefixes are useful during development but will be noisy in production. A lightweight debug utility or process.env.NODE_ENV guard 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 event close-image-zoom lacks type safety.

The close-image-zoom event is wired via window.addEventListener / removeEventListener using 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 / mouseup handle desktop dragging, but there are no corresponding touchstart / touchmove / touchend handlers. 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: Hardcoded modelId: 'gemini-3-pro-image-preview' for restored logos.

This fabricates a GeneratedImage with a model ID that may not correspond to the actual model used to generate the saved logo. If downstream code uses modelId for 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 new Storage client 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 url fields 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.scrollHeight

Based 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 number 3 for brand-kit generator checkpoint step.

The hardcoded step index 3 is 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: casing typed as string instead of WordmarkCasing.

Both WordmarkLogoAsset.casing and WordmarkLogoAssetInput.casing are typed as string, but a WordmarkCasing union type exists in brand-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: normalizeFontFileName and normalizePaletteFileName are 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, and getLogoReferenceChecksums all 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

Comment on lines +111 to +142
// 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 },
)
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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, uploadLogoAssetToGCSdecodeBase64ToBuffer 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.

Comment on lines +518 to +537

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
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

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

Comment on lines +122 to +131
const chatId = useMemo(() => {
return params.threadId || mbChatState.isNewChat
? mbChatState.newChatThreadId
: activeThread?.threadId
}, [
activeThread?.threadId,
params,
mbChatState.isNewChat,
mbChatState.newChatThreadId,
])
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 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 -100

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

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

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

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

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

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

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

Repository: bitcashorg/masterbots

Length of output: 219


Critical operator precedence bug in chatIdparams.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.

Comment on lines +217 to +281
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])
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +91 to +146
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,
])
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

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.

Comment on lines +105 to +124
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 }
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +164 to +172
//* 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)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@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.

onConvertToDocument is 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 for ChatPanelProControls, 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: Avoid any for chatOptions parameter.

The biome-ignore suppresses the lint warning, but ChatRequestOptions (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: handleContinueGeneration is recreated every render — consider useCallback.

This async handler is passed to a Button's onClick. Without useCallback, 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. !chatbot is 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) and saveLogoAssets (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 LogoAssetsPayload in the interface, mutation, and callback.


629-702: Near-identical mutation logic duplicated from saveBrandKitMutation.

saveLogoAssetsMutation (lines 629–702) mirrors saveBrandKitMutation (lines 567–626) almost line-for-line — same endpoint, same IDB sync, same invalidation. Only the request body key differs (brandKit vs logoAssets).

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: handleWordmarkPromptSubmit and handleSymbolPromptSubmit are 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/generatedImage update 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 with useMemo.

Comment on lines +297 to +310
// 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],
)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the useImageGeneration hook implementation
fd "use-image-generation" --type f -e ts -e tsx

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

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

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

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

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

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

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

Comment on lines +759 to +817
// 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])
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

@vercel vercel bot temporarily deployed to Preview – masterbots February 13, 2026 16:14 Inactive
"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"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

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 | 🟠 Major

Async submission handlers lack error handling — unhandled rejections possible.

handleWordmarkPromptSubmit, handleSymbolPromptSubmit, handleMediaPromptSubmit, and onSubmit are all awaited but none are wrapped in try/catch. If any of these reject, the promise rejection goes unhandled. Combined with the premature setInput(''), the user loses their input with no feedback.

Wrap the async calls (or the entire function body after validation) in a try/catch that shows an error via customSonner and 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: el should be $el to 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."

Comment on lines +178 to +179
const isBrandKitInputDisabled =
isBrandKitOpen && !isWordmarkScope && !isSymbolScope
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the file in question to understand the context better
fd 'prompt-form' --type f -e tsx | head -5

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

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

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

Repository: bitcashorg/masterbots

Length of output: 92


🏁 Script executed:

# Find the file directly
find . -path "*prompt-form/index.tsx" -type f

Repository: bitcashorg/masterbots

Length of output: 180


🏁 Script executed:

# Search for activeGenerationScope without file type specification
rg -n 'activeGenerationScope' -B 2 -A 2

Repository: bitcashorg/masterbots

Length of output: 3486


🏁 Script executed:

# Search for isBrandKitOpen assignments
rg -n 'isBrandKitOpen.*=' -B 1 -A 1 | head -100

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

Repository: bitcashorg/masterbots

Length of output: 3892


🏁 Script executed:

# Search for setActiveGenerationScope calls to understand when scope changes
rg -n 'setActiveGenerationScope' -B 3 -A 3

Repository: bitcashorg/masterbots

Length of output: 4088


🏁 Script executed:

# Look at the BrandKitFlow and how it manages state
rg -n 'BrandKitFlow' -B 2 -A 10

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

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

Repository: bitcashorg/masterbots

Length of output: 1948


🏁 Script executed:

# Check if activeGenerationScope changes based on isBrandKitOpen
rg -n 'isBrandKitOpen' -B 5 -A 5

Repository: bitcashorg/masterbots

Length of output: 10744


🏁 Script executed:

# Search for where brand kit state might affect activeGenerationScope
rg -n 'setIsBrandKitOpen' -B 5 -A 5

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

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

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

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

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

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

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

Comment on lines +334 to +340
}, [
isWorkspaceActive,
activeGenerationScope,
selectedSymbolStyle,
getGenerationInstance,
setInput,
])
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find the useWorkspaceMedia hook definition
fd 'use-workspace-media' --type f -e ts -e tsx

Repository: bitcashorg/masterbots

Length of output: 111


🏁 Script executed:

# Also search for useWorkspaceMedia in the codebase
rg 'useWorkspaceMedia' --type ts --type tsx -l

Repository: bitcashorg/masterbots

Length of output: 92


🏁 Script executed:

# Search for getGenerationInstance declarations and usage
rg 'getGenerationInstance' --type ts --type tsx -B2 -A2 | head -150

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

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

Repository: bitcashorg/masterbots

Length of output: 991


🏁 Script executed:

# Check the exact hook definition and its return
rg 'export.*useWorkspaceMedia' --type ts --type js -A30

Repository: bitcashorg/masterbots

Length of output: 719


🏁 Script executed:

# Find useImageGeneration hook
fd 'use-image-generation' --type f -e ts -e tsx

Repository: bitcashorg/masterbots

Length of output: 156


🏁 Script executed:

# Search for useImageGeneration definition
rg 'export.*useImageGeneration|function useImageGeneration' --type ts --type js -A 20

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

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

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

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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 $el prefix 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."

Comment on lines +593 to 597
// Media scope: generate or refine image
if (activeTab === 'media') {
await handleMediaPromptSubmit(trimmedInput)
return
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant