fix(admin-v2): Restore complete admin-v2 application
The admin-v2 application was incomplete in the repository. This commit restores all missing components: - Admin pages (76 pages): dashboard, ai, compliance, dsgvo, education, infrastructure, communication, development, onboarding, rbac - SDK pages (45 pages): tom, dsfa, vvt, loeschfristen, einwilligungen, vendor-compliance, tom-generator, dsr, and more - Developer portal (25 pages): API docs, SDK guides, frameworks - All components, lib files, hooks, and types - Updated package.json with all dependencies The issue was caused by incomplete initial repository state - the full admin-v2 codebase existed in backend/admin-v2 and docs-src/admin-v2 but was never fully synced to the main admin-v2 directory. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
524
admin-v2/app/(admin)/ai/quality/page.tsx
Normal file
524
admin-v2/app/(admin)/ai/quality/page.tsx
Normal file
@@ -0,0 +1,524 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user