fix: 修复TypeScript配置错误并更新项目文档

详细说明:
- 修复了@n8n/config包的TypeScript配置错误
- 移除了不存在的jest-expect-message类型引用
- 清理了所有TypeScript构建缓存
- 更新了可行性分析文档,添加了技术实施方案
- 更新了Agent prompt文档
- 添加了会展策划工作流文档
- 包含了n8n-chinese-translation子项目
- 添加了exhibition-demo展示系统框架
This commit is contained in:
Yep_Q
2025-09-08 10:49:45 +08:00
parent 8cf9d36d81
commit 3db7af209c
426 changed files with 71699 additions and 4401 deletions

View File

@@ -0,0 +1,151 @@
# AI 会展策划系统 - 多Agent协同演示
## 🚀 项目概述
这是一个基于React构建的多Agent协同演示系统展示了7个专业AI Agent如何协同工作生成完整的会展策划方案。设计风格参考了字节跳动、Flowith、Raycast等现代化产品。
## ✨ 核心特性
- **7个专业Agent**:信息检索、设计、财务预算、格式编辑、活动执行、营销宣传、中央协调
- **实时工作流可视化**动态展示Agent协作流程
- **打字机效果**30-40字/秒的文字生成动画
- **现代化UI设计**:简洁、实用、年轻化的界面风格
- **完整演示流程**约3分钟完整展示策划方案生成过程
## 🛠 技术栈
- **框架**: React 18 + TypeScript
- **样式**: Tailwind CSS
- **动画**: Framer Motion
- **状态管理**: Zustand
- **构建工具**: Vite
## 📦 安装与运行
### 前置要求
- Node.js >= 16.0.0
- npm >= 7.0.0 或 pnpm >= 6.0.0
### 安装步骤
```bash
# 进入项目目录
cd web_frontend/exhibition-demo
# 安装依赖推荐使用pnpm
pnpm install
# 或使用npm
npm install
# 启动开发服务器
pnpm dev
# 或
npm run dev
```
开发服务器将在 `http://localhost:5173` 启动
### 构建生产版本
```bash
# 构建项目
pnpm build
# 或
npm run build
# 预览构建结果
pnpm preview
# 或
npm run preview
```
## 📁 项目结构
```
exhibition-demo/
├── src/
│ ├── components/ # React组件
│ │ ├── AgentCard.tsx # Agent卡片组件
│ │ ├── ContentGenerator.tsx # 内容生成器
│ │ ├── ProgressBar.tsx # 进度条
│ │ └── WorkflowVisualization.tsx # 工作流可视化
│ ├── pages/ # 页面组件
│ │ ├── LandingPage.tsx # 启动页
│ │ ├── WorkflowPage.tsx # 工作流演示页
│ │ └── ResultPage.tsx # 结果展示页
│ ├── store/ # 状态管理
│ │ └── demoStore.ts # Zustand store
│ ├── styles/ # 样式文件
│ ├── utils/ # 工具函数
│ ├── App.tsx # 主应用组件
│ ├── main.tsx # 入口文件
│ └── index.css # 全局样式
├── public/ # 静态资源
├── index.html # HTML模板
├── package.json # 项目配置
├── tailwind.config.js # Tailwind配置
├── vite.config.ts # Vite配置
└── tsconfig.json # TypeScript配置
```
## 🎯 使用说明
1. **启动演示**:访问首页,点击"开始演示"按钮
2. **观看流程**系统将自动展示Agent协作过程
3. **查看结果**:演示完成后,可以浏览完整的策划方案
4. **控制选项**:支持暂停/继续/重新开始
## 🎨 设计特点
- **Glass Morphism**:玻璃态效果,增强层次感
- **渐变色彩**:蓝紫渐变主题,科技感十足
- **微动画**:精致的过渡和交互动画
- **响应式布局**:适配不同屏幕尺寸
## 📊 演示内容
演示案例为"2024长三角国际新能源汽车与智能交通产业博览会",包含:
1. **策划案概述**
2. **展会介绍与预期效果**
3. **营销方案**
4. **现场运营方案**
5. **预算与收益分析**
6. **风险评估与应急预案**
## 🔧 配置说明
### 调整演示速度
`src/store/demoStore.ts` 中修改:
```typescript
controls: {
speed: 1, // 1-5倍速
// ...
}
```
### 修改文字生成速度
在组件中调整 `speed` 参数默认35字/秒)
## 🤝 贡献指南
欢迎提交Issue和Pull Request来改进项目。
## 📝 许可证
MIT License
## 🙏 致谢
- n8n Workflow Platform
- DeepSeek AI
- Google Gemini
- 所有开源项目贡献者
---
如有问题,请联系项目维护者。

