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 */
|
||||
function withPlaceholders(text, wordMap) {
|
||||
/** Replace matched words with {{objectId}} or {{wordId}} placeholders */
|
||||
function withPlaceholders(text, wordMap, objectAssignments = {}) {
|
||||
if (!text) return '';
|
||||
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, `{{${w.id}}}`);
|
||||
const id = objectAssignments[w.id] || w.id;
|
||||
result = result.replace(re, `{{${id}}}`);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
@@ -163,7 +165,7 @@ const PAIR_TYPES = [
|
||||
{ value: 'word', label: 'Wort', hint: 'Frage + Positive/Negative Wörter' },
|
||||
];
|
||||
|
||||
function PairForm({ objectId, onPairSaved }) {
|
||||
function PairForm({ objectId, allObjects = [], onPairSaved }) {
|
||||
const lang = getUserLang();
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [savedFlash, setSavedFlash] = useState(false);
|
||||
@@ -182,6 +184,9 @@ function PairForm({ objectId, onPairSaved }) {
|
||||
// Yes/No answer
|
||||
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
|
||||
const [positiveWords, setPositiveWords] = useState([]);
|
||||
const [negativeWords, setNegativeWords] = useState([]);
|
||||
@@ -246,7 +251,7 @@ function PairForm({ objectId, onPairSaved }) {
|
||||
let questionId = null;
|
||||
if (type !== 'text' && question.trim()) {
|
||||
const q = await apiPost('/questions', {
|
||||
[`sentence_${lang}`]: withPlaceholders(question, wordMap),
|
||||
[`sentence_${lang}`]: withPlaceholders(question, wordMap, objectAssignments),
|
||||
status: 'draft',
|
||||
});
|
||||
questionId = q.id;
|
||||
@@ -257,7 +262,7 @@ function PairForm({ objectId, onPairSaved }) {
|
||||
if (type === 'text' || type === 'question') {
|
||||
const posBody = { status: 'draft' };
|
||||
if (positive.trim())
|
||||
posBody[`positive_sentence_${lang}`] = withPlaceholders(positive, wordMap);
|
||||
posBody[`positive_sentence_${lang}`] = withPlaceholders(positive, wordMap, objectAssignments);
|
||||
const s = await apiPost('/statements', posBody);
|
||||
posStmtId = s.id;
|
||||
} else if (type === 'yes_no') {
|
||||
@@ -274,7 +279,7 @@ function PairForm({ objectId, onPairSaved }) {
|
||||
// Negative statement
|
||||
let negStmtId = null;
|
||||
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);
|
||||
negStmtId = s.id;
|
||||
} 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 && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<span className="text-xs text-slate-400 w-full">Erkannte Wörter:</span>
|
||||
{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>
|
||||
))}
|
||||
<div className="space-y-1.5 pt-1">
|
||||
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wide">Erkannte Wörter</span>
|
||||
{Object.entries(wordMap).map(([title, w]) => {
|
||||
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>
|
||||
@@ -494,7 +531,7 @@ function PairForm({ objectId, onPairSaved }) {
|
||||
}
|
||||
|
||||
// ─── PairsPanel (right 2/5) ───────────────────────────────────────────────────
|
||||
function PairsPanel({ selectedObject, objectPairs, loadingPairs, onPairSaved }) {
|
||||
function PairsPanel({ selectedObject, allObjects, objectPairs, loadingPairs, onPairSaved }) {
|
||||
const lang = getUserLang();
|
||||
|
||||
if (!selectedObject) {
|
||||
@@ -520,6 +557,7 @@ function PairsPanel({ selectedObject, objectPairs, loadingPairs, onPairSaved })
|
||||
<div className="p-3 border-b border-slate-100">
|
||||
<PairForm
|
||||
objectId={selectedObject.id}
|
||||
allObjects={allObjects}
|
||||
onPairSaved={onPairSaved}
|
||||
/>
|
||||
</div>
|
||||
@@ -828,6 +866,7 @@ export default function StatementCreation() {
|
||||
|
||||
<PairsPanel
|
||||
selectedObject={selectedObjectWithIndex}
|
||||
allObjects={objects}
|
||||
objectPairs={objectPairs}
|
||||
loadingPairs={loadingPairs}
|
||||
onPairSaved={handlePairSaved}
|
||||
|
||||
Reference in New Issue
Block a user