Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website, Klausur-Service, School-Service, Voice-Service, Geo-Service, BreakPilot Drive, Agent-Core Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
525 lines
22 KiB
TypeScript
525 lines
22 KiB
TypeScript
'use client'
|
||
|
||
/**
|
||
* Quality & Audit Page
|
||
*
|
||
* Ermoeglicht Auditoren:
|
||
* - Chunk-Suche und Stichproben
|
||
* - Traceability: Chunk → Requirement → Control
|
||
* - Dokumenten-Vollstaendigkeitspruefung
|
||
*/
|
||
|
||
import { useState, useCallback } from 'react'
|
||
import Link from 'next/link'
|
||
import { PagePurpose } from '@/components/common/PagePurpose'
|
||
|
||
const API_PROXY = '/api/legal-corpus'
|
||
|
||
// Types
|
||
interface ChunkDetail {
|
||
id: string
|
||
text: string
|
||
regulation_code: string
|
||
regulation_name: string
|
||
article: string | null
|
||
paragraph: string | null
|
||
chunk_index: number
|
||
chunk_position: 'beginning' | 'middle' | 'end'
|
||
source_url: string
|
||
score?: number
|
||
}
|
||
|
||
interface Requirement {
|
||
id: string
|
||
text: string
|
||
category: string
|
||
source_chunk_id: string
|
||
regulation_code: string
|
||
}
|
||
|
||
interface Control {
|
||
id: string
|
||
name: string
|
||
description: string
|
||
source_requirement_ids: string[]
|
||
regulation_codes: string[]
|
||
}
|
||
|
||
interface TraceabilityResult {
|
||
chunk: ChunkDetail
|
||
requirements: Requirement[]
|
||
controls: Control[]
|
||
}
|
||
|
||
// Regulations for filtering
|
||
const REGULATIONS = [
|
||
{ code: 'GDPR', name: 'DSGVO' },
|
||
{ code: 'EPRIVACY', name: 'ePrivacy' },
|
||
{ code: 'TDDDG', name: 'TDDDG' },
|
||
{ code: 'SCC', name: 'Standardvertragsklauseln' },
|
||
{ code: 'DPF', name: 'EU-US DPF' },
|
||
{ code: 'AIACT', name: 'EU AI Act' },
|
||
{ code: 'CRA', name: 'Cyber Resilience Act' },
|
||
{ code: 'NIS2', name: 'NIS2' },
|
||
{ code: 'EUCSA', name: 'EU Cybersecurity Act' },
|
||
{ code: 'DATAACT', name: 'Data Act' },
|
||
{ code: 'DGA', name: 'Data Governance Act' },
|
||
{ code: 'DSA', name: 'Digital Services Act' },
|
||
{ code: 'EAA', name: 'Accessibility Act' },
|
||
{ code: 'DSM', name: 'DSM-Urheberrecht' },
|
||
{ code: 'PLD', name: 'Produkthaftung' },
|
||
{ code: 'GPSR', name: 'Product Safety' },
|
||
{ code: 'BSI-TR-03161-1', name: 'BSI-TR Teil 1' },
|
||
{ code: 'BSI-TR-03161-2', name: 'BSI-TR Teil 2' },
|
||
{ code: 'BSI-TR-03161-3', name: 'BSI-TR Teil 3' },
|
||
]
|
||
|
||
const TYPE_COLORS: Record<string, string> = {
|
||
eu_regulation: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400',
|
||
eu_directive: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400',
|
||
de_law: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400',
|
||
bsi_standard: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400',
|
||
}
|
||
|
||
export default function QualityPage() {
|
||
// Search state
|
||
const [searchQuery, setSearchQuery] = useState('')
|
||
const [searchResults, setSearchResults] = useState<ChunkDetail[]>([])
|
||
const [searching, setSearching] = useState(false)
|
||
const [selectedRegulation, setSelectedRegulation] = useState<string>('')
|
||
const [topK, setTopK] = useState(10)
|
||
|
||
// Traceability state
|
||
const [selectedChunk, setSelectedChunk] = useState<ChunkDetail | null>(null)
|
||
const [traceability, setTraceability] = useState<TraceabilityResult | null>(null)
|
||
const [loadingTrace, setLoadingTrace] = useState(false)
|
||
|
||
// Quick sample queries for auditors
|
||
const sampleQueries = [
|
||
{ label: 'Art. 17 DSGVO (Recht auf Loeschung)', query: 'Recht auf Löschung Artikel 17', reg: 'GDPR' },
|
||
{ label: 'Einwilligung TDDDG', query: 'Einwilligung Endeinrichtung speichern', reg: 'TDDDG' },
|
||
{ label: 'AI Act Hochrisiko', query: 'Hochrisiko-KI-System Anforderungen', reg: 'AIACT' },
|
||
{ label: 'NIS2 Sicherheitsmaßnahmen', query: 'Cybersicherheitsrisikomanagement Maßnahmen', reg: 'NIS2' },
|
||
{ label: 'BSI Authentifizierung', query: 'Authentifizierung Zwei-Faktor mobile', reg: 'BSI-TR-03161-1' },
|
||
]
|
||
|
||
const handleSearch = useCallback(async () => {
|
||
if (!searchQuery.trim()) return
|
||
|
||
setSearching(true)
|
||
setSearchResults([])
|
||
setSelectedChunk(null)
|
||
setTraceability(null)
|
||
|
||
try {
|
||
let url = `${API_PROXY}?action=search&query=${encodeURIComponent(searchQuery)}&top_k=${topK}`
|
||
if (selectedRegulation) {
|
||
url += `®ulations=${encodeURIComponent(selectedRegulation)}`
|
||
}
|
||
|
||
const res = await fetch(url)
|
||
if (res.ok) {
|
||
const data = await res.json()
|
||
setSearchResults(data.results || [])
|
||
}
|
||
} catch (error) {
|
||
console.error('Search failed:', error)
|
||
} finally {
|
||
setSearching(false)
|
||
}
|
||
}, [searchQuery, selectedRegulation, topK])
|
||
|
||
const loadTraceability = useCallback(async (chunk: ChunkDetail) => {
|
||
setSelectedChunk(chunk)
|
||
setLoadingTrace(true)
|
||
|
||
try {
|
||
// Try to load traceability (requirements and controls derived from this chunk)
|
||
const res = await fetch(`${API_PROXY}?action=traceability&chunk_id=${encodeURIComponent(chunk.id || chunk.regulation_code + '_' + chunk.chunk_index)}®ulation=${encodeURIComponent(chunk.regulation_code)}`)
|
||
|
||
if (res.ok) {
|
||
const data = await res.json()
|
||
setTraceability({
|
||
chunk,
|
||
requirements: data.requirements || [],
|
||
controls: data.controls || [],
|
||
})
|
||
} else {
|
||
// If traceability endpoint doesn't exist yet, show placeholder
|
||
setTraceability({
|
||
chunk,
|
||
requirements: [],
|
||
controls: [],
|
||
})
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to load traceability:', error)
|
||
setTraceability({
|
||
chunk,
|
||
requirements: [],
|
||
controls: [],
|
||
})
|
||
} finally {
|
||
setLoadingTrace(false)
|
||
}
|
||
}, [])
|
||
|
||
const handleSampleQuery = (query: string, reg: string) => {
|
||
setSearchQuery(query)
|
||
setSelectedRegulation(reg)
|
||
// Auto-search after setting
|
||
setTimeout(() => {
|
||
handleSearch()
|
||
}, 100)
|
||
}
|
||
|
||
const highlightText = (text: string, query: string) => {
|
||
if (!query) return text
|
||
const words = query.toLowerCase().split(' ').filter(w => w.length > 2)
|
||
let result = text
|
||
words.forEach(word => {
|
||
const regex = new RegExp(`(${word})`, 'gi')
|
||
result = result.replace(regex, '<mark class="bg-yellow-200 dark:bg-yellow-800 px-0.5 rounded">$1</mark>')
|
||
})
|
||
return result
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
{/* Header */}
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||
Qualitaet & Audit
|
||
</h1>
|
||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||
Stichproben und Traceability fuer Compliance-Auditoren
|
||
</p>
|
||
</div>
|
||
<Link
|
||
href="/ai/rag"
|
||
className="text-sm text-blue-600 hover:text-blue-800 dark:text-blue-400"
|
||
>
|
||
← Zurueck zu RAG
|
||
</Link>
|
||
</div>
|
||
|
||
<PagePurpose
|
||
title="Audit-Werkzeuge"
|
||
purpose="Pruefen Sie die Qualitaet der Compliance-Datenbank. Suchen Sie gezielt nach Paragraphen, Saetzen oder Begriffen und verfolgen Sie, wie Anforderungen und Controls abgeleitet wurden."
|
||
audience={['Auditoren', 'Compliance-Beauftragte', 'Qualitaetssicherung']}
|
||
architecture={{
|
||
services: ['klausur-service', 'embedding-service', 'qdrant'],
|
||
databases: ['Qdrant Vector DB']
|
||
}}
|
||
/>
|
||
|
||
{/* Quick Sample Queries */}
|
||
<div className="bg-white dark:bg-slate-800 rounded-lg shadow p-4">
|
||
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
|
||
Schnell-Stichproben
|
||
</h3>
|
||
<div className="flex flex-wrap gap-2">
|
||
{sampleQueries.map((sq, idx) => (
|
||
<button
|
||
key={idx}
|
||
onClick={() => handleSampleQuery(sq.query, sq.reg)}
|
||
className="px-3 py-1.5 text-xs bg-gray-100 hover:bg-gray-200 dark:bg-slate-700 dark:hover:bg-slate-600 text-gray-700 dark:text-gray-300 rounded-full transition-colors"
|
||
>
|
||
{sq.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Search Section */}
|
||
<div className="bg-white dark:bg-slate-800 rounded-lg shadow p-6">
|
||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||
Chunk-Suche
|
||
</h2>
|
||
|
||
<div className="space-y-4">
|
||
{/* Search Input */}
|
||
<div className="flex gap-4">
|
||
<div className="flex-1">
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||
Suchbegriff / Paragraph / Artikeltext
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={searchQuery}
|
||
onChange={(e) => setSearchQuery(e.target.value)}
|
||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||
placeholder="z.B. 'Recht auf Löschung' oder 'Art. 17 Abs. 1'"
|
||
className="w-full px-4 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-slate-700 dark:text-white"
|
||
/>
|
||
</div>
|
||
<div className="w-48">
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||
Regulierung
|
||
</label>
|
||
<select
|
||
value={selectedRegulation}
|
||
onChange={(e) => setSelectedRegulation(e.target.value)}
|
||
className="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-slate-700 dark:text-white"
|
||
>
|
||
<option value="">Alle</option>
|
||
{REGULATIONS.map((reg) => (
|
||
<option key={reg.code} value={reg.code}>
|
||
{reg.name}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
<div className="w-24">
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||
Anzahl
|
||
</label>
|
||
<select
|
||
value={topK}
|
||
onChange={(e) => setTopK(parseInt(e.target.value))}
|
||
className="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-slate-700 dark:text-white"
|
||
>
|
||
<option value="5">5</option>
|
||
<option value="10">10</option>
|
||
<option value="20">20</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<button
|
||
onClick={handleSearch}
|
||
disabled={searching || !searchQuery.trim()}
|
||
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||
>
|
||
{searching ? 'Suche laeuft...' : 'Suchen'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Results Grid */}
|
||
{searchResults.length > 0 && (
|
||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||
{/* Search Results List */}
|
||
<div className="bg-white dark:bg-slate-800 rounded-lg shadow p-4">
|
||
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-4">
|
||
Gefundene Chunks ({searchResults.length})
|
||
</h3>
|
||
<div className="space-y-3 max-h-[600px] overflow-y-auto">
|
||
{searchResults.map((result, idx) => (
|
||
<div
|
||
key={idx}
|
||
onClick={() => loadTraceability(result)}
|
||
className={`p-4 border rounded-lg cursor-pointer transition-all ${
|
||
selectedChunk?.text === result.text
|
||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
|
||
: 'border-gray-200 dark:border-slate-700 hover:border-gray-300 dark:hover:border-slate-600'
|
||
}`}
|
||
>
|
||
{/* Header */}
|
||
<div className="flex items-center justify-between mb-2">
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-xs font-medium px-2 py-0.5 bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400 rounded">
|
||
{result.regulation_code}
|
||
</span>
|
||
{result.article && (
|
||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||
Art. {result.article}
|
||
{result.paragraph && ` Abs. ${result.paragraph}`}
|
||
</span>
|
||
)}
|
||
</div>
|
||
<span className="text-xs text-gray-400">
|
||
Score: {(result.score || 0).toFixed(3)}
|
||
</span>
|
||
</div>
|
||
|
||
{/* Text Preview */}
|
||
<p
|
||
className="text-sm text-gray-700 dark:text-gray-300 line-clamp-4"
|
||
dangerouslySetInnerHTML={{
|
||
__html: highlightText(result.text.substring(0, 400) + (result.text.length > 400 ? '...' : ''), searchQuery)
|
||
}}
|
||
/>
|
||
|
||
{/* Metadata */}
|
||
<div className="mt-2 flex items-center gap-4 text-xs text-gray-400">
|
||
<span>Chunk #{result.chunk_index || idx}</span>
|
||
<span>{result.text.length} Zeichen</span>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Traceability Panel */}
|
||
<div className="bg-white dark:bg-slate-800 rounded-lg shadow p-4">
|
||
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-4">
|
||
Traceability
|
||
</h3>
|
||
|
||
{!selectedChunk ? (
|
||
<div className="text-center py-12 text-gray-500 dark:text-gray-400">
|
||
<svg className="w-12 h-12 mx-auto mb-4 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||
</svg>
|
||
<p>Waehlen Sie einen Chunk aus der Liste, um die Traceability zu sehen.</p>
|
||
</div>
|
||
) : loadingTrace ? (
|
||
<div className="text-center py-12">
|
||
<div className="animate-spin w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full mx-auto mb-4"></div>
|
||
<p className="text-gray-500 dark:text-gray-400">Lade Traceability...</p>
|
||
</div>
|
||
) : traceability ? (
|
||
<div className="space-y-6">
|
||
{/* Selected Chunk Detail */}
|
||
<div className="border-l-4 border-blue-500 pl-4">
|
||
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
📄 Ausgewaehlter Chunk
|
||
</h4>
|
||
<div className="bg-gray-50 dark:bg-slate-700 rounded p-3">
|
||
<div className="flex items-center gap-2 mb-2">
|
||
<span className="text-xs font-medium px-2 py-0.5 bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400 rounded">
|
||
{traceability.chunk.regulation_code}
|
||
</span>
|
||
{traceability.chunk.article && (
|
||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||
Art. {traceability.chunk.article}
|
||
{traceability.chunk.paragraph && ` Abs. ${traceability.chunk.paragraph}`}
|
||
</span>
|
||
)}
|
||
</div>
|
||
<p className="text-sm text-gray-600 dark:text-gray-300 whitespace-pre-wrap">
|
||
{traceability.chunk.text}
|
||
</p>
|
||
{traceability.chunk.source_url && (
|
||
<a
|
||
href={traceability.chunk.source_url}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="mt-2 inline-flex items-center gap-1 text-xs text-blue-600 hover:underline"
|
||
>
|
||
🔗 Quelle oeffnen
|
||
</a>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Arrow Down */}
|
||
<div className="flex justify-center">
|
||
<svg className="w-6 h-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" />
|
||
</svg>
|
||
</div>
|
||
|
||
{/* Requirements */}
|
||
<div className="border-l-4 border-orange-500 pl-4">
|
||
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
📋 Extrahierte Anforderungen ({traceability.requirements.length})
|
||
</h4>
|
||
{traceability.requirements.length > 0 ? (
|
||
<div className="space-y-2">
|
||
{traceability.requirements.map((req, idx) => (
|
||
<div key={idx} className="bg-orange-50 dark:bg-orange-900/20 rounded p-3">
|
||
<div className="flex items-center gap-2 mb-1">
|
||
<span className="text-xs font-medium text-orange-700 dark:text-orange-400">
|
||
{req.category || 'Anforderung'}
|
||
</span>
|
||
</div>
|
||
<p className="text-sm text-gray-600 dark:text-gray-300">{req.text}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<p className="text-sm text-gray-500 dark:text-gray-400 italic">
|
||
Keine Anforderungen aus diesem Chunk extrahiert.
|
||
<br />
|
||
<span className="text-xs">(Requirements-Extraktion ist noch nicht implementiert)</span>
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
{/* Arrow Down */}
|
||
<div className="flex justify-center">
|
||
<svg className="w-6 h-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" />
|
||
</svg>
|
||
</div>
|
||
|
||
{/* Controls */}
|
||
<div className="border-l-4 border-green-500 pl-4">
|
||
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
✅ Abgeleitete Controls ({traceability.controls.length})
|
||
</h4>
|
||
{traceability.controls.length > 0 ? (
|
||
<div className="space-y-2">
|
||
{traceability.controls.map((ctrl, idx) => (
|
||
<div key={idx} className="bg-green-50 dark:bg-green-900/20 rounded p-3">
|
||
<div className="font-medium text-sm text-green-700 dark:text-green-400 mb-1">
|
||
{ctrl.name}
|
||
</div>
|
||
<p className="text-sm text-gray-600 dark:text-gray-300">{ctrl.description}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<p className="text-sm text-gray-500 dark:text-gray-400 italic">
|
||
Keine Controls aus diesem Chunk abgeleitet.
|
||
<br />
|
||
<span className="text-xs">(Control-Ableitung ist noch nicht implementiert)</span>
|
||
</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Empty State */}
|
||
{!searching && searchResults.length === 0 && searchQuery && (
|
||
<div className="bg-white dark:bg-slate-800 rounded-lg shadow p-12 text-center">
|
||
<svg className="w-16 h-16 mx-auto mb-4 text-gray-300 dark:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||
</svg>
|
||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
|
||
Keine Ergebnisse gefunden
|
||
</h3>
|
||
<p className="text-gray-500 dark:text-gray-400">
|
||
Versuchen Sie einen anderen Suchbegriff oder waehlen Sie eine andere Regulierung.
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* Initial State */}
|
||
{!searching && searchResults.length === 0 && !searchQuery && (
|
||
<div className="bg-white dark:bg-slate-800 rounded-lg shadow p-12 text-center">
|
||
<svg className="w-16 h-16 mx-auto mb-4 text-gray-300 dark:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||
</svg>
|
||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
|
||
Bereit fuer Stichproben
|
||
</h3>
|
||
<p className="text-gray-500 dark:text-gray-400 max-w-md mx-auto">
|
||
Geben Sie einen Suchbegriff ein, um Chunks zu finden. Sie koennen nach Artikeln,
|
||
Paragraphen oder spezifischen Textpassagen suchen.
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* Audit Info */}
|
||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||
<h3 className="text-sm font-medium text-blue-800 dark:text-blue-400 mb-2">
|
||
ℹ️ Hinweise fuer Auditoren
|
||
</h3>
|
||
<ul className="text-sm text-blue-700 dark:text-blue-300 space-y-1 list-disc list-inside">
|
||
<li>Die Suche ist semantisch - aehnliche Begriffe werden gefunden, auch wenn die exakte Formulierung abweicht</li>
|
||
<li>Jeder Chunk entspricht einem logischen Textabschnitt aus dem Originaldokument</li>
|
||
<li>Die Traceability zeigt, wie aus dem Originaltext Anforderungen und Controls abgeleitet wurden</li>
|
||
<li>Klicken Sie auf "Quelle oeffnen", um das Originaldokument zu pruefen</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|