/* ===== 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 (
    <div className="page wide">
      <div className="page-header">
        <div className="heading">
          <h1>{brand?.name || workspaceBrand?.name || 'Workspace'} · <span className="hl">社媒营销操作台</span></h1>
          <p>{openRiskComments.length || pendingReviews.length ? `你有 ${openRiskComments.length + pendingReviews.length} 条待处理判断` : '当前没有待处理风险判断'} · 首页只读取品牌记忆、社媒同步、市场感知和运营对象数据。</p>
        </div>
        <div className="actions">
          <button className="btn" onClick={() => onNav('memory')}><Icon name="upload" className="icon sm"/>上传资料</button>
          <button className="btn primary" onClick={() => onNav('strategy')}><Icon name="plus" className="icon sm"/>新建策略对象</button>
        </div>
      </div>

      <div className="grid g-4">
        <MetricCard label="活跃运营对象" value={String(activeInitiatives.length)} delta={`${activeCampaigns.length} Campaign · ${activeInitiatives.filter(item => item.type === 'series').length} Series`} accent/>
        <MetricCard label="品牌记忆规则" value={String(activeConstraints.length)} delta={`${productCount} products · ${brand ? '已加载' : '待加载'}`}/>
        <MetricCard label="同步内容" value={String(ownedPosts.length)} delta={`${accounts.length} 账号 · ${lastSyncText}`}/>
        <MetricCard label="风险评论" value={String(openRiskComments.length)} delta={`评论 ${dashboardCompactNumber(comments.length)} · 互动 ${dashboardCompactNumber(totalInteraction)}`}/>
      </div>

      <div className="grid" style={{gridTemplateColumns:'1.4fr 1fr'}}>
        <CurrentCampaignCard initiative={activeInitiative} posts={ownedPosts} comments={comments} totalViews={totalViews} totalInteraction={totalInteraction} onNav={onNav}/>
        <EventFeedCard events={events} pendingCount={openRiskComments.length + pendingReviews.length} syncState={syncState}/>
      </div>

      <div className="grid g-2">
        <SystemHealthCard brand={brand} syncState={syncState} initiatives={brandInitiatives} posts={posts} marketState={marketState} riskCount={openRiskComments.length}/>
        <BrandPulseCard posts={ownedPosts}/>
      </div>
    </div>
  );
};

const MetricCard = ({ label, value, delta, accent }) => (
  <div className="metric">
    <div className={`label ${/阅读\/播放|互动转化/.test(label) ? 'metric-label-compact' : ''}`.trim()}>{label}</div>
    <div className="value" style={accent ? {color:'var(--accent)'} : {}}>{value}</div>
    <div className="delta">{delta}</div>
    <svg className="spark" width="52" height="18" viewBox="0 0 52 18">
      <polyline points="0,12 8,10 16,13 24,7 32,9 40,4 52,6" fill="none" stroke="currentColor" strokeWidth="1.2" style={{color: accent ? 'var(--accent)' : 'var(--ink-3)'}}/>
    </svg>
  </div>
);

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 (
    <div className="card">
      <div className="card-header">
        <h3>当前运营对象</h3>
        <span className="count">未创建</span>
        <div className="actions"><button className="btn sm" onClick={() => onNav('strategy')}>进入推演 <Icon name="chevron" className="icon sm"/></button></div>
      </div>
      <div className="card-body">
        <div className="callout">
          尚未读取到当前品牌的运营对象。创建 Campaign、Series 或 Content 后，首页会按 PRD 的中枢闭环展示进度和表现。
          <small>数据来源：campaign-state / Brand Memory / Social Sync</small>
        </div>
        <PipelineBar steps={[]} label="WORKFLOW PIPELINE"/>
      </div>
    </div>
  );
  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 (
    <div className="card">
      <div className="card-header">
        <h3>当前运营对象</h3>
        <span className="count">{initiative.name || '未命名对象'}</span>
        <div className="actions">
          <span className="chip live"><span className="dot"></span>{dashboardInitiativeTypeLabel(initiative.type)}</span>
          <button className="btn sm" onClick={() => onNav('strategy')}>进入推演 <Icon name="chevron" className="icon sm"/></button>
        </div>
      </div>
      <div className="card-body">
        <div className="callout">
          {initiative.coreMessage || initiative.masterBrief?.objective || '当前对象缺少核心命题，需要在策略推演里补齐目标、受众和内容边界。'}
          <small>{dashboardFormatRange(initiative.startDate, initiative.endDate)} · {initiative.masterBrief?.title || 'Master Brief 未生成'}</small>
        </div>

        <PipelineBar steps={steps} label={`${dashboardInitiativeTypeLabel(initiative.type).toUpperCase()} PIPELINE`}/>

        <div className="grid g-3" style={{marginTop:20}}>
          <SubMetric label="关联内容" value={String(linkedPosts.length)} target={`同步 ${posts.length} 条`} pct={posts.length ? Math.round((linkedPosts.length / posts.length) * 100) : 0}/>
          <SubMetric label="阅读/播放" value={dashboardCompactNumber(linkedViews || totalViews)} target={linkedPosts.length ? '关联内容汇总' : '全部自有内容'} pct={totalViews ? Math.min(100, Math.round(((linkedViews || totalViews) / totalViews) * 100)) : 0}/>
          <SubMetric label="互动转化" value={engagementRate} target={`互动 ${dashboardCompactNumber(linkedInteraction || totalInteraction)}`} pct={linkedViews ? Math.min(100, Math.round((linkedInteraction / Math.max(linkedViews, 1)) * 1000)) : 0} accent/>
        </div>
      </div>
    </div>
  );
};

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 (
  <div style={{marginTop:18, padding:'14px 16px', background:'var(--bg-2)', border:'1px solid var(--hairline)', borderRadius:8}}>
    <div style={{fontFamily:'var(--f-mono)', fontSize:10, color:'var(--ink-4)', letterSpacing:'0.08em', textTransform:'uppercase', marginBottom:10, display:'flex', justifyContent:'space-between'}}>
      <span>{label}</span>
      <span>{doneCount}/{rows.length} 已完成 · {activeCount} 进行中</span>
    </div>
    <div style={{position:'relative', height:6, background:'var(--hairline)', borderRadius:3, overflow:'hidden'}}>
      <div style={{position:'absolute', left:0, top:0, bottom:0, width:`${donePct}%`, background:'var(--accent)', borderRadius:3}}/>
      <div style={{position:'absolute', left:`${donePct}%`, top:0, bottom:0, width:`${activePct}%`, background:'var(--accent)', opacity:0.35, borderRadius:3}}/>
    </div>
    <div style={{display:'grid', gridTemplateColumns:`repeat(${rows.length}, 1fr)`, gap:0, marginTop:10}}>
      {rows.map((s, i) => (
        <div key={i} style={{display:'flex', flexDirection:'column', gap:3, minWidth:0, paddingRight:6, borderLeft: i === 0 ? 'none' : '1px solid var(--hairline)', paddingLeft: i === 0 ? 0 : 8}}>
          <div style={{display:'flex', alignItems:'center', gap:5}}>
            <span style={{
              width:6, height:6, borderRadius:'50%', flexShrink:0,
              background: s.status === 'done' ? 'var(--accent)' : s.status === 'active' ? 'var(--ink)' : 'var(--ink-4)',
              boxShadow: s.status === 'active' ? '0 0 0 3px oklch(0.92 0.01 90)' : 'none',
            }}/>
            <span style={{fontSize:11, fontWeight:500, color: s.status === 'pending' ? 'var(--ink-4)' : 'var(--ink)', whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis'}}>{s.label}</span>
          </div>
          <span style={{fontFamily:'var(--f-mono)', fontSize:9.5, color:'var(--ink-4)', paddingLeft:11, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis'}}>{s.note}</span>
        </div>
      ))}
    </div>
  </div>
); }

const SubMetric = ({ label, value, target, pct, accent }) => (
  <div style={{padding:'12px 14px', background:'var(--bg-2)', borderRadius:8, border:'1px solid var(--hairline)'}}>
    <div style={{fontFamily:'var(--f-mono)', fontSize:10, textTransform:'uppercase', color:'var(--ink-4)', letterSpacing:'0.06em'}}>{label}</div>
    <div style={{fontSize:20, fontFamily:'var(--f-serif)', marginTop:2, color: accent ? 'var(--accent)' : 'var(--ink)'}}>{value}</div>
    <div className={`bar ${accent ? 'accent' : ''}`} style={{marginTop:8}}><span style={{width: pct + '%'}}></span></div>
    <div style={{fontSize:10, color:'var(--ink-4)', marginTop:4, fontFamily:'var(--f-mono)'}}>{target} · {pct}%</div>
  </div>
);

const EventFeedCard = ({ events = [], pendingCount = 0, syncState = {} }) => (
  <div className="card" style={{display:'flex', flexDirection:'column'}}>
    <div className="card-header">
      <h3>事件流 · Live</h3>
      <span className="count">{events.length} 条</span>
      <div className="actions">
        <span className={`chip ${pendingCount ? 'danger' : 'ghost'} mono`} style={{fontSize:10}}>{pendingCount ? `${pendingCount} pending` : (syncState.status || 'idle')}</span>
      </div>
    </div>
    <div className="card-body flush" style={{flex:1}}>
      {events.map((e, i) => (
        <div key={i} style={{padding:'12px 14px', borderBottom: i < events.length - 1 ? '1px solid var(--hairline)' : 'none', display:'flex', gap:12}}>
          <span className={`chip ${{risk:'danger', opp:'accent', ok:'', info:'info'}[e.type] || ''}`} style={{alignSelf:'flex-start', fontSize:10}}>
            <span className="dot" style={e.type==='ok' ? {background:'var(--accent)'} : {}}></span>
            {{risk:'风险', opp:'机会', ok:'完成', info:'提示'}[e.type]}
          </span>
          <div style={{flex:1, minWidth:0}}>
            <div style={{fontSize:12, fontWeight:500, color:'var(--ink)'}}>{e.title}</div>
            <div style={{fontSize:11, color:'var(--ink-3)', marginTop:2}}>{e.source} · <span className="mono" style={{color:'var(--ink-4)'}}>{e.meta}</span></div>
          </div>
          <div style={{fontFamily:'var(--f-mono)', fontSize:10, color:'var(--ink-4)'}}>{e.time}</div>
        </div>
      ))}
      {!events.length && <div style={{padding:'16px 14px', fontSize:12, color:'var(--ink-3)'}}>暂无事件。连接社媒同步或市场感知后会自动出现风险、机会和异常判断。</div>}
    </div>
  </div>
);

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 (
  <div className="card">
    <div className="card-header"><h3>Agent 集群 · 健康度</h3><span className="count">{modules.length} modules</span></div>
    <div className="card-body">
      <div style={{display:'grid', gridTemplateColumns:'repeat(3,1fr)', gap:8}}>
        {modules.map(([id, name, s, note]) => (
          <div key={id} style={{padding:8, background:'var(--bg-2)', borderRadius:6, border:'1px solid var(--hairline)', display:'flex', alignItems:'center', gap:8}}>
            <span style={{width:6, height:6, borderRadius:'50%', background: s==='ok' ? 'var(--accent)' : s==='warn' ? 'var(--warn)' : 'var(--ink-4)'}}></span>
            <div style={{minWidth:0, flex:1}}>
              <div style={{fontSize:11}}>{name}</div>
              <div style={{fontFamily:'var(--f-mono)', fontSize:9, color:'var(--ink-4)', whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis'}}>{note}</div>
            </div>
          </div>
        ))}
      </div>
    </div>
  </div>
); };

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 (
  <div className="card">
    <div className="card-header"><h3>品牌脉搏 · 近 14 天</h3><span className="count">{dailyRows.length ? `${dailyRows.length} 天` : '待同步'}</span></div>
    <div className="card-body">
      <svg viewBox="0 0 400 140" style={{width:'100%', height:140}}>
        <defs>
          <linearGradient id="pulseGrad" x1="0" x2="0" y1="0" y2="1">
            <stop offset="0" stopColor="oklch(0.45 0.12 155)" stopOpacity="0.2"/>
            <stop offset="1" stopColor="oklch(0.45 0.12 155)" stopOpacity="0"/>
          </linearGradient>
        </defs>
        {[20,40,60,80,100,120].map(y => <line key={y} x1="0" x2="400" y1={y} y2={y} stroke="var(--hairline)" strokeDasharray="2 3"/>)}
        {dailyRows.length > 1 && <path d={`M ${viewPoints} L 400,140 L 0,140 Z`} fill="url(#pulseGrad)"/>}
        {dailyRows.length > 1 && <polyline points={viewPoints} fill="none" stroke="oklch(0.45 0.12 155)" strokeWidth="1.5"/>}
        {dailyRows.length > 1 && <polyline points={interactionPoints} fill="none" stroke="var(--ink-3)" strokeWidth="1" strokeDasharray="3 3"/>}
        {!dailyRows.length && <text x="200" y="72" textAnchor="middle" fontSize="12" fill="var(--ink-4)">暂无同步内容表现数据</text>}
      </svg>
      <div style={{display:'flex', gap:12, justifyContent:'space-between', marginTop:4, fontFamily:'var(--f-mono)', fontSize:10, color:'var(--ink-4)'}}>
        {dailyRows.length ? dailyRows.filter((_, i) => i === 0 || i === dailyRows.length - 1 || i % Math.ceil(dailyRows.length / 4) === 0).map(row => <span key={row.label}>{row.label}</span>) : <span>等待 social-sync-state</span>}
      </div>
      <div style={{display:'flex', gap:16, marginTop:12, flexWrap:'wrap'}}>
        <Legend color="oklch(0.45 0.12 155)" label="阅读/播放"/>
        <Legend color="var(--ink-3)" label="互动" dashed/>
      </div>
    </div>
  </div>
); };
const Legend = ({ color, label, dashed }) => (
  <div style={{display:'flex', alignItems:'center', gap:6, fontSize:11, color:'var(--ink-3)'}}>
    <span style={{width:16, height:2, background: dashed ? 'transparent' : color, borderTop: dashed ? `1.5px dashed ${color}` : 'none'}}></span>
    {label}
  </div>
);

// ========== 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 (
  <div className="page wide">
    <div className="page-header">
      <div className="heading">
        <h1>品牌记忆 · <span className="hl">长期认知</span>构建中</h1>
        <p>上传的每一份资料都会被解析为事实、偏好与决策，写入可被策略对象调用的品牌图谱。</p>
      </div>
      <div className="actions">
        <span className="chip ghost mono" style={{fontSize:10}}>
          Workspace · {brand?.name || BRAND.name}
        </span>
        <button className="btn"><Icon name="external" className="icon sm"/>导出图谱</button>
        <button className="btn primary"><Icon name="upload" className="icon sm"/>上传资料</button>
      </div>
    </div>

    <StructuredBrandMemoryPanel
      activeTab={memoryTab}
      onTab={setMemoryTab}
      brand={brand}
      activeConstraints={activeConstraints}
      socialMemory={socialMemory}
      socialWriteStatus={socialWriteStatus}
      onWriteSocialMemory={writeSocialMemory}
      onUpdateBrand={nextBrand => brand && updateBrand(brand.id, nextBrand)}
    />

    {brandToolsReady && brandState.ready && brand && (
      <div className="card longform-policy-card">
        <div className="card-header">
          <h3>品牌治理 · Brand Governance</h3>
          <span className="count">{brand.name} · {activeConstraints.length} active rules · {(brand.products || []).length} products</span>
          <div className="actions">
            <span className="chip ghost mono" style={{fontSize:10}}>source: Brand Memory</span>
          </div>
        </div>
        <div className="card-body">
          <div className="longform-policy-summary">
            <MetricCard label="启用护栏" value={String(activeConstraints.length)} delta={`${brand.constraints.length - activeConstraints.length} disabled`}/>
            <MetricCard label="产品 Profile" value={String(brand.products?.length || 0)} delta={(brand.products || []).map(product => product.name).join(' / ') || '未配置'}/>
            <MetricCard label="叙事主题" value={String(brand.narrative?.requiredThemes?.length || 0)} delta="Brand Narrative"/>
            <MetricCard label="禁用角度" value={String(brand.narrative?.forbiddenAngles?.length || 0)} delta="Governance"/>
          </div>
          <BrandProductMaintenance
            brand={brand}
            onApply={nextBrand => updateBrand(brand.id, nextBrand)}
          />
          <GuardrailMaintenance
            brand={brand}
            onApply={nextBrand => updateBrand(brand.id, nextBrand)}
          />
        </div>
      </div>
    )}

    <div className="grid" style={{gridTemplateColumns:'340px 1fr'}}>
      <div className="col">
        <UploadZone/>
        <div className="card">
          <div className="card-header"><h3>已入库资料</h3><span className="count">{MEMORY_DOCS.length}</span></div>
          <div className="card-body flush">
            {MEMORY_DOCS.map((d, i) => (
              <div key={i} style={{padding:'10px 14px', borderBottom: i < MEMORY_DOCS.length-1 ? '1px solid var(--hairline)' : 'none', display:'flex', gap:10}}>
                <Icon name="doc" className="icon md" style={{color:'var(--ink-3)'}}/>
                <div style={{flex:1, minWidth:0}}>
                  <div style={{fontSize:12, fontWeight:500, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap'}}>{d.name}</div>
                  <div style={{fontSize:10, color:'var(--ink-4)', fontFamily:'var(--f-mono)', marginTop:2}}>{d.size} · {d.time}</div>
                </div>
                <div style={{display:'flex', flexDirection:'column', alignItems:'flex-end', gap:2}}>
                  {d.status === 'parsed' && <span className="chip" style={{fontSize:10}}><span className="dot"></span>{d.facts} facts</span>}
                  {d.status === 'parsing' && <span className="chip info shine" style={{fontSize:10}}><span className="dot"></span>解析中</span>}
                  {d.status === 'queued' && <span className="chip ghost mono" style={{fontSize:10}}>queued</span>}
                </div>
              </div>
            ))}
          </div>
        </div>
      </div>

      <div className="col">
        <div className="card">
          <div className="card-header">
            <h3>品牌图谱 · Brand Memory Graph</h3>
            <span className="count">1,284 节点 · 3,820 关系</span>
            <div className="actions">
              <span className="chip ghost mono" style={{fontSize:10}}>layout: radial</span>
              <button className="btn sm ghost">过滤 <Icon name="filter" className="icon sm"/></button>
            </div>
          </div>
          <div className="card-body" style={{padding:0}}>
            <BrandGraph/>
          </div>
        </div>

        <div className="grid g-3">
          <MemoryFacet title="品牌定位" items={['全域跨界新物种', 'New Breed. No Boundaries.', '一车多能 · 自由切换']} accent/>
          <MemoryFacet title="语言边界" items={['概念车能力需标注边界', '禁：完全自动驾驶 / 无人驾驶', '慎用：财富自由 / 牛马']}/>
          <MemoryFacet title="决策偏好" items={['用场景定义品牌', '商用与乘用跨界', '用户共创定义产品']}/>
          <MemoryFacet title="核心卖点池" items={['全域智能', '全域适应', '全域美学']}/>
          <MemoryFacet title="风险词池" items={['量产承诺', '绝对安全', '替代驾驶员']}/>
          <MemoryFacet title="历史成功样本" items={['品牌核心讯息 0420', 'TVC 新物种脚本', '牛马野逍遥共创计划']}/>
        </div>
      </div>
    </div>
  </div>
  );
};

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 <span className="tag mono" style={colorMap[meta.tone] || {}}>{meta.label}</span>;
};

const MemoryTraceMeta = ({ item = {}, fallback = {}, compact = false }) => {
  const trace = getMemoryTrace(item, fallback);
  return (
    <div className="row" style={{gap:4, flexWrap:'wrap', marginTop:compact ? 5 : 8, minWidth:0}}>
      <MemoryFactChip status={trace.factStatus}/>
      {trace.provenance && <span className="tag mono" style={{overflowWrap:'anywhere'}}>{trace.provenance}</span>}
      {trace.source && <span className="tag mono" style={{overflowWrap:'anywhere'}}>source: {trace.source}</span>}
      {trace.sourceRefs.slice(0, compact ? 2 : 6).map(ref => <span key={ref} className="tag mono" style={{overflowWrap:'anywhere'}}>ref: {ref}</span>)}
      {trace.conflicts.map(ref => <span key={ref} className="tag mono" style={{color:'#580018', overflowWrap:'anywhere'}}>conflict: {ref}</span>)}
    </div>
  );
};

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 = '') => (
    <div className="row" style={{gap:4, flexWrap:'wrap', marginTop:7}}>
      {items.map(item => <span key={`${prefix}${item}`} className="tag mono" style={{overflowWrap:'anywhere'}}>{prefix}{item}</span>)}
    </div>
  );
  return (
    <div style={{display:'grid', gap:12}}>
      <div className="grid" style={{gridTemplateColumns:'repeat(auto-fit, minmax(min(180px, 100%), 1fr))', gap:10}}>
        <MetricCard label="项目阶段" value={String(stages.length)} delta="可供报表抽取"/>
        <MetricCard label="原文确定" value={String(confirmedCount)} delta="documented"/>
        <MetricCard label="推测提取" value={String(inferredCount)} delta="needs traceable source"/>
        <MetricCard label="冲突/待确认" value={String(adjustedCount + pendingCount)} delta="requires review" accent={adjustedCount + pendingCount > 0}/>
      </div>

          <div className="longform-maintenance-rule" style={{display:'grid', gridTemplateColumns:'28px minmax(0, 1fr)', gap:10, alignItems:'start'}}>
        <Icon name="doc" className="icon sm" style={{color:'var(--accent)', marginTop:2}}/>
        <div style={{minWidth:0}}>
          <b>统一溯源规则</b>
          <p>品牌记忆每个模块都使用同一套标识：原文确定可直接进入报表；推测提取只能作为待校验摘要；待确认进入写回队列；冲突修订会保留原始来源与冲突 ID，方便回溯。</p>
          <div className="row" style={{gap:5, flexWrap:'wrap', marginTop:8}}>
            <MemoryFactChip status="confirmed"/>
            <MemoryFactChip status="inferred"/>
            <MemoryFactChip status="pending-confirmation"/>
            <MemoryFactChip status="conflict-adjusted"/>
          </div>
        </div>
      </div>

      <div style={{display:'grid', gap:10}}>
        {stages.map(stage => (
          <div key={stage.id} className="longform-maintenance-rule" style={{display:'grid', gap:10, minWidth:0}}>
            <div style={{display:'flex', justifyContent:'space-between', gap:10, alignItems:'flex-start', flexWrap:'wrap'}}>
              <div style={{minWidth:0}}>
                <MemoryTraceMeta item={stage} compact/>
                <div className="row" style={{gap:5, flexWrap:'wrap', marginTop:6}}>
                  <span className="tag mono">{stage.period || '阶段周期待确认'}</span>
                </div>
                <b style={{display:'block', fontSize:13.5, marginTop:7}}>{stage.name}</b>
              </div>
            </div>

            <div className="grid" style={{gridTemplateColumns:'repeat(auto-fit, minmax(min(220px, 100%), 1fr))', gap:10}}>
              <div>
                <span className="tag mono">阶段目标</span>
                {renderTagList(stage.stageGoals || [], '')}
              </div>
              <div>
                <span className="tag mono">阶段任务</span>
                {renderTagList(stage.stageObjectives || [], '')}
              </div>
              <div>
                <span className="tag mono">策略边界</span>
                {renderTagList(stage.brandStrategyBoundaries || [], '')}
              </div>
            </div>

            <div className="grid" style={{gridTemplateColumns:'minmax(0, 1fr) minmax(0, 1fr)', gap:10}}>
              <div style={{display:'grid', gap:7}}>
                <span className="tag mono">运营偏好</span>
                {(stage.operatingPreferences || []).map(pref => (
                  <div key={pref.id || pref.label} style={{display:'grid', gap:5, padding:'8px 9px', background:'var(--bg-2)', border:'1px solid var(--hairline)', borderRadius:7, minWidth:0}}>
                    <div className="row" style={{gap:5, flexWrap:'wrap'}}>
                      <MemoryTraceMeta item={pref} fallback={{ source: stage.source, sourceEvidenceId: stage.sourceEvidenceId, provenance: stage.provenance }} compact/>
                    </div>
                    <p style={{margin:0, fontSize:11.5, lineHeight:1.5, color:'var(--ink-3)', overflowWrap:'anywhere'}}>{pref.label}</p>
                  </div>
                ))}
              </div>
              <div style={{display:'grid', gap:8, alignContent:'start'}}>
                <span className="tag mono">平台 / 下游依赖</span>
                {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}
              </div>
            </div>

            {stage.sourceRefs?.length ? (
              <div className="row" style={{gap:4, flexWrap:'wrap'}}>
                {stage.sourceRefs.map(ref => <span key={ref} className="tag mono">source: {ref}</span>)}
              </div>
            ) : null}
          </div>
        ))}
      </div>
    </div>
  );
};

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 (
    <div style={{display:'grid', gap:12}}>
      <div className="grid" style={{gridTemplateColumns:'repeat(auto-fit, minmax(min(180px, 100%), 1fr))', gap:10}}>
        <MetricCard label="客户偏好" value={String(preferences.length)} delta={`偏好 ${preferCount} · 避免 ${avoidCount}`}/>
        <MetricCard label="决策记忆" value={String(decisions.length)} delta={`${decisions.filter(item => item.status === 'confirmed').length} confirmed`}/>
        <MetricCard label="长期稳定项" value={String(preferences.filter(item => item.stability === 'long-term').length)} delta="不随短期数据变化"/>
      </div>

      <div className="grid" style={{gridTemplateColumns:'minmax(0, 1fr) minmax(0, 1fr)', gap:12, alignItems:'start'}}>
        <div style={{display:'grid', gap:8}}>
          <div style={{fontSize:12, fontWeight:700}}>客户偏好池</div>
          {preferences.map(item => (
            <div key={item.id} className="longform-maintenance-rule">
              <div className="row" style={{gap:6, flexWrap:'wrap'}}>
                <span className="tag mono">{item.polarity === 'avoid' ? 'Avoid' : 'Prefer'}</span>
                <span className="tag mono">{item.stability || 'medium'}</span>
              </div>
              <b>{item.label}</b>
              <p>{item.description}</p>
              <MemoryTraceMeta item={item} fallback={{ factStatus: item.status || 'confirmed', provenance: 'documented', source: item.source || 'brand-memory' }} compact/>
            </div>
          ))}
        </div>

        <div style={{display:'grid', gap:8}}>
          <div style={{fontSize:12, fontWeight:700}}>历史决策摘要</div>
          {decisions.map(item => (
            <div key={item.id} className="longform-maintenance-rule">
              <div className="row" style={{gap:6, flexWrap:'wrap'}}>
                <span className="tag mono">{item.status || 'pending'}</span>
                <span className="tag mono">{item.type || 'decision'}</span>
              </div>
              <b>{item.title}</b>
              <p>{item.rationale || item.text}</p>
              <div className="row" style={{gap:4, flexWrap:'wrap', marginTop:7}}>
                {(item.impact || []).map(scope => <span key={scope} className="tag mono">{scope}</span>)}
              </div>
              <MemoryTraceMeta item={item} fallback={{ factStatus: item.status || 'confirmed', provenance: item.source === '人工确认' ? 'manual-confirmed' : 'documented', source: item.source || item.decidedAt || 'manual' }} compact/>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
};

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 (
    <div style={{display:'grid', gap:12}}>
      <div className="grid" style={{gridTemplateColumns:'repeat(auto-fit, minmax(min(180px, 100%), 1fr))', gap:10}}>
        <MetricCard label="证据源" value={String(evidenceSources.length)} delta={`${evidenceSources.filter(item => item.status === 'confirmed').length} confirmed`}/>
        <MetricCard label="冲突待确认" value={String(pendingConflicts.length)} delta="requires user decision" accent={pendingConflicts.length > 0}/>
        <MetricCard label="待确认写回" value={String(pendingLearnings.length)} delta="pending learning"/>
        <MetricCard label="已确认学习" value={String(learnings.filter(item => item.status === 'confirmed').length)} delta="confirmed learning"/>
      </div>

      <div style={{display:'grid', gap:8}}>
        <div style={{fontSize:12, fontWeight:700}}>冲突通知</div>
        {pendingConflicts.length ? pendingConflicts.map(conflict => (
          <div key={conflict.id} style={{display:'grid', gridTemplateColumns:'28px minmax(0, 1fr) auto', gap:10, alignItems:'start', padding:'11px 12px', background:'rgba(88, 0, 24, 0.06)', border:'1px solid rgba(88, 0, 24, 0.22)', borderRadius:8}}>
            <Icon name="warn" className="icon sm" style={{color:'#580018', marginTop:2}}/>
            <div style={{minWidth:0}}>
              <div className="row" style={{gap:6, flexWrap:'wrap'}}>
                <span className="tag mono">Conflict</span>
                <span className="tag mono">{conflict.severity || 'needs-confirmation'}</span>
              </div>
              <b style={{display:'block', fontSize:12.5, marginTop:6}}>{conflict.title}</b>
              <p style={{margin:'5px 0 0', fontSize:11.5, color:'var(--ink-3)', lineHeight:1.55}}>{conflict.message}</p>
              <MemoryTraceMeta item={conflict} fallback={{ factStatus: 'pending-confirmation', provenance: 'conflict-detected', source: conflict.sourceEvidenceId || 'conflict-source', conflicts: [conflict.id] }} compact/>
              <div className="grid" style={{gridTemplateColumns:'repeat(auto-fit, minmax(min(240px, 100%), 1fr))', gap:8, marginTop:8}}>
                <div className="longform-maintenance-rule" style={{padding:8}}>
                  <span className="tag mono">初稿信息</span>
                  <p>{conflict.draftClaim}</p>
                  <MemoryTraceMeta item={{ factStatus: 'superseded-draft', provenance: 'documented-draft', source: conflict.sourceEvidenceId }} compact/>
                </div>
                <div className="longform-maintenance-rule" style={{padding:8}}>
                  <span className="tag mono">当前事实</span>
                  <p>{conflict.currentFact}</p>
                  <MemoryTraceMeta item={{ factStatus: 'conflict-adjusted', provenance: 'current-execution-fact', source: 'source-official-social', conflicts: [conflict.id] }} compact/>
                </div>
              </div>
              <div className="row" style={{gap:4, flexWrap:'wrap', marginTop:8}}>
                {(conflict.downstream || []).map(item => <span key={item} className="tag mono">{item}</span>)}
              </div>
            </div>
            <div className="actions" style={{alignSelf:'start'}}>
              {(conflict.options || ['accept_current', 'keep_as_draft', 'needs_more_review']).map(option => (
                <button key={option} className={`btn sm ${option === conflict.recommendedResolution ? 'primary' : 'ghost'}`} onClick={() => resolveConflict(conflict.id, option)}>
                  {conflictActionText[option] || option}
                </button>
              ))}
            </div>
          </div>
        )) : (
          <div className="longform-maintenance-rule">
            <span className="tag mono">Conflict</span>
            <b>暂无冲突待确认</b>
            <p>当新资料与当前执行事实、官方账号数据或已确认记忆冲突时，会先进入这里等待用户明确。</p>
          </div>
        )}
      </div>

      <div style={{display:'grid', gap:8}}>
        <div style={{fontSize:12, fontWeight:700}}>待确认写回</div>
        {pendingLearnings.length ? pendingLearnings.map(item => (
          <div key={item.id} style={{display:'grid', gridTemplateColumns:'minmax(0, 1fr) auto', gap:10, alignItems:'start', padding:'11px 12px', background:'var(--bg-2)', border:'1px solid var(--hairline)', borderRadius:8}}>
            <div style={{minWidth:0}}>
              <div className="row" style={{gap:6, flexWrap:'wrap'}}>
                <span className="tag mono">{item.type || 'learning'}</span>
                <span className="tag mono">{item.status}</span>
              </div>
              <b style={{display:'block', fontSize:12.5, marginTop:6}}>{item.title}</b>
              <p style={{margin:'5px 0 0', fontSize:11.5, color:'var(--ink-3)', lineHeight:1.55}}>{item.text}</p>
              {item.evidencePostIds?.length ? <span className="chip ghost mono" style={{fontSize:10, marginTop:8}}>evidence posts: {item.evidencePostIds.length}</span> : null}
              <MemoryTraceMeta item={item} fallback={{ factStatus: item.status || 'pending-confirmation', provenance: 'pending-writeback', source: item.source || 'brand-memory' }} compact/>
            </div>
            <div className="actions">
              <button className="btn sm" onClick={() => updateLearningStatus(item.id, 'rejected')}>拒绝</button>
              <button className="btn sm primary" onClick={() => updateLearningStatus(item.id, 'confirmed')}>确认写回</button>
            </div>
          </div>
        )) : (
          <div className="longform-maintenance-rule">
            <span className="tag mono">Queue</span>
            <b>暂无待确认写回</b>
            <p>线下导入、社媒内容归档、复盘学习都会先进入这里，确认后再成为稳定记忆。</p>
          </div>
        )}
      </div>

      <div style={{display:'grid', gap:8}}>
        <div style={{fontSize:12, fontWeight:700}}>来源证据链</div>
        <div className="grid" style={{gridTemplateColumns:'repeat(auto-fit, minmax(min(260px, 100%), 1fr))', gap:10}}>
          {evidenceSources.map(source => (
            <div key={source.id} className="longform-maintenance-rule">
              <div className="row" style={{gap:6, flexWrap:'wrap'}}>
                <span className="tag mono">{source.type}</span>
                <span className="tag mono">{source.status}</span>
                <span className="tag mono">{source.stability}</span>
              </div>
              <b>{source.title}</b>
              <p>{(source.facts || []).join('；')}</p>
              <MemoryTraceMeta item={source} fallback={{ factStatus: source.status || 'confirmed', provenance: source.status === 'superseded-draft' ? 'documented-draft' : 'documented', source: source.sourcePath || source.title, sourceRefs: [source.id] }} compact/>
              <span className="chip ghost mono" style={{fontSize:10, marginTop:8}}>{source.confidence || 'medium'} confidence</span>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
};

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 (
    <div style={{display:'grid', gap:10}}>
      <div className="grid" style={{gridTemplateColumns:'repeat(auto-fit, minmax(min(180px, 100%), 1fr))', gap:10}}>
        <MetricCard label="启用规则" value={String(rules.length)} delta="active guardrails"/>
        <MetricCard label="强制规则" value={String(requiredCount)} delta="required / P0"/>
        <MetricCard label="规则类别" value={String(categories.length || 1)} delta={categories.slice(0, 4).join(' / ') || 'brand governance'}/>
      </div>

      <div style={{display:'grid', gap:8}}>
        {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 (
            <div key={item.id || item.rule} style={{display:'grid', gridTemplateColumns:'34px minmax(0, 1fr) auto', gap:10, alignItems:'start', padding:'11px 12px', background:'var(--bg-2)', border:'1px solid var(--hairline)', borderRadius:8, minWidth:0}}>
              <div style={{width:28, height:28, borderRadius:6, display:'grid', placeItems:'center', background:isRequired ? 'rgba(88, 0, 24, 0.1)' : 'var(--surface)', border:'1px solid var(--hairline)', color:isRequired ? '#580018' : 'var(--accent)'}}>
                <Icon name={isRequired ? 'warn' : 'check'} className="icon sm"/>
              </div>
              <div style={{minWidth:0}}>
                <div style={{display:'flex', alignItems:'center', gap:6, flexWrap:'wrap'}}>
                  <span className="tag mono">{levelLabel}</span>
                  <b style={{fontSize:12.5, lineHeight:1.35}}>{item.label || `${item.level} · 品牌护栏`}</b>
                </div>
                <p style={{margin:'6px 0 0', fontSize:11.5, lineHeight:1.55, color:'var(--ink-3)', overflowWrap:'anywhere'}}>{item.description || item.rule}</p>
                {(scopes.length || terms.length) ? (
                  <div style={{display:'grid', gap:6, marginTop:8}}>
                    {scopes.length ? (
                      <div className="row" style={{gap:4, flexWrap:'wrap'}}>
                        {scopes.map(scope => <span key={scope} className="tag mono">{scope}</span>)}
                      </div>
                    ) : null}
                    {terms.length ? (
                      <div className="row" style={{gap:4, flexWrap:'wrap'}}>
                        {terms.slice(0, 8).map(term => <span key={term} className="tag mono" style={{color:'#580018'}}>禁：{term}</span>)}
                      </div>
                    ) : null}
                  </div>
                ) : null}
                <MemoryTraceMeta item={item} fallback={{ factStatus: item.status || 'confirmed', provenance: 'documented', source: item.source || 'brand-policy' }} compact/>
              </div>
              <span className="chip ghost mono" style={{fontSize:10, whiteSpace:'nowrap'}}>{item.category || 'guardrail'}</span>
            </div>
          );
        })}
      </div>
    </div>
  );
};

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 (
    <div className="grid" style={{gridTemplateColumns:'repeat(auto-fit, minmax(min(260px, 100%), 1fr))', gap:10}}>
      {previewItems.map(asset => (
        <div key={asset.label} className="longform-maintenance-rule" style={{display:'grid', gap:10}}>
          <div style={{minHeight:150, background:asset.background || '#111111', border:'1px solid var(--hairline)', borderRadius:8, display:'grid', placeItems:'center', padding:18, overflow:'hidden'}}>
            <img
              src={asset.src}
              alt={asset.name || asset.label}
              style={{maxWidth:'100%', width:'auto', height:'auto', ...asset.boxStyle, objectFit:'contain', display:'block'}}
            />
          </div>
              <div>
                <span className="tag mono">{asset.label}</span>
                <b>{asset.name}</b>
                <p>{asset.usage}</p>
                <MemoryTraceMeta item={asset} fallback={{ factStatus: 'confirmed', provenance: 'documented', source: asset.source || 'brand-logos' }} compact/>
              </div>
            </div>
      ))}
    </div>
  );
};

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 (
    <div className="card">
      <div className="card-header">
        <h3>结构化品牌记忆 · Structured Memory</h3>
        <span className="count">MONTX / 领越 · 8 source files · {activeConstraints.length || source.guardrails.length} active rules</span>
        <div className="actions">
          <span className="chip ghost mono" style={{fontSize:10}}>source: /Downloads/广汽品牌资产</span>
        </div>
      </div>
      <div className="card-body" style={{display:'grid', gap:12}}>
        <BrandLogoMemoryPreview visualSystem={brand?.visualSystem}/>

        {pendingConflicts.length > 0 && (
          <div style={{display:'flex', justifyContent:'space-between', alignItems:'center', gap:12, flexWrap:'wrap', padding:'10px 12px', background:'rgba(88, 0, 24, 0.06)', border:'1px solid rgba(88, 0, 24, 0.22)', borderRadius:8}}>
            <div style={{display:'flex', gap:8, alignItems:'flex-start', minWidth:260, flex:'1 1 420px'}}>
              <Icon name="bell" className="icon sm" style={{color:'#580018', marginTop:2}}/>
              <div>
                <b style={{fontSize:12}}>发现 {pendingConflicts.length} 个品牌记忆冲突，需要人工确认</b>
                <p style={{margin:'3px 0 0', fontSize:11, color:'var(--ink-3)'}}>新资料不会直接覆盖当前事实；请在证据写回里选择以当前事实为准、仅保留初稿或稍后处理。</p>
              </div>
            </div>
            <button className="btn sm primary" onClick={() => onTab('evidence')}>查看冲突</button>
          </div>
        )}

        <div className="longform-maintenance-rule" style={{display:'grid', gridTemplateColumns:'28px minmax(0, 1fr)', gap:10, alignItems:'start'}}>
          <Icon name="doc" className="icon sm" style={{color:'var(--accent)', marginTop:2}}/>
          <div style={{minWidth:0}}>
            <b>全局来源标识</b>
            <p>本页所有品牌记忆条目都必须带确定性与来源。推测、待确认、冲突修订不会静默覆盖原始文档，会保留来源和冲突 ID 供后续回溯。</p>
            <MemoryTraceMeta item={{ factStatus: 'confirmed', provenance: 'documented', source: '原始文档' }} compact/>
            <MemoryTraceMeta item={{ factStatus: 'inferred', provenance: 'inferred-from-approved-content', source: '推测提取' }} compact/>
            <MemoryTraceMeta item={{ factStatus: 'conflict-adjusted', provenance: 'conflict-adjusted', source: '当前执行事实', conflicts: ['conflict-id'] }} compact/>
          </div>
        </div>

        <div className="longform-equal-tabs" style={{gridTemplateColumns:'repeat(auto-fit, minmax(92px, 1fr))', width:'100%'}} role="tablist" aria-label="结构化品牌记忆分类">
          {BRAND_MEMORY_TABS.map(tab => (
            <button key={tab.id} type="button" className={activeTab === tab.id ? 'active' : ''} onClick={() => onTab(tab.id)} role="tab" aria-selected={activeTab === tab.id}>
              {tab.label}
            </button>
          ))}
        </div>

        {activeTab === 'brand' && (
          <div className="grid" style={{gridTemplateColumns:'1.2fr 1fr', gap:12}}>
            <div className="longform-profile-summary" style={{marginTop:0}}>
              {source.brand.map(item => (
                <div key={item.label}>
                  <span className="tag mono">{item.label}</span>
                  <b>{item.value}</b>
                  <p>{item.meta}</p>
                  <MemoryTraceMeta item={item} fallback={{ factStatus: 'confirmed', provenance: 'documented', source: '领越品牌核心讯息-0420-V1.pdf', sourceRefs: ['source-core-message-0420'] }} compact/>
                </div>
              ))}
            </div>
            <div className="longform-guardrail-list">
              {source.values.map(item => (
                <div key={item.title} className="longform-guardrail">
                  <Icon name="check" className="icon sm"/>
                  <div>
                    <b>{item.title}</b>
                    <span>{item.text}</span>
                    <MemoryTraceMeta item={item} fallback={{ factStatus: 'confirmed', provenance: 'documented', source: '领越品牌核心讯息-0420-V1.pdf', sourceRefs: ['source-core-message-0420'] }} compact/>
                  </div>
                </div>
              ))}
            </div>
          </div>
        )}

        {activeTab === 'product' && (
          <div className="longform-guardrail-list">
            {(products.length ? products : source.products).map(product => (
              <div key={product.id || product.name} className="longform-guardrail">
                <Icon name="content" className="icon sm"/>
                <div>
                  <b>{product.name}</b>
                  <span>{product.narrative?.positioning || product.text || product.positioning}</span>
                  <div className="row" style={{gap:4, flexWrap:'wrap', marginTop:7}}>
                    {(product.narrative?.coreCapabilities || [product.tag, product.source]).filter(Boolean).slice(0, 7).map(item => <span key={item} className="tag">{item}</span>)}
                  </div>
                  <MemoryTraceMeta item={product} fallback={{ factStatus: 'confirmed', provenance: 'documented', source: product.source || '新闻稿 / 讲话稿 / 产品稿', sourceRefs: ['source-news-product-speech'] }} compact/>
                </div>
              </div>
            ))}
          </div>
        )}

        {activeTab === 'visual' && (
          <VisualSystemMemoryPanel visualSystem={brand?.visualSystem} sourceVisual={source.visual}/>
        )}

        {activeTab === 'campaign' && (
          <div className="grid" style={{gridTemplateColumns:'repeat(2, minmax(0, 1fr))', gap:10}}>
            {source.campaigns.map(item => (
              <div key={item.title} className="longform-maintenance-rule">
                <span className="tag mono">Campaign</span>
                <b>{item.title}</b>
                <p>{item.text}</p>
                <MemoryTraceMeta item={item} fallback={{ factStatus: 'confirmed', provenance: 'documented', source: item.title.includes('牛马') ? '牛马野逍遥-0415.pptx' : 'gac-brand-assets-2026-04' }} compact/>
              </div>
            ))}
          </div>
        )}

        {activeTab === 'operation' && (
          <OperatingStageMemoryPanel brand={brand}/>
        )}

        {activeTab === 'social' && (
          <SocialContentMemoryPanel
            socialMemory={socialMemory}
            savedSocialAssets={brand?.socialAssets}
            writeStatus={socialWriteStatus}
            onWrite={onWriteSocialMemory}
          />
        )}

        {activeTab === 'preference' && (
          <PreferenceDecisionMemoryPanel brand={brand}/>
        )}

        {activeTab === 'evidence' && (
          <EvidenceWritebackMemoryPanel brand={brand} onUpdateBrand={onUpdateBrand}/>
        )}

        {activeTab === 'guardrail' && (
          <GuardrailRulesMemoryPanel rules={activeConstraints.length ? activeConstraints : source.guardrails}/>
        )}
      </div>
    </div>
  );
};

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 (
    <div style={{display:'grid', gap:12}}>
      {viGuidelines.length > 0 && (
        <div className="longform-guardrail">
          <Icon name="doc" className="icon sm"/>
          <div>
            <b>VI Source · {viGuidelines[0].name}</b>
            <span>{viGuidelines[0].file} · {viGuidelines[0].pages} pages · {viGuidelines[0].source}</span>
            <MemoryTraceMeta item={viGuidelines[0]} fallback={{ factStatus: 'confirmed', provenance: 'documented', source: viGuidelines[0].file || 'MONTX_GUIDELINES_V1.1.pptx', sourceRefs: ['source-vi-guidelines-v11'] }} compact/>
          </div>
        </div>
      )}

      {logoAssets.length > 0 && (
        <div className="grid" style={{gridTemplateColumns:'repeat(auto-fit, minmax(min(220px, 100%), 1fr))', gap:10}}>
          {logoAssets.map(asset => (
            <div key={asset.id || asset.src} className="longform-maintenance-rule" style={{display:'grid', gap:10}}>
              <div style={{height:112, background:asset.background || '#111111', border:'1px solid var(--hairline)', borderRadius:8, display:'grid', placeItems:'center', padding:14, overflow:'hidden'}}>
                <img src={asset.src} alt={asset.name} style={{maxWidth:'100%', maxHeight:84, width:'auto', height:'auto', objectFit:'contain', display:'block'}}/>
              </div>
              <div>
                <span className="tag mono">{asset.type || 'logo'}</span>
                <b>{asset.name}</b>
                <p>{asset.usage}</p>
                <MemoryTraceMeta item={asset} fallback={{ factStatus: 'confirmed', provenance: 'documented', source: asset.source || 'brand-logos', sourceRefs: ['source-vi-guidelines-v11'] }} compact/>
              </div>
            </div>
          ))}
        </div>
      )}

      {colorTokens.length > 0 && (
        <div className="grid" style={{gridTemplateColumns:'repeat(auto-fit, minmax(min(180px, 100%), 1fr))', gap:10}}>
          {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 (
              <div key={token.id || token.name} className="longform-maintenance-rule" style={{padding:0, overflow:'hidden'}}>
                <div style={{height:76, background: swatch, borderBottom:'1px solid var(--hairline)', display:'flex', alignItems:'flex-end', padding:10, color: darkText ? '#000000' : '#FFFFFF'}}>
                  <span className="mono" style={{fontSize:11, fontWeight:700}}>{token.hex}{token.alpha ? ` / ${Math.round(token.alpha * 100)}%` : ''}</span>
                </div>
                <div style={{padding:10}}>
                  <span className="tag mono">{token.role}</span>
                  <b>{token.name}</b>
                  <p>{token.usage}</p>
                  <div className="row" style={{gap:4, flexWrap:'wrap', marginTop:7}}>
                    {token.rgb && <span className="tag mono">RGB {token.rgb}</span>}
                    {token.cmyk && <span className="tag mono">{token.cmyk}</span>}
                    {token.pms && <span className="tag mono">PMS {token.pms}</span>}
                  </div>
                  <MemoryTraceMeta item={token} fallback={{ factStatus: 'confirmed', provenance: 'documented', source: 'MONTX_GUIDELINES_V1.1.pptx', sourceRefs: ['source-vi-guidelines-v11'] }} compact/>
                </div>
              </div>
            );
          })}
        </div>
      )}

      {Object.keys(semanticColors).length > 0 && (
        <div className="longform-guardrail">
          <Icon name="check" className="icon sm"/>
          <div>
            <b>Semantic Color Tokens</b>
            <span>供长图文、内容生成、视觉检查共用的语义色值。</span>
            <div className="row" style={{gap:4, flexWrap:'wrap', marginTop:7}}>
              {Object.entries(semanticColors).map(([key, value]) => <span key={key} className="tag mono">{key}: {value}</span>)}
            </div>
            <MemoryTraceMeta item={{ source: 'MONTX_GUIDELINES_V1.1.pptx' }} fallback={{ factStatus: 'confirmed', provenance: 'documented', sourceRefs: ['source-vi-guidelines-v11'] }} compact/>
          </div>
        </div>
      )}

      {logoLockups.length > 0 && (
        <div className="grid" style={{gridTemplateColumns:'repeat(auto-fit, minmax(min(260px, 100%), 1fr))', gap:10}}>
          {logoLockups.map(lockup => (
            <div key={lockup.id} className="longform-maintenance-rule">
              <span className="tag mono">Logo Lockup</span>
              <b>{lockup.name}</b>
              <p>{lockup.usage}</p>
              <p>{[lockup.construction, lockup.isolation].filter(Boolean).join('；')}</p>
              <MemoryTraceMeta item={lockup} fallback={{ factStatus: 'confirmed', provenance: 'documented', source: 'MONTX_GUIDELINES_V1.1.pptx', sourceRefs: ['source-vi-guidelines-v11'] }} compact/>
            </div>
          ))}
        </div>
      )}

      {typography.length > 0 && (
        <div className="grid" style={{gridTemplateColumns:'repeat(auto-fit, minmax(min(220px, 100%), 1fr))', gap:10}}>
          {typography.map(type => (
            <div key={type.id} className="longform-maintenance-rule">
              <span className="tag mono">{type.role}</span>
              <b>{type.name}</b>
              <p>{type.usage}</p>
              <MemoryTraceMeta item={type} fallback={{ factStatus: 'confirmed', provenance: 'documented', source: 'MONTX_GUIDELINES_V1.1.pptx', sourceRefs: ['source-vi-guidelines-v11'] }} compact/>
            </div>
          ))}
        </div>
      )}

      {groupedRules.length > 0 && (
        <div className="grid" style={{gridTemplateColumns:'repeat(auto-fit, minmax(min(260px, 100%), 1fr))', gap:10}}>
          {groupedRules.map(group => (
            <div key={group.label} className="longform-maintenance-rule">
              <span className="tag mono">{group.label}</span>
              <b>{group.items.length} 条结构化规则</b>
              <p>{group.items.join('；')}</p>
              <MemoryTraceMeta item={group} fallback={{ factStatus: 'confirmed', provenance: 'documented', source: 'MONTX_GUIDELINES_V1.1.pptx / MONTX_DEFAULT_SKILL.md', sourceRefs: ['source-vi-guidelines-v11'] }} compact/>
            </div>
          ))}
        </div>
      )}

      {applicationRules.length > 0 && (
        <div className="grid" style={{gridTemplateColumns:'repeat(auto-fit, minmax(min(220px, 100%), 1fr))', gap:10}}>
          {applicationRules.map(rule => (
            <div key={rule.id} className="longform-maintenance-rule">
              <span className="tag mono">VI Application</span>
              <b>{rule.name}</b>
              <p>{rule.usage}</p>
              <MemoryTraceMeta item={rule} fallback={{ factStatus: 'confirmed', provenance: 'documented', source: 'MONTX_GUIDELINES_V1.1.pptx', sourceRefs: ['source-vi-guidelines-v11'] }} compact/>
            </div>
          ))}
        </div>
      )}

      <div className="grid" style={{gridTemplateColumns:'repeat(auto-fit, minmax(min(260px, 100%), 1fr))', gap:10}}>
        {sourceVisual.map(item => (
          <div key={item.label} className="longform-maintenance-rule">
            <span className="tag mono">Source Visual</span>
            <b>{item.label}</b>
            <p>{item.value}</p>
            <MemoryTraceMeta item={item} fallback={{ factStatus: 'confirmed', provenance: 'documented', source: 'MONTX_GUIDELINES_V1.1.pptx', sourceRefs: ['source-vi-guidelines-v11'] }} compact/>
          </div>
        ))}
      </div>
    </div>
  );
};

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 (
    <div style={{display:'grid', gap:12}}>
      <div className="longform-policy-summary" style={{marginTop:0}}>
        <MetricCard label="官方账号" value={String(accounts.length)} delta={accounts.map(account => account.platform).filter(Boolean).slice(0, 4).join(' / ') || '待同步'}/>
        <MetricCard label="认可内容" value={String(posts.length)} delta="已发布内容证据池"/>
        <MetricCard label="评论样本" value={formatMemoryNumber(comments.total || 0)} delta={`正向 ${formatMemoryNumber(comments.positive || 0)} · 风险 ${formatMemoryNumber(comments.risk || 0)}`}/>
        <MetricCard label="记忆状态" value={savedAt ? '已归档' : '待写入'} delta={savedAt || socialMemory?.updatedAt || 'social sync'}/>
      </div>

      <div style={{display:'flex', justifyContent:'space-between', alignItems:'center', gap:12, flexWrap:'wrap', padding:'10px 12px', background:'var(--bg-2)', border:'1px solid var(--hairline)', borderRadius:8}}>
        <div style={{minWidth:260, flex:'1 1 420px'}}>
          <div style={{fontSize:12, fontWeight:600}}>客户认可内容 · Approved Social Evidence</div>
          <div style={{fontSize:11, color:'var(--ink-3)', marginTop:2}}>
            官方账号已发布内容作为品牌表达证据；写入后进入 pending learning，后续再人工提升为规则或案例。
          </div>
          <MemoryTraceMeta item={{ factStatus: savedAt ? 'confirmed' : 'pending-confirmation', provenance: savedAt ? 'approved-social-content' : 'pending-writeback', source: 'source-official-social', sourceRefs: ['social-sync-state.json'] }} compact/>
        </div>
        <div className="actions" style={{flex:'0 0 auto'}}>
          {writeStatus && <span className="chip" style={{fontSize:10}}><span className="dot"></span>{writeStatus}</span>}
          <button className="btn sm primary" onClick={onWrite} disabled={!posts.length}>
            <Icon name="check" className="icon sm"/>写入品牌记忆
          </button>
        </div>
      </div>

      <div className="grid" style={{gridTemplateColumns:'minmax(240px, 0.34fr) minmax(0, 1fr)', gap:12, alignItems:'start'}}>
        <div style={accountPanelStyle}>
          <div style={{fontSize:12, fontWeight:700}}>官方账号</div>
          {accounts.length ? accounts.map(account => (
            <div key={account.account_id} style={{...socialCardStyle, display:'grid', gridTemplateColumns:'22px minmax(0, 1fr)', gap:8, alignItems:'start'}}>
              <Icon name="content" className="icon sm" style={{color:'var(--accent)', marginTop:2}}/>
              <div style={{minWidth:0}}>
                <b style={{display:'block', fontSize:12, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap'}}>{account.platform} · {account.account_name || account.handle}</b>
                <span style={{display:'block', fontSize:11, color:'var(--ink-4)', lineHeight:1.45, marginTop:2}}>{account.positioning || account.owner || account.handle || '官方账号资产'}</span>
                <div className="row" style={{gap:4, flexWrap:'wrap', marginTop:8}}>
                  <span className="tag mono">{account.status || 'active'}</span>
                  {account.follower_delta_period ? <span className="tag mono">+{formatMemoryNumber(account.follower_delta_period)} followers</span> : null}
                </div>
                <MemoryTraceMeta item={{ factStatus: 'confirmed', provenance: 'official-account-sync', source: account.source || 'social-sync', sourceRefs: [account.account_id] }} compact/>
              </div>
            </div>
          )) : (
            <div style={{...socialCardStyle, display:'grid', gridTemplateColumns:'22px minmax(0, 1fr)', gap:8}}>
              <Icon name="warn" className="icon sm" style={{color:'var(--accent)', marginTop:2}}/>
              <div><b style={{fontSize:12}}>暂无账号同步</b><span style={{display:'block', fontSize:11, color:'var(--ink-4)'}}>社媒监测同步后，这里会显示官方账号资产。</span></div>
            </div>
          )}
        </div>

        <div style={{display:'grid', gap:8, minWidth:0}}>
          <div style={{display:'flex', justifyContent:'space-between', alignItems:'center', gap:8}}>
            <div style={{fontSize:12, fontWeight:700}}>认可内容证据池</div>
            <span className="tag mono">{posts.length} posts</span>
          </div>
          {posts.length ? posts.map(post => (
            <div key={post.post_id} style={{...socialCardStyle, display:'grid', gridTemplateColumns:post.cover_url ? '76px minmax(0, 1fr) auto' : '28px minmax(0, 1fr) auto', gap:10, alignItems:'start'}}>
              {post.cover_url ? (
                <img src={post.cover_url} alt="" style={{width:76, height:76, objectFit:'cover', borderRadius:6, border:'1px solid var(--hairline)', display:'block'}}/>
              ) : (
                <Icon name="doc" className="icon sm" style={{color:'var(--accent)', marginTop:2}}/>
              )}
              <div style={{minWidth:0}}>
                <b style={{display:'-webkit-box', WebkitLineClamp:2, WebkitBoxOrient:'vertical', overflow:'hidden', fontSize:12.5, lineHeight:1.35}}>{post.title}</b>
                <span style={{display:'block', fontSize:11, color:'var(--ink-4)', marginTop:4, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap'}}>{post.platform} · {post.content_type || '内容'} · {post.campaign || '未标记策略对象'}</span>
                <div className="row" style={{gap:4, flexWrap:'wrap', marginTop:8}}>
                  <span className="tag mono">阅读/播放 {formatMemoryNumber(post.metrics?.views || 0)}</span>
                  <span className="tag mono">赞 {formatMemoryNumber(post.metrics?.likes || 0)}</span>
                  <span className="tag mono">评 {formatMemoryNumber(post.metrics?.comments || 0)}</span>
                  <span className="tag mono">转 {formatMemoryNumber(post.metrics?.shares || 0)}</span>
                </div>
                <MemoryTraceMeta item={{ factStatus: 'confirmed', provenance: post.memoryRole || 'approved-social-content', source: post.source || 'social-sync', sourceRefs: [post.post_id] }} compact/>
              </div>
              {post.url && <a className="btn sm ghost" href={getMemoryPostUrl(post)} target="_blank" rel="noreferrer" style={{alignSelf:'start'}}>打开</a>}
            </div>
          )) : (
            <div style={{...socialCardStyle, display:'grid', gridTemplateColumns:'22px minmax(0, 1fr)', gap:8}}>
              <Icon name="warn" className="icon sm" style={{color:'var(--accent)', marginTop:2}}/>
              <div>
                <b style={{fontSize:12}}>暂无已发布内容</b>
                <span style={{display:'block', fontSize:11, color:'var(--ink-4)'}}>同步官方账号内容后，会按互动与发布时间沉淀为客户认可样本。</span>
              </div>
            </div>
          )}
        </div>
      </div>
    </div>
  );
};

const UploadZone = () => (
  <div className="card" style={{padding:0}}>
    <div style={{padding:24, border:'1.5px dashed var(--hairline-2)', borderRadius:'var(--radius-lg)', textAlign:'center', margin:12, background:'var(--bg-2)'}}>
      <Icon name="upload" className="icon" style={{width:22, height:22, color:'var(--ink-3)'}}/>
      <div style={{marginTop:8, fontSize:13}}>拖入文档 · 会议纪要 · 图片</div>
      <div style={{fontSize:11, color:'var(--ink-4)', marginTop:4, fontFamily:'var(--f-mono)'}}>PDF · DOCX · MD · TXT · PNG</div>
      <button className="btn primary sm" style={{marginTop:12}}>选择文件</button>
    </div>
  </div>
);

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 (
    <svg viewBox="0 0 640 420" style={{width:'100%', height:440, display:'block'}}>
      <defs>
        <radialGradient id="bgDot" cx="50%" cy="50%">
          <stop offset="0" stopColor="oklch(0.95 0.02 155)" stopOpacity="0.6"/>
          <stop offset="1" stopColor="oklch(0.95 0.02 155)" stopOpacity="0"/>
        </radialGradient>
      </defs>
      <circle cx={cx} cy={cy} r="160" fill="url(#bgDot)"/>
      {/* grid dots */}
      {Array.from({length: 20}).map((_, r) =>
        Array.from({length: 32}).map((_, c) => (
          <circle key={`${r}-${c}`} cx={c*20+10} cy={r*20+10} r="0.7" fill="var(--hairline-2)"/>
        ))
      )}

      {/* hub lines */}
      {hubs.map((h, i) => (
        <line key={`hl-${i}`} x1={cx} y1={cy} x2={h.x} y2={h.y} stroke={catColor[h.cat]} strokeOpacity="0.3" strokeWidth="1"/>
      ))}
      {/* leaf lines */}
      {leaves.map((l, i) => {
        const h = hubs[l.hub];
        return <line key={`ll-${i}`} x1={h.x} y1={h.y} x2={h.x + l.offX} y2={h.y + l.offY} stroke={catColor[h.cat]} strokeOpacity="0.2" strokeWidth="0.8" strokeDasharray="2 2"/>;
      })}

      {/* hubs */}
      {hubs.map((h, i) => (
        <g key={i}>
          <circle cx={h.x} cy={h.y} r={h.r} fill="white" stroke={catColor[h.cat]} strokeWidth="1.5"/>
          <circle cx={h.x} cy={h.y} r={h.r-6} fill={catColor[h.cat]} fillOpacity="0.15"/>
          <text x={h.x} y={h.y + 3} textAnchor="middle" fontSize="10" fontWeight="500" fill={catColor[h.cat]}>{h.label}</text>
        </g>
      ))}
      {/* leaves */}
      {leaves.map((l, i) => {
        const h = hubs[l.hub];
        return (
          <g key={`lf-${i}`}>
            <rect x={h.x + l.offX - 36} y={h.y + l.offY - 8} width="72" height="16" rx="3" fill="white" stroke="var(--hairline)" strokeWidth="0.8"/>
            <text x={h.x + l.offX} y={h.y + l.offY + 3} textAnchor="middle" fontSize="9" fill="var(--ink-2)">{l.label}</text>
          </g>
        );
      })}

      {/* center */}
      <circle cx={cx} cy={cy} r={center.r+8} fill="var(--ink)" fillOpacity="0.06"/>
      <circle cx={cx} cy={cy} r={center.r} fill="var(--ink)" stroke="var(--ink)"/>
      <text x={cx} y={cy - 2} textAnchor="middle" fontSize="11" fill="white" fontFamily="var(--f-serif)">MONTX</text>
      <text x={cx} y={cy + 11} textAnchor="middle" fontSize="9" fill="oklch(0.85 0.005 80)" fontFamily="var(--f-mono)">BRAND CORE</text>

      {/* legend */}
      <g transform="translate(20, 390)">
        {Object.entries(catColor).slice(0, 7).map(([k, c], i) => (
          <g key={k} transform={`translate(${i*88}, 0)`}>
            <circle cx="5" cy="0" r="4" fill={c} fillOpacity="0.2" stroke={c}/>
            <text x="14" y="3" fontSize="9" fill="var(--ink-3)">{({identity:'身份',product:'产品',boundary:'边界',decision:'决策',selling:'卖点',risk:'风险',preference:'偏好'})[k]}</text>
          </g>
        ))}
      </g>
    </svg>
  );
};

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 }) => (
  <div className="card">
    <div className="card-header"><h3 style={accent ? {color:'var(--accent)'} : {}}>{title}</h3></div>
    <div className="card-body" style={{display:'flex', flexDirection:'column', gap:6}}>
      {items.map((it, i) => (
        <div key={i} style={{fontSize:12, padding:'4px 0', color:'var(--ink-2)', borderBottom: i < items.length-1 ? '1px dashed var(--hairline)' : 'none'}}>
          · {it}
        </div>
      ))}
      <MemoryTraceMeta item={getMemoryFacetTrace(title)} compact/>
    </div>
  </div>
);

