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>
294 lines
11 KiB
TypeScript
294 lines
11 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import type { EHSuggestion } from '@/app/korrektur/types'
|
|
import { DEFAULT_CRITERIA, ANNOTATION_COLORS, NIBIS_ATTRIBUTION } from '@/app/korrektur/types'
|
|
|
|
interface EHSuggestionPanelProps {
|
|
suggestions: EHSuggestion[]
|
|
isLoading: boolean
|
|
onLoadSuggestions: (criterion?: string) => void
|
|
onInsertSuggestion?: (text: string) => void
|
|
className?: string
|
|
}
|
|
|
|
export function EHSuggestionPanel({
|
|
suggestions,
|
|
isLoading,
|
|
onLoadSuggestions,
|
|
onInsertSuggestion,
|
|
className = '',
|
|
}: EHSuggestionPanelProps) {
|
|
const [selectedCriterion, setSelectedCriterion] = useState<string | undefined>(undefined)
|
|
const [expandedSuggestion, setExpandedSuggestion] = useState<string | null>(null)
|
|
|
|
const handleLoadSuggestions = () => {
|
|
onLoadSuggestions(selectedCriterion)
|
|
}
|
|
|
|
// Group suggestions by criterion
|
|
const groupedSuggestions = suggestions.reduce((acc, suggestion) => {
|
|
if (!acc[suggestion.criterion]) {
|
|
acc[suggestion.criterion] = []
|
|
}
|
|
acc[suggestion.criterion].push(suggestion)
|
|
return acc
|
|
}, {} as Record<string, EHSuggestion[]>)
|
|
|
|
return (
|
|
<div className={`space-y-4 ${className}`}>
|
|
{/* Header with Attribution (CTRL-SRC-002) */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h3 className="text-white font-semibold">EH-Vorschlaege</h3>
|
|
<p className="text-white/40 text-xs">Aus 500+ NiBiS Dokumenten</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Attribution Notice */}
|
|
<div className="p-2 rounded-lg bg-white/5 border border-white/10">
|
|
<div className="flex items-center gap-2 text-xs text-white/50">
|
|
<svg className="w-3 h-3 flex-shrink-0" 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>
|
|
<span>
|
|
Quelle: {NIBIS_ATTRIBUTION.publisher} •{' '}
|
|
<a
|
|
href={NIBIS_ATTRIBUTION.license_url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-blue-400 hover:underline"
|
|
>
|
|
{NIBIS_ATTRIBUTION.license}
|
|
</a>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Criterion Filter */}
|
|
<div className="flex flex-wrap gap-2">
|
|
<button
|
|
onClick={() => setSelectedCriterion(undefined)}
|
|
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-colors ${
|
|
selectedCriterion === undefined
|
|
? 'bg-purple-500 text-white'
|
|
: 'bg-white/10 text-white/60 hover:bg-white/20'
|
|
}`}
|
|
>
|
|
Alle
|
|
</button>
|
|
{Object.entries(DEFAULT_CRITERIA).map(([id, config]) => {
|
|
const color = ANNOTATION_COLORS[id as keyof typeof ANNOTATION_COLORS] || '#6b7280'
|
|
return (
|
|
<button
|
|
key={id}
|
|
onClick={() => setSelectedCriterion(id)}
|
|
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-colors ${
|
|
selectedCriterion === id
|
|
? 'text-white'
|
|
: 'text-white/60 hover:bg-white/20'
|
|
}`}
|
|
style={{
|
|
backgroundColor: selectedCriterion === id ? color : 'rgba(255,255,255,0.1)',
|
|
}}
|
|
>
|
|
{config.name}
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
{/* Load Button */}
|
|
<button
|
|
onClick={handleLoadSuggestions}
|
|
disabled={isLoading}
|
|
className="w-full px-4 py-3 rounded-xl bg-gradient-to-r from-blue-500 to-cyan-500 text-white font-medium hover:shadow-lg hover:shadow-blue-500/30 transition-all disabled:opacity-50 flex items-center justify-center gap-2"
|
|
>
|
|
{isLoading ? (
|
|
<>
|
|
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
|
RAG-Suche laeuft...
|
|
</>
|
|
) : (
|
|
<>
|
|
<svg className="w-5 h-5" 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>
|
|
EH-Vorschlaege laden
|
|
</>
|
|
)}
|
|
</button>
|
|
|
|
{/* Suggestions List */}
|
|
{suggestions.length > 0 && (
|
|
<div className="space-y-3 max-h-96 overflow-y-auto">
|
|
{Object.entries(groupedSuggestions).map(([criterion, criterionSuggestions]) => {
|
|
const config = DEFAULT_CRITERIA[criterion]
|
|
const color = ANNOTATION_COLORS[criterion as keyof typeof ANNOTATION_COLORS] || '#6b7280'
|
|
|
|
return (
|
|
<div key={criterion} className="space-y-2">
|
|
{/* Criterion Header */}
|
|
<div className="flex items-center gap-2 sticky top-0 bg-slate-900/95 backdrop-blur-sm py-1">
|
|
<div
|
|
className="w-2 h-2 rounded-full"
|
|
style={{ backgroundColor: color }}
|
|
/>
|
|
<span className="text-white/60 text-xs font-medium">
|
|
{config?.name || criterion}
|
|
</span>
|
|
<span className="text-white/30 text-xs">
|
|
({criterionSuggestions.length})
|
|
</span>
|
|
</div>
|
|
|
|
{/* Suggestions */}
|
|
{criterionSuggestions.map((suggestion, index) => (
|
|
<SuggestionCard
|
|
key={`${criterion}-${index}`}
|
|
suggestion={suggestion}
|
|
color={color}
|
|
isExpanded={expandedSuggestion === `${criterion}-${index}`}
|
|
onToggle={() =>
|
|
setExpandedSuggestion(
|
|
expandedSuggestion === `${criterion}-${index}`
|
|
? null
|
|
: `${criterion}-${index}`
|
|
)
|
|
}
|
|
onInsert={onInsertSuggestion}
|
|
/>
|
|
))}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
{/* Empty State */}
|
|
{!isLoading && suggestions.length === 0 && (
|
|
<div className="p-6 rounded-xl bg-white/5 border border-white/10 text-center">
|
|
<div className="w-12 h-12 rounded-full bg-white/10 flex items-center justify-center mx-auto mb-3">
|
|
<svg className="w-6 h-6 text-white/30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
|
</svg>
|
|
</div>
|
|
<p className="text-white/60 text-sm">
|
|
Klicken Sie auf "EH-Vorschlaege laden" um<br />
|
|
relevante Bewertungskriterien zu finden.
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// SUGGESTION CARD
|
|
// =============================================================================
|
|
|
|
interface SuggestionCardProps {
|
|
suggestion: EHSuggestion
|
|
color: string
|
|
isExpanded: boolean
|
|
onToggle: () => void
|
|
onInsert?: (text: string) => void
|
|
}
|
|
|
|
function SuggestionCard({
|
|
suggestion,
|
|
color,
|
|
isExpanded,
|
|
onToggle,
|
|
onInsert,
|
|
}: SuggestionCardProps) {
|
|
const relevancePercent = Math.round(suggestion.relevance_score * 100)
|
|
|
|
return (
|
|
<div
|
|
className="rounded-xl bg-white/5 border border-white/10 overflow-hidden transition-all"
|
|
style={{ borderLeftColor: color, borderLeftWidth: '3px' }}
|
|
>
|
|
{/* Header */}
|
|
<button
|
|
onClick={onToggle}
|
|
className="w-full px-3 py-2 flex items-center justify-between hover:bg-white/5 transition-colors"
|
|
>
|
|
<div className="flex items-center gap-2 flex-1 min-w-0">
|
|
<div
|
|
className="flex-shrink-0 w-8 h-8 rounded-lg flex items-center justify-center text-xs font-bold"
|
|
style={{
|
|
backgroundColor: `${color}20`,
|
|
color: color,
|
|
}}
|
|
>
|
|
{relevancePercent}%
|
|
</div>
|
|
<p className="text-white/70 text-sm truncate text-left">
|
|
{suggestion.excerpt.slice(0, 60)}...
|
|
</p>
|
|
</div>
|
|
<svg
|
|
className={`w-4 h-4 text-white/40 transition-transform flex-shrink-0 ml-2 ${
|
|
isExpanded ? 'rotate-180' : ''
|
|
}`}
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
</svg>
|
|
</button>
|
|
|
|
{/* Expanded Content */}
|
|
{isExpanded && (
|
|
<div className="px-3 pb-3 space-y-2">
|
|
<p className="text-white/60 text-sm whitespace-pre-wrap">
|
|
{suggestion.excerpt}
|
|
</p>
|
|
|
|
{/* Source Attribution */}
|
|
<div className="flex items-center gap-2 text-xs text-white/40 pt-1 border-t border-white/10">
|
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
<span>
|
|
{suggestion.source_document || 'NiBiS Kerncurriculum'} ({NIBIS_ATTRIBUTION.license})
|
|
</span>
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
{onInsert && (
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={() => onInsert(suggestion.excerpt)}
|
|
className="flex-1 px-3 py-2 rounded-lg bg-white/10 text-white/70 text-xs hover:bg-white/20 hover:text-white transition-colors flex items-center justify-center gap-2"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
|
</svg>
|
|
Einfuegen
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
// Insert with citation (CTRL-SRC-002)
|
|
const citation = `\n\n[Quelle: ${suggestion.source_document || 'NiBiS Kerncurriculum'}, ${NIBIS_ATTRIBUTION.publisher}, ${NIBIS_ATTRIBUTION.license}]`
|
|
onInsert(suggestion.excerpt + citation)
|
|
}}
|
|
className="px-3 py-2 rounded-lg bg-blue-500/20 text-blue-300 text-xs hover:bg-blue-500/30 transition-colors flex items-center justify-center gap-1"
|
|
title="Mit Quellenangabe einfuegen"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.172 13.828a4 4 0 015.656 0l4-4a4 4 0 00-5.656-5.656l-1.102 1.101" />
|
|
</svg>
|
|
+ Zitat
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|