/* ===== Screens Part 1: Dashboard, Memory, Sensing ===== */
// ========== 00 · DASHBOARD ==========
const dashboardApiUrl = (pathname) => (window.location.protocol === 'file:' ? `http://localhost:4173${pathname}` : pathname);
const dashboardNumber = (value) => Number(value || 0);
const dashboardCompactNumber = (value) => {
const n = dashboardNumber(value);
if (n >= 100000000) return `${(n / 100000000).toFixed(1)}亿`;
if (n >= 10000) return `${(n / 10000).toFixed(1)}万`;
return n.toLocaleString();
};
const dashboardDateValue = (value = '') => {
if (!value) return 0;
const parsed = new Date(String(value).replace(/\//g, '-'));
return Number.isNaN(parsed.getTime()) ? 0 : parsed.getTime();
};
const dashboardFormatDate = (value, fallback = '未记录') => {
const time = dashboardDateValue(value);
if (!time) return fallback;
return new Date(time).toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false,
});
};
const dashboardFormatRange = (start, end) => {
const s = dashboardDateValue(start);
const e = dashboardDateValue(end);
if (!s && !e) return '未设置周期';
const fmt = time => new Date(time).toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' });
return `${s ? fmt(s) : '未设开始'} - ${e ? fmt(e) : '未设结束'}`;
};
const dashboardMetric = (row = {}, keys = []) => {
for (const key of keys) {
const value = key.split('.').reduce((acc, part) => acc?.[part], row);
if (value !== undefined && value !== null && value !== '') return dashboardNumber(value);
}
return 0;
};
const dashboardPostViews = (post = {}) => dashboardMetric(post, ['views', 'play_count', 'plays', 'read_count', 'reads', 'organic_impressions', 'raw.views', 'raw.play_count', 'raw.read_count']);
const dashboardPostInteraction = (post = {}) => dashboardMetric(post, ['likes', 'like_count', 'raw.likes', 'raw.like_count'])
+ dashboardMetric(post, ['comments', 'comment_count', 'raw.comments', 'raw.comment_count'])
+ dashboardMetric(post, ['shares', 'share_count', 'reposts', 'raw.shares', 'raw.share_count'])
+ dashboardMetric(post, ['saves', 'collects', 'raw.saves', 'raw.collect_count']);
const dashboardIsExternalPost = (post = {}) => {
const type = `${post.source_type || ''} ${post.asset_type || ''} ${post.source_post_type || ''} ${post.relationship_status || ''}`.toLowerCase();
return Boolean(post.is_official_post === false || /earned|ugc|kol|external/.test(type));
};
const dashboardIsRiskComment = (comment = {}) => {
const level = String(comment.pr_risk_level || comment.risk_level || '').toLowerCase();
const sentiment = Number(comment.sentiment || 0);
const action = String(comment.action_status || '').toLowerCase();
return ['high', 'medium'].includes(level) || sentiment < -0.25 || /needs|review/.test(action);
};
const dashboardMergeRows = (baseRows = [], extraRows = [], idKey) => {
const map = new Map();
[...baseRows, ...extraRows].forEach(row => {
if (!row?.[idKey]) return;
map.set(row[idKey], { ...(map.get(row[idKey]) || {}), ...row });
});
return Array.from(map.values());
};
const dashboardBrandMatches = (brand = {}, brandId = '') => {
const brandNames = [brandId, brand?.id, brand?.name, brand?.cn].filter(Boolean).map(item => String(item).toLowerCase());
return (row = {}) => {
const rowBrand = String(row.brand_id || row.brandId || '').toLowerCase();
if (rowBrand && brandNames.includes(rowBrand)) return true;
const text = [row.account_id, row.account_name, row.name, row.handle, row.title, row.caption].filter(Boolean).join(' ').toLowerCase();
return brandNames.some(name => name && text.includes(name));
};
};
const dashboardActiveConstraints = (brand) => {
if (!brand) return [];
if (window.BrandMemoryStore?.getActiveConstraints) return window.BrandMemoryStore.getActiveConstraints(brand);
return Array.isArray(brand.constraints) ? brand.constraints.filter(item => item.enabled !== false) : [];
};
const dashboardLatestPostTime = (post = {}) => dashboardDateValue(post.published_at || post.publish_time || post.created_at || post.updated_at);
const dashboardInitiativeTypeLabel = (type = 'campaign') => ({
campaign: 'Campaign',
pillar: 'Pillar',
series: 'Series',
standalone: 'Content',
social_care: 'Care',
}[type] || 'Object');
const Dashboard = ({ onNav, workspaceContext }) => {
const brandStoreReady = Boolean(window.BrandMemoryStore?.useBrandMemoryProfiles);
const brandState = brandStoreReady ? window.BrandMemoryStore.useBrandMemoryProfiles() : { brands: [] };
const brandId = workspaceContext?.brandId || BRAND?.id || 'montx';
const workspaceBrand = (WORKSPACE_BRANDS || []).find(item => item.id === brandId) || WORKSPACE_BRANDS?.[0] || BRAND;
const brand = brandStoreReady ? window.BrandMemoryStore.getBrandProfile(brandId, brandState.brands) : workspaceBrand;
const brandMatches = dashboardBrandMatches(brand || workspaceBrand, brandId);
const [syncState, setSyncState] = React.useState({ accounts: [], posts: [], comments: [], status: 'idle', lastSync: '', errors: [] });
const [initiatives, setInitiatives] = React.useState([]);
const [marketState, setMarketState] = React.useState({ signals: [], reviewQueue: [], status: 'idle', errors: [] });
React.useEffect(() => {
let cancelled = false;
const loadDashboardData = async () => {
const [socialResult, initiativeResult, signalResult, reviewResult] = await Promise.allSettled([
fetch(dashboardApiUrl('/api/social/sync/status')).then(res => res.json()),
fetch(dashboardApiUrl('/api/initiatives')).then(res => res.json()),
fetch(dashboardApiUrl('/api/market/signals')).then(res => res.json()),
fetch(dashboardApiUrl('/api/market/review-queue')).then(res => res.json()),
]);
if (cancelled) return;
if (socialResult.status === 'fulfilled') {
const data = socialResult.value || {};
setSyncState({
accounts: data.accounts || [],
posts: data.posts || [],
comments: data.comments || [],
status: data.status || 'idle',
lastSync: data.lastFinishedAt || '',
nextRun: data.nextRunAt || '',
errors: data.errors || [],
});
} else {
setSyncState(prev => ({ ...prev, status: 'offline', errors: [{ message: '同步接口未连接' }] }));
}
if (initiativeResult.status === 'fulfilled') setInitiatives(initiativeResult.value?.initiatives || initiativeResult.value?.campaigns || []);
if (signalResult.status === 'fulfilled' || reviewResult.status === 'fulfilled') {
setMarketState({
signals: signalResult.status === 'fulfilled' ? (signalResult.value?.signals || []) : [],
reviewQueue: reviewResult.status === 'fulfilled' ? (reviewResult.value?.reviewQueue || reviewResult.value?.tasks || []) : [],
status: 'ok',
errors: [],
});
} else {
setMarketState({ signals: [], reviewQueue: [], status: 'offline', errors: [{ message: '市场感知接口未连接' }] });
}
};
loadDashboardData();
const timer = setInterval(loadDashboardData, 30000);
return () => {
cancelled = true;
clearInterval(timer);
};
}, [brandId]);
const accounts = React.useMemo(() => dashboardMergeRows(window.SOCIAL_ACCOUNTS || [], syncState.accounts || [], 'account_id').filter(brandMatches), [syncState.accounts, brandId, brand?.id]);
const accountIds = React.useMemo(() => new Set(accounts.map(account => account.account_id)), [accounts]);
const posts = React.useMemo(() => dashboardMergeRows(window.SOCIAL_POSTS || [], syncState.posts || [], 'post_id').filter(post => brandMatches(post) || accountIds.has(post.account_id)), [syncState.posts, accountIds, brandId, brand?.id]);
const comments = React.useMemo(() => {
const postIds = new Set(posts.map(post => post.post_id));
return dashboardMergeRows(window.SOCIAL_COMMENTS || [], syncState.comments || [], 'comment_id').filter(comment => postIds.has(comment.post_id));
}, [syncState.comments, posts]);
const ownedPosts = posts.filter(post => !dashboardIsExternalPost(post));
const brandInitiatives = initiatives.filter(item => (item.brandId || item.brand_id || brandId) === brandId && !['archived', 'done', 'completed'].includes(String(item.status || '').toLowerCase()));
const activeInitiatives = brandInitiatives.filter(item => ['active', 'running', 'live'].includes(String(item.status || '').toLowerCase()));
const activeCampaigns = activeInitiatives.filter(item => (item.type || 'campaign') === 'campaign');
const activeInitiative = activeInitiatives[0] || brandInitiatives[0] || null;
const activeConstraints = dashboardActiveConstraints(brand);
const productCount = Array.isArray(brand?.products) ? brand.products.length : Array.isArray(workspaceBrand?.products) ? workspaceBrand.products.length : 0;
const totalViews = ownedPosts.reduce((sum, post) => sum + dashboardPostViews(post), 0);
const totalInteraction = ownedPosts.reduce((sum, post) => sum + dashboardPostInteraction(post), 0);
const riskComments = comments.filter(dashboardIsRiskComment);
const openRiskComments = riskComments.filter(comment => !['resolved', 'closed'].includes(String(comment.action_status || '').toLowerCase()));
const marketSignals = marketState.signals || [];
const pendingReviews = marketState.reviewQueue || [];
const pendingEvents = [
...openRiskComments.map(comment => ({ type: 'risk', title: `风险评论待复核:${String(comment.text || comment.keyword || '未命名评论').slice(0, 26)}`, source: '发布监控 · 评论', meta: comment.pr_risk_level || comment.risk_level || 'needs-review', time: dashboardFormatDate(comment.created_at || comment.published_at, '待处理') })),
...pendingReviews.map(item => ({ type: 'risk', title: item.title || item.reason || '市场感知待复核', source: '市场感知 · 事实/风险待确认', meta: item.reason || item.risk_level || item.status || 'pending', time: dashboardFormatDate(item.created_at || item.updated_at, '待处理') })),
];
const latestPosts = [...ownedPosts].sort((a, b) => dashboardLatestPostTime(b) - dashboardLatestPostTime(a)).slice(0, 2);
const events = [
...pendingEvents.slice(0, 3),
...latestPosts.map(post => ({ type: 'ok', title: `已同步内容:${String(post.title || post.caption || '未命名内容').slice(0, 30)}`, source: `社媒资产 · ${post.platform || '未标记平台'}`, meta: `阅读/播放 ${dashboardCompactNumber(dashboardPostViews(post))}`, time: dashboardFormatDate(post.published_at || post.publish_time || post.created_at, '已同步') })),
marketSignals.length ? { type: 'opp', title: `市场感知输出 ${marketSignals.length} 条判断素材`, source: 'Market Sensing', meta: '机会 / 风险 / 变化 / 异常', time: marketState.status === 'ok' ? '已更新' : '待连接' } : null,
syncState.errors?.length ? { type: 'risk', title: '社媒同步存在错误', source: 'Social Sync', meta: syncState.errors.map(item => item.message || item.platform).filter(Boolean).join(' / ') || '查看同步日志', time: '需处理' } : null,
!posts.length ? { type: 'info', title: '暂无同步内容表现数据', source: '社媒资产', meta: '连接同步后自动生成首页指标', time: '待同步' } : null,
].filter(Boolean).slice(0, 5);
const lastSyncText = syncState.lastSync ? `上次同步 ${dashboardFormatDate(syncState.lastSync)}` : '等待社媒同步';
return (
{brand?.name || workspaceBrand?.name || 'Workspace'} · 社媒营销操作台
{openRiskComments.length || pendingReviews.length ? `你有 ${openRiskComments.length + pendingReviews.length} 条待处理判断` : '当前没有待处理风险判断'} · 首页只读取品牌记忆、社媒同步、市场感知和运营对象数据。
item.type === 'series').length} Series`} accent/>
);
};
const MetricCard = ({ label, value, delta, accent }) => (
);
const dashboardPostIds = (post = {}) => [post.post_id, post.content_id, post.id].filter(Boolean);
const dashboardPostBriefIds = (post = {}) => [
post.primaryMasterBriefId,
post.briefMapping?.primaryMasterBriefId,
...(post.masterBriefIds || []),
...(post.briefMapping?.masterBriefIds || []),
].filter(Boolean);
const dashboardInitiativeMatchesPost = (initiative = {}, post = {}) => {
const ids = new Set(initiative.assetPostIds || []);
if (dashboardPostIds(post).some(id => ids.has(id))) return true;
if (ids.size) return false;
const briefIds = new Set(dashboardPostBriefIds(post));
if (initiative.id && briefIds.has(initiative.id)) return true;
const parentIds = [post.parentCampaignId, post.campaignId, post.briefMapping?.parentCampaignId].filter(Boolean);
if (initiative.id && parentIds.includes(initiative.id)) return true;
if (ids.size || briefIds.size || parentIds.length) return false;
return (initiative.assetCampaignLabels || []).some(label => label && [post.campaign, post.product, post.content_type, post.title, post.caption].join(' ').includes(label));
};
const CurrentCampaignCard = ({ initiative, posts, comments, totalViews, totalInteraction, onNav }) => {
if (!initiative) return (
当前运营对象
未创建
尚未读取到当前品牌的运营对象。创建 Campaign、Series 或 Content 后,首页会按 PRD 的中枢闭环展示进度和表现。
数据来源:campaign-state / Brand Memory / Social Sync
);
const linkedPosts = posts.filter(post => dashboardInitiativeMatchesPost(initiative, post));
const linkedViews = linkedPosts.reduce((sum, post) => sum + dashboardPostViews(post), 0);
const linkedInteraction = linkedPosts.reduce((sum, post) => sum + dashboardPostInteraction(post), 0);
const linkedComments = new Set(linkedPosts.map(post => post.post_id));
const linkedRisk = comments.filter(comment => linkedComments.has(comment.post_id) && dashboardIsRiskComment(comment)).length;
const steps = [
{ label: '品牌记忆', status: 'done', note: '已加载' },
{ label: '市场感知', status: linkedPosts.length ? 'done' : 'active', note: linkedPosts.length ? `${linkedPosts.length} 内容证据` : '等待证据' },
{ label: '策略推演', status: initiative.coreMessage || initiative.goals?.length ? 'done' : 'active', note: initiative.stage || dashboardInitiativeTypeLabel(initiative.type) },
{ label: 'Brief 编排', status: initiative.masterBrief ? 'done' : 'pending', note: initiative.masterBrief?.version || '待生成' },
{ label: '发布监控', status: linkedPosts.length ? 'active' : 'pending', note: `${linkedPosts.length} 已关联` },
{ label: '复盘学习', status: linkedRisk ? 'active' : (linkedPosts.length ? 'pending' : 'pending'), note: linkedRisk ? `${linkedRisk} 风险待处理` : '待写回' },
];
const engagementRate = linkedViews ? `${((linkedInteraction / linkedViews) * 100).toFixed(2)}%` : '待计算';
return (
当前运营对象
{initiative.name || '未命名对象'}
{dashboardInitiativeTypeLabel(initiative.type)}
{initiative.coreMessage || initiative.masterBrief?.objective || '当前对象缺少核心命题,需要在策略推演里补齐目标、受众和内容边界。'}
{dashboardFormatRange(initiative.startDate, initiative.endDate)} · {initiative.masterBrief?.title || 'Master Brief 未生成'}
);
};
function PipelineBar({ steps = [], label = 'PIPELINE' }) {
const fallbackSteps = [
{ label: '品牌记忆', status: 'pending', note: '待加载' },
{ label: '市场感知', status: 'pending', note: '待同步' },
{ label: '策略推演', status: 'pending', note: '待创建' },
{ label: 'Brief 编排', status: 'pending', note: '待生成' },
{ label: '发布监控', status: 'pending', note: '待关联' },
{ label: '复盘学习', status: 'pending', note: '待触发' },
];
const rows = steps.length ? steps : fallbackSteps;
const doneCount = rows.filter(s => s.status === 'done').length;
const activeCount = rows.filter(s => s.status === 'active').length;
const donePct = rows.length ? (doneCount / rows.length) * 100 : 0;
const activePct = rows.length ? (activeCount / rows.length) * 100 : 0;
return (
{label}
{doneCount}/{rows.length} 已完成 · {activeCount} 进行中
{rows.map((s, i) => (
))}
); }
const SubMetric = ({ label, value, target, pct, accent }) => (
{label}
{value}
{target} · {pct}%
);
const EventFeedCard = ({ events = [], pendingCount = 0, syncState = {} }) => (
事件流 · Live
{events.length} 条
{pendingCount ? `${pendingCount} pending` : (syncState.status || 'idle')}
{events.map((e, i) => (
{{risk:'风险', opp:'机会', ok:'完成', info:'提示'}[e.type]}
{e.title}
{e.source} · {e.meta}
{e.time}
))}
{!events.length &&
暂无事件。连接社媒同步或市场感知后会自动出现风险、机会和异常判断。
}
);
const SystemHealthCard = ({ brand, syncState = {}, initiatives = [], posts = [], marketState = {}, riskCount = 0 }) => {
const modules = [
['brand-memory', '品牌记忆', brand ? 'ok' : 'idle', brand ? 'profile loaded' : 'no profile'],
['social-sync', '社媒同步', syncState.status === 'ok' || posts.length ? 'ok' : syncState.errors?.length ? 'warn' : 'idle', `${posts.length} posts`],
['market-sensing', '市场感知', marketState.signals?.length ? 'ok' : marketState.status === 'offline' ? 'warn' : 'idle', `${marketState.signals?.length || 0} signals`],
['campaign-sim', '策略推演', initiatives.length ? 'ok' : 'idle', `${initiatives.length} objects`],
['brief-engine', 'Brief 编排', initiatives.some(item => item.masterBrief) ? 'ok' : 'idle', `${initiatives.filter(item => item.masterBrief).length} briefs`],
['publishing', '发布监控', posts.length ? 'ok' : 'idle', `${posts.length} assets`],
['risk-mon', '风险监听', riskCount ? 'warn' : 'ok', `${riskCount} open`],
['learning', '学习闭环', posts.length ? 'idle' : 'idle', posts.length ? 'ready for review' : 'waiting data'],
];
return (
Agent 集群 · 健康度
{modules.length} modules
{modules.map(([id, name, s, note]) => (
))}
); };
const BrandPulseCard = ({ posts = [] }) => {
const dailyRows = React.useMemo(() => {
const map = new Map();
posts.forEach(post => {
const time = dashboardLatestPostTime(post);
if (!time) return;
const key = new Date(time).toISOString().slice(5, 10);
const row = map.get(key) || { label: key, views: 0, interaction: 0 };
row.views += dashboardPostViews(post);
row.interaction += dashboardPostInteraction(post);
map.set(key, row);
});
return Array.from(map.values()).sort((a, b) => a.label.localeCompare(b.label)).slice(-14);
}, [posts]);
const maxViews = Math.max(...dailyRows.map(row => row.views), 1);
const maxInteraction = Math.max(...dailyRows.map(row => row.interaction), 1);
const toPoints = (rows, key, max, minY = 22, maxY = 126) => rows.map((row, i) => {
const x = rows.length <= 1 ? 0 : (i / (rows.length - 1)) * 400;
const y = maxY - (row[key] / max) * (maxY - minY);
return `${x.toFixed(1)},${y.toFixed(1)}`;
}).join(' ');
const viewPoints = toPoints(dailyRows, 'views', maxViews);
const interactionPoints = toPoints(dailyRows, 'interaction', maxInteraction, 56, 126);
return (
品牌脉搏 · 近 14 天
{dailyRows.length ? `${dailyRows.length} 天` : '待同步'}
{dailyRows.length ? dailyRows.filter((_, i) => i === 0 || i === dailyRows.length - 1 || i % Math.ceil(dailyRows.length / 4) === 0).map(row => {row.label}) : 等待 social-sync-state}
); };
const Legend = ({ color, label, dashed }) => (
{label}
);
// ========== 01 · BRAND MEMORY ==========
const MEMORY_DOCS = [
{ name: '领越品牌核心讯息-0420-V1.pdf', size: '504 KB', status: 'parsed', facts: 42, time: '04-20' },
{ name: '广汽领程新品牌品牌策略方案-0418_final.pptx', size: '32 MB', status: 'parsed', facts: 58, time: '04-18' },
{ name: 'MONTX_GUIDELINES_V1.1.pptx', size: '14 MB', status: 'parsed', facts: 31, time: '04-2026' },
{ name: '全域跨界新物种降临—MONTX发布两款概念车.docx', size: '28 KB', status: 'parsed', facts: 76, time: '04-23' },
{ name: '2026 北京车展・杜培源讲话稿.docx', size: '28 KB', status: 'parsed', facts: 64, time: '04-20' },
{ name: '牛马野逍遥-0415.pptx', size: '31 MB', status: 'parsed', facts: 49, time: '04-15' },
{ name: 'TVC 广汽新物种创意传播内容0414.pptx', size: '208 MB', status: 'parsed', facts: 18, time: '04-14' },
{ name: 'MONTX 社媒平台账号主页品牌头像.png/jpg', size: '1080px', status: 'parsed', facts: 3, time: 'asset' },
];
const MONTX_MEMORY_SOURCE = {
brand: [
{ label: '品牌名', value: 'MONTX / 领越', meta: '广汽领程新品牌' },
{ label: '品牌定位', value: '全域跨界新物种', meta: 'New Breed. No Boundaries.' },
{ label: '品牌口号', value: '领现在,越未来', meta: 'New Breed. No Boundaries.' },
{ label: '品牌愿景', value: '共创更自由美好的事业与生活', meta: '事业与生活双场景' },
{ label: '品牌使命', value: '为用户打造一车多能的全域场景新体验', meta: '一车多能 / 自由切换' },
{ label: '目标用户', value: '注重跨界多用途的“尝鲜型”用户', meta: '商业装载 + 生活方式 + 探索需求' },
],
values: [
{ title: '全域智能', text: '智能科技与新能源驱动模式加持,覆盖城市通勤、越野脱困、重载运输、智能座舱等复合场景。' },
{ title: '全域适应', text: '越级性能和跨界功能带来全域地形、商业运载、休旅生活与移动办公的适应能力。' },
{ title: '全域美学', text: '以功能为本,探索跨界先锋设计理念,让实用属性与先锋美感自然共生。' },
],
products: [
{ name: 'MONTX P10', tag: '概念车 · 全域移动地空母舰', text: '未来先锋皮卡,从单一装载工具升维为支持立体出行的未来装备。核心叙事包含地空双栖、四轮分布式驱动电机、智能底盘、车机协同、机能美学与战术副手。', source: '讲话稿 / 产品稿' },
{ name: 'MONTX V10', tag: '概念车 · 全域移动整备舱', text: '面向数字游民、移动办公与长周期旅居的概念 VAN。核心叙事包含智能双舱、模块化座舱、工作/社交/睡眠切换、四开式尾门、自循环基地与隐形管家。', source: '讲话稿 / 产品稿' },
],
visual: [
{ label: 'Logo 含义', value: 'Brandmark 融合字母 M 与山峰/车辙意象,表达 rugged capability、adventure、freedom to explore。' },
{ label: 'Brandmark 安全区', value: '四周保留 50% X,确保图形清晰突出。' },
{ label: 'Vertical Lockup', value: 'Wordmark 高度为 brandmark 的 33%,两者间距为 brandmark 的 66%,整体数学居中。' },
{ label: 'Horizontal Bilingual', value: '仅用于中文市场;wordmark 高度为 brandmark 的 75%,横向中英组合安全区为 150% X。' },
{ label: '产品 Logo 工艺', value: 'Brushed metal / chrome / internal illuminated,用于车标、尾标和产品背部发光标识。' },
{ label: '社媒头像', value: 'MONTX 社媒平台账号主页品牌头像 PNG/JPG,1080 x 1080。' },
],
campaigns: [
{ title: '品牌视频 / TVC', text: '从用户“不想妥协”的洞察切入,讲品牌由来与实力,再呈现多场景体验,最终回到定位与口号。' },
{ title: 'Hyper 预热视频', text: '关键词:Hyper Performance / Hyper Intelligence / Hyper Design / Hyper Capability / Hyper Safety。' },
{ title: '牛马野逍遥共创计划', text: '真开源 + 真共创 + 真定制;用户从 0 到 1 主导整车设计,KOL 从代言升级为联合发起人。' },
{ title: '共创传播矩阵', text: '头部 KOL FREEROAD 自由公路主导共创流程,腰部数字游民/旅行/科技达人扩散,官方账号承接直播与切片。' },
],
guardrails: [
{ level: 'P0', rule: 'P10/V10 当前均为概念车叙事,载人飞行器、线控滑移方向盘、时间感知自动进化等不得写成量产承诺。' },
{ level: 'P0', rule: '涉及自动识别、自动接管、主动调整时,禁止暗示完全自动驾驶、无人驾驶、替代驾驶员或绝对安全。' },
{ level: 'P1', rule: '“牛马”只允许在“牛马野逍遥”共创计划语境内出现,不进入品牌常规口吻,不替用户自嘲。' },
{ level: 'P1', rule: '“财富自由”只作为历史讲话素材引用,常规生成中避免金融收益或商业成功的夸大承诺。' },
{ level: 'P1', rule: '品牌语气可以开拓、全域、机能、先锋、共创,但不能轻浮玩梗、竞品碾压或过度硬广。' },
],
};
const BRAND_MEMORY_TABS = [
{ id: 'brand', label: '品牌主档' },
{ id: 'product', label: '产品 Profile' },
{ id: 'visual', label: '视觉规范' },
{ id: 'campaign', label: '策略对象记忆' },
{ id: 'operation', label: '运营阶段' },
{ id: 'social', label: '社媒内容' },
{ id: 'preference', label: '偏好决策' },
{ id: 'evidence', label: '证据写回' },
{ id: 'guardrail', label: '护栏规则' },
];
const memoryNumber = (value) => Number(value || 0);
const formatMemoryNumber = (value) => {
const n = memoryNumber(value);
if (n >= 100000000) return `${(n / 100000000).toFixed(1)}亿`;
if (n >= 10000) return `${(n / 10000).toFixed(1)}万`;
return String(Math.round(n));
};
const mergeMemoryRows = (baseRows = [], extraRows = [], idKey) => {
const map = new Map();
[...baseRows, ...extraRows].forEach(row => {
if (!row?.[idKey]) return;
map.set(row[idKey], { ...(map.get(row[idKey]) || {}), ...row });
});
return Array.from(map.values());
};
const getMemoryPostUrl = (post = {}) => post.url || post.manual_metrics?.url || post.raw?.share_url || '';
const BrandMemory = ({ workspaceContext }) => {
const brandStoreReady = Boolean(window.BrandMemoryStore?.useBrandMemoryProfiles);
const brandToolsReady = Boolean(brandStoreReady && window.BrandProductMaintenancePanel && window.GuardrailMaintenancePanel);
const brandState = brandStoreReady ? window.BrandMemoryStore.useBrandMemoryProfiles() : { brands: [], setBrands: () => {}, ready: false };
const [syncedAssets, setSyncedAssets] = React.useState({ accounts: [], posts: [], comments: [], lastSync: '', status: 'idle', errors: [] });
const [socialWriteStatus, setSocialWriteStatus] = React.useState('');
const [memoryTab, setMemoryTab] = React.useState('brand');
const brands = brandState.brands || [];
const brand = brandStoreReady ? window.BrandMemoryStore.getBrandProfile(workspaceContext?.brandId, brands) : null;
const BrandProductMaintenance = brandToolsReady ? window.BrandProductMaintenancePanel : null;
const GuardrailMaintenance = brandToolsReady ? window.GuardrailMaintenancePanel : null;
const updateBrand = (brandId, nextBrand) => {
if (!brandStoreReady) return;
brandState.setBrands(prev => prev.map(item => item.id === brandId ? window.BrandMemoryStore.normalizeBrandProfiles([nextBrand])[0] : item));
};
const activeConstraints = brandStoreReady && brand ? window.BrandMemoryStore.getActiveConstraints(brand) : [];
React.useEffect(() => {
let cancelled = false;
const load = async () => {
try {
const response = await fetch('/api/social/sync/status');
const data = await response.json();
if (!cancelled && response.ok) {
setSyncedAssets({
accounts: data.accounts || [],
posts: data.posts || [],
comments: data.comments || [],
lastSync: data.lastFinishedAt || '',
status: data.status || 'idle',
errors: data.errors || [],
});
}
} catch (error) {
if (!cancelled) setSyncedAssets(prev => ({ ...prev, errors: [{ platform: 'local', message: error.message || '读取社媒同步状态失败' }] }));
}
};
load();
const timer = setInterval(load, 30000);
return () => {
cancelled = true;
clearInterval(timer);
};
}, []);
const socialMemory = React.useMemo(() => {
if (!brandStoreReady || !window.BrandMemoryStore?.buildSocialMemorySnapshot) return null;
const accounts = mergeMemoryRows(window.SOCIAL_ACCOUNTS || [], syncedAssets.accounts || [], 'account_id');
const posts = mergeMemoryRows(window.SOCIAL_POSTS || [], syncedAssets.posts || [], 'post_id');
const comments = mergeMemoryRows(window.SOCIAL_COMMENTS || [], syncedAssets.comments || [], 'comment_id');
return window.BrandMemoryStore.buildSocialMemorySnapshot({
brandId: brand?.id || workspaceContext?.brandId || BRAND.id,
accounts,
posts,
comments,
lastSync: syncedAssets.lastSync,
});
}, [brandStoreReady, brand?.id, workspaceContext?.brandId, syncedAssets]);
const writeSocialMemory = () => {
if (!brandStoreReady || !brand || !socialMemory || !window.BrandMemoryStore?.applySocialMemorySnapshot) return;
const nextBrand = window.BrandMemoryStore.applySocialMemorySnapshot(brand, socialMemory);
updateBrand(brand.id, nextBrand);
setSocialWriteStatus(`已写入 ${socialMemory.accounts.length} 个账号 / ${socialMemory.approvedPosts.length} 条客户认可内容`);
};
return (
品牌记忆 · 长期认知构建中
上传的每一份资料都会被解析为事实、偏好与决策,写入可被策略对象调用的品牌图谱。
Workspace · {brand?.name || BRAND.name}
brand && updateBrand(brand.id, nextBrand)}
/>
{brandToolsReady && brandState.ready && brand && (
品牌治理 · Brand Governance
{brand.name} · {activeConstraints.length} active rules · {(brand.products || []).length} products
source: Brand Memory
product.name).join(' / ') || '未配置'}/>
updateBrand(brand.id, nextBrand)}
/>
updateBrand(brand.id, nextBrand)}
/>
)}
已入库资料
{MEMORY_DOCS.length}
{MEMORY_DOCS.map((d, i) => (
{d.name}
{d.size} · {d.time}
{d.status === 'parsed' && {d.facts} facts}
{d.status === 'parsing' && 解析中}
{d.status === 'queued' && queued}
))}
品牌图谱 · Brand Memory Graph
1,284 节点 · 3,820 关系
layout: radial
);
};
const getBrandLogoAssets = visualSystem => (visualSystem?.logoAssets || []).filter(asset => asset?.src);
const MEMORY_FACT_STATUS_LABELS = {
confirmed: { label: '原文确定', tone: 'confirmed' },
documented: { label: '原文确定', tone: 'confirmed' },
'documented-draft': { label: '初稿确定', tone: 'draft' },
'superseded-draft': { label: '历史初稿', tone: 'draft' },
inferred: { label: '推测提取', tone: 'inferred' },
pending: { label: '待确认', tone: 'pending' },
'pending-confirmation': { label: '待确认', tone: 'pending' },
'conflict-adjusted': { label: '冲突修订', tone: 'conflict' },
resolved: { label: '冲突已处理', tone: 'confirmed' },
archived: { label: '已归档', tone: 'draft' },
rejected: { label: '已拒绝', tone: 'draft' },
};
const memoryList = value => Array.isArray(value) ? value.filter(Boolean) : (value ? [value] : []);
const memoryUniq = items => Array.from(new Set((items || []).filter(Boolean)));
const inferMemoryFactStatus = (item = {}, fallback = {}) => {
const raw = item.factStatus || item.traceStatus || fallback.factStatus || fallback.traceStatus || item.status || fallback.status;
if (raw === 'active') return 'confirmed';
if (raw === 'confirmed') return 'confirmed';
if (raw === 'pending') return 'pending-confirmation';
if (raw === 'superseded-draft') return 'superseded-draft';
if (raw) return raw;
const provenance = item.provenance || fallback.provenance || '';
if (String(provenance).includes('conflict')) return 'conflict-adjusted';
if (String(provenance).includes('inferred')) return 'inferred';
return 'confirmed';
};
const getMemoryFactStatus = status => MEMORY_FACT_STATUS_LABELS[status] || { label: status || '未标注', tone: 'neutral' };
const getMemoryTrace = (item = {}, fallback = {}) => ({
factStatus: inferMemoryFactStatus(item, fallback),
provenance: item.provenance || fallback.provenance || 'documented',
source: item.source || item.sourceEvidenceId || fallback.source || fallback.sourceEvidenceId || 'brand-memory',
sourceRefs: memoryUniq([
...memoryList(fallback.sourceRefs),
...memoryList(item.sourceRefs),
]),
conflicts: memoryUniq([
...memoryList(fallback.conflicts),
...memoryList(item.conflicts),
...memoryList(item.conflictId),
]),
});
const MemoryFactChip = ({ status }) => {
const meta = getMemoryFactStatus(status);
const colorMap = {
confirmed: { color: 'var(--accent)' },
draft: { color: 'var(--ink-3)' },
inferred: { color: '#7c5a00' },
pending: { color: '#8a4b00' },
conflict: { color: '#580018' },
};
return {meta.label};
};
const MemoryTraceMeta = ({ item = {}, fallback = {}, compact = false }) => {
const trace = getMemoryTrace(item, fallback);
return (
{trace.provenance && {trace.provenance}}
{trace.source && source: {trace.source}}
{trace.sourceRefs.slice(0, compact ? 2 : 6).map(ref => ref: {ref})}
{trace.conflicts.map(ref => conflict: {ref})}
);
};
const OperatingStageMemoryPanel = ({ brand = {} }) => {
const stages = brand.projectOperatingStages || [];
const confirmedCount = stages.filter(item => item.factStatus === 'confirmed').length;
const inferredCount = stages.filter(item => item.factStatus === 'inferred').length;
const adjustedCount = stages.filter(item => item.factStatus === 'conflict-adjusted' || (item.operatingPreferences || []).some(pref => pref.factStatus === 'conflict-adjusted')).length;
const pendingCount = stages.filter(item => item.factStatus === 'pending-confirmation' || (item.operatingPreferences || []).some(pref => pref.factStatus === 'pending-confirmation')).length;
const renderTagList = (items = [], prefix = '') => (
{items.map(item => {prefix}{item})}
);
return (
0}/>
统一溯源规则
品牌记忆每个模块都使用同一套标识:原文确定可直接进入报表;推测提取只能作为待校验摘要;待确认进入写回队列;冲突修订会保留原始来源与冲突 ID,方便回溯。
{stages.map(stage => (
{stage.period || '阶段周期待确认'}
{stage.name}
阶段目标
{renderTagList(stage.stageGoals || [], '')}
阶段任务
{renderTagList(stage.stageObjectives || [], '')}
策略边界
{renderTagList(stage.brandStrategyBoundaries || [], '')}
运营偏好
{(stage.operatingPreferences || []).map(pref => (
))}
平台 / 下游依赖
{stage.platformPlan?.overseas?.length ? renderTagList(stage.platformPlan.overseas, '海外: ') : null}
{stage.platformPlan?.domestic?.length ? renderTagList(stage.platformPlan.domestic, '国内: ') : null}
{stage.downstream?.length ? renderTagList(stage.downstream, 'downstream: ') : null}
{stage.conflicts?.length ? renderTagList(stage.conflicts, 'conflict: ') : null}
{stage.sourceRefs?.length ? (
{stage.sourceRefs.map(ref => source: {ref})}
) : null}
))}
);
};
const PreferenceDecisionMemoryPanel = ({ brand = {} }) => {
const preferences = brand.preferences || [];
const decisions = brand.decisions || [];
const preferCount = preferences.filter(item => item.polarity === 'prefer').length;
const avoidCount = preferences.filter(item => item.polarity === 'avoid').length;
return (
item.status === 'confirmed').length} confirmed`}/>
item.stability === 'long-term').length)} delta="不随短期数据变化"/>
客户偏好池
{preferences.map(item => (
{item.polarity === 'avoid' ? 'Avoid' : 'Prefer'}
{item.stability || 'medium'}
{item.label}
{item.description}
))}
历史决策摘要
{decisions.map(item => (
{item.status || 'pending'}
{item.type || 'decision'}
{item.title}
{item.rationale || item.text}
{(item.impact || []).map(scope => {scope})}
))}
);
};
const EvidenceWritebackMemoryPanel = ({ brand = {}, onUpdateBrand }) => {
const evidenceSources = brand.evidenceSources || [];
const learnings = brand.learnings || [];
const conflicts = brand.memoryConflicts || [];
const pendingConflicts = conflicts.filter(item => item.status === 'pending');
const pendingLearnings = learnings.filter(item => item.status === 'pending');
const conflictActionText = {
accept_current: '以当前事实为准',
keep_as_draft: '仅保留初稿',
needs_more_review: '稍后处理',
};
const resolveConflict = (conflictId, resolution) => {
if (!onUpdateBrand || !window.BrandMemoryStore?.applyMemoryConflictResolution) return;
onUpdateBrand(window.BrandMemoryStore.applyMemoryConflictResolution(brand, conflictId, resolution));
};
const updateLearningStatus = (learningId, status) => {
if (!onUpdateBrand) return;
const stampedAt = new Date().toLocaleString('zh-CN', { hour12: false });
const nextLearnings = learnings.map(item => item.id === learningId ? {
...item,
status,
[status === 'confirmed' ? 'confirmedAt' : 'rejectedAt']: stampedAt,
} : item);
const nextEvidenceSources = evidenceSources.map(source => (
source.id === 'source-official-social' && status === 'confirmed'
? { ...source, status: 'confirmed', updatedAt: stampedAt }
: source
));
onUpdateBrand({ ...brand, learnings: nextLearnings, evidenceSources: nextEvidenceSources, updatedAt: stampedAt });
};
return (
item.status === 'confirmed').length} confirmed`}/>
0}/>
item.status === 'confirmed').length)} delta="confirmed learning"/>
冲突通知
{pendingConflicts.length ? pendingConflicts.map(conflict => (
Conflict
{conflict.severity || 'needs-confirmation'}
{conflict.title}
{conflict.message}
初稿信息
{conflict.draftClaim}
当前事实
{conflict.currentFact}
{(conflict.downstream || []).map(item => {item})}
{(conflict.options || ['accept_current', 'keep_as_draft', 'needs_more_review']).map(option => (
))}
)) : (
Conflict
暂无冲突待确认
当新资料与当前执行事实、官方账号数据或已确认记忆冲突时,会先进入这里等待用户明确。
)}
待确认写回
{pendingLearnings.length ? pendingLearnings.map(item => (
{item.type || 'learning'}
{item.status}
{item.title}
{item.text}
{item.evidencePostIds?.length ?
evidence posts: {item.evidencePostIds.length} : null}
)) : (
Queue
暂无待确认写回
线下导入、社媒内容归档、复盘学习都会先进入这里,确认后再成为稳定记忆。
)}
来源证据链
{evidenceSources.map(source => (
{source.type}
{source.status}
{source.stability}
{source.title}
{(source.facts || []).join(';')}
{source.confidence || 'medium'} confidence
))}
);
};
const GuardrailRulesMemoryPanel = ({ rules = [] }) => {
const requiredCount = rules.filter(rule => (rule.severity || rule.level) === 'required' || rule.level === 'P0').length;
const categories = Array.from(new Set(rules.map(rule => rule.category).filter(Boolean)));
return (
{rules.map(item => {
const isRequired = (item.severity || item.level) === 'required' || item.level === 'P0';
const scopes = item.scope || [];
const terms = item.forbiddenTerms || [];
const levelLabel = item.level || (isRequired ? 'P0' : 'P1');
return (
{levelLabel}
{item.label || `${item.level} · 品牌护栏`}
{item.description || item.rule}
{(scopes.length || terms.length) ? (
{scopes.length ? (
{scopes.map(scope => {scope})}
) : null}
{terms.length ? (
{terms.slice(0, 8).map(term => 禁:{term})}
) : null}
) : null}
{item.category || 'guardrail'}
);
})}
);
};
const BrandLogoMemoryPreview = ({ visualSystem = {} }) => {
const logoAssets = getBrandLogoAssets(visualSystem);
if (!logoAssets.length) return null;
const brandmark = logoAssets.find(asset => asset.type === 'brandmark') || logoAssets[0];
const wordmark = logoAssets.find(asset => asset.id === 'wordmark-ink') || logoAssets.find(asset => asset.type === 'wordmark') || logoAssets[0];
const previewItems = [
{ ...brandmark, label: 'Primary Brandmark', boxStyle: { maxWidth: 118, maxHeight: 118 } },
{ ...wordmark, label: 'Primary Wordmark', boxStyle: { maxWidth: 360, maxHeight: 82 } },
];
return (
{previewItems.map(asset => (
{asset.label}
{asset.name}
{asset.usage}
))}
);
};
const StructuredBrandMemoryPanel = ({ activeTab, onTab, brand, activeConstraints = [], socialMemory, socialWriteStatus, onWriteSocialMemory, onUpdateBrand }) => {
const source = MONTX_MEMORY_SOURCE;
const products = brand?.id === 'montx' && brand.products?.length
? brand.products
: [];
const pendingConflicts = (brand?.memoryConflicts || []).filter(item => item.status === 'pending');
return (
结构化品牌记忆 · Structured Memory
MONTX / 领越 · 8 source files · {activeConstraints.length || source.guardrails.length} active rules
source: /Downloads/广汽品牌资产
{pendingConflicts.length > 0 && (
发现 {pendingConflicts.length} 个品牌记忆冲突,需要人工确认
新资料不会直接覆盖当前事实;请在证据写回里选择以当前事实为准、仅保留初稿或稍后处理。
)}
全局来源标识
本页所有品牌记忆条目都必须带确定性与来源。推测、待确认、冲突修订不会静默覆盖原始文档,会保留来源和冲突 ID 供后续回溯。
{BRAND_MEMORY_TABS.map(tab => (
))}
{activeTab === 'brand' && (
{source.brand.map(item => (
{item.label}
{item.value}
{item.meta}
))}
{source.values.map(item => (
))}
)}
{activeTab === 'product' && (
{(products.length ? products : source.products).map(product => (
{product.name}
{product.narrative?.positioning || product.text || product.positioning}
{(product.narrative?.coreCapabilities || [product.tag, product.source]).filter(Boolean).slice(0, 7).map(item => {item})}
))}
)}
{activeTab === 'visual' && (
)}
{activeTab === 'campaign' && (
{source.campaigns.map(item => (
Campaign
{item.title}
{item.text}
))}
)}
{activeTab === 'operation' && (
)}
{activeTab === 'social' && (
)}
{activeTab === 'preference' && (
)}
{activeTab === 'evidence' && (
)}
{activeTab === 'guardrail' && (
)}
);
};
const VisualSystemMemoryPanel = ({ visualSystem = {}, sourceVisual = [] }) => {
const logoAssets = getBrandLogoAssets(visualSystem);
const colorTokens = visualSystem.colorTokens || [];
const semanticColors = visualSystem.semanticColors || {};
const viGuidelines = visualSystem.viGuidelines || [];
const logoLockups = visualSystem.logoLockups || [];
const typography = visualSystem.typography || [];
const applicationRules = visualSystem.applicationRules || [];
const groupedRules = [
{ label: 'Color Rules', items: visualSystem.colorRules || [] },
{ label: 'Logo Rules', items: visualSystem.logoRules || [] },
{ label: 'Layout Rules', items: visualSystem.layoutRules || [] },
{ label: 'Image Rules', items: visualSystem.imageRules || [] },
].filter(group => group.items.length);
return (
{viGuidelines.length > 0 && (
VI Source · {viGuidelines[0].name}
{viGuidelines[0].file} · {viGuidelines[0].pages} pages · {viGuidelines[0].source}
)}
{logoAssets.length > 0 && (
{logoAssets.map(asset => (
{asset.type || 'logo'}
{asset.name}
{asset.usage}
))}
)}
{colorTokens.length > 0 && (
{colorTokens.map(token => {
const swatch = token.alpha ? `rgba(${token.rgb}, ${token.alpha})` : token.hex;
const darkText = ['#E1E0D8', '#FFFFFF'].includes(String(token.hex || '').toUpperCase()) || token.alpha;
return (
{token.hex}{token.alpha ? ` / ${Math.round(token.alpha * 100)}%` : ''}
{token.role}
{token.name}
{token.usage}
{token.rgb && RGB {token.rgb}}
{token.cmyk && {token.cmyk}}
{token.pms && PMS {token.pms}}
);
})}
)}
{Object.keys(semanticColors).length > 0 && (
Semantic Color Tokens
供长图文、内容生成、视觉检查共用的语义色值。
{Object.entries(semanticColors).map(([key, value]) => {key}: {value})}
)}
{logoLockups.length > 0 && (
{logoLockups.map(lockup => (
Logo Lockup
{lockup.name}
{lockup.usage}
{[lockup.construction, lockup.isolation].filter(Boolean).join(';')}
))}
)}
{typography.length > 0 && (
{typography.map(type => (
{type.role}
{type.name}
{type.usage}
))}
)}
{groupedRules.length > 0 && (
{groupedRules.map(group => (
{group.label}
{group.items.length} 条结构化规则
{group.items.join(';')}
))}
)}
{applicationRules.length > 0 && (
{applicationRules.map(rule => (
VI Application
{rule.name}
{rule.usage}
))}
)}
{sourceVisual.map(item => (
Source Visual
{item.label}
{item.value}
))}
);
};
const SocialContentMemoryPanel = ({ socialMemory, savedSocialAssets, writeStatus, onWrite }) => {
const accounts = socialMemory?.accounts || [];
const posts = socialMemory?.approvedPosts || [];
const comments = socialMemory?.commentsSummary || {};
const savedAt = savedSocialAssets?.updatedAt || '';
const accountPanelStyle = {display:'grid', gap:8, alignContent:'start'};
const socialCardStyle = {background:'var(--bg-2)', border:'1px solid var(--hairline)', borderRadius:8, padding:10, minWidth:0};
return (
account.platform).filter(Boolean).slice(0, 4).join(' / ') || '待同步'}/>
客户认可内容 · Approved Social Evidence
官方账号已发布内容作为品牌表达证据;写入后进入 pending learning,后续再人工提升为规则或案例。
{writeStatus && {writeStatus}}
官方账号
{accounts.length ? accounts.map(account => (
{account.platform} · {account.account_name || account.handle}
{account.positioning || account.owner || account.handle || '官方账号资产'}
{account.status || 'active'}
{account.follower_delta_period ? +{formatMemoryNumber(account.follower_delta_period)} followers : null}
)) : (
暂无账号同步社媒监测同步后,这里会显示官方账号资产。
)}
认可内容证据池
{posts.length} posts
{posts.length ? posts.map(post => (
{post.cover_url ? (

) : (
)}
{post.title}
{post.platform} · {post.content_type || '内容'} · {post.campaign || '未标记策略对象'}
阅读/播放 {formatMemoryNumber(post.metrics?.views || 0)}
赞 {formatMemoryNumber(post.metrics?.likes || 0)}
评 {formatMemoryNumber(post.metrics?.comments || 0)}
转 {formatMemoryNumber(post.metrics?.shares || 0)}
{post.url &&
打开}
)) : (
暂无已发布内容
同步官方账号内容后,会按互动与发布时间沉淀为客户认可样本。
)}
);
};
const UploadZone = () => (
拖入文档 · 会议纪要 · 图片
PDF · DOCX · MD · TXT · PNG
);
const BrandGraph = () => {
// Radial network viz — hand-composed positions
const cx = 320, cy = 220;
const center = { x: cx, y: cy, label: 'MONTX\n领越', type: 'brand', r: 32 };
const hubs = [
{ x: cx + 140, y: cy - 120, label: '品牌定位', cat: 'identity', r: 20 },
{ x: cx - 160, y: cy - 90, label: '产品矩阵', cat: 'product', r: 20 },
{ x: cx - 200, y: cy + 60, label: '语言边界', cat: 'boundary', r: 20 },
{ x: cx - 80, y: cy + 150, label: '历史决策', cat: 'decision', r: 20 },
{ x: cx + 100, y: cy + 160, label: '卖点池', cat: 'selling', r: 20 },
{ x: cx + 220, y: cy + 30, label: '风险词', cat: 'risk', r: 20 },
{ x: cx + 60, y: cy - 180, label: '客户偏好', cat: 'preference', r: 20 },
];
const leaves = [
{ hub: 0, offX: 90, offY: -30, label: '全域跨界' },
{ hub: 0, offX: 110, offY: 20, label: '新物种' },
{ hub: 1, offX: -90, offY: 10, label: 'P10 概念车' },
{ hub: 1, offX: -80, offY: -40, label: 'V10 概念车' },
{ hub: 1, offX: -100, offY: 55, label: 'GAIA 平台' },
{ hub: 2, offX: -80, offY: 20, label: '禁完全自动驾驶' },
{ hub: 2, offX: -60, offY: 60, label: '禁量产承诺' },
{ hub: 3, offX: -40, offY: 60, label: '用户共创' },
{ hub: 3, offX: 60, offY: 40, label: '场景定义品牌' },
{ hub: 4, offX: 40, offY: 70, label: '全域智能' },
{ hub: 4, offX: 100, offY: 30, label: '全域适应' },
{ hub: 4, offX: 80, offY: -20, label: '全域美学' },
{ hub: 5, offX: 70, offY: 20, label: '牛马语境' },
{ hub: 5, offX: 60, offY: -30, label: '财富自由' },
{ hub: 6, offX: 50, offY: -40, label: '一车多能' },
{ hub: 6, offX: 20, offY: -60, label: '自由切换' },
];
const catColor = {
identity: 'oklch(0.45 0.12 155)',
product: 'oklch(0.5 0.08 245)',
boundary: 'oklch(0.55 0.1 25)',
decision: 'oklch(0.5 0.08 280)',
selling: 'oklch(0.55 0.12 60)',
risk: 'oklch(0.55 0.18 25)',
preference: 'oklch(0.5 0.1 200)',
};
return (
);
};
const getMemoryFacetTrace = title => {
if (title.includes('历史')) return { factStatus: 'confirmed', provenance: 'documented', source: 'gac-brand-assets-2026-04 / source-official-social' };
if (title.includes('决策')) return { factStatus: 'confirmed', provenance: 'manual-confirmed', source: '品牌决策记忆' };
if (title.includes('风险') || title.includes('语言')) return { factStatus: 'confirmed', provenance: 'documented', source: 'brand-policy / source-news-product-speech' };
return { factStatus: 'confirmed', provenance: 'documented', source: '领越品牌核心讯息-0420-V1.pdf' };
};
const MemoryFacet = ({ title, items, accent }) => (
{title}
{items.map((it, i) => (
· {it}
))}
);
// ========== 02 · MARKET SENSING ==========
const MARKET_TIME_WINDOWS = [
{ id: '24h', label: '近 24h', days: 1 },
{ id: '7d', label: '近 7d', days: 7 },
{ id: '30d', label: '近 30d', days: 30 },
];
const MARKET_SIGNAL_TYPES = [
{ id: 'all', label: '全部信号' },
{ id: 'opportunity', label: '机会' },
{ id: 'risk', label: '风险' },
{ id: 'trend', label: '趋势' },
{ id: 'competitor', label: '竞品' },
{ id: 'comparison', label: '对比' },
];
const MARKET_VIEW_TABS = [
{ id: 'overview', label: '总览 Overview' },
{ id: 'evidence', label: '原文证据 Evidence' },
{ id: 'learning', label: '学习地图 Learning' },
{ id: 'memory', label: '记忆摘要 Memory' },
];
const MARKET_CONTENT_FOCUS_FILTERS = [
{ id: 'all', label: '全部内容' },
{ id: 'product_model', label: '车型 / 产品' },
{ id: 'brand', label: '品牌' },
{ id: 'mixed', label: '混合' },
];
const MARKET_PLATFORMS = ['全部平台', '小红书', '抖音', 'B站', 'Instagram', 'YouTube', '公众号', '竞品', '品类'];
const marketApiUrl = (pathname) => (window.location.protocol === 'file:' ? `http://localhost:4173${pathname}` : pathname);
const SENSE_SIGNALS = [
{ id: 'sig-001', time: '14:22', freshness: 1, plat: '品类', source: '搜索聚合', type: 'trend', title: '「数字游民 车」跨平台搜索量 +36%', detail: '知乎、B站和小红书均出现增长,关键词与移动办公、睡眠舱、长周期旅居绑定。', tag: 'opportunity', product: 'V10 对应场景', score: 85, volume: 312, confidence: 0.82, sentiment: 0.36, owner: '市场感知', action: '记录为品类需求变化,供下游按任务查询数字游民与移动办公场景。', evidence: ['知乎提问 +29%', 'B站弹幕共现 64 次', '小红书收藏率 6.8%'], themes: ['数字游民', '移动办公', '旅居'] },
{ id: 'sig-002', time: '14:08', freshness: 1, plat: '抖音', source: '竞品达人视频', type: 'competitor', title: '竞品皮卡把「可玩改装」作为主命题放量', detail: '3 位腰部汽车达人连续发布同类话题,评论区集中追问模块化货箱与外放电能力。', tag: 'risk', product: 'P10 对应场景', score: 88, volume: 183, confidence: 0.79, sentiment: 0.18, owner: '市场感知', action: '记录为竞品内容范式与评论关注点,不直接生成应对动作。', evidence: ['互动率 4.1%', '改装评论占比 31%', '外放电提问 47 条'], themes: ['共创改装', '外放电', '货箱'] },
{ id: 'sig-003', time: '13:42', freshness: 1, plat: '行业媒体', source: '品类文章', type: 'trend', title: '“商用 + 生活方式”混合车主画像被行业媒体放大', detail: '多篇行业文章讨论一车多能、长途穿越、露营装备和移动办公的组合需求。', tag: 'opportunity', product: 'P10 对应场景', score: 81, volume: 44, confidence: 0.76, sentiment: 0.31, owner: '市场感知', action: '记录为品类叙事变化,沉淀到市场经验库供策略查询。', evidence: ['行业文章 7 篇', '一车多能共现 39 次', '长文转发率高'], themes: ['事业生活', '一车多能', 'PR'] },
{ id: 'sig-004', time: '13:15', freshness: 1, plat: '竞品', source: '价格与配置', type: 'competitor', title: '同级新能源皮卡开始用价格锚点抢认知', detail: '经销商短视频把“低门槛越野生活”作为第一卖点,弱化高端机能与共创叙事。', tag: 'risk', product: 'P10 对应场景', score: 79, volume: 127, confidence: 0.68, sentiment: 0.05, owner: '市场感知', action: '记录为竞品价格话术样本,供下游比较定位时按需读取。', evidence: ['低价话术 34 条', '配置对比 12 条', '评论关注预算'], themes: ['价格', '竞品', '差异化'] },
{ id: 'sig-005', time: '12:58', freshness: 1, plat: 'B站', source: '品类评论异常', type: 'risk', title: '“飞行汽车 / 地空车”量产误读在品类讨论中聚集', detail: '外部讨论把概念能力、短期量产和真实交付混在一起,容易影响概念车传播边界。', tag: 'risk', product: 'P10 风险边界', score: 93, volume: 61, confidence: 0.9, sentiment: -0.08, owner: '市场感知', action: '记录为概念边界风险样本,供审核或内容模块查询护栏依据。', evidence: ['疑问占比 18%', '负向情绪 -0.08', 'P0 护栏命中'], themes: ['概念边界', 'P10', '风险'] },
{ id: 'sig-006', time: '12:41', freshness: 1, plat: '竞品', source: '对比归因', type: 'comparison', title: '外部讨论显示“系统能力”比“低价改装”更能承接高意向人群', detail: '同样讨论皮卡生活方式时,高意向评论更关注能力边界、场景完整性和可靠证据。', tag: 'info', product: 'P10 对应场景', score: 82, volume: 156, confidence: 0.74, sentiment: 0.22, owner: '市场感知', action: '记录为对比型市场洞察,供下游在策略推演时主动查询。', evidence: ['场景词占比 44%', '竞品价格词占比 38%', '高意向评论 +21%'], themes: ['对比信号', 'P10', '策略差异'] },
{ id: 'sig-007', time: '11:40', freshness: 7, plat: '海外搜索', source: '长尾词', type: 'trend', title: '“concept van office”长尾词稳定爬升', detail: '英文长尾词与 remote work、modular cabin、sleep mode 同步增长。', tag: 'info', product: 'V10 对应场景', score: 67, volume: 58, confidence: 0.64, sentiment: 0.28, owner: '市场感知', action: '记录为海外长尾需求样本,等待更多证据后进入模式抽象。', evidence: ['长尾词 +14%', 'remote work 共现', '点击率 3.2%'], themes: ['V10', '海外搜索', '移动办公'] },
];
const MARKET_SENSING_OBJECTS = [
{ id: 'competitor', title: 'A. 竞品信号', count: 5, text: '竞品卖点、达人组合、评论区表扬/吐槽与市场教育动作。', examples: ['低价改装话术', '闪充 / 智驾叙事', '达人视频放量'] },
{ id: 'category', title: 'B. 品类 / 市场信号', count: 7, text: '需求热点、使用场景、审美、价格敏感度和平台语法变化。', examples: ['数字游民车', '商用 + 生活方式', '移动办公'] },
{ id: 'media', title: 'C. 媒体 / 达人 / 用户声量', count: 4, text: '外部媒体、达人、用户自然讨论,不把自有账号内容算入主信号。', examples: ['行业文章', '搜索聚合', 'UGC 讨论'] },
{ id: 'risk', title: 'D. 风险信号', count: 3, text: '负面词聚集、舆情苗头、达人外部风险和平台规则变化。', examples: ['量产误读', '话题风险', '达人环境变化'] },
];
const MARKET_OUTPUTS = [
{ id: 'daily', title: '日市场摘要', cadence: '每日', status: 'ready', text: '24h 内高优先级信号已压缩为可查询摘要,下游模块按任务主动读取。' },
{ id: 'weekly', title: '周市场摘要', cadence: '每周', status: 'draft', text: '近 7d 品类讨论变化保留为经验记录,不直接转成内容任务。' },
{ id: 'competitor', title: '竞品异动报告', cadence: '实时', status: 'ready', text: '竞品话术、评论问题和传播节奏已沉淀为对标记忆。' },
{ id: 'risk', title: '风险观察记录', cadence: '实时', status: 'urgent', text: 'P10 飞行器量产误读已记录为风险观察,供下游任务按需读取。' },
{ id: 'category', title: '品类讨论趋势摘要', cadence: '每日', status: 'ready', text: '数字游民、移动办公、旅居场景保留为趋势查询条件。' },
{ id: 'brand-anomaly', title: '概念边界异常记录', cadence: '实时', status: 'watch', text: '外部讨论中的概念车/量产车混淆被保留为风险证据。' },
{ id: 'creator-risk', title: '达人环境变化记录', cadence: '每日', status: 'watch', text: '改装类达人内容放量被记录为平台内容环境变化。' },
];
const marketTagText = { opportunity: '机会', risk: '风险', info: '提示' };
const marketTypeText = { brand: '品牌', trend: '趋势', risk: '风险', competitor: '竞品', comparison: '对比' };
const marketFocusText = { product_model: '车型/产品向', brand: '品牌向', mixed: '车型+品牌', unclear: '未明确' };
const marketStatusText = { ready: '已就绪', draft: '草稿', urgent: '紧急', watch: '观察中' };
const marketSourceText = {
request: '外部请求数据',
'social-sync-state': '同步数据',
'fallback-fixtures': '示例数据',
static: '静态示例',
'static-fallback': '静态兜底',
api: '接口数据',
};
const marketPriorityText = { core: '核心候选', benchmark: '标杆参照', 'narrative-reference': '叙事参照' };
const marketCadenceText = { live: '实时', daily: '每日', weekly: '每周' };
const marketLearningPriorityText = {
P1: '类似阶段',
P2: '同产品功能',
P3: '同用户场景',
P4: '内容切角',
P5: '风险案例',
};
const marketLearningRelationText = {
path_case: '路径案例',
method_case: '方法案例',
product_competitor: '产品竞品',
audience_case: '人群案例',
risk_case: '风险案例',
content_angle_case: '切角案例',
};
const localizeMarketText = (text = '') => String(text || '')
.replace(/Strategy Input/g, '策略输入')
.replace(/Market Sensing/g, '市场感知')
.replace(/Context Pack/g, '上下文包');
const localizeMarketEvidence = (text = '') => {
const value = String(text || '');
if (value === 'competitor source detected from structured brand fields') return '已根据结构化品牌字段识别为竞品来源';
if (value === 'high engagement competitor content detected') return '检测到高互动竞品内容';
if (value === 'external same-industry source detected; competitor status not confirmed') return '检测到同业外部来源,但竞品身份尚未确认';
if (value === 'high engagement external industry content detected') return '检测到高互动同业外部内容';
if (value === 'competitor or comparison monitoring language detected') return '检测到竞品或对比监控语义';
if (value === 'launch / time-bound CTA language detected') return '检测到上市/节点型行动语义';
if (value === 'product superiority claim without explicit proof') return '产品优势表达缺少明确证据';
if (value === 'competitor product advantage claim captured as unverified market claim') return '竞品优势主张已记录为未验证市场话术';
if (value === 'recurring series cadence or column language detected') return '检测到固定栏目或周期内容语义';
if (value.startsWith('hashtags detected:')) return value.replace('hashtags detected:', '检测到话题标签:');
return value;
};
const getMarketChipClass = tag => tag === 'opportunity' ? 'accent' : tag === 'risk' ? 'danger' : 'info';
const getMarketWindow = id => MARKET_TIME_WINDOWS.find(item => item.id === id) || MARKET_TIME_WINDOWS[0];
const marketSeverityScore = { critical: 98, high: 92, medium: 78, low: 64 };
const marketSignalTypeLabel = {
brand_or_claim_risk: '品牌 / 主张风险',
competitor_activity_spike: '竞品异动',
external_industry_signal: '外部行业信号',
recurring_education_pattern: '长期栏目机会',
time_bound_campaign_candidate: '限时 Campaign 候选',
};
const marketBrandLogoText = {
byd: 'BYD',
'fang-cheng-bao': '豹',
'geely-radar-riddara': 'R',
'gwm-pao-tank': 'GWM',
'saic-maxus-ldv': 'MAX',
'deepal-g318': '深',
'chery-icar-jetour-jaecoo': 'iCAR',
'dongfeng-m-hero': '猛',
rivian: 'RIV',
'tesla-cybertruck': 'T',
'kia-pbv-pv5': 'KIA',
'volkswagen-id-buzz': 'VW',
'zeekr-li-xpeng-nio': 'EV',
};
const marketBrandAccent = {
byd: 'red',
'fang-cheng-bao': 'bronze',
'geely-radar-riddara': 'green',
'gwm-pao-tank': 'bronze',
'saic-maxus-ldv': 'blue',
'deepal-g318': 'blue',
'chery-icar-jetour-jaecoo': 'green',
'dongfeng-m-hero': 'dark',
rivian: 'green',
'tesla-cybertruck': 'dark',
'kia-pbv-pv5': 'blue',
'volkswagen-id-buzz': 'blue',
'zeekr-li-xpeng-nio': 'dark',
};
const normalizeBrandId = value => String(value || '').toLowerCase().trim();
const getMarketBrandMeta = (signal = {}, competitors = []) => {
const sourceBrandId = normalizeBrandId(signal.brand_id || signal.source_context?.source_brand_id);
const candidate = competitors.find(item => {
const ids = [item.brand_id, item.brand_name].map(normalizeBrandId);
return ids.includes(sourceBrandId);
});
const brandId = sourceBrandId || normalizeBrandId(candidate?.brand_id) || 'external';
return {
brandId,
brandName: candidate?.brand_name || (brandId === 'external' ? '外部来源' : brandId.toUpperCase()),
logoText: candidate?.logo_text || marketBrandLogoText[brandId] || (brandId === 'external' ? '外' : brandId.slice(0, 3).toUpperCase()),
accent: candidate?.logo_accent || marketBrandAccent[brandId] || 'neutral',
priority: candidate?.priority || '',
};
};
const inferMarketTag = signal => {
if (signal.category === 'risk' || /risk|claim|guardrail/i.test(signal.signal_type || '')) return 'risk';
if (signal.category === 'opportunity' || /pattern|education/i.test(signal.signal_type || '')) return 'opportunity';
return 'info';
};
const inferMarketType = signal => {
if (signal.category === 'competitor') return 'competitor';
if (signal.category === 'risk') return 'risk';
if (/comparison/i.test(signal.signal_type || '')) return 'comparison';
return signal.category === 'opportunity' ? 'trend' : (signal.category || 'brand');
};
const formatMarketSignalTime = value => {
const date = value ? new Date(value) : null;
if (!date || Number.isNaN(date.getTime())) return 'live';
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', hour12: false });
};
const normalizeApiMarketSignal = (signal = {}, index = 0, competitors = []) => {
const confidence = Number(signal.confidence || 0);
const tag = inferMarketTag(signal);
const type = inferMarketType(signal);
const evidence = (signal.evidence || []).filter(Boolean).map(localizeMarketEvidence);
const severityBase = marketSeverityScore[signal.severity] || 70;
const score = Math.max(50, Math.min(99, Math.round(Math.max(confidence * 100, severityBase))));
const brand = getMarketBrandMeta(signal, competitors);
const focus = signal.content_focus || {};
const focusType = focus.focus_type || 'unclear';
const models = signal.models?.length ? signal.models : (focus.model_names || []);
return {
id: signal.signal_id || `api-signal-${index}`,
time: formatMarketSignalTime(signal.detected_at),
freshness: 1,
plat: signal.platform || (signal.category === 'competitor' ? '竞品' : signal.category === 'risk' ? '风险' : '市场'),
source: marketSignalTypeLabel[signal.signal_type] || signal.signal_type || 'market sensing',
brand,
brandId: brand.brandId,
brandName: brand.brandName,
focusType,
focusLabel: marketFocusText[focusType] || focus.focus_label || '未明确',
models,
sourceContentType: signal.source_content_type || focus.source_content_type || '',
rawText: localizeMarketText(signal.raw_text || signal.summary || ''),
postDate: signal.post_date || '',
accountName: signal.source_meta?.account_name || signal.source_meta?.username || '',
sourceUrl: signal.source_meta?.url || '',
signalType: signal.signal_type || '',
sourceScope: signal.source_context?.source_scope || '',
competitorStatus: signal.source_context?.competitor_status || '',
contentIds: signal.content_ids || [],
type,
title: signal.title || '市场感知信号',
detail: localizeMarketText(signal.summary || ''),
tag,
product: (signal.content_ids || []).join(' / ') || signal.brand_id || '外部来源',
score,
volume: evidence.length || (signal.content_ids || []).length || 1,
confidence: confidence || 0.72,
sentiment: tag === 'risk' ? -0.08 : 0,
owner: signal.owner || '市场感知',
action: localizeMarketText(signal.recommended_action || '保留在市场洞察库,供下游任务按需读取。'),
evidence,
themes: [
marketFocusText[focusType] || focus.focus_label,
...(models || []).slice(0, 2),
signal.source_content_type || focus.source_content_type,
signal.category,
signal.signal_type,
signal.severity,
...(evidence.slice(0, 2)),
].filter(Boolean).slice(0, 4),
apiSignal: signal,
};
};
const normalizeApiMarketEvidence = (item = {}, index = 0, competitors = []) => {
const signal = item.signal || {};
const confidence = Number(item.confidence || signal.confidence || 0.72);
const brand = getMarketBrandMeta({
brand_id: item.brand_id,
source_context: item.source_context,
}, competitors);
const focus = item.content_focus || {};
const focusType = focus.focus_type || 'unclear';
const models = item.models?.length ? item.models : (focus.model_names || []);
const evidence = (item.evidence || signal.evidence || []).filter(Boolean).map(localizeMarketEvidence);
return {
id: item.content_id || `market-evidence-${index}`,
time: item.post_date || 'raw',
freshness: 1,
plat: item.platform || '外部来源',
source: signal.signal_type ? (marketSignalTypeLabel[signal.signal_type] || signal.signal_type) : '原文证据',
brand,
brandId: brand.brandId,
brandName: brand.brandName,
focusType,
focusLabel: marketFocusText[focusType] || focus.focus_label || '未明确',
models,
sourceContentType: item.source_content_type || focus.source_content_type || '',
rawText: localizeMarketText(item.raw_text || ''),
postDate: item.post_date || '',
accountName: item.source_meta?.account_name || item.source_meta?.username || '',
sourceUrl: item.source_meta?.url || '',
signalType: signal.signal_type || '',
sourceScope: item.source_context?.source_scope || '',
competitorStatus: item.source_context?.competitor_status || '',
contentIds: [item.content_id].filter(Boolean),
type: item.source_context?.source_scope === 'external_competitor' ? 'competitor' : 'trend',
title: signal.signal_type ? '竞品原文命中信号候选' : '竞品原文证据',
detail: localizeMarketText(item.raw_text || ''),
tag: signal.signal_type ? inferMarketTag({ signal_type: signal.signal_type, category: 'competitor' }) : 'info',
product: item.content_id || '外部来源',
score: Math.max(50, Math.min(99, Math.round(confidence * 100))),
volume: evidence.length || 1,
confidence,
sentiment: 0,
owner: '市场感知',
action: signal.signal_type ? '这条原文已命中市场信号候选,保留为推导证据。' : '保留为竞品原文证据,供后续聚类、信号压缩和策略推演查询。',
evidence,
themes: [
marketFocusText[focusType] || focus.focus_label,
...(models || []).slice(0, 2),
item.source_content_type || focus.source_content_type,
signal.signal_type,
...(evidence.slice(0, 1)),
].filter(Boolean).slice(0, 4),
apiEvidence: item,
};
};
const buildMarketContextOutputs = (contextPack, reviewQueue = [], patterns = []) => {
if (!contextPack) return MARKET_OUTPUTS;
const snapshot = contextPack.market_snapshot || {};
const memory = contextPack.brand_memory || {};
return [
{
id: 'api-context-pack',
title: '市场洞察沉淀',
cadence: 'live',
status: 'ready',
text: snapshot.summary ? `${snapshot.summary} 下游模块按任务主动读取,不主动推送。` : '已生成市场总结,供策略、内容和审核模块按任务主动读取。',
},
{
id: 'api-risk-notes',
title: '事实/风险待确认',
cadence: 'live',
status: reviewQueue.length ? 'urgent' : 'watch',
text: reviewQueue.length ? `${reviewQueue.length} 个内容对象需要人工复核,优先确认产品事实、证据和概念边界。` : '当前没有高优先级人工复核项。',
},
{
id: 'api-brand-memory',
title: '可沉淀经验',
cadence: 'daily',
status: 'ready',
text: `已识别 ${memory.known_campaigns?.length || 0} 个 campaign 候选、${memory.known_series?.length || 0} 个栏目候选,保留为市场感知经验库。`,
},
{
id: 'api-patterns',
title: '经验模式',
cadence: 'weekly',
status: patterns.length ? 'draft' : 'watch',
text: patterns[0]?.summary || '等待更多历史内容后抽象可复用经验模式。',
},
];
};
const getSignalCounters = signals => {
const competitorSignals = signals.filter(item => item.type === 'competitor');
const riskSignals = signals.filter(item => item.tag === 'risk');
const opportunitySignals = signals.filter(item => item.tag === 'opportunity');
const anomalySignals = signals.filter(item => item.score >= 90 || item.type === 'risk');
const changes = signals.filter(item => ['trend', 'brand', 'competitor', 'comparison'].includes(item.type));
const comparisonSignals = signals.filter(item => item.type === 'comparison');
return [
{ label: '竞品信号', value: competitorSignals.length, delta: `${competitorSignals.filter(item => item.score >= 75).length} 个高置信观察`, color: 'var(--accent)' },
{ label: '风险信号', value: riskSignals.length, delta: `${riskSignals.filter(item => item.score >= 85).length} 个高优先级证据`, color: 'var(--danger)' },
{ label: '变化信号', value: changes.length, delta: `${signals.reduce((sum, item) => sum + item.volume, 0)} 条证据`, color: 'var(--info)' },
{ label: '异常信号', value: anomalySignals.length, delta: anomalySignals[0]?.title.slice(0, 12) || '无异常', color: 'var(--warn)' },
{ label: '对比信号', value: comparisonSignals.length, delta: comparisonSignals[0]?.themes?.[2] || '等待归因', color: 'var(--ink)' },
];
};
const getMarketFocusCounters = signals => {
const count = focusType => signals.filter(item => item.focusType === focusType).length;
const modelSignals = signals.filter(item => item.focusType === 'product_model' || item.focusType === 'mixed');
const topModels = Array.from(new Set(modelSignals.flatMap(item => item.models || []).filter(Boolean))).slice(0, 4);
return [
{ id: 'product_model', title: '车型 / 产品内容', value: count('product_model'), text: topModels.length ? `主要车系:${topModels.join(' / ')}` : '围绕车型、配置、技术、试驾和使用场景。' },
{ id: 'brand', title: '品牌内容', value: count('brand'), text: '围绕品牌文化、全球合作、销量里程碑、用户关系和社会议题。' },
{ id: 'mixed', title: '混合内容', value: count('mixed'), text: '车型发布、技术发布和品牌背书同时出现,需要拆开看策略作用。' },
];
};
const MarketSensing = ({ workspaceContext }) => {
const [windowId, setWindowId] = React.useState('24h');
const [activeTab, setActiveTab] = React.useState('overview');
const [typeFilter, setTypeFilter] = React.useState('all');
const [focusFilter, setFocusFilter] = React.useState('all');
const [platform, setPlatform] = React.useState('全部平台');
const [query, setQuery] = React.useState('');
const [selectedId, setSelectedId] = React.useState(SENSE_SIGNALS[0].id);
const [writtenIds, setWrittenIds] = React.useState([]);
const [marketState, setMarketState] = React.useState({
loading: true,
error: '',
source: 'static',
updatedAt: '',
signals: SENSE_SIGNALS,
reviewQueue: [],
contextPack: null,
patterns: [],
competitors: [],
learningMap: null,
evidence: [],
evidenceTotal: 0,
evidenceLoading: false,
});
const activeWindow = getMarketWindow(windowId);
const brandName = (WORKSPACE_BRANDS.find(item => item.id === workspaceContext?.brandId) || WORKSPACE_BRANDS[0])?.name || BRAND.name;
const brandId = workspaceContext?.brandId || 'montx';
const signals = marketState.signals?.length ? marketState.signals : SENSE_SIGNALS;
const evidenceRows = marketState.evidence?.length ? marketState.evidence : signals;
const platforms = React.useMemo(() => {
const livePlatforms = Array.from(new Set([...signals, ...evidenceRows].map(item => item.plat).filter(Boolean)));
return Array.from(new Set([...MARKET_PLATFORMS, ...livePlatforms]));
}, [signals, evidenceRows]);
const loadMarketData = React.useCallback(async (forceRefresh = false) => {
setMarketState(prev => ({ ...prev, loading: true, error: '' }));
try {
const refreshQuery = forceRefresh ? '&refresh=1' : '';
const [signalsResponse, reviewResponse, patternsResponse, contextResponse, competitorsResponse, learningResponse, evidenceResponse] = await Promise.all([
fetch(marketApiUrl(`/api/market/signals?limit=50${refreshQuery}`)),
fetch(marketApiUrl(`/api/market/review-queue?limit=50${refreshQuery}`)),
fetch(marketApiUrl(`/api/market/patterns?limit=50${refreshQuery}`)),
fetch(marketApiUrl('/api/market/context-pack'), {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ brand_id: brandId, topic: `${brandName} 市场感知`, refresh: forceRefresh }),
}),
fetch(marketApiUrl('/api/market/competitors')),
fetch(marketApiUrl(`/api/market/learning-map${forceRefresh ? '?refresh=1' : ''}`)),
fetch(marketApiUrl(`/api/market/evidence?limit=200&sort=date${forceRefresh ? '&offset=0' : ''}`)),
]);
if (!signalsResponse.ok || !reviewResponse.ok || !patternsResponse.ok || !contextResponse.ok || !competitorsResponse.ok || !learningResponse.ok || !evidenceResponse.ok) {
throw new Error('市场感知接口暂不可用');
}
const [signalsData, reviewData, patternsData, contextData, competitorsData, learningData, evidenceData] = await Promise.all([
signalsResponse.json(),
reviewResponse.json(),
patternsResponse.json(),
contextResponse.json(),
competitorsResponse.json(),
learningResponse.json(),
evidenceResponse.json(),
]);
const competitorCandidates = competitorsData.competitorCandidates || [];
const apiSignals = (signalsData.signals || []).map((signal, index) => normalizeApiMarketSignal(signal, index, competitorCandidates));
const apiEvidence = (evidenceData.evidence || []).map((item, index) => normalizeApiMarketEvidence(item, index, competitorCandidates));
setMarketState({
loading: false,
error: '',
source: signalsData.source || (signalsData.versioning ? 'api' : 'static'),
updatedAt: signalsData.updatedAt || contextData.contextPack?.context_pack_id || '',
signals: apiSignals.length ? apiSignals : SENSE_SIGNALS,
reviewQueue: reviewData.reviewQueue || [],
contextPack: contextData.contextPack || null,
patterns: patternsData.patterns || [],
competitors: competitorCandidates,
learningMap: learningData.learning_map || learningData || null,
evidence: apiEvidence,
evidenceTotal: evidenceData.total || apiEvidence.length,
evidenceLoading: false,
});
} catch (error) {
setMarketState(prev => ({
...prev,
loading: false,
error: error.message || '市场感知接口暂不可用',
source: 'static-fallback',
signals: prev.signals?.length ? prev.signals : SENSE_SIGNALS,
evidence: prev.evidence?.length ? prev.evidence : (prev.signals?.length ? prev.signals : SENSE_SIGNALS),
}));
}
}, [brandId, brandName]);
React.useEffect(() => { loadMarketData(); }, [loadMarketData]);
const loadMoreEvidence = React.useCallback(async () => {
if (marketState.evidenceLoading) return;
setMarketState(prev => ({ ...prev, evidenceLoading: true }));
try {
const offset = marketState.evidence?.length || 0;
const response = await fetch(marketApiUrl(`/api/market/evidence?limit=200&offset=${offset}&sort=date`));
const data = await response.json();
if (!response.ok || !data.ok) throw new Error(data.error || '原文证据接口暂不可用');
const nextRows = (data.evidence || []).map((item, index) => normalizeApiMarketEvidence(item, offset + index, marketState.competitors || []));
setMarketState(prev => {
const byId = new Map((prev.evidence || []).map(item => [item.id, item]));
nextRows.forEach(item => byId.set(item.id, item));
return {
...prev,
evidence: Array.from(byId.values()),
evidenceTotal: data.total || prev.evidenceTotal || byId.size,
evidenceLoading: false,
};
});
} catch (error) {
setMarketState(prev => ({ ...prev, evidenceLoading: false, error: error.message || '原文证据接口暂不可用' }));
}
}, [marketState.evidence?.length, marketState.evidenceLoading, marketState.competitors]);
const filteredSignals = React.useMemo(() => {
const q = query.trim().toLowerCase();
return signals.filter(item => {
const inWindow = item.freshness <= activeWindow.days;
const inType = typeFilter === 'all' || item.tag === typeFilter || item.type === typeFilter;
const inFocus = focusFilter === 'all' || item.focusType === focusFilter;
const inPlatform = platform === '全部平台' || item.plat === platform;
const inQuery = !q || [item.title, item.detail, item.rawText, item.product, item.source, item.brandName, item.accountName, item.focusLabel, item.sourceContentType, item.postDate, item.signalType, ...(item.models || []), ...(item.themes || [])].join(' ').toLowerCase().includes(q);
return inWindow && inType && inFocus && inPlatform && inQuery;
}).sort((a, b) => b.score - a.score);
}, [signals, activeWindow.days, typeFilter, focusFilter, platform, query]);
const filteredEvidence = React.useMemo(() => {
const q = query.trim().toLowerCase();
return evidenceRows.filter(item => {
const inType = typeFilter === 'all' || item.tag === typeFilter || item.type === typeFilter;
const inFocus = focusFilter === 'all' || item.focusType === focusFilter;
const inPlatform = platform === '全部平台' || item.plat === platform;
const inQuery = !q || [item.title, item.detail, item.rawText, item.product, item.source, item.brandName, item.accountName, item.focusLabel, item.sourceContentType, item.postDate, item.signalType, ...(item.models || []), ...(item.themes || [])].join(' ').toLowerCase().includes(q);
return inType && inFocus && inPlatform && inQuery;
});
}, [evidenceRows, typeFilter, focusFilter, platform, query]);
const selectedSignal = filteredSignals.find(item => item.id === selectedId) || filteredSignals[0] || signals[0] || SENSE_SIGNALS[0];
const selectedEvidence = filteredEvidence.find(item => item.id === selectedId) || filteredEvidence[0] || evidenceRows[0] || selectedSignal;
const activeSelected = activeTab === 'evidence' ? selectedEvidence : selectedSignal;
const counters = getSignalCounters(filteredSignals);
const focusCounters = React.useMemo(() => getMarketFocusCounters(filteredSignals), [filteredSignals]);
const outputRows = React.useMemo(
() => buildMarketContextOutputs(marketState.contextPack, marketState.reviewQueue, marketState.patterns),
[marketState.contextPack, marketState.reviewQueue, marketState.patterns]
);
const writeSelected = () => {
if (!activeSelected) return;
setWrittenIds(prev => prev.includes(activeSelected.id) ? prev : [...prev, activeSelected.id]);
};
React.useEffect(() => {
if (activeSelected && activeSelected.id !== selectedId) setSelectedId(activeSelected.id);
}, [activeSelected?.id, activeTab]);
return (
市场感知 · {brandName} 外部信号工作台
以同行业外部品牌、品类趋势、媒体达人和用户自然讨论为主信号;MONTX 自有内容只作为对照基线和护栏输入。
{MARKET_TIME_WINDOWS.map(item => (
))}
{counters.map(item => )}
{focusCounters.map(item => (
))}
{MARKET_VIEW_TABS.map(tab => (
))}
{MARKET_SIGNAL_TYPES.map(item => (
))}
{MARKET_CONTENT_FOCUS_FILTERS.map(item => (
))}
setQuery(event.target.value)} placeholder="搜索原文 / 场景 / 产品 / 主题"/>
{filteredSignals.length} 条信号 · {marketSourceText[marketState.source] || marketState.source}
{activeTab === 'overview' && (
<>
外部市场声量 vs 自有基线
覆盖示意 · 外部优先
信号流 · Compressed
{filteredSignals.length} / {signals.length}
{marketState.error && 静态兜底}
按优先级排序
{filteredSignals.length ? filteredSignals.map((signal, i) => (
setSelectedId(signal.id)}
/>
)) : (
当前筛选没有匹配信号。调整时间窗、平台或关键词后重试。
)}
>
)}
{activeTab === 'evidence' && (
)}
{activeTab === 'learning' && }
{activeTab === 'memory' && (
)}
);
};
const MarketSignalRow = ({ signal, active, written, isLast, onSelect }) => (
);
const MarketEvidencePanel = ({ signals = [], loaded = 0, total = 0, selectedSignal, written, onSelect, onWrite, onLoadMore, loadingMore = false }) => {
const activeSignal = signals.find(item => item.id === selectedSignal?.id) || signals[0] || null;
const canLoadMore = loaded < total;
return (
竞品原文 · Evidence Feed
筛选 {signals.length} · 已载入 {loaded || signals.length} / 原始库 {total || signals.length}
{canLoadMore && (
)}
原文 → 分类 → 信号
{signals.length ? signals.map((signal, index) => (
)) : (
当前筛选没有匹配原文。调整平台、内容方向或关键词后重试。
)}
);
};
const MarketRawEvidenceDetail = ({ signal }) => {
if (!signal) {
return (
原文推导 · Derivation
等待选择
选择一条竞品原文后,这里会展示原文、分类字段和推导证据。
);
}
const derivationSteps = [
{ label: '原文', value: signal.rawText || signal.detail },
{ label: '内容分类', value: `${signal.focusLabel || '未明确'} / ${signal.sourceContentType || '未标注'}` },
{ label: '信号类型', value: marketSignalTypeLabel[signal.signalType] || signal.signalType || signal.source },
{ label: '沉淀方式', value: signal.action },
];
return (
原文推导 · Derivation
{signal.postDate || signal.time}
{signal.brand?.logoText || 'EXT'}
{signal.brandName || '外部来源'}
{[signal.plat, signal.accountName, signal.postDate || '未标注日期', (signal.contentIds || []).join(' / ') || signal.product].filter(Boolean).join(' · ')}
{signal.sourceUrl && (
打开原帖链接
)}
原文内容
{signal.rawText || signal.detail}
{derivationSteps.map(item => (
{item.label}
{item.value}
))}
{(signal.evidence || []).map(item => (
{item}
))}
);
};
const MarketLearningMap = ({ learningMap, loading = false }) => {
const priorities = learningMap?.learning_priorities || [];
const profile = learningMap?.brand_learning_profile || {};
const strategyNotes = learningMap?.recommended_for_strategy || [];
const [selectedCase, setSelectedCase] = React.useState(null);
const [caseState, setCaseState] = React.useState({ loading: false, error: '', caseStudy: null });
const openCaseStudy = React.useCallback(async item => {
setSelectedCase(item);
setCaseState({ loading: true, error: '', caseStudy: null });
try {
const response = await fetch(marketApiUrl(`/api/market/case-studies/${encodeURIComponent(item.case_id)}`));
const data = await response.json();
if (!response.ok || !data.ok) throw new Error(data.error || data.message || '案例详情暂未生成');
setCaseState({ loading: false, error: '', caseStudy: data.caseStudy || null });
} catch (error) {
setCaseState({ loading: false, error: error.message || '案例详情暂未生成', caseStudy: null });
}
}, []);
if (!learningMap && loading) {
return (
学习地图 · Learning Map
品牌记忆推导中
);
}
if (!learningMap) return null;
return (
<>
学习地图 · Brand Memory Learning Map
{profile.product_stage || 'stage'} · {profile.launch_window || 'launch window'}
{profile.brand_name || 'MONTX'} 当前学习任务
{learningMap.learning_principle || '按品牌当前战略问题选择学习对象。'}
{(profile.strategic_tasks || []).slice(0, 5).map(item => {item})}
{priorities.map(priority => (
{priority.priority}
{marketLearningPriorityText[priority.priority] || priority.title}
{priority.intent?.why || priority.description}
{(priority.cases || []).slice(0, 3).map(item => (
))}
))}
{strategyNotes.slice(0, 4).map(note => (
{note}
))}
{selectedCase && (
setSelectedCase(null)}
/>
)}
>
);
};
const MarketCaseStudyDrawer = ({ caseItem, caseStudy, loading, error, onClose }) => {
const scope = caseStudy?.data_scope || {};
const breakdown = caseStudy?.content_breakdown || {};
const patterns = caseStudy?.transferable_patterns || [];
const risks = caseStudy?.risk_patterns || [];
const implications = caseStudy?.montx_implications || [];
const audience = caseStudy?.audience_insights || [];
return (
{caseItem?.case_id || caseStudy?.case_id}
{caseItem?.case_brand || caseStudy?.brand} · 案例详情
{caseItem?.case_product || '长周期预热案例'}
{loading ? (
) : error ? (
{error}
当前案例可能还没有生成结构化 case study。先运行对应分析脚本后再打开。
) : (
{caseStudy?.diagnostic_summary?.one_sentence || '案例结论待补充'}
{caseStudy?.diagnostic_summary?.why_it_matters_to_montx}
内容生命周期
{(breakdown.lifecycle_stages || []).slice(0, 6).map(item => )}
车型拆分
{(breakdown.model_focus || []).map(item => {item.key} · {item.count})}
可迁移内容范式
{patterns.map(item => (
{item.name}
{item.montx_transfer}
))}
评论洞察
{audience.slice(0, 5).map(item => (
{item.label} · {item.count}
{item.insight}
))}
风险模式
{risks.map(item => (
{item.risk}
{item.implication}
))}
沉淀给 MONTX 的启示
{implications.map(item => (
{item}
))}
)}
);
};
const MarketCaseStat = ({ label, value }) => (
{label}
{value}
);
const MarketCaseBar = ({ item, total }) => {
const width = total ? Math.max(6, Math.round((item.count / total) * 100)) : 0;
return (
);
};
const MarketSignalDetail = ({ signal, written, onWrite }) => {
if (!signal) return null;
return (
信号详情 · 洞察记录
{signal.brandName || signal.product}
{signal.action}
记录方 · {signal.owner} · 置信度 {(signal.confidence * 100).toFixed(0)}%
= 85 ? '高置信' : '观察'}/>
0 ? '+' : ''}${signal.sentiment}`} sub={marketTypeText[signal.type] || signal.type}/>
{(signal.evidence || []).map(item => (
{item}
))}
);
};
const MarketObjectMap = ({ reviewQueue = [], signals = [] }) => {
const counts = {
competitor: signals.filter(item => item.type === 'competitor').length,
category: signals.filter(item => item.type === 'trend' || item.tag === 'opportunity').length,
media: signals.filter(item => ['小红书', '抖音', 'B站', 'Instagram', 'YouTube', '行业媒体', '海外搜索'].includes(item.plat)).length,
risk: reviewQueue.length || signals.filter(item => item.tag === 'risk').length,
};
return (
外部感知对象 · Four Signal Objects
PRD 3.2
{MARKET_SENSING_OBJECTS.map(item => (
{item.title}
{counts[item.id] || item.count}
{item.text}
{item.examples.map(example => {example})}
))}
);
};
const MarketMemorySummary = ({ outputs = MARKET_OUTPUTS, loading = false }) => (
市场记忆摘要 · Query Outputs
{outputs.length} 条可查询记录
{loading ? '同步中' : '按需读取'}
{outputs.map(item => (
{marketCadenceText[item.cadence] || item.cadence}
{item.title}
{item.text}
{marketStatusText[item.status] || item.status}
))}
);
const SignalCounter = ({ label, value, delta, color }) => (
);
const SensingChart = () => (
);
const HeatMatrix = () => {
const rows = ['小红书', '抖音', '微博', 'B站', '知乎', '汽车之家', '公众号'];
const cols = ['全域', 'P10', 'V10', '办公', '旅居', '共创', '边界'];
const data = [
[9,8,7,8,9,7,5],
[8,9,6,6,7,8,6],
[6,6,5,5,5,6,4],
[5,8,8,7,8,6,9],
[4,6,7,8,7,5,8],
[3,8,4,4,5,7,6],
[7,6,7,8,7,6,8],
];
return (
|
{cols.map(c => {c} | )}
{rows.map((r, i) => (
| {r} |
{data[i].map((v, j) => (
6 ? 'white' : 'var(--ink-2)',
borderRadius:3,
}}>{v} |
))}
))}
);
};
const MarketSourceCoverage = ({ competitors = [] }) => (
外部数据源覆盖 · Source Coverage
{competitors.length} 个候选竞品
{[
['外部自然声量', 'Instagram / YouTube 二创与转发', 74, 'ok'],
['品类趋势', '小红书 / B站 / 知乎 / 公众号聚合', 81, 'ok'],
['竞品监听', competitors.length ? competitors.map(item => item.brand_name || item.brand_id).join(' / ') : '等待确认竞品种子', competitors.length ? 76 : 42, competitors.length ? 'ok' : 'warn'],
['风险异常', '护栏命中、评论聚集、概念误读', 86, 'ok'],
['自有声量基线', 'MONTX 官方社媒仅用于对照,不进入主信号流', 92, 'ok'],
].map(([name, desc, pct, status]) => (
))}
);
const SensingPipeline = () => (
感知链路 · From Signal To Memory
查询服务
{[
['采集', '平台/竞品/评论进入信号池', 'done'],
['标准化', '实体、车型、平台、证据与置信度归一', 'done'],
['判定', '信号类型、竞品关系、风险边界归因', 'active'],
['沉淀', '生成 Context Pack / Pattern / Case Study', 'active'],
['服务', '下游模块按任务主动查询,不主动推送', 'pending'],
].map(([title, text, status], index) => (
{String(index + 1).padStart(2, '0')}
{title}
{text}
))}
);
const MiniStat = ({ label, value, sub }) => (
);
Object.assign(window, { Dashboard, BrandMemory, MarketSensing });