fix: robust placeholder resolution + readable pair cards

- resolvePlaceholders: replace fragile [wo] char class with general
  /^(.+?)\.[a-z]+:[0-9a-f-]{36}$/i — handles any type prefix safely
- Add sync stripPlaceholders() helper for card display (no async needed)
- PairsPanel cards now show actual sentence text instead of UUID stubs,
  using enriched pair.question / pair.positive_statement data from API

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 13:46:06 +02:00
parent f739f529e9
commit d57313084f

View File

@@ -31,26 +31,42 @@ function withPlaceholders(text, wordMap, objectAssignments = {}) {
return result;
}
// Sync helper — strips placeholders to their label for display (no async needed)
function stripPlaceholders(text) {
if (!text) return '';
return text.replace(/\{\{([^}]+)\}\}/g, (_, inner) => {
// New format: "label.type:uuid"
const m = inner.match(/^(.+?)\.[a-z]+:[0-9a-f-]{36}$/i);
return m ? m[1] : inner;
});
}
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 oldUuids = [];
// Single pass: handle both new and old format
let result = text.replace(/\{\{([^}]+)\}\}/g, (fullMatch, inner) => {
// New format: "label.type:uuid" — extract label directly, no lookup needed
const newFmt = inner.match(/^(.+?)\.[a-z]+:([0-9a-f-]{36})$/i);
if (newFmt) return newFmt[1];
// Old format: bare uuid — collect for async lookup
const oldFmt = inner.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
if (oldFmt) { oldUuids.push(inner); return fullMatch; }
return inner;
});
if (!oldUuids.length) return result;
// Resolve remaining bare UUIDs
const idMap = {};
(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;
if (oldUuids.includes(obj.id)) {
idMap[obj.id] = (obj._words || []).slice(0, 2).map(w => w.titel_de).filter(Boolean).join('/') || 'Objekt';
}
});
await Promise.all(uuids.filter(id => !idMap[id]).map(async id => {
await Promise.all(oldUuids.filter(id => !idMap[id]).map(async id => {
try { const w = await apiFetch(`/words/${id}`); if (w) idMap[id] = w.titel_de || w.titel_en || id; }
catch { idMap[id] = id; }
}));
return text.replace(/\{\{([^}]+)\}\}/g, (_, id) => idMap[id] || id);
return result.replace(/\{\{([^}]+)\}\}/g, (_, id) => idMap[id] || id);
}
// ─── Fuzzy word matching ──────────────────────────────────────────────────────
@@ -842,9 +858,24 @@ function PairsPanel({ selectedObject, allObjects, objectPairs, loadingPairs, onP
<button onClick={() => setEditingId(pair.id)}
className="ml-auto text-xs text-slate-400 hover:text-indigo-600 px-1.5 py-0.5 rounded hover:bg-indigo-50 transition-colors" title="Bearbeiten">✏️</button>
</div>
{pair.question_id && <p className="text-xs text-slate-500 font-mono opacity-60 truncate">F: {pair.question_id.slice(0,8)}…</p>}
{pair.positive_statement_id && <p className="text-xs text-green-600 font-mono opacity-60 truncate">+ {pair.positive_statement_id.slice(0,8)}…</p>}
{pair.negative_statement_id && <p className="text-xs text-red-500 font-mono opacity-60 truncate"> {pair.negative_statement_id.slice(0,8)}…</p>}
{pair.question && (
<p className="text-xs text-slate-600 truncate">
<span className="text-slate-400 mr-1">F:</span>
{stripPlaceholders(pair.question.sentence_de || pair.question.sentence_en || '')}
</p>
)}
{pair.positive_statement && (pair.positive_statement.positive_sentence_de || pair.positive_statement.positive_sentence_en) && (
<p className="text-xs text-green-700 truncate">
<span className="text-green-400 mr-1">+</span>
{stripPlaceholders(pair.positive_statement.positive_sentence_de || pair.positive_statement.positive_sentence_en || '')}
</p>
)}
{pair.negative_statement && (pair.negative_statement.negative_sentence_de || pair.negative_statement.negative_sentence_en) && (
<p className="text-xs text-red-500 truncate">
<span className="text-red-300 mr-1"></span>
{stripPlaceholders(pair.negative_statement.negative_sentence_de || pair.negative_statement.negative_sentence_en || '')}
</p>
)}
</div>
)}
</div>