Services: Admin-Compliance, Backend-Compliance, AI-Compliance-SDK, Consent-SDK, Developer-Portal, PCA-Platform, DSMS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
272 lines
12 KiB
TypeScript
272 lines
12 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
|
|
interface OperationPermission {
|
|
id: string
|
|
source_id: string
|
|
operation: string
|
|
is_allowed: boolean
|
|
requires_citation: boolean
|
|
notes?: string
|
|
}
|
|
|
|
interface SourceWithOperations {
|
|
id: string
|
|
domain: string
|
|
name: string
|
|
license: string
|
|
is_active: boolean
|
|
operations: OperationPermission[]
|
|
}
|
|
|
|
interface OperationsMatrixTabProps {
|
|
apiBase: string
|
|
}
|
|
|
|
const OPERATIONS = [
|
|
{ id: 'lookup', name: 'Lookup', description: 'Inhalt anzeigen/durchsuchen', icon: '🔍' },
|
|
{ id: 'rag', name: 'RAG', description: 'Retrieval Augmented Generation', icon: '🤖' },
|
|
{ id: 'training', name: 'Training', description: 'KI-Training (VERBOTEN)', icon: '🚫' },
|
|
{ id: 'export', name: 'Export', description: 'Daten exportieren', icon: '📤' },
|
|
]
|
|
|
|
export function OperationsMatrixTab({ apiBase }: OperationsMatrixTabProps) {
|
|
const [sources, setSources] = useState<SourceWithOperations[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [updating, setUpdating] = useState<string | null>(null)
|
|
|
|
useEffect(() => {
|
|
fetchMatrix()
|
|
}, [])
|
|
|
|
const fetchMatrix = async () => {
|
|
try {
|
|
setLoading(true)
|
|
const res = await fetch(`${apiBase}/v1/admin/operations-matrix`)
|
|
if (!res.ok) throw new Error('Fehler beim Laden')
|
|
|
|
const data = await res.json()
|
|
setSources(data.sources || [])
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Unbekannter Fehler')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const togglePermission = async (
|
|
source: SourceWithOperations,
|
|
operationId: string,
|
|
field: 'is_allowed' | 'requires_citation'
|
|
) => {
|
|
// Find the permission
|
|
const permission = source.operations.find((op) => op.operation === operationId)
|
|
if (!permission) return
|
|
|
|
// Block enabling training
|
|
if (operationId === 'training' && field === 'is_allowed' && !permission.is_allowed) {
|
|
setError('Training mit externen Daten ist VERBOTEN und kann nicht aktiviert werden.')
|
|
return
|
|
}
|
|
|
|
const updateId = `${permission.id}-${field}`
|
|
setUpdating(updateId)
|
|
|
|
try {
|
|
const newValue = !permission[field]
|
|
const res = await fetch(`${apiBase}/v1/admin/operations/${permission.id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ [field]: newValue }),
|
|
})
|
|
|
|
if (!res.ok) {
|
|
const errData = await res.json()
|
|
throw new Error(errData.message || errData.error || 'Fehler beim Aktualisieren')
|
|
}
|
|
|
|
fetchMatrix()
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Unbekannter Fehler')
|
|
} finally {
|
|
setUpdating(null)
|
|
}
|
|
}
|
|
|
|
if (loading) {
|
|
return <div className="text-center py-12 text-slate-500">Lade Operations-Matrix...</div>
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
{/* Error Display */}
|
|
{error && (
|
|
<div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg text-red-700 flex items-center justify-between">
|
|
<span>{error}</span>
|
|
<button onClick={() => setError(null)} className="text-red-500 hover:text-red-700">
|
|
×
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Legend */}
|
|
<div className="bg-white rounded-xl border border-slate-200 p-4 mb-6">
|
|
<h3 className="font-medium text-slate-900 mb-3">Legende</h3>
|
|
<div className="flex flex-wrap gap-4 text-sm">
|
|
<div className="flex items-center gap-2">
|
|
<span className="w-8 h-8 flex items-center justify-center bg-green-100 text-green-700 rounded">
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
</span>
|
|
<span className="text-slate-600">Erlaubt</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="w-8 h-8 flex items-center justify-center bg-red-100 text-red-700 rounded">
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</span>
|
|
<span className="text-slate-600">Verboten</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="w-8 h-8 flex items-center justify-center bg-amber-100 text-amber-700 rounded text-xs">
|
|
Cite
|
|
</span>
|
|
<span className="text-slate-600">Zitation erforderlich</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="w-8 h-8 flex items-center justify-center bg-slate-800 text-white rounded">
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
|
</svg>
|
|
</span>
|
|
<span className="text-slate-600">System-gesperrt (Training)</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Matrix Table */}
|
|
<div className="bg-white rounded-xl border border-slate-200 overflow-x-auto">
|
|
<table className="w-full min-w-[800px]">
|
|
<thead className="bg-slate-50 border-b border-slate-200">
|
|
<tr>
|
|
<th className="text-left px-4 py-3 text-xs font-medium text-slate-500 uppercase">Quelle</th>
|
|
{OPERATIONS.map((op) => (
|
|
<th key={op.id} className="text-center px-4 py-3">
|
|
<div className="flex flex-col items-center gap-1">
|
|
<span className="text-lg">{op.icon}</span>
|
|
<span className="text-xs font-medium text-slate-500 uppercase">{op.name}</span>
|
|
<span className="text-xs text-slate-400 font-normal">{op.description}</span>
|
|
</div>
|
|
</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-slate-100">
|
|
{sources.length === 0 ? (
|
|
<tr>
|
|
<td colSpan={5} className="px-4 py-8 text-center text-slate-500">
|
|
Keine Quellen vorhanden
|
|
</td>
|
|
</tr>
|
|
) : (
|
|
sources.map((source) => (
|
|
<tr key={source.id} className={`hover:bg-slate-50 ${!source.is_active ? 'opacity-50' : ''}`}>
|
|
<td className="px-4 py-3">
|
|
<div>
|
|
<div className="font-medium text-slate-800">{source.name}</div>
|
|
<code className="text-xs text-slate-500">{source.domain}</code>
|
|
</div>
|
|
</td>
|
|
{OPERATIONS.map((op) => {
|
|
const permission = source.operations.find((p) => p.operation === op.id)
|
|
const isTraining = op.id === 'training'
|
|
const isAllowed = permission?.is_allowed ?? false
|
|
const requiresCitation = permission?.requires_citation ?? false
|
|
const isUpdating = updating === `${permission?.id}-is_allowed` || updating === `${permission?.id}-requires_citation`
|
|
|
|
return (
|
|
<td key={op.id} className="px-4 py-3 text-center">
|
|
<div className="flex flex-col items-center gap-2">
|
|
{/* Is Allowed Toggle */}
|
|
<button
|
|
onClick={() => togglePermission(source, op.id, 'is_allowed')}
|
|
disabled={isTraining || isUpdating || !source.is_active}
|
|
className={`w-10 h-10 flex items-center justify-center rounded transition-colors ${
|
|
isTraining
|
|
? 'bg-slate-800 text-white cursor-not-allowed'
|
|
: isAllowed
|
|
? 'bg-green-100 text-green-700 hover:bg-green-200'
|
|
: 'bg-red-100 text-red-700 hover:bg-red-200'
|
|
} ${isUpdating ? 'opacity-50' : ''}`}
|
|
title={isTraining ? 'Training ist system-weit gesperrt' : isAllowed ? 'Klicken zum Deaktivieren' : 'Klicken zum Aktivieren'}
|
|
>
|
|
{isTraining ? (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
|
</svg>
|
|
) : isAllowed ? (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
) : (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
)}
|
|
</button>
|
|
|
|
{/* Citation Required Toggle (only for allowed non-training ops) */}
|
|
{isAllowed && !isTraining && (
|
|
<button
|
|
onClick={() => togglePermission(source, op.id, 'requires_citation')}
|
|
disabled={isUpdating || !source.is_active}
|
|
className={`px-2 py-1 text-xs rounded transition-colors ${
|
|
requiresCitation
|
|
? 'bg-amber-100 text-amber-700 hover:bg-amber-200'
|
|
: 'bg-slate-100 text-slate-500 hover:bg-slate-200'
|
|
} ${isUpdating ? 'opacity-50' : ''}`}
|
|
title={requiresCitation ? 'Zitation erforderlich - Klicken zum Aendern' : 'Klicken um Zitation zu erfordern'}
|
|
>
|
|
{requiresCitation ? 'Cite ✓' : 'Cite'}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</td>
|
|
)
|
|
})}
|
|
</tr>
|
|
))
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{/* Training Warning */}
|
|
<div className="mt-6 bg-red-50 border border-red-200 rounded-xl p-6">
|
|
<div className="flex items-start gap-4">
|
|
<div className="w-10 h-10 rounded-full bg-red-100 flex items-center justify-center flex-shrink-0">
|
|
<svg className="w-5 h-5 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<h3 className="font-semibold text-red-800">Training-Operation: System-gesperrt</h3>
|
|
<p className="text-sm text-red-700 mt-1">
|
|
Das Training von KI-Modellen mit gecrawlten externen Daten ist aufgrund von Urheberrechts- und
|
|
Datenschutzbestimmungen grundsaetzlich verboten. Diese Einschraenkung ist im System hart kodiert
|
|
und kann nicht ueber diese Oberflaeche geaendert werden.
|
|
</p>
|
|
<p className="text-sm text-red-600 mt-2 font-medium">
|
|
Ausnahmen erfordern eine schriftliche Genehmigung des DSB und eine rechtliche Pruefung.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|