nexus/.planning/phases/44-video-presentations/44-03-PLAN.md

265 lines
11 KiB
Markdown

---
phase: 44-video-presentations
plan: 03
type: execute
wave: 2
depends_on: ["44-01"]
files_modified:
- ui/src/hooks/useContentJob.ts
- ui/src/components/PresentationPanel.tsx
- ui/src/pages/ContentStudio.tsx
autonomous: true
requirements:
- PRES-01
- PRES-02
- PRES-03
- PRES-04
must_haves:
truths:
- "User can enter a prompt and select pitch-deck or demo-video type to generate a presentation"
- "Progress bar updates in real-time during Remotion render via SSE content_job.progress events"
- "Completed presentation shows an MP4 video player and download button"
- "ContentStudio has a Presentations tab that renders PresentationPanel"
artifacts:
- path: "ui/src/components/PresentationPanel.tsx"
provides: "Presentation generation UI with prompt, type selector, progress, video player"
min_lines: 80
- path: "ui/src/hooks/useContentJob.ts"
provides: "Extended SSE handler that reads data.progress for fine-grained progress"
contains: "data.progress"
- path: "ui/src/pages/ContentStudio.tsx"
provides: "Presentations tab in Content Studio"
contains: "presentations"
key_links:
- from: "ui/src/components/PresentationPanel.tsx"
to: "ui/src/hooks/useContentJob.ts"
via: "useContentJob hook"
pattern: "useContentJob"
- from: "ui/src/pages/ContentStudio.tsx"
to: "ui/src/components/PresentationPanel.tsx"
via: "PresentationPanel import"
pattern: "PresentationPanel"
---
<objective>
Extend useContentJob to surface fine-grained progress, create the PresentationPanel UI component, and add a Presentations tab to ContentStudio.
Purpose: Complete the user-facing experience for generating presentations and videos with real-time progress feedback.
Output: PresentationPanel.tsx, updated useContentJob.ts, updated ContentStudio.tsx
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/44-video-presentations/44-RESEARCH.md
@.planning/phases/44-video-presentations/44-01-SUMMARY.md
@ui/src/hooks/useContentJob.ts
@ui/src/pages/ContentStudio.tsx
@ui/src/components/BrandKitPanel.tsx (panel pattern reference)
@ui/src/components/DocumentGeneratePanel.tsx (panel pattern reference)
<interfaces>
From ui/src/hooks/useContentJob.ts:
```typescript
type JobStatus = "idle" | "queued" | "running" | "done" | "failed";
interface ContentJobState {
jobId: string | null;
status: JobStatus;
progress: number;
resultAssetId: string | null;
errorMessage: string | null;
}
export function useContentJob(companyId: string | null): ContentJobState & { submit, reset };
```
From ui/src/api/contentJobs.ts:
```typescript
export async function submitContentJob(...): Promise<{ jobId: string }>;
export async function getContentJobAsset(companyId: string, assetId: string): Promise<string>;
```
From server/src/services/renderers/types.ts (Plan 01):
```typescript
export interface PresentationBundle {
type: "presentation-bundle";
presentationType: "pitch-deck" | "demo-video";
title: string;
slideCount: number;
durationInFrames: number;
fps: number;
mp4Base64: string;
inputProps: Record<string, unknown>;
}
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Extend useContentJob to surface fine-grained SSE progress</name>
<files>ui/src/hooks/useContentJob.ts</files>
<read_first>
- ui/src/hooks/useContentJob.ts (full file — understand current SSE event handler)
</read_first>
<action>
Modify the `es.addEventListener("status", ...)` handler in `useContentJob.ts` to check for a `progress` field in the SSE payload.
Update the `data` type assertion inside the status event handler to include `progress?: number`:
```typescript
const data = JSON.parse(e.data as string) as {
status?: string;
progress?: number;
resultAssetId?: string | null;
errorMessage?: string | null;
};
```
Change the progress calculation to prefer the fine-grained value when present:
```typescript
const progress =
typeof data.progress === "number"
? data.progress
: statusToProgress(status);
```
This is backward-compatible — non-video jobs that don't send `data.progress` still use the coarse `statusToProgress` mapping. Video jobs that send progress 0-100 via content_job.progress will update the progress bar in real-time.
No other changes to the hook. The existing `state.progress` field is already a number 0-100.
</action>
<verify>
<automated>cd /opt/nexus && grep -c "data.progress" ui/src/hooks/useContentJob.ts</automated>
</verify>
<acceptance_criteria>
- grep -q "progress?: number" ui/src/hooks/useContentJob.ts
- grep -q "typeof data.progress" ui/src/hooks/useContentJob.ts
- grep -q "statusToProgress" ui/src/hooks/useContentJob.ts
</acceptance_criteria>
<done>useContentJob reads data.progress from SSE events for fine-grained video render progress, falls back to coarse statusToProgress for other job types</done>
</task>
<task type="auto">
<name>Task 2: Create PresentationPanel and add Presentations tab to ContentStudio</name>
<files>ui/src/components/PresentationPanel.tsx, ui/src/pages/ContentStudio.tsx</files>
<read_first>
- ui/src/components/BrandKitPanel.tsx (full — panel structure pattern)
- ui/src/components/DocumentGeneratePanel.tsx (first 80 lines — simpler panel pattern)
- ui/src/pages/ContentStudio.tsx (full — tab structure)
- ui/src/api/contentJobs.ts (getContentJobAsset function)
</read_first>
<action>
**ui/src/components/PresentationPanel.tsx:**
Create following the BrandKitPanel pattern. Props: `{ companyId: string }`.
State:
- `prompt` (string) — user's description
- `videoType` ("pitch-deck" | "demo-video") — defaults to "pitch-deck"
- `bundle` (PresentationBundle | null) — parsed result
Define `PresentationBundle` interface locally (same as server types but only the fields needed by UI):
```typescript
interface PresentationBundle {
type: "presentation-bundle";
presentationType: "pitch-deck" | "demo-video";
title: string;
slideCount: number;
durationInFrames: number;
fps: number;
mp4Base64: string;
inputProps: Record<string, unknown>;
}
```
Use `useContentJob(companyId)` hook. Submit with `job.submit("presentation", { prompt, videoType, title: prompt.slice(0, 60) })`.
When `job.status === "done"` and `job.resultAssetId` is set, fetch the asset via `getContentJobAsset(companyId, job.resultAssetId)`, fetch the URL, parse JSON as `PresentationBundle`, set to `bundle` state. Same pattern as BrandKitPanel.
Layout (using shadcn Card, Button, Textarea, Progress, Select components):
1. **Card header:** "Generate Presentation"
2. **Prompt textarea:** 5 rows, placeholder "Describe the presentation you want — topic, audience, key points..."
3. **Type selector:** Two radio buttons or a Select dropdown with options "Pitch Deck" (value "pitch-deck") and "Demo Video" (value "demo-video"). Import `Select, SelectContent, SelectItem, SelectTrigger, SelectValue` from `@/components/ui/select`.
4. **Generate button:** Disabled when no prompt or generating. Shows Loader2 spinner when generating.
5. **Progress bar:** Show `<Progress value={job.progress} />` when status is "queued" or "running". Display percentage text next to it: `{job.progress}%`. This is where the fine-grained SSE progress from PRES-04 is visible.
6. **Error display:** If `job.status === "failed"`, show error message in red text.
7. **Result section (when bundle is set):**
- Title and slide count info
- An HTML5 `<video>` element playing the MP4. Convert `bundle.mp4Base64` to a blob URL:
```typescript
const videoUrl = useMemo(() => {
if (!bundle?.mp4Base64) return null;
const bytes = Uint8Array.from(atob(bundle.mp4Base64), c => c.charCodeAt(0));
return URL.createObjectURL(new Blob([bytes], { type: "video/mp4" }));
}, [bundle?.mp4Base64]);
```
Render: `<video src={videoUrl} controls className="w-full rounded-lg aspect-video" />`
- Download button: creates an `<a>` tag with `href={videoUrl}` and `download={bundle.title}.mp4`
- "Generate Another" button that calls `job.reset()` and clears `bundle`/`prompt`
Clean up blob URL on unmount with `useEffect(() => () => { if (videoUrl) URL.revokeObjectURL(videoUrl) }, [videoUrl])`.
Import from: `useState`, `useMemo`, `useEffect` from react; `Loader2`, `Download`, `Video` from lucide-react; Card/Button/Textarea/Progress/Select from shadcn.
**ui/src/pages/ContentStudio.tsx:**
Add import: `import { PresentationPanel } from "../components/PresentationPanel";`
Add a new `TabsTrigger` with `value="presentations"` and text "Presentations" — place it after the "brand" trigger.
Add a new `TabsContent` with `value="presentations"`:
```tsx
<TabsContent value="presentations" className="mt-4">
{companyId ? (
<PresentationPanel companyId={companyId} />
) : (
<p className="text-sm text-muted-foreground">Select a company to get started.</p>
)}
</TabsContent>
```
</action>
<verify>
<automated>cd /opt/nexus && grep -c "PresentationPanel" ui/src/components/PresentationPanel.tsx && grep -c "presentations" ui/src/pages/ContentStudio.tsx && grep -c "PresentationPanel" ui/src/pages/ContentStudio.tsx</automated>
</verify>
<acceptance_criteria>
- grep -q "PresentationPanel" ui/src/components/PresentationPanel.tsx
- grep -q "useContentJob" ui/src/components/PresentationPanel.tsx
- grep -q "pitch-deck" ui/src/components/PresentationPanel.tsx
- grep -q "demo-video" ui/src/components/PresentationPanel.tsx
- grep -q "mp4Base64" ui/src/components/PresentationPanel.tsx
- grep -q "video/mp4" ui/src/components/PresentationPanel.tsx
- grep -q "job.progress" ui/src/components/PresentationPanel.tsx
- grep -q 'value="presentations"' ui/src/pages/ContentStudio.tsx
- grep -q "PresentationPanel" ui/src/pages/ContentStudio.tsx
</acceptance_criteria>
<done>PresentationPanel renders prompt input, type selector, real-time progress bar, MP4 video player with download; ContentStudio has Presentations tab</done>
</task>
</tasks>
<verification>
- `grep -q "data.progress" ui/src/hooks/useContentJob.ts` confirms fine-grained progress
- `grep -q "PresentationPanel" ui/src/components/PresentationPanel.tsx` confirms panel exists
- `grep -q 'value="presentations"' ui/src/pages/ContentStudio.tsx` confirms tab exists
- `grep -q "video/mp4" ui/src/components/PresentationPanel.tsx` confirms MP4 playback
- `pnpm --filter @paperclipai/ui exec -- npx tsc --noEmit 2>&1 | tail -3` passes
</verification>
<success_criteria>
- useContentJob reads data.progress for fine-grained SSE progress (PRES-04)
- PresentationPanel allows prompt + type selection for pitch-deck and demo-video (PRES-01, PRES-03)
- Progress bar shows real-time render percentage (PRES-04)
- Completed render shows MP4 video player with download (PRES-02)
- ContentStudio has 8 tabs including Presentations
- All changes follow existing codebase patterns (Card, useContentJob, getContentJobAsset)
</success_criteria>
<output>
After completion, create `.planning/phases/44-video-presentations/44-03-SUMMARY.md`
</output>