12 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 42-wallpapers-social-format-conversion-voice | 06 | execute | 3 |
|
|
true |
|
|
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
-
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.
- 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 />} />
- Add lazy import for 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>