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:
2026-05-10 16:49:24 +02:00
parent 214f8a2019
commit 40c36182f1
5 changed files with 191 additions and 108 deletions

29
app.py
View File

@@ -1916,24 +1916,37 @@ def directus_db_object_words(obj_id):
if word.get("status") == "archived": if word.get("status") == "archived":
continue continue
items.append({ items.append({
"junction_id": entry.get("id"),
"word_id": word["id"], "word_id": word["id"],
"titel_de": word.get("titel_de", ""), "titel_de": word.get("titel_de", ""),
"level": word.get("level") or 50, "level": word.get("level") or 50,
"status": word.get("status", ""),
}) })
return jsonify({"data": items}) return jsonify({"data": items})
else: # POST — replace with single word else: # POST — add a single word to the object (M2M, allows multiple)
body = request.get_json(force=True, silent=True) or {} body = request.get_json(force=True, silent=True) or {}
titel_de = (body.get("titel_de") or "").strip() titel_de = (body.get("titel_de") or "").strip()
level = int(body.get("level") or 50) level = int(body.get("level") or 50)
# Delete all existing junctions for this object
existing, _ = _directus("GET", f"/items/db_objects_db_words?filter[db_objects_id][_eq]={obj_id}&fields=id&limit=20", token)
for e in (existing.get("data") or []):
_directus("DELETE", f"/items/db_objects_db_words/{e['id']}", token)
if not titel_de: if not titel_de:
return jsonify({"ok": True, "cleared": True}) return jsonify({"error": "titel_de required"}), 400
wid, _ = _find_or_create_db_word(titel_de, level, token) wid, _ = _find_or_create_db_word(titel_de, level, token)
_directus("POST", "/items/db_objects_db_words", token, {"db_objects_id": obj_id, "db_words_id": wid}) # Check if already linked to avoid duplicates
return jsonify({"ok": True, "word_id": wid}) existing, _ = _directus("GET",
f"/items/db_objects_db_words?filter[db_objects_id][_eq]={obj_id}&filter[db_words_id][_eq]={wid}&fields=id&limit=1",
token)
if existing.get("data"):
return jsonify({"ok": True, "already_exists": True, "word_id": wid, "junction_id": existing["data"][0]["id"]})
resp, s = _directus("POST", "/items/db_objects_db_words", token,
{"db_objects_id": obj_id, "db_words_id": wid})
junction_id = resp["data"]["id"] if s in (200, 201) else None
return jsonify({"ok": True, "word_id": wid, "junction_id": junction_id})
@app.route("/api/directus/db-objects/<obj_id>/words/<junction_id>", methods=["DELETE"])
def directus_db_object_word_delete(obj_id, junction_id):
token = request.headers.get("Authorization", "")
_directus("DELETE", f"/items/db_objects_db_words/{junction_id}", token)
return jsonify({"ok": True})
@app.route("/api/directus/db-pairs/<pair_id>", methods=["PATCH", "DELETE"]) @app.route("/api/directus/db-pairs/<pair_id>", methods=["PATCH", "DELETE"])

View File

@@ -463,15 +463,29 @@ export async function getDbObjectWords(objectId: string, token: string): Promise
return data.data as DbWord[] return data.data as DbWord[]
} }
export async function saveDbObjectWord( // Add a single word to an object (M2M)
export async function addDbObjectWord(
objectId: string, objectId: string,
word: { titel_de: string; level: number } | null, word: { titel_de: string; level: number },
token: string token: string
) { ) {
const res = await fetch(`/api/directus/db-objects/${objectId}/words`, { const res = await fetch(`/api/directus/db-objects/${objectId}/words`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: token }, headers: { 'Content-Type': 'application/json', Authorization: token },
body: JSON.stringify(word ? { titel_de: word.titel_de, level: word.level } : { titel_de: '' }), body: JSON.stringify(word),
})
return res.json()
}
// Remove a single word from an object by junction ID
export async function deleteDbObjectWord(
objectId: string,
junctionId: string | number,
token: string
) {
const res = await fetch(`/api/directus/db-objects/${objectId}/words/${junctionId}`, {
method: 'DELETE',
headers: { Authorization: token },
}) })
return res.json() return res.json()
} }

