'use client' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import type { AdvisorResponse } from '@/lib/sdk/advisor/contract' export interface AdvisorCase { id: string threadId: string question: string response: AdvisorResponse | null selectedContext: string | null status: 'loading' | 'done' | 'error' error?: string } /** A topic: the first case's question is the title; follow-ups are the rest, in order. */ export interface AdvisorThread { id: string title: string cases: AdvisorCase[] } interface HistoryTurn { role: 'user' | 'assistant' content: string } interface UseAdvisorCaseArgs { currentStep: string country: string } let counter = 0 const uid = (p: string) => `${p}-${Date.now()}-${counter++}` /** * Drives the Advisor as topic THREADS of CASES. Each ask posts {question, context?, history} and * receives a structured AdvisorResponse (clarify | answer) — no streaming, no answer-text parsing. * A follow-up appends to the active thread (and carries the thread's prior Q&A as history); * newTopic() starts a fresh thread. selectContext() re-runs a case scoped to a chosen domain. */ export function useAdvisorCase({ currentStep, country }: UseAdvisorCaseArgs) { const [cases, setCases] = useState([]) const [busy, setBusy] = useState(false) const [activeCaseId, setActiveCaseId] = useState(null) const [activeThreadId, setActiveThreadId] = useState(null) const abortRef = useRef(null) const patch = useCallback((id: string, p: Partial) => { setCases((prev) => prev.map((c) => (c.id === id ? { ...c, ...p } : c))) }, []) // Prior answered turns of a thread, up to (but excluding) `beforeId`, for contextual follow-ups. const buildHistory = useCallback( (threadId: string, beforeId?: string): HistoryTurn[] => { const turns: HistoryTurn[] = [] for (const c of cases) { if (c.threadId !== threadId) continue if (beforeId && c.id === beforeId) break const a = c.response?.answer ?? c.response?.general_answer if (!a) continue turns.push({ role: 'user', content: c.question }, { role: 'assistant', content: a }) } return turns }, [cases], ) const run = useCallback( async (id: string, question: string, context: string | null, history: HistoryTurn[]) => { setBusy(true) abortRef.current = new AbortController() try { const res = await fetch('/api/sdk/compliance-advisor/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ question, context, history, currentStep, country }), signal: abortRef.current.signal, }) if (!res.ok) { const e = await res.json().catch(() => ({ error: 'Unbekannter Fehler' })) throw new Error(e.error || `Server-Fehler (${res.status})`) } const data = (await res.json()) as AdvisorResponse patch(id, { response: data, status: 'done', selectedContext: context }) } catch (err) { if ((err as Error).name === 'AbortError') { patch(id, { status: 'done' }) return } patch(id, { status: 'error', error: err instanceof Error ? err.message : 'Verbindung fehlgeschlagen', }) } finally { setBusy(false) } }, [currentStep, country, patch], ) const ask = useCallback( (question: string, opts?: { newThread?: boolean }) => { const q = question.trim() if (!q || busy) return const startNew = opts?.newThread || !activeThreadId || cases.length === 0 const threadId = startNew ? uid('thread') : activeThreadId! const id = uid('case') const history = startNew ? [] : buildHistory(threadId) setCases((prev) => [ ...prev, { id, threadId, question: q, response: null, selectedContext: null, status: 'loading' }, ]) setActiveThreadId(threadId) setActiveCaseId(id) void run(id, q, null, history) }, [busy, activeThreadId, cases.length, buildHistory, run], ) const newTopic = useCallback((question: string) => ask(question, { newThread: true }), [ask]) const selectContext = useCallback( (id: string, context: string) => { const c = cases.find((x) => x.id === id) if (!c || busy) return patch(id, { status: 'loading', selectedContext: context }) void run(id, c.question, context, buildHistory(c.threadId, id)) }, [cases, busy, run, patch, buildHistory], ) const remove = useCallback((id: string) => { setCases((prev) => prev.filter((c) => c.id !== id)) }, []) const selectCase = useCallback((id: string) => { setActiveCaseId(id) setCases((prev) => { const c = prev.find((x) => x.id === id) if (c) setActiveThreadId(c.threadId) return prev }) }, []) const stop = useCallback(() => { abortRef.current?.abort() setBusy(false) }, []) // Keep the active selection valid after deletions. useEffect(() => { if (activeCaseId && !cases.some((c) => c.id === activeCaseId)) { const last = cases[cases.length - 1] ?? null setActiveCaseId(last?.id ?? null) setActiveThreadId(last?.threadId ?? null) } }, [cases, activeCaseId]) const threads = useMemo(() => { const order: string[] = [] const byId = new Map() for (const c of cases) { let t = byId.get(c.threadId) if (!t) { t = { id: c.threadId, title: c.question, cases: [] } byId.set(c.threadId, t) order.push(c.threadId) } t.cases.push(c) } return order.map((id) => byId.get(id)!) }, [cases]) return { cases, threads, busy, activeCaseId, activeThreadId, ask, newTopic, selectContext, selectCase, remove, stop, } }