591cae5ebc
Reworks the advisor toward a Compliance Case Workspace (review feedback): - Rename user-facing "Quellen" -> "Evidence". - Evidence grouped by document/regulation family (count + expandable) — no more unsorted DSK/DSK/DPF/... jumble. - Human-readable regulation names via a display registry (DSK Sdm B51 -> "DSK Standard-Datenschutzmodell (SDM)" / Kapitel B51); generic, bridges G2. - Evidence summary "Antwort basiert auf" with meaningful counts; Regelwerke = distinct FAMILIES (fixes the inflated count). NO fabricated trust score (needs a defined basis). - Expanded mode = 3-column workspace (question+summary | answer | evidence, independent scroll) + history switcher; narrow mode stays stacked. - Prompt: push aggressive markdown structure (## per aspect, numbered phases). Deferred/coordinated on board: C8 diagrams (RAG contract), answer<->evidence coupling [1] (needs LLM citation anchors — phase 2), G1 retrieval relevance + G2 metadata (RAG). tsc clean, 17 vitest, check-loc 0. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
96 lines
3.2 KiB
TypeScript
96 lines
3.2 KiB
TypeScript
'use client'
|
||
|
||
import { useState } from 'react'
|
||
import { ChevronDown, ChevronRight, ExternalLink } from 'lucide-react'
|
||
import type { KnowledgeUnit } from '@/lib/sdk/advisor/evidence'
|
||
import { resolveRegulation } from '@/lib/sdk/advisor/regulation-display'
|
||
|
||
/**
|
||
* A single evidence unit. Standalone: friendly regulation name + hierarchy. Compact (inside a
|
||
* document group): chapter/section only (the group already names the regulation). [öffnen] opens
|
||
* the original source; the optional snippet lets the user peek the cited text.
|
||
*/
|
||
export function KnowledgeUnitCard({ unit, compact }: { unit: KnowledgeUnit; compact?: boolean }) {
|
||
const [open, setOpen] = useState(false)
|
||
const d = resolveRegulation(unit.regulation)
|
||
const crumbs = [unit.section, unit.subsection, unit.paragraph, unit.footnoteRef].filter(
|
||
(x): x is string => Boolean(x),
|
||
)
|
||
const href = unit.open?.originalUrl
|
||
const canOpen = !!href && /^https?:\/\//i.test(href)
|
||
|
||
let header: string
|
||
let sub: string[]
|
||
if (!compact) {
|
||
header = d.familyLabel
|
||
sub = crumbs
|
||
} else if (d.chapter) {
|
||
header = `Kapitel ${d.chapter}`
|
||
sub = crumbs
|
||
} else {
|
||
header = crumbs[0] || unit.label || d.familyLabel
|
||
sub = crumbs.slice(1)
|
||
}
|
||
|
||
return (
|
||
<div
|
||
className={
|
||
compact
|
||
? 'rounded-md border border-gray-100 bg-gray-50 p-2'
|
||
: '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">{header}</div>
|
||
{sub.length > 0 ? (
|
||
<div className="mt-0.5 flex flex-wrap items-center gap-x-1 text-[11px] text-gray-500">
|
||
{sub.map((c, i) => (
|
||
<span key={i} className="flex items-center gap-1">
|
||
{i > 0 && <span className="text-gray-300">›</span>}
|
||
{c}
|
||
</span>
|
||
))}
|
||
</div>
|
||
) : (
|
||
!compact &&
|
||
unit.label &&
|
||
unit.label !== header && (
|
||
<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>
|
||
)
|
||
}
|