Refactor ExercisePickerModal for Enhanced Search Functionality
All checks were successful
Deploy Development / deploy (push) Successful in 41s
Test Suite / pytest-backend (push) Successful in 40s
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 1m16s

- Updated `effectivePickerQuery` logic to improve search handling based on planning context, allowing for a single input field in planning mode.
- Simplified query construction by utilizing `effectivePickerQuery` throughout the component, enhancing clarity and user experience.
- Adjusted UI elements and labels to better reflect the context of the search, providing clearer guidance for users.
- Modified `TrainingUnitEditPage` to ensure proper unit ID resolution, improving integration with the exercise picker.
This commit is contained in:
Lars 2026-05-22 22:24:49 +02:00
parent 905bce198f
commit d019c20338
2 changed files with 84 additions and 56 deletions

View File

@ -75,10 +75,13 @@ export default function ExercisePickerModal({
const usePlanningSearch = Boolean(planningContext?.unitId && Number(planningContext.unitId) > 0) const usePlanningSearch = Boolean(planningContext?.unitId && Number(planningContext.unitId) > 0)
const effectivePickerQuery = useMemo( /** Gemeinsamer Suchtext — in Planung nur ein Feld; in Bibliothek beide Felder kombiniert. */
() => [debouncedSearch, debouncedAi].filter(Boolean).join(' ').trim(), const effectivePickerQuery = useMemo(() => {
[debouncedSearch, debouncedAi] if (usePlanningSearch) {
) return (debouncedSearch || debouncedAi).trim()
}
return [debouncedSearch, debouncedAi].filter(Boolean).join(' ').trim()
}, [usePlanningSearch, debouncedSearch, debouncedAi])
const { const {
title: quickTitle, title: quickTitle,
@ -249,9 +252,7 @@ export default function ExercisePickerModal({
if (filters.skill_max_level) q.skill_max_level = n(filters.skill_max_level) if (filters.skill_max_level) q.skill_max_level = n(filters.skill_max_level)
if (filters.exclude_without_focus) q.exclude_without_focus = true if (filters.exclude_without_focus) q.exclude_without_focus = true
if (filters.include_archived) q.include_archived = true if (filters.include_archived) q.include_archived = true
if (debouncedSearch) q.search = debouncedSearch if (effectivePickerQuery) q.search = effectivePickerQuery
if (debouncedAi) q.ai_search = debouncedAi
if (!debouncedSearch && debouncedAi) q.search = debouncedAi
if ( if (
Array.isArray(exerciseKindAny) && Array.isArray(exerciseKindAny) &&
exerciseKindAny.length > 0 exerciseKindAny.length > 0
@ -259,7 +260,7 @@ export default function ExercisePickerModal({
q.exercise_kind_any = exerciseKindAny q.exercise_kind_any = exerciseKindAny
} }
return q return q
}, [filters, debouncedSearch, debouncedAi, exerciseKindAny]) }, [filters, effectivePickerQuery, exerciseKindAny])
const reload = useCallback(async () => { const reload = useCallback(async () => {
if (!open || !catalogsReady) return if (!open || !catalogsReady) return
@ -345,8 +346,6 @@ export default function ExercisePickerModal({
usePlanningSearch, usePlanningSearch,
planningContext, planningContext,
effectivePickerQuery, effectivePickerQuery,
debouncedSearch,
debouncedAi,
exerciseKindAny, exerciseKindAny,
]) ])
@ -496,7 +495,13 @@ export default function ExercisePickerModal({
> >
<div className="admin-modal-sheet__header"> <div className="admin-modal-sheet__header">
<h3 className="admin-modal-sheet__title"> <h3 className="admin-modal-sheet__title">
{multiSelect ? 'Übungen auswählen' : 'Übung auswählen'} {usePlanningSearch
? multiSelect
? 'Planungs-KI: Übungen vorschlagen'
: 'Planungs-KI: Übung vorschlagen'
: multiSelect
? 'Übungen auswählen'
: 'Übung auswählen'}
</h3> </h3>
<button type="button" className="btn btn-secondary admin-modal-sheet__close" onClick={onClose}> <button type="button" className="btn btn-secondary admin-modal-sheet__close" onClick={onClose}>
Schließen Schließen
@ -575,51 +580,72 @@ export default function ExercisePickerModal({
) : null} ) : null}
</div> </div>
) : null} ) : null}
{!usePlanningSearch ? (
<p
style={{
margin: '0 0 10px',
padding: '8px 10px',
borderRadius: '8px',
background: 'var(--surface2)',
border: '1px solid var(--border)',
fontSize: '12px',
color: 'var(--text2)',
}}
>
<strong style={{ color: 'var(--text1)' }}>Bibliothekssuche (Volltext)</strong> Planungs-KI mit
Kontext (Einheit, Plan, Anker) gibt es in der{' '}
<strong>Trainingseinheit bearbeiten</strong>, nach dem Speichern der Einheit.
</p>
) : null}
<div style={{ display: 'grid', gap: '0.65rem' }}> <div style={{ display: 'grid', gap: '0.65rem' }}>
<div> {usePlanningSearch ? (
<label className="form-label"> <div>
{usePlanningSearch ? 'Planungs-Anfrage' : 'Volltextsuche'} <label className="form-label">Planungs-Anfrage (KI)</label>
</label> <input
<input type="search"
type="search" className="form-input"
className="form-input" placeholder="z. B. Schlage mir die nächste Übung vor, baut auf dem Plan auf und trainiert Schnellkraft …"
placeholder={ value={searchInput || aiSearchInput}
usePlanningSearch onChange={(e) => {
? 'z. B. Schlage mir die nächste Übung vor, Vertiefung, Reaktion mit Partner …' const v = e.target.value
: 'Stichwort, Titelfragment…' setSearchInput(v)
} setAiSearchInput(v)
value={searchInput} }}
onChange={(e) => setSearchInput(e.target.value)} autoComplete="off"
autoComplete="off" />
/>
</div>
<div>
<label className="form-label">
{usePlanningSearch ? 'Planungs-Anfrage (Zusatz, optional)' : 'Semantisch / '}
{!usePlanningSearch ? (
<span title="aktuell gleiche Datenbanksuche wie Volltext; später KI-Verfeinerung möglich">
KI-Feld
</span>
) : null}
</label>
<input
type="search"
className="form-input"
placeholder={
usePlanningSearch
? 'Alternative Formulierung — wird mit oben kombiniert'
: 'zweites Suchkonzept oder Umschreibung…'
}
value={aiSearchInput}
onChange={(e) => setAiSearchInput(e.target.value)}
autoComplete="off"
/>
{usePlanningSearch ? (
<p style={{ margin: '4px 0 0', fontSize: '11px', color: 'var(--text3)' }}> <p style={{ margin: '4px 0 0', fontSize: '11px', color: 'var(--text3)' }}>
Beide Felder bilden eine gemeinsame Planungs-Anfrage. Leer lassen = nächste Übung aus Planungskontext. Mit Text = KI-Intent + Profil + Ranking.
</p> </p>
) : null} </div>
</div> ) : (
<>
<div>
<label className="form-label">Volltextsuche</label>
<input
type="search"
className="form-input"
placeholder="Stichwort, Titelfragment…"
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}
autoComplete="off"
/>
</div>
<div>
<label className="form-label">
Ergänzung /{' '}
<span title="Wird mit Volltextsuche kombiniert">zweites Suchfeld</span>
</label>
<input
type="search"
className="form-input"
placeholder="zweites Suchkonzept oder Umschreibung…"
value={aiSearchInput}
onChange={(e) => setAiSearchInput(e.target.value)}
autoComplete="off"
/>
</div>
</>
)}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem', alignItems: 'center' }}> <div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem', alignItems: 'center' }}>
<button type="button" className="btn btn-secondary" onClick={() => setFilterOpen(!filterOpen)}> <button type="button" className="btn btn-secondary" onClick={() => setFilterOpen(!filterOpen)}>
{filterOpen ? 'Filter ausblenden' : 'Erweiterte Filter'} {filterOpen ? 'Filter ausblenden' : 'Erweiterte Filter'}

