shinkan-jinkendo/frontend/src/utils/planningContextForExerciseAi.js
Lars d4b1780193
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m14s
Enhance Gap Fill Offer with Context Preview and Update Version
- Added `context_preview` to the `build_gap_fill_offer` function, providing a structured overview of the roadmap snapshot.
- Introduced `gapOfferContextDisplayLines` utility to format context information for UI display, improving clarity for users.
- Updated `ExerciseProgressionPathBuilder` and related components to utilize the new context preview, enhancing the user experience.
- Incremented application version to 0.8.213 to reflect these changes.
2026-06-09 16:27:03 +02:00

163 lines
6.6 KiB
JavaScript

/**
* 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
}