View File

@@ -0,0 +1,16 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AI 会展策划系统 - 多Agent协同演示</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,38 @@
{
"name": "exhibition-demo",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"framer-motion": "^11.0.0",
"zustand": "^4.5.0",
"react-markdown": "^9.0.0",
"react-syntax-highlighter": "^15.5.0",
"lucide-react": "^0.300.0",
"clsx": "^2.1.0",
"tailwind-merge": "^2.2.0"
},
"devDependencies": {
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"@types/react-syntax-highlighter": "^15.5.11",
"@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.16",
"eslint": "^8.55.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"postcss": "^8.4.32",
"tailwindcss": "^3.4.0",
"typescript": "^5.2.2",
"vite": "^5.0.8"
}
}

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@@ -0,0 +1,72 @@
import React, { useState } from 'react';
import { AnimatePresence, motion } from 'framer-motion';
import LandingPage from './pages/LandingPage';
import WorkflowPage from './pages/WorkflowPage';
import ResultPage from './pages/ResultPage';
import { useDemoStore } from './store/demoStore';
type PageType = 'landing' | 'workflow' | 'result';
function App() {
const [currentPage, setCurrentPage] = useState<PageType>('landing');
const { status } = useDemoStore();
React.useEffect(() => {
if (status === 'completed') {
setCurrentPage('result');
}
}, [status]);
const handleStartDemo = () => {
setCurrentPage('workflow');
};
const handleRestart = () => {
useDemoStore.getState().reset();
setCurrentPage('landing');
};
return (
<div className="min-h-screen bg-gradient-to-br from-neutral-50 to-neutral-100 dark:from-neutral-950 dark:to-neutral-900">
<AnimatePresence mode="wait">
{currentPage === 'landing' && (
<motion.div
key="landing"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.5 }}
>
<LandingPage onStart={handleStartDemo} />
</motion.div>
)}
{currentPage === 'workflow' && (
<motion.div
key="workflow"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.5 }}
>
<WorkflowPage />
</motion.div>
)}
{currentPage === 'result' && (
<motion.div
key="result"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.5 }}
>
<ResultPage onRestart={handleRestart} />
</motion.div>
)}
</AnimatePresence>
</div>
);
}
export default App;

View 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;

View 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;

View 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;

View File

@@ -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;

View File

