|
@@ -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">
|