/** * Planungs-KI Phase D: strukturierter Kontext für suggestExerciseAi. */ 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 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 = '', } = {}) { const afterIdx = Number(offer?.insert_after_index) const stepA = Number.isFinite(afterIdx) && afterIdx >= 0 ? pathSteps[afterIdx] : null const stepB = Number.isFinite(afterIdx) && afterIdx >= 0 ? pathSteps[afterIdx + 1] : null 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 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 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: stageSpec?.learning_goal || 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 || offer?.from_title || null, neighbor_after_title: stepB?.exerciseTitle || offer?.to_title || null, 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('Ausgangslage (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(' · ')) } return lines }