@@ -0,0 +1,140 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--primary: 217.2 91.2% 59.8%;
--primary-foreground: 222.2 47.4% 11.2%;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--primary: 217.2 91.2% 59.8%;
--primary-foreground: 222.2 47.4% 11.2%;
}
* {
@apply border-neutral-200 dark:border-neutral-800;
}
body {
@apply bg-white dark:bg-neutral-950 text-neutral-900 dark:text-neutral-50;
font-feature-settings: "rlig" 1, "calt" 1;
}
}
@layer components {
.glass-morphism {
@apply backdrop-blur-xl bg-white/80 dark:bg-neutral-900/80 border border-neutral-200/50 dark:border-neutral-800/50;
}
.gradient-border {
background: linear-gradient(white, white) padding-box,
linear-gradient(to right, #3b82f6, #8b5cf6) border-box;
border: 2px solid transparent;
}
.text-gradient {
@apply bg-gradient-to-r from-blue-500 to-purple-500 bg-clip-text text-transparent;
}
.animate-typewriter {
overflow: hidden;
white-space: nowrap;
border-right: 2px solid;
animation: typing 3.5s steps(40, end), blink-caret 0.75s step-end infinite;
}
@keyframes typing {
from { width: 0 }
to { width: 100% }
}
@keyframes blink-caret {
from, to { border-color: transparent }
50% { border-color: currentColor }
}
.loading-spinner {
@apply inline-block w-8 h-8 border-4 border-neutral-200 dark:border-neutral-800 rounded-full;
border-top-color: #3b82f6;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg) }
}
.agent-card {
@apply relative overflow-hidden rounded-2xl p-6 transition-all duration-300;
@apply hover:scale-105 hover:shadow-2xl;
@apply bg-gradient-to-br from-neutral-50 to-neutral-100;
@apply dark:from-neutral-900 dark:to-neutral-800;
}
.agent-card.active {
@apply ring-2 ring-blue-500 ring-offset-2 dark:ring-offset-neutral-950;
animation: pulse-glow 2s infinite;
}
@keyframes pulse-glow {
0%, 100% {
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.4);
}
50% {
box-shadow: 0 0 0 10px rgba(59, 130, 246, 0);
}
}
.workflow-line {
stroke-dasharray: 1000;
stroke-dashoffset: 1000;
animation: draw 2s ease-in-out forwards;
}
@keyframes draw {
to {
stroke-dashoffset: 0;
}
}
.content-section {
@apply opacity-0 transform translate-y-4;
animation: fadeInUp 0.6s ease-out forwards;
}
@keyframes fadeInUp {
to {
opacity: 1;
transform: translateY(0);
}
}
}
@layer utilities {
.scrollbar-thin {
scrollbar-width: thin;
scrollbar-color: theme('colors.neutral.400') transparent;
}
.scrollbar-thin::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.scrollbar-thin::-webkit-scrollbar-track {
background: transparent;
}
.scrollbar-thin::-webkit-scrollbar-thumb {
@apply bg-neutral-400 dark:bg-neutral-600 rounded-full;
}
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
@apply bg-neutral-500 dark:bg-neutral-500;
}
}

View File

@@ -0,0 +1,10 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@@ -0,0 +1,131 @@
import React from 'react';
import { motion } from 'framer-motion';
import { ArrowRight, Sparkles, Layers, Zap } from 'lucide-react';
interface LandingPageProps {
onStart: () => void;
}
const LandingPage: React.FC<LandingPageProps> = ({ onStart }) => {
return (
<div className="min-h-screen flex items-center justify-center p-8">
<div className="max-w-4xl w-full">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="text-center space-y-8"
>
{/* Logo/Icon */}
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2, type: "spring", stiffness: 200 }}
className="inline-flex items-center justify-center w-24 h-24 rounded-3xl bg-gradient-to-br from-blue-500 to-purple-500 shadow-2xl"
>
<Sparkles className="w-12 h-12 text-white" />
</motion.div>
{/* Title */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.4 }}
>
<h1 className="text-6xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
AI
</h1>
<p className="mt-4 text-xl text-neutral-600 dark:text-neutral-400">
Agent协同 · ·
</p>
</motion.div>
{/* Features */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.6 }}
className="grid grid-cols-3 gap-6 mt-12"
>
<div className="glass-morphism rounded-2xl p-6 hover:scale-105 transition-transform">
<Layers className="w-8 h-8 text-blue-500 mb-4" />
<h3 className="font-semibold text-lg mb-2">7Agent</h3>
<p className="text-sm text-neutral-600 dark:text-neutral-400">
</p>
</div>
<div className="glass-morphism rounded-2xl p-6 hover:scale-105 transition-transform">
<Zap className="w-8 h-8 text-purple-500 mb-4" />
<h3 className="font-semibold text-lg mb-2"></h3>
<p className="text-sm text-neutral-600 dark:text-neutral-400">
</p>
</div>
<div className="glass-morphism rounded-2xl p-6 hover:scale-105 transition-transform">
<Sparkles className="w-8 h-8 text-green-500 mb-4" />
<h3 className="font-semibold text-lg mb-2"></h3>
<p className="text-sm text-neutral-600 dark:text-neutral-400">
</p>
</div>
</motion.div>
{/* Demo Info */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.8 }}
className="glass-morphism rounded-2xl p-8 text-left"
>
<h2 className="text-2xl font-semibold mb-4"></h2>
<div className="space-y-2 text-neutral-600 dark:text-neutral-400">
<p>
<span className="font-medium text-neutral-900 dark:text-neutral-100"></span>
2024
</p>
<p>
<span className="font-medium text-neutral-900 dark:text-neutral-100"></span>
50,00035050,000
</p>
<p>
<span className="font-medium text-neutral-900 dark:text-neutral-100"></span>
3
</p>
</div>
</motion.div>
{/* Start Button */}
<motion.button
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 1 }}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={onStart}
className="group relative inline-flex items-center gap-3 px-8 py-4 bg-gradient-to-r from-blue-500 to-purple-500 text-white font-semibold text-lg rounded-2xl shadow-xl hover:shadow-2xl transition-all"
>
<span></span>
<ArrowRight className="w-5 h-5 group-hover:translate-x-1 transition-transform" />
{/* Glow effect */}
<div className="absolute inset-0 rounded-2xl bg-gradient-to-r from-blue-500 to-purple-500 blur-xl opacity-50 group-hover:opacity-70 transition-opacity" />
</motion.button>
{/* Footer */}
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1.2 }}
className="text-sm text-neutral-500 dark:text-neutral-500 mt-8"
>
Powered by n8n Workflow · DeepSeek · Google Gemini
</motion.p>
</motion.div>
</div>
</div>
);
};
export default LandingPage;

