diff --git a/frontend/src/components/DrawCanvas.tsx b/frontend/src/components/DrawCanvas.tsx index 9a01f1a..b205a8c 100644 --- a/frontend/src/components/DrawCanvas.tsx +++ b/frontend/src/components/DrawCanvas.tsx @@ -22,10 +22,11 @@ interface Props { selectedObjectId: string | null mode: 'rect' | 'polygon' onHasSelection: (has: boolean) => void + readOnly?: boolean } export default forwardRef(function DrawCanvas( - { imageSrc, objects, selectedObjectId, mode, onHasSelection }, + { imageSrc, objects, selectedObjectId, mode, onHasSelection, readOnly = false }, ref ) { const canvasRef = useRef(null) @@ -349,6 +350,8 @@ export default forwardRef(function DrawCanvas( } } + if (readOnly) return + canvas.addEventListener('mousedown', onMouseDown) canvas.addEventListener('mousemove', onMouseMove) canvas.addEventListener('mouseup', onMouseUp) @@ -359,7 +362,7 @@ export default forwardRef(function DrawCanvas( canvas.removeEventListener('mouseup', onMouseUp) canvas.removeEventListener('mouseleave', onMouseLeave) } - }, [redraw]) + }, [readOnly, redraw]) return }) diff --git a/frontend/src/components/GenerateObjectsList.tsx b/frontend/src/components/GenerateObjectsList.tsx new file mode 100644 index 0000000..408342b --- /dev/null +++ b/frontend/src/components/GenerateObjectsList.tsx @@ -0,0 +1,85 @@ +import type { DirectusObject } from '../types' + +interface Props { + objects: DirectusObject[] + selectedId: string | null + onSelect: (id: string) => void +} + +interface TreeNode { + obj: DirectusObject + children: TreeNode[] +} + +function buildTree(objects: DirectusObject[]): TreeNode[] { + const idSet = new Set(objects.map(o => o.id)) + const roots = objects.filter(o => !o.parent || !idSet.has(o.parent)) + const childrenOf = (parentId: string): TreeNode[] => + objects + .filter(o => o.parent === parentId) + .map(o => ({ obj: o, children: childrenOf(o.id) })) + return roots.map(o => ({ obj: o, children: childrenOf(o.id) })) +} + +function label(obj: DirectusObject): string { + return obj.user_notes?.trim() || `Objekt ${obj.id.slice(0, 6)}` +} + +function TreeItem({ + node, + depth, + selectedId, + onSelect, +}: { + node: TreeNode + depth: number + selectedId: string | null + onSelect: (id: string) => void +}) { + const { obj, children } = node + const isSelected = obj.id === selectedId + + return ( +
+
onSelect(obj.id)} + > + {depth > 0 && } + {label(obj)} +
+ {children.map(child => ( + + ))} +
+ ) +} + +export default function GenerateObjectsList({ objects, selectedId, onSelect }: Props) { + if (objects.length === 0) { + return
Noch keine Objekte.
+ } + + const tree = buildTree(objects) + + return ( +
+ {tree.map(node => ( + + ))} +
+ ) +} diff --git a/frontend/src/index.css b/frontend/src/index.css index 61e661e..ae3e31d 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -921,6 +921,59 @@ select:focus { font-style: italic; } +/* ===================================================== + GENERATE OBJECTS LIST + ===================================================== */ +.gen-objects-list { + display: flex; + flex-direction: column; + gap: 2px; + max-height: calc(100vh - 120px); + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: var(--border) transparent; +} + +.gen-object-item { + display: flex; + align-items: center; + gap: 5px; + padding: 6px 10px; + border-radius: var(--r-md); + cursor: pointer; + transition: background 0.12s, border-color 0.12s; + border: 1px solid transparent; + min-height: 32px; +} + +.gen-object-item:hover { + background: var(--surface-3); + border-color: var(--border); +} + +.gen-object-item.selected { + background: var(--primary-muted); + border-color: var(--primary); +} + +.gen-object-indent-marker { + font-size: 11px; + color: var(--text-3); + flex-shrink: 0; +} + +.gen-object-label { + font-size: 12.5px; + color: var(--text-1); + line-height: 1.4; + word-break: break-word; +} + +.gen-object-item.selected .gen-object-label { + color: var(--primary-muted-fg); + font-weight: 600; +} + /* ===================================================== WORDS CLOUD ===================================================== */ diff --git a/frontend/src/pages/GenerateIt.tsx b/frontend/src/pages/GenerateIt.tsx index 2ba6598..d414b65 100644 --- a/frontend/src/pages/GenerateIt.tsx +++ b/frontend/src/pages/GenerateIt.tsx @@ -1,5 +1,6 @@ -import { useState, useEffect } from 'react' -import ObjectsList from '../components/ObjectsList' +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 { @@ -12,8 +13,8 @@ import { getSentences, } from '../api' import { useAuth } from '../context/AuthContext' -import type { DirectusObject } from '../types' -import type { ObjectMeta, Sentence } from '../types' +import type { DirectusObject, CanvasObject } from '../types' +import type { Sentence } from '../types' const ChevronLeftIcon = () => ( @@ -118,31 +119,16 @@ function extractWords(sentences: Sentence[]): string[] { return [...new Set(words)].sort((a, b) => a.localeCompare(b)) } -// ── Helper ──────────────────────────────────────────────────────────────────── - -function directusObjToMeta(obj: DirectusObject): ObjectMeta { - 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 -} - // ── Component ───────────────────────────────────────────────────────────────── export default function GenerateIt() { const { token } = useAuth() + const canvasRef = useRef(null) + const [pictureList, setPictureList] = useState([]) const [currentIndex, setCurrentIndex] = useState(-1) - const [objects, setObjects] = useState([]) - const [selectedObj, setSelectedObj] = useState(null) + const [directusObjects, setDirectusObjects] = useState([]) + const [selectedObjId, setSelectedObjId] = useState(null) const [sentences, setSentences] = useState([]) const [isGeneratingDetails, setIsGeneratingDetails] = useState(false) const [isGeneratingSentence, setIsGeneratingSentence] = useState(false) @@ -159,6 +145,14 @@ export default function GenerateIt() { const words = extractWords(sentences) + const canvasObjects: CanvasObject[] = directusObjects.map((obj, i) => ({ + id: obj.id, + visible: true, + selections: obj.selections, + index: i + 1, + hierarchy: 1, + })) + useEffect(() => { if (!token) return getDirectusPictures(token, 'drawing_created') @@ -168,15 +162,14 @@ export default function GenerateIt() { useEffect(() => { if (!currentPicture || !token) { - setObjects([]); setSelectedObj(null); setSentences([]) + setDirectusObjects([]); setSelectedObjId(null); setSentences([]) return } getDirectusObjects(currentPicture.id, token) .then(objs => { - const metas = objs.map(directusObjToMeta) - setObjects(metas) - if (metas.length > 0) { setSelectedObj(metas[0]); loadSentences(metas[0].id) } - else { setSelectedObj(null); setSentences([]) } + setDirectusObjects(objs) + if (objs.length > 0) { setSelectedObjId(objs[0].id); loadSentences(objs[0].id) } + else { setSelectedObjId(null); setSentences([]) } }) .catch(console.error) }, [currentPicture?.id, token]) @@ -186,14 +179,10 @@ export default function GenerateIt() { } const handleGenerateDetails = async () => { - const target = selectedObj ?? objects[0] - if (!target) return + if (!selectedObjId) return setIsGeneratingDetails(true) try { - const data = await generateDetails(target.id) - const updated = { ...target, ...data } - setSelectedObj(updated) - setObjects(prev => prev.map(o => o.id === target.id ? updated : o)) + await generateDetails(selectedObjId) } catch (e) { alert(e instanceof Error ? e.message : 'Fehler bei KI-Details') } finally { @@ -202,13 +191,11 @@ export default function GenerateIt() { } const handleGenerateSentence = async () => { - const target = selectedObj ?? objects[0] - if (!target) return + if (!selectedObjId) return setIsGeneratingSentence(true) try { - const data = await generateSentence(target.id) + const data = await generateSentence(selectedObjId) setSentences(prev => [...prev, data.sentence]) - setSelectedObj(prev => prev ? { ...prev, latest_sentence: data.sentence } : prev) } catch (e) { alert(e instanceof Error ? e.message : 'Fehler bei KI-Sentence') } finally { @@ -270,11 +257,11 @@ export default function GenerateIt() {
- - @@ -291,32 +278,31 @@ export default function GenerateIt() {

Objekte - {objects.length > 0 && {objects.length}} + {directusObjects.length > 0 && {directusObjects.length}}

- { - const obj = objects.find(o => o.id === id) - if (obj) { setSelectedObj(obj); loadSentences(obj.id) } - }} - onObjectsChange={setObjects} - isGeneratePage={true} - onShowDetails={obj => setSelectedObj(obj)} - onLoadSentences={loadSentences} + { setSelectedObjId(id); loadSentences(id) }} />
- {/* Center: Image + Prompt Editor */} + {/* Center: Canvas + Prompt Editor */}
- {currentPicture && token && ( - Bild - )} +
+
+ {}} + readOnly + /> +
+
{/* Prompt Editor */}