Spaces:
Sleeping
Sleeping
Commit ·
6bb2fbe
0
Parent(s):
chore: initial commit for huggingface spaces
Browse files- .env +8 -0
- .env.example +21 -0
- .gitignore +25 -0
- Dockerfile +44 -0
- README.md +49 -0
- api/app.ts +67 -0
- api/index.ts +9 -0
- api/lib/supabase.ts +28 -0
- api/routes/auth.ts +33 -0
- api/server.ts +90 -0
- api/services/ai.service.ts +98 -0
- api/services/stripe.service.ts +84 -0
- api/services/workflow.service.ts +36 -0
- eslint.config.js +28 -0
- index.html +24 -0
- nodemon.json +10 -0
- package.json +64 -0
- pnpm-lock.yaml +0 -0
- postcss.config.js +10 -0
- public/favicon.svg +4 -0
- shared/types/index.ts +57 -0
- src/App.tsx +13 -0
- src/assets/react.svg +1 -0
- src/components/Empty.tsx +8 -0
- src/hooks/useTheme.ts +29 -0
- src/i18n/index.ts +22 -0
- src/index.css +14 -0
- src/lib/utils.ts +6 -0
- src/locales/en/translation.json +45 -0
- src/locales/zh/translation.json +45 -0
- src/main.tsx +11 -0
- src/pages/Home.tsx +3 -0
- src/pages/dashboard/Chat.tsx +128 -0
- src/pages/dashboard/Layout.tsx +102 -0
- src/pages/dashboard/Workflow.tsx +62 -0
- src/store/useStore.ts +42 -0
- src/vite-env.d.ts +1 -0
- tailwind.config.js +13 -0
- tsconfig.json +40 -0
- vercel.json +12 -0
- vite.config.ts +46 -0
.env
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AI 配置 (SiliconFlow)
|
| 2 |
+
OPENAI_API_KEY=sk-smktrezkauofcvezxqsqbuzogzcduiikhcinhswercwhapze
|
| 3 |
+
OPENAI_API_BASE_URL=https://api.siliconflow.cn/v1
|
| 4 |
+
MODEL_NAME=Qwen/Qwen2.5-7B-Instruct
|
| 5 |
+
|
| 6 |
+
# 部署配置
|
| 7 |
+
PORT=3000
|
| 8 |
+
NODE_ENV=development
|
.env.example
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AI 配置 (OpenAI 或 SiliconFlow)
|
| 2 |
+
OPENAI_API_KEY=your_openai_api_key
|
| 3 |
+
OPENAI_API_BASE_URL=https://api.openai.com/v1
|
| 4 |
+
|
| 5 |
+
# Supabase 配置
|
| 6 |
+
SUPABASE_URL=your_supabase_url
|
| 7 |
+
SUPABASE_ANON_KEY=your_supabase_anon_key
|
| 8 |
+
SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key
|
| 9 |
+
|
| 10 |
+
# Redis 配置 (并发队列)
|
| 11 |
+
REDIS_HOST=localhost
|
| 12 |
+
REDIS_PORT=6379
|
| 13 |
+
REDIS_PASSWORD=
|
| 14 |
+
|
| 15 |
+
# Stripe 配置 (支付系统)
|
| 16 |
+
STRIPE_SECRET_KEY=your_stripe_secret_key
|
| 17 |
+
STRIPE_WEBHOOK_SECRET=your_stripe_webhook_secret
|
| 18 |
+
|
| 19 |
+
# 部署配置
|
| 20 |
+
PORT=3000
|
| 21 |
+
NODE_ENV=development
|
.gitignore
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Logs
|
| 2 |
+
logs
|
| 3 |
+
*.log
|
| 4 |
+
npm-debug.log*
|
| 5 |
+
yarn-debug.log*
|
| 6 |
+
yarn-error.log*
|
| 7 |
+
pnpm-debug.log*
|
| 8 |
+
lerna-debug.log*
|
| 9 |
+
|
| 10 |
+
node_modules
|
| 11 |
+
dist
|
| 12 |
+
dist-ssr
|
| 13 |
+
*.local
|
| 14 |
+
|
| 15 |
+
# Editor directories and files
|
| 16 |
+
.vscode/*
|
| 17 |
+
!.vscode/extensions.json
|
| 18 |
+
.idea
|
| 19 |
+
.DS_Store
|
| 20 |
+
*.suo
|
| 21 |
+
*.ntvs*
|
| 22 |
+
*.njsproj
|
| 23 |
+
*.sln
|
| 24 |
+
*.sw?
|
| 25 |
+
.vite.env
|
Dockerfile
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 使用 Node.js 20 作为基础镜像
|
| 2 |
+
FROM node:20-slim AS base
|
| 3 |
+
|
| 4 |
+
# 设置工作目录
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# 安装 pnpm
|
| 8 |
+
RUN npm install -g pnpm
|
| 9 |
+
|
| 10 |
+
# --- 构建阶段 ---
|
| 11 |
+
FROM base AS builder
|
| 12 |
+
|
| 13 |
+
# 复制依赖定义
|
| 14 |
+
COPY package.json pnpm-lock.yaml ./
|
| 15 |
+
|
| 16 |
+
# 安装依赖
|
| 17 |
+
RUN pnpm install
|
| 18 |
+
|
| 19 |
+
# 复制源代码
|
| 20 |
+
COPY . .
|
| 21 |
+
|
| 22 |
+
# 构建前端和后端
|
| 23 |
+
RUN pnpm run build
|
| 24 |
+
|
| 25 |
+
# --- 运行阶段 ---
|
| 26 |
+
FROM base AS runner
|
| 27 |
+
|
| 28 |
+
WORKDIR /app
|
| 29 |
+
|
| 30 |
+
# 复制构建产物和必要的运行文件
|
| 31 |
+
COPY --from=builder /app/dist ./dist
|
| 32 |
+
COPY --from=builder /app/package.json ./package.json
|
| 33 |
+
COPY --from=builder /app/pnpm-lock.yaml ./pnpm-lock.yaml
|
| 34 |
+
COPY --from=builder /app/node_modules ./node_modules
|
| 35 |
+
COPY --from=builder /app/api ./api
|
| 36 |
+
COPY --from=builder /app/.env ./.env
|
| 37 |
+
|
| 38 |
+
# 暴露端口 (Hugging Face Spaces 默认使用 7860)
|
| 39 |
+
EXPOSE 7860
|
| 40 |
+
ENV PORT=7860
|
| 41 |
+
|
| 42 |
+
# 启动命令 (使用 tsx 运行后端,或者编译后运行)
|
| 43 |
+
# 这里我们直接运行后端服务
|
| 44 |
+
CMD ["pnpm", "run", "server:dev"]
|
README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Codex AI 平台
|
| 3 |
+
emoji: 🌐
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: false
|
| 9 |
+
short_description: Codex AI 平台 - 全栈 AI 编排与 RAG 演示系统
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
# Codex AI 平台 (Codex AI Platform)
|
| 13 |
+
|
| 14 |
+
这是一个基于 `SiliconFlow` (Qwen2.5-7B-Instruct) 的全栈 AI 应用平台。集成智能对话、可视化工作流编排等功能。
|
| 15 |
+
|
| 16 |
+
## 🛡️ 技术特点
|
| 17 |
+
- **AI 驱动**:使用 `SiliconFlow` 提供的 Qwen2.5 免费模型,支持流式对话。
|
| 18 |
+
- **高性能架构**:基于 `Express` + `Vite` 构建,支持快速响应。
|
| 19 |
+
- **可视化拓扑**:基于 `React Flow` 构建工作流引擎。
|
| 20 |
+
- **容器化部署**:支持 Docker 一键部署至 Hugging Face Spaces。
|
| 21 |
+
|
| 22 |
+
## 🚀 快速开始
|
| 23 |
+
|
| 24 |
+
### 环境变量配置
|
| 25 |
+
在本地运行时,请创建 `.env` 文件并填入以下内容:
|
| 26 |
+
```bash
|
| 27 |
+
OPENAI_API_KEY=您的硅基流动API_KEY
|
| 28 |
+
OPENAI_API_BASE_URL=https://api.siliconflow.cn/v1
|
| 29 |
+
MODEL_NAME=Qwen/Qwen2.5-7B-Instruct
|
| 30 |
+
PORT=7860
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
### 本地运行
|
| 34 |
+
```bash
|
| 35 |
+
# 安装依赖
|
| 36 |
+
pnpm install
|
| 37 |
+
|
| 38 |
+
# 启动开发环境
|
| 39 |
+
pnpm run dev
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
## 🛠️ 技术栈
|
| 43 |
+
- **前端**: React 18 + Vite + Tailwind CSS + Framer Motion
|
| 44 |
+
- **后端**: Node.js + Express + TypeScript
|
| 45 |
+
- **AI**: SiliconFlow (OpenAI 兼容接口)
|
| 46 |
+
- **部署**: Docker
|
| 47 |
+
|
| 48 |
+
## 📝 许可证
|
| 49 |
+
MIT
|
api/app.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* This is a API server
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
import express, {
|
| 6 |
+
type Request,
|
| 7 |
+
type Response,
|
| 8 |
+
type NextFunction,
|
| 9 |
+
} from 'express'
|
| 10 |
+
import cors from 'cors'
|
| 11 |
+
import path from 'path'
|
| 12 |
+
import dotenv from 'dotenv'
|
| 13 |
+
import { fileURLToPath } from 'url'
|
| 14 |
+
import authRoutes from './routes/auth.js'
|
| 15 |
+
|
| 16 |
+
// for esm mode
|
| 17 |
+
const __filename = fileURLToPath(import.meta.url)
|
| 18 |
+
const __dirname = path.dirname(__filename)
|
| 19 |
+
|
| 20 |
+
// load env
|
| 21 |
+
dotenv.config()
|
| 22 |
+
|
| 23 |
+
const app: express.Application = express()
|
| 24 |
+
|
| 25 |
+
app.use(cors())
|
| 26 |
+
app.use(express.json({ limit: '10mb' }))
|
| 27 |
+
app.use(express.urlencoded({ extended: true, limit: '10mb' }))
|
| 28 |
+
|
| 29 |
+
/**
|
| 30 |
+
* API Routes
|
| 31 |
+
*/
|
| 32 |
+
app.use('/api/auth', authRoutes)
|
| 33 |
+
|
| 34 |
+
/**
|
| 35 |
+
* health
|
| 36 |
+
*/
|
| 37 |
+
app.use(
|
| 38 |
+
'/api/health',
|
| 39 |
+
(req: Request, res: Response, next: NextFunction): void => {
|
| 40 |
+
res.status(200).json({
|
| 41 |
+
success: true,
|
| 42 |
+
message: 'ok',
|
| 43 |
+
})
|
| 44 |
+
},
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
/**
|
| 48 |
+
* error handler middleware
|
| 49 |
+
*/
|
| 50 |
+
app.use((error: Error, req: Request, res: Response, next: NextFunction) => {
|
| 51 |
+
res.status(500).json({
|
| 52 |
+
success: false,
|
| 53 |
+
error: 'Server internal error',
|
| 54 |
+
})
|
| 55 |
+
})
|
| 56 |
+
|
| 57 |
+
/**
|
| 58 |
+
* 404 handler
|
| 59 |
+
*/
|
| 60 |
+
app.use((req: Request, res: Response) => {
|
| 61 |
+
res.status(404).json({
|
| 62 |
+
success: false,
|
| 63 |
+
error: 'API not found',
|
| 64 |
+
})
|
| 65 |
+
})
|
| 66 |
+
|
| 67 |
+
export default app
|
api/index.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Vercel deploy entry handler, for serverless deployment, please don't modify this file
|
| 3 |
+
*/
|
| 4 |
+
import type { VercelRequest, VercelResponse } from '@vercel/node';
|
| 5 |
+
import app from './app.js';
|
| 6 |
+
|
| 7 |
+
export default function handler(req: VercelRequest, res: VercelResponse) {
|
| 8 |
+
return app(req, res);
|
| 9 |
+
}
|
api/lib/supabase.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* 模拟 Supabase 客户端 (为了在没有 Supabase 时保证项目运行)
|
| 3 |
+
* 注意:由于用户要求“不接 Supabase”,这里暂时使用内存模拟或跳过数据库操作
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
export const supabase = {
|
| 7 |
+
rpc: async (name: string, args: any) => {
|
| 8 |
+
console.log(`[Supabase Mock] 调用 RPC: ${name}`, args);
|
| 9 |
+
return { data: [], error: null };
|
| 10 |
+
},
|
| 11 |
+
from: (table: string) => ({
|
| 12 |
+
insert: async (data: any) => {
|
| 13 |
+
console.log(`[Supabase Mock] 插入表 ${table}:`, data);
|
| 14 |
+
return { data, error: null };
|
| 15 |
+
},
|
| 16 |
+
update: (data: any) => ({
|
| 17 |
+
eq: async (column: string, value: any) => {
|
| 18 |
+
console.log(`[Supabase Mock] 更新表 ${table} (条件 ${column}=${value}):`, data);
|
| 19 |
+
return { data, error: null };
|
| 20 |
+
},
|
| 21 |
+
}),
|
| 22 |
+
select: () => ({
|
| 23 |
+
eq: () => ({
|
| 24 |
+
single: async () => ({ data: null, error: null }),
|
| 25 |
+
}),
|
| 26 |
+
}),
|
| 27 |
+
}),
|
| 28 |
+
};
|
api/routes/auth.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* 用户认证 API 路由演示。
|
| 3 |
+
* 处理用户注册、登录、Token 管理等。
|
| 4 |
+
*/
|
| 5 |
+
import { Router, type Request, type Response } from 'express'
|
| 6 |
+
|
| 7 |
+
const router = Router()
|
| 8 |
+
|
| 9 |
+
/**
|
| 10 |
+
* 用户注册
|
| 11 |
+
* POST /api/auth/register
|
| 12 |
+
*/
|
| 13 |
+
router.post('/register', async (req: Request, res: Response): Promise<void> => {
|
| 14 |
+
// TODO: 实现注册逻辑
|
| 15 |
+
})
|
| 16 |
+
|
| 17 |
+
/**
|
| 18 |
+
* 用户登录
|
| 19 |
+
* POST /api/auth/login
|
| 20 |
+
*/
|
| 21 |
+
router.post('/login', async (req: Request, res: Response): Promise<void> => {
|
| 22 |
+
// TODO: 实现登录逻辑
|
| 23 |
+
})
|
| 24 |
+
|
| 25 |
+
/**
|
| 26 |
+
* 用户登出
|
| 27 |
+
* POST /api/auth/logout
|
| 28 |
+
*/
|
| 29 |
+
router.post('/logout', async (req: Request, res: Response): Promise<void> => {
|
| 30 |
+
// TODO: 实现登出逻辑
|
| 31 |
+
})
|
| 32 |
+
|
| 33 |
+
export default router
|
api/server.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import express from 'express';
|
| 2 |
+
import cors from 'cors';
|
| 3 |
+
import path from 'path';
|
| 4 |
+
import { fileURLToPath } from 'url';
|
| 5 |
+
import dotenv from 'dotenv';
|
| 6 |
+
import { StripeService } from './services/stripe.service.js';
|
| 7 |
+
import { setupWorkers } from './lib/queue.js';
|
| 8 |
+
import { AIService } from './services/ai.service.ts';
|
| 9 |
+
|
| 10 |
+
dotenv.config();
|
| 11 |
+
|
| 12 |
+
const __filename = fileURLToPath(import.meta.url);
|
| 13 |
+
const __dirname = path.dirname(__filename);
|
| 14 |
+
|
| 15 |
+
const app = express();
|
| 16 |
+
const port = process.env.PORT || 7860;
|
| 17 |
+
|
| 18 |
+
// Stripe Webhook 路由 (需要原始 body 格式进行签名验证)
|
| 19 |
+
app.post('/api/payment/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
|
| 20 |
+
const sig = req.headers['stripe-signature'] as string;
|
| 21 |
+
try {
|
| 22 |
+
await StripeService.handleWebhook(sig, req.body);
|
| 23 |
+
res.json({ received: true });
|
| 24 |
+
} catch (err: any) {
|
| 25 |
+
res.status(400).send(`Webhook 错误: ${err.message}`);
|
| 26 |
+
}
|
| 27 |
+
});
|
| 28 |
+
|
| 29 |
+
// 通用中间件
|
| 30 |
+
app.use(express.json());
|
| 31 |
+
app.use(cors());
|
| 32 |
+
|
| 33 |
+
// 初始化并发任务处理器 (队列)
|
| 34 |
+
setupWorkers(
|
| 35 |
+
async (job) => {
|
| 36 |
+
// 处理 AI 任务 (工作流/RAG)
|
| 37 |
+
console.log(`[队列] 正在处理 AI 任务: ${job.id}`);
|
| 38 |
+
},
|
| 39 |
+
async (job) => {
|
| 40 |
+
// 处理文档向量化任务
|
| 41 |
+
const { documentId, content, userId } = job.data;
|
| 42 |
+
await AIService.processDocument(documentId, content, userId);
|
| 43 |
+
}
|
| 44 |
+
);
|
| 45 |
+
|
| 46 |
+
// 健康检查接口
|
| 47 |
+
app.get('/api/health', (req, res) => res.json({ status: 'ok', engine: 'Qwen2.5-7B', provider: 'SiliconFlow' }));
|
| 48 |
+
|
| 49 |
+
// AI 流式对话接口
|
| 50 |
+
app.post('/api/ai/chat', async (req, res) => {
|
| 51 |
+
const { query, userId, knowledgeBaseId } = req.body;
|
| 52 |
+
try {
|
| 53 |
+
const { stream, sources } = await AIService.chatWithKnowledge(userId, query, knowledgeBaseId);
|
| 54 |
+
|
| 55 |
+
res.setHeader('Content-Type', 'text/event-stream');
|
| 56 |
+
res.setHeader('Cache-Control', 'no-cache');
|
| 57 |
+
res.setHeader('Connection', 'keep-alive');
|
| 58 |
+
|
| 59 |
+
// 发送检索来源信息
|
| 60 |
+
res.write(`data: ${JSON.stringify({ sources })}\n\n`);
|
| 61 |
+
|
| 62 |
+
for await (const chunk of stream) {
|
| 63 |
+
const content = chunk.choices[0]?.delta?.content || '';
|
| 64 |
+
if (content) {
|
| 65 |
+
res.write(`data: ${JSON.stringify({ content })}\n\n`);
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
res.end();
|
| 69 |
+
} catch (err) {
|
| 70 |
+
console.error('[AI] 对话接口出错:', err);
|
| 71 |
+
res.status(500).json({ error: 'AI 对话失败,请检查 API 配置' });
|
| 72 |
+
}
|
| 73 |
+
});
|
| 74 |
+
|
| 75 |
+
// 静态文件服务:将前端构建产物 dist 目录映射到根路径
|
| 76 |
+
// 这样在 Hugging Face Spaces 上运行一个端口即可访问完整应用
|
| 77 |
+
const distPath = path.resolve(__dirname, '../dist');
|
| 78 |
+
app.use(express.static(distPath));
|
| 79 |
+
|
| 80 |
+
// 所有非 API 请求均返回 index.html (支持单页应用前端路由)
|
| 81 |
+
app.get('*', (req, res) => {
|
| 82 |
+
res.sendFile(path.join(distPath, 'index.html'));
|
| 83 |
+
});
|
| 84 |
+
|
| 85 |
+
app.listen(port, () => {
|
| 86 |
+
console.log(`[服务器] 全栈后端运行在端口: ${port}`);
|
| 87 |
+
console.log(`[模式] 模型使用: ${process.env.MODEL_NAME || 'Qwen/Qwen2.5-7B-Instruct'}`);
|
| 88 |
+
});
|
| 89 |
+
|
| 90 |
+
export default app;
|
api/services/ai.service.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import OpenAI from 'openai';
|
| 2 |
+
import { supabase } from '../lib/supabase.js';
|
| 3 |
+
import dotenv from 'dotenv';
|
| 4 |
+
|
| 5 |
+
dotenv.config();
|
| 6 |
+
|
| 7 |
+
// 使用 SiliconFlow 提供的免费 Qwen2.5-7B-Instruct 模型
|
| 8 |
+
const openai = new OpenAI({
|
| 9 |
+
apiKey: process.env.OPENAI_API_KEY,
|
| 10 |
+
baseURL: process.env.OPENAI_API_BASE_URL || 'https://api.siliconflow.cn/v1',
|
| 11 |
+
});
|
| 12 |
+
|
| 13 |
+
const MODEL = process.env.MODEL_NAME || 'Qwen/Qwen2.5-7B-Instruct';
|
| 14 |
+
|
| 15 |
+
export class AIService {
|
| 16 |
+
/**
|
| 17 |
+
* 增强型 RAG 检索对话 (当前由于不接 Supabase,向量搜索被模拟)
|
| 18 |
+
*/
|
| 19 |
+
static async chatWithKnowledge(userId: string, query: string, knowledgeBaseId?: string) {
|
| 20 |
+
// 1. 生成查询向量 (可选,如果不使用 RAG 则跳过)
|
| 21 |
+
let context = '';
|
| 22 |
+
let sources: string[] = [];
|
| 23 |
+
|
| 24 |
+
try {
|
| 25 |
+
// 只有在需要 RAG 时才执行,这里根据项目现状调用模拟
|
| 26 |
+
const embeddingResponse = await openai.embeddings.create({
|
| 27 |
+
model: 'BAAI/bge-m3', // SiliconFlow 推荐的向量模型
|
| 28 |
+
input: query,
|
| 29 |
+
});
|
| 30 |
+
const queryEmbedding = embeddingResponse.data[0].embedding;
|
| 31 |
+
|
| 32 |
+
// 2. 相似度搜索 (通过 Mock 返回空)
|
| 33 |
+
const { data: chunks, error } = await supabase.rpc('match_chunks', {
|
| 34 |
+
query_embedding: queryEmbedding,
|
| 35 |
+
match_threshold: 0.5,
|
| 36 |
+
match_count: 5,
|
| 37 |
+
filter_user_id: userId,
|
| 38 |
+
});
|
| 39 |
+
|
| 40 |
+
if (!error && chunks && chunks.length > 0) {
|
| 41 |
+
context = chunks.map((c: any) => c.content).join('\n\n');
|
| 42 |
+
sources = chunks.map((c: any) => c.metadata?.filename || '未知文档');
|
| 43 |
+
}
|
| 44 |
+
} catch (e) {
|
| 45 |
+
console.warn('[AIService] RAG 检索失败,切换到基础对话模式');
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// 3. 流式生成响应
|
| 49 |
+
const stream = await openai.chat.completions.create({
|
| 50 |
+
model: MODEL,
|
| 51 |
+
messages: [
|
| 52 |
+
{
|
| 53 |
+
role: 'system',
|
| 54 |
+
content: `你是一个专业的 AI 助手。${context ? `请基于以下参考资料回答。如果资料中没有,请告知用户。请保持回复的专业与客观。\n\n参考资料:\n${context}` : '请直接回答用户的问题。'}`
|
| 55 |
+
},
|
| 56 |
+
{ role: 'user', content: query },
|
| 57 |
+
],
|
| 58 |
+
stream: true,
|
| 59 |
+
});
|
| 60 |
+
|
| 61 |
+
return { stream, sources };
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
/**
|
| 65 |
+
* 文档向量化处理逻辑
|
| 66 |
+
*/
|
| 67 |
+
static async processDocument(documentId: string, content: string, userId: string) {
|
| 68 |
+
const chunks = AIService.splitIntoChunks(content, 1000);
|
| 69 |
+
|
| 70 |
+
for (const chunkText of chunks) {
|
| 71 |
+
try {
|
| 72 |
+
const embedding = await openai.embeddings.create({
|
| 73 |
+
model: 'BAAI/bge-m3',
|
| 74 |
+
input: chunkText,
|
| 75 |
+
});
|
| 76 |
+
|
| 77 |
+
await supabase.from('chunks').insert({
|
| 78 |
+
document_id: documentId,
|
| 79 |
+
user_id: userId,
|
| 80 |
+
content: chunkText,
|
| 81 |
+
embedding: embedding.data[0].embedding,
|
| 82 |
+
});
|
| 83 |
+
} catch (e) {
|
| 84 |
+
console.error('[AIService] 向量化处理单条 chunk 失败:', e);
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
await supabase.from('documents').update({ status: 'completed' }).eq('id', documentId);
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
private static splitIntoChunks(text: string, size: number): string[] {
|
| 92 |
+
const chunks: string[] = [];
|
| 93 |
+
for (let i = 0; i < text.length; i += size) {
|
| 94 |
+
chunks.push(text.slice(i, i + size));
|
| 95 |
+
}
|
| 96 |
+
return chunks;
|
| 97 |
+
}
|
| 98 |
+
}
|
api/services/stripe.service.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Stripe from 'stripe';
|
| 2 |
+
import dotenv from 'dotenv';
|
| 3 |
+
import { supabase } from '../lib/supabase.js';
|
| 4 |
+
|
| 5 |
+
dotenv.config();
|
| 6 |
+
|
| 7 |
+
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || '', {
|
| 8 |
+
apiVersion: '2024-11-20.acacia' as any,
|
| 9 |
+
});
|
| 10 |
+
|
| 11 |
+
export class StripeService {
|
| 12 |
+
/**
|
| 13 |
+
* 为用户创建 Stripe 客户
|
| 14 |
+
*/
|
| 15 |
+
static async createCustomer(email: string, name: string, userId: string) {
|
| 16 |
+
const customer = await stripe.customers.create({
|
| 17 |
+
email,
|
| 18 |
+
name,
|
| 19 |
+
metadata: { userId },
|
| 20 |
+
});
|
| 21 |
+
|
| 22 |
+
// 更新数据库中的 stripe_customer_id
|
| 23 |
+
await supabase.from('users').update({ stripe_customer_id: customer.id }).eq('id', userId);
|
| 24 |
+
return customer;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
/**
|
| 28 |
+
* 创建订阅 Checkout 会话
|
| 29 |
+
*/
|
| 30 |
+
static async createSubscriptionSession(customerId: string, priceId: string) {
|
| 31 |
+
const session = await stripe.checkout.sessions.create({
|
| 32 |
+
customer: customerId,
|
| 33 |
+
payment_method_types: ['card'],
|
| 34 |
+
line_items: [{ price: priceId, quantity: 1 }],
|
| 35 |
+
mode: 'subscription',
|
| 36 |
+
success_url: `${process.env.FRONTEND_URL}/dashboard/billing?success=true`,
|
| 37 |
+
cancel_url: `${process.env.FRONTEND_URL}/dashboard/billing?canceled=true`,
|
| 38 |
+
});
|
| 39 |
+
return session;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
/**
|
| 43 |
+
* 处理 Stripe Webhook 回调
|
| 44 |
+
*/
|
| 45 |
+
static async handleWebhook(signature: string, payload: Buffer) {
|
| 46 |
+
let event: Stripe.Event;
|
| 47 |
+
try {
|
| 48 |
+
event = stripe.webhooks.constructEvent(
|
| 49 |
+
payload,
|
| 50 |
+
signature,
|
| 51 |
+
process.env.STRIPE_WEBHOOK_SECRET || ''
|
| 52 |
+
);
|
| 53 |
+
} catch (err: any) {
|
| 54 |
+
throw new Error(`Webhook Signature 验证失败: ${err.message}`);
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
switch (event.type) {
|
| 58 |
+
case 'customer.subscription.created':
|
| 59 |
+
case 'customer.subscription.updated': {
|
| 60 |
+
const subscription = event.data.object as Stripe.Subscription;
|
| 61 |
+
const customerId = subscription.customer as string;
|
| 62 |
+
// 更新用户订阅状态
|
| 63 |
+
await StripeService.updateUserSubscription(customerId, subscription.status);
|
| 64 |
+
break;
|
| 65 |
+
}
|
| 66 |
+
case 'customer.subscription.deleted': {
|
| 67 |
+
const subscription = event.data.object as Stripe.Subscription;
|
| 68 |
+
const customerId = subscription.customer as string;
|
| 69 |
+
await StripeService.updateUserSubscription(customerId, 'free');
|
| 70 |
+
break;
|
| 71 |
+
}
|
| 72 |
+
}
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
private static async updateUserSubscription(stripeCustomerId: string, status: string) {
|
| 76 |
+
let subscriptionStatus: 'free' | 'pro' | 'enterprise' = 'free';
|
| 77 |
+
if (status === 'active') subscriptionStatus = 'pro'; // 简化逻辑
|
| 78 |
+
|
| 79 |
+
await supabase
|
| 80 |
+
.from('users')
|
| 81 |
+
.update({ subscription_status: subscriptionStatus })
|
| 82 |
+
.eq('stripe_customer_id', stripeCustomerId);
|
| 83 |
+
}
|
| 84 |
+
}
|
api/services/workflow.service.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Workflow, WorkflowNode } from '../../shared/types/index.js';
|
| 2 |
+
|
| 3 |
+
export class WorkflowService {
|
| 4 |
+
/**
|
| 5 |
+
* 解析并执行工作流拓扑
|
| 6 |
+
*/
|
| 7 |
+
static async execute(workflow: Workflow, initialInput: any) {
|
| 8 |
+
console.log(`[Workflow] 开始执行: ${workflow.name} (ID: ${workflow.id})`);
|
| 9 |
+
|
| 10 |
+
// 此处应实现复杂的拓扑排序与异步执行逻辑
|
| 11 |
+
// 为了演示,我们采用简单的线性执行
|
| 12 |
+
let result = initialInput;
|
| 13 |
+
const sortedNodes = [...workflow.nodes].sort((a, b) => a.position.x - b.position.x);
|
| 14 |
+
|
| 15 |
+
for (const node of sortedNodes) {
|
| 16 |
+
result = await WorkflowService.executeNode(node, result);
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
return result;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
private static async executeNode(node: WorkflowNode, input: any) {
|
| 23 |
+
console.log(`[Workflow] 执行节点: ${node.type} (${node.id})`);
|
| 24 |
+
|
| 25 |
+
switch (node.type) {
|
| 26 |
+
case 'llm':
|
| 27 |
+
return `AI 处理结果: ${input}`;
|
| 28 |
+
case 'knowledge_base':
|
| 29 |
+
return `知识库检索结果: ${input}`;
|
| 30 |
+
case 'condition':
|
| 31 |
+
return input;
|
| 32 |
+
default:
|
| 33 |
+
return input;
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
}
|
eslint.config.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import js from '@eslint/js'
|
| 2 |
+
import globals from 'globals'
|
| 3 |
+
import reactHooks from 'eslint-plugin-react-hooks'
|
| 4 |
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
| 5 |
+
import tseslint from 'typescript-eslint'
|
| 6 |
+
|
| 7 |
+
export default tseslint.config(
|
| 8 |
+
{ ignores: ['dist'] },
|
| 9 |
+
{
|
| 10 |
+
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
| 11 |
+
files: ['**/*.{ts,tsx}'],
|
| 12 |
+
languageOptions: {
|
| 13 |
+
ecmaVersion: 2020,
|
| 14 |
+
globals: globals.browser,
|
| 15 |
+
},
|
| 16 |
+
plugins: {
|
| 17 |
+
'react-hooks': reactHooks,
|
| 18 |
+
'react-refresh': reactRefresh,
|
| 19 |
+
},
|
| 20 |
+
rules: {
|
| 21 |
+
...reactHooks.configs.recommended.rules,
|
| 22 |
+
'react-refresh/only-export-components': [
|
| 23 |
+
'warn',
|
| 24 |
+
{ allowConstantExport: true },
|
| 25 |
+
],
|
| 26 |
+
},
|
| 27 |
+
},
|
| 28 |
+
)
|
index.html
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
+
<title>My Trae Project</title>
|
| 8 |
+
<script type="module">
|
| 9 |
+
if (import.meta.hot?.on) {
|
| 10 |
+
import.meta.hot.on('vite:error', (error) => {
|
| 11 |
+
if (error.err) {
|
| 12 |
+
console.error(
|
| 13 |
+
[error.err.message, error.err.frame].filter(Boolean).join('\n'),
|
| 14 |
+
)
|
| 15 |
+
}
|
| 16 |
+
})
|
| 17 |
+
}
|
| 18 |
+
</script>
|
| 19 |
+
</head>
|
| 20 |
+
<body>
|
| 21 |
+
<div id="root"></div>
|
| 22 |
+
<script type="module" src="/src/main.tsx"></script>
|
| 23 |
+
</body>
|
| 24 |
+
</html>
|
nodemon.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"watch": ["api"],
|
| 3 |
+
"ext": "ts,mts,js,json",
|
| 4 |
+
"ignore": ["api/dist/*"],
|
| 5 |
+
"exec": "tsx api/server.ts",
|
| 6 |
+
"env": {
|
| 7 |
+
"NODE_ENV": "development"
|
| 8 |
+
},
|
| 9 |
+
"delay": 1000
|
| 10 |
+
}
|
package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "codex-ai-platform",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "0.0.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"client:dev": "vite",
|
| 8 |
+
"build": "tsc -b && vite build",
|
| 9 |
+
"lint": "eslint .",
|
| 10 |
+
"preview": "vite preview",
|
| 11 |
+
"check": "tsc --noEmit",
|
| 12 |
+
"server:dev": "nodemon",
|
| 13 |
+
"dev": "concurrently \"npm run client:dev\" \"npm run server:dev\""
|
| 14 |
+
},
|
| 15 |
+
"dependencies": {
|
| 16 |
+
"@supabase/supabase-js": "^2.45.4",
|
| 17 |
+
"bullmq": "^5.12.0",
|
| 18 |
+
"clsx": "^2.1.1",
|
| 19 |
+
"cors": "^2.8.5",
|
| 20 |
+
"dotenv": "^17.2.1",
|
| 21 |
+
"express": "^4.21.2",
|
| 22 |
+
"framer-motion": "^11.11.11",
|
| 23 |
+
"i18next": "^23.11.5",
|
| 24 |
+
"i18next-browser-languagedetector": "^8.0.0",
|
| 25 |
+
"ioredis": "^5.4.1",
|
| 26 |
+
"lucide-react": "^0.511.0",
|
| 27 |
+
"openai": "^4.71.1",
|
| 28 |
+
"react": "^18.3.1",
|
| 29 |
+
"react-dom": "^18.3.1",
|
| 30 |
+
"react-i18next": "^14.1.2",
|
| 31 |
+
"react-router-dom": "^7.3.0",
|
| 32 |
+
"reactflow": "^11.11.4",
|
| 33 |
+
"stripe": "^17.3.0",
|
| 34 |
+
"tailwind-merge": "^3.0.2",
|
| 35 |
+
"zustand": "^5.0.3"
|
| 36 |
+
},
|
| 37 |
+
"devDependencies": {
|
| 38 |
+
"vite-plugin-pwa": "^0.21.1",
|
| 39 |
+
"@eslint/js": "^9.25.0",
|
| 40 |
+
"@types/cors": "^2.8.19",
|
| 41 |
+
"@types/express": "^4.17.21",
|
| 42 |
+
"@types/node": "^22.15.30",
|
| 43 |
+
"@types/react": "^18.3.12",
|
| 44 |
+
"@types/react-dom": "^18.3.1",
|
| 45 |
+
"@vitejs/plugin-react": "^4.4.1",
|
| 46 |
+
"@vercel/node": "^5.3.6",
|
| 47 |
+
"autoprefixer": "^10.4.21",
|
| 48 |
+
"babel-plugin-react-dev-locator": "^1.0.0",
|
| 49 |
+
"concurrently": "^9.2.0",
|
| 50 |
+
"postcss": "^8.5.3",
|
| 51 |
+
"tailwindcss": "^3.4.17",
|
| 52 |
+
"eslint": "^9.25.0",
|
| 53 |
+
"eslint-plugin-react-hooks": "^5.2.0",
|
| 54 |
+
"eslint-plugin-react-refresh": "^0.4.19",
|
| 55 |
+
"globals": "^16.0.0",
|
| 56 |
+
"nodemon": "^3.1.10",
|
| 57 |
+
"tsx": "^4.20.3",
|
| 58 |
+
"typescript": "~5.8.3",
|
| 59 |
+
"typescript-eslint": "^8.30.1",
|
| 60 |
+
"vite": "^6.3.5",
|
| 61 |
+
"vite-plugin-trae-solo-badge": "^1.0.0",
|
| 62 |
+
"vite-tsconfig-paths": "^5.1.4"
|
| 63 |
+
}
|
| 64 |
+
}
|
pnpm-lock.yaml
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
postcss.config.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** WARNING: DON'T EDIT THIS FILE */
|
| 2 |
+
/** WARNING: DON'T EDIT THIS FILE */
|
| 3 |
+
/** WARNING: DON'T EDIT THIS FILE */
|
| 4 |
+
|
| 5 |
+
export default {
|
| 6 |
+
plugins: {
|
| 7 |
+
tailwindcss: {},
|
| 8 |
+
autoprefixer: {},
|
| 9 |
+
},
|
| 10 |
+
};
|
public/favicon.svg
ADDED
|
|
shared/types/index.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// 核心业务类型定义
|
| 2 |
+
export interface User {
|
| 3 |
+
id: string;
|
| 4 |
+
email: string;
|
| 5 |
+
name: string;
|
| 6 |
+
subscription_status: 'free' | 'pro' | 'enterprise';
|
| 7 |
+
stripe_customer_id?: string;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
export interface Document {
|
| 11 |
+
id: string;
|
| 12 |
+
filename: string;
|
| 13 |
+
status: 'pending' | 'processing' | 'completed' | 'failed';
|
| 14 |
+
chunks_count: number;
|
| 15 |
+
created_at: string;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
export interface WorkflowNode {
|
| 19 |
+
id: string;
|
| 20 |
+
type: 'llm' | 'input' | 'output' | 'condition' | 'knowledge_base';
|
| 21 |
+
data: any;
|
| 22 |
+
position: { x: number; y: number };
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
export interface WorkflowEdge {
|
| 26 |
+
id: string;
|
| 27 |
+
source: string;
|
| 28 |
+
target: string;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
export interface Workflow {
|
| 32 |
+
id: string;
|
| 33 |
+
name: string;
|
| 34 |
+
description: string;
|
| 35 |
+
nodes: WorkflowNode[];
|
| 36 |
+
edges: WorkflowEdge[];
|
| 37 |
+
status: 'draft' | 'published';
|
| 38 |
+
version: number;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
export interface AIResponse {
|
| 42 |
+
content: string;
|
| 43 |
+
role: 'user' | 'assistant';
|
| 44 |
+
sources?: string[];
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
export interface PaymentIntent {
|
| 48 |
+
clientSecret: string;
|
| 49 |
+
publishableKey: string;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
export interface TaskStatus {
|
| 53 |
+
jobId: string;
|
| 54 |
+
status: 'active' | 'waiting' | 'completed' | 'failed';
|
| 55 |
+
progress: number;
|
| 56 |
+
result?: any;
|
| 57 |
+
}
|
src/App.tsx
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
|
| 2 |
+
import Home from "@/pages/Home";
|
| 3 |
+
|
| 4 |
+
export default function App() {
|
| 5 |
+
return (
|
| 6 |
+
<Router>
|
| 7 |
+
<Routes>
|
| 8 |
+
<Route path="/" element={<Home />} />
|
| 9 |
+
<Route path="/other" element={<div className="text-center text-xl">Other Page - Coming Soon</div>} />
|
| 10 |
+
</Routes>
|
| 11 |
+
</Router>
|
| 12 |
+
);
|
| 13 |
+
}
|
src/assets/react.svg
ADDED
|
|
src/components/Empty.tsx
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { cn } from '@/lib/utils'
|
| 2 |
+
|
| 3 |
+
// Empty component
|
| 4 |
+
export default function Empty() {
|
| 5 |
+
return (
|
| 6 |
+
<div className={cn('flex h-full items-center justify-center')}>Empty</div>
|
| 7 |
+
)
|
| 8 |
+
}
|
src/hooks/useTheme.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useEffect } from 'react';
|
| 2 |
+
|
| 3 |
+
type Theme = 'light' | 'dark';
|
| 4 |
+
|
| 5 |
+
export function useTheme() {
|
| 6 |
+
const [theme, setTheme] = useState<Theme>(() => {
|
| 7 |
+
const savedTheme = localStorage.getItem('theme') as Theme;
|
| 8 |
+
if (savedTheme) {
|
| 9 |
+
return savedTheme;
|
| 10 |
+
}
|
| 11 |
+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
| 12 |
+
});
|
| 13 |
+
|
| 14 |
+
useEffect(() => {
|
| 15 |
+
document.documentElement.classList.remove('light', 'dark');
|
| 16 |
+
document.documentElement.classList.add(theme);
|
| 17 |
+
localStorage.setItem('theme', theme);
|
| 18 |
+
}, [theme]);
|
| 19 |
+
|
| 20 |
+
const toggleTheme = () => {
|
| 21 |
+
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
|
| 22 |
+
};
|
| 23 |
+
|
| 24 |
+
return {
|
| 25 |
+
theme,
|
| 26 |
+
toggleTheme,
|
| 27 |
+
isDark: theme === 'dark'
|
| 28 |
+
};
|
| 29 |
+
}
|
src/i18n/index.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import i18n from 'i18next';
|
| 2 |
+
import { initReactI18next } from 'react-i18next';
|
| 3 |
+
import LanguageDetector from 'i18next-browser-languagedetector';
|
| 4 |
+
|
| 5 |
+
import enTranslation from '../locales/en/translation.json';
|
| 6 |
+
import zhTranslation from '../locales/zh/translation.json';
|
| 7 |
+
|
| 8 |
+
i18n
|
| 9 |
+
.use(LanguageDetector)
|
| 10 |
+
.use(initReactI18next)
|
| 11 |
+
.init({
|
| 12 |
+
resources: {
|
| 13 |
+
en: { translation: enTranslation },
|
| 14 |
+
zh: { translation: zhTranslation },
|
| 15 |
+
},
|
| 16 |
+
fallbackLng: 'zh',
|
| 17 |
+
interpolation: {
|
| 18 |
+
escapeValue: false,
|
| 19 |
+
},
|
| 20 |
+
});
|
| 21 |
+
|
| 22 |
+
export default i18n;
|
src/index.css
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@tailwind base;
|
| 2 |
+
@tailwind components;
|
| 3 |
+
@tailwind utilities;
|
| 4 |
+
|
| 5 |
+
:root {
|
| 6 |
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
| 7 |
+
line-height: 1.5;
|
| 8 |
+
font-weight: 400;
|
| 9 |
+
|
| 10 |
+
font-synthesis: none;
|
| 11 |
+
text-rendering: optimizeLegibility;
|
| 12 |
+
-webkit-font-smoothing: antialiased;
|
| 13 |
+
-moz-osx-font-smoothing: grayscale;
|
| 14 |
+
}
|
src/lib/utils.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { clsx, type ClassValue } from "clsx"
|
| 2 |
+
import { twMerge } from "tailwind-merge"
|
| 3 |
+
|
| 4 |
+
export function cn(...inputs: ClassValue[]) {
|
| 5 |
+
return twMerge(clsx(inputs))
|
| 6 |
+
}
|
src/locales/en/translation.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"common": {
|
| 3 |
+
"welcome": "Welcome back",
|
| 4 |
+
"dashboard": "Dashboard",
|
| 5 |
+
"chat": "AI Chat",
|
| 6 |
+
"knowledge": "Knowledge Base",
|
| 7 |
+
"workflow": "Workflow",
|
| 8 |
+
"billing": "Billing",
|
| 9 |
+
"settings": "Settings",
|
| 10 |
+
"logout": "Logout",
|
| 11 |
+
"save": "Save",
|
| 12 |
+
"cancel": "Cancel",
|
| 13 |
+
"submit": "Submit",
|
| 14 |
+
"loading": "Loading...",
|
| 15 |
+
"error": "Error occurred"
|
| 16 |
+
},
|
| 17 |
+
"dashboard": {
|
| 18 |
+
"title": "Developer Console",
|
| 19 |
+
"description": "Manage your AI assets, orchestrate business processes, and integrate payment loops.",
|
| 20 |
+
"stats": {
|
| 21 |
+
"active_workflows": "Active Workflows",
|
| 22 |
+
"kb_usage": "KB Usage",
|
| 23 |
+
"api_calls": "API Calls"
|
| 24 |
+
}
|
| 25 |
+
},
|
| 26 |
+
"chat": {
|
| 27 |
+
"placeholder": "Type your message...",
|
| 28 |
+
"warning": "AI-generated content may be inaccurate, please use with caution.",
|
| 29 |
+
"clear": "Clear Chat"
|
| 30 |
+
},
|
| 31 |
+
"workflow": {
|
| 32 |
+
"title": "Visual Workflow Orchestrator",
|
| 33 |
+
"subtitle": "Drag and drop nodes to build complex AI logic",
|
| 34 |
+
"add_node": "Add Node",
|
| 35 |
+
"save_draft": "Save Draft",
|
| 36 |
+
"run_test": "Run Test"
|
| 37 |
+
},
|
| 38 |
+
"billing": {
|
| 39 |
+
"current_plan": "Current Plan",
|
| 40 |
+
"upgrade": "Upgrade Plan",
|
| 41 |
+
"free": "Free",
|
| 42 |
+
"pro": "Pro",
|
| 43 |
+
"enterprise": "Enterprise"
|
| 44 |
+
}
|
| 45 |
+
}
|
src/locales/zh/translation.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"common": {
|
| 3 |
+
"welcome": "欢迎回来",
|
| 4 |
+
"dashboard": "控制台",
|
| 5 |
+
"chat": "智能对话",
|
| 6 |
+
"knowledge": "知识库",
|
| 7 |
+
"workflow": "工作流",
|
| 8 |
+
"billing": "订阅与支付",
|
| 9 |
+
"settings": "个人设置",
|
| 10 |
+
"logout": "退出登录",
|
| 11 |
+
"save": "保存",
|
| 12 |
+
"cancel": "取消",
|
| 13 |
+
"submit": "提交",
|
| 14 |
+
"loading": "加载中...",
|
| 15 |
+
"error": "发生错误"
|
| 16 |
+
},
|
| 17 |
+
"dashboard": {
|
| 18 |
+
"title": "开发者控制台",
|
| 19 |
+
"description": "管理您的 AI 资产、编排业务流程并集成支付闭环。",
|
| 20 |
+
"stats": {
|
| 21 |
+
"active_workflows": "活跃工作流",
|
| 22 |
+
"kb_usage": "知识库占用",
|
| 23 |
+
"api_calls": "API 调用量"
|
| 24 |
+
}
|
| 25 |
+
},
|
| 26 |
+
"chat": {
|
| 27 |
+
"placeholder": "输入您的问题...",
|
| 28 |
+
"warning": "AI 生成的内容可能存在误差,请审慎参考。",
|
| 29 |
+
"clear": "清除对话"
|
| 30 |
+
},
|
| 31 |
+
"workflow": {
|
| 32 |
+
"title": "可视化工作流编排",
|
| 33 |
+
"subtitle": "拖拽节点构建复杂的 AI 业务逻辑",
|
| 34 |
+
"add_node": "添加节点",
|
| 35 |
+
"save_draft": "保存草稿",
|
| 36 |
+
"run_test": "运行测试"
|
| 37 |
+
},
|
| 38 |
+
"billing": {
|
| 39 |
+
"current_plan": "当前方案",
|
| 40 |
+
"upgrade": "升级方案",
|
| 41 |
+
"free": "免费版",
|
| 42 |
+
"pro": "专业版",
|
| 43 |
+
"enterprise": "企业版"
|
| 44 |
+
}
|
| 45 |
+
}
|
src/main.tsx
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import ReactDOM from 'react-dom/client';
|
| 3 |
+
import App from './App';
|
| 4 |
+
import './index.css';
|
| 5 |
+
import './i18n'; // 导入 i18n 配置
|
| 6 |
+
|
| 7 |
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
| 8 |
+
<React.StrictMode>
|
| 9 |
+
<App />
|
| 10 |
+
</React.StrictMode>
|
| 11 |
+
);
|
src/pages/Home.tsx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export default function Home() {
|
| 2 |
+
return <div></div>;
|
| 3 |
+
}
|
src/pages/dashboard/Chat.tsx
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useRef, useEffect } from 'react';
|
| 2 |
+
import { Send, User, Bot, Loader2, Trash2 } from 'lucide-react';
|
| 3 |
+
import { useStore } from '@/store/useStore';
|
| 4 |
+
import { motion, AnimatePresence } from 'framer-motion';
|
| 5 |
+
import { useTranslation } from 'react-i18next';
|
| 6 |
+
|
| 7 |
+
export default function ChatPage() {
|
| 8 |
+
const { t } = useTranslation();
|
| 9 |
+
const [input, setInput] = useState('');
|
| 10 |
+
const [isLoading, setIsLoading] = useState(false);
|
| 11 |
+
const { chatHistory, addChatMessage, clearChat } = useStore();
|
| 12 |
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
| 13 |
+
|
| 14 |
+
const scrollToBottom = () => {
|
| 15 |
+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
| 16 |
+
};
|
| 17 |
+
|
| 18 |
+
useEffect(() => {
|
| 19 |
+
scrollToBottom();
|
| 20 |
+
}, [chatHistory]);
|
| 21 |
+
|
| 22 |
+
const handleSubmit = async (e: React.FormEvent) => {
|
| 23 |
+
e.preventDefault();
|
| 24 |
+
if (!input.trim() || isLoading) return;
|
| 25 |
+
|
| 26 |
+
const userQuery = input.trim();
|
| 27 |
+
setInput('');
|
| 28 |
+
setIsLoading(true);
|
| 29 |
+
|
| 30 |
+
addChatMessage({ content: userQuery, role: 'user' });
|
| 31 |
+
|
| 32 |
+
try {
|
| 33 |
+
const response = await fetch('/api/ai/chat', {
|
| 34 |
+
method: 'POST',
|
| 35 |
+
headers: { 'Content-Type': 'application/json' },
|
| 36 |
+
body: JSON.stringify({ query: userQuery, userId: 'test-user' }),
|
| 37 |
+
});
|
| 38 |
+
|
| 39 |
+
const reader = response.body?.getReader();
|
| 40 |
+
const decoder = new TextDecoder();
|
| 41 |
+
let assistantContent = '';
|
| 42 |
+
|
| 43 |
+
if (reader) {
|
| 44 |
+
while (true) {
|
| 45 |
+
const { done, value } = await reader.read();
|
| 46 |
+
if (done) break;
|
| 47 |
+
const chunk = decoder.decode(value);
|
| 48 |
+
const lines = chunk.split('\n');
|
| 49 |
+
for (const line of lines) {
|
| 50 |
+
if (line.startsWith('data: ')) {
|
| 51 |
+
try {
|
| 52 |
+
const data = JSON.parse(line.slice(6));
|
| 53 |
+
if (data.content) assistantContent += data.content;
|
| 54 |
+
} catch (e) {}
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
addChatMessage({ content: assistantContent, role: 'assistant' });
|
| 59 |
+
}
|
| 60 |
+
} catch (err) {
|
| 61 |
+
addChatMessage({ content: t('common.error'), role: 'assistant' });
|
| 62 |
+
} finally {
|
| 63 |
+
setIsLoading(false);
|
| 64 |
+
}
|
| 65 |
+
};
|
| 66 |
+
|
| 67 |
+
return (
|
| 68 |
+
<div className="flex flex-col h-full max-w-4xl mx-auto bg-white rounded-xl shadow-sm border border-zinc-200 overflow-hidden">
|
| 69 |
+
<div className="p-4 border-b border-zinc-100 flex items-center justify-between bg-zinc-50/50">
|
| 70 |
+
<h2 className="text-sm font-semibold text-zinc-900">{t('common.chat')}</h2>
|
| 71 |
+
<button
|
| 72 |
+
onClick={clearChat}
|
| 73 |
+
className="p-1.5 text-zinc-400 hover:text-red-500 transition-colors"
|
| 74 |
+
title={t('chat.clear')}
|
| 75 |
+
>
|
| 76 |
+
<Trash2 size={16} />
|
| 77 |
+
</button>
|
| 78 |
+
</div>
|
| 79 |
+
|
| 80 |
+
<div className="flex-1 overflow-y-auto p-4 lg:p-6 space-y-6">
|
| 81 |
+
<AnimatePresence>
|
| 82 |
+
{chatHistory.map((msg, idx) => (
|
| 83 |
+
<motion.div
|
| 84 |
+
key={idx}
|
| 85 |
+
initial={{ opacity: 0, y: 10 }}
|
| 86 |
+
animate={{ opacity: 1, y: 0 }}
|
| 87 |
+
className={`flex gap-4 ${msg.role === 'user' ? 'flex-row-reverse' : 'flex-row'}`}
|
| 88 |
+
>
|
| 89 |
+
<div className={`w-8 h-8 rounded-lg flex items-center justify-center shrink-0 ${
|
| 90 |
+
msg.role === 'user' ? 'bg-blue-600 text-white' : 'bg-zinc-100 text-zinc-600'
|
| 91 |
+
}`}>
|
| 92 |
+
{msg.role === 'user' ? <User size={16} /> : <Bot size={16} />}
|
| 93 |
+
</div>
|
| 94 |
+
<div className={`
|
| 95 |
+
max-w-[85%] px-4 py-2 rounded-2xl text-sm leading-relaxed
|
| 96 |
+
${msg.role === 'user' ? 'bg-blue-600 text-white rounded-tr-none' : 'bg-zinc-100 text-zinc-900 rounded-tl-none'}
|
| 97 |
+
`}>
|
| 98 |
+
{msg.content}
|
| 99 |
+
</div>
|
| 100 |
+
</motion.div>
|
| 101 |
+
))}
|
| 102 |
+
</AnimatePresence>
|
| 103 |
+
<div ref={messagesEndRef} />
|
| 104 |
+
</div>
|
| 105 |
+
|
| 106 |
+
<form onSubmit={handleSubmit} className="p-4 border-t border-zinc-100">
|
| 107 |
+
<div className="relative flex items-center">
|
| 108 |
+
<input
|
| 109 |
+
type="text"
|
| 110 |
+
value={input}
|
| 111 |
+
onChange={(e) => setInput(e.target.value)}
|
| 112 |
+
placeholder={t('chat.placeholder')}
|
| 113 |
+
disabled={isLoading}
|
| 114 |
+
className="w-full pl-4 pr-12 py-3 bg-zinc-50 border-none rounded-xl focus:ring-2 focus:ring-blue-500 text-sm transition-all"
|
| 115 |
+
/>
|
| 116 |
+
<button
|
| 117 |
+
type="submit"
|
| 118 |
+
disabled={isLoading || !input.trim()}
|
| 119 |
+
className="absolute right-2 p-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 transition-colors"
|
| 120 |
+
>
|
| 121 |
+
{isLoading ? <Loader2 size={18} className="animate-spin" /> : <Send size={18} />}
|
| 122 |
+
</button>
|
| 123 |
+
</div>
|
| 124 |
+
<p className="mt-2 text-[10px] text-center text-zinc-400">{t('chat.warning')}</p>
|
| 125 |
+
</form>
|
| 126 |
+
</div>
|
| 127 |
+
);
|
| 128 |
+
}
|
src/pages/dashboard/Layout.tsx
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState } from 'react';
|
| 2 |
+
import { Link, Outlet, useLocation } from 'react-router-dom';
|
| 3 |
+
import {
|
| 4 |
+
LayoutDashboard, MessageSquare, Database, GitBranch,
|
| 5 |
+
CreditCard, Settings, Menu, X, Globe
|
| 6 |
+
} from 'lucide-react';
|
| 7 |
+
import { useTranslation } from 'react-i18next';
|
| 8 |
+
import { useStore } from '@/store/useStore';
|
| 9 |
+
|
| 10 |
+
export default function DashboardLayout() {
|
| 11 |
+
const { t } = useTranslation();
|
| 12 |
+
const { language, setLanguage } = useStore();
|
| 13 |
+
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
| 14 |
+
const location = useLocation();
|
| 15 |
+
|
| 16 |
+
const sidebarItems = [
|
| 17 |
+
{ icon: LayoutDashboard, label: t('common.dashboard'), path: '/dashboard' },
|
| 18 |
+
{ icon: MessageSquare, label: t('common.chat'), path: '/dashboard/chat' },
|
| 19 |
+
{ icon: Database, label: t('common.knowledge'), path: '/dashboard/knowledge' },
|
| 20 |
+
{ icon: GitBranch, label: t('common.workflow'), path: '/dashboard/workflow' },
|
| 21 |
+
{ icon: CreditCard, label: t('common.billing'), path: '/dashboard/billing' },
|
| 22 |
+
{ icon: Settings, label: t('common.settings'), path: '/dashboard/settings' },
|
| 23 |
+
];
|
| 24 |
+
|
| 25 |
+
return (
|
| 26 |
+
<div className="flex h-screen bg-zinc-50 overflow-hidden">
|
| 27 |
+
{/* 移动端菜单按钮 */}
|
| 28 |
+
<button
|
| 29 |
+
className="lg:hidden fixed top-4 left-4 z-50 p-2 bg-white rounded-md shadow-md"
|
| 30 |
+
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
|
| 31 |
+
>
|
| 32 |
+
{isSidebarOpen ? <X size={20} /> : <Menu size={20} />}
|
| 33 |
+
</button>
|
| 34 |
+
|
| 35 |
+
{/* 侧边栏 */}
|
| 36 |
+
<aside className={`
|
| 37 |
+
fixed inset-y-0 left-0 z-40 w-64 bg-white border-r border-zinc-200 transform transition-transform duration-200 ease-in-out
|
| 38 |
+
lg:relative lg:translate-x-0
|
| 39 |
+
${isSidebarOpen ? 'translate-x-0' : '-translate-x-full'}
|
| 40 |
+
`}>
|
| 41 |
+
<div className="flex flex-col h-full">
|
| 42 |
+
<div className="p-6 flex items-center justify-between">
|
| 43 |
+
<h1 className="text-xl font-bold text-blue-600 flex items-center gap-2">
|
| 44 |
+
<div className="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center text-white text-xs">AI</div>
|
| 45 |
+
Codex
|
| 46 |
+
</h1>
|
| 47 |
+
</div>
|
| 48 |
+
|
| 49 |
+
<nav className="flex-1 px-4 space-y-1 overflow-y-auto">
|
| 50 |
+
{sidebarItems.map((item) => (
|
| 51 |
+
<Link
|
| 52 |
+
key={item.path}
|
| 53 |
+
to={item.path}
|
| 54 |
+
onClick={() => setIsSidebarOpen(false)}
|
| 55 |
+
className={`
|
| 56 |
+
flex items-center gap-3 px-3 py-2 rounded-lg transition-colors
|
| 57 |
+
${location.pathname === item.path
|
| 58 |
+
? 'bg-blue-50 text-blue-600'
|
| 59 |
+
: 'text-zinc-600 hover:bg-zinc-100'}
|
| 60 |
+
`}
|
| 61 |
+
>
|
| 62 |
+
<item.icon size={18} />
|
| 63 |
+
<span className="font-medium text-sm">{item.label}</span>
|
| 64 |
+
</Link>
|
| 65 |
+
))}
|
| 66 |
+
</nav>
|
| 67 |
+
|
| 68 |
+
{/* 语言切换 & 用户信息 */}
|
| 69 |
+
<div className="p-4 border-t border-zinc-200 space-y-4">
|
| 70 |
+
<button
|
| 71 |
+
onClick={() => setLanguage(language === 'zh' ? 'en' : 'zh')}
|
| 72 |
+
className="flex items-center gap-3 px-3 py-2 w-full text-zinc-600 hover:bg-zinc-100 rounded-lg transition-colors text-sm"
|
| 73 |
+
>
|
| 74 |
+
<Globe size={18} />
|
| 75 |
+
<span>{language === 'zh' ? 'English' : '中文'}</span>
|
| 76 |
+
</button>
|
| 77 |
+
<div className="flex items-center gap-3 px-3 py-2">
|
| 78 |
+
<div className="w-8 h-8 bg-zinc-200 rounded-full flex items-center justify-center text-[10px]">USER</div>
|
| 79 |
+
<div className="flex-1 min-w-0">
|
| 80 |
+
<p className="text-xs font-medium text-zinc-900 truncate">Dev User</p>
|
| 81 |
+
<p className="text-[10px] text-zinc-500 truncate">{t('billing.free')}</p>
|
| 82 |
+
</div>
|
| 83 |
+
</div>
|
| 84 |
+
</div>
|
| 85 |
+
</div>
|
| 86 |
+
</aside>
|
| 87 |
+
|
| 88 |
+
<main className="flex-1 flex flex-col overflow-hidden relative">
|
| 89 |
+
<div className="flex-1 overflow-y-auto p-4 lg:p-8">
|
| 90 |
+
<Outlet />
|
| 91 |
+
</div>
|
| 92 |
+
</main>
|
| 93 |
+
|
| 94 |
+
{isSidebarOpen && (
|
| 95 |
+
<div
|
| 96 |
+
className="fixed inset-0 z-30 bg-black/20 lg:hidden"
|
| 97 |
+
onClick={() => setIsSidebarOpen(false)}
|
| 98 |
+
/>
|
| 99 |
+
)}
|
| 100 |
+
</div>
|
| 101 |
+
);
|
| 102 |
+
}
|
src/pages/dashboard/Workflow.tsx
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useCallback } from 'react';
|
| 2 |
+
import ReactFlow, {
|
| 3 |
+
addEdge, Background, Controls, MiniMap,
|
| 4 |
+
useNodesState, useEdgesState, Connection, Edge
|
| 5 |
+
} from 'reactflow';
|
| 6 |
+
import 'reactflow/dist/style.css';
|
| 7 |
+
import { Play, Save, Plus } from 'lucide-react';
|
| 8 |
+
import { useTranslation } from 'react-i18next';
|
| 9 |
+
|
| 10 |
+
const initialNodes = [
|
| 11 |
+
{ id: '1', type: 'input', data: { label: 'Input' }, position: { x: 250, y: 5 }, style: { background: '#EBF5FF', border: '1px solid #3B82F6', borderRadius: '8px' } },
|
| 12 |
+
{ id: '2', data: { label: 'LLM Node' }, position: { x: 250, y: 100 }, style: { background: '#FDF2F2', border: '1px solid #EF4444', borderRadius: '8px' } },
|
| 13 |
+
];
|
| 14 |
+
|
| 15 |
+
const initialEdges = [{ id: 'e1-2', source: '1', target: '2', animated: true }];
|
| 16 |
+
|
| 17 |
+
export default function WorkflowPage() {
|
| 18 |
+
const { t } = useTranslation();
|
| 19 |
+
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
| 20 |
+
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
| 21 |
+
|
| 22 |
+
const onConnect = useCallback(
|
| 23 |
+
(params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)),
|
| 24 |
+
[setEdges]
|
| 25 |
+
);
|
| 26 |
+
|
| 27 |
+
return (
|
| 28 |
+
<div className="h-full flex flex-col bg-white rounded-xl shadow-sm border border-zinc-200 overflow-hidden">
|
| 29 |
+
<div className="p-4 border-b border-zinc-100 flex items-center justify-between bg-zinc-50/50">
|
| 30 |
+
<div>
|
| 31 |
+
<h2 className="text-sm font-semibold text-zinc-900">{t('workflow.title')}</h2>
|
| 32 |
+
<p className="text-[10px] text-zinc-500">{t('workflow.subtitle')}</p>
|
| 33 |
+
</div>
|
| 34 |
+
<div className="flex items-center gap-2">
|
| 35 |
+
<button className="flex items-center gap-1.5 px-3 py-1.5 bg-white border border-zinc-200 rounded-lg text-[10px] font-medium text-zinc-600 hover:bg-zinc-50 transition-colors">
|
| 36 |
+
<Plus size={12} />
|
| 37 |
+
{t('workflow.add_node')}
|
| 38 |
+
</button>
|
| 39 |
+
<button className="flex items-center gap-1.5 px-3 py-1.5 bg-blue-600 text-white rounded-lg text-[10px] font-medium hover:bg-blue-700 transition-colors">
|
| 40 |
+
<Play size={12} />
|
| 41 |
+
{t('workflow.run_test')}
|
| 42 |
+
</button>
|
| 43 |
+
</div>
|
| 44 |
+
</div>
|
| 45 |
+
|
| 46 |
+
<div className="flex-1 relative">
|
| 47 |
+
<ReactFlow
|
| 48 |
+
nodes={nodes}
|
| 49 |
+
edges={edges}
|
| 50 |
+
onNodesChange={onNodesChange}
|
| 51 |
+
onEdgesChange={onEdgesChange}
|
| 52 |
+
onConnect={onConnect}
|
| 53 |
+
fitView
|
| 54 |
+
>
|
| 55 |
+
<Background color="#F1F5F9" gap={20} />
|
| 56 |
+
<Controls />
|
| 57 |
+
<MiniMap nodeStrokeWidth={3} zoomable pannable />
|
| 58 |
+
</ReactFlow>
|
| 59 |
+
</div>
|
| 60 |
+
</div>
|
| 61 |
+
);
|
| 62 |
+
}
|
src/store/useStore.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { create } from 'zustand';
|
| 2 |
+
import { User, Workflow, AIResponse } from '@shared/types';
|
| 3 |
+
import i18n from '@/i18n';
|
| 4 |
+
|
| 5 |
+
interface AppState {
|
| 6 |
+
user: User | null;
|
| 7 |
+
language: string;
|
| 8 |
+
setLanguage: (lang: string) => void;
|
| 9 |
+
setUser: (user: User | null) => void;
|
| 10 |
+
|
| 11 |
+
chatHistory: AIResponse[];
|
| 12 |
+
addChatMessage: (message: AIResponse) => void;
|
| 13 |
+
clearChat: () => void;
|
| 14 |
+
|
| 15 |
+
workflows: Workflow[];
|
| 16 |
+
activeWorkflow: Workflow | null;
|
| 17 |
+
setActiveWorkflow: (workflow: Workflow | null) => void;
|
| 18 |
+
updateWorkflow: (workflow: Workflow) => void;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
export const useStore = create<AppState>((set) => ({
|
| 22 |
+
user: null,
|
| 23 |
+
language: i18n.language || 'zh',
|
| 24 |
+
setLanguage: (lang) => {
|
| 25 |
+
i18n.changeLanguage(lang);
|
| 26 |
+
set({ language: lang });
|
| 27 |
+
},
|
| 28 |
+
setUser: (user) => set({ user }),
|
| 29 |
+
|
| 30 |
+
chatHistory: [],
|
| 31 |
+
addChatMessage: (message) => set((state) => ({
|
| 32 |
+
chatHistory: [...state.chatHistory, message]
|
| 33 |
+
})),
|
| 34 |
+
clearChat: () => set({ chatHistory: [] }),
|
| 35 |
+
|
| 36 |
+
workflows: [],
|
| 37 |
+
activeWorkflow: null,
|
| 38 |
+
setActiveWorkflow: (workflow) => set({ activeWorkflow: workflow }),
|
| 39 |
+
updateWorkflow: (workflow) => set((state) => ({
|
| 40 |
+
workflows: state.workflows.map((w) => w.id === workflow.id ? workflow : w)
|
| 41 |
+
})),
|
| 42 |
+
}));
|
src/vite-env.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
/// <reference types="vite/client" />
|
tailwind.config.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** @type {import('tailwindcss').Config} */
|
| 2 |
+
|
| 3 |
+
export default {
|
| 4 |
+
darkMode: "class",
|
| 5 |
+
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
| 6 |
+
theme: {
|
| 7 |
+
container: {
|
| 8 |
+
center: true,
|
| 9 |
+
},
|
| 10 |
+
extend: {},
|
| 11 |
+
},
|
| 12 |
+
plugins: [],
|
| 13 |
+
};
|
tsconfig.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
| 4 |
+
"target": "ES2020",
|
| 5 |
+
"useDefineForClassFields": true,
|
| 6 |
+
"lib": [
|
| 7 |
+
"ES2020",
|
| 8 |
+
"DOM",
|
| 9 |
+
"DOM.Iterable"
|
| 10 |
+
],
|
| 11 |
+
"module": "ESNext",
|
| 12 |
+
"skipLibCheck": true,
|
| 13 |
+
"moduleResolution": "bundler",
|
| 14 |
+
"allowImportingTsExtensions": true,
|
| 15 |
+
"verbatimModuleSyntax": false,
|
| 16 |
+
"moduleDetection": "force",
|
| 17 |
+
"noEmit": true,
|
| 18 |
+
"jsx": "react-jsx",
|
| 19 |
+
"strict": false,
|
| 20 |
+
"noUnusedLocals": false,
|
| 21 |
+
"noUnusedParameters": false,
|
| 22 |
+
"noFallthroughCasesInSwitch": false,
|
| 23 |
+
"noUncheckedSideEffectImports": false,
|
| 24 |
+
"forceConsistentCasingInFileNames": false,
|
| 25 |
+
"baseUrl": "./",
|
| 26 |
+
"paths": {
|
| 27 |
+
"@/*": [
|
| 28 |
+
"./src/*"
|
| 29 |
+
]
|
| 30 |
+
},
|
| 31 |
+
"types": [
|
| 32 |
+
"node",
|
| 33 |
+
"express"
|
| 34 |
+
]
|
| 35 |
+
},
|
| 36 |
+
"include": [
|
| 37 |
+
"src",
|
| 38 |
+
"api"
|
| 39 |
+
]
|
| 40 |
+
}
|
vercel.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"rewrites": [
|
| 3 |
+
{
|
| 4 |
+
"source": "/api/(.*)",
|
| 5 |
+
"destination": "/api/index"
|
| 6 |
+
},
|
| 7 |
+
{
|
| 8 |
+
"source": "/(.*)",
|
| 9 |
+
"destination": "/index.html"
|
| 10 |
+
}
|
| 11 |
+
]
|
| 12 |
+
}
|
vite.config.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from 'vite';
|
| 2 |
+
import react from '@vitejs/plugin-react';
|
| 3 |
+
import { VitePWA } from 'vite-plugin-pwa';
|
| 4 |
+
import path from 'path';
|
| 5 |
+
|
| 6 |
+
export default defineConfig({
|
| 7 |
+
plugins: [
|
| 8 |
+
react(),
|
| 9 |
+
VitePWA({
|
| 10 |
+
registerType: 'autoUpdate',
|
| 11 |
+
includeAssets: ['favicon.svg', 'apple-touch-icon.png', 'masked-icon.svg'],
|
| 12 |
+
manifest: {
|
| 13 |
+
name: 'Codex AI Platform',
|
| 14 |
+
short_name: 'CodexAI',
|
| 15 |
+
description: '全栈 AI 知识库与工作流编排平台',
|
| 16 |
+
theme_color: '#1E40AF',
|
| 17 |
+
icons: [
|
| 18 |
+
{
|
| 19 |
+
src: 'pwa-192x192.png',
|
| 20 |
+
sizes: '192x192',
|
| 21 |
+
type: 'image/png'
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
src: 'pwa-512x512.png',
|
| 25 |
+
sizes: '512x512',
|
| 26 |
+
type: 'image/png'
|
| 27 |
+
}
|
| 28 |
+
]
|
| 29 |
+
}
|
| 30 |
+
})
|
| 31 |
+
],
|
| 32 |
+
resolve: {
|
| 33 |
+
alias: {
|
| 34 |
+
'@': path.resolve(__dirname, './src'),
|
| 35 |
+
'@shared': path.resolve(__dirname, './shared'),
|
| 36 |
+
},
|
| 37 |
+
},
|
| 38 |
+
server: {
|
| 39 |
+
proxy: {
|
| 40 |
+
'/api': {
|
| 41 |
+
target: 'http://localhost:3001',
|
| 42 |
+
changeOrigin: true,
|
| 43 |
+
},
|
| 44 |
+
},
|
| 45 |
+
},
|
| 46 |
+
});
|