997 lines
41 KiB
TypeScript
997 lines
41 KiB
TypeScript
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;
|