Files
Chandler Copeland df4676c23c 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>
2026-03-21 16:54:43 -06:00

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
13-01
13-02
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
true
AI-01
AI-02
truths artifacts key_links
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
path provides contains
teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx FieldPlacer with aiPlacementKey prop that triggers loadFields re-fetch aiPlacementKey
path provides contains
teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewerWrapper.tsx PdfViewerWrapper threads aiPlacementKey and onAiPlacementKeyChange to PdfViewer/FieldPlacer aiPlacementKey
path provides contains
teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx AI Auto-place button with loading state, calls onAiAutoPlace callback AI Auto-place
path provides contains
teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/DocumentPageClient.tsx handleAiAutoPlace that calls route, updates textFillData, increments aiPlacementKey, resets previewToken handleAiAutoPlace
from to via pattern
teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx /api/documents/[id]/ai-prepare onAiAutoPlace callback → DocumentPageClient.handleAiAutoPlace → fetch POST onAiAutoPlace|handleAiAutoPlace
from to via pattern
teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/DocumentPageClient.tsx teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx aiPlacementKey prop threaded through PdfViewerWrapper aiPlacementKey
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.

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

Read 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
Task 1: Add aiPlacementKey prop to FieldPlacer and thread through PdfViewerWrapper teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewerWrapper.tsx **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:

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

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

Task 2: Add AI Auto-place button to PreparePanel and wire DocumentPageClient handler teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/DocumentPageClient.tsx **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:

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

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

    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:

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

<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>
After completion, create `.planning/phases/13-ai-field-placement-and-pre-fill/13-03-SUMMARY.md`