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>
This commit is contained in:
258
studio-v2/components/korrektur/CriteriaPanel.tsx
Normal file
258
studio-v2/components/korrektur/CriteriaPanel.tsx
Normal file
@@ -0,0 +1,258 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user