Progressionsgraph verbessert #54
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user