Files
breakpilot-compliance/admin-compliance/components/sdk/advisor/KnowledgeUnitCard.tsx
T
Benjamin Admin 49171e841f feat(advisor): Evidence Workspace — structured panes, markdown, sources as knowledge units
Rebuilds the Compliance Advisor floating widget from a plain chat into an Evidence
Workspace: pinned last question, markdown-rendered answer (clean prose), and separate
panes for Sources (hierarchical Knowledge Units), Figures (C8, conditional) and
Footnotes (C-FN), plus a stats bar (Quellen/Regelwerke/Diagramme/Fußnoten). Scrollable
turn history; stays a floating icon on every SDK page.

Architecture (user direction): the frontend renders ONLY structured evidence and NEVER
parses the answer text. The proxy now returns a JSON AdvisorEvidenceMeta line followed
by the streamed markdown answer; advisor-rag exposes structured results; an adapter maps
RAG/compiler output to the frontend envelope. Figures/footnotes wire in once the
RAG-ingestion contract lands (requested on the board) — figures pane is conditional.

- lib/sdk/advisor/{evidence,evidence-adapter}.ts (+ adapter test, 7 cases)
- components/sdk/advisor/* panes + in-house safe Markdown (no new dep, no dangerouslySetInnerHTML) + test
- useAdvisorStream (meta-line parse + streamed answer) + useAdvisorEmail (escaped)
- proxy: evidence-meta-v1 envelope + clean-prose prompt (no inline citations)
- tsc clean, 11 vitest pass, check-loc 0. ESLint not installed in this node_modules -> CI lints on push.

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

69 lines
2.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useState } from 'react'
import { ChevronDown, ChevronRight, ExternalLink } from 'lucide-react'
import type { KnowledgeUnit } from '@/lib/sdk/advisor/evidence'
/**
* A source rendered as a hierarchical Knowledge Unit (Regelwerk → Section → Paragraph → Footnote),
* not a text-list line. [öffnen] resolves to the original source when available; the optional
* snippet lets the user peek the cited text.
*/
export function KnowledgeUnitCard({ unit }: { unit: KnowledgeUnit }) {
const [open, setOpen] = useState(false)
const crumbs = [unit.section, unit.subsection, unit.paragraph, unit.footnoteRef].filter(Boolean)
const href = unit.open?.originalUrl
const canOpen = href && /^https?:\/\//i.test(href)
return (
<div className="rounded-lg border border-gray-200 bg-white p-2.5">
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<div className="truncate text-xs font-semibold text-gray-900">{unit.regulation.short}</div>
{crumbs.length > 0 ? (
<div className="mt-0.5 flex flex-wrap items-center gap-x-1 text-[11px] text-gray-500">
{crumbs.map((c, i) => (
<span key={i} className="flex items-center gap-1">
{i > 0 && <span className="text-gray-300"></span>}
{c}
</span>
))}
</div>
) : (
unit.label && <div className="mt-0.5 text-[11px] text-gray-500">{unit.label}</div>
)}
</div>
{canOpen && (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="flex flex-shrink-0 items-center gap-0.5 rounded px-1.5 py-0.5 text-[11px] font-medium text-indigo-600 hover:bg-indigo-50"
>
<ExternalLink className="h-3 w-3" />
öffnen
</a>
)}
</div>
{unit.snippet && (
<div className="mt-1.5">
<button
type="button"
onClick={() => setOpen((v) => !v)}
className="flex items-center gap-0.5 text-[11px] text-gray-400 hover:text-gray-600"
>
{open ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}
Textauszug
</button>
{open && (
<p className="mt-1 border-l-2 border-gray-200 pl-2 text-[11px] italic text-gray-500">
{unit.snippet}
</p>
)}
</div>
)}
</div>
)
}