- 服务层拆分:删除 api/helpers.rs,新增 citation/note/session/pipeline/paper/vision 独立服务模块
- Agent 工具精简:paper_content+paper_outline 合并为 paper.rs,图片分析逻辑下沉至 services/vision
- 并发模型升级:std::sync::{Mutex,RwLock} → tokio::sync::{Mutex,RwLock},消除 async
上下文中的阻塞风险
- 客户端加固:HTTP 客户端统一超时配置、ADS 429 / arXiv 503 自动重试、构造函数返回 Result
- 启动安全:全局 panic hook 日志化、空密码拒绝启动、向量表维度不匹配需显式确认
- CLI 扩展:构建完整 AppState 复用服务层,新增 Content/Outline/Citations/Search/Process 子命令
- 前端:移动端汉堡菜单、侧栏滑出面板、引用星系触屏手势(单指拖拽/双指缩放)
243 lines
11 KiB
TypeScript
243 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 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 w-64 border-r border-slate-200 lg:w-64'
|
|
}`}
|
|
>
|
|
<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>
|
|
</>
|
|
);
|
|
}
|