- Config 按职责拆分为 LlmConfig/EmbeddingConfig/VisionConfig/CdsConfig/StorageConfig 五个子结构

- AppState 拆分为 LlmState/DataSourceState/SessionState 三个子结构,消除 50+ 平铺字段
- 新增 ServiceError 结构化错误类型替代 handler 中的 msg.contains() 字符串匹配
- 移除 api::handlers 兼容命名空间,路由直接引用 agent/auth/papers 等模块

- login_rate_limiter/upload_rate_limiter 从 Mutex\<HashMap\> 迁移为 DashMap(无锁)
- 新增 session_last_active: DashMap\<String, AtomicU64\>,auth 中间件快速路径免写锁
- 会话过期清理改为按间隔触发(300s),避免每次请求全表扫描
- MAX_SESSIONS 1000→10000,SSE 广播通道 256→1024

- AgentTool trait 新增 is_internal()/display_name(),SSE 事件携带工具元数据
- 新增 GET /chat/tools 端点暴露注册工具列表
- pending_questions/pending_permissions 增加 created_at 时间戳,自动清理过期条目(10min TTL)
- Agent 超时现在正确 abort 后台任务并设置取消令牌

- FITS 解析: APOGEE/DESI 改用 read_image+切片替代 read_rows(修复 fitsio panic)
- ZTF: CIRCLE 参数分隔符 +→空格(修复 IRSA 400),半径自动裁剪至硬上限
- MAST TESS: parse_tic_json 兼容数组/对象两种 API 响应格式
- 统一检索: per_target_limit 默认 50→1,sources 支持 per-source release/version
- Gaia 测光从 VizieR 镜像切换至官方 TAP 服务

- 向量化降级从串行改为并发 5 条/批(buffer_unordered)
- 混合检索 RRF 合并从借用改为 owned RetrievalResult

- PDF 中间件: URL 解码 %2F/%2E 后判扩展名;文件名过滤非 ASCII + 禁 \ 防头注入
- chat_agent 日志截断问题内容至 50 字符;list_sessions 强制 limit clamp

- 设计令牌三层架构: primitive→semantic→component,浅色暖纸张学术/暗色 Night Indigo
- useTheme hook + ThemeToggle 侧边栏组件 + main.tsx 防 FOUC 初始化
- 全组件从硬编码 slate 色迁移至语义令牌(bg-surface/text-content/border-subtle 等)
- 新增 ToastContainer 非阻塞通知系统

- deploy.sh 引入 SSH ControlMaster 单次密码复用
This commit is contained in:
fmq
2026-07-11 14:12:11 +08:00
parent 8f1ed6d08c
commit 5ad0cf1d28
142 changed files with 6349 additions and 3470 deletions
+415 -260
View File
@@ -16,6 +16,9 @@ import { UncachedPaperModal } from './components/dialogs/UncachedPaperModal';
import { PaperDetailModal } from './components/dialogs/PaperDetailModal';
import { Logo } from './components/Logo';
import { ErrorBoundary } from './components/ErrorBoundary';
import { motion, AnimatePresence } from 'framer-motion';
import { ToastContainer } from './components/layout/ToastContainer';
import type { ToastItem } from './components/layout/ToastContainer';
// 引入拆分出的业务逻辑 Hooks
import { useAuth } from './hooks/useAuth';
@@ -29,7 +32,7 @@ export default function App() {
// 移动端菜单显示状态
const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false);
// 1. 全局自定义 Dialog 弹窗管理 (Alert / Confirm)
// 1. 全局自定义 Dialog 弹窗与 Toast 通知管理
const [dialog, setDialog] = useState<{
type: 'alert' | 'confirm';
title: string;
@@ -38,14 +41,53 @@ export default function App() {
onCancel?: () => void;
} | null>(null);
const showAlert = useCallback((message: string, title = '系统提示') => {
setDialog({
type: 'alert',
title,
message,
onConfirm: () => {},
});
}, []);
const [toasts, setToasts] = useState<ToastItem[]>([]);
const showToast = useCallback(
(message: string, type: 'success' | 'error' | 'info' = 'success') => {
const id = Date.now() + Math.random();
setToasts((prev) => [...prev, { id, message, type }]);
setTimeout(() => {
setToasts((prev) => prev.filter((t) => t.id !== id));
}, 3000);
},
[]
);
const showAlert = useCallback(
(message: string, title = '系统提示') => {
const toastTriggers = [
'成功',
'完成',
'复制',
'导入',
'保存',
'通知',
'已存',
'提示',
];
const isToast = toastTriggers.some(
(t) =>
title.includes(t) ||
message.includes('成功') ||
message.includes('复制') ||
message.includes('完成') ||
message.includes('已存')
);
if (isToast) {
showToast(message, 'success');
} else {
setDialog({
type: 'alert',
title,
message,
onConfirm: () => {},
});
}
},
[showToast]
);
const showConfirm = useCallback(
(message: string, onConfirm: () => void, title = '确认操作') => {
@@ -344,10 +386,10 @@ export default function App() {
// 4. 渲染顶层登录页面
if (auth.isAuthenticated === null) {
return (
<div className="flex h-screen w-screen items-center justify-center bg-[#f4f6f9]">
<div className="flex h-screen w-screen items-center justify-center bg-app">
<div className="flex flex-col items-center gap-4">
<Loader className="w-6 h-6 text-[#106ba3] animate-spin" />
<span className="text-xs font-bold text-[#0a2540] tracking-wider font-sans">
<Loader className="w-6 h-6 text-blueprint animate-spin" />
<span className="text-xs font-bold text-content tracking-wider font-sans">
...
</span>
</div>
@@ -357,26 +399,32 @@ export default function App() {
if (auth.isAuthenticated === false) {
return (
<div className="flex h-screen w-screen items-center justify-center bg-[#f4f6f9] overflow-hidden relative select-none font-sans">
<div className="absolute top-1/4 left-1/4 w-[500px] h-[500px] bg-sky-200/20 rounded-full blur-3xl" />
<div className="absolute bottom-1/4 right-1/4 w-[500px] h-[500px] bg-indigo-200/20 rounded-full blur-3xl" />
<div className="flex h-screen w-screen items-center justify-center bg-app overflow-hidden relative select-none font-sans">
<div
className="absolute top-1/4 left-1/4 w-[500px] h-[500px] rounded-full blur-3xl"
style={{ backgroundColor: 'var(--blob-a)' }}
/>
<div
className="absolute bottom-1/4 right-1/4 w-[500px] h-[500px] rounded-full blur-3xl"
style={{ backgroundColor: 'var(--blob-b)' }}
/>
<div className="console-panel rounded-lg p-8 max-w-sm w-full mx-4 shadow-sm z-10 relative bg-white border border-[#d2d8e2]">
<div className="console-panel rounded-lg p-8 max-w-sm w-full mx-4 z-10 relative">
<div className="flex flex-col items-center text-center mb-7 select-none">
<div className="w-14 h-14 mb-3">
<div className="w-14 h-14 mb-3 text-blueprint">
<Logo gradientId="loginStarGrad" />
</div>
<h2 className="text-sm font-bold text-[#0a2540] tracking-wider mb-1">
<h2 className="text-sm font-bold text-content tracking-wider mb-1">
AstroResearch
</h2>
<p className="text-[10px] text-[#5c6b84] font-medium tracking-wide">
<p className="text-[10px] text-secondary font-medium tracking-wide">
·
</p>
</div>
<form onSubmit={auth.handleLogin} className="space-y-4">
<div className="space-y-1.5">
<label className="text-[11px] font-bold text-[#0a2540] block">
<label className="text-[11px] font-bold text-content block">
访
</label>
<div className="relative">
@@ -387,16 +435,16 @@ export default function App() {
placeholder="请输入系统访问密码"
autoFocus
disabled={auth.loggingIn}
className="w-full pl-9 pr-4 py-2 rounded-lg bg-slate-50 border border-[#d2d8e2] text-slate-900 placeholder-slate-400 focus:outline-none focus:border-[#106ba3] focus:bg-white transition-all text-xs font-medium"
className="w-full pl-9 pr-4 py-2 rounded-lg bg-sunken border border-default text-content placeholder-tertiary focus:outline-none focus:border-blueprint focus:bg-surface transition-all text-xs font-medium"
/>
<div className="absolute left-3 top-2.5 text-slate-400">
<Lock className="w-3.5 h-3.5 text-slate-500" />
<div className="absolute left-3 top-2.5 text-tertiary">
<Lock className="w-3.5 h-3.5" />
</div>
</div>
</div>
{auth.loginError && (
<div className="p-2.5 rounded-lg bg-red-50 border border-red-200 text-[10px] font-bold text-red-700 leading-relaxed">
<div className="p-2.5 rounded-lg bg-rose-50 dark:bg-rose-500/10 border border-rose-200 dark:border-rose-500/30 text-[10px] font-bold text-rose-700 dark:text-rose-300 leading-relaxed">
{auth.loginError}
</div>
)}
@@ -404,7 +452,7 @@ export default function App() {
<button
type="submit"
disabled={auth.loggingIn}
className="w-full btn-console btn-console-primary py-2 rounded-lg text-xs font-bold transition-all shadow-xs flex items-center justify-center gap-2 cursor-pointer"
className="w-full btn-console btn-console-primary py-2 rounded-lg text-xs font-bold transition-all flex items-center justify-center gap-2 cursor-pointer"
>
{auth.loggingIn ? (
<>
@@ -423,7 +471,7 @@ export default function App() {
// 5. 正常工作面板布局装配
return (
<div className="flex h-[100dvh] overflow-hidden text-slate-800 bg-slate-100 select-text">
<div className="flex h-[100dvh] overflow-hidden text-content bg-app select-text">
{/* 导航左侧栏 */}
<Sidebar
activeTab={activeTab}
@@ -438,26 +486,26 @@ export default function App() {
{/* 主工作区 */}
<main className="flex-1 flex flex-col overflow-hidden relative">
{/* 移动端顶部 Header */}
<header className="lg:hidden flex items-center justify-between px-4 pb-3 bg-white border-b border-slate-200 select-none shrink-0 z-20 pt-[calc(env(safe-area-inset-top,0px)+12px)]">
<header className="lg:hidden flex items-center justify-between px-4 pb-3 bg-surface border-b border-subtle select-none shrink-0 z-20 pt-[calc(env(safe-area-inset-top,0px)+12px)]">
<div className="flex items-center gap-3">
<button
type="button"
onClick={() => setIsMobileSidebarOpen(true)}
className="p-1.5 rounded-lg border border-slate-200 hover:bg-slate-55 text-slate-600 transition-all cursor-pointer flex items-center justify-center"
className="p-1.5 rounded-lg hover:bg-sunken text-secondary transition-all cursor-pointer flex items-center justify-center"
title="打开菜单"
>
<Menu className="w-5 h-5" />
</button>
<div className="flex items-center gap-2">
<div className="w-6 h-6">
<div className="w-6 h-6 text-blueprint">
<Logo gradientId="mobileHeaderStarGrad" />
</div>
<span className="text-xs font-bold text-slate-800 tracking-wider">
<span className="text-xs font-bold text-content tracking-wider">
AstroResearch
</span>
</div>
</div>
<span className="text-[10px] font-bold px-2.5 py-1 rounded bg-slate-100 text-slate-600 font-sans tracking-wide uppercase">
<span className="text-[10px] font-bold px-2.5 py-1 rounded bg-sunken text-secondary font-sans tracking-wide uppercase">
{activeTab === 'search' && '统一检索'}
{activeTab === 'library' && '馆藏管理'}
{activeTab === 'reader' && '双语阅读'}
@@ -487,241 +535,342 @@ export default function App() {
}`}
>
<ErrorBoundary>
{activeTab === 'search' && (
<SearchPanel
searchQuery={search.searchQuery}
setSearchQuery={search.setSearchQuery}
searchSource={search.searchSource}
setSearchSource={search.setSearchSource}
searchRows={search.searchRows}
searchStart={search.searchStart}
searchSort={search.searchSort}
handlePageChange={search.handlePageChange}
handleSortChange={search.handleSortChange}
handleRowsChange={search.handleRowsChange}
searching={search.searching}
handleSearch={search.handleSearch}
searchResults={search.searchResults}
exportingList={search.exportingList}
toggleExportItem={search.toggleExportItem}
handleExportBibtex={search.handleExportBibtex}
exporting={search.exporting}
bibtexContent={search.bibtexContent}
downloadingBibcodes={library.downloadingBibcodes}
handleDownload={(bibcode, force) =>
library.handleDownload(
bibcode,
force,
search.setSearchResults
)
}
selectedPaper={library.selectedPaper}
setSelectedPaper={library.setSelectedPaper}
openReader={openReader}
setActiveTab={setActiveTab}
loadCitations={citations.loadCitations}
showAlert={showAlert}
onShowDetail={(paper) =>
library.setDetailBibcode(paper.bibcode)
}
/>
)}
<AnimatePresence mode="wait">
{activeTab === 'search' && (
<motion.div
key="search"
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -5 }}
transition={{ duration: 0.15 }}
className="w-full flex-1 flex flex-col min-h-0"
>
<SearchPanel
searchQuery={search.searchQuery}
setSearchQuery={search.setSearchQuery}
searchSource={search.searchSource}
setSearchSource={search.setSearchSource}
searchRows={search.searchRows}
searchStart={search.searchStart}
searchSort={search.searchSort}
handlePageChange={search.handlePageChange}
handleSortChange={search.handleSortChange}
handleRowsChange={search.handleRowsChange}
searching={search.searching}
handleSearch={search.handleSearch}
searchResults={search.searchResults}
exportingList={search.exportingList}
toggleExportItem={search.toggleExportItem}
handleExportBibtex={search.handleExportBibtex}
exporting={search.exporting}
bibtexContent={search.bibtexContent}
downloadingBibcodes={library.downloadingBibcodes}
handleDownload={(bibcode, force) =>
library.handleDownload(
bibcode,
force,
search.setSearchResults
)
}
selectedPaper={library.selectedPaper}
setSelectedPaper={library.setSelectedPaper}
openReader={openReader}
setActiveTab={setActiveTab}
loadCitations={citations.loadCitations}
showAlert={showAlert}
onShowDetail={(paper) =>
library.setDetailBibcode(paper.bibcode)
}
/>
</motion.div>
)}
{activeTab === 'library' && (
<LibraryPanel
library={library.library}
fetchLibrary={library.fetchLibrary}
setActiveTab={setActiveTab}
onShowDetail={(paper) =>
library.setDetailBibcode(paper.bibcode)
}
onOpenReader={openReader}
onOpenCitation={(paper) =>
library.openCitation(
paper,
setActiveTab,
citations.loadCitations
)
}
searchTerm={library.searchTerm}
setSearchTerm={library.setSearchTerm}
filterStatus={library.filterStatus}
setFilterStatus={library.setFilterStatus}
filterDoctype={library.filterDoctype}
setFilterDoctype={library.setFilterDoctype}
sortBy={library.sortBy}
setSortBy={library.setSortBy}
filterAuthor={library.filterAuthor}
setFilterAuthor={library.setFilterAuthor}
filterYear={library.filterYear}
setFilterYear={library.setFilterYear}
filterJournal={library.filterJournal}
setFilterJournal={library.setFilterJournal}
currentPage={library.currentPage}
setCurrentPage={library.setCurrentPage}
pageSize={library.pageSize}
setPageSize={library.setPageSize}
totalCount={library.totalCount}
loading={library.loading}
/>
)}
{activeTab === 'library' && (
<motion.div
key="library"
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -5 }}
transition={{ duration: 0.15 }}
className="w-full flex-1 flex flex-col min-h-0"
>
<LibraryPanel
library={library.library}
fetchLibrary={library.fetchLibrary}
setActiveTab={setActiveTab}
onShowDetail={(paper) =>
library.setDetailBibcode(paper.bibcode)
}
onOpenReader={openReader}
onOpenCitation={(paper) =>
library.openCitation(
paper,
setActiveTab,
citations.loadCitations
)
}
searchTerm={library.searchTerm}
setSearchTerm={library.setSearchTerm}
filterStatus={library.filterStatus}
setFilterStatus={library.setFilterStatus}
filterDoctype={library.filterDoctype}
setFilterDoctype={library.setFilterDoctype}
sortBy={library.sortBy}
setSortBy={library.setSortBy}
filterAuthor={library.filterAuthor}
setFilterAuthor={library.setFilterAuthor}
filterYear={library.filterYear}
setFilterYear={library.setFilterYear}
filterJournal={library.filterJournal}
setFilterJournal={library.setFilterJournal}
currentPage={library.currentPage}
setCurrentPage={library.setCurrentPage}
pageSize={library.pageSize}
setPageSize={library.setPageSize}
totalCount={library.totalCount}
loading={library.loading}
/>
</motion.div>
)}
{activeTab === 'reader' &&
(library.selectedPaper ? (
<ReaderPanel
selectedPaper={library.selectedPaper}
library={library.library}
recentlySelected={library.recentlySelected}
onSwitchPaper={openReader}
parsing={parsing}
handleParse={handleParse}
translating={translating}
handleTranslate={handleTranslate}
vectorizing={vectorizing}
handleVectorize={handleVectorize}
showNotesPanel={notes.showNotesPanel}
setShowNotesPanel={notes.setShowNotesPanel}
notes={notes.notes}
englishText={englishText}
chineseText={chineseText}
handleTextSelection={notes.handleTextSelection}
selectedParagraphIdx={notes.selectedParagraphIdx}
setSelectedParagraphIdx={notes.setSelectedParagraphIdx}
selectedText={notes.selectedText}
setSelectedText={notes.setSelectedText}
newNoteColor={notes.newNoteColor}
setNewNoteColor={notes.setNewNoteColor}
newNoteText={notes.newNoteText}
setNewNoteText={notes.setNewNoteText}
handleCreateNote={() =>
notes.handleCreateNote(library.selectedPaper)
}
handleDeleteNote={notes.handleDeleteNote}
showConfirm={showConfirm}
onJumpToSource={handleJumpToSource}
/>
) : (
<div className="w-full flex-1 flex flex-col items-center justify-center p-12 bg-white rounded-2xl border border-slate-200 shadow-xs min-h-[450px] select-none">
<div className="w-16 h-16 rounded-2xl bg-sky-50 border border-sky-100 flex items-center justify-center text-sky-600 mb-5 shadow-2xs">
<BookOpen className="w-8 h-8" />
</div>
<h3 className="text-base font-bold text-slate-800 tracking-wide mb-2">
</h3>
<p className="text-xs text-slate-500 max-w-sm text-center leading-relaxed mb-6">
"馆藏管理"
</p>
<button
onClick={() => setActiveTab('library')}
className="px-6 py-2.5 bg-sky-600 hover:bg-sky-700 text-white rounded-lg text-xs font-bold transition-all shadow-xs hover:shadow-md cursor-pointer hover:scale-102 active:scale-98"
>
</button>
</div>
))}
{activeTab === 'reader' && (
<motion.div
key="reader"
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -5 }}
transition={{ duration: 0.15 }}
className="w-full flex-1 flex flex-col min-h-0"
>
{library.selectedPaper ? (
<ReaderPanel
selectedPaper={library.selectedPaper}
library={library.library}
recentlySelected={library.recentlySelected}
onSwitchPaper={openReader}
parsing={parsing}
handleParse={handleParse}
translating={translating}
handleTranslate={handleTranslate}
vectorizing={vectorizing}
handleVectorize={handleVectorize}
showNotesPanel={notes.showNotesPanel}
setShowNotesPanel={notes.setShowNotesPanel}
notes={notes.notes}
englishText={englishText}
chineseText={chineseText}
handleTextSelection={notes.handleTextSelection}
selectedParagraphIdx={notes.selectedParagraphIdx}
setSelectedParagraphIdx={notes.setSelectedParagraphIdx}
selectedText={notes.selectedText}
setSelectedText={notes.setSelectedText}
newNoteColor={notes.newNoteColor}
setNewNoteColor={notes.setNewNoteColor}
newNoteText={notes.newNoteText}
setNewNoteText={notes.setNewNoteText}
handleCreateNote={() =>
notes.handleCreateNote(library.selectedPaper)
}
handleDeleteNote={notes.handleDeleteNote}
showConfirm={showConfirm}
onJumpToSource={handleJumpToSource}
/>
) : (
<div className="w-full flex-1 flex flex-col items-center justify-center p-12 bg-surface rounded-2xl min-h-[450px] select-none relative overflow-hidden">
<div
className="absolute inset-0 opacity-[0.03] dark:opacity-[0.05] pointer-events-none"
style={{
background:
'radial-gradient(circle at center, var(--color-blueprint) 0%, transparent 65%)',
}}
/>
<motion.div
animate={{ y: [0, -6, 0] }}
transition={{
repeat: Infinity,
duration: 4,
ease: 'easeInOut',
}}
className="w-16 h-16 rounded-2xl bg-blueprint/10 dark:bg-blueprint/15 border border-blueprint/20 flex items-center justify-center text-blueprint mb-5 relative z-10"
>
<BookOpen className="w-8 h-8" />
</motion.div>
<h3 className="text-base font-bold text-content tracking-wide mb-2 relative z-10">
</h3>
<p className="text-xs text-secondary max-w-sm text-center leading-relaxed mb-6 relative z-10 font-medium">
"馆藏管理"
</p>
<button
onClick={() => setActiveTab('library')}
className="btn-console btn-console-primary px-6 py-2.5 rounded-lg text-xs font-bold transition-all cursor-pointer hover:scale-105 active:scale-95 relative z-10"
>
</button>
</div>
)}
</motion.div>
)}
{activeTab === 'citation' &&
(library.selectedPaper ? (
<CitationPanel
selectedPaper={library.selectedPaper}
library={library.library}
recentlySelected={library.recentlySelected}
onSwitchPaper={(paper) =>
library.openCitation(
paper,
setActiveTab,
citations.loadCitations
)
}
loadingCitations={citations.loadingCitations}
citationNetwork={citations.citationNetwork}
citationHistory={citations.citationHistory}
loadCitations={citations.loadCitations}
onUncachedClick={citations.setUncachedBibcode}
/>
) : (
<div className="w-full flex-1 flex flex-col items-center justify-center p-12 bg-white rounded-2xl border border-slate-200 shadow-xs min-h-[450px] select-none">
<div className="w-16 h-16 rounded-2xl bg-sky-50 border border-sky-100 flex items-center justify-center text-sky-600 mb-5 shadow-2xs">
<GitFork className="w-8 h-8" />
</div>
<h3 className="text-base font-bold text-slate-800 tracking-wide mb-2">
</h3>
<p className="text-xs text-slate-500 max-w-sm text-center leading-relaxed mb-6">
"引用 -
被引""馆藏管理"
</p>
<button
onClick={() => setActiveTab('library')}
className="px-6 py-2.5 bg-sky-600 hover:bg-sky-700 text-white rounded-lg text-xs font-bold transition-all shadow-xs hover:shadow-md cursor-pointer hover:scale-102 active:scale-98"
>
</button>
</div>
))}
{activeTab === 'citation' && (
<motion.div
key="citation"
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -5 }}
transition={{ duration: 0.15 }}
className="w-full flex-1 flex flex-col min-h-0"
>
{library.selectedPaper ? (
<CitationPanel
selectedPaper={library.selectedPaper}
library={library.library}
recentlySelected={library.recentlySelected}
onSwitchPaper={(paper) =>
library.openCitation(
paper,
setActiveTab,
citations.loadCitations
)
}
loadingCitations={citations.loadingCitations}
citationNetwork={citations.citationNetwork}
citationHistory={citations.citationHistory}
loadCitations={citations.loadCitations}
onUncachedClick={citations.setUncachedBibcode}
/>
) : (
<div className="w-full flex-1 flex flex-col items-center justify-center p-12 bg-surface rounded-2xl min-h-[450px] select-none relative overflow-hidden">
<div
className="absolute inset-0 opacity-[0.03] dark:opacity-[0.05] pointer-events-none"
style={{
background:
'radial-gradient(circle at center, var(--color-blueprint) 0%, transparent 65%)',
}}
/>
<motion.div
animate={{ y: [0, -6, 0] }}
transition={{
repeat: Infinity,
duration: 4,
ease: 'easeInOut',
}}
className="w-16 h-16 rounded-2xl bg-blueprint/10 dark:bg-blueprint/15 border border-blueprint/20 flex items-center justify-center text-blueprint mb-5 relative z-10"
>
<GitFork className="w-8 h-8" />
</motion.div>
<h3 className="text-base font-bold text-content tracking-wide mb-2 relative z-10">
</h3>
<p className="text-xs text-secondary max-w-sm text-center leading-relaxed mb-6 relative z-10 font-medium">
"引用 -
被引""馆藏管理"
</p>
<button
onClick={() => setActiveTab('library')}
className="btn-console btn-console-primary px-6 py-2.5 rounded-lg text-xs font-bold transition-all cursor-pointer hover:scale-105 active:scale-95 relative z-10"
>
</button>
</div>
)}
</motion.div>
)}
{activeTab === 'sync' && <SyncPanel />}
{activeTab === 'sync' && (
<motion.div
key="sync"
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -5 }}
transition={{ duration: 0.15 }}
className="w-full flex-1 flex flex-col min-h-0"
>
<SyncPanel />
</motion.div>
)}
{activeTab === 'observation' && (
<ObservationPanel
capabilities={observation.capabilities}
currentSpec={observation.currentSpec}
searchForm={observation.searchForm}
setSearchForm={observation.setSearchForm}
searchResults={observation.searchResults}
searching={observation.searching}
searchError={observation.searchError}
runSearch={observation.runSearch}
selectedSourceIds={observation.selectedSourceIds}
toggleSelect={observation.toggleSelect}
selectAll={observation.selectAll}
selectNone={observation.selectNone}
downloading={observation.downloading}
downloadResult={observation.downloadResult}
downloadError={observation.downloadError}
setDownloadResult={observation.setDownloadResult}
downloadSelected={observation.downloadSelected}
downloadByIds={observation.downloadByIds}
downloadByCoordinates={observation.downloadByCoordinates}
unifiedResult={observation.unifiedResult}
unifiedSearching={observation.unifiedSearching}
unifiedError={observation.unifiedError}
runUnifiedSearch={observation.runUnifiedSearch}
resolveNames={observation.resolveNames}
unifiedSelected={observation.unifiedSelected}
toggleUnifiedSelect={observation.toggleUnifiedSelect}
downloadUnifiedSelected={observation.downloadUnifiedSelected}
libraryItems={observation.libraryItems}
libraryTotal={observation.libraryTotal}
libraryLoading={observation.libraryLoading}
libraryError={observation.libraryError}
fetchLibrary={observation.fetchLibrary}
libSource={observation.libSource}
setLibSource={observation.setLibSource}
libProduct={observation.libProduct}
setLibProduct={observation.setLibProduct}
libSearch={observation.libSearch}
setLibSearch={observation.setLibSearch}
libSort={observation.libSort}
setLibSort={observation.setLibSort}
libPage={observation.libPage}
setLibPage={observation.setLibPage}
libPageSize={observation.libPageSize}
setLibPageSize={observation.setLibPageSize}
resetLibraryFilters={observation.resetLibraryFilters}
/>
)}
{activeTab === 'observation' && (
<motion.div
key="observation"
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -5 }}
transition={{ duration: 0.15 }}
className="w-full flex-1 flex flex-col min-h-0"
>
<ObservationPanel
capabilities={observation.capabilities}
currentSpec={observation.currentSpec}
searchForm={observation.searchForm}
setSearchForm={observation.setSearchForm}
searchResults={observation.searchResults}
searching={observation.searching}
searchError={observation.searchError}
runSearch={observation.runSearch}
selectedSourceIds={observation.selectedSourceIds}
toggleSelect={observation.toggleSelect}
selectAll={observation.selectAll}
selectNone={observation.selectNone}
downloading={observation.downloading}
downloadResult={observation.downloadResult}
downloadError={observation.downloadError}
setDownloadResult={observation.setDownloadResult}
downloadSelected={observation.downloadSelected}
downloadByIds={observation.downloadByIds}
downloadByCoordinates={observation.downloadByCoordinates}
unifiedResult={observation.unifiedResult}
unifiedSearching={observation.unifiedSearching}
unifiedError={observation.unifiedError}
runUnifiedSearch={observation.runUnifiedSearch}
resolveNames={observation.resolveNames}
unifiedSelected={observation.unifiedSelected}
toggleUnifiedSelect={observation.toggleUnifiedSelect}
downloadUnifiedSelected={
observation.downloadUnifiedSelected
}
libraryItems={observation.libraryItems}
libraryTotal={observation.libraryTotal}
libraryLoading={observation.libraryLoading}
libraryError={observation.libraryError}
fetchLibrary={observation.fetchLibrary}
libSource={observation.libSource}
setLibSource={observation.setLibSource}
libProduct={observation.libProduct}
setLibProduct={observation.setLibProduct}
libSearch={observation.libSearch}
setLibSearch={observation.setLibSearch}
libSort={observation.libSort}
setLibSort={observation.setLibSort}
libPage={observation.libPage}
setLibPage={observation.setLibPage}
libPageSize={observation.libPageSize}
setLibPageSize={observation.setLibPageSize}
resetLibraryFilters={observation.resetLibraryFilters}
/>
</motion.div>
)}
{activeTab === 'agent' && (
<ResearchAgentPanel
showConfirm={showConfirm}
showAlert={showAlert}
openReader={openReader}
setActiveTab={setActiveTab}
citations={citations}
library={library}
/>
)}
{activeTab === 'agent' && (
<motion.div
key="agent"
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -5 }}
transition={{ duration: 0.15 }}
className="w-full flex-1 flex flex-col min-h-0"
>
<ResearchAgentPanel
showConfirm={showConfirm}
showAlert={showAlert}
openReader={openReader}
setActiveTab={setActiveTab}
citations={citations}
library={library}
/>
</motion.div>
)}
</AnimatePresence>
</ErrorBoundary>
</div>
</div>
@@ -730,6 +879,12 @@ export default function App() {
{/* 全局统一 Alert / Confirm 弹窗 */}
<GlobalDialog dialog={dialog} onClose={() => setDialog(null)} />
{/* 全局非阻塞 Toast 通知容器 */}
<ToastContainer
toasts={toasts}
onClose={(id) => setToasts((prev) => prev.filter((t) => t.id !== id))}
/>
{/* 未入库文献操作引导弹窗 */}
<UncachedPaperModal
bibcode={citations.uncachedBibcode}
+91 -50
View File
@@ -13,6 +13,7 @@ import {
Paperclip,
} from 'lucide-react';
import axios from 'axios';
import { motion } from 'framer-motion';
import {
AgentMarkdown,
useAgentSSE,
@@ -25,8 +26,9 @@ import {
routeToolCall,
routeToolResult,
routeTextDelta,
setGlobalTools,
} from './agent';
import type { SSEEventHandlers, TimelineItem } from './agent';
import type { SSEEventHandlers, TimelineItem, AgentToolMeta } from './agent';
interface RetrievalSource {
bibcode: string;
@@ -142,6 +144,14 @@ export function AIAssistantPanel({
});
}, [bibcode]);
// ── 初始化加载工具元数据 ──
useEffect(() => {
axios
.get<AgentToolMeta[]>('/api/chat/tools')
.then((res) => setGlobalTools(res.data))
.catch((e) => console.error('加载工具列表失败:', e));
}, []);
const handleStop = async () => {
sseStop();
if (sessionId) {
@@ -195,14 +205,31 @@ export function AIAssistantPanel({
)
);
},
onToolCall: (step, _id, name, args) => {
onToolCall: (step, _id, name, args, is_internal, display_name) => {
setMessages((prev) =>
updateAiTimeline(prev, (timeline) =>
routeToolCall(timeline, step, _id, name, args)
routeToolCall(
timeline,
step,
_id,
name,
args,
is_internal,
display_name
)
)
);
},
onToolResult: (_step, tcId, name, _output, isError, metadata) => {
onToolResult: (
_step,
tcId,
name,
_output,
isError,
metadata,
is_internal,
display_name
) => {
setExpandedResults((prev) => ({ ...prev, [tcId]: true }));
setMessages((prev) => {
if (prev.length === 0) return prev;
@@ -213,7 +240,10 @@ export function AIAssistantPanel({
tcId,
name,
_output,
isError
isError,
metadata,
is_internal,
display_name
);
// RAG 来源收集
const sources = [...(last.sources || [])];
@@ -336,12 +366,13 @@ export function AIAssistantPanel({
);
}
case 'tool_call': {
const tcId = item.id;
const tcId = item.id || `ai-tc-${item.step}-${idx}`;
return (
<ToolCallCard
key={`tc-${tcId}-${idx}`}
step={item.step}
name={item.name}
display_name={item.display_name}
arguments={item.arguments}
result={item.result}
isStreaming={isStreaming}
@@ -366,20 +397,20 @@ export function AIAssistantPanel({
<div
ref={scrollContainerRef}
onScroll={handleScroll}
className="flex-1 overflow-y-auto p-4 pb-28 space-y-4 min-h-0"
className="flex-1 overflow-y-auto p-4 pb-28 space-y-4 min-h-0 bg-sunken"
>
{messages.length === 0 ? (
<div className="py-6 space-y-6">
<div className="text-center space-y-2 max-w-sm mx-auto">
<Compass className="w-10 h-10 mx-auto text-sky-500 opacity-60" />
<h3 className="text-xs font-bold text-slate-800"></h3>
<p className="text-[11px] text-slate-500 leading-relaxed font-semibold">
<Compass className="w-10 h-10 mx-auto text-blueprint opacity-60" />
<h3 className="text-xs font-bold text-content"></h3>
<p className="text-[11px] text-secondary leading-relaxed font-semibold">
AI
</p>
</div>
<div className="space-y-2">
<span className="text-[10px] font-bold text-slate-400 tracking-wider uppercase">
<span className="text-[10px] font-bold text-tertiary tracking-wider uppercase">
:
</span>
<div className="space-y-2">
@@ -387,7 +418,7 @@ export function AIAssistantPanel({
<button
key={idx}
onClick={() => handleSend(q)}
className="w-full text-left bg-white hover:bg-slate-55 border border-slate-200 rounded-md p-3 text-xs text-slate-700 leading-relaxed font-medium transition-all cursor-pointer hover:border-slate-350"
className="w-full text-left bg-surface hover:bg-sunken rounded-md p-3 text-xs text-secondary leading-relaxed font-medium transition-all cursor-pointer hover:border-subtle"
>
{q}
</button>
@@ -397,18 +428,23 @@ export function AIAssistantPanel({
</div>
) : (
messages.map((msg, index) => (
<div
<motion.div
key={index}
className={`flex flex-col ${msg.sender === 'user' ? 'items-end' : 'items-start'} space-y-1.5`}
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25, ease: 'easeOut' }}
className={`flex flex-col ${msg.sender === 'user' ? 'items-end' : 'items-start'} space-y-1.5 w-full`}
>
<span className="text-[10px] font-bold text-slate-400 px-1">
{msg.sender === 'user' ? '我' : '天文科研助手'}
</span>
{msg.sender !== 'user' && (
<span className="text-[10px] font-bold text-tertiary px-1">
</span>
)}
<div
className={`max-w-[95%] rounded-lg px-4 py-3 text-xs leading-relaxed font-medium shadow-xs border ${
className={`max-w-[95%] rounded-lg px-4 py-3 text-xs leading-relaxed font-medium ${
msg.sender === 'user'
? 'bg-blueprint text-white border-blueprint select-text'
: 'bg-white text-slate-800 border-slate-250 select-text'
? 'bg-blueprint/10 text-content dark:bg-blueprint/15 select-text'
: 'border border-subtle bg-surface text-content select-text'
}`}
>
{msg.sender === 'user' ? (
@@ -426,12 +462,12 @@ export function AIAssistantPanel({
<div className="space-y-2.5">
{/* 时间线条目(思考/工具调用/子代理容器)*/}
{msg.timeline.length > 0 && (
<div className="border border-slate-205 bg-slate-100/30 rounded-md p-2 select-none">
<div className="text-[10px] font-bold text-slate-600 flex items-center gap-1.5 mb-2 px-1">
<Brain className="w-3.5 h-3.5 text-slate-450" />
<div className=" bg-sunken/60 rounded-md p-2 select-none">
<div className="text-[10px] font-bold text-secondary flex items-center gap-1.5 mb-2 px-1">
<Brain className="w-3.5 h-3.5 text-tertiary" />
<span> ({msg.timeline.length} )</span>
</div>
<div className="pl-4 border-l border-slate-200 space-y-2">
<div className="pl-4 border-l border-subtle space-y-2">
{msg.timeline.map((item, tIdx) =>
renderTimelineItem(item, tIdx, !!streaming)
)}
@@ -441,12 +477,12 @@ export function AIAssistantPanel({
{/* 最终回答 */}
{msg.text ? (
<div className="prose prose-sm max-w-none text-slate-800 leading-relaxed prose-headings:text-slate-900 prose-headings:font-bold prose-strong:text-slate-900 prose-code:text-sky-700 prose-img:rounded-lg">
<div className="prose prose-sm max-w-none text-content leading-relaxed prose-headings:text-content prose-headings:font-bold prose-strong:text-content prose-code:text-blueprint prose-img:rounded-lg">
<AgentMarkdown>{msg.text}</AgentMarkdown>
</div>
) : (
!streaming && (
<span className="text-slate-400 italic">
<span className="text-tertiary italic">
...
</span>
)
@@ -457,8 +493,8 @@ export function AIAssistantPanel({
{/* 关联来源 */}
{msg.sender === 'ai' && msg.sources && msg.sources.length > 0 && (
<div className="w-full mt-2 pl-2 space-y-1.5 border-l-2 border-slate-200">
<span className="text-[9px] font-bold text-slate-400 select-none">
<div className="w-full mt-2 pl-2 space-y-1.5 border-l-2 border-subtle">
<span className="text-[9px] font-bold text-tertiary select-none">
:
</span>
<div className="grid grid-cols-1 gap-1.5 w-[95%]">
@@ -468,19 +504,19 @@ export function AIAssistantPanel({
onClick={() =>
onJumpToSource(src.bibcode, src.paragraph_index)
}
className="flex items-center justify-between text-left bg-white hover:bg-slate-100 border border-slate-200 rounded-md px-2.5 py-1.5 text-[10px] text-slate-655 font-semibold transition-all shadow-sm cursor-pointer hover:border-sky-300"
className="flex items-center justify-between text-left bg-surface hover:bg-sunken rounded-md px-2.5 py-1.5 text-[10px] text-secondary font-semibold transition-all shadow-sm cursor-pointer hover:border-blueprint/40"
title={src.content}
>
<div className="flex items-center gap-1.5 truncate mr-2">
<BookOpen className="w-3 h-3 text-sky-600 shrink-0" />
<span className="font-bold text-slate-700 truncate">
<BookOpen className="w-3 h-3 text-blueprint shrink-0" />
<span className="font-bold text-secondary truncate">
{src.bibcode}
</span>
<span className="text-slate-400 text-[9px] font-bold">
<span className="text-tertiary text-[9px] font-bold">
§{src.paragraph_index + 1}
</span>
</div>
<span className="text-slate-400 text-[9px] shrink-0 font-medium">
<span className="text-tertiary text-[9px] shrink-0 font-medium">
: {src.distance.toFixed(3)}
</span>
</button>
@@ -488,24 +524,29 @@ export function AIAssistantPanel({
</div>
</div>
)}
</div>
</motion.div>
))
)}
{streaming &&
messages.length > 0 &&
!messages[messages.length - 1].text && (
<div className="flex items-center space-x-2 text-slate-500 pl-2">
<Loader className="w-4 h-4 animate-spin text-sky-600" />
<span className="text-[10px] font-bold">
...
<div className="flex items-center space-x-2.5 text-secondary pl-2 py-1 select-none">
<Loader className="w-3.5 h-3.5 animate-spin text-blueprint" />
<span className="text-[10px] font-bold tracking-wide">
</span>
<span className="flex gap-0.5">
<span className="w-1 h-1 rounded-full bg-blueprint animate-bounce [animation-delay:-0.3s]"></span>
<span className="w-1 h-1 rounded-full bg-blueprint animate-bounce [animation-delay:-0.15s]"></span>
<span className="w-1 h-1 rounded-full bg-blueprint animate-bounce"></span>
</span>
</div>
)}
{error && (
<div className="flex items-start gap-2 bg-red-50 border border-red-200 text-red-700 rounded-lg p-3 text-xs w-[95%] font-semibold">
<AlertCircle className="w-4 h-4 text-red-500 shrink-0 mt-0.5" />
<div className="flex items-start gap-2 bg-red-50 border border-red-200 text-red-700 dark:bg-rose-500/10 dark:text-rose-300 dark:border-rose-500/30 rounded-lg p-3 text-xs w-[95%] font-semibold">
<AlertCircle className="w-4 h-4 text-danger shrink-0 mt-0.5" />
<div>
<div className="font-bold"></div>
<div className="text-[10px] opacity-80 mt-0.5">{error}</div>
@@ -522,9 +563,9 @@ export function AIAssistantPanel({
e.preventDefault();
handleSend(input);
}}
className="absolute bottom-0 left-0 right-0 p-3 bg-transparent pointer-events-none shrink-0"
className="absolute bottom-0 left-0 right-0 px-3 pb-0 pt-3 bg-transparent pointer-events-none shrink-0"
>
<div className="bg-slate-50 border border-slate-300 rounded-lg p-2.5 focus-within:bg-white focus-within:border-blueprint focus-within:ring-1 focus-within:ring-blueprint/10 transition-all relative pointer-events-auto shadow-sm flex flex-col gap-2">
<div className="bg-sunken border border-default rounded-lg p-2.5 focus-within:bg-surface focus-within:border-blueprint focus-within:ring-1 focus-within:ring-blueprint/10 transition-all relative pointer-events-auto shadow-sm flex flex-col gap-2">
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
@@ -540,9 +581,9 @@ export function AIAssistantPanel({
pendingImage ? '针对选中图表提问...' : '向 AI 馆藏助手提问...'
}
rows={2}
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-[36px] max-h-32 pb-1"
className="w-full bg-transparent resize-none border-none outline-none focus:outline-none focus:ring-0 text-xs text-content placeholder-tertiary leading-relaxed font-semibold min-h-[36px] max-h-32 pb-1"
/>
<div className="flex justify-between items-center mt-1.5 pt-1.5 border-t border-slate-200/40">
<div className="flex justify-between items-center mt-1.5 pt-1.5 border-t border-subtle">
<div className="flex gap-2 items-center min-w-0">
<input
ref={fileInputRef}
@@ -562,8 +603,8 @@ export function AIAssistantPanel({
title="上传或粘贴图片(也可直接 Ctrl+V 粘贴)"
className={`p-1 rounded-md border text-[10px] font-bold transition-all cursor-pointer flex items-center justify-center shrink-0 ${
pendingImage
? 'bg-sky-50 border-sky-300 text-sky-700'
: 'bg-white border-slate-200 text-slate-400 hover:text-sky-600 hover:border-sky-200'
? 'bg-sky-50 border-sky-300 text-sky-700 dark:bg-sky-500/10 dark:text-sky-300 dark:border-sky-500/30'
: 'bg-surface border-subtle text-tertiary hover:text-blueprint hover:border-blueprint/20'
} disabled:opacity-60`}
>
<Paperclip className="w-3.5 h-3.5" />
@@ -577,12 +618,12 @@ export function AIAssistantPanel({
: `data:${pendingImage.mime_type};base64,${pendingImage.data}`
}
alt="Preview"
className="w-5 h-5 object-cover rounded border border-slate-300 shadow-2xs"
className="w-5 h-5 object-cover rounded shadow-sm"
/>
<button
type="button"
onClick={() => setPendingImage(null)}
className="absolute -top-1 -right-1 bg-slate-500/80 hover:bg-slate-700 text-white rounded-full w-3 h-3 shadow-xs transition-colors cursor-pointer flex items-center justify-center"
className="absolute -top-1 -right-1 bg-slate-500/80 hover:bg-slate-700 text-white rounded-full w-3 h-3 transition-colors cursor-pointer flex items-center justify-center"
title="移除图片"
>
<X className="w-2 h-2" />
@@ -595,7 +636,7 @@ export function AIAssistantPanel({
<button
type="button"
onClick={handleStop}
className="p-1 rounded-lg bg-slate-800 hover:bg-slate-950 text-white transition-colors cursor-pointer flex items-center justify-center shadow-xs"
className="p-1 rounded-lg bg-elevated hover:bg-slate-950 text-white transition-colors cursor-pointer flex items-center justify-center"
title="手动停止执行"
>
<Square className="w-3.5 h-3.5" />
@@ -604,7 +645,7 @@ export function AIAssistantPanel({
<button
type="submit"
disabled={!input.trim() && !pendingImage}
className="p-1 px-2.5 rounded-lg bg-sky-600 hover:bg-sky-700 text-white disabled:bg-slate-100 disabled:text-slate-350 transition-colors cursor-pointer flex items-center justify-center gap-1 shadow-xs text-[10px] font-bold"
className="p-1 px-2.5 rounded-lg bg-blueprint hover:opacity-90 text-white disabled:bg-sunken disabled:text-tertiary transition-colors cursor-pointer flex items-center justify-center gap-1 text-[10px] font-bold"
>
<Send className="w-3.5 h-3.5" />
<span></span>
@@ -1,6 +1,7 @@
// dashboard/src/components/CitationGalaxyCanvas.tsx
import { useEffect, useRef } from 'react';
import type { CitationNetwork } from '../types';
import { useTheme } from '../hooks/useTheme';
interface CanvasProps {
networks: CitationNetwork[];
@@ -9,6 +10,24 @@ interface CanvasProps {
onNodeClick: (bibcode: string) => void;
}
// 从 CSS 令牌读取当前主题的 canvas 配色(随浅色/暗色切换)
function readCanvasColors() {
const cs = getComputedStyle(document.documentElement);
const get = (k: string, fallback: string) =>
cs.getPropertyValue(k).trim() || fallback;
return {
active: get('--canvas-active', '#0369a1'),
inactive: get('--canvas-inactive', '#475569'),
reference: get('--canvas-reference', '#b45309'),
citation: get('--canvas-citation', '#0e7490'),
link: get('--canvas-link', 'rgba(203, 213, 225, 0.22)'),
ring: get('--canvas-ring', 'rgba(148, 163, 184, 0.08)'),
pulse: get('--canvas-pulse', 'rgba(3, 105, 161, 0.025)'),
label: get('--canvas-label', '#475569'),
labelHover: get('--canvas-label-hover', '#0369a1'),
};
}
interface Node {
id: string;
label: string;
@@ -34,6 +53,7 @@ export function CitationGalaxyCanvas({
onNodeClick,
}: CanvasProps) {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const { theme } = useTheme();
useEffect(() => {
const canvas = canvasRef.current;
@@ -47,6 +67,9 @@ export function CitationGalaxyCanvas({
canvas.height = rect.height * dpr;
ctx.scale(dpr, dpr);
// 读取当前主题的 canvas 配色(浅色/暗色自适应)
const C = readCanvasColors();
const MAX_NODES = nodeLimit;
const allIds = new Set<string>();
const nodes: Node[] = [];
@@ -69,7 +92,7 @@ export function CitationGalaxyCanvas({
vx: 0,
vy: 0,
radius,
color: isActive ? '#0369a1' : '#475569',
color: isActive ? C.active : C.inactive,
type: 'center',
inDb: true,
});
@@ -97,7 +120,7 @@ export function CitationGalaxyCanvas({
vx: 0,
vy: 0,
radius: 8, // 初始值,稍后按连线数重算
color: '#b45309',
color: C.reference,
type: 'reference',
inDb,
});
@@ -129,7 +152,7 @@ export function CitationGalaxyCanvas({
vx: 0,
vy: 0,
radius: 8, // 初始值,稍后按连线数重算
color: '#0e7490',
color: C.citation,
type: 'citation',
inDb,
});
@@ -256,7 +279,7 @@ export function CitationGalaxyCanvas({
// 绘制背景宇宙引力线 & 刻度圈 - 极其素雅的学术引力线
const centerNode = nodes.find((n) => n.id === activeNetwork.bibcode);
if (centerNode) {
ctx.strokeStyle = 'rgba(148, 163, 184, 0.08)';
ctx.strokeStyle = C.ring;
ctx.lineWidth = 0.75;
ctx.setLineDash([3, 5]);
@@ -269,7 +292,7 @@ export function CitationGalaxyCanvas({
ctx.stroke();
// 动态引力波圈 - 调低对比度,使其呈极其微弱安静的向外平滑脉冲
ctx.strokeStyle = 'rgba(3, 105, 161, 0.025)';
ctx.strokeStyle = C.pulse;
ctx.setLineDash([]);
const pulseRadius = 130 + (frameCount % 120) * 0.4;
ctx.beginPath();
@@ -286,7 +309,7 @@ export function CitationGalaxyCanvas({
ctx.beginPath();
ctx.moveTo(sourceNode.x, sourceNode.y);
ctx.lineTo(targetNode.x, targetNode.y);
ctx.strokeStyle = 'rgba(203, 213, 225, 0.22)';
ctx.strokeStyle = C.link;
ctx.stroke();
}
});
@@ -327,7 +350,7 @@ export function CitationGalaxyCanvas({
ctx.lineWidth = 0.75;
ctx.stroke();
ctx.fillStyle = isHovered ? '#0369a1' : '#475569';
ctx.fillStyle = isHovered ? C.labelHover : C.label;
ctx.font = isHovered ? 'bold 9.5px sans-serif' : '9px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(
@@ -531,7 +554,7 @@ export function CitationGalaxyCanvas({
canvas.removeEventListener('touchmove', handleTouchMove);
canvas.removeEventListener('touchend', handleTouchEnd);
};
}, [networks, activeNetwork, nodeLimit, onNodeClick]);
}, [networks, activeNetwork, nodeLimit, onNodeClick, theme]);
return (
<canvas
+6 -6
View File
@@ -53,22 +53,22 @@ export function CustomSelect<T extends string | number = string | number>({
type="button"
disabled={disabled}
onClick={() => setIsOpen(!isOpen)}
className={`w-full flex items-center justify-between gap-1.5 bg-white border border-slate-250 hover:border-slate-350 rounded-md text-xs font-semibold text-slate-700 transition-all text-left outline-none cursor-pointer ${
className={`w-full flex items-center justify-between gap-1.5 bg-surface border border-default hover:border-border-strong rounded-md text-xs font-semibold text-secondary transition-all text-left outline-none cursor-pointer ${
size === 'sm' ? 'px-2 py-0.5' : 'px-2.5 py-2'
} ${
isOpen ? 'border-blueprint ring-1 ring-blueprint bg-white' : ''
} ${disabled ? 'bg-slate-50 text-slate-450 cursor-not-allowed border-slate-200' : ''}`}
isOpen ? 'border-blueprint ring-1 ring-blueprint bg-surface' : ''
} ${disabled ? 'bg-sunken text-tertiary cursor-not-allowed border-subtle' : ''}`}
>
<span className="truncate">{selectedOption?.label}</span>
<ChevronDown
className={`w-3.5 h-3.5 text-slate-400 transition-transform duration-200 shrink-0 ${
className={`w-3.5 h-3.5 text-tertiary transition-transform duration-200 shrink-0 ${
isOpen ? 'rotate-180 text-blueprint' : ''
}`}
/>
</button>
{isOpen && !disabled && (
<div className="absolute left-0 mt-1.5 w-full min-w-[80px] bg-white border border-slate-200 rounded-md shadow-md py-1 z-50 max-h-60 overflow-y-auto scrollbar-thin">
<div className="absolute left-0 mt-1.5 w-full min-w-[80px] bg-elevated rounded-md shadow-md py-1 z-50 max-h-60 overflow-y-auto scrollbar-thin">
{options.map((option) => {
const isSelected = option.value === value;
return (
@@ -84,7 +84,7 @@ export function CustomSelect<T extends string | number = string | number>({
} ${
isSelected
? 'bg-blueprint/5 text-blueprint font-bold'
: 'text-slate-600 hover:bg-slate-50 hover:text-slate-900 font-medium'
: 'text-secondary hover:bg-sunken hover:text-content font-medium'
}`}
>
{option.label}
+5 -7
View File
@@ -36,19 +36,17 @@ export class ErrorBoundary extends Component<Props, State> {
}
return (
<div className="flex flex-col items-center justify-center p-8 bg-white rounded-xl border border-slate-200 shadow-sm min-h-[300px]">
<div className="w-12 h-12 rounded-full bg-red-50 border border-red-100 flex items-center justify-center text-red-500 mb-4">
<div className="flex flex-col items-center justify-center p-8 bg-surface rounded-xl shadow-sm min-h-[300px]">
<div className="w-12 h-12 rounded-full bg-red-50 border border-red-100 dark:bg-rose-500/10 dark:border-rose-500/30 flex items-center justify-center text-red-500 dark:text-rose-400 mb-4">
<AlertTriangle className="w-6 h-6" />
</div>
<h3 className="text-sm font-bold text-slate-800 mb-2">
</h3>
<p className="text-xs text-slate-500 text-center max-w-sm mb-4">
<h3 className="text-sm font-bold text-content mb-2"></h3>
<p className="text-xs text-secondary text-center max-w-sm mb-4">
{this.state.error?.message || '发生了未知错误'}
</p>
<button
onClick={this.handleRetry}
className="flex items-center gap-2 px-4 py-2 bg-slate-100 hover:bg-slate-200 text-slate-700 rounded-lg text-xs font-semibold transition-colors cursor-pointer"
className="flex items-center gap-2 px-4 py-2 bg-sunken hover:bg-sunken text-secondary rounded-lg text-xs font-semibold transition-colors cursor-pointer"
>
<RefreshCw className="w-3.5 h-3.5" />
+6 -6
View File
@@ -16,27 +16,27 @@ export function Logo({
xmlns="http://www.w3.org/2000/svg"
className={className}
>
{/* Outer Orbit Ring (Light Blue) */}
{/* Outer Orbit Ring (Adaptive) */}
<circle
cx="24"
cy="24"
r="21.5"
stroke="#38bdf8"
stroke="currentColor"
strokeWidth="1.2"
strokeOpacity="0.6"
strokeOpacity="0.4"
strokeDasharray="2 2"
/>
{/* Secondary Ellipse Orbit (Light Blue, tilted along top-left to bottom-right diagonal) */}
{/* Secondary Ellipse Orbit (Adaptive, tilted along top-left to bottom-right diagonal) */}
<ellipse
cx="24"
cy="24"
rx="21"
ry="8.5"
transform="rotate(36 24 24)"
stroke="#0284c7"
stroke="currentColor"
strokeWidth="1.2"
strokeOpacity="0.5"
strokeOpacity="0.35"
/>
{/* Main Ellipse Orbit (Adaptive Color, tilted along bottom-left to top-right diagonal, major axis slightly larger than outer circle diameter) */}
+15 -18
View File
@@ -29,19 +29,19 @@ export function PaperCard({
return (
<div
onClick={onClick}
className="console-panel p-5 rounded-lg border border-slate-200 hover:border-slate-400 bg-white flex flex-col justify-between relative overflow-hidden group transition-all cursor-pointer shadow-sm select-none"
className="console-panel p-5 rounded-lg flex flex-col justify-between relative overflow-hidden group cursor-pointer select-none"
>
{/* Top Right Corner Status Badge */}
{statusBadge}
<div className="pr-10">
<div>
<h3 className="font-bold text-xs text-slate-900 line-clamp-2 hover:text-blueprint transition-all leading-relaxed">
<h3 className="font-bold text-xs text-content line-clamp-2 hover:text-blueprint transition-all leading-relaxed">
{getDoctypeBadge(paper.doctype)}
<span className="align-middle">{paper.title}</span>
</h3>
</div>
<div className="text-[10px] text-slate-500 font-semibold mt-1.5 uppercase">
<div className="text-[10px] text-secondary font-semibold mt-1.5 uppercase">
: {paper.authors.slice(0, 2).join(', ')}
{paper.authors.length > 2 ? ' 等' : ''} | : {paper.year}
</div>
@@ -49,7 +49,7 @@ export function PaperCard({
{/* Footer Area */}
{(footerLeft || footerRight) && (
<div className="flex items-center justify-between border-t border-slate-100 pt-3 mt-4 text-[10px]">
<div className="flex items-center justify-between border-t border-subtle pt-3 mt-4 text-[10px]">
<div className="flex flex-col gap-1 flex-1 min-w-0">
{footerLeft}
</div>
@@ -67,36 +67,33 @@ export function PaperCard({
// default 'list' variant
return (
<div
className={`console-panel p-6 rounded-lg border transition-all relative ${
isSelected
? 'border-slate-800 bg-slate-50/30'
: 'border-slate-200 hover:border-slate-350 hover:bg-white'
className={`console-panel p-6 rounded-lg relative ${
isSelected ? '!border-blueprint !bg-blueprint/5' : ''
}`}
>
{/* Selection indicator side bar */}
{isSelected && (
<div className="absolute top-0 left-0 w-1.5 h-full bg-slate-800 rounded-l-lg" />
<div className="absolute top-0 left-0 w-1.5 h-full bg-blueprint rounded-l-lg" />
)}
<div className="flex flex-col md:flex-row md:justify-between md:items-start gap-4 mb-3">
<div className="flex-1">
<h3
className="font-bold text-sm text-slate-900 hover:text-slate-800 cursor-pointer transition-all line-clamp-2 leading-relaxed"
className="font-bold text-sm text-content hover:text-content cursor-pointer transition-all line-clamp-2 leading-relaxed"
onClick={onClick}
>
{getDoctypeBadge(paper.doctype)}
<span className="align-middle">{paper.title}</span>
</h3>
<p className="text-xs text-slate-500 font-medium mt-2 leading-snug">
<p className="text-xs text-secondary font-medium mt-2 leading-snug">
:{' '}
<span className="text-slate-800">
<span className="text-content">
{paper.authors.slice(0, 5).join(', ')}
{paper.authors.length > 5 ? ' 等' : ''}
</span>{' '}
:{' '}
<span className="text-slate-850 font-bold">{paper.year}</span>
:{' '}
<span className="italic text-slate-700">
: <span className="text-content font-bold">{paper.year}</span>{' '}
:{' '}
<span className="italic text-secondary">
{paper.pub_journal || '未标注'}
</span>
</p>
@@ -113,10 +110,10 @@ export function PaperCard({
{/* Footer Area */}
{(footerLeft || footerRight) && (
<div className="flex flex-col sm:flex-row justify-between items-stretch sm:items-center border-t border-slate-100 pt-4 gap-3">
<div className="flex flex-col sm:flex-row justify-between items-stretch sm:items-center border-t border-subtle pt-4 gap-3">
{footerLeft && <div className="flex gap-2">{footerLeft}</div>}
{footerRight && (
<div className="text-xs text-slate-450 flex flex-wrap gap-x-4 gap-y-1 font-mono">
<div className="text-xs text-tertiary flex flex-wrap gap-x-4 gap-y-1 font-mono">
{footerRight}
</div>
)}
@@ -70,7 +70,7 @@ export function AgentInputArea({
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">
<div className="absolute bottom-0 left-0 right-0 px-4 pb-0 pt-4 bg-transparent pointer-events-none shrink-0 z-30 flex flex-col gap-3">
<form
onSubmit={(e) => {
e.preventDefault();
@@ -78,7 +78,7 @@ export function AgentInputArea({
}}
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">
<div className="bg-sunken border border-default rounded-lg p-2.5 focus-within:bg-surface focus-within:border-blueprint focus-within:ring-1 focus-within:ring-blueprint/10 transition-all flex flex-col gap-2">
{/* 图片预览 */}
{pendingImage && (
<div className="relative inline-block mb-1 group select-none self-start">
@@ -89,12 +89,12 @@ export function AgentInputArea({
: `data:${pendingImage.mime_type};base64,${pendingImage.data}`
}
alt="Preview"
className="w-12 h-12 object-cover rounded-lg border border-slate-200 shadow-sm"
className="w-12 h-12 object-cover rounded-lg"
/>
<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"
className="absolute -top-1.5 -right-1.5 bg-slate-500/80 hover:bg-slate-700 text-white rounded-full p-0.5 transition-colors cursor-pointer flex items-center justify-center"
title="移除图片"
>
<X className="w-2.5 h-2.5" />
@@ -114,9 +114,9 @@ export function AgentInputArea({
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"
className="w-full bg-transparent resize-none border-none outline-none focus:outline-none focus:ring-0 text-xs text-content placeholder-tertiary 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 justify-between items-center mt-2.5 pt-2 border-t border-subtle">
<div className="flex flex-wrap gap-2 items-center">
{/* 图片上传 */}
<input
@@ -138,14 +138,14 @@ export function AgentInputArea({
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'
: 'bg-surface border-subtle text-tertiary 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">
<div className="flex gap-1 bg-sunken p-0.5 rounded-lg ">
{agentModes.map((m) => {
const IconComp = getIconComponent(m.icon);
const isSelected = agentMode === m.id;
@@ -158,8 +158,8 @@ export function AgentInputArea({
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'
? 'bg-surface text-blueprint border-subtle'
: 'bg-transparent border-transparent text-tertiary hover:text-blueprint'
} disabled:opacity-60`}
>
<IconComp
@@ -174,7 +174,7 @@ export function AgentInputArea({
{/* 思考模式开关 */}
{agentMode === 'default' && (
<>
<div className="h-4 w-px bg-slate-200 mx-1 hidden sm:block" />
<div className="h-4 w-px bg-border-subtle mx-1 hidden sm:block" />
<button
type="button"
onClick={() => setThinking(!thinking)}
@@ -186,8 +186,8 @@ export function AgentInputArea({
}
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'
? 'bg-blueprint/5 border-blueprint/30 text-blueprint'
: 'bg-surface border-subtle text-tertiary hover:text-blueprint hover:border-blueprint/20'
} disabled:opacity-60`}
>
<Brain
@@ -204,7 +204,7 @@ export function AgentInputArea({
<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"
className="p-2 rounded-md bg-elevated hover:bg-slate-950 text-white transition-colors cursor-pointer flex items-center justify-center"
title="手动停止执行"
>
<Square className="w-3.5 h-3.5 fill-white" />
@@ -213,7 +213,7 @@ export function AgentInputArea({
<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"
className="p-2 rounded-md bg-blueprint hover:opacity-90 text-white disabled:bg-sunken disabled:text-tertiary transition-colors cursor-pointer flex items-center justify-center"
>
<Send className="w-3.5 h-3.5" />
</button>
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect } from 'react';
import {
PanelLeftOpen,
BarChart3,
@@ -12,15 +12,20 @@ import {
RotateCcw,
RefreshCw,
GitBranch,
X,
Sliders,
ListTodo,
} from 'lucide-react';
import { AgentMarkdown } from './AgentMarkdown';
import { ThoughtCard } from './ThoughtCard';
import { ToolCallCard } from './ToolCallCard';
import { TodoTaskCard } from './SpecialToolRenderers';
import { AnswerCard } from './AnswerCard';
import { SubAgentContainer } from './SubAgentContainer';
import { PARENT_COLORS } from './constants';
import type { ColorScheme } from './constants';
import type { TimelineItem } from './types';
import { getToolMeta } from './index';
import type {
ProcessedTurn,
@@ -98,8 +103,18 @@ interface AgentMessageListProps {
setActiveTab?: (tab: TabId) => void;
citations?: ReturnType<typeof useCitations>;
library?: ReturnType<typeof useLibrary>;
showTaskSidebar?: boolean;
setShowTaskSidebar?: (val: boolean) => void;
hasTodos?: boolean;
activeTodos?: any[];
}
// 内部工具(is_internal = true)渲染模式配置:
// - 'full': 开发阶段以完整卡片模式展示,方便调试与排错
// - 'micro': 以微缩徽标模式展示
// - 'hidden': 彻底隐藏,不在聊天时间线中展示
const INTERNAL_TOOL_RENDER_MODE: 'full' | 'micro' | 'hidden' = 'full';
export function AgentMessageList({
turns,
activeTurn,
@@ -139,7 +154,32 @@ export function AgentMessageList({
setActiveTab,
citations,
library,
showTaskSidebar,
setShowTaskSidebar,
hasTodos,
activeTodos,
}: AgentMessageListProps) {
const [isSessionControlExpanded, setIsSessionControlExpanded] =
React.useState(false);
// 点击外部自动关闭指标或审计日志面板(避开对切换按钮本身的点击判定)
useEffect(() => {
if (!showMetrics && !showAuditLog) return;
const handleClickOutside = (e: MouseEvent) => {
const target = e.target as HTMLElement;
const isInsidePanel = target.closest('.absolute.left-4.right-4.top-2');
const isTriggerButton = target.closest('.panel-trigger');
if (!isInsidePanel && !isTriggerButton) {
setShowMetrics(false);
setShowAuditLog(false);
}
};
document.addEventListener('click', handleClickOutside);
return () => document.removeEventListener('click', handleClickOutside);
}, [showMetrics, showAuditLog, setShowMetrics, setShowAuditLog]);
const renderTimelineItem = (
item: TimelineItem,
idx: number,
@@ -180,19 +220,59 @@ export function AgentMessageList({
);
}
case 'tool_call': {
// 如果是系统级内部工具,并且执行没有出错,则直接在前端隐藏,避免聊天时间线过于嘈杂
if (
SYSTEM_INTERNAL_TOOLS.includes(item.name) &&
!item.result?.isError
) {
return null;
const meta = getToolMeta(item.name);
const isInternal =
meta?.is_internal ||
item.is_internal ||
SYSTEM_INTERNAL_TOOLS.includes(item.name);
const displayName =
meta?.display_name || item.display_name || item.name;
// 如果是系统级内部工具,并且执行没有出错
if (isInternal && !item.result?.isError) {
if (INTERNAL_TOOL_RENDER_MODE === 'hidden') {
return null; // 彻底不显示
}
if (INTERNAL_TOOL_RENDER_MODE === 'micro') {
const isPending = !item.result;
return (
<div
key={`tc-internal-${item.id || idx}`}
className="flex items-center gap-2 py-0.5 px-2.5 rounded-full bg-sunken/40 dark:bg-sunken/20 text-[10px] text-tertiary select-none w-fit font-mono my-0.5 ml-1 animate-in fade-in slide-in-from-left-1 duration-200"
>
{isPending ? (
<>
<span className="relative flex h-1.5 w-1.5 shrink-0">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-blueprint opacity-75"></span>
<span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-blueprint"></span>
</span>
<span className="font-extrabold text-content">
{displayName}
</span>
<span className="text-[9px] text-blueprint/90 animate-pulse font-bold">
</span>
</>
) : (
<>
<span className="h-1.5 w-1.5 rounded-full bg-secondary shrink-0" />
<span className="text-secondary/80">{displayName}</span>
<span className="text-[9px] text-secondary/40 font-medium">
</span>
</>
)}
</div>
);
}
// 'full' 模式下,直接继续往下执行,渲染为完整的 ToolCallCard 样式
}
const tcId = item.id;
const tcId = item.id || `tc-${item.step}-${idx}`;
return (
<ToolCallCard
key={`tc-${tcId}-${idx}`}
step={item.step}
name={item.name}
display_name={displayName}
arguments={item.arguments}
result={item.result}
isStreaming={isStreaming}
@@ -222,27 +302,27 @@ export function AgentMessageList({
};
return (
<div className="flex-1 flex flex-col overflow-hidden bg-white relative">
<div className="flex-1 flex flex-col overflow-hidden bg-surface relative">
{/* 会话头部 */}
<div className="px-5 py-4 border-b border-slate-200 flex items-center justify-between shrink-0 bg-white">
<div className="px-5 py-4 flex items-center justify-between shrink-0 bg-surface">
<div className="min-w-0 pr-4 flex items-center gap-2">
{sidebarCollapsed && (
<button
onClick={() => setSidebarCollapsed(false)}
className="p-1.5 rounded-lg border border-slate-200 bg-white hover:bg-slate-50 text-slate-500 hover:text-slate-700 transition-all cursor-pointer mr-1 shrink-0"
className="p-1.5 rounded-lg bg-surface hover:bg-sunken text-secondary hover:text-secondary transition-all cursor-pointer mr-1 shrink-0"
title="展开侧栏"
>
<PanelLeftOpen className="w-4 h-4" />
</button>
)}
<div>
<h3 className="text-xs font-bold text-slate-800 tracking-wide line-clamp-1">
<h3 className="text-xs font-bold text-content tracking-wide line-clamp-1">
{currentSessionId
? sessions.find((s) => s.session_id === currentSessionId)
?.title || '未命名会话'
: '探索性科研研讨'}
</h3>
<p className="text-[10px] text-slate-400 font-semibold mt-0.5">
<p className="text-[10px] text-tertiary font-semibold mt-0.5">
</p>
</div>
@@ -254,10 +334,10 @@ export function AgentMessageList({
setShowMetrics(!showMetrics);
setShowAuditLog(false);
}}
className={`p-1.5 rounded-lg border text-xs font-bold transition-all cursor-pointer flex items-center gap-1 ${
className={`panel-trigger p-1.5 rounded-lg border text-xs font-bold transition-all cursor-pointer flex items-center gap-1 ${
showMetrics
? 'bg-blueprint/5 border-blueprint/20 text-blueprint'
: 'bg-white border-slate-200 text-slate-500 hover:text-blueprint hover:border-blueprint/20'
: 'bg-surface border-subtle text-secondary hover:text-blueprint hover:border-blueprint/20'
}`}
title="运行指标"
>
@@ -270,10 +350,10 @@ export function AgentMessageList({
setShowAuditLog(!showAuditLog);
setShowMetrics(false);
}}
className={`p-1.5 rounded-lg border text-xs font-bold transition-all cursor-pointer flex items-center gap-1 ${
className={`panel-trigger p-1.5 rounded-lg border text-xs font-bold transition-all cursor-pointer flex items-center gap-1 ${
showAuditLog
? 'bg-amber-50 border-amber-200 text-amber-700'
: 'bg-white border-slate-200 text-slate-500 hover:text-amber-600 hover:border-amber-200'
? 'bg-blueprint/5 border-blueprint/20 text-blueprint'
: 'bg-surface border-subtle text-secondary hover:text-blueprint hover:border-blueprint/20'
}`}
title="审计日志"
>
@@ -283,7 +363,7 @@ export function AgentMessageList({
{currentSessionId && (
<button
onClick={(e) => handleDeleteSession(currentSessionId, e)}
className="p-1.5 rounded-lg border border-slate-200 bg-white text-slate-500 hover:text-red-600 hover:border-red-200 transition-all cursor-pointer flex items-center gap-1"
className="p-1.5 rounded-lg border bg-surface border-subtle text-secondary hover:text-danger hover:border-red-200 dark:hover:border-red-500/30 transition-all cursor-pointer flex items-center gap-1"
title="删除会话"
>
<Trash2 className="w-3.5 h-3.5" />
@@ -292,39 +372,95 @@ export function AgentMessageList({
</div>
</div>
{/* Agent 运行指标面板 — 固定在聊天区域上方 */}
{showMetrics && (
<div className="border-b border-sky-200 bg-white px-5 py-4 max-h-[45vh] overflow-y-auto scrollbar-thin shrink-0">
<AgentMetricsPanel showAlert={showAlert} />
</div>
)}
{/* 悬浮面板容器 */}
<div className="relative z-20 shrink-0">
{/* Agent 运行指标面板 — 悬浮在聊天区域上方 */}
{showMetrics && (
<div className="absolute left-4 right-4 top-2 bg-surface/95 backdrop-blur-md border border-subtle rounded-xl shadow-2xl px-5 py-4 max-h-[50vh] overflow-y-auto scrollbar-thin animate-in slide-in-from-top-2 fade-in duration-200 z-30">
<div className="absolute right-4 top-4 z-10">
<button
onClick={() => setShowMetrics(false)}
className="p-1 rounded-md hover:bg-sunken text-tertiary hover:text-content transition-colors cursor-pointer"
title="关闭"
>
<X className="w-4 h-4" />
</button>
</div>
<AgentMetricsPanel showAlert={showAlert} />
</div>
)}
{/* 会话审计日志 — 固定在聊天区域上方 */}
{showAuditLog && currentSessionId && (
<div className="border-b border-amber-200 bg-white px-5 py-4 max-h-[45vh] overflow-y-auto scrollbar-thin shrink-0">
<AuditLogViewer
sessionId={currentSessionId}
onClose={() => setShowAuditLog(false)}
/>
</div>
)}
{/* 会话审计日志 — 悬浮在聊天区域上方 */}
{showAuditLog && currentSessionId && (
<div className="absolute left-4 right-4 top-2 bg-surface/95 backdrop-blur-md border border-subtle rounded-xl shadow-2xl px-5 py-4 max-h-[50vh] overflow-y-auto scrollbar-thin animate-in slide-in-from-top-2 fade-in duration-200 z-30">
<AuditLogViewer
sessionId={currentSessionId}
onClose={() => setShowAuditLog(false)}
/>
</div>
)}
{/* 科研任务看板 — 悬浮在右上角 */}
{showTaskSidebar &&
hasTodos &&
activeTodos &&
activeTodos.length > 0 && (
<div className="absolute right-4 top-2 w-80 bg-surface/95 backdrop-blur-md border border-subtle rounded-xl shadow-2xl flex flex-col max-h-[50vh] overflow-hidden animate-in slide-in-from-top-2 fade-in duration-200 z-30">
{/* 固定头部 — 不随内容滚动 */}
<div className="flex items-center justify-between px-4 py-3 border-b border-subtle bg-sunken/10 shrink-0">
<div className="flex items-center gap-2">
<div className="w-2.5 h-2.5 rounded-full bg-blueprint animate-pulse" />
<h4 className="text-[11px] font-extrabold text-content tracking-wider uppercase">
</h4>
</div>
<button
onClick={() =>
setShowTaskSidebar && setShowTaskSidebar(false)
}
className="p-1 rounded hover:bg-sunken text-tertiary hover:text-content transition-colors cursor-pointer"
title="关闭"
>
<X className="w-4 h-4" />
</button>
</div>
{/* 可滚动区域 — 仅滚动任务列表 */}
<div className="flex-1 overflow-y-auto p-4 scrollbar-thin">
<TodoTaskCard todos={activeTodos} />
</div>
</div>
)}
{/* 悬浮打开任务看板按钮 — 样式参考光谱放大按钮 */}
{hasTodos && !showTaskSidebar && setShowTaskSidebar && (
<button
onClick={() => setShowTaskSidebar(true)}
className="absolute top-2 right-4 p-1.5 rounded bg-surface/90 hover:bg-surface border border-subtle hover:border-default text-tertiary hover:text-content transition-all shadow-xs z-30 cursor-pointer flex items-center gap-1.5"
title="展开任务规划"
>
<ListTodo className="w-3.5 h-3.5 text-blueprint" />
<span className="text-[10px] font-bold"></span>
</button>
)}
</div>
{/* 消息滚动区域 */}
<div
ref={scrollContainerRef}
onScroll={handleScroll}
className={`flex-1 overflow-y-auto p-5 space-y-6 bg-slate-50/50 scrollbar-thin transition-all duration-300 ${
className={`flex-1 overflow-y-auto p-5 space-y-6 bg-sunken scrollbar-thin transition-all duration-300 [overflow-anchor:auto] ${
hasPendingPermission || hasPendingQuestion ? 'pb-80' : 'pb-36'
}`}
>
{turns.length === 0 && !activeTurn ? (
<div className="py-12 space-y-8 max-w-xl mx-auto">
<div className="text-center space-y-2">
<Compass className="w-12 h-12 mx-auto text-sky-500 opacity-60" />
<h3 className="text-sm font-extrabold text-slate-800 tracking-wider">
<Compass className="w-12 h-12 mx-auto text-blueprint opacity-60" />
<h3 className="text-sm font-extrabold text-content tracking-wider">
</h3>
<p className="text-xs text-slate-500 leading-relaxed font-semibold">
<p className="text-xs text-secondary leading-relaxed font-semibold">
NASA ADS PDF/HTML SQLite-Vec
RAG CDS Sesame
@@ -333,7 +469,7 @@ export function AgentMessageList({
{/* 快速提示词 */}
<div className="space-y-3">
<span className="text-[10px] font-bold text-slate-400 tracking-widest uppercase block">
<span className="text-[10px] font-bold text-tertiary tracking-widest uppercase block">
:
</span>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
@@ -341,12 +477,12 @@ export function AgentMessageList({
<button
key={idx}
onClick={() => handleSend(q)}
className="w-full text-left bg-white hover:bg-blueprint/5 border border-slate-200 hover:border-blueprint/30 rounded-lg p-3 text-xs text-slate-700 leading-relaxed font-medium transition-all shadow-2xs hover:shadow-xs cursor-pointer flex flex-col gap-1 group"
className="w-full text-left bg-surface hover:bg-blueprint/5 hover:border-blueprint/30 rounded-lg p-3 text-xs text-secondary leading-relaxed font-medium transition-all cursor-pointer flex flex-col gap-1 group"
>
<span className="group-hover:text-blueprint font-semibold">
{q}
</span>
<span className="text-[9px] text-slate-400 uppercase font-bold tracking-wider">
<span className="text-[9px] text-tertiary uppercase font-bold tracking-wider">
</span>
</button>
@@ -361,20 +497,17 @@ export function AgentMessageList({
<div key={turn.turn_index} className="space-y-4">
{/* 用户提问 */}
<div className="flex flex-col items-end space-y-1 group relative">
<span className="text-[10px] font-bold text-slate-400 px-1">
</span>
<div className="flex items-center gap-2 max-w-[85%]">
{turn.questionMessageId && (
<button
onClick={() => handleRewind(turn.questionMessageId)}
className="opacity-0 group-hover:opacity-100 text-slate-400 hover:text-amber-600 p-1.5 rounded-lg hover:bg-slate-100 transition-all cursor-pointer shrink-0"
className="opacity-0 group-hover:opacity-100 flex items-center justify-center w-7 h-7 rounded-full bg-surface border border-subtle shadow-sm text-tertiary hover:text-amber-600 hover:bg-sunken hover:border-amber-200/60 dark:hover:border-amber-500/30 transition-all duration-200 cursor-pointer shrink-0 mr-1"
title="回退到此消息之前"
>
<Rewind className="w-3.5 h-3.5" />
</button>
)}
<div className="rounded-lg px-4 py-3 text-xs leading-relaxed font-semibold shadow-2xs border bg-blueprint text-white border-blueprint select-text flex-1 space-y-2">
<div className="rounded-lg px-4 py-3 text-xs leading-relaxed font-semibold bg-blueprint/10 text-content dark:bg-blueprint/15 select-text flex-1 space-y-2">
{turn.imagePath && (
<img
src={turn.imagePath}
@@ -389,7 +522,7 @@ export function AgentMessageList({
{/* 时间线 — 思考/工具/答案 */}
{turn.timeline.length > 0 && (
<div className="pl-4 border-l border-slate-200 space-y-3 my-2 relative">
<div className="pl-4 border-l border-strong space-y-3 my-2 relative">
{turn.timeline.map((item: TimelineItem, idx: number) =>
renderTimelineItem(item, idx, false)
)}
@@ -398,7 +531,7 @@ export function AgentMessageList({
{/* Token 用量 */}
{turn.usage && turn.usage.total_tokens > 0 && (
<div className="flex items-center gap-1 text-[9px] text-slate-400 font-mono pl-4">
<div className="flex items-center gap-1 text-[9px] text-tertiary font-mono pl-4">
<span>Tokens: ~{turn.usage.total_tokens}</span>
</div>
)}
@@ -410,10 +543,7 @@ export function AgentMessageList({
<div className="space-y-4">
{/* 当前提问 */}
<div className="flex flex-col items-end space-y-1">
<span className="text-[10px] font-bold text-slate-400 px-1">
</span>
<div className="max-w-[85%] rounded-lg px-4 py-3 text-xs leading-relaxed font-semibold shadow-2xs border bg-blueprint text-white border-blueprint select-text space-y-2">
<div className="max-w-[85%] rounded-lg px-4 py-3 text-xs leading-relaxed font-semibold bg-blueprint/10 text-content dark:bg-blueprint/15 select-text space-y-2">
{activeTurn.imagePath && (
<img
src={activeTurn.imagePath}
@@ -427,7 +557,7 @@ export function AgentMessageList({
{/* SSE Stream Timeline */}
{activeTurn.timeline.length > 0 && (
<div className="pl-4 border-l border-slate-200 space-y-3 my-2 relative">
<div className="pl-4 border-l border-strong space-y-3 my-2 relative">
{activeTurn.timeline.map(
(item: TimelineItem, idx: number) =>
renderTimelineItem(item, idx, true)
@@ -438,11 +568,11 @@ export function AgentMessageList({
{/* 最终回答 */}
{activeTurn.finalAnswer && (
<div className="flex flex-col items-start space-y-1">
<span className="text-[10px] font-bold text-slate-400 px-1 flex items-center gap-1">
<span className="text-[10px] font-bold text-tertiary px-1 flex items-center gap-1">
<CheckCircle2 className="w-3.5 h-3.5 text-blueprint" />
<span></span>
</span>
<div className="max-w-[90%] rounded-lg px-5 py-4 text-xs leading-relaxed font-semibold shadow-xs border bg-white text-slate-800 border-slate-200 select-text prose prose-sm max-w-none prose-headings:text-slate-900 prose-headings:font-bold prose-strong:text-slate-900 prose-code:text-blueprint prose-img:rounded-lg">
<div className="max-w-[90%] rounded-lg px-5 py-4 text-xs leading-relaxed font-semibold border bg-surface text-content border-subtle select-text prose prose-sm max-w-none prose-headings:text-content prose-headings:font-bold prose-strong:text-content prose-code:text-blueprint prose-img:rounded-lg">
<AgentMarkdown>{activeTurn.finalAnswer}</AgentMarkdown>
</div>
</div>
@@ -450,7 +580,7 @@ export function AgentMessageList({
{/* 活跃的加载指示器 */}
{streaming && !activeTurn.finalAnswer && !activeTurn.error && (
<div className="flex items-center space-x-2 text-slate-500 pl-2">
<div className="flex items-center space-x-2 text-secondary pl-2">
<Loader className="w-4 h-4 animate-spin text-blueprint" />
<span className="text-[10px] font-bold">
...
@@ -460,8 +590,8 @@ export function AgentMessageList({
{/* Error block */}
{activeTurn.error && (
<div className="flex items-start gap-2 bg-red-50 border border-red-200 text-red-750 rounded-lg p-3 text-xs w-[90%] font-semibold">
<AlertTriangle className="w-4 h-4 text-red-500 shrink-0 mt-0.5" />
<div className="flex items-start gap-2 bg-red-50 border border-red-200 text-red-700 dark:bg-rose-500/10 dark:text-rose-300 dark:border-rose-500/30 rounded-lg p-3 text-xs w-[90%] font-semibold">
<AlertTriangle className="w-4 h-4 text-danger shrink-0 mt-0.5" />
<div>
<div className="font-bold"></div>
<div className="text-[10px] opacity-80 mt-0.5">
@@ -473,37 +603,88 @@ export function AgentMessageList({
</div>
)}
{/* 会话操作小图标栏 */}
{/* 会话操作控制台 (Session Version Controller) */}
{currentSessionId && turns.length > 0 && !streaming && (
<div className="flex items-center gap-1 pt-2.5 justify-start border-t border-slate-100/60 mt-4 max-w-[200px]">
<button
onClick={() => handleRewind(undefined, 1)}
className="p-1.5 rounded-lg text-slate-400 hover:text-amber-600 hover:bg-slate-100 transition-all cursor-pointer"
title="回退最近一轮对话 (undo)"
<div className="flex items-center mt-6 ml-4 select-none animate-in fade-in duration-300">
<div
className={`console-panel rounded-full px-3 py-1 flex items-center bg-surface border border-subtle shadow-md w-fit hover:border-strong transition-all duration-300 ${
isSessionControlExpanded ? 'gap-1.5' : 'gap-1'
}`}
>
<Rewind className="w-4 h-4" />
</button>
<button
onClick={handleRestoreRewind}
className="p-1.5 rounded-lg text-slate-400 hover:text-green-600 hover:bg-slate-100 transition-all cursor-pointer"
title="恢复上次回退操作"
>
<RotateCcw className="w-4 h-4" />
</button>
<button
onClick={handleRetry}
className="p-1.5 rounded-lg text-slate-400 hover:text-indigo-600 hover:bg-slate-100 transition-all cursor-pointer"
title="重试最后一轮对话"
>
<RefreshCw className="w-4 h-4" />
</button>
<button
onClick={handleBranch}
className="p-1.5 rounded-lg text-slate-400 hover:text-blue-600 hover:bg-slate-100 transition-all cursor-pointer"
title="分叉当前会话"
>
<GitBranch className="w-4 h-4" />
</button>
{/* 可点击的控制头,用于折叠/展开 */}
<div
onClick={() =>
setIsSessionControlExpanded(!isSessionControlExpanded)
}
className="flex items-center gap-1.5 text-[9px] font-extrabold text-tertiary tracking-wider uppercase pl-0.5 cursor-pointer hover:text-content transition-colors select-none"
>
<Sliders
className={`w-3 h-3 transition-transform duration-300 ${
isSessionControlExpanded
? 'rotate-90 text-blueprint'
: 'text-tertiary'
}`}
/>
<span></span>
{/* 微型展开/折叠指示标签 */}
<span className="text-[7px] text-tertiary/60 ml-0.5">
{isSessionControlExpanded ? '◀' : '▶'}
</span>
</div>
{/* 展开内容:滑出显示的操作按键 */}
<div
className={`flex items-center transition-all duration-300 ease-in-out overflow-hidden ${
isSessionControlExpanded
? 'max-w-[150px] opacity-100'
: 'max-w-0 opacity-0 pointer-events-none'
}`}
>
<div className="h-3.5 w-[1px] bg-border-subtle mx-1.5 shrink-0" />
<div className="flex items-center gap-1 shrink-0">
<button
onClick={(e) => {
e.stopPropagation();
handleRewind(undefined, 1);
}}
className="w-7 h-7 rounded-full flex items-center justify-center text-secondary border border-transparent hover:text-amber-600 hover:bg-sunken hover:border-amber-200/60 dark:hover:border-amber-500/30 transition-all duration-200 cursor-pointer"
title="回退最近一轮对话 (undo)"
>
<Rewind className="w-3.5 h-3.5" />
</button>
<button
onClick={(e) => {
e.stopPropagation();
handleRestoreRewind();
}}
className="w-7 h-7 rounded-full flex items-center justify-center text-secondary border border-transparent hover:text-green-600 hover:bg-sunken hover:border-green-200/60 dark:hover:border-green-500/30 transition-all duration-200 cursor-pointer"
title="恢复上次回退操作"
>
<RotateCcw className="w-3.5 h-3.5" />
</button>
<button
onClick={(e) => {
e.stopPropagation();
handleRetry();
}}
className="w-7 h-7 rounded-full flex items-center justify-center text-secondary border border-transparent hover:text-indigo-600 hover:bg-sunken hover:border-indigo-200/60 dark:hover:border-indigo-500/30 transition-all duration-200 cursor-pointer"
title="重试最后一轮对话"
>
<RefreshCw className="w-3.5 h-3.5" />
</button>
<button
onClick={(e) => {
e.stopPropagation();
handleBranch();
}}
className="w-7 h-7 rounded-full flex items-center justify-center text-secondary border border-transparent hover:text-blue-600 hover:bg-sunken hover:border-blue-200/60 dark:hover:border-blue-500/30 transition-all duration-200 cursor-pointer"
title="分叉当前会话"
>
<GitBranch className="w-3.5 h-3.5" />
</button>
</div>
</div>
</div>
</div>
)}
</div>
@@ -11,6 +11,7 @@ import {
Loader,
} from 'lucide-react';
import type { AgentMetricsResponse } from '../../types';
import { getToolMeta } from './index';
interface AgentMetricsPanelProps {
showAlert?: (message: string, title?: string) => void;
@@ -54,43 +55,74 @@ const TOOL_LABELS: Record<string, string> = {
// 工具调用的分类色
const CATEGORY_COLORS: Record<string, string> = {
read_file: 'bg-blue-100 text-blue-700 border-blue-200',
grep_files: 'bg-blue-100 text-blue-700 border-blue-200',
glob_files: 'bg-blue-100 text-blue-700 border-blue-200',
run_bash: 'bg-slate-200 text-slate-700 border-slate-300',
file_write: 'bg-blue-100 text-blue-700 border-blue-200',
file_edit: 'bg-blue-100 text-blue-700 border-blue-200',
search_papers: 'bg-emerald-100 text-emerald-700 border-emerald-200',
get_paper_metadata: 'bg-emerald-100 text-emerald-700 border-emerald-200',
download_paper: 'bg-emerald-100 text-emerald-700 border-emerald-200',
parse_paper: 'bg-emerald-100 text-emerald-700 border-emerald-200',
get_paper_content: 'bg-emerald-100 text-emerald-700 border-emerald-200',
rag_search: 'bg-violet-100 text-violet-700 border-violet-200',
query_target: 'bg-amber-100 text-amber-700 border-amber-200',
query_vizier: 'bg-violet-100 text-violet-700 border-violet-200',
cone_search: 'bg-violet-100 text-violet-700 border-violet-200',
find_observation: 'bg-slate-200 text-slate-700 border-slate-300',
catalog_operation: 'bg-violet-100 text-violet-700 border-violet-200',
save_note: 'bg-teal-100 text-teal-700 border-teal-200',
todo_write: 'bg-orange-100 text-orange-700 border-orange-200',
compress_context: 'bg-rose-100 text-rose-700 border-rose-200',
load_skill: 'bg-indigo-100 text-indigo-700 border-indigo-200',
subagent: 'bg-purple-100 text-purple-700 border-purple-200',
delegate_research: 'bg-purple-100 text-purple-700 border-purple-200',
ask_user: 'bg-amber-100 text-amber-700 border-amber-200',
save_memory: 'bg-pink-100 text-pink-700 border-pink-200',
load_memory: 'bg-pink-100 text-pink-700 border-pink-200',
bg_task_run: 'bg-cyan-100 text-cyan-700 border-cyan-200',
bg_task_check: 'bg-cyan-100 text-cyan-700 border-cyan-200',
spawn_teammate: 'bg-fuchsia-100 text-fuchsia-700 border-fuchsia-200',
send_teammate_message: 'bg-fuchsia-100 text-fuchsia-700 border-fuchsia-200',
team_broadcast: 'bg-fuchsia-100 text-fuchsia-700 border-fuchsia-200',
check_team_inbox: 'bg-fuchsia-100 text-fuchsia-700 border-fuchsia-200',
read_file:
'bg-blue-100 text-blue-700 border-transparent dark:bg-blue-500/15 dark:text-blue-300 dark:border-blue-500/30',
grep_files:
'bg-blue-100 text-blue-700 border-transparent dark:bg-blue-500/15 dark:text-blue-300 dark:border-blue-500/30',
glob_files:
'bg-blue-100 text-blue-700 border-transparent dark:bg-blue-500/15 dark:text-blue-300 dark:border-blue-500/30',
run_bash: 'bg-border-subtle text-secondary border-subtle',
file_write:
'bg-blue-100 text-blue-700 border-transparent dark:bg-blue-500/15 dark:text-blue-300 dark:border-blue-500/30',
file_edit:
'bg-blue-100 text-blue-700 border-transparent dark:bg-blue-500/15 dark:text-blue-300 dark:border-blue-500/30',
search_papers:
'bg-emerald-100 text-emerald-700 border-transparent dark:bg-emerald-500/15 dark:text-emerald-300 dark:border-emerald-500/30',
get_paper_metadata:
'bg-emerald-100 text-emerald-700 border-transparent dark:bg-emerald-500/15 dark:text-emerald-300 dark:border-emerald-500/30',
download_paper:
'bg-emerald-100 text-emerald-700 border-transparent dark:bg-emerald-500/15 dark:text-emerald-300 dark:border-emerald-500/30',
parse_paper:
'bg-emerald-100 text-emerald-700 border-transparent dark:bg-emerald-500/15 dark:text-emerald-300 dark:border-emerald-500/30',
get_paper_content:
'bg-emerald-100 text-emerald-700 border-transparent dark:bg-emerald-500/15 dark:text-emerald-300 dark:border-emerald-500/30',
rag_search:
'bg-violet-100 text-violet-700 border-transparent dark:bg-violet-500/15 dark:text-violet-300 dark:border-violet-500/30',
query_target:
'bg-amber-100 text-amber-700 border-transparent dark:bg-amber-500/15 dark:text-amber-300 dark:border-amber-500/30',
query_vizier:
'bg-violet-100 text-violet-700 border-transparent dark:bg-violet-500/15 dark:text-violet-300 dark:border-violet-500/30',
cone_search:
'bg-violet-100 text-violet-700 border-transparent dark:bg-violet-500/15 dark:text-violet-300 dark:border-violet-500/30',
find_observation: 'bg-border-subtle text-secondary border-subtle',
catalog_operation:
'bg-violet-100 text-violet-700 border-transparent dark:bg-violet-500/15 dark:text-violet-300 dark:border-violet-500/30',
save_note:
'bg-teal-100 text-teal-700 border-teal-200 dark:bg-teal-500/15 dark:text-teal-300 dark:border-teal-500/30',
todo_write:
'bg-orange-100 text-orange-700 border-transparent dark:bg-orange-500/15 dark:text-orange-300 dark:border-orange-500/30',
compress_context:
'bg-rose-100 text-rose-700 border-transparent dark:bg-rose-500/15 dark:text-rose-300 dark:border-rose-500/30',
load_skill:
'bg-indigo-100 text-indigo-700 border-transparent dark:bg-indigo-500/15 dark:text-indigo-300 dark:border-indigo-500/30',
subagent:
'bg-purple-100 text-purple-700 border-transparent dark:bg-purple-500/15 dark:text-purple-300 dark:border-purple-500/30',
delegate_research:
'bg-purple-100 text-purple-700 border-transparent dark:bg-purple-500/15 dark:text-purple-300 dark:border-purple-500/30',
ask_user:
'bg-amber-100 text-amber-700 border-transparent dark:bg-amber-500/15 dark:text-amber-300 dark:border-amber-500/30',
save_memory:
'bg-pink-100 text-pink-700 border-transparent dark:bg-pink-500/15 dark:text-pink-300 dark:border-pink-500/30',
load_memory:
'bg-pink-100 text-pink-700 border-transparent dark:bg-pink-500/15 dark:text-pink-300 dark:border-pink-500/30',
bg_task_run:
'bg-cyan-100 text-cyan-700 border-transparent dark:bg-cyan-500/15 dark:text-cyan-300 dark:border-cyan-500/30',
bg_task_check:
'bg-cyan-100 text-cyan-700 border-transparent dark:bg-cyan-500/15 dark:text-cyan-300 dark:border-cyan-500/30',
spawn_teammate:
'bg-fuchsia-100 text-fuchsia-700 border-transparent dark:bg-fuchsia-500/15 dark:text-fuchsia-300 dark:border-fuchsia-500/30',
send_teammate_message:
'bg-fuchsia-100 text-fuchsia-700 border-transparent dark:bg-fuchsia-500/15 dark:text-fuchsia-300 dark:border-fuchsia-500/30',
team_broadcast:
'bg-fuchsia-100 text-fuchsia-700 border-transparent dark:bg-fuchsia-500/15 dark:text-fuchsia-300 dark:border-fuchsia-500/30',
check_team_inbox:
'bg-fuchsia-100 text-fuchsia-700 border-transparent dark:bg-fuchsia-500/15 dark:text-fuchsia-300 dark:border-fuchsia-500/30',
};
export function AgentMetricsPanel({ showAlert }: AgentMetricsPanelProps) {
const [metrics, setMetrics] = useState<AgentMetricsResponse | null>(null);
const [loading, setLoading] = useState(false);
const [hideInternal, setHideInternal] = useState(false);
const fetchMetrics = async () => {
setLoading(true);
@@ -130,29 +162,38 @@ export function AgentMetricsPanel({ showAlert }: AgentMetricsPanelProps) {
};
}, [showAlert]);
// 提取工具调用排行(取前10
// 提取工具调用排行(取前15
const toolBreakdown = metrics?.tool_call_breakdown
? Object.entries(metrics.tool_call_breakdown)
.filter(([name]) => !hideInternal || !getToolMeta(name)?.is_internal)
.sort(([, a], [, b]) => b - a)
.slice(0, 15)
: [];
const maxToolCalls = toolBreakdown.length > 0 ? toolBreakdown[0][1] : 1;
const filteredBreakdown = metrics?.tool_call_breakdown
? Object.fromEntries(
Object.entries(metrics.tool_call_breakdown).filter(
([name]) => !hideInternal || !getToolMeta(name)?.is_internal
)
)
: {};
return (
<div className="space-y-5">
{/* 头部 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<BarChart3 className="w-4 h-4 text-blueprint" />
<h3 className="text-xs font-extrabold text-slate-800 tracking-wide">
<h3 className="text-xs font-extrabold text-content tracking-wide">
</h3>
</div>
<button
onClick={fetchMetrics}
disabled={loading}
className="p-1.5 rounded-md bg-slate-100 hover:bg-slate-200 text-slate-500 hover:text-slate-700 transition-colors cursor-pointer disabled:opacity-50"
className="p-1.5 rounded-md bg-sunken hover:bg-border-subtle text-secondary hover:text-secondary transition-colors cursor-pointer disabled:opacity-50"
title="刷新指标"
>
<RefreshCw
@@ -162,7 +203,7 @@ export function AgentMetricsPanel({ showAlert }: AgentMetricsPanelProps) {
</div>
{loading && !metrics ? (
<div className="flex items-center justify-center py-12 text-slate-400 gap-2">
<div className="flex items-center justify-center py-12 text-tertiary gap-2">
<Loader className="w-4 h-4 animate-spin text-blueprint" />
<span className="text-xs font-bold">...</span>
</div>
@@ -198,11 +239,23 @@ export function AgentMetricsPanel({ showAlert }: AgentMetricsPanelProps) {
{/* 工具调用分布 */}
<div className="space-y-2.5">
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">
</span>
<div className="flex items-center justify-between">
<span className="text-[10px] font-bold text-tertiary uppercase tracking-wider">
</span>
<button
onClick={() => setHideInternal(!hideInternal)}
className={`px-2 py-0.5 rounded text-[9px] font-bold border transition-colors cursor-pointer select-none ${
hideInternal
? 'bg-blueprint/15 border-blueprint text-blueprint'
: 'bg-surface border-subtle text-secondary hover:bg-sunken hover:border-strong'
}`}
>
{hideInternal ? '显示系统工具' : '隐藏系统工具'}
</button>
</div>
{toolBreakdown.length === 0 ? (
<p className="text-xs text-slate-400 italic py-4 text-center">
<p className="text-xs text-tertiary italic py-4 text-center">
</p>
) : (
@@ -211,23 +264,26 @@ export function AgentMetricsPanel({ showAlert }: AgentMetricsPanelProps) {
const barWidth = Math.max((count / maxToolCalls) * 100, 2);
const colorClass =
CATEGORY_COLORS[name] ||
'bg-slate-100 text-slate-700 border-slate-200';
const label = TOOL_LABELS[name] || name;
'bg-sunken text-secondary border-subtle';
const label =
getToolMeta(name)?.display_name ||
TOOL_LABELS[name] ||
name;
return (
<div key={name} className="flex items-center gap-2.5">
<span
className="text-[10px] text-slate-500 w-24 shrink-0 text-right font-medium truncate"
className="text-[10px] text-secondary w-24 shrink-0 text-right font-medium truncate"
title={label}
>
{label}
</span>
<div className="flex-1 h-5 bg-slate-100 rounded-full overflow-hidden border border-slate-200">
<div className="flex-1 h-5 bg-sunken rounded-full overflow-hidden ">
<div
className={`h-full rounded-full transition-all duration-500 ${colorClass.split(' ')[0]}`}
style={{ width: `${barWidth}%` }}
/>
</div>
<span className="text-[10px] font-bold text-slate-600 w-8 text-right shrink-0">
<span className="text-[10px] font-bold text-secondary w-8 text-right shrink-0">
{count}
</span>
</div>
@@ -238,21 +294,21 @@ export function AgentMetricsPanel({ showAlert }: AgentMetricsPanelProps) {
</div>
{/* 工具分类统计 */}
<div className="border-t border-slate-200 pt-3">
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-wider block mb-2">
<div className="border-t border-subtle pt-3">
<span className="text-[10px] font-bold text-tertiary uppercase tracking-wider block mb-2">
</span>
<div className="flex flex-wrap gap-1.5">
{Object.entries(
getCategoryCounts(metrics.tool_call_breakdown)
).map(([category, count]) => (
<span
key={category}
className="px-2.5 py-1 rounded-lg text-[10px] font-bold border bg-slate-50 text-slate-600 border-slate-200"
>
{category}: {count}
</span>
))}
{Object.entries(getCategoryCounts(filteredBreakdown)).map(
([category, count]) => (
<span
key={category}
className="px-2.5 py-1 rounded-lg text-[10px] font-bold border bg-sunken text-secondary border-subtle"
>
{category}: {count}
</span>
)
)}
</div>
</div>
</>
@@ -274,11 +330,14 @@ function MetricCard({
color: string;
}) {
const colorMap: Record<string, string> = {
sky: 'border-slate-200 bg-slate-50 text-blueprint',
emerald: 'border-emerald-200 bg-emerald-50/40 text-emerald-700',
violet: 'border-violet-200 bg-violet-50/40 text-violet-700',
rose: 'border-rose-200 bg-rose-50/40 text-rose-700',
amber: 'border-amber-200 bg-amber-50/40 text-amber-700',
sky: 'border-subtle bg-sunken text-blueprint',
emerald:
'border-transparent bg-emerald-50/40 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300 dark:border-emerald-500/30',
violet:
'border-transparent bg-violet-50/40 text-violet-700 dark:bg-violet-500/15 dark:text-violet-300 dark:border-violet-500/30',
rose: 'border-transparent bg-rose-50/40 text-rose-700 dark:bg-rose-500/15 dark:text-rose-300 dark:border-rose-500/30',
amber:
'border-transparent bg-amber-50/40 text-amber-700 dark:bg-amber-500/15 dark:text-amber-300 dark:border-amber-500/30',
};
return (
@@ -69,35 +69,35 @@ export function AgentSessionSidebar({
<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"
className="fixed inset-0 bg-black/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 ${
className={`bg-surface border-r border-subtle 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'
: 'translate-x-0'
}`}
>
<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">
<div className="p-4 bg-surface flex items-center justify-between shrink-0">
<span className="text-xs font-extrabold text-content 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"
className="p-1 rounded-md bg-surface hover:bg-sunken text-secondary hover:text-content transition-all cursor-pointer"
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"
className="p-1 rounded-md bg-surface hover:bg-sunken text-secondary hover:text-secondary transition-all cursor-pointer"
title="收起侧栏"
>
<PanelLeftClose className="w-3.5 h-3.5" />
@@ -106,34 +106,34 @@ export function AgentSessionSidebar({
</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="px-3 py-2 bg-surface border-b border-subtle 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"
className="w-full pl-8 pr-7 py-1.5 rounded-md bg-sunken border border-default text-content placeholder-tertiary focus:outline-none focus:border-blueprint focus:bg-surface 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 className="absolute left-2.5 top-2.5 text-tertiary">
<Search className="w-3.5 h-3.5 text-tertiary" />
</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"
className="absolute right-2 top-2 p-0.5 rounded-full hover:bg-border-subtle text-tertiary hover:text-secondary 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">
<label className="flex items-center gap-1.5 text-[10px] text-secondary 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"
className="rounded text-blueprint border-subtle focus:ring-blueprint w-3 h-3 cursor-pointer"
/>
<span></span>
</label>
@@ -143,12 +143,12 @@ export function AgentSessionSidebar({
<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">
<div className="flex items-center justify-center p-8 text-tertiary 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 className="text-center py-12 text-tertiary text-[11px] italic">
</div>
) : (
@@ -165,15 +165,15 @@ export function AgentSessionSidebar({
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'
? 'bg-blueprint/5 border-blueprint text-blueprint font-bold'
: 'border-transparent bg-surface hover:bg-sunken text-secondary'
}`}
>
<div className="flex items-center justify-between w-full">
<span className="text-[10px] font-bold text-slate-400 flex items-center gap-1">
<span className="text-[10px] font-bold text-tertiary flex items-center gap-1">
{isMessage ? (
<>
<MessageSquare className="w-3 h-3 text-slate-400" />
<MessageSquare className="w-3 h-3 text-tertiary" />
<span>{role === 'user' ? '提问' : '解答'}</span>
</>
) : (
@@ -184,18 +184,18 @@ export function AgentSessionSidebar({
)}
</span>
{result.created_at && (
<span className="text-[8px] text-slate-400 font-mono">
<span className="text-[8px] text-tertiary 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">
<span className="text-xs leading-snug line-clamp-1 block text-left font-bold text-content">
{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"
className="text-[10px] text-secondary font-medium leading-normal block text-left line-clamp-3 bg-sunken/50 p-1.5 rounded /50"
dangerouslySetInnerHTML={{ __html: result.snippet }}
/>
</button>
@@ -204,12 +204,12 @@ export function AgentSessionSidebar({
)
) : // 正常的会话列表渲染
loadingSessions ? (
<div className="flex items-center justify-center p-8 text-slate-400 text-xs gap-2">
<div className="flex items-center justify-center p-8 text-tertiary 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 className="text-center py-12 text-tertiary text-[11px] italic">
</div>
) : (
@@ -221,22 +221,22 @@ export function AgentSessionSidebar({
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'
? 'bg-blueprint/5 border-blueprint text-blueprint font-bold'
: 'border-transparent bg-transparent hover:bg-sunken text-secondary'
}`}
>
<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">
<span className="text-xs leading-snug truncate block text-left">
{session.title || '无标题会话'}
</span>
<span className="text-[9px] text-slate-400 font-semibold block text-left">
<span className="text-[9px] text-tertiary 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"
className="text-tertiary hover:text-danger p-1 rounded-md opacity-0 group-hover:opacity-100 hover:bg-surface border border-transparent hover:border-subtle transition-all cursor-pointer shrink-0"
title="删除此会话"
>
<Trash2 className="w-3 h-3" />
@@ -21,13 +21,13 @@ export function AnswerCard({
className={`absolute -left-[21px] top-3 w-2.5 h-2.5 rounded-full border-2 border-white ${colorScheme.answer}`}
/>
<div className="flex flex-col items-start">
<span className="text-[10px] font-bold text-slate-400 px-1 flex items-center gap-1 mb-1">
<span className="text-[10px] font-bold text-tertiary px-1 flex items-center gap-1 mb-1">
<CheckCircle2
className={`w-3.5 h-3.5 text-emerald-500 ${isStreaming ? 'animate-pulse' : ''}`}
className={`w-3.5 h-3.5 text-success ${isStreaming ? 'animate-pulse' : ''}`}
/>
<span></span>
</span>
<div className="w-full rounded-2xl px-5 py-4 text-xs leading-relaxed font-semibold shadow-xs border bg-white text-slate-800 border-slate-200 prose prose-sm max-w-none select-text prose-headings:text-slate-900 prose-headings:font-bold prose-strong:text-slate-900 prose-code:text-sky-700 prose-img:rounded-lg">
<div className="w-full rounded-2xl px-5 py-4 text-xs leading-relaxed font-semibold border bg-surface text-content border-subtle prose prose-sm max-w-none select-text prose-headings:text-content prose-headings:font-bold prose-strong:text-content prose-code:text-blueprint prose-img:rounded-lg">
<AgentMarkdown>{content}</AgentMarkdown>
</div>
</div>
@@ -149,17 +149,17 @@ export function AskUserQuestionCard({
return (
<div
key={q.question_id}
className="rounded-md border border-slate-200 border-l-4 border-l-blueprint bg-slate-50 p-4 transition-all pointer-events-auto text-xs shadow-sm"
className="rounded-md border-l-4 border-l-blueprint bg-sunken p-4 transition-all pointer-events-auto text-xs"
>
{/* 标题栏 - 头部与问题放在同一行,可省略独立的问题文本框 */}
<div className="mb-2.5 flex items-center justify-between border-b border-slate-200 pb-2 min-w-0">
<div className="mb-2.5 flex items-center justify-between border-b border-subtle pb-2 min-w-0">
<div className="flex items-center gap-1.5 min-w-0 flex-1 mr-3">
<MessageCircle className="h-3.5 w-3.5 text-blueprint shrink-0" />
<span className="px-1.5 py-0.2 rounded bg-slate-200 border border-slate-300 text-[9px] font-bold text-slate-755 shrink-0">
<span className="px-1.5 py-0.2 rounded bg-border-subtle text-[9px] font-bold text-content shrink-0">
{q.header || '提问'}
</span>
<span
className="font-extrabold text-slate-800 text-[11px] truncate flex-1"
className="font-extrabold text-content text-[11px] truncate flex-1"
title={q.question || ''}
>
{q.question || ''}
@@ -189,7 +189,7 @@ export function AskUserQuestionCard({
{/* 选项列表 */}
{options.length > 0 && (
<div className="space-y-1.5">
<span className="text-[9px] font-bold text-slate-400 uppercase tracking-wider block">
<span className="text-[9px] font-bold text-tertiary uppercase tracking-wider block">
{multiSelect ? '可多选' : '请选择一项'}
</span>
<div className="space-y-1.5">
@@ -216,7 +216,7 @@ export function AskUserQuestionCard({
className={`w-full text-left px-2.5 py-1.5 rounded-md border text-[11px] font-semibold transition-all cursor-pointer flex items-center gap-2 ${
selected
? 'bg-blueprint/5 border-blueprint text-blueprint font-bold'
: 'bg-white border-slate-200/60 text-slate-700 hover:border-slate-300 hover:bg-slate-50'
: 'bg-surface border-subtle text-secondary hover:border-subtle hover:bg-sunken'
} disabled:opacity-50`}
>
{/* 选择指示器 */}
@@ -224,14 +224,14 @@ export function AskUserQuestionCard({
selected ? (
<CheckSquare className="w-3.5 h-3.5 text-blueprint shrink-0" />
) : (
<Square className="w-3.5 h-3.5 text-slate-400 shrink-0" />
<Square className="w-3.5 h-3.5 text-tertiary shrink-0" />
)
) : (
<div
className={`w-3.5 h-3.5 rounded-full border-2 shrink-0 ${
selected
? 'border-blueprint bg-blueprint'
: 'border-slate-300'
: 'border-subtle'
}`}
>
{selected && (
@@ -244,12 +244,12 @@ export function AskUserQuestionCard({
{/* 将 label 与 desc 合并在同一行呈现以压缩高度 */}
<div className="min-w-0 flex-1 flex items-baseline gap-1.5 truncate">
<span className="font-extrabold text-slate-800 text-[11px] shrink-0 leading-normal">
<span className="font-extrabold text-content text-[11px] shrink-0 leading-normal">
{label}
</span>
{desc && (
<span
className="text-[10px] text-slate-500 font-medium truncate leading-normal"
className="text-[10px] text-secondary font-medium truncate leading-normal"
title={desc}
>
{desc}
@@ -276,13 +276,13 @@ export function AskUserQuestionCard({
disabled={isSubmitting}
placeholder="补充说明(可选)..."
rows={1}
className="w-full bg-white border border-slate-200/60 rounded-md px-2.5 py-1.5 text-[11px] text-slate-800 placeholder-slate-400 focus:outline-none focus:border-blueprint focus:ring-1 focus:ring-blueprint/10 resize-none disabled:opacity-50 min-h-[32px] max-h-24 leading-normal"
className="w-full bg-surface border border-default rounded-md px-2.5 py-1.5 text-[11px] text-content placeholder-tertiary focus:outline-none focus:border-blueprint focus:ring-1 focus:ring-blueprint/10 resize-none disabled:opacity-50 min-h-[32px] max-h-24 leading-normal"
/>
</div>
{/* 错误提示 */}
{qError && (
<div className="text-[10px] text-red-700 bg-red-50 border border-red-200 rounded-md px-2.5 py-1.5 font-bold">
<div className="text-[10px] text-danger bg-red-50 border border-red-200 dark:bg-rose-500/10 dark:border-rose-500/30 rounded-md px-2.5 py-1.5 font-bold">
{qError}
</div>
)}
@@ -292,7 +292,7 @@ export function AskUserQuestionCard({
<button
onClick={() => handleSubmit(q.question_id)}
disabled={isSubmitting}
className="flex-1 flex items-center justify-center gap-1.5 bg-emerald-700 hover:bg-emerald-800 border border-emerald-700 text-white rounded-md py-1.5 text-[11px] font-bold transition-colors cursor-pointer disabled:opacity-50"
className="flex-1 flex items-center justify-center gap-1.5 bg-success hover:opacity-90 border border-success text-white rounded-md py-1.5 text-[11px] font-bold transition-colors cursor-pointer disabled:opacity-50"
>
{isSubmitting ? (
<>
@@ -309,7 +309,7 @@ export function AskUserQuestionCard({
<button
onClick={() => dismissQuestion(q.question_id)}
disabled={isSubmitting}
className="px-3 bg-slate-100 hover:bg-slate-200 text-slate-700 border border-slate-250 rounded-md text-[11px] font-bold transition-colors cursor-pointer disabled:opacity-50"
className="px-3 bg-sunken hover:bg-border-subtle text-secondary rounded-md text-[11px] font-bold transition-colors cursor-pointer disabled:opacity-50"
>
</button>
@@ -98,14 +98,14 @@ export function AuditLogViewer({ sessionId, onClose }: AuditLogViewerProps) {
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<ScrollText className="w-4 h-4 text-blueprint" />
<h3 className="text-xs font-extrabold text-slate-800 tracking-wide">
<h3 className="text-xs font-extrabold text-content tracking-wide">
</h3>
</div>
{onClose && (
<button
onClick={onClose}
className="text-[10px] font-bold text-slate-400 hover:text-slate-600 transition-colors cursor-pointer"
className="text-[10px] font-bold text-tertiary hover:text-secondary transition-colors cursor-pointer"
>
</button>
@@ -115,44 +115,42 @@ export function AuditLogViewer({ sessionId, onClose }: AuditLogViewerProps) {
{/* 汇总 */}
{entries.length > 0 && (
<div className="grid grid-cols-3 gap-2">
<div className="rounded-md bg-emerald-50/40 border border-emerald-200 px-3 py-2 text-center">
<div className="text-xs font-extrabold text-emerald-700">
{okCount}
</div>
<div className="text-[9px] font-bold text-emerald-500"></div>
<div className="rounded-md bg-emerald-50/40 border border-transparent px-3 py-2 text-center">
<div className="text-xs font-extrabold text-success">{okCount}</div>
<div className="text-[9px] font-bold text-success"></div>
</div>
<div className="rounded-md bg-rose-50/40 border border-rose-200 px-3 py-2 text-center">
<div className="text-xs font-extrabold text-rose-700">
<div className="rounded-md bg-rose-50/40 border border-transparent px-3 py-2 text-center">
<div className="text-xs font-extrabold text-danger">
{failCount}
</div>
<div className="text-[9px] font-bold text-rose-500"></div>
<div className="text-[9px] font-bold text-danger"></div>
</div>
<div className="rounded-md bg-slate-50 border border-slate-200 px-3 py-2 text-center">
<div className="text-xs font-extrabold text-slate-700">
<div className="rounded-md bg-sunken px-3 py-2 text-center">
<div className="text-xs font-extrabold text-secondary">
{(totalElapsed / 1000).toFixed(1)}s
</div>
<div className="text-[9px] font-bold text-slate-500"></div>
<div className="text-[9px] font-bold text-secondary"></div>
</div>
</div>
)}
{/* 加载/错误状态 */}
{loading && (
<div className="flex items-center justify-center py-8 text-slate-400 gap-2">
<div className="flex items-center justify-center py-8 text-tertiary gap-2">
<Loader className="w-4 h-4 animate-spin text-blueprint" />
<span className="text-xs font-bold">...</span>
</div>
)}
{error && (
<div className="flex items-start gap-2 text-slate-800 bg-slate-50 border border-slate-200 border-l-4 border-l-rose-500 rounded-md px-3 py-2 text-xs font-semibold">
<AlertTriangle className="w-3.5 h-3.5 text-rose-500 shrink-0 mt-0.2" />
<div className="flex items-start gap-2 text-content bg-sunken border-l-4 border-l-rose-500 rounded-md px-3 py-2 text-xs font-semibold">
<AlertTriangle className="w-3.5 h-3.5 text-danger shrink-0 mt-0.2" />
<div>{error}</div>
</div>
)}
{/* 日志表格 */}
{!loading && entries.length === 0 && !error && (
<p className="text-xs text-slate-400 italic py-8 text-center">
<p className="text-xs text-tertiary italic py-8 text-center">
</p>
)}
@@ -161,20 +159,20 @@ export function AuditLogViewer({ sessionId, onClose }: AuditLogViewerProps) {
<div className="overflow-x-auto">
<table className="w-full text-[10px]">
<thead>
<tr className="border-b border-slate-200 text-left">
<th className="pb-2 pr-2 font-extrabold text-slate-400 uppercase tracking-wider w-10">
<tr className="border-b border-subtle text-left">
<th className="pb-2 pr-2 font-extrabold text-tertiary uppercase tracking-wider w-10">
</th>
<th className="pb-2 pr-2 font-extrabold text-slate-400 uppercase tracking-wider">
<th className="pb-2 pr-2 font-extrabold text-tertiary uppercase tracking-wider">
</th>
<th className="pb-2 pr-2 font-extrabold text-slate-400 uppercase tracking-wider w-12">
<th className="pb-2 pr-2 font-extrabold text-tertiary uppercase tracking-wider w-12">
</th>
<th className="pb-2 pr-2 font-extrabold text-slate-400 uppercase tracking-wider w-16 text-right">
<th className="pb-2 pr-2 font-extrabold text-tertiary uppercase tracking-wider w-16 text-right">
</th>
<th className="pb-2 font-extrabold text-slate-400 uppercase tracking-wider">
<th className="pb-2 font-extrabold text-tertiary uppercase tracking-wider">
</th>
</tr>
@@ -188,39 +186,39 @@ export function AuditLogViewer({ sessionId, onClose }: AuditLogViewerProps) {
return (
<tr
key={entry.id}
className={`border-b border-slate-100 ${
className={`border-b border-subtle ${
entry.status === 'FAIL' ? 'bg-rose-50/30' : ''
}`}
>
<td className="py-2 pr-2 font-mono text-slate-500 align-top">
<td className="py-2 pr-2 font-mono text-secondary align-top">
#{entry.step}
</td>
<td className="py-2 pr-2 font-semibold text-slate-700 align-top">
<td className="py-2 pr-2 font-semibold text-secondary align-top">
{getToolDisplayName(entry.tool_name)}
{entry.tool_name && (
<span className="text-[9px] text-slate-400 font-mono block">
<span className="text-[9px] text-tertiary font-mono block">
{entry.tool_name}
</span>
)}
</td>
<td className="py-2 pr-2 align-top">
{entry.status === 'OK' ? (
<span className="inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded bg-emerald-100 text-emerald-700 font-bold text-[9px]">
<span className="inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded bg-emerald-100 text-success font-bold text-[9px]">
<CheckCircle2 className="w-2.5 h-2.5" />
OK
</span>
) : entry.status === 'FAIL' ? (
<span className="inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded bg-rose-100 text-rose-700 font-bold text-[9px]">
<span className="inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded bg-rose-100 text-danger font-bold text-[9px]">
<XCircle className="w-2.5 h-2.5" />
FAIL
</span>
) : (
<span className="text-slate-400 text-[9px]">
<span className="text-tertiary text-[9px]">
{entry.status}
</span>
)}
</td>
<td className="py-2 pr-2 text-right font-mono text-slate-500 align-top">
<td className="py-2 pr-2 text-right font-mono text-secondary align-top">
<span className="flex items-center gap-0.5 justify-end">
<Clock className="w-2.5 h-2.5" />
{entry.elapsed_ms >= 1000
@@ -233,7 +231,7 @@ export function AuditLogViewer({ sessionId, onClose }: AuditLogViewerProps) {
<div>
{isExpanded ? (
<div className="space-y-1">
<pre className="font-mono text-[9px] text-slate-600 bg-slate-50 border border-slate-200 rounded-md p-2 max-h-32 overflow-y-auto whitespace-pre-wrap leading-relaxed">
<pre className="font-mono text-[9px] text-secondary bg-sunken rounded-md p-2 max-h-32 overflow-y-auto whitespace-pre-wrap leading-relaxed">
{entry.output_preview}
</pre>
<button
@@ -257,7 +255,7 @@ export function AuditLogViewer({ sessionId, onClose }: AuditLogViewerProps) {
[entry.id]: true,
}))
}
className="text-left font-mono text-[9px] text-slate-500 hover:text-blueprint transition-colors cursor-pointer flex items-center gap-0.5 max-w-[200px]"
className="text-left font-mono text-[9px] text-secondary hover:text-blueprint transition-colors cursor-pointer flex items-center gap-0.5 max-w-[200px]"
>
<ChevronDown className="w-2.5 h-2.5 shrink-0" />
<span className="truncate">
@@ -270,7 +268,7 @@ export function AuditLogViewer({ sessionId, onClose }: AuditLogViewerProps) {
)}
</div>
) : (
<span className="text-slate-400 italic text-[9px]">
<span className="text-tertiary italic text-[9px]">
</span>
)}
@@ -96,17 +96,17 @@ export function PermissionRequestCard({
return (
<div
key={req.permission_id}
className="rounded-md border border-slate-200 border-l-4 border-l-amber-500 bg-slate-50 p-4 transition-all pointer-events-auto text-xs shadow-sm"
className="rounded-md border-l-4 border-l-amber-500 bg-sunken p-4 transition-all pointer-events-auto text-xs"
>
{/* 标题与控制栏 */}
<div className="mb-2.5 flex items-center justify-between border-b border-slate-200 pb-2">
<div className="mb-2.5 flex items-center justify-between border-b border-subtle pb-2">
<div className="flex items-center gap-1.5 min-w-0">
<Shield className="h-3.5 w-3.5 text-amber-500 shrink-0" />
<span className="font-bold text-slate-800 tracking-wide text-[11px] shrink-0">
<span className="font-bold text-content tracking-wide text-[11px] shrink-0">
</span>
<code
className="rounded bg-slate-200/80 border border-slate-300 px-1.5 py-0.5 text-[9px] font-mono font-bold text-slate-700 truncate"
className="rounded bg-border-subtle px-1.5 py-0.5 text-[9px] font-mono font-bold text-secondary truncate"
title={req.tool_name}
>
{req.tool_name}
@@ -117,7 +117,7 @@ export function PermissionRequestCard({
<button
type="button"
onClick={() => toggleParams(req.permission_id)}
className="text-[10px] font-bold text-slate-500 hover:text-slate-800 underline underline-offset-2 cursor-pointer select-none"
className="text-[10px] font-bold text-secondary hover:text-content underline underline-offset-2 cursor-pointer select-none"
>
{showArgs ? '隐藏参数' : '参数详情'}
</button>
@@ -125,13 +125,13 @@ export function PermissionRequestCard({
</div>
{/* 警告消息 */}
<p className="mb-3 text-[11px] font-medium text-slate-750 leading-relaxed">
<p className="mb-3 text-[11px] font-medium text-secondary leading-relaxed">
{req.message}
</p>
{/* 显示工具参数的简化预览 - 按需展开 */}
{hasArgs && showArgs && (
<pre className="mb-3 max-h-24 overflow-auto rounded bg-white border border-slate-200 p-2 text-[9px] font-mono text-slate-600 scrollbar-thin">
<pre className="mb-3 max-h-24 overflow-auto rounded bg-surface p-2 text-[9px] font-mono text-secondary scrollbar-thin">
{JSON.stringify(req.arguments, null, 2)}
</pre>
)}
@@ -141,7 +141,7 @@ export function PermissionRequestCard({
<button
onClick={() => respond(req.tool_call_id, true, false)}
disabled={submitting[req.tool_call_id]}
className="flex items-center gap-1 px-3 py-1.5 rounded-md text-[10px] font-bold transition-all cursor-pointer bg-emerald-700 hover:bg-emerald-800 text-white disabled:opacity-50"
className="flex items-center gap-1 px-3 py-1.5 rounded-md text-[10px] font-bold transition-all cursor-pointer bg-success hover:opacity-90 text-white disabled:opacity-50"
>
{submitting[req.tool_call_id] ? (
<Loader className="h-3 w-3 animate-spin" />
@@ -153,14 +153,14 @@ export function PermissionRequestCard({
<button
onClick={() => respond(req.tool_call_id, true, true)}
disabled={submitting[req.tool_call_id]}
className="flex items-center gap-1 px-3 py-1.5 rounded-md text-[10px] font-bold transition-all cursor-pointer bg-slate-800 hover:bg-slate-900 text-white disabled:opacity-50"
className="flex items-center gap-1 px-3 py-1.5 rounded-md text-[10px] font-bold transition-all cursor-pointer bg-elevated hover:bg-slate-900 text-white disabled:opacity-50"
>
</button>
<button
onClick={() => respond(req.tool_call_id, false, false)}
disabled={submitting[req.tool_call_id]}
className="flex items-center gap-1 px-3 py-1.5 rounded-md text-[10px] font-bold transition-all cursor-pointer bg-slate-100 border border-slate-250 text-slate-700 hover:bg-slate-200 disabled:opacity-50"
className="flex items-center gap-1 px-3 py-1.5 rounded-md text-[10px] font-bold transition-all cursor-pointer bg-sunken border border-default text-secondary hover:bg-border-subtle disabled:opacity-50"
>
<X className="h-3 w-3" />
@@ -5,14 +5,13 @@ import {
GitFork,
Download,
Loader2,
CheckCircle2,
Circle,
Activity,
AlertTriangle,
Cpu,
Check,
Table as TableIcon,
Database,
Lock,
} from 'lucide-react';
import type { StandardPaper } from '../../types';
import type { useCitations } from '../../hooks/useCitations';
@@ -55,10 +54,10 @@ export function QueryTargetCard({ metadata }: QueryTargetCardProps) {
const vizierUrl = `https://vizier.cds.unistra.fr/viz-bin/VizieR-s?${encodeURIComponent(target_name)}`;
return (
<div className="bg-sky-50/40 border border-sky-100 rounded-lg p-3.5 space-y-3 text-xs shadow-2xs">
<div className="flex items-center justify-between border-b border-sky-100/70 pb-2">
<div className="flex items-center gap-1.5 font-bold text-sky-900 text-[11px] uppercase tracking-wide">
<Orbit className="w-3.5 h-3.5 text-sky-600 animate-spin-slow" />
<div className="bg-sky-50/40 border border-transparent dark:bg-sky-500/10 dark:border-sky-500/30 rounded-lg p-3.5 space-y-3 text-xs">
<div className="flex items-center justify-between border-b border-transparent dark:border-sky-500/30 pb-2">
<div className="flex items-center gap-1.5 font-bold text-sky-900 dark:text-sky-300 text-[11px] uppercase tracking-wide">
<Orbit className="w-3.5 h-3.5 text-sky-600 dark:text-sky-300 animate-spin-slow" />
<span></span>
</div>
<div className="flex gap-2">
@@ -66,7 +65,7 @@ export function QueryTargetCard({ metadata }: QueryTargetCardProps) {
href={simbadUrl}
target="_blank"
rel="noopener noreferrer"
className="text-[10px] font-bold text-sky-600 hover:text-sky-800 flex items-center gap-0.5 hover:underline"
className="text-[10px] font-bold text-sky-600 dark:text-sky-300 hover:opacity-90 flex items-center gap-0.5 hover:underline"
>
<span>SIMBAD</span>
<ExternalLink className="w-2.5 h-2.5" />
@@ -75,7 +74,7 @@ export function QueryTargetCard({ metadata }: QueryTargetCardProps) {
href={vizierUrl}
target="_blank"
rel="noopener noreferrer"
className="text-[10px] font-bold text-sky-600 hover:text-sky-800 flex items-center gap-0.5 hover:underline"
className="text-[10px] font-bold text-sky-600 dark:text-sky-300 hover:opacity-90 flex items-center gap-0.5 hover:underline"
>
<span>VizieR</span>
<ExternalLink className="w-2.5 h-2.5" />
@@ -83,65 +82,65 @@ export function QueryTargetCard({ metadata }: QueryTargetCardProps) {
</div>
</div>
<div className="grid grid-cols-2 gap-x-4 gap-y-2 text-slate-700">
<div className="grid grid-cols-2 gap-x-4 gap-y-2 text-secondary">
<div>
<span className="text-[10px] text-slate-400 font-bold block">
<span className="text-[10px] text-tertiary font-bold block">
</span>
<span className="font-semibold text-slate-900">
<span className="font-semibold text-content">
{oname || target_name}
</span>
</div>
{otype && (
<div>
<span className="text-[10px] text-slate-400 font-bold block">
<span className="text-[10px] text-tertiary font-bold block">
</span>
<span className="font-semibold text-slate-900">{otype}</span>
<span className="font-semibold text-content">{otype}</span>
</div>
)}
{ra && (
<div>
<span className="text-[10px] text-slate-400 font-bold block">
<span className="text-[10px] text-tertiary font-bold block">
(RA)
</span>
<span className="font-mono font-medium text-slate-900">{ra}</span>
<span className="font-mono font-medium text-content">{ra}</span>
</div>
)}
{dec && (
<div>
<span className="text-[10px] text-slate-400 font-bold block">
<span className="text-[10px] text-tertiary font-bold block">
(Dec)
</span>
<span className="font-mono font-medium text-slate-900">{dec}</span>
<span className="font-mono font-medium text-content">{dec}</span>
</div>
)}
{v_magnitude !== undefined && (
<div>
<span className="text-[10px] text-slate-400 font-bold block">
<span className="text-[10px] text-tertiary font-bold block">
V星等
</span>
<span className="font-mono font-semibold text-slate-900">
<span className="font-mono font-semibold text-content">
{v_magnitude.toFixed(3)} mag
</span>
</div>
)}
{parallax !== undefined && (
<div>
<span className="text-[10px] text-slate-400 font-bold block">
<span className="text-[10px] text-tertiary font-bold block">
(Parallax)
</span>
<span className="font-mono font-medium text-slate-900">
<span className="font-mono font-medium text-content">
{parallax.toFixed(4)} mas
</span>
</div>
)}
{spectral_type && (
<div>
<span className="text-[10px] text-slate-400 font-bold block">
<span className="text-[10px] text-tertiary font-bold block">
</span>
<span className="font-mono font-bold text-slate-900">
<span className="font-mono font-bold text-content">
{spectral_type}
</span>
</div>
@@ -150,9 +149,9 @@ export function QueryTargetCard({ metadata }: QueryTargetCardProps) {
{/* 别名 */}
{aliases.length > 0 && (
<div className="border-t border-sky-100/50 pt-2 text-[10px]">
<span className="text-slate-400 font-bold block"></span>
<span className="text-slate-600 leading-normal line-clamp-2">
<div className="border-t border-transparent dark:border-sky-500/30 pt-2 text-[10px]">
<span className="text-tertiary font-bold block"></span>
<span className="text-secondary leading-normal line-clamp-2">
{aliases.join(', ')}
</span>
</div>
@@ -160,8 +159,8 @@ export function QueryTargetCard({ metadata }: QueryTargetCardProps) {
{/* 多波段测光 */}
{Object.keys(photometry).length > 0 && (
<div className="border-t border-sky-100/50 pt-2">
<span className="text-[10px] text-slate-400 font-bold block mb-1">
<div className="border-t border-transparent dark:border-sky-500/30 pt-2">
<span className="text-[10px] text-tertiary font-bold block mb-1">
</span>
<div className="flex flex-wrap gap-1.5">
@@ -170,10 +169,12 @@ export function QueryTargetCard({ metadata }: QueryTargetCardProps) {
.map(([band, val]) => (
<div
key={band}
className="bg-white/60 border border-sky-100/50 px-2 py-0.5 rounded flex items-center gap-1 font-mono text-[10px]"
className="bg-white/60 border border-transparent dark:bg-sky-500/10 dark:border-sky-500/30 px-2 py-0.5 rounded flex items-center gap-1 font-mono text-[10px]"
>
<span className="font-bold text-sky-850">{band}</span>
<span className="text-slate-700 font-semibold">
<span className="font-bold text-sky-900 dark:text-sky-300">
{band}
</span>
<span className="text-secondary font-semibold">
{val.toFixed(3)}
</span>
</div>
@@ -220,7 +221,7 @@ export function PaperListCard({
if (papers.length === 0) {
return (
<div className="text-[10px] text-slate-400 italic p-2 border border-dashed border-slate-200 rounded-md">
<div className="text-[10px] text-tertiary italic p-2 border border-dashed border-subtle rounded-md">
</div>
);
@@ -300,7 +301,7 @@ export function PaperListCard({
return (
<div className="space-y-2 max-w-full select-none">
<div className="flex items-center gap-1.5 text-[10px] font-bold text-slate-400 uppercase tracking-wider pl-1">
<div className="flex items-center gap-1.5 text-[10px] font-bold text-tertiary uppercase tracking-wider pl-1">
<span> {metadata.count} </span>
</div>
<div className="space-y-2 max-h-96 overflow-y-auto pr-1 scrollbar-thin">
@@ -318,17 +319,17 @@ export function PaperListCard({
return (
<div
key={paper.bibcode}
className="bg-white border border-slate-200/80 rounded-lg p-3 hover:shadow-2xs transition-all flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 text-xs relative"
className="bg-surface rounded-lg p-3 hover:transition-all flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 text-xs relative"
>
<div className="min-w-0 flex-1 space-y-1">
<h4
className="font-extrabold text-slate-800 leading-tight line-clamp-2 select-text"
className="font-extrabold text-content leading-tight line-clamp-2 select-text"
title={paper.title}
>
{paper.title}
</h4>
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-[10px] text-slate-500 font-semibold">
<span className="font-bold text-slate-700">
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-[10px] text-secondary font-semibold">
<span className="font-bold text-secondary">
{paper.first_author} et al.
</span>
<span>·</span>
@@ -351,7 +352,7 @@ export function PaperListCard({
</>
)}
</div>
<div className="text-[9px] font-mono text-slate-400 font-bold select-all truncate">
<div className="text-[9px] font-mono text-tertiary font-bold select-all truncate">
{paper.bibcode}
</div>
</div>
@@ -362,7 +363,7 @@ export function PaperListCard({
{isDownloaded ? (
<button
onClick={() => triggerRead(paper.bibcode)}
className="flex items-center gap-1 px-2.5 py-1 rounded bg-sky-600 hover:bg-sky-700 text-white font-bold text-[10px] transition-colors cursor-pointer"
className="flex items-center gap-1 px-2.5 py-1 rounded bg-blueprint hover:opacity-90 text-white font-bold text-[10px] transition-colors cursor-pointer"
title="立即在对比阅读器中打开"
>
<BookOpen className="w-3 h-3" />
@@ -371,18 +372,18 @@ export function PaperListCard({
) : isDownloading ? (
<button
disabled
className="flex items-center gap-1 px-2.5 py-1 rounded bg-slate-100 text-slate-400 border border-slate-200 font-bold text-[10px] cursor-not-allowed"
className="flex items-center gap-1 px-2.5 py-1 rounded bg-sunken text-tertiary font-bold text-[10px] cursor-not-allowed"
>
<Loader2 className="w-3 h-3 animate-spin text-slate-400" />
<Loader2 className="w-3 h-3 animate-spin text-tertiary" />
<span></span>
</button>
) : (
<button
onClick={() => triggerDownload(paper.bibcode)}
className="flex items-center gap-1 px-2.5 py-1 rounded bg-white hover:bg-slate-50 text-sky-650 border border-sky-200 hover:border-sky-300 font-bold text-[10px] transition-colors cursor-pointer"
className="flex items-center gap-1 px-2.5 py-1 rounded bg-surface hover:bg-sunken text-blueprint border border-blueprint/20 hover:border-blueprint/40 font-bold text-[10px] transition-colors cursor-pointer"
title="下载文献资源到本地"
>
<Download className="w-3 h-3 text-sky-600" />
<Download className="w-3 h-3 text-blueprint" />
<span></span>
</button>
)}
@@ -390,7 +391,7 @@ export function PaperListCard({
{/* 2. 引用拓扑 */}
<button
onClick={() => triggerGalaxy(paper)}
className="flex items-center justify-center p-1 rounded bg-white hover:bg-slate-50 text-slate-500 hover:text-sky-600 border border-slate-200 hover:border-slate-350 transition-colors cursor-pointer"
className="flex items-center justify-center p-1 rounded bg-surface hover:bg-sunken text-secondary hover:text-blueprint hover:border-blueprint/40 transition-colors cursor-pointer"
title="生成并查看引用星系图"
>
<GitFork className="w-3.5 h-3.5" />
@@ -401,7 +402,7 @@ export function PaperListCard({
href={adsUrl}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center p-1 rounded bg-white hover:bg-slate-50 text-slate-500 hover:text-slate-700 border border-slate-200 hover:border-slate-350 transition-colors"
className="flex items-center justify-center p-1 rounded bg-surface hover:bg-sunken text-secondary hover:text-secondary hover:border-subtle transition-colors"
title="访问 NASA ADS 详情页"
>
<ExternalLink className="w-3.5 h-3.5" />
@@ -433,65 +434,100 @@ export function TodoTaskCard({ todos }: TodoTaskCardProps) {
if (!Array.isArray(todos) || todos.length === 0) return null;
return (
<div className="bg-slate-50 border border-slate-200 rounded-lg p-3.5 space-y-3 text-xs shadow-2xs select-none">
<div className="flex items-center gap-1.5 font-bold text-slate-800 text-[11px] uppercase tracking-wide border-b border-slate-200/70 pb-2">
<Activity className="w-3.5 h-3.5 text-blueprint animate-pulse" />
<span></span>
</div>
<div className="space-y-0.5 relative pl-1 select-none">
{todos.map((todo, idx) => {
const isCompleted = todo.status === 'completed';
const isInProgress = todo.status === 'in_progress';
<div className="space-y-2">
{todos.map((todo) => {
const isCompleted = todo.status === 'completed';
const isInProgress = todo.status === 'in_progress';
// 智能依赖阻塞检查
const isBlocked =
todo.blockedBy &&
todo.blockedBy.some((depId) => {
const dep = todos.find((t) => t.id === depId);
return dep && dep.status !== 'completed';
});
// 基于状态选择 Lucide 图标而不用 Emoji
let statusIcon = (
<Circle className="w-4.5 h-4.5 text-slate-350 shrink-0" />
);
let statusStyle = 'border-slate-200 bg-white text-slate-700';
// 状态元素定义
let statusIcon = (
<div className="w-4 h-4 rounded-full border border-subtle bg-surface flex items-center justify-center text-[8px] text-tertiary/60 shrink-0 font-mono font-bold">
{idx + 1}
</div>
);
let rowClass = 'opacity-70';
let titleClass = 'text-secondary font-medium';
let badgeClass = 'bg-sunken text-tertiary';
if (isCompleted) {
statusIcon = (
<CheckCircle2 className="w-4.5 h-4.5 text-emerald-600 shrink-0" />
);
statusStyle =
'border-emerald-100 bg-emerald-50/20 text-slate-400 line-through';
} else if (isInProgress) {
statusIcon = (
<Loader2 className="w-4.5 h-4.5 text-blueprint shrink-0 animate-spin" />
);
statusStyle =
'border-blueprint bg-blueprint/5 text-slate-900 font-bold border-l-4';
}
return (
<div
key={todo.id}
className={`border rounded-lg p-2.5 flex items-start gap-2.5 transition-all ${statusStyle}`}
>
{statusIcon}
<div className="min-w-0 flex-1 space-y-1">
<div className="flex flex-wrap items-center gap-2">
<span className="font-mono text-[9px] bg-slate-200 text-slate-600 px-1 rounded font-extrabold leading-normal select-all">
{todo.id}
</span>
{todo.blockedBy && todo.blockedBy.length > 0 && (
<span className="font-mono text-[8px] bg-amber-50 text-amber-700 border border-amber-200 px-1 rounded flex items-center gap-0.5 leading-normal">
<span>:</span>
<span className="font-bold">
{todo.blockedBy.join(', ')}
</span>
</span>
)}
</div>
<p className="text-[11px] leading-relaxed select-text font-semibold">
{todo.content}
</p>
</div>
if (isCompleted) {
statusIcon = (
<div className="w-4.5 h-4.5 rounded-full bg-emerald-500 border border-emerald-600 flex items-center justify-center shrink-0 shadow-[0_0_6px_rgba(16,185,129,0.2)]">
<Check className="w-2.5 h-2.5 text-white stroke-[3]" />
</div>
);
})}
</div>
rowClass = 'hover:bg-emerald-500/[0.02]';
titleClass = 'text-tertiary line-through font-medium';
badgeClass =
'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400';
} else if (isInProgress) {
statusIcon = (
<div className="w-4.5 h-4.5 rounded-full bg-blueprint border border-blueprint flex items-center justify-center shrink-0 shadow-[0_0_8px_rgba(59,130,246,0.4)] animate-pulse">
<Loader2 className="w-2.5 h-2.5 text-white animate-spin" />
</div>
);
rowClass =
'bg-blueprint/[0.02] dark:bg-blueprint/[0.04] hover:bg-blueprint/[0.04]';
titleClass = 'text-content font-extrabold text-[11.5px]';
badgeClass = 'bg-blueprint/20 text-blueprint font-bold';
} else if (isBlocked) {
statusIcon = (
<div className="w-4.5 h-4.5 rounded-full bg-amber-500/15 border border-amber-500/30 flex items-center justify-center shrink-0">
<Lock className="w-2.5 h-2.5 text-amber-600 dark:text-amber-400" />
</div>
);
rowClass = 'opacity-50 hover:bg-amber-500/[0.01]';
titleClass = 'text-secondary/80 font-medium';
badgeClass = 'bg-amber-500/10 text-amber-700 dark:text-amber-300';
} else {
// 普通 pending 状态
rowClass = 'hover:bg-sunken/40';
}
return (
<div
key={todo.id}
className={`relative flex items-start gap-3.5 px-2 py-2.5 rounded-lg transition-all duration-150 ${rowClass}`}
>
{/* 时间线垂直轴线 */}
{idx < todos.length - 1 && (
<div
className={`absolute left-[17px] top-[26px] bottom-[-16px] w-[1.5px] z-0 ${
isCompleted
? 'bg-emerald-500/40'
: isInProgress
? 'bg-gradient-to-b from-blueprint to-subtle/50'
: 'border-l border-dashed border-subtle/60'
}`}
/>
)}
{/* 左侧状态点 */}
<div className="relative z-10 flex items-center justify-center w-5 h-5 shrink-0">
{statusIcon}
</div>
{/* 右侧单行任务信息 */}
<div className="min-w-0 flex-1 text-[11px] leading-relaxed select-text font-semibold tracking-wide">
<span
className={`inline-block mr-2 px-1.5 py-0.2 rounded text-[8px] font-mono tracking-wider font-extrabold uppercase leading-normal align-middle shrink-0 ${badgeClass}`}
>
{todo.id}
</span>
<span className={`${titleClass} align-middle`}>
{todo.content}
</span>
</div>
</div>
);
})}
</div>
);
}
@@ -518,34 +554,37 @@ export function BgTaskProgressCard({ task }: BgTaskProgressCardProps) {
let statusText = '执行中';
let progressColor = 'bg-blueprint animate-infinite-scroll';
let statusIcon = <Cpu className="w-4 h-4 text-blueprint animate-spin-slow" />;
let cardBg = 'bg-sky-50/20 border-sky-100';
let cardBg =
'bg-sky-50/20 border-transparent dark:bg-sky-500/10 dark:border-sky-500/30';
if (isCompleted) {
statusText = '已完成';
progressColor = 'bg-emerald-600';
statusIcon = <Check className="w-4.5 h-4.5 text-white" />;
cardBg = 'bg-emerald-50/10 border-emerald-100';
cardBg =
'bg-emerald-50/10 border-transparent dark:bg-emerald-500/10 dark:border-emerald-500/30';
} else if (isFailed) {
statusText = '执行失败';
progressColor = 'bg-rose-600';
statusIcon = <AlertTriangle className="w-4.5 h-4.5 text-rose-600" />;
cardBg = 'bg-rose-50/10 border-rose-150';
cardBg =
'bg-rose-50/10 border-transparent dark:bg-rose-500/10 dark:border-rose-500/30';
}
return (
<div
className={`border rounded-lg p-3.5 space-y-3.5 text-xs shadow-2xs select-none ${cardBg}`}
className={`border rounded-lg p-3.5 space-y-3.5 text-xs select-none ${cardBg}`}
>
<div className="flex items-center justify-between border-b border-slate-200/50 pb-2">
<div className="flex items-center gap-1.5 font-bold text-slate-800 text-[11px] uppercase tracking-wide">
<div className="flex items-center justify-between border-b border-subtle pb-2">
<div className="flex items-center gap-1.5 font-bold text-content text-[11px] uppercase tracking-wide">
{isRunning ? (
<Loader2 className="w-3.5 h-3.5 text-blueprint animate-spin" />
) : (
<Activity className="w-3.5 h-3.5 text-slate-600" />
<Activity className="w-3.5 h-3.5 text-secondary" />
)}
<span></span>
</div>
<span className="font-mono text-[9px] bg-slate-200 text-slate-600 px-1.5 py-0.2 rounded font-extrabold select-all">
<span className="font-mono text-[9px] bg-border-subtle text-secondary px-1.5 py-0.2 rounded font-extrabold select-all">
ID: {task_id}
</span>
</div>
@@ -554,23 +593,23 @@ export function BgTaskProgressCard({ task }: BgTaskProgressCardProps) {
<div
className={`w-8 h-8 rounded-full flex items-center justify-center border shrink-0 ${
isRunning
? 'bg-sky-100/60 border-sky-200'
? 'bg-sky-100/60 border-transparent dark:bg-sky-500/15 dark:border-sky-500/30'
: isCompleted
? 'bg-emerald-600 border-emerald-700'
: 'bg-rose-100 border-rose-200'
: 'bg-rose-100 border-transparent dark:bg-rose-500/15 dark:border-rose-500/30'
}`}
>
{statusIcon}
</div>
<div className="min-w-0 flex-1 space-y-1.5">
<div className="grid grid-cols-2 gap-x-2 gap-y-0.5 text-slate-500 font-semibold text-[10px]">
<div className="grid grid-cols-2 gap-x-2 gap-y-0.5 text-secondary font-semibold text-[10px]">
<div>
<span></span>
<code className="text-slate-700 font-bold">{tool_name}</code>
<code className="text-secondary font-bold">{tool_name}</code>
</div>
<div>
<span></span>
<code className="text-slate-700 font-bold truncate block select-all">
<code className="text-secondary font-bold truncate block select-all">
{bibcode}
</code>
</div>
@@ -582,8 +621,8 @@ export function BgTaskProgressCard({ task }: BgTaskProgressCardProps) {
isRunning
? 'text-blueprint'
: isCompleted
? 'text-emerald-700'
: 'text-rose-700'
? 'text-success'
: 'text-danger'
}`}
>
{statusText}
@@ -591,7 +630,7 @@ export function BgTaskProgressCard({ task }: BgTaskProgressCardProps) {
</div>
{/* 进度条 */}
<div className="w-full h-1.5 bg-slate-100 rounded-full overflow-hidden relative">
<div className="w-full h-1.5 bg-sunken rounded-full overflow-hidden relative">
<div
className={`h-full rounded-full transition-all duration-500 ${progressColor} ${
isRunning ? 'w-2/3 animate-pulse' : 'w-full'
@@ -600,7 +639,7 @@ export function BgTaskProgressCard({ task }: BgTaskProgressCardProps) {
</div>
{isRunning && (
<p className="text-[9px] text-slate-400 font-medium leading-relaxed">
<p className="text-[9px] text-tertiary font-medium leading-relaxed">
</p>
)}
@@ -655,18 +694,18 @@ export function VizierResultCard({ metadata }: VizierResultCardProps) {
: 'https://vizier.cds.unistra.fr/';
return (
<div className="bg-violet-50/40 border border-violet-100 rounded-lg p-3.5 space-y-3 text-xs shadow-2xs">
<div className="bg-violet-50/40 border border-transparent dark:bg-violet-500/10 dark:border-violet-500/30 rounded-lg p-3.5 space-y-3 text-xs">
{/* 标题栏 */}
<div className="flex items-center justify-between border-b border-violet-100/70 pb-2">
<div className="flex items-center gap-1.5 font-bold text-violet-900 text-[11px] uppercase tracking-wide">
<TableIcon className="w-3.5 h-3.5 text-violet-600" />
<div className="flex items-center justify-between border-b border-transparent dark:border-violet-500/30 pb-2">
<div className="flex items-center gap-1.5 font-bold text-violet-900 dark:text-violet-300 text-[11px] uppercase tracking-wide">
<TableIcon className="w-3.5 h-3.5 text-violet-600 dark:text-violet-300" />
<span>VizieR </span>
</div>
<a
href={cdsUrl}
target="_blank"
rel="noopener noreferrer"
className="text-[10px] font-bold text-violet-600 hover:text-violet-800 flex items-center gap-0.5 hover:underline"
className="text-[10px] font-bold text-violet-600 dark:text-violet-300 hover:opacity-90 flex items-center gap-0.5 hover:underline"
>
<span>CDS</span>
<ExternalLink className="w-2.5 h-2.5" />
@@ -674,19 +713,19 @@ export function VizierResultCard({ metadata }: VizierResultCardProps) {
</div>
{/* 元信息 */}
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-[10px] text-slate-600">
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-[10px] text-secondary">
{table_name && (
<span className="flex items-center gap-1 font-semibold text-violet-700">
<span className="flex items-center gap-1 font-semibold text-violet-700 dark:text-violet-300">
<Database className="w-3 h-3" />
{table_name}
</span>
)}
<span className="bg-violet-100 text-violet-700 px-1.5 py-0.5 rounded font-medium">
<span className="bg-violet-100 text-violet-700 dark:bg-violet-500/15 dark:text-violet-300 px-1.5 py-0.5 rounded font-medium">
{row_count}
</span>
<span className="text-slate-500">{fields.length} </span>
<span className="text-secondary">{fields.length} </span>
{truncated && (
<span className="flex items-center gap-0.5 text-amber-600 font-medium">
<span className="flex items-center gap-0.5 text-amber-600 dark:text-amber-300 font-medium">
<AlertTriangle className="w-3 h-3" />
</span>
@@ -695,16 +734,16 @@ export function VizierResultCard({ metadata }: VizierResultCardProps) {
{/* 表格 */}
{fields.length === 0 || rows.length === 0 ? (
<p className="text-[11px] text-slate-400 italic py-2">()</p>
<p className="text-[11px] text-tertiary italic py-2">()</p>
) : (
<div className="overflow-x-auto max-h-72 overflow-y-auto border border-slate-200 rounded">
<div className="overflow-x-auto max-h-72 overflow-y-auto rounded">
<table className="w-full text-[10px] border-collapse">
<thead className="sticky top-0 bg-slate-100 z-10">
<thead className="sticky top-0 bg-sunken z-10">
<tr>
{fields.map((f, i) => (
<th
key={i}
className="px-2 py-1.5 text-left font-bold text-slate-600 border-b border-slate-200 whitespace-nowrap"
className="px-2 py-1.5 text-left font-bold text-secondary border-b border-subtle whitespace-nowrap"
title={
f.description || f.unit
? `${f.description || ''} ${f.unit ? `[${f.unit}]` : ''}`.trim()
@@ -713,7 +752,7 @@ export function VizierResultCard({ metadata }: VizierResultCardProps) {
>
{f.name}
{f.unit && (
<span className="text-slate-400 font-normal ml-1">
<span className="text-tertiary font-normal ml-1">
[{f.unit}]
</span>
)}
@@ -725,12 +764,12 @@ export function VizierResultCard({ metadata }: VizierResultCardProps) {
{displayRows.map((row, ri) => (
<tr
key={ri}
className={ri % 2 === 0 ? 'bg-white' : 'bg-slate-50/50'}
className={ri % 2 === 0 ? 'bg-surface' : 'bg-sunken/50'}
>
{fields.map((_, ci) => (
<td
key={ci}
className="px-2 py-1 text-slate-700 border-b border-slate-100 whitespace-nowrap font-mono"
className="px-2 py-1 text-secondary border-b border-subtle whitespace-nowrap font-mono"
>
{renderCell(row[ci])}
</td>
@@ -744,7 +783,7 @@ export function VizierResultCard({ metadata }: VizierResultCardProps) {
{/* 底部提示 */}
{hiddenCount > 0 && (
<p className="text-[10px] text-slate-400">
<p className="text-[10px] text-tertiary">
{MAX_DISPLAY_ROWS} {hiddenCount}
</p>
)}
@@ -754,11 +793,11 @@ export function VizierResultCard({ metadata }: VizierResultCardProps) {
// ==========================================
// 6. 统一观测数据下载结果卡片 (find_observation)
// 跨 (LAMOST/Gaia/SDSS/DESI) × (Spectrum/LightCurve/Photometry/Image)
// 支持多 artifact 产物(如 Gaia 光变 G/BP/RP 三波段各一个文件)
// 跨 (LAMOST/Gaia/SDSS/DESI) × (Spectrum/LightCurve/Photometry/Image)
// 支持多 artifact 产物(如 Gaia 光变 G/BP/RP 三波段各一个文件)
//
// 实现已抽出到共享组件 ObservationResultCard,本函数仅作为 Agent 工具结果
// 渲染入口(metadata 形状与 ObservationBatchResult 完全一致)。
// 实现已抽出到共享组件 ObservationResultCard,本函数仅作为 Agent 工具结果
// 渲染入口(metadata 形状与 ObservationBatchResult 完全一致)。
// ==========================================
import { ObservationResultCard } from '../observation/ObservationResultCard';
import type { ObservationBatchResult } from '../observation/constants';
@@ -1,6 +1,6 @@
// dashboard/src/components/agent/SubAgentContainer.tsx
// 子代理容器:展示子代理的执行过程(可折叠,含嵌套时间线)
import { Network, CheckCircle2, Loader } from 'lucide-react';
import { Network, Loader } from 'lucide-react';
import type { TimelineItem } from './types';
import { SUBAGENT_COLORS } from './constants';
import { ThoughtCard } from './ThoughtCard';
@@ -26,7 +26,7 @@ interface SubAgentContainerProps {
export function SubAgentContainer({
status,
children,
summary,
summary: _summary,
isStreaming: parentStreaming,
isCollapsed,
onToggle,
@@ -42,84 +42,80 @@ export function SubAgentContainer({
const toolCount = children.filter((c) => c.type === 'tool_call').length;
return (
<div className="relative space-y-2 select-none">
<div className="relative space-y-1.5 select-none w-full">
<div
className={`absolute -left-[21px] top-3 w-2.5 h-2.5 rounded-full border-2 border-white ${
parentStreaming ? 'bg-violet-500' : 'bg-violet-400'
parentStreaming ? 'bg-amber-500' : 'bg-amber-400'
}`}
/>
<div className="border border-violet-200 bg-violet-50/30 rounded-md shadow-2xs overflow-hidden">
{/* 头部 */}
<button
onClick={onToggle}
className="w-full flex items-center justify-between px-3.5 py-2.5 cursor-pointer hover:bg-violet-50/80 transition-colors"
>
<div className="flex items-center gap-2">
<Network
className={`w-4 h-4 ${
parentStreaming ? 'text-violet-500' : 'text-violet-400'
}`}
/>
<span className="text-[10px] font-extrabold text-violet-700 tracking-wider uppercase">
{parentStreaming ? '子代理执行中...' : '子代理完成'}
</span>
{isComplete && (
<span className="text-[9px] font-bold text-violet-500 ml-1 flex items-center gap-0.5">
<CheckCircle2 className="w-3 h-3" />
{stepCount} · {toolCount}
</span>
)}
{parentStreaming && (
<Loader className="w-3 h-3 text-violet-500 animate-spin" />
)}
</div>
<span className="text-[10px] font-bold text-violet-600 hover:text-violet-800 shrink-0">
{isCollapsed ? '展开' : '收起'}
{/* Capsule Header (w-fit) - 控制台按钮体系 */}
<div
onClick={onToggle}
className={`rounded-full px-3.5 py-1.5 w-fit cursor-pointer flex items-center gap-2 border select-none transition-all duration-200 ${
!isCollapsed
? 'bg-amber-500/5 border-amber-500 shadow-sm'
: 'bg-surface border-subtle shadow-sm hover:bg-sunken hover:border-strong'
}`}
>
<Network
className={`w-3.5 h-3.5 ${
parentStreaming ? 'text-amber-500 animate-pulse' : 'text-amber-500'
}`}
/>
<span className="text-[10px] font-extrabold text-amber-700 dark:text-amber-300 tracking-wider uppercase">
{parentStreaming ? '子代理执行中...' : '子代理完成'}
</span>
{isComplete && (
<span className="text-[8px] text-tertiary font-bold ml-1.5 bg-sunken px-1.5 py-0.5 rounded-full border border-subtle">
{stepCount} · {toolCount}
</span>
</button>
{/* 折叠态摘要 */}
{isCollapsed && summary && (
<div className="px-3.5 pb-3 border-t border-violet-100/50">
<p className="text-[11px] text-violet-600 font-medium leading-relaxed line-clamp-2 mt-2">
{summary.length > 200 ? summary.slice(0, 200) + '...' : summary}
</p>
</div>
)}
{isCollapsed && !summary && parentStreaming && (
<div className="px-3.5 pb-3 border-t border-violet-100/50">
<p className="text-[11px] text-violet-400 italic font-medium mt-2">
...
</p>
</div>
)}
</div>
{/* 展开态:嵌套时间线 */}
{/* 展开的详情:向右缩进的单独子容器 (CSS Grid 折叠动效) */}
<div
className={`grid transition-all duration-300 ease-in-out ml-8 relative ${
!isCollapsed
? 'grid-rows-[1fr] opacity-100 mt-2'
: 'grid-rows-[0fr] opacity-0 pointer-events-none'
}`}
>
{/* L型树形连接线 (L-shaped Connector Line) - 增强对比,向右偏移 */}
{!isCollapsed && (
<div className="border-t border-violet-100/50 px-3.5 py-3">
<div className="absolute -left-4 -top-1.5 w-4 h-4.5 border-l border-b border-dashed border-tertiary/60 rounded-bl-md pointer-events-none" />
)}
<div className="overflow-hidden">
<div className="console-panel rounded-lg p-3.5 space-y-3.5 border-l-2 border-l-amber-500/50 shadow-inner">
{/* 子代理正在初始化步骤 */}
{children.length === 0 && parentStreaming && (
<div className="flex items-center gap-2 text-violet-400 py-2">
<Loader className="w-3 h-3 animate-spin" />
<span className="text-[10px] font-bold">...</span>
<div className="flex items-center gap-2 text-amber-500/70 py-1.5 animate-in fade-in duration-200">
<Loader className="w-3.5 h-3.5 animate-spin" />
<span className="text-[10px] font-bold">
...
</span>
</div>
)}
{/* 嵌套时间线渲染 */}
{children.length > 0 && (
<div className="pl-3.5 border-l border-dashed border-amber-500/20 space-y-3.5 nested-timeline-wrapper">
{children.map((child, childIdx) =>
renderNestedTimelineItem(
child,
childIdx,
parentStreaming,
expandedThoughts,
expandedArgs,
expandedResults,
onToggleThought,
onToggleArgs,
onToggleResult
)
)}
</div>
)}
<div className="pl-4 border-l border-violet-100 space-y-3">
{children.map((child, childIdx) =>
renderNestedTimelineItem(
child,
childIdx,
parentStreaming,
expandedThoughts,
expandedArgs,
expandedResults,
onToggleThought,
onToggleArgs,
onToggleResult
)
)}
</div>
</div>
)}
</div>
</div>
</div>
);
@@ -153,12 +149,13 @@ function renderNestedTimelineItem(
);
}
case 'tool_call': {
const tcId = item.id;
const tcId = item.id || `sub-tc-${item.step}-${idx}`;
return (
<ToolCallCard
key={`tc-${tcId}-${idx}`}
step={item.step}
name={item.name}
display_name={item.display_name}
arguments={item.arguments}
result={item.result}
isStreaming={isStreaming}
+27 -35
View File
@@ -13,63 +13,55 @@ interface ThoughtCardProps {
}
export function ThoughtCard({
step,
content,
isStreaming,
colorScheme,
isExpanded,
onToggle,
}: ThoughtCardProps) {
const preview = content.length > 120 ? content.slice(0, 120) + '…' : content;
const dotColor = isStreaming
? colorScheme.thought.active
: colorScheme.thought.idle;
// 从 colorScheme 推导包裹色(用于容器背景)
const isSub = colorScheme.thought.idle.includes('violet');
const bgClass = isSub
? 'bg-violet-50/40 border border-violet-100/50'
: 'bg-purple-50/40 border border-purple-100/50';
const textClass = isSub ? 'text-violet-600' : 'text-purple-600';
const previewClass = isSub ? 'text-violet-400' : 'text-purple-400';
const hoverClass = isSub
? 'text-violet-500 hover:text-violet-700'
: 'text-purple-500 hover:text-purple-700';
const mutedClass = isSub ? 'text-violet-400' : 'text-purple-400';
// 统一使用高级学术浅灰色(Stone Gray)风格
const bgClass =
'bg-stone-100/50 border border-transparent dark:bg-stone-900/30 dark:border-stone-800/30';
const textClass = 'text-stone-600 dark:text-stone-300';
const handleCardClick = (e: React.MouseEvent) => {
const target = e.target as HTMLElement;
if (
target.closest('a') ||
target.closest('button') ||
target.closest('input') ||
target.closest('.thought-content')
) {
return;
}
onToggle();
};
return (
<div className="relative space-y-2 select-none">
<div className="relative space-y-2 select-none w-full">
<div
className={`absolute -left-[21px] top-3 w-2.5 h-2.5 rounded-full border-2 border-white ${dotColor}`}
/>
<div className={`space-y-1.5 rounded-md p-3 select-text ${bgClass}`}>
<button
onClick={onToggle}
className="w-full flex items-center justify-between text-left cursor-pointer"
>
<div
onClick={handleCardClick}
className={`space-y-1.5 rounded-md p-3 select-text cursor-pointer ${bgClass}`}
>
<div className="w-full flex items-center justify-between text-left">
<div
className={`flex items-center gap-1.5 text-[10px] font-extrabold tracking-wider uppercase ${textClass}`}
>
<Brain className="w-3.5 h-3.5" />
<span> (Thought)</span>
<span className={`text-[9px] font-mono ${mutedClass}`}>
· Step {step}
</span>
<span></span>
</div>
<span className={`text-[10px] font-bold shrink-0 ${hoverClass}`}>
{isExpanded ? '收起' : '展开'}
</span>
</button>
{isExpanded ? (
<p className="text-slate-700 font-medium leading-relaxed font-sans whitespace-pre-wrap text-xs">
</div>
{isExpanded && (
<p className="text-secondary font-medium leading-relaxed font-sans whitespace-pre-wrap text-xs thought-content">
{content}
</p>
) : (
<p
className={`text-[11px] italic font-medium leading-relaxed ${previewClass}`}
>
{preview}
</p>
)}
</div>
</div>
+130 -62
View File
@@ -1,8 +1,7 @@
// dashboard/src/components/agent/ToolCallCard.tsx
// 工具调用卡片:展示参数和结果,皆可独立折叠
import { Settings, Eye } from 'lucide-react';
import { Settings } from 'lucide-react';
import type { ColorScheme } from './constants';
import { getToolDisplayName } from './toolDisplayNames';
import { AgentMarkdown } from './AgentMarkdown';
import { useAutoScroll } from './useAutoScroll';
import {
@@ -21,6 +20,7 @@ import type { TabId } from '../layout/Sidebar';
interface ToolCallCardProps {
step: number;
name: string;
display_name?: string;
arguments: Record<string, unknown>;
result?: {
output: string;
@@ -40,8 +40,8 @@ interface ToolCallCardProps {
}
export function ToolCallCard({
step,
name,
display_name,
arguments: args,
result,
isStreaming,
@@ -271,80 +271,148 @@ export function ToolCallCard({
const specialCard = renderSpecialCard();
const isExpanded = isArgsExpanded || isResultExpanded;
const handleHeaderClick = () => {
if (isExpanded) {
if (isArgsExpanded) onToggleArgs();
if (isResultExpanded) onToggleResult();
} else {
if (hasResult) {
onToggleResult();
} else {
onToggleArgs();
}
}
};
const handleTabClick = (tab: 'args' | 'result') => {
if (tab === 'args') {
if (!isArgsExpanded) onToggleArgs();
if (isResultExpanded) onToggleResult();
} else {
if (!isResultExpanded) onToggleResult();
if (isArgsExpanded) onToggleArgs();
}
};
const detailBorderColor = isArgsExpanded
? 'border-l-blueprint/60'
: result?.isError
? 'border-l-danger/60'
: 'border-l-success/60';
return (
<div className="relative space-y-2 select-none w-full">
<div className="relative space-y-1.5 select-none w-full">
<div
className={`absolute -left-[21px] top-3 w-2.5 h-2.5 rounded-full border-2 border-white ${dotColor}`}
/>
<div className="space-y-2 border border-slate-100 bg-white rounded-md p-3 shadow-2xs w-full overflow-hidden">
{/* 工具名称 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-1.5 text-[10px] font-extrabold text-sky-700 tracking-wider uppercase">
<Settings
className={`w-3.5 h-3.5 ${hasResult ? 'text-slate-500' : 'text-sky-500 animate-spin'}`}
/>
<span>: {getToolDisplayName(name)}</span>
<span className="text-[9px] text-slate-400 font-mono font-medium">
· Step {step}
</span>
</div>
<button
onClick={onToggleArgs}
className="text-[10px] font-bold text-sky-600 hover:text-sky-800 hover:underline cursor-pointer flex items-center gap-0.5"
>
{isArgsExpanded ? '收起参数' : '展开参数'}
</button>
</div>
{/* Capsule Header (w-fit) - 控制台按钮体系 */}
<div
onClick={handleHeaderClick}
className={`rounded-full px-3.5 py-1.5 w-fit cursor-pointer flex items-center gap-2 border select-none transition-all duration-200 ${
isExpanded
? 'bg-blueprint/5 border-blueprint shadow-sm'
: 'bg-surface border-subtle shadow-sm hover:bg-sunken hover:border-strong'
}`}
>
<Settings
className={`w-3.5 h-3.5 ${hasResult ? 'text-secondary' : 'text-blueprint animate-spin'}`}
/>
<span className="text-[10px] font-extrabold text-blueprint tracking-wider uppercase">
: {display_name || name}
</span>
</div>
{/* 参数 */}
{isArgsExpanded && (
<div className="font-mono text-[10px] bg-slate-50 border border-slate-200 rounded-md p-2 text-slate-650 overflow-x-auto whitespace-pre-wrap max-w-full leading-relaxed select-text">
{JSON.stringify(args, null, 2)}
</div>
{/* 展开的详情:向右缩进的单独子容器 (CSS Grid 折叠动效) */}
<div
className={`grid transition-all duration-300 ease-in-out ml-8 relative ${
isExpanded
? 'grid-rows-[1fr] opacity-100 mt-2'
: 'grid-rows-[0fr] opacity-0 pointer-events-none'
}`}
>
{/* L型树形连接线 (L-shaped Connector Line) - 增强对比,向右偏移 */}
{isExpanded && (
<div className="absolute -left-4 -top-1.5 w-4 h-4.5 border-l border-b border-dashed border-tertiary/60 rounded-bl-md pointer-events-none" />
)}
{/* 结果/特殊卡片 */}
{specialCard ? (
<div className="border-t border-slate-100 pt-2.5 mt-2.5 w-full overflow-hidden">
{specialCard}
</div>
) : (
hasResult && (
<div className="space-y-1.5 border-t border-slate-100 pt-2.5 mt-2.5">
<div className="flex items-center justify-between">
<div className="flex items-center gap-1.5 text-[10px] font-extrabold text-emerald-700 tracking-wider uppercase">
<Eye className="w-3.5 h-3.5" />
<span></span>
{result!.isError && (
<span className="px-1.5 py-0.2 rounded bg-rose-50 text-rose-700 border border-rose-100 text-[9px]">
</span>
)}
</div>
<div className="overflow-hidden">
<div
className={`console-panel rounded-lg p-3.5 space-y-3.5 border-l-2 ${detailBorderColor} shadow-inner`}
>
{/* 标签栏 (Tabs) - 极简拟物分段控制器 */}
<div className="flex bg-sunken border border-subtle rounded-full p-0.5 w-fit gap-0.5 select-none shadow-inner">
<button
onClick={() => handleTabClick('args')}
className={`px-3 py-1 text-[9px] font-bold rounded-full transition-all duration-200 cursor-pointer select-none flex items-center gap-1.5 ${
isArgsExpanded
? 'bg-surface text-content shadow-sm border border-subtle'
: 'border border-transparent text-secondary hover:text-content'
}`}
>
<span className="w-1.5 h-1.5 rounded-full bg-blueprint/60" />
<span></span>
</button>
{hasResult && (
<button
onClick={onToggleResult}
className="text-[10px] font-bold text-sky-600 hover:text-sky-800 hover:underline cursor-pointer flex items-center gap-0.5"
onClick={() => handleTabClick('result')}
className={`px-3 py-1 text-[9px] font-bold rounded-full transition-all duration-200 cursor-pointer select-none flex items-center gap-1.5 ${
isResultExpanded
? 'bg-surface text-content shadow-sm border border-subtle'
: 'border border-transparent text-secondary hover:text-content'
}`}
>
{isResultExpanded ? '收起结果' : '展开结果'}
<span
className={`w-1.5 h-1.5 rounded-full ${
result!.isError
? 'bg-danger/80 animate-pulse'
: 'bg-success/60'
}`}
/>
<span></span>
</button>
</div>
{isResultExpanded ? (
)}
</div>
{/* 统一内容区 (虚线分隔,平铺展示,带滚动条占位稳定器) */}
<div className="mt-2.5 pt-2.5 border-t border-dashed border-subtle">
{/* 传入参数内容 */}
{isArgsExpanded && (
<div
ref={scrollContainerRef}
onScroll={handleScroll}
className="text-xs bg-slate-50 border border-slate-200 rounded-md p-2.5 text-slate-700 overflow-auto select-text leading-relaxed max-h-[60vh] prose prose-sm max-w-none prose-headings:text-slate-800 prose-code:text-sky-700 prose-pre:bg-slate-100 prose-pre:text-xs prose-img:rounded-lg"
key="args-content"
onClick={(e) => e.stopPropagation()}
className="font-mono text-[9px] bg-sunken border-l-2 border-blueprint/30 rounded-r-md p-2.5 text-secondary overflow-x-auto whitespace-pre-wrap max-w-full leading-relaxed select-text animate-in fade-in duration-200 [scrollbar-gutter:stable]"
>
<AgentMarkdown>{result!.output}</AgentMarkdown>
<div ref={chatEndRef} />
{JSON.stringify(args, null, 2)}
</div>
) : (
<div className="text-[10px] text-slate-400 italic font-medium">
( {result!.output.length} )
)}
{/* 返回结果内容 */}
{isResultExpanded && (
<div
key="result-content"
className="animate-in fade-in duration-200"
>
{specialCard ? (
<div className="w-full overflow-hidden special-card-wrap">
{specialCard}
</div>
) : (
<div
ref={scrollContainerRef}
onScroll={handleScroll}
onClick={(e) => e.stopPropagation()}
className="text-xs text-secondary overflow-auto select-text leading-relaxed max-h-[60vh] prose prose-sm max-w-none prose-headings:text-content prose-code:text-blueprint prose-pre:bg-sunken prose-pre:text-xs prose-img:rounded-lg [scrollbar-gutter:stable]"
>
<AgentMarkdown>{result!.output}</AgentMarkdown>
<div ref={chatEndRef} />
</div>
)}
</div>
)}
</div>
)
)}
</div>
</div>
</div>
</div>
);
+6 -6
View File
@@ -40,13 +40,13 @@ export interface ColorScheme {
}
export const PARENT_COLORS: ColorScheme = {
thought: { active: 'bg-slate-400', idle: 'bg-slate-300' },
tool_call: { active: 'bg-blueprint', idle: 'bg-slate-300' },
answer: 'bg-slate-550',
thought: { active: 'bg-secondary', idle: 'bg-tertiary' },
tool_call: { active: 'bg-blueprint', idle: 'bg-tertiary' },
answer: 'bg-success',
};
export const SUBAGENT_COLORS: ColorScheme = {
thought: { active: 'bg-slate-500', idle: 'bg-slate-400' },
tool_call: { active: 'bg-blueprint', idle: 'bg-slate-350' },
answer: 'bg-slate-450',
thought: { active: 'bg-amber-500', idle: 'bg-amber-400' },
tool_call: { active: 'bg-blueprint', idle: 'bg-tertiary' },
answer: 'bg-success',
};
+2 -1
View File
@@ -2,7 +2,8 @@
// Agent 渲染共享模块 barrel export
export { safeSchema, PARENT_COLORS, SUBAGENT_COLORS } from './constants';
export type { ColorScheme } from './constants';
export { getToolDisplayName } from './toolDisplayNames';
export { getToolMeta, setGlobalTools } from './toolDisplayNames';
export type { AgentToolMeta } from './toolDisplayNames';
export { AgentMarkdown } from './AgentMarkdown';
export { ThoughtCard } from './ThoughtCard';
export { ToolCallCard } from './ToolCallCard';
@@ -84,7 +84,9 @@ export function routeToolCall(
step: number,
id: string,
name: string,
args: Record<string, unknown>
args: Record<string, unknown>,
is_internal?: boolean,
display_name?: string
): TimelineItem[] {
// 创建 subagent_container
if (isSubagentContainerCall(name)) {
@@ -120,17 +122,35 @@ export function routeToolCall(
step,
id,
name: cleanName,
display_name,
arguments: args,
is_internal,
},
];
});
}
// 无活跃容器 → fallback
return upsertParentToolCall(timeline, step, id, cleanName, args);
return upsertParentToolCall(
timeline,
step,
id,
cleanName,
args,
is_internal,
display_name
);
}
// 父代理工具调用
return upsertParentToolCall(timeline, step, id, name, args);
return upsertParentToolCall(
timeline,
step,
id,
name,
args,
is_internal,
display_name
);
}
// 路由 tool_result 事件
@@ -140,7 +160,9 @@ export function routeToolResult(
name: string,
output: string,
isError: boolean,
metadata?: Record<string, unknown>
metadata?: Record<string, unknown>,
is_internal?: boolean,
display_name?: string
): TimelineItem[] {
// 匹配 subagent_container id → 完成容器
const saCompleteIdx = timeline.findIndex(
@@ -167,7 +189,12 @@ export function routeToolResult(
return updateContainerChild(timeline, saIdx, (children) =>
children.map((c) => {
if (c.type === 'tool_call' && c.id === toolCallId && !c.result) {
return { ...c, result: { output, isError, metadata } };
return {
...c,
is_internal: is_internal ?? c.is_internal,
display_name: display_name ?? c.display_name,
result: { output, isError, metadata },
};
}
return c;
})
@@ -178,7 +205,12 @@ export function routeToolResult(
// 父代理工具结果
return timeline.map((t) => {
if (t.type === 'tool_call' && t.id === toolCallId) {
return { ...t, result: { output, isError, metadata } };
return {
...t,
is_internal: is_internal ?? t.is_internal,
display_name: display_name ?? t.display_name,
result: { output, isError, metadata },
};
}
return t;
});
@@ -231,7 +263,9 @@ function upsertParentToolCall(
step: number,
id: string,
name: string,
args: Record<string, unknown>
args: Record<string, unknown>,
is_internal?: boolean,
display_name?: string
): TimelineItem[] {
const existing = timeline.find(
(t) => t.type === 'tool_call' && 'id' in t && t.id === id
@@ -239,7 +273,15 @@ function upsertParentToolCall(
if (existing) return timeline;
return [
...timeline,
{ type: 'tool_call' as const, step, id, name, arguments: args },
{
type: 'tool_call' as const,
step,
id,
name,
display_name,
arguments: args,
is_internal,
},
];
}
@@ -1,87 +1,37 @@
// dashboard/src/components/agent/toolDisplayNames.ts
// 工具名称 → 人类可读中文名称(所有面板共用)
// 共享工具元数据客户端缓存
export function getToolDisplayName(name: string): string {
switch (name) {
// 文件系统工具
case 'read_file':
return '读取文件内容';
case 'grep_files':
return '正则搜索文件';
case 'glob_files':
return '通配符匹配文件';
case 'run_bash':
return '执行 Shell 命令';
case 'file_write':
return '写入文件';
case 'file_edit':
return '精确编辑文件';
// 天文科研工具
case 'search_papers':
return '检索 ADS/arXiv 文献';
case 'get_paper_metadata':
return '获取文献详细元数据';
case 'download_paper':
return '下载文献全文资源';
case 'parse_paper':
return '结构化解析文献内容';
case 'get_paper_content':
return '获取文献全文内容';
case 'rag_search':
return '检索馆藏知识库';
case 'query_target':
return '查询天体物理参数 (CDS)';
case 'query_vizier':
return '查询 VizieR 星表';
case 'cone_search':
return '锥形检索天体';
case 'find_observation':
return '下载观测数据';
case 'catalog_operation':
return '星表操作';
case 'save_note':
return '保存文献手札';
// Agent 控制工具
case 'todo_write':
return '管理任务列表';
case 'compress_context':
return '压缩上下文窗口';
case 'load_skill':
return '加载专家技能';
case 'subagent':
return '派发子代理';
case 'delegate_research':
return '派发子代理';
case 'ask_user':
return '向用户提问';
// 记忆系统
case 'save_memory':
return '保存项目记忆';
case 'load_memory':
return '读取项目记忆';
// 图片分析
case 'analyze_image':
return '分析图像内容';
// 后台任务
case 'bg_task_run':
return '启动后台任务';
case 'bg_task_check':
return '检查后台任务状态';
// 团队协作
case 'spawn_teammate':
return '创建团队成员';
case 'send_teammate_message':
return '发送团队成员消息';
case 'team_broadcast':
return '团队广播消息';
case 'check_team_inbox':
return '检查团队收件箱';
default: {
// 子代理工具名带 [sub] 前缀
if (name.startsWith('[sub] ')) {
return `子代理: ${getToolDisplayName(name.slice(6))}`;
}
return name;
}
}
export interface AgentToolMeta {
name: string;
display_name: string;
is_internal: boolean;
}
let globalToolsCache: Record<string, AgentToolMeta> = {};
export function setGlobalTools(tools: AgentToolMeta[]) {
const map: Record<string, AgentToolMeta> = {};
for (const t of tools) {
map[t.name] = t;
}
globalToolsCache = map;
}
export function getToolMeta(name: string): AgentToolMeta | undefined {
if (name.startsWith('[sub] ')) {
const inner = getToolMeta(name.slice(6));
if (inner) {
return {
name,
display_name: `子代理: ${inner.display_name}`,
is_internal: inner.is_internal,
};
}
return {
name,
display_name: name,
is_internal: false,
};
}
return globalToolsCache[name];
}
+8 -2
View File
@@ -9,7 +9,9 @@ export type TimelineItem =
step: number;
id: string;
name: string;
display_name?: string;
arguments: Record<string, unknown>;
is_internal?: boolean;
result?: {
output: string;
isError: boolean;
@@ -33,7 +35,9 @@ export interface SSEEventHandlers {
step: number,
id: string,
name: string,
args: Record<string, unknown>
args: Record<string, unknown>,
is_internal?: boolean,
display_name?: string
) => void;
onToolResult?: (
step: number,
@@ -41,7 +45,9 @@ export interface SSEEventHandlers {
name: string,
output: string,
isError: boolean,
metadata?: Record<string, unknown>
metadata?: Record<string, unknown>,
is_internal?: boolean,
display_name?: string
) => void;
onTextDelta?: (content: string, toolCallId?: string) => void;
onUsage?: (usage: {
@@ -100,7 +100,9 @@ export function useAgentSSE(): UseAgentSSEReturn {
event.step,
event.id,
event.name,
event.arguments
event.arguments,
event.is_internal,
event.display_name
);
break;
case 'tool_result':
@@ -110,7 +112,9 @@ export function useAgentSSE(): UseAgentSSEReturn {
event.name,
event.output,
event.is_error,
event.metadata
event.metadata,
event.is_internal,
event.display_name
);
break;
case 'text_delta':
@@ -31,7 +31,7 @@ export function BaseModal({
return (
<div
onClick={onClose}
className="fixed inset-0 flex items-center justify-center bg-slate-900/40 backdrop-blur-xs transition-all animate-fade-in cursor-pointer"
className="fixed inset-0 flex items-center justify-center bg-black/40 dark:bg-black/60 backdrop-blur-xs transition-all animate-fade-in cursor-pointer"
style={{ zIndex }}
>
<div
@@ -39,7 +39,7 @@ export function BaseModal({
className={`bg-card rounded-lg border border-precision shadow-sm w-full p-6 space-y-4 cursor-default animate-fade-in mx-4 ${sizeClasses[size]}`}
>
{/* 头部栏 */}
<div className="flex items-start justify-between border-b border-slate-150 pb-2.5 shrink-0">
<div className="flex items-start justify-between border-b border-subtle pb-2.5 shrink-0">
<div className="min-w-0 flex-1">
{typeof title === 'string' ? (
<h3 className="text-xs font-bold text-main flex items-center gap-1.5 pr-4">
@@ -56,7 +56,7 @@ export function BaseModal({
</div>
<button
onClick={onClose}
className="text-slate-400 hover:text-slate-600 p-1 rounded-md hover:bg-slate-100 transition-colors shrink-0 cursor-pointer"
className="text-tertiary hover:text-secondary p-1 rounded-md hover:bg-sunken transition-colors shrink-0 cursor-pointer"
title="关闭"
>
<svg
@@ -71,7 +71,7 @@ export function PaperDetailModal({
</div>
{/* 期刊 & 年份 */}
<div className="grid grid-cols-5 gap-4 border-y border-slate-150 py-2.5 shrink-0 h-14">
<div className="grid grid-cols-5 gap-4 border-y border-subtle py-2.5 shrink-0 h-14">
<div className="space-y-0.5 flex flex-col justify-center min-w-0 col-span-4">
<span className="text-muted font-bold block text-[10px] leading-tight">
@@ -96,7 +96,7 @@ export function PaperDetailModal({
{/* 摘要 */}
<div className="space-y-1.5 flex flex-col h-40 shrink-0">
<span className="text-muted font-bold block"></span>
<p className="text-main leading-relaxed font-normal bg-slate-50 p-3 flex-1 rounded-md border border-precision text-justify overflow-y-auto scrollbar-thin select-text">
<p className="text-main leading-relaxed font-normal bg-sunken p-3 flex-1 rounded-md border border-precision text-justify overflow-y-auto scrollbar-thin select-text">
{paper.abstract_text || '该文献暂无摘要数据。'}
</p>
</div>
@@ -109,22 +109,22 @@ export function PaperDetailModal({
{paper.keywords.map((kw) => (
<span
key={kw}
className="px-2 py-0.5 rounded bg-slate-100 border border-precision text-muted font-bold text-[9px]"
className="px-2 py-0.5 rounded bg-sunken border border-precision text-muted font-bold text-[9px]"
>
{kw}
</span>
))}
</div>
) : (
<div className="flex items-center justify-center flex-1 bg-slate-50 border border-precision rounded-md text-[10px] text-muted font-bold select-none">
<div className="flex items-center justify-center flex-1 bg-sunken border border-precision rounded-md text-[10px] text-muted font-bold select-none">
</div>
)}
</div>
{/* 标识符 */}
<div className="border-t border-slate-155 pt-3 grid grid-cols-1 sm:grid-cols-3 gap-2 text-[10px] font-mono mt-auto shrink-0">
<div className="bg-slate-50 px-2.5 py-1.5 rounded-md border border-precision">
<div className="border-t border-subtle pt-3 grid grid-cols-1 sm:grid-cols-3 gap-2 text-[10px] font-mono mt-auto shrink-0">
<div className="bg-sunken px-2.5 py-1.5 rounded-md border border-precision">
<span className="text-muted font-bold block">BIBCODE</span>
<span
className="text-main font-semibold select-all truncate block"
@@ -149,7 +149,7 @@ export function PaperDetailModal({
)}
</span>
</div>
<div className="bg-slate-50 px-2.5 py-1.5 rounded-md border border-precision">
<div className="bg-sunken px-2.5 py-1.5 rounded-md border border-precision">
<span className="text-muted font-bold block">DOI</span>
<span
className="text-main font-semibold select-all truncate block"
@@ -174,7 +174,7 @@ export function PaperDetailModal({
)}
</span>
</div>
<div className="bg-slate-50 px-2.5 py-1.5 rounded-md border border-precision">
<div className="bg-sunken px-2.5 py-1.5 rounded-md border border-precision">
<span className="text-muted font-bold block">ARXIV ID</span>
<span
className="text-main font-semibold select-all truncate block"
@@ -202,10 +202,10 @@ export function PaperDetailModal({
</div>
{/* 手动上传文件(应对防爬阻断) */}
<div className="border-t border-slate-150 pt-3 space-y-2">
<div className="border-t border-subtle pt-3 space-y-2">
<div className="flex items-center justify-between">
<span className="text-muted font-bold block">线</span>
<span className="text-[9px] text-amber-800 font-bold bg-amber-50 px-2 py-0.5 rounded-md border border-amber-200/60">
<span className="text-[9px] text-amber-800 dark:text-amber-300 font-bold bg-amber-50 dark:bg-amber-500/10 px-2 py-0.5 rounded-md border border-amber-200/60 dark:border-amber-500/30">
/
</span>
</div>
@@ -214,7 +214,7 @@ export function PaperDetailModal({
</p>
<div className="grid grid-cols-2 gap-2">
<div className="flex flex-col items-center justify-center border border-dashed border-precision rounded-md p-2 hover:bg-slate-50 transition-colors relative cursor-pointer group min-h-[50px]">
<div className="flex flex-col items-center justify-center border border-dashed border-precision rounded-md p-2 hover:bg-sunken transition-colors relative cursor-pointer group min-h-[50px]">
<input
type="file"
accept="application/pdf"
@@ -226,7 +226,7 @@ export function PaperDetailModal({
disabled={uploadingBibcode === paper.bibcode}
/>
{paper.has_pdf && (
<span className="absolute top-1 right-1 text-[8px] font-bold text-emerald-800 bg-emerald-50 border border-emerald-200/60 px-1 py-0.2 rounded-md select-none pointer-events-none z-10">
<span className="absolute top-1 right-1 text-[8px] font-bold text-emerald-800 dark:text-emerald-300 bg-emerald-50 dark:bg-emerald-500/10 border border-emerald-200/60 dark:border-emerald-500/30 px-1 py-0.2 rounded-md select-none pointer-events-none z-10">
</span>
)}
@@ -238,7 +238,7 @@ export function PaperDetailModal({
<span className="text-[8px] text-muted"> .pdf </span>
</div>
<div className="flex flex-col items-center justify-center border border-dashed border-precision rounded-md p-2 hover:bg-slate-50 transition-colors relative cursor-pointer group min-h-[50px]">
<div className="flex flex-col items-center justify-center border border-dashed border-precision rounded-md p-2 hover:bg-sunken transition-colors relative cursor-pointer group min-h-[50px]">
<input
type="file"
accept="text/html,.html"
@@ -250,7 +250,7 @@ export function PaperDetailModal({
disabled={uploadingBibcode === paper.bibcode}
/>
{paper.has_html && (
<span className="absolute top-1 right-1 text-[8px] font-bold text-emerald-800 bg-emerald-50 border border-emerald-200/60 px-1 py-0.2 rounded-md select-none pointer-events-none z-10">
<span className="absolute top-1 right-1 text-[8px] font-bold text-emerald-800 dark:text-emerald-300 bg-emerald-50 dark:bg-emerald-500/10 border border-emerald-200/60 dark:border-emerald-500/30 px-1 py-0.2 rounded-md select-none pointer-events-none z-10">
</span>
)}
@@ -292,17 +292,17 @@ export function PaperDetailModal({
{/* 自动下载失败诊断 */}
{!paper.is_downloaded && (paper.pdf_error || paper.html_error) && (
<div
className={`rounded-md p-3 text-[10px] space-y-1 border border-slate-200 border-l-4 ${
className={`rounded-md p-3 text-[10px] space-y-1 border-l-4 ${
paper.pdf_error === 'no_resource' &&
paper.html_error === 'no_resource'
? 'bg-slate-50 border-l-amber-500 text-slate-800'
: 'bg-slate-50 border-l-rose-500 text-slate-800'
? 'bg-sunken border-l-amber-500 text-content'
: 'bg-sunken border-l-rose-500 text-content'
}`}
>
{paper.pdf_error === 'no_resource' &&
paper.html_error === 'no_resource' ? (
<div className="leading-relaxed">
<div className="font-bold flex items-center gap-1.5 text-slate-900 text-[11px] mb-1">
<div className="font-bold flex items-center gap-1.5 text-content text-[11px] mb-1">
<span className="w-1.5 h-1.5 rounded-full bg-amber-500" />
</div>
@@ -310,13 +310,13 @@ export function PaperDetailModal({
</div>
) : (
<>
<div className="font-bold flex items-center gap-1.5 text-slate-900 text-[11px]">
<div className="font-bold flex items-center gap-1.5 text-content text-[11px]">
<span className="w-1.5 h-1.5 rounded-full bg-rose-600" />
</div>
{paper.pdf_error && (
<div className="leading-relaxed">
<span className="font-semibold text-slate-600">
<span className="font-semibold text-secondary">
PDF :{' '}
</span>
{paper.pdf_error}
@@ -324,7 +324,7 @@ export function PaperDetailModal({
)}
{paper.html_error && (
<div className="leading-relaxed mt-0.5">
<span className="font-semibold text-slate-600">
<span className="font-semibold text-secondary">
HTML :{' '}
</span>
{paper.html_error}
@@ -337,7 +337,7 @@ export function PaperDetailModal({
</div>
{/* 底部操作:整合所有动作(阅读、图谱、下载) */}
<div className="flex flex-wrap gap-2 pt-3 border-t border-slate-150">
<div className="flex flex-wrap gap-2 pt-3 border-t border-subtle">
{paper.is_downloaded ? (
<>
<button
@@ -26,7 +26,7 @@ export function UncachedPaperModal({
<div className="space-y-2">
<p className="text-xs text-main leading-relaxed">
{' '}
<span className="font-mono bg-slate-100 px-1.5 py-0.5 rounded-md text-blueprint font-bold select-all">
<span className="font-mono bg-sunken px-1.5 py-0.5 rounded-md text-blueprint font-bold select-all">
{bibcode}
</span>{' '}
+41 -26
View File
@@ -13,6 +13,8 @@ import {
} from 'lucide-react';
import type { StandardPaper } from '../../types';
import { Logo } from '../Logo';
import { ThemeToggle } from './ThemeToggle';
import { useTheme } from '../../hooks/useTheme';
export type TabId =
| 'search'
@@ -44,6 +46,7 @@ export function Sidebar({
}: SidebarProps) {
const [isCollapsed, setIsCollapsed] = useState(false);
const effectiveCollapsed = isCollapsed && !isOpen;
const { theme, toggleTheme } = useTheme();
return (
<>
@@ -58,7 +61,7 @@ export function Sidebar({
)}
<aside
className={`bg-slate-50 border-r border-slate-200 flex flex-col justify-between py-6 z-40 select-none transition-all duration-355 cubic-bezier(0.4, 0, 0.2, 1) fixed inset-y-0 left-0 lg:relative lg:translate-x-0 ${
className={`bg-surface flex flex-col justify-between py-6 z-40 select-none transition-all duration-300 fixed inset-y-0 left-0 lg:relative lg:translate-x-0 sidebar-seam ${
isOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'
} ${
isOpen ? 'w-64 px-4' : effectiveCollapsed ? 'w-16 px-2' : 'w-64 px-4'
@@ -73,22 +76,21 @@ export function Sidebar({
: 'justify-between px-3'
}`}
>
{/* Logo & 标题文字 */}
{/* Logo按钮 (只在折叠状态下可点击展开) */}
<div className="flex items-center gap-3 min-w-0">
{/* Logo按钮 (只在折叠状态下可点击展开) */}
<button
type="button"
disabled={!effectiveCollapsed}
onClick={() => setIsCollapsed(false)}
className={`flex items-center justify-center shrink-0 rounded-lg transition-all duration-300 ${
effectiveCollapsed
? 'w-11 h-11 bg-white hover:bg-slate-55 border border-slate-200 cursor-pointer shadow-xs hover:shadow-sm'
? 'w-11 h-11 bg-elevated hover:bg-sunken cursor-pointer'
: 'w-9 h-9 bg-transparent border border-transparent cursor-default'
}`}
title={effectiveCollapsed ? '展开导航' : undefined}
>
<div
className={`transition-all duration-300 flex items-center justify-center ${effectiveCollapsed ? 'w-8 h-8' : 'w-9 h-9'}`}
className={`transition-all duration-300 flex items-center justify-center text-blueprint ${effectiveCollapsed ? 'w-8 h-8' : 'w-9 h-9'}`}
>
<Logo gradientId="sidebarStarGrad" />
</div>
@@ -102,20 +104,22 @@ export function Sidebar({
: 'opacity-100 max-w-[150px] scale-100 translate-x-0'
}`}
>
<h1 className="text-sm font-bold text-slate-800 tracking-wider whitespace-nowrap">
<h1 className="text-sm font-bold text-content tracking-wider whitespace-nowrap">
AstroResearch
</h1>
<span className="text-[11px] text-slate-500 block font-medium font-sans whitespace-nowrap">
<span className="text-[11px] text-secondary block font-medium font-sans whitespace-nowrap">
</span>
</div>
</div>
{/* 折叠控制按钮 (圆润悬浮效果,仅在桌面端显示) */}
{/* 折叠控制按钮 (仅在桌面端显示) */}
<button
type="button"
onClick={() => setIsCollapsed(true)}
className={`p-1.5 rounded-full bg-slate-100 hover:bg-slate-200 border border-slate-200 text-slate-505 hover:text-slate-800 transition-all duration-300 cursor-pointer shadow-2xs shrink-0 items-center justify-center hover:scale-105 active:scale-95 hidden lg:flex ${
aria-label="收起导航菜单"
aria-expanded={!effectiveCollapsed}
className={`p-1.5 rounded-full bg-sunken hover:bg-border-subtle text-secondary hover:text-content transition-all duration-300 cursor-pointer shrink-0 items-center justify-center hover:scale-105 active:scale-95 hidden lg:flex ${
effectiveCollapsed
? 'opacity-0 scale-75 pointer-events-none w-0 h-0 p-0 border-0 overflow-hidden'
: 'opacity-100 scale-100'
@@ -147,6 +151,8 @@ export function Sidebar({
<button
key={tab.id}
title={effectiveCollapsed ? tab.label : undefined}
aria-label={tab.label}
aria-current={isActive ? 'page' : undefined}
onClick={() => {
setActiveTab(tab.id);
if (tab.id === 'citation' && selectedPaper) {
@@ -156,16 +162,16 @@ export function Sidebar({
}}
className={`w-full flex items-center rounded-lg text-xs font-semibold tracking-wider transition-all duration-300 border ${
effectiveCollapsed
? 'px-2 py-2.5 justify-center'
: 'px-3 py-2.5'
? 'px-2 py-3 justify-center min-h-[44px]'
: 'px-3 py-3 min-h-[44px]'
} ${
isActive
? 'bg-slate-100 border-slate-200 text-slate-850 shadow-xs'
: 'border-transparent text-slate-650 hover:bg-slate-100 hover:text-slate-800'
? 'bg-sunken border-transparent text-content'
: 'border-transparent text-secondary hover:bg-sunken hover:text-content'
}`}
>
<Icon
className={`w-4 h-4 shrink-0 transition-colors duration-300 ${isActive ? 'text-slate-700' : 'text-slate-500'}`}
className={`w-4 h-4 shrink-0 transition-colors duration-300 ${isActive ? 'text-blueprint' : 'text-secondary group-hover:text-content'}`}
/>
<span
className={`truncate transition-all duration-300 origin-left ${
@@ -182,14 +188,20 @@ export function Sidebar({
</nav>
</div>
{/* 底部当前选定文献提示 (平滑动画版本) 与 退出登录 */}
{/* 底部:主题切换 + 当前选定文献 + 退出登录 */}
<div className="space-y-3">
<ThemeToggle
theme={theme}
onToggle={toggleTheme}
collapsed={effectiveCollapsed}
/>
{selectedPaper ? (
<div
className={`relative overflow-hidden transition-all duration-300 border rounded-lg ${
effectiveCollapsed
? 'p-0 border-transparent bg-transparent flex justify-center'
: 'p-3.5 border-slate-200 bg-slate-100/50'
: 'p-3.5 border-transparent bg-sunken'
}`}
title={
effectiveCollapsed
@@ -199,7 +211,7 @@ export function Sidebar({
>
{/* 折叠下的微型图书图标 */}
<div
className={`transition-all duration-300 flex items-center justify-center rounded-lg border border-slate-200 bg-slate-55 text-slate-650 shrink-0 shadow-xs ${
className={`transition-all duration-300 flex items-center justify-center rounded-lg bg-sunken text-secondary shrink-0 ${
effectiveCollapsed
? 'w-9 h-9 opacity-100 scale-100'
: 'w-0 h-0 opacity-0 scale-75 overflow-hidden'
@@ -216,13 +228,13 @@ export function Sidebar({
: 'opacity-100 max-w-[200px] max-h-40'
}`}
>
<span className="text-[9px] font-bold text-slate-500 tracking-widest block mb-1">
<span className="text-[9px] font-bold text-tertiary tracking-widest block mb-1">
</span>
<h4 className="text-xs text-slate-800 font-bold line-clamp-2 mb-2 leading-relaxed">
<h4 className="text-xs text-content font-bold line-clamp-2 mb-2 leading-relaxed">
{selectedPaper.title}
</h4>
<div className="flex items-center justify-between text-[10px] font-medium text-slate-500 gap-2">
<div className="flex items-center justify-between text-[10px] font-medium text-secondary gap-2">
<span className="shrink-0">
: {selectedPaper.year}
</span>
@@ -237,13 +249,13 @@ export function Sidebar({
className={`relative overflow-hidden transition-all duration-300 border rounded-lg ${
effectiveCollapsed
? 'p-0 border-transparent bg-transparent flex justify-center'
: 'p-3 border-slate-200 bg-slate-100/30'
: 'p-3 border-transparent bg-sunken'
}`}
title={effectiveCollapsed ? '未选定研究目标' : undefined}
>
{/* 折叠下的微型馆藏图标 */}
<div
className={`transition-all duration-300 flex items-center justify-center rounded-lg border border-slate-200 bg-slate-100/30 text-slate-400 shrink-0 ${
className={`transition-all duration-300 flex items-center justify-center rounded-lg bg-sunken text-tertiary shrink-0 ${
effectiveCollapsed
? 'w-9 h-9 opacity-100 scale-100'
: 'w-0 h-0 opacity-0 scale-75 overflow-hidden'
@@ -260,7 +272,7 @@ export function Sidebar({
: 'opacity-100 max-w-[200px] max-h-12'
}`}
>
<span className="text-[10px] text-slate-400 font-medium tracking-wide">
<span className="text-[10px] text-tertiary font-medium tracking-wide">
</span>
</div>
@@ -271,12 +283,15 @@ export function Sidebar({
<button
type="button"
onClick={onLogout}
className={`w-full flex items-center rounded-lg text-xs font-semibold tracking-wider transition-all duration-300 border border-transparent text-slate-500 hover:bg-rose-50/50 hover:text-rose-600 cursor-pointer group ${
effectiveCollapsed ? 'px-2 py-2.5 justify-center' : 'px-3 py-2.5'
aria-label="退出登录"
className={`w-full flex items-center rounded-lg text-xs font-semibold tracking-wider transition-all duration-300 border border-transparent text-secondary hover:bg-rose-50/50 dark:hover:bg-rose-500/10 hover:text-danger cursor-pointer group ${
effectiveCollapsed
? 'px-2 py-3 justify-center min-h-[44px]'
: 'px-3 py-3 min-h-[44px]'
}`}
title={effectiveCollapsed ? '退出登录' : undefined}
>
<LogOut className="w-4 h-4 shrink-0 text-slate-400 group-hover:text-rose-505 transition-colors" />
<LogOut className="w-4 h-4 shrink-0 text-tertiary group-hover:text-danger transition-colors" />
<span
className={`truncate transition-all duration-300 origin-left ${
effectiveCollapsed
@@ -0,0 +1,47 @@
// dashboard/src/components/layout/ThemeToggle.tsx
//
// 浅色/暗色主题切换按钮。适配侧边栏折叠态(仅图标)与展开态。
import { Sun, Moon } from 'lucide-react';
import type { Theme } from '../../hooks/useTheme';
interface ThemeToggleProps {
theme: Theme;
onToggle: () => void;
collapsed?: boolean;
}
export function ThemeToggle({
theme,
onToggle,
collapsed = false,
}: ThemeToggleProps) {
const isDark = theme === 'dark';
return (
<button
type="button"
onClick={onToggle}
title={isDark ? '切换到浅色模式' : '切换到暗色模式'}
aria-label={isDark ? '切换到浅色模式' : '切换到暗色模式'}
className={`w-full flex items-center rounded-lg text-xs font-semibold tracking-wider transition-all duration-300 border border-transparent text-secondary hover:bg-sunken hover:text-content cursor-pointer group ${
collapsed
? 'px-2 py-3 justify-center min-h-[44px]'
: 'px-3 py-3 min-h-[44px]'
}`}
>
{isDark ? (
<Sun className="w-4 h-4 shrink-0 text-amber-400 group-hover:text-amber-300 transition-colors" />
) : (
<Moon className="w-4 h-4 shrink-0 text-secondary group-hover:text-blueprint transition-colors" />
)}
<span
className={`truncate transition-all duration-300 origin-left ${
collapsed
? 'opacity-0 max-w-0 pointer-events-none select-none overflow-hidden scale-90 -translate-x-2'
: 'opacity-100 max-w-[150px] scale-100 translate-x-0 ml-3'
}`}
>
{isDark ? '浅色模式' : '深空模式'}
</span>
</button>
);
}
@@ -0,0 +1,71 @@
import { motion, AnimatePresence } from 'framer-motion';
import { CheckCircle, AlertCircle, Info, X } from 'lucide-react';
export interface ToastItem {
id: number;
message: string;
type: 'success' | 'error' | 'info';
}
interface ToastContainerProps {
toasts: ToastItem[];
onClose: (id: number) => void;
}
export function ToastContainer({ toasts, onClose }: ToastContainerProps) {
return (
<div className="fixed top-5 right-5 z-[9999] flex flex-col gap-3 max-w-sm w-full pointer-events-none px-4 sm:px-0">
<AnimatePresence>
{toasts.map((toast) => {
const isSuccess = toast.type === 'success';
const isError = toast.type === 'error';
return (
<motion.div
key={toast.id}
initial={{ opacity: 0, y: -20, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -10, scale: 0.95 }}
transition={{ duration: 0.2 }}
className={`pointer-events-auto flex items-start gap-3 p-3.5 rounded-lg border shadow-lg backdrop-blur-md transition-all ${
isSuccess
? 'bg-emerald-50/90 dark:bg-emerald-950/80 border-emerald-200 dark:border-emerald-500/30 text-emerald-800 dark:text-emerald-200'
: isError
? 'bg-rose-50/90 dark:bg-rose-950/80 border-rose-200 dark:border-rose-500/30 text-rose-800 dark:text-rose-200'
: 'bg-slate-50/90 dark:bg-slate-900/80 border-slate-200 dark:border-slate-800 text-slate-800 dark:text-slate-200'
}`}
>
{/* Type Icon */}
<div className="shrink-0 mt-0.5">
{isSuccess && (
<CheckCircle className="w-4 h-4 text-emerald-600 dark:text-emerald-400" />
)}
{isError && (
<AlertCircle className="w-4 h-4 text-rose-600 dark:text-rose-400" />
)}
{!isSuccess && !isError && (
<Info className="w-4 h-4 text-blueprint" />
)}
</div>
{/* Message */}
<p className="text-[11px] font-bold leading-relaxed flex-1 select-text">
{toast.message}
</p>
{/* Close Button */}
<button
type="button"
onClick={() => onClose(toast.id)}
className="shrink-0 p-0.5 rounded-full hover:bg-black/5 dark:hover:bg-white/5 transition-all text-secondary hover:text-content cursor-pointer"
title="关闭通知"
>
<X className="w-3.5 h-3.5" />
</button>
</motion.div>
);
})}
</AnimatePresence>
</div>
);
}
@@ -1,14 +1,15 @@
// dashboard/src/components/observation/ObservationPreviewRenderer.tsx
//
// 预览渲染分发器 —— 按 ObservationPreview.kind 路由到对应绘图组件
//
// 扩展新产品类型的可视化时:
// 1. 实现 XxxPlot 组件(如 LightCurvePlot.tsx
// 2. 在此 switch 加 case(从联合类型窄化出对应变体)
// 3. 在 constants.ts 的 canPreview() 放开该 product
// 现有 Spectrum 渲染无需改动(OCP:对扩展开放,对修改关闭)。
// 支持点击右上角“放大”图标,通过 React Portal 全屏打开高清晰灯箱模态框。
// 针对光谱图,在大图模态框中会以高精尺寸重新渲染,获得更准确的数据滑块精度;
// 同时解析展示 FITS 头信息(红移、天体分类、有效温度、金属丰度等)。
import { useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { Maximize2, X, Info } from 'lucide-react';
import type { ObservationPreview } from './constants';
import { SOURCE_THEME, PRODUCT_LABEL } from './constants';
import { SpectrumPlot } from './SpectrumPlot';
interface ObservationPreviewRendererProps {
@@ -18,44 +19,197 @@ interface ObservationPreviewRendererProps {
export function ObservationPreviewRenderer({
preview,
}: ObservationPreviewRendererProps) {
switch (preview.kind) {
case 'spectrum':
// 窄化:preview 此处为 { kind: 'spectrum' } & SpectrumPreview
return <SpectrumPlot segments={preview.segments} />;
const [isZoomed, setIsZoomed] = useState(false);
case 'lightcurve':
// TODO: 实现 LightCurvePlot 后替换占位
return (
<div className="text-[10px] text-slate-400 py-4 text-center">
线
</div>
);
// 监听 Escape 按键以快速关闭模态框
useEffect(() => {
if (!isZoomed) return;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
setIsZoomed(false);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isZoomed]);
case 'photometry':
// TODO: 实现 PhotometryTable 后替换占位
return (
<div className="text-[10px] text-slate-400 py-4 text-center">
</div>
);
const sourceTheme = SOURCE_THEME[preview.source] ?? {
badge: 'bg-border-subtle text-secondary',
label: preview.source,
dot: 'bg-secondary',
};
const productLabel = PRODUCT_LABEL[preview.kind] ?? preview.kind;
case 'image':
// image 变体已有 preview_data_url,直接渲染
return (
<img
src={preview.preview_data_url}
alt="cutout"
className="w-full rounded border border-slate-200"
style={{ maxHeight: 200, objectFit: 'contain' }}
/>
);
return (
<>
<div className="relative group w-full overflow-hidden rounded border border-default/40 bg-sunken/30 select-none">
{/* 常规缩略预览 */}
{preview.kind === 'spectrum' && (
<SpectrumPlot segments={preview.segments} height={180} />
)}
default:
// 穷尽性检查:未来新增 kind 会在编译期报错
return (
<div className="text-[10px] text-slate-400 py-4 text-center">
</div>
);
}
{preview.kind === 'lightcurve' && (
<div className="text-[10px] text-tertiary py-4 text-center">
线
</div>
)}
{preview.kind === 'photometry' && (
<div className="text-[10px] text-tertiary py-4 text-center">
</div>
)}
{preview.kind === 'image' && (
<img
src={preview.preview_data_url}
alt="cutout"
className="w-full rounded bg-black/10 object-contain"
style={{ maxHeight: 200 }}
/>
)}
{/* 悬浮放大触控层 */}
{(preview.kind === 'spectrum' || preview.kind === 'image') && (
<button
onClick={() => setIsZoomed(true)}
className="absolute top-1.5 right-1.5 p-1 rounded bg-surface/90 hover:bg-surface border border-subtle hover:border-default text-tertiary hover:text-content opacity-0 group-hover:opacity-45 hover:!opacity-100 transition-all shadow-xs cursor-pointer"
title="放大查看"
>
<Maximize2 className="w-3 h-3" />
</button>
)}
</div>
{/* 全屏放大灯箱 —— 使用 React Portal 挂载到 body 下,彻底解决父组件 transform 裁剪 bug */}
{isZoomed &&
createPortal(
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
{/* 模糊遮罩背景 */}
<div
className="absolute inset-0 bg-slate-950/75 backdrop-blur-md transition-opacity duration-300"
onClick={() => setIsZoomed(false)}
/>
{/* 放大面板卡片 */}
<div className="console-panel rounded-lg w-full max-w-4xl max-h-[90vh] bg-surface flex flex-col z-10 overflow-hidden shadow-2xl animate-in zoom-in-95 duration-200">
{/* 顶部标题栏 */}
<div className="flex items-center justify-between border-b border-subtle px-5 py-4 select-none">
<div className="flex items-center gap-2.5">
<span
className={`px-2 py-0.5 rounded text-[10px] font-bold ${sourceTheme.badge}`}
>
{sourceTheme.label}
</span>
<span className="px-2 py-0.5 rounded text-[10px] font-bold bg-border-subtle text-secondary">
{productLabel}
</span>
<span className="text-xs font-bold text-secondary font-mono">
ID: {preview.source_id}
</span>
</div>
<button
onClick={() => setIsZoomed(false)}
className="p-1 rounded-md hover:bg-sunken text-tertiary hover:text-content transition-colors cursor-pointer"
title="关闭"
>
<X className="w-4 h-4" />
</button>
</div>
{/* 中部高精大图/光谱图表渲染 */}
<div className="p-6 overflow-y-auto flex-1 flex flex-col items-center justify-center bg-sunken/40">
{preview.kind === 'spectrum' && (
<div className="w-full bg-surface rounded-lg border border-default p-4 shadow-sm">
{/* 高度提升至 420px 渲染,大幅扩充图线拉伸区域,交互触感更精准 */}
<SpectrumPlot segments={preview.segments} height={420} />
</div>
)}
{preview.kind === 'image' && (
<div className="relative rounded-lg border border-default overflow-hidden bg-black flex items-center justify-center p-1.5 shadow-sm max-w-full">
<img
src={preview.preview_data_url}
alt="cutout-enlarged"
className="rounded max-w-full max-h-[60vh] object-contain"
/>
</div>
)}
</div>
{/* 底部详细 FITS 头元数据状态栏 */}
{preview.meta && (
<div className="border-t border-subtle px-5 py-3.5 bg-surface text-[11px] text-secondary flex flex-wrap gap-x-6 gap-y-1.5 select-none items-center">
<div className="flex items-center gap-1 text-tertiary">
<Info className="w-3.5 h-3.5" />
<span></span>
</div>
{preview.meta.ra !== undefined &&
preview.meta.dec !== undefined && (
<div className="font-mono">
:{' '}
<span className="text-content font-bold">
{preview.meta.ra.toFixed(5)}
</span>
,{' '}
<span className="text-content font-bold">
{preview.meta.dec.toFixed(5)}
</span>
</div>
)}
{preview.meta.z !== undefined && (
<div className="font-mono">
z:{' '}
<span className="text-content font-bold">
{preview.meta.z.toFixed(4)}
</span>
</div>
)}
{preview.meta.class && (
<div>
:{' '}
<span className="text-content font-bold uppercase">
{preview.meta.class}
</span>
</div>
)}
{preview.meta.snr !== undefined && (
<div className="font-mono">
SNR:{' '}
<span className="text-content font-bold">
{preview.meta.snr.toFixed(1)}
</span>
</div>
)}
{preview.meta.teff !== undefined && (
<div className="font-mono">
Teff:{' '}
<span className="text-content font-bold">
{preview.meta.teff.toFixed(0)} K
</span>
</div>
)}
{preview.meta.logg !== undefined && (
<div className="font-mono">
log g:{' '}
<span className="text-content font-bold">
{preview.meta.logg.toFixed(2)}
</span>
</div>
)}
{preview.meta.fe_h !== undefined && (
<div className="font-mono">
[Fe/H]:{' '}
<span className="text-content font-bold">
{preview.meta.fe_h.toFixed(2)}
</span>
</div>
)}
</div>
)}
</div>
</div>,
document.body
)}
</>
);
}
@@ -50,11 +50,11 @@ export function ObservationResultCard({ result }: ObservationResultCardProps) {
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
return (
<div className="bg-slate-50/60 border border-slate-200 rounded-lg p-3.5 space-y-3 text-xs shadow-2xs">
<div className="bg-sunken rounded-lg p-3.5 space-y-3 text-xs">
{/* 标题栏:数据源 + 产品类型双标签 */}
<div className="flex items-center justify-between border-b border-slate-200/70 pb-2">
<div className="flex items-center gap-1.5 font-bold text-slate-800 text-[11px] uppercase tracking-wide">
<Activity className="w-3.5 h-3.5 text-slate-500" />
<div className="flex items-center justify-between border-b border-subtle pb-2">
<div className="flex items-center gap-1.5 font-bold text-content text-[11px] uppercase tracking-wide">
<Activity className="w-3.5 h-3.5 text-secondary" />
<span></span>
</div>
<div className="flex items-center gap-1">
@@ -63,7 +63,7 @@ export function ObservationResultCard({ result }: ObservationResultCardProps) {
>
{theme.label}
</span>
<span className="px-2 py-0.5 rounded text-[10px] font-bold bg-slate-200 text-slate-700">
<span className="px-2 py-0.5 rounded text-[10px] font-bold bg-border-subtle text-secondary">
{productLabel}
{product.subtype ? ` · ${product.subtype}` : ''}
</span>
@@ -71,24 +71,24 @@ export function ObservationResultCard({ result }: ObservationResultCardProps) {
</div>
{/* 查询信息 */}
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-[10px] text-slate-600">
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-[10px] text-secondary">
{ra !== undefined && dec !== undefined && (
<span className="font-mono">
ra={ra.toFixed(4)} dec={dec.toFixed(4)}
{radius_deg !== undefined ? ` r=${radius_deg}°` : ''}
</span>
)}
<span className="bg-slate-200 text-slate-700 px-1.5 py-0.5 rounded font-medium">
<span className="bg-border-subtle text-secondary px-1.5 py-0.5 rounded font-medium">
{matched_count}
</span>
{products.length > 0 && (
<span className="flex items-center gap-0.5 text-emerald-600 font-medium">
<span className="flex items-center gap-0.5 text-success font-medium">
<CheckCircle2 className="w-3 h-3" />
{products.length}
</span>
)}
{failures.length > 0 && (
<span className="flex items-center gap-0.5 text-rose-600 font-medium">
<span className="flex items-center gap-0.5 text-danger font-medium">
<AlertTriangle className="w-3 h-3" />
{failures.length}
</span>
@@ -100,21 +100,18 @@ export function ObservationResultCard({ result }: ObservationResultCardProps) {
const allCached =
p.artifacts.length > 0 && p.artifacts.every((a) => a.cached);
return (
<div
key={i}
className="border border-slate-200 rounded bg-white p-2.5 space-y-1.5"
>
<div key={i} className=" rounded bg-surface p-2.5 space-y-1.5">
<div className="flex items-center justify-between">
<span className="font-mono font-medium text-slate-700">
<span className="font-mono font-medium text-secondary">
{p.source_label}
</span>
{allCached ? (
<span className="flex items-center gap-0.5 text-[10px] text-emerald-600">
<span className="flex items-center gap-0.5 text-[10px] text-success">
<CheckCircle2 className="w-3 h-3" />
</span>
) : (
<span className="text-[10px] text-slate-400"></span>
<span className="text-[10px] text-tertiary"></span>
)}
</div>
{/* 多 artifact 展示(Gaia 光变 G/BP/RP 三波段) */}
@@ -138,10 +135,10 @@ export function ObservationResultCard({ result }: ObservationResultCardProps) {
};
return (
<div key={ai} className="space-y-1">
<div className="flex items-center justify-between text-[10px] pl-2 border-l-2 border-slate-100">
<span className="text-slate-500 flex items-center gap-1">
<div className="flex items-center justify-between text-[10px] pl-2 border-l-2 border-subtle">
<span className="text-secondary flex items-center gap-1">
{a.band && (
<span className="px-1 py-0.5 rounded bg-violet-100 text-violet-700 font-medium">
<span className="px-1 py-0.5 rounded bg-violet-100 text-violet-700 dark:bg-violet-500/15 dark:text-violet-300 font-medium">
{a.band}
</span>
)}
@@ -152,7 +149,11 @@ export function ObservationResultCard({ result }: ObservationResultCardProps) {
{previewable && (
<button
onClick={togglePreview}
className="flex items-center gap-0.5 font-medium text-slate-500 hover:text-slate-800 transition-colors"
className={`flex items-center gap-1 px-1.5 py-0.5 rounded transition-all font-medium text-[10px] ${
isOpen
? 'text-blueprint bg-blueprint/10 font-bold'
: 'text-secondary hover:text-content hover:bg-sunken'
}`}
title="预览"
>
{previewState.loading ? (
@@ -167,7 +168,7 @@ export function ObservationResultCard({ result }: ObservationResultCardProps) {
href={a.file_url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-0.5 font-bold text-sky-600 hover:text-sky-800 hover:underline"
className="flex items-center gap-1 px-1.5 py-0.5 rounded text-secondary hover:text-blueprint hover:bg-blueprint/10 transition-all font-medium text-[10px]"
>
<Download className="w-3 h-3" />
<span>{a.file_format.toUpperCase()}</span>
@@ -178,7 +179,7 @@ export function ObservationResultCard({ result }: ObservationResultCardProps) {
{previewable && isOpen && (
<div className="pl-2">
{previewState.error ? (
<p className="text-[10px] text-rose-500 py-1">
<p className="text-[10px] text-danger py-1">
{previewState.error}
</p>
) : previewState.data ? (
@@ -186,7 +187,7 @@ export function ObservationResultCard({ result }: ObservationResultCardProps) {
preview={previewState.data}
/>
) : previewState.loading ? (
<div className="flex items-center gap-1 text-[10px] text-slate-400 py-4 justify-center">
<div className="flex items-center gap-1 text-[10px] text-tertiary py-4 justify-center">
<Loader2 className="w-3 h-3 animate-spin" />
FITS ...
</div>
@@ -204,18 +205,20 @@ export function ObservationResultCard({ result }: ObservationResultCardProps) {
{failures.map((f, i) => (
<div
key={`f${i}`}
className="border border-rose-200 rounded bg-rose-50/50 p-2.5"
className="border border-rose-200 dark:border-rose-500/30 rounded bg-rose-50/50 dark:bg-rose-500/10 p-2.5"
>
<div className="flex items-center gap-1 text-rose-700 font-medium">
<div className="flex items-center gap-1 text-rose-700 dark:text-rose-300 font-medium">
<AlertTriangle className="w-3 h-3" />
<span className="font-mono text-[10px]">{f.source_label}</span>
</div>
<p className="text-[10px] text-rose-600 mt-1 break-all">{f.error}</p>
<p className="text-[10px] text-rose-600 dark:text-rose-400 mt-1 break-all">
{f.error}
</p>
</div>
))}
{!hasResult && (
<p className="text-[11px] text-slate-400 italic py-2">
<p className="text-[11px] text-tertiary italic py-2">
({productLabel})
</p>
)}
@@ -10,6 +10,7 @@
// - 简洁坐标轴 + 网格线
import { useState, useMemo } from 'react';
import { useTheme } from '../../hooks/useTheme';
import type { SpectrumSegment } from './constants';
interface SpectrumPlotProps {
@@ -17,7 +18,7 @@ interface SpectrumPlotProps {
height?: number;
}
// 数据源 → 段配色(与 SOURCE_THEME 协调的线条色
// 数据源 → 段配色(频段语义色,跨主题保持一致以便辨识
const SEGMENT_COLORS: Record<string, string> = {
combined: '#64748b', // slate-500
xp_merged: '#0ea5e9', // sky-500
@@ -31,6 +32,23 @@ const SEGMENT_COLORS: Record<string, string> = {
const DEFAULT_COLOR = '#64748b';
// 读取 CSS 令牌当前值(随主题切换)
function usePlotColors() {
const { theme } = useTheme();
return useMemo(() => {
// theme 仅作为失效依赖:切换浅色/暗色后强制重读 CSS 变量
void theme;
const cs = getComputedStyle(document.documentElement);
return {
grid: cs.getPropertyValue('--plot-grid').trim() || '#e2e8f0',
axis: cs.getPropertyValue('--plot-axis').trim() || '#64748b',
axisStrong: cs.getPropertyValue('--plot-axis-strong').trim() || '#94a3b8',
tooltipBg: cs.getPropertyValue('--plot-tooltip-bg').trim() || '#1e293b',
tooltipFg: cs.getPropertyValue('--plot-tooltip-fg').trim() || '#ffffff',
};
}, [theme]);
}
interface PlotPoint {
wl: number;
flux: number;
@@ -39,6 +57,8 @@ interface PlotPoint {
export function SpectrumPlot({ segments, height = 180 }: SpectrumPlotProps) {
const [hover, setHover] = useState<PlotPoint | null>(null);
const plotColors = usePlotColors();
const hoveredBand = hover ? (segments[hover.segIdx]?.band ?? '') : '';
// 合并所有段的数据点,计算全局范围
const { points, wlMin, wlMax, fluxMin, fluxMax } = useMemo(() => {
@@ -52,7 +72,8 @@ export function SpectrumPlot({ segments, height = 180 }: SpectrumPlotProps) {
for (let i = 0; i < n; i++) {
const wl = seg.wavelength[i];
const fl = seg.flux[i];
if (!isFinite(wl) || !isFinite(fl)) continue;
if (wl === null || fl === null || !isFinite(wl) || !isFinite(fl))
continue;
pts.push({ wl, flux: fl, segIdx: si });
if (wl < wMin) wMin = wl;
if (wl > wMax) wMax = wl;
@@ -72,7 +93,7 @@ export function SpectrumPlot({ segments, height = 180 }: SpectrumPlotProps) {
}, [segments]);
if (points.length === 0) {
return <div className="text-xs text-slate-400 p-2"></div>;
return <div className="text-xs text-tertiary p-2"></div>;
}
const width = 600;
@@ -92,11 +113,14 @@ export function SpectrumPlot({ segments, height = 180 }: SpectrumPlotProps) {
const paths = segments.map((seg) => {
const n = Math.min(seg.wavelength.length, seg.flux.length);
let d = '';
let isFirst = true;
for (let i = 0; i < n; i++) {
const wl = seg.wavelength[i];
const fl = seg.flux[i];
if (!isFinite(wl) || !isFinite(fl)) continue;
d += `${i === 0 ? 'M' : 'L'}${xScale(wl).toFixed(1)},${yScale(fl).toFixed(1)} `;
if (wl === null || fl === null || !isFinite(wl) || !isFinite(fl))
continue;
d += `${isFirst ? 'M' : 'L'}${xScale(wl).toFixed(1)},${yScale(fl).toFixed(1)} `;
isFirst = false;
}
return {
d,
@@ -121,14 +145,15 @@ export function SpectrumPlot({ segments, height = 180 }: SpectrumPlotProps) {
if (v >= 1000) return `${(v / 1000).toFixed(2)}k`;
return v.toFixed(0);
};
const fmtFlux = (v: number) => {
const fmtFlux = (v: number | null | undefined) => {
if (v === null || v === undefined || isNaN(v)) return '—';
const abs = Math.abs(v);
if (abs !== 0 && (abs < 0.01 || abs >= 1e4)) return v.toExponential(1);
return v.toFixed(2);
};
return (
<div className="relative w-full bg-slate-50/80 border border-slate-200 rounded p-1">
<div className="relative w-full bg-sunken rounded p-1">
<svg
viewBox={`0 0 ${width} ${height}`}
className="w-full"
@@ -144,7 +169,7 @@ export function SpectrumPlot({ segments, height = 180 }: SpectrumPlotProps) {
x2={width - padR}
y1={yScale(v)}
y2={yScale(v)}
stroke="#e2e8f0"
stroke={plotColors.grid}
strokeWidth="0.5"
/>
))}
@@ -155,7 +180,7 @@ export function SpectrumPlot({ segments, height = 180 }: SpectrumPlotProps) {
x2={xScale(v)}
y1={padT}
y2={height - padB}
stroke="#e2e8f0"
stroke={plotColors.grid}
strokeWidth="0.5"
/>
))}
@@ -180,7 +205,7 @@ export function SpectrumPlot({ segments, height = 180 }: SpectrumPlotProps) {
y={height - padB + 12}
textAnchor="middle"
fontSize="9"
fill="#64748b"
fill={plotColors.axis}
>
{fmtWl(v)}
</text>
@@ -193,7 +218,7 @@ export function SpectrumPlot({ segments, height = 180 }: SpectrumPlotProps) {
y={yScale(v) + 3}
textAnchor="end"
fontSize="9"
fill="#64748b"
fill={plotColors.axis}
>
{fmtFlux(v)}
</text>
@@ -205,7 +230,7 @@ export function SpectrumPlot({ segments, height = 180 }: SpectrumPlotProps) {
y={height - 4}
textAnchor="end"
fontSize="8"
fill="#94a3b8"
fill={plotColors.axisStrong}
>
λ ({segments[0]?.wavelength_unit ?? 'Å'})
</text>
@@ -214,7 +239,7 @@ export function SpectrumPlot({ segments, height = 180 }: SpectrumPlotProps) {
y={padT + 4}
textAnchor="start"
fontSize="8"
fill="#94a3b8"
fill={plotColors.axisStrong}
>
flux ({segments[0]?.flux_unit ?? ''})
</text>
@@ -227,7 +252,7 @@ export function SpectrumPlot({ segments, height = 180 }: SpectrumPlotProps) {
x2={xScale(hover.wl)}
y1={padT}
y2={height - padB}
stroke="#94a3b8"
stroke={plotColors.axisStrong}
strokeWidth="0.5"
strokeDasharray="2,2"
/>
@@ -273,13 +298,20 @@ export function SpectrumPlot({ segments, height = 180 }: SpectrumPlotProps) {
{/* hover tooltip */}
{hover && (
<div
className="absolute pointer-events-none bg-slate-800 text-white text-[10px] px-1.5 py-0.5 rounded shadow-lg"
className="absolute pointer-events-none text-[9px] sm:text-[10px] font-bold px-2 py-1 rounded-md shadow-md border border-white/10 dark:border-black/35 backdrop-blur-[2px]"
style={{
left: `${((xScale(hover.wl) - padL) / plotW) * 100}%`,
top: `${((yScale(hover.flux) - padT) / plotH) * 100}%`,
transform: 'translate(-50%, -120%)',
left: `${(xScale(hover.wl) / width) * 100}%`,
top: `${(yScale(hover.flux) / height) * 100}%`,
transform: 'translate(-50%, -125%)',
backgroundColor: plotColors.tooltipBg,
color: plotColors.tooltipFg,
}}
>
{hoveredBand && (
<span className="opacity-75 mr-1 font-extrabold select-none">
[{hoveredBand}]
</span>
)}
λ={hover.wl.toFixed(1)} · f={fmtFlux(hover.flux)}
</div>
)}
@@ -290,7 +322,7 @@ export function SpectrumPlot({ segments, height = 180 }: SpectrumPlotProps) {
{segments.map((seg, i) => (
<span
key={`leg${i}`}
className="flex items-center gap-1 text-[9px] text-slate-600"
className="flex items-center gap-1 text-[9px] text-secondary"
>
<span
className="inline-block w-2 h-0.5"
File diff suppressed because it is too large Load Diff
@@ -7,52 +7,59 @@ import type { Candidate } from '../../types';
// ── 数据源主题色(source 轴)──
// 与 SOURCE_THEME 在 SpecialToolRenderers.tsx 的取色保持一致,统一视觉
// 浅色用 *-100/*-700 徽章对;暗色用 */15 + *-300,保证暗底下仍有辨识度但不过亮
export const SOURCE_THEME: Record<
string,
{ badge: string; label: string; dot: string }
> = {
lamost: {
badge: 'bg-amber-100 text-amber-700',
badge:
'bg-amber-100 text-amber-700 dark:bg-amber-500/15 dark:text-amber-300',
label: 'LAMOST',
dot: 'bg-amber-500',
},
gaia: {
badge: 'bg-sky-100 text-sky-700',
badge: 'bg-sky-100 text-sky-700 dark:bg-sky-500/15 dark:text-sky-300',
label: 'Gaia',
dot: 'bg-sky-500',
},
sdss: {
badge: 'bg-indigo-100 text-indigo-700',
badge:
'bg-indigo-100 text-indigo-700 dark:bg-indigo-500/15 dark:text-indigo-300',
label: 'SDSS',
dot: 'bg-indigo-500',
},
desi: {
badge: 'bg-emerald-100 text-emerald-700',
badge:
'bg-emerald-100 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300',
label: 'DESI',
dot: 'bg-emerald-500',
},
twomass: {
badge: 'bg-orange-100 text-orange-700',
badge:
'bg-orange-100 text-orange-700 dark:bg-orange-500/15 dark:text-orange-300',
label: '2MASS',
dot: 'bg-orange-500',
},
allwise: {
badge: 'bg-rose-100 text-rose-700',
badge: 'bg-rose-100 text-rose-700 dark:bg-rose-500/15 dark:text-rose-300',
label: 'AllWISE',
dot: 'bg-rose-500',
},
panstarrs: {
badge: 'bg-cyan-100 text-cyan-700',
badge: 'bg-cyan-100 text-cyan-700 dark:bg-cyan-500/15 dark:text-cyan-300',
label: 'Pan-STARRS',
dot: 'bg-cyan-500',
},
ztf: {
badge: 'bg-fuchsia-100 text-fuchsia-700',
badge:
'bg-fuchsia-100 text-fuchsia-700 dark:bg-fuchsia-500/15 dark:text-fuchsia-300',
label: 'ZTF',
dot: 'bg-fuchsia-500',
},
tess: {
badge: 'bg-violet-100 text-violet-700',
badge:
'bg-violet-100 text-violet-700 dark:bg-violet-500/15 dark:text-violet-300',
label: 'TESS',
dot: 'bg-violet-500',
},
@@ -217,20 +224,26 @@ export interface TargetRequest {
label?: string;
}
export interface UnifiedSourceSpec {
source: string;
product: ProductSpec;
release?: string;
version?: string;
}
/// 统一检索请求体
export interface UnifiedSearchRequest {
targets: TargetRequest[];
/// None/空 = 全部已注册源;非空 = 仅这些 (source, product)
sources?: [string, ProductSpec][];
release?: string;
version?: string;
per_target_limit?: number; // 默认 50
sources?: UnifiedSourceSpec[];
per_target_limit?: number; // 默认 1
}
/// 一个 (source, product) 的聚合候选源组
/// 一个 (source, product, release, version) 的聚合候选源组
export interface SourceCandidateGroup {
source: string;
product: ProductSpec;
release?: string;
version?: string;
candidates: Candidate[];
}
@@ -47,6 +47,7 @@ interface BilingualViewerProps {
highlightCandidates: HighlightCandidate[];
hoveredTarget: TargetInfo | null;
hoverCardPos: { x: number; y: number; isAbove?: boolean } | null;
selectedParagraphIdx?: number | null;
children?: React.ReactNode;
}
@@ -67,35 +68,56 @@ function PaperMetadataDossier({
return (
<div
id={id}
className="mb-3 sm:mb-6 rounded-lg p-3 sm:p-5 bg-slate-50 border border-slate-200 relative overflow-hidden select-text"
className="mb-3 sm:mb-6 rounded-lg p-3 sm:p-5 bg-sunken relative overflow-hidden select-text"
>
{/* 科技风背景线段装饰 - 极细学术边框线 */}
<div className="absolute top-0 right-0 w-16 h-16 bg-slate-100/50 pointer-events-none border-l border-b border-slate-200/60" />
{/* 科技感文献折角装饰 (Realistic folded page corner) */}
<svg
className="absolute top-0 right-0 w-8 h-8 pointer-events-none z-10 select-none"
viewBox="0 0 32 32"
fill="none"
>
{/* 用背景色遮罩掉原卡片的直角部分 */}
<path d="M 0 0 L 32 0 L 32 32 Z" fill="var(--bg-app)" />
{/* 折角阴影 */}
<path
d="M 0 0 L 0 32 L 32 32 Z"
fill="rgba(0, 0, 0, 0.06)"
className="dark:fill-[rgba(0,0,0,0.3)]"
/>
{/* 折角页本身 */}
<path
d="M 0 0 L 0 32 L 32 32 Z"
fill="var(--bg-surface)"
stroke="var(--border-strong)"
strokeWidth="0.8"
strokeLinejoin="round"
/>
</svg>
{/* 顶部档案标徽 */}
<div className="flex items-center gap-2 mb-2 sm:mb-3 select-none">
<span className="inline-flex items-center px-2 py-0.5 rounded text-[9px] font-bold bg-slate-700 text-white uppercase tracking-wider">
<span className="inline-flex items-center px-2 py-0.5 rounded text-[9px] font-bold bg-elevated text-content uppercase tracking-wider">
{isChinese ? '文献元数据档案' : 'Paper Metadata Dossier'}
</span>
{metadata.date && (
<span className="text-[10px] font-bold text-slate-400 font-mono">
<span className="text-[10px] font-bold text-tertiary font-mono">
Year: {metadata.date}
</span>
)}
</div>
{/* 标题 */}
<h1 className="text-sm font-bold text-slate-900 leading-snug mb-1.5 sm:mb-3 select-text">
<h1 className="text-sm font-bold text-content leading-snug mb-1.5 sm:mb-3 select-text">
{metadata.title}
</h1>
{/* 作者群 */}
{metadata.author && metadata.author.length > 0 && (
<div className="text-xs text-slate-600 mb-2 sm:mb-4 font-medium leading-relaxed select-text">
<span className="text-slate-400 font-semibold mr-1">
<div className="text-xs text-secondary mb-2 sm:mb-4 font-medium leading-relaxed select-text">
<span className="text-tertiary font-semibold mr-1">
{isChinese ? '作者群体:' : 'Authors:'}
</span>
<span className="italic text-slate-750">
<span className="italic text-secondary">
{metadata.author.join(', ')}
</span>
</div>
@@ -103,11 +125,11 @@ function PaperMetadataDossier({
{/* 底部期刊与跳转 ADS 按钮 */}
<div className="flex flex-wrap items-center justify-between gap-2 sm:gap-3 pt-2 sm:pt-3 border-t border-slate-200/60 select-none">
<div className="text-[10px] text-slate-500 font-semibold">
<span className="text-slate-400 mr-1">
<div className="text-[10px] text-secondary font-semibold">
<span className="text-tertiary mr-1">
{isChinese ? '发表期刊/预印本:' : 'Publisher:'}
</span>
<span className="text-slate-700">
<span className="text-secondary">
{metadata.publisher || 'arXiv Preprint'}
</span>
</div>
@@ -117,7 +139,7 @@ function PaperMetadataDossier({
href={metadata.source}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-[10px] font-extrabold px-3 py-1 rounded bg-sky-50 text-sky-700 border border-sky-200 hover:bg-sky-100 hover:text-sky-850 transition-all cursor-pointer shadow-xs active:scale-95"
className="inline-flex items-center gap-1 text-[10px] font-extrabold px-3 py-1 rounded bg-sky-50 dark:bg-sky-500/10 text-sky-700 dark:text-sky-300 border border-sky-200 dark:border-sky-500/30 hover:bg-sky-100 hover:text-content transition-all cursor-pointer active:scale-95"
title="在 NASA ADS 官方文献数据库中查看源页面"
>
<span>ADS NASA Portal</span>
@@ -153,6 +175,7 @@ export function BilingualViewer({
highlightCandidates,
hoveredTarget,
hoverCardPos,
selectedParagraphIdx = null,
children,
}: BilingualViewerProps) {
// 监测屏幕宽度以判断是否在平板设备上(宽度 < 1024px
@@ -290,18 +313,18 @@ export function BilingualViewer({
<div
onMouseOver={handleMouseOver}
onMouseLeave={handleContainerMouseLeave}
className="console-panel rounded-lg px-3 pt-3 pb-2 sm:p-6 bg-white border border-slate-200 relative flex flex-col min-h-0 overflow-hidden"
className="console-panel rounded-lg px-3 pt-3 pb-2 sm:p-6 bg-surface relative flex flex-col min-h-0 overflow-hidden"
>
<div className="flex items-center justify-between mb-2 sm:mb-4 border-b border-slate-100 pb-1.5 sm:pb-2.5 shrink-0 select-none">
<div className="flex items-center justify-between mb-2 sm:mb-4 border-b border-subtle pb-1.5 sm:pb-2.5 shrink-0 select-none">
<div className="flex items-center gap-2">
<span className="text-xs font-bold text-slate-800">
<span className="text-xs font-bold text-content">
</span>
{selectedPaper.has_pdf && (
<button
type="button"
onClick={() => setShowPdf(!showPdf)}
className="text-[9px] font-bold px-2 py-0.5 rounded bg-slate-50 text-blueprint border border-slate-200 hover:bg-slate-100 transition-all cursor-pointer flex items-center gap-1"
className="text-[9px] font-bold px-2 py-0.5 rounded bg-sunken text-blueprint hover:bg-sunken transition-all cursor-pointer flex items-center gap-1"
>
{showPdf ? (
<>
@@ -318,21 +341,21 @@ export function BilingualViewer({
</div>
{showPdf ? (
<div className="flex-1 min-h-0 w-full rounded-lg overflow-hidden border border-slate-200 bg-slate-50">
<div className="flex-1 min-h-0 w-full rounded-lg overflow-hidden bg-sunken">
{pdfLoading ? (
<div className="flex flex-col items-center justify-center h-full text-slate-500 space-y-3">
<Loader className="w-8 h-8 animate-spin text-sky-600" />
<div className="flex flex-col items-center justify-center h-full text-secondary space-y-3">
<Loader className="w-8 h-8 animate-spin text-blueprint" />
<p className="text-xs font-bold"> PDF ...</p>
</div>
) : pdfError ? (
<div className="flex flex-col items-center justify-center h-full text-slate-500 space-y-3">
<p className="text-xs font-bold text-rose-600">
<div className="flex flex-col items-center justify-center h-full text-secondary space-y-3">
<p className="text-xs font-bold text-danger">
PDF : {pdfError}
</p>
<button
type="button"
onClick={() => loadPdfBlob(selectedPaper.bibcode)}
className="text-[10px] font-bold px-3 py-1 rounded bg-sky-50 text-sky-700 border border-sky-200 hover:bg-sky-100 cursor-pointer"
className="text-[10px] font-bold px-3 py-1 rounded bg-sky-50 dark:bg-sky-500/10 text-sky-700 dark:text-sky-300 border border-sky-200 dark:border-sky-500/30 hover:bg-sky-100 cursor-pointer"
>
</button>
@@ -346,8 +369,8 @@ export function BilingualViewer({
) : null}
</div>
) : parsing ? (
<div className="flex-1 flex flex-col items-center justify-center text-slate-500 space-y-3">
<Loader className="w-8 h-8 animate-spin text-sky-600" />
<div className="flex-1 flex flex-col items-center justify-center text-secondary space-y-3">
<Loader className="w-8 h-8 animate-spin text-blueprint" />
<p className="text-xs font-bold text-center">
Markdown ...
</p>
@@ -358,7 +381,7 @@ export function BilingualViewer({
onScroll={handleEnglishScroll}
className="flex-1 overflow-y-auto pr-1 scrollbar-thin"
>
<div className="prose prose-sm max-w-none leading-relaxed text-slate-800 prose-headings:text-slate-900 prose-headings:font-bold prose-strong:text-slate-900 prose-code:text-sky-700 prose-blockquote:border-slate-300 prose-blockquote:text-slate-500 prose-img:max-w-full prose-img:rounded-lg prose-p:my-1.5 sm:prose-p:my-3 prose-headings:my-2 sm:prose-headings:my-4">
<div className="prose prose-sm max-w-none leading-relaxed text-content prose-headings:text-content prose-headings:font-bold prose-strong:text-content prose-code:text-blueprint prose-blockquote:border-subtle prose-blockquote:text-secondary prose-img:max-w-full prose-img:rounded-lg prose-p:my-1.5 sm:prose-p:my-3 prose-headings:my-2 sm:prose-headings:my-4">
{/* 渲染英文元数据档案面板 */}
{engMeta && (
<PaperMetadataDossier
@@ -372,19 +395,37 @@ export function BilingualViewer({
const matchedNote = notes.find(
(n) => n.paragraph_index === idx
);
const isFocused = selectedParagraphIdx === idx;
const highlightStyle = matchedNote
? NOTE_COLORS[matchedNote.highlight_color]
: null;
let leftBorderClass = 'border-l-transparent';
if (isFocused) {
leftBorderClass = 'border-l-blueprint';
} else if (matchedNote) {
if (matchedNote.highlight_color === 'green')
leftBorderClass = 'border-l-emerald-500';
else if (matchedNote.highlight_color === 'cyan')
leftBorderClass = 'border-l-cyan-500';
else if (matchedNote.highlight_color === 'amber')
leftBorderClass = 'border-l-amber-500';
else if (matchedNote.highlight_color === 'purple')
leftBorderClass = 'border-l-purple-500';
}
const cardStyles = isFocused
? 'bg-blueprint/5 dark:bg-blueprint/15 border border-blueprint/20 rounded-r-md shadow-xs'
: highlightStyle
? `${highlightStyle.bg} border border-slate-200 dark:border-slate-800/40 rounded-r-md`
: 'hover:bg-sunken border border-transparent';
return (
<div
key={idx}
id={`paragraph-block-${idx}`}
onMouseUp={() => handleTextSelection(idx)}
className={`cursor-text relative rounded px-1.5 -mx-1.5 py-1 transition-colors duration-200 ${
highlightStyle
? `${highlightStyle.bg} ${highlightStyle.border} border`
: 'hover:bg-slate-100'
}`}
className={`cursor-text relative px-3.5 -mx-1.5 py-2 transition-all duration-300 border-l-4 ${leftBorderClass} ${cardStyles}`}
>
<ReactMarkdown
remarkPlugins={[remarkMath, remarkGfm]}
@@ -401,7 +442,7 @@ export function BilingualViewer({
);
return (
<span
className="celestial-target cursor-pointer font-bold text-sky-600 underline decoration-dotted decoration-2 hover:text-sky-850"
className="celestial-target cursor-pointer font-bold text-blueprint underline decoration-dotted decoration-2 hover:text-content"
data-target-name={targetName}
>
{children}
@@ -411,7 +452,7 @@ export function BilingualViewer({
return (
<a
href={href}
className="text-sky-600 hover:underline"
className="text-blueprint hover:underline"
{...props}
>
{children}
@@ -428,12 +469,12 @@ export function BilingualViewer({
</div>
</div>
) : (
<div className="flex-1 flex flex-col items-center justify-center text-slate-400 select-none">
<FileText className="w-12 h-12 mb-3 text-slate-300" />
<p className="text-xs font-bold text-slate-500">
<div className="flex-1 flex flex-col items-center justify-center text-tertiary select-none">
<FileText className="w-12 h-12 mb-3 text-tertiary" />
<p className="text-xs font-bold text-secondary">
</p>
<p className="text-xs text-slate-400 mt-1">
<p className="text-xs text-tertiary mt-1">
</p>
</div>
@@ -443,15 +484,15 @@ export function BilingualViewer({
{/* 中文翻译视窗 (或双语对照) */}
{(viewMode === 'chinese' || viewMode === 'bilingual') && (
<div className="console-panel rounded-lg px-3 pt-2 pb-3 sm:p-6 bg-white border border-slate-200 relative flex flex-col overflow-hidden min-h-0">
<div className="hidden sm:flex items-center justify-between mb-2 sm:mb-4 border-b border-slate-100 pb-1.5 sm:pb-2.5 shrink-0 select-none">
<span className="text-xs font-bold text-slate-800">
<div className="console-panel rounded-lg px-3 pt-2 pb-3 sm:p-6 bg-surface relative flex flex-col overflow-hidden min-h-0">
<div className="hidden sm:flex items-center justify-between mb-2 sm:mb-4 border-b border-subtle pb-1.5 sm:pb-2.5 shrink-0 select-none">
<span className="text-xs font-bold text-content">
</span>
</div>
{translating ? (
<div className="flex-1 flex flex-col items-center justify-center text-slate-500 space-y-3">
<div className="flex-1 flex flex-col items-center justify-center text-secondary space-y-3">
<Loader className="w-8 h-8 animate-spin text-blueprint" />
<p className="text-xs font-bold text-center">
...
@@ -463,7 +504,7 @@ export function BilingualViewer({
onScroll={handleChineseScroll}
className="flex-1 overflow-y-auto pr-1 scrollbar-thin"
>
<div className="prose prose-sm max-w-none leading-relaxed text-slate-800 prose-headings:text-slate-900 prose-strong:text-slate-900 prose-code:text-sky-700 prose-blockquote:border-slate-350 prose-img:max-w-full prose-img:rounded-lg prose-p:my-1.5 sm:prose-p:my-3 prose-headings:my-2 sm:prose-headings:my-4">
<div className="prose prose-sm max-w-none leading-relaxed text-content prose-headings:text-content prose-strong:text-content prose-code:text-blueprint prose-blockquote:border-subtle prose-img:max-w-full prose-img:rounded-lg prose-p:my-1.5 sm:prose-p:my-3 prose-headings:my-2 sm:prose-headings:my-4">
{/* 渲染中文元数据档案面板 */}
{mergedChnMeta && (
<PaperMetadataDossier
@@ -473,33 +514,63 @@ export function BilingualViewer({
/>
)}
{splitParagraphs(chnPure).map((para: string, idx: number) => (
<div
key={idx}
id={`paragraph-block-${idx}`}
className="relative rounded px-1.5 -mx-1.5 py-1 transition-colors duration-200 hover:bg-slate-100"
>
<ReactMarkdown
remarkPlugins={[remarkMath, remarkGfm]}
rehypePlugins={[
rehypeRaw,
[rehypeSanitize, safeSchema],
rehypeKatex,
]}
{splitParagraphs(chnPure).map((para: string, idx: number) => {
const matchedNote = notes.find(
(n) => n.paragraph_index === idx
);
const isFocused = selectedParagraphIdx === idx;
const highlightStyle = matchedNote
? NOTE_COLORS[matchedNote.highlight_color]
: null;
let leftBorderClass = 'border-l-transparent';
if (isFocused) {
leftBorderClass = 'border-l-blueprint';
} else if (matchedNote) {
if (matchedNote.highlight_color === 'green')
leftBorderClass = 'border-l-emerald-500';
else if (matchedNote.highlight_color === 'cyan')
leftBorderClass = 'border-l-cyan-500';
else if (matchedNote.highlight_color === 'amber')
leftBorderClass = 'border-l-amber-500';
else if (matchedNote.highlight_color === 'purple')
leftBorderClass = 'border-l-purple-500';
}
const cardStyles = isFocused
? 'bg-blueprint/5 dark:bg-blueprint/15 border border-blueprint/20 rounded-r-md shadow-xs'
: highlightStyle
? `${highlightStyle.bg} border border-slate-200 dark:border-slate-800/40 rounded-r-md`
: 'hover:bg-sunken border border-transparent';
return (
<div
key={idx}
id={`paragraph-block-${idx}`}
className={`relative px-3.5 -mx-1.5 py-2 transition-all duration-300 border-l-4 ${leftBorderClass} ${cardStyles}`}
>
{para}
</ReactMarkdown>
</div>
))}
<ReactMarkdown
remarkPlugins={[remarkMath, remarkGfm]}
rehypePlugins={[
rehypeRaw,
[rehypeSanitize, safeSchema],
rehypeKatex,
]}
>
{para}
</ReactMarkdown>
</div>
);
})}
</div>
</div>
) : (
<div className="flex-1 flex flex-col items-center justify-center text-slate-400 select-none">
<Languages className="w-12 h-12 mb-3 text-slate-300" />
<p className="text-xs font-bold text-slate-500">
<div className="flex-1 flex flex-col items-center justify-center text-tertiary select-none">
<Languages className="w-12 h-12 mb-3 text-tertiary" />
<p className="text-xs font-bold text-secondary">
</p>
<p className="text-xs text-slate-400 mt-1">
<p className="text-xs text-tertiary mt-1">
</p>
</div>
@@ -523,10 +594,10 @@ export function BilingualViewer({
transform: hoverCardPos.isAbove ? 'translateY(-100%)' : 'none',
zIndex: 9999,
}}
className="bg-white rounded-lg p-4 border border-slate-350 shadow-sm w-72 text-[11px] space-y-2 pointer-events-auto select-text animate-in fade-in duration-200"
className="bg-surface rounded-lg p-4 shadow-sm w-72 text-[11px] space-y-2 pointer-events-auto select-text animate-in fade-in duration-200"
>
<div className="flex items-center justify-between border-b border-slate-200 pb-2">
<span className="font-bold text-slate-900 text-[13px]">
<div className="flex items-center justify-between border-b border-subtle pb-2">
<span className="font-bold text-content text-[13px]">
{hoveredTarget.target_name}
</span>
<div className="flex items-center gap-1.5 select-none">
@@ -534,7 +605,7 @@ export function BilingualViewer({
href={`https://simbad.cds.unistra.fr/simbad/sim-id?Ident=${encodeURIComponent(hoveredTarget.target_name)}`}
target="_blank"
rel="noopener noreferrer"
className="text-[9px] font-extrabold bg-slate-100 hover:bg-slate-200 text-slate-700 hover:text-slate-900 px-1.5 py-0.5 rounded border border-slate-200 transition-all cursor-pointer"
className="text-[9px] font-extrabold bg-sunken hover:bg-border-subtle text-secondary hover:text-content px-1.5 py-0.5 rounded transition-all cursor-pointer"
title="在 SIMBAD 中查询该天体详情"
>
SIMBAD
@@ -543,59 +614,59 @@ export function BilingualViewer({
href={`https://vizier.cds.unistra.fr/viz-bin/VizieR?-c=${encodeURIComponent(hoveredTarget.target_name)}`}
target="_blank"
rel="noopener noreferrer"
className="text-[9px] font-extrabold bg-slate-100 hover:bg-slate-200 text-slate-700 hover:text-slate-900 px-1.5 py-0.5 rounded border border-slate-200 transition-all cursor-pointer"
className="text-[9px] font-extrabold bg-sunken hover:bg-border-subtle text-secondary hover:text-content px-1.5 py-0.5 rounded transition-all cursor-pointer"
title="在 VizieR 中查询相关文献和表数据"
>
VizieR
</a>
</div>
</div>
<div className="grid grid-cols-2 gap-x-2 gap-y-1.5 text-slate-600">
<div className="grid grid-cols-2 gap-x-2 gap-y-1.5 text-secondary">
{/* 天体类型 & 官方名称 */}
<div className="col-span-2 flex items-center gap-2">
{hoveredTarget.otype && (
<span className="inline-flex items-center px-1.5 py-0.5 rounded bg-amber-50 text-amber-850 border border-amber-200/70 text-[9px] font-bold font-mono">
<span className="inline-flex items-center px-1.5 py-0.5 rounded bg-amber-50 dark:bg-amber-500/10 text-amber-700 dark:text-amber-300 border border-amber-200/70 dark:border-amber-500/30 text-[9px] font-bold font-mono">
{hoveredTarget.otype}
</span>
)}
{hoveredTarget.oname &&
hoveredTarget.oname !== hoveredTarget.target_name && (
<span className="text-[10px] text-slate-400 font-medium truncate">
<span className="text-[10px] text-tertiary font-medium truncate">
aka {hoveredTarget.oname}
</span>
)}
</div>
{/* 坐标 */}
<div>
<span className="text-slate-500 font-bold tracking-wide text-[9px] uppercase block">
<span className="text-secondary font-bold tracking-wide text-[9px] uppercase block">
RA (J2000)
</span>
<div className="font-mono font-bold text-slate-800">
<div className="font-mono font-bold text-content">
{hoveredTarget.ra || '未知'}
</div>
</div>
<div>
<span className="text-slate-500 font-bold tracking-wide text-[9px] uppercase block">
<span className="text-secondary font-bold tracking-wide text-[9px] uppercase block">
Dec (J2000)
</span>
<div className="font-mono font-bold text-slate-800">
<div className="font-mono font-bold text-content">
{hoveredTarget.dec || '未知'}
</div>
</div>
{/* 光谱型 & 视星等 */}
<div>
<span className="text-slate-500 font-bold tracking-wide text-[9px] uppercase block">
<span className="text-secondary font-bold tracking-wide text-[9px] uppercase block">
</span>
<div className="font-bold text-slate-800">
<div className="font-bold text-content">
{hoveredTarget.spectral_type || '未知'}
</div>
</div>
<div>
<span className="text-slate-500 font-bold tracking-wide text-[9px] uppercase block">
<span className="text-secondary font-bold tracking-wide text-[9px] uppercase block">
(V)
</span>
<div className="font-bold text-slate-800">
<div className="font-bold text-content">
{hoveredTarget.v_magnitude !== null &&
hoveredTarget.v_magnitude !== undefined
? `${hoveredTarget.v_magnitude.toFixed(2)}`
@@ -604,10 +675,10 @@ export function BilingualViewer({
</div>
{/* 视差 + 误差 + 估算距离 */}
<div className="col-span-2">
<span className="text-slate-500 font-bold tracking-wide text-[9px] uppercase block">
<span className="text-secondary font-bold tracking-wide text-[9px] uppercase block">
/
</span>
<div className="font-bold text-slate-800">
<div className="font-bold text-content">
{hoveredTarget.parallax !== null &&
hoveredTarget.parallax !== undefined
? `${hoveredTarget.parallax.toFixed(2)}${hoveredTarget.parallax_err != null ? `±${hoveredTarget.parallax_err.toFixed(4)}` : ''} mas (~${(1000.0 / hoveredTarget.parallax).toFixed(1)} pc)`
@@ -616,10 +687,10 @@ export function BilingualViewer({
</div>
{/* 自行 */}
<div className="col-span-2">
<span className="text-slate-500 font-bold tracking-wide text-[9px] uppercase block">
<span className="text-secondary font-bold tracking-wide text-[9px] uppercase block">
</span>
<div className="font-bold text-slate-800 text-[10.5px]">
<div className="font-bold text-content text-[10.5px]">
{hoveredTarget.pm_ra != null && hoveredTarget.pm_de != null
? `RA ${hoveredTarget.pm_ra.toFixed(3)}, Dec ${hoveredTarget.pm_de.toFixed(3)} mas/yr`
: '未知'}
@@ -627,10 +698,10 @@ export function BilingualViewer({
</div>
{/* 视向速度 */}
<div className="col-span-2">
<span className="text-slate-500 font-bold tracking-wide text-[9px] uppercase block">
<span className="text-secondary font-bold tracking-wide text-[9px] uppercase block">
</span>
<div className="font-bold text-slate-800">
<div className="font-bold text-content">
{hoveredTarget.radial_velocity != null
? `${hoveredTarget.radial_velocity.toFixed(1)} km/s`
: '未知'}
@@ -641,15 +712,15 @@ export function BilingualViewer({
{hoveredTarget.photometry &&
Object.keys(hoveredTarget.photometry).length > 1 && (
<div className="border-t border-slate-200/60 pt-2">
<span className="text-slate-500 font-bold tracking-wide text-[9px] uppercase block mb-1">
<span className="text-secondary font-bold tracking-wide text-[9px] uppercase block mb-1">
</span>
<div className="flex flex-wrap gap-x-2.5 gap-y-0.5 text-[10px] font-mono text-slate-600">
<div className="flex flex-wrap gap-x-2.5 gap-y-0.5 text-[10px] font-mono text-secondary">
{Object.entries(hoveredTarget.photometry).map(
([band, mag]) => (
<span key={band} className="whitespace-nowrap">
<span className="text-slate-400">{band}</span>{' '}
<span className="font-bold text-slate-700">
<span className="text-tertiary">{band}</span>{' '}
<span className="font-bold text-secondary">
{mag.toFixed(3)}
</span>
</span>
@@ -660,10 +731,10 @@ export function BilingualViewer({
)}
{hoveredTarget.aliases && hoveredTarget.aliases.length > 0 && (
<div className="border-t border-slate-200/60 pt-2">
<span className="text-slate-500 font-bold tracking-wide text-[9px] uppercase block mb-0.5">
<span className="text-secondary font-bold tracking-wide text-[9px] uppercase block mb-0.5">
</span>
<div className="text-[10px] text-slate-500 font-medium leading-relaxed max-h-[36px] overflow-y-auto scrollbar-thin font-mono">
<div className="text-[10px] text-secondary font-medium leading-relaxed max-h-[36px] overflow-y-auto scrollbar-thin font-mono">
{hoveredTarget.aliases.slice(0, 8).join(', ')}
{hoveredTarget.aliases.length > 8 && ' ...'}
</div>
@@ -74,32 +74,32 @@ export function ReaderNotesSidebar({
<button
type="button"
onClick={() => setShowNotesPanel(false)}
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"
className="fixed inset-0 bg-black/30 backdrop-blur-xs z-35 lg:hidden cursor-pointer w-full h-full border-none outline-none"
aria-label="关闭侧栏"
/>
<div className="console-panel rounded-lg border border-slate-200 bg-slate-50 flex flex-col overflow-hidden shadow-sm h-full fixed top-0 right-0 h-full w-[88vw] sm:w-[380px] z-40 lg:relative lg:top-auto lg:right-auto lg:h-full lg:w-auto lg:z-10">
<div className="px-4 py-3 border-b border-slate-200 flex items-center justify-between bg-white shrink-0">
<span className="text-xs font-bold text-slate-800">
<div className="console-panel rounded-lg bg-sunken flex flex-col overflow-hidden shadow-sm h-full fixed top-0 right-0 h-full w-[88vw] sm:w-[380px] z-40 lg:relative lg:top-auto lg:right-auto lg:h-full lg:w-auto lg:z-10">
<div className="px-4 py-3 border-b border-subtle flex items-center justify-between bg-surface shrink-0">
<span className="text-xs font-bold text-content">
{sidebarTab === 'notes' ? '观测记录手札' : '文献 AI 问答'}
</span>
<button
onClick={() => setShowNotesPanel(false)}
className="text-slate-400 hover:text-slate-600 transition-colors cursor-pointer"
className="text-tertiary hover:text-secondary transition-colors cursor-pointer"
>
<X className="w-4 h-4" />
</button>
</div>
{/* 标签栏选择器 */}
<div className="flex border-b border-slate-200 bg-white select-none shrink-0">
<div className="flex border-b border-subtle bg-surface select-none shrink-0">
<button
type="button"
onClick={() => setSidebarTab('notes')}
className={`flex-1 py-2 text-center text-xs font-bold transition-all border-b-2 cursor-pointer ${
sidebarTab === 'notes'
? 'border-blueprint text-blueprint'
: 'border-transparent text-slate-500 hover:text-slate-700'
: 'border-transparent text-secondary hover:text-secondary'
}`}
>
({notes.length})
@@ -110,7 +110,7 @@ export function ReaderNotesSidebar({
className={`flex-1 py-2 text-center text-xs font-bold transition-all border-b-2 cursor-pointer ${
sidebarTab === 'ai'
? 'border-blueprint text-blueprint'
: 'border-transparent text-slate-500 hover:text-slate-700'
: 'border-transparent text-secondary hover:text-secondary'
}`}
>
AI
@@ -121,14 +121,14 @@ export function ReaderNotesSidebar({
{sidebarTab === 'notes' ? (
<div className="flex-1 flex flex-col overflow-hidden min-h-0">
{/* 天体标识符列表 */}
<div className="px-4 py-3 border-b border-slate-200 bg-white space-y-2 shrink-0">
<div className="px-4 py-3 border-b border-subtle bg-surface space-y-2 shrink-0">
<div className="flex items-center justify-between">
<span className="text-[10px] font-bold text-slate-400 tracking-wider uppercase">
<span className="text-[10px] font-bold text-tertiary tracking-wider uppercase">
(CDS)
</span>
<div className="flex items-center gap-1.5 select-none">
{(loadingTargets || identifyingTargets) && (
<Loader className="w-3 h-3 animate-spin text-slate-400" />
<Loader className="w-3 h-3 animate-spin text-tertiary" />
)}
{selectedPaper.has_markdown && (
<button
@@ -137,7 +137,7 @@ export function ReaderNotesSidebar({
handleIdentifyTargets(selectedPaper.bibcode)
}
disabled={identifyingTargets || loadingTargets}
className="text-[9px] font-bold px-1.5 py-0.5 rounded bg-sky-50 text-sky-700 border border-sky-200 hover:bg-sky-100 transition-all cursor-pointer flex items-center gap-0.5"
className="text-[9px] font-bold px-1.5 py-0.5 rounded bg-sky-50 dark:bg-sky-500/10 text-sky-700 dark:text-sky-300 border border-sky-200 dark:border-sky-500/30 hover:bg-sky-100 transition-all cursor-pointer flex items-center gap-0.5"
title="从文献正文中自动识别天体目标并查询 CDS Sesame"
>
<Sparkles className="w-2.5 h-2.5" />
@@ -148,7 +148,7 @@ export function ReaderNotesSidebar({
</div>
<div className="flex flex-wrap gap-1.5 max-h-24 overflow-y-auto pr-1 scrollbar-thin">
{targets.length === 0 ? (
<span className="text-[10px] text-slate-400 italic">
<span className="text-[10px] text-tertiary italic">
</span>
) : (
@@ -175,7 +175,7 @@ export function ReaderNotesSidebar({
>
{t.target_name}
{t.spectral_type && (
<span className="text-slate-400 font-medium font-mono">
<span className="text-tertiary font-medium font-mono">
({t.spectral_type})
</span>
)}
@@ -193,7 +193,7 @@ export function ReaderNotesSidebar({
value={manualTargetName}
onChange={(e) => setManualTargetName(e.target.value)}
placeholder="手动关联天体(如 M 31)..."
className="flex-1 bg-slate-50 border border-slate-200 rounded px-2.5 py-1 text-[10px] text-slate-900 placeholder-slate-400 leading-relaxed font-semibold focus:outline-none focus:bg-white focus:border-sky-500 focus:ring-1 focus:ring-sky-500/10"
className="flex-1 bg-sunken border border-default rounded px-2.5 py-1 text-[10px] text-content placeholder-tertiary leading-relaxed font-semibold focus:outline-none focus:bg-surface focus:border-blueprint focus:ring-1 focus:ring-blueprint/10"
/>
<button
type="submit"
@@ -207,18 +207,18 @@ export function ReaderNotesSidebar({
{/* 新建笔记输入区 */}
{selectedParagraphIdx !== null && (
<div className="px-4 py-4 border-b border-slate-200 bg-white space-y-3 shrink-0">
<div className="text-xs font-bold text-slate-700">
<div className="px-4 py-4 border-b border-subtle bg-surface space-y-3 shrink-0">
<div className="text-xs font-bold text-secondary">
#{selectedParagraphIdx + 1}
</div>
{selectedText && (
<div className="text-xs text-slate-600 italic line-clamp-2 bg-slate-55 px-2.5 py-1.5 rounded border border-slate-200">
<div className="text-xs text-secondary italic line-clamp-2 bg-sunken px-2.5 py-1.5 rounded ">
"{selectedText}"
</div>
)}
{/* 颜色标记 */}
<div className="flex gap-2 items-center">
<span className="text-xs text-slate-500 font-semibold">
<span className="text-xs text-secondary font-semibold">
:
</span>
<div className="flex gap-1.5">
@@ -244,7 +244,7 @@ export function ReaderNotesSidebar({
onChange={(e) => setNewNoteText(e.target.value)}
placeholder="记录段落心得或重点..."
rows={3}
className="w-full bg-white border border-slate-300 rounded-lg text-xs text-slate-900 placeholder-slate-400 px-3 py-2 resize-none focus:outline-none focus:border-sky-500 focus:ring-1 focus:ring-sky-500/10 leading-relaxed"
className="w-full bg-surface border border-default rounded-lg text-xs text-content placeholder-tertiary px-3 py-2 resize-none focus:outline-none focus:border-blueprint focus:ring-1 focus:ring-blueprint/10 leading-relaxed"
/>
<div className="flex gap-2">
<button
@@ -270,8 +270,8 @@ export function ReaderNotesSidebar({
{/* 笔记列表 */}
<div className="flex-1 overflow-y-auto p-4 space-y-3 min-h-0 scrollbar-thin">
{notes.length === 0 ? (
<div className="text-center text-slate-400 text-xs py-12 space-y-2 select-none">
<Pencil className="w-8 h-8 mx-auto opacity-30 text-slate-500" />
<div className="text-center text-tertiary text-xs py-12 space-y-2 select-none">
<Pencil className="w-8 h-8 mx-auto opacity-30 text-secondary" />
<div></div>
</div>
) : (
@@ -281,31 +281,31 @@ export function ReaderNotesSidebar({
return (
<div
key={note.id}
className="p-4 rounded-lg border text-xs bg-white border-slate-200 relative group overflow-hidden"
className="p-4 rounded-lg border text-xs bg-surface border-subtle relative group overflow-hidden"
>
{/* 彩色左标记条 */}
<div
className={`absolute left-0 top-0 w-1 h-full ${style.bg.split(' ')[0]}`}
/>
<div className="text-slate-500 text-[10px] font-bold mb-1">
<div className="text-secondary text-[10px] font-bold mb-1">
#{note.paragraph_index + 1}
</div>
{note.selected_text && (
<div className="text-slate-505 italic line-clamp-2 mb-2 border-l-2 border-slate-200 pl-2">
<div className="text-secondary italic line-clamp-2 mb-2 border-l-2 border-subtle pl-2">
"{note.selected_text}"
</div>
)}
<p className="text-slate-800 leading-relaxed font-medium">
<p className="text-content leading-relaxed font-medium">
{note.note_text}
</p>
<div className="flex items-center justify-between mt-3 border-t border-slate-100 pt-2 text-[10px] select-none">
<span className="text-slate-400 font-semibold">
<div className="flex items-center justify-between mt-3 border-t border-subtle pt-2 text-[10px] select-none">
<span className="text-tertiary font-semibold">
{note.created_at.split('T')[0]}
</span>
<button
onClick={() => handleDeleteNote(note.id)}
className="text-slate-400 hover:text-red-600 transition-colors cursor-pointer"
className="text-tertiary hover:text-red-600 transition-colors cursor-pointer"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
@@ -70,15 +70,15 @@ export function ReaderToolbar({
const [showActionsMenu, setShowActionsMenu] = useState(false);
return (
<div className="flex flex-col xl:flex-row xl:items-center xl:justify-between border-b border-slate-200 pb-3 shrink-0 gap-3">
<div className="flex flex-col xl:flex-row xl:items-center xl:justify-between border-b border-subtle pb-3 shrink-0 gap-3">
<div className="min-w-0 w-full xl:flex-1 pr-0 xl:pr-4">
<h2
className="text-sm font-bold text-slate-900 line-clamp-1 leading-snug"
className="text-sm font-bold text-content line-clamp-1 leading-snug"
title={selectedPaper.title}
>
{selectedPaper.title}
</h2>
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-slate-500 mt-1 font-semibold">
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-secondary mt-1 font-semibold">
<span>: {selectedPaper.pub_journal || '未标注'}</span>
<span className="hidden sm:inline"></span>
<span>: {selectedPaper.bibcode}</span>
@@ -93,7 +93,7 @@ export function ReaderToolbar({
className="btn-console btn-console-secondary px-2 sm:px-4 py-2 rounded-lg text-xs font-bold flex items-center gap-1 sm:gap-2 transition-all cursor-pointer shrink-0"
title="快速切换文献"
>
<BookOpen className="w-3.5 h-3.5 text-sky-600" />
<BookOpen className="w-3.5 h-3.5 text-blueprint" />
<span className="hidden sm:inline"></span>
<span className="sm:hidden"></span>
<ChevronDown
@@ -107,12 +107,12 @@ export function ReaderToolbar({
className="fixed inset-0 z-45"
onClick={() => setShowSwitchMenu(false)}
/>
<div className="absolute left-0 xl:right-0 xl:left-auto mt-1.5 w-72 rounded-md bg-white border border-slate-200 shadow-md py-1 z-50 text-xs max-h-96 overflow-y-auto scrollbar-thin">
<div className="absolute left-0 xl:right-0 xl:left-auto mt-1.5 w-72 rounded-md bg-surface shadow-md py-1 z-50 text-xs max-h-96 overflow-y-auto scrollbar-thin">
{/* 最近阅读部分 */}
{recentlySelected.length > 0 && (
<div className="border-b border-slate-100 pb-1.5 mb-1.5">
<div className="px-3 py-1 text-[10px] font-bold text-slate-400 uppercase flex items-center gap-1">
<History className="w-3.5 h-3.5 text-slate-400" />
<div className="border-b border-subtle pb-1.5 mb-1.5">
<div className="px-3 py-1 text-[10px] font-bold text-tertiary uppercase flex items-center gap-1">
<History className="w-3.5 h-3.5 text-tertiary" />
<span></span>
</div>
{recentlySelected.map((paper) => (
@@ -122,16 +122,16 @@ export function ReaderToolbar({
onSwitchPaper(paper);
setShowSwitchMenu(false);
}}
className={`w-full px-3 py-2 text-left hover:bg-slate-50 transition-colors flex flex-col gap-0.5 border-l-2 ${
className={`w-full px-3 py-2 text-left hover:bg-sunken transition-colors flex flex-col gap-0.5 border-l-2 ${
paper.bibcode === selectedPaper.bibcode
? 'border-blueprint bg-blueprint/5 text-blueprint font-bold'
: 'border-transparent text-slate-700 font-medium'
: 'border-transparent text-secondary font-medium'
}`}
>
<span className="truncate block leading-tight text-left">
{paper.title}
</span>
<span className="text-[9px] text-slate-400 font-semibold font-mono text-left">
<span className="text-[9px] text-tertiary font-semibold font-mono text-left">
{paper.bibcode}
</span>
</button>
@@ -141,12 +141,12 @@ export function ReaderToolbar({
{/* 全部馆藏已下载文献 */}
<div>
<div className="px-3 py-1 text-[10px] font-bold text-slate-400 uppercase flex items-center gap-1">
<FileText className="w-3.5 h-3.5 text-slate-400" />
<div className="px-3 py-1 text-[10px] font-bold text-tertiary uppercase flex items-center gap-1">
<FileText className="w-3.5 h-3.5 text-tertiary" />
<span></span>
</div>
{library.filter((p) => p.is_downloaded).length === 0 ? (
<div className="px-3 py-2 text-slate-400 italic">
<div className="px-3 py-2 text-tertiary italic">
</div>
) : (
@@ -159,16 +159,16 @@ export function ReaderToolbar({
onSwitchPaper(paper);
setShowSwitchMenu(false);
}}
className={`w-full px-3 py-2 text-left hover:bg-slate-50 transition-colors flex flex-col gap-0.5 border-l-2 ${
className={`w-full px-3 py-2 text-left hover:bg-sunken transition-colors flex flex-col gap-0.5 border-l-2 ${
paper.bibcode === selectedPaper.bibcode
? 'border-blueprint bg-blueprint/5 text-blueprint font-bold'
: 'border-transparent text-slate-700 font-medium'
: 'border-transparent text-secondary font-medium'
}`}
>
<span className="truncate block leading-tight text-left">
{paper.title}
</span>
<span className="text-[9px] text-slate-400 font-semibold font-mono text-left">
<span className="text-[9px] text-tertiary font-semibold font-mono text-left">
{paper.bibcode}
</span>
</button>
@@ -182,14 +182,14 @@ export function ReaderToolbar({
{/* 原文/中文/对照视图切换按钮 */}
{(englishText || chineseText) && (
<div className="flex gap-0.5 sm:gap-1 bg-slate-100 p-0.5 rounded-lg border border-slate-200 shrink-0">
<div className="flex gap-0.5 sm:gap-1 bg-sunken p-0.5 rounded-lg shrink-0">
<button
type="button"
onClick={() => setViewMode('english')}
className={`px-1.5 sm:px-2.5 py-1 rounded-md text-[10px] sm:text-xs font-bold transition-all cursor-pointer ${
viewMode === 'english'
? 'bg-white text-slate-800 shadow-sm'
: 'text-slate-500 hover:text-slate-700'
? 'bg-surface text-content shadow-sm'
: 'text-secondary hover:text-secondary'
}`}
>
@@ -199,8 +199,8 @@ export function ReaderToolbar({
onClick={() => setViewMode('chinese')}
className={`px-1.5 sm:px-2.5 py-1 rounded-md text-[10px] sm:text-xs font-bold transition-all cursor-pointer ${
viewMode === 'chinese'
? 'bg-white text-slate-800 shadow-sm'
: 'text-slate-500 hover:text-slate-700'
? 'bg-surface text-content shadow-sm'
: 'text-secondary hover:text-secondary'
}`}
>
@@ -210,8 +210,8 @@ export function ReaderToolbar({
onClick={() => setViewMode('bilingual')}
className={`px-1.5 sm:px-2.5 py-1 rounded-md text-[10px] sm:text-xs font-bold transition-all cursor-pointer ${
viewMode === 'bilingual'
? 'bg-white text-slate-800 shadow-sm'
: 'text-slate-500 hover:text-slate-700'
? 'bg-surface text-content shadow-sm'
: 'text-secondary hover:text-secondary'
}`}
>
@@ -234,10 +234,10 @@ export function ReaderToolbar({
}}
className={`hidden xl:flex px-3 py-1.5 rounded-lg text-xs font-bold items-center gap-1.5 border transition-all cursor-pointer ${
showPdf
? 'bg-slate-50 text-slate-400 border-slate-200 cursor-not-allowed opacity-70'
? 'bg-sunken text-tertiary border-subtle cursor-not-allowed opacity-70'
: syncScroll
? 'bg-sky-50 text-sky-700 border-sky-200 shadow-sm'
: 'bg-white text-slate-600 border-slate-200 hover:bg-slate-55'
? 'bg-sky-50 dark:bg-sky-500/10 text-sky-700 dark:text-sky-300 border-sky-200 dark:border-sky-500/30 shadow-sm'
: 'bg-surface text-secondary border-subtle hover:bg-sunken'
}`}
title={
showPdf
@@ -246,7 +246,7 @@ export function ReaderToolbar({
}
>
<span
className={`w-1.5 h-1.5 rounded-full ${!showPdf && syncScroll ? 'bg-sky-500 animate-pulse' : 'bg-slate-300'}`}
className={`w-1.5 h-1.5 rounded-full ${!showPdf && syncScroll ? 'bg-sky-500 dark:bg-sky-400 animate-pulse' : 'bg-slate-300'}`}
/>
<span>
{showPdf ? 'PDF暂不支持' : syncScroll ? '开启' : '关闭'}
@@ -373,7 +373,7 @@ export function ReaderToolbar({
className="btn-console btn-console-secondary px-2 sm:px-4 py-2 rounded-lg text-xs font-bold flex items-center gap-1 sm:gap-2 transition-all cursor-pointer shrink-0"
title="更多文献操作"
>
<SlidersHorizontal className="w-3.5 h-3.5 text-sky-600" />
<SlidersHorizontal className="w-3.5 h-3.5 text-blueprint" />
<span className="hidden sm:inline"></span>
<span className="sm:hidden"></span>
<ChevronDown
@@ -387,7 +387,7 @@ export function ReaderToolbar({
className="fixed inset-0 z-45"
onClick={() => setShowActionsMenu(false)}
/>
<div className="absolute right-0 mt-1.5 w-max min-w-[145px] rounded-md bg-white border border-slate-200 shadow-md py-1 z-50 text-xs">
<div className="absolute right-0 mt-1.5 w-max min-w-[145px] rounded-md bg-surface shadow-md py-1 z-50 text-xs">
{/* 同步滚动 */}
{(englishText || chineseText) && viewMode === 'bilingual' && (
<button
@@ -403,11 +403,11 @@ export function ReaderToolbar({
className={`w-full text-left px-4 py-2 text-xs transition-colors flex items-center gap-2 font-medium cursor-pointer outline-none ${
syncScroll
? 'bg-blueprint/5 text-blueprint font-bold border-l-2 border-blueprint'
: 'text-slate-600 hover:bg-slate-50 hover:text-slate-900'
: 'text-secondary hover:bg-sunken hover:text-content'
}`}
>
<span
className={`w-1.5 h-1.5 rounded-full ${!showPdf && syncScroll ? 'bg-sky-500 animate-pulse' : 'bg-slate-355'}`}
className={`w-1.5 h-1.5 rounded-full ${!showPdf && syncScroll ? 'bg-sky-500 dark:bg-sky-400 animate-pulse' : 'bg-border-subtle'}`}
/>
<span>{syncScroll ? '已开启' : '已关闭'}</span>
</button>
@@ -421,9 +421,9 @@ export function ReaderToolbar({
setShowActionsMenu(false);
}}
disabled={parsing}
className="w-full text-left px-4 py-2 text-xs transition-colors flex items-center gap-2 text-slate-600 hover:bg-slate-50 hover:text-slate-900 font-medium cursor-pointer outline-none"
className="w-full text-left px-4 py-2 text-xs transition-colors flex items-center gap-2 text-secondary hover:bg-sunken hover:text-content font-medium cursor-pointer outline-none"
>
<FileText className="w-4 h-4 text-slate-400" />
<FileText className="w-4 h-4 text-tertiary" />
<span>{parsing ? '正文解析中...' : '解析源文正文'}</span>
</button>
) : (
@@ -439,9 +439,9 @@ export function ReaderToolbar({
);
}}
disabled={parsing}
className="w-full text-left px-4 py-2 text-xs transition-colors flex items-center gap-2 text-slate-600 hover:bg-slate-50 hover:text-slate-900 font-medium cursor-pointer outline-none"
className="w-full text-left px-4 py-2 text-xs transition-colors flex items-center gap-2 text-secondary hover:bg-sunken hover:text-content font-medium cursor-pointer outline-none"
>
<RotateCw className="w-4 h-4 text-slate-400" />
<RotateCw className="w-4 h-4 text-tertiary" />
<span></span>
</button>
)}
@@ -455,9 +455,9 @@ export function ReaderToolbar({
setShowActionsMenu(false);
}}
disabled={translating}
className="w-full text-left px-4 py-2 text-xs transition-colors flex items-center gap-2 text-slate-600 hover:bg-slate-50 hover:text-slate-900 font-medium cursor-pointer outline-none"
className="w-full text-left px-4 py-2 text-xs transition-colors flex items-center gap-2 text-secondary hover:bg-sunken hover:text-content font-medium cursor-pointer outline-none"
>
<Languages className="w-4 h-4 text-slate-400" />
<Languages className="w-4 h-4 text-tertiary" />
<span>{translating ? '翻译中...' : '生成智能翻译'}</span>
</button>
)}
@@ -475,9 +475,9 @@ export function ReaderToolbar({
);
}}
disabled={translating}
className="w-full text-left px-4 py-2 text-xs transition-colors flex items-center gap-2 text-slate-600 hover:bg-slate-50 hover:text-slate-900 font-medium cursor-pointer outline-none"
className="w-full text-left px-4 py-2 text-xs transition-colors flex items-center gap-2 text-secondary hover:bg-sunken hover:text-content font-medium cursor-pointer outline-none"
>
<RotateCw className="w-4 h-4 text-slate-400" />
<RotateCw className="w-4 h-4 text-tertiary" />
<span></span>
</button>
)}
@@ -490,7 +490,7 @@ export function ReaderToolbar({
setShowActionsMenu(false);
}}
disabled={vectorizing}
className="w-full text-left px-4 py-2 text-xs transition-colors flex items-center gap-2 text-slate-600 hover:bg-slate-50 hover:text-slate-900 font-medium cursor-pointer outline-none"
className="w-full text-left px-4 py-2 text-xs transition-colors flex items-center gap-2 text-secondary hover:bg-sunken hover:text-content font-medium cursor-pointer outline-none"
>
<Sparkles className="w-4 h-4 text-amber-500" />
<span>{vectorizing ? '知识入库中...' : '知识入库'}</span>
@@ -510,9 +510,9 @@ export function ReaderToolbar({
);
}}
disabled={vectorizing}
className="w-full text-left px-4 py-2 text-xs transition-colors flex items-center gap-2 text-slate-600 hover:bg-slate-50 hover:text-slate-900 font-medium cursor-pointer outline-none"
className="w-full text-left px-4 py-2 text-xs transition-colors flex items-center gap-2 text-secondary hover:bg-sunken hover:text-content font-medium cursor-pointer outline-none"
>
<RotateCw className="w-4 h-4 text-slate-400" />
<RotateCw className="w-4 h-4 text-tertiary" />
<span></span>
</button>
)}
@@ -528,7 +528,7 @@ export function ReaderToolbar({
}}
className={`btn-console px-2 sm:px-4 py-2 rounded-lg text-xs font-bold flex items-center gap-1 sm:gap-2 transition-all cursor-pointer shrink-0 ${
showNotesPanel
? 'bg-sky-50 text-sky-700 border-sky-200 shadow-xs'
? 'bg-sky-50 dark:bg-sky-500/10 text-sky-700 dark:text-sky-300 border-sky-200 dark:border-sky-500/30'
: 'btn-console-secondary'
}`}
title="开启或关闭侧边学术助手(包含阅读手札与 AI 问答)"
@@ -537,7 +537,7 @@ export function ReaderToolbar({
<span className="hidden sm:inline"></span>
<span className="sm:hidden"></span>
{notesCount > 0 && (
<span className="px-1.5 py-0.2 text-[9px] rounded-full bg-sky-100 text-sky-850 font-extrabold select-none shrink-0">
<span className="px-1.5 py-0.2 text-[9px] rounded-full bg-sky-100 text-content font-extrabold select-none shrink-0">
{notesCount}
</span>
)}
@@ -59,20 +59,20 @@ export function BatchPipelineCard({
handleStopBatch,
}: BatchPipelineCardProps) {
return (
<div className="console-panel p-6 rounded-lg border border-slate-200 bg-white space-y-6 relative overflow-hidden shadow-sm">
<div className="console-panel p-6 rounded-lg bg-surface space-y-6 relative overflow-hidden shadow-sm">
<div className="flex flex-col gap-1 select-none">
<h3 className="text-xs font-bold text-slate-900 flex items-center gap-2">
<Download className="w-4 h-4 text-slate-700" />
<h3 className="text-xs font-bold text-content flex items-center gap-2">
<Download className="w-4 h-4 text-secondary" />
<span>线</span>
</h3>
<p className="text-slate-500 text-xs mt-0.5">
<p className="text-secondary text-xs mt-0.5">
PDF/HTML Markdown
</p>
</div>
{batchError && (
<div className="p-4 rounded-md bg-rose-50 border border-rose-200/60 flex gap-3 text-xs text-rose-800 items-start">
<div className="p-4 rounded-md bg-rose-50 border border-rose-200/60 dark:bg-rose-500/10 dark:border-rose-500/30 flex gap-3 text-xs text-rose-800 dark:text-rose-300 items-start">
<AlertTriangle className="w-4 h-4 shrink-0 mt-0.5" />
<div>{batchError}</div>
</div>
@@ -81,7 +81,7 @@ export function BatchPipelineCard({
{/* 条件设置 */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="space-y-2">
<label className="text-xs font-bold text-slate-700 block">
<label className="text-xs font-bold text-secondary block">
</label>
<CustomSelect
@@ -104,7 +104,7 @@ export function BatchPipelineCard({
</div>
<div className="space-y-2">
<label className="text-xs font-bold text-slate-700 block">
<label className="text-xs font-bold text-secondary block">
</label>
<input
@@ -114,12 +114,12 @@ export function BatchPipelineCard({
onChange={(e) =>
setBatchLimitCount(Math.max(1, parseInt(e.target.value) || 0))
}
className="w-full px-3 py-2 rounded-md bg-slate-50 border border-slate-300 text-slate-900 focus:outline-none focus:border-blueprint transition-all text-xs font-medium"
className="w-full px-3 py-2 rounded-md bg-sunken text-content focus:outline-none focus:border-blueprint transition-all text-xs font-medium"
/>
</div>
<div className="space-y-2">
<label className="text-xs font-bold text-slate-700 block">
<label className="text-xs font-bold text-secondary block">
</label>
<CustomSelect
@@ -141,12 +141,12 @@ export function BatchPipelineCard({
</div>
{/* 执行策略 */}
<div className="space-y-2 border-t border-slate-100 pt-4">
<label className="text-xs font-bold text-slate-700 block">
<div className="space-y-2 border-t border-subtle pt-4">
<label className="text-xs font-bold text-secondary block">
</label>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 select-none">
<label className="flex items-center gap-2 text-xs font-medium text-slate-700 cursor-pointer">
<label className="flex items-center gap-2 text-xs font-medium text-secondary cursor-pointer">
<input
type="checkbox"
checked={skipCompleted}
@@ -157,7 +157,7 @@ export function BatchPipelineCard({
<span></span>
</label>
<label className="flex items-center gap-2 text-xs font-medium text-slate-700 cursor-pointer">
<label className="flex items-center gap-2 text-xs font-medium text-secondary cursor-pointer">
<input
type="checkbox"
checked={skipFailed}
@@ -168,7 +168,7 @@ export function BatchPipelineCard({
<span></span>
</label>
<label className="flex items-center gap-2 text-xs font-medium text-slate-700 cursor-pointer">
<label className="flex items-center gap-2 text-xs font-medium text-secondary cursor-pointer">
<input
type="checkbox"
checked={skipPrecedingFailed}
@@ -179,7 +179,7 @@ export function BatchPipelineCard({
<span></span>
</label>
<label className="flex items-center gap-2 text-xs font-medium text-slate-700 cursor-pointer">
<label className="flex items-center gap-2 text-xs font-medium text-secondary cursor-pointer">
<input
type="checkbox"
checked={skipPrecedingUncompleted}
@@ -193,13 +193,13 @@ export function BatchPipelineCard({
</div>
{/* 控制按钮 */}
<div className="flex justify-end pt-2 border-t border-slate-100">
<div className="flex justify-end pt-2 border-t border-subtle">
<div className="w-full md:w-1/3 flex">
{batchStatus.active ? (
<button
type="button"
onClick={handleStopBatch}
className="w-full py-2 rounded-md bg-rose-750 hover:bg-rose-855 text-white text-xs font-bold flex items-center justify-center gap-2 transition-all shadow-sm cursor-pointer"
className="w-full py-2 rounded-md bg-danger hover:opacity-90 text-white text-xs font-bold flex items-center justify-center gap-2 transition-all shadow-sm cursor-pointer"
>
<StopCircle className="w-3.5 h-3.5" />
@@ -219,9 +219,9 @@ export function BatchPipelineCard({
{/* 进度与终端日志展示 */}
{(batchStatus.active || batchStatus.total > 0) && (
<div className="space-y-4 pt-4 border-t border-slate-200">
<div className="space-y-4 pt-4 border-t border-subtle">
<div className="space-y-1.5">
<div className="flex justify-between text-xs font-bold text-slate-600 select-none">
<div className="flex justify-between text-xs font-bold text-secondary select-none">
<span className="flex items-center gap-1.5">
{batchStatus.action === 'download' && (
<Download className="w-3.5 h-3.5 text-blueprint" />
@@ -257,7 +257,7 @@ export function BatchPipelineCard({
batchStatus.download_failed > 0) ||
(batchStatus.action !== 'download' &&
batchStatus.parse_failed > 0)) && (
<span className="text-red-500 ml-2 font-bold">
<span className="text-danger ml-2 font-bold">
({' '}
{batchStatus.action === 'download'
? batchStatus.download_failed
@@ -267,7 +267,7 @@ export function BatchPipelineCard({
)}
</span>
</div>
<div className="w-full h-2 rounded-md bg-slate-100 overflow-hidden border border-slate-200 select-none">
<div className="w-full h-2 rounded-md bg-sunken overflow-hidden select-none">
<div
className={`h-full transition-all duration-300 ${
batchStatus.action === 'download'
@@ -302,11 +302,11 @@ export function BatchPipelineCard({
</div>
{batchStatus.active && batchStatus.current_bibcode && (
<div className="text-xs font-bold text-slate-600 flex items-center gap-2">
<div className="text-xs font-bold text-secondary flex items-center gap-2">
<Loader className="w-3.5 h-3.5 text-blueprint animate-spin" />
<span>
:{' '}
<code className="bg-slate-100 px-2 py-0.5 rounded-md font-mono font-bold text-slate-800 border border-slate-200 select-all">
<code className="bg-sunken px-2 py-0.5 rounded-md font-mono font-bold text-content select-all">
{batchStatus.current_bibcode}
</code>
</span>
@@ -315,15 +315,15 @@ export function BatchPipelineCard({
{/* 滚动日志终端 */}
<div className="space-y-2">
<label className="text-xs font-bold text-slate-700 block select-none">
<label className="text-xs font-bold text-secondary block select-none">
</label>
<div
ref={logsContainerRef}
className="bg-slate-50 text-slate-800 font-mono text-[11px] p-4 rounded-md h-48 overflow-y-auto border border-slate-250 space-y-1 scrollbar-thin scrollbar-thumb-slate-300 relative select-all"
className="bg-sunken text-content font-mono text-[11px] p-4 rounded-md h-48 overflow-y-auto space-y-1 scrollbar-thin relative select-all"
>
{batchStatus.logs.length === 0 ? (
<div className="text-slate-400 italic select-none">
<div className="text-tertiary italic select-none">
...
</div>
) : (
@@ -22,13 +22,13 @@ export function BookmarkletToolCard() {
};
return (
<div className="console-panel p-6 rounded-lg border border-slate-200 bg-white space-y-4 shadow-sm">
<div className="flex flex-col gap-1 border-b border-slate-100 pb-2 select-none">
<h3 className="text-xs font-bold text-slate-900 flex items-center gap-2">
<div className="console-panel p-6 rounded-lg bg-surface space-y-4 shadow-sm">
<div className="flex flex-col gap-1 border-b border-subtle pb-2 select-none">
<h3 className="text-xs font-bold text-content flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-blueprint" />
<span></span>
</h3>
<p className="text-slate-500 text-xs mt-1 leading-relaxed">
<p className="text-secondary text-xs mt-1 leading-relaxed">
Cloudflare/WAF
PDF/HTML
AstroResearch
@@ -36,10 +36,10 @@ export function BookmarkletToolCard() {
</p>
</div>
<div className="space-y-3.5 text-xs text-slate-700">
<div className="space-y-3.5 text-xs text-secondary">
<div className="space-y-1.5 font-bold select-none">
<span className="text-slate-500"></span>
<p className="text-[11px] text-slate-400 font-normal mt-1 leading-relaxed">
<span className="text-secondary"></span>
<p className="text-[11px] text-tertiary font-normal mt-1 leading-relaxed">
URL JavaScript
</p>
@@ -48,7 +48,7 @@ export function BookmarkletToolCard() {
{/* 可直接拖拽的链接按钮 */}
<a
ref={linkRef}
className="px-4 py-2 bg-blueprint hover:bg-slate-800 text-white rounded-md text-xs font-bold transition-all shadow-2xs cursor-move flex items-center gap-1.5"
className="px-4 py-2 bg-blueprint hover:opacity-90 text-white rounded-md text-xs font-bold transition-all shadow-sm cursor-move flex items-center gap-1.5"
title="拖拽我到您的书签栏中"
onClick={(e) => e.preventDefault()}
>
@@ -69,7 +69,7 @@ export function BookmarkletToolCard() {
<button
onClick={handleCopyCode}
className="px-3 py-2 bg-slate-100 hover:bg-slate-200 text-slate-700 rounded-md text-[11px] font-bold transition-all cursor-pointer"
className="px-3 py-2 bg-sunken hover:bg-border-subtle text-secondary rounded-md text-[11px] font-bold transition-all cursor-pointer"
>
JavaScript
</button>
@@ -77,23 +77,23 @@ export function BookmarkletToolCard() {
</div>
<div className="space-y-1.5 font-bold mt-4 select-none">
<span className="text-slate-500">使</span>
<ol className="list-decimal list-inside text-[11px] text-slate-500 font-normal pl-1 space-y-1.5 mt-1 leading-relaxed">
<span className="text-secondary">使</span>
<ol className="list-decimal list-inside text-[11px] text-secondary font-normal pl-1 space-y-1.5 mt-1 leading-relaxed">
<li>
{' '}
<span className="font-bold text-slate-750">
<span className="font-bold text-secondary">
BIBCODE / DOI / arXiv ID
</span>
</li>
<li>
<span className="font-bold text-slate-750"></span>
<span className="font-bold text-secondary"></span>
</li>
<li>
<span className="font-bold text-slate-750">
<span className="font-bold text-secondary">
Bibcode
</span>
@@ -63,15 +63,15 @@ export function MetadataSyncCard({
return (
<div className="space-y-6">
{/* 控制面板卡片 */}
<div className="console-panel p-6 rounded-lg space-y-6 relative overflow-hidden bg-white border border-slate-200">
<div className="console-panel p-6 rounded-lg space-y-6 relative overflow-hidden bg-surface ">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-xs font-bold text-slate-700 block flex justify-between items-center">
<label className="text-xs font-bold text-secondary block flex justify-between items-center">
<span> (Query)</span>
<button
type="button"
onClick={() => setShowBuilder(!showBuilder)}
className="text-xs text-slate-655 hover:text-slate-850 font-bold flex items-center gap-1 cursor-pointer underline underline-offset-2"
className="text-xs text-secondary hover:text-content font-bold flex items-center gap-1 cursor-pointer underline underline-offset-2"
>
<SlidersHorizontal className="w-3.5 h-3.5" />
{showBuilder ? '隐藏高级构造' : '高级检索构造'}
@@ -83,12 +83,12 @@ export function MetadataSyncCard({
onChange={(e) => setQuery(e.target.value)}
disabled={status.active}
placeholder="例如: hot subdwarf, Gaia BH1..."
className="w-full px-4 py-2 rounded-md bg-slate-50 border border-slate-300 text-slate-900 placeholder-slate-400 focus:outline-none focus:border-blueprint focus:bg-white transition-all text-xs font-medium"
className="w-full px-4 py-2 rounded-md bg-sunken border border-default text-content placeholder-tertiary focus:outline-none focus:border-blueprint focus:bg-surface transition-all text-xs font-medium"
/>
<div className="text-[11px] text-slate-450 flex flex-wrap gap-x-2.5 px-0.5 mt-1 select-none">
<div className="text-[11px] text-tertiary flex flex-wrap gap-x-2.5 px-0.5 mt-1 select-none">
<span>:</span>
<span>
<code className="text-slate-700 bg-slate-100 px-1 py-0.2 rounded font-mono">
<code className="text-secondary bg-sunken px-1 py-0.2 rounded font-mono">
author:"Althaus" AND year:2020-2023
</code>
</span>
@@ -96,7 +96,7 @@ export function MetadataSyncCard({
</div>
<div className="space-y-2">
<label className="text-xs font-bold text-slate-700 block">
<label className="text-xs font-bold text-secondary block">
</label>
<div className="flex gap-2">
@@ -112,7 +112,7 @@ export function MetadataSyncCard({
onClick={() => setSource(src.id as 'all' | 'ads' | 'arxiv')}
className={`flex-1 py-2 rounded-md text-xs font-bold border transition-all cursor-pointer ${
source === src.id
? 'bg-slate-100 border-slate-350 text-slate-855 shadow-2xs'
? 'bg-sunken border-subtle text-content shadow-sm'
: 'btn-console btn-console-secondary'
}`}
>
@@ -125,13 +125,13 @@ export function MetadataSyncCard({
{/* 动态表单生成器 */}
{showBuilder && (
<div className="p-4 rounded-md bg-slate-50 border border-slate-200 space-y-3.5 transition-all">
<div className="text-xs font-bold text-slate-700 flex justify-between items-center select-none">
<div className="p-4 rounded-md bg-sunken space-y-3.5 transition-all">
<div className="text-xs font-bold text-secondary flex justify-between items-center select-none">
<span></span>
<button
type="button"
onClick={handleAddRule}
className="text-xs text-slate-655 hover:text-slate-855 font-bold cursor-pointer"
className="text-xs text-secondary hover:text-content font-bold cursor-pointer"
>
+
</button>
@@ -152,7 +152,7 @@ export function MetadataSyncCard({
]}
/>
) : (
<div className="w-24 text-center text-xs text-slate-500 font-semibold select-none">
<div className="w-24 text-center text-xs text-secondary font-semibold select-none">
</div>
)}
@@ -183,14 +183,14 @@ export function MetadataSyncCard({
? '例如: Althaus'
: '请输入检索词...'
}
className="flex-1 px-3 py-1.5 rounded-md bg-white border border-slate-300 text-slate-900 placeholder-slate-450 focus:outline-none focus:border-blueprint text-xs font-medium"
className="flex-1 px-3 py-1.5 rounded-md bg-surface border border-default text-content placeholder-tertiary focus:outline-none focus:border-blueprint text-xs font-medium"
/>
{rules.length > 1 && (
<button
type="button"
onClick={() => handleRemoveRule(idx)}
className="text-slate-500 hover:text-slate-800 text-xs font-bold px-2 py-1.5 cursor-pointer"
className="text-secondary hover:text-content text-xs font-bold px-2 py-1.5 cursor-pointer"
>
</button>
@@ -201,11 +201,11 @@ export function MetadataSyncCard({
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 pt-2 border-t border-slate-200">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 pt-2 border-t border-subtle">
<div className="space-y-2">
<label className="text-xs font-bold text-slate-700 block flex items-center justify-between">
<label className="text-xs font-bold text-secondary block flex items-center justify-between">
<span></span>
<span className="text-[11px] text-slate-450 font-normal">
<span className="text-[11px] text-tertiary font-normal">
API
</span>
</label>
@@ -216,7 +216,7 @@ export function MetadataSyncCard({
onChange={(e) =>
setLimit(Math.max(1, parseInt(e.target.value) || 0))
}
className="w-full px-4 py-2 rounded-md bg-slate-50 border border-slate-300 text-slate-900 focus:outline-none focus:border-blueprint transition-all text-xs font-medium"
className="w-full px-4 py-2 rounded-md bg-sunken text-content focus:outline-none focus:border-blueprint transition-all text-xs font-medium"
/>
</div>
@@ -225,7 +225,7 @@ export function MetadataSyncCard({
type="button"
disabled={status.active || estimating}
onClick={handleEstimate}
className="flex-1 py-2 rounded-md bg-white border border-slate-300 hover:bg-slate-50 text-slate-750 text-xs font-bold flex items-center justify-center gap-2 transition-all disabled:opacity-40 cursor-pointer"
className="flex-1 py-2 rounded-md bg-surface border border-default hover:bg-sunken text-secondary text-xs font-bold flex items-center justify-center gap-2 transition-all disabled:opacity-40 cursor-pointer"
>
{estimating ? (
<Loader className="w-3.5 h-3.5 animate-spin" />
@@ -248,11 +248,11 @@ export function MetadataSyncCard({
{/* 预估结果 */}
{estimatedCount !== null && !status.active && (
<div className="p-4 rounded-md bg-slate-50 border border-slate-200 flex gap-3 text-xs text-slate-800 items-center select-none animate-in fade-in duration-250">
<Info className="w-4 h-4 shrink-0 text-slate-500" />
<div className="p-4 rounded-md bg-sunken flex gap-3 text-xs text-content items-center select-none animate-in fade-in duration-250">
<Info className="w-4 h-4 shrink-0 text-secondary" />
<div>
{' '}
<strong className="text-slate-900 font-bold">
<strong className="text-content font-bold">
{estimatedCount}
</strong>{' '}
@@ -266,10 +266,10 @@ export function MetadataSyncCard({
{/* 实时同步进度 */}
{(status.active || status.synced > 0) && (
<div className="console-panel p-6 rounded-lg border border-slate-200 bg-white space-y-4 shadow-sm">
<div className="console-panel p-6 rounded-lg bg-surface space-y-4 shadow-sm">
<div className="flex justify-between items-center">
<div>
<h3 className="text-xs font-bold text-slate-850 flex items-center gap-2">
<h3 className="text-xs font-bold text-content flex items-center gap-2">
{status.active ? (
<>
<Loader className="w-4 h-4 text-blueprint animate-spin" />
@@ -282,9 +282,9 @@ export function MetadataSyncCard({
</>
)}
</h3>
<p className="text-slate-500 text-[11px] mt-1.5 font-semibold leading-relaxed select-all">
<p className="text-secondary text-[11px] mt-1.5 font-semibold leading-relaxed select-all">
:{' '}
<code className="bg-slate-100 px-1 py-0.5 rounded text-slate-700 font-mono">
<code className="bg-sunken px-1 py-0.5 rounded text-secondary font-mono">
{status.query}
</code>{' '}
:{' '}
@@ -295,12 +295,12 @@ export function MetadataSyncCard({
: 'arXiv'}
</p>
</div>
<span className="text-xs font-bold text-slate-800">
<span className="text-xs font-bold text-content">
{status.synced} / {status.total}
</span>
</div>
<div className="w-full h-3 rounded-md bg-slate-100 overflow-hidden border border-slate-200 select-none">
<div className="w-full h-3 rounded-md bg-sunken overflow-hidden select-none">
<div
className="h-full bg-blueprint transition-all duration-500 shadow-sm"
style={{ width: `${percent}%` }}
@@ -309,7 +309,7 @@ export function MetadataSyncCard({
{status.active &&
(status.source === 'all' || status.source === 'arxiv') ? (
<div className="p-3 rounded-md bg-slate-50 border border-slate-200 border-l-4 border-l-amber-500 text-xs text-slate-700 flex items-start gap-1.5 select-none">
<div className="p-3 rounded-md bg-sunken border-l-4 border-l-amber-500 text-xs text-secondary flex items-start gap-1.5 select-none">
<Lightbulb className="w-4 h-4 shrink-0 mt-0.5" />
<span>
arXiv 3000
+34 -15
View File
@@ -18,7 +18,6 @@ import type {
UnifiedSearchRequest,
UnifiedSearchResult,
ResolvedTarget,
ProductSpec,
} from '../components/observation/constants';
interface UseObservationProps {
@@ -345,18 +344,36 @@ export function useObservation({ isAuthenticated }: UseObservationProps) {
*/
const downloadUnifiedSelected = useCallback(async () => {
if (unifiedSelected.size === 0 || !unifiedResult) return [];
// 收集 key → {source, product, subtype, source_id}
// 收集 key → {source, product, subtype, release, version, ids}
const groups = new Map<
string,
{ spec: [string, ProductSpec]; ids: string[] }
{
source: string;
product: string;
subtype?: string;
release?: string;
version?: string;
ids: string[];
}
>();
for (const key of unifiedSelected) {
const [source, product, subtype, ...idParts] = key.split('#');
const source_id = idParts.join('#'); // source_id 理论上不含 #,但保守处理
const groupKey = `${source}#${product}#${subtype}`;
const parts = key.split('#');
if (parts.length < 6) continue;
const source = parts[0];
const product = parts[1];
const subtype = parts[2] || undefined;
const release = parts[3] || undefined;
const version = parts[4] || undefined;
const source_id = parts.slice(5).join('#');
const groupKey = `${source}#${product}#${subtype ?? ''}#${release ?? ''}#${version ?? ''}`;
if (!groups.has(groupKey)) {
groups.set(groupKey, {
spec: [source, { product, subtype: subtype || undefined }],
source,
product,
subtype,
release,
version,
ids: [],
});
}
@@ -367,28 +384,30 @@ export function useObservation({ isAuthenticated }: UseObservationProps) {
setDownloading(true);
setDownloadError(null);
try {
for (const { spec, ids } of groups.values()) {
for (const group of groups.values()) {
try {
const res = await axios.post<ObservationBatchResult>(
'/api/observation/download',
{
source: spec[0],
product: spec[1].product,
subtype: spec[1].subtype || undefined,
source: group.source,
product: group.product,
subtype: group.subtype,
release: group.release,
version: group.version,
force: false,
mode: 'identifiers',
source_ids: ids,
source_ids: group.ids,
}
);
results.push(res.data);
} catch (e: unknown) {
// 单组失败不中断,记录为 failures 占位 batch
results.push({
source: spec[0],
product: spec[1],
source: group.source,
product: { product: group.product, subtype: group.subtype },
matched_count: 0,
products: [],
failures: ids.map((id) => ({
failures: group.ids.map((id) => ({
source_label: id,
error: extractErrorMessage(e, '下载失败'),
})),
+40 -4
View File
@@ -6,8 +6,10 @@ import {
routeToolCall,
routeToolResult,
routeTextDelta,
setGlobalTools,
getToolMeta,
} from '../components/agent';
import type { TimelineItem } from '../components/agent';
import type { TimelineItem, AgentToolMeta } from '../components/agent';
import type { SessionSummary, MessageRecord } from '../types';
import { extractErrorMessage } from '../utils/apiError';
@@ -198,7 +200,7 @@ export function useResearchAgent({
(async () => {
try {
setLoadingSessions(true);
const [sessionsRes, modesRes] = await Promise.all([
const [sessionsRes, modesRes, toolsRes] = await Promise.all([
axios.get<SessionSummary[]>('/api/chat/sessions'),
axios.get<AgentMode[]>('/api/chat/modes').catch(() => ({
data: [
@@ -222,10 +224,14 @@ export function useResearchAgent({
},
] as AgentMode[],
})),
axios.get<AgentToolMeta[]>('/api/chat/tools').catch(() => ({
data: [] as AgentToolMeta[],
})),
]);
if (!active) return;
setSessions(sessionsRes.data);
setAgentModes(modesRes.data);
setGlobalTools(toolsRes.data);
} catch (e) {
console.error('初始化 Agent 数据失败:', e);
} finally {
@@ -729,7 +735,9 @@ export function useResearchAgent({
event.step,
event.id,
event.name,
event.arguments
event.arguments,
event.is_internal,
event.display_name
),
};
});
@@ -749,7 +757,9 @@ export function useResearchAgent({
event.name,
event.output,
event.is_error,
event.metadata
event.metadata,
event.is_internal,
event.display_name
),
};
});
@@ -827,6 +837,25 @@ export function useResearchAgent({
const turns = groupMessagesIntoTurns(messages);
// Scan turns backwards to find the latest todo list from a successful todo_write call
const activeTodos = (() => {
for (let i = turns.length - 1; i >= 0; i--) {
const turn = turns[i];
const todoCall = turn.timeline.find(
(item) => item.type === 'tool_call' && item.name === 'todo_write'
);
if (
todoCall &&
todoCall.type === 'tool_call' &&
todoCall.arguments &&
Array.isArray(todoCall.arguments.todos)
) {
return todoCall.arguments.todos;
}
}
return [];
})();
const toggleThought = (key: string) =>
setExpandedThoughts((prev) => ({ ...prev, [key]: !prev[key] }));
const toggleArgs = (tcId: string) =>
@@ -891,6 +920,7 @@ export function useResearchAgent({
handleStop,
handleSend,
turns,
activeTodos,
toggleThought,
toggleArgs,
toggleResult,
@@ -981,12 +1011,15 @@ function groupMessagesIntoTurns(messages: MessageRecord[]): ProcessedTurn[] {
} catch {
/* ignore */
}
const meta = getToolMeta(tc.function.name);
turn.timeline.push({
type: 'tool_call',
step: stepNum,
id: tc.id,
name: tc.function.name,
display_name: meta?.display_name || tc.function.name,
arguments: parsedArgs,
is_internal: meta?.is_internal || false,
});
}
} else if (hasThought && hasContent) {
@@ -1081,12 +1114,15 @@ function groupMessagesIntoTurns(messages: MessageRecord[]): ProcessedTurn[] {
} catch {
/* ignore */
}
const meta = getToolMeta(`[sub] ${tc.function.name}`);
children.push({
type: 'tool_call',
step: stepNum,
id: tc.id,
name: tc.function.name,
display_name: meta?.display_name || tc.function.name,
arguments: parsedArgs,
is_internal: meta?.is_internal || false,
});
}
} else if (hasThought && hasContent) {
+69
View File
@@ -0,0 +1,69 @@
// dashboard/src/hooks/useTheme.ts
//
// 浅色 / 暗色双主题管理。
// - main.tsx 已在渲染前根据 localStorage('astro_theme') / 系统偏好设置 .dark 类(防 FOUC)。
// - 本 hook 同步 React 状态与 DOM 类,并提供切换/持久化。
import { useCallback, useEffect, useState } from 'react';
export type Theme = 'light' | 'dark';
const STORAGE_KEY = 'astro_theme';
function readInitialTheme(): Theme {
if (typeof document !== 'undefined') {
return document.documentElement.classList.contains('dark')
? 'dark'
: 'light';
}
return 'light';
}
export function useTheme() {
const [theme, setTheme] = useState<Theme>(readInitialTheme);
// 写入 DOM + localStorage
const applyTheme = useCallback((next: Theme) => {
const root = document.documentElement;
root.classList.toggle('dark', next === 'dark');
try {
localStorage.setItem(STORAGE_KEY, next);
} catch {
/* localStorage 不可用时静默 */
}
setTheme(next);
}, []);
const toggleTheme = useCallback(() => {
setTheme((prev) => {
const next: Theme = prev === 'dark' ? 'light' : 'dark';
const root = document.documentElement;
root.classList.toggle('dark', next === 'dark');
try {
localStorage.setItem(STORAGE_KEY, next);
} catch {
/* 静默 */
}
return next;
});
}, []);
// 跟随系统偏好变化(仅当用户未显式选择时)
useEffect(() => {
const mql = window.matchMedia('(prefers-color-scheme: dark)');
const handler = (e: MediaQueryListEvent) => {
let stored: string | null = null;
try {
stored = localStorage.getItem(STORAGE_KEY);
} catch {
/* 静默 */
}
if (!stored) {
document.documentElement.classList.toggle('dark', e.matches);
setTheme(e.matches ? 'dark' : 'light');
}
};
mql.addEventListener('change', handler);
return () => mql.removeEventListener('change', handler);
}, []);
return { theme, setTheme: applyTheme, toggleTheme };
}
+312 -99
View File
@@ -2,215 +2,429 @@
@import 'tailwindcss';
@plugin "@tailwindcss/typography";
@theme {
--color-main: var(--text-main);
--color-muted: var(--text-muted);
--color-card: var(--bg-card);
--color-sidebar: var(--bg-sidebar);
--color-blueprint: var(--accent-blueprint);
--color-star: var(--accent-star);
--color-precision: var(--border-precision);
/* 暗色变体跟随 .dark 类(手动切换),而非仅 prefers-color-scheme */
@custom-variant dark (&:where(.dark, .dark *));
--font-mono: var(--font-mono);
}
/*
* 设计令牌三层架构primitive semantic component
* :root 浅色语义层暖纸张学术风
* .dark 暗色语义层覆盖深空天文风 · Night Indigo
* @theme 将语义令牌桥接为 Tailwind 工具类
* 旧令牌名--accent-blueprint 作为别名保留向后兼容
* */
:root {
color-scheme: light;
font-family:
'Inter',
system-ui,
-apple-system,
sans-serif;
color-scheme: light;
/* 极极简白蓝学术科技风配色 - 升级为暖调纸张学术护眼色 */
--bg-main: #faf8f5; /* 学术纸张底色 */
--bg-card: #ffffff;
--bg-sidebar: #0f2540; /* 保持深色侧边栏作为界面骨架结构 */
--text-main: #1e293b; /* 优雅深石板灰,对比度更佳 */
--text-muted: #475569; /* 中度石板灰,提高可读性 */
/* ── 浅色语义层:暖纸张学术(统一暖中性 stone 色温,消除暖底+冷蓝灰线的割裂)── */
--bg-app: #faf9f6; /* 暖白纸底 */
--bg-surface: #ffffff; /* 卡片/面板(纯白,与暖底仅微差) */
--bg-elevated: #ffffff; /* 悬浮面板/弹层 */
--bg-sunken: #f4f2ed; /* 凹陷输入框/次级容器(暖浅灰) */
--accent-blueprint: #0369a1; /* 低饱和度学术蓝 */
--text-primary: #292524; /* 暖墨黑(stone-800,去蓝调) */
--text-secondary: #57534e; /* 暖深灰(stone-600 */
--text-tertiary: #a8a29e; /* 暖中灰(stone-400,占位/禁用) */
--border-default: #4e4b45; /* 卡片/分隔线:调浅,弱化描边感 */
--border-subtle: #f5f2ec; /* 更轻的暖分割线(近乎隐形) */
--border-strong: #ddd7cc; /* 暖灰强调线(hover/激活) */
--accent-primary: #0369a1; /* 学术蓝 blueprint */
--accent-primary-hover: #0d5988;
--accent-on-primary: #ffffff;
--accent-star: #b45309; /* 经典低饱和度琥珀橙 */
--border-precision: #e2e8f0; /* 极细轻量级分割线 */
--success: #047857;
--warning: #b45309;
--danger: #dc2626;
/* 装饰光斑(登录页) */
--blob-a: rgba(186, 230, 253, 0.35); /* sky-200 */
--blob-b: rgba(199, 210, 254, 0.3); /* indigo-200 */
/* 引用星系 canvas 语义色(JS 读取) */
--canvas-active: #0369a1;
--canvas-inactive: #57534e;
--canvas-reference: #b45309;
--canvas-citation: #0e7490;
--canvas-link: rgba(120, 113, 108, 0.22);
--canvas-ring: rgba(120, 113, 108, 0.1);
--canvas-pulse: rgba(3, 105, 161, 0.04);
--canvas-label: #57534e;
--canvas-label-hover: #0369a1;
/* 光谱图 SVG 语义色 */
--plot-grid: #e7e3da;
--plot-axis: #78716c;
--plot-axis-strong: #a8a29e;
--plot-tooltip-bg: #292524;
--plot-tooltip-fg: #faf9f6;
--font-mono: 'JetBrains Mono', monospace;
/* ── 旧令牌名别名(向后兼容,使现有 text-blueprint/bg-card 等零改动适配双主题)── */
--bg-main: var(--bg-app);
--bg-card: var(--bg-surface);
--bg-sidebar: var(--bg-surface);
--text-main: var(--text-primary);
--text-muted: var(--text-secondary);
--border-precision: var(--border-default);
--accent-blueprint: var(--accent-primary);
--accent-on-blueprint: var(--accent-on-primary);
}
.dark {
color-scheme: dark;
/* ── 暗色语义层:深空天文 · Night Indigo(取自 UI/UX 技能推荐)── */
--bg-app: #0a0f1e; /* 深空底色 */
--bg-surface: #131a2f; /* 卡片(深靛) */
--bg-elevated: #1b2440; /* 悬浮面板/弹层 */
--bg-sunken: #0d1426; /* 凹陷输入 */
--text-primary: #e8edf5; /* 柔和高对比白(去纯白刺眼) */
--text-secondary: #8b98b3; /* 中性靛灰 */
--text-tertiary: #5e6b85; /* 暗灰蓝 */
/* 边框压暗至极低不透明度,隐入深空底,避免"白色描边"感 */
--border-default: rgba(207, 211, 225, 0.699);
--border-subtle: rgba(139, 152, 179, 0.06);
--border-strong: rgba(139, 152, 179, 0.18);
--accent-primary: #38bdf8; /* 亮天蓝,暗色下高对比 */
--accent-primary-hover: #0ea5e9;
--accent-on-primary: #0a0f1e; /* 深底文字 */
--accent-star: #f59e0b; /* 提亮的琥珀 */
--success: #34d399;
--warning: #fbbf24;
--danger: #f87171;
--blob-a: rgba(56, 189, 248, 0.12);
--blob-b: rgba(99, 102, 241, 0.12);
--canvas-active: #38bdf8;
--canvas-inactive: #5e6b85;
--canvas-reference: #f59e0b;
--canvas-citation: #22d3ee;
--canvas-link: rgba(139, 152, 179, 0.14);
--canvas-ring: rgba(139, 152, 179, 0.07);
--canvas-pulse: rgba(56, 189, 248, 0.05);
--canvas-label: #8b98b3;
--canvas-label-hover: #7dd3fc;
--plot-grid: rgba(139, 152, 179, 0.12);
--plot-axis: #8b98b3;
--plot-axis-strong: #5e6b85;
--plot-tooltip-bg: #1b2440;
--plot-tooltip-fg: #e8edf5;
}
/*
* @theme语义令牌 Tailwind 工具类桥接
* 可直接写 bg-surface / text-content / text-secondary / border-default / text-danger
* */
@theme {
--color-surface: var(--bg-surface);
--color-elevated: var(--bg-elevated);
--color-sunken: var(--bg-sunken);
--color-app: var(--bg-app);
--color-content: var(--text-primary);
--color-secondary: var(--text-secondary);
--color-tertiary: var(--text-tertiary);
--color-border-default: var(--border-default);
--color-border-subtle: var(--border-subtle);
--color-border-strong: var(--border-strong);
--color-blueprint: var(--accent-primary);
--color-star: var(--accent-star);
--color-success: var(--success);
--color-warning: var(--warning);
--color-danger: var(--danger);
/* 旧桥接(保留,向后兼容) */
--color-main: var(--text-main);
--color-muted: var(--text-muted);
--color-card: var(--bg-card);
--color-sidebar: var(--bg-sidebar);
--color-precision: var(--border-precision);
--font-mono: var(--font-mono);
}
/* 自定义 border 实用工具类以匹配 components 里的直接引用 */
@utility border-default {
border-color: var(--border-default);
}
@utility border-subtle {
border-color: var(--border-subtle);
}
@utility border-strong {
border-color: var(--border-strong);
}
/*
* 基础排版与全局滚动条
* */
body {
margin: 0;
background-color: var(--bg-main);
color: var(--text-main);
background-color: var(--bg-app);
color: var(--text-primary);
min-height: 100vh;
overflow: hidden;
position: relative;
}
/* Custom premium scrollbar styling for light mode */
/* 平滑主题切换过渡 */
body,
.btn-console,
.select-console {
transition:
background-color 0.2s ease,
border-color 0.2s ease,
color 0.2s ease;
}
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: #f4f6f9;
background: transparent;
}
::-webkit-scrollbar-thumb {
background: #cbd5e1;
background: var(--border-strong);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
background: var(--text-tertiary);
}
/* Premium clean panel cards */
/*
* 组件类全部令牌化自动适配双主题
* */
/* 侧边栏右侧分隔:用极淡阴影代替硬边框,避免"竖黑线"感 */
.sidebar-seam {
box-shadow: 1px 0 0 0 var(--border-subtle);
}
/* 精致学术卡片面板(微弱暖灰边框,配合石色双层阴影,支持高阶悬停微动与暗色顶部高光) */
.console-panel {
background: var(--bg-card);
border: 1px solid var(--border-precision);
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05); /* 弱化阴影,更显扁平学术感 */
background: var(--bg-surface);
border: 1px solid var(--border-subtle);
box-shadow:
0 1px 3px 0 rgba(120, 113, 108, 0.04),
0 4px 12px 0 rgba(120, 113, 108, 0.03);
transition:
background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
border-color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1),
transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.dark .console-panel {
border-color: var(--border-subtle);
box-shadow:
0 4px 20px 0 rgba(0, 0, 0, 0.4),
inset 0 1px 0 0 rgba(255, 255, 255, 0.04);
}
/* 悬停微动交互:针对可点击卡片或指定交互面板 */
.console-panel[class*='cursor-pointer']:hover,
.console-panel-interactive:hover {
transform: translateY(-2px);
border-color: var(--border-strong);
box-shadow:
0 6px 16px -2px rgba(120, 113, 108, 0.08),
0 12px 24px -4px rgba(120, 113, 108, 0.06);
}
.dark .console-panel[class*='cursor-pointer']:hover,
.dark .console-panel-interactive:hover {
border-color: var(--border-strong);
box-shadow:
0 12px 28px -4px rgba(0, 0, 0, 0.5),
0 8px 12px -6px rgba(0, 0, 0, 0.5),
inset 0 1px 0 0 rgba(255, 255, 255, 0.08);
}
.console-panel-active {
border-color: var(--accent-blueprint);
border-color: var(--accent-primary);
box-shadow:
0 0 0 1px var(--accent-blueprint),
0 1px 3px 0 rgba(0, 0, 0, 0.05);
0 0 0 1px var(--accent-primary),
0 4px 12px 0 rgba(3, 105, 161, 0.08);
}
.dark .console-panel-active {
border-color: var(--accent-primary);
box-shadow:
0 0 0 1px var(--accent-primary),
0 4px 24px 0 rgba(0, 0, 0, 0.45),
inset 0 1px 0 0 rgba(255, 255, 255, 0.06);
}
/* High contrast clean console button */
/* 按钮:默认 */
.btn-console {
background: #ffffff;
border: 1px solid var(--border-precision);
color: var(--text-main);
background: var(--bg-surface);
border: 1px solid var(--border-default);
color: var(--text-primary);
font-weight: 500;
transition: all 0.2s ease;
}
.btn-console:hover:not(:disabled) {
background: #f8fafc;
border-color: #94a3b8;
color: var(--text-main);
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
background: var(--bg-sunken);
border-color: var(--border-strong);
color: var(--text-primary);
}
/* 按钮:主要 */
.btn-console-primary {
background: var(--accent-blueprint);
border: 1px solid var(--accent-blueprint);
color: #ffffff;
background: var(--accent-primary);
border: 1px solid var(--accent-primary);
color: var(--accent-on-primary);
}
.btn-console-primary:hover:not(:disabled) {
background: #0d5988;
border-color: #0d5988;
color: #ffffff;
box-shadow: 0 2px 4px 0 rgba(16, 107, 163, 0.2);
background: var(--accent-primary-hover);
border-color: var(--accent-primary-hover);
color: var(--accent-on-primary);
box-shadow: 0 2px 4px 0
color-mix(in srgb, var(--accent-primary) 25%, transparent);
}
/* 按钮:次要 */
.btn-console-secondary {
background: #f1f5f9;
border: 1px solid var(--border-precision);
color: var(--text-muted);
background: var(--bg-sunken);
border: 1px solid var(--border-default);
color: var(--text-secondary);
}
.btn-console-secondary:hover:not(:disabled) {
background: #e2e8f0;
color: var(--text-main);
background: var(--border-default);
color: var(--text-primary);
}
/* Premium clean console select dropdown styling */
/* 原生 select(已基本被 CustomSelect 取代,保留令牌化) */
.select-console {
display: inline-block;
background-color: #ffffff;
border: 1px solid var(--border-precision);
border-radius: 0.5rem; /* 8px */
padding: 0.5rem 1.75rem 0.5rem 0.625rem; /* padding-right leaves space for custom arrow */
color: var(--text-main);
font-size: 0.75rem; /* text-xs */
background-color: var(--bg-surface);
border: 1px solid var(--border-default);
border-radius: 0.5rem;
padding: 0.5rem 1.75rem 0.5rem 0.625rem;
color: var(--text-primary);
font-size: 0.75rem;
font-weight: 500;
cursor: pointer;
outline: none;
transition: all 0.2s ease;
appearance: none;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%2364748b' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
background-position: right 0.5rem center;
background-repeat: no-repeat;
background-size: 1.25rem;
}
.select-console:hover:not(:disabled) {
border-color: #94a3b8;
color: var(--text-main);
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
border-color: var(--border-strong);
color: var(--text-primary);
}
.select-console:focus {
border-color: var(--accent-blueprint);
box-shadow: 0 0 0 1px var(--accent-blueprint);
border-color: var(--accent-primary);
box-shadow: 0 0 0 1px var(--accent-primary);
}
.select-console:disabled {
background-color: #f1f5f9;
color: #94a3b8;
background-color: var(--bg-sunken);
color: var(--text-tertiary);
cursor: not-allowed;
}
/* FTS search result highlights */
/* FTS 搜索结果高亮 */
mark {
background-color: rgba(254, 240, 138, 0.7); /* translucent yellow bg */
color: #854d0e; /* text-yellow-800 */
background-color: color-mix(in srgb, var(--warning) 22%, transparent);
color: var(--text-primary);
padding-left: 0.125rem;
padding-right: 0.125rem;
border-radius: 0.125rem;
font-weight: 600;
}
/* LaTeX-style academic publishing typography overrides */
/*
* LaTeX 学术排版覆盖prose 表格/引用/公式
* */
.prose {
font-family:
'Inter',
system-ui,
-apple-system,
sans-serif;
color: var(--text-main);
color: var(--text-primary);
}
/* Academic Three-Line Table (LaTeX booktabs style) */
/* 统一代码块与行内代码的学术控制台主题配色 */
.prose pre {
background-color: var(--bg-sunken) !important;
color: var(--text-secondary) !important;
border: 1px solid var(--border-subtle) !important;
border-radius: 0.375rem;
padding: 0.75rem 1rem !important;
margin: 0.75rem 0 !important;
}
.prose code {
color: var(--accent-primary) !important;
font-family: var(--font-mono) !important;
font-size: 0.85em !important;
font-weight: 600 !important;
}
/* 去掉 Tailwind Typography 默认加在 code 前后的反引号 */
.prose code::before,
.prose code::after {
content: '' !important;
}
.prose pre code {
background-color: transparent !important;
color: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
padding: 0 !important;
border: none !important;
}
/* 学术三线表(LaTeX booktabs */
.prose table {
width: 100% !important;
border-collapse: collapse !important;
border-top: 2px solid #334155 !important; /* thick top rule */
border-bottom: 2px solid #334155 !important; /* thick bottom rule */
border-top: 2px solid var(--border-strong) !important;
border-bottom: 2px solid var(--border-strong) !important;
margin-top: 1.5em !important;
margin-bottom: 1.5em !important;
font-size: 0.85em !important;
}
.prose thead {
border-bottom: 1.2px solid #475569 !important; /* mid rule */
border-bottom: 1.2px solid var(--text-tertiary) !important;
}
.prose thead th {
font-weight: 700 !important;
color: #1e293b !important;
color: var(--text-primary) !important;
padding: 0.5em 0.75em !important;
text-align: left !important;
border: none !important;
}
.prose tbody tr {
border-bottom: 0.5px solid #e2e8f0 !important; /* light horizontal line */
border-bottom: 0.5px solid var(--border-subtle) !important;
background: transparent !important;
}
.prose tbody tr:last-child {
border-bottom: none !important; /* last row doesn't have thin border because of the thick table bottom rule */
border-bottom: none !important;
}
.prose tbody td {
padding: 0.5em 0.75em !important;
color: #334155 !important;
color: var(--text-secondary) !important;
border: none !important;
}
/* Remove vertical borders and striping that are generated by default */
.prose table,
.prose th,
.prose td {
@@ -218,25 +432,24 @@ mark {
border-right: none !important;
}
/* LaTeX Blockquote */
/* LaTeX 引用块 */
.prose blockquote {
font-style: italic !important;
color: #475569 !important;
border-left: 3px solid #64748b !important; /* solid charcoal left line */
background-color: #fdfcf9 !important; /* warmer background for blockquotes */
color: var(--text-secondary) !important;
border-left: 3px solid var(--text-tertiary) !important;
background-color: var(--bg-sunken) !important;
padding: 0.75em 1.25em !important;
margin: 1.25em 0 !important;
border-radius: 2px !important;
font-weight: 500 !important;
quotes: none !important;
}
.prose blockquote p::before,
.prose blockquote p::after {
content: '' !important;
}
/* Formulas and Math spacing */
/* 公式间距 */
.prose .katex-display {
margin: 1.2em 0 !important;
padding: 0.5em !important;
+15
View File
@@ -3,6 +3,21 @@ import { createRoot } from 'react-dom/client';
import './index.css';
import App from './App.tsx';
// ── 防 FOUC:在任何 React 渲染前确定主题 ──
// localStorage 优先,其次跟随系统 prefers-color-scheme,默认浅色。
(() => {
try {
const stored = localStorage.getItem('astro_theme');
const prefersDark = window.matchMedia(
'(prefers-color-scheme: dark)'
).matches;
const isDark = stored ? stored === 'dark' : prefersDark;
document.documentElement.classList.toggle('dark', isDark);
} catch {
/* localStorage 不可用时静默回退到浅色 */
}
})();
// 注册 PWA Service Worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
+38 -38
View File
@@ -56,12 +56,12 @@ export function CitationPanel({
return (
<div className="w-full flex-1 flex flex-col space-y-4 min-h-0">
<div className="flex flex-col md:flex-row md:items-center justify-between border-b border-slate-200 pb-3 gap-3">
<div className="flex flex-col md:flex-row md:items-center justify-between border-b border-subtle pb-3 gap-3">
<div>
<h2 className="text-sm font-bold tracking-wider text-slate-900 uppercase">
<h2 className="text-sm font-bold tracking-wider text-content uppercase">
</h2>
<p className="text-xs text-slate-500 mt-1">
<p className="text-xs text-secondary mt-1">
visual -
</p>
</div>
@@ -88,12 +88,12 @@ export function CitationPanel({
className="fixed inset-0 z-40"
onClick={() => setShowSwitchMenu(false)}
/>
<div className="absolute right-0 mt-1.5 w-72 rounded-md bg-white border border-slate-200 shadow-md py-1 z-50 text-xs max-h-96 overflow-y-auto scrollbar-thin">
<div className="absolute right-0 mt-1.5 w-72 rounded-md bg-surface shadow-md py-1 z-50 text-xs max-h-96 overflow-y-auto scrollbar-thin">
{/* 最近文献 */}
{recentlySelected.length > 0 && (
<div className="border-b border-slate-100 pb-1.5 mb-1.5">
<div className="px-3 py-1 text-[10px] font-bold text-slate-400 uppercase flex items-center gap-1">
<History className="w-3.5 h-3.5 text-slate-400" />
<div className="border-b border-subtle pb-1.5 mb-1.5">
<div className="px-3 py-1 text-[10px] font-bold text-tertiary uppercase flex items-center gap-1">
<History className="w-3.5 h-3.5 text-tertiary" />
<span></span>
</div>
{recentlySelected.map((paper) => (
@@ -103,16 +103,16 @@ export function CitationPanel({
onSwitchPaper(paper);
setShowSwitchMenu(false);
}}
className={`w-full px-3 py-2 text-left hover:bg-slate-50 transition-colors flex flex-col gap-0.5 border-l-2 ${
className={`w-full px-3 py-2 text-left hover:bg-sunken transition-colors flex flex-col gap-0.5 border-l-2 ${
paper.bibcode === selectedPaper.bibcode
? 'border-blueprint bg-blueprint/5 text-blueprint font-bold'
: 'border-transparent text-slate-700 font-medium'
: 'border-transparent text-secondary font-medium'
}`}
>
<span className="truncate block leading-tight text-left">
{paper.title}
</span>
<span className="text-[9px] text-slate-400 font-semibold font-mono text-left">
<span className="text-[9px] text-tertiary font-semibold font-mono text-left">
{paper.bibcode}
</span>
</button>
@@ -122,12 +122,12 @@ export function CitationPanel({
{/* 全部已下载文献 */}
<div>
<div className="px-3 py-1 text-[10px] font-bold text-slate-400 uppercase flex items-center gap-1">
<FileText className="w-3.5 h-3.5 text-slate-400" />
<div className="px-3 py-1 text-[10px] font-bold text-tertiary uppercase flex items-center gap-1">
<FileText className="w-3.5 h-3.5 text-tertiary" />
<span></span>
</div>
{library.filter((p) => p.is_downloaded).length === 0 ? (
<div className="px-3 py-2 text-slate-400 italic">
<div className="px-3 py-2 text-tertiary italic">
</div>
) : (
@@ -140,16 +140,16 @@ export function CitationPanel({
onSwitchPaper(paper);
setShowSwitchMenu(false);
}}
className={`w-full px-3 py-2 text-left hover:bg-slate-50 transition-colors flex flex-col gap-0.5 border-l-2 ${
className={`w-full px-3 py-2 text-left hover:bg-sunken transition-colors flex flex-col gap-0.5 border-l-2 ${
paper.bibcode === selectedPaper.bibcode
? 'border-blueprint bg-blueprint/5 text-blueprint font-bold'
: 'border-transparent text-slate-700 font-medium'
: 'border-transparent text-secondary font-medium'
}`}
>
<span className="truncate block leading-tight text-left">
{paper.title}
</span>
<span className="text-[9px] text-slate-400 font-semibold font-mono text-left">
<span className="text-[9px] text-tertiary font-semibold font-mono text-left">
{paper.bibcode}
</span>
</button>
@@ -161,8 +161,8 @@ export function CitationPanel({
)}
</div>
)}
<div className="flex items-center gap-2 border border-slate-200 bg-white px-3 py-1.5 rounded-md shadow-sm">
<span className="text-[11px] text-slate-500 font-bold">
<div className="flex items-center gap-2 bg-surface px-3 py-1.5 rounded-md shadow-sm">
<span className="text-[11px] text-secondary font-bold">
:
</span>
<input
@@ -172,7 +172,7 @@ export function CitationPanel({
step="5"
value={nodeLimit}
onChange={(e) => setNodeLimit(Number(e.target.value))}
className="w-20 h-1.5 bg-slate-200 rounded-lg appearance-none cursor-pointer accent-blueprint focus:outline-none"
className="w-20 h-1.5 bg-border-subtle rounded-lg appearance-none cursor-pointer accent-blueprint focus:outline-none"
/>
<span className="text-xs font-bold text-blueprint w-6 text-right">
{nodeLimit}
@@ -193,16 +193,16 @@ export function CitationPanel({
</div>
{loadingCitations ? (
<div className="console-panel rounded-lg flex-1 flex flex-col items-center justify-center text-slate-500 bg-white">
<div className="console-panel rounded-lg flex-1 flex flex-col items-center justify-center text-secondary bg-surface">
<Loader className="w-8 h-8 animate-spin text-blueprint mb-2" />
<p className="text-xs font-bold text-slate-600">
<p className="text-xs font-bold text-secondary">
...
</p>
</div>
) : citationNetwork ? (
<div className="flex-1 console-panel rounded-lg border border-slate-200 overflow-hidden relative flex flex-col md:flex-row bg-white min-h-0">
<div className="flex-1 console-panel rounded-lg overflow-hidden relative flex flex-col md:flex-row bg-surface min-h-0">
{/* 可视化画布 */}
<div className="flex-1 relative bg-slate-50/30 min-h-[350px] md:min-h-0">
<div className="flex-1 relative bg-sunken/60 min-h-[350px] md:min-h-0">
<CitationGalaxyCanvas
networks={citationHistory}
activeNetwork={citationNetwork}
@@ -210,27 +210,27 @@ export function CitationPanel({
onNodeClick={handleNodeClick}
/>
</div>
{/* 右侧关系详情面板 */}
<div className="w-full md:w-80 h-64 md:h-auto border-t md:border-t-0 md:border-l border-slate-200 bg-slate-50 p-6 space-y-6 overflow-y-auto scrollbar-thin">
{/* 右侧关系详情面板(靠背景色阶与图谱区分层,无需描边) */}
<div className="w-full md:w-80 h-64 md:h-auto bg-surface p-6 space-y-6 overflow-y-auto scrollbar-thin">
<div>
<span className="text-[10px] font-bold text-blueprint tracking-wider block mb-2">
</span>
<h4 className="text-xs font-bold text-slate-900 leading-relaxed font-sans">
<h4 className="text-xs font-bold text-content leading-relaxed font-sans">
{citationNetwork.title}
</h4>
<p className="text-[10px] text-slate-600 font-mono mt-2 bg-slate-200/50 border border-slate-300/60 px-2 py-1.5 rounded select-all truncate">
<p className="text-[10px] text-secondary font-mono mt-2 bg-border-subtle px-2 py-1.5 rounded select-all truncate">
{citationNetwork.bibcode}
</p>
</div>
<div className="border-t border-slate-200 pt-4">
<span className="text-xs font-bold text-slate-700 block mb-2">
<div className="border-t border-subtle pt-4">
<span className="text-xs font-bold text-secondary block mb-2">
( // 共 {citationNetwork.references.length} 篇)
</span>
<div className="space-y-1.5 max-h-48 overflow-y-auto scrollbar-thin pr-1">
{citationNetwork.references.length === 0 ? (
<span className="text-xs text-slate-400 italic">
<span className="text-xs text-tertiary italic">
</span>
) : (
@@ -238,7 +238,7 @@ export function CitationPanel({
<div
key={bib}
onClick={() => handleNodeClick(bib)}
className="text-[10px] text-slate-700 hover:text-blueprint font-mono py-1.5 px-2.5 rounded bg-white border border-slate-200 hover:border-blueprint/40 cursor-pointer transition-all truncate"
className="text-[10px] text-secondary hover:text-blueprint font-mono py-1.5 px-2.5 rounded bg-surface hover:border-blueprint/40 cursor-pointer transition-all truncate"
>
{bib}
</div>
@@ -247,13 +247,13 @@ export function CitationPanel({
</div>
</div>
<div className="border-t border-slate-200 pt-4">
<span className="text-xs font-bold text-slate-700 block mb-2">
<div className="border-t border-subtle pt-4">
<span className="text-xs font-bold text-secondary block mb-2">
( // 共 {citationNetwork.citations.length} 篇)
</span>
<div className="space-y-1.5 max-h-48 overflow-y-auto scrollbar-thin pr-1">
{citationNetwork.citations.length === 0 ? (
<span className="text-xs text-slate-400 italic">
<span className="text-xs text-tertiary italic">
</span>
) : (
@@ -261,7 +261,7 @@ export function CitationPanel({
<div
key={bib}
onClick={() => handleNodeClick(bib)}
className="text-[10px] text-slate-700 hover:text-blueprint font-mono py-1.5 px-2.5 rounded bg-white border border-slate-200 hover:border-blueprint/40 cursor-pointer transition-all truncate"
className="text-[10px] text-secondary hover:text-blueprint font-mono py-1.5 px-2.5 rounded bg-surface hover:border-blueprint/40 cursor-pointer transition-all truncate"
>
{bib}
</div>
@@ -272,9 +272,9 @@ export function CitationPanel({
</div>
</div>
) : (
<div className="console-panel rounded-lg flex-1 flex flex-col items-center justify-center text-slate-500 bg-white">
<GitFork className="w-12 h-12 mb-3 text-slate-300" />
<p className="text-xs font-bold text-slate-500">
<div className="console-panel rounded-lg flex-1 flex flex-col items-center justify-center text-secondary bg-surface">
<GitFork className="w-12 h-12 mb-3 text-tertiary" />
<p className="text-xs font-bold text-secondary">
</p>
</div>
+78 -66
View File
@@ -16,6 +16,7 @@ import type { StandardPaper } from '../types';
import { CustomSelect } from '../components/CustomSelect';
import { PaperCard } from '../components/PaperCard';
import type { TabId } from '../components/layout/Sidebar';
import { motion } from 'framer-motion';
interface LibraryPanelProps {
library: StandardPaper[];
@@ -197,12 +198,12 @@ export function LibraryPanel({
return (
<div className="w-full max-w-5xl mx-auto space-y-6">
<div className="flex items-center justify-between mb-4 border-b border-slate-200 pb-4 select-none">
<div className="flex items-center justify-between mb-4 border-b border-subtle pb-4 select-none">
<div>
<h2 className="text-sm font-bold tracking-wider text-slate-900 uppercase">
<h2 className="text-sm font-bold tracking-wider text-content uppercase">
</h2>
<p className="text-xs text-slate-500 mt-1">
<p className="text-xs text-secondary mt-1">
线
</p>
</div>
@@ -223,8 +224,8 @@ export function LibraryPanel({
<div
className={`px-4 py-2.5 rounded-md text-xs font-bold flex items-center gap-2 transition-all select-none ${
syncFeedback.type === 'success'
? 'bg-emerald-50 border border-emerald-200/60 text-emerald-850'
: 'bg-rose-50 border border-rose-200/60 text-rose-850'
? 'bg-emerald-50 dark:bg-emerald-500/10 border border-emerald-200/60 dark:border-emerald-500/30 text-success dark:text-emerald-300'
: 'bg-rose-50 dark:bg-rose-500/10 border border-rose-200/60 dark:border-rose-500/30 text-danger dark:text-rose-300'
}`}
>
{syncFeedback.type === 'success' ? (
@@ -238,15 +239,15 @@ export function LibraryPanel({
{/* 搜索、筛选与排序工具栏 */}
{(!isLibraryEmpty || !isNoFilters) && (
<div className="flex flex-col gap-3.5 bg-white p-4 rounded-lg border border-slate-200 shadow-sm text-xs font-semibold text-slate-700">
<div className="flex flex-col gap-3.5 bg-surface p-4 rounded-lg shadow-sm text-xs font-semibold text-secondary">
<div className="flex flex-col sm:flex-row gap-3">
{/* 本地检索 */}
<div className="flex-1 space-y-1.5">
<label className="block text-slate-500 font-bold">
<label className="block text-secondary font-bold">
</label>
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 w-3.5 h-3.5" />
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-tertiary w-3.5 h-3.5" />
<input
type="text"
value={searchTerm}
@@ -255,7 +256,7 @@ export function LibraryPanel({
setCurrentPage(1);
}}
placeholder="搜索文献标题、作者、摘要、Bibcode、arXiv ID 或 DOI..."
className="w-full pl-9 pr-8 py-2 rounded-md bg-slate-50 border border-slate-250 text-slate-900 placeholder-slate-400 focus:outline-none focus:border-blueprint focus:bg-white transition-all text-xs font-medium"
className="w-full pl-9 pr-8 py-2 rounded-md bg-sunken border border-default text-content placeholder-tertiary focus:outline-none focus:border-blueprint focus:bg-surface transition-all text-xs font-medium"
/>
{searchTerm && (
<button
@@ -264,7 +265,7 @@ export function LibraryPanel({
setSearchTerm('');
setCurrentPage(1);
}}
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 transition-all p-0.5 rounded-full hover:bg-slate-100 cursor-pointer flex items-center justify-center"
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-tertiary hover:text-secondary transition-all p-0.5 rounded-full hover:bg-sunken cursor-pointer flex items-center justify-center"
title="清空检索内容"
>
<X className="w-3 h-3" />
@@ -275,7 +276,7 @@ export function LibraryPanel({
{/* 状态过滤 */}
<div className="w-full sm:w-36 space-y-1.5 font-bold flex flex-col">
<label className="block text-slate-500 font-bold">
<label className="block text-secondary font-bold">
</label>
<CustomSelect
@@ -300,7 +301,7 @@ export function LibraryPanel({
{/* 文献类型筛选 */}
<div className="w-full sm:w-36 space-y-1.5 font-bold flex flex-col">
<label className="block text-slate-500 font-bold">
<label className="block text-secondary font-bold">
</label>
<CustomSelect
@@ -330,7 +331,7 @@ export function LibraryPanel({
{/* 排序方式 */}
<div className="w-full sm:w-36 space-y-1.5 font-bold flex flex-col">
<label className="block text-slate-500 font-bold">
<label className="block text-secondary font-bold">
</label>
<CustomSelect
@@ -357,7 +358,7 @@ export function LibraryPanel({
onClick={() => setShowAdvanced(!showAdvanced)}
className={`w-full sm:w-auto px-4 py-2 rounded-md border text-xs font-bold transition-all shrink-0 flex items-center justify-center gap-1.5 cursor-pointer ${
showAdvanced
? 'bg-slate-100 border-slate-350 text-slate-855'
? 'bg-sunken border-subtle text-content'
: 'btn-console btn-console-secondary'
}`}
>
@@ -369,10 +370,10 @@ export function LibraryPanel({
{/* 展开的元数据细化筛选字段 */}
{showAdvanced && (
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 pt-3.5 border-t border-slate-100 w-full transition-all">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 pt-3.5 border-t border-subtle w-full transition-all">
{/* 作者过滤 */}
<div className="space-y-1.5">
<label className="block text-slate-500 font-bold">
<label className="block text-secondary font-bold">
</label>
<input
@@ -383,13 +384,13 @@ export function LibraryPanel({
setCurrentPage(1);
}}
placeholder="如: Althaus..."
className="w-full px-3 py-2 rounded-md bg-slate-50 border border-slate-250 text-slate-900 placeholder-slate-400 focus:outline-none focus:border-blueprint focus:bg-white transition-all text-xs font-medium"
className="w-full px-3 py-2 rounded-md bg-sunken border border-default text-content placeholder-tertiary focus:outline-none focus:border-blueprint focus:bg-surface transition-all text-xs font-medium"
/>
</div>
{/* 年份过滤 */}
<div className="space-y-1.5">
<label className="block text-slate-500 font-bold">
<label className="block text-secondary font-bold">
</label>
<input
@@ -400,13 +401,13 @@ export function LibraryPanel({
setCurrentPage(1);
}}
placeholder="如: 2023..."
className="w-full px-3 py-2 rounded-md bg-slate-50 border border-slate-250 text-slate-900 placeholder-slate-400 focus:outline-none focus:border-blueprint focus:bg-white transition-all text-xs font-medium"
className="w-full px-3 py-2 rounded-md bg-sunken border border-default text-content placeholder-tertiary focus:outline-none focus:border-blueprint focus:bg-surface transition-all text-xs font-medium"
/>
</div>
{/* 期刊过滤 */}
<div className="space-y-1.5">
<label className="block text-slate-500 font-bold">
<label className="block text-secondary font-bold">
</label>
<input
@@ -417,7 +418,7 @@ export function LibraryPanel({
setCurrentPage(1);
}}
placeholder="如: ApJ 或 MNRAS..."
className="w-full px-3 py-2 rounded-md bg-slate-50 border border-slate-250 text-slate-900 placeholder-slate-450 focus:outline-none focus:border-blueprint focus:bg-white transition-all text-xs font-medium"
className="w-full px-3 py-2 rounded-md bg-sunken border border-default text-content placeholder-tertiary focus:outline-none focus:border-blueprint focus:bg-surface transition-all text-xs font-medium"
/>
</div>
</div>
@@ -426,47 +427,60 @@ export function LibraryPanel({
)}
{loading ? (
<div className="console-panel p-24 text-center border border-slate-200 bg-white rounded-lg select-none">
<div className="console-panel p-24 text-center bg-surface rounded-lg select-none">
<Loader2 className="w-10 h-10 animate-spin text-blueprint mx-auto mb-4" />
<p className="text-xs text-slate-500 font-bold">
<p className="text-xs text-secondary font-bold">
...
</p>
</div>
) : isLibraryEmpty ? (
<div className="console-panel p-16 rounded-lg text-center border-2 border-dashed border-slate-300">
<Library className="w-12 h-12 text-slate-400 mx-auto mb-4" />
<h3 className="font-bold text-slate-800 text-sm mb-2">
<div className="console-panel p-16 rounded-lg text-center border-2 border-dashed border-subtle relative overflow-hidden select-none">
<div
className="absolute inset-0 opacity-[0.03] dark:opacity-[0.05] pointer-events-none"
style={{
background:
'radial-gradient(circle at center, var(--color-blueprint) 0%, transparent 65%)',
}}
/>
<motion.div
animate={{ y: [0, -6, 0] }}
transition={{ repeat: Infinity, duration: 4, ease: 'easeInOut' }}
className="w-16 h-16 rounded-2xl bg-blueprint/10 dark:bg-blueprint/15 border border-blueprint/20 flex items-center justify-center text-blueprint mx-auto mb-5 relative z-10"
>
<Library className="w-8 h-8" />
</motion.div>
<h3 className="font-bold text-content text-sm mb-2 relative z-10">
</h3>
<p className="text-xs text-slate-500 mb-6 max-w-md mx-auto">
<p className="text-xs text-secondary mb-6 max-w-md mx-auto relative z-10 font-medium leading-relaxed">
线便
</p>
<button
onClick={() => setActiveTab('search')}
className="btn-console btn-console-primary px-6 py-2.5 rounded-md text-xs font-bold cursor-pointer"
className="btn-console btn-console-primary px-6 py-2.5 rounded-md text-xs font-bold cursor-pointer relative z-10 hover:scale-105 active:scale-95 transition-all"
>
</button>
</div>
) : library.length === 0 ? (
<div className="console-panel p-12 rounded-lg text-center border border-slate-200 bg-white">
<SlidersHorizontal className="w-10 h-10 text-slate-350 mx-auto mb-3" />
<h3 className="font-bold text-slate-700 text-xs mb-1.5">
<div className="console-panel p-12 rounded-lg text-center bg-surface">
<SlidersHorizontal className="w-10 h-10 text-tertiary mx-auto mb-3" />
<h3 className="font-bold text-secondary text-xs mb-1.5">
</h3>
<p className="text-xs text-slate-455 max-w-sm mx-auto mb-4">
<p className="text-xs text-tertiary max-w-sm mx-auto mb-4">
线
</p>
<button
onClick={handleResetFilters}
className="px-4 py-1.5 bg-slate-100 hover:bg-slate-200 text-slate-655 rounded-md text-xs font-bold transition-all cursor-pointer"
className="px-4 py-1.5 bg-sunken hover:bg-border-subtle text-secondary rounded-md text-xs font-bold transition-all cursor-pointer"
>
</button>
</div>
) : (
<div className="space-y-4">
<div className="flex justify-between items-center text-xs text-slate-500 font-bold px-1 select-none">
<div className="flex justify-between items-center text-xs text-secondary font-bold px-1 select-none">
<span> / {totalCount} </span>
<div className="flex items-center gap-2">
<span></span>
@@ -509,19 +523,19 @@ export function LibraryPanel({
}
className={`absolute top-0 right-0 px-2 py-0.5 text-[9px] font-bold border-b border-l rounded-bl ${
paper.has_vector
? 'bg-amber-50 text-amber-700 border-amber-200/60'
? 'bg-amber-50 dark:bg-amber-500/10 text-warning dark:text-amber-300 border-amber-200/60 dark:border-amber-500/30'
: paper.has_translation
? 'bg-emerald-50 text-emerald-850 border-emerald-200/60'
? 'bg-emerald-50 dark:bg-emerald-500/10 text-success dark:text-emerald-300 border-emerald-200/60 dark:border-emerald-500/30'
: paper.has_markdown
? 'bg-blue-50 text-blue-800 border-blue-200/60'
: paper.is_downloaded
? 'bg-slate-100 text-slate-800 border-slate-250'
? 'bg-sunken text-content border-subtle'
: paper.pdf_error === 'no_resource' &&
paper.html_error === 'no_resource'
? 'bg-slate-50 text-slate-550 border-slate-200 cursor-help'
? 'bg-sunken text-secondary border-subtle cursor-help'
: paper.pdf_error || paper.html_error
? 'bg-rose-50 text-rose-850 border-rose-200/60 cursor-help'
: 'bg-slate-50 text-slate-550 border-slate-100'
? 'bg-rose-50 dark:bg-rose-500/10 text-danger dark:text-rose-300 border-rose-200/60 dark:border-rose-500/30 cursor-help'
: 'bg-sunken text-secondary border-subtle'
}`}
>
{paper.has_vector
@@ -549,7 +563,7 @@ export function LibraryPanel({
e.stopPropagation();
onOpenReader(paper);
}}
className="px-2.5 py-1 rounded bg-slate-100 hover:bg-slate-200 text-slate-800 border border-slate-250 font-bold transition-all text-[9px] flex items-center gap-1 shadow-2xs cursor-pointer animate-fade-in"
className="px-3 py-2 rounded bg-sunken hover:bg-border-subtle text-content font-bold transition-all text-[9px] flex items-center gap-1 shadow-sm cursor-pointer animate-fade-in min-h-[34px]"
>
<BookOpen className="w-3 h-3" />
<span></span>
@@ -560,7 +574,7 @@ export function LibraryPanel({
e.stopPropagation();
onOpenCitation(paper);
}}
className="px-2.5 py-1 rounded bg-slate-50 hover:bg-slate-150 text-slate-655 border border-slate-250 font-bold transition-all text-[9px] flex items-center gap-1 shadow-2xs cursor-pointer animate-fade-in"
className="px-3 py-2 rounded bg-sunken hover:bg-sunken text-secondary font-bold transition-all text-[9px] flex items-center gap-1 shadow-sm cursor-pointer animate-fade-in min-h-[34px]"
>
<GitFork className="w-3 h-3" />
<span></span>
@@ -572,17 +586,17 @@ export function LibraryPanel({
<>
<div className="flex items-center gap-1">
<span
className={`w-1.5 h-1.5 rounded-full ${paper.has_translation ? 'bg-emerald-500' : 'bg-slate-200'}`}
className={`w-1.5 h-1.5 rounded-full ${paper.has_translation ? 'bg-emerald-500' : 'bg-border-subtle'}`}
/>
<span className="text-slate-400 text-[9px]">
<span className="text-tertiary text-[9px]">
{paper.has_translation ? '翻译' : '未翻译'}
</span>
</div>
<div className="flex items-center gap-1">
<span
className={`w-1.5 h-1.5 rounded-full ${paper.has_vector ? 'bg-amber-500' : 'bg-slate-200'}`}
className={`w-1.5 h-1.5 rounded-full ${paper.has_vector ? 'bg-amber-500' : 'bg-border-subtle'}`}
/>
<span className="text-slate-400 text-[9px]">
<span className="text-tertiary text-[9px]">
{paper.has_vector ? '向量库' : '未向量化'}
</span>
</div>
@@ -595,20 +609,20 @@ export function LibraryPanel({
{/* 分页控制面板 */}
{totalPages > 1 && (
<div className="flex items-center justify-between border border-slate-200 bg-white px-5 py-4 rounded-xl shadow-xs select-none">
<div className="flex items-center justify-between bg-surface px-5 py-4 rounded-xl select-none">
{/* 移动端两键分页 */}
<div className="flex flex-1 justify-between sm:hidden">
<button
disabled={currentPage <= 1}
onClick={() => setCurrentPage(currentPage - 1)}
className="relative inline-flex items-center rounded-lg border border-slate-250 bg-white px-4 py-2 text-xs font-bold text-slate-700 hover:bg-slate-50 disabled:opacity-50 transition-all cursor-pointer"
className="relative inline-flex items-center rounded-lg bg-surface px-4 py-2 text-xs font-bold text-secondary hover:bg-sunken disabled:opacity-50 transition-all cursor-pointer"
>
</button>
<button
disabled={currentPage >= totalPages}
onClick={() => setCurrentPage(currentPage + 1)}
className="relative ml-3 inline-flex items-center rounded-lg border border-slate-250 bg-white px-4 py-2 text-xs font-bold text-slate-700 hover:bg-slate-50 disabled:opacity-50 transition-all cursor-pointer"
className="relative ml-3 inline-flex items-center rounded-lg bg-surface px-4 py-2 text-xs font-bold text-secondary hover:bg-sunken disabled:opacity-50 transition-all cursor-pointer"
>
</button>
@@ -617,19 +631,17 @@ export function LibraryPanel({
{/* 桌面端完整分页 */}
<div className="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
<div>
<p className="text-xs text-slate-500 font-semibold">
<p className="text-xs text-secondary font-semibold">
{' '}
<span className="font-bold text-slate-900">
<span className="font-bold text-content">
{(currentPage - 1) * pageSize + 1}
</span>{' '}
{' '}
<span className="font-bold text-slate-900">
<span className="font-bold text-content">
{Math.min(currentPage * pageSize, totalCount)}
</span>{' '}
{' '}
<span className="font-bold text-slate-900">
{totalCount}
</span>{' '}
<span className="font-bold text-content">{totalCount}</span>{' '}
</p>
</div>
@@ -645,8 +657,8 @@ export function LibraryPanel({
onClick={() => setCurrentPage(currentPage - 1)}
className={`h-8 px-3 rounded-lg flex items-center justify-center text-xs font-bold transition-all ${
currentPage <= 1
? 'bg-slate-50 border border-slate-100 text-slate-400 cursor-not-allowed opacity-50'
: 'bg-white hover:bg-slate-50 border border-slate-200 text-slate-650 hover:text-slate-800 cursor-pointer shadow-3xs hover:shadow-2xs'
? 'bg-sunken text-tertiary cursor-not-allowed opacity-50'
: 'bg-surface hover:bg-sunken text-secondary hover:text-content cursor-pointer hover:shadow-sm'
}`}
>
<span></span>
@@ -666,8 +678,8 @@ export function LibraryPanel({
onClick={() => setCurrentPage(pageNum)}
className={`h-8 min-w-8 rounded-lg flex items-center justify-center text-xs font-bold transition-all cursor-pointer ${
currentPage === pageNum
? 'bg-blueprint text-white shadow-xs'
: 'bg-white hover:bg-slate-50 border border-slate-200 text-slate-655 hover:text-slate-800 shadow-3xs'
? 'bg-blueprint text-white'
: 'bg-surface hover:bg-sunken text-secondary hover:text-content'
}`}
>
{pageNum}
@@ -680,7 +692,7 @@ export function LibraryPanel({
return (
<span
key={pageNum}
className="h-8 min-w-8 flex items-center justify-center text-slate-400 text-xs font-bold"
className="h-8 min-w-8 flex items-center justify-center text-tertiary text-xs font-bold"
>
...
</span>
@@ -694,8 +706,8 @@ export function LibraryPanel({
onClick={() => setCurrentPage(currentPage + 1)}
className={`h-8 px-3 rounded-lg flex items-center justify-center text-xs font-bold transition-all ${
currentPage >= totalPages
? 'bg-slate-50 border border-slate-100 text-slate-400 cursor-not-allowed opacity-50'
: 'bg-white hover:bg-slate-50 border border-slate-200 text-slate-655 hover:text-slate-800 cursor-pointer shadow-3xs hover:shadow-2xs'
? 'bg-sunken text-tertiary cursor-not-allowed opacity-50'
: 'bg-surface hover:bg-sunken text-secondary hover:text-content cursor-pointer hover:shadow-sm'
}`}
>
<span></span>
@@ -703,8 +715,8 @@ export function LibraryPanel({
</nav>
{/* 快捷跳转输入框 */}
<div className="flex items-center gap-1.5 border-l border-slate-200 pl-4 h-6">
<span className="text-slate-450 text-[11px] font-semibold">
<div className="flex items-center gap-1.5 border-l border-subtle pl-4 h-6">
<span className="text-tertiary text-[11px] font-semibold">
</span>
<input
@@ -713,9 +725,9 @@ export function LibraryPanel({
onChange={handlePageInputChange}
onKeyDown={handlePageInputKeyDown}
onBlur={handlePageInputBlur}
className="w-10 h-7 text-center rounded-md border border-slate-250 bg-slate-55/50 text-slate-800 text-xs font-bold focus:outline-none focus:border-blueprint focus:bg-white focus:ring-1 focus:ring-blueprint transition-all shadow-3xs"
className="w-10 h-7 text-center rounded-md bg-sunken/50 text-content text-xs font-bold focus:outline-none focus:border-blueprint focus:bg-surface focus:ring-1 focus:ring-blueprint transition-all"
/>
<span className="text-slate-455 text-[11px] font-semibold">
<span className="text-tertiary text-[11px] font-semibold">
/ {totalPages}
</span>
</div>
+143 -78
View File
@@ -118,23 +118,23 @@ export function ObservationPanel(props: ObservationPanelProps) {
return (
<div className="w-full max-w-5xl mx-auto space-y-6">
{/* 标题栏 */}
<div className="flex items-center justify-between mb-4 border-b border-slate-200 pb-4 select-none">
<div className="flex items-center justify-between mb-4 border-b border-subtle pb-4 select-none">
<div>
<h2 className="text-sm font-bold tracking-wider text-slate-900 uppercase">
<h2 className="text-sm font-bold tracking-wider text-content uppercase">
</h2>
<p className="text-xs text-slate-500 mt-1">
<p className="text-xs text-secondary mt-1">
/ 线 / /
</p>
</div>
{/* 二级视图切换 SegControl */}
<div className="flex items-center gap-1 bg-slate-100 p-1 rounded-lg border border-slate-200">
<div className="flex items-center gap-1 bg-sunken p-1 rounded-lg ">
<button
onClick={() => setView('search')}
className={`px-3 py-1.5 rounded-md text-xs font-bold flex items-center gap-1.5 transition-all ${
view === 'search'
? 'bg-white text-slate-800 shadow-xs'
: 'text-slate-500 hover:text-slate-700'
? 'bg-surface text-content'
: 'text-secondary hover:text-content'
}`}
>
<Search className="w-3.5 h-3.5" />
@@ -144,8 +144,8 @@ export function ObservationPanel(props: ObservationPanelProps) {
onClick={() => setView('unified')}
className={`px-3 py-1.5 rounded-md text-xs font-bold flex items-center gap-1.5 transition-all ${
view === 'unified'
? 'bg-white text-slate-800 shadow-xs'
: 'text-slate-500 hover:text-slate-700'
? 'bg-surface text-content'
: 'text-secondary hover:text-content'
}`}
>
<Globe className="w-3.5 h-3.5" />
@@ -155,8 +155,8 @@ export function ObservationPanel(props: ObservationPanelProps) {
onClick={() => setView('library')}
className={`px-3 py-1.5 rounded-md text-xs font-bold flex items-center gap-1.5 transition-all ${
view === 'library'
? 'bg-white text-slate-800 shadow-xs'
: 'text-slate-500 hover:text-slate-700'
? 'bg-surface text-content'
: 'text-secondary hover:text-content'
}`}
>
<Database className="w-3.5 h-3.5" />
@@ -304,16 +304,16 @@ function SearchView({
return (
<div className="space-y-5">
{/* 表单卡片 */}
<div className="bg-white border border-slate-200 rounded-lg p-4 shadow-xs space-y-4">
<div className="console-panel rounded-lg p-4 space-y-4">
{/* 输入模式切换 */}
<div className="flex items-center gap-2 text-xs font-bold">
<span className="text-slate-500"></span>
<span className="text-secondary"></span>
<button
onClick={() => setInputMode('coordinates')}
className={`px-2.5 py-1 rounded-md transition-all ${
inputMode === 'coordinates'
? 'bg-blueprint text-white'
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
: 'bg-sunken text-secondary hover:bg-border-subtle'
}`}
>
@@ -323,7 +323,7 @@ function SearchView({
className={`px-2.5 py-1 rounded-md transition-all ${
inputMode === 'identifiers'
? 'bg-blueprint text-white'
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
: 'bg-sunken text-secondary hover:bg-border-subtle'
}`}
>
@@ -385,7 +385,7 @@ function SearchView({
}
>
{releaseOptions.length === 0 ? (
<div className="w-full px-2.5 py-2 rounded-md bg-slate-50 border border-slate-200 text-slate-400 text-xs font-medium select-none">
<div className="w-full px-2.5 py-2 rounded-md bg-sunken text-tertiary text-xs font-medium select-none">
</div>
) : (
@@ -431,7 +431,7 @@ function SearchView({
{/* Internal DR 需登录提示 */}
{currentReleaseNeedsAuth && (
<div className="flex items-center gap-2 text-xs text-rose-700 bg-rose-50 border border-rose-200 rounded-md p-2.5">
<div className="flex items-center gap-2 text-xs text-rose-700 dark:text-rose-300 bg-rose-50 dark:bg-rose-500/10 border border-rose-200 dark:border-rose-500/30 rounded-md p-2.5">
<Lock className="w-3.5 h-3.5 shrink-0" />
<span>
{searchForm.release.toUpperCase()} Internal
@@ -442,7 +442,7 @@ function SearchView({
{/* 交叉约束提示:当前 release 不支持该 subtype */}
{subtypeBlocked && (
<div className="flex items-center gap-2 text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded-md p-2.5">
<div className="flex items-center gap-2 text-xs text-amber-700 dark:text-amber-300 bg-amber-50 dark:bg-amber-500/10 border border-amber-200 dark:border-amber-500/30 rounded-md p-2.5">
<AlertTriangle className="w-3.5 h-3.5 shrink-0" />
<span>{subtypeBlocked}</span>
{currentSpec?.subtypes && currentSpec.subtypes.length > 0 && (
@@ -476,7 +476,7 @@ function SearchView({
setSearchForm((f) => ({ ...f, ra: e.target.value }))
}
placeholder="0~360"
className="w-full px-2.5 py-2 rounded-md bg-slate-50 border border-slate-250 text-slate-900 placeholder-slate-400 focus:outline-none focus:border-blueprint focus:bg-white transition-all text-xs font-mono"
className="w-full px-2.5 py-2 rounded-md bg-sunken border border-default text-content placeholder-tertiary focus:outline-none focus:border-blueprint focus:bg-surface transition-all text-xs font-mono"
/>
</Field>
<Field label="Dec(度)">
@@ -488,7 +488,7 @@ function SearchView({
setSearchForm((f) => ({ ...f, dec: e.target.value }))
}
placeholder="-90~90"
className="w-full px-2.5 py-2 rounded-md bg-slate-50 border border-slate-250 text-slate-900 placeholder-slate-400 focus:outline-none focus:border-blueprint focus:bg-white transition-all text-xs font-mono"
className="w-full px-2.5 py-2 rounded-md bg-sunken border border-default text-content placeholder-tertiary focus:outline-none focus:border-blueprint focus:bg-surface transition-all text-xs font-mono"
/>
</Field>
<Field
@@ -503,7 +503,7 @@ function SearchView({
setSearchForm((f) => ({ ...f, radius: e.target.value }))
}
placeholder={`默认 0.1,建议 ≤${currentSpec?.suggested_max_radius_deg ?? 1}°`}
className="w-full px-2.5 py-2 rounded-md bg-slate-50 border border-slate-250 text-slate-900 placeholder-slate-400 focus:outline-none focus:border-blueprint focus:bg-white transition-all text-xs font-mono"
className="w-full px-2.5 py-2 rounded-md bg-sunken border border-default text-content placeholder-tertiary focus:outline-none focus:border-blueprint focus:bg-surface transition-all text-xs font-mono"
/>
</Field>
</div>
@@ -514,7 +514,7 @@ function SearchView({
currentSpec.suggested_max_radius_deg &&
parseFloat(searchForm.radius) <=
currentSpec.hard_max_radius_deg && (
<div className="flex items-center gap-2 text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded-md p-2.5">
<div className="flex items-center gap-2 text-xs text-amber-700 dark:text-amber-300 bg-amber-50 dark:bg-amber-500/10 border border-amber-200 dark:border-amber-500/30 rounded-md p-2.5">
<AlertTriangle className="w-3.5 h-3.5 shrink-0" />
<span>
{searchForm.radius}°
@@ -536,7 +536,7 @@ function SearchView({
)}
{searching ? '检索中...' : '检索候选源'}
</button>
<span className="text-[10px] text-slate-400">
<span className="text-[10px] text-tertiary">
"按坐标下载全部"
</span>
<button
@@ -564,7 +564,7 @@ function SearchView({
? `格式:${identifierFormat},如 65214031805717376`
: '输入源标识符,多个用逗号或换行分隔'
}
className="w-full px-2.5 py-2 rounded-md bg-slate-50 border border-slate-250 text-slate-900 placeholder-slate-400 focus:outline-none focus:border-blueprint focus:bg-white transition-all text-xs font-mono"
className="w-full px-2.5 py-2 rounded-md bg-sunken border border-default text-content placeholder-tertiary focus:outline-none focus:border-blueprint focus:bg-surface transition-all text-xs font-mono"
/>
</Field>
<button
@@ -585,7 +585,7 @@ function SearchView({
{/* 检索错误 */}
{searchError && (
<div className="flex items-center gap-2 text-xs text-rose-700 bg-rose-50 border border-rose-200 rounded-md p-3">
<div className="flex items-center gap-2 text-xs text-rose-700 dark:text-rose-300 bg-rose-50 dark:bg-rose-500/10 border border-rose-200 dark:border-rose-500/30 rounded-md p-3">
<AlertTriangle className="w-4 h-4 shrink-0" />
<span>{searchError}</span>
</div>
@@ -595,10 +595,10 @@ function SearchView({
{inputMode === 'coordinates' && searchResults.length > 0 && (
<div className="space-y-3">
<div className="flex items-center justify-between">
<h3 className="text-xs font-bold text-slate-700 flex items-center gap-1.5">
<h3 className="text-xs font-bold text-secondary flex items-center gap-1.5">
<Activity className="w-3.5 h-3.5 text-blueprint" />
<span className="text-slate-400 font-normal">
<span className="text-tertiary font-normal">
{searchResults.length}
</span>
</h3>
@@ -609,14 +609,14 @@ function SearchView({
>
</button>
<span className="text-slate-300">|</span>
<span className="text-tertiary">|</span>
<button
onClick={selectNone}
className="text-slate-500 hover:underline font-bold"
className="text-secondary hover:underline font-bold"
>
</button>
<span className="text-slate-400 ml-2">
<span className="text-tertiary ml-2">
{selectedSourceIds.size}
</span>
</div>
@@ -632,7 +632,7 @@ function SearchView({
className={`flex items-start gap-2 p-2.5 rounded-md border cursor-pointer transition-all ${
checked
? 'border-blueprint bg-blueprint/5 ring-1 ring-blueprint'
: 'border-slate-200 bg-white hover:border-slate-300'
: 'border-subtle bg-surface hover:border-subtle'
}`}
>
<input
@@ -648,11 +648,11 @@ function SearchView({
>
{theme.label}
</span>
<span className="font-mono text-[10px] text-slate-700 truncate font-semibold">
<span className="font-mono text-[10px] text-secondary truncate font-semibold">
{c.source_id}
</span>
</div>
<div className="flex items-center gap-2 text-[10px] text-slate-500">
<div className="flex items-center gap-2 text-[10px] text-secondary">
{c.ra != null && c.dec != null && (
<span className="font-mono">
({c.ra.toFixed(4)}, {c.dec.toFixed(4)})
@@ -692,15 +692,15 @@ function SearchView({
!searchError &&
searchForm.ra &&
searchForm.dec && (
<div className="text-center py-8 text-xs text-slate-400">
<Telescope className="w-8 h-8 mx-auto mb-2 text-slate-300" />
<div className="text-center py-8 text-xs text-tertiary">
<Telescope className="w-8 h-8 mx-auto mb-2 text-tertiary" />
</div>
)}
{/* 下载错误 */}
{downloadError && (
<div className="flex items-center gap-2 text-xs text-rose-700 bg-rose-50 border border-rose-200 rounded-md p-3">
<div className="flex items-center gap-2 text-xs text-rose-700 dark:text-rose-300 bg-rose-50 dark:bg-rose-500/10 border border-rose-200 dark:border-rose-500/30 rounded-md p-3">
<AlertTriangle className="w-4 h-4 shrink-0" />
<span>{downloadError}</span>
</div>
@@ -710,13 +710,13 @@ function SearchView({
{downloadResult && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<h3 className="text-xs font-bold text-slate-700 flex items-center gap-1.5">
<CheckCircle2 className="w-3.5 h-3.5 text-emerald-600" />
<h3 className="text-xs font-bold text-secondary flex items-center gap-1.5">
<CheckCircle2 className="w-3.5 h-3.5 text-success" />
</h3>
<button
onClick={() => setDownloadResult(null)}
className="text-[10px] text-slate-400 hover:text-slate-600"
className="text-[10px] text-tertiary hover:text-secondary"
>
<X className="w-3 h-3" />
</button>
@@ -776,25 +776,25 @@ function LibraryView({
</div>
{/* 筛选工具栏 */}
<div className="flex flex-col sm:flex-row gap-3 bg-white p-4 rounded-lg border border-slate-200 shadow-sm text-xs font-semibold text-slate-700">
<div className="console-panel flex flex-col sm:flex-row gap-3 p-4 rounded-lg text-xs font-semibold text-secondary">
<div className="flex-1 space-y-1.5">
<label className="block text-slate-500 font-bold">
<label className="block text-secondary font-bold">
source_id
</label>
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 w-3.5 h-3.5" />
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-tertiary w-3.5 h-3.5" />
<input
type="text"
value={libSearch}
onChange={(e) => setLibSearch(e.target.value)}
placeholder="按 source_id 模糊匹配..."
className="w-full pl-9 pr-8 py-2 rounded-md bg-slate-50 border border-slate-250 text-slate-900 placeholder-slate-400 focus:outline-none focus:border-blueprint focus:bg-white transition-all text-xs font-medium"
className="w-full pl-9 pr-8 py-2 rounded-md bg-sunken border border-default text-content placeholder-tertiary focus:outline-none focus:border-blueprint focus:bg-surface transition-all text-xs font-medium"
/>
{libSearch && (
<button
type="button"
onClick={() => setLibSearch('')}
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 p-0.5"
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-tertiary hover:text-secondary p-0.5"
>
<X className="w-3 h-3" />
</button>
@@ -865,7 +865,7 @@ function LibraryView({
{/* 错误提示 */}
{libraryError && (
<div className="flex items-center gap-2 text-xs text-rose-700 bg-rose-50 border border-rose-200 rounded-md p-3">
<div className="flex items-center gap-2 text-xs text-rose-700 dark:text-rose-300 bg-rose-50 dark:bg-rose-500/10 border border-rose-200 dark:border-rose-500/30 rounded-md p-3">
<AlertTriangle className="w-4 h-4 shrink-0" />
<span>{libraryError}</span>
</div>
@@ -873,7 +873,7 @@ function LibraryView({
{/* 加载态 */}
{libraryLoading && (
<div className="flex flex-col items-center justify-center py-12 text-slate-500">
<div className="flex flex-col items-center justify-center py-12 text-secondary">
<Loader2 className="w-8 h-8 animate-spin text-blueprint mb-2" />
<span className="text-xs">...</span>
</div>
@@ -881,8 +881,8 @@ function LibraryView({
{/* 空态 */}
{!libraryLoading && libraryItems.length === 0 && !libraryError && (
<div className="flex flex-col items-center justify-center py-16 text-slate-400">
<Database className="w-10 h-10 mb-3 text-slate-300" />
<div className="flex flex-col items-center justify-center py-16 text-tertiary">
<Database className="w-10 h-10 mb-3 text-tertiary" />
<p className="text-xs font-medium"></p>
<p className="text-[10px] mt-1">
@@ -893,17 +893,42 @@ function LibraryView({
{/* 列表 */}
{!libraryLoading && libraryItems.length > 0 && (
<>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{/* 移动端单栏列表 */}
<div className="space-y-3 md:hidden">
{libraryItems.map((rec) => (
<LibraryCard
key={`${rec.source}|${rec.product}|${rec.source_id}`}
key={`mob-${rec.source}|${rec.product}|${rec.source_id}`}
rec={rec}
/>
))}
</div>
{/* 桌面端双栏瀑布流 */}
<div className="hidden md:flex md:flex-row gap-3 items-start">
<div className="flex-1 flex flex-col gap-3 min-w-0">
{libraryItems
.filter((_, idx) => idx % 2 === 0)
.map((rec) => (
<LibraryCard
key={`desk-l-${rec.source}|${rec.product}|${rec.source_id}`}
rec={rec}
/>
))}
</div>
<div className="flex-1 flex flex-col gap-3 min-w-0">
{libraryItems
.filter((_, idx) => idx % 2 !== 0)
.map((rec) => (
<LibraryCard
key={`desk-r-${rec.source}|${rec.product}|${rec.source_id}`}
rec={rec}
/>
))}
</div>
</div>
{/* 分页栏 */}
<div className="flex items-center justify-between flex-wrap gap-3 pt-4 border-t border-slate-200 text-xs font-semibold text-slate-600">
<div className="flex items-center justify-between flex-wrap gap-3 pt-4 border-t border-subtle text-xs font-semibold text-secondary">
<div className="flex items-center gap-2">
<span></span>
<CustomSelect
@@ -925,14 +950,14 @@ function LibraryView({
<button
onClick={() => setLibPage(1)}
disabled={libPage <= 1}
className="px-2.5 py-1.5 rounded-md bg-white border border-slate-200 hover:bg-slate-50 disabled:opacity-40 font-bold"
className="btn-console px-2.5 py-1.5 rounded-md disabled:opacity-40 text-[10px] font-bold"
>
</button>
<button
onClick={() => setLibPage(libPage - 1)}
disabled={libPage <= 1}
className="px-2.5 py-1.5 rounded-md bg-white border border-slate-200 hover:bg-slate-50 disabled:opacity-40 font-bold"
className="btn-console px-2.5 py-1.5 rounded-md disabled:opacity-40 text-[10px] font-bold"
>
</button>
@@ -942,14 +967,14 @@ function LibraryView({
<button
onClick={() => setLibPage(libPage + 1)}
disabled={libPage >= totalPages}
className="px-2.5 py-1.5 rounded-md bg-white border border-slate-200 hover:bg-slate-50 disabled:opacity-40 font-bold"
className="btn-console px-2.5 py-1.5 rounded-md disabled:opacity-40 text-[10px] font-bold"
>
</button>
<button
onClick={() => setLibPage(totalPages)}
disabled={libPage >= totalPages}
className="px-2.5 py-1.5 rounded-md bg-white border border-slate-200 hover:bg-slate-50 disabled:opacity-40 font-bold"
className="btn-console px-2.5 py-1.5 rounded-md disabled:opacity-40 text-[10px] font-bold"
>
</button>
@@ -980,7 +1005,7 @@ function Field({
compact ? 'w-full sm:w-36 space-y-1.5 font-bold' : 'space-y-1.5'
}
>
<label className="block text-slate-500 font-bold text-xs">{label}</label>
<label className="block text-secondary font-bold text-xs">{label}</label>
{children}
</div>
);
@@ -996,12 +1021,12 @@ function StatCard({
value: string;
}) {
return (
<div className="bg-white border border-slate-200 rounded-lg p-3 shadow-xs">
<div className="flex items-center gap-1.5 text-[10px] font-bold text-slate-500 tracking-wider uppercase mb-1.5">
<div className="console-panel rounded-lg p-3">
<div className="flex items-center gap-1.5 text-[10px] font-bold text-secondary tracking-wider uppercase mb-1.5">
{icon}
<span>{label}</span>
</div>
<div className="text-base font-bold text-slate-800">{value}</div>
<div className="text-base font-bold text-content">{value}</div>
</div>
);
}
@@ -1031,31 +1056,66 @@ function LibraryCard({ rec }: { rec: ObservationRecord }) {
const { fetchPreview, getPreview } = useObservationPreview();
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
// 点击卡片主体切换第一个产物的预览状态(排除按钮或下载链接的点击)
const handleCardClick = (e: React.MouseEvent) => {
const target = e.target as HTMLElement;
if (
target.closest('button') ||
target.closest('a') ||
target.closest('input')
) {
return;
}
if (artifacts.length > 0) {
const firstIdx = 0;
const previewKey = `${rec.source_id}#${firstIdx}`;
const previewable = canPreview(rec.product, artifacts[firstIdx].format);
if (!previewable) return;
const isOpen = expanded[previewKey] ?? false;
const previewState = getPreview(rec.source_id, firstIdx);
const next = !isOpen;
setExpanded((m) => ({ ...m, [previewKey]: next }));
if (next && !previewState.data && !previewState.loading) {
fetchPreview(
rec.source,
rec.product,
undefined,
rec.source_id,
firstIdx
);
}
}
};
return (
<div className="bg-white border border-slate-200 rounded-lg p-3.5 shadow-xs hover:shadow-md hover:border-slate-300 transition-all">
<div className="flex items-center justify-between mb-2 pb-2 border-b border-slate-100">
<div
onClick={handleCardClick}
className="console-panel rounded-lg p-3.5 cursor-pointer select-none"
>
<div className="flex items-center justify-between mb-2 pb-2 border-b border-subtle">
<div className="flex items-center gap-1.5">
<span
className={`px-2 py-0.5 rounded text-[10px] font-bold ${theme.badge}`}
>
{theme.label}
</span>
<span className="px-2 py-0.5 rounded text-[10px] font-bold bg-slate-200 text-slate-700">
<span className="px-2 py-0.5 rounded text-[10px] font-bold bg-border-subtle text-secondary">
{productLabel}
</span>
</div>
<span className="text-[9px] text-slate-400 font-mono">
<span className="text-[9px] text-tertiary font-mono">
{rec.created_at.replace('T', ' ').slice(0, 19)}
</span>
</div>
<div className="flex items-center justify-between mb-2">
<div className="min-w-0">
<div className="text-[9px] font-bold text-slate-400 tracking-widest uppercase">
<div className="text-[9px] font-bold text-tertiary tracking-widest uppercase">
Source ID
</div>
<div
className="font-mono text-xs text-slate-800 font-semibold truncate"
className="font-mono text-xs text-content font-semibold truncate"
title={rec.source_id}
>
{rec.source_id}
@@ -1063,10 +1123,10 @@ function LibraryCard({ rec }: { rec: ObservationRecord }) {
</div>
{rec.ra != null && rec.dec != null && (
<div className="text-right shrink-0 ml-2">
<div className="text-[9px] font-bold text-slate-400 tracking-widest uppercase">
<div className="text-[9px] font-bold text-tertiary tracking-widest uppercase">
</div>
<div className="font-mono text-[10px] text-slate-600">
<div className="font-mono text-[10px] text-secondary">
{rec.ra.toFixed(4)}, {rec.dec.toFixed(4)}
</div>
</div>
@@ -1074,7 +1134,7 @@ function LibraryCard({ rec }: { rec: ObservationRecord }) {
</div>
{artifacts.length === 0 ? (
<p className="text-[10px] text-slate-400 italic"></p>
<p className="text-[10px] text-tertiary italic"></p>
) : (
<div className="space-y-1">
{artifacts.map((a, i) => {
@@ -1098,8 +1158,8 @@ function LibraryCard({ rec }: { rec: ObservationRecord }) {
};
return (
<div key={i} className="space-y-1">
<div className="flex items-center justify-between text-[10px] pl-2 border-l-2 border-slate-100">
<span className="text-slate-500 flex items-center gap-1 min-w-0">
<div className="flex items-center justify-between text-[10px] pl-2 border-l-2 border-subtle">
<span className="text-secondary flex items-center gap-1 min-w-0">
{a.band && (
<span className="px-1 py-0.5 rounded bg-violet-100 text-violet-700 font-medium shrink-0">
{a.band}
@@ -1108,7 +1168,7 @@ function LibraryCard({ rec }: { rec: ObservationRecord }) {
<span className="font-mono uppercase shrink-0">
{a.format}
</span>
<span className="text-slate-400">
<span className="text-tertiary">
·{' '}
{typeof a.size === 'number'
? formatFileSize(a.size)
@@ -1119,13 +1179,17 @@ function LibraryCard({ rec }: { rec: ObservationRecord }) {
{previewable && (
<button
onClick={togglePreview}
className="flex items-center gap-0.5 font-medium text-slate-500 hover:text-slate-800 transition-colors"
className={`p-1 rounded transition-all flex items-center justify-center cursor-pointer ${
isOpen
? 'text-blueprint bg-blueprint/10 font-bold'
: 'text-secondary hover:text-content hover:bg-sunken'
}`}
title="预览"
>
{previewState.loading ? (
<Loader2 className="w-3 h-3 animate-spin" />
<Loader2 className="w-3.5 h-3.5 animate-spin" />
) : (
<LineChart className="w-3 h-3" />
<LineChart className="w-3.5 h-3.5" />
)}
</button>
)}
@@ -1133,9 +1197,10 @@ function LibraryCard({ rec }: { rec: ObservationRecord }) {
href={`/api/files/${a.path}`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-0.5 font-bold text-sky-600 hover:text-sky-800 hover:underline"
className="p-1 rounded text-secondary hover:text-blueprint hover:bg-blueprint/10 transition-all flex items-center justify-center cursor-pointer"
title="下载"
>
<Download className="w-3 h-3" />
<Download className="w-3.5 h-3.5" />
</a>
</span>
</div>
@@ -1143,13 +1208,13 @@ function LibraryCard({ rec }: { rec: ObservationRecord }) {
{previewable && isOpen && (
<div className="pl-2">
{previewState.error ? (
<p className="text-[10px] text-rose-500 py-1">
<p className="text-[10px] text-danger py-1">
{previewState.error}
</p>
) : previewState.data ? (
<ObservationPreviewRenderer preview={previewState.data} />
) : previewState.loading ? (
<div className="flex items-center gap-1 text-[10px] text-slate-400 py-4 justify-center">
<div className="flex items-center gap-1 text-[10px] text-tertiary py-4 justify-center">
<Loader2 className="w-3 h-3 animate-spin" />
FITS ...
</div>
@@ -1160,7 +1225,7 @@ function LibraryCard({ rec }: { rec: ObservationRecord }) {
);
})}
{artifacts.length > 1 && (
<div className="flex items-center gap-1 text-[10px] text-slate-400 pt-1">
<div className="flex items-center gap-1 text-[10px] text-tertiary pt-1">
<CheckCircle2 className="w-3 h-3" />
<span>
{artifacts.length} · {formatFileSize(totalSize)}
+1
View File
@@ -199,6 +199,7 @@ export function ReaderPanel(props: ReaderPanelProps) {
highlightCandidates={state.highlightCandidates}
hoveredTarget={state.hoveredTarget}
hoverCardPos={state.hoverCardPos}
selectedParagraphIdx={selectedParagraphIdx}
>
{/* 学术助手侧边栏 */}
<ReaderNotesSidebar
+10 -2
View File
@@ -1,3 +1,4 @@
import { useState } from 'react';
import { useResearchAgent } from '../hooks/useResearchAgent';
import { AgentSessionSidebar } from '../components/agent/AgentSessionSidebar';
import { AgentMessageList } from '../components/agent/AgentMessageList';
@@ -29,9 +30,12 @@ export function ResearchAgentPanel({
library,
}: ResearchAgentPanelProps) {
const state = useResearchAgent({ showConfirm, showAlert });
const [showTaskSidebar, setShowTaskSidebar] = useState(false);
const hasTodos = state.activeTodos && state.activeTodos.length > 0;
return (
<div className="w-full flex-1 flex overflow-hidden bg-slate-100 rounded-lg border border-slate-200 h-full lg:h-[calc(100vh-130px)] shadow-sm">
<div className="w-full flex-1 flex overflow-hidden bg-sunken rounded-lg h-full lg:h-[calc(100vh-130px)]">
{/* 左侧会话列表侧栏 */}
<AgentSessionSidebar
sessions={state.sessions}
@@ -51,7 +55,7 @@ export function ResearchAgentPanel({
/>
{/* 右侧会话对话展示及输入面板 */}
<div className="flex-1 flex flex-col overflow-hidden bg-white relative">
<div className="flex-1 flex flex-col overflow-hidden bg-surface relative">
<AgentMessageList
turns={state.turns}
activeTurn={state.activeTurn}
@@ -91,6 +95,10 @@ export function ResearchAgentPanel({
setActiveTab={setActiveTab}
citations={citations}
library={library}
showTaskSidebar={showTaskSidebar}
setShowTaskSidebar={setShowTaskSidebar}
hasTodos={hasTodos}
activeTodos={state.activeTodos}
/>
<AgentInputArea
+78 -63
View File
@@ -133,11 +133,11 @@ export function SearchPanel({
return (
<div className="space-y-6 w-full max-w-5xl mx-auto">
{/* 标题 */}
<div className="flex flex-col gap-1.5 border-b border-slate-200 pb-3">
<h2 className="text-sm font-bold tracking-wider text-slate-900 uppercase">
<div className="flex flex-col gap-1.5 border-b border-subtle pb-3">
<h2 className="text-sm font-bold tracking-wider text-content uppercase">
</h2>
<p className="text-slate-500 text-xs">
<p className="text-secondary text-xs">
NASA ADS arXiv
</p>
@@ -148,13 +148,13 @@ export function SearchPanel({
<form onSubmit={handleSearch} className="space-y-4">
<div className="flex flex-col md:flex-row gap-3 items-stretch">
<div className="relative flex-1">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400 w-5 h-5" />
<Search className="absolute left-4 top-1/2 -translate-y-1/2 text-tertiary w-5 h-5" />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="检索天文学文献 (支持关键字、作者、发表年份,如 'hot subdwarf year:2020-2023')"
className="w-full pl-12 pr-4 py-3 rounded-md bg-slate-50 border border-slate-300 text-slate-900 placeholder-slate-400 focus:outline-none focus:border-slate-500 focus:bg-white transition-all text-sm font-medium"
className="w-full pl-12 pr-4 py-3 rounded-md bg-sunken border border-default text-content placeholder-tertiary focus:outline-none focus:border-secondary focus:bg-surface transition-all text-sm font-medium"
/>
</div>
<div className="flex gap-2">
@@ -163,7 +163,7 @@ export function SearchPanel({
onClick={() => setShowBuilder(!showBuilder)}
className={`px-4 py-3 rounded-md border text-xs font-bold transition-all shrink-0 flex items-center gap-1.5 ${
showBuilder
? 'bg-slate-100 border-slate-350 text-slate-855'
? 'bg-sunken border-subtle text-content'
: 'btn-console'
}`}
>
@@ -187,13 +187,13 @@ export function SearchPanel({
{/* 条件构造器 */}
{showBuilder && (
<div className="p-4 rounded-md bg-slate-50 border border-slate-200 space-y-3 transition-all">
<div className="text-xs font-bold text-slate-700 flex justify-between items-center">
<div className="p-4 rounded-md bg-sunken space-y-3 transition-all">
<div className="text-xs font-bold text-secondary flex justify-between items-center">
<span></span>
<button
type="button"
onClick={handleAddRule}
className="text-xs text-slate-700 hover:text-slate-900 font-bold"
className="text-xs text-secondary hover:text-content font-bold"
>
+
</button>
@@ -203,7 +203,7 @@ export function SearchPanel({
{rules.map((rule, idx) => (
<div
key={idx}
className="flex flex-col sm:flex-row sm:items-center gap-2 pb-3.5 sm:pb-0 border-b border-slate-200/60 sm:border-0 last:border-0 last:pb-0"
className="flex flex-col sm:flex-row sm:items-center gap-2 pb-3.5 sm:pb-0 border-b border-subtle sm:border-0 last:border-0 last:pb-0"
>
{idx > 0 ? (
<CustomSelect
@@ -219,7 +219,7 @@ export function SearchPanel({
]}
/>
) : (
<div className="w-full sm:w-24 text-left sm:text-center text-[10px] sm:text-xs text-slate-400 sm:text-slate-500 font-bold sm:font-semibold py-1 uppercase tracking-wider select-none">
<div className="w-full sm:w-24 text-left sm:text-center text-[10px] sm:text-xs text-tertiary sm:text-secondary font-bold sm:font-semibold py-1 uppercase tracking-wider select-none">
</div>
)}
@@ -239,27 +239,42 @@ export function SearchPanel({
]}
/>
<input
type="text"
value={rule.val}
onChange={(e) =>
handleRuleChange(idx, 'val', e.target.value)
}
placeholder={
rule.field === 'year'
? '例如: 2020-2023 或 2022'
: rule.field === 'author'
? '例如: Althaus'
: '请输入检索词...'
}
className="w-full sm:flex-1 px-3 py-1.5 rounded-md bg-white border border-slate-300 text-slate-900 placeholder-slate-400 focus:outline-none focus:border-slate-500 text-xs font-medium"
/>
<div className="w-full sm:flex-1 flex flex-col gap-1">
<input
type="text"
value={rule.val}
onChange={(e) =>
handleRuleChange(idx, 'val', e.target.value)
}
placeholder={
rule.field === 'year'
? '例如: 2020-2023 或 2022'
: rule.field === 'author'
? '例如: Althaus'
: '请输入检索词...'
}
className={`w-full px-3 py-1.5 rounded-md bg-surface border text-content placeholder-tertiary focus:outline-none text-xs font-medium ${
rule.field === 'year' &&
rule.val.trim() !== '' &&
!/^\d{4}(-\d{4})?$/.test(rule.val.trim())
? 'border-rose-400 focus:border-rose-500 bg-rose-50/5 dark:bg-rose-500/5'
: 'border-default focus:border-secondary'
}`}
/>
{rule.field === 'year' &&
rule.val.trim() !== '' &&
!/^\d{4}(-\d{4})?$/.test(rule.val.trim()) && (
<span className="text-[10px] text-rose-500 font-bold ml-1 animate-pulse">
建议格式: 2022 2020-2023
</span>
)}
</div>
{rules.length > 1 && (
<button
type="button"
onClick={() => handleRemoveRule(idx)}
className="text-rose-600 hover:text-rose-800 text-xs font-bold px-3 py-1.5 self-end sm:self-auto border border-rose-100 rounded-md sm:border-0 bg-rose-50/20 sm:bg-transparent mt-1 sm:mt-0 transition-all cursor-pointer"
className="text-danger hover:text-danger text-xs font-bold px-3 py-1.5 self-end sm:self-auto border border-rose-100 rounded-md sm:border-0 bg-rose-50/20 sm:bg-transparent mt-1 sm:mt-0 transition-all cursor-pointer"
>
</button>
@@ -270,32 +285,32 @@ export function SearchPanel({
</div>
)}
<div className="text-[11px] text-slate-500 flex flex-wrap gap-x-4 gap-y-1 px-1 items-center">
<span className="font-semibold text-slate-650 flex items-center gap-1">
<div className="text-[11px] text-secondary flex flex-wrap gap-x-4 gap-y-1 px-1 items-center">
<span className="font-semibold text-secondary flex items-center gap-1">
<Lightbulb className="w-3.5 h-3.5 text-amber-500" />
:
</span>
<span>
:{' '}
<code className="bg-slate-200 px-1 py-0.5 rounded text-slate-800 font-mono text-[10px]">
<code className="bg-border-subtle px-1 py-0.5 rounded text-content font-mono text-[10px]">
author:"Althaus"
</code>
</span>
<span>
:{' '}
<code className="bg-slate-200 px-1 py-0.5 rounded text-slate-800 font-mono text-[10px]">
<code className="bg-border-subtle px-1 py-0.5 rounded text-content font-mono text-[10px]">
title:"hot subdwarf"
</code>
</span>
<span>
:{' '}
<code className="bg-slate-200 px-1 py-0.5 rounded text-slate-800 font-mono text-[10px]">
<code className="bg-border-subtle px-1 py-0.5 rounded text-content font-mono text-[10px]">
year:2020-2023
</code>
</span>
</div>
<div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-4 pt-3 border-t border-slate-200">
<div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-4 pt-3 border-t border-subtle">
{/* 数据源平台 */}
<div className="flex gap-4">
{[
@@ -305,7 +320,7 @@ export function SearchPanel({
].map((src) => (
<label
key={src.id}
className="flex items-center gap-2 cursor-pointer text-xs font-bold text-slate-700"
className="flex items-center gap-2 cursor-pointer text-xs font-bold text-secondary"
>
<input
type="radio"
@@ -319,8 +334,8 @@ export function SearchPanel({
<span
className={
searchSource === src.id
? 'text-slate-900 font-bold'
: 'text-slate-500 hover:text-slate-700'
? 'text-content font-bold'
: 'text-secondary hover:text-secondary'
}
>
{src.label}
@@ -332,7 +347,7 @@ export function SearchPanel({
{/* 排序及每页条数 */}
<div className="flex flex-wrap items-center gap-4">
<div className="flex items-center gap-2">
<span className="text-xs font-semibold text-slate-500">
<span className="text-xs font-semibold text-secondary">
:
</span>
<CustomSelect
@@ -348,7 +363,7 @@ export function SearchPanel({
</div>
<div className="flex items-center gap-2">
<span className="text-xs font-semibold text-slate-500">
<span className="text-xs font-semibold text-secondary">
:
</span>
<CustomSelect
@@ -383,11 +398,11 @@ export function SearchPanel({
{/* BibTeX 导出显示 */}
{bibtexContent && (
<div className="console-panel p-5 rounded-lg border border-slate-200 relative overflow-hidden bg-slate-50/20">
<h3 className="text-xs font-bold text-slate-700 mb-3 flex items-center gap-2">
<CheckCircle className="w-4 h-4 text-emerald-600" /> BibTeX
<div className="console-panel p-5 rounded-lg relative overflow-hidden bg-slate-50/20">
<h3 className="text-xs font-bold text-secondary mb-3 flex items-center gap-2">
<CheckCircle className="w-4 h-4 text-success" /> BibTeX
</h3>
<pre className="bg-white p-4 rounded-md border border-slate-200 text-xs text-slate-800 font-mono overflow-x-auto whitespace-pre-wrap max-h-60 leading-relaxed shadow-inner">
<pre className="bg-surface p-4 rounded-md text-xs text-content font-mono overflow-x-auto whitespace-pre-wrap max-h-60 leading-relaxed shadow-inner">
{bibtexContent}
</pre>
<button
@@ -406,9 +421,9 @@ export function SearchPanel({
<div className="relative min-h-[200px]">
{searching && (
<div className="absolute inset-0 bg-white/60 backdrop-blur-[1px] z-10 flex items-center justify-center rounded-lg">
<div className="flex flex-col items-center gap-3 bg-white p-6 rounded-lg shadow-sm border border-slate-200 text-center">
<Loader className="w-8 h-8 text-slate-600 animate-spin" />
<span className="text-xs font-bold text-slate-600">
<div className="flex flex-col items-center gap-3 bg-surface p-6 rounded-lg shadow-sm text-center">
<Loader className="w-8 h-8 text-secondary animate-spin" />
<span className="text-xs font-bold text-secondary">
...
</span>
</div>
@@ -432,12 +447,12 @@ export function SearchPanel({
<button
type="button"
onClick={() => onShowDetail(paper)}
className="px-3 py-1.5 rounded-md bg-white border border-slate-250 text-slate-655 hover:bg-slate-50 hover:border-slate-350 transition-all text-xs font-bold flex items-center cursor-pointer"
className="px-3.5 py-2.5 min-h-[38px] rounded-md bg-surface border border-default text-secondary hover:bg-sunken hover:border-border-strong transition-all text-xs font-bold flex items-center cursor-pointer"
>
</button>
{paper.is_downloaded ? (
<span className="px-2.5 py-1 rounded-md bg-emerald-50 text-emerald-800 border border-emerald-200/60 text-xs font-bold flex items-center gap-1">
<span className="px-2.5 py-1 rounded-md bg-emerald-50 dark:bg-emerald-500/10 text-emerald-800 dark:text-emerald-300 border border-emerald-200/60 dark:border-emerald-500/30 text-xs font-bold flex items-center gap-1">
<CheckCircle className="w-3.5 h-3.5" />
</span>
) : (
@@ -446,18 +461,18 @@ export function SearchPanel({
paper.html_error === 'no_resource' ? (
<span
title="已手动标记为【无有效全文资源】,批量下载时自动跳过"
className="px-2 py-1 bg-slate-50 text-slate-600 border border-slate-200 text-[10px] font-bold rounded-md cursor-help flex items-center gap-1"
className="px-2 py-1 bg-sunken text-secondary text-[10px] font-bold rounded-md cursor-help flex items-center gap-1"
>
<AlertTriangle className="w-3 h-3 text-slate-455" />{' '}
<AlertTriangle className="w-3 h-3 text-tertiary" />{' '}
</span>
) : (
(paper.pdf_error || paper.html_error) && (
<span
title={`自动下载失败:${[paper.pdf_error, paper.html_error].filter(Boolean).join('; ')}`}
className="px-2 py-1 bg-rose-50 text-rose-800 border border-rose-200/60 text-[10px] font-bold rounded-md cursor-help flex items-center gap-1"
className="px-2 py-1 bg-rose-50 dark:bg-rose-500/10 text-rose-800 dark:text-rose-300 border border-rose-200/60 dark:border-rose-500/30 text-[10px] font-bold rounded-md cursor-help flex items-center gap-1"
>
<AlertTriangle className="w-3 h-3 text-rose-600" />{' '}
<AlertTriangle className="w-3 h-3 text-danger" />{' '}
</span>
)
@@ -465,7 +480,7 @@ export function SearchPanel({
<button
onClick={() => handleDownload(paper.bibcode)}
disabled={isDownloading}
className="btn-console btn-console-primary px-3 py-1.5 rounded-md text-xs font-bold flex items-center gap-1 transition-all"
className="btn-console btn-console-primary px-3.5 py-2.5 min-h-[38px] rounded-md text-xs font-bold flex items-center gap-1 transition-all"
>
{isDownloading ? (
<Loader className="w-3.5 h-3.5 animate-spin" />
@@ -477,19 +492,19 @@ export function SearchPanel({
</div>
)}
<label className="flex items-center gap-1.5 cursor-pointer border border-slate-250 px-2.5 py-1.5 rounded-md bg-slate-50 hover:bg-slate-100 text-slate-700 transition-all text-xs font-semibold">
<label className="flex items-center gap-1.5 cursor-pointer px-2.5 py-1.5 rounded-md bg-sunken hover:bg-sunken text-secondary transition-all text-xs font-semibold">
<input
type="checkbox"
checked={exportingList.includes(paper.bibcode)}
onChange={() => toggleExportItem(paper.bibcode)}
className="rounded text-slate-700 border-slate-300"
className="rounded text-secondary border-subtle"
/>
<span></span>
</label>
</>
}
abstract={
<p className="text-xs text-slate-600 line-clamp-3 leading-relaxed mb-4 font-normal">
<p className="text-xs text-secondary line-clamp-3 leading-relaxed mb-4 font-normal">
{paper.abstract_text || '暂无文献摘要数据。'}
</p>
}
@@ -497,7 +512,7 @@ export function SearchPanel({
<>
<button
onClick={() => openReader(paper)}
className="px-4 py-1.5 rounded-md bg-slate-100 border border-slate-250 text-xs font-bold text-slate-800 hover:bg-slate-200 transition-all"
className="px-4.5 py-2.5 min-h-[38px] rounded-md bg-sunken border border-default text-xs font-bold text-content hover:bg-border-subtle transition-all"
>
</button>
@@ -507,7 +522,7 @@ export function SearchPanel({
setActiveTab('citation');
loadCitations(paper.bibcode, true);
}}
className="px-4 py-1.5 rounded-md bg-slate-50 border border-slate-250 text-xs font-bold text-slate-600 hover:border-slate-400 hover:text-slate-800 transition-all"
className="px-4.5 py-2.5 min-h-[38px] rounded-md bg-sunken text-xs font-bold text-secondary hover:border-border-strong hover:text-content transition-all"
>
</button>
@@ -518,14 +533,14 @@ export function SearchPanel({
{paper.doi && <span>DOI: {paper.doi}</span>}
<span>
Bibcode:{' '}
<span className="font-semibold text-slate-700">
<span className="font-semibold text-secondary">
{paper.bibcode}
</span>
</span>
{paper.citation_count > 0 && (
<span>
:{' '}
<span className="text-sky-750 font-bold">
<span className="text-secondary font-bold">
{paper.citation_count}
</span>
</span>
@@ -540,16 +555,16 @@ export function SearchPanel({
{/* 分页控制栏 */}
{searchResults.length > 0 && (
<div className="flex items-center justify-between p-4 bg-white border border-slate-200 rounded-lg max-w-5xl mx-auto shadow-sm">
<div className="flex items-center justify-between p-4 bg-surface rounded-lg max-w-5xl mx-auto shadow-sm">
<button
onClick={() => handlePageChange(searchStart - searchRows)}
disabled={!hasPreviousPage || searching}
className="px-4 py-2 border border-slate-250 rounded-md bg-white text-xs font-bold text-slate-700 hover:bg-slate-50 disabled:opacity-40 disabled:hover:bg-white flex items-center gap-1 transition-all"
className="px-4 py-2 rounded-md bg-surface text-xs font-bold text-secondary hover:bg-sunken disabled:opacity-40 disabled:hover:bg-surface flex items-center gap-1 transition-all"
>
<ChevronLeft className="w-4 h-4" />
</button>
<span className="text-xs font-bold text-slate-600">
<span className="text-xs font-bold text-secondary">
{currentPage} ( {searchStart + 1} -{' '}
{searchStart + searchResults.length} )
</span>
@@ -557,7 +572,7 @@ export function SearchPanel({
<button
onClick={() => handlePageChange(searchStart + searchRows)}
disabled={!hasNextPage || searching}
className="px-4 py-2 border border-slate-250 rounded-md bg-white text-xs font-bold text-slate-700 hover:bg-slate-50 disabled:opacity-40 disabled:hover:bg-white flex items-center gap-1 transition-all"
className="px-4 py-2 rounded-md bg-surface text-xs font-bold text-secondary hover:bg-sunken disabled:opacity-40 disabled:hover:bg-surface flex items-center gap-1 transition-all"
>
<ChevronRight className="w-4 h-4" />
</button>
+19 -19
View File
@@ -11,11 +11,11 @@ export function SyncPanel() {
return (
<div className="space-y-6 w-full max-w-3xl mx-auto">
{/* 标题 */}
<div className="flex flex-col gap-1.5 border-b border-slate-200 pb-3 select-none">
<h2 className="text-sm font-bold tracking-wider text-slate-900 uppercase">
<div className="flex flex-col gap-1.5 border-b border-subtle pb-3 select-none">
<h2 className="text-sm font-bold tracking-wider text-content uppercase">
</h2>
<p className="text-slate-500 text-xs">
<p className="text-secondary text-xs">
NASA ADS arXiv
线
</p>
@@ -23,7 +23,7 @@ export function SyncPanel() {
{/* 全局错误显示 */}
{state.errorMsg && (
<div className="p-4 rounded-md bg-rose-50 border border-rose-200/60 flex gap-3 text-xs text-rose-800 items-start">
<div className="p-4 rounded-md bg-rose-50 dark:bg-rose-500/10 border border-rose-200/60 dark:border-rose-500/30 flex gap-3 text-xs text-rose-800 dark:text-rose-300 items-start">
<AlertTriangle className="w-4 h-4 shrink-0 mt-0.5" />
<div className="font-semibold">{state.errorMsg}</div>
</div>
@@ -74,36 +74,36 @@ export function SyncPanel() {
/>
{/* 3. 常用批量同步检索配置 */}
<div className="console-panel p-6 rounded-lg border border-slate-200 bg-white space-y-4 shadow-sm">
<div className="flex flex-col gap-1 border-b border-slate-100 pb-2 select-none">
<h3 className="text-xs font-bold text-slate-900 flex items-center gap-2">
<SlidersHorizontal className="w-4 h-4 text-slate-700" />
<div className="console-panel p-6 rounded-lg bg-surface space-y-4 shadow-sm">
<div className="flex flex-col gap-1 border-b border-subtle pb-2 select-none">
<h3 className="text-xs font-bold text-content flex items-center gap-2">
<SlidersHorizontal className="w-4 h-4 text-secondary" />
<span></span>
</h3>
<p className="text-slate-500 text-xs mt-0.5">
<p className="text-secondary text-xs mt-0.5">
</p>
</div>
{state.syncQueries.length === 0 ? (
<div className="text-center py-6 text-xs text-slate-400 italic select-none">
<div className="text-center py-6 text-xs text-tertiary italic select-none">
</div>
) : (
<div className="divide-y divide-slate-100 max-h-80 overflow-y-auto pr-1 scrollbar-thin">
<div className="divide-y divide-border-subtle max-h-80 overflow-y-auto pr-1 scrollbar-thin">
{state.syncQueries.map((sq) => (
<div
key={sq.id}
className="py-3 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 text-xs"
>
<div className="space-y-1">
<div className="font-bold text-slate-800 break-all select-all font-mono">
<div className="font-bold text-content break-all select-all font-mono">
{sq.query}
</div>
<div className="text-[10px] text-slate-500 flex items-center gap-2.5 font-semibold">
<div className="text-[10px] text-secondary flex items-center gap-2.5 font-semibold">
<span>
:{' '}
<strong className="text-slate-700">
<strong className="text-secondary">
{sq.source === 'all'
? '全部'
: sq.source === 'ads'
@@ -113,13 +113,13 @@ export function SyncPanel() {
</span>
<span>
:{' '}
<strong className="text-slate-700">
<strong className="text-secondary">
{sq.limit_count}
</strong>
</span>
<span>
:{' '}
<strong className="text-slate-700">{sq.last_run}</strong>
<strong className="text-secondary">{sq.last_run}</strong>
</span>
</div>
</div>
@@ -127,7 +127,7 @@ export function SyncPanel() {
<button
type="button"
onClick={() => state.handleReuseQuery(sq)}
className="px-2.5 py-1.5 rounded-md bg-slate-50 border border-slate-250 text-slate-655 hover:bg-slate-100 hover:text-slate-800 transition-all text-xs font-bold cursor-pointer"
className="px-2.5 py-1.5 rounded-md bg-sunken border border-default text-secondary hover:bg-sunken hover:text-content transition-all text-xs font-bold cursor-pointer"
>
</button>
@@ -135,14 +135,14 @@ export function SyncPanel() {
type="button"
disabled={state.status.active}
onClick={() => state.handleQuickSync(sq)}
className="px-2.5 py-1.5 rounded-md bg-slate-100 border border-slate-250 text-slate-800 hover:bg-slate-200 transition-all text-xs font-bold cursor-pointer disabled:opacity-40"
className="px-2.5 py-1.5 rounded-md bg-sunken border border-default text-content hover:bg-border-subtle transition-all text-xs font-bold cursor-pointer disabled:opacity-40"
>
</button>
<button
type="button"
onClick={() => state.handleDeleteQuery(sq.id)}
className="px-2.5 py-1.5 rounded-md bg-rose-50 border border-rose-200/60 text-rose-855 hover:bg-rose-100 transition-all text-xs font-bold cursor-pointer"
className="px-2.5 py-1.5 rounded-md bg-rose-50 dark:bg-rose-500/10 border border-rose-200/60 dark:border-rose-500/30 text-rose-700 dark:text-rose-300 hover:bg-rose-100 transition-all text-xs font-bold cursor-pointer"
>
</button>
+42 -21
View File
@@ -6,89 +6,110 @@ export const getDoctypeBadge = (doctype: string) => {
const typeMap: Record<string, { label: string; style: string }> = {
article: {
label: '期刊文章',
style: 'bg-blue-50 text-blue-700 border-blue-200',
style:
'bg-blue-50 dark:bg-blue-500/10 text-blue-700 dark:text-blue-300 border-blue-200 dark:border-blue-500/30',
},
eprint: {
label: '预印本',
style: 'bg-purple-50 text-purple-700 border-purple-200',
style:
'bg-purple-50 dark:bg-purple-500/10 text-purple-700 dark:text-purple-300 border-purple-200 dark:border-purple-500/30',
},
inproceedings: {
label: '会议论文',
style: 'bg-amber-50 text-amber-700 border-amber-200',
style:
'bg-amber-50 dark:bg-amber-500/10 text-amber-700 dark:text-amber-300 border-amber-200 dark:border-amber-500/30',
},
proceedings: {
label: '会议集',
style: 'bg-amber-50 text-amber-700 border-amber-200',
style:
'bg-amber-50 dark:bg-amber-500/10 text-amber-700 dark:text-amber-300 border-amber-200 dark:border-amber-500/30',
},
proposal: {
label: '观测提案',
style: 'bg-rose-50 text-rose-700 border-rose-200',
style:
'bg-rose-50 dark:bg-rose-500/10 text-rose-700 dark:text-rose-300 border-rose-200 dark:border-rose-500/30',
},
abstract: {
label: '会议摘要',
style: 'bg-slate-50 text-slate-700 border-slate-200',
style:
'bg-sunken dark:bg-slate-500/10 text-secondary dark:text-slate-300 border-subtle dark:border-slate-500/30',
},
catalog: {
label: '星表数据',
style: 'bg-indigo-50 text-indigo-700 border-indigo-200',
style:
'bg-indigo-50 dark:bg-indigo-500/10 text-indigo-700 dark:text-indigo-300 border-indigo-200 dark:border-indigo-500/30',
},
dataset: {
label: '星表数据',
style: 'bg-indigo-50 text-indigo-700 border-indigo-200',
style:
'bg-indigo-50 dark:bg-indigo-500/10 text-indigo-700 dark:text-indigo-300 border-indigo-200 dark:border-indigo-500/30',
},
software: {
label: '软件代码',
style: 'bg-teal-50 text-teal-700 border-teal-200',
style:
'bg-teal-50 dark:bg-teal-500/10 text-teal-700 dark:text-teal-300 border-teal-200 dark:border-teal-500/30',
},
phdthesis: {
label: '博士论文',
style: 'bg-cyan-50 text-cyan-700 border-cyan-200',
style:
'bg-cyan-50 dark:bg-cyan-500/10 text-cyan-700 dark:text-cyan-300 border-cyan-200 dark:border-cyan-500/30',
},
mastersthesis: {
label: '硕士论文',
style: 'bg-cyan-50 text-cyan-700 border-cyan-200',
style:
'bg-cyan-50 dark:bg-cyan-500/10 text-cyan-700 dark:text-cyan-300 border-cyan-200 dark:border-cyan-500/30',
},
circular: {
label: '天文电报',
style: 'bg-orange-50 text-orange-700 border-orange-200',
style:
'bg-orange-50 dark:bg-orange-500/10 text-orange-700 dark:text-orange-300 border-orange-200 dark:border-orange-500/30',
},
inbook: {
label: '图书章节',
style: 'bg-emerald-50 text-emerald-700 border-emerald-200',
style:
'bg-emerald-50 dark:bg-emerald-500/10 text-emerald-700 dark:text-emerald-300 border-emerald-200 dark:border-emerald-500/30',
},
book: {
label: '学术专著',
style: 'bg-emerald-50 text-emerald-700 border-emerald-200',
style:
'bg-emerald-50 dark:bg-emerald-500/10 text-emerald-700 dark:text-emerald-300 border-emerald-200 dark:border-emerald-500/30',
},
editorial: {
label: '期刊社论',
style: 'bg-slate-50 text-slate-700 border-slate-200',
style:
'bg-sunken dark:bg-slate-500/10 text-secondary dark:text-slate-300 border-subtle dark:border-slate-500/30',
},
erratum: {
label: '勘误说明',
style: 'bg-red-50 text-red-700 border-red-200',
style:
'bg-red-50 dark:bg-red-500/10 text-red-700 dark:text-red-300 border-red-200 dark:border-red-500/30',
},
misc: {
label: '其他文献',
style: 'bg-slate-50 text-slate-700 border-slate-200',
style:
'bg-sunken dark:bg-slate-500/10 text-secondary dark:text-slate-300 border-subtle dark:border-slate-500/30',
},
newsletter: {
label: '简报新闻',
style: 'bg-slate-50 text-slate-700 border-slate-200',
style:
'bg-sunken dark:bg-slate-500/10 text-secondary dark:text-slate-300 border-subtle dark:border-slate-500/30',
},
techreport: {
label: '技术报告',
style: 'bg-cyan-50 text-cyan-700 border-cyan-200',
style:
'bg-cyan-50 dark:bg-cyan-500/10 text-cyan-700 dark:text-cyan-300 border-cyan-200 dark:border-cyan-500/30',
},
obituary: {
label: '讣告',
style: 'bg-slate-50 text-slate-700 border-slate-200',
style:
'bg-sunken dark:bg-slate-500/10 text-secondary dark:text-slate-300 border-subtle dark:border-slate-500/30',
},
};
const val = doctype ? doctype.toLowerCase() : 'article';
const match = typeMap[val] || {
label: '文献',
style: 'bg-slate-50 text-slate-700 border-slate-200',
style:
'bg-sunken dark:bg-slate-500/10 text-secondary dark:text-slate-300 border-subtle dark:border-slate-500/30',
};
return (
<span