Enhance Progression Path Features with LLM Start/Target Extraction
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
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

- Added `include_llm_start_target` option to `ProgressionPathSuggestRequest` for improved roadmap suggestions.
- Introduced new classes `StartTargetExtractArtifact` and `StartTargetResolveMeta` to handle LLM extraction results and metadata.
- Implemented `try_llm_start_target_extract` function to extract start and target states from goal queries using LLM.
- Updated `resolve_roadmap_structured_input` to prioritize user inputs, LLM extractions, and regex parsing for start/target resolution.
- Enhanced `ExerciseProgressionPathBuilder` to utilize new structured inputs and display extraction sources.
- Incremented application version to 0.8.211 to reflect these changes.
This commit is contained in:
Lars 2026-06-09 12:54:08 +02:00
parent 9dd44ce3ca
commit fad1058d54
7 changed files with 343 additions and 36 deletions

View File

@ -0,0 +1,52 @@
-- Migration 087: Planungs-KI — LLM Start/Ziel-Extraktion aus Trainer-Anfrage (Alternative zu Regex)
INSERT INTO ai_prompts (
slug, display_name, description, template,
category, output_format, output_schema, is_system_default, default_template, active, sort_order
)
SELECT
'planning_progression_start_target',
'Progressions-Roadmap Start/Ziel-Extraktion',
'Versteht die Trainer-Anfrage und formuliert dedizierte Ausgangslage, Zielzustand und Ergänzungen (ohne Gruppen-Tracking).',
$t$Du bist Assistent für Kampfsport-Trainer und analysierst eine Anfrage für einen didaktischen Progressionsgraphen.
Trainer-Anfrage (Ursprungstext):
{{goal_query}}
Semantic Brief (heuristisch): {{semantic_brief_json}}
Bereits vom Trainer eingegebene Ergänzungen (falls vorhanden): {{user_notes}}
Aufgabe:
1. **primary_topic** Kern-Thema/Technik in kurzer, präziser Bezeichnung (z. B. Kumite Beinarbeit, Mae Geri).
2. **start_situation** Ausgangslage in eigenen Worten: Was kann der Athlet/die Gruppe *jetzt* (laut Anfrage oder sinnvoll ableitbar)? Konkret, beobachtbar, ohne Gruppenanalyse aus der Datenbank.
3. **target_state** Zielzustand in eigenen Worten: Was soll am Ende der Progression erreicht sein? Konkret, didaktisch nutzbar.
4. **roadmap_notes** Ergänzungen aus dem Ursprungstext: Fokus, Kontext (z. B. Kumite), besondere Anforderungen, Einschränkungen, die der Trainer erwähnt hat oder die für die Roadmap relevant sind. Nicht wiederholen, was bereits in start_situation/target_state steht.
5. **extraction_notes** Kurz (12 Sätze): Was war explizit vs. abgeleitet? Wo war die Anfrage unklar?
Regeln:
- Keine Gruppenanalyse nur das, was aus dem Text hervorgeht oder didaktisch naheliegend formuliert ist.
- Formuliere start_situation und target_state **eigenständig und verständlich**, nicht nur Textfragmente kopieren.
- Bei von bis : Start und Ziel aus diesem Bogen schärfen und präzise beschreiben.
- Bei nur einem Thema ohne Bogen: start_situation und target_state didaktisch sinnvoll formulieren oder leer lassen, wenn nicht ableitbar dann in extraction_notes erklären.
- Antworte NUR mit JSON.
{
"primary_topic": "",
"start_situation": "",
"target_state": "",
"roadmap_notes": "",
"extraction_notes": ""
}$t$,
'training',
'json',
'{"type":"object","properties":{"primary_topic":{"type":"string"},"start_situation":{"type":"string"},"target_state":{"type":"string"},"roadmap_notes":{"type":"string"},"extraction_notes":{"type":"string"}}}'::jsonb,
true,
NULL,
true,
13
WHERE NOT EXISTS (SELECT 1 FROM ai_prompts WHERE slug = 'planning_progression_start_target');
UPDATE ai_prompts SET default_template = template
WHERE slug = 'planning_progression_start_target'
AND (default_template IS NULL OR TRIM(default_template) = '');

