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 => ( insertAtCursor(chip.objectId, chip.wordId)} + onClick={() => insertAtCursor(chip)} style={{ padding: '2px 7px', borderRadius: 'var(--r-full)', fontSize: 11, border: '1px solid #93c5fd', cursor: 'pointer', @@ -137,7 +171,7 @@ function PairForm({ objectId, token, objectChips, onSaved, onCancel }: PairFormP onChange={e => setQuestionDe(e.target.value)} onFocus={() => { lastFocusedRef.current = 'question' }} rows={2} - placeholder="question_de…" + placeholder="z.B. Was ist das? oder: Was macht {1.Hund}?" style={{ width: '100%', resize: 'vertical', padding: '6px 8px', borderRadius: 'var(--r-sm)', border: '1px solid var(--border)', @@ -145,11 +179,7 @@ function PairForm({ objectId, token, objectChips, onSaved, onCancel }: PairFormP fontFamily: 'var(--font)', fontSize: 12, boxSizing: 'border-box', }} /> - {questionDe && ( - - {resolveTemplate(questionDe)} - - )} + {questionDe && } {/* 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 => ( insertAtCursor(chip.objectId, chip.wordId)} + onClick={() => insertAtCursor(chip)} style={{ padding: '2px 7px', borderRadius: 'var(--r-full)', fontSize: 11, border: '1px solid #93c5fd', cursor: 'pointer', @@ -325,11 +348,7 @@ function PairsList({ pairs, loading, objectId, token, objectChips, onRefresh }: placeholder="Aussage (Pflicht)…" style={{ width: '100%', padding: '5px 8px', borderRadius: 'var(--r-sm)', border: '1px solid var(--border)', background: 'var(--surface-2)', color: 'var(--text-1)', fontSize: 12, resize: 'vertical', boxSizing: 'border-box' }} /> - {editStatement && ( - - {resolveTemplate(editStatement)} - - )} + {editStatement && } - {editQuestion && ( - - {resolveTemplate(editQuestion)} - - )} + {editQuestion && } @@ -371,12 +386,14 @@ function PairsList({ pairs, loading, objectId, token, objectChips, onRefresh }: {pair.statements.map(s => ( - Aussage:{resolveTemplate(s.statement_de)} + Aussage: + ))} {pair.questions.map(q => ( - - Frage:{resolveTemplate(q.question_de)} + + Frage: + ))} >
- {resolveTemplate(questionDe)} -
- {resolveTemplate(statementDe)} -
- {resolveTemplate(editStatement)} -
- {resolveTemplate(editQuestion)} -