WFDASH-WEBAPP/src/components/ai-fill-quote-button.tsx
Dave ef6193367d feat(quote-assistant): ai colour token, marker, fill button
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 19:45:44 +02:00

79 lines
2 KiB
TypeScript

"use client";
import { useState } from "react";
import { Bot, Loader2 } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import getPocketBase from "@/lib/pocketbase";
interface RunResponse {
filled?: number;
unmatched?: number;
error?: string | null;
}
function plural(n: number): string {
return `${n} line${n === 1 ? "" : "s"}`;
}
export function AiFillQuoteButton({
quoteId,
onDone,
size = "sm",
}: {
quoteId: string;
onDone: () => void;
size?: "sm" | "default";
}) {
const [running, setRunning] = useState(false);
async function run() {
if (running) return;
setRunning(true);
try {
const token = getPocketBase().authStore.token;
const res = await fetch(`/api/v1/quotes/${encodeURIComponent(quoteId)}/assistant/run`, {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
});
const body = (await res.json().catch(() => ({}))) as RunResponse;
if (!res.ok) {
toast.error(body.error || `AI fill failed (${res.status})`);
return;
}
const filled = body.filled ?? 0;
const unmatched = body.unmatched ?? 0;
if (body.error) {
toast.error(body.error);
} else if (filled === 0 && unmatched === 0) {
toast.info("AI had no open lines to fill");
} else {
toast.success(
unmatched > 0
? `AI filled ${plural(filled)} · ${unmatched} without a match`
: `AI filled ${plural(filled)}`,
);
}
onDone();
} catch (err) {
toast.error(err instanceof Error ? err.message : "AI fill failed");
} finally {
setRunning(false);
}
}
return (
<Button
type="button"
variant="outline"
size={size}
disabled={running}
onClick={run}
className="border-ai/30 text-ai hover:bg-ai/10 hover:text-ai"
>
{running ? <Loader2 className="animate-spin" aria-hidden /> : <Bot aria-hidden />}
{running ? "AI is filling…" : "Let AI fill this quote"}
</Button>
);
}