2026-05-14 21:49:56 +02:00
/ * *
* Übungen : Liste / CRUD , Medien & Archiv - Anbindung , Progressionsgraphen , KI - Hilfen .
* /
import { stripHtmlToText } from '../utils/htmlUtils'
import { normalizeAdvanceMode , parseComboRepSeriesCountUi } from '../utils/combinationMethodProfileUi'
2026-05-21 12:35:45 +02:00
import { normalizeExerciseSkillIntensity } from '../constants/exerciseSkillIntensity'
2026-05-14 21:49:56 +02:00
import { request , API _URL , ACTIVE _CLUB _STORAGE _KEY } from './client.js'
/** Wie `mergeActiveClubHeader` in client.js — lokal, damit Raw-`fetch`-Pfade nicht von einem Namensimport abhängen. */
function withActiveClubHeaders ( headers = { } ) {
const cid = localStorage . getItem ( ACTIVE _CLUB _STORAGE _KEY )
if ( cid && /^\d+$/ . test ( String ( cid ) . trim ( ) ) ) {
return { ... headers , 'X-Active-Club-Id' : String ( cid ) . trim ( ) }
}
return { ... headers }
}
// ============================================================================
// Exercises
// ============================================================================
export async function listExercises ( filters = { } ) {
const q = new URLSearchParams ( )
Object . entries ( filters ) . forEach ( ( [ k , v ] ) => {
if ( v === undefined || v === null ) return
if ( typeof v === 'boolean' ) {
q . set ( k , v ? 'true' : 'false' )
return
}
if ( Array . isArray ( v ) ) {
if ( v . length === 0 ) return
v . forEach ( ( item ) => {
if ( item !== '' && item !== undefined && item !== null && String ( item ) . trim ( ) !== '' ) {
q . append ( k , String ( item ) )
}
} )
return
}
if ( String ( v ) . trim ( ) !== '' ) q . set ( k , String ( v ) )
} )
const query = q . toString ( )
return request ( ` /api/exercises ${ query ? '?' + query : '' } ` )
}
/** Formular → API-Body (M:N gemäß EXERCISES_API_SPEC + training_types) */
export function buildExerciseApiPayload ( formData , extras = { } ) {
const num = ( v ) => ( v === '' || v == null ? null : Number ( v ) )
const goalHtml = formData . goal || ''
const execHtml = formData . execution || ''
const goalText = stripHtmlToText ( goalHtml )
const execText = stripHtmlToText ( execHtml )
if ( ! goalText && ! execText ) {
throw new Error ( 'Ziel oder Durchführung ausfüllen (mindestens eines, auch mit Formatierung).' )
}
const mapFocus = ( formData . focus _areas _multi || [ ] )
. filter ( ( x ) => x && x . focus _area _id )
. map ( ( x ) => ( { focus _area _id : Number ( x . focus _area _id ) , is _primary : ! ! x . is _primary } ) )
const mapStyles = ( formData . training _styles _multi || [ ] )
. filter ( ( x ) => x && x . training _style _id )
. map ( ( x ) => ( { training _style _id : Number ( x . training _style _id ) , is _primary : ! ! x . is _primary } ) )
const mapTTypes = ( formData . training _types _multi || [ ] )
. filter ( ( x ) => x && x . training _type _id )
. map ( ( x ) => ( { training _type _id : Number ( x . training _type _id ) , is _primary : ! ! x . is _primary } ) )
const mapTg = ( formData . target _groups _multi || [ ] )
. filter ( ( x ) => x && x . target _group _id )
. map ( ( x ) => ( { target _group _id : Number ( x . target _group _id ) , is _primary : ! ! x . is _primary } ) )
const visibilityNorm = String ( formData . visibility || 'private' ) . trim ( ) . toLowerCase ( )
const payload = {
title : ( formData . title || '' ) . trim ( ) ,
summary : formData . summary || null ,
goal : goalHtml . trim ( ) ? goalHtml : null ,
execution : execHtml . trim ( ) ? execHtml : null ,
preparation : formData . preparation || null ,
trainer _notes : formData . trainer _notes || null ,
duration _min : num ( formData . duration _min ) ,
duration _max : num ( formData . duration _max ) ,
group _size _min : num ( formData . group _size _min ) ,
group _size _max : num ( formData . group _size _max ) ,
equipment : Array . isArray ( formData . equipment ) ? formData . equipment : [ ] ,
focus _areas _multi : mapFocus ,
training _styles _multi : mapStyles ,
training _types _multi : mapTTypes ,
target _groups _multi : mapTg ,
age _groups : [ ] ,
skills : ( formData . skills || [ ] ) . map ( ( s ) => ( {
skill _id : s . skill _id ,
Enhance Exercise Management and AI Integration
- Updated the exercise form to include a tabbed navigation structure, improving user experience with sections for Stammdaten, Anleitung, Einordnung, Varianten, and Medien & Mehr.
- Introduced the concept of **Freigabelevel** (visibility level) in the UI, replacing previous terminology for clarity and consistency across components.
- Implemented new AI endpoints for exercise suggestions and regeneration, allowing for dynamic content generation without direct database writes.
- Removed the legacy `is_primary` flag from exercise skills in the UI, ensuring that intensity levels (`niedrig`, `mittel`, `hoch`) are the primary focus for skill management.
- Enhanced the variant management process with improved saving mechanisms and UI updates to reflect changes more intuitively.
2026-05-22 07:52:31 +02:00
is _primary : ! ! s . is _primary ,
2026-05-21 12:35:45 +02:00
intensity : normalizeExerciseSkillIntensity ( s . intensity ) ,
2026-05-14 21:49:56 +02:00
required _level : s . required _level || null ,
target _level : s . target _level || null ,
Enhance Exercise Management and AI Integration
- Updated the exercise form to include a tabbed navigation structure, improving user experience with sections for Stammdaten, Anleitung, Einordnung, Varianten, and Medien & Mehr.
- Introduced the concept of **Freigabelevel** (visibility level) in the UI, replacing previous terminology for clarity and consistency across components.
- Implemented new AI endpoints for exercise suggestions and regeneration, allowing for dynamic content generation without direct database writes.
- Removed the legacy `is_primary` flag from exercise skills in the UI, ensuring that intensity levels (`niedrig`, `mittel`, `hoch`) are the primary focus for skill management.
- Enhanced the variant management process with improved saving mechanisms and UI updates to reflect changes more intuitively.
2026-05-22 07:52:31 +02:00
ai _suggested : ! ! s . ai _suggested ,
2026-05-14 21:49:56 +02:00
} ) ) ,
visibility : visibilityNorm ,
status : formData . status || 'draft' ,
club _id : visibilityNorm === 'club' ? num ( formData . club _id ) : null ,
exercise _kind :
String ( formData . exercise _kind || 'simple' ) . toLowerCase ( ) === 'combination'
? 'combination'
: 'simple' ,
... extras ,
}
const isCombo = payload . exercise _kind === 'combination'
if ( isCombo ) {
let mpObj = { }
const mpRaw = typeof formData . method _profile _json === 'string' ? formData . method _profile _json . trim ( ) : ''
if ( mpRaw ) {
try {
const parsed = JSON . parse ( mpRaw )
if ( ! parsed || typeof parsed !== 'object' || Array . isArray ( parsed ) ) {
throw new Error ( 'Ablaufprofil muss ein JSON-Objekt sein.' )
}
mpObj = parsed
} catch ( e ) {
if ( e instanceof SyntaxError ) {
throw new Error ( 'Ablaufprofil (JSON): Syntax ungültig.' )
}
throw e
}
}
const slotRows = Array . isArray ( formData . combination _slots ) ? formData . combination _slots : [ ]
const combination _slots = [ ]
function parseTimingField ( raw ) {
if ( raw === '' || raw == null || raw === undefined ) return undefined
const n = parseInt ( String ( raw ) , 10 )
return Number . isFinite ( n ) ? n : undefined
}
for ( let i = 0 ; i < slotRows . length ; i += 1 ) {
const row = slotRows [ i ] || { }
let ids = Array . isArray ( row . candidate _exercise _ids )
? row . candidate _exercise _ids . map ( ( x ) => Number ( x ) ) . filter ( ( n ) => Number . isFinite ( n ) )
: [ ]
/** Legacy: noch idsText Unterstützung für Import von älteren FormStand */
if ( ( ! ids || ids . length === 0 ) && typeof row . idsText === 'string' && row . idsText . trim ( ) ) {
ids = row . idsText
. split ( /[\s,;]+/ )
. map ( ( s ) => s . trim ( ) )
. filter ( Boolean )
. map ( ( s ) => parseInt ( s , 10 ) )
. filter ( ( n ) => Number . isFinite ( n ) )
}
combination _slots . push ( {
slot _index : i ,
title : ( typeof row . title === 'string' && row . title . trim ( ) ) || null ,
candidate _exercise _ids : ids ,
} )
}
const slot _profiles _v1 _next = [ ]
for ( let i = 0 ; i < slotRows . length ; i += 1 ) {
const row = slotRows [ i ] || { }
const o = { slot _index : i }
const advanceMode = normalizeAdvanceMode ( row . advance _mode )
if ( advanceMode !== 'timed' ) o . advance _mode = advanceMode
const load = parseTimingField ( row . load _sec )
const crs = parseTimingField ( row . consecutive _reps )
const rsc = parseTimingField ( row . rep _series _count )
const intra = parseTimingField ( row . intra _rep _rest _sec )
const tran = parseTimingField ( row . transition _after _sec )
const serienUi = parseComboRepSeriesCountUi ( row . rep _series _count )
const allowInterSeriesPause =
advanceMode === 'timed' ||
( ( advanceMode === 'rep' || advanceMode === 'manual' ) && serienUi >= 2 )
if ( advanceMode === 'timed' && load !== undefined && load >= 0 ) o . load _sec = Math . round ( load )
if ( crs !== undefined && crs >= 1 ) o . consecutive _reps = Math . round ( crs )
if (
rsc !== undefined &&
rsc >= 1 &&
( advanceMode === 'rep' || advanceMode === 'manual' )
) {
o . rep _series _count = Math . round ( rsc )
}
if ( intra !== undefined && intra >= 0 && allowInterSeriesPause ) o . intra _rep _rest _sec = Math . round ( intra )
if ( tran !== undefined && tran >= 0 ) o . transition _after _sec = Math . round ( tran )
if ( Object . keys ( o ) . length > 1 ) slot _profiles _v1 _next . push ( o )
}
payload . method _archetype = ( formData . method _archetype || '' ) . trim ( ) || null
if ( slot _profiles _v1 _next . length > 0 ) mpObj . slot _profiles _v1 = slot _profiles _v1 _next
else delete mpObj . slot _profiles _v1
payload . method _profile = mpObj
payload . combination _slots = combination _slots
} else {
payload . method _archetype = null
payload . method _profile = { }
}
return payload
}
export async function uploadExerciseMedia ( exerciseId , formData ) {
const token = localStorage . getItem ( 'authToken' )
const headers = withActiveClubHeaders ( { } )
if ( token ) headers [ 'X-Auth-Token' ] = token
const response = await fetch ( ` ${ API _URL } /api/exercises/ ${ exerciseId } /media ` , {
method : 'POST' ,
headers ,
body : formData ,
} )
if ( ! response . ok ) {
const text = await response . text ( )
let parsed = null
try {
parsed = text ? JSON . parse ( text ) : null
} catch {
parsed = null
}
const d = parsed ? . detail
if (
response . status === 409 &&
d &&
typeof d === 'object' &&
! Array . isArray ( d ) &&
typeof d . code === 'string'
) {
const e = new Error (
typeof d . message === 'string' ? d . message : 'Upload konnte nicht verarbeitet werden' ,
)
e . code = d . code
e . status = 409
e . payload = d
throw e
}
if ( response . status === 413 ) {
const nginx = ( text || '' ) . toLowerCase ( ) . includes ( 'nginx' )
throw new Error (
nginx
? 'Die Anfrage ist zu groß (413). Häufig: nginx „client_max_body_size“ — z. B. große/r mehrere Videos oder Bulk-Upload. Dateien kleiner aufteilen oder Server-Limit erhöhen (Frontend-Image Neu bauen).'
: 'Die Anfrage ist zu groß (413). Dateigröße oder Server-Limit prüfen.' ,
)
}
const msg =
typeof d === 'string'
? d
: d != null && typeof d === 'object' && typeof d . message === 'string'
? d . message
: d != null
? JSON . stringify ( d )
: text && text . length < 400 && ! /^\s*</ . test ( text )
? ` HTTP ${ response . status } : ${ text . trim ( ) } `
: ` HTTP ${ response . status } `
throw new Error ( msg )
}
return response . json ( )
}
export async function updateExerciseMedia ( exerciseId , mediaId , data ) {
return request ( ` /api/exercises/ ${ exerciseId } /media/ ${ mediaId } ` , {
method : 'PUT' ,
body : JSON . stringify ( data ) ,
} )
}
export async function deleteExerciseMedia ( exerciseId , mediaId ) {
return request ( ` /api/exercises/ ${ exerciseId } /media/ ${ mediaId } ` , { method : 'DELETE' } )
}
export async function reorderExerciseMedia ( exerciseId , mediaIds ) {
return request ( ` /api/exercises/ ${ exerciseId } /media/reorder ` , {
method : 'PUT' ,
body : JSON . stringify ( { media _ids : mediaIds } ) ,
} )
}
/** Papierkorb / Recovery — `action`: trash_soft | trash_hidden | recover | purge | reactivate */
export async function postMediaAssetLifecycle ( assetId , action , extra = { } ) {
return request ( ` /api/media-assets/ ${ assetId } /lifecycle ` , {
method : 'POST' ,
body : JSON . stringify ( { action , ... extra } ) ,
} )
}
/** Archiv: aktive media_assets sichtbar für den Nutzer (Bibliotheksrechte). */
export async function listMediaAssets ( params = { } ) {
const sp = new URLSearchParams ( )
if ( params . q ) sp . set ( 'q' , params . q )
if ( params . limit != null ) sp . set ( 'limit' , String ( params . limit ) )
if ( params . offset != null ) sp . set ( 'offset' , String ( params . offset ) )
if ( params . lifecycle ) sp . set ( 'lifecycle' , String ( params . lifecycle ) )
if ( params . media _kind ) sp . set ( 'media_kind' , String ( params . media _kind ) )
if ( params . club _id != null && params . club _id !== '' ) sp . set ( 'club_id' , String ( params . club _id ) )
if ( params . uploaded _by != null && params . uploaded _by !== '' ) sp . set ( 'uploaded_by' , String ( params . uploaded _by ) )
if ( params . include _filter _meta ) sp . set ( 'include_filter_meta' , 'true' )
const qs = sp . toString ( )
return request ( ` /api/media-assets ${ qs ? ` ? ${ qs } ` : '' } ` )
}
export async function patchMediaAsset ( assetId , data ) {
return request ( ` /api/media-assets/ ${ assetId } ` , {
method : 'PATCH' ,
body : JSON . stringify ( data ) ,
} )
}
export async function bulkMediaLifecycle ( data ) {
return request ( '/api/media-assets/bulk-lifecycle' , {
method : 'POST' ,
body : JSON . stringify ( data ) ,
} )
}
export async function bulkPatchMediaAssets ( data ) {
return request ( '/api/media-assets/bulk-patch' , {
method : 'POST' ,
body : JSON . stringify ( data ) ,
} )
}
/ * *
* Mehrere Dateien ins Medienarchiv ( ` POST /api/media-assets/bulk-upload ` ) .
* @ param { File [ ] } files
* @ param { { visibility ? : string , club _id ? : number } } [ options ]
* /
export async function bulkUploadMediaAssets ( files , options = { } ) {
const visibility = options . visibility || 'private'
const token = localStorage . getItem ( 'authToken' )
const headers = withActiveClubHeaders ( { } )
if ( token ) headers [ 'X-Auth-Token' ] = token
const formData = new FormData ( )
formData . append ( 'visibility' , String ( visibility ) )
if ( options . club _id != null && options . club _id !== '' ) {
formData . append ( 'club_id' , String ( options . club _id ) )
}
// Copyright + P-06: Rechte-Erklaerung + Kontextfelder
if ( options . copyright _notice != null && String ( options . copyright _notice ) . trim ( ) )
formData . append ( 'copyright_notice' , String ( options . copyright _notice ) . trim ( ) )
const p06Fields = [
'rights_holder_confirmed' ,
'contains_identifiable_persons' ,
'person_consent_confirmed' ,
'person_consent_context' ,
'contains_minors' ,
'parental_consent_confirmed' ,
'parental_consent_context' ,
'contains_music' ,
'music_rights_confirmed' ,
'music_rights_context' ,
'contains_third_party_content' ,
'third_party_rights_confirmed' ,
'third_party_rights_context' ,
]
for ( const f of p06Fields ) {
if ( options [ f ] != null && options [ f ] !== '' ) formData . append ( f , String ( options [ f ] ) )
}
const arr = Array . isArray ( files ) ? files : [ files ]
for ( const f of arr ) {
if ( f ) formData . append ( 'files' , f )
}
const url = ` ${ API _URL } /api/media-assets/bulk-upload `
const response = await fetch ( url , {
method : 'POST' ,
headers ,
body : formData ,
} )
if ( ! response . ok ) {
const text = await response . text ( )
let parsed = null
try {
parsed = JSON . parse ( text )
} catch {
parsed = null
}
if ( parsed ? . detail != null ) {
const d = parsed . detail
throw new Error ( typeof d === 'string' ? d : JSON . stringify ( d ) )
}
const snippet = ( text || '' ) . replace ( /\s+/g , ' ' ) . trim ( ) . slice ( 0 , 180 )
throw new Error ( snippet ? ` HTTP ${ response . status } : ${ snippet } ` : ` HTTP ${ response . status } ` )
}
return response . json ( )
}
/** Übung: bestehendes Archiv-Medium verknüpfen (`POST /api/exercises/{id}/media/from-asset`). */
export async function getMediaAssetJournal ( assetId ) {
return request ( ` /api/admin/media-rights/assets/ ${ assetId } /journal ` )
}
export async function addMediaAssetDeclarationCorrection ( assetId , body ) {
return request ( ` /api/admin/media-rights/assets/ ${ assetId } /correction ` , {
method : 'POST' ,
body : JSON . stringify ( body ) ,
} )
}
export async function attachExerciseMediaFromAsset ( exerciseId , body ) {
return request ( ` /api/exercises/ ${ exerciseId } /media/from-asset ` , {
method : 'POST' ,
body : JSON . stringify ( body ) ,
} )
}
// P-11: Legal-Hold-Endpunkte
export async function setMediaAssetLegalHold ( assetId , reasonCode , reasonNote ) {
return request ( ` /api/admin/media-assets/ ${ assetId } /legal-hold ` , {
method : 'POST' ,
body : JSON . stringify ( { reason _code : reasonCode , reason _note : reasonNote } ) ,
} )
}
export async function releaseMediaAssetLegalHold ( assetId , releaseNote ) {
return request ( ` /api/admin/media-assets/ ${ assetId } /legal-hold/release ` , {
method : 'POST' ,
body : JSON . stringify ( { release _note : releaseNote } ) ,
} )
}
export async function listMediaAssetsWithLegalHold ( limit = 100 , offset = 0 ) {
return request ( ` /api/admin/media-assets/legal-hold?limit= ${ limit } &offset= ${ offset } ` )
}
export async function getExercise ( id ) {
return request ( ` /api/exercises/ ${ id } ` )
}
export async function createExercise ( data ) {
return request ( '/api/exercises' , {
method : 'POST' ,
body : JSON . stringify ( data ) ,
} )
}
export async function updateExercise ( id , data ) {
const token = localStorage . getItem ( 'authToken' )
const headers = withActiveClubHeaders ( { 'Content-Type' : 'application/json' } )
if ( token ) headers [ 'X-Auth-Token' ] = token
const url = ` ${ API _URL } /api/exercises/ ${ id } `
const response = await fetch ( url , {
method : 'PUT' ,
headers ,
body : JSON . stringify ( data ) ,
} )
if ( ! response . ok ) {
const text = await response . text ( )
let parsed = null
try {
parsed = JSON . parse ( text )
} catch {
parsed = null
}
const d = parsed ? . detail
if (
response . status === 422 &&
d &&
typeof d === 'object' &&
! Array . isArray ( d ) &&
typeof d . code === 'string'
) {
const e = new Error ( typeof d . message === 'string' ? d . message : 'Validierung fehlgeschlagen' )
e . status = 422
e . code = d . code
e . payload = d
throw e
}
if ( parsed ? . detail != null ) {
const msg = typeof parsed . detail === 'string' ? parsed . detail : JSON . stringify ( parsed . detail )
throw new Error ( msg )
}
const snippet = ( text || '' ) . replace ( /\s+/g , ' ' ) . trim ( ) . slice ( 0 , 180 )
throw new Error ( snippet ? ` HTTP ${ response . status } : ${ snippet } ` : ` HTTP ${ response . status } ` )
}
return response . json ( )
}
/** Massenänderung Übungen: Sichtbarkeit, Status, Katalog-Zuordnungen (`PATCH /api/exercises/bulk-metadata`). */
export async function bulkPatchExercisesMetadata ( data ) {
return request ( '/api/exercises/bulk-metadata' , {
method : 'PATCH' ,
body : JSON . stringify ( data ) ,
} )
}
export async function deleteExercise ( id ) {
return request ( ` /api/exercises/ ${ id } ` , { method : 'DELETE' } )
}
export async function createExerciseVariant ( exerciseId , data ) {
return request ( ` /api/exercises/ ${ exerciseId } /variants ` , {
method : 'POST' ,
body : JSON . stringify ( data ) ,
} )
}
export async function updateExerciseVariant ( exerciseId , variantId , data ) {
return request ( ` /api/exercises/ ${ exerciseId } /variants/ ${ variantId } ` , {
method : 'PUT' ,
body : JSON . stringify ( data ) ,
} )
}
export async function deleteExerciseVariant ( exerciseId , variantId ) {
return request ( ` /api/exercises/ ${ exerciseId } /variants/ ${ variantId } ` , { method : 'DELETE' } )
}
export async function reorderExerciseVariants ( exerciseId , variantIds ) {
return request ( ` /api/exercises/ ${ exerciseId } /variants/reorder ` , {
method : 'PUT' ,
body : JSON . stringify ( { variant _ids : variantIds } ) ,
} )
}
// Progressionsgraphen (Übung → Übung), Migration 032/033
export async function listExerciseProgressionGraphs ( ) {
return request ( '/api/exercise-progression-graphs' )
}
export async function getExerciseProgressionGraph ( id , { includeEdges = false } = { } ) {
const q = includeEdges ? '?include_edges=true' : ''
return request ( ` /api/exercise-progression-graphs/ ${ id } ${ q } ` )
}
2026-06-11 12:10:46 +02:00
export async function getProgressionGraphVisibilityPromotionCandidates (
graphId ,
{ targetVisibility = 'club' } = { } ,
) {
const q = new URLSearchParams ( { target _visibility : targetVisibility } )
return request (
` /api/exercise-progression-graphs/ ${ graphId } /visibility-promotion-candidates? ${ q } ` ,
)
}
2026-05-14 21:49:56 +02:00
export async function createExerciseProgressionGraph ( data ) {
return request ( '/api/exercise-progression-graphs' , {
method : 'POST' ,
body : JSON . stringify ( data ) ,
} )
}
export async function updateExerciseProgressionGraph ( id , data ) {
return request ( ` /api/exercise-progression-graphs/ ${ id } ` , {
method : 'PUT' ,
body : JSON . stringify ( data ) ,
} )
}
export async function deleteExerciseProgressionGraph ( id ) {
return request ( ` /api/exercise-progression-graphs/ ${ id } ` , { method : 'DELETE' } )
}
export async function listExerciseProgressionEdges ( graphId , query = { } ) {
const q = new URLSearchParams ( )
if ( query . from _exercise _id != null ) q . set ( 'from_exercise_id' , String ( query . from _exercise _id ) )
if ( query . to _exercise _id != null ) q . set ( 'to_exercise_id' , String ( query . to _exercise _id ) )
const qs = q . toString ( )
return request ( ` /api/exercise-progression-graphs/ ${ graphId } /edges ${ qs ? ` ? ${ qs } ` : '' } ` )
}
export async function createExerciseProgressionEdge ( graphId , data ) {
return request ( ` /api/exercise-progression-graphs/ ${ graphId } /edges ` , {
method : 'POST' ,
body : JSON . stringify ( data ) ,
} )
}
export async function updateExerciseProgressionEdge ( graphId , edgeId , data ) {
return request ( ` /api/exercise-progression-graphs/ ${ graphId } /edges/ ${ edgeId } ` , {
method : 'PUT' ,
body : JSON . stringify ( data ) ,
} )
}
export async function deleteExerciseProgressionEdge ( graphId , edgeId ) {
return request ( ` /api/exercise-progression-graphs/ ${ graphId } /edges/ ${ edgeId } ` , { method : 'DELETE' } )
}
export async function createExerciseProgressionSequence ( graphId , data ) {
return request ( ` /api/exercise-progression-graphs/ ${ graphId } /edges/sequence ` , {
method : 'POST' ,
body : JSON . stringify ( data ) ,
} )
}
export async function deleteExerciseProgressionEdgesBatch ( graphId , edgeIds ) {
return request ( ` /api/exercise-progression-graphs/ ${ graphId } /edges/delete-batch ` , {
method : 'POST' ,
body : JSON . stringify ( { edge _ids : edgeIds } ) ,
} )
}
Enhance Exercise Management and AI Integration
- Updated the exercise form to include a tabbed navigation structure, improving user experience with sections for Stammdaten, Anleitung, Einordnung, Varianten, and Medien & Mehr.
- Introduced the concept of **Freigabelevel** (visibility level) in the UI, replacing previous terminology for clarity and consistency across components.
- Implemented new AI endpoints for exercise suggestions and regeneration, allowing for dynamic content generation without direct database writes.
- Removed the legacy `is_primary` flag from exercise skills in the UI, ensuring that intensity levels (`niedrig`, `mittel`, `hoch`) are the primary focus for skill management.
- Enhanced the variant management process with improved saving mechanisms and UI updates to reflect changes more intuitively.
2026-05-22 07:52:31 +02:00
/** KI (OpenRouter): Nur Vorschlaege; Speichern ueber normales exercise PUT/POST. */
export async function suggestExerciseAi ( payload = { } ) {
2026-05-14 21:49:56 +02:00
return request ( '/api/exercises/ai/suggest' , {
method : 'POST' ,
Enhance Exercise Management and AI Integration
- Updated the exercise form to include a tabbed navigation structure, improving user experience with sections for Stammdaten, Anleitung, Einordnung, Varianten, and Medien & Mehr.
- Introduced the concept of **Freigabelevel** (visibility level) in the UI, replacing previous terminology for clarity and consistency across components.
- Implemented new AI endpoints for exercise suggestions and regeneration, allowing for dynamic content generation without direct database writes.
- Removed the legacy `is_primary` flag from exercise skills in the UI, ensuring that intensity levels (`niedrig`, `mittel`, `hoch`) are the primary focus for skill management.
- Enhanced the variant management process with improved saving mechanisms and UI updates to reflect changes more intuitively.
2026-05-22 07:52:31 +02:00
body : JSON . stringify ( {
include _summary : true ,
include _skills : true ,
... payload ,
} ) ,
2026-05-14 21:49:56 +02:00
} )
}
Enhance Exercise Management and AI Integration
- Updated the exercise form to include a tabbed navigation structure, improving user experience with sections for Stammdaten, Anleitung, Einordnung, Varianten, and Medien & Mehr.
- Introduced the concept of **Freigabelevel** (visibility level) in the UI, replacing previous terminology for clarity and consistency across components.
- Implemented new AI endpoints for exercise suggestions and regeneration, allowing for dynamic content generation without direct database writes.
- Removed the legacy `is_primary` flag from exercise skills in the UI, ensuring that intensity levels (`niedrig`, `mittel`, `hoch`) are the primary focus for skill management.
- Enhanced the variant management process with improved saving mechanisms and UI updates to reflect changes more intuitively.
2026-05-22 07:52:31 +02:00
export async function regenerateExerciseAi ( exerciseId , payload = { } ) {
2026-05-14 21:49:56 +02:00
return request ( ` /api/exercises/ ${ exerciseId } /ai/regenerate ` , {
method : 'POST' ,
Enhance Exercise Management and AI Integration
- Updated the exercise form to include a tabbed navigation structure, improving user experience with sections for Stammdaten, Anleitung, Einordnung, Varianten, and Medien & Mehr.
- Introduced the concept of **Freigabelevel** (visibility level) in the UI, replacing previous terminology for clarity and consistency across components.
- Implemented new AI endpoints for exercise suggestions and regeneration, allowing for dynamic content generation without direct database writes.
- Removed the legacy `is_primary` flag from exercise skills in the UI, ensuring that intensity levels (`niedrig`, `mittel`, `hoch`) are the primary focus for skill management.
- Enhanced the variant management process with improved saving mechanisms and UI updates to reflect changes more intuitively.
2026-05-22 07:52:31 +02:00
body : JSON . stringify ( {
regenerate : [ 'summary' , 'skills' ] ,
... payload ,
} ) ,
2026-05-14 21:49:56 +02:00
} )
}