shinkan-jinkendo/frontend/src/utils/exerciseAiQuickCreate.js

101 lines
3.1 KiB
JavaScript
Raw Normal View History

/**
* 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, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
}
/** 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,
}
}