View File

@@ -10,11 +10,12 @@ import {
updateDbObject, updateDbObject,
deleteDbObject, deleteDbObject,
getDbObjectWords, getDbObjectWords,
saveDbObjectWord, addDbObjectWord,
deleteDbObjectWord,
directusAssetUrl, directusAssetUrl,
} from '../api' } from '../api'
import { useAuth } from '../context/AuthContext' import { useAuth } from '../context/AuthContext'
import type { DbPicture, DbObject, Selection, CanvasObject } from '../types' import type { DbPicture, DbObject, DbWord, Selection, CanvasObject } from '../types'
const ChevronLeftIcon = () => ( const ChevronLeftIcon = () => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"> <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 [selectedObjectId, setSelectedObjectId] = useState<string | null>(null)
const [currentSelections, setCurrentSelections] = useState<Selection[]>([]) const [currentSelections, setCurrentSelections] = useState<Selection[]>([])
const [userNotes, setUserNotes] = useState('') const [userNotes, setUserNotes] = useState('')
// per-object labels: objectId → titel_de // per-object words: objectId → DbWord[]
const [objectLabels, setObjectLabels] = useState<Record<string, string>>({}) const [objectWords, setObjectWords] = useState<Record<string, DbWord[]>>({})
// track which label was last saved to detect changes // per-object word input values: objectId → current input text
const savedLabelsRef = useRef<Record<string, string>>({}) const [wordInputs, setWordInputs] = useState<Record<string, string>>({})
const [editingNotes, setEditingNotes] = useState<{ id: string; notes: string } | null>(null) const [editingNotes, setEditingNotes] = useState<{ id: string; notes: string } | null>(null)
const [mode, setMode] = useState<'rect' | 'polygon'>('polygon') const [mode, setMode] = useState<'rect' | 'polygon'>('polygon')
const [hasSelection, setHasSelection] = useState(false) const [hasSelection, setHasSelection] = useState(false)
@@ -86,8 +87,8 @@ export default function DrawIt() {
useEffect(() => { useEffect(() => {
if (!currentPicture || !token) { if (!currentPicture || !token) {
setObjects([]); setSelectedObjectId(null) setObjects([]); setSelectedObjectId(null)
setObjectLabels({}) setObjectWords({})
savedLabelsRef.current = {} setWordInputs({})
setImageLoaded(false) setImageLoaded(false)
return return
} }
@@ -95,16 +96,15 @@ export default function DrawIt() {
.then(objs => { .then(objs => {
setObjects(objs.map(o => ({ ...o, visible: true }))) setObjects(objs.map(o => ({ ...o, visible: true })))
setSelectedObjectId(null) setSelectedObjectId(null)
// Load word for each object // Load words for each object
const newLabels: Record<string, string> = {} const newWords: Record<string, DbWord[]> = {}
const promises = objs.map(obj => const promises = objs.map(obj =>
getDbObjectWords(obj.id, token) getDbObjectWords(obj.id, token)
.then(words => { newLabels[obj.id] = words[0]?.titel_de ?? '' }) .then(words => { newWords[obj.id] = words })
.catch(() => { newLabels[obj.id] = '' }) .catch(() => { newWords[obj.id] = [] })
) )
Promise.all(promises).then(() => { Promise.all(promises).then(() => {
setObjectLabels(newLabels) setObjectWords(newWords)
savedLabelsRef.current = { ...newLabels }
}) })
}) })
.catch(console.error) .catch(console.error)
@@ -116,16 +116,32 @@ export default function DrawIt() {
const handleHasSelection = useCallback((has: boolean) => setHasSelection(has), []) const handleHasSelection = useCallback((has: boolean) => setHasSelection(has), [])
const handleLabelBlur = async (objId: string) => { const handleAddObjectWord = async (objId: string) => {
if (!token) return if (!token) return
const current = objectLabels[objId] ?? '' const titel_de = (wordInputs[objId] || '').trim()
const saved = savedLabelsRef.current[objId] ?? '' if (!titel_de) return
if (current === saved) return
try { try {
await saveDbObjectWord(objId, { titel_de: current, level: 50 }, token) const result = await addDbObjectWord(objId, { titel_de, level: 50 }, token)
savedLabelsRef.current[objId] = current if (result.ok) {
const words = await getDbObjectWords(objId, token)
setObjectWords(prev => ({ ...prev, [objId]: words }))
setWordInputs(prev => ({ ...prev, [objId]: '' }))
}
} catch (e) { } 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, user_notes: userNotes.trim() || null,
}, token) }, token)
setObjects(prev => [...prev, { ...obj, visible: true }]) setObjects(prev => [...prev, { ...obj, visible: true }])
setObjectLabels(prev => ({ ...prev, [obj.id]: '' })) setObjectWords(prev => ({ ...prev, [obj.id]: [] }))
savedLabelsRef.current[obj.id] = ''
setCurrentSelections([]) setCurrentSelections([])
setUserNotes('') setUserNotes('')
canvasRef.current?.resetSelection() canvasRef.current?.resetSelection()
@@ -194,8 +209,8 @@ export default function DrawIt() {
try { try {
await deleteDbObject(objId, token) await deleteDbObject(objId, token)
setObjects(prev => prev.filter(o => o.id !== objId)) setObjects(prev => prev.filter(o => o.id !== objId))
setObjectLabels(prev => { const n = { ...prev }; delete n[objId]; return n }) setObjectWords(prev => { const n = { ...prev }; delete n[objId]; return n })
delete savedLabelsRef.current[objId] setWordInputs(prev => { const n = { ...prev }; delete n[objId]; return n })
if (selectedObjectId === objId) setSelectedObjectId(null) if (selectedObjectId === objId) setSelectedObjectId(null)
showStatus('Objekt gelöscht.') showStatus('Objekt gelöscht.')
} catch (e) { } catch (e) {
@@ -288,22 +303,43 @@ export default function DrawIt() {
</div> </div>
)} )}
{/* Per-object label input */} {/* Per-object multi-word chips */}
<div style={{ padding: '6px 8px', borderTop: '1px solid var(--border)' }} onClick={e => e.stopPropagation()}> <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> <label style={{ fontSize: 10, color: 'var(--text-2)', display: 'block', marginBottom: 3 }}>Wörter</label>
<input <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, marginBottom: 4 }}>
type="text" {(objectWords[obj.id] || []).map(w => (
value={objectLabels[obj.id] ?? ''} <span key={w.junction_id} style={{
onChange={e => setObjectLabels(prev => ({ ...prev, [obj.id]: e.target.value }))} display: 'flex', alignItems: 'center', gap: 3,
onBlur={() => handleLabelBlur(obj.id)} padding: '2px 8px', background: '#e0e7ff', color: '#3730a3',
placeholder="Bezeichnung…" borderRadius: 9999, fontSize: 11,
style={{ }}>
width: '100%', padding: '4px 7px', borderRadius: 'var(--r-sm)', {w.titel_de}
border: '1px solid var(--border)', background: 'var(--surface-2)', <button
color: 'var(--text-1)', fontFamily: 'var(--font)', fontSize: 12, onClick={() => handleRemoveObjectWord(obj.id, w.junction_id!)}
boxSizing: 'border-box', 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>
</div> </div>
))} ))}

View File

@@ -13,7 +13,7 @@ import {
directusAssetUrl, directusAssetUrl,
} from '../api' } from '../api'
import { useAuth } from '../context/AuthContext' import { useAuth } from '../context/AuthContext'
import type { DbPicture, DbObject, DbPair, CanvasObject, ObjectChip } from '../types' import type { DbPicture, DbObject, DbWord, DbPair, CanvasObject, ObjectChip } from '../types'
const ChevronLeftIcon = () => ( const ChevronLeftIcon = () => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
@@ -47,19 +47,19 @@ function PairForm({ objectId, token, objectChips, onSaved, onCancel }: PairFormP
const statementRef = useRef<HTMLTextAreaElement>(null) const statementRef = useRef<HTMLTextAreaElement>(null)
const lastFocusedRef = useRef<'question' | 'statement'>('statement') const lastFocusedRef = useRef<'question' | 'statement'>('statement')
const resolveTemplate = (text: string) => const resolveTemplate = (text: string): string =>
text.replace(/\{obj:([^}]+)\}/g, (_, id) => { text.replace(/\{([^.}]+)\.([^}]+)\}/g, (_, oid, wid) => {
const chip = objectChips.find(c => c.objectId === id) const chip = objectChips.find(c => c.objectId === oid && c.wordId === wid)
return chip?.label ? chip.label : '{?}' return chip ? chip.label : '{?}'
}) })
const insertAtCursor = (objectId: string, _label: string) => { const insertAtCursor = (oid: string, wordId: string) => {
const ref = lastFocusedRef.current === 'question' ? questionRef : statementRef const ref = lastFocusedRef.current === 'question' ? questionRef : statementRef
const ta = ref.current const ta = ref.current
if (!ta) return if (!ta) return
const start = ta.selectionStart ?? ta.value.length const start = ta.selectionStart ?? ta.value.length
const end = ta.selectionEnd ?? ta.value.length const end = ta.selectionEnd ?? ta.value.length
const placeholder = `{obj:${objectId}}` const placeholder = `{${oid}.${wordId}}`
const newVal = ta.value.slice(0, start) + placeholder + ta.value.slice(end) const newVal = ta.value.slice(0, start) + placeholder + ta.value.slice(end)
if (lastFocusedRef.current === 'question') setQuestionDe(newVal) if (lastFocusedRef.current === 'question') setQuestionDe(newVal)
else setStatementDe(newVal) else setStatementDe(newVal)
@@ -104,19 +104,22 @@ function PairForm({ objectId, token, objectChips, onSaved, onCancel }: PairFormP
{/* Object chips for insertion */} {/* Object chips for insertion */}
{objectChips.length > 0 && ( {objectChips.length > 0 && (
<div className="flex flex-wrap gap-1 mb-2" style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}> <div>
{objectChips.map(chip => ( <p style={{ fontSize: 11, color: 'var(--text-2)', marginBottom: 4 }}>Objekt-Wörter einfügen:</p>
<button key={chip.objectId} type="button" <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
onClick={() => insertAtCursor(chip.objectId, chip.label)} {objectChips.map(chip => (
style={{ <button key={`${chip.objectId}.${chip.wordId}`} type="button"
padding: '2px 7px', borderRadius: 'var(--r-full)', fontSize: 11, onClick={() => insertAtCursor(chip.objectId, chip.wordId)}
border: '1px solid #93c5fd', cursor: 'pointer', style={{
background: '#eff6ff', color: '#1d4ed8', padding: '2px 7px', borderRadius: 'var(--r-full)', fontSize: 11,
}} border: '1px solid #93c5fd', cursor: 'pointer',
> background: '#eff6ff', color: '#1d4ed8',
{chip.index}. {chip.label || '?'} }}
</button> >
))} {chip.objectIndex}. {chip.label || '?'}
</button>
))}
</div>
</div> </div>
)} )}
@@ -212,19 +215,19 @@ function PairsList({ pairs, loading, objectId, token, objectChips, onRefresh }:
const editStatementRef = useRef<HTMLTextAreaElement>(null) const editStatementRef = useRef<HTMLTextAreaElement>(null)
const editLastFocusedRef = useRef<'question' | 'statement'>('statement') const editLastFocusedRef = useRef<'question' | 'statement'>('statement')
const resolveTemplate = (text: string) => const resolveTemplate = (text: string): string =>
text.replace(/\{obj:([^}]+)\}/g, (_, id) => { text.replace(/\{([^.}]+)\.([^}]+)\}/g, (_, oid, wid) => {
const chip = objectChips.find(c => c.objectId === id) const chip = objectChips.find(c => c.objectId === oid && c.wordId === wid)
return chip?.label ? chip.label : '{?}' return chip ? chip.label : '{?}'
}) })
const insertAtCursor = (objectId: string, _label: string) => { const insertAtCursor = (oid: string, wordId: string) => {
const ref = editLastFocusedRef.current === 'question' ? editQuestionRef : editStatementRef const ref = editLastFocusedRef.current === 'question' ? editQuestionRef : editStatementRef
const ta = ref.current const ta = ref.current
if (!ta) return if (!ta) return
const start = ta.selectionStart ?? ta.value.length const start = ta.selectionStart ?? ta.value.length
const end = ta.selectionEnd ?? ta.value.length const end = ta.selectionEnd ?? ta.value.length
const placeholder = `{obj:${objectId}}` const placeholder = `{${oid}.${wordId}}`
const newVal = ta.value.slice(0, start) + placeholder + ta.value.slice(end) const newVal = ta.value.slice(0, start) + placeholder + ta.value.slice(end)
if (editLastFocusedRef.current === 'question') setEditQuestion(newVal) if (editLastFocusedRef.current === 'question') setEditQuestion(newVal)
else setEditStatement(newVal) else setEditStatement(newVal)
@@ -291,19 +294,22 @@ function PairsList({ pairs, loading, objectId, token, objectChips, onRefresh }:
{/* Object chips for edit mode */} {/* Object chips for edit mode */}
{objectChips.length > 0 && ( {objectChips.length > 0 && (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}> <div>
{objectChips.map(chip => ( <p style={{ fontSize: 11, color: 'var(--text-2)', marginBottom: 4 }}>Objekt-Wörter einfügen:</p>
<button key={chip.objectId} type="button" <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
onClick={() => insertAtCursor(chip.objectId, chip.label)} {objectChips.map(chip => (
style={{ <button key={`${chip.objectId}.${chip.wordId}`} type="button"
padding: '2px 7px', borderRadius: 'var(--r-full)', fontSize: 11, onClick={() => insertAtCursor(chip.objectId, chip.wordId)}
border: '1px solid #93c5fd', cursor: 'pointer', style={{
background: '#eff6ff', color: '#1d4ed8', padding: '2px 7px', borderRadius: 'var(--r-full)', fontSize: 11,
}} border: '1px solid #93c5fd', cursor: 'pointer',
> background: '#eff6ff', color: '#1d4ed8',
{chip.index}. {chip.label || '?'} }}
</button> >
))} {chip.objectIndex}. {chip.label || '?'}
</button>
))}
</div>
</div> </div>
)} )}
@@ -409,8 +415,8 @@ export default function GenerateIt() {
const [imageLoaded, setImageLoaded] = useState(false) const [imageLoaded, setImageLoaded] = useState(false)
const [pairs, setPairs] = useState<DbPair[]>([]) const [pairs, setPairs] = useState<DbPair[]>([])
const [pairsLoading, setPairsLoading] = useState(false) const [pairsLoading, setPairsLoading] = useState(false)
// per-object labels: objectId → titel_de // per-object words: objectId → DbWord[]
const [objectLabels, setObjectLabels] = useState<Record<string, string>>({}) const [objectWords, setObjectWords] = useState<Record<string, DbWord[]>>({})
const currentPicture: DbPicture | null = const currentPicture: DbPicture | null =
currentIndex >= 0 && currentIndex < pictureList.length ? pictureList[currentIndex] : null currentIndex >= 0 && currentIndex < pictureList.length ? pictureList[currentIndex] : null
@@ -423,12 +429,17 @@ export default function GenerateIt() {
hierarchy: 1, hierarchy: 1,
})) }))
// Build object chips from dbObjects + objectLabels // Build object chips from dbObjects + objectWords (one chip per word)
const objectChips: ObjectChip[] = dbObjects.map((obj, i) => ({ const objectChips: ObjectChip[] = dbObjects.flatMap((obj, i) => {
objectId: obj.id, const words = objectWords[obj.id] || []
label: objectLabels[obj.id] ?? '', return words.map(w => ({
index: i + 1, objectId: obj.id,
})) wordId: w.word_id,
junctionId: w.junction_id!,
label: w.titel_de,
objectIndex: i + 1,
}))
})
// Load db_pictures with status=objects_created // Load db_pictures with status=objects_created
useEffect(() => { useEffect(() => {
@@ -442,21 +453,21 @@ export default function GenerateIt() {
useEffect(() => { useEffect(() => {
if (!currentPicture || !token) { if (!currentPicture || !token) {
setDbObjects([]); setSelectedObjId(null); setPairs([]); setImageLoaded(false) setDbObjects([]); setSelectedObjId(null); setPairs([]); setImageLoaded(false)
setObjectLabels({}) setObjectWords({})
return return
} }
getDbObjects(currentPicture.id, token) getDbObjects(currentPicture.id, token)
.then(objs => { .then(objs => {
setDbObjects(objs) setDbObjects(objs)
setSelectedObjId(objs.length > 0 ? objs[0].id : null) setSelectedObjId(objs.length > 0 ? objs[0].id : null)
// Load word/label for each object // Load words for each object
const newLabels: Record<string, string> = {} const newWords: Record<string, DbWord[]> = {}
const promises = objs.map(obj => const promises = objs.map(obj =>
getDbObjectWords(obj.id, token) getDbObjectWords(obj.id, token)
.then(words => { newLabels[obj.id] = words[0]?.titel_de ?? '' }) .then(words => { newWords[obj.id] = words })
.catch(() => { newLabels[obj.id] = '' }) .catch(() => { newWords[obj.id] = [] })
) )
Promise.all(promises).then(() => setObjectLabels(newLabels)) Promise.all(promises).then(() => setObjectWords(newWords))
}) })
.catch(console.error) .catch(console.error)
}, [currentPicture?.id, token]) }, [currentPicture?.id, token])
@@ -527,10 +538,13 @@ export default function GenerateIt() {
<div className="object-item-header"> <div className="object-item-header">
<div className="object-item-text"> <div className="object-item-text">
<strong>Objekt {i + 1}</strong> <strong>Objekt {i + 1}</strong>
{objectLabels[obj.id] && ( {(objectWords[obj.id] || []).length > 0 ? (
<span style={{ color: 'var(--primary)', fontWeight: 500 }}>{objectLabels[obj.id]}</span> <span style={{ color: 'var(--primary)', fontWeight: 500 }}>
{(objectWords[obj.id] || []).map(w => w.titel_de).join(', ')}
</span>
) : (
<span>{obj.selections?.length ? `${obj.selections.length} Auswahl(en)` : ''}</span>
)} )}
<span>{obj.selections?.length ? `${obj.selections.length} Auswahl(en)` : ''}</span>
</div> </div>
</div> </div>
{obj.user_notes && ( {obj.user_notes && (

View File

@@ -111,7 +111,13 @@ export interface DbPair {
statements: DbPairStatement[] statements: DbPairStatement[]
} }
export interface ObjectChip { objectId: string; label: string; index: number } export interface ObjectChip {
objectId: string
wordId: string
junctionId: string | number
label: string // titel_de
objectIndex: number // the object's index number for display
}
// Legacy — still used by GenerateIt // Legacy — still used by GenerateIt
export interface ObjectMeta { export interface ObjectMeta {