diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/api/v1/__init__.py b/backend/app/api/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/api/v1/api.py b/backend/app/api/v1/api.py new file mode 100644 index 0000000..5f60c65 --- /dev/null +++ b/backend/app/api/v1/api.py @@ -0,0 +1,15 @@ +# File: backend/app/api/v1/api.py +# Description: 聚合 v1 版本的所有 API 路由 + +from fastapi import APIRouter +from app.api.v1.endpoints import chat # 导入聊天路由 + +# 创建 v1 版本的总路由 +api_router = APIRouter() + +# 将聊天路由包含到 v1 总路由中,并添加前缀 +api_router.include_router(chat.router, prefix="/chat", tags=["chat"]) + +# --- 如果有其他路由,也在这里 include --- +# from app.api.v1.endpoints import workflow +# api_router.include_router(workflow.router, prefix="/workflow", tags=["workflow"]) diff --git a/backend/app/api/v1/endpoints/__init__.py b/backend/app/api/v1/endpoints/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/api/v1/endpoints/chat.py b/backend/app/api/v1/endpoints/chat.py new file mode 100644 index 0000000..90e9e40 --- /dev/null +++ b/backend/app/api/v1/endpoints/chat.py @@ -0,0 +1,41 @@ +# File: backend/app/api/v1/endpoints/chat.py (更新) +# Description: 聊天功能的 API 路由 (使用 ChatService) + +from fastapi import APIRouter, HTTPException, Depends +from app.models.pydantic_models import ChatRequest, ChatResponse +# 导入 ChatService 实例 +from app.services.chat_service import chat_service_instance, ChatService + +router = APIRouter() + +# --- (可选) 使用 FastAPI 的依赖注入来获取 ChatService 实例 --- +# 这样更符合 FastAPI 的风格,方便测试和替换实现 +# async def get_chat_service() -> ChatService: +# return chat_service_instance + +@router.post("/", response_model=ChatResponse) +async def handle_chat_message( + request: ChatRequest, + # chat_service: ChatService = Depends(get_chat_service) # 使用依赖注入 +): + """ + 处理用户发送的聊天消息,并使用 LangChain 获取 AI 回复 + """ + user_message = request.message + # session_id = request.session_id # 如果 ChatRequest 中包含 session_id + print(f"接收到用户消息: {user_message}") + + try: + # --- 调用 ChatService 获取 AI 回复 --- + # 使用全局实例 (简单方式) + ai_reply = await chat_service_instance.get_ai_reply(user_message) + # 或者使用依赖注入获取的实例 + # ai_reply = await chat_service.get_ai_reply(user_message, session_id) + + print(f"发送 AI 回复: {ai_reply}") + return ChatResponse(reply=ai_reply) + + except Exception as e: + # 如果 ChatService 抛出异常,捕获并返回 HTTP 500 错误 + print(f"处理聊天消息时发生错误: {e}") + raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file diff --git a/backend/app/api/v1/endpoints/rag.py b/backend/app/api/v1/endpoints/rag.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/api/v1/endpoints/workflow.py b/backend/app/api/v1/endpoints/workflow.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/core/__init__.py b/backend/app/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/core/config.py b/backend/app/core/config.py new file mode 100644 index 0000000..d8327ff --- /dev/null +++ b/backend/app/core/config.py @@ -0,0 +1,15 @@ +# File: backend/app/core/config.py (新建或修改) +# Description: 读取环境变量配置 + +import os +from dotenv import load_dotenv + +# 加载 .env 文件中的环境变量 +# 这会在应用启动时查找 .env 文件并加载其内容 +load_dotenv() + +# 从环境变量获取 OpenAI API Key +OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") +GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY") # 如果使用 Google + +# 可以在这里添加其他配置项 \ No newline at end of file diff --git a/backend/app/db/__init__.py b/backend/app/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/db/session.py b/backend/app/db/session.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..45e4ef2 --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,32 @@ +# File: backend/app/main.py (确认 load_dotenv 调用位置) +# Description: FastAPI 应用入口 + +from fastapi import FastAPI +from starlette.middleware.cors import CORSMiddleware +from app.api.v1.api import api_router as api_router_v1 +# 确保在创建 FastAPI 实例之前加载环境变量 +from app.core.config import OPENAI_API_KEY # 导入会触发 load_dotenv + +# 创建 FastAPI 应用实例 +app = FastAPI(title="CherryAI Backend", version="0.1.0") + +# --- 配置 CORS --- +origins = [ + "http://localhost:3000", + "http://127.0.0.1:3000", +] +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# --- 挂载 API 路由 --- +app.include_router(api_router_v1, prefix="/api/v1") + +# --- 根路径 --- +@app.get("/", tags=["Root"]) +async def read_root(): + return {"message": "欢迎来到 CherryAI 后端!"} diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/models/db_models.py b/backend/app/models/db_models.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/models/pydantic_models.py b/backend/app/models/pydantic_models.py new file mode 100644 index 0000000..e8f1f97 --- /dev/null +++ b/backend/app/models/pydantic_models.py @@ -0,0 +1,16 @@ +# File: backend/app/models/pydantic_models.py +# Description: Pydantic 模型定义 API 数据结构 + +from pydantic import BaseModel + +class ChatRequest(BaseModel): + """聊天请求模型""" + message: str + # 可以添加更多字段,如 user_id, session_id 等 + +class ChatResponse(BaseModel): + """聊天响应模型""" + reply: str + # 可以添加更多字段,如 message_id, status 等 + +# --- 你可以在这里添加其他功能的模型 --- \ No newline at end of file diff --git a/backend/app/services/__init__.py b/backend/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/services/chat_service.py b/backend/app/services/chat_service.py new file mode 100644 index 0000000..2dab1d6 --- /dev/null +++ b/backend/app/services/chat_service.py @@ -0,0 +1,92 @@ +# File: backend/app/services/chat_service.py (新建) +# Description: 封装 LangChain 聊天逻辑 + +from langchain_openai import ChatOpenAI +from langchain_google_genai import ChatGoogleGenerativeAI # 如果使用 Google +from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder +from langchain_core.output_parsers import StrOutputParser +from langchain_core.messages import HumanMessage, AIMessage + +# --- 可选:添加内存管理 --- +# 简单的内存实现 (可以替换为更复杂的 LangChain Memory 类) +chat_history = {} # 使用字典存储不同会话的内存,需要 session_id + +class ChatService: + """处理 AI 聊天交互的服务""" + + def __init__(self, api_key: str): + """ + 初始化 ChatService + Args: + api_key (str): 用于 LLM 的 API 密钥 + """ + # --- 选择并初始化 LLM --- + # 使用 OpenAI GPT-3.5 Turbo (推荐) 或 GPT-4 + # self.llm = ChatOpenAI(model="gpt-3.5-turbo", api_key=api_key, temperature=0.7) + # self.llm = ChatOpenAI(model="gpt-4", api_key=api_key, temperature=0.7) + + # --- 如果使用 Google Gemini --- + self.llm = ChatGoogleGenerativeAI(model="gemini-2.0-flash", google_api_key=api_key, convert_system_message_to_human=True) + + # --- 定义 Prompt 模板 --- + # 包含系统消息、历史记录占位符和当前用户输入 + self.prompt = ChatPromptTemplate.from_messages([ + ("system", "你是一个乐于助人的 AI 助手,请用简洁明了的语言回答问题。你的名字叫 CherryAI。"), + MessagesPlaceholder(variable_name="chat_history"), # 用于插入历史消息 + ("human", "{input}"), # 用户当前输入 + ]) + + # --- 定义输出解析器 --- + # 将 LLM 的输出解析为字符串 + self.output_parser = StrOutputParser() + + # --- 构建 LangChain Expression Language (LCEL) 链 --- + self.chain = self.prompt | self.llm | self.output_parser + + async def get_ai_reply(self, user_message: str, session_id: str = "default_session") -> str: + """ + 获取 AI 对用户消息的回复 (异步) + Args: + user_message (str): 用户发送的消息 + session_id (str): (可选) 用于区分不同对话的会话 ID,以支持内存 + Returns: + str: AI 的回复文本 + Raises: + Exception: 如果调用 AI 服务时发生错误 + """ + try: + # --- 获取当前会话的历史记录 (如果需要内存) --- + current_chat_history = chat_history.get(session_id, []) + + # --- 使用 ainvoke 进行异步调用 --- + ai_response = await self.chain.ainvoke({ + "input": user_message, + "chat_history": current_chat_history, # 传入历史记录 + }) + + # --- 更新会话历史记录 (如果需要内存) --- + # 只保留最近 N 轮对话,防止历史过长 + max_history_length = 10 # 保留最近 5 轮对话 (10条消息) + current_chat_history.append(HumanMessage(content=user_message)) + current_chat_history.append(AIMessage(content=ai_response)) + # 如果历史记录超过长度,移除最早的消息 + if len(current_chat_history) > max_history_length: + chat_history[session_id] = current_chat_history[-max_history_length:] + else: + chat_history[session_id] = current_chat_history + + return ai_response + + except Exception as e: + print(f"调用 LangChain 时出错: {e}") + # 可以进行更细致的错误处理,例如区分 API 错误和内部错误 + raise Exception(f"AI 服务暂时不可用: {e}") + + +# --- 在文件末尾,创建 ChatService 的实例 --- +# 从配置中获取 API Key +from app.core.config import GOOGLE_API_KEY +if not GOOGLE_API_KEY: + raise ValueError("请在 .env 文件中设置 GOOGLE_API_KEY") + +chat_service_instance = ChatService(api_key=GOOGLE_API_KEY) diff --git a/backend/app/services/rag_service.py b/backend/app/services/rag_service.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/services/workflow_service.py b/backend/app/services/workflow_service.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/utils/__init__.py b/backend/app/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/frontend/.vscode/settings.json b/frontend/.vscode/settings.json new file mode 100644 index 0000000..3b835cd --- /dev/null +++ b/frontend/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "css.customData": [ + ".vscode/tailwindcss.json" + ], + "files.associations": { + "*.css": "tailwindcss" + } +} \ No newline at end of file diff --git a/frontend/app/chat/page.tsx b/frontend/app/chat/page.tsx new file mode 100644 index 0000000..82ec6ae --- /dev/null +++ b/frontend/app/chat/page.tsx @@ -0,0 +1,291 @@ +// File: frontend/app/chat/page.tsx (重构布局) +// Description: AI 聊天界面组件,包含左侧助手面板、中间聊天区、右侧会话面板 + +'use client'; + +import React, { useState, useRef, useEffect } from 'react'; +import { SendHorizontal, Loader2, PanelRightOpen, PanelRightClose, UserPlus, Settings2 } from 'lucide-react'; // 添加图标 +import { sendChatMessage } from '@/lib/api'; + +// --- 数据接口定义 --- +interface Message { + id: string; + text: string; + sender: 'user' | 'ai'; + isError?: boolean; +} + +interface ChatSession { + id: string; + title: string; + createdAt: Date; +} + +// 定义助手接口 (示例) +interface Assistant { + id: string; + name: string; + description: string; + avatar?: string; // 可选头像 URL 或 emoji + systemPrompt: string; // 核心:系统提示 + model: string; // 例如 'gpt-3.5-turbo', 'gemini-pro' + temperature: number; + // 可以添加 top_p, max_tokens 等其他参数 +} + +export default function ChatPage() { + // --- State Variables --- + const [inputMessage, setInputMessage] = useState(''); + const [messages, setMessages] = useState([ + { id: 'init-1', text: '你好!我是默认助手,有什么可以帮你的吗?', sender: 'ai' }, + ]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [isSessionPanelOpen, setIsSessionPanelOpen] = useState(true); // 右侧会话面板状态 + + // --- Mock Data --- + // 助手列表 Mock 数据 + const [assistants, setAssistants] = useState([ + { id: 'asst-default', name: '默认助手', description: '通用聊天助手', avatar: '🤖', systemPrompt: '你是一个乐于助人的 AI 助手。', model: 'gpt-3.5-turbo', temperature: 0.7 }, + { id: 'asst-coder', name: '代码助手', description: '帮助编写和解释代码', avatar: '💻', systemPrompt: '你是一个专业的代码助手,精通多种编程语言。请提供清晰、准确的代码示例和解释。', model: 'gpt-4', temperature: 0.5 }, + { id: 'asst-writer', name: '写作助手', description: '协助进行创意写作', avatar: '✍️', systemPrompt: '你是一位富有创意的写作伙伴,擅长构思情节、润色文字。', model: 'gemini-pro', temperature: 0.9 }, + ]); + const [currentAssistantId, setCurrentAssistantId] = useState('asst-default'); // 当前选中的助手 + + // 会话列表 Mock 数据 + const [sessions, setSessions] = useState([ + {id: 'session-1', title: '关于 RAG 技术的讨论', createdAt: new Date(Date.now() - 3600000)}, + {id: 'session-2', title: 'Python FastAPI 项目构思', createdAt: new Date(Date.now() - 7200000)}, + {id: 'session-3', title: '如何学习 LangChain', createdAt: new Date()}, + ]); + const [currentSessionId, setCurrentSessionId] = useState('session-3'); // 当前选中的会话 + + // --- Refs --- + const messagesEndRef = useRef(null); + + // --- Effects --- + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [messages]); + + // --- Event Handlers --- + const handleSendMessage = async (e?: React.FormEvent) => { + e?.preventDefault(); + const trimmedMessage = inputMessage.trim(); + if (!trimmedMessage || isLoading) return; + setError(null); + + const userMessage: Message = { + id: Date.now().toString(), + text: trimmedMessage, + sender: 'user', + }; + setMessages((prevMessages) => [...prevMessages, userMessage]); + setInputMessage(''); + setIsLoading(true); + + try { + // TODO: 调用后端时需要传递 currentAssistantId 和 currentSessionId + // const aiReply = await sendChatMessage(trimmedMessage, currentSessionId, currentAssistantId); + const aiReply = await sendChatMessage(trimmedMessage); // 暂时保持不变 + + const aiMessage: Message = { + id: Date.now().toString() + '_ai', + text: aiReply, + sender: 'ai', + }; + setMessages((prevMessages) => [...prevMessages, aiMessage]); + } catch (err: any) { + console.error("Failed to send message:", err); + const errorMessageText = err.message || 'An unknown error occurred.'; + setError(errorMessageText); + const errorMessage: Message = { + id: Date.now().toString() + '_err', + text: `错误: ${errorMessageText}`, + sender: 'ai', + isError: true, + }; + setMessages((prevMessages) => [...prevMessages, errorMessage]); + } finally { + setIsLoading(false); + } + }; + + const handleInputChange = (e: React.ChangeEvent) => { + setInputMessage(e.target.value); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey && !isLoading) { + e.preventDefault(); + handleSendMessage(); + } + }; + + const toggleSessionPanel = () => { + setIsSessionPanelOpen(!isSessionPanelOpen); + }; + + const handleSelectAssistant = (assistantId: string) => { + setCurrentAssistantId(assistantId); + // TODO: 可能需要清空当前消息或开始新会话 + console.log("Selected assistant:", assistantId); + // 可以在这里加载助手的初始消息或更新聊天标题 + const selectedAssistant = assistants.find(a => a.id === assistantId); + setMessages([ + { id: `init-${assistantId}`, text: `你好!我是${selectedAssistant?.name || '助手'}。${selectedAssistant?.description || ''}`, sender: 'ai' }, + ]); + setCurrentSessionId(`new-${Date.now()}`); // 切换助手时通常开始新会话 + setSessions(prev => [...prev, {id: `new-${Date.now()}`, title: `与 ${selectedAssistant?.name} 的新对话`, createdAt: new Date()}]); // 添加新会话到列表 + } + + // --- JSX Rendering --- + return ( + // 最外层 Flex 容器 +
{/* 使用 gap 添加间距 */} + + {/* 左侧助手面板 */} + + + {/* 中间主聊天区域 */} +
+ {/* 聊天窗口标题 - 显示当前助手和切换会话按钮 */} +
+
+ {assistants.find(a => a.id === currentAssistantId)?.avatar || '👤'} +

