// Column 2 — Analysis blocks. Per-block editable cards for the selected file. const analysisStyles = { col: { background:'var(--paper)', display:'flex', flexDirection:'column', minHeight:0, borderRight:'1px solid var(--line)' }, head: { padding:'16px 28px 14px 28px', borderBottom:'1px solid var(--line)', display:'flex', alignItems:'flex-end', gap:20, justifyContent:'space-between' }, eyebrow: { fontFamily:'var(--mono)', fontSize:10, letterSpacing:'.16em', color:'var(--ink-3)', textTransform:'uppercase' }, title: { fontFamily:'var(--serif)', fontSize:22, lineHeight:1.1, marginTop:4, color:'var(--ink)' }, sub: { fontSize:11.5, color:'var(--ink-3)', marginTop:4, maxWidth:540 }, rightHead: { display:'flex', gap:6 }, pill: { height:26, padding:'0 10px', border:'1px solid var(--line-2)', borderRadius:999, background:'var(--paper)', fontSize:11, color:'var(--ink-2)', display:'inline-flex', alignItems:'center', gap:6, cursor:'pointer' }, pillActive: { background:'var(--ink)', color:'var(--paper)', border:'1px solid var(--ink)' }, scroll: { overflowY:'auto', flex:1, padding:'20px 28px 160px 28px' }, fileCard: { border:'1px solid var(--line)', borderRadius:10, background:'var(--paper)', marginBottom:24, overflow:'hidden' }, fileHead: { display:'grid', gridTemplateColumns:'auto 1fr auto', columnGap:14, alignItems:'center', padding:'14px 18px', borderBottom:'1px solid var(--line)', background:'var(--paper-2)' }, fileIcon: { width:34, height:34, borderRadius:6, background:'var(--paper)', border:'1px solid var(--line-2)', display:'flex', alignItems:'center', justifyContent:'center', color:'var(--ink-2)' }, fileTitle: { fontFamily:'var(--serif)', fontSize:16, color:'var(--ink)', lineHeight:1.2 }, fileSub: { fontFamily:'var(--mono)', fontSize:10, color:'var(--ink-3)', marginTop:3, letterSpacing:'.06em', textTransform:'uppercase' }, actions: { display:'flex', gap:6 }, fileBody: { padding:'14px 22px 18px 22px' }, block: { borderTop:'1px dashed var(--line)', padding:'16px 0 4px 0' }, blockFirst: { borderTop:'none', paddingTop:4 }, blockHead: { display:'flex', alignItems:'baseline', justifyContent:'space-between', gap:12, marginBottom:8 }, blockTitle: { fontFamily:'var(--serif)', fontSize:14, color:'var(--ink)', fontWeight:500 }, blockMeta: { fontFamily:'var(--mono)', fontSize:10, color:'var(--ink-4)', letterSpacing:'.08em', textTransform:'uppercase' }, summary: { fontFamily:'var(--display)', fontSize:15.5, lineHeight:1.5, color:'var(--ink)', fontStyle:'italic', fontWeight:400, cursor:'text', padding:'8px 12px', margin:'0 -12px', borderRadius:8, minHeight:46, borderWidth:1, borderStyle:'solid', borderColor:'transparent', transition:'background .15s, border-color .15s', position:'relative', }, summaryEditing: { fontFamily:'var(--display)', fontSize:15.5, lineHeight:1.5, color:'var(--ink)', fontStyle:'italic', fontWeight:400, width:'calc(100% + 24px)', minHeight:80, border:'1px solid var(--accent)', borderRadius:8, padding:'8px 12px', margin:'0 -12px', background:'var(--hl)', resize:'vertical', outline:'none' }, summaryHint: { position:'absolute', top:4, right:8, fontFamily:'var(--mono)', fontSize:9, letterSpacing:'.1em', color:'var(--ink-4)', textTransform:'uppercase', opacity:0, transition:'opacity .15s', }, editCommitRow: { display:'flex', gap:8, justifyContent:'flex-end', marginTop:6 }, editBtn: { fontFamily:'var(--mono)', fontSize:10, letterSpacing:'.1em', textTransform:'uppercase', padding:'5px 10px', borderRadius:999, cursor:'pointer' }, kwRow: { display:'flex', flexWrap:'wrap', gap:8, marginTop:10 }, kw: (drag, editing) => ({ display:'inline-flex', alignItems:'stretch', borderRadius:999, overflow:'hidden', border: editing ? '1px solid var(--accent)' : '1px solid var(--line-2)', background: editing ? 'var(--hl)' : 'var(--paper)', cursor: drag ? 'grabbing' : 'pointer', userSelect:'none', boxShadow: drag ? '0 8px 20px rgba(0,0,0,.08)' : 'none', transform: drag ? 'scale(1.03)' : 'none', transition:'transform .14s, box-shadow .14s', }), kwDrag: { padding:'0 6px 0 8px', display:'flex', alignItems:'center', color:'var(--ink-4)' }, kwMain: { padding:'6px 10px', display:'flex', alignItems:'center', gap:8 }, kwEn: { fontSize:12.5, color:'var(--ink)', fontWeight:500 }, kwKo: { fontFamily:'var(--display)', fontSize:13, color:'var(--ink-2)', fontStyle:'italic' }, heat: { width:28, height:3, borderRadius:2, background:'var(--line)', overflow:'hidden', position:'relative' }, heatFill: (v) => ({ position:'absolute', inset:0, width:`${v*100}%`, background:'var(--accent)' }), kwAdd: { padding:'0 9px', display:'flex', alignItems:'center', borderLeft:'1px solid var(--line)', color:'var(--ink-3)', cursor:'pointer' }, kwDel: { padding:'0 9px', display:'flex', alignItems:'center', borderLeft:'1px solid var(--line)', color:'var(--ink-4)', cursor:'pointer' }, addKwBtn: { marginTop:8, padding:'4px 10px', border:'1px dashed var(--line-2)', borderRadius:999, background:'transparent', fontSize:11, color:'var(--ink-3)', cursor:'pointer', fontFamily:'var(--mono)', letterSpacing:'.06em' }, tierTag: (t) => ({ fontFamily:'var(--mono)', fontSize:9, letterSpacing:'.12em', textTransform:'uppercase', color: t==='metaphor' ? 'var(--accent)' : t==='universal' ? 'var(--accent-3)' : 'var(--ink-4)', marginRight:4, }), emptyBlock: { border:'1px dashed var(--line-2)', borderRadius:10, padding:'40px 28px', textAlign:'center', color:'var(--ink-3)', fontFamily:'var(--display)', fontSize:18, fontStyle:'italic', }, }; const mutateStyles = { overlay: { position:'fixed', inset:0, background:'rgba(0,0,0,.35)', zIndex:60, display:'flex', alignItems:'center', justifyContent:'center' }, panel: { width:520, maxHeight:'78vh', background:'var(--paper)', borderRadius:14, boxShadow:'0 30px 60px rgba(0,0,0,.22)', display:'flex', flexDirection:'column', overflow:'hidden' }, head: { padding:'16px 20px 12px 20px', borderBottom:'1px solid var(--line)' }, headRow: { display:'flex', alignItems:'center', gap:10, justifyContent:'space-between' }, word: { fontFamily:'var(--serif)', fontSize:20, color:'var(--ink)' }, wordKo: { fontFamily:'var(--display)', fontSize:15, color:'var(--ink-2)', fontStyle:'italic', marginLeft:6 }, source: { fontFamily:'var(--mono)', fontSize:10, color:'var(--ink-4)', letterSpacing:'.08em', textTransform:'uppercase', marginTop:4 }, actions: { display:'flex', gap:6, padding:'12px 20px', borderBottom:'1px solid var(--line)', background:'var(--paper-2)' }, actionBtn: (active) => ({ height:28, padding:'0 12px', border: active ? '1px solid var(--ink)' : '1px solid var(--line-2)', background: active ? 'var(--ink)' : 'var(--paper)', color: active ? 'var(--paper)' : 'var(--ink)', borderRadius:999, fontSize:11.5, fontFamily:'var(--sans)', cursor:'pointer', display:'inline-flex', alignItems:'center', gap:4, }), body: { flex:1, overflowY:'auto', padding:'14px 20px', minHeight:120 }, msg: (me) => ({ margin:'8px 0', padding:'10px 12px', borderRadius:10, background: me ? 'var(--ink)' : 'var(--paper-2)', color: me ? 'var(--paper)' : 'var(--ink)', fontSize:12.5, lineHeight:1.55, maxWidth:'92%', alignSelf: me ? 'flex-end' : 'flex-start', fontFamily: me ? 'var(--sans)' : 'var(--serif)', }), msgWrap: { display:'flex', flexDirection:'column' }, sugRow: { display:'flex', flexWrap:'wrap', gap:8, marginTop:10 }, sug: { display:'inline-flex', alignItems:'stretch', borderRadius:999, border:'1px solid var(--line-2)', background:'var(--paper)', cursor:'pointer' }, sugMain: { padding:'6px 12px', display:'flex', alignItems:'center', gap:8 }, sugEn: { fontSize:12.5, color:'var(--ink)', fontWeight:500 }, sugKo: { fontFamily:'var(--display)', fontSize:13, color:'var(--ink-2)', fontStyle:'italic' }, sugAdd: { padding:'0 10px', display:'flex', alignItems:'center', borderLeft:'1px solid var(--line)', color:'var(--accent)' }, composer: { padding:'10px 14px', borderTop:'1px solid var(--line)', display:'flex', gap:8, alignItems:'center', background:'var(--paper)' }, input: { flex:1, border:'1px solid var(--line-2)', borderRadius:999, padding:'7px 14px', fontSize:12.5, fontFamily:'var(--sans)', background:'var(--paper-2)', outline:'none', color:'var(--ink)' }, send: { height:30, padding:'0 14px', border:'1px solid var(--ink)', background:'var(--ink)', color:'var(--paper)', borderRadius:999, fontSize:12, cursor:'pointer' }, closeBtn: { border:'none', background:'transparent', color:'var(--ink-3)', cursor:'pointer', fontFamily:'var(--mono)', fontSize:10, letterSpacing:'.14em' }, }; function KeywordMutatePopup({ file_id, block_id, kw, onClose, onAddKw, onReplaceKw }) { const [action, setAction] = React.useState('sharper'); const [input, setInput] = React.useState(''); const [busy, setBusy] = React.useState(false); const [messages, setMessages] = React.useState([]); // {me, text, kws?} const bodyRef = React.useRef(null); React.useEffect(() => { // Auto-fire initial mutate when action picked fresh (not on re-render). run(action, null); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); React.useEffect(() => { if (bodyRef.current) bodyRef.current.scrollTop = bodyRef.current.scrollHeight; }, [messages, busy]); const run = async (actionKey, freeText) => { setBusy(true); const userText = freeText || ({ sharper: '이 키워드, 더 날카롭게.', opposite: '이 키워드를 뒤집어봐.', similar: '비슷한 각도로 2~3개 더.', explain: '이 키워드가 왜 어울리거나 안 어울리는지 설명해줘.', }[actionKey] || '제안 부탁해.'); const nextMessages = [...messages, {me:true, text:userText}]; setMessages(nextMessages); try { const res = await API.mutateKeyword({ file_id, block_id, term: kw.ko || kw.w, ko: kw.ko || null, action: actionKey, conversation: nextMessages.map(m => ({me:m.me, text:m.text})), }); setMessages(m => [...m, {me:false, text:res.reply || '', kws: res.kws || []}]); } catch(e) { setMessages(m => [...m, {me:false, text:'⚠ ' + e.message}]); } finally { setBusy(false); } }; const onSend = () => { const t = input.trim(); if (!t) return; setInput(''); run(action, t); }; const onAction = (a) => { setAction(a); run(a, null); }; return (
e.stopPropagation()}>
{kw.w} {kw.ko && · {kw.ko}} {kw.tier && {kw.tier}}
Mini-chat · mutate this keyword
{[ ['sharper','Sharper ↑'], ['opposite','Opposite ⇄'], ['similar','Similar ≈'], ['explain','Explain ℹ'], ].map(([k,label]) => ( ))}
{messages.map((m,i) => (
{m.text || (busy && i===messages.length-1 ? '…' : '')}
{!m.me && m.kws && m.kws.length>0 && (
{m.kws.map((s,j) => (
onReplaceKw && onReplaceKw({...s, block_id, tier: s.tier || kw.tier})}> {s.tier && {s.tier}} {s.w} {s.ko && {s.ko}}
onAddKw({...s, from: file_id, tier: s.tier || kw.tier, block_id})}>
))}
)}
))} {busy &&
Thinking…
}
setInput(e.target.value)} onKeyDown={e => { if (e.key==='Enter') onSend(); }}/>
); } function KwChip({ kw, fileId, onDrag, onClick, onAdd, onDelete, editing }) { const [dragging, setDragging] = React.useState(false); return (
{ e.dataTransfer.setData('application/kw', JSON.stringify({...kw, from: fileId})); e.dataTransfer.effectAllowed = 'copy'; setDragging(true); }} onDragEnd={() => setDragging(false)} style={analysisStyles.kw(dragging, editing)} title={kw.why}>
{kw.tier && {kw.tier}} {kw.w} {kw.ko && {kw.ko}} {typeof kw.heat === 'number' && ( )}
{onDelete && (
×
)}
); } function EditableSummary({ value, onSave }) { const [editing, setEditing] = React.useState(false); const [hover, setHover] = React.useState(false); const [saving, setSaving] = React.useState(false); const [draft, setDraft] = React.useState(value || ''); const taRef = React.useRef(null); React.useEffect(() => { if (!editing) setDraft(value || ''); }, [value, editing]); const commit = async () => { const trimmed = draft.trim(); if (trimmed === (value || '').trim()) { setEditing(false); return; } setSaving(true); try { await onSave(trimmed); } finally { setSaving(false); setEditing(false); } }; const cancel = () => { setDraft(value || ''); setEditing(false); }; if (editing) { return (