nexus/.planning/phases/42-wallpapers-social-format-conversion-voice/42-06-PLAN.md

12 KiB

phase plan type wave depends_on files_modified autonomous requirements must_haves
42-wallpapers-social-format-conversion-voice 06 execute 3
42-03
ui/src/pages/ConvertPage.tsx
ui/src/components/ConvertPanel.tsx
ui/src/api/convert.ts
ui/src/App.tsx
true
CONV-01
CONV-02
CONV-03
CONV-04
CONV-05
CONV-06
CONV-07
CONV-08
CONV-09
truths artifacts key_links
User can drag-drop a file, select a target format, and download the converted file
Format chips are grouped by Images, Audio/Video, Documents, Data
All format pairs are selectable — unavailable direct converters show AI fallback notice
Deep-link /convert/png/svg pre-selects PNG source and SVG target on mount
MIME validation error shows inline when magic bytes mismatch extension
Drag-drop zone shows correct states (idle, dragover, error)
Convert button is disabled until both source file and target format are selected
path provides contains
ui/src/pages/ConvertPage.tsx Standalone /convert page with deep-link routing sourceFormat
path provides contains
ui/src/components/ConvertPanel.tsx Drag-drop zone + format chips + convert action bar ConvertSourceZone
path provides exports
ui/src/api/convert.ts submitConvertJob (multipart), getConverterCapabilities
submitConvertJob
getConverterCapabilities
path provides contains
ui/src/App.tsx Routes for /convert, /convert/:sourceFormat, /convert/:sourceFormat/:targetFormat ConvertPage
from to via pattern
ui/src/pages/ConvertPage.tsx ui/src/components/ConvertPanel.tsx renders ConvertPanel with route params ConvertPanel
from to via pattern
ui/src/components/ConvertPanel.tsx ui/src/api/convert.ts submitConvertJob for multipart upload submitConvertJob
from to via pattern
ui/src/api/convert.ts /api/companies/:companyId/convert FormData multipart POST FormData
from to via pattern
ui/src/App.tsx ui/src/pages/ConvertPage.tsx Route path convert convert
Build the format conversion UI page with drag-drop upload, grouped format chips, AI fallback indicator, deep-link routing, and inline MIME validation errors.

Purpose: This surfaces the conversion renderer (Plan 03) to users. It fulfills the UI side of all CONV requirements. Output: ConvertPage, ConvertPanel, convert API client, routes in App.tsx.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/42-wallpapers-social-format-conversion-voice/42-RESEARCH.md @.planning/phases/42-wallpapers-social-format-conversion-voice/42-UI-SPEC.md @.planning/phases/42-wallpapers-social-format-conversion-voice/42-03-SUMMARY.md

@ui/src/App.tsx @ui/src/api/contentJobs.ts @ui/src/api/client.ts @ui/src/hooks/useContentJob.ts @ui/src/components/ChatFileDropZone.tsx

