MUTATE · MINI-CHAT
on
{kw.en} · {kw.ko}
type: {kw.type}{kw.gloss?` · ${kw.gloss}`:''}
{messages.map((m,i) => {
if (m.role === 'user') {
return (
{m.text}
);
}
if (m.role === 'ai-greeting') {
return (
);
}
return (
{m.explainer}
{(m.candidates || []).length > 0 && (
{m.candidates.map((c, j) => (
{(c.tier||c.type||'').toUpperCase()}
{c.w || c.en}
{c.ko}
{(c.why || c.gloss) &&
"{c.why || c.gloss}"
}
))}
)}
);
})}
{thinking && (
)}
{messages.filter(m => m.role==='user').length === 0 && (
{QUICK.map(q => (
))}
)}
);
};
const V2AIAvatar = () => (
);
const V2Dot = ({delay}) => (
);
// === v3.4 view modes: By theme + By quote ===
const V2ThemeView = ({blocks, pinnedSet, onPin, onChipClick}) => {
// Group keywords by simple co-occurrence buckets — five tier-themed groups,
// each pulling that tier's keywords across all blocks.
const tiers = [
{key:'METAPHOR', num:'01', title:'한 줄의 은유', en:'Compressed metaphor'},
{key:'UNIVERSAL', num:'02', title:'보편적 가치', en:'Universal value'},
{key:'EMOTION', num:'03', title:'정서·긴장', en:'Affect'},
{key:'SENSORY', num:'04', title:'감각의 디테일', en:'Sensory'},
{key:'LITERAL', num:'05', title:'표면 단어', en:'Surface'},
];
const map = {};
blocks.forEach(b => (b.keywords||[]).forEach(k => map[k.id] = {...k, blockId: b.id, fileId: b.file}));
const groupedByTier = {};
Object.values(map).forEach(k => { (groupedByTier[k.type] ||= []).push(k); });
return (
{tiers.map(t => {
const kws = groupedByTier[t.key] || [];
if (!kws.length) return null;
return (
{t.num}
{t.title}
· {t.en}
{kws.length} KW
{kws.map(kw => (
onChipClick?.(kw)}
style={{outline: pinnedSet.has(kw.id) ? '2px solid var(--primary)' : 'none', outlineOffset:'1px'}}>
{kw.en}
·{kw.ko}
))}
);
})}
);
};
const V2QuoteView = ({blocks, pinnedSet, onPin, onChipClick}) => {
const TIERS = ['LITERAL','SENSORY','EMOTION','UNIVERSAL','METAPHOR'];
return (
ORIGINAL EXCERPT → DERIVED KEYWORDS · 원문에서 직접 길어 올린 키워드
{blocks.map((b, i) => (
№{String(i+1).padStart(2,'0')}
{b.title}
{b.timecode && · {b.timecode}}
"
{b.excerpt}
SUMMARY
{b.summary}
KEYWORDS DERIVED
{(b.keywords||[]).length} found
{TIERS.map(t => {
const kws = (b.keywords||[]).filter(k => k.type === t);
if (!kws.length) return null;
return (
{t}
{kws.map(k => (
onChipClick?.({...k, blockId: b.id, fileId: b.file})}
style={{outline: pinnedSet.has(k.id) ? '2px solid var(--primary)' : 'none', outlineOffset:'1px'}}>
{k.en}
·{k.ko}
))}
);
})}
))}
);
};
const V2AnalysisStage = ({palette, project, pinnedIds, onPin, onUnpin, onSaveSummary, onAnalyzeFile, onGoIdeate}) => {
const [selectedFile, setSelectedFile] = React.useState((palette.files||[])[0]?.id);
const [boardMode, setBoardMode] = React.useState('list');
const [view, setView] = React.useState('per-file');
const [activeKw, setActiveKw] = React.useState(null);
const pinnedSet = React.useMemo(() => new Set(pinnedIds), [pinnedIds]);
const blocks = (palette.blocks || []).filter(b => !selectedFile || b.file === selectedFile);
const allBlocks = palette.blocks || [];
return (
{[['per-file','Per file'],['by-theme','By theme'],['by-quote','By quote']].map(([k,l])=>(
))}
{blocks.length === 0 && view === 'per-file' && (
이 파일은 아직 분석되지 않았습니다. 파일 카드의 Analyze 버튼을 눌러주세요.
)}
{view === 'per-file' && blocks.map(block => (
onPin?.(kw)}
onChipClick={kw => setActiveKw(kw)}
onIdeate={() => onGoIdeate?.()}
onSaveSummary={onSaveSummary}/>
))}
{view === 'by-theme' && (
onPin?.(kw)} onChipClick={kw => setActiveKw(kw)}/>
)}
{view === 'by-quote' && (
onPin?.(kw)} onChipClick={kw => setActiveKw(kw)}/>
)}
setActiveKw(kw)} mode={boardMode} setMode={setBoardMode}
onGoIdeate={onGoIdeate}/>
{activeKw && (
setActiveKw(null)}
onPin={c => onPin?.(c)}
pinnedSet={pinnedSet}/>
)}
);
};
window.V2AnalysisStage = V2AnalysisStage;