// ========== 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 (
  <div className="page wide market-sensing-page">
    <div className="page-header">
      <div className="heading">
        <h1>市场感知 · <span className="hl">{brandName}</span> 外部信号工作台</h1>
        <p>以同行业外部品牌、品类趋势、媒体达人和用户自然讨论为主信号；MONTX 自有内容只作为对照基线和护栏输入。</p>
      </div>
      <div className="actions">
        <div className="segmented market-window-control">
          {MARKET_TIME_WINDOWS.map(item => (
            <button key={item.id} className={windowId === item.id ? 'active' : ''} onClick={() => setWindowId(item.id)}>{item.label}</button>
          ))}
        </div>
        <button className="btn" onClick={() => loadMarketData(true)} disabled={marketState.loading}>
          <Icon name="refresh" className="icon sm"/>{marketState.loading ? '同步中' : '刷新数据'}
        </button>
      </div>
    </div>

    <div className="grid market-counter-grid">
      {counters.map(item => <SignalCounter key={item.label} {...item}/>)}
    </div>

    <div className="grid market-focus-grid">
      {focusCounters.map(item => (
        <button key={item.id} className={`card market-focus-card ${focusFilter === item.id ? 'active' : ''}`} onClick={() => setFocusFilter(focusFilter === item.id ? 'all' : item.id)}>
          <b>{item.value}</b>
          <span>{item.title}</span>
          <small>{item.text}</small>
        </button>
      ))}
    </div>

    <div className="card market-tabs-card">
      <div className="card-body">
        <div className="longform-equal-tabs market-view-tabs" role="tablist" aria-label="市场感知视图分类">
          {MARKET_VIEW_TABS.map(tab => (
            <button key={tab.id} type="button" className={activeTab === tab.id ? 'active' : ''} onClick={() => setActiveTab(tab.id)} role="tab" aria-selected={activeTab === tab.id}>
              {tab.label}
            </button>
          ))}
        </div>
      </div>
    </div>

    <div className="card">
      <div className="card-body market-filter-bar">
        <div className="segmented market-type-control">
          {MARKET_SIGNAL_TYPES.map(item => (
            <button key={item.id} className={typeFilter === item.id ? 'active' : ''} onClick={() => setTypeFilter(item.id)}>{item.label}</button>
          ))}
        </div>
        <div className="segmented market-focus-control">
          {MARKET_CONTENT_FOCUS_FILTERS.map(item => (
            <button key={item.id} className={focusFilter === item.id ? 'active' : ''} onClick={() => setFocusFilter(item.id)}>{item.label}</button>
          ))}
        </div>
        <select className="input market-platform-select" value={platform} onChange={event => setPlatform(event.target.value)}>
          {platforms.map(item => <option key={item} value={item}>{item}</option>)}
        </select>
        <div className="search market-signal-search">
          <Icon name="search" className="icon sm"/>
          <input className="input" value={query} onChange={event => setQuery(event.target.value)} placeholder="搜索原文 / 场景 / 产品 / 主题"/>
        </div>
        <span className={`chip ${marketState.error ? 'warn' : 'live'}`}><span className="dot"></span>{filteredSignals.length} 条信号 · {marketSourceText[marketState.source] || marketState.source}</span>
      </div>
    </div>

    {activeTab === 'overview' && (
      <>
        <div className="grid" style={{gridTemplateColumns:'1fr 1fr'}}>
          <div className="card">
            <div className="card-header">
              <h3>外部市场声量 vs 自有基线</h3>
              <span className="count">覆盖示意 · 外部优先</span>
            </div>
            <div className="card-body">
              <SensingChart/>
              <div style={{display:'flex', gap:16, marginTop:12, flexWrap:'wrap'}}>
                <Legend color="oklch(0.45 0.12 155)" label="外部品类 / 用户需求"/>
                <Legend color="oklch(0.55 0.18 25)" label="新能源皮卡 / VAN 竞品"/>
                <Legend color="var(--ink-3)" label="全域出行品类均值" dashed/>
              </div>
            </div>
          </div>

          <div className="card">
            <div className="card-header"><h3>平台 × 主题 · 热度矩阵</h3><span className="count">示意数据</span></div>
            <div className="card-body">
              <HeatMatrix/>
            </div>
          </div>
        </div>

        <MarketSignalDetail signal={selectedSignal} written={writtenIds.includes(selectedSignal?.id)} onWrite={writeSelected}/>

        <div className="card">
          <div className="card-header">
            <h3>信号流 · Compressed</h3>
            <span className="count">{filteredSignals.length} / {signals.length}</span>
            <div className="actions">
              {marketState.error && <span className="chip warn compact"><span className="dot"></span>静态兜底</span>}
              <span className="chip ghost mono" style={{fontSize:10}}>按优先级排序</span>
            </div>
          </div>
          <div className="card-body flush">
            {filteredSignals.length ? filteredSignals.map((signal, i) => (
              <MarketSignalRow
                key={signal.id}
                signal={signal}
                active={selectedSignal?.id === signal.id}
                written={writtenIds.includes(signal.id)}
                isLast={i === filteredSignals.length - 1}
                onSelect={() => setSelectedId(signal.id)}
              />
            )) : (
              <div style={{padding:18, color:'var(--ink-3)', fontSize:12}}>当前筛选没有匹配信号。调整时间窗、平台或关键词后重试。</div>
            )}
          </div>
        </div>

        <div className="grid" style={{gridTemplateColumns:'0.9fr 1.1fr'}}>
          <MarketSourceCoverage competitors={marketState.competitors}/>
          <SensingPipeline/>
        </div>
      </>
    )}

    {activeTab === 'evidence' && (
      <MarketEvidencePanel
        signals={filteredEvidence}
        loaded={marketState.evidence?.length || filteredEvidence.length}
        total={marketState.evidenceTotal || filteredEvidence.length}
        selectedSignal={selectedEvidence}
        written={writtenIds.includes(selectedEvidence?.id)}
        onSelect={setSelectedId}
        onWrite={writeSelected}
        onLoadMore={loadMoreEvidence}
        loadingMore={marketState.evidenceLoading}
      />
    )}

    {activeTab === 'learning' && <MarketLearningMap learningMap={marketState.learningMap} loading={marketState.loading}/>}

    {activeTab === 'memory' && (
      <div className="grid" style={{gridTemplateColumns:'1.05fr 1.2fr'}}>
        <MarketObjectMap reviewQueue={marketState.reviewQueue} signals={signals}/>
        <MarketMemorySummary outputs={outputRows} loading={marketState.loading}/>
      </div>
    )}
  </div>
  );
};

