Files
deerflow-code/web-code/pages/LightAppRun.tsx
T
2026-05-28 18:29:37 +08:00

997 lines
41 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect } from 'react';
import { useTheme } from '../contexts/ThemeContext';
import {
ArrowLeft,
Save,
Download,
Send,
RotateCcw,
PlayCircle,
CheckCircle2,
Circle,
Loader2,
ChevronDown,
ChevronRight,
FileText,
Link2,
Upload,
Clock,
AlertTriangle,
Sparkles,
Users,
Copy,
CheckCheck,
RefreshCw,
Database,
BarChart3,
PieChart,
LineChart,
Table,
User,
MessageSquare,
Network,
Target,
BookOpen,
Calendar,
Search,
} from 'lucide-react';
import { LightAppType } from './LightAppSquare';
// 应用配置接口
interface AppConfig {
id: LightAppType;
name: string;
description: string;
icon: React.ElementType;
iconColor: string;
bgColor: string;
inputs: {
name: string;
label: string;
placeholder: string;
type: 'text' | 'textarea' | 'link' | 'upload';
required: boolean;
}[];
params: {
name: string;
label: string;
options: { value: string; label: string }[];
default: string;
}[];
dimensions: {
name: string;
label: string;
default: boolean;
}[];
steps: string[];
previewFields: { key: string; label: string }[];
}
interface LightAppRunProps {
onNavigate: (page: string, appId?: string) => void;
appId?: LightAppType;
}
// 应用配置数据
const appConfigs: Record<LightAppType, AppConfig> = {
'info-ty': {
id: 'info-ty',
name: '信息TY',
description: '围绕单条或多条信息进行TY向分析',
icon: FileText,
iconColor: 'text-blue-500',
bgColor: 'bg-blue-500/10',
inputs: [
{ name: 'mainInput', label: '信息文本', placeholder: '请输入信息内容...', type: 'textarea', required: true },
{ name: 'link', label: '信息链接', placeholder: '可选,粘贴信息链接', type: 'link', required: false },
{ name: 'materials', label: '上传材料', placeholder: '支持 txt, pdf, docx', type: 'upload', required: false },
],
params: [
{ name: 'timeRange', label: '时间范围', options: [
{ value: '7d', label: '近7天' },
{ value: '30d', label: '近30天' },
{ value: '90d', label: '近90天' },
{ value: 'all', label: '全部' },
], default: '30d' },
{ name: 'dataRange', label: '数据范围', options: [
{ value: 'all', label: '全部' },
{ value: '国内', label: '国内' },
{ value: '境外', label: '境外' },
], default: 'all' },
{ name: 'depth', label: '输出深度', options: [
{ value: 'brief', label: '简略' },
{ value: 'standard', label: '标准' },
{ value: 'detailed', label: '详细' },
], default: 'standard' },
],
dimensions: [
{ name: 'basic', label: '基础分析', default: true },
{ name: 'risk', label: '风险提示', default: true },
{ name: 'summary', label: '摘要结论', default: true },
],
steps: ['已识别输入对象', '已获取数据', '已完成核心分析', '已生成结果'],
previewFields: [
{ key: 'title', label: '信息标题' },
{ key: 'time', label: '发布时间' },
{ key: 'source', label: '信息来源' },
],
},
'info-pg': {
id: 'info-pg',
name: '信息PG',
description: '围绕单条或多条信息进行PG向分析',
icon: User,
iconColor: 'text-cyan-500',
bgColor: 'bg-cyan-500/10',
inputs: [
{ name: 'mainInput', label: '信息文本', placeholder: '请输入信息内容...', type: 'textarea', required: true },
{ name: 'materials', label: '上传材料', placeholder: '支持 txt, pdf, docx', type: 'upload', required: false },
],
params: [
{ name: 'timeRange', label: '时间范围', options: [
{ value: '30d', label: '近30天' },
{ value: '90d', label: '近90天' },
{ value: '180d', label: '近半年' },
{ value: 'all', label: '全部' },
], default: '30d' },
{ name: 'count', label: '抓取条数', options: [
{ value: '50', label: '50条' },
{ value: '100', label: '100条' },
{ value: '200', label: '200条' },
{ value: '500', label: '500条' },
], default: '100' },
],
dimensions: [
{ name: 'profile', label: '画像分析', default: true },
{ name: 'tendency', label: '内容倾向', default: true },
{ name: 'behavior', label: '行为特征', default: true },
],
steps: ['已识别输入对象', '已获取数据', '已完成画像分析', '已生成结果'],
previewFields: [
{ key: 'title', label: '信息标题' },
{ key: 'author', label: '发布者' },
{ key: 'type', label: '内容类型' },
],
},
'info-spread': {
id: 'info-spread',
name: '信息传播分析',
description: '分析信息传播路径、传播范围与传播表现',
icon: Network,
iconColor: 'text-teal-500',
bgColor: 'bg-teal-500/10',
inputs: [
{ name: 'link', label: '信息链接', placeholder: '请粘贴信息链接', type: 'link', required: true },
{ name: 'text', label: '信息文本', placeholder: '可选,补充信息文本', type: 'textarea', required: false },
],
params: [
{ name: 'scope', label: '传播范围', options: [
{ value: 'all', label: '全部' },
{ value: 'domestic', label: '国内' },
{ value: 'overseas', label: '境外' },
], default: 'all' },
{ name: 'timeRange', label: '时间范围', options: [
{ value: '7d', label: '近7天' },
{ value: '30d', label: '近30天' },
{ value: '90d', label: '近90天' },
], default: '7d' },
],
dimensions: [
{ name: 'path', label: '传播路径', default: true },
{ name: 'nodes', label: '传播节点', default: true },
{ name: 'performance', label: '传播表现', default: true },
{ name: 'impact', label: '影响摘要', default: true },
],
steps: ['已识别信息', '已获取传播数据', '已完成传播路径分析', '已生成结果'],
previewFields: [
{ key: 'title', label: '信息标题' },
{ key: 'originalPlatform', label: '原始平台' },
{ key: 'totalSpread', label: '总传播量' },
],
},
'account-pg': {
id: 'account-pg',
name: '账号PG',
description: '围绕账号内容与行为特征进行PG分析',
icon: Users,
iconColor: 'text-purple-500',
bgColor: 'bg-purple-500/10',
inputs: [
{ name: 'accountId', label: '账号ID/链接', placeholder: '请输入账号ID或粘贴账号链接', type: 'text', required: true },
{ name: 'materials', label: '上传内容包', placeholder: '支持 json, csv, zip', type: 'upload', required: false },
],
params: [
{ name: 'timeRange', label: '时间范围', options: [
{ value: '30d', label: '近30天' },
{ value: '90d', label: '近90天' },
{ value: '180d', label: '近半年' },
{ value: 'all', label: '全部' },
], default: '30d' },
{ name: 'count', label: '抓取条数', options: [
{ value: '50', label: '50条' },
{ value: '100', label: '100条' },
{ value: '200', label: '200条' },
{ value: '500', label: '500条' },
], default: '100' },
],
dimensions: [
{ name: 'profile', label: '账号画像', default: true },
{ name: 'tendency', label: '内容倾向', default: true },
{ name: 'behavior', label: '行为特征', default: true },
{ name: 'impact', label: '影响判断', default: true },
],
steps: ['已定位账号', '已抓取内容', '已完成内容倾向分析', '已完成账号画像分析', '已生成结果'],
previewFields: [
{ key: 'accountName', label: '账号名称' },
{ key: 'accountType', label: '账号类型' },
{ key: 'followers', label: '粉丝数' },
],
},
'sentiment': {
id: 'sentiment',
name: '三情分析',
description: '对事件进行舆情、社情、民情综合分析',
icon: MessageSquare,
iconColor: 'text-orange-500',
bgColor: 'bg-orange-500/10',
inputs: [
{ name: 'eventName', label: '事件名称', placeholder: '请输入事件名称', type: 'text', required: true },
{ name: 'eventLink', label: '事件链接', placeholder: '可选,粘贴事件相关链接', type: 'link', required: false },
{ name: 'materials', label: '上传材料', placeholder: '支持上传相关素材', type: 'upload', required: false },
],
params: [
{ name: 'timeRange', label: '时间范围', options: [
{ value: '7d', label: '近7天' },
{ value: '30d', label: '近30天' },
{ value: '90d', label: '近90天' },
], default: '7d' },
{ name: 'dataRange', label: '数据范围', options: [
{ value: 'all', label: '全部' },
{ value: '国内', label: '国内' },
{ value: '境外', label: '境外' },
], default: 'all' },
],
dimensions: [
{ name: 'publicSentiment', label: '舆情分析', default: true },
{ name: 'socialSentiment', label: '社情分析', default: true },
{ name: 'publicOpinion', label: '民情分析', default: true },
{ name: 'riskAlert', label: '风险提示', default: true },
],
steps: ['已识别事件', '已聚合相关内容', '已完成舆情分析', '已完成社情分析', '已完成民情分析', '已生成结果'],
previewFields: [
{ key: 'eventName', label: '事件名称' },
{ key: 'eventTime', label: '事件时间' },
{ key: 'relatedCount', label: '相关内容数' },
],
},
'event': {
id: 'event',
name: '事件分析',
description: '围绕某个事件进行整体分析',
icon: Target,
iconColor: 'text-red-500',
bgColor: 'bg-red-500/10',
inputs: [
{ name: 'eventName', label: '事件名称', placeholder: '请输入事件名称', type: 'text', required: true },
{ name: 'eventDesc', label: '事件说明', placeholder: '请补充事件相关说明', type: 'textarea', required: false },
{ name: 'materials', label: '上传材料', placeholder: '支持上传事件相关材料', type: 'upload', required: false },
],
params: [
{ name: 'timeRange', label: '时间范围', options: [
{ value: '30d', label: '近30天' },
{ value: '90d', label: '近90天' },
{ value: '180d', label: '近半年' },
{ value: 'all', label: '全部' },
], default: '30d' },
{ name: 'depth', label: '输出深度', options: [
{ value: 'brief', label: '简略' },
{ value: 'standard', label: '标准' },
{ value: 'detailed', label: '详细' },
], default: 'standard' },
],
dimensions: [
{ name: 'context', label: '事件脉络', default: true },
{ name: 'keyPoints', label: '关键节点', default: true },
{ name: 'risk', label: '风险判断', default: true },
{ name: 'suggestions', label: '初步建议', default: true },
],
steps: ['已识别事件', '已整理事件材料', '已完成脉络分析', '已完成风险判断', '已生成结果'],
previewFields: [
{ key: 'eventName', label: '事件名称' },
{ key: 'eventStage', label: '所处阶段' },
{ key: 'keyPlayers', label: '关键参与者' },
],
},
'topic': {
id: 'topic',
name: '专题分析',
description: '对某专题做集中分析',
icon: BookOpen,
iconColor: 'text-green-500',
bgColor: 'bg-green-500/10',
inputs: [
{ name: 'topicName', label: '专题名称', placeholder: '请输入专题名称', type: 'text', required: true },
{ name: 'materials', label: '上传材料', placeholder: '支持上传专题相关文本集合', type: 'upload', required: false },
],
params: [
{ name: 'depth', label: '分析深度', options: [
{ value: 'surface', label: '表面' },
{ value: 'standard', label: '标准' },
{ value: 'deep', label: '深度' },
], default: 'standard' },
{ name: 'sourceRange', label: '来源范围', options: [
{ value: 'all', label: '全部' },
{ value: '国内', label: '国内' },
{ value: '境外', label: '境外' },
], default: 'all' },
],
dimensions: [
{ name: 'structure', label: '专题结构', default: true },
{ name: 'coreIssues', label: '核心问题', default: true },
{ name: 'viewpoints', label: '观点聚合', default: true },
{ name: 'conclusions', label: '结论建议', default: true },
],
steps: ['已识别专题', '已获取相关材料', '已完成结构分析', '已聚合核心观点', '已生成结果'],
previewFields: [
{ key: 'topicName', label: '专题名称' },
{ key: 'relatedDocs', label: '相关文档' },
{ key: 'coverage', label: '覆盖范围' },
],
},
};
type ResultTab = 'overview' | 'details' | 'raw';
const LightAppRun: React.FC<LightAppRunProps> = ({ onNavigate, appId }) => {
const { theme } = useTheme();
const isDark = theme === 'dark';
const [currentAppId, setCurrentAppId] = useState<LightAppType>(appId || 'sentiment');
const [resultTab, setResultTab] = useState<ResultTab>('overview');
const [isRunning, setIsRunning] = useState(false);
const [currentStep, setCurrentStep] = useState(0);
const [copied, setCopied] = useState(false);
const [formData, setFormData] = useState<Record<string, string>>({});
const [params, setParams] = useState<Record<string, string>>({});
const [dimensions, setDimensions] = useState<Record<string, boolean>>({});
const [hasResult, setHasResult] = useState(false);
const [result, setResult] = useState<any>(null);
const config = appConfigs[currentAppId];
const Icon = config.icon;
// 初始化表单数据
useEffect(() => {
console.log('当前应用:', currentAppId);
if (config) {
const initialForm: Record<string, string> = {};
config.inputs.forEach(input => {
initialForm[input.name] = '';
});
setFormData(initialForm);
const initialParams: Record<string, string> = {};
config.params.forEach(param => {
initialParams[param.name] = param.default;
});
setParams(initialParams);
const initialDims: Record<string, boolean> = {};
config.dimensions.forEach(dim => {
initialDims[dim.name] = dim.default;
});
setDimensions(initialDims);
}
}, [currentAppId]);
// 执行分析
const handleRun = async () => {
setIsRunning(true);
setCurrentStep(0);
setHasResult(false);
// 模拟执行步骤
for (let i = 0; i < config.steps.length; i++) {
await new Promise(resolve => setTimeout(resolve, 800));
setCurrentStep(i + 1);
}
// 生成结果
await new Promise(resolve => setTimeout(resolve, 500));
setResult(generateMockResult());
setHasResult(true);
setIsRunning(false);
};
// 生成模拟结果
const generateMockResult = () => {
switch (currentAppId) {
case 'sentiment':
return {
overview: {
overallSentiment: '中偏负',
mainConcerns: ['活动形式', '品牌调性', '执行成本'],
riskAlert: '讨论热度上升,负向占比增加',
sentimentTrend: '下行',
},
details: {
publicSentiment: '舆情方面,主要关注点是活动本身的创新性和参与体验。部分用户对活动细节提出质疑,整体讨论呈现多元化特征。',
socialSentiment: '社情方面,该事件引发了关于活动公平性的讨论,社交媒体上相关话题讨论量显著上升。',
publicOpinion: '民情方面,公众对该活动的看法分化明显,支持和反对声音并存,需关注后续舆情走向。',
riskAssessment: '当前风险等级:中。建议持续关注舆情变化,准备应对预案。',
},
raw: [
{ type: '正面评论', content: '活动创意很好,期待参与', count: 234 },
{ type: '中性评论', content: '等待活动正式开始', count: 567 },
{ type: '负面评论', content: '对活动细节表示质疑', count: 189 },
],
};
case 'event':
return {
overview: {
eventStage: '发酵期',
keyNodes: '活动公布期,关注度持续上升',
riskLevel: '中',
suggestion: '建议加强信息透明度,及时回应公众关切',
},
details: {
context: '该事件起因于...,经过...,目前处于...阶段。',
keyPoints: ['关键节点一:...', '关键节点二:...', '关键节点三:...'],
riskJudgment: '风险可控,但需关注...',
suggestions: '1. ... 2. ... 3. ...',
},
raw: [
{ time: '2026-03-18', title: '事件起因', content: '...' },
{ time: '2026-03-19', title: '初步发酵', content: '...' },
{ time: '2026-03-20', title: '关注度上升', content: '...' },
],
};
case 'account-pg':
return {
overview: {
accountProfile: '该账号为...类型账号',
contentTendency: '内容倾向明显偏...',
influenceJudgment: '具有一定影响力,需关注...',
},
details: {
profileAnalysis: '账号画像分析...',
contentAnalysis: '内容倾向分析...',
behaviorAnalysis: '行为特征分析...',
influenceAnalysis: '影响判断分析...',
},
raw: [
{ date: '2026-03-15', content: '...' },
{ date: '2026-03-16', content: '...' },
{ date: '2026-03-17', content: '...' },
],
};
default:
return {
overview: { summary: '分析结果摘要...' },
details: { analysis: '详细分析内容...' },
raw: [],
};
}
};
// 重置
const handleReset = () => {
const initialForm: Record<string, string> = {};
config.inputs.forEach(input => {
initialForm[input.name] = '';
});
setFormData(initialForm);
const initialParams: Record<string, string> = {};
config.params.forEach(param => {
initialParams[param.name] = param.default;
});
setParams(initialParams);
const initialDims: Record<string, boolean> = {};
config.dimensions.forEach(dim => {
initialDims[dim.name] = dim.default;
});
setDimensions(initialDims);
setHasResult(false);
setResult(null);
setCurrentStep(0);
};
// 复制结果
const handleCopy = () => {
navigator.clipboard.writeText(JSON.stringify(result, null, 2));
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
// 保存快照
const handleSaveSnapshot = () => {
// 模拟保存
alert('快照已保存到运行记录');
onNavigate('light-app-records');
};
// 导出
const handleExport = () => {
const blob = new Blob([JSON.stringify(result, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${config.name}_结果_${new Date().toISOString().split('T')[0]}.json`;
a.click();
};
return (
<div className="space-y-4">
{/* 顶部操作栏 */}
<div className={`rounded-xl border p-4 ${
isDark ? 'bg-slate-800/50 border-slate-700' : 'bg-white border-gray-200'
}`}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<button
onClick={() => onNavigate('light-app-square')}
className={`p-2 rounded-lg transition-colors ${
isDark ? 'hover:bg-slate-700 text-slate-400' : 'hover:bg-gray-100 text-gray-500'
}`}
>
<ArrowLeft className="w-5 h-5" />
</button>
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${config.bgColor}`}>
<Icon className={`w-5 h-5 ${config.iconColor}`} />
</div>
<div>
<h1 className={`text-lg font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>
{config.name}
</h1>
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
{config.description}
</p>
</div>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={handleSaveSnapshot}
disabled={!hasResult}
className={`px-4 py-2 rounded-lg text-sm font-medium flex items-center gap-2 transition-colors ${
hasResult
? isDark
? 'bg-slate-700 text-slate-300 hover:bg-slate-600'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
: isDark
? 'bg-slate-800 text-slate-600 cursor-not-allowed'
: 'bg-gray-50 text-gray-400 cursor-not-allowed'
}`}
>
<Save className="w-4 h-4" />
保存快照
</button>
<button
onClick={handleExport}
disabled={!hasResult}
className={`px-4 py-2 rounded-lg text-sm font-medium flex items-center gap-2 transition-colors ${
hasResult
? isDark
? 'bg-slate-700 text-slate-300 hover:bg-slate-600'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
: isDark
? 'bg-slate-800 text-slate-600 cursor-not-allowed'
: 'bg-gray-50 text-gray-400 cursor-not-allowed'
}`}
>
<Download className="w-4 h-4" />
导出结果
</button>
<button
onClick={() => onNavigate('task-create')}
disabled={!hasResult}
className={`px-4 py-2 rounded-lg text-sm font-medium flex items-center gap-2 transition-colors ${
hasResult
? isDark
? 'bg-blue-500/20 text-blue-400 hover:bg-blue-500/30 border border-blue-500/30'
: 'bg-red-50 text-red-600 hover:bg-red-100 border border-red-200'
: isDark
? 'bg-slate-800 text-slate-600 cursor-not-allowed border border-transparent'
: 'bg-gray-50 text-gray-400 cursor-not-allowed border border-transparent'
}`}
>
<Send className="w-4 h-4" />
转正式任务
</button>
</div>
</div>
</div>
{/* 主体内容区 - 三栏布局 */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-4">
{/* 左侧:输入与配置 */}
<div className={`lg:col-span-4 rounded-xl border p-4 ${
isDark ? 'bg-slate-800/50 border-slate-700' : 'bg-white border-gray-200'
}`}>
<div className="space-y-6">
{/* 输入对象 */}
<div>
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
输入对象
</h3>
<div className="space-y-3">
{config.inputs.map(input => (
<div key={input.name}>
<label className={`block text-xs mb-1.5 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
{input.label}
{input.required && <span className="text-red-500 ml-1">*</span>}
</label>
{input.type === 'textarea' ? (
<textarea
value={formData[input.name] || ''}
onChange={(e) => setFormData({ ...formData, [input.name]: e.target.value })}
placeholder={input.placeholder}
rows={4}
className={`w-full px-3 py-2 rounded-lg border text-sm transition-colors focus:outline-none focus:ring-2 ${
isDark
? 'bg-slate-900 border-slate-600 text-slate-200 placeholder-slate-500 focus:border-blue-500'
: 'bg-gray-50 border-gray-300 text-gray-900 placeholder-gray-400 focus:border-red-500'
}`}
/>
) : input.type === 'upload' ? (
<div className={`border-2 border-dashed rounded-lg p-4 text-center cursor-pointer transition-colors ${
isDark
? 'border-slate-600 hover:border-blue-500/50'
: 'border-gray-300 hover:border-red-500/50'
}`}>
<Upload className={`w-6 h-6 mx-auto mb-2 ${isDark ? 'text-slate-500' : 'text-gray-400'}`} />
<p className={`text-xs ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
{input.placeholder}
</p>
</div>
) : (
<input
type="text"
value={formData[input.name] || ''}
onChange={(e) => setFormData({ ...formData, [input.name]: e.target.value })}
placeholder={input.placeholder}
className={`w-full px-3 py-2 rounded-lg border text-sm transition-colors focus:outline-none focus:ring-2 ${
isDark
? 'bg-slate-900 border-slate-600 text-slate-200 placeholder-slate-500 focus:border-blue-500'
: 'bg-gray-50 border-gray-300 text-gray-900 placeholder-gray-400 focus:border-red-500'
}`}
/>
)}
</div>
))}
</div>
</div>
{/* 参数配置 */}
<div>
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
参数配置
</h3>
<div className="space-y-3">
{config.params.map(param => (
<div key={param.name}>
<label className={`block text-xs mb-1.5 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
{param.label}
</label>
<select
value={params[param.name] || param.default}
onChange={(e) => setParams({ ...params, [param.name]: e.target.value })}
className={`w-full px-3 py-2 rounded-lg border text-sm transition-colors focus:outline-none focus:ring-2 ${
isDark
? 'bg-slate-900 border-slate-600 text-slate-200 focus:border-blue-500'
: 'bg-gray-50 border-gray-300 text-gray-900 focus:border-red-500'
}`}
>
{param.options.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
))}
</div>
</div>
{/* 分析维度 */}
<div>
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
分析维度
</h3>
<div className="space-y-2">
{config.dimensions.map(dim => (
<label
key={dim.name}
className={`flex items-center gap-2 p-2 rounded-lg cursor-pointer transition-colors ${
isDark ? 'hover:bg-slate-700/50' : 'hover:bg-gray-50'
}`}
>
<input
type="checkbox"
checked={dimensions[dim.name] ?? dim.default}
onChange={(e) => setDimensions({ ...dimensions, [dim.name]: e.target.checked })}
className="w-4 h-4 rounded border-gray-300"
/>
<span className={`text-sm ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
{dim.label}
</span>
</label>
))}
</div>
</div>
</div>
</div>
{/* 中间:执行过程/对象预览 */}
<div className={`lg:col-span-4 rounded-xl border p-4 ${
isDark ? 'bg-slate-800/50 border-slate-700' : 'bg-white border-gray-200'
}`}>
<div className="space-y-6">
{/* 执行过程 */}
<div>
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
执行过程
</h3>
<div className="space-y-2">
{config.steps.map((step, index) => (
<div key={index} className="flex items-center gap-3">
{isRunning && index === currentStep ? (
<Loader2 className={`w-4 h-4 animate-spin ${isDark ? 'text-blue-400' : 'text-red-500'}`} />
) : index < currentStep || hasResult ? (
<CheckCircle2 className={`w-4 h-4 ${isDark ? 'text-green-400' : 'text-green-500'}`} />
) : (
<Circle className={`w-4 h-4 ${isDark ? 'text-slate-600' : 'text-gray-300'}`} />
)}
<span className={`text-sm ${
index < currentStep || hasResult
? isDark ? 'text-slate-300' : 'text-gray-700'
: isDark ? 'text-slate-500' : 'text-gray-400'
}`}>
{index + 1}. {step}
</span>
</div>
))}
</div>
</div>
{/* 对象预览 */}
<div>
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
对象预览
</h3>
<div className={`rounded-lg p-4 ${
isDark ? 'bg-slate-900/50' : 'bg-gray-50'
}`}>
{formData.mainInput || formData.eventName || formData.accountId || formData.link || formData.topicName ? (
<div className="space-y-2">
{config.previewFields.map(field => (
<div key={field.key} className="flex items-start gap-2">
<span className={`text-xs font-medium min-w-[60px] ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
{field.label}:
</span>
<span className={`text-sm ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>
{formData.mainInput?.slice(0, 30) || formData.eventName || formData.accountId || formData.link || formData.topicName}
</span>
</div>
))}
</div>
) : (
<p className={`text-sm text-center ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
请在左侧输入对象后预览
</p>
)}
</div>
</div>
</div>
</div>
{/* 右侧:结果区 */}
<div className={`lg:col-span-4 rounded-xl border p-4 ${
isDark ? 'bg-slate-800/50 border-slate-700' : 'bg-white border-gray-200'
}`}>
<div className="flex items-center justify-between mb-4">
<h3 className={`text-sm font-medium ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
分析结果
</h3>
{hasResult && (
<div className="flex items-center gap-1">
<button
onClick={handleCopy}
className={`p-1.5 rounded-lg transition-colors ${
isDark ? 'hover:bg-slate-700 text-slate-400' : 'hover:bg-gray-100 text-gray-500'
}`}
>
{copied ? <CheckCheck className="w-4 h-4 text-green-500" /> : <Copy className="w-4 h-4" />}
</button>
</div>
)}
</div>
{/* 结果标签页 */}
{hasResult ? (
<>
<div className="flex gap-1 mb-4">
{(['overview', 'details', 'raw'] as ResultTab[]).map(tab => (
<button
key={tab}
onClick={() => setResultTab(tab)}
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-colors ${
resultTab === tab
? isDark
? 'bg-blue-500/20 text-blue-400'
: 'bg-red-50 text-red-600'
: isDark
? 'text-slate-400 hover:text-slate-300'
: 'text-gray-500 hover:text-gray-700'
}`}
>
{tab === 'overview' ? '概览' : tab === 'details' ? '详细结果' : '原始数据'}
</button>
))}
</div>
{/* 结果内容 */}
<div className={`rounded-lg p-4 ${
isDark ? 'bg-slate-900/50' : 'bg-gray-50'
}`}>
{resultTab === 'overview' && (
<div className="space-y-3">
{Object.entries(result?.overview || {}).map(([key, value]) => (
<div key={key}>
<span className={`text-xs font-medium ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
{key === 'overallSentiment' ? '总体情绪' :
key === 'mainConcerns' ? '主要关注点' :
key === 'riskAlert' ? '风险提示' :
key === 'sentimentTrend' ? '情绪趋势' :
key === 'eventStage' ? '事件阶段' :
key === 'keyNodes' ? '关键节点' :
key === 'riskLevel' ? '风险等级' :
key === 'suggestion' ? '建议' :
key === 'accountProfile' ? '账号画像' :
key === 'contentTendency' ? '内容倾向' :
key === 'influenceJudgment' ? '影响判断' :
key === 'summary' ? '结果摘要' : key}:
</span>
<p className={`text-sm mt-1 ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>
{Array.isArray(value) ? value.join('、') : String(value)}
</p>
</div>
))}
</div>
)}
{resultTab === 'details' && (
<div className="space-y-3">
{Object.entries(result?.details || {}).map(([key, value]) => (
<div key={key}>
<span className={`text-xs font-medium ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
{key === 'publicSentiment' ? '舆情分析' :
key === 'socialSentiment' ? '社情分析' :
key === 'publicOpinion' ? '民情分析' :
key === 'riskAssessment' ? '风险评估' :
key === 'context' ? '事件脉络' :
key === 'keyPoints' ? '关键节点' :
key === 'riskJudgment' ? '风险判断' :
key === 'suggestions' ? '建议' :
key === 'profileAnalysis' ? '画像分析' :
key === 'contentAnalysis' ? '内容分析' :
key === 'behaviorAnalysis' ? '行为分析' :
key === 'influenceAnalysis' ? '影响分析' :
key === 'analysis' ? '分析' : key}:
</span>
<p className={`text-sm mt-1 ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>
{Array.isArray(value) ? value.join('\n') : String(value)}
</p>
</div>
))}
</div>
)}
{resultTab === 'raw' && (
<div className="space-y-2">
{(result?.raw || []).length > 0 ? (
result.raw.map((item: any, index: number) => (
<div key={index} className={`p-2 rounded ${isDark ? 'bg-slate-800/50' : 'bg-white'}`}>
<div className="flex items-center gap-2 mb-1">
{item.type && (
<span className={`px-2 py-0.5 rounded text-xs ${
isDark ? 'bg-blue-500/20 text-blue-400' : 'bg-blue-50 text-blue-600'
}`}>
{item.type}
</span>
)}
{item.time && (
<span className={`text-xs ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
{item.time}
</span>
)}
{item.date && (
<span className={`text-xs ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
{item.date}
</span>
)}
</div>
<p className={`text-sm ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>
{item.content || item.title || JSON.stringify(item)}
</p>
</div>
))
) : (
<p className={`text-sm text-center ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
无原始数据
</p>
)}
</div>
)}
</div>
</>
) : (
<div className="text-center py-12">
<Sparkles className={`w-12 h-12 mx-auto mb-4 ${isDark ? 'text-slate-600' : 'text-gray-300'}`} />
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
{isRunning ? '分析中...' : '点击下方按钮开始分析'}
</p>
</div>
)}
</div>
</div>
{/* 底部操作区 */}
<div className={`rounded-xl border p-4 ${
isDark ? 'bg-slate-800/50 border-slate-700' : 'bg-white border-gray-200'
}`}>
<div className="flex items-center justify-between">
<button
onClick={handleReset}
disabled={isRunning}
className={`px-6 py-2.5 rounded-lg font-medium flex items-center gap-2 transition-colors ${
isRunning
? isDark
? 'bg-slate-800 text-slate-600 cursor-not-allowed'
: 'bg-gray-50 text-gray-400 cursor-not-allowed'
: isDark
? 'bg-slate-700 text-slate-300 hover:bg-slate-600'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
<RotateCcw className="w-4 h-4" />
重置
</button>
<button
onClick={handleRun}
disabled={isRunning || !formData.mainInput && !formData.eventName && !formData.accountId && !formData.link && !formData.topicName}
className={`px-8 py-2.5 rounded-lg font-medium flex items-center gap-2 transition-all ${
isRunning
? isDark
? 'bg-blue-500/20 text-blue-400 cursor-wait'
: 'bg-red-50 text-red-400 cursor-wait'
: formData.mainInput || formData.eventName || formData.accountId || formData.link || formData.topicName
? isDark
? 'bg-blue-600 text-white hover:bg-blue-500 shadow-lg shadow-blue-500/25'
: 'bg-red-500 text-white hover:bg-red-600 shadow-lg shadow-red-500/25'
: isDark
? 'bg-slate-700 text-slate-500 cursor-not-allowed'
: 'bg-gray-200 text-gray-400 cursor-not-allowed'
}`}
>
{isRunning ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
分析中...
</>
) : (
<>
<PlayCircle className="w-4 h-4" />
开始分析
</>
)}
</button>
</div>
</div>
</div>
);
};
export default LightAppRun;