代码质量:
- 新增 .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 限制
228 lines
8.4 KiB
TypeScript
228 lines
8.4 KiB
TypeScript
import React from 'react';
|
|
import {
|
|
Brain,
|
|
Compass,
|
|
BookOpen,
|
|
Send,
|
|
Square,
|
|
Paperclip,
|
|
X,
|
|
} from 'lucide-react';
|
|
import type { AgentMode } from '../../hooks/useResearchAgent';
|
|
|
|
interface AgentInputAreaProps {
|
|
input: string;
|
|
setInput: (val: string) => void;
|
|
streaming: boolean;
|
|
thinking: boolean;
|
|
setThinking: (val: boolean) => void;
|
|
agentMode: string;
|
|
setAgentMode: (val: string) => void;
|
|
agentModes: AgentMode[];
|
|
pendingImage: {
|
|
data?: string;
|
|
path?: string;
|
|
mime_type: string;
|
|
name: string;
|
|
} | null;
|
|
setPendingImage: (
|
|
val: {
|
|
data?: string;
|
|
path?: string;
|
|
mime_type: string;
|
|
name: string;
|
|
} | null
|
|
) => void;
|
|
onSend: (text: string) => void;
|
|
onStop: () => void;
|
|
fileInputRef: React.RefObject<HTMLInputElement | null>;
|
|
attachImage: (file: File) => void;
|
|
handlePaste: (e: React.ClipboardEvent) => void;
|
|
}
|
|
|
|
const ICON_MAP: Record<string, React.ComponentType<{ className?: string }>> = {
|
|
Brain,
|
|
Compass,
|
|
BookOpen,
|
|
};
|
|
|
|
function getIconComponent(
|
|
iconName: string
|
|
): React.ComponentType<{ className?: string }> {
|
|
return ICON_MAP[iconName] || Brain;
|
|
}
|
|
|
|
export function AgentInputArea({
|
|
input,
|
|
setInput,
|
|
streaming,
|
|
thinking,
|
|
setThinking,
|
|
agentMode,
|
|
setAgentMode,
|
|
agentModes,
|
|
pendingImage,
|
|
setPendingImage,
|
|
onSend,
|
|
onStop,
|
|
fileInputRef,
|
|
attachImage,
|
|
handlePaste,
|
|
}: AgentInputAreaProps) {
|
|
return (
|
|
<div className="absolute bottom-0 left-0 right-0 p-4 bg-transparent pointer-events-none shrink-0 z-30 flex flex-col gap-3">
|
|
<form
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
onSend(input);
|
|
}}
|
|
className="max-w-4xl mx-auto w-full pointer-events-auto"
|
|
>
|
|
<div className="bg-slate-50 border border-slate-200/60 rounded-lg p-2.5 focus-within:bg-white focus-within:border-blueprint focus-within:ring-1 focus-within:ring-blueprint/10 transition-all shadow-sm flex flex-col gap-2">
|
|
{/* 图片预览 */}
|
|
{pendingImage && (
|
|
<div className="relative inline-block mb-1 group select-none self-start">
|
|
<img
|
|
src={
|
|
pendingImage.path
|
|
? `/api/files/${pendingImage.path}`
|
|
: `data:${pendingImage.mime_type};base64,${pendingImage.data}`
|
|
}
|
|
alt="Preview"
|
|
className="w-12 h-12 object-cover rounded-lg border border-slate-200 shadow-sm"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setPendingImage(null)}
|
|
className="absolute -top-1.5 -right-1.5 bg-slate-500/80 hover:bg-slate-700 text-white rounded-full p-0.5 shadow-sm transition-colors cursor-pointer flex items-center justify-center"
|
|
title="移除图片"
|
|
>
|
|
<X className="w-2.5 h-2.5" />
|
|
</button>
|
|
</div>
|
|
)}
|
|
<textarea
|
|
value={input}
|
|
onChange={(e) => setInput(e.target.value)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
e.preventDefault();
|
|
onSend(input);
|
|
}
|
|
}}
|
|
disabled={streaming}
|
|
placeholder="向科研智能体提问(可粘贴或上传图片,配合深度研究模式分析图表)..."
|
|
rows={2}
|
|
onPaste={handlePaste}
|
|
className="w-full bg-transparent resize-none border-none outline-none focus:outline-none focus:ring-0 text-xs text-slate-900 placeholder-slate-400 leading-relaxed font-semibold min-h-[44px] max-h-40"
|
|
/>
|
|
<div className="flex justify-between items-center mt-2.5 pt-2 border-t border-slate-100/80">
|
|
<div className="flex flex-wrap gap-2 items-center">
|
|
{/* 图片上传 */}
|
|
<input
|
|
ref={fileInputRef}
|
|
type="file"
|
|
accept="image/*"
|
|
className="hidden"
|
|
onChange={(e) => {
|
|
const file = e.target.files?.[0];
|
|
if (file) attachImage(file);
|
|
e.target.value = ''; // 允许重复选择同一文件
|
|
}}
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => fileInputRef.current?.click()}
|
|
disabled={streaming}
|
|
title="上传或粘贴图片(也可直接 Ctrl+V 粘贴)"
|
|
className={`p-1.5 rounded-lg border text-[10px] font-bold transition-all cursor-pointer flex items-center gap-1 ${
|
|
pendingImage
|
|
? 'bg-blueprint/5 border-blueprint/30 text-blueprint'
|
|
: 'bg-white border-slate-200 text-slate-400 hover:text-blueprint hover:border-blueprint/20'
|
|
} disabled:opacity-60`}
|
|
>
|
|
<Paperclip className="w-3.5 h-3.5" />
|
|
</button>
|
|
|
|
{/* Agent 模式选择器 */}
|
|
<div className="flex gap-1 bg-slate-100 p-0.5 rounded-lg border border-slate-200">
|
|
{agentModes.map((m) => {
|
|
const IconComp = getIconComponent(m.icon);
|
|
const isSelected = agentMode === m.id;
|
|
return (
|
|
<button
|
|
key={m.id}
|
|
type="button"
|
|
onClick={() => setAgentMode(m.id)}
|
|
disabled={streaming}
|
|
title={m.description}
|
|
className={`px-2 py-1.5 rounded-md text-[10px] font-extrabold transition-all border cursor-pointer flex items-center gap-1 shrink-0 ${
|
|
isSelected
|
|
? 'bg-white text-blueprint shadow-xs border-slate-200/50'
|
|
: 'bg-transparent border-transparent text-slate-400 hover:text-blueprint'
|
|
} disabled:opacity-60`}
|
|
>
|
|
<IconComp
|
|
className={`w-3.5 h-3.5 ${isSelected ? 'text-blueprint' : ''}`}
|
|
/>
|
|
<span className="hidden sm:inline">{m.name}</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* 思考模式开关 */}
|
|
{agentMode === 'default' && (
|
|
<>
|
|
<div className="h-4 w-px bg-slate-200 mx-1 hidden sm:block" />
|
|
<button
|
|
type="button"
|
|
onClick={() => setThinking(!thinking)}
|
|
disabled={streaming}
|
|
title={
|
|
thinking
|
|
? '思考模式已开启(启用 LLM 推理过程)'
|
|
: '思考模式已关闭(点击开启)'
|
|
}
|
|
className={`px-2.5 py-1.5 rounded-lg border text-[10px] font-bold transition-all cursor-pointer flex items-center gap-1 shrink-0 ${
|
|
thinking
|
|
? 'bg-blueprint/5 border-blueprint/30 text-blueprint shadow-3xs'
|
|
: 'bg-white border-slate-200 text-slate-400 hover:text-blueprint hover:border-blueprint/20'
|
|
} disabled:opacity-60`}
|
|
>
|
|
<Brain
|
|
className={`w-3.5 h-3.5 ${thinking ? 'text-blueprint' : ''}`}
|
|
/>
|
|
<span className="hidden sm:inline">思考</span>
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
{streaming ? (
|
|
<button
|
|
type="button"
|
|
onClick={onStop}
|
|
className="p-2 rounded-md bg-slate-800 hover:bg-slate-950 text-white transition-colors cursor-pointer flex items-center justify-center shadow-xs"
|
|
title="手动停止执行"
|
|
>
|
|
<Square className="w-3.5 h-3.5 fill-white" />
|
|
</button>
|
|
) : (
|
|
<button
|
|
type="submit"
|
|
disabled={!input.trim()}
|
|
className="p-2 rounded-md bg-blueprint hover:bg-[#0d5988] text-white disabled:bg-slate-100 disabled:text-slate-350 transition-colors cursor-pointer flex items-center justify-center shadow-xs"
|
|
>
|
|
<Send className="w-3.5 h-3.5" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|