fix: 修复TypeScript配置错误并更新项目文档
详细说明: - 修复了@n8n/config包的TypeScript配置错误 - 移除了不存在的jest-expect-message类型引用 - 清理了所有TypeScript构建缓存 - 更新了可行性分析文档,添加了技术实施方案 - 更新了Agent prompt文档 - 添加了会展策划工作流文档 - 包含了n8n-chinese-translation子项目 - 添加了exhibition-demo展示系统框架
This commit is contained in:
156
web_frontend/exhibition-demo/src/components/AgentCard.tsx
Normal file
156
web_frontend/exhibition-demo/src/components/AgentCard.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Agent } from '../store/demoStore';
|
||||
import { Loader2, Check, Brain } from 'lucide-react';
|
||||
|
||||
interface AgentCardProps {
|
||||
agent: Agent;
|
||||
isActive: boolean;
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
const AgentCard: React.FC<AgentCardProps> = ({ agent, isActive, compact = false }) => {
|
||||
const getStatusIcon = () => {
|
||||
switch (agent.status) {
|
||||
case 'thinking':
|
||||
return <Brain className="w-4 h-4 animate-pulse" />;
|
||||
case 'generating':
|
||||
return <Loader2 className="w-4 h-4 animate-spin" />;
|
||||
case 'done':
|
||||
return <Check className="w-4 h-4 text-green-500" />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = () => {
|
||||
switch (agent.status) {
|
||||
case 'thinking':
|
||||
return 'bg-blue-500';
|
||||
case 'generating':
|
||||
return 'bg-purple-500';
|
||||
case 'done':
|
||||
return 'bg-green-500';
|
||||
default:
|
||||
return 'bg-neutral-400';
|
||||
}
|
||||
};
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<motion.div
|
||||
className={`
|
||||
relative p-4 rounded-xl transition-all duration-300
|
||||
${isActive ? 'agent-card active' : 'agent-card'}
|
||||
${isActive ? 'scale-105' : 'hover:scale-102'}
|
||||
`}
|
||||
whileHover={{ y: -2 }}
|
||||
>
|
||||
{/* Status Indicator */}
|
||||
<div className={`absolute top-2 right-2 w-2 h-2 rounded-full ${getStatusColor()}`} />
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-2xl">{agent.icon}</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-semibold text-sm truncate">{agent.name}</h3>
|
||||
<p className="text-xs text-neutral-500 dark:text-neutral-400 truncate">
|
||||
{agent.model}
|
||||
</p>
|
||||
</div>
|
||||
{getStatusIcon()}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className={`
|
||||
relative p-6 rounded-2xl transition-all duration-300
|
||||
${isActive ? 'agent-card active' : 'agent-card'}
|
||||
`}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-4xl">{agent.icon}</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold">{agent.name}</h3>
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">
|
||||
{agent.model}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{getStatusIcon()}
|
||||
<div className={`w-3 h-3 rounded-full ${getStatusColor()} animate-pulse`} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Role Description */}
|
||||
<p className="text-sm text-neutral-600 dark:text-neutral-300 mb-4">
|
||||
{agent.role}
|
||||
</p>
|
||||
|
||||
{/* Prompt Display (if active) */}
|
||||
{isActive && agent.prompt && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
className="mt-4 p-4 bg-neutral-100 dark:bg-neutral-800 rounded-lg"
|
||||
>
|
||||
<h4 className="text-sm font-semibold mb-2 text-neutral-700 dark:text-neutral-300">
|
||||
Prompt:
|
||||
</h4>
|
||||
<TypewriterText text={agent.prompt} speed={40} />
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Output Display (if available) */}
|
||||
{agent.output && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
className="mt-4 p-4 bg-green-50 dark:bg-green-900/20 rounded-lg border border-green-200 dark:border-green-800"
|
||||
>
|
||||
<h4 className="text-sm font-semibold mb-2 text-green-700 dark:text-green-300">
|
||||
输出:
|
||||
</h4>
|
||||
<p className="text-sm text-neutral-700 dark:text-neutral-300">
|
||||
{agent.output}
|
||||
</p>
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
// Typewriter Component
|
||||
const TypewriterText: React.FC<{ text: string; speed: number }> = ({ text, speed }) => {
|
||||
const [displayedText, setDisplayedText] = React.useState('');
|
||||
const [currentIndex, setCurrentIndex] = React.useState(0);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (currentIndex < text.length) {
|
||||
const timeout = setTimeout(() => {
|
||||
setDisplayedText(prev => prev + text[currentIndex]);
|
||||
setCurrentIndex(prev => prev + 1);
|
||||
}, speed);
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
}, [currentIndex, text, speed]);
|
||||
|
||||
return (
|
||||
<p className="text-sm font-mono text-neutral-600 dark:text-neutral-400">
|
||||
{displayedText}
|
||||
{currentIndex < text.length && (
|
||||
<span className="inline-block w-2 h-4 bg-neutral-400 animate-pulse ml-1" />
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgentCard;
|
||||
104
web_frontend/exhibition-demo/src/components/ContentGenerator.tsx
Normal file
104
web_frontend/exhibition-demo/src/components/ContentGenerator.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { FileText, Image, Loader2 } from 'lucide-react';
|
||||
|
||||
interface ContentGeneratorProps {
|
||||
content?: string;
|
||||
images?: string[];
|
||||
speed?: number;
|
||||
}
|
||||
|
||||
const ContentGenerator: React.FC<ContentGeneratorProps> = ({
|
||||
content = "正在生成会展策划方案内容...",
|
||||
images = [],
|
||||
speed = 35
|
||||
}) => {
|
||||
const [displayedContent, setDisplayedContent] = useState('');
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const [loadingImages, setLoadingImages] = useState<Set<number>>(new Set());
|
||||
|
||||
useEffect(() => {
|
||||
if (currentIndex < content.length) {
|
||||
const timeout = setTimeout(() => {
|
||||
setDisplayedContent(prev => prev + content[currentIndex]);
|
||||
setCurrentIndex(prev => prev + 1);
|
||||
}, speed);
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
}, [currentIndex, content, speed]);
|
||||
|
||||
const handleImageLoad = (index: number) => {
|
||||
setLoadingImages(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(index);
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Text Content */}
|
||||
<div className="p-4 bg-neutral-50 dark:bg-neutral-900 rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<FileText className="w-4 h-4 text-blue-500" />
|
||||
<span className="text-sm font-medium text-neutral-600 dark:text-neutral-400">
|
||||
生成中...
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="font-mono text-sm text-neutral-700 dark:text-neutral-300 whitespace-pre-wrap">
|
||||
{displayedContent}
|
||||
{currentIndex < content.length && (
|
||||
<span className="inline-block w-2 h-4 bg-blue-500 animate-pulse ml-1" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Images */}
|
||||
{images.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Image className="w-4 h-4 text-purple-500" />
|
||||
<span className="text-sm font-medium text-neutral-600 dark:text-neutral-400">
|
||||
相关图片
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{images.map((src, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: index * 0.2 }}
|
||||
className="relative aspect-video rounded-lg overflow-hidden bg-neutral-100 dark:bg-neutral-800"
|
||||
>
|
||||
{loadingImages.has(index) && (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Loader2 className="w-6 h-6 animate-spin text-neutral-400" />
|
||||
</div>
|
||||
)}
|
||||
<img
|
||||
src={src}
|
||||
alt={`Generated content ${index + 1}`}
|
||||
className="w-full h-full object-cover"
|
||||
onLoad={() => handleImageLoad(index)}
|
||||
onLoadStart={() => setLoadingImages(prev => new Set(prev).add(index))}
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stats */}
|
||||
<div className="flex items-center gap-6 text-xs text-neutral-500 dark:text-neutral-400">
|
||||
<span>字符: {displayedContent.length}</span>
|
||||
<span>速度: {speed}字/秒</span>
|
||||
<span>进度: {Math.round((currentIndex / content.length) * 100)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContentGenerator;
|
||||
29
web_frontend/exhibition-demo/src/components/ProgressBar.tsx
Normal file
29
web_frontend/exhibition-demo/src/components/ProgressBar.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
interface ProgressBarProps {
|
||||
progress: number;
|
||||
}
|
||||
|
||||
const ProgressBar: React.FC<ProgressBarProps> = ({ progress }) => {
|
||||
return (
|
||||
<div className="relative w-full h-2 bg-neutral-200 dark:bg-neutral-800 rounded-full overflow-hidden">
|
||||
<motion.div
|
||||
className="absolute left-0 top-0 h-full bg-gradient-to-r from-blue-500 to-purple-500 rounded-full"
|
||||
initial={{ width: '0%' }}
|
||||
animate={{ width: `${progress}%` }}
|
||||
transition={{ duration: 0.5, ease: 'easeInOut' }}
|
||||
>
|
||||
{/* Shimmer effect */}
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent animate-shimmer" />
|
||||
</motion.div>
|
||||
|
||||
{/* Progress text */}
|
||||
<div className="absolute -top-8 left-0 text-sm text-neutral-600 dark:text-neutral-400">
|
||||
进度: {Math.round(progress)}%
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProgressBar;
|
||||
@@ -0,0 +1,178 @@
|
||||
import React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Agent } from '../store/demoStore';
|
||||
|
||||
interface WorkflowVisualizationProps {
|
||||
agents: Agent[];
|
||||
currentAgent: string | null;
|
||||
}
|
||||
|
||||
const WorkflowVisualization: React.FC<WorkflowVisualizationProps> = ({
|
||||
agents,
|
||||
currentAgent
|
||||
}) => {
|
||||
// 定义工作流布局
|
||||
const layout = {
|
||||
phase1: ['retrieval', 'design', 'budget'],
|
||||
coordinator: ['coordinator'],
|
||||
phase2: ['format', 'execution', 'marketing'],
|
||||
};
|
||||
|
||||
const getNodePosition = (agentId: string) => {
|
||||
if (layout.phase1.includes(agentId)) {
|
||||
const index = layout.phase1.indexOf(agentId);
|
||||
return { x: 100 + index * 150, y: 50 };
|
||||
}
|
||||
if (agentId === 'coordinator') {
|
||||
return { x: 250, y: 200 };
|
||||
}
|
||||
if (layout.phase2.includes(agentId)) {
|
||||
const index = layout.phase2.indexOf(agentId);
|
||||
return { x: 100 + index * 150, y: 350 };
|
||||
}
|
||||
return { x: 0, y: 0 };
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-[450px]">
|
||||
<svg className="absolute inset-0 w-full h-full">
|
||||
{/* Draw connections */}
|
||||
{/* Phase 1 to Coordinator */}
|
||||
{layout.phase1.map((agentId) => {
|
||||
const start = getNodePosition(agentId);
|
||||
const end = getNodePosition('coordinator');
|
||||
return (
|
||||
<motion.line
|
||||
key={`${agentId}-coordinator`}
|
||||
x1={start.x}
|
||||
y1={start.y + 30}
|
||||
x2={end.x}
|
||||
y2={end.y - 30}
|
||||
stroke="url(#gradient)"
|
||||
strokeWidth="2"
|
||||
strokeDasharray="5,5"
|
||||
initial={{ pathLength: 0 }}
|
||||
animate={{ pathLength: 1 }}
|
||||
transition={{ duration: 2, delay: 0.5 }}
|
||||
className={currentAgent === agentId ? 'animate-pulse' : ''}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Coordinator to Phase 2 */}
|
||||
{layout.phase2.map((agentId) => {
|
||||
const start = getNodePosition('coordinator');
|
||||
const end = getNodePosition(agentId);
|
||||
return (
|
||||
<motion.line
|
||||
key={`coordinator-${agentId}`}
|
||||
x1={start.x}
|
||||
y1={start.y + 30}
|
||||
x2={end.x}
|
||||
y2={end.y - 30}
|
||||
stroke="url(#gradient)"
|
||||
strokeWidth="2"
|
||||
strokeDasharray="5,5"
|
||||
initial={{ pathLength: 0 }}
|
||||
animate={{ pathLength: 1 }}
|
||||
transition={{ duration: 2, delay: 1 }}
|
||||
className={currentAgent === agentId ? 'animate-pulse' : ''}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Phase 2 back to Coordinator */}
|
||||
{layout.phase2.map((agentId) => {
|
||||
const start = getNodePosition(agentId);
|
||||
const end = getNodePosition('coordinator');
|
||||
return (
|
||||
<motion.line
|
||||
key={`${agentId}-coordinator-back`}
|
||||
x1={start.x}
|
||||
y1={start.y - 30}
|
||||
x2={end.x}
|
||||
y2={end.y + 30}
|
||||
stroke="url(#gradient)"
|
||||
strokeWidth="2"
|
||||
strokeDasharray="5,5"
|
||||
initial={{ pathLength: 0 }}
|
||||
animate={{ pathLength: 1 }}
|
||||
transition={{ duration: 2, delay: 1.5 }}
|
||||
className={currentAgent === agentId ? 'animate-pulse' : ''}
|
||||
opacity={0.5}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Gradient definition */}
|
||||
<defs>
|
||||
<linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stopColor="#3b82f6" />
|
||||
<stop offset="100%" stopColor="#8b5cf6" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
{/* Agent Nodes */}
|
||||
{agents.map((agent) => {
|
||||
const position = getNodePosition(agent.id);
|
||||
const isActive = currentAgent === agent.id;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={agent.id}
|
||||
className={`
|
||||
absolute transform -translate-x-1/2 -translate-y-1/2
|
||||
w-20 h-20 rounded-2xl flex flex-col items-center justify-center
|
||||
${isActive ? 'bg-gradient-to-br from-blue-500 to-purple-500 scale-110' : 'bg-white dark:bg-neutral-800'}
|
||||
${isActive ? 'shadow-2xl' : 'shadow-lg'}
|
||||
transition-all duration-300
|
||||
${!isActive && 'hover:scale-105'}
|
||||
border-2 ${isActive ? 'border-transparent' : 'border-neutral-200 dark:border-neutral-700'}
|
||||
`}
|
||||
style={{ left: position.x, top: position.y }}
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{
|
||||
delay: agents.indexOf(agent) * 0.1,
|
||||
type: 'spring',
|
||||
stiffness: 200
|
||||
}}
|
||||
>
|
||||
<span className={`text-2xl ${isActive ? 'scale-125' : ''}`}>
|
||||
{agent.icon}
|
||||
</span>
|
||||
<span className={`
|
||||
text-xs mt-1 font-medium text-center px-1
|
||||
${isActive ? 'text-white' : 'text-neutral-700 dark:text-neutral-300'}
|
||||
`}>
|
||||
{agent.name.split('专家')[0]}
|
||||
</span>
|
||||
|
||||
{/* Status dot */}
|
||||
<div className={`
|
||||
absolute -top-1 -right-1 w-3 h-3 rounded-full
|
||||
${agent.status === 'done' ? 'bg-green-500' : ''}
|
||||
${agent.status === 'thinking' ? 'bg-blue-500 animate-pulse' : ''}
|
||||
${agent.status === 'generating' ? 'bg-purple-500 animate-pulse' : ''}
|
||||
${agent.status === 'waiting' ? 'bg-neutral-400' : ''}
|
||||
`} />
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Labels */}
|
||||
<div className="absolute top-0 left-0 text-sm text-neutral-500 dark:text-neutral-400">
|
||||
第一阶段:信息收集
|
||||
</div>
|
||||
<div className="absolute top-[180px] left-[180px] text-sm text-neutral-500 dark:text-neutral-400">
|
||||
中央协调
|
||||
</div>
|
||||
<div className="absolute bottom-20 left-0 text-sm text-neutral-500 dark:text-neutral-400">
|
||||
第二阶段:方案细化
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkflowVisualization;
|
||||
Reference in New Issue
Block a user