View File

@@ -0,0 +1,261 @@
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
FileText,
Target,
TrendingUp,
Calendar,
DollarSign,
AlertTriangle,
ChevronLeft,
ChevronRight,
RotateCcw,
Download
} from 'lucide-react';
import { useDemoStore } from '../store/demoStore';
interface ResultPageProps {
onRestart: () => void;
}
const sections = [
{ id: 'overview', title: '策划案概述', icon: FileText },
{ id: 'introduction', title: '展会介绍与预期效果', icon: Target },
{ id: 'marketing', title: '营销方案', icon: TrendingUp },
{ id: 'operation', title: '现场运营方案', icon: Calendar },
{ id: 'budget', title: '预算与收益分析', icon: DollarSign },
{ id: 'risk', title: '风险评估与应急预案', icon: AlertTriangle },
];
const ResultPage: React.FC<ResultPageProps> = ({ onRestart }) => {
const [currentSection, setCurrentSection] = useState(0);
const { generatedContent } = useDemoStore();
const handlePrevious = () => {
if (currentSection > 0) {
setCurrentSection(currentSection - 1);
}
};
const handleNext = () => {
if (currentSection < sections.length - 1) {
setCurrentSection(currentSection + 1);
}
};
// Mock content for demonstration
const getSectionContent = (sectionId: string) => {
const mockContent = {
overview: {
title: '2024长三角国际新能源汽车与智能交通产业博览会',
content: `
## 策划背景
在全球碳中和目标推动下,新能源汽车产业迎来爆发式增长。长三角地区作为中国汽车产业核心集群,聚集了特斯拉、上汽、蔚来等龙头企业。
## 策划目的
- 打造长三角地区新能源汽车与智能交通领域第一展会品牌
- 吸引300家优质展商实现现场意向交易额超8亿元人民币
- 促进产业链上下游合作,推动技术创新和产业升级
- 推广绿色出行理念,助力碳中和目标实现
`,
images: ['/api/placeholder/600/400', '/api/placeholder/600/400']
},
introduction: {
title: '展会规模与预期',
content: `
## 展会主题
「智行未来,绿动长三角」
## 预计规模
- 展览面积50,000平方米
- 标准展位1,200个9平米/个)
- 特装展位20,000平方米
- 参展商家数350家
- 参观人次50,000人次
## 预期效果
- 现场成交额预计超10亿元
- 带动相关产业收入约30亿元
- 媒体曝光量超1亿次
`,
images: []
},
// Add more sections...
};
return mockContent[sectionId] || generatedContent[sectionId] || mockContent.overview;
};
const currentSectionData = getSectionContent(sections[currentSection].id);
return (
<div className="min-h-screen p-6">
<div className="max-w-7xl mx-auto">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="mb-8"
>
<div className="flex items-center justify-between">
<h1 className="text-4xl font-bold text-gradient">
</h1>
<div className="flex items-center gap-4">
<button
onClick={() => {}}
className="px-4 py-2 glass-morphism rounded-xl hover:scale-105 transition-transform flex items-center gap-2"
>
<Download className="w-4 h-4" />
</button>
<button
onClick={onRestart}
className="px-4 py-2 glass-morphism rounded-xl hover:scale-105 transition-transform flex items-center gap-2"
>
<RotateCcw className="w-4 h-4" />
</button>
</div>
</div>
</motion.div>
{/* Section Tabs */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2 }}
className="mb-8"
>
<div className="flex items-center gap-2 overflow-x-auto scrollbar-thin pb-2">
{sections.map((section, index) => {
const Icon = section.icon;
return (
<button
key={section.id}
onClick={() => setCurrentSection(index)}
className={`
flex items-center gap-2 px-4 py-2 rounded-xl transition-all whitespace-nowrap
${currentSection === index
? 'bg-gradient-to-r from-blue-500 to-purple-500 text-white shadow-lg'
: 'glass-morphism hover:scale-105'
}
`}
>
<Icon className="w-4 h-4" />
<span className="font-medium">{section.title}</span>
</button>
);
})}
</div>
</motion.div>
{/* Content Area */}
<AnimatePresence mode="wait">
<motion.div
key={currentSection}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.3 }}
className="glass-morphism rounded-2xl p-8"
>
{/* Section Header */}
<div className="mb-6">
<h2 className="text-3xl font-bold mb-2">
{currentSectionData.title}
</h2>
<div className="h-1 w-20 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full" />
</div>
{/* Section Content */}
<div className="prose prose-neutral dark:prose-invert max-w-none">
<div className="whitespace-pre-wrap text-neutral-700 dark:text-neutral-300">
{currentSectionData.content}
</div>
</div>
{/* Images */}
{currentSectionData.images && currentSectionData.images.length > 0 && (
<div className="mt-8 grid grid-cols-2 gap-4">
{currentSectionData.images.map((src, index) => (
<motion.div
key={index}
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: index * 0.1 }}
className="rounded-xl overflow-hidden shadow-lg"
>
<img
src={src}
alt={`Section image ${index + 1}`}
className="w-full h-full object-cover"
/>
</motion.div>
))}
</div>
)}
</motion.div>
</AnimatePresence>
{/* Navigation */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.4 }}
className="mt-8 flex items-center justify-between"
>
<button
onClick={handlePrevious}
disabled={currentSection === 0}
className={`
px-4 py-2 rounded-xl flex items-center gap-2 transition-all
${currentSection === 0
? 'opacity-50 cursor-not-allowed glass-morphism'
: 'glass-morphism hover:scale-105'
}
`}
>
<ChevronLeft className="w-4 h-4" />
</button>
{/* Page Indicators */}
<div className="flex items-center gap-2">
{sections.map((_, index) => (
<div
key={index}
className={`
w-2 h-2 rounded-full transition-all
${currentSection === index
? 'w-8 bg-gradient-to-r from-blue-500 to-purple-500'
: 'bg-neutral-400'
}
`}
/>
))}
</div>
<button
onClick={handleNext}
disabled={currentSection === sections.length - 1}
className={`
px-4 py-2 rounded-xl flex items-center gap-2 transition-all
${currentSection === sections.length - 1
? 'opacity-50 cursor-not-allowed glass-morphism'
: 'glass-morphism hover:scale-105'
}
`}
>
<ChevronRight className="w-4 h-4" />
</button>
</motion.div>
</div>
</div>
);
};
export default ResultPage;

