diff --git a/CLAUDE.md b/CLAUDE.md index bb83685..bf1be51 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -46,6 +46,9 @@ cargo test test_name npm run dev # HMR dev server on :5173, proxies /api to :8000 npm run build # TypeScript check + Vite production build → dashboard/dist/ npm run lint # ESLint +npm run lint:fix # ESLint with auto-fix +npm run format # Prettier format all source files +npm run format:check # Check formatting without modifying files ``` ## Architecture Overview @@ -169,13 +172,14 @@ Session-level modes configure the agent's identity, tool access, step limits, th Three built-in modes (registered in `ModeRegistry::builtins()`): -| Mode | ID | Tools | Max Steps | Thinking | Permission Profile | -|------|-----|-------|-----------|----------|---------------------| -| 通用科研助手 | `default` | All (unrestricted) | 8 (default) | user-controlled | none | -| 深度研究 | `deep-research` | All | 16 | forced on | `research` | -| 文献阅读助手 | `literature-reader` | Allowlist (read-only + literature) | 6 | forced off | `readonly` | +| Mode | ID | Tools | Max Steps | Thinking | Permission Profile | +| ------------ | --------------------- | ---------------------------------- | ----------- | --------------- | ------------------ | +| 通用科研助手 | `default` | All (unrestricted) | 8 (default) | user-controlled | none | +| 深度研究 | `deep-research` | All | 16 | forced on | `research` | +| 文献阅读助手 | `literature-reader` | Allowlist (read-only + literature) | 6 | forced off | `readonly` | Key types in `modes/mod.rs`: + - **`AgentMode`** — static definition struct: id, name, description, icon, identity/principles overrides, extra sections, `ToolSet`, `ModeConfig` - **`ToolSet`** — `All`, `Allowlist(&[&str])`, or `Except(&[&str])` — constrains which tools are available - **`ModeConfig`** — optional overrides for `max_steps`, `enable_thinking`, `tool_timeout_secs`, `permission_profile` @@ -190,6 +194,7 @@ The mode is stored in the `agent_sessions.mode` column (migration `2026062400000 Tool execution is gated by a priority-ordered rule chain: **Deny > Allow > Ask** (first match wins). Rules support content-level pattern matching (e.g., `run_bash(rm *)`). **Permission modes** (set via `AGENT_PERMISSION_MODE` env or mode's `permission_profile`): + - `default` — full rule chain evaluation - `accept_edits` — auto-allow `file_write`/`file_edit` within the working directory - `bypass` — skip all Ask checks (Deny rules still enforced) @@ -198,6 +203,7 @@ Tool execution is gated by a priority-ordered rule chain: **Deny > Allow > Ask** **Permission profiles** (`src/agent/runtime/permission_profile.rs`) are named presets loaded by `AGENT_PERMISSION_MODE` or a mode's `permission_profile` field. Profiles `research` and `readonly` are used by deep-research and literature-reader modes respectively. **Configuration** (in `.env`): + - `AGENT_PERMISSIONS_DENY` — comma-separated deny rules (e.g., `run_bash(rm *),run_bash(sudo *)`) - `AGENT_PERMISSIONS_ALLOW` — comma-separated allow rules - `AGENT_PERMISSIONS_ASK` — comma-separated ask rules @@ -207,11 +213,11 @@ Tool execution is gated by a priority-ordered rule chain: **Deny > Allow > Ask** The system supports three LLM tiers with cascade fallback: -| Tier | Env Prefix | Purpose | Fallback | -|------|-----------|---------|----------| -| Primary | `LLM_` | Main agent reasoning | — | -| Medium | `LLM_MEDIUM_` | Translation, RAG | Primary LLM config | -| Fast | `LLM_FAST_` | Memory extraction, background tasks | Medium LLM config | +| Tier | Env Prefix | Purpose | Fallback | +| ------- | --------------- | ----------------------------------- | ------------------ | +| Primary | `LLM_` | Main agent reasoning | — | +| Medium | `LLM_MEDIUM_` | Translation, RAG | Primary LLM config | +| Fast | `LLM_FAST_` | Memory extraction, background tasks | Medium LLM config | Each tier has `_API_KEY`, `_API_BASE`, `_MODEL` variants. Unset tiers cascade to the next tier down. @@ -224,6 +230,7 @@ When `LLM_VISION_MODEL` env is set, the `analyze_image` tool (`src/agent/tools/a ### Auto Memory Extraction Controlled by env vars: + - `EXTRACT_MEMORY_ENABLED` (default `false`) — enables automatic memory extraction at session end and during compaction - `EXTRACT_MEMORY_THROTTLE_TURNS` (default `3`) — extract every N turns - `EXTRACT_MEMORY_MAX_STEPS` (default `3`) — sub-agent max steps for extraction diff --git a/Dockerfile b/Dockerfile index 1a8a0db..9fb2b41 100644 --- a/Dockerfile +++ b/Dockerfile @@ -87,6 +87,7 @@ COPY --from=backend-builder /usr/local/bin/astroresearch /app/ COPY --from=frontend-builder /app/dashboard/dist/ ./dashboard/dist/ COPY skills/ ./skills/ COPY migrations/ ./migrations/ +COPY dictionary.txt ./ RUN mkdir -p /app/library /app/logs /app/bin diff --git a/Dockerfile.modeB b/Dockerfile.modeB index 0efce7e..f4bab34 100644 --- a/Dockerfile.modeB +++ b/Dockerfile.modeB @@ -79,6 +79,7 @@ FROM gcr.io/distroless/cc-debian12:nonroot COPY --from=frontend-builder /app/dashboard/dist/ /app/dashboard/dist/ COPY skills/ /app/skills/ COPY migrations/ /app/migrations/ +COPY dictionary.txt /app/ # 二进制 COPY --from=backend-builder /usr/local/bin/astroresearch /app/astroresearch diff --git a/dashboard/.prettierrc b/dashboard/.prettierrc new file mode 100644 index 0000000..7812743 --- /dev/null +++ b/dashboard/.prettierrc @@ -0,0 +1,11 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "bracketSpacing": true, + "arrowParens": "always", + "endOfLine": "lf" +} \ No newline at end of file diff --git a/dashboard/eslint.config.js b/dashboard/eslint.config.js index ef614d2..0080c07 100644 --- a/dashboard/eslint.config.js +++ b/dashboard/eslint.config.js @@ -3,6 +3,7 @@ import globals from 'globals' import reactHooks from 'eslint-plugin-react-hooks' import reactRefresh from 'eslint-plugin-react-refresh' import tseslint from 'typescript-eslint' +import prettier from 'eslint-plugin-prettier' import { defineConfig, globalIgnores } from 'eslint/config' export default defineConfig([ @@ -18,5 +19,11 @@ export default defineConfig([ languageOptions: { globals: globals.browser, }, + plugins: { + prettier, + }, + rules: { + 'prettier/prettier': 'warn', + }, }, ]) diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index 4efbc15..ab1124f 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -34,10 +34,13 @@ "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", "eslint": "^10.3.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.6", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.6.0", "jsdom": "^26.1.0", + "prettier": "^3.9.4", "tailwindcss": "^4.3.0", "typescript": "~6.0.2", "typescript-eslint": "^8.59.2", @@ -1112,6 +1115,18 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@pkgr/core": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.3.6.tgz", + "integrity": "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==", + "dev": true, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", @@ -3188,6 +3203,51 @@ } } }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.6.tgz", + "integrity": "sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.1", + "synckit": "^0.11.13" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, "node_modules/eslint-plugin-react-hooks": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", @@ -3343,6 +3403,12 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -5664,6 +5730,33 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.9.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.4.tgz", + "integrity": "sha512-yWG/o/4oJfo036EKAfK6ACAoDOfHeRHx4tuxkfBZiauURiaSmYwlpOr5LQqKtIkRD2z1PLteme2WoxEnj4tHTg==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", + "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", @@ -6144,6 +6237,21 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, + "node_modules/synckit": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.13.tgz", + "integrity": "sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==", + "dev": true, + "dependencies": { + "@pkgr/core": "^0.3.6" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, "node_modules/tailwind-merge": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", diff --git a/dashboard/package.json b/dashboard/package.json index 365f2de..8cd6e2c 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -7,6 +7,9 @@ "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", + "lint:fix": "eslint . --fix", + "format": "prettier --write \"src/**/*.{ts,tsx,css,md}\"", + "format:check": "prettier --check \"src/**/*.{ts,tsx,css,md}\"", "preview": "vite preview", "test": "vitest run", "test:watch": "vitest" @@ -38,10 +41,13 @@ "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", "eslint": "^10.3.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.6", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.6.0", "jsdom": "^26.1.0", + "prettier": "^3.9.4", "tailwindcss": "^4.3.0", "typescript": "~6.0.2", "typescript-eslint": "^8.59.2", diff --git a/dashboard/public/favicon.svg b/dashboard/public/favicon.svg index 53271d8..60f3e02 100644 --- a/dashboard/public/favicon.svg +++ b/dashboard/public/favicon.svg @@ -1,25 +1,85 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + \ No newline at end of file diff --git a/dashboard/public/logo.png b/dashboard/public/logo.png index 9181e74..2ea626d 100644 Binary files a/dashboard/public/logo.png and b/dashboard/public/logo.png differ diff --git a/dashboard/scratch/temp_logo.svg b/dashboard/scratch/temp_logo.svg new file mode 100644 index 0000000..17c4d59 --- /dev/null +++ b/dashboard/scratch/temp_logo.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dashboard/scratch/temp_logo_maskable.svg b/dashboard/scratch/temp_logo_maskable.svg new file mode 100644 index 0000000..304dc35 --- /dev/null +++ b/dashboard/scratch/temp_logo_maskable.svg @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dashboard/src/App.tsx b/dashboard/src/App.tsx index 1d03440..d5f4b73 100644 --- a/dashboard/src/App.tsx +++ b/dashboard/src/App.tsx @@ -45,20 +45,34 @@ export default function App() { }); }, []); - const showConfirm = useCallback((message: string, onConfirm: () => void, title = '确认操作') => { - setDialog({ - type: 'confirm', - title, - message, - onConfirm, - }); - }, []); + const showConfirm = useCallback( + (message: string, onConfirm: () => void, title = '确认操作') => { + setDialog({ + type: 'confirm', + title, + message, + onConfirm, + }); + }, + [] + ); // 2. 局部状态定义 (多组件共用或全局网络进度状态) - const [activeTab, setActiveTab] = useState<'search' | 'library' | 'reader' | 'citation' | 'sync' | 'agent'>(() => { + const [activeTab, setActiveTab] = useState< + 'search' | 'library' | 'reader' | 'citation' | 'sync' | 'agent' + >(() => { const saved = localStorage.getItem('astro_active_tab'); - const validTabs = ['search', 'library', 'reader', 'citation', 'sync', 'agent'] as const; - return (validTabs.includes(saved as typeof validTabs[number]) ? saved : 'search') as typeof validTabs[number]; + const validTabs = [ + 'search', + 'library', + 'reader', + 'citation', + 'sync', + 'agent', + ] as const; + return ( + validTabs.includes(saved as (typeof validTabs)[number]) ? saved : 'search' + ) as (typeof validTabs)[number]; }); const [englishText, setEnglishText] = useState(''); @@ -80,45 +94,54 @@ export default function App() { // 协调:进入阅读器方法 (需要拉取正文和笔记) const libraryRef = useRef | null>(null); - const openReader = useCallback(async (paper: StandardPaper, skipTabSwitch = false) => { - const lib = libraryRef.current; - if (!lib) return; - lib.setSelectedPaper(paper); - localStorage.setItem('last_read_bibcode', paper.bibcode); - lib.addPaperToRecent(paper); + const openReader = useCallback( + async (paper: StandardPaper, skipTabSwitch = false) => { + const lib = libraryRef.current; + if (!lib) return; + lib.setSelectedPaper(paper); + localStorage.setItem('last_read_bibcode', paper.bibcode); + lib.addPaperToRecent(paper); - setEnglishText(''); - setChineseText(''); - notes.setNotes([]); - notes.setShowNotesPanel(false); + setEnglishText(''); + setChineseText(''); + notes.setNotes([]); + notes.setShowNotesPanel(false); - if (!skipTabSwitch) { - setActiveTab('reader'); - } - - // 异步加载文献详情正文 - try { - const res = await axios.get<{ paper: StandardPaper, english_content?: string, translation_content?: string }>('/api/paper', { - params: { bibcode: paper.bibcode } - }); - if (res.data.english_content) { - setEnglishText(res.data.english_content); + if (!skipTabSwitch) { + setActiveTab('reader'); } - if (res.data.translation_content) { - setChineseText(res.data.translation_content); - } - } catch (e) { - console.error('加载文献详情失败', e); - } - // 异步加载文献已存笔记 - try { - const nRes = await axios.get('/api/notes', { params: { bibcode: paper.bibcode } }); - notes.setNotes(nRes.data); - } catch (e) { - console.error('加载笔记失败', e); - } - }, [notes]); + // 异步加载文献详情正文 + try { + const res = await axios.get<{ + paper: StandardPaper; + english_content?: string; + translation_content?: string; + }>('/api/paper', { + params: { bibcode: paper.bibcode }, + }); + if (res.data.english_content) { + setEnglishText(res.data.english_content); + } + if (res.data.translation_content) { + setChineseText(res.data.translation_content); + } + } catch (e) { + console.error('加载文献详情失败', e); + } + + // 异步加载文献已存笔记 + try { + const nRes = await axios.get('/api/notes', { + params: { bibcode: paper.bibcode }, + }); + notes.setNotes(nRes.data); + } catch (e) { + console.error('加载笔记失败', e); + } + }, + [notes] + ); const library = useLibrary({ isAuthenticated: auth.isAuthenticated, @@ -127,72 +150,113 @@ export default function App() { openReader, }); - useEffect(() => { libraryRef.current = library; }); + useEffect(() => { + libraryRef.current = library; + }); + + // 当切换到引用星系页并且有选中文献时,若引用图谱为空或与当前选中文献不匹配,则自动拉取引用拓扑数据 + useEffect(() => { + if ( + activeTab === 'citation' && + library.selectedPaper && + !citations.loadingCitations + ) { + const isMismatched = + !citations.citationNetwork || + citations.citationNetwork.bibcode !== library.selectedPaper.bibcode; + if (isMismatched) { + citations.loadCitations(library.selectedPaper.bibcode, true); + } + } + }, [activeTab, library.selectedPaper, citations]); const search = useSearch({ showAlert }); // 协调:从 RAG 问答结果跳转至目标文献段落并高亮 - const handleJumpToSource = useCallback(async (bibcode: string, paragraphIndex: number) => { - setActiveTab('reader'); - - // 如果当前选中的不是目标文献,则先加载它 - if (!library.selectedPaper || library.selectedPaper.bibcode !== bibcode) { - setEnglishText(''); - setChineseText(''); - notes.setNotes([]); - notes.setShowNotesPanel(false); - - try { - const paperRes = await axios.get<{ paper: StandardPaper, english_content?: string, translation_content?: string }>('/api/paper', { - params: { bibcode } - }); - - library.setSelectedPaper(paperRes.data.paper); - if (paperRes.data.english_content) { - setEnglishText(paperRes.data.english_content); + const handleJumpToSource = useCallback( + async (bibcode: string, paragraphIndex: number) => { + setActiveTab('reader'); + + // 如果当前选中的不是目标文献,则先加载它 + if (!library.selectedPaper || library.selectedPaper.bibcode !== bibcode) { + setEnglishText(''); + setChineseText(''); + notes.setNotes([]); + notes.setShowNotesPanel(false); + + try { + const paperRes = await axios.get<{ + paper: StandardPaper; + english_content?: string; + translation_content?: string; + }>('/api/paper', { + params: { bibcode }, + }); + + library.setSelectedPaper(paperRes.data.paper); + if (paperRes.data.english_content) { + setEnglishText(paperRes.data.english_content); + } + if (paperRes.data.translation_content) { + setChineseText(paperRes.data.translation_content); + } + + const nRes = await axios.get('/api/notes', { + params: { bibcode }, + }); + notes.setNotes(nRes.data); + } catch (e) { + console.error('加载跳转文献失败:', e); + return; } - if (paperRes.data.translation_content) { - setChineseText(paperRes.data.translation_content); + } + + // 强制开启侧边栏 + notes.setShowNotesPanel(true); + + // 延迟等 DOM 渲染完成进行平滑滚动与闪烁高亮 + setTimeout(() => { + const element = document.getElementById( + `paragraph-block-${paragraphIndex}` + ); + if (element) { + element.scrollIntoView({ behavior: 'smooth', block: 'center' }); + element.classList.add('bg-amber-100', 'transition-all'); + setTimeout(() => { + element.classList.remove('bg-amber-100'); + }, 2500); } - - const nRes = await axios.get('/api/notes', { params: { bibcode } }); - notes.setNotes(nRes.data); - } catch (e) { - console.error('加载跳转文献失败:', e); - return; - } - } - - // 强制开启侧边栏 - notes.setShowNotesPanel(true); - - // 延迟等 DOM 渲染完成进行平滑滚动与闪烁高亮 - setTimeout(() => { - const element = document.getElementById(`paragraph-block-${paragraphIndex}`); - if (element) { - element.scrollIntoView({ behavior: 'smooth', block: 'center' }); - element.classList.add('bg-amber-100', 'transition-all'); - setTimeout(() => { - element.classList.remove('bg-amber-100'); - }, 2500); - } - }, 400); - }, [library.selectedPaper, notes]); + }, 400); + }, + [library, notes] + ); // 协调:触发文献解析 const handleParse = async (bibcode: string, force = false) => { setParsing(true); try { - const res = await axios.post<{ markdown: string }>('/api/parse', { bibcode, force }); + const res = await axios.post<{ markdown: string }>('/api/parse', { + bibcode, + force, + }); setEnglishText(res.data.markdown); - - library.setLibrary(prev => prev.map(p => p.bibcode === bibcode ? { ...p, has_markdown: true } : p)); + + library.setLibrary((prev) => + prev.map((p) => + p.bibcode === bibcode ? { ...p, has_markdown: true } : p + ) + ); if (library.selectedPaper?.bibcode === bibcode) { - library.setSelectedPaper(prev => prev ? { ...prev, has_markdown: true } : null); + library.setSelectedPaper((prev) => + prev ? { ...prev, has_markdown: true } : null + ); } } catch (e) { console.error('文献解析失败', e); - showAlert('文献排版解析失败,请检查是否已完成 HTML/PDF 下载,并配置了 MinerU API 节点。', '解析失败'); + showAlert( + '文献排版解析失败,请检查是否已完成 HTML/PDF 下载,并配置了 MinerU API 节点。', + '解析失败' + ); } finally { setParsing(false); } @@ -202,16 +266,28 @@ export default function App() { const handleTranslate = async (bibcode: string, force = false) => { setTranslating(true); try { - const res = await axios.post<{ translation: string }>('/api/translate', { bibcode, force }); + const res = await axios.post<{ translation: string }>('/api/translate', { + bibcode, + force, + }); setChineseText(res.data.translation); - - library.setLibrary(prev => prev.map(p => p.bibcode === bibcode ? { ...p, has_translation: true } : p)); + + library.setLibrary((prev) => + prev.map((p) => + p.bibcode === bibcode ? { ...p, has_translation: true } : p + ) + ); if (library.selectedPaper?.bibcode === bibcode) { - library.setSelectedPaper(prev => prev ? { ...prev, has_translation: true } : null); + library.setSelectedPaper((prev) => + prev ? { ...prev, has_translation: true } : null + ); } } catch (e) { console.error('文献翻译失败', e); - showAlert('翻译失败,请检查 .env 中的大模型 API 密钥与端点配置。', '翻译失败'); + showAlert( + '翻译失败,请检查 .env 中的大模型 API 密钥与端点配置。', + '翻译失败' + ); } finally { setTranslating(false); } @@ -221,16 +297,30 @@ export default function App() { const handleVectorize = async (bibcode: string) => { setVectorizing(true); try { - const res = await axios.post<{ chunk_count: number }>('/api/embed', { bibcode }); - - library.setLibrary(prev => prev.map(p => p.bibcode === bibcode ? { ...p, has_vector: true } : p)); + const res = await axios.post<{ chunk_count: number }>('/api/embed', { + bibcode, + }); + + library.setLibrary((prev) => + prev.map((p) => + p.bibcode === bibcode ? { ...p, has_vector: true } : p + ) + ); if (library.selectedPaper?.bibcode === bibcode) { - library.setSelectedPaper(prev => prev ? { ...prev, has_vector: true } : null); + library.setSelectedPaper((prev) => + prev ? { ...prev, has_vector: true } : null + ); } - showAlert(`文献已成功录入馆藏智能库,共提炼并录入 ${res.data.chunk_count} 个核心知识节点。`, '知识入库成功'); + showAlert( + `文献已成功录入馆藏智能库,共提炼并录入 ${res.data.chunk_count} 个核心知识节点。`, + '知识入库成功' + ); } catch (e) { console.error('知识入库失败', e); - showAlert('知识入库失败,请检查 .env 中的 Embedding API 配置。', '知识入库失败'); + showAlert( + '知识入库失败,请检查 .env 中的 Embedding API 配置。', + '知识入库失败' + ); } finally { setVectorizing(false); } @@ -245,7 +335,9 @@ export default function App() {
- 正在校验系统安全凭证... + + 正在校验系统安全凭证... +
); @@ -256,24 +348,30 @@ export default function App() {
- +
-

AstroResearch

-

天文学科研辅助系统 · 安全登录

+

+ AstroResearch +

+

+ 天文学科研辅助系统 · 安全登录 +

- +
auth.setPassword(e.target.value)} + onChange={(e) => auth.setPassword(e.target.value)} placeholder="请输入系统访问密码" autoFocus disabled={auth.loggingIn} @@ -314,11 +412,10 @@ export default function App() { // 5. 正常工作面板布局装配 return (
- {/* 导航左侧栏 */} -
- AstroResearch + + AstroResearch +
@@ -356,17 +455,27 @@ export default function App() { -
-
+
+
{activeTab === 'search' && ( - library.handleDownload(bibcode, force, search.setSearchResults)} + 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)} + onShowDetail={(paper) => + library.setDetailBibcode(paper.bibcode) + } /> )} {activeTab === 'library' && ( - library.setDetailBibcode(paper.bibcode)} + onShowDetail={(paper) => + library.setDetailBibcode(paper.bibcode) + } onOpenReader={openReader} - onOpenCitation={(paper) => library.openCitation(paper, setActiveTab, citations.loadCitations)} + 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 === 'reader' && ( - library.selectedPaper ? ( - notes.handleCreateNote(library.selectedPaper)} + handleCreateNote={() => + notes.handleCreateNote(library.selectedPaper) + } handleDeleteNote={notes.handleDeleteNote} showConfirm={showConfirm} onJumpToSource={handleJumpToSource} @@ -445,7 +592,9 @@ export default function App() {
-

未选定阅读文献

+

+ 未选定阅读文献 +

双语对比阅读功能需要基于已下载的馆藏文献。请前往"馆藏管理"选择或上传一篇文献,然后开启沉浸式双语精读。

@@ -456,16 +605,21 @@ export default function App() { 选择文献
- ) - )} + ))} - {activeTab === 'citation' && ( - library.selectedPaper ? ( - library.openCitation(paper, setActiveTab, citations.loadCitations)} + onSwitchPaper={(paper) => + library.openCitation( + paper, + setActiveTab, + citations.loadCitations + ) + } loadingCitations={citations.loadingCitations} citationNetwork={citations.citationNetwork} citationHistory={citations.citationHistory} @@ -477,9 +631,12 @@ export default function App() {
-

未选定中心文献

+

+ 未选定中心文献 +

- 引用星系拓扑图谱用于展示单篇文献的"引用 - 被引"星系脉络。请前往"馆藏管理"选择一篇文献并生成其引用图谱。 + 引用星系拓扑图谱用于展示单篇文献的"引用 - + 被引"星系脉络。请前往"馆藏管理"选择一篇文献并生成其引用图谱。

- ) - )} + ))} - {activeTab === 'sync' && ( - - )} + {activeTab === 'sync' && } {activeTab === 'agent' && ( - )} @@ -507,10 +665,7 @@ export default function App() { {/* 全局统一 Alert / Confirm 弹窗 */} - setDialog(null)} - /> + setDialog(null)} /> {/* 未入库文献操作引导弹窗 */} library.setDetailBibcode(null)} uploadingBibcode={library.uploadingBibcode} downloadingBibcodes={library.downloadingBibcodes} - handleManualUpload={(bibcode, type, file) => library.handleManualUpload(bibcode, type, file, search.setSearchResults)} - handleMarkNoResource={(bibcode, clear) => library.handleMarkNoResource(bibcode, clear, search.setSearchResults)} - handleDownload={(bibcode, force) => library.handleDownload(bibcode, force, search.setSearchResults)} + handleManualUpload={(bibcode, type, file) => + library.handleManualUpload( + bibcode, + type, + file, + search.setSearchResults + ) + } + handleMarkNoResource={(bibcode, clear) => + library.handleMarkNoResource(bibcode, clear, search.setSearchResults) + } + handleDownload={(bibcode, force) => + library.handleDownload(bibcode, force, search.setSearchResults) + } openReader={openReader} setActiveTab={setActiveTab} setSelectedPaper={library.setSelectedPaper} diff --git a/dashboard/src/components/AIAssistantPanel.tsx b/dashboard/src/components/AIAssistantPanel.tsx index 9b1804a..d1bc5a1 100644 --- a/dashboard/src/components/AIAssistantPanel.tsx +++ b/dashboard/src/components/AIAssistantPanel.tsx @@ -2,15 +2,29 @@ // 文献 AI 问答助手 — 使用共享 AgentMarkdown / useAgentSSE / 子代理路由 / 渲染组件 import { useState, useRef, useEffect } from 'react'; import { - Send, Loader, X, BookOpen, AlertCircle, Compass, - Brain, Square, Paperclip, + Send, + Loader, + X, + BookOpen, + AlertCircle, + Compass, + Brain, + Square, + Paperclip, } from 'lucide-react'; import axios from 'axios'; import { - AgentMarkdown, useAgentSSE, useAutoScroll, - ThoughtCard, ToolCallCard, SubAgentContainer, + AgentMarkdown, + useAgentSSE, + useAutoScroll, + ThoughtCard, + ToolCallCard, + SubAgentContainer, PARENT_COLORS, - routeThought, routeToolCall, routeToolResult, routeTextDelta, + routeThought, + routeToolCall, + routeToolResult, + routeTextDelta, } from './agent'; import type { SSEEventHandlers, TimelineItem } from './agent'; @@ -51,13 +65,22 @@ export function AIAssistantPanel({ const [sessionId, setSessionId] = useState(null); // 折叠状态(共享 ThoughtCard / ToolCallCard / SubAgentContainer 的展开控制) - const [expandedThoughts, setExpandedThoughts] = useState>({}); + const [expandedThoughts, setExpandedThoughts] = useState< + Record + >({}); const [expandedArgs, setExpandedArgs] = useState>({}); - const [expandedResults, setExpandedResults] = useState>({}); - const [collapsedSubAgents, setCollapsedSubAgents] = useState>({}); + const [expandedResults, setExpandedResults] = useState< + Record + >({}); + const [collapsedSubAgents, setCollapsedSubAgents] = useState< + Record + >({}); const [pendingImage, setPendingImage] = useState<{ - data?: string; path?: string; mime_type: string; name: string; + data?: string; + path?: string; + mime_type: string; + name: string; } | null>(null); const fileInputRef = useRef(null); @@ -67,13 +90,13 @@ export function AIAssistantPanel({ // ── 折叠控制 ── const toggleThought = (key: string) => - setExpandedThoughts(prev => ({ ...prev, [key]: !prev[key] })); + setExpandedThoughts((prev) => ({ ...prev, [key]: !prev[key] })); const toggleArgs = (tcId: string) => - setExpandedArgs(prev => ({ ...prev, [tcId]: !prev[tcId] })); + setExpandedArgs((prev) => ({ ...prev, [tcId]: !prev[tcId] })); const toggleResult = (tcId: string) => - setExpandedResults(prev => ({ ...prev, [tcId]: !prev[tcId] })); + setExpandedResults((prev) => ({ ...prev, [tcId]: !prev[tcId] })); const toggleSubAgent = (id: string) => - setCollapsedSubAgents(prev => ({ ...prev, [id]: !prev[id] })); + setCollapsedSubAgents((prev) => ({ ...prev, [id]: !prev[id] })); // ── 图片处理 ── const attachImage = (file: File) => { @@ -122,7 +145,11 @@ export function AIAssistantPanel({ const handleStop = async () => { sseStop(); if (sessionId) { - try { await axios.post(`/api/chat/sessions/${sessionId}/stop`); } catch { /* noop */ } + try { + await axios.post(`/api/chat/sessions/${sessionId}/stop`); + } catch { + /* noop */ + } } }; @@ -138,18 +165,23 @@ export function AIAssistantPanel({ text: questionText, timeline: [], imageUrl: figure - ? (figure.path - ? `/api/files/${figure.path}` - : `data:${figure.mime_type};base64,${figure.data}`) + ? figure.path + ? `/api/files/${figure.path}` + : `data:${figure.mime_type};base64,${figure.data}` : undefined, }; - setMessages(prev => [...prev, userMsg]); + setMessages((prev) => [...prev, userMsg]); setInput(''); setShouldAutoScroll(true); // AI 消息占位符(用 TimelineItem[] 替代旧 AgentStep[]) - const aiMsg: Message = { sender: 'ai', text: '', timeline: [], sources: [] }; - setMessages(prev => [...prev, aiMsg]); + const aiMsg: Message = { + sender: 'ai', + text: '', + timeline: [], + sources: [], + }; + setMessages((prev) => [...prev, aiMsg]); // ── SSE 事件处理器(使用共享子代理路由)── const handlers: SSEEventHandlers = { @@ -157,47 +189,79 @@ export function AIAssistantPanel({ if (!sessionId) setSessionId(sid); }, onThought: (step, content) => { - setMessages(prev => updateAiTimeline(prev, (timeline) => - routeThought(timeline, step, content), - )); + setMessages((prev) => + updateAiTimeline(prev, (timeline) => + routeThought(timeline, step, content) + ) + ); }, onToolCall: (step, _id, name, args) => { - setMessages(prev => updateAiTimeline(prev, (timeline) => - routeToolCall(timeline, step, _id, name, args), - )); + setMessages((prev) => + updateAiTimeline(prev, (timeline) => + routeToolCall(timeline, step, _id, name, args) + ) + ); }, onToolResult: (_step, tcId, name, _output, isError, metadata) => { - setExpandedResults(prev => ({ ...prev, [tcId]: true })); - setMessages(prev => { + setExpandedResults((prev) => ({ ...prev, [tcId]: true })); + setMessages((prev) => { if (prev.length === 0) return prev; const last = prev[prev.length - 1]; if (last.sender !== 'ai') return prev; - const newTimeline = routeToolResult(last.timeline, tcId, name, _output, isError); + const newTimeline = routeToolResult( + last.timeline, + tcId, + name, + _output, + isError + ); // RAG 来源收集 const sources = [...(last.sources || [])]; - if (name === 'rag_search' && metadata?.sources && Array.isArray(metadata.sources)) { - for (const s of metadata.sources as Array<{ bibcode: string; paragraph_index: number; preview?: string; distance?: number }>) { - if (!sources.some( - c => c.bibcode === s.bibcode && c.paragraph_index === s.paragraph_index, - )) { + if ( + name === 'rag_search' && + metadata?.sources && + Array.isArray(metadata.sources) + ) { + for (const s of metadata.sources as Array<{ + bibcode: string; + paragraph_index: number; + preview?: string; + distance?: number; + }>) { + if ( + !sources.some( + (c) => + c.bibcode === s.bibcode && + c.paragraph_index === s.paragraph_index + ) + ) { sources.push({ - bibcode: s.bibcode, paragraph_index: s.paragraph_index, - content: s.preview || '', distance: s.distance ?? 0, + bibcode: s.bibcode, + paragraph_index: s.paragraph_index, + content: s.preview || '', + distance: s.distance ?? 0, }); } } } - return [...prev.slice(0, -1), { ...last, timeline: newTimeline, sources }]; + return [ + ...prev.slice(0, -1), + { ...last, timeline: newTimeline, sources }, + ]; }); }, onTextDelta: (content, toolCallId) => { if (toolCallId) { - setExpandedResults(prev => ({ ...prev, [toolCallId]: true })); + setExpandedResults((prev) => ({ ...prev, [toolCallId]: true })); } - setMessages(prev => { + setMessages((prev) => { const last = prev[prev.length - 1]; if (last?.sender !== 'ai') return prev; - const { timeline, answerDelta } = routeTextDelta(last.timeline, content, toolCallId); + const { timeline, answerDelta } = routeTextDelta( + last.timeline, + content, + toolCallId + ); return [ ...prev.slice(0, -1), { ...last, timeline, text: last.text + answerDelta }, @@ -211,7 +275,8 @@ export function AIAssistantPanel({ const contextPrefix = `[当前正在阅读文献: ${bibcode}] `; const requestQuery = questionText.includes(bibcode) - ? questionText : `${contextPrefix}${questionText}`; + ? questionText + : `${contextPrefix}${questionText}`; let imagePayload: { data: string; mime_type: string } | undefined; if (figure?.data) { @@ -227,12 +292,16 @@ export function AIAssistantPanel({ ? { data: imagePayload.data, mime_type: imagePayload.mime_type } : undefined, }, - handlers, + handlers ); }; // ── 时间线条目渲染(委托到共享组件)── - const renderTimelineItem = (item: TimelineItem, idx: number, isStreaming: boolean) => { + const renderTimelineItem = ( + item: TimelineItem, + idx: number, + isStreaming: boolean + ) => { switch (item.type) { case 'subagent_container': return ( @@ -305,7 +374,8 @@ export function AIAssistantPanel({

