userName 1 天之前
父节点
当前提交
50fa65f28f

+ 131 - 27
backend/api/chat_history.py

@@ -3,9 +3,10 @@
 """
 from fastapi import APIRouter, Depends, HTTPException, status
 from sqlalchemy.orm import Session
-from pydantic import BaseModel
+from pydantic import BaseModel, validator
 from typing import List, Optional
 from datetime import datetime
+from enum import Enum
 
 from backend.core.database import get_db
 from backend.core.models import User, ChatHistory, ChatMessage
@@ -13,10 +14,41 @@ from backend.core.auth import get_current_user
 
 router = APIRouter(tags=["chat"], prefix="/chat-history")
 
+# 添加角色枚举类型
+class MessageRole(str, Enum):
+    USER = "user"
+    ASSISTANT = "assistant" 
+    SYSTEM = "system"
+
 class MessageCreate(BaseModel):
-    role: str
+    role: MessageRole  # 使用枚举类型替代字符串
     content: str
     
+    # 添加额外验证器
+    @validator('content')
+    def content_must_not_be_empty(cls, v):
+        if not v or not v.strip():
+            raise ValueError('消息内容不能为空')
+        return v
+        
+    # 添加角色验证器,尝试将字符串转换为枚举
+    @validator('role', pre=True)
+    def validate_role(cls, v):
+        if isinstance(v, MessageRole):
+            return v
+            
+        if isinstance(v, str):
+            # 尝试将字符串匹配到枚举值
+            if v.lower() == "user":
+                return MessageRole.USER
+            elif v.lower() == "assistant":
+                return MessageRole.ASSISTANT
+            elif v.lower() == "system":
+                return MessageRole.SYSTEM
+        
+        # 如果无法匹配,默认返回USER
+        return MessageRole.USER
+
 class MessageResponse(BaseModel):
     id: int
     role: str
@@ -49,6 +81,9 @@ class ChatHistorySummary(BaseModel):
     class Config:
         orm_mode = True
 
+class ChatHistoryUpdate(BaseModel):
+    title: Optional[str] = None
+
 @router.post("/", response_model=ChatHistoryResponse)
 async def create_chat_history(
     chat_data: ChatHistoryCreate,
@@ -56,28 +91,36 @@ async def create_chat_history(
     db: Session = Depends(get_db)
 ):
     """创建新的聊天历史"""
-    # 创建聊天历史
-    chat = ChatHistory(
-        user_id=current_user.id,
-        title=chat_data.title
-    )
-    db.add(chat)
-    db.commit()
-    db.refresh(chat)
-    
-    # 添加消息
-    for msg_data in chat_data.messages:
-        message = ChatMessage(
-            chat_id=chat.id,
-            role=msg_data.role,
-            content=msg_data.content
+    try:
+        # 打印接收到的数据
+        print("收到的聊天历史数据:", chat_data)
+        
+        # 创建聊天历史
+        chat = ChatHistory(
+            user_id=current_user.id,
+            title=chat_data.title
         )
-        db.add(message)
-    
-    db.commit()
-    db.refresh(chat)
-    
-    return chat
+        db.add(chat)
+        db.commit()
+        db.refresh(chat)
+        
+        # 添加消息
+        for msg_data in chat_data.messages:
+            message = ChatMessage(
+                chat_id=chat.id,
+                role=msg_data.role,
+                content=msg_data.content
+            )
+            db.add(message)
+        
+        db.commit()
+        db.refresh(chat)
+        
+        return chat
+    except Exception as e:
+        # 打印详细错误信息
+        print("保存聊天历史出错:", str(e))
+        raise
 
 @router.get("/", response_model=List[ChatHistorySummary])
 async def get_chat_histories(
@@ -85,10 +128,43 @@ async def get_chat_histories(
     db: Session = Depends(get_db)
 ):
     """获取用户的所有聊天历史摘要"""
-    chats = db.query(ChatHistory)\
+    # 获取基本聊天历史记录
+    chats_query = db.query(ChatHistory)\
         .filter(ChatHistory.user_id == current_user.id)\
-        .order_by(ChatHistory.updated_at.desc())\
-        .all()
+        .order_by(ChatHistory.updated_at.desc())
+    
+    chats = chats_query.all()
+    
+    # 增强聊天记录信息
+    for chat in chats:
+        # 获取消息数量
+        message_count = db.query(ChatMessage)\
+            .filter(ChatMessage.chat_id == chat.id)\
+            .count()
+        chat.message_count = message_count
+        
+        # 获取最新的用户消息作为预览
+        last_user_message = db.query(ChatMessage)\
+            .filter(ChatMessage.chat_id == chat.id, ChatMessage.role == "user")\
+            .order_by(ChatMessage.timestamp.desc())\
+            .first()
+        
+        if last_user_message:
+            # 截取前100个字符作为预览
+            preview = last_user_message.content[:100]
+            if len(last_user_message.content) > 100:
+                preview += "..."
+            chat.preview = preview
+        else:
+            chat.preview = ""
+        
+        # 检查是否包含代码
+        has_code = db.query(ChatMessage)\
+            .filter(
+                ChatMessage.chat_id == chat.id,
+                ChatMessage.content.like("%```%")
+            ).count() > 0
+        chat.has_code = has_code
     
     return chats
 
@@ -170,4 +246,32 @@ async def delete_chat_history(
     db.delete(chat)
     db.commit()
     
-    return {"message": "聊天历史已删除"} 
+    return {"message": "聊天历史已删除"}
+
+@router.put("/{chat_id}", response_model=ChatHistoryResponse)
+async def update_chat_history(
+    chat_id: int,
+    update_data: ChatHistoryUpdate,
+    current_user: User = Depends(get_current_user),
+    db: Session = Depends(get_db)
+):
+    """更新聊天历史的标题"""
+    chat = db.query(ChatHistory)\
+        .filter(ChatHistory.id == chat_id, ChatHistory.user_id == current_user.id)\
+        .first()
+    
+    if not chat:
+        raise HTTPException(
+            status_code=status.HTTP_404_NOT_FOUND,
+            detail="聊天历史不存在或无权访问"
+        )
+    
+    # 更新标题
+    if update_data.title:
+        chat.title = update_data.title
+    
+    chat.updated_at = datetime.utcnow()
+    db.commit()
+    db.refresh(chat)
+    
+    return chat 

+ 12 - 0
backend/api/research_history.py

@@ -75,6 +75,18 @@ async def get_research_histories(
         .order_by(ResearchHistory.updated_at.desc())\
         .all()
     
+    # 确保每个研究历史都有基本字段
+    for research in researches:
+        if not hasattr(research, 'keywords') or research.keywords is None:
+            research.keywords = []
+            
+        # 截取研究意图作为摘要
+        if research.research_intent and len(research.research_intent) > 150:
+            research.research_intent = research.research_intent[:150] + "..."
+            
+        # 添加研究相关的论文数量
+        research.paper_count = len(research.papers) if research.papers else 0
+    
     return researches
 
 @router.get("/{research_id}", response_model=ResearchHistoryResponse)

+ 6 - 1
backend/core/models.py

@@ -78,4 +78,9 @@ class ResearchHistory(Base):
     papers = Column(JSON, default=list)
     
     # 关系
-    user = relationship("User", back_populates="researches") 
+    user = relationship("User", back_populates="researches")
+    
+    # 注释掉以下新增字段
+    # research_type = Column(String(50), default="general")
+    # favorited_papers = Column(JSON, default=list)
+    # notes = Column(Text, nullable=True) 

+ 299 - 75
lightstar-web/src/app/ai-chat/page.tsx

@@ -3,6 +3,10 @@
 import { GlassCard } from "@/components/ui/glass-card";
 import { Button } from "@/components/ui/button";
 import { useState, useRef, useEffect } from "react";
+import axios from "axios";
+
+// 在文件顶部导入区域之后,组件定义之前添加
+const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000/api";
 
 // 消息类型定义
 interface Message {
@@ -12,6 +16,20 @@ interface Message {
   timestamp: Date;
 }
 
+// 在文件顶部添加这个接口
+interface ApiMessage {
+  id: number;
+  role: string;
+  content: string;
+  timestamp: string;
+}
+
+// 添加一个接口来定义来自API的消息格式
+interface ApiResponseMessage {
+  role: string;
+  content: string;
+}
+
 // 系统预设消息 - 定义AI助手的角色和功能
 const SYSTEM_PROMPT = `你是"文献启明星"科研助手系统中的AI顾问,专为科研人员、学者和学生设计。
 你的主要职责包括:
@@ -56,13 +74,13 @@ export default function AIChatPage() {
   // 状态管理
   const [messages, setMessages] = useState<Message[]>([
     {
-      id: "system-message",
+      id: `system-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
       content: SYSTEM_PROMPT,
       role: "system",
       timestamp: new Date(),
     },
     {
-      id: "welcome-message",
+      id: `welcome-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
       content: "您好!我是文献启明星的AI研究助手。作为您的科研伙伴,我可以帮助您解答学术问题、分析研究方向、提供文献建议,以及辅助论文写作。请问今天有什么我可以协助您的研究工作吗?",
       role: "assistant",
       timestamp: new Date(),
@@ -76,6 +94,11 @@ export default function AIChatPage() {
   const messagesEndRef = useRef<HTMLDivElement>(null);
   const chatContainerRef = useRef<HTMLDivElement>(null);
   
+  // 在组件的状态变量之后添加一个保存聊天的状态
+  const [chatId, setChatId] = useState<number | null>(null);
+  const [chatTitle, setChatTitle] = useState("新对话");
+  const [isSaving, setIsSaving] = useState(false);
+  
   // 过滤系统消息,只显示用户和助手的消息
   useEffect(() => {
     setVisibleMessages(messages.filter(msg => msg.role !== "system"));
@@ -88,101 +111,293 @@ export default function AIChatPage() {
     }
   }, [visibleMessages]);
   
+  // 添加从URL获取聊天ID的逻辑
+  useEffect(() => {
+    const urlParams = new URLSearchParams(window.location.search);
+    const id = urlParams.get('id');
+    if (id) {
+      setChatId(parseInt(id));
+      // 加载已存在的聊天
+      loadExistingChat(parseInt(id));
+    }
+  }, []);
+  
+  // 添加加载现有聊天的函数
+  const loadExistingChat = async (id: number) => {
+    try {
+      const token = localStorage.getItem("token");
+      if (!token) return;
+      
+      const response = await axios.get(`${API_BASE_URL}/chat-history/${id}`, {
+        headers: {
+          Authorization: `Bearer ${token}`
+        }
+      });
+      
+      if (response.data) {
+        const chatData = response.data;
+        setChatTitle(chatData.title);
+        
+        // 设置消息,保留系统消息
+        const systemMessage = messages.find(m => m.role === 'system');
+        const loadedMessages = chatData.messages.map((msg: ApiMessage) => ({
+          id: `loaded-${msg.id}-${Math.random().toString(36).substring(2, 9)}`,
+          content: msg.content,
+          role: msg.role,
+          timestamp: new Date(msg.timestamp)
+        }));
+        
+        if (systemMessage) {
+          setMessages([systemMessage, ...loadedMessages]);
+        } else {
+          setMessages(loadedMessages);
+        }
+      }
+    } catch (err: any) {
+      console.error("加载聊天历史失败:", err);
+      setError("无法加载聊天历史,可能已被删除或您无权访问");
+    }
+  };
+  
   // 发送消息
   const sendMessage = async () => {
-    if (!inputText.trim() || isLoading) return;
-    
-    // 清除之前的错误
-    setError(null);
+    if (!inputText.trim()) return;
     
-    // 创建用户消息
-    const userMessage: Message = {
-      id: Date.now().toString(),
-      content: inputText,
-      role: "user",
+    const uniqueUserId = `user-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
+
+    const userMessage = {
+      id: uniqueUserId,  // 使用时间戳+随机字符串确保唯一性
+      content: inputText.trim(),
+      role: "user" as const,
       timestamp: new Date(),
     };
     
-    // 更新消息列表
-    setMessages(prev => [...prev, userMessage]);
+    setMessages(prev => {
+      // 检查是否已经存在相同ID的消息
+      if (prev.some(msg => msg.id === userMessage.id)) {
+        return prev;
+      }
+      return [...prev, userMessage];
+    });
     setInputText("");
     setIsLoading(true);
-    
-    // 添加超时控制
-    const timeoutPromise = new Promise((_, reject) => {
-      const now = new Date();
-      const timeString = now.toLocaleTimeString(); // 获取当前时间
-      setTimeout(() => 
-        reject(new Error(`请求超时,请检查网络连接 [${timeString}]`)), 
-        30000); // 30秒超时
-    });
+    setError(null);
     
     try {
-      // 准备发送到API的消息历史(包括系统消息)
-      const apiMessages = messages
-        .concat(userMessage)
-        .map(msg => ({
-          role: msg.role,
-          content: msg.content
-        }));
+      // 添加请求详细日志
+      console.log("发送到AI服务的消息:", {
+        messages: messages.filter(m => m.role !== 'system').concat(userMessage)
+          .map(m => ({role: m.role, content: m.content})),
+        systemMessage: messages.find(m => m.role === 'system')?.content
+      });
       
-      // 使用Promise.race添加超时控制
-      const response = await Promise.race([
-        fetchWithRetry("/api/chat", {
-          method: "POST",
-          headers: { "Content-Type": "application/json" },
-          body: JSON.stringify({ messages: apiMessages })
-        }),
-        timeoutPromise
-      ]) as Response;
+      const response = await axios.post(`${API_BASE_URL}/chat`, {
+        messages: [
+          ...messages.filter(m => m.role !== 'system'),
+          userMessage
+        ].map(m => ({
+          role: m.role,
+          content: m.content
+        })),
+        systemMessage: messages.find(m => m.role === 'system')?.content,
+      });
       
-      if (!response.ok) {
-        let errorMessage = "服务器错误";
-        try {
-          const errorData = await response.json();
-          errorMessage = errorData.error || errorMessage;
-        } catch (e) {
-          // 解析JSON失败,使用默认错误消息
+      console.log("AI服务原始响应:", response.data);
+      
+      // 增强的响应格式检查和错误处理
+      if (!response.data) {
+        throw new Error("AI返回了空响应");
+      }
+      
+      // 尝试处理不同的响应格式
+      let aiContent = "";
+      
+      // 检查响应中的messages数组
+      if (response.data.messages && Array.isArray(response.data.messages)) {
+        // 找到最后一个assistant消息 - 添加明确的类型
+        const assistantMessage = response.data.messages
+          .filter((msg: ApiResponseMessage) => msg.role === 'assistant')
+          .pop();
+          
+        if (assistantMessage && assistantMessage.content) {
+          aiContent = assistantMessage.content;
         }
-        throw new Error(errorMessage);
+      } 
+      // 如果没有找到messages数组或assistant消息,尝试其他格式
+      else if (typeof response.data === 'string') {
+        aiContent = response.data;
+      } else if (response.data.content) {
+        aiContent = response.data.content;
+      } else if (response.data.message) {
+        aiContent = response.data.message;
+      } else if (response.data.text) {
+        aiContent = response.data.text;
+      } else if (response.data.choices && Array.isArray(response.data.choices) && response.data.choices.length > 0) {
+        // OpenAI风格格式
+        const choice = response.data.choices[0];
+        aiContent = choice.message?.content || choice.text || "";
       }
       
-      const data = await response.json();
+      if (!aiContent) {
+        console.error("无法从响应中提取AI回复:", response.data);
+        throw new Error("AI返回的响应格式不正确或内容为空");
+      }
       
-      // 创建AI回复消息
-      const aiResponse: Message = {
-        id: (Date.now() + 1).toString(),
-        content: data.message?.content || 
-                 (data.messages && data.messages.length > 0 ? 
-                   data.messages[data.messages.length - 1].content : 
-                   "抱歉,接收到了空响应"),
-        role: "assistant",
+      const uniqueId = `assistant-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
+      
+      const assistantMessage = {
+        id: uniqueId,  // 使用时间戳+随机字符串确保唯一性
+        content: aiContent,
+        role: "assistant" as const,
         timestamp: new Date(),
       };
       
-      setMessages(prev => [...prev, aiResponse]);
-    } catch (err) {
-      console.error("聊天请求失败:", err);
+      setMessages(prev => {
+        // 检查是否已经存在相同ID的消息
+        if (prev.some(msg => msg.id === assistantMessage.id)) {
+          return prev;
+        }
+        return [...prev, assistantMessage];
+      });
       
-      // 获取具体的错误信息
-      const errorMsg = err instanceof Error 
-        ? err.message 
-        : "与AI服务通信失败,请检查网络连接或后端服务是否正常运行";
+      // 保存聊天历史
+      await saveChatToHistory(userMessage, assistantMessage);
       
-      // 设置错误状态
-      setError(errorMsg);
+    } catch (err: any) {
+      console.error("AI聊天请求失败:", err);
+      if (err.response) {
+        console.error("服务器响应:", err.response.status, err.response.data);
+      }
+      setError(`与AI服务通信失败: ${err.message || "未知错误"}`);
+      // 即使API失败,也将用户消息添加到聊天记录
+      setMessages(prev => {
+        // 检查是否已经存在相同ID的消息
+        if (prev.some(msg => msg.id === userMessage.id)) {
+          return prev;
+        }
+        return [...prev, userMessage];
+      });
+    } finally {
+      setIsLoading(false);
+    }
+  };
+  
+  // 修改 saveChatToHistory 函数
+  const saveChatToHistory = async (userMsg: Message, assistantMsg: Message) => {
+    try {
+      const token = localStorage.getItem("token");
+      if (!token) return; // 未登录不保存
       
-      // 添加用户友好的错误消息
-      const errorMessage: Message = {
-        id: (Date.now() + 1).toString(),
-        content: `很抱歉,发生了错误:${errorMsg}。请稍后再试,或者联系管理员检查后端服务是否正常运行。`,
-        role: "assistant",
-        timestamp: new Date(),
+      // 确保角色名称符合后端期望的格式
+      const formatRole = (role: string): string => {
+        // 确保角色只能是这三种值之一
+        if (role === "user" || role === "assistant" || role === "system") {
+          return role;
+        }
+        // 默认返回 "user"
+        return "user";
       };
       
-      setMessages(prev => [...prev, errorMessage]);
-    } finally {
-      setIsLoading(false);
+      // 安全地获取内容,防止undefined错误
+      const getSafeContent = (msg: Message | undefined): string => {
+        if (!msg || typeof msg.content !== 'string') {
+          return "";
+        }
+        return msg.content;
+      };
+      
+      // 如果是新对话,先创建聊天历史
+      if (!chatId) {
+        // 使用用户消息作为标题,添加安全检查
+        const title = userMsg && userMsg.content ? 
+          (userMsg.content.length > 30 ? userMsg.content.substring(0, 30) + "..." : userMsg.content) :
+          "新对话";
+        
+        const simplifiedMessages = [
+          {
+            role: formatRole(userMsg?.role || "user"),
+            content: getSafeContent(userMsg)
+          }
+        ];
+        
+        // 只有当assistantMsg存在时才添加到消息列表中
+        if (assistantMsg && assistantMsg.content) {
+          simplifiedMessages.push({
+            role: formatRole(assistantMsg.role || "assistant"),
+            content: getSafeContent(assistantMsg)
+          });
+        }
+        
+        // 创建新聊天历史
+        const response = await axios.post(
+          `${API_BASE_URL}/chat-history/`, 
+          {
+            title: title,
+            messages: simplifiedMessages
+          },
+          {
+            headers: { Authorization: `Bearer ${token}` }
+          }
+        );
+        
+        if (response.data && response.data.id) {
+          setChatId(response.data.id);
+          setChatTitle(title);
+          
+          // 更新URL
+          const url = new URL(window.location.href);
+          url.searchParams.set('id', response.data.id.toString());
+          window.history.pushState({}, '', url);
+        }
+      } else {
+        // 已有聊天历史,添加新消息
+        if (userMsg && userMsg.content) {
+          try {
+            // 添加用户消息
+            await axios.post(
+              `${API_BASE_URL}/chat-history/${chatId}/messages`, 
+              {
+                role: formatRole(userMsg.role),
+                content: getSafeContent(userMsg)
+              },
+              {
+                headers: { Authorization: `Bearer ${token}` }
+              }
+            );
+          } catch (msgErr: any) {
+            console.error("添加用户消息失败:", msgErr);
+            if (msgErr.response) {
+              console.error("错误详情:", msgErr.response.status, msgErr.response.data);
+            }
+          }
+        }
+        
+        if (assistantMsg && assistantMsg.content) {
+          try {
+            // 添加AI助手消息
+            await axios.post(
+              `${API_BASE_URL}/chat-history/${chatId}/messages`, 
+              {
+                role: formatRole(assistantMsg.role),
+                content: getSafeContent(assistantMsg)
+              },
+              {
+                headers: { Authorization: `Bearer ${token}` }
+              }
+            );
+          } catch (msgErr: any) {
+            console.error("添加AI助手消息失败:", msgErr);
+            if (msgErr.response) {
+              console.error("错误详情:", msgErr.response.status, msgErr.response.data);
+            }
+          }
+        }
+      }
+    } catch (err: any) {
+      console.error("保存聊天历史失败:", err);
+      if (err.response) {
+        console.error("错误详情:", err.response.data);
+      }
     }
   };
   
@@ -198,18 +413,27 @@ export default function AIChatPage() {
   const clearChat = () => {
     setMessages([
       {
-        id: "system-message",
+        id: `system-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
         content: SYSTEM_PROMPT,
         role: "system",
         timestamp: new Date(),
       },
       {
-        id: "welcome-message",
+        id: `welcome-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
         content: "聊天已重置。我是文献启明星的AI研究助手,请问有什么我可以帮助您的?",
         role: "assistant",
         timestamp: new Date(),
       }
     ]);
+    
+    // 重置聊天ID和标题
+    setChatId(null);
+    setChatTitle("新对话");
+    
+    // 移除URL中的id参数
+    const url = new URL(window.location.href);
+    url.searchParams.delete('id');
+    window.history.pushState({}, '', url);
   };
   
   return (

+ 520 - 25
lightstar-web/src/app/profile/page.tsx

@@ -2,7 +2,7 @@
 
 import { GlassCard } from "@/components/ui/glass-card";
 import { Button } from "@/components/ui/button";
-import { useState, useEffect } from "react";
+import { useState, useEffect, useMemo } from "react";
 import Link from "next/link";
 import { useSearchParams } from 'next/navigation';
 import axios, { AxiosError } from "axios";
@@ -49,6 +49,27 @@ interface UserData {
   position: string;
 }
 
+interface ChatHistoryItem {
+  id: number;
+  title: string;
+  created_at: string;
+  updated_at: string;
+  preview?: string;
+  message_count?: number;
+  has_code?: boolean;
+}
+
+interface ResearchHistoryItem {
+  id: number;
+  title: string;
+  created_at: string;
+  updated_at: string;
+  research_intent?: string;
+  keywords?: string[];
+  directions?: any[];
+  papers?: any[];
+}
+
 // API基础URL
 const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000/api";
 
@@ -85,13 +106,27 @@ export default function ProfilePage() {
   const [editedUser, setEditedUser] = useState<UserData>({...userData});
   const [isLoading, setIsLoading] = useState(true);
   const [error, setError] = useState("");
-  const [chatHistory, setChatHistory] = useState([]);
-  const [researchHistory, setResearchHistory] = useState([]);
+  const [chatHistory, setChatHistory] = useState<ChatHistoryItem[]>([]);
+  const [researchHistory, setResearchHistory] = useState<ResearchHistoryItem[]>([]);
   const [isLoggedIn, setIsLoggedIn] = useState(false); // 默认为未登录
   const [loginUsername, setLoginUsername] = useState('');
   const [loginPassword, setLoginPassword] = useState('');
   const [loginError, setLoginError] = useState('');
   
+  // 对话历史筛选相关
+  const [isFilterChatOpen, setIsFilterChatOpen] = useState(false);
+  const [chatSearchTerm, setChatSearchTerm] = useState('');
+  const [chatDateFilter, setChatDateFilter] = useState('all');
+  const [chatPage, setChatPage] = useState(0);
+  const chatPageSize = 5;
+
+  // 研究历史筛选相关
+  const [isFilterResearchOpen, setIsFilterResearchOpen] = useState(false);
+  const [researchSearchTerm, setResearchSearchTerm] = useState('');
+  const [researchDateFilter, setResearchDateFilter] = useState('all');
+  const [researchPage, setResearchPage] = useState(0);
+  const researchPageSize = 5;
+  
   // 每当URL参数变化时更新激活的标签页
   useEffect(() => {
     if (tabParam && tabs.some(tab => tab.id === tabParam)) {
@@ -208,12 +243,13 @@ export default function ProfilePage() {
         const token = localStorage.getItem("token");
         if (!token) return;
         
-        const response = await axios.get(`${API_BASE_URL}/chat-history`, {
+        const response = await axios.get(`${API_BASE_URL}/chat-history/`, {
           headers: {
             Authorization: `Bearer ${token}`
           }
         });
         
+        console.log("聊天历史数据:", response.data);
         setChatHistory(response.data || []);
       } catch (err) {
         console.error("获取聊天历史失败:", err);
@@ -372,49 +408,362 @@ export default function ProfilePage() {
     alert("设置保存成功");
   };
   
+  // 对话历史筛选计算逻辑
+  const filteredChatHistory = useMemo(() => {
+    let result = [...chatHistory];
+    
+    // 搜索筛选
+    if (chatSearchTerm.trim() !== '') {
+      result = result.filter(chat => 
+        chat.title.toLowerCase().includes(chatSearchTerm.toLowerCase())
+      );
+    }
+    
+    // 日期筛选
+    const now = new Date();
+    if (chatDateFilter === 'today') {
+      result = result.filter(chat => {
+        const chatDate = new Date(chat.created_at);
+        return chatDate.toDateString() === now.toDateString();
+      });
+    } else if (chatDateFilter === 'week') {
+      const weekStart = new Date(now);
+      weekStart.setDate(now.getDate() - now.getDay());
+      result = result.filter(chat => {
+        const chatDate = new Date(chat.created_at);
+        return chatDate >= weekStart;
+      });
+    } else if (chatDateFilter === 'month') {
+      const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
+      result = result.filter(chat => {
+        const chatDate = new Date(chat.created_at);
+        return chatDate >= monthStart;
+      });
+    }
+    
+    // 分页
+    return result.slice(chatPage * chatPageSize, (chatPage + 1) * chatPageSize);
+  }, [chatHistory, chatSearchTerm, chatDateFilter, chatPage]);
+
+  // 研究历史筛选计算逻辑
+  const filteredResearchHistory = useMemo(() => {
+    let result = [...researchHistory];
+    
+    // 搜索筛选
+    if (researchSearchTerm.trim() !== '') {
+      result = result.filter(research => 
+        research.title.toLowerCase().includes(researchSearchTerm.toLowerCase())
+      );
+    }
+    
+    // 日期筛选
+    const now = new Date();
+    if (researchDateFilter === 'today') {
+      result = result.filter(research => {
+        const researchDate = new Date(research.created_at);
+        return researchDate.toDateString() === now.toDateString();
+      });
+    } else if (researchDateFilter === 'week') {
+      const weekStart = new Date(now);
+      weekStart.setDate(now.getDate() - now.getDay());
+      result = result.filter(research => {
+        const researchDate = new Date(research.created_at);
+        return researchDate >= weekStart;
+      });
+    } else if (researchDateFilter === 'month') {
+      const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
+      result = result.filter(research => {
+        const researchDate = new Date(research.created_at);
+        return researchDate >= monthStart;
+      });
+    }
+    
+    // 分页
+    return result.slice(researchPage * researchPageSize, (researchPage + 1) * researchPageSize);
+  }, [researchHistory, researchSearchTerm, researchDateFilter, researchPage]);
+  
   // 使用记录部分的渲染
   const renderHistoryContent = () => {
     return (
       <div>
-        <h3 className="text-lg font-medium text-white mb-4">聊天历史</h3>
+        <div className="flex justify-between items-center mb-4">
+          <h3 className="text-lg font-medium text-white">AI 对话历史</h3>
+          {chatHistory.length > 0 && (
+            <button
+              onClick={() => setIsFilterChatOpen(!isFilterChatOpen)}
+              className="text-white/70 hover:text-white text-sm flex items-center gap-1"
+            >
+              <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+                <polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon>
+              </svg>
+              筛选
+            </button>
+          )}
+        </div>
+
+        {isFilterChatOpen && (
+          <div className="p-3 bg-white/5 rounded-lg mb-4">
+            <div className="flex gap-3 flex-wrap">
+              <input
+                type="text"
+                placeholder="搜索对话标题..."
+                value={chatSearchTerm}
+                onChange={(e) => setChatSearchTerm(e.target.value)}
+                className="bg-white/10 text-white border border-white/20 rounded-lg p-2 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--theme-accent)]/50"
+              />
+              <select
+                value={chatDateFilter}
+                onChange={(e) => setChatDateFilter(e.target.value)}
+                className="bg-white/10 text-white border border-white/20 rounded-lg p-2 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--theme-accent)]/50"
+              >
+                <option value="all">所有时间</option>
+                <option value="today">今天</option>
+                <option value="week">本周</option>
+                <option value="month">本月</option>
+              </select>
+            </div>
+          </div>
+        )}
+
         {chatHistory.length === 0 ? (
-          <p className="text-white/60">暂无聊天历史记录</p>
+          <div className="p-8 text-center bg-white/5 rounded-lg">
+            <div className="text-white/50 mb-3">
+              <svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1" strokeLinecap="round" strokeLinejoin="round" className="mx-auto">
+                <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
+              </svg>
+            </div>
+            <p className="text-white/60">暂无聊天历史记录</p>
+            <Link 
+              href="/ai-chat" 
+              className="mt-4 inline-block px-4 py-2 bg-gradient-to-r from-[var(--theme-accent)] to-[var(--theme-highlight)] rounded-lg text-white text-sm hover:opacity-90 transition-opacity"
+            >
+              开始新对话
+            </Link>
+          </div>
         ) : (
           <div className="space-y-3">
-            {chatHistory.map((chat: any) => (
-              <div key={chat.id} className="p-3 bg-white/5 rounded-lg hover:bg-white/10">
+            {filteredChatHistory.map((chat: ChatHistoryItem) => (
+              <div key={chat.id} className="p-4 bg-white/5 rounded-lg hover:bg-white/10 transition-colors">
                 <div className="flex justify-between items-center">
                   <h4 className="font-medium text-white">{chat.title}</h4>
-                  <span className="text-white/50 text-sm">
-                    {new Date(chat.created_at).toLocaleDateString()}
-                  </span>
+                  <div className="flex gap-2">
+                    <span className="text-white/50 text-sm">
+                      {new Date(chat.created_at).toLocaleDateString()}
+                    </span>
+                    <button
+                      onClick={() => handleExportChat(chat.id)}
+                      className="text-white/50 hover:text-[var(--theme-accent)]"
+                      title="导出对话"
+                    >
+                      <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+                        <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
+                        <polyline points="7 10 12 15 17 10"></polyline>
+                        <line x1="12" y1="15" x2="12" y2="3"></line>
+                      </svg>
+                    </button>
+                    <button 
+                      onClick={() => handleDeleteChat(chat.id)}
+                      className="text-white/50 hover:text-red-400"
+                      title="删除对话"
+                    >
+                      <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+                        <path d="M3 6h18"></path>
+                        <path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path>
+                      </svg>
+                    </button>
+                  </div>
+                </div>
+                
+                {/* 对话预览 */}
+                <p className="text-white/70 text-sm mt-2 line-clamp-2">
+                  {chat.preview || "点击查看对话详情..."}
+                </p>
+                
+                <div className="mt-3 flex justify-between items-center">
+                  <div>
+                    <span className="inline-block px-2 py-1 bg-white/10 rounded-full text-xs text-white/70 mr-2">
+                      {chat.message_count || 0} 条消息
+                    </span>
+                    {chat.has_code && (
+                      <span className="inline-block px-2 py-1 bg-[var(--theme-accent)]/20 rounded-full text-xs text-[var(--theme-accent)]">
+                        包含代码
+                      </span>
+                    )}
+                  </div>
+                  <Link href={`/ai-chat?id=${chat.id}`} className="text-[var(--theme-accent)] text-sm hover:underline">
+                    查看对话
+                  </Link>
                 </div>
-                <Link href={`/ai-chat?id=${chat.id}`} className="mt-2 text-[var(--theme-accent)] text-sm hover:underline">
-                  查看对话
-                </Link>
               </div>
             ))}
+            
+            {/* 分页控制 */}
+            {chatHistory.length > chatPageSize && (
+              <div className="flex justify-center mt-6">
+                <div className="flex gap-1">
+                  <button 
+                    onClick={() => setChatPage(p => Math.max(0, p - 1))}
+                    disabled={chatPage === 0}
+                    className={`px-3 py-1 rounded-md ${chatPage === 0 ? 'bg-white/5 text-white/30' : 'bg-white/10 text-white hover:bg-white/20'}`}
+                  >
+                    上一页
+                  </button>
+                  <span className="px-3 py-1 bg-white/5 rounded-md text-white">
+                    {chatPage + 1} / {Math.ceil(chatHistory.length / chatPageSize)}
+                  </span>
+                  <button 
+                    onClick={() => setChatPage(p => p + 1 < Math.ceil(chatHistory.length / chatPageSize) ? p + 1 : p)}
+                    disabled={chatPage + 1 >= Math.ceil(chatHistory.length / chatPageSize)}
+                    className={`px-3 py-1 rounded-md ${chatPage + 1 >= Math.ceil(chatHistory.length / chatPageSize) ? 'bg-white/5 text-white/30' : 'bg-white/10 text-white hover:bg-white/20'}`}
+                  >
+                    下一页
+                  </button>
+                </div>
+              </div>
+            )}
           </div>
         )}
-        
-        <h3 className="text-lg font-medium text-white mb-4 mt-8">研究历史</h3>
+
+        {/* 研究历史也使用类似的改进 */}
+        <div className="flex justify-between items-center mb-4 mt-8">
+          <h3 className="text-lg font-medium text-white">研究历史</h3>
+          {researchHistory.length > 0 && (
+            <button
+              onClick={() => setIsFilterResearchOpen(!isFilterResearchOpen)}
+              className="text-white/70 hover:text-white text-sm flex items-center gap-1"
+            >
+              <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+                <polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon>
+              </svg>
+              筛选
+            </button>
+          )}
+        </div>
+
+        {/* 研究历史筛选器 */}
+        {isFilterResearchOpen && (
+          <div className="p-3 bg-white/5 rounded-lg mb-4">
+            <div className="flex gap-3 flex-wrap">
+              <input
+                type="text"
+                placeholder="搜索研究标题..."
+                value={researchSearchTerm}
+                onChange={(e) => setResearchSearchTerm(e.target.value)}
+                className="bg-white/10 text-white border border-white/20 rounded-lg p-2 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--theme-accent)]/50"
+              />
+              <select
+                value={researchDateFilter}
+                onChange={(e) => setResearchDateFilter(e.target.value)}
+                className="bg-white/10 text-white border border-white/20 rounded-lg p-2 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--theme-accent)]/50"
+              >
+                <option value="all">所有时间</option>
+                <option value="today">今天</option>
+                <option value="week">本周</option>
+                <option value="month">本月</option>
+              </select>
+            </div>
+          </div>
+        )}
+
         {researchHistory.length === 0 ? (
-          <p className="text-white/60">暂无研究历史记录</p>
+          <div className="p-8 text-center bg-white/5 rounded-lg">
+            <div className="text-white/50 mb-3">
+              <svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1" strokeLinecap="round" strokeLinejoin="round" className="mx-auto">
+                <path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path>
+                <path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path>
+              </svg>
+            </div>
+            <p className="text-white/60">暂无研究历史记录</p>
+            <Link 
+              href="/research" 
+              className="mt-4 inline-block px-4 py-2 bg-gradient-to-r from-[var(--theme-accent)] to-[var(--theme-highlight)] rounded-lg text-white text-sm hover:opacity-90 transition-opacity"
+            >
+              开始新研究
+            </Link>
+          </div>
         ) : (
           <div className="space-y-3">
-            {researchHistory.map((research: any) => (
-              <div key={research.id} className="p-3 bg-white/5 rounded-lg hover:bg-white/10">
+            {filteredResearchHistory.map((research: ResearchHistoryItem) => (
+              <div key={research.id} className="p-4 bg-white/5 rounded-lg hover:bg-white/10 transition-colors">
                 <div className="flex justify-between items-center">
                   <h4 className="font-medium text-white">{research.title}</h4>
-                  <span className="text-white/50 text-sm">
-                    {new Date(research.created_at).toLocaleDateString()}
-                  </span>
+                  <div className="flex gap-2">
+                    <span className="text-white/50 text-sm">
+                      {new Date(research.created_at).toLocaleDateString()}
+                    </span>
+                    <button
+                      onClick={() => handleExportResearch(research.id)}
+                      className="text-white/50 hover:text-[var(--theme-accent)]"
+                      title="导出研究"
+                    >
+                      <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+                        <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
+                        <polyline points="7 10 12 15 17 10"></polyline>
+                        <line x1="12" y1="15" x2="12" y2="3"></line>
+                      </svg>
+                    </button>
+                    <button 
+                      onClick={() => handleDeleteResearch(research.id)}
+                      className="text-white/50 hover:text-red-400"
+                      title="删除研究"
+                    >
+                      <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+                        <path d="M3 6h18"></path>
+                        <path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path>
+                      </svg>
+                    </button>
+                  </div>
+                </div>
+                
+                {/* 研究摘要 */}
+                <p className="text-white/70 text-sm mt-2 line-clamp-2">
+                  {research.research_intent || "点击查看研究详情..."}
+                </p>
+                
+                <div className="mt-3 flex justify-between items-center">
+                  <div className="flex flex-wrap gap-1">
+                    {research.keywords && research.keywords.slice(0, 3).map((keyword: string, index: number) => (
+                      <span key={index} className="inline-block px-2 py-1 bg-white/10 rounded-full text-xs text-white/70">
+                        {keyword}
+                      </span>
+                    ))}
+                    {research.keywords && research.keywords.length > 3 && (
+                      <span className="inline-block px-2 py-1 rounded-full text-xs text-white/50">
+                        +{research.keywords.length - 3}
+                      </span>
+                    )}
+                  </div>
+                  <Link href={`/research?id=${research.id}`} className="text-[var(--theme-accent)] text-sm hover:underline">
+                    查看研究
+                  </Link>
                 </div>
-                <Link href={`/research?id=${research.id}`} className="mt-2 text-[var(--theme-accent)] text-sm hover:underline">
-                  查看研究
-                </Link>
               </div>
             ))}
+            
+            {/* 分页控制 */}
+            {researchHistory.length > researchPageSize && (
+              <div className="flex justify-center mt-6">
+                <div className="flex gap-1">
+                  <button 
+                    onClick={() => setResearchPage(p => Math.max(0, p - 1))}
+                    disabled={researchPage === 0}
+                    className={`px-3 py-1 rounded-md ${researchPage === 0 ? 'bg-white/5 text-white/30' : 'bg-white/10 text-white hover:bg-white/20'}`}
+                  >
+                    上一页
+                  </button>
+                  <span className="px-3 py-1 bg-white/5 rounded-md text-white">
+                    {researchPage + 1} / {Math.ceil(researchHistory.length / researchPageSize)}
+                  </span>
+                  <button 
+                    onClick={() => setResearchPage(p => p + 1 < Math.ceil(researchHistory.length / researchPageSize) ? p + 1 : p)}
+                    disabled={researchPage + 1 >= Math.ceil(researchHistory.length / researchPageSize)}
+                    className={`px-3 py-1 rounded-md ${researchPage + 1 >= Math.ceil(researchHistory.length / researchPageSize) ? 'bg-white/5 text-white/30' : 'bg-white/10 text-white hover:bg-white/20'}`}
+                  >
+                    下一页
+                  </button>
+                </div>
+              </div>
+            )}
           </div>
         )}
       </div>
@@ -500,6 +849,152 @@ export default function ProfilePage() {
     }
   };
   
+  // 删除聊天历史
+  const handleDeleteChat = async (chatId: number) => {
+    if (!confirm("确定要删除这个对话历史吗?此操作无法撤销。")) {
+      return;
+    }
+    
+    try {
+      const token = localStorage.getItem("token");
+      if (!token) return;
+      
+      await axios.delete(`${API_BASE_URL}/chat-history/${chatId}`, {
+        headers: {
+          Authorization: `Bearer ${token}`
+        }
+      });
+      
+      // 更新本地状态,移除已删除的对话
+      setChatHistory(prevHistory => prevHistory.filter(chat => chat.id !== chatId));
+      
+    } catch (err) {
+      console.error("删除聊天历史失败:", err);
+      alert("删除聊天历史失败,请稍后再试");
+    }
+  };
+
+  // 删除研究历史
+  const handleDeleteResearch = async (researchId: number) => {
+    if (!confirm("确定要删除这个研究历史吗?此操作无法撤销。")) {
+      return;
+    }
+    
+    try {
+      const token = localStorage.getItem("token");
+      if (!token) return;
+      
+      await axios.delete(`${API_BASE_URL}/research-history/${researchId}`, {
+        headers: {
+          Authorization: `Bearer ${token}`
+        }
+      });
+      
+      // 更新本地状态,移除已删除的研究
+      setResearchHistory(prevHistory => prevHistory.filter(research => research.id !== researchId));
+      
+    } catch (err) {
+      console.error("删除研究历史失败:", err);
+      alert("删除研究历史失败,请稍后再试");
+    }
+  };
+
+  // 导出聊天历史
+  const handleExportChat = async (chatId: number) => {
+    try {
+      const token = localStorage.getItem("token");
+      if (!token) return;
+      
+      // 获取完整对话历史
+      const response = await axios.get(`${API_BASE_URL}/chat-history/${chatId}`, {
+        headers: {
+          Authorization: `Bearer ${token}`
+        }
+      });
+      
+      const chat = response.data;
+      
+      // 构建导出文本
+      let exportText = `# ${chat.title}\n`;
+      exportText += `日期: ${new Date(chat.created_at).toLocaleString()}\n\n`;
+      
+      for (const msg of chat.messages) {
+        if (msg.role === 'system') continue;
+        const role = msg.role === 'user' ? '用户' : 'AI助手';
+        exportText += `## ${role}\n${msg.content}\n\n`;
+      }
+      
+      // 创建并下载文件
+      const blob = new Blob([exportText], { type: 'text/markdown' });
+      const url = URL.createObjectURL(blob);
+      const a = document.createElement('a');
+      a.href = url;
+      a.download = `对话-${chat.title}-${new Date().toISOString().split('T')[0]}.md`;
+      a.click();
+      URL.revokeObjectURL(url);
+      
+    } catch (err) {
+      console.error("导出聊天历史失败:", err);
+      alert("导出聊天历史失败,请稍后再试");
+    }
+  };
+
+  // 导出研究历史
+  const handleExportResearch = async (researchId: number) => {
+    try {
+      const token = localStorage.getItem("token");
+      if (!token) return;
+      
+      // 获取完整研究历史
+      const response = await axios.get(`${API_BASE_URL}/research-history/${researchId}`, {
+        headers: {
+          Authorization: `Bearer ${token}`
+        }
+      });
+      
+      const research = response.data;
+      
+      // 构建导出文本
+      let exportText = `# ${research.title}\n`;
+      exportText += `日期: ${new Date(research.created_at).toLocaleString()}\n\n`;
+      
+      exportText += `## 研究意图\n${research.research_intent}\n\n`;
+      
+      exportText += `## 关键词\n`;
+      for (const keyword of research.keywords) {
+        exportText += `- ${keyword}\n`;
+      }
+      exportText += '\n';
+      
+      exportText += `## 研究方向\n`;
+      for (const direction of research.directions) {
+        exportText += `### ${direction.title}\n${direction.description}\n\n`;
+      }
+      
+      exportText += `## 相关论文\n`;
+      for (const paper of research.papers) {
+        exportText += `### ${paper.title}\n`;
+        exportText += `作者: ${paper.authors.join(', ')}\n`;
+        exportText += `发表于: ${paper.journal || paper.conference || '未知'}, ${paper.year}\n`;
+        exportText += `摘要: ${paper.abstract || '无摘要'}\n`;
+        exportText += `DOI: ${paper.doi || '无'}\n\n`;
+      }
+      
+      // 创建并下载文件
+      const blob = new Blob([exportText], { type: 'text/markdown' });
+      const url = URL.createObjectURL(blob);
+      const a = document.createElement('a');
+      a.href = url;
+      a.download = `研究-${research.title}-${new Date().toISOString().split('T')[0]}.md`;
+      a.click();
+      URL.revokeObjectURL(url);
+      
+    } catch (err) {
+      console.error("导出研究历史失败:", err);
+      alert("导出研究历史失败,请稍后再试");
+    }
+  };
+  
   return (
     <main className="relative flex min-h-[calc(100vh-64px)] flex-col">
       <div className="container mx-auto px-4 py-6 flex flex-col">

+ 3 - 3
lightstar-web/src/components/forms/research-form.tsx

@@ -1,6 +1,6 @@
 "use client";
 
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, FormEvent } from 'react';
 import { Button } from '@/components/ui/button';
 import { ErrorMessage } from '@/components/ui/error-message';
 
@@ -481,7 +481,7 @@ export function ResearchForm({ className = "", onSubmit }: ResearchFormProps) {
   };
   
   // 表单提交
-  const handleSubmit = (e: React.FormEvent) => {
+  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
     e.preventDefault();
     
     if (!researchTopic.trim()) {
@@ -497,7 +497,7 @@ export function ResearchForm({ className = "", onSubmit }: ResearchFormProps) {
         const newTopic = researchTopic; // 保存当前主题
         const newEvent = new Event('submit') as React.FormEvent;
         setResearchTopic(newTopic);
-        handleSubmit(newEvent);
+        handleSubmit(newEvent as FormEvent<HTMLFormElement>);
       }, 10);
       return;
     }