Enhance Roadmap Slot Matching and Gap Offer Logic
All checks were successful
Deploy Development / deploy (push) Successful in 45s
Test Suite / pytest-backend (push) Successful in 45s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m22s
All checks were successful
Deploy Development / deploy (push) Successful in 45s
Test Suite / pytest-backend (push) Successful in 45s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m22s
- Introduced `_roadmap_step_passes_post_match_gate` to validate steps after matching, ensuring only relevant steps proceed. - Enhanced `_enrich_roadmap_unfilled_gap_offers` to generate AI gap offers for unfilled roadmap slots, improving exercise suggestions. - Updated `suggest_progression_path` to incorporate new gap offer logic and streamline the handling of roadmap steps. - Refined frontend logic in `applyMatchStepsToSlots` to better manage step assignments and improve clarity in slot handling. - Bumped version to 0.8.231 to reflect the new features and improvements.
This commit is contained in:
parent
d448c3191f
commit
63c99b0ec5
|
|
@ -1272,9 +1272,35 @@ def _match_roadmap_slot(
|
||||||
else:
|
else:
|
||||||
step["slot_status"] = "matched"
|
step["slot_status"] = "matched"
|
||||||
step["roadmap_match_source"] = "stage_spec"
|
step["roadmap_match_source"] = "stage_spec"
|
||||||
|
if step.get("roadmap_match_source") != "slot_best_match" and not _roadmap_step_passes_post_match_gate(
|
||||||
|
cur,
|
||||||
|
step,
|
||||||
|
goal_query=goal_query,
|
||||||
|
semantic_brief=semantic_brief,
|
||||||
|
):
|
||||||
|
return None, stage_spec
|
||||||
return step, None
|
return step, None
|
||||||
|
|
||||||
|
|
||||||
|
def _roadmap_step_passes_post_match_gate(
|
||||||
|
cur,
|
||||||
|
step: Dict[str, Any],
|
||||||
|
*,
|
||||||
|
goal_query: str,
|
||||||
|
semantic_brief: PlanningSemanticBrief,
|
||||||
|
) -> bool:
|
||||||
|
"""Abgleich mit Pfad-QA — kein Rematch-Treffer, der sofort wieder stage_mismatch wäre."""
|
||||||
|
if step.get("exercise_id") is None:
|
||||||
|
return False
|
||||||
|
issues = detect_off_topic_steps(
|
||||||
|
cur,
|
||||||
|
[step],
|
||||||
|
brief=semantic_brief,
|
||||||
|
goal_query=goal_query,
|
||||||
|
)
|
||||||
|
return not issues
|
||||||
|
|
||||||
|
|
||||||
def _normalize_roadmap_steps_coverage(
|
def _normalize_roadmap_steps_coverage(
|
||||||
steps: List[Dict[str, Any]],
|
steps: List[Dict[str, Any]],
|
||||||
*,
|
*,
|
||||||
|
|
@ -1394,6 +1420,92 @@ def _purge_stage_mismatch_roadmap_slots(
|
||||||
return out, new_unfilled
|
return out, new_unfilled
|
||||||
|
|
||||||
|
|
||||||
|
def _enrich_roadmap_unfilled_gap_offers(
|
||||||
|
cur,
|
||||||
|
*,
|
||||||
|
steps: List[Dict[str, Any]],
|
||||||
|
gap_fill_offers: List[Dict[str, Any]],
|
||||||
|
body: ProgressionPathSuggestRequest,
|
||||||
|
roadmap_ctx: ProgressionRoadmapContext,
|
||||||
|
goal_query: str,
|
||||||
|
semantic_brief: PlanningSemanticBrief,
|
||||||
|
) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
|
||||||
|
"""KI-Lücken-Angebote für alle leeren Roadmap-Slots (nach Rematch/Normalize)."""
|
||||||
|
if not body.include_ai_gap_fill:
|
||||||
|
return steps, gap_fill_offers
|
||||||
|
|
||||||
|
seen_offer_ids = {o.get("offer_id") for o in gap_fill_offers if o.get("offer_id")}
|
||||||
|
out_steps: List[Dict[str, Any]] = []
|
||||||
|
offers = list(gap_fill_offers)
|
||||||
|
|
||||||
|
for raw in steps:
|
||||||
|
step = dict(raw)
|
||||||
|
if step.get("exercise_id") is not None:
|
||||||
|
out_steps.append(step)
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
major_idx = int(step["roadmap_major_step_index"])
|
||||||
|
except (TypeError, ValueError, KeyError):
|
||||||
|
out_steps.append(step)
|
||||||
|
continue
|
||||||
|
if step.get("gap_offer") and step.get("proposal_key"):
|
||||||
|
oid = step["gap_offer"].get("offer_id")
|
||||||
|
if oid and oid not in seen_offer_ids:
|
||||||
|
offers.append(dict(step["gap_offer"]))
|
||||||
|
seen_offer_ids.add(oid)
|
||||||
|
out_steps.append(step)
|
||||||
|
continue
|
||||||
|
stage_spec = next(
|
||||||
|
(
|
||||||
|
s
|
||||||
|
for s in (roadmap_ctx.stage_specs or [])
|
||||||
|
if int(s.major_step_index) == major_idx
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
learning_goal = (
|
||||||
|
(stage_spec.learning_goal if stage_spec else None)
|
||||||
|
or step.get("roadmap_learning_goal")
|
||||||
|
or step.get("title")
|
||||||
|
or ""
|
||||||
|
).strip()
|
||||||
|
spec = {
|
||||||
|
"source": "roadmap_unfilled",
|
||||||
|
"insert_after_index": max(major_idx - 1, -1),
|
||||||
|
"roadmap_major_step_index": major_idx,
|
||||||
|
"phase": (step.get("roadmap_phase") or "vertiefung").strip().lower(),
|
||||||
|
"title_hint": (learning_goal or f"Slot {major_idx + 1}")[:120],
|
||||||
|
"sketch": learning_goal,
|
||||||
|
"rationale": (
|
||||||
|
f"Slot {major_idx + 1} — keine passende Bibliotheks-Übung; "
|
||||||
|
"KI-Entwurf für diese Stufe."
|
||||||
|
),
|
||||||
|
}
|
||||||
|
offer = build_gap_fill_offer(
|
||||||
|
spec=spec,
|
||||||
|
steps=steps,
|
||||||
|
goal_query=goal_query,
|
||||||
|
brief=semantic_brief,
|
||||||
|
proposal=None,
|
||||||
|
roadmap_snapshot=_roadmap_gap_snapshot_for_spec(
|
||||||
|
cur,
|
||||||
|
roadmap_ctx,
|
||||||
|
spec,
|
||||||
|
goal_query=goal_query,
|
||||||
|
semantic_brief=semantic_brief,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
step["gap_offer"] = offer
|
||||||
|
step["proposal_key"] = offer.get("offer_id")
|
||||||
|
step["slot_status"] = "unfilled"
|
||||||
|
if offer.get("offer_id") and offer.get("offer_id") not in seen_offer_ids:
|
||||||
|
offers.append(offer)
|
||||||
|
seen_offer_ids.add(offer.get("offer_id"))
|
||||||
|
out_steps.append(step)
|
||||||
|
|
||||||
|
return out_steps, offers
|
||||||
|
|
||||||
|
|
||||||
def _merge_rematch_unfilled(
|
def _merge_rematch_unfilled(
|
||||||
roadmap_unfilled: List[Tuple[int, StageSpecArtifact]],
|
roadmap_unfilled: List[Tuple[int, StageSpecArtifact]],
|
||||||
rematch_new_unfilled: List[Tuple[int, StageSpecArtifact]],
|
rematch_new_unfilled: List[Tuple[int, StageSpecArtifact]],
|
||||||
|
|
@ -2269,7 +2381,7 @@ def suggest_progression_path(
|
||||||
elif gaps and roadmap_first:
|
elif gaps and roadmap_first:
|
||||||
unfilled_gaps = list(gaps)
|
unfilled_gaps = list(gaps)
|
||||||
|
|
||||||
if body.include_llm_path_qa:
|
if body.include_llm_path_qa and not roadmap_first:
|
||||||
llm_qa, llm_qa_applied = try_llm_qa_progression_path(
|
llm_qa, llm_qa_applied = try_llm_qa_progression_path(
|
||||||
cur,
|
cur,
|
||||||
goal_query=goal_query,
|
goal_query=goal_query,
|
||||||
|
|
@ -2348,6 +2460,22 @@ def suggest_progression_path(
|
||||||
roadmap_first=roadmap_first,
|
roadmap_first=roadmap_first,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if body.include_llm_path_qa and roadmap_first:
|
||||||
|
gaps = detect_path_gaps(
|
||||||
|
cur,
|
||||||
|
steps,
|
||||||
|
brief=semantic_brief,
|
||||||
|
roadmap_first=roadmap_first,
|
||||||
|
)
|
||||||
|
llm_qa, llm_qa_applied = try_llm_qa_progression_path(
|
||||||
|
cur,
|
||||||
|
goal_query=goal_query,
|
||||||
|
brief=semantic_brief,
|
||||||
|
steps=steps,
|
||||||
|
gaps=gaps,
|
||||||
|
bridge_inserts=bridge_inserts,
|
||||||
|
)
|
||||||
|
|
||||||
llm_gap_specs = parse_llm_suggested_new_exercises(
|
llm_gap_specs = parse_llm_suggested_new_exercises(
|
||||||
llm_qa,
|
llm_qa,
|
||||||
brief=semantic_brief,
|
brief=semantic_brief,
|
||||||
|
|
@ -2397,6 +2525,22 @@ def suggest_progression_path(
|
||||||
if offer.get("offer_id") not in seen_offer_ids:
|
if offer.get("offer_id") not in seen_offer_ids:
|
||||||
gap_fill_offers.append(offer)
|
gap_fill_offers.append(offer)
|
||||||
|
|
||||||
|
if roadmap_first and roadmap_ctx is not None:
|
||||||
|
steps = _normalize_roadmap_steps_coverage(
|
||||||
|
steps,
|
||||||
|
roadmap_ctx=roadmap_ctx,
|
||||||
|
max_steps=max_steps,
|
||||||
|
)
|
||||||
|
steps, gap_fill_offers = _enrich_roadmap_unfilled_gap_offers(
|
||||||
|
cur,
|
||||||
|
steps=steps,
|
||||||
|
gap_fill_offers=gap_fill_offers,
|
||||||
|
body=body,
|
||||||
|
roadmap_ctx=roadmap_ctx,
|
||||||
|
goal_query=goal_query,
|
||||||
|
semantic_brief=semantic_brief,
|
||||||
|
)
|
||||||
|
|
||||||
multistage_qa = run_multistage_path_qa(
|
multistage_qa = run_multistage_path_qa(
|
||||||
off_topic_steps=off_topic_steps,
|
off_topic_steps=off_topic_steps,
|
||||||
stripped_off_topic=stripped_off_topic,
|
stripped_off_topic=stripped_off_topic,
|
||||||
|
|
@ -2428,71 +2572,6 @@ def suggest_progression_path(
|
||||||
path_qa["refine_log"] = refine_log
|
path_qa["refine_log"] = refine_log
|
||||||
path_qa["refine_count"] = len(refine_log)
|
path_qa["refine_count"] = len(refine_log)
|
||||||
|
|
||||||
if roadmap_first and roadmap_ctx is not None:
|
|
||||||
steps = _normalize_roadmap_steps_coverage(
|
|
||||||
steps,
|
|
||||||
roadmap_ctx=roadmap_ctx,
|
|
||||||
max_steps=max_steps,
|
|
||||||
)
|
|
||||||
if body.include_ai_gap_fill:
|
|
||||||
seen_offer_ids = {o.get("offer_id") for o in gap_fill_offers if o.get("offer_id")}
|
|
||||||
for step in steps:
|
|
||||||
if step.get("exercise_id") is not None:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
major_idx = int(step["roadmap_major_step_index"])
|
|
||||||
except (TypeError, ValueError, KeyError):
|
|
||||||
continue
|
|
||||||
if step.get("gap_offer") and step.get("proposal_key"):
|
|
||||||
oid = step["gap_offer"].get("offer_id")
|
|
||||||
if oid and oid not in seen_offer_ids:
|
|
||||||
gap_fill_offers.append(dict(step["gap_offer"]))
|
|
||||||
seen_offer_ids.add(oid)
|
|
||||||
continue
|
|
||||||
stage_spec = next(
|
|
||||||
(
|
|
||||||
s
|
|
||||||
for s in (roadmap_ctx.stage_specs or [])
|
|
||||||
if int(s.major_step_index) == major_idx
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
learning_goal = (
|
|
||||||
(stage_spec.learning_goal if stage_spec else None)
|
|
||||||
or step.get("roadmap_learning_goal")
|
|
||||||
or step.get("title")
|
|
||||||
or ""
|
|
||||||
).strip()
|
|
||||||
spec = {
|
|
||||||
"source": "roadmap_unfilled",
|
|
||||||
"insert_after_index": max(major_idx - 1, -1),
|
|
||||||
"roadmap_major_step_index": major_idx,
|
|
||||||
"phase": (step.get("roadmap_phase") or "vertiefung").strip().lower(),
|
|
||||||
"title_hint": (learning_goal or f"Slot {major_idx + 1}")[:120],
|
|
||||||
"sketch": learning_goal,
|
|
||||||
"rationale": f"Slot {major_idx + 1} — keine passende Bibliotheks-Übung; KI-Entwurf für diese Stufe.",
|
|
||||||
}
|
|
||||||
offer = build_gap_fill_offer(
|
|
||||||
spec=spec,
|
|
||||||
steps=steps,
|
|
||||||
goal_query=goal_query,
|
|
||||||
brief=semantic_brief,
|
|
||||||
proposal=None,
|
|
||||||
roadmap_snapshot=_roadmap_gap_snapshot_for_spec(
|
|
||||||
cur,
|
|
||||||
roadmap_ctx,
|
|
||||||
spec,
|
|
||||||
goal_query=goal_query,
|
|
||||||
semantic_brief=semantic_brief,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
step["gap_offer"] = offer
|
|
||||||
step["proposal_key"] = offer.get("offer_id")
|
|
||||||
step["slot_status"] = "unfilled"
|
|
||||||
if offer.get("offer_id") and offer.get("offer_id") not in seen_offer_ids:
|
|
||||||
gap_fill_offers.append(offer)
|
|
||||||
seen_offer_ids.add(offer.get("offer_id"))
|
|
||||||
|
|
||||||
filled_library_steps = sum(1 for s in steps if s.get("exercise_id") is not None)
|
filled_library_steps = sum(1 for s in steps if s.get("exercise_id") is not None)
|
||||||
match_summary = {
|
match_summary = {
|
||||||
"roadmap_first": roadmap_first,
|
"roadmap_first": roadmap_first,
|
||||||
|
|
|
||||||
|
|
@ -207,8 +207,29 @@ def rematch_roadmap_slots(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
goal = (stage_spec.learning_goal or "").strip()
|
||||||
|
major = None
|
||||||
|
if roadmap_ctx.roadmap:
|
||||||
|
major = next(
|
||||||
|
(m for m in roadmap_ctx.roadmap.major_steps if int(m.index) == int(major_idx)),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
steps_by_major[int(major_idx)] = {
|
||||||
|
"exercise_id": None,
|
||||||
|
"variant_id": None,
|
||||||
|
"title": goal or f"Slot {major_idx + 1}",
|
||||||
|
"is_ai_proposal": False,
|
||||||
|
"roadmap_major_step_index": int(major_idx),
|
||||||
|
"roadmap_phase": major.phase if major else None,
|
||||||
|
"roadmap_learning_goal": goal or None,
|
||||||
|
"roadmap_match_source": "unfilled",
|
||||||
|
"slot_status": "unfilled",
|
||||||
|
"reasons": ["Keine passende Übung für Roadmap-Stufe"],
|
||||||
|
}
|
||||||
if unfilled_spec is not None:
|
if unfilled_spec is not None:
|
||||||
new_unfilled.append((step_index, unfilled_spec))
|
new_unfilled.append((step_index, unfilled_spec))
|
||||||
|
elif stage_spec is not None:
|
||||||
|
new_unfilled.append((step_index, stage_spec))
|
||||||
rematch_log.append(
|
rematch_log.append(
|
||||||
{
|
{
|
||||||
"roadmap_major_step_index": int(major_idx),
|
"roadmap_major_step_index": int(major_idx),
|
||||||
|
|
|
||||||
|
|
@ -183,3 +183,43 @@ def test_rematch_excludes_replaced_exercise_from_used():
|
||||||
match_slot_fn=_fake_match,
|
match_slot_fn=_fake_match,
|
||||||
)
|
)
|
||||||
assert 99 in seen_used[0]
|
assert 99 in seen_used[0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_rematch_unfilled_leaves_placeholder_step():
|
||||||
|
specs = _stage_specs()
|
||||||
|
ctx = ProgressionRoadmapContext(
|
||||||
|
goal_query="Mae Geri",
|
||||||
|
max_steps=3,
|
||||||
|
stage_specs=specs,
|
||||||
|
)
|
||||||
|
steps = [
|
||||||
|
{"exercise_id": 10, "title": "OK", "roadmap_major_step_index": 0},
|
||||||
|
{"exercise_id": 99, "title": "Falsch", "roadmap_major_step_index": 1},
|
||||||
|
]
|
||||||
|
|
||||||
|
def _no_match(cur, *, stage_spec, **kwargs):
|
||||||
|
return None, stage_spec
|
||||||
|
|
||||||
|
ordered, log, unfilled = rematch_roadmap_slots(
|
||||||
|
None,
|
||||||
|
tenant=None,
|
||||||
|
body=None,
|
||||||
|
goal_query="Mae Geri",
|
||||||
|
max_steps=3,
|
||||||
|
semantic_brief=None,
|
||||||
|
path_target_profile=None,
|
||||||
|
path_intent="",
|
||||||
|
roadmap_ctx=ctx,
|
||||||
|
steps=steps,
|
||||||
|
slot_indices={1},
|
||||||
|
rematch_reasons={1: "stage_mismatch"},
|
||||||
|
match_slot_fn=_no_match,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(ordered) == 2
|
||||||
|
slot1 = ordered[1]
|
||||||
|
assert slot1["exercise_id"] is None
|
||||||
|
assert slot1["slot_status"] == "unfilled"
|
||||||
|
assert slot1["roadmap_match_source"] == "unfilled"
|
||||||
|
assert log[0]["action"] == "rematch_unfilled"
|
||||||
|
assert len(unfilled) == 1
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Shinkan Jinkendo Version Information
|
# Shinkan Jinkendo Version Information
|
||||||
|
|
||||||
APP_VERSION = "0.8.230"
|
APP_VERSION = "0.8.231"
|
||||||
BUILD_DATE = "2026-05-22"
|
BUILD_DATE = "2026-05-22"
|
||||||
DB_SCHEMA_VERSION = "20260607090"
|
DB_SCHEMA_VERSION = "20260607090"
|
||||||
|
|
||||||
|
|
@ -38,7 +38,7 @@ MODULE_VERSIONS = {
|
||||||
"skill_profiles": "1.0.0", # Phase 3: gewichtetes Fähigkeiten-Profil + skill-discovery/suggestions
|
"skill_profiles": "1.0.0", # Phase 3: gewichtetes Fähigkeiten-Profil + skill-discovery/suggestions
|
||||||
"methods": "0.1.0",
|
"methods": "0.1.0",
|
||||||
"exercises": "2.37.1", # KI-Endpoints: feature_usage nach ai_calls consume
|
"exercises": "2.37.1", # KI-Endpoints: feature_usage nach ai_calls consume
|
||||||
"planning_exercise_suggest": "0.23.5", # Roadmap-Match strikt; stage_mismatch → unfilled + KI-Gap
|
"planning_exercise_suggest": "0.23.6", # Gap-Angebote nach Rematch; LLM-QA auf finalem Pfad; Post-Match-Gate
|
||||||
"training_units": "0.4.0", # POST .../publish-to-framework: Ablauf aus geplanter Einheit → Rahmen-Slot-Blueprint
|
"training_units": "0.4.0", # POST .../publish-to-framework: Ablauf aus geplanter Einheit → Rahmen-Slot-Blueprint
|
||||||
"training_programs": "0.1.0",
|
"training_programs": "0.1.0",
|
||||||
"planning": "0.15.0", # Vorlagen: Strukturvorschau, Bearbeiten inkl. Split-Sessions + Beschreibung
|
"planning": "0.15.0", # Vorlagen: Strukturvorschau, Bearbeiten inkl. Split-Sessions + Beschreibung
|
||||||
|
|
|
||||||
|
|
@ -856,21 +856,16 @@ export function slotsToEvaluateSteps(draft) {
|
||||||
|
|
||||||
export function applyMatchStepsToSlots(draft, apiSteps) {
|
export function applyMatchStepsToSlots(draft, apiSteps) {
|
||||||
const steps = Array.isArray(apiSteps) ? apiSteps : []
|
const steps = Array.isArray(apiSteps) ? apiSteps : []
|
||||||
const nextSlots = (draft.slots || []).map((slot) => ({
|
const stepByMajor = new Map()
|
||||||
...slot,
|
|
||||||
primary: { ...slot.primary },
|
|
||||||
siblings: [...(slot.siblings || [])],
|
|
||||||
}))
|
|
||||||
|
|
||||||
const touchedMajors = new Set()
|
|
||||||
for (const step of steps) {
|
for (const step of steps) {
|
||||||
if (step.roadmap_major_step_index == null || !Number.isFinite(Number(step.roadmap_major_step_index))) {
|
if (step.roadmap_major_step_index == null || !Number.isFinite(Number(step.roadmap_major_step_index))) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const idx = Number(step.roadmap_major_step_index)
|
stepByMajor.set(Number(step.roadmap_major_step_index), step)
|
||||||
if (idx < 0 || idx >= nextSlots.length) continue
|
}
|
||||||
touchedMajors.add(idx)
|
|
||||||
|
|
||||||
|
const mapStepToPrimary = (step, slot) => {
|
||||||
|
const midx = Number(slot.majorStepIndex)
|
||||||
const isProposal = Boolean(step.is_ai_proposal) || step.exercise_id == null
|
const isProposal = Boolean(step.is_ai_proposal) || step.exercise_id == null
|
||||||
const hasAiPayload = Boolean(step.ai_suggestion) || Boolean(step.proposal_key)
|
const hasAiPayload = Boolean(step.ai_suggestion) || Boolean(step.proposal_key)
|
||||||
const isUnfilledSlot =
|
const isUnfilledSlot =
|
||||||
|
|
@ -880,32 +875,49 @@ export function applyMatchStepsToSlots(draft, apiSteps) {
|
||||||
Boolean(step.gap_offer)
|
Boolean(step.gap_offer)
|
||||||
if (isProposal && !hasAiPayload && isUnfilledSlot) {
|
if (isProposal && !hasAiPayload && isUnfilledSlot) {
|
||||||
const offer = step.gap_offer || {}
|
const offer = step.gap_offer || {}
|
||||||
nextSlots[idx].primary = proposalSlotExercise({
|
return proposalSlotExercise({
|
||||||
title:
|
title:
|
||||||
offer.title_hint ||
|
offer.title_hint ||
|
||||||
step.roadmap_learning_goal ||
|
step.roadmap_learning_goal ||
|
||||||
step.title ||
|
step.title ||
|
||||||
nextSlots[idx].learning_goal ||
|
slot.learning_goal ||
|
||||||
`Slot ${idx + 1}`,
|
`Slot ${midx + 1}`,
|
||||||
proposalKey: offer.offer_id || step.proposal_key || `roadmap-unfilled-${idx}`,
|
proposalKey: offer.offer_id || step.proposal_key || `roadmap-unfilled-${midx}`,
|
||||||
aiSuggestion: offer.ai_suggestion || null,
|
aiSuggestion: offer.ai_suggestion || null,
|
||||||
})
|
})
|
||||||
} else if (isProposal && !hasAiPayload) {
|
}
|
||||||
nextSlots[idx].primary = emptySlotExercise()
|
if (isProposal && !hasAiPayload) {
|
||||||
} else if (isProposal) {
|
return emptySlotExercise()
|
||||||
nextSlots[idx].primary = proposalSlotExercise({
|
}
|
||||||
title: step.title || nextSlots[idx].learning_goal,
|
if (isProposal) {
|
||||||
|
return proposalSlotExercise({
|
||||||
|
title: step.title || slot.learning_goal,
|
||||||
proposalKey: step.proposal_key,
|
proposalKey: step.proposal_key,
|
||||||
aiSuggestion: step.ai_suggestion,
|
aiSuggestion: step.ai_suggestion,
|
||||||
})
|
})
|
||||||
} else {
|
}
|
||||||
nextSlots[idx].primary = librarySlotExercise({
|
return librarySlotExercise({
|
||||||
exerciseId: step.exercise_id,
|
exerciseId: step.exercise_id,
|
||||||
exerciseTitle: step.title || `Übung #${step.exercise_id}`,
|
exerciseTitle: step.title || `Übung #${step.exercise_id}`,
|
||||||
variantId: step.variant_id,
|
variantId: step.variant_id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nextSlots = (draft.slots || []).map((slot) => {
|
||||||
|
const base = {
|
||||||
|
...slot,
|
||||||
|
primary: { ...slot.primary },
|
||||||
|
siblings: [...(slot.siblings || [])],
|
||||||
}
|
}
|
||||||
|
const step = stepByMajor.get(Number(slot.majorStepIndex))
|
||||||
|
if (!step) {
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
primary: mapStepToPrimary(step, slot),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return syncProgressionRoadmapFromSlots({ ...draft, slots: nextSlots, dirty: true })
|
return syncProgressionRoadmapFromSlots({ ...draft, slots: nextSlots, dirty: true })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user