Files
hejyou_content_creation/frontend/src/api.ts
admin 214f8a2019 feat: object label per object + {obj:UUID} sentence placeholders
- Annotate: per-object single label input (M2M via db_objects_db_words), auto-save on blur, remove picture-level word section
- Generate: object chips insert {obj:UUID} at cursor position in question/statement textarea
- Live preview resolves {obj:UUID} → actual object label
- PairsList display also resolves placeholders
- Remove F/A/B word chip system from pair form (replaced by object placeholders)
- Backend: POST /api/directus/db-objects/<id>/words replaces existing word with single label

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 13:04:37 +02:00

505 lines
18 KiB
TypeScript

import type { ObjectMeta, Sentence, PictureWord } from './types'
const DIRECTUS_URL = 'https://db.hejyou.com'
export async function directusLogin(email: string, password: string): Promise<string> {
const res = await fetch('/api/directus/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
})
const data = await res.json()
if (!res.ok) throw new Error(data.errors?.[0]?.message || 'Login fehlgeschlagen')
return data.data.access_token
}
export interface DirectusPicture {
id: string
media: string
status: string
}
export async function getDirectusPictures(token: string, status = 'new'): Promise<DirectusPicture[]> {
const res = await fetch(`/api/directus/pictures?status=${status}`, {
headers: { Authorization: `Bearer ${token}` },
})
if (!res.ok) throw new Error('Fehler beim Laden der Directus-Bilder')
const data = await res.json()
return data.data as DirectusPicture[]
}
export async function updatePictureStatus(pictureId: string, status: string, token: string): Promise<void> {
const res = await fetch(`/api/directus/pictures/${pictureId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ status }),
})
if (!res.ok) throw new Error('Fehler beim Aktualisieren des Status')
}
export function directusAssetUrl(mediaId: string, token: string): string {
return `${DIRECTUS_URL}/assets/${mediaId}?access_token=${token}`
}
import type { DirectusObject, Selection } from './types'
export async function getDirectusObjects(pictureId: string, token: string): Promise<DirectusObject[]> {
const res = await fetch(`/api/directus/objects?picture_id=${pictureId}`, {
headers: { Authorization: `Bearer ${token}` },
})
if (!res.ok) throw new Error('Fehler beim Laden der Objekte')
const data = await res.json()
return data.data as DirectusObject[]
}
export async function createDirectusObject(payload: {
picture: string
selections: Selection[] | null
user_notes: string | null
parent: string | null
}, token: string): Promise<DirectusObject> {
const res = await fetch('/api/directus/objects', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ ...payload, status: 'draft' }),
})
const data = await res.json()
if (!res.ok) throw new Error(data.errors?.[0]?.message || 'Fehler beim Speichern')
return data.data as DirectusObject
}
export async function updateDirectusObject(
objId: string,
payload: Partial<Pick<DirectusObject, 'user_notes' | 'parent'>>,
token: string
): Promise<DirectusObject> {
const res = await fetch(`/api/directus/objects/${objId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify(payload),
})
const data = await res.json()
if (!res.ok) throw new Error(data.errors?.[0]?.message || 'Fehler beim Aktualisieren')
return data.data as DirectusObject
}
export async function deleteDirectusObject(objId: string, token: string): Promise<void> {
const res = await fetch(`/api/directus/objects/${objId}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${token}` },
})
if (!res.ok) throw new Error('Fehler beim Löschen')
}
export async function getImages(mode: 'draw' | 'generate'): Promise<string[]> {
const res = await fetch(`/api/images?mode=${mode}`)
if (!res.ok) throw new Error('Fehler beim Laden der Bilder')
const data = await res.json()
return data.images as string[]
}
export async function getObjects(filename: string): Promise<ObjectMeta[]> {
const res = await fetch(`/api/objects?filename=${encodeURIComponent(filename)}`)
if (!res.ok) throw new Error('Fehler beim Laden der Objekte')
const data = await res.json()
return (data.objects || []) as ObjectMeta[]
}
export async function cropImage(payload: {
filename: string
selections: Array<{
number: number
mode: string
bbox?: { x: number; y: number; width: number; height: number } | null
polygon?: Array<{ x: number; y: number }> | null
}>
title_de: string
position_de: string
action_de: string
condition_de: string
}): Promise<{ id: string; image_file: string; meta_file: string }> {
const res = await fetch('/api/crop', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
const data = await res.json()
if (!res.ok) throw new Error(data.error || 'Fehler beim Speichern des Ausschnitts')
return data
}
export async function saveImage(filename: string): Promise<{ old_name: string; new_name: string }> {
const res = await fetch('/api/image/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filename }),
})
const data = await res.json()
if (!res.ok) throw new Error(data.error || 'Fehler beim Speichern des Bildes')
return data
}
export async function updateObjectMeta(
objId: string,
meta: { title_de: string; position_de: string; action_de: string; condition_de: string }
): Promise<void> {
const res = await fetch(`/api/object/${encodeURIComponent(objId)}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(meta),
})
if (!res.ok) throw new Error('Fehler beim Aktualisieren der Metadaten')
}
export async function updateHierarchy(objId: string, hierarchy: number): Promise<void> {
const res = await fetch(`/api/object/${encodeURIComponent(objId)}/hierarchy`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ hierarchy }),
})
if (!res.ok) throw new Error('Fehler beim Aktualisieren der Hierarchie')
}
export async function updateParent(objId: string, parentId: string | null): Promise<void> {
const res = await fetch(`/api/object/${encodeURIComponent(objId)}/parent`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ parent_id: parentId }),
})
if (!res.ok) throw new Error('Fehler beim Aktualisieren der Parent-Relation')
}
export async function generateDetails(objId: string): Promise<Partial<ObjectMeta>> {
const res = await fetch(`/api/object/${encodeURIComponent(objId)}/generate_details`, {
method: 'POST',
})
const data = await res.json()
if (!res.ok) throw new Error(data.error || 'Fehler bei KI-Details')
return data
}
export async function getSentences(objId: string): Promise<Sentence[]> {
const res = await fetch(`/api/object/${encodeURIComponent(objId)}/sentences`)
if (!res.ok) throw new Error('Fehler beim Laden der Sätze')
const data = await res.json()
return (data.sentences || []) as Sentence[]
}
export async function generateSentence(
objId: string
): Promise<{ sentence: Sentence; count: number }> {
const res = await fetch(`/api/object/${encodeURIComponent(objId)}/generate_sentence`, {
method: 'POST',
})
const data = await res.json()
if (!res.ok) throw new Error(data.error || 'Fehler bei KI-Sentence')
return data
}
export interface GenerateStats {
words_created: number
words_linked: number
questions_created: number
questions_linked: number
}
export async function generateQuestions(
objId: string,
prompt: string,
token: string
): Promise<{ ok: boolean; stats: GenerateStats }> {
const res = await fetch(`/api/object/${encodeURIComponent(objId)}/generate_questions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ prompt }),
})
const data = await res.json()
if (!res.ok) throw new Error(data.error || 'Fehler bei Generate')
return data
}
export async function publishQuestions(
objId: string,
token: string
): Promise<{ ok: boolean; published_questions: number; published_words: number }> {
const res = await fetch(`/api/object/${encodeURIComponent(objId)}/publish_questions`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
})
const data = await res.json()
if (!res.ok) throw new Error(data.error || 'Fehler bei Publish')
return data
}
export interface ObjectQuestion {
id: string
question_de: string
answer_de: string
short_answer_de: string | null
distractor_words: string[]
level: number
status: string
}
export interface ObjectWord {
id: string
title_de: string
level: number
status: string
}
export async function getObjectQuestions(objId: string, token: string): Promise<ObjectQuestion[]> {
const res = await fetch(`/api/object/${encodeURIComponent(objId)}/questions`, {
headers: { Authorization: `Bearer ${token}` },
})
const data = await res.json()
if (!res.ok) throw new Error('Fehler beim Laden der Fragen')
return data.data as ObjectQuestion[]
}
export async function getObjectWords(objId: string, token: string): Promise<ObjectWord[]> {
const res = await fetch(`/api/object/${encodeURIComponent(objId)}/words`, {
headers: { Authorization: `Bearer ${token}` },
})
const data = await res.json()
if (!res.ok) throw new Error('Fehler beim Laden der Wörter')
return data.data as ObjectWord[]
}
export async function deleteQuestion(qId: string, token: string): Promise<void> {
const res = await fetch(`/api/question/${encodeURIComponent(qId)}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${token}` },
})
if (!res.ok) throw new Error('Fehler beim Löschen der Frage')
}
export async function deleteWord(wId: string, token: string): Promise<void> {
const res = await fetch(`/api/word/${encodeURIComponent(wId)}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${token}` },
})
if (!res.ok) throw new Error('Fehler beim Löschen des Worts')
}
export async function getPictureWords(pictureId: string, token: string): Promise<PictureWord[]> {
const res = await fetch(`/api/directus/pictures/${pictureId}/words`, {
headers: { Authorization: `Bearer ${token}` },
})
const data = await res.json()
if (!res.ok) throw new Error('Fehler beim Laden der Bild-Wörter')
return data.data as PictureWord[]
}
export async function savePictureWords(
pictureId: string,
words: { title_de: string; level: number }[],
token: string
): Promise<{ saved: number }> {
const res = await fetch(`/api/directus/pictures/${pictureId}/words`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ words }),
})
const data = await res.json()
if (!res.ok) throw new Error(data.error || 'Fehler beim Speichern der Wörter')
return data
}
export async function purgeOrphans(objId: string, token: string): Promise<{ orphans_removed: number }> {
const res = await fetch(`/api/object/${encodeURIComponent(objId)}/purge-orphans`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
})
const data = await res.json()
if (!res.ok) throw new Error('Fehler beim Bereinigen')
return data
}
export async function purgeAllOrphans(token: string): Promise<{ orphans_removed: number }> {
const res = await fetch('/api/purge-all-orphans', {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
})
const data = await res.json()
if (!res.ok) throw new Error('Fehler beim globalen Bereinigen')
return data
}
// ── DB Pictures ───────────────────────────────────────────────────────────────
import type { DbPicture, DbObject, DbWord, DbPair } from './types'
export async function getDbPictures(token: string, status = 'draft'): Promise<DbPicture[]> {
const res = await fetch(`/api/directus/db-pictures?status=${status}`, {
headers: { Authorization: `Bearer ${token}` },
})
if (!res.ok) throw new Error('Fehler beim Laden der db_pictures')
const data = await res.json()
return data.data as DbPicture[]
}
export async function updateDbPictureStatus(pictureId: string, status: string, token: string): Promise<void> {
const res = await fetch(`/api/directus/db-pictures/${pictureId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ status }),
})
if (!res.ok) throw new Error('Fehler beim Aktualisieren des Bild-Status')
}
// ── DB Objects ────────────────────────────────────────────────────────────────
export async function getDbObjects(pictureId: string, token: string): Promise<DbObject[]> {
const res = await fetch(`/api/directus/db-objects?picture_id=${pictureId}`, {
headers: { Authorization: `Bearer ${token}` },
})
if (!res.ok) throw new Error('Fehler beim Laden der db_objects')
const data = await res.json()
return data.data as DbObject[]
}
export async function createDbObject(payload: {
picture: string
selections: import('./types').Selection[] | null
user_notes: string | null
}, token: string): Promise<DbObject> {
const res = await fetch('/api/directus/db-objects', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ ...payload, status: 'draft' }),
})
const data = await res.json()
if (!res.ok) throw new Error(data.errors?.[0]?.message || 'Fehler beim Erstellen des Objekts')
return data.data as DbObject
}
export async function updateDbObject(
objId: string,
payload: Partial<Pick<DbObject, 'user_notes'>>,
token: string
): Promise<DbObject> {
const res = await fetch(`/api/directus/db-objects/${objId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify(payload),
})
const data = await res.json()
if (!res.ok) throw new Error(data.errors?.[0]?.message || 'Fehler beim Aktualisieren')
return data.data as DbObject
}
export async function deleteDbObject(objId: string, token: string): Promise<void> {
const res = await fetch(`/api/directus/db-objects/${objId}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${token}` },
})
if (!res.ok) throw new Error('Fehler beim Löschen des Objekts')
}
// ── DB Words for a picture ────────────────────────────────────────────────────
export async function getDbPictureWords(pictureId: string, token: string): Promise<DbWord[]> {
const res = await fetch(`/api/directus/db-pictures/${pictureId}/words`, {
headers: { Authorization: `Bearer ${token}` },
})
const data = await res.json()
if (!res.ok) throw new Error('Fehler beim Laden der Wörter')
return data.data as DbWord[]
}
export async function saveDbPictureWords(
pictureId: string,
words: { titel_de: string; level: number }[],
token: string
): Promise<{ saved: number }> {
const res = await fetch(`/api/directus/db-pictures/${pictureId}/words`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ words }),
})
const data = await res.json()
if (!res.ok) throw new Error(data.error || 'Fehler beim Speichern der Wörter')
return data
}
// ── DB Pairs for an object ────────────────────────────────────────────────────
export async function getDbObjectPairs(objectId: string, token: string): Promise<DbPair[]> {
const res = await fetch(`/api/directus/db-objects/${objectId}/pairs`, {
headers: { Authorization: `Bearer ${token}` },
})
const data = await res.json()
if (!res.ok) throw new Error('Fehler beim Laden der Pairs')
return data.data as DbPair[]
}
export async function createDbPair(
objectId: string,
payload: {
question_de?: string
statement_de: string
level: number
words: { titel_de: string; level: number; link_to: 'question' | 'statement' | 'both' }[]
},
token: string
): Promise<{ ok: boolean; pair_id: string; statement_id: string; question_id: string | null }> {
const res = await fetch(`/api/directus/db-objects/${objectId}/pairs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify(payload),
})
const data = await res.json()
if (!res.ok) throw new Error(data.error || 'Fehler beim Erstellen des Pairs')
return data
}
export async function getDbObjectWords(objectId: string, token: string): Promise<DbWord[]> {
const res = await fetch(`/api/directus/db-objects/${objectId}/words`, {
headers: { Authorization: `Bearer ${token}` },
})
const data = await res.json()
if (!res.ok) throw new Error('Fehler beim Laden der Objekt-Wörter')
return data.data as DbWord[]
}
export async function saveDbObjectWord(
objectId: string,
word: { titel_de: string; level: number } | null,
token: string
) {
const res = await fetch(`/api/directus/db-objects/${objectId}/words`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: token },
body: JSON.stringify(word ? { titel_de: word.titel_de, level: word.level } : { titel_de: '' }),
})
return res.json()
}
export async function updateDbPair(
pairId: string,
payload: {
level?: number
question_de?: string
statement_de?: string
words_add?: { titel_de: string; level: number; link_to: 'question' | 'statement' | 'both' }[]
words_remove?: { junction_id: number; link_to: 'question' | 'statement' | 'both' }[]
},
token: string
): Promise<void> {
const res = await fetch(`/api/directus/db-pairs/${pairId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify(payload),
})
if (!res.ok) throw new Error('Fehler beim Aktualisieren des Pairs')
}
export async function deleteDbPair(pairId: string, token: string): Promise<void> {
const res = await fetch(`/api/directus/db-pairs/${pairId}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${token}` },
})
if (!res.ok) throw new Error('Fehler beim Löschen des Pairs')
}