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

11 KiB

phase plan type wave depends_on files_modified autonomous requirements must_haves
44-video-presentations 03 execute 2
44-01
ui/src/hooks/useContentJob.ts
ui/src/components/PresentationPanel.tsx
ui/src/pages/ContentStudio.tsx
true
PRES-01
PRES-02
PRES-03
PRES-04
truths artifacts key_links
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
path provides min_lines
ui/src/components/PresentationPanel.tsx Presentation generation UI with prompt, type selector, progress, video player 80
path provides contains
ui/src/hooks/useContentJob.ts Extended SSE handler that reads data.progress for fine-grained progress data.progress
path provides contains
ui/src/pages/ContentStudio.tsx Presentations tab in Content Studio presentations
from to via pattern
ui/src/components/PresentationPanel.tsx ui/src/hooks/useContentJob.ts useContentJob hook useContentJob
from to via pattern
ui/src/pages/ContentStudio.tsx ui/src/components/PresentationPanel.tsx PresentationPanel import PresentationPanel
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

<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/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) 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:

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

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>;
}
Task 1: Extend useContentJob to surface fine-grained SSE progress ui/src/hooks/useContentJob.ts - ui/src/hooks/useContentJob.ts (full file — understand current SSE event handler) 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.
cd /opt/nexus && grep -c "data.progress" ui/src/hooks/useContentJob.ts - 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 useContentJob reads data.progress from SSE events for fine-grained video render progress, falls back to coarse statusToProgress for other job types Task 2: Create PresentationPanel and add Presentations tab to ContentStudio ui/src/components/PresentationPanel.tsx, ui/src/pages/ContentStudio.tsx - 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) **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>
```
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 - 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 PresentationPanel renders prompt input, type selector, real-time progress bar, MP4 video player with download; ContentStudio has Presentations tab - `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

<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>
After completion, create `.planning/phases/44-video-presentations/44-03-SUMMARY.md`