文献阅读助手

- 专注文献精读与理解的 AI 助手。支持语义检索、逐段解读、公式分析,严格只读模式保障文献安全。 + 专注文献精读与理解的 AI + 助手。支持语义检索、逐段解读、公式分析,严格只读模式保障文献安全。

@@ -363,7 +433,7 @@ export function AIAssistantPanel({
{msg.timeline.map((item, tIdx) => - renderTimelineItem(item, tIdx, !!streaming), + renderTimelineItem(item, tIdx, !!streaming) )}
@@ -376,7 +446,9 @@ export function AIAssistantPanel({
) : ( !streaming && ( - 正在构建最终结论... + + 正在构建最终结论... + ) )}
@@ -393,13 +465,17 @@ export function AIAssistantPanel({ {msg.sources.map((src, sIdx) => ( {isOpen && !disabled && ( -
- {options.map(option => { +
+ {options.map((option) => { const isSelected = option.value === value; return (
- 作者: {paper.authors.slice(0, 2).join(', ')}{paper.authors.length > 2 ? ' 等' : ''} | 发表年份: {paper.year} + 作者: {paper.authors.slice(0, 2).join(', ')} + {paper.authors.length > 2 ? ' 等' : ''} | 发表年份: {paper.year}
@@ -67,11 +68,15 @@ export function PaperCard({ return (
{/* Selection indicator side bar */} - {isSelected &&
} + {isSelected && ( +
+ )}
@@ -83,7 +88,17 @@ export function PaperCard({ {paper.title}

- 作者: {paper.authors.slice(0, 5).join(', ')}{paper.authors.length > 5 ? ' 等' : ''} • 年份: {paper.year} • 出版期刊: {paper.pub_journal || '未标注'} + 作者:{' '} + + {paper.authors.slice(0, 5).join(', ')} + {paper.authors.length > 5 ? ' 等' : ''} + {' '} + • 年份:{' '} + {paper.year} • + 出版期刊:{' '} + + {paper.pub_journal || '未标注'} +

@@ -99,11 +114,7 @@ export function PaperCard({ {/* Footer Area */} {(footerLeft || footerRight) && (
- {footerLeft && ( -
- {footerLeft} -
- )} + {footerLeft &&
{footerLeft}
} {footerRight && (
{footerRight} @@ -114,4 +125,3 @@ export function PaperCard({
); } - diff --git a/dashboard/src/components/agent/AgentInputArea.tsx b/dashboard/src/components/agent/AgentInputArea.tsx index 4bc3488..32460b3 100644 --- a/dashboard/src/components/agent/AgentInputArea.tsx +++ b/dashboard/src/components/agent/AgentInputArea.tsx @@ -1,6 +1,12 @@ import React from 'react'; import { - Brain, Compass, BookOpen, Send, Square, Paperclip, X + Brain, + Compass, + BookOpen, + Send, + Square, + Paperclip, + X, } from 'lucide-react'; import type { AgentMode } from '../../hooks/useResearchAgent'; @@ -13,8 +19,20 @@ interface AgentInputAreaProps { agentMode: string; setAgentMode: (val: string) => void; agentModes: AgentMode[]; - pendingImage: { data?: string; path?: string; mime_type: string; name: string } | null; - setPendingImage: (val: { data?: string; path?: string; mime_type: string; name: string } | null) => void; + pendingImage: { + data?: string; + path?: string; + mime_type: string; + name: string; + } | null; + setPendingImage: ( + val: { + data?: string; + path?: string; + mime_type: string; + name: string; + } | null + ) => void; onSend: (text: string) => void; onStop: () => void; fileInputRef: React.RefObject; @@ -25,10 +43,12 @@ interface AgentInputAreaProps { const ICON_MAP: Record> = { Brain, Compass, - BookOpen + BookOpen, }; -function getIconComponent(iconName: string): React.ComponentType<{ className?: string }> { +function getIconComponent( + iconName: string +): React.ComponentType<{ className?: string }> { return ICON_MAP[iconName] || Brain; } @@ -63,7 +83,11 @@ export function AgentInputArea({ {pendingImage && (
Preview @@ -119,7 +143,7 @@ export function AgentInputArea({ > - + {/* Agent 模式选择器 */}
{agentModes.map((m) => { @@ -138,7 +162,9 @@ export function AgentInputArea({ : 'bg-transparent border-transparent text-slate-400 hover:text-blueprint' } disabled:opacity-60`} > - + {m.name} ); @@ -153,14 +179,20 @@ export function AgentInputArea({ type="button" onClick={() => setThinking(!thinking)} disabled={streaming} - title={thinking ? '思考模式已开启(启用 LLM 推理过程)' : '思考模式已关闭(点击开启)'} + title={ + thinking + ? '思考模式已开启(启用 LLM 推理过程)' + : '思考模式已关闭(点击开启)' + } className={`px-2.5 py-1.5 rounded-lg border text-[10px] font-bold transition-all cursor-pointer flex items-center gap-1 shrink-0 ${ thinking ? 'bg-blueprint/5 border-blueprint/30 text-blueprint shadow-3xs' : 'bg-white border-slate-200 text-slate-400 hover:text-blueprint hover:border-blueprint/20' } disabled:opacity-60`} > - + 思考 @@ -168,7 +200,7 @@ export function AgentInputArea({
- {streaming ? ( + {streaming ? ( )}
@@ -255,22 +315,28 @@ export function AgentMessageList({ 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 ${ - (hasPendingPermission || hasPendingQuestion) ? 'pb-80' : 'pb-36' + hasPendingPermission || hasPendingQuestion ? 'pb-80' : 'pb-36' }`} > {turns.length === 0 && !activeTurn ? (
-

智能天体物理科研助手

+

+ 智能天体物理科研助手 +

- 具备 NASA ADS 跨文献库检索、PDF/HTML 原文结构化解析、SQLite-Vec 本地文献 RAG 语义检索、以及 CDS Sesame 真实天体参数解析等专业级工具。 + 具备 NASA ADS 跨文献库检索、PDF/HTML 原文结构化解析、SQLite-Vec + 本地文献 RAG 语义检索、以及 CDS Sesame + 真实天体参数解析等专业级工具。

{/* 快速提示词 */}
- 推荐研究提问方向: + + 推荐研究提问方向: +
{QUICK_PROMPTS.map((q, idx) => ( ))}
@@ -292,7 +362,9 @@ export function AgentMessageList({
{/* 用户提问 */}
- + + 我 +
{turn.questionMessageId && (
@@ -315,7 +391,9 @@ export function AgentMessageList({ {/* 时间线 — 思考/工具/答案 */} {turn.timeline.length > 0 && (
- {turn.timeline.map((item: TimelineItem, idx: number) => renderTimelineItem(item, idx, false))} + {turn.timeline.map((item: TimelineItem, idx: number) => + renderTimelineItem(item, idx, false) + )}
)} @@ -333,10 +411,16 @@ export function AgentMessageList({
{/* 当前提问 */}
- + + 我 +
{activeTurn.imagePath && ( - 用户上传的图片 + 用户上传的图片 )}
{activeTurn.question}
@@ -345,7 +429,10 @@ export function AgentMessageList({ {/* SSE Stream Timeline */} {activeTurn.timeline.length > 0 && (
- {activeTurn.timeline.map((item: TimelineItem, idx: number) => renderTimelineItem(item, idx, true))} + {activeTurn.timeline.map( + (item: TimelineItem, idx: number) => + renderTimelineItem(item, idx, true) + )}
)} @@ -366,7 +453,9 @@ export function AgentMessageList({ {streaming && !activeTurn.finalAnswer && !activeTurn.error && (
- 智能体正在搜集信息进行推理分析... + + 智能体正在搜集信息进行推理分析... +
)} @@ -376,7 +465,9 @@ export function AgentMessageList({
查询发生错误
-
{activeTurn.error}
+
+ {activeTurn.error} +
)} @@ -431,11 +522,15 @@ export function AgentMessageList({ loadSessionHistory(currentSessionId, true); } }} - onQuestionCountChange={(count: number) => setHasPendingQuestion(count > 0)} + onQuestionCountChange={(count: number) => + setHasPendingQuestion(count > 0) + } /> setHasPendingPermission(count > 0)} + onRequestCountChange={(count: number) => + setHasPendingPermission(count > 0) + } />
diff --git a/dashboard/src/components/agent/AgentMetricsPanel.tsx b/dashboard/src/components/agent/AgentMetricsPanel.tsx index b9577b7..405a68d 100644 --- a/dashboard/src/components/agent/AgentMetricsPanel.tsx +++ b/dashboard/src/components/agent/AgentMetricsPanel.tsx @@ -1,7 +1,15 @@ // dashboard/src/features/agent/AgentMetricsPanel.tsx import { useState, useEffect } from 'react'; import axios from 'axios'; -import { BarChart3, Activity, AlertTriangle, Zap, Brain, RefreshCw, Loader } from 'lucide-react'; +import { + BarChart3, + Activity, + AlertTriangle, + Zap, + Brain, + RefreshCw, + Loader, +} from 'lucide-react'; import type { AgentMetricsResponse } from '../../types'; interface AgentMetricsPanelProps { @@ -83,7 +91,10 @@ export function AgentMetricsPanel({ showAlert }: AgentMetricsPanelProps) { setMetrics(res.data); } catch (e) { console.error('获取智能体指标失败:', e); - showAlert?.('获取智能体运行指标失败,请确认后端服务状态。', '指标加载出错'); + showAlert?.( + '获取智能体运行指标失败,请确认后端服务状态。', + '指标加载出错' + ); } finally { setLoading(false); } @@ -98,13 +109,18 @@ export function AgentMetricsPanel({ showAlert }: AgentMetricsPanelProps) { if (active) setMetrics(res.data); } catch (e) { console.error('获取智能体指标失败:', e); - showAlert?.('获取智能体运行指标失败,请确认后端服务状态。', '指标加载出错'); + showAlert?.( + '获取智能体运行指标失败,请确认后端服务状态。', + '指标加载出错' + ); } finally { if (active) setLoading(false); } })(); - return () => { active = false; }; - }, []); + return () => { + active = false; + }; + }, [showAlert]); // 提取工具调用排行(取前10) const toolBreakdown = metrics?.tool_call_breakdown @@ -131,7 +147,9 @@ export function AgentMetricsPanel({ showAlert }: AgentMetricsPanelProps) { 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" title="刷新指标" > - +
@@ -183,11 +201,16 @@ export function AgentMetricsPanel({ showAlert }: AgentMetricsPanelProps) {
{toolBreakdown.map(([name, count]) => { const barWidth = Math.max((count / maxToolCalls) * 100, 2); - const colorClass = CATEGORY_COLORS[name] || 'bg-slate-100 text-slate-700 border-slate-200'; + const colorClass = + CATEGORY_COLORS[name] || + 'bg-slate-100 text-slate-700 border-slate-200'; const label = TOOL_LABELS[name] || name; return (
- + {label}
@@ -212,7 +235,9 @@ export function AgentMetricsPanel({ showAlert }: AgentMetricsPanelProps) { 按功能域分布
- {Object.entries(getCategoryCounts(metrics.tool_call_breakdown)).map(([category, count]) => ( + {Object.entries( + getCategoryCounts(metrics.tool_call_breakdown) + ).map(([category, count]) => ( +
{icon} - {label} -
-
- {value} + + {label} +
+
{value}
); } // 按功能域分组统计 -function getCategoryCounts(breakdown: Record): Record { +function getCategoryCounts( + breakdown: Record +): Record { const categories: Record = { - '文件系统': ['read_file', 'grep_files', 'glob_files', 'run_bash', 'file_write', 'file_edit'], - '文献科研': ['search_papers', 'get_paper_metadata', 'download_paper', 'parse_paper', 'get_paper_content'], + 文件系统: [ + 'read_file', + 'grep_files', + 'glob_files', + 'run_bash', + 'file_write', + 'file_edit', + ], + 文献科研: [ + 'search_papers', + 'get_paper_metadata', + 'download_paper', + 'parse_paper', + 'get_paper_content', + ], 'RAG/天体': ['rag_search', 'query_target', 'save_note'], - 'Agent控制': ['todo_write', 'compress_context', 'load_skill', 'subagent', 'delegate_research', 'ask_user'], - '记忆系统': ['save_memory', 'load_memory'], - '后台任务': ['bg_task_run', 'bg_task_check'], - '团队协作': ['spawn_teammate', 'send_teammate_message', 'team_broadcast', 'check_team_inbox'], + Agent控制: [ + 'todo_write', + 'compress_context', + 'load_skill', + 'subagent', + 'delegate_research', + 'ask_user', + ], + 记忆系统: ['save_memory', 'load_memory'], + 后台任务: ['bg_task_run', 'bg_task_check'], + 团队协作: [ + 'spawn_teammate', + 'send_teammate_message', + 'team_broadcast', + 'check_team_inbox', + ], }; const result: Record = {}; diff --git a/dashboard/src/components/agent/AgentSessionSidebar.tsx b/dashboard/src/components/agent/AgentSessionSidebar.tsx index 8d665a1..414f213 100644 --- a/dashboard/src/components/agent/AgentSessionSidebar.tsx +++ b/dashboard/src/components/agent/AgentSessionSidebar.tsx @@ -1,8 +1,19 @@ import React from 'react'; import { - Clock, Plus, PanelLeftClose, Search, X, MessageSquare, BookOpen, Trash2, Loader + Clock, + Plus, + PanelLeftClose, + Search, + X, + MessageSquare, + BookOpen, + Trash2, + Loader, } from 'lucide-react'; -import type { SessionSummary, SearchResult } from '../../hooks/useResearchAgent'; +import type { + SessionSummary, + SearchResult, +} from '../../hooks/useResearchAgent'; interface AgentSessionSidebarProps { sessions: SessionSummary[]; @@ -37,7 +48,6 @@ export function AgentSessionSidebar({ onNewSession, onDeleteSession, }: AgentSessionSidebarProps) { - const handleSelectSession = (id: string | null) => { setCurrentSessionId(id); if (typeof window !== 'undefined' && window.innerWidth < 1024) { @@ -64,11 +74,11 @@ export function AgentSessionSidebar({ /> )} -
@@ -101,7 +111,7 @@ export function AgentSessionSidebar({ setSearchQuery(e.target.value)} + 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" /> @@ -122,7 +132,7 @@ export function AgentSessionSidebar({ setSearchScopeOnlyCurrent(e.target.checked)} + onChange={(e) => setSearchScopeOnlyCurrent(e.target.checked)} className="rounded text-blueprint border-slate-300 focus:ring-blueprint w-3 h-3 cursor-pointer" /> 仅搜索当前会话 @@ -145,7 +155,9 @@ export function AgentSessionSidebar({ searchResults.map((result, idx) => { const isActive = result.session_id === currentSessionId; const isMessage = result.result_type.startsWith('message/'); - const role = isMessage ? result.result_type.split('/')[1] : null; + const role = isMessage + ? result.result_type.split('/')[1] + : null; return (
- + {result.title || '无标题会话'} @@ -190,49 +202,48 @@ export function AgentSessionSidebar({ ); }) ) + ) : // 正常的会话列表渲染 + loadingSessions ? ( +
+ + 加载历史会话中... +
+ ) : sessions.length === 0 ? ( +
+ 暂无历史会话记录 +
) : ( - // 正常的会话列表渲染 - loadingSessions ? ( -
- - 加载历史会话中... -
- ) : sessions.length === 0 ? ( -
- 暂无历史会话记录 -
- ) : ( - sessions.map(session => { - const isActive = session.session_id === currentSessionId; - return ( + sessions.map((session) => { + const isActive = session.session_id === currentSessionId; + return ( + + - ); - }) - ) + + ); + }) )}
diff --git a/dashboard/src/components/agent/AnswerCard.tsx b/dashboard/src/components/agent/AnswerCard.tsx index f86b99d..723321f 100644 --- a/dashboard/src/components/agent/AnswerCard.tsx +++ b/dashboard/src/components/agent/AnswerCard.tsx @@ -10,7 +10,11 @@ interface AnswerCardProps { isStreaming?: boolean; } -export function AnswerCard({ content, colorScheme, isStreaming }: AnswerCardProps) { +export function AnswerCard({ + content, + colorScheme, + isStreaming, +}: AnswerCardProps) { return (
结论
- - {content} - + {content}
diff --git a/dashboard/src/components/agent/AskUserQuestionCard.tsx b/dashboard/src/components/agent/AskUserQuestionCard.tsx index 7a90e9b..6a7da0d 100644 --- a/dashboard/src/components/agent/AskUserQuestionCard.tsx +++ b/dashboard/src/components/agent/AskUserQuestionCard.tsx @@ -9,8 +9,13 @@ interface AskUserQuestionCardProps { onQuestionCountChange?: (count: number) => void; } -export function AskUserQuestionCard({ onAnswered, onQuestionCountChange }: AskUserQuestionCardProps) { - const [pendingQuestions, setPendingQuestions] = useState([]); +export function AskUserQuestionCard({ + onAnswered, + onQuestionCountChange, +}: AskUserQuestionCardProps) { + const [pendingQuestions, setPendingQuestions] = useState( + [] + ); const [answers, setAnswers] = useState>({}); const [freeText, setFreeText] = useState>({}); const [submitting, setSubmitting] = useState>({}); @@ -28,7 +33,7 @@ export function AskUserQuestionCard({ onAnswered, onQuestionCountChange }: AskUs const data = Array.isArray(res.data) ? res.data : []; setPendingQuestions(data); // 自动展开新问题 - setExpanded(prev => { + setExpanded((prev) => { const next = { ...prev }; for (const q of data) { if (q && q.question_id && !(q.question_id in next)) { @@ -56,14 +61,18 @@ export function AskUserQuestionCard({ onAnswered, onQuestionCountChange }: AskUs onQuestionCountChange?.(pendingQuestions.length); }, [pendingQuestions.length, onQuestionCountChange]); - const toggleOption = (questionId: string, optionLabel: string, multiSelect: boolean) => { - setAnswers(prev => { + const toggleOption = ( + questionId: string, + optionLabel: string, + multiSelect: boolean + ) => { + setAnswers((prev) => { const current = prev[questionId] || []; if (multiSelect) { return { ...prev, [questionId]: current.includes(optionLabel) - ? current.filter(o => o !== optionLabel) + ? current.filter((o) => o !== optionLabel) : [...current, optionLabel], }; } else { @@ -73,8 +82,8 @@ export function AskUserQuestionCard({ onAnswered, onQuestionCountChange }: AskUs }; const handleSubmit = async (questionId: string) => { - setSubmitting(prev => ({ ...prev, [questionId]: true })); - setError(prev => ({ ...prev, [questionId]: null })); + setSubmitting((prev) => ({ ...prev, [questionId]: true })); + setError((prev) => ({ ...prev, [questionId]: null })); try { await axios.post('/api/chat/answer', { question_id: questionId, @@ -82,19 +91,21 @@ export function AskUserQuestionCard({ onAnswered, onQuestionCountChange }: AskUs free_text: freeText[questionId] || null, }); // 移除已回答的问题 - setPendingQuestions(prev => prev.filter(q => q.question_id !== questionId)); + setPendingQuestions((prev) => + prev.filter((q) => q.question_id !== questionId) + ); // 清理状态 - setAnswers(prev => { + setAnswers((prev) => { const next = { ...prev }; delete next[questionId]; return next; }); - setFreeText(prev => { + setFreeText((prev) => { const next = { ...prev }; delete next[questionId]; return next; }); - setError(prev => { + setError((prev) => { const next = { ...prev }; delete next[questionId]; return next; @@ -103,27 +114,30 @@ export function AskUserQuestionCard({ onAnswered, onQuestionCountChange }: AskUs } catch (e: unknown) { console.error('提交答案失败:', e); const axiosError = e as { response?: { status?: number } }; - const msg = axiosError.response?.status === 410 - ? '该问题已超时或已被回答' - : axiosError.response?.status === 404 - ? '未找到该问题' - : '提交失败,请稍后重试'; - setError(prev => ({ ...prev, [questionId]: msg })); + const msg = + axiosError.response?.status === 410 + ? '该问题已超时或已被回答' + : axiosError.response?.status === 404 + ? '未找到该问题' + : '提交失败,请稍后重试'; + setError((prev) => ({ ...prev, [questionId]: msg })); } finally { - setSubmitting(prev => ({ ...prev, [questionId]: false })); + setSubmitting((prev) => ({ ...prev, [questionId]: false })); } }; const dismissQuestion = (questionId: string) => { - setPendingQuestions(prev => prev.filter(q => q.question_id !== questionId)); - setExpanded(prev => ({ ...prev, [questionId]: false })); + setPendingQuestions((prev) => + prev.filter((q) => q.question_id !== questionId) + ); + setExpanded((prev) => ({ ...prev, [questionId]: false })); }; if (pendingQuestions.length === 0) return null; return (
- {pendingQuestions.map(q => { + {pendingQuestions.map((q) => { if (!q || !q.question_id) return null; const isExpanded = expanded[q.question_id] !== false; @@ -144,7 +158,10 @@ export function AskUserQuestionCard({ onAnswered, onQuestionCountChange }: AskUs {q.header || '提问'} - + {q.question || ''}
@@ -152,7 +169,12 @@ export function AskUserQuestionCard({ onAnswered, onQuestionCountChange }: AskUs {options.length > 0 && (