/* ===== Screens Part 2: Strategy (AI chat), Creators ===== */ // ========== 03 · CAMPAIGN STRATEGY ========== const campaignApiUrl = (pathname) => (window.location.protocol === 'file:' ? `http://localhost:4173${pathname}` : pathname); const campaignListValue = (value) => Array.isArray(value) ? value.join('、') : String(value || ''); const campaignSplitValue = (value) => String(value || '').split(/[、,,;;\n]/).map(item => item.trim()).filter(Boolean); const campaignNumber = (value) => Number(value || 0); const campaignViews = (post = {}) => campaignNumber(post.views || post.plays || post.reads || post.organic_impressions); const campaignInteraction = (post = {}) => campaignNumber(post.likes) + campaignNumber(post.comments) + campaignNumber(post.shares) + campaignNumber(post.saves); const campaignCompact = (value) => { const n = campaignNumber(value); if (n >= 100000000) return `${(n / 100000000).toFixed(1)}亿`; if (n >= 10000) return `${(n / 10000).toFixed(1)}万`; return String(Math.round(n)); }; const campaignPostIds = (post = {}) => [post.post_id, post.content_id, post.id].filter(Boolean); const campaignPostBriefIds = (post = {}) => [ post.primaryMasterBriefId, post.briefMapping?.primaryMasterBriefId, ...(post.masterBriefIds || []), ...(post.briefMapping?.masterBriefIds || []), ].filter(Boolean); const campaignMatchesPost = (campaign = {}, post = {}) => { const ids = new Set(campaign.assetPostIds || []); if (campaignPostIds(post).some(id => ids.has(id))) return true; if (ids.size) return false; const briefIds = new Set(campaignPostBriefIds(post)); if (campaign.id && briefIds.has(campaign.id)) return true; const parentIds = [post.parentCampaignId, post.campaignId, post.briefMapping?.parentCampaignId].filter(Boolean); if (campaign.id && parentIds.includes(campaign.id)) return true; if (ids.size || briefIds.size || parentIds.length) return false; const labels = campaign.assetCampaignLabels?.length ? campaign.assetCampaignLabels : [campaign.name]; return labels.some(label => label && [post.campaign, post.product, post.content_type, post.title, post.caption].join(' ').includes(label)); }; const INITIATIVE_TYPE_META = { campaign: { label: 'Campaign', text: '阶段性活动', tone: 'accent' }, pillar: { label: 'Pillar', text: '长期内容支柱', tone: 'info' }, series: { label: 'Series', text: '固定栏目', tone: 'info' }, standalone: { label: 'Content', text: '单篇/内容任务', tone: 'ghost' }, social_care: { label: 'Care', text: '社群照护', tone: 'warn' }, }; const SOCIAL_STRATEGY_OBJECTS = [ { type: 'pillar', title: 'Content Pillars', cn: '长期内容支柱', examples: [ { en: 'Product Education', cn: '产品教育' }, { en: 'Technology & Innovation', cn: '技术与创新' }, { en: 'Ownership Experience', cn: '车主体验' }, { en: 'Lifestyle & Use Cases', cn: '生活方式与使用场景' }, ], }, { type: 'series', title: 'Content Series', cn: '固定栏目', examples: [ { en: 'Feature Friday', cn: '周五功能讲解' }, { en: 'EV Myth Busters', cn: '电动车误区破解' }, { en: 'Owner’s Garage', cn: '车主车库' }, { en: 'Test Drive Tuesdays', cn: '周二试驾日' }, ], }, { type: 'campaign', title: 'Social Media Campaigns', cn: '阶段性 Campaign', examples: [ { en: 'New Model Launch', cn: '新车型发布' }, { en: 'Auto Show', cn: '车展活动' }, { en: 'Creator Collaboration', cn: '创作者合作' }, { en: 'UGC Challenge', cn: '用户共创挑战' }, ], }, { type: 'social_care', title: 'Community / Social Care', cn: '社群响应', examples: [ { en: 'Comment Response', cn: '评论回复' }, { en: 'DM Handling', cn: '私信处理' }, { en: 'Lead Routing', cn: '线索分发' }, { en: 'Crisis Response', cn: '危机响应' }, ], }, { type: 'standalone', title: 'Standalone Content', cn: '单篇内容任务', examples: [ { en: 'Model Introduction', cn: '车型介绍' }, { en: 'Feature Explainer', cn: '功能说明' }, { en: 'Service Guide', cn: '服务说明' }, { en: 'Brand Culture Story', cn: '品牌文化长文' }, ], }, ]; const campaignAppendUniqueText = (current = '', additions = []) => { const values = campaignSplitValue(current); additions.filter(Boolean).forEach(item => { if (!values.includes(item)) values.push(item); }); return values.join('、'); }; const isOpenMarketHandoff = item => !['consumed', 'archived'].includes(item.status); const DEFAULT_STRATEGY_THESIS = { title: 'MONTX 2026-2027 预热策略母题', timeframe: '2026-2027', coreThesis: '不要急着卖车,先卖一种判断。', marketBelief: '未来的车不应该只是更智能、更大、更快,而应该更懂真实生活、更可信、更值得等待。', contentParadigm: ['问题定义', '用户洞察', '品牌原则', '设计取舍', '研发证据', '场景想象', '社群共创', '车型期待', '上市转化'], guardrails: ['不急于销售转化', '不把概念车写成确定量产承诺', '不堆参数,不做空泛科技感', '优先建立可信判断和长期期待'], }; const STRATEGY_STAGE_HINTS = { 问题定义: '用外部痛点和行业错位定义“为什么需要另一种车”。', 用户洞察: '把真实生活、移动办公、户外、自驾、共创用户的具体矛盾讲清楚。', 品牌原则: '解释 MONTX 做判断的原则,不急着堆卖点。', 设计取舍: '讲清楚为什么这样设计、放弃了什么、坚持了什么。', 研发证据: '用测试、工程、材料、原型和团队证据建立可信度。', 场景想象: '把产品放进真实生活,而不是只放进参数表。', 社群共创: '让用户参与定义车、场景、功能和表达。', 车型期待: '逐步建立 P10 / V10 等车型期待,但保持概念边界。', 上市转化: '最后才进入预约、试驾、线索和销售转化。', }; const strategyText = value => Array.isArray(value) ? value.join('、') : String(value || ''); const strategySplitList = value => String(value || '').split(/[、;;\n]/).map(item => item.trim()).filter(Boolean); const strategyInferStage = (initiative = {}) => { const text = [initiative.name, initiative.stage, initiative.coreMessage, ...(initiative.goals || []), ...(initiative.sellingPoints || [])].join(' '); if (/牛马|共创|creator|ugc|kol|达人/i.test(text)) return '社群共创'; if (/车展|邀请|探访|发布会|prelaunch|预发布|首发|launch/i.test(text)) return '车型期待'; if (/tvc|品牌|slogan|principle|culture/i.test(text)) return '品牌原则'; if (/研发|测试|证据|工程|技术|innovation|feature/i.test(text)) return '研发证据'; if (/场景|生活|数字游民|户外|移动办公|use case|lifestyle/i.test(text)) return '场景想象'; if (/车型|产品介绍|外观|功能|设计|取舍|model|product/i.test(text)) return '设计取舍'; if (/洞察|用户|人群|痛点/i.test(text)) return '用户洞察'; if ((initiative.type || '') === 'campaign') return '车型期待'; return '问题定义'; }; const buildStrategySimulation = (thesis = DEFAULT_STRATEGY_THESIS, initiatives = []) => { const stages = thesis.contentParadigm?.length ? thesis.contentParadigm : DEFAULT_STRATEGY_THESIS.contentParadigm; return stages.map(stage => { const matched = initiatives.filter(item => strategyInferStage(item) === stage); return { stage, hint: STRATEGY_STAGE_HINTS[stage] || '作为内容创作策略的一段叙事推进。', objects: matched.slice(0, 4), status: matched.length ? 'covered' : 'gap', recommendation: matched.length ? `已有 ${matched.length} 个策略对象可承接,下一步把 brief 改写成“${stage}”视角。` : `缺少“${stage}”内容,需要新增 pillar / series / standalone brief。`, }; }); }; const Strategy = ({ workspaceContext }) => { const brandStoreReady = Boolean(window.BrandMemoryStore?.useBrandMemoryProfiles); const brandState = brandStoreReady ? window.BrandMemoryStore.useBrandMemoryProfiles() : { brands: [], setBrands: () => {}, ready: false }; const brandId = workspaceContext?.brandId || 'montx'; const brand = brandStoreReady ? window.BrandMemoryStore.getBrandProfile(brandId, brandState.brands) : null; const [campaigns, setCampaigns] = React.useState([]); const [activeId, setActiveId] = React.useState(''); const [form, setForm] = React.useState(null); const [social, setSocial] = React.useState({ posts: [], comments: [], status: 'idle' }); const [marketHandoffs, setMarketHandoffs] = React.useState([]); const [strategyThesis, setStrategyThesis] = React.useState(DEFAULT_STRATEGY_THESIS); const [thesisStatus, setThesisStatus] = React.useState(''); const [saving, setSaving] = React.useState(''); const [writeStatus, setWriteStatus] = React.useState(''); const loadCampaigns = React.useCallback(async () => { const response = await fetch(campaignApiUrl('/api/initiatives')); const data = await response.json(); const rows = data.initiatives || data.campaigns || []; setCampaigns(rows); setActiveId(current => current || rows[0]?.id || ''); }, []); const loadSocial = React.useCallback(async () => { try { const response = await fetch(campaignApiUrl('/api/social/sync/status')); const data = await response.json(); setSocial({ posts: data.posts || [], comments: data.comments || [], status: data.status || 'idle' }); } catch { setSocial({ posts: [], comments: [], status: 'offline' }); } }, []); const loadMarketHandoffs = React.useCallback(async () => { try { const response = await fetch(campaignApiUrl('/api/market/handoffs?target_module=strategy&limit=10')); const data = await response.json(); if (response.ok) setMarketHandoffs((data.handoffs || []).filter(isOpenMarketHandoff)); } catch { setMarketHandoffs([]); } }, []); const loadStrategyThesis = React.useCallback(async () => { try { const response = await fetch(campaignApiUrl('/api/strategy/thesis')); const data = await response.json(); if (response.ok && data.strategyThesis) setStrategyThesis({ ...DEFAULT_STRATEGY_THESIS, ...data.strategyThesis, contentParadigmText: strategyText(data.strategyThesis.contentParadigm), guardrailsText: strategyText(data.strategyThesis.guardrails), }); } catch { setStrategyThesis({ ...DEFAULT_STRATEGY_THESIS, contentParadigmText: strategyText(DEFAULT_STRATEGY_THESIS.contentParadigm), guardrailsText: strategyText(DEFAULT_STRATEGY_THESIS.guardrails), }); } }, []); React.useEffect(() => { loadCampaigns(); loadSocial(); loadMarketHandoffs(); loadStrategyThesis(); }, [loadCampaigns, loadSocial, loadMarketHandoffs, loadStrategyThesis]); const activeCampaign = campaigns.find(item => item.id === activeId) || campaigns[0] || null; React.useEffect(() => { if (activeCampaign) setForm({ ...activeCampaign, goalsText: campaignListValue(activeCampaign.goals), platformFocusText: campaignListValue(activeCampaign.platformFocus), audienceText: campaignListValue(activeCampaign.audience), sellingPointsText: campaignListValue(activeCampaign.sellingPoints), contentBoundariesText: campaignListValue(activeCampaign.contentBoundaries), riskBoundariesText: campaignListValue(activeCampaign.riskBoundaries), assetCampaignLabelsText: campaignListValue(activeCampaign.assetCampaignLabels), type: activeCampaign.type || 'campaign', }); }, [activeCampaign?.id]); const linkedPosts = React.useMemo(() => (social.posts || []).filter(post => campaignMatchesPost(activeCampaign || {}, post)), [social.posts, activeCampaign]); const commentsByPost = React.useMemo(() => { const map = new Map(); (social.comments || []).forEach(comment => { const list = map.get(comment.post_id) || []; list.push(comment); map.set(comment.post_id, list); }); return map; }, [social.comments]); const linkedCommentCount = linkedPosts.reduce((sum, post) => sum + Math.max(campaignNumber(post.comments), (commentsByPost.get(post.post_id) || []).length), 0); const metrics = { posts: linkedPosts.length, views: linkedPosts.reduce((sum, post) => sum + campaignViews(post), 0), interaction: linkedPosts.reduce((sum, post) => sum + campaignInteraction(post), 0), comments: linkedCommentCount, }; const topPosts = [...linkedPosts].sort((a, b) => campaignInteraction(b) - campaignInteraction(a) || campaignViews(b) - campaignViews(a)).slice(0, 6); const patchForm = (key, value) => setForm(prev => ({ ...(prev || {}), [key]: value })); const patchStrategyThesis = (key, value) => setStrategyThesis(prev => ({ ...(prev || DEFAULT_STRATEGY_THESIS), [key]: value })); const saveStrategyThesis = async () => { setThesisStatus('saving'); const payload = { ...strategyThesis, contentParadigm: strategySplitList(strategyThesis.contentParadigmText || strategyText(strategyThesis.contentParadigm)), guardrails: strategySplitList(strategyThesis.guardrailsText || strategyText(strategyThesis.guardrails)), source: strategyThesis.source || 'market_sensing_session', }; const response = await fetch(campaignApiUrl('/api/strategy/thesis'), { method: 'PATCH', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ strategyThesis: payload }), }); const data = await response.json(); if (response.ok && data.strategyThesis) { setStrategyThesis({ ...data.strategyThesis, contentParadigmText: strategyText(data.strategyThesis.contentParadigm), guardrailsText: strategyText(data.strategyThesis.guardrails), }); setThesisStatus('saved'); setTimeout(() => setThesisStatus(''), 1200); } else { setThesisStatus('error'); } }; const applyThesisToCurrentObject = () => { const guardrails = strategySplitList(strategyThesis.guardrailsText || strategyText(strategyThesis.guardrails)); const stage = strategyInferStage(form || activeCampaign || {}); setForm(prev => ({ ...(prev || {}), coreMessage: prev?.coreMessage || strategyThesis.marketBelief || strategyThesis.coreThesis, contentBoundariesText: campaignAppendUniqueText(prev?.contentBoundariesText, [ `策略母题:${strategyThesis.coreThesis}`, `内容阶段:${stage}`, STRATEGY_STAGE_HINTS[stage], ]), riskBoundariesText: campaignAppendUniqueText(prev?.riskBoundariesText, guardrails), })); setThesisStatus('applied'); setTimeout(() => setThesisStatus(''), 1200); }; const applyMarketHandoff = async (handoff = marketHandoffs[0]) => { const pack = handoff?.contextPack || {}; const snapshot = pack.market_snapshot || {}; const benchmark = pack.competitive_benchmark || {}; const riskNotes = pack.risk_notes || []; const activeSignals = snapshot.active_signals || []; const signalActions = activeSignals.map(item => item.recommended_action).filter(Boolean).slice(0, 3); const riskBoundaries = riskNotes.flatMap(item => item.review_reason || []).map(item => `市场感知复核:${item}`); const contentBoundaries = [ ...(benchmark.white_spaces || []).map(item => `市场空位:${item}`), ...signalActions, ]; setForm(prev => ({ ...(prev || {}), coreMessage: prev?.coreMessage || snapshot.summary || prev?.masterBrief?.coreMessage || '', contentBoundariesText: campaignAppendUniqueText(prev?.contentBoundariesText, contentBoundaries), riskBoundariesText: campaignAppendUniqueText(prev?.riskBoundariesText, riskBoundaries), marketContextPackId: handoff?.context_pack_id || '', })); setWriteStatus(`已应用 Market Context:${handoff?.context_pack_id || 'context-pack'}`); if (handoff?.handoff_id) { await fetch(campaignApiUrl(`/api/market/handoffs/${encodeURIComponent(handoff.handoff_id)}/status`), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ status: 'consumed', consumed_by: form?.id || activeCampaign?.id || 'strategy-workbench', note: 'Applied to strategy object form' }), }).catch(() => {}); loadMarketHandoffs(); } }; const formPayload = () => ({ ...form, goals: campaignSplitValue(form.goalsText), platformFocus: campaignSplitValue(form.platformFocusText), audience: campaignSplitValue(form.audienceText), sellingPoints: campaignSplitValue(form.sellingPointsText), contentBoundaries: campaignSplitValue(form.contentBoundariesText), riskBoundaries: campaignSplitValue(form.riskBoundariesText), assetCampaignLabels: campaignSplitValue(form.assetCampaignLabelsText || form.name), }); const saveCampaign = async () => { if (!form) return; setSaving('saving'); const response = await fetch(campaignApiUrl(`/api/initiatives/${form.id}`), { method: 'PATCH', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ initiative: formPayload() }), }); const data = await response.json(); setCampaigns(rows => rows.map(item => item.id === data.initiative.id ? data.initiative : item)); setSaving('saved'); setTimeout(() => setSaving(''), 1200); }; const createCampaign = async (type = 'standalone') => { const meta = INITIATIVE_TYPE_META[type] || INITIATIVE_TYPE_META.standalone; const response = await fetch(campaignApiUrl('/api/initiatives'), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ type, name: `新 ${meta.label} ${campaigns.length + 1}`, brandId, status: 'draft', stage: meta.text, goals: ['沉淀社媒资产', '形成可复盘的内容证据'], platformFocus: ['小红书', '视频号', '微信公众号'], }), }); const data = await response.json(); setCampaigns(rows => [data.initiative, ...rows]); setActiveId(data.initiative.id); }; const generateMasterBrief = async () => { if (!form) return; setSaving('brief'); const response = await fetch(campaignApiUrl(`/api/initiatives/${form.id}/master-brief`), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ initiative: formPayload() }), }); const data = await response.json(); setCampaigns(rows => rows.map(item => item.id === data.initiative.id ? data.initiative : item)); setForm(prev => ({ ...prev, masterBrief: data.masterBrief })); setSaving('brief-saved'); setTimeout(() => setSaving(''), 1200); }; const writeCampaignToMemory = () => { if (!brandStoreReady || !brand || !activeCampaign) { setWriteStatus('品牌记忆未就绪'); return; } const stampedAt = new Date().toLocaleString('zh-CN', { hour12: false }); const memoryCampaign = { id: `initiative-${activeCampaign.id}`, name: activeCampaign.name, title: activeCampaign.name, status: activeCampaign.status, type: activeCampaign.type || 'campaign', source: 'campaign-workbench', factStatus: 'confirmed', provenance: 'operational', description: activeCampaign.coreMessage || activeCampaign.masterBrief?.coreMessage || '策略对象主命题待补充', metrics: { posts: metrics.posts, views: metrics.views, interaction: metrics.interaction, comments: metrics.comments }, updatedAt: stampedAt, }; const learning = { id: `initiative-learning-${activeCampaign.id}`, status: 'pending', type: 'content-initiative-social-ops', source: 'campaign-workbench', title: `${activeCampaign.name} 社媒运营闭环`, text: `已关联 ${metrics.posts} 条社媒内容,阅读/播放 ${campaignCompact(metrics.views)},互动 ${campaignCompact(metrics.interaction)},评论 ${campaignCompact(metrics.comments)}。`, createdAt: stampedAt, }; brandState.setBrands(prev => prev.map(item => item.id === brand.id ? window.BrandMemoryStore.normalizeBrandProfile({ ...item, campaigns: [...(item.campaigns || []).filter(row => row.id !== memoryCampaign.id), memoryCampaign], learnings: [...(item.learnings || []).filter(row => row.id !== learning.id), learning], updatedAt: stampedAt, }) : item )); setWriteStatus('已写回品牌记忆'); }; if (!form) { return
阶段性 Campaign、栏目、内容支柱和单篇内容都可以生成 Master Brief,并关联社媒资产复盘。
{thesis.marketBelief || DEFAULT_STRATEGY_THESIS.marketBelief}
{snapshot.summary || 'Market Context Pack 已就绪。'}
不只按粉丝和报价排序。调性 · 人群重合 · 评论质量 · 稳定性 · 风险,综合匹配本轮命题。
| 达人 | 平台 | 分层 | 粉丝 | Fit | 情绪 | 风险 | 报价 (w) | |
|---|---|---|---|---|---|---|---|---|
| toggle(c.handle)} style={{cursor:'pointer', accentColor:'var(--accent)'}}/> |
{c.avatar}
{c.handle}
{c.tags.join(' · ')}
|
{c.platform} | {c.followers} |
{c.fit}
|
0.5 ? 'var(--accent)' : c.sentiment > 0.4 ? 'var(--ink-2)' : 'var(--warn)', fontFamily:'var(--f-mono)', fontSize:11}}> +{c.sentiment.toFixed(2)} | {{low:'低',med:'中',high:'高'}[c.risk]} | {c.price} |