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
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:
parent
f2650dac57
commit
d4b1780193
|
|
@ -349,6 +349,7 @@ def build_gap_fill_offer(
|
|||
step_b=step_b,
|
||||
roadmap_snapshot=roadmap_snapshot,
|
||||
)
|
||||
ctx_preview = dict(roadmap_snapshot) if roadmap_snapshot else None
|
||||
offer: Dict[str, Any] = {
|
||||
"offer_id": offer_id,
|
||||
"source": spec.get("source"),
|
||||
|
|
@ -357,12 +358,14 @@ def build_gap_fill_offer(
|
|||
"title_hint": spec.get("title_hint"),
|
||||
"sketch": spec.get("sketch"),
|
||||
"goal_for_ai": goal_for_ai or spec.get("sketch"),
|
||||
"context_preview": ctx_preview,
|
||||
"phase": spec.get("phase"),
|
||||
"rationale": spec.get("rationale"),
|
||||
"has_ai_payload": False,
|
||||
"from_title": (step_a or {}).get("title"),
|
||||
"to_title": (step_b or {}).get("title"),
|
||||
"primary_topic": (brief.primary_topic if brief else None),
|
||||
"roadmap_major_step_index": spec.get("roadmap_major_step_index"),
|
||||
}
|
||||
if proposal:
|
||||
offer["has_ai_payload"] = True
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
"""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_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 "variable Rhythmen" 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
|
||||
|
||||
APP_VERSION = "0.8.212"
|
||||
APP_VERSION = "0.8.213"
|
||||
BUILD_DATE = "2026-06-07"
|
||||
DB_SCHEMA_VERSION = "20260607087"
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export default function ExerciseAiSuggestPreviewModal({
|
|||
applyLabel = 'Übung anlegen',
|
||||
applyDisabled = false,
|
||||
zIndex = 2000,
|
||||
planningContextLines = [],
|
||||
}) {
|
||||
useEffect(() => {
|
||||
if (!draft) return undefined
|
||||
|
|
@ -86,6 +87,31 @@ export default function ExerciseAiSuggestPreviewModal({
|
|||
<h3 style={{ marginTop: 0, fontSize: '1.1rem', marginBottom: '6px' }}>{dialogTitle}</h3>
|
||||
<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>
|
||||
<label className="form-label" htmlFor="ai-draft-title">
|
||||
|
|
|
|||
|
|
@ -10,7 +10,10 @@ import {
|
|||
buildQuickCreateAiPreview,
|
||||
buildQuickCreateExercisePayloadFromDraft,
|
||||
} from '../utils/exerciseAiQuickCreate'
|
||||
import { buildPathGapPlanningContextForAi } from '../utils/planningContextForExerciseAi'
|
||||
import {
|
||||
buildPathGapPlanningContextForAi,
|
||||
gapOfferContextDisplayLines,
|
||||
} from '../utils/planningContextForExerciseAi'
|
||||
|
||||
function applyResolvedStructuredFromRoadmap(progressionRoadmap, setters) {
|
||||
const rs = progressionRoadmap?.resolved_structured
|
||||
|
|
@ -20,6 +23,37 @@ function applyResolvedStructuredFromRoadmap(progressionRoadmap, setters) {
|
|||
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) {
|
||||
const map = {
|
||||
user: 'manuell',
|
||||
|
|
@ -217,6 +251,7 @@ export default function ExerciseProgressionPathBuilder({
|
|||
const [quickCreateDraft, setQuickCreateDraft] = useState(null)
|
||||
const [quickSaving, setQuickSaving] = useState(false)
|
||||
const [quickAiError, setQuickAiError] = useState('')
|
||||
const [activePlanningContextLines, setActivePlanningContextLines] = useState([])
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
|
@ -363,6 +398,18 @@ export default function ExerciseProgressionPathBuilder({
|
|||
await runGapFillAiSuggest(offer)
|
||||
}
|
||||
|
||||
const gapContextFallbackParams = {
|
||||
goalQuery,
|
||||
semanticBrief,
|
||||
graphId,
|
||||
pathSteps,
|
||||
editableMajorSteps,
|
||||
progressionRoadmap,
|
||||
startSituation,
|
||||
targetState,
|
||||
roadmapNotes,
|
||||
}
|
||||
|
||||
const runGapFillAiSuggest = async (offer) => {
|
||||
const title = (offer?.title_hint || '').trim()
|
||||
if (title.length < 3) {
|
||||
|
|
@ -391,17 +438,11 @@ export default function ExerciseProgressionPathBuilder({
|
|||
setQuickCreateDraft(null)
|
||||
setQuickSaving(true)
|
||||
setGeneratingOfferId(offer?.offer_id || null)
|
||||
const contextLines = gapOfferContextDisplayLines(offer, gapContextFallbackParams)
|
||||
setActivePlanningContextLines(contextLines)
|
||||
const planningContext = buildPathGapPlanningContextForAi({
|
||||
goalQuery,
|
||||
semanticBrief,
|
||||
offer,
|
||||
graphId,
|
||||
pathSteps,
|
||||
editableMajorSteps,
|
||||
progressionRoadmap,
|
||||
startSituation,
|
||||
targetState,
|
||||
roadmapNotes,
|
||||
...gapContextFallbackParams,
|
||||
})
|
||||
try {
|
||||
const aiRes = await api.suggestExerciseAi({
|
||||
|
|
@ -1193,6 +1234,9 @@ export default function ExerciseProgressionPathBuilder({
|
|||
{offer.replace_step_index != null ? ' (ersetzt themenfremden Schritt)' : ''}
|
||||
</p>
|
||||
) : null}
|
||||
<GapOfferContextPreview
|
||||
lines={gapOfferContextDisplayLines(offer, gapContextFallbackParams)}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -1384,8 +1428,10 @@ export default function ExerciseProgressionPathBuilder({
|
|||
onDraftChange={setQuickCreateDraft}
|
||||
onDiscard={() => {
|
||||
setQuickCreateDraft(null)
|
||||
setActivePlanningContextLines([])
|
||||
if (activeOffer) setQuickCreateOpen(true)
|
||||
}}
|
||||
planningContextLines={activePlanningContextLines}
|
||||
onApply={applyQuickCreateDraft}
|
||||
focusAreas={focusAreas}
|
||||
skillsCatalog={skillsCatalog}
|
||||
|
|
|
|||
|
|
@ -132,3 +132,31 @@ export function buildPathGapPlanningContextForAi({
|
|||
}
|
||||
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