Fertigstellen-Button + drawing_created Status-Flow

- DrawIt: Button "Fertigstellen" unter Objektliste setzt Picture-Status auf drawing_created
- Bild verschwindet danach aus der Annotieren-Ansicht
- GenerateIt: lädt jetzt Directus-Bilder mit status=drawing_created
- GenerateIt: zeigt Bild-Vorschau + Directus-Objekte
- app.py: PATCH-Endpunkt für Pictures + Status-Parameter im GET

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-25 21:49:13 +02:00
parent e18d9a5796
commit a42fadef09
4 changed files with 111 additions and 65 deletions

13
app.py
View File

@@ -67,9 +67,18 @@ def directus_auth_login():
@app.route("/api/directus/pictures", methods=["GET"]) @app.route("/api/directus/pictures", methods=["GET"])
def directus_pictures(): def directus_pictures():
"""Proxy: Directus-Bilder (status=new).""" """Proxy: Directus-Bilder nach Status filtern."""
token = request.headers.get("Authorization", "") token = request.headers.get("Authorization", "")
data, status = _directus("GET", "/items/pictures?filter[status][_eq]=new&fields=id,media,status&sort=date_created", token) pic_status = request.args.get("status", "new")
data, status = _directus("GET", f"/items/pictures?filter[status][_eq]={pic_status}&fields=id,media,status&sort=date_created", token)
return jsonify(data), status
@app.route("/api/directus/pictures/<pic_id>", methods=["PATCH"])
def directus_picture(pic_id):
"""Proxy: Bild-Status aktualisieren."""
token = request.headers.get("Authorization", "")
data, status = _directus("PATCH", f"/items/pictures/{pic_id}", token, body=request.get_json())
return jsonify(data), status return jsonify(data), status

View File

