- Rechte Sidebar in zwei Frames aufgeteilt: Objects (bisherig) + Words (neu) - Words-Frame: Wörter + Level (1–100) per Bild anlegen, dedupliziert via words_pictures Junction - Pending-Words in Primary-Farbe mit inline Level-Edit, gespeicherte Words in neutralem Grau - Save-Button speichert alle pending Words nach Directus (status=draft, title_de, level, picture-Link) - Automatisches Laden der Bild-Words bei Bildwechsel - Backend: GET/POST /api/directus/pictures/<pic_id>/words (words_pictures Junction, _find_or_create_word) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
328 lines
11 KiB
TypeScript
328 lines
11 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
|
|
}
|