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,
|
busy = false,
|
||||||
error = '',
|
error = '',
|
||||||
onRunAi,
|
onRunAi,
|
||||||
showSketchField = false,
|
showSketchField = true,
|
||||||
|
sketchOptional = true,
|
||||||
hint,
|
hint,
|
||||||
}) {
|
}) {
|
||||||
const canRun =
|
const canRun =
|
||||||
!busy &&
|
!busy &&
|
||||||
(title || '').trim().length >= 3 &&
|
(title || '').trim().length >= 3 &&
|
||||||
(sketch || '').trim().length > 0 &&
|
focusAreaId &&
|
||||||
focusAreaId
|
(sketchOptional || (sketch || '').trim().length > 0)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -85,23 +86,27 @@ export default function ExerciseAiQuickCreateOffer({
|
||||||
{showSketchField ? (
|
{showSketchField ? (
|
||||||
<div>
|
<div>
|
||||||
<label className="form-label" htmlFor="ex-ai-quick-sketch">
|
<label className="form-label" htmlFor="ex-ai-quick-sketch">
|
||||||
Skizze / Idee *
|
Kurzbeschreibung / Idee{sketchOptional ? ' (optional)' : ' *'}
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
id="ex-ai-quick-sketch"
|
id="ex-ai-quick-sketch"
|
||||||
className="form-input"
|
className="form-input"
|
||||||
rows={3}
|
rows={4}
|
||||||
value={sketch}
|
value={sketch}
|
||||||
onChange={(e) => onSketchChange(e.target.value)}
|
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>
|
</div>
|
||||||
) : (
|
) : null}
|
||||||
<p style={{ margin: 0, fontSize: '12px', color: 'var(--text3)' }}>
|
|
||||||
Ausgangstext: {(sketch || '').trim().slice(0, 160)}
|
|
||||||
{(sketch || '').trim().length > 160 ? '…' : ''}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{error ? (
|
{error ? (
|
||||||
<p style={{ margin: 0, fontSize: '13px', color: 'var(--danger)' }}>{error}</p>
|
<p style={{ margin: 0, fontSize: '13px', color: 'var(--danger)' }}>{error}</p>
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,11 @@ import ExerciseFocusRulePicker from './ExerciseFocusRulePicker'
|
||||||
import CatalogRulePicker from './CatalogRulePicker'
|
import CatalogRulePicker from './CatalogRulePicker'
|
||||||
import ExerciseAiSuggestPreviewModal from './ExerciseAiSuggestPreviewModal'
|
import ExerciseAiSuggestPreviewModal from './ExerciseAiSuggestPreviewModal'
|
||||||
import ExerciseAiQuickCreateOffer from './ExerciseAiQuickCreateOffer'
|
import ExerciseAiQuickCreateOffer from './ExerciseAiQuickCreateOffer'
|
||||||
|
import { useExerciseAiQuickCreateFields } from '../hooks/useExerciseAiQuickCreateFields'
|
||||||
import {
|
import {
|
||||||
buildQuickCreateAiPreview,
|
buildQuickCreateAiPreview,
|
||||||
buildQuickCreateExercisePayloadFromDraft,
|
buildQuickCreateExercisePayloadFromDraft,
|
||||||
aiPreviewToQuickCreateDraft,
|
aiPreviewToQuickCreateDraft,
|
||||||
parseSearchQueryForQuickCreate,
|
|
||||||
} from '../utils/exerciseAiQuickCreate'
|
} from '../utils/exerciseAiQuickCreate'
|
||||||
|
|
||||||
const PAGE_SIZE = 100
|
const PAGE_SIZE = 100
|
||||||
|
|
@ -61,14 +61,21 @@ export default function ExercisePickerModal({
|
||||||
const [loadingMore, setLoadingMore] = useState(false)
|
const [loadingMore, setLoadingMore] = useState(false)
|
||||||
const [hasMore, setHasMore] = useState(false)
|
const [hasMore, setHasMore] = useState(false)
|
||||||
const [multiPicked, setMultiPicked] = useState([])
|
const [multiPicked, setMultiPicked] = useState([])
|
||||||
const [quickTitle, setQuickTitle] = useState('')
|
|
||||||
const [quickSketch, setQuickSketch] = useState('')
|
|
||||||
const [quickFocusAreaId, setQuickFocusAreaId] = useState('')
|
|
||||||
const [quickSaving, setQuickSaving] = useState(false)
|
const [quickSaving, setQuickSaving] = useState(false)
|
||||||
const [quickAiError, setQuickAiError] = useState('')
|
const [quickAiError, setQuickAiError] = useState('')
|
||||||
const [quickCreateDraft, setQuickCreateDraft] = useState(null)
|
const [quickCreateDraft, setQuickCreateDraft] = useState(null)
|
||||||
const pickerScrollRef = useRef(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) => {
|
const toggleMultiPick = (ex) => {
|
||||||
setMultiPicked((prev) =>
|
setMultiPicked((prev) =>
|
||||||
prev.some((p) => p.id === ex.id) ? prev.filter((p) => p.id !== ex.id) : [...prev, ex]
|
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)
|
return () => clearTimeout(t)
|
||||||
}, [aiSearchInput])
|
}, [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 =
|
const showQuickCreateOffer =
|
||||||
enableQuickCreateDraft &&
|
enableQuickCreateDraft &&
|
||||||
catalogsReady &&
|
catalogsReady &&
|
||||||
|
|
@ -147,9 +142,7 @@ export default function ExercisePickerModal({
|
||||||
setList([])
|
setList([])
|
||||||
setHasMore(false)
|
setHasMore(false)
|
||||||
setMultiPicked([])
|
setMultiPicked([])
|
||||||
setQuickTitle('')
|
resetQuickCreateFields()
|
||||||
setQuickSketch('')
|
|
||||||
setQuickFocusAreaId('')
|
|
||||||
setQuickSaving(false)
|
setQuickSaving(false)
|
||||||
setQuickAiError('')
|
setQuickAiError('')
|
||||||
setQuickCreateDraft(null)
|
setQuickCreateDraft(null)
|
||||||
|
|
@ -329,10 +322,7 @@ export default function ExercisePickerModal({
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const sketch = (quickSketch || '').trim()
|
const sketch = (quickSketch || '').trim()
|
||||||
if (!sketch) {
|
|
||||||
alert('Bitte einen Suchbegriff oder eine Skizze eingeben.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const focusId = parseInt(String(quickFocusAreaId).trim(), 10)
|
const focusId = parseInt(String(quickFocusAreaId).trim(), 10)
|
||||||
if (!Number.isFinite(focusId) || focusId < 1) {
|
if (!Number.isFinite(focusId) || focusId < 1) {
|
||||||
alert('Bitte einen Fokusbereich wählen.')
|
alert('Bitte einen Fokusbereich wählen.')
|
||||||
|
|
@ -348,7 +338,7 @@ export default function ExercisePickerModal({
|
||||||
try {
|
try {
|
||||||
const aiRes = await api.suggestExerciseAi({
|
const aiRes = await api.suggestExerciseAi({
|
||||||
title,
|
title,
|
||||||
goal: sketch,
|
goal: sketch || undefined,
|
||||||
execution: '',
|
execution: '',
|
||||||
preparation: '',
|
preparation: '',
|
||||||
trainer_notes: '',
|
trainer_notes: '',
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@ import {
|
||||||
buildQuickCreateAiPreview,
|
buildQuickCreateAiPreview,
|
||||||
buildQuickCreateExercisePayloadFromDraft,
|
buildQuickCreateExercisePayloadFromDraft,
|
||||||
aiPreviewToQuickCreateDraft,
|
aiPreviewToQuickCreateDraft,
|
||||||
parseSearchQueryForQuickCreate,
|
|
||||||
} from '../../utils/exerciseAiQuickCreate'
|
} from '../../utils/exerciseAiQuickCreate'
|
||||||
|
import { useExerciseAiQuickCreateFields } from '../../hooks/useExerciseAiQuickCreateFields'
|
||||||
import { buildExercisesListReturnContext } from '../../utils/navReturnContext'
|
import { buildExercisesListReturnContext } from '../../utils/navReturnContext'
|
||||||
import { buildExerciseListFilterChips } from '../../utils/exerciseListFilterChips'
|
import { buildExerciseListFilterChips } from '../../utils/exerciseListFilterChips'
|
||||||
import { skillCatalogPathLabel } from '../../utils/skillCatalogTree'
|
import { skillCatalogPathLabel } from '../../utils/skillCatalogTree'
|
||||||
|
|
@ -89,13 +89,21 @@ function ExercisesListPageRoot() {
|
||||||
const [peekExercise, setPeekExercise] = useState(null)
|
const [peekExercise, setPeekExercise] = useState(null)
|
||||||
const [saveModuleModalOpen, setSaveModuleModalOpen] = useState(false)
|
const [saveModuleModalOpen, setSaveModuleModalOpen] = useState(false)
|
||||||
const [aiQuickCreateEnabled, setAiQuickCreateEnabled] = 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 [quickSaving, setQuickSaving] = useState(false)
|
||||||
const [quickAiError, setQuickAiError] = useState('')
|
const [quickAiError, setQuickAiError] = useState('')
|
||||||
const [quickCreateDraft, setQuickCreateDraft] = useState(null)
|
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(() => {
|
useEffect(() => {
|
||||||
if (!user?.id) return
|
if (!user?.id) return
|
||||||
if (prefsAppliedRef.current) return
|
if (prefsAppliedRef.current) return
|
||||||
|
|
@ -143,18 +151,6 @@ function ExercisesListPageRoot() {
|
||||||
return () => clearTimeout(t)
|
return () => clearTimeout(t)
|
||||||
}, [aiSearchInput])
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (!filterModalOpen) return
|
if (!filterModalOpen) return
|
||||||
const onKey = (e) => {
|
const onKey = (e) => {
|
||||||
|
|
@ -347,10 +343,7 @@ function ExercisesListPageRoot() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const sketch = (quickSketch || '').trim()
|
const sketch = (quickSketch || '').trim()
|
||||||
if (!sketch) {
|
|
||||||
alert('Bitte Suchtext oder Skizze eingeben.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const focusId = parseInt(String(quickFocusAreaId).trim(), 10)
|
const focusId = parseInt(String(quickFocusAreaId).trim(), 10)
|
||||||
if (!Number.isFinite(focusId) || focusId < 1) {
|
if (!Number.isFinite(focusId) || focusId < 1) {
|
||||||
alert('Bitte einen Fokusbereich wählen.')
|
alert('Bitte einen Fokusbereich wählen.')
|
||||||
|
|
@ -366,7 +359,7 @@ function ExercisesListPageRoot() {
|
||||||
try {
|
try {
|
||||||
const aiRes = await api.suggestExerciseAi({
|
const aiRes = await api.suggestExerciseAi({
|
||||||
title,
|
title,
|
||||||
goal: sketch,
|
goal: sketch || undefined,
|
||||||
execution: '',
|
execution: '',
|
||||||
preparation: '',
|
preparation: '',
|
||||||
trainer_notes: '',
|
trainer_notes: '',
|
||||||
|
|
@ -668,10 +661,9 @@ function ExercisesListPageRoot() {
|
||||||
busy={quickSaving}
|
busy={quickSaving}
|
||||||
error={quickAiError}
|
error={quickAiError}
|
||||||
onRunAi={runQuickCreateAiSuggest}
|
onRunAi={runQuickCreateAiSuggest}
|
||||||
showSketchField={aiQuickCreateEnabled}
|
|
||||||
hint={
|
hint={
|
||||||
aiQuickCreateEnabled
|
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
|
: 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