diff --git a/src/pages/ContentCreation.jsx b/src/pages/ContentCreation.jsx index c7f0b38..88d2008 100644 --- a/src/pages/ContentCreation.jsx +++ b/src/pages/ContentCreation.jsx @@ -22,18 +22,25 @@ function withPlaceholders(text, wordMap, objectAssignments = {}) { let result = text; Object.entries(wordMap).sort((a, b) => b[0].length - a[0].length).forEach(([title, w]) => { const re = new RegExp(`\\b${title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'gi'); - result = result.replace(re, `{{${objectAssignments[w.id] || w.id}}}`); + result = result.replace(re, match => { + const objectId = objectAssignments[w.id]; + if (objectId) return `{{${match}.o:${objectId}}}`; + return `{{${match}.w:${w.id}}}`; + }); }); return result; } async function resolvePlaceholders(text, allObjects) { if (!text) return ''; + // New format: {{label.w:uuid}} or {{label.o:uuid}} — label is embedded, no lookup needed + text = text.replace(/\{\{([^.}]+)\.[wo]:([^}]+)\}\}/g, (_, label) => label); + // Old format fallback: {{uuid}} const matches = [...text.matchAll(/\{\{([^}]+)\}\}/g)]; if (!matches.length) return text; const uuids = [...new Set(matches.map(m => m[1]))]; const idMap = {}; - allObjects.forEach(obj => { + (allObjects || []).forEach(obj => { if (uuids.includes(obj.id)) { const label = (obj._words || []).slice(0, 2).map(w => w.titel_de).filter(Boolean).join('/') || 'Objekt'; idMap[obj.id] = label; @@ -46,6 +53,15 @@ async function resolvePlaceholders(text, allObjects) { return text.replace(/\{\{([^}]+)\}\}/g, (_, id) => idMap[id] || id); } +// ─── Fuzzy word matching ────────────────────────────────────────────────────── + +function fuzzyMatch(token, wordTitle, threshold = 0.6) { + const t = token.toLowerCase(); + const w = wordTitle.toLowerCase(); + if (!t.startsWith(w) && !w.startsWith(t)) return false; + return Math.min(t.length, w.length) / Math.max(t.length, w.length) >= threshold; +} + // ─── HighlightedTextarea ────────────────────────────────────────────────────── function HighlightedTextarea({ value, onChange, wordMap, rows = 2, placeholder, onSelectionChange }) { @@ -161,12 +177,15 @@ function PairForm({ objectId, allObjects, onPairSaved }) { const [wordMap, setWordMap] = useState({}); const [objectAssignments, setObjectAssignments] = useState({}); const [selection, setSelection] = useState(''); + const [wordInput, setWordInput] = useState(''); const [creatingWord, setCreatingWord] = useState(false); const [yesNoAnswer, setYesNoAnswer] = useState(null); const [positiveWords, setPositiveWords] = useState([]); const [negativeWords, setNegativeWords] = useState([]); const [saving, setSaving] = useState(false); + useEffect(() => { setWordInput(selection); }, [selection]); + const allText = `${question} ${positive} ${negative}`; useEffect(() => { if (!allText.trim()) { setWordMap({}); return; } @@ -174,8 +193,12 @@ function PairForm({ objectId, allObjects, onPairSaved }) { const tokens = [...new Set(allText.split(/[\s.,!?;:()\[\]"']+/).filter(w => w.length >= 2))]; if (!tokens.length) return; const results = await Promise.allSettled( - tokens.map(w => apiFetch(`/words?titel_${lang}=${encodeURIComponent(w)}&limit=1`) - .then(d => Array.isArray(d) && d.length ? { key: w.toLowerCase(), word: d[0] } : null)) + tokens.map(async w => { + const data = await apiFetch(`/words?search=${encodeURIComponent(w)}&limit=5`); + const candidates = Array.isArray(data) ? data : []; + const match = candidates.find(word => fuzzyMatch(w, word[`titel_${lang}`] || word.titel_de || '')); + return match ? { key: w.toLowerCase(), word: match } : null; + }) ); const map = {}; results.forEach(r => { if (r.status === 'fulfilled' && r.value) map[r.value.key] = r.value.word; }); @@ -185,11 +208,12 @@ function PairForm({ objectId, allObjects, onPairSaved }) { }, [allText, lang]); async function handleCreateWord() { - if (!selection.trim()) return; + if (!wordInput.trim()) return; setCreatingWord(true); try { - const w = await apiPost('/words', { [`titel_${lang}`]: selection.trim() }); + const w = await apiPost('/words', { [`titel_${lang}`]: wordInput.trim() }); setWordMap(prev => ({ ...prev, [selection.trim().toLowerCase()]: w })); + setWordInput(''); } catch (e) { alert('Fehler: ' + e.message); } finally { setCreatingWord(false); } } @@ -273,11 +297,18 @@ function PairForm({ objectId, allObjects, onPairSaved }) { {type && (