AstroResearch/dashboard/src/components/agent/AgentInputArea.tsx
Asfmq 0ba85d7749 refactor: Prettier 格式化全量前端、Library 服务端分页筛选、Agent 工具输出可视化与 .agent 目录统一
代码质量:
  - 新增 .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 限制
2026-07-03 00:11:02 +08:00

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>
);
}