diff --git a/frontend/src/components/ExerciseAiQuickCreateOffer.jsx b/frontend/src/components/ExerciseAiQuickCreateOffer.jsx index ed03333..206d6b4 100644 --- a/frontend/src/components/ExerciseAiQuickCreateOffer.jsx +++ b/frontend/src/components/ExerciseAiQuickCreateOffer.jsx @@ -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 (
- Ausgangstext: {(sketch || '').trim().slice(0, 160)} - {(sketch || '').trim().length > 160 ? '…' : ''} -
- )} + ) : null} {error ? ({error}
diff --git a/frontend/src/components/ExercisePickerModal.jsx b/frontend/src/components/ExercisePickerModal.jsx index a678e42..5cf3859 100644 --- a/frontend/src/components/ExercisePickerModal.jsx +++ b/frontend/src/components/ExercisePickerModal.jsx @@ -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: '', diff --git a/frontend/src/components/exercises/ExercisesListPageRoot.jsx b/frontend/src/components/exercises/ExercisesListPageRoot.jsx index 18200bd..3b4cf3b 100644 --- a/frontend/src/components/exercises/ExercisesListPageRoot.jsx +++ b/frontend/src/components/exercises/ExercisesListPageRoot.jsx @@ -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 } /> diff --git a/frontend/src/hooks/useExerciseAiQuickCreateFields.js b/frontend/src/hooks/useExerciseAiQuickCreateFields.js new file mode 100644 index 0000000..be7ecb0 --- /dev/null +++ b/frontend/src/hooks/useExerciseAiQuickCreateFields.js @@ -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, + } +}