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:
2026-04-26 10:32:22 +02:00
parent 2755632524
commit 0f83210aec
3 changed files with 185 additions and 16 deletions

View File

@@ -230,3 +230,52 @@ export async function publishQuestions(
if (!res.ok) throw new Error(data.error || 'Fehler bei Publish')
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')
}

View File

@@ -10,7 +10,13 @@ import {
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'
@@ -121,6 +127,8 @@ export default function GenerateIt() {
const [generateResult, setGenerateResult] = useState<GenerateStats | null>(null)
const [generateError, setGenerateError] = useState<string | null>(null)
const [publishResult, setPublishResult] = useState<{ q: number; w: number } | null>(null)
const [questions, setQuestions] = useState<ObjectQuestion[]>([])
const [objWords, setObjWords] = useState<ObjectWord[]>([])
// Prompt layouts
const [layouts, setLayouts] = useState<PromptLayout[]>(loadLayouts)
@@ -152,6 +160,7 @@ export default function GenerateIt() {
if (!currentPicture || !token) {
setDirectusObjects([]); setSelectedObjId(null)
setGenerateResult(null); setPublishResult(null); setGenerateError(null)
setQuestions([]); setObjWords([])
return
}
getDirectusObjects(currentPicture.id, token)
@@ -163,6 +172,30 @@ export default function GenerateIt() {
.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)
@@ -172,6 +205,7 @@ export default function GenerateIt() {
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 {
@@ -382,24 +416,60 @@ export default function GenerateIt() {
</div>
</main>
{/* Right: Generate results */}
<aside className="sidebar sidebar--right">
<div className="sidebar-panel" style={{ flex: 1 }}>
<h3 className="sidebar-heading">Ergebnis</h3>
{generateResult ? (
<div style={{ padding: '8px 12px', fontSize: 13, display: 'flex', flexDirection: 'column', gap: 6 }}>
<div><strong>{generateResult.questions_created}</strong> Fragen erstellt</div>
<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>
{/* Words sidebar */}
<aside className="sidebar sidebar--words">
<div className="sidebar-panel" style={{ flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
<h3 className="sidebar-heading">
Wörter
{objWords.length > 0 && <span className="badge">{objWords.length}</span>}
</h3>
{objWords.length === 0 ? (
<div className="empty-state"></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 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>
</aside>
{/* 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>