View File

@ -124,7 +124,9 @@ export default function TrainingUnitEditPage() {
const [saveModuleOpen, setSaveModuleOpen] = useState(false) const [saveModuleOpen, setSaveModuleOpen] = useState(false)
const exercisePickerPlanningContext = useMemo(() => { const exercisePickerPlanningContext = useMemo(() => {
if (!editingUnit?.id) return null const resolvedUnitId =
editingUnit?.id ?? (Number.isFinite(unitId) && unitId > 0 ? unitId : null)
if (!resolvedUnitId) return null
const target = exercisePickerTarget const target = exercisePickerTarget
const secs = formData.sections || [] const secs = formData.sections || []
const sIdx = target?.sIdx ?? 0 const sIdx = target?.sIdx ?? 0
@ -164,13 +166,13 @@ export default function TrainingUnitEditPage() {
} }
} }
return { return {
unitId: Number(editingUnit.id), unitId: Number(resolvedUnitId),
sectionOrderIndex: sIdx, sectionOrderIndex: sIdx,
anchorExerciseId: Number.isFinite(anchorExerciseId) && anchorExerciseId > 0 ? anchorExerciseId : null, anchorExerciseId: Number.isFinite(anchorExerciseId) && anchorExerciseId > 0 ? anchorExerciseId : null,
progressionGraphId: null, progressionGraphId: null,
plannedExerciseIds, plannedExerciseIds,
} }
}, [editingUnit?.id, exercisePickerTarget, formData.sections]) }, [editingUnit?.id, unitId, exercisePickerTarget, formData.sections])
const goBack = useCallback(() => { const goBack = useCallback(() => {
goNavReturn(navigate, location, { goNavReturn(navigate, location, {