Fragen & Wörter nach Generate it anzeigen und löschen können
- Neue Endpoints: GET /api/object/<id>/questions+words, DELETE /api/question/<id>, DELETE /api/word/<id> - GenerateIt: Wörter-Sidebar mit ×-Chips, Fragen-Sidebar mit Level-Badge und × - Laden beim Objekt-Wechsel und nach Generate it Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
50
app.py
50
app.py
@@ -1164,6 +1164,56 @@ def publish_questions(obj_id: str):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/object/<obj_id>/questions", methods=["GET"])
|
||||||
|
def get_object_questions_list(obj_id: str):
|
||||||
|
"""Gibt alle verknüpften Fragen eines Objekts zurück."""
|
||||||
|
token = request.headers.get("Authorization", "")
|
||||||
|
fields = "questions_id.id,questions_id.question_de,questions_id.answer_de,questions_id.level,questions_id.status"
|
||||||
|
data, _ = _directus(
|
||||||
|
"GET",
|
||||||
|
f"/items/questions_objects?filter[objects_id][_eq]={obj_id}&fields={fields}&limit=200",
|
||||||
|
token,
|
||||||
|
)
|
||||||
|
items = [e["questions_id"] for e in (data.get("data") or []) if e.get("questions_id")]
|
||||||
|
items.sort(key=lambda x: x.get("level") or 0)
|
||||||
|
return jsonify({"data": items})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/object/<obj_id>/words", methods=["GET"])
|
||||||
|
def get_object_words_list(obj_id: str):
|
||||||
|
"""Gibt alle verknüpften Wörter eines Objekts zurück."""
|
||||||
|
token = request.headers.get("Authorization", "")
|
||||||
|
fields = "words_id.id,words_id.title_de,words_id.level,words_id.status"
|
||||||
|
data, _ = _directus(
|
||||||
|
"GET",
|
||||||
|
f"/items/words_objects?filter[objects_id][_eq]={obj_id}&fields={fields}&limit=2000",
|
||||||
|
token,
|
||||||
|
)
|
||||||
|
items = [e["words_id"] for e in (data.get("data") or []) if e.get("words_id")]
|
||||||
|
items.sort(key=lambda x: x.get("title_de") or "")
|
||||||
|
return jsonify({"data": items})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/question/<q_id>", methods=["DELETE"])
|
||||||
|
def delete_question_item(q_id: str):
|
||||||
|
"""Löscht eine Frage aus Directus."""
|
||||||
|
token = request.headers.get("Authorization", "")
|
||||||
|
_, status = _directus("DELETE", f"/items/questions/{q_id}", token)
|
||||||
|
if status in (200, 204):
|
||||||
|
return jsonify({"ok": True})
|
||||||
|
return jsonify({"error": "Delete failed"}), status
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/word/<w_id>", methods=["DELETE"])
|
||||||
|
def delete_word_item(w_id: str):
|
||||||
|
"""Löscht ein Wort aus Directus."""
|
||||||
|
token = request.headers.get("Authorization", "")
|
||||||
|
_, status = _directus("DELETE", f"/items/words/{w_id}", token)
|
||||||
|
if status in (200, 204):
|
||||||
|
return jsonify({"ok": True})
|
||||||
|
return jsonify({"error": "Delete failed"}), status
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||||
|
|
||||||
|
|||||||
@@ -230,3 +230,52 @@ export async function publishQuestions(
|
|||||||
if (!res.ok) throw new Error(data.error || 'Fehler bei Publish')
|
if (!res.ok) throw new Error(data.error || 'Fehler bei Publish')
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ObjectQuestion {
|
||||||
|
id: string
|
||||||
|
question_de: string
|
||||||
|
answer_de: 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')
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,7 +10,13 @@ import {
|
|||||||
getDirectusObjects,
|
getDirectusObjects,
|
||||||
generateQuestions,
|
generateQuestions,
|
||||||
publishQuestions,
|
publishQuestions,
|
||||||
|
getObjectQuestions,
|
||||||
|
getObjectWords,
|
||||||
|
deleteQuestion,
|
||||||
|
deleteWord,
|
||||||
type GenerateStats,
|
type GenerateStats,
|
||||||
|
type ObjectQuestion,
|
||||||
|
type ObjectWord,
|
||||||
} 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'
|
||||||
@@ -121,6 +127,8 @@ export default function GenerateIt() {
|
|||||||
const [generateResult, setGenerateResult] = useState<GenerateStats | null>(null)
|
const [generateResult, setGenerateResult] = useState<GenerateStats | null>(null)
|
||||||
const [generateError, setGenerateError] = useState<string | null>(null)
|
const [generateError, setGenerateError] = useState<string | null>(null)
|
||||||
const [publishResult, setPublishResult] = useState<{ q: number; w: number } | null>(null)
|
const [publishResult, setPublishResult] = useState<{ q: number; w: number } | null>(null)
|
||||||
|
const [questions, setQuestions] = useState<ObjectQuestion[]>([])
|
||||||
|
const [objWords, setObjWords] = useState<ObjectWord[]>([])
|
||||||
|
|
||||||
// Prompt layouts
|
// Prompt layouts
|
||||||
const [layouts, setLayouts] = useState<PromptLayout[]>(loadLayouts)
|
const [layouts, setLayouts] = useState<PromptLayout[]>(loadLayouts)
|
||||||
@@ -152,6 +160,7 @@ export default function GenerateIt() {
|
|||||||
if (!currentPicture || !token) {
|
if (!currentPicture || !token) {
|
||||||
setDirectusObjects([]); setSelectedObjId(null)
|
setDirectusObjects([]); setSelectedObjId(null)
|
||||||
setGenerateResult(null); setPublishResult(null); setGenerateError(null)
|
setGenerateResult(null); setPublishResult(null); setGenerateError(null)
|
||||||
|
setQuestions([]); setObjWords([])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
getDirectusObjects(currentPicture.id, token)
|
getDirectusObjects(currentPicture.id, token)
|
||||||
@@ -163,6 +172,30 @@ export default function GenerateIt() {
|
|||||||
.catch(console.error)
|
.catch(console.error)
|
||||||
}, [currentPicture?.id, token])
|
}, [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 () => {
|
const handleGenerate = async () => {
|
||||||
if (!selectedObjId || !token) return
|
if (!selectedObjId || !token) return
|
||||||
setIsGenerating(true)
|
setIsGenerating(true)
|
||||||
@@ -172,6 +205,7 @@ export default function GenerateIt() {
|
|||||||
try {
|
try {
|
||||||
const res = await generateQuestions(selectedObjId, promptText, token)
|
const res = await generateQuestions(selectedObjId, promptText, token)
|
||||||
setGenerateResult(res.stats)
|
setGenerateResult(res.stats)
|
||||||
|
reloadQW(selectedObjId)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setGenerateError(e instanceof Error ? e.message : 'Fehler beim Generieren')
|
setGenerateError(e instanceof Error ? e.message : 'Fehler beim Generieren')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -382,24 +416,60 @@ export default function GenerateIt() {
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* Right: Generate results */}
|
{/* Words sidebar */}
|
||||||
<aside className="sidebar sidebar--right">
|
<aside className="sidebar sidebar--words">
|
||||||
<div className="sidebar-panel" style={{ flex: 1 }}>
|
<div className="sidebar-panel" style={{ flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
|
||||||
<h3 className="sidebar-heading">Ergebnis</h3>
|
<h3 className="sidebar-heading">
|
||||||
{generateResult ? (
|
Wörter
|
||||||
<div style={{ padding: '8px 12px', fontSize: 13, display: 'flex', flexDirection: 'column', gap: 6 }}>
|
{objWords.length > 0 && <span className="badge">{objWords.length}</span>}
|
||||||
<div><strong>{generateResult.questions_created}</strong> Fragen erstellt</div>
|
</h3>
|
||||||
<div><strong>{generateResult.questions_linked}</strong> Fragen verknüpft</div>
|
{objWords.length === 0 ? (
|
||||||
<div><strong>{generateResult.words_created}</strong> Wörter erstellt</div>
|
<div className="empty-state">–</div>
|
||||||
<div><strong>{generateResult.words_linked}</strong> Wörter verknüpft</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="empty-state">Klicke „Generate it".</div>
|
<div style={{ overflowY: 'auto', flex: 1, padding: '4px 8px', display: 'flex', flexWrap: 'wrap', gap: 4, alignContent: 'flex-start' }}>
|
||||||
|
{objWords.map(w => (
|
||||||
|
<span key={w.id} className="word-chip" style={{ display: 'inline-flex', alignItems: 'center', gap: 3 }}>
|
||||||
|
{w.title_de}
|
||||||
|
<button
|
||||||
|
onClick={() => handleDeleteWord(w.id)}
|
||||||
|
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 0, lineHeight: 1, color: 'var(--muted)', fontSize: 10, marginLeft: 1 }}
|
||||||
|
title="Löschen"
|
||||||
|
>✕</button>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{publishResult && (
|
</div>
|
||||||
<div style={{ padding: '0 12px 8px', fontSize: 13, display: 'flex', flexDirection: 'column', gap: 6, color: 'var(--primary)' }}>
|
</aside>
|
||||||
<div>↑ {publishResult.q} Fragen veröffentlicht</div>
|
|
||||||
<div>↑ {publishResult.w} Wörter veröffentlicht</div>
|
{/* Questions sidebar */}
|
||||||
|
<aside className="sidebar sidebar--right" style={{ width: 280, minWidth: 220 }}>
|
||||||
|
<div className="sidebar-panel" style={{ flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<h3 className="sidebar-heading">
|
||||||
|
Fragen
|
||||||
|
{questions.length > 0 && <span className="badge">{questions.length}</span>}
|
||||||
|
</h3>
|
||||||
|
{questions.length === 0 ? (
|
||||||
|
<div className="empty-state">Klicke „Generate it".</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ overflowY: 'auto', flex: 1 }}>
|
||||||
|
{questions.map(q => (
|
||||||
|
<div key={q.id} style={{ padding: '6px 10px', borderBottom: '1px solid var(--border)', fontSize: 12 }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 4 }}>
|
||||||
|
<span style={{ fontWeight: 600, color: 'var(--muted)', fontSize: 10, flexShrink: 0 }}>
|
||||||
|
L{q.level}
|
||||||
|
{q.status === 'published' && <span style={{ marginLeft: 4, color: 'var(--success)' }}>↑</span>}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDeleteQuestion(q.id)}
|
||||||
|
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 0, lineHeight: 1, color: 'var(--muted)', fontSize: 11, flexShrink: 0 }}
|
||||||
|
title="Frage löschen"
|
||||||
|
>✕</button>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: 2, color: 'var(--fg)' }}>{q.question_de}</div>
|
||||||
|
<div style={{ marginTop: 2, color: 'var(--muted)', fontStyle: 'italic' }}>{q.answer_de}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user