shinkan-jinkendo/frontend/src/components/CombinationMethodProfileEditor.jsx
Lars 12fd3926b2
All checks were successful
Deploy Development / deploy (push) Successful in 38s
Test Suite / pytest-backend (push) Successful in 35s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 11s
Test Suite / playwright-tests (push) Successful in 1m4s
feat(combination-exercises): enhance method profile integration and update specifications
- Updated app version to 0.8.102, reflecting recent enhancements in combination exercises.
- Introduced structured method profiles for combination exercises, allowing for detailed planning and coaching support.
- Enhanced frontend components to display method profiles in the Exercise and Combination Coach views.
- Updated documentation to include new specifications and implementation details for method archetypes and profiles.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 07:16:58 +02:00

176 lines
6.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useMemo, useState } from 'react'
import { archetypeCoachHint, combinationArchetypeLabel } from '../constants/combinationArchetypes'
import {
METHOD_PROFILE_GUI_FIELDS,
parseProfileJson,
setFullProfileRawJson,
updateProfileGuided,
} from '../utils/combinationMethodProfileUi'
function clampInt(n, min, max) {
if (!Number.isFinite(n)) return null
let x = n
if (typeof min === 'number' && x < min) x = min
if (typeof max === 'number' && x > max) x = max
return Math.round(x)
}
/**
* Kombination: geführtes Ablaufprofil + optionales Roh-JSON.
*/
export default function CombinationMethodProfileEditor({
methodArchetype,
methodProfileJson,
onChangeMethodProfileJson,
}) {
const arch = typeof methodArchetype === 'string' ? methodArchetype.trim() : ''
const fieldsGui = METHOD_PROFILE_GUI_FIELDS[arch]
const fields = Array.isArray(fieldsGui) ? fieldsGui : null
const parseState = useMemo(() => parseProfileJson(methodProfileJson || '{}'), [methodProfileJson])
const [rawOpenError, setRawOpenError] = useState(null)
const [rawDraft, setRawDraft] = useState(null)
const profileObj = parseState.ok ? parseState.obj : {}
const applyGuided = (key, value, kind) => {
if (kind === 'bool') {
const res = updateProfileGuided(arch, methodProfileJson || '{}', key, value, 'bool')
if (!res.ok) return
onChangeMethodProfileJson(res.json)
return
}
if (value === '' || value === undefined || value === null) {
const res = updateProfileGuided(arch, methodProfileJson || '{}', key, '', 'int')
if (!res.ok) return
onChangeMethodProfileJson(res.json)
return
}
const num = typeof value === 'number' ? value : parseInt(String(value), 10)
if (!Number.isFinite(num)) return
const def = METHOD_PROFILE_GUI_FIELDS[arch]?.find((f) => f.key === key && f.kind === 'int')
const c = clampInt(num, def?.min, def?.max)
if (c == null) return
const res = updateProfileGuided(arch, methodProfileJson || '{}', key, c, 'int')
if (!res.ok) return
onChangeMethodProfileJson(res.json)
}
const archeLabel = arch ? combinationArchetypeLabel(arch) : null
const openAdvanced = () => {
setRawOpenError(null)
const p = parseProfileJson(methodProfileJson || '{}')
setRawDraft(p.ok ? JSON.stringify(p.obj, null, 2) : String(methodProfileJson || ''))
}
return (
<div style={{ marginTop: '10px' }}>
{arch ? (
<p style={{ fontSize: '12px', color: 'var(--text2)', lineHeight: 1.48, margin: '0 0 12px' }}>
<strong style={{ color: 'var(--text1)' }}>
Coach &amp; Planung:{' '}
{archeLabel && archeLabel !== arch ? `${archeLabel} · ` : ''}
</strong>
{archetypeCoachHint(arch)}
</p>
) : (
<p style={{ fontSize: '12px', color: 'var(--text3)', margin: '0 0 12px' }}>
Wähle einen Archetyp, um das Ablaufprofil strukturiert zu erfassen oder nur das JSON weiter unten.
</p>
)}
{!parseState.ok ? (
<p style={{ color: 'var(--danger)', fontSize: '13px', marginBottom: '10px' }}>{parseState.error}</p>
) : null}
{fields && fields.length > 0 ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px', marginBottom: '12px' }}>
{fields.map((def) => {
if (def.kind === 'bool') {
const ck = !!profileObj[def.key]
return (
<label
key={def.key}
style={{ display: 'flex', gap: '8px', alignItems: 'center', cursor: 'pointer', fontSize: '0.9rem' }}
>
<input type="checkbox" checked={ck} onChange={(e) => applyGuided(def.key, e.target.checked, 'bool')} />
<span>{def.label}</span>
</label>
)
}
const v = profileObj[def.key]
const str =
v === undefined || v === null ? '' : typeof v === 'number' && Number.isFinite(v) ? String(v) : String(v)
return (
<div className="form-row" key={def.key}>
<label className="form-label" style={{ fontSize: '12px' }}>
{def.label}
</label>
<input
type="number"
className="form-input"
min={def.min}
max={def.max}
value={str}
placeholder="optional"
onChange={(e) => {
const t = e.target.value
if (t.trim() === '') applyGuided(def.key, '', 'int')
else applyGuided(def.key, parseInt(t, 10), 'int')
}}
/>
</div>
)
})}
</div>
) : arch && fields && fields.length === 0 ? (
<p style={{ fontSize: '12px', color: 'var(--text3)', margin: '0 0 10px' }}>
Für diesen Archetyp gibt es keine vorgegebenen Profilfelder nutze die Freitexte der Kombination oder RohJSON bei Bedarf.
</p>
) : null}
<details
style={{
marginTop: '4px',
borderRadius: '8px',
border: '1px solid var(--border)',
padding: '8px 10px',
background: 'var(--surface)',
}}
onToggle={(ev) => {
if (ev.target.open) openAdvanced()
}}
>
<summary style={{ cursor: 'pointer', fontSize: '0.82rem', fontWeight: 600 }}>Erweitert: JSON direkt bearbeiten</summary>
<p style={{ fontSize: '11px', color: 'var(--text3)', margin: '8px 0 6px', lineHeight: 1.4 }}>
Zusätzliche Schlüssel (Piloten). Geführte Felder können dieselben Schlüssel beim nächsten Speichern überlagern.
</p>
<textarea
className="form-input"
rows={8}
style={{ fontFamily: 'Consolas,monospace', fontSize: '12px' }}
value={rawDraft != null ? rawDraft : methodProfileJson || '{}'}
onChange={(e) => {
setRawDraft(e.target.value)
setRawOpenError(null)
}}
spellCheck={false}
onBlur={() => {
const src = rawDraft != null ? rawDraft : methodProfileJson
const res = setFullProfileRawJson(src || '{}')
if (!res.ok) {
setRawOpenError(res.error)
return
}
setRawOpenError(null)
setRawDraft(null)
onChangeMethodProfileJson(res.json)
}}
/>
{rawOpenError ? <p style={{ color: 'var(--danger)', fontSize: '12px', marginTop: '6px' }}>{rawOpenError}</p> : null}
</details>
</div>
)
}