101 lines
3.1 KiB
JavaScript
101 lines
3.1 KiB
JavaScript
|
|
/**
|
||
|
|
* KI-gestützte Schnellanlage einer Übung (Planung / ExercisePickerModal).
|
||
|
|
*/
|
||
|
|
import { stripHtmlToText } from './htmlUtils'
|
||
|
|
import { normalizeSkillLevelSlug } from '../constants/skillLevels'
|
||
|
|
import {
|
||
|
|
EXERCISE_SKILL_INTENSITY_DEFAULT,
|
||
|
|
normalizeExerciseSkillIntensity,
|
||
|
|
} from '../constants/exerciseSkillIntensity'
|
||
|
|
|
||
|
|
function escapeHtmlText(s) {
|
||
|
|
return String(s)
|
||
|
|
.replace(/&/g, '&')
|
||
|
|
.replace(/</g, '<')
|
||
|
|
.replace(/>/g, '>')
|
||
|
|
.replace(/"/g, '"')
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Plaintext → Absätze für RichTextEditor / API. */
|
||
|
|
export function aiPlainTextToMinimalHtml(text) {
|
||
|
|
const raw = String(text || '').trim()
|
||
|
|
if (!raw) return ''
|
||
|
|
const parts = raw.split(/\n+/).map((p) => p.trim()).filter(Boolean)
|
||
|
|
const paras = parts.length ? parts : [raw]
|
||
|
|
return paras.map((p) => `<p>${escapeHtmlText(p)}</p>`).join('')
|
||
|
|
}
|
||
|
|
|
||
|
|
export function normalizeAiSkillRowFromApi(sug) {
|
||
|
|
const sid = Number(sug?.skill_id)
|
||
|
|
if (!Number.isFinite(sid) || sid < 1) return null
|
||
|
|
return {
|
||
|
|
skill_id: sid,
|
||
|
|
intensity: normalizeExerciseSkillIntensity(sug.intensity) || EXERCISE_SKILL_INTENSITY_DEFAULT,
|
||
|
|
required_level: normalizeSkillLevelSlug(sug.required_level) || 'grundlagen',
|
||
|
|
target_level:
|
||
|
|
normalizeSkillLevelSlug(sug.target_level) ||
|
||
|
|
normalizeSkillLevelSlug(sug.required_level) ||
|
||
|
|
'grundlagen',
|
||
|
|
is_primary: !!sug.is_primary,
|
||
|
|
ai_suggested: true,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Baut createExercise-Payload aus suggestExerciseAi-Antwort.
|
||
|
|
* @throws {Error} wenn Ziel/Durchführung nach KI leer wären
|
||
|
|
*/
|
||
|
|
export function buildQuickCreateExercisePayload({ title, focusAreaId, sketchPlain, apiRes }) {
|
||
|
|
const sketchHtml = aiPlainTextToMinimalHtml(sketchPlain)
|
||
|
|
const fields = apiRes?.instructions?.fields || {}
|
||
|
|
|
||
|
|
let goal = (fields.goal || '').trim() || sketchHtml
|
||
|
|
let execution = (fields.execution || '').trim()
|
||
|
|
const prep = (fields.preparation || '').trim() || null
|
||
|
|
const trainerNotes = (fields.trainer_notes || '').trim() || null
|
||
|
|
|
||
|
|
if (!stripHtmlToText(goal).trim() && !stripHtmlToText(execution).trim()) {
|
||
|
|
throw new Error('KI lieferte keine verwertbare Anleitung (Ziel/Durchführung leer).')
|
||
|
|
}
|
||
|
|
if (!stripHtmlToText(goal).trim()) goal = execution
|
||
|
|
if (!stripHtmlToText(execution).trim()) execution = goal
|
||
|
|
|
||
|
|
let summary = null
|
||
|
|
if (apiRes?.summary?.text) {
|
||
|
|
const sumHtml = aiPlainTextToMinimalHtml(apiRes.summary.text)
|
||
|
|
if (sumHtml) summary = sumHtml
|
||
|
|
}
|
||
|
|
|
||
|
|
const skills = []
|
||
|
|
if (Array.isArray(apiRes?.skills)) {
|
||
|
|
for (const sug of apiRes.skills) {
|
||
|
|
const row = normalizeAiSkillRowFromApi(sug)
|
||
|
|
if (row) skills.push(row)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const fid = Number(focusAreaId)
|
||
|
|
if (!Number.isFinite(fid) || fid < 1) {
|
||
|
|
throw new Error('Fokusbereich ist erforderlich.')
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
title: (title || '').trim(),
|
||
|
|
summary,
|
||
|
|
goal,
|
||
|
|
execution,
|
||
|
|
preparation: prep,
|
||
|
|
trainer_notes: trainerNotes,
|
||
|
|
visibility: 'private',
|
||
|
|
status: 'draft',
|
||
|
|
equipment: [],
|
||
|
|
focus_areas_multi: [{ focus_area_id: fid, is_primary: true }],
|
||
|
|
training_styles_multi: [],
|
||
|
|
training_types_multi: [],
|
||
|
|
target_groups_multi: [],
|
||
|
|
age_groups: [],
|
||
|
|
skills,
|
||
|
|
club_id: null,
|
||
|
|
}
|
||
|
|
}
|