feat: object assignment per word placeholder in statements
- withPlaceholders(text, wordMap, objectAssignments) — uses {{objectId}}
when an object is assigned to a word, otherwise {{wordId}}
- PairForm: objectAssignments state (carried over between saves)
- Detected words section shows object dropdown per word
— only objects from current picture that have the word linked
— shows "Objekt #N — word1, word2" labels
— unselected = {{wordId}}, selected = {{objectId}}
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -20,14 +20,16 @@ function tokenize(text, wordMap) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Replace matched words with {{uuid}} placeholders */
|
/** Replace matched words with {{uuid}} placeholders */
|
||||||
function withPlaceholders(text, wordMap) {
|
/** Replace matched words with {{objectId}} or {{wordId}} placeholders */
|
||||||
|
function withPlaceholders(text, wordMap, objectAssignments = {}) {
|
||||||
if (!text) return '';
|
if (!text) return '';
|
||||||
let result = text;
|
let result = text;
|
||||||
Object.entries(wordMap)
|
Object.entries(wordMap)
|
||||||
.sort((a, b) => b[0].length - a[0].length)
|
.sort((a, b) => b[0].length - a[0].length)
|
||||||
.forEach(([title, w]) => {
|
.forEach(([title, w]) => {
|
||||||
const re = new RegExp(`\\b${title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'gi');
|
const re = new RegExp(`\\b${title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'gi');
|
||||||
result = result.replace(re, `{{${w.id}}}`);
|
const id = objectAssignments[w.id] || w.id;
|
||||||
|
result = result.replace(re, `{{${id}}}`);
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -163,7 +165,7 @@ const PAIR_TYPES = [
|
|||||||
{ value: 'word', label: 'Wort', hint: 'Frage + Positive/Negative Wörter' },
|
{ value: 'word', label: 'Wort', hint: 'Frage + Positive/Negative Wörter' },
|
||||||
];
|
];
|
||||||
|
|
||||||
function PairForm({ objectId, onPairSaved }) {
|
function PairForm({ objectId, allObjects = [], onPairSaved }) {
|
||||||
const lang = getUserLang();
|
const lang = getUserLang();
|
||||||
const [showForm, setShowForm] = useState(false);
|
const [showForm, setShowForm] = useState(false);
|
||||||
const [savedFlash, setSavedFlash] = useState(false);
|
const [savedFlash, setSavedFlash] = useState(false);
|
||||||
@@ -182,6 +184,9 @@ function PairForm({ objectId, onPairSaved }) {
|
|||||||
// Yes/No answer
|
// Yes/No answer
|
||||||
const [yesNoAnswer, setYesNoAnswer] = useState(null);
|
const [yesNoAnswer, setYesNoAnswer] = useState(null);
|
||||||
|
|
||||||
|
// Object assignments per word: { wordId → objectId } — carried over between saves
|
||||||
|
const [objectAssignments, setObjectAssignments] = useState({});
|
||||||
|
|
||||||
// Word pickers — carried over between saves
|
// Word pickers — carried over between saves
|
||||||
const [positiveWords, setPositiveWords] = useState([]);
|
const [positiveWords, setPositiveWords] = useState([]);
|
||||||
const [negativeWords, setNegativeWords] = useState([]);
|
const [negativeWords, setNegativeWords] = useState([]);
|
||||||
@@ -246,7 +251,7 @@ function PairForm({ objectId, onPairSaved }) {
|
|||||||
let questionId = null;
|
let questionId = null;
|
||||||
if (type !== 'text' && question.trim()) {
|
if (type !== 'text' && question.trim()) {
|
||||||
const q = await apiPost('/questions', {
|
const q = await apiPost('/questions', {
|
||||||
[`sentence_${lang}`]: withPlaceholders(question, wordMap),
|
[`sentence_${lang}`]: withPlaceholders(question, wordMap, objectAssignments),
|
||||||
status: 'draft',
|
status: 'draft',
|
||||||
});
|
});
|
||||||
questionId = q.id;
|
questionId = q.id;
|
||||||
@@ -257,7 +262,7 @@ function PairForm({ objectId, onPairSaved }) {
|
|||||||
if (type === 'text' || type === 'question') {
|
if (type === 'text' || type === 'question') {
|
||||||
const posBody = { status: 'draft' };
|
const posBody = { status: 'draft' };
|
||||||
if (positive.trim())
|
if (positive.trim())
|
||||||
posBody[`positive_sentence_${lang}`] = withPlaceholders(positive, wordMap);
|
posBody[`positive_sentence_${lang}`] = withPlaceholders(positive, wordMap, objectAssignments);
|
||||||
const s = await apiPost('/statements', posBody);
|
const s = await apiPost('/statements', posBody);
|
||||||
posStmtId = s.id;
|
posStmtId = s.id;
|
||||||
} else if (type === 'yes_no') {
|
} else if (type === 'yes_no') {
|
||||||
@@ -274,7 +279,7 @@ function PairForm({ objectId, onPairSaved }) {
|
|||||||
// Negative statement
|
// Negative statement
|
||||||
let negStmtId = null;
|
let negStmtId = null;
|
||||||
if (type === 'question' && negative.trim()) {
|
if (type === 'question' && negative.trim()) {
|
||||||
const negBody = { status: 'draft', [`negative_sentence_${lang}`]: withPlaceholders(negative, wordMap) };
|
const negBody = { status: 'draft', [`negative_sentence_${lang}`]: withPlaceholders(negative, wordMap, objectAssignments) };
|
||||||
const s = await apiPost('/statements', negBody);
|
const s = await apiPost('/statements', negBody);
|
||||||
negStmtId = s.id;
|
negStmtId = s.id;
|
||||||
} else if (type === 'word' && negativeWords.length) {
|
} else if (type === 'word' && negativeWords.length) {
|
||||||
@@ -462,13 +467,45 @@ function PairForm({ objectId, onPairSaved }) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Detected words indicator — shown for all types */}
|
{/* Detected words — with optional object assignment per word */}
|
||||||
{Object.keys(wordMap).length > 0 && (
|
{Object.keys(wordMap).length > 0 && (
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="space-y-1.5 pt-1">
|
||||||
<span className="text-xs text-slate-400 w-full">Erkannte Wörter:</span>
|
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wide">Erkannte Wörter</span>
|
||||||
{Object.entries(wordMap).map(([title, w]) => (
|
{Object.entries(wordMap).map(([title, w]) => {
|
||||||
<span key={w.id} className="text-xs bg-indigo-100 text-indigo-700 rounded px-1.5 py-0.5">{title}</span>
|
const matchingObjs = allObjects.filter(o => o._words?.some(ow => ow.id === w.id));
|
||||||
))}
|
const assigned = objectAssignments[w.id] || '';
|
||||||
|
return (
|
||||||
|
<div key={w.id} className="flex items-center gap-2">
|
||||||
|
<span className="text-xs bg-indigo-100 text-indigo-700 rounded px-2 py-0.5 font-medium shrink-0">
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
{matchingObjs.length > 0 ? (
|
||||||
|
<select
|
||||||
|
value={assigned}
|
||||||
|
onChange={e => setObjectAssignments(prev => ({
|
||||||
|
...prev,
|
||||||
|
[w.id]: e.target.value || null,
|
||||||
|
}))}
|
||||||
|
className={`flex-1 text-xs border rounded px-1.5 py-0.5 bg-white focus:outline-none focus:ring-1 focus:ring-indigo-400
|
||||||
|
${assigned ? 'border-indigo-400 text-indigo-700 bg-indigo-50' : 'border-slate-200 text-slate-500'}`}
|
||||||
|
>
|
||||||
|
<option value="">— nur Wort ({{wordId}})</option>
|
||||||
|
{matchingObjs.map(obj => {
|
||||||
|
const idx = allObjects.indexOf(obj);
|
||||||
|
const wordLabels = (obj._words || []).slice(0, 3).map(ow => ow.titel_de || ow.id).join(', ');
|
||||||
|
return (
|
||||||
|
<option key={obj.id} value={obj.id}>
|
||||||
|
Objekt #{idx + 1}{wordLabels ? ` — ${wordLabels}` : ''}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</select>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs text-slate-300 italic">kein Objekt im Bild</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -494,7 +531,7 @@ function PairForm({ objectId, onPairSaved }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ─── PairsPanel (right 2/5) ───────────────────────────────────────────────────
|
// ─── PairsPanel (right 2/5) ───────────────────────────────────────────────────
|
||||||
function PairsPanel({ selectedObject, objectPairs, loadingPairs, onPairSaved }) {
|
function PairsPanel({ selectedObject, allObjects, objectPairs, loadingPairs, onPairSaved }) {
|
||||||
const lang = getUserLang();
|
const lang = getUserLang();
|
||||||
|
|
||||||
if (!selectedObject) {
|
if (!selectedObject) {
|
||||||
@@ -520,6 +557,7 @@ function PairsPanel({ selectedObject, objectPairs, loadingPairs, onPairSaved })
|
|||||||
<div className="p-3 border-b border-slate-100">
|
<div className="p-3 border-b border-slate-100">
|
||||||
<PairForm
|
<PairForm
|
||||||
objectId={selectedObject.id}
|
objectId={selectedObject.id}
|
||||||
|
allObjects={allObjects}
|
||||||
onPairSaved={onPairSaved}
|
onPairSaved={onPairSaved}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -828,6 +866,7 @@ export default function StatementCreation() {
|
|||||||
|
|
||||||
<PairsPanel
|
<PairsPanel
|
||||||
selectedObject={selectedObjectWithIndex}
|
selectedObject={selectedObjectWithIndex}
|
||||||
|
allObjects={objects}
|
||||||
objectPairs={objectPairs}
|
objectPairs={objectPairs}
|
||||||
loadingPairs={loadingPairs}
|
loadingPairs={loadingPairs}
|
||||||
onPairSaved={handlePairSaved}
|
onPairSaved={handlePairSaved}
|
||||||
|
|||||||
Reference in New Issue
Block a user