Generate it / Publish it: Claude Haiku integration + Generate page redesign

- GenerateIt page: objects sidebar, readOnly canvas, collapsible prompt bar
- Generate it: calls Claude Haiku, saves questions/words to Directus as draft
- Publish it: promotes draft questions/words to published
- Deduplication: links existing words/questions instead of duplicating
- GenerateObjectsList: tree view with user_notes labels
- DrawCanvas: readOnly prop to disable mouse interaction
- api.ts: generateQuestions + publishQuestions endpoints
- requirements.txt: anthropic==0.40.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-26 10:16:28 +02:00
parent 99a8d7e0aa
commit 0360bcd1e6
5 changed files with 436 additions and 80 deletions

View File

@@ -25,7 +25,28 @@
"Bash(docker build *)", "Bash(docker build *)",
"mcp__coolify__private-keys", "mcp__coolify__private-keys",
"Bash(pkill -f \"python app.py\")", "Bash(pkill -f \"python app.py\")",
"Bash(curl -s http://localhost:8000/)" "Bash(curl -s http://localhost:8000/)",
"mcp__coolify__applications",
"Bash(ssh-keygen -t ed25519 -C \"coolify-content-mentor\" -f /tmp/coolify_content_mentor -N \"\" -q)",
"Bash(ssh-keygen -lf /tmp/coolify_content_mentor.pub)",
"Bash(python3 -c ' *)",
"Bash(ssh -i /tmp/coolify_content_mentor -o StrictHostKeyChecking=no -o ConnectTimeout=10 -T git@git.hyggecraftery.com)",
"Bash(ssh -i /tmp/coolify_content_mentor -o StrictHostKeyChecking=no -o ConnectTimeout=10 -vvv git@git.hyggecraftery.com)",
"Bash(nc -zv git.hyggecraftery.com 2222)",
"Bash(nc -zv git.hyggecraftery.com 3000)",
"mcp__coolify__deployments",
"Bash(git add *)",
"Bash(git commit -m ' *)",
"Bash(git push *)",
"mcp__directus__read-items",
"mcp__directus__system-prompt",
"Read(//Users/tim/.claude/**)",
"Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\(json.dumps\\(d.get\\('mcpServers',{}\\).get\\('directus',{}\\), indent=2\\)\\)\")",
"Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); mcps=d.get\\('mcpServers',{}\\); print\\(json.dumps\\(mcps.get\\('directus', mcps.get\\('mcp__directus',{}\\)\\), indent=2\\)\\)\")",
"mcp__directus__read-fields",
"mcp__directus__create-field",
"mcp__directus__update-field",
"mcp__directus__read-collections"
] ]
} }
} }

295
app.py
View File