View File

@ -76,6 +76,7 @@ class ProgressionPathSuggestRequest(BaseModel):
include_ai_gap_fill: bool = True
include_roadmap_preview: bool = False
include_llm_roadmap: bool = True
include_llm_start_target: bool = True
roadmap_first: bool = False
roadmap_only: bool = False
roadmap_override: Optional[RoadmapOverridePayload] = None
@ -544,6 +545,7 @@ def suggest_progression_path(
semantic_brief=semantic_brief,
cur=cur,
include_llm_roadmap=body.include_llm_roadmap,
include_llm_start_target=body.include_llm_start_target,
structured=roadmap_structured,
)
progression_roadmap = progression_roadmap_to_api_dict(roadmap_ctx)

View File

@ -32,6 +32,7 @@ from planning_exercise_semantics import (
_logger = logging.getLogger("shinkan.planning_progression_roadmap")
# Nur Slugs — Templates in DB (ai_prompts), bearbeitbar im Admin.
PROMPT_SLUG_START_TARGET = "planning_progression_start_target"
PROMPT_SLUG_GOAL_ANALYSIS = "planning_progression_goal_analysis"
PROMPT_SLUG_ROADMAP = "planning_progression_roadmap"
PROMPT_SLUG_STAGE_SPEC = "planning_progression_stage_spec"
@ -117,6 +118,26 @@ class RoadmapStructuredInput(BaseModel):
roadmap_notes: Optional[str] = Field(default=None, max_length=2000)
class StartTargetExtractArtifact(BaseModel):
"""LLM-Ergebnis: dedizierte Beschreibung von Ausgang, Ziel und Ergänzungen."""
primary_topic: str = ""
start_situation: str = ""
target_state: str = ""
roadmap_notes: str = ""
extraction_notes: str = ""
class StartTargetResolveMeta(BaseModel):
"""Herkunft der aufgelösten Felder (user > llm > regex)."""
start_source: str = "none"
target_source: str = "none"
notes_source: str = "none"
topic_source: str = "none"
llm_start_target_applied: bool = False
class RoadmapOverridePayload(BaseModel):
"""Vom Trainer bearbeitete Major Steps — steuert Retrieval ohne erneute Roadmap-KI."""
@ -131,10 +152,14 @@ class ProgressionRoadmapContext(BaseModel):
goal_query: str
max_steps: int = Field(ge=2, le=10, default=5)
semantic_brief: Optional[Dict[str, Any]] = None
resolved_structured: Optional[RoadmapStructuredInput] = None
start_target_extract: Optional[StartTargetExtractArtifact] = None
start_target_resolve: Optional[StartTargetResolveMeta] = None
goal_analysis: Optional[GoalAnalysisArtifact] = None
roadmap: Optional[RoadmapArtifact] = None
stage_specs: List[StageSpecArtifact] = Field(default_factory=list)
pipeline_phase: str = "roadmap_v1"
llm_start_target_applied: bool = False
llm_goal_analysis_applied: bool = False
llm_roadmap_applied: bool = False
llm_stage_spec_applied: bool = False
@ -177,6 +202,31 @@ def _run_prompt_json(
return None
def try_llm_start_target_extract(
cur,
*,
goal_query: str,
brief: PlanningSemanticBrief,
user_notes: str = "",
) -> Tuple[Optional[StartTargetExtractArtifact], bool]:
obj = _run_prompt_json(
cur,
PROMPT_SLUG_START_TARGET,
{
"goal_query": goal_query or "",
"semantic_brief_json": json.dumps(brief_to_summary_dict(brief), ensure_ascii=False),
"user_notes": (user_notes or "").strip(),
},
)
if not obj:
return None, False
try:
return StartTargetExtractArtifact.model_validate(obj), True
except ValidationError as exc:
_logger.warning("Start/Ziel-Extraktion JSON ungültig: %s", exc)
return None, False
def try_llm_goal_analysis(
cur,
*,
@ -297,6 +347,100 @@ def _extract_topic_from_goal_query(goal_query: str, brief: PlanningSemanticBrief
return topic or "Technik"
def _merge_roadmap_notes(*parts: Optional[str]) -> Optional[str]:
seen: set[str] = set()
lines: List[str] = []
for raw in parts:
s = (raw or "").strip()
if not s:
continue
key = s.lower()
if key in seen:
continue
seen.add(key)
lines.append(s)
return "\n".join(lines) if lines else None
def resolve_roadmap_structured_input(
goal_query: str,
structured: Optional[RoadmapStructuredInput],
*,
brief: PlanningSemanticBrief,
cur=None,
include_llm: bool = False,
) -> Tuple[RoadmapStructuredInput, StartTargetResolveMeta, Optional[StartTargetExtractArtifact]]:
"""Priorität je Feld: Trainer-Eingabe > LLM-Extraktion > Regex (von … bis …)."""
user = structured or RoadmapStructuredInput()
user_start = (user.start_situation or "").strip()
user_target = (user.target_state or "").strip()
user_notes = (user.roadmap_notes or "").strip()
llm_extract: Optional[StartTargetExtractArtifact] = None
llm_ok = False
if include_llm and cur is not None:
llm_extract, llm_ok = try_llm_start_target_extract(
cur,
goal_query=goal_query,
brief=brief,
user_notes=user_notes,
)
parsed_start, parsed_target = parse_start_target_from_goal_query(goal_query)
meta = StartTargetResolveMeta(llm_start_target_applied=llm_ok)
if user_start:
start = user_start
meta.start_source = "user"
elif llm_ok and (llm_extract.start_situation or "").strip():
start = llm_extract.start_situation.strip()
meta.start_source = "llm"
elif parsed_start:
start = parsed_start
meta.start_source = "regex"
else:
start = ""
if user_target:
target = user_target
meta.target_source = "user"
elif llm_ok and (llm_extract.target_state or "").strip():
target = llm_extract.target_state.strip()
meta.target_source = "llm"
elif parsed_target:
target = parsed_target
meta.target_source = "regex"
else:
target = ""
llm_notes = (llm_extract.roadmap_notes or "").strip() if llm_ok and llm_extract else ""
if user_notes and llm_notes:
notes = _merge_roadmap_notes(user_notes, llm_notes) or ""
meta.notes_source = "merged"
elif user_notes:
notes = user_notes
meta.notes_source = "user"
elif llm_notes:
notes = llm_notes
meta.notes_source = "llm"
else:
notes = ""
meta.notes_source = "none"
if llm_ok and (llm_extract.primary_topic or "").strip():
meta.topic_source = "llm"
else:
meta.topic_source = "heuristic"
resolved = RoadmapStructuredInput(
start_situation=start or None,
target_state=target or None,
roadmap_notes=notes or None,
)
return resolved, meta, llm_extract if llm_ok else None
def parse_start_target_from_goal_query(goal_query: str) -> Tuple[Optional[str], Optional[str]]:
"""„von … bis …“ aus Freitext (z. B. Kumite Beinarbeit von X bis Y)."""
q = (goal_query or "").strip()
@ -343,9 +487,10 @@ def build_goal_analysis(
brief: PlanningSemanticBrief,
*,
structured: Optional[RoadmapStructuredInput] = None,
topic_override: Optional[str] = None,
) -> GoalAnalysisArtifact:
"""Phase A — aus Anfrage, optionalen Feldern und Semantic Brief."""
topic = _extract_topic_from_goal_query(goal_query, brief)
topic = (topic_override or "").strip() or _extract_topic_from_goal_query(goal_query, brief)
parsed_start, parsed_target = parse_start_target_from_goal_query(goal_query)
start = (structured.start_situation if structured else None) or parsed_start
@ -797,7 +942,7 @@ def _merge_structured_into_goal_analysis(
brief: PlanningSemanticBrief,
structured: Optional[RoadmapStructuredInput],
) -> GoalAnalysisArtifact:
ga_struct = build_goal_analysis(goal_query, brief, structured=structured)
ga_struct = build_goal_analysis(goal_query, brief, structured=structured, topic_override=None)
if not _has_specific_start_target(ga_struct):
return llm_ga
merged_criteria = list(
@ -822,14 +967,22 @@ def run_progression_roadmap_pipeline(
semantic_brief: Optional[PlanningSemanticBrief] = None,
cur=None,
include_llm_roadmap: bool = False,
include_llm_start_target: bool = False,
structured: Optional[RoadmapStructuredInput] = None,
) -> ProgressionRoadmapContext:
"""Workflow-lite: Phase A → B → C. LLM über ai_prompts-Slugs; deterministischer Fallback."""
brief = semantic_brief or build_semantic_brief(goal_query)
resolved, resolve_meta, llm_extract = resolve_roadmap_structured_input(
goal_query,
structured,
brief=brief,
cur=cur,
include_llm=include_llm_start_target,
)
parsed_start, parsed_target = parse_start_target_from_goal_query(goal_query)
llm_goal_query = _roadmap_llm_goal_block(
goal_query,
structured=structured,
structured=resolved,
parsed_start=parsed_start,
parsed_target=parsed_target,
)
@ -837,9 +990,24 @@ def run_progression_roadmap_pipeline(
goal_query=goal_query.strip(),
max_steps=max_steps,
semantic_brief=brief_to_summary_dict(brief),
resolved_structured=resolved,
start_target_extract=llm_extract,
start_target_resolve=resolve_meta,
llm_start_target_applied=resolve_meta.llm_start_target_applied,
)
if resolve_meta.llm_start_target_applied:
ctx.prompt_slugs.append(PROMPT_SLUG_START_TARGET)
goal_analysis = build_goal_analysis(goal_query, brief, structured=structured)
topic_override = None
if llm_extract and (llm_extract.primary_topic or "").strip():
topic_override = llm_extract.primary_topic.strip()
goal_analysis = build_goal_analysis(
goal_query,
brief,
structured=resolved,
topic_override=topic_override,
)
if include_llm_roadmap and cur is not None:
llm_ga, ga_ok = try_llm_goal_analysis(cur, goal_query=llm_goal_query, brief=brief)
if ga_ok and llm_ga:
@ -847,7 +1015,7 @@ def run_progression_roadmap_pipeline(
llm_ga,
goal_query=goal_query,
brief=brief,
structured=structured,
structured=resolved,
)
ctx.llm_goal_analysis_applied = True
ctx.prompt_slugs.append(PROMPT_SLUG_GOAL_ANALYSIS)
@ -901,18 +1069,37 @@ def run_progression_roadmap_pipeline(
def progression_roadmap_to_api_dict(ctx: ProgressionRoadmapContext) -> Dict[str, Any]:
resolve = ctx.start_target_resolve
return {
"goal_analysis": ctx.goal_analysis.model_dump() if ctx.goal_analysis else None,
"resolved_structured": (
ctx.resolved_structured.model_dump() if ctx.resolved_structured else None
),
"start_target_extract": (
ctx.start_target_extract.model_dump() if ctx.start_target_extract else None
),
"start_target_sources": (
{
"start": resolve.start_source,
"target": resolve.target_source,
"notes": resolve.notes_source,
"topic": resolve.topic_source,
}
if resolve
else None
),
"roadmap": ctx.roadmap.model_dump() if ctx.roadmap else None,
"stage_specs": [s.model_dump() for s in ctx.stage_specs],
"pipeline_phase": ctx.pipeline_phase,
"major_step_count": len(ctx.roadmap.major_steps) if ctx.roadmap else 0,
"micro_objective_count": len(ctx.roadmap.micro_objectives) if ctx.roadmap else 0,
"llm_start_target_applied": ctx.llm_start_target_applied,
"llm_goal_analysis_applied": ctx.llm_goal_analysis_applied,
"llm_roadmap_applied": ctx.llm_roadmap_applied,
"llm_stage_spec_applied": ctx.llm_stage_spec_applied,
"prompt_slugs": list(ctx.prompt_slugs),
"prompt_slug_catalog": {
"start_target": PROMPT_SLUG_START_TARGET,
"goal_analysis": PROMPT_SLUG_GOAL_ANALYSIS,
"roadmap": PROMPT_SLUG_ROADMAP,
"stage_spec": PROMPT_SLUG_STAGE_SPEC,
@ -921,6 +1108,7 @@ def progression_roadmap_to_api_dict(ctx: ProgressionRoadmapContext) -> Dict[str,
__all__ = [
"PROMPT_SLUG_START_TARGET",
"PROMPT_SLUG_GOAL_ANALYSIS",
"PROMPT_SLUG_ROADMAP",
"PROMPT_SLUG_STAGE_SPEC",
@ -931,8 +1119,11 @@ __all__ = [
"RoadmapArtifact",
"RoadmapOverridePayload",
"RoadmapStructuredInput",
"StartTargetExtractArtifact",
"StartTargetResolveMeta",
"normalize_major_steps_for_override",
"parse_start_target_from_goal_query",
"resolve_roadmap_structured_input",
"roadmap_context_from_override",
"StageSpecArtifact",
"build_goal_analysis",
@ -945,6 +1136,7 @@ __all__ = [
"develop_micro_objectives",
"progression_roadmap_to_api_dict",
"run_progression_roadmap_pipeline",
"try_llm_start_target_extract",
"try_llm_goal_analysis",
"try_llm_roadmap",
"try_llm_stage_specs",

View File

@ -72,6 +72,7 @@ def post_progression_path_suggest(
or body.include_llm_path_qa
or body.include_ai_gap_fill
or body.include_llm_roadmap
or body.include_llm_start_target
)
club_id = resolve_club_id_for_probe(tenant) if uses_ai else None
if uses_ai:

View File

@ -3,6 +3,7 @@ from planning_progression_roadmap import (
PROMPT_SLUG_GOAL_ANALYSIS,
PROMPT_SLUG_ROADMAP,
PROMPT_SLUG_STAGE_SPEC,
PROMPT_SLUG_START_TARGET,
MajorStep,
RoadmapStructuredInput,
StageSpecArtifact,
@ -12,6 +13,7 @@ from planning_progression_roadmap import (
develop_micro_objectives,
parse_start_target_from_goal_query,
progression_roadmap_to_api_dict,
resolve_roadmap_structured_input,
resolve_step_exercise_kind_filter,
run_progression_roadmap_pipeline,
stage_spec_exercise_kind_filter,
@ -137,12 +139,53 @@ def test_roadmap_context_from_override():
def test_api_dict_exposes_prompt_slug_catalog():
ctx = run_progression_roadmap_pipeline("Mae Geri", max_steps=3, include_llm_roadmap=False)
api = progression_roadmap_to_api_dict(ctx)
assert api["prompt_slug_catalog"]["start_target"] == PROMPT_SLUG_START_TARGET
assert api["prompt_slug_catalog"]["goal_analysis"] == PROMPT_SLUG_GOAL_ANALYSIS
assert api["prompt_slug_catalog"]["roadmap"] == PROMPT_SLUG_ROADMAP
assert api["prompt_slug_catalog"]["stage_spec"] == PROMPT_SLUG_STAGE_SPEC
assert api["prompt_slugs"] == []
def test_resolve_structured_user_overrides_regex():
brief = build_semantic_brief(KUMITE_GOAL)
structured = RoadmapStructuredInput(
start_situation="Trainer-Start explizit",
target_state="Trainer-Ziel explizit",
)
resolved, meta, llm_raw = resolve_roadmap_structured_input(
KUMITE_GOAL, structured, brief=brief, include_llm=False
)
assert llm_raw is None
assert resolved.start_situation == "Trainer-Start explizit"
assert resolved.target_state == "Trainer-Ziel explizit"
assert meta.start_source == "user"
assert meta.target_source == "user"
def test_resolve_structured_regex_fallback_without_llm():
brief = build_semantic_brief(KUMITE_GOAL)
resolved, meta, _ = resolve_roadmap_structured_input(
KUMITE_GOAL, None, brief=brief, include_llm=False
)
assert meta.start_source == "regex"
assert meta.target_source == "regex"
assert "Steppbewegung" in (resolved.start_situation or "")
assert "dynamischen" in (resolved.target_state or "")
def test_resolve_structured_merges_user_and_llm_notes():
brief = build_semantic_brief("Kumite Beinarbeit")
structured = RoadmapStructuredInput(roadmap_notes="Kindergruppe 1012")
resolved, meta, _ = resolve_roadmap_structured_input(
"Kumite Beinarbeit",
structured,
brief=brief,
include_llm=False,
)
assert resolved.roadmap_notes == "Kindergruppe 1012"
assert meta.notes_source == "user"
def test_parse_start_target_kumite_beinarbeit():
start, target = parse_start_target_from_goal_query(KUMITE_GOAL)
assert start is not None

View File

@ -1,8 +1,8 @@
# Shinkan Jinkendo Version Information
APP_VERSION = "0.8.210"
APP_VERSION = "0.8.211"
BUILD_DATE = "2026-06-07"
DB_SCHEMA_VERSION = "20260606086"
DB_SCHEMA_VERSION = "20260607087"
MODULE_VERSIONS = {
"legal_documents": "1.4.0", # Admin: Live-Vorschau pro Abschnitt + modale Vollvorschau (Editor + Dokumentenliste)
@ -38,7 +38,7 @@ MODULE_VERSIONS = {
"skill_profiles": "1.0.0", # Phase 3: gewichtetes Fähigkeiten-Profil + skill-discovery/suggestions
"methods": "0.1.0",
"exercises": "2.37.1", # KI-Endpoints: feature_usage nach ai_calls consume
"planning_exercise_suggest": "0.20.2", # Strukturierte Roadmap-Eingaben Start/Ziel + von-bis-Parsing
"planning_exercise_suggest": "0.21.0", # LLM Start/Ziel-Extraktion (planning_progression_start_target)
"training_units": "0.4.0", # POST .../publish-to-framework: Ablauf aus geplanter Einheit → Rahmen-Slot-Blueprint
"training_programs": "0.1.0",
"planning": "0.15.0", # Vorlagen: Strukturvorschau, Bearbeiten inkl. Split-Sessions + Beschreibung

View File

@ -12,18 +12,24 @@ import {
} from '../utils/exerciseAiQuickCreate'
import { buildPathGapPlanningContextForAi } from '../utils/planningContextForExerciseAi'
/** „von … bis …“ aus Freitext (z. B. Kumite Beinarbeit von X bis Y). */
function parseStartTargetFromGoalQuery(q) {
const text = String(q || '').trim()
if (!text) return { start: '', target: '' }
const m = text.match(
/\bvon\s+((?:(?:der|die|dem|das|einer?|einem)\s+)?.+?)\s+bis\s+(?:zur?|zum|zu der|zu einem)?\s*(.+)$/is,
)
if (!m) return { start: '', target: '' }
const start = m[1].trim().replace(/[.,;]+$/, '')
const target = m[2].trim().replace(/[.,;]+$/, '')
if (start.length < 4 || target.length < 4) return { start: '', target: '' }
return { start, target }
function applyResolvedStructuredFromRoadmap(progressionRoadmap, setters) {
const rs = progressionRoadmap?.resolved_structured
if (!rs) return
if (rs.start_situation) setters.setStartSituation(String(rs.start_situation))
if (rs.target_state) setters.setTargetState(String(rs.target_state))
if (rs.roadmap_notes) setters.setRoadmapNotes(String(rs.roadmap_notes))
}
function sourceLabel(source) {
const map = {
user: 'manuell',
llm: 'KI-Extraktion',
regex: 'Muster (von … bis …)',
merged: 'manuell + KI',
heuristic: 'heuristisch',
none: '—',
}
return map[source] || source || '—'
}
function roadmapStructuredPayload(startSituation, targetState, roadmapNotes) {
@ -535,19 +541,6 @@ export default function ExerciseProgressionPathBuilder({
alert('Zuerst einen Graphen wählen.')
return
}
let start = startSituation.trim()
let target = targetState.trim()
if (!start || !target) {
const parsed = parseStartTargetFromGoalQuery(q)
if (parsed.start && !start) {
start = parsed.start
setStartSituation(parsed.start)
}
if (parsed.target && !target) {
target = parsed.target
setTargetState(parsed.target)
}
}
setLoadingRoadmap(true)
setError('')
try {
@ -561,9 +554,10 @@ export default function ExerciseProgressionPathBuilder({
include_ai_gap_fill: false,
include_roadmap_preview: true,
include_llm_roadmap: true,
include_llm_start_target: true,
roadmap_only: true,
progression_graph_id: Number(graphId),
...roadmapStructuredPayload(start, target, roadmapNotes),
...roadmapStructuredPayload(startSituation, targetState, roadmapNotes),
})
const majors = mapMajorStepsFromApi(res?.progression_roadmap)
if (majors.length < 2) {
@ -571,7 +565,13 @@ export default function ExerciseProgressionPathBuilder({
}
setEditableMajorSteps(majors)
setMaxSteps(majors.length)
setProgressionRoadmap(res?.progression_roadmap || null)
const roadmap = res?.progression_roadmap || null
setProgressionRoadmap(roadmap)
applyResolvedStructuredFromRoadmap(roadmap, {
setStartSituation,
setTargetState,
setRoadmapNotes,
})
setSemanticBrief(res?.semantic_brief_summary || null)
setPathSteps([])
setTargetSummary(null)
@ -767,7 +767,8 @@ export default function ExerciseProgressionPathBuilder({
</div>
</div>
<p style={{ fontSize: '11px', color: 'var(--text3)', margin: '6px 0 0', lineHeight: 1.4 }}>
Bei von bis im Ziel werden Start und Ziel automatisch vorausgefüllt, wenn die Felder leer sind.
Leer gelassen: Start/Ziel werden per KI aus dem Zieltext verstanden und formuliert (Fallback: Muster
von bis ). Manuelle Eingaben haben Vorrang.
</p>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '10px', alignItems: 'flex-end', marginTop: '10px' }}>
<button
@ -819,6 +820,11 @@ export default function ExerciseProgressionPathBuilder({
>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px', alignItems: 'center', marginBottom: '8px' }}>
<strong style={{ fontSize: '13px' }}>Zielanalyse</strong>
{progressionRoadmap.llm_start_target_applied ? (
<span className="exercise-tag" style={{ borderColor: 'var(--accent)' }}>
KI Start/Ziel
</span>
) : null}
{progressionRoadmap.llm_goal_analysis_applied ? (
<span className="exercise-tag" style={{ borderColor: 'var(--accent)' }}>
KI-Zielanalyse
@ -826,6 +832,12 @@ export default function ExerciseProgressionPathBuilder({
) : (
<span className="exercise-tag">heuristisch</span>
)}
{progressionRoadmap.start_target_sources ? (
<span className="exercise-tag" style={{ fontSize: '11px' }}>
Start: {sourceLabel(progressionRoadmap.start_target_sources.start)} · Ziel:{' '}
{sourceLabel(progressionRoadmap.start_target_sources.target)}
</span>
) : null}
{progressionRoadmap.goal_analysis.primary_topic ? (
<span className="exercise-tag">Thema: {progressionRoadmap.goal_analysis.primary_topic}</span>
) : null}
@ -847,6 +859,11 @@ export default function ExerciseProgressionPathBuilder({
))}
</ul>
) : null}
{progressionRoadmap.start_target_extract?.extraction_notes ? (
<p style={{ margin: '8px 0 0', fontSize: '11px', color: 'var(--text3)' }}>
{progressionRoadmap.start_target_extract.extraction_notes}
</p>
) : null}
</div>
</div>
) : null}