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:
@@ -4,7 +4,7 @@ import { useCallback, useState } from 'react'
|
||||
import { Check, Loader2, Mail, Maximize2, MessagesSquare, Minimize2, Send, Square, X } from 'lucide-react'
|
||||
import { EXAMPLE_QUESTIONS } from './advisor/EmptyState'
|
||||
import { EvidenceWorkspace } from './advisor/EvidenceWorkspace'
|
||||
import { useAdvisorStream } from './advisor/useAdvisorStream'
|
||||
import { useAdvisorCase } from './advisor/useAdvisorCase'
|
||||
import { useAdvisorEmail } from './advisor/useAdvisorEmail'
|
||||
|
||||
interface ComplianceAdvisorWidgetProps {
|
||||
@@ -15,9 +15,9 @@ type Country = 'DE' | 'AT' | 'CH' | 'EU'
|
||||
const COUNTRIES: Country[] = ['DE', 'AT', 'CH', 'EU']
|
||||
|
||||
/**
|
||||
* Compliance Advisor — Evidence Workspace as a floating widget on every SDK page.
|
||||
* Renders ONLY structured evidence from the SDK (answer + sources + figures + footnotes);
|
||||
* it never parses the answer text. See memory: advisor-evidence-workspace-no-parse.
|
||||
* Compliance Advisor — a floating Case Workspace on every SDK page.
|
||||
* Renders ONLY structured SDK data (clarify/answer contract); it never parses the answer text.
|
||||
* See memory: advisor-evidence-workspace-no-parse, advisor-clarity-gate-contract.
|
||||
*/
|
||||
export function ComplianceAdvisorWidget({ currentStep = 'default' }: ComplianceAdvisorWidgetProps) {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
@@ -25,17 +25,17 @@ export function ComplianceAdvisorWidget({ currentStep = 'default' }: ComplianceA
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const [country, setCountry] = useState<Country>('DE')
|
||||
|
||||
const { turns, isStreaming, send, stop } = useAdvisorStream({ currentStep, country })
|
||||
const email = useAdvisorEmail(turns, country, currentStep)
|
||||
const { cases, busy, ask, selectContext, stop } = useAdvisorCase({ currentStep, country })
|
||||
const email = useAdvisorEmail(cases, country, currentStep)
|
||||
const exampleQuestions = EXAMPLE_QUESTIONS[currentStep] || EXAMPLE_QUESTIONS.default
|
||||
|
||||
const submit = useCallback(
|
||||
(q: string) => {
|
||||
if (!q.trim() || isStreaming) return
|
||||
if (!q.trim() || busy) return
|
||||
setInputValue('')
|
||||
void send(q)
|
||||
ask(q)
|
||||
},
|
||||
[isStreaming, send],
|
||||
[busy, ask],
|
||||
)
|
||||
|
||||
const onKeyDown = (e: React.KeyboardEvent) => {
|
||||
@@ -63,7 +63,6 @@ export function ComplianceAdvisorWidget({ currentStep = 'default' }: ComplianceA
|
||||
isExpanded ? 'h-[85vh] w-[960px]' : 'h-[560px] w-[420px]'
|
||||
}`}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between rounded-t-2xl bg-gradient-to-r from-purple-600 to-indigo-600 px-4 py-3 text-white">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-white/20">
|
||||
@@ -87,7 +86,7 @@ export function ComplianceAdvisorWidget({ currentStep = 'default' }: ComplianceA
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{turns.length > 0 && (
|
||||
{cases.length > 0 && (
|
||||
<button
|
||||
onClick={email.send}
|
||||
disabled={email.sending}
|
||||
@@ -95,13 +94,7 @@ export function ComplianceAdvisorWidget({ currentStep = 'default' }: ComplianceA
|
||||
title={email.sent ? 'Email gesendet!' : 'Beratungsprotokoll als Email senden'}
|
||||
aria-label="Als Email an DSB senden"
|
||||
>
|
||||
{email.sent ? (
|
||||
<Check className="h-5 w-5" />
|
||||
) : email.sending ? (
|
||||
<Loader2 className="h-5 w-5 animate-spin" />
|
||||
) : (
|
||||
<Mail className="h-5 w-5" />
|
||||
)}
|
||||
{email.sent ? <Check className="h-5 w-5" /> : email.sending ? <Loader2 className="h-5 w-5 animate-spin" /> : <Mail className="h-5 w-5" />}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
@@ -121,15 +114,15 @@ export function ComplianceAdvisorWidget({ currentStep = 'default' }: ComplianceA
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Evidence Workspace */}
|
||||
<EvidenceWorkspace
|
||||
turns={turns}
|
||||
cases={cases}
|
||||
expanded={isExpanded}
|
||||
busy={busy}
|
||||
exampleQuestions={exampleQuestions}
|
||||
onExample={submit}
|
||||
onSelectContext={selectContext}
|
||||
/>
|
||||
|
||||
{/* Input */}
|
||||
<div className="rounded-b-2xl border-t border-gray-200 bg-white p-3">
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
@@ -138,15 +131,11 @@ export function ComplianceAdvisorWidget({ currentStep = 'default' }: ComplianceA
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder="Frage eingeben..."
|
||||
disabled={isStreaming}
|
||||
disabled={busy}
|
||||
className="flex-1 rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-transparent focus:outline-none focus:ring-2 focus:ring-purple-500 disabled:opacity-50"
|
||||
/>
|
||||
{isStreaming ? (
|
||||
<button
|
||||
onClick={stop}
|
||||
className="rounded-lg bg-red-500 px-4 py-2 text-white transition-colors hover:bg-red-600"
|
||||
title="Generierung stoppen"
|
||||
>
|
||||
{busy ? (
|
||||
<button onClick={stop} className="rounded-lg bg-red-500 px-4 py-2 text-white transition-colors hover:bg-red-600" title="Abbrechen">
|
||||
<Square className="h-5 w-5" />
|
||||
</button>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user