From server/src/routes/convert.ts (Plan 03): ``` POST /api/companies/:companyId/convert — multipart/form-data with fields: file, targetFormat Returns: 202 { jobId, status: "queued" } Error: 422 { error, actualMime, claimedMime }

GET /api/system/converters Returns: { imageConverter: boolean, audioVideoConverter: boolean, docConverter: boolean, dataConverter: boolean }


From server/src/services/renderers/types.ts:
```typescript
interface ConvertBundle {
  type: "convert-bundle";
  outputFilename: string;
  outputMime: string;
  outputBase64: string;
  method: "direct" | "ai-bridge";
}
Task 1: Create convert API client and ConvertPanel component ui/src/api/convert.ts, ui/src/components/ConvertPanel.tsx ui/src/api/contentJobs.ts, ui/src/api/client.ts, ui/src/components/ChatFileDropZone.tsx, ui/src/hooks/useContentJob.ts 1. Create ui/src/api/convert.ts: - Export async function submitConvertJob(companyId: string, file: File, targetFormat: string): Promise<{ jobId: string; status: string } | { error: string; actualMime: string; claimedMime: string }> - Build FormData with file and targetFormat - POST to /api/companies/{companyId}/convert with FormData (do NOT set Content-Type header — browser sets multipart boundary) - If response status === 422: return the MIME validation error body - If response status === 202: return { jobId, status } - Throw on other errors - Export async function getConverterCapabilities(): Promise<{ imageConverter: boolean; audioVideoConverter: boolean; docConverter: boolean; dataConverter: boolean }> - GET /api/system/converters - Return JSON body
  1. Create ui/src/components/ConvertPanel.tsx — this is a large component containing three visual zones:

    FORMAT_GROUPS constant (define at top of file):

    const FORMAT_GROUPS = {
      Images: ["png", "jpg", "svg", "webp", "gif"],
      "Audio/Video": ["mp3", "mp4", "wav", "ogg", "webm"],
      Documents: ["md", "html", "pdf", "docx"],
      Data: ["csv", "json", "xlsx"],
    };
    

    Props: { initialSourceFormat?: string; initialTargetFormat?: string; companyId: string }

    State:

    • file: File | null
    • targetFormat: string | null (selected chip)
    • mimeError: { actualMime: string; claimedMime: string } | null
    • dragOver: boolean
    • capabilities: from getConverterCapabilities() (fetch on mount)
    • Use useContentJob(companyId) for job tracking after submit

    ConvertSourceZone (left column):

    • Drag-drop zone: min-h-[120px], p-6, dashed border (2px dashed var(--border))
    • role="button", tabIndex={0}, aria-label="Upload file - drop here or press Enter to browse"
    • onKeyDown: Enter/Space triggers hidden file input click
    • onDragOver: set dragOver=true, prevent default
    • onDragLeave: set dragOver=false
    • onDrop: extract file, set dragOver=false
    • Idle state: bg-secondary border, copy "Drop a file here or click to browse" (or "Drop a {FORMAT} file here or click to browse" if initialSourceFormat provided)
    • Dragover state: bg-accent/30, border-primary, copy "Release to select"
    • After file selected: show filename (text-sm font-medium), file size (text-xs text-muted-foreground), detected MIME (text-xs font-mono text-muted-foreground)
    • MIME error state: bg-destructive/10, border-destructive, error copy: "File extension does not match content. Got {actualMime}, expected {claimedMime}."
    • Click anywhere in zone to re-select file

    ConvertTargetSelector (right column):

    • Render FORMAT_GROUPS as sections. Each group has a label (text-sm font-medium) and a row of chips
    • Each chip: rounded-full px-3 py-1 text-xs font-medium
    • Idle: bg-muted text-muted-foreground. Selected: bg-primary text-primary-foreground
    • All chips are always selectable — never disabled (CONV-08: unavailable direct paths fall to AI bridge)
    • role="radiogroup" on container, role="radio" + aria-checked on each chip
    • When selected pair has no direct converter (check capabilities: e.g. Documents group when docConverter===false), show AI fallback notice below chips: "No direct converter for this pair - AI bridge will be used." (text-xs text-muted-foreground, Info icon 14px, bg-secondary rounded-md p-3)

    ConvertActionBar (full width below):

    • "Convert File" Button (primary, disabled until file !== null AND targetFormat !== null)
    • On click: call submitConvertJob. If result has "error" key: set mimeError. If result has "jobId": track via useContentJob SSE.
    • Progress bar below button (same SSE pattern)
    • After job done with ConvertBundle: "Download {outputFilename}" Button (primary) — create blob from outputBase64, trigger download
    • Error state: "Conversion failed - {detail}. Try again."
    • Empty state: heading "No conversion yet", body "Upload a file and choose a target format to convert."

    Layout: Two-column on desktop (grid grid-cols-1 md:grid-cols-2 gap-6), single column on mobile.

    prefers-reduced-motion: Disable drag-drop zone color transition.

    Initial format pre-selection: If initialSourceFormat or initialTargetFormat props provided, pre-select the corresponding chip (case-insensitive normalize per Pitfall 6). cd /opt/nexus/ui && npx tsc --noEmit 2>&1 | head -20 <acceptance_criteria>

    • grep "submitConvertJob" ui/src/api/convert.ts
    • grep "getConverterCapabilities" ui/src/api/convert.ts
    • grep "FormData" ui/src/api/convert.ts
    • grep "FORMAT_GROUPS" ui/src/components/ConvertPanel.tsx
    • grep "ConvertSourceZone|drag" ui/src/components/ConvertPanel.tsx
    • grep "role="radiogroup"" ui/src/components/ConvertPanel.tsx
    • grep "AI bridge" ui/src/components/ConvertPanel.tsx
    • grep "Convert File" ui/src/components/ConvertPanel.tsx
    • grep "actualMime" ui/src/components/ConvertPanel.tsx
    • grep "initialSourceFormat" ui/src/components/ConvertPanel.tsx </acceptance_criteria> Convert API client handles multipart upload and MIME errors. ConvertPanel has drag-drop zone, grouped format chips, AI fallback notice, and download.
