"use client" import { useCallback, useEffect, useState } from "react" import { createPortal } from "react-dom" import { STORE_VOICE_FIELDS, KNOWLEDGE_TEMPLATE_ANSWER_MAX, type StoreVoiceFieldKey, type KnowledgeTemplateStored, } from "@/lib/kb/knowledgeTemplate" import { cn } from "@/lib/utils" import { Loader2, Save, X } from "lucide-react" type StructuredKnowledgeModalProps = { open: boolean onOpenChange: (open: boolean) => void storeId: string knowledgeBaseMode: "UNSET" | "STRUCTURED" | "DOCUMENTS" onSaved: () => void | Promise isLight: boolean } export function StructuredKnowledgeModal({ open, onOpenChange, storeId, knowledgeBaseMode, onSaved, isLight, }: StructuredKnowledgeModalProps) { const [mounted, setMounted] = useState(false) const [loading, setLoading] = useState(false) const [saving, setSaving] = useState(false) const [error, setError] = useState(null) const [answers, setAnswers] = useState>( () => STORE_VOICE_FIELDS.reduce( (acc, f) => { acc[f.key] = "" return acc }, {} as Record, ), ) const loadForm = useCallback(async () => { const res = await fetch(`/api/stores/${encodeURIComponent(storeId)}/knowledge-template`) if (!res.ok) return const data = (await res.json()) as { knowledgeTemplate: KnowledgeTemplateStored | null } const tmpl = data.knowledgeTemplate setAnswers((prev) => { const next = { ...prev } for (const f of STORE_VOICE_FIELDS) { const v = tmpl?.genericAnswers?.[f.key] next[f.key] = typeof v === "string" ? v : "" } return next }) }, [storeId]) useEffect(() => { setMounted(true) }, []) useEffect(() => { if (!open || !storeId) return let cancelled = false ; (async () => { try { setLoading(true) setError(null) await loadForm() } finally { if (!cancelled) setLoading(false) } })() return () => { cancelled = true } }, [open, storeId, loadForm]) useEffect(() => { if (!open) return const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onOpenChange(false) } window.addEventListener("keydown", onKey) return () => window.removeEventListener("keydown", onKey) }, [open, onOpenChange]) useEffect(() => { if (open) { document.body.style.overflow = "hidden" } else { document.body.style.overflow = "" } return () => { document.body.style.overflow = "" } }, [open]) async function handleSave(e: React.FormEvent) { e.preventDefault() setSaving(true) setError(null) const payload: Record = { template: { answers, }, } if (knowledgeBaseMode === "DOCUMENTS") { payload.knowledgeBaseMode = "STRUCTURED" } const trySave = async (confirmDestructiveSwitch?: boolean) => { const body = { ...payload, ...(confirmDestructiveSwitch ? { confirmDestructiveSwitch: true } : {}), } const res = await fetch(`/api/stores/${encodeURIComponent(storeId)}/knowledge-template`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }) const j = await res.json().catch(() => ({})) if (res.status === 400 && j.code === "CONFIRM_REQUIRED") { const ok = window.confirm( "Switching to structured knowledge will remove all uploaded documents and FAQs for this store. Continue?", ) if (!ok) return false return await trySave(true) } if (!res.ok) { throw new Error(j.message || "Failed to save") } return true } try { const ok = await trySave() if (ok) { await onSaved() onOpenChange(false) } } catch (err) { setError(err instanceof Error ? err.message : "Failed to save") } finally { setSaving(false) } } if (!mounted || !open) return null const panelClass = isLight ? "bg-white border-slate-200 text-slate-900 shadow-xl" : "bg-slate-900/95 border-white/10 text-white shadow-2xl backdrop-blur-xl" return createPortal(
{loading ? (
Loading…
) : (
void handleSave(e)} className="flex min-h-0 flex-1 flex-col" >
{error && (
{error}
)} {STORE_VOICE_FIELDS.map((field) => (

{field.description}