const { useMemo, useRef, useState } = React; const STAR = { exact: '⭐⭐⭐', tone_diff: '⭐⭐', near_sound: '⭐', }; function matchLabel(level) { return STAR[level] || '⭐'; } function makeMarkdown(text, segments) { const anchors = segments.filter((s) => s.is_anchor); const lines = [`# 文字雷達:${text}`, '']; for (const s of anchors) { lines.push(`## ${s.word}`); lines.push('### 🔊 諧音路徑'); if (!s.homophones?.length) { lines.push('- (無)'); } else { lines.push(`- ${s.word} ${s.pinyin}`); for (const h of s.homophones) { const dist = h.semantic_distance ? ` / 語義:${h.semantic_distance}` : ''; lines.push(` - ${h.word} ${h.pinyin} ${matchLabel(h.match_level)}${dist}`); } } lines.push(''); lines.push('### 📖 成語碰撞'); if (!s.idiom_hits?.length) { lines.push('- (無)'); } else { for (const i of s.idiom_hits) { lines.push(`- **${i.matched_char}** → ${i.idiom}${i.explanation ? `:${i.explanation}` : ''}`); } } lines.push(''); lines.push('### 🔪 拆字'); if (!s.char_split?.length) { lines.push('- (無)'); } else { for (const c of s.char_split) { lines.push(`- ${c.char} → ${(c.homophones || []).join('、') || '(無)'}`); } } lines.push(''); lines.push('---'); lines.push(''); } return lines.join('\n'); } function App() { const [text, setText] = useState('我在矽谷找工作,投了五十封履歷,結果只有三家回'); const [matchLevel, setMatchLevel] = useState('tone_ignored'); const [minFrequency, setMinFrequency] = useState(500); const [maxHomophones, setMaxHomophones] = useState(10); const [result, setResult] = useState(null); const [selected, setSelected] = useState(null); const [showAllIdioms, setShowAllIdioms] = useState(false); const [scanning, setScanning] = useState(false); const rightPanelRef = useRef(null); const segments = result?.segments || []; const anchors = useMemo(() => segments.filter((s) => s.is_anchor), [segments]); function selectAnchor(seg) { setSelected(seg); setShowAllIdioms(false); if (rightPanelRef.current) { rightPanelRef.current.scrollTop = 0; } } async function runScan() { setScanning(true); try { const resp = await fetch('/api/scan', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text, config: { match_level: matchLevel, min_frequency: Number(minFrequency), max_homophones: Number(maxHomophones), include_idioms: true, include_split: true, }, }), }); const data = await resp.json(); setResult(data); const first = data.segments.find((s) => s.is_anchor); selectAnchor(first || null); } finally { setScanning(false); } } function renderTextWithAnchors() { if (!segments.length) return 先輸入文字並按「掃描」。; const nodes = []; let cursor = 0; segments.forEach((seg, i) => { const [start, end] = seg.position; if (start > cursor) { nodes.push({text.slice(cursor, start)}); } const content = text.slice(start, end); if (seg.is_anchor) { const active = selected?.position?.[0] === start && selected?.position?.[1] === end; nodes.push( selectAnchor(seg)} title={`POS: ${seg.pos || '-'}`} > {content} ); } else { nodes.push({content}); } cursor = end; }); if (cursor < text.length) { nodes.push({text.slice(cursor)}); } return nodes; } async function exportMarkdown() { if (!segments.length) return; const md = makeMarkdown(text, segments); await navigator.clipboard.writeText(md); alert('已複製 Markdown(可直接貼到 Obsidian)'); } const groupedHomophones = useMemo(() => { if (!selected?.homophones) return []; const groups = [ { level: 'exact', label: '⭐⭐⭐ 完全同音' }, { level: 'tone_diff', label: '⭐⭐ 同音異調' }, { level: 'near_sound', label: '⭐ 近音' }, ]; return groups .map((g) => ({ ...g, items: selected.homophones.filter((h) => h.match_level === g.level) })) .filter((g) => g.items.length > 0); }, [selected]); const idiomList = useMemo(() => { const list = selected?.idiom_hits || []; return showAllIdioms ? list : list.slice(0, 5); }, [selected, showAllIdioms]); return (