View File

@@ -0,0 +1,169 @@
import React, { useEffect, useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { useDemoStore } from '../store/demoStore';
import AgentCard from '../components/AgentCard';
import WorkflowVisualization from '../components/WorkflowVisualization';
import ContentGenerator from '../components/ContentGenerator';
import ProgressBar from '../components/ProgressBar';
import { Play, Pause, RotateCcw } from 'lucide-react';
const WorkflowPage: React.FC = () => {
const {
status,
agents,
currentAgent,
progress,
startDemo,
pauseDemo,
resumeDemo,
reset,
} = useDemoStore();
const [showContent, setShowContent] = useState(false);
useEffect(() => {
// 自动开始演示
if (status === 'idle') {
setTimeout(() => {
startDemo();
}, 500);
}
}, [status, startDemo]);
const handleTogglePlay = () => {
if (status === 'running') {
pauseDemo();
} else if (status === 'paused') {
resumeDemo();
}
};
return (
<div className="min-h-screen p-6">
<div className="max-w-7xl mx-auto">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="mb-8"
>
<div className="flex items-center justify-between mb-4">
<h1 className="text-3xl font-bold text-gradient">
AI Agent
</h1>
{/* Control Buttons */}
<div className="flex items-center gap-4">
<button
onClick={handleTogglePlay}
className="p-3 rounded-xl glass-morphism hover:scale-105 transition-transform"
>
{status === 'running' ? (
<Pause className="w-5 h-5" />
) : (
<Play className="w-5 h-5" />
)}
</button>
<button
onClick={reset}
className="p-3 rounded-xl glass-morphism hover:scale-105 transition-transform"
>
<RotateCcw className="w-5 h-5" />
</button>
</div>
</div>
{/* Progress Bar */}
<ProgressBar progress={progress} />
</motion.div>
{/* Main Content Area */}
<div className="grid grid-cols-12 gap-6">
{/* Left: Workflow Visualization */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.2 }}
className="col-span-5"
>
<div className="glass-morphism rounded-2xl p-6 h-full">
<h2 className="text-xl font-semibold mb-4"></h2>
<WorkflowVisualization agents={agents} currentAgent={currentAgent} />
</div>
</motion.div>
{/* Right: Content Area */}
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.3 }}
className="col-span-7 space-y-6"
>
{/* Active Agent Display */}
<div className="glass-morphism rounded-2xl p-6">
<h2 className="text-xl font-semibold mb-4"></h2>
<AnimatePresence mode="wait">
{currentAgent && (
<motion.div
key={currentAgent}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
>
<AgentCard
agent={agents.find(a => a.id === currentAgent)!}
isActive={true}
/>
</motion.div>
)}
</AnimatePresence>
</div>
{/* Content Generation Display */}
{showContent && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="glass-morphism rounded-2xl p-6"
>
<h2 className="text-xl font-semibold mb-4"></h2>
<ContentGenerator />
</motion.div>
)}
</motion.div>
</div>
{/* Bottom: Agent List */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
className="mt-8"
>
<div className="glass-morphism rounded-2xl p-6">
<h2 className="text-xl font-semibold mb-4">Agent </h2>
<div className="grid grid-cols-4 gap-4">
{agents.map((agent) => (
<motion.div
key={agent.id}
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: agents.indexOf(agent) * 0.1 }}
>
<AgentCard
agent={agent}
isActive={currentAgent === agent.id}
compact={true}
/>
</motion.div>
))}
</div>
</div>
</motion.div>
</div>
</div>
);
};
export default WorkflowPage;

