KI Implementierung (MVP) auf Übungen #46
|
|
@ -2735,258 +2735,6 @@ function ExerciseFormPageRoot() {
|
|||
</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 && (
|
||||
<MediaPreviewModal
|
||||
title={(mediaPreview.title || '').trim() || mediaPreview.original_filename || `Medium #${mediaPreview.id}`}
|
||||
|
|
@ -3017,6 +2765,259 @@ function ExerciseFormPageRoot() {
|
|||
</form>
|
||||
</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
|
||||
open={comboStationPickerIx !== null}
|
||||
onClose={() => setComboStationPickerIx(null)}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user