import { useState, useEffect, useRef } from 'react' import DrawCanvas, { type DrawCanvasHandle } from '../components/DrawCanvas' import GenerateObjectsList from '../components/GenerateObjectsList' import Topbar from '../components/Topbar' import { getDirectusPictures, directusAssetUrl, type DirectusPicture, getDirectusObjects, generateQuestions, publishQuestions, getObjectQuestions, getObjectWords, deleteQuestion, deleteWord, type GenerateStats, type ObjectQuestion, type ObjectWord, } from '../api' import { useAuth } from '../context/AuthContext' import type { DirectusObject, CanvasObject } from '../types' const ChevronLeftIcon = () => ( ) const ChevronRightIcon = () => ( ) const GenerateIcon = () => ( ) const PublishIcon = () => ( ) const SaveIcon = () => ( ) // ── Prompt layout system ────────────────────────────────────────────────────── const DEFAULT_PROMPT = `Du bist ein erfahrener Sprachlernexperte. Du erhältst die Beschreibung eines Objekts aus einem Bild (Titel, Position, Zustand, Aktion) sowie ggf. dessen Elternobjekt als Kontext. Deine Aufgabe: Erstelle ausschließlich für das genannte Objekt (nicht für das Elternobjekt) Sprachlernfragen auf 10 Niveaustufen (1–10): * Stufe 1–2 (Anfänger): Einfachste Erkennungs- oder Ja/Nein-Fragen, z.B. „Kannst du den Hund sehen?" * Stufe 3–5 (Grundstufe): Beschreibende Fragen zu Farbe, Form, Position * Stufe 6–8 (Mittelstufe): Fragen zu Funktion, Vergleich oder Kontext * Stufe 9–10 (Fortgeschritten): Sprachlich anspruchsvolle, kreative oder erklärende Fragen – die Komplexität liegt in Grammatik, Wortschatz und Satzbau, nicht im abstrakten Denken Regeln: * Jede Frage muss sich direkt auf das Objekt beziehen * \`words\`: Enthält alle einzigartigen Tokens aus Frage UND Antwort zusammen (Satzzeichen ausgenommen, keine Duplikate) * \`short_answer\`: Ein einzelnes treffendes Wort als Kurzantwort (z.B. „Ja", „schwarz", „wendig") * \`distractor_words\`: Genau 5 Wörter, die thematisch passen, aber NICHT in Frage oder Antwort vorkommen und NICHT die Antwort sind * Gib ausschließlich valides JSON aus – kein Text, kein Markdown Ausgabeformat: { "levels": [ { "level": 1, "question": "Kannst du den Hund sehen?", "answer": "Ja, ich kann den Hund sehen.", "short_answer": "Ja", "words": ["Kannst", "du", "den", "Hund", "sehen", "Ja", "ich", "kann"], "distractor_words": ["Nein", "vielleicht", "Katze", "hören", "groß"] } ] } Informationen: {user-notes_object} Elternobjekt: {user-notes_parentobject}` interface PromptLayout { name: string prompt: string } const STORAGE_KEY = 'cm_prompt_layouts' function loadLayouts(): PromptLayout[] { const defaultLayout: PromptLayout = { name: 'Standard', prompt: DEFAULT_PROMPT } try { const stored = localStorage.getItem(STORAGE_KEY) if (!stored) return [defaultLayout] const parsed = JSON.parse(stored) as PromptLayout[] const hasDefault = parsed.some(l => l.name === 'Standard') return hasDefault ? parsed : [defaultLayout, ...parsed] } catch { return [defaultLayout] } } function persistLayouts(layouts: PromptLayout[]): void { localStorage.setItem(STORAGE_KEY, JSON.stringify(layouts)) } // ── Component ───────────────────────────────────────────────────────────────── export default function GenerateIt() { const { token } = useAuth() const canvasRef = useRef(null) const [pictureList, setPictureList] = useState([]) const [currentIndex, setCurrentIndex] = useState(-1) const [directusObjects, setDirectusObjects] = useState([]) const [selectedObjId, setSelectedObjId] = useState(null) const [isGenerating, setIsGenerating] = useState(false) const [isPublishing, setIsPublishing] = useState(false) const [generateResult, setGenerateResult] = useState(null) const [generateError, setGenerateError] = useState(null) const [publishResult, setPublishResult] = useState<{ q: number; w: number } | null>(null) const [questions, setQuestions] = useState([]) const [objWords, setObjWords] = useState([]) // Prompt layouts const [layouts, setLayouts] = useState(loadLayouts) const [selectedLayoutName, setSelectedLayoutName] = useState('Standard') const [promptText, setPromptText] = useState(() => loadLayouts()[0]?.prompt ?? DEFAULT_PROMPT) const [showSaveDialog, setShowSaveDialog] = useState(false) const [newLayoutName, setNewLayoutName] = useState('') const [promptOpen, setPromptOpen] = useState(false) const currentPicture: DirectusPicture | null = currentIndex >= 0 && currentIndex < pictureList.length ? pictureList[currentIndex] : null const canvasObjects: CanvasObject[] = directusObjects.map((obj, i) => ({ id: obj.id, visible: true, selections: obj.selections, index: i + 1, hierarchy: 1, })) useEffect(() => { if (!token) return getDirectusPictures(token, 'drawing_created') .then(pics => { setPictureList(pics); setCurrentIndex(pics.length > 0 ? 0 : -1) }) .catch(console.error) }, [token]) useEffect(() => { if (!currentPicture || !token) { setDirectusObjects([]); setSelectedObjId(null) setGenerateResult(null); setPublishResult(null); setGenerateError(null) setQuestions([]); setObjWords([]) return } getDirectusObjects(currentPicture.id, token) .then(objs => { setDirectusObjects(objs) if (objs.length > 0) setSelectedObjId(objs[0].id) else setSelectedObjId(null) }) .catch(console.error) }, [currentPicture?.id, token]) useEffect(() => { if (!selectedObjId || !token) { setQuestions([]); setObjWords([]); return } getObjectQuestions(selectedObjId, token).then(setQuestions).catch(console.error) getObjectWords(selectedObjId, token).then(setObjWords).catch(console.error) }, [selectedObjId, token]) const reloadQW = (objId: string) => { if (!token) return getObjectQuestions(objId, token).then(setQuestions).catch(console.error) getObjectWords(objId, token).then(setObjWords).catch(console.error) } const handleDeleteQuestion = async (qId: string) => { if (!token) return await deleteQuestion(qId, token) setQuestions(qs => qs.filter(q => q.id !== qId)) } const handleDeleteWord = async (wId: string) => { if (!token) return await deleteWord(wId, token) setObjWords(ws => ws.filter(w => w.id !== wId)) } const handleGenerate = async () => { if (!selectedObjId || !token) return setIsGenerating(true) setGenerateResult(null) setGenerateError(null) setPublishResult(null) try { const res = await generateQuestions(selectedObjId, promptText, token) setGenerateResult(res.stats) reloadQW(selectedObjId) } catch (e) { setGenerateError(e instanceof Error ? e.message : 'Fehler beim Generieren') } finally { setIsGenerating(false) } } const handlePublish = async () => { if (!selectedObjId || !token) return setIsPublishing(true) setPublishResult(null) try { const res = await publishQuestions(selectedObjId, token) setPublishResult({ q: res.published_questions, w: res.published_words }) } catch (e) { setGenerateError(e instanceof Error ? e.message : 'Fehler beim Veröffentlichen') } finally { setIsPublishing(false) } } // Layout handlers const handleSelectLayout = (name: string) => { const layout = layouts.find(l => l.name === name) if (layout) { setSelectedLayoutName(name); setPromptText(layout.prompt) } } const handleSaveLayout = () => { const trimmed = newLayoutName.trim() if (!trimmed) return const newLayout: PromptLayout = { name: trimmed, prompt: promptText } const updated = [...layouts.filter(l => l.name !== trimmed), newLayout] setLayouts(updated) persistLayouts(updated) setSelectedLayoutName(trimmed) setNewLayoutName('') setShowSaveDialog(false) } const handleDeleteLayout = (name: string) => { if (name === 'Standard') return const updated = layouts.filter(l => l.name !== name) setLayouts(updated) persistLayouts(updated) if (selectedLayoutName === name) { const first = updated[0] setSelectedLayoutName(first.name) setPromptText(first.prompt) } } const imageNav = (
{pictureList.length > 0 ? ( <> {currentIndex + 1} / {pictureList.length} ) : ( Keine Bilder )}
{generateResult && !isGenerating && ( ✓ {generateResult.questions_created}F +{generateResult.words_created}W neu )} {publishResult && ( ↑ {publishResult.q}F {publishResult.w}W )} {generateError && ( {generateError} )}
) return (
{/* Left: Objects */} {/* Center: Canvas + Prompt Bar */}
{}} readOnly />
{/* Collapsible Prompt Bar */}
setPromptOpen(o => !o)}> {promptOpen ? '▾' : '▸'} Prompt {selectedLayoutName}
e.stopPropagation()}> {selectedLayoutName !== 'Standard' && ( )}
{promptOpen && (
{showSaveDialog && (
setNewLayoutName(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') handleSaveLayout(); if (e.key === 'Escape') setShowSaveDialog(false) }} autoFocus />
)}