View File

@@ -0,0 +1,165 @@
import { create } from 'zustand';
export interface Agent {
id: string;
name: string;
icon: string;
model: string;
role: string;
status: 'waiting' | 'thinking' | 'generating' | 'done';
input?: string;
output?: string;
prompt?: string;
}
export interface DemoState {
// 演示状态
status: 'idle' | 'running' | 'paused' | 'completed';
currentPhase: number;
currentAgent: string | null;
progress: number;
// Agent配置
agents: Agent[];
// 生成的内容
generatedContent: {
[key: string]: {
title: string;
content: string;
images: string[];
isComplete: boolean;
};
};
// 控制选项
controls: {
speed: number; // 1-5倍速
autoAdvance: boolean;
showDetails: boolean;
};
// Actions
startDemo: () => void;
pauseDemo: () => void;
resumeDemo: () => void;
setCurrentAgent: (agentId: string) => void;
updateAgentStatus: (agentId: string, status: Agent['status']) => void;
updateAgentOutput: (agentId: string, output: string) => void;
addGeneratedContent: (section: string, content: any) => void;
setProgress: (progress: number) => void;
reset: () => void;
}
const initialAgents: Agent[] = [
{
id: 'retrieval',
name: '信息检索专家',
icon: '🔍',
model: 'DeepSeek Chat Model5',
role: '市场调研、数据收集、竞品分析',
status: 'waiting',
},
{
id: 'design',
name: '设计专家',
icon: '🎨',
model: 'Google Gemini Chat Model2',
role: '视觉设计、空间布局、品牌形象',
status: 'waiting',
},
{
id: 'budget',
name: '财务预算专家',
icon: '💰',
model: 'DeepSeek Chat Model2',
role: '成本核算、预算规划、ROI分析',
status: 'waiting',
},
{
id: 'format',
name: '格式编辑专家',
icon: '📝',
model: 'DeepSeek Chat Model4',
role: '文档格式化、内容结构优化',
status: 'waiting',
},
{
id: 'execution',
name: '活动执行专家',
icon: '⚡',
model: 'DeepSeek Chat Model1',
role: '执行计划、时间线管理、任务分配',
status: 'waiting',
},
{
id: 'marketing',
name: '营销宣传专家',
icon: '📢',
model: 'DeepSeek Chat Model3',
role: '推广策略、媒体规划、品牌传播',
status: 'waiting',
},
{
id: 'coordinator',
name: '会展策划专家',
icon: '🎯',
model: 'Chat Models + Memories',
role: '中央协调、方案整合、决策支持',
status: 'waiting',
},
];
export const useDemoStore = create<DemoState>((set) => ({
status: 'idle',
currentPhase: 0,
currentAgent: null,
progress: 0,
agents: initialAgents,
generatedContent: {},
controls: {
speed: 1,
autoAdvance: true,
showDetails: true,
},
startDemo: () => set({ status: 'running', progress: 0 }),
pauseDemo: () => set({ status: 'paused' }),
resumeDemo: () => set({ status: 'running' }),
setCurrentAgent: (agentId) => set({ currentAgent: agentId }),
updateAgentStatus: (agentId, status) =>
set((state) => ({
agents: state.agents.map((agent) =>
agent.id === agentId ? { ...agent, status } : agent
),
})),
updateAgentOutput: (agentId, output) =>
set((state) => ({
agents: state.agents.map((agent) =>
agent.id === agentId ? { ...agent, output } : agent
),
})),
addGeneratedContent: (section, content) =>
set((state) => ({
generatedContent: {
...state.generatedContent,
[section]: content,
},
})),
setProgress: (progress) => set({ progress }),
reset: () =>
set({
status: 'idle',
currentPhase: 0,
currentAgent: null,
progress: 0,
agents: initialAgents,
generatedContent: {},
}),
}));

