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 effectivePickerQuery = useMemo(
() => [debouncedSearch, debouncedAi].filter(Boolean).join(' ').trim(),
[debouncedSearch, debouncedAi]
)
/** Gemeinsamer Suchtext — in Planung nur ein Feld; in Bibliothek beide Felder kombiniert. */
const effectivePickerQuery = useMemo(() => {
if (usePlanningSearch) {
return (debouncedSearch || debouncedAi).trim()
}
return [debouncedSearch, debouncedAi].filter(Boolean).join(' ').trim()
}, [usePlanningSearch, debouncedSearch, debouncedAi])
const {
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.exclude_without_focus) q.exclude_without_focus = true
if (filters.include_archived) q.include_archived = true
if (debouncedSearch) q.search = debouncedSearch
if (debouncedAi) q.ai_search = debouncedAi
if (!debouncedSearch && debouncedAi) q.search = debouncedAi
if (effectivePickerQuery) q.search = effectivePickerQuery
if (
Array.isArray(exerciseKindAny) &&
exerciseKindAny.length > 0
@ -259,7 +260,7 @@ export default function ExercisePickerModal({
q.exercise_kind_any = exerciseKindAny
}
return q
}, [filters, debouncedSearch, debouncedAi, exerciseKindAny])
}, [filters, effectivePickerQuery, exerciseKindAny])
const reload = useCallback(async () => {
if (!open || !catalogsReady) return
@ -345,8 +346,6 @@ export default function ExercisePickerModal({
usePlanningSearch,
planningContext,
effectivePickerQuery,
debouncedSearch,
debouncedAi,
exerciseKindAny,
])
@ -496,7 +495,13 @@ export default function ExercisePickerModal({
>
<div className="admin-modal-sheet__header">
<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>
<button type="button" className="btn btn-secondary admin-modal-sheet__close" onClick={onClose}>
Schließen
@ -575,51 +580,72 @@ export default function ExercisePickerModal({
) : null}
</div>
) : 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>
<label className="form-label">
{usePlanningSearch ? 'Planungs-Anfrage' : 'Volltextsuche'}
</label>
<input
type="search"
className="form-input"
placeholder={
usePlanningSearch
? 'z. B. Schlage mir die nächste Übung vor, Vertiefung, Reaktion mit Partner …'
: 'Stichwort, Titelfragment…'
}
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}
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 ? (
{usePlanningSearch ? (
<div>
<label className="form-label">Planungs-Anfrage (KI)</label>
<input
type="search"
className="form-input"
placeholder="z. B. Schlage mir die nächste Übung vor, baut auf dem Plan auf und trainiert Schnellkraft …"
value={searchInput || aiSearchInput}
onChange={(e) => {
const v = e.target.value
setSearchInput(v)
setAiSearchInput(v)
}}
autoComplete="off"
/>
<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>
) : 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' }}>
<button type="button" className="btn btn-secondary" onClick={() => setFilterOpen(!filterOpen)}>
{filterOpen ? 'Filter ausblenden' : 'Erweiterte Filter'}

View File

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