// v2 FINAL stage — one-pager preview + revision chat with model picker
const V2_FINAL_MODELS = [
{id:'nemotron',name:'Nemotron 3 Nano Omni', tag:'NVIDIA · multimodal', dot:'#76B900'},
{id:'claude', name:'Claude Opus 4.7', tag:'Anthropic', dot:'#006747'},
{id:'gemini', name:'Gemini 3.1 Pro', tag:'Google', dot:'#4285F4'},
{id:'gpt', name:'GPT-5', tag:'OpenAI', dot:'#10A37F'},
{id:'omni', name:'Palette Omni', tag:'Hybrid', dot:'#1A1A1A'},
];
const V2FinalStage = ({palette, projectId, pinnedIds, onExportDeck, onExportMarkdown}) => {
const map = {};
(palette.blocks || []).forEach(b => (b.keywords || []).forEach(k => map[k.id] = k));
const pinned = pinnedIds.map(id => map[id]).filter(Boolean);
const headlineKw = pinned.find(k => k.type === 'METAPHOR') || pinned[0];
const [exporting, setExporting] = React.useState(null); // 'md' | 'deck' | null
const exportMd = async () => {
setExporting('md');
try { await onExportMarkdown?.(); } finally { setExporting(null); }
};
const exportDeck = async () => {
setExporting('deck');
try { await onExportDeck?.(); } finally { setExporting(null); }
};
return (
최종 산출물
{palette.project?.name} · v3 draft
COPY · ONE-PAGER PREVIEW
{headlineKw ? (
<>
{headlineKw.ko}
{headlineKw.en}
>
) : (
아직 핀된 키워드가 없습니다
)}
{palette.project?.brief || '프로젝트 브리프가 없습니다. Data 단계에서 작성해주세요.'}
BASED ON
{pinned.slice(0, 6).map(k => (
· {k.ko || k.en}
))}
);
};
const V2FinalChat = ({palette, projectId, pinnedIds}) => {
const [model, setModel] = React.useState(V2_FINAL_MODELS[0]);
const [pickerOpen, setPickerOpen] = React.useState(false);
const [messages, setMessages] = React.useState([
{role:'ai', text:'Final 덱의 카피·구조·톤을 자유롭게 다듬어 드릴 수 있어요. 무엇을 바꿔볼까요?', hint:true, model: V2_FINAL_MODELS[0].id},
]);
const [draft, setDraft] = React.useState('');
const [thinking, setThinking] = React.useState(false);
const send = async () => {
const v = draft.trim(); if (!v || thinking) return;
const userMsg = {role:'user', text: v};
const next = [...messages.filter(m => !m.hint), userMsg];
setMessages([{role:'ai', text:'Final 덱의 카피·구조·톤을 자유롭게 다듬어 드릴 수 있어요. 무엇을 바꿔볼까요?', hint:true, model: model.id}, ...next]);
setDraft(''); setThinking(true);
try {
const res = await window.API.brainstorm(projectId, model.id, next.map(m => ({me: m.role==='user', text: m.text})));
setMessages(prev => [...prev, {role:'ai', text: res.reply, items: extractList(res.reply), kws: res.kws || [], model: model.id}]);
} catch(e) {
setMessages(prev => [...prev, {role:'ai', text:'에러: '+e.message, model: model.id}]);
} finally { setThinking(false); }
};
const presets = ['더 짧게','시제 바꿔','감정 톤↑','영문 버전','한 줄 요약','전체 흐름 점검'];
return (
{pickerOpen && (
{V2_FINAL_MODELS.map(m => (
))}
)}
FINAL · CHAT
{messages.map((m, i) => {
const mdl = V2_FINAL_MODELS.find(x => x.id === m.model) || model;
return (
{m.role==='user' ? 'YOU' : <>{mdl.name.toUpperCase()}>}
{m.text}
{(m.items || []).length > 0 && (
{m.items.map((it,j) => (
-
{it}
))}
)}
);
})}
{thinking && (
)}
{presets.map(p => (
))}
);
};
const extractList = (text) => {
if (!text) return [];
const lines = text.split('\n').filter(l => /^\s*([-*•·]|\d+\.)\s+/.test(l));
return lines.map(l => l.replace(/^\s*([-*•·]|\d+\.)\s+/, '').trim()).slice(0, 6);
};
window.V2FinalStage = V2FinalStage;