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>
73 lines
2.6 KiB
TypeScript
73 lines
2.6 KiB
TypeScript
'use client'
|
|
|
|
import { useCallback, useState } from 'react'
|
|
import type { AdvisorCase } from './useAdvisorCase'
|
|
|
|
function esc(s: string): string {
|
|
return s
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
}
|
|
|
|
function evidenceHtml(c: AdvisorCase): string {
|
|
const ev = c.response?.evidence ?? []
|
|
if (ev.length === 0) return ''
|
|
const items = ev
|
|
.map(
|
|
(e) =>
|
|
`<li>${esc(e.document)}${e.section ? ` — ${esc(e.section)}` : ''}${e.paragraph ? ` ${esc(e.paragraph)}` : ''}</li>`,
|
|
)
|
|
.join('')
|
|
return `<p style="color:#64748b;font-size:12px;margin:4px 0 0;">Evidence:</p><ul style="color:#64748b;font-size:12px;margin:2px 0;">${items}</ul>`
|
|
}
|
|
|
|
/** Sends the consultation cases (question + answer + evidence) as an email to the DSB. */
|
|
export function useAdvisorEmail(cases: AdvisorCase[], country: string, currentStep: string) {
|
|
const [sending, setSending] = useState(false)
|
|
const [sent, setSent] = useState(false)
|
|
|
|
const send = useCallback(async () => {
|
|
if (cases.length === 0 || sending) return
|
|
setSending(true)
|
|
try {
|
|
const qaHtml = cases
|
|
.map((c) => {
|
|
const a = c.response?.answer || c.response?.general_answer || '(keine Antwort)'
|
|
return `<div style="margin-bottom:16px;"><p style="font-weight:600;color:#1e293b;">Frage: ${esc(
|
|
c.question,
|
|
)}</p><p style="color:#475569;white-space:pre-wrap;">${esc(a)}</p>${evidenceHtml(c)}</div>`
|
|
})
|
|
.join('')
|
|
|
|
const bodyHtml = `
|
|
<h2 style="color:#1e293b;">Compliance Advisor — Beratungsprotokoll</h2>
|
|
<p style="color:#64748b;font-size:13px;">Datum: ${esc(new Date().toLocaleString('de-DE'))} | Land: ${esc(country)} | Kontext: ${esc(currentStep)}</p>
|
|
<hr style="border-color:#e2e8f0;margin:16px 0;">
|
|
${qaHtml}
|
|
<hr style="border-color:#e2e8f0;margin:16px 0;">
|
|
<p style="color:#94a3b8;font-size:11px;">Automatisch erstellt vom BreakPilot Compliance Advisor</p>`
|
|
|
|
await fetch('/api/sdk/v1/agent/notify', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
recipient: 'dsb@breakpilot.local',
|
|
subject: `Compliance Advisor — ${cases.length} Fragen (${currentStep})`,
|
|
body_html: bodyHtml,
|
|
role: 'Datenschutzbeauftragter',
|
|
}),
|
|
})
|
|
setSent(true)
|
|
setTimeout(() => setSent(false), 3000)
|
|
} catch (e) {
|
|
console.error('Email send failed:', e)
|
|
} finally {
|
|
setSending(false)
|
|
}
|
|
}, [cases, sending, country, currentStep])
|
|
|
|
return { send, sending, sent }
|
|
}
|