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 [previewToken, setPreviewToken] = useState<string | null>(null);
|
||||||
const [selectedFieldId, setSelectedFieldId] = useState<string | null>(null);
|
const [selectedFieldId, setSelectedFieldId] = useState<string | null>(null);
|
||||||
const [textFillData, setTextFillData] = useState<Record<string, string>>({});
|
const [textFillData, setTextFillData] = useState<Record<string, string>>({});
|
||||||
|
const [aiPlacementKey, setAiPlacementKey] = useState(0);
|
||||||
|
|
||||||
const handleFieldsChanged = useCallback(() => {
|
const handleFieldsChanged = useCallback(() => {
|
||||||
setPreviewToken(null);
|
setPreviewToken(null);
|
||||||
@@ -40,6 +41,24 @@ export function DocumentPageClient({
|
|||||||
setPreviewToken(null); // TXTF-03: reset staleness on quick fill
|
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 (
|
return (
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
<div className="lg:col-span-2">
|
<div className="lg:col-span-2">
|
||||||
@@ -51,6 +70,7 @@ export function DocumentPageClient({
|
|||||||
textFillData={textFillData}
|
textFillData={textFillData}
|
||||||
onFieldSelect={setSelectedFieldId}
|
onFieldSelect={setSelectedFieldId}
|
||||||
onFieldValueChange={handleFieldValueChange}
|
onFieldValueChange={handleFieldValueChange}
|
||||||
|
aiPlacementKey={aiPlacementKey}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<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}
|
textFillData={textFillData}
|
||||||
selectedFieldId={selectedFieldId}
|
selectedFieldId={selectedFieldId}
|
||||||
onQuickFill={handleQuickFill}
|
onQuickFill={handleQuickFill}
|
||||||
|
onAiAutoPlace={handleAiAutoPlace}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ interface PreparePanelProps {
|
|||||||
textFillData: Record<string, string>;
|
textFillData: Record<string, string>;
|
||||||
selectedFieldId: string | null;
|
selectedFieldId: string | null;
|
||||||
onQuickFill: (fieldId: string, value: string) => void;
|
onQuickFill: (fieldId: string, value: string) => void;
|
||||||
|
onAiAutoPlace: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseEmails(raw: string | undefined): string[] {
|
function parseEmails(raw: string | undefined): string[] {
|
||||||
@@ -31,6 +32,7 @@ export function PreparePanel({
|
|||||||
agentDownloadUrl, signedAt, clientPropertyAddress,
|
agentDownloadUrl, signedAt, clientPropertyAddress,
|
||||||
previewToken, onPreviewTokenChange,
|
previewToken, onPreviewTokenChange,
|
||||||
textFillData, selectedFieldId, onQuickFill,
|
textFillData, selectedFieldId, onQuickFill,
|
||||||
|
onAiAutoPlace,
|
||||||
}: PreparePanelProps) {
|
}: PreparePanelProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [recipients, setRecipients] = useState(defaultEmail ?? '');
|
const [recipients, setRecipients] = useState(defaultEmail ?? '');
|
||||||
@@ -39,6 +41,7 @@ export function PreparePanel({
|
|||||||
if (defaultEmail) setRecipients(defaultEmail);
|
if (defaultEmail) setRecipients(defaultEmail);
|
||||||
}, [defaultEmail]);
|
}, [defaultEmail]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [aiLoading, setAiLoading] = useState(false);
|
||||||
const [result, setResult] = useState<{ ok: boolean; message: string } | null>(null);
|
const [result, setResult] = useState<{ ok: boolean; message: string } | null>(null);
|
||||||
const [previewBytes, setPreviewBytes] = useState<ArrayBuffer | null>(null);
|
const [previewBytes, setPreviewBytes] = useState<ArrayBuffer | null>(null);
|
||||||
const [showPreview, setShowPreview] = useState(false);
|
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() {
|
async function handlePreview() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setResult(null);
|
setResult(null);
|
||||||
@@ -227,6 +242,15 @@ export function PreparePanel({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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
|
<button
|
||||||
onClick={handlePreview}
|
onClick={handlePreview}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
|
|||||||
Reference in New Issue
Block a user