From ceaa7eff3cccc4d6c85523f20c4b1620dafe53e2 Mon Sep 17 00:00:00 2001 From: admin Date: Sun, 10 May 2026 21:08:41 +0200 Subject: [PATCH] feat: lesbares Placeholder-Format {1.Hund} in Pair-Textfeldern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Textarea zeigt {1.Hund} (Index + Wort), Directus speichert {uuid.uuid} - displayToStorage / storageToDisplay Konvertierung bei Save und Edit - PreviewText-Komponente: Placeholder farblich hervorgehoben (blau) - Chip-Klick fügt {1.Hund} an Cursor-Position ein - startEdit konvertiert gespeicherte UUIDs zurück zu lesbarem Format Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/pages/GenerateIt.tsx | 121 +++++++++++++++++------------- 1 file changed, 69 insertions(+), 52 deletions(-) diff --git a/frontend/src/pages/GenerateIt.tsx b/frontend/src/pages/GenerateIt.tsx index 3e5e431..d097498 100644 --- a/frontend/src/pages/GenerateIt.tsx +++ b/frontend/src/pages/GenerateIt.tsx @@ -29,6 +29,44 @@ const ChevronRightIcon = () => ( ) +// ── Placeholder helpers ─────────────────────────────────────────────────────── + +// Display format: {1.Hund} ←→ Storage format: {objectUUID.wordUUID} + +function displayToStorage(text: string, chips: ObjectChip[]): string { + return text.replace(/\{(\d+)\.([^}]+)\}/g, (match, indexStr, label) => { + const chip = chips.find(c => c.objectIndex === parseInt(indexStr) && c.label === label) + return chip ? `{${chip.objectId}.${chip.wordId}}` : match + }) +} + +function storageToDisplay(text: string, chips: ObjectChip[]): string { + // UUIDs are long (36 chars with hyphens); display indexes are short numbers + return text.replace(/\{([0-9a-f-]{30,})\.([0-9a-f-]{30,})\}/g, (match, objectId, wordId) => { + const chip = chips.find(c => c.objectId === objectId && c.wordId === wordId) + return chip ? `{${chip.objectIndex}.${chip.label}}` : match + }) +} + +// Renders text with {index.label} placeholders highlighted in blue +function PreviewText({ text, chips }: { text: string; chips: ObjectChip[] }) { + const display = storageToDisplay(text, chips) + const parts = display.split(/(\{\d+\.[^}]+\})/g) + return ( + + {parts.map((part, i) => { + const m = part.match(/^\{(\d+)\.([^}]+)\}$/) + if (m) return ( + + {m[2]} + + ) + return {part} + })} + + ) +} + // ── PairForm ────────────────────────────────────────────────────────────────── interface PairFormProps { @@ -41,8 +79,8 @@ interface PairFormProps { function PairForm({ objectId, token, objectChips, onSaved, onCancel }: PairFormProps) { const [level, setLevel] = useState(50) - const [questionDe, setQuestionDe] = useState('') - const [statementDe, setStatementDe] = useState('') + const [questionDe, setQuestionDe] = useState('') // display format: {1.Hund} + const [statementDe, setStatementDe] = useState('') // display format: {1.Hund} const [saving, setSaving] = useState(false) const [error, setError] = useState('') @@ -50,19 +88,14 @@ function PairForm({ objectId, token, objectChips, onSaved, onCancel }: PairFormP const statementRef = useRef(null) const lastFocusedRef = useRef<'question' | 'statement'>('statement') - const resolveTemplate = (text: string): string => - text.replace(/\{([^.}]+)\.([^}]+)\}/g, (_, oid, wid) => { - const chip = objectChips.find(c => c.objectId === oid && c.wordId === wid) - return chip ? chip.label : '{?}' - }) - - const insertAtCursor = (oid: string, wordId: string) => { + // Insert display-format placeholder at cursor: {1.Hund} + const insertAtCursor = (chip: ObjectChip) => { const ref = lastFocusedRef.current === 'question' ? questionRef : statementRef const ta = ref.current if (!ta) return const start = ta.selectionStart ?? ta.value.length const end = ta.selectionEnd ?? ta.value.length - const placeholder = `{${oid}.${wordId}}` + const placeholder = `{${chip.objectIndex}.${chip.label}}` const newVal = ta.value.slice(0, start) + placeholder + ta.value.slice(end) if (lastFocusedRef.current === 'question') setQuestionDe(newVal) else setStatementDe(newVal) @@ -73,13 +106,14 @@ function PairForm({ objectId, token, objectChips, onSaved, onCancel }: PairFormP } const handleSave = async () => { - if (!statementDe.trim()) { setError('Aussage (statement_de) ist Pflicht.'); return } + if (!statementDe.trim()) { setError('Aussage ist Pflicht.'); return } setSaving(true) setError('') try { + // Convert display format → storage format (UUIDs) before saving await createDbPair(objectId, { - question_de: questionDe.trim() || undefined, - statement_de: statementDe.trim(), + question_de: questionDe.trim() ? displayToStorage(questionDe.trim(), objectChips) : undefined, + statement_de: displayToStorage(statementDe.trim(), objectChips), level, words: [], }, token) @@ -112,7 +146,7 @@ function PairForm({ objectId, token, objectChips, onSaved, onCancel }: PairFormP
{objectChips.map(chip => (
{/* Statement (required) */} @@ -163,7 +193,7 @@ function PairForm({ objectId, token, objectChips, onSaved, onCancel }: PairFormP onChange={e => setStatementDe(e.target.value)} onFocus={() => { lastFocusedRef.current = 'statement' }} rows={2} - placeholder="statement_de…" + placeholder="z.B. Das ist ein {1.Hund}." style={{ width: '100%', resize: 'vertical', padding: '6px 8px', borderRadius: 'var(--r-sm)', border: `1px solid ${statementDe.trim() ? 'var(--border)' : 'var(--danger)'}`, @@ -171,11 +201,7 @@ function PairForm({ objectId, token, objectChips, onSaved, onCancel }: PairFormP fontFamily: 'var(--font)', fontSize: 12, boxSizing: 'border-box', }} /> - {statementDe && ( -

- {resolveTemplate(statementDe)} -

- )} + {statementDe &&
} {error &&
{error}
} @@ -218,19 +244,14 @@ function PairsList({ pairs, loading, objectId, token, objectChips, onRefresh }: const editStatementRef = useRef(null) const editLastFocusedRef = useRef<'question' | 'statement'>('statement') - const resolveTemplate = (text: string): string => - text.replace(/\{([^.}]+)\.([^}]+)\}/g, (_, oid, wid) => { - const chip = objectChips.find(c => c.objectId === oid && c.wordId === wid) - return chip ? chip.label : '{?}' - }) - - const insertAtCursor = (oid: string, wordId: string) => { + // Insert display-format placeholder at cursor: {1.Hund} + const insertAtCursor = (chip: ObjectChip) => { const ref = editLastFocusedRef.current === 'question' ? editQuestionRef : editStatementRef const ta = ref.current if (!ta) return const start = ta.selectionStart ?? ta.value.length const end = ta.selectionEnd ?? ta.value.length - const placeholder = `{${oid}.${wordId}}` + const placeholder = `{${chip.objectIndex}.${chip.label}}` const newVal = ta.value.slice(0, start) + placeholder + ta.value.slice(end) if (editLastFocusedRef.current === 'question') setEditQuestion(newVal) else setEditStatement(newVal) @@ -243,8 +264,9 @@ function PairsList({ pairs, loading, objectId, token, objectChips, onRefresh }: const startEdit = (pair: DbPair) => { setEditingId(pair.id) setEditLevel(pair.level) - setEditStatement(pair.statements[0]?.statement_de ?? '') - setEditQuestion(pair.questions[0]?.question_de ?? '') + // Convert storage format (UUIDs) → display format ({1.Hund}) for editing + setEditStatement(storageToDisplay(pair.statements[0]?.statement_de ?? '', objectChips)) + setEditQuestion(storageToDisplay(pair.questions[0]?.question_de ?? '', objectChips)) } const saveEdit = async () => { @@ -253,8 +275,9 @@ function PairsList({ pairs, loading, objectId, token, objectChips, onRefresh }: try { await updateDbPair(editingId, { level: editLevel, - statement_de: editStatement, - question_de: editQuestion, + // Convert display format → storage format (UUIDs) before saving + statement_de: displayToStorage(editStatement, objectChips), + question_de: editQuestion.trim() ? displayToStorage(editQuestion, objectChips) : '', }, token) setEditingId(null) onRefresh() @@ -302,7 +325,7 @@ function PairsList({ pairs, loading, objectId, token, objectChips, onRefresh }:
{objectChips.map(chip => (