/** * Planungs-KI Phase C3: Ziel → Übungspfad vorschlagen → in Progressionsgraph speichern. */ import React, { useCallback, useState } from 'react' import api from '../utils/api' function emptyPathStep() { return { exerciseId: null, exerciseTitle: '', variantId: null, variants: [], reasons: [] } } function mapApiStepToRow(step) { const variants = Array.isArray(step?.variants) ? step.variants : [] const rawVid = step?.variant_id ?? step?.suggested_variant_id ?? null const variantId = rawVid != null && Number.isFinite(Number(rawVid)) && Number(rawVid) > 0 ? Number(rawVid) : null return { exerciseId: step?.exercise_id != null ? Number(step.exercise_id) : null, exerciseTitle: (step?.title || '').trim() || (step?.exercise_id ? `Übung #${step.exercise_id}` : ''), variantId, variants, reasons: Array.isArray(step?.reasons) ? step.reasons : [], isBridge: Boolean(step?.is_bridge), semanticScore: step?.semantic_score, } } export default function ExerciseProgressionPathBuilder({ graphId, disabled = false, onSaved, }) { const [goalQuery, setGoalQuery] = useState('') const [maxSteps, setMaxSteps] = useState(5) const [segmentNotes, setSegmentNotes] = useState('') const [loading, setLoading] = useState(false) const [saving, setSaving] = useState(false) const [error, setError] = useState('') const [targetSummary, setTargetSummary] = useState(null) const [semanticBrief, setSemanticBrief] = useState(null) const [pathQa, setPathQa] = useState(null) const [pathSteps, setPathSteps] = useState([]) const patchStep = useCallback((idx, patch) => { setPathSteps((prev) => prev.map((row, i) => (i === idx ? { ...row, ...patch } : row))) }, []) const removeStep = useCallback((idx) => { setPathSteps((prev) => (prev.length <= 2 ? prev : prev.filter((_, i) => i !== idx))) }, []) const moveStep = useCallback((idx, dir) => { setPathSteps((prev) => { const j = idx + dir if (j < 0 || j >= prev.length) return prev const next = [...prev] const t = next[idx] next[idx] = next[j] next[j] = t return next }) }, []) const suggestPath = async () => { const q = (goalQuery || '').trim() if (q.length < 3) { alert('Ziel-Anfrage: mindestens 3 Zeichen.') return } if (!graphId) { alert('Zuerst einen Graphen wählen.') return } setLoading(true) setError('') try { const res = await api.suggestProgressionPath({ query: q, max_steps: Number(maxSteps), include_llm_intent: true, include_path_qa: true, include_llm_path_qa: true, progression_graph_id: Number(graphId), }) const rows = (Array.isArray(res?.steps) ? res.steps : []).map(mapApiStepToRow) if (rows.length < 2) { throw new Error('Zu wenig Schritte im Vorschlag.') } setPathSteps(rows) setTargetSummary(res?.target_profile_summary || null) setSemanticBrief(res?.semantic_brief_summary || null) setPathQa(res?.path_qa || null) if (!segmentNotes.trim() && q) setSegmentNotes(q.slice(0, 400)) } catch (e) { console.error(e) setError(e.message || 'Pfad-Vorschlag fehlgeschlagen') setPathSteps([]) setTargetSummary(null) setSemanticBrief(null) setPathQa(null) } finally { setLoading(false) } } const savePathToGraph = async () => { if (!graphId) { alert('Zuerst einen Graphen wählen.') return } const steps = pathSteps.filter((s) => s.exerciseId != null) if (steps.length < 2) { alert('Mindestens zwei Schritte mit Übung nötig.') return } const n = steps.length - 1 const noteRaw = segmentNotes.trim() const segment_notes = Array.from({ length: n }, (_, i) => { const reasons = (steps[i + 1]?.reasons || []).slice(0, 2).join(' · ') if (reasons) return reasons return noteRaw || null }) setSaving(true) setError('') try { await api.createExerciseProgressionSequence(Number(graphId), { steps: steps.map((s) => ({ exercise_id: s.exerciseId, variant_id: s.variantId || null, })), segment_notes, }) setPathSteps([]) setTargetSummary(null) setSemanticBrief(null) setPathQa(null) if (typeof onSaved === 'function') await onSaved() alert(`${n} Nachfolger-Kante(n) aus KI-Pfad gespeichert.`) } catch (e) { console.error(e) setError(e.message || 'Speichern fehlgeschlagen') } finally { setSaving(false) } } return (

KI: Pfad zum Ziel

Ziel in Freitext formulieren — die Planungs-KI schlägt eine semantisch passende, aufbauende Reihenfolge vor, prüft Lücken (ggf. Brücken-Übungen) und optional per LLM-QS. Nach Review in den Graph speichern.

setGoalQuery(e.target.value)} placeholder="z. B. sichere Reaktion im Partnertraining aufbauen …" disabled={disabled || loading || saving} />
setMaxSteps(Math.max(2, Math.min(10, Number(e.target.value) || 5)))} disabled={disabled || loading || saving} />
{error ? (

{error}

) : null} {(semanticBrief || targetSummary) && pathSteps.length > 0 ? (
{semanticBrief?.primary_topic ? ( Thema: {semanticBrief.primary_topic} ) : null} {Array.isArray(semanticBrief?.development_arc) && semanticBrief.development_arc.slice(0, 3).map((phase) => ( {phase} ))} {Array.isArray(targetSummary?.focus_areas) && targetSummary.focus_areas.slice(0, 1).map((fa) => ( Fokus: {fa} ))}
) : null} {pathQa && pathSteps.length > 0 ? (
Pfad-QS: {pathQa.overall_ok ? 'OK' : 'Hinweise'} {pathQa.quality_score != null ? ` (${Math.round(Number(pathQa.quality_score) * 100)} %)` : ''} {pathQa.topic_coverage ? (

{pathQa.topic_coverage}

) : null} {Array.isArray(pathQa.issues) && pathQa.issues.length > 0 ? ( ) : null} {Number(pathQa.bridge_insert_count) > 0 ? (

{pathQa.bridge_insert_count} Brücken-Übung(en) eingefügt (Lückenfüller).

) : null} {Array.isArray(targetSummary?.top_skills) && targetSummary.top_skills.slice(0, 2).map((sk) => ( {sk.name} ))}
) : null} {pathSteps.length > 0 ? ( <>
{pathSteps.map((step, idx) => (
{step.exerciseTitle} (#{step.exerciseId})
{step.reasons?.length ? (
    {step.reasons.slice(0, 2).map((r) => (
  • {r}
  • ))}
) : null}
))}