Add explain analysis workflow and UI

This commit is contained in:
2026-03-16 22:28:41 +08:00
parent 3a5558b576
commit 1f5ee3698e
49 changed files with 8888 additions and 1476 deletions

View File

@@ -0,0 +1,308 @@
import React from 'react';
import { formatDateTime } from '../../utils/formatters';
function categoryLabel(value) {
const normalized = String(value || '').trim().toLowerCase();
const labels = {
market: '市场交易',
policy: '政策监管',
earnings: '业绩财报',
product_tech: '产品技术',
competition: '竞争格局',
management: '管理层动态',
};
return labels[normalized] || value || '';
}
function relevanceLabel(value) {
const normalized = String(value || '').trim().toLowerCase();
const labels = {
high: '高相关',
medium: '中相关',
low: '低相关',
relevant: '高相关',
};
return labels[normalized] || value || '';
}
function analysisSourceLabel(value) {
const normalized = String(value || '').trim().toLowerCase();
if (normalized === 'llm') return 'LLM分析';
if (normalized === 'local') return '规则分析';
return value || '';
}
function sentimentStyle(sentiment) {
const normalized = String(sentiment || '').trim().toLowerCase();
if (normalized === 'positive') {
return { border: '#16a34a', background: '#f0fdf4', color: '#166534', label: '利多' };
}
if (normalized === 'negative') {
return { border: '#dc2626', background: '#fef2f2', color: '#991b1b', label: '利空' };
}
return { border: '#6b7280', background: '#f9fafb', color: '#4b5563', label: '中性' };
}
export default function ExplainNewsSection({
newsSnapshot,
visibleNewsByCategory,
visibleNews,
activeNewsCategory,
onSelectNewsCategory,
activeNewsSentiment,
onSelectNewsSentiment,
newsCategories,
tickerNews,
isOpen,
onToggle,
}) {
return (
<div className="section">
<div className="section-header">
<h2 className="section-title">新闻面板</h2>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
<div style={{ fontSize: 11, color: '#666666' }}>
{newsSnapshot?.source ? `最近 ${visibleNewsByCategory.length} 条 · ${newsSnapshot.source}` : `最近 ${visibleNewsByCategory.length} 条真实新闻`}
</div>
<button
onClick={onToggle}
style={{
border: '1px solid #111111',
background: isOpen ? '#111111' : '#ffffff',
color: isOpen ? '#ffffff' : '#111111',
padding: '7px 10px',
fontFamily: 'inherit',
fontSize: 11,
fontWeight: 700,
cursor: 'pointer'
}}
>
{isOpen ? '收起新闻面板' : '展开新闻面板'}
</button>
</div>
</div>
{!isOpen ? (
<div className="empty-state">新闻面板已收起需要时再展开查看分类情绪和新闻卡片</div>
) : (
<>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginBottom: 14 }}>
<button
onClick={() => onSelectNewsCategory('all')}
style={{
border: '1px solid #111111',
background: activeNewsCategory === 'all' ? '#111111' : '#ffffff',
color: activeNewsCategory === 'all' ? '#ffffff' : '#111111',
padding: '7px 10px',
fontFamily: 'inherit',
fontSize: 11,
fontWeight: 700,
cursor: 'pointer'
}}
>
全部 {visibleNews.length}
</button>
{Object.entries(newsCategories)
.filter(([, meta]) => Number(meta?.count || 0) > 0)
.map(([key, meta]) => {
const isActive = activeNewsCategory === key;
const pos = Number(meta?.positive_ids?.length || 0);
const neg = Number(meta?.negative_ids?.length || 0);
return (
<button
key={key}
onClick={() => onSelectNewsCategory(key)}
style={{
border: '1px solid #2563eb',
background: isActive ? '#2563eb' : '#ffffff',
color: isActive ? '#ffffff' : '#2563eb',
padding: '7px 10px',
fontFamily: 'inherit',
fontSize: 11,
fontWeight: 700,
cursor: 'pointer'
}}
>
{categoryLabel(meta.label || key)} {meta.count}{pos || neg ? ` · +${pos}/-${neg}` : ''}
</button>
);
})}
</div>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginBottom: 14 }}>
{[
{ key: 'all', label: '全部情绪' },
{ key: 'positive', label: '利多' },
{ key: 'negative', label: '利空' },
{ key: 'neutral', label: '中性' }
].map((item) => {
const isActive = activeNewsSentiment === item.key;
return (
<button
key={item.key}
onClick={() => onSelectNewsSentiment(item.key)}
style={{
border: '1px solid #111111',
background: isActive ? '#111111' : '#ffffff',
color: isActive ? '#ffffff' : '#111111',
padding: '6px 10px',
fontFamily: 'inherit',
fontSize: 11,
fontWeight: 700,
cursor: 'pointer'
}}
>
{item.label}
</button>
);
})}
</div>
{tickerNews.length === 0 ? (
<div className="empty-state">当前数据源没有返回相关新闻</div>
) : (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(320px, 1fr))', gap: 16 }}>
{visibleNewsByCategory.map((item) => (
<div
key={item.id}
style={{
border: '1px solid #000000',
background: '#ffffff',
padding: 14,
minHeight: 180
}}
>
{(() => {
const sentimentMeta = sentimentStyle(item.sentiment);
return (
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginBottom: 10 }}>
<span style={{
display: 'inline-flex',
padding: '2px 6px',
border: `1px solid ${sentimentMeta.border}`,
background: sentimentMeta.background,
color: sentimentMeta.color,
fontSize: 10,
fontWeight: 700
}}>
{sentimentMeta.label}
</span>
{item.relevance ? (
<span style={{
display: 'inline-flex',
padding: '2px 6px',
border: '1px solid #111111',
color: '#111111',
fontSize: 10,
fontWeight: 700
}}>
{relevanceLabel(item.relevance)}
</span>
) : null}
{item.analysisSource ? (
<span style={{
display: 'inline-flex',
padding: '2px 6px',
border: '1px solid #6b7280',
color: '#4b5563',
fontSize: 10,
fontWeight: 700
}}>
{analysisSourceLabel(item.analysisSource)}
</span>
) : null}
{item.analysisModelLabel ? (
<span style={{
display: 'inline-flex',
padding: '2px 6px',
border: '1px solid #9ca3af',
color: '#374151',
fontSize: 10,
fontWeight: 700
}}>
{item.analysisModelLabel}
</span>
) : null}
{typeof item.retT0 === 'number' ? (
<span style={{ fontSize: 10, color: item.retT0 >= 0 ? '#15803d' : '#b91c1c', fontWeight: 700 }}>
T0 {item.retT0 >= 0 ? '+' : ''}{(item.retT0 * 100).toFixed(2)}%
</span>
) : null}
</div>
);
})()}
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 12, marginBottom: 8 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
{item.category ? (
<span style={{
display: 'inline-flex',
padding: '2px 6px',
border: '1px solid #111111',
color: '#111111',
fontSize: 10,
fontWeight: 700
}}>
{categoryLabel(item.category)}
</span>
) : null}
<strong style={{ fontSize: 13 }}>{item.title}</strong>
</div>
<span style={{ fontSize: 10, color: '#666666', whiteSpace: 'nowrap' }}>
{formatDateTime(item.date)}
</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10, flexWrap: 'wrap' }}>
<span style={{ fontSize: 10, color: '#666666', textTransform: 'uppercase', letterSpacing: 0.6 }}>
{item.source}
</span>
{item.related ? (
<span style={{ fontSize: 10, color: '#666666' }}>
关联: {item.related}
</span>
) : null}
</div>
<div style={{ fontSize: 12, lineHeight: 1.7, color: '#000000', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
{item.summary || '该新闻没有可用摘要。'}
</div>
{item.keyDiscussion ? (
<div style={{ marginTop: 10, fontSize: 11, lineHeight: 1.7, color: '#374151' }}>
<strong>核心讨论:</strong> {item.keyDiscussion}
</div>
) : null}
{item.reasonGrowth ? (
<div style={{ marginTop: 8, fontSize: 11, lineHeight: 1.7, color: '#166534' }}>
<strong>利多逻辑:</strong> {item.reasonGrowth}
</div>
) : null}
{item.reasonDecrease ? (
<div style={{ marginTop: 8, fontSize: 11, lineHeight: 1.7, color: '#991b1b' }}>
<strong>利空逻辑:</strong> {item.reasonDecrease}
</div>
) : null}
{item.url ? (
<div style={{ marginTop: 12 }}>
<a
href={item.url}
target="_blank"
rel="noreferrer"
style={{ fontSize: 11, fontWeight: 700, color: '#111111', textDecoration: 'underline' }}
>
查看原文
</a>
</div>
) : null}
</div>
))}
</div>
)}
</>
)}
</div>
);
}