const MarketSignalRow = ({ signal, active, written, isLast, onSelect }) => (
  <button
    className={`market-signal-row ${active ? 'active' : ''}`}
    onClick={onSelect}
    style={{borderBottom: isLast ? 'none' : '1px solid var(--hairline)'}}
  >
    <div className="market-signal-time">
      <b>{signal.time}</b>
      <span>{signal.source}</span>
    </div>
    <div className="market-brand-cell">
      <span className={`market-brand-logo ${signal.brand?.accent || 'neutral'}`}>{signal.brand?.logoText || 'EXT'}</span>
      <span>{signal.brandName || signal.brand?.brandName || '外部来源'}</span>
      {signal.brand?.priority && <small>{marketPriorityText[signal.brand.priority] || signal.brand.priority}</small>}
    </div>
    <span className="tag" style={{alignSelf:'flex-start', minWidth:54, justifyContent:'center'}}>{signal.plat}</span>
    <div className="market-signal-copy">
      <div className="market-signal-title">
        <b>{signal.title}</b>
        {written && <span className="tag accent mono">已读</span>}
      </div>
      <p>{signal.detail}</p>
      <div className="market-signal-themes">
        {signal.focusLabel && <span className="tag accent mono">{signal.focusLabel}</span>}
        {(signal.themes || []).map(theme => <span key={theme} className="tag mono">{theme}</span>)}
      </div>
    </div>
    <div className="market-score-cell">
      <span className={`chip ${getMarketChipClass(signal.tag)}`}><span className="dot"></span>{marketTagText[signal.tag]}</span>
      <b>{signal.score}</b>
      <small>优先级</small>
    </div>
  </button>
);

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 (
    <div className="grid market-evidence-layout">
      <div className="card">
        <div className="card-header">
          <h3>竞品原文 · Evidence Feed</h3>
          <span className="count">筛选 {signals.length} · 已载入 {loaded || signals.length} / 原始库 {total || signals.length}</span>
          <div className="actions">
            {canLoadMore && (
              <button className="btn sm" type="button" onClick={onLoadMore} disabled={loadingMore}>
                <Icon name="plus" className="icon sm"/>{loadingMore ? '加载中' : '加载更多'}
              </button>
            )}
            <span className="chip ghost mono" style={{fontSize:10}}>原文 → 分类 → 信号</span>
          </div>
        </div>
        <div className="card-body flush market-evidence-list">
          {signals.length ? signals.map((signal, index) => (
            <button
              key={signal.id}
              type="button"
              className={`market-evidence-row ${activeSignal?.id === signal.id ? 'active' : ''}`}
              onClick={() => onSelect(signal.id)}
              style={{borderBottom: index === signals.length - 1 ? 'none' : '1px solid var(--hairline)'}}
            >
              <div className="market-evidence-head">
                <div className="market-brand-cell">
                  <span className={`market-brand-logo ${signal.brand?.accent || 'neutral'}`}>{signal.brand?.logoText || 'EXT'}</span>
                  <span>{signal.brandName || signal.brand?.brandName || '外部来源'}</span>
                  {signal.brand?.priority && <small>{marketPriorityText[signal.brand.priority] || signal.brand.priority}</small>}
                </div>
                <div className="market-evidence-meta">
                  <span className="tag">{signal.plat}</span>
                  {signal.accountName && <span className="tag mono">{signal.accountName}</span>}
                  {signal.postDate && <span className="tag mono">{signal.postDate}</span>}
                  {signal.sourceContentType && <span className="tag mono">{signal.sourceContentType}</span>}
                </div>
              </div>
              <p className="market-evidence-raw">{signal.rawText || signal.detail}</p>
              <div className="market-signal-themes">
                {signal.focusLabel && <span className="tag accent mono">{signal.focusLabel}</span>}
                {(signal.models || []).slice(0, 3).map(model => <span key={model} className="tag mono">{model}</span>)}
                {(signal.contentIds || []).slice(0, 1).map(contentId => <span key={contentId} className="tag mono">{contentId}</span>)}
              </div>
            </button>
          )) : (
            <div className="market-empty-state">当前筛选没有匹配原文。调整平台、内容方向或关键词后重试。</div>
          )}
        </div>
      </div>

      <div className="market-evidence-side">
        <MarketRawEvidenceDetail signal={activeSignal}/>
        <MarketSignalDetail signal={activeSignal} written={written} onWrite={onWrite}/>
      </div>
    </div>
  );
};

