feat: multi-word per object + {objectID.wordID} placeholders
- Annotate: multiple words per object via db_objects_db_words M2M, word chips with add/remove per object card
- Generate sidebar: objects shown with comma-separated word list as display name
- Generate pair form: all object words as suggestion chips, click inserts {objectId.wordId} at cursor
- Preview resolves {objectId.wordId} → actual word text
- Backend: POST adds single word (no replace), new DELETE /db-objects/<id>/words/<junctionId>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,11 +10,12 @@ import {
|
||||
updateDbObject,
|
||||
deleteDbObject,
|
||||
getDbObjectWords,
|
||||
saveDbObjectWord,
|
||||
addDbObjectWord,
|
||||
deleteDbObjectWord,
|
||||
directusAssetUrl,
|
||||
} from '../api'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import type { DbPicture, DbObject, Selection, CanvasObject } from '../types'
|
||||
import type { DbPicture, DbObject, DbWord, Selection, CanvasObject } 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">
|
||||
@@ -42,10 +43,10 @@ export default function DrawIt() {
|
||||
const [selectedObjectId, setSelectedObjectId] = useState<string | null>(null)
|
||||
const [currentSelections, setCurrentSelections] = useState<Selection[]>([])
|
||||
const [userNotes, setUserNotes] = useState('')
|
||||
// per-object labels: objectId → titel_de
|
||||
const [objectLabels, setObjectLabels] = useState<Record<string, string>>({})
|
||||
// track which label was last saved to detect changes
|
||||
const savedLabelsRef = useRef<Record<string, string>>({})
|
||||
// per-object words: objectId → DbWord[]
|
||||
const [objectWords, setObjectWords] = useState<Record<string, DbWord[]>>({})
|
||||
// per-object word input values: objectId → current input text
|
||||
const [wordInputs, setWordInputs] = useState<Record<string, string>>({})
|
||||
const [editingNotes, setEditingNotes] = useState<{ id: string; notes: string } | null>(null)
|
||||
const [mode, setMode] = useState<'rect' | 'polygon'>('polygon')
|
||||
const [hasSelection, setHasSelection] = useState(false)
|
||||
@@ -86,8 +87,8 @@ export default function DrawIt() {
|
||||
useEffect(() => {
|
||||
if (!currentPicture || !token) {
|
||||
setObjects([]); setSelectedObjectId(null)
|
||||
setObjectLabels({})
|
||||
savedLabelsRef.current = {}
|
||||
setObjectWords({})
|
||||
setWordInputs({})
|
||||
setImageLoaded(false)
|
||||
return
|
||||
}
|
||||
@@ -95,16 +96,15 @@ export default function DrawIt() {
|
||||
.then(objs => {
|
||||
setObjects(objs.map(o => ({ ...o, visible: true })))
|
||||
setSelectedObjectId(null)
|
||||
// Load word for each object
|
||||
const newLabels: Record<string, string> = {}
|
||||
// Load words for each object
|
||||
const newWords: Record<string, DbWord[]> = {}
|
||||
const promises = objs.map(obj =>
|
||||
getDbObjectWords(obj.id, token)
|
||||
.then(words => { newLabels[obj.id] = words[0]?.titel_de ?? '' })
|
||||
.catch(() => { newLabels[obj.id] = '' })
|
||||
.then(words => { newWords[obj.id] = words })
|
||||
.catch(() => { newWords[obj.id] = [] })
|
||||
)
|
||||
Promise.all(promises).then(() => {
|
||||
setObjectLabels(newLabels)
|
||||
savedLabelsRef.current = { ...newLabels }
|
||||
setObjectWords(newWords)
|
||||
})
|
||||
})
|
||||
.catch(console.error)
|
||||
@@ -116,16 +116,32 @@ export default function DrawIt() {
|
||||
|
||||
const handleHasSelection = useCallback((has: boolean) => setHasSelection(has), [])
|
||||
|
||||
const handleLabelBlur = async (objId: string) => {
|
||||
const handleAddObjectWord = async (objId: string) => {
|
||||
if (!token) return
|
||||
const current = objectLabels[objId] ?? ''
|
||||
const saved = savedLabelsRef.current[objId] ?? ''
|
||||
if (current === saved) return
|
||||
const titel_de = (wordInputs[objId] || '').trim()
|
||||
if (!titel_de) return
|
||||
try {
|
||||
await saveDbObjectWord(objId, { titel_de: current, level: 50 }, token)
|
||||
savedLabelsRef.current[objId] = current
|
||||
const result = await addDbObjectWord(objId, { titel_de, level: 50 }, token)
|
||||
if (result.ok) {
|
||||
const words = await getDbObjectWords(objId, token)
|
||||
setObjectWords(prev => ({ ...prev, [objId]: words }))
|
||||
setWordInputs(prev => ({ ...prev, [objId]: '' }))
|
||||
}
|
||||
} catch (e) {
|
||||
showStatus(e instanceof Error ? e.message : 'Fehler beim Speichern der Bezeichnung.', true)
|
||||
showStatus(e instanceof Error ? e.message : 'Fehler beim Hinzufügen des Worts.', true)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemoveObjectWord = async (objId: string, junctionId: string | number) => {
|
||||
if (!token) return
|
||||
try {
|
||||
await deleteDbObjectWord(objId, junctionId, token)
|
||||
setObjectWords(prev => ({
|
||||
...prev,
|
||||
[objId]: (prev[objId] || []).filter(w => w.junction_id !== junctionId)
|
||||
}))
|
||||
} catch (e) {
|
||||
showStatus(e instanceof Error ? e.message : 'Fehler beim Entfernen des Worts.', true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,8 +163,7 @@ export default function DrawIt() {
|
||||
user_notes: userNotes.trim() || null,
|
||||
}, token)
|
||||
setObjects(prev => [...prev, { ...obj, visible: true }])
|
||||
setObjectLabels(prev => ({ ...prev, [obj.id]: '' }))
|
||||
savedLabelsRef.current[obj.id] = ''
|
||||
setObjectWords(prev => ({ ...prev, [obj.id]: [] }))
|
||||
setCurrentSelections([])
|
||||
setUserNotes('')
|
||||
canvasRef.current?.resetSelection()
|
||||
@@ -194,8 +209,8 @@ export default function DrawIt() {
|
||||
try {
|
||||
await deleteDbObject(objId, token)
|
||||
setObjects(prev => prev.filter(o => o.id !== objId))
|
||||
setObjectLabels(prev => { const n = { ...prev }; delete n[objId]; return n })
|
||||
delete savedLabelsRef.current[objId]
|
||||
setObjectWords(prev => { const n = { ...prev }; delete n[objId]; return n })
|
||||
setWordInputs(prev => { const n = { ...prev }; delete n[objId]; return n })
|
||||
if (selectedObjectId === objId) setSelectedObjectId(null)
|
||||
showStatus('Objekt gelöscht.')
|
||||
} catch (e) {
|
||||
@@ -288,22 +303,43 @@ export default function DrawIt() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Per-object label input */}
|
||||
{/* Per-object multi-word chips */}
|
||||
<div style={{ padding: '6px 8px', borderTop: '1px solid var(--border)' }} onClick={e => e.stopPropagation()}>
|
||||
<label style={{ fontSize: 10, color: 'var(--text-2)', display: 'block', marginBottom: 3 }}>Bezeichnung</label>
|
||||
<input
|
||||
type="text"
|
||||
value={objectLabels[obj.id] ?? ''}
|
||||
onChange={e => setObjectLabels(prev => ({ ...prev, [obj.id]: e.target.value }))}
|
||||
onBlur={() => handleLabelBlur(obj.id)}
|
||||
placeholder="Bezeichnung…"
|
||||
style={{
|
||||
width: '100%', padding: '4px 7px', borderRadius: 'var(--r-sm)',
|
||||
border: '1px solid var(--border)', background: 'var(--surface-2)',
|
||||
color: 'var(--text-1)', fontFamily: 'var(--font)', fontSize: 12,
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
/>
|
||||
<label style={{ fontSize: 10, color: 'var(--text-2)', display: 'block', marginBottom: 3 }}>Wörter</label>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, marginBottom: 4 }}>
|
||||
{(objectWords[obj.id] || []).map(w => (
|
||||
<span key={w.junction_id} style={{
|
||||
display: 'flex', alignItems: 'center', gap: 3,
|
||||
padding: '2px 8px', background: '#e0e7ff', color: '#3730a3',
|
||||
borderRadius: 9999, fontSize: 11,
|
||||
}}>
|
||||
{w.titel_de}
|
||||
<button
|
||||
onClick={() => handleRemoveObjectWord(obj.id, w.junction_id!)}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#818cf8', padding: 0, lineHeight: 1, fontSize: 13 }}
|
||||
title="Entfernen"
|
||||
>×</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<input
|
||||
type="text"
|
||||
value={wordInputs[obj.id] || ''}
|
||||
onChange={e => setWordInputs(prev => ({ ...prev, [obj.id]: e.target.value }))}
|
||||
onKeyDown={e => { if (e.key === 'Enter') handleAddObjectWord(obj.id) }}
|
||||
placeholder="Wort hinzufügen…"
|
||||
style={{
|
||||
flex: 1, padding: '3px 7px', borderRadius: 'var(--r-sm)',
|
||||
border: '1px solid var(--border)', background: 'var(--surface-2)',
|
||||
color: 'var(--text-1)', fontFamily: 'var(--font)', fontSize: 12,
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleAddObjectWord(obj.id)}
|
||||
style={{ padding: '3px 8px', borderRadius: 'var(--r-sm)', background: '#6366f1', color: '#fff', border: 'none', cursor: 'pointer', fontSize: 12 }}
|
||||
>+</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user