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>
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
'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 }
|
||||
}
|
||||
Reference in New Issue
Block a user