feat(13-03): add AI Auto-place button to PreparePanel and wire DocumentPageClient handler

- Add onAiAutoPlace prop and handleAiAutoPlaceClick with aiLoading state to PreparePanel
- Add violet AI Auto-place button above Preview button (Draft documents only)
- Add aiPlacementKey state and handleAiAutoPlace callback in DocumentPageClient
- handleAiAutoPlace: POST /api/documents/[id]/ai-prepare, merges textFillData, increments aiPlacementKey, resets previewToken
- Pass aiPlacementKey to PdfViewerWrapper and onAiAutoPlace to PreparePanel
This commit is contained in:
Chandler Copeland
2026-03-21 17:08:10 -06:00
parent 3e11eef1c4
commit bfdaee14ed
2 changed files with 45 additions and 0 deletions

View File

@@ -25,6 +25,7 @@ export function DocumentPageClient({
const [previewToken, setPreviewToken] = useState<string | null>(null);
const [selectedFieldId, setSelectedFieldId] = useState<string | null>(null);
const [textFillData, setTextFillData] = useState<Record<string, string>>({});
const [aiPlacementKey, setAiPlacementKey] = useState(0);
const handleFieldsChanged = useCallback(() => {
setPreviewToken(null);
@@ -40,6 +41,24 @@ export function DocumentPageClient({
setPreviewToken(null); // TXTF-03: reset staleness on quick fill
}, []);
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]);
return (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2">
@@ -51,6 +70,7 @@ export function DocumentPageClient({
textFillData={textFillData}
onFieldSelect={setSelectedFieldId}
onFieldValueChange={handleFieldValueChange}
aiPlacementKey={aiPlacementKey}
/>
</div>
<div className="lg:col-span-1 lg:sticky lg:top-6 lg:self-start lg:max-h-[calc(100vh-6rem)] lg:overflow-y-auto">
@@ -67,6 +87,7 @@ export function DocumentPageClient({
textFillData={textFillData}
selectedFieldId={selectedFieldId}
onQuickFill={handleQuickFill}
onAiAutoPlace={handleAiAutoPlace}
/>
</div>
</div>

View File

@@ -16,6 +16,7 @@ interface PreparePanelProps {
textFillData: Record<string, string>;
selectedFieldId: string | null;
onQuickFill: (fieldId: string, value: string) => void;
onAiAutoPlace: () => Promise<void>;
}
function parseEmails(raw: string | undefined): string[] {
@@ -31,6 +32,7 @@ export function PreparePanel({
agentDownloadUrl, signedAt, clientPropertyAddress,
previewToken, onPreviewTokenChange,
textFillData, selectedFieldId, onQuickFill,
onAiAutoPlace,
}: PreparePanelProps) {
const router = useRouter();
const [recipients, setRecipients] = useState(defaultEmail ?? '');
@@ -39,6 +41,7 @@ export function PreparePanel({
if (defaultEmail) setRecipients(defaultEmail);
}, [defaultEmail]);
const [loading, setLoading] = useState(false);
const [aiLoading, setAiLoading] = useState(false);
const [result, setResult] = useState<{ ok: boolean; message: string } | null>(null);
const [previewBytes, setPreviewBytes] = useState<ArrayBuffer | null>(null);
const [showPreview, setShowPreview] = useState(false);
@@ -93,6 +96,18 @@ export function PreparePanel({
);
}
async function handleAiAutoPlaceClick() {
setAiLoading(true);
setResult(null);
try {
await onAiAutoPlace();
} catch (e) {
setResult({ ok: false, message: String(e) });
} finally {
setAiLoading(false);
}
}
async function handlePreview() {
setLoading(true);
setResult(null);
@@ -227,6 +242,15 @@ export function PreparePanel({
)}
</div>
<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>
<button
onClick={handlePreview}
disabled={loading}