f9b7ba2424
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>
98 lines
3.0 KiB
TypeScript
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 }
|
|
}
|