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:
@@ -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')
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user