大致框架有了
This commit is contained in:
parent
be4b28d6f6
commit
fd8a4a15f5
0
backend/app/__init__.py
Normal file
0
backend/app/__init__.py
Normal file
0
backend/app/api/__init__.py
Normal file
0
backend/app/api/__init__.py
Normal file
0
backend/app/api/v1/__init__.py
Normal file
0
backend/app/api/v1/__init__.py
Normal file
15
backend/app/api/v1/api.py
Normal file
15
backend/app/api/v1/api.py
Normal file
@ -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"])
|
||||
0
backend/app/api/v1/endpoints/__init__.py
Normal file
0
backend/app/api/v1/endpoints/__init__.py
Normal file
41
backend/app/api/v1/endpoints/chat.py
Normal file
41
backend/app/api/v1/endpoints/chat.py
Normal file
@ -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))
|
||||
0
backend/app/api/v1/endpoints/rag.py
Normal file
0
backend/app/api/v1/endpoints/rag.py
Normal file
0
backend/app/api/v1/endpoints/workflow.py
Normal file
0
backend/app/api/v1/endpoints/workflow.py
Normal file
0
backend/app/core/__init__.py
Normal file
0
backend/app/core/__init__.py
Normal file
15
backend/app/core/config.py
Normal file
15
backend/app/core/config.py
Normal file
@ -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
|
||||
|
||||
# 可以在这里添加其他配置项
|
||||
0
backend/app/db/__init__.py
Normal file
0
backend/app/db/__init__.py
Normal file
0
backend/app/db/session.py
Normal file
0
backend/app/db/session.py
Normal file
32
backend/app/main.py
Normal file
32
backend/app/main.py
Normal file
@ -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 后端!"}
|
||||
0
backend/app/models/__init__.py
Normal file
0
backend/app/models/__init__.py
Normal file
0
backend/app/models/db_models.py
Normal file
0
backend/app/models/db_models.py
Normal file
16
backend/app/models/pydantic_models.py
Normal file
16
backend/app/models/pydantic_models.py
Normal file
@ -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 等
|
||||
|
||||
# --- 你可以在这里添加其他功能的模型 ---
|
||||
0
backend/app/services/__init__.py
Normal file
0
backend/app/services/__init__.py
Normal file
92
backend/app/services/chat_service.py
Normal file
92
backend/app/services/chat_service.py
Normal file
@ -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)
|
||||
0
backend/app/services/rag_service.py
Normal file
0
backend/app/services/rag_service.py
Normal file
0
backend/app/services/workflow_service.py
Normal file
0
backend/app/services/workflow_service.py
Normal file
0
backend/app/utils/__init__.py
Normal file
0
backend/app/utils/__init__.py
Normal file
8
frontend/.vscode/settings.json
vendored
Normal file
8
frontend/.vscode/settings.json
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"css.customData": [
|
||||
".vscode/tailwindcss.json"
|
||||
],
|
||||
"files.associations": {
|
||||
"*.css": "tailwindcss"
|
||||
}
|
||||
}
|
||||
291
frontend/app/chat/page.tsx
Normal file
291
frontend/app/chat/page.tsx
Normal file
@ -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<Message[]>([
|
||||
{ id: 'init-1', text: '你好!我是默认助手,有什么可以帮你的吗?', sender: 'ai' },
|
||||
]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isSessionPanelOpen, setIsSessionPanelOpen] = useState(true); // 右侧会话面板状态
|
||||
|
||||
// --- Mock Data ---
|
||||
// 助手列表 Mock 数据
|
||||
const [assistants, setAssistants] = useState<Assistant[]>([
|
||||
{ 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<string>('asst-default'); // 当前选中的助手
|
||||
|
||||
// 会话列表 Mock 数据
|
||||
const [sessions, setSessions] = useState<ChatSession[]>([
|
||||
{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<string>('session-3'); // 当前选中的会话
|
||||
|
||||
// --- Refs ---
|
||||
const messagesEndRef = useRef<null | HTMLDivElement>(null);
|
||||
|
||||
// --- Effects ---
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, [messages]);
|
||||
|
||||
// --- Event Handlers ---
|
||||
const handleSendMessage = async (e?: React.FormEvent<HTMLFormElement>) => {
|
||||
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<HTMLInputElement>) => {
|
||||
setInputMessage(e.target.value);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
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 容器
|
||||
<div className="flex h-full gap-4"> {/* 使用 gap 添加间距 */}
|
||||
|
||||
{/* 左侧助手面板 */}
|
||||
<aside className="w-64 bg-white dark:bg-gray-800 rounded-lg shadow-md p-4 flex flex-col">
|
||||
<h2 className="text-lg font-semibold mb-4 text-gray-800 dark:text-gray-200 flex items-center justify-between">
|
||||
<span>助手列表</span>
|
||||
{/* TODO: 添加助手创建/编辑入口 */}
|
||||
{/* <button className="p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700" title="管理助手">
|
||||
<Settings2 size={18} />
|
||||
</button> */}
|
||||
</h2>
|
||||
<button className="mb-4 w-full px-3 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors text-sm flex items-center justify-center gap-2">
|
||||
<UserPlus size={16} /> 添加助手
|
||||
</button>
|
||||
<div className="flex-1 overflow-y-auto space-y-2 pr-1"> {/* 添加右内边距防止滚动条遮挡 */}
|
||||
{/* 渲染助手列表 */}
|
||||
{assistants.map(assistant => (
|
||||
<div
|
||||
key={assistant.id}
|
||||
onClick={() => handleSelectAssistant(assistant.id)}
|
||||
className={`p-3 rounded-lg cursor-pointer flex items-center gap-3 ${
|
||||
currentAssistantId === assistant.id
|
||||
? 'bg-red-100 dark:bg-red-900/50 ring-2 ring-red-300 dark:ring-red-700' // 添加选中高亮和边框
|
||||
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
title={assistant.description}
|
||||
>
|
||||
<span className="text-xl">{assistant.avatar || '👤'}</span> {/* 显示头像 */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<p className={`text-sm font-medium truncate ${currentAssistantId === assistant.id ? 'text-red-800 dark:text-red-200' : 'text-gray-800 dark:text-gray-200'}`}>
|
||||
{assistant.name}
|
||||
</p>
|
||||
<p className={`text-xs truncate ${currentAssistantId === assistant.id ? 'text-red-600 dark:text-red-400' : 'text-gray-500 dark:text-gray-400'}`}>
|
||||
{assistant.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* 中间主聊天区域 */}
|
||||
<div className="flex flex-col flex-1 bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden">
|
||||
{/* 聊天窗口标题 - 显示当前助手和切换会话按钮 */}
|
||||
<div className="flex justify-between items-center p-4 border-b dark:border-gray-700">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xl">{assistants.find(a => a.id === currentAssistantId)?.avatar || '👤'}</span>
|
||||
<h1 className="text-lg font-semibold text-gray-800 dark:text-gray-200">
|
||||
{assistants.find(a => a.id === currentAssistantId)?.name || '助手'}
|
||||
</h1>
|
||||
{/* TODO: 显示模型、温度等信息 */}
|
||||
{/* <span className="text-xs bg-gray-200 dark:bg-gray-700 px-1.5 py-0.5 rounded">
|
||||
{assistants.find(a => a.id === currentAssistantId)?.model}
|
||||
</span> */}
|
||||
</div>
|
||||
<button
|
||||
onClick={toggleSessionPanel}
|
||||
className="p-1 rounded text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700"
|
||||
title={isSessionPanelOpen ? "关闭会话面板" : "打开会话面板"}
|
||||
>
|
||||
{isSessionPanelOpen ? <PanelRightClose size={20} /> : <PanelRightOpen size={20} />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 消息显示区域 */}
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||
{messages.map((message) => (
|
||||
<div
|
||||
key={message.id}
|
||||
className={`flex ${
|
||||
message.sender === 'user' ? 'justify-end' : 'justify-start'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`max-w-xs md:max-w-md lg:max-w-lg px-4 py-2 rounded-lg shadow ${
|
||||
message.sender === 'user'
|
||||
? 'bg-red-500 text-white'
|
||||
: message.isError
|
||||
? 'bg-red-100 dark:bg-red-900/50 text-red-700 dark:text-red-300'
|
||||
: 'bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200'
|
||||
}`}
|
||||
>
|
||||
<p className="text-sm whitespace-pre-wrap">{message.text}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{isLoading && (
|
||||
<div className="flex justify-center items-center p-2">
|
||||
<Loader2 className="h-5 w-5 animate-spin text-gray-500 dark:text-gray-400" />
|
||||
<span className="ml-2 text-sm text-gray-500 dark:text-gray-400">AI 正在思考...</span>
|
||||
</div>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* 消息输入区域 */}
|
||||
<div className="p-4 border-t dark:border-gray-700">
|
||||
<form onSubmit={handleSendMessage} className="flex items-center space-x-2">
|
||||
<input
|
||||
type="text"
|
||||
value={inputMessage}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={isLoading ? "AI 正在回复..." : "输入你的消息..."}
|
||||
disabled={isLoading}
|
||||
className="flex-1 px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-200 dark:focus:ring-red-600 disabled:opacity-70 transition-opacity"
|
||||
aria-label="聊天输入框"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!inputMessage.trim() || isLoading}
|
||||
className="p-2 rounded-lg bg-red-500 text-white hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 disabled:opacity-50 disabled:cursor-not-allowed transition-all flex items-center justify-center h-10 w-10"
|
||||
aria-label={isLoading ? "正在发送" : "发送消息"}
|
||||
>
|
||||
{isLoading ? <Loader2 className="h-5 w-5 animate-spin" /> : <SendHorizontal size={20} />}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧会话管理面板 */}
|
||||
<aside className={`bg-white dark:bg-gray-800 rounded-lg shadow-md p-4 flex flex-col transition-all duration-300 ease-in-out ${isSessionPanelOpen ? 'w-64' : 'w-0 p-0 border-0 overflow-hidden opacity-0'}`}> {/* 调整关闭时的样式 */}
|
||||
<h2 className="text-lg font-semibold mb-4 text-gray-800 dark:text-gray-200 whitespace-nowrap">话题列表</h2> {/* 改为话题 */}
|
||||
<button className="mb-4 w-full px-3 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors text-sm whitespace-nowrap flex items-center justify-center gap-2">
|
||||
+ 新建话题
|
||||
</button>
|
||||
<div className="flex-1 overflow-y-auto space-y-2 pr-1">
|
||||
{sessions.map(session => (
|
||||
<div
|
||||
key={session.id}
|
||||
onClick={() => setCurrentSessionId(session.id)}
|
||||
className={`p-2 rounded-lg cursor-pointer text-sm truncate whitespace-nowrap ${
|
||||
currentSessionId === session.id
|
||||
? 'bg-red-100 dark:bg-red-900/50 text-red-700 dark:text-red-400 font-medium'
|
||||
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
title={session.title}
|
||||
>
|
||||
{session.title}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
12
frontend/app/knowledge/page.tsx
Normal file
12
frontend/app/knowledge/page.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
// File: frontend/app/knowledge/page.tsx
|
||||
// Description: 知识库页面占位符
|
||||
|
||||
export default function KnowledgePage() {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold mb-4 dark:text-gray-200">知识库管理</h1>
|
||||
<p className="text-gray-700 dark:text-gray-400">这里将用于上传文档、管理知识库。</p>
|
||||
{/* 后续添加文件上传、列表展示等功能 */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -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 (
|
||||
<html lang="en">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
{children}
|
||||
<html lang="zh-CN">
|
||||
<body className={`${geistSans.variable} ${geistMono.variable} antialiased flex h-screen bg-gray-100 dark:bg-gray-900`}>
|
||||
{/* 侧边栏导航 */}
|
||||
<aside className="w-16 bg-white dark:bg-gray-800 p-3 shadow-md flex flex-col items-center"> {/* 调整内边距和对齐 */}
|
||||
{/* Logo */}
|
||||
<div className="mb-6"> {/* 调整 Logo 边距 */}
|
||||
<Link href="/" className="flex items-center justify-center text-3xl font-bold text-red-600 dark:text-red-500" title="CherryAI 主页">
|
||||
<span>🍒</span> {/* 只显示图标 */}
|
||||
</Link>
|
||||
</div>
|
||||
{/* 导航链接 */}
|
||||
<nav className="flex-grow w-full">
|
||||
<ul className="flex flex-col items-center space-y-2"> {/* 调整列表项间距 */}
|
||||
{navItems.map((item) => (
|
||||
<li key={item.name} className="w-full">
|
||||
<Link
|
||||
href={item.href}
|
||||
className="relative flex items-center justify-center p-2 rounded-lg text-gray-600 dark:text-gray-400 hover:bg-red-100 dark:hover:bg-red-900/50 hover:text-red-700 dark:hover:text-red-400 transition-colors duration-200 group" // 居中图标
|
||||
title={item.name} // 保留原生 title
|
||||
>
|
||||
<item.icon className="h-6 w-6 flex-shrink-0" />
|
||||
{/* Tooltip 文字标签 */}
|
||||
<span
|
||||
className="absolute left-full top-1/2 -translate-y-1/2 ml-3 px-2 py-1 bg-gray-900 dark:bg-gray-700 text-white text-xs rounded shadow-lg opacity-0 group-hover:opacity-100 transition-opacity duration-200 delay-150 whitespace-nowrap pointer-events-none" // 使用 pointer-events-none 避免干扰悬浮
|
||||
>
|
||||
{item.name}
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
{/* 侧边栏底部 (可选,放用户头像或设置入口) */}
|
||||
<div className="mt-auto">
|
||||
<button className="p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700" title="用户设置">
|
||||
<Settings className="h-6 w-6 text-gray-600 dark:text-gray-400" />
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* 主内容区域 */}
|
||||
<main className="flex-1 overflow-y-auto p-6 bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100">
|
||||
{children} {/* 这里会渲染当前路由匹配到的页面组件 */}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,103 +1,13 @@
|
||||
import Image from "next/image";
|
||||
// File: frontend/app/page.tsx
|
||||
// Description: 应用主页 (根路径 '/')
|
||||
|
||||
export default function Home() {
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
|
||||
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={180}
|
||||
height={38}
|
||||
priority
|
||||
/>
|
||||
<ol className="list-inside list-decimal text-sm/6 text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
|
||||
<li className="mb-2 tracking-[-.01em]">
|
||||
Get started by editing{" "}
|
||||
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-[family-name:var(--font-geist-mono)] font-semibold">
|
||||
app/page.tsx
|
||||
</code>
|
||||
.
|
||||
</li>
|
||||
<li className="tracking-[-.01em]">
|
||||
Save and see your changes instantly.
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
||||
<a
|
||||
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
Deploy now
|
||||
</a>
|
||||
<a
|
||||
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Read our docs
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/file.svg"
|
||||
alt="File icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Learn
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/window.svg"
|
||||
alt="Window icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Examples
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/globe.svg"
|
||||
alt="Globe icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Go to nextjs.org →
|
||||
</a>
|
||||
</footer>
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<h1 className="text-4xl font-bold mb-4 text-gray-800 dark:text-gray-200">欢迎来到 CherryAI 🍒</h1>
|
||||
<p className="text-lg text-gray-600 dark:text-gray-400">
|
||||
请从左侧导航选择功能开始使用。
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
12
frontend/app/workflow/page.tsx
Normal file
12
frontend/app/workflow/page.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
// File: frontend/app/workflow/page.tsx
|
||||
// Description: 工作流页面占位符
|
||||
|
||||
export default function WorkflowPage() {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold mb-4 dark:text-gray-200">工作流编辑器</h1>
|
||||
<p className="text-gray-700 dark:text-gray-400">这里将集成 React Flow 来构建可视化工作流。</p>
|
||||
{/* 后续添加 React Flow 画布和节点面板 */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
51
frontend/lib/api.ts
Normal file
51
frontend/lib/api.ts
Normal file
@ -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<string> => {
|
||||
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 () => { ... };
|
||||
@ -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"
|
||||
}
|
||||
|
||||
86
frontend/pnpm-lock.yaml
generated
86
frontend/pnpm-lock.yaml
generated
@ -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: {}
|
||||
|
||||
4
frontend/styles/globals.css
Normal file
4
frontend/styles/globals.css
Normal file
@ -0,0 +1,4 @@
|
||||
@import "tailwindcss";
|
||||
@import "tailwindcss/preflight";
|
||||
|
||||
@tailwind utilities;
|
||||
25
frontend/tailwind.config.ts
Normal file
25
frontend/tailwind.config.ts
Normal file
@ -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;
|
||||
Loading…
x
Reference in New Issue
Block a user