docs(13): create phase 13 AI field placement plan
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>
This commit is contained in:
@@ -0,0 +1,326 @@
|
||||
---
|
||||
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>
|
||||
Reference in New Issue
Block a user