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