Files
breakpilot-lehrer/studio-v2/components/korrektur/CriteriaPanel.tsx
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
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>
2026-02-11 23:47:26 +01:00

259 lines
7.9 KiB
TypeScript

'use client'
import { useMemo } from 'react'
import type { CriteriaScores, Annotation } from '@/app/korrektur/types'
import { DEFAULT_CRITERIA, ANNOTATION_COLORS, calculateGrade, getGradeLabel } from '@/app/korrektur/types'
interface CriteriaPanelProps {
scores: CriteriaScores
annotations: Annotation[]
onScoreChange: (criterion: string, value: number) => void
onLoadEHSuggestions?: (criterion: string) => void
isLoading?: boolean
className?: string
}
export function CriteriaPanel({
scores,
annotations,
onScoreChange,
onLoadEHSuggestions,
isLoading = false,
className = '',
}: CriteriaPanelProps) {
// Count annotations per criterion
const annotationCounts = useMemo(() => {
const counts: Record<string, number> = {}
for (const annotation of annotations) {
const type = annotation.linked_criterion || annotation.type
counts[type] = (counts[type] || 0) + 1
}
return counts
}, [annotations])
// Calculate total grade
const { totalWeightedScore, totalWeight, gradePoints, gradeLabel } = useMemo(() => {
let weightedScore = 0
let weight = 0
for (const [criterion, config] of Object.entries(DEFAULT_CRITERIA)) {
const score = scores[criterion]
if (score !== undefined) {
weightedScore += score * config.weight
weight += config.weight
}
}
const percentage = weight > 0 ? weightedScore / weight : 0
const grade = calculateGrade(percentage)
const label = getGradeLabel(grade)
return {
totalWeightedScore: weightedScore,
totalWeight: weight,
gradePoints: grade,
gradeLabel: label,
}
}, [scores])
return (
<div className={`space-y-4 ${className}`}>
{/* Criteria List */}
{Object.entries(DEFAULT_CRITERIA).map(([criterion, config]) => {
const score = scores[criterion] || 0
const annotationCount = annotationCounts[criterion] || 0
const color = ANNOTATION_COLORS[criterion as keyof typeof ANNOTATION_COLORS] || '#6b7280'
return (
<CriterionCard
key={criterion}
id={criterion}
name={config.name}
weight={config.weight}
score={score}
annotationCount={annotationCount}
color={color}
onScoreChange={(value) => onScoreChange(criterion, value)}
onLoadSuggestions={
onLoadEHSuggestions
? () => onLoadEHSuggestions(criterion)
: undefined
}
/>
)
})}
{/* Total Score */}
<div className="p-4 rounded-2xl bg-gradient-to-r from-purple-500/20 to-pink-500/20 border border-purple-500/30">
<div className="flex items-center justify-between mb-2">
<span className="text-white/60 text-sm">Gesamtnote</span>
<span className="text-2xl font-bold text-white">
{gradePoints} Punkte
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-white/40 text-xs">
{Math.round(totalWeightedScore / totalWeight)}% gewichtet
</span>
<span className="text-lg font-semibold text-purple-300">
({gradeLabel})
</span>
</div>
</div>
</div>
)
}
// =============================================================================
// CRITERION CARD
// =============================================================================
interface CriterionCardProps {
id: string
name: string
weight: number
score: number
annotationCount: number
color: string
onScoreChange: (value: number) => void
onLoadSuggestions?: () => void
}
function CriterionCard({
id,
name,
weight,
score,
annotationCount,
color,
onScoreChange,
onLoadSuggestions,
}: CriterionCardProps) {
const gradePoints = calculateGrade(score)
const gradeLabel = getGradeLabel(gradePoints)
return (
<div className="p-4 rounded-2xl bg-white/5 border border-white/10">
{/* Header */}
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: color }}
/>
<span className="text-white font-medium">{name}</span>
<span className="text-white/40 text-xs">({weight}%)</span>
</div>
<span className="text-white/60 text-sm">
{gradePoints} P ({gradeLabel})
</span>
</div>
{/* Slider */}
<div className="relative mb-3">
<input
type="range"
min="0"
max="100"
value={score}
onChange={(e) => onScoreChange(Number(e.target.value))}
className="w-full h-2 bg-white/10 rounded-full appearance-none cursor-pointer"
style={{
background: `linear-gradient(to right, ${color} ${score}%, rgba(255,255,255,0.1) ${score}%)`,
}}
/>
<div className="flex justify-between mt-1 text-xs text-white/30">
<span>0</span>
<span>25</span>
<span>50</span>
<span>75</span>
<span>100</span>
</div>
</div>
{/* Footer */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
{annotationCount > 0 && (
<span
className="px-2 py-0.5 rounded-full text-xs font-medium"
style={{ backgroundColor: `${color}20`, color }}
>
{annotationCount} Anmerkung{annotationCount !== 1 ? 'en' : ''}
</span>
)}
</div>
{onLoadSuggestions && (id === 'inhalt' || id === 'struktur') && (
<button
onClick={onLoadSuggestions}
className="text-xs text-purple-400 hover:text-purple-300 transition-colors flex items-center gap-1"
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
EH-Vorschlaege
</button>
)}
</div>
</div>
)
}
// =============================================================================
// COMPACT CRITERIA SUMMARY
// =============================================================================
interface CriteriaSummaryProps {
scores: CriteriaScores
className?: string
}
export function CriteriaSummary({ scores, className = '' }: CriteriaSummaryProps) {
const { gradePoints, gradeLabel } = useMemo(() => {
let weightedScore = 0
let weight = 0
for (const [criterion, config] of Object.entries(DEFAULT_CRITERIA)) {
const score = scores[criterion]
if (score !== undefined) {
weightedScore += score * config.weight
weight += config.weight
}
}
const percentage = weight > 0 ? weightedScore / weight : 0
const grade = calculateGrade(percentage)
const label = getGradeLabel(grade)
return { gradePoints: grade, gradeLabel: label }
}, [scores])
return (
<div className={`flex items-center gap-3 ${className}`}>
{Object.entries(DEFAULT_CRITERIA).map(([criterion, config]) => {
const score = scores[criterion] || 0
const color = ANNOTATION_COLORS[criterion as keyof typeof ANNOTATION_COLORS] || '#6b7280'
return (
<div
key={criterion}
className="flex items-center gap-1"
title={`${config.name}: ${score}%`}
>
<div
className="w-2 h-2 rounded-full"
style={{ backgroundColor: color }}
/>
<span className="text-white/60 text-xs">{score}</span>
</div>
)
})}
<div className="w-px h-4 bg-white/20" />
<span className="text-white font-medium text-sm">
{gradePoints} ({gradeLabel})
</span>
</div>
)
}