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:
@@ -195,3 +195,38 @@ export async function generateSentence(
|
||||
if (!res.ok) throw new Error(data.error || 'Fehler bei KI-Sentence')
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import DrawCanvas, { type DrawCanvasHandle } from '../components/DrawCanvas'
|
||||
import GenerateObjectsList from '../components/GenerateObjectsList'
|
||||
import SentencesList from '../components/SentencesList'
|
||||
|
||||
import Topbar from '../components/Topbar'
|
||||
import {
|
||||
getDirectusPictures,
|
||||
directusAssetUrl,
|
||||
type DirectusPicture,
|
||||
getDirectusObjects,
|
||||
generateDetails,
|
||||
generateSentence,
|
||||
getSentences,
|
||||
generateQuestions,
|
||||
publishQuestions,
|
||||
type GenerateStats,
|
||||
} from '../api'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import type { DirectusObject, CanvasObject } from '../types'
|
||||
import type { Sentence } from '../types'
|
||||
|
||||
const ChevronLeftIcon = () => (
|
||||
<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>
|
||||
)
|
||||
|
||||
const SparkleIcon = () => (
|
||||
const GenerateIcon = () => (
|
||||
<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" />
|
||||
</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">
|
||||
<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>
|
||||
)
|
||||
|
||||
@@ -106,18 +105,6 @@ function persistLayouts(layouts: PromptLayout[]): void {
|
||||
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 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -129,9 +116,11 @@ export default function GenerateIt() {
|
||||
const [currentIndex, setCurrentIndex] = useState(-1)
|
||||
const [directusObjects, setDirectusObjects] = useState<DirectusObject[]>([])
|
||||
const [selectedObjId, setSelectedObjId] = useState<string | null>(null)
|
||||
const [sentences, setSentences] = useState<Sentence[]>([])
|
||||
const [isGeneratingDetails, setIsGeneratingDetails] = useState(false)
|
||||
const [isGeneratingSentence, setIsGeneratingSentence] = useState(false)
|
||||
const [isGenerating, setIsGenerating] = useState(false)
|
||||
const [isPublishing, setIsPublishing] = 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
|
||||
const [layouts, setLayouts] = useState<PromptLayout[]>(loadLayouts)
|
||||
@@ -144,8 +133,6 @@ export default function GenerateIt() {
|
||||
const currentPicture: DirectusPicture | null =
|
||||
currentIndex >= 0 && currentIndex < pictureList.length ? pictureList[currentIndex] : null
|
||||
|
||||
const words = extractWords(sentences)
|
||||
|
||||
const canvasObjects: CanvasObject[] = directusObjects.map((obj, i) => ({
|
||||
id: obj.id,
|
||||
visible: true,
|
||||
@@ -163,44 +150,46 @@ export default function GenerateIt() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentPicture || !token) {
|
||||
setDirectusObjects([]); setSelectedObjId(null); setSentences([])
|
||||
setDirectusObjects([]); setSelectedObjId(null)
|
||||
setGenerateResult(null); setPublishResult(null); setGenerateError(null)
|
||||
return
|
||||
}
|
||||
getDirectusObjects(currentPicture.id, token)
|
||||
.then(objs => {
|
||||
setDirectusObjects(objs)
|
||||
if (objs.length > 0) { setSelectedObjId(objs[0].id); loadSentences(objs[0].id) }
|
||||
else { setSelectedObjId(null); setSentences([]) }
|
||||
if (objs.length > 0) setSelectedObjId(objs[0].id)
|
||||
else setSelectedObjId(null)
|
||||
})
|
||||
.catch(console.error)
|
||||
}, [currentPicture?.id, token])
|
||||
|
||||
const loadSentences = async (objId: string) => {
|
||||
try { setSentences(await getSentences(objId)) } catch (e) { console.error(e) }
|
||||
}
|
||||
|
||||
const handleGenerateDetails = async () => {
|
||||
if (!selectedObjId) return
|
||||
setIsGeneratingDetails(true)
|
||||
const handleGenerate = async () => {
|
||||
if (!selectedObjId || !token) return
|
||||
setIsGenerating(true)
|
||||
setGenerateResult(null)
|
||||
setGenerateError(null)
|
||||
setPublishResult(null)
|
||||
try {
|
||||
await generateDetails(selectedObjId)
|
||||
const res = await generateQuestions(selectedObjId, promptText, token)
|
||||
setGenerateResult(res.stats)
|
||||
} catch (e) {
|
||||
alert(e instanceof Error ? e.message : 'Fehler bei KI-Details')
|
||||
setGenerateError(e instanceof Error ? e.message : 'Fehler beim Generieren')
|
||||
} finally {
|
||||
setIsGeneratingDetails(false)
|
||||
setIsGenerating(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleGenerateSentence = async () => {
|
||||
if (!selectedObjId) return
|
||||
setIsGeneratingSentence(true)
|
||||
const handlePublish = async () => {
|
||||
if (!selectedObjId || !token) return
|
||||
setIsPublishing(true)
|
||||
setPublishResult(null)
|
||||
try {
|
||||
const data = await generateSentence(selectedObjId)
|
||||
setSentences(prev => [...prev, data.sentence])
|
||||
const res = await publishQuestions(selectedObjId, token)
|
||||
setPublishResult({ q: res.published_questions, w: res.published_words })
|
||||
} catch (e) {
|
||||
alert(e instanceof Error ? e.message : 'Fehler bei KI-Sentence')
|
||||
setGenerateError(e instanceof Error ? e.message : 'Fehler beim Veröffentlichen')
|
||||
} finally {
|
||||
setIsGeneratingSentence(false)
|
||||
setIsPublishing(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,14 +247,40 @@ export default function GenerateIt() {
|
||||
|
||||
<div style={{ width: 1, height: 24, background: 'var(--border)' }} />
|
||||
|
||||
<button className="btn-ghost btn-sm" onClick={handleGenerateDetails} disabled={isGeneratingDetails || !selectedObjId}>
|
||||
<SparkleIcon />
|
||||
{isGeneratingDetails ? 'Generiere…' : 'KI-Details'}
|
||||
<button
|
||||
className="btn-ghost btn-sm"
|
||||
onClick={handleGenerate}
|
||||
disabled={isGenerating || isPublishing || !selectedObjId}
|
||||
style={isGenerating ? { opacity: 0.7 } : undefined}
|
||||
>
|
||||
<GenerateIcon />
|
||||
{isGenerating ? 'Generiere…' : 'Generate it'}
|
||||
</button>
|
||||
<button className="btn-ghost btn-sm" onClick={handleGenerateSentence} disabled={isGeneratingSentence || !selectedObjId}>
|
||||
<ChatIcon />
|
||||
{isGeneratingSentence ? 'Generiere…' : 'KI-Sentence'}
|
||||
<button
|
||||
className="btn-ghost btn-sm"
|
||||
onClick={handlePublish}
|
||||
disabled={isPublishing || isGenerating || !selectedObjId || !generateResult}
|
||||
title={!generateResult ? 'Erst Generate it ausführen' : undefined}
|
||||
>
|
||||
<PublishIcon />
|
||||
{isPublishing ? 'Veröffentliche…' : 'Publish it'}
|
||||
</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>
|
||||
)
|
||||
|
||||
@@ -284,7 +299,7 @@ export default function GenerateIt() {
|
||||
<GenerateObjectsList
|
||||
objects={directusObjects}
|
||||
selectedId={selectedObjId}
|
||||
onSelect={id => { setSelectedObjId(id); loadSentences(id) }}
|
||||
onSelect={id => setSelectedObjId(id)}
|
||||
/>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -367,33 +382,26 @@ export default function GenerateIt() {
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Words */}
|
||||
<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 */}
|
||||
{/* Right: Generate results */}
|
||||
<aside className="sidebar sidebar--right">
|
||||
<div className="sidebar-panel" style={{ flex: 1 }}>
|
||||
<h3 className="sidebar-heading">
|
||||
Sätze
|
||||
{sentences.length > 0 && <span className="badge">{sentences.length}</span>}
|
||||
</h3>
|
||||
<SentencesList sentences={sentences} />
|
||||
<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>
|
||||
) : (
|
||||
<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>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user