Enhance Gap Fill Offer with Context Preview and Update Version
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

- 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.
This commit is contained in:
Lars 2026-06-09 16:27:03 +02:00
parent f2650dac57
commit d4b1780193
6 changed files with 136 additions and 12 deletions

View File

@ -349,6 +349,7 @@ def build_gap_fill_offer(
step_b=step_b, step_b=step_b,
roadmap_snapshot=roadmap_snapshot, roadmap_snapshot=roadmap_snapshot,
) )
ctx_preview = dict(roadmap_snapshot) if roadmap_snapshot else None
offer: Dict[str, Any] = { offer: Dict[str, Any] = {
"offer_id": offer_id, "offer_id": offer_id,
"source": spec.get("source"), "source": spec.get("source"),
@ -357,12 +358,14 @@ def build_gap_fill_offer(
"title_hint": spec.get("title_hint"), "title_hint": spec.get("title_hint"),
"sketch": spec.get("sketch"), "sketch": spec.get("sketch"),
"goal_for_ai": goal_for_ai or spec.get("sketch"), "goal_for_ai": goal_for_ai or spec.get("sketch"),
"context_preview": ctx_preview,
"phase": spec.get("phase"), "phase": spec.get("phase"),
"rationale": spec.get("rationale"), "rationale": spec.get("rationale"),
"has_ai_payload": False, "has_ai_payload": False,
"from_title": (step_a or {}).get("title"), "from_title": (step_a or {}).get("title"),
"to_title": (step_b or {}).get("title"), "to_title": (step_b or {}).get("title"),
"primary_topic": (brief.primary_topic if brief else None), "primary_topic": (brief.primary_topic if brief else None),
"roadmap_major_step_index": spec.get("roadmap_major_step_index"),
} }
if proposal: if proposal:
offer["has_ai_payload"] = True offer["has_ai_payload"] = True

View File

@ -1,5 +1,9 @@
"""Tests Planungs-KI Phase E3 — Lücken-Angebote und Off-Topic.""" """Tests Planungs-KI Phase E3 — Lücken-Angebote und Off-Topic."""
from planning_exercise_path_ai_fill import build_gap_fill_goal_text, collect_gap_fill_specs from planning_exercise_path_ai_fill import (
build_gap_fill_goal_text,
build_gap_fill_offer,
collect_gap_fill_specs,
)
from planning_exercise_path_qa import parse_llm_suggested_new_exercises, strip_off_topic_steps_from_path from planning_exercise_path_qa import parse_llm_suggested_new_exercises, strip_off_topic_steps_from_path
from planning_exercise_semantics import build_semantic_brief from planning_exercise_semantics import build_semantic_brief
@ -113,3 +117,20 @@ def test_build_gap_fill_goal_text_includes_roadmap_snapshot():
assert "explosiver Angriff" in text assert "explosiver Angriff" in text
assert "variable Rhythmen" in text assert "variable Rhythmen" in text
assert "timing" in text assert "timing" in text
def test_build_gap_fill_offer_exposes_context_preview():
brief = build_semantic_brief("Kumite Beinarbeit")
offer = build_gap_fill_offer(
spec={"source": "roadmap_unfilled", "phase": "vertiefung", "title_hint": "Rhythmen"},
steps=[{"title": "A"}, {"title": "B"}],
goal_query="Kumite Beinarbeit",
brief=brief,
roadmap_snapshot={
"start_situation": "Steppbewegung",
"target_state": "explosiver Angriff",
"stage_learning_goal": "variable Rhythmen",
},
)
assert offer["context_preview"]["start_situation"] == "Steppbewegung"
assert "variable Rhythmen" in offer["goal_for_ai"]

View File

@ -1,6 +1,6 @@
# Shinkan Jinkendo Version Information # Shinkan Jinkendo Version Information
APP_VERSION = "0.8.212" APP_VERSION = "0.8.213"
BUILD_DATE = "2026-06-07" BUILD_DATE = "2026-06-07"
DB_SCHEMA_VERSION = "20260607087" DB_SCHEMA_VERSION = "20260607087"

View File

@ -21,6 +21,7 @@ export default function ExerciseAiSuggestPreviewModal({
applyLabel = 'Übung anlegen', applyLabel = 'Übung anlegen',
applyDisabled = false, applyDisabled = false,
zIndex = 2000, zIndex = 2000,
planningContextLines = [],
}) { }) {
useEffect(() => { useEffect(() => {
if (!draft) return undefined if (!draft) return undefined
@ -86,6 +87,31 @@ export default function ExerciseAiSuggestPreviewModal({
<h3 style={{ marginTop: 0, fontSize: '1.1rem', marginBottom: '6px' }}>{dialogTitle}</h3> <h3 style={{ marginTop: 0, fontSize: '1.1rem', marginBottom: '6px' }}>{dialogTitle}</h3>
<p style={{ fontSize: '13px', color: 'var(--text3)', marginTop: 0, marginBottom: '16px' }}>{hint}</p> <p style={{ fontSize: '13px', color: 'var(--text3)', marginTop: 0, marginBottom: '16px' }}>{hint}</p>
{Array.isArray(planningContextLines) && planningContextLines.length > 0 ? (
<div
style={{
marginBottom: '16px',
padding: '10px 12px',
borderRadius: '8px',
border: '1px solid color-mix(in srgb, var(--accent) 35%, var(--border))',
background: 'color-mix(in srgb, var(--accent) 6%, var(--surface))',
fontSize: '12px',
}}
>
<strong style={{ display: 'block', marginBottom: '6px', fontSize: '13px' }}>
Planungskontext (an KI übergeben)
</strong>
<dl style={{ margin: 0, display: 'grid', gap: '6px' }}>
{planningContextLines.map(({ label, value }) => (
<div key={label}>
<dt style={{ margin: 0, fontSize: '11px', color: 'var(--text3)' }}>{label}</dt>
<dd style={{ margin: '2px 0 0', lineHeight: 1.45, color: 'var(--text2)' }}>{value}</dd>
</div>
))}
</dl>
</div>
) : null}
<div style={{ display: 'grid', gap: '12px', marginBottom: '18px' }}> <div style={{ display: 'grid', gap: '12px', marginBottom: '18px' }}>
<div> <div>
<label className="form-label" htmlFor="ai-draft-title"> <label className="form-label" htmlFor="ai-draft-title">

View File

@ -10,7 +10,10 @@ import {
buildQuickCreateAiPreview, buildQuickCreateAiPreview,
buildQuickCreateExercisePayloadFromDraft, buildQuickCreateExercisePayloadFromDraft,
} from '../utils/exerciseAiQuickCreate' } from '../utils/exerciseAiQuickCreate'
import { buildPathGapPlanningContextForAi } from '../utils/planningContextForExerciseAi' import {
buildPathGapPlanningContextForAi,
gapOfferContextDisplayLines,
} from '../utils/planningContextForExerciseAi'
function applyResolvedStructuredFromRoadmap(progressionRoadmap, setters) { function applyResolvedStructuredFromRoadmap(progressionRoadmap, setters) {
const rs = progressionRoadmap?.resolved_structured const rs = progressionRoadmap?.resolved_structured
@ -20,6 +23,37 @@ function applyResolvedStructuredFromRoadmap(progressionRoadmap, setters) {
if (rs.roadmap_notes) setters.setRoadmapNotes(String(rs.roadmap_notes)) if (rs.roadmap_notes) setters.setRoadmapNotes(String(rs.roadmap_notes))
} }
function GapOfferContextPreview({ lines }) {
if (!Array.isArray(lines) || lines.length === 0) return null
return (
<details
style={{
marginTop: '8px',
fontSize: '12px',
color: 'var(--text2)',
borderTop: '1px dashed var(--border)',
paddingTop: '8px',
}}
>
<summary style={{ cursor: 'pointer', color: 'var(--accent-dark)', fontWeight: 600 }}>
KI-Kontext für diese Übung ({lines.length} Punkte)
</summary>
<dl style={{ margin: '8px 0 0', display: 'grid', gap: '6px' }}>
{lines.map(({ label, value }) => (
<div key={label}>
<dt style={{ margin: 0, fontSize: '11px', color: 'var(--text3)' }}>{label}</dt>
<dd style={{ margin: '2px 0 0', lineHeight: 1.45 }}>{value}</dd>
</div>
))}
</dl>
<p style={{ margin: '8px 0 0', fontSize: '11px', color: 'var(--text3)', lineHeight: 1.4 }}>
Dieser Kontext wird an die Übungs-KI übergeben (Ziel, Fähigkeiten, Anleitung) nicht nur das
Stufen-Lernziel oben.
</p>
</details>
)
}
function sourceLabel(source) { function sourceLabel(source) {
const map = { const map = {
user: 'manuell', user: 'manuell',
@ -217,6 +251,7 @@ export default function ExerciseProgressionPathBuilder({
const [quickCreateDraft, setQuickCreateDraft] = useState(null) const [quickCreateDraft, setQuickCreateDraft] = useState(null)
const [quickSaving, setQuickSaving] = useState(false) const [quickSaving, setQuickSaving] = useState(false)
const [quickAiError, setQuickAiError] = useState('') const [quickAiError, setQuickAiError] = useState('')
const [activePlanningContextLines, setActivePlanningContextLines] = useState([])
useEffect(() => { useEffect(() => {
let cancelled = false let cancelled = false
@ -363,6 +398,18 @@ export default function ExerciseProgressionPathBuilder({
await runGapFillAiSuggest(offer) await runGapFillAiSuggest(offer)
} }
const gapContextFallbackParams = {
goalQuery,
semanticBrief,
graphId,
pathSteps,
editableMajorSteps,
progressionRoadmap,
startSituation,
targetState,
roadmapNotes,
}
const runGapFillAiSuggest = async (offer) => { const runGapFillAiSuggest = async (offer) => {
const title = (offer?.title_hint || '').trim() const title = (offer?.title_hint || '').trim()
if (title.length < 3) { if (title.length < 3) {
@ -391,17 +438,11 @@ export default function ExerciseProgressionPathBuilder({
setQuickCreateDraft(null) setQuickCreateDraft(null)
setQuickSaving(true) setQuickSaving(true)
setGeneratingOfferId(offer?.offer_id || null) setGeneratingOfferId(offer?.offer_id || null)
const contextLines = gapOfferContextDisplayLines(offer, gapContextFallbackParams)
setActivePlanningContextLines(contextLines)
const planningContext = buildPathGapPlanningContextForAi({ const planningContext = buildPathGapPlanningContextForAi({
goalQuery,
semanticBrief,
offer, offer,
graphId, ...gapContextFallbackParams,
pathSteps,
editableMajorSteps,
progressionRoadmap,
startSituation,
targetState,
roadmapNotes,
}) })
try { try {
const aiRes = await api.suggestExerciseAi({ const aiRes = await api.suggestExerciseAi({
@ -1193,6 +1234,9 @@ export default function ExerciseProgressionPathBuilder({
{offer.replace_step_index != null ? ' (ersetzt themenfremden Schritt)' : ''} {offer.replace_step_index != null ? ' (ersetzt themenfremden Schritt)' : ''}
</p> </p>
) : null} ) : null}
<GapOfferContextPreview
lines={gapOfferContextDisplayLines(offer, gapContextFallbackParams)}
/>
</div> </div>
<button <button
type="button" type="button"
@ -1384,8 +1428,10 @@ export default function ExerciseProgressionPathBuilder({
onDraftChange={setQuickCreateDraft} onDraftChange={setQuickCreateDraft}
onDiscard={() => { onDiscard={() => {
setQuickCreateDraft(null) setQuickCreateDraft(null)
setActivePlanningContextLines([])
if (activeOffer) setQuickCreateOpen(true) if (activeOffer) setQuickCreateOpen(true)
}} }}
planningContextLines={activePlanningContextLines}
onApply={applyQuickCreateDraft} onApply={applyQuickCreateDraft}
focusAreas={focusAreas} focusAreas={focusAreas}
skillsCatalog={skillsCatalog} skillsCatalog={skillsCatalog}

View File

@ -132,3 +132,31 @@ export function buildPathGapPlanningContextForAi({
} }
return Object.fromEntries(Object.entries(ctx).filter(([, v]) => v != null && v !== '')) 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
}