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>
14 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 13-ai-field-placement-and-pre-fill | 03 | execute | 3 |
|
|
true |
|
|
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.
<execution_context> @/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md @/Users/ccopeland/.claude/get-shit-done/templates/summary.md </execution_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.mdRead next.config.ts and check for Next.js guide in node_modules/next/dist/docs/ before writing component code.
Current FieldPlacer props (FieldPlacer.tsx line 155-166):
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:
// 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):
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):
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
-
Add
aiPlacementKey?: numbertoFieldPlacerPropsinterface. -
Add
aiPlacementKey = 0to the destructured props in the function signature. -
Update the
loadFieldsuseEffect dependency array to includeaiPlacementKey:// Change from: useEffect(() => { ... loadFields(); }, [docId]); // Change to: useEffect(() => { ... loadFields(); }, [docId, aiPlacementKey]);This causes FieldPlacer to re-fetch from DB whenever
aiPlacementKeyincrements 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:
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.
cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | grep -E "FieldPlacer|PdfViewer|error TS" | head -20
- 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
-
Add
onAiAutoPlace: () => Promise<void>toPreparePanelPropsinterface. -
Destructure
onAiAutoPlacefrom props. -
Add a local
aiLoadingstate:const [aiLoading, setAiLoading] = useState(false). -
Add an
handleAiAutoPlaceClickfunction in the component:async function handleAiAutoPlaceClick() { setAiLoading(true); setResult(null); try { await onAiAutoPlace(); } catch (e) { setResult({ ok: false, message: String(e) }); } finally { setAiLoading(false); } } -
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':<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:
-
Add
aiPlacementKeystate:const [aiPlacementKey, setAiPlacementKey] = useState(0). -
Add
handleAiAutoPlacecallback: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]); -
Pass
aiPlacementKeytoPdfViewerWrapper:<PdfViewerWrapper docId={docId} docStatus={docStatus} onFieldsChanged={handleFieldsChanged} selectedFieldId={selectedFieldId} textFillData={textFillData} onFieldSelect={setSelectedFieldId} onFieldValueChange={handleFieldValueChange} aiPlacementKey={aiPlacementKey} // NEW /> -
Pass
onAiAutoPlace={handleAiAutoPlace}toPreparePanel.
Key design decisions:
- AI placement REPLACES all existing fields in the DB (the route's
db.updateoverwritessignatureFields) - 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
resultstate cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | grep -E "PreparePanel|DocumentPageClient|error TS" | head -20- 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
<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>