// Column 3 — Board. Drop zone for selected keywords + brainstorm (omni LLM) + final section. const boardStyles = { col: { background:'linear-gradient(180deg, var(--paper) 0%, var(--paper-2) 100%)', display:'flex', flexDirection:'column', minHeight:0 }, head: { padding:'16px 24px 14px 24px', borderBottom:'1px solid var(--line)' }, 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 }, scroll: { overflowY:'auto', flex:1, padding:'18px 22px 32px 22px' }, section: { marginBottom:22 }, sectionHead: { display:'flex', alignItems:'baseline', justifyContent:'space-between', marginBottom:10 }, sectionTitle: { fontFamily:'var(--mono)', fontSize:10, letterSpacing:'.14em', color:'var(--ink-3)', textTransform:'uppercase' }, sectionCount: { fontFamily:'var(--mono)', fontSize:10, color:'var(--ink-4)' }, dropzone: (hot) => ({ minHeight: 60, border: hot ? '1.5px dashed var(--accent)' : '1.5px dashed var(--line-2)', borderRadius:10, padding:10, display:'flex', flexWrap:'wrap', gap:8, alignContent:'flex-start', background: hot ? 'rgba(244,81,28,.06)' : 'transparent', transition:'all .15s', }), pinned: (colorVar) => ({ display:'inline-flex', alignItems:'stretch', borderRadius:8, overflow:'hidden', border:'1px solid var(--line-2)', background:'var(--paper)', boxShadow:'0 1px 0 rgba(0,0,0,.04)', }), pinnedAccent: (c) => ({ width:4, background: c==='accent-2' ? 'var(--accent-2)' : c==='accent-3' ? 'var(--accent-3)' : c==='ink' ? 'var(--ink)' : 'var(--accent)', }), pinnedBody: { padding:'8px 10px 8px 10px', minWidth:120, maxWidth:260 }, pinnedRow: { display:'flex', alignItems:'baseline', gap:8, flexWrap:'wrap', rowGap:2 }, pinnedEn: { fontSize:13, color:'var(--ink)', fontWeight:500, whiteSpace:'nowrap' }, pinnedKo: { fontFamily:'var(--display)', fontSize:13.5, color:'var(--ink-2)', fontStyle:'italic', whiteSpace:'nowrap' }, pinnedNote: { fontSize:10.5, color:'var(--ink-3)', marginTop:4, lineHeight:1.4 }, pinnedSrc: { fontFamily:'var(--mono)', fontSize:9, color:'var(--ink-4)', marginTop:3, letterSpacing:'.05em' }, pinnedClose: { padding:'0 8px', borderLeft:'1px solid var(--line)', display:'flex', alignItems:'center', color:'var(--ink-4)', cursor:'pointer' }, emptyDrop: { fontSize:11.5, color:'var(--ink-4)', fontStyle:'italic', padding:'8px 4px' }, brainBox: { border:'1px solid var(--line)', borderRadius:10, background:'var(--paper)', padding:0, overflow:'hidden', marginBottom:18, }, brainHead: { padding:'10px 14px', borderBottom:'1px solid var(--line)', display:'flex', justifyContent:'space-between', alignItems:'center', background:'var(--paper-2)' }, brainTitle: { fontFamily:'var(--serif)', fontSize:14, color:'var(--ink)' }, modelPick: { display:'flex', gap:4, fontFamily:'var(--mono)', fontSize:9.5, letterSpacing:'.08em' }, modelChip: (a) => ({ padding:'3px 7px', borderRadius:4, border:'1px solid var(--line-2)', background: a ? 'var(--ink)' : 'var(--paper)', color: a ? 'var(--paper)' : 'var(--ink-3)', cursor:'pointer', textTransform:'uppercase', }), messages: { padding:'12px 14px', maxHeight:260, overflowY:'auto', display:'flex', flexDirection:'column', gap:10 }, msg: (me) => ({ maxWidth:'86%', alignSelf: me ? 'flex-end' : 'flex-start', padding:'8px 11px', fontSize:12.5, lineHeight:1.5, borderRadius:10, background: me ? 'var(--ink)' : 'var(--paper-2)', color: me ? 'var(--paper)' : 'var(--ink)', border: me ? 'none' : '1px solid var(--line)', }), msgKw: { display:'flex', flexWrap:'wrap', gap:6, marginTop:8 }, ghostKw: { display:'inline-flex', alignItems:'center', gap:6, padding:'3px 8px', borderRadius:999, background:'var(--paper)', border:'1px dashed var(--line-2)', fontSize:11.5, cursor:'grab', }, msgMeta: { fontFamily:'var(--mono)', fontSize:9, color:'var(--ink-4)', marginTop:4, letterSpacing:'.06em' }, composer: { display:'flex', gap:8, padding:'10px 12px', borderTop:'1px solid var(--line)', background:'var(--paper-2)', alignItems:'center' }, composerInput: { flex:1, border:'1px solid var(--line-2)', borderRadius:8, padding:'8px 10px', fontSize:12.5, background:'var(--paper)', outline:'none', fontFamily:'var(--sans)' }, sendBtn: { height:32, padding:'0 12px', border:'1px solid var(--ink)', borderRadius:8, background:'var(--ink)', color:'var(--paper)', fontSize:12, cursor:'pointer', display:'inline-flex', alignItems:'center', gap:5 }, finalBox: { background:'var(--ink)', color:'var(--paper)', borderRadius:12, padding:'16px 18px 18px 18px', marginTop:6, boxShadow:'0 10px 30px rgba(0,0,0,.12)', backgroundImage:'radial-gradient(circle at 100% 0%, rgba(244,81,28,.35), transparent 55%)', }, finalHead: { display:'flex', justifyContent:'space-between', alignItems:'baseline', marginBottom:10 }, finalTitle: { fontFamily:'var(--serif)', fontSize:22, color:'var(--paper)', letterSpacing:'-.01em' }, finalTag: { fontFamily:'var(--mono)', fontSize:9.5, letterSpacing:'.14em', color:'rgba(255,255,255,.55)', textTransform:'uppercase' }, finalList: { display:'flex', flexWrap:'wrap', gap:8, marginTop:12, marginBottom:14 }, finalKw: { padding:'6px 10px', borderRadius:999, border:'1px solid rgba(255,255,255,.25)', fontSize:12.5, display:'inline-flex', alignItems:'center', gap:8 }, finalKo: { fontFamily:'var(--display)', fontStyle:'italic', color:'rgba(255,255,255,.7)' }, finalActions: { display:'flex', gap:8, marginTop:4 }, ghostBtn: { height:30, padding:'0 12px', borderRadius:999, border:'1px solid rgba(255,255,255,.3)', background:'transparent', color:'var(--paper)', fontSize:11.5, cursor:'pointer' }, primaryBtn: { height:30, padding:'0 14px', borderRadius:999, border:'1px solid var(--accent)', background:'var(--accent)', color:'var(--paper)', fontSize:11.5, cursor:'pointer', fontWeight:500 }, }; function PinnedChip({ k, onRemove, fileNameById }) { const fromLabel = k.from && fileNameById && fileNameById[k.from] ? fileNameById[k.from] : k.from; return (
{k.w} {k.ko}
{(k.note || k.why) &&
{k.note || k.why}
} {fromLabel &&
from · {fromLabel}
}
onRemove(k)}>
); } function WatchingChip({ k, onPromote, onRemove }) { return (
{k.w} {k.ko}
); } const BRAINSTORM_INTRO = { me:false, model:'Palette · omni', text:"왼쪽에서 키워드를 모아두면 여기서 함께 이야기해요. 상투어는 부숴 드리고, 숨은 긴장감도 찾아낼게요.", kws:[], }; const refStyles = { wrap: { border:'1px solid var(--line)', borderRadius:10, background:'var(--paper)', overflow:'hidden' }, bar: { display:'flex', gap:8, padding:'10px 12px', borderBottom:'1px solid var(--line)', alignItems:'center', background:'var(--paper-2)', flexWrap:'wrap' }, input: { flex:1, minWidth:140, border:'1px solid var(--line-2)', borderRadius:999, padding:'6px 12px', fontSize:12, fontFamily:'var(--sans)', background:'var(--paper)', outline:'none' }, siteChip: (a) => ({ padding:'4px 9px', borderRadius:999, border:'1px solid var(--line-2)', background: a ? 'var(--ink)' : 'var(--paper)', color: a ? 'var(--paper)' : 'var(--ink-3)', fontSize:10.5, fontFamily:'var(--mono)', letterSpacing:'.08em', textTransform:'uppercase', cursor:'pointer', }), searchBtn: { height:28, padding:'0 14px', borderRadius:999, border:'1px solid var(--accent)', background:'var(--accent)', color:'var(--paper)', fontSize:12, cursor:'pointer', fontFamily:'var(--sans)' }, grid: { display:'grid', gridTemplateColumns:'repeat(auto-fill, minmax(150px, 1fr))', gridAutoRows:'1fr', gap:10, padding:'12px', alignItems:'stretch' }, card: { border:'1px solid var(--line)', borderRadius:8, overflow:'hidden', background:'var(--paper)', display:'flex', flexDirection:'column', cursor:'pointer', transition:'transform .12s, box-shadow .12s', height:'100%' }, cardBody: { display:'flex', flexDirection:'column', flex:1, minHeight:0 }, thumb: { width:'100%', aspectRatio:'16/9', objectFit:'cover', background:'var(--paper-3)', display:'block' }, meta: { padding:'6px 8px 4px 8px', fontSize:11, color:'var(--ink)', lineHeight:1.35, flex:1, display:'flex', flexDirection:'column', minHeight:0 }, titleL: { display:'-webkit-box', WebkitLineClamp:2, WebkitBoxOrient:'vertical', overflow:'hidden', wordBreak:'break-word', minHeight:'calc(1.35em * 2)', // always reserve 2 lines so cards align }, siteTag: { fontFamily:'var(--mono)', fontSize:9, letterSpacing:'.1em', color:'var(--ink-4)', textTransform:'uppercase', marginTop:3, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }, actions: { display:'flex', justifyContent:'space-between', padding:'0 8px 8px 8px', alignItems:'center', marginTop:'auto' }, iconBtn: { width:22, height:22, border:'1px solid var(--line-2)', borderRadius:'50%', display:'inline-flex', alignItems:'center', justifyContent:'center', cursor:'pointer', background:'var(--paper)', color:'var(--ink-3)' }, empty: { padding:'28px 16px', textAlign:'center', color:'var(--ink-4)', fontFamily:'var(--serif)', fontStyle:'italic', fontSize:14 }, savedStrip: { display:'flex', gap:8, overflowX:'auto', padding:'10px 12px', borderTop:'1px solid var(--line)' }, savedCard: { width:150, minWidth:150, maxWidth:150, border:'1px solid var(--line-2)', borderRadius:6, overflow:'hidden', position:'relative', cursor:'pointer', flexShrink:0, display:'flex', flexDirection:'column' }, savedThumb: { width:150, aspectRatio:'16/9', objectFit:'cover', display:'block', background:'var(--paper-3)' }, savedTitle: { padding:'4px 6px 6px 6px', fontSize:10.5, lineHeight:1.3, color:'var(--ink)', display:'-webkit-box', WebkitLineClamp:2, WebkitBoxOrient:'vertical', overflow:'hidden', wordBreak:'break-word', minHeight:'calc(1.3em * 2 + 10px)' }, savedRemove: { position:'absolute', top:4, right:4, width:18, height:18, borderRadius:'50%', border:'none', background:'rgba(0,0,0,.55)', color:'white', fontSize:10, cursor:'pointer', display:'inline-flex', alignItems:'center', justifyContent:'center' }, }; const SITE_GROUPS = [ { id: 'youtube_kr', label: 'YouTube KR', kind: 'video' }, { id: 'youtube_jp', label: 'YouTube JP', kind: 'video' }, { id: 'tvcf', label: 'TVCF', kind: 'video' }, { id: 'ads_of_the_world', label: 'Ads of the World · 글로벌 광고', kind: 'video' }, { id: 'naver_images', label: 'Naver · 네이버', kind: 'image' }, { id: 'google_images_kr', label: 'Google KR', kind: 'image' }, { id: 'google_images_jp', label: 'Google JP', kind: 'image' }, { id: 'pinterest', label: 'Pinterest', kind: 'image' }, ]; const SITE_LABEL = SITE_GROUPS.reduce((a, s) => (a[s.id] = s.label, a), {}); function ReferencesPanel({ project, boardPinned, savedRefs, onSearched, onSaveRef, onRemoveRef, onDownloadRef }) { const folders = (project && project.folders) || []; // Prefer a folder that has "reference" or "레퍼런스" in its name. const defaultFolderId = React.useMemo(() => { const hit = folders.find(f => /reference|레퍼런스/i.test(f.name || '')); return (hit || folders[0] || {}).id; }, [folders]); const [targetFolderId, setTargetFolderId] = React.useState(defaultFolderId); const [downloading, setDownloading] = React.useState({}); // Keep targetFolderId valid for the current project: if it isn't one of // this project's folders (because we switched projects), snap to default. React.useEffect(() => { const valid = folders.some(f => f.id === targetFolderId); if (!valid && defaultFolderId) setTargetFolderId(defaultFolderId); }, [defaultFolderId, folders, targetFolderId]); const triggerDownload = async (cand, rid) => { if (!onDownloadRef) { console.warn('onDownloadRef not wired'); return; } const key = rid || cand.url; setDownloading(d => ({...d, [key]: true})); try { await onDownloadRef({ ref_id: rid, candidate: cand, folder_id: targetFolderId }); } catch(e) { console.error('download failed', e); } finally { setDownloading(d => { const x={...d}; delete x[key]; return x; }); } }; const [query, setQuery] = React.useState(''); const [sites, setSites] = React.useState(() => SITE_GROUPS.reduce((a, s) => (a[s.id] = true, a), {}) ); const [busy, setBusy] = React.useState(false); const [results, setResults] = React.useState(null); const [error, setError] = React.useState(null); const [tab, setTab] = React.useState('all'); // 'all' | 'videos' | 'images' // Clear search state whenever we switch to a different project so the // previous project's results don't linger on screen. React.useEffect(() => { setResults(null); setError(null); setBusy(false); setTab('all'); setQuery(''); }, [project && project.id]); const kwList = React.useMemo(() => boardPinned.map(k => k.ko || k.w).filter(Boolean), [boardPinned]); const toggleSite = (id) => setSites(s => ({...s, [id]: !s[id]})); const runSearch = async () => { setError(null); setBusy(true); try { const chosen = Object.keys(sites).filter(s => sites[s]); const res = await API.searchReferences(project.id, { natural_language_query: query.trim() || null, keywords: kwList, sites: chosen.length ? chosen : SITE_GROUPS.map(s => s.id), target_count: 24, per_site_max: 10, }); setResults(res.candidates || []); onSearched && onSearched(res.candidates || []); } catch(e) { setError('레퍼런스 찾기 실패 — ' + e.message); } finally { setBusy(false); } }; const filtered = React.useMemo(() => { if (!results) return null; if (tab === 'videos') return results.filter(c => c.kind !== 'image'); if (tab === 'images') return results.filter(c => c.kind === 'image'); return results; }, [results, tab]); const tabStyle = (a) => ({ padding:'4px 10px', borderRadius:999, border:'1px solid ' + (a ? 'var(--ink)' : 'var(--line-2)'), background: a ? 'var(--ink)' : 'var(--paper)', color: a ? 'var(--paper)' : 'var(--ink-2)', fontSize:11, fontFamily:'var(--sans)', cursor:'pointer', }); const openCard = (c) => window.open(c.url, '_blank', 'noopener,noreferrer'); return (
setQuery(e.target.value)} onKeyDown={e => { if (e.key==='Enter') runSearch(); }}/>
{SITE_GROUPS.map(s => ( toggleSite(s.id)} title={s.kind === 'image' ? 'image source' : 'video source'}> {s.kind === 'image' ? '🖼 ' : '▶ '}{s.label} ))}
자료실 폴더 · · 다운로드 버튼 누르면 이 폴더로 저장
{results && results.length > 0 && (
)} {error &&
{error}
} {results === null && !busy && (
🎬 🖼
{kwList.length ? `모아둔 ${kwList.length}개 키워드로 YouTube · TVCF · Naver · Google · Pinterest 한 번에 검색` : '키워드를 모아둔 뒤 ▶ 레퍼런스 찾기 버튼을 누르면 영상과 이미지가 여기 나와요.'}
Korean + Japanese bias · click to open · + to save
)} {results !== null && results.length === 0 && !busy && (
결과 없음 — 다른 키워드로 시도해볼까요?
)} {filtered && filtered.length > 0 && (
{filtered.map((c, i) => { const isImage = c.kind === 'image'; const thumbStyle = isImage ? { ...refStyles.thumb, aspectRatio:'1 / 1' } : refStyles.thumb; return (
{ e.currentTarget.style.transform='translateY(-1px)'; e.currentTarget.style.boxShadow='0 8px 18px rgba(0,0,0,.08)'; }} onMouseLeave={e => { e.currentTarget.style.transform='none'; e.currentTarget.style.boxShadow='none'; }}>
openCard(c)} title={c.title} style={{position:'relative', display:'flex', flexDirection:'column', flex:1, minHeight:0}}> {c.thumbnail ? { e.currentTarget.style.display='none'; }}/> :
no thumb
} {isImage ? '🖼 ' : '▶ '}{SITE_LABEL[c.site] || c.site}
{c.title || '(제목 없음)'}
{SITE_LABEL[c.site] || c.site}{c.duration ? ` · ${Math.round(c.duration/60)}분` : ''}
{c.matched_keywords && c.matched_keywords.length ? `↳ ${c.matched_keywords.slice(0,2).join(', ')}` : ''}
{e.stopPropagation(); if (!downloading[c.url]) triggerDownload(c, null);}}> {downloading[c.url] ? '…' : '↓'}
{e.stopPropagation(); onSaveRef(c);}}>
); })}
)} {savedRefs && savedRefs.length > 0 && (
{savedRefs.map(s => (
window.open(s.url, '_blank', 'noopener,noreferrer')} title={s.title}> {s.thumbnail && }
{s.title}
))}
)}
); } function BoardCol({ project, board, setBoard, messages, setMessages, distilled, onDistill, onExport, onExportDeck, fileNameById, onAddKw, savedRefs, onSaveRef, onRemoveRef, onDownloadRef, onSearchedRefs, step }) { const [hot, setHot] = React.useState(null); // 'pinned' | 'watching' | null const [model, setModel] = React.useState('omni'); const [input, setInput] = React.useState(''); const msgs = (messages && messages.length) ? messages : [BRAINSTORM_INTRO]; const onDrop = (zone) => (e) => { e.preventDefault(); setHot(null); const raw = e.dataTransfer.getData('application/kw'); if (!raw) return; const k = JSON.parse(raw); if (zone === 'pinned') { setBoard({...board, pinned:[...board.pinned, {...k, color: ['accent','accent-2','accent-3','ink'][board.pinned.length%4]}]}); } else { setBoard({...board, watching:[...board.watching, k]}); } }; const allowDrop = (zone) => (e) => { e.preventDefault(); setHot(zone); }; const leave = () => setHot(null); const removePinned = (k) => setBoard({...board, pinned: board.pinned.filter(x => x.w !== k.w)}); const removeWatch = (k) => setBoard({...board, watching: board.watching.filter(x => x.w !== k.w)}); const promote = (k) => setBoard({ ...board, pinned: [...board.pinned, {...k, color:'accent'}], watching: board.watching.filter(x => x.w !== k.w) }); const [sending, setSending] = React.useState(false); const send = async () => { if (!input.trim() || sending) return; const base = (messages && messages.length) ? messages : [BRAINSTORM_INTRO]; const withUser = [...base, { me:true, text: input }]; setMessages(withUser); const prompt = input; setInput(''); setSending(true); try { const res = await API.brainstorm(project.id, model, withUser); const reply = { me:false, model: res.model, text: res.reply, kws: res.kws || [] }; setMessages([...withUser, reply]); } catch(e) { setMessages([...withUser, { me:false, model:'(error)', text:'Brainstorm failed: ' + e.message, kws:[] }]); } finally { setSending(false); } }; const resetBrainstorm = () => { if (!confirm('Clear the brainstorm history for this project?')) return; setMessages([]); }; // Project-scoped sub-tabs: Board (default), References (fullscreen), Final. const [tab, setTab] = React.useState('board'); React.useEffect(() => { setTab('board'); }, [project && project.id]); // Sync with the top-bar step indicator: step 4 = references, step 5 = final. // (steps 0–2 are the 3 columns, step 3 = Board column root, so map 4/5 here.) React.useEffect(() => { if (step === 4) setTab('references'); else if (step === 5) setTab('final'); else if (step === 3) setTab('board'); }, [step]); const TABS = [ { id:'board', label:'🎨 보드 · Board' }, { id:'references', label:'🎬 레퍼런스 · References' }, { id:'final', label:'📝 최종 · Final deck' }, ]; const tabBtn = (active) => ({ padding:'6px 14px', borderRadius:999, border:'1px solid ' + (active ? 'var(--ink)' : 'var(--line-2)'), background: active ? 'var(--ink)' : 'var(--paper)', color: active ? 'var(--paper)' : 'var(--ink-2)', fontSize:11.5, fontFamily:'var(--sans)', cursor:'pointer', }); // Final-deck subtree, reused by the stacked + dedicated views. const finalDeck = (
04 · 최종 키워드 · Final 프로젝트에 저장됨 · v0.4
{project.name}
{board.pinned.length ? `확정 · ${board.pinned.length}개` : '작성 중'}
{project.brief && (
“{project.brief}”
)}
{board.pinned.length === 0 && (
위에서 키워드를 모아두면 여기 최종본으로 굳어요.
)} {board.pinned.map((k,i) => (
{k.w} · {k.ko}
{k.note && — {k.note}}
))}
{onExport && onExportDeck && ( )}
); const referencesPanelNode = ( ); return (
03 · 조합 · Board
{tab === 'references' ? '레퍼런스 영상·이미지 · References' : tab === 'final' ? '최종 키워드 · Final deck' : '키워드 정리 · Keyword board'}
{tab === 'references' ? '검색 · 다운로드 · 자료실로 바로 저장' : tab === 'final' ? '모아둔 키워드로 덱을 출력하고 공유하세요.' : '키워드를 여기 끌어 모으고, 레퍼런스를 붙이고, 최종본을 내보내세요.'}
{TABS.map(t => ( ))}
{tab === 'references' ? (
{referencesPanelNode}
) : tab === 'final' ? (
{finalDeck}
) : (
● 모아둔 키워드 · Pinned {board.pinned.length} / 6 권장
{board.pinned.length === 0 &&
여기로 키워드를 끌어오거나, + 를 눌러 모아두세요.
} {board.pinned.map((k,i) => )}
○ 관심 · Watching {board.watching.length}
{board.watching.length === 0 &&
당장은 아니지만 기억해두고 싶은 단어들.
} {board.watching.map((k,i) => )}
✦ 핵심 키워드 · Top words {distilled && distilled.length ? '새로고침' : '추출'}
{(!distilled || !distilled.length) && (
파일 분석이 끝나면 자동으로 여기에 쌓여요 · 수동 추출 도 가능.
)} {distilled && distilled.map((k,i) => (
{ e.dataTransfer.setData('application/kw', JSON.stringify(k)); e.dataTransfer.effectAllowed = 'copy'; }} onClick={() => onAddKw && onAddKw(k)} style={{...boardStyles.ghostKw, cursor:'grab'}} title={(k.why||'') + (k.occurrences ? ` · seen ×${k.occurrences}` : '')}> {k.w} {k.ko} {k.occurrences > 1 && ×{k.occurrences}}
))}
▶ 레퍼런스 영상 · Video references {savedRefs ? `${savedRefs.length} 저장됨` : 'YouTube · TVCF · AoTW'}
대화로 아이디어 · Brainstorm {messages && messages.length > 0 && ( 초기화 )}
함께 이야기 나눠요
{[['omni','Omni'],['claude','Claude'],['gpt','GPT'],['gemini','Gemini']].map(([id,l]) => ( setModel(id)}>{l} ))}
{msgs.map((m,i) => (
{m.text} {m.kws && m.kws.length > 0 && (
{m.kws.map((k,j) => (
{ e.dataTransfer.setData('application/kw', JSON.stringify(k)); e.dataTransfer.effectAllowed = 'copy'; }} style={boardStyles.ghostKw} title={k.why}> {k.w} {k.ko}
))}
)}
{!m.me && m.model &&
{m.model}
}
))}
setInput(e.target.value)} onKeyDown={e => { if (e.key==='Enter') send(); }} disabled={sending} placeholder={sending ? '생각하는 중…' : "대화로 묻기 — '조용함과 유능함 사이를 잇는 단어는?'"}/>
04 · 최종 키워드 · Final 프로젝트에 저장됨 · v0.4
{project.name}
{board.pinned.length ? `확정 · ${board.pinned.length}개` : '작성 중'}
{project.brief && (
“{project.brief}”
)}
{board.pinned.length === 0 && (
위에서 키워드를 모아두면 여기 최종본으로 굳어요.
)} {board.pinned.map((k,i) => ( {k.w} {k.ko} ))}
)}
); } window.BoardCol = BoardCol;