@@ -2,13 +2,18 @@ from pathlib import Path
from datetime import datetime from datetime import datetime
from uuid import uuid4 from uuid import uuid4
import json import json
import os
import urllib.request import urllib.request
import urllib.error import urllib.error
import urllib.parse
from flask import Flask, send_from_directory, request, jsonify from flask import Flask, send_from_directory, request, jsonify
from flask_cors import CORS from flask_cors import CORS
from PIL import Image from PIL import Image
import ollama import ollama
import anthropic as _anthropic_sdk
ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY", "")
DIRECTUS_URL = "https://db.hejyou.com" DIRECTUS_URL = "https://db.hejyou.com"
@@ -878,8 +883,294 @@ def crop_image():
) )
# ── Claude / Directus Helpers ─────────────────────────────────────────────────
def _ensure_junction(collection: str, field1: str, field2: str, token: str):
"""Create a simple M2M junction collection in Directus if it doesn't exist."""
_, status = _directus("GET", f"/collections/{collection}", token)
if status == 200:
return
body = {
"collection": collection,
"schema": {},
"meta": {"hidden": True, "icon": "import_export"},
"fields": [
{
"field": "id",
"type": "integer",
"schema": {"is_primary_key": True, "has_auto_increment": True},
"meta": {"hidden": True},
},
{"field": field1, "type": "uuid", "schema": {}, "meta": {"hidden": True}},
{"field": field2, "type": "uuid", "schema": {}, "meta": {"hidden": True}},
],
}
_directus("POST", "/collections", token, body)
def _ensure_link(collection: str, match: dict, payload: dict, token: str):
"""Insert a junction row only if it doesn't already exist."""
qs = "&".join(
f"filter[{k}][_eq]={urllib.parse.quote(str(v), safe='')}"
for k, v in match.items()
)
data, status = _directus("GET", f"/items/{collection}?{qs}&limit=1", token)
if status == 200 and data.get("data"):
return # already linked
_directus("POST", f"/items/{collection}", token, payload)
def _find_or_create_word(title_de: str, level: int, token: str):
"""Return (word_id, is_new). Creates word with status=draft if missing."""
enc = urllib.parse.quote(title_de, safe="")
data, status = _directus(
"GET", f"/items/words?filter[title_de][_eq]={enc}&fields=id&limit=1", token
)
if status == 200 and data.get("data"):
return data["data"][0]["id"], False
body = {"status": "draft", "title_de": title_de, "level": level}
data, status = _directus("POST", "/items/words", token, body)
if status in (200, 201):
return data["data"]["id"], True
raise RuntimeError(f"Word creation failed ({status}): {data}")
def _find_or_create_question(question_de: str, answer_de: str, level: int,
short_answer_id, obj_id: str, token: str):
"""Return (question_id, is_new). Creates question with status=draft if missing."""
enc = urllib.parse.quote(question_de, safe="")
data, status = _directus(
"GET",
f"/items/questions?filter[question_de][_eq]={enc}&fields=id&limit=1",
token,
)
if status == 200 and data.get("data"):
return data["data"][0]["id"], False
body = {
"status": "draft",
"question_de": question_de,
"answer_de": answer_de,
"level": level,
"object": obj_id,
}
if short_answer_id:
body["short_answer"] = short_answer_id
data, status = _directus("POST", "/items/questions", token, body)
if status in (200, 201):
return data["data"]["id"], True
raise RuntimeError(f"Question creation failed ({status}): {data}")
# ── Generate & Publish endpoints ──────────────────────────────────────────────
@app.route("/api/object/<obj_id>/generate_questions", methods=["POST"])
def generate_questions(obj_id: str):
"""
1. Holt Objekt + Elternobjekt aus Directus
2. Füllt Prompt-Platzhalter
3. Ruft Claude Haiku auf
4. Speichert Wörter + Fragen als Entwurf in Directus
"""
token = request.headers.get("Authorization", "")
body = request.get_json(force=True, silent=True) or {}
prompt_template = (body.get("prompt") or "").strip()
if not prompt_template:
return jsonify({"error": "Missing prompt"}), 400
if not ANTHROPIC_API_KEY:
return jsonify({"error": "ANTHROPIC_API_KEY not configured on server"}), 500
# Objekt laden
obj_resp, s = _directus("GET", f"/items/objects/{obj_id}?fields=id,user_notes,parent", token)
if s != 200:
return jsonify({"error": "Object not found"}), 404
obj = obj_resp.get("data") or {}
# Elternobjekt laden
parent_notes = ""
if obj.get("parent"):
p_resp, _ = _directus("GET", f"/items/objects/{obj['parent']}?fields=id,user_notes", token)
parent_notes = ((p_resp.get("data") or {}).get("user_notes") or "")
# Platzhalter ersetzen
prompt = (
prompt_template
.replace("{user-notes_object}", obj.get("user_notes") or "")
.replace("{user-notes_parentobject}", parent_notes)
)
# Claude Haiku aufrufen
try:
client = _anthropic_sdk.Anthropic(api_key=ANTHROPIC_API_KEY)
msg = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=4096,
messages=[{"role": "user", "content": prompt}],
)
raw = msg.content[0].text
except Exception as e:
return jsonify({"error": f"Claude API error: {e}"}), 500
# JSON parsen
try:
ai = json.loads(raw)
except Exception:
try:
start = raw.index("{")
end = raw.rindex("}") + 1
ai = json.loads(raw[start:end])
except Exception:
return jsonify({"error": "Invalid JSON from AI", "raw": raw[:400]}), 500
levels = ai.get("levels", [])
if not levels:
return jsonify({"error": "No levels in AI response"}), 500
# Junction-Collections sicherstellen
for col, f1, f2 in [
("words_objects", "words_id", "objects_id"),
("questions_objects", "questions_id", "objects_id"),
("questions_distractor_words", "questions_id", "words_id"),
]:
_ensure_junction(col, f1, f2, token)
stats = {
"words_created": 0,
"words_linked": 0,
"questions_created": 0,
"questions_linked": 0,
}
for lvl in levels:
level = int(lvl.get("level") or 1)
q_de = (lvl.get("question") or "").strip()
a_de = (lvl.get("answer") or "").strip()
short_text = (lvl.get("short_answer") or "").strip()
words_list = [str(w).strip() for w in lvl.get("words", []) if str(w).strip()]
distractor_list = [str(w).strip() for w in lvl.get("distractor_words", []) if str(w).strip()]
if not q_de:
continue
# Kurzantwort-Wort
short_answer_id = None
if short_text:
try:
wid, is_new = _find_or_create_word(short_text, level, token)
short_answer_id = wid
_ensure_link(
"words_objects",
{"words_id": wid, "objects_id": obj_id},
{"words_id": wid, "objects_id": obj_id},
token,
)
stats["words_created" if is_new else "words_linked"] += 1
except Exception as e:
print(f"[generate_questions] short_answer error: {e}")
# Alle einzigartigen Wörter verarbeiten
all_unique = list(dict.fromkeys(words_list + distractor_list))
word_map: dict[str, str] = {} # title_de → id
for w in all_unique:
try:
wid, is_new = _find_or_create_word(w, level, token)
word_map[w] = wid
_ensure_link(
"words_objects",
{"words_id": wid, "objects_id": obj_id},
{"words_id": wid, "objects_id": obj_id},
token,
)
stats["words_created" if is_new else "words_linked"] += 1
except Exception as e:
print(f"[generate_questions] word error '{w}': {e}")
# Frage anlegen / verknüpfen
try:
q_id, q_is_new = _find_or_create_question(q_de, a_de, level, short_answer_id, obj_id, token)
except Exception as e:
print(f"[generate_questions] question error level {level}: {e}")
continue
stats["questions_created" if q_is_new else "questions_linked"] += 1
# Frage ↔ Objekt
_ensure_link(
"questions_objects",
{"questions_id": q_id, "objects_id": obj_id},
{"questions_id": q_id, "objects_id": obj_id},
token,
)
# related_words (bestehende Junction questions_words)
for w in words_list:
if w in word_map:
_ensure_link(
"questions_words",
{"questions_id": q_id, "words_id": word_map[w]},
{"questions_id": q_id, "words_id": word_map[w]},
token,
)
# distractor_words
for w in distractor_list:
if w in word_map:
_ensure_link(
"questions_distractor_words",
{"questions_id": q_id, "words_id": word_map[w]},
{"questions_id": q_id, "words_id": word_map[w]},
token,
)
print(f"[generate_questions] obj={obj_id} stats={stats}")
return jsonify({"ok": True, "object_id": obj_id, "stats": stats})
@app.route("/api/object/<obj_id>/publish_questions", methods=["POST"])
def publish_questions(obj_id: str):
"""Ändert Status aller verknüpften Entwurfs-Fragen und -Wörter auf 'published'."""
token = request.headers.get("Authorization", "")
# Verknüpfte Fragen
q_resp, _ = _directus(
"GET",
f"/items/questions_objects?filter[objects_id][_eq]={obj_id}&fields=questions_id&limit=200",
token,
)
q_ids = [e["questions_id"] for e in (q_resp.get("data") or [])]
# Verknüpfte Wörter
w_resp, _ = _directus(
"GET",
f"/items/words_objects?filter[objects_id][_eq]={obj_id}&fields=words_id&limit=2000",
token,
)
w_ids = [e["words_id"] for e in (w_resp.get("data") or [])]
published_q = 0
published_w = 0
for q_id in q_ids:
_, s = _directus("PATCH", f"/items/questions/{q_id}", token, {"status": "published"})
if s == 200:
published_q += 1
for w_id in w_ids:
_, s = _directus("PATCH", f"/items/words/{w_id}", token, {"status": "published"})
if s == 200:
published_w += 1
return jsonify({
"ok": True,
"published_questions": published_q,
"published_words": published_w,
})
if __name__ == "__main__": if __name__ == "__main__":
# Du kannst den Port hier anpassen, z.B. 8000 oder 5000.
# Für externen Zugriff später einfach in Nginx/Reverse Proxy weiterleiten.
app.run(host="0.0.0.0", port=8000, debug=True) app.run(host="0.0.0.0", port=8000, debug=True)

