/** * Planungs-KI Phase D: strukturierter Kontext für suggestExerciseAi. */ import { slotsAsPathStepRows } from './progressionGraphDraft.js' export function buildPickerPlanningContextForAi({ planningContextSummary = null, planningContext = null, searchQuery = '', } = {}) { if (!planningContextSummary && !planningContext) return null const ctx = { source: 'planning_picker', search_query: (searchQuery || '').trim() || null, unit_title: planningContextSummary?.unit_title || null, group_name: planningContextSummary?.group_name || null, section_title: planningContextSummary?.section_title || planningContext?.sectionTitle || null, section_guidance_notes: planningContext?.sectionGuidanceNotes || null, section_exercise_count: planningContextSummary?.section_exercise_count ?? null, last_section_exercise_title: planningContextSummary?.last_section_exercise_title || planningContext?.lastExerciseTitle || null, anchor_title: planningContextSummary?.anchor_title || null, progression_graph_name: planningContextSummary?.progression_graph_name || null, planned_count: planningContextSummary?.planned_count ?? null, intent_resolved: planningContextSummary?.intent_resolved || planningContext?.intentHint || null, } return Object.fromEntries(Object.entries(ctx).filter(([, v]) => v != null && v !== '')) } function majorIndexFromStep(step) { const raw = step?.roadmap_major_step_index ?? step?.roadmapMajorStepIndex if (raw == null || !Number.isFinite(Number(raw))) return null return Number(raw) } function priorPathStepsBeforeMajor(pathSteps, majorIdx) { if (majorIdx == null || !Number.isFinite(Number(majorIdx))) return [] const mi = Number(majorIdx) return (pathSteps || []) .filter((s) => { const idx = majorIndexFromStep(s) return idx != null && idx < mi }) .sort((a, b) => (majorIndexFromStep(a) || 0) - (majorIndexFromStep(b) || 0)) } function stepDisplayFields(step) { if (!step) return null const title = String(step.title || step.exerciseTitle || '').trim() const learningGoal = String( step.roadmap_learning_goal || step.roadmapLearningGoal || step.learning_goal || '', ).trim() const phase = String(step.roadmap_phase || step.roadmapPhase || step.phase || '').trim() const startState = String(step.roadmap_start_state || step.start_state || '').trim() const targetState = String(step.roadmap_target_state || step.target_state || '').trim() const criteria = Array.isArray(step.success_criteria) ? step.success_criteria.map((x) => String(x || '').trim()).filter(Boolean).slice(0, 4) : [] const majorStepIndex = majorIndexFromStep(step) const out = { title: title || null, learning_goal: learningGoal || null, start_state: startState || null, target_state: targetState || null, phase: phase || null, success_criteria: criteria.length ? criteria : null, major_step_index: majorStepIndex, } const hasData = Object.values(out).some((v) => v != null && v !== '') return hasData ? out : null } export function buildProgressionEntryState({ majorStepIndex = null, priorSteps = [], startSituation = '', currentStageStart = '', } = {}) { const priorCompact = (priorSteps || []) .map(stepDisplayFields) .filter(Boolean) const achievements = [] const detailLines = [] for (const p of priorCompact) { if (Array.isArray(p.success_criteria) && p.success_criteria.length) { achievements.push(...p.success_criteria) } else if (p.learning_goal) { achievements.push(p.learning_goal) } const labelParts = [] if (p.major_step_index != null) labelParts.push(`Stufe ${p.major_step_index + 1}`) if (p.phase) labelParts.push(`(${p.phase})`) if (p.title) labelParts.push(`„${p.title}"`) const prefix = labelParts.length ? labelParts.join(' ') : 'Vorstufe' const achieved = p.target_state || (Array.isArray(p.success_criteria) && p.success_criteria.length ? p.success_criteria.join('; ') : '') || p.learning_goal || '' if (achieved) detailLines.push(`${prefix}: erreicht — ${achieved}`) } let entryState = (currentStageStart || '').trim() if (!entryState && priorCompact.length) { const immediate = priorCompact[priorCompact.length - 1] entryState = immediate.target_state || (Array.isArray(immediate.success_criteria) && immediate.success_criteria.length ? immediate.success_criteria.join('; ') : '') || immediate.learning_goal || '' } else if (!entryState && (startSituation || '').trim()) { entryState = startSituation.trim() } if (priorCompact.length && (startSituation || '').trim() && !entryState) { detailLines.unshift(`Ausgangsbasis Pfad: ${startSituation.trim()}`) } const out = {} if (entryState) out.entry_state = entryState if (detailLines.length) out.entry_state_detail = detailLines.join('\n') if (priorCompact.length) out.prior_steps = priorCompact.slice(0, 6) if (achievements.length) out.prior_achievements = [...new Set(achievements)].slice(0, 8) return out } function stageSpecForMajorIndex(progressionRoadmap, majorIdx) { if (majorIdx == null || !progressionRoadmap) return null const specs = progressionRoadmap?.stage_specs if (!Array.isArray(specs)) return null const hit = specs.find((s) => Number(s.major_step_index) === Number(majorIdx)) if (!hit) return null const majors = progressionRoadmap?.roadmap?.major_steps const major = Array.isArray(majors) && majors.find((m) => Number(m.index) === Number(majorIdx)) return major ? { ...hit, phase: hit.phase || major.phase } : hit } export function buildPathGapPlanningContextForAi({ goalQuery = '', semanticBrief = null, offer = null, graphId = null, pathSteps = [], editableMajorSteps = [], progressionRoadmap = null, startSituation = '', targetState = '', roadmapNotes = '', stageLearningGoalOverride = '', gapTrainerSupplements = '', } = {}) { const majorIdxRaw = offer?.roadmap_major_step_index ?? offer?.gap?.roadmap_major_step_index const majorIdx = majorIdxRaw != null && Number.isFinite(Number(majorIdxRaw)) ? Number(majorIdxRaw) : null const priorSteps = majorIdx != null ? priorPathStepsBeforeMajor(pathSteps, majorIdx) : [] const afterIdx = Number(offer?.insert_after_index) const stepA = priorSteps.length > 0 ? priorSteps[priorSteps.length - 1] : Number.isFinite(afterIdx) && afterIdx >= 0 ? pathSteps[afterIdx] : null const stepB = majorIdx != null ? (pathSteps || []).find((s) => majorIndexFromStep(s) === majorIdx + 1) || (Number.isFinite(afterIdx) && afterIdx >= 0 ? pathSteps[afterIdx + 1] : null) : Number.isFinite(afterIdx) && afterIdx >= 0 ? pathSteps[afterIdx + 1] : null const majorStep = majorIdx != null && editableMajorSteps[majorIdx] ? editableMajorSteps[majorIdx] : null const stageSpec = stageSpecForMajorIndex(progressionRoadmap, majorIdx) const ga = progressionRoadmap?.goal_analysis || null const rs = progressionRoadmap?.resolved_structured || null const start = (startSituation || '').trim() || rs?.start_situation || ga?.start_assumption || null const target = (targetState || '').trim() || rs?.target_state || ga?.target_state || null const notes = (roadmapNotes || '').trim() || rs?.roadmap_notes || null const skillHints = [] if (Array.isArray(semanticBrief?.must_phrases)) { semanticBrief.must_phrases.slice(0, 4).forEach((p) => { const s = String(p || '').trim() if (s) skillHints.push(s) }) } if (Array.isArray(semanticBrief?.development_arc) && semanticBrief.development_arc.length) { skillHints.push( `Entwicklungsbogen: ${semanticBrief.development_arc.slice(0, 5).join(' → ')}`, ) } const entryState = buildProgressionEntryState({ majorStepIndex: majorIdx, priorSteps, startSituation: start, currentStageStart: stageSpec?.start_state || '', }) const ctx = { source: 'progression_path_gap_fill', goal_query: (goalQuery || '').trim() || null, primary_topic: ga?.primary_topic || semanticBrief?.primary_topic || null, progression_graph_id: graphId != null ? Number(graphId) : null, gap_source: offer?.source || null, gap_phase: offer?.phase || offer?.gap?.expected_phase || null, roadmap_major_step_index: majorIdx, roadmap_phase: majorStep?.phase || stageSpec?.phase || offer?.phase || null, roadmap_learning_goal: (majorStep?.learning_goal || offer?.title_hint || offer?.gap?.learning_goal || '').trim() || null, start_situation: start, target_state: target, roadmap_notes: notes, stage_learning_goal: (stageLearningGoalOverride || '').trim() || stageSpec?.learning_goal || null, stage_start_state: stageSpec?.start_state || null, stage_target_state: stageSpec?.target_state || null, gap_trainer_supplements: (gapTrainerSupplements || '').trim() || null, stage_phase: stageSpec?.phase || majorStep?.phase || null, stage_exercise_type: stageSpec?.exercise_type || null, stage_load_profile: Array.isArray(stageSpec?.load_profile) ? stageSpec.load_profile.slice(0, 6) : null, stage_success_criteria: Array.isArray(stageSpec?.success_criteria) ? stageSpec.success_criteria.slice(0, 4) : null, stage_anti_patterns: Array.isArray(stageSpec?.anti_patterns) ? stageSpec.anti_patterns.slice(0, 3) : null, path_success_criteria: Array.isArray(ga?.success_criteria) ? ga.success_criteria.slice(0, 4) : null, skill_hints: skillHints.length ? skillHints : null, neighbor_before_title: stepA?.exerciseTitle || stepA?.title || offer?.from_title || null, neighbor_after_title: stepB?.exerciseTitle || stepB?.title || offer?.to_title || null, ...entryState, path_step_count: Array.isArray(pathSteps) ? pathSteps.length : 0, major_step_count: editableMajorSteps?.length || progressionRoadmap?.major_step_count || progressionRoadmap?.roadmap?.major_steps?.length || null, } return Object.fromEntries(Object.entries(ctx).filter(([, v]) => v != null && v !== '')) } /** Lesbare Zeilen für UI — aus API context_preview oder lokal berechnet. */ export function gapOfferContextDisplayLines(offer, fallbackParams = null) { const raw = offer?.context_preview || (fallbackParams ? buildPathGapPlanningContextForAi({ offer, ...fallbackParams }) : null) if (!raw) return [] const lines = [] const push = (label, value) => { const v = String(value || '').trim() if (v) lines.push({ label, value: v }) } push('Eingangszustand (Vorstufen)', raw.entry_state) if (raw.entry_state_detail && raw.entry_state_detail !== raw.entry_state) { push('Bisheriger Pfad', raw.entry_state_detail) } if (Array.isArray(raw.prior_achievements) && raw.prior_achievements.length) { push('Erreichte Voraussetzungen', raw.prior_achievements.slice(0, 6).join(' · ')) } push('Ausgangslage (gesamter Pfad)', raw.start_situation) push('Gesamtziel (Pfad)', raw.target_state) push('Ergänzungen', raw.roadmap_notes) push('Stufen-Lernziel', raw.stage_learning_goal || raw.roadmap_learning_goal) if (raw.stage_phase) push('Phase', raw.stage_phase) if (Array.isArray(raw.stage_load_profile) && raw.stage_load_profile.length) { push('Belastung', raw.stage_load_profile.join(', ')) } if (Array.isArray(raw.stage_success_criteria) && raw.stage_success_criteria.length) { push('Erfolgskriterien', raw.stage_success_criteria.slice(0, 3).join(' · ')) } if (Array.isArray(raw.skill_hints) && raw.skill_hints.length) { push('Fähigkeiten-Fokus', raw.skill_hints.slice(0, 3).join(' · ')) } if (Array.isArray(raw.expected_skills) && raw.expected_skills.length) { const names = raw.expected_skills .map((s) => String(s?.skill_name || '').trim()) .filter(Boolean) .slice(0, 5) if (names.length) push('Erwartete Fähigkeiten', names.join(' · ')) } push('Trainer-Ergänzungen', raw.gap_trainer_supplements) return lines } /** Zieltext für KI aus Slot-Kontext (Graph-Editor ohne API-Offer). */ export function buildSlotGapGoalForAi(draft, slotIndex, { goalQuery = '' } = {}) { const slot = draft?.slots?.[slotIndex] if (!slot) return '' const pathSteps = slotsAsPathStepRows(draft) const majorIdx = slot.majorStepIndex const priorSteps = priorPathStepsBeforeMajor(pathSteps, majorIdx) const start = (draft.startSituation || '').trim() const stageSpec = majorIdx != null && draft.progressionRoadmap ? stageSpecForMajorIndex(draft.progressionRoadmap, majorIdx) : null const entry = buildProgressionEntryState({ majorStepIndex: majorIdx, priorSteps, startSituation: start, currentStageStart: stageSpec?.start_state || '', }) const parts = [ goalQuery ? `Planungsziel (gesamter Pfad): ${goalQuery}` : '', entry.entry_state ? `Eingangszustand (erreichte Voraussetzungen): ${entry.entry_state}` : start ? `Ausgangslage (Pfad): ${start}` : '', entry.entry_state_detail && entry.entry_state_detail !== entry.entry_state ? `Bisheriger Pfad:\n${entry.entry_state_detail}` : '', (slot.learning_goal || '').trim() ? `Lernziel dieser Roadmap-Stufe: ${(slot.learning_goal || '').trim()}` : '', (slot.phase || '').trim() ? `Entwicklungsphase: ${slot.phase}` : '', 'Die Übung baut didaktisch auf den Vorstufen auf — Voraussetzungen explizit benennen, messbares Stufenziel.', ].filter(Boolean) return parts.join('\n\n').trim() } export function initialStageLearningGoalFromOffer(offer, fallbackParams = null) { const lines = gapOfferContextDisplayLines(offer, fallbackParams) const hit = lines.find((l) => l.label === 'Stufen-Lernziel') return hit?.value || (offer?.title_hint || '').trim() }