Progression optimiert Phase A #55
|
|
@ -168,6 +168,8 @@ def build_progression_path_gap_planning_context(
|
||||||
resolved_structured: Optional[Mapping[str, Any]] = None,
|
resolved_structured: Optional[Mapping[str, Any]] = None,
|
||||||
stage_spec: Optional[Mapping[str, Any]] = None,
|
stage_spec: Optional[Mapping[str, Any]] = None,
|
||||||
semantic_brief: Optional[Mapping[str, Any]] = None,
|
semantic_brief: Optional[Mapping[str, Any]] = None,
|
||||||
|
stage_learning_goal_override: Optional[str] = None,
|
||||||
|
gap_trainer_supplements: Optional[str] = None,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Kontext für KI-Neuanlage aus Progressionsgraph-Pfad-Lücke."""
|
"""Kontext für KI-Neuanlage aus Progressionsgraph-Pfad-Lücke."""
|
||||||
offer = offer or {}
|
offer = offer or {}
|
||||||
|
|
@ -205,6 +207,11 @@ def build_progression_path_gap_planning_context(
|
||||||
semantic_brief=semantic_brief,
|
semantic_brief=semantic_brief,
|
||||||
)
|
)
|
||||||
ctx.update(snap)
|
ctx.update(snap)
|
||||||
|
if stage_learning_goal_override and stage_learning_goal_override.strip():
|
||||||
|
ctx["stage_learning_goal"] = _trim_str(stage_learning_goal_override, limit=1200)
|
||||||
|
ctx["roadmap_learning_goal"] = ctx["stage_learning_goal"]
|
||||||
|
if gap_trainer_supplements and gap_trainer_supplements.strip():
|
||||||
|
ctx["gap_trainer_supplements"] = _trim_str(gap_trainer_supplements, limit=2000)
|
||||||
return sanitize_planning_context_for_ai(ctx)
|
return sanitize_planning_context_for_ai(ctx)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -78,3 +78,14 @@ def test_gap_planning_context_carries_snapshot_fields():
|
||||||
)
|
)
|
||||||
assert ctx["start_situation"] == "Start"
|
assert ctx["start_situation"] == "Start"
|
||||||
assert ctx["stage_learning_goal"] == "Stufenziel"
|
assert ctx["stage_learning_goal"] == "Stufenziel"
|
||||||
|
|
||||||
|
|
||||||
|
def test_gap_planning_context_trainer_supplements_and_stage_override():
|
||||||
|
ctx = build_progression_path_gap_planning_context(
|
||||||
|
goal_query="Kumite",
|
||||||
|
stage_spec={"learning_goal": "Original"},
|
||||||
|
stage_learning_goal_override="Angepasstes Stufenziel",
|
||||||
|
gap_trainer_supplements="Nur Partnerübung, Kindergruppe",
|
||||||
|
)
|
||||||
|
assert ctx["stage_learning_goal"] == "Angepasstes Stufenziel"
|
||||||
|
assert ctx["gap_trainer_supplements"] == "Nur Partnerübung, Kindergruppe"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Shinkan Jinkendo Version Information
|
# Shinkan Jinkendo Version Information
|
||||||
|
|
||||||
APP_VERSION = "0.8.213"
|
APP_VERSION = "0.8.214"
|
||||||
BUILD_DATE = "2026-06-07"
|
BUILD_DATE = "2026-06-07"
|
||||||
DB_SCHEMA_VERSION = "20260607087"
|
DB_SCHEMA_VERSION = "20260607087"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
import api from '../utils/api'
|
import api from '../utils/api'
|
||||||
import ExerciseAiQuickCreateModal from './exercises/ExerciseAiQuickCreateModal'
|
import ExerciseAiQuickCreateModal from './exercises/ExerciseAiQuickCreateModal'
|
||||||
|
import ExerciseGapFillPrepModal from './exercises/ExerciseGapFillPrepModal'
|
||||||
import ExerciseAiSuggestPreviewModal from './ExerciseAiSuggestPreviewModal'
|
import ExerciseAiSuggestPreviewModal from './ExerciseAiSuggestPreviewModal'
|
||||||
import {
|
import {
|
||||||
aiPreviewToQuickCreateDraft,
|
aiPreviewToQuickCreateDraft,
|
||||||
|
|
@ -13,6 +14,7 @@ import {
|
||||||
import {
|
import {
|
||||||
buildPathGapPlanningContextForAi,
|
buildPathGapPlanningContextForAi,
|
||||||
gapOfferContextDisplayLines,
|
gapOfferContextDisplayLines,
|
||||||
|
initialStageLearningGoalFromOffer,
|
||||||
} from '../utils/planningContextForExerciseAi'
|
} from '../utils/planningContextForExerciseAi'
|
||||||
|
|
||||||
function applyResolvedStructuredFromRoadmap(progressionRoadmap, setters) {
|
function applyResolvedStructuredFromRoadmap(progressionRoadmap, setters) {
|
||||||
|
|
@ -252,6 +254,12 @@ export default function ExerciseProgressionPathBuilder({
|
||||||
const [quickSaving, setQuickSaving] = useState(false)
|
const [quickSaving, setQuickSaving] = useState(false)
|
||||||
const [quickAiError, setQuickAiError] = useState('')
|
const [quickAiError, setQuickAiError] = useState('')
|
||||||
const [activePlanningContextLines, setActivePlanningContextLines] = useState([])
|
const [activePlanningContextLines, setActivePlanningContextLines] = useState([])
|
||||||
|
const [gapPrepOpen, setGapPrepOpen] = useState(false)
|
||||||
|
const [gapPrepTitle, setGapPrepTitle] = useState('')
|
||||||
|
const [gapPrepStageGoal, setGapPrepStageGoal] = useState('')
|
||||||
|
const [gapPrepSupplements, setGapPrepSupplements] = useState('')
|
||||||
|
const [gapPrepFocusAreaId, setGapPrepFocusAreaId] = useState('')
|
||||||
|
const [gapPrepError, setGapPrepError] = useState('')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
|
|
@ -391,13 +399,6 @@ export default function ExerciseProgressionPathBuilder({
|
||||||
setQuickAiError('')
|
setQuickAiError('')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleGapFillClick = async (offer) => {
|
|
||||||
if (!confirmPathExpansionIfNeeded(offer, pathSteps.length, maxSteps, setMaxSteps)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await runGapFillAiSuggest(offer)
|
|
||||||
}
|
|
||||||
|
|
||||||
const gapContextFallbackParams = {
|
const gapContextFallbackParams = {
|
||||||
goalQuery,
|
goalQuery,
|
||||||
semanticBrief,
|
semanticBrief,
|
||||||
|
|
@ -410,21 +411,70 @@ export default function ExerciseProgressionPathBuilder({
|
||||||
roadmapNotes,
|
roadmapNotes,
|
||||||
}
|
}
|
||||||
|
|
||||||
const runGapFillAiSuggest = async (offer) => {
|
const closeGapFillPrep = () => {
|
||||||
const title = (offer?.title_hint || '').trim()
|
if (quickSaving) return
|
||||||
if (title.length < 3) {
|
setGapPrepOpen(false)
|
||||||
alert('Titel-Hinweis fehlt — bitte Pfad erneut vorschlagen.')
|
setGapPrepError('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const openGapFillPrep = (offer) => {
|
||||||
|
const defaultFocus = resolveDefaultFocusAreaId(targetSummary, focusAreas)
|
||||||
|
setActiveOffer(offer)
|
||||||
|
setGapPrepTitle((offer?.title_hint || '').trim())
|
||||||
|
setGapPrepStageGoal(initialStageLearningGoalFromOffer(offer, gapContextFallbackParams))
|
||||||
|
setGapPrepSupplements('')
|
||||||
|
setGapPrepFocusAreaId(defaultFocus ? String(defaultFocus) : '')
|
||||||
|
setActivePlanningContextLines(gapOfferContextDisplayLines(offer, gapContextFallbackParams))
|
||||||
|
setGapPrepError('')
|
||||||
|
setGapPrepOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleGapFillClick = (offer) => {
|
||||||
|
if (!confirmPathExpansionIfNeeded(offer, pathSteps.length, maxSteps, setMaxSteps)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const goalText = (offer?.goal_for_ai || offer?.sketch || '').trim()
|
openGapFillPrep(offer)
|
||||||
const focusId = resolveDefaultFocusAreaId(targetSummary, focusAreas)
|
}
|
||||||
|
|
||||||
|
const submitGapFillPrep = async () => {
|
||||||
|
const title = (gapPrepTitle || '').trim()
|
||||||
|
if (title.length < 3) {
|
||||||
|
alert('Titel: mindestens 3 Zeichen.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const focusId = parseInt(String(gapPrepFocusAreaId).trim(), 10)
|
||||||
|
if (!Number.isFinite(focusId) || focusId < 1) {
|
||||||
|
alert('Bitte einen Fokusbereich wählen.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!activeOffer) return
|
||||||
|
setGapPrepError('')
|
||||||
|
await runGapFillAiSuggest(activeOffer, {
|
||||||
|
title,
|
||||||
|
stageLearningGoal: (gapPrepStageGoal || '').trim(),
|
||||||
|
supplements: (gapPrepSupplements || '').trim(),
|
||||||
|
focusAreaId: focusId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const runGapFillAiSuggest = async (offer, prep = null) => {
|
||||||
|
const title = (prep?.title || offer?.title_hint || '').trim()
|
||||||
|
if (title.length < 3) {
|
||||||
|
alert('Titel: mindestens 3 Zeichen.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const supplements = (prep?.supplements || '').trim()
|
||||||
|
const stageGoal = (prep?.stageLearningGoal || '').trim()
|
||||||
|
let goalText = (offer?.goal_for_ai || offer?.sketch || '').trim()
|
||||||
|
if (supplements) {
|
||||||
|
goalText = `${goalText}\n\nTrainer-Ergänzungen für diese Übung:\n${supplements}`.trim()
|
||||||
|
}
|
||||||
|
const focusId =
|
||||||
|
prep?.focusAreaId != null && Number.isFinite(Number(prep.focusAreaId))
|
||||||
|
? Number(prep.focusAreaId)
|
||||||
|
: resolveDefaultFocusAreaId(targetSummary, focusAreas)
|
||||||
if (!focusId) {
|
if (!focusId) {
|
||||||
alert('Kein Fokusbereich verfügbar — bitte Kataloge laden oder manuell wählen.')
|
alert('Kein Fokusbereich verfügbar — bitte im Vorbereitungsdialog wählen.')
|
||||||
setQuickTitle(title)
|
|
||||||
setQuickSketch(goalText)
|
|
||||||
setQuickFocusAreaId('')
|
|
||||||
setActiveOffer(offer)
|
|
||||||
setQuickCreateOpen(true)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const focusRow = (focusAreas || []).find((x) => Number(x.id) === focusId)
|
const focusRow = (focusAreas || []).find((x) => Number(x.id) === focusId)
|
||||||
|
|
@ -438,11 +488,16 @@ 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)
|
const contextParams = {
|
||||||
|
...gapContextFallbackParams,
|
||||||
|
stageLearningGoalOverride: stageGoal,
|
||||||
|
gapTrainerSupplements: supplements,
|
||||||
|
}
|
||||||
|
const contextLines = gapOfferContextDisplayLines(offer, contextParams)
|
||||||
setActivePlanningContextLines(contextLines)
|
setActivePlanningContextLines(contextLines)
|
||||||
const planningContext = buildPathGapPlanningContextForAi({
|
const planningContext = buildPathGapPlanningContextForAi({
|
||||||
offer,
|
offer,
|
||||||
...gapContextFallbackParams,
|
...contextParams,
|
||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
const aiRes = await api.suggestExerciseAi({
|
const aiRes = await api.suggestExerciseAi({
|
||||||
|
|
@ -450,7 +505,7 @@ export default function ExerciseProgressionPathBuilder({
|
||||||
goal: goalText || undefined,
|
goal: goalText || undefined,
|
||||||
execution: '',
|
execution: '',
|
||||||
preparation: '',
|
preparation: '',
|
||||||
trainer_notes: '',
|
trainer_notes: supplements || '',
|
||||||
focus_area_hint: focusHint || undefined,
|
focus_area_hint: focusHint || undefined,
|
||||||
focus_areas_context: [{ focus_area_id: focusId, is_primary: true }],
|
focus_areas_context: [{ focus_area_id: focusId, is_primary: true }],
|
||||||
planning_context: planningContext || undefined,
|
planning_context: planningContext || undefined,
|
||||||
|
|
@ -469,12 +524,13 @@ export default function ExerciseProgressionPathBuilder({
|
||||||
sketchPlain: goalText,
|
sketchPlain: goalText,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
setGapPrepOpen(false)
|
||||||
setQuickCreateOpen(false)
|
setQuickCreateOpen(false)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
const msg = e?.message || String(e)
|
const msg = e?.message || String(e)
|
||||||
|
setGapPrepError(msg)
|
||||||
setQuickAiError(msg)
|
setQuickAiError(msg)
|
||||||
setQuickCreateOpen(true)
|
|
||||||
} finally {
|
} finally {
|
||||||
setQuickSaving(false)
|
setQuickSaving(false)
|
||||||
setGeneratingOfferId(null)
|
setGeneratingOfferId(null)
|
||||||
|
|
@ -1205,7 +1261,7 @@ export default function ExerciseProgressionPathBuilder({
|
||||||
Fehlende oder zu ersetzende Schritte ({pathSteps.length}/{maxSteps} im Pfad).
|
Fehlende oder zu ersetzende Schritte ({pathSteps.length}/{maxSteps} im Pfad).
|
||||||
{pathSteps.length >= maxSteps
|
{pathSteps.length >= maxSteps
|
||||||
? ' Der Pfad ist voll — beim Einfügen können Sie die Pfadlänge dynamisch vergrößern (ohne neuen Vorschlag); Ersatz-Angebote ersetzen einen Schritt.'
|
? ' Der Pfad ist voll — beim Einfügen können Sie die Pfadlänge dynamisch vergrößern (ohne neuen Vorschlag); Ersatz-Angebote ersetzen einen Schritt.'
|
||||||
: ' „Mit KI anlegen“ erzeugt einen vollständigen Entwurf und fügt die Übung ein.'}
|
: ' Zuerst Kontext prüfen und ergänzen, dann KI-Entwurf erstellen und einfügen.'}
|
||||||
</p>
|
</p>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
||||||
{gapFillOffers.map((offer) => (
|
{gapFillOffers.map((offer) => (
|
||||||
|
|
@ -1255,12 +1311,12 @@ export default function ExerciseProgressionPathBuilder({
|
||||||
: 'Pfad voll — Klick fragt, ob die Pfadlänge vergrößert werden soll'
|
: 'Pfad voll — Klick fragt, ob die Pfadlänge vergrößert werden soll'
|
||||||
: offer.replace_step_index != null
|
: offer.replace_step_index != null
|
||||||
? 'Ersetzt den themenfremden Schritt im Pfad'
|
? 'Ersetzt den themenfremden Schritt im Pfad'
|
||||||
: 'KI-Entwurf mit Pfad-Kontext generieren'
|
: 'Kontext prüfen, Ergänzungen mitgeben, dann KI-Entwurf'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{generatingOfferId === offer.offer_id
|
{generatingOfferId === offer.offer_id
|
||||||
? 'KI erstellt Entwurf …'
|
? 'KI erstellt Entwurf …'
|
||||||
: 'Mit KI anlegen'}
|
: 'Vorbereiten & KI anlegen'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1406,6 +1462,25 @@ export default function ExerciseProgressionPathBuilder({
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
<ExerciseGapFillPrepModal
|
||||||
|
open={gapPrepOpen}
|
||||||
|
onClose={closeGapFillPrep}
|
||||||
|
offer={activeOffer}
|
||||||
|
contextLines={activePlanningContextLines}
|
||||||
|
title={gapPrepTitle}
|
||||||
|
onTitleChange={setGapPrepTitle}
|
||||||
|
stageLearningGoal={gapPrepStageGoal}
|
||||||
|
onStageLearningGoalChange={setGapPrepStageGoal}
|
||||||
|
supplements={gapPrepSupplements}
|
||||||
|
onSupplementsChange={setGapPrepSupplements}
|
||||||
|
focusAreaId={gapPrepFocusAreaId}
|
||||||
|
onFocusAreaChange={setGapPrepFocusAreaId}
|
||||||
|
focusAreas={focusAreas}
|
||||||
|
busy={quickSaving}
|
||||||
|
error={gapPrepError}
|
||||||
|
onSubmit={submitGapFillPrep}
|
||||||
|
/>
|
||||||
|
|
||||||
<ExerciseAiQuickCreateModal
|
<ExerciseAiQuickCreateModal
|
||||||
open={quickCreateOpen}
|
open={quickCreateOpen}
|
||||||
onClose={closeQuickCreate}
|
onClose={closeQuickCreate}
|
||||||
|
|
@ -1428,8 +1503,11 @@ export default function ExerciseProgressionPathBuilder({
|
||||||
onDraftChange={setQuickCreateDraft}
|
onDraftChange={setQuickCreateDraft}
|
||||||
onDiscard={() => {
|
onDiscard={() => {
|
||||||
setQuickCreateDraft(null)
|
setQuickCreateDraft(null)
|
||||||
setActivePlanningContextLines([])
|
if (activeOffer) {
|
||||||
if (activeOffer) setQuickCreateOpen(true)
|
setGapPrepOpen(true)
|
||||||
|
} else {
|
||||||
|
setActivePlanningContextLines([])
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
planningContextLines={activePlanningContextLines}
|
planningContextLines={activePlanningContextLines}
|
||||||
onApply={applyQuickCreateDraft}
|
onApply={applyQuickCreateDraft}
|
||||||
|
|
|
||||||
186
frontend/src/components/exercises/ExerciseGapFillPrepModal.jsx
Normal file
186
frontend/src/components/exercises/ExerciseGapFillPrepModal.jsx
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
import React, { useEffect } from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vorbereitung vor KI-Übungsanlage aus Pfad-Lücke: Kontext prüfen, Ergänzungen mitgeben.
|
||||||
|
*/
|
||||||
|
export default function ExerciseGapFillPrepModal({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
offer = null,
|
||||||
|
contextLines = [],
|
||||||
|
title = '',
|
||||||
|
onTitleChange,
|
||||||
|
stageLearningGoal = '',
|
||||||
|
onStageLearningGoalChange,
|
||||||
|
supplements = '',
|
||||||
|
onSupplementsChange,
|
||||||
|
focusAreaId = '',
|
||||||
|
onFocusAreaChange,
|
||||||
|
focusAreas = [],
|
||||||
|
busy = false,
|
||||||
|
error = '',
|
||||||
|
onSubmit,
|
||||||
|
}) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return undefined
|
||||||
|
const onKey = (e) => {
|
||||||
|
if (e.key === 'Escape' && !busy) {
|
||||||
|
e.preventDefault()
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener('keydown', onKey)
|
||||||
|
return () => window.removeEventListener('keydown', onKey)
|
||||||
|
}, [open, busy, onClose])
|
||||||
|
|
||||||
|
if (!open || !offer) return null
|
||||||
|
|
||||||
|
const readOnlyLines = (contextLines || []).filter(
|
||||||
|
(line) => line.label !== 'Stufen-Lernziel',
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="admin-modal-backdrop"
|
||||||
|
role="presentation"
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget && !busy) onClose()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="admin-modal-sheet"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="gap-fill-prep-title"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
style={{ maxWidth: '680px' }}
|
||||||
|
>
|
||||||
|
<div className="admin-modal-sheet__header">
|
||||||
|
<h3 id="gap-fill-prep-title" className="admin-modal-sheet__title">
|
||||||
|
Übung mit KI vorbereiten
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary admin-modal-sheet__close"
|
||||||
|
disabled={busy}
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
Schließen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="admin-modal-sheet__body">
|
||||||
|
<p className="muted" style={{ marginTop: 0, marginBottom: '12px', lineHeight: 1.45 }}>
|
||||||
|
Prüfen und anpassen, was an die KI geht. Ergänzungen fließen in Ziel, Trainerhinweise und
|
||||||
|
Planungskontext ein — erst danach wird der Entwurf erzeugt.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{readOnlyLines.length > 0 ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginBottom: '14px',
|
||||||
|
padding: '10px 12px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
background: 'var(--surface2)',
|
||||||
|
fontSize: '12px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<strong style={{ display: 'block', marginBottom: '6px' }}>Pfad-Kontext (aus Roadmap)</strong>
|
||||||
|
<dl style={{ margin: 0, display: 'grid', gap: '6px' }}>
|
||||||
|
{readOnlyLines.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' }}>
|
||||||
|
<div>
|
||||||
|
<label className="form-label" htmlFor="gap-prep-title">
|
||||||
|
Titel-Vorschlag *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="gap-prep-title"
|
||||||
|
className="form-input"
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => onTitleChange(e.target.value)}
|
||||||
|
disabled={busy}
|
||||||
|
maxLength={280}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label" htmlFor="gap-prep-stage-goal">
|
||||||
|
Stufen-Lernziel (anpassbar)
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="gap-prep-stage-goal"
|
||||||
|
className="form-input"
|
||||||
|
rows={2}
|
||||||
|
value={stageLearningGoal}
|
||||||
|
onChange={(e) => onStageLearningGoalChange(e.target.value)}
|
||||||
|
disabled={busy}
|
||||||
|
placeholder="Was soll diese Übung in der Roadmap-Stufe leisten?"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label" htmlFor="gap-prep-supplements">
|
||||||
|
Ergänzungen / Anpassungen für die KI
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="gap-prep-supplements"
|
||||||
|
className="form-input"
|
||||||
|
rows={3}
|
||||||
|
value={supplements}
|
||||||
|
onChange={(e) => onSupplementsChange(e.target.value)}
|
||||||
|
disabled={busy}
|
||||||
|
placeholder="z. B. nur Partnerübung, 10–12 Jahre, Fokus auf Reaktion unter Druck, keine Sprünge …"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label" htmlFor="gap-prep-focus">
|
||||||
|
Fokusbereich *
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="gap-prep-focus"
|
||||||
|
className="form-input"
|
||||||
|
value={focusAreaId}
|
||||||
|
onChange={(e) => onFocusAreaChange(e.target.value)}
|
||||||
|
disabled={busy}
|
||||||
|
>
|
||||||
|
<option value="">Bitte wählen …</option>
|
||||||
|
{(focusAreas || []).map((fa) => (
|
||||||
|
<option key={fa.id} value={fa.id}>
|
||||||
|
{fa.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{offer.from_title && offer.to_title ? (
|
||||||
|
<p style={{ fontSize: '11px', color: 'var(--text3)', margin: '12px 0 0' }}>
|
||||||
|
Einordnung: zwischen „{offer.from_title}“ und „{offer.to_title}“
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{error ? (
|
||||||
|
<p className="form-error" style={{ marginTop: '12px' }}>
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className="admin-modal-sheet__footer" style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
||||||
|
<button type="button" className="btn btn-secondary" disabled={busy} onClick={onClose}>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
<button type="button" className="btn btn-primary" disabled={busy} onClick={onSubmit}>
|
||||||
|
{busy ? 'KI erstellt Entwurf …' : 'KI-Entwurf erstellen'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -53,6 +53,8 @@ export function buildPathGapPlanningContextForAi({
|
||||||
startSituation = '',
|
startSituation = '',
|
||||||
targetState = '',
|
targetState = '',
|
||||||
roadmapNotes = '',
|
roadmapNotes = '',
|
||||||
|
stageLearningGoalOverride = '',
|
||||||
|
gapTrainerSupplements = '',
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const afterIdx = Number(offer?.insert_after_index)
|
const afterIdx = Number(offer?.insert_after_index)
|
||||||
const stepA = Number.isFinite(afterIdx) && afterIdx >= 0 ? pathSteps[afterIdx] : null
|
const stepA = Number.isFinite(afterIdx) && afterIdx >= 0 ? pathSteps[afterIdx] : null
|
||||||
|
|
@ -105,7 +107,9 @@ export function buildPathGapPlanningContextForAi({
|
||||||
start_situation: start,
|
start_situation: start,
|
||||||
target_state: target,
|
target_state: target,
|
||||||
roadmap_notes: notes,
|
roadmap_notes: notes,
|
||||||
stage_learning_goal: stageSpec?.learning_goal || null,
|
stage_learning_goal:
|
||||||
|
(stageLearningGoalOverride || '').trim() || stageSpec?.learning_goal || null,
|
||||||
|
gap_trainer_supplements: (gapTrainerSupplements || '').trim() || null,
|
||||||
stage_phase: stageSpec?.phase || majorStep?.phase || null,
|
stage_phase: stageSpec?.phase || majorStep?.phase || null,
|
||||||
stage_exercise_type: stageSpec?.exercise_type || null,
|
stage_exercise_type: stageSpec?.exercise_type || null,
|
||||||
stage_load_profile: Array.isArray(stageSpec?.load_profile)
|
stage_load_profile: Array.isArray(stageSpec?.load_profile)
|
||||||
|
|
@ -158,5 +162,12 @@ export function gapOfferContextDisplayLines(offer, fallbackParams = null) {
|
||||||
if (Array.isArray(raw.skill_hints) && raw.skill_hints.length) {
|
if (Array.isArray(raw.skill_hints) && raw.skill_hints.length) {
|
||||||
push('Fähigkeiten-Fokus', raw.skill_hints.slice(0, 3).join(' · '))
|
push('Fähigkeiten-Fokus', raw.skill_hints.slice(0, 3).join(' · '))
|
||||||
}
|
}
|
||||||
|
push('Trainer-Ergänzungen', raw.gap_trainer_supplements)
|
||||||
return lines
|
return lines
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function initialStageLearningGoalFromOffer(offer, fallbackParams = null) {
|
||||||
|
const lines = gapOfferContextDisplayLines(offer, fallbackParams)
|
||||||
|
const hit = lines.find((l) => l.label === 'Stufen-Lernziel')
|
||||||
|
return hit?.value || (offer?.title_hint || '').trim()
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user