View File

@@ -195,3 +195,38 @@ export async function generateSentence(
if (!res.ok) throw new Error(data.error || 'Fehler bei KI-Sentence') if (!res.ok) throw new Error(data.error || 'Fehler bei KI-Sentence')
return data 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
}

View File

@@ -1,20 +1,19 @@
import { useState, useEffect, useRef } from 'react' import { useState, useEffect, useRef } from 'react'
import DrawCanvas, { type DrawCanvasHandle } from '../components/DrawCanvas' import DrawCanvas, { type DrawCanvasHandle } from '../components/DrawCanvas'
import GenerateObjectsList from '../components/GenerateObjectsList' import GenerateObjectsList from '../components/GenerateObjectsList'
import SentencesList from '../components/SentencesList'
import Topbar from '../components/Topbar' import Topbar from '../components/Topbar'
import { import {
getDirectusPictures, getDirectusPictures,
directusAssetUrl, directusAssetUrl,
type DirectusPicture, type DirectusPicture,
getDirectusObjects, getDirectusObjects,
generateDetails, generateQuestions,
generateSentence, publishQuestions,
getSentences, type GenerateStats,
} from '../api' } from '../api'
import { useAuth } from '../context/AuthContext' import { useAuth } from '../context/AuthContext'
import type { DirectusObject, CanvasObject } from '../types' import type { DirectusObject, CanvasObject } from '../types'
import type { Sentence } from '../types'
const ChevronLeftIcon = () => ( const ChevronLeftIcon = () => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
@@ -28,15 +27,15 @@ const ChevronRightIcon = () => (
</svg> </svg>
) )
const SparkleIcon = () => ( const GenerateIcon = () => (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 2l2.4 7.4H22l-6.2 4.5 2.4 7.4L12 17l-6.2 4.3 2.4-7.4L2 9.4h7.6z" /> <path d="M12 2l2.4 7.4H22l-6.2 4.5 2.4 7.4L12 17l-6.2 4.3 2.4-7.4L2 9.4h7.6z" />
</svg> </svg>
) )
const ChatIcon = () => ( const PublishIcon = () => (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" /> <path d="M12 19V5M5 12l7-7 7 7" />
</svg> </svg>
) )
@@ -106,18 +105,6 @@ function persistLayouts(layouts: PromptLayout[]): void {
localStorage.setItem(STORAGE_KEY, JSON.stringify(layouts)) localStorage.setItem(STORAGE_KEY, JSON.stringify(layouts))
} }
// ── Word extraction ───────────────────────────────────────────────────────────
function extractWords(sentences: Sentence[]): string[] {
const allText = sentences
.flatMap(s => [s.question_simple_en, s.answer_simple_en, s.question_advanced_en, s.answer_advanced_en])
.join(' ')
const words = allText
.split(/\s+/)
.map(w => w.replace(/[^a-zA-ZäöüÄÖÜßéèêàâùûîô'-]/g, ''))
.filter(w => w.length > 1)
return [...new Set(words)].sort((a, b) => a.localeCompare(b))
}
// ── Component ───────────────────────────────────────────────────────────────── // ── Component ─────────────────────────────────────────────────────────────────
@@ -129,9 +116,11 @@ export default function GenerateIt() {
const [currentIndex, setCurrentIndex] = useState(-1) const [currentIndex, setCurrentIndex] = useState(-1)
const [directusObjects, setDirectusObjects] = useState<DirectusObject[]>([]) const [directusObjects, setDirectusObjects] = useState<DirectusObject[]>([])
const [selectedObjId, setSelectedObjId] = useState<string | null>(null) const [selectedObjId, setSelectedObjId] = useState<string | null>(null)
const [sentences, setSentences] = useState<Sentence[]>([]) const [isGenerating, setIsGenerating] = useState(false)
const [isGeneratingDetails, setIsGeneratingDetails] = useState(false) const [isPublishing, setIsPublishing] = useState(false)
const [isGeneratingSentence, setIsGeneratingSentence] = useState(false) const [generateResult, setGenerateResult] = useState<GenerateStats | null>(null)
const [generateError, setGenerateError] = useState<string | null>(null)
const [publishResult, setPublishResult] = useState<{ q: number; w: number } | null>(null)
// Prompt layouts // Prompt layouts
const [layouts, setLayouts] = useState<PromptLayout[]>(loadLayouts) const [layouts, setLayouts] = useState<PromptLayout[]>(loadLayouts)
@@ -144,8 +133,6 @@ export default function GenerateIt() {
const currentPicture: DirectusPicture | null = const currentPicture: DirectusPicture | null =
currentIndex >= 0 && currentIndex < pictureList.length ? pictureList[currentIndex] : null currentIndex >= 0 && currentIndex < pictureList.length ? pictureList[currentIndex] : null
const words = extractWords(sentences)
const canvasObjects: CanvasObject[] = directusObjects.map((obj, i) => ({ const canvasObjects: CanvasObject[] = directusObjects.map((obj, i) => ({
id: obj.id, id: obj.id,
visible: true, visible: true,
@@ -163,44 +150,46 @@ export default function GenerateIt() {
useEffect(() => { useEffect(() => {
if (!currentPicture || !token) { if (!currentPicture || !token) {
setDirectusObjects([]); setSelectedObjId(null); setSentences([]) setDirectusObjects([]); setSelectedObjId(null)
setGenerateResult(null); setPublishResult(null); setGenerateError(null)
return return
} }
getDirectusObjects(currentPicture.id, token) getDirectusObjects(currentPicture.id, token)
.then(objs => { .then(objs => {
setDirectusObjects(objs) setDirectusObjects(objs)
if (objs.length > 0) { setSelectedObjId(objs[0].id); loadSentences(objs[0].id) } if (objs.length > 0) setSelectedObjId(objs[0].id)
else { setSelectedObjId(null); setSentences([]) } else setSelectedObjId(null)
}) })
.catch(console.error) .catch(console.error)
}, [currentPicture?.id, token]) }, [currentPicture?.id, token])
const loadSentences = async (objId: string) => { const handleGenerate = async () => {
try { setSentences(await getSentences(objId)) } catch (e) { console.error(e) } if (!selectedObjId || !token) return
} setIsGenerating(true)
setGenerateResult(null)
const handleGenerateDetails = async () => { setGenerateError(null)
if (!selectedObjId) return setPublishResult(null)
setIsGeneratingDetails(true)
try { try {
await generateDetails(selectedObjId) const res = await generateQuestions(selectedObjId, promptText, token)
setGenerateResult(res.stats)
} catch (e) { } catch (e) {
alert(e instanceof Error ? e.message : 'Fehler bei KI-Details') setGenerateError(e instanceof Error ? e.message : 'Fehler beim Generieren')
} finally { } finally {
setIsGeneratingDetails(false) setIsGenerating(false)
} }
} }
const handleGenerateSentence = async () => { const handlePublish = async () => {
if (!selectedObjId) return if (!selectedObjId || !token) return
setIsGeneratingSentence(true) setIsPublishing(true)
setPublishResult(null)
try { try {
const data = await generateSentence(selectedObjId) const res = await publishQuestions(selectedObjId, token)
setSentences(prev => [...prev, data.sentence]) setPublishResult({ q: res.published_questions, w: res.published_words })
} catch (e) { } catch (e) {
alert(e instanceof Error ? e.message : 'Fehler bei KI-Sentence') setGenerateError(e instanceof Error ? e.message : 'Fehler beim Veröffentlichen')
} finally { } finally {
setIsGeneratingSentence(false) setIsPublishing(false)
} }
} }
@@ -258,14 +247,40 @@ export default function GenerateIt() {
<div style={{ width: 1, height: 24, background: 'var(--border)' }} /> <div style={{ width: 1, height: 24, background: 'var(--border)' }} />
<button className="btn-ghost btn-sm" onClick={handleGenerateDetails} disabled={isGeneratingDetails || !selectedObjId}> <button
<SparkleIcon /> className="btn-ghost btn-sm"
{isGeneratingDetails ? 'Generiere…' : 'KI-Details'} onClick={handleGenerate}
disabled={isGenerating || isPublishing || !selectedObjId}
style={isGenerating ? { opacity: 0.7 } : undefined}
>
<GenerateIcon />
{isGenerating ? 'Generiere…' : 'Generate it'}
</button> </button>
<button className="btn-ghost btn-sm" onClick={handleGenerateSentence} disabled={isGeneratingSentence || !selectedObjId}> <button
<ChatIcon /> className="btn-ghost btn-sm"
{isGeneratingSentence ? 'Generiere…' : 'KI-Sentence'} onClick={handlePublish}
disabled={isPublishing || isGenerating || !selectedObjId || !generateResult}
title={!generateResult ? 'Erst Generate it ausführen' : undefined}
>
<PublishIcon />
{isPublishing ? 'Veröffentliche…' : 'Publish it'}
</button> </button>
{generateResult && !isGenerating && (
<span style={{ fontSize: 11, color: 'var(--success)', background: 'var(--success-bg)', border: '1px solid var(--success)', borderRadius: 'var(--r-full)', padding: '2px 8px' }}>
{generateResult.questions_created}F +{generateResult.words_created}W neu
</span>
)}
{publishResult && (
<span style={{ fontSize: 11, color: 'var(--primary-muted-fg)', background: 'var(--primary-muted)', border: '1px solid var(--primary)', borderRadius: 'var(--r-full)', padding: '2px 8px' }}>
{publishResult.q}F {publishResult.w}W
</span>
)}
{generateError && (
<span style={{ fontSize: 11, color: 'var(--danger)', background: 'var(--danger-bg)', border: '1px solid var(--danger)', borderRadius: 'var(--r-full)', padding: '2px 8px', maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{generateError}
</span>
)}
</div> </div>
) )
@@ -284,7 +299,7 @@ export default function GenerateIt() {
<GenerateObjectsList <GenerateObjectsList
objects={directusObjects} objects={directusObjects}
selectedId={selectedObjId} selectedId={selectedObjId}
onSelect={id => { setSelectedObjId(id); loadSentences(id) }} onSelect={id => setSelectedObjId(id)}
/> />
</div> </div>
</aside> </aside>
@@ -367,33 +382,26 @@ export default function GenerateIt() {
</div> </div>
</main> </main>
{/* Words */} {/* Right: Generate results */}
<aside className="sidebar sidebar--words">
<div className="sidebar-panel" style={{ flex: 1 }}>
<h3 className="sidebar-heading">
Wörter
{words.length > 0 && <span className="badge">{words.length}</span>}
</h3>
{words.length === 0 ? (
<div className="empty-state">Noch keine Wörter.</div>
) : (
<div className="words-cloud">
{words.map(w => (
<span key={w} className="word-chip">{w}</span>
))}
</div>
)}
</div>
</aside>
{/* Right: Sentences */}
<aside className="sidebar sidebar--right"> <aside className="sidebar sidebar--right">
<div className="sidebar-panel" style={{ flex: 1 }}> <div className="sidebar-panel" style={{ flex: 1 }}>
<h3 className="sidebar-heading"> <h3 className="sidebar-heading">Ergebnis</h3>
Sätze {generateResult ? (
{sentences.length > 0 && <span className="badge">{sentences.length}</span>} <div style={{ padding: '8px 12px', fontSize: 13, display: 'flex', flexDirection: 'column', gap: 6 }}>
</h3> <div><strong>{generateResult.questions_created}</strong> Fragen erstellt</div>
<SentencesList sentences={sentences} /> <div><strong>{generateResult.questions_linked}</strong> Fragen verknüpft</div>
<div><strong>{generateResult.words_created}</strong> Wörter erstellt</div>
<div><strong>{generateResult.words_linked}</strong> Wörter verknüpft</div>
</div>
) : (
<div className="empty-state">Klicke Generate it".</div>
)}
{publishResult && (
<div style={{ padding: '0 12px 8px', fontSize: 13, display: 'flex', flexDirection: 'column', gap: 6, color: 'var(--primary)' }}>
<div> {publishResult.q} Fragen veröffentlicht</div>
<div> {publishResult.w} Wörter veröffentlicht</div>
</div>
)}
</div> </div>
</aside> </aside>
</div> </div>

View File

@@ -3,4 +3,5 @@ Flask-Cors==4.0.1
gunicorn==22.0.0 gunicorn==22.0.0
Pillow==11.0.0 Pillow==11.0.0
ollama==0.3.0 ollama==0.3.0
anthropic==0.40.0