3v324v23 commited on
Commit
6bb2fbe
·
0 Parent(s):

chore: initial commit for huggingface spaces

Browse files
.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
+ });