Files
breakpilot-compliance/admin-compliance/components/sdk/advisor/useAdvisorCase.ts
T
Benjamin Admin f9b7ba2424 feat(advisor): v3 Clarity Gate — Case model + clarify/answer contract, [n] citations
Builds the FE against the SDK<->FE Clarity-Gate contract (board 2026-07-01 /
advisor-clarity-gate-contract). The advisor is now a CASE, not a chat:
- Request {question, context?}; response {mode: clarify|answer, clarity, general_answer,
  answer, evidence, citations, visual_evidence, footnotes}.
- clarify mode: short L1 general answer (marked "allgemeine Definition, ohne Rechtsquelle")
  + domain context chips; picking a chip re-runs the case scoped (-> answer).
- answer mode: markdown answer with clickable [n] citation markers coupled to evidence
  cards (highlight + scroll), evidence grouped by document family, visual_evidence
  (visual_type), footnotes, honest summary counts (no trust score).
- FE never parses the answer for structure — only the deliberate [n] markers, mapped via
  citations[]. New: contract.ts, useAdvisorCase, useCitationHighlight, ClarifyView,
  EvidenceUnitCard, VisualEvidencePane, CaseView. Removed the v2 stream/chat components.

NOT deployed: FE shape-switch (JSON modes) must deploy TOGETHER with the SDK endpoint
delivering the contract (board deploy-coupling). Proxy/route.ts unchanged (SDK-owned).
tsc clean, 16 vitest (incl. clarify+answer fixtures), check-loc 0.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-01 11:31:28 +02:00

98 lines
3.0 KiB
TypeScript

'use client'
import { useCallback, useRef, useState } from 'react'
import type { AdvisorResponse } from '@/lib/sdk/advisor/contract'
export interface AdvisorCase {
id: string
question: string
response: AdvisorResponse | null
selectedContext: string | null
status: 'loading' | 'done' | 'error'
error?: string
}
interface UseAdvisorCaseArgs {
currentStep: string
country: string
}
/**
* Drives the Advisor as a series of CASES. Each ask posts {question, context?} and receives a
* structured AdvisorResponse (mode: clarify | answer) — no streaming, no answer-text parsing.
* selectContext() re-runs the same case scoped to a chosen domain (clarify -> answer).
*/
export function useAdvisorCase({ currentStep, country }: UseAdvisorCaseArgs) {
const [cases, setCases] = useState<AdvisorCase[]>([])
const [busy, setBusy] = useState(false)
const abortRef = useRef<AbortController | null>(null)
const patch = useCallback((id: string, p: Partial<AdvisorCase>) => {
setCases((prev) => prev.map((c) => (c.id === id ? { ...c, ...p } : c)))
}, [])
const run = useCallback(
async (id: string, question: string, context: string | null) => {
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, 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) => {
const q = question.trim()
if (!q || busy) return
const id = `case-${Date.now()}`
setCases((prev) => [
...prev,
{ id, question: q, response: null, selectedContext: null, status: 'loading' },
])
void run(id, q, null)
},
[busy, run],
)
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)
},
[cases, busy, run, patch],
)
const stop = useCallback(() => {
abortRef.current?.abort()
setBusy(false)
}, [])
return { cases, busy, ask, selectContext, stop }
}