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
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:
parent
294740b780
commit
c816e50c68
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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: '',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
62
frontend/src/hooks/useExerciseAiQuickCreateFields.js
Normal file
62
frontend/src/hooks/useExerciseAiQuickCreateFields.js
Normal 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,
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user