+ {assistants.find(a => a.id === currentAssistantId)?.name || '助手'} +

+ {/* TODO: 显示模型、温度等信息 */} + {/* + {assistants.find(a => a.id === currentAssistantId)?.model} + */} +
+ +
+ + {/* 消息显示区域 */} +
+ {messages.map((message) => ( +
+
+

{message.text}

+
+
+ ))} + {isLoading && ( +
+ + AI 正在思考... +
+ )} +
+
+ + {/* 消息输入区域 */} +
+
+ + +
+
+
+ + {/* 右侧会话管理面板 */} + + +
+ ); +} diff --git a/frontend/app/globals.css b/frontend/app/globals.css deleted file mode 100644 index a2dc41e..0000000 --- a/frontend/app/globals.css +++ /dev/null @@ -1,26 +0,0 @@ -@import "tailwindcss"; - -:root { - --background: #ffffff; - --foreground: #171717; -} - -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); -} - -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } -} - -body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; -} diff --git a/frontend/app/knowledge/page.tsx b/frontend/app/knowledge/page.tsx new file mode 100644 index 0000000..71142a6 --- /dev/null +++ b/frontend/app/knowledge/page.tsx @@ -0,0 +1,12 @@ +// File: frontend/app/knowledge/page.tsx +// Description: 知识库页面占位符 + +export default function KnowledgePage() { + return ( +
+

知识库管理

+

这里将用于上传文档、管理知识库。

+ {/* 后续添加文件上传、列表展示等功能 */} +
+ ); +} \ No newline at end of file diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index f7fa87e..2fe76d5 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -1,7 +1,15 @@ + +// File: frontend/app/layout.tsx +// Description: 全局根布局文件,包含 HTML 结构和侧边栏导航 + import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; -import "./globals.css"; +import Link from "next/link"; +import { BotMessageSquare, Workflow, Database, Settings, Users } from "lucide-react"; // 添加 Users 图标 +import "./../styles/globals.css"; // 确保正确引入全局样式 + +// 配置 Inter 字体 const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"], @@ -12,23 +20,73 @@ const geistMono = Geist_Mono({ subsets: ["latin"], }); +// 定义应用元数据 export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "CherryAI", + description: "AI 对话、工作流与 RAG 平台", }; +// 定义导航链接项 +const navItems = [ + { name: "对话", href: "/chat", icon: BotMessageSquare }, + { name: "助手", href: "/assistants", icon: Users }, // 将助手管理移到这里 + { name: "工作流", href: "/workflow", icon: Workflow }, + { name: "知识库", href: "/knowledge", icon: Database }, + { name: "设置", href: "/settings", icon: Settings }, // 添加设置示例 +]; + +// 根布局组件 export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return ( - - - {children} + + + {/* 侧边栏导航 */} + + + {/* 主内容区域 */} +
+ {children} {/* 这里会渲染当前路由匹配到的页面组件 */} +
); -} +} \ No newline at end of file diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 88f0cc9..cd6a75b 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -1,103 +1,13 @@ -import Image from "next/image"; +// File: frontend/app/page.tsx +// Description: 应用主页 (根路径 '/') -export default function Home() { +export default function HomePage() { return ( -
-
- Next.js logo -
    -
  1. - Get started by editing{" "} - - app/page.tsx - - . -
  2. -
  3. - Save and see your changes instantly. -
  4. -
