代码质量:
- 新增 .prettierrc + eslint-plugin-prettier,全量格式化所有 TSX/TS/CSS 文件
- npm scripts 新增 format / format:check / lint:fix
Logo 重设计:
- SVG logo 从简单星月改为望远镜+轨道+三角架+星光,favicon 同步更新
Library 服务端分页与多维筛选:
- LibraryQueryParams 支持 q(全局搜索)/status(下载状态)/doctype/author/year/journal/sort_by
- API 返回 {items, total},默认每页 12 条
- 前端新增筛选面板:搜索栏、状态下拉、文献类型、排序、分页控件
Agent 工具输出可视化:
- 新增 SpecialToolRenderers.tsx:query_target 天体参数卡片(含 SIMBAD/VizieR 链接)、search_papers
文献列表(内嵌下载/阅读/星系跳转按钮)
- 系统内部工具(compress_context 等)无错误时自动隐藏,减少时间线噪音
- ToolCallCard 与 Reader/Citation 打通:可在工具输出中直接打开文献或跳转引用星系
.agent 目录统一:
- memory/tool-results/trajectories/images 全部归入 library/.agent/ 子目录
PDF 加载修复:
- BilingualViewer 改用 fetch → blob:// URL 加载 PDF,绕过 iframe SameSite Cookie 限制
254 lines
11 KiB
TypeScript
254 lines
11 KiB
TypeScript
import React from 'react';
|
|
import {
|
|
Clock,
|
|
Plus,
|
|
PanelLeftClose,
|
|
Search,
|
|
X,
|
|
MessageSquare,
|
|
BookOpen,
|
|
Trash2,
|
|
Loader,
|
|
} from 'lucide-react';
|
|
import type {
|
|
SessionSummary,
|
|
SearchResult,
|
|
} from '../../hooks/useResearchAgent';
|
|
|
|
interface AgentSessionSidebarProps {
|
|
sessions: SessionSummary[];
|
|
currentSessionId: string | null;
|
|
setCurrentSessionId: (id: string | null) => void;
|
|
loadingSessions: boolean;
|
|
searchQuery: string;
|
|
setSearchQuery: (val: string) => void;
|
|
searchScopeOnlyCurrent: boolean;
|
|
setSearchScopeOnlyCurrent: (val: boolean) => void;
|
|
searchResults: SearchResult[];
|
|
loadingSearch: boolean;
|
|
collapsed: boolean;
|
|
onToggleCollapse: (val: boolean) => void;
|
|
onNewSession: () => void;
|
|
onDeleteSession: (sessionId: string, e: React.MouseEvent) => void;
|
|
}
|
|
|
|
export function AgentSessionSidebar({
|
|
sessions,
|
|
currentSessionId,
|
|
setCurrentSessionId,
|
|
loadingSessions,
|
|
searchQuery,
|
|
setSearchQuery,
|
|
searchScopeOnlyCurrent,
|
|
setSearchScopeOnlyCurrent,
|
|
searchResults,
|
|
loadingSearch,
|
|
collapsed,
|
|
onToggleCollapse,
|
|
onNewSession,
|
|
onDeleteSession,
|
|
}: AgentSessionSidebarProps) {
|
|
const handleSelectSession = (id: string | null) => {
|
|
setCurrentSessionId(id);
|
|
if (typeof window !== 'undefined' && window.innerWidth < 1024) {
|
|
onToggleCollapse(true);
|
|
}
|
|
};
|
|
|
|
const handleCreateNewSession = () => {
|
|
onNewSession();
|
|
if (typeof window !== 'undefined' && window.innerWidth < 1024) {
|
|
onToggleCollapse(true);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{/* 移动端背景遮罩层 */}
|
|
{!collapsed && (
|
|
<button
|
|
type="button"
|
|
onClick={() => onToggleCollapse(true)}
|
|
className="fixed inset-0 bg-slate-900/30 backdrop-blur-xs z-35 lg:hidden cursor-pointer w-full h-full border-none outline-none"
|
|
aria-label="关闭侧栏"
|
|
/>
|
|
)}
|
|
|
|
<div
|
|
className={`bg-slate-50 flex flex-col justify-between shrink-0 select-none transition-all duration-300 ease-in-out fixed inset-y-0 left-0 z-40 w-64 lg:relative lg:translate-x-0 ${
|
|
collapsed
|
|
? '-translate-x-full lg:translate-x-0 lg:w-0 lg:overflow-hidden lg:opacity-0 lg:border-r-0'
|
|
: 'translate-x-0 border-r border-slate-200'
|
|
}`}
|
|
>
|
|
<div className="flex flex-col min-h-0 flex-1">
|
|
<div className="p-4 border-b border-slate-200 bg-white flex items-center justify-between shrink-0">
|
|
<span className="text-xs font-extrabold text-slate-800 tracking-wider flex items-center gap-1.5">
|
|
<Clock className="w-3.5 h-3.5 text-blueprint" />
|
|
<span>会话历史</span>
|
|
</span>
|
|
<div className="flex items-center gap-1 shrink-0">
|
|
<button
|
|
onClick={handleCreateNewSession}
|
|
className="p-1 rounded-md border border-slate-200 bg-white hover:bg-slate-50 text-slate-650 hover:text-slate-800 transition-all cursor-pointer shadow-2xs"
|
|
title="新建会话"
|
|
>
|
|
<Plus className="w-3.5 h-3.5" />
|
|
</button>
|
|
<button
|
|
onClick={() => onToggleCollapse(true)}
|
|
className="p-1 rounded-md border border-slate-200 bg-white hover:bg-slate-50 text-slate-555 hover:text-slate-700 transition-all cursor-pointer"
|
|
title="收起侧栏"
|
|
>
|
|
<PanelLeftClose className="w-3.5 h-3.5" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 搜索框区域 */}
|
|
<div className="px-3 py-2 border-b border-slate-200/60 bg-white flex flex-col gap-1.5 shrink-0">
|
|
<div className="relative">
|
|
<input
|
|
type="text"
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
placeholder="搜索历史会话或消息内容..."
|
|
className="w-full pl-8 pr-7 py-1.5 rounded-md bg-slate-50 border border-slate-200 text-slate-800 placeholder-slate-400 focus:outline-none focus:border-blueprint focus:bg-white transition-all text-[11px] font-medium"
|
|
/>
|
|
<div className="absolute left-2.5 top-2.5 text-slate-400">
|
|
<Search className="w-3.5 h-3.5 text-slate-400" />
|
|
</div>
|
|
{searchQuery && (
|
|
<button
|
|
onClick={() => setSearchQuery('')}
|
|
className="absolute right-2 top-2 p-0.5 rounded-full hover:bg-slate-200 text-slate-400 hover:text-slate-600 transition-colors cursor-pointer"
|
|
>
|
|
<X className="w-3 h-3" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
{currentSessionId && (
|
|
<label className="flex items-center gap-1.5 text-[10px] text-slate-500 cursor-pointer font-medium select-none ml-0.5">
|
|
<input
|
|
type="checkbox"
|
|
checked={searchScopeOnlyCurrent}
|
|
onChange={(e) => setSearchScopeOnlyCurrent(e.target.checked)}
|
|
className="rounded text-blueprint border-slate-300 focus:ring-blueprint w-3 h-3 cursor-pointer"
|
|
/>
|
|
<span>仅搜索当前会话</span>
|
|
</label>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-y-auto p-2.5 space-y-1.5 scrollbar-thin">
|
|
{searchQuery.trim() ? (
|
|
loadingSearch ? (
|
|
<div className="flex items-center justify-center p-8 text-slate-400 text-xs gap-2">
|
|
<Loader className="w-3.5 h-3.5 animate-spin text-blueprint" />
|
|
<span>检索历史记录中...</span>
|
|
</div>
|
|
) : searchResults.length === 0 ? (
|
|
<div className="text-center py-12 text-slate-400 text-[11px] italic">
|
|
未找到匹配的历史记录
|
|
</div>
|
|
) : (
|
|
searchResults.map((result, idx) => {
|
|
const isActive = result.session_id === currentSessionId;
|
|
const isMessage = result.result_type.startsWith('message/');
|
|
const role = isMessage
|
|
? result.result_type.split('/')[1]
|
|
: null;
|
|
|
|
return (
|
|
<button
|
|
key={`${result.session_id}-${idx}`}
|
|
onClick={() => handleSelectSession(result.session_id)}
|
|
className={`w-full text-left p-2.5 rounded-md border transition-all duration-200 flex flex-col gap-1 cursor-pointer ${
|
|
isActive
|
|
? 'bg-blueprint/5 border-blueprint text-blueprint font-bold shadow-2xs'
|
|
: 'border-transparent bg-white hover:bg-slate-100 text-slate-650 shadow-3xs'
|
|
}`}
|
|
>
|
|
<div className="flex items-center justify-between w-full">
|
|
<span className="text-[10px] font-bold text-slate-400 flex items-center gap-1">
|
|
{isMessage ? (
|
|
<>
|
|
<MessageSquare className="w-3 h-3 text-slate-400" />
|
|
<span>{role === 'user' ? '提问' : '解答'}</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<BookOpen className="w-3 h-3 text-blueprint" />
|
|
<span>会话</span>
|
|
</>
|
|
)}
|
|
</span>
|
|
{result.created_at && (
|
|
<span className="text-[8px] text-slate-400 font-mono">
|
|
{result.created_at.split(' ')[0]}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
<span className="text-xs leading-snug line-clamp-1 block text-left font-bold text-slate-800">
|
|
{result.title || '无标题会话'}
|
|
</span>
|
|
|
|
<p
|
|
className="text-[10px] text-slate-500 font-medium leading-normal block text-left line-clamp-3 bg-slate-50/50 p-1.5 rounded border border-slate-100/50"
|
|
dangerouslySetInnerHTML={{ __html: result.snippet }}
|
|
/>
|
|
</button>
|
|
);
|
|
})
|
|
)
|
|
) : // 正常的会话列表渲染
|
|
loadingSessions ? (
|
|
<div className="flex items-center justify-center p-8 text-slate-400 text-xs gap-2">
|
|
<Loader className="w-3.5 h-3.5 animate-spin text-blueprint" />
|
|
<span>加载历史会话中...</span>
|
|
</div>
|
|
) : sessions.length === 0 ? (
|
|
<div className="text-center py-12 text-slate-400 text-[11px] italic">
|
|
暂无历史会话记录
|
|
</div>
|
|
) : (
|
|
sessions.map((session) => {
|
|
const isActive = session.session_id === currentSessionId;
|
|
return (
|
|
<button
|
|
key={session.session_id}
|
|
onClick={() => handleSelectSession(session.session_id)}
|
|
className={`w-full text-left p-3 rounded-md border transition-all duration-200 group flex items-start justify-between gap-2 cursor-pointer ${
|
|
isActive
|
|
? 'bg-blueprint/5 border-blueprint text-blueprint font-bold shadow-2xs'
|
|
: 'border-transparent bg-transparent hover:bg-slate-100 text-slate-655'
|
|
}`}
|
|
>
|
|
<div className="flex-1 min-w-0 flex flex-col gap-1 text-left">
|
|
<span className="text-xs leading-snug line-clamp-2 block text-left">
|
|
{session.title || '无标题会话'}
|
|
</span>
|
|
<span className="text-[9px] text-slate-400 font-semibold block text-left">
|
|
{session.turn_count} 轮交互 •{' '}
|
|
{session.updated_at.split(' ')[0]}
|
|
</span>
|
|
</div>
|
|
<button
|
|
onClick={(e) => onDeleteSession(session.session_id, e)}
|
|
className="text-slate-400 hover:text-red-655 p-1 rounded-md opacity-0 group-hover:opacity-100 hover:bg-white border border-transparent hover:border-slate-200 transition-all cursor-pointer shrink-0"
|
|
title="删除此会话"
|
|
>
|
|
<Trash2 className="w-3 h-3" />
|
|
</button>
|
|
</button>
|
|
);
|
|
})
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|