fix: 修复TypeScript配置错误并更新项目文档
详细说明: - 修复了@n8n/config包的TypeScript配置错误 - 移除了不存在的jest-expect-message类型引用 - 清理了所有TypeScript构建缓存 - 更新了可行性分析文档,添加了技术实施方案 - 更新了Agent prompt文档 - 添加了会展策划工作流文档 - 包含了n8n-chinese-translation子项目 - 添加了exhibition-demo展示系统框架
This commit is contained in:
151
web_frontend/exhibition-demo/README.md
Normal file
151
web_frontend/exhibition-demo/README.md
Normal 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
|
||||
- 所有开源项目贡献者
|
||||
|
||||
---
|
||||
|
||||
如有问题,请联系项目维护者。
|
||||
16
web_frontend/exhibition-demo/index.html
Normal file
16
web_frontend/exhibition-demo/index.html
Normal 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>
|
||||
38
web_frontend/exhibition-demo/package.json
Normal file
38
web_frontend/exhibition-demo/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
6
web_frontend/exhibition-demo/postcss.config.js
Normal file
6
web_frontend/exhibition-demo/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
72
web_frontend/exhibition-demo/src/App.tsx
Normal file
72
web_frontend/exhibition-demo/src/App.tsx
Normal 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;
|
||||
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;
|
||||
140
web_frontend/exhibition-demo/src/index.css
Normal file
140
web_frontend/exhibition-demo/src/index.css
Normal 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;
|
||||
}
|
||||
}
|
||||
10
web_frontend/exhibition-demo/src/main.tsx
Normal file
10
web_frontend/exhibition-demo/src/main.tsx
Normal 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>
|
||||
);
|
||||
131
web_frontend/exhibition-demo/src/pages/LandingPage.tsx
Normal file
131
web_frontend/exhibition-demo/src/pages/LandingPage.tsx
Normal 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">7个专业Agent</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,000平方米展览面积,350家参展商,预计50,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;
|
||||
261
web_frontend/exhibition-demo/src/pages/ResultPage.tsx
Normal file
261
web_frontend/exhibition-demo/src/pages/ResultPage.tsx
Normal 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;
|
||||
169
web_frontend/exhibition-demo/src/pages/WorkflowPage.tsx
Normal file
169
web_frontend/exhibition-demo/src/pages/WorkflowPage.tsx
Normal 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;
|
||||
165
web_frontend/exhibition-demo/src/store/demoStore.ts
Normal file
165
web_frontend/exhibition-demo/src/store/demoStore.ts
Normal 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: {},
|
||||
}),
|
||||
}));
|
||||
86
web_frontend/exhibition-demo/tailwind.config.js
Normal file
86
web_frontend/exhibition-demo/tailwind.config.js
Normal 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: [],
|
||||
}
|
||||
31
web_frontend/exhibition-demo/tsconfig.json
Normal file
31
web_frontend/exhibition-demo/tsconfig.json
Normal 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" }]
|
||||
}
|
||||
10
web_frontend/exhibition-demo/tsconfig.node.json
Normal file
10
web_frontend/exhibition-demo/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
12
web_frontend/exhibition-demo/vite.config.ts
Normal file
12
web_frontend/exhibition-demo/vite.config.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user