@@ -19,8 +19,8 @@ export interface DirectusPicture {
status: string status: string
} }
export async function getDirectusPictures(token: string): Promise<DirectusPicture[]> { export async function getDirectusPictures(token: string, status = 'new'): Promise<DirectusPicture[]> {
const res = await fetch('/api/directus/pictures', { const res = await fetch(`/api/directus/pictures?status=${status}`, {
headers: { Authorization: `Bearer ${token}` }, headers: { Authorization: `Bearer ${token}` },
}) })
if (!res.ok) throw new Error('Fehler beim Laden der Directus-Bilder') if (!res.ok) throw new Error('Fehler beim Laden der Directus-Bilder')
@@ -28,6 +28,15 @@ export async function getDirectusPictures(token: string): Promise<DirectusPictur
return data.data as DirectusPicture[] return data.data as DirectusPicture[]
} }
export async function updatePictureStatus(pictureId: string, status: string, token: string): Promise<void> {
const res = await fetch(`/api/directus/pictures/${pictureId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ status }),
})
if (!res.ok) throw new Error('Fehler beim Aktualisieren des Status')
}
export function directusAssetUrl(mediaId: string, token: string): string { export function directusAssetUrl(mediaId: string, token: string): string {
return `${DIRECTUS_URL}/assets/${mediaId}?access_token=${token}` return `${DIRECTUS_URL}/assets/${mediaId}?access_token=${token}`
} }

View File

@@ -4,6 +4,7 @@ import Topbar from '../components/Topbar'
import { import {
getDirectusPictures, directusAssetUrl, type DirectusPicture, getDirectusPictures, directusAssetUrl, type DirectusPicture,
getDirectusObjects, createDirectusObject, updateDirectusObject, deleteDirectusObject, getDirectusObjects, createDirectusObject, updateDirectusObject, deleteDirectusObject,
updatePictureStatus,
} from '../api' } from '../api'
import { useAuth } from '../context/AuthContext' import { useAuth } from '../context/AuthContext'
import type { DirectusObject, Selection, CanvasObject } from '../types' import type { DirectusObject, Selection, CanvasObject } from '../types'
@@ -38,6 +39,7 @@ export default function DrawIt() {
const [mode, setMode] = useState<'rect' | 'polygon'>('polygon') const [mode, setMode] = useState<'rect' | 'polygon'>('polygon')
const [hasSelection, setHasSelection] = useState(false) const [hasSelection, setHasSelection] = useState(false)
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
const [finishing, setFinishing] = useState(false)
const [status, setStatus] = useState('') const [status, setStatus] = useState('')
const [statusError, setStatusError] = useState(false) const [statusError, setStatusError] = useState(false)
@@ -106,6 +108,22 @@ export default function DrawIt() {
} }
} }
const finishPicture = async () => {
if (!currentPicture || !token) return
setFinishing(true)
try {
await updatePictureStatus(currentPicture.id, 'drawing_created', token)
setPictureList(prev => prev.filter(p => p.id !== currentPicture.id))
setCurrentIndex(i => Math.max(0, i - 1))
setObjects([])
showStatus('Bild fertiggestellt.')
} catch (e) {
showStatus(e instanceof Error ? e.message : 'Fehler.', true)
} finally {
setFinishing(false)
}
}
const saveNoteEdit = async () => { const saveNoteEdit = async () => {
if (!editingNotes || !token) return if (!editingNotes || !token) return
try { try {
@@ -220,6 +238,19 @@ export default function DrawIt() {
</div> </div>
)} )}
</div> </div>
{objects.length > 0 && (
<div className="sidebar-panel">
<button
className="btn-primary btn-sm btn-block"
onClick={finishPicture}
disabled={finishing}
style={{ background: 'var(--success, #16a34a)' }}
>
{finishing ? 'Wird fertiggestellt…' : '✅ Fertigstellen'}
</button>
</div>
)}
</aside> </aside>
{/* Center: Canvas */} {/* Center: Canvas */}

View File

@@ -3,7 +3,9 @@ import ObjectsList from '../components/ObjectsList'
import DetailsPanel from '../components/DetailsPanel' import DetailsPanel from '../components/DetailsPanel'
import SentencesList from '../components/SentencesList' import SentencesList from '../components/SentencesList'
import Topbar from '../components/Topbar' import Topbar from '../components/Topbar'
import { getImages, getObjects, generateDetails, generateSentence, getSentences } from '../api' import { getDirectusPictures, directusAssetUrl, type DirectusPicture, getDirectusObjects, generateDetails, generateSentence, getSentences } from '../api'
import { useAuth } from '../context/AuthContext'
import type { DirectusObject, Selection } from '../types'
import type { ObjectMeta, Sentence } from '../types' import type { ObjectMeta, Sentence } from '../types'
const ChevronLeftIcon = () => ( const ChevronLeftIcon = () => (
@@ -30,54 +32,58 @@ const ChatIcon = () => (
</svg> </svg>
) )
function directusObjToMeta(obj: DirectusObject, index: number): ObjectMeta {
const first = obj.selections?.[0]
return {
id: obj.id,
image_file: '',
title_de: '',
position_de: '',
action_de: '',
condition_de: '',
hierarchy: 1,
parent_id: obj.parent ?? undefined,
user_notes: obj.user_notes ?? '',
selections: obj.selections ?? [],
} as unknown as ObjectMeta
}
export default function GenerateIt() { export default function GenerateIt() {
const [imageList, setImageList] = useState<string[]>([]) const { token } = useAuth()
const [pictureList, setPictureList] = useState<DirectusPicture[]>([])
const [currentIndex, setCurrentIndex] = useState(-1) const [currentIndex, setCurrentIndex] = useState(-1)
const [directusObjects, setDirectusObjects] = useState<DirectusObject[]>([])
const [objects, setObjects] = useState<ObjectMeta[]>([]) const [objects, setObjects] = useState<ObjectMeta[]>([])
const [selectedObj, setSelectedObj] = useState<ObjectMeta | null>(null) const [selectedObj, setSelectedObj] = useState<ObjectMeta | null>(null)
const [sentences, setSentences] = useState<Sentence[]>([]) const [sentences, setSentences] = useState<Sentence[]>([])
const [isGeneratingDetails, setIsGeneratingDetails] = useState(false) const [isGeneratingDetails, setIsGeneratingDetails] = useState(false)
const [isGeneratingSentence, setIsGeneratingSentence] = useState(false) const [isGeneratingSentence, setIsGeneratingSentence] = useState(false)
const currentFilename = const currentPicture: DirectusPicture | null =
currentIndex >= 0 && currentIndex < imageList.length ? imageList[currentIndex] : null currentIndex >= 0 && currentIndex < pictureList.length ? pictureList[currentIndex] : null
useEffect(() => { useEffect(() => {
getImages('generate') if (!token) return
.then(imgs => { getDirectusPictures(token, 'drawing_created')
setImageList(imgs) .then(pics => { setPictureList(pics); setCurrentIndex(pics.length > 0 ? 0 : -1) })
setCurrentIndex(imgs.length - 1)
})
.catch(console.error) .catch(console.error)
}, []) }, [token])
useEffect(() => { useEffect(() => {
if (!currentFilename) { if (!currentPicture || !token) { setDirectusObjects([]); setObjects([]); setSelectedObj(null); setSentences([]); return }
setObjects([]) getDirectusObjects(currentPicture.id, token)
setSelectedObj(null)
setSentences([])
return
}
getObjects(currentFilename)
.then(objs => { .then(objs => {
setObjects(objs) setDirectusObjects(objs)
if (objs.length > 0) { const metas = objs.map((o, i) => directusObjToMeta(o, i))
setSelectedObj(objs[0]) setObjects(metas)
loadSentences(objs[0].id) if (metas.length > 0) { setSelectedObj(metas[0]); loadSentences(metas[0].id) }
} else { else { setSelectedObj(null); setSentences([]) }
setSelectedObj(null)
setSentences([])
}
}) })
.catch(console.error) .catch(console.error)
}, [currentFilename]) }, [currentPicture?.id, token])
const loadSentences = async (objId: string) => { const loadSentences = async (objId: string) => {
try { try { setSentences(await getSentences(objId)) } catch (e) { console.error(e) }
setSentences(await getSentences(objId))
} catch (e) {
console.error(e)
}
} }
const handleGenerateDetails = async () => { const handleGenerateDetails = async () => {
@@ -114,48 +120,32 @@ export default function GenerateIt() {
const imageNav = ( const imageNav = (
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div className="image-nav"> <div className="image-nav">
<button <button className="btn-icon" onClick={() => setCurrentIndex(i => i - 1)} disabled={currentIndex <= 0}>
className="btn-icon"
onClick={() => setCurrentIndex(i => i - 1)}
disabled={currentIndex <= 0}
>
<ChevronLeftIcon /> <ChevronLeftIcon />
</button> </button>
<span className="image-counter"> <span className="image-counter">
{imageList.length > 0 ? ( {pictureList.length > 0 ? (
<> <>
<span className="image-counter-num">{currentIndex + 1}</span> <span className="image-counter-num">{currentIndex + 1}</span>
<span className="image-counter-sep">/</span> <span className="image-counter-sep">/</span>
<span className="image-counter-total">{imageList.length}</span> <span className="image-counter-total">{pictureList.length}</span>
</> </>
) : ( ) : (
<span className="image-counter-empty">Keine Bilder</span> <span className="image-counter-empty">Keine Bilder</span>
)} )}
</span> </span>
<button <button className="btn-icon" onClick={() => setCurrentIndex(i => i + 1)} disabled={currentIndex >= pictureList.length - 1}>
className="btn-icon"
onClick={() => setCurrentIndex(i => i + 1)}
disabled={currentIndex >= imageList.length - 1}
>
<ChevronRightIcon /> <ChevronRightIcon />
</button> </button>
</div> </div>
<div style={{ width: 1, height: 24, background: 'var(--border)' }} /> <div style={{ width: 1, height: 24, background: 'var(--border)' }} />
<button <button className="btn-ghost btn-sm" onClick={handleGenerateDetails} disabled={isGeneratingDetails || !selectedObj}>
className="btn-ghost btn-sm"
onClick={handleGenerateDetails}
disabled={isGeneratingDetails || !selectedObj}
>
<SparkleIcon /> <SparkleIcon />
{isGeneratingDetails ? 'Generiere…' : 'KI-Details'} {isGeneratingDetails ? 'Generiere…' : 'KI-Details'}
</button> </button>
<button <button className="btn-ghost btn-sm" onClick={handleGenerateSentence} disabled={isGeneratingSentence || !selectedObj}>
className="btn-ghost btn-sm"
onClick={handleGenerateSentence}
disabled={isGeneratingSentence || !selectedObj}
>
<ChatIcon /> <ChatIcon />
{isGeneratingSentence ? 'Generiere…' : 'KI-Sentence'} {isGeneratingSentence ? 'Generiere…' : 'KI-Sentence'}
</button> </button>
@@ -170,16 +160,16 @@ export default function GenerateIt() {
{/* Left: Objects */} {/* Left: Objects */}
<aside className="sidebar"> <aside className="sidebar">
<div className="sidebar-panel" style={{ flex: 1 }}> <div className="sidebar-panel" style={{ flex: 1 }}>
<h3 className="sidebar-heading">Objekte</h3> <h3 className="sidebar-heading">
Objekte
{objects.length > 0 && <span className="badge">{objects.length}</span>}
</h3>
<ObjectsList <ObjectsList
objects={objects} objects={objects}
selectedObjectId={selectedObj?.id ?? null} selectedObjectId={selectedObj?.id ?? null}
onSelect={id => { onSelect={id => {
const obj = objects.find(o => o.id === id) const obj = objects.find(o => o.id === id)
if (obj) { if (obj) { setSelectedObj(obj); loadSentences(obj.id) }
setSelectedObj(obj)
loadSentences(obj.id)
}
}} }}
onObjectsChange={setObjects} onObjectsChange={setObjects}
isGeneratePage={true} isGeneratePage={true}
@@ -189,9 +179,16 @@ export default function GenerateIt() {
</div> </div>
</aside> </aside>
{/* Center: Details */} {/* Center: Image + Details */}
<main className="canvas-area" style={{ alignItems: 'flex-start', justifyContent: 'flex-start' }}> <main className="canvas-area" style={{ alignItems: 'flex-start', justifyContent: 'flex-start', flexDirection: 'column', gap: 12, padding: 16 }}>
<div style={{ width: '100%', maxWidth: 520, display: 'flex', flexDirection: 'column', gap: 1 }}> {currentPicture && token && (
<img
src={directusAssetUrl(currentPicture.media, token)}
alt="Bild"
style={{ maxWidth: '100%', maxHeight: 320, borderRadius: 8, objectFit: 'contain', border: '1px solid var(--border)' }}
/>
)}
<div style={{ width: '100%', maxWidth: 520 }}>
<div className="sidebar-panel" style={{ background: 'var(--surface)', borderRadius: 'var(--r-lg)', border: '1px solid var(--border)' }}> <div className="sidebar-panel" style={{ background: 'var(--surface)', borderRadius: 'var(--r-lg)', border: '1px solid var(--border)' }}>
<DetailsPanel obj={selectedObj} objects={objects} sentences={sentences} /> <DetailsPanel obj={selectedObj} objects={objects} sentences={sentences} />
</div> </div>