4 plans in 4 sequential waves covering: - Plan 01 (TDD): openai install, extract-text.ts, field-placement.ts, aiCoordsToPagePdfSpace unit test - Plan 02: POST /api/documents/[id]/ai-prepare route with all guards - Plan 03: UI wiring — aiPlacementKey in FieldPlacer, AI Auto-place button in PreparePanel - Plan 04: Unit test gate + human E2E verification checkpoint Satisfies AI-01, AI-02. Completes v1.1 Smart Document Preparation milestone. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
327 lines
14 KiB
Markdown
327 lines
14 KiB
Markdown
---
|
|
phase: 13-ai-field-placement-and-pre-fill
|
|
plan: 03
|
|
type: execute
|
|
wave: 3
|
|
depends_on:
|
|
- 13-01
|
|
- 13-02
|
|
files_modified:
|
|
- teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx
|
|
- teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewerWrapper.tsx
|
|
- teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx
|
|
- teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/DocumentPageClient.tsx
|
|
autonomous: true
|
|
requirements:
|
|
- AI-01
|
|
- AI-02
|
|
|
|
must_haves:
|
|
truths:
|
|
- "PreparePanel has an 'AI Auto-place' button that calls POST /api/documents/[id]/ai-prepare and shows loading state"
|
|
- "After AI placement succeeds, FieldPlacer re-fetches fields from DB and displays the AI-placed field overlays"
|
|
- "After AI placement, DocumentPageClient's textFillData is updated with the returned pre-fill map and previewToken is reset to null"
|
|
- "Agent can review, move, resize, or delete any AI-placed field after placement — fields are editable, not locked"
|
|
artifacts:
|
|
- path: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx"
|
|
provides: "FieldPlacer with aiPlacementKey prop that triggers loadFields re-fetch"
|
|
contains: "aiPlacementKey"
|
|
- path: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewerWrapper.tsx"
|
|
provides: "PdfViewerWrapper threads aiPlacementKey and onAiPlacementKeyChange to PdfViewer/FieldPlacer"
|
|
contains: "aiPlacementKey"
|
|
- path: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx"
|
|
provides: "AI Auto-place button with loading state, calls onAiAutoPlace callback"
|
|
contains: "AI Auto-place"
|
|
- path: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/DocumentPageClient.tsx"
|
|
provides: "handleAiAutoPlace that calls route, updates textFillData, increments aiPlacementKey, resets previewToken"
|
|
contains: "handleAiAutoPlace"
|
|
key_links:
|
|
- from: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx"
|
|
to: "/api/documents/[id]/ai-prepare"
|
|
via: "onAiAutoPlace callback → DocumentPageClient.handleAiAutoPlace → fetch POST"
|
|
pattern: "onAiAutoPlace|handleAiAutoPlace"
|
|
- from: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/DocumentPageClient.tsx"
|
|
to: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx"
|
|
via: "aiPlacementKey prop threaded through PdfViewerWrapper"
|
|
pattern: "aiPlacementKey"
|
|
---
|
|
|
|
<objective>
|
|
Wire the "AI Auto-place" button into the PreparePanel and connect it to the FieldPlacer reload mechanism via DocumentPageClient state.
|
|
|
|
Purpose: The AI utilities (Plan 01) and route (Plan 02) are server-side. This plan adds the client-side interaction: the button, the loading state, the post-success state updates (fields reload + textFillData update + preview reset), and the FieldPlacer re-fetch trigger.
|
|
|
|
Output: Four files modified — FieldPlacer gets `aiPlacementKey` prop, PdfViewerWrapper threads it, PreparePanel gets "AI Auto-place" button, DocumentPageClient orchestrates the handler.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md
|
|
@/Users/ccopeland/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/STATE.md
|
|
@.planning/phases/13-ai-field-placement-and-pre-fill/13-RESEARCH.md
|
|
@.planning/phases/13-ai-field-placement-and-pre-fill/13-01-SUMMARY.md
|
|
@.planning/phases/13-ai-field-placement-and-pre-fill/13-02-SUMMARY.md
|
|
|
|
Read next.config.ts and check for Next.js guide in node_modules/next/dist/docs/ before writing component code.
|
|
</context>
|
|
|
|
<interfaces>
|
|
<!-- Current component interfaces. Extracted from codebase — no exploration needed. -->
|
|
|
|
Current FieldPlacer props (FieldPlacer.tsx line 155-166):
|
|
```typescript
|
|
interface FieldPlacerProps {
|
|
docId: string;
|
|
pageInfo: PageInfo | null;
|
|
currentPage: number;
|
|
children: React.ReactNode;
|
|
readOnly?: boolean;
|
|
onFieldsChanged?: () => void;
|
|
selectedFieldId?: string | null;
|
|
textFillData?: Record<string, string>;
|
|
onFieldSelect?: (fieldId: string | null) => void;
|
|
onFieldValueChange?: (fieldId: string, value: string) => void;
|
|
}
|
|
// FieldPlacer loads fields in useEffect([docId]) — adding aiPlacementKey to dependency array triggers re-fetch
|
|
```
|
|
|
|
Current PdfViewerWrapper props:
|
|
```typescript
|
|
// PdfViewerWrapper({ docId, docStatus, onFieldsChanged, selectedFieldId, textFillData, onFieldSelect, onFieldValueChange })
|
|
// It wraps PdfViewer (dynamic import, ssr: false) which renders FieldPlacer
|
|
// PdfViewer is at src/app/portal/(protected)/documents/[docId]/_components/PdfViewer.tsx
|
|
// It passes all props through to FieldPlacer
|
|
```
|
|
|
|
Current PreparePanel props (PreparePanel.tsx lines 6-18):
|
|
```typescript
|
|
interface PreparePanelProps {
|
|
docId: string;
|
|
defaultEmail: string;
|
|
clientName: string;
|
|
currentStatus: string;
|
|
agentDownloadUrl?: string | null;
|
|
signedAt?: Date | null;
|
|
clientPropertyAddress?: string | null;
|
|
previewToken: string | null;
|
|
onPreviewTokenChange: (token: string | null) => void;
|
|
textFillData: Record<string, string>;
|
|
selectedFieldId: string | null;
|
|
onQuickFill: (fieldId: string, value: string) => void;
|
|
}
|
|
```
|
|
|
|
Current DocumentPageClient state (DocumentPageClient.tsx):
|
|
```typescript
|
|
const [previewToken, setPreviewToken] = useState<string | null>(null);
|
|
const [selectedFieldId, setSelectedFieldId] = useState<string | null>(null);
|
|
const [textFillData, setTextFillData] = useState<Record<string, string>>({});
|
|
// handleFieldsChanged, handleFieldValueChange, handleQuickFill already exist
|
|
```
|
|
</interfaces>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Add aiPlacementKey prop to FieldPlacer and thread through PdfViewerWrapper</name>
|
|
<files>
|
|
teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx
|
|
teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewerWrapper.tsx
|
|
</files>
|
|
<action>
|
|
**FieldPlacer.tsx changes:**
|
|
|
|
1. Add `aiPlacementKey?: number` to `FieldPlacerProps` interface.
|
|
|
|
2. Add `aiPlacementKey = 0` to the destructured props in the function signature.
|
|
|
|
3. Update the `loadFields` useEffect dependency array to include `aiPlacementKey`:
|
|
```typescript
|
|
// Change from:
|
|
useEffect(() => { ... loadFields(); }, [docId]);
|
|
// Change to:
|
|
useEffect(() => { ... loadFields(); }, [docId, aiPlacementKey]);
|
|
```
|
|
This causes FieldPlacer to re-fetch from DB whenever `aiPlacementKey` increments after AI placement.
|
|
|
|
No other changes to FieldPlacer.tsx.
|
|
|
|
**PdfViewerWrapper.tsx changes:**
|
|
|
|
Read PdfViewer.tsx first to understand how it passes props to FieldPlacer — the thread needs to go: `DocumentPageClient → PdfViewerWrapper → PdfViewer → FieldPlacer`.
|
|
|
|
Add `aiPlacementKey?: number` to PdfViewerWrapper's props interface and pass it through to PdfViewer:
|
|
```typescript
|
|
export function PdfViewerWrapper({
|
|
docId, docStatus, onFieldsChanged, selectedFieldId, textFillData,
|
|
onFieldSelect, onFieldValueChange,
|
|
aiPlacementKey, // NEW
|
|
}: {
|
|
docId: string;
|
|
docStatus?: string;
|
|
onFieldsChanged?: () => void;
|
|
selectedFieldId?: string | null;
|
|
textFillData?: Record<string, string>;
|
|
onFieldSelect?: (fieldId: string | null) => void;
|
|
onFieldValueChange?: (fieldId: string, value: string) => void;
|
|
aiPlacementKey?: number; // NEW
|
|
}) {
|
|
return (
|
|
<PdfViewer
|
|
docId={docId}
|
|
docStatus={docStatus}
|
|
onFieldsChanged={onFieldsChanged}
|
|
selectedFieldId={selectedFieldId}
|
|
textFillData={textFillData}
|
|
onFieldSelect={onFieldSelect}
|
|
onFieldValueChange={onFieldValueChange}
|
|
aiPlacementKey={aiPlacementKey} // NEW
|
|
/>
|
|
);
|
|
}
|
|
```
|
|
|
|
Also read PdfViewer.tsx and add `aiPlacementKey?: number` to its props + pass through to FieldPlacer. PdfViewer is the component that actually renders FieldPlacer wrapping the PDF page canvas.
|
|
</action>
|
|
<verify>
|
|
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | grep -E "FieldPlacer|PdfViewer|error TS" | head -20</automated>
|
|
</verify>
|
|
<done>
|
|
- FieldPlacer accepts and uses `aiPlacementKey` in the loadFields useEffect dependency array
|
|
- PdfViewerWrapper and PdfViewer thread `aiPlacementKey` through to FieldPlacer
|
|
- TypeScript compiles without errors for these files
|
|
</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Add AI Auto-place button to PreparePanel and wire DocumentPageClient handler</name>
|
|
<files>
|
|
teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx
|
|
teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/DocumentPageClient.tsx
|
|
</files>
|
|
<action>
|
|
**PreparePanel.tsx changes:**
|
|
|
|
1. Add `onAiAutoPlace: () => Promise<void>` to `PreparePanelProps` interface.
|
|
|
|
2. Destructure `onAiAutoPlace` from props.
|
|
|
|
3. Add a local `aiLoading` state: `const [aiLoading, setAiLoading] = useState(false)`.
|
|
|
|
4. Add an `handleAiAutoPlaceClick` function in the component:
|
|
```typescript
|
|
async function handleAiAutoPlaceClick() {
|
|
setAiLoading(true);
|
|
setResult(null);
|
|
try {
|
|
await onAiAutoPlace();
|
|
} catch (e) {
|
|
setResult({ ok: false, message: String(e) });
|
|
} finally {
|
|
setAiLoading(false);
|
|
}
|
|
}
|
|
```
|
|
|
|
5. Add the "AI Auto-place" button to the JSX. Position it ABOVE the Preview button, at the top of the Draft panel (after the recipients textarea section, before the quick-fill section). Only show it when `currentStatus === 'Draft'`:
|
|
```tsx
|
|
<button
|
|
onClick={handleAiAutoPlaceClick}
|
|
disabled={aiLoading || loading}
|
|
className="w-full py-2 px-4 bg-violet-600 text-white rounded hover:bg-violet-700 disabled:opacity-50 disabled:cursor-not-allowed text-sm font-medium"
|
|
type="button"
|
|
>
|
|
{aiLoading ? 'AI placing fields...' : 'AI Auto-place Fields'}
|
|
</button>
|
|
```
|
|
Use violet (#7c3aed) to visually distinguish from the gray Preview button and blue Prepare and Send button.
|
|
|
|
**DocumentPageClient.tsx changes:**
|
|
|
|
1. Add `aiPlacementKey` state: `const [aiPlacementKey, setAiPlacementKey] = useState(0)`.
|
|
|
|
2. Add `handleAiAutoPlace` callback:
|
|
```typescript
|
|
const handleAiAutoPlace = useCallback(async () => {
|
|
const res = await fetch(`/api/documents/${docId}/ai-prepare`, { method: 'POST' });
|
|
if (!res.ok) {
|
|
const err = await res.json().catch(() => ({ error: 'AI placement failed' }));
|
|
throw new Error(err.error ?? err.message ?? 'AI placement failed');
|
|
}
|
|
const { textFillData: aiTextFill } = await res.json() as {
|
|
fields: unknown[];
|
|
textFillData: Record<string, string>;
|
|
};
|
|
// Merge AI pre-fill into existing textFillData (AI values take precedence)
|
|
setTextFillData(prev => ({ ...prev, ...aiTextFill }));
|
|
// Trigger FieldPlacer to re-fetch from DB (fields were written server-side)
|
|
setAiPlacementKey(k => k + 1);
|
|
// Reset preview staleness — fields changed
|
|
setPreviewToken(null);
|
|
}, [docId]);
|
|
```
|
|
|
|
3. Pass `aiPlacementKey` to `PdfViewerWrapper`:
|
|
```tsx
|
|
<PdfViewerWrapper
|
|
docId={docId}
|
|
docStatus={docStatus}
|
|
onFieldsChanged={handleFieldsChanged}
|
|
selectedFieldId={selectedFieldId}
|
|
textFillData={textFillData}
|
|
onFieldSelect={setSelectedFieldId}
|
|
onFieldValueChange={handleFieldValueChange}
|
|
aiPlacementKey={aiPlacementKey} // NEW
|
|
/>
|
|
```
|
|
|
|
4. Pass `onAiAutoPlace={handleAiAutoPlace}` to `PreparePanel`.
|
|
|
|
Key design decisions:
|
|
- AI placement REPLACES all existing fields in the DB (the route's `db.update` overwrites `signatureFields`)
|
|
- FieldPlacer re-fetches from DB via the aiPlacementKey increment — this is the single source of truth
|
|
- textFillData is MERGED (not replaced) so any manually typed values are preserved alongside AI pre-fills
|
|
- previewToken is reset to null — agent must re-preview after AI placement
|
|
- Error from the route is thrown and caught in PreparePanel's handleAiAutoPlaceClick, displayed via the existing `result` state
|
|
</action>
|
|
<verify>
|
|
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | grep -E "PreparePanel|DocumentPageClient|error TS" | head -20</automated>
|
|
</verify>
|
|
<done>
|
|
- PreparePanel has "AI Auto-place Fields" button with violet color, loading state, error display
|
|
- DocumentPageClient has handleAiAutoPlace calling /api/documents/[id]/ai-prepare
|
|
- After success: textFillData merges AI pre-fills, aiPlacementKey increments, previewToken resets to null
|
|
- Error from route is surfaced to user via PreparePanel result state
|
|
- TypeScript compiles without errors across all 4 modified files
|
|
</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
- `npx tsc --noEmit` passes with no errors
|
|
- FieldPlacer.tsx: `aiPlacementKey` in loadFields useEffect dependency array
|
|
- PdfViewerWrapper.tsx + PdfViewer.tsx: `aiPlacementKey` threaded through
|
|
- PreparePanel.tsx: "AI Auto-place Fields" button renders (violet, above Preview button) for Draft docs
|
|
- DocumentPageClient.tsx: `handleAiAutoPlace` callback, `aiPlacementKey` state, wired to both components
|
|
- Agent can still drag, move, resize, delete fields after AI placement (existing FieldPlacer behavior unchanged)
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- TypeScript compiles clean for all 4 files
|
|
- "AI Auto-place Fields" button is visible in PreparePanel for Draft documents
|
|
- Clicking the button calls POST /api/documents/[id]/ai-prepare
|
|
- On success: FieldPlacer shows AI-placed fields (via DB re-fetch), textFillData merges pre-fills, previewToken is null
|
|
- On error: error message displayed in PreparePanel result area
|
|
- Existing functionality unchanged: drag-and-drop, click-to-edit, quick-fill, preview, prepare-and-send
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/13-ai-field-placement-and-pre-fill/13-03-SUMMARY.md`
|
|
</output>
|