View File

@@ -0,0 +1,86 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
colors: {
// 参考 Raycast / 字节跳动设计风格的配色
primary: {
50: '#f0f9ff',
100: '#e0f2fe',
200: '#bae6fd',
300: '#7dd3fc',
400: '#38bdf8',
500: '#0ea5e9',
600: '#0284c7',
700: '#0369a1',
800: '#075985',
900: '#0c4a6e',
},
neutral: {
50: '#fafafa',
100: '#f5f5f5',
200: '#e5e5e5',
300: '#d4d4d4',
400: '#a3a3a3',
500: '#737373',
600: '#525252',
700: '#404040',
800: '#262626',
900: '#171717',
950: '#0a0a0a',
},
accent: {
purple: '#8b5cf6',
blue: '#3b82f6',
green: '#10b981',
orange: '#f97316',
}
},
fontFamily: {
sans: ['Inter', 'SF Pro Display', '-apple-system', 'BlinkMacSystemFont', 'system-ui', 'sans-serif'],
mono: ['SF Mono', 'Monaco', 'Consolas', 'monospace'],
},
animation: {
'fade-in': 'fadeIn 0.5s ease-in-out',
'slide-up': 'slideUp 0.5s ease-out',
'pulse-soft': 'pulseSoft 2s infinite',
'typewriter': 'typewriter 0.1s steps(1)',
'glow': 'glow 2s ease-in-out infinite',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
slideUp: {
'0%': { transform: 'translateY(10px)', opacity: '0' },
'100%': { transform: 'translateY(0)', opacity: '1' },
},
pulseSoft: {
'0%, 100%': { opacity: '1' },
'50%': { opacity: '0.5' },
},
typewriter: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
glow: {
'0%, 100%': {
boxShadow: '0 0 5px rgba(59, 130, 246, 0.5), 0 0 20px rgba(59, 130, 246, 0.3)'
},
'50%': {
boxShadow: '0 0 20px rgba(59, 130, 246, 0.7), 0 0 40px rgba(59, 130, 246, 0.5)'
},
},
},
backdropBlur: {
xs: '2px',
},
},
},
plugins: [],
}

View File

@@ -0,0 +1,31 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
/* Path mapping */
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1,12 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': '/src',
},
},
})