// v2 IDEATE stage — parallel chat sessions + presets + saved seeds // Detect structured patterns in AI replies based on preset hint + heuristics. // Returns { kind: 'taglines' | 'ladder' | 'reasoning' | 'text', data: ... } const parseAIReply = (text, presetKey) => { if (!text) return {kind: 'text', data: null}; const lines = text.split('\n').map(l => l.trim()).filter(Boolean); // Taglines: lines like "1. KO COPY (en copy)" or "1) ..." — extract pairs if (presetKey === 'tagline' || /^\s*\d+[\.\)]/.test(lines[0])) { const items = []; for (const ln of lines) { const m = ln.match(/^\s*(\d+|\d+[a-z]?)[\.\):]\s*(.+)$/); if (!m) continue; const body = m[2]; const enMatch = body.match(/^(.+?)\s*[—\-/·]\s*([A-Za-z][^—\-/·]+)$/) || body.match(/^(.+?)\s*\(([^)]+)\)\s*$/); if (enMatch) items.push({n: m[1], ko: enMatch[1].trim().replace(/['"]/g,''), en: enMatch[2].trim().replace(/['"]/g,'')}); else items.push({n: m[1], ko: body.replace(/['"]/g,''), en: ''}); } if (items.length >= 2) return {kind: 'taglines', data: {items}}; } // Ladder: 5 tier labels in order (LITERAL/SENSORY/EMOTION/UNIVERSAL/METAPHOR) const TIERS = ['LITERAL','SENSORY','EMOTION','UNIVERSAL','METAPHOR']; const ladder = []; let txtRest = text; for (const tier of TIERS) { const re = new RegExp(`(?:^|\\n)\\s*\\*?\\*?(?:${tier}|${tier.toLowerCase()})\\*?\\*?[:\\s—\\-]+(.+?)(?=(?:\\n\\s*\\*?\\*?(?:${TIERS.join('|')}|${TIERS.map(t=>t.toLowerCase()).join('|')})\\*?\\*?[:\\s—\\-])|$)`, 'is'); const m = txtRest.match(re); if (m) { const body = m[1].trim().replace(/^["']|["']$/g, ''); const enMatch = body.match(/^(.+?)\s*\(([^)]+)\)\s*$/); if (enMatch) ladder.push({type: tier, copy: enMatch[1].trim(), en: enMatch[2].trim()}); else ladder.push({type: tier, copy: body, en: ''}); } } if (presetKey === 'ladder' || ladder.length >= 4) { return {kind: 'ladder', data: {items: ladder}}; } // Reasoning: paragraph + bullet followups (lines starting with -/•/·) const followups = lines .filter(l => /^[-•·]\s+/.test(l)) .map(l => l.replace(/^[-•·]\s+/, '')) .map(l => { const m = l.match(/^(.+?)\s*[—\-]\s*([A-Za-z].+)$/) || l.match(/^(.+?)\s*\(([^)]+)\)\s*$/); return m ? {ko: m[1].trim(), en: m[2].trim()} : {ko: l, en: ''}; }); if (followups.length >= 2) { const para = lines.filter(l => !/^[-•·]\s+/.test(l) && !/^\s*\d+[\.\)]/.test(l)).join(' '); return {kind: 'reasoning', data: {text: para || text.slice(0, 240), followups}}; } return {kind: 'text', data: null}; }; const V2IdeateStage = ({palette, projectId, pinnedIds}) => { // Build keyword index const map = {}; (palette.blocks || []).forEach(b => (b.keywords || []).forEach(k => map[k.id] = {...k, blockId: b.id, fileId: b.file})); const allPinned = pinnedIds.map(id => map[id]).filter(Boolean); const [sessions, setSessions] = React.useState(() => [ { id: 's1', title: 'New chat', context: pinnedIds.slice(0, 4), messages: [], }, ]); const [activeId, setActiveId] = React.useState('s1'); const [savedSeeds, setSavedSeeds] = React.useState([]); const [draft, setDraft] = React.useState(''); const [thinking, setThinking] = React.useState(false); const active = sessions.find(s => s.id === activeId) || sessions[0]; const presets = [ {key:'combine', label:'Combine', sub:'결합'}, {key:'twist', label:'Twist', sub:'클리셰 우회'}, {key:'ladder', label:'Ladder', sub:'5층위'}, {key:'tagline', label:'Tagline', sub:'7단어 헤드라인'}, {key:'audience', label:'Audience swap', sub:'톤 이동'}, {key:'why', label:'Why this?', sub:'근거 설명'}, ]; const newSession = () => { const id = 's' + (sessions.length + 1) + '_' + Math.floor(Math.random()*999); const next = {id, title: 'New chat', context: pinnedIds.slice(0, 4), messages: []}; setSessions([...sessions, next]); setActiveId(id); }; const removeContext = (kid) => { setSessions(sessions.map(s => s.id === activeId ? {...s, context: s.context.filter(c => c !== kid)} : s)); }; const addContext = (kid) => { setSessions(sessions.map(s => s.id === activeId ? {...s, context: [...new Set([...s.context, kid])]} : s)); }; const presetText = (key) => ({ combine: '핀된 키워드 두세 개를 결합한 새로운 표현 5개를 만들어줘.', twist: '뻔한 광고 클리셰를 비껴가는 비틀어진 카피 3개를 만들어줘.', ladder: '핀된 첫 번째 키워드로 LITERAL → SENSORY → EMOTION → UNIVERSAL → METAPHOR 5층위 카피 사다리를 만들어줘.', tagline: '7단어 이내의 헤드라인 5종을 한국어와 영어로 각각 만들어줘.', audience: '현재 타깃을 다른 연령/성별로 옮겼을 때 카피의 어휘와 톤이 어떻게 변해야 하는지 분석해줘.', why: '이 키워드들이 왜 이 브리프에 잘 맞는지, 헤리티지/감각/심리/원형 측면에서 한 단락씩 설명해줘.', })[key] || ''; const send = async (textIn, presetKey) => { const text = (textIn || draft).trim(); if (!text || thinking) return; setDraft(''); const userMsg = {role:'user', text, preset: presetKey || null}; const ctxKws = active.context.map(id => map[id]).filter(Boolean); const ctxLabel = ctxKws.length ? `[Context: ${ctxKws.map(k => `${k.en} · ${k.ko}`).join(' / ')}]\n\n` : ''; const sessUpdated = {...active, messages: [...active.messages, userMsg]}; setSessions(sessions.map(s => s.id === active.id ? sessUpdated : s)); setThinking(true); try { const res = await window.API.brainstorm(projectId, 'omni', sessUpdated.messages.map(m => ({me: m.role==='user', text: ctxLabel + m.text}))); const parsed = parseAIReply(res.reply || '', presetKey); const aiMsg = {role:'ai', text: res.reply || '', kws: res.kws || [], model: res.model, kind: parsed.kind, structured: parsed.data}; setSessions(prev => prev.map(s => s.id === active.id ? {...s, messages: [...sessUpdated.messages, aiMsg], title: sessUpdated.title === 'New chat' ? text.slice(0, 32) : sessUpdated.title} : s)); } catch(e) { setSessions(prev => prev.map(s => s.id === active.id ? {...s, messages: [...sessUpdated.messages, {role:'ai', text:'에러: '+e.message, kws:[]}]} : s)); } finally { setThinking(false); } }; const seedFromMsg = (text, from) => setSavedSeeds(prev => [...prev, {ko: text, from}]); const removeSeed = (idx) => setSavedSeeds(prev => prev.filter((_, i) => i !== idx)); const regenerate = () => { const lastUser = [...active.messages].reverse().find(m => m.role === 'user'); if (!lastUser) return; // Drop the latest AI reply and re-send const trimmed = active.messages.slice(0, active.messages.lastIndexOf(lastUser)); setSessions(sessions.map(s => s.id === active.id ? {...s, messages: trimmed} : s)); setTimeout(() => send(lastUser.text, lastUser.preset), 50); }; const messageAction = (msg, action) => { const followups = { 'Refine': `방금 답변을 더 정교하게 다듬어줘. 같은 형식, 같은 길이로.`, 'Shorter': `방금 답변을 더 짧게 줄여줘. 핵심만.`, 'Translate': `방금 답변을 영문 카피 톤으로 번역해줘.`, 'Branch': `방금 답변과 다른 방향으로 새 후보를 뽑아줘.`, }[action] || action; send(followups, null); }; return (