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:
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user