代码修改
This commit is contained in:
1 parent
8f5af46ec2
commit
8f75e12871
168 files changed
+76575
No files matched your search
@@ -0,0 +1,877 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import {
|
||||
ArrowLeft,
|
||||
Save,
|
||||
RotateCcw,
|
||||
Power,
|
||||
Trash2,
|
||||
Copy,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Bot,
|
||||
Target,
|
||||
Brain,
|
||||
Database,
|
||||
Box,
|
||||
Route,
|
||||
FileOutput,
|
||||
Sparkles,
|
||||
Lightbulb,
|
||||
MessageSquare,
|
||||
Play,
|
||||
Send,
|
||||
Plus,
|
||||
X,
|
||||
Check,
|
||||
AlertCircle,
|
||||
Wrench,
|
||||
Layers,
|
||||
Zap,
|
||||
Clock,
|
||||
} from 'lucide-react';
|
||||
|
||||
interface AgentDetailProps {
|
||||
agentId: string | null;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
interface ExpandedSection {
|
||||
basic: boolean;
|
||||
responsibility: boolean;
|
||||
skills: boolean;
|
||||
datasource: boolean;
|
||||
function: boolean;
|
||||
thinking: boolean;
|
||||
output: boolean;
|
||||
}
|
||||
|
||||
const AgentDetail: React.FC<AgentDetailProps> = ({ agentId, onBack }) => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
|
||||
// 主题颜色类
|
||||
const cardBg = isDark ? 'bg-slate-800/30' : 'bg-white';
|
||||
const cardBorder = isDark ? 'border-blue-800/30' : 'border-red-200';
|
||||
const textPrimary = isDark ? 'text-white' : 'text-gray-900';
|
||||
const textSecondary = isDark ? 'text-slate-400' : 'text-gray-600';
|
||||
const textMuted = isDark ? 'text-slate-500' : 'text-gray-500';
|
||||
const btnSecondary = isDark ? 'bg-slate-700/50 hover:bg-slate-600/50 border-blue-800/30 text-slate-300' : 'bg-gray-100 hover:bg-gray-200 border-gray-300 text-gray-700';
|
||||
const btnPrimary = isDark ? 'bg-blue-600 hover:bg-blue-500' : 'bg-red-600 hover:bg-red-500';
|
||||
const inputBg = isDark ? 'bg-slate-900/50' : 'bg-gray-50';
|
||||
const inputBorder = isDark ? 'border-blue-800/30' : 'border-gray-300';
|
||||
|
||||
const [isEnabled, setIsEnabled] = useState(true);
|
||||
const [expandedSections, setExpandedSections] = useState<ExpandedSection>({
|
||||
basic: true,
|
||||
responsibility: true,
|
||||
skills: true,
|
||||
datasource: true,
|
||||
function: true,
|
||||
thinking: true,
|
||||
output: true,
|
||||
});
|
||||
const [chatMessages, setChatMessages] = useState<{role: 'user' | 'agent'; content: string}[]>([
|
||||
{ role: 'agent', content: '核心受众应聚焦18-25岁基层青年群体,原因是该群体社交媒体活跃度高、注重性价比、偏好真实有态度的品牌叙事。' }
|
||||
]);
|
||||
const [chatInput, setChatInput] = useState('');
|
||||
const [aiSuggestion, setAiSuggestion] = useState('');
|
||||
const [showGeneratedConfig, setShowGeneratedConfig] = useState(false);
|
||||
|
||||
const toggleSection = (section: keyof ExpandedSection) => {
|
||||
setExpandedSections(prev => ({ ...prev, [section]: !prev[section] }));
|
||||
};
|
||||
|
||||
const skills = [
|
||||
{ name: '受众分层', type: '分析类', enabled: true, trigger: '自动', priority: 1 },
|
||||
{ name: '核心受众识别', type: '推断类', enabled: true, trigger: '自动', priority: 1 },
|
||||
{ name: '受众画像抽取', type: '抽取类', enabled: true, trigger: '条件触发', priority: 2 },
|
||||
{ name: '阻力因素提炼', type: '分析类', enabled: true, trigger: '手动', priority: 3 },
|
||||
];
|
||||
|
||||
const dataSources = [
|
||||
{ name: '宣传知识库', type: '知识库', status: 'connected', permission: '只读', trigger: '自动', access: '品牌案例、活动方法论、历史策略' },
|
||||
{ name: 'Y系统', type: '业务系统', status: 'connected', permission: '只读', trigger: '条件触发', access: '用户标签、传播数据' },
|
||||
{ name: 'A系统', type: '业务系统', status: 'disconnected', permission: '无', trigger: '不可用', access: '—' },
|
||||
{ name: '市场数据库', type: '外部数据源', status: 'connected', permission: '只读', trigger: '手动', access: '市场报告、用户研究摘要' },
|
||||
];
|
||||
|
||||
const functions = [
|
||||
{ name: '信息抽取器', type: '抽取', enabled: true, trigger: '自动', usage: '提取任务中的受众相关信息' },
|
||||
{ name: '标签聚类器', type: '分析', enabled: true, trigger: '条件触发', usage: '辅助进行受众分层' },
|
||||
{ name: '案例检索器', type: '检索', enabled: true, trigger: '手动', usage: '调用历史案例进行参照' },
|
||||
{ name: '结构化输出器', type: '格式化', enabled: true, trigger: '自动', usage: '输出标准JSON字段' },
|
||||
];
|
||||
|
||||
const thinkingSteps = [
|
||||
{ step: 1, name: '识别任务中的目标对象', desc: '提取任务中明确或隐含的目标受众信息', required: true, skill: '受众分层', func: '信息抽取器', ds: '无' },
|
||||
{ step: 2, name: '判断是否需要分层', desc: '根据任务目标判断是否区分核心/次级/影响者受众', required: true, skill: '核心受众识别', func: '标签聚类器', ds: 'Y系统' },
|
||||
{ step: 3, name: '提取核心受众特征', desc: '分析年龄、身份、关切与行为特征', required: true, skill: '画像抽取', func: '结构化输出器', ds: '宣传知识库' },
|
||||
{ step: 4, name: '输出结构化结果', desc: '形成标准受众分析输出', required: true, skill: '—', func: '结构化输出器', ds: '—' },
|
||||
];
|
||||
|
||||
const outputFields = [
|
||||
{ name: 'core_audience', type: '文本', required: true },
|
||||
{ name: 'secondary_audience', type: '文本', required: false },
|
||||
{ name: 'key_traits', type: '数组', required: true },
|
||||
{ name: 'concerns', type: '数组', required: false },
|
||||
{ name: 'barriers', type: '数组', required: false },
|
||||
{ name: 'trust_sources', type: '数组', required: false },
|
||||
];
|
||||
|
||||
const aiOptimizations = [
|
||||
{ issue: '当前已配置"受众画像抽取"技能,但未配置对应输出字段', canFix: true },
|
||||
{ issue: '思考路径中"判断是否需要分层"步骤未绑定功能调用', canFix: true },
|
||||
{ issue: '已绑定A系统,但当前无任何步骤使用该数据源', canFix: false },
|
||||
];
|
||||
|
||||
const executionDetails = {
|
||||
dataSources: ['宣传知识库', 'Y系统'],
|
||||
skills: ['受众分层', '核心受众识别', '画像抽取'],
|
||||
functions: ['信息抽取器', '标签聚类器', '结构化输出器'],
|
||||
outputs: ['core_audience', 'key_traits', 'barriers'],
|
||||
};
|
||||
|
||||
const handleSendMessage = () => {
|
||||
if (chatInput.trim()) {
|
||||
setChatMessages([...chatMessages, { role: 'user', content: chatInput }]);
|
||||
setChatInput('');
|
||||
setTimeout(() => {
|
||||
setChatMessages(prev => [...prev, {
|
||||
role: 'agent',
|
||||
content: '核心受众应聚焦18-25岁基层青年群体,原因是该群体社交媒体活跃度高、注重性价比、偏好真实有态度的品牌叙事。'
|
||||
}]);
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGenerateConfig = () => {
|
||||
setAiSuggestion('正在分析职责、技能、数据源...');
|
||||
setTimeout(() => {
|
||||
setShowGeneratedConfig(true);
|
||||
setAiSuggestion('');
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
const SectionHeader = ({
|
||||
title,
|
||||
subtitle,
|
||||
icon: Icon,
|
||||
section,
|
||||
children
|
||||
}: {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
icon: React.ElementType;
|
||||
section: keyof ExpandedSection;
|
||||
children: React.ReactNode;
|
||||
}) => (
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl`}>
|
||||
<button
|
||||
onClick={() => toggleSection(section)}
|
||||
className={`w-full flex items-center justify-between p-4 ${isDark ? 'hover:bg-slate-800/50' : 'hover:bg-gray-100'} transition-colors sticky top-0 z-10 ${cardBg} backdrop-blur-xl`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-10 h-10 rounded-lg ${isDark ? 'bg-blue-500/20' : 'bg-red-100'} flex items-center justify-center`}>
|
||||
<Icon className={`w-5 h-5 ${isDark ? 'text-blue-400' : 'text-red-500'}`} />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<h3 className={`${textPrimary} font-semibold`}>{title}</h3>
|
||||
<p className={`${textMuted} text-xs`}>{subtitle}</p>
|
||||
</div>
|
||||
</div>
|
||||
{expandedSections[section] ? (
|
||||
<ChevronDown className={`w-5 h-5 ${textSecondary}`} />
|
||||
) : (
|
||||
<ChevronRight className={`w-5 h-5 ${textSecondary}`} />
|
||||
)}
|
||||
</button>
|
||||
{expandedSections[section] && (
|
||||
<div className="px-4 pb-4">
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-[calc(100vh-140px)] flex flex-col overflow-hidden">
|
||||
{/* 顶部操作栏 */}
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} p-4 flex-shrink-0`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<button onClick={onBack} className={`p-2 ${isDark ? 'hover:bg-slate-700/50' : 'hover:bg-gray-200'} rounded-lg transition-colors`}>
|
||||
<ArrowLeft className={`w-5 h-5 ${textSecondary}`} />
|
||||
</button>
|
||||
<div>
|
||||
<p className={`${textSecondary} text-xs`}>智能体能力配置</p>
|
||||
<div className="flex items-center gap-2 mt-0.5">
|
||||
<Bot className={`w-4 h-4 ${isDark ? 'text-blue-400' : 'text-red-500'}`} />
|
||||
<span className={`${textPrimary} font-semibold`}>AudienceAgent</span>
|
||||
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${
|
||||
isEnabled
|
||||
? isDark ? 'bg-green-500/20 text-green-400' : 'bg-green-100 text-green-600'
|
||||
: isDark ? 'bg-slate-500/20 text-slate-400' : 'bg-gray-200 text-gray-500'
|
||||
}`}>
|
||||
{isEnabled ? '启用' : '停用'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button className={`flex items-center gap-2 px-4 py-2 ${btnPrimary} text-white rounded-lg text-sm transition-colors`}>
|
||||
<Save className="w-4 h-4" />
|
||||
保存
|
||||
</button>
|
||||
<button className={`flex items-center gap-2 px-4 py-2 ${btnSecondary} rounded-lg text-sm transition-colors`}>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
重置
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsEnabled(!isEnabled)}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm transition-colors ${
|
||||
isEnabled
|
||||
? isDark ? 'bg-amber-500/20 hover:bg-amber-500/30 border border-amber-500/30 text-amber-400' : 'bg-amber-100 hover:bg-amber-200 border border-amber-200 text-amber-600'
|
||||
: isDark ? 'bg-green-500/20 hover:bg-green-500/30 border border-green-500/30 text-green-400' : 'bg-green-100 hover:bg-green-200 border border-green-200 text-green-600'
|
||||
}`}
|
||||
>
|
||||
<Power className="w-4 h-4" />
|
||||
{isEnabled ? '停用' : '启用'}
|
||||
</button>
|
||||
<button className={`flex items-center gap-2 px-4 py-2 ${btnSecondary} ${isDark ? 'hover:text-red-400' : 'hover:text-red-500'} rounded-lg text-sm transition-colors`}>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
删除
|
||||
</button>
|
||||
<button className={`flex items-center gap-2 px-4 py-2 ${btnSecondary} rounded-lg text-sm transition-colors`}>
|
||||
<Copy className="w-4 h-4" />
|
||||
复制
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 主内容区:三栏布局 */}
|
||||
<div className="flex-1 grid grid-cols-12 gap-4 mt-4 min-h-0">
|
||||
{/* 左侧主配置区 (65%) - 带自定义滚动条 */}
|
||||
<div className="col-span-8 flex flex-col gap-4 h-full overflow-y-auto scrollbar-thin scrollbar-thumb-slate-600 scrollbar-track-transparent hover:scrollbar-thumb-slate-500">
|
||||
{/* 模块1:基本信息 */}
|
||||
<SectionHeader
|
||||
title="基本信息"
|
||||
subtitle="定义智能体的基本身份、用途和交互属性"
|
||||
icon={Bot}
|
||||
section="basic"
|
||||
>
|
||||
<div className="space-y-4 pt-2">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className={`block ${textSecondary} text-sm mb-2`}>智能体名称</label>
|
||||
<input
|
||||
type="text"
|
||||
defaultValue="AudienceAgent"
|
||||
className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm focus:outline-none ${isDark ? 'focus:border-blue-500/50' : 'focus:border-red-400'}`}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={`block ${textSecondary} text-sm mb-2`}>智能体编码</label>
|
||||
<input
|
||||
type="text"
|
||||
defaultValue="AGENT_AUDIENCE_001"
|
||||
className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm focus:outline-none ${isDark ? 'focus:border-blue-500/50' : 'focus:border-red-400'}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className={`block ${textSecondary} text-sm mb-2`}>智能体分类</label>
|
||||
<select className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm focus:outline-none ${isDark ? 'focus:border-blue-500/50' : 'focus:border-red-400'}`}>
|
||||
<option>分析类</option>
|
||||
<option>编排类</option>
|
||||
<option>叙事类</option>
|
||||
<option>校验类</option>
|
||||
<option>成稿类</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className={`block ${textSecondary} text-sm mb-2`}>默认交互模式</label>
|
||||
<select className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm focus:outline-none ${isDark ? 'focus:border-blue-500/50' : 'focus:border-red-400'}`}>
|
||||
<option>分析式</option>
|
||||
<option>对话式</option>
|
||||
<option>指令式</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className={`block ${textSecondary} text-sm mb-2`}>智能体说明</label>
|
||||
<textarea
|
||||
rows={2}
|
||||
defaultValue="负责分析核心受众特征,输出受众画像和行为特征,为后续策略制定提供数据支持"
|
||||
className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm focus:outline-none ${isDark ? 'focus:border-blue-500/50' : 'focus:border-red-400'} resize-none`}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className={`block ${textSecondary} text-sm mb-2`}>专业标签</label>
|
||||
<div className={`flex flex-wrap gap-2 p-3 ${inputBg} ${inputBorder} rounded-lg min-h-[46px]`}>
|
||||
{['受众分析', '用户画像', '分层识别'].map((tag) => (
|
||||
<span key={tag} className={`px-2 py-1 ${isDark ? 'bg-blue-500/20 text-blue-400' : 'bg-red-100 text-red-600'} rounded text-xs flex items-center gap-1`}>
|
||||
{tag}
|
||||
<button className={`hover:${isDark ? 'text-blue-300' : 'text-red-400'}`}><X className="w-3 h-3" /></button>
|
||||
</span>
|
||||
))}
|
||||
<button className={`px-2 py-1 ${textSecondary} hover:${textPrimary} text-xs flex items-center gap-1`}>
|
||||
<Plus className="w-3 h-3" /> 添加
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className={`block ${textSecondary} text-sm mb-2`}>适用任务关键词</label>
|
||||
<div className={`flex flex-wrap gap-2 p-3 ${inputBg} ${inputBorder} rounded-lg min-h-[46px]`}>
|
||||
{['宣传对象分析', '核心人群识别'].map((tag) => (
|
||||
<span key={tag} className={`px-2 py-1 ${isDark ? 'bg-cyan-500/20 text-cyan-400' : 'bg-orange-100 text-orange-600'} rounded text-xs flex items-center gap-1`}>
|
||||
{tag}
|
||||
<button className={`hover:${isDark ? 'text-cyan-300' : 'text-orange-400'}`}><X className="w-3 h-3" /></button>
|
||||
</span>
|
||||
))}
|
||||
<button className={`px-2 py-1 ${textSecondary} hover:${textPrimary} text-xs flex items-center gap-1`}>
|
||||
<Plus className="w-3 h-3" /> 添加
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`flex items-center gap-6 pt-2 border-t ${cardBorder}`}>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className={`${textSecondary} text-sm`}>支持单独问答</span>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" className="sr-only peer" defaultChecked />
|
||||
<div className={`w-11 h-6 ${isDark ? 'bg-slate-700' : 'bg-gray-300'} peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all ${isDark ? 'peer-checked:bg-blue-600' : 'peer-checked:bg-red-500'}`}></div>
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className={`${textSecondary} text-sm`}>支持单独任务执行</span>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" className="sr-only peer" defaultChecked />
|
||||
<div className={`w-11 h-6 ${isDark ? 'bg-slate-700' : 'bg-gray-300'} peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all ${isDark ? 'peer-checked:bg-blue-600' : 'peer-checked:bg-red-500'}`}></div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SectionHeader>
|
||||
|
||||
{/* 模块2:职责与边界 */}
|
||||
<SectionHeader
|
||||
title="职责与边界"
|
||||
subtitle="明确智能体的工作范围、边界与禁止事项"
|
||||
icon={Target}
|
||||
section="responsibility"
|
||||
>
|
||||
<div className="space-y-4 pt-2">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className={`block ${textSecondary} text-sm mb-2`}>核心职责</label>
|
||||
<textarea
|
||||
rows={3}
|
||||
defaultValue="识别任务中的关键受众群体,对受众进行分层,并提炼特征、关切与阻力,形成完整的受众画像报告。"
|
||||
className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm focus:outline-none ${isDark ? 'focus:border-blue-500/50' : 'focus:border-red-400'} resize-none`}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={`block ${textSecondary} text-sm mb-2`}>职责边界</label>
|
||||
<textarea
|
||||
rows={3}
|
||||
defaultValue="不直接生成叙事方案;不负责风险最终审核;不替代行为分析;不在缺少依据时擅自扩大受众范围。"
|
||||
className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm focus:outline-none ${isDark ? 'focus:border-blue-500/50' : 'focus:border-red-400'} resize-none`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className={`block ${textSecondary} text-sm mb-2`}>禁止事项</label>
|
||||
<textarea
|
||||
rows={2}
|
||||
defaultValue="不得在缺少依据时擅自扩大受众范围;不得输出与任务无关的人群判断;不得更改任务目标。"
|
||||
className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm focus:outline-none ${isDark ? 'focus:border-blue-500/50' : 'focus:border-red-400'} resize-none`}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={`block ${textSecondary} text-sm mb-2`}>典型任务</label>
|
||||
<textarea
|
||||
rows={2}
|
||||
defaultValue="分析某宣传任务的核心受众是谁;输出受众画像;识别影响者与阻力群体;提炼受众关切与信任来源。"
|
||||
className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm focus:outline-none ${isDark ? 'focus:border-blue-500/50' : 'focus:border-red-400'} resize-none`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className={`block ${textSecondary} text-sm mb-2`}>不处理的问题</label>
|
||||
<textarea
|
||||
rows={2}
|
||||
defaultValue="不负责撰写传播文案;不负责渠道投放建议;不生成具体执行方案;不进行风险评估。"
|
||||
className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm focus:outline-none ${isDark ? 'focus:border-blue-500/50' : 'focus:border-red-400'} resize-none`}
|
||||
/>
|
||||
</div>
|
||||
<div className={`flex items-center gap-2 pt-2 border-t ${cardBorder}`}>
|
||||
<button className={`flex items-center gap-2 px-3 py-1.5 ${isDark ? 'bg-blue-500/20 hover:bg-blue-500/30 border border-blue-500/30 text-blue-400' : 'bg-red-100 hover:bg-red-200 border border-red-200 text-red-600'} rounded-lg text-xs transition-colors`}>
|
||||
<Sparkles className="w-3 h-3" /> AI生成职责
|
||||
</button>
|
||||
<button className={`flex items-center gap-2 px-3 py-1.5 ${btnSecondary} rounded-lg text-xs transition-colors`}>
|
||||
<Lightbulb className="w-3 h-3" /> AI补全边界
|
||||
</button>
|
||||
<button className={`flex items-center gap-2 px-3 py-1.5 ${btnSecondary} rounded-lg text-xs transition-colors`}>
|
||||
<AlertCircle className="w-3 h-3" /> AI发现冲突
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</SectionHeader>
|
||||
|
||||
{/* 模块3:技能配置 */}
|
||||
<SectionHeader
|
||||
title="技能配置"
|
||||
subtitle="定义智能体当前启用的技能及其触发方式"
|
||||
icon={Layers}
|
||||
section="skills"
|
||||
>
|
||||
<div className="space-y-3 pt-2">
|
||||
{skills.map((skill, index) => (
|
||||
<div key={index} className={`p-3 ${inputBg} ${inputBorder} rounded-lg`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-8 h-8 rounded ${isDark ? 'bg-purple-500/20' : 'bg-purple-100'} flex items-center justify-center`}>
|
||||
<Brain className={`w-4 h-4 ${isDark ? 'text-purple-400' : 'text-purple-600'}`} />
|
||||
</div>
|
||||
<div>
|
||||
<p className={`${textPrimary} text-sm font-medium`}>{skill.name}</p>
|
||||
<p className={`${textMuted} text-xs`}>类型:{skill.type} · 触发:{skill.trigger} · 优先级:{skill.priority}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button className={`p-1.5 ${isDark ? 'hover:bg-slate-700/50' : 'hover:bg-gray-200'} rounded transition-colors`}>
|
||||
<Wrench className={`w-4 h-4 ${textSecondary}`} />
|
||||
</button>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" className="sr-only peer" defaultChecked={skill.enabled} />
|
||||
<div className={`w-9 h-5 ${isDark ? 'bg-slate-700' : 'bg-gray-300'} peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all ${isDark ? 'peer-checked:bg-green-600' : 'peer-checked:bg-green-500'}`}></div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className={`flex items-center gap-2 pt-2 border-t ${cardBorder}`}>
|
||||
<button className={`flex items-center gap-2 px-3 py-1.5 ${btnSecondary} rounded-lg text-xs transition-colors`}>
|
||||
<Plus className="w-3 h-3" /> 新增Skill
|
||||
</button>
|
||||
<button className={`flex items-center gap-2 px-3 py-1.5 ${isDark ? 'bg-blue-500/20 hover:bg-blue-500/30 border border-blue-500/30 text-blue-400' : 'bg-red-100 hover:bg-red-200 border border-red-200 text-red-600'} rounded-lg text-xs transition-colors`}>
|
||||
<Sparkles className="w-3 h-3" /> AI推荐Skill
|
||||
</button>
|
||||
<button className={`flex items-center gap-2 px-3 py-1.5 ${btnSecondary} rounded-lg text-xs transition-colors`}>
|
||||
<Zap className="w-3 h-3" /> AI生成技能组
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</SectionHeader>
|
||||
|
||||
{/* 模块4:数据源访问 */}
|
||||
<SectionHeader
|
||||
title="数据源访问"
|
||||
subtitle="定义智能体可读取的知识与系统数据"
|
||||
icon={Database}
|
||||
section="datasource"
|
||||
>
|
||||
<div className="space-y-3 pt-2">
|
||||
{dataSources.map((ds, index) => (
|
||||
<div key={index} className={`p-3 ${inputBg} ${inputBorder} rounded-lg`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-8 h-8 rounded flex items-center justify-center ${
|
||||
ds.status === 'connected' ? (isDark ? 'bg-green-500/20' : 'bg-green-100') : (isDark ? 'bg-slate-500/20' : 'bg-gray-200')
|
||||
}`}>
|
||||
<Database className={`w-4 h-4 ${ds.status === 'connected' ? (isDark ? 'text-green-400' : 'text-green-600') : textSecondary}`} />
|
||||
</div>
|
||||
<div>
|
||||
<p className={`${textPrimary} text-sm font-medium`}>{ds.name}</p>
|
||||
<p className={`${textMuted} text-xs`}>类型:{ds.type} · 权限:{ds.permission} · 调用:{ds.trigger}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className={`px-2 py-0.5 rounded text-xs ${
|
||||
ds.status === 'connected'
|
||||
? isDark ? 'bg-green-500/20 text-green-400' : 'bg-green-100 text-green-600'
|
||||
: isDark ? 'bg-slate-500/20 text-slate-400' : 'bg-gray-200 text-gray-500'
|
||||
}`}>
|
||||
{ds.status === 'connected' ? '已连接' : '未连接'}
|
||||
</span>
|
||||
</div>
|
||||
<p className={`${textSecondary} text-xs mt-2`}>可访问:{ds.access}</p>
|
||||
</div>
|
||||
))}
|
||||
<div className={`flex items-center gap-2 pt-2 border-t ${cardBorder}`}>
|
||||
<button className={`flex items-center gap-2 px-3 py-1.5 ${btnSecondary} rounded-lg text-xs transition-colors`}>
|
||||
<Plus className="w-3 h-3" /> 绑定数据源
|
||||
</button>
|
||||
<button className={`flex items-center gap-2 px-3 py-1.5 ${isDark ? 'bg-blue-500/20 hover:bg-blue-500/30 border border-blue-500/30 text-blue-400' : 'bg-red-100 hover:bg-red-200 border border-red-200 text-red-600'} rounded-lg text-xs transition-colors`}>
|
||||
<Sparkles className="w-3 h-3" /> AI推荐数据源
|
||||
</button>
|
||||
<button className={`flex items-center gap-2 px-3 py-1.5 ${btnSecondary} rounded-lg text-xs transition-colors`}>
|
||||
<AlertCircle className="w-3 h-3" /> AI识别缺失
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</SectionHeader>
|
||||
|
||||
{/* 模块5:功能调用 */}
|
||||
<SectionHeader
|
||||
title="功能调用"
|
||||
subtitle="定义智能体可调配的处理功能与工具能力"
|
||||
icon={Box}
|
||||
section="function"
|
||||
>
|
||||
<div className="space-y-3 pt-2">
|
||||
{functions.map((func, index) => (
|
||||
<div key={index} className={`p-3 ${inputBg} ${inputBorder} rounded-lg`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-8 h-8 rounded ${isDark ? 'bg-cyan-500/20' : 'bg-cyan-100'} flex items-center justify-center`}>
|
||||
<Box className={`w-4 h-4 ${isDark ? 'text-cyan-400' : 'text-cyan-600'}`} />
|
||||
</div>
|
||||
<div>
|
||||
<p className={`${textPrimary} text-sm font-medium`}>{func.name}</p>
|
||||
<p className={`${textMuted} text-xs`}>类型:{func.type} · 调用:{func.trigger}</p>
|
||||
</div>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" className="sr-only peer" defaultChecked={func.enabled} />
|
||||
<div className={`w-9 h-5 ${isDark ? 'bg-slate-700' : 'bg-gray-300'} peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all ${isDark ? 'peer-checked:bg-green-600' : 'peer-checked:bg-green-500'}`}></div>
|
||||
</label>
|
||||
</div>
|
||||
<p className={`${textSecondary} text-xs mt-2`}>用途:{func.usage}</p>
|
||||
</div>
|
||||
))}
|
||||
<div className={`flex items-center gap-2 pt-2 border-t ${cardBorder}`}>
|
||||
<button className={`flex items-center gap-2 px-3 py-1.5 ${btnSecondary} rounded-lg text-xs transition-colors`}>
|
||||
<Plus className="w-3 h-3" /> 新增功能
|
||||
</button>
|
||||
<button className={`flex items-center gap-2 px-3 py-1.5 ${isDark ? 'bg-blue-500/20 hover:bg-blue-500/30 border border-blue-500/30 text-blue-400' : 'bg-red-100 hover:bg-red-200 border border-red-200 text-red-600'} rounded-lg text-xs transition-colors`}>
|
||||
<Sparkles className="w-3 h-3" /> AI推荐功能
|
||||
</button>
|
||||
<button className={`flex items-center gap-2 px-3 py-1.5 ${btnSecondary} rounded-lg text-xs transition-colors`}>
|
||||
<Zap className="w-3 h-3" /> AI检查冗余
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</SectionHeader>
|
||||
|
||||
{/* 模块6:思考路径 */}
|
||||
<SectionHeader
|
||||
title="思考路径"
|
||||
subtitle="定义智能体处理任务时的默认分析顺序"
|
||||
icon={Route}
|
||||
section="thinking"
|
||||
>
|
||||
<div className="space-y-3 pt-2">
|
||||
{thinkingSteps.map((step) => (
|
||||
<div key={step.step} className={`p-3 ${inputBg} ${inputBorder} rounded-lg`}>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className={`w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0 mt-0.5 ${
|
||||
step.step <= 2
|
||||
? isDark ? 'bg-green-500 text-white' : 'bg-green-500 text-white'
|
||||
: isDark ? 'bg-blue-500 text-white' : 'bg-blue-500 text-white'
|
||||
}`}>
|
||||
{step.step}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className={`${textPrimary} text-sm font-medium`}>{step.name}</p>
|
||||
{step.required && (
|
||||
<span className={`px-1.5 py-0.5 ${isDark ? 'bg-red-500/20 text-red-400' : 'bg-red-100 text-red-600'} rounded text-[10px]`}>必选</span>
|
||||
)}
|
||||
</div>
|
||||
<p className={`${textSecondary} text-xs mt-1`}>{step.desc}</p>
|
||||
<div className="flex items-center gap-4 mt-2 text-xs">
|
||||
<span className={`${textMuted}`}>Skill: <span className={isDark ? 'text-purple-400' : 'text-purple-600'}>{step.skill}</span></span>
|
||||
<span className={`${textMuted}`}>功能: <span className={isDark ? 'text-cyan-400' : 'text-cyan-600'}>{step.func}</span></span>
|
||||
<span className={`${textMuted}`}>数据源: <span className={isDark ? 'text-green-400' : 'text-green-600'}>{step.ds}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<button className={`p-1.5 ${isDark ? 'hover:bg-slate-700/50' : 'hover:bg-gray-200'} rounded transition-colors`}>
|
||||
<Wrench className={`w-4 h-4 ${textSecondary}`} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className={`flex items-center gap-2 pt-2 border-t ${cardBorder}`}>
|
||||
<button className={`flex items-center gap-2 px-3 py-1.5 ${btnSecondary} rounded-lg text-xs transition-colors`}>
|
||||
<Plus className="w-3 h-3" /> 新增步骤
|
||||
</button>
|
||||
<button className={`flex items-center gap-2 px-3 py-1.5 ${isDark ? 'bg-blue-500/20 hover:bg-blue-500/30 border border-blue-500/30 text-blue-400' : 'bg-red-100 hover:bg-red-200 border border-red-200 text-red-600'} rounded-lg text-xs transition-colors`}>
|
||||
<Sparkles className="w-3 h-3" /> AI生成思考路径
|
||||
</button>
|
||||
<button className={`flex items-center gap-2 px-3 py-1.5 ${btnSecondary} rounded-lg text-xs transition-colors`}>
|
||||
<Route className="w-3 h-3" /> AI优化顺序
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</SectionHeader>
|
||||
|
||||
{/* 模块7:输出结构 */}
|
||||
<SectionHeader
|
||||
title="输出结构"
|
||||
subtitle="定义对话输出、结构化输出与可复用片段"
|
||||
icon={FileOutput}
|
||||
section="output"
|
||||
>
|
||||
<div className="space-y-4 pt-2">
|
||||
<div className={`p-3 ${isDark ? 'bg-blue-500/10 border border-blue-500/30' : 'bg-red-50 border border-red-200'} rounded-lg`}>
|
||||
<p className={`${isDark ? 'text-blue-400' : 'text-red-600'} text-sm font-medium mb-2`}>A. 对话摘要输出</p>
|
||||
<p className={`${textSecondary} text-xs`}>核心受众结论 · 结论理由摘要</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className={`${isDark ? 'text-cyan-400' : 'text-orange-600'} text-sm font-medium mb-3`}>B. 结构化输出字段</p>
|
||||
<div className="space-y-2">
|
||||
{outputFields.map((field, index) => (
|
||||
<div key={index} className={`flex items-center gap-3 p-2 ${inputBg} ${inputBorder} rounded-lg`}>
|
||||
<input
|
||||
type="text"
|
||||
defaultValue={field.name}
|
||||
className={`flex-1 px-3 py-1.5 ${isDark ? 'bg-slate-800/50' : 'bg-white'} ${inputBorder} rounded ${isDark ? 'text-cyan-400' : 'text-orange-600'} text-sm font-mono focus:outline-none ${isDark ? 'focus:border-blue-500/50' : 'focus:border-red-400'}`}
|
||||
/>
|
||||
<select className={`px-3 py-1.5 ${isDark ? 'bg-slate-800/50' : 'bg-white'} ${inputBorder} rounded ${textPrimary} text-xs`}>
|
||||
<option selected={field.type === '文本'}>文本</option>
|
||||
<option selected={field.type === '数组'}>数组</option>
|
||||
<option>对象</option>
|
||||
</select>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<span className={`${textSecondary} text-xs`}>{field.required ? '必填' : '可选'}</span>
|
||||
<input type="checkbox" className="sr-only peer" defaultChecked={field.required} />
|
||||
<div className={`w-9 h-5 ${isDark ? 'bg-slate-700' : 'bg-gray-300'} peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all ${isDark ? 'peer-checked:bg-blue-600' : 'peer-checked:bg-red-500'}`}></div>
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`p-3 ${inputBg} ${inputBorder} rounded-lg`}>
|
||||
<p className={`${isDark ? 'text-green-400' : 'text-green-600'} text-sm font-medium mb-2`}>C. 报告片段输出</p>
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<label className={`${textMuted} text-xs`}>字段名</label>
|
||||
<input
|
||||
type="text"
|
||||
defaultValue="audience_analysis_paragraph"
|
||||
className={`w-full px-3 py-1.5 ${isDark ? 'bg-slate-800/50' : 'bg-white'} ${inputBorder} rounded ${isDark ? 'text-cyan-400' : 'text-orange-600'} text-sm font-mono focus:outline-none ${isDark ? 'focus:border-blue-500/50' : 'focus:border-red-400'}`}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={`${textMuted} text-xs`}>用途说明</label>
|
||||
<input
|
||||
type="text"
|
||||
defaultValue={'用于写入正式方案中的"受众分析"段落'}
|
||||
className={`w-full px-3 py-1.5 ${isDark ? 'bg-slate-800/50' : 'bg-white'} ${inputBorder} rounded ${textPrimary} text-sm focus:outline-none ${isDark ? 'focus:border-blue-500/50' : 'focus:border-red-400'}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`flex items-center gap-2 pt-2 border-t ${cardBorder}`}>
|
||||
<button className={`flex items-center gap-2 px-3 py-1.5 ${isDark ? 'bg-blue-500/20 hover:bg-blue-500/30 border border-blue-500/30 text-blue-400' : 'bg-red-100 hover:bg-red-200 border border-red-200 text-red-600'} rounded-lg text-xs transition-colors`}>
|
||||
<Sparkles className="w-3 h-3" /> AI生成输出字段
|
||||
</button>
|
||||
<button className={`flex items-center gap-2 px-3 py-1.5 ${btnSecondary} rounded-lg text-xs transition-colors`}>
|
||||
<Lightbulb className="w-3 h-3" /> AI补全字段说明
|
||||
</button>
|
||||
<button className={`flex items-center gap-2 px-3 py-1.5 ${btnSecondary} rounded-lg text-xs transition-colors`}>
|
||||
<Check className="w-3 h-3" /> AI校验完整性
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</SectionHeader>
|
||||
</div>
|
||||
|
||||
{/* 右侧辅助区 (35%) */}
|
||||
<div className="col-span-4 flex flex-col gap-4 min-h-0 overflow-y-auto pl-2">
|
||||
{/* 卡片1:AI辅助生成 */}
|
||||
<div className={`${isDark ? 'bg-gradient-to-br from-purple-600/20 to-blue-600/20' : 'bg-gradient-to-br from-purple-100 to-blue-100'} backdrop-blur-xl ${isDark ? 'border border-purple-500/30' : 'border border-purple-300'} rounded-xl p-4`}>
|
||||
<h3 className={`${textPrimary} font-semibold mb-3 flex items-center gap-2`}>
|
||||
<Sparkles className={`w-4 h-4 ${isDark ? 'text-purple-400' : 'text-purple-600'}`} />
|
||||
AI辅助生成
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className={`block ${textSecondary} text-xs mb-1.5`}>一句话描述</label>
|
||||
<textarea
|
||||
rows={2}
|
||||
placeholder="例如:这是一个负责识别宣传任务核心受众并输出画像的分析型智能体..."
|
||||
className={`w-full px-3 py-2 ${inputBg} ${isDark ? 'border-purple-500/30' : 'border-purple-300'} rounded-lg ${textPrimary} text-xs focus:outline-none ${isDark ? 'focus:border-purple-500/50' : 'focus:border-purple-400'} resize-none`}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={`block ${textSecondary} text-xs mb-1.5`}>示例任务</label>
|
||||
<textarea
|
||||
rows={2}
|
||||
placeholder="例如:请分析某青年节宣传活动的核心受众、次级受众及其主要关切..."
|
||||
className={`w-full px-3 py-2 ${inputBg} ${isDark ? 'border-purple-500/30' : 'border-purple-300'} rounded-lg ${textPrimary} text-xs focus:outline-none ${isDark ? 'focus:border-purple-500/50' : 'focus:border-purple-400'} resize-none`}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleGenerateConfig}
|
||||
className={`w-full px-4 py-2 ${isDark ? 'bg-purple-600 hover:bg-purple-500' : 'bg-purple-500 hover:bg-purple-400'} text-white rounded-lg text-sm font-medium transition-colors flex items-center justify-center gap-2`}
|
||||
>
|
||||
<Zap className="w-4 h-4" />
|
||||
一键生成配置
|
||||
</button>
|
||||
{showGeneratedConfig && (
|
||||
<div className={`p-3 ${inputBg} ${isDark ? 'border-purple-500/30' : 'border-purple-300'} rounded-lg space-y-2`}>
|
||||
<p className={`${isDark ? 'text-purple-400' : 'text-purple-600'} text-xs font-medium`}>生成建议:</p>
|
||||
<div className={`space-y-1 text-xs ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
|
||||
<p>• 推荐职责:受众识别与画像输出</p>
|
||||
<p>• 推荐技能:分层识别、特征抽取</p>
|
||||
<p>• 推荐数据源:Y系统、宣传知识库</p>
|
||||
<p>• 推荐思考路径:4步法</p>
|
||||
</div>
|
||||
<div className={`flex items-center gap-2 pt-2 border-t ${isDark ? 'border-purple-500/30' : 'border-purple-300'}`}>
|
||||
<button className={`flex-1 px-3 py-1.5 ${isDark ? 'bg-green-600 hover:bg-green-500' : 'bg-green-500 hover:bg-green-400'} text-white rounded text-xs transition-colors`}>
|
||||
采纳全部
|
||||
</button>
|
||||
<button className={`flex-1 px-3 py-1.5 ${btnSecondary} rounded text-xs transition-colors`}>
|
||||
逐项插入
|
||||
</button>
|
||||
<button className={`px-3 py-1.5 ${btnSecondary} rounded text-xs transition-colors`}>
|
||||
忽略
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 卡片2:AI优化建议 */}
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-4`}>
|
||||
<h3 className={`${textPrimary} font-semibold mb-3 flex items-center gap-2`}>
|
||||
<Lightbulb className={`w-4 h-4 ${isDark ? 'text-yellow-400' : 'text-yellow-500'}`} />
|
||||
AI优化建议
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{aiOptimizations.map((opt, index) => (
|
||||
<div key={index} className={`p-2 ${isDark ? 'bg-amber-500/10 border border-amber-500/30' : 'bg-amber-50 border border-amber-200'} rounded-lg`}>
|
||||
<p className={`${isDark ? 'text-slate-300' : 'text-gray-700'} text-xs mb-2`}>{opt.issue}</p>
|
||||
<div className="flex items-center gap-2">
|
||||
{opt.canFix && (
|
||||
<button className={`px-2 py-1 ${btnPrimary} text-white rounded text-xs transition-colors`}>
|
||||
一键修复
|
||||
</button>
|
||||
)}
|
||||
<button className={`px-2 py-1 ${btnSecondary} rounded text-xs transition-colors`}>
|
||||
忽略
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 卡片3:对话试用 */}
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-4 flex flex-col`}>
|
||||
<h3 className={`${textPrimary} font-semibold mb-3 flex items-center gap-2`}>
|
||||
<MessageSquare className={`w-4 h-4 ${isDark ? 'text-cyan-400' : 'text-cyan-600'}`} />
|
||||
对话试用
|
||||
</h3>
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="flex-1">
|
||||
<label className={`${textMuted} text-xs mb-1`}>模式</label>
|
||||
<select className={`w-full px-2 py-1.5 ${inputBg} ${inputBorder} rounded ${textPrimary} text-xs`}>
|
||||
<option>问答</option>
|
||||
<option>任务</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className={`${textSecondary} text-xs`}>数据源</span>
|
||||
<label className="relative inline-flex items-center cursor-pointer mx-1">
|
||||
<input type="checkbox" className="sr-only peer" defaultChecked />
|
||||
<div className={`w-9 h-5 ${isDark ? 'bg-slate-700' : 'bg-gray-300'} peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all ${isDark ? 'peer-checked:bg-green-600' : 'peer-checked:bg-green-500'}`}></div>
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className={`${textSecondary} text-xs`}>功能</span>
|
||||
<label className="relative inline-flex items-center cursor-pointer mx-1">
|
||||
<input type="checkbox" className="sr-only peer" defaultChecked />
|
||||
<div className={`w-9 h-5 ${isDark ? 'bg-slate-700' : 'bg-gray-300'} peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all ${isDark ? 'peer-checked:bg-green-600' : 'peer-checked:bg-green-500'}`}></div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`flex-1 min-h-[180px] max-h-[180px] overflow-y-auto p-3 ${inputBg} ${inputBorder} rounded-lg mb-3 space-y-3`}>
|
||||
{chatMessages.map((msg, index) => (
|
||||
<div key={index} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
|
||||
<div className={`max-w-[85%] p-2.5 rounded-lg text-xs ${
|
||||
msg.role === 'user'
|
||||
? isDark ? 'bg-blue-600 text-white' : 'bg-red-500 text-white'
|
||||
: isDark ? 'bg-slate-800 text-slate-300' : 'bg-gray-200 text-gray-800'
|
||||
}`}>
|
||||
{msg.content}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={chatInput}
|
||||
onChange={(e) => setChatInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSendMessage()}
|
||||
placeholder="请输入问题或任务要求..."
|
||||
className={`flex-1 px-3 py-2 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-xs focus:outline-none ${isDark ? 'focus:border-blue-500/50' : 'focus:border-red-400'}`}
|
||||
/>
|
||||
<button
|
||||
onClick={handleSendMessage}
|
||||
className={`px-3 py-2 ${btnPrimary} text-white rounded-lg transition-colors`}
|
||||
>
|
||||
<Send className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<button className={`mt-2 w-full px-3 py-1.5 ${btnSecondary} rounded text-xs transition-colors`}>
|
||||
插入示例任务
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 卡片4:本轮执行详情 */}
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-4`}>
|
||||
<h3 className={`${textPrimary} font-semibold mb-3 flex items-center gap-2`}>
|
||||
<Clock className={`w-4 h-4 ${isDark ? 'text-green-400' : 'text-green-600'}`} />
|
||||
本轮执行详情
|
||||
</h3>
|
||||
<div className="space-y-3 text-xs">
|
||||
<div>
|
||||
<p className={`${textMuted} mb-1.5`}>调用数据源</p>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{executionDetails.dataSources.map((ds) => (
|
||||
<span key={ds} className={`px-2 py-0.5 ${isDark ? 'bg-green-500/20 text-green-400' : 'bg-green-100 text-green-600'} rounded`}>{ds}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className={`${textMuted} mb-1.5`}>启用技能</p>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{executionDetails.skills.map((skill) => (
|
||||
<span key={skill} className={`px-2 py-0.5 ${isDark ? 'bg-purple-500/20 text-purple-400' : 'bg-purple-100 text-purple-600'} rounded`}>{skill}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className={`${textMuted} mb-1.5`}>调用功能</p>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{executionDetails.functions.map((func) => (
|
||||
<span key={func} className={`px-2 py-0.5 ${isDark ? 'bg-cyan-500/20 text-cyan-400' : 'bg-cyan-100 text-cyan-600'} rounded`}>{func}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className={`${textMuted} mb-1.5`}>输出字段</p>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{executionDetails.outputs.map((out) => (
|
||||
<span key={out} className={`px-2 py-0.5 ${isDark ? 'bg-blue-500/20 text-blue-400' : 'bg-blue-100 text-blue-600'} rounded font-mono`}>{out}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgentDetail;
|
||||
@@ -0,0 +1,404 @@
|
||||
// ============================================
|
||||
// AgentList.tsx - 智能体列表页面组件
|
||||
// ============================================
|
||||
// 功能描述:展示系统中所有智能体的列表,支持搜索、筛选、分页等功能
|
||||
// 主要特性:
|
||||
// - 智能体卡片展示(名称、描述、模型类型、状态等)
|
||||
// - 支持按类型和状态筛选
|
||||
// - 支持搜索智能体名称
|
||||
// - 分页功能
|
||||
// - 暗黑/亮色主题切换
|
||||
// - 查看、编辑、测试智能体操作
|
||||
// ============================================
|
||||
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
// 导入图标组件,用于界面中的各种图标显示
|
||||
import { Plus, Search, Eye, Edit, Play, Pause, Trash2, Bot, ChevronRight, GitBranch } from 'lucide-react';
|
||||
// 导入主题上下文,用于获取当前主题(暗黑/亮色)
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
// 导入页面类型定义,用于页面导航
|
||||
import { PageType } from '../components/Sidebar';
|
||||
// 导入请求服务,用于与后端 API 交互
|
||||
import { requestCrudService } from '@/service/request-axios-service';
|
||||
// 导入 TDesign 组件库的加载和分页组件
|
||||
import { Loading, Pagination } from 'tdesign-react';
|
||||
|
||||
/**
|
||||
* AgentItem - 智能体数据项接口定义
|
||||
* 描述单个智能体的数据结构
|
||||
*/
|
||||
interface AgentItem {
|
||||
avatarUrl: string; // 智能体头像 URL
|
||||
createdAt: string; // 创建时间
|
||||
description: string; // 智能体描述
|
||||
id: string; // 智能体唯一标识符
|
||||
modelName: string; // 使用的模型名称
|
||||
name: string; // 智能体名称
|
||||
systemPrompt: string; // 系统提示词
|
||||
userId: string; // 创建者用户 ID
|
||||
isDraft: boolean; // 是否为草稿状态
|
||||
}
|
||||
|
||||
/**
|
||||
* AgentListProps - AgentList 组件属性接口
|
||||
* 定义组件接收的外部属性
|
||||
*/
|
||||
interface AgentListProps {
|
||||
onViewDetail: (id: string) => void; // 查看智能体详情的回调函数
|
||||
onNavigate: (page: PageType) => void; // 页面导航的回调函数
|
||||
}
|
||||
|
||||
// 定义 API 基础 URL,从环境变量中获取,如果不存在则为空字符串
|
||||
const zncmUrl = import.meta.env.VITE_API_BASE_URL || '';
|
||||
// 创建请求服务实例,用于后续 API 调用
|
||||
const service = requestCrudService;
|
||||
|
||||
/**
|
||||
* AgentList - 智能体列表组件
|
||||
* 主组件函数,负责渲染智能体列表页面
|
||||
* @param onViewDetail - 查看详情的回调函数
|
||||
* @param onNavigate - 页面导航的回调函数
|
||||
*/
|
||||
const AgentList: React.FC<AgentListProps> = ({ onViewDetail, onNavigate }) => {
|
||||
// 获取当前主题配置
|
||||
const { theme } = useTheme();
|
||||
// 判断是否为暗黑模式
|
||||
const isDark = theme === 'dark';
|
||||
|
||||
// 智能体列表数据状态,存储所有智能体信息
|
||||
const [agents, setAgents] = useState<AgentItem[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
// 加载状态,用于显示加载动画
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
// 当前页码,默认为第 1 页
|
||||
const [pageNum, setPageNum] = useState<number>(1);
|
||||
// 每页显示数量,默认为 10 条
|
||||
const [pageSize, setPageSize] = useState<number>(10); // 每页显示 10 条
|
||||
// 数据总数,用于分页组件显示
|
||||
const [total, setTotal] = useState<number>(0);
|
||||
// 使用 ref 存储分页状态,避免组件重新渲染时丢失
|
||||
const state = useRef({
|
||||
pageNum: 1,
|
||||
pageSize: 10
|
||||
});
|
||||
|
||||
// 回车处理
|
||||
const handleKeyDown = async (e) => {
|
||||
if ((e.metaKey || e.ctrlKey || e.shiftKey) && e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
} else if (e.keyCode === 13) {
|
||||
e.preventDefault();
|
||||
await agentListSearch();
|
||||
} else if (e.key === "/") {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据智能体类型返回对应的标签颜色样式
|
||||
* 根据当前主题(暗黑/亮色)返回不同的颜色配置
|
||||
* @param category - 智能体类型(如 GLM-5、Qwen3.5-27B、叙事生成等)
|
||||
* @returns 返回对应的 CSS 类名,用于标签背景色和文字颜色
|
||||
*/
|
||||
const getCategoryColor = (category: string) => {
|
||||
// 暗黑模式下的颜色配置
|
||||
if (isDark) {
|
||||
switch (category) {
|
||||
case 'GLM-5':
|
||||
return 'bg-blue-500/20 text-blue-400'; // 蓝色背景 + 蓝色文字
|
||||
case 'Qwen3.5-27B':
|
||||
return 'bg-cyan-500/20 text-cyan-400'; // 青色背景 + 青色文字
|
||||
case 'Qwen3-32B':
|
||||
return 'bg-purple-500/20 text-purple-400'; // 紫色背景 + 紫色文字
|
||||
case '叙事生成':
|
||||
return 'bg-pink-500/20 text-pink-400'; // 粉色背景 + 粉色文字
|
||||
case '逻辑链构建':
|
||||
return 'bg-amber-500/20 text-amber-400'; // 琥珀色背景 + 琥珀色文字
|
||||
case '风险评估':
|
||||
return 'bg-red-500/20 text-red-400'; // 红色背景 + 红色文字
|
||||
case '方案生成':
|
||||
return 'bg-green-500/20 text-green-400'; // 绿色背景 + 绿色文字
|
||||
default:
|
||||
return 'bg-slate-500/20 text-slate-400'; // 默认灰色
|
||||
}
|
||||
} else {
|
||||
// 亮色模式下的颜色配置,统一使用红色系
|
||||
switch (category) {
|
||||
case 'GLM-5':
|
||||
return 'bg-red-100 text-red-600';
|
||||
case 'Qwen3.5-27B':
|
||||
return 'bg-red-100 text-red-600';
|
||||
case 'Qwen3-32B':
|
||||
return 'bg-red-100 text-red-600';
|
||||
case '叙事生成':
|
||||
return 'bg-red-100 text-red-600';
|
||||
case '逻辑链构建':
|
||||
return 'bg-red-100 text-red-600';
|
||||
case '风险评估':
|
||||
return 'bg-red-100 text-red-600';
|
||||
case '方案生成':
|
||||
return 'bg-red-100 text-red-600';
|
||||
default:
|
||||
return 'bg-gray-100 text-gray-600'; // 默认灰色
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据智能体类型返回对应的图标颜色样式
|
||||
* 用于智能体卡片左侧图标的背景色和图标颜色
|
||||
* @param type - 智能体类型
|
||||
* @returns 返回包含背景色(bg)和图标颜色(icon)的对象
|
||||
*/
|
||||
const getTypeIconColor = (type: string) => {
|
||||
// 暗黑模式下的图标颜色配置
|
||||
if (isDark) {
|
||||
switch (type) {
|
||||
case 'GLM-5':
|
||||
return { bg: 'bg-blue-500/20', icon: 'text-blue-400' }; // 蓝色系
|
||||
case 'Qwen3.5-27B':
|
||||
return { bg: 'bg-cyan-500/20', icon: 'text-cyan-400' }; // 青色系
|
||||
case 'Qwen3-32B':
|
||||
return { bg: 'bg-pink-500/20', icon: 'text-pink-400' }; // 粉色系
|
||||
case '校验类':
|
||||
return { bg: 'bg-amber-500/20', icon: 'text-amber-400' }; // 琥珀色系
|
||||
case '成稿类':
|
||||
return { bg: 'bg-green-500/20', icon: 'text-green-400' }; // 绿色系
|
||||
default:
|
||||
return { bg: 'bg-slate-500/20', icon: 'text-slate-400' }; // 默认灰色
|
||||
}
|
||||
} else {
|
||||
// 亮色模式下统一使用红色系
|
||||
return { bg: 'bg-red-100', icon: 'text-red-500' };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取智能体列表数据
|
||||
* 调用后端 API 获取智能体列表,并更新组件状态
|
||||
* 请求参数包括页码、每页数量和搜索名称
|
||||
*/
|
||||
const agentListSearch = async () => {
|
||||
// 构建请求参数对象
|
||||
const parmy = {
|
||||
pageNum: state.current.pageNum, // 当前页码
|
||||
pageSize: state.current.pageSize, // 每页显示数量
|
||||
name: searchQuery // 搜索名称(当前为空,可扩展)
|
||||
}
|
||||
// 开始加载,显示加载动画
|
||||
setLoading(true);
|
||||
// 调用后端 API 获取智能体列表数据
|
||||
const res = await requestCrudService.post(`${zncmUrl}/agent/expertAgents/list`, parmy);
|
||||
// 加载完成,隐藏加载动画
|
||||
setLoading(false);
|
||||
// 打印响应数据到控制台,用于调试
|
||||
console.log('智能体列表', res);
|
||||
// 解析响应数据
|
||||
const result = res.data;
|
||||
// 更新数据总数,用于分页
|
||||
setTotal(result.total);
|
||||
// 更新智能体列表数据,如果无数据则设置为空数组
|
||||
setAgents(result.records || []);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 页码变化时的回调函数
|
||||
* 当用户切换页码时触发,更新页码并重新获取数据
|
||||
* @param value - 新的页码值
|
||||
*/
|
||||
const pageIndexChange = async (value: number) => {
|
||||
// 更新 ref 中存储的页码值
|
||||
state.current.pageNum = value;
|
||||
// 更新页码状态,触发组件重新渲染
|
||||
setPageNum(value);
|
||||
// 重新获取智能体列表数据
|
||||
await agentListSearch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 每页显示数量变化时的回调函数
|
||||
* 当用户切换每页显示条数时触发,更新每页数量并重新获取数据
|
||||
* @param value - 新的每页显示数量
|
||||
*/
|
||||
const pageSizeChange = async (value: number) => {
|
||||
// 更新 ref 中存储的每页显示数量
|
||||
state.current.pageSize = value;
|
||||
// 更新每页显示数量状态,触发组件重新渲染
|
||||
setPageSize(value);
|
||||
// 重新获取智能体列表数据
|
||||
await agentListSearch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件挂载时的副作用
|
||||
* 组件首次渲染时自动调用一次获取智能体列表
|
||||
* 依赖数组为空,表示只在组件挂载时执行一次
|
||||
*/
|
||||
useEffect(() => {
|
||||
agentListSearch();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 页面标题区 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className={`text-2xl font-bold mb-2 ${isDark ? 'text-white' : 'text-gray-900'}`}>智能体配置管理</h1>
|
||||
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>统一管理系统中的智能体配置</p>
|
||||
</div>
|
||||
<button className={`flex items-center gap-2 px-4 py-2.5 text-white rounded-lg transition-colors shadow-md ${isDark
|
||||
? 'bg-blue-600 hover:bg-blue-500'
|
||||
: 'bg-red-600 hover:bg-red-500'
|
||||
}`}>
|
||||
<Plus className="w-4 h-4" />
|
||||
<span className="text-sm font-medium">新建智能体</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 搜索和筛选区 */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative flex-1 max-w-md">
|
||||
<Search className={`absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 ${isDark ? 'text-slate-500' : 'text-red-400'}`} />
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onKeyDown={(e) => handleKeyDown(e)}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="搜索智能体名称..."
|
||||
className={`w-full pl-10 pr-4 py-2.5 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-red-500/20 transition-colors ${isDark
|
||||
? 'bg-slate-800/50 border-blue-800/30 text-white placeholder-slate-500'
|
||||
: 'bg-white border-red-200 text-gray-900 placeholder-red-300'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
<button className={`px-4 py-2.5 border rounded-lg text-sm transition-colors ${isDark
|
||||
? 'bg-slate-800/50 border-blue-800/30 text-slate-300 hover:bg-slate-700/50'
|
||||
: 'bg-white border-red-200 text-gray-700 hover:bg-red-50'
|
||||
}`}>
|
||||
全部类型
|
||||
</button>
|
||||
<button className={`px-4 py-2.5 border rounded-lg text-sm transition-colors ${isDark
|
||||
? 'bg-slate-800/50 border-blue-800/30 text-slate-300 hover:bg-slate-700/50'
|
||||
: 'bg-white border-red-200 text-gray-700 hover:bg-red-50'
|
||||
}`}>
|
||||
全部状态
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 分类标签 */}
|
||||
<div className="flex items-center gap-3">
|
||||
<span className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>按类型筛选:</span>
|
||||
{['编排类', '分析类', '叙事类', '校验类', '成稿类'].map((type) => (
|
||||
<button
|
||||
key={type}
|
||||
className={`px-3 py-1.5 border rounded-full text-sm transition-colors ${isDark
|
||||
? 'bg-slate-800/50 border-blue-800/30 text-slate-300 hover:bg-blue-600/20 hover:border-blue-500/50 hover:text-blue-400'
|
||||
: 'bg-white border-red-200 text-gray-700 hover:bg-red-50 hover:border-red-300 hover:text-red-600'
|
||||
}`}
|
||||
>
|
||||
{type}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 智能体列表 */}
|
||||
<Loading loading={loading}>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{agents.map((agent) => {
|
||||
const typeColors = getTypeIconColor(agent.modelName);
|
||||
return (
|
||||
<div
|
||||
key={agent.id}
|
||||
className={`rounded-xl p-5 transition-all duration-300 group cursor-pointer border theme-card ${isDark
|
||||
? 'bg-slate-800/30 backdrop-blur-xl border-blue-800/30 hover:border-blue-500/50'
|
||||
: 'bg-white border-red-100 hover:border-red-300 hover:shadow-md'
|
||||
}`}
|
||||
onClick={() => {
|
||||
onViewDetail(agent.id);
|
||||
onNavigate('agent-detail');
|
||||
}}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-12 h-12 rounded-xl flex items-center justify-center ${typeColors.bg}`}>
|
||||
<Bot className={`w-6 h-6 ${typeColors.icon}`} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className={`font-semibold text-lg group-hover:text-red-500 transition-colors ${isDark ? 'text-white' : 'text-gray-900'}`}>{agent.name}</h3>
|
||||
<p className={`text-sm ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>{agent.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium ${agent.isDraft
|
||||
? isDark
|
||||
? 'bg-green-500/20 text-green-400'
|
||||
: 'bg-green-100 text-green-600'
|
||||
: isDark
|
||||
? 'bg-yellow-500/20 text-yellow-400'
|
||||
: 'bg-yellow-100 text-yellow-600'
|
||||
}`}
|
||||
>
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${agent.isDraft
|
||||
? isDark ? 'bg-green-400' : 'bg-green-500'
|
||||
: isDark ? 'bg-yellow-400' : 'bg-yellow-500'
|
||||
}`} />
|
||||
{agent.isDraft ? '启用' : '暂停'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<span className={`px-2.5 py-1 rounded-full text-xs font-medium ${getCategoryColor(agent.modelName)}`}>
|
||||
{agent.modelName}
|
||||
</span>
|
||||
{/* <span className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>{agent.description}</span> */}
|
||||
</div>
|
||||
|
||||
<div className={`flex items-center justify-between pt-4 border-t ${isDark ? 'border-blue-800/30' : 'border-red-100'
|
||||
}`}>
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="overflow-hidden text-ellipsis line-clamp-3">
|
||||
{/* <p className={`text-xs mb-1 ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>输入字段</p> */}
|
||||
<p className={`font-medium ${isDark ? 'text-cyan-400' : 'text-red-600'}`}>{agent.systemPrompt} 个</p>
|
||||
</div>
|
||||
{/* <div>
|
||||
<p className={`text-xs mb-1 ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>输出字段</p>
|
||||
<p className={`font-medium ${isDark ? 'text-green-400' : 'text-red-600'}`}>{agent.outputs} 个</p>
|
||||
</div> */}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button className={`p-2 rounded-lg transition-colors group/btn ${isDark ? 'hover:bg-slate-700/50' : 'hover:bg-red-50'
|
||||
}`} title="查看">
|
||||
<Eye className={`w-4 h-4 ${isDark ? 'text-slate-400 group-hover/btn:text-white' : 'text-red-400 group-hover/btn:text-red-600'}`} />
|
||||
</button>
|
||||
<button className={`p-2 rounded-lg transition-colors group/btn ${isDark ? 'hover:bg-slate-700/50' : 'hover:bg-red-50'
|
||||
}`} title="编辑">
|
||||
<Edit className={`w-4 h-4 ${isDark ? 'text-slate-400 group-hover/btn:text-white' : 'text-red-400 group-hover/btn:text-red-600'}`} />
|
||||
</button>
|
||||
<button className={`p-2 rounded-lg transition-colors group/btn ${isDark ? 'hover:bg-green-500/20' : 'hover:bg-green-50'
|
||||
}`} title="测试">
|
||||
<Play className={`w-4 h-4 ${isDark ? 'text-slate-400 group-hover/btn:text-green-400' : 'text-red-400 group-hover/btn:text-green-600'}`} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Loading>
|
||||
|
||||
{/* 分页 */}
|
||||
<div className={`flex items-center justify-between pt-4 ${isDark ? '' : 'border-t border-red-100'}`}>
|
||||
<Pagination
|
||||
total={total}
|
||||
current={pageNum}
|
||||
pageSize={pageSize}
|
||||
onCurrentChange={(page) => pageIndexChange(Number(page))}
|
||||
onPageSizeChange={(size) => pageSizeChange(Number(size))} // 每页数量变化时的回调
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgentList;
|
||||
@@ -0,0 +1,601 @@
|
||||
"use client";
|
||||
|
||||
import { toolsCrudService } from "@/service/request-tools-service";
|
||||
import {
|
||||
Sparkles,
|
||||
ArrowRight,
|
||||
Plus,
|
||||
Bot,
|
||||
Trash2,
|
||||
Loader2,
|
||||
Pencil,
|
||||
Info,
|
||||
MessageCircle,
|
||||
Eye,
|
||||
EyeOff,
|
||||
} from "lucide-react";
|
||||
import { useAtomValue } from "jotai";
|
||||
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { toast } from "sonner";
|
||||
import { Button, Dialog, Switch } from "tdesign-react";
|
||||
import { toolsUser, useChatContext } from "@/contexts/ChatContext";
|
||||
interface ExpertAgent {
|
||||
id: number,
|
||||
name: string,
|
||||
avatar_url: string,
|
||||
description: string,
|
||||
system_prompt: string,
|
||||
model_name: string,
|
||||
user_id: string,
|
||||
created_at: string,
|
||||
is_draft: boolean,
|
||||
}
|
||||
|
||||
interface expert {
|
||||
id: number;
|
||||
name: string;
|
||||
avatar_url: string | null;
|
||||
description: string | null;
|
||||
system_prompt: string;
|
||||
model_name: string | null;
|
||||
user_id: string;
|
||||
created_at: string;
|
||||
is_draft: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将纯 SVG 字符串转换为 data URL
|
||||
*/
|
||||
export const svgToDataUrl = (svg: string) => {
|
||||
const encoded = encodeURIComponent(svg)
|
||||
.replace(/'/g, '%27')
|
||||
.replace(/"/g, '%22');
|
||||
return `data:image/svg+xml;charset=utf-8,${encoded}`;
|
||||
}
|
||||
|
||||
const backendUrl = import.meta.env.VITE_AIPI_BACKEND_URL || '';
|
||||
const service = toolsCrudService
|
||||
|
||||
// 专家详情模态框组件
|
||||
function ExpertDetailDialog({
|
||||
expert,
|
||||
open,
|
||||
onOpenChange,
|
||||
agentSearch,
|
||||
}: {
|
||||
expert: ExpertAgent;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
agentSearch?: () => void;
|
||||
}) {
|
||||
const router = useNavigate();
|
||||
const [isDraft, setIsDraft] = useState<boolean>(expert.is_draft || true);
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
|
||||
const isSvg = expert.avatar_url
|
||||
? /<svg[\s\S]*<\/svg>/i.test(expert.avatar_url)
|
||||
: false;
|
||||
const avatarSrc =
|
||||
isSvg && expert.avatar_url
|
||||
? svgToDataUrl(expert.avatar_url)
|
||||
: expert.avatar_url;
|
||||
|
||||
const gradients = [
|
||||
{ from: "rose-500", to: "pink-500" },
|
||||
{ from: "violet-500", to: "purple-500" },
|
||||
{ from: "amber-500", to: "orange-500" },
|
||||
{ from: "emerald-500", to: "teal-500" },
|
||||
{ from: "blue-500", to: "indigo-500" },
|
||||
{ from: "cyan-500", to: "sky-500" },
|
||||
];
|
||||
const gradient = gradients[expert.id % gradients.length];
|
||||
|
||||
const handleTogglePublished = async (checked: boolean) => {
|
||||
// checked 为 true 表示发布(is_draft = false),checked 为 false 表示草稿(is_draft = true)
|
||||
const newDraftStatus: boolean = !checked;
|
||||
setIsUpdating(true);
|
||||
try {
|
||||
const response = await service.patch(
|
||||
`${backendUrl}/api/v1/expert-agents/${expert.id}`,
|
||||
{ is_draft: newDraftStatus }
|
||||
);
|
||||
|
||||
setIsDraft(newDraftStatus);
|
||||
toast.success(newDraftStatus ? "已设为草稿" : "已发布");
|
||||
await agentSearch();
|
||||
} catch (error) {
|
||||
console.error("Toggle publish error:", error);
|
||||
toast.error("操作失败");
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStartChat = () => {
|
||||
onOpenChange(false);
|
||||
router(`/agents/new-chat?expert_id=${expert.id}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
visible={open}
|
||||
header="专家详情"
|
||||
width="520px"
|
||||
confirmBtn="保存"
|
||||
cancelBtn="取消"
|
||||
onConfirm={() => onOpenChange(false)}
|
||||
onClose={() => onOpenChange(false)}
|
||||
>
|
||||
<div className="flex flex-col items-center space-y-4 py-4">
|
||||
{/* 头像 */}
|
||||
<div
|
||||
className={`w-20 h-20 rounded-2xl bg-gradient-to-br from-${gradient.from} to-${gradient.to} flex items-center justify-center shadow-lg`}
|
||||
>
|
||||
{avatarSrc ? (
|
||||
<img
|
||||
src={avatarSrc}
|
||||
alt={expert.name}
|
||||
className="w-12 h-12 rounded-xl object-contain"
|
||||
/>
|
||||
) : (
|
||||
<Bot className="w-10 h-10 text-white" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 名称和描述 */}
|
||||
<div className="text-center">
|
||||
<h3 className="text-xl font-semibold text-gray-900">
|
||||
{expert.name}
|
||||
</h3>
|
||||
{expert.description ? (
|
||||
<p className="mt-2 text-sm text-gray-500">{expert.description}</p>
|
||||
) : (
|
||||
<p className="mt-2 text-sm text-gray-400 italic">暂无描述</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 发布开关 */}
|
||||
<div className="flex items-center justify-between w-full px-4 py-3 bg-gray-50 rounded-xl">
|
||||
<div className="flex items-center gap-2">
|
||||
{isDraft ? (
|
||||
<EyeOff className="w-4 h-4 text-gray-400" />
|
||||
) : (
|
||||
<Eye className="w-4 h-4 text-gray-600" />
|
||||
)}
|
||||
<span className="text-sm text-gray-700">
|
||||
{isDraft ? "草稿状态" : "已发布"}
|
||||
</span>
|
||||
</div>
|
||||
<Switch
|
||||
value={!isDraft}
|
||||
onChange={handleTogglePublished}
|
||||
disabled={isUpdating}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 开始聊天按钮 */}
|
||||
<Button
|
||||
onClick={handleStartChat}
|
||||
className="w-full bg-gradient-to-r border-[bg-gradient-to-r] from-violet-500 to-purple-500 hover:from-violet-600 hover:to-purple-600"
|
||||
>
|
||||
<MessageCircle className="w-4 h-4 mr-2" />
|
||||
开始聊天
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function ExpertCard({ expert, changeState }: { expert: ExpertAgent, changeState: () => void }) {
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [showDetail, setShowDetail] = useState(false);
|
||||
|
||||
const isSvg = expert.avatar_url
|
||||
? /<svg[\s\S]*<\/svg>/i.test(expert.avatar_url)
|
||||
: false;
|
||||
// 如果是纯 SVG,转换为 data URL;否则直接使用
|
||||
const avatarSrc =
|
||||
isSvg && expert.avatar_url
|
||||
? svgToDataUrl(expert.avatar_url)
|
||||
: expert.avatar_url;
|
||||
const router = useNavigate();
|
||||
|
||||
const handleDelete = async (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (!confirm(`确定要删除专家 "${expert.name}" 吗?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
await service.delete(`${backendUrl}/api/v1/expert-agents/${expert.id}`);
|
||||
await changeState();
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Generate consistent gradient based on expert id
|
||||
const gradients = [
|
||||
{ from: "rose-500", to: "pink-500", shadow: "rose-500" },
|
||||
{ from: "violet-500", to: "purple-500", shadow: "violet-500" },
|
||||
{ from: "amber-500", to: "orange-500", shadow: "amber-500" },
|
||||
{ from: "emerald-500", to: "teal-500", shadow: "emerald-500" },
|
||||
{ from: "blue-500", to: "indigo-500", shadow: "blue-500" },
|
||||
{ from: "cyan-500", to: "sky-500", shadow: "cyan-500" },
|
||||
];
|
||||
const gradient = gradients[expert.id % gradients.length];
|
||||
|
||||
return (
|
||||
<div
|
||||
className="rounded-lg border text-card-foreground shadow-sm group relative h-full cursor-pointer overflow-hidden border-gray-200/60 book-bg-yellow bg-white transition-all duration-300 hover:border-gray-300 hover:shadow-xl hover:shadow-gray-200/40"
|
||||
>
|
||||
<div
|
||||
className="group relative h-full p-[20px] cursor-pointer overflow-hidden border-gray-200/60 book-bg-yellow bg-white transition-all duration-300 hover:border-gray-300 hover:shadow-xl hover:shadow-gray-200/40"
|
||||
onClick={() => router(`/agents/new-chat?expert_id=${expert.id}`)}
|
||||
>
|
||||
{/* Subtle gradient overlay on hover */}
|
||||
<div
|
||||
className={`absolute inset-0 bg-gradient-to-br from-${gradient.from}/5 to-${gradient.to}/5 opacity-0 transition-opacity duration-300 group-hover:opacity-100 pointer-events-none`}
|
||||
/>
|
||||
|
||||
<div className="relative pb-4">
|
||||
<div className="flex items-start justify-between">
|
||||
{/* Enhanced icon with animated gradient background */}
|
||||
<div className="relative">
|
||||
<div
|
||||
className={`w-14 h-14 rounded-2xl bg-gradient-to-br from-${gradient.from} to-${gradient.to} flex items-center justify-center shadow-lg shadow-${gradient.shadow}/30 transition-all duration-300 group-hover:scale-110 group-hover:shadow-xl group-hover:shadow-${gradient.shadow}/40`}
|
||||
>
|
||||
{avatarSrc ? (
|
||||
// avatarSrc 已经是正确的 URL 格式(纯 SVG 已转换为 data URL)
|
||||
<img
|
||||
src={avatarSrc}
|
||||
alt={expert.name}
|
||||
className="w-9 h-9 rounded-xl object-contain"
|
||||
/>
|
||||
) : (
|
||||
<Bot className="w-7 h-7 text-white" />
|
||||
)}
|
||||
</div>
|
||||
{/* Glowing effect on hover */}
|
||||
<div
|
||||
className={`absolute inset-0 rounded-2xl bg-gradient-to-br from-${gradient.from} to-${gradient.to} opacity-0 blur-xl transition-opacity duration-300 group-hover:opacity-30 -z-10`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Enhanced action buttons with larger click areas */}
|
||||
<div className="flex items-center gap-1">
|
||||
{expert.user_id && (
|
||||
<>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setShowDetail(true);
|
||||
}}
|
||||
className="group/btn relative flex h-10 min-w-[40px] items-center justify-center rounded-xl text-gray-400 transition-all duration-300 hover:bg-gray-100 hover:text-gray-600"
|
||||
title="查看详情"
|
||||
>
|
||||
<Info className="w-4 h-4 transition-transform duration-200 group-hover/btn:scale-110" />
|
||||
</button>
|
||||
<span
|
||||
className="group/btn relative flex h-10 min-w-[40px] items-center justify-center rounded-xl text-gray-400 transition-all duration-300 hover:bg-blue-50 hover:text-blue-600"
|
||||
onClick={(e) => {e.stopPropagation(); router(`/agents/new-expert/${expert.id}`)}}
|
||||
title="编辑专家"
|
||||
>
|
||||
<Pencil className="w-4 h-4 transition-transform duration-200 group-hover/btn:scale-110" />
|
||||
</span>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleDelete(e);
|
||||
}}
|
||||
disabled={isDeleting}
|
||||
className="group/btn relative flex h-10 min-w-[40px] items-center justify-center rounded-xl text-gray-400 transition-all duration-300 hover:bg-red-50 hover:text-red-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
title="删除专家"
|
||||
>
|
||||
{isDeleting ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<Trash2 className="w-4 h-4 transition-transform duration-200 group-hover/btn:scale-110" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative pb-4 pt-0">
|
||||
{/* 发布状态标识 */}
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
{/* Enhanced title with better typography */}
|
||||
<div className="text-lg font-semibold text-gray-900 transition-colors duration-200 group-hover:text-gray-900 line-clamp-1 flex-1">
|
||||
{expert.name}
|
||||
</div>
|
||||
{/* 发布状态徽章 */}
|
||||
{!expert.is_draft ? (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-emerald-50 text-emerald-700 border border-emerald-200">
|
||||
<Eye className="w-3 h-3" />
|
||||
已发布
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-500 border border-gray-200">
|
||||
<EyeOff className="w-3 h-3" />
|
||||
草稿
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Enhanced description with better readability */}
|
||||
{expert.description ? (
|
||||
<div className="mt-2 text-sm text-gray-500 leading-relaxed line-clamp-2">
|
||||
{expert.description}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-2 text-sm text-gray-400 italic line-clamp-2">
|
||||
暂无描述
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Interactive indicator */}
|
||||
<div className="mt-4 flex items-center gap-2 text-xs font-medium text-gray-400 transition-all duration-300 group-hover:text-gray-600">
|
||||
<span>点击开始对话</span>
|
||||
<ArrowRight className="w-3 h-3 transition-transform duration-300 group-hover:translate-x-1" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom accent bar that animates on hover */}
|
||||
<div
|
||||
className={`absolute bottom-0 left-0 right-0 h-1 bg-gradient-to-r from-${gradient.from} to-${gradient.to} transform scale-x-0 transition-transform duration-300 group-hover:scale-x-100 origin-left`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 详情模态框 */}
|
||||
<ExpertDetailDialog
|
||||
expert={expert}
|
||||
open={showDetail}
|
||||
onOpenChange={(open) => {
|
||||
setShowDetail(open);
|
||||
changeState()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LoadingSkeleton() {
|
||||
return (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="h-48 rounded-2xl bg-gray-100 animate-pulse" />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ExpertsAgentsPage() {
|
||||
const {toolsUser, setToolsUser} = useChatContext()
|
||||
const router = useNavigate();
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [llmModels, setLlmModels] = useState<{ id: string; name: string }[]>(
|
||||
[]
|
||||
);
|
||||
const [activeTab, setActiveTab] = useState<"mine" | "published">("mine");
|
||||
const queryClient = '';
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isError, setIsError] = useState(false);
|
||||
const [experts, setExperts] = useState<ExpertAgent[]>([]);
|
||||
const toolsUserData = useRef<toolsUser | null>(null)
|
||||
const filteredExperts = [];
|
||||
|
||||
// 对于"我的专家",需要根据 user_id 过滤(因为后端返回所有专家)
|
||||
// 获取可用的 LLM 模型列表
|
||||
useEffect(() => {
|
||||
toolsLoginSearch();
|
||||
}, [activeTab]);
|
||||
const toolsLoginSearch = async () => {
|
||||
const formData = new FormData();
|
||||
formData.append('username', 'liuqiang@qq.com');
|
||||
formData.append('password', '123456');
|
||||
formData.append('grant_type', 'password');
|
||||
const res: any = await fetch(`${backendUrl}/auth/jwt/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
},
|
||||
body: formData,
|
||||
}).then(res => {
|
||||
return res.json();
|
||||
});
|
||||
console.log('登录信息', res);
|
||||
localStorage.setItem('toolToken', res.access_token);
|
||||
await searchMeChange();
|
||||
await expertAgentSearh();
|
||||
await llmSearch();
|
||||
}
|
||||
|
||||
const llmSearch = async () => {
|
||||
const res = await service.get(`${backendUrl}/api/v1/expert-agents/llms`);
|
||||
console.log('模型',res);
|
||||
const result = JSON.parse(JSON.stringify(res));
|
||||
setLlmModels(result)
|
||||
}
|
||||
|
||||
const searchMeChange = async () => {
|
||||
const res = await service.get(`${backendUrl}/users/me`, '');
|
||||
const result =JSON.parse(JSON.stringify(res));
|
||||
toolsUserData.current = result;
|
||||
setToolsUser(result as toolsUser);
|
||||
}
|
||||
const expertAgentSearh = async () => {
|
||||
const param = {
|
||||
is_draft: activeTab === 'mine'
|
||||
}
|
||||
const res: any = await service.post(`${backendUrl}/api/v1/expert-agents/list`, param);
|
||||
console.log('res', res);
|
||||
const result = res.filter(item => item.user_id === toolsUserData.current.id);
|
||||
console.log(result);
|
||||
setExperts(result);
|
||||
}
|
||||
const handleCreateExpert = async () => {
|
||||
setIsCreating(true);
|
||||
try {
|
||||
const request = {
|
||||
name: "未命名专家",
|
||||
model_name: llmModels[0]?.name ?? null,
|
||||
system_prompt: "你是一个专业的助手,能够回答用户的问题。",
|
||||
};
|
||||
const res: any = await service.post(`${backendUrl}/api/v1/expert-agents`, request);
|
||||
// 跳转到编辑页面
|
||||
// router(`/agents/new-expert/${res.id}`);
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white">
|
||||
{/* Header */}
|
||||
<div
|
||||
className="px-8 py-12 border-b border-gray-100"
|
||||
>
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-violet-500 to-purple-500 flex items-center justify-center shadow-lg shadow-violet-500/20">
|
||||
<Sparkles className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<h1 className="text-3xl font-semibold text-gray-900">我的专家</h1>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleCreateExpert}
|
||||
disabled={isCreating}
|
||||
className="inline-flex items-center gap-2 px-5 py-2.5 bg-gray-900 dark-theme-btn text-white rounded-xl text-sm font-medium hover:bg-gray-800 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isCreating ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Plus className="w-4 h-4" />
|
||||
)}
|
||||
创建专家
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-gray-500 mb-6">管理你的专属 AI 专家助手</p>
|
||||
|
||||
{/* 标签切换 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
setActiveTab("mine")
|
||||
}}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all ${activeTab === "mine"
|
||||
? "bg-gray-900 text-white"
|
||||
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
|
||||
}`}
|
||||
>
|
||||
我的专家
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setActiveTab("published");
|
||||
}}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all ${activeTab === "published"
|
||||
? "bg-gray-900 text-white"
|
||||
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
|
||||
}`}
|
||||
>
|
||||
发布的专家
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main content */}
|
||||
<div className="px-8 py-12">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{isLoading ? (
|
||||
<LoadingSkeleton />
|
||||
) : isError ? (
|
||||
<div className="text-center py-16 text-red-500">
|
||||
<p>加载失败: {"未知错误"}</p>
|
||||
</div>
|
||||
) : experts.length === 0 ? (
|
||||
<div className="text-center py-16">
|
||||
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-gray-100 flex items-center justify-center">
|
||||
<Bot className="w-8 h-8 text-gray-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
{activeTab === "mine" ? "暂无我的专家" : "暂无已发布的专家"}
|
||||
</h3>
|
||||
<p className="text-gray-500 mb-6">
|
||||
{activeTab === "mine"
|
||||
? "创建你的第一个专家助手,开始高效工作"
|
||||
: "还没有专家发布到广场"}
|
||||
</p>
|
||||
{activeTab === "mine" && (
|
||||
<button
|
||||
onClick={handleCreateExpert}
|
||||
disabled={isCreating}
|
||||
className="inline-flex items-center gap-2 px-6 py-3 bg-gray-900 text-white rounded-xl font-medium hover:bg-gray-800 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isCreating ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Plus className="w-4 h-4" />
|
||||
)}
|
||||
创建第一个专家
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{experts.map((expert, index) => (
|
||||
<div
|
||||
key={expert.id}
|
||||
>
|
||||
<ExpertCard expert={expert} changeState={expertAgentSearh} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick start section */}
|
||||
<div
|
||||
className="px-8 pb-12"
|
||||
>
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="rounded-2xl border border-gray-100 bg-gradient-to-br dark-item-card from-violet-50 to-purple-50 p-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-2">
|
||||
不确定从哪个开始?
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
直接开始对话,AI 会自动匹配最合适的专家助手
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="inline-flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-violet-500 to-purple-500 text-white rounded-xl font-medium hover:from-violet-600 hover:to-purple-600 hover:shadow-lg hover:shadow-violet-500/25 transition-all duration-300"
|
||||
>
|
||||
开始对话
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,636 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Send,
|
||||
Search,
|
||||
History,
|
||||
Bot,
|
||||
Sparkles,
|
||||
Database,
|
||||
Layers,
|
||||
FileText,
|
||||
Bookmark,
|
||||
ChevronRight,
|
||||
Cpu,
|
||||
Target,
|
||||
MessageSquare,
|
||||
Zap,
|
||||
CheckCircle,
|
||||
} from 'lucide-react';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { PageType } from '../components/Sidebar';
|
||||
|
||||
interface Agent {
|
||||
id: string;
|
||||
name: string;
|
||||
category: string;
|
||||
tags: string[];
|
||||
description: string;
|
||||
skills: string[];
|
||||
dataSources: string[];
|
||||
functions: string[];
|
||||
outputFields: string[];
|
||||
}
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
timestamp: Date;
|
||||
agentName?: string;
|
||||
skills?: string[];
|
||||
dataSources?: string[];
|
||||
functions?: string[];
|
||||
structuredOutput?: Record<string, any>;
|
||||
}
|
||||
|
||||
const AgentQA: React.FC<{ onNavigate: (page: PageType) => void }> = ({ onNavigate }) => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
|
||||
// 模拟智能体列表
|
||||
const agents: Agent[] = [
|
||||
{
|
||||
id: 'audience-agent',
|
||||
name: 'AudienceAgent',
|
||||
category: '受众分析',
|
||||
tags: ['受众分层', '画像抽取', '行为预测'],
|
||||
description: '专业的受众分析智能体,能够识别目标受众特征并生成画像',
|
||||
skills: ['受众分层', '核心受众识别', '画像抽取', '行为预测'],
|
||||
dataSources: ['知识库', 'Y系统', 'A系统'],
|
||||
functions: ['信息抽取器', '标签聚类器', '结构化输出器'],
|
||||
outputFields: ['core_audience', 'key_traits', 'barriers', 'opportunities'],
|
||||
},
|
||||
{
|
||||
id: 'brand-agent',
|
||||
name: 'BrandAgent',
|
||||
category: '品牌分析',
|
||||
tags: ['品牌定位', '传播策略', '调性分析'],
|
||||
description: '专注品牌建设与传播策略的智能体',
|
||||
skills: ['品牌定位分析', '传播路径规划', '品牌调性设计'],
|
||||
dataSources: ['知识库', '行业数据库'],
|
||||
functions: ['品牌诊断器', '策略生成器'],
|
||||
outputFields: ['brand_position', 'communication_path', 'tone_guide'],
|
||||
},
|
||||
{
|
||||
id: 'content-agent',
|
||||
name: 'ContentAgent',
|
||||
category: '内容创作',
|
||||
tags: ['文案撰写', '创意生成', '多形式内容'],
|
||||
description: '智能内容创作助手,支持多种形式的内容生成',
|
||||
skills: ['文案撰写', '创意生成', '多形式内容', 'A/B测试文案'],
|
||||
dataSources: ['知识库', '优秀案例库'],
|
||||
functions: ['文案生成器', '风格适配器', '质量评估器'],
|
||||
outputFields: ['headlines', 'body_copy', 'call_to_action', 'variants'],
|
||||
},
|
||||
{
|
||||
id: 'channel-agent',
|
||||
name: 'ChannelAgent',
|
||||
category: '渠道规划',
|
||||
tags: ['渠道选择', '投放策略', '效果评估'],
|
||||
description: '专业的渠道分析与规划智能体',
|
||||
skills: ['渠道选择', '投放策略规划', '效果评估', '预算分配'],
|
||||
dataSources: ['Y系统', 'A系统', '行业数据'],
|
||||
functions: ['渠道分析器', '投放优化器', 'ROI预测器'],
|
||||
outputFields: ['recommended_channels', 'budget_allocation', 'expected_roi'],
|
||||
},
|
||||
];
|
||||
|
||||
// 状态
|
||||
const [selectedAgent, setSelectedAgent] = useState<Agent>(agents[0]);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// 过滤智能体
|
||||
const filteredAgents = agents.filter(agent =>
|
||||
agent.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
agent.category.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
agent.tags.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
);
|
||||
|
||||
// 处理发送消息
|
||||
const handleSend = () => {
|
||||
if (!inputValue.trim()) return;
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
const userMessage: Message = {
|
||||
id: Date.now().toString(),
|
||||
role: 'user',
|
||||
content: inputValue,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
setMessages([...messages, userMessage]);
|
||||
setInputValue('');
|
||||
|
||||
// 模拟AI响应
|
||||
setTimeout(() => {
|
||||
const response: Message = {
|
||||
id: (Date.now() + 1).toString(),
|
||||
role: 'assistant',
|
||||
content: `基于${selectedAgent.name}的专业能力,我来为您分析:\n\n根据您的问题,我识别到核心受众特征如下:\n\n**核心受众识别**:\n- 主要年龄段:25-35岁\n- 职业特征:都市白领\n- 消费能力:中高端\n- 关注点:品质生活、个人成长\n\n**关键特质**:\n1. 追求个性化表达\n2. 注重社交认同\n3. 愿意为品质付费\n\n**可能存在的阻力**:\n- 价格敏感度较高\n- 对品牌忠诚度有限\n\n以上分析是否满足您的需求?如需调整维度,请继续补充信息。`,
|
||||
timestamp: new Date(),
|
||||
agentName: selectedAgent.name,
|
||||
skills: selectedAgent.skills,
|
||||
dataSources: selectedAgent.dataSources,
|
||||
functions: selectedAgent.functions,
|
||||
structuredOutput: {
|
||||
core_audience: '25-35岁都市白领',
|
||||
key_traits: ['追求个性化', '注重社交认同', '品质导向'],
|
||||
barriers: ['价格敏感', '品牌忠诚度有限'],
|
||||
opportunities: ['社交媒体影响大', '口碑传播效果好'],
|
||||
},
|
||||
};
|
||||
|
||||
setMessages(prev => [...prev, response]);
|
||||
setIsLoading(false);
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveRecord = () => {
|
||||
alert('问答记录已保存');
|
||||
};
|
||||
|
||||
const handleCollect = () => {
|
||||
alert('已添加到收藏');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`h-[calc(100vh-120px)] flex flex-col theme-bg rounded-xl ${isDark ? 'bg-slate-800/50' : 'bg-white'} border ${isDark ? 'border-slate-700/50' : 'border-red-100'}`}>
|
||||
{/* 顶部工具栏 */}
|
||||
<div className={`flex items-center justify-between px-6 py-4 border-b ${isDark ? 'border-slate-700/50' : 'border-red-100'}`}>
|
||||
<div className="flex items-center gap-4">
|
||||
<h1 className={`text-lg font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
||||
智能体问答
|
||||
</h1>
|
||||
<div className={`flex items-center gap-2 px-3 py-1.5 rounded-lg ${isDark ? 'bg-slate-700/50' : 'bg-red-50'}`}>
|
||||
<Bot className={`w-4 h-4 ${isDark ? 'text-blue-400' : 'text-red-500'}`} />
|
||||
<span className={`text-sm ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
|
||||
当前智能体:
|
||||
</span>
|
||||
<span className={`text-sm font-medium ${isDark ? 'text-blue-400' : 'text-red-600'}`}>
|
||||
{selectedAgent.name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => onNavigate('qa-records')}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all ${
|
||||
isDark
|
||||
? 'bg-slate-700/50 text-slate-300 hover:bg-slate-600/50'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
<History className="w-4 h-4" />
|
||||
历史记录
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 主体内容区 */}
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{/* 左侧智能体区 */}
|
||||
<div className={`w-72 border-r flex-shrink-0 overflow-y-auto ${isDark ? 'border-slate-700/50 bg-slate-800/30' : 'border-red-100 bg-gray-50/50'}`}>
|
||||
{/* 智能体搜索 */}
|
||||
<div className="p-4">
|
||||
<div className={`relative ${isDark ? 'bg-slate-700/50' : 'bg-white'} rounded-lg`}>
|
||||
<Search className={`absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 ${isDark ? 'text-slate-400' : 'text-gray-400'}`} />
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="搜索智能体..."
|
||||
className={`w-full pl-10 pr-4 py-2 rounded-lg text-sm focus:outline-none ${
|
||||
isDark
|
||||
? 'bg-transparent text-white placeholder-slate-500'
|
||||
: 'bg-transparent text-gray-800 placeholder-gray-400'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 智能体列表 */}
|
||||
<div className="px-4 pb-4">
|
||||
<h3 className={`text-xs font-medium uppercase tracking-wider mb-3 ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
|
||||
智能体列表
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{filteredAgents.map((agent) => (
|
||||
<button
|
||||
key={agent.id}
|
||||
onClick={() => setSelectedAgent(agent)}
|
||||
className={`w-full text-left p-3 rounded-lg transition-all ${
|
||||
selectedAgent.id === agent.id
|
||||
? isDark
|
||||
? 'bg-blue-600/20 border border-blue-500/50'
|
||||
: 'bg-red-50 border border-red-200'
|
||||
: isDark
|
||||
? 'bg-slate-700/30 border border-transparent hover:bg-slate-700/50'
|
||||
: 'bg-white border border-gray-200 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${
|
||||
selectedAgent.id === agent.id
|
||||
? isDark
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-red-500 text-white'
|
||||
: isDark
|
||||
? 'bg-slate-600 text-slate-300'
|
||||
: 'bg-gray-100 text-gray-600'
|
||||
}`}>
|
||||
<Bot className="w-5 h-5" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className={`text-sm font-medium truncate ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
||||
{agent.name}
|
||||
</div>
|
||||
<div className={`text-xs ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
{agent.category}
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRight className={`w-4 h-4 ${isDark ? 'text-slate-500' : 'text-gray-400'}`} />
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 当前智能体能力摘要 */}
|
||||
<div className={`p-4 border-t ${isDark ? 'border-slate-700/50' : 'border-red-100'}`}>
|
||||
<h3 className={`text-xs font-medium uppercase tracking-wider mb-3 ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
|
||||
能力摘要
|
||||
</h3>
|
||||
|
||||
{/* 核心职责 */}
|
||||
<div className="mb-4">
|
||||
<div className={`flex items-center gap-2 mb-2 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
<Target className="w-4 h-4" />
|
||||
<span className="text-xs">核心职责</span>
|
||||
</div>
|
||||
<p className={`text-sm ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
|
||||
{selectedAgent.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 已启用技能 */}
|
||||
<div className="mb-4">
|
||||
<div className={`flex items-center gap-2 mb-2 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
<Sparkles className="w-4 h-4" />
|
||||
<span className="text-xs">已启用技能</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{selectedAgent.skills.map((skill, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className={`px-2 py-0.5 rounded text-xs ${
|
||||
isDark ? 'bg-blue-600/30 text-blue-400' : 'bg-blue-100 text-blue-600'
|
||||
}`}
|
||||
>
|
||||
{skill}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 可访问数据源 */}
|
||||
<div className="mb-4">
|
||||
<div className={`flex items-center gap-2 mb-2 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
<Database className="w-4 h-4" />
|
||||
<span className="text-xs">可访问数据源</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{selectedAgent.dataSources.map((source, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className={`px-2 py-0.5 rounded text-xs ${
|
||||
isDark ? 'bg-green-600/30 text-green-400' : 'bg-green-100 text-green-600'
|
||||
}`}
|
||||
>
|
||||
{source}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 可调用功能 */}
|
||||
<div>
|
||||
<div className={`flex items-center gap-2 mb-2 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
<Layers className="w-4 h-4" />
|
||||
<span className="text-xs">可调用功能</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{selectedAgent.functions.map((func, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className={`px-2 py-0.5 rounded text-xs ${
|
||||
isDark ? 'bg-purple-600/30 text-purple-400' : 'bg-purple-100 text-purple-600'
|
||||
}`}
|
||||
>
|
||||
{func}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 中间对话区 */}
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
{/* 消息列表 */}
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||
{messages.length === 0 ? (
|
||||
<div className={`h-full flex flex-col items-center justify-center ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
|
||||
<Bot className="w-16 h-16 mb-4 opacity-50" />
|
||||
<p className="text-lg">开始与{selectedAgent.name}对话</p>
|
||||
<p className="text-sm mt-2">基于智能体专业能力获取精准回答</p>
|
||||
</div>
|
||||
) : (
|
||||
messages.map((msg) => (
|
||||
<div key={msg.id} className="space-y-3">
|
||||
{/* 用户消息 */}
|
||||
<div className={`flex gap-3 ${msg.role === 'user' ? 'justify-end' : ''}`}>
|
||||
{msg.role === 'assistant' && (
|
||||
<div className={`w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 ${
|
||||
isDark ? 'bg-blue-600/20 text-blue-400' : 'bg-red-100 text-red-500'
|
||||
}`}>
|
||||
<Bot className="w-4 h-4" />
|
||||
</div>
|
||||
)}
|
||||
<div className={`max-w-[70%] rounded-xl px-4 py-3 ${
|
||||
msg.role === 'user'
|
||||
? isDark
|
||||
? 'bg-blue-600/30 text-white'
|
||||
: 'bg-red-500 text-white'
|
||||
: isDark
|
||||
? 'bg-slate-700/50 text-slate-200'
|
||||
: 'bg-gray-100 text-gray-800'
|
||||
}`}>
|
||||
<p className="whitespace-pre-wrap">{msg.content}</p>
|
||||
</div>
|
||||
{msg.role === 'user' && (
|
||||
<div className={`w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 ${
|
||||
isDark ? 'bg-slate-700 text-slate-300' : 'bg-gray-200 text-gray-600'
|
||||
}`}>
|
||||
<div className="w-4 h-4 rounded-full bg-current" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 智能体执行详情 */}
|
||||
{msg.role === 'assistant' && (
|
||||
<div className={`ml-11 p-3 rounded-xl ${isDark ? 'bg-slate-800/50' : 'bg-gray-50'}`}>
|
||||
{/* 本轮启用技能 */}
|
||||
<div className="mb-3">
|
||||
<h4 className={`text-xs font-medium mb-2 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
本轮启用技能
|
||||
</h4>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{msg.skills?.map((skill, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className={`px-2 py-0.5 rounded text-xs ${
|
||||
isDark ? 'bg-blue-600/30 text-blue-400' : 'bg-blue-100 text-blue-600'
|
||||
}`}
|
||||
>
|
||||
{skill}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 本轮访问数据源 */}
|
||||
<div className="mb-3">
|
||||
<h4 className={`text-xs font-medium mb-2 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
本轮访问数据源
|
||||
</h4>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{msg.dataSources?.map((source, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className={`px-2 py-0.5 rounded text-xs ${
|
||||
isDark ? 'bg-green-600/30 text-green-400' : 'bg-green-100 text-green-600'
|
||||
}`}
|
||||
>
|
||||
{source}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 本轮调用功能 */}
|
||||
<div className="mb-3">
|
||||
<h4 className={`text-xs font-medium mb-2 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
本轮调用功能
|
||||
</h4>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{msg.functions?.map((func, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className={`px-2 py-0.5 rounded text-xs ${
|
||||
isDark ? 'bg-purple-600/30 text-purple-400' : 'bg-purple-100 text-purple-600'
|
||||
}`}
|
||||
>
|
||||
{func}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 结构化输出 */}
|
||||
{msg.structuredOutput && (
|
||||
<div>
|
||||
<h4 className={`text-xs font-medium mb-2 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
结构化输出
|
||||
</h4>
|
||||
<div className={`p-3 rounded-lg ${isDark ? 'bg-slate-700/50' : 'bg-white'}`}>
|
||||
{Object.entries(msg.structuredOutput).map(([key, value]) => (
|
||||
<div key={key} className="flex items-start gap-2 mb-1 last:mb-0">
|
||||
<span className={`text-xs font-mono ${isDark ? 'text-blue-400' : 'text-blue-600'}`}>
|
||||
{key}:
|
||||
</span>
|
||||
<span className={`text-xs ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>
|
||||
{Array.isArray(value) ? value.join(', ') : String(value)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
||||
{isLoading && (
|
||||
<div className="flex gap-3">
|
||||
<div className={`w-8 h-8 rounded-lg flex items-center justify-center ${
|
||||
isDark ? 'bg-blue-600/20 text-blue-400' : 'bg-red-100 text-red-500'
|
||||
}`}>
|
||||
<Bot className="w-4 h-4" />
|
||||
</div>
|
||||
<div className={`max-w-[70%] rounded-xl px-4 py-3 ${isDark ? 'bg-slate-700/50' : 'bg-gray-100'}`}>
|
||||
<div className="flex gap-1">
|
||||
<div className={`w-2 h-2 rounded-full animate-bounce ${isDark ? 'bg-slate-500' : 'bg-gray-400'}`} style={{ animationDelay: '0ms' }} />
|
||||
<div className={`w-2 h-2 rounded-full animate-bounce ${isDark ? 'bg-slate-500' : 'bg-gray-400'}`} style={{ animationDelay: '150ms' }} />
|
||||
<div className={`w-2 h-2 rounded-full animate-bounce ${isDark ? 'bg-slate-500' : 'bg-gray-400'}`} style={{ animationDelay: '300ms' }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 底部输入区 */}
|
||||
<div className={`border-t p-4 ${isDark ? 'border-slate-700/50' : 'border-red-100'}`}>
|
||||
<div className={`flex items-end gap-3 rounded-xl p-3 ${isDark ? 'bg-slate-700/50' : 'bg-gray-100'}`}>
|
||||
<textarea
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="输入问题或任务要求..."
|
||||
rows={1}
|
||||
className={`flex-1 bg-transparent resize-none focus:outline-none ${
|
||||
isDark ? 'text-white placeholder-slate-500' : 'text-gray-800 placeholder-gray-400'
|
||||
}`}
|
||||
/>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleSaveRecord}
|
||||
className={`p-2 rounded-lg transition-colors ${
|
||||
isDark ? 'hover:bg-slate-600 text-slate-400' : 'hover:bg-gray-200 text-gray-500'
|
||||
}`}
|
||||
title="保存记录"
|
||||
>
|
||||
<Bookmark className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleCollect}
|
||||
className={`p-2 rounded-lg transition-colors ${
|
||||
isDark ? 'hover:bg-slate-600 text-slate-400' : 'hover:bg-gray-200 text-gray-500'
|
||||
}`}
|
||||
title="加入收藏"
|
||||
>
|
||||
<Bookmark className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleSend}
|
||||
disabled={!inputValue.trim() || isLoading}
|
||||
className={`px-4 py-2 rounded-lg font-medium transition-all flex items-center gap-2 ${
|
||||
inputValue.trim() && !isLoading
|
||||
? isDark
|
||||
? 'bg-blue-600 hover:bg-blue-700 text-white'
|
||||
: 'bg-red-500 hover:bg-red-600 text-white'
|
||||
: isDark
|
||||
? 'bg-slate-600 text-slate-400'
|
||||
: 'bg-gray-300 text-gray-500'
|
||||
}`}
|
||||
>
|
||||
<Send className="w-4 h-4" />
|
||||
发送
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧执行详情区 */}
|
||||
<div className={`w-72 border-l flex-shrink-0 overflow-y-auto p-4 ${
|
||||
isDark ? 'border-slate-700/50 bg-slate-800/30' : 'border-red-100 bg-gray-50/50'
|
||||
}`}>
|
||||
<h3 className={`text-sm font-medium mb-4 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
|
||||
执行详情
|
||||
</h3>
|
||||
|
||||
{/* 本轮启用技能 */}
|
||||
<div className="mb-6">
|
||||
<div className={`flex items-center gap-2 mb-3 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
<Sparkles className="w-4 h-4" />
|
||||
<span className="text-xs font-medium">本轮启用技能</span>
|
||||
</div>
|
||||
<div className={`p-3 rounded-xl ${isDark ? 'bg-slate-700/50' : 'bg-white'}`}>
|
||||
<div className="space-y-2">
|
||||
{selectedAgent.skills.map((skill, idx) => (
|
||||
<div key={idx} className="flex items-center gap-2">
|
||||
<CheckCircle className={`w-3 h-3 ${isDark ? 'text-blue-400' : 'text-green-500'}`} />
|
||||
<span className={`text-sm ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>{skill}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 本轮访问数据源 */}
|
||||
<div className="mb-6">
|
||||
<div className={`flex items-center gap-2 mb-3 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
<Database className="w-4 h-4" />
|
||||
<span className="text-xs font-medium">本轮访问数据源</span>
|
||||
</div>
|
||||
<div className={`p-3 rounded-xl ${isDark ? 'bg-slate-700/50' : 'bg-white'}`}>
|
||||
<div className="space-y-2">
|
||||
{selectedAgent.dataSources.map((source, idx) => (
|
||||
<div key={idx} className="flex items-center gap-2">
|
||||
<Database className={`w-3 h-3 ${isDark ? 'text-green-400' : 'text-green-500'}`} />
|
||||
<span className={`text-sm ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>{source}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 本轮调用功能 */}
|
||||
<div className="mb-6">
|
||||
<div className={`flex items-center gap-2 mb-3 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
<Layers className="w-4 h-4" />
|
||||
<span className="text-xs font-medium">本轮调用功能</span>
|
||||
</div>
|
||||
<div className={`p-3 rounded-xl ${isDark ? 'bg-slate-700/50' : 'bg-white'}`}>
|
||||
<div className="space-y-2">
|
||||
{selectedAgent.functions.map((func, idx) => (
|
||||
<div key={idx} className="flex items-center gap-2">
|
||||
<Cpu className={`w-3 h-3 ${isDark ? 'text-purple-400' : 'text-purple-500'}`} />
|
||||
<span className={`text-sm ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>{func}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 结构化输出字段 */}
|
||||
<div>
|
||||
<div className={`flex items-center gap-2 mb-3 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
<FileText className="w-4 h-4" />
|
||||
<span className="text-xs font-medium">输出字段</span>
|
||||
</div>
|
||||
<div className={`p-3 rounded-xl ${isDark ? 'bg-slate-700/50' : 'bg-white'}`}>
|
||||
<div className="space-y-2">
|
||||
{selectedAgent.outputFields.map((field, idx) => (
|
||||
<div key={idx} className="flex items-center gap-2">
|
||||
<Zap className={`w-3 h-3 ${isDark ? 'text-yellow-400' : 'text-yellow-500'}`} />
|
||||
<span className={`text-xs font-mono ${isDark ? 'text-blue-400' : 'text-blue-600'}`}>{field}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgentQA;
|
||||
@@ -0,0 +1,532 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Bot,
|
||||
PlayCircle,
|
||||
ArrowLeft,
|
||||
Send,
|
||||
Clock,
|
||||
CheckCircle,
|
||||
AlertCircle,
|
||||
FileText,
|
||||
Sparkles,
|
||||
User,
|
||||
Settings,
|
||||
Zap,
|
||||
BookOpen,
|
||||
Lightbulb,
|
||||
Target,
|
||||
Users,
|
||||
MessageSquare,
|
||||
} from 'lucide-react';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { PageType } from '../components/Sidebar';
|
||||
|
||||
interface AgentTrialProps {
|
||||
onNavigate: (page: PageType) => void;
|
||||
}
|
||||
|
||||
// 单智能体能力卡片
|
||||
const agentCapabilities = [
|
||||
{
|
||||
id: 'goal',
|
||||
name: 'GoalAgent',
|
||||
role: '目标分析专家',
|
||||
description: '分析业务目标,提取核心KPI和成功标准',
|
||||
icon: Target,
|
||||
color: 'blue',
|
||||
skills: ['目标分解', 'KPI提取', '优先级排序'],
|
||||
example: '分析这次五四青年节宣传的核心目标是什么?',
|
||||
},
|
||||
{
|
||||
id: 'audience',
|
||||
name: 'AudienceAgent',
|
||||
role: '受众分析专家',
|
||||
description: '定义目标受众画像,分析用户特征和需求',
|
||||
icon: Users,
|
||||
color: 'cyan',
|
||||
skills: ['用户画像', '行为分析', '需求挖掘'],
|
||||
example: '谁是我们的核心受众?他们的特征是什么?',
|
||||
},
|
||||
{
|
||||
id: 'behavior',
|
||||
name: 'BehaviorAgent',
|
||||
role: '行为设计专家',
|
||||
description: '设计用户行为路径和转化漏斗',
|
||||
icon: Sparkles,
|
||||
color: 'green',
|
||||
skills: ['行为设计', '转化路径', '激励策略'],
|
||||
example: '我们希望用户产生什么样的行为?',
|
||||
},
|
||||
{
|
||||
id: 'driver',
|
||||
name: 'DriverAgent',
|
||||
role: '驱动因素专家',
|
||||
description: '分析影响用户决策的核心驱动因素',
|
||||
icon: Lightbulb,
|
||||
color: 'yellow',
|
||||
skills: ['驱动分析', '动机挖掘', '阻力识别'],
|
||||
example: '什么因素会驱动用户做出我们期望的行为?',
|
||||
},
|
||||
{
|
||||
id: 'narrative',
|
||||
name: 'NarrativeAgent',
|
||||
role: '叙事策略专家',
|
||||
description: '设计品牌叙事框架和内容策略',
|
||||
icon: MessageSquare,
|
||||
color: 'purple',
|
||||
skills: ['故事设计', '内容策略', '情感共鸣'],
|
||||
example: '我们应该如何讲述品牌故事?',
|
||||
},
|
||||
{
|
||||
id: 'risk',
|
||||
name: 'RiskAgent',
|
||||
role: '风险评估专家',
|
||||
description: '识别项目风险,制定应对策略',
|
||||
icon: AlertCircle,
|
||||
color: 'red',
|
||||
skills: ['风险识别', '影响评估', '应对策略'],
|
||||
example: '这个方案有哪些潜在风险?',
|
||||
},
|
||||
];
|
||||
|
||||
const AgentTrial: React.FC<AgentTrialProps> = ({ onNavigate }) => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
|
||||
// 主题颜色类
|
||||
const cardBg = isDark ? 'bg-slate-800/30' : 'bg-white';
|
||||
const cardBorder = isDark ? 'border-blue-800/30' : 'border-red-200';
|
||||
const textPrimary = isDark ? 'text-white' : 'text-gray-900';
|
||||
const textSecondary = isDark ? 'text-slate-400' : 'text-gray-600';
|
||||
const textMuted = isDark ? 'text-slate-500' : 'text-gray-500';
|
||||
const btnSecondary = isDark ? 'bg-slate-700/50 hover:bg-slate-600/50 border-blue-800/30 text-slate-300' : 'bg-gray-100 hover:bg-gray-200 border-gray-300 text-gray-700';
|
||||
const btnPrimary = isDark ? 'bg-blue-600 hover:bg-blue-500' : 'bg-red-600 hover:bg-red-500';
|
||||
const inputBg = isDark ? 'bg-slate-900/50' : 'bg-gray-50';
|
||||
const inputBorder = isDark ? 'border-blue-800/30' : 'border-gray-300';
|
||||
|
||||
const [selectedAgent, setSelectedAgent] = useState<string | null>(null);
|
||||
const [inputText, setInputText] = useState('');
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
const [conversation, setConversation] = useState<Array<{ role: 'user' | 'agent'; content: string; time: string }>>([]);
|
||||
|
||||
const currentAgent = agentCapabilities.find((a) => a.id === selectedAgent);
|
||||
|
||||
const handleSelectAgent = (agentId: string) => {
|
||||
setSelectedAgent(agentId);
|
||||
const agent = agentCapabilities.find((a) => a.id === agentId);
|
||||
if (agent) {
|
||||
setInputText(agent.example);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSend = async () => {
|
||||
if (!inputText.trim() || !currentAgent) return;
|
||||
|
||||
const now = new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
|
||||
|
||||
// 添加用户消息
|
||||
setConversation((prev) => [...prev, { role: 'user', content: inputText, time: now }]);
|
||||
const userInput = inputText;
|
||||
setInputText('');
|
||||
setIsRunning(true);
|
||||
|
||||
// 模拟智能体回复
|
||||
setTimeout(() => {
|
||||
const responseTime = new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
|
||||
let response = '';
|
||||
|
||||
switch (currentAgent.id) {
|
||||
case 'goal':
|
||||
response = `【目标分析完成】
|
||||
|
||||
基于您的描述,我分析出以下核心目标:
|
||||
|
||||
1. **主目标**:提升品牌在Z世代用户中的认知度
|
||||
2. **次目标**:建立品牌年轻化形象,增强情感连接
|
||||
3. **量化指标**:
|
||||
- 曝光量:500万+
|
||||
- 互动率:5%+
|
||||
- UGC数量:1000+
|
||||
|
||||
建议优先聚焦主目标,确保资源集中。`;
|
||||
break;
|
||||
case 'audience':
|
||||
response = `【受众分析完成】
|
||||
|
||||
核心受众画像:
|
||||
|
||||
**人口特征**
|
||||
- 年龄:18-25岁
|
||||
- 身份:学生、基层青年工作者
|
||||
- 地域:一二线城市为主
|
||||
|
||||
**行为特征**
|
||||
- 社交媒体重度用户
|
||||
- 短视频爱好者
|
||||
- 注重真实性和态度
|
||||
|
||||
**核心需求**
|
||||
- 社交认同与自我表达
|
||||
- 归属感与群体认同
|
||||
- 性价比与实用性`;
|
||||
break;
|
||||
case 'behavior':
|
||||
response = `【行为设计完成】
|
||||
|
||||
目标行为路径:
|
||||
|
||||
浏览 → 点赞 → 评论 → 转发 → 创作
|
||||
|
||||
**关键转化节点**
|
||||
1. 引起注意:内容需有话题性
|
||||
2. 激发互动:设置讨论话题
|
||||
3. 促进创作:提供UGC模板和激励
|
||||
|
||||
**激励机制建议**
|
||||
- 虚拟勋章/积分
|
||||
- 优质内容曝光
|
||||
- 创作者社群归属`;
|
||||
break;
|
||||
case 'driver':
|
||||
response = `【驱动因素分析完成】
|
||||
|
||||
核心驱动因素:
|
||||
|
||||
1. **社交认同需求**
|
||||
- 年轻人渴望被关注和认可
|
||||
- UGC是自我表达的重要方式
|
||||
|
||||
2. **群体归属感**
|
||||
- 青年节与年轻人天然关联
|
||||
- 参与活动获得身份认同
|
||||
|
||||
3. **表达欲驱动**
|
||||
- 有态度的品牌内容激发创作
|
||||
- 提供创作框架降低门槛
|
||||
|
||||
**主要阻力**
|
||||
- 注意力的稀缺
|
||||
- 创作成本感知`;
|
||||
break;
|
||||
case 'narrative':
|
||||
response = `【叙事策略完成】
|
||||
|
||||
核心叙事框架:
|
||||
|
||||
**主题**:真实、有态度的青年故事
|
||||
|
||||
**叙事结构**
|
||||
1. 讲述真实的人物故事
|
||||
2. 引发情感共鸣
|
||||
3. 传递品牌价值观
|
||||
4. 激发用户参与
|
||||
|
||||
**内容类型**
|
||||
- 人物专访型
|
||||
- 事件记录型
|
||||
- 观点表达型
|
||||
|
||||
**关键要素**
|
||||
- 真实感 > 精致感
|
||||
- 态度感 > 说教感
|
||||
- 参与感 > 观看感`;
|
||||
break;
|
||||
case 'risk':
|
||||
response = `【风险评估完成】
|
||||
|
||||
识别的风险项:
|
||||
|
||||
**高风险**
|
||||
1. 内容质量不稳定
|
||||
- 风险:UGC质量参差不齐
|
||||
- 对策:建立内容审核机制
|
||||
|
||||
**中风险**
|
||||
2. 预算控制难度大
|
||||
- 风险:投放效果不可控
|
||||
- 对策:分阶段预算分配
|
||||
|
||||
3. 青年群体注意力分散
|
||||
- 风险:话题热度难维持
|
||||
- 对策:多波次内容规划
|
||||
|
||||
**建议**:重点关注内容质量风险`;
|
||||
break;
|
||||
default:
|
||||
response = '正在分析中...';
|
||||
}
|
||||
|
||||
setConversation((prev) => [...prev, { role: 'agent', content: response, time: responseTime }]);
|
||||
setIsRunning(false);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 顶部操作栏 */}
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-4`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={() => onNavigate('agent-list')}
|
||||
className={`flex items-center gap-2 px-3 py-2 ${btnSecondary} rounded-lg text-sm transition-colors`}
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
返回
|
||||
</button>
|
||||
<div className={`h-8 w-px ${isDark ? 'bg-blue-800/30' : 'bg-red-200'}`} />
|
||||
<div>
|
||||
<h1 className={`${textPrimary} font-bold text-lg`}>单智能体试用</h1>
|
||||
<p className={`${textSecondary} text-sm`}>直接调用单个智能体进行专项分析</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className={`px-3 py-1.5 ${isDark ? 'bg-green-500/20 text-green-400' : 'bg-green-100 text-green-700'} rounded-full text-sm font-medium flex items-center gap-1.5`}>
|
||||
<Bot className="w-4 h-4" />
|
||||
能力调用模式
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-12 gap-6">
|
||||
{/* 左侧:智能体选择区 */}
|
||||
<div className="col-span-4">
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-4`}>
|
||||
<h3 className={`${textPrimary} font-semibold text-sm mb-4 flex items-center gap-2`}>
|
||||
<Bot className={`w-4 h-4 ${isDark ? 'text-purple-400' : 'text-purple-600'}`} />
|
||||
选择智能体
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{agentCapabilities.map((agent) => {
|
||||
const Icon = agent.icon;
|
||||
const isSelected = selectedAgent === agent.id;
|
||||
return (
|
||||
<button
|
||||
key={agent.id}
|
||||
onClick={() => handleSelectAgent(agent.id)}
|
||||
className={`w-full p-4 rounded-xl border transition-all text-left ${
|
||||
isSelected
|
||||
? `${isDark ? 'bg-blue-500/20 border-blue-500/50' : 'bg-red-50 border-red-300'}`
|
||||
: `${inputBg} ${cardBorder} hover:${isDark ? 'border-blue-500/30' : 'border-red-400'}`
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className={`w-10 h-10 rounded-lg ${isDark ? 'bg-blue-500/20' : 'bg-red-100'} flex items-center justify-center flex-shrink-0`}>
|
||||
<Icon className={`w-5 h-5 ${isDark ? 'text-blue-400' : 'text-red-600'}`} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className={`text-sm font-medium ${isSelected ? (isDark ? 'text-blue-400' : 'text-red-600') : textPrimary}`}>
|
||||
{agent.name}
|
||||
</p>
|
||||
<p className={`${textSecondary} text-xs mt-0.5`}>{agent.role}</p>
|
||||
<p className={`${textMuted} text-xs mt-1 line-clamp-2`}>{agent.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
{isSelected && (
|
||||
<div className={`mt-3 pt-3 border-t ${isDark ? 'border-blue-500/30' : 'border-red-300'}`}>
|
||||
<p className={`text-xs ${textSecondary} mb-2`}>核心技能</p>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{agent.skills.map((skill, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className={`px-2 py-0.5 ${isDark ? 'bg-blue-500/20 text-blue-400' : 'bg-red-100 text-red-600'} rounded text-xs`}
|
||||
>
|
||||
{skill}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 使用说明 */}
|
||||
<div className={`mt-4 ${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-4`}>
|
||||
<h3 className={`${textPrimary} font-semibold text-sm mb-3 flex items-center gap-2`}>
|
||||
<BookOpen className={`w-4 h-4 ${isDark ? 'text-blue-400' : 'text-blue-600'}`} />
|
||||
使用说明
|
||||
</h3>
|
||||
<div className={`space-y-2 text-xs ${textSecondary}`}>
|
||||
<div className="flex items-start gap-2">
|
||||
<span className={`font-bold ${isDark ? 'text-blue-400' : 'text-blue-600'}`}>1.</span>
|
||||
<p>选择一个智能体进入专项分析模式</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<span className={`font-bold ${isDark ? 'text-blue-400' : 'text-blue-600'}`}>2.</span>
|
||||
<p>输入您的问题或任务要求</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<span className={`font-bold ${isDark ? 'text-blue-400' : 'text-blue-600'}`}>3.</span>
|
||||
<p>智能体将基于其专业能力给出分析结果</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<span className={`font-bold ${isDark ? 'text-blue-400' : 'text-blue-600'}`}>4.</span>
|
||||
<p>结果可用于后续场景任务或直接参考</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧:对话区 */}
|
||||
<div className="col-span-8">
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl flex flex-col h-[calc(100vh-220px)]`}>
|
||||
{selectedAgent ? (
|
||||
<>
|
||||
{/* 顶部智能体信息 */}
|
||||
<div className={`p-4 border-b ${cardBorder}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
{currentAgent && (
|
||||
<>
|
||||
<div className={`w-10 h-10 rounded-lg ${isDark ? 'bg-blue-500/20' : 'bg-red-100'} flex items-center justify-center`}>
|
||||
<currentAgent.icon className={`w-5 h-5 ${isDark ? 'text-blue-400' : 'text-red-600'}`} />
|
||||
</div>
|
||||
<div>
|
||||
<p className={`${textPrimary} font-medium`}>{currentAgent.name}</p>
|
||||
<p className={`${textSecondary} text-xs`}>{currentAgent.role}</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`px-2 py-1 ${isDark ? 'bg-green-500/20 text-green-400' : 'bg-green-100 text-green-700'} rounded text-xs flex items-center gap-1`}>
|
||||
<Zap className="w-3 h-3" />
|
||||
独立运行中
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 对话区域 */}
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||
{conversation.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-center">
|
||||
<div className={`w-16 h-16 rounded-full ${isDark ? 'bg-blue-500/20' : 'bg-red-100'} flex items-center justify-center mb-4`}>
|
||||
<Bot className={`w-8 h-8 ${isDark ? 'text-blue-400' : 'text-red-600'}`} />
|
||||
</div>
|
||||
<p className={`${textPrimary} font-medium mb-2`}>开始与 {currentAgent?.name} 对话</p>
|
||||
<p className={`${textSecondary} text-sm max-w-md`}>
|
||||
{currentAgent?.description}
|
||||
<br />
|
||||
请在下方输入您的问题或任务要求
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
conversation.map((msg, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`flex gap-3 ${msg.role === 'user' ? 'flex-row-reverse' : ''}`}
|
||||
>
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${
|
||||
msg.role === 'user'
|
||||
? (isDark ? 'bg-blue-500/20' : 'bg-red-100')
|
||||
: (isDark ? 'bg-blue-500/20' : 'bg-red-100')
|
||||
}`}
|
||||
>
|
||||
{msg.role === 'user' ? (
|
||||
<User className={`w-4 h-4 ${isDark ? 'text-blue-400' : 'text-red-600'}`} />
|
||||
) : (
|
||||
<Bot className={`w-4 h-4 ${isDark ? 'text-blue-400' : 'text-red-600'}`} />
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={`max-w-[80%] rounded-2xl px-4 py-3 ${
|
||||
msg.role === 'user'
|
||||
? (isDark ? 'bg-blue-600 text-white' : 'bg-red-600 text-white')
|
||||
: `${inputBg} ${inputBorder}`
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`text-xs mb-1 ${
|
||||
msg.role === 'user' ? (isDark ? 'text-blue-200' : 'text-red-200') : textMuted
|
||||
}`}
|
||||
>
|
||||
{msg.role === 'user' ? '您' : currentAgent?.name} · {msg.time}
|
||||
</div>
|
||||
<div className="text-sm whitespace-pre-wrap">{msg.content}</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
{isRunning && (
|
||||
<div className="flex gap-3">
|
||||
<div className={`w-8 h-8 rounded-full ${isDark ? 'bg-green-500/20' : 'bg-green-100'} flex items-center justify-center`}>
|
||||
<Bot className={`w-4 h-4 ${isDark ? 'text-green-400' : 'text-green-600'}`} />
|
||||
</div>
|
||||
<div className={`${inputBg} ${inputBorder} rounded-2xl px-4 py-3`}>
|
||||
<div className={`flex items-center gap-2 ${textSecondary} text-sm`}>
|
||||
<div className={`w-2 h-2 ${isDark ? 'bg-green-400' : 'bg-green-500'} rounded-full animate-pulse`} />
|
||||
{currentAgent?.name} 正在分析中...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 输入区域 */}
|
||||
<div className={`p-4 border-t ${cardBorder}`}>
|
||||
<div className="flex gap-3">
|
||||
<textarea
|
||||
value={inputText}
|
||||
onChange={(e) => setInputText(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
}}
|
||||
placeholder={`输入您要咨询 ${currentAgent?.name} 的问题...`}
|
||||
rows={3}
|
||||
className={`flex-1 px-4 py-3 ${inputBg} ${inputBorder} rounded-xl ${textPrimary} text-sm ${textMuted} resize-none focus:outline-none focus:${isDark ? 'border-blue-500/50' : 'border-red-400'}`}
|
||||
/>
|
||||
<div className="flex flex-col gap-2">
|
||||
<button
|
||||
onClick={handleSend}
|
||||
disabled={!inputText.trim() || isRunning}
|
||||
className={`px-6 py-3 ${btnPrimary} ${isDark ? 'disabled:bg-slate-700 disabled:text-slate-500' : 'disabled:bg-gray-300 disabled:text-gray-500'} text-white rounded-xl text-sm font-medium transition-colors flex items-center gap-2`}
|
||||
>
|
||||
<Send className="w-4 h-4" />
|
||||
发送
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setConversation([]);
|
||||
setInputText('');
|
||||
}}
|
||||
className={`px-4 py-2 ${btnSecondary} rounded-lg text-sm transition-colors`}
|
||||
>
|
||||
清空
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className={`${textMuted} text-xs mt-2`}>
|
||||
按 Enter 发送,Shift+Enter 换行
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex-1 flex flex-col items-center justify-center text-center p-8">
|
||||
<div className={`w-20 h-20 rounded-full ${isDark ? 'bg-blue-500/20' : 'bg-red-100'} flex items-center justify-center mb-6`}>
|
||||
<PlayCircle className={`w-10 h-10 ${isDark ? 'text-blue-400' : 'text-red-600'}`} />
|
||||
</div>
|
||||
<h3 className={`${textPrimary} font-semibold text-lg mb-2`}>选择智能体开始试用</h3>
|
||||
<p className={`${textSecondary} text-sm max-w-md`}>
|
||||
左侧选择一个智能体,即可进入单智能体对话模式。
|
||||
<br />
|
||||
该模式适合进行专项分析、快速验证或测试单个智能体能力。
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgentTrial;
|
||||
@@ -0,0 +1,900 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import {
|
||||
Scale,
|
||||
Search,
|
||||
Plus,
|
||||
Edit,
|
||||
Trash2,
|
||||
Download,
|
||||
Filter,
|
||||
Folder,
|
||||
FolderOpen,
|
||||
ChevronRight,
|
||||
TrendingUp,
|
||||
TrendingDown,
|
||||
Minus,
|
||||
Target,
|
||||
CheckCircle,
|
||||
AlertCircle,
|
||||
Clock,
|
||||
Star,
|
||||
BarChart3,
|
||||
PieChart,
|
||||
Activity,
|
||||
Settings,
|
||||
Copy,
|
||||
Link2,
|
||||
FileText,
|
||||
User,
|
||||
Calendar,
|
||||
} from 'lucide-react';
|
||||
import { PageType } from '../components/Sidebar';
|
||||
|
||||
interface EvaluationIndex {
|
||||
id: string;
|
||||
name: string;
|
||||
category: string;
|
||||
type: 'quantitative' | 'qualitative' | 'composite';
|
||||
weight: number;
|
||||
description: string;
|
||||
calculationMethod?: string;
|
||||
dataSource?: string;
|
||||
updateFrequency: 'realtime' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'quarterly';
|
||||
status: 'active' | 'draft' | 'deprecated';
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
author: string;
|
||||
tags: string[];
|
||||
baseline?: number;
|
||||
target?: number;
|
||||
currentValue?: number;
|
||||
trend: 'up' | 'down' | 'stable';
|
||||
relatedIndices: string[];
|
||||
}
|
||||
|
||||
interface EvaluationSystem {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
category: string;
|
||||
indices: string[];
|
||||
status: 'active' | 'draft' | 'archived';
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
author: string;
|
||||
}
|
||||
|
||||
interface EvaluationIndexResourceManagementProps {
|
||||
onNavigate: (page: PageType) => void;
|
||||
}
|
||||
|
||||
const mockIndices: EvaluationIndex[] = [
|
||||
{
|
||||
id: 'idx-001',
|
||||
name: '网络影响力指数',
|
||||
category: '影响力评估',
|
||||
type: 'composite',
|
||||
weight: 0.25,
|
||||
description: '综合评估目标在网络空间的影响力水平',
|
||||
calculationMethod: '加权平均法',
|
||||
dataSource: '社交媒体数据、新闻报道量',
|
||||
updateFrequency: 'daily',
|
||||
status: 'active',
|
||||
createdAt: '2023-06-15',
|
||||
updatedAt: '2024-03-10',
|
||||
author: '评估组A',
|
||||
tags: ['影响力', '网络', '综合'],
|
||||
baseline: 50,
|
||||
target: 80,
|
||||
currentValue: 72,
|
||||
trend: 'up',
|
||||
relatedIndices: ['idx-002', 'idx-003'],
|
||||
},
|
||||
{
|
||||
id: 'idx-002',
|
||||
name: '传播范围指数',
|
||||
category: '传播效果',
|
||||
type: 'quantitative',
|
||||
weight: 0.20,
|
||||
description: '衡量信息在网络中的传播覆盖范围',
|
||||
calculationMethod: '触达人数 × 地域覆盖率',
|
||||
dataSource: '平台API数据',
|
||||
updateFrequency: 'realtime',
|
||||
status: 'active',
|
||||
createdAt: '2023-07-20',
|
||||
updatedAt: '2024-03-12',
|
||||
author: '评估组B',
|
||||
tags: ['传播', '覆盖', '定量'],
|
||||
baseline: 10000,
|
||||
target: 50000,
|
||||
currentValue: 42500,
|
||||
trend: 'up',
|
||||
relatedIndices: ['idx-001'],
|
||||
},
|
||||
{
|
||||
id: 'idx-003',
|
||||
name: '舆论倾向指数',
|
||||
category: '舆情分析',
|
||||
type: 'qualitative',
|
||||
weight: 0.30,
|
||||
description: '评估网络舆论对目标的整体态度倾向',
|
||||
calculationMethod: '情感分析模型',
|
||||
dataSource: '社交媒体评论、新闻评论区',
|
||||
updateFrequency: 'hourly',
|
||||
status: 'active',
|
||||
createdAt: '2023-08-10',
|
||||
updatedAt: '2024-03-11',
|
||||
author: '评估组A',
|
||||
tags: ['舆情', '情感', '定性'],
|
||||
baseline: 0,
|
||||
target: 75,
|
||||
currentValue: 68,
|
||||
trend: 'stable',
|
||||
relatedIndices: ['idx-001', 'idx-004'],
|
||||
},
|
||||
{
|
||||
id: 'idx-004',
|
||||
name: '响应时效指数',
|
||||
category: '效能评估',
|
||||
type: 'quantitative',
|
||||
weight: 0.15,
|
||||
description: '评估对网络事件的响应速度',
|
||||
calculationMethod: '平均响应时间 (小时)',
|
||||
dataSource: '事件追踪系统',
|
||||
updateFrequency: 'weekly',
|
||||
status: 'active',
|
||||
createdAt: '2023-09-05',
|
||||
updatedAt: '2024-03-08',
|
||||
author: '评估组C',
|
||||
tags: ['时效', '效能', '定量'],
|
||||
baseline: 24,
|
||||
target: 4,
|
||||
currentValue: 6,
|
||||
trend: 'down',
|
||||
relatedIndices: ['idx-005'],
|
||||
},
|
||||
{
|
||||
id: 'idx-005',
|
||||
name: '协调联动指数',
|
||||
category: '协作效能',
|
||||
type: 'composite',
|
||||
weight: 0.10,
|
||||
description: '评估多部门协作的顺畅程度',
|
||||
calculationMethod: '任务完成率 × 沟通效率',
|
||||
dataSource: '协同办公系统',
|
||||
updateFrequency: 'monthly',
|
||||
status: 'draft',
|
||||
createdAt: '2024-01-15',
|
||||
updatedAt: '2024-03-05',
|
||||
author: '评估组D',
|
||||
tags: ['协作', '联动', '综合'],
|
||||
trend: 'stable',
|
||||
relatedIndices: ['idx-004'],
|
||||
},
|
||||
{
|
||||
id: 'idx-006',
|
||||
name: '风险预警准确率',
|
||||
category: '预警效能',
|
||||
type: 'quantitative',
|
||||
weight: 0.25,
|
||||
description: '风险预警的准确程度',
|
||||
calculationMethod: '准确预警数 / 总预警数',
|
||||
dataSource: '预警系统日志',
|
||||
updateFrequency: 'monthly',
|
||||
status: 'active',
|
||||
createdAt: '2023-10-20',
|
||||
updatedAt: '2024-02-28',
|
||||
author: '评估组A',
|
||||
tags: ['预警', '准确率', '定量'],
|
||||
baseline: 60,
|
||||
target: 90,
|
||||
currentValue: 82,
|
||||
trend: 'up',
|
||||
relatedIndices: [],
|
||||
},
|
||||
{
|
||||
id: 'idx-007',
|
||||
name: '策略有效性指数',
|
||||
category: '策略评估',
|
||||
type: 'composite',
|
||||
weight: 0.30,
|
||||
description: '评估所采取措施的有效性',
|
||||
calculationMethod: '多维度加权评估',
|
||||
dataSource: '策略执行数据、效果追踪',
|
||||
updateFrequency: 'quarterly',
|
||||
status: 'active',
|
||||
createdAt: '2023-11-10',
|
||||
updatedAt: '2024-03-01',
|
||||
author: '评估组B',
|
||||
tags: ['策略', '效果', '综合'],
|
||||
baseline: 50,
|
||||
target: 85,
|
||||
currentValue: 78,
|
||||
trend: 'up',
|
||||
relatedIndices: ['idx-001', 'idx-003'],
|
||||
},
|
||||
{
|
||||
id: 'idx-008',
|
||||
name: '信息完整度',
|
||||
category: '数据质量',
|
||||
type: 'quantitative',
|
||||
weight: 0.10,
|
||||
description: '目标画像信息的完整程度',
|
||||
calculationMethod: '已填写字段数 / 总字段数',
|
||||
dataSource: '画像数据库',
|
||||
updateFrequency: 'weekly',
|
||||
status: 'deprecated',
|
||||
createdAt: '2023-05-01',
|
||||
updatedAt: '2024-01-15',
|
||||
author: '评估组C',
|
||||
tags: ['数据', '完整度', '定量'],
|
||||
trend: 'stable',
|
||||
relatedIndices: [],
|
||||
},
|
||||
];
|
||||
|
||||
const mockSystems: EvaluationSystem[] = [
|
||||
{
|
||||
id: 'sys-001',
|
||||
name: '网络态势综合评估体系',
|
||||
description: '用于评估网络空间整体态势的综合指标体系',
|
||||
category: '态势评估',
|
||||
indices: ['idx-001', 'idx-002', 'idx-003'],
|
||||
status: 'active',
|
||||
createdAt: '2023-06-01',
|
||||
updatedAt: '2024-03-10',
|
||||
author: '评估部',
|
||||
},
|
||||
{
|
||||
id: 'sys-002',
|
||||
name: '响应效能评估体系',
|
||||
description: '评估应急响应和处置效能的指标体系',
|
||||
category: '效能评估',
|
||||
indices: ['idx-004', 'idx-005'],
|
||||
status: 'active',
|
||||
createdAt: '2023-09-01',
|
||||
updatedAt: '2024-03-08',
|
||||
author: '效能组',
|
||||
},
|
||||
];
|
||||
|
||||
const typeLabels: Record<EvaluationIndex['type'], string> = {
|
||||
quantitative: '定量',
|
||||
qualitative: '定性',
|
||||
composite: '综合',
|
||||
};
|
||||
|
||||
const typeColors: Record<EvaluationIndex['type'], string> = {
|
||||
quantitative: 'bg-blue-500/20 text-blue-400',
|
||||
qualitative: 'bg-purple-500/20 text-purple-400',
|
||||
composite: 'bg-orange-500/20 text-orange-400',
|
||||
};
|
||||
|
||||
const statusColors: Record<EvaluationIndex['status'], { bg: string; text: string; label: string }> = {
|
||||
active: { bg: 'bg-green-500/20', text: 'text-green-400', label: '使用中' },
|
||||
draft: { bg: 'bg-yellow-500/20', text: 'text-yellow-400', label: '草稿' },
|
||||
deprecated: { bg: 'bg-gray-500/20', text: 'text-gray-400', label: '已废弃' },
|
||||
};
|
||||
|
||||
const frequencyLabels: Record<EvaluationIndex['updateFrequency'], string> = {
|
||||
realtime: '实时',
|
||||
hourly: '每小时',
|
||||
daily: '每日',
|
||||
weekly: '每周',
|
||||
monthly: '每月',
|
||||
quarterly: '每季度',
|
||||
};
|
||||
|
||||
const trendIcons: Record<EvaluationIndex['trend'], React.ReactNode> = {
|
||||
up: <TrendingUp className="w-4 h-4 text-green-400" />,
|
||||
down: <TrendingDown className="w-4 h-4 text-red-400" />,
|
||||
stable: <Minus className="w-4 h-4 text-gray-400" />,
|
||||
};
|
||||
|
||||
const categories = ['全部', '影响力评估', '传播效果', '舆情分析', '效能评估', '协作效能', '预警效能', '策略评估', '数据质量'];
|
||||
|
||||
const EvaluationIndexResourceManagement: React.FC<EvaluationIndexResourceManagementProps> = ({ onNavigate }) => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
|
||||
const [viewMode, setViewMode] = useState<'index' | 'system'>('index');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedCategory, setSelectedCategory] = useState('全部');
|
||||
const [selectedType, setSelectedType] = useState<string>('all');
|
||||
const [selectedStatus, setSelectedStatus] = useState<string>('all');
|
||||
const [selectedIndex, setSelectedIndex] = useState<EvaluationIndex | null>(null);
|
||||
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(new Set(['影响力评估', '传播效果', '舆情分析']));
|
||||
|
||||
const filteredIndices = mockIndices.filter(index => {
|
||||
const matchesSearch = index.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
index.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
index.tags.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase()));
|
||||
const matchesCategory = selectedCategory === '全部' || index.category === selectedCategory;
|
||||
const matchesType = selectedType === 'all' || index.type === selectedType;
|
||||
const matchesStatus = selectedStatus === 'all' || index.status === selectedStatus;
|
||||
return matchesSearch && matchesCategory && matchesType && matchesStatus;
|
||||
});
|
||||
|
||||
const stats = {
|
||||
total: mockIndices.length,
|
||||
active: mockIndices.filter(i => i.status === 'active').length,
|
||||
draft: mockIndices.filter(i => i.status === 'draft').length,
|
||||
deprecated: mockIndices.filter(i => i.status === 'deprecated').length,
|
||||
};
|
||||
|
||||
const groupedIndices = filteredIndices.reduce((acc, index) => {
|
||||
if (!acc[index.category]) {
|
||||
acc[index.category] = [];
|
||||
}
|
||||
acc[index.category].push(index);
|
||||
return acc;
|
||||
}, {} as Record<string, EvaluationIndex[]>);
|
||||
|
||||
const toggleCategory = (category: string) => {
|
||||
setExpandedCategories(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(category)) {
|
||||
next.delete(category);
|
||||
} else {
|
||||
next.add(category);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const calculateProgress = (index: EvaluationIndex) => {
|
||||
if (!index.baseline || !index.target || !index.currentValue) return null;
|
||||
const progress = ((index.currentValue - index.baseline) / (index.target - index.baseline)) * 100;
|
||||
return Math.min(Math.max(progress, 0), 100);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 页面标题 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`p-2 rounded-lg ${isDark ? 'bg-amber-500/20' : 'bg-amber-100'}`}>
|
||||
<Scale className={`w-6 h-6 ${isDark ? 'text-amber-400' : 'text-amber-600'}`} />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className={`text-2xl font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
||||
评估指标体系资源管理
|
||||
</h1>
|
||||
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
管理和维护评估指标库与指标体系
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg font-medium transition-all ${
|
||||
isDark
|
||||
? 'bg-slate-700 hover:bg-slate-600 text-white'
|
||||
: 'bg-gray-100 hover:bg-gray-200 text-gray-700'
|
||||
}`}
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
导出
|
||||
</button>
|
||||
<button
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg font-medium transition-all ${
|
||||
isDark
|
||||
? 'bg-amber-600 hover:bg-amber-500 text-white'
|
||||
: 'bg-amber-500 hover:bg-amber-600 text-white'
|
||||
}`}
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
添加指标
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<div className={`p-4 rounded-xl ${isDark ? 'bg-slate-800/50' : 'bg-white'} border ${isDark ? 'border-amber-800/30' : 'border-amber-100'}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>指标总数</p>
|
||||
<p className={`text-2xl font-semibold mt-1 ${isDark ? 'text-white' : 'text-gray-900'}`}>{stats.total}</p>
|
||||
</div>
|
||||
<div className={`p-3 rounded-lg ${isDark ? 'bg-amber-500/20' : 'bg-amber-50'}`}>
|
||||
<Scale className={`w-6 h-6 ${isDark ? 'text-amber-400' : 'text-amber-500'}`} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`p-4 rounded-xl ${isDark ? 'bg-slate-800/50' : 'bg-white'} border ${isDark ? 'border-green-800/30' : 'border-green-100'}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>使用中</p>
|
||||
<p className={`text-2xl font-semibold mt-1 ${isDark ? 'text-white' : 'text-gray-900'}`}>{stats.active}</p>
|
||||
</div>
|
||||
<div className={`p-3 rounded-lg bg-green-500/20`}>
|
||||
<CheckCircle className="w-6 h-6 text-green-400" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`p-4 rounded-xl ${isDark ? 'bg-slate-800/50' : 'bg-white'} border ${isDark ? 'border-yellow-800/30' : 'border-yellow-100'}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>草稿</p>
|
||||
<p className={`text-2xl font-semibold mt-1 ${isDark ? 'text-white' : 'text-gray-900'}`}>{stats.draft}</p>
|
||||
</div>
|
||||
<div className={`p-3 rounded-lg bg-yellow-500/20`}>
|
||||
<Edit className="w-6 h-6 text-yellow-400" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`p-4 rounded-xl ${isDark ? 'bg-slate-800/50' : 'bg-white'} border ${isDark ? 'border-gray-700' : 'border-gray-200'}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>已废弃</p>
|
||||
<p className={`text-2xl font-semibold mt-1 ${isDark ? 'text-white' : 'text-gray-900'}`}>{stats.deprecated}</p>
|
||||
</div>
|
||||
<div className={`p-3 rounded-lg bg-gray-500/20`}>
|
||||
<AlertCircle className="w-6 h-6 text-gray-400" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 视图切换和筛选栏 */}
|
||||
<div className="space-y-4">
|
||||
{/* 视图切换 */}
|
||||
<div className={`flex items-center rounded-lg p-1 w-fit ${isDark ? 'bg-slate-800/50' : 'bg-gray-100'}`}>
|
||||
<button
|
||||
onClick={() => setViewMode('index')}
|
||||
className={`px-4 py-2 rounded-md text-sm font-medium transition-all ${
|
||||
viewMode === 'index'
|
||||
? isDark ? 'bg-amber-500/20 text-amber-400' : 'bg-white text-amber-600 shadow-sm'
|
||||
: isDark ? 'text-slate-400' : 'text-gray-500'
|
||||
}`}
|
||||
>
|
||||
<Scale className="w-4 h-4 inline mr-2" />
|
||||
指标管理
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('system')}
|
||||
className={`px-4 py-2 rounded-md text-sm font-medium transition-all ${
|
||||
viewMode === 'system'
|
||||
? isDark ? 'bg-amber-500/20 text-amber-400' : 'bg-white text-amber-600 shadow-sm'
|
||||
: isDark ? 'text-slate-400' : 'text-gray-500'
|
||||
}`}
|
||||
>
|
||||
<BarChart3 className="w-4 h-4 inline mr-2" />
|
||||
体系管理
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 筛选栏 */}
|
||||
<div className={`p-4 rounded-xl ${isDark ? 'bg-slate-800/50' : 'bg-white'} border ${isDark ? 'border-amber-800/30' : 'border-amber-100'}`}>
|
||||
<div className="flex items-center gap-4 flex-wrap">
|
||||
{/* 搜索框 */}
|
||||
<div className="flex-1 min-w-[200px] relative">
|
||||
<Search className={`absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 ${isDark ? 'text-slate-400' : 'text-gray-400'}`} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索指标名称、描述或标签..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className={`w-full pl-10 pr-4 py-2 rounded-lg border transition-all ${
|
||||
isDark
|
||||
? 'bg-slate-900/50 border-slate-700 text-white placeholder-slate-500 focus:border-amber-500'
|
||||
: 'bg-gray-50 border-gray-200 text-gray-900 placeholder-gray-400 focus:border-amber-400'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 分类筛选 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Filter className={`w-4 h-4 ${isDark ? 'text-slate-400' : 'text-gray-400'}`} />
|
||||
<select
|
||||
value={selectedCategory}
|
||||
onChange={(e) => setSelectedCategory(e.target.value)}
|
||||
className={`px-3 py-2 rounded-lg border transition-all ${
|
||||
isDark
|
||||
? 'bg-slate-900/50 border-slate-700 text-white'
|
||||
: 'bg-gray-50 border-gray-200 text-gray-900'
|
||||
}`}
|
||||
>
|
||||
{categories.map(cat => (
|
||||
<option key={cat} value={cat}>{cat}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 类型筛选 */}
|
||||
<select
|
||||
value={selectedType}
|
||||
onChange={(e) => setSelectedType(e.target.value)}
|
||||
className={`px-3 py-2 rounded-lg border transition-all ${
|
||||
isDark
|
||||
? 'bg-slate-900/50 border-slate-700 text-white'
|
||||
: 'bg-gray-50 border-gray-200 text-gray-900'
|
||||
}`}
|
||||
>
|
||||
<option value="all">全部类型</option>
|
||||
<option value="quantitative">定量</option>
|
||||
<option value="qualitative">定性</option>
|
||||
<option value="composite">综合</option>
|
||||
</select>
|
||||
|
||||
{/* 状态筛选 */}
|
||||
<select
|
||||
value={selectedStatus}
|
||||
onChange={(e) => setSelectedStatus(e.target.value)}
|
||||
className={`px-3 py-2 rounded-lg border transition-all ${
|
||||
isDark
|
||||
? 'bg-slate-900/50 border-slate-700 text-white'
|
||||
: 'bg-gray-50 border-gray-200 text-gray-900'
|
||||
}`}
|
||||
>
|
||||
<option value="all">全部状态</option>
|
||||
<option value="active">使用中</option>
|
||||
<option value="draft">草稿</option>
|
||||
<option value="deprecated">已废弃</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 指标列表 */}
|
||||
{viewMode === 'index' ? (
|
||||
<div className={`rounded-xl overflow-hidden border ${isDark ? 'bg-slate-800/50 border-amber-800/30' : 'bg-white border-amber-100'}`}>
|
||||
{Object.entries(groupedIndices).map(([category, indices]) => (
|
||||
<div key={category} className="border-b border-gray-200 dark:border-slate-700 last:border-b-0">
|
||||
{/* 分类标题 */}
|
||||
<button
|
||||
onClick={() => toggleCategory(category)}
|
||||
className={`w-full px-4 py-3 flex items-center justify-between ${
|
||||
isDark ? 'hover:bg-slate-700/50' : 'hover:bg-gray-50'
|
||||
} transition-colors`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
{expandedCategories.has(category) ? (
|
||||
<FolderOpen className={`w-5 h-5 ${isDark ? 'text-amber-400' : 'text-amber-500'}`} />
|
||||
) : (
|
||||
<Folder className={`w-5 h-5 ${isDark ? 'text-amber-400' : 'text-amber-500'}`} />
|
||||
)}
|
||||
<span className={`font-medium ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
||||
{category}
|
||||
</span>
|
||||
<span className={`px-2 py-0.5 rounded-full text-xs ${
|
||||
isDark ? 'bg-slate-700 text-slate-300' : 'bg-gray-100 text-gray-600'
|
||||
}`}>
|
||||
{indices.length}个指标
|
||||
</span>
|
||||
</div>
|
||||
<ChevronRight className={`w-4 h-4 transition-transform ${expandedCategories.has(category) ? 'rotate-90' : ''} ${isDark ? 'text-slate-400' : 'text-gray-400'}`} />
|
||||
</button>
|
||||
|
||||
{/* 指标列表 */}
|
||||
{expandedCategories.has(category) && (
|
||||
<div className={`${isDark ? 'bg-slate-900/30' : 'bg-gray-50/50'}`}>
|
||||
{indices.map((index, idx) => (
|
||||
<div
|
||||
key={index.id}
|
||||
className={`px-4 py-4 cursor-pointer transition-colors ${
|
||||
isDark ? 'hover:bg-slate-700/50' : 'hover:bg-white'
|
||||
} ${idx !== indices.length - 1 ? 'border-b border-gray-200 dark:border-slate-700/50' : ''}`}
|
||||
onClick={() => setSelectedIndex(index)}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className={`p-2 rounded-lg ${typeColors[index.type]}`}>
|
||||
{index.type === 'quantitative' ? <BarChart3 className="w-4 h-4" /> :
|
||||
index.type === 'qualitative' ? <PieChart className="w-4 h-4" /> :
|
||||
<Activity className="w-4 h-4" />}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className={`font-medium ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
||||
{index.name}
|
||||
</h3>
|
||||
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
{index.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 进度条 */}
|
||||
{index.currentValue !== undefined && index.baseline !== undefined && index.target !== undefined && (
|
||||
<div className="mt-3">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
当前值: {index.currentValue}
|
||||
</span>
|
||||
<span className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
目标: {index.target}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2 bg-gray-200 dark:bg-slate-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full transition-all ${
|
||||
calculateProgress(index)! >= 100 ? 'bg-green-500' :
|
||||
calculateProgress(index)! >= 50 ? 'bg-amber-500' : 'bg-red-500'
|
||||
}`}
|
||||
style={{ width: `${calculateProgress(index)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 ml-4">
|
||||
<div className="text-right">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`px-2 py-1 rounded text-xs ${typeColors[index.type]}`}>
|
||||
{typeLabels[index.type]}
|
||||
</span>
|
||||
<span className={`px-2 py-1 rounded text-xs ${statusColors[index.status].bg} ${statusColors[index.status].text}`}>
|
||||
{statusColors[index.status].label}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-1 justify-end">
|
||||
{index.trend && trendIcons[index.trend]}
|
||||
<span className={`text-xs ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
{index.updateFrequency === 'realtime' ? '实时' :
|
||||
index.updateFrequency === 'daily' ? '每日' :
|
||||
index.updateFrequency === 'weekly' ? '每周' :
|
||||
index.updateFrequency === 'monthly' ? '每月' : '每季度'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 mt-3 pt-3 border-t border-gray-200 dark:border-slate-700/50">
|
||||
<div className="flex items-center gap-1 text-sm">
|
||||
<Star className={`w-4 h-4 ${isDark ? 'text-amber-400' : 'text-amber-500'}`} />
|
||||
<span className={isDark ? 'text-slate-400' : 'text-gray-500'}>权重: {index.weight * 100}%</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{index.tags.slice(0, 3).map(tag => (
|
||||
<span
|
||||
key={tag}
|
||||
className={`px-2 py-0.5 rounded text-xs ${
|
||||
isDark ? 'bg-slate-700 text-slate-300' : 'bg-gray-100 text-gray-600'
|
||||
}`}
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
/* 体系管理视图 */
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{mockSystems.map(system => (
|
||||
<div
|
||||
key={system.id}
|
||||
className={`p-5 rounded-xl ${isDark ? 'bg-slate-800/50' : 'bg-white'} border ${isDark ? 'border-amber-800/30' : 'border-amber-100'}`}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`p-3 rounded-lg ${isDark ? 'bg-amber-500/20' : 'bg-amber-50'}`}>
|
||||
<BarChart3 className={`w-6 h-6 ${isDark ? 'text-amber-400' : 'text-amber-500'}`} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className={`font-medium ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
||||
{system.name}
|
||||
</h3>
|
||||
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
{system.category}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
||||
system.status === 'active' ? 'bg-green-500/20 text-green-400' :
|
||||
system.status === 'draft' ? 'bg-yellow-500/20 text-yellow-400' :
|
||||
'bg-gray-500/20 text-gray-400'
|
||||
}`}>
|
||||
{system.status === 'active' ? '使用中' : system.status === 'draft' ? '草稿' : '已归档'}
|
||||
</span>
|
||||
</div>
|
||||
<p className={`text-sm mb-4 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
{system.description}
|
||||
</p>
|
||||
<div className="flex items-center justify-between pt-3 border-t border-gray-200 dark:border-slate-700">
|
||||
<span className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
包含 {system.indices.length} 个指标
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<button className={`p-1.5 rounded-lg ${isDark ? 'hover:bg-slate-700 text-slate-400' : 'hover:bg-gray-100 text-gray-500'}`}>
|
||||
<Edit className="w-4 h-4" />
|
||||
</button>
|
||||
<button className={`p-1.5 rounded-lg ${isDark ? 'hover:bg-slate-700 text-slate-400' : 'hover:bg-gray-100 text-gray-500'}`}>
|
||||
<Copy className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 指标详情弹窗 */}
|
||||
{selectedIndex && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50" onClick={() => setSelectedIndex(null)}>
|
||||
<div
|
||||
className={`w-full max-w-3xl rounded-2xl ${isDark ? 'bg-slate-800' : 'bg-white'} shadow-xl max-h-[90vh] overflow-y-auto`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="sticky top-0 p-6 border-b ${isDark ? 'border-slate-700 bg-slate-800' : 'border-gray-200 bg-white'}">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`p-3 rounded-lg ${typeColors[selectedIndex.type]}`}>
|
||||
{selectedIndex.type === 'quantitative' ? <BarChart3 className="w-5 h-5" /> :
|
||||
selectedIndex.type === 'qualitative' ? <PieChart className="w-5 h-5" /> :
|
||||
<Activity className="w-5 h-5" />}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className={`text-xl font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
||||
{selectedIndex.name}
|
||||
</h2>
|
||||
<span className={`px-2 py-0.5 rounded text-xs ${typeColors[selectedIndex.type]}`}>
|
||||
{typeLabels[selectedIndex.type]}
|
||||
</span>
|
||||
</div>
|
||||
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
{selectedIndex.category}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setSelectedIndex(null)}
|
||||
className={`p-2 rounded-lg ${isDark ? 'hover:bg-slate-700 text-slate-400' : 'hover:bg-gray-100 text-gray-500'}`}
|
||||
>
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 space-y-6">
|
||||
<div>
|
||||
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'} mb-2`}>指标描述</p>
|
||||
<p className={isDark ? 'text-white' : 'text-gray-900'}>{selectedIndex.description}</p>
|
||||
</div>
|
||||
|
||||
{selectedIndex.calculationMethod && (
|
||||
<div className={`p-4 rounded-lg ${isDark ? 'bg-slate-900/50' : 'bg-gray-50'}`}>
|
||||
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'} mb-1`}>计算方法</p>
|
||||
<p className={isDark ? 'text-white' : 'text-gray-700'}>{selectedIndex.calculationMethod}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedIndex.dataSource && (
|
||||
<div>
|
||||
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'} mb-1`}>数据来源</p>
|
||||
<p className={isDark ? 'text-white' : 'text-gray-900'}>{selectedIndex.dataSource}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'} mb-1`}>权重</p>
|
||||
<p className={`font-medium ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
||||
{selectedIndex.weight * 100}%
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'} mb-1`}>更新频率</p>
|
||||
<p className={`font-medium ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
||||
{frequencyLabels[selectedIndex.updateFrequency]}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'} mb-1`}>状态</p>
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${statusColors[selectedIndex.status].bg} ${statusColors[selectedIndex.status].text}`}>
|
||||
{statusColors[selectedIndex.status].label}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'} mb-1`}>趋势</p>
|
||||
<div className="flex items-center gap-2">
|
||||
{trendIcons[selectedIndex.trend]}
|
||||
<span className={isDark ? 'text-white' : 'text-gray-900'}>
|
||||
{selectedIndex.trend === 'up' ? '上升' : selectedIndex.trend === 'down' ? '下降' : '稳定'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{selectedIndex.baseline !== undefined && (
|
||||
<div>
|
||||
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'} mb-1`}>基线值</p>
|
||||
<p className={isDark ? 'text-white' : 'text-gray-900'}>{selectedIndex.baseline}</p>
|
||||
</div>
|
||||
)}
|
||||
{selectedIndex.target !== undefined && (
|
||||
<div>
|
||||
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'} mb-1`}>目标值</p>
|
||||
<p className={isDark ? 'text-white' : 'text-gray-900'}>{selectedIndex.target}</p>
|
||||
</div>
|
||||
)}
|
||||
{selectedIndex.currentValue !== undefined && (
|
||||
<div>
|
||||
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'} mb-1`}>当前值</p>
|
||||
<p className={`font-medium ${isDark ? 'text-white' : 'text-gray-900'}`}>{selectedIndex.currentValue}</p>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'} mb-1`}>作者</p>
|
||||
<p className={isDark ? 'text-white' : 'text-gray-900'}>{selectedIndex.author}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'} mb-1`}>更新时间</p>
|
||||
<p className={isDark ? 'text-white' : 'text-gray-900'}>{selectedIndex.updatedAt}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'} mb-2`}>标签</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedIndex.tags.map(tag => (
|
||||
<span
|
||||
key={tag}
|
||||
className={`px-3 py-1 rounded-full text-sm ${
|
||||
isDark ? 'bg-slate-700 text-slate-300' : 'bg-gray-100 text-gray-600'
|
||||
}`}
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedIndex.relatedIndices.length > 0 && (
|
||||
<div>
|
||||
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'} mb-2`}>关联指标</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedIndex.relatedIndices.map(relatedId => {
|
||||
const related = mockIndices.find(i => i.id === relatedId);
|
||||
return related ? (
|
||||
<span
|
||||
key={relatedId}
|
||||
className={`px-3 py-1 rounded-full text-sm cursor-pointer ${
|
||||
isDark ? 'bg-amber-500/20 text-amber-400' : 'bg-amber-50 text-amber-600'
|
||||
}`}
|
||||
>
|
||||
{related.name}
|
||||
</span>
|
||||
) : null;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={`p-6 border-t ${isDark ? 'border-slate-700' : 'border-gray-200'} flex justify-end gap-3`}>
|
||||
<button
|
||||
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||
isDark ? 'bg-slate-700 hover:bg-slate-600 text-white' : 'bg-gray-100 hover:bg-gray-200 text-gray-700'
|
||||
}`}
|
||||
>
|
||||
<Download className="w-4 h-4 inline mr-2" />
|
||||
导出
|
||||
</button>
|
||||
<button
|
||||
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||
isDark ? 'bg-amber-600 hover:bg-amber-500 text-white' : 'bg-amber-500 hover:bg-amber-600 text-white'
|
||||
}`}
|
||||
>
|
||||
<Edit className="w-4 h-4 inline mr-2" />
|
||||
编辑
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EvaluationIndexResourceManagement;
|
||||
@@ -0,0 +1,529 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { PageType } from '../components/Sidebar';
|
||||
import {
|
||||
FileText,
|
||||
Download,
|
||||
Printer,
|
||||
RefreshCw,
|
||||
ArrowLeft,
|
||||
ChevronRight,
|
||||
Target,
|
||||
Users,
|
||||
TrendingUp,
|
||||
MessageSquare,
|
||||
Megaphone,
|
||||
Shield,
|
||||
BarChart3,
|
||||
Link2,
|
||||
Clock,
|
||||
AlertTriangle,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
History,
|
||||
FileUp,
|
||||
Send,
|
||||
Eye,
|
||||
} from 'lucide-react';
|
||||
|
||||
interface FinalPlanProps {
|
||||
onNavigate: (page: PageType) => void;
|
||||
}
|
||||
|
||||
const FinalPlan: React.FC<FinalPlanProps> = ({ onNavigate }) => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
|
||||
// 主题颜色类
|
||||
const cardBg = isDark ? 'bg-slate-800/30' : 'bg-white';
|
||||
const cardBorder = isDark ? 'border-blue-800/30' : 'border-red-200';
|
||||
const textPrimary = isDark ? 'text-white' : 'text-gray-900';
|
||||
const textSecondary = isDark ? 'text-slate-400' : 'text-gray-600';
|
||||
const textMuted = isDark ? 'text-slate-500' : 'text-gray-500';
|
||||
const btnSecondary = isDark ? 'bg-slate-700/50 hover:bg-slate-600/50 border-blue-800/30 text-slate-300' : 'bg-gray-100 hover:bg-gray-200 border-gray-300 text-gray-700';
|
||||
const btnPrimary = isDark ? 'bg-blue-600 hover:bg-blue-500' : 'bg-red-600 hover:bg-red-500';
|
||||
const inputBg = isDark ? 'bg-slate-900/50' : 'bg-gray-50';
|
||||
const inputBorder = isDark ? 'border-blue-800/30' : 'border-gray-300';
|
||||
|
||||
const [activeSection, setActiveSection] = useState('background');
|
||||
const [reviewStatus, setReviewStatus] = useState<'draft' | 'pending' | 'approved' | 'rejected'>('pending');
|
||||
const [showVersionHistory, setShowVersionHistory] = useState(false);
|
||||
|
||||
// 版本历史数据
|
||||
const versionHistory = [
|
||||
{ version: 'V3', date: '2026-03-19 16:00', author: '张策略', changes: '完成驱动分析与叙事策略优化' },
|
||||
{ version: 'V2', date: '2026-03-19 14:30', author: '李智能', changes: '修正受众细分,补充渠道安排' },
|
||||
{ version: 'V1', date: '2026-03-19 10:00', author: '张策略', changes: '初始版本生成' },
|
||||
];
|
||||
|
||||
const sections = [
|
||||
{ id: 'background', label: '一、任务背景', icon: FileText },
|
||||
{ id: 'goal', label: '二、宣传目标', icon: Target },
|
||||
{ id: 'audience', label: '三、目标受众', icon: Users },
|
||||
{ id: 'behavior', label: '四、预期行为', icon: TrendingUp },
|
||||
{ id: 'driver', label: '五、驱动因素', icon: MessageSquare },
|
||||
{ id: 'narrative', label: '六、叙事策略', icon: Megaphone },
|
||||
{ id: 'channel', label: '七、渠道安排', icon: Link2 },
|
||||
{ id: 'risk', label: '八、风险控制', icon: Shield },
|
||||
{ id: 'evaluation', label: '九、效果评估', icon: BarChart3 },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 顶部操作栏 */}
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-4`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={() => onNavigate('strategy-result')}
|
||||
className={`flex items-center gap-2 px-3 py-2 ${btnSecondary} rounded-lg text-sm transition-colors`}
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
返回
|
||||
</button>
|
||||
<div className={`h-8 w-px ${isDark ? 'bg-blue-800/30' : 'bg-gray-200'}`} />
|
||||
<div>
|
||||
<h1 className={`${textPrimary} font-bold text-lg`}>五四青年节宣传活动策略方案</h1>
|
||||
<p className={`${textSecondary} text-sm`}>最终版方案 · V3 · 2026-03-19</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
{/* 审核状态 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`${textSecondary} text-sm`}>审核状态:</span>
|
||||
<div className={`flex items-center gap-1.5 px-3 py-1.5 rounded-full text-sm font-medium ${
|
||||
reviewStatus === 'approved'
|
||||
? 'bg-green-500/20 text-green-400'
|
||||
: reviewStatus === 'pending'
|
||||
? 'bg-yellow-500/20 text-yellow-400'
|
||||
: reviewStatus === 'rejected'
|
||||
? 'bg-red-500/20 text-red-400'
|
||||
: `${inputBg} ${textSecondary}`
|
||||
}`}>
|
||||
{reviewStatus === 'approved' ? (
|
||||
<><CheckCircle className="w-4 h-4" /> 已通过</>
|
||||
) : reviewStatus === 'pending' ? (
|
||||
<><Clock className="w-4 h-4" /> 审核中</>
|
||||
) : reviewStatus === 'rejected' ? (
|
||||
<><XCircle className="w-4 h-4" /> 未通过</>
|
||||
) : (
|
||||
<><FileText className="w-4 h-4" /> 草稿</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 版本历史按钮 */}
|
||||
<button
|
||||
onClick={() => setShowVersionHistory(!showVersionHistory)}
|
||||
className={`flex items-center gap-2 px-3 py-2 ${btnSecondary} rounded-lg text-sm transition-colors`}
|
||||
>
|
||||
<History className="w-4 h-4" />
|
||||
版本历史
|
||||
</button>
|
||||
|
||||
<div className={`h-8 w-px ${isDark ? 'bg-blue-800/30' : 'bg-gray-200'}`} />
|
||||
|
||||
<button className={`flex items-center gap-2 px-4 py-2.5 ${btnSecondary} rounded-lg text-sm transition-colors`}>
|
||||
<Printer className="w-4 h-4" />
|
||||
打印
|
||||
</button>
|
||||
<button className={`flex items-center gap-2 px-4 py-2.5 ${btnSecondary} rounded-lg text-sm transition-colors`}>
|
||||
<Download className="w-4 h-4" />
|
||||
导出Word
|
||||
</button>
|
||||
<button className={`flex items-center gap-2 px-4 py-2.5 ${btnSecondary} rounded-lg text-sm transition-colors`}>
|
||||
<Download className="w-4 h-4" />
|
||||
导出PDF
|
||||
</button>
|
||||
<button className={`flex items-center gap-2 px-4 py-2.5 ${btnPrimary} text-white rounded-lg text-sm transition-colors`}>
|
||||
<FileUp className="w-4 h-4" />
|
||||
导出汇报提纲
|
||||
</button>
|
||||
|
||||
{/* 审核操作 */}
|
||||
{reviewStatus === 'pending' ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setReviewStatus('approved')}
|
||||
className={`flex items-center gap-2 px-4 py-2.5 ${btnPrimary} text-white rounded-lg text-sm transition-colors`}
|
||||
>
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
通过
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setReviewStatus('rejected')}
|
||||
className="flex items-center gap-2 px-4 py-2.5 bg-red-600/20 hover:bg-red-600/30 border border-red-500/30 text-red-400 rounded-lg text-sm transition-colors"
|
||||
>
|
||||
<XCircle className="w-4 h-4" />
|
||||
驳回
|
||||
</button>
|
||||
</div>
|
||||
) : reviewStatus === 'draft' ? (
|
||||
<button className={`flex items-center gap-2 px-4 py-2.5 ${btnPrimary} text-white rounded-lg text-sm transition-colors`}>
|
||||
<Send className="w-4 h-4" />
|
||||
提交审核
|
||||
</button>
|
||||
) : (
|
||||
<button className={`flex items-center gap-2 px-4 py-2.5 ${btnPrimary} text-white rounded-lg text-sm transition-colors`}>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
发起修订
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 版本历史面板 */}
|
||||
{showVersionHistory && (
|
||||
<div className={`mt-4 pt-4 border-t ${cardBorder}`}>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{versionHistory.map((v, i) => (
|
||||
<div
|
||||
key={v.version}
|
||||
className={`p-4 rounded-lg border ${
|
||||
i === 0
|
||||
? 'bg-blue-500/10 border-blue-500/30'
|
||||
: `${inputBg} ${cardBorder}`
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className={`text-sm font-bold ${
|
||||
i === 0 ? 'text-blue-400' : textSecondary
|
||||
}`}>
|
||||
{v.version}
|
||||
</span>
|
||||
{i === 0 && (
|
||||
<span className="px-2 py-0.5 bg-blue-500/20 text-blue-400 rounded text-xs">
|
||||
当前
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className={`${textSecondary} text-xs mb-1`}>{v.date}</p>
|
||||
<p className={`${textPrimary} text-sm`}>{v.changes}</p>
|
||||
<p className={`${textMuted} text-xs mt-1`}>作者:{v.author}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 gap-6">
|
||||
{/* 左侧导航目录 (20%) */}
|
||||
<div className="col-span-1">
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-4 sticky top-6`}>
|
||||
<h3 className={`${textPrimary} font-semibold mb-4`}>目录导航</h3>
|
||||
<nav className="space-y-1">
|
||||
{sections.map((section) => (
|
||||
<button
|
||||
key={section.id}
|
||||
onClick={() => setActiveSection(section.id)}
|
||||
className={`w-full flex items-center gap-2 px-3 py-2 rounded-lg text-left text-sm transition-colors ${
|
||||
activeSection === section.id
|
||||
? 'bg-blue-600/20 text-blue-400 border border-blue-500/30'
|
||||
: `${textSecondary} hover:${isDark ? 'bg-slate-700/50' : 'bg-gray-100'} hover:${textPrimary}`
|
||||
}`}
|
||||
>
|
||||
<span className="text-xs opacity-50">{section.label.split('、')[0]}</span>
|
||||
<span>{section.label.split('、')[1]}</span>
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* 版本信息 */}
|
||||
<div className={`mt-6 pt-4 border-t ${cardBorder}`}>
|
||||
<div className={`flex items-center gap-2 ${textSecondary} text-xs mb-2`}>
|
||||
<Clock className="w-3 h-3" />
|
||||
版本信息
|
||||
</div>
|
||||
<div className="space-y-1 text-xs">
|
||||
<p className={textSecondary}>当前版本:<span className={isDark ? 'text-cyan-400' : 'text-red-500'}>V3</span></p>
|
||||
<p className={textSecondary}>生成时间:2026-03-19 16:00</p>
|
||||
<p className={textSecondary}>关联任务:TASK-2026-0319-001</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 风险提示 */}
|
||||
<div className={`mt-4 p-3 bg-amber-500/10 border border-amber-500/30 rounded-lg`}>
|
||||
<div className={`flex items-center gap-2 text-amber-400 text-xs font-medium mb-1`}>
|
||||
<AlertTriangle className="w-3 h-3" />
|
||||
风险提示
|
||||
</div>
|
||||
<p className={`${textSecondary} text-xs`}>需关注预算控制和内容质量</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 中间正文区 (60%) */}
|
||||
<div className="col-span-2">
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-8`}>
|
||||
{/* 标题 */}
|
||||
<div className={`text-center mb-8 pb-6 border-b ${cardBorder}`}>
|
||||
<h1 className={`text-2xl font-bold ${textPrimary} mb-2`}>五四青年节宣传活动策略方案</h1>
|
||||
<p className={textSecondary}>最终版 · V3</p>
|
||||
</div>
|
||||
|
||||
{/* 逻辑总括 - 高亮显示 */}
|
||||
<div className="mb-8 p-6 bg-gradient-to-r from-blue-600/20 to-cyan-600/20 border border-blue-500/30 rounded-xl">
|
||||
<p className={`${textSecondary} leading-relaxed`}>
|
||||
<strong className="text-cyan-400">逻辑总括:</strong>
|
||||
本次宣传拟面向<span className="text-cyan-400 font-medium">【18-25岁基层青年】</span>,
|
||||
围绕其<span className="text-yellow-400 font-medium">【社交认同与自我表达】</span>驱动,
|
||||
传播<span className="text-purple-400 font-medium">【真实、有态度的青年故事】</span>叙事,
|
||||
促使其产生<span className="text-green-400 font-medium">【主动参与互动并生成UGC内容】</span>行为,
|
||||
从而实现<span className="text-blue-400 font-medium">【提升品牌在Z世代用户中的认知度】</span>目标。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 正文内容 */}
|
||||
<div className="space-y-8">
|
||||
{/* 一、任务背景 */}
|
||||
<section>
|
||||
<h2 className={`${textPrimary} font-bold text-lg mb-4 flex items-center gap-2`}>
|
||||
<FileText className="w-5 h-5 text-blue-400" />
|
||||
一、任务背景
|
||||
</h2>
|
||||
<div className={`${textSecondary} leading-relaxed space-y-3 pl-7`}>
|
||||
<p>五四青年节将至,公司希望借此契机开展一次面向年轻群体的品牌宣传活动,提升品牌在Z世代用户中的认知度和好感度。</p>
|
||||
<p>本次活动需要制定一套完整的宣传策略,包括目标受众分析、行为设计、叙事框架、渠道安排等内容。</p>
|
||||
<div className={`p-4 ${inputBg} ${cardBorder} rounded-lg`}>
|
||||
<p className={`${textMuted} text-sm mb-2`}>特殊约束</p>
|
||||
<p>• 预算有限,需注重性价比</p>
|
||||
<p>• 需在5月1日前完成所有准备工作</p>
|
||||
<p>• 传播效果需可量化评估</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 二、宣传目标 */}
|
||||
<section>
|
||||
<h2 className={`${textPrimary} font-bold text-lg mb-4 flex items-center gap-2`}>
|
||||
<Target className="w-5 h-5 text-blue-400" />
|
||||
二、宣传目标
|
||||
</h2>
|
||||
<div className="pl-7 space-y-4">
|
||||
<div className="p-4 bg-blue-500/10 border border-blue-500/30 rounded-lg">
|
||||
<p className="text-blue-400 text-sm font-medium mb-2">主目标</p>
|
||||
<p className={textPrimary}>提升品牌在Z世代用户中的认知度</p>
|
||||
<p className={`${textSecondary} text-sm mt-2`}>具体指标:活动期间品牌相关话题曝光量达500万+</p>
|
||||
</div>
|
||||
<div className="p-4 bg-green-500/10 border border-green-500/30 rounded-lg">
|
||||
<p className="text-green-400 text-sm font-medium mb-2">次目标</p>
|
||||
<p className={textPrimary}>建立品牌年轻化形象,增强用户情感连接</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 三、目标受众 */}
|
||||
<section>
|
||||
<h2 className={`${textPrimary} font-bold text-lg mb-4 flex items-center gap-2`}>
|
||||
<Users className="w-5 h-5 text-cyan-400" />
|
||||
三、目标受众
|
||||
</h2>
|
||||
<div className="pl-7 space-y-4">
|
||||
<div className="p-4 bg-cyan-500/10 border border-cyan-500/30 rounded-lg">
|
||||
<p className="text-cyan-400 text-sm font-medium mb-2">核心受众</p>
|
||||
<p className={`${textPrimary} font-medium`}>18-25岁基层青年</p>
|
||||
<div className="mt-3 grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p className={`${textMuted} text-xs mb-1`}>人口特征</p>
|
||||
<p className={`${textSecondary} text-sm`}>学生、基层工作者为主</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className={`${textMuted} text-xs mb-1`}>媒体习惯</p>
|
||||
<p className={`${textSecondary} text-sm`}>短视频、社交平台重度用户</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`p-4 ${inputBg} ${isDark ? 'border-slate-700/30' : 'border-gray-200'} rounded-lg`}>
|
||||
<p className={`${textSecondary} text-sm font-medium mb-2`}>次级受众</p>
|
||||
<p className={textSecondary}>25-30岁职场新人</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 四、预期行为及关键驱动 */}
|
||||
<section>
|
||||
<h2 className={`${textPrimary} font-bold text-lg mb-4 flex items-center gap-2`}>
|
||||
<TrendingUp className="w-5 h-5 text-green-400" />
|
||||
四、预期行为及关键驱动
|
||||
</h2>
|
||||
<div className="pl-7 space-y-4">
|
||||
<div className="p-4 bg-green-500/10 border border-green-500/30 rounded-lg">
|
||||
<p className="text-green-400 text-sm font-medium mb-2">目标行为</p>
|
||||
<p className={textPrimary}>主动参与话题讨论、生成UGC内容</p>
|
||||
<div className="mt-3 flex items-center gap-2">
|
||||
<span className={`${textSecondary} text-sm`}>转化路径:</span>
|
||||
{['浏览', '点赞', '评论', '转发', '创作'].map((step, i) => (
|
||||
<React.Fragment key={step}>
|
||||
<span className={`px-2 py-1 rounded text-xs ${
|
||||
i < 2 ? 'bg-green-500/20 text-green-400' : `${inputBg} ${textSecondary}`
|
||||
}`}>
|
||||
{step}
|
||||
</span>
|
||||
{i < 4 && <span className={isDark ? 'text-slate-600' : 'text-gray-400'}>→</span>}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 bg-yellow-500/10 border border-yellow-500/30 rounded-lg">
|
||||
<p className="text-yellow-400 text-sm font-medium mb-2">核心驱动</p>
|
||||
<p className={textPrimary}>社交认同与自我表达</p>
|
||||
<div className="mt-2 flex gap-2">
|
||||
{['归属感需求', '表达欲驱动', '群体认同激励'].map((item) => (
|
||||
<span key={item} className="px-2 py-1 bg-yellow-500/20 text-yellow-400 rounded text-xs">
|
||||
{item}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 五、叙事策略 */}
|
||||
<section>
|
||||
<h2 className={`${textPrimary} font-bold text-lg mb-4 flex items-center gap-2`}>
|
||||
<Megaphone className="w-5 h-5 text-purple-400" />
|
||||
五、叙事策略
|
||||
</h2>
|
||||
<div className="pl-7 space-y-4">
|
||||
<div className="p-4 bg-purple-500/10 border border-purple-500/30 rounded-lg">
|
||||
<p className="text-purple-400 text-sm font-medium mb-2">主叙事模式</p>
|
||||
<p className={textPrimary}>真实、有态度的青年故事</p>
|
||||
</div>
|
||||
<div className={`p-4 ${inputBg} ${isDark ? 'border-slate-700/30' : 'border-gray-200'} rounded-lg`}>
|
||||
<p className={`${textSecondary} text-sm font-medium mb-2`}>叙事策略</p>
|
||||
<p className={textSecondary}>人物+事件结合,以真实故事传递品牌价值观。讲述年轻人自己的故事,引发情感共鸣。</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 六、渠道与执行安排 */}
|
||||
<section>
|
||||
<h2 className={`${textPrimary} font-bold text-lg mb-4 flex items-center gap-2`}>
|
||||
<Link2 className="w-5 h-5 text-pink-400" />
|
||||
六、渠道与执行安排
|
||||
</h2>
|
||||
<div className="pl-7">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{[
|
||||
{ platform: '抖音', role: '主战场,短视频传播', weight: '40%' },
|
||||
{ platform: '微博', role: '话题引爆,社交传播', weight: '30%' },
|
||||
{ platform: '小红书', role: '种草转化,UGC沉淀', weight: '30%' },
|
||||
].map((channel) => (
|
||||
<div key={channel.platform} className="p-4 bg-pink-500/10 border border-pink-500/30 rounded-lg">
|
||||
<p className="text-pink-400 font-medium">{channel.platform}</p>
|
||||
<p className={`${textSecondary} text-sm mt-1`}>{channel.role}</p>
|
||||
<p className={`${textMuted} text-xs mt-2`}>预算占比:{channel.weight}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 七、风险控制 */}
|
||||
<section>
|
||||
<h2 className={`${textPrimary} font-bold text-lg mb-4 flex items-center gap-2`}>
|
||||
<Shield className="w-5 h-5 text-red-400" />
|
||||
七、风险控制
|
||||
</h2>
|
||||
<div className="pl-7 space-y-3">
|
||||
{[
|
||||
{ risk: '预算有限,需控制成本', mitigation: '提前布局预热,优化投放策略' },
|
||||
{ risk: '青年群体注意力分散', mitigation: '内容差异化,强调情感共鸣' },
|
||||
{ risk: 'UGC质量难以保证', mitigation: '设置激励机制,建立内容审核' },
|
||||
].map((item, i) => (
|
||||
<div key={i} className={`flex gap-4 p-3 ${inputBg} ${isDark ? 'border-slate-700/30' : 'border-gray-200'} rounded-lg`}>
|
||||
<div className="flex-1">
|
||||
<p className="text-red-400 text-xs font-medium">风险{i + 1}</p>
|
||||
<p className={`${textPrimary} text-sm`}>{item.risk}</p>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-green-400 text-xs font-medium">应对措施</p>
|
||||
<p className={`${textSecondary} text-sm`}>{item.mitigation}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 八、效果评估 */}
|
||||
<section>
|
||||
<h2 className={`${textPrimary} font-bold text-lg mb-4 flex items-center gap-2`}>
|
||||
<BarChart3 className="w-5 h-5 text-amber-400" />
|
||||
八、效果评估
|
||||
</h2>
|
||||
<div className="pl-7">
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
{[
|
||||
{ metric: '曝光量', target: '500万+', current: '0' },
|
||||
{ metric: '互动率', target: '5%', current: '0%' },
|
||||
{ metric: 'UGC数量', target: '1000+', current: '0' },
|
||||
{ metric: '话题热度', target: 'TOP 20', current: '-' },
|
||||
].map((item) => (
|
||||
<div key={item.metric} className="p-4 bg-amber-500/10 border border-amber-500/30 rounded-lg text-center">
|
||||
<p className="text-amber-400 text-xs">{item.metric}</p>
|
||||
<p className={`${textPrimary} font-bold text-lg mt-1`}>{item.target}</p>
|
||||
<p className={`${textMuted} text-xs mt-1`}>目标值</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧辅助区 (20%) */}
|
||||
<div className="col-span-1 space-y-6">
|
||||
{/* 关联讨论记录 */}
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-4`}>
|
||||
<h3 className={`${textPrimary} font-semibold mb-4`}>关联讨论记录</h3>
|
||||
<button className={`w-full flex items-center justify-between p-3 ${inputBg} hover:${isDark ? 'bg-slate-800/50' : 'bg-gray-200'} ${cardBorder} rounded-lg transition-colors`}>
|
||||
<div className="flex items-center gap-2 text-blue-400 text-sm">
|
||||
<FileText className="w-4 h-4" />
|
||||
TASK-2026-0319-001
|
||||
</div>
|
||||
<ChevronRight className={`w-4 h-4 ${textSecondary}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 快速操作 */}
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-4`}>
|
||||
<h3 className={`${textPrimary} font-semibold mb-4`}>快速操作</h3>
|
||||
<div className="space-y-2">
|
||||
<button className={`w-full flex items-center gap-3 p-3 ${inputBg} hover:${isDark ? 'bg-slate-800/50' : 'bg-gray-200'} ${cardBorder} rounded-lg transition-colors text-left`}>
|
||||
<Download className="w-4 h-4 text-green-400" />
|
||||
<div>
|
||||
<p className={`${textPrimary} text-sm`}>导出完整方案</p>
|
||||
<p className={`${textMuted} text-xs`}>Word格式</p>
|
||||
</div>
|
||||
</button>
|
||||
<button className={`w-full flex items-center gap-3 p-3 ${inputBg} hover:${isDark ? 'bg-slate-800/50' : 'bg-gray-200'} ${cardBorder} rounded-lg transition-colors text-left`}>
|
||||
<RefreshCw className="w-4 h-4 text-blue-400" />
|
||||
<div>
|
||||
<p className={`${textPrimary} text-sm`}>发起新版本修订</p>
|
||||
<p className={`${textMuted} text-xs`}>基于当前版本</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 分享 */}
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-4`}>
|
||||
<h3 className={`${textPrimary} font-semibold mb-4`}>分享方案</h3>
|
||||
<div className="flex gap-2">
|
||||
<button className={`flex-1 p-2 ${inputBg} hover:${isDark ? 'bg-slate-800/50' : 'bg-gray-200'} ${cardBorder} rounded-lg transition-colors`}>
|
||||
<p className={`${textSecondary} text-xs`}>复制链接</p>
|
||||
</button>
|
||||
<button className={`flex-1 p-2 ${inputBg} hover:${isDark ? 'bg-slate-800/50' : 'bg-gray-200'} ${cardBorder} rounded-lg transition-colors`}>
|
||||
<p className={`${textSecondary} text-xs`}>发送邮件</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FinalPlan;
|
||||
@@ -0,0 +1,984 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Send,
|
||||
Upload,
|
||||
RotateCcw,
|
||||
Bookmark,
|
||||
History,
|
||||
Sparkles,
|
||||
BookOpen,
|
||||
Brain,
|
||||
GitCompare,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
MessageSquare,
|
||||
FileText,
|
||||
CheckCircle,
|
||||
AlertCircle,
|
||||
} from 'lucide-react';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { PageType } from '../components/Sidebar';
|
||||
|
||||
type QAMode = 'single-round' | 'multi-round' | 'deep-search';
|
||||
type CompareMode = 'compare-off' | 'compare-on';
|
||||
type ViewMode = 'single' | 'compare';
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
timestamp: Date;
|
||||
sources?: SourceItem[];
|
||||
}
|
||||
|
||||
interface SourceItem {
|
||||
title: string;
|
||||
type: string;
|
||||
snippet: string;
|
||||
}
|
||||
|
||||
interface CompareResult {
|
||||
leftMode: QAMode;
|
||||
rightMode: QAMode;
|
||||
leftContent: string;
|
||||
rightContent: string;
|
||||
sources: {
|
||||
left: SourceItem[];
|
||||
right: SourceItem[];
|
||||
};
|
||||
differences?: {
|
||||
consensus: string;
|
||||
differences: string;
|
||||
leftAdvantage: string;
|
||||
rightAdvantage: string;
|
||||
};
|
||||
}
|
||||
|
||||
const GeneralQA: React.FC<{ onNavigate: (page: PageType) => void }> = ({ onNavigate }) => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
|
||||
// 状态管理
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('single');
|
||||
const [qaMode, setQaMode] = useState<QAMode>('single-round');
|
||||
const [compareLeftMode, setCompareLeftMode] = useState<QAMode>('single-round');
|
||||
const [compareRightMode, setCompareRightMode] = useState<QAMode>('deep-search');
|
||||
|
||||
// 知识范围
|
||||
const [knowledgeScope, setKnowledgeScope] = useState<'all' | 'specific-kb' | 'specific-domain'>('all');
|
||||
|
||||
// 回答偏好
|
||||
const [answerStyle, setAnswerStyle] = useState<'concise' | 'detailed'>('detailed');
|
||||
const [showSources, setShowSources] = useState(true);
|
||||
const [showProcess, setShowProcess] = useState(false);
|
||||
const [autoSummary, setAutoSummary] = useState(false);
|
||||
|
||||
// 对比设置
|
||||
const [compareLayout, setCompareLayout] = useState<'horizontal' | 'vertical'>('horizontal');
|
||||
const [autoSummarizeDiff, setAutoSummarizeDiff] = useState(true);
|
||||
const [recommendBetter, setRecommendBetter] = useState(true);
|
||||
|
||||
// 消息和结果
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [compareResult, setCompareResult] = useState<CompareResult | null>(null);
|
||||
|
||||
// 检索过程展示
|
||||
const [searchProcess, setSearchProcess] = useState({
|
||||
currentMode: '单轮RAG',
|
||||
rounds: 1,
|
||||
hits: 5,
|
||||
queryRewrite: '',
|
||||
});
|
||||
|
||||
// 示例消息
|
||||
const sampleMessages: Message[] = [
|
||||
{
|
||||
id: '1',
|
||||
role: 'user',
|
||||
content: 'RAG技术的工作原理是什么?',
|
||||
timestamp: new Date(),
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
role: 'assistant',
|
||||
content: 'RAG(检索增强生成)技术的工作原理主要包括以下几个步骤:\n\n1. **检索阶段**:系统首先将用户问题转换为查询向量,在知识库中进行相似度搜索,返回最相关的文档片段。\n\n2. **增强阶段**:将检索到的相关文档与原始问题组合,形成增强后的提示词上下文。\n\n3. **生成阶段**:将增强后的提示词发送给大语言模型,生成最终回答。\n\n这种方式可以有效解决LLM知识过时、幻觉等问题,提升回答的准确性和可信度。',
|
||||
timestamp: new Date(),
|
||||
sources: [
|
||||
{ title: 'RAG技术白皮书', type: 'PDF', snippet: 'RAG是一种结合检索和生成的技术架构...' },
|
||||
{ title: 'LLM应用实践', type: '文档', snippet: '检索增强生成可以有效提升回答质量...' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const handleSend = () => {
|
||||
if (!inputValue.trim()) return;
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
// 模拟发送消息
|
||||
const userMessage: Message = {
|
||||
id: Date.now().toString(),
|
||||
role: 'user',
|
||||
content: inputValue,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
setMessages([...messages, userMessage]);
|
||||
setInputValue('');
|
||||
|
||||
// 模拟AI响应
|
||||
setTimeout(() => {
|
||||
setMessages(prev => [...prev, ...sampleMessages]);
|
||||
setIsLoading(false);
|
||||
setSearchProcess({
|
||||
currentMode: qaMode === 'single-round' ? '单轮RAG' : qaMode === 'multi-round' ? '多轮RAG' : '深度检索',
|
||||
rounds: qaMode === 'multi-round' ? 3 : 1,
|
||||
hits: 5,
|
||||
queryRewrite: qaMode === 'deep-search' ? '原问题→分解子问题→独立检索→综合回答' : '',
|
||||
});
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
const handleCompare = () => {
|
||||
if (!inputValue.trim()) return;
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
// 模拟对比结果
|
||||
setTimeout(() => {
|
||||
setCompareResult({
|
||||
leftMode: compareLeftMode,
|
||||
rightMode: compareRightMode,
|
||||
leftContent: '单轮RAG回答:直接检索并生成答案,响应速度快,但可能遗漏跨文档的关联信息。适用于简单事实查询。',
|
||||
rightContent: '深度检索回答:通过问题分解、多轮检索和综合分析,提供更全面的答案。包含详细的推理过程和更多的引用来源。',
|
||||
sources: {
|
||||
left: [
|
||||
{ title: 'RAG基础教程', type: '文档', snippet: '单轮RAG是最基础的检索增强方式...' },
|
||||
],
|
||||
right: [
|
||||
{ title: '高级RAG技术', type: '论文', snippet: '深度检索通过query改写和迭代优化...' },
|
||||
{ title: 'RAG最佳实践', type: '指南', snippet: '多阶段检索可以显著提升召回率...' },
|
||||
],
|
||||
},
|
||||
differences: {
|
||||
consensus: '两种方式都使用向量检索技术,都能引用原始文档',
|
||||
differences: '深度检索采用多轮查询改写,覆盖更全面',
|
||||
leftAdvantage: '响应更快,资源消耗更低',
|
||||
rightAdvantage: '答案更完整,引用更充分',
|
||||
},
|
||||
});
|
||||
setIsLoading(false);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
const handleSaveRecord = () => {
|
||||
alert('问答记录已保存');
|
||||
};
|
||||
|
||||
const handleCollect = () => {
|
||||
alert('已添加到收藏');
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
if (viewMode === 'compare') {
|
||||
handleCompare();
|
||||
} else {
|
||||
handleSend();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 获取模式标签
|
||||
const getModeLabel = (mode: QAMode) => {
|
||||
switch (mode) {
|
||||
case 'single-round': return '单轮RAG';
|
||||
case 'multi-round': return '多轮RAG';
|
||||
case 'deep-search': return '深度检索';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`h-[calc(100vh-120px)] flex flex-col theme-bg rounded-xl ${isDark ? 'bg-slate-800/50' : 'bg-white'} border ${isDark ? 'border-slate-700/50' : 'border-red-100'}`}>
|
||||
{/* 顶部模式栏 */}
|
||||
<div className={`flex items-center justify-between px-6 py-4 border-b ${isDark ? 'border-slate-700/50' : 'border-red-100'}`}>
|
||||
<div className="flex items-center gap-4">
|
||||
<h1 className={`text-lg font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
||||
通用问答
|
||||
</h1>
|
||||
<span className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
支持单轮RAG、多轮RAG、深度检索及结果对比
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 模式切换按钮 */}
|
||||
<div className={`flex rounded-lg p-1 ${isDark ? 'bg-slate-700/50' : 'bg-red-50'}`}>
|
||||
<button
|
||||
onClick={() => setViewMode('single')}
|
||||
className={`px-4 py-2 rounded-md text-sm font-medium transition-all ${
|
||||
viewMode === 'single'
|
||||
? isDark
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-red-500 text-white'
|
||||
: isDark
|
||||
? 'text-slate-400 hover:text-white'
|
||||
: 'text-gray-600 hover:text-red-600'
|
||||
}`}
|
||||
>
|
||||
单一模式
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('compare')}
|
||||
className={`px-4 py-2 rounded-md text-sm font-medium transition-all ${
|
||||
viewMode === 'compare'
|
||||
? isDark
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-red-500 text-white'
|
||||
: isDark
|
||||
? 'text-slate-400 hover:text-white'
|
||||
: 'text-gray-600 hover:text-red-600'
|
||||
}`}
|
||||
>
|
||||
对比模式
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => onNavigate('qa-records')}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all ${
|
||||
isDark
|
||||
? 'bg-slate-700/50 text-slate-300 hover:bg-slate-600/50'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
<History className="w-4 h-4" />
|
||||
历史记录
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 主体内容区 */}
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{/* 左侧参数区 */}
|
||||
<div className={`w-72 border-r flex-shrink-0 overflow-y-auto p-4 ${
|
||||
isDark ? 'border-slate-700/50 bg-slate-800/30' : 'border-red-100 bg-gray-50/50'
|
||||
}`}>
|
||||
{viewMode === 'single' ? (
|
||||
<>
|
||||
{/* 问答模式选择 */}
|
||||
<div className="mb-6">
|
||||
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
|
||||
问答模式
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{[
|
||||
{ value: 'single-round', label: '单轮RAG', icon: Sparkles, desc: '快速直接' },
|
||||
{ value: 'multi-round', label: '多轮RAG', icon: Brain, desc: '深入理解' },
|
||||
{ value: 'deep-search', label: '深度检索', icon: BookOpen, desc: '全面分析' },
|
||||
].map((mode) => (
|
||||
<button
|
||||
key={mode.value}
|
||||
onClick={() => setQaMode(mode.value as QAMode)}
|
||||
className={`w-full flex items-center gap-3 p-3 rounded-lg transition-all text-left ${
|
||||
qaMode === mode.value
|
||||
? isDark
|
||||
? 'bg-blue-600/20 border border-blue-500/50 text-blue-400'
|
||||
: 'bg-red-50 border border-red-200 text-red-600'
|
||||
: isDark
|
||||
? 'bg-slate-700/30 border border-transparent text-slate-400 hover:bg-slate-700/50'
|
||||
: 'bg-white border border-gray-200 text-gray-600 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<mode.icon className="w-5 h-5" />
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-medium">{mode.label}</div>
|
||||
<div className={`text-xs ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>{mode.desc}</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* 对比模式设置 */}
|
||||
<div className="mb-6">
|
||||
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
|
||||
对比模式设置
|
||||
</h3>
|
||||
|
||||
{/* 左侧模式 */}
|
||||
<div className="mb-4">
|
||||
<label className={`text-xs mb-2 block ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
模式A
|
||||
</label>
|
||||
<select
|
||||
value={compareLeftMode}
|
||||
onChange={(e) => setCompareLeftMode(e.target.value as QAMode)}
|
||||
className={`w-full px-3 py-2 rounded-lg text-sm ${
|
||||
isDark
|
||||
? 'bg-slate-700/50 border border-slate-600 text-white'
|
||||
: 'bg-white border border-gray-200 text-gray-700'
|
||||
}`}
|
||||
>
|
||||
<option value="single-round">单轮RAG</option>
|
||||
<option value="multi-round">多轮RAG</option>
|
||||
<option value="deep-search">深度检索</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className={`text-center my-2 ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
|
||||
<span className="text-lg">VS</span>
|
||||
</div>
|
||||
|
||||
{/* 右侧模式 */}
|
||||
<div className="mb-4">
|
||||
<label className={`text-xs mb-2 block ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
模式B
|
||||
</label>
|
||||
<select
|
||||
value={compareRightMode}
|
||||
onChange={(e) => setCompareRightMode(e.target.value as QAMode)}
|
||||
className={`w-full px-3 py-2 rounded-lg text-sm ${
|
||||
isDark
|
||||
? 'bg-slate-700/50 border border-slate-600 text-white'
|
||||
: 'bg-white border border-gray-200 text-gray-700'
|
||||
}`}
|
||||
>
|
||||
<option value="single-round">单轮RAG</option>
|
||||
<option value="multi-round">多轮RAG</option>
|
||||
<option value="deep-search">深度检索</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 对比布局 */}
|
||||
<div className="mb-6">
|
||||
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
|
||||
对比布局
|
||||
</h3>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setCompareLayout('horizontal')}
|
||||
className={`flex-1 px-3 py-2 rounded-lg text-sm transition-all ${
|
||||
compareLayout === 'horizontal'
|
||||
? isDark
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-red-500 text-white'
|
||||
: isDark
|
||||
? 'bg-slate-700/50 text-slate-300'
|
||||
: 'bg-gray-100 text-gray-600'
|
||||
}`}
|
||||
>
|
||||
左右对比
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCompareLayout('vertical')}
|
||||
className={`flex-1 px-3 py-2 rounded-lg text-sm transition-all ${
|
||||
compareLayout === 'vertical'
|
||||
? isDark
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-red-500 text-white'
|
||||
: isDark
|
||||
? 'bg-slate-700/50 text-slate-300'
|
||||
: 'bg-gray-100 text-gray-600'
|
||||
}`}
|
||||
>
|
||||
上下对比
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 知识范围 */}
|
||||
<div className="mb-6">
|
||||
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
|
||||
知识范围
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{[
|
||||
{ value: 'all', label: '全部知识源' },
|
||||
{ value: 'specific-kb', label: '指定知识库' },
|
||||
{ value: 'specific-domain', label: '指定资源域' },
|
||||
].map((option) => (
|
||||
<label
|
||||
key={option.value}
|
||||
className={`flex items-center gap-2 p-2 rounded-lg cursor-pointer transition-all ${
|
||||
knowledgeScope === option.value
|
||||
? isDark
|
||||
? 'bg-blue-600/20 text-blue-400'
|
||||
: 'bg-red-50 text-red-600'
|
||||
: isDark
|
||||
? 'text-slate-400 hover:bg-slate-700/30'
|
||||
: 'text-gray-600 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="knowledge-scope"
|
||||
checked={knowledgeScope === option.value}
|
||||
onChange={() => setKnowledgeScope(option.value as any)}
|
||||
className="sr-only"
|
||||
/>
|
||||
<div className={`w-4 h-4 rounded-full border-2 flex items-center justify-center ${
|
||||
knowledgeScope === option.value
|
||||
? isDark
|
||||
? 'border-blue-500 bg-blue-500'
|
||||
: 'border-red-500 bg-red-500'
|
||||
: isDark
|
||||
? 'border-slate-500'
|
||||
: 'border-gray-300'
|
||||
}`}>
|
||||
{knowledgeScope === option.value && (
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-white" />
|
||||
)}
|
||||
</div>
|
||||
<span className="text-sm">{option.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 回答偏好 */}
|
||||
<div className="mb-6">
|
||||
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
|
||||
回答偏好
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
<label className={`flex items-center gap-2 p-2 rounded-lg cursor-pointer transition-all ${
|
||||
isDark ? 'text-slate-400 hover:bg-slate-700/30' : 'text-gray-600 hover:bg-gray-50'
|
||||
}`}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={answerStyle === 'detailed'}
|
||||
onChange={() => setAnswerStyle(answerStyle === 'detailed' ? 'concise' : 'detailed')}
|
||||
className="sr-only"
|
||||
/>
|
||||
<div className={`w-4 h-4 rounded border flex items-center justify-center ${
|
||||
answerStyle === 'detailed'
|
||||
? isDark
|
||||
? 'bg-blue-500 border-blue-500'
|
||||
: 'bg-red-500 border-red-500'
|
||||
: isDark
|
||||
? 'border-slate-500'
|
||||
: 'border-gray-300'
|
||||
}`}>
|
||||
{answerStyle === 'detailed' && (
|
||||
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-sm">详细回答</span>
|
||||
</label>
|
||||
|
||||
<label className={`flex items-center gap-2 p-2 rounded-lg cursor-pointer transition-all ${
|
||||
isDark ? 'text-slate-400 hover:bg-slate-700/30' : 'text-gray-600 hover:bg-gray-50'
|
||||
}`}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showSources}
|
||||
onChange={() => setShowSources(!showSources)}
|
||||
className="sr-only"
|
||||
/>
|
||||
<div className={`w-4 h-4 rounded border flex items-center justify-center ${
|
||||
showSources
|
||||
? isDark
|
||||
? 'bg-blue-500 border-blue-500'
|
||||
: 'bg-red-500 border-red-500'
|
||||
: isDark
|
||||
? 'border-slate-500'
|
||||
: 'border-gray-300'
|
||||
}`}>
|
||||
{showSources && (
|
||||
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-sm">展示引用来源</span>
|
||||
</label>
|
||||
|
||||
<label className={`flex items-center gap-2 p-2 rounded-lg cursor-pointer transition-all ${
|
||||
isDark ? 'text-slate-400 hover:bg-slate-700/30' : 'text-gray-600 hover:bg-gray-50'
|
||||
}`}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showProcess}
|
||||
onChange={() => setShowProcess(!showProcess)}
|
||||
className="sr-only"
|
||||
/>
|
||||
<div className={`w-4 h-4 rounded border flex items-center justify-center ${
|
||||
showProcess
|
||||
? isDark
|
||||
? 'bg-blue-500 border-blue-500'
|
||||
: 'bg-red-500 border-red-500'
|
||||
: isDark
|
||||
? 'border-slate-500'
|
||||
: 'border-gray-300'
|
||||
}`}>
|
||||
{showProcess && (
|
||||
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-sm">展示检索过程</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 对比设置 - 仅对比模式 */}
|
||||
{viewMode === 'compare' && (
|
||||
<div className="mb-6">
|
||||
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
|
||||
对比设置
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
<label className={`flex items-center gap-2 p-2 rounded-lg cursor-pointer transition-all ${
|
||||
isDark ? 'text-slate-400 hover:bg-slate-700/30' : 'text-gray-600 hover:bg-gray-50'
|
||||
}`}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={autoSummarizeDiff}
|
||||
onChange={() => setAutoSummarizeDiff(!autoSummarizeDiff)}
|
||||
className="sr-only"
|
||||
/>
|
||||
<div className={`w-4 h-4 rounded border flex items-center justify-center ${
|
||||
autoSummarizeDiff
|
||||
? isDark
|
||||
? 'bg-blue-500 border-blue-500'
|
||||
: 'bg-red-500 border-red-500'
|
||||
: isDark
|
||||
? 'border-slate-500'
|
||||
: 'border-gray-300'
|
||||
}`}>
|
||||
{autoSummarizeDiff && (
|
||||
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-sm">自动总结差异</span>
|
||||
</label>
|
||||
|
||||
<label className={`flex items-center gap-2 p-2 rounded-lg cursor-pointer transition-all ${
|
||||
isDark ? 'text-slate-400 hover:bg-slate-700/30' : 'text-gray-600 hover:bg-gray-50'
|
||||
}`}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={recommendBetter}
|
||||
onChange={() => setRecommendBetter(!recommendBetter)}
|
||||
className="sr-only"
|
||||
/>
|
||||
<div className={`w-4 h-4 rounded border flex items-center justify-center ${
|
||||
recommendBetter
|
||||
? isDark
|
||||
? 'bg-blue-500 border-blue-500'
|
||||
: 'bg-red-500 border-red-500'
|
||||
: isDark
|
||||
? 'border-slate-500'
|
||||
: 'border-gray-300'
|
||||
}`}>
|
||||
{recommendBetter && (
|
||||
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-sm">推荐更优答案</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 中间结果区 */}
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
{viewMode === 'single' ? (
|
||||
<>
|
||||
{/* 聊天消息区 */}
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||
{messages.length === 0 ? (
|
||||
<div className={`h-full flex flex-col items-center justify-center ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
|
||||
<MessageSquare className="w-16 h-16 mb-4 opacity-50" />
|
||||
<p className="text-lg">开始你的问答</p>
|
||||
<p className="text-sm mt-2">输入问题,获取基于知识库的智能回答</p>
|
||||
</div>
|
||||
) : (
|
||||
messages.map((msg) => (
|
||||
<div key={msg.id} className="space-y-3">
|
||||
{/* 用户消息 */}
|
||||
<div className={`flex gap-3 ${msg.role === 'user' ? 'justify-end' : ''}`}>
|
||||
{msg.role === 'assistant' && (
|
||||
<div className={`w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 ${
|
||||
isDark ? 'bg-blue-600/20 text-blue-400' : 'bg-red-100 text-red-500'
|
||||
}`}>
|
||||
<Brain className="w-4 h-4" />
|
||||
</div>
|
||||
)}
|
||||
<div className={`max-w-[70%] rounded-xl px-4 py-3 ${
|
||||
msg.role === 'user'
|
||||
? isDark
|
||||
? 'bg-blue-600/30 text-white'
|
||||
: 'bg-red-500 text-white'
|
||||
: isDark
|
||||
? 'bg-slate-700/50 text-slate-200'
|
||||
: 'bg-gray-100 text-gray-800'
|
||||
}`}>
|
||||
<p className="whitespace-pre-wrap">{msg.content}</p>
|
||||
</div>
|
||||
{msg.role === 'user' && (
|
||||
<div className={`w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 ${
|
||||
isDark ? 'bg-slate-700 text-slate-300' : 'bg-gray-200 text-gray-600'
|
||||
}`}>
|
||||
<div className="w-4 h-4 rounded-full bg-current" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 来源信息 */}
|
||||
{msg.role === 'assistant' && msg.sources && showSources && (
|
||||
<div className={`ml-11 p-3 rounded-lg ${isDark ? 'bg-slate-800/50' : 'bg-gray-50'}`}>
|
||||
<h4 className={`text-xs font-medium mb-2 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
引用来源
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{msg.sources.map((source, idx) => (
|
||||
<div key={idx} className={`text-xs p-2 rounded ${isDark ? 'bg-slate-700/50' : 'bg-white'}`}>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<FileText className={`w-3 h-3 ${isDark ? 'text-blue-400' : 'text-red-400'}`} />
|
||||
<span className={`font-medium ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
|
||||
{source.title}
|
||||
</span>
|
||||
<span className={`px-1.5 py-0.5 rounded text-xs ${
|
||||
isDark ? 'bg-slate-600 text-slate-400' : 'bg-gray-200 text-gray-500'
|
||||
}`}>
|
||||
{source.type}
|
||||
</span>
|
||||
</div>
|
||||
<p className={isDark ? 'text-slate-400' : 'text-gray-500'}>{source.snippet}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
||||
{isLoading && (
|
||||
<div className="flex gap-3">
|
||||
<div className={`w-8 h-8 rounded-lg flex items-center justify-center ${
|
||||
isDark ? 'bg-blue-600/20 text-blue-400' : 'bg-red-100 text-red-500'
|
||||
}`}>
|
||||
<Brain className="w-4 h-4" />
|
||||
</div>
|
||||
<div className={`max-w-[70%] rounded-xl px-4 py-3 ${isDark ? 'bg-slate-700/50' : 'bg-gray-100'}`}>
|
||||
<div className="flex gap-1">
|
||||
<div className={`w-2 h-2 rounded-full animate-bounce ${isDark ? 'bg-slate-500' : 'bg-gray-400'}`} style={{ animationDelay: '0ms' }} />
|
||||
<div className={`w-2 h-2 rounded-full animate-bounce ${isDark ? 'bg-slate-500' : 'bg-gray-400'}`} style={{ animationDelay: '150ms' }} />
|
||||
<div className={`w-2 h-2 rounded-full animate-bounce ${isDark ? 'bg-slate-500' : 'bg-gray-400'}`} style={{ animationDelay: '300ms' }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* 对比结果区 */}
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
{compareResult ? (
|
||||
compareLayout === 'horizontal' ? (
|
||||
<div className="grid grid-cols-2 gap-4 h-full">
|
||||
{/* 左侧结果 */}
|
||||
<div className={`rounded-xl border ${isDark ? 'border-slate-700/50' : 'border-red-200'}`}>
|
||||
<div className={`px-4 py-3 border-b ${isDark ? 'border-slate-700/50 bg-slate-800/30' : 'border-red-100 bg-red-50/50'}`}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
||||
isDark ? 'bg-blue-600/30 text-blue-400' : 'bg-blue-100 text-blue-600'
|
||||
}`}>
|
||||
{getModeLabel(compareResult.leftMode)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<p className={`whitespace-pre-wrap ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
|
||||
{compareResult.leftContent}
|
||||
</p>
|
||||
|
||||
{/* 左侧来源 */}
|
||||
<div className={`mt-4 pt-4 border-t ${isDark ? 'border-slate-700/50' : 'border-red-100'}`}>
|
||||
<h4 className={`text-xs font-medium mb-2 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
引用来源
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{compareResult.sources.left.map((source, idx) => (
|
||||
<div key={idx} className={`text-xs p-2 rounded ${isDark ? 'bg-slate-700/50' : 'bg-gray-50'}`}>
|
||||
<span className={`font-medium ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
|
||||
{source.title}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧结果 */}
|
||||
<div className={`rounded-xl border ${isDark ? 'border-slate-700/50' : 'border-red-200'}`}>
|
||||
<div className={`px-4 py-3 border-b ${isDark ? 'border-slate-700/50 bg-slate-800/30' : 'border-red-100 bg-red-50/50'}`}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
||||
isDark ? 'bg-purple-600/30 text-purple-400' : 'bg-purple-100 text-purple-600'
|
||||
}`}>
|
||||
{getModeLabel(compareResult.rightMode)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<p className={`whitespace-pre-wrap ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
|
||||
{compareResult.rightContent}
|
||||
</p>
|
||||
|
||||
{/* 右侧来源 */}
|
||||
<div className={`mt-4 pt-4 border-t ${isDark ? 'border-slate-700/50' : 'border-red-100'}`}>
|
||||
<h4 className={`text-xs font-medium mb-2 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
引用来源
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{compareResult.sources.right.map((source, idx) => (
|
||||
<div key={idx} className={`text-xs p-2 rounded ${isDark ? 'bg-slate-700/50' : 'bg-gray-50'}`}>
|
||||
<span className={`font-medium ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
|
||||
{source.title}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{/* 上方结果 */}
|
||||
<div className={`rounded-xl border ${isDark ? 'border-slate-700/50' : 'border-red-200'}`}>
|
||||
<div className={`px-4 py-3 border-b ${isDark ? 'border-slate-700/50 bg-slate-800/30' : 'border-red-100 bg-red-50/50'}`}>
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
||||
isDark ? 'bg-blue-600/30 text-blue-400' : 'bg-blue-100 text-blue-600'
|
||||
}`}>
|
||||
{getModeLabel(compareResult.leftMode)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<p className={`whitespace-pre-wrap ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
|
||||
{compareResult.leftContent}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 下方结果 */}
|
||||
<div className={`rounded-xl border ${isDark ? 'border-slate-700/50' : 'border-red-200'}`}>
|
||||
<div className={`px-4 py-3 border-b ${isDark ? 'border-slate-700/50 bg-slate-800/30' : 'border-red-100 bg-red-50/50'}`}>
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
||||
isDark ? 'bg-purple-600/30 text-purple-400' : 'bg-purple-100 text-purple-600'
|
||||
}`}>
|
||||
{getModeLabel(compareResult.rightMode)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<p className={`whitespace-pre-wrap ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
|
||||
{compareResult.rightContent}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className={`h-full flex flex-col items-center justify-center ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
|
||||
<GitCompare className="w-16 h-16 mb-4 opacity-50" />
|
||||
<p className="text-lg">对比模式</p>
|
||||
<p className="text-sm mt-2">输入问题,对比不同模式的回答效果</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 底部输入区 */}
|
||||
<div className={`border-t p-4 ${isDark ? 'border-slate-700/50' : 'border-red-100'}`}>
|
||||
<div className={`flex items-end gap-3 rounded-xl p-3 ${isDark ? 'bg-slate-700/50' : 'bg-gray-100'}`}>
|
||||
<textarea
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={viewMode === 'compare' ? '输入问题进行对比分析...' : '输入你的问题...'}
|
||||
rows={1}
|
||||
className={`flex-1 bg-transparent resize-none focus:outline-none ${
|
||||
isDark ? 'text-white placeholder-slate-500' : 'text-gray-800 placeholder-gray-400'
|
||||
}`}
|
||||
/>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button className={`p-2 rounded-lg transition-colors ${
|
||||
isDark ? 'hover:bg-slate-600 text-slate-400' : 'hover:bg-gray-200 text-gray-500'
|
||||
}`}>
|
||||
<Upload className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setMessages([]);
|
||||
setCompareResult(null);
|
||||
}}
|
||||
className={`p-2 rounded-lg transition-colors ${
|
||||
isDark ? 'hover:bg-slate-600 text-slate-400' : 'hover:bg-gray-200 text-gray-500'
|
||||
}`}
|
||||
title="重新提问"
|
||||
>
|
||||
<RotateCcw className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleSaveRecord}
|
||||
className={`p-2 rounded-lg transition-colors ${
|
||||
isDark ? 'hover:bg-slate-600 text-slate-400' : 'hover:bg-gray-200 text-gray-500'
|
||||
}`}
|
||||
title="保存记录"
|
||||
>
|
||||
<Bookmark className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleCollect}
|
||||
className={`p-2 rounded-lg transition-colors ${
|
||||
isDark ? 'hover:bg-slate-600 text-slate-400' : 'hover:bg-gray-200 text-gray-500'
|
||||
}`}
|
||||
title="加入收藏"
|
||||
>
|
||||
<Bookmark className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={viewMode === 'compare' ? handleCompare : handleSend}
|
||||
disabled={!inputValue.trim() || isLoading}
|
||||
className={`px-4 py-2 rounded-lg font-medium transition-all flex items-center gap-2 ${
|
||||
inputValue.trim() && !isLoading
|
||||
? isDark
|
||||
? 'bg-blue-600 hover:bg-blue-700 text-white'
|
||||
: 'bg-red-500 hover:bg-red-600 text-white'
|
||||
: isDark
|
||||
? 'bg-slate-600 text-slate-400'
|
||||
: 'bg-gray-300 text-gray-500'
|
||||
}`}
|
||||
>
|
||||
<Send className="w-4 h-4" />
|
||||
发送
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧辅助区 */}
|
||||
{viewMode === 'single' && showProcess && (
|
||||
<div className={`w-72 border-l flex-shrink-0 overflow-y-auto p-4 ${
|
||||
isDark ? 'border-slate-700/50 bg-slate-800/30' : 'border-red-100 bg-gray-50/50'
|
||||
}`}>
|
||||
{/* 检索过程 */}
|
||||
<div className="mb-6">
|
||||
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
|
||||
检索过程
|
||||
</h3>
|
||||
<div className={`p-4 rounded-xl ${isDark ? 'bg-slate-700/50' : 'bg-white'}`}>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className={`text-xs ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>当前模式</span>
|
||||
<span className={`text-sm font-medium ${isDark ? 'text-blue-400' : 'text-blue-600'}`}>
|
||||
{searchProcess.currentMode}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className={`text-xs ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>检索轮次</span>
|
||||
<span className={`text-sm ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
|
||||
{searchProcess.rounds}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className={`text-xs ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>命中数</span>
|
||||
<span className={`text-sm ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
|
||||
{searchProcess.hits}
|
||||
</span>
|
||||
</div>
|
||||
{searchProcess.queryRewrite && (
|
||||
<div>
|
||||
<span className={`text-xs ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>Query改写</span>
|
||||
<p className={`text-xs mt-1 ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>
|
||||
{searchProcess.queryRewrite}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 对比模式的差异总结 */}
|
||||
{viewMode === 'compare' && compareResult?.differences && (
|
||||
<div className={`w-72 border-l flex-shrink-0 overflow-y-auto p-4 ${
|
||||
isDark ? 'border-slate-700/50 bg-slate-800/30' : 'border-red-100 bg-gray-50/50'
|
||||
}`}>
|
||||
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
|
||||
差异总结
|
||||
</h3>
|
||||
<div className={`p-4 rounded-xl space-y-4 ${isDark ? 'bg-slate-700/50' : 'bg-white'}`}>
|
||||
<div>
|
||||
<span className={`text-xs flex items-center gap-1 ${isDark ? 'text-green-400' : 'text-green-600'}`}>
|
||||
<CheckCircle className="w-3 h-3" />
|
||||
一致观点
|
||||
</span>
|
||||
<p className={`text-xs mt-1 ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>
|
||||
{compareResult.differences.consensus}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className={`text-xs flex items-center gap-1 ${isDark ? 'text-yellow-400' : 'text-yellow-600'}`}>
|
||||
<AlertCircle className="w-3 h-3" />
|
||||
差异观点
|
||||
</span>
|
||||
<p className={`text-xs mt-1 ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>
|
||||
{compareResult.differences.differences}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className={`text-xs ${isDark ? 'text-blue-400' : 'text-blue-600'}`}>
|
||||
{getModeLabel(compareResult.leftMode)}优势
|
||||
</span>
|
||||
<p className={`text-xs mt-1 ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>
|
||||
{compareResult.differences.leftAdvantage}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className={`text-xs ${isDark ? 'text-purple-400' : 'text-purple-600'}`}>
|
||||
{getModeLabel(compareResult.rightMode)}优势
|
||||
</span>
|
||||
<p className={`text-xs mt-1 ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>
|
||||
{compareResult.differences.rightAdvantage}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GeneralQA;
|
||||
@@ -0,0 +1,54 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTheme } from "../contexts/ThemeContext";
|
||||
import { PageType } from "../components/Sidebar";
|
||||
const knowledgeURL = import.meta.env.VITE_AIPI_KNOWLEDGE_URL || "";
|
||||
import "./css/KnowledgeBaseResourceManagement.css";
|
||||
import { requestCrudService } from "@/service/request-axios-service";
|
||||
|
||||
interface KnowledgeBaseResourceManagementProps {
|
||||
onNavigate: (page: PageType) => void;
|
||||
}
|
||||
|
||||
const KnowledgeBaseResourceManagement: React.FC<
|
||||
KnowledgeBaseResourceManagementProps
|
||||
> = ({ onNavigate }) => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === "dark";
|
||||
const service = requestCrudService;
|
||||
const [iframeUrl, setIframeUrl] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
initIframeUrl();
|
||||
}, []);
|
||||
|
||||
const initIframeUrl = async () => {
|
||||
// const url = `${knowledgeURL}/#/knowledge/knowledge-search?isIframe=true`;
|
||||
|
||||
const jsonStr = localStorage.getItem("userInfo");
|
||||
console.log('jsonStr', jsonStr)
|
||||
if (jsonStr) {
|
||||
const userInfo = JSON.parse(jsonStr);
|
||||
console.log('userInfo', userInfo);
|
||||
const url = `https://ch1.b.uat.4.cn/knowes/api/iframeKnowledgeUrl`;
|
||||
const token = userInfo.accessToken;
|
||||
service.post(url, { token, skin: "red" }).then((res:any) => {
|
||||
console.log({ iframeUrl: res.iframeUrl});
|
||||
setIframeUrl(
|
||||
res.iframeUrl
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// setIframeUrl(
|
||||
// "http://localhost:4210/#/knowledge/knowledge-search?isIframe=true"
|
||||
// );
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="iframe-container">
|
||||
<iframe src={iframeUrl} className="full-size-iframe"></iframe>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KnowledgeBaseResourceManagement;
|
||||
@@ -0,0 +1,485 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import {
|
||||
BarChart,
|
||||
FileText,
|
||||
Table,
|
||||
Search,
|
||||
Send,
|
||||
Sparkles,
|
||||
Clock,
|
||||
ChevronRight,
|
||||
Bot,
|
||||
Loader2,
|
||||
Copy,
|
||||
CheckCheck,
|
||||
Download,
|
||||
RefreshCw,
|
||||
FileBarChart,
|
||||
Database,
|
||||
Brain,
|
||||
PieChart,
|
||||
LineChart,
|
||||
FileSpreadsheet,
|
||||
FileSearch,
|
||||
BookOpen,
|
||||
} from 'lucide-react';
|
||||
|
||||
interface LightAppCenterProps {
|
||||
onNavigate: (page: string) => void;
|
||||
initialApp?: 'analysis' | 'report' | 'data' | 'research';
|
||||
}
|
||||
|
||||
type AppType = 'analysis' | 'report' | 'data' | 'research';
|
||||
|
||||
interface AppCard {
|
||||
id: AppType;
|
||||
title: string;
|
||||
description: string;
|
||||
icon: React.ElementType;
|
||||
color: string;
|
||||
}
|
||||
|
||||
const apps: AppCard[] = [
|
||||
{
|
||||
id: 'analysis',
|
||||
title: '快速分析',
|
||||
description: '对文本、数据进行快速分析处理,提取关键信息和洞察',
|
||||
icon: BarChart,
|
||||
color: 'blue',
|
||||
},
|
||||
{
|
||||
id: 'report',
|
||||
title: '报告生成',
|
||||
description: '基于输入内容生成结构化分析报告,支持多种格式',
|
||||
icon: FileText,
|
||||
color: 'green',
|
||||
},
|
||||
{
|
||||
id: 'data',
|
||||
title: '数据处理',
|
||||
description: '数据清洗、转换、统计和可视化处理',
|
||||
icon: Table,
|
||||
color: 'purple',
|
||||
},
|
||||
{
|
||||
id: 'research',
|
||||
title: '研究助手',
|
||||
description: '辅助学术研究和资料整理,支持文献分析和摘要',
|
||||
icon: Search,
|
||||
color: 'orange',
|
||||
},
|
||||
];
|
||||
|
||||
const LightAppCenter: React.FC<LightAppCenterProps> = ({ onNavigate, initialApp }) => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
const [selectedApp, setSelectedApp] = useState<AppType | null>(initialApp || null);
|
||||
const [input, setInput] = useState('');
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [result, setResult] = useState('');
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<'input' | 'history'>('input');
|
||||
|
||||
const handleAppSelect = (app: AppCard) => {
|
||||
setSelectedApp(app.id);
|
||||
setInput('');
|
||||
setResult('');
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!input.trim() || !selectedApp) return;
|
||||
|
||||
setIsProcessing(true);
|
||||
setResult('');
|
||||
|
||||
// 模拟处理过程
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
|
||||
// 根据不同应用生成模拟结果
|
||||
let mockResult = '';
|
||||
switch (selectedApp) {
|
||||
case 'analysis':
|
||||
mockResult = `【分析结果】
|
||||
|
||||
1. 关键主题
|
||||
• 主题A:核心概念和定义
|
||||
• 主题B:应用场景和实践
|
||||
• 主题C:发展趋势和展望
|
||||
|
||||
2. 情感分析
|
||||
• 整体倾向:中性偏正面
|
||||
• 关键情感词:创新、发展、应用
|
||||
|
||||
3. 关键实体
|
||||
• 机构:示例机构
|
||||
• 人物:相关专家
|
||||
• 地点:主要应用区域
|
||||
|
||||
4. 重要数据
|
||||
• 增长率:15-20%
|
||||
• 覆盖范围:多个领域
|
||||
• 时间周期:持续进行`;
|
||||
break;
|
||||
case 'report':
|
||||
mockResult = `# 分析报告
|
||||
|
||||
## 概述
|
||||
基于提供的内容,生成以下分析报告。
|
||||
|
||||
## 主要发现
|
||||
|
||||
### 1. 核心内容
|
||||
详细描述主要发现和观察结果...
|
||||
|
||||
### 2. 深入分析
|
||||
对关键点进行深入分析和解读...
|
||||
|
||||
### 3. 结论建议
|
||||
基于分析结果提出建议和行动方案...
|
||||
|
||||
## 附录
|
||||
- 数据来源
|
||||
- 分析方法
|
||||
- 局限性说明`;
|
||||
break;
|
||||
case 'data':
|
||||
mockResult = `【数据处理结果】
|
||||
|
||||
| 指标 | 数值 | 变化 |
|
||||
|------|------|------|
|
||||
| 样本量 | 1,234 | +12% |
|
||||
| 准确率 | 95.6% | +3.2% |
|
||||
| 覆盖率 | 89.3% | +8.7% |
|
||||
|
||||
统计摘要:
|
||||
• 总计处理 1,234 条记录
|
||||
• 有效数据 1,156 条(93.7%)
|
||||
• 异常数据 78 条(6.3%)
|
||||
|
||||
图表数据已准备好可视化`;
|
||||
break;
|
||||
case 'research':
|
||||
mockResult = `【研究摘要】
|
||||
|
||||
## 文献概览
|
||||
找到相关研究文献 12 篇,主要涉及以下方向:
|
||||
|
||||
### 核心观点
|
||||
1. 研究方向A - 理论基础
|
||||
2. 研究方向B - 方法论
|
||||
3. 研究方向C - 实证应用
|
||||
|
||||
### 关键结论
|
||||
• 结论1:...
|
||||
• 结论2:...
|
||||
• 结论3:...
|
||||
|
||||
### 研究空白
|
||||
当前研究存在以下空白点,可作为进一步探索的方向...
|
||||
|
||||
### 建议阅读
|
||||
1. 《核心文献一》- 理论基础
|
||||
2. 《核心文献二》- 方法创新
|
||||
3. 《核心文献三》- 实践应用`;
|
||||
break;
|
||||
}
|
||||
|
||||
setResult(mockResult);
|
||||
setIsProcessing(false);
|
||||
};
|
||||
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard.writeText(result);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
|
||||
const getColorClasses = (color: string) => {
|
||||
const colors: Record<string, { bg: string; text: string; border: string; light: string }> = {
|
||||
blue: {
|
||||
bg: isDark ? 'bg-blue-500/20' : 'bg-blue-50',
|
||||
text: isDark ? 'text-blue-400' : 'text-blue-600',
|
||||
border: isDark ? 'border-blue-500/30' : 'border-blue-200',
|
||||
light: isDark ? 'hover:bg-blue-500/30' : 'hover:bg-blue-100',
|
||||
},
|
||||
green: {
|
||||
bg: isDark ? 'bg-green-500/20' : 'bg-green-50',
|
||||
text: isDark ? 'text-green-400' : 'text-green-600',
|
||||
border: isDark ? 'border-green-500/30' : 'border-green-200',
|
||||
light: isDark ? 'hover:bg-green-500/30' : 'hover:bg-green-100',
|
||||
},
|
||||
purple: {
|
||||
bg: isDark ? 'bg-purple-500/20' : 'bg-purple-50',
|
||||
text: isDark ? 'text-purple-400' : 'text-purple-600',
|
||||
border: isDark ? 'border-purple-500/30' : 'border-purple-200',
|
||||
light: isDark ? 'hover:bg-purple-500/30' : 'hover:bg-purple-100',
|
||||
},
|
||||
orange: {
|
||||
bg: isDark ? 'bg-orange-500/20' : 'bg-orange-50',
|
||||
text: isDark ? 'text-orange-400' : 'text-orange-600',
|
||||
border: isDark ? 'border-orange-500/30' : 'border-orange-200',
|
||||
light: isDark ? 'hover:bg-orange-500/30' : 'hover:bg-orange-100',
|
||||
},
|
||||
};
|
||||
return colors[color];
|
||||
};
|
||||
|
||||
const selectedAppData = apps.find(a => a.id === selectedApp);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 页面标题 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className={`text-2xl font-bold theme-text ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
||||
轻应用中心
|
||||
</h1>
|
||||
<p className={`mt-1 text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
快速调用工具完成临时分析需求
|
||||
</p>
|
||||
</div>
|
||||
{selectedApp && (
|
||||
<button
|
||||
onClick={() => setSelectedApp(null)}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 ${
|
||||
isDark
|
||||
? 'bg-slate-700 text-slate-300 hover:bg-slate-600'
|
||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
返回应用列表
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 应用选择卡片 */}
|
||||
{!selectedApp && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{apps.map((app) => {
|
||||
const Icon = app.icon;
|
||||
const colors = getColorClasses(app.color);
|
||||
return (
|
||||
<button
|
||||
key={app.id}
|
||||
onClick={() => handleAppSelect(app)}
|
||||
className={`p-6 rounded-xl border transition-all duration-200 text-left group ${
|
||||
isDark
|
||||
? `bg-slate-800/50 ${colors.border} hover:${colors.bg}`
|
||||
: `bg-white ${colors.border} hover:${colors.bg}`
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className={`p-3 rounded-lg ${colors.bg}`}>
|
||||
<Icon className={`w-6 h-6 ${colors.text}`} />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className={`font-semibold group-hover:${colors.text} ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
||||
{app.title}
|
||||
</h3>
|
||||
<p className={`mt-1 text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
{app.description}
|
||||
</p>
|
||||
</div>
|
||||
<ChevronRight className={`w-5 h-5 ${isDark ? 'text-slate-500' : 'text-gray-400'} group-hover:${colors.text}`} />
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 应用工作区 */}
|
||||
{selectedApp && selectedAppData && (
|
||||
<div className={`rounded-xl border overflow-hidden ${
|
||||
isDark ? 'bg-slate-800/50 border-slate-700' : 'bg-white border-gray-200'
|
||||
}`}>
|
||||
{/* 应用标题栏 */}
|
||||
<div className={`px-6 py-4 border-b flex items-center gap-3 ${
|
||||
isDark ? 'border-slate-700 bg-slate-800' : 'border-gray-200 bg-gray-50'
|
||||
}`}>
|
||||
<div className={`p-2 rounded-lg ${getColorClasses(selectedAppData.color).bg}`}>
|
||||
{React.createElement(selectedAppData.icon, {
|
||||
className: `w-5 h-5 ${getColorClasses(selectedAppData.color).text}`
|
||||
})}
|
||||
</div>
|
||||
<div>
|
||||
<h2 className={`font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
||||
{selectedAppData.title}
|
||||
</h2>
|
||||
<p className={`text-xs ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
{selectedAppData.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 标签页 */}
|
||||
<div className={`px-6 border-b ${isDark ? 'border-slate-700' : 'border-gray-200'}`}>
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
onClick={() => setActiveTab('input')}
|
||||
className={`py-3 px-1 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeTab === 'input'
|
||||
? isDark
|
||||
? 'border-blue-500 text-blue-400'
|
||||
: 'border-red-500 text-red-600'
|
||||
: isDark
|
||||
? 'border-transparent text-slate-400 hover:text-slate-300'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
输入分析
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('history')}
|
||||
className={`py-3 px-1 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeTab === 'history'
|
||||
? isDark
|
||||
? 'border-blue-500 text-blue-400'
|
||||
: 'border-red-500 text-red-600'
|
||||
: isDark
|
||||
? 'border-transparent text-slate-400 hover:text-slate-300'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
历史记录
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
{activeTab === 'input' ? (
|
||||
<div className="space-y-4">
|
||||
{/* 输入区域 */}
|
||||
<div>
|
||||
<label className={`block text-sm font-medium mb-2 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
|
||||
输入内容
|
||||
</label>
|
||||
<textarea
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
placeholder={`请输入需要${selectedAppData.title}的内容...`}
|
||||
rows={6}
|
||||
className={`w-full px-4 py-3 rounded-lg border transition-colors focus:outline-none focus:ring-2 resize-none ${
|
||||
isDark
|
||||
? 'bg-slate-900 border-slate-600 text-slate-200 placeholder-slate-500 focus:border-blue-500 focus:ring-blue-500/20'
|
||||
: 'bg-gray-50 border-gray-300 text-gray-900 placeholder-gray-400 focus:border-red-500 focus:ring-red-500/20'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<button className={`p-2 rounded-lg ${isDark ? 'hover:bg-slate-700 text-slate-400' : 'hover:bg-gray-100 text-gray-500'}`}>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
</button>
|
||||
<button className={`p-2 rounded-lg ${isDark ? 'hover:bg-slate-700 text-slate-400' : 'hover:bg-gray-100 text-gray-500'}`}>
|
||||
<FileBarChart className="w-4 h-4" />
|
||||
</button>
|
||||
<button className={`p-2 rounded-lg ${isDark ? 'hover:bg-slate-700 text-slate-400' : 'hover:bg-gray-100 text-gray-500'}`}>
|
||||
<Database className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={!input.trim() || isProcessing}
|
||||
className={`px-6 py-2.5 rounded-lg font-medium flex items-center gap-2 transition-all duration-200 ${
|
||||
!input.trim() || isProcessing
|
||||
? isDark
|
||||
? 'bg-slate-700 text-slate-500 cursor-not-allowed'
|
||||
: 'bg-gray-200 text-gray-400 cursor-not-allowed'
|
||||
: isDark
|
||||
? 'bg-blue-600 text-white hover:bg-blue-500'
|
||||
: 'bg-red-500 text-white hover:bg-red-600'
|
||||
}`}
|
||||
>
|
||||
{isProcessing ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
处理中...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Send className="w-4 h-4" />
|
||||
开始处理
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 结果区域 */}
|
||||
{result && (
|
||||
<div className={`mt-6 p-4 rounded-lg border ${
|
||||
isDark ? 'bg-slate-900 border-slate-700' : 'bg-gray-50 border-gray-200'
|
||||
}`}>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles className={`w-4 h-4 ${isDark ? 'text-blue-400' : 'text-red-500'}`} />
|
||||
<span className={`text-sm font-medium ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
|
||||
处理结果
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className={`p-1.5 rounded-lg transition-colors ${
|
||||
isDark ? 'hover:bg-slate-700 text-slate-400' : 'hover:bg-gray-200 text-gray-500'
|
||||
}`}
|
||||
>
|
||||
{copied ? (
|
||||
<CheckCheck className="w-4 h-4 text-green-500" />
|
||||
) : (
|
||||
<Copy className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
<button className={`p-1.5 rounded-lg transition-colors ${
|
||||
isDark ? 'hover:bg-slate-700 text-slate-400' : 'hover:bg-gray-200 text-gray-500'
|
||||
}`}>
|
||||
<Download className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<pre className={`whitespace-pre-wrap text-sm font-mono leading-relaxed ${
|
||||
isDark ? 'text-slate-300' : 'text-gray-600'
|
||||
}`}>
|
||||
{result}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12">
|
||||
<Clock 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'}`}>
|
||||
暂无历史记录
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 提示信息 */}
|
||||
{!selectedApp && (
|
||||
<div className={`p-4 rounded-lg border ${
|
||||
isDark ? 'bg-blue-500/10 border-blue-500/30' : 'bg-blue-50 border-blue-200'
|
||||
}`}>
|
||||
<div className="flex items-start gap-3">
|
||||
<Sparkles className={`w-5 h-5 mt-0.5 ${isDark ? 'text-blue-400' : 'text-blue-500'}`} />
|
||||
<div>
|
||||
<p className={`text-sm font-medium ${isDark ? 'text-blue-300' : 'text-blue-800'}`}>
|
||||
关于轻应用中心
|
||||
</p>
|
||||
<p className={`mt-1 text-sm ${isDark ? 'text-blue-400/80' : 'text-blue-700'}`}>
|
||||
轻应用中心提供了一系列快速分析工具,适合处理临时性的分析需求。相比完整的任务级应用,轻应用更加轻量便捷,可以快速调用多种工具完成分析工作。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LightAppCenter;
|
||||
@@ -0,0 +1,545 @@
|
||||
import React, { useState, useMemo, useEffect, useRef } from 'react';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import {
|
||||
Search,
|
||||
Clock,
|
||||
ChevronRight,
|
||||
Download,
|
||||
Send,
|
||||
Trash2,
|
||||
FileText,
|
||||
User,
|
||||
MessageSquare,
|
||||
Network,
|
||||
Target,
|
||||
BookOpen,
|
||||
History,
|
||||
Filter,
|
||||
Calendar,
|
||||
AlertTriangle,
|
||||
CheckCircle2,
|
||||
Eye,
|
||||
} from 'lucide-react';
|
||||
import { LightAppType } from './LightAppSquare';
|
||||
import { requestCrudService } from '@/service/request-axios-service';
|
||||
import { DateRangePicker, DateRangePickerProps, DateRangeValue, Pagination, Select } from 'tdesign-react';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
// 运行记录类型
|
||||
interface RunRecord {
|
||||
id: string;
|
||||
appId: LightAppType;
|
||||
appName: string;
|
||||
appIcon: React.ElementType;
|
||||
iconColor: string;
|
||||
input: string;
|
||||
runTime: string;
|
||||
resultSummary: string;
|
||||
status: number;
|
||||
riskLevel?: 'low' | 'medium' | 'high';
|
||||
sentiment?: string;
|
||||
inputObject?: string;
|
||||
}
|
||||
|
||||
interface stateData {
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
pageNum: number;
|
||||
pageSize: number;
|
||||
status: string | number;
|
||||
}
|
||||
|
||||
// 应用信息映射
|
||||
const appInfoMap: Record<LightAppType, { name: string; icon: React.ElementType; iconColor: string }> = {
|
||||
'info-ty': { name: '信息TY', icon: FileText, iconColor: 'text-blue-500' },
|
||||
'info-pg': { name: '信息PG', icon: User, iconColor: 'text-cyan-500' },
|
||||
'info-spread': { name: '信息传播分析', icon: Network, iconColor: 'text-teal-500' },
|
||||
'account-pg': { name: '账号PG', icon: User, iconColor: 'text-purple-500' },
|
||||
'sentiment': { name: '三情分析', icon: MessageSquare, iconColor: 'text-orange-500' },
|
||||
'event': { name: '事件分析', icon: Target, iconColor: 'text-red-500' },
|
||||
'topic': { name: '专题分析', icon: BookOpen, iconColor: 'text-green-500' },
|
||||
};
|
||||
|
||||
interface LightAppRecordsProps {
|
||||
onNavigate: (page: string, appId?: string) => void;
|
||||
}
|
||||
|
||||
const appIconList = [
|
||||
{
|
||||
appIcon: MessageSquare,
|
||||
iconColor: 'text-orange-500',
|
||||
},
|
||||
{
|
||||
appIcon: Target,
|
||||
iconColor: 'text-red-500',
|
||||
},
|
||||
{
|
||||
appIcon: User,
|
||||
iconColor: 'text-purple-500',
|
||||
},
|
||||
{
|
||||
appIcon: Network,
|
||||
iconColor: 'text-teal-500',
|
||||
},
|
||||
{
|
||||
appIcon: BookOpen,
|
||||
iconColor: 'text-green-500',
|
||||
},
|
||||
{
|
||||
appIcon: FileText,
|
||||
iconColor: 'text-blue-500',
|
||||
},
|
||||
]
|
||||
|
||||
const service = requestCrudService;
|
||||
const zncmUrl = import.meta.env.VITE_API_BASE_URL || '';
|
||||
const LightAppRecords: React.FC<LightAppRecordsProps> = ({ onNavigate }) => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [appFilter, setAppFilter] = useState<LightAppType | 'all'>('all');
|
||||
const [timeFilter, setTimeFilter] = useState<string>('all');
|
||||
const [records, setRecords] = useState<RunRecord[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [pageNum, setPageNum] = useState<number>(1);
|
||||
const [pageSize, setPageSize] = useState<number>(10); // 每页显示 10 条
|
||||
const [total, setTotal] = useState<number>(0);
|
||||
const [presets] = useState<DateRangePickerProps['presets']>({
|
||||
最近7天: [dayjs().subtract(6, 'day').toDate(), dayjs().toDate()],
|
||||
最近3天: [dayjs().subtract(2, 'day').toDate(), dayjs().toDate()],
|
||||
今天: [dayjs().toDate(), dayjs().toDate()],
|
||||
});
|
||||
const [range, setRange] = useState<DateRangeValue>([]);
|
||||
const state = useRef<stateData>({
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
status: ''
|
||||
})
|
||||
const [statusValue, setStatusValue] = useState<number | string>('');
|
||||
// 执行状态,0=执行中, 1=成功, 2=失败, 3=部分成功
|
||||
const [statusList] = useState([
|
||||
{ value: '', label: '全部' },
|
||||
{ value: 0, label: '执行中' },
|
||||
{ value: 1, label: '成功' },
|
||||
{ value: 2, label: '失败' },
|
||||
{ value: 3, label: '部分成功' },
|
||||
])
|
||||
|
||||
const statusChange = async (val) => {
|
||||
state.current.status = val;
|
||||
setStatusValue(val);
|
||||
await historyPageSearch();
|
||||
}
|
||||
|
||||
// 时间日期选择器查询
|
||||
const setRangeChange = async (val) => {
|
||||
setRange(val);
|
||||
console.log('时间日期', val);
|
||||
state.current.startTime = '';
|
||||
state.current.endTime = '';
|
||||
if (val.length > 0) {
|
||||
state.current.startTime = val[0];
|
||||
state.current.endTime = val[1];
|
||||
}
|
||||
await historyPageSearch();
|
||||
}
|
||||
|
||||
// 过滤记录
|
||||
// const filteredRecords = useMemo(() => {
|
||||
// return records.filter(record => {
|
||||
// // 搜索过滤
|
||||
// const matchesSearch = searchQuery === '' ||
|
||||
// record.input.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
// record.appName.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
// record.resultSummary.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
|
||||
// // 应用类型过滤
|
||||
// const matchesApp = appFilter === 'all' || record.appId === appFilter;
|
||||
|
||||
// // 时间过滤
|
||||
// let matchesTime = true;
|
||||
// if (timeFilter !== 'all') {
|
||||
// const recordDate = new Date(record.runTime);
|
||||
// const now = new Date();
|
||||
// if (timeFilter === '7d') {
|
||||
// const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||
// matchesTime = recordDate >= weekAgo;
|
||||
// } else if (timeFilter === '30d') {
|
||||
// const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||||
// matchesTime = recordDate >= monthAgo;
|
||||
// }
|
||||
// }
|
||||
|
||||
// return matchesSearch && matchesApp && matchesTime;
|
||||
// });
|
||||
// }, [records, searchQuery, appFilter, timeFilter]);
|
||||
|
||||
// 查看详情
|
||||
const handleView = (record: RunRecord) => {
|
||||
console.log('查看详情:', record);
|
||||
|
||||
onNavigate('light-app-run', record.appId);
|
||||
};
|
||||
|
||||
// 导出
|
||||
const handleExport = (record: RunRecord) => {
|
||||
const data = JSON.stringify(record, null, 2);
|
||||
const blob = new Blob([data], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${record.appName}_${record.runTime.replace(/[: ]/g, '-')}.json`;
|
||||
a.click();
|
||||
};
|
||||
|
||||
// 转正式任务
|
||||
const handleConvert = (record: RunRecord) => {
|
||||
onNavigate('task-create');
|
||||
};
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status: number) => {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return { bg: 'bg-green-500/10', text: 'text-green-500', border: 'border-green-500/30' };
|
||||
case 2:
|
||||
return { bg: 'bg-red-500/10', text: 'text-red-500', border: 'border-red-500/30' };
|
||||
case 3:
|
||||
return { bg: 'bg-yellow-500/10', text: 'text-yellow-500', border: 'border-yellow-500/30' };
|
||||
default:
|
||||
return { bg: 'bg-gray-500/10', text: 'text-gray-500', border: 'border-gray-500/30' };
|
||||
}
|
||||
};
|
||||
|
||||
// 获取状态标签
|
||||
const getStatusLabel = (status: number) => {
|
||||
switch (status) {
|
||||
case 0: return '执行中';
|
||||
case 1: return '成功';
|
||||
case 2: return '失败';
|
||||
case 3: return '部分';
|
||||
default: return status;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 回车处理
|
||||
const handleKeyDown = async (e) => {
|
||||
if ((e.metaKey || e.ctrlKey || e.shiftKey) && e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
} else if (e.keyCode === 13) {
|
||||
e.preventDefault();
|
||||
await historyPageSearch();
|
||||
} else if (e.key === "/") {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const historyPageSearch = async () => {
|
||||
const parmy = {
|
||||
appName: searchQuery,
|
||||
keyword: '',
|
||||
pageNum: state.current.pageNum,
|
||||
pageSize: state.current.pageSize,
|
||||
startTime: state.current.startTime,
|
||||
endTime: state.current.endTime,
|
||||
status: state.current.status
|
||||
}
|
||||
setLoading(true);
|
||||
const res = await service.post(`${zncmUrl}/app/history/page`, parmy);
|
||||
console.log('res', res);
|
||||
setLoading(false);
|
||||
const result = res.data;
|
||||
result.records.forEach((item: RunRecord, index: number) => {
|
||||
const realIndex = index % appIconList.length;
|
||||
item.appIcon = appIconList[realIndex].appIcon;
|
||||
item.iconColor = appIconList[realIndex].iconColor;
|
||||
})
|
||||
setTotal(result.total);
|
||||
setRecords(result.records || '');
|
||||
}
|
||||
|
||||
const pageIndexChange = async (value: number) => {
|
||||
state.current.pageNum = value;
|
||||
setPageNum(value);
|
||||
await historyPageSearch();
|
||||
}
|
||||
|
||||
const pageSizeChange = async (value: number) => {
|
||||
state.current.pageSize = value;
|
||||
setPageSize(value);
|
||||
await historyPageSearch();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetch('https://ch1.b.uat.4.cn/zncmweb/templates/index.json') // 注意:不需要 public 前缀
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('网络请求失败');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(jsonData => {
|
||||
console.log('jsonData', jsonData);
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
});
|
||||
historyPageSearch();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 顶部工具栏 */}
|
||||
<div className={`rounded-xl border p-4 ${isDark ? 'bg-slate-800/50 border-slate-700' : 'bg-white border-gray-200'
|
||||
}`}>
|
||||
<div className="flex flex-col lg:flex-row lg:items-center gap-4">
|
||||
{/* 面包屑和标题 */}
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<History className={`w-5 h-5 ${isDark ? 'text-blue-400' : 'text-red-500'}`} />
|
||||
<span className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>轻应用中心 /</span>
|
||||
<span className={`text-sm font-medium ${isDark ? 'text-white' : 'text-gray-900'}`}>运行记录</span>
|
||||
</div>
|
||||
|
||||
{/* 搜索框 */}
|
||||
<div className="flex-1 max-w-md">
|
||||
<div className="relative">
|
||||
<Search className={`absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 ${isDark ? 'text-slate-500' : 'text-gray-400'}`} />
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onKeyDown={(e) => handleKeyDown(e)}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="搜索记录..."
|
||||
className={`w-full pl-10 pr-4 py-2 rounded-lg border 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 focus:ring-blue-500/20'
|
||||
: 'bg-gray-50 border-gray-300 text-gray-900 placeholder-gray-400 focus:border-red-500 focus:ring-red-500/20'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 筛选 */}
|
||||
<div className="flex items-center gap-3">
|
||||
{/* <select
|
||||
value={appFilter}
|
||||
onChange={(e) => setAppFilter(e.target.value as LightAppType | 'all')}
|
||||
className={`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-700 focus:border-red-500'
|
||||
}`}
|
||||
>
|
||||
<option value="all">全部应用</option>
|
||||
<option value="info-ty">信息TY</option>
|
||||
<option value="info-pg">信息PG</option>
|
||||
<option value="info-spread">信息传播分析</option>
|
||||
<option value="account-pg">账号PG</option>
|
||||
<option value="sentiment">三情分析</option>
|
||||
<option value="event">事件分析</option>
|
||||
<option value="topic">专题分析</option>
|
||||
</select> */}
|
||||
<Select
|
||||
value={statusValue}
|
||||
onChange={statusChange}
|
||||
style={{ width: '135px' }}
|
||||
clearable
|
||||
options={statusList} />
|
||||
<DateRangePicker value={range} presets={presets} onChange={(val) => setRangeChange(val)} enableTimePicker={true} clearable />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 记录列表 */}
|
||||
<div className={`rounded-xl border overflow-hidden ${isDark ? 'bg-slate-800/50 border-slate-700' : 'bg-white border-gray-200'
|
||||
}`}>
|
||||
{/* 表头 */}
|
||||
<div className={`grid grid-cols-12 gap-4 px-4 py-3 border-b ${isDark ? 'border-slate-700 bg-slate-800/30' : 'border-gray-200 bg-gray-50'
|
||||
}`}>
|
||||
<div className={`col-span-3 text-xs font-medium ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
应用名称
|
||||
</div>
|
||||
<div className={`col-span-2 text-xs font-medium ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
输入对象
|
||||
</div>
|
||||
<div className={`col-span-2 text-xs font-medium ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
运行时间
|
||||
</div>
|
||||
<div className={`col-span-2 text-xs font-medium ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
结果摘要
|
||||
</div>
|
||||
<div className={`col-span-1 text-xs font-medium ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
状态
|
||||
</div>
|
||||
<div className={`col-span-2 text-xs font-medium ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
操作
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 记录行 */}
|
||||
{records.length > 0 ? (
|
||||
records.map((record) => {
|
||||
const Icon = record.appIcon;
|
||||
const statusColor = getStatusColor(record.status);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={record.id}
|
||||
className={`grid grid-cols-12 gap-4 px-4 py-4 border-b items-center ${isDark ? 'border-slate-700/50 hover:bg-slate-700/30' : 'border-gray-100 hover:bg-gray-50'
|
||||
} transition-colors`}
|
||||
>
|
||||
{/* 应用名称 */}
|
||||
<div className="col-span-3 flex items-center gap-3">
|
||||
<div className={`p-2 rounded-lg ${isDark ? 'bg-slate-700/50' : 'bg-gray-100'}`}>
|
||||
<Icon className={`w-4 h-4 ${record.iconColor}`} />
|
||||
</div>
|
||||
<div>
|
||||
<span className={`text-sm font-medium ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
||||
{record.appName}
|
||||
</span>
|
||||
{/* {record.sentiment && (
|
||||
<span className={`ml-2 px-1.5 py-0.5 rounded text-xs ${record.sentiment === '偏正' ? 'bg-green-500/10 text-green-500' :
|
||||
record.sentiment === '偏负' ? 'bg-red-500/10 text-red-500' :
|
||||
'bg-gray-500/10 text-gray-500'
|
||||
}`}>
|
||||
{record.sentiment}
|
||||
</span>
|
||||
)} */}
|
||||
{/* {record.riskLevel && (
|
||||
<span className={`ml-2 px-1.5 py-0.5 rounded text-xs ${record.riskLevel === 'high' ? 'bg-red-500/10 text-red-500' :
|
||||
record.riskLevel === 'medium' ? 'bg-yellow-500/10 text-yellow-500' :
|
||||
'bg-green-500/10 text-green-500'
|
||||
}`}>
|
||||
{record.riskLevel === 'high' ? '高风险' :
|
||||
record.riskLevel === 'medium' ? '中风险' : '低风险'}
|
||||
</span>
|
||||
)} */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 输入对象 */}
|
||||
<div className="col-span-2">
|
||||
<span className={`text-sm truncate block ${isDark ? 'text-slate-400' : 'text-gray-600'}`} title={record.input}>
|
||||
{record.inputObject || '-'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 运行时间 */}
|
||||
<div className="col-span-2">
|
||||
<span className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-600'}`}>
|
||||
{record.runTime}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 结果摘要 */}
|
||||
<div className="col-span-2">
|
||||
<span className={`text-sm ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
|
||||
{record.resultSummary || '-'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 状态 */}
|
||||
<div className="col-span-1">
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${statusColor.bg} ${statusColor.text}`}>
|
||||
{getStatusLabel(record.status)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 操作 */}
|
||||
<div className="col-span-2 flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => handleView(record)}
|
||||
className={`p-1.5 rounded-lg transition-colors ${isDark ? 'hover:bg-slate-600 text-slate-400' : 'hover:bg-gray-200 text-gray-500'
|
||||
}`}
|
||||
title="查看"
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleExport(record)}
|
||||
className={`p-1.5 rounded-lg transition-colors ${isDark ? 'hover:bg-slate-600 text-slate-400' : 'hover:bg-gray-200 text-gray-500'
|
||||
}`}
|
||||
title="导出"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleConvert(record)}
|
||||
className={`p-1.5 rounded-lg transition-colors ${isDark ? 'hover:bg-slate-600 text-slate-400' : 'hover:bg-gray-200 text-gray-500'
|
||||
}`}
|
||||
title="转正式任务"
|
||||
>
|
||||
<Send className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
className={`p-1.5 rounded-lg transition-colors ${isDark ? 'hover:bg-slate-600 text-slate-400' : 'hover:bg-gray-200 text-gray-500'
|
||||
}`}
|
||||
title="删除"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="text-center py-12">
|
||||
<History 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'}`}>
|
||||
暂无运行记录
|
||||
</p>
|
||||
</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">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle2 className={`w-4 h-4 ${isDark ? 'text-green-400' : 'text-green-500'}`} />
|
||||
<span className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
成功: <span className={`font-medium ${isDark ? 'text-white' : 'text-gray-900'}`}>{filteredRecords.filter(r => r.status === 'success').length}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertTriangle className={`w-4 h-4 ${isDark ? 'text-yellow-400' : 'text-yellow-500'}`} />
|
||||
<span className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
部分: <span className={`font-medium ${isDark ? 'text-white' : 'text-gray-900'}`}>{filteredRecords.filter(r => r.status === 'partial').length}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
共 <span className={`font-medium ${isDark ? 'text-white' : 'text-gray-900'}`}>{filteredRecords.length}</span> 条记录
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onNavigate('light-app-square')}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium flex items-center gap-2 transition-colors ${isDark
|
||||
? 'bg-blue-500/20 text-blue-400 hover:bg-blue-500/30'
|
||||
: 'bg-red-50 text-red-600 hover:bg-red-100'
|
||||
}`}
|
||||
>
|
||||
<FileText className="w-4 h-4" />
|
||||
新建分析
|
||||
</button>
|
||||
</div>
|
||||
</div> */}
|
||||
{/* 分页 */}
|
||||
<div className="p-4 text-center">
|
||||
<Pagination
|
||||
total={total}
|
||||
current={pageNum}
|
||||
pageSize={pageSize}
|
||||
onCurrentChange={(page) => pageIndexChange(Number(page))}
|
||||
onPageSizeChange={(size) => pageSizeChange(Number(size))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LightAppRecords;
|
||||
@@ -0,0 +1,996 @@
|
||||
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;
|
||||
@@ -0,0 +1,681 @@
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import {
|
||||
Search,
|
||||
ChevronRight,
|
||||
PlayCircle,
|
||||
Clock,
|
||||
Star,
|
||||
FileText,
|
||||
User,
|
||||
Zap,
|
||||
BookOpen,
|
||||
TrendingUp,
|
||||
MessageSquare,
|
||||
Network,
|
||||
Link2,
|
||||
Filter,
|
||||
Layout,
|
||||
Sparkles,
|
||||
BarChart3,
|
||||
Users,
|
||||
Target,
|
||||
PieChart,
|
||||
Send,
|
||||
Plus,
|
||||
} from 'lucide-react';
|
||||
import { Button, Dialog, Radio, Upload, Input, MessagePlugin, Row, Col, Textarea } from 'tdesign-react';
|
||||
import { get } from '../lib/api';
|
||||
import type { UploadFile, UploadProps } from 'tdesign-react';
|
||||
import { requestCrudService } from '@/service/request-axios-service';
|
||||
|
||||
interface LightAppSquareProps {
|
||||
onNavigate: (page: string, appId?: string) => void;
|
||||
}
|
||||
|
||||
// 定义轻应用类型
|
||||
export type LightAppType =
|
||||
| 'info-ty' // 信息TY
|
||||
| 'info-pg' // 信息PG
|
||||
| 'info-spread' // 信息传播分析
|
||||
| 'account-pg' // 账号PG
|
||||
| 'sentiment' // 三情分析
|
||||
| 'event' // 事件分析
|
||||
| 'topic'; // 专题分析
|
||||
|
||||
// 应用分类
|
||||
type AppCategory = 'info' | 'account' | 'event' | 'topic';
|
||||
|
||||
interface LightApp {
|
||||
id: LightAppType;
|
||||
name: string;
|
||||
description: string;
|
||||
category: AppCategory;
|
||||
categoryName: string;
|
||||
input: string;
|
||||
output: string;
|
||||
icon: React.ElementType;
|
||||
color: string;
|
||||
bgColor: string;
|
||||
borderColor: string;
|
||||
iconColor: string;
|
||||
dimensions?: string[]; // 可选的额外分析维度
|
||||
}
|
||||
|
||||
interface analysisDataItem {
|
||||
appName: string;
|
||||
appDesc: string;
|
||||
appIcon: string;
|
||||
jumpUrl: string;
|
||||
appType: number
|
||||
}
|
||||
|
||||
interface analysisListItem {
|
||||
categoryName: string;
|
||||
category: string;
|
||||
appName: string;
|
||||
appDesc: string;
|
||||
appIcon: string;
|
||||
jumpUrl: string;
|
||||
appType: number
|
||||
}
|
||||
|
||||
interface analysisListData {
|
||||
internalList: analysisListItem[];
|
||||
externalList: analysisListItem[];
|
||||
}
|
||||
|
||||
// 应用数据
|
||||
const lightApps: LightApp[] = [
|
||||
// 信息分析类
|
||||
{
|
||||
id: 'info-ty',
|
||||
name: '信息TY',
|
||||
description: '围绕单条或多条信息进行TY向分析',
|
||||
category: 'info',
|
||||
categoryName: '信息分析类',
|
||||
input: '信息文本/链接/素材',
|
||||
output: '信息特征、重点摘要、分析判断',
|
||||
icon: FileText,
|
||||
color: 'blue',
|
||||
bgColor: 'bg-blue-500/10',
|
||||
borderColor: 'border-blue-500/30',
|
||||
iconColor: 'text-blue-500',
|
||||
dimensions: ['基础分析', '风险提示', '摘要结论'],
|
||||
},
|
||||
{
|
||||
id: 'info-pg',
|
||||
name: '信息PG',
|
||||
description: '围绕单条或多条信息进行PG向分析',
|
||||
category: 'info',
|
||||
categoryName: '信息分析类',
|
||||
input: '信息文本/链接/素材',
|
||||
output: '画像特征、相关判断',
|
||||
icon: User,
|
||||
color: 'cyan',
|
||||
bgColor: 'bg-cyan-500/10',
|
||||
borderColor: 'border-cyan-500/30',
|
||||
iconColor: 'text-cyan-500',
|
||||
dimensions: ['账号画像', '内容倾向', '行为特征'],
|
||||
},
|
||||
{
|
||||
id: 'info-spread',
|
||||
name: '信息传播分析',
|
||||
description: '分析信息传播路径、传播范围与传播表现',
|
||||
category: 'info',
|
||||
categoryName: '信息分析类',
|
||||
input: '信息链接/信息集合',
|
||||
output: '传播过程、传播节点、传播影响',
|
||||
icon: Network,
|
||||
color: 'teal',
|
||||
bgColor: 'bg-teal-500/10',
|
||||
borderColor: 'border-teal-500/30',
|
||||
iconColor: 'text-teal-500',
|
||||
dimensions: ['传播路径', '传播节点', '传播表现', '影响摘要'],
|
||||
},
|
||||
// 账号分析类
|
||||
{
|
||||
id: 'account-pg',
|
||||
name: '账号PG',
|
||||
description: '围绕账号内容与行为特征进行PG分析',
|
||||
category: 'account',
|
||||
categoryName: '账号分析类',
|
||||
input: '账号ID/账号链接/内容集合',
|
||||
output: '账号画像、内容倾向、行为特征',
|
||||
icon: Users,
|
||||
color: 'purple',
|
||||
bgColor: 'bg-purple-500/10',
|
||||
borderColor: 'border-purple-500/30',
|
||||
iconColor: 'text-purple-500',
|
||||
dimensions: ['账号画像', '内容倾向', '行为特征', '影响判断'],
|
||||
},
|
||||
// 事件分析类
|
||||
{
|
||||
id: 'sentiment',
|
||||
name: '三情分析',
|
||||
description: '对事件进行舆情、社情、民情综合分析',
|
||||
category: 'event',
|
||||
categoryName: '事件分析类',
|
||||
input: '事件关键词/事件链接/素材',
|
||||
output: '情绪概览、关注焦点、风险提示',
|
||||
icon: MessageSquare,
|
||||
color: 'orange',
|
||||
bgColor: 'bg-orange-500/10',
|
||||
borderColor: 'border-orange-500/30',
|
||||
iconColor: 'text-orange-500',
|
||||
dimensions: ['舆情分析', '社情分析', '民情分析', '风险提示'],
|
||||
},
|
||||
{
|
||||
id: 'event',
|
||||
name: '事件分析',
|
||||
description: '围绕某个事件进行整体分析',
|
||||
category: 'event',
|
||||
categoryName: '事件分析类',
|
||||
input: '事件名称/事件材料/事件链接',
|
||||
output: '事件脉络、关键节点、风险判断',
|
||||
icon: Target,
|
||||
color: 'red',
|
||||
bgColor: 'bg-red-500/10',
|
||||
borderColor: 'border-red-500/30',
|
||||
iconColor: 'text-red-500',
|
||||
dimensions: ['事件脉络', '关键节点', '风险判断', '初步建议'],
|
||||
},
|
||||
// 专题分析类
|
||||
{
|
||||
id: 'topic',
|
||||
name: '专题分析',
|
||||
description: '对某专题做集中分析',
|
||||
category: 'topic',
|
||||
categoryName: '专题分析类',
|
||||
input: '专题名称/专题材料/文本集合',
|
||||
output: '专题结构、核心问题、观点聚合、结论建议',
|
||||
icon: BookOpen,
|
||||
color: 'green',
|
||||
bgColor: 'bg-green-500/10',
|
||||
borderColor: 'border-green-500/30',
|
||||
iconColor: 'text-green-500',
|
||||
dimensions: ['专题结构', '核心问题', '观点聚合', '结论建议'],
|
||||
},
|
||||
];
|
||||
|
||||
const categoryOrder: AppCategory[] = ['info', 'account', 'event', 'topic'];
|
||||
const zncmUrl = import.meta.env.VITE_API_BASE_URL || '';
|
||||
|
||||
const LightAppSquare: React.FC<LightAppSquareProps> = ({ onNavigate }) => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [categoryFilter, setCategoryFilter] = useState<AppCategory | 'all'>('all');
|
||||
const [recentApps, setRecentApps] = useState<LightAppType[]>(['sentiment', 'event', 'account-pg']);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [analysisData, setAnalysisData] = useState<analysisDataItem>({
|
||||
appName: '',
|
||||
appDesc: '',
|
||||
appIcon: '',
|
||||
jumpUrl: '',
|
||||
appType: 2,
|
||||
});
|
||||
const [analysisList, setAnalysisList] = useState<analysisListData>(
|
||||
{
|
||||
internalList: [], // 内部应用
|
||||
externalList: [], // 外部应用
|
||||
})
|
||||
const [fileFail, setFileFail] = useState<UploadFile[]>([
|
||||
// 如果需要默认回显图片,可以在这里初始化
|
||||
// { name: '默认图片.png', url: 'https://tdesign.gtimg.com/site/avatar.jpg', status: 'fail' }
|
||||
]);
|
||||
const showImageFileName = true;
|
||||
const service = requestCrudService;
|
||||
// 解构出 Group 和 Button,方便使用
|
||||
const { Group: RadioGroup, Button: RadioButton } = Radio;
|
||||
// 用于格式化接口返回的数据,使其符合 TDesign 的要求
|
||||
const formatResponse = (res: any) => {
|
||||
// 假设接口返回 { code: 0, data: { url: '...' } }
|
||||
// 如果接口报错,可以在这里处理,让组件显示失败状态
|
||||
if (res.code !== 0) {
|
||||
return {
|
||||
error: res.message || '上传失败',
|
||||
url: '',
|
||||
name: ''
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
url: res.data.url,
|
||||
name: res.data.name,
|
||||
};
|
||||
};
|
||||
|
||||
const handleChange: UploadProps['onChange'] = (files) => {
|
||||
setFileFail(files);
|
||||
console.log('当前文件列表:', files);
|
||||
};
|
||||
|
||||
// 过滤应用
|
||||
const filteredApps = useMemo(() => {
|
||||
return lightApps.filter(app => {
|
||||
const matchesSearch = searchQuery === '' ||
|
||||
app.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
app.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
app.input.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
const matchesCategory = categoryFilter === 'all' || app.category === categoryFilter;
|
||||
return matchesSearch && matchesCategory;
|
||||
});
|
||||
}, [searchQuery, categoryFilter]);
|
||||
|
||||
// 按分类分组
|
||||
const groupedApps = useMemo(() => {
|
||||
const groups: Record<AppCategory, LightApp[]> = {
|
||||
info: [],
|
||||
account: [],
|
||||
event: [],
|
||||
topic: [],
|
||||
};
|
||||
filteredApps.forEach(app => {
|
||||
groups[app.category].push(app);
|
||||
});
|
||||
return groups;
|
||||
}, [filteredApps]);
|
||||
|
||||
// 处理应用启动
|
||||
const handleLaunchApp = (app: LightApp) => {
|
||||
console.log('启动应用:', app);
|
||||
|
||||
// 更新最近使用
|
||||
setRecentApps(prev => {
|
||||
const filtered = prev.filter(id => id !== app.id);
|
||||
return [app.id, ...filtered].slice(0, 5);
|
||||
});
|
||||
onNavigate('light-app-run', app.id);
|
||||
};
|
||||
|
||||
// 获取分类颜色
|
||||
const getCategoryColor = (category: AppCategory) => {
|
||||
const colors: Record<AppCategory, { text: string; bg: string }> = {
|
||||
info: { text: isDark ? 'text-blue-400' : 'text-blue-600', bg: isDark ? 'bg-blue-500/20' : 'bg-blue-50' },
|
||||
account: { text: isDark ? 'text-purple-400' : 'text-purple-600', bg: isDark ? 'bg-purple-500/20' : 'bg-purple-50' },
|
||||
event: { text: isDark ? 'text-orange-400' : 'text-orange-600', bg: isDark ? 'bg-orange-500/20' : 'bg-orange-50' },
|
||||
topic: { text: isDark ? 'text-green-400' : 'text-green-600', bg: isDark ? 'bg-green-500/20' : 'bg-green-50' },
|
||||
};
|
||||
return colors[category];
|
||||
};
|
||||
|
||||
// 获取应用列表
|
||||
const analysisListSearch = async () => {
|
||||
const parmy = {
|
||||
appName: '',
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
}
|
||||
|
||||
const res = await service.post(`${zncmUrl}/app/analysis/list`, parmy)
|
||||
console.log(res);
|
||||
const result = res.data;
|
||||
const listData: analysisListData = {
|
||||
internalList: [],
|
||||
externalList: [],
|
||||
}
|
||||
result.records.map((item: analysisListItem) => {
|
||||
if (item.appType === 1) {
|
||||
item.category = 'info';
|
||||
item.categoryName = '内部应用';
|
||||
listData.internalList.push(item)
|
||||
} else if (item.appType === 2) {
|
||||
item.category = 'account';
|
||||
item.categoryName = '外部应用';
|
||||
listData.internalList.push(item)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 添加应用
|
||||
const analysieAddListChange = async () => {
|
||||
const res = await service.post(`${zncmUrl}/app/analysis/add`, analysisData);
|
||||
console.log(res);
|
||||
MessagePlugin.success('添加成功');
|
||||
}
|
||||
|
||||
const showDialog = () => {
|
||||
setAnalysisData({
|
||||
appName: '',
|
||||
appDesc: '',
|
||||
appIcon: '',
|
||||
jumpUrl: '',
|
||||
appType: 2,
|
||||
})
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
const handleOk = async () => {
|
||||
await analysieAddListChange();
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const getBase64 = (file: File): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = () => resolve(reader.result as string);
|
||||
reader.onerror = (error) => reject(error);
|
||||
});
|
||||
};
|
||||
|
||||
const imgUpload: UploadProps['requestMethod'] = async (file: any) => {
|
||||
console.log('file', file);
|
||||
const base64 = await getBase64(file.raw)
|
||||
handleDataChange('appIcon', base64);
|
||||
return {
|
||||
status: 'success',
|
||||
// response 对象里的数据会传给 formatResponse
|
||||
response: {
|
||||
url: '',
|
||||
name: '',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 通用的处理函数
|
||||
const handleDataChange = (key: keyof typeof analysisData, value: string | number) => {
|
||||
setAnalysisData((prev) => ({
|
||||
...prev,
|
||||
[key]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
// 发送请求
|
||||
useEffect(() => {
|
||||
analysisListSearch();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 顶部工具栏 */}
|
||||
<div className={`rounded-xl border p-4 ${isDark ? 'bg-slate-800/50 border-slate-700' : 'bg-white border-gray-200'
|
||||
}`}>
|
||||
<div className="flex flex-col lg:flex-row lg:items-center gap-4">
|
||||
{/* 面包屑和标题 */}
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<Layout className={`w-5 h-5 ${isDark ? 'text-blue-400' : 'text-red-500'}`} />
|
||||
<span className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>轻应用中心 /</span>
|
||||
<span className={`text-sm font-medium ${isDark ? 'text-white' : 'text-gray-900'}`}>应用广场</span>
|
||||
</div>
|
||||
|
||||
{/* 搜索框 */}
|
||||
<div className="flex-1 max-w-md">
|
||||
<div className="relative">
|
||||
<Search className={`absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 ${isDark ? 'text-slate-500' : 'text-gray-400'}`} />
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="搜索应用名称或分析对象..."
|
||||
className={`w-full pl-10 pr-4 py-2 rounded-lg border 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 focus:ring-blue-500/20'
|
||||
: 'bg-gray-50 border-gray-300 text-gray-900 placeholder-gray-400 focus:border-red-500 focus:ring-red-500/20'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 筛选和操作 */}
|
||||
<div className="flex items-center gap-3">
|
||||
<select
|
||||
value={categoryFilter}
|
||||
onChange={(e) => setCategoryFilter(e.target.value as AppCategory | 'all')}
|
||||
className={`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-700 focus:border-red-500'
|
||||
}`}
|
||||
>
|
||||
<option value="all">全部分类</option>
|
||||
<option value="info">信息分析类</option>
|
||||
<option value="account">账号分析类</option>
|
||||
<option value="event">事件分析类</option>
|
||||
<option value="topic">专题分析类</option>
|
||||
</select>
|
||||
<button
|
||||
onClick={() => onNavigate('light-app-records')}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium flex items-center gap-2 transition-colors ${isDark
|
||||
? 'bg-slate-700 text-slate-300 hover:bg-slate-600'
|
||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
<Clock className="w-4 h-4" />
|
||||
运行记录
|
||||
</button>
|
||||
<Button theme="primary" onClick={showDialog}>
|
||||
<span className="flex items-center">
|
||||
<Plus className="w-4 h-4 mr-[10px]" />
|
||||
添加
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 最近使用 */}
|
||||
{recentApps.length > 0 && searchQuery === '' && categoryFilter === 'all' && (
|
||||
<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 gap-2 mb-4">
|
||||
<Clock className={`w-4 h-4 ${isDark ? 'text-blue-400' : 'text-red-500'}`} />
|
||||
<h3 className={`text-sm font-medium ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>最近使用</h3>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{recentApps.map(appId => {
|
||||
const app = lightApps.find(a => a.id === appId);
|
||||
if (!app) return null;
|
||||
const Icon = app.icon;
|
||||
return (
|
||||
<button
|
||||
key={app.id}
|
||||
onClick={() => handleLaunchApp(app)}
|
||||
className={`px-4 py-2 rounded-lg border text-sm font-medium flex items-center gap-2 transition-all hover:scale-105 ${isDark
|
||||
? `bg-slate-700/50 border-slate-600 text-slate-300 hover:bg-slate-700`
|
||||
: `bg-gray-50 border-gray-200 text-gray-700 hover:bg-gray-100`
|
||||
}`}
|
||||
>
|
||||
<Icon className={`w-4 h-4 ${app.iconColor}`} />
|
||||
{app.name}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 应用分组区 */}
|
||||
<div className="space-y-6">
|
||||
{categoryOrder.map(category => {
|
||||
const apps = groupedApps[category];
|
||||
if (apps.length === 0) return null;
|
||||
const categoryInfo = lightApps.find(a => a.category === category);
|
||||
const catColor = getCategoryColor(category);
|
||||
|
||||
return (
|
||||
<div key={category} className="space-y-4">
|
||||
{/* 分类标题 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className={`text-lg font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
||||
{categoryInfo?.categoryName}
|
||||
</h2>
|
||||
<span className={`px-2 py-0.5 rounded text-xs font-medium ${catColor.bg} ${catColor.text}`}>
|
||||
{apps.length} 个应用
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 应用卡片网格 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{apps.map(app => {
|
||||
const Icon = app.icon;
|
||||
return (
|
||||
<div
|
||||
key={app.id}
|
||||
className={`rounded-xl border p-5 transition-all duration-200 hover:shadow-lg group ${isDark
|
||||
? 'bg-slate-800/50 border-slate-700 hover:border-slate-600'
|
||||
: 'bg-white border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
{/* 应用头部 */}
|
||||
<div className="flex items-start gap-4 mb-4">
|
||||
<div className={`p-3 rounded-lg ${app.bgColor}`}>
|
||||
<Icon className={`w-6 h-6 ${app.iconColor}`} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className={`font-semibold group-hover:${app.iconColor} ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
||||
{app.name}
|
||||
</h3>
|
||||
<p className={`mt-1 text-sm line-clamp-2 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
{app.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 输入输出 */}
|
||||
<div className={`text-xs space-y-1 mb-4 ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="font-medium">输入:</span>
|
||||
<span>{app.input}</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="font-medium">输出:</span>
|
||||
<span>{app.output}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 立即使用按钮 */}
|
||||
<button
|
||||
onClick={() => handleLaunchApp(app)}
|
||||
className={`w-full py-2.5 rounded-lg font-medium flex items-center justify-center gap-2 transition-all ${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'
|
||||
}`}
|
||||
>
|
||||
<PlayCircle className="w-4 h-4" />
|
||||
立即使用
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 空状态 */}
|
||||
{filteredApps.length === 0 && (
|
||||
<div className={`text-center py-12 rounded-xl border ${isDark ? 'bg-slate-800/50 border-slate-700' : 'bg-white border-gray-200'
|
||||
}`}>
|
||||
<Search 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'}`}>
|
||||
未找到匹配的应用,请尝试其他搜索词或筛选条件
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 底部提示 */}
|
||||
<div className={`p-4 rounded-xl border ${isDark ? 'bg-blue-500/10 border-blue-500/30' : 'bg-blue-50 border-blue-200'
|
||||
}`}>
|
||||
<div className="flex items-start gap-3">
|
||||
<Sparkles className={`w-5 h-5 mt-0.5 ${isDark ? 'text-blue-400' : 'text-blue-500'}`} />
|
||||
<div>
|
||||
<p className={`text-sm font-medium ${isDark ? 'text-blue-300' : 'text-blue-800'}`}>
|
||||
关于轻应用中心
|
||||
</p>
|
||||
<p className={`mt-1 text-sm ${isDark ? 'text-blue-400/80' : 'text-blue-700'}`}>
|
||||
轻应用中心提供了一系列快速分析工具,适合处理临时性的分析需求。相比完整的任务级应用,轻应用更加轻量便捷,可以快速调用多种工具完成分析工作。运行结果可保存快照、导出或转为正式任务。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog
|
||||
header="添加应用"
|
||||
visible={visible}
|
||||
onClose={handleClose} // 监听关闭事件
|
||||
onConfirm={handleOk} // 点击确定按钮
|
||||
onCancel={handleClose} // 点击取消按钮
|
||||
closeOnOverlayClick={true} // 点击遮罩层是否关闭
|
||||
>
|
||||
<div className='mb-[10px]'>
|
||||
<Upload
|
||||
// 对应 v-model
|
||||
files={fileFail}
|
||||
onChange={handleChange}
|
||||
// 对应 theme="image"
|
||||
theme="image"
|
||||
requestMethod={imgUpload}
|
||||
// 对应 accept
|
||||
accept="image/*"
|
||||
tips="系统logo"
|
||||
// 对应 :show-image-file-name
|
||||
showImageFileName={showImageFileName}
|
||||
// 对应 :format-response
|
||||
formatResponse={formatResponse}
|
||||
// 可选:限制上传数量(演示单张)
|
||||
multiple={false}
|
||||
// 可选:上传失败时的提示(如果接口直接返回 4xx/5xx)
|
||||
onFail={({ e, file }) => {
|
||||
MessagePlugin.error(`文件 ${file.name} 上传失败`);
|
||||
}} />
|
||||
</div>
|
||||
<div className='mb-[10px] overflow-hidden'>
|
||||
<Row>
|
||||
<Col span={2}>
|
||||
<div>名称:</div>
|
||||
</Col>
|
||||
<Col span={9}>
|
||||
<Input placeholder="请输入应用名称" value={analysisData.appName} onChange={(val) => handleDataChange('appName', val)} clearable />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
<div className='mb-[10px] overflow-hidden'>
|
||||
<Row>
|
||||
<Col span={2}>
|
||||
<div>详情:</div>
|
||||
</Col>
|
||||
<Col span={9}>
|
||||
<Textarea placeholder="请输入应用详情" value={analysisData.appDesc} onChange={(val) => handleDataChange('appDesc', val)} autosize />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
<div className='mb-[10px] overflow-hidden'>
|
||||
<Row>
|
||||
<Col span={2}>
|
||||
<div>类型:</div>
|
||||
</Col>
|
||||
<Col span={9}>
|
||||
<RadioGroup
|
||||
variant="primary-filled" // 对应 variant="primary-filled"
|
||||
value={analysisData.appType} // 绑定状态值
|
||||
onChange={(value) => handleDataChange('appType', Number(value))}
|
||||
>
|
||||
{/* 3. 使用 RadioButton */}
|
||||
<RadioButton value={1}>内部应用</RadioButton>
|
||||
<RadioButton value={2}>外部应用</RadioButton>
|
||||
<RadioButton value={3}>第三方集成</RadioButton>
|
||||
</RadioGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
<div className='mb-[10px] overflow-hidden'>
|
||||
<Row>
|
||||
<Col span={2}>
|
||||
<div>链接:</div>
|
||||
</Col>
|
||||
<Col span={9}>
|
||||
<Input placeholder="请输入链接" value={analysisData.jumpUrl} onChange={(val) => handleDataChange('jumpUrl', val)} clearable />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LightAppSquare;
|
||||
@@ -0,0 +1,979 @@
|
||||
import React, { useState, useMemo, useEffect, useRef } from "react";
|
||||
import { useTheme } from "../contexts/ThemeContext";
|
||||
import {
|
||||
Search,
|
||||
ChevronRight,
|
||||
PlayCircle,
|
||||
Clock,
|
||||
Star,
|
||||
FileText,
|
||||
User,
|
||||
Zap,
|
||||
BookOpen,
|
||||
TrendingUp,
|
||||
MessageSquare,
|
||||
Network,
|
||||
Link2,
|
||||
Filter,
|
||||
Layout,
|
||||
Sparkles,
|
||||
BarChart3,
|
||||
Users,
|
||||
Target,
|
||||
PieChart,
|
||||
Send,
|
||||
Plus,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
Radio,
|
||||
Upload,
|
||||
Input,
|
||||
MessagePlugin,
|
||||
Row,
|
||||
Col,
|
||||
Textarea,
|
||||
Loading,
|
||||
Empty,
|
||||
Pagination,
|
||||
} from "tdesign-react";
|
||||
import { get } from "../lib/api";
|
||||
import type { UploadFile, UploadProps } from "tdesign-react";
|
||||
import { requestCrudService } from "@/service/request-axios-service";
|
||||
import { AppCard } from "@/components/square/AppCard";
|
||||
|
||||
interface LightAppSquareProps {
|
||||
onNavigate: (page: string, appId?: string) => void;
|
||||
}
|
||||
|
||||
// 定义轻应用类型
|
||||
export type LightAppType =
|
||||
| "info-ty" // 信息TY
|
||||
| "info-pg" // 信息PG
|
||||
| "info-spread" // 信息传播分析
|
||||
| "account-pg" // 账号PG
|
||||
| "sentiment" // 三情分析
|
||||
| "event" // 事件分析
|
||||
| "topic"; // 专题分析
|
||||
|
||||
// 应用分类
|
||||
type AppCategory = "info" | "account" | "event" | "topic";
|
||||
|
||||
interface LightApp {
|
||||
id: LightAppType;
|
||||
name: string;
|
||||
description: string;
|
||||
category: AppCategory;
|
||||
categoryName: string;
|
||||
input: string;
|
||||
output: string;
|
||||
icon: React.ElementType;
|
||||
color: string;
|
||||
bgColor: string;
|
||||
borderColor: string;
|
||||
iconColor: string;
|
||||
dimensions?: string[]; // 可选的额外分析维度
|
||||
}
|
||||
|
||||
interface analysisDataItem {
|
||||
appName: string;
|
||||
appDesc: string;
|
||||
appIcon: string;
|
||||
jumpUrl: string;
|
||||
appType: number;
|
||||
}
|
||||
|
||||
interface analysisListItem {
|
||||
id: number;
|
||||
categoryName: string;
|
||||
category: string;
|
||||
appName: string;
|
||||
appDesc: string;
|
||||
appIcon: string;
|
||||
jumpUrl: string;
|
||||
appType: number;
|
||||
}
|
||||
|
||||
interface analysisListData {
|
||||
key: "info" | "account" | "event" | "topic";
|
||||
list: analysisListItem[];
|
||||
}
|
||||
|
||||
// 应用数据
|
||||
const lightApps: LightApp[] = [
|
||||
// 信息分析类
|
||||
{
|
||||
id: "info-ty",
|
||||
name: "信息TY",
|
||||
description: "围绕单条或多条信息进行TY向分析",
|
||||
category: "info",
|
||||
categoryName: "信息分析类",
|
||||
input: "信息文本/链接/素材",
|
||||
output: "信息特征、重点摘要、分析判断",
|
||||
icon: FileText,
|
||||
color: "blue",
|
||||
bgColor: "bg-blue-500/10",
|
||||
borderColor: "border-blue-500/30",
|
||||
iconColor: "text-blue-500",
|
||||
dimensions: ["基础分析", "风险提示", "摘要结论"],
|
||||
},
|
||||
{
|
||||
id: "info-pg",
|
||||
name: "信息PG",
|
||||
description: "围绕单条或多条信息进行PG向分析",
|
||||
category: "info",
|
||||
categoryName: "信息分析类",
|
||||
input: "信息文本/链接/素材",
|
||||
output: "画像特征、相关判断",
|
||||
icon: User,
|
||||
color: "cyan",
|
||||
bgColor: "bg-cyan-500/10",
|
||||
borderColor: "border-cyan-500/30",
|
||||
iconColor: "text-cyan-500",
|
||||
dimensions: ["账号画像", "内容倾向", "行为特征"],
|
||||
},
|
||||
{
|
||||
id: "info-spread",
|
||||
name: "信息传播分析",
|
||||
description: "分析信息传播路径、传播范围与传播表现",
|
||||
category: "info",
|
||||
categoryName: "信息分析类",
|
||||
input: "信息链接/信息集合",
|
||||
output: "传播过程、传播节点、传播影响",
|
||||
icon: Network,
|
||||
color: "teal",
|
||||
bgColor: "bg-teal-500/10",
|
||||
borderColor: "border-teal-500/30",
|
||||
iconColor: "text-teal-500",
|
||||
dimensions: ["传播路径", "传播节点", "传播表现", "影响摘要"],
|
||||
},
|
||||
// 账号分析类
|
||||
{
|
||||
id: "account-pg",
|
||||
name: "账号PG",
|
||||
description: "围绕账号内容与行为特征进行PG分析",
|
||||
category: "account",
|
||||
categoryName: "账号分析类",
|
||||
input: "账号ID/账号链接/内容集合",
|
||||
output: "账号画像、内容倾向、行为特征",
|
||||
icon: Users,
|
||||
color: "purple",
|
||||
bgColor: "bg-purple-500/10",
|
||||
borderColor: "border-purple-500/30",
|
||||
iconColor: "text-purple-500",
|
||||
dimensions: ["账号画像", "内容倾向", "行为特征", "影响判断"],
|
||||
},
|
||||
// 事件分析类
|
||||
{
|
||||
id: "sentiment",
|
||||
name: "三情分析",
|
||||
description: "对事件进行舆情、社情、民情综合分析",
|
||||
category: "event",
|
||||
categoryName: "事件分析类",
|
||||
input: "事件关键词/事件链接/素材",
|
||||
output: "情绪概览、关注焦点、风险提示",
|
||||
icon: MessageSquare,
|
||||
color: "orange",
|
||||
bgColor: "bg-orange-500/10",
|
||||
borderColor: "border-orange-500/30",
|
||||
iconColor: "text-orange-500",
|
||||
dimensions: ["舆情分析", "社情分析", "民情分析", "风险提示"],
|
||||
},
|
||||
{
|
||||
id: "event",
|
||||
name: "事件分析",
|
||||
description: "围绕某个事件进行整体分析",
|
||||
category: "event",
|
||||
categoryName: "事件分析类",
|
||||
input: "事件名称/事件材料/事件链接",
|
||||
output: "事件脉络、关键节点、风险判断",
|
||||
icon: Target,
|
||||
color: "red",
|
||||
bgColor: "bg-red-500/10",
|
||||
borderColor: "border-red-500/30",
|
||||
iconColor: "text-red-500",
|
||||
dimensions: ["事件脉络", "关键节点", "风险判断", "初步建议"],
|
||||
},
|
||||
// 专题分析类
|
||||
{
|
||||
id: "topic",
|
||||
name: "专题分析",
|
||||
description: "对某专题做集中分析",
|
||||
category: "topic",
|
||||
categoryName: "专题分析类",
|
||||
input: "专题名称/专题材料/文本集合",
|
||||
output: "专题结构、核心问题、观点聚合、结论建议",
|
||||
icon: BookOpen,
|
||||
color: "green",
|
||||
bgColor: "bg-green-500/10",
|
||||
borderColor: "border-green-500/30",
|
||||
iconColor: "text-green-500",
|
||||
dimensions: ["专题结构", "核心问题", "观点聚合", "结论建议"],
|
||||
},
|
||||
];
|
||||
|
||||
const categoryConfig = {
|
||||
info: {
|
||||
title: "内部应用",
|
||||
hoverColor: "group-hover:text-blue-500", // 内部应用 hover 蓝色
|
||||
btnTheme: {
|
||||
dark: "bg-blue-500/20 text-blue-400 hover:bg-blue-500/30 border-blue-500/30",
|
||||
light: "bg-blue-50 text-blue-600 hover:bg-blue-100 border-blue-200",
|
||||
},
|
||||
},
|
||||
account: {
|
||||
title: "外部应用",
|
||||
hoverColor: "group-hover:text-purple-500", // 外部应用 hover 紫色
|
||||
btnTheme: {
|
||||
dark: "bg-purple-500/20 text-purple-400 hover:bg-purple-500/30 border-purple-500/30",
|
||||
light: "bg-red-50 text-red-600 hover:bg-red-100 border-red-200", // 保留你原本外部应用的红色风格
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const categoryOrder: AppCategory[] = ["info", "account", "event", "topic"];
|
||||
const zncmUrl = import.meta.env.VITE_API_BASE_URL || "";
|
||||
|
||||
const LightAppSquare: React.FC<LightAppSquareProps> = ({ onNavigate }) => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === "dark";
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [categoryFilter, setCategoryFilter] = useState<AppCategory | "all">(
|
||||
"all"
|
||||
);
|
||||
const [recentApps, setRecentApps] = useState<LightAppType[]>([
|
||||
"sentiment",
|
||||
"event",
|
||||
"account-pg",
|
||||
]);
|
||||
const [historyApps, setHistoryApps] = useState([]);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [editDataShow, setEditDataShow] = useState(false);
|
||||
const [analysisData, setAnalysisData] = useState<analysisDataItem>({
|
||||
appName: "",
|
||||
appDesc: "",
|
||||
appIcon: "",
|
||||
jumpUrl: "",
|
||||
appType: 2,
|
||||
});
|
||||
const [analysisList, setAnalysisList] = useState<analysisListData[]>([
|
||||
{ key: "info", list: [] },
|
||||
{ key: "account", list: [] },
|
||||
]);
|
||||
const [fileFail, setFileFail] = useState<UploadFile[]>([
|
||||
// 如果需要默认回显图片,可以在这里初始化
|
||||
// { name: '默认图片.png', url: 'https://tdesign.gtimg.com/site/avatar.jpg', status: 'fail' }
|
||||
]);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [pageNum, setPageNum] = useState<number>(1);
|
||||
const [pageSize, setPageSize] = useState<number>(10); // 每页显示 10 条
|
||||
const [total, setTotal] = useState<number>(0);
|
||||
const state = useRef({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
});
|
||||
const showImageFileName = true;
|
||||
const service = requestCrudService;
|
||||
// 解构出 Group 和 Button,方便使用
|
||||
const { Group: RadioGroup, Button: RadioButton } = Radio;
|
||||
// 用于格式化接口返回的数据,使其符合 TDesign 的要求
|
||||
const formatResponse = (res: any) => {
|
||||
// 假设接口返回 { code: 0, data: { url: '...' } }
|
||||
// 如果接口报错,可以在这里处理,让组件显示失败状态
|
||||
if (res.code !== 0) {
|
||||
return {
|
||||
error: res.message || "上传失败",
|
||||
url: "",
|
||||
name: "",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
url: res.data.url,
|
||||
name: res.data.name,
|
||||
};
|
||||
};
|
||||
|
||||
const handleChange: UploadProps["onChange"] = (files) => {
|
||||
setFileFail(files);
|
||||
console.log("当前文件列表:", files);
|
||||
};
|
||||
|
||||
// 过滤应用
|
||||
const filteredApps = useMemo(() => {
|
||||
return lightApps.filter((app) => {
|
||||
const matchesSearch =
|
||||
searchQuery === "" ||
|
||||
app.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
app.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
app.input.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
const matchesCategory =
|
||||
categoryFilter === "all" || app.category === categoryFilter;
|
||||
return matchesSearch && matchesCategory;
|
||||
});
|
||||
}, [searchQuery, categoryFilter]);
|
||||
|
||||
// 按分类分组
|
||||
const groupedApps = useMemo(() => {
|
||||
const groups: Record<AppCategory, LightApp[]> = {
|
||||
info: [],
|
||||
account: [],
|
||||
event: [],
|
||||
topic: [],
|
||||
};
|
||||
filteredApps.forEach((app) => {
|
||||
groups[app.category].push(app);
|
||||
});
|
||||
return groups;
|
||||
}, [filteredApps]);
|
||||
|
||||
// 处理应用启动
|
||||
const handleLaunchApp = (app: LightApp) => {
|
||||
console.log("启动应用:", app);
|
||||
|
||||
// 更新最近使用
|
||||
setRecentApps((prev) => {
|
||||
const filtered = prev.filter((id) => id !== app.id);
|
||||
return [app.id, ...filtered].slice(0, 5);
|
||||
});
|
||||
onNavigate("light-app-run", app.id);
|
||||
};
|
||||
|
||||
// 获取分类颜色
|
||||
const getCategoryColor = (category: AppCategory) => {
|
||||
const colors: Record<AppCategory, { text: string; bg: string }> = {
|
||||
info: {
|
||||
text: isDark ? "text-blue-400" : "text-blue-600",
|
||||
bg: isDark ? "bg-blue-500/20" : "bg-blue-50",
|
||||
},
|
||||
account: {
|
||||
text: isDark ? "text-purple-400" : "text-purple-600",
|
||||
bg: isDark ? "bg-purple-500/20" : "bg-purple-50",
|
||||
},
|
||||
event: {
|
||||
text: isDark ? "text-orange-400" : "text-orange-600",
|
||||
bg: isDark ? "bg-orange-500/20" : "bg-orange-50",
|
||||
},
|
||||
topic: {
|
||||
text: isDark ? "text-green-400" : "text-green-600",
|
||||
bg: isDark ? "bg-green-500/20" : "bg-green-50",
|
||||
},
|
||||
};
|
||||
return colors[category];
|
||||
};
|
||||
|
||||
// 获取应用列表
|
||||
const analysisListSearch = async () => {
|
||||
const parmy = {
|
||||
appName: searchQuery,
|
||||
pageNum: state.current.pageNum,
|
||||
pageSize: state.current.pageSize,
|
||||
};
|
||||
setLoading(true);
|
||||
const res = await service.post(`${zncmUrl}/app/analysis/list`, parmy);
|
||||
console.log(res);
|
||||
setLoading(false);
|
||||
const result = res.data;
|
||||
const listData: analysisListData[] = [
|
||||
{ key: "info", list: [] },
|
||||
{ key: "account", list: [] },
|
||||
];
|
||||
result.records.map((item: analysisListItem) => {
|
||||
if (item.appType === 1) {
|
||||
item.category = "info";
|
||||
item.categoryName = "内部应用";
|
||||
listData[0].list.push(item);
|
||||
} else if (item.appType === 2) {
|
||||
item.category = "account";
|
||||
item.categoryName = "外部应用";
|
||||
listData[1].list.push(item);
|
||||
}
|
||||
setAnalysisList(listData);
|
||||
});
|
||||
setTotal(result.total);
|
||||
};
|
||||
|
||||
// 添加应用
|
||||
const analysieAddListChange = async () => {
|
||||
const res = await service.post(`${zncmUrl}/app/analysis/add`, analysisData);
|
||||
console.log(res);
|
||||
MessagePlugin.success("添加成功");
|
||||
await analysisListSearch();
|
||||
};
|
||||
|
||||
// 回车处理
|
||||
const handleKeyDown = async (e) => {
|
||||
if ((e.metaKey || e.ctrlKey || e.shiftKey) && e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
} else if (e.keyCode === 13) {
|
||||
e.preventDefault();
|
||||
await analysisListSearch();
|
||||
} else if (e.key === "/") {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
// 添加应用
|
||||
const showDialog = () => {
|
||||
setAnalysisData({
|
||||
appName: "",
|
||||
appDesc: "",
|
||||
appIcon: "",
|
||||
jumpUrl: "",
|
||||
appType: 2,
|
||||
});
|
||||
setFileFail([]);
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
const handleOk = async () => {
|
||||
await analysieAddListChange();
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
// 编辑应用
|
||||
const editShowChange = (item: analysisDataItem) => {
|
||||
setAnalysisData(item);
|
||||
if (item.appIcon) {
|
||||
setFileFail([
|
||||
{ name: "默认图片.png", url: item.appIcon, status: "fail" },
|
||||
]);
|
||||
}
|
||||
setEditDataShow(true);
|
||||
};
|
||||
|
||||
const handleOkEdit = () => {
|
||||
editDataChange();
|
||||
setEditDataShow(false);
|
||||
};
|
||||
|
||||
const handleCloseEdit = () => {
|
||||
setEditDataShow(false);
|
||||
};
|
||||
|
||||
const getBase64 = (file: File): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = () => resolve(reader.result as string);
|
||||
reader.onerror = (error) => reject(error);
|
||||
});
|
||||
};
|
||||
|
||||
const imgUpload: UploadProps["requestMethod"] = async (file: any) => {
|
||||
console.log("file", file);
|
||||
const base64 = await getBase64(file.raw);
|
||||
handleDataChange("appIcon", base64);
|
||||
return {
|
||||
status: "success",
|
||||
// response 对象里的数据会传给 formatResponse
|
||||
response: {
|
||||
url: "",
|
||||
name: "",
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// 通用的处理函数
|
||||
const handleDataChange = (
|
||||
key: keyof typeof analysisData,
|
||||
value: string | number
|
||||
) => {
|
||||
setAnalysisData((prev) => ({
|
||||
...prev,
|
||||
[key]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
// 删除应用
|
||||
const delDataChange = async (item: analysisListItem) => {
|
||||
console.log("删除", item);
|
||||
setLoading(true);
|
||||
const res = await service.get(`${zncmUrl}/app/analysis/delete/${item.id}`);
|
||||
console.log(res);
|
||||
MessagePlugin.success("操作成功");
|
||||
await analysisListSearch();
|
||||
};
|
||||
const getHistoryApps = async () => {
|
||||
const reqParam = {
|
||||
limit: 5,
|
||||
};
|
||||
const res = await service.get(`${zncmUrl}/app/history/recent`, reqParam);
|
||||
if (res && res.data) {
|
||||
console.log({ resData: res.data });
|
||||
setHistoryApps(res.data);
|
||||
}
|
||||
};
|
||||
// 编辑应用
|
||||
const editDataChange = async () => {
|
||||
setLoading(true);
|
||||
const res = await service.post(
|
||||
`${zncmUrl}/app/analysis/update`,
|
||||
analysisData
|
||||
);
|
||||
console.log(res);
|
||||
MessagePlugin.success("操作成功");
|
||||
await analysisListSearch();
|
||||
};
|
||||
|
||||
const pageIndexChange = async (value: number) => {
|
||||
state.current.pageNum = value;
|
||||
setPageNum(value);
|
||||
await analysisListSearch();
|
||||
};
|
||||
|
||||
const pageSizeChange = async (value: number) => {
|
||||
state.current.pageSize = value;
|
||||
setPageSize(value);
|
||||
await analysisListSearch();
|
||||
};
|
||||
|
||||
// 发送请求
|
||||
useEffect(() => {
|
||||
analysisListSearch();
|
||||
getHistoryApps();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
//获取最近使用应用列表
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* 顶部工具栏 */}
|
||||
<div
|
||||
className={`rounded-xl border p-4 ${
|
||||
isDark
|
||||
? "bg-slate-800/50 border-slate-700"
|
||||
: "bg-white border-gray-200"
|
||||
}`}
|
||||
>
|
||||
<div className="flex flex-col lg:flex-row lg:items-center gap-4">
|
||||
{/* 面包屑和标题 */}
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<Layout
|
||||
className={`w-5 h-5 ${isDark ? "text-blue-400" : "text-red-500"}`}
|
||||
/>
|
||||
<span
|
||||
className={`text-sm ${
|
||||
isDark ? "text-slate-400" : "text-gray-500"
|
||||
}`}
|
||||
>
|
||||
轻应用中心 /
|
||||
</span>
|
||||
<span
|
||||
className={`text-sm font-medium ${
|
||||
isDark ? "text-white" : "text-gray-900"
|
||||
}`}
|
||||
>
|
||||
应用广场
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 搜索框 */}
|
||||
<div className="flex-1 max-w-md">
|
||||
<div className="relative">
|
||||
<Search
|
||||
className={`absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 ${
|
||||
isDark ? "text-slate-500" : "text-gray-400"
|
||||
}`}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={(e) => handleKeyDown(e)}
|
||||
placeholder="搜索应用名称或分析对象..."
|
||||
className={`w-full pl-10 pr-4 py-2 rounded-lg border 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 focus:ring-blue-500/20"
|
||||
: "bg-gray-50 border-gray-300 text-gray-900 placeholder-gray-400 focus:border-red-500 focus:ring-red-500/20"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 筛选和操作 */}
|
||||
<div className="flex items-center gap-3">
|
||||
{/* <select
|
||||
value={categoryFilter}
|
||||
onChange={(e) => setCategoryFilter(e.target.value as AppCategory | 'all')}
|
||||
className={`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-700 focus:border-red-500'
|
||||
}`}
|
||||
>
|
||||
<option value="all">全部分类</option>
|
||||
<option value="info">信息分析类</option>
|
||||
<option value="account">账号分析类</option>
|
||||
<option value="event">事件分析类</option>
|
||||
<option value="topic">专题分析类</option>
|
||||
</select> */}
|
||||
<button
|
||||
onClick={() => onNavigate("light-app-records")}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium flex items-center gap-2 transition-colors ${
|
||||
isDark
|
||||
? "bg-slate-700 text-slate-300 hover:bg-slate-600"
|
||||
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
|
||||
}`}
|
||||
>
|
||||
<Clock className="w-4 h-4" />
|
||||
运行记录
|
||||
</button>
|
||||
<Button theme="primary" onClick={showDialog}>
|
||||
<span className="flex items-center">
|
||||
<Plus className="w-4 h-4 mr-[10px]" />
|
||||
添加
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 最近使用 */}
|
||||
{historyApps.length > 0 &&
|
||||
searchQuery === "" &&
|
||||
categoryFilter === "all" && (
|
||||
<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 gap-2 mb-4">
|
||||
<Clock
|
||||
className={`w-4 h-4 ${
|
||||
isDark ? "text-blue-400" : "text-red-500"
|
||||
}`}
|
||||
/>
|
||||
<h3
|
||||
className={`text-sm font-medium ${
|
||||
isDark ? "text-slate-300" : "text-gray-700"
|
||||
}`}
|
||||
>
|
||||
最近使用
|
||||
</h3>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{historyApps.map((app) => {
|
||||
// const app = historyApps.find((a) => a.id === appId);
|
||||
// if (!app) return null;
|
||||
// const Icon = app.icon;
|
||||
return (
|
||||
<button
|
||||
// key={app.id}
|
||||
// onClick={() => handleLaunchApp(app)}
|
||||
className={`px-4 py-2 rounded-lg border text-sm font-medium flex items-center gap-2 transition-all hover:scale-105 ${
|
||||
isDark
|
||||
? `bg-slate-700/50 border-slate-600 text-slate-300 hover:bg-slate-700`
|
||||
: `bg-gray-50 border-gray-200 text-gray-700 hover:bg-gray-100`
|
||||
}`}
|
||||
>
|
||||
{/* <Icon className={`w-4 h-4 ${app.iconColor}`} /> */}
|
||||
{app.appName}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 应用分组区 */}
|
||||
<Loading loading={loading} text="正在加载中...">
|
||||
<div className="space-y-6">
|
||||
{analysisList.map((section) => {
|
||||
const config = categoryConfig[section.key];
|
||||
const colorStyle = getCategoryColor(section.key);
|
||||
|
||||
return (
|
||||
<div key={section.key} className="space-y-4">
|
||||
{/* 标题区域 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<h2
|
||||
className={`text-lg font-semibold ${
|
||||
isDark ? "text-white" : "text-gray-900"
|
||||
}`}
|
||||
>
|
||||
{config.title}
|
||||
</h2>
|
||||
<span
|
||||
className={`px-2 py-0.5 rounded text-xs font-medium ${colorStyle.bg} ${colorStyle.text}`}
|
||||
>
|
||||
{section.list.length} 个应用
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 卡片网格 */}
|
||||
{section.list.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{section.list.map((app) => (
|
||||
<AppCard
|
||||
key={app.id}
|
||||
app={app}
|
||||
isDark={isDark}
|
||||
hoverColor={config.hoverColor}
|
||||
btnClass={
|
||||
isDark ? config.btnTheme.dark : config.btnTheme.light
|
||||
}
|
||||
delDataChange={delDataChange}
|
||||
editShowChange={editShowChange}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={`text-center py-10 text-sm ${
|
||||
isDark ? "text-slate-500" : "text-gray-400"
|
||||
}`}
|
||||
>
|
||||
<Empty description="暂无数据" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Loading>
|
||||
<div className="p-4 text-center">
|
||||
<Pagination
|
||||
total={total}
|
||||
current={pageNum}
|
||||
pageSize={pageSize}
|
||||
onCurrentChange={(page) => pageIndexChange(Number(page))}
|
||||
onPageSizeChange={(size) => pageSizeChange(Number(size))} // 每页数量变化时的回调
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 底部提示 */}
|
||||
<div
|
||||
className={`p-4 rounded-xl border ${
|
||||
isDark
|
||||
? "bg-blue-500/10 border-blue-500/30"
|
||||
: "bg-blue-50 border-blue-200"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<Sparkles
|
||||
className={`w-5 h-5 mt-0.5 ${
|
||||
isDark ? "text-blue-400" : "text-blue-500"
|
||||
}`}
|
||||
/>
|
||||
<div>
|
||||
<p
|
||||
className={`text-sm font-medium ${
|
||||
isDark ? "text-blue-300" : "text-blue-800"
|
||||
}`}
|
||||
>
|
||||
关于轻应用中心
|
||||
</p>
|
||||
<p
|
||||
className={`mt-1 text-sm ${
|
||||
isDark ? "text-blue-400/80" : "text-blue-700"
|
||||
}`}
|
||||
>
|
||||
轻应用中心提供了一系列快速分析工具,适合处理临时性的分析需求。相比完整的任务级应用,轻应用更加轻量便捷,可以快速调用多种工具完成分析工作。运行结果可保存快照、导出或转为正式任务。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog
|
||||
header="添加应用"
|
||||
visible={visible}
|
||||
onClose={handleClose} // 监听关闭事件
|
||||
onConfirm={handleOk} // 点击确定按钮
|
||||
onCancel={handleClose} // 点击取消按钮
|
||||
closeOnOverlayClick={true} // 点击遮罩层是否关闭
|
||||
>
|
||||
<div className="mb-[10px]">
|
||||
<Upload
|
||||
// 对应 v-model
|
||||
files={fileFail}
|
||||
onChange={handleChange}
|
||||
// 对应 theme="image"
|
||||
theme="image"
|
||||
requestMethod={imgUpload}
|
||||
// 对应 accept
|
||||
accept="image/*"
|
||||
tips="系统logo"
|
||||
// 对应 :show-image-file-name
|
||||
showImageFileName={showImageFileName}
|
||||
// 对应 :format-response
|
||||
formatResponse={formatResponse}
|
||||
// 可选:限制上传数量(演示单张)
|
||||
multiple={false}
|
||||
// 可选:上传失败时的提示(如果接口直接返回 4xx/5xx)
|
||||
onFail={({ e, file }) => {
|
||||
MessagePlugin.error(`文件 ${file.name} 上传失败`);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-[10px] overflow-hidden">
|
||||
<Row>
|
||||
<Col span={2}>
|
||||
<div>名称:</div>
|
||||
</Col>
|
||||
<Col span={9}>
|
||||
<Input
|
||||
placeholder="请输入应用名称"
|
||||
value={analysisData.appName}
|
||||
onChange={(val) => handleDataChange("appName", val)}
|
||||
clearable
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
<div className="mb-[10px] overflow-hidden">
|
||||
<Row>
|
||||
<Col span={2}>
|
||||
<div>详情:</div>
|
||||
</Col>
|
||||
<Col span={9}>
|
||||
<Textarea
|
||||
placeholder="请输入应用详情"
|
||||
value={analysisData.appDesc}
|
||||
onChange={(val) => handleDataChange("appDesc", val)}
|
||||
autosize
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
<div className="mb-[10px] overflow-hidden">
|
||||
<Row>
|
||||
<Col span={2}>
|
||||
<div>类型:</div>
|
||||
</Col>
|
||||
<Col span={9}>
|
||||
<RadioGroup
|
||||
variant="primary-filled" // 对应 variant="primary-filled"
|
||||
value={analysisData.appType} // 绑定状态值
|
||||
onChange={(value) => handleDataChange("appType", Number(value))}
|
||||
>
|
||||
{/* 3. 使用 RadioButton */}
|
||||
<RadioButton value={1}>内部应用</RadioButton>
|
||||
<RadioButton value={2}>外部应用</RadioButton>
|
||||
<RadioButton value={3}>第三方集成</RadioButton>
|
||||
</RadioGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
<div className="mb-[10px] overflow-hidden">
|
||||
<Row>
|
||||
<Col span={2}>
|
||||
<div>链接:</div>
|
||||
</Col>
|
||||
<Col span={9}>
|
||||
<Input
|
||||
placeholder="请输入链接"
|
||||
value={analysisData.jumpUrl}
|
||||
onChange={(val) => handleDataChange("jumpUrl", val)}
|
||||
clearable
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</Dialog>
|
||||
<Dialog
|
||||
header="编辑应用"
|
||||
visible={editDataShow}
|
||||
onClose={handleCloseEdit} // 监听关闭事件
|
||||
onConfirm={handleOkEdit} // 点击确定按钮
|
||||
onCancel={handleCloseEdit} // 点击取消按钮
|
||||
closeOnOverlayClick={true} // 点击遮罩层是否关闭
|
||||
>
|
||||
<div className="mb-[10px]">
|
||||
{/* <Upload
|
||||
// 对应 v-model
|
||||
files={fileFail}
|
||||
onChange={handleChange}
|
||||
// 对应 theme="image"
|
||||
theme="image"
|
||||
requestMethod={imgUpload}
|
||||
// 对应 accept
|
||||
accept="image/*"
|
||||
tips="系统logo"
|
||||
// 对应 :show-image-file-name
|
||||
showImageFileName={showImageFileName}
|
||||
// 对应 :format-response
|
||||
formatResponse={formatResponse}
|
||||
// 可选:限制上传数量(演示单张)
|
||||
multiple={false}
|
||||
// 可选:上传失败时的提示(如果接口直接返回 4xx/5xx)
|
||||
onFail={({ e, file }) => {
|
||||
MessagePlugin.error(`文件 ${file.name} 上传失败`);
|
||||
}} /> */}
|
||||
</div>
|
||||
<div className="mb-[10px] overflow-hidden">
|
||||
<Row>
|
||||
<Col span={2}>
|
||||
<div>名称:</div>
|
||||
</Col>
|
||||
<Col span={9}>
|
||||
<Input
|
||||
placeholder="请输入应用名称"
|
||||
value={analysisData.appName}
|
||||
onChange={(val) => handleDataChange("appName", val)}
|
||||
clearable
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
<div className="mb-[10px] overflow-hidden">
|
||||
<Row>
|
||||
<Col span={2}>
|
||||
<div>详情:</div>
|
||||
</Col>
|
||||
<Col span={9}>
|
||||
<Textarea
|
||||
placeholder="请输入应用详情"
|
||||
value={analysisData.appDesc}
|
||||
onChange={(val) => handleDataChange("appDesc", val)}
|
||||
autosize
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
<div className="mb-[10px] overflow-hidden">
|
||||
<Row>
|
||||
<Col span={2}>
|
||||
<div>类型:</div>
|
||||
</Col>
|
||||
<Col span={9}>
|
||||
<RadioGroup
|
||||
variant="primary-filled" // 对应 variant="primary-filled"
|
||||
value={analysisData.appType} // 绑定状态值
|
||||
onChange={(value) => handleDataChange("appType", Number(value))}
|
||||
>
|
||||
{/* 3. 使用 RadioButton */}
|
||||
<RadioButton value={1}>内部应用</RadioButton>
|
||||
<RadioButton value={2}>外部应用</RadioButton>
|
||||
<RadioButton value={3}>第三方集成</RadioButton>
|
||||
</RadioGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
<div className="mb-[10px] overflow-hidden">
|
||||
<Row>
|
||||
<Col span={2}>
|
||||
<div>链接:</div>
|
||||
</Col>
|
||||
<Col span={9}>
|
||||
<Input
|
||||
placeholder="请输入链接"
|
||||
value={analysisData.jumpUrl}
|
||||
onChange={(val) => handleDataChange("jumpUrl", val)}
|
||||
clearable
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LightAppSquare;
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Outlet, useNavigate } from 'react-router-dom';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
const zncmUrl = import.meta.env.VITE_AIPI_CONSUMER_URL || '';
|
||||
import { requestCrudService } from '@/service/request-axios-service';
|
||||
const service = requestCrudService;
|
||||
const Login: React.FC = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const yUserId = searchParams.get('yUserId');
|
||||
const toolToken = searchParams.get('token');
|
||||
if (toolToken) {
|
||||
localStorage.setItem('tools_token', toolToken);
|
||||
}
|
||||
const loginSearch = async () => {
|
||||
const param = {
|
||||
yuserId: yUserId
|
||||
}
|
||||
const res: any = await service.get(`${zncmUrl}/cop/userDetailByYuserId`, param);
|
||||
console.log('res yUserId', res , yUserId)
|
||||
const result = res.data;
|
||||
localStorage.setItem('userId', result.id)
|
||||
localStorage.setItem('userInfo', JSON.stringify(result));
|
||||
navigate('/qa-center/qa-from');
|
||||
}
|
||||
useEffect(() => {
|
||||
console.log('登录信息',searchParams.get('yUserId'));
|
||||
loginSearch();
|
||||
}, []);
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
登录中...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default Login
|
||||
@@ -0,0 +1,44 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTheme } from "../contexts/ThemeContext";
|
||||
import { requestCrudService } from "@/service/request-axios-service";
|
||||
import "./css/KnowledgeBaseResourceManagement.css";
|
||||
|
||||
import { PageType } from "../components/Sidebar";
|
||||
|
||||
interface NetworkActorResourceManagementProps {
|
||||
onNavigate: (page: PageType) => void;
|
||||
}
|
||||
|
||||
const NetworkActorResourceManagement: React.FC<
|
||||
NetworkActorResourceManagementProps
|
||||
> = ({ onNavigate }) => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === "dark";
|
||||
const [iframeUrl, setIframeUrl] = useState("");
|
||||
const b2SearchUrl = import.meta.env.VITE_AIPI_B2ACTOR_URL;
|
||||
|
||||
// const service = requestCrudService;
|
||||
useEffect(() => {
|
||||
initIframeUrl();
|
||||
}, []);
|
||||
const initIframeUrl = async () => {
|
||||
// const url = `${knowledgeURL}/#/knowledge/knowledge-search?isIframe=true`;
|
||||
|
||||
const jsonStr = localStorage.getItem("userInfo");
|
||||
console.log("jsonStr", jsonStr);
|
||||
// if (jsonStr) {
|
||||
// const userInfo = JSON.parse(jsonStr);
|
||||
// console.log("userInfo", userInfo);
|
||||
// }
|
||||
console.log({ iframeUrl: `${b2SearchUrl}/fromB/toXWT?isFrame=1` });
|
||||
setIframeUrl(`${b2SearchUrl}/fromB/toXWT?isFrame=1`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="iframe-container">
|
||||
<iframe src={iframeUrl} className="full-size-iframe"></iframe>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NetworkActorResourceManagement;
|
||||
@@ -0,0 +1,810 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import {
|
||||
Activity,
|
||||
AlertTriangle,
|
||||
Clock,
|
||||
Filter,
|
||||
RefreshCw,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
X,
|
||||
Link2,
|
||||
ArrowRight,
|
||||
Trash2,
|
||||
} from 'lucide-react';
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// Types //
|
||||
// ------------------------------------------------------------------ //
|
||||
|
||||
interface CallTrace {
|
||||
id: string;
|
||||
trace_id?: string;
|
||||
task_id?: string;
|
||||
parent_call_id?: string;
|
||||
call_type: 'model' | 'mcp' | 'skill';
|
||||
status: 'success' | 'failed' | 'cancelled' | 'timeout';
|
||||
started_at: number;
|
||||
ended_at?: number;
|
||||
duration_ms: number;
|
||||
name: string;
|
||||
action: string;
|
||||
mcp_key?: string;
|
||||
mcp_name?: string;
|
||||
skill_key?: string;
|
||||
skill_name?: string;
|
||||
model?: string;
|
||||
input_summary?: string;
|
||||
output_summary?: string;
|
||||
error_id?: string;
|
||||
error_message?: string;
|
||||
meta?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface ErrorLog {
|
||||
id: string;
|
||||
trace_id?: string;
|
||||
task_id?: string;
|
||||
call_id?: string;
|
||||
source: string;
|
||||
level: 'warning' | 'error';
|
||||
happened_at: number;
|
||||
message: string;
|
||||
recoverable: boolean;
|
||||
mcp_key?: string;
|
||||
mcp_name?: string;
|
||||
skill_key?: string;
|
||||
skill_name?: string;
|
||||
model?: string;
|
||||
action?: string;
|
||||
input_summary?: string;
|
||||
params_summary?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface PagedResponse<T> {
|
||||
items: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
}
|
||||
|
||||
interface Options {
|
||||
models: string[];
|
||||
mcps: { key: string; name: string }[];
|
||||
skills: { key: string; name: string }[];
|
||||
statuses: string[];
|
||||
sources: string[];
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// Helpers //
|
||||
// ------------------------------------------------------------------ //
|
||||
|
||||
function formatTs(ms: number): string {
|
||||
return new Date(ms).toLocaleString('zh-CN', { hour12: false });
|
||||
}
|
||||
|
||||
function formatDuration(ms: number): string {
|
||||
if (ms < 1000) return `${ms}ms`;
|
||||
return `${(ms / 1000).toFixed(2)}s`;
|
||||
}
|
||||
|
||||
const STATUS_COLORS: Record<string, string> = {
|
||||
success: 'bg-green-500/20 text-green-400',
|
||||
failed: 'bg-red-500/20 text-red-400',
|
||||
timeout: 'bg-yellow-500/20 text-yellow-400',
|
||||
cancelled: 'bg-slate-500/20 text-slate-400',
|
||||
warning: 'bg-yellow-500/20 text-yellow-400',
|
||||
error: 'bg-red-500/20 text-red-400',
|
||||
};
|
||||
|
||||
const TYPE_COLORS: Record<string, string> = {
|
||||
model: 'bg-blue-500/20 text-blue-400',
|
||||
mcp: 'bg-purple-500/20 text-purple-400',
|
||||
skill: 'bg-cyan-500/20 text-cyan-400',
|
||||
};
|
||||
|
||||
const SOURCE_COLORS: Record<string, string> = {
|
||||
model: 'bg-blue-500/20 text-blue-400',
|
||||
llm: 'bg-blue-500/20 text-blue-400',
|
||||
mcp: 'bg-purple-500/20 text-purple-400',
|
||||
skill: 'bg-cyan-500/20 text-cyan-400',
|
||||
orchestrator: 'bg-orange-500/20 text-orange-400',
|
||||
network: 'bg-pink-500/20 text-pink-400',
|
||||
};
|
||||
|
||||
const backUrl = 'https://ch1.b.uat.4.cn/zncmback'
|
||||
|
||||
function Badge({ label, colorClass }: { label: string; colorClass: string }) {
|
||||
return (
|
||||
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${colorClass}`}>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// API //
|
||||
// ------------------------------------------------------------------ //
|
||||
|
||||
async function fetchOptions(): Promise<Options> {
|
||||
const res = await fetch(`${backUrl}/api/observability/options`);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function fetchCalls(params: Record<string, string | number | undefined>): Promise<PagedResponse<CallTrace>> {
|
||||
const qs = new URLSearchParams();
|
||||
Object.entries(params).forEach(([k, v]) => {
|
||||
if (v !== undefined && v !== null && v !== '') qs.append(k, String(v));
|
||||
});
|
||||
const res = await fetch(`${backUrl}/api/observability/calls?${qs}`);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function fetchErrors(params: Record<string, string | number | undefined>): Promise<PagedResponse<ErrorLog>> {
|
||||
const qs = new URLSearchParams();
|
||||
Object.entries(params).forEach(([k, v]) => {
|
||||
if (v !== undefined && v !== null && v !== '') qs.append(k, String(v));
|
||||
});
|
||||
const res = await fetch(`${backUrl}/api/observability/errors?${qs}`);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// Shared UI atoms //
|
||||
// ------------------------------------------------------------------ //
|
||||
|
||||
function Select({
|
||||
value, onChange, options, placeholder, cls,
|
||||
}: {
|
||||
value: string; onChange: (v: string) => void;
|
||||
options: { value: string; label: string }[]; placeholder?: string; cls?: string;
|
||||
}) {
|
||||
return (
|
||||
<select value={value} onChange={(e) => onChange(e.target.value)}
|
||||
className={`text-sm rounded-lg border px-3 py-1.5 outline-none ${cls ?? ''}`}>
|
||||
{placeholder && <option value="">{placeholder}</option>}
|
||||
{options.map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
function Pagination({ page, pageSize, total, onPage, isDark }: {
|
||||
page: number; pageSize: number; total: number; onPage: (p: number) => void; isDark: boolean;
|
||||
}) {
|
||||
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
||||
const textMuted = isDark ? 'text-slate-400' : 'text-gray-500';
|
||||
const btn = `p-1.5 rounded-lg transition-all disabled:opacity-40 ${isDark ? 'text-slate-400 hover:text-white hover:bg-slate-700' : 'text-gray-500 hover:text-gray-900 hover:bg-gray-100'}`;
|
||||
return (
|
||||
<div className={`flex items-center gap-3 text-sm ${textMuted}`}>
|
||||
<span>共 {total} 条</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<button className={btn} disabled={page <= 1} onClick={() => onPage(page - 1)}><ChevronLeft className="w-4 h-4" /></button>
|
||||
<span className="px-2">{page} / {totalPages}</span>
|
||||
<button className={btn} disabled={page >= totalPages} onClick={() => onPage(page + 1)}><ChevronRight className="w-4 h-4" /></button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// Linked items — shown inside detail modals //
|
||||
// ------------------------------------------------------------------ //
|
||||
|
||||
function LinkedErrors({ callId, traceId, isDark }: { callId: string; traceId?: string; isDark: boolean }) {
|
||||
const [items, setItems] = useState<ErrorLog[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetchErrors({ call_id: callId, page: 1, page_size: 10 })
|
||||
.then((r) => setItems(r.items))
|
||||
.catch(() => setItems([]))
|
||||
.finally(() => setLoading(false));
|
||||
}, [callId]);
|
||||
|
||||
const label = isDark ? 'text-slate-400' : 'text-gray-500';
|
||||
const rowBg = isDark ? 'bg-red-900/20 border-red-800/30' : 'bg-red-50 border-red-200';
|
||||
|
||||
if (loading) return <p className={`text-xs ${label}`}>加载相关错误…</p>;
|
||||
if (items.length === 0) return <p className={`text-xs ${label}`}>无相关错误日志</p>;
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
{items.map((e) => (
|
||||
<div key={e.id} className={`rounded-lg border p-2.5 ${rowBg}`}>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Badge label={e.level} colorClass={STATUS_COLORS[e.level] ?? ''} />
|
||||
<Badge label={e.source} colorClass={SOURCE_COLORS[e.source] ?? ''} />
|
||||
<span className={`text-xs ${label}`}>{formatTs(e.happened_at)}</span>
|
||||
</div>
|
||||
<p className={`text-sm mt-1 ${isDark ? 'text-slate-200' : 'text-gray-800'}`}>{e.message}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LinkedCall({ callId, isDark }: { callId: string; isDark: boolean }) {
|
||||
const [item, setItem] = useState<CallTrace | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// call_id in error = the id of the CallTrace record
|
||||
fetchCalls({ trace_id: callId, page: 1, page_size: 1 })
|
||||
.then((r) => setItem(r.items[0] ?? null))
|
||||
.catch(() => setItem(null))
|
||||
.finally(() => setLoading(false));
|
||||
}, [callId]);
|
||||
|
||||
const label = isDark ? 'text-slate-400' : 'text-gray-500';
|
||||
const rowBg = isDark ? 'bg-blue-900/20 border-blue-800/30' : 'bg-blue-50 border-blue-200';
|
||||
|
||||
if (loading) return <p className={`text-xs ${label}`}>加载关联调用…</p>;
|
||||
if (!item) return <p className={`text-xs ${label}`}>未找到关联调用记录(ID: {callId})</p>;
|
||||
return (
|
||||
<div className={`rounded-lg border p-2.5 ${rowBg}`}>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Badge label={item.call_type} colorClass={TYPE_COLORS[item.call_type] ?? ''} />
|
||||
<Badge label={item.status} colorClass={STATUS_COLORS[item.status] ?? ''} />
|
||||
<span className={`text-xs font-mono ${label}`}>{formatDuration(item.duration_ms)}</span>
|
||||
<span className={`text-xs ${label}`}>{formatTs(item.started_at)}</span>
|
||||
</div>
|
||||
<p className={`text-sm mt-1 font-medium ${isDark ? 'text-slate-200' : 'text-gray-800'}`}>{item.name}</p>
|
||||
<p className={`text-xs mt-0.5 ${label}`}>{item.action}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// Detail Modals //
|
||||
// ------------------------------------------------------------------ //
|
||||
|
||||
function Section({ title, isDark, children }: { title: string; isDark: boolean; children: React.ReactNode }) {
|
||||
const divider = isDark ? 'border-blue-800/20' : 'border-gray-200';
|
||||
const label = isDark ? 'text-slate-400' : 'text-gray-500';
|
||||
return (
|
||||
<div className={`border-t pt-4 mt-4 ${divider}`}>
|
||||
<div className={`flex items-center gap-1.5 mb-2 text-xs font-semibold uppercase tracking-wide ${label}`}>
|
||||
<Link2 className="w-3.5 h-3.5" />
|
||||
{title}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FieldRow({ label, value, isDark }: { label: string; value?: string | null; isDark: boolean }) {
|
||||
if (!value) return null;
|
||||
const rowBg = isDark ? 'bg-slate-800/40' : 'bg-gray-50';
|
||||
const textPrimary = isDark ? 'text-white' : 'text-gray-900';
|
||||
const textSecondary = isDark ? 'text-slate-400' : 'text-gray-500';
|
||||
return (
|
||||
<div className={`rounded-lg p-2.5 ${rowBg}`}>
|
||||
<span className={`text-xs font-medium ${textSecondary}`}>{label}</span>
|
||||
<p className={`text-sm mt-0.5 break-all whitespace-pre-wrap ${textPrimary}`}>{value}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CallDetailModal({ item, onClose, isDark }: { item: CallTrace; onClose: () => void; isDark: boolean }) {
|
||||
const bg = isDark ? 'bg-slate-900 border-blue-800/30' : 'bg-white border-gray-200';
|
||||
const textPrimary = isDark ? 'text-white' : 'text-gray-900';
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" onClick={onClose}>
|
||||
<div className={`relative w-full max-w-2xl max-h-[85vh] overflow-y-auto rounded-xl border shadow-2xl p-6 ${bg}`}
|
||||
onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className={`font-semibold text-base ${textPrimary}`}>调用详情</h3>
|
||||
<button onClick={onClose} className={`p-1.5 rounded-lg ${isDark ? 'hover:bg-slate-700' : 'hover:bg-gray-100'}`}>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<FieldRow label="ID" value={item.id} isDark={isDark} />
|
||||
<FieldRow label="Trace ID" value={item.trace_id} isDark={isDark} />
|
||||
<FieldRow label="Task ID" value={item.task_id} isDark={isDark} />
|
||||
<FieldRow label="Parent Call ID" value={item.parent_call_id} isDark={isDark} />
|
||||
<FieldRow label="类型" value={item.call_type} isDark={isDark} />
|
||||
<FieldRow label="状态" value={item.status} isDark={isDark} />
|
||||
<FieldRow label="名称" value={item.name} isDark={isDark} />
|
||||
<FieldRow label="操作" value={item.action} isDark={isDark} />
|
||||
<FieldRow label="开始时间" value={formatTs(item.started_at)} isDark={isDark} />
|
||||
<FieldRow label="结束时间" value={item.ended_at ? formatTs(item.ended_at) : undefined} isDark={isDark} />
|
||||
<FieldRow label="耗时" value={formatDuration(item.duration_ms)} isDark={isDark} />
|
||||
<FieldRow label="模型" value={item.model} isDark={isDark} />
|
||||
<FieldRow label="MCP Key" value={item.mcp_key} isDark={isDark} />
|
||||
<FieldRow label="MCP 名称" value={item.mcp_name} isDark={isDark} />
|
||||
<FieldRow label="Skill Key" value={item.skill_key} isDark={isDark} />
|
||||
<FieldRow label="Skill 名称" value={item.skill_name} isDark={isDark} />
|
||||
<FieldRow label="输入摘要" value={item.input_summary} isDark={isDark} />
|
||||
<FieldRow label="输出摘要" value={item.output_summary} isDark={isDark} />
|
||||
<FieldRow label="错误信息" value={item.error_message} isDark={isDark} />
|
||||
<FieldRow label="Meta" value={item.meta && Object.keys(item.meta).length ? JSON.stringify(item.meta, null, 2) : undefined} isDark={isDark} />
|
||||
</div>
|
||||
|
||||
<Section title="相关错误日志" isDark={isDark}>
|
||||
<LinkedErrors callId={item.id} traceId={item.trace_id} isDark={isDark} />
|
||||
</Section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ErrorDetailModal({ item, onClose, isDark }: { item: ErrorLog; onClose: () => void; isDark: boolean }) {
|
||||
const bg = isDark ? 'bg-slate-900 border-blue-800/30' : 'bg-white border-gray-200';
|
||||
const textPrimary = isDark ? 'text-white' : 'text-gray-900';
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" onClick={onClose}>
|
||||
<div className={`relative w-full max-w-2xl max-h-[85vh] overflow-y-auto rounded-xl border shadow-2xl p-6 ${bg}`}
|
||||
onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className={`font-semibold text-base ${textPrimary}`}>错误详情</h3>
|
||||
<button onClick={onClose} className={`p-1.5 rounded-lg ${isDark ? 'hover:bg-slate-700' : 'hover:bg-gray-100'}`}>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<FieldRow label="ID" value={item.id} isDark={isDark} />
|
||||
<FieldRow label="Trace ID" value={item.trace_id} isDark={isDark} />
|
||||
<FieldRow label="Task ID" value={item.task_id} isDark={isDark} />
|
||||
<FieldRow label="Call ID" value={item.call_id} isDark={isDark} />
|
||||
<FieldRow label="来源" value={item.source} isDark={isDark} />
|
||||
<FieldRow label="级别" value={item.level} isDark={isDark} />
|
||||
<FieldRow label="发生时间" value={formatTs(item.happened_at)} isDark={isDark} />
|
||||
<FieldRow label="可恢复" value={item.recoverable ? '是' : '否'} isDark={isDark} />
|
||||
<FieldRow label="消息" value={item.message} isDark={isDark} />
|
||||
<FieldRow label="MCP Key" value={item.mcp_key} isDark={isDark} />
|
||||
<FieldRow label="MCP 名称" value={item.mcp_name} isDark={isDark} />
|
||||
<FieldRow label="Skill Key" value={item.skill_key} isDark={isDark} />
|
||||
<FieldRow label="Skill 名称" value={item.skill_name} isDark={isDark} />
|
||||
<FieldRow label="模型" value={item.model} isDark={isDark} />
|
||||
<FieldRow label="操作" value={item.action} isDark={isDark} />
|
||||
<FieldRow label="输入摘要" value={item.input_summary} isDark={isDark} />
|
||||
<FieldRow label="参数摘要" value={item.params_summary && Object.keys(item.params_summary).length ? JSON.stringify(item.params_summary, null, 2) : undefined} isDark={isDark} />
|
||||
</div>
|
||||
|
||||
{item.call_id && (
|
||||
<Section title="关联调用记录" isDark={isDark}>
|
||||
<LinkedCall callId={item.call_id} isDark={isDark} />
|
||||
</Section>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// Task ID pill — clickable, stops row-click propagation //
|
||||
// ------------------------------------------------------------------ //
|
||||
|
||||
function TaskIdPill({
|
||||
taskId, onFilter, isDark,
|
||||
}: { taskId?: string | null; onFilter: (id: string) => void; isDark: boolean }) {
|
||||
if (!taskId) return <span className={isDark ? 'text-slate-600' : 'text-gray-400'}>-</span>;
|
||||
return (
|
||||
<button
|
||||
title="按此 Task ID 筛选"
|
||||
onClick={(e) => { e.stopPropagation(); onFilter(taskId); }}
|
||||
className={`font-mono text-xs px-1.5 py-0.5 rounded transition-all max-w-[100px] truncate block
|
||||
${isDark ? 'text-blue-400 hover:bg-blue-500/20' : 'text-blue-600 hover:bg-blue-50'}`}
|
||||
>
|
||||
{taskId}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// 调用耗时统计 Tab //
|
||||
// ------------------------------------------------------------------ //
|
||||
|
||||
interface CallsTabProps {
|
||||
opts: Options;
|
||||
isDark: boolean;
|
||||
externalTaskId: string;
|
||||
onSwitchToErrors: (taskId: string) => void;
|
||||
}
|
||||
|
||||
function CallsTab({ opts, isDark, externalTaskId, onSwitchToErrors }: CallsTabProps) {
|
||||
const [filters, setFilters] = useState({
|
||||
call_type: '', model: '', mcp_key: '', skill_key: '', status: '',
|
||||
task_id: externalTaskId, sort_by: 'started_at', sort_order: 'desc',
|
||||
});
|
||||
const [page, setPage] = useState(1);
|
||||
const [data, setData] = useState<PagedResponse<CallTrace> | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [fetchError, setFetchError] = useState('');
|
||||
const [selected, setSelected] = useState<CallTrace | null>(null);
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
// sync external task_id changes
|
||||
useEffect(() => {
|
||||
setFilters((prev) => ({ ...prev, task_id: externalTaskId }));
|
||||
}, [externalTaskId]);
|
||||
|
||||
const load = useCallback(async (p: number, f = filters) => {
|
||||
setLoading(true);
|
||||
setFetchError('');
|
||||
try {
|
||||
const result = await fetchCalls({ ...f, page: p, page_size: PAGE_SIZE });
|
||||
setData(result);
|
||||
setPage(p);
|
||||
} catch (e) {
|
||||
setFetchError(String(e));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [filters]);
|
||||
|
||||
useEffect(() => { load(1); }, [filters]);
|
||||
|
||||
const setF = (key: string, val: string) => setFilters((prev) => ({ ...prev, [key]: val }));
|
||||
|
||||
const cardBg = isDark ? 'bg-slate-800/30' : 'bg-white';
|
||||
const cardBorder = isDark ? 'border-blue-800/30' : 'border-red-200';
|
||||
const textPrimary = isDark ? 'text-white' : 'text-gray-900';
|
||||
const textSecondary = isDark ? 'text-slate-400' : 'text-gray-600';
|
||||
const selectCls = isDark ? 'bg-slate-900/60 border-blue-800/40 text-slate-200' : 'bg-white border-gray-300 text-gray-700';
|
||||
const inputCls = isDark ? 'bg-slate-900/60 border-blue-800/40 text-slate-200 placeholder-slate-500' : 'bg-white border-gray-300 text-gray-700 placeholder-gray-400';
|
||||
const trHover = isDark ? 'hover:bg-slate-700/30' : 'hover:bg-gray-50';
|
||||
const thCls = `text-left text-xs font-medium uppercase tracking-wide py-2.5 px-3 ${isDark ? 'text-slate-400' : 'text-gray-500'}`;
|
||||
const tdCls = `py-2.5 px-3 text-sm ${textPrimary}`;
|
||||
const divider = isDark ? 'border-blue-800/20' : 'border-gray-200';
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className={`${cardBg} border ${cardBorder} rounded-xl p-4`}>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Filter className="w-4 h-4 text-blue-400" />
|
||||
<span className={`text-sm font-medium ${textPrimary}`}>筛选条件</span>
|
||||
{filters.task_id && (
|
||||
<span className="flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-blue-500/20 text-blue-400">
|
||||
Task: {filters.task_id}
|
||||
<button onClick={() => setF('task_id', '')} className="ml-0.5 hover:text-white">
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Select value={filters.call_type} onChange={(v) => setF('call_type', v)} placeholder="全部类型" cls={selectCls}
|
||||
options={[{ value: 'model', label: '模型' }, { value: 'mcp', label: 'MCP' }, { value: 'skill', label: 'Skill' }]} />
|
||||
<Select value={filters.model} onChange={(v) => setF('model', v)} placeholder="全部模型" cls={selectCls}
|
||||
options={opts.models.map((m) => ({ value: m, label: m }))} />
|
||||
<Select value={filters.mcp_key} onChange={(v) => setF('mcp_key', v)} placeholder="全部MCP" cls={selectCls}
|
||||
options={opts.mcps.map((m) => ({ value: m.key, label: m.name || m.key }))} />
|
||||
<Select value={filters.skill_key} onChange={(v) => setF('skill_key', v)} placeholder="全部Skill" cls={selectCls}
|
||||
options={opts.skills.map((s) => ({ value: s.key, label: s.name || s.key }))} />
|
||||
<Select value={filters.status} onChange={(v) => setF('status', v)} placeholder="全部状态" cls={selectCls}
|
||||
options={opts.statuses.map((s) => ({ value: s, label: s }))} />
|
||||
<input value={filters.task_id} onChange={(e) => setF('task_id', e.target.value)}
|
||||
placeholder="Task ID" className={`text-sm rounded-lg border px-3 py-1.5 outline-none w-36 ${inputCls}`} />
|
||||
<Select value={filters.sort_by} onChange={(v) => setF('sort_by', v)} cls={selectCls}
|
||||
options={[{ value: 'started_at', label: '按时间' }, { value: 'duration_ms', label: '按耗时' }]} />
|
||||
<Select value={filters.sort_order} onChange={(v) => setF('sort_order', v)} cls={selectCls}
|
||||
options={[{ value: 'desc', label: '降序' }, { value: 'asc', label: '升序' }]} />
|
||||
<button onClick={() => load(1)}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium ${isDark ? 'bg-blue-600 hover:bg-blue-500 text-white' : 'bg-red-600 hover:bg-red-500 text-white'}`}>
|
||||
<RefreshCw className={`w-3.5 h-3.5 ${loading ? 'animate-spin' : ''}`} />刷新
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`${cardBg} border ${cardBorder} rounded-xl overflow-hidden`}>
|
||||
{fetchError && <div className="p-4 text-red-400 text-sm">{fetchError}</div>}
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className={`border-b ${divider}`}>
|
||||
<th className={thCls}>时间</th>
|
||||
<th className={thCls}>类型</th>
|
||||
<th className={thCls}>名称</th>
|
||||
<th className={thCls}>操作</th>
|
||||
<th className={thCls}>耗时</th>
|
||||
<th className={thCls}>状态</th>
|
||||
<th className={thCls}>Task ID</th>
|
||||
<th className={thCls}>输入摘要</th>
|
||||
<th className={thCls}>输出摘要</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{loading && !data && <tr><td colSpan={9} className={`py-8 text-center text-sm ${textSecondary}`}>加载中…</td></tr>}
|
||||
{!loading && data?.items.length === 0 && <tr><td colSpan={9} className={`py-8 text-center text-sm ${textSecondary}`}>暂无数据</td></tr>}
|
||||
{data?.items.map((row) => (
|
||||
<tr key={row.id} className={`border-b ${divider} cursor-pointer transition-colors ${trHover}`}
|
||||
onClick={() => setSelected(row)}>
|
||||
<td className={`${tdCls} whitespace-nowrap text-xs ${textSecondary}`}>{formatTs(row.started_at)}</td>
|
||||
<td className={tdCls}><Badge label={row.call_type} colorClass={TYPE_COLORS[row.call_type] ?? ''} /></td>
|
||||
<td className={`${tdCls} max-w-[120px] truncate`}>{row.name}</td>
|
||||
<td className={`${tdCls} max-w-[160px] truncate ${textSecondary} text-xs`}>{row.action}</td>
|
||||
<td className={`${tdCls} whitespace-nowrap font-mono`}>{formatDuration(row.duration_ms)}</td>
|
||||
<td className={tdCls}><Badge label={row.status} colorClass={STATUS_COLORS[row.status] ?? ''} /></td>
|
||||
<td className={tdCls}>
|
||||
<div className="flex items-center gap-1">
|
||||
<TaskIdPill taskId={row.task_id} onFilter={(id) => setF('task_id', id)} isDark={isDark} />
|
||||
{row.task_id && (
|
||||
<button title="在报错日志中查看" onClick={(e) => { e.stopPropagation(); onSwitchToErrors(row.task_id!); }}
|
||||
className={`p-0.5 rounded transition-all ${isDark ? 'text-slate-500 hover:text-red-400' : 'text-gray-400 hover:text-red-500'}`}>
|
||||
<ArrowRight className="w-3 h-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className={`${tdCls} max-w-[160px] truncate text-xs ${textSecondary}`}>{row.input_summary ?? '-'}</td>
|
||||
<td className={`${tdCls} max-w-[160px] truncate text-xs ${textSecondary}`}>{row.output_summary ?? '-'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{data && (
|
||||
<div className={`flex items-center justify-end px-4 py-3 border-t ${divider}`}>
|
||||
<Pagination page={data.page} pageSize={data.page_size} total={data.total} onPage={(p) => load(p)} isDark={isDark} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selected && <CallDetailModal item={selected} onClose={() => setSelected(null)} isDark={isDark} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// 报错日志 Tab //
|
||||
// ------------------------------------------------------------------ //
|
||||
|
||||
interface ErrorsTabProps {
|
||||
opts: Options;
|
||||
isDark: boolean;
|
||||
externalTaskId: string;
|
||||
onSwitchToCalls: (taskId: string) => void;
|
||||
}
|
||||
|
||||
function ErrorsTab({ opts, isDark, externalTaskId, onSwitchToCalls }: ErrorsTabProps) {
|
||||
const [filters, setFilters] = useState({
|
||||
source: '', model: '', mcp_key: '', skill_key: '', level: '',
|
||||
task_id: externalTaskId, sort_order: 'desc',
|
||||
});
|
||||
const [page, setPage] = useState(1);
|
||||
const [data, setData] = useState<PagedResponse<ErrorLog> | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [fetchError, setFetchError] = useState('');
|
||||
const [selected, setSelected] = useState<ErrorLog | null>(null);
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
useEffect(() => {
|
||||
setFilters((prev) => ({ ...prev, task_id: externalTaskId }));
|
||||
}, [externalTaskId]);
|
||||
|
||||
const load = useCallback(async (p: number, f = filters) => {
|
||||
setLoading(true);
|
||||
setFetchError('');
|
||||
try {
|
||||
const result = await fetchErrors({ ...f, page: p, page_size: PAGE_SIZE });
|
||||
setData(result);
|
||||
setPage(p);
|
||||
} catch (e) {
|
||||
setFetchError(String(e));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [filters]);
|
||||
|
||||
useEffect(() => { load(1); }, [filters]);
|
||||
|
||||
const setF = (key: string, val: string) => setFilters((prev) => ({ ...prev, [key]: val }));
|
||||
|
||||
const cardBg = isDark ? 'bg-slate-800/30' : 'bg-white';
|
||||
const cardBorder = isDark ? 'border-blue-800/30' : 'border-red-200';
|
||||
const textPrimary = isDark ? 'text-white' : 'text-gray-900';
|
||||
const textSecondary = isDark ? 'text-slate-400' : 'text-gray-600';
|
||||
const selectCls = isDark ? 'bg-slate-900/60 border-blue-800/40 text-slate-200' : 'bg-white border-gray-300 text-gray-700';
|
||||
const inputCls = isDark ? 'bg-slate-900/60 border-blue-800/40 text-slate-200 placeholder-slate-500' : 'bg-white border-gray-300 text-gray-700 placeholder-gray-400';
|
||||
const trHover = isDark ? 'hover:bg-slate-700/30' : 'hover:bg-gray-50';
|
||||
const thCls = `text-left text-xs font-medium uppercase tracking-wide py-2.5 px-3 ${isDark ? 'text-slate-400' : 'text-gray-500'}`;
|
||||
const tdCls = `py-2.5 px-3 text-sm ${textPrimary}`;
|
||||
const divider = isDark ? 'border-blue-800/20' : 'border-gray-200';
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className={`${cardBg} border ${cardBorder} rounded-xl p-4`}>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Filter className="w-4 h-4 text-red-400" />
|
||||
<span className={`text-sm font-medium ${textPrimary}`}>筛选条件</span>
|
||||
{filters.task_id && (
|
||||
<span className="flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-blue-500/20 text-blue-400">
|
||||
Task: {filters.task_id}
|
||||
<button onClick={() => setF('task_id', '')} className="ml-0.5 hover:text-white">
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Select value={filters.source} onChange={(v) => setF('source', v)} placeholder="全部来源" cls={selectCls}
|
||||
options={opts.sources.map((s) => ({ value: s, label: s }))} />
|
||||
<Select value={filters.level} onChange={(v) => setF('level', v)} placeholder="全部级别" cls={selectCls}
|
||||
options={[{ value: 'warning', label: '警告' }, { value: 'error', label: '错误' }]} />
|
||||
<Select value={filters.model} onChange={(v) => setF('model', v)} placeholder="全部模型" cls={selectCls}
|
||||
options={opts.models.map((m) => ({ value: m, label: m }))} />
|
||||
<Select value={filters.mcp_key} onChange={(v) => setF('mcp_key', v)} placeholder="全部MCP" cls={selectCls}
|
||||
options={opts.mcps.map((m) => ({ value: m.key, label: m.name || m.key }))} />
|
||||
<Select value={filters.skill_key} onChange={(v) => setF('skill_key', v)} placeholder="全部Skill" cls={selectCls}
|
||||
options={opts.skills.map((s) => ({ value: s.key, label: s.name || s.key }))} />
|
||||
<input value={filters.task_id} onChange={(e) => setF('task_id', e.target.value)}
|
||||
placeholder="Task ID" className={`text-sm rounded-lg border px-3 py-1.5 outline-none w-36 ${inputCls}`} />
|
||||
<Select value={filters.sort_order} onChange={(v) => setF('sort_order', v)} cls={selectCls}
|
||||
options={[{ value: 'desc', label: '降序' }, { value: 'asc', label: '升序' }]} />
|
||||
<button onClick={() => load(1)}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium ${isDark ? 'bg-blue-600 hover:bg-blue-500 text-white' : 'bg-red-600 hover:bg-red-500 text-white'}`}>
|
||||
<RefreshCw className={`w-3.5 h-3.5 ${loading ? 'animate-spin' : ''}`} />刷新
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`${cardBg} border ${cardBorder} rounded-xl overflow-hidden`}>
|
||||
{fetchError && <div className="p-4 text-red-400 text-sm">{fetchError}</div>}
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className={`border-b ${divider}`}>
|
||||
<th className={thCls}>时间</th>
|
||||
<th className={thCls}>级别</th>
|
||||
<th className={thCls}>来源</th>
|
||||
<th className={thCls}>消息</th>
|
||||
<th className={thCls}>可恢复</th>
|
||||
<th className={thCls}>Task ID</th>
|
||||
<th className={thCls}>MCP</th>
|
||||
<th className={thCls}>Skill</th>
|
||||
<th className={thCls}>模型</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{loading && !data && <tr><td colSpan={9} className={`py-8 text-center text-sm ${textSecondary}`}>加载中…</td></tr>}
|
||||
{!loading && data?.items.length === 0 && <tr><td colSpan={9} className={`py-8 text-center text-sm ${textSecondary}`}>暂无数据</td></tr>}
|
||||
{data?.items.map((row) => (
|
||||
<tr key={row.id} className={`border-b ${divider} cursor-pointer transition-colors ${trHover}`}
|
||||
onClick={() => setSelected(row)}>
|
||||
<td className={`${tdCls} whitespace-nowrap text-xs ${textSecondary}`}>{formatTs(row.happened_at)}</td>
|
||||
<td className={tdCls}><Badge label={row.level} colorClass={STATUS_COLORS[row.level] ?? ''} /></td>
|
||||
<td className={tdCls}><Badge label={row.source} colorClass={SOURCE_COLORS[row.source] ?? ''} /></td>
|
||||
<td className={`${tdCls} max-w-[240px] truncate`}>{row.message}</td>
|
||||
<td className={`${tdCls} text-xs`}>
|
||||
<span className={row.recoverable ? 'text-green-400' : 'text-red-400'}>{row.recoverable ? '是' : '否'}</span>
|
||||
</td>
|
||||
<td className={tdCls}>
|
||||
<div className="flex items-center gap-1">
|
||||
<TaskIdPill taskId={row.task_id} onFilter={(id) => setF('task_id', id)} isDark={isDark} />
|
||||
{row.task_id && (
|
||||
<button title="在调用耗时中查看" onClick={(e) => { e.stopPropagation(); onSwitchToCalls(row.task_id!); }}
|
||||
className={`p-0.5 rounded transition-all ${isDark ? 'text-slate-500 hover:text-blue-400' : 'text-gray-400 hover:text-blue-500'}`}>
|
||||
<ArrowRight className="w-3 h-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className={`${tdCls} text-xs ${textSecondary}`}>{row.mcp_name ?? row.mcp_key ?? '-'}</td>
|
||||
<td className={`${tdCls} text-xs ${textSecondary}`}>{row.skill_name ?? row.skill_key ?? '-'}</td>
|
||||
<td className={`${tdCls} text-xs ${textSecondary}`}>{row.model ?? '-'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{data && (
|
||||
<div className={`flex items-center justify-end px-4 py-3 border-t ${divider}`}>
|
||||
<Pagination page={data.page} pageSize={data.page_size} total={data.total} onPage={(p) => load(p)} isDark={isDark} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selected && <ErrorDetailModal item={selected} onClose={() => setSelected(null)} isDark={isDark} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// Main Page //
|
||||
// ------------------------------------------------------------------ //
|
||||
|
||||
const MANUAL_PURGE_DAYS = 14;
|
||||
|
||||
async function callPurge(retainDays: number): Promise<{ deleted_call_traces: number; deleted_error_logs: number }> {
|
||||
const res = await fetch(`${backUrl}/api/observability/purge?retain_days=${retainDays}`, { method: 'DELETE' });
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export default function ObservabilityPage() {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
const [activeTab, setActiveTab] = useState<'calls' | 'errors'>('calls');
|
||||
const [opts, setOpts] = useState<Options>({ models: [], mcps: [], skills: [], statuses: [], sources: [] });
|
||||
const [sharedTaskId, setSharedTaskId] = useState('');
|
||||
const [purging, setPurging] = useState(false);
|
||||
const [purgeMsg, setPurgeMsg] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
fetchOptions().then(setOpts).catch(console.error);
|
||||
}, []);
|
||||
|
||||
const handlePurge = async () => {
|
||||
if (!window.confirm(`确认清理 ${MANUAL_PURGE_DAYS} 天前的旧记录?此操作不可撤销。`)) return;
|
||||
setPurging(true);
|
||||
setPurgeMsg('');
|
||||
try {
|
||||
const r = await callPurge(MANUAL_PURGE_DAYS);
|
||||
setPurgeMsg(`已清理:调用记录 ${r.deleted_call_traces} 条,错误日志 ${r.deleted_error_logs} 条`);
|
||||
} catch (e) {
|
||||
setPurgeMsg(`清理失败:${e}`);
|
||||
} finally {
|
||||
setPurging(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSwitchToErrors = (taskId: string) => {
|
||||
setSharedTaskId(taskId);
|
||||
setActiveTab('errors');
|
||||
};
|
||||
const handleSwitchToCalls = (taskId: string) => {
|
||||
setSharedTaskId(taskId);
|
||||
setActiveTab('calls');
|
||||
};
|
||||
|
||||
const cardBg = isDark ? 'bg-slate-800/30' : 'bg-white';
|
||||
const cardBorder = isDark ? 'border-blue-800/30' : 'border-red-200';
|
||||
const textPrimary = isDark ? 'text-white' : 'text-gray-900';
|
||||
const textSecondary = isDark ? 'text-slate-400' : 'text-gray-600';
|
||||
const tabActive = isDark ? 'bg-blue-600/20 text-blue-400 border border-blue-500/30' : 'bg-red-50 text-red-600 border border-red-200';
|
||||
const tabInactive = isDark ? 'text-slate-400 hover:text-white hover:bg-slate-800/50' : 'text-gray-600 hover:text-red-600 hover:bg-red-50';
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className={`${cardBg} backdrop-blur-xl border ${cardBorder} rounded-xl p-4`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${isDark ? 'bg-blue-500/20' : 'bg-red-50'}`}>
|
||||
<Activity className={`w-5 h-5 ${isDark ? 'text-blue-400' : 'text-red-500'}`} />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className={`font-bold text-lg ${textPrimary}`}>可观测性</h1>
|
||||
<p className={`text-sm ${textSecondary}`}>调用耗时统计 · 报错日志</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{purgeMsg && (
|
||||
<span className={`text-xs px-2 py-1 rounded-lg ${isDark ? 'bg-slate-700 text-slate-300' : 'bg-gray-100 text-gray-600'}`}>
|
||||
{purgeMsg}
|
||||
</span>
|
||||
)}
|
||||
<button onClick={handlePurge} disabled={purging}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition-all
|
||||
${isDark ? 'border-red-800/40 text-red-400 hover:bg-red-900/20 disabled:opacity-40' : 'border-red-200 text-red-600 hover:bg-red-50 disabled:opacity-40'}`}>
|
||||
<Trash2 className={`w-3.5 h-3.5 ${purging ? 'animate-spin' : ''}`} />
|
||||
清理 {MANUAL_PURGE_DAYS} 天前数据
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => setActiveTab('calls')}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all ${activeTab === 'calls' ? tabActive : tabInactive}`}>
|
||||
<Clock className="w-4 h-4" />调用耗时统计
|
||||
</button>
|
||||
<button onClick={() => setActiveTab('errors')}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all ${activeTab === 'errors' ? tabActive : tabInactive}`}>
|
||||
<AlertTriangle className="w-4 h-4" />报错日志
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{activeTab === 'calls' ? (
|
||||
<CallsTab opts={opts} isDark={isDark} externalTaskId={sharedTaskId} onSwitchToErrors={handleSwitchToErrors} />
|
||||
) : (
|
||||
<ErrorsTab opts={opts} isDark={isDark} externalTaskId={sharedTaskId} onSwitchToCalls={handleSwitchToCalls} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,758 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Search,
|
||||
Bookmark,
|
||||
BookmarkCheck,
|
||||
Copy,
|
||||
Download,
|
||||
MessageSquare,
|
||||
Bot,
|
||||
History,
|
||||
Calendar,
|
||||
Filter,
|
||||
ChevronDown,
|
||||
GitCompare,
|
||||
FileText,
|
||||
ArrowRight,
|
||||
X,
|
||||
Star,
|
||||
CheckCircle,
|
||||
AlertCircle,
|
||||
Send,
|
||||
} from 'lucide-react';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { PageType } from '../components/Sidebar';
|
||||
|
||||
type RecordType = 'all' | 'general' | 'agent';
|
||||
type ModeType = 'all' | 'single-round' | 'multi-round' | 'deep-search' | 'compare';
|
||||
type CollectionStatus = 'all' | 'collected';
|
||||
|
||||
interface QARecord {
|
||||
id: string;
|
||||
type: 'general' | 'agent';
|
||||
mode: 'single-round' | 'multi-round' | 'deep-search' | 'compare';
|
||||
question: string;
|
||||
answer: string;
|
||||
agentName?: string;
|
||||
createTime: Date;
|
||||
isCollected: boolean;
|
||||
isHighValue: boolean;
|
||||
isReusable: boolean;
|
||||
isConvertible: boolean;
|
||||
notes?: string;
|
||||
evaluation?: string;
|
||||
sources?: string[];
|
||||
}
|
||||
|
||||
const QARecords: React.FC<{ onNavigate: (page: PageType) => void }> = ({ onNavigate }) => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
|
||||
// 模拟问答记录数据
|
||||
const mockRecords: QARecord[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'general',
|
||||
mode: 'compare',
|
||||
question: 'RAG技术的工作原理是什么?',
|
||||
answer: '单轮RAG回答:直接检索并生成答案,响应速度快...',
|
||||
createTime: new Date('2026-03-19 14:30'),
|
||||
isCollected: true,
|
||||
isHighValue: true,
|
||||
isReusable: true,
|
||||
isConvertible: true,
|
||||
sources: ['RAG技术白皮书', 'LLM应用实践'],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'agent',
|
||||
mode: 'single-round',
|
||||
question: '目标受众应如何分层?',
|
||||
answer: '基于受众分析智能体的专业能力,识别出核心受众特征...',
|
||||
agentName: 'AudienceAgent',
|
||||
createTime: new Date('2026-03-19 10:20'),
|
||||
isCollected: false,
|
||||
isHighValue: true,
|
||||
isReusable: true,
|
||||
isConvertible: true,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'general',
|
||||
mode: 'deep-search',
|
||||
question: '品牌年轻化传播策略有哪些?',
|
||||
answer: '通过深度检索分析,总结出以下品牌年轻化传播策略...',
|
||||
createTime: new Date('2026-03-18 16:45'),
|
||||
isCollected: true,
|
||||
isHighValue: false,
|
||||
isReusable: false,
|
||||
isConvertible: false,
|
||||
notes: '需要进一步补充具体案例',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
type: 'agent',
|
||||
mode: 'multi-round',
|
||||
question: '如何制定有效的用户激活策略?',
|
||||
answer: '通过多轮对话深入分析,制定以下用户激活策略...',
|
||||
agentName: 'ChannelAgent',
|
||||
createTime: new Date('2026-03-18 11:15'),
|
||||
isCollected: false,
|
||||
isHighValue: true,
|
||||
isReusable: true,
|
||||
isConvertible: true,
|
||||
evaluation: '策略实用性强,建议转化任务执行',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
type: 'general',
|
||||
mode: 'single-round',
|
||||
question: '内容营销的最佳实践是什么?',
|
||||
answer: '内容营销最佳实践包括:明确目标受众、创造高质量内容...',
|
||||
createTime: new Date('2026-03-17 09:00'),
|
||||
isCollected: false,
|
||||
isHighValue: false,
|
||||
isReusable: true,
|
||||
isConvertible: false,
|
||||
},
|
||||
];
|
||||
|
||||
// 状态
|
||||
const [records, setRecords] = useState<QARecord[]>(mockRecords);
|
||||
const [selectedRecord, setSelectedRecord] = useState<QARecord | null>(mockRecords[0]);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [recordType, setRecordType] = useState<RecordType>('all');
|
||||
const [modeType, setModeType] = useState<ModeType>('all');
|
||||
const [collectionStatus, setCollectionStatus] = useState<CollectionStatus>('all');
|
||||
const [timeRange, setTimeRange] = useState('all');
|
||||
const [notesInput, setNotesInput] = useState('');
|
||||
|
||||
// 筛选记录
|
||||
const filteredRecords = records.filter((record) => {
|
||||
// 搜索过滤
|
||||
if (searchQuery && !record.question.toLowerCase().includes(searchQuery.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
// 类型过滤
|
||||
if (recordType !== 'all' && record.type !== recordType) {
|
||||
return false;
|
||||
}
|
||||
// 模式过滤
|
||||
if (modeType !== 'all' && record.mode !== modeType) {
|
||||
return false;
|
||||
}
|
||||
// 收藏状态过滤
|
||||
if (collectionStatus === 'collected' && !record.isCollected) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// 获取模式标签
|
||||
const getModeLabel = (mode: QARecord['mode']) => {
|
||||
switch (mode) {
|
||||
case 'single-round': return '单轮RAG';
|
||||
case 'multi-round': return '多轮RAG';
|
||||
case 'deep-search': return '深度检索';
|
||||
case 'compare': return '对比';
|
||||
}
|
||||
};
|
||||
|
||||
// 获取模式颜色
|
||||
const getModeColor = (mode: QARecord['mode']) => {
|
||||
switch (mode) {
|
||||
case 'single-round':
|
||||
return isDark ? 'bg-blue-600/30 text-blue-400' : 'bg-blue-100 text-blue-600';
|
||||
case 'multi-round':
|
||||
return isDark ? 'bg-green-600/30 text-green-400' : 'bg-green-100 text-green-600';
|
||||
case 'deep-search':
|
||||
return isDark ? 'bg-purple-600/30 text-purple-400' : 'bg-purple-100 text-purple-600';
|
||||
case 'compare':
|
||||
return isDark ? 'bg-orange-600/30 text-orange-400' : 'bg-orange-100 text-orange-600';
|
||||
}
|
||||
};
|
||||
|
||||
// 切换收藏
|
||||
const toggleCollect = (recordId: string) => {
|
||||
setRecords(records.map(record =>
|
||||
record.id === recordId ? { ...record, isCollected: !record.isCollected } : record
|
||||
));
|
||||
if (selectedRecord?.id === recordId) {
|
||||
setSelectedRecord({ ...selectedRecord, isCollected: !selectedRecord.isCollected });
|
||||
}
|
||||
};
|
||||
|
||||
// 复制问题
|
||||
const copyQuestion = (question: string) => {
|
||||
navigator.clipboard.writeText(question);
|
||||
alert('问题已复制');
|
||||
};
|
||||
|
||||
// 导出记录
|
||||
const exportRecord = (record: QARecord) => {
|
||||
alert(`导出记录: ${record.question.substring(0, 20)}...`);
|
||||
};
|
||||
|
||||
// 转到任务
|
||||
const convertToTask = (record: QARecord, taskType: 'single' | 'consult' | 'scene') => {
|
||||
const messages = {
|
||||
single: '单智能体任务',
|
||||
consult: '多智能体会商',
|
||||
scene: '场景任务',
|
||||
};
|
||||
alert(`已转为${messages[taskType]}`);
|
||||
};
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (date: Date) => {
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - date.getTime();
|
||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (days === 0) {
|
||||
return `今天 ${date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}`;
|
||||
} else if (days === 1) {
|
||||
return `昨天 ${date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}`;
|
||||
} else if (days < 7) {
|
||||
return `${days}天前`;
|
||||
} else {
|
||||
return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`h-[calc(100vh-120px)] flex flex-col theme-bg rounded-xl ${isDark ? 'bg-slate-800/50' : 'bg-white'} border ${isDark ? 'border-slate-700/50' : 'border-red-100'}`}>
|
||||
{/* 顶部筛选栏 */}
|
||||
<div className={`flex items-center justify-between px-6 py-4 border-b ${isDark ? 'border-slate-700/50' : 'border-red-100'}`}>
|
||||
<div className="flex items-center gap-4">
|
||||
<h1 className={`text-lg font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
||||
问答记录
|
||||
</h1>
|
||||
<span className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
查看历史问答、对比结果与高价值记录
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 筛选控件 */}
|
||||
<div className="flex items-center gap-3">
|
||||
{/* 搜索框 */}
|
||||
<div className={`relative ${isDark ? 'bg-slate-700/50' : 'bg-gray-100'} rounded-lg`}>
|
||||
<Search className={`absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 ${isDark ? 'text-slate-400' : 'text-gray-400'}`} />
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="搜索问题关键词..."
|
||||
className={`w-64 pl-10 pr-4 py-2 rounded-lg text-sm focus:outline-none ${
|
||||
isDark
|
||||
? 'bg-transparent text-white placeholder-slate-500'
|
||||
: 'bg-transparent text-gray-800 placeholder-gray-400'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 记录类型 */}
|
||||
<div className="relative">
|
||||
<select
|
||||
value={recordType}
|
||||
onChange={(e) => setRecordType(e.target.value as RecordType)}
|
||||
className={`appearance-none px-4 py-2 pr-10 rounded-lg text-sm cursor-pointer ${
|
||||
isDark
|
||||
? 'bg-slate-700/50 text-slate-300 border border-slate-600'
|
||||
: 'bg-gray-100 text-gray-700 border border-gray-200'
|
||||
}`}
|
||||
>
|
||||
<option value="all">全部类型</option>
|
||||
<option value="general">通用问答</option>
|
||||
<option value="agent">智能体问答</option>
|
||||
</select>
|
||||
<ChevronDown className={`absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 pointer-events-none ${
|
||||
isDark ? 'text-slate-400' : 'text-gray-400'
|
||||
}`} />
|
||||
</div>
|
||||
|
||||
{/* 模式 */}
|
||||
<div className="relative">
|
||||
<select
|
||||
value={modeType}
|
||||
onChange={(e) => setModeType(e.target.value as ModeType)}
|
||||
className={`appearance-none px-4 py-2 pr-10 rounded-lg text-sm cursor-pointer ${
|
||||
isDark
|
||||
? 'bg-slate-700/50 text-slate-300 border border-slate-600'
|
||||
: 'bg-gray-100 text-gray-700 border border-gray-200'
|
||||
}`}
|
||||
>
|
||||
<option value="all">全部模式</option>
|
||||
<option value="single-round">单轮RAG</option>
|
||||
<option value="multi-round">多轮RAG</option>
|
||||
<option value="deep-search">深度检索</option>
|
||||
<option value="compare">对比记录</option>
|
||||
</select>
|
||||
<ChevronDown className={`absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 pointer-events-none ${
|
||||
isDark ? 'text-slate-400' : 'text-gray-400'
|
||||
}`} />
|
||||
</div>
|
||||
|
||||
{/* 收藏状态 */}
|
||||
<div className="relative">
|
||||
<select
|
||||
value={collectionStatus}
|
||||
onChange={(e) => setCollectionStatus(e.target.value as CollectionStatus)}
|
||||
className={`appearance-none px-4 py-2 pr-10 rounded-lg text-sm cursor-pointer ${
|
||||
isDark
|
||||
? 'bg-slate-700/50 text-slate-300 border border-slate-600'
|
||||
: 'bg-gray-100 text-gray-700 border border-gray-200'
|
||||
}`}
|
||||
>
|
||||
<option value="all">全部</option>
|
||||
<option value="collected">已收藏</option>
|
||||
</select>
|
||||
<ChevronDown className={`absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 pointer-events-none ${
|
||||
isDark ? 'text-slate-400' : 'text-gray-400'
|
||||
}`} />
|
||||
</div>
|
||||
|
||||
{/* 时间范围 */}
|
||||
<div className="relative">
|
||||
<select
|
||||
value={timeRange}
|
||||
onChange={(e) => setTimeRange(e.target.value)}
|
||||
className={`appearance-none px-4 py-2 pr-10 rounded-lg text-sm cursor-pointer ${
|
||||
isDark
|
||||
? 'bg-slate-700/50 text-slate-300 border border-slate-600'
|
||||
: 'bg-gray-100 text-gray-700 border border-gray-200'
|
||||
}`}
|
||||
>
|
||||
<option value="all">全部时间</option>
|
||||
<option value="today">今天</option>
|
||||
<option value="week">本周</option>
|
||||
<option value="month">本月</option>
|
||||
</select>
|
||||
<ChevronDown className={`absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 pointer-events-none ${
|
||||
isDark ? 'text-slate-400' : 'text-gray-400'
|
||||
}`} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 主体内容区 */}
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{/* 左侧记录列表 */}
|
||||
<div className={`w-80 border-r flex-shrink-0 overflow-y-auto ${isDark ? 'border-slate-700/50' : 'border-red-100'}`}>
|
||||
<div className="p-4">
|
||||
<div className="space-y-2">
|
||||
{filteredRecords.map((record) => (
|
||||
<button
|
||||
key={record.id}
|
||||
onClick={() => {
|
||||
setSelectedRecord(record);
|
||||
setNotesInput(record.notes || '');
|
||||
}}
|
||||
className={`w-full text-left p-3 rounded-lg transition-all ${
|
||||
selectedRecord?.id === record.id
|
||||
? isDark
|
||||
? 'bg-blue-600/20 border border-blue-500/50'
|
||||
: 'bg-red-50 border border-red-200'
|
||||
: isDark
|
||||
? 'bg-slate-700/30 border border-transparent hover:bg-slate-700/50'
|
||||
: 'bg-white border border-gray-200 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-2 mb-2">
|
||||
<span className={`px-1.5 py-0.5 rounded text-xs font-medium ${
|
||||
record.type === 'general'
|
||||
? isDark
|
||||
? 'bg-blue-600/30 text-blue-400'
|
||||
: 'bg-blue-100 text-blue-600'
|
||||
: isDark
|
||||
? 'bg-purple-600/30 text-purple-400'
|
||||
: 'bg-purple-100 text-purple-600'
|
||||
}`}>
|
||||
{record.type === 'general' ? '通用问答' : '智能体'}
|
||||
</span>
|
||||
<span className={`px-1.5 py-0.5 rounded text-xs font-medium ${getModeColor(record.mode)}`}>
|
||||
{getModeLabel(record.mode)}
|
||||
</span>
|
||||
{record.isCollected && (
|
||||
<BookmarkCheck className={`w-3.5 h-3.5 ${isDark ? 'text-yellow-400' : 'text-yellow-500'}`} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className={`text-sm line-clamp-2 ${isDark ? 'text-slate-200' : 'text-gray-800'}`}>
|
||||
{record.question}
|
||||
</p>
|
||||
|
||||
<div className={`flex items-center gap-2 mt-2 text-xs ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
|
||||
<Calendar className="w-3 h-3" />
|
||||
<span>{formatTime(record.createTime)}</span>
|
||||
{record.agentName && (
|
||||
<>
|
||||
<span>·</span>
|
||||
<Bot className="w-3 h-3" />
|
||||
<span>{record.agentName}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 标签 */}
|
||||
<div className="flex items-center gap-1 mt-2">
|
||||
{record.isHighValue && (
|
||||
<span className={`px-1.5 py-0.5 rounded text-xs ${
|
||||
isDark ? 'bg-yellow-600/30 text-yellow-400' : 'bg-yellow-100 text-yellow-600'
|
||||
}`}>
|
||||
高价值
|
||||
</span>
|
||||
)}
|
||||
{record.isReusable && (
|
||||
<span className={`px-1.5 py-0.5 rounded text-xs ${
|
||||
isDark ? 'bg-green-600/30 text-green-400' : 'bg-green-100 text-green-600'
|
||||
}`}>
|
||||
可复用
|
||||
</span>
|
||||
)}
|
||||
{record.isConvertible && (
|
||||
<span className={`px-1.5 py-0.5 rounded text-xs ${
|
||||
isDark ? 'bg-blue-600/30 text-blue-400' : 'bg-blue-100 text-blue-600'
|
||||
}`}>
|
||||
可转任务
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{filteredRecords.length === 0 && (
|
||||
<div className={`text-center py-8 ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
|
||||
<History className="w-12 h-12 mx-auto mb-3 opacity-50" />
|
||||
<p>暂无匹配记录</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 中间记录详情 */}
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
{selectedRecord ? (
|
||||
<div className="max-w-3xl mx-auto">
|
||||
{/* 问题 */}
|
||||
<div className={`p-4 rounded-xl mb-4 ${isDark ? 'bg-slate-700/50' : 'bg-gray-50'}`}>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className={`px-1.5 py-0.5 rounded text-xs font-medium ${
|
||||
isDark ? 'bg-slate-600 text-slate-300' : 'bg-gray-200 text-gray-600'
|
||||
}`}>
|
||||
问题
|
||||
</span>
|
||||
</div>
|
||||
<p className={`text-lg ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
||||
{selectedRecord.question}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 回答 */}
|
||||
<div className={`p-4 rounded-xl mb-4 ${isDark ? 'bg-slate-700/50' : 'bg-gray-50'}`}>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className={`px-1.5 py-0.5 rounded text-xs font-medium ${
|
||||
isDark ? 'bg-blue-600/30 text-blue-400' : 'bg-blue-100 text-blue-600'
|
||||
}`}>
|
||||
{selectedRecord.type === 'general' ? 'RAG回答' : '智能体回答'}
|
||||
</span>
|
||||
<span className={`px-1.5 py-0.5 rounded text-xs font-medium ${getModeColor(selectedRecord.mode)}`}>
|
||||
{getModeLabel(selectedRecord.mode)}
|
||||
</span>
|
||||
{selectedRecord.agentName && (
|
||||
<span className={`px-1.5 py-0.5 rounded text-xs font-medium ${
|
||||
isDark ? 'bg-purple-600/30 text-purple-400' : 'bg-purple-100 text-purple-600'
|
||||
}`}>
|
||||
{selectedRecord.agentName}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className={`whitespace-pre-wrap ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
|
||||
{selectedRecord.answer}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 来源摘要 */}
|
||||
{selectedRecord.sources && selectedRecord.sources.length > 0 && (
|
||||
<div className={`p-4 rounded-xl mb-4 ${isDark ? 'bg-slate-700/50' : 'bg-gray-50'}`}>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<FileText className={`w-4 h-4 ${isDark ? 'text-slate-400' : 'text-gray-500'}`} />
|
||||
<span className={`text-sm font-medium ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
|
||||
来源摘要
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedRecord.sources.map((source, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className={`px-3 py-1 rounded-lg text-sm ${
|
||||
isDark ? 'bg-slate-600/50 text-slate-300' : 'bg-white text-gray-600 border border-gray-200'
|
||||
}`}
|
||||
>
|
||||
{source}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 评价 */}
|
||||
{selectedRecord.evaluation && (
|
||||
<div className={`p-4 rounded-xl mb-4 ${isDark ? 'bg-yellow-600/20 border border-yellow-600/30' : 'bg-yellow-50 border border-yellow-200'}`}>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Star className={`w-4 h-4 ${isDark ? 'text-yellow-400' : 'text-yellow-500'}`} />
|
||||
<span className={`text-sm font-medium ${isDark ? 'text-yellow-400' : 'text-yellow-700'}`}>
|
||||
人工评价
|
||||
</span>
|
||||
</div>
|
||||
<p className={`text-sm ${isDark ? 'text-yellow-300' : 'text-yellow-700'}`}>
|
||||
{selectedRecord.evaluation}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className={`h-full flex flex-col items-center justify-center ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
|
||||
<MessageSquare className="w-16 h-16 mb-4 opacity-50" />
|
||||
<p className="text-lg">选择一条记录查看详情</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 右侧操作与标签区 */}
|
||||
<div className={`w-72 border-l flex-shrink-0 overflow-y-auto p-4 ${isDark ? 'border-slate-700/50 bg-slate-800/30' : 'border-red-100 bg-gray-50/50'}`}>
|
||||
{selectedRecord ? (
|
||||
<>
|
||||
{/* 记录标签 */}
|
||||
<div className="mb-6">
|
||||
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
|
||||
记录标签
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
onClick={() => toggleCollect(selectedRecord.id)}
|
||||
className={`w-full flex items-center gap-3 p-3 rounded-lg transition-all ${
|
||||
selectedRecord.isCollected
|
||||
? isDark
|
||||
? 'bg-yellow-600/20 border border-yellow-600/50'
|
||||
: 'bg-yellow-50 border border-yellow-200'
|
||||
: isDark
|
||||
? 'bg-slate-700/50 border border-transparent hover:bg-slate-600/50'
|
||||
: 'bg-white border border-gray-200 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
{selectedRecord.isCollected ? (
|
||||
<BookmarkCheck className={`w-5 h-5 ${isDark ? 'text-yellow-400' : 'text-yellow-500'}`} />
|
||||
) : (
|
||||
<Bookmark className={`w-5 h-5 ${isDark ? 'text-slate-400' : 'text-gray-400'}`} />
|
||||
)}
|
||||
<span className={`text-sm ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
|
||||
{selectedRecord.isCollected ? '已收藏' : '收藏'}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`w-full flex items-center gap-3 p-3 rounded-lg transition-all ${
|
||||
selectedRecord.isHighValue
|
||||
? isDark
|
||||
? 'bg-yellow-600/20 border border-yellow-600/50'
|
||||
: 'bg-yellow-50 border border-yellow-200'
|
||||
: isDark
|
||||
? 'bg-slate-700/50 border border-transparent hover:bg-slate-600/50'
|
||||
: 'bg-white border border-gray-200 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<Star className={`w-5 h-5 ${selectedRecord.isHighValue ? (isDark ? 'text-yellow-400' : 'text-yellow-500') : (isDark ? 'text-slate-400' : 'text-gray-400')}`} />
|
||||
<span className={`text-sm ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
|
||||
高价值
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`w-full flex items-center gap-3 p-3 rounded-lg transition-all ${
|
||||
selectedRecord.isReusable
|
||||
? isDark
|
||||
? 'bg-green-600/20 border border-green-600/50'
|
||||
: 'bg-green-50 border border-green-200'
|
||||
: isDark
|
||||
? 'bg-slate-700/50 border border-transparent hover:bg-slate-600/50'
|
||||
: 'bg-white border border-gray-200 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<Copy className={`w-5 h-5 ${selectedRecord.isReusable ? (isDark ? 'text-green-400' : 'text-green-500') : (isDark ? 'text-slate-400' : 'text-gray-400')}`} />
|
||||
<span className={`text-sm ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
|
||||
可复用
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`w-full flex items-center gap-3 p-3 rounded-lg transition-all ${
|
||||
selectedRecord.isConvertible
|
||||
? isDark
|
||||
? 'bg-blue-600/20 border border-blue-600/50'
|
||||
: 'bg-blue-50 border border-blue-200'
|
||||
: isDark
|
||||
? 'bg-slate-700/50 border border-transparent hover:bg-slate-600/50'
|
||||
: 'bg-white border border-gray-200 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<ArrowRight className={`w-5 h-5 ${selectedRecord.isConvertible ? (isDark ? 'text-blue-400' : 'text-blue-500') : (isDark ? 'text-slate-400' : 'text-gray-400')}`} />
|
||||
<span className={`text-sm ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
|
||||
可转任务
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="mb-6">
|
||||
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
|
||||
操作
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
onClick={() => toggleCollect(selectedRecord.id)}
|
||||
className={`w-full flex items-center gap-3 p-3 rounded-lg transition-all ${
|
||||
isDark
|
||||
? 'bg-slate-700/50 hover:bg-slate-600/50 text-slate-300'
|
||||
: 'bg-white hover:bg-gray-50 text-gray-700 border border-gray-200'
|
||||
}`}
|
||||
>
|
||||
{selectedRecord.isCollected ? (
|
||||
<>
|
||||
<BookmarkCheck className="w-5 h-5 text-yellow-500" />
|
||||
<span className="text-sm">取消收藏</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Bookmark className="w-5 h-5" />
|
||||
<span className="text-sm">收藏</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => exportRecord(selectedRecord)}
|
||||
className={`w-full flex items-center gap-3 p-3 rounded-lg transition-all ${
|
||||
isDark
|
||||
? 'bg-slate-700/50 hover:bg-slate-600/50 text-slate-300'
|
||||
: 'bg-white hover:bg-gray-50 text-gray-700 border border-gray-200'
|
||||
}`}
|
||||
>
|
||||
<Download className="w-5 h-5" />
|
||||
<span className="text-sm">导出</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => copyQuestion(selectedRecord.question)}
|
||||
className={`w-full flex items-center gap-3 p-3 rounded-lg transition-all ${
|
||||
isDark
|
||||
? 'bg-slate-700/50 hover:bg-slate-600/50 text-slate-300'
|
||||
: 'bg-white hover:bg-gray-50 text-gray-700 border border-gray-200'
|
||||
}`}
|
||||
>
|
||||
<Copy className="w-5 h-5" />
|
||||
<span className="text-sm">复制问题</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 转任务 */}
|
||||
{selectedRecord.isConvertible && (
|
||||
<div className="mb-6">
|
||||
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
|
||||
转为任务
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
onClick={() => convertToTask(selectedRecord, 'single')}
|
||||
className={`w-full flex items-center gap-3 p-3 rounded-lg transition-all ${
|
||||
isDark
|
||||
? 'bg-blue-600/20 hover:bg-blue-600/30 text-blue-400 border border-blue-600/50'
|
||||
: 'bg-blue-50 hover:bg-blue-100 text-blue-600 border border-blue-200'
|
||||
}`}
|
||||
>
|
||||
<Bot className="w-5 h-5" />
|
||||
<span className="text-sm flex-1 text-left">转单智能体任务</span>
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => convertToTask(selectedRecord, 'consult')}
|
||||
className={`w-full flex items-center gap-3 p-3 rounded-lg transition-all ${
|
||||
isDark
|
||||
? 'bg-purple-600/20 hover:bg-purple-600/30 text-purple-400 border border-purple-600/50'
|
||||
: 'bg-purple-50 hover:bg-purple-100 text-purple-600 border border-purple-200'
|
||||
}`}
|
||||
>
|
||||
<MessageSquare className="w-5 h-5" />
|
||||
<span className="text-sm flex-1 text-left">转多智能体会商</span>
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => convertToTask(selectedRecord, 'scene')}
|
||||
className={`w-full flex items-center gap-3 p-3 rounded-lg transition-all ${
|
||||
isDark
|
||||
? 'bg-green-600/20 hover:bg-green-600/30 text-green-400 border border-green-600/50'
|
||||
: 'bg-green-50 hover:bg-green-100 text-green-600 border border-green-200'
|
||||
}`}
|
||||
>
|
||||
<GitCompare className="w-5 h-5" />
|
||||
<span className="text-sm flex-1 text-left">转场景任务</span>
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 备注 */}
|
||||
<div>
|
||||
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
|
||||
备注
|
||||
</h3>
|
||||
<textarea
|
||||
value={notesInput}
|
||||
onChange={(e) => setNotesInput(e.target.value)}
|
||||
placeholder="添加人工评价或后续处理建议..."
|
||||
rows={4}
|
||||
className={`w-full p-3 rounded-lg text-sm resize-none focus:outline-none ${
|
||||
isDark
|
||||
? 'bg-slate-700/50 text-slate-300 placeholder-slate-500 border border-slate-600'
|
||||
: 'bg-white text-gray-700 placeholder-gray-400 border border-gray-200'
|
||||
}`}
|
||||
/>
|
||||
{notesInput !== (selectedRecord.notes || '') && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setRecords(records.map(r =>
|
||||
r.id === selectedRecord.id ? { ...r, notes: notesInput } : r
|
||||
));
|
||||
setSelectedRecord({ ...selectedRecord, notes: notesInput });
|
||||
alert('备注已保存');
|
||||
}}
|
||||
className={`w-full mt-2 px-4 py-2 rounded-lg text-sm font-medium transition-all ${
|
||||
isDark
|
||||
? 'bg-blue-600 hover:bg-blue-700 text-white'
|
||||
: 'bg-red-500 hover:bg-red-600 text-white'
|
||||
}`}
|
||||
>
|
||||
保存备注
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className={`h-full flex flex-col items-center justify-center ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
|
||||
<MessageSquare className="w-12 h-12 mb-3 opacity-50" />
|
||||
<p className="text-sm">选择记录后可操作</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default QARecords;
|
||||
@@ -0,0 +1,543 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
FileText,
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
Download,
|
||||
Edit3,
|
||||
Send,
|
||||
CheckCircle,
|
||||
Clock,
|
||||
Bot,
|
||||
Target,
|
||||
Users,
|
||||
TrendingUp,
|
||||
MessageSquare,
|
||||
Megaphone,
|
||||
Shield,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
RefreshCw,
|
||||
MoreHorizontal,
|
||||
Share2,
|
||||
Printer,
|
||||
History,
|
||||
AlertCircle,
|
||||
Check,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { PageType } from '../components/Sidebar';
|
||||
|
||||
interface ResultDetailProps {
|
||||
taskId?: string;
|
||||
onNavigate: (page: PageType, taskId?: string) => void;
|
||||
onBack?: () => void;
|
||||
}
|
||||
|
||||
// 模拟任务详情数据
|
||||
const mockTaskDetail = {
|
||||
id: 'task-001',
|
||||
title: '五四青年节宣传活动策略',
|
||||
category: '规划任务',
|
||||
subCategory: '叙事策略',
|
||||
status: 'completed',
|
||||
createdAt: '2026-03-19 10:30',
|
||||
updatedAt: '2026-03-19 15:30',
|
||||
completedAt: '2026-03-19 15:30',
|
||||
reportType: 'strategy',
|
||||
agentCount: 6,
|
||||
description: '针对五四青年节的品牌传播策略方案',
|
||||
scene: '宣传策略生成',
|
||||
duration: '5小时32分',
|
||||
conclusion: {
|
||||
main: '五四青年节宣传活动应聚焦"真实、有态度的青年故事"这一叙事方向,以18-25岁基层青年为核心受众,优先在短视频平台建立品牌认知。',
|
||||
recommended: [
|
||||
'以真实人物故事为核心叙事内容',
|
||||
'采用平等对话而非说教式的传播语气',
|
||||
'优先布局抖音、小红书等短视频平台',
|
||||
'建立UGC激励机制促进用户参与',
|
||||
],
|
||||
notRecommended: [
|
||||
'过度包装的精致化内容',
|
||||
'单向输出的宣传式传播',
|
||||
'纯文字为主的长图文形式',
|
||||
],
|
||||
pendingIssues: [
|
||||
'叙事角度:事件叙事 vs 人物叙事(待进一步验证)',
|
||||
'渠道策略:短视频为主 vs 社交媒体为主',
|
||||
'预算在内容制作和达人投放的分配比例',
|
||||
],
|
||||
},
|
||||
strategyChain: [
|
||||
{ id: 1, name: '目标', icon: Target, color: 'blue', status: 'completed', result: '提升品牌在Z世代用户中的认知度' },
|
||||
{ id: 2, name: '受众', icon: Users, color: 'cyan', status: 'completed', result: '18-25岁基层青年' },
|
||||
{ id: 3, name: '行为', icon: TrendingUp, color: 'green', status: 'completed', result: '主动参与话题讨论、生成UGC内容' },
|
||||
{ id: 4, name: '驱动', icon: MessageSquare, color: 'yellow', status: 'completed', result: '社交认同与自我表达' },
|
||||
{ id: 5, name: '叙事', icon: Megaphone, color: 'purple', status: 'completed', result: '真实、有态度的青年故事' },
|
||||
{ id: 6, name: '风险', icon: Shield, color: 'red', status: 'completed', result: 'UGC质量不稳定、话题热度维持' },
|
||||
],
|
||||
versionHistory: [
|
||||
{ version: 'V3', time: '2026-03-19 15:30', changes: ['叙事由"事件主导"改为"人物+事件结合"', '风险项已修正'] },
|
||||
{ version: 'V2', time: '2026-03-19 14:20', changes: ['受众由"全体青年"收窄为"基层青年"', '目标行为已明确'] },
|
||||
{ version: 'V1', time: '2026-03-19 10:30', changes: ['初始版本生成'] },
|
||||
],
|
||||
agentOpinions: [
|
||||
{ agentName: 'GoalAgent', role: '目标分析专家', opinion: '建议聚焦提升品牌认知度这一核心目标,短期内不追求直接转化', adopted: true, color: 'blue' },
|
||||
{ agentName: 'AudienceAgent', role: '受众分析专家', opinion: '核心受众应为18-25岁基层青年群体,注重"真实感"和"态度感"', adopted: true, color: 'cyan' },
|
||||
{ agentName: 'NarrativeAgent', role: '叙事策略专家', opinion: '应采用"真实、有态度的青年故事"叙事框架', adopted: true, color: 'purple' },
|
||||
{ agentName: 'RiskAgent', role: '风险评估专家', opinion: '需注意UGC质量不稳定和话题热度维持的风险', adopted: true, color: 'red' },
|
||||
],
|
||||
disputes: [
|
||||
{
|
||||
issue: '叙事角度选择',
|
||||
description: '人物叙事 vs 事件叙事',
|
||||
viewpoints: [
|
||||
{ agent: 'NarrativeAgent', opinion: '人物叙事更有情感穿透力' },
|
||||
{ agent: 'GoalAgent', opinion: '事件叙事更聚焦目标达成' },
|
||||
],
|
||||
status: '待确认',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const ResultDetail: React.FC<ResultDetailProps> = ({ taskId, onNavigate, onBack }) => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
|
||||
// 主题颜色类
|
||||
const cardBg = isDark ? 'bg-slate-800/30' : 'bg-white';
|
||||
const cardBorder = isDark ? 'border-blue-800/30' : 'border-gray-200';
|
||||
const textPrimary = isDark ? 'text-white' : 'text-gray-900';
|
||||
const textSecondary = isDark ? 'text-slate-400' : 'text-gray-600';
|
||||
const textMuted = isDark ? 'text-slate-500' : 'text-gray-500';
|
||||
const btnPrimary = isDark ? 'bg-blue-600 hover:bg-blue-500' : 'bg-red-600 hover:bg-red-500';
|
||||
const btnSecondary = isDark ? 'bg-slate-700/50 hover:bg-slate-600/50 border-blue-800/30 text-slate-300' : 'bg-gray-100 hover:bg-gray-200 border-gray-300 text-gray-700';
|
||||
const inputBg = isDark ? 'bg-slate-900/50' : 'bg-gray-50';
|
||||
|
||||
const [activeTab, setActiveTab] = useState<'conclusion' | 'process' | 'opinions' | 'history'>('conclusion');
|
||||
const [expandedSection, setExpandedSection] = useState<string | null>('goal');
|
||||
const [showReportMenu, setShowReportMenu] = useState(false);
|
||||
|
||||
const task = mockTaskDetail;
|
||||
|
||||
const toggleSection = (section: string) => {
|
||||
setExpandedSection(expandedSection === section ? null : section);
|
||||
};
|
||||
|
||||
const getColorClasses = (color: string) => {
|
||||
const colors: Record<string, { bg: string; text: string }> = {
|
||||
blue: { bg: isDark ? 'bg-blue-500/20' : 'bg-blue-50', text: isDark ? 'text-blue-400' : 'text-blue-600' },
|
||||
cyan: { bg: isDark ? 'bg-cyan-500/20' : 'bg-cyan-50', text: isDark ? 'text-cyan-400' : 'text-cyan-600' },
|
||||
green: { bg: isDark ? 'bg-green-500/20' : 'bg-green-50', text: isDark ? 'text-green-400' : 'text-green-600' },
|
||||
yellow: { bg: isDark ? 'bg-yellow-500/20' : 'bg-yellow-50', text: isDark ? 'text-yellow-400' : 'text-yellow-600' },
|
||||
purple: { bg: isDark ? 'bg-purple-500/20' : 'bg-purple-50', text: isDark ? 'text-purple-400' : 'text-purple-600' },
|
||||
red: { bg: isDark ? 'bg-red-500/20' : 'bg-red-50', text: isDark ? 'text-red-400' : 'text-red-600' },
|
||||
};
|
||||
return colors[color] || colors.blue;
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
if (onBack) {
|
||||
onBack();
|
||||
} else {
|
||||
onNavigate('result-management');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 顶部信息栏 */}
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-5`}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={handleBack}
|
||||
className={`flex items-center gap-2 px-3 py-2 ${btnSecondary} rounded-lg text-sm transition-colors`}
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
返回
|
||||
</button>
|
||||
<div className={`h-8 w-px ${isDark ? 'bg-blue-800/30' : 'bg-gray-200'}`} />
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-10 h-10 rounded-xl bg-blue-500/20 flex items-center justify-center`}>
|
||||
<FileText className="w-5 h-5 text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className={`${textPrimary} font-bold text-lg`}>{task.title}</h1>
|
||||
<p className={`${textMuted} text-sm`}>{task.subCategory} · {task.status === 'completed' ? '已完成' : '进行中'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<button className={`flex items-center gap-2 px-3 py-2 ${btnSecondary} rounded-lg text-sm transition-colors`}>
|
||||
<Edit3 className="w-4 h-4" />
|
||||
编辑
|
||||
</button>
|
||||
<button className={`flex items-center gap-2 px-3 py-2 ${btnSecondary} rounded-lg text-sm transition-colors`}>
|
||||
<History className="w-4 h-4" />
|
||||
历史版本
|
||||
</button>
|
||||
<button className={`flex items-center gap-2 px-3 py-2 ${btnSecondary} rounded-lg text-sm transition-colors`}>
|
||||
<Download className="w-4 h-4" />
|
||||
导出报告
|
||||
</button>
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setShowReportMenu(!showReportMenu)}
|
||||
className={`flex items-center gap-2 px-4 py-2 ${btnPrimary} text-white rounded-lg text-sm transition-colors`}
|
||||
>
|
||||
<Send className="w-4 h-4" />
|
||||
呈报
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
</button>
|
||||
{showReportMenu && (
|
||||
<div className={`absolute right-0 top-full mt-2 w-48 ${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl shadow-xl py-2 z-10`}>
|
||||
<button className={`w-full flex items-center gap-3 px-4 py-2.5 text-sm ${textPrimary} hover:${isDark ? 'bg-slate-700/50' : 'bg-gray-50'} transition-colors`}>
|
||||
<Share2 className="w-4 h-4" />
|
||||
分享链接
|
||||
</button>
|
||||
<button className={`w-full flex items-center gap-3 px-4 py-2.5 text-sm ${textPrimary} hover:${isDark ? 'bg-slate-700/50' : 'bg-gray-50'} transition-colors`}>
|
||||
<Printer className="w-4 h-4" />
|
||||
打印
|
||||
</button>
|
||||
<button className={`w-full flex items-center gap-3 px-4 py-2.5 text-sm ${textPrimary} hover:${isDark ? 'bg-slate-700/50' : 'bg-gray-50'} transition-colors`}>
|
||||
<Send className="w-4 h-4" />
|
||||
发送至邮箱
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 基本信息 */}
|
||||
<div className={`grid grid-cols-5 gap-4 p-4 ${inputBg} rounded-lg`}>
|
||||
<div className="text-center">
|
||||
<p className={`${textMuted} text-xs mb-1`}>使用场景</p>
|
||||
<p className={`${textPrimary} font-medium text-sm`}>{task.scene}</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className={`${textMuted} text-xs mb-1`}>运行时长</p>
|
||||
<p className={`${textPrimary} font-medium text-sm`}>{task.duration}</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className={`${textMuted} text-xs mb-1`}>参与智能体</p>
|
||||
<p className={`${textPrimary} font-medium text-sm`}>{task.agentCount}个</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className={`${textMuted} text-xs mb-1`}>当前版本</p>
|
||||
<p className={`${isDark ? 'text-cyan-400' : 'text-cyan-600'} font-medium text-sm`}>V{task.versionHistory.length}</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className={`${textMuted} text-xs mb-1`}>完成时间</p>
|
||||
<p className={`${textPrimary} font-medium text-sm`}>{task.completedAt}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab切换 */}
|
||||
<div className={`flex items-center gap-1 ${cardBg} p-1 rounded-lg w-fit`}>
|
||||
<button
|
||||
onClick={() => setActiveTab('conclusion')}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
activeTab === 'conclusion'
|
||||
? `${btnPrimary} text-white`
|
||||
: `${textSecondary} ${isDark ? 'hover:text-white hover:bg-slate-700/50' : 'hover:text-gray-900 hover:bg-gray-100'}`
|
||||
}`}
|
||||
>
|
||||
会商结论
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('process')}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
activeTab === 'process'
|
||||
? `${btnPrimary} text-white`
|
||||
: `${textSecondary} ${isDark ? 'hover:text-white hover:bg-slate-700/50' : 'hover:text-gray-900 hover:bg-gray-100'}`
|
||||
}`}
|
||||
>
|
||||
策略链
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('opinions')}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
activeTab === 'opinions'
|
||||
? `${btnPrimary} text-white`
|
||||
: `${textSecondary} ${isDark ? 'hover:text-white hover:bg-slate-700/50' : 'hover:text-gray-900 hover:bg-gray-100'}`
|
||||
}`}
|
||||
>
|
||||
观点汇总
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('history')}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
activeTab === 'history'
|
||||
? `${btnPrimary} text-white`
|
||||
: `${textSecondary} ${isDark ? 'hover:text-white hover:bg-slate-700/50' : 'hover:text-gray-900 hover:bg-gray-100'}`
|
||||
}`}
|
||||
>
|
||||
版本历史
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 内容区 */}
|
||||
{activeTab === 'conclusion' && (
|
||||
<div className="grid grid-cols-12 gap-6">
|
||||
{/* 会商结论 */}
|
||||
<div className="col-span-8 space-y-6">
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-5`}>
|
||||
<h3 className={`${textPrimary} font-semibold text-sm mb-4 flex items-center gap-2`}>
|
||||
<CheckCircle className={`w-4 h-4 ${isDark ? 'text-green-400' : 'text-green-600'}`} />
|
||||
本次会商结论
|
||||
</h3>
|
||||
<div className={`p-4 ${isDark ? 'bg-green-500/10 border-green-500/30' : 'bg-green-50 border-green-200'} rounded-lg`}>
|
||||
<p className={`${isDark ? 'text-green-300' : 'text-green-800'} leading-relaxed`}>{task.conclusion.main}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-5`}>
|
||||
<h3 className={`${textPrimary} font-semibold text-sm mb-4 flex items-center gap-2`}>
|
||||
<Check className={`w-4 h-4 ${isDark ? 'text-green-400' : 'text-green-600'}`} />
|
||||
推荐采用方向
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{task.conclusion.recommended.map((item, index) => (
|
||||
<div key={index} className={`flex items-center gap-3 p-3 ${isDark ? 'bg-green-500/10 border-green-500/30' : 'bg-green-50 border-green-200'} rounded-lg`}>
|
||||
<div className={`w-6 h-6 rounded-full ${isDark ? 'bg-green-500/20' : 'bg-green-100'} flex items-center justify-center`}>
|
||||
<Check className={`w-4 h-4 ${isDark ? 'text-green-400' : 'text-green-600'}`} />
|
||||
</div>
|
||||
<p className={`${isDark ? 'text-green-300' : 'text-green-800'} text-sm`}>{item}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-5`}>
|
||||
<h3 className={`${textPrimary} font-semibold text-sm mb-4 flex items-center gap-2`}>
|
||||
<X className={`w-4 h-4 ${isDark ? 'text-red-400' : 'text-red-600'}`} />
|
||||
不建议采用方向
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{task.conclusion.notRecommended.map((item, index) => (
|
||||
<div key={index} className={`flex items-center gap-3 p-3 ${isDark ? 'bg-red-500/10 border-red-500/30' : 'bg-red-50 border-red-200'} rounded-lg`}>
|
||||
<div className={`w-6 h-6 rounded-full ${isDark ? 'bg-red-500/20' : 'bg-red-100'} flex items-center justify-center`}>
|
||||
<X className={`w-4 h-4 ${isDark ? 'text-red-400' : 'text-red-600'}`} />
|
||||
</div>
|
||||
<p className={`${isDark ? 'text-red-300' : 'text-red-800'} text-sm`}>{item}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 侧边栏 */}
|
||||
<div className="col-span-4 space-y-6">
|
||||
<div className={`${cardBg} backdrop-blur-xl ${isDark ? 'border-yellow-800/30' : 'border-yellow-200'} rounded-xl p-5`}>
|
||||
<h3 className={`${textPrimary} font-semibold text-sm mb-4 flex items-center gap-2`}>
|
||||
<AlertCircle className={`w-4 h-4 ${isDark ? 'text-yellow-400' : 'text-yellow-600'}`} />
|
||||
待确认问题
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{task.conclusion.pendingIssues.map((item, index) => (
|
||||
<div key={index} className={`p-3 ${isDark ? 'bg-yellow-500/10 border-yellow-500/30' : 'bg-yellow-50 border-yellow-200'} rounded-lg`}>
|
||||
<p className={`${isDark ? 'text-yellow-300' : 'text-yellow-800'} text-sm`}>{item}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-5`}>
|
||||
<h3 className={`${textPrimary} font-semibold text-sm mb-4`}>操作</h3>
|
||||
<div className="space-y-2">
|
||||
<button className={`w-full flex items-center gap-3 px-4 py-3 ${btnSecondary} rounded-xl text-sm transition-colors`}>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
重新发起会商
|
||||
</button>
|
||||
<button className={`w-full flex items-center gap-3 px-4 py-3 ${btnPrimary} text-white rounded-xl text-sm transition-colors`}>
|
||||
<FileText className="w-4 h-4" />
|
||||
生成最终方案
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'process' && (
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-5`}>
|
||||
{/* 策略链可视化 */}
|
||||
<h3 className={`${textPrimary} font-semibold text-sm mb-4`}>策略链结果</h3>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
{task.strategyChain.map((item, index) => {
|
||||
const Icon = item.icon;
|
||||
const colors = getColorClasses(item.color);
|
||||
return (
|
||||
<React.Fragment key={item.id}>
|
||||
<div className="flex flex-col items-center">
|
||||
<div className={`w-14 h-14 rounded-xl bg-${item.color}-500/20 border-2 border-${item.color}-500/50 flex items-center justify-center mb-2`}>
|
||||
<Icon className={`w-7 h-7 text-${item.color}-400`} />
|
||||
</div>
|
||||
<span className={`text-${item.color}-400 text-sm font-medium`}>{item.name}</span>
|
||||
<span className={`${textMuted} text-xs mt-1 flex items-center gap-1`}>
|
||||
<CheckCircle className="w-3 h-3 text-green-400" /> 已确认
|
||||
</span>
|
||||
</div>
|
||||
{index < task.strategyChain.length - 1 && (
|
||||
<ArrowRight className="w-6 h-6 text-slate-600 mb-6" />
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 详细结果可折叠展开 */}
|
||||
<div className={`${inputBg} rounded-xl overflow-hidden border ${cardBorder}`}>
|
||||
{task.strategyChain.map((section) => {
|
||||
const Icon = section.icon;
|
||||
const colors = getColorClasses(section.color);
|
||||
const isExpanded = expandedSection === section.name;
|
||||
return (
|
||||
<div key={section.id} className={`border-b last:border-0 ${cardBorder}`}>
|
||||
<button
|
||||
onClick={() => toggleSection(section.name)}
|
||||
className="w-full flex items-center justify-between p-4 hover:bg-slate-700/20 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-10 h-10 rounded-lg bg-${section.color}-500/20 flex items-center justify-center`}>
|
||||
<Icon className={`w-5 h-5 text-${section.color}-400`} />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<p className={`${textPrimary} font-medium`}>{section.name}</p>
|
||||
<p className={`text-${section.color}-400 text-sm`}>{section.result}</p>
|
||||
</div>
|
||||
</div>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className={`w-5 h-5 ${textSecondary}`} />
|
||||
) : (
|
||||
<ChevronRight className={`w-5 h-5 ${textSecondary}`} />
|
||||
)}
|
||||
</button>
|
||||
{isExpanded && (
|
||||
<div className="px-4 pb-4 pt-0 ml-13">
|
||||
<div className={`p-4 ${cardBg} rounded-lg`}>
|
||||
<p className={`${textSecondary} text-sm`}>详细分析内容将在此处展示,包含智能体推理过程和依据。</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'opinions' && (
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-5`}>
|
||||
<h3 className={`${textPrimary} font-semibold text-sm mb-4 flex items-center gap-2`}>
|
||||
<Bot className={`w-4 h-4 ${isDark ? 'text-cyan-400' : 'text-cyan-600'}`} />
|
||||
各智能体观点汇总
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
{task.agentOpinions.map((agent) => {
|
||||
const colors = getColorClasses(agent.color);
|
||||
return (
|
||||
<div key={agent.agentName} className={`p-4 ${inputBg} ${cardBorder} rounded-xl`}>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-10 h-10 rounded-lg ${colors.bg} flex items-center justify-center`}>
|
||||
<Bot className={`w-5 h-5 ${colors.text}`} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className={`text-sm font-medium ${colors.text}`}>{agent.agentName}</p>
|
||||
{agent.adopted ? (
|
||||
<span className={`px-1.5 py-0.5 ${isDark ? 'bg-green-500/20 text-green-400' : 'bg-green-100 text-green-700'} rounded text-xs`}>
|
||||
已采纳
|
||||
</span>
|
||||
) : (
|
||||
<span className={`px-1.5 py-0.5 ${isDark ? 'bg-slate-500/20 text-slate-400' : 'bg-gray-200 text-gray-600'} rounded text-xs`}>
|
||||
未采纳
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className={`${textMuted} text-xs`}>{agent.role}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pl-13">
|
||||
<p className={`${textSecondary} text-sm`}>{agent.opinion}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* 分歧点 */}
|
||||
{task.disputes.length > 0 && (
|
||||
<div className={`p-4 ${isDark ? 'bg-yellow-500/10 border-yellow-500/30' : 'bg-yellow-50 border-yellow-200'} rounded-xl`}>
|
||||
<h4 className={`${textPrimary} font-medium text-sm mb-3 flex items-center gap-2`}>
|
||||
<AlertCircle className={`w-4 h-4 ${isDark ? 'text-yellow-400' : 'text-yellow-600'}`} />
|
||||
争议点
|
||||
</h4>
|
||||
{task.disputes.map((dispute, idx) => (
|
||||
<div key={idx} className="space-y-2">
|
||||
<p className={`${textPrimary} text-sm font-medium`}>{dispute.issue}</p>
|
||||
<p className={`${textMuted} text-xs`}>{dispute.description}</p>
|
||||
{dispute.viewpoints.map((vp, i) => (
|
||||
<div key={i} className={`p-2 ${isDark ? 'bg-yellow-500/5' : 'bg-yellow-50'} rounded-lg`}>
|
||||
<span className={`${textMuted} text-xs mr-2`}>{vp.agent}:</span>
|
||||
<span className={`${isDark ? 'text-yellow-300' : 'text-yellow-800'} text-xs`}>{vp.opinion}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'history' && (
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-5`}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className={`${textPrimary} font-semibold text-sm`}>版本迭代记录</h3>
|
||||
<button className="flex items-center gap-1 text-blue-400 text-sm hover:text-blue-300 transition-colors">
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
对比
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{task.versionHistory.map((version, index) => (
|
||||
<div
|
||||
key={version.version}
|
||||
className={`p-4 rounded-xl border ${
|
||||
index === 0
|
||||
? isDark ? 'bg-cyan-500/10 border-cyan-500/30' : 'bg-cyan-50 border-cyan-200'
|
||||
: `${inputBg} border-slate-700/30`
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className={`font-bold ${
|
||||
index === 0 ? (isDark ? 'text-cyan-400' : 'text-cyan-600') : textSecondary
|
||||
}`}>
|
||||
{version.version}
|
||||
</span>
|
||||
<span className={`${textMuted} text-xs`}>{version.time}</span>
|
||||
</div>
|
||||
<ul className="space-y-1">
|
||||
{version.changes.map((change, i) => (
|
||||
<li key={i} className={`${textPrimary} text-sm flex items-start gap-2`}>
|
||||
<span className={textSecondary}>•</span>
|
||||
{change}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResultDetail;
|
||||
@@ -0,0 +1,551 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
FileText,
|
||||
Search,
|
||||
Filter,
|
||||
Plus,
|
||||
Download,
|
||||
Edit3,
|
||||
Send,
|
||||
Clock,
|
||||
CheckCircle,
|
||||
AlertCircle,
|
||||
ChevronRight,
|
||||
Target,
|
||||
Brain,
|
||||
TrendingUp,
|
||||
Compass,
|
||||
Database,
|
||||
MoreHorizontal,
|
||||
FolderOpen,
|
||||
Folder,
|
||||
BarChart3,
|
||||
} from 'lucide-react';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { PageType } from '../components/Sidebar';
|
||||
|
||||
type ResultCategory = 'planning' | 'mnty' | 'effect-pg' | 'xk' | 'dq-research' | 'other';
|
||||
|
||||
interface ResultTask {
|
||||
id: string;
|
||||
title: string;
|
||||
category: ResultCategory;
|
||||
subCategory: string;
|
||||
status: 'completed' | 'pending' | 'draft';
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
reportType: 'strategy' | 'consult' | 'analysis' | 'summary';
|
||||
agentCount: number;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface ResultManagementProps {
|
||||
onNavigate: (page: PageType, taskId?: string) => void;
|
||||
initialCategory?: ResultCategory;
|
||||
}
|
||||
|
||||
// 任务类别配置
|
||||
const categoryConfig: Record<ResultCategory, { label: string; icon: React.ElementType; color: string; subCategories: string[] }> = {
|
||||
'planning': {
|
||||
label: '规划任务',
|
||||
icon: Target,
|
||||
color: 'blue',
|
||||
subCategories: ['目标分析', '品牌策略', '内容规划', '渠道规划', '叙事策略']
|
||||
},
|
||||
'mnty': {
|
||||
label: 'MNTY任务',
|
||||
icon: Brain,
|
||||
color: 'purple',
|
||||
subCategories: ['机制研究', '方案设计', 'ZSTY分析']
|
||||
},
|
||||
'effect-pg': {
|
||||
label: '效果PG任务',
|
||||
icon: TrendingUp,
|
||||
color: 'amber',
|
||||
subCategories: ['行为PG', '效果评估', '信息PG', '账号PG']
|
||||
},
|
||||
'xk': {
|
||||
label: 'XK任务',
|
||||
icon: Compass,
|
||||
color: 'cyan',
|
||||
subCategories: ['任务JC', '任务调度']
|
||||
},
|
||||
'dq-research': {
|
||||
label: 'DQ研究类任务',
|
||||
icon: Database,
|
||||
color: 'green',
|
||||
subCategories: ['受众分析', '知识库研究', '趋势研究']
|
||||
},
|
||||
'other': {
|
||||
label: '其他类任务',
|
||||
icon: MoreHorizontal,
|
||||
color: 'slate',
|
||||
subCategories: ['通用分析', '临时任务', '其他']
|
||||
}
|
||||
};
|
||||
|
||||
// 模拟任务数据
|
||||
const mockTasks: ResultTask[] = [
|
||||
{
|
||||
id: 'task-001',
|
||||
title: '五四青年节宣传活动策略',
|
||||
category: 'planning',
|
||||
subCategory: '叙事策略',
|
||||
status: 'completed',
|
||||
createdAt: '2026-03-19 10:30',
|
||||
updatedAt: '2026-03-19 15:30',
|
||||
reportType: 'strategy',
|
||||
agentCount: 6,
|
||||
description: '针对五四青年节的品牌传播策略方案'
|
||||
},
|
||||
{
|
||||
id: 'task-002',
|
||||
title: '品牌年轻化受众分析',
|
||||
category: 'dq-research',
|
||||
subCategory: '受众分析',
|
||||
status: 'completed',
|
||||
createdAt: '2026-03-18 14:20',
|
||||
updatedAt: '2026-03-18 16:45',
|
||||
reportType: 'analysis',
|
||||
agentCount: 3,
|
||||
description: '分析Z世代用户的消费行为和品牌偏好'
|
||||
},
|
||||
{
|
||||
id: 'task-003',
|
||||
title: 'UGC激励机制设计',
|
||||
category: 'effect-pg',
|
||||
subCategory: '效果评估',
|
||||
status: 'completed',
|
||||
createdAt: '2026-03-17 09:00',
|
||||
updatedAt: '2026-03-17 11:20',
|
||||
reportType: 'strategy',
|
||||
agentCount: 4,
|
||||
description: '设计用户生成内容激励机制方案'
|
||||
},
|
||||
{
|
||||
id: 'task-004',
|
||||
title: '短视频平台效果预测',
|
||||
category: 'mnty',
|
||||
subCategory: '方案设计',
|
||||
status: 'pending',
|
||||
createdAt: '2026-03-20 08:00',
|
||||
updatedAt: '2026-03-20 08:00',
|
||||
reportType: 'consult',
|
||||
agentCount: 5,
|
||||
description: '预测不同短视频平台的传播效果'
|
||||
},
|
||||
{
|
||||
id: 'task-005',
|
||||
title: '活动传播风险评估',
|
||||
category: 'planning',
|
||||
subCategory: '目标分析',
|
||||
status: 'draft',
|
||||
createdAt: '2026-03-20 10:00',
|
||||
updatedAt: '2026-03-20 10:30',
|
||||
reportType: 'analysis',
|
||||
agentCount: 2,
|
||||
description: '评估五四活动可能面临的传播风险'
|
||||
},
|
||||
{
|
||||
id: 'task-006',
|
||||
title: '竞品营销策略研究',
|
||||
category: 'dq-research',
|
||||
subCategory: '趋势研究',
|
||||
status: 'completed',
|
||||
createdAt: '2026-03-16 13:00',
|
||||
updatedAt: '2026-03-16 18:00',
|
||||
reportType: 'analysis',
|
||||
agentCount: 3,
|
||||
description: '分析主要竞品的营销策略和效果'
|
||||
},
|
||||
];
|
||||
|
||||
const ResultManagement: React.FC<ResultManagementProps> = ({ onNavigate, initialCategory = 'planning' }) => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
|
||||
// 主题颜色类
|
||||
const cardBg = isDark ? 'bg-slate-800/30' : 'bg-white';
|
||||
const cardBorder = isDark ? 'border-blue-800/30' : 'border-gray-200';
|
||||
const textPrimary = isDark ? 'text-white' : 'text-gray-900';
|
||||
const textSecondary = isDark ? 'text-slate-400' : 'text-gray-600';
|
||||
const textMuted = isDark ? 'text-slate-500' : 'text-gray-500';
|
||||
const btnPrimary = isDark ? 'bg-blue-600 hover:bg-blue-500' : 'bg-red-600 hover:bg-red-500';
|
||||
const btnSecondary = isDark ? 'bg-slate-700/50 hover:bg-slate-600/50 border-blue-800/30 text-slate-300' : 'bg-gray-100 hover:bg-gray-200 border-gray-300 text-gray-700';
|
||||
const inputBg = isDark ? 'bg-slate-900/50' : 'bg-gray-50';
|
||||
const inputBorder = isDark ? 'border-blue-800/30' : 'border-gray-300';
|
||||
|
||||
// 状态
|
||||
const [selectedCategory, setSelectedCategory] = useState<ResultCategory>(initialCategory);
|
||||
const [selectedSubCategory, setSelectedSubCategory] = useState<string | null>(null);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState<'all' | 'completed' | 'pending' | 'draft'>('all');
|
||||
const [expandedCategories, setExpandedCategories] = useState<Set<ResultCategory>>(
|
||||
new Set([initialCategory])
|
||||
);
|
||||
|
||||
// 切换分类展开状态
|
||||
const toggleCategory = (category: ResultCategory) => {
|
||||
setExpandedCategories(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(category)) {
|
||||
next.delete(category);
|
||||
} else {
|
||||
next.add(category);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
// 过滤任务
|
||||
const filteredTasks = mockTasks.filter(task => {
|
||||
const matchesCategory = task.category === selectedCategory;
|
||||
const matchesSubCategory = !selectedSubCategory || task.subCategory === selectedSubCategory;
|
||||
const matchesSearch = !searchQuery ||
|
||||
task.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
task.description.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
const matchesStatus = statusFilter === 'all' || task.status === statusFilter;
|
||||
return matchesCategory && matchesSubCategory && matchesSearch && matchesStatus;
|
||||
});
|
||||
|
||||
// 获取状态样式
|
||||
const getStatusStyle = (status: ResultTask['status']) => {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return {
|
||||
bg: isDark ? 'bg-green-500/20 text-green-400' : 'bg-green-100 text-green-700',
|
||||
icon: CheckCircle,
|
||||
label: '已完成'
|
||||
};
|
||||
case 'pending':
|
||||
return {
|
||||
bg: isDark ? 'bg-yellow-500/20 text-yellow-400' : 'bg-yellow-100 text-yellow-700',
|
||||
icon: Clock,
|
||||
label: '进行中'
|
||||
};
|
||||
case 'draft':
|
||||
return {
|
||||
bg: isDark ? 'bg-slate-500/20 text-slate-400' : 'bg-gray-100 text-gray-700',
|
||||
icon: Edit3,
|
||||
label: '草稿'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// 获取报告类型样式
|
||||
const getReportTypeStyle = (type: ResultTask['reportType']) => {
|
||||
const styles = {
|
||||
'strategy': { color: 'blue', label: '策略报告' },
|
||||
'consult': { color: 'purple', label: '会商报告' },
|
||||
'analysis': { color: 'cyan', label: '分析报告' },
|
||||
'summary': { color: 'green', label: '总结报告' }
|
||||
};
|
||||
return styles[type];
|
||||
};
|
||||
|
||||
// 查看任务详情
|
||||
const handleViewDetail = (taskId: string) => {
|
||||
onNavigate('result-detail', taskId);
|
||||
};
|
||||
|
||||
const currentCategoryConfig = categoryConfig[selectedCategory];
|
||||
const CategoryIcon = currentCategoryConfig.icon;
|
||||
|
||||
return (
|
||||
<div className="h-full flex gap-6">
|
||||
{/* 左侧分类导航 */}
|
||||
<div className={`w-72 flex-shrink-0 ${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-4 overflow-hidden flex flex-col`}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className={`${textPrimary} font-semibold text-base`}>结果管理</h2>
|
||||
<button className={`p-1.5 rounded-lg ${btnSecondary}`}>
|
||||
<Plus className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 分类列表 */}
|
||||
<div className="flex-1 overflow-y-auto space-y-2 custom-scrollbar">
|
||||
{(Object.entries(categoryConfig) as [ResultCategory, typeof categoryConfig[ResultCategory]][]).map(([key, config]) => {
|
||||
const Icon = config.icon;
|
||||
const isExpanded = expandedCategories.has(key);
|
||||
const isActive = selectedCategory === key;
|
||||
|
||||
return (
|
||||
<div key={key} className="space-y-1">
|
||||
{/* 一级分类 */}
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedCategory(key);
|
||||
setSelectedSubCategory(null);
|
||||
if (!expandedCategories.has(key)) {
|
||||
toggleCategory(key);
|
||||
}
|
||||
}}
|
||||
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg transition-all text-left ${
|
||||
isActive
|
||||
? isDark
|
||||
? 'bg-blue-600/20 text-blue-400 border border-blue-500/30'
|
||||
: 'bg-red-50 text-red-600 border border-red-200'
|
||||
: isDark
|
||||
? 'text-slate-400 hover:text-white hover:bg-slate-700/50'
|
||||
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<FolderOpen className={`w-4 h-4 ${isActive ? '' : 'opacity-60'}`} />
|
||||
) : (
|
||||
<Folder className={`w-4 h-4 ${isActive ? '' : 'opacity-60'}`} />
|
||||
)}
|
||||
<span className="text-sm font-medium flex-1">{config.label}</span>
|
||||
<ChevronRight className={`w-4 h-4 transition-transform ${isExpanded ? 'rotate-90' : ''} ${isActive ? '' : 'opacity-60'}`} />
|
||||
</button>
|
||||
|
||||
{/* 二级分类 */}
|
||||
{isExpanded && (
|
||||
<div className="ml-4 pl-4 border-l space-y-1">
|
||||
{/* 全部 */}
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedCategory(key);
|
||||
setSelectedSubCategory(null);
|
||||
}}
|
||||
className={`w-full flex items-center gap-2 px-3 py-2 rounded-lg transition-all text-left ${
|
||||
selectedCategory === key && !selectedSubCategory
|
||||
? isDark
|
||||
? 'bg-blue-500/20 text-blue-400'
|
||||
: 'bg-red-100 text-red-600'
|
||||
: isDark
|
||||
? 'text-slate-500 hover:text-slate-300'
|
||||
: 'text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
<BarChart3 className="w-3.5 h-3.5" />
|
||||
<span className="text-xs font-medium">全部</span>
|
||||
<span className={`ml-auto px-1.5 py-0.5 rounded text-xs ${
|
||||
isDark ? 'bg-slate-700 text-slate-400' : 'bg-gray-200 text-gray-500'
|
||||
}`}>
|
||||
{mockTasks.filter(t => t.category === key).length}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* 各子分类 */}
|
||||
{config.subCategories.map(sub => {
|
||||
const count = mockTasks.filter(t => t.category === key && t.subCategory === sub).length;
|
||||
if (count === 0) return null;
|
||||
return (
|
||||
<button
|
||||
key={sub}
|
||||
onClick={() => {
|
||||
setSelectedCategory(key);
|
||||
setSelectedSubCategory(sub);
|
||||
}}
|
||||
className={`w-full flex items-center gap-2 px-3 py-2 rounded-lg transition-all text-left ${
|
||||
selectedCategory === key && selectedSubCategory === sub
|
||||
? isDark
|
||||
? 'bg-blue-500/20 text-blue-400'
|
||||
: 'bg-red-100 text-red-600'
|
||||
: isDark
|
||||
? 'text-slate-500 hover:text-slate-300'
|
||||
: 'text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
<div className={`w-1.5 h-1.5 rounded-full ${
|
||||
isActive ? `bg-${config.color}-400` : (isDark ? 'bg-slate-600' : 'bg-gray-300')
|
||||
}`} />
|
||||
<span className="text-xs">{sub}</span>
|
||||
<span className={`ml-auto px-1.5 py-0.5 rounded text-xs ${
|
||||
isDark ? 'bg-slate-700 text-slate-400' : 'bg-gray-200 text-gray-500'
|
||||
}`}>
|
||||
{count}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧任务列表 */}
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
{/* 顶部工具栏 */}
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-4 mb-4`}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-10 h-10 rounded-xl bg-${currentCategoryConfig.color}-500/20 flex items-center justify-center`}>
|
||||
<CategoryIcon className={`w-5 h-5 text-${currentCategoryConfig.color}-400`} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className={`${textPrimary} font-semibold`}>
|
||||
{selectedSubCategory || currentCategoryConfig.label}
|
||||
</h2>
|
||||
<p className={`${textMuted} text-xs`}>
|
||||
共 {filteredTasks.length} 个结果
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button className={`flex items-center gap-2 px-3 py-2 rounded-lg ${btnSecondary} text-sm`}>
|
||||
<Download className="w-4 h-4" />
|
||||
批量导出
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 搜索和筛选 */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`flex-1 relative ${inputBg} rounded-lg`}>
|
||||
<Search className={`absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 ${textMuted}`} />
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="搜索任务标题或描述..."
|
||||
className={`w-full pl-10 pr-4 py-2.5 rounded-lg text-sm bg-transparent focus:outline-none ${
|
||||
isDark ? 'text-white placeholder-slate-500' : 'text-gray-800 placeholder-gray-400'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value as typeof statusFilter)}
|
||||
className={`px-3 py-2.5 rounded-lg text-sm ${inputBg} ${inputBorder} focus:outline-none ${
|
||||
isDark ? 'text-slate-300' : 'text-gray-700'
|
||||
}`}
|
||||
>
|
||||
<option value="all">全部状态</option>
|
||||
<option value="completed">已完成</option>
|
||||
<option value="pending">进行中</option>
|
||||
<option value="draft">草稿</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 任务列表 */}
|
||||
<div className={`flex-1 ${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl overflow-hidden`}>
|
||||
{filteredTasks.length === 0 ? (
|
||||
<div className="h-full flex flex-col items-center justify-center">
|
||||
<FileText className={`w-16 h-16 ${textMuted} mb-4`} />
|
||||
<p className={`${textSecondary} text-lg`}>暂无结果</p>
|
||||
<p className={`${textMuted} text-sm mt-1`}>在对应任务中生成结果后,将在此显示</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full overflow-y-auto custom-scrollbar">
|
||||
<table className="w-full">
|
||||
<thead className={`sticky top-0 ${isDark ? 'bg-slate-800/80' : 'bg-gray-50'}`}>
|
||||
<tr className={`text-xs ${textMuted} border-b ${cardBorder}`}>
|
||||
<th className="text-left font-medium px-4 py-3">任务名称</th>
|
||||
<th className="text-left font-medium px-4 py-3">报告类型</th>
|
||||
<th className="text-left font-medium px-4 py-3">智能体数</th>
|
||||
<th className="text-left font-medium px-4 py-3">状态</th>
|
||||
<th className="text-left font-medium px-4 py-3">更新时间</th>
|
||||
<th className="text-left font-medium px-4 py-3">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredTasks.map((task) => {
|
||||
const statusInfo = getStatusStyle(task.status);
|
||||
const StatusIcon = statusInfo.icon;
|
||||
const reportTypeInfo = getReportTypeStyle(task.reportType);
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={task.id}
|
||||
className={`border-b cursor-pointer hover:${
|
||||
isDark ? 'bg-slate-700/30' : 'bg-gray-50'
|
||||
} transition-colors ${cardBorder}`}
|
||||
onClick={() => handleViewDetail(task.id)}
|
||||
>
|
||||
<td className="px-4 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-10 h-10 rounded-lg bg-${reportTypeInfo.color}-500/20 flex items-center justify-center flex-shrink-0`}>
|
||||
<FileText className={`w-5 h-5 text-${reportTypeInfo.color}-400`} />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className={`${textPrimary} text-sm font-medium truncate`}>{task.title}</p>
|
||||
<p className={`${textMuted} text-xs truncate`}>{task.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-4">
|
||||
<span className={`px-2 py-1 rounded-lg text-xs font-medium ${
|
||||
isDark ? `bg-${reportTypeInfo.color}-500/20 text-${reportTypeInfo.color}-400` : `bg-${reportTypeInfo.color}-100 text-${reportTypeInfo.color}-700`
|
||||
}`}>
|
||||
{reportTypeInfo.label}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-4">
|
||||
<span className={`${textSecondary} text-sm`}>{task.agentCount}</span>
|
||||
</td>
|
||||
<td className="px-4 py-4">
|
||||
<span className={`inline-flex items-center gap-1.5 px-2 py-1 rounded-lg text-xs font-medium ${statusInfo.bg}`}>
|
||||
<StatusIcon className="w-3 h-3" />
|
||||
{statusInfo.label}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-4">
|
||||
<span className={`${textMuted} text-sm`}>{task.updatedAt}</span>
|
||||
</td>
|
||||
<td className="px-4 py-4" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => handleViewDetail(task.id)}
|
||||
className={`p-1.5 rounded-lg hover:${isDark ? 'bg-slate-700' : 'bg-gray-100'} transition-colors`}
|
||||
title="查看详情"
|
||||
>
|
||||
<FileText className={`w-4 h-4 ${textSecondary}`} />
|
||||
</button>
|
||||
<button
|
||||
className={`p-1.5 rounded-lg hover:${isDark ? 'bg-slate-700' : 'bg-gray-100'} transition-colors`}
|
||||
title="编辑"
|
||||
>
|
||||
<Edit3 className={`w-4 h-4 ${textSecondary}`} />
|
||||
</button>
|
||||
<button
|
||||
className={`p-1.5 rounded-lg hover:${isDark ? 'bg-slate-700' : 'bg-gray-100'} transition-colors`}
|
||||
title="下载"
|
||||
>
|
||||
<Download className={`w-4 h-4 ${textSecondary}`} />
|
||||
</button>
|
||||
<button
|
||||
className={`p-1.5 rounded-lg hover:${isDark ? 'bg-slate-700' : 'bg-gray-100'} transition-colors`}
|
||||
title="呈报"
|
||||
>
|
||||
<Send className={`w-4 h-4 ${textSecondary}`} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 自定义滚动条样式 */}
|
||||
<style>{`
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: ${isDark ? 'rgba(148, 163, 184, 0.3)' : 'rgba(0, 0, 0, 0.15)'};
|
||||
border-radius: 3px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: ${isDark ? 'rgba(148, 163, 184, 0.5)' : 'rgba(0, 0, 0, 0.25)'};
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResultManagement;
|
||||
@@ -0,0 +1,556 @@
|
||||
import React, { useState } from 'react';
|
||||
import { ArrowLeft, Save, RotateCcw, Play, Pause, Settings, FileText, GitBranch, ListChecks, Shield, Bot, Clock, CheckCircle, X, Plus, ChevronDown, AlertTriangle } from 'lucide-react';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
|
||||
interface SceneDetailProps {
|
||||
sceneId: string | null;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
interface FlowNode {
|
||||
id: number;
|
||||
name: string;
|
||||
agent: string;
|
||||
status: 'completed' | 'active' | 'pending';
|
||||
inputs: number;
|
||||
outputs: number;
|
||||
inputSource: string[];
|
||||
outputFields: string[];
|
||||
releaseCondition: string;
|
||||
rollbackNode: string;
|
||||
allowParallel: boolean;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const SceneDetail: React.FC<SceneDetailProps> = ({ sceneId, onBack }) => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
|
||||
// 主题颜色类
|
||||
const cardBg = isDark ? 'bg-slate-800/30' : 'bg-white';
|
||||
const cardBorder = isDark ? 'border-blue-800/30' : 'border-red-200';
|
||||
const textPrimary = isDark ? 'text-white' : 'text-gray-900';
|
||||
const textSecondary = isDark ? 'text-slate-400' : 'text-gray-600';
|
||||
const textMuted = isDark ? 'text-slate-500' : 'text-gray-500';
|
||||
const btnSecondary = isDark ? 'bg-slate-700/50 hover:bg-slate-600/50 border-blue-800/30 text-slate-300' : 'bg-gray-100 hover:bg-gray-200 border-gray-300 text-gray-700';
|
||||
const btnPrimary = isDark ? 'bg-blue-600 hover:bg-blue-500' : 'bg-red-600 hover:bg-red-500';
|
||||
const inputBg = isDark ? 'bg-slate-900/50' : 'bg-gray-50';
|
||||
const inputBorder = isDark ? 'border-blue-800/30' : 'border-gray-300';
|
||||
|
||||
const [activeTab, setActiveTab] = useState('flow');
|
||||
const [selectedNode, setSelectedNode] = useState<FlowNode | null>(null);
|
||||
|
||||
const tabs = [
|
||||
{ id: 'basic', label: '基础设置', icon: FileText },
|
||||
{ id: 'flow', label: '流程编排', icon: GitBranch },
|
||||
{ id: 'template', label: '输入输出模板', icon: ListChecks },
|
||||
{ id: 'rules', label: '运行规则', icon: Shield },
|
||||
];
|
||||
|
||||
const flowNodes: FlowNode[] = [
|
||||
{ id: 1, name: '目标分析', agent: 'GoalAgent', status: 'completed', inputs: 2, outputs: 3, inputSource: ['task_background', 'primary_goal_hint'], outputFields: ['goal_model.primary_goal', 'goal_model.secondary_goal'], releaseCondition: '目标明确且可量化', rollbackNode: '无', allowParallel: false, description: '分析并确定宣传的核心目标' },
|
||||
{ id: 2, name: '受众分析', agent: 'AudienceAgent', status: 'completed', inputs: 3, outputs: 4, inputSource: ['goal_model', 'task_background'], outputFields: ['audience_model.core_audience', 'audience_model.secondary_audience'], releaseCondition: '核心受众明确', rollbackNode: '目标分析', allowParallel: false, description: '分析目标受众特征和需求' },
|
||||
{ id: 3, name: '行为分析', agent: 'BehaviorAgent', status: 'active', inputs: 4, outputs: 3, inputSource: ['audience_model', 'goal_model'], outputFields: ['behavior_model.target_behavior', 'behavior_model.conversion_path'], releaseCondition: '目标行为明确', rollbackNode: '受众分析', allowParallel: false, description: '设计目标受众的预期行为' },
|
||||
{ id: 4, name: '驱动分析', agent: 'DriverAgent', status: 'pending', inputs: 3, outputs: 2, inputSource: ['behavior_model', 'audience_model'], outputFields: ['driver_model.primary_driver', 'driver_model.secondary_driver'], releaseCondition: '核心驱动明确', rollbackNode: '行为分析', allowParallel: true, description: '分析影响受众行为的关键驱动因素' },
|
||||
{ id: 5, name: '叙事分析', agent: 'NarrativeAgent', status: 'pending', inputs: 2, outputs: 4, inputSource: ['driver_model', 'audience_model'], outputFields: ['narrative_model.core_narrative', 'narrative_model.message_points'], releaseCondition: '叙事框架明确', rollbackNode: '驱动分析', allowParallel: false, description: '设计传播叙事策略' },
|
||||
{ id: 6, name: '逻辑链收敛', agent: 'LogicChainAgent', status: 'pending', inputs: 4, outputs: 2, inputSource: ['goal_model', 'audience_model', 'behavior_model', 'narrative_model'], outputFields: ['strategy_chain.logic_statement', 'strategy_chain.consistency_score'], releaseCondition: '逻辑链校验通过', rollbackNode: '叙事分析', allowParallel: false, description: '验证策略链完整性和一致性' },
|
||||
{ id: 7, name: '风险评估', agent: 'RiskAgent', status: 'pending', inputs: 2, outputs: 3, inputSource: ['strategy_chain', 'task_constraints'], outputFields: ['risk_model.risk_items', 'risk_model.mitigation_suggestions'], releaseCondition: '高风险项已处理', rollbackNode: '逻辑链收敛', allowParallel: true, description: '评估策略执行风险' },
|
||||
{ id: 8, name: '方案生成', agent: 'DraftAgent', status: 'pending', inputs: 3, outputs: 5, inputSource: ['strategy_chain', 'narrative_model', 'risk_model'], outputFields: ['final_plan', 'execution_timeline', 'evaluation_metrics'], releaseCondition: '方案完整可执行', rollbackNode: '风险评估', allowParallel: false, description: '生成最终宣传方案' },
|
||||
];
|
||||
|
||||
const sceneSummary = {
|
||||
flowNodesCount: 8,
|
||||
boundAgentsCount: 8,
|
||||
serialNodes: 6,
|
||||
parallelNodes: 2,
|
||||
logicChainEnabled: true,
|
||||
lastPublishedTime: '2026-03-18 14:30',
|
||||
};
|
||||
|
||||
const inputTemplate = [
|
||||
{ name: '任务标题', type: 'STRING', required: true },
|
||||
{ name: '任务背景', type: 'TEXT', required: true },
|
||||
{ name: '上级要求', type: 'TEXT', required: false },
|
||||
{ name: '时间要求', type: 'DATE', required: true },
|
||||
{ name: '资源条件', type: 'JSON', required: false },
|
||||
{ name: '风险边界', type: 'TEXT', required: false },
|
||||
];
|
||||
|
||||
const outputTemplate = {
|
||||
middle: ['goal_model', 'audience_model', 'behavior_model', 'driver_model', 'narrative_model', 'strategy_chain'],
|
||||
final: ['讨论结果摘要', '最终策略链', '正式方案', '风险说明', '评估指标'],
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 顶部导航 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={onBack}
|
||||
className={`p-2 ${isDark ? 'hover:bg-slate-800/50' : 'hover:bg-gray-100'} rounded-lg transition-colors`}
|
||||
>
|
||||
<ArrowLeft className={`w-5 h-5 ${textSecondary}`} />
|
||||
</button>
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className={`text-2xl font-bold ${textPrimary}`}>业务链条配置详情</h1>
|
||||
<span className="px-2.5 py-1 bg-green-500/20 text-green-400 rounded-full text-xs font-medium">
|
||||
启用
|
||||
</span>
|
||||
</div>
|
||||
<p className={`${textSecondary} text-sm mt-1`}></p>
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="flex items-center gap-3">
|
||||
<button className={`flex items-center gap-2 px-4 py-2.5 ${btnSecondary} rounded-lg transition-colors`}>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
<span className="text-sm">重置</span>
|
||||
</button>
|
||||
<button className={`flex items-center gap-2 px-4 py-2.5 ${btnPrimary} ${textPrimary} rounded-lg transition-colors`}>
|
||||
<Save className="w-4 h-4" />
|
||||
<span className="text-sm">保存配置</span>
|
||||
</button>
|
||||
</div> */}
|
||||
</div>
|
||||
|
||||
{/* Tab切换 */}
|
||||
{/* <div className={`flex items-center gap-2 border-b ${cardBorder}`}>
|
||||
{tabs.map((tab) => {
|
||||
const Icon = tab.icon;
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`flex items-center gap-2 px-4 py-3 border-b-2 transition-colors ${
|
||||
activeTab === tab.id
|
||||
? 'border-blue-500 text-blue-400'
|
||||
: `border-transparent ${textSecondary} hover:${textPrimary}`
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
<span className="text-sm font-medium">{tab.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div> */}
|
||||
|
||||
{/* 内容区 */}
|
||||
<div className="grid grid-cols-4 gap-6">
|
||||
{/* 主内容区 */}
|
||||
<div className="col-span-3">
|
||||
{activeTab === 'basic' && (
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
{/* 左侧表单区 */}
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-6 space-y-4`}>
|
||||
<h3 className={`${textPrimary} font-semibold mb-4`}>基础信息</h3>
|
||||
<div>
|
||||
<label className={`block ${textSecondary} text-sm mb-2`}>场景名称</label>
|
||||
<input
|
||||
type="text"
|
||||
defaultValue="宣传策略生成"
|
||||
className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm focus:outline-none focus:border-blue-500/50`}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={`block ${textSecondary} text-sm mb-2`}>场景编码</label>
|
||||
<input
|
||||
type="text"
|
||||
defaultValue="SCENE_001"
|
||||
className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm focus:outline-none focus:border-blue-500/50`}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={`block ${textSecondary} text-sm mb-2`}>场景说明</label>
|
||||
<textarea
|
||||
rows={3}
|
||||
defaultValue="用于生成完整的宣传策略方案,包含目标、受众、行为、驱动、叙事等维度的分析"
|
||||
className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm focus:outline-none focus:border-blue-500/50 resize-none`}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={`block ${textSecondary} text-sm mb-2`}>场景分类</label>
|
||||
<select className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm focus:outline-none focus:border-blue-500/50`}>
|
||||
<option>策略生成</option>
|
||||
<option>方案推演</option>
|
||||
<option>活动复盘</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧说明区 */}
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-6 space-y-4`}>
|
||||
<h3 className={`${textPrimary} font-semibold mb-4`}>场景说明</h3>
|
||||
<div className="p-4 bg-blue-500/10 border border-blue-500/30 rounded-lg">
|
||||
<p className="text-blue-400 text-sm font-medium mb-2">场景简介</p>
|
||||
<p className={`${textSecondary} text-sm`}>完整的宣传策略生成流程,覆盖从目标分析到方案输出的全链路。</p>
|
||||
</div>
|
||||
<div className={`p-4 ${inputBg} ${inputBorder} rounded-lg`}>
|
||||
<p className={`${textSecondary} text-sm font-medium mb-2`}>最近修改记录</p>
|
||||
<p className={`${textSecondary} text-sm`}>2026-03-18 14:30 由 张策略 修改</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="p-3 bg-cyan-500/10 border border-cyan-500/30 rounded-lg text-center">
|
||||
<p className="text-cyan-400 text-2xl font-bold">12</p>
|
||||
<p className={`${textSecondary} text-xs`}>关联任务</p>
|
||||
</div>
|
||||
<div className="p-3 bg-purple-500/10 border border-purple-500/30 rounded-lg text-center">
|
||||
<p className="text-purple-400 text-2xl font-bold">8</p>
|
||||
<p className={`${textSecondary} text-xs`}>关联智能体</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`flex items-center justify-between py-3 border-t ${cardBorder}`}>
|
||||
<span className={`${textSecondary} text-sm`}>设为默认场景</span>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" className="sr-only peer" defaultChecked />
|
||||
<div className="w-11 h-6 bg-slate-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'flow' && (
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-6`}>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h3 className={`${textPrimary} font-semibold`}>流程编排</h3>
|
||||
<button className="flex items-center gap-2 px-3 py-1.5 bg-blue-600/20 hover:bg-blue-600/30 border border-blue-500/30 text-blue-400 rounded-lg text-sm transition-colors">
|
||||
<Plus className="w-4 h-4" />
|
||||
添加节点
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 流程图区域 */}
|
||||
<div className="relative">
|
||||
{/* 流程节点 */}
|
||||
<div className="space-y-4">
|
||||
{flowNodes.map((node, index) => (
|
||||
<div key={node.id} className="relative">
|
||||
<div className="flex items-center gap-4">
|
||||
{/* 连接线 */}
|
||||
{index > 0 && (
|
||||
<div className="absolute left-6 -top-4 w-0.5 h-4 bg-blue-500/50" />
|
||||
)}
|
||||
|
||||
{/* 节点卡片 */}
|
||||
<button
|
||||
onClick={() => setSelectedNode(node)}
|
||||
className={`flex-1 flex items-center gap-4 p-4 rounded-xl border-2 transition-all text-left ${
|
||||
selectedNode?.id === node.id
|
||||
? 'bg-blue-500/20 border-blue-500 shadow-lg shadow-blue-500/20'
|
||||
: node.status === 'completed'
|
||||
? 'bg-green-500/10 border-green-500/30 hover:border-green-500/50'
|
||||
: node.status === 'active'
|
||||
? 'bg-blue-500/10 border-blue-500/50 hover:border-blue-500'
|
||||
: `${inputBg} ${cardBorder} ${isDark ? 'hover:border-blue-700' : 'hover:border-red-300'}`
|
||||
}`}
|
||||
>
|
||||
{/* 节点序号 */}
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center text-sm font-bold ${
|
||||
node.status === 'completed'
|
||||
? 'bg-green-500 text-white'
|
||||
: node.status === 'active'
|
||||
? 'bg-blue-500 text-white'
|
||||
: isDark ? 'bg-slate-700 text-slate-400' : 'bg-gray-200 text-gray-600'
|
||||
}`}>
|
||||
{node.status === 'completed' ? <CheckCircle className="w-5 h-5" /> : node.id}
|
||||
</div>
|
||||
|
||||
{/* 节点信息 */}
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<h4 className={`${textPrimary} font-medium`}>{node.name}</h4>
|
||||
<span className={`px-2 py-0.5 rounded text-xs font-medium ${
|
||||
node.status === 'completed' ? 'bg-green-500/20 text-green-400' :
|
||||
node.status === 'active' ? 'bg-blue-500/20 text-blue-400' :
|
||||
isDark ? 'bg-slate-700 text-slate-400' : 'bg-gray-200 text-gray-600'
|
||||
}`}>
|
||||
{node.status === 'completed' ? '已完成' : node.status === 'active' ? '进行中' : '待执行'}
|
||||
</span>
|
||||
{node.allowParallel && (
|
||||
<span className="px-2 py-0.5 bg-amber-500/20 text-amber-400 rounded text-xs">
|
||||
可并行
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={`flex items-center gap-4 text-sm ${textSecondary}`}>
|
||||
<span className="flex items-center gap-1">
|
||||
<Bot className="w-3 h-3" />
|
||||
{node.agent}
|
||||
</span>
|
||||
<span>输入 {node.inputs}</span>
|
||||
<span>输出 {node.outputs}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 放行条件 */}
|
||||
<div className="text-right">
|
||||
<p className={`${textMuted} text-xs mb-1`}>放行条件</p>
|
||||
<p className={`${textSecondary} text-sm`}>{node.releaseCondition}</p>
|
||||
</div>
|
||||
|
||||
<ChevronDown className={`w-5 h-5 ${textMuted}`} />
|
||||
</button>
|
||||
|
||||
{/* 回退虚线 */}
|
||||
{node.rollbackNode !== '无' && (
|
||||
<div className="absolute -right-16 top-1/2 transform -translate-y-1/2">
|
||||
<div className="flex items-center gap-1 text-amber-400 text-xs">
|
||||
<span className="w-8 border-t-2 border-dashed border-amber-400/50" />
|
||||
<RotateCcw className="w-3 h-3" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 图例 */}
|
||||
<div className={`mt-6 flex items-center gap-6 pt-6 border-t ${cardBorder}`}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-green-400" />
|
||||
<span className={`${textSecondary} text-sm`}>已完成</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-blue-400" />
|
||||
<span className={`${textSecondary} text-sm`}>进行中</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-3 h-3 rounded-full ${isDark ? 'bg-slate-600' : 'bg-gray-400'}`} />
|
||||
<span className={`${textSecondary} text-sm`}>待执行</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-amber-400" />
|
||||
<span className={`${textSecondary} text-sm`}>可并行</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'template' && (
|
||||
<div className="space-y-6">
|
||||
{/* 任务输入模板 */}
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-6`}>
|
||||
<h3 className={`${textPrimary} font-semibold mb-4`}>任务输入模板</h3>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{inputTemplate.map((field) => (
|
||||
<div key={field.name} className={`p-4 ${inputBg} ${inputBorder} rounded-lg`}>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<p className={`${textPrimary} text-sm font-medium`}>{field.name}</p>
|
||||
{field.required && (
|
||||
<span className="px-2 py-0.5 bg-red-500/20 text-red-400 rounded text-xs">必填</span>
|
||||
)}
|
||||
</div>
|
||||
<p className={`${textMuted} text-xs`}>{field.type}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 中间输出模板 */}
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-6`}>
|
||||
<h3 className={`${textPrimary} font-semibold mb-4`}>中间输出模板</h3>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{outputTemplate.middle.map((field) => (
|
||||
<div key={field} className="p-4 bg-cyan-500/10 border border-cyan-500/30 rounded-lg">
|
||||
<p className="text-cyan-400 text-sm font-mono">{field}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 最终输出模板 */}
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-6`}>
|
||||
<h3 className={`${textPrimary} font-semibold mb-4`}>最终输出模板</h3>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{outputTemplate.final.map((field) => (
|
||||
<div key={field} className="p-4 bg-green-500/10 border border-green-500/30 rounded-lg">
|
||||
<p className="text-green-400 text-sm">{field}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'rules' && (
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-6 space-y-6`}>
|
||||
<h3 className={`${textPrimary} font-semibold mb-4`}>运行规则配置</h3>
|
||||
|
||||
{[
|
||||
{ title: '启用状态门控', desc: '每个阶段需要明确放行才能进入下一阶段', enabled: true },
|
||||
{ title: '启用逻辑链校验', desc: '自动检查策略链的完整性和一致性', enabled: true },
|
||||
{ title: '启用风险强制复核', desc: '高风险项必须人工确认才能继续', enabled: false },
|
||||
{ title: '允许局部并行', desc: '部分独立阶段可以并行执行', enabled: true },
|
||||
].map((rule, index) => (
|
||||
<div key={index} className={`flex items-center justify-between py-4 border-b ${cardBorder} last:border-0`}>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className={`${textPrimary} font-medium`}>{rule.title}</p>
|
||||
{rule.enabled && (
|
||||
<CheckCircle className="w-4 h-4 text-green-400" />
|
||||
)}
|
||||
</div>
|
||||
<p className={`${textMuted} text-sm mt-1`}>{rule.desc}</p>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" className="sr-only peer" defaultChecked={rule.enabled} />
|
||||
<div className="w-11 h-6 bg-slate-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="grid grid-cols-2 gap-6 pt-4">
|
||||
<div>
|
||||
<label className={`block ${textSecondary} text-sm mb-2`}>最大讨论轮数</label>
|
||||
<input
|
||||
type="number"
|
||||
defaultValue={10}
|
||||
className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm focus:outline-none focus:border-blue-500/50`}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={`block ${textSecondary} text-sm mb-2`}>最大回退次数</label>
|
||||
<input
|
||||
type="number"
|
||||
defaultValue={3}
|
||||
className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm focus:outline-none focus:border-blue-500/50`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-4">
|
||||
<label className={`block ${textSecondary} text-sm mb-2`}>异常中断处理方式</label>
|
||||
<select className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm focus:outline-none focus:border-blue-500/50`}>
|
||||
<option>自动保存并暂停</option>
|
||||
<option>自动回退到上一节点</option>
|
||||
<option>通知负责人并等待</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 右侧场景摘要区 */}
|
||||
<div className="space-y-4">
|
||||
{/* 场景统计卡片 */}
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-5`}>
|
||||
<h3 className={`${textPrimary} font-semibold mb-4`}>场景摘要</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between p-3 bg-blue-500/10 border border-blue-500/30 rounded-lg">
|
||||
<span className={`${textSecondary} text-sm`}>流程节点数</span>
|
||||
<span className="text-blue-400 font-bold">{sceneSummary.flowNodesCount}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-3 bg-purple-500/10 border border-purple-500/30 rounded-lg">
|
||||
<span className={`${textSecondary} text-sm`}>绑定智能体</span>
|
||||
<span className="text-purple-400 font-bold">{sceneSummary.boundAgentsCount}</span>
|
||||
</div>
|
||||
<div className={`flex items-center justify-between p-3 ${inputBg} ${isDark ? 'border-slate-700/30' : 'border-gray-300'} rounded-lg`}>
|
||||
<span className={`${textSecondary} text-sm`}>串行节点</span>
|
||||
<span className={`${textSecondary}`}>{sceneSummary.serialNodes}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-3 bg-amber-500/10 border border-amber-500/30 rounded-lg">
|
||||
<span className={`${textSecondary} text-sm`}>并行节点</span>
|
||||
<span className="text-amber-400">{sceneSummary.parallelNodes}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-3 bg-green-500/10 border border-green-500/30 rounded-lg">
|
||||
<span className={`${textSecondary} text-sm`}>逻辑链校验</span>
|
||||
<span className="text-green-400">{sceneSummary.logicChainEnabled ? '已启用' : '未启用'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 最近发布时间 */}
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-5`}>
|
||||
<div className={`flex items-center gap-2 ${textSecondary} text-sm mb-2`}>
|
||||
<Clock className="w-4 h-4" />
|
||||
最近发布时间
|
||||
</div>
|
||||
<p className={`${textPrimary}`}>{sceneSummary.lastPublishedTime}</p>
|
||||
</div>
|
||||
|
||||
{/* 节点属性面板 */}
|
||||
{selectedNode && (
|
||||
<div className={`${cardBg} backdrop-blur-xl border border-blue-500/30 rounded-xl p-5`}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className={`${textPrimary} font-semibold`}>节点属性</h3>
|
||||
<button
|
||||
onClick={() => setSelectedNode(null)}
|
||||
className={`p-1 ${isDark ? 'hover:bg-slate-700/50' : 'hover:bg-gray-100'} rounded`}
|
||||
>
|
||||
<X className={`w-4 h-4 ${textSecondary}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p className={`${textMuted} text-xs mb-1`}>节点名称</p>
|
||||
<p className={`${textPrimary}`}>{selectedNode.name}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className={`${textMuted} text-xs mb-1`}>绑定智能体</p>
|
||||
<p className="text-cyan-400">{selectedNode.agent}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className={`${textMuted} text-xs mb-1`}>上游依赖</p>
|
||||
<div className="space-y-1">
|
||||
{selectedNode.inputSource.map((input) => (
|
||||
<div key={input} className={`${textSecondary} text-sm ${inputBg} px-2 py-1 rounded`}>
|
||||
{input}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className={`${textMuted} text-xs mb-1`}>输出字段</p>
|
||||
<div className="space-y-1">
|
||||
{selectedNode.outputFields.map((output) => (
|
||||
<div key={output} className="text-green-400 text-sm bg-green-500/10 px-2 py-1 rounded">
|
||||
{output}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className={`${textMuted} text-xs mb-1`}>放行条件</p>
|
||||
<p className="text-amber-400 text-sm">{selectedNode.releaseCondition}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className={`${textMuted} text-xs mb-1`}>回退节点</p>
|
||||
<p className={`${textSecondary} text-sm`}>{selectedNode.rollbackNode}</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className={`${textMuted} text-sm`}>允许并行</span>
|
||||
<span className={selectedNode.allowParallel ? 'text-green-400' : `${textSecondary}`}>
|
||||
{selectedNode.allowParallel ? '是' : '否'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`mt-4 pt-4 border-t ${cardBorder} space-y-2`}>
|
||||
<button className={`w-full flex items-center justify-center gap-2 px-3 py-2 ${btnPrimary} ${textPrimary} rounded-lg text-sm transition-colors`}>
|
||||
<Settings className="w-4 h-4" />
|
||||
编辑节点
|
||||
</button>
|
||||
<div className="flex gap-2">
|
||||
<button className={`flex-1 flex items-center justify-center gap-1 px-3 py-2 ${btnSecondary} rounded-lg text-sm transition-colors`}>
|
||||
<Plus className="w-3 h-3" />
|
||||
前置
|
||||
</button>
|
||||
<button className={`flex-1 flex items-center justify-center gap-1 px-3 py-2 ${btnSecondary} rounded-lg text-sm transition-colors`}>
|
||||
<Plus className="w-3 h-3" />
|
||||
后置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SceneDetail;
|
||||
@@ -0,0 +1,482 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Plus, Search, Eye, Edit, Copy, Trash2, Play, Pause, ChevronRight, Layers } from 'lucide-react';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { PageType } from '../components/Sidebar';
|
||||
|
||||
import { requestCrudService } from '@/service/request-axios-service';
|
||||
import { Pagination, Dialog, Input, Textarea, Radio, Button, MessagePlugin, Popconfirm, Loading } from 'tdesign-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
interface SceneListProps {
|
||||
onViewDetail: (id: string) => void;
|
||||
onNavigate: (page: PageType) => void;
|
||||
}
|
||||
|
||||
interface sceneItem {
|
||||
id?: number;
|
||||
status?: number;
|
||||
applicableTask?: string;
|
||||
sceneName?: string;
|
||||
sceneType: string;
|
||||
sceneIntro: string;
|
||||
sceneCode: string;
|
||||
relatedAgent: string;
|
||||
deleted?: number;
|
||||
updateTime?: string;
|
||||
taskJson?: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
const zncmUrl = import.meta.env.VITE_API_BASE_URL || '';
|
||||
const sceneStatus = {
|
||||
0: '执行中',
|
||||
1: '成功',
|
||||
2: '失败',
|
||||
3: '部分成功'
|
||||
}
|
||||
const SceneList: React.FC<SceneListProps> = ({ onViewDetail, onNavigate }) => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [editDataShow, setEditDataShow] = useState(false);
|
||||
const [sceneName, setSceneName] = useState('');
|
||||
const [scenes, setScenes] = useState<sceneItem[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [editLoading, setEditLoading] = useState<boolean>(false);
|
||||
const [pageNum, setPageNum] = useState<number>(1);
|
||||
const [pageSize, setPageSize] = useState<number>(10);
|
||||
const [total, setTotal] = useState<number>(0);
|
||||
const [formData, setFormData] = useState<sceneItem>({
|
||||
applicableTask: '',
|
||||
relatedAgent: '',
|
||||
sceneCode: '',
|
||||
sceneIntro: '',
|
||||
sceneName: '',
|
||||
sceneType: '',
|
||||
taskJson: '',
|
||||
version: '',
|
||||
});
|
||||
const state = useRef({
|
||||
pageNum: 1,
|
||||
pageSize: 10
|
||||
});
|
||||
const navigate = useNavigate();
|
||||
|
||||
const scenesDataSearch = async (pageNumParam?: number, pageSizeParam?: number, sceneNameParam?: string) => {
|
||||
const parmy = {
|
||||
pageNum: pageNumParam ?? state.current.pageNum,
|
||||
pageSize: pageSizeParam ?? state.current.pageSize,
|
||||
sceneName: sceneNameParam ?? sceneName
|
||||
}
|
||||
setLoading(true);
|
||||
const res = await requestCrudService.get(`${zncmUrl}/scene/scenes`, parmy);
|
||||
setLoading(false);
|
||||
res.data.records.forEach((item) => {
|
||||
if (item.taskJson) {
|
||||
item.taskJson = JSON.parse(item.taskJson);
|
||||
}
|
||||
})
|
||||
console.log('res.data.records', res.data.records)
|
||||
setScenes(res.data.records || []);
|
||||
setTotal(res.data.total || 0);
|
||||
}
|
||||
|
||||
const addScene = async () => {
|
||||
const res = await requestCrudService.post(`${zncmUrl}/scene/sceneAdd`, formData);
|
||||
setVisible(false);
|
||||
MessagePlugin.success('新增场景成功');
|
||||
await scenesDataSearch();
|
||||
}
|
||||
|
||||
const updateScene = async () => {
|
||||
setEditLoading(true);
|
||||
const res = await requestCrudService.post(`${zncmUrl}/scene/sceneUpdate`, {
|
||||
...formData,
|
||||
});
|
||||
setEditLoading(false);
|
||||
setEditDataShow(false);
|
||||
MessagePlugin.success('更新场景成功');
|
||||
await scenesDataSearch();
|
||||
}
|
||||
|
||||
const handleDataChange = (key: keyof typeof formData, value: string | number) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[key]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const showDialog = () => {
|
||||
setFormData({
|
||||
applicableTask: '',
|
||||
relatedAgent: '',
|
||||
sceneCode: '',
|
||||
sceneIntro: '',
|
||||
sceneName: '',
|
||||
sceneType: '',
|
||||
version: ''
|
||||
});
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
const editShowChange = async (item: sceneItem) => {
|
||||
const response = await fetch('https://ch1.b.uat.4.cn/zncmweb/templates/6b_template.json');
|
||||
const data = await response.json();
|
||||
console.log('response', JSON.stringify(data));
|
||||
// let taskJson =
|
||||
setFormData({
|
||||
applicableTask: item.applicableTask,
|
||||
deleted: item.deleted,
|
||||
id: item.id,
|
||||
relatedAgent: item.relatedAgent,
|
||||
sceneCode: item.sceneCode,
|
||||
sceneIntro: item.sceneIntro,
|
||||
sceneName: item.sceneName,
|
||||
sceneType: item.sceneType,
|
||||
status: item.status,
|
||||
taskJson: JSON.stringify(data),
|
||||
version: item.version
|
||||
});
|
||||
setEditDataShow(true);
|
||||
}
|
||||
|
||||
const { Group: RadioGroup, Button: RadioButton } = Radio;
|
||||
|
||||
const delDataChange = async (item: sceneItem) => {
|
||||
const res = await requestCrudService.delete(`${zncmUrl}/scene/sceneDelete/${item.id}`)
|
||||
console.log(res);
|
||||
MessagePlugin.success('场景删除成功');
|
||||
await scenesDataSearch();
|
||||
}
|
||||
|
||||
const handleConfirm = (item: sceneItem) => {
|
||||
// 确认删除逻辑
|
||||
console.log('用户点击了确认');
|
||||
delDataChange(item);
|
||||
return true; // 返回 true 表示确认执行
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
// 取消删除逻辑
|
||||
console.log('用户点击了取消');
|
||||
return true; // 返回 true 表示取消
|
||||
};
|
||||
const pageIndexChange = async (value: number) => {
|
||||
state.current.pageNum = value;
|
||||
setPageNum(value);
|
||||
await scenesDataSearch();
|
||||
}
|
||||
|
||||
const pageSizeChange = async (value: number) => {
|
||||
state.current.pageSize = value;
|
||||
setPageSize(value);
|
||||
await scenesDataSearch();
|
||||
}
|
||||
|
||||
// 回车处理
|
||||
const handleKeyDown = async (e) => {
|
||||
if ((e.metaKey || e.ctrlKey || e.shiftKey) && e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
} else if (e.keyCode === 13) {
|
||||
e.preventDefault();
|
||||
await scenesDataSearch();
|
||||
} else if (e.key === "/") {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
scenesDataSearch();
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 页面标题区 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className={`text-2xl font-bold ${isDark ? 'text-white' : 'text-gray-900'} mb-2`}>业务链条管理</h1>
|
||||
<p className={`${isDark ? 'text-slate-400' : 'text-gray-600'} text-sm`}>管理和配置宣传策略流程模板</p>
|
||||
</div>
|
||||
<Button theme="primary" onClick={showDialog}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
<span className="text-sm font-medium">新建场景</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 搜索和筛选区 */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative flex-1 max-w-md">
|
||||
<Search className={`absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 ${isDark ? 'text-slate-500' : 'text-gray-400'}`} />
|
||||
<input
|
||||
type="text"
|
||||
value={sceneName}
|
||||
onChange={(e) => setSceneName(e.target.value)}
|
||||
onKeyDown={(e) => handleKeyDown(e)}
|
||||
placeholder="搜索场景名称..."
|
||||
className={`w-full pl-10 pr-4 py-2.5 ${isDark ? 'bg-slate-800/30' : 'bg-gray-50'} border ${isDark ? 'border-blue-800/30' : 'border-gray-300'} rounded-lg ${isDark ? 'text-white' : 'text-gray-900'} placeholder-${isDark ? 'text-slate-500' : 'text-gray-500'} text-sm focus:outline-none focus:border-blue-500/50 transition-colors`}
|
||||
/>
|
||||
</div>
|
||||
{/* <button className={`px-4 py-2.5 ${isDark ? 'bg-slate-800/50 border-blue-800/30' : 'bg-gray-100 border-gray-300'} rounded-lg ${isDark ? 'bg-slate-700/50 border-blue-800/30 text-slate-300 hover:bg-slate-600/50' : 'bg-gray-100 border-gray-300 text-gray-700 hover:bg-gray-200'} transition-colors`}>
|
||||
全部类型
|
||||
</button>
|
||||
<button className={`px-4 py-2.5 ${isDark ? 'bg-slate-800/50 border-blue-800/30' : 'bg-gray-100 border-gray-300'} rounded-lg ${isDark ? 'bg-slate-700/50 border-blue-800/30 text-slate-300 hover:bg-slate-600/50' : 'bg-gray-100 border-gray-300 text-gray-700 hover:bg-gray-200'} transition-colors`}>
|
||||
全部状态
|
||||
</button> */}
|
||||
</div>
|
||||
|
||||
{/* 场景列表 */}
|
||||
<div className={`${isDark ? 'bg-slate-800/30' : 'bg-white'} backdrop-blur-xl border ${isDark ? 'border-blue-800/20' : 'border-gray-200'} rounded-xl overflow-hidden`}>
|
||||
<Loading loading={loading} text="正在加载...">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className={`border-b ${isDark ? 'border-blue-800/20' : 'border-gray-200'}`}>
|
||||
<th className={`text-left px-6 py-4 text-xs font-semibold ${isDark ? 'text-slate-400' : 'text-gray-600'} uppercase tracking-wider`}>场景名称</th>
|
||||
<th className={`text-left px-6 py-4 text-xs font-semibold ${isDark ? 'text-slate-400' : 'text-gray-600'} uppercase tracking-wider`}>场景类型</th>
|
||||
<th className={`text-left px-6 py-4 text-xs font-semibold ${isDark ? 'text-slate-400' : 'text-gray-600'} uppercase tracking-wider`}>适用任务</th>
|
||||
<th className={`text-left px-6 py-4 text-xs font-semibold ${isDark ? 'text-slate-400' : 'text-gray-600'} uppercase tracking-wider`}>版本</th>
|
||||
<th className={`text-left px-6 py-4 text-xs font-semibold ${isDark ? 'text-slate-400' : 'text-gray-600'} uppercase tracking-wider`}>更新时间</th>
|
||||
<th className={`text-left px-6 py-4 text-xs font-semibold ${isDark ? 'text-slate-400' : 'text-gray-600'} uppercase tracking-wider`}>状态</th>
|
||||
<th className={`text-left px-6 py-4 text-xs font-semibold ${isDark ? 'text-slate-400' : 'text-gray-600'} uppercase tracking-wider`}>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{scenes.map((scene) => (
|
||||
<tr key={scene.id} className={`border-b ${isDark ? 'border-blue-800/20' : 'border-gray-200'} hover:bg-slate-700/20 transition-colors`}>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-blue-500/20 flex items-center justify-center">
|
||||
<Layers className="w-5 h-5 text-blue-400" />
|
||||
</div>
|
||||
<span className={`${isDark ? 'text-white' : 'text-gray-900'} font-medium`}>{scene.sceneName}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className={`${isDark ? 'text-slate-400' : 'text-gray-600'} text-sm`}>{scene.sceneType}</span>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className={`${isDark ? 'text-slate-400' : 'text-gray-600'} text-sm`}>{scene.sceneIntro}</span>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className={`${isDark ? 'text-slate-400' : 'text-gray-600'} text-sm`}>{scene.version}</span>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className={`${isDark ? 'text-slate-400' : 'text-gray-600'} text-sm`}>{scene.updateTime}</span>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<span
|
||||
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium ${scene.status === 0
|
||||
? 'bg-green-500/20 text-green-400'
|
||||
: scene.status === 1
|
||||
? 'bg-yellow-500/20 text-yellow-400'
|
||||
: `${isDark ? 'bg-slate-500/20 text-slate-400' : 'bg-gray-500/20 text-gray-500'}`
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`w-1.5 h-1.5 rounded-full ${scene.status === 0
|
||||
? 'bg-green-400'
|
||||
: scene.status === 1
|
||||
? 'bg-yellow-400'
|
||||
: isDark ? 'bg-slate-400' : 'bg-gray-400'
|
||||
}`}
|
||||
/>
|
||||
{sceneStatus[scene.status]}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate(`/scene-detail/${scene.id}`);
|
||||
}}
|
||||
className="p-2 hover:bg-slate-700/50 rounded-lg transition-colors group"
|
||||
title="查看"
|
||||
>
|
||||
<Eye className={`w-4 h-4 ${isDark ? 'text-slate-500' : 'text-gray-500'} group-hover:${isDark ? 'text-white' : 'text-gray-900'}`} />
|
||||
</button>
|
||||
{/* <button className="p-2 hover:bg-slate-700/50 rounded-lg transition-colors group" title="编辑" onClick={() => editShowChange(scene)}>
|
||||
<Edit className={`w-4 h-4 ${isDark ? 'text-slate-500' : 'text-gray-500'} group-hover:${isDark ? 'text-white' : 'text-gray-900'}`} />
|
||||
</button> */}
|
||||
{/* <button className="p-2 hover:bg-slate-700/50 rounded-lg transition-colors group" title="复制">
|
||||
<Copy className={`w-4 h-4 ${isDark ? 'text-slate-500' : 'text-gray-500'} group-hover:${isDark ? 'text-white' : 'text-gray-900'}`} />
|
||||
</button> */}
|
||||
{/* <button
|
||||
onClick={() => onNavigate('task-scene')}
|
||||
className="p-2 hover:bg-blue-500/20 rounded-lg transition-colors group"
|
||||
title="启动"
|
||||
>
|
||||
<Play className={`w-4 h-4 ${isDark ? 'text-slate-500' : 'text-gray-500'} group-hover:text-blue-400`} />
|
||||
</button> */}
|
||||
{/* <Popconfirm
|
||||
content="确认删除吗"
|
||||
onConfirm={() => handleConfirm(scene)}
|
||||
onCancel={handleCancel}
|
||||
>
|
||||
<button className="p-2 hover:bg-red-500/20 rounded-lg transition-colors group" title="删除">
|
||||
<Trash2 className={`w-4 h-4 ${isDark ? 'text-slate-500' : 'text-gray-500'} group-hover:text-red-400`} />
|
||||
</button>
|
||||
</Popconfirm> */}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</Loading>
|
||||
</div>
|
||||
|
||||
{/* 分页 */}
|
||||
<div className="p-4 text-center">
|
||||
<Pagination
|
||||
total={total}
|
||||
current={pageNum}
|
||||
pageSize={pageSize}
|
||||
onCurrentChange={(page) => pageIndexChange(Number(page))}
|
||||
onPageSizeChange={(size) => pageSizeChange(Number(size))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 新增对话框 */}
|
||||
<Dialog
|
||||
header="新增场景"
|
||||
visible={visible}
|
||||
onConfirm={addScene}
|
||||
onCancel={() => setVisible(false)}
|
||||
onClose={() => setVisible(false)}
|
||||
closeOnOverlayClick={true}
|
||||
>
|
||||
<div className="mb-4">
|
||||
<Input
|
||||
label="场景名称"
|
||||
value={formData.sceneName}
|
||||
onChange={(val) => handleDataChange('sceneName', val)}
|
||||
placeholder="请输入场景名称"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<Input
|
||||
label="场景类型"
|
||||
value={formData.sceneType}
|
||||
onChange={(val) => handleDataChange('sceneType', val)}
|
||||
placeholder="请输入场景类型"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<Input
|
||||
label="适用任务"
|
||||
value={formData.applicableTask}
|
||||
onChange={(val) => handleDataChange('applicableTask', val)}
|
||||
placeholder="请输入适用任务"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<Input
|
||||
label="场景代码"
|
||||
value={formData.sceneCode}
|
||||
onChange={(val) => handleDataChange('sceneCode', val)}
|
||||
placeholder="请输入场景代码"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<Input
|
||||
label="版本"
|
||||
value={formData.version}
|
||||
onChange={(val) => handleDataChange('version', val)}
|
||||
placeholder="请输入版本"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Textarea
|
||||
value={formData.sceneIntro}
|
||||
onChange={(val) => handleDataChange('sceneIntro', val)}
|
||||
placeholder="请输入场景简介"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
{/* 编辑对话框 */}
|
||||
<Dialog
|
||||
header="编辑场景"
|
||||
visible={editDataShow}
|
||||
onConfirm={updateScene}
|
||||
confirmBtn={{
|
||||
content: '确定',
|
||||
theme: 'primary',
|
||||
loading: editLoading,
|
||||
}}
|
||||
onCancel={() => setEditDataShow(false)}
|
||||
onClose={() => setEditDataShow(false)}
|
||||
closeOnOverlayClick={true}
|
||||
>
|
||||
<div className="mb-4">
|
||||
<Input
|
||||
label="场景名称"
|
||||
value={formData.sceneName}
|
||||
onChange={(val) => handleDataChange('sceneName', val)}
|
||||
placeholder="请输入场景名称"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<Input
|
||||
label="场景类型"
|
||||
value={formData.sceneType}
|
||||
onChange={(val) => handleDataChange('sceneType', val)}
|
||||
placeholder="请输入场景类型"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<Input
|
||||
label="适用任务"
|
||||
value={formData.applicableTask}
|
||||
onChange={(val) => handleDataChange('applicableTask', val)}
|
||||
placeholder="请输入适用任务"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<Input
|
||||
label="场景代码"
|
||||
value={formData.sceneCode}
|
||||
onChange={(val) => handleDataChange('sceneCode', val)}
|
||||
placeholder="请输入场景代码"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<Input
|
||||
label="版本"
|
||||
value={formData.version}
|
||||
onChange={(val) => handleDataChange('version', val)}
|
||||
placeholder="请输入版本"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<Textarea
|
||||
value={formData.sceneIntro}
|
||||
onChange={(val) => handleDataChange('sceneIntro', val)}
|
||||
placeholder="请输入场景简介"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
{/* <div>
|
||||
<Textarea
|
||||
value={formData.taskJson}
|
||||
onChange={(val) => handleDataChange('taskJson', val)}
|
||||
placeholder="请输入场景JSON"
|
||||
className="w-full"
|
||||
/>
|
||||
</div> */}
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SceneList;
|
||||
@@ -0,0 +1,347 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
FileText,
|
||||
Clock,
|
||||
CheckCircle,
|
||||
AlertTriangle,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Target,
|
||||
Users,
|
||||
TrendingUp,
|
||||
MessageSquare,
|
||||
Megaphone,
|
||||
Shield,
|
||||
ArrowRight,
|
||||
Download,
|
||||
RefreshCw,
|
||||
Eye,
|
||||
} from 'lucide-react';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { PageType } from '../components/Sidebar';
|
||||
|
||||
interface StrategyResultProps {
|
||||
onNavigate: (page: PageType) => void;
|
||||
}
|
||||
|
||||
const StrategyResult: React.FC<StrategyResultProps> = ({ onNavigate }) => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
|
||||
// 主题颜色类
|
||||
const cardBg = isDark ? 'bg-slate-800/30' : 'bg-white';
|
||||
const cardBorder = isDark ? 'border-blue-800/30' : 'border-red-200';
|
||||
const textPrimary = isDark ? 'text-white' : 'text-gray-900';
|
||||
const textSecondary = isDark ? 'text-slate-400' : 'text-gray-600';
|
||||
const textMuted = isDark ? 'text-slate-500' : 'text-gray-500';
|
||||
const btnSecondary = isDark ? 'bg-slate-700/50 hover:bg-slate-600/50 border-blue-800/30 text-slate-300' : 'bg-gray-100 hover:bg-gray-200 border-gray-300 text-gray-700';
|
||||
const btnPrimary = isDark ? 'bg-blue-600 hover:bg-blue-500' : 'bg-red-600 hover:bg-red-500';
|
||||
const inputBg = isDark ? 'bg-slate-900/50' : 'bg-gray-50';
|
||||
const inputBorder = isDark ? 'border-blue-800/30' : 'border-gray-300';
|
||||
|
||||
const [expandedSection, setExpandedSection] = useState<string | null>('goal');
|
||||
|
||||
const strategyChain = [
|
||||
{ id: 1, name: '目标', icon: Target, color: 'blue', status: 'completed' },
|
||||
{ id: 2, name: '受众', icon: Users, color: 'cyan', status: 'completed' },
|
||||
{ id: 3, name: '行为', icon: TrendingUp, color: 'green', status: 'completed' },
|
||||
{ id: 4, name: '驱动', icon: MessageSquare, color: 'yellow', status: 'completed' },
|
||||
{ id: 5, name: '叙事', icon: Megaphone, color: 'purple', status: 'completed' },
|
||||
{ id: 6, name: '风险', icon: Shield, color: 'red', status: 'completed' },
|
||||
];
|
||||
|
||||
const versionHistory = [
|
||||
{ version: 'V3', time: '2026-03-19 15:30', changes: ['叙事由"事件主导"改为"人物+事件结合"', '风险项已修正'] },
|
||||
{ version: 'V2', time: '2026-03-19 14:20', changes: ['受众由"全体青年"收窄为"基层青年"', '目标行为已明确'] },
|
||||
{ version: 'V1', time: '2026-03-19 10:30', changes: ['初始版本生成'] },
|
||||
];
|
||||
|
||||
const toggleSection = (section: string) => {
|
||||
setExpandedSection(expandedSection === section ? null : section);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 顶部信息栏 */}
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-5`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-blue-500/20 to-cyan-500/20 flex items-center justify-center">
|
||||
<FileText className="w-6 h-6 text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className={`${textPrimary} font-bold text-lg`}>五四青年节宣传活动策略</h2>
|
||||
<p className={`${textSecondary} text-sm`}>策略讨论结果 · 已收敛</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-12 w-px bg-blue-800/30" />
|
||||
<div className="grid grid-cols-4 gap-6 text-sm">
|
||||
<div>
|
||||
<p className={`${textMuted}`}>使用场景</p>
|
||||
<p className={`${textPrimary} font-medium`}>宣传策略生成</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className={`${textMuted}`}>运行时长</p>
|
||||
<p className={`${textPrimary} font-medium`}>5小时32分</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className={`${textMuted}`}>当前版本</p>
|
||||
<p className="text-cyan-400 font-medium">V3</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className={`${textMuted}`}>完成度</p>
|
||||
<p className="text-green-400 font-medium flex items-center gap-1">
|
||||
<CheckCircle className="w-4 h-4" /> 100%
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => onNavigate('task-scene')}
|
||||
className={`flex items-center gap-2 px-4 py-2.5 ${btnSecondary} rounded-lg text-sm transition-colors`}
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
返回工作台
|
||||
</button>
|
||||
<button className={`flex items-center gap-2 px-4 py-2.5 ${btnPrimary} text-white rounded-lg text-sm transition-colors`}>
|
||||
<FileText className="w-4 h-4" />
|
||||
生成最终方案
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-6">
|
||||
{/* 左侧 - 策略链和详情 (70%) */}
|
||||
<div className="col-span-2 space-y-6">
|
||||
{/* 策略链可视化 */}
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-5`}>
|
||||
<h3 className={`${textPrimary} font-semibold mb-4`}>策略链结果</h3>
|
||||
<div className="flex items-center justify-between">
|
||||
{strategyChain.map((item, index) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<React.Fragment key={item.id}>
|
||||
<div className="flex flex-col items-center">
|
||||
<div className={`w-14 h-14 rounded-xl bg-${item.color}-500/20 border-2 border-${item.color}-500/50 flex items-center justify-center mb-2`}>
|
||||
<Icon className={`w-7 h-7 text-${item.color}-400`} />
|
||||
</div>
|
||||
<span className={`text-${item.color}-400 text-sm font-medium`}>{item.name}</span>
|
||||
<span className={`${textMuted} text-xs mt-1 flex items-center gap-1`}>
|
||||
<CheckCircle className="w-3 h-3 text-green-400" /> 已确认
|
||||
</span>
|
||||
</div>
|
||||
{index < strategyChain.length - 1 && (
|
||||
<ArrowRight className="w-6 h-6 text-slate-600 mb-6" />
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 详细结果 */}
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl overflow-hidden`}>
|
||||
{[
|
||||
{
|
||||
id: 'goal',
|
||||
title: '目标',
|
||||
icon: Target,
|
||||
color: 'blue',
|
||||
content: {
|
||||
primary: '提升品牌在Z世代用户中的认知度',
|
||||
secondary: '建立品牌年轻化形象,增强用户情感连接',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'audience',
|
||||
title: '受众',
|
||||
icon: Users,
|
||||
color: 'cyan',
|
||||
content: {
|
||||
primary: '核心受众:18-25岁基层青年',
|
||||
secondary: '次级受众:25-30岁职场新人',
|
||||
insights: ['社交媒体重度用户', '注重性价比', '偏好真实、有态度的品牌'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'behavior',
|
||||
title: '行为',
|
||||
icon: TrendingUp,
|
||||
color: 'green',
|
||||
content: {
|
||||
primary: '主动参与话题讨论、生成UGC内容',
|
||||
conversion: '浏览→点赞→评论→转发→创作',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'driver',
|
||||
title: '驱动',
|
||||
icon: MessageSquare,
|
||||
color: 'yellow',
|
||||
content: {
|
||||
primary: '社交认同与自我表达',
|
||||
secondary: ['归属感需求', '表达欲驱动', '群体认同激励'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'narrative',
|
||||
title: '叙事',
|
||||
icon: Megaphone,
|
||||
color: 'purple',
|
||||
content: {
|
||||
primary: '真实、有态度的青年故事',
|
||||
approach: '人物+事件结合,以真实故事传递品牌价值观',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'risk',
|
||||
title: '风险',
|
||||
icon: Shield,
|
||||
color: 'red',
|
||||
content: {
|
||||
risks: ['预算有限,需控制成本', '青年群体注意力分散', 'UGC质量难以保证'],
|
||||
suggestions: ['提前布局预热', '设置激励机制', '建立内容审核机制'],
|
||||
},
|
||||
},
|
||||
].map((section) => {
|
||||
const Icon = section.icon;
|
||||
const isExpanded = expandedSection === section.id;
|
||||
return (
|
||||
<div key={section.id} className={`border-b ${cardBorder} last:border-0`}>
|
||||
<button
|
||||
onClick={() => toggleSection(section.id)}
|
||||
className="w-full flex items-center justify-between p-4 hover:bg-slate-700/20 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-10 h-10 rounded-lg bg-${section.color}-500/20 flex items-center justify-center`}>
|
||||
<Icon className={`w-5 h-5 text-${section.color}-400`} />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<p className={`${textPrimary} font-medium`}>{section.title}</p>
|
||||
<p className={`text-${section.color}-400 text-sm`}>{section.content.primary}</p>
|
||||
</div>
|
||||
</div>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className={`w-5 h-5 ${textSecondary}`} />
|
||||
) : (
|
||||
<ChevronRight className={`w-5 h-5 ${textSecondary}`} />
|
||||
)}
|
||||
</button>
|
||||
{isExpanded && (
|
||||
<div className="px-4 pb-4 pt-0 ml-13">
|
||||
<div className={`p-4 ${inputBg} rounded-lg space-y-3`}>
|
||||
{Object.entries(section.content).map(([key, value]) => (
|
||||
<div key={key}>
|
||||
<p className={`${textMuted} text-xs uppercase mb-1`}>{key}</p>
|
||||
{Array.isArray(value) ? (
|
||||
<ul className={`list-disc list-inside ${textSecondary} text-sm space-y-1`}>
|
||||
{value.map((item, index) => (
|
||||
<li key={index}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className={`${textPrimary} text-sm`}>{value}</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧 - 摘要和版本记录 (30%) */}
|
||||
<div className="space-y-6">
|
||||
{/* 关键结论摘要 */}
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-5`}>
|
||||
<h3 className={`${textPrimary} font-semibold mb-4`}>关键结论摘要</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="p-3 bg-blue-500/10 border border-blue-500/30 rounded-lg">
|
||||
<p className="text-blue-400 text-xs font-medium mb-1">为什么选择这类受众</p>
|
||||
<p className={`${textPrimary} text-sm`}>基层青年是品牌年轻化战略的核心突破口,具有强社交传播属性</p>
|
||||
</div>
|
||||
<div className="p-3 bg-green-500/10 border border-green-500/30 rounded-lg">
|
||||
<p className="text-green-400 text-xs font-medium mb-1">为什么设计这种行为目标</p>
|
||||
<p className={`${textPrimary} text-sm`}>UGC是最高效的信任传递方式,用户生成内容具有真实性和可信度</p>
|
||||
</div>
|
||||
<div className="p-3 bg-purple-500/10 border border-purple-500/30 rounded-lg">
|
||||
<p className="text-purple-400 text-xs font-medium mb-1">为什么采用这种叙事</p>
|
||||
<p className={`${textPrimary} text-sm`}>真实故事比硬广更能打动年轻人,人物视角增强代入感</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 版本对比记录 */}
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-5`}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className={`${textPrimary} font-semibold`}>版本迭代记录</h3>
|
||||
<button className="flex items-center gap-1 text-blue-400 text-sm hover:text-blue-300 transition-colors">
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
对比
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{versionHistory.map((version, index) => (
|
||||
<div
|
||||
key={version.version}
|
||||
className={`p-3 rounded-lg border ${
|
||||
index === 0
|
||||
? 'bg-cyan-500/10 border-cyan-500/30'
|
||||
: `${inputBg} border-slate-700/30`
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className={`font-bold ${
|
||||
index === 0 ? 'text-cyan-400' : textSecondary
|
||||
}`}>
|
||||
{version.version}
|
||||
</span>
|
||||
<span className={`${textMuted} text-xs`}>{version.time}</span>
|
||||
</div>
|
||||
<ul className="space-y-1">
|
||||
{version.changes.map((change, i) => (
|
||||
<li key={i} className={`${textPrimary} text-xs flex items-start gap-1`}>
|
||||
<span className={textSecondary}>•</span>
|
||||
{change}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 导出选项 */}
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-5`}>
|
||||
<h3 className={`${textPrimary} font-semibold mb-4`}>导出选项</h3>
|
||||
<div className="space-y-2">
|
||||
<button className={`w-full flex items-center justify-between p-3 ${inputBg} hover:bg-slate-800/50 ${inputBorder} rounded-lg transition-colors`}>
|
||||
<div className="flex items-center gap-3">
|
||||
<FileText className="w-5 h-5 text-blue-400" />
|
||||
<span className={`${textPrimary} text-sm`}>导出讨论记录</span>
|
||||
</div>
|
||||
<Download className={`w-4 h-4 ${textSecondary}`} />
|
||||
</button>
|
||||
<button className={`w-full flex items-center justify-between p-3 ${inputBg} hover:bg-slate-800/50 ${inputBorder} rounded-lg transition-colors`}>
|
||||
<div className="flex items-center gap-3">
|
||||
<FileText className="w-5 h-5 text-green-400" />
|
||||
<span className={`${textPrimary} text-sm`}>导出策略结果</span>
|
||||
</div>
|
||||
<Download className={`w-4 h-4 ${textSecondary}`} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StrategyResult;
|
||||
@@ -0,0 +1,558 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { PageType } from '../components/Sidebar';
|
||||
import {
|
||||
Settings,
|
||||
User,
|
||||
Bell,
|
||||
Shield,
|
||||
Database,
|
||||
Palette,
|
||||
Key,
|
||||
Globe,
|
||||
Monitor,
|
||||
Smartphone,
|
||||
Mail,
|
||||
MessageSquare,
|
||||
Clock,
|
||||
Save,
|
||||
RefreshCw,
|
||||
CheckCircle,
|
||||
AlertTriangle,
|
||||
ChevronRight,
|
||||
Building,
|
||||
Users,
|
||||
Bot,
|
||||
Workflow,
|
||||
FileText,
|
||||
Zap,
|
||||
} from 'lucide-react';
|
||||
|
||||
interface SystemSettingsProps {
|
||||
onNavigate: (page: PageType) => void;
|
||||
}
|
||||
|
||||
const SystemSettings: React.FC<SystemSettingsProps> = ({ onNavigate }) => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
|
||||
// 主题颜色类
|
||||
const cardBg = isDark ? 'bg-slate-800/30' : 'bg-white';
|
||||
const cardBorder = isDark ? 'border-blue-800/30' : 'border-red-200';
|
||||
const textPrimary = isDark ? 'text-white' : 'text-gray-900';
|
||||
const textSecondary = isDark ? 'text-slate-400' : 'text-gray-600';
|
||||
const textMuted = isDark ? 'text-slate-500' : 'text-gray-500';
|
||||
const btnSecondary = isDark ? 'bg-slate-700/50 hover:bg-slate-600/50 border-blue-800/30 text-slate-300' : 'bg-gray-100 hover:bg-gray-200 border-gray-300 text-gray-700';
|
||||
const btnPrimary = isDark ? 'bg-blue-600 hover:bg-blue-500' : 'bg-red-600 hover:bg-red-500';
|
||||
const inputBg = isDark ? 'bg-slate-900/50' : 'bg-gray-50';
|
||||
const inputBorder = isDark ? 'border-blue-800/30' : 'border-gray-300';
|
||||
|
||||
const [activeTab, setActiveTab] = useState('general');
|
||||
const [settings, setSettings] = useState({
|
||||
// 通用设置
|
||||
systemName: '智能策略系统',
|
||||
systemVersion: 'V1.0.0',
|
||||
language: 'zh-CN',
|
||||
timezone: 'Asia/Shanghai',
|
||||
// 通知设置
|
||||
emailNotification: true,
|
||||
inAppNotification: true,
|
||||
smsNotification: false,
|
||||
// 安全设置
|
||||
sessionTimeout: 30,
|
||||
passwordExpiration: 90,
|
||||
twoFactorAuth: false,
|
||||
// 运行规则
|
||||
maxDiscussionRounds: 10,
|
||||
maxRollbackTimes: 3,
|
||||
enableStateGate: true,
|
||||
enableLogicChainCheck: true,
|
||||
enableRiskReview: true,
|
||||
allowPartialParallel: true,
|
||||
});
|
||||
|
||||
const tabs = [
|
||||
{ id: 'general', label: '通用设置', icon: Settings },
|
||||
{ id: 'user', label: '用户管理', icon: Users },
|
||||
{ id: 'agent', label: '智能体管理', icon: Bot },
|
||||
{ id: 'notification', label: '通知设置', icon: Bell },
|
||||
{ id: 'security', label: '安全设置', icon: Shield },
|
||||
{ id: 'workflow', label: '运行规则', icon: Workflow },
|
||||
{ id: 'template', label: '模板管理', icon: FileText },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 顶部标题栏 */}
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-4`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-10 h-10 rounded-lg bg-blue-500/20 flex items-center justify-center">
|
||||
<Settings className="w-5 h-5 text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className={`${textPrimary} font-bold text-lg`}>系统设置</h1>
|
||||
<p className={`${textSecondary} text-sm`}>管理智能策略系统的各项配置</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="px-3 py-1.5 bg-green-500/20 text-green-400 rounded-full text-sm font-medium flex items-center gap-1.5">
|
||||
<span className="w-2 h-2 bg-green-400 rounded-full" />
|
||||
系统运行正常
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-12 gap-6">
|
||||
{/* 左侧导航 (20%) */}
|
||||
<div className="col-span-3">
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-4 sticky top-6`}>
|
||||
<h3 className={`${textSecondary} text-xs font-medium mb-3 uppercase tracking-wider`}>设置分类</h3>
|
||||
<nav className="space-y-1">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-left text-sm transition-colors ${
|
||||
activeTab === tab.id
|
||||
? 'bg-blue-600/20 text-blue-400 border border-blue-500/30'
|
||||
: `${textSecondary} hover:bg-slate-700/50 hover:${textPrimary}`
|
||||
}`}
|
||||
>
|
||||
<tab.icon className="w-4 h-4" />
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* 系统信息 */}
|
||||
<div className="mt-6 pt-4 border-t border-blue-800/30">
|
||||
<h3 className={`${textSecondary} text-xs font-medium mb-3 uppercase tracking-wider`}>系统信息</h3>
|
||||
<div className="space-y-2 text-xs">
|
||||
<div className="flex justify-between">
|
||||
<span className={textMuted}>版本号</span>
|
||||
<span className="text-cyan-400">{settings.systemVersion}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className={textMuted}>运行环境</span>
|
||||
<span className={textSecondary}>生产环境</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className={textMuted}>最后更新</span>
|
||||
<span className={textSecondary}>2026-03-19</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧设置区 (80%) */}
|
||||
<div className="col-span-9">
|
||||
{/* 通用设置 */}
|
||||
{activeTab === 'general' && (
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-6`}>
|
||||
<h2 className={`${textPrimary} font-semibold text-lg mb-6 flex items-center gap-2`}>
|
||||
<Settings className="w-5 h-5 text-blue-400" />
|
||||
通用设置
|
||||
</h2>
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className={`block ${textSecondary} text-sm mb-2`}>系统名称</label>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.systemName}
|
||||
onChange={(e) => setSettings({ ...settings, systemName: e.target.value })}
|
||||
className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm`}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={`block ${textSecondary} text-sm mb-2`}>系统版本</label>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.systemVersion}
|
||||
disabled
|
||||
className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textMuted} text-sm`}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={`block ${textSecondary} text-sm mb-2`}>语言</label>
|
||||
<select
|
||||
value={settings.language}
|
||||
onChange={(e) => setSettings({ ...settings, language: e.target.value })}
|
||||
className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm`}
|
||||
>
|
||||
<option value="zh-CN">简体中文</option>
|
||||
<option value="en-US">English</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className={`block ${textSecondary} text-sm mb-2`}>时区</label>
|
||||
<select
|
||||
value={settings.timezone}
|
||||
onChange={(e) => setSettings({ ...settings, timezone: e.target.value })}
|
||||
className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm`}
|
||||
>
|
||||
<option value="Asia/Shanghai">北京时间 (UTC+8)</option>
|
||||
<option value="UTC">世界时间 (UTC)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-blue-800/30">
|
||||
<div className="flex items-center justify-between p-4 bg-blue-500/10 border border-blue-500/30 rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<Zap className="w-5 h-5 text-blue-400" />
|
||||
<div>
|
||||
<p className={`${textPrimary} text-sm font-medium`}>自动保存设置</p>
|
||||
<p className={`${textSecondary} text-xs`}>开启后,系统会自动保存您的设置更改</p>
|
||||
</div>
|
||||
</div>
|
||||
<button className={`px-4 py-2 ${btnPrimary} ${textPrimary} rounded-lg text-sm transition-colors`}>
|
||||
已开启
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 用户管理 */}
|
||||
{activeTab === 'user' && (
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-6`}>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className={`${textPrimary} font-semibold text-lg flex items-center gap-2`}>
|
||||
<Users className="w-5 h-5 text-cyan-400" />
|
||||
用户管理
|
||||
</h2>
|
||||
<button className={`px-4 py-2 ${btnPrimary} ${textPrimary} rounded-lg text-sm transition-colors flex items-center gap-2`}>
|
||||
<User className="w-4 h-4" />
|
||||
添加用户
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{ name: '张策略', role: '管理员', email: 'zhang@company.com', status: 'active' },
|
||||
{ name: '李智能', role: '策略分析师', email: 'li@company.com', status: 'active' },
|
||||
{ name: '王智能体', role: '智能体管理员', email: 'wang@company.com', status: 'active' },
|
||||
{ name: '赵运维', role: '运维工程师', email: 'zhao@company.com', status: 'inactive' },
|
||||
].map((user, i) => (
|
||||
<div key={i} className={`flex items-center justify-between p-4 ${inputBg} ${inputBorder} rounded-lg`}>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-10 h-10 rounded-full bg-blue-500/20 flex items-center justify-center">
|
||||
<User className="w-5 h-5 text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className={`${textPrimary} text-sm font-medium`}>{user.name}</p>
|
||||
<p className={`${textSecondary} text-xs`}>{user.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className={`px-3 py-1 rounded-full text-xs ${
|
||||
user.role === '管理员' ? 'bg-purple-500/20 text-purple-400' : 'bg-blue-500/20 text-blue-400'
|
||||
}`}>
|
||||
{user.role}
|
||||
</span>
|
||||
<span className={`px-3 py-1 rounded-full text-xs ${
|
||||
user.status === 'active' ? 'bg-green-500/20 text-green-400' : 'bg-slate-500/20 text-slate-400'
|
||||
}`}>
|
||||
{user.status === 'active' ? '在线' : '离线'}
|
||||
</span>
|
||||
<button className="p-2 hover:bg-slate-700 rounded-lg transition-colors">
|
||||
<ChevronRight className={`w-4 h-4 ${textSecondary}`} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 智能体管理 */}
|
||||
{activeTab === 'agent' && (
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-6`}>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className={`${textPrimary} font-semibold text-lg flex items-center gap-2`}>
|
||||
<Bot className="w-5 h-5 text-purple-400" />
|
||||
智能体管理
|
||||
</h2>
|
||||
<button className={`px-4 py-2 ${btnPrimary} ${textPrimary} rounded-lg text-sm transition-colors flex items-center gap-2`}>
|
||||
<Bot className="w-4 h-4" />
|
||||
添加智能体
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{[
|
||||
{ name: 'GoalAgent', type: '目标分析', status: 'running', tasks: 156 },
|
||||
{ name: 'AudienceAgent', type: '受众分析', status: 'running', tasks: 142 },
|
||||
{ name: 'BehaviorAgent', type: '行为分析', status: 'idle', tasks: 98 },
|
||||
{ name: 'DriverAgent', type: '驱动分析', status: 'idle', tasks: 87 },
|
||||
{ name: 'NarrativeAgent', type: '叙事分析', status: 'idle', tasks: 76 },
|
||||
{ name: 'LogicChainAgent', type: '逻辑链收敛', status: 'idle', tasks: 65 },
|
||||
].map((agent, i) => (
|
||||
<div key={i} className={`p-4 ${inputBg} ${inputBorder} rounded-lg`}>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Bot className="w-4 h-4 text-purple-400" />
|
||||
<span className={`${textPrimary} text-sm font-medium`}>{agent.name}</span>
|
||||
</div>
|
||||
<span className={`px-2 py-0.5 rounded text-xs ${
|
||||
agent.status === 'running' ? 'bg-green-500/20 text-green-400' : 'bg-slate-500/20 text-slate-400'
|
||||
}`}>
|
||||
{agent.status === 'running' ? '运行中' : '空闲'}
|
||||
</span>
|
||||
</div>
|
||||
<p className={`${textSecondary} text-xs mb-3`}>{agent.type}</p>
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className={textMuted}>已完成任务</span>
|
||||
<span className="text-cyan-400">{agent.tasks}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 通知设置 */}
|
||||
{activeTab === 'notification' && (
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-6`}>
|
||||
<h2 className={`${textPrimary} font-semibold text-lg mb-6 flex items-center gap-2`}>
|
||||
<Bell className="w-5 h-5 text-amber-400" />
|
||||
通知设置
|
||||
</h2>
|
||||
<div className="space-y-4">
|
||||
{[
|
||||
{ id: 'email', label: '邮件通知', description: '接收任务完成、风险提醒等邮件通知', icon: Mail, enabled: settings.emailNotification },
|
||||
{ id: 'inApp', label: '站内通知', description: '在系统内显示通知消息', icon: MessageSquare, enabled: settings.inAppNotification },
|
||||
{ id: 'sms', label: '短信通知', description: '发送重要事项的短信提醒', icon: Smartphone, enabled: settings.smsNotification },
|
||||
].map((item) => (
|
||||
<div key={item.id} className={`flex items-center justify-between p-4 ${inputBg} ${inputBorder} rounded-lg`}>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-10 h-10 rounded-lg bg-blue-500/20 flex items-center justify-center">
|
||||
<item.icon className="w-5 h-5 text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className={`${textPrimary} text-sm font-medium`}>{item.label}</p>
|
||||
<p className={`${textSecondary} text-xs`}>{item.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setSettings({ ...settings, [item.id === 'email' ? 'emailNotification' : item.id === 'inApp' ? 'inAppNotification' : 'smsNotification']: !item.enabled })}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
item.enabled
|
||||
? 'bg-green-600 hover:bg-green-500 text-white'
|
||||
: btnSecondary
|
||||
}`}
|
||||
>
|
||||
{item.enabled ? '已开启' : '已关闭'}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="pt-4 border-t border-blue-800/30">
|
||||
<div>
|
||||
<label className={`block ${textSecondary} text-sm mb-2`}>通知时段</label>
|
||||
<div className="flex items-center gap-4">
|
||||
<select className={`px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm`}>
|
||||
<option>09:00</option>
|
||||
<option>10:00</option>
|
||||
<option>11:00</option>
|
||||
</select>
|
||||
<span className={textMuted}>至</span>
|
||||
<select className={`px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm`}>
|
||||
<option>18:00</option>
|
||||
<option>19:00</option>
|
||||
<option>20:00</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 安全设置 */}
|
||||
{activeTab === 'security' && (
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-6`}>
|
||||
<h2 className={`${textPrimary} font-semibold text-lg mb-6 flex items-center gap-2`}>
|
||||
<Shield className="w-5 h-5 text-red-400" />
|
||||
安全设置
|
||||
</h2>
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className={`block ${textSecondary} text-sm mb-2`}>会话超时时间(分钟)</label>
|
||||
<select
|
||||
value={settings.sessionTimeout}
|
||||
onChange={(e) => setSettings({ ...settings, sessionTimeout: Number(e.target.value) })}
|
||||
className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm`}
|
||||
>
|
||||
<option value={15}>15 分钟</option>
|
||||
<option value={30}>30 分钟</option>
|
||||
<option value={60}>1 小时</option>
|
||||
<option value={120}>2 小时</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className={`block ${textSecondary} text-sm mb-2`}>密码过期时间(天)</label>
|
||||
<select
|
||||
value={settings.passwordExpiration}
|
||||
onChange={(e) => setSettings({ ...settings, passwordExpiration: Number(e.target.value) })}
|
||||
className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm`}
|
||||
>
|
||||
<option value={30}>30 天</option>
|
||||
<option value={60}>60 天</option>
|
||||
<option value={90}>90 天</option>
|
||||
<option value={180}>180 天</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`flex items-center justify-between p-4 ${inputBg} ${inputBorder} rounded-lg`}>
|
||||
<div className="flex items-center gap-4">
|
||||
<Key className="w-5 h-5 text-purple-400" />
|
||||
<div>
|
||||
<p className={`${textPrimary} text-sm font-medium`}>双因素认证</p>
|
||||
<p className={`${textSecondary} text-xs`}>登录时需要额外的验证码验证</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setSettings({ ...settings, twoFactorAuth: !settings.twoFactorAuth })}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
settings.twoFactorAuth
|
||||
? 'bg-green-600 hover:bg-green-500 text-white'
|
||||
: btnSecondary
|
||||
}`}
|
||||
>
|
||||
{settings.twoFactorAuth ? '已开启' : '已关闭'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-blue-800/30">
|
||||
<button className="flex items-center gap-2 px-4 py-2.5 bg-red-600/20 hover:bg-red-600/30 border border-red-500/30 text-red-400 rounded-lg text-sm transition-colors">
|
||||
<Shield className="w-4 h-4" />
|
||||
重置所有安全设置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 运行规则 */}
|
||||
{activeTab === 'workflow' && (
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-6`}>
|
||||
<h2 className={`${textPrimary} font-semibold text-lg mb-6 flex items-center gap-2`}>
|
||||
<Workflow className="w-5 h-5 text-green-400" />
|
||||
运行规则配置
|
||||
</h2>
|
||||
<div className="space-y-4">
|
||||
{[
|
||||
{ id: 'enableStateGate', label: '启用状态门控', description: '智能体输出需要通过状态门控检查才能进入下一节点' },
|
||||
{ id: 'enableLogicChainCheck', label: '启用逻辑链校验', description: '在逻辑链收敛阶段进行一致性检查' },
|
||||
{ id: 'enableRiskReview', label: '启用风险强制复核', description: '高风险项必须经过人工确认才能继续' },
|
||||
{ id: 'allowPartialParallel', label: '允许局部并行', description: '独立的流程节点可以并行执行' },
|
||||
].map((item) => (
|
||||
<div key={item.id} className={`flex items-center justify-between p-4 ${inputBg} ${inputBorder} rounded-lg`}>
|
||||
<div>
|
||||
<p className={`${textPrimary} text-sm font-medium`}>{item.label}</p>
|
||||
<p className={`${textSecondary} text-xs mt-1`}>{item.description}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setSettings({ ...settings, [item.id]: !settings[item.id as keyof typeof settings] as boolean })}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
settings[item.id as keyof typeof settings]
|
||||
? 'bg-green-600 hover:bg-green-500 text-white'
|
||||
: btnSecondary
|
||||
}`}
|
||||
>
|
||||
{settings[item.id as keyof typeof settings] ? '已开启' : '已关闭'}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="grid grid-cols-2 gap-6 pt-4 border-t border-blue-800/30">
|
||||
<div>
|
||||
<label className={`block ${textSecondary} text-sm mb-2`}>最大讨论轮数</label>
|
||||
<input
|
||||
type="number"
|
||||
value={settings.maxDiscussionRounds}
|
||||
onChange={(e) => setSettings({ ...settings, maxDiscussionRounds: Number(e.target.value) })}
|
||||
className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm`}
|
||||
/>
|
||||
<p className={`${textMuted} text-xs mt-1`}>超出此轮数将自动收敛</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className={`block ${textSecondary} text-sm mb-2`}>最大回退次数</label>
|
||||
<input
|
||||
type="number"
|
||||
value={settings.maxRollbackTimes}
|
||||
onChange={(e) => setSettings({ ...settings, maxRollbackTimes: Number(e.target.value) })}
|
||||
className={`w-full px-4 py-2.5 ${inputBg} ${inputBorder} rounded-lg ${textPrimary} text-sm`}
|
||||
/>
|
||||
<p className={`${textMuted} text-xs mt-1`}>超出此次数将终止任务</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 模板管理 */}
|
||||
{activeTab === 'template' && (
|
||||
<div className={`${cardBg} backdrop-blur-xl ${cardBorder} rounded-xl p-6`}>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className={`${textPrimary} font-semibold text-lg flex items-center gap-2`}>
|
||||
<FileText className="w-5 h-5 text-cyan-400" />
|
||||
模板管理
|
||||
</h2>
|
||||
<button className={`px-4 py-2 ${btnPrimary} ${textPrimary} rounded-lg text-sm transition-colors flex items-center gap-2`}>
|
||||
<FileText className="w-4 h-4" />
|
||||
新建模板
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{[
|
||||
{ name: '宣传策略模板', type: '策略生成', fields: 24, updated: '2026-03-15' },
|
||||
{ name: '活动方案模板', type: '方案推演', fields: 32, updated: '2026-03-10' },
|
||||
{ name: '市场分析模板', type: '市场调研', fields: 18, updated: '2026-03-08' },
|
||||
{ name: '产品推广模板', type: '产品营销', fields: 28, updated: '2026-03-05' },
|
||||
{ name: '品牌公关模板', type: '公关传播', fields: 22, updated: '2026-03-01' },
|
||||
{ name: '用户调研模板', type: '用户洞察', fields: 16, updated: '2026-02-28' },
|
||||
].map((template, i) => (
|
||||
<div key={i} className={`p-4 ${inputBg} ${inputBorder} rounded-lg hover:border-blue-500/50 transition-colors cursor-pointer`}>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText className="w-4 h-4 text-cyan-400" />
|
||||
<span className={`${textPrimary} text-sm font-medium`}>{template.name}</span>
|
||||
</div>
|
||||
<ChevronRight className={`w-4 h-4 ${textSecondary}`} />
|
||||
</div>
|
||||
<p className={`${textSecondary} text-xs mb-2`}>{template.type}</p>
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className={textMuted}>{template.fields} 个字段</span>
|
||||
<span className={textMuted}>更新于 {template.updated}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 保存按钮 */}
|
||||
<div className="mt-6 flex items-center justify-end gap-3">
|
||||
<button className={`px-6 py-2.5 ${btnSecondary} rounded-lg text-sm transition-colors flex items-center gap-2`}>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
重置
|
||||
</button>
|
||||
<button className={`px-6 py-2.5 ${btnPrimary} ${textPrimary} rounded-lg text-sm transition-colors flex items-center gap-2`}>
|
||||
<Save className="w-4 h-4" />
|
||||
保存设置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemSettings;
|
||||
@@ -0,0 +1,166 @@
|
||||
import { Outlet } from 'react-router-dom';
|
||||
export interface SceneInfo {
|
||||
applicableTask: string;
|
||||
createTime: string;
|
||||
deleted: number;
|
||||
id: number;
|
||||
relatedAgent: string;
|
||||
sceneCode: string;
|
||||
sceneIntro: string;
|
||||
sceneName: string;
|
||||
sceneType: string;
|
||||
status: number;
|
||||
updateTime: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface TaskListItem {
|
||||
createBy: number;
|
||||
createTime: string;
|
||||
deptId: null;
|
||||
id: number;
|
||||
isTop: number;
|
||||
issuingDept: string;
|
||||
keywords: null;
|
||||
overview: string;
|
||||
planTaskTop: number;
|
||||
platform: null;
|
||||
ptaskId: null;
|
||||
recommendedBasis: null;
|
||||
scene: string; // 或 SceneInfo,如果你已经解析
|
||||
sourceType: number;
|
||||
step: null;
|
||||
taskContent: string;
|
||||
taskDirection: string;
|
||||
taskEndDate: string;
|
||||
taskLevel: number;
|
||||
taskName: string;
|
||||
taskNo: string;
|
||||
taskStartDate: string;
|
||||
taskStatus: number;
|
||||
taskSuperiorFlag: number;
|
||||
taskTimeLimit: null;
|
||||
taskType: number;
|
||||
column: string;
|
||||
updateBy: number;
|
||||
updateTime: string;
|
||||
endCount: number;
|
||||
sumAll: number;
|
||||
progress: number;
|
||||
taskStatusData: any;
|
||||
}
|
||||
|
||||
export interface ActionListItem {
|
||||
actionDay: number;
|
||||
actionEndDate: string;
|
||||
actionManager: number;
|
||||
actionName: string;
|
||||
actionNo: any;
|
||||
actionStartDate: string;
|
||||
actionStatus: number;
|
||||
actionTerm: number;
|
||||
actionTitle: any;
|
||||
actorName: any;
|
||||
audienceGroup: any
|
||||
audienceGroupId: any;
|
||||
audienceRecReason: any
|
||||
behavior: any
|
||||
createBy: number
|
||||
createTime: string;
|
||||
deleteFlag: number;
|
||||
deptId: any;
|
||||
deptName: string;
|
||||
drivingFactor: any;
|
||||
expectedBehaviorId: any;
|
||||
id: number
|
||||
keywords: string
|
||||
level: number
|
||||
scene: string
|
||||
sourceType: number
|
||||
step: null
|
||||
taskId: number
|
||||
updateBy: number
|
||||
updateTime: number
|
||||
useActionId: null
|
||||
}
|
||||
|
||||
export interface actionColumnItem {
|
||||
icon: string;
|
||||
columnName: string;
|
||||
actionList: ActionListItem[]
|
||||
}
|
||||
|
||||
export const taskStatusList = {
|
||||
'0': {
|
||||
label: '待签收',
|
||||
color: '#0062d9',
|
||||
icon: '待签收.svg'
|
||||
},
|
||||
'10': {
|
||||
label: '待分析',
|
||||
color: '#2f9acc',
|
||||
icon: '待CEH.svg'
|
||||
},
|
||||
'20': {
|
||||
label: '分析中',
|
||||
color: '#d97b00',
|
||||
icon: '进行中.svg'
|
||||
},
|
||||
'25': {
|
||||
label: '分析完成已提交',
|
||||
color: '#1eb07f',
|
||||
icon: '完成.svg'
|
||||
},
|
||||
'30': {
|
||||
label: '已分析待审核',
|
||||
color: '#1eb07f',
|
||||
icon: '完成.svg'
|
||||
},
|
||||
'40': {
|
||||
label: '分析审核通过',
|
||||
color: '#1eb07f',
|
||||
icon: '完成.svg'
|
||||
},
|
||||
'50': {
|
||||
label: '分析审核未通过',
|
||||
color: '#cf1322',
|
||||
icon: '未通过.svg'
|
||||
},
|
||||
'60': {
|
||||
label: '行动已下发待签收',
|
||||
color: '#1eb07f',
|
||||
icon: '完成.svg'
|
||||
},
|
||||
'70': {
|
||||
label: '行动已签收待策划',
|
||||
color: '#1eb07f',
|
||||
icon: '完成.svg'
|
||||
},
|
||||
'80': {
|
||||
label: '行动策划已提交',
|
||||
color: '#1eb07f',
|
||||
icon: '完成.svg'
|
||||
},
|
||||
'100': {
|
||||
label: '作战方案待审核',
|
||||
color: '#1eb07f',
|
||||
icon: '完成.svg'
|
||||
},
|
||||
'110': {
|
||||
label: '作战方案审核通过',
|
||||
color: '#1eb07f',
|
||||
icon: '完成.svg'
|
||||
},
|
||||
'120': {
|
||||
label: '作战方案未通过',
|
||||
color: '#cf1322',
|
||||
icon: '未通过.svg'
|
||||
}
|
||||
};
|
||||
export default function TaskManagement() {
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,546 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import {
|
||||
FileBox,
|
||||
Search,
|
||||
Plus,
|
||||
Edit,
|
||||
Trash2,
|
||||
Download,
|
||||
Upload,
|
||||
Filter,
|
||||
Grid3X3,
|
||||
List,
|
||||
MoreVertical,
|
||||
ChevronRight,
|
||||
FileText,
|
||||
Image,
|
||||
Film,
|
||||
Music,
|
||||
Archive,
|
||||
ExternalLink,
|
||||
Copy,
|
||||
Share2,
|
||||
} from 'lucide-react';
|
||||
import { PageType } from '../components/Sidebar';
|
||||
|
||||
interface ZZResource {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'document' | 'image' | 'video' | 'audio' | 'archive' | 'other';
|
||||
size: string;
|
||||
category: string;
|
||||
tags: string[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
author: string;
|
||||
status: 'active' | 'archived' | 'draft';
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface ZZResourceManagementProps {
|
||||
onNavigate: (page: PageType) => void;
|
||||
}
|
||||
|
||||
const mockResources: ZZResource[] = [
|
||||
{
|
||||
id: 'zz-001',
|
||||
name: '品牌视觉规范手册',
|
||||
type: 'document',
|
||||
size: '15.2 MB',
|
||||
category: '品牌资产',
|
||||
tags: ['品牌', 'VI', '规范'],
|
||||
createdAt: '2024-01-15',
|
||||
updatedAt: '2024-03-10',
|
||||
author: '张三',
|
||||
status: 'active',
|
||||
description: '公司品牌视觉识别系统标准手册,包含LOGO、色彩、字体等规范'
|
||||
},
|
||||
{
|
||||
id: 'zz-002',
|
||||
name: '产品宣传视频素材',
|
||||
type: 'video',
|
||||
size: '256 MB',
|
||||
category: '营销素材',
|
||||
tags: ['视频', '宣传', '产品'],
|
||||
createdAt: '2024-02-20',
|
||||
updatedAt: '2024-03-08',
|
||||
author: '李四',
|
||||
status: 'active',
|
||||
description: '2024年第一季度产品宣传视频原始素材'
|
||||
},
|
||||
{
|
||||
id: 'zz-003',
|
||||
name: '活动背景音乐库',
|
||||
type: 'audio',
|
||||
size: '1.2 GB',
|
||||
category: '音频素材',
|
||||
tags: ['音乐', '背景', '活动'],
|
||||
createdAt: '2024-01-08',
|
||||
updatedAt: '2024-02-28',
|
||||
author: '王五',
|
||||
status: 'active',
|
||||
description: '各类活动背景音乐合集,包含不同风格和时长'
|
||||
},
|
||||
{
|
||||
id: 'zz-004',
|
||||
name: '图标矢量素材包',
|
||||
type: 'image',
|
||||
size: '45 MB',
|
||||
category: '设计素材',
|
||||
tags: ['图标', '矢量', 'UI'],
|
||||
createdAt: '2024-03-01',
|
||||
updatedAt: '2024-03-15',
|
||||
author: '赵六',
|
||||
status: 'active',
|
||||
description: '500+高质量矢量图标素材,适用于各类项目'
|
||||
},
|
||||
{
|
||||
id: 'zz-005',
|
||||
name: '历史项目归档',
|
||||
type: 'archive',
|
||||
size: '2.5 GB',
|
||||
category: '归档文件',
|
||||
tags: ['归档', '历史', '项目'],
|
||||
createdAt: '2023-12-01',
|
||||
updatedAt: '2024-01-20',
|
||||
author: '系统',
|
||||
status: 'archived',
|
||||
description: '2023年度已完成项目文件归档'
|
||||
},
|
||||
{
|
||||
id: 'zz-006',
|
||||
name: 'PPT模板套装',
|
||||
type: 'document',
|
||||
size: '88 MB',
|
||||
category: '模板素材',
|
||||
tags: ['PPT', '模板', '演示'],
|
||||
createdAt: '2024-02-10',
|
||||
updatedAt: '2024-03-12',
|
||||
author: '钱七',
|
||||
status: 'draft',
|
||||
description: '商务演示PPT模板,包含多种风格和场景'
|
||||
},
|
||||
];
|
||||
|
||||
const typeIcons: Record<ZZResource['type'], React.ReactNode> = {
|
||||
document: <FileText className="w-5 h-5" />,
|
||||
image: <Image className="w-5 h-5" />,
|
||||
video: <Film className="w-5 h-5" />,
|
||||
audio: <Music className="w-5 h-5" />,
|
||||
archive: <Archive className="w-5 h-5" />,
|
||||
other: <FileBox className="w-5 h-5" />,
|
||||
};
|
||||
|
||||
const typeColors: Record<ZZResource['type'], string> = {
|
||||
document: 'bg-blue-500/20 text-blue-400',
|
||||
image: 'bg-green-500/20 text-green-400',
|
||||
video: 'bg-purple-500/20 text-purple-400',
|
||||
audio: 'bg-orange-500/20 text-orange-400',
|
||||
archive: 'bg-gray-500/20 text-gray-400',
|
||||
other: 'bg-slate-500/20 text-slate-400',
|
||||
};
|
||||
|
||||
const statusColors: Record<ZZResource['status'], { bg: string; text: string; label: string }> = {
|
||||
active: { bg: 'bg-green-500/20', text: 'text-green-400', label: '使用中' },
|
||||
archived: { bg: 'bg-gray-500/20', text: 'text-gray-400', label: '已归档' },
|
||||
draft: { bg: 'bg-yellow-500/20', text: 'text-yellow-400', label: '草稿' },
|
||||
};
|
||||
|
||||
const categories = ['全部', '品牌资产', '营销素材', '音频素材', '设计素材', '模板素材', '归档文件'];
|
||||
|
||||
const ZZResourceManagement: React.FC<ZZResourceManagementProps> = ({ onNavigate }) => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
|
||||
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedCategory, setSelectedCategory] = useState('全部');
|
||||
const [selectedStatus, setSelectedStatus] = useState<string>('all');
|
||||
const [selectedResource, setSelectedResource] = useState<ZZResource | null>(null);
|
||||
const [showUploadModal, setShowUploadModal] = useState(false);
|
||||
|
||||
const filteredResources = mockResources.filter(resource => {
|
||||
const matchesSearch = resource.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
resource.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
resource.tags.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase()));
|
||||
const matchesCategory = selectedCategory === '全部' || resource.category === selectedCategory;
|
||||
const matchesStatus = selectedStatus === 'all' || resource.status === selectedStatus;
|
||||
return matchesSearch && matchesCategory && matchesStatus;
|
||||
});
|
||||
|
||||
const stats = {
|
||||
total: mockResources.length,
|
||||
active: mockResources.filter(r => r.status === 'active').length,
|
||||
archived: mockResources.filter(r => r.status === 'archived').length,
|
||||
draft: mockResources.filter(r => r.status === 'draft').length,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 页面标题 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`p-2 rounded-lg ${isDark ? 'bg-blue-500/20' : 'bg-red-100'}`}>
|
||||
<FileBox className={`w-6 h-6 ${isDark ? 'text-blue-400' : 'text-red-500'}`} />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className={`text-2xl font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
||||
ZZ资源管理
|
||||
</h1>
|
||||
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
管理各类制作资源和素材文件
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowUploadModal(true)}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg font-medium transition-all ${
|
||||
isDark
|
||||
? 'bg-blue-600 hover:bg-blue-500 text-white'
|
||||
: 'bg-red-500 hover:bg-red-600 text-white'
|
||||
}`}
|
||||
>
|
||||
<Upload className="w-4 h-4" />
|
||||
上传资源
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<div className={`p-4 rounded-xl ${isDark ? 'bg-slate-800/50' : 'bg-white'} border ${isDark ? 'border-blue-800/30' : 'border-red-100'}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>资源总数</p>
|
||||
<p className={`text-2xl font-semibold mt-1 ${isDark ? 'text-white' : 'text-gray-900'}`}>{stats.total}</p>
|
||||
</div>
|
||||
<div className={`p-3 rounded-lg ${isDark ? 'bg-blue-500/20' : 'bg-red-50'}`}>
|
||||
<FileBox className={`w-6 h-6 ${isDark ? 'text-blue-400' : 'text-red-400'}`} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`p-4 rounded-xl ${isDark ? 'bg-slate-800/50' : 'bg-white'} border ${isDark ? 'border-green-800/30' : 'border-green-100'}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>使用中</p>
|
||||
<p className={`text-2xl font-semibold mt-1 ${isDark ? 'text-white' : 'text-gray-900'}`}>{stats.active}</p>
|
||||
</div>
|
||||
<div className={`p-3 rounded-lg bg-green-500/20`}>
|
||||
<FileText className="w-6 h-6 text-green-400" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`p-4 rounded-xl ${isDark ? 'bg-slate-800/50' : 'bg-white'} border ${isDark ? 'border-gray-700' : 'border-gray-200'}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>已归档</p>
|
||||
<p className={`text-2xl font-semibold mt-1 ${isDark ? 'text-white' : 'text-gray-900'}`}>{stats.archived}</p>
|
||||
</div>
|
||||
<div className={`p-3 rounded-lg bg-gray-500/20`}>
|
||||
<Archive className="w-6 h-6 text-gray-400" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`p-4 rounded-xl ${isDark ? 'bg-slate-800/50' : 'bg-white'} border ${isDark ? 'border-yellow-800/30' : 'border-yellow-100'}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>草稿</p>
|
||||
<p className={`text-2xl font-semibold mt-1 ${isDark ? 'text-white' : 'text-gray-900'}`}>{stats.draft}</p>
|
||||
</div>
|
||||
<div className={`p-3 rounded-lg bg-yellow-500/20`}>
|
||||
<Edit className="w-6 h-6 text-yellow-400" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 筛选和搜索 */}
|
||||
<div className={`p-4 rounded-xl ${isDark ? 'bg-slate-800/50' : 'bg-white'} border ${isDark ? 'border-blue-800/30' : 'border-red-100'}`}>
|
||||
<div className="flex items-center gap-4 flex-wrap">
|
||||
{/* 搜索框 */}
|
||||
<div className="flex-1 min-w-[200px] relative">
|
||||
<Search className={`absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 ${isDark ? 'text-slate-400' : 'text-gray-400'}`} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索资源名称、描述或标签..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className={`w-full pl-10 pr-4 py-2 rounded-lg border transition-all ${
|
||||
isDark
|
||||
? 'bg-slate-900/50 border-slate-700 text-white placeholder-slate-500 focus:border-blue-500'
|
||||
: 'bg-gray-50 border-gray-200 text-gray-900 placeholder-gray-400 focus:border-red-400'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 分类筛选 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Filter className={`w-4 h-4 ${isDark ? 'text-slate-400' : 'text-gray-400'}`} />
|
||||
<select
|
||||
value={selectedCategory}
|
||||
onChange={(e) => setSelectedCategory(e.target.value)}
|
||||
className={`px-3 py-2 rounded-lg border transition-all ${
|
||||
isDark
|
||||
? 'bg-slate-900/50 border-slate-700 text-white'
|
||||
: 'bg-gray-50 border-gray-200 text-gray-900'
|
||||
}`}
|
||||
>
|
||||
{categories.map(cat => (
|
||||
<option key={cat} value={cat}>{cat}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 状态筛选 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<select
|
||||
value={selectedStatus}
|
||||
onChange={(e) => setSelectedStatus(e.target.value)}
|
||||
className={`px-3 py-2 rounded-lg border transition-all ${
|
||||
isDark
|
||||
? 'bg-slate-900/50 border-slate-700 text-white'
|
||||
: 'bg-gray-50 border-gray-200 text-gray-900'
|
||||
}`}
|
||||
>
|
||||
<option value="all">全部状态</option>
|
||||
<option value="active">使用中</option>
|
||||
<option value="archived">已归档</option>
|
||||
<option value="draft">草稿</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 视图切换 */}
|
||||
<div className={`flex items-center rounded-lg p-1 ${isDark ? 'bg-slate-900/50' : 'bg-gray-100'}`}>
|
||||
<button
|
||||
onClick={() => setViewMode('grid')}
|
||||
className={`p-2 rounded-md transition-all ${
|
||||
viewMode === 'grid'
|
||||
? isDark ? 'bg-blue-500/20 text-blue-400' : 'bg-white text-red-500 shadow-sm'
|
||||
: isDark ? 'text-slate-400' : 'text-gray-500'
|
||||
}`}
|
||||
>
|
||||
<Grid3X3 className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('list')}
|
||||
className={`p-2 rounded-md transition-all ${
|
||||
viewMode === 'list'
|
||||
? isDark ? 'bg-blue-500/20 text-blue-400' : 'bg-white text-red-500 shadow-sm'
|
||||
: isDark ? 'text-slate-400' : 'text-gray-500'
|
||||
}`}
|
||||
>
|
||||
<List className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 资源列表 */}
|
||||
{viewMode === 'grid' ? (
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{filteredResources.map(resource => (
|
||||
<div
|
||||
key={resource.id}
|
||||
className={`p-4 rounded-xl cursor-pointer transition-all hover:scale-[1.02] ${
|
||||
isDark ? 'bg-slate-800/50 hover:bg-slate-800' : 'bg-white hover:bg-gray-50'
|
||||
} border ${isDark ? 'border-blue-800/30' : 'border-red-100'} hover:border-blue-500/50`}
|
||||
onClick={() => setSelectedResource(resource)}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className={`p-3 rounded-lg ${typeColors[resource.type]}`}>
|
||||
{typeIcons[resource.type]}
|
||||
</div>
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${statusColors[resource.status].bg} ${statusColors[resource.status].text}`}>
|
||||
{statusColors[resource.status].label}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className={`font-medium mb-1 ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
||||
{resource.name}
|
||||
</h3>
|
||||
<p className={`text-sm mb-3 line-clamp-2 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
{resource.description}
|
||||
</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
{resource.tags.slice(0, 2).map(tag => (
|
||||
<span
|
||||
key={tag}
|
||||
className={`px-2 py-0.5 rounded text-xs ${
|
||||
isDark ? 'bg-slate-700 text-slate-300' : 'bg-gray-100 text-gray-600'
|
||||
}`}
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<span className={`text-xs ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
|
||||
{resource.size}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className={`rounded-xl overflow-hidden border ${isDark ? 'bg-slate-800/50 border-blue-800/30' : 'bg-white border-red-100'}`}>
|
||||
<table className="w-full">
|
||||
<thead className={isDark ? 'bg-slate-900/50' : 'bg-gray-50'}>
|
||||
<tr>
|
||||
<th className={`px-4 py-3 text-left text-xs font-medium uppercase tracking-wider ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>资源名称</th>
|
||||
<th className={`px-4 py-3 text-left text-xs font-medium uppercase tracking-wider ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>类型</th>
|
||||
<th className={`px-4 py-3 text-left text-xs font-medium uppercase tracking-wider ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>分类</th>
|
||||
<th className={`px-4 py-3 text-left text-xs font-medium uppercase tracking-wider ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>大小</th>
|
||||
<th className={`px-4 py-3 text-left text-xs font-medium uppercase tracking-wider ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>状态</th>
|
||||
<th className={`px-4 py-3 text-left text-xs font-medium uppercase tracking-wider ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>更新日期</th>
|
||||
<th className={`px-4 py-3 text-right text-xs font-medium uppercase tracking-wider ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 dark:divide-slate-700">
|
||||
{filteredResources.map(resource => (
|
||||
<tr
|
||||
key={resource.id}
|
||||
className={`cursor-pointer transition-colors ${isDark ? 'hover:bg-slate-700/50' : 'hover:bg-gray-50'}`}
|
||||
onClick={() => setSelectedResource(resource)}
|
||||
>
|
||||
<td className={`px-4 py-3 ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`p-2 rounded-lg ${typeColors[resource.type]}`}>
|
||||
{typeIcons[resource.type]}
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">{resource.name}</p>
|
||||
<p className={`text-xs ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>{resource.author}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className={`px-4 py-3 ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>
|
||||
{resource.type === 'document' ? '文档' :
|
||||
resource.type === 'image' ? '图片' :
|
||||
resource.type === 'video' ? '视频' :
|
||||
resource.type === 'audio' ? '音频' :
|
||||
resource.type === 'archive' ? '归档' : '其他'}
|
||||
</td>
|
||||
<td className={`px-4 py-3 ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>{resource.category}</td>
|
||||
<td className={`px-4 py-3 ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>{resource.size}</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${statusColors[resource.status].bg} ${statusColors[resource.status].text}`}>
|
||||
{statusColors[resource.status].label}
|
||||
</span>
|
||||
</td>
|
||||
<td className={`px-4 py-3 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>{resource.updatedAt}</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center justify-end gap-2" onClick={(e) => e.stopPropagation()}>
|
||||
<button className={`p-1.5 rounded-lg transition-colors ${isDark ? 'hover:bg-slate-600 text-slate-400' : 'hover:bg-gray-100 text-gray-500'}`}>
|
||||
<Download className="w-4 h-4" />
|
||||
</button>
|
||||
<button className={`p-1.5 rounded-lg transition-colors ${isDark ? 'hover:bg-slate-600 text-slate-400' : 'hover:bg-gray-100 text-gray-500'}`}>
|
||||
<Edit className="w-4 h-4" />
|
||||
</button>
|
||||
<button className={`p-1.5 rounded-lg transition-colors ${isDark ? 'hover:bg-red-500/20 text-red-400' : 'hover:bg-red-50 text-red-500'}`}>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 资源详情弹窗 */}
|
||||
{selectedResource && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50" onClick={() => setSelectedResource(null)}>
|
||||
<div
|
||||
className={`w-full max-w-2xl rounded-2xl ${isDark ? 'bg-slate-800' : 'bg-white'} shadow-xl`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="p-6 border-b ${isDark ? 'border-slate-700' : 'border-gray-200'}">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`p-3 rounded-lg ${typeColors[selectedResource.type]}`}>
|
||||
{typeIcons[selectedResource.type]}
|
||||
</div>
|
||||
<div>
|
||||
<h2 className={`text-xl font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
||||
{selectedResource.name}
|
||||
</h2>
|
||||
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
{selectedResource.category} · {selectedResource.size}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setSelectedResource(null)}
|
||||
className={`p-2 rounded-lg ${isDark ? 'hover:bg-slate-700 text-slate-400' : 'hover:bg-gray-100 text-gray-500'}`}
|
||||
>
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
<div>
|
||||
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'} mb-1`}>描述</p>
|
||||
<p className={isDark ? 'text-white' : 'text-gray-900'}>{selectedResource.description}</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'} mb-1`}>创建者</p>
|
||||
<p className={isDark ? 'text-white' : 'text-gray-900'}>{selectedResource.author}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'} mb-1`}>创建时间</p>
|
||||
<p className={isDark ? 'text-white' : 'text-gray-900'}>{selectedResource.createdAt}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'} mb-1`}>更新时间</p>
|
||||
<p className={isDark ? 'text-white' : 'text-gray-900'}>{selectedResource.updatedAt}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'} mb-1`}>状态</p>
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${statusColors[selectedResource.status].bg} ${statusColors[selectedResource.status].text}`}>
|
||||
{statusColors[selectedResource.status].label}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'} mb-2`}>标签</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedResource.tags.map(tag => (
|
||||
<span
|
||||
key={tag}
|
||||
className={`px-3 py-1 rounded-full text-sm ${
|
||||
isDark ? 'bg-slate-700 text-slate-300' : 'bg-gray-100 text-gray-600'
|
||||
}`}
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`p-6 border-t ${isDark ? 'border-slate-700' : 'border-gray-200'} flex justify-end gap-3`}>
|
||||
<button
|
||||
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||
isDark ? 'bg-slate-700 hover:bg-slate-600 text-white' : 'bg-gray-100 hover:bg-gray-200 text-gray-700'
|
||||
}`}
|
||||
>
|
||||
<Download className="w-4 h-4 inline mr-2" />
|
||||
下载
|
||||
</button>
|
||||
<button
|
||||
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||
isDark ? 'bg-blue-600 hover:bg-blue-500 text-white' : 'bg-red-500 hover:bg-red-600 text-white'
|
||||
}`}
|
||||
>
|
||||
<Edit className="w-4 h-4 inline mr-2" />
|
||||
编辑
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ZZResourceManagement;
|
||||
@@ -0,0 +1,10 @@
|
||||
.iframe-container {
|
||||
width: 100%;
|
||||
height: calc(100vh - 100px); /* 可根据需求调整 */
|
||||
}
|
||||
|
||||
.full-size-iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
Reference in new issue
Block a user