- - -
- +
+

欢迎来到 CherryAI 🍒

+

+ 请从左侧导航选择功能开始使用。 +

); -} +} \ No newline at end of file diff --git a/frontend/app/workflow/page.tsx b/frontend/app/workflow/page.tsx new file mode 100644 index 0000000..9883fb9 --- /dev/null +++ b/frontend/app/workflow/page.tsx @@ -0,0 +1,12 @@ +// File: frontend/app/workflow/page.tsx +// Description: 工作流页面占位符 + +export default function WorkflowPage() { + return ( +
+

工作流编辑器

+

这里将集成 React Flow 来构建可视化工作流。

+ {/* 后续添加 React Flow 画布和节点面板 */} +
+ ); +} \ No newline at end of file diff --git a/frontend/lib/api.ts b/frontend/lib/api.ts new file mode 100644 index 0000000..1bca3d8 --- /dev/null +++ b/frontend/lib/api.ts @@ -0,0 +1,51 @@ +// File: frontend/lib/api.ts (新建或修改) +// Description: 用于调用后端 API 的工具函数 + +import axios from "axios"; + +// 从环境变量读取后端 API 地址,如果没有则使用默认值 +// 确保在 .env.local 文件中定义 NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1 +const API_BASE_URL = + process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000/api/v1"; + +// 创建 axios 实例,可以设置一些全局配置 +const apiClient = axios.create({ + baseURL: API_BASE_URL, + headers: { + "Content-Type": "application/json", + }, +}); + +/** + * 发送聊天消息到后端 + * @param message 用户输入的消息文本 + * @returns AI 的回复文本 + * @throws 如果 API 请求失败则抛出错误 + */ +export const sendChatMessage = async (message: string): Promise => { + try { + // 发送 POST 请求到后端的 /chat/ 端点 + const response = await apiClient.post("/chat/", { message }); + // 检查响应数据和 reply 字段是否存在 + if (response.data && response.data.reply) { + return response.data.reply; // 返回 AI 的回复 + } else { + // 如果响应格式不符合预期,抛出错误 + throw new Error("Invalid response format from server"); + } + } catch (error) { + console.error("Error calling chat API:", error); // 在控制台打印详细错误 + // 检查是否是 Axios 错误并且有响应体 + if (axios.isAxiosError(error) && error.response) { + // 尝试从响应体中获取错误详情,否则提供通用消息 + throw new Error( + error.response.data?.detail || "Failed to communicate with server" + ); + } + // 如果不是 Axios 错误或没有响应体,抛出通用错误 + throw new Error("Failed to send message. Please try again."); + } +}; + +// --- 你可以在这里添加调用其他后端 API 的函数 --- +// export const getWorkflows = async () => { ... }; diff --git a/frontend/package.json b/frontend/package.json index 5573152..4a0cbfd 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,20 +9,22 @@ "lint": "next lint" }, "dependencies": { + "axios": "^1.9.0", + "lucide-react": "^0.503.0", + "next": "15.3.1", "react": "^19.0.0", - "react-dom": "^19.0.0", - "next": "15.3.1" + "react-dom": "^19.0.0" }, "devDependencies": { - "typescript": "^5", + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", - "@tailwindcss/postcss": "^4", - "tailwindcss": "^4", "eslint": "^9", "eslint-config-next": "15.3.1", - "@eslint/eslintrc": "^3" + "tailwindcss": "^4", + "typescript": "^5" }, "packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39" } diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index b63b4af..a741831 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -8,6 +8,12 @@ importers: .: dependencies: + axios: + specifier: ^1.9.0 + version: 1.9.0 + lucide-react: + specifier: ^0.503.0 + version: 0.503.0(react@19.1.0) next: specifier: 15.3.1 version: 15.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -620,6 +626,9 @@ packages: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -628,6 +637,9 @@ packages: resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} engines: {node: '>=4'} + axios@1.9.0: + resolution: {integrity: sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==} + axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} @@ -689,6 +701,10 @@ packages: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} engines: {node: '>=12.5.0'} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -742,6 +758,10 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + detect-libc@2.0.4: resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} engines: {node: '>=8'} @@ -960,10 +980,23 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} + form-data@4.0.2: + resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} + engines: {node: '>= 6'} + function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -1287,6 +1320,11 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + lucide-react@0.503.0: + resolution: {integrity: sha512-HGGkdlPWQ0vTF8jJ5TdIqhQXZi6uh3LnNgfZ8MHiuxFfX3RZeA79r2MW2tHAZKlAVfoNE8esm3p+O6VkIvpj6w==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -1299,6 +1337,14 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -1439,6 +1485,9 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -2238,12 +2287,22 @@ snapshots: async-function@1.0.0: {} + asynckit@0.4.0: {} + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 axe-core@4.10.3: {} + axios@1.9.0: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.2 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + axobject-query@4.1.0: {} balanced-match@1.0.2: {} @@ -2311,6 +2370,10 @@ snapshots: color-string: 1.9.1 optional: true + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + concat-map@0.0.1: {} cross-spawn@7.0.6: @@ -2363,6 +2426,8 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + delayed-stream@1.0.0: {} + detect-libc@2.0.4: {} doctrine@2.1.0: @@ -2729,10 +2794,19 @@ snapshots: flatted@3.3.3: {} + follow-redirects@1.15.9: {} + for-each@0.3.5: dependencies: is-callable: 1.2.7 + form-data@4.0.2: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + mime-types: 2.1.35 + function-bind@1.1.2: {} function.prototype.name@1.1.8: @@ -3052,6 +3126,10 @@ snapshots: dependencies: js-tokens: 4.0.0 + lucide-react@0.503.0(react@19.1.0): + dependencies: + react: 19.1.0 + math-intrinsics@1.1.0: {} merge2@1.4.1: {} @@ -3061,6 +3139,12 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 @@ -3207,6 +3291,8 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + proxy-from-env@1.1.0: {} + punycode@2.3.1: {} queue-microtask@1.2.3: {} diff --git a/frontend/styles/globals.css b/frontend/styles/globals.css new file mode 100644 index 0000000..1ee522e --- /dev/null +++ b/frontend/styles/globals.css @@ -0,0 +1,4 @@ +@import "tailwindcss"; +@import "tailwindcss/preflight"; + +@tailwind utilities; \ No newline at end of file diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts new file mode 100644 index 0000000..dd51a28 --- /dev/null +++ b/frontend/tailwind.config.ts @@ -0,0 +1,25 @@ +// File: frontend/tailwind.config.ts +// Description: Tailwind CSS 配置文件,启用基于系统偏好的暗色模式 + +import type { Config } from "tailwindcss"; + +const config: Config = { + content: [ + "./pages/**/*.{js,ts,jsx,tsx,mdx}", // 如果有 pages 目录 + "./components/**/*.{js,ts,jsx,tsx,mdx}", + "./app/**/*.{js,ts,jsx,tsx,mdx}", // 主要关注 app 目录 + ], + darkMode: "media", // <--- 关键:设置为 'media' 以跟随系统设置 + theme: { + extend: { + // 你可以在这里扩展你的主题,例如添加自定义颜色、字体等 + // backgroundImage: { + // "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", + // "gradient-conic": + // "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", + // }, + }, + }, + plugins: [], +}; +export default config;