Task 2: Create ConvertPage and wire routes in App.tsx ui/src/pages/ConvertPage.tsx, ui/src/App.tsx ui/src/App.tsx, ui/src/pages/ContentStudio.tsx 1. Create ui/src/pages/ConvertPage.tsx: - Read sourceFormat and targetFormat from URL params using useParams() - Normalize params to lowercase (Pitfall 6: case-insensitive) - If params are invalid format strings not in FORMAT_GROUPS values, silently ignore (render default state) - Get companyId from context (same pattern as ContentStudio.tsx) - Render page heading "Convert File" (h1, heading style from UI spec) - Render ConvertPanel with { initialSourceFormat, initialTargetFormat, companyId }
  1. Update ui/src/App.tsx:
    • Add lazy import for ConvertPage:
      const ConvertPage = lazy(() => import("./pages/ConvertPage").then(m => ({ default: m.ConvertPage })));
      
    • Add three routes inside boardRoutes() function, after the content-studio route:
      <Route path="convert" element={<ConvertPage />} />
      <Route path="convert/:sourceFormat" element={<ConvertPage />} />
      <Route path="convert/:sourceFormat/:targetFormat" element={<ConvertPage />} />
      

This gives deep-link support: /convert/png/svg pre-selects PNG as source filter and SVG as target chip. cd /opt/nexus/ui && npx tsc --noEmit 2>&1 | head -20 <acceptance_criteria> - grep "ConvertPage" ui/src/pages/ConvertPage.tsx - grep "sourceFormat" ui/src/pages/ConvertPage.tsx - grep "targetFormat" ui/src/pages/ConvertPage.tsx - grep "toLowerCase" ui/src/pages/ConvertPage.tsx - grep "ConvertPage" ui/src/App.tsx - grep "convert/:sourceFormat/:targetFormat" ui/src/App.tsx - grep "convert/:sourceFormat" ui/src/App.tsx </acceptance_criteria> ConvertPage reads URL params for deep-link pre-selection. Three route variants wired in App.tsx. Format params normalized case-insensitively.

- `cd /opt/nexus/ui && npx tsc --noEmit` passes - /convert route renders ConvertPage with ConvertPanel - /convert/png/svg pre-selects PNG and SVG - All format chips are always selectable (never disabled) - AI fallback notice shows for pairs without direct converter

<success_criteria>

  • Drag-drop upload with correct idle/dragover/error states
  • Format chips grouped by category, all selectable
  • AI fallback notice for unavailable direct paths
  • Deep-link routing with case-insensitive format params
  • MIME validation error shown inline on magic-byte mismatch
  • Download button after successful conversion
  • tsc compiles cleanly </success_criteria>
After completion, create `.planning/phases/42-wallpapers-social-format-conversion-voice/42-06-SUMMARY.md`