const MarketRawEvidenceDetail = ({ signal }) => {
  if (!signal) {
    return (
      <div className="card">
        <div className="card-header">
          <h3>原文推导 · Derivation</h3>
          <span className="count">等待选择</span>
        </div>
        <div className="card-body">
          <p className="market-empty-state">选择一条竞品原文后，这里会展示原文、分类字段和推导证据。</p>
        </div>
      </div>
    );
  }
  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 (
    <div className="card">
      <div className="card-header">
        <h3>原文推导 · Derivation</h3>
        <span className="count">{signal.postDate || signal.time}</span>
      </div>
      <div className="card-body market-raw-detail">
        <div className="market-raw-source">
          <span className={`market-brand-logo ${signal.brand?.accent || 'neutral'}`}>{signal.brand?.logoText || 'EXT'}</span>
          <div>
            <b>{signal.brandName || '外部来源'}</b>
            <p>{[signal.plat, signal.accountName, signal.postDate || '未标注日期', (signal.contentIds || []).join(' / ') || signal.product].filter(Boolean).join(' · ')}</p>
          </div>
        </div>
        {signal.sourceUrl && (
          <a className="market-source-link" href={signal.sourceUrl} target="_blank" rel="noreferrer">打开原帖链接</a>
        )}
        <div className="market-raw-block">
          <span>原文内容</span>
          <p>{signal.rawText || signal.detail}</p>
        </div>
        <div className="market-derivation-list">
          {derivationSteps.map(item => (
            <div key={item.label} className="market-derivation-step">
              <span>{item.label}</span>
              <p>{item.value}</p>
            </div>
          ))}
        </div>
        <div className="market-evidence-evidence">
          {(signal.evidence || []).map(item => (
            <span key={item} className="tag mono">{item}</span>
          ))}
        </div>
      </div>
    </div>
  );
};

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 (
      <div className="card market-learning-card">
        <div className="card-header">
          <h3>学习地图 · Learning Map</h3>
          <span className="count">品牌记忆推导中</span>
        </div>
        <div className="card-body">
          <div className="skeleton-line"></div>
          <div className="skeleton-line short"></div>
        </div>
      </div>
    );
  }
  if (!learningMap) return null;
  return (
    <>
    <div className="card market-learning-card">
      <div className="card-header">
        <h3>学习地图 · Brand Memory Learning Map</h3>
        <span className="count">{profile.product_stage || 'stage'} · {profile.launch_window || 'launch window'}</span>
      </div>
      <div className="card-body">
        <div className="market-learning-principle">
          <b>{profile.brand_name || 'MONTX'} 当前学习任务</b>
          <p>{learningMap.learning_principle || '按品牌当前战略问题选择学习对象。'}</p>
          <div className="market-signal-themes">
            {(profile.strategic_tasks || []).slice(0, 5).map(item => <span key={item} className="tag mono">{item}</span>)}
          </div>
        </div>
        <div className="market-learning-grid">
          {priorities.map(priority => (
            <div key={priority.priority} className={`market-learning-priority ${priority.priority.toLowerCase()}`}>
              <div className="market-learning-head">
                <span className="tag accent mono">{priority.priority}</span>
                <b>{marketLearningPriorityText[priority.priority] || priority.title}</b>
              </div>
              <p>{priority.intent?.why || priority.description}</p>
              <div className="market-learning-cases">
                {(priority.cases || []).slice(0, 3).map(item => (
                  <button key={item.case_id} type="button" className="market-learning-case" onClick={() => openCaseStudy(item)}>
                    <div>
                      <b>{item.case_brand}</b>
                      <span>{item.case_product}</span>
                    </div>
                    <small>{item.strategy_usable_insight}</small>
                    <div className="market-signal-themes">
                      {(item.relationship_type || []).slice(0, 3).map(type => <span key={type} className="tag mono">{marketLearningRelationText[type] || type}</span>)}
                    </div>
                  </button>
                ))}
              </div>
            </div>
          ))}
        </div>
        <div className="market-learning-notes">
          {strategyNotes.slice(0, 4).map(note => (
            <div key={note} className="market-learning-note">
              <Icon name="check" className="icon sm"/>
              <span>{note}</span>
            </div>
          ))}
        </div>
      </div>
    </div>
    {selectedCase && (
      <MarketCaseStudyDrawer
        caseItem={selectedCase}
        caseStudy={caseState.caseStudy}
        loading={caseState.loading}
        error={caseState.error}
        onClose={() => 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 (
    <div className="market-case-overlay" role="dialog" aria-modal="true">
      <div className="market-case-drawer">
        <div className="market-case-header">
          <div>
            <span className="tag accent mono">{caseItem?.case_id || caseStudy?.case_id}</span>
            <h3>{caseItem?.case_brand || caseStudy?.brand} · 案例详情</h3>
            <p>{caseItem?.case_product || '长周期预热案例'}</p>
          </div>
          <button className="btn icon-only" onClick={onClose} aria-label="关闭案例详情"><Icon name="x" className="icon sm"/></button>
        </div>
        {loading ? (
          <div className="market-case-body">
            <div className="skeleton-line"></div>
            <div className="skeleton-line short"></div>
          </div>
        ) : error ? (
          <div className="market-case-body">
            <div className="callout warn">
              {error}
              <small>当前案例可能还没有生成结构化 case study。先运行对应分析脚本后再打开。</small>
            </div>
          </div>
        ) : (
          <div className="market-case-body">
            <section className="market-case-summary">
              <b>{caseStudy?.diagnostic_summary?.one_sentence || '案例结论待补充'}</b>
              <p>{caseStudy?.diagnostic_summary?.why_it_matters_to_montx}</p>
            </section>
            <div className="market-case-stats">
              <MarketCaseStat label="帖子" value={scope.posts || 0}/>
              <MarketCaseStat label="评论" value={scope.comments || 0}/>
              <MarketCaseStat label="时间范围" value={`${scope.first_post_date || '-'} / ${scope.last_post_date || '-'}`}/>
            </div>
            <section>
              <h4>内容生命周期</h4>
              <div className="market-case-bars">
                {(breakdown.lifecycle_stages || []).slice(0, 6).map(item => <MarketCaseBar key={item.key} item={item} total={scope.posts}/>)}
              </div>
            </section>
            <section>
              <h4>车型拆分</h4>
              <div className="market-signal-themes">
                {(breakdown.model_focus || []).map(item => <span key={item.key} className="tag mono">{item.key} · {item.count}</span>)}
              </div>
            </section>
            <section>
              <h4>可迁移内容范式</h4>
              <div className="market-case-list">
                {patterns.map(item => (
                  <article key={item.id}>
                    <b>{item.name}</b>
                    <p>{item.montx_transfer}</p>
                  </article>
                ))}
              </div>
            </section>
            <section>
              <h4>评论洞察</h4>
              <div className="market-case-list compact">
                {audience.slice(0, 5).map(item => (
                  <article key={item.theme_id}>
                    <b>{item.label} · {item.count}</b>
                    <p>{item.insight}</p>
                  </article>
                ))}
              </div>
            </section>
            <section>
              <h4>风险模式</h4>
              <div className="market-case-list compact">
                {risks.map(item => (
                  <article key={item.id} className="warn">
                    <b>{item.risk}</b>
                    <p>{item.implication}</p>
                  </article>
                ))}
              </div>
            </section>
            <section>
              <h4>沉淀给 MONTX 的启示</h4>
              <div className="market-case-checks">
                {implications.map(item => (
                  <div key={item}><Icon name="check" className="icon sm"/><span>{item}</span></div>
                ))}
              </div>
            </section>
          </div>
        )}
      </div>
    </div>
  );
};

const MarketCaseStat = ({ label, value }) => (
  <div className="market-case-stat">
    <span>{label}</span>
    <b>{value}</b>
  </div>
);

const MarketCaseBar = ({ item, total }) => {
  const width = total ? Math.max(6, Math.round((item.count / total) * 100)) : 0;
  return (
    <div className="market-case-bar">
      <div><span>{item.key}</span><b>{item.count}</b></div>
      <i><em style={{width:`${width}%`}}></em></i>
    </div>
  );
};

const MarketSignalDetail = ({ signal, written, onWrite }) => {
  if (!signal) return null;
  return (
    <div className="card">
      <div className="card-header">
        <h3>信号详情 · 洞察记录</h3>
        <span className="count">{signal.brandName || signal.product}</span>
      </div>
      <div className="card-body">
        <div className="callout" style={{fontSize:14}}>
          {signal.action}
          <small>记录方 · {signal.owner} · 置信度 {(signal.confidence * 100).toFixed(0)}%</small>
        </div>
        <div className="grid g-3" style={{marginTop:12}}>
          <MiniStat label="优先级" value={signal.score} sub={signal.score >= 85 ? '高置信' : '观察'}/>
          <MiniStat label="证据量" value={signal.volume} sub={signal.freshness === 1 ? '24h' : `${signal.freshness}d`}/>
          <MiniStat label="情绪" value={`${signal.sentiment > 0 ? '+' : ''}${signal.sentiment}`} sub={marketTypeText[signal.type] || signal.type}/>
          <MiniStat label="内容方向" value={signal.focusLabel || '未明确'} sub={(signal.models || []).slice(0, 2).join(' / ') || signal.sourceContentType || '待补充'}/>
        </div>
        <div style={{marginTop:12, display:'grid', gap:7}}>
          {(signal.evidence || []).map(item => (
            <div key={item} style={{display:'flex', gap:8, alignItems:'center', padding:'7px 8px', border:'1px solid var(--hairline)', borderRadius:6, background:'var(--bg-2)'}}>
              <Icon name="check" className="icon sm" style={{color:'var(--accent)'}}/>
              <span style={{fontSize:12, color:'var(--ink-2)'}}>{item}</span>
            </div>
          ))}
        </div>
        <button className={`btn ${written ? '' : 'primary'}`} style={{width:'100%', justifyContent:'center', marginTop:12}} onClick={onWrite}>
          <Icon name={written ? 'check' : 'spark'} className="icon sm"/>
          {written ? '已标记已读' : '标记为已读'}
        </button>
      </div>
    </div>
  );
};

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 (
  <div className="card">
    <div className="card-header">
      <h3>外部感知对象 · Four Signal Objects</h3>
      <span className="count">PRD 3.2</span>
    </div>
    <div className="card-body market-object-grid">
      {MARKET_SENSING_OBJECTS.map(item => (
        <div key={item.id} className={`market-object-card ${item.id}`}>
          <div className="market-object-head">
            <b>{item.title}</b>
            <span>{counts[item.id] || item.count}</span>
          </div>
          <p>{item.text}</p>
          <div className="market-signal-themes">
            {item.examples.map(example => <span key={example} className="tag mono">{example}</span>)}
          </div>
        </div>
      ))}
    </div>
  </div>
  );
};

const MarketMemorySummary = ({ outputs = MARKET_OUTPUTS, loading = false }) => (
  <div className="card">
    <div className="card-header">
      <h3>市场记忆摘要 · Query Outputs</h3>
      <span className="count">{outputs.length} 条可查询记录</span>
      <div className="actions">
        <span className={`chip ${loading ? 'info' : 'ghost'} mono`} style={{fontSize:10}}>{loading ? '同步中' : '按需读取'}</span>
      </div>
    </div>
    <div className="card-body market-output-list">
      {outputs.map(item => (
        <div key={item.id} className={`market-output-item ${item.status}`}>
          <div>
            <span className="tag mono">{marketCadenceText[item.cadence] || item.cadence}</span>
            <b>{item.title}</b>
          </div>
          <p>{item.text}</p>
          <span className={`chip compact ${item.status === 'urgent' ? 'danger' : item.status === 'ready' ? 'accent' : 'info'}`}>
            <span className="dot"></span>{marketStatusText[item.status] || item.status}
          </span>
        </div>
      ))}
    </div>
  </div>
);

const SignalCounter = ({ label, value, delta, color }) => (
  <div className="metric">
    <div className="label">{label}</div>
    <div className="value" style={{color}}>{value}</div>
    <div className="delta" style={{color: 'var(--ink-3)'}}>{delta}</div>
  </div>
);

const SensingChart = () => (
  <svg viewBox="0 0 400 180" style={{width:'100%', height:180}}>
    {[20,50,80,110,140].map(y => <line key={y} x1="0" x2="400" y1={y} y2={y} stroke="var(--hairline)" strokeDasharray="2 3"/>)}
    <polyline points="0,108 30,104 60,106 90,98 120,96 150,92 180,90 210,84 240,82 270,76 300,74 330,70 360,66 400,64" fill="none" stroke="var(--ink-3)" strokeWidth="1" strokeDasharray="4 3"/>
    <polyline points="0,96 30,94 60,88 90,84 120,86 150,78 180,72 210,70 240,60 270,58 300,50 330,48 360,42 400,38" fill="none" stroke="oklch(0.55 0.18 25)" strokeWidth="1.5"/>
    <path d="M 0,122 L 30,118 L 60,111 L 90,105 L 120,96 L 150,90 L 180,82 L 210,76 L 240,62 L 270,54 L 300,48 L 330,39 L 360,31 L 400,26 L 400,180 L 0,180 Z" fill="oklch(0.45 0.12 155 / 0.10)"/>
    <polyline points="0,122 30,118 60,111 90,105 120,96 150,90 180,82 210,76 240,62 270,54 300,48 330,39 360,31 400,26" fill="none" stroke="oklch(0.45 0.12 155)" strokeWidth="2"/>
    {[[0,122],[30,118],[60,111],[90,105],[120,96],[150,90],[180,82],[210,76],[240,62],[270,54],[300,48],[330,39],[360,31],[400,26]].map(([x,y], i) => (
      <circle key={i} cx={x} cy={y} r="2" fill="oklch(0.45 0.12 155)"/>
    ))}
  </svg>
);

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 (
    <div style={{overflowX:'auto'}}>
      <table style={{borderCollapse:'separate', borderSpacing:2, fontSize:10, fontFamily:'var(--f-mono)'}}>
        <thead>
          <tr>
            <th></th>
            {cols.map(c => <th key={c} style={{padding:4, color:'var(--ink-4)', fontWeight:400, minWidth:44}}>{c}</th>)}
          </tr>
        </thead>
        <tbody>
          {rows.map((r, i) => (
            <tr key={r}>
              <td style={{padding:'4px 8px', color:'var(--ink-3)', textAlign:'right'}}>{r}</td>
              {data[i].map((v, j) => (
                <td key={j} style={{
                  width:44, height:28,
                  background: `oklch(${0.98 - v*0.04} ${v*0.015} 155)`,
                  textAlign:'center',
                  color: v > 6 ? 'white' : 'var(--ink-2)',
                  borderRadius:3,
                }}>{v}</td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
};

const MarketSourceCoverage = ({ competitors = [] }) => (
  <div className="card">
    <div className="card-header"><h3>外部数据源覆盖 · Source Coverage</h3><span className="count">{competitors.length} 个候选竞品</span></div>
    <div className="card-body" style={{display:'grid', gap:9}}>
      {[
        ['外部自然声量', '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]) => (
        <div key={name} className="market-source-row">
          <div>
            <b>{name}</b>
            <span>{desc}</span>
          </div>
          <div className={`bar ${status === 'ok' ? 'accent' : ''}`}><span style={{width:`${pct}%`}}></span></div>
          <small>{pct}%</small>
        </div>
      ))}
    </div>
  </div>
);

const SensingPipeline = () => (
  <div className="card">
    <div className="card-header">
      <h3>感知链路 · From Signal To Memory</h3>
      <span className="count">查询服务</span>
    </div>
    <div className="card-body">
      <div className="market-pipeline">
        {[
          ['采集', '平台/竞品/评论进入信号池', 'done'],
          ['标准化', '实体、车型、平台、证据与置信度归一', 'done'],
          ['判定', '信号类型、竞品关系、风险边界归因', 'active'],
          ['沉淀', '生成 Context Pack / Pattern / Case Study', 'active'],
          ['服务', '下游模块按任务主动查询，不主动推送', 'pending'],
        ].map(([title, text, status], index) => (
          <div key={title} className={`market-pipeline-step ${status}`}>
            <span>{String(index + 1).padStart(2, '0')}</span>
            <b>{title}</b>
            <p>{text}</p>
          </div>
        ))}
      </div>
    </div>
  </div>
);

const MiniStat = ({ label, value, sub }) => (
  <div style={{padding:8, background:'var(--bg-2)', borderRadius:6, border:'1px solid var(--hairline)'}}>
    <div style={{fontFamily:'var(--f-mono)', fontSize:9, color:'var(--ink-4)', textTransform:'uppercase'}}>{label}</div>
    <div style={{fontSize:16, fontFamily:'var(--f-serif)', marginTop:2}}>{value}</div>
    <div style={{fontSize:10, color:'var(--ink-4)', fontFamily:'var(--f-mono)'}}>{sub}</div>
  </div>
);

Object.assign(window, { Dashboard, BrandMemory, MarketSensing });
