KI Implementierung (MVP) auf Übungen #46
|
|
@ -2735,258 +2735,6 @@ function ExerciseFormPageRoot() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{aiSuggestionPreview &&
|
|
||||||
(() => {
|
|
||||||
const p = aiSuggestionPreview
|
|
||||||
const summaryBoxSx = {
|
|
||||||
padding: '10px 12px',
|
|
||||||
borderRadius: '8px',
|
|
||||||
border: '1px solid var(--border)',
|
|
||||||
background: 'var(--surface2)',
|
|
||||||
fontSize: '13px',
|
|
||||||
lineHeight: 1.45,
|
|
||||||
whiteSpace: 'pre-wrap',
|
|
||||||
wordBreak: 'break-word',
|
|
||||||
minHeight: '72px',
|
|
||||||
}
|
|
||||||
const canApplySomething =
|
|
||||||
(p.applySummary && p.summaryAfterHtml) || p.skillChoices.some((c) => c.include)
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
aria-label="KI-Vorschlag prüfen"
|
|
||||||
style={{
|
|
||||||
position: 'fixed',
|
|
||||||
inset: 0,
|
|
||||||
background: 'rgba(0,0,0,0.5)',
|
|
||||||
zIndex: 1001,
|
|
||||||
overflow: 'auto',
|
|
||||||
padding: '16px',
|
|
||||||
}}
|
|
||||||
onClick={() => discardExerciseAiSuggestionPreview()}
|
|
||||||
onKeyDown={(e) => e.key === 'Escape' && discardExerciseAiSuggestionPreview()}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="card"
|
|
||||||
style={{
|
|
||||||
maxWidth: 760,
|
|
||||||
margin: '3vh auto',
|
|
||||||
maxHeight: '92vh',
|
|
||||||
overflow: 'auto',
|
|
||||||
position: 'relative',
|
|
||||||
}}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<h3 style={{ marginTop: 0, fontSize: '1.1rem', marginBottom: '6px' }}>KI-Vorschlag übernehmen</h3>
|
|
||||||
<p style={{ fontSize: '13px', color: 'var(--text3)', marginTop: 0, marginBottom: '16px' }}>
|
|
||||||
Vergleichen und nur die gewünschten Teile übernehmen. Es werden keine Daten automatisch gespeichert.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{p.hasSummaryProposal ? (
|
|
||||||
<section style={{ marginBottom: '20px' }} aria-labelledby="ai-preview-summary-heading">
|
|
||||||
<div
|
|
||||||
id="ai-preview-summary-heading"
|
|
||||||
style={{ fontWeight: 600, fontSize: '0.95rem', marginBottom: '10px' }}
|
|
||||||
>
|
|
||||||
Kurzfassung
|
|
||||||
</div>
|
|
||||||
<label
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '8px',
|
|
||||||
marginBottom: '10px',
|
|
||||||
cursor: 'pointer',
|
|
||||||
fontSize: '14px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={p.applySummary}
|
|
||||||
onChange={(e) =>
|
|
||||||
setAiSuggestionPreview((prev) =>
|
|
||||||
prev ? { ...prev, applySummary: e.target.checked } : prev,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
Kurzfassung durch Vorschlag ersetzen (bestehende Kurzbeschreibung wird überschrieben)
|
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'grid',
|
|
||||||
gridTemplateColumns: 'minmax(0,1fr) minmax(0,1fr)',
|
|
||||||
gap: '12px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<div style={{ fontSize: '12px', color: 'var(--text3)', marginBottom: '4px' }}>
|
|
||||||
Aktuell (ohne Formatierung)
|
|
||||||
</div>
|
|
||||||
<div style={summaryBoxSx}>{p.summaryBeforePlain || '(leer)'}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div style={{ fontSize: '12px', color: 'var(--text3)', marginBottom: '4px' }}>KI-Vorschlag</div>
|
|
||||||
<div style={{ ...summaryBoxSx, borderColor: 'var(--accent-dark, rgba(29,158,117,0.45))' }}>
|
|
||||||
{p.summaryAfterPlain || '(leer)'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{p.skillsRequested ? (
|
|
||||||
<section aria-labelledby="ai-preview-skills-heading">
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
gap: '8px',
|
|
||||||
marginBottom: '10px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div id="ai-preview-skills-heading" style={{ fontWeight: 600, fontSize: '0.95rem' }}>
|
|
||||||
Fähigkeiten ({p.skillChoices.length}
|
|
||||||
{p.skillChoices.length === 1 ? ' Vorschlag' : ' Vorschläge'})
|
|
||||||
</div>
|
|
||||||
{p.skillChoices.length > 0 ? (
|
|
||||||
<div style={{ display: 'flex', gap: '6px', flexWrap: 'wrap' }}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-secondary"
|
|
||||||
style={{ fontSize: '11px', padding: '4px 8px' }}
|
|
||||||
onClick={() =>
|
|
||||||
setAiSuggestionPreview((prev) =>
|
|
||||||
prev
|
|
||||||
? {
|
|
||||||
...prev,
|
|
||||||
skillChoices: prev.skillChoices.map((x) => ({ ...x, include: true })),
|
|
||||||
}
|
|
||||||
: prev,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Alle auswählen
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-secondary"
|
|
||||||
style={{ fontSize: '11px', padding: '4px 8px' }}
|
|
||||||
onClick={() =>
|
|
||||||
setAiSuggestionPreview((prev) =>
|
|
||||||
prev
|
|
||||||
? {
|
|
||||||
...prev,
|
|
||||||
skillChoices: prev.skillChoices.map((x) => ({ ...x, include: false })),
|
|
||||||
}
|
|
||||||
: prev,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Alle abwählen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
{p.skillChoices.length === 0 ? (
|
|
||||||
<p style={{ fontSize: '13px', color: 'var(--text3)', margin: 0 }}>
|
|
||||||
Keine passenden Fähigkeiten — der Katalog-Vorschlag war leer oder enthielt nur ungültige IDs.
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
<ul style={{ listStyle: 'none', padding: 0, margin: 0 }}>
|
|
||||||
{p.skillChoices.map((c) => (
|
|
||||||
<li
|
|
||||||
key={c.key}
|
|
||||||
style={{
|
|
||||||
border: '1px solid var(--border)',
|
|
||||||
borderRadius: '8px',
|
|
||||||
padding: '10px 12px',
|
|
||||||
marginBottom: '10px',
|
|
||||||
background: 'var(--surface)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
gap: '10px',
|
|
||||||
alignItems: 'flex-start',
|
|
||||||
cursor: 'pointer',
|
|
||||||
margin: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={c.include}
|
|
||||||
onChange={() =>
|
|
||||||
setAiSuggestionPreview((prev) =>
|
|
||||||
prev
|
|
||||||
? {
|
|
||||||
...prev,
|
|
||||||
skillChoices: prev.skillChoices.map((x) =>
|
|
||||||
x.skill_id === c.skill_id ? { ...x, include: !x.include } : x,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
: prev,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
style={{ marginTop: '4px' }}
|
|
||||||
/>
|
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
|
||||||
<div style={{ fontWeight: 600, fontSize: '13px', marginBottom: '6px' }}>
|
|
||||||
{c.kind === 'add' ? 'Neu hinzufügen' : 'Bestehende Zeile aktualisieren'}
|
|
||||||
</div>
|
|
||||||
{c.kind === 'update' && c.before ? (
|
|
||||||
<div style={{ fontSize: '12px', lineHeight: 1.5 }}>
|
|
||||||
<div style={{ color: 'var(--text3)', marginBottom: '2px' }}>Bisher</div>
|
|
||||||
<div style={{ marginBottom: '8px' }}>
|
|
||||||
{describeExerciseSkillRowForPreview(c.before, skillsCatalog)}
|
|
||||||
</div>
|
|
||||||
<div style={{ color: 'var(--text3)', marginBottom: '2px' }}>Nach KI-Vorschlag</div>
|
|
||||||
<div>{describeExerciseSkillRowForPreview(c.after, skillsCatalog)}</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div style={{ fontSize: '13px', lineHeight: 1.5 }}>
|
|
||||||
{describeExerciseSkillRowForPreview(c.after, skillsCatalog)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
marginTop: '20px',
|
|
||||||
paddingTop: '14px',
|
|
||||||
borderTop: '1px solid var(--border)',
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'flex-end',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
gap: '10px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<button type="button" className="btn btn-secondary" onClick={discardExerciseAiSuggestionPreview}>
|
|
||||||
Abbrechen
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-primary"
|
|
||||||
disabled={!canApplySomething}
|
|
||||||
onClick={() => applyExerciseAiSuggestionPreview()}
|
|
||||||
>
|
|
||||||
Ausgewähltes übernehmen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})()}
|
|
||||||
{mediaPreview && (
|
{mediaPreview && (
|
||||||
<MediaPreviewModal
|
<MediaPreviewModal
|
||||||
title={(mediaPreview.title || '').trim() || mediaPreview.original_filename || `Medium #${mediaPreview.id}`}
|
title={(mediaPreview.title || '').trim() || mediaPreview.original_filename || `Medium #${mediaPreview.id}`}
|
||||||
|
|
@ -3017,6 +2765,259 @@ function ExerciseFormPageRoot() {
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{aiSuggestionPreview &&
|
||||||
|
(() => {
|
||||||
|
const p = aiSuggestionPreview
|
||||||
|
const summaryBoxSx = {
|
||||||
|
padding: '10px 12px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
background: 'var(--surface2)',
|
||||||
|
fontSize: '13px',
|
||||||
|
lineHeight: 1.45,
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
minHeight: '72px',
|
||||||
|
}
|
||||||
|
const canApplySomething =
|
||||||
|
(p.applySummary && p.summaryAfterHtml) || p.skillChoices.some((c) => c.include)
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label="KI-Vorschlag prüfen"
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
inset: 0,
|
||||||
|
background: 'rgba(0,0,0,0.5)',
|
||||||
|
zIndex: 1001,
|
||||||
|
overflow: 'auto',
|
||||||
|
padding: '16px',
|
||||||
|
}}
|
||||||
|
onClick={() => discardExerciseAiSuggestionPreview()}
|
||||||
|
onKeyDown={(e) => e.key === 'Escape' && discardExerciseAiSuggestionPreview()}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="card"
|
||||||
|
style={{
|
||||||
|
maxWidth: 760,
|
||||||
|
margin: '3vh auto',
|
||||||
|
maxHeight: '92vh',
|
||||||
|
overflow: 'auto',
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<h3 style={{ marginTop: 0, fontSize: '1.1rem', marginBottom: '6px' }}>KI-Vorschlag übernehmen</h3>
|
||||||
|
<p style={{ fontSize: '13px', color: 'var(--text3)', marginTop: 0, marginBottom: '16px' }}>
|
||||||
|
Vergleichen und nur die gewünschten Teile übernehmen. Es werden keine Daten automatisch gespeichert.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{p.hasSummaryProposal ? (
|
||||||
|
<section style={{ marginBottom: '20px' }} aria-labelledby="ai-preview-summary-heading">
|
||||||
|
<div
|
||||||
|
id="ai-preview-summary-heading"
|
||||||
|
style={{ fontWeight: 600, fontSize: '0.95rem', marginBottom: '10px' }}
|
||||||
|
>
|
||||||
|
Kurzfassung
|
||||||
|
</div>
|
||||||
|
<label
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '8px',
|
||||||
|
marginBottom: '10px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '14px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={p.applySummary}
|
||||||
|
onChange={(e) =>
|
||||||
|
setAiSuggestionPreview((prev) =>
|
||||||
|
prev ? { ...prev, applySummary: e.target.checked } : prev,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
Kurzfassung durch Vorschlag ersetzen (bestehende Kurzbeschreibung wird überschrieben)
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'minmax(0,1fr) minmax(0,1fr)',
|
||||||
|
gap: '12px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: '12px', color: 'var(--text3)', marginBottom: '4px' }}>
|
||||||
|
Aktuell (ohne Formatierung)
|
||||||
|
</div>
|
||||||
|
<div style={summaryBoxSx}>{p.summaryBeforePlain || '(leer)'}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: '12px', color: 'var(--text3)', marginBottom: '4px' }}>KI-Vorschlag</div>
|
||||||
|
<div style={{ ...summaryBoxSx, borderColor: 'var(--accent-dark, rgba(29,158,117,0.45))' }}>
|
||||||
|
{p.summaryAfterPlain || '(leer)'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{p.skillsRequested ? (
|
||||||
|
<section aria-labelledby="ai-preview-skills-heading">
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
gap: '8px',
|
||||||
|
marginBottom: '10px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div id="ai-preview-skills-heading" style={{ fontWeight: 600, fontSize: '0.95rem' }}>
|
||||||
|
Fähigkeiten ({p.skillChoices.length}
|
||||||
|
{p.skillChoices.length === 1 ? ' Vorschlag' : ' Vorschläge'})
|
||||||
|
</div>
|
||||||
|
{p.skillChoices.length > 0 ? (
|
||||||
|
<div style={{ display: 'flex', gap: '6px', flexWrap: 'wrap' }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary"
|
||||||
|
style={{ fontSize: '11px', padding: '4px 8px' }}
|
||||||
|
onClick={() =>
|
||||||
|
setAiSuggestionPreview((prev) =>
|
||||||
|
prev
|
||||||
|
? {
|
||||||
|
...prev,
|
||||||
|
skillChoices: prev.skillChoices.map((x) => ({ ...x, include: true })),
|
||||||
|
}
|
||||||
|
: prev,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Alle auswählen
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary"
|
||||||
|
style={{ fontSize: '11px', padding: '4px 8px' }}
|
||||||
|
onClick={() =>
|
||||||
|
setAiSuggestionPreview((prev) =>
|
||||||
|
prev
|
||||||
|
? {
|
||||||
|
...prev,
|
||||||
|
skillChoices: prev.skillChoices.map((x) => ({ ...x, include: false })),
|
||||||
|
}
|
||||||
|
: prev,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Alle abwählen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
{p.skillChoices.length === 0 ? (
|
||||||
|
<p style={{ fontSize: '13px', color: 'var(--text3)', margin: 0 }}>
|
||||||
|
Keine passenden Fähigkeiten — der Katalog-Vorschlag war leer oder enthielt nur ungültige IDs.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<ul style={{ listStyle: 'none', padding: 0, margin: 0 }}>
|
||||||
|
{p.skillChoices.map((c) => (
|
||||||
|
<li
|
||||||
|
key={c.key}
|
||||||
|
style={{
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '10px 12px',
|
||||||
|
marginBottom: '10px',
|
||||||
|
background: 'var(--surface)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: '10px',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
cursor: 'pointer',
|
||||||
|
margin: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={c.include}
|
||||||
|
onChange={() =>
|
||||||
|
setAiSuggestionPreview((prev) =>
|
||||||
|
prev
|
||||||
|
? {
|
||||||
|
...prev,
|
||||||
|
skillChoices: prev.skillChoices.map((x) =>
|
||||||
|
x.skill_id === c.skill_id ? { ...x, include: !x.include } : x,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: prev,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
style={{ marginTop: '4px' }}
|
||||||
|
/>
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<div style={{ fontWeight: 600, fontSize: '13px', marginBottom: '6px' }}>
|
||||||
|
{c.kind === 'add' ? 'Neu hinzufügen' : 'Bestehende Zeile aktualisieren'}
|
||||||
|
</div>
|
||||||
|
{c.kind === 'update' && c.before ? (
|
||||||
|
<div style={{ fontSize: '12px', lineHeight: 1.5 }}>
|
||||||
|
<div style={{ color: 'var(--text3)', marginBottom: '2px' }}>Bisher</div>
|
||||||
|
<div style={{ marginBottom: '8px' }}>
|
||||||
|
{describeExerciseSkillRowForPreview(c.before, skillsCatalog)}
|
||||||
|
</div>
|
||||||
|
<div style={{ color: 'var(--text3)', marginBottom: '2px' }}>Nach KI-Vorschlag</div>
|
||||||
|
<div>{describeExerciseSkillRowForPreview(c.after, skillsCatalog)}</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ fontSize: '13px', lineHeight: 1.5 }}>
|
||||||
|
{describeExerciseSkillRowForPreview(c.after, skillsCatalog)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: '20px',
|
||||||
|
paddingTop: '14px',
|
||||||
|
borderTop: '1px solid var(--border)',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: '10px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button type="button" className="btn btn-secondary" onClick={discardExerciseAiSuggestionPreview}>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-primary"
|
||||||
|
disabled={!canApplySomething}
|
||||||
|
onClick={() => applyExerciseAiSuggestionPreview()}
|
||||||
|
>
|
||||||
|
Ausgewähltes übernehmen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
|
||||||
<ExercisePickerModal
|
<ExercisePickerModal
|
||||||
open={comboStationPickerIx !== null}
|
open={comboStationPickerIx !== null}
|
||||||
onClose={() => setComboStationPickerIx(null)}
|
onClose={() => setComboStationPickerIx(null)}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user