Refactor Exercise Creation Components to Utilize Custom Hook for Quick Create Fields
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 40s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m20s

- Updated ExerciseAiQuickCreateOffer to set showSketchField to true by default and introduced sketchOptional prop for improved flexibility in exercise creation.
- Refactored ExercisePickerModal and ExercisesListPageRoot to leverage useExerciseAiQuickCreateFields hook, simplifying state management for quick create fields.
- Removed deprecated parsing logic and streamlined error handling for sketch input, enhancing user experience during exercise creation.
- Improved placeholder text and labels for clarity, ensuring better guidance for users when providing input for AI-generated exercises.
This commit is contained in:
Lars 2026-05-22 19:36:22 +02:00
parent 294740b780
commit c816e50c68
4 changed files with 108 additions and 59 deletions

View File

@ -16,14 +16,15 @@ export default function ExerciseAiQuickCreateOffer({
busy = false,
error = '',
onRunAi,
showSketchField = false,
showSketchField = true,
sketchOptional = true,
hint,
}) {
const canRun =
!busy &&
(title || '').trim().length >= 3 &&
(sketch || '').trim().length > 0 &&
focusAreaId
focusAreaId &&
(sketchOptional || (sketch || '').trim().length > 0)
return (
<div
@ -85,23 +86,27 @@ export default function ExerciseAiQuickCreateOffer({
{showSketchField ? (
<div>
<label className="form-label" htmlFor="ex-ai-quick-sketch">
Skizze / Idee *
Kurzbeschreibung / Idee{sketchOptional ? ' (optional)' : ' *'}
</label>
<textarea
id="ex-ai-quick-sketch"
className="form-input"
rows={3}
rows={4}
value={sketch}
onChange={(e) => onSketchChange(e.target.value)}
placeholder="Kurze Beschreibung für die KI …"
placeholder={
sketchOptional
? 'Leer lassen: KI schlägt Inhalt frei vor (Titel + Fokus). Oder kurz beschreiben, was die Übung tun soll …'
: 'Kurze Beschreibung für die KI …'
}
/>
{searchLabel && !(sketch || '').trim() ? (
<p style={{ margin: '6px 0 0', fontSize: '12px', color: 'var(--text3)' }}>
Suchbegriff: {searchLabel} wird als Titel übernommen.
</p>
) : null}
</div>
) : (
<p style={{ margin: 0, fontSize: '12px', color: 'var(--text3)' }}>
Ausgangstext: {(sketch || '').trim().slice(0, 160)}
{(sketch || '').trim().length > 160 ? '…' : ''}
</p>
)}
) : null}
{error ? (
<p style={{ margin: 0, fontSize: '13px', color: 'var(--danger)' }}>{error}</p>

View File

@ -19,11 +19,11 @@ import ExerciseFocusRulePicker from './ExerciseFocusRulePicker'
import CatalogRulePicker from './CatalogRulePicker'
import ExerciseAiSuggestPreviewModal from './ExerciseAiSuggestPreviewModal'
import ExerciseAiQuickCreateOffer from './ExerciseAiQuickCreateOffer'
import { useExerciseAiQuickCreateFields } from '../hooks/useExerciseAiQuickCreateFields'
import {
buildQuickCreateAiPreview,
buildQuickCreateExercisePayloadFromDraft,
aiPreviewToQuickCreateDraft,
parseSearchQueryForQuickCreate,
} from '../utils/exerciseAiQuickCreate'
const PAGE_SIZE = 100
@ -61,14 +61,21 @@ export default function ExercisePickerModal({
const [loadingMore, setLoadingMore] = useState(false)
const [hasMore, setHasMore] = useState(false)
const [multiPicked, setMultiPicked] = useState([])
const [quickTitle, setQuickTitle] = useState('')
const [quickSketch, setQuickSketch] = useState('')
const [quickFocusAreaId, setQuickFocusAreaId] = useState('')
const [quickSaving, setQuickSaving] = useState(false)
const [quickAiError, setQuickAiError] = useState('')
const [quickCreateDraft, setQuickCreateDraft] = useState(null)
const pickerScrollRef = useRef(null)
const {
title: quickTitle,
sketch: quickSketch,
focusAreaId: quickFocusAreaId,
setTitle: setQuickTitle,
setSketch: setQuickSketch,
setFocusAreaId: setQuickFocusAreaId,
resetQuickCreateFields,
} = useExerciseAiQuickCreateFields(debouncedSearch, { enabled: open && enableQuickCreateDraft })
const toggleMultiPick = (ex) => {
setMultiPicked((prev) =>
prev.some((p) => p.id === ex.id) ? prev.filter((p) => p.id !== ex.id) : [...prev, ex]
@ -85,18 +92,6 @@ export default function ExercisePickerModal({
return () => clearTimeout(t)
}, [aiSearchInput])
const parsedQuickCreate = useMemo(
() => parseSearchQueryForQuickCreate(debouncedSearch),
[debouncedSearch],
)
useEffect(() => {
if (!enableQuickCreateDraft || !debouncedSearch) return
setQuickTitle(parsedQuickCreate.title)
setQuickSketch(parsedQuickCreate.sketch || debouncedSearch)
setQuickAiError('')
}, [enableQuickCreateDraft, debouncedSearch, parsedQuickCreate.title, parsedQuickCreate.sketch])
const showQuickCreateOffer =
enableQuickCreateDraft &&
catalogsReady &&
@ -147,9 +142,7 @@ export default function ExercisePickerModal({
setList([])
setHasMore(false)
setMultiPicked([])
setQuickTitle('')
setQuickSketch('')
setQuickFocusAreaId('')
resetQuickCreateFields()
setQuickSaving(false)
setQuickAiError('')
setQuickCreateDraft(null)
@ -329,10 +322,7 @@ export default function ExercisePickerModal({
return
}
const sketch = (quickSketch || '').trim()
if (!sketch) {
alert('Bitte einen Suchbegriff oder eine Skizze eingeben.')
return
}
const focusId = parseInt(String(quickFocusAreaId).trim(), 10)
if (!Number.isFinite(focusId) || focusId < 1) {
alert('Bitte einen Fokusbereich wählen.')
@ -348,7 +338,7 @@ export default function ExercisePickerModal({
try {
const aiRes = await api.suggestExerciseAi({
title,
goal: sketch,
goal: sketch || undefined,
execution: '',
preparation: '',
trainer_notes: '',

View File

@ -18,8 +18,8 @@ import {
buildQuickCreateAiPreview,
buildQuickCreateExercisePayloadFromDraft,
aiPreviewToQuickCreateDraft,
parseSearchQueryForQuickCreate,
} from '../../utils/exerciseAiQuickCreate'
import { useExerciseAiQuickCreateFields } from '../../hooks/useExerciseAiQuickCreateFields'
import { buildExercisesListReturnContext } from '../../utils/navReturnContext'
import { buildExerciseListFilterChips } from '../../utils/exerciseListFilterChips'
import { skillCatalogPathLabel } from '../../utils/skillCatalogTree'
@ -89,13 +89,21 @@ function ExercisesListPageRoot() {
const [peekExercise, setPeekExercise] = useState(null)
const [saveModuleModalOpen, setSaveModuleModalOpen] = useState(false)
const [aiQuickCreateEnabled, setAiQuickCreateEnabled] = useState(false)
const [quickTitle, setQuickTitle] = useState('')
const [quickSketch, setQuickSketch] = useState('')
const [quickFocusAreaId, setQuickFocusAreaId] = useState('')
const [quickSaving, setQuickSaving] = useState(false)
const [quickAiError, setQuickAiError] = useState('')
const [quickCreateDraft, setQuickCreateDraft] = useState(null)
const {
title: quickTitle,
sketch: quickSketch,
focusAreaId: quickFocusAreaId,
setTitle: setQuickTitle,
setSketch: setQuickSketch,
setFocusAreaId: setQuickFocusAreaId,
} = useExerciseAiQuickCreateFields(debouncedSearch, {
enabled: pageTab === 'list' && (aiQuickCreateEnabled || debouncedSearch.length >= 3),
})
useEffect(() => {
if (!user?.id) return
if (prefsAppliedRef.current) return
@ -143,18 +151,6 @@ function ExercisesListPageRoot() {
return () => clearTimeout(t)
}, [aiSearchInput])
const parsedQuickCreate = useMemo(
() => parseSearchQueryForQuickCreate(debouncedSearch),
[debouncedSearch],
)
useEffect(() => {
if (!debouncedSearch) return
setQuickTitle(parsedQuickCreate.title)
setQuickSketch(parsedQuickCreate.sketch || debouncedSearch)
setQuickAiError('')
}, [debouncedSearch, parsedQuickCreate.title, parsedQuickCreate.sketch])
useEffect(() => {
if (!filterModalOpen) return
const onKey = (e) => {
@ -347,10 +343,7 @@ function ExercisesListPageRoot() {
return
}
const sketch = (quickSketch || '').trim()
if (!sketch) {
alert('Bitte Suchtext oder Skizze eingeben.')
return
}
const focusId = parseInt(String(quickFocusAreaId).trim(), 10)
if (!Number.isFinite(focusId) || focusId < 1) {
alert('Bitte einen Fokusbereich wählen.')
@ -366,7 +359,7 @@ function ExercisesListPageRoot() {
try {
const aiRes = await api.suggestExerciseAi({
title,
goal: sketch,
goal: sketch || undefined,
execution: '',
preparation: '',
trainer_notes: '',
@ -668,10 +661,9 @@ function ExercisesListPageRoot() {
busy={quickSaving}
error={quickAiError}
onRunAi={runQuickCreateAiSuggest}
showSketchField={aiQuickCreateEnabled}
hint={
aiQuickCreateEnabled
? 'KI-Anlage: Suchtext oder eigene Skizze als Ausgang — Fokusbereich wählen, dann KI-Vorschlag erzeugen und bearbeiten.'
? 'KI-Anlage: Titel aus Suche oder manuell; Kurzbeschreibung optional — leer für freien KI-Vorschlag, ausgefüllt als Ausgangsidee.'
: undefined
}
/>

View File

@ -0,0 +1,62 @@
import { useState, useEffect, useMemo, useRef, useCallback } from 'react'
import { parseSearchQueryForQuickCreate } from '../utils/exerciseAiQuickCreate'
/**
* Titel aus Suche vorbelegen; Kurzbeschreibung optional und manuell editierbar.
* Suchwechsel setzt touched zurück und befüllt neu solange der Nutzer nicht editiert hat.
*/
export function useExerciseAiQuickCreateFields(debouncedSearch, { enabled = true } = {}) {
const [title, setTitleState] = useState('')
const [sketch, setSketchState] = useState('')
const [focusAreaId, setFocusAreaId] = useState('')
const titleTouchedRef = useRef(false)
const sketchTouchedRef = useRef(false)
const lastSearchRef = useRef('')
const parsed = useMemo(() => parseSearchQueryForQuickCreate(debouncedSearch), [debouncedSearch])
useEffect(() => {
if (!enabled) return
if (debouncedSearch !== lastSearchRef.current) {
lastSearchRef.current = debouncedSearch
titleTouchedRef.current = false
sketchTouchedRef.current = false
}
if (!debouncedSearch) return
if (!titleTouchedRef.current) {
setTitleState(parsed.title)
}
if (!sketchTouchedRef.current) {
setSketchState('')
}
}, [enabled, debouncedSearch, parsed.title])
const setTitle = useCallback((v) => {
titleTouchedRef.current = true
setTitleState(v)
}, [])
const setSketch = useCallback((v) => {
sketchTouchedRef.current = true
setSketchState(v)
}, [])
const resetQuickCreateFields = useCallback(() => {
setTitleState('')
setSketchState('')
setFocusAreaId('')
titleTouchedRef.current = false
sketchTouchedRef.current = false
lastSearchRef.current = ''
}, [])
return {
title,
sketch,
focusAreaId,
setTitle,
setSketch,
setFocusAreaId,
resetQuickCreateFields,
}
}