From 36a8ba8400fa4969704c1792bbaac0b0d9e232a0 Mon Sep 17 00:00:00 2001 From: Research Assistant Date: Sun, 10 May 2026 13:44:50 +0800 Subject: [PATCH 01/37] refactor(dashboard): Phase 1 cleanup - remove Copy Context, deep-reading dead code, static Quick Actions grid - Delete Copy Context and Copy Collection Context from ACTIONS array - Remove all _renderDeep* dead methods (StatusCard, Pass1Card, QACard) - Remove _getPassCompletion and _extractPass1Content helpers - Remove deep-reading mode case from _renderModeHeader - Remove Copy Context handlers from _runAction - Remove copy-context command palette registrations - Delete static Quick Actions section from _buildPanel - Delete _renderActions method - Replace ready-state Copy Context fallback in _renderNextStepCard - Add implementation plan at docs/superpowers/plans/ --- .../plans/2026-05-10-dashboard-redesign.md | 459 ++++++++++++++++++ paperforge/plugin/main.js | 235 +-------- paperforge/plugin/src/testable.js | 16 - 3 files changed, 461 insertions(+), 249 deletions(-) create mode 100644 docs/superpowers/plans/2026-05-10-dashboard-redesign.md diff --git a/docs/superpowers/plans/2026-05-10-dashboard-redesign.md b/docs/superpowers/plans/2026-05-10-dashboard-redesign.md new file mode 100644 index 00000000..32516205 --- /dev/null +++ b/docs/superpowers/plans/2026-05-10-dashboard-redesign.md @@ -0,0 +1,459 @@ +# PaperForge Dashboard 重设计 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 将 PaperForge Dashboard 从 "功能展示墙" 重构为 "工作流控制台",只保留 global / collection / paper 三个视图,删除 Copy Context 类按钮和 dead deep-reading 代码。 + +**Architecture:** 3 个模式(global=系统首页, collection=批量工作台, paper=阅读辅助侧栏)。Workspace 检测用 `dirname(filePath)` 提取 zotero_key。精读内容从 formal note 正文 `## 🔍 精读` section 直接读取。Discussion 卡片做 UI 壳+空态(数据尚未生产)。Actions 由全局静态网格改为各视图内 contextual 按钮。 + +**Tech Stack:** Vanilla Obsidian DOM API (`createEl`, `addClass`, `setText`), Node.js fs/path, single-file plugin (`main.js`) + +--- + +## 文件结构 + +| 文件 | 改动类型 | 职责 | +|------|---------|------| +| `paperforge/plugin/main.js` | 大量修改 | 所有渲染方法、路由、actions | +| `paperforge/plugin/src/testable.js` | 小改 | 删除 Copy Context actions | +| `paperforge/plugin/styles.css` | 中等改动 | 新组件样式 | +| `docs/ux-contract.md` | 更新 | workflow specification | + +--- + +## Vault 实际数据结构(来自 D:\L\Med\test1\ 勘探) + +``` +Workspace 路径: Resources/Literature/{domain}/{zotero_key} - {title}/ +Formal note: {workspace}/{zotero_key} - {title}.md +Fulltext: {workspace}/fulltext.md (OCR 后的副本) +AI dir: {workspace}/ai/ (目前为空,无 discussion.json) +``` + +**Workspace 检测策略(用户指定):** `dirname(filePath)` → 正则 `^([A-Z0-9]{8})` 提取 zotero_key + +**精读内容格式(来自 3EWBBTAS formal note 正文):** +``` +## 🔍 精读 +### Pass 1: 概览 + **一句话总览**: ... + **5 Cs 快速评估**: ... + **Figure 导读**: ... +### Pass 2: 精读还原 + ... +### Pass 3: 深度理解 + ... +``` + +**Discussion 现状:** 不存在。所有 `ai/` 目录为空。做 UI 壳+空态。 + +--- + +### Task 1: 清理 testable.js — 删除 Copy Context actions + +**Files:** +- Modify: `paperforge/plugin/src/testable.js:172-187` + +- [ ] **Step 1: 从 ACTIONS 数组中删除两个 Copy Context action** + +删除 `paperforge-copy-context` 和 `paperforge-copy-collection-context` 两个 action 对象(lines 172-187)。保留其余 4 个 action(sync, ocr, doctor, repair)。注意删除后数组尾部逗号清理。 + +- [ ] **Step 2: 运行测试确认** + +```bash +cd paperforge/plugin && npx vitest run --reporter=verbose +``` + +预期:所有现有测试通过(ACTIONS 数组长度从 6 变为 4)。 + +--- + +### Task 2: 清理 main.js — 删除 dead deep-reading 代码 + +**Files:** +- Modify: `paperforge/plugin/main.js` + +- [ ] **Step 1: 删除 `_renderDeepStatusCard` 方法 (lines 1222-1245)** + +- [ ] **Step 2: 删除 `_getPassCompletion` 方法 (lines 1247-1251)** + +- [ ] **Step 3: 删除 `_renderDeepPass1Card` 方法 (lines 1253-1280)** + +- [ ] **Step 4: 删除 `_extractPass1Content` 方法 (lines 1282-1299)** + +- [ ] **Step 5: 删除 `_renderDeepQACard` 方法 (lines 1301-1342)** + +- [ ] **Step 6: 更新 `_currentMode` 注释 (line 375)** + +将 `'global' | 'paper' | 'collection' | 'deep-reading'` 改为 `'global' | 'paper' | 'collection'` + +--- + +### Task 3: 清理 main.js — 删除 Copy Context UI + 静态 Quick Actions 网格 + +**Files:** +- Modify: `paperforge/plugin/main.js` + +- [ ] **Step 1: 从 `_renderPaperMode` 删除 Copy Context 按钮 (lines 1073-1080)** + +删除 ctxBtn 创建和事件监听。 + +- [ ] **Step 2: 从 `_buildPanel` 删除静态 Quick Actions section (lines 461-465)** + +删除 `.paperforge-actions-section` 的创建、`this._actionsGrid` 引用、`this._renderActions()` 调用。 + +- [ ] **Step 3: 删除 `_renderActions` 方法 (lines 937-949)** + +- [ ] **Step 4: 从 `_runAction` 删除两个 Copy Context handler (lines 1478-1526)** + +删除 `paperforge-copy-context` 和 `paperforge-copy-collection-context` 的纯 JS clipboard 处理分支。 + +- [ ] **Step 5: 从 `_renderModeHeader` 删除 deep-reading case (lines 1703-1714)** + +- [ ] **Step 6: 从 Plugin.onload 删除 Copy Context 命令注册 (lines 2881-2913)** + +- [ ] **Step 7: 从 `_renderNextStepCard` 删除 ready 状态的 Copy Context fallback (lines 1195-1203)** + +--- + +### Task 4: 修复 mode routing — workspace path 检测 + +**Files:** +- Modify: `paperforge/plugin/main.js:_resolveModeForFile (lines 957-978)` + +- [ ] **Step 1: 添加 `_extractZoteroKeyFromPath(filePath)` 辅助方法** + +在 `_resolveModeForFile` 前新增方法。逻辑: +1. `dirname = path.dirname(filePath)` +2. `basename = path.basename(dirname)` +3. 正则 `^([A-Z0-9]{8})(\s*-\s*)` 提取第一个 token +4. 返回 zotero_key 或 null + +- [ ] **Step 2: 修改 `_resolveModeForFile`** + +在 `.md` 分支的 `zotero_key` 检测之后,新增 fallback:如果是 `.md` 且无 frontmatter zotero_key,调用 `_extractZoteroKeyFromPath(filePath)` 检测。如果提取到 key 且在 index 中存在,返回 `{ mode: 'paper', ... }`。 + +同时在 `ext` 判断的最后(return global 之前),对任意文件类型也做一次 workspace path 检测(用于 fulltext.md 等非 formal note 文件的归属)。 + +完整逻辑: +```js +_resolveModeForFile(file) { + if (!file) return { mode: 'global', filePath: null, key: null, domain: null }; + const ext = file.extension; + const filePath = file.path; + + if (ext === 'base') { + return { mode: 'collection', filePath, key: null, domain: file.basename }; + } + + if (ext === 'md') { + const cache = this.app.metadataCache.getFileCache(file); + const fmKey = cache && cache.frontmatter && cache.frontmatter.zotero_key; + if (fmKey) { + return { mode: 'paper', filePath, key: fmKey, domain: null }; + } + } + + // Workspace path detection for any file + const wsKey = this._extractZoteroKeyFromPath(filePath); + if (wsKey && this._findEntry(wsKey)) { + return { mode: 'paper', filePath, key: wsKey, domain: null }; + } + + return { mode: 'global', filePath, key: null, domain: null }; +} +``` + +--- + +### Task 5: 重做 Global 视图 — 系统首页 + +**Files:** +- Modify: `paperforge/plugin/main.js:_renderGlobalMode (lines 1024-1044)` + +- [ ] **Step 1: 重写 `_renderGlobalMode`** + +新布局: +```text +[Drift Banner] (hidden by default) +[Library Snapshot]: papers · PDFs ready · OCR done · deep-read done +[System Status]: runtime ✓ · index ✓ · Zotero export ✓ · OCR token ✓ +[Issues Panel]: 仅有问题时显示 +[Actions]: Open Literature Hub · Sync Library +``` + +实现: +1. Header + version + 总体状态一行(`系统正常` / `检测到 N 个问题`) +2. Library Snapshot: 从 `_fetchStats` 获取数据,显示 4 个 compact metric pill(papers, PDFs, OCR done, deep-read done) +3. System Status: 4 行 status pill(runtime match/mismatch, index healthy/corrupt, Zotero export detected/missing, OCR token configured/missing) +4. Issues Panel: 当 path_errors > 0 或 runtime mismatch 时显示,包含 `Run Doctor` / `Repair Issues` 按钮 +5. Contextual Actions: `Open Literature Hub` (+ 如果全库 queued OCR > 0 显示 `Run OCR`) + +- [ ] **Step 2: 删除从 global 的 `_renderOcr` 引用和 OCR section DOM 构建** + +不再在 `_renderGlobalMode` 中创建 `this._ocrSection`、`this._ocrBadge`、`this._ocrTrack`、`this._ocrCounts`。 + +- [ ] **Step 3: 调整 `_renderStats` 中的 metric cards** + +从 "Papers / Formal Notes / Exports" 3 张卡改为只保留对用户有意义的总量。数据源改为从 `this._getCachedIndex()` 单次聚合(不再依赖 CLI dashboard --json)。 + +聚合逻辑(复用 `_fallbackFetchStats` 中的部分代码): +- papers = items.length +- pdfReady = items.filter(i => i.has_pdf).length +- ocrDone = items.filter(i => i.ocr_status === 'done').length +- deepReadDone = items.filter(i => i.deep_reading_status === 'done').length + +--- + +### Task 6: 重做 Collection/Base 视图 — 批量工作台 + +**Files:** +- Modify: `paperforge/plugin/main.js:_renderCollectionMode (lines 1344-1410)` + +- [ ] **Step 1: 重写 `_renderCollectionMode`** + +新布局: +```text +[Header]: 其他 · 150 papers +[Workflow Overview]: 150 total → 148 PDF → 8 OCR → 8 analyze-ready → 1 deep-read +[OCR Pipeline]: 待处理142 | 处理中0 | 已完成8 | 需处理0 [████████░░] [Run OCR] +[Next Work]: 待OCR: 142 · 可进入精读: 7 · 精读完成: 1 +[Issue Summary]: 紧凑版,仅在有问题时展开详情 +[Actions]: Sync Library · Run OCR +``` + +**单次聚合逻辑:** +```js +const items = domainItems; +const total = items.length; +const pdfReady = items.filter(i => i.has_pdf).length; +const ocrDone = items.filter(i => i.ocr_status === 'done').length; +const analyzeReady = items.filter(i => i.ocr_status === 'done' && i.analyze === true).length; +const deepReadDone = items.filter(i => i.deep_reading_status === 'done').length; + +// OCR pipeline 4-bucket aggregation +const ocrPending = items.filter(i => ['pending','queued'].includes(i.ocr_status)).length; +const ocrProcessing = items.filter(i => i.ocr_status === 'processing').length; +const ocrDone2 = items.filter(i => i.ocr_status === 'done').length; +const ocrAttention = items.filter(i => ['failed','blocked','done_incomplete','nopdf'].includes(i.ocr_status)).length; +``` + +- [ ] **Step 2: 实现 `_renderWorkflowOverview` 辅助方法(或在 render 内部直接构建)** + +Workflow 漏斗使用 5 个横向串联的 stage pill。 + +- [ ] **Step 3: 实现 base 内的 OCR Pipeline(复用 `_renderOcr` 的进度条逻辑)** + +4-bucket 进度条 + count labels。需要单独的容器引用(不与 global 共享 `this._ocrSection`)。 + +- [ ] **Step 4: 实现 Issue Summary 紧凑版** + +默认显示 "暂无阻塞问题"。有异常时显示 `N 篇 PDF 缺失 · N 篇 OCR 失败 · N 篇 Asset drift`。 + +- [ ] **Step 5: 重写 Contextual Actions** + +base 模式 actions 区域:`Sync Library` + `Run OCR`。维护动作仅在异常时出现。 + +--- + +### Task 7: 重做 Per-paper 视图 — 阅读辅助侧栏 + +**Files:** +- Modify: `paperforge/plugin/main.js:_renderPaperMode + 相关方法 (lines 1047-1204)` + +- [ ] **Step 1: 重写 `_renderPaperMode`** + +新布局: +```text +[Header]: title · authors · year +[Status Strip]: PDF ✓ OCR ✓ 精读 已完成 +[文章概览]: Pass 1 一句话总览 / abstract / 空态 +[最近讨论]: 最新 session 最近 2-3 条 Q&A / 空态隐藏 +[下一步]: 精读已完成 · 可继续讨论 +[文件]: Open PDF · Open Fulltext +[技术详情]: 默认折叠(PDF health, OCR raw status, asset state, paths) +``` + +- [ ] **Step 2: 实现 `_renderPaperStatusStrip(container, entry)`** + +删除 lifecycle stepper + health matrix + maturity gauge,压缩为一行 compact status pills: + +状态判断: +- PDF: `has_pdf` ? '✓ 可用' : '✗ 缺失' +- OCR: `ocr_status === 'done'` ? '✓ 完成' : `['pending','queued'].includes(ocr_status)` ? '待处理' : `ocr_status === 'processing'` ? '处理中' : '✗ 异常' +- 精读: `deep_reading_status === 'done'` ? '✓ 完成' : '待完成' + +- [ ] **Step 3: 实现 `_renderPaperOverviewCard(container, entry)`** + +数据源:formal note 正文(通过 `app.vault.read(noteFile)` 读取) + +提取逻辑: +1. 如果正文包含 `## 🔍 精读`,在 section 内找 `**一句话总览**` 或 `**文章摘要**` +2. Fallback:显示 entry 中的 `abstract`(如果 frontmatter 有) +3. 最终 fallback:显示 "尚未生成文章概览" + +默认只显示短摘要(截断 200 字符),展开按钮显示更多。 + +- [ ] **Step 4: 实现 `_renderRecentDiscussionCard(container, entry)`** + +数据源:`{workspace}/ai/discussion.json`(通过 `app.vault.adapter.read()` 读取) + +Discussion.json 格式(来自 2Y9M3ILK 实际数据): +```json +{ "sessions": [{ "agent":"pf-paper", "model":"deepseek-v4-pro", "qa_pairs": [{"question": "...", "answer": "..."}] }] } +``` +回答可能较长(500+ 中文字符)。 + +渲染: +- 无数据时:整张卡隐藏 +- 有数据时:取 `sessions[-1]` 最近 session,展示最近 2-3 个 `qa_pairs` +- 每条显示:`提问:完整问题` + `解答:{截断150字符}... [展开]` +- 点击 [展开] 后显示完整回答(inline expand,不跳转) +- 底部 "查看全部讨论 →" 链接(打开 discussion.md) +- 截断函数:中文按字符计,英文按词计,保证不会在 Obsidian wikilink 中间截断 + +- [ ] **Step 5: 实现 `_renderPaperNextStepCard(container, entry)`** + +简化版下一步推荐(删除 lifecycle maturity 视角): +``` +无 PDF → "等待 PDF" +有 PDF 未入 OCR → "可加入 OCR 队列" +OCR pending/queued/processing → "OCR 进行中" +OCR failed/blocked → "OCR 需要处理" +OCR 完成 精读未完成 → "可开始精读" +精读完成 → "已完成 · 可继续讨论" +``` + +- [ ] **Step 6: 实现 `_renderPaperFilesRow(container, entry)`** + +两个按钮:`Open PDF`(使用 pdf_path wikilink)、`Open Fulltext`(使用 fulltext_path) + +- [ ] **Step 7: 实现 `_renderPaperTechnicalDetails(container, entry)`** + +默认折叠的 section,包含:PDF health, OCR raw status, asset state, note path, fulltext path。仅在异常时展开或用户手动展开。 + +--- + +### Task 8: 更新 CSS 样式 + +**Files:** +- Modify: `paperforge/plugin/styles.css` + +- [ ] **Step 1: 添加新组件样式** + +```css +/* Status Strip — compact horizontal pills */ +.paperforge-status-strip { ... } +.paperforge-status-pill { ... } /* pill.ok / pill.warn / pill.fail */ + +/* Paper Overview Card */ +.paperforge-paper-overview { ... } +.paperforge-paper-overview-excerpt { ... } + +/* Recent Discussion Card */ +.paperforge-discussion-card { ... } +.paperforge-discussion-item { ... } + +/* Paper Files Row */ +.paperforge-paper-files { ... } + +/* Paper Technical Details (collapsible) */ +.paperforge-technical-details { ... } +.paperforge-technical-details-toggle { ... } + +/* Workflow Overview (base) */ +.paperforge-workflow-overview { ... } +.paperforge-workflow-stage { ... } + +/* Issue Summary (base + global) */ +.paperforge-issue-summary { ... } + +/* Library Snapshot (global) */ +.paperforge-library-snapshot { ... } + +/* System Status (global) */ +.paperforge-system-status { ... } +``` + +- [ ] **Step 2: 清理旧样式** + +保持以下样式的兼容性(因为其他模式可能仍复用部分结构): +- `.paperforge-header`, `.paperforge-mode-badge` +- `.paperforge-paper-view`, `.paperforge-paper-header`, `.paperforge-paper-title` +- `.paperforge-collection-view` +- `.paperforge-metrics`, `.paperforge-metric-card`(global 仍用) +- `.paperforge-ocr-section`, `.paperforge-progress-track`, `.paperforge-ocr-counts`(base 用) + +--- + +### Task 9: 更新 UX contract 文档 + +**Files:** +- Modify: `docs/ux-contract.md` + +- [ ] **Step 1: 更新 Workflow 4 — Dashboard View** + +更新 W4-S3 per-paper dashboard 描述:从 "lifecycle stepper, health matrix, next-step recommendation" 改为 "status strip, paper overview, recent discussion, next-step, files row, technical details"。 + +更新 W4-S2 添加 workflow overview columns 描述。 + +- [ ] **Step 2: 添加全局和 collection 视图描述** + +添加 Workflow 4 新的 sub-steps 描述 global(系统首页)和 collection(批量工作台)。 + +--- + +### Task 10: 验证 + +- [ ] **Step 1: 在 Obsidian 中加载插件** + +1. 用 Symbolic link 或直接复制插件文件到 test vault 的 `.obsidian/plugins/paperforge/` +2. 打开 Obsidian,确认 PaperForge 图标出现在左侧 sidebar +3. 点击图标打开 Dashboard + +- [ ] **Step 2: 验证 mode routing** + +- 不打开任何文件 → global mode ✓ +- 打开 `.base` 文件 → collection mode ✓ +- 打开 formal note → paper mode ✓ +- 打开 `fulltext.md` → paper mode ✓(workspace detection) +- 打开 workshop 目录下其他文件 → paper mode ✓ + +- [ ] **Step 3: 验证 global 视图** + +- Library snapshot 显示计数正确 +- System status 显示 runtime/index/Zotero/OCR token 状态 +- Issues 仅在有问题时出现 +- Actions 有 Open Literature Hub + Sync Library + +- [ ] **Step 4: 验证 base 视图** + +- Workflow overview 漏斗显示正确 +- OCR pipeline 进度条对应当前 base +- Issue summary 紧凑显示 +- No Copy Collection Context + +- [ ] **Step 5: 验证 paper 视图** + +- Status strip 显示正确 +- 文章概览从 formal note 提取内容 +- Discussion 卡有数据时显示,无数据时隐藏 +- 下一步推荐文字正确 +- 技术详情默认折叠 +- No Copy Context + +- [ ] **Step 6: 检查控制台** + +确认无 JS 错误、无 console.error。 + +--- + +## 非目标(明确不做的) + +1. 不做 Copy Context / Copy Collection Context +2. 不做独立 deep-reading dashboard +3. 不让每个 view 挂同一套 Quick Actions +4. 不把 global 做成第二个 collection overview +5. 不在 per-paper 保留 lifecycle stepper / health matrix / maturity gauge +6. 不放所有健康指标全部展开 +7. 不在 dashboard 里弥补 skill 检索能力不足 diff --git a/paperforge/plugin/main.js b/paperforge/plugin/main.js index edab7739..afffcab9 100644 --- a/paperforge/plugin/main.js +++ b/paperforge/plugin/main.js @@ -372,7 +372,7 @@ function patchEntryWorkflowState(entry, patch) { class PaperForgeStatusView extends ItemView { constructor(leaf) { super(leaf); - this._currentMode = null; // 'global' | 'paper' | 'collection' | 'deep-reading' (D-05) + this._currentMode = null; // 'global' | 'paper' | 'collection' this._currentDomain = null; // domain name when in collection mode (D-15) this._currentPaperKey = null; // zotero_key when in per-paper mode (D-03) this._currentPaperEntry = null; // full entry when in per-paper mode @@ -458,11 +458,6 @@ class PaperForgeStatusView extends ItemView { /* ── Mode-Switched Content Area (per D-06) ── */ this._contentEl = root.createEl('div', { cls: 'paperforge-content-area' }); - /* ── Quick Actions (visible in all modes per D-07 / "Specific Ideas") ── */ - const actions = root.createEl('div', { cls: 'paperforge-actions-section' }); - actions.createEl('h4', { cls: 'paperforge-actions-title', text: 'Quick Actions' }); - this._actionsGrid = actions.createEl('div', { cls: 'paperforge-actions-grid' }); - this._renderActions(); // extracted so actions can be re-rendered per mode } /* ---------------------------------------------------------------------- */ @@ -934,19 +929,6 @@ class PaperForgeStatusView extends ItemView { } } - /* ── Render Quick Actions (extracted from _buildPanel for mode-aware reuse) ── */ - _renderActions() { - this._actionsGrid.empty(); - for (const a of ACTIONS) { - const card = this._actionsGrid.createEl('div', { cls: 'paperforge-action-card' }); - if (a.disabled) card.addClass('disabled'); - card.createEl('div', { cls: 'paperforge-action-card-icon', text: a.icon }); - card.createEl('div', { cls: 'paperforge-action-card-title', text: a.title }); - card.createEl('div', { cls: 'paperforge-action-card-desc', text: a.desc }); - card.createEl('div', { cls: 'paperforge-action-card-hint', text: a.disabled ? 'Coming soon' : 'Click to run' }); - card.addEventListener('click', () => this._runAction(a, card)); - } - } /* ── Invalidate cached index (D-14) ── */ _invalidateIndex() { @@ -1069,16 +1051,6 @@ class PaperForgeStatusView extends ItemView { // ============ Contextual Action Buttons Row (D-10, D-11, D-12, D-13) ============ const actionsRow = view.createEl('div', { cls: 'paperforge-paper-actions' }); - - // "Copy Context" button (D-11) — reuse existing paperforge-copy-context action - const ctxBtn = actionsRow.createEl('button', { cls: 'paperforge-contextual-btn' }); - ctxBtn.createEl('span', { cls: 'paperforge-contextual-btn-icon', text: '\u2139' }); - ctxBtn.createEl('span', { text: 'Copy Context' }); - ctxBtn.addEventListener('click', () => { - const action = ACTIONS.find(a => a.id === 'paperforge-copy-context'); - if (action) this._runAction(action, ctxBtn); - }); - // "Open Fulltext" button (D-12) — open fulltext.md in Obsidian if (entry.fulltext_path) { const ftBtn = actionsRow.createEl('button', { cls: 'paperforge-contextual-btn' }); @@ -1193,13 +1165,8 @@ class PaperForgeStatusView extends ItemView { const labelEl = card.createEl('div', { cls: 'paperforge-agent-platform-label' }); labelEl.setText(t('run_in_agent').replace('{0}', platform)); } else if (nextStep === 'ready') { - // Fall back to existing Copy Context button. const trigger = card.createEl('button', { cls: 'paperforge-next-step-trigger' }); - trigger.createEl('span', { text: '\u2139 Copy Context' }); - trigger.addEventListener('click', () => { - const action = ACTIONS.find(a => a.id === 'paperforge-copy-context'); - if (action) this._runAction(action, trigger); - }); + trigger.createEl('span', { text: '✓ ' + info.label }); } } @@ -1219,127 +1186,10 @@ class PaperForgeStatusView extends ItemView { } } - /* ── Deep-Reading Status Card ── */ - _renderDeepStatusCard(container, entry) { - const card = container.createEl('div', { cls: 'paperforge-deepreading-card' }); - card.createEl('div', { cls: 'paperforge-deepreading-card-title', text: '\uD83D\uDCCA \u72B6\u6001\u6982\u89C8' }); - if (!entry) { - card.createEl('div', { cls: 'paperforge-deepreading-empty', text: '\u6682\u65E0\u6570\u636E' }); - return; - } - const statusItems = [ - { label: 'Figure-Map', value: entry.figure_map ? '\u2705 \u5DF2\u751F\u6210' : '\u23F3 \u5F85\u751F\u6210' }, - { label: 'OCR \u72B6\u6001', value: entry.ocr_status === 'done' ? '\u2705 \u5DF2\u5B8C\u6210' : '\u23F3 ' + (entry.ocr_status || '\u5F85\u5904\u7406') }, - { label: 'Pass \u5B8C\u6210', value: this._getPassCompletion(entry) }, - { label: '\u5065\u5EB7\u72B6\u51B5', value: entry.health && entry.health.pdf_health === 'healthy' ? '\u2705 \u6B63\u5E38' : '\u26A0\uFE0F \u9700\u5173\u6CE8' }, - ]; - const list = card.createEl('div', { cls: 'paperforge-deepreading-status-list' }); - for (const item of statusItems) { - const row = list.createEl('div', { cls: 'paperforge-deepreading-status-row' }); - row.createEl('span', { cls: 'paperforge-deepreading-status-label', text: item.label }); - row.createEl('span', { cls: 'paperforge-deepreading-status-value', text: item.value }); - } - } - _getPassCompletion(entry) { - const status = entry.deep_reading_status || 'pending'; - if (status === 'done') return '\u2705 3/3 (\u5DF2\u5B8C\u6210)'; - return '\u23F3 \u5F85\u5B8C\u6210'; - } - - /* ── Deep-Reading Pass 1 Summary Card ── */ - _renderDeepPass1Card(container, text) { - const card = container.createEl('div', { cls: 'paperforge-deepreading-card' }); - card.createEl('div', { cls: 'paperforge-deepreading-card-title', text: '\uD83D\uDCDD Pass 1 \u603B\u7ED3' }); - - if (!text || text.trim() === '') { - card.createEl('div', { cls: 'paperforge-deepreading-empty', text: '\u6682\u65E0 Pass 1 \u603B\u7ED3' }); - return; - } - - const extracted = this._extractPass1Content(text); - if (!extracted) { - card.createEl('div', { cls: 'paperforge-deepreading-empty', text: '\u6682\u65E0 Pass 1 \u603B\u7ED3' }); - return; - } - - const content = card.createEl('div', { cls: 'paperforge-deepreading-pass1-content' }); - const lines = extracted.split('\n').filter(l => l.trim()); - for (const line of lines) { - if (line.startsWith('### ')) { - content.createEl('h4', { cls: 'paperforge-deepreading-pass1-subheading', text: line.replace('### ', '') }); - } else if (line.startsWith('**') && line.endsWith('**')) { - content.createEl('p', { cls: 'paperforge-deepreading-pass1-marker', text: line }); - } else if (line.trim()) { - content.createEl('p', { cls: 'paperforge-deepreading-pass1-text', text: line }); - } - } - } - - _extractPass1Content(text) { - const markers = ['**\u4E00\u53E5\u8BDD\u603B\u89C8**', '## Pass 1', '**\u6587\u7AE0\u6458\u8981**']; - for (const marker of markers) { - const idx = text.indexOf(marker); - if (idx !== -1) { - const after = idx + marker.length; - // Cut at next major section marker - const cutMarkers = ['**\u8BC1\u636E\u8FB9\u754C**', '**Figure \u5BFC\u8BFB**', '**\u4E3B\u8981\u53D1\u73B0**']; - let nextCut = text.length; - for (const cm of cutMarkers) { - const ci = text.indexOf(cm, after); - if (ci !== -1 && ci < nextCut) nextCut = ci; - } - return text.substring(after, nextCut).trim(); - } - } - return null; - } - - /* ── Deep-Reading AI Q&A History Card ── */ - _renderDeepQACard(container, data) { - const card = container.createEl('div', { cls: 'paperforge-deepreading-card' }); - - // D-10: Collapsible header - const header = card.createEl('div', { cls: 'paperforge-deepreading-card-header collapsible' }); - header.createEl('div', { cls: 'paperforge-deepreading-card-title', text: '\uD83D\uDCAC AI \u95EE\u7B54\u8BB0\u5F55' }); - - const body = card.createEl('div', { cls: 'paperforge-deepreading-card-body collapsed' }); - - // Toggle collapse - header.addEventListener('click', () => { - body.classList.toggle('collapsed'); - }); - - // D-12: Empty states - if (!data || !data.sessions || data.sessions.length === 0) { - body.createEl('div', { cls: 'paperforge-deepreading-empty', text: !data ? '\u6682\u65E0\u8BA8\u8BBA\u8BB0\u5F55' : '\u6682\u65E0\u95EE\u7B54\u5185\u5BB9' }); - return; - } - - // D-08: Sessions-based grouping - for (const session of data.sessions) { - const sessionEl = body.createEl('div', { cls: 'paperforge-deepreading-session' }); - const sessionHeader = sessionEl.createEl('div', { cls: 'paperforge-deepreading-session-header' }); - sessionHeader.createEl('span', { text: session.model || 'AI' }); - sessionHeader.createEl('span', { cls: 'paperforge-deepreading-session-date', text: session.started || '' }); - - // D-09: Dialog bubbles - if (session.qa_pairs) { - for (const qa of session.qa_pairs) { - const qBubble = sessionEl.createEl('div', { cls: 'paperforge-deepreading-bubble question' }); - qBubble.createEl('div', { cls: 'bubble-label', text: '\u95EE\u9898' }); - qBubble.createEl('div', { cls: 'bubble-text', text: qa.question }); - - const aBubble = sessionEl.createEl('div', { cls: 'paperforge-deepreading-bubble answer' }); - aBubble.createEl('div', { cls: 'bubble-label', text: '\u89E3\u7B54' }); - aBubble.createEl('div', { cls: 'bubble-text', text: qa.answer }); - } - } - } - } /* ── Collection Mode Render (Phase 30) ── */ _renderCollectionMode() { @@ -1475,42 +1325,6 @@ class PaperForgeStatusView extends ItemView { modal.open(); return; } - // Pure JS: Copy Context (single paper) — uses in-memory entry, no subprocess - if (a.id === 'paperforge-copy-context') { - let entry = this._currentPaperEntry; - if (!entry) { - const file = this.app.workspace.getActiveFile(); - if (file) { - const cache = this.app.metadataCache.getFileCache(file); - const key = cache?.frontmatter?.zotero_key; - if (key) entry = this._findEntry(key); - } - } - if (entry) { - navigator.clipboard.writeText(JSON.stringify(entry, null, 2)).then(() => { - this._showMessage('[OK] ' + (entry.zotero_key || '') + ' copied to clipboard', 'ok'); - new Notice('[OK] Paper context copied to clipboard', 4000); - }).catch(e => { - this._showMessage('[!!] Clipboard failed: ' + e.message, 'error'); - }); - } else { - this._showMessage('[!!] No paper entry found', 'error'); - new Notice('[!!] Open a paper note or select one in the dashboard first', 5000); - } - return; - } - - // Pure JS: Copy Collection Context — uses cached index, no subprocess - if (a.id === 'paperforge-copy-collection-context') { - const items = this._cachedItems || []; - navigator.clipboard.writeText(JSON.stringify(items, null, 2)).then(() => { - this._showMessage('[OK] ' + items.length + ' entries copied to clipboard', 'ok'); - new Notice('[OK] ' + items.length + ' papers copied to clipboard', 4000); - }).catch(e => { - this._showMessage('[!!] Clipboard failed: ' + e.message, 'error'); - }); - return; - } // Guard: disabled actions show coming-soon notice if (a.disabled) { @@ -1687,18 +1501,6 @@ class PaperForgeStatusView extends ItemView { modeName = this._currentDomain || 'Unknown Domain'; break; - case 'deep-reading': - badge.addClass('deep-reading'); - badge.setText('Deep'); - this._headerTitle.setText('Deep Reading'); - if (this._currentPaperEntry && this._currentPaperEntry.title) { - modeName = this._currentPaperEntry.title; - } else if (this._currentPaperKey) { - modeName = this._currentPaperKey; - } else { - modeName = 'Unknown paper'; - } - break; } if (modeName) { @@ -2865,39 +2667,6 @@ module.exports = class PaperForgePlugin extends Plugin { }); } - /* ── Command palette: context actions (use view\u2019s _runAction for key resolution) ── */ - this.addCommand({ - id: 'paperforge-copy-context', - name: 'Copy Context: Copy active paper\u2019s index entry JSON to clipboard', - callback: () => { - const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_PAPERFORGE); - if (leaves.length > 0 && leaves[0].view instanceof PaperForgeStatusView) { - const action = ACTIONS.find(a => a.id === 'paperforge-copy-context'); - if (action) { - const tempCard = document.createElement('div'); - leaves[0].view._runAction(action, tempCard); - } - } else { - new Notice('[!!] Open PaperForge Dashboard first (click sidebar icon)', 5000); - } - }, - }); - this.addCommand({ - id: 'paperforge-copy-collection-context', - name: 'Copy Collection Context: Copy all canonical index entries to clipboard', - callback: () => { - const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_PAPERFORGE); - if (leaves.length > 0 && leaves[0].view instanceof PaperForgeStatusView) { - const action = ACTIONS.find(a => a.id === 'paperforge-copy-collection-context'); - if (action) { - const tempCard = document.createElement('div'); - leaves[0].view._runAction(action, tempCard); - } - } else { - new Notice('[!!] Open PaperForge Dashboard first (click sidebar icon)', 5000); - } - }, - }); /* ── Auto-update PaperForge (deferred — don't slow startup) ── */ if (this.settings.auto_update !== false) { diff --git a/paperforge/plugin/src/testable.js b/paperforge/plugin/src/testable.js index 891f0e9b..1f099e91 100644 --- a/paperforge/plugin/src/testable.js +++ b/paperforge/plugin/src/testable.js @@ -169,22 +169,6 @@ const ACTIONS = [ args: ["--fix", "--fix-paths"], okMsg: "Repair complete", }, - { - id: "paperforge-copy-context", - title: "Copy Context", - desc: "Copy this paper\u2019s canonical index entry JSON to clipboard for AI use", - icon: "\u2139", - cmd: "context", - okMsg: "Context copied", - }, - { - id: "paperforge-copy-collection-context", - title: "Copy Collection Context", - desc: "Copy canonical index entries for all visible papers to clipboard", - icon: "\u2261", - cmd: "context", - okMsg: "Collection context copied", - }, ]; function buildCommandArgs(action, key, filter) { From 0475387c5514620e046cfe27b60c85fe1504e3c6 Mon Sep 17 00:00:00 2001 From: Research Assistant Date: Sun, 10 May 2026 13:46:18 +0800 Subject: [PATCH 02/37] fix(dashboard): add workspace path detection for paper mode routing - Add _extractZoteroKeyFromPath: extract key from dirname pattern '{KEY} - {title}' - Modify _resolveModeForFile: fallback to workspace detection for any file type - Fixes fulltext.md and other workspace files dropping to global mode --- paperforge/plugin/main.js | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/paperforge/plugin/main.js b/paperforge/plugin/main.js index afffcab9..f5d943d1 100644 --- a/paperforge/plugin/main.js +++ b/paperforge/plugin/main.js @@ -935,6 +935,15 @@ class PaperForgeStatusView extends ItemView { this._cachedItems = null; } + /* ── Extract zotero_key from workspace directory name ── */ + _extractZoteroKeyFromPath(filePath) { + if (!filePath) return null; + const dirname = path.dirname(filePath); + const basename = path.basename(dirname); + const match = basename.match(/^([A-Z0-9]{8})(?:\s*-\s*)/); + return match ? match[1] : null; + } + /* ── Pure Mode Resolution (D-07, Phase 32) ── */ _resolveModeForFile(file) { if (!file) return { mode: 'global', filePath: null, key: null, domain: null }; @@ -947,13 +956,17 @@ class PaperForgeStatusView extends ItemView { } if (ext === 'md') { - // Standard .md — check for zotero_key in frontmatter. const cache = this.app.metadataCache.getFileCache(file); - const key = cache && cache.frontmatter && cache.frontmatter.zotero_key; - if (key) { - return { mode: 'paper', filePath, key, domain: null }; + const fmKey = cache && cache.frontmatter && cache.frontmatter.zotero_key; + if (fmKey) { + return { mode: 'paper', filePath, key: fmKey, domain: null }; } - return { mode: 'global', filePath, key: null, domain: null }; + } + + // Workspace path detection: any file inside a paper workspace directory + const wsKey = this._extractZoteroKeyFromPath(filePath); + if (wsKey && this._findEntry(wsKey)) { + return { mode: 'paper', filePath, key: wsKey, domain: null }; } return { mode: 'global', filePath, key: null, domain: null }; From c39ee8fa60803045ae52e6de8aa8fc72026e7aff Mon Sep 17 00:00:00 2001 From: Research Assistant Date: Sun, 10 May 2026 13:50:01 +0800 Subject: [PATCH 03/37] refactor(dashboard): redesign per-paper view as reading companion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace lifecycle stepper + health matrix + maturity gauge with compact status strip - Add paper overview card: extract Pass 1 summary from formal note ## 🔍 精读 - Add recent discussion card: read ai/discussion.json with 150-char truncation + expand - Add files row: Open PDF + Open Fulltext - Add technical details section: collapsed by default - Keep OCR queue toggle and simplified next-step card --- paperforge/plugin/main.js | 276 +++++++++++++++++++++++++++++++++----- 1 file changed, 239 insertions(+), 37 deletions(-) diff --git a/paperforge/plugin/main.js b/paperforge/plugin/main.js index f5d943d1..9edc63a1 100644 --- a/paperforge/plugin/main.js +++ b/paperforge/plugin/main.js @@ -1038,20 +1038,17 @@ class PaperForgeStatusView extends ItemView { this._fetchStats(); } - /* ── Per-Paper Mode Render (D-01 through D-09, Phase 29) ── */ + /* ── Per-Paper Mode Render: Reading Companion ── */ _renderPaperMode() { const entry = this._currentPaperEntry; const key = this._currentPaperKey; - // --- Handle loading / empty / not-found states (D-03) --- if (!key) { - // No key at all (defensive fallback) this._renderEmptyState(this._contentEl, 'No paper data available.'); return; } if (!entry) { - // Key exists but entry not found in index (D-18) this._contentEl.createEl('div', { cls: 'paperforge-content-placeholder', text: 'Paper "' + key + '" not found in canonical index. Sync first.', @@ -1059,44 +1056,29 @@ class PaperForgeStatusView extends ItemView { return; } - // --- Create per-paper view container (D-04 through D-09) --- const view = this._contentEl.createEl('div', { cls: 'paperforge-paper-view' }); - // ============ Contextual Action Buttons Row (D-10, D-11, D-12, D-13) ============ - const actionsRow = view.createEl('div', { cls: 'paperforge-paper-actions' }); - // "Open Fulltext" button (D-12) — open fulltext.md in Obsidian - if (entry.fulltext_path) { - const ftBtn = actionsRow.createEl('button', { cls: 'paperforge-contextual-btn' }); - ftBtn.createEl('span', { cls: 'paperforge-contextual-btn-icon', text: '\uD83D\uDCC4' }); - ftBtn.createEl('span', { text: 'Open Fulltext' }); - ftBtn.addEventListener('click', () => this._openFulltext(entry.fulltext_path)); - } - - // ============ Paper Metadata Header (D-04) ============ + // ── Header: title, authors, year ── const header = view.createEl('div', { cls: 'paperforge-paper-header' }); - const titleText = entry.title || 'Untitled'; - header.createEl('div', { cls: 'paperforge-paper-title', text: titleText }); - + header.createEl('div', { cls: 'paperforge-paper-title', text: entry.title || 'Untitled' }); const meta = header.createEl('div', { cls: 'paperforge-paper-meta' }); if (entry.authors && entry.authors.length > 0) { - const authorsStr = entry.authors.join(', '); - meta.createEl('span', { cls: 'paperforge-paper-authors', text: authorsStr }); + meta.createEl('span', { cls: 'paperforge-paper-authors', text: entry.authors.join(', ') }); } if (entry.year) { meta.createEl('span', { cls: 'paperforge-paper-year', text: String(entry.year) }); } - // ============ Lifecycle Stepper (D-05) ============ - this._renderLifecycleStepper(view, entry, entry.lifecycle); + // ── Status Strip: PDF · OCR · DeepRead ── + this._renderPaperStatusStrip(view, entry); - // ============ Health Matrix (D-06) ============ - this._renderHealthMatrix(view, entry.health); + // ── Paper Overview: Pass 1 summary from note body ── + this._renderPaperOverviewCard(view, entry); - // ============ Maturity Gauge (D-07) ============ - const maturity = entry.maturity || {}; - this._renderMaturityGauge(view, maturity.level, maturity.blocking); + // ── Recent Discussion ── + this._renderRecentDiscussionCard(view, entry); - // ============ OCR Queue Control (DASH-01) ============ + // ── OCR Queue Toggle ── const ocrRow = view.createEl('div', { cls: 'paperforge-paper-actions' }); const inQueue = entry.do_ocr === true; const ocrBtn = ocrRow.createEl('button', { cls: 'paperforge-contextual-btn' }); @@ -1109,23 +1091,243 @@ class PaperForgeStatusView extends ItemView { return; } const newValue = !inQueue; - await this.app.fileManager.processFrontMatter(noteFile, (fm) => { - fm.do_ocr = newValue; - }); + await this.app.fileManager.processFrontMatter(noteFile, (fm) => { fm.do_ocr = newValue; }); this._patchCachedEntry(key, { do_ocr: newValue }); this._currentPaperEntry = patchEntryWorkflowState(this._currentPaperEntry, { do_ocr: newValue }); new Notice(newValue ? t('ocr_queue_added') : t('ocr_queue_removed')); this._refreshCurrentMode(); }); if (inQueue) { - ocrRow.createEl('span', { - cls: 'paperforge-ocr-queue-hint', - text: 'OCR ' + (entry.ocr_status === 'done' ? 'already done' : 'pending'), - }); + ocrRow.createEl('span', { cls: 'paperforge-ocr-queue-hint', text: 'OCR ' + (entry.ocr_status === 'done' ? 'already done' : 'pending') }); } - // ============ Next-Step Recommendation Card (D-08, D-09) ============ + // ── Next Step ── this._renderNextStepCard(view, entry, key); + + // ── Files Row ── + this._renderPaperFilesRow(view, entry); + + // ── Technical Details (collapsed) ── + this._renderPaperTechnicalDetails(view, entry); + } + + /* ── Paper Status Strip: compact pills ── */ + _renderPaperStatusStrip(container, entry) { + const strip = container.createEl('div', { cls: 'paperforge-status-strip' }); + const items = [ + { key: 'pdf', label: 'PDF', ok: entry.has_pdf === true }, + { key: 'ocr', label: 'OCR', + ok: entry.ocr_status === 'done', + pending: ['pending','queued','processing'].includes(entry.ocr_status || ''), + fail: ['failed','blocked','done_incomplete','nopdf'].includes(entry.ocr_status || '') }, + { key: 'deep', label: '精读', ok: entry.deep_reading_status === 'done' }, + ]; + for (const item of items) { + const pill = strip.createEl('span', { cls: 'paperforge-status-pill' }); + let statusCls = 'pending'; + if (item.ok) statusCls = 'ok'; + else if (item.fail) statusCls = 'fail'; + else if (item.pending) statusCls = 'pending'; + pill.addClass(statusCls); + const icon = item.ok ? '\u2713' : (item.fail ? '\u2717' : '\u25CB'); + pill.createEl('span', { cls: 'paperforge-status-pill-icon', text: icon }); + pill.createEl('span', { text: ' ' + item.label }); + } + } + + /* ── Paper Overview Card: read from formal note body ── */ + _renderPaperOverviewCard(container, entry) { + const card = container.createEl('div', { cls: 'paperforge-paper-overview' }); + const header = card.createEl('div', { cls: 'paperforge-paper-overview-header' }); + header.createEl('span', { cls: 'paperforge-paper-overview-title', text: '文章概览' }); + const body = card.createEl('div', { cls: 'paperforge-paper-overview-body' }); + const excerptEl = body.createEl('div', { cls: 'paperforge-paper-overview-excerpt', text: '加载中...' }); + + // Async read formal note body + if (entry.note_path) { + const noteFile = this.app.vault.getAbstractFileByPath(entry.note_path); + if (noteFile) { + this.app.vault.read(noteFile).then((content) => { + const extracted = this._extractOverviewFromNote(content); + if (extracted) { + const truncated = extracted.length > 200 ? extracted.slice(0, 200) + '...' : extracted; + excerptEl.setText(truncated); + if (extracted.length > 200) { + const expandBtn = body.createEl('button', { cls: 'paperforge-paper-overview-expand', text: '展开' }); + let expanded = false; + expandBtn.addEventListener('click', () => { + excerptEl.setText(expanded ? truncated : extracted); + expandBtn.setText(expanded ? '展开' : '收起'); + expanded = !expanded; + }); + } + } else { + excerptEl.setText('尚未生成文章概览。运行 /pf-deep 开始精读。'); + } + }).catch(() => { + excerptEl.setText('无法读取笔记内容'); + }); + } else { + excerptEl.setText('笔记文件不存在'); + } + } else { + excerptEl.setText('尚未生成文章概览'); + } + } + + /* ── Extract overview from formal note body ── */ + _extractOverviewFromNote(content) { + if (!content) return null; + // Find "## 🔍 精读" section + const deepIdx = content.indexOf('## 🔍 精读'); + if (deepIdx === -1) return null; + const section = content.slice(deepIdx); + // Extract "**一句话总览**" or "**文章摘要**" or "**一句话总览:**" + const markers = ['**一句话总览:**', '**一句话总览**', '**文章摘要:**', '**文章摘要**']; + for (const marker of markers) { + const idx = section.indexOf(marker); + if (idx !== -1) { + const after = section.slice(idx + marker.length); + // Cut at next marker or double newline + const cutMarkers = ['**5 Cs', '**Figure', '**证据', '### Pass 2', '## ']; + let nextCut = after.length; + for (const cm of cutMarkers) { + const ci = after.indexOf(cm); + if (ci !== -1 && ci < nextCut) nextCut = ci; + } + // Also stop at double newline + const nnIdx = after.indexOf('\n\n'); + if (nnIdx !== -1 && nnIdx < nextCut) nextCut = nnIdx; + let text = after.slice(0, nextCut).trim(); + // Clean up leading ** if marker didn't include trailing ** + if (text.startsWith('**')) text = text.slice(2); + if (text.endsWith('**')) text = text.slice(0, -2); + return text || null; + } + } + // Fallback: return first paragraph of the deep reading section + const firstNewline = section.indexOf('\n'); + if (firstNewline === -1) return null; + const para = section.slice(firstNewline + 1).split('\n\n')[0].trim(); + // Skip empty or header-only lines + if (!para || para.startsWith('###') || para.startsWith('##')) return null; + return para.length > 300 ? para.slice(0, 300) + '...' : para; + } + + /* ── Recent Discussion Card: read ai/discussion.json ── */ + _renderRecentDiscussionCard(container, entry) { + const card = container.createEl('div', { cls: 'paperforge-discussion-card' }); + card.style.display = 'none'; // hidden by default + + if (!entry.note_path) return; + const wsDir = path.dirname(entry.note_path); + const discPath = wsDir + '/ai/discussion.json'; + + this.app.vault.adapter.read(discPath).then((raw) => { + const data = JSON.parse(raw); + if (!data.sessions || data.sessions.length === 0) return; + + card.style.display = 'block'; + const header = card.createEl('div', { cls: 'paperforge-discussion-header' }); + header.createEl('span', { cls: 'paperforge-discussion-title', text: '最近讨论' }); + + const latestSession = data.sessions[data.sessions.length - 1]; + const pairs = (latestSession.qa_pairs || []).slice(-3); + + for (const qa of pairs) { + const item = card.createEl('div', { cls: 'paperforge-discussion-item' }); + const qEl = item.createEl('div', { cls: 'paperforge-discussion-q', text: '提问:' + qa.question }); + const aEl = item.createEl('div', { cls: 'paperforge-discussion-a' }); + const shortAnswer = qa.answer && qa.answer.length > 150 + ? qa.answer.slice(0, 150) + '...' + : (qa.answer || ''); + aEl.createEl('span', { cls: 'paperforge-discussion-a-text', text: '解答:' + shortAnswer }); + if (qa.answer && qa.answer.length > 150) { + const expandLink = aEl.createEl('button', { cls: 'paperforge-discussion-expand', text: '展开' }); + let expanded = false; + expandLink.addEventListener('click', () => { + const textSpan = aEl.querySelector('.paperforge-discussion-a-text'); + if (textSpan) { + textSpan.setText(expanded ? ('解答:' + shortAnswer) : ('解答:' + qa.answer)); + } + expandLink.setText(expanded ? '展开' : '收起'); + expanded = !expanded; + }); + } + } + + // "查看全部" link + const viewAll = card.createEl('a', { cls: 'paperforge-discussion-viewall', text: '查看全部讨论 →' }); + viewAll.addEventListener('click', (e) => { + e.preventDefault(); + const discMdPath = wsDir + '/ai/discussion.md'; + const discFile = this.app.vault.getAbstractFileByPath(discMdPath); + if (discFile) { + this.app.workspace.openLinkText(discMdPath, ''); + } else { + new Notice('讨论文件尚未生成'); + } + }); + }).catch(() => { + // No discussion.json — card stays hidden + }); + } + + /* ── Paper Files Row ── */ + _renderPaperFilesRow(container, entry) { + const row = container.createEl('div', { cls: 'paperforge-paper-files' }); + // Open PDF + if (entry.pdf_path) { + const pdfBtn = row.createEl('button', { cls: 'paperforge-contextual-btn' }); + pdfBtn.createEl('span', { cls: 'paperforge-contextual-btn-icon', text: '\uD83D\uDCC4' }); + pdfBtn.createEl('span', { text: 'Open PDF' }); + pdfBtn.addEventListener('click', () => { + // pdf_path is a wikilink like [[System/Zotero/storage/KEY/file.pdf]] + const pathMatch = entry.pdf_path.match(/\[\[([^\]]+)\]\]/); + const targetPath = pathMatch ? pathMatch[1] : entry.pdf_path; + const file = this.app.vault.getAbstractFileByPath(targetPath); + if (file) { + this.app.workspace.openLinkText(targetPath, ''); + } else { + new Notice('[!!] PDF not found: ' + targetPath, 6000); + } + }); + } + // Open Fulltext + if (entry.fulltext_path) { + const ftBtn = row.createEl('button', { cls: 'paperforge-contextual-btn' }); + ftBtn.createEl('span', { cls: 'paperforge-contextual-btn-icon', text: '\uD83D\uDCDD' }); + ftBtn.createEl('span', { text: 'Open Fulltext' }); + ftBtn.addEventListener('click', () => this._openFulltext(entry.fulltext_path)); + } + } + + /* ── Paper Technical Details (collapsed by default) ── */ + _renderPaperTechnicalDetails(container, entry) { + const section = container.createEl('div', { cls: 'paperforge-technical-details' }); + const toggle = section.createEl('button', { cls: 'paperforge-technical-details-toggle', text: '技术详情 ▸' }); + const body = section.createEl('div', { cls: 'paperforge-technical-details-body collapsed' }); + body.style.display = 'none'; + + toggle.addEventListener('click', () => { + const visible = body.style.display !== 'none'; + body.style.display = visible ? 'none' : 'block'; + toggle.setText(visible ? '技术详情 ▸' : '技术详情 ▾'); + }); + + const health = entry.health || {}; + const rows = [ + ['PDF Health', health.pdf_health || '\u2014'], + ['OCR Status', entry.ocr_status || '\u2014'], + ['Asset Health', health.asset_health || '\u2014'], + ['Note Path', entry.note_path || '\u2014'], + ['Fulltext Path', entry.fulltext_path || '\u2014'], + ]; + for (const [label, value] of rows) { + const row = body.createEl('div', { cls: 'paperforge-technical-row' }); + row.createEl('span', { cls: 'paperforge-technical-label', text: label }); + row.createEl('span', { cls: 'paperforge-technical-value', text: String(value) }); + } } /* ── Next-Step Recommendation Card (D-08, D-09) ── */ From c5097373f4d441bff48e6288db93883f4029b437 Mon Sep 17 00:00:00 2001 From: Research Assistant Date: Sun, 10 May 2026 13:53:06 +0800 Subject: [PATCH 04/37] refactor(dashboard): redesign base/collection view as batch workflow workspace - Replace metric cards + bar chart + health grid with workflow funnel overview - Move OCR pipeline from global to base (same progress bar logic, new container) - Replace health matrix with compact issue summary (only visible when issues exist) - Add contextual action buttons: Sync Library + Run OCR - Remove _renderCollectionHealth (replaced by inline issue summary) --- paperforge/plugin/main.js | 169 +++++++++++++++++++++++--------------- 1 file changed, 104 insertions(+), 65 deletions(-) diff --git a/paperforge/plugin/main.js b/paperforge/plugin/main.js index 9edc63a1..1b82b70f 100644 --- a/paperforge/plugin/main.js +++ b/paperforge/plugin/main.js @@ -1406,97 +1406,136 @@ class PaperForgeStatusView extends ItemView { - /* ── Collection Mode Render (Phase 30) ── */ + /* ── Collection Mode Render: Batch Workflow Workspace ── */ _renderCollectionMode() { const domain = this._currentDomain || 'Unknown'; const domainItems = this._filterByDomain(domain); const view = this._contentEl.createEl('div', { cls: 'paperforge-collection-view' }); - // --- Empty state --- if (domainItems.length === 0) { this._renderEmptyState(view, 'No papers found in domain "' + domain + '". Sync some papers first.'); return; } - // --- Single-pass aggregation --- - const healthAgg = { - pdf_health: { healthy: 0, unhealthy: 0 }, - ocr_health: { healthy: 0, unhealthy: 0 }, - note_health: { healthy: 0, unhealthy: 0 }, - asset_health: { healthy: 0, unhealthy: 0 }, - }; - let hasPdf = 0; - let fulltextReady = 0; - let deepRead = 0; + // ── Single-pass aggregation ── + const totalPapers = domainItems.length; + let hasPdf = 0, ocrDone = 0, analyzeReady = 0, deepRead = 0; + let ocrPending = 0, ocrProcessing = 0, ocrFailed = 0; + let pdfIssues = 0, ocrIssues = 0, noteIssues = 0, assetIssues = 0; for (const item of domainItems) { if (item.has_pdf) hasPdf++; - if (item.ocr_status === 'done') fulltextReady++; + if (item.ocr_status === 'done') ocrDone++; + if (item.ocr_status === 'done' && item.analyze === true) analyzeReady++; if (item.deep_reading_status === 'done') deepRead++; + const ocs = item.ocr_status || ''; + if (ocs === 'pending' || ocs === 'queued') ocrPending++; + else if (ocs === 'processing') ocrProcessing++; + else if (ocs === 'failed' || ocs === 'blocked' || ocs === 'done_incomplete' || ocs === 'nopdf') ocrFailed++; + const health = item.health || {}; - for (const dim of ['pdf_health', 'ocr_health', 'note_health', 'asset_health']) { - const val = health[dim] || 'healthy'; - if (val === 'healthy') healthAgg[dim].healthy++; - else healthAgg[dim].unhealthy++; - } + if (health.pdf_health && health.pdf_health !== 'healthy') pdfIssues++; + if (health.ocr_health && health.ocr_health !== 'healthy') ocrIssues++; + if (health.note_health && health.note_health !== 'healthy') noteIssues++; + if (health.asset_health && health.asset_health !== 'healthy') assetIssues++; } - // --- Metric cards row (D-03) --- - const metrics = view.createEl('div', { cls: 'paperforge-collection-metrics' }); - const totalPapers = domainItems.length; + // ── Header ── + const header = view.createEl('div', { cls: 'paperforge-collection-header' }); + header.createEl('div', { cls: 'paperforge-collection-title', text: domain }); + header.createEl('div', { cls: 'paperforge-collection-count', text: totalPapers + ' papers' }); - const metricCards = [ - { value: totalPapers, label: 'Papers', color: 'var(--color-cyan)', barMax: 0 }, - { value: fulltextReady, label: 'Fulltext Ready', color: 'var(--color-green)', barMax: totalPapers }, - { value: deepRead, label: 'Deep Read', color: 'var(--color-yellow)', barMax: totalPapers }, + // ── Workflow Overview (funnel) ── + const wfSection = view.createEl('div', { cls: 'paperforge-workflow-overview' }); + wfSection.createEl('div', { cls: 'paperforge-section-label', text: 'Workflow Overview' }); + const funnel = wfSection.createEl('div', { cls: 'paperforge-workflow-funnel' }); + const stages = [ + { value: totalPapers, label: 'Total' }, + { value: hasPdf, label: 'PDF Ready' }, + { value: ocrDone, label: 'OCR Done' }, + { value: deepRead, label: 'Deep Read' }, ]; - for (const m of metricCards) { - const card = metrics.createEl('div', { cls: 'paperforge-metric-card' }); - card.style.setProperty('--metric-color', m.color); - card.createEl('div', { cls: 'paperforge-metric-value', text: (m.value)?.toString() || '\u2014' }); - card.createEl('div', { cls: 'paperforge-metric-label', text: m.label }); - if (m.barMax > 0) { - this._buildMetricBar(card, m.value, m.barMax); + for (let i = 0; i < stages.length; i++) { + const stage = funnel.createEl('div', { cls: 'paperforge-workflow-stage' }); + stage.createEl('div', { cls: 'paperforge-workflow-stage-value', text: String(stages[i].value) }); + stage.createEl('div', { cls: 'paperforge-workflow-stage-label', text: stages[i].label }); + if (i < stages.length - 1) { + funnel.createEl('div', { cls: 'paperforge-workflow-arrow', text: '\u2192' }); } } - // --- Lifecycle distribution bar chart (cumulative, D-04) --- - this._renderBarChart(view, { - indexed: totalPapers - hasPdf, - pdf_ready: hasPdf, - fulltext_ready: fulltextReady, - deep_read_done: deepRead, + // ── OCR Pipeline (scoped to this base) ── + if (ocrPending + ocrProcessing + ocrDone + ocrFailed > 0) { + const ocrSection = view.createEl('div', { cls: 'paperforge-ocr-section' }); + const ocrHeader = ocrSection.createEl('div', { cls: 'paperforge-collection-ocr-header' }); + ocrHeader.createEl('h4', { cls: 'paperforge-ocr-title', text: 'OCR Pipeline' }); + const ocrBadge = ocrHeader.createEl('span', { cls: 'paperforge-ocr-badge idle' }); + if (ocrProcessing > 0) { ocrBadge.addClass('active'); ocrBadge.setText('Processing'); } + else if (ocrPending > 0) ocrBadge.setText('Pending'); + else { ocrBadge.addClass('idle'); ocrBadge.setText('Idle'); } + + const ocrTrack = ocrSection.createEl('div', { cls: 'paperforge-progress-track' }); + if (ocrProcessing > 0) ocrTrack.addClass('paperforge-processing'); + const totalOcr = ocrPending + ocrProcessing + ocrDone + ocrFailed; + const ocrSegs = [ + { cls: 'pending', count: ocrPending }, + { cls: 'active', count: ocrProcessing }, + { cls: 'done', count: ocrDone }, + { cls: 'failed', count: ocrFailed }, + ]; + for (const s of ocrSegs) { + if (s.count > 0) { + const pct = (s.count / totalOcr * 100).toFixed(1); + ocrTrack.createEl('div', { cls: `paperforge-progress-seg ${s.cls}`, attr: { style: `width:${pct}%` } }); + } + } + + const ocrCounts = ocrSection.createEl('div', { cls: 'paperforge-ocr-counts' }); + const ocrLabels = [ + { cls: 'pending', value: ocrPending, label: 'Pending' }, + { cls: 'active', value: ocrProcessing, label: 'Processing' }, + { cls: 'done', value: ocrDone, label: 'Done' }, + { cls: 'failed', value: ocrFailed, label: 'Needs Attention' }, + ]; + for (const l of ocrLabels) { + const cnt = ocrCounts.createEl('div', { cls: 'paperforge-ocr-count' }); + cnt.createEl('div', { cls: 'paperforge-ocr-count-value', text: l.value.toString() }); + cnt.createEl('div', { cls: 'paperforge-ocr-count-label', text: l.label }); + } + } + + // ── Issue Summary (compact, only when issues exist) ── + const totalIssues = pdfIssues + ocrIssues + noteIssues + assetIssues; + if (totalIssues > 0) { + const issueSection = view.createEl('div', { cls: 'paperforge-issue-summary' }); + issueSection.createEl('div', { cls: 'paperforge-section-label', text: 'Issues' }); + const parts = []; + if (pdfIssues > 0) parts.push(pdfIssues + ' PDF'); + if (ocrIssues > 0) parts.push(ocrIssues + ' OCR'); + if (noteIssues > 0) parts.push(noteIssues + ' Note'); + if (assetIssues > 0) parts.push(assetIssues + ' Asset'); + issueSection.createEl('div', { cls: 'paperforge-issue-text', text: parts.join(' · ') + ' issues' }); + } + + // ── Contextual Actions ── + const actionsRow = view.createEl('div', { cls: 'paperforge-collection-actions' }); + const syncBtn = actionsRow.createEl('button', { cls: 'paperforge-contextual-btn' }); + syncBtn.createEl('span', { cls: 'paperforge-contextual-btn-icon', text: '\u21BB' }); + syncBtn.createEl('span', { text: 'Sync Library' }); + syncBtn.addEventListener('click', () => { + const action = ACTIONS.find(a => a.id === 'paperforge-sync'); + if (action) this._runAction(action, syncBtn); }); - // --- Health overview (D-05) --- - this._renderCollectionHealth(view, healthAgg); - } - - /* ── Collection Health Overview (Phase 30, D-05) ── */ - _renderCollectionHealth(container, healthAgg) { - if (!healthAgg) return; - - const dimensions = [ - { key: 'pdf_health', label: 'PDF Health', okLabel: 'Healthy', failLabel: 'Broken' }, - { key: 'ocr_health', label: 'OCR Health', okLabel: 'Done', failLabel: 'Pending/Failed' }, - { key: 'note_health', label: 'Note Health', okLabel: 'Present', failLabel: 'Missing' }, - { key: 'asset_health', label: 'Asset Health', okLabel: 'Valid', failLabel: 'Drifted' }, - ]; - - const grid = container.createEl('div', { cls: 'paperforge-collection-health' }); - - for (const dim of dimensions) { - const data = healthAgg[dim.key] || { healthy: 0, unhealthy: 0 }; - const cell = grid.createEl('div', { cls: 'paperforge-collection-health-cell' }); - cell.createEl('div', { cls: 'paperforge-collection-health-cell-label', text: dim.label }); - - const counts = cell.createEl('div', { cls: 'paperforge-collection-health-counts' }); - const okSpan = counts.createEl('span', { cls: 'ok', text: String(data.healthy) + ' ' + dim.okLabel }); - counts.createEl('span', { text: ' \u00B7 ' }); - const failSpan = counts.createEl('span', { cls: 'fail', text: String(data.unhealthy) + ' ' + dim.failLabel }); - } + const ocrActionBtn = actionsRow.createEl('button', { cls: 'paperforge-contextual-btn' }); + ocrActionBtn.createEl('span', { cls: 'paperforge-contextual-btn-icon', text: '\u229E' }); + ocrActionBtn.createEl('span', { text: 'Run OCR' }); + ocrActionBtn.addEventListener('click', () => { + const action = ACTIONS.find(a => a.id === 'paperforge-ocr'); + if (action) this._runAction(action, ocrActionBtn); + }); } /* ── Refresh current mode (called on index change, D-09, REFR-01) ── */ From 9226ad0f037cb3e3667f4bdbd7120153603be82d Mon Sep 17 00:00:00 2001 From: Research Assistant Date: Sun, 10 May 2026 13:56:17 +0800 Subject: [PATCH 05/37] refactor(dashboard): contract global view to system homepage - Remove OCR pipeline and metric cards from global mode (moved to base) - Add library snapshot: papers, PDFs ready, OCR done, deep-read done - Add system status grid: runtime, index, Zotero export, OCR token - Add issues panel with contextual Run Doctor / Repair Issues (only when issues exist) - Add contextual Start Working actions: Open Literature Hub + Sync Library - Add null guards to _renderStats and _renderOcr for safe backwards compat --- paperforge/plugin/main.js | 146 ++++++++++++++++++++++++++++++++++---- 1 file changed, 131 insertions(+), 15 deletions(-) diff --git a/paperforge/plugin/main.js b/paperforge/plugin/main.js index 1b82b70f..78294890 100644 --- a/paperforge/plugin/main.js +++ b/paperforge/plugin/main.js @@ -697,6 +697,8 @@ class PaperForgeStatusView extends ItemView { return; } + if (!this._metricsEl) return; // guard: global mode no longer creates metrics container + this._metricsEl.removeClass('paperforge-loading'); const totalPapers = d.total_papers || 0; @@ -720,6 +722,7 @@ class PaperForgeStatusView extends ItemView { /* ── OCR Pipeline ── */ _renderOcr(d) { + if (!this._ocrSection) return; // guard: global mode no longer creates OCR container const ocr = d.ocr || {}; const total = ocr.total || 0; @@ -1015,27 +1018,140 @@ class PaperForgeStatusView extends ItemView { } } - /* ── Global Mode Render (existing dashboard, extracted from _fetchStats + _renderOcr) ── */ + /* ── Global Mode Render: System Homepage ── */ _renderGlobalMode() { + const view = this._contentEl.createEl('div', { cls: 'paperforge-global-view' }); + // Drift warning banner (hidden by default, shown on version mismatch) - this._driftBannerEl = this._contentEl.createEl('div', { cls: 'paperforge-drift-banner' }); + this._driftBannerEl = view.createEl('div', { cls: 'paperforge-drift-banner' }); this._driftBannerEl.style.display = 'none'; - // Metric cards - this._metricsEl = this._contentEl.createEl('div', { cls: 'paperforge-metrics' }); + // ── Library Snapshot ── + const items = this._getCachedIndex(); + const totalPapers = items.length; + let pdfReady = 0, ocrDone = 0, deepReadDone = 0, pdfIssues = 0; + for (const item of items) { + if (item.has_pdf) pdfReady++; + if (item.ocr_status === 'done') ocrDone++; + if (item.deep_reading_status === 'done') deepReadDone++; + const h = item.health || {}; + if (h.pdf_health && h.pdf_health !== 'healthy') pdfIssues++; + } - // OCR pipeline section (_renderOcr expects these instance vars) - this._ocrSection = this._contentEl.createEl('div', { cls: 'paperforge-ocr-section' }); - this._ocrSection.style.display = 'none'; - const ocrHeader = this._ocrSection.createEl('div', { cls: 'paperforge-ocr-header' }); - ocrHeader.createEl('h4', { cls: 'paperforge-ocr-title', text: 'OCR Pipeline' }); - this._ocrBadge = ocrHeader.createEl('span', { cls: 'paperforge-ocr-badge idle', text: 'Idle' }); - this._ocrTrack = this._ocrSection.createEl('div', { cls: 'paperforge-progress-track' }); - this._ocrCounts = this._ocrSection.createEl('div', { cls: 'paperforge-ocr-counts' }); - this._ocrEmpty = this._ocrSection.createEl('div', { cls: 'paperforge-ocr-empty', text: 'No OCR tasks yet. Mark papers with do_ocr: true to start.' }); + const snapshot = view.createEl('div', { cls: 'paperforge-library-snapshot' }); + snapshot.createEl('div', { cls: 'paperforge-section-label', text: 'Library Snapshot' }); + const pills = snapshot.createEl('div', { cls: 'paperforge-snapshot-pills' }); + const snapData = [ + { value: totalPapers, label: 'papers' }, + { value: pdfReady, label: 'PDFs ready' }, + { value: ocrDone, label: 'OCR done' }, + { value: deepReadDone, label: 'deep-read done' }, + ]; + for (const s of snapData) { + const pill = pills.createEl('div', { cls: 'paperforge-snapshot-pill' }); + pill.createEl('span', { cls: 'paperforge-snapshot-value', text: String(s.value) }); + pill.createEl('span', { cls: 'paperforge-snapshot-label', text: ' ' + s.label }); + } - this._cachedStats = null; // force fresh load - this._fetchStats(); + // ── System Status ── + const statusSection = view.createEl('div', { cls: 'paperforge-system-status' }); + statusSection.createEl('div', { cls: 'paperforge-section-label', text: 'System Status' }); + const statusGrid = statusSection.createEl('div', { cls: 'paperforge-status-grid' }); + + // Runtime + const plugin = this.app.plugins.plugins['paperforge']; + const pluginVer = plugin?.manifest?.version || '?'; + const pyVer = this._paperforgeVersion || '\u2014'; + const runtimeOk = pyVer === 'v' + pluginVer; + this._renderSystemStatusRow(statusGrid, 'Runtime', runtimeOk ? 'healthy' : 'mismatch', + runtimeOk ? ('v' + pluginVer) : ('plugin v' + pluginVer + ' \u2260 CLI ' + pyVer)); + + // Index + const index = this._loadIndex(); + const indexOk = index && index.items && index.items.length > 0; + this._renderSystemStatusRow(statusGrid, 'Index', indexOk ? 'healthy' : 'missing', + indexOk ? (index.items.length + ' entries') : 'formal-library.json not found'); + + // Zotero export (check if any JSON export exists) + const systemDir = plugin?.settings?.system_dir || 'System'; + const vp = this.app.vault.adapter.basePath; + let exportOk = false, exportDetail = 'No exports found'; + try { + const exportsDir = path.join(vp, systemDir, 'PaperForge', 'exports'); + if (fs.existsSync(exportsDir)) { + const files = fs.readdirSync(exportsDir).filter(f => f.endsWith('.json')); + exportOk = files.length > 0; + exportDetail = exportOk ? (files.length + ' export(s)') : 'No JSON exports'; + } + } catch (_) {} + this._renderSystemStatusRow(statusGrid, 'Zotero Export', exportOk ? 'healthy' : 'missing', exportDetail); + + // OCR token + const tokenOk = !!(plugin?.settings?.paddleocr_api_key); + this._renderSystemStatusRow(statusGrid, 'OCR Token', tokenOk ? 'configured' : 'missing', + tokenOk ? 'Configured' : 'Not set'); + + // ── Issues Panel (only when issues exist) ── + const hasVersionMismatch = !runtimeOk && pyVer !== '\u2014'; + const hasIssues = pdfIssues > 0 || hasVersionMismatch || !indexOk || !exportOk || !tokenOk; + if (hasIssues) { + const issueSection = view.createEl('div', { cls: 'paperforge-issue-summary' }); + issueSection.createEl('div', { cls: 'paperforge-section-label', text: 'Issues' }); + const issueList = issueSection.createEl('div', { cls: 'paperforge-issue-list' }); + if (pdfIssues > 0) issueList.createEl('div', { cls: 'paperforge-issue-item', text: pdfIssues + ' PDF path errors' }); + if (hasVersionMismatch) issueList.createEl('div', { cls: 'paperforge-issue-item', text: 'Runtime version mismatch' }); + if (!indexOk) issueList.createEl('div', { cls: 'paperforge-issue-item', text: 'Index missing or corrupted' }); + if (!exportOk) issueList.createEl('div', { cls: 'paperforge-issue-item', text: 'No Zotero export found' }); + if (!tokenOk) issueList.createEl('div', { cls: 'paperforge-issue-item', text: 'PaddleOCR API key not configured' }); + + const issueActions = issueSection.createEl('div', { cls: 'paperforge-issue-actions' }); + const doctorBtn = issueActions.createEl('button', { cls: 'paperforge-contextual-btn' }); + doctorBtn.createEl('span', { text: 'Run Doctor' }); + doctorBtn.addEventListener('click', () => { + const action = ACTIONS.find(a => a.id === 'paperforge-doctor'); + if (action) this._runAction(action, doctorBtn); + }); + const repairBtn = issueActions.createEl('button', { cls: 'paperforge-contextual-btn' }); + repairBtn.createEl('span', { text: 'Repair Issues' }); + repairBtn.addEventListener('click', () => { + const action = ACTIONS.find(a => a.id === 'paperforge-repair'); + if (action) this._runAction(action, repairBtn); + }); + } + + // ── Contextual Actions ── + const actionsRow = view.createEl('div', { cls: 'paperforge-global-actions' }); + actionsRow.createEl('div', { cls: 'paperforge-section-label', text: 'Start Working' }); + const btnsRow = actionsRow.createEl('div', { cls: 'paperforge-global-actions-row' }); + const hubBtn = btnsRow.createEl('button', { cls: 'paperforge-contextual-btn' }); + hubBtn.createEl('span', { cls: 'paperforge-contextual-btn-icon', text: '\uD83D\uDCC1' }); + hubBtn.createEl('span', { text: 'Open Literature Hub' }); + hubBtn.addEventListener('click', () => { + const baseDir = plugin?.settings?.base_dir || 'Bases'; + const baseFile = this.app.vault.getAbstractFileByPath(baseDir); + if (baseFile) { + this.app.workspace.revealLeaf().setViewState({ type: 'file-explorer', active: true }); + } else { + new Notice('[!!] Base directory not found: ' + baseDir, 6000); + } + }); + + const globalSyncBtn = btnsRow.createEl('button', { cls: 'paperforge-contextual-btn' }); + globalSyncBtn.createEl('span', { cls: 'paperforge-contextual-btn-icon', text: '\u21BB' }); + globalSyncBtn.createEl('span', { text: 'Sync Library' }); + globalSyncBtn.addEventListener('click', () => { + const action = ACTIONS.find(a => a.id === 'paperforge-sync'); + if (action) this._runAction(action, globalSyncBtn); + }); + } + + /* ── System Status Row helper ── */ + _renderSystemStatusRow(container, label, status, detail) { + const row = container.createEl('div', { cls: 'paperforge-status-row' }); + const dot = row.createEl('span', { cls: 'paperforge-status-dot' }); + dot.addClass(status === 'healthy' || status === 'configured' ? 'ok' : 'fail'); + row.createEl('span', { cls: 'paperforge-status-label', text: label }); + row.createEl('span', { cls: 'paperforge-status-detail', text: detail || '' }); } /* ── Per-Paper Mode Render: Reading Companion ── */ From b9fb1c919c8f69769db325aaca87cc58a97c2d06 Mon Sep 17 00:00:00 2001 From: Research Assistant Date: Sun, 10 May 2026 14:04:14 +0800 Subject: [PATCH 06/37] style(dashboard): add refined CSS for redesigned per-paper, collection, and global views - Status strip pills matching existing badge design (color-green/text-error) - Paper overview card with card-style hover border + accent left border via ::before - Discussion card with session-based Q&A styling + expand/truncate - Workflow overview funnel with centered stage pills and arrows - Issue summary with subtle error border + dot indicators - Library snapshot pills and system status grid for global view - Contextual button base style shared across all views - All new components use Obsidian CSS variables for dark/light theme support --- paperforge/plugin/styles.css | 565 +++++++++++++++++++++++++++++++++++ 1 file changed, 565 insertions(+) diff --git a/paperforge/plugin/styles.css b/paperforge/plugin/styles.css index 530dd5a9..4cc7bbbb 100644 --- a/paperforge/plugin/styles.css +++ b/paperforge/plugin/styles.css @@ -1567,3 +1567,568 @@ justify-content: flex-end; padding-top: 8px; } + +/* ========================================================================== + SECTION 39 — Dashboard Redesign: Shared Components + ========================================================================== */ + +/* ── Section Label (used across global / collection / paper) ── */ +.paperforge-section-label { + font-size: var(--font-ui-small); + font-weight: var(--font-semibold); + color: var(--text-normal); + margin: 0 0 10px 0; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +/* ── Contextual Button (card-action style, used across all views) ── */ +.paperforge-contextual-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 14px; + background: var(--background-secondary); + border: 1px solid var(--background-modifier-border); + border-radius: var(--radius-s); + color: var(--text-muted); + font-size: var(--font-ui-smaller); + font-weight: var(--font-medium); + cursor: pointer; + transition: background 0.15s, border-color 0.15s, color 0.15s; + white-space: nowrap; +} + +.paperforge-contextual-btn:hover { + background: var(--background-modifier-hover); + border-color: var(--interactive-accent); + color: var(--text-normal); +} + +.paperforge-contextual-btn.running { + opacity: 0.6; + pointer-events: none; +} + +.paperforge-contextual-btn-icon { + font-size: 14px; + line-height: 1; +} + +/* ========================================================================== + SECTION 40 — Dashboard Redesign: Per-Paper View (Reading Companion) + ========================================================================== */ + +/* ── Status Strip ── */ +.paperforge-status-strip { + display: flex; + gap: 8px; + padding: 8px 0; + border-top: 1px solid var(--background-modifier-border); + border-bottom: 1px solid var(--background-modifier-border); + margin: 10px 0; +} + +.paperforge-status-pill { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 3px 12px; + border-radius: 10px; + font-size: 11px; + font-weight: var(--font-semibold); + background: var(--background-modifier-hover); + color: var(--text-muted); + line-height: 1.4; +} + +.paperforge-status-pill.ok { + background: var(--color-green); + color: var(--text-on-accent); +} + +.paperforge-status-pill.fail { + background: var(--text-error); + color: var(--text-on-accent); +} + +.paperforge-status-pill.pending { + background: var(--background-modifier-hover); + color: var(--text-muted); +} + +.paperforge-status-pill-icon { + font-size: 10px; + line-height: 1; +} + +/* ── Paper Overview Card ── */ +.paperforge-paper-overview { + margin: 12px 0; + padding: 14px; + background: var(--background-secondary); + border: 1px solid var(--background-modifier-border); + border-radius: var(--radius-m); + transition: border-color 0.15s; +} + +.paperforge-paper-overview:hover { + border-color: var(--interactive-accent); +} + +.paperforge-paper-overview-header { + margin-bottom: 8px; +} + +.paperforge-paper-overview-title { + font-size: var(--font-ui-small); + font-weight: var(--font-semibold); + color: var(--text-accent); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.paperforge-paper-overview-body { + font-size: var(--font-ui-smaller); + color: var(--text-muted); + line-height: 1.6; +} + +.paperforge-paper-overview-excerpt { + white-space: pre-wrap; + word-break: break-word; +} + +.paperforge-paper-overview-expand { + display: inline-block; + margin-top: 6px; + padding: 2px 0; + background: none; + border: none; + color: var(--text-accent); + font-size: var(--font-ui-smaller); + font-weight: var(--font-medium); + cursor: pointer; + transition: color 0.15s; +} + +.paperforge-paper-overview-expand:hover { + color: var(--interactive-accent-hover); + text-decoration: underline; +} + +/* ── Discussion Card ── */ +.paperforge-discussion-card { + margin: 12px 0; + padding: 14px; + background: var(--background-secondary); + border: 1px solid var(--background-modifier-border); + border-radius: var(--radius-m); + transition: border-color 0.15s; +} + +.paperforge-discussion-card:hover { + border-color: var(--interactive-accent); +} + +.paperforge-discussion-header { + margin-bottom: 8px; +} + +.paperforge-discussion-title { + font-size: var(--font-ui-small); + font-weight: var(--font-semibold); + color: var(--text-accent); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.paperforge-discussion-item { + margin-bottom: 8px; + padding-bottom: 8px; + border-bottom: 1px solid var(--background-modifier-border-hover); +} + +.paperforge-discussion-item:last-child { + border-bottom: none; + margin-bottom: 4px; + padding-bottom: 0; +} + +.paperforge-discussion-q { + font-size: var(--font-ui-smaller); + font-weight: var(--font-semibold); + color: var(--text-normal); + margin-bottom: 4px; + line-height: 1.5; +} + +.paperforge-discussion-a { + font-size: var(--font-ui-smaller); + color: var(--text-muted); + line-height: 1.5; +} + +.paperforge-discussion-expand { + display: inline; + padding: 0; + margin-left: 4px; + background: none; + border: none; + color: var(--text-accent); + font-size: var(--font-ui-smaller); + font-weight: var(--font-medium); + cursor: pointer; + transition: color 0.15s; +} + +.paperforge-discussion-expand:hover { + color: var(--interactive-accent-hover); + text-decoration: underline; +} + +.paperforge-discussion-viewall { + display: block; + text-align: right; + margin-top: 6px; + font-size: var(--font-ui-smaller); + font-weight: var(--font-medium); + color: var(--text-accent); + cursor: pointer; + text-decoration: none; + transition: color 0.15s; +} + +.paperforge-discussion-viewall:hover { + color: var(--interactive-accent-hover); +} + +/* ── Paper Files Row ── */ +.paperforge-paper-files { + display: flex; + gap: 8px; + margin: 12px 0; +} + +/* ── Technical Details ── */ +.paperforge-technical-details { + margin: 8px 0; +} + +.paperforge-technical-details-toggle { + width: 100%; + padding: 8px 12px; + background: var(--background-secondary); + border: 1px solid var(--background-modifier-border); + border-radius: var(--radius-s); + color: var(--text-muted); + font-size: var(--font-ui-smaller); + font-weight: var(--font-medium); + cursor: pointer; + text-align: left; + transition: background 0.15s, border-color 0.15s; +} + +.paperforge-technical-details-toggle:hover { + background: var(--background-modifier-hover); + border-color: var(--interactive-accent); + color: var(--text-normal); +} + +.paperforge-technical-details-body { + margin-top: 6px; + padding: 10px 14px; + background: var(--background-primary-alt); + border: 1px solid var(--background-modifier-border); + border-radius: var(--radius-s); + font-size: var(--font-ui-smaller); +} + +.paperforge-technical-row { + display: flex; + justify-content: space-between; + padding: 3px 0; +} + +.paperforge-technical-label { + color: var(--text-faint); + flex-shrink: 0; + margin-right: 12px; +} + +.paperforge-technical-value { + color: var(--text-muted); + max-width: 60%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + text-align: right; +} + +/* ========================================================================== + SECTION 41 — Dashboard Redesign: Collection / Base View (Batch Workspace) + ========================================================================== */ + +/* ── Collection Header ── */ +.paperforge-collection-header { + margin-bottom: 12px; +} + +.paperforge-collection-title { + font-size: var(--font-ui-medium); + font-weight: var(--font-bold); + color: var(--text-normal); + line-height: 1.3; +} + +.paperforge-collection-count { + font-size: var(--font-ui-smaller); + color: var(--text-muted); + margin-top: 2px; +} + +/* ── Workflow Overview (funnel) ── */ +.paperforge-workflow-overview { + margin: 12px 0; + padding: 16px; + background: var(--background-secondary); + border: 1px solid var(--background-modifier-border); + border-radius: var(--radius-m); + transition: border-color 0.15s; +} + +.paperforge-workflow-overview:hover { + border-color: var(--interactive-accent); +} + +.paperforge-workflow-funnel { + display: flex; + align-items: center; + gap: 4px; + flex-wrap: wrap; + justify-content: center; +} + +.paperforge-workflow-stage { + display: flex; + flex-direction: column; + align-items: center; + padding: 8px 14px; + background: var(--background-primary-alt); + border-radius: var(--radius-s); + min-width: 60px; +} + +.paperforge-workflow-stage-value { + font-size: clamp(16px, 3vw, 22px); + font-weight: var(--font-bold); + color: var(--text-accent); + line-height: 1.2; + letter-spacing: -0.3px; +} + +.paperforge-workflow-stage-label { + font-size: 10px; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.3px; + margin-top: 2px; + text-align: center; +} + +.paperforge-workflow-arrow { + color: var(--text-faint); + font-size: var(--font-ui-smaller); + flex-shrink: 0; +} + +/* ── OCR Pipeline in Base ── */ +.paperforge-collection-ocr-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 14px; +} + +/* ── Issue Summary ── */ +.paperforge-issue-summary { + margin: 12px 0; + padding: 12px 14px; + background: var(--background-secondary); + border: 1px solid var(--text-error); + border-radius: var(--radius-m); +} + +.paperforge-issue-summary .paperforge-section-label { + color: var(--text-error); +} + +.paperforge-issue-text { + font-size: var(--font-ui-smaller); + color: var(--text-muted); + line-height: 1.5; +} + +.paperforge-issue-list { + margin: 6px 0 8px 0; +} + +.paperforge-issue-item { + font-size: var(--font-ui-smaller); + color: var(--text-error); + padding: 2px 0; + padding-left: 12px; + position: relative; +} + +.paperforge-issue-item::before { + content: ''; + position: absolute; + left: 0; + top: 8px; + width: 5px; + height: 5px; + border-radius: 50%; + background: var(--text-error); +} + +.paperforge-issue-actions { + display: flex; + gap: 8px; + margin-top: 10px; +} + +/* ── Collection Actions ── */ +.paperforge-collection-actions { + display: flex; + gap: 8px; + margin-top: 12px; + padding-top: 12px; + border-top: 1px solid var(--background-modifier-border); +} + +/* ========================================================================== + SECTION 42 — Dashboard Redesign: Global / Home View (System Homepage) + ========================================================================== */ + +/* ── Library Snapshot ── */ +.paperforge-library-snapshot { + padding: 16px; + background: var(--background-secondary); + border: 1px solid var(--background-modifier-border); + border-radius: var(--radius-m); + transition: border-color 0.15s; +} + +.paperforge-library-snapshot:hover { + border-color: var(--interactive-accent); +} + +.paperforge-snapshot-pills { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.paperforge-snapshot-pill { + display: flex; + flex-direction: column; + align-items: center; + padding: 10px 16px; + background: var(--background-primary-alt); + border-radius: var(--radius-s); + min-width: 64px; + flex: 1; +} + +.paperforge-snapshot-value { + font-size: clamp(16px, 3vw, 22px); + font-weight: var(--font-bold); + color: var(--text-accent); + line-height: 1.2; + letter-spacing: -0.3px; +} + +.paperforge-snapshot-label { + font-size: 10px; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.3px; + margin-top: 2px; + text-align: center; +} + +/* ── System Status ── */ +.paperforge-system-status { + padding: 16px; + background: var(--background-secondary); + border: 1px solid var(--background-modifier-border); + border-radius: var(--radius-m); + transition: border-color 0.15s; +} + +.paperforge-system-status:hover { + border-color: var(--interactive-accent); +} + +.paperforge-status-grid { + display: flex; + flex-direction: column; + gap: 2px; +} + +.paperforge-status-row { + display: flex; + align-items: center; + gap: 10px; + padding: 5px 0; +} + +.paperforge-status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; + background: var(--background-modifier-border); + transition: background 0.3s; +} + +.paperforge-status-dot.ok { + background: var(--color-green); +} + +.paperforge-status-dot.fail { + background: var(--text-error); +} + +.paperforge-status-label { + font-size: var(--font-ui-smaller); + color: var(--text-muted); + min-width: 80px; +} + +.paperforge-status-detail { + font-size: var(--font-ui-smaller); + color: var(--text-faint); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* ── Global Actions ── */ +.paperforge-global-actions { + padding: 16px; + background: var(--background-secondary); + border: 1px solid var(--background-modifier-border); + border-radius: var(--radius-m); + transition: border-color 0.15s; +} + +.paperforge-global-actions:hover { + border-color: var(--interactive-accent); +} + +.paperforge-global-actions-row { + display: flex; + gap: 8px; +} From 808cae61f6f08f47675b1d8fac21c476a1b08691 Mon Sep 17 00:00:00 2001 From: Research Assistant Date: Sun, 10 May 2026 14:06:37 +0800 Subject: [PATCH 07/37] feat(dashboard): add do_ocr/analyze workflow toggle checkboxes to per-paper view - Replace OCR queue toggle button with checkbox pair (do_ocr + analyze) - Mirrors Base view toggles: writes to formal note frontmatter via processFrontMatter - Dashboard modify event handler picks up file change and auto-refreshes - Workers (OCR, deep-reading) read frontmatter from note file on disk --- paperforge/plugin/main.js | 42 ++++++++++++++++---------------- paperforge/plugin/styles.css | 47 ++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 21 deletions(-) diff --git a/paperforge/plugin/main.js b/paperforge/plugin/main.js index 78294890..5623689c 100644 --- a/paperforge/plugin/main.js +++ b/paperforge/plugin/main.js @@ -1194,27 +1194,27 @@ class PaperForgeStatusView extends ItemView { // ── Recent Discussion ── this._renderRecentDiscussionCard(view, entry); - // ── OCR Queue Toggle ── - const ocrRow = view.createEl('div', { cls: 'paperforge-paper-actions' }); - const inQueue = entry.do_ocr === true; - const ocrBtn = ocrRow.createEl('button', { cls: 'paperforge-contextual-btn' }); - ocrBtn.createEl('span', { cls: 'paperforge-contextual-btn-icon', text: inQueue ? '\u23F1' : '\u23F0' }); - ocrBtn.createEl('span', { text: inQueue ? t('ocr_queue_remove') : t('ocr_queue_add') }); - ocrBtn.addEventListener('click', async () => { - const noteFile = entry.note_path ? this.app.vault.getAbstractFileByPath(entry.note_path) : null; - if (!noteFile) { - new Notice('[!!] Note file not found: ' + (entry.note_path || 'unknown'), 6000); - return; - } - const newValue = !inQueue; - await this.app.fileManager.processFrontMatter(noteFile, (fm) => { fm.do_ocr = newValue; }); - this._patchCachedEntry(key, { do_ocr: newValue }); - this._currentPaperEntry = patchEntryWorkflowState(this._currentPaperEntry, { do_ocr: newValue }); - new Notice(newValue ? t('ocr_queue_added') : t('ocr_queue_removed')); - this._refreshCurrentMode(); - }); - if (inQueue) { - ocrRow.createEl('span', { cls: 'paperforge-ocr-queue-hint', text: 'OCR ' + (entry.ocr_status === 'done' ? 'already done' : 'pending') }); + // ── Workflow Toggles (checkboxes, same as Base view) ── + const togglesRow = view.createEl('div', { cls: 'paperforge-workflow-toggles' }); + const toggleFields = [ + { key: 'do_ocr', label: 'OCR', hint: '加入 OCR 队列' }, + { key: 'analyze', label: 'Analyze', hint: '标记待精读' }, + ]; + for (const tf of toggleFields) { + const label = togglesRow.createEl('label', { cls: 'paperforge-workflow-toggle' }); + const cb = label.createEl('input', { type: 'checkbox', cls: 'paperforge-workflow-checkbox' }); + cb.checked = entry[tf.key] === true; + label.createEl('span', { cls: 'paperforge-workflow-toggle-label', text: tf.label }); + label.createEl('span', { cls: 'paperforge-workflow-toggle-hint', text: tf.hint }); + cb.addEventListener('change', async () => { + const noteFile = entry.note_path ? this.app.vault.getAbstractFileByPath(entry.note_path) : null; + if (!noteFile) { new Notice('[!!] Note file not found: ' + (entry.note_path || 'unknown'), 6000); return; } + const newVal = cb.checked; + await this.app.fileManager.processFrontMatter(noteFile, (fm) => { fm[tf.key] = newVal; }); + this._patchCachedEntry(key, { [tf.key]: newVal }); + this._currentPaperEntry = patchEntryWorkflowState(this._currentPaperEntry, { [tf.key]: newVal }); + this._refreshCurrentMode(); // sync UI; workers pick up frontmatter change from file + }); } // ── Next Step ── diff --git a/paperforge/plugin/styles.css b/paperforge/plugin/styles.css index 4cc7bbbb..55d6ebb9 100644 --- a/paperforge/plugin/styles.css +++ b/paperforge/plugin/styles.css @@ -2132,3 +2132,50 @@ display: flex; gap: 8px; } + +/* ========================================================================== + SECTION 43 — Dashboard Redesign: Workflow Toggle Checkboxes (per-paper) + ========================================================================== */ + +.paperforge-workflow-toggles { + display: flex; + gap: 16px; + padding: 10px 14px; + background: var(--background-secondary); + border: 1px solid var(--background-modifier-border); + border-radius: var(--radius-m); + margin: 12px 0; + transition: border-color 0.15s; +} + +.paperforge-workflow-toggles:hover { + border-color: var(--interactive-accent); +} + +.paperforge-workflow-toggle { + display: flex; + align-items: center; + gap: 6px; + cursor: pointer; + font-size: var(--font-ui-smaller); + color: var(--text-muted); + user-select: none; +} + +.paperforge-workflow-checkbox { + width: 15px; + height: 15px; + margin: 0; + cursor: pointer; + accent-color: var(--interactive-accent); +} + +.paperforge-workflow-toggle-label { + font-weight: var(--font-semibold); + color: var(--text-normal); +} + +.paperforge-workflow-toggle-hint { + color: var(--text-faint); + font-size: 11px; +} From d00977ccbbb93adbe1c75ae889c750b280b819a2 Mon Sep 17 00:00:00 2001 From: Research Assistant Date: Sun, 10 May 2026 14:07:53 +0800 Subject: [PATCH 08/37] docs: update UX contract for redesigned dashboard with 3-mode views - Replace W4-S3 (old lifecycle stepper) with new paper mode specs: status strip, overview card, discussion card, workflow toggles, files row, technical details - Add W4-S4: workspace path detection keeps paper mode for fulltext.md - Add W4-S5: global mode as system homepage - Add W4-S6: workflow toggle checkboxes sync to Base via processFrontMatter (no extra sync needed) --- docs/ux-contract.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/ux-contract.md b/docs/ux-contract.md index 5ea04acd..f3b04c2d 100644 --- a/docs/ux-contract.md +++ b/docs/ux-contract.md @@ -46,9 +46,12 @@ | Step ID | Trigger | Action | Measurable Outcome | Error Contract | |---------|---------|--------|-------------------|----------------| -| W4-S1 | User opens Obsidian vault | Obsidian loads vault | Plugin view is registered; user navigates to `Bases/` directory and sees `.base` files rendered as interactive tables with filterable columns | Plugin not loaded; stderr in dev console shows "Failed to register PaperForge view" | -| W4-S2 | Base view renders with workflow-gate columns | Lifecycle columns visible | Base view displays columns: `has_pdf`, `do_ocr`, `analyze`, `ocr_status`, `deep_reading_status`. Each paper appears as one row with status values. Filter UI present for `do_ocr`, `analyze`, `ocr_status` | Missing columns; all values appear as empty or "N/A" | -| W4-S3 | Per-paper dashboard shows lifecycle stepper | Detailed view renders | Clicking a paper row reveals: lifecycle stepper (PENDING -> PROCESSING -> DONE), health matrix (pdf, ocr, metadata status), next-step recommendation badge; all components render without console errors | Lifecycle stepper missing; health matrix shows all red; recommendation badge absent | +| W4-S1 | User opens Obsidian vault | Plugin loads and registers dashboard view | Plugin view registered; sidebar icon visible; clicking opens PaperForge dashboard panel | Plugin not loaded; console shows "Failed to register PaperForge view" | +| W4-S2 | User opens a `.base` file | Collection mode activates | Dashboard switches to `collection` mode; shows: workflow funnel (Total → PDF Ready → OCR Done → Deep Read), OCR pipeline progress bar scoped to current base, issue summary (compact, only when issues exist), action buttons (Sync Library, Run OCR) | Falls to global mode; missing stats | +| W4-S3 | User opens a formal note (`.md` with `zotero_key`) | Paper mode activates | Dashboard switches to `paper` mode; shows: status strip (PDF/OCR/DeepRead pills), paper overview card (Pass 1 summary from `## 🔍 精读`), workflow toggle checkboxes (`do_ocr`/`analyze` — sync to Base), next-step card, files row (Open PDF / Open Fulltext), collapsible technical details | Falls to global mode; components empty or erroring | +| W4-S4 | User opens `fulltext.md` or other workspace file | Paper mode activates via workspace detection | Dashboard stays in `paper` mode for any file inside a paper workspace directory (`{key} - {title}/`). zotero_key resolved from dirname pattern | Falls to global mode | +| W4-S5 | User opens no file or non-workspace `.md` | Global mode activates | Dashboard shows: library snapshot (papers / PDFs / OCR done / deep-read done), system status grid (runtime / index / Zotero export / OCR token), issues panel (only when anomalies exist with Run Doctor / Repair Issues), action buttons (Open Literature Hub / Sync Library) | Shows loading/empty state; console errors | +| W4-S6 | User toggles `do_ocr` or `analyze` checkbox | Workflow toggle writes to frontmatter | Checkbox state writes to formal note YAML frontmatter via `processFrontMatter`; Base view reflects change immediately (reads from file); dashboard auto-refreshes via `modify` event | Frontmatter not written; notice shows "Note file not found" | --- From 064644c7c618b4d112b6c9e8f01bf7248cdfc7e3 Mon Sep 17 00:00:00 2001 From: Research Assistant Date: Sun, 10 May 2026 14:10:41 +0800 Subject: [PATCH 09/37] test: update ACTIONS count from 6 to 4, remove copy-context test cases --- paperforge/plugin/tests/commands.test.mjs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/paperforge/plugin/tests/commands.test.mjs b/paperforge/plugin/tests/commands.test.mjs index b2650f1a..eac32396 100644 --- a/paperforge/plugin/tests/commands.test.mjs +++ b/paperforge/plugin/tests/commands.test.mjs @@ -9,8 +9,8 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; const { ACTIONS, buildCommandArgs, runSubprocess } = await import('../src/testable.js'); describe('ACTIONS', () => { - it('has exactly 6 entries', () => { - expect(ACTIONS).toHaveLength(6); + it('has exactly 4 entries', () => { + expect(ACTIONS).toHaveLength(4); }); it('every entry has id, title, cmd, okMsg', () => { for (const a of ACTIONS) { @@ -26,12 +26,6 @@ describe('ACTIONS', () => { it('repair action is enabled (no disabled flag)', () => { expect(ACTIONS.find(a => a.id === 'paperforge-repair')?.disabled).toBeUndefined(); }); - it('copy-context action has no needsKey (pure JS)', () => { - expect(ACTIONS.find(a => a.id === 'paperforge-copy-context')?.needsKey).toBeUndefined(); - }); - it('copy-collection-context action has no needsFilter (pure JS)', () => { - expect(ACTIONS.find(a => a.id === 'paperforge-copy-collection-context')?.needsFilter).toBeUndefined(); - }); }); describe('buildCommandArgs', () => { From 6d70e1f857a3e7d1bc6c097538b3a6f15b5c5e7d Mon Sep 17 00:00:00 2001 From: Research Assistant Date: Sun, 10 May 2026 14:17:14 +0800 Subject: [PATCH 10/37] refactor: inline testable.js into main.js to remove external module dependency - ACTIONS, resolvePythonExecutable, getPluginVersion, checkRuntimeVersion, classifyError, buildRuntimeInstallCommand, parseRuntimeStatus, buildCommandArgs, runSubprocess now defined directly in main.js - Plugin no longer needs src/testable.js at runtime - Tests continue to import from src/testable.js (unchanged) - Fixes 'Cannot find module ./src/testable' error in Obsidian plugin loader --- paperforge/plugin/main.js | 205 +++++++++++++++++++++++++++++++++++--- 1 file changed, 193 insertions(+), 12 deletions(-) diff --git a/paperforge/plugin/main.js b/paperforge/plugin/main.js index 5623689c..6ded452f 100644 --- a/paperforge/plugin/main.js +++ b/paperforge/plugin/main.js @@ -1,20 +1,201 @@ const { Plugin, Notice, ItemView, Modal, Setting, PluginSettingTab, addIcon } = require('obsidian'); -const { exec, execFile, spawn } = require('node:child_process'); +const { exec, execFile, spawn, execFileSync } = require('node:child_process'); const fs = require('fs'); const path = require('path'); const os = require('os'); -const { - resolvePythonExecutable, - getPluginVersion, - checkRuntimeVersion, - classifyError, - buildRuntimeInstallCommand, - parseRuntimeStatus, - ACTIONS, - buildCommandArgs, - runSubprocess, -} = require('./src/testable'); +// ── Inlined from src/testable.js ── + +function resolvePythonExecutable(vaultPath, settings, _fs, _execFileSync) { + const f = _fs || fs; + const execSync = _execFileSync || execFileSync; + + if (settings && settings.python_path && settings.python_path.trim()) { + const manualPath = settings.python_path.trim(); + if (f.existsSync(manualPath)) { + return { path: manualPath, source: "manual", extraArgs: [] }; + } + } + + const venvCandidates = [ + path.join(vaultPath, ".paperforge-test-venv", "Scripts", "python.exe"), + path.join(vaultPath, ".venv", "Scripts", "python.exe"), + path.join(vaultPath, "venv", "Scripts", "python.exe"), + ]; + for (const candidate of venvCandidates) { + try { + if (f.existsSync(candidate)) { + return { path: candidate, source: "auto-detected", extraArgs: [] }; + } + } catch {} + } + + const systemCandidates = [ + { path: "py", extraArgs: ["-3"] }, + { path: "python", extraArgs: [] }, + { path: "python3", extraArgs: [] }, + ]; + for (const candidate of systemCandidates) { + try { + const verOut = execSync(candidate.path, [...candidate.extraArgs, "--version"], { + encoding: "utf-8", timeout: 5000, windowsHide: true, + }); + if (verOut && verOut.toLowerCase().includes("python")) { + return { path: candidate.path, source: "auto-detected", extraArgs: candidate.extraArgs }; + } + } catch {} + } + + return { path: "python", source: "auto-detected", extraArgs: [] }; +} + +function getPluginVersion(app) { + try { + const manifest = app && app.plugins && app.plugins.plugins && + app.plugins.plugins["paperforge"] && app.plugins.plugins["paperforge"].manifest; + return (manifest && manifest.version) || null; + } catch { + return null; + } +} + +function checkRuntimeVersion(pythonExe, pluginVersion, cwd, timeout, _execFile) { + if (timeout === undefined) timeout = 10000; + const exe = _execFile || execFile; + + return new Promise((resolve) => { + exe(pythonExe, ["-c", "import paperforge; print(paperforge.__version__)"], + { cwd, timeout }, + (err, stdout) => { + if (err) { + resolve({ status: "not-installed", pyVersion: null, pluginVersion, error: err.message }); + return; + } + const pyVer = (stdout && stdout.trim()) || null; + if (pyVer === pluginVersion) { + resolve({ status: "match", pyVersion: pyVer, pluginVersion, error: null }); + } else { + resolve({ status: "mismatch", pyVersion: pyVer, pluginVersion, error: null }); + } + }); + }); +} + +function classifyError(errorCode) { + const code = String(errorCode); + const patterns = { + ENOENT: { type: "python_missing", message: "Python executable not found", recoverable: true }, + "python-missing": { type: "python_missing", message: "Python executable not found", recoverable: true }, + MODULE_NOT_FOUND: { type: "import_failed", message: "PaperForge package not installed", recoverable: true }, + "import-failed": { type: "import_failed", message: "PaperForge package not installed", recoverable: true }, + "version-mismatch": { type: "version_mismatch", message: "Plugin and package versions differ", recoverable: true, action: "sync-runtime" }, + "pip-failed": { type: "pip_install_failure", message: "pip install command failed", recoverable: true }, + ETIMEDOUT: { type: "timeout", message: "Subprocess timed out", recoverable: true, action: "retry" }, + timeout: { type: "timeout", message: "Subprocess timed out", recoverable: true, action: "retry" }, + }; + const match = patterns[code]; + if (match) return { ...match }; + return { type: "unknown", message: String(errorCode), recoverable: false }; +} + +function buildRuntimeInstallCommand(pythonExe, version, extraArgs) { + if (extraArgs === undefined) extraArgs = []; + const url = `git+https://github.com/LLLin000/PaperForge.git@${version}`; + const args = [...extraArgs, "-m", "pip", "install", "--upgrade", url]; + return { cmd: pythonExe, args, url, timeout: 120000 }; +} + +function parseRuntimeStatus(err, stdout, stderr) { + if (!err && stdout) { + return { status: "ok", version: stdout.trim() }; + } + if (err && err.code === "ENOENT") { + const classified = classifyError("ENOENT"); + return { status: "error", version: null, ...classified }; + } + if (stderr && stderr.includes("No module named paperforge")) { + const classified = classifyError("import-failed"); + return { status: "error", version: null, ...classified }; + } + if (err && err.killed) { + const classified = classifyError("timeout"); + return { status: "error", version: null, ...classified }; + } + if (stderr && stderr.includes("ModuleNotFoundError")) { + const classified = classifyError("import-failed"); + return { status: "error", version: null, ...classified }; + } + return { status: "error", version: null, type: "unknown", + message: err ? err.message : String(stderr), recoverable: false }; +} + +const ACTIONS = [ + { + id: "paperforge-sync", + title: "Sync Library", + desc: "Pull new references from Zotero and generate literature notes", + icon: "\u21BB", + cmd: "sync", + okMsg: "Sync complete", + }, + { + id: "paperforge-ocr", + title: "Run OCR", + desc: "Extract full text and figures from PDFs via PaddleOCR", + icon: "\u229E", + cmd: "ocr", + okMsg: "OCR started", + }, + { + id: "paperforge-doctor", + title: "Run Doctor", + desc: "Verify PaperForge setup \u2014 check configs, Zotero, paths, and index health", + icon: "\u2695", + cmd: "doctor", + okMsg: "Doctor complete", + }, + { + id: "paperforge-repair", + title: "Repair Issues", + desc: "Fix three-way state divergence, path errors, and rebuild index", + icon: "\u21BA", + cmd: "repair", + args: ["--fix", "--fix-paths"], + okMsg: "Repair complete", + }, +]; + +function buildCommandArgs(action, key, filter) { + const args = Array.isArray(action.args) ? [...action.args] : []; + if (action.needsKey && key) args.push(key); + if (action.needsFilter || filter) args.push("--all"); + return args; +} + +function runSubprocess(pythonExe, args, cwd, timeout, _spawn) { + const sp = _spawn || spawn; + + return new Promise((resolve) => { + const startTime = Date.now(); + const child = sp(pythonExe, args, { cwd, timeout, windowsHide: true }); + const stdoutChunks = []; + const stderrChunks = []; + + child.stdout.on("data", (data) => { stdoutChunks.push(data.toString("utf-8")); }); + child.stderr.on("data", (data) => { stderrChunks.push(data.toString("utf-8")); }); + + child.on("close", (code) => { + resolve({ stdout: stdoutChunks.join(""), stderr: stderrChunks.join(""), + exitCode: code, elapsed: Date.now() - startTime }); + }); + + child.on("error", (err) => { + resolve({ stdout: stdoutChunks.join(""), + stderr: stderrChunks.join("") + "\n" + err.message, + exitCode: -1, elapsed: Date.now() - startTime }); + }); + }); +} From 2ad8af1474bfc2f956816f7bfd70449ec3b87f6b Mon Sep 17 00:00:00 2001 From: Research Assistant Date: Sun, 10 May 2026 14:29:32 +0800 Subject: [PATCH 11/37] fix(dashboard): fix discussion card path on Windows + OCR token fallback check - Discussion card: use lastIndexOf('/') instead of path.dirname to avoid Windows backslash separator breaking Obsidian vault path resolution - OCR token: also check .env PADDLEOCR_API_TOKEN as fallback when plugin settings paddleocr_api_key is empty --- paperforge/plugin/main.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/paperforge/plugin/main.js b/paperforge/plugin/main.js index 6ded452f..88658ac6 100644 --- a/paperforge/plugin/main.js +++ b/paperforge/plugin/main.js @@ -1267,8 +1267,19 @@ class PaperForgeStatusView extends ItemView { } catch (_) {} this._renderSystemStatusRow(statusGrid, 'Zotero Export', exportOk ? 'healthy' : 'missing', exportDetail); - // OCR token - const tokenOk = !!(plugin?.settings?.paddleocr_api_key); + // OCR token (check plugin settings + .env fallback) + let tokenOk = !!(plugin?.settings?.paddleocr_api_key); + if (!tokenOk) { + try { + const sysDir = plugin?.settings?.system_dir || 'System'; + const envPath = path.join(vp, sysDir, 'PaperForge', '.env'); + if (fs.existsSync(envPath)) { + const envContent = fs.readFileSync(envPath, 'utf-8'); + const tokenMatch = envContent.match(/^PADDLEOCR_API_TOKEN\s*=\s*(.+)$/m); + tokenOk = !!(tokenMatch && tokenMatch[1] && tokenMatch[1].trim()); + } + } catch (_) {} + } this._renderSystemStatusRow(statusGrid, 'OCR Token', tokenOk ? 'configured' : 'missing', tokenOk ? 'Configured' : 'Not set'); @@ -1517,7 +1528,8 @@ class PaperForgeStatusView extends ItemView { card.style.display = 'none'; // hidden by default if (!entry.note_path) return; - const wsDir = path.dirname(entry.note_path); + const lastSlash = entry.note_path.lastIndexOf('/'); + const wsDir = lastSlash !== -1 ? entry.note_path.substring(0, lastSlash) : '.'; const discPath = wsDir + '/ai/discussion.json'; this.app.vault.adapter.read(discPath).then((raw) => { From 01dddfcd0a56f328f9ba534b6422dd7561bf34f5 Mon Sep 17 00:00:00 2001 From: Research Assistant Date: Sun, 10 May 2026 14:31:36 +0800 Subject: [PATCH 12/37] fix(dashboard): check OS env for PaddleOCR token (match doctor logic) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Doctor reads PADDLEOCR_API_TOKEN from OS environment variables - Dashboard now checks 3 sources: plugin settings → .env file → process.env --- paperforge/plugin/main.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/paperforge/plugin/main.js b/paperforge/plugin/main.js index 88658ac6..85be9f2c 100644 --- a/paperforge/plugin/main.js +++ b/paperforge/plugin/main.js @@ -1267,7 +1267,7 @@ class PaperForgeStatusView extends ItemView { } catch (_) {} this._renderSystemStatusRow(statusGrid, 'Zotero Export', exportOk ? 'healthy' : 'missing', exportDetail); - // OCR token (check plugin settings + .env fallback) + // OCR token (check: plugin settings → .env file → OS environment) let tokenOk = !!(plugin?.settings?.paddleocr_api_key); if (!tokenOk) { try { @@ -1280,6 +1280,9 @@ class PaperForgeStatusView extends ItemView { } } catch (_) {} } + if (!tokenOk) { + tokenOk = !!(process.env.PADDLEOCR_API_TOKEN || process.env.PADDLEOCR_API_KEY || process.env.OCR_TOKEN); + } this._renderSystemStatusRow(statusGrid, 'OCR Token', tokenOk ? 'configured' : 'missing', tokenOk ? 'Configured' : 'Not set'); From 25b91b29ca7ff868eadb65aa601199b8c6da14df Mon Sep 17 00:00:00 2001 From: Research Assistant Date: Sun, 10 May 2026 14:38:22 +0800 Subject: [PATCH 13/37] fix(dashboard): only show issue panel for serious blockers - Collection view: remove issue summary entirely (health counts are workflow state, not errors) - Global view: only show issues for runtime mismatch, index missing, export missing, token missing - Remove pdfIssues aggregation (not critical) - CSS: soften issue colors (orange left border instead of red, muted text) --- paperforge/plugin/main.js | 31 ++++--------------------------- paperforge/plugin/styles.css | 20 ++++---------------- 2 files changed, 8 insertions(+), 43 deletions(-) diff --git a/paperforge/plugin/main.js b/paperforge/plugin/main.js index 85be9f2c..6413d752 100644 --- a/paperforge/plugin/main.js +++ b/paperforge/plugin/main.js @@ -1210,13 +1210,11 @@ class PaperForgeStatusView extends ItemView { // ── Library Snapshot ── const items = this._getCachedIndex(); const totalPapers = items.length; - let pdfReady = 0, ocrDone = 0, deepReadDone = 0, pdfIssues = 0; + let pdfReady = 0, ocrDone = 0, deepReadDone = 0; for (const item of items) { if (item.has_pdf) pdfReady++; if (item.ocr_status === 'done') ocrDone++; if (item.deep_reading_status === 'done') deepReadDone++; - const h = item.health || {}; - if (h.pdf_health && h.pdf_health !== 'healthy') pdfIssues++; } const snapshot = view.createEl('div', { cls: 'paperforge-library-snapshot' }); @@ -1286,14 +1284,13 @@ class PaperForgeStatusView extends ItemView { this._renderSystemStatusRow(statusGrid, 'OCR Token', tokenOk ? 'configured' : 'missing', tokenOk ? 'Configured' : 'Not set'); - // ── Issues Panel (only when issues exist) ── + // ── Issues Panel (only for serious blockers) ── const hasVersionMismatch = !runtimeOk && pyVer !== '\u2014'; - const hasIssues = pdfIssues > 0 || hasVersionMismatch || !indexOk || !exportOk || !tokenOk; + const hasIssues = hasVersionMismatch || !indexOk || !exportOk || !tokenOk; if (hasIssues) { const issueSection = view.createEl('div', { cls: 'paperforge-issue-summary' }); - issueSection.createEl('div', { cls: 'paperforge-section-label', text: 'Issues' }); + issueSection.createEl('div', { cls: 'paperforge-section-label', text: '需要处理' }); const issueList = issueSection.createEl('div', { cls: 'paperforge-issue-list' }); - if (pdfIssues > 0) issueList.createEl('div', { cls: 'paperforge-issue-item', text: pdfIssues + ' PDF path errors' }); if (hasVersionMismatch) issueList.createEl('div', { cls: 'paperforge-issue-item', text: 'Runtime version mismatch' }); if (!indexOk) issueList.createEl('div', { cls: 'paperforge-issue-item', text: 'Index missing or corrupted' }); if (!exportOk) issueList.createEl('div', { cls: 'paperforge-issue-item', text: 'No Zotero export found' }); @@ -1734,7 +1731,6 @@ class PaperForgeStatusView extends ItemView { const totalPapers = domainItems.length; let hasPdf = 0, ocrDone = 0, analyzeReady = 0, deepRead = 0; let ocrPending = 0, ocrProcessing = 0, ocrFailed = 0; - let pdfIssues = 0, ocrIssues = 0, noteIssues = 0, assetIssues = 0; for (const item of domainItems) { if (item.has_pdf) hasPdf++; @@ -1746,12 +1742,6 @@ class PaperForgeStatusView extends ItemView { if (ocs === 'pending' || ocs === 'queued') ocrPending++; else if (ocs === 'processing') ocrProcessing++; else if (ocs === 'failed' || ocs === 'blocked' || ocs === 'done_incomplete' || ocs === 'nopdf') ocrFailed++; - - const health = item.health || {}; - if (health.pdf_health && health.pdf_health !== 'healthy') pdfIssues++; - if (health.ocr_health && health.ocr_health !== 'healthy') ocrIssues++; - if (health.note_health && health.note_health !== 'healthy') noteIssues++; - if (health.asset_health && health.asset_health !== 'healthy') assetIssues++; } // ── Header ── @@ -1818,19 +1808,6 @@ class PaperForgeStatusView extends ItemView { } } - // ── Issue Summary (compact, only when issues exist) ── - const totalIssues = pdfIssues + ocrIssues + noteIssues + assetIssues; - if (totalIssues > 0) { - const issueSection = view.createEl('div', { cls: 'paperforge-issue-summary' }); - issueSection.createEl('div', { cls: 'paperforge-section-label', text: 'Issues' }); - const parts = []; - if (pdfIssues > 0) parts.push(pdfIssues + ' PDF'); - if (ocrIssues > 0) parts.push(ocrIssues + ' OCR'); - if (noteIssues > 0) parts.push(noteIssues + ' Note'); - if (assetIssues > 0) parts.push(assetIssues + ' Asset'); - issueSection.createEl('div', { cls: 'paperforge-issue-text', text: parts.join(' · ') + ' issues' }); - } - // ── Contextual Actions ── const actionsRow = view.createEl('div', { cls: 'paperforge-collection-actions' }); const syncBtn = actionsRow.createEl('button', { cls: 'paperforge-contextual-btn' }); diff --git a/paperforge/plugin/styles.css b/paperforge/plugin/styles.css index 55d6ebb9..85b0939e 100644 --- a/paperforge/plugin/styles.css +++ b/paperforge/plugin/styles.css @@ -1955,12 +1955,13 @@ margin: 12px 0; padding: 12px 14px; background: var(--background-secondary); - border: 1px solid var(--text-error); + border: 1px solid var(--background-modifier-border); + border-left: 3px solid var(--color-orange); border-radius: var(--radius-m); } .paperforge-issue-summary .paperforge-section-label { - color: var(--text-error); + color: var(--color-orange); } .paperforge-issue-text { @@ -1975,21 +1976,8 @@ .paperforge-issue-item { font-size: var(--font-ui-smaller); - color: var(--text-error); + color: var(--text-muted); padding: 2px 0; - padding-left: 12px; - position: relative; -} - -.paperforge-issue-item::before { - content: ''; - position: absolute; - left: 0; - top: 8px; - width: 5px; - height: 5px; - border-radius: 50%; - background: var(--text-error); } .paperforge-issue-actions { From 806292febb3d7a9e7f90902fec576f5019166ee2 Mon Sep 17 00:00:00 2001 From: Research Assistant Date: Sun, 10 May 2026 14:57:12 +0800 Subject: [PATCH 14/37] style(dashboard): luxury-minimal visual refinement - Section labels: accent left-border matching metric card pattern - Cards: ::before pseudo-element for smooth elevation on hover - Per-paper header: larger title (16px), author (13px)/year (12px faint) hierarchy - Body text: bumped from 11px to 13.5px for overview/discussion - Status pills: rgba() translucent backgrounds with colored text - Dark theme: deeper shadow on card hover (0 2px 12px rgba(0,0,0,0.3)) - Refined spacing, font weights, and transition timing for premium feel --- paperforge/plugin/styles.css | 428 ++++++++++++++++++++++------------- 1 file changed, 274 insertions(+), 154 deletions(-) diff --git a/paperforge/plugin/styles.css b/paperforge/plugin/styles.css index 85b0939e..8a8243e6 100644 --- a/paperforge/plugin/styles.css +++ b/paperforge/plugin/styles.css @@ -1139,20 +1139,23 @@ } .paperforge-paper-header { - padding-bottom: 8px; + padding-bottom: 10px; border-bottom: 1px solid var(--background-modifier-border); } .paperforge-paper-title { - font-size: var(--font-ui-large); - font-weight: var(--font-semibold); + font-size: 16px; + font-weight: 600; color: var(--text-normal); line-height: 1.4; - margin: 0 0 6px 0; + margin: 0 0 4px 0; } .paperforge-paper-meta { - font-size: var(--font-ui-small); + display: flex; + align-items: baseline; + gap: 4px; + font-size: 13px; color: var(--text-muted); line-height: 1.5; } @@ -1163,10 +1166,13 @@ .paperforge-paper-year { display: inline; + font-size: 12px; + color: var(--text-faint); } .paperforge-paper-year::before { - content: " · "; + content: " \u00B7 "; + color: var(--text-faint); } .paperforge-paper-actions { @@ -1572,28 +1578,31 @@ SECTION 39 — Dashboard Redesign: Shared Components ========================================================================== */ -/* ── Section Label (used across global / collection / paper) ── */ +/* ── Section Label ── */ .paperforge-section-label { - font-size: var(--font-ui-small); - font-weight: var(--font-semibold); - color: var(--text-normal); + font-size: 11px; + font-weight: 600; + color: var(--text-muted); margin: 0 0 10px 0; + padding-left: 10px; + border-left: 3px solid var(--interactive-accent); text-transform: uppercase; - letter-spacing: 0.5px; + letter-spacing: 0.06em; + line-height: 1.3; } -/* ── Contextual Button (card-action style, used across all views) ── */ +/* ── Contextual Button ── */ .paperforge-contextual-btn { display: inline-flex; align-items: center; - gap: 6px; - padding: 6px 14px; + gap: 5px; + padding: 5px 12px; background: var(--background-secondary); border: 1px solid var(--background-modifier-border); border-radius: var(--radius-s); color: var(--text-muted); - font-size: var(--font-ui-smaller); - font-weight: var(--font-medium); + font-size: 12px; + font-weight: 500; cursor: pointer; transition: background 0.15s, border-color 0.15s, color 0.15s; white-space: nowrap; @@ -1616,64 +1625,82 @@ } /* ========================================================================== - SECTION 40 — Dashboard Redesign: Per-Paper View (Reading Companion) + SECTION 40 — Per-Paper View (Reading Companion) ========================================================================== */ /* ── Status Strip ── */ .paperforge-status-strip { display: flex; - gap: 8px; + gap: 6px; padding: 8px 0; - border-top: 1px solid var(--background-modifier-border); - border-bottom: 1px solid var(--background-modifier-border); - margin: 10px 0; + margin: 6px 0; } .paperforge-status-pill { display: inline-flex; align-items: center; gap: 4px; - padding: 3px 12px; - border-radius: 10px; - font-size: 11px; - font-weight: var(--font-semibold); - background: var(--background-modifier-hover); + padding: 2px 10px; + border-radius: 4px; + font-size: 12px; + font-weight: 500; + line-height: 1.5; + background: var(--background-primary-alt); color: var(--text-muted); - line-height: 1.4; } .paperforge-status-pill.ok { - background: var(--color-green); - color: var(--text-on-accent); + background: rgba(var(--background-modifier-success-rgb, 0, 200, 100), 0.12); + color: var(--text-success, var(--color-green)); } .paperforge-status-pill.fail { - background: var(--text-error); - color: var(--text-on-accent); + background: rgba(var(--background-modifier-error-rgb, 255, 80, 80), 0.12); + color: var(--text-error); } .paperforge-status-pill.pending { - background: var(--background-modifier-hover); + background: var(--background-primary-alt); color: var(--text-muted); } .paperforge-status-pill-icon { - font-size: 10px; + font-size: 11px; line-height: 1; } +/* Theme dark: softer pill backgrounds */ +.theme-dark .paperforge-status-pill.ok { + background: rgba(100, 200, 120, 0.15); +} +.theme-dark .paperforge-status-pill.fail { + background: rgba(255, 100, 100, 0.15); +} + /* ── Paper Overview Card ── */ .paperforge-paper-overview { - margin: 12px 0; - padding: 14px; - background: var(--background-secondary); + margin: 10px 0; + padding: 16px; + background: var(--background-primary); border: 1px solid var(--background-modifier-border); border-radius: var(--radius-m); - transition: border-color 0.15s; + transition: border-color 0.2s, box-shadow 0.2s; + position: relative; } -.paperforge-paper-overview:hover { - border-color: var(--interactive-accent); +.paperforge-paper-overview::before { + content: ''; + position: absolute; + inset: -1px; + border-radius: inherit; + opacity: 0; + box-shadow: 0 2px 8px rgba(0,0,0,0.08); + transition: opacity 0.2s; + pointer-events: none; +} + +.paperforge-paper-overview:hover::before { + opacity: 1; } .paperforge-paper-overview-header { @@ -1681,17 +1708,17 @@ } .paperforge-paper-overview-title { - font-size: var(--font-ui-small); - font-weight: var(--font-semibold); + font-size: 11px; + font-weight: 700; color: var(--text-accent); text-transform: uppercase; - letter-spacing: 0.5px; + letter-spacing: 0.06em; } .paperforge-paper-overview-body { - font-size: var(--font-ui-smaller); + font-size: 13.5px; color: var(--text-muted); - line-height: 1.6; + line-height: 1.65; } .paperforge-paper-overview-excerpt { @@ -1702,71 +1729,84 @@ .paperforge-paper-overview-expand { display: inline-block; margin-top: 6px; - padding: 2px 0; + padding: 3px 0; background: none; border: none; + border-bottom: 1px dashed var(--text-accent); color: var(--text-accent); - font-size: var(--font-ui-smaller); - font-weight: var(--font-medium); + font-size: 12px; + font-weight: 500; cursor: pointer; - transition: color 0.15s; + transition: color 0.15s, border-color 0.15s; } .paperforge-paper-overview-expand:hover { color: var(--interactive-accent-hover); - text-decoration: underline; + border-color: var(--interactive-accent-hover); } /* ── Discussion Card ── */ .paperforge-discussion-card { - margin: 12px 0; - padding: 14px; - background: var(--background-secondary); + margin: 10px 0; + padding: 16px; + background: var(--background-primary); border: 1px solid var(--background-modifier-border); border-radius: var(--radius-m); - transition: border-color 0.15s; + transition: border-color 0.2s, box-shadow 0.2s; + position: relative; } -.paperforge-discussion-card:hover { - border-color: var(--interactive-accent); +.paperforge-discussion-card::before { + content: ''; + position: absolute; + inset: -1px; + border-radius: inherit; + opacity: 0; + box-shadow: 0 2px 8px rgba(0,0,0,0.08); + transition: opacity 0.2s; + pointer-events: none; +} + +.paperforge-discussion-card:hover::before { + opacity: 1; } .paperforge-discussion-header { - margin-bottom: 8px; + margin-bottom: 10px; } .paperforge-discussion-title { - font-size: var(--font-ui-small); - font-weight: var(--font-semibold); + font-size: 11px; + font-weight: 700; color: var(--text-accent); text-transform: uppercase; - letter-spacing: 0.5px; + letter-spacing: 0.06em; } .paperforge-discussion-item { - margin-bottom: 8px; - padding-bottom: 8px; - border-bottom: 1px solid var(--background-modifier-border-hover); + margin-bottom: 10px; + padding-bottom: 10px; + border-bottom: 1px solid var(--background-modifier-border); } .paperforge-discussion-item:last-child { border-bottom: none; - margin-bottom: 4px; + margin-bottom: 0; padding-bottom: 0; } .paperforge-discussion-q { - font-size: var(--font-ui-smaller); - font-weight: var(--font-semibold); + font-size: 13.5px; + font-weight: 600; color: var(--text-normal); margin-bottom: 4px; line-height: 1.5; } .paperforge-discussion-a { - font-size: var(--font-ui-smaller); + font-size: 13.5px; color: var(--text-muted); - line-height: 1.5; + line-height: 1.6; } .paperforge-discussion-expand { @@ -1775,24 +1815,24 @@ margin-left: 4px; background: none; border: none; + border-bottom: 1px dashed var(--text-accent); color: var(--text-accent); - font-size: var(--font-ui-smaller); - font-weight: var(--font-medium); + font-size: 12px; + font-weight: 500; cursor: pointer; transition: color 0.15s; } .paperforge-discussion-expand:hover { color: var(--interactive-accent-hover); - text-decoration: underline; } .paperforge-discussion-viewall { display: block; text-align: right; margin-top: 6px; - font-size: var(--font-ui-smaller); - font-weight: var(--font-medium); + font-size: 12px; + font-weight: 500; color: var(--text-accent); cursor: pointer; text-decoration: none; @@ -1801,53 +1841,54 @@ .paperforge-discussion-viewall:hover { color: var(--interactive-accent-hover); + text-decoration: underline; } /* ── Paper Files Row ── */ .paperforge-paper-files { display: flex; gap: 8px; - margin: 12px 0; + margin: 10px 0; } /* ── Technical Details ── */ .paperforge-technical-details { - margin: 8px 0; + margin: 6px 0; } .paperforge-technical-details-toggle { width: 100%; - padding: 8px 12px; - background: var(--background-secondary); + padding: 6px 12px; + background: var(--background-primary); border: 1px solid var(--background-modifier-border); border-radius: var(--radius-s); - color: var(--text-muted); - font-size: var(--font-ui-smaller); - font-weight: var(--font-medium); + color: var(--text-faint); + font-size: 11px; + font-weight: 500; cursor: pointer; text-align: left; - transition: background 0.15s, border-color 0.15s; + transition: background 0.15s, border-color 0.15s, color 0.15s; } .paperforge-technical-details-toggle:hover { background: var(--background-modifier-hover); - border-color: var(--interactive-accent); - color: var(--text-normal); + border-color: var(--background-modifier-border-hover); + color: var(--text-muted); } .paperforge-technical-details-body { - margin-top: 6px; - padding: 10px 14px; + margin-top: 4px; + padding: 8px 12px; background: var(--background-primary-alt); border: 1px solid var(--background-modifier-border); border-radius: var(--radius-s); - font-size: var(--font-ui-smaller); + font-size: 11px; } .paperforge-technical-row { display: flex; justify-content: space-between; - padding: 3px 0; + padding: 2px 0; } .paperforge-technical-label { @@ -1866,45 +1907,57 @@ } /* ========================================================================== - SECTION 41 — Dashboard Redesign: Collection / Base View (Batch Workspace) + SECTION 41 — Collection / Base View (Batch Workspace) ========================================================================== */ /* ── Collection Header ── */ .paperforge-collection-header { - margin-bottom: 12px; + margin-bottom: 10px; } .paperforge-collection-title { - font-size: var(--font-ui-medium); - font-weight: var(--font-bold); + font-size: 16px; + font-weight: 600; color: var(--text-normal); line-height: 1.3; } .paperforge-collection-count { - font-size: var(--font-ui-smaller); + font-size: 12px; color: var(--text-muted); margin-top: 2px; } -/* ── Workflow Overview (funnel) ── */ +/* ── Workflow Overview ── */ .paperforge-workflow-overview { - margin: 12px 0; + margin: 10px 0; padding: 16px; - background: var(--background-secondary); + background: var(--background-primary); border: 1px solid var(--background-modifier-border); border-radius: var(--radius-m); - transition: border-color 0.15s; + transition: border-color 0.2s, box-shadow 0.2s; + position: relative; } -.paperforge-workflow-overview:hover { - border-color: var(--interactive-accent); +.paperforge-workflow-overview::before { + content: ''; + position: absolute; + inset: -1px; + border-radius: inherit; + opacity: 0; + box-shadow: 0 2px 8px rgba(0,0,0,0.08); + transition: opacity 0.2s; + pointer-events: none; +} + +.paperforge-workflow-overview:hover::before { + opacity: 1; } .paperforge-workflow-funnel { display: flex; align-items: center; - gap: 4px; + gap: 3px; flex-wrap: wrap; justify-content: center; } @@ -1913,15 +1966,15 @@ display: flex; flex-direction: column; align-items: center; - padding: 8px 14px; + padding: 8px 12px; background: var(--background-primary-alt); - border-radius: var(--radius-s); - min-width: 60px; + border-radius: 6px; + min-width: 56px; } .paperforge-workflow-stage-value { - font-size: clamp(16px, 3vw, 22px); - font-weight: var(--font-bold); + font-size: 20px; + font-weight: 700; color: var(--text-accent); line-height: 1.2; letter-spacing: -0.3px; @@ -1938,7 +1991,7 @@ .paperforge-workflow-arrow { color: var(--text-faint); - font-size: var(--font-ui-smaller); + font-size: 11px; flex-shrink: 0; } @@ -1950,11 +2003,11 @@ margin-bottom: 14px; } -/* ── Issue Summary ── */ +/* ── Issue Summary (serious blockers only) ── */ .paperforge-issue-summary { - margin: 12px 0; + margin: 10px 0; padding: 12px 14px; - background: var(--background-secondary); + background: var(--background-primary); border: 1px solid var(--background-modifier-border); border-left: 3px solid var(--color-orange); border-radius: var(--radius-m); @@ -1964,18 +2017,12 @@ color: var(--color-orange); } -.paperforge-issue-text { - font-size: var(--font-ui-smaller); - color: var(--text-muted); - line-height: 1.5; -} - .paperforge-issue-list { - margin: 6px 0 8px 0; + margin: 4px 0 6px 0; } .paperforge-issue-item { - font-size: var(--font-ui-smaller); + font-size: 13px; color: var(--text-muted); padding: 2px 0; } @@ -1983,33 +2030,45 @@ .paperforge-issue-actions { display: flex; gap: 8px; - margin-top: 10px; + margin-top: 8px; } /* ── Collection Actions ── */ .paperforge-collection-actions { display: flex; gap: 8px; - margin-top: 12px; - padding-top: 12px; + margin-top: 10px; + padding-top: 10px; border-top: 1px solid var(--background-modifier-border); } /* ========================================================================== - SECTION 42 — Dashboard Redesign: Global / Home View (System Homepage) + SECTION 42 — Global / Home View (System Homepage) ========================================================================== */ /* ── Library Snapshot ── */ .paperforge-library-snapshot { padding: 16px; - background: var(--background-secondary); + background: var(--background-primary); border: 1px solid var(--background-modifier-border); border-radius: var(--radius-m); - transition: border-color 0.15s; + transition: border-color 0.2s, box-shadow 0.2s; + position: relative; } -.paperforge-library-snapshot:hover { - border-color: var(--interactive-accent); +.paperforge-library-snapshot::before { + content: ''; + position: absolute; + inset: -1px; + border-radius: inherit; + opacity: 0; + box-shadow: 0 2px 8px rgba(0,0,0,0.08); + transition: opacity 0.2s; + pointer-events: none; +} + +.paperforge-library-snapshot:hover::before { + opacity: 1; } .paperforge-snapshot-pills { @@ -2022,16 +2081,16 @@ display: flex; flex-direction: column; align-items: center; - padding: 10px 16px; + padding: 10px 14px; background: var(--background-primary-alt); - border-radius: var(--radius-s); - min-width: 64px; + border-radius: 6px; + min-width: 60px; flex: 1; } .paperforge-snapshot-value { - font-size: clamp(16px, 3vw, 22px); - font-weight: var(--font-bold); + font-size: 20px; + font-weight: 700; color: var(--text-accent); line-height: 1.2; letter-spacing: -0.3px; @@ -2049,32 +2108,44 @@ /* ── System Status ── */ .paperforge-system-status { padding: 16px; - background: var(--background-secondary); + background: var(--background-primary); border: 1px solid var(--background-modifier-border); border-radius: var(--radius-m); - transition: border-color 0.15s; + transition: border-color 0.2s, box-shadow 0.2s; + position: relative; } -.paperforge-system-status:hover { - border-color: var(--interactive-accent); +.paperforge-system-status::before { + content: ''; + position: absolute; + inset: -1px; + border-radius: inherit; + opacity: 0; + box-shadow: 0 2px 8px rgba(0,0,0,0.08); + transition: opacity 0.2s; + pointer-events: none; +} + +.paperforge-system-status:hover::before { + opacity: 1; } .paperforge-status-grid { display: flex; flex-direction: column; - gap: 2px; + gap: 1px; } .paperforge-status-row { display: flex; align-items: center; - gap: 10px; - padding: 5px 0; + gap: 8px; + padding: 4px 0; } .paperforge-status-dot { - width: 8px; - height: 8px; + width: 7px; + height: 7px; border-radius: 50%; flex-shrink: 0; background: var(--background-modifier-border); @@ -2090,13 +2161,13 @@ } .paperforge-status-label { - font-size: var(--font-ui-smaller); + font-size: 13px; color: var(--text-muted); - min-width: 80px; + min-width: 85px; } .paperforge-status-detail { - font-size: var(--font-ui-smaller); + font-size: 12px; color: var(--text-faint); overflow: hidden; text-overflow: ellipsis; @@ -2106,14 +2177,26 @@ /* ── Global Actions ── */ .paperforge-global-actions { padding: 16px; - background: var(--background-secondary); + background: var(--background-primary); border: 1px solid var(--background-modifier-border); border-radius: var(--radius-m); - transition: border-color 0.15s; + transition: border-color 0.2s, box-shadow 0.2s; + position: relative; } -.paperforge-global-actions:hover { - border-color: var(--interactive-accent); +.paperforge-global-actions::before { + content: ''; + position: absolute; + inset: -1px; + border-radius: inherit; + opacity: 0; + box-shadow: 0 2px 8px rgba(0,0,0,0.08); + transition: opacity 0.2s; + pointer-events: none; +} + +.paperforge-global-actions:hover::before { + opacity: 1; } .paperforge-global-actions-row { @@ -2122,22 +2205,34 @@ } /* ========================================================================== - SECTION 43 — Dashboard Redesign: Workflow Toggle Checkboxes (per-paper) + SECTION 43 — Workflow Toggle Checkboxes (per-paper) ========================================================================== */ .paperforge-workflow-toggles { display: flex; - gap: 16px; - padding: 10px 14px; - background: var(--background-secondary); + gap: 14px; + padding: 12px 16px; + background: var(--background-primary); border: 1px solid var(--background-modifier-border); border-radius: var(--radius-m); - margin: 12px 0; - transition: border-color 0.15s; + margin: 10px 0; + transition: border-color 0.2s, box-shadow 0.2s; + position: relative; } -.paperforge-workflow-toggles:hover { - border-color: var(--interactive-accent); +.paperforge-workflow-toggles::before { + content: ''; + position: absolute; + inset: -1px; + border-radius: inherit; + opacity: 0; + box-shadow: 0 2px 8px rgba(0,0,0,0.08); + transition: opacity 0.2s; + pointer-events: none; +} + +.paperforge-workflow-toggles:hover::before { + opacity: 1; } .paperforge-workflow-toggle { @@ -2145,7 +2240,7 @@ align-items: center; gap: 6px; cursor: pointer; - font-size: var(--font-ui-smaller); + font-size: 13px; color: var(--text-muted); user-select: none; } @@ -2159,7 +2254,7 @@ } .paperforge-workflow-toggle-label { - font-weight: var(--font-semibold); + font-weight: 600; color: var(--text-normal); } @@ -2167,3 +2262,28 @@ color: var(--text-faint); font-size: 11px; } + +/* ========================================================================== + SECTION 44 — Dark Theme Specific Overrides + ========================================================================== */ + +.theme-dark .paperforge-paper-overview, +.theme-dark .paperforge-discussion-card, +.theme-dark .paperforge-workflow-overview, +.theme-dark .paperforge-library-snapshot, +.theme-dark .paperforge-system-status, +.theme-dark .paperforge-global-actions, +.theme-dark .paperforge-workflow-toggles { + background: var(--background-primary); + border-color: var(--background-modifier-border); +} + +.theme-dark .paperforge-paper-overview::before, +.theme-dark .paperforge-discussion-card::before, +.theme-dark .paperforge-workflow-overview::before, +.theme-dark .paperforge-library-snapshot::before, +.theme-dark .paperforge-system-status::before, +.theme-dark .paperforge-global-actions::before, +.theme-dark .paperforge-workflow-toggles::before { + box-shadow: 0 2px 12px rgba(0,0,0,0.3); +} From fd47d52c13bc7a251eb0df00e99ce97b41a11b1e Mon Sep 17 00:00:00 2001 From: Research Assistant Date: Sun, 10 May 2026 15:19:59 +0800 Subject: [PATCH 15/37] fix(dashboard): fix discussion card rendering after sync read migration - Replace leftover .catch() from old Promise chain with proper try/catch - Use fs.readFileSync for reliable file access in Obsidian plugin context --- paperforge/plugin/main.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/paperforge/plugin/main.js b/paperforge/plugin/main.js index 6413d752..93229e6a 100644 --- a/paperforge/plugin/main.js +++ b/paperforge/plugin/main.js @@ -1532,7 +1532,12 @@ class PaperForgeStatusView extends ItemView { const wsDir = lastSlash !== -1 ? entry.note_path.substring(0, lastSlash) : '.'; const discPath = wsDir + '/ai/discussion.json'; - this.app.vault.adapter.read(discPath).then((raw) => { + const vp = this.app.vault.adapter.basePath; + const absPath = path.join(vp, discPath); + + try { + if (!fs.existsSync(absPath)) return; + const raw = fs.readFileSync(absPath, 'utf-8'); const data = JSON.parse(raw); if (!data.sessions || data.sessions.length === 0) return; @@ -1577,9 +1582,9 @@ class PaperForgeStatusView extends ItemView { new Notice('讨论文件尚未生成'); } }); - }).catch(() => { + } catch (e) { // No discussion.json — card stays hidden - }); + } } /* ── Paper Files Row ── */ From 82bcbc7946ca68960cc90aaca542f7f651484af1 Mon Sep 17 00:00:00 2001 From: Research Assistant Date: Sun, 10 May 2026 15:28:58 +0800 Subject: [PATCH 16/37] fix(dashboard): remove redundant _refreshCurrentMode from checkbox handler - processFrontMatter triggers modify event which auto-refreshes the view - Double refresh was resetting technical details toggle state (flash on first click) - Discussion card only shows for papers with ai/discussion.json (currently 2Y9M3ILK only) --- paperforge/plugin/main.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/paperforge/plugin/main.js b/paperforge/plugin/main.js index 93229e6a..1ca6fdd8 100644 --- a/paperforge/plugin/main.js +++ b/paperforge/plugin/main.js @@ -1405,7 +1405,7 @@ class PaperForgeStatusView extends ItemView { await this.app.fileManager.processFrontMatter(noteFile, (fm) => { fm[tf.key] = newVal; }); this._patchCachedEntry(key, { [tf.key]: newVal }); this._currentPaperEntry = patchEntryWorkflowState(this._currentPaperEntry, { [tf.key]: newVal }); - this._refreshCurrentMode(); // sync UI; workers pick up frontmatter change from file + // modify event handler auto-refreshes — no explicit _refreshCurrentMode needed }); } @@ -1583,7 +1583,7 @@ class PaperForgeStatusView extends ItemView { } }); } catch (e) { - // No discussion.json — card stays hidden + console.error('PaperForge: discussion.json read error', discPath, e.message); } } From f8fac94222bfe341a4dba5cfcc39d15feeacebeb Mon Sep 17 00:00:00 2001 From: Research Assistant Date: Sun, 10 May 2026 15:59:20 +0800 Subject: [PATCH 17/37] style(dashboard): establish CSS base --pf-* vars, typography tokens, utility classes --- paperforge/plugin/styles.css | 116 ++++++++++++++++++++++++++++++++++- 1 file changed, 114 insertions(+), 2 deletions(-) diff --git a/paperforge/plugin/styles.css b/paperforge/plugin/styles.css index 8a8243e6..23a66981 100644 --- a/paperforge/plugin/styles.css +++ b/paperforge/plugin/styles.css @@ -5,14 +5,126 @@ /* ── Root Panel ── */ .paperforge-status-panel { - padding: 20px; + font-family: inherit; + font-size: 14px; + color: var(--text-normal); + padding: 14px; height: 100%; overflow-y: auto; display: flex; flex-direction: column; - gap: 24px; + gap: 12px; + --pf-surface: var(--background-secondary); + --pf-surface-alt: var(--background-primary-alt); + --pf-border: var(--background-modifier-border); + --pf-border-hover: var(--background-modifier-border-hover); + --pf-accent: var(--interactive-accent); + --pf-radius: var(--radius-m); } +/* ── Typography tokens ── */ +.pf-title { font-size: 16px; font-weight: 600; line-height: 1.35; color: var(--text-normal); } +.pf-body { font-size: 14px; font-weight: 400; line-height: 1.55; } +.pf-meta { font-size: 13px; font-weight: 400; line-height: 1.4; color: var(--text-muted); } +.pf-label { font-size: 12px; font-weight: 600; line-height: 1.3; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.04em; } + +/* ── Base card (only for primary content modules) ── */ +.pf-card { + background: var(--pf-surface); + border: 1px solid var(--pf-border); + border-radius: var(--pf-radius); + padding: 12px; +} + +/* ── Pill ── */ +.pf-pill { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border-radius: 999px; + background: var(--pf-surface-alt); + color: var(--text-muted); + font-size: 12px; + font-weight: 500; + line-height: 1.4; +} + +.pf-pill--ok { color: var(--text-success); } +.pf-pill--warn { color: var(--text-warning); } +.pf-pill--fail { color: var(--text-error); } + +/* ── Complete state row ── */ +.pf-complete-row { + display: flex; + align-items: center; + gap: 6px; + color: var(--text-success); + font-size: 12px; + font-weight: 400; + line-height: 1.4; + padding: 4px 0; +} + +/* ── Disclosure row (technical details) ── */ +.pf-disclosure-row { + color: var(--text-muted); + font-size: 12px; + font-weight: 400; + line-height: 1.4; + padding: 4px 0; + cursor: pointer; + user-select: none; +} +.pf-disclosure-row:hover { color: var(--text-normal); } +.pf-disclosure-body { padding: 6px 0 2px 0; } + +/* ── Buttons ── */ +.pf-btn-secondary { + display: inline-flex; + align-items: center; + gap: 4px; + background: var(--interactive-normal); + border: 1px solid var(--pf-border); + color: var(--text-normal); + border-radius: var(--button-radius, 4px); + font-size: 12px; + font-weight: 400; + padding: 3px 8px; + cursor: pointer; + transition: background 0.15s; + white-space: nowrap; +} +.pf-btn-secondary:hover { background: var(--interactive-hover); border-color: var(--pf-border-hover); } + +.pf-btn-primary { + background: var(--interactive-accent); + color: var(--text-on-accent); + border-radius: var(--button-radius, 4px); + font-size: 12px; + font-weight: 500; + padding: 4px 10px; + border: none; + cursor: pointer; + transition: opacity 0.15s; +} +.pf-btn-primary:hover { opacity: 0.85; } +.pf-btn-primary.running { opacity: 0.5; pointer-events: none; } + +/* ── Section Label ── */ +.pf-section-label { + font-size: 12px; + font-weight: 600; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.04em; + margin-bottom: 6px; +} + +/* ── Spacing utilities ── */ +.pf-stack-8 > * + * { margin-top: 8px; } +.pf-stack-12 > * + * { margin-top: 12px; } + /* ========================================================================== SECTION 1 — Header ========================================================================== */ From 0f9aae4a88f5af5c1e2d2712da69458e8f4dcb0e Mon Sep 17 00:00:00 2001 From: Research Assistant Date: Sun, 10 May 2026 16:05:45 +0800 Subject: [PATCH 18/37] style(dashboard): rewrite Sections 39-43 per Native Light Surface Design spec - Remove all box-shadows and ::before elevation pseudo-elements - Typography constrained to 4 sizes (16/14/13/12) and 3 weights (600/500/400) - Cards only for primary content modules (overview, discussion, OCR pipe, issues) - Status pills: 999px radius, text-only status colors (no colored backgrounds) - Technical details: inline disclosure row, not a bordered box - Workflow toggles: not a card (simple flex row) - Contextual buttons: Obsidian interactive-normal variables - Section labels: no accent border, simple uppercase 12px muted - Dark theme: no shadows, only background tweaks --- paperforge/plugin/styles.css | 546 +++++++++++++++++++++++++++++++++++ 1 file changed, 546 insertions(+) diff --git a/paperforge/plugin/styles.css b/paperforge/plugin/styles.css index 23a66981..842b8a35 100644 --- a/paperforge/plugin/styles.css +++ b/paperforge/plugin/styles.css @@ -1686,6 +1686,552 @@ padding-top: 8px; } +/* ========================================================================== + SECTION 39 — Shared Components (Native Light Surface Design) + ========================================================================== */ + +/* ── Section Label ── */ +.paperforge-section-label { + font-size: 12px; + font-weight: 600; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.04em; + margin-bottom: 6px; +} + +/* ── Status Pill ── */ +.paperforge-status-pill { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border-radius: 999px; + background: var(--background-primary-alt); + color: var(--text-muted); + font-size: 12px; + font-weight: 500; + line-height: 1.4; +} + +.paperforge-status-pill-icon { font-size: 11px; line-height: 1; } + +.paperforge-status-pill.ok { color: var(--text-success); } +.paperforge-status-pill.fail { color: var(--text-error); } +.paperforge-status-pill.pending { color: var(--text-muted); } + +/* ── Cards (only primary content modules) ── */ +.paperforge-paper-overview, +.paperforge-discussion-card { + margin: 8px 0; + padding: 12px; + background: var(--background-secondary); + border: 1px solid var(--background-modifier-border); + border-radius: var(--radius-m); +} + +/* ── Paper Overview ── */ +.paperforge-paper-overview-header { margin-bottom: 6px; } + +.paperforge-paper-overview-title { + font-size: 12px; + font-weight: 600; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.paperforge-paper-overview-body { + font-size: 14px; + font-weight: 400; + color: var(--text-muted); + line-height: 1.55; +} + +.paperforge-paper-overview-excerpt { + white-space: pre-wrap; + word-break: break-word; +} + +.paperforge-paper-overview-expand { + display: inline-block; + margin-top: 4px; + padding: 2px 0; + background: none; + border: none; + color: var(--text-accent); + font-size: 13px; + font-weight: 400; + cursor: pointer; + transition: color 0.15s; +} + +.paperforge-paper-overview-expand:hover { + color: var(--interactive-accent-hover); +} + +/* ── Discussion Card ── */ +.paperforge-discussion-header { margin-bottom: 8px; } + +.paperforge-discussion-title { + font-size: 12px; + font-weight: 600; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.paperforge-discussion-item { + margin-bottom: 8px; + padding-bottom: 8px; + border-bottom: 1px solid var(--background-modifier-border); +} + +.paperforge-discussion-item:last-child { + border-bottom: none; + margin-bottom: 0; + padding-bottom: 0; +} + +.paperforge-discussion-q { + font-size: 14px; + font-weight: 600; + color: var(--text-normal); + margin-bottom: 4px; + line-height: 1.5; +} + +.paperforge-discussion-a { + font-size: 14px; + font-weight: 400; + color: var(--text-muted); + line-height: 1.55; +} + +.paperforge-discussion-expand { + display: inline; + padding: 0; + margin-left: 4px; + background: none; + border: none; + color: var(--text-accent); + font-size: 13px; + font-weight: 400; + cursor: pointer; + transition: color 0.15s; +} + +.paperforge-discussion-expand:hover { color: var(--interactive-accent-hover); } + +.paperforge-discussion-viewall { + display: block; + text-align: right; + margin-top: 4px; + font-size: 13px; + font-weight: 400; + color: var(--text-accent); + cursor: pointer; + text-decoration: none; + transition: color 0.15s; +} + +.paperforge-discussion-viewall:hover { color: var(--interactive-accent-hover); } + +/* ── Complete state row ── */ +.paperforge-complete-row { + display: flex; + align-items: center; + gap: 6px; + color: var(--text-success); + font-size: 13px; + font-weight: 400; + line-height: 1.4; + padding: 4px 0; +} + +/* ── Technical Details disclosure ── */ +.paperforge-technical-details { + margin: 4px 0; +} + +.paperforge-technical-details-toggle { + display: inline-block; + padding: 4px 0; + background: none; + border: none; + color: var(--text-muted); + font-size: 13px; + font-weight: 400; + cursor: pointer; + user-select: none; + transition: color 0.15s; +} + +.paperforge-technical-details-toggle:hover { color: var(--text-normal); } + +.paperforge-technical-details-body { + padding: 6px 0 2px 0; + font-size: 12px; + color: var(--text-faint); +} + +.paperforge-technical-row { + display: flex; + justify-content: space-between; + padding: 2px 0; +} + +.paperforge-technical-label { + color: var(--text-faint); + flex-shrink: 0; + margin-right: 12px; +} + +.paperforge-technical-value { + color: var(--text-muted); + max-width: 60%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + text-align: right; +} + +/* ── Contextual buttons ── */ +.paperforge-contextual-btn { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 3px 8px; + background: var(--interactive-normal); + border: 1px solid var(--background-modifier-border); + color: var(--text-normal); + border-radius: var(--button-radius, 4px); + font-size: 12px; + font-weight: 400; + cursor: pointer; + transition: background 0.15s; + white-space: nowrap; +} + +.paperforge-contextual-btn:hover { + background: var(--interactive-hover); + border-color: var(--background-modifier-border-hover); +} + +.paperforge-contextual-btn.running { + opacity: 0.6; + pointer-events: none; +} + +.paperforge-contextual-btn-icon { + font-size: 14px; + line-height: 1; +} + +.paperforge-contextual-btn.primary { + background: var(--interactive-accent); + color: var(--text-on-accent); + border-color: var(--interactive-accent); + font-weight: 500; +} + +.paperforge-contextual-btn.primary:hover { + opacity: 0.85; +} + +/* ========================================================================== + SECTION 40 — Per-Paper View + ========================================================================== */ + +/* ── Status Strip + Files Row (merged) ── */ +.paperforge-status-strip { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 8px; + padding: 6px 0; + margin: 4px 0; +} + +.paperforge-status-strip-left { + display: flex; + align-items: center; + gap: 6px; +} + +.paperforge-status-strip-right { + display: flex; + align-items: center; + gap: 6px; +} + +/* ── Workflow Toggles (inline, not a card) ── */ +.paperforge-workflow-toggles { + display: flex; + gap: 14px; + padding: 6px 0; +} + +.paperforge-workflow-toggle { + display: flex; + align-items: center; + gap: 6px; + cursor: pointer; + font-size: 13px; + color: var(--text-muted); + user-select: none; +} + +.paperforge-workflow-checkbox { + width: 15px; + height: 15px; + margin: 0; + cursor: pointer; + accent-color: var(--interactive-accent); +} + +.paperforge-workflow-toggle-label { + font-weight: 500; + color: var(--text-normal); +} + +.paperforge-workflow-toggle-hint { + color: var(--text-faint); + font-size: 12px; +} + +/* ========================================================================== + SECTION 41 — Collection / Base View + ========================================================================== */ + +.paperforge-collection-header { + margin-bottom: 8px; +} + +.paperforge-collection-title { + font-size: 16px; + font-weight: 600; + color: var(--text-normal); + line-height: 1.35; +} + +.paperforge-collection-count { + font-size: 13px; + color: var(--text-muted); + margin-top: 2px; +} + +.paperforge-workflow-overview { + margin: 8px 0; + padding: 12px; + background: var(--background-secondary); + border: 1px solid var(--background-modifier-border); + border-radius: var(--radius-m); +} + +.paperforge-workflow-funnel { + display: flex; + align-items: center; + gap: 3px; + flex-wrap: wrap; + justify-content: center; +} + +.paperforge-workflow-stage { + display: flex; + flex-direction: column; + align-items: center; + padding: 6px 12px; + background: var(--background-primary-alt); + border-radius: 6px; + min-width: 52px; +} + +.paperforge-workflow-stage-value { + font-size: 18px; + font-weight: 600; + color: var(--text-normal); + line-height: 1.2; +} + +.paperforge-workflow-stage-label { + font-size: 11px; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.04em; + margin-top: 2px; + text-align: center; +} + +.paperforge-workflow-arrow { + color: var(--text-faint); + font-size: 12px; + flex-shrink: 0; +} + +.paperforge-collection-ocr-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 14px; +} + +.paperforge-issue-summary { + margin: 8px 0; + padding: 10px 12px; + background: var(--background-secondary); + border: 1px solid var(--background-modifier-border); + border-left: 3px solid var(--color-orange, #e67e22); + border-radius: var(--radius-m); +} + +.paperforge-issue-summary .paperforge-section-label { + color: var(--color-orange, #e67e22); +} + +.paperforge-issue-list { + margin: 4px 0 6px 0; +} + +.paperforge-issue-item { + font-size: 14px; + color: var(--text-muted); + padding: 2px 0; +} + +.paperforge-issue-actions { + display: flex; + gap: 8px; + margin-top: 8px; +} + +.paperforge-collection-actions { + display: flex; + gap: 8px; + margin-top: 8px; + padding-top: 8px; + border-top: 1px solid var(--background-modifier-border); +} + +/* ========================================================================== + SECTION 42 — Global / Home View + ========================================================================== */ + +.paperforge-library-snapshot { + padding: 12px; + background: var(--background-secondary); + border: 1px solid var(--background-modifier-border); + border-radius: var(--radius-m); +} + +.paperforge-snapshot-pills { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.paperforge-snapshot-pill { + display: flex; + flex-direction: column; + align-items: center; + padding: 8px 14px; + background: var(--background-primary-alt); + border-radius: 6px; + min-width: 56px; + flex: 1; +} + +.paperforge-snapshot-value { + font-size: 18px; + font-weight: 600; + color: var(--text-normal); + line-height: 1.2; +} + +.paperforge-snapshot-label { + font-size: 11px; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.04em; + margin-top: 2px; + text-align: center; +} + +.paperforge-system-status { + padding: 12px; + background: var(--background-secondary); + border: 1px solid var(--background-modifier-border); + border-radius: var(--radius-m); +} + +.paperforge-status-grid { + display: flex; + flex-direction: column; + gap: 1px; +} + +.paperforge-status-row { + display: flex; + align-items: center; + gap: 8px; + padding: 3px 0; +} + +.paperforge-status-dot { + width: 7px; + height: 7px; + border-radius: 50%; + flex-shrink: 0; + background: var(--background-modifier-border); + transition: background 0.3s; +} + +.paperforge-status-dot.ok { background: var(--text-success); } +.paperforge-status-dot.fail { background: var(--text-error); } + +.paperforge-status-label { + font-size: 14px; + color: var(--text-muted); + min-width: 85px; +} + +.paperforge-status-detail { + font-size: 13px; + color: var(--text-faint); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.paperforge-global-actions { + padding: 12px; + background: var(--background-secondary); + border: 1px solid var(--background-modifier-border); + border-radius: var(--radius-m); +} + +.paperforge-global-actions-row { + display: flex; + gap: 8px; +} + +/* ========================================================================== + SECTION 43 — Dark Theme + ========================================================================== */ + +.theme-dark .paperforge-status-pill { + background: var(--background-primary-alt); +} + +.theme-dark .paperforge-status-pill.ok { + color: var(--text-success); +} + +.theme-dark .paperforge-status-pill.fail { + color: var(--text-error); +} + /* ========================================================================== SECTION 39 — Dashboard Redesign: Shared Components ========================================================================== */ From 1bb320b70ecad3eefe5cfb827cd234b83f30fb6b Mon Sep 17 00:00:00 2001 From: Research Assistant Date: Sun, 10 May 2026 16:09:53 +0800 Subject: [PATCH 19/37] refactor(dashboard): restructure per-paper layout per visual spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Merge status strip + file buttons into one row (left pills, right buttons) - Move OCR/Analyze toggles into technical details disclosure body - Replace All Set card with compact complete state row - Save/restore technical details expanded state to prevent toggle flash - Delete unused _renderPaperStatusStrip and _renderPaperFilesRow methods - UI text: use Chinese labels (打开 PDF, 打开全文, 加入 OCR, 标记精读) --- paperforge/plugin/main.js | 163 +++++++++++++++++++------------------- 1 file changed, 82 insertions(+), 81 deletions(-) diff --git a/paperforge/plugin/main.js b/paperforge/plugin/main.js index 1ca6fdd8..a9571404 100644 --- a/paperforge/plugin/main.js +++ b/paperforge/plugin/main.js @@ -1366,7 +1366,7 @@ class PaperForgeStatusView extends ItemView { const view = this._contentEl.createEl('div', { cls: 'paperforge-paper-view' }); - // ── Header: title, authors, year ── + // ── Header ── const header = view.createEl('div', { cls: 'paperforge-paper-header' }); header.createEl('div', { cls: 'paperforge-paper-title', text: entry.title || 'Untitled' }); const meta = header.createEl('div', { cls: 'paperforge-paper-meta' }); @@ -1377,51 +1377,11 @@ class PaperForgeStatusView extends ItemView { meta.createEl('span', { cls: 'paperforge-paper-year', text: String(entry.year) }); } - // ── Status Strip: PDF · OCR · DeepRead ── - this._renderPaperStatusStrip(view, entry); + // ── Status Strip + File Buttons (merged row) ── + const strip = view.createEl('div', { cls: 'paperforge-status-strip' }); + const stripLeft = strip.createEl('div', { cls: 'paperforge-status-strip-left' }); + const stripRight = strip.createEl('div', { cls: 'paperforge-status-strip-right' }); - // ── Paper Overview: Pass 1 summary from note body ── - this._renderPaperOverviewCard(view, entry); - - // ── Recent Discussion ── - this._renderRecentDiscussionCard(view, entry); - - // ── Workflow Toggles (checkboxes, same as Base view) ── - const togglesRow = view.createEl('div', { cls: 'paperforge-workflow-toggles' }); - const toggleFields = [ - { key: 'do_ocr', label: 'OCR', hint: '加入 OCR 队列' }, - { key: 'analyze', label: 'Analyze', hint: '标记待精读' }, - ]; - for (const tf of toggleFields) { - const label = togglesRow.createEl('label', { cls: 'paperforge-workflow-toggle' }); - const cb = label.createEl('input', { type: 'checkbox', cls: 'paperforge-workflow-checkbox' }); - cb.checked = entry[tf.key] === true; - label.createEl('span', { cls: 'paperforge-workflow-toggle-label', text: tf.label }); - label.createEl('span', { cls: 'paperforge-workflow-toggle-hint', text: tf.hint }); - cb.addEventListener('change', async () => { - const noteFile = entry.note_path ? this.app.vault.getAbstractFileByPath(entry.note_path) : null; - if (!noteFile) { new Notice('[!!] Note file not found: ' + (entry.note_path || 'unknown'), 6000); return; } - const newVal = cb.checked; - await this.app.fileManager.processFrontMatter(noteFile, (fm) => { fm[tf.key] = newVal; }); - this._patchCachedEntry(key, { [tf.key]: newVal }); - this._currentPaperEntry = patchEntryWorkflowState(this._currentPaperEntry, { [tf.key]: newVal }); - // modify event handler auto-refreshes — no explicit _refreshCurrentMode needed - }); - } - - // ── Next Step ── - this._renderNextStepCard(view, entry, key); - - // ── Files Row ── - this._renderPaperFilesRow(view, entry); - - // ── Technical Details (collapsed) ── - this._renderPaperTechnicalDetails(view, entry); - } - - /* ── Paper Status Strip: compact pills ── */ - _renderPaperStatusStrip(container, entry) { - const strip = container.createEl('div', { cls: 'paperforge-status-strip' }); const items = [ { key: 'pdf', label: 'PDF', ok: entry.has_pdf === true }, { key: 'ocr', label: 'OCR', @@ -1431,7 +1391,7 @@ class PaperForgeStatusView extends ItemView { { key: 'deep', label: '精读', ok: entry.deep_reading_status === 'done' }, ]; for (const item of items) { - const pill = strip.createEl('span', { cls: 'paperforge-status-pill' }); + const pill = stripLeft.createEl('span', { cls: 'paperforge-status-pill' }); let statusCls = 'pending'; if (item.ok) statusCls = 'ok'; else if (item.fail) statusCls = 'fail'; @@ -1441,6 +1401,44 @@ class PaperForgeStatusView extends ItemView { pill.createEl('span', { cls: 'paperforge-status-pill-icon', text: icon }); pill.createEl('span', { text: ' ' + item.label }); } + + // File buttons in status strip row + if (entry.pdf_path) { + const pdfBtn = stripRight.createEl('button', { cls: 'paperforge-contextual-btn' }); + pdfBtn.createEl('span', { cls: 'paperforge-contextual-btn-icon', text: '\uD83D\uDCC4' }); + pdfBtn.createEl('span', { text: '打开 PDF' }); + pdfBtn.addEventListener('click', () => { + const pathMatch = entry.pdf_path.match(/\[\[([^\]]+)\]\]/); + const targetPath = pathMatch ? pathMatch[1] : entry.pdf_path; + const file = this.app.vault.getAbstractFileByPath(targetPath); + if (file) { this.app.workspace.openLinkText(targetPath, ''); } + else { new Notice('[!!] PDF not found: ' + targetPath, 6000); } + }); + } + if (entry.fulltext_path) { + const ftBtn = stripRight.createEl('button', { cls: 'paperforge-contextual-btn' }); + ftBtn.createEl('span', { cls: 'paperforge-contextual-btn-icon', text: '\uD83D\uDCDD' }); + ftBtn.createEl('span', { text: '打开全文' }); + ftBtn.addEventListener('click', () => this._openFulltext(entry.fulltext_path)); + } + + // ── Paper Overview ── + this._renderPaperOverviewCard(view, entry); + + // ── Complete state or Next Step ── + if (entry.next_step === 'ready' && entry.deep_reading_status === 'done') { + const complete = view.createEl('div', { cls: 'paperforge-complete-row' }); + complete.createEl('span', { text: '\u2713' }); + complete.createEl('span', { text: '已完成,可直接使用' }); + } else { + this._renderNextStepCard(view, entry, key); + } + + // ── Recent Discussion ── + this._renderRecentDiscussionCard(view, entry); + + // ── Technical Details (disclosure) ── + this._renderPaperTechnicalDetails(view, entry); } /* ── Paper Overview Card: read from formal note body ── */ @@ -1587,48 +1585,51 @@ class PaperForgeStatusView extends ItemView { } } - /* ── Paper Files Row ── */ - _renderPaperFilesRow(container, entry) { - const row = container.createEl('div', { cls: 'paperforge-paper-files' }); - // Open PDF - if (entry.pdf_path) { - const pdfBtn = row.createEl('button', { cls: 'paperforge-contextual-btn' }); - pdfBtn.createEl('span', { cls: 'paperforge-contextual-btn-icon', text: '\uD83D\uDCC4' }); - pdfBtn.createEl('span', { text: 'Open PDF' }); - pdfBtn.addEventListener('click', () => { - // pdf_path is a wikilink like [[System/Zotero/storage/KEY/file.pdf]] - const pathMatch = entry.pdf_path.match(/\[\[([^\]]+)\]\]/); - const targetPath = pathMatch ? pathMatch[1] : entry.pdf_path; - const file = this.app.vault.getAbstractFileByPath(targetPath); - if (file) { - this.app.workspace.openLinkText(targetPath, ''); - } else { - new Notice('[!!] PDF not found: ' + targetPath, 6000); - } - }); - } - // Open Fulltext - if (entry.fulltext_path) { - const ftBtn = row.createEl('button', { cls: 'paperforge-contextual-btn' }); - ftBtn.createEl('span', { cls: 'paperforge-contextual-btn-icon', text: '\uD83D\uDCDD' }); - ftBtn.createEl('span', { text: 'Open Fulltext' }); - ftBtn.addEventListener('click', () => this._openFulltext(entry.fulltext_path)); - } - } - - /* ── Paper Technical Details (collapsed by default) ── */ + /* ── Paper Technical Details (disclosure with workflow toggles) ── */ _renderPaperTechnicalDetails(container, entry) { + const key = this._currentPaperKey; const section = container.createEl('div', { cls: 'paperforge-technical-details' }); - const toggle = section.createEl('button', { cls: 'paperforge-technical-details-toggle', text: '技术详情 ▸' }); - const body = section.createEl('div', { cls: 'paperforge-technical-details-body collapsed' }); + const toggle = section.createEl('button', { cls: 'paperforge-technical-details-toggle' }); + const body = section.createEl('div', { cls: 'paperforge-technical-details-body' }); body.style.display = 'none'; + // Restore expanded state from previous render (prevents flash on refresh) + if (this._techDetailsExpanded) { + body.style.display = 'block'; + toggle.setText('技术详情 ▾'); + } else { + toggle.setText('技术详情 ▸'); + } + toggle.addEventListener('click', () => { const visible = body.style.display !== 'none'; body.style.display = visible ? 'none' : 'block'; toggle.setText(visible ? '技术详情 ▸' : '技术详情 ▾'); + this._techDetailsExpanded = !visible; }); + // Workflow toggles inside disclosure + const togglesRow = body.createEl('div', { cls: 'paperforge-workflow-toggles' }); + const toggleFields = [ + { key: 'do_ocr', label: 'OCR', hint: '加入 OCR' }, + { key: 'analyze', label: '精读', hint: '标记精读' }, + ]; + for (const tf of toggleFields) { + const label = togglesRow.createEl('label', { cls: 'paperforge-workflow-toggle' }); + const cb = label.createEl('input', { type: 'checkbox', cls: 'paperforge-workflow-checkbox' }); + cb.checked = entry[tf.key] === true; + label.createEl('span', { cls: 'paperforge-workflow-toggle-label', text: tf.label }); + label.createEl('span', { cls: 'paperforge-workflow-toggle-hint', text: tf.hint }); + cb.addEventListener('change', async () => { + const noteFile = entry.note_path ? this.app.vault.getAbstractFileByPath(entry.note_path) : null; + if (!noteFile) { new Notice('[!!] Note file not found', 6000); return; } + const newVal = cb.checked; + await this.app.fileManager.processFrontMatter(noteFile, (fm) => { fm[tf.key] = newVal; }); + this._patchCachedEntry(key, { [tf.key]: newVal }); + this._currentPaperEntry = patchEntryWorkflowState(this._currentPaperEntry, { [tf.key]: newVal }); + }); + } + const health = entry.health || {}; const rows = [ ['PDF Health', health.pdf_health || '\u2014'], @@ -1637,10 +1638,10 @@ class PaperForgeStatusView extends ItemView { ['Note Path', entry.note_path || '\u2014'], ['Fulltext Path', entry.fulltext_path || '\u2014'], ]; - for (const [label, value] of rows) { + for (const [l, v] of rows) { const row = body.createEl('div', { cls: 'paperforge-technical-row' }); - row.createEl('span', { cls: 'paperforge-technical-label', text: label }); - row.createEl('span', { cls: 'paperforge-technical-value', text: String(value) }); + row.createEl('span', { cls: 'paperforge-technical-label', text: l }); + row.createEl('span', { cls: 'paperforge-technical-value', text: String(v) }); } } From 68e548e637354c8ea2405b9bae88557e2a59fa04 Mon Sep 17 00:00:00 2001 From: Research Assistant Date: Sun, 10 May 2026 16:10:26 +0800 Subject: [PATCH 20/37] fix(dashboard): reset expanded state on mode switch - Clear _techDetailsExpanded when switching modes to prevent stale state - Ensures technical details starts collapsed for each new paper --- paperforge/plugin/main.js | 1 + 1 file changed, 1 insertion(+) diff --git a/paperforge/plugin/main.js b/paperforge/plugin/main.js index a9571404..bbb69b5f 100644 --- a/paperforge/plugin/main.js +++ b/paperforge/plugin/main.js @@ -1177,6 +1177,7 @@ class PaperForgeStatusView extends ItemView { this._currentMode = mode; this._currentFilePath = filePath; + this._techDetailsExpanded = false; // reset disclosure state on mode switch // Clear existing content (D-06) this._contentEl.empty(); From 71d44c896660339a0cf16c7538be199f05743bfb Mon Sep 17 00:00:00 2001 From: Research Assistant Date: Sun, 10 May 2026 16:41:38 +0800 Subject: [PATCH 21/37] fix(dashboard): use Obsidian adapter API for discussion.json read (handles unicode paths reliably) --- paperforge/plugin/main.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/paperforge/plugin/main.js b/paperforge/plugin/main.js index bbb69b5f..10f39d4e 100644 --- a/paperforge/plugin/main.js +++ b/paperforge/plugin/main.js @@ -1524,19 +1524,19 @@ class PaperForgeStatusView extends ItemView { /* ── Recent Discussion Card: read ai/discussion.json ── */ _renderRecentDiscussionCard(container, entry) { const card = container.createEl('div', { cls: 'paperforge-discussion-card' }); - card.style.display = 'none'; // hidden by default + card.style.display = 'none'; if (!entry.note_path) return; const lastSlash = entry.note_path.lastIndexOf('/'); const wsDir = lastSlash !== -1 ? entry.note_path.substring(0, lastSlash) : '.'; const discPath = wsDir + '/ai/discussion.json'; - const vp = this.app.vault.adapter.basePath; - const absPath = path.join(vp, discPath); - - try { - if (!fs.existsSync(absPath)) return; - const raw = fs.readFileSync(absPath, 'utf-8'); + // Use Obsidian adapter for path correctness (handles unicode reliably) + this.app.vault.adapter.exists(discPath).then((exists) => { + if (!exists) return; + return this.app.vault.adapter.read(discPath); + }).then((raw) => { + if (!raw) return; const data = JSON.parse(raw); if (!data.sessions || data.sessions.length === 0) return; @@ -1581,9 +1581,9 @@ class PaperForgeStatusView extends ItemView { new Notice('讨论文件尚未生成'); } }); - } catch (e) { + }).catch((e) => { console.error('PaperForge: discussion.json read error', discPath, e.message); - } + }); } /* ── Paper Technical Details (disclosure with workflow toggles) ── */ From b31d5e94d90d143b668dfc978bab00b5c8d0dfac Mon Sep 17 00:00:00 2001 From: Research Assistant Date: Sun, 10 May 2026 16:58:15 +0800 Subject: [PATCH 22/37] debug(dashboard): add console.log for discussion.json path troubleshooting --- paperforge/plugin/main.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/paperforge/plugin/main.js b/paperforge/plugin/main.js index 10f39d4e..40aaaf79 100644 --- a/paperforge/plugin/main.js +++ b/paperforge/plugin/main.js @@ -1530,9 +1530,11 @@ class PaperForgeStatusView extends ItemView { const lastSlash = entry.note_path.lastIndexOf('/'); const wsDir = lastSlash !== -1 ? entry.note_path.substring(0, lastSlash) : '.'; const discPath = wsDir + '/ai/discussion.json'; + console.log('PaperForge: looking for discussion at', discPath); // Use Obsidian adapter for path correctness (handles unicode reliably) this.app.vault.adapter.exists(discPath).then((exists) => { + console.log('PaperForge: discussion exists?', exists); if (!exists) return; return this.app.vault.adapter.read(discPath); }).then((raw) => { From f9fbbfb2f05efc33c8fa6b23bbd9f694d901c5a6 Mon Sep 17 00:00:00 2001 From: Research Assistant Date: Sun, 10 May 2026 17:12:22 +0800 Subject: [PATCH 23/37] fix(dashboard): fix _renderNextStepCard this.plugin is undefined - Replace this.plugin with this.app.plugins.plugins['paperforge'] - PaperForgeStatusView does not have a plugin property - Remove debug console.log lines --- paperforge/plugin/main.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/paperforge/plugin/main.js b/paperforge/plugin/main.js index 40aaaf79..ef4f11d6 100644 --- a/paperforge/plugin/main.js +++ b/paperforge/plugin/main.js @@ -1530,11 +1530,9 @@ class PaperForgeStatusView extends ItemView { const lastSlash = entry.note_path.lastIndexOf('/'); const wsDir = lastSlash !== -1 ? entry.note_path.substring(0, lastSlash) : '.'; const discPath = wsDir + '/ai/discussion.json'; - console.log('PaperForge: looking for discussion at', discPath); // Use Obsidian adapter for path correctness (handles unicode reliably) this.app.vault.adapter.exists(discPath).then((exists) => { - console.log('PaperForge: discussion exists?', exists); if (!exists) return; return this.app.vault.adapter.read(discPath); }).then((raw) => { @@ -1694,7 +1692,7 @@ class PaperForgeStatusView extends ItemView { }); // Show "Run in [agent_platform]" label below the button (DASH-02) - const platform = this.plugin.settings?.agent_platform || 'opencode'; + const platform = this.app.plugins.plugins['paperforge']?.settings?.agent_platform || 'opencode'; const labelEl = card.createEl('div', { cls: 'paperforge-agent-platform-label' }); labelEl.setText(t('run_in_agent').replace('{0}', platform)); } else if (nextStep === 'ready') { From 09a520fb7ae5c8cc2985726a7a2ff5f87c745e0b Mon Sep 17 00:00:00 2001 From: Research Assistant Date: Sun, 10 May 2026 19:14:31 +0800 Subject: [PATCH 24/37] style: strengthen collection and global dashboard hierarchy --- paperforge/plugin/styles.css | 1408 ++++++++++------------------------ 1 file changed, 418 insertions(+), 990 deletions(-) diff --git a/paperforge/plugin/styles.css b/paperforge/plugin/styles.css index 842b8a35..dae7f145 100644 --- a/paperforge/plugin/styles.css +++ b/paperforge/plugin/styles.css @@ -8,9 +8,10 @@ font-family: inherit; font-size: 14px; color: var(--text-normal); - padding: 14px; + padding: 14px 14px 56px 14px; height: 100%; overflow-y: auto; + scrollbar-gutter: stable; display: flex; flex-direction: column; gap: 12px; @@ -20,6 +21,10 @@ --pf-border-hover: var(--background-modifier-border-hover); --pf-accent: var(--interactive-accent); --pf-radius: var(--radius-m); + --pf-paper-accent: #6f8780; + --pf-collection-accent: #9a6b52; + --pf-global-accent: #5f7180; + --pf-warm-line: #a28a5d; } /* ── Typography tokens ── */ @@ -380,8 +385,8 @@ } .paperforge-ocr-count-value { - font-size: var(--font-ui-medium); - font-weight: var(--font-bold); + font-size: 18px; + font-weight: 600; color: var(--text-normal); line-height: 1.2; } @@ -1203,6 +1208,7 @@ font-weight: var(--font-semibold); padding: 2px 8px; border-radius: 10px; + border: 1px solid transparent; text-transform: uppercase; letter-spacing: 0.5px; line-height: 1.4; @@ -1210,18 +1216,21 @@ } .paperforge-mode-badge.global { - background: var(--interactive-accent); - color: var(--text-on-accent); + background: color-mix(in srgb, var(--pf-global-accent) 12%, var(--background-secondary)); + border-color: color-mix(in srgb, var(--pf-global-accent) 28%, var(--pf-border)); + color: var(--pf-global-accent); } .paperforge-mode-badge.paper { - background: var(--color-cyan); - color: var(--text-on-accent); + background: color-mix(in srgb, var(--pf-paper-accent) 12%, var(--background-secondary)); + border-color: color-mix(in srgb, var(--pf-paper-accent) 28%, var(--pf-border)); + color: var(--pf-paper-accent); } .paperforge-mode-badge.collection { - background: var(--color-purple); - color: var(--text-on-accent); + background: color-mix(in srgb, var(--pf-collection-accent) 12%, var(--background-secondary)); + border-color: color-mix(in srgb, var(--pf-collection-accent) 28%, var(--pf-border)); + color: var(--pf-collection-accent); } .paperforge-mode-name { @@ -1294,36 +1303,6 @@ flex-wrap: wrap; } -.paperforge-contextual-btn { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 6px 14px; - font-size: var(--font-ui-small); - font-weight: var(--font-semibold); - color: var(--text-normal); - background: var(--background-secondary-alt); - border: 1px solid var(--background-modifier-border); - border-radius: var(--radius-m); - cursor: pointer; - transition: background 0.15s, color 0.15s; - user-select: none; -} - -.paperforge-contextual-btn:hover { - background: var(--background-modifier-hover); - color: var(--text-accent); -} - -.paperforge-contextual-btn:active { - transform: scale(0.97); -} - -.paperforge-contextual-btn-icon { - font-size: 14px; - line-height: 1; -} - /* ========================================================================== SECTION 16 — Next-Step Recommendation Card ========================================================================== */ @@ -1393,6 +1372,8 @@ display: flex; flex-direction: column; gap: 24px; + max-width: 800px; + margin: 0 auto; } .paperforge-collection-metrics { @@ -1687,65 +1668,170 @@ } /* ========================================================================== - SECTION 39 — Shared Components (Native Light Surface Design) + SECTION 39 — Dashboard Visual System ========================================================================== */ -/* ── Section Label ── */ +/* Shared tokens */ .paperforge-section-label { font-size: 12px; font-weight: 600; color: var(--text-muted); + margin: 0 0 10px 0; + padding-left: 10px; + border-left: 3px solid var(--pf-warm-line); text-transform: uppercase; - letter-spacing: 0.04em; - margin-bottom: 6px; + letter-spacing: 0.06em; + line-height: 1.3; +} + +.paperforge-contextual-btn { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 5px 12px; + background: var(--background-secondary); + border: 1px solid var(--pf-border); + border-radius: var(--radius-s); + color: var(--text-muted); + font-size: 12px; + font-weight: 500; + cursor: pointer; + transition: background 0.15s, border-color 0.15s, color 0.15s; + white-space: nowrap; + user-select: none; +} + +.paperforge-contextual-btn:hover { + background: var(--background-modifier-hover); + border-color: color-mix(in srgb, var(--pf-warm-line) 52%, var(--pf-border-hover)); + color: var(--text-normal); +} + +.paperforge-contextual-btn.running { + opacity: 0.6; + pointer-events: none; +} + +.paperforge-contextual-btn:active { + transform: scale(0.97); +} + +.paperforge-contextual-btn:focus-visible { + outline: 2px solid color-mix(in srgb, var(--interactive-accent) 62%, white 38%); + outline-offset: 2px; +} + +.paperforge-contextual-btn-icon { + font-size: 14px; + line-height: 1; } -/* ── Status Pill ── */ .paperforge-status-pill { display: inline-flex; align-items: center; gap: 4px; - padding: 2px 8px; + padding: 2px 10px; + border: 1px solid var(--pf-border); border-radius: 999px; - background: var(--background-primary-alt); - color: var(--text-muted); font-size: 12px; font-weight: 500; - line-height: 1.4; -} - -.paperforge-status-pill-icon { font-size: 11px; line-height: 1; } - -.paperforge-status-pill.ok { color: var(--text-success); } -.paperforge-status-pill.fail { color: var(--text-error); } -.paperforge-status-pill.pending { color: var(--text-muted); } - -/* ── Cards (only primary content modules) ── */ -.paperforge-paper-overview, -.paperforge-discussion-card { - margin: 8px 0; - padding: 12px; - background: var(--background-secondary); - border: 1px solid var(--background-modifier-border); - border-radius: var(--radius-m); -} - -/* ── Paper Overview ── */ -.paperforge-paper-overview-header { margin-bottom: 6px; } - -.paperforge-paper-overview-title { - font-size: 12px; - font-weight: 600; + line-height: 1.5; + background: var(--background-primary-alt); color: var(--text-muted); +} + +.paperforge-status-pill.pending { + background: color-mix(in srgb, var(--pf-paper-accent) 9%, var(--background-primary)); + border-color: color-mix(in srgb, var(--pf-paper-accent) 20%, var(--pf-border)); + color: var(--pf-paper-accent); +} + +.paperforge-status-pill.ok { + background: rgba(var(--background-modifier-success-rgb, 0, 200, 100), 0.12); + border-color: rgba(var(--background-modifier-success-rgb, 0, 200, 100), 0.2); + color: var(--text-success, var(--color-green)); +} + +.paperforge-status-pill.fail { + background: rgba(var(--background-modifier-error-rgb, 255, 80, 80), 0.12); + border-color: rgba(var(--background-modifier-error-rgb, 255, 80, 80), 0.22); + color: var(--text-error); +} + +.paperforge-status-pill-icon { + font-size: 11px; + line-height: 1; +} + +.paperforge-library-snapshot, +.paperforge-system-status, +.paperforge-global-actions { + padding: 20px; + background: color-mix(in srgb, var(--background-primary) 92%, var(--background-secondary) 8%); + border: 1px solid color-mix(in srgb, var(--pf-warm-line) 20%, var(--pf-border)); + border-radius: calc(var(--pf-radius) + 2px); + box-shadow: 0 1px 2px rgba(20, 18, 14, 0.04), 0 10px 24px rgba(20, 18, 14, 0.04); + transition: border-color 0.2s, box-shadow 0.2s, transform 0.2s; +} + +.paperforge-library-snapshot:hover, +.paperforge-system-status:hover, +.paperforge-global-actions:hover { + border-color: color-mix(in srgb, var(--pf-warm-line) 42%, var(--pf-border)); + box-shadow: 0 2px 10px rgba(20, 18, 14, 0.08), 0 14px 28px rgba(20, 18, 14, 0.06); +} + +/* Paper mode */ +.paperforge-status-strip { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 10px; + padding: 10px 0 8px; + margin: 4px 0 6px; +} + +.paperforge-status-strip-left, +.paperforge-status-strip-right { + display: flex; + align-items: center; + gap: 6px; + flex-wrap: wrap; +} + +.paperforge-paper-overview { + margin: 10px 0; + padding: 18px; + background: var(--background-primary); + border: 1px solid color-mix(in srgb, var(--pf-warm-line) 18%, var(--pf-border)); + border-radius: calc(var(--pf-radius) + 2px); + box-shadow: inset 3px 0 0 color-mix(in srgb, var(--pf-paper-accent) 16%, transparent); + transition: border-color 0.2s, box-shadow 0.2s; +} + +.paperforge-paper-overview:hover { + border-color: color-mix(in srgb, var(--pf-warm-line) 34%, var(--pf-border)); + box-shadow: inset 3px 0 0 color-mix(in srgb, var(--pf-paper-accent) 22%, transparent), 0 2px 8px rgba(0, 0, 0, 0.06); +} + +.paperforge-paper-overview-header { + margin-bottom: 8px; +} + +.paperforge-paper-overview-title, +.paperforge-discussion-title { + font-size: 12px; + font-weight: 700; + color: var(--pf-paper-accent); text-transform: uppercase; - letter-spacing: 0.04em; + letter-spacing: 0.06em; } .paperforge-paper-overview-body { font-size: 14px; - font-weight: 400; - color: var(--text-muted); - line-height: 1.55; + color: var(--text-normal); + line-height: 1.72; } .paperforge-paper-overview-excerpt { @@ -1753,38 +1839,58 @@ word-break: break-word; } -.paperforge-paper-overview-expand { - display: inline-block; - margin-top: 4px; - padding: 2px 0; - background: none; - border: none; - color: var(--text-accent); - font-size: 13px; - font-weight: 400; - cursor: pointer; - transition: color 0.15s; +.paperforge-paper-overview-expand, +.paperforge-discussion-expand, +.paperforge-discussion-viewall { + color: var(--pf-paper-accent); } -.paperforge-paper-overview-expand:hover { +.paperforge-paper-overview-expand { + display: inline-block; + margin-top: 6px; + padding: 3px 0; + background: none; + border: none; + border-bottom: 1px dashed currentColor; + font-size: 12px; + font-weight: 500; + cursor: pointer; + transition: color 0.15s, border-color 0.15s; +} + +.paperforge-paper-overview-expand:hover, +.paperforge-discussion-expand:hover, +.paperforge-discussion-viewall:hover { color: var(--interactive-accent-hover); } -/* ── Discussion Card ── */ -.paperforge-discussion-header { margin-bottom: 8px; } +.paperforge-paper-overview-expand:hover { + border-color: currentColor; +} -.paperforge-discussion-title { - font-size: 12px; - font-weight: 600; - color: var(--text-muted); - text-transform: uppercase; - letter-spacing: 0.04em; +.paperforge-discussion-header { + margin-bottom: 10px; +} + +.paperforge-discussion-card { + margin: 10px 0; + padding: 18px; + background: var(--background-primary); + border: 1px solid color-mix(in srgb, var(--pf-warm-line) 18%, var(--pf-border)); + border-radius: calc(var(--pf-radius) + 2px); + box-shadow: inset 3px 0 0 color-mix(in srgb, var(--pf-paper-accent) 11%, transparent); + transition: border-color 0.2s, box-shadow 0.2s; +} + +.paperforge-discussion-card:hover { + border-color: color-mix(in srgb, var(--pf-warm-line) 34%, var(--pf-border)); + box-shadow: inset 3px 0 0 color-mix(in srgb, var(--pf-paper-accent) 16%, transparent), 0 2px 8px rgba(0, 0, 0, 0.06); } .paperforge-discussion-item { - margin-bottom: 8px; - padding-bottom: 8px; - border-bottom: 1px solid var(--background-modifier-border); + margin-bottom: 10px; + padding-bottom: 10px; + border-bottom: 1px solid var(--pf-border); } .paperforge-discussion-item:last-child { @@ -1803,9 +1909,8 @@ .paperforge-discussion-a { font-size: 14px; - font-weight: 400; - color: var(--text-muted); - line-height: 1.55; + color: var(--text-normal); + line-height: 1.68; } .paperforge-discussion-expand { @@ -1814,71 +1919,93 @@ margin-left: 4px; background: none; border: none; - color: var(--text-accent); - font-size: 13px; - font-weight: 400; + border-bottom: 1px dashed currentColor; + font-size: 12px; + font-weight: 500; cursor: pointer; - transition: color 0.15s; + transition: color 0.15s, border-color 0.15s; } -.paperforge-discussion-expand:hover { color: var(--interactive-accent-hover); } - .paperforge-discussion-viewall { display: block; text-align: right; - margin-top: 4px; - font-size: 13px; - font-weight: 400; - color: var(--text-accent); + margin-top: 6px; + font-size: 12px; + font-weight: 500; cursor: pointer; text-decoration: none; transition: color 0.15s; } -.paperforge-discussion-viewall:hover { color: var(--interactive-accent-hover); } - -/* ── Complete state row ── */ -.paperforge-complete-row { - display: flex; - align-items: center; - gap: 6px; - color: var(--text-success); - font-size: 13px; - font-weight: 400; - line-height: 1.4; - padding: 4px 0; +.paperforge-discussion-viewall:hover { + text-decoration: underline; +} + +.paperforge-paper-files { + display: flex; + gap: 8px; + margin: 10px 0; +} + +.paperforge-status-strip-right .paperforge-contextual-btn { + min-height: 34px; + padding: 7px 12px; + background: color-mix(in srgb, var(--background-secondary) 78%, var(--background-primary)); + border-color: color-mix(in srgb, var(--pf-border) 88%, var(--pf-warm-line)); + color: var(--text-muted); +} + +.paperforge-status-strip-right .paperforge-contextual-btn:hover { + background: color-mix(in srgb, var(--background-modifier-hover) 78%, var(--background-primary)); + border-color: color-mix(in srgb, var(--pf-border-hover) 82%, var(--pf-warm-line)); + color: var(--text-normal); } -/* ── Technical Details disclosure ── */ .paperforge-technical-details { - margin: 4px 0; + margin: 8px 0 4px; } .paperforge-technical-details-toggle { - display: inline-block; - padding: 4px 0; + width: 100%; + min-height: 34px; + padding: 8px 0; background: none; border: none; + border-radius: 0; color: var(--text-muted); - font-size: 13px; - font-weight: 400; + font-size: 12px; + font-weight: 600; cursor: pointer; - user-select: none; + text-align: left; + letter-spacing: 0.02em; transition: color 0.15s; } -.paperforge-technical-details-toggle:hover { color: var(--text-normal); } +.paperforge-technical-details-toggle:hover { + background: none; + border-color: transparent; + color: var(--text-normal); +} .paperforge-technical-details-body { - padding: 6px 0 2px 0; + margin-top: 0; + padding: 8px 0 0; + background: none; + border: none; + border-radius: 0; font-size: 12px; - color: var(--text-faint); } .paperforge-technical-row { display: flex; justify-content: space-between; - padding: 2px 0; + gap: 12px; + padding: 6px 0; + border-bottom: 1px solid color-mix(in srgb, var(--pf-border) 72%, transparent); +} + +.paperforge-technical-row:last-child { + border-bottom: none; } .paperforge-technical-label { @@ -1896,87 +2023,27 @@ text-align: right; } -/* ── Contextual buttons ── */ -.paperforge-contextual-btn { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 3px 8px; - background: var(--interactive-normal); - border: 1px solid var(--background-modifier-border); - color: var(--text-normal); - border-radius: var(--button-radius, 4px); - font-size: 12px; - font-weight: 400; - cursor: pointer; - transition: background 0.15s; - white-space: nowrap; -} - -.paperforge-contextual-btn:hover { - background: var(--interactive-hover); - border-color: var(--background-modifier-border-hover); -} - -.paperforge-contextual-btn.running { - opacity: 0.6; - pointer-events: none; -} - -.paperforge-contextual-btn-icon { - font-size: 14px; - line-height: 1; -} - -.paperforge-contextual-btn.primary { - background: var(--interactive-accent); - color: var(--text-on-accent); - border-color: var(--interactive-accent); - font-weight: 500; -} - -.paperforge-contextual-btn.primary:hover { - opacity: 0.85; -} - -/* ========================================================================== - SECTION 40 — Per-Paper View - ========================================================================== */ - -/* ── Status Strip + Files Row (merged) ── */ -.paperforge-status-strip { - display: flex; - align-items: center; - justify-content: space-between; - flex-wrap: wrap; - gap: 8px; - padding: 6px 0; - margin: 4px 0; -} - -.paperforge-status-strip-left { - display: flex; - align-items: center; - gap: 6px; -} - -.paperforge-status-strip-right { - display: flex; - align-items: center; - gap: 6px; -} - -/* ── Workflow Toggles (inline, not a card) ── */ .paperforge-workflow-toggles { display: flex; + flex-wrap: wrap; gap: 14px; - padding: 6px 0; + margin: 0 0 10px; + padding: 0 0 10px; + background: none; + border: none; + border-bottom: 1px solid color-mix(in srgb, var(--pf-border) 72%, transparent); + border-radius: 0; } .paperforge-workflow-toggle { display: flex; align-items: center; gap: 6px; + min-height: 34px; + padding: 6px 10px; + background: color-mix(in srgb, var(--background-primary-alt) 88%, var(--background-primary)); + border: 1px solid color-mix(in srgb, var(--pf-border) 82%, var(--pf-paper-accent)); + border-radius: 999px; cursor: pointer; font-size: 13px; color: var(--text-muted); @@ -1992,28 +2059,25 @@ } .paperforge-workflow-toggle-label { - font-weight: 500; + font-weight: 600; color: var(--text-normal); } .paperforge-workflow-toggle-hint { color: var(--text-faint); - font-size: 12px; + font-size: 11px; } -/* ========================================================================== - SECTION 41 — Collection / Base View - ========================================================================== */ - +/* Collection mode */ .paperforge-collection-header { - margin-bottom: 8px; + margin-bottom: 10px; } .paperforge-collection-title { font-size: 16px; font-weight: 600; color: var(--text-normal); - line-height: 1.35; + line-height: 1.3; } .paperforge-collection-count { @@ -2022,628 +2086,67 @@ margin-top: 2px; } +.paperforge-global-view { + display: flex; + flex-direction: column; + gap: 18px; + max-width: 800px; + margin: 0 auto; +} + +.paperforge-global-view .paperforge-section-label, +.paperforge-workflow-overview .paperforge-section-label { + margin-bottom: 12px; +} + .paperforge-workflow-overview { - margin: 8px 0; - padding: 12px; - background: var(--background-secondary); - border: 1px solid var(--background-modifier-border); - border-radius: var(--radius-m); + margin: 10px 0; + padding: 18px; + background: color-mix(in srgb, var(--background-primary) 94%, var(--background-primary-alt) 6%); + border: 1px solid color-mix(in srgb, var(--pf-warm-line) 20%, var(--pf-border)); + border-radius: calc(var(--pf-radius) + 2px); + box-shadow: inset 3px 0 0 color-mix(in srgb, var(--pf-collection-accent) 12%, transparent); + transition: border-color 0.2s, box-shadow 0.2s; +} + +.paperforge-workflow-overview:hover { + border-color: color-mix(in srgb, var(--pf-warm-line) 42%, var(--pf-border)); + box-shadow: inset 3px 0 0 color-mix(in srgb, var(--pf-collection-accent) 17%, transparent), 0 2px 10px rgba(20, 18, 14, 0.08); } .paperforge-workflow-funnel { display: flex; align-items: center; - gap: 3px; - flex-wrap: wrap; - justify-content: center; -} - -.paperforge-workflow-stage { - display: flex; - flex-direction: column; - align-items: center; - padding: 6px 12px; - background: var(--background-primary-alt); - border-radius: 6px; - min-width: 52px; -} - -.paperforge-workflow-stage-value { - font-size: 18px; - font-weight: 600; - color: var(--text-normal); - line-height: 1.2; -} - -.paperforge-workflow-stage-label { - font-size: 11px; - color: var(--text-muted); - text-transform: uppercase; - letter-spacing: 0.04em; - margin-top: 2px; - text-align: center; -} - -.paperforge-workflow-arrow { - color: var(--text-faint); - font-size: 12px; - flex-shrink: 0; -} - -.paperforge-collection-ocr-header { - display: flex; - align-items: center; - gap: 10px; - margin-bottom: 14px; -} - -.paperforge-issue-summary { - margin: 8px 0; - padding: 10px 12px; - background: var(--background-secondary); - border: 1px solid var(--background-modifier-border); - border-left: 3px solid var(--color-orange, #e67e22); - border-radius: var(--radius-m); -} - -.paperforge-issue-summary .paperforge-section-label { - color: var(--color-orange, #e67e22); -} - -.paperforge-issue-list { - margin: 4px 0 6px 0; -} - -.paperforge-issue-item { - font-size: 14px; - color: var(--text-muted); - padding: 2px 0; -} - -.paperforge-issue-actions { - display: flex; - gap: 8px; - margin-top: 8px; -} - -.paperforge-collection-actions { - display: flex; - gap: 8px; - margin-top: 8px; - padding-top: 8px; - border-top: 1px solid var(--background-modifier-border); -} - -/* ========================================================================== - SECTION 42 — Global / Home View - ========================================================================== */ - -.paperforge-library-snapshot { - padding: 12px; - background: var(--background-secondary); - border: 1px solid var(--background-modifier-border); - border-radius: var(--radius-m); -} - -.paperforge-snapshot-pills { - display: flex; - gap: 8px; - flex-wrap: wrap; -} - -.paperforge-snapshot-pill { - display: flex; - flex-direction: column; - align-items: center; - padding: 8px 14px; - background: var(--background-primary-alt); - border-radius: 6px; - min-width: 56px; - flex: 1; -} - -.paperforge-snapshot-value { - font-size: 18px; - font-weight: 600; - color: var(--text-normal); - line-height: 1.2; -} - -.paperforge-snapshot-label { - font-size: 11px; - color: var(--text-muted); - text-transform: uppercase; - letter-spacing: 0.04em; - margin-top: 2px; - text-align: center; -} - -.paperforge-system-status { - padding: 12px; - background: var(--background-secondary); - border: 1px solid var(--background-modifier-border); - border-radius: var(--radius-m); -} - -.paperforge-status-grid { - display: flex; - flex-direction: column; - gap: 1px; -} - -.paperforge-status-row { - display: flex; - align-items: center; - gap: 8px; - padding: 3px 0; -} - -.paperforge-status-dot { - width: 7px; - height: 7px; - border-radius: 50%; - flex-shrink: 0; - background: var(--background-modifier-border); - transition: background 0.3s; -} - -.paperforge-status-dot.ok { background: var(--text-success); } -.paperforge-status-dot.fail { background: var(--text-error); } - -.paperforge-status-label { - font-size: 14px; - color: var(--text-muted); - min-width: 85px; -} - -.paperforge-status-detail { - font-size: 13px; - color: var(--text-faint); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.paperforge-global-actions { - padding: 12px; - background: var(--background-secondary); - border: 1px solid var(--background-modifier-border); - border-radius: var(--radius-m); -} - -.paperforge-global-actions-row { - display: flex; - gap: 8px; -} - -/* ========================================================================== - SECTION 43 — Dark Theme - ========================================================================== */ - -.theme-dark .paperforge-status-pill { - background: var(--background-primary-alt); -} - -.theme-dark .paperforge-status-pill.ok { - color: var(--text-success); -} - -.theme-dark .paperforge-status-pill.fail { - color: var(--text-error); -} - -/* ========================================================================== - SECTION 39 — Dashboard Redesign: Shared Components - ========================================================================== */ - -/* ── Section Label ── */ -.paperforge-section-label { - font-size: 11px; - font-weight: 600; - color: var(--text-muted); - margin: 0 0 10px 0; - padding-left: 10px; - border-left: 3px solid var(--interactive-accent); - text-transform: uppercase; - letter-spacing: 0.06em; - line-height: 1.3; -} - -/* ── Contextual Button ── */ -.paperforge-contextual-btn { - display: inline-flex; - align-items: center; - gap: 5px; - padding: 5px 12px; - background: var(--background-secondary); - border: 1px solid var(--background-modifier-border); - border-radius: var(--radius-s); - color: var(--text-muted); - font-size: 12px; - font-weight: 500; - cursor: pointer; - transition: background 0.15s, border-color 0.15s, color 0.15s; - white-space: nowrap; -} - -.paperforge-contextual-btn:hover { - background: var(--background-modifier-hover); - border-color: var(--interactive-accent); - color: var(--text-normal); -} - -.paperforge-contextual-btn.running { - opacity: 0.6; - pointer-events: none; -} - -.paperforge-contextual-btn-icon { - font-size: 14px; - line-height: 1; -} - -/* ========================================================================== - SECTION 40 — Per-Paper View (Reading Companion) - ========================================================================== */ - -/* ── Status Strip ── */ -.paperforge-status-strip { - display: flex; gap: 6px; - padding: 8px 0; - margin: 6px 0; -} - -.paperforge-status-pill { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 2px 10px; - border-radius: 4px; - font-size: 12px; - font-weight: 500; - line-height: 1.5; - background: var(--background-primary-alt); - color: var(--text-muted); -} - -.paperforge-status-pill.ok { - background: rgba(var(--background-modifier-success-rgb, 0, 200, 100), 0.12); - color: var(--text-success, var(--color-green)); -} - -.paperforge-status-pill.fail { - background: rgba(var(--background-modifier-error-rgb, 255, 80, 80), 0.12); - color: var(--text-error); -} - -.paperforge-status-pill.pending { - background: var(--background-primary-alt); - color: var(--text-muted); -} - -.paperforge-status-pill-icon { - font-size: 11px; - line-height: 1; -} - -/* Theme dark: softer pill backgrounds */ -.theme-dark .paperforge-status-pill.ok { - background: rgba(100, 200, 120, 0.15); -} -.theme-dark .paperforge-status-pill.fail { - background: rgba(255, 100, 100, 0.15); -} - -/* ── Paper Overview Card ── */ -.paperforge-paper-overview { - margin: 10px 0; - padding: 16px; - background: var(--background-primary); - border: 1px solid var(--background-modifier-border); - border-radius: var(--radius-m); - transition: border-color 0.2s, box-shadow 0.2s; - position: relative; -} - -.paperforge-paper-overview::before { - content: ''; - position: absolute; - inset: -1px; - border-radius: inherit; - opacity: 0; - box-shadow: 0 2px 8px rgba(0,0,0,0.08); - transition: opacity 0.2s; - pointer-events: none; -} - -.paperforge-paper-overview:hover::before { - opacity: 1; -} - -.paperforge-paper-overview-header { - margin-bottom: 8px; -} - -.paperforge-paper-overview-title { - font-size: 11px; - font-weight: 700; - color: var(--text-accent); - text-transform: uppercase; - letter-spacing: 0.06em; -} - -.paperforge-paper-overview-body { - font-size: 13.5px; - color: var(--text-muted); - line-height: 1.65; -} - -.paperforge-paper-overview-excerpt { - white-space: pre-wrap; - word-break: break-word; -} - -.paperforge-paper-overview-expand { - display: inline-block; - margin-top: 6px; - padding: 3px 0; - background: none; - border: none; - border-bottom: 1px dashed var(--text-accent); - color: var(--text-accent); - font-size: 12px; - font-weight: 500; - cursor: pointer; - transition: color 0.15s, border-color 0.15s; -} - -.paperforge-paper-overview-expand:hover { - color: var(--interactive-accent-hover); - border-color: var(--interactive-accent-hover); -} - -/* ── Discussion Card ── */ -.paperforge-discussion-card { - margin: 10px 0; - padding: 16px; - background: var(--background-primary); - border: 1px solid var(--background-modifier-border); - border-radius: var(--radius-m); - transition: border-color 0.2s, box-shadow 0.2s; - position: relative; -} - -.paperforge-discussion-card::before { - content: ''; - position: absolute; - inset: -1px; - border-radius: inherit; - opacity: 0; - box-shadow: 0 2px 8px rgba(0,0,0,0.08); - transition: opacity 0.2s; - pointer-events: none; -} - -.paperforge-discussion-card:hover::before { - opacity: 1; -} - -.paperforge-discussion-header { - margin-bottom: 10px; -} - -.paperforge-discussion-title { - font-size: 11px; - font-weight: 700; - color: var(--text-accent); - text-transform: uppercase; - letter-spacing: 0.06em; -} - -.paperforge-discussion-item { - margin-bottom: 10px; - padding-bottom: 10px; - border-bottom: 1px solid var(--background-modifier-border); -} - -.paperforge-discussion-item:last-child { - border-bottom: none; - margin-bottom: 0; - padding-bottom: 0; -} - -.paperforge-discussion-q { - font-size: 13.5px; - font-weight: 600; - color: var(--text-normal); - margin-bottom: 4px; - line-height: 1.5; -} - -.paperforge-discussion-a { - font-size: 13.5px; - color: var(--text-muted); - line-height: 1.6; -} - -.paperforge-discussion-expand { - display: inline; - padding: 0; - margin-left: 4px; - background: none; - border: none; - border-bottom: 1px dashed var(--text-accent); - color: var(--text-accent); - font-size: 12px; - font-weight: 500; - cursor: pointer; - transition: color 0.15s; -} - -.paperforge-discussion-expand:hover { - color: var(--interactive-accent-hover); -} - -.paperforge-discussion-viewall { - display: block; - text-align: right; - margin-top: 6px; - font-size: 12px; - font-weight: 500; - color: var(--text-accent); - cursor: pointer; - text-decoration: none; - transition: color 0.15s; -} - -.paperforge-discussion-viewall:hover { - color: var(--interactive-accent-hover); - text-decoration: underline; -} - -/* ── Paper Files Row ── */ -.paperforge-paper-files { - display: flex; - gap: 8px; - margin: 10px 0; -} - -/* ── Technical Details ── */ -.paperforge-technical-details { - margin: 6px 0; -} - -.paperforge-technical-details-toggle { - width: 100%; - padding: 6px 12px; - background: var(--background-primary); - border: 1px solid var(--background-modifier-border); - border-radius: var(--radius-s); - color: var(--text-faint); - font-size: 11px; - font-weight: 500; - cursor: pointer; - text-align: left; - transition: background 0.15s, border-color 0.15s, color 0.15s; -} - -.paperforge-technical-details-toggle:hover { - background: var(--background-modifier-hover); - border-color: var(--background-modifier-border-hover); - color: var(--text-muted); -} - -.paperforge-technical-details-body { - margin-top: 4px; - padding: 8px 12px; - background: var(--background-primary-alt); - border: 1px solid var(--background-modifier-border); - border-radius: var(--radius-s); - font-size: 11px; -} - -.paperforge-technical-row { - display: flex; - justify-content: space-between; - padding: 2px 0; -} - -.paperforge-technical-label { - color: var(--text-faint); - flex-shrink: 0; - margin-right: 12px; -} - -.paperforge-technical-value { - color: var(--text-muted); - max-width: 60%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - text-align: right; -} - -/* ========================================================================== - SECTION 41 — Collection / Base View (Batch Workspace) - ========================================================================== */ - -/* ── Collection Header ── */ -.paperforge-collection-header { - margin-bottom: 10px; -} - -.paperforge-collection-title { - font-size: 16px; - font-weight: 600; - color: var(--text-normal); - line-height: 1.3; -} - -.paperforge-collection-count { - font-size: 12px; - color: var(--text-muted); - margin-top: 2px; -} - -/* ── Workflow Overview ── */ -.paperforge-workflow-overview { - margin: 10px 0; - padding: 16px; - background: var(--background-primary); - border: 1px solid var(--background-modifier-border); - border-radius: var(--radius-m); - transition: border-color 0.2s, box-shadow 0.2s; - position: relative; -} - -.paperforge-workflow-overview::before { - content: ''; - position: absolute; - inset: -1px; - border-radius: inherit; - opacity: 0; - box-shadow: 0 2px 8px rgba(0,0,0,0.08); - transition: opacity 0.2s; - pointer-events: none; -} - -.paperforge-workflow-overview:hover::before { - opacity: 1; -} - -.paperforge-workflow-funnel { - display: flex; - align-items: center; - gap: 3px; flex-wrap: wrap; - justify-content: center; + justify-content: flex-start; } .paperforge-workflow-stage { display: flex; flex-direction: column; align-items: center; - padding: 8px 12px; - background: var(--background-primary-alt); - border-radius: 6px; - min-width: 56px; + padding: 12px 14px; + background: color-mix(in srgb, var(--background-primary-alt) 88%, var(--background-primary) 12%); + border: 1px solid color-mix(in srgb, var(--pf-collection-accent) 22%, var(--pf-border)); + border-radius: 8px; + min-width: 72px; } .paperforge-workflow-stage-value { - font-size: 20px; + font-size: 26px; font-weight: 700; - color: var(--text-accent); + color: var(--pf-collection-accent); line-height: 1.2; - letter-spacing: -0.3px; + letter-spacing: -0.5px; } .paperforge-workflow-stage-label { - font-size: 10px; + font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.3px; - margin-top: 2px; + margin-top: 4px; text-align: center; } @@ -2653,7 +2156,6 @@ flex-shrink: 0; } -/* ── OCR Pipeline in Base ── */ .paperforge-collection-ocr-header { display: flex; align-items: center; @@ -2661,77 +2163,60 @@ margin-bottom: 14px; } -/* ── Issue Summary (serious blockers only) ── */ .paperforge-issue-summary { margin: 10px 0; padding: 12px 14px; background: var(--background-primary); - border: 1px solid var(--background-modifier-border); - border-left: 3px solid var(--color-orange); - border-radius: var(--radius-m); + border: 1px solid var(--pf-border); + border-left: 3px solid var(--pf-warm-line); + border-radius: var(--pf-radius); } .paperforge-issue-summary .paperforge-section-label { - color: var(--color-orange); + border-left-color: var(--pf-warm-line); + color: var(--text-normal); } .paperforge-issue-list { - margin: 4px 0 6px 0; + margin: 8px 0 12px 0; + display: flex; + flex-direction: column; + gap: 6px; } .paperforge-issue-item { font-size: 13px; - color: var(--text-muted); - padding: 2px 0; -} - -.paperforge-issue-actions { + font-weight: 500; + color: var(--text-normal); + padding: 6px 10px; + background: color-mix(in srgb, var(--background-secondary) 50%, transparent); + border-radius: calc(var(--pf-radius) - 2px); display: flex; - gap: 8px; - margin-top: 8px; + align-items: center; } -/* ── Collection Actions ── */ +.paperforge-issue-actions, .paperforge-collection-actions { display: flex; gap: 8px; - margin-top: 10px; - padding-top: 10px; - border-top: 1px solid var(--background-modifier-border); } -/* ========================================================================== - SECTION 42 — Global / Home View (System Homepage) - ========================================================================== */ - -/* ── Library Snapshot ── */ -.paperforge-library-snapshot { - padding: 16px; - background: var(--background-primary); - border: 1px solid var(--background-modifier-border); - border-radius: var(--radius-m); - transition: border-color 0.2s, box-shadow 0.2s; - position: relative; +.paperforge-issue-actions { + margin-top: 8px; } -.paperforge-library-snapshot::before { - content: ''; - position: absolute; - inset: -1px; - border-radius: inherit; - opacity: 0; - box-shadow: 0 2px 8px rgba(0,0,0,0.08); - transition: opacity 0.2s; - pointer-events: none; -} - -.paperforge-library-snapshot:hover::before { - opacity: 1; +.paperforge-collection-actions { + align-items: center; + flex-wrap: wrap; + margin-top: 8px; + padding-top: 8px; + border-top: 1px solid color-mix(in srgb, var(--pf-border) 82%, var(--pf-warm-line)); } +/* Global mode */ .paperforge-snapshot-pills { display: flex; - gap: 8px; + gap: 10px; flex-wrap: wrap; } @@ -2739,55 +2224,32 @@ display: flex; flex-direction: column; align-items: center; - padding: 10px 14px; - background: var(--background-primary-alt); - border-radius: 6px; - min-width: 60px; + justify-content: center; + padding: 14px 16px; + background: color-mix(in srgb, var(--background-primary-alt) 86%, var(--background-primary) 14%); + border: 1px solid color-mix(in srgb, var(--pf-global-accent) 22%, var(--pf-border)); + border-radius: 8px; + min-width: 72px; flex: 1; } .paperforge-snapshot-value { - font-size: 20px; + font-size: 26px; font-weight: 700; - color: var(--text-accent); + color: var(--pf-global-accent); line-height: 1.2; - letter-spacing: -0.3px; + letter-spacing: -0.5px; } .paperforge-snapshot-label { - font-size: 10px; + font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.3px; - margin-top: 2px; + margin-top: 4px; text-align: center; } -/* ── System Status ── */ -.paperforge-system-status { - padding: 16px; - background: var(--background-primary); - border: 1px solid var(--background-modifier-border); - border-radius: var(--radius-m); - transition: border-color 0.2s, box-shadow 0.2s; - position: relative; -} - -.paperforge-system-status::before { - content: ''; - position: absolute; - inset: -1px; - border-radius: inherit; - opacity: 0; - box-shadow: 0 2px 8px rgba(0,0,0,0.08); - transition: opacity 0.2s; - pointer-events: none; -} - -.paperforge-system-status:hover::before { - opacity: 1; -} - .paperforge-status-grid { display: flex; flex-direction: column; @@ -2797,8 +2259,8 @@ .paperforge-status-row { display: flex; align-items: center; - gap: 8px; - padding: 4px 0; + gap: 10px; + padding: 6px 0; } .paperforge-status-dot { @@ -2806,12 +2268,12 @@ height: 7px; border-radius: 50%; flex-shrink: 0; - background: var(--background-modifier-border); + background: var(--pf-border); transition: background 0.3s; } .paperforge-status-dot.ok { - background: var(--color-green); + background: var(--text-success); } .paperforge-status-dot.fail { @@ -2825,123 +2287,89 @@ } .paperforge-status-detail { - font-size: 12px; + font-size: 13px; color: var(--text-faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -/* ── Global Actions ── */ -.paperforge-global-actions { - padding: 16px; - background: var(--background-primary); - border: 1px solid var(--background-modifier-border); - border-radius: var(--radius-m); - transition: border-color 0.2s, box-shadow 0.2s; - position: relative; -} - -.paperforge-global-actions::before { - content: ''; - position: absolute; - inset: -1px; - border-radius: inherit; - opacity: 0; - box-shadow: 0 2px 8px rgba(0,0,0,0.08); - transition: opacity 0.2s; - pointer-events: none; -} - -.paperforge-global-actions:hover::before { - opacity: 1; -} - .paperforge-global-actions-row { display: flex; gap: 8px; + flex-wrap: wrap; } -/* ========================================================================== - SECTION 43 — Workflow Toggle Checkboxes (per-paper) - ========================================================================== */ - -.paperforge-workflow-toggles { - display: flex; - gap: 14px; - padding: 12px 16px; - background: var(--background-primary); - border: 1px solid var(--background-modifier-border); - border-radius: var(--radius-m); - margin: 10px 0; - transition: border-color 0.2s, box-shadow 0.2s; - position: relative; -} - -.paperforge-workflow-toggles::before { - content: ''; - position: absolute; - inset: -1px; - border-radius: inherit; - opacity: 0; - box-shadow: 0 2px 8px rgba(0,0,0,0.08); - transition: opacity 0.2s; - pointer-events: none; -} - -.paperforge-workflow-toggles:hover::before { - opacity: 1; -} - -.paperforge-workflow-toggle { - display: flex; - align-items: center; - gap: 6px; - cursor: pointer; +.paperforge-collection-actions .paperforge-contextual-btn, +.paperforge-global-actions-row .paperforge-contextual-btn { + min-height: 36px; + padding: 8px 14px; font-size: 13px; - color: var(--text-muted); - user-select: none; -} - -.paperforge-workflow-checkbox { - width: 15px; - height: 15px; - margin: 0; - cursor: pointer; - accent-color: var(--interactive-accent); -} - -.paperforge-workflow-toggle-label { font-weight: 600; - color: var(--text-normal); } -.paperforge-workflow-toggle-hint { - color: var(--text-faint); +.paperforge-global-actions-row .paperforge-contextual-btn.primary { + background: color-mix(in srgb, var(--pf-global-accent) 86%, black 14%); + border-color: color-mix(in srgb, var(--pf-global-accent) 90%, black 10%); + color: var(--text-on-accent); +} + +.paperforge-collection-actions .paperforge-contextual-btn.primary { + background: color-mix(in srgb, var(--pf-collection-accent) 84%, black 16%); + border-color: color-mix(in srgb, var(--pf-collection-accent) 88%, black 12%); + color: var(--text-on-accent); +} + +.paperforge-global-actions-row .paperforge-contextual-btn.primary:hover { + background: color-mix(in srgb, var(--pf-global-accent) 78%, black 22%); + border-color: color-mix(in srgb, var(--pf-global-accent) 82%, black 18%); +} + +.paperforge-collection-actions .paperforge-contextual-btn.primary:hover { + background: color-mix(in srgb, var(--pf-collection-accent) 76%, black 24%); + border-color: color-mix(in srgb, var(--pf-collection-accent) 80%, black 20%); +} + +.paperforge-collection-view .paperforge-ocr-section { + padding: 18px 18px 16px; + background: color-mix(in srgb, var(--background-secondary) 90%, var(--background-primary) 10%); + border-color: color-mix(in srgb, var(--pf-warm-line) 20%, var(--background-modifier-border)); + box-shadow: 0 1px 2px rgba(20, 18, 14, 0.04), 0 8px 20px rgba(20, 18, 14, 0.04); +} + +.paperforge-collection-view .paperforge-ocr-count-label { font-size: 11px; } -/* ========================================================================== - SECTION 44 — Dark Theme Specific Overrides - ========================================================================== */ - -.theme-dark .paperforge-paper-overview, -.theme-dark .paperforge-discussion-card, -.theme-dark .paperforge-workflow-overview, +.theme-dark .paperforge-collection-view .paperforge-ocr-section, .theme-dark .paperforge-library-snapshot, .theme-dark .paperforge-system-status, .theme-dark .paperforge-global-actions, -.theme-dark .paperforge-workflow-toggles { - background: var(--background-primary); - border-color: var(--background-modifier-border); +.theme-dark .paperforge-workflow-overview { + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3); } -.theme-dark .paperforge-paper-overview::before, -.theme-dark .paperforge-discussion-card::before, -.theme-dark .paperforge-workflow-overview::before, -.theme-dark .paperforge-library-snapshot::before, -.theme-dark .paperforge-system-status::before, -.theme-dark .paperforge-global-actions::before, -.theme-dark .paperforge-workflow-toggles::before { - box-shadow: 0 2px 12px rgba(0,0,0,0.3); +/* Dark theme overrides */ +.theme-dark .paperforge-status-pill.ok { + background: rgba(100, 200, 120, 0.15); +} + +.theme-dark .paperforge-status-pill.fail { + background: rgba(255, 100, 100, 0.15); +} + +.theme-dark .paperforge-library-snapshot:hover, +.theme-dark .paperforge-system-status:hover, +.theme-dark .paperforge-global-actions:hover, +.theme-dark .paperforge-workflow-overview:hover, +.theme-dark .paperforge-workflow-toggles:hover { + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3); +} + +.theme-dark .paperforge-paper-overview:hover { + box-shadow: inset 3px 0 0 color-mix(in srgb, var(--pf-paper-accent) 22%, transparent), 0 2px 12px rgba(0, 0, 0, 0.3); +} + +.theme-dark .paperforge-discussion-card:hover { + box-shadow: inset 3px 0 0 color-mix(in srgb, var(--pf-paper-accent) 16%, transparent), 0 2px 12px rgba(0, 0, 0, 0.3); } From d8a0d4d83ea1eda9b3341196a281d3e57c2cfef0 Mon Sep 17 00:00:00 2001 From: Research Assistant Date: Sun, 10 May 2026 19:15:10 +0800 Subject: [PATCH 25/37] docs: align dashboard ux contract with refined ui --- docs/ux-contract.md | 6 +++--- paperforge/plugin/main.js | 29 ++++++++++++++++++++--------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/docs/ux-contract.md b/docs/ux-contract.md index f3b04c2d..f4dfc155 100644 --- a/docs/ux-contract.md +++ b/docs/ux-contract.md @@ -47,10 +47,10 @@ | Step ID | Trigger | Action | Measurable Outcome | Error Contract | |---------|---------|--------|-------------------|----------------| | W4-S1 | User opens Obsidian vault | Plugin loads and registers dashboard view | Plugin view registered; sidebar icon visible; clicking opens PaperForge dashboard panel | Plugin not loaded; console shows "Failed to register PaperForge view" | -| W4-S2 | User opens a `.base` file | Collection mode activates | Dashboard switches to `collection` mode; shows: workflow funnel (Total → PDF Ready → OCR Done → Deep Read), OCR pipeline progress bar scoped to current base, issue summary (compact, only when issues exist), action buttons (Sync Library, Run OCR) | Falls to global mode; missing stats | -| W4-S3 | User opens a formal note (`.md` with `zotero_key`) | Paper mode activates | Dashboard switches to `paper` mode; shows: status strip (PDF/OCR/DeepRead pills), paper overview card (Pass 1 summary from `## 🔍 精读`), workflow toggle checkboxes (`do_ocr`/`analyze` — sync to Base), next-step card, files row (Open PDF / Open Fulltext), collapsible technical details | Falls to global mode; components empty or erroring | +| W4-S2 | User opens a `.base` file | Collection mode activates | Dashboard switches to `collection` mode; preserves module order: header, workflow funnel (Total → PDF Ready → OCR Done → Deep Read), OCR pipeline scoped to current base, collection-scoped issue/health messaging only inside the collection view, then action buttons (Run OCR primary, Sync Library secondary) | Falls to global mode; missing stats; collection issues appear as a separate extra module outside the collection view | +| W4-S3 | User opens a formal note (`.md` with `zotero_key`) | Paper mode activates | Dashboard switches to `paper` mode; preserves module order: paper header, status/file row, paper overview card, next-step or complete row, discussion card, collapsible technical details; panel keeps bottom-safe-area padding so final content is fully visible; expanding technical details does not trigger scrollbar reflow/width jump; first discussion expand does not collapse on the first click | Falls to global mode; components empty or erroring; bottom content is clipped; expanding discussion/technical details causes collapse or layout jump | | W4-S4 | User opens `fulltext.md` or other workspace file | Paper mode activates via workspace detection | Dashboard stays in `paper` mode for any file inside a paper workspace directory (`{key} - {title}/`). zotero_key resolved from dirname pattern | Falls to global mode | -| W4-S5 | User opens no file or non-workspace `.md` | Global mode activates | Dashboard shows: library snapshot (papers / PDFs / OCR done / deep-read done), system status grid (runtime / index / Zotero export / OCR token), issues panel (only when anomalies exist with Run Doctor / Repair Issues), action buttons (Open Literature Hub / Sync Library) | Shows loading/empty state; console errors | +| W4-S5 | User opens no file or non-workspace `.md` | Global mode activates | Dashboard shows, in preserved order: library snapshot (papers / PDFs / OCR done / deep-read done), system status grid (runtime / index / Zotero export / OCR token), optional issues panel (only when anomalies exist with Run Doctor / Repair Issues), then action buttons (Open Literature Hub primary / Sync Library secondary); light and dark themes both keep muted accents, readable contrast, and visible keyboard focus on collection/global action controls | Shows loading/empty state; console errors; module order changes unexpectedly; light/dark theme contrast or focus visibility regresses | | W4-S6 | User toggles `do_ocr` or `analyze` checkbox | Workflow toggle writes to frontmatter | Checkbox state writes to formal note YAML frontmatter via `processFrontMatter`; Base view reflects change immediately (reads from file); dashboard auto-refreshes via `modify` event | Frontmatter not written; notice shows "Note file not found" | --- diff --git a/paperforge/plugin/main.js b/paperforge/plugin/main.js index ef4f11d6..01891377 100644 --- a/paperforge/plugin/main.js +++ b/paperforge/plugin/main.js @@ -1316,7 +1316,7 @@ class PaperForgeStatusView extends ItemView { const actionsRow = view.createEl('div', { cls: 'paperforge-global-actions' }); actionsRow.createEl('div', { cls: 'paperforge-section-label', text: 'Start Working' }); const btnsRow = actionsRow.createEl('div', { cls: 'paperforge-global-actions-row' }); - const hubBtn = btnsRow.createEl('button', { cls: 'paperforge-contextual-btn' }); + const hubBtn = btnsRow.createEl('button', { cls: 'paperforge-contextual-btn primary' }); hubBtn.createEl('span', { cls: 'paperforge-contextual-btn-icon', text: '\uD83D\uDCC1' }); hubBtn.createEl('span', { text: 'Open Literature Hub' }); hubBtn.addEventListener('click', () => { @@ -1817,6 +1817,14 @@ class PaperForgeStatusView extends ItemView { // ── Contextual Actions ── const actionsRow = view.createEl('div', { cls: 'paperforge-collection-actions' }); + const ocrActionBtn = actionsRow.createEl('button', { cls: 'paperforge-contextual-btn primary' }); + ocrActionBtn.createEl('span', { cls: 'paperforge-contextual-btn-icon', text: '\u229E' }); + ocrActionBtn.createEl('span', { text: 'Run OCR' }); + ocrActionBtn.addEventListener('click', () => { + const action = ACTIONS.find(a => a.id === 'paperforge-ocr'); + if (action) this._runAction(action, ocrActionBtn); + }); + const syncBtn = actionsRow.createEl('button', { cls: 'paperforge-contextual-btn' }); syncBtn.createEl('span', { cls: 'paperforge-contextual-btn-icon', text: '\u21BB' }); syncBtn.createEl('span', { text: 'Sync Library' }); @@ -1824,14 +1832,6 @@ class PaperForgeStatusView extends ItemView { const action = ACTIONS.find(a => a.id === 'paperforge-sync'); if (action) this._runAction(action, syncBtn); }); - - const ocrActionBtn = actionsRow.createEl('button', { cls: 'paperforge-contextual-btn' }); - ocrActionBtn.createEl('span', { cls: 'paperforge-contextual-btn-icon', text: '\u229E' }); - ocrActionBtn.createEl('span', { text: 'Run OCR' }); - ocrActionBtn.addEventListener('click', () => { - const action = ACTIONS.find(a => a.id === 'paperforge-ocr'); - if (action) this._runAction(action, ocrActionBtn); - }); } /* ── Refresh current mode (called on index change, D-09, REFR-01) ── */ @@ -2067,6 +2067,17 @@ class PaperForgeStatusView extends ItemView { const leafHandler = this.app.workspace.on('active-leaf-change', () => { clearTimeout(this._leafChangeTimer); this._leafChangeTimer = setTimeout(() => { + const resolved = this._resolveModeForFile(this.app.workspace.getActiveFile()); + const nextMode = resolved.mode; + const nextFilePath = resolved.filePath; + + // Clicking inside the dashboard can activate its leaf without changing + // the underlying paper/base context. Avoid rebuilding the whole mode + // tree in that case, or transient UI state like discussion expansion resets. + if (this._currentMode === nextMode && this._currentFilePath === nextFilePath) { + return; + } + this._detectAndSwitch(); }, 300); }); From 61822c8a9466d5248306fe2d25d76a37f61fc29f Mon Sep 17 00:00:00 2001 From: Research Assistant Date: Sun, 10 May 2026 19:15:28 +0800 Subject: [PATCH 26/37] docs: add dashboard ui refinement specs and plans --- .../plans/2026-05-10-dashboard-ui-refine.md | 398 ++++++++++++++++++ .../2026-05-10-dashboard-visual-refinement.md | 161 +++++++ .../2026-05-10-dashboard-ui-refine-design.md | 395 +++++++++++++++++ 3 files changed, 954 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-10-dashboard-ui-refine.md create mode 100644 docs/superpowers/plans/2026-05-10-dashboard-visual-refinement.md create mode 100644 docs/superpowers/specs/2026-05-10-dashboard-ui-refine-design.md diff --git a/docs/superpowers/plans/2026-05-10-dashboard-ui-refine.md b/docs/superpowers/plans/2026-05-10-dashboard-ui-refine.md new file mode 100644 index 00000000..2f589d75 --- /dev/null +++ b/docs/superpowers/plans/2026-05-10-dashboard-ui-refine.md @@ -0,0 +1,398 @@ +# Dashboard UI Refine Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Refine the PaperForge Obsidian dashboard so `paper`, `collection`, and `global` modes feel stable, readable, and visually intentional while fixing the current layout and interaction defects. + +**Architecture:** Keep the existing three-mode dashboard structure in `paperforge/plugin/main.js`, but reduce refresh-related state loss and clean up the dashboard CSS into one authoritative visual system in `paperforge/plugin/styles.css`. Preserve Obsidian structural tokens for surfaces/text/borders, then layer a muted PaperForge accent system on top for badges, metrics, pills, and primary actions. + +**Tech Stack:** Obsidian plugin DOM API, CommonJS plugin runtime, Vitest, CSS custom properties, Obsidian theme variables. + +--- + +## File Structure + +- Modify: `paperforge/plugin/main.js` + - Dashboard leaf activation / refresh behavior + - Paper-mode transient state preservation + - Small DOM/class adjustments only if required by the new visual system +- Modify: `paperforge/plugin/styles.css` + - Consolidate duplicate dashboard CSS + - Add stable scroll / bottom-safe-area behavior + - Implement the `Quiet Research Desk` palette and hierarchy rules +- Modify: `docs/ux-contract.md` + - Update workflow contract expectations for `paper`, `collection`, and `global` +- Test: `paperforge/plugin/tests/*.mjs` + - Keep existing suite green + - Add a focused regression test only if a new pure helper or testable branch is extracted + +--- + +### Task 1: Lock Scroll Stability and Preserve the Existing Refresh Fix + +**Files:** +- Modify: `paperforge/plugin/main.js` +- Modify: `paperforge/plugin/styles.css` +- Test: `paperforge/plugin/tests/*.mjs` + +- [ ] **Step 1: Add a regression note to the plan execution log** + +Record the two interaction defects at the top of your execution scratchpad / task notes to preserve focus during implementation: + +```text +- First discussion expand must not collapse on first click. +- Technical-details expand must not trigger width jump from late scrollbar appearance. +``` + +- [ ] **Step 2: Verify the current automated baseline before edits** + +Run: `npm test` +Workdir: `paperforge/plugin` +Expected: `40 passed` (or current equivalent with zero failures) + +- [ ] **Step 3: Verify the existing discussion-refresh fix remains intact** + +Do not re-implement the `active-leaf-change` guard if it is already present. Instead, confirm the current `main.js` logic still short-circuits leaf-focus-only transitions and does not get accidentally removed while refining UI code. + +Verification target: + +```text +If resolved mode and file path are unchanged, active-leaf-change must not rebuild the mode tree. +``` + +This protection must continue to preserve both: + +```text +- discussion expanded/collapsed state +- technical-details expanded/collapsed state +``` + +- [ ] **Step 4: Stabilize the root scroll container in `styles.css`** + +Apply bottom safe-area and stable vertical scrollbar behavior at the dashboard container level. + +Implementation shape: + +```css +.paperforge-status-panel { + overflow-y: auto; + scrollbar-gutter: stable; + padding: 14px 14px 56px 14px; +} +``` + +If `scrollbar-gutter` alone is insufficient during manual verification, add a compatible fallback without changing mode structure, for example a permanent vertical overflow reservation strategy at the same container boundary. + +- [ ] **Step 5: Run the automated suite again** + +Run: `npm test` +Workdir: `paperforge/plugin` +Expected: zero failures + +- [ ] **Step 6: Commit** + +```bash +git add paperforge/plugin/main.js paperforge/plugin/styles.css +git commit -m "fix: stabilize dashboard leaf refresh and scroll layout" +``` + +--- + +### Task 2: Consolidate Dashboard CSS Authority + +**Files:** +- Modify: `paperforge/plugin/styles.css` +- Test: `paperforge/plugin/tests/*.mjs` + +- [ ] **Step 1: Identify the authoritative dashboard section blocks** + +Use the existing redesign spec as the source of truth and remove overlapping duplicate selector definitions for: + +```text +.paperforge-section-label +.paperforge-contextual-btn +.paperforge-status-strip +.paperforge-paper-overview +.paperforge-discussion-card +.paperforge-technical-details* +.paperforge-workflow-overview* +.paperforge-library-snapshot* +.paperforge-system-status* +.paperforge-global-actions* +.paperforge-workflow-toggles* +``` + +- [ ] **Step 2: Rewrite to one authoritative visual system** + +Prefer a single final selector block per dashboard component family. Do not stack another layer of overrides at the bottom of the file. + +Target structure: + +```css +/* shared tokens */ +/* paper mode */ +/* collection mode */ +/* global mode */ +/* dark theme overrides */ +``` + +- [ ] **Step 3: Introduce muted PaperForge accent tokens** + +Keep surfaces/text/borders Obsidian-native, and add a restrained PaperForge tint layer. + +Implementation shape: + +```css +.paperforge-status-panel { + --pf-paper-accent: ...; + --pf-collection-accent: ...; + --pf-global-accent: ...; + --pf-warm-line: ...; +} +``` + +Do not apply these as large full-card backgrounds. + +- [ ] **Step 4: Run the automated suite** + +Run: `npm test` +Workdir: `paperforge/plugin` +Expected: zero failures + +- [ ] **Step 5: Commit** + +```bash +git add paperforge/plugin/styles.css +git commit -m "refactor: unify dashboard style system" +``` + +--- + +### Task 3: Rebalance Paper Mode + +**Files:** +- Modify: `paperforge/plugin/styles.css` +- Modify: `paperforge/plugin/main.js` (only if a class hook is needed) +- Test: `paperforge/plugin/tests/*.mjs` + +- [ ] **Step 1: Raise paper-mode comfort without changing module order** + +Keep the existing order: + +```text +paper header +status/file row +overview card +next-step or complete row +discussion card +technical details +``` + +- [ ] **Step 2: Make technical details lighter than primary cards** + +Use disclosure styling that reads as metadata, not as a full secondary card. + +Implementation shape: + +```css +.paperforge-technical-details-toggle { + width: 100%; + min-height: 34px; +} + +.paperforge-technical-details-body { + padding-top: 8px; +} +``` + +- [ ] **Step 3: Keep file actions neutral and discussion readable** + +Preserve neutral file-opening actions in `paper` mode; do not promote them with strong tinting. Maintain body copy at `14px` minimum and keep discussion/overview as the most legible blocks. + +- [ ] **Step 4: Run the automated suite** + +Run: `npm test` +Workdir: `paperforge/plugin` +Expected: zero failures + +- [ ] **Step 5: Commit** + +```bash +git add paperforge/plugin/main.js paperforge/plugin/styles.css +git commit -m "style: rebalance paper mode for reading" +``` + +--- + +### Task 4: Strengthen Collection and Global Hierarchy + +**Files:** +- Modify: `paperforge/plugin/styles.css` +- Modify: `paperforge/plugin/main.js` (only if action classes or wrappers need a tiny hook) +- Test: `paperforge/plugin/tests/*.mjs` + +- [ ] **Step 1: Scale up collection metrics and workflow stage presence** + +Meet the measured targets from the spec: + +```css +.paperforge-workflow-stage { + min-width: 72px; +} + +.paperforge-snapshot-pill { + min-width: 72px; +} + +.paperforge-collection-actions .paperforge-contextual-btn, +.paperforge-global-actions-row .paperforge-contextual-btn { + font-size: 13px; +} + +.paperforge-workflow-stage-value, +.paperforge-snapshot-value { + font-size: 22px; +} +``` + +Also keep `collection` / `global` labels and detail text at spec minimums: + +```css +.paperforge-workflow-stage-label, +.paperforge-snapshot-label { + font-size: 11px; +} + +.paperforge-section-label { + font-size: 12px; +} + +.paperforge-status-label, +.paperforge-status-detail, +.paperforge-collection-count { + font-size: 13px; +} +``` + +- [ ] **Step 2: Promote the primary actions explicitly** + +Apply stronger but muted emphasis only to: + +```text +global: Open Literature Hub +collection: Run OCR +``` + +Keep `Sync Library` secondary in both modes. + +Enforce button size targets while doing this: + +```css +.paperforge-collection-actions .paperforge-contextual-btn, +.paperforge-global-actions-row .paperforge-contextual-btn { + min-height: 36px; +} +``` + +- [ ] **Step 3: Increase global homepage presence without changing inventory/order** + +Keep the current order: + +```text +library snapshot +system status +optional issues +start working +``` + +Make cards, labels, and controls read as a homepage rather than a compact utility panel. +Keep the OCR module visually heavier than the collection action row by giving the OCR section stronger padding / surface presence than the actions beneath it. + +- [ ] **Step 4: Run the automated suite** + +Run: `npm test` +Workdir: `paperforge/plugin` +Expected: zero failures + +- [ ] **Step 5: Commit** + +```bash +git add paperforge/plugin/main.js paperforge/plugin/styles.css +git commit -m "style: strengthen collection and global dashboard hierarchy" +``` + +--- + +### Task 5: Update UX Contract and Final Verification + +**Files:** +- Modify: `docs/ux-contract.md` +- Test: `paperforge/plugin/tests/*.mjs` + +- [ ] **Step 1: Update Workflow 4 contract entries** + +Reflect the refined dashboard expectations in `docs/ux-contract.md`: + +```text +- paper mode bottom-safe-area expectation +- no scrollbar reflow on technical-details expand +- no first-click discussion collapse +- preserved module order for collection mode +- collection-mode issues remain scoped inside the collection view rather than being promoted into a separate extra module +- preserved module order for global mode +- light/dark theme verification expectation +``` + +- [ ] **Step 2: Run the full automated suite** + +Run: `npm test` +Workdir: `paperforge/plugin` +Expected: zero failures + +- [ ] **Step 3: Perform manual verification in Obsidian desktop** + +Checklist: + +```text +- Windows light theme: paper bottom content is fully visible +- Windows light theme: technical details expand without width jump +- Windows light theme: first discussion expand does not flash closed +- Windows light theme: with discussion expanded, leaf activation alone does not collapse it when resolved identity is unchanged +- Windows light theme: with technical details expanded, leaf activation alone does not collapse it when resolved identity is unchanged +- Windows light theme: collection feels larger and action hierarchy is obvious +- Windows light theme: global feels like a homepage, not a utility panel +- Windows light theme: keyboard focus is visible on collection/global action controls +- Windows dark theme: contrast, focus visibility, and muted accents remain readable +- Windows dark theme: keyboard focus is visible on collection/global action controls +``` + +- [ ] **Step 4: Commit** + +```bash +git add docs/ux-contract.md paperforge/plugin/main.js paperforge/plugin/styles.css +git commit -m "docs: align dashboard ux contract with refined ui" +``` + +--- + +## Implementation Notes + +- Do not add new dashboard modes. +- Do not change CLI/data contracts. +- Do not introduce theme-specific palettes. +- Prefer removing duplicate CSS over overriding it again. +- If automated UI regression coverage requires extracting a small pure helper, keep the extraction minimal and local to dashboard behavior. + +## Final Verification Commands + +```bash +cd paperforge/plugin +npm test +``` + +Expected: + +```text +Test Files 3 passed +Tests 40 passed +``` diff --git a/docs/superpowers/plans/2026-05-10-dashboard-visual-refinement.md b/docs/superpowers/plans/2026-05-10-dashboard-visual-refinement.md new file mode 100644 index 00000000..62ab09fa --- /dev/null +++ b/docs/superpowers/plans/2026-05-10-dashboard-visual-refinement.md @@ -0,0 +1,161 @@ +# PaperForge Dashboard Visual Refinement Plan + +> **For agentic workers:** Use superpowers:subagent-driven-development or inline execution. Steps use checkbox (`- [ ]`) syntax. + +**Goal:** Apply Native Light Surface Design spec — strict typography, minimal containers, Obsidian-native colors, no decorative shadows, locale-unified UI text. + +**Architecture:** CSS-first: establish --pf-* custom properties and .pf-* utility classes on `.paperforge-status-panel`, then rewrite Sections 39-43 with constrained typography (4 sizes, 3 weights), light borders, and contextual cards only for primary content modules. JS: restructure per-paper layout to merge header/status/files rows, collapse OCR/Analyze into technical details, compact "All Set" state. + +**Tech Stack:** Obsidian DOM API (vanilla JS), CSS with Obsidian semantic variables only. + +**Commit style:** `type(scope): description` (English, semantic, existing pattern). + +--- + +## Files +- Modify: `paperforge/plugin/styles.css` (large rewrite of Sections 39-43) +- Modify: `paperforge/plugin/main.js` (per-paper layout + locale text) + +--- + +### Task 1: Commit current CSS base changes + +**Files:** styles.css (staged change on paperforge-status-panel + utility classes) + +- [ ] Git commit the pending styles.css change + +```bash +git add paperforge/plugin/styles.css +git commit -m "style(dashboard): establish CSS base --pf-* vars, typography tokens, utility classes" +``` + +--- + +### Task 2: Rewrite CSS Section 39 — Shared components + +**Files:** styles.css (replace Section 39 "Shared Components") + +Replace the existing Section 39 with: +- `.pf-card` — only for primary content modules (no box-shadow) +- `.pf-pill` + modifiers `.pf-pill--ok/warn/fail` — 999px radius, subtle +- `.pf-btn-secondary` / `.pf-btn-primary` — Obsidian interactive vars +- `.pf-complete-row` — compact green text, no card +- `.pf-disclosure-row` + `.pf-disclosure-body` — simple text+cursor +- `.pf-section-label` — 12px/600/uppercase/no accent border +- `.pf-stack-8` / `.pf-stack-12` — spacing utilities +- Remove old `.paperforge-section-label` left-border accent +- Remove old contextual button styles (replaced by pf-btn-*) +- Remove old card ::before elevation pseudo-elements + +--- + +### Task 3: Rewrite CSS Section 40 — Per-paper view + +**Files:** styles.css (replace Section 40) + +Apply constrained typography to per-paper components: +- `.paperforge-paper-header`: title 16px/600, meta 13px/400, year 12px/400 (no bullet separator, use ` · `) +- `.paperforge-status-strip`: remove borders, keep flex + gap +- `.paperforge-status-pill`: inherit `.pf-pill` patterns (no colored backgrounds, text-only status colors) +- `.paperforge-paper-overview`: use `.pf-card` base (no shadow, no ::before) +- `.paperforge-discussion-card`: use `.pf-card` base (no shadow, no ::before) +- `.paperforge-discussion-q`: 14px/600 +- `.paperforge-discussion-a`: 14px/400 +- `.paperforge-discussion-expand`: text accent only +- `.paperforge-paper-files`: move inline with status strip +- `.paperforge-technical-details-toggle`: use `.pf-disclosure-row` style +- `.paperforge-technical-details-body`: use `.pf-disclosure-body` style +- `.paperforge-workflow-toggles`: NOT a card (remove background, border, shadow). Simple flex row. +- Dark theme overrides: remove shadow rules, keep only background/border tweaks + +--- + +### Task 4: Rewrite CSS Section 41 — Collection/Base + +**Files:** styles.css (replace Section 41) + +- `.paperforge-workflow-overview`: `.pf-card` base, remove shadow +- `.paperforge-workflow-stage-value`: 18px/600, accent only for metric +- `.paperforge-collection-header`: title 16px/600 +- `.paperforge-issue-summary`: light border-left accent (orange), no full red border +- `.paperforge-collection-actions`: simple flex row + +--- + +### Task 5: Rewrite CSS Section 42 — Global/Home + +**Files:** styles.css (replace Section 42) + +- `.paperforge-library-snapshot`: `.pf-card` base, remove shadow +- `.paperforge-system-status`: `.pf-card` base, remove shadow +- `.paperforge-global-actions`: `.pf-card` base, remove shadow +- `.paperforge-snapshot-value`: 18px/600 +- `.paperforge-status-row`: dot stays, label 14px + +--- + +### Task 6: Rewrite JS per-paper layout (merge header + status strip + file row) + +**Files:** main.js — `_renderPaperMode` and helpers + +Layout per spec: +``` +[PAPER badge] title +authors · year +[PDF ✓] [OCR ✓] [精读 ✓] [打开 PDF] [打开全文] +┌ 文章概览 ─────────────┐ +└──────────────────────┘ +✓ 已完成,可直接使用 +┌ 最近讨论 ─────────────┐ +└──────────────────────┘ +技术详情 ▸ +``` + +Changes: +1. Status strip: place PDF pill + OCR pill + DeepRead pill left, Open PDF + Open Fulltext right (same flex row) +2. No separate `paperforge-paper-files` section below — file buttons ARE in the status row +3. Workflow toggles (OCR/Analyze checkbox): remove from standalone section, move into `_renderPaperTechnicalDetails` disclosure body +4. Next step card: when `nextStep === 'ready'`, render compact `.pf-complete-row` instead of card +5. Next step card: remove `'RECOMMENDED NEXT STEP'` all-caps label, use "下一步" (zh) / "Next Step" (en) +6. Locale: all UI text use Chinese when T===zh + +--- + +### Task 7: Locale cleanup + +**Files:** main.js — ensure all visible UI labels are locale-consistent + +- "Open PDF" → `t('open_pdf')` or hardcoded 中文 `打开 PDF` +- "Open Fulltext" → `打开全文` +- "Copy /pf-deep Command" → `复制精读命令` or remove +- "Run in {0}" → remove or make Chinese +- Next-step labels: use Chinese when zh +- Section labels: "文章概览" stays, "最近讨论" stays, "技术详情" stays +- "Add to OCR Queue" → `加入 OCR` +- "Remove from OCR Queue" → `移出 OCR` +- "Analyze" label → `精读` / `标记精读` + +Add missing i18n keys to LANG.zh and LANG.en. + +--- + +### Task 8: Regression verification + +- [ ] Verify syntax: `node --check main.js` +- [ ] Run tests: `npx vitest run` (all 40 pass) +- [ ] Copy to test vault +- [ ] Visual check: per-paper with 3EWBBTAS (deep-read done), 2Y9M3ILK (discussion), B29TFCL4 (no OCR) +- [ ] Check global view layout +- [ ] Check collection view layout +- [ ] Check dark theme (toggle Obsidian theme to dark) +- [ ] Check narrow sidebar width + +--- + +## Non-goals +- Don't add new fonts +- Don't add decorative shadows to cards +- Don't add colored backgrounds to status pills +- Don't use more than 4 font sizes +- Don't use font-weight 700 +- Don't rebuild existing working modules (only restructure layout, not logic) diff --git a/docs/superpowers/specs/2026-05-10-dashboard-ui-refine-design.md b/docs/superpowers/specs/2026-05-10-dashboard-ui-refine-design.md new file mode 100644 index 00000000..8cd7e11a --- /dev/null +++ b/docs/superpowers/specs/2026-05-10-dashboard-ui-refine-design.md @@ -0,0 +1,395 @@ +# PaperForge Dashboard UI Refine Design + +> Approved direction: `Quiet Research Desk` +> Tone: `Calm editorial` +> Color posture: preserve PaperForge's own muted identity more than Obsidian theme accent, while remaining structurally theme-compatible. + +--- + +## Goal + +Refine the PaperForge Obsidian dashboard so the three active modes feel stable, readable, and visually intentional. + +This pass must remove current interaction defects, increase visual hierarchy in `global` and `collection` modes, and replace the current overly small / overly uniform surface treatment with a warmer, quieter dashboard language that still fits naturally inside Obsidian. + +--- + +## In Scope + +1. Fix layout and interaction defects in the current dashboard UI. +2. Simplify and unify dashboard CSS so one visual system controls all three modes. +3. Increase perceived weight and readability of `global` and `collection` views. +4. Preserve `paper` mode as the calmest mode, optimized for reading and discussion. +5. Add restrained color accents inspired by muted Morandi / Maillard / soft American editorial palettes. +6. Keep compatibility with Obsidian themes for structure, contrast, and dark/light behavior. + +## Out of Scope + +1. Adding new dashboard modes. +2. Changing data contracts or CLI behavior. +3. Major DOM restructuring outside the current dashboard components. +4. Theme-specific per-theme palettes. +5. New analytics, charts, or workflow states. + +--- + +## Problems To Solve + +### 1. Bottom clipping in `paper` mode + +The dashboard scroll container ends too tightly against Obsidian's lower chrome. Expanded technical details can be visually blocked by the bottom bar. + +### 2. Scrollbar-induced layout jump + +Opening technical details can push content over the vertical overflow threshold, causing the right scrollbar to appear only in the expanded state. This changes content width and makes text reflow visibly. + +### 3. First discussion expand flash + +The first click on a discussion expand control can trigger an unnecessary mode refresh due to `active-leaf-change`, which destroys transient local UI state. + +### 4. `collection` mode feels underweighted and empty + +The funnel, OCR section, and actions are all visually too small and too light relative to the available vertical space. The action row feels like footer utilities instead of primary workflow controls. + +### 5. `global` mode feels like a compressed utility panel + +Snapshot metrics, system status, and actions do not yet read as a real homepage. Typography and card weight are too restrained. + +### 6. Visual language is too uniform and slightly sterile + +Nearly every module uses the same thin border, small label, and low-energy box treatment. The result is clean but fatiguing. + +### 7. CSS authority is split across duplicated sections + +`styles.css` contains overlapping dashboard definitions from multiple iterations. The file currently mixes the newer native-light-surface pass with later redefinitions, causing inconsistency and making further refinement brittle. + +--- + +## Design Principles + +### Quiet Research Desk + +The dashboard should feel like a well-kept research desk, not an admin console. + +- Calm, not cold +- Warm, not decorative +- Structured, not dense +- Native to Obsidian, not generic web SaaS + +### Mode personality hierarchy + +- `paper`: quietest, most reading-oriented, least chrome +- `collection`: strongest workflow personality, denser operational cues +- `global`: homepage personality, strongest sense of entry and orientation + +### Structural theme compatibility + +The dashboard should inherit all major structural tokens from Obsidian: + +- surfaces +- text hierarchy +- borders +- hover / active interaction colors +- dark/light behavior + +### Selective brand tinting + +PaperForge should keep a light house style of its own through restrained accent zones only: + +- mode badges +- section markers +- emphasized numbers +- status pills +- light hover / focus accents + +Accent tinting must never dominate full-card backgrounds. + +--- + +## Color Direction + +### Palette family + +Use a muted editorial palette rather than a bright product palette. + +Candidate families: + +- `paper`: sage / dusty teal / cool gray-green +- `collection`: terracotta / muted amber / clay brown +- `global`: navy-gray / smoky blue / quiet brass accent + +### Rules + +1. No large high-saturation fills. +2. No dependence on the user's Obsidian accent as the primary PaperForge identity. +3. Accent color should be visible mostly in compact anchors, not whole surfaces. +4. Dark mode uses the same family, but with contrast adjusted by theme variables. + +### Token ownership + +Structural tokens come from Obsidian variables: + +- surfaces: `--background-*` +- body and metadata text: `--text-*` +- borders and separators: `--background-modifier-*` +- default hover / active button surfaces: `--interactive-*` +- default focus visibility should remain Obsidian-native unless a stronger PaperForge focus ring is required for contrast + +PaperForge-owned tint tokens may be introduced only for: + +- mode badges +- section markers +- emphasized metric numbers +- semantic status pills +- optional primary action emphasis +- light card-edge or label accents in `global` and `collection` + +PaperForge-owned tints must not replace Obsidian's base surface, body text, or default control contrast model. + +--- + +## Visual System Changes + +### Typography + +Raise hierarchy contrast across all modes. + +- Larger metric numbers in `global` and `collection` +- Section labels become more deliberate but not louder +- Buttons gain more presence through padding and weight, not bright fills +- `paper` body text remains the most reading-friendly text block + +Measured targets: + +- `global` / `collection` metric values: minimum `22px`, target `22-24px` +- `paper` section body copy: `14px` minimum, no reduction below current reading size +- `global` / `collection` section labels: minimum `12px` +- `global` / `collection` metadata/detail text: minimum `13px` +- primary and contextual action buttons in `global` / `collection`: minimum `13px` text size + +### Spacing + +- Increase inner card padding in `global` and `collection` +- Add consistent mode-level vertical rhythm +- Add stable bottom safe area to the scroll container +- Reduce cramped micro-gaps around pills, buttons, and disclosure areas + +Measured targets: + +- root dashboard bottom safe area: minimum `56px` +- primary cards in `global` / `collection`: minimum `16px` inner padding +- action buttons: minimum `34px` visual height +- disclosure body top padding: minimum `8px` +- `collection` / `global` mode section gap: minimum `16px` + +### Surfaces + +- Use fewer visual formulas, not more +- Primary content modules may remain carded +- Lightweight metadata / disclosure sections should not pretend to be full cards +- Remove mixed old/new box-shadow and border treatments + +### Actions + +- `collection` and `global` actions must feel like meaningful controls, not utility links +- Action rows should have larger targets and better anchoring in layout +- Primary actions may use slightly stronger tinting, but still muted + +Primary action ownership: + +- `global`: `Open Literature Hub` is the primary navigational action; `Sync Library` is secondary +- `collection`: `Run OCR` is the primary workflow action; `Sync Library` is secondary +- `paper`: keep file-opening actions neutral; do not introduce a new strong primary tint in the status/file row + +Interaction rules: + +- focus rings must remain clearly visible in both light and dark themes +- interactive controls must keep at least Obsidian-native focus visibility +- tinted buttons or pills must maintain readable text contrast in both light and dark themes + +--- + +## Mode-Specific Design + +### Paper Mode + +Keep `paper` mode closest to the current reading companion model. + +Changes: + +1. Add bottom safety padding so technical details and discussion are never cramped by the Obsidian bottom bar. +2. Stabilize vertical scrollbar reservation to eliminate width jump when expanding sections. +3. Preserve the current header and merged status/file row structure. +4. Keep overview and discussion as the main content cards. +5. Make technical details lighter and calmer than primary cards. +6. Ensure discussion expand/collapse interaction never triggers accidental refresh-state loss. + +Measured targets: + +- technical-details expand/collapse must not cause horizontal text reflow from scrollbar appearance on supported desktop targets +- local discussion expand state and technical-details disclosure state must survive leaf activation when resolved mode and file identity stay unchanged +- paper mode may use the weakest accent density of the three modes + +### Collection Mode + +Make `collection` mode feel like an operational workspace. + +Changes: + +1. Increase funnel visual weight and number emphasis. +2. Make OCR pipeline feel like a central module, not a thin progress strip. +3. Enlarge action controls and treat them as workflow entry points. +4. Reduce the sense of unused lower space by giving the mode stronger section bodies and spacing. +5. Use warmer muted workflow accents than `paper` mode. + +Canonical module order: + +1. collection header +2. workflow overview +3. OCR pipeline +4. collection actions + +Measured targets: + +- workflow stage blocks: minimum `72px` visual width +- workflow stage labels: minimum `11px` +- collection action buttons: minimum `36px` visual height +- OCR module must visually read heavier than the action row beneath it +- collection mode must preserve the canonical module order above + +### Global Mode + +Make `global` mode feel like the dashboard homepage. + +Changes: + +1. Increase snapshot card presence. +2. Strengthen system status readability. +3. Give the start-working area more intentional prominence. +4. Keep issues clearly visible when present, but not alarmist. +5. Use the calmest version of the homepage accent family so the screen feels trustworthy rather than flashy. + +Measured targets: + +- snapshot metric blocks: minimum `72px` visual width +- global action buttons: minimum `36px` visual height +- homepage modules should preserve existing inventory and order: snapshot, system status, optional issues, start working + +--- + +## Technical Decisions + +### 1. Single CSS authority + +Dashboard-specific duplicated definitions in `styles.css` must be consolidated so each dashboard selector has one authoritative definition block. + +The implementation should prefer the newer redesign architecture and remove overlapping legacy overrides instead of stacking more overrides on top. + +### 2. Stable scroll layout + +The root dashboard scroll container should reserve scrollbar space consistently where supported, rather than only when overflow appears. + +Fallback rule: + +- on desktop targets where native gutter reservation is unavailable or behaves as overlay-only, the implementation must still avoid visible width-jump during technical-details expansion through compatible fallback styling or permanent vertical overflow reservation + +### 3. Bottom safe area + +The main scroll container or mode content container should include explicit bottom padding sized to prevent visual collision with Obsidian's lower status area. + +### 4. Preserve local UI state where appropriate + +Mode refreshes should not happen when only the dashboard leaf becomes active while the resolved mode/file identity stays unchanged. + +State preservation rules: + +- preserve discussion expanded/collapsed state across leaf activation with unchanged identity +- preserve technical-details disclosure expanded/collapsed state across leaf activation with unchanged identity +- reset those states on actual mode change or file identity change +- explicit manual refresh may reset transient state +- file content mutation that changes the current entry data may refresh the view, but must not transiently collapse state before the refresh completes + +### 5. Theme-aware implementation model + +Implementation should rely on Obsidian variables for structural tokens, and use PaperForge-owned CSS custom properties for restrained accent tints. + +--- + +## Acceptance Criteria + +### Functional + +1. `paper` mode content is fully readable at the bottom with no visual obstruction from Obsidian chrome. +2. Opening technical details no longer causes a first-order layout jump from scrollbar appearance. +3. Discussion expand/collapse no longer flashes closed on first click. +4. Existing mode routing and dashboard actions continue to work. +5. Leaf activation alone with unchanged resolved mode/file identity does not rebuild the current mode tree. + +### Visual + +1. `collection` and `global` metric values are visibly larger than current live values and meet the measured targets above. +2. `paper` remains the lowest-accent, reading-first mode. +3. The three modes keep the same module inventory/order described in this spec while gaining differentiated emphasis. +4. Accent color remains limited to small emphasis zones rather than full-card fills. +5. Theme compatibility is preserved in both light and dark contexts. + +### Accessibility / Interaction + +1. Interactive controls retain visible keyboard focus in light and dark themes. +2. Contextual action buttons in `global` and `collection` meet the minimum visual height targets above. +3. Tinted pills, labels, and emphasized metrics maintain readable contrast against their backgrounds. +4. Expanding technical details must not change content wrap width in Windows desktop verification. + +### Code Quality + +1. Dashboard CSS duplication is materially reduced. +2. New styling logic is easier to reason about than the current stacked overrides. +3. Existing plugin tests continue to pass. + +### UX Contract Deltas Required + +`docs/ux-contract.md` must be updated to reflect: + +1. `paper` mode bottom-safe-area expectation so lower content is not visually obstructed. +2. `paper` mode technical-details expansion must not introduce layout reflow from scrollbar appearance. +3. `paper` mode discussion expand must not collapse on first click due to leaf activation. +4. `collection` mode must preserve current module inventory and order while using larger visual hierarchy. +5. `global` mode must preserve current module inventory and order while using larger visual hierarchy. +6. light and dark theme verification must be mentioned in dashboard view expectations. + +--- + +## Verification Plan + +1. Run plugin test suite after implementation. +2. Manually verify `paper`, `collection`, and `global` modes in Obsidian. +3. Specifically verify: + - paper bottom spacing + - technical details expansion stability + - discussion expand first-click behavior + - perceived size and hierarchy in collection/global + - light and dark theme legibility + - visible keyboard focus on action controls +4. Manual verification targets: + - Obsidian desktop on Windows in light theme + - Obsidian desktop on Windows in dark theme + +--- + +## Files Expected To Change + +- `paperforge/plugin/styles.css` +- `paperforge/plugin/main.js` +- `docs/ux-contract.md` + +Possible test touch-up only if needed: + +- `paperforge/plugin/tests/commands.test.mjs` + +--- + +## Notes + +This is a refinement pass, not a dashboard rewrite. + +The best result is a dashboard that feels more comfortable after ten minutes of use, not just prettier in a screenshot. From e662033fd9b901f36c2193ae124ba5611cce17c2 Mon Sep 17 00:00:00 2001 From: Research Assistant Date: Sun, 10 May 2026 20:16:33 +0800 Subject: [PATCH 27/37] =?UTF-8?q?feat:=20dashboard=20ui=20refine=20?= =?UTF-8?q?=E2=80=94=20arrow=20icons,=20maillard=20colors,=20responsive=20?= =?UTF-8?q?grid,=20setup=20bugfix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dashboard UI: - Replace text expand/collapse with SVG arrow + clickable gradient container - Map agent_platform key to display name (OpenCode, not opencode) - Maillard/Morandi palette for OCR progress bars (pending brown, done deep green) - Shorten 'Needs Attention' to 'Attention' to prevent label wrap - Responsive workflow overview: CSS Grid + container queries (narrow: 2x2, wide: 4x1 with max-width 160px stop) Setup Wizard: - Fix Phase 7 verification crash (undefined agents_src/agents_dst variables) Skill restructure: - Move chart-reading references under references/ directory - Remove old pf-*.md script files (deployed via skill_deploy service) - Add new reference files (deep-reading, paper-qa, paper-resolution, save-session) --- paperforge/plugin/main.js | 46 +++- paperforge/plugin/styles.css | 106 +++++--- paperforge/setup_wizard.py | 2 +- paperforge/skills/literature-qa/SKILL.md | 80 ++++++ .../chart-reading/GSEA富集图.md | 0 .../{ => references}/chart-reading/INDEX.md | 0 .../chart-reading/ROC与PR曲线.md | 0 .../chart-reading/Western Blot条带图.md | 0 .../chart-reading/免疫荧光定量图.md | 0 .../chart-reading/折线图与时间序列.md | 0 .../chart-reading/散点图与气泡图.md | 0 .../chart-reading/显微照片与SEM图.md | 0 .../chart-reading/条形图与误差棒.md | 0 .../chart-reading/桑基图与弦图.md | 0 .../chart-reading/森林图与Meta分析.md | 0 .../chart-reading/火山图与曼哈顿图.md | 0 .../chart-reading/热图与聚类图.md | 0 .../chart-reading/生存曲线.md | 0 .../chart-reading/箱式图与小提琴图.md | 0 .../chart-reading/组织学半定量图.md | 0 .../chart-reading/网络图与通路图.md | 0 .../chart-reading/蛋白质结构图.md | 0 .../chart-reading/降维图(PCA-tSNE-UMAP).md | 0 .../chart-reading/雷达图与漏斗图.md | 0 .../literature-qa/references/deep-reading.md | 171 +++++++++++++ .../deep-subagent.md} | 0 .../literature-qa/references/paper-qa.md | 61 +++++ .../references/paper-resolution.md | 173 +++++++++++++ .../literature-qa/references/save-session.md | 55 ++++ .../skills/literature-qa/scripts/pf-deep.md | 237 ------------------ .../skills/literature-qa/scripts/pf-end.md | 67 ----- .../skills/literature-qa/scripts/pf-ocr.md | 102 -------- .../skills/literature-qa/scripts/pf-paper.md | 107 -------- .../skills/literature-qa/scripts/pf-status.md | 94 ------- .../skills/literature-qa/scripts/pf-sync.md | 85 ------- 35 files changed, 648 insertions(+), 738 deletions(-) create mode 100644 paperforge/skills/literature-qa/SKILL.md rename paperforge/skills/literature-qa/{ => references}/chart-reading/GSEA富集图.md (100%) rename paperforge/skills/literature-qa/{ => references}/chart-reading/INDEX.md (100%) rename paperforge/skills/literature-qa/{ => references}/chart-reading/ROC与PR曲线.md (100%) rename paperforge/skills/literature-qa/{ => references}/chart-reading/Western Blot条带图.md (100%) rename paperforge/skills/literature-qa/{ => references}/chart-reading/免疫荧光定量图.md (100%) rename paperforge/skills/literature-qa/{ => references}/chart-reading/折线图与时间序列.md (100%) rename paperforge/skills/literature-qa/{ => references}/chart-reading/散点图与气泡图.md (100%) rename paperforge/skills/literature-qa/{ => references}/chart-reading/显微照片与SEM图.md (100%) rename paperforge/skills/literature-qa/{ => references}/chart-reading/条形图与误差棒.md (100%) rename paperforge/skills/literature-qa/{ => references}/chart-reading/桑基图与弦图.md (100%) rename paperforge/skills/literature-qa/{ => references}/chart-reading/森林图与Meta分析.md (100%) rename paperforge/skills/literature-qa/{ => references}/chart-reading/火山图与曼哈顿图.md (100%) rename paperforge/skills/literature-qa/{ => references}/chart-reading/热图与聚类图.md (100%) rename paperforge/skills/literature-qa/{ => references}/chart-reading/生存曲线.md (100%) rename paperforge/skills/literature-qa/{ => references}/chart-reading/箱式图与小提琴图.md (100%) rename paperforge/skills/literature-qa/{ => references}/chart-reading/组织学半定量图.md (100%) rename paperforge/skills/literature-qa/{ => references}/chart-reading/网络图与通路图.md (100%) rename paperforge/skills/literature-qa/{ => references}/chart-reading/蛋白质结构图.md (100%) rename paperforge/skills/literature-qa/{ => references}/chart-reading/降维图(PCA-tSNE-UMAP).md (100%) rename paperforge/skills/literature-qa/{ => references}/chart-reading/雷达图与漏斗图.md (100%) create mode 100644 paperforge/skills/literature-qa/references/deep-reading.md rename paperforge/skills/literature-qa/{prompt_deep_subagent.md => references/deep-subagent.md} (100%) create mode 100644 paperforge/skills/literature-qa/references/paper-qa.md create mode 100644 paperforge/skills/literature-qa/references/paper-resolution.md create mode 100644 paperforge/skills/literature-qa/references/save-session.md delete mode 100644 paperforge/skills/literature-qa/scripts/pf-deep.md delete mode 100644 paperforge/skills/literature-qa/scripts/pf-end.md delete mode 100644 paperforge/skills/literature-qa/scripts/pf-ocr.md delete mode 100644 paperforge/skills/literature-qa/scripts/pf-paper.md delete mode 100644 paperforge/skills/literature-qa/scripts/pf-status.md delete mode 100644 paperforge/skills/literature-qa/scripts/pf-sync.md diff --git a/paperforge/plugin/main.js b/paperforge/plugin/main.js index 01891377..9f732d23 100644 --- a/paperforge/plugin/main.js +++ b/paperforge/plugin/main.js @@ -1460,11 +1460,20 @@ class PaperForgeStatusView extends ItemView { const truncated = extracted.length > 200 ? extracted.slice(0, 200) + '...' : extracted; excerptEl.setText(truncated); if (extracted.length > 200) { - const expandBtn = body.createEl('button', { cls: 'paperforge-paper-overview-expand', text: '展开' }); + const expandContainer = body.createEl('div', { cls: 'paperforge-expand-container' }); + const expandBtn = expandContainer.createEl('button', { cls: 'paperforge-expand-icon', title: '展开/收起' }); + + // Insert arrow SVG + expandBtn.innerHTML = ''; + let expanded = false; - expandBtn.addEventListener('click', () => { + + // Make the whole container clickable + expandContainer.addEventListener('click', () => { excerptEl.setText(expanded ? truncated : extracted); - expandBtn.setText(expanded ? '展开' : '收起'); + expandBtn.innerHTML = expanded + ? '' + : ''; expanded = !expanded; }); } @@ -1556,14 +1565,23 @@ class PaperForgeStatusView extends ItemView { : (qa.answer || ''); aEl.createEl('span', { cls: 'paperforge-discussion-a-text', text: '解答:' + shortAnswer }); if (qa.answer && qa.answer.length > 150) { - const expandLink = aEl.createEl('button', { cls: 'paperforge-discussion-expand', text: '展开' }); + const expandContainer = aEl.createEl('div', { cls: 'paperforge-expand-container' }); + const expandBtn = expandContainer.createEl('button', { cls: 'paperforge-expand-icon', title: '展开/收起' }); + + // Insert arrow SVG + expandBtn.innerHTML = ''; + let expanded = false; - expandLink.addEventListener('click', () => { + + // Make the whole container clickable + expandContainer.addEventListener('click', () => { const textSpan = aEl.querySelector('.paperforge-discussion-a-text'); if (textSpan) { textSpan.setText(expanded ? ('解答:' + shortAnswer) : ('解答:' + qa.answer)); } - expandLink.setText(expanded ? '展开' : '收起'); + expandBtn.innerHTML = expanded + ? '' + : ''; expanded = !expanded; }); } @@ -1692,9 +1710,19 @@ class PaperForgeStatusView extends ItemView { }); // Show "Run in [agent_platform]" label below the button (DASH-02) - const platform = this.app.plugins.plugins['paperforge']?.settings?.agent_platform || 'opencode'; + const platformKey = this.app.plugins.plugins['paperforge']?.settings?.agent_platform || 'opencode'; + const AGENTS = { + 'opencode': 'OpenCode', + 'claude': 'Claude Code', + 'cursor': 'Cursor', + 'github_copilot': 'GitHub Copilot', + 'windsurf': 'Windsurf', + 'codex': 'Codex', + 'cline': 'Cline' + }; + const platformName = AGENTS[platformKey] || platformKey; const labelEl = card.createEl('div', { cls: 'paperforge-agent-platform-label' }); - labelEl.setText(t('run_in_agent').replace('{0}', platform)); + labelEl.setText(t('run_in_agent').replace('{0}', platformName)); } else if (nextStep === 'ready') { const trigger = card.createEl('button', { cls: 'paperforge-next-step-trigger' }); trigger.createEl('span', { text: '✓ ' + info.label }); @@ -1806,7 +1834,7 @@ class PaperForgeStatusView extends ItemView { { cls: 'pending', value: ocrPending, label: 'Pending' }, { cls: 'active', value: ocrProcessing, label: 'Processing' }, { cls: 'done', value: ocrDone, label: 'Done' }, - { cls: 'failed', value: ocrFailed, label: 'Needs Attention' }, + { cls: 'failed', value: ocrFailed, label: 'Attention' }, ]; for (const l of ocrLabels) { const cnt = ocrCounts.createEl('div', { cls: 'paperforge-ocr-count' }); diff --git a/paperforge/plugin/styles.css b/paperforge/plugin/styles.css index dae7f145..f861ef3a 100644 --- a/paperforge/plugin/styles.css +++ b/paperforge/plugin/styles.css @@ -15,6 +15,8 @@ display: flex; flex-direction: column; gap: 12px; + container-type: inline-size; + container-name: pfpanel; --pf-surface: var(--background-secondary); --pf-surface-alt: var(--background-primary-alt); --pf-border: var(--background-modifier-border); @@ -348,7 +350,11 @@ } .paperforge-progress-seg.pending { - background: var(--text-faint); + background: #e2d9c8; +} + +.theme-dark .paperforge-progress-seg.pending { + background: #4a4135; } .paperforge-progress-seg.active { @@ -357,7 +363,11 @@ } .paperforge-progress-seg.done { - background: var(--color-green); + background: #3e5a47; +} + +.theme-dark .paperforge-progress-seg.done { + background: #4f735b; } .paperforge-progress-seg.failed { @@ -1839,33 +1849,49 @@ word-break: break-word; } -.paperforge-paper-overview-expand, -.paperforge-discussion-expand, +.paperforge-expand-container, .paperforge-discussion-viewall { color: var(--pf-paper-accent); } -.paperforge-paper-overview-expand { - display: inline-block; - margin-top: 6px; - padding: 3px 0; - background: none; - border: none; - border-bottom: 1px dashed currentColor; - font-size: 12px; - font-weight: 500; +.paperforge-expand-container { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + margin-top: 8px; + padding: 4px 0; cursor: pointer; - transition: color 0.15s, border-color 0.15s; + transition: background-color 0.2s ease, color 0.2s ease; + border-radius: var(--pf-radius); + background: linear-gradient(to bottom, transparent, color-mix(in srgb, var(--pf-surface-alt) 50%, transparent)); } -.paperforge-paper-overview-expand:hover, -.paperforge-discussion-expand:hover, +.paperforge-expand-icon { + background: none; + border: none; + padding: 0; + margin: 0; + color: inherit; + display: flex; + align-items: center; + justify-content: center; + pointer-events: none; /* Let container handle clicks */ + opacity: 0.6; + transition: opacity 0.2s ease; +} + +.paperforge-expand-container:hover, .paperforge-discussion-viewall:hover { color: var(--interactive-accent-hover); } -.paperforge-paper-overview-expand:hover { - border-color: currentColor; +.paperforge-expand-container:hover { + background: color-mix(in srgb, var(--pf-surface-alt) 80%, transparent); +} + +.paperforge-expand-container:hover .paperforge-expand-icon { + opacity: 1; } .paperforge-discussion-header { @@ -1913,18 +1939,7 @@ line-height: 1.68; } -.paperforge-discussion-expand { - display: inline; - padding: 0; - margin-left: 4px; - background: none; - border: none; - border-bottom: 1px dashed currentColor; - font-size: 12px; - font-weight: 500; - cursor: pointer; - transition: color 0.15s, border-color 0.15s; -} + .paperforge-discussion-viewall { display: block; @@ -2115,22 +2130,40 @@ } .paperforge-workflow-funnel { - display: flex; + display: grid; + grid-template-columns: 1fr auto 1fr auto 1fr auto 1fr; + gap: 8px 6px; align-items: center; - gap: 6px; - flex-wrap: wrap; - justify-content: flex-start; +} + +@container pfpanel (max-width: 380px) { + .paperforge-workflow-funnel { + grid-template-columns: 1fr auto 1fr; + } + .paperforge-workflow-funnel > div:nth-child(4) { + display: none; + } +} + +@container pfpanel (max-width: 220px) { + .paperforge-workflow-funnel { + grid-template-columns: 1fr; + } + .paperforge-workflow-arrow { + display: none; + } } .paperforge-workflow-stage { display: flex; flex-direction: column; align-items: center; - padding: 12px 14px; + padding: 12px 6px; background: color-mix(in srgb, var(--background-primary-alt) 88%, var(--background-primary) 12%); border: 1px solid color-mix(in srgb, var(--pf-collection-accent) 22%, var(--pf-border)); border-radius: 8px; - min-width: 72px; + min-width: 0; + width: 100%; } .paperforge-workflow-stage-value { @@ -2148,6 +2181,7 @@ letter-spacing: 0.3px; margin-top: 4px; text-align: center; + word-break: break-word; } .paperforge-workflow-arrow { diff --git a/paperforge/setup_wizard.py b/paperforge/setup_wizard.py index f6f58a8a..6bd28b01 100644 --- a/paperforge/setup_wizard.py +++ b/paperforge/setup_wizard.py @@ -1012,7 +1012,7 @@ def headless_setup( "OCR dir": (pf_path / "ocr").exists(), "paperforge.json": pf_json.exists(), "Obsidian plugin": (vault / ".obsidian" / "plugins" / "paperforge" / "main.js").exists(), - "AGENTS.md": True if not agents_src.exists() else agents_dst.exists(), + "AGENTS.md": (vault / "AGENTS.md").exists(), } failed = [k for k, v in checks.items() if not v] if failed: diff --git a/paperforge/skills/literature-qa/SKILL.md b/paperforge/skills/literature-qa/SKILL.md new file mode 100644 index 00000000..2ad91877 --- /dev/null +++ b/paperforge/skills/literature-qa/SKILL.md @@ -0,0 +1,80 @@ +--- +name: literature-qa +description: > + 学术文献精读与问答。MUST trigger when user types /pf-deep, /pf-paper, /pf-end, + or says "精读", "深度阅读", "读一下", "查一下这篇论文", "帮我看看这篇文章", + "这篇文章讲了什么", "保存讨论", "结束讨论", "做这篇文献的问答", + or any phrase about reading/analyzing papers in their Zotero library. + 支持 Zotero key, DOI, 标题, 作者/年份, 自然语言描述定位论文. +license: Apache-2.0 +compatibility: opencode +--- + +# Literature QA — 学术文献精读与问答 + +## 路由表 + +Agent 读到本文件后,首先根据用户意图路由到对应的 reference 文件: + +| 用户意图 | 典型输入 | 加载文件 | +|---------|---------|---------| +| 三阶段精读(指定论文) | `/pf-deep `, `pf-deep `, "精读 XXX", "深度阅读 XXX" | [references/deep-reading.md](references/deep-reading.md) | +| 三阶段精读(查看队列) | `/pf-deep`(无参数), "精读队列", "有哪些该读了" | [references/deep-reading.md](references/deep-reading.md) | +| 论文问答 | `/pf-paper `, `pf-paper `, "做这篇的问答", "帮我看看 XXX" | [references/paper-qa.md](references/paper-qa.md) | +| 保存讨论记录 | `/pf-end`, `pf-end`, "保存", "结束讨论", "完成讨论" | [references/save-session.md](references/save-session.md) | + +> **重要:** 加载 reference 文件后,严格按照该文件的流程执行。不要跳过任何步骤。 + +## 论文定位(所有路由共用,先执行) + +详见 [references/paper-resolution.md](references/paper-resolution.md)。 + +**核心原则:二路定位。Python 干确定的活,Agent 干理解的活。路径全在环境变量里,零硬编码。** + +### 第零步:加载环境变量(每条路由启动时跑一次) + +```pwsh +python -m paperforge.worker.paper_resolver env --vault . --shell pwsh | Invoke-Expression +``` + +此后所有路径用环境变量引用,不再拼接: + +| 变量 | 含义 | +| ------------------ | --------------------------- | +| `$env:PF_VAULT` | vault 根目录 | +| `$env:PF_INDEX_PATH` | formal-library.json 路径 | +| `$env:PF_LITERATURE_DIR` | formal notes 目录 | +| `$env:PF_OCR_DIR` | OCR 结果目录 | + +### 第一步:判断输入类型,选择路径 + +| 输入特征 | 执行命令 | +|---------|---------| +| 8位 key | `python -m paperforge.worker.paper_resolver resolve-key --vault "$env:PF_VAULT"` | +| DOI | `python -m paperforge.worker.paper_resolver resolve-doi "" --vault "$env:PF_VAULT"` | +| 标题片段 | `python -m paperforge.worker.paper_resolver search --title "..." --vault "$env:PF_VAULT"` | +| 作者+年份 | `python -m paperforge.worker.paper_resolver search --author "Smith" --year 2024 --vault "$env:PF_VAULT"` | +| 自然语言 | Agent 读 `$env:PF_INDEX_PATH` 指向的 formal-library.json | + +### 第二步:处理结果 + +- **Python 返回匹配:** 直接使用返回的 workspace(`formal_note_path` 等由 config 动态计算) +- **Python 搜不到:** Agent grep fallback:`rg -l "zotero_key:.*ABC" "$env:PF_LITERATURE_DIR/"` +- **自然语言搜不到:** 告知用户 "未找到,请确认或先运行 `paperforge sync`" +- **命中多篇:** 列出候选清单让用户选 + +## 文件结构 + +``` +literature-qa/ +├── SKILL.md ← 本文件(路由入口) +├── references/ +│ ├── deep-reading.md ← 精读工作流 +│ ├── paper-qa.md ← 问答工作流 +│ ├── save-session.md ← 保存记录工作流 +│ ├── paper-resolution.md ← 论文定位详细协议 +│ ├── deep-subagent.md ← 子代理提示词模板 +│ └── chart-reading/ ← 19 个图表类型阅读指南 +└── scripts/ + └── ld_deep.py ← 精读引擎(Python) +``` diff --git a/paperforge/skills/literature-qa/chart-reading/GSEA富集图.md b/paperforge/skills/literature-qa/references/chart-reading/GSEA富集图.md similarity index 100% rename from paperforge/skills/literature-qa/chart-reading/GSEA富集图.md rename to paperforge/skills/literature-qa/references/chart-reading/GSEA富集图.md diff --git a/paperforge/skills/literature-qa/chart-reading/INDEX.md b/paperforge/skills/literature-qa/references/chart-reading/INDEX.md similarity index 100% rename from paperforge/skills/literature-qa/chart-reading/INDEX.md rename to paperforge/skills/literature-qa/references/chart-reading/INDEX.md diff --git a/paperforge/skills/literature-qa/chart-reading/ROC与PR曲线.md b/paperforge/skills/literature-qa/references/chart-reading/ROC与PR曲线.md similarity index 100% rename from paperforge/skills/literature-qa/chart-reading/ROC与PR曲线.md rename to paperforge/skills/literature-qa/references/chart-reading/ROC与PR曲线.md diff --git a/paperforge/skills/literature-qa/chart-reading/Western Blot条带图.md b/paperforge/skills/literature-qa/references/chart-reading/Western Blot条带图.md similarity index 100% rename from paperforge/skills/literature-qa/chart-reading/Western Blot条带图.md rename to paperforge/skills/literature-qa/references/chart-reading/Western Blot条带图.md diff --git a/paperforge/skills/literature-qa/chart-reading/免疫荧光定量图.md b/paperforge/skills/literature-qa/references/chart-reading/免疫荧光定量图.md similarity index 100% rename from paperforge/skills/literature-qa/chart-reading/免疫荧光定量图.md rename to paperforge/skills/literature-qa/references/chart-reading/免疫荧光定量图.md diff --git a/paperforge/skills/literature-qa/chart-reading/折线图与时间序列.md b/paperforge/skills/literature-qa/references/chart-reading/折线图与时间序列.md similarity index 100% rename from paperforge/skills/literature-qa/chart-reading/折线图与时间序列.md rename to paperforge/skills/literature-qa/references/chart-reading/折线图与时间序列.md diff --git a/paperforge/skills/literature-qa/chart-reading/散点图与气泡图.md b/paperforge/skills/literature-qa/references/chart-reading/散点图与气泡图.md similarity index 100% rename from paperforge/skills/literature-qa/chart-reading/散点图与气泡图.md rename to paperforge/skills/literature-qa/references/chart-reading/散点图与气泡图.md diff --git a/paperforge/skills/literature-qa/chart-reading/显微照片与SEM图.md b/paperforge/skills/literature-qa/references/chart-reading/显微照片与SEM图.md similarity index 100% rename from paperforge/skills/literature-qa/chart-reading/显微照片与SEM图.md rename to paperforge/skills/literature-qa/references/chart-reading/显微照片与SEM图.md diff --git a/paperforge/skills/literature-qa/chart-reading/条形图与误差棒.md b/paperforge/skills/literature-qa/references/chart-reading/条形图与误差棒.md similarity index 100% rename from paperforge/skills/literature-qa/chart-reading/条形图与误差棒.md rename to paperforge/skills/literature-qa/references/chart-reading/条形图与误差棒.md diff --git a/paperforge/skills/literature-qa/chart-reading/桑基图与弦图.md b/paperforge/skills/literature-qa/references/chart-reading/桑基图与弦图.md similarity index 100% rename from paperforge/skills/literature-qa/chart-reading/桑基图与弦图.md rename to paperforge/skills/literature-qa/references/chart-reading/桑基图与弦图.md diff --git a/paperforge/skills/literature-qa/chart-reading/森林图与Meta分析.md b/paperforge/skills/literature-qa/references/chart-reading/森林图与Meta分析.md similarity index 100% rename from paperforge/skills/literature-qa/chart-reading/森林图与Meta分析.md rename to paperforge/skills/literature-qa/references/chart-reading/森林图与Meta分析.md diff --git a/paperforge/skills/literature-qa/chart-reading/火山图与曼哈顿图.md b/paperforge/skills/literature-qa/references/chart-reading/火山图与曼哈顿图.md similarity index 100% rename from paperforge/skills/literature-qa/chart-reading/火山图与曼哈顿图.md rename to paperforge/skills/literature-qa/references/chart-reading/火山图与曼哈顿图.md diff --git a/paperforge/skills/literature-qa/chart-reading/热图与聚类图.md b/paperforge/skills/literature-qa/references/chart-reading/热图与聚类图.md similarity index 100% rename from paperforge/skills/literature-qa/chart-reading/热图与聚类图.md rename to paperforge/skills/literature-qa/references/chart-reading/热图与聚类图.md diff --git a/paperforge/skills/literature-qa/chart-reading/生存曲线.md b/paperforge/skills/literature-qa/references/chart-reading/生存曲线.md similarity index 100% rename from paperforge/skills/literature-qa/chart-reading/生存曲线.md rename to paperforge/skills/literature-qa/references/chart-reading/生存曲线.md diff --git a/paperforge/skills/literature-qa/chart-reading/箱式图与小提琴图.md b/paperforge/skills/literature-qa/references/chart-reading/箱式图与小提琴图.md similarity index 100% rename from paperforge/skills/literature-qa/chart-reading/箱式图与小提琴图.md rename to paperforge/skills/literature-qa/references/chart-reading/箱式图与小提琴图.md diff --git a/paperforge/skills/literature-qa/chart-reading/组织学半定量图.md b/paperforge/skills/literature-qa/references/chart-reading/组织学半定量图.md similarity index 100% rename from paperforge/skills/literature-qa/chart-reading/组织学半定量图.md rename to paperforge/skills/literature-qa/references/chart-reading/组织学半定量图.md diff --git a/paperforge/skills/literature-qa/chart-reading/网络图与通路图.md b/paperforge/skills/literature-qa/references/chart-reading/网络图与通路图.md similarity index 100% rename from paperforge/skills/literature-qa/chart-reading/网络图与通路图.md rename to paperforge/skills/literature-qa/references/chart-reading/网络图与通路图.md diff --git a/paperforge/skills/literature-qa/chart-reading/蛋白质结构图.md b/paperforge/skills/literature-qa/references/chart-reading/蛋白质结构图.md similarity index 100% rename from paperforge/skills/literature-qa/chart-reading/蛋白质结构图.md rename to paperforge/skills/literature-qa/references/chart-reading/蛋白质结构图.md diff --git a/paperforge/skills/literature-qa/chart-reading/降维图(PCA-tSNE-UMAP).md b/paperforge/skills/literature-qa/references/chart-reading/降维图(PCA-tSNE-UMAP).md similarity index 100% rename from paperforge/skills/literature-qa/chart-reading/降维图(PCA-tSNE-UMAP).md rename to paperforge/skills/literature-qa/references/chart-reading/降维图(PCA-tSNE-UMAP).md diff --git a/paperforge/skills/literature-qa/chart-reading/雷达图与漏斗图.md b/paperforge/skills/literature-qa/references/chart-reading/雷达图与漏斗图.md similarity index 100% rename from paperforge/skills/literature-qa/chart-reading/雷达图与漏斗图.md rename to paperforge/skills/literature-qa/references/chart-reading/雷达图与漏斗图.md diff --git a/paperforge/skills/literature-qa/references/deep-reading.md b/paperforge/skills/literature-qa/references/deep-reading.md new file mode 100644 index 00000000..3563a5d7 --- /dev/null +++ b/paperforge/skills/literature-qa/references/deep-reading.md @@ -0,0 +1,171 @@ +# 三阶段精读 + +Keshav 三阶段组会式精读。触发后执行以下工作流。 + +> **路径说明:** 本文件中的 `scripts/ld_deep.py` 相对于本 skill 目录。Agent 运行时 skill 目录由平台注入(通常为 `/paperforge/skills/literature-qa/`)。如不确定,AI 应从 SKILL.md 所在目录推断。 + +--- + +## 前置条件检查 + +执行前确认: +- [ ] 已完成论文定位(参考 [paper-resolution.md](paper-resolution.md)),拿到 zotero_key 和 workspace +- [ ] `analyze: true`(在 formal note frontmatter 中,resolver 返回的 workspace 里可查到) +- [ ] `ocr_status: done`(在 resolver 返回的 workspace 里可查到) + +如果前置条件不满足,告知用户并停止。 + +--- + +## 执行流程 + +### Step 1: Prepare(机械操作,跑脚本) + +```bash +python scripts/ld_deep.py prepare --vault . --key +``` + +返回 JSON 解析: +- `status: "ok"` → 继续 +- `status: "error"` → 报告 `message` 给用户,停止 + +Prepare 做的事情(Agent 不需要关心细节): +- 检查 analyze 和 ocr_status +- 生成 figure-map.json 和 chart-type-map.json +- 在 formal note 中插入 `## 🔍 精读` 骨架 + +读 formal note 确认骨架已插入。 + +--- + +### Step 2: Pass 1 — 概览 + +只填 `### Pass 1: 概览` 区域。不碰 Pass 2/3。 + +**填写内容:** + +- **一句话总览**:论文类型 + 核心发现,一句话。 +- **5 Cs 快速评估**: + - **Category**(类型):RCT / 队列研究 / 病例对照 / 综述 / 基础研究 / ... + - **Context**(上下文):该领域当前共识,本文要解决什么问题 + - **Correctness**(合理性初判):初步直觉,逻辑是否有明显漏洞 + - **Contributions**(贡献):1-3 条 + - **Clarity**(清晰度):写作质量,图表可读性 +- **Figure 导读**(基于 fulltext.md 浏览各图 caption): + - 关键主图:列出并一句话概括每个主图要证明什么 + - 证据转折点:哪个 figure 是叙事的关键转折 + - 需要重点展开的 supplementary:如果有 + - 关键表格:列出 + +填完立即保存 formal note。 + +--- + +### Step 3: Pass 2 — 精读还原 + +填 `### Pass 2: 精读还原` 区域。**按 figure 顺序逐个处理。** + +#### 图表类型定位(两步) + +**Step A: Python 给建议(快速初筛)** +```bash +python scripts/ld_deep.py chart-type-scan --vault . --key +``` +输出每个 figure 的关键词命中结果。这只是建议,Agent 不要盲信。 + +**Step B: Agent 读 caption 做最终判断** + +对每个 figure: +1. 读该 figure 的 caption(来自 fulltext.md 或 figure-map.json) +2. 根据 caption 内容,对照 [chart-reading/INDEX.md](chart-reading/INDEX.md) 判断图表类型 +3. 如果 Python 建议和 Agent 判断不一致 → 以 Agent 判断为准 +4. 如果无法确定类型 → 跳过 chart guide,按通用 figure 结构分析 +5. 确定类型后,读对应的 chart-reading 指南(如 `chart-reading/条形图与误差棒.md`),按指南中的检查清单分析 + +#### 每张 Figure 的子标题(固定,不可少) + +按以下格式填入 formal note 中该 figure 的 callout block: + +``` +**图像定位与核心问题**:页码 + 要回答什么问题 +**方法与结果**:实验设计/数据来源/技术手段。核心数据、趋势、对比。 +**图表质量审查**:按 chart-reading 指南检查坐标轴、单位、误差棒、统计标注等。 +**作者解释**:作者在正文中对该图的解读 +**我的理解**:自己的理解(区分于作者解释) +**疑点/局限**:读图时发现的疑问,用 `> [!warning]` 突出 +``` + +#### 每张 Table 的子标题 + +``` +回答什么问题、关键字段/分组、主要结果、我的理解、疑点/局限 +``` + +#### 每张 figure 填完立即保存,再处理下一张。 + +#### 所有 figure/table 处理完后,填: + +**关键方法补课**:简要解释不熟悉的实验技术(1-2 项即可) + +**主要发现与新意**: +- 发现 1:...(来源:Figure X) +- 发现 2:...(来源:Figure Y / Table Z) + +保存。 + +--- + +### Step 4: Postprocess(跑校验脚本,修正问题) + +```bash +python scripts/ld_deep.py postprocess-pass2 --figures --format text +``` + +- 输出 `OK` → 继续 +- 输出错误 → 按错误提示修正(包含行号),修正后重新跑 +- 最多 3 轮修正。3 轮后仍失败 → 报告剩余错误给用户 + +--- + +### Step 5: Pass 3 — 深度理解 + +填 `### Pass 3: 深度理解` 区域。基于 Pass 1/2 已写的内容。 + +**填写内容:** + +- **假设挑战与隐藏缺陷**:隐含假设;如果放宽某个假设结论还成立吗;缺少哪些关键引用;实验/分析技术潜在问题 +- **哪些结论扎实,哪些仍存疑**: + - **较扎实**:... + - **仍存疑**:...(用 `> [!warning]`) +- **Discussion 与 Conclusion 怎么读**:作者真正完成了什么;哪些地方有拔高;哪些是推测 +- **对我的启发**:研究设计上;figure 组织上;方法组合上;未来工作想法 +- **遗留问题**:...(用 `> [!question]`) + +保存。 + +--- + +### Step 6: Final Validation + +```bash +python scripts/ld_deep.py validate-note --fulltext +``` + +- 输出 `OK` → 告知用户精读完成 +- 输出错误 → 修正缺失项,不报告成功直到通过 + +--- + +## Callout 格式规则 + +- `> [!important]`:每个 main finding +- `> [!warning]`:疑问、局限、证据边界、仍存疑条目 +- `> [!question]`:遗留问题 +- **间距:** 相邻 callout block 之间必须有空行,否则 Obsidian 会合并 + - 正确:`> [!important] A\n\n> [!important] B` + - 错误:`> [!important] A\n> [!important] B` + +## Supplementary 规则 + +- 默认不逐张展开 supplementary figure/table +- 仅在以下情况纳入:对主结论形成关键支撑、补足方法可信度、限制主文结论解释范围、作者在正文中明显依赖该补充材料 diff --git a/paperforge/skills/literature-qa/prompt_deep_subagent.md b/paperforge/skills/literature-qa/references/deep-subagent.md similarity index 100% rename from paperforge/skills/literature-qa/prompt_deep_subagent.md rename to paperforge/skills/literature-qa/references/deep-subagent.md diff --git a/paperforge/skills/literature-qa/references/paper-qa.md b/paperforge/skills/literature-qa/references/paper-qa.md new file mode 100644 index 00000000..626f23e0 --- /dev/null +++ b/paperforge/skills/literature-qa/references/paper-qa.md @@ -0,0 +1,61 @@ +# 论文问答 + +交互式论文 Q&A 工作台。不强制要求 OCR,但 OCR 完成后回答更准确。 + +--- + +## 前置条件 + +- [ ] 已完成论文定位(参考 [paper-resolution.md](paper-resolution.md)),拿到 zotero_key 和 workspace +- [ ] OCR 完成(推荐但非强制) + +--- + +## 执行流程 + +### Step 1: 加载论文 + +1. 确认 workspace 路径 +2. 读 `fulltext.md`(如果存在)作为主要回答依据 +3. 读 formal note frontmatter 获取元数据(标题、作者、期刊、年份) +4. 如果 fulltext.md 不存在,告知用户 "OCR 文本不可用,回答将基于元数据和公开信息" + +### Step 2: 显示论文信息 + +``` +已加载论文: [title] ([year], [journal]) +作者: [authors] +Zotero Key: [key] +领域: [domain] +OCR 状态: [done / 不可用] +结束对话时说 "保存" 即可保存讨论。 +请问有什么问题? +``` + +### Step 3: 进入 Q&A 模式 + +- 等待用户提问 +- 每次回答后等待下一个问题 +- 持续到用户说 "保存"、"结束"、"完成" 等关键词 + +--- + +## 回答原则 + +- **严格基于** fulltext.md 中的文本内容回答 +- 引用原文时标注来源页码/章节(如 "第 3 页,Methods 部分") +- 用中文(简体中文)回答 +- 论文中未提及的内容,明确说明 "论文中未提及该内容" +- 需要结合论文以外知识的问题,说明 "该问题需要结合论文以外的知识" + +--- + +## 切换模式 + +用户在当前对话中可以说 "精读这篇文章" 切换到 deep-reading 模式。此时加载 [deep-reading.md](deep-reading.md) 执行精读流程。 + +--- + +## 保存记录 + +用户说 "保存"、"结束"、"完成"、"保存讨论" 时,加载 [save-session.md](save-session.md) 执行保存。不要自动保存。 diff --git a/paperforge/skills/literature-qa/references/paper-resolution.md b/paperforge/skills/literature-qa/references/paper-resolution.md new file mode 100644 index 00000000..763425eb --- /dev/null +++ b/paperforge/skills/literature-qa/references/paper-resolution.md @@ -0,0 +1,173 @@ +# 论文定位协议 + +本文件定义如何将用户输入(key / DOI / 标题 / 作者年份 / 自然语言)解析为论文 workspace。所有子流程(deep-reading, paper-qa, save-session)共用此协议。 + +## 核心原则 + +1. **确定性输入走 Python。** key、DOI、标题片段、作者+年份 —— 这些是机器能精确处理的。 +2. **自然语言走 Agent。** "关于骨再生的那篇"、"去年那篇Nature" —— 需要 AI 理解语义。 +3. **Python 搜不到时 Agent 兜底。** 不是报错,是换个方式再试一次。 +4. **所有路径从环境变量获取。** 每条路由启动时先跑 `python -m paperforge.worker.paper_resolver env --vault . --shell pwsh | Invoke-Expression`。此后 `$env:PF_VAULT`、`$env:PF_LITERATURE_DIR`、`$env:PF_OCR_DIR`、`$env:PF_INDEX_PATH` 全程可用。不写死任何目录名。 + +--- + +## 前置:获取 vault 路径配置(每个路由启动时跑一次) + +```bash +python -m paperforge.worker.paper_resolver paths --vault . +``` + +返回示例(所有路径均由 `paperforge.json` 动态计算): +```json +{ + "ok": true, + "data": { + "vault_root": "/path/to/vault", + "index_path": "/PaperForge/indexes/formal-library.json", + "literature_dir": "/", + "ocr_dir": "/PaperForge/ocr" + } +} +``` + +**后续所有路径操作使用此输出中的值,不要自己拼接。** + +--- + +## 输入类型判断 + +### 类型 1: Zotero Key(8位字符) + +**识别规则:** 8位字母数字组合,如 `XGT9Z257`、`ABC12345` + +**执行命令:** +```bash +python -m paperforge.worker.paper_resolver resolve-key --vault . +``` + +**返回示例(路径由 paperforge.json 配置决定,不固定):** +```json +{ + "ok": true, + "data": { + "match": { + "key": "ABC12345", + "title": "TGF-beta in Bone Regeneration", + "domain": "骨科", + "formal_note_path": "...", + "ocr_path": "...", + "fulltext_path": "...", + "ocr_status": "done" + } + } +} +``` + +**返回 `"match": null` 时:** Agent fallback — 用 `paths` 命令获取的 `literature_dir` 路径 grep frontmatter: +```bash +rg -l "zotero_key:.*ABC12345" / +``` + +--- + +### 类型 2: DOI + +**识别规则:** 以 `10.` 开头的标准 DOI 格式,可能带 URL 前缀 + +**执行命令:** +```bash +python -m paperforge.worker.paper_resolver resolve-doi "10.1016/j.jse.2018.01.001" --vault . +``` + +**返回格式同类型1。** 返回的路径直接使用,不自己拼接。 + +--- + +### 类型 3: 标题片段 + +**识别规则:** 看起来像论文标题的文本(含学术关键词,非自然语言问句) + +**执行命令:** +```bash +python -m paperforge.worker.paper_resolver search --title "Predictive findings on MRI" --vault . +``` + +**返回示例:** +```json +{ + "ok": true, + "data": { + "matches": [{ "key": "...", "title": "...", "formal_note_path": "...", ... }], + "count": 3 + } +} +``` + +--- + +### 类型 4: 作者 + 年份 + +**识别规则:** 包含作者名(英文姓)和年份 + +**执行命令:** +```bash +python -m paperforge.worker.paper_resolver search --author "Smith" --year 2024 --vault . +``` + +--- + +### 类型 5: 自然语言 + +**识别规则:** 中文自然语言描述,如 "关于骨再生的那篇"、"去年Nature上那篇讲TGF的" + +**Agent 操作:** +1. 用 `paths` 命令获取的 `index_path` 读 `formal-library.json` +2. 理解用户意图中的关键信息:主题词、年份、期刊、领域 +3. 在 JSON 的 `title`、`domain`、`journal`、`abstract` 字段中搜索匹配 +4. 如果 formal-library.json 无结果,用 `paths` 命令获取的 `literature_dir` grep formal notes: + ```bash + rg -i -l "骨再生|bone regeneration" / --include "*.md" -g "!*.canvas" + ``` +5. 读匹配的 frontmatter 确认是目标论文 + +--- + +## 多篇匹配处理 + +当搜索返回多个匹配时,Agent 必须列出候选清单,让用户选择: + +``` +找到 3 篇匹配的论文: + +[1] ABC12345 — TGF-beta in Bone Regeneration (2024, 骨科, OCR: done) +[2] DEF67890 — Bone Healing After Fracture (2023, 骨科, OCR: pending) +[3] GHI11111 — Scaffold Design for Bone Repair (2024, 骨科, OCR: done) + +请输入编号选择,或 refine 搜索词。 +``` + +**格式要求:** +- 编号 + key + title + (year, domain, ocr_status) +- 不要只列标题或只列 key + +--- + +## Fallback 顺序 + +``` +输入 + │ + ├── 看起来像 key/DOI/标题/作者年份? + │ └── YES → Python paper_resolver + │ ├── 有结果 → 使用 + │ └── 无结果 → Agent grep fallback + │ ├── 有结果 → 使用 + │ └── 无结果 → 告知用户 + │ + └── 看起来像自然语言? + └── Agent 读 formal-library.json + ├── 有结果 → 列出/使用 + └── 无结果 → Agent grep fallback + ├── 有结果 → 使用 + └── 无结果 → 告知用户 +``` diff --git a/paperforge/skills/literature-qa/references/save-session.md b/paperforge/skills/literature-qa/references/save-session.md new file mode 100644 index 00000000..de023e21 --- /dev/null +++ b/paperforge/skills/literature-qa/references/save-session.md @@ -0,0 +1,55 @@ +# 保存讨论记录 + +将 paper-qa 会话中的 Q&A 记录持久化到论文工作区。 + +--- + +## 触发条件 + +- 用户显式说 "保存"、"保存记录"、"结束"、"完成讨论"、"save" +- 或显式输入 `/pf-end` +- 不要自动触发 + +--- + +## 执行 + +### Step 1: 收集 Q&A 对 + +汇总本次 paper-qa 会话中所有 Q&A,序列化为 JSON 数组: + +```json +[ + { + "question": "用户的问题", + "answer": "Agent 的回答", + "source": "user_question", + "timestamp": "2026-05-10T12:00:00+08:00" + } +] +``` + +`source` 为 `"user_question"`(用户提问)或 `"agent_analysis"`(Agent 主动分析)。 + +### Step 2: 调用 discussion 模块 + +```bash +python -m paperforge.worker.discussion record \ + --vault . \ + --agent pf-paper \ + --model "" \ + --qa-pairs '' +``` + +### Step 3: 确认结果 + +CLI 返回 `{"status": "ok", ...}` → 告知用户记录已保存。 + +返回 `{"status": "error"}` → 记录错误,重试一次。仍失败则告知用户。 + +--- + +## 注意事项 + +- 仅 paper-qa 会话需要记录。deep-reading 的内容直接写入 formal note,不需要通过本文件。 +- 如果无法从 formal-library.json 找到论文 domain/title,记录失败不应影响用户使用。 diff --git a/paperforge/skills/literature-qa/scripts/pf-deep.md b/paperforge/skills/literature-qa/scripts/pf-deep.md deleted file mode 100644 index 3d2af01c..00000000 --- a/paperforge/skills/literature-qa/scripts/pf-deep.md +++ /dev/null @@ -1,237 +0,0 @@ ---- -name: pf-deep -description: Complete three-pass deep reading of an academic paper (Keshav method). Requires OCR fulltext. Searches by Zotero key, title, DOI, or PMID. -allowed-tools: [Read, Bash, Edit] ---- - -# pf-deep - -## Purpose - -基于单篇论文的组会式精读入口。 - -1. 解析 `pf-deep ` 中的查询词 -2. 支持 Zotero key、标题片段、DOI、PMID、关键词 -3. 优先搜索本地 Zotero 并锁定单篇论文 -4. 绑定该论文对应的: - - `/PaperForge/ocr//fulltext.md` - - `/PaperForge/ocr//meta.json` - - `//.../KEY - Title.md` -5. 在正式文献卡片中检查或创建 `## 精读` -6. 以"研究思路 + figure-by-figure"方式一次性完成精读写回 - -## CLI Equivalent - -```bash -# 准备阶段(间接) -python .opencode/skills/pf-deep/scripts/ld_deep.py prepare --vault "" --key -# 返回 JSON:{status, formal_note, fulltext_md, figures, tables} -``` - -> `pf-deep` 是 **Agent 层命令**,通过 Python 代码自动检测论文状态,无需先行 CLI 准备。 - -## Detection(自动检测,无需手动 sync) - -启动时,Agent 执行以下 Python 检测命令,代码会自动判断是否需要精读: - -```bash -python .opencode/skills/pf-deep/scripts/ld_deep.py prepare --vault "" --key -``` - -返回 JSON: -- `status: "ok"` → 就绪,可以开始精读 -- `status: "error"` → 被阻塞(`message` 说明原因:analyze=false / OCR 未完成 / 未找到论文) - -Agent 根据返回的 `status` 决定是否进入精读流程,不自行读取 frontmatter。 - -**队列模式**(无参数时自动检测):Agent 运行: -```bash -python .opencode/skills/pf-deep/scripts/ld_deep.py queue --vault "" -``` -代码自动扫描 canonical index 中 `analyze=true` 且 `deep_reading_status=pending` 的论文,按 OCR 状态分组。 - -## Arguments - -| 参数 | 必需 | 说明 | -|------|------|------| -| `` | 是(queue 模式除外) | Zotero key、标题片段、DOI、PMID 或关键词 | -| `queue` | 否 | 启动批量精读队列模式 | - -### 参数说明 - -1. 如果输入看起来像 8 位 Zotero key,则直接按 key 解析。 -2. 否则先在本地 Zotero 中搜索标题/摘要。 -3. 若命中唯一结果或明显最佳结果,则直接载入。 -4. 若存在多个合理候选,则先列候选清单再让用户选。 -5. 不要强迫用户先知道 Zotero key。 - -## Example - -### 单篇精读(已知 key) - -```bash -pf-deep XGT9Z257 -pf-deep Predictive findings on magnetic resonance imaging -pf-deep 10.1016/j.jse.2018.01.001 -``` - -### 无参数:自动检测队列 - -```bash -pf-deep -``` - -当不提供具体 key/标题时,agent 自动检测精读队列: - -1. 运行 `python .opencode/skills/pf-deep/scripts/ld_deep.py queue --vault ""` 扫描队列 -2. 解析输出的 JSON,按 OCR 状态分组展示 -3. 由用户选择篇目 - -无需先跑 `paperforge sync`。 - -## Output - -Agent 在正式笔记中创建或更新 `## 精读` 区域,包含: - -- **Pass 1: 概览** — 一句话总览、5 Cs 快速评估、Figure 导读 -- **Pass 2: 精读还原** — Figure-by-Figure 解析、Table-by-Table 解析、关键方法补课、主要发现与新意 -- **Pass 3: 深度理解** — 假设挑战与隐藏缺陷、结论扎实性评估、Discussion 解读、个人启发、遗留问题 - -## Error Handling - -### OCR 未完成 -- **表现**:Agent 提示 `ocr_status` 不是 `done` -- **解决**:先运行 `paperforge ocr`,确认 `meta.json` 中 `ocr_status` 变为 `done` - -### 内容已存在(覆盖确认) -- **表现**:正式笔记中已存在 `## 精读` 区域且包含非占位符的实质内容 -- **处理**:Agent **必须**询问用户:追加 / 覆盖 / 跳过 - -### 未找到论文 -- **表现**:Zotero key 无效或搜索无结果 -- **解决**:确认 key 正确,或尝试用标题片段搜索 - -## 精读结构参考 - -### 执行原则 - -- `pf-deep` 对用户来说是一次触发直接完成。 -- 内部逻辑分两步: - 1. 先生成 `## 精读` 骨架和 figure 标题位 - 2. 再补全所有空段 -- 后续再次运行时(用户选择追加): - - 只补空段,不覆盖已有内容 - -### 精读定位 - -这不是综述提取,也不是信息摘录。目标是模拟高水平博士/博士后组会讲解单篇论文的学习型精读。 - -主线必须是: - -1. 文章整体研究思路 -2. 主文 figure 逐张解析 -3. 关键方法补课 -4. 主要发现、新意、疑点与启发 - -### Supplementary 规则 - -- 默认不逐张展开 supplementary figure/table。 -- 仅在以下情况下纳入: - - 对主结论形成关键支撑 - - 补足方法可信度 - - 限制主文结论的解释范围 - - 作者在正文中明显依赖该补充材料 - -### 标准骨架 - -```md -## 精读 - -**证据边界**:区分三层信息:`论文结果`、`作者解释`、`我的理解/推断`。 - -### Pass 1: 概览 - -**一句话总览** -(待补充) - -**5 Cs 快速评估** -- **Category**(类型): -- **Context**(上下文): -- **Correctness**(合理性初判): -- **Contributions**(贡献): -- **Clarity**(清晰度): - -**Figure 导读** -- 关键主图: -- 证据转折点: -- 需要重点展开的 supplementary: -- 关键表格: - -### Pass 2: 精读还原 - -#### Figure-by-Figure 解析 -(每张 figure 下方按以下顺序填写) -- **图像定位与核心问题**:页码 + 要回答什么问题 -- **方法与结果**:方法 + 结果 -- **作者解释**:作者对该图的解读 -- **我的理解**:自己的理解(区分于作者解释) -- **在全文中的作用**:该图在整体故事线中的位置 -- **疑点 / 局限**:读图时发现的疑问 - -#### Table-by-Table 解析 -(如有重要表格,按同样结构展开) - -#### 关键方法补课 -- 方法 1: -- 方法 2: - -#### 主要发现与新意 -**主要发现** -- 发现 1: -- 发现 2: - -### Pass 3: 深度理解 - -#### 假设挑战与隐藏缺陷 -- 隐含假设: -- 如果放宽某个假设,结论还成立吗? -- 缺少哪些关键引用? -- 实验/分析技术的潜在问题: - -#### 哪些结论扎实,哪些仍存疑 -**较扎实** -- - -**仍存疑** -- - -#### Discussion 与 Conclusion 怎么读 -- 作者真正完成了什么: -- 哪些地方有拔高: -- 哪些地方是推测: - -#### 对我的启发 -- 研究设计上: -- figure 组织上: -- 方法组合上: -- 未来工作想法: - -#### 遗留问题 -**遗留问题** -- -``` - -### Figure 节要求 - -每个 figure 小节按以下顺序填写: - -- **图像定位与核心问题**:页码 + 要回答什么问题 -- **方法与结果**:方法 + 结果 -- **作者解释**:作者对该图的解读 -- **我的理解**:自己的理解(区分于作者解释) -- **在全文中的作用**:该图在整体故事线中的位置 -- **疑点 / 局限**:读图时发现的疑问(可酌情用 `> [!warning]` 突出) - -## See Also - -- [pf-paper](pf-paper.md) — 快速摘要与问答 diff --git a/paperforge/skills/literature-qa/scripts/pf-end.md b/paperforge/skills/literature-qa/scripts/pf-end.md deleted file mode 100644 index 0f3a0f04..00000000 --- a/paperforge/skills/literature-qa/scripts/pf-end.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -name: pf-end -description: Save the paper Q&A discussion record. Triggered when user says "保存" "结束" "save" or types $pf-end. Summarizes all Q&A pairs and writes them to the paper's ai/discussion directory via the discussion module. -allowed-tools: [Read, Bash] ---- - -# pf-end - -## Purpose - -结束当前论文对话并保存讨论记录。需要用户**显式要求**时才执行,不自动触发。 - -1. 汇总本次对话中所有 Q&A 对 -2. 通过 `discussion.record_session()` 写入论文工作区的 `ai/` 目录 -3. 告知用户记录已保存 - -## Trigger - -用户说以下任一关键词时执行(全平台通用): -- `保存` / `记录` / `保存记录` -- `结束` / `完成` -- `save discussion` / `save` / `done` - -也可以显式指定 key: -- `{prefix}pf-end `(OpenCode) -- `保存 `(全平台) - -如果未指定 key,则使用当前已加载的论文 key。 - -## Save Format - -将会话期间的所有 Q&A 序列化为 JSON 数组: - -```json -{ - "question": "用户的问题", - "answer": "Agent 的回答", - "source": "user_question", - "timestamp": "2026-05-06T12:00:00+08:00" -} -``` - -`source` 为 `"user_question"`(用户提问)或 `"agent_analysis"`(Agent 主动分析)。 - -## Command - -```bash -python -m paperforge.worker.discussion record \ - --vault "" \ - --agent pf-paper \ - --model "" \ - --qa-pairs '' -``` - -## Verification - -CLI 返回: -```json -{"status": "ok", "json_path": "Literature/{domain}/{key} - {title}/ai/discussion.json", "md_path": "..."} -``` - -如果 `status` 为 `"error"`,记录错误信息并重试,不要跳过。 - -## See Also - -- [pf-paper](pf-paper.md) — 论文 Q&A 工作台 -- [pf-deep](pf-deep.md) — 完整三阶段精读 diff --git a/paperforge/skills/literature-qa/scripts/pf-ocr.md b/paperforge/skills/literature-qa/scripts/pf-ocr.md deleted file mode 100644 index 000611a3..00000000 --- a/paperforge/skills/literature-qa/scripts/pf-ocr.md +++ /dev/null @@ -1,102 +0,0 @@ ---- -name: pf-ocr -description: Process the PDF OCR queue for formal notes marked do_ocr: true. Uploads PDFs to PaddleOCR API and extracts fulltext with figures. -allowed-tools: [Bash] ---- - -# /pf-ocr - -## Purpose - -处理正式笔记 frontmatter 中 `do_ocr: true` 的 PDF OCR 队列。 - -`paperforge ocr` 会自动读取 `paperforge.json` 定位 ocr 目录和 worker 脚本,运行 OCR 并自动诊断结果。 - -## CLI Equivalent - -```bash -paperforge ocr -``` - -## Prerequisites - -- [ ] formal note frontmatter 中 `do_ocr: true` 已设置 -- [ ] PDF 附件存在(`has_pdf: true`) -- [ ] PaddleOCR API Key 已配置(`.env` 中 `PADDLEOCR_API_TOKEN`) -- [ ] 网络连接正常(可访问 PaddleOCR 服务) - -## Arguments - -| 参数 | 必需 | 说明 | -|------|------|------| -| `--diagnose` | 否 | 仅诊断配置,不上传 PDF | -| `--key ` | 否 | 仅处理指定 Zotero key 的文献 | -| `--vault ` | 否 | 指定 Vault 根目录(默认当前目录) | -| `--live` | 否 | 与 `--diagnose` 联用,执行实时 PDF 测试 | - -### 诊断等级 - -| 等级 | 检查项 | 失败含义 | -|------|--------|---------| -| L1 | API token 存在性 | `PADDLEOCR_API_TOKEN` 缺失或为空 | -| L2 | URL 可达性 | 无法连接 PaddleOCR 服务 | -| L3 | API 格式验证 | 服务可达但响应格式异常 | -| L4 | 实时 PDF 往返 | 完整提交和结果获取失败 | - -> Exit code `0` = 所有检查通过。Exit code `1` = 至少一项检查失败。 - -## Example - -```bash -# 处理所有标记 do_ocr: true 的文献 -paperforge ocr - -# 仅处理指定文献 -paperforge ocr --key ABCDEFG - -# 诊断模式(不实际运行) -paperforge ocr --diagnose - -# 完整诊断(含实时测试) -paperforge ocr --diagnose --live - -# 指定 Vault 目录 -paperforge ocr --vault /path/to/vault -``` - -## Output - -OCR 完成后,每个文献生成以下文件: - -``` -/PaperForge/ocr// -├── fulltext.md # 提取的全文(含 分页标记) -├── images/ # 自动切割的图表图片 -├── meta.json # OCR 元数据(含 ocr_status) -└── figure-map.json # 图表索引(自动生成) -``` - -`meta.json` 中的 `ocr_status` 字段: -- `pending` — 等待处理 -- `processing` — 正在处理 -- `done` — 完成 -- `failed` — 失败 - -## Error Handling - -### API Token 缺失(L1 失败) -- `PADDLEOCR_API_TOKEN` 未设置 → 在 `.env` 文件中添加 - -### 服务不可达(L2 失败) -- 无法连接 PaddleOCR → 检查网络连接 - -### PDF 上传失败(L4 失败) -- PDF 提交后未返回结果 → 检查 PDF 文件是否损坏 - -### OCR 状态卡住 -- `ocr_status` 长期显示 `processing` → 重新设置 `do_ocr: true` 后再次运行 - -## See Also - -- [pf-sync](pf-sync.md) — 文献同步(生成正式笔记) -- [pf-deep](pf-deep.md) — 深度精读(依赖 OCR 结果) diff --git a/paperforge/skills/literature-qa/scripts/pf-paper.md b/paperforge/skills/literature-qa/scripts/pf-paper.md deleted file mode 100644 index aa3e032b..00000000 --- a/paperforge/skills/literature-qa/scripts/pf-paper.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -name: pf-paper -description: Quick paper Q&A workbench. Load a paper by Zotero key, title, DOI, or PMID and answer questions. Does not require OCR. -allowed-tools: [Read, Bash] ---- - -# pf-paper - -## Purpose - -基于 Zotero OCR 文本的单篇论文工作台入口。 - -1. 解析 `pf-paper ` 中的查询词 -2. 支持 Zotero key、标题片段、DOI、PMID、关键词 -3. 优先搜索本地 Zotero,解析到单篇目标论文 -4. 根据 Vault 根目录的 `paperforge.json` 加载 `/PaperForge/ocr//fulltext.md` 作为主文本 -5. 读取 `meta.json` 显示论文标题、作者、期刊、年份 -6. 进入 Q&A 模式,用中文回答用户关于该论文的问题 -7. 在当前论文上下文中,用户可再说"精读这篇文章"切换到 deep 层 - -## CLI Equivalent - -```bash -# 准备阶段(间接) -paperforge sync # 生成正式笔记 -``` - -> `pf-paper` 是 **Agent 层命令**,无直接 CLI 等效命令。 - -## Prerequisites - -- [ ] 正式笔记已生成(`paperforge sync` 生成) -- [ ] `fulltext.md` 存在(推荐,用于基于原文回答;如不存在则基于元数据回答) -- [ ] discussion.py 模块可用(`python -m paperforge.worker.discussion --help` 可执行) - -> **注意**:与 `/pf-deep` 不同,`pf-paper` **不强制要求** OCR 完成。没有 OCR 时基于论文元数据和公开信息回答。 - -## Arguments - -| 参数 | 必需 | 说明 | -|------|------|------| -| `` | 是 | Zotero key、标题片段、DOI、PMID 或关键词 | -| ` ...` | 否 | 可同时加载多篇论文 | - -### 解析规则 - -1. 如果输入看起来像 8 位 Zotero key,则直接按 key 解析。 -2. 否则先在本地 Zotero 中搜索标题/摘要。 -3. 若命中唯一结果或明显最佳结果,则直接载入。 -4. 若存在多个合理候选,则先列候选清单再让用户选。 -5. 不要强迫用户先知道 Zotero key。 - -## Example - -```bash -pf-paper XGT9Z257 -pf-paper Predictive findings on magnetic resonance imaging -pf-paper 10.1016/j.jse.2018.01.001 -pf-paper XGT9Z257 PQR8KLM -``` - -## Output - -加载成功后显示: - -``` -已加载论文: [title] ([year], [journal]) -Zotero Key: [key] -结束对话时说"保存记录"即可保存讨论。 -请问有什么问题? -``` - -### 回答原则 - -- **严格基于** `fulltext.md` 中的文本内容回答 -- 引用原文时标注来源页码/章节 -- 用中文(简体中文)回答 -- 论文中未提及的内容,明确说明"论文中未提及该内容" -- 需要结合论文以外知识的问题,说明"该问题需要结合论文以外的知识回答" - -## Error Handling - -### 论文未找到 -- **表现**:Zotero key 无效或搜索无结果 -- **解决**:确认 key 正确,或尝试用标题片段搜索 - -### 多个候选结果 -- **表现**:搜索返回多个匹配的论文 -- **处理**:Agent 列出候选清单,让用户选择目标论文 - -### OCR 文件缺失 -- **表现**:`fulltext.md` 不存在 -- **处理**:Agent 基于元数据和公开信息回答,并告知用户"OCR 文本不可用,回答基于元数据" - -## Saving Discussion Record - -当用户说"保存"、"结束"、"完成"等关键词时(或显式输入 `{prefix}pf-end `),必须执行记录保存。具体步骤见 [pf-end](pf-end.md)。 - -### 注意事项 - -- 仅 `/pf-paper` Q&A 需要记录。`/pf-deep` 不记录(精读内容已写入正式笔记)。 -- 如果无法从 formal-library.json 找到论文 domain/title,记录失败不应影响用户使用。 - -## See Also - -- [pf-end](pf-end.md) — 结束对话并保存记录 -- [pf-deep](pf-deep.md) — 完整三阶段精读 diff --git a/paperforge/skills/literature-qa/scripts/pf-status.md b/paperforge/skills/literature-qa/scripts/pf-status.md deleted file mode 100644 index 67d22f94..00000000 --- a/paperforge/skills/literature-qa/scripts/pf-status.md +++ /dev/null @@ -1,94 +0,0 @@ ---- -name: pf-status -description: Check PaperForge installation and runtime status. Verifies configuration, paths, and data integrity. -allowed-tools: [Bash] ---- - -# /pf-status - -## Purpose - -查看 PaperForge 当前安装与运行状态。 - -`paperforge status` 会检查: - -- 安装完整性(Python 包、依赖) -- 配置文件(`paperforge.json`、`.env`) -- 路径连通性(exports、ocr、literature 目录) -- Zotero 数据目录链接状态 -- Better BibTeX 导出文件状态 - -## CLI Equivalent - -```bash -paperforge status -``` - -## Prerequisites - -无特殊前置条件。此命令用于诊断安装问题,即使在配置不完整时也会尽量输出可用信息。 - -## Arguments - -| 参数 | 必需 | 说明 | -|------|------|------| -| `--vault ` | 否 | 指定 Vault 根目录(默认当前目录) | - -## Example - -```bash -# 检查当前目录的 Vault 状态 -paperforge status - -# 检查指定 Vault 的状态 -paperforge status --vault /path/to/vault -``` - -## Output - -典型输出示例: - -``` -PaperForge Lite v1.2 -==================== - -[安装检查] -✓ Python 包: paperforge v1.2.0 -✓ 依赖: requests, pymupdf, pillow - -[配置检查] -✓ paperforge.json: 存在且有效 -✓ .env: 存在 -✓ PADDLEOCR_API_TOKEN: 已设置 - -[路径检查] -✓ exports: /PaperForge/exports/ -✓ ocr: /PaperForge/ocr/ -✓ literature: // -✓ Zotero: /Zotero/ - -[数据检查] -✓ library.json: 存在,包含 150 条文献 -✓ 正式笔记: 150 篇 - -状态: 一切正常 ✅ -``` - -## Error Handling - -### 配置缺失 -- `✗ paperforge.json: 未找到` → 运行 `paperforge doctor` 或重新执行安装 - -### 路径错误 -- `✗ Zotero: 目录不存在或不是有效链接` → 创建 junction/symlink 到 Zotero 数据目录 - -### 依赖缺失 -- `✗ 依赖: requests 未安装` → `pip install requests pymupdf pillow` - -### API Key 未设置 -- `✗ PADDLEOCR_API_TOKEN: 未设置` → 在 `.env` 文件中添加 API token - -## See Also - -- [pf-sync](pf-sync.md) — 文献同步 -- [pf-ocr](pf-ocr.md) — OCR 提取 diff --git a/paperforge/skills/literature-qa/scripts/pf-sync.md b/paperforge/skills/literature-qa/scripts/pf-sync.md deleted file mode 100644 index 02dedf87..00000000 --- a/paperforge/skills/literature-qa/scripts/pf-sync.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -name: pf-sync -description: Sync Zotero Better BibTeX JSON export and generate/update formal literature notes. -allowed-tools: [Bash] ---- - -# /pf-sync - -## Purpose - -同步 Zotero Better BibTeX JSON 导出并生成/更新正式文献笔记。 - -`paperforge sync` 读取 Zotero JSON 中的新条目,直接生成正式文献笔记。 - -自动读取 `paperforge.json` 定位 exports 目录和 literature 目录。 - -## CLI Equivalent - -```bash -paperforge sync -``` - -## Prerequisites - -- [ ] Zotero 已安装且 Better BibTeX 插件已启用 -- [ ] Better BibTeX 已配置自动导出 JSON -- [ ] JSON 导出文件存在(`/PaperForge/exports/library.json`) -- [ ] `paperforge.json` 配置正确(Vault 根目录下) - -## Arguments - -| 参数 | 必需 | 说明 | -|------|------|------| -| `--dry-run` | 否 | 预览变更,不实际写入文件 | -| `--domain ` | 否 | 仅同步指定领域(如 `骨科`) | -| `--selection` | 否 | (已废弃)仅保留以兼容旧版 | -| `--index` | 否 | (已废弃)仅保留以兼容旧版 | -| `--vault ` | 否 | 指定 Vault 根目录(默认当前目录) | - -### 选项 - -```bash -paperforge sync --dry-run # 预览同步结果 -paperforge sync --domain 骨科 # 按领域过滤同步 -``` - -## Example - -```bash -# 同步 Zotero 并生成正式笔记 -paperforge sync - -# 预览模式 -paperforge sync --dry-run - -# 仅同步特定领域 -paperforge sync --domain 骨科 - -# 指定 Vault 目录 -paperforge sync --vault /path/to/vault -``` - -## Output - -``` -[INFO] Found 5 new items -[INFO] Created 骨科/XXXXXXX.md -[INFO] Generated 5 formal notes -[INFO] Output: //骨科/XXXXXXX - Title.md -``` - -生成文件:`/// - .md` - -## Error Handling - -### JSON 文件不存在 -- `[ERROR] library.json not found` → 检查 Better BibTeX 导出路径 - -### 空 JSON 导出 -- `[INFO] Found 0 new items` → 确认 Zotero 中有带 citation key 的文献,Better BibTeX 已启用"Keep updated" - -## See Also - -- [pf-ocr](pf-ocr.md) — OCR 提取(下一步操作) -- [pf-status](pf-status.md) — 检查系统状态 From c6c572329616d089cd2d50fe304f2117a21e1537 Mon Sep 17 00:00:00 2001 From: Research Assistant <research@example.com> Date: Sun, 10 May 2026 20:43:32 +0800 Subject: [PATCH 28/37] style: tighten collection header spacing, fix u00B7 encoding, add stage max-width - Fix CSS unicode escape: \\u00B7 -> literal middle dot character - Remove 'papers' count line in collection header - Tighten collection-header padding from 64px to 14px top only - Reset collection-title margin to 0 - Add max-width: 160px + justify-self: center to workflow stages - workflow-stage-label font-size: 11px -> 12px --- paperforge/plugin/main.js | 1 - paperforge/plugin/styles.css | 25 ++++++++++++++++--------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/paperforge/plugin/main.js b/paperforge/plugin/main.js index 9f732d23..d1446470 100644 --- a/paperforge/plugin/main.js +++ b/paperforge/plugin/main.js @@ -1782,7 +1782,6 @@ class PaperForgeStatusView extends ItemView { // ── Header ── const header = view.createEl('div', { cls: 'paperforge-collection-header' }); header.createEl('div', { cls: 'paperforge-collection-title', text: domain }); - header.createEl('div', { cls: 'paperforge-collection-count', text: totalPapers + ' papers' }); // ── Workflow Overview (funnel) ── const wfSection = view.createEl('div', { cls: 'paperforge-workflow-overview' }); diff --git a/paperforge/plugin/styles.css b/paperforge/plugin/styles.css index f861ef3a..1ba83d44 100644 --- a/paperforge/plugin/styles.css +++ b/paperforge/plugin/styles.css @@ -1302,7 +1302,7 @@ } .paperforge-paper-year::before { - content: " \u00B7 "; + content: " · "; color: var(--text-faint); } @@ -2085,21 +2085,18 @@ /* Collection mode */ .paperforge-collection-header { - margin-bottom: 10px; + padding: 14px 0 0 0; } .paperforge-collection-title { - font-size: 16px; + font-size: 28px; font-weight: 600; color: var(--text-normal); line-height: 1.3; + margin: 0; } -.paperforge-collection-count { - font-size: 13px; - color: var(--text-muted); - margin-top: 2px; -} + .paperforge-global-view { display: flex; @@ -2143,6 +2140,10 @@ .paperforge-workflow-funnel > div:nth-child(4) { display: none; } + .paperforge-workflow-stage { + max-width: none; + justify-self: stretch; + } } @container pfpanel (max-width: 220px) { @@ -2152,6 +2153,10 @@ .paperforge-workflow-arrow { display: none; } + .paperforge-workflow-stage { + max-width: none; + justify-self: stretch; + } } .paperforge-workflow-stage { @@ -2164,6 +2169,8 @@ border-radius: 8px; min-width: 0; width: 100%; + max-width: 160px; + justify-self: center; } .paperforge-workflow-stage-value { @@ -2175,7 +2182,7 @@ } .paperforge-workflow-stage-label { - font-size: 11px; + font-size: 12px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.3px; From 5c6ccaad344bcb019f7cdf294aab487b262c2065 Mon Sep 17 00:00:00 2001 From: Research Assistant <research@example.com> Date: Sun, 10 May 2026 20:16:33 +0800 Subject: [PATCH 29/37] =?UTF-8?q?feat:=20dashboard=20ui=20refine=20?= =?UTF-8?q?=E2=80=94=20arrow=20icons,=20maillard=20colors,=20responsive=20?= =?UTF-8?q?grid,=20setup=20bugfix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dashboard UI: - Replace text expand/collapse with SVG arrow + clickable gradient container - Map agent_platform key to display name (OpenCode, not opencode) - Maillard/Morandi palette for OCR progress bars (pending brown, done deep green) - Shorten 'Needs Attention' to 'Attention' to prevent label wrap - Responsive workflow overview: CSS Grid + container queries (narrow: 2x2, wide: 4x1 with max-width 160px stop) Setup Wizard: - Fix Phase 7 verification crash (undefined agents_src/agents_dst variables) Skill restructure: - Move chart-reading references under references/ directory - Remove old pf-*.md script files (deployed via skill_deploy service) - Add new reference files (deep-reading, paper-qa, paper-resolution, save-session) --- paperforge/plugin/main.js | 46 +- paperforge/plugin/styles.css | 1464 ++++++----------- paperforge/setup_wizard.py | 2 +- paperforge/skills/literature-qa/SKILL.md | 80 + .../chart-reading/GSEA富集图.md | 0 .../{ => references}/chart-reading/INDEX.md | 0 .../chart-reading/ROC与PR曲线.md | 0 .../chart-reading/Western Blot条带图.md | 0 .../chart-reading/免疫荧光定量图.md | 0 .../chart-reading/折线图与时间序列.md | 0 .../chart-reading/散点图与气泡图.md | 0 .../chart-reading/显微照片与SEM图.md | 0 .../chart-reading/条形图与误差棒.md | 0 .../chart-reading/桑基图与弦图.md | 0 .../chart-reading/森林图与Meta分析.md | 0 .../chart-reading/火山图与曼哈顿图.md | 0 .../chart-reading/热图与聚类图.md | 0 .../chart-reading/生存曲线.md | 0 .../chart-reading/箱式图与小提琴图.md | 0 .../chart-reading/组织学半定量图.md | 0 .../chart-reading/网络图与通路图.md | 0 .../chart-reading/蛋白质结构图.md | 0 .../chart-reading/降维图(PCA-tSNE-UMAP).md | 0 .../chart-reading/雷达图与漏斗图.md | 0 .../literature-qa/references/deep-reading.md | 171 ++ .../deep-subagent.md} | 0 .../literature-qa/references/paper-qa.md | 61 + .../references/paper-resolution.md | 173 ++ .../literature-qa/references/save-session.md | 55 + .../skills/literature-qa/scripts/pf-deep.md | 237 --- .../skills/literature-qa/scripts/pf-end.md | 67 - .../skills/literature-qa/scripts/pf-ocr.md | 102 -- .../skills/literature-qa/scripts/pf-paper.md | 107 -- .../skills/literature-qa/scripts/pf-status.md | 94 -- .../skills/literature-qa/scripts/pf-sync.md | 85 - 35 files changed, 1041 insertions(+), 1703 deletions(-) create mode 100644 paperforge/skills/literature-qa/SKILL.md rename paperforge/skills/literature-qa/{ => references}/chart-reading/GSEA富集图.md (100%) rename paperforge/skills/literature-qa/{ => references}/chart-reading/INDEX.md (100%) rename paperforge/skills/literature-qa/{ => references}/chart-reading/ROC与PR曲线.md (100%) rename paperforge/skills/literature-qa/{ => references}/chart-reading/Western Blot条带图.md (100%) rename paperforge/skills/literature-qa/{ => references}/chart-reading/免疫荧光定量图.md (100%) rename paperforge/skills/literature-qa/{ => references}/chart-reading/折线图与时间序列.md (100%) rename paperforge/skills/literature-qa/{ => references}/chart-reading/散点图与气泡图.md (100%) rename paperforge/skills/literature-qa/{ => references}/chart-reading/显微照片与SEM图.md (100%) rename paperforge/skills/literature-qa/{ => references}/chart-reading/条形图与误差棒.md (100%) rename paperforge/skills/literature-qa/{ => references}/chart-reading/桑基图与弦图.md (100%) rename paperforge/skills/literature-qa/{ => references}/chart-reading/森林图与Meta分析.md (100%) rename paperforge/skills/literature-qa/{ => references}/chart-reading/火山图与曼哈顿图.md (100%) rename paperforge/skills/literature-qa/{ => references}/chart-reading/热图与聚类图.md (100%) rename paperforge/skills/literature-qa/{ => references}/chart-reading/生存曲线.md (100%) rename paperforge/skills/literature-qa/{ => references}/chart-reading/箱式图与小提琴图.md (100%) rename paperforge/skills/literature-qa/{ => references}/chart-reading/组织学半定量图.md (100%) rename paperforge/skills/literature-qa/{ => references}/chart-reading/网络图与通路图.md (100%) rename paperforge/skills/literature-qa/{ => references}/chart-reading/蛋白质结构图.md (100%) rename paperforge/skills/literature-qa/{ => references}/chart-reading/降维图(PCA-tSNE-UMAP).md (100%) rename paperforge/skills/literature-qa/{ => references}/chart-reading/雷达图与漏斗图.md (100%) create mode 100644 paperforge/skills/literature-qa/references/deep-reading.md rename paperforge/skills/literature-qa/{prompt_deep_subagent.md => references/deep-subagent.md} (100%) create mode 100644 paperforge/skills/literature-qa/references/paper-qa.md create mode 100644 paperforge/skills/literature-qa/references/paper-resolution.md create mode 100644 paperforge/skills/literature-qa/references/save-session.md delete mode 100644 paperforge/skills/literature-qa/scripts/pf-deep.md delete mode 100644 paperforge/skills/literature-qa/scripts/pf-end.md delete mode 100644 paperforge/skills/literature-qa/scripts/pf-ocr.md delete mode 100644 paperforge/skills/literature-qa/scripts/pf-paper.md delete mode 100644 paperforge/skills/literature-qa/scripts/pf-status.md delete mode 100644 paperforge/skills/literature-qa/scripts/pf-sync.md diff --git a/paperforge/plugin/main.js b/paperforge/plugin/main.js index ef4f11d6..67083316 100644 --- a/paperforge/plugin/main.js +++ b/paperforge/plugin/main.js @@ -1460,11 +1460,20 @@ class PaperForgeStatusView extends ItemView { const truncated = extracted.length > 200 ? extracted.slice(0, 200) + '...' : extracted; excerptEl.setText(truncated); if (extracted.length > 200) { - const expandBtn = body.createEl('button', { cls: 'paperforge-paper-overview-expand', text: '展开' }); + const expandContainer = body.createEl('div', { cls: 'paperforge-expand-container' }); + const expandBtn = expandContainer.createEl('button', { cls: 'paperforge-expand-icon', title: '展开/收起' }); + + // Insert arrow SVG + expandBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>'; + let expanded = false; - expandBtn.addEventListener('click', () => { + + // Make the whole container clickable + expandContainer.addEventListener('click', () => { excerptEl.setText(expanded ? truncated : extracted); - expandBtn.setText(expanded ? '展开' : '收起'); + expandBtn.innerHTML = expanded + ? '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>' + : '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"></polyline></svg>'; expanded = !expanded; }); } @@ -1556,14 +1565,23 @@ class PaperForgeStatusView extends ItemView { : (qa.answer || ''); aEl.createEl('span', { cls: 'paperforge-discussion-a-text', text: '解答:' + shortAnswer }); if (qa.answer && qa.answer.length > 150) { - const expandLink = aEl.createEl('button', { cls: 'paperforge-discussion-expand', text: '展开' }); + const expandContainer = aEl.createEl('div', { cls: 'paperforge-expand-container' }); + const expandBtn = expandContainer.createEl('button', { cls: 'paperforge-expand-icon', title: '展开/收起' }); + + // Insert arrow SVG + expandBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>'; + let expanded = false; - expandLink.addEventListener('click', () => { + + // Make the whole container clickable + expandContainer.addEventListener('click', () => { const textSpan = aEl.querySelector('.paperforge-discussion-a-text'); if (textSpan) { textSpan.setText(expanded ? ('解答:' + shortAnswer) : ('解答:' + qa.answer)); } - expandLink.setText(expanded ? '展开' : '收起'); + expandBtn.innerHTML = expanded + ? '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>' + : '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"></polyline></svg>'; expanded = !expanded; }); } @@ -1692,9 +1710,19 @@ class PaperForgeStatusView extends ItemView { }); // Show "Run in [agent_platform]" label below the button (DASH-02) - const platform = this.app.plugins.plugins['paperforge']?.settings?.agent_platform || 'opencode'; + const platformKey = this.app.plugins.plugins['paperforge']?.settings?.agent_platform || 'opencode'; + const AGENTS = { + 'opencode': 'OpenCode', + 'claude': 'Claude Code', + 'cursor': 'Cursor', + 'github_copilot': 'GitHub Copilot', + 'windsurf': 'Windsurf', + 'codex': 'Codex', + 'cline': 'Cline' + }; + const platformName = AGENTS[platformKey] || platformKey; const labelEl = card.createEl('div', { cls: 'paperforge-agent-platform-label' }); - labelEl.setText(t('run_in_agent').replace('{0}', platform)); + labelEl.setText(t('run_in_agent').replace('{0}', platformName)); } else if (nextStep === 'ready') { const trigger = card.createEl('button', { cls: 'paperforge-next-step-trigger' }); trigger.createEl('span', { text: '✓ ' + info.label }); @@ -1806,7 +1834,7 @@ class PaperForgeStatusView extends ItemView { { cls: 'pending', value: ocrPending, label: 'Pending' }, { cls: 'active', value: ocrProcessing, label: 'Processing' }, { cls: 'done', value: ocrDone, label: 'Done' }, - { cls: 'failed', value: ocrFailed, label: 'Needs Attention' }, + { cls: 'failed', value: ocrFailed, label: 'Attention' }, ]; for (const l of ocrLabels) { const cnt = ocrCounts.createEl('div', { cls: 'paperforge-ocr-count' }); diff --git a/paperforge/plugin/styles.css b/paperforge/plugin/styles.css index 842b8a35..f861ef3a 100644 --- a/paperforge/plugin/styles.css +++ b/paperforge/plugin/styles.css @@ -8,18 +8,25 @@ font-family: inherit; font-size: 14px; color: var(--text-normal); - padding: 14px; + padding: 14px 14px 56px 14px; height: 100%; overflow-y: auto; + scrollbar-gutter: stable; display: flex; flex-direction: column; gap: 12px; + container-type: inline-size; + container-name: pfpanel; --pf-surface: var(--background-secondary); --pf-surface-alt: var(--background-primary-alt); --pf-border: var(--background-modifier-border); --pf-border-hover: var(--background-modifier-border-hover); --pf-accent: var(--interactive-accent); --pf-radius: var(--radius-m); + --pf-paper-accent: #6f8780; + --pf-collection-accent: #9a6b52; + --pf-global-accent: #5f7180; + --pf-warm-line: #a28a5d; } /* ── Typography tokens ── */ @@ -343,7 +350,11 @@ } .paperforge-progress-seg.pending { - background: var(--text-faint); + background: #e2d9c8; +} + +.theme-dark .paperforge-progress-seg.pending { + background: #4a4135; } .paperforge-progress-seg.active { @@ -352,7 +363,11 @@ } .paperforge-progress-seg.done { - background: var(--color-green); + background: #3e5a47; +} + +.theme-dark .paperforge-progress-seg.done { + background: #4f735b; } .paperforge-progress-seg.failed { @@ -380,8 +395,8 @@ } .paperforge-ocr-count-value { - font-size: var(--font-ui-medium); - font-weight: var(--font-bold); + font-size: 18px; + font-weight: 600; color: var(--text-normal); line-height: 1.2; } @@ -1203,6 +1218,7 @@ font-weight: var(--font-semibold); padding: 2px 8px; border-radius: 10px; + border: 1px solid transparent; text-transform: uppercase; letter-spacing: 0.5px; line-height: 1.4; @@ -1210,18 +1226,21 @@ } .paperforge-mode-badge.global { - background: var(--interactive-accent); - color: var(--text-on-accent); + background: color-mix(in srgb, var(--pf-global-accent) 12%, var(--background-secondary)); + border-color: color-mix(in srgb, var(--pf-global-accent) 28%, var(--pf-border)); + color: var(--pf-global-accent); } .paperforge-mode-badge.paper { - background: var(--color-cyan); - color: var(--text-on-accent); + background: color-mix(in srgb, var(--pf-paper-accent) 12%, var(--background-secondary)); + border-color: color-mix(in srgb, var(--pf-paper-accent) 28%, var(--pf-border)); + color: var(--pf-paper-accent); } .paperforge-mode-badge.collection { - background: var(--color-purple); - color: var(--text-on-accent); + background: color-mix(in srgb, var(--pf-collection-accent) 12%, var(--background-secondary)); + border-color: color-mix(in srgb, var(--pf-collection-accent) 28%, var(--pf-border)); + color: var(--pf-collection-accent); } .paperforge-mode-name { @@ -1294,36 +1313,6 @@ flex-wrap: wrap; } -.paperforge-contextual-btn { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 6px 14px; - font-size: var(--font-ui-small); - font-weight: var(--font-semibold); - color: var(--text-normal); - background: var(--background-secondary-alt); - border: 1px solid var(--background-modifier-border); - border-radius: var(--radius-m); - cursor: pointer; - transition: background 0.15s, color 0.15s; - user-select: none; -} - -.paperforge-contextual-btn:hover { - background: var(--background-modifier-hover); - color: var(--text-accent); -} - -.paperforge-contextual-btn:active { - transform: scale(0.97); -} - -.paperforge-contextual-btn-icon { - font-size: 14px; - line-height: 1; -} - /* ========================================================================== SECTION 16 — Next-Step Recommendation Card ========================================================================== */ @@ -1393,6 +1382,8 @@ display: flex; flex-direction: column; gap: 24px; + max-width: 800px; + margin: 0 auto; } .paperforge-collection-metrics { @@ -1687,65 +1678,170 @@ } /* ========================================================================== - SECTION 39 — Shared Components (Native Light Surface Design) + SECTION 39 — Dashboard Visual System ========================================================================== */ -/* ── Section Label ── */ +/* Shared tokens */ .paperforge-section-label { font-size: 12px; font-weight: 600; color: var(--text-muted); + margin: 0 0 10px 0; + padding-left: 10px; + border-left: 3px solid var(--pf-warm-line); text-transform: uppercase; - letter-spacing: 0.04em; - margin-bottom: 6px; + letter-spacing: 0.06em; + line-height: 1.3; +} + +.paperforge-contextual-btn { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 5px 12px; + background: var(--background-secondary); + border: 1px solid var(--pf-border); + border-radius: var(--radius-s); + color: var(--text-muted); + font-size: 12px; + font-weight: 500; + cursor: pointer; + transition: background 0.15s, border-color 0.15s, color 0.15s; + white-space: nowrap; + user-select: none; +} + +.paperforge-contextual-btn:hover { + background: var(--background-modifier-hover); + border-color: color-mix(in srgb, var(--pf-warm-line) 52%, var(--pf-border-hover)); + color: var(--text-normal); +} + +.paperforge-contextual-btn.running { + opacity: 0.6; + pointer-events: none; +} + +.paperforge-contextual-btn:active { + transform: scale(0.97); +} + +.paperforge-contextual-btn:focus-visible { + outline: 2px solid color-mix(in srgb, var(--interactive-accent) 62%, white 38%); + outline-offset: 2px; +} + +.paperforge-contextual-btn-icon { + font-size: 14px; + line-height: 1; } -/* ── Status Pill ── */ .paperforge-status-pill { display: inline-flex; align-items: center; gap: 4px; - padding: 2px 8px; + padding: 2px 10px; + border: 1px solid var(--pf-border); border-radius: 999px; - background: var(--background-primary-alt); - color: var(--text-muted); font-size: 12px; font-weight: 500; - line-height: 1.4; -} - -.paperforge-status-pill-icon { font-size: 11px; line-height: 1; } - -.paperforge-status-pill.ok { color: var(--text-success); } -.paperforge-status-pill.fail { color: var(--text-error); } -.paperforge-status-pill.pending { color: var(--text-muted); } - -/* ── Cards (only primary content modules) ── */ -.paperforge-paper-overview, -.paperforge-discussion-card { - margin: 8px 0; - padding: 12px; - background: var(--background-secondary); - border: 1px solid var(--background-modifier-border); - border-radius: var(--radius-m); -} - -/* ── Paper Overview ── */ -.paperforge-paper-overview-header { margin-bottom: 6px; } - -.paperforge-paper-overview-title { - font-size: 12px; - font-weight: 600; + line-height: 1.5; + background: var(--background-primary-alt); color: var(--text-muted); +} + +.paperforge-status-pill.pending { + background: color-mix(in srgb, var(--pf-paper-accent) 9%, var(--background-primary)); + border-color: color-mix(in srgb, var(--pf-paper-accent) 20%, var(--pf-border)); + color: var(--pf-paper-accent); +} + +.paperforge-status-pill.ok { + background: rgba(var(--background-modifier-success-rgb, 0, 200, 100), 0.12); + border-color: rgba(var(--background-modifier-success-rgb, 0, 200, 100), 0.2); + color: var(--text-success, var(--color-green)); +} + +.paperforge-status-pill.fail { + background: rgba(var(--background-modifier-error-rgb, 255, 80, 80), 0.12); + border-color: rgba(var(--background-modifier-error-rgb, 255, 80, 80), 0.22); + color: var(--text-error); +} + +.paperforge-status-pill-icon { + font-size: 11px; + line-height: 1; +} + +.paperforge-library-snapshot, +.paperforge-system-status, +.paperforge-global-actions { + padding: 20px; + background: color-mix(in srgb, var(--background-primary) 92%, var(--background-secondary) 8%); + border: 1px solid color-mix(in srgb, var(--pf-warm-line) 20%, var(--pf-border)); + border-radius: calc(var(--pf-radius) + 2px); + box-shadow: 0 1px 2px rgba(20, 18, 14, 0.04), 0 10px 24px rgba(20, 18, 14, 0.04); + transition: border-color 0.2s, box-shadow 0.2s, transform 0.2s; +} + +.paperforge-library-snapshot:hover, +.paperforge-system-status:hover, +.paperforge-global-actions:hover { + border-color: color-mix(in srgb, var(--pf-warm-line) 42%, var(--pf-border)); + box-shadow: 0 2px 10px rgba(20, 18, 14, 0.08), 0 14px 28px rgba(20, 18, 14, 0.06); +} + +/* Paper mode */ +.paperforge-status-strip { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 10px; + padding: 10px 0 8px; + margin: 4px 0 6px; +} + +.paperforge-status-strip-left, +.paperforge-status-strip-right { + display: flex; + align-items: center; + gap: 6px; + flex-wrap: wrap; +} + +.paperforge-paper-overview { + margin: 10px 0; + padding: 18px; + background: var(--background-primary); + border: 1px solid color-mix(in srgb, var(--pf-warm-line) 18%, var(--pf-border)); + border-radius: calc(var(--pf-radius) + 2px); + box-shadow: inset 3px 0 0 color-mix(in srgb, var(--pf-paper-accent) 16%, transparent); + transition: border-color 0.2s, box-shadow 0.2s; +} + +.paperforge-paper-overview:hover { + border-color: color-mix(in srgb, var(--pf-warm-line) 34%, var(--pf-border)); + box-shadow: inset 3px 0 0 color-mix(in srgb, var(--pf-paper-accent) 22%, transparent), 0 2px 8px rgba(0, 0, 0, 0.06); +} + +.paperforge-paper-overview-header { + margin-bottom: 8px; +} + +.paperforge-paper-overview-title, +.paperforge-discussion-title { + font-size: 12px; + font-weight: 700; + color: var(--pf-paper-accent); text-transform: uppercase; - letter-spacing: 0.04em; + letter-spacing: 0.06em; } .paperforge-paper-overview-body { font-size: 14px; - font-weight: 400; - color: var(--text-muted); - line-height: 1.55; + color: var(--text-normal); + line-height: 1.72; } .paperforge-paper-overview-excerpt { @@ -1753,38 +1849,74 @@ word-break: break-word; } -.paperforge-paper-overview-expand { - display: inline-block; - margin-top: 4px; - padding: 2px 0; - background: none; - border: none; - color: var(--text-accent); - font-size: 13px; - font-weight: 400; - cursor: pointer; - transition: color 0.15s; +.paperforge-expand-container, +.paperforge-discussion-viewall { + color: var(--pf-paper-accent); } -.paperforge-paper-overview-expand:hover { +.paperforge-expand-container { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + margin-top: 8px; + padding: 4px 0; + cursor: pointer; + transition: background-color 0.2s ease, color 0.2s ease; + border-radius: var(--pf-radius); + background: linear-gradient(to bottom, transparent, color-mix(in srgb, var(--pf-surface-alt) 50%, transparent)); +} + +.paperforge-expand-icon { + background: none; + border: none; + padding: 0; + margin: 0; + color: inherit; + display: flex; + align-items: center; + justify-content: center; + pointer-events: none; /* Let container handle clicks */ + opacity: 0.6; + transition: opacity 0.2s ease; +} + +.paperforge-expand-container:hover, +.paperforge-discussion-viewall:hover { color: var(--interactive-accent-hover); } -/* ── Discussion Card ── */ -.paperforge-discussion-header { margin-bottom: 8px; } +.paperforge-expand-container:hover { + background: color-mix(in srgb, var(--pf-surface-alt) 80%, transparent); +} -.paperforge-discussion-title { - font-size: 12px; - font-weight: 600; - color: var(--text-muted); - text-transform: uppercase; - letter-spacing: 0.04em; +.paperforge-expand-container:hover .paperforge-expand-icon { + opacity: 1; +} + +.paperforge-discussion-header { + margin-bottom: 10px; +} + +.paperforge-discussion-card { + margin: 10px 0; + padding: 18px; + background: var(--background-primary); + border: 1px solid color-mix(in srgb, var(--pf-warm-line) 18%, var(--pf-border)); + border-radius: calc(var(--pf-radius) + 2px); + box-shadow: inset 3px 0 0 color-mix(in srgb, var(--pf-paper-accent) 11%, transparent); + transition: border-color 0.2s, box-shadow 0.2s; +} + +.paperforge-discussion-card:hover { + border-color: color-mix(in srgb, var(--pf-warm-line) 34%, var(--pf-border)); + box-shadow: inset 3px 0 0 color-mix(in srgb, var(--pf-paper-accent) 16%, transparent), 0 2px 8px rgba(0, 0, 0, 0.06); } .paperforge-discussion-item { - margin-bottom: 8px; - padding-bottom: 8px; - border-bottom: 1px solid var(--background-modifier-border); + margin-bottom: 10px; + padding-bottom: 10px; + border-bottom: 1px solid var(--pf-border); } .paperforge-discussion-item:last-child { @@ -1803,82 +1935,92 @@ .paperforge-discussion-a { font-size: 14px; - font-weight: 400; - color: var(--text-muted); - line-height: 1.55; + color: var(--text-normal); + line-height: 1.68; } -.paperforge-discussion-expand { - display: inline; - padding: 0; - margin-left: 4px; - background: none; - border: none; - color: var(--text-accent); - font-size: 13px; - font-weight: 400; - cursor: pointer; - transition: color 0.15s; -} -.paperforge-discussion-expand:hover { color: var(--interactive-accent-hover); } .paperforge-discussion-viewall { display: block; text-align: right; - margin-top: 4px; - font-size: 13px; - font-weight: 400; - color: var(--text-accent); + margin-top: 6px; + font-size: 12px; + font-weight: 500; cursor: pointer; text-decoration: none; transition: color 0.15s; } -.paperforge-discussion-viewall:hover { color: var(--interactive-accent-hover); } - -/* ── Complete state row ── */ -.paperforge-complete-row { - display: flex; - align-items: center; - gap: 6px; - color: var(--text-success); - font-size: 13px; - font-weight: 400; - line-height: 1.4; - padding: 4px 0; +.paperforge-discussion-viewall:hover { + text-decoration: underline; +} + +.paperforge-paper-files { + display: flex; + gap: 8px; + margin: 10px 0; +} + +.paperforge-status-strip-right .paperforge-contextual-btn { + min-height: 34px; + padding: 7px 12px; + background: color-mix(in srgb, var(--background-secondary) 78%, var(--background-primary)); + border-color: color-mix(in srgb, var(--pf-border) 88%, var(--pf-warm-line)); + color: var(--text-muted); +} + +.paperforge-status-strip-right .paperforge-contextual-btn:hover { + background: color-mix(in srgb, var(--background-modifier-hover) 78%, var(--background-primary)); + border-color: color-mix(in srgb, var(--pf-border-hover) 82%, var(--pf-warm-line)); + color: var(--text-normal); } -/* ── Technical Details disclosure ── */ .paperforge-technical-details { - margin: 4px 0; + margin: 8px 0 4px; } .paperforge-technical-details-toggle { - display: inline-block; - padding: 4px 0; + width: 100%; + min-height: 34px; + padding: 8px 0; background: none; border: none; + border-radius: 0; color: var(--text-muted); - font-size: 13px; - font-weight: 400; + font-size: 12px; + font-weight: 600; cursor: pointer; - user-select: none; + text-align: left; + letter-spacing: 0.02em; transition: color 0.15s; } -.paperforge-technical-details-toggle:hover { color: var(--text-normal); } +.paperforge-technical-details-toggle:hover { + background: none; + border-color: transparent; + color: var(--text-normal); +} .paperforge-technical-details-body { - padding: 6px 0 2px 0; + margin-top: 0; + padding: 8px 0 0; + background: none; + border: none; + border-radius: 0; font-size: 12px; - color: var(--text-faint); } .paperforge-technical-row { display: flex; justify-content: space-between; - padding: 2px 0; + gap: 12px; + padding: 6px 0; + border-bottom: 1px solid color-mix(in srgb, var(--pf-border) 72%, transparent); +} + +.paperforge-technical-row:last-child { + border-bottom: none; } .paperforge-technical-label { @@ -1896,87 +2038,27 @@ text-align: right; } -/* ── Contextual buttons ── */ -.paperforge-contextual-btn { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 3px 8px; - background: var(--interactive-normal); - border: 1px solid var(--background-modifier-border); - color: var(--text-normal); - border-radius: var(--button-radius, 4px); - font-size: 12px; - font-weight: 400; - cursor: pointer; - transition: background 0.15s; - white-space: nowrap; -} - -.paperforge-contextual-btn:hover { - background: var(--interactive-hover); - border-color: var(--background-modifier-border-hover); -} - -.paperforge-contextual-btn.running { - opacity: 0.6; - pointer-events: none; -} - -.paperforge-contextual-btn-icon { - font-size: 14px; - line-height: 1; -} - -.paperforge-contextual-btn.primary { - background: var(--interactive-accent); - color: var(--text-on-accent); - border-color: var(--interactive-accent); - font-weight: 500; -} - -.paperforge-contextual-btn.primary:hover { - opacity: 0.85; -} - -/* ========================================================================== - SECTION 40 — Per-Paper View - ========================================================================== */ - -/* ── Status Strip + Files Row (merged) ── */ -.paperforge-status-strip { - display: flex; - align-items: center; - justify-content: space-between; - flex-wrap: wrap; - gap: 8px; - padding: 6px 0; - margin: 4px 0; -} - -.paperforge-status-strip-left { - display: flex; - align-items: center; - gap: 6px; -} - -.paperforge-status-strip-right { - display: flex; - align-items: center; - gap: 6px; -} - -/* ── Workflow Toggles (inline, not a card) ── */ .paperforge-workflow-toggles { display: flex; + flex-wrap: wrap; gap: 14px; - padding: 6px 0; + margin: 0 0 10px; + padding: 0 0 10px; + background: none; + border: none; + border-bottom: 1px solid color-mix(in srgb, var(--pf-border) 72%, transparent); + border-radius: 0; } .paperforge-workflow-toggle { display: flex; align-items: center; gap: 6px; + min-height: 34px; + padding: 6px 10px; + background: color-mix(in srgb, var(--background-primary-alt) 88%, var(--background-primary)); + border: 1px solid color-mix(in srgb, var(--pf-border) 82%, var(--pf-paper-accent)); + border-radius: 999px; cursor: pointer; font-size: 13px; color: var(--text-muted); @@ -1992,28 +2074,25 @@ } .paperforge-workflow-toggle-label { - font-weight: 500; + font-weight: 600; color: var(--text-normal); } .paperforge-workflow-toggle-hint { color: var(--text-faint); - font-size: 12px; + font-size: 11px; } -/* ========================================================================== - SECTION 41 — Collection / Base View - ========================================================================== */ - +/* Collection mode */ .paperforge-collection-header { - margin-bottom: 8px; + margin-bottom: 10px; } .paperforge-collection-title { font-size: 16px; font-weight: 600; color: var(--text-normal); - line-height: 1.35; + line-height: 1.3; } .paperforge-collection-count { @@ -2022,629 +2101,87 @@ margin-top: 2px; } +.paperforge-global-view { + display: flex; + flex-direction: column; + gap: 18px; + max-width: 800px; + margin: 0 auto; +} + +.paperforge-global-view .paperforge-section-label, +.paperforge-workflow-overview .paperforge-section-label { + margin-bottom: 12px; +} + .paperforge-workflow-overview { - margin: 8px 0; - padding: 12px; - background: var(--background-secondary); - border: 1px solid var(--background-modifier-border); - border-radius: var(--radius-m); + margin: 10px 0; + padding: 18px; + background: color-mix(in srgb, var(--background-primary) 94%, var(--background-primary-alt) 6%); + border: 1px solid color-mix(in srgb, var(--pf-warm-line) 20%, var(--pf-border)); + border-radius: calc(var(--pf-radius) + 2px); + box-shadow: inset 3px 0 0 color-mix(in srgb, var(--pf-collection-accent) 12%, transparent); + transition: border-color 0.2s, box-shadow 0.2s; +} + +.paperforge-workflow-overview:hover { + border-color: color-mix(in srgb, var(--pf-warm-line) 42%, var(--pf-border)); + box-shadow: inset 3px 0 0 color-mix(in srgb, var(--pf-collection-accent) 17%, transparent), 0 2px 10px rgba(20, 18, 14, 0.08); } .paperforge-workflow-funnel { - display: flex; + display: grid; + grid-template-columns: 1fr auto 1fr auto 1fr auto 1fr; + gap: 8px 6px; align-items: center; - gap: 3px; - flex-wrap: wrap; - justify-content: center; +} + +@container pfpanel (max-width: 380px) { + .paperforge-workflow-funnel { + grid-template-columns: 1fr auto 1fr; + } + .paperforge-workflow-funnel > div:nth-child(4) { + display: none; + } +} + +@container pfpanel (max-width: 220px) { + .paperforge-workflow-funnel { + grid-template-columns: 1fr; + } + .paperforge-workflow-arrow { + display: none; + } } .paperforge-workflow-stage { display: flex; flex-direction: column; align-items: center; - padding: 6px 12px; - background: var(--background-primary-alt); - border-radius: 6px; - min-width: 52px; -} - -.paperforge-workflow-stage-value { - font-size: 18px; - font-weight: 600; - color: var(--text-normal); - line-height: 1.2; -} - -.paperforge-workflow-stage-label { - font-size: 11px; - color: var(--text-muted); - text-transform: uppercase; - letter-spacing: 0.04em; - margin-top: 2px; - text-align: center; -} - -.paperforge-workflow-arrow { - color: var(--text-faint); - font-size: 12px; - flex-shrink: 0; -} - -.paperforge-collection-ocr-header { - display: flex; - align-items: center; - gap: 10px; - margin-bottom: 14px; -} - -.paperforge-issue-summary { - margin: 8px 0; - padding: 10px 12px; - background: var(--background-secondary); - border: 1px solid var(--background-modifier-border); - border-left: 3px solid var(--color-orange, #e67e22); - border-radius: var(--radius-m); -} - -.paperforge-issue-summary .paperforge-section-label { - color: var(--color-orange, #e67e22); -} - -.paperforge-issue-list { - margin: 4px 0 6px 0; -} - -.paperforge-issue-item { - font-size: 14px; - color: var(--text-muted); - padding: 2px 0; -} - -.paperforge-issue-actions { - display: flex; - gap: 8px; - margin-top: 8px; -} - -.paperforge-collection-actions { - display: flex; - gap: 8px; - margin-top: 8px; - padding-top: 8px; - border-top: 1px solid var(--background-modifier-border); -} - -/* ========================================================================== - SECTION 42 — Global / Home View - ========================================================================== */ - -.paperforge-library-snapshot { - padding: 12px; - background: var(--background-secondary); - border: 1px solid var(--background-modifier-border); - border-radius: var(--radius-m); -} - -.paperforge-snapshot-pills { - display: flex; - gap: 8px; - flex-wrap: wrap; -} - -.paperforge-snapshot-pill { - display: flex; - flex-direction: column; - align-items: center; - padding: 8px 14px; - background: var(--background-primary-alt); - border-radius: 6px; - min-width: 56px; - flex: 1; -} - -.paperforge-snapshot-value { - font-size: 18px; - font-weight: 600; - color: var(--text-normal); - line-height: 1.2; -} - -.paperforge-snapshot-label { - font-size: 11px; - color: var(--text-muted); - text-transform: uppercase; - letter-spacing: 0.04em; - margin-top: 2px; - text-align: center; -} - -.paperforge-system-status { - padding: 12px; - background: var(--background-secondary); - border: 1px solid var(--background-modifier-border); - border-radius: var(--radius-m); -} - -.paperforge-status-grid { - display: flex; - flex-direction: column; - gap: 1px; -} - -.paperforge-status-row { - display: flex; - align-items: center; - gap: 8px; - padding: 3px 0; -} - -.paperforge-status-dot { - width: 7px; - height: 7px; - border-radius: 50%; - flex-shrink: 0; - background: var(--background-modifier-border); - transition: background 0.3s; -} - -.paperforge-status-dot.ok { background: var(--text-success); } -.paperforge-status-dot.fail { background: var(--text-error); } - -.paperforge-status-label { - font-size: 14px; - color: var(--text-muted); - min-width: 85px; -} - -.paperforge-status-detail { - font-size: 13px; - color: var(--text-faint); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.paperforge-global-actions { - padding: 12px; - background: var(--background-secondary); - border: 1px solid var(--background-modifier-border); - border-radius: var(--radius-m); -} - -.paperforge-global-actions-row { - display: flex; - gap: 8px; -} - -/* ========================================================================== - SECTION 43 — Dark Theme - ========================================================================== */ - -.theme-dark .paperforge-status-pill { - background: var(--background-primary-alt); -} - -.theme-dark .paperforge-status-pill.ok { - color: var(--text-success); -} - -.theme-dark .paperforge-status-pill.fail { - color: var(--text-error); -} - -/* ========================================================================== - SECTION 39 — Dashboard Redesign: Shared Components - ========================================================================== */ - -/* ── Section Label ── */ -.paperforge-section-label { - font-size: 11px; - font-weight: 600; - color: var(--text-muted); - margin: 0 0 10px 0; - padding-left: 10px; - border-left: 3px solid var(--interactive-accent); - text-transform: uppercase; - letter-spacing: 0.06em; - line-height: 1.3; -} - -/* ── Contextual Button ── */ -.paperforge-contextual-btn { - display: inline-flex; - align-items: center; - gap: 5px; - padding: 5px 12px; - background: var(--background-secondary); - border: 1px solid var(--background-modifier-border); - border-radius: var(--radius-s); - color: var(--text-muted); - font-size: 12px; - font-weight: 500; - cursor: pointer; - transition: background 0.15s, border-color 0.15s, color 0.15s; - white-space: nowrap; -} - -.paperforge-contextual-btn:hover { - background: var(--background-modifier-hover); - border-color: var(--interactive-accent); - color: var(--text-normal); -} - -.paperforge-contextual-btn.running { - opacity: 0.6; - pointer-events: none; -} - -.paperforge-contextual-btn-icon { - font-size: 14px; - line-height: 1; -} - -/* ========================================================================== - SECTION 40 — Per-Paper View (Reading Companion) - ========================================================================== */ - -/* ── Status Strip ── */ -.paperforge-status-strip { - display: flex; - gap: 6px; - padding: 8px 0; - margin: 6px 0; -} - -.paperforge-status-pill { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 2px 10px; - border-radius: 4px; - font-size: 12px; - font-weight: 500; - line-height: 1.5; - background: var(--background-primary-alt); - color: var(--text-muted); -} - -.paperforge-status-pill.ok { - background: rgba(var(--background-modifier-success-rgb, 0, 200, 100), 0.12); - color: var(--text-success, var(--color-green)); -} - -.paperforge-status-pill.fail { - background: rgba(var(--background-modifier-error-rgb, 255, 80, 80), 0.12); - color: var(--text-error); -} - -.paperforge-status-pill.pending { - background: var(--background-primary-alt); - color: var(--text-muted); -} - -.paperforge-status-pill-icon { - font-size: 11px; - line-height: 1; -} - -/* Theme dark: softer pill backgrounds */ -.theme-dark .paperforge-status-pill.ok { - background: rgba(100, 200, 120, 0.15); -} -.theme-dark .paperforge-status-pill.fail { - background: rgba(255, 100, 100, 0.15); -} - -/* ── Paper Overview Card ── */ -.paperforge-paper-overview { - margin: 10px 0; - padding: 16px; - background: var(--background-primary); - border: 1px solid var(--background-modifier-border); - border-radius: var(--radius-m); - transition: border-color 0.2s, box-shadow 0.2s; - position: relative; -} - -.paperforge-paper-overview::before { - content: ''; - position: absolute; - inset: -1px; - border-radius: inherit; - opacity: 0; - box-shadow: 0 2px 8px rgba(0,0,0,0.08); - transition: opacity 0.2s; - pointer-events: none; -} - -.paperforge-paper-overview:hover::before { - opacity: 1; -} - -.paperforge-paper-overview-header { - margin-bottom: 8px; -} - -.paperforge-paper-overview-title { - font-size: 11px; - font-weight: 700; - color: var(--text-accent); - text-transform: uppercase; - letter-spacing: 0.06em; -} - -.paperforge-paper-overview-body { - font-size: 13.5px; - color: var(--text-muted); - line-height: 1.65; -} - -.paperforge-paper-overview-excerpt { - white-space: pre-wrap; - word-break: break-word; -} - -.paperforge-paper-overview-expand { - display: inline-block; - margin-top: 6px; - padding: 3px 0; - background: none; - border: none; - border-bottom: 1px dashed var(--text-accent); - color: var(--text-accent); - font-size: 12px; - font-weight: 500; - cursor: pointer; - transition: color 0.15s, border-color 0.15s; -} - -.paperforge-paper-overview-expand:hover { - color: var(--interactive-accent-hover); - border-color: var(--interactive-accent-hover); -} - -/* ── Discussion Card ── */ -.paperforge-discussion-card { - margin: 10px 0; - padding: 16px; - background: var(--background-primary); - border: 1px solid var(--background-modifier-border); - border-radius: var(--radius-m); - transition: border-color 0.2s, box-shadow 0.2s; - position: relative; -} - -.paperforge-discussion-card::before { - content: ''; - position: absolute; - inset: -1px; - border-radius: inherit; - opacity: 0; - box-shadow: 0 2px 8px rgba(0,0,0,0.08); - transition: opacity 0.2s; - pointer-events: none; -} - -.paperforge-discussion-card:hover::before { - opacity: 1; -} - -.paperforge-discussion-header { - margin-bottom: 10px; -} - -.paperforge-discussion-title { - font-size: 11px; - font-weight: 700; - color: var(--text-accent); - text-transform: uppercase; - letter-spacing: 0.06em; -} - -.paperforge-discussion-item { - margin-bottom: 10px; - padding-bottom: 10px; - border-bottom: 1px solid var(--background-modifier-border); -} - -.paperforge-discussion-item:last-child { - border-bottom: none; - margin-bottom: 0; - padding-bottom: 0; -} - -.paperforge-discussion-q { - font-size: 13.5px; - font-weight: 600; - color: var(--text-normal); - margin-bottom: 4px; - line-height: 1.5; -} - -.paperforge-discussion-a { - font-size: 13.5px; - color: var(--text-muted); - line-height: 1.6; -} - -.paperforge-discussion-expand { - display: inline; - padding: 0; - margin-left: 4px; - background: none; - border: none; - border-bottom: 1px dashed var(--text-accent); - color: var(--text-accent); - font-size: 12px; - font-weight: 500; - cursor: pointer; - transition: color 0.15s; -} - -.paperforge-discussion-expand:hover { - color: var(--interactive-accent-hover); -} - -.paperforge-discussion-viewall { - display: block; - text-align: right; - margin-top: 6px; - font-size: 12px; - font-weight: 500; - color: var(--text-accent); - cursor: pointer; - text-decoration: none; - transition: color 0.15s; -} - -.paperforge-discussion-viewall:hover { - color: var(--interactive-accent-hover); - text-decoration: underline; -} - -/* ── Paper Files Row ── */ -.paperforge-paper-files { - display: flex; - gap: 8px; - margin: 10px 0; -} - -/* ── Technical Details ── */ -.paperforge-technical-details { - margin: 6px 0; -} - -.paperforge-technical-details-toggle { + padding: 12px 6px; + background: color-mix(in srgb, var(--background-primary-alt) 88%, var(--background-primary) 12%); + border: 1px solid color-mix(in srgb, var(--pf-collection-accent) 22%, var(--pf-border)); + border-radius: 8px; + min-width: 0; width: 100%; - padding: 6px 12px; - background: var(--background-primary); - border: 1px solid var(--background-modifier-border); - border-radius: var(--radius-s); - color: var(--text-faint); - font-size: 11px; - font-weight: 500; - cursor: pointer; - text-align: left; - transition: background 0.15s, border-color 0.15s, color 0.15s; -} - -.paperforge-technical-details-toggle:hover { - background: var(--background-modifier-hover); - border-color: var(--background-modifier-border-hover); - color: var(--text-muted); -} - -.paperforge-technical-details-body { - margin-top: 4px; - padding: 8px 12px; - background: var(--background-primary-alt); - border: 1px solid var(--background-modifier-border); - border-radius: var(--radius-s); - font-size: 11px; -} - -.paperforge-technical-row { - display: flex; - justify-content: space-between; - padding: 2px 0; -} - -.paperforge-technical-label { - color: var(--text-faint); - flex-shrink: 0; - margin-right: 12px; -} - -.paperforge-technical-value { - color: var(--text-muted); - max-width: 60%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - text-align: right; -} - -/* ========================================================================== - SECTION 41 — Collection / Base View (Batch Workspace) - ========================================================================== */ - -/* ── Collection Header ── */ -.paperforge-collection-header { - margin-bottom: 10px; -} - -.paperforge-collection-title { - font-size: 16px; - font-weight: 600; - color: var(--text-normal); - line-height: 1.3; -} - -.paperforge-collection-count { - font-size: 12px; - color: var(--text-muted); - margin-top: 2px; -} - -/* ── Workflow Overview ── */ -.paperforge-workflow-overview { - margin: 10px 0; - padding: 16px; - background: var(--background-primary); - border: 1px solid var(--background-modifier-border); - border-radius: var(--radius-m); - transition: border-color 0.2s, box-shadow 0.2s; - position: relative; -} - -.paperforge-workflow-overview::before { - content: ''; - position: absolute; - inset: -1px; - border-radius: inherit; - opacity: 0; - box-shadow: 0 2px 8px rgba(0,0,0,0.08); - transition: opacity 0.2s; - pointer-events: none; -} - -.paperforge-workflow-overview:hover::before { - opacity: 1; -} - -.paperforge-workflow-funnel { - display: flex; - align-items: center; - gap: 3px; - flex-wrap: wrap; - justify-content: center; -} - -.paperforge-workflow-stage { - display: flex; - flex-direction: column; - align-items: center; - padding: 8px 12px; - background: var(--background-primary-alt); - border-radius: 6px; - min-width: 56px; } .paperforge-workflow-stage-value { - font-size: 20px; + font-size: 26px; font-weight: 700; - color: var(--text-accent); + color: var(--pf-collection-accent); line-height: 1.2; - letter-spacing: -0.3px; + letter-spacing: -0.5px; } .paperforge-workflow-stage-label { - font-size: 10px; + font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.3px; - margin-top: 2px; + margin-top: 4px; text-align: center; + word-break: break-word; } .paperforge-workflow-arrow { @@ -2653,7 +2190,6 @@ flex-shrink: 0; } -/* ── OCR Pipeline in Base ── */ .paperforge-collection-ocr-header { display: flex; align-items: center; @@ -2661,77 +2197,60 @@ margin-bottom: 14px; } -/* ── Issue Summary (serious blockers only) ── */ .paperforge-issue-summary { margin: 10px 0; padding: 12px 14px; background: var(--background-primary); - border: 1px solid var(--background-modifier-border); - border-left: 3px solid var(--color-orange); - border-radius: var(--radius-m); + border: 1px solid var(--pf-border); + border-left: 3px solid var(--pf-warm-line); + border-radius: var(--pf-radius); } .paperforge-issue-summary .paperforge-section-label { - color: var(--color-orange); + border-left-color: var(--pf-warm-line); + color: var(--text-normal); } .paperforge-issue-list { - margin: 4px 0 6px 0; + margin: 8px 0 12px 0; + display: flex; + flex-direction: column; + gap: 6px; } .paperforge-issue-item { font-size: 13px; - color: var(--text-muted); - padding: 2px 0; -} - -.paperforge-issue-actions { + font-weight: 500; + color: var(--text-normal); + padding: 6px 10px; + background: color-mix(in srgb, var(--background-secondary) 50%, transparent); + border-radius: calc(var(--pf-radius) - 2px); display: flex; - gap: 8px; - margin-top: 8px; + align-items: center; } -/* ── Collection Actions ── */ +.paperforge-issue-actions, .paperforge-collection-actions { display: flex; gap: 8px; - margin-top: 10px; - padding-top: 10px; - border-top: 1px solid var(--background-modifier-border); } -/* ========================================================================== - SECTION 42 — Global / Home View (System Homepage) - ========================================================================== */ - -/* ── Library Snapshot ── */ -.paperforge-library-snapshot { - padding: 16px; - background: var(--background-primary); - border: 1px solid var(--background-modifier-border); - border-radius: var(--radius-m); - transition: border-color 0.2s, box-shadow 0.2s; - position: relative; +.paperforge-issue-actions { + margin-top: 8px; } -.paperforge-library-snapshot::before { - content: ''; - position: absolute; - inset: -1px; - border-radius: inherit; - opacity: 0; - box-shadow: 0 2px 8px rgba(0,0,0,0.08); - transition: opacity 0.2s; - pointer-events: none; -} - -.paperforge-library-snapshot:hover::before { - opacity: 1; +.paperforge-collection-actions { + align-items: center; + flex-wrap: wrap; + margin-top: 8px; + padding-top: 8px; + border-top: 1px solid color-mix(in srgb, var(--pf-border) 82%, var(--pf-warm-line)); } +/* Global mode */ .paperforge-snapshot-pills { display: flex; - gap: 8px; + gap: 10px; flex-wrap: wrap; } @@ -2739,55 +2258,32 @@ display: flex; flex-direction: column; align-items: center; - padding: 10px 14px; - background: var(--background-primary-alt); - border-radius: 6px; - min-width: 60px; + justify-content: center; + padding: 14px 16px; + background: color-mix(in srgb, var(--background-primary-alt) 86%, var(--background-primary) 14%); + border: 1px solid color-mix(in srgb, var(--pf-global-accent) 22%, var(--pf-border)); + border-radius: 8px; + min-width: 72px; flex: 1; } .paperforge-snapshot-value { - font-size: 20px; + font-size: 26px; font-weight: 700; - color: var(--text-accent); + color: var(--pf-global-accent); line-height: 1.2; - letter-spacing: -0.3px; + letter-spacing: -0.5px; } .paperforge-snapshot-label { - font-size: 10px; + font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.3px; - margin-top: 2px; + margin-top: 4px; text-align: center; } -/* ── System Status ── */ -.paperforge-system-status { - padding: 16px; - background: var(--background-primary); - border: 1px solid var(--background-modifier-border); - border-radius: var(--radius-m); - transition: border-color 0.2s, box-shadow 0.2s; - position: relative; -} - -.paperforge-system-status::before { - content: ''; - position: absolute; - inset: -1px; - border-radius: inherit; - opacity: 0; - box-shadow: 0 2px 8px rgba(0,0,0,0.08); - transition: opacity 0.2s; - pointer-events: none; -} - -.paperforge-system-status:hover::before { - opacity: 1; -} - .paperforge-status-grid { display: flex; flex-direction: column; @@ -2797,8 +2293,8 @@ .paperforge-status-row { display: flex; align-items: center; - gap: 8px; - padding: 4px 0; + gap: 10px; + padding: 6px 0; } .paperforge-status-dot { @@ -2806,12 +2302,12 @@ height: 7px; border-radius: 50%; flex-shrink: 0; - background: var(--background-modifier-border); + background: var(--pf-border); transition: background 0.3s; } .paperforge-status-dot.ok { - background: var(--color-green); + background: var(--text-success); } .paperforge-status-dot.fail { @@ -2825,123 +2321,89 @@ } .paperforge-status-detail { - font-size: 12px; + font-size: 13px; color: var(--text-faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -/* ── Global Actions ── */ -.paperforge-global-actions { - padding: 16px; - background: var(--background-primary); - border: 1px solid var(--background-modifier-border); - border-radius: var(--radius-m); - transition: border-color 0.2s, box-shadow 0.2s; - position: relative; -} - -.paperforge-global-actions::before { - content: ''; - position: absolute; - inset: -1px; - border-radius: inherit; - opacity: 0; - box-shadow: 0 2px 8px rgba(0,0,0,0.08); - transition: opacity 0.2s; - pointer-events: none; -} - -.paperforge-global-actions:hover::before { - opacity: 1; -} - .paperforge-global-actions-row { display: flex; gap: 8px; + flex-wrap: wrap; } -/* ========================================================================== - SECTION 43 — Workflow Toggle Checkboxes (per-paper) - ========================================================================== */ - -.paperforge-workflow-toggles { - display: flex; - gap: 14px; - padding: 12px 16px; - background: var(--background-primary); - border: 1px solid var(--background-modifier-border); - border-radius: var(--radius-m); - margin: 10px 0; - transition: border-color 0.2s, box-shadow 0.2s; - position: relative; -} - -.paperforge-workflow-toggles::before { - content: ''; - position: absolute; - inset: -1px; - border-radius: inherit; - opacity: 0; - box-shadow: 0 2px 8px rgba(0,0,0,0.08); - transition: opacity 0.2s; - pointer-events: none; -} - -.paperforge-workflow-toggles:hover::before { - opacity: 1; -} - -.paperforge-workflow-toggle { - display: flex; - align-items: center; - gap: 6px; - cursor: pointer; +.paperforge-collection-actions .paperforge-contextual-btn, +.paperforge-global-actions-row .paperforge-contextual-btn { + min-height: 36px; + padding: 8px 14px; font-size: 13px; - color: var(--text-muted); - user-select: none; -} - -.paperforge-workflow-checkbox { - width: 15px; - height: 15px; - margin: 0; - cursor: pointer; - accent-color: var(--interactive-accent); -} - -.paperforge-workflow-toggle-label { font-weight: 600; - color: var(--text-normal); } -.paperforge-workflow-toggle-hint { - color: var(--text-faint); +.paperforge-global-actions-row .paperforge-contextual-btn.primary { + background: color-mix(in srgb, var(--pf-global-accent) 86%, black 14%); + border-color: color-mix(in srgb, var(--pf-global-accent) 90%, black 10%); + color: var(--text-on-accent); +} + +.paperforge-collection-actions .paperforge-contextual-btn.primary { + background: color-mix(in srgb, var(--pf-collection-accent) 84%, black 16%); + border-color: color-mix(in srgb, var(--pf-collection-accent) 88%, black 12%); + color: var(--text-on-accent); +} + +.paperforge-global-actions-row .paperforge-contextual-btn.primary:hover { + background: color-mix(in srgb, var(--pf-global-accent) 78%, black 22%); + border-color: color-mix(in srgb, var(--pf-global-accent) 82%, black 18%); +} + +.paperforge-collection-actions .paperforge-contextual-btn.primary:hover { + background: color-mix(in srgb, var(--pf-collection-accent) 76%, black 24%); + border-color: color-mix(in srgb, var(--pf-collection-accent) 80%, black 20%); +} + +.paperforge-collection-view .paperforge-ocr-section { + padding: 18px 18px 16px; + background: color-mix(in srgb, var(--background-secondary) 90%, var(--background-primary) 10%); + border-color: color-mix(in srgb, var(--pf-warm-line) 20%, var(--background-modifier-border)); + box-shadow: 0 1px 2px rgba(20, 18, 14, 0.04), 0 8px 20px rgba(20, 18, 14, 0.04); +} + +.paperforge-collection-view .paperforge-ocr-count-label { font-size: 11px; } -/* ========================================================================== - SECTION 44 — Dark Theme Specific Overrides - ========================================================================== */ - -.theme-dark .paperforge-paper-overview, -.theme-dark .paperforge-discussion-card, -.theme-dark .paperforge-workflow-overview, +.theme-dark .paperforge-collection-view .paperforge-ocr-section, .theme-dark .paperforge-library-snapshot, .theme-dark .paperforge-system-status, .theme-dark .paperforge-global-actions, -.theme-dark .paperforge-workflow-toggles { - background: var(--background-primary); - border-color: var(--background-modifier-border); +.theme-dark .paperforge-workflow-overview { + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3); } -.theme-dark .paperforge-paper-overview::before, -.theme-dark .paperforge-discussion-card::before, -.theme-dark .paperforge-workflow-overview::before, -.theme-dark .paperforge-library-snapshot::before, -.theme-dark .paperforge-system-status::before, -.theme-dark .paperforge-global-actions::before, -.theme-dark .paperforge-workflow-toggles::before { - box-shadow: 0 2px 12px rgba(0,0,0,0.3); +/* Dark theme overrides */ +.theme-dark .paperforge-status-pill.ok { + background: rgba(100, 200, 120, 0.15); +} + +.theme-dark .paperforge-status-pill.fail { + background: rgba(255, 100, 100, 0.15); +} + +.theme-dark .paperforge-library-snapshot:hover, +.theme-dark .paperforge-system-status:hover, +.theme-dark .paperforge-global-actions:hover, +.theme-dark .paperforge-workflow-overview:hover, +.theme-dark .paperforge-workflow-toggles:hover { + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3); +} + +.theme-dark .paperforge-paper-overview:hover { + box-shadow: inset 3px 0 0 color-mix(in srgb, var(--pf-paper-accent) 22%, transparent), 0 2px 12px rgba(0, 0, 0, 0.3); +} + +.theme-dark .paperforge-discussion-card:hover { + box-shadow: inset 3px 0 0 color-mix(in srgb, var(--pf-paper-accent) 16%, transparent), 0 2px 12px rgba(0, 0, 0, 0.3); } diff --git a/paperforge/setup_wizard.py b/paperforge/setup_wizard.py index f6f58a8a..6bd28b01 100644 --- a/paperforge/setup_wizard.py +++ b/paperforge/setup_wizard.py @@ -1012,7 +1012,7 @@ def headless_setup( "OCR dir": (pf_path / "ocr").exists(), "paperforge.json": pf_json.exists(), "Obsidian plugin": (vault / ".obsidian" / "plugins" / "paperforge" / "main.js").exists(), - "AGENTS.md": True if not agents_src.exists() else agents_dst.exists(), + "AGENTS.md": (vault / "AGENTS.md").exists(), } failed = [k for k, v in checks.items() if not v] if failed: diff --git a/paperforge/skills/literature-qa/SKILL.md b/paperforge/skills/literature-qa/SKILL.md new file mode 100644 index 00000000..2ad91877 --- /dev/null +++ b/paperforge/skills/literature-qa/SKILL.md @@ -0,0 +1,80 @@ +--- +name: literature-qa +description: > + 学术文献精读与问答。MUST trigger when user types /pf-deep, /pf-paper, /pf-end, + or says "精读", "深度阅读", "读一下", "查一下这篇论文", "帮我看看这篇文章", + "这篇文章讲了什么", "保存讨论", "结束讨论", "做这篇文献的问答", + or any phrase about reading/analyzing papers in their Zotero library. + 支持 Zotero key, DOI, 标题, 作者/年份, 自然语言描述定位论文. +license: Apache-2.0 +compatibility: opencode +--- + +# Literature QA — 学术文献精读与问答 + +## 路由表 + +Agent 读到本文件后,首先根据用户意图路由到对应的 reference 文件: + +| 用户意图 | 典型输入 | 加载文件 | +|---------|---------|---------| +| 三阶段精读(指定论文) | `/pf-deep <query>`, `pf-deep <query>`, "精读 XXX", "深度阅读 XXX" | [references/deep-reading.md](references/deep-reading.md) | +| 三阶段精读(查看队列) | `/pf-deep`(无参数), "精读队列", "有哪些该读了" | [references/deep-reading.md](references/deep-reading.md) | +| 论文问答 | `/pf-paper <query>`, `pf-paper <query>`, "做这篇的问答", "帮我看看 XXX" | [references/paper-qa.md](references/paper-qa.md) | +| 保存讨论记录 | `/pf-end`, `pf-end`, "保存", "结束讨论", "完成讨论" | [references/save-session.md](references/save-session.md) | + +> **重要:** 加载 reference 文件后,严格按照该文件的流程执行。不要跳过任何步骤。 + +## 论文定位(所有路由共用,先执行) + +详见 [references/paper-resolution.md](references/paper-resolution.md)。 + +**核心原则:二路定位。Python 干确定的活,Agent 干理解的活。路径全在环境变量里,零硬编码。** + +### 第零步:加载环境变量(每条路由启动时跑一次) + +```pwsh +python -m paperforge.worker.paper_resolver env --vault . --shell pwsh | Invoke-Expression +``` + +此后所有路径用环境变量引用,不再拼接: + +| 变量 | 含义 | +| ------------------ | --------------------------- | +| `$env:PF_VAULT` | vault 根目录 | +| `$env:PF_INDEX_PATH` | formal-library.json 路径 | +| `$env:PF_LITERATURE_DIR` | formal notes 目录 | +| `$env:PF_OCR_DIR` | OCR 结果目录 | + +### 第一步:判断输入类型,选择路径 + +| 输入特征 | 执行命令 | +|---------|---------| +| 8位 key | `python -m paperforge.worker.paper_resolver resolve-key <KEY> --vault "$env:PF_VAULT"` | +| DOI | `python -m paperforge.worker.paper_resolver resolve-doi "<DOI>" --vault "$env:PF_VAULT"` | +| 标题片段 | `python -m paperforge.worker.paper_resolver search --title "..." --vault "$env:PF_VAULT"` | +| 作者+年份 | `python -m paperforge.worker.paper_resolver search --author "Smith" --year 2024 --vault "$env:PF_VAULT"` | +| 自然语言 | Agent 读 `$env:PF_INDEX_PATH` 指向的 formal-library.json | + +### 第二步:处理结果 + +- **Python 返回匹配:** 直接使用返回的 workspace(`formal_note_path` 等由 config 动态计算) +- **Python 搜不到:** Agent grep fallback:`rg -l "zotero_key:.*ABC" "$env:PF_LITERATURE_DIR/"` +- **自然语言搜不到:** 告知用户 "未找到,请确认或先运行 `paperforge sync`" +- **命中多篇:** 列出候选清单让用户选 + +## 文件结构 + +``` +literature-qa/ +├── SKILL.md ← 本文件(路由入口) +├── references/ +│ ├── deep-reading.md ← 精读工作流 +│ ├── paper-qa.md ← 问答工作流 +│ ├── save-session.md ← 保存记录工作流 +│ ├── paper-resolution.md ← 论文定位详细协议 +│ ├── deep-subagent.md ← 子代理提示词模板 +│ └── chart-reading/ ← 19 个图表类型阅读指南 +└── scripts/ + └── ld_deep.py ← 精读引擎(Python) +``` diff --git a/paperforge/skills/literature-qa/chart-reading/GSEA富集图.md b/paperforge/skills/literature-qa/references/chart-reading/GSEA富集图.md similarity index 100% rename from paperforge/skills/literature-qa/chart-reading/GSEA富集图.md rename to paperforge/skills/literature-qa/references/chart-reading/GSEA富集图.md diff --git a/paperforge/skills/literature-qa/chart-reading/INDEX.md b/paperforge/skills/literature-qa/references/chart-reading/INDEX.md similarity index 100% rename from paperforge/skills/literature-qa/chart-reading/INDEX.md rename to paperforge/skills/literature-qa/references/chart-reading/INDEX.md diff --git a/paperforge/skills/literature-qa/chart-reading/ROC与PR曲线.md b/paperforge/skills/literature-qa/references/chart-reading/ROC与PR曲线.md similarity index 100% rename from paperforge/skills/literature-qa/chart-reading/ROC与PR曲线.md rename to paperforge/skills/literature-qa/references/chart-reading/ROC与PR曲线.md diff --git a/paperforge/skills/literature-qa/chart-reading/Western Blot条带图.md b/paperforge/skills/literature-qa/references/chart-reading/Western Blot条带图.md similarity index 100% rename from paperforge/skills/literature-qa/chart-reading/Western Blot条带图.md rename to paperforge/skills/literature-qa/references/chart-reading/Western Blot条带图.md diff --git a/paperforge/skills/literature-qa/chart-reading/免疫荧光定量图.md b/paperforge/skills/literature-qa/references/chart-reading/免疫荧光定量图.md similarity index 100% rename from paperforge/skills/literature-qa/chart-reading/免疫荧光定量图.md rename to paperforge/skills/literature-qa/references/chart-reading/免疫荧光定量图.md diff --git a/paperforge/skills/literature-qa/chart-reading/折线图与时间序列.md b/paperforge/skills/literature-qa/references/chart-reading/折线图与时间序列.md similarity index 100% rename from paperforge/skills/literature-qa/chart-reading/折线图与时间序列.md rename to paperforge/skills/literature-qa/references/chart-reading/折线图与时间序列.md diff --git a/paperforge/skills/literature-qa/chart-reading/散点图与气泡图.md b/paperforge/skills/literature-qa/references/chart-reading/散点图与气泡图.md similarity index 100% rename from paperforge/skills/literature-qa/chart-reading/散点图与气泡图.md rename to paperforge/skills/literature-qa/references/chart-reading/散点图与气泡图.md diff --git a/paperforge/skills/literature-qa/chart-reading/显微照片与SEM图.md b/paperforge/skills/literature-qa/references/chart-reading/显微照片与SEM图.md similarity index 100% rename from paperforge/skills/literature-qa/chart-reading/显微照片与SEM图.md rename to paperforge/skills/literature-qa/references/chart-reading/显微照片与SEM图.md diff --git a/paperforge/skills/literature-qa/chart-reading/条形图与误差棒.md b/paperforge/skills/literature-qa/references/chart-reading/条形图与误差棒.md similarity index 100% rename from paperforge/skills/literature-qa/chart-reading/条形图与误差棒.md rename to paperforge/skills/literature-qa/references/chart-reading/条形图与误差棒.md diff --git a/paperforge/skills/literature-qa/chart-reading/桑基图与弦图.md b/paperforge/skills/literature-qa/references/chart-reading/桑基图与弦图.md similarity index 100% rename from paperforge/skills/literature-qa/chart-reading/桑基图与弦图.md rename to paperforge/skills/literature-qa/references/chart-reading/桑基图与弦图.md diff --git a/paperforge/skills/literature-qa/chart-reading/森林图与Meta分析.md b/paperforge/skills/literature-qa/references/chart-reading/森林图与Meta分析.md similarity index 100% rename from paperforge/skills/literature-qa/chart-reading/森林图与Meta分析.md rename to paperforge/skills/literature-qa/references/chart-reading/森林图与Meta分析.md diff --git a/paperforge/skills/literature-qa/chart-reading/火山图与曼哈顿图.md b/paperforge/skills/literature-qa/references/chart-reading/火山图与曼哈顿图.md similarity index 100% rename from paperforge/skills/literature-qa/chart-reading/火山图与曼哈顿图.md rename to paperforge/skills/literature-qa/references/chart-reading/火山图与曼哈顿图.md diff --git a/paperforge/skills/literature-qa/chart-reading/热图与聚类图.md b/paperforge/skills/literature-qa/references/chart-reading/热图与聚类图.md similarity index 100% rename from paperforge/skills/literature-qa/chart-reading/热图与聚类图.md rename to paperforge/skills/literature-qa/references/chart-reading/热图与聚类图.md diff --git a/paperforge/skills/literature-qa/chart-reading/生存曲线.md b/paperforge/skills/literature-qa/references/chart-reading/生存曲线.md similarity index 100% rename from paperforge/skills/literature-qa/chart-reading/生存曲线.md rename to paperforge/skills/literature-qa/references/chart-reading/生存曲线.md diff --git a/paperforge/skills/literature-qa/chart-reading/箱式图与小提琴图.md b/paperforge/skills/literature-qa/references/chart-reading/箱式图与小提琴图.md similarity index 100% rename from paperforge/skills/literature-qa/chart-reading/箱式图与小提琴图.md rename to paperforge/skills/literature-qa/references/chart-reading/箱式图与小提琴图.md diff --git a/paperforge/skills/literature-qa/chart-reading/组织学半定量图.md b/paperforge/skills/literature-qa/references/chart-reading/组织学半定量图.md similarity index 100% rename from paperforge/skills/literature-qa/chart-reading/组织学半定量图.md rename to paperforge/skills/literature-qa/references/chart-reading/组织学半定量图.md diff --git a/paperforge/skills/literature-qa/chart-reading/网络图与通路图.md b/paperforge/skills/literature-qa/references/chart-reading/网络图与通路图.md similarity index 100% rename from paperforge/skills/literature-qa/chart-reading/网络图与通路图.md rename to paperforge/skills/literature-qa/references/chart-reading/网络图与通路图.md diff --git a/paperforge/skills/literature-qa/chart-reading/蛋白质结构图.md b/paperforge/skills/literature-qa/references/chart-reading/蛋白质结构图.md similarity index 100% rename from paperforge/skills/literature-qa/chart-reading/蛋白质结构图.md rename to paperforge/skills/literature-qa/references/chart-reading/蛋白质结构图.md diff --git a/paperforge/skills/literature-qa/chart-reading/降维图(PCA-tSNE-UMAP).md b/paperforge/skills/literature-qa/references/chart-reading/降维图(PCA-tSNE-UMAP).md similarity index 100% rename from paperforge/skills/literature-qa/chart-reading/降维图(PCA-tSNE-UMAP).md rename to paperforge/skills/literature-qa/references/chart-reading/降维图(PCA-tSNE-UMAP).md diff --git a/paperforge/skills/literature-qa/chart-reading/雷达图与漏斗图.md b/paperforge/skills/literature-qa/references/chart-reading/雷达图与漏斗图.md similarity index 100% rename from paperforge/skills/literature-qa/chart-reading/雷达图与漏斗图.md rename to paperforge/skills/literature-qa/references/chart-reading/雷达图与漏斗图.md diff --git a/paperforge/skills/literature-qa/references/deep-reading.md b/paperforge/skills/literature-qa/references/deep-reading.md new file mode 100644 index 00000000..3563a5d7 --- /dev/null +++ b/paperforge/skills/literature-qa/references/deep-reading.md @@ -0,0 +1,171 @@ +# 三阶段精读 + +Keshav 三阶段组会式精读。触发后执行以下工作流。 + +> **路径说明:** 本文件中的 `scripts/ld_deep.py` 相对于本 skill 目录。Agent 运行时 skill 目录由平台注入(通常为 `<installation_path>/paperforge/skills/literature-qa/`)。如不确定,AI 应从 SKILL.md 所在目录推断。 + +--- + +## 前置条件检查 + +执行前确认: +- [ ] 已完成论文定位(参考 [paper-resolution.md](paper-resolution.md)),拿到 zotero_key 和 workspace +- [ ] `analyze: true`(在 formal note frontmatter 中,resolver 返回的 workspace 里可查到) +- [ ] `ocr_status: done`(在 resolver 返回的 workspace 里可查到) + +如果前置条件不满足,告知用户并停止。 + +--- + +## 执行流程 + +### Step 1: Prepare(机械操作,跑脚本) + +```bash +python scripts/ld_deep.py prepare --vault . --key <ZOTERO_KEY> +``` + +返回 JSON 解析: +- `status: "ok"` → 继续 +- `status: "error"` → 报告 `message` 给用户,停止 + +Prepare 做的事情(Agent 不需要关心细节): +- 检查 analyze 和 ocr_status +- 生成 figure-map.json 和 chart-type-map.json +- 在 formal note 中插入 `## 🔍 精读` 骨架 + +读 formal note 确认骨架已插入。 + +--- + +### Step 2: Pass 1 — 概览 + +只填 `### Pass 1: 概览` 区域。不碰 Pass 2/3。 + +**填写内容:** + +- **一句话总览**:论文类型 + 核心发现,一句话。 +- **5 Cs 快速评估**: + - **Category**(类型):RCT / 队列研究 / 病例对照 / 综述 / 基础研究 / ... + - **Context**(上下文):该领域当前共识,本文要解决什么问题 + - **Correctness**(合理性初判):初步直觉,逻辑是否有明显漏洞 + - **Contributions**(贡献):1-3 条 + - **Clarity**(清晰度):写作质量,图表可读性 +- **Figure 导读**(基于 fulltext.md 浏览各图 caption): + - 关键主图:列出并一句话概括每个主图要证明什么 + - 证据转折点:哪个 figure 是叙事的关键转折 + - 需要重点展开的 supplementary:如果有 + - 关键表格:列出 + +填完立即保存 formal note。 + +--- + +### Step 3: Pass 2 — 精读还原 + +填 `### Pass 2: 精读还原` 区域。**按 figure 顺序逐个处理。** + +#### 图表类型定位(两步) + +**Step A: Python 给建议(快速初筛)** +```bash +python scripts/ld_deep.py chart-type-scan --vault . --key <ZOTERO_KEY> +``` +输出每个 figure 的关键词命中结果。这只是建议,Agent 不要盲信。 + +**Step B: Agent 读 caption 做最终判断** + +对每个 figure: +1. 读该 figure 的 caption(来自 fulltext.md 或 figure-map.json) +2. 根据 caption 内容,对照 [chart-reading/INDEX.md](chart-reading/INDEX.md) 判断图表类型 +3. 如果 Python 建议和 Agent 判断不一致 → 以 Agent 判断为准 +4. 如果无法确定类型 → 跳过 chart guide,按通用 figure 结构分析 +5. 确定类型后,读对应的 chart-reading 指南(如 `chart-reading/条形图与误差棒.md`),按指南中的检查清单分析 + +#### 每张 Figure 的子标题(固定,不可少) + +按以下格式填入 formal note 中该 figure 的 callout block: + +``` +**图像定位与核心问题**:页码 + 要回答什么问题 +**方法与结果**:实验设计/数据来源/技术手段。核心数据、趋势、对比。 +**图表质量审查**:按 chart-reading 指南检查坐标轴、单位、误差棒、统计标注等。 +**作者解释**:作者在正文中对该图的解读 +**我的理解**:自己的理解(区分于作者解释) +**疑点/局限**:读图时发现的疑问,用 `> [!warning]` 突出 +``` + +#### 每张 Table 的子标题 + +``` +回答什么问题、关键字段/分组、主要结果、我的理解、疑点/局限 +``` + +#### 每张 figure 填完立即保存,再处理下一张。 + +#### 所有 figure/table 处理完后,填: + +**关键方法补课**:简要解释不熟悉的实验技术(1-2 项即可) + +**主要发现与新意**: +- 发现 1:...(来源:Figure X) +- 发现 2:...(来源:Figure Y / Table Z) + +保存。 + +--- + +### Step 4: Postprocess(跑校验脚本,修正问题) + +```bash +python scripts/ld_deep.py postprocess-pass2 <formal_note_path> --figures <N> --format text +``` + +- 输出 `OK` → 继续 +- 输出错误 → 按错误提示修正(包含行号),修正后重新跑 +- 最多 3 轮修正。3 轮后仍失败 → 报告剩余错误给用户 + +--- + +### Step 5: Pass 3 — 深度理解 + +填 `### Pass 3: 深度理解` 区域。基于 Pass 1/2 已写的内容。 + +**填写内容:** + +- **假设挑战与隐藏缺陷**:隐含假设;如果放宽某个假设结论还成立吗;缺少哪些关键引用;实验/分析技术潜在问题 +- **哪些结论扎实,哪些仍存疑**: + - **较扎实**:... + - **仍存疑**:...(用 `> [!warning]`) +- **Discussion 与 Conclusion 怎么读**:作者真正完成了什么;哪些地方有拔高;哪些是推测 +- **对我的启发**:研究设计上;figure 组织上;方法组合上;未来工作想法 +- **遗留问题**:...(用 `> [!question]`) + +保存。 + +--- + +### Step 6: Final Validation + +```bash +python scripts/ld_deep.py validate-note <formal_note_path> --fulltext <fulltext_path> +``` + +- 输出 `OK` → 告知用户精读完成 +- 输出错误 → 修正缺失项,不报告成功直到通过 + +--- + +## Callout 格式规则 + +- `> [!important]`:每个 main finding +- `> [!warning]`:疑问、局限、证据边界、仍存疑条目 +- `> [!question]`:遗留问题 +- **间距:** 相邻 callout block 之间必须有空行,否则 Obsidian 会合并 + - 正确:`> [!important] A\n\n> [!important] B` + - 错误:`> [!important] A\n> [!important] B` + +## Supplementary 规则 + +- 默认不逐张展开 supplementary figure/table +- 仅在以下情况纳入:对主结论形成关键支撑、补足方法可信度、限制主文结论解释范围、作者在正文中明显依赖该补充材料 diff --git a/paperforge/skills/literature-qa/prompt_deep_subagent.md b/paperforge/skills/literature-qa/references/deep-subagent.md similarity index 100% rename from paperforge/skills/literature-qa/prompt_deep_subagent.md rename to paperforge/skills/literature-qa/references/deep-subagent.md diff --git a/paperforge/skills/literature-qa/references/paper-qa.md b/paperforge/skills/literature-qa/references/paper-qa.md new file mode 100644 index 00000000..626f23e0 --- /dev/null +++ b/paperforge/skills/literature-qa/references/paper-qa.md @@ -0,0 +1,61 @@ +# 论文问答 + +交互式论文 Q&A 工作台。不强制要求 OCR,但 OCR 完成后回答更准确。 + +--- + +## 前置条件 + +- [ ] 已完成论文定位(参考 [paper-resolution.md](paper-resolution.md)),拿到 zotero_key 和 workspace +- [ ] OCR 完成(推荐但非强制) + +--- + +## 执行流程 + +### Step 1: 加载论文 + +1. 确认 workspace 路径 +2. 读 `fulltext.md`(如果存在)作为主要回答依据 +3. 读 formal note frontmatter 获取元数据(标题、作者、期刊、年份) +4. 如果 fulltext.md 不存在,告知用户 "OCR 文本不可用,回答将基于元数据和公开信息" + +### Step 2: 显示论文信息 + +``` +已加载论文: [title] ([year], [journal]) +作者: [authors] +Zotero Key: [key] +领域: [domain] +OCR 状态: [done / 不可用] +结束对话时说 "保存" 即可保存讨论。 +请问有什么问题? +``` + +### Step 3: 进入 Q&A 模式 + +- 等待用户提问 +- 每次回答后等待下一个问题 +- 持续到用户说 "保存"、"结束"、"完成" 等关键词 + +--- + +## 回答原则 + +- **严格基于** fulltext.md 中的文本内容回答 +- 引用原文时标注来源页码/章节(如 "第 3 页,Methods 部分") +- 用中文(简体中文)回答 +- 论文中未提及的内容,明确说明 "论文中未提及该内容" +- 需要结合论文以外知识的问题,说明 "该问题需要结合论文以外的知识" + +--- + +## 切换模式 + +用户在当前对话中可以说 "精读这篇文章" 切换到 deep-reading 模式。此时加载 [deep-reading.md](deep-reading.md) 执行精读流程。 + +--- + +## 保存记录 + +用户说 "保存"、"结束"、"完成"、"保存讨论" 时,加载 [save-session.md](save-session.md) 执行保存。不要自动保存。 diff --git a/paperforge/skills/literature-qa/references/paper-resolution.md b/paperforge/skills/literature-qa/references/paper-resolution.md new file mode 100644 index 00000000..763425eb --- /dev/null +++ b/paperforge/skills/literature-qa/references/paper-resolution.md @@ -0,0 +1,173 @@ +# 论文定位协议 + +本文件定义如何将用户输入(key / DOI / 标题 / 作者年份 / 自然语言)解析为论文 workspace。所有子流程(deep-reading, paper-qa, save-session)共用此协议。 + +## 核心原则 + +1. **确定性输入走 Python。** key、DOI、标题片段、作者+年份 —— 这些是机器能精确处理的。 +2. **自然语言走 Agent。** "关于骨再生的那篇"、"去年那篇Nature" —— 需要 AI 理解语义。 +3. **Python 搜不到时 Agent 兜底。** 不是报错,是换个方式再试一次。 +4. **所有路径从环境变量获取。** 每条路由启动时先跑 `python -m paperforge.worker.paper_resolver env --vault . --shell pwsh | Invoke-Expression`。此后 `$env:PF_VAULT`、`$env:PF_LITERATURE_DIR`、`$env:PF_OCR_DIR`、`$env:PF_INDEX_PATH` 全程可用。不写死任何目录名。 + +--- + +## 前置:获取 vault 路径配置(每个路由启动时跑一次) + +```bash +python -m paperforge.worker.paper_resolver paths --vault . +``` + +返回示例(所有路径均由 `paperforge.json` 动态计算): +```json +{ + "ok": true, + "data": { + "vault_root": "/path/to/vault", + "index_path": "<system_dir>/PaperForge/indexes/formal-library.json", + "literature_dir": "<resources_dir>/<literature_dir>", + "ocr_dir": "<system_dir>/PaperForge/ocr" + } +} +``` + +**后续所有路径操作使用此输出中的值,不要自己拼接。** + +--- + +## 输入类型判断 + +### 类型 1: Zotero Key(8位字符) + +**识别规则:** 8位字母数字组合,如 `XGT9Z257`、`ABC12345` + +**执行命令:** +```bash +python -m paperforge.worker.paper_resolver resolve-key <KEY> --vault . +``` + +**返回示例(路径由 paperforge.json 配置决定,不固定):** +```json +{ + "ok": true, + "data": { + "match": { + "key": "ABC12345", + "title": "TGF-beta in Bone Regeneration", + "domain": "骨科", + "formal_note_path": "...", + "ocr_path": "...", + "fulltext_path": "...", + "ocr_status": "done" + } + } +} +``` + +**返回 `"match": null` 时:** Agent fallback — 用 `paths` 命令获取的 `literature_dir` 路径 grep frontmatter: +```bash +rg -l "zotero_key:.*ABC12345" <literature_dir>/ +``` + +--- + +### 类型 2: DOI + +**识别规则:** 以 `10.` 开头的标准 DOI 格式,可能带 URL 前缀 + +**执行命令:** +```bash +python -m paperforge.worker.paper_resolver resolve-doi "10.1016/j.jse.2018.01.001" --vault . +``` + +**返回格式同类型1。** 返回的路径直接使用,不自己拼接。 + +--- + +### 类型 3: 标题片段 + +**识别规则:** 看起来像论文标题的文本(含学术关键词,非自然语言问句) + +**执行命令:** +```bash +python -m paperforge.worker.paper_resolver search --title "Predictive findings on MRI" --vault . +``` + +**返回示例:** +```json +{ + "ok": true, + "data": { + "matches": [{ "key": "...", "title": "...", "formal_note_path": "...", ... }], + "count": 3 + } +} +``` + +--- + +### 类型 4: 作者 + 年份 + +**识别规则:** 包含作者名(英文姓)和年份 + +**执行命令:** +```bash +python -m paperforge.worker.paper_resolver search --author "Smith" --year 2024 --vault . +``` + +--- + +### 类型 5: 自然语言 + +**识别规则:** 中文自然语言描述,如 "关于骨再生的那篇"、"去年Nature上那篇讲TGF的" + +**Agent 操作:** +1. 用 `paths` 命令获取的 `index_path` 读 `formal-library.json` +2. 理解用户意图中的关键信息:主题词、年份、期刊、领域 +3. 在 JSON 的 `title`、`domain`、`journal`、`abstract` 字段中搜索匹配 +4. 如果 formal-library.json 无结果,用 `paths` 命令获取的 `literature_dir` grep formal notes: + ```bash + rg -i -l "骨再生|bone regeneration" <literature_dir>/ --include "*.md" -g "!*.canvas" + ``` +5. 读匹配的 frontmatter 确认是目标论文 + +--- + +## 多篇匹配处理 + +当搜索返回多个匹配时,Agent 必须列出候选清单,让用户选择: + +``` +找到 3 篇匹配的论文: + +[1] ABC12345 — TGF-beta in Bone Regeneration (2024, 骨科, OCR: done) +[2] DEF67890 — Bone Healing After Fracture (2023, 骨科, OCR: pending) +[3] GHI11111 — Scaffold Design for Bone Repair (2024, 骨科, OCR: done) + +请输入编号选择,或 refine 搜索词。 +``` + +**格式要求:** +- 编号 + key + title + (year, domain, ocr_status) +- 不要只列标题或只列 key + +--- + +## Fallback 顺序 + +``` +输入 + │ + ├── 看起来像 key/DOI/标题/作者年份? + │ └── YES → Python paper_resolver + │ ├── 有结果 → 使用 + │ └── 无结果 → Agent grep fallback + │ ├── 有结果 → 使用 + │ └── 无结果 → 告知用户 + │ + └── 看起来像自然语言? + └── Agent 读 formal-library.json + ├── 有结果 → 列出/使用 + └── 无结果 → Agent grep fallback + ├── 有结果 → 使用 + └── 无结果 → 告知用户 +``` diff --git a/paperforge/skills/literature-qa/references/save-session.md b/paperforge/skills/literature-qa/references/save-session.md new file mode 100644 index 00000000..de023e21 --- /dev/null +++ b/paperforge/skills/literature-qa/references/save-session.md @@ -0,0 +1,55 @@ +# 保存讨论记录 + +将 paper-qa 会话中的 Q&A 记录持久化到论文工作区。 + +--- + +## 触发条件 + +- 用户显式说 "保存"、"保存记录"、"结束"、"完成讨论"、"save" +- 或显式输入 `/pf-end` +- 不要自动触发 + +--- + +## 执行 + +### Step 1: 收集 Q&A 对 + +汇总本次 paper-qa 会话中所有 Q&A,序列化为 JSON 数组: + +```json +[ + { + "question": "用户的问题", + "answer": "Agent 的回答", + "source": "user_question", + "timestamp": "2026-05-10T12:00:00+08:00" + } +] +``` + +`source` 为 `"user_question"`(用户提问)或 `"agent_analysis"`(Agent 主动分析)。 + +### Step 2: 调用 discussion 模块 + +```bash +python -m paperforge.worker.discussion record <ZOTERO_KEY> \ + --vault . \ + --agent pf-paper \ + --model "<CURRENT_MODEL>" \ + --qa-pairs '<JSON_ARRAY>' +``` + +### Step 3: 确认结果 + +CLI 返回 `{"status": "ok", ...}` → 告知用户记录已保存。 + +返回 `{"status": "error"}` → 记录错误,重试一次。仍失败则告知用户。 + +--- + +## 注意事项 + +- 仅 paper-qa 会话需要记录。deep-reading 的内容直接写入 formal note,不需要通过本文件。 +- 如果无法从 formal-library.json 找到论文 domain/title,记录失败不应影响用户使用。 diff --git a/paperforge/skills/literature-qa/scripts/pf-deep.md b/paperforge/skills/literature-qa/scripts/pf-deep.md deleted file mode 100644 index 3d2af01c..00000000 --- a/paperforge/skills/literature-qa/scripts/pf-deep.md +++ /dev/null @@ -1,237 +0,0 @@ ---- -name: pf-deep -description: Complete three-pass deep reading of an academic paper (Keshav method). Requires OCR fulltext. Searches by Zotero key, title, DOI, or PMID. -allowed-tools: [Read, Bash, Edit] ---- - -# <prefix>pf-deep - -## Purpose - -基于单篇论文的组会式精读入口。 - -1. 解析 `<prefix>pf-deep <query>` 中的查询词 -2. 支持 Zotero key、标题片段、DOI、PMID、关键词 -3. 优先搜索本地 Zotero 并锁定单篇论文 -4. 绑定该论文对应的: - - `<system_dir>/PaperForge/ocr/<KEY>/fulltext.md` - - `<system_dir>/PaperForge/ocr/<KEY>/meta.json` - - `<resources_dir>/<literature_dir>/.../KEY - Title.md` -5. 在正式文献卡片中检查或创建 `## 精读` -6. 以"研究思路 + figure-by-figure"方式一次性完成精读写回 - -## CLI Equivalent - -```bash -# 准备阶段(间接) -python .opencode/skills/pf-deep/scripts/ld_deep.py prepare --vault "<VAULT_PATH>" --key <ZOTERO_KEY> -# 返回 JSON:{status, formal_note, fulltext_md, figures, tables} -``` - -> `<prefix>pf-deep` 是 **Agent 层命令**,通过 Python 代码自动检测论文状态,无需先行 CLI 准备。 - -## Detection(自动检测,无需手动 sync) - -启动时,Agent 执行以下 Python 检测命令,代码会自动判断是否需要精读: - -```bash -python .opencode/skills/pf-deep/scripts/ld_deep.py prepare --vault "<VAULT_PATH>" --key <ZOTERO_KEY> -``` - -返回 JSON: -- `status: "ok"` → 就绪,可以开始精读 -- `status: "error"` → 被阻塞(`message` 说明原因:analyze=false / OCR 未完成 / 未找到论文) - -Agent 根据返回的 `status` 决定是否进入精读流程,不自行读取 frontmatter。 - -**队列模式**(无参数时自动检测):Agent 运行: -```bash -python .opencode/skills/pf-deep/scripts/ld_deep.py queue --vault "<VAULT_PATH>" -``` -代码自动扫描 canonical index 中 `analyze=true` 且 `deep_reading_status=pending` 的论文,按 OCR 状态分组。 - -## Arguments - -| 参数 | 必需 | 说明 | -|------|------|------| -| `<query>` | 是(queue 模式除外) | Zotero key、标题片段、DOI、PMID 或关键词 | -| `queue` | 否 | 启动批量精读队列模式 | - -### 参数说明 - -1. 如果输入看起来像 8 位 Zotero key,则直接按 key 解析。 -2. 否则先在本地 Zotero 中搜索标题/摘要。 -3. 若命中唯一结果或明显最佳结果,则直接载入。 -4. 若存在多个合理候选,则先列候选清单再让用户选。 -5. 不要强迫用户先知道 Zotero key。 - -## Example - -### 单篇精读(已知 key) - -```bash -<prefix>pf-deep XGT9Z257 -<prefix>pf-deep Predictive findings on magnetic resonance imaging -<prefix>pf-deep 10.1016/j.jse.2018.01.001 -``` - -### 无参数:自动检测队列 - -```bash -<prefix>pf-deep -``` - -当不提供具体 key/标题时,agent 自动检测精读队列: - -1. 运行 `python .opencode/skills/pf-deep/scripts/ld_deep.py queue --vault "<VAULT_PATH>"` 扫描队列 -2. 解析输出的 JSON,按 OCR 状态分组展示 -3. 由用户选择篇目 - -无需先跑 `paperforge sync`。 - -## Output - -Agent 在正式笔记中创建或更新 `## 精读` 区域,包含: - -- **Pass 1: 概览** — 一句话总览、5 Cs 快速评估、Figure 导读 -- **Pass 2: 精读还原** — Figure-by-Figure 解析、Table-by-Table 解析、关键方法补课、主要发现与新意 -- **Pass 3: 深度理解** — 假设挑战与隐藏缺陷、结论扎实性评估、Discussion 解读、个人启发、遗留问题 - -## Error Handling - -### OCR 未完成 -- **表现**:Agent 提示 `ocr_status` 不是 `done` -- **解决**:先运行 `paperforge ocr`,确认 `meta.json` 中 `ocr_status` 变为 `done` - -### 内容已存在(覆盖确认) -- **表现**:正式笔记中已存在 `## 精读` 区域且包含非占位符的实质内容 -- **处理**:Agent **必须**询问用户:追加 / 覆盖 / 跳过 - -### 未找到论文 -- **表现**:Zotero key 无效或搜索无结果 -- **解决**:确认 key 正确,或尝试用标题片段搜索 - -## 精读结构参考 - -### 执行原则 - -- `<prefix>pf-deep` 对用户来说是一次触发直接完成。 -- 内部逻辑分两步: - 1. 先生成 `## 精读` 骨架和 figure 标题位 - 2. 再补全所有空段 -- 后续再次运行时(用户选择追加): - - 只补空段,不覆盖已有内容 - -### 精读定位 - -这不是综述提取,也不是信息摘录。目标是模拟高水平博士/博士后组会讲解单篇论文的学习型精读。 - -主线必须是: - -1. 文章整体研究思路 -2. 主文 figure 逐张解析 -3. 关键方法补课 -4. 主要发现、新意、疑点与启发 - -### Supplementary 规则 - -- 默认不逐张展开 supplementary figure/table。 -- 仅在以下情况下纳入: - - 对主结论形成关键支撑 - - 补足方法可信度 - - 限制主文结论的解释范围 - - 作者在正文中明显依赖该补充材料 - -### 标准骨架 - -```md -## 精读 - -**证据边界**:区分三层信息:`论文结果`、`作者解释`、`我的理解/推断`。 - -### Pass 1: 概览 - -**一句话总览** -(待补充) - -**5 Cs 快速评估** -- **Category**(类型): -- **Context**(上下文): -- **Correctness**(合理性初判): -- **Contributions**(贡献): -- **Clarity**(清晰度): - -**Figure 导读** -- 关键主图: -- 证据转折点: -- 需要重点展开的 supplementary: -- 关键表格: - -### Pass 2: 精读还原 - -#### Figure-by-Figure 解析 -(每张 figure 下方按以下顺序填写) -- **图像定位与核心问题**:页码 + 要回答什么问题 -- **方法与结果**:方法 + 结果 -- **作者解释**:作者对该图的解读 -- **我的理解**:自己的理解(区分于作者解释) -- **在全文中的作用**:该图在整体故事线中的位置 -- **疑点 / 局限**:读图时发现的疑问 - -#### Table-by-Table 解析 -(如有重要表格,按同样结构展开) - -#### 关键方法补课 -- 方法 1: -- 方法 2: - -#### 主要发现与新意 -**主要发现** -- 发现 1: -- 发现 2: - -### Pass 3: 深度理解 - -#### 假设挑战与隐藏缺陷 -- 隐含假设: -- 如果放宽某个假设,结论还成立吗? -- 缺少哪些关键引用? -- 实验/分析技术的潜在问题: - -#### 哪些结论扎实,哪些仍存疑 -**较扎实** -- - -**仍存疑** -- - -#### Discussion 与 Conclusion 怎么读 -- 作者真正完成了什么: -- 哪些地方有拔高: -- 哪些地方是推测: - -#### 对我的启发 -- 研究设计上: -- figure 组织上: -- 方法组合上: -- 未来工作想法: - -#### 遗留问题 -**遗留问题** -- -``` - -### Figure 节要求 - -每个 figure 小节按以下顺序填写: - -- **图像定位与核心问题**:页码 + 要回答什么问题 -- **方法与结果**:方法 + 结果 -- **作者解释**:作者对该图的解读 -- **我的理解**:自己的理解(区分于作者解释) -- **在全文中的作用**:该图在整体故事线中的位置 -- **疑点 / 局限**:读图时发现的疑问(可酌情用 `> [!warning]` 突出) - -## See Also - -- [pf-paper](pf-paper.md) — 快速摘要与问答 diff --git a/paperforge/skills/literature-qa/scripts/pf-end.md b/paperforge/skills/literature-qa/scripts/pf-end.md deleted file mode 100644 index 0f3a0f04..00000000 --- a/paperforge/skills/literature-qa/scripts/pf-end.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -name: pf-end -description: Save the paper Q&A discussion record. Triggered when user says "保存" "结束" "save" or types $pf-end. Summarizes all Q&A pairs and writes them to the paper's ai/discussion directory via the discussion module. -allowed-tools: [Read, Bash] ---- - -# <prefix>pf-end - -## Purpose - -结束当前论文对话并保存讨论记录。需要用户**显式要求**时才执行,不自动触发。 - -1. 汇总本次对话中所有 Q&A 对 -2. 通过 `discussion.record_session()` 写入论文工作区的 `ai/` 目录 -3. 告知用户记录已保存 - -## Trigger - -用户说以下任一关键词时执行(全平台通用): -- `保存` / `记录` / `保存记录` -- `结束` / `完成` -- `save discussion` / `save` / `done` - -也可以显式指定 key: -- `{prefix}pf-end <zotero_key>`(OpenCode) -- `保存 <zotero_key>`(全平台) - -如果未指定 key,则使用当前已加载的论文 key。 - -## Save Format - -将会话期间的所有 Q&A 序列化为 JSON 数组: - -```json -{ - "question": "用户的问题", - "answer": "Agent 的回答", - "source": "user_question", - "timestamp": "2026-05-06T12:00:00+08:00" -} -``` - -`source` 为 `"user_question"`(用户提问)或 `"agent_analysis"`(Agent 主动分析)。 - -## Command - -```bash -python -m paperforge.worker.discussion record <ZOTERO_KEY> \ - --vault "<VAULT_PATH>" \ - --agent pf-paper \ - --model "<MODEL_NAME>" \ - --qa-pairs '<JSON_ARRAY>' -``` - -## Verification - -CLI 返回: -```json -{"status": "ok", "json_path": "Literature/{domain}/{key} - {title}/ai/discussion.json", "md_path": "..."} -``` - -如果 `status` 为 `"error"`,记录错误信息并重试,不要跳过。 - -## See Also - -- [pf-paper](pf-paper.md) — 论文 Q&A 工作台 -- [pf-deep](pf-deep.md) — 完整三阶段精读 diff --git a/paperforge/skills/literature-qa/scripts/pf-ocr.md b/paperforge/skills/literature-qa/scripts/pf-ocr.md deleted file mode 100644 index 000611a3..00000000 --- a/paperforge/skills/literature-qa/scripts/pf-ocr.md +++ /dev/null @@ -1,102 +0,0 @@ ---- -name: pf-ocr -description: Process the PDF OCR queue for formal notes marked do_ocr: true. Uploads PDFs to PaddleOCR API and extracts fulltext with figures. -allowed-tools: [Bash] ---- - -# /pf-ocr - -## Purpose - -处理正式笔记 frontmatter 中 `do_ocr: true` 的 PDF OCR 队列。 - -`paperforge ocr` 会自动读取 `paperforge.json` 定位 ocr 目录和 worker 脚本,运行 OCR 并自动诊断结果。 - -## CLI Equivalent - -```bash -paperforge ocr -``` - -## Prerequisites - -- [ ] formal note frontmatter 中 `do_ocr: true` 已设置 -- [ ] PDF 附件存在(`has_pdf: true`) -- [ ] PaddleOCR API Key 已配置(`.env` 中 `PADDLEOCR_API_TOKEN`) -- [ ] 网络连接正常(可访问 PaddleOCR 服务) - -## Arguments - -| 参数 | 必需 | 说明 | -|------|------|------| -| `--diagnose` | 否 | 仅诊断配置,不上传 PDF | -| `--key <KEY>` | 否 | 仅处理指定 Zotero key 的文献 | -| `--vault <PATH>` | 否 | 指定 Vault 根目录(默认当前目录) | -| `--live` | 否 | 与 `--diagnose` 联用,执行实时 PDF 测试 | - -### 诊断等级 - -| 等级 | 检查项 | 失败含义 | -|------|--------|---------| -| L1 | API token 存在性 | `PADDLEOCR_API_TOKEN` 缺失或为空 | -| L2 | URL 可达性 | 无法连接 PaddleOCR 服务 | -| L3 | API 格式验证 | 服务可达但响应格式异常 | -| L4 | 实时 PDF 往返 | 完整提交和结果获取失败 | - -> Exit code `0` = 所有检查通过。Exit code `1` = 至少一项检查失败。 - -## Example - -```bash -# 处理所有标记 do_ocr: true 的文献 -paperforge ocr - -# 仅处理指定文献 -paperforge ocr --key ABCDEFG - -# 诊断模式(不实际运行) -paperforge ocr --diagnose - -# 完整诊断(含实时测试) -paperforge ocr --diagnose --live - -# 指定 Vault 目录 -paperforge ocr --vault /path/to/vault -``` - -## Output - -OCR 完成后,每个文献生成以下文件: - -``` -<system_dir>/PaperForge/ocr/<key>/ -├── fulltext.md # 提取的全文(含 <!-- page N --> 分页标记) -├── images/ # 自动切割的图表图片 -├── meta.json # OCR 元数据(含 ocr_status) -└── figure-map.json # 图表索引(自动生成) -``` - -`meta.json` 中的 `ocr_status` 字段: -- `pending` — 等待处理 -- `processing` — 正在处理 -- `done` — 完成 -- `failed` — 失败 - -## Error Handling - -### API Token 缺失(L1 失败) -- `PADDLEOCR_API_TOKEN` 未设置 → 在 `.env` 文件中添加 - -### 服务不可达(L2 失败) -- 无法连接 PaddleOCR → 检查网络连接 - -### PDF 上传失败(L4 失败) -- PDF 提交后未返回结果 → 检查 PDF 文件是否损坏 - -### OCR 状态卡住 -- `ocr_status` 长期显示 `processing` → 重新设置 `do_ocr: true` 后再次运行 - -## See Also - -- [pf-sync](pf-sync.md) — 文献同步(生成正式笔记) -- [pf-deep](pf-deep.md) — 深度精读(依赖 OCR 结果) diff --git a/paperforge/skills/literature-qa/scripts/pf-paper.md b/paperforge/skills/literature-qa/scripts/pf-paper.md deleted file mode 100644 index aa3e032b..00000000 --- a/paperforge/skills/literature-qa/scripts/pf-paper.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -name: pf-paper -description: Quick paper Q&A workbench. Load a paper by Zotero key, title, DOI, or PMID and answer questions. Does not require OCR. -allowed-tools: [Read, Bash] ---- - -# <prefix>pf-paper - -## Purpose - -基于 Zotero OCR 文本的单篇论文工作台入口。 - -1. 解析 `<prefix>pf-paper <query>` 中的查询词 -2. 支持 Zotero key、标题片段、DOI、PMID、关键词 -3. 优先搜索本地 Zotero,解析到单篇目标论文 -4. 根据 Vault 根目录的 `paperforge.json` 加载 `<system_dir>/PaperForge/ocr/<KEY>/fulltext.md` 作为主文本 -5. 读取 `meta.json` 显示论文标题、作者、期刊、年份 -6. 进入 Q&A 模式,用中文回答用户关于该论文的问题 -7. 在当前论文上下文中,用户可再说"精读这篇文章"切换到 deep 层 - -## CLI Equivalent - -```bash -# 准备阶段(间接) -paperforge sync # 生成正式笔记 -``` - -> `<prefix>pf-paper` 是 **Agent 层命令**,无直接 CLI 等效命令。 - -## Prerequisites - -- [ ] 正式笔记已生成(`paperforge sync` 生成) -- [ ] `fulltext.md` 存在(推荐,用于基于原文回答;如不存在则基于元数据回答) -- [ ] discussion.py 模块可用(`python -m paperforge.worker.discussion --help` 可执行) - -> **注意**:与 `/pf-deep` 不同,`<prefix>pf-paper` **不强制要求** OCR 完成。没有 OCR 时基于论文元数据和公开信息回答。 - -## Arguments - -| 参数 | 必需 | 说明 | -|------|------|------| -| `<query>` | 是 | Zotero key、标题片段、DOI、PMID 或关键词 | -| `<query2> ...` | 否 | 可同时加载多篇论文 | - -### 解析规则 - -1. 如果输入看起来像 8 位 Zotero key,则直接按 key 解析。 -2. 否则先在本地 Zotero 中搜索标题/摘要。 -3. 若命中唯一结果或明显最佳结果,则直接载入。 -4. 若存在多个合理候选,则先列候选清单再让用户选。 -5. 不要强迫用户先知道 Zotero key。 - -## Example - -```bash -<prefix>pf-paper XGT9Z257 -<prefix>pf-paper Predictive findings on magnetic resonance imaging -<prefix>pf-paper 10.1016/j.jse.2018.01.001 -<prefix>pf-paper XGT9Z257 PQR8KLM -``` - -## Output - -加载成功后显示: - -``` -已加载论文: [title] ([year], [journal]) -Zotero Key: [key] -结束对话时说"保存记录"即可保存讨论。 -请问有什么问题? -``` - -### 回答原则 - -- **严格基于** `fulltext.md` 中的文本内容回答 -- 引用原文时标注来源页码/章节 -- 用中文(简体中文)回答 -- 论文中未提及的内容,明确说明"论文中未提及该内容" -- 需要结合论文以外知识的问题,说明"该问题需要结合论文以外的知识回答" - -## Error Handling - -### 论文未找到 -- **表现**:Zotero key 无效或搜索无结果 -- **解决**:确认 key 正确,或尝试用标题片段搜索 - -### 多个候选结果 -- **表现**:搜索返回多个匹配的论文 -- **处理**:Agent 列出候选清单,让用户选择目标论文 - -### OCR 文件缺失 -- **表现**:`fulltext.md` 不存在 -- **处理**:Agent 基于元数据和公开信息回答,并告知用户"OCR 文本不可用,回答基于元数据" - -## Saving Discussion Record - -当用户说"保存"、"结束"、"完成"等关键词时(或显式输入 `{prefix}pf-end <key>`),必须执行记录保存。具体步骤见 [pf-end](pf-end.md)。 - -### 注意事项 - -- 仅 `/pf-paper` Q&A 需要记录。`/pf-deep` 不记录(精读内容已写入正式笔记)。 -- 如果无法从 formal-library.json 找到论文 domain/title,记录失败不应影响用户使用。 - -## See Also - -- [pf-end](pf-end.md) — 结束对话并保存记录 -- [pf-deep](pf-deep.md) — 完整三阶段精读 diff --git a/paperforge/skills/literature-qa/scripts/pf-status.md b/paperforge/skills/literature-qa/scripts/pf-status.md deleted file mode 100644 index 67d22f94..00000000 --- a/paperforge/skills/literature-qa/scripts/pf-status.md +++ /dev/null @@ -1,94 +0,0 @@ ---- -name: pf-status -description: Check PaperForge installation and runtime status. Verifies configuration, paths, and data integrity. -allowed-tools: [Bash] ---- - -# /pf-status - -## Purpose - -查看 PaperForge 当前安装与运行状态。 - -`paperforge status` 会检查: - -- 安装完整性(Python 包、依赖) -- 配置文件(`paperforge.json`、`.env`) -- 路径连通性(exports、ocr、literature 目录) -- Zotero 数据目录链接状态 -- Better BibTeX 导出文件状态 - -## CLI Equivalent - -```bash -paperforge status -``` - -## Prerequisites - -无特殊前置条件。此命令用于诊断安装问题,即使在配置不完整时也会尽量输出可用信息。 - -## Arguments - -| 参数 | 必需 | 说明 | -|------|------|------| -| `--vault <PATH>` | 否 | 指定 Vault 根目录(默认当前目录) | - -## Example - -```bash -# 检查当前目录的 Vault 状态 -paperforge status - -# 检查指定 Vault 的状态 -paperforge status --vault /path/to/vault -``` - -## Output - -典型输出示例: - -``` -PaperForge Lite v1.2 -==================== - -[安装检查] -✓ Python 包: paperforge v1.2.0 -✓ 依赖: requests, pymupdf, pillow - -[配置检查] -✓ paperforge.json: 存在且有效 -✓ .env: 存在 -✓ PADDLEOCR_API_TOKEN: 已设置 - -[路径检查] -✓ exports: <system_dir>/PaperForge/exports/ -✓ ocr: <system_dir>/PaperForge/ocr/ -✓ literature: <resources_dir>/<literature_dir>/ -✓ Zotero: <system_dir>/Zotero/ - -[数据检查] -✓ library.json: 存在,包含 150 条文献 -✓ 正式笔记: 150 篇 - -状态: 一切正常 ✅ -``` - -## Error Handling - -### 配置缺失 -- `✗ paperforge.json: 未找到` → 运行 `paperforge doctor` 或重新执行安装 - -### 路径错误 -- `✗ Zotero: 目录不存在或不是有效链接` → 创建 junction/symlink 到 Zotero 数据目录 - -### 依赖缺失 -- `✗ 依赖: requests 未安装` → `pip install requests pymupdf pillow` - -### API Key 未设置 -- `✗ PADDLEOCR_API_TOKEN: 未设置` → 在 `.env` 文件中添加 API token - -## See Also - -- [pf-sync](pf-sync.md) — 文献同步 -- [pf-ocr](pf-ocr.md) — OCR 提取 diff --git a/paperforge/skills/literature-qa/scripts/pf-sync.md b/paperforge/skills/literature-qa/scripts/pf-sync.md deleted file mode 100644 index 02dedf87..00000000 --- a/paperforge/skills/literature-qa/scripts/pf-sync.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -name: pf-sync -description: Sync Zotero Better BibTeX JSON export and generate/update formal literature notes. -allowed-tools: [Bash] ---- - -# /pf-sync - -## Purpose - -同步 Zotero Better BibTeX JSON 导出并生成/更新正式文献笔记。 - -`paperforge sync` 读取 Zotero JSON 中的新条目,直接生成正式文献笔记。 - -自动读取 `paperforge.json` 定位 exports 目录和 literature 目录。 - -## CLI Equivalent - -```bash -paperforge sync -``` - -## Prerequisites - -- [ ] Zotero 已安装且 Better BibTeX 插件已启用 -- [ ] Better BibTeX 已配置自动导出 JSON -- [ ] JSON 导出文件存在(`<system_dir>/PaperForge/exports/library.json`) -- [ ] `paperforge.json` 配置正确(Vault 根目录下) - -## Arguments - -| 参数 | 必需 | 说明 | -|------|------|------| -| `--dry-run` | 否 | 预览变更,不实际写入文件 | -| `--domain <DOMAIN>` | 否 | 仅同步指定领域(如 `骨科`) | -| `--selection` | 否 | (已废弃)仅保留以兼容旧版 | -| `--index` | 否 | (已废弃)仅保留以兼容旧版 | -| `--vault <PATH>` | 否 | 指定 Vault 根目录(默认当前目录) | - -### 选项 - -```bash -paperforge sync --dry-run # 预览同步结果 -paperforge sync --domain 骨科 # 按领域过滤同步 -``` - -## Example - -```bash -# 同步 Zotero 并生成正式笔记 -paperforge sync - -# 预览模式 -paperforge sync --dry-run - -# 仅同步特定领域 -paperforge sync --domain 骨科 - -# 指定 Vault 目录 -paperforge sync --vault /path/to/vault -``` - -## Output - -``` -[INFO] Found 5 new items -[INFO] Created 骨科/XXXXXXX.md -[INFO] Generated 5 formal notes -[INFO] Output: <resources_dir>/<literature_dir>/骨科/XXXXXXX - Title.md -``` - -生成文件:`<resources_dir>/<literature_dir>/<domain>/<key> - <Title>.md` - -## Error Handling - -### JSON 文件不存在 -- `[ERROR] library.json not found` → 检查 Better BibTeX 导出路径 - -### 空 JSON 导出 -- `[INFO] Found 0 new items` → 确认 Zotero 中有带 citation key 的文献,Better BibTeX 已启用"Keep updated" - -## See Also - -- [pf-ocr](pf-ocr.md) — OCR 提取(下一步操作) -- [pf-status](pf-status.md) — 检查系统状态 From 9b88c441b0b48dc54e8f7c7dac4bf40fe79b267d Mon Sep 17 00:00:00 2001 From: Research Assistant <research@example.com> Date: Sun, 10 May 2026 20:39:09 +0800 Subject: [PATCH 30/37] refactor(skills): modular literature-qa skill with router-first architecture - Replace 6 scattered pf-*.md files with single SKILL.md router + 4 reference files - Add paper_resolver.py: deterministic key/DOI/field search (4 subcommands) - Remove pf-sync/ocr/status (CLI-only commands, no skill wrapper needed) - Move chart-reading/ into references/ for flat structure - Make ld_deep.py --vault optional (auto-detect from cwd) - Add 26 unit tests for paper_resolver - All paths dynamically resolved from paperforge.json, zero hardcoding --- .../specs/2026-05-10-skill-refine-design.md | 431 ++++++++++++++++++ paperforge/skills/literature-qa/SKILL.md | 61 +-- .../literature-qa/references/deep-reading.md | 24 +- .../references/paper-resolution.md | 157 ++----- .../skills/literature-qa/scripts/ld_deep.py | 9 +- paperforge/worker/paper_resolver.py | 384 ++++++++++++++++ tests/unit/test_paper_resolver.py | 361 +++++++++++++++ 7 files changed, 1254 insertions(+), 173 deletions(-) create mode 100644 docs/superpowers/specs/2026-05-10-skill-refine-design.md create mode 100644 paperforge/worker/paper_resolver.py create mode 100644 tests/unit/test_paper_resolver.py diff --git a/docs/superpowers/specs/2026-05-10-skill-refine-design.md b/docs/superpowers/specs/2026-05-10-skill-refine-design.md new file mode 100644 index 00000000..4ec41b64 --- /dev/null +++ b/docs/superpowers/specs/2026-05-10-skill-refine-design.md @@ -0,0 +1,431 @@ +# PaperForge Skill Refactoring: Modular Literature QA + +**Date:** 2026-05-10 +**Branch:** `feature/skill-refine` +**Author:** Overseer + VT-OS/OPENCODE + +--- + +## 1. Problem Statement + +### Current Pain Points + +1. **No unified paper resolution.** `pf-deep` and `pf-paper` each duplicate paper-search logic in their `.md` files. When a user says "那篇关于骨再生的", agents search blindly — reading directories, grepping metadata, guessing paths. + +2. **File organization semantically wrong.** Skill instruction files (`.md`) live in `scripts/` alongside executable code (`ld_deep.py`). No `SKILL.md` router. The `prompt_deep_subagent.md` sits orphaned at root level. + +3. **Too many low-value skills.** `pf-sync`, `pf-ocr`, `pf-status` are thin wrappers around CLI commands (`paperforge sync/ocr/status`). They add 3 more entries to the skill list without providing agent-level value beyond what a single `bash` command does. + +4. **No chart-reading routing clarity.** Inside `pf-deep`, the mechanism for routing to the right chart-reading guide is implicit — buried in `ld_deep.py` keyword matching. The agent skill file doesn't explain how to reference chart-reading guides. + +### User's Core Request + +> "用户用自然语言描述'某篇文章','某个主题','某个zoterokey',他能快速定位到这篇文章的workspace还有与其相关的各种参数,frontmatter,路径等" + +Translation: given ANY reference to a paper (key, DOI, title fragment, author+year, or natural language description), the system must quickly resolve it to a workspace with all related parameters. + +--- + +## 2. Design Goals + +1. **Two-path resolution.** Python handles deterministic lookups (key, DOI, structured fields). Agent handles natural language. Python failure → Agent fallback. + +2. **Single shared resolution module.** All sub-skills (`deep-reading`, `paper-qa`, `save-session`) consume the same `paper-resolution.md` reference + `paper_resolver.py` worker. Zero duplication. + +3. **Compliant skill structure.** Follow the agent skills standard: `SKILL.md` as router, `references/` for detailed workflows loaded on demand, `scripts/` for executable code only. + +4. **Token efficient.** SKILL.md under 100 lines. Sub-workflows in separate files loaded only when routed to. Python scripts executed, not loaded into context. + +5. **Installable/distributable.** All assets under `paperforge/skills/literature-qa/` shipped with pip install. Python worker at `paperforge/worker/paper_resolver.py` also shipped. + +--- + +## 3. Target Architecture + +### 3.1 Directory Structure + +``` +paperforge/skills/literature-qa/ +│ +├── SKILL.md ← Router: natural language → sub-skill routing +│ +├── references/ ← Detailed workflows (loaded on demand) +│ ├── deep-reading.md ← Keshav 3-pass deep reading workflow +│ ├── paper-qa.md ← Interactive paper Q&A workflow +│ ├── save-session.md ← Save discussion record workflow +│ ├── paper-resolution.md ← Paper location protocol (shared by all) +│ ├── deep-subagent.md ← Subagent prompt template for deep reading +│ └── chart-reading/ ← 19 chart-type reading guides +│ ├── INDEX.md +│ ├── 条形图与误差棒.md +│ ├── 森林图与Meta分析.md +│ ├── 折线图与时间序列.md +│ ├── 散点图与气泡图.md +│ ├── ROC与PR曲线.md +│ ├── 生存曲线.md +│ ├── 箱式图与小提琴图.md +│ ├── 热图与聚类图.md +│ ├── 桑基图与弦图.md +│ ├── 火山图与曼哈顿图.md +│ ├── 网络图与通路图.md +│ ├── GSEA富集图.md +│ ├── 降维图(PCA-tSNE-UMAP).md +│ ├── 雷达图与漏斗图.md +│ ├── 组织学半定量图.md +│ ├── 免疫荧光定量图.md +│ ├── 显微照片与SEM图.md +│ ├── Western Blot条带图.md +│ └── 蛋白质结构图.md +│ +└── scripts/ ← Executable code only (not loaded into context) + └── ld_deep.py ← Deep reading engine (existing, has CLI: prepare, queue, chart-type-scan, postprocess-pass2, validate-note, etc.) + +paperforge/worker/ +└── paper_resolver.py ← NEW: Deterministic paper lookup engine + +tests/unit/ +└── test_paper_resolver.py ← NEW: Tests for paper resolver +``` + +### 3.2 Files to Delete + +| File | Reason | +|------|--------| +| `paperforge/skills/literature-qa/scripts/pf-sync.md` | Thin CLI wrapper; users run `paperforge sync` directly | +| `paperforge/skills/literature-qa/scripts/pf-ocr.md` | Thin CLI wrapper; users run `paperforge ocr` directly | +| `paperforge/skills/literature-qa/scripts/pf-status.md` | Thin CLI wrapper; users run `paperforge status` directly | + +### 3.3 Files to Add + +| File | Purpose | +|------|---------| +| `paperforge/skills/literature-qa/SKILL.md` | Main router with trigger table + paper resolution protocol | +| `paperforge/skills/literature-qa/references/paper-resolution.md` | Shared paper location protocol | +| `paperforge/worker/paper_resolver.py` | Deterministic lookup engine (key, DOI, structured fields) | +| `tests/unit/test_paper_resolver.py` | Unit tests for resolver | + +### 3.4 Files to Rewrite + +| File (old → new) | Change | +|---|---| +| `scripts/pf-deep.md` → `references/deep-reading.md` | Add `disable-model-invocation: true`; remove embedded search logic; call resolver + ld_deep.py; add chart-reading routing section | +| `scripts/pf-paper.md` → `references/paper-qa.md` | Add `disable-model-invocation: true`; remove embedded search logic; call resolver | +| `scripts/pf-end.md` → `references/save-session.md` | Add `disable-model-invocation: true`; strip verbose explanations | +| `prompt_deep_subagent.md` → `references/deep-subagent.md` | Minimal edits, update paths | + +--- + +## 4. Component Design + +### 4.1 SKILL.md — Router + +Purpose: Single entry point. Agent reads this first. It routes to the correct reference file based on user intent. + +```yaml +--- +name: literature-qa +description: 学术文献精读与问答。支持Zotero key、DOI、标题、作者/年份或自然语言定位论文。Use when user wants to deep-read, analyze, or ask questions about papers in their Zotero library. Triggered by /pf-deep, /pf-paper, /pf-end, or phrases like "精读这篇", "查一下XX文章", "读那篇关于YY的". +--- +``` + +**Routing table (core of SKILL.md):** + +| Trigger | User says | Load | Action | +|---------|-----------|------|--------| +| `/pf-deep <query>` | "精读/深度阅读/读一下 XX" | `references/deep-reading.md` | Keshav 3-pass deep reading | +| `/pf-deep` (no args) | "精读队列/有哪些该读了" | `references/deep-reading.md` | Show deep reading queue | +| `/pf-paper <query>` | "查/问/帮我看看 XX" | `references/paper-qa.md` | Interactive Q&A mode | +| `/pf-end` | "保存/结束/完成讨论" | `references/save-session.md` | Persist discussion record | + +**Paper resolution protocol (inline in SKILL.md, short version):** + +``` +Determine input type → run exact command: + + 8-char key → python -m paperforge.worker.paper_resolver resolve-key <KEY> --vault . + DOI → python -m paperforge.worker.paper_resolver resolve-doi <DOI> --vault . + Author+year → python -m paperforge.worker.paper_resolver search --author "X" --year Y --vault . + Title fragment → python -m paperforge.worker.paper_resolver search --title "X" --vault . + Natural language → Agent reads indexes/formal-library.json, searches title/domain/year fields + + Python returns empty → Agent fallback: grep frontmatter in formal notes directory + Multiple matches → List candidates for user to choose +``` + +### 4.2 paper_resolver.py — Deterministic Lookup Engine + +A new Python module at `paperforge/worker/paper_resolver.py`. + +**CLI interface (3 subcommands):** + +```bash +# Exact key lookup +python -m paperforge.worker.paper_resolver resolve-key <KEY> --vault <PATH> +# → {"ok": true, "key": "ABC12345", "title": "...", "formal_note": "...", "ocr_path": "...", "frontmatter": {...}} + +# Exact DOI lookup +python -m paperforge.worker.paper_resolver resolve-doi <DOI> --vault <PATH> + +# Structured field search (at least one of --title, --author, --year, --domain required) +python -m paperforge.worker.paper_resolver search --title "..." --author "..." --year 2024 --domain "骨科" --vault <PATH> +# → {"ok": true, "matches": [{...}, ...], "count": 3} +``` + +**--vault resolution:** Uses `paperforge.json` in vault root to locate `indexes/formal-library.json` and formal notes directory, via existing `paperforge.core.config` path utilities. Same mechanism as `paperforge sync/ocr/deep-reading`. + +**Internal design:** + +```python +class PaperResolver: + """Deterministic paper lookup from formal-library.json index.""" + + def __init__(self, index_path: str, formal_notes_dir: str): + # Load formal-library.json into memory + + def resolve_key(self, key: str) -> PaperMeta | None: + """Exact key match. Key is 8-char alphanumeric.""" + + def resolve_doi(self, doi: str) -> PaperMeta | None: + """Exact DOI match from frontmatter.""" + + def search(self, title: str = None, author: str = None, + year: int = None, domain: str = None) -> list[PaperMeta]: + """Multi-field search with substring matching, sorted by relevance.""" + + def get_workspace(self, key: str) -> PaperWorkspace: + """Given a key, return all paths and frontmatter.""" + +@dataclass +class PaperMeta: + key: str + title: str + domain: str + year: int + authors: str # Semi-colon separated from formal-library.json + doi: str + collection_path: str + +@dataclass +class PaperWorkspace: + key: str + title: str + domain: str + formal_note_path: str # Literature/{domain}/{key} - {title}.md + ocr_path: str # 99_System/PaperForge/ocr/{key}/ + fulltext_path: str # 99_System/PaperForge/ocr/{key}/fulltext.md + frontmatter: dict +``` + +**Data source:** `formal-library.json` (already generated by `paperforge sync` in `indexes/`). This JSON contains full metadata: key, title, authors, year, doi, domain, collection_path, journal, abstract, etc. + +**Important distinction:** +- **Python structured search** (`resolve_key`, `resolve_doi`, `search`) operates on a subset of fields in `PaperMeta`. This handles deterministic lookups. +- **Agent natural language search** reads the full `formal-library.json` directly (all fields available), plus can grep formal note frontmatter as fallback. This handles "那篇关于骨再生的Nature" type queries. + +**Error handling:** Returns empty list on no match (never crashes). Agent handles the empty case via fallback. + +### 4.3 references/deep-reading.md — Deep Reading Workflow + +Key design decisions: +- `disable-model-invocation: true` — never triggered automatically, only via SKILL.md routing +- No embedded paper search logic — delegates to `paper-resolution.md` +- Clear chart-reading routing section + +**Chart-reading routing logic (new addition):** + +关键原则:**Agent 负责判断图表类型,Python 只给建议,Agent 做最终决策。** + +```markdown +## Chart Reading Guide Routing + +### 两步定位法 + +**Step 1: Python 给建议(快速初筛)** +```bash +python <skill_dir>/scripts/ld_deep.py chart-type-scan --vault . --key <KEY> +``` +输出每个 figure 的关键词命中结果。这只是建议,Agent 不要盲信。 + +**Step 2: Agent 读 caption 做最终判断** +对每个 figure,Agent 必须: +1. 读该 figure 的 caption(来自 fulltext.md 或 figure-map.json) +2. 根据 caption 内容,对照 `references/chart-reading/INDEX.md` 判断图表类型 +3. 如果 Python 建议和 Agent 判断不一致 → 以 Agent 判断为准 +4. 如果无法确定类型 → 跳过 chart guide,按通用 figure 结构分析 + +### 图表类型 → 指南文件映射 + +| 图表类型 | 指南文件 | +|---------|---------| +| 条形图/误差棒 | `references/chart-reading/条形图与误差棒.md` | +| 森林图/Meta分析 | `references/chart-reading/森林图与Meta分析.md` | +| 折线图/时间序列 | `references/chart-reading/折线图与时间序列.md` | +| 散点图/气泡图 | `references/chart-reading/散点图与气泡图.md` | +| ROC/PR曲线 | `references/chart-reading/ROC与PR曲线.md` | +| 生存曲线/Kaplan-Meier | `references/chart-reading/生存曲线.md` | +| 箱式图/小提琴图 | `references/chart-reading/箱式图与小提琴图.md` | +| 热图/聚类图 | `references/chart-reading/热图与聚类图.md` | +| 桑基图/弦图 | `references/chart-reading/桑基图与弦图.md` | +| 火山图/曼哈顿图 | `references/chart-reading/火山图与曼哈顿图.md` | +| 网络图/通路图 | `references/chart-reading/网络图与通路图.md` | +| GSEA富集图 | `references/chart-reading/GSEA富集图.md` | +| PCA/t-SNE/UMAP降维图 | `references/chart-reading/降维图(PCA-tSNE-UMAP).md` | +| 雷达图/漏斗图 | `references/chart-reading/雷达图与漏斗图.md` | +| 组织学半定量图 | `references/chart-reading/组织学半定量图.md` | +| 免疫荧光定量图 | `references/chart-reading/免疫荧光定量图.md` | +| 显微照片/SEM/TEM | `references/chart-reading/显微照片与SEM图.md` | +| Western Blot条带图 | `references/chart-reading/Western Blot条带图.md` | +| 蛋白质3D结构图 | `references/chart-reading/蛋白质结构图.md` | + +完整索引见 `references/chart-reading/INDEX.md`。遇到边界情况优先读 INDEX.md 中的详细描述。 +``` + +**Core workflow (unchanged from current, just moved to references/):** +1. `prepare` — check analyze status, verify OCR, generate figure-map +2. Pass 1 Overview — one-sentence summary, 5 Cs, figure guided reading +3. Pass 2 Reconstruction — figure-by-figure (6 sub-headings per figure), table-by-table +4. Pass 2 Postprocess — validate figure order, sub-headings, empty blocks +5. Pass 3 Deep Understanding — assumptions, conclusions, discussion, inspiration +6. Final Validation — structural validation of the note + +### 4.4 references/paper-qa.md — Interactive Q&A Workflow + +- `disable-model-invocation: true` +- No embedded paper search logic +- Loads fulltext via resolved workspace +- Q&A mode with standard answering principles (cite sources, use Chinese, flag missing info) +- Triggers save-session.md on "save"/"done"/"end" + +### 4.5 references/save-session.md — Save Discussion Record + +- `disable-model-invocation: true` +- Collects Q&A pairs from session +- Calls existing module: `python -m paperforge.worker.discussion record <KEY> --vault . --qa-pairs '<JSON>'` +- `paperforge/worker/discussion.py` already exists (not part of this refactor) +- Only for paper-qa sessions (deep-reading writes directly to formal note) + +### 4.6 references/paper-resolution.md — Shared Resolution Protocol + +Full detailed version of the resolution protocol. Referenced by all three sub-skills. + +``` +Contents: +- Input type detection rules (regex patterns for key/DOI/author+year) +- Exact commands for each type (copy-paste ready for agent) +- Fallback procedure when Python returns empty +- Multi-match handling (how to present candidate list) +- Workspace structure (what paths to report after resolution) +``` + +--- + +## 5. Data Flow + +``` +User: "/pf-deep 关于骨再生的那篇" + │ + ▼ +SKILL.md router reads trigger, routes to deep-reading.md + │ + ▼ +deep-reading.md Step 1: "Follow paper-resolution.md" + │ + ▼ +paper-resolution.md: "Natural language → Agent reads formal-library.json" + │ + ▼ +Agent reads indexes/formal-library.json, searches title/domain + → Found 1 match: key=ABC12345 + │ + ▼ +deep-reading.md Step 2: "Run ld_deep.py prepare" + │ + ▼ +ld_deep.py prepare → checks OCR, generates figure-map, inserts scaffold + │ + ▼ +deep-reading.md Step 3: "Run chart-type-scan" + → Detects: bar, survival, scatter + → Agent reads chart-reading/条形图与误差棒.md, 生存曲线.md, 散点图与气泡图.md + │ + ▼ +Agent executes Pass 1 → Pass 2 (per figure, per chart guide) → Postprocess → Pass 3 → Validate + │ + ▼ +Result written to Literature/骨科/ABC12345 - Title.md under ## 🔍 精读 +``` + +--- + +## 6. Error Handling + +| Scenario | Handler | Behavior | +|----------|---------|----------| +| Python resolver returns empty | Agent fallback | grep frontmatter in formal notes directory | +| Both Python and Agent fail | Agent reports | "未找到匹配的论文。请确认 Zotero key、标题或搜索词是否正确。" | +| Multiple candidates found | Agent lists | Numbered list with key, title, year, domain; user selects | +| OCR not complete | ld_deep.py prepare | Returns error status; agent reports "OCR未完成,请先运行 paperforge ocr" | +| Deep reading content already exists | Agent asks | "内容已存在,追加/覆盖/跳过?" | +| Chart type unknown | Agent skips | Proceeds with figure analysis without chart guide | + +--- + +## 7. Token Budget Analysis + +| Component | Current Tokens (approx) | After Refactor | Savings | +|-----------|------------------------|----------------|---------| +| `pf-deep.md` (237 lines) | ~3000 | ~300 (references/deep-reading.md) | -2700 | +| `pf-paper.md` (107 lines) | ~1400 | ~250 (references/paper-qa.md) | -1150 | +| `pf-end.md` (67 lines) | ~900 | ~150 (references/save-session.md) | -750 | +| `pf-sync.md` (85 lines) | ~1100 | 0 (deleted) | -1100 | +| `pf-ocr.md` (102 lines) | ~1300 | 0 (deleted) | -1300 | +| `pf-status.md` (94 lines) | ~1200 | 0 (deleted) | -1200 | +| `SKILL.md` (new) | 0 | ~500 | +500 | +| `paper-resolution.md` (new) | 0 | ~300 | +300 | +| **Total in context upon trigger** | **~6900 (all loaded at once)** | **~800 (SKILL.md + one reference)** | **-6100** | + +Key insight: After refactoring, the agent loads only SKILL.md (~500 tokens) and the one reference file for the triggered sub-skill (~150-300 tokens). Previously all 6 skill files were discoverable, and the agent might load multiple. The paper_resolver.py scripts execute via bash, consuming zero context tokens for their content. + +--- + +## 8. Implementation Plan + +### Phase 1: Add new modules +1. Create `paperforge/worker/paper_resolver.py` with 3 CLI subcommands +2. Create `tests/unit/test_paper_resolver.py` + +### Phase 2: Rewrite skill structure +3. Create `paperforge/skills/literature-qa/SKILL.md` (router) +4. Create `references/paper-resolution.md` (shared protocol) +5. Rewrite `scripts/pf-deep.md` → `references/deep-reading.md` +6. Rewrite `scripts/pf-paper.md` → `references/paper-qa.md` +7. Rewrite `scripts/pf-end.md` → `references/save-session.md` +8. Move `prompt_deep_subagent.md` → `references/deep-subagent.md` +9. Move `chart-reading/` into `references/chart-reading/` + +### Phase 3: Cleanup +10. Delete `scripts/pf-sync.md` +11. Delete `scripts/pf-ocr.md` +12. Delete `scripts/pf-status.md` +13. Delete old copies of rewritten files: `scripts/pf-deep.md`, `scripts/pf-paper.md`, `scripts/pf-end.md`, `prompt_deep_subagent.md` (all moved to `references/`) + +### Phase 4: Verify +13. Run unit tests: `python -m pytest tests/unit/test_paper_resolver.py -q` +14. Verify skill loads correctly in OpenCode +15. Manual test: `/pf-deep ABC12345` resolves correctly +16. Manual test: `/pf-deep 关于骨再生的那篇` routes to agent search → resolves + +--- + +## 9. Risks and Mitigations + +| Risk | Mitigation | +|------|-----------| +| `formal-library.json` index stale after sync | Document: "If paper not found, run `paperforge sync` first" | +| Agent natural language search misses the paper | Two-layer fallback: formal-library.json → grep formal notes | +| Skill routing confusion (SKILL.md + 3 sub-skills all visible) | Sub-skills use `disable-model-invocation: true`; only SKILL.md is auto-discoverable | +| Chart-reading path references break after move | All paths relative to skill directory; test with `python -m paperforge.worker.paper_resolver` | + +--- + +*Vault-Tec -- Preparing for the Future! Spec compiled by Terminal VT-OS/OPENCODE, serial VTC-2077-OC-4111.* diff --git a/paperforge/skills/literature-qa/SKILL.md b/paperforge/skills/literature-qa/SKILL.md index 2ad91877..83728fbf 100644 --- a/paperforge/skills/literature-qa/SKILL.md +++ b/paperforge/skills/literature-qa/SKILL.md @@ -1,11 +1,14 @@ --- name: literature-qa description: > - 学术文献精读与问答。MUST trigger when user types /pf-deep, /pf-paper, /pf-end, - or says "精读", "深度阅读", "读一下", "查一下这篇论文", "帮我看看这篇文章", - "这篇文章讲了什么", "保存讨论", "结束讨论", "做这篇文献的问答", - or any phrase about reading/analyzing papers in their Zotero library. - 支持 Zotero key, DOI, 标题, 作者/年份, 自然语言描述定位论文. + 学术文献精读与问答。MUST trigger when user types /pf-deep /pf-paper /pf-end + or says 精读 深度阅读 读一下这篇 读文献 带我读 组会讲这篇 帮我精读 讲讲这篇论文 + 查一下这篇论文 帮我看看这篇文章 这篇文章讲了什么 这篇讲了什么 论文问答 做这篇的问答 + 保存讨论 结束讨论 保存记录 完成讨论 精读队列 有哪些该读了 + or uses natural language like 那篇关于XX的文章 去年那篇XX 找一下XX的文献 + or any phrase about reading analyzing deep-reading papers in their Zotero library. + 支持 Zotero key DOI 标题 作者 年份 自然语言描述定位论文. + Routes intent internally via routing table—no separate sub-skills needed. license: Apache-2.0 compatibility: opencode --- @@ -18,10 +21,10 @@ Agent 读到本文件后,首先根据用户意图路由到对应的 reference | 用户意图 | 典型输入 | 加载文件 | |---------|---------|---------| -| 三阶段精读(指定论文) | `/pf-deep <query>`, `pf-deep <query>`, "精读 XXX", "深度阅读 XXX" | [references/deep-reading.md](references/deep-reading.md) | +| 三阶段精读(指定论文) | `/pf-deep <query>`, `pf-deep <query>`, "精读 XXX", "深度阅读 XXX", "带我读", "组会讲这篇", "读一下这篇" | [references/deep-reading.md](references/deep-reading.md) | | 三阶段精读(查看队列) | `/pf-deep`(无参数), "精读队列", "有哪些该读了" | [references/deep-reading.md](references/deep-reading.md) | -| 论文问答 | `/pf-paper <query>`, `pf-paper <query>`, "做这篇的问答", "帮我看看 XXX" | [references/paper-qa.md](references/paper-qa.md) | -| 保存讨论记录 | `/pf-end`, `pf-end`, "保存", "结束讨论", "完成讨论" | [references/save-session.md](references/save-session.md) | +| 论文问答 | `/pf-paper <query>`, `pf-paper <query>`, "做这篇的问答", "帮我看看 XXX", "这篇文章讲了什么", "查一下" | [references/paper-qa.md](references/paper-qa.md) | +| 保存讨论记录 | `/pf-end`, `pf-end`, "保存", "结束讨论", "完成讨论", "保存记录" | [references/save-session.md](references/save-session.md) | > **重要:** 加载 reference 文件后,严格按照该文件的流程执行。不要跳过任何步骤。 @@ -29,39 +32,23 @@ Agent 读到本文件后,首先根据用户意图路由到对应的 reference 详见 [references/paper-resolution.md](references/paper-resolution.md)。 -**核心原则:二路定位。Python 干确定的活,Agent 干理解的活。路径全在环境变量里,零硬编码。** +**核心原则:所有路径操作走 Python,Agent 只管调命令、读输出。零硬编码、零平台依赖。** -### 第零步:加载环境变量(每条路由启动时跑一次) +### 快速索引 -```pwsh -python -m paperforge.worker.paper_resolver env --vault . --shell pwsh | Invoke-Expression -``` +| 你要做什么 | 跑这个 Python 命令 | +|-----------|-------------------| +| 定位论文(按 key) | `python -m paperforge.worker.paper_resolver resolve-key <KEY> --vault .` | +| 定位论文(按 DOI) | `python -m paperforge.worker.paper_resolver resolve-doi "<DOI>" --vault .` | +| 搜索论文(字段匹配) | `python -m paperforge.worker.paper_resolver search --title "..." --author "..." --year ... --vault .` | +| 获取 vault 路径 | `python -m paperforge.worker.paper_resolver paths --vault .` | -此后所有路径用环境变量引用,不再拼接: +### 处理结果 -| 变量 | 含义 | -| ------------------ | --------------------------- | -| `$env:PF_VAULT` | vault 根目录 | -| `$env:PF_INDEX_PATH` | formal-library.json 路径 | -| `$env:PF_LITERATURE_DIR` | formal notes 目录 | -| `$env:PF_OCR_DIR` | OCR 结果目录 | - -### 第一步:判断输入类型,选择路径 - -| 输入特征 | 执行命令 | -|---------|---------| -| 8位 key | `python -m paperforge.worker.paper_resolver resolve-key <KEY> --vault "$env:PF_VAULT"` | -| DOI | `python -m paperforge.worker.paper_resolver resolve-doi "<DOI>" --vault "$env:PF_VAULT"` | -| 标题片段 | `python -m paperforge.worker.paper_resolver search --title "..." --vault "$env:PF_VAULT"` | -| 作者+年份 | `python -m paperforge.worker.paper_resolver search --author "Smith" --year 2024 --vault "$env:PF_VAULT"` | -| 自然语言 | Agent 读 `$env:PF_INDEX_PATH` 指向的 formal-library.json | - -### 第二步:处理结果 - -- **Python 返回匹配:** 直接使用返回的 workspace(`formal_note_path` 等由 config 动态计算) -- **Python 搜不到:** Agent grep fallback:`rg -l "zotero_key:.*ABC" "$env:PF_LITERATURE_DIR/"` -- **自然语言搜不到:** 告知用户 "未找到,请确认或先运行 `paperforge sync`" -- **命中多篇:** 列出候选清单让用户选 +- **Python 返回匹配:** 直接使用返回的 workspace +- **Python 返回空:** Agent 自己搜。用 `paths` 拿到的 `literature_dir` 和 `index_path`,grep frontmatter / 读 formal-library.json +- **自然语言输入:** Agent 自己理解语义,读 formal-library.json(`paths` 里的 `index_path`)搜索 +- **命中多篇:** 列出候选清单(key, title, year, domain, ocr_status),让用户选 ## 文件结构 diff --git a/paperforge/skills/literature-qa/references/deep-reading.md b/paperforge/skills/literature-qa/references/deep-reading.md index 3563a5d7..b8162772 100644 --- a/paperforge/skills/literature-qa/references/deep-reading.md +++ b/paperforge/skills/literature-qa/references/deep-reading.md @@ -22,19 +22,14 @@ Keshav 三阶段组会式精读。触发后执行以下工作流。 ### Step 1: Prepare(机械操作,跑脚本) ```bash -python scripts/ld_deep.py prepare --vault . --key <ZOTERO_KEY> +python scripts/ld_deep.py prepare --key <ZOTERO_KEY> ``` 返回 JSON 解析: -- `status: "ok"` → 继续 +- `status: "ok"` → 记下 `figure_map`、`chart_type_map`、`formal_note`、`fulltext_md`、`figures`、`tables` 路径和数量 → 继续 - `status: "error"` → 报告 `message` 给用户,停止 -Prepare 做的事情(Agent 不需要关心细节): -- 检查 analyze 和 ocr_status -- 生成 figure-map.json 和 chart-type-map.json -- 在 formal note 中插入 `## 🔍 精读` 骨架 - -读 formal note 确认骨架已插入。 +读 formal note 确认 `## 🔍 精读` 骨架已插入。 --- @@ -67,19 +62,16 @@ Prepare 做的事情(Agent 不需要关心细节): #### 图表类型定位(两步) -**Step A: Python 给建议(快速初筛)** -```bash -python scripts/ld_deep.py chart-type-scan --vault . --key <ZOTERO_KEY> -``` -输出每个 figure 的关键词命中结果。这只是建议,Agent 不要盲信。 +**Step A: 读 prepare 生成的 chart-type-map** +Step 1 的 `prepare` 输出中已包含 `chart_type_map` 路径。读该文件,获取每个 figure 的关键词命中结果。这只是建议。 **Step B: Agent 读 caption 做最终判断** 对每个 figure: -1. 读该 figure 的 caption(来自 fulltext.md 或 figure-map.json) +1. 读该 figure 的 caption(来自 prepare 返回的 `fulltext_md` 或 `figure_map`) 2. 根据 caption 内容,对照 [chart-reading/INDEX.md](chart-reading/INDEX.md) 判断图表类型 -3. 如果 Python 建议和 Agent 判断不一致 → 以 Agent 判断为准 -4. 如果无法确定类型 → 跳过 chart guide,按通用 figure 结构分析 +3. chart-type-map 建议和 Agent 判断不一致 → 以 Agent 判断为准 +4. 无法确定类型 → 跳过 chart guide,按通用 figure 结构分析 5. 确定类型后,读对应的 chart-reading 指南(如 `chart-reading/条形图与误差棒.md`),按指南中的检查清单分析 #### 每张 Figure 的子标题(固定,不可少) diff --git a/paperforge/skills/literature-qa/references/paper-resolution.md b/paperforge/skills/literature-qa/references/paper-resolution.md index 763425eb..42bf4eff 100644 --- a/paperforge/skills/literature-qa/references/paper-resolution.md +++ b/paperforge/skills/literature-qa/references/paper-resolution.md @@ -1,140 +1,75 @@ # 论文定位协议 -本文件定义如何将用户输入(key / DOI / 标题 / 作者年份 / 自然语言)解析为论文 workspace。所有子流程(deep-reading, paper-qa, save-session)共用此协议。 +本文件定义如何将用户输入解析为论文 workspace。所有子流程公用。 ## 核心原则 -1. **确定性输入走 Python。** key、DOI、标题片段、作者+年份 —— 这些是机器能精确处理的。 -2. **自然语言走 Agent。** "关于骨再生的那篇"、"去年那篇Nature" —— 需要 AI 理解语义。 -3. **Python 搜不到时 Agent 兜底。** 不是报错,是换个方式再试一次。 -4. **所有路径从环境变量获取。** 每条路由启动时先跑 `python -m paperforge.worker.paper_resolver env --vault . --shell pwsh | Invoke-Expression`。此后 `$env:PF_VAULT`、`$env:PF_LITERATURE_DIR`、`$env:PF_OCR_DIR`、`$env:PF_INDEX_PATH` 全程可用。不写死任何目录名。 +1. **Python 做确定性查找。** key、DOI、标题片段、作者+年份。 +2. **Agent 做理解和兜底。** 自然语言、Python 无结果时的 fallback 搜索。 +3. **路径从 `paths` 获取,不硬编码。** --- -## 前置:获取 vault 路径配置(每个路由启动时跑一次) +## 通用命令 -```bash -python -m paperforge.worker.paper_resolver paths --vault . -``` - -返回示例(所有路径均由 `paperforge.json` 动态计算): -```json -{ - "ok": true, - "data": { - "vault_root": "/path/to/vault", - "index_path": "<system_dir>/PaperForge/indexes/formal-library.json", - "literature_dir": "<resources_dir>/<literature_dir>", - "ocr_dir": "<system_dir>/PaperForge/ocr" - } -} -``` - -**后续所有路径操作使用此输出中的值,不要自己拼接。** +| 操作 | 命令 | +|------|------| +| 获取 vault 路径 | `python -m paperforge.worker.paper_resolver paths --vault .` | +| 按 key 查 | `python -m paperforge.worker.paper_resolver resolve-key <KEY> --vault .` | +| 按 DOI 查 | `python -m paperforge.worker.paper_resolver resolve-doi "<DOI>" --vault .` | +| 按字段搜 | `python -m paperforge.worker.paper_resolver search --title "..." --author "..." --year ... --domain "..." --vault .` | --- ## 输入类型判断 -### 类型 1: Zotero Key(8位字符) +### 类型 1: Zotero Key(8位字母数字组合) -**识别规则:** 8位字母数字组合,如 `XGT9Z257`、`ABC12345` - -**执行命令:** -```bash +``` python -m paperforge.worker.paper_resolver resolve-key <KEY> --vault . ``` -**返回示例(路径由 paperforge.json 配置决定,不固定):** -```json -{ - "ok": true, - "data": { - "match": { - "key": "ABC12345", - "title": "TGF-beta in Bone Regeneration", - "domain": "骨科", - "formal_note_path": "...", - "ocr_path": "...", - "fulltext_path": "...", - "ocr_status": "done" - } - } -} +返回 JSON 含 `key`, `title`, `domain`, `formal_note_path`, `ocr_path`, `fulltext_path`, `ocr_status` 等。所有路径由 `paperforge.json` 配置决定。 + +### 类型 2: DOI(以 `10.` 开头,可能带 URL 前缀) + +``` +python -m paperforge.worker.paper_resolver resolve-doi "<DOI>" --vault . ``` -**返回 `"match": null` 时:** Agent fallback — 用 `paths` 命令获取的 `literature_dir` 路径 grep frontmatter: -```bash -rg -l "zotero_key:.*ABC12345" <literature_dir>/ -``` - ---- - -### 类型 2: DOI - -**识别规则:** 以 `10.` 开头的标准 DOI 格式,可能带 URL 前缀 - -**执行命令:** -```bash -python -m paperforge.worker.paper_resolver resolve-doi "10.1016/j.jse.2018.01.001" --vault . -``` - -**返回格式同类型1。** 返回的路径直接使用,不自己拼接。 - ---- +返回格式同类型 1。 ### 类型 3: 标题片段 -**识别规则:** 看起来像论文标题的文本(含学术关键词,非自然语言问句) - -**执行命令:** -```bash -python -m paperforge.worker.paper_resolver search --title "Predictive findings on MRI" --vault . +``` +python -m paperforge.worker.paper_resolver search --title "..." --vault . ``` -**返回示例:** -```json -{ - "ok": true, - "data": { - "matches": [{ "key": "...", "title": "...", "formal_note_path": "...", ... }], - "count": 3 - } -} -``` - ---- +返回 `{"matches": [...], "count": N}`。 ### 类型 4: 作者 + 年份 -**识别规则:** 包含作者名(英文姓)和年份 - -**执行命令:** -```bash +``` python -m paperforge.worker.paper_resolver search --author "Smith" --year 2024 --vault . ``` ---- +### 类型 5: 自然语言("关于骨再生的那篇") -### 类型 5: 自然语言 - -**识别规则:** 中文自然语言描述,如 "关于骨再生的那篇"、"去年Nature上那篇讲TGF的" - -**Agent 操作:** -1. 用 `paths` 命令获取的 `index_path` 读 `formal-library.json` -2. 理解用户意图中的关键信息:主题词、年份、期刊、领域 -3. 在 JSON 的 `title`、`domain`、`journal`、`abstract` 字段中搜索匹配 -4. 如果 formal-library.json 无结果,用 `paths` 命令获取的 `literature_dir` grep formal notes: - ```bash - rg -i -l "骨再生|bone regeneration" <literature_dir>/ --include "*.md" -g "!*.canvas" - ``` -5. 读匹配的 frontmatter 确认是目标论文 +Agent 自己处理: +1. 跑 `paths` 获取 `index_path` +2. 读 `index_path` 指向的 `formal-library.json` +3. 在 `title`、`domain`、`journal`、`abstract` 中搜匹配 +4. 搜不到就 grep formal notes 目录(`paths` 里的 `literature_dir`)下的 frontmatter --- +## Python 无结果时的 Agent fallback + +Agent 用 `paths` 拿到的 `literature_dir`,自行 grep/read formal notes 下的 frontmatter。 + ## 多篇匹配处理 -当搜索返回多个匹配时,Agent 必须列出候选清单,让用户选择: +列出候选清单让用户选: ``` 找到 3 篇匹配的论文: @@ -146,28 +81,14 @@ python -m paperforge.worker.paper_resolver search --author "Smith" --year 2024 - 请输入编号选择,或 refine 搜索词。 ``` -**格式要求:** -- 编号 + key + title + (year, domain, ocr_status) -- 不要只列标题或只列 key - ---- - ## Fallback 顺序 ``` 输入 │ - ├── 看起来像 key/DOI/标题/作者年份? - │ └── YES → Python paper_resolver - │ ├── 有结果 → 使用 - │ └── 无结果 → Agent grep fallback - │ ├── 有结果 → 使用 - │ └── 无结果 → 告知用户 + ├── 像 key/DOI/标题/作者年份? + │ └── Python paper_resolver → 有/无结果 → Agent 兜底 │ - └── 看起来像自然语言? - └── Agent 读 formal-library.json - ├── 有结果 → 列出/使用 - └── 无结果 → Agent grep fallback - ├── 有结果 → 使用 - └── 无结果 → 告知用户 + └── 自然语言? + └── Agent 读 formal-library.json → 搜 → 有/无 ``` diff --git a/paperforge/skills/literature-qa/scripts/ld_deep.py b/paperforge/skills/literature-qa/scripts/ld_deep.py index 3e613f39..74f76f67 100644 --- a/paperforge/skills/literature-qa/scripts/ld_deep.py +++ b/paperforge/skills/literature-qa/scripts/ld_deep.py @@ -1589,11 +1589,11 @@ def main() -> int: full_validate_parser.add_argument("--tables", help="Comma-separated selected table numbers to validate") queue_parser = subparsers.add_parser("queue", help="List papers awaiting deep reading from canonical index") - queue_parser.add_argument("--vault", type=Path, required=True, help="Path to the vault root") + queue_parser.add_argument("--vault", type=Path, default=None, help="Path to the vault root (auto-detected if omitted)") queue_parser.add_argument("--format", choices=["json", "table"], default="json", help="Output format") prepare_parser = subparsers.add_parser("prepare", help="Auto-detect and prepare a paper for deep reading") - prepare_parser.add_argument("--vault", type=Path, required=True, help="Path to the vault root") + prepare_parser.add_argument("--vault", type=Path, default=None, help="Path to the vault root (auto-detected if omitted)") prepare_parser.add_argument("--key", required=True, help="Zotero key") prepare_parser.add_argument("--force", action="store_true", help="Re-run even if deep_reading_status=done") prepare_parser.add_argument("--format", choices=["json", "table"], default="json", help="Output format") @@ -1618,6 +1618,11 @@ def main() -> int: args = parser.parse_args() + if args.command in ("prepare", "queue"): + if args.vault is None: + from paperforge.config import resolve_vault as _resolve_vault + args.vault = _resolve_vault(Path.cwd()) + if args.command == "prepare": result = prepare_deep_reading(args.vault, args.key, force=args.force) if args.format == "json": diff --git a/paperforge/worker/paper_resolver.py b/paperforge/worker/paper_resolver.py new file mode 100644 index 00000000..7c860be6 --- /dev/null +++ b/paperforge/worker/paper_resolver.py @@ -0,0 +1,384 @@ +"""Deterministic paper lookup engine. + +Resolves papers by key, DOI, or structured field search from formal-library.json. +Natural language queries are handled by the Agent — this module handles only +deterministic lookups. +""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +from dataclasses import dataclass, field +from pathlib import Path +from typing import Optional + + +@dataclass +class PaperMeta: + """Lightweight paper metadata for structured search results.""" + + key: str + title: str + domain: str + year: str = "" + authors: str = "" + doi: str = "" + journal: str = "" + collection_path: str = "" + lifecycle: str = "" + + +@dataclass +class PaperWorkspace: + """Full workspace paths and metadata for a resolved paper.""" + + key: str + title: str + domain: str + formal_note_path: str + ocr_path: str + fulltext_path: str + frontmatter: dict = field(default_factory=dict) + year: str = "" + authors: str = "" + doi: str = "" + journal: str = "" + ocr_status: str = "" + deep_reading_status: str = "" + analyze: bool = False + do_ocr: bool = False + has_pdf: bool = False + + +class PaperResolver: + """Deterministic paper lookup from formal-library.json index.""" + + def __init__(self, vault: Path): + self._vault = vault + self._paths: dict[str, Path] = {} + self._items: list[dict] = [] + self._loaded = False + + def _ensure_loaded(self): + if self._loaded: + return + try: + from paperforge.config import paperforge_paths + + self._paths = paperforge_paths(self._vault) + except Exception: + self._paths = {} + try: + from paperforge.worker.asset_index import read_index + + data = read_index(self._vault) + if isinstance(data, dict): + self._items = data.get("items", []) + except Exception: + self._items = [] + self._loaded = True + + def resolve_key(self, key: str) -> Optional[PaperWorkspace]: + """Exact match on zotero_key.""" + self._ensure_loaded() + for entry in self._items: + if entry.get("zotero_key", "").strip().upper() == key.strip().upper(): + return self._build_workspace(entry) + return None + + def resolve_doi(self, doi: str) -> Optional[PaperWorkspace]: + """Exact match on DOI (case-insensitive, normalized).""" + self._ensure_loaded() + normalized = self._normalize_doi(doi) + for entry in self._items: + entry_doi = self._normalize_doi(entry.get("doi", "")) + if entry_doi and entry_doi == normalized: + return self._build_workspace(entry) + return None + + def search( + self, + title: Optional[str] = None, + author: Optional[str] = None, + year: Optional[int | str] = None, + domain: Optional[str] = None, + limit: int = 20, + ) -> list[PaperWorkspace]: + """Multi-field search with substring matching, sorted by relevance score. + + All supplied fields must match (AND logic). Each field contributes to + a relevance score for ranking. + """ + self._ensure_loaded() + scored: list[tuple[int, PaperWorkspace]] = [] + + title_lower = title.strip().lower() if title else None + author_lower = author.strip().lower() if author else None + year_str = str(year).strip() if year is not None else None + domain_lower = domain.strip().lower() if domain else None + + for entry in self._items: + score = 0 + entry_title = (entry.get("title") or "").lower() + entry_authors = _flatten_authors(entry.get("authors", "")).lower() + entry_year = str(entry.get("year", "")) + entry_domain = (entry.get("domain") or "").lower() + + if title_lower: + if title_lower == entry_title: + score += 100 + elif title_lower in entry_title: + score += 30 + else: + continue + if author_lower: + if author_lower in entry_authors: + score += 50 + else: + continue + if year_str: + if year_str == entry_year: + score += 40 + else: + continue + if domain_lower: + if domain_lower == entry_domain: + score += 20 + elif domain_lower in entry_domain: + score += 10 + else: + continue + + if score > 0 or not any([title_lower, author_lower, year_str, domain_lower]): + scored.append((score, self._build_workspace(entry))) + + scored.sort(key=lambda x: x[0], reverse=True) + return [ws for _, ws in scored[:limit]] + + def _build_workspace(self, entry: dict) -> PaperWorkspace: + """Build a PaperWorkspace from an index entry dict.""" + key = entry.get("zotero_key", "") + title = entry.get("title", "") + domain = entry.get("domain", "") + note_path = entry.get("note_path", "") + + ocr_base = _resolve_ocr_base(self._paths, key) + fulltext_path = entry.get("fulltext_path", "") + if not fulltext_path and ocr_base: + fulltext_path = str(ocr_base / "fulltext.md") + + frontmatter = {} + if note_path: + try: + note_full = self._vault / note_path + if note_full.exists(): + from paperforge.adapters.obsidian_frontmatter import read_frontmatter_dict + + frontmatter = read_frontmatter_dict(note_full.read_text(encoding="utf-8")) + except Exception: + pass + + authors_raw = entry.get("authors", "") + if isinstance(authors_raw, list): + authors_str = "; ".join(authors_raw) + else: + authors_str = str(authors_raw) + + return PaperWorkspace( + key=key, + title=title, + domain=domain, + formal_note_path=note_path, + ocr_path=str(ocr_base) if ocr_base else "", + fulltext_path=fulltext_path, + frontmatter=frontmatter, + year=str(entry.get("year", "")), + authors=authors_str, + doi=entry.get("doi", ""), + journal=entry.get("journal", ""), + ocr_status=entry.get("ocr_status", ""), + deep_reading_status=entry.get("deep_reading_status", ""), + analyze=bool(entry.get("analyze", False)), + do_ocr=bool(entry.get("do_ocr", False)), + has_pdf=bool(entry.get("has_pdf", False)), + ) + + @staticmethod + def _normalize_doi(doi: str) -> str: + """Normalize DOI to lowercase, strip URL prefixes.""" + s = doi.strip().lower() + s = re.sub(r"^https?://(dx\.)?doi\.org/", "", s) + return s + + +def _resolve_ocr_base(paths: dict[str, Path], key: str) -> Optional[Path]: + """Get the OCR directory for a given zotero key.""" + ocr_dir = paths.get("ocr") + if ocr_dir and key: + candidate = ocr_dir / key + if candidate.exists(): + return candidate + return None + + +def _flatten_authors(authors) -> str: + """Convert authors (list or str) to a single searchable string.""" + if isinstance(authors, list): + return "; ".join(authors) + return str(authors) + + +# --------------------------------------------------------------------------- +# CLI entry point +# --------------------------------------------------------------------------- + + +def _ok_result(command: str, data: dict) -> str: + """Produce a PFResult-compatible JSON string to stdout.""" + from paperforge import __version__ + + result = { + "ok": True, + "command": command, + "version": __version__, + "data": data, + "error": None, + } + return json.dumps(result, ensure_ascii=False, indent=2) + + +def _err_result(command: str, message: str) -> str: + from paperforge import __version__ + + result = { + "ok": False, + "command": command, + "version": __version__, + "data": None, + "error": {"code": "INTERNAL_ERROR", "message": message}, + } + return json.dumps(result, ensure_ascii=False, indent=2) + + +def _workspace_to_dict(ws: PaperWorkspace) -> dict: + return { + "key": ws.key, + "title": ws.title, + "domain": ws.domain, + "year": ws.year, + "authors": ws.authors, + "doi": ws.doi, + "journal": ws.journal, + "formal_note_path": ws.formal_note_path, + "ocr_path": ws.ocr_path, + "fulltext_path": ws.fulltext_path, + "ocr_status": ws.ocr_status, + "deep_reading_status": ws.deep_reading_status, + "analyze": ws.analyze, + "do_ocr": ws.do_ocr, + "has_pdf": ws.has_pdf, + } + + +def main(): + parser = argparse.ArgumentParser(description="PaperForge paper resolver") + sub = parser.add_subparsers(dest="command", required=True) + + rk = sub.add_parser("resolve-key") + rk.add_argument("key", help="Zotero key (8-char alphanumeric)") + rk.add_argument("--vault", required=True, help="Path to vault root") + + rd = sub.add_parser("resolve-doi") + rd.add_argument("doi", help="DOI string") + rd.add_argument("--vault", required=True, help="Path to vault root") + + sr = sub.add_parser("search") + sr.add_argument("--title", default=None) + sr.add_argument("--author", default=None) + sr.add_argument("--year", default=None) + sr.add_argument("--domain", default=None) + sr.add_argument("--limit", type=int, default=20) + sr.add_argument("--vault", required=True, help="Path to vault root") + + pt = sub.add_parser("paths") + pt.add_argument("--vault", required=True, help="Path to vault root") + + args = parser.parse_args() + + try: + vault = Path(args.vault).resolve() + if not vault.exists(): + print(_err_result(args.command, f"Vault not found: {args.vault}"), file=sys.stdout) + sys.exit(1) + + resolver = PaperResolver(vault) + + if args.command == "resolve-key": + ws = resolver.resolve_key(args.key) + if ws: + print(_ok_result("resolve-key", {"match": _workspace_to_dict(ws)}), file=sys.stdout) + else: + print( + json.dumps({ + "ok": True, + "command": "resolve-key", + "data": {"match": None, "message": f"No paper found with key: {args.key}"}, + "error": None, + }, ensure_ascii=False, indent=2), + file=sys.stdout, + ) + + elif args.command == "resolve-doi": + ws = resolver.resolve_doi(args.doi) + if ws: + print(_ok_result("resolve-doi", {"match": _workspace_to_dict(ws)}), file=sys.stdout) + else: + print( + json.dumps({ + "ok": True, + "command": "resolve-doi", + "data": {"match": None, "message": f"No paper found with DOI: {args.doi}"}, + "error": None, + }, ensure_ascii=False, indent=2), + file=sys.stdout, + ) + + elif args.command == "search": + results = resolver.search( + title=args.title, + author=args.author, + year=args.year, + domain=args.domain, + limit=args.limit, + ) + print( + _ok_result( + "search", + { + "matches": [_workspace_to_dict(ws) for ws in results], + "count": len(results), + }, + ), + file=sys.stdout, + ) + + elif args.command == "paths": + resolver._ensure_loaded() + paths_data = { + "vault_root": str(resolver._vault), + "index_path": str(resolver._paths.get("index", "")), + "literature_dir": str(resolver._paths.get("literature", "")), + "ocr_dir": str(resolver._paths.get("ocr", "")), + } + print(_ok_result("paths", paths_data), file=sys.stdout) + + except Exception as exc: + print(_err_result(args.command, str(exc)), file=sys.stdout) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/tests/unit/test_paper_resolver.py b/tests/unit/test_paper_resolver.py new file mode 100644 index 00000000..d8360de1 --- /dev/null +++ b/tests/unit/test_paper_resolver.py @@ -0,0 +1,361 @@ +"""Tests for paperforge.worker.paper_resolver.""" + +import json +from pathlib import Path + +import pytest + +from paperforge.worker.paper_resolver import PaperResolver, PaperWorkspace + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +def _make_index_entry( + key="ABC12345", + title="Test Paper Title", + domain="骨科", + year=2024, + authors="Smith J; Doe J", + doi="10.1234/test.2024", + journal="Journal of Testing", + note_path=None, + ocr_status="done", + deep_reading_status="pending", + analyze=True, + do_ocr=True, + has_pdf=True, + **overrides, +): + if note_path is None: + note_path = f"Literature/{domain}/{key} - {title}.md" + entry = { + "zotero_key": key, + "title": title, + "domain": domain, + "year": year, + "authors": authors.split("; "), + "doi": doi, + "journal": journal, + "note_path": note_path, + "ocr_status": ocr_status, + "deep_reading_status": deep_reading_status, + "analyze": analyze, + "do_ocr": do_ocr, + "has_pdf": has_pdf, + "fulltext_path": "", + "collection_path": "", + "first_author": authors.split("; ")[0] if authors else "", + "abstract": "This is a test abstract.", + "impact_factor": "5.0", + "pmid": "", + "collections": [], + "collection_tags": [], + "collection_group": [], + "pdf_path": "", + "ocr_job_id": "", + "ocr_md_path": "", + "ocr_json_path": "", + "deep_reading_md_path": "", + "paper_root": "", + "main_note_path": "", + "deep_reading_path": "", + "ai_path": "", + "lifecycle": "fulltext_ready", + "health": {}, + "maturity": {}, + "next_step": "/pf-deep", + } + entry.update(overrides) + return entry + + +def _make_index_envelope(items): + return {"schema_version": "2", "generated_at": "2026-01-01T00:00:00", "paper_count": len(items), "items": items} + + +def _write_index(path: Path, envelope: dict): + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(envelope, ensure_ascii=False), encoding="utf-8") + + +def _write_vault_config(vault: Path): + (vault / "paperforge.json").write_text( + json.dumps( + { + "vault_config": { + "system_dir": "99_System", + "resources_dir": "03_Resources", + "literature_dir": "Literature", + } + } + ), + encoding="utf-8", + ) + + +# --------------------------------------------------------------------------- +# Patch PaperResolver to bypass real file system dependencies +# --------------------------------------------------------------------------- + + +@pytest.fixture +def resolver_with_index(tmp_path): + """Create a PaperResolver preloaded with a test index.""" + vault = tmp_path / "test_vault" + vault.mkdir(parents=True) + _write_vault_config(vault) + + # Create index path structure + indexes_dir = vault / "99_System" / "PaperForge" / "indexes" + indexes_dir.mkdir(parents=True) + + items = [ + _make_index_entry(key="ABC12345", title="TGF-beta in Bone Regeneration", domain="骨科", year=2024, authors="Smith J; Lee K", doi="10.1234/tgf.2024"), + _make_index_entry(key="DEF67890", title="MRI Predictive Findings in Rotator Cuff", domain="运动医学", year=2023, authors="Chen W; Wang L", doi="10.5678/mri.2023"), + _make_index_entry(key="GHI11111", title="Another Bone Study", domain="骨科", year=2024, authors="Smith J; Zhang Y", doi="10.9999/bone.2024"), + ] + _write_index(indexes_dir / "formal-library.json", _make_index_envelope(items)) + + # Create literature dirs and minimal formal notes + for item in items: + note_dir = vault / Path(item["note_path"]).parent + note_dir.mkdir(parents=True, exist_ok=True) + frontmatter = f"""--- +zotero_key: "{item['zotero_key']}" +domain: "{item['domain']}" +title: "{item['title']}" +year: {item['year']} +doi: "{item['doi']}" +ocr_status: "{item['ocr_status']}" +deep_reading_status: "{item['deep_reading_status']}" +analyze: {str(item['analyze']).lower()} +do_ocr: {str(item['do_ocr']).lower()} +has_pdf: {str(item['has_pdf']).lower()} +--- +""" + (vault / item["note_path"]).write_text(frontmatter, encoding="utf-8") + + # Create OCR dir for ABC12345 + ocr_dir = vault / "99_System" / "PaperForge" / "ocr" / "ABC12345" + ocr_dir.mkdir(parents=True) + (ocr_dir / "fulltext.md").write_text("# Fulltext\nTest content.", encoding="utf-8") + + resolver = PaperResolver(vault) + resolver._ensure_loaded() + return resolver + + +@pytest.fixture +def empty_resolver(tmp_path): + """Create a PaperResolver with no index data.""" + vault = tmp_path / "empty_vault" + vault.mkdir(parents=True) + _write_vault_config(vault) + return PaperResolver(vault) + + +# --------------------------------------------------------------------------- +# resolve_key tests +# --------------------------------------------------------------------------- + + +class TestResolveKey: + def test_exact_match(self, resolver_with_index): + ws = resolver_with_index.resolve_key("ABC12345") + assert ws is not None + assert ws.key == "ABC12345" + assert ws.title == "TGF-beta in Bone Regeneration" + assert ws.domain == "骨科" + assert ws.year == "2024" + + def test_case_insensitive(self, resolver_with_index): + ws = resolver_with_index.resolve_key("abc12345") + assert ws is not None + assert ws.key == "ABC12345" + + def test_no_match(self, resolver_with_index): + ws = resolver_with_index.resolve_key("ZZZZZZZZ") + assert ws is None + + def test_empty_key(self, resolver_with_index): + ws = resolver_with_index.resolve_key("") + assert ws is None + + def test_whitespace_trimmed(self, resolver_with_index): + ws = resolver_with_index.resolve_key(" ABC12345 ") + assert ws is not None + assert ws.key == "ABC12345" + + +# --------------------------------------------------------------------------- +# resolve_doi tests +# --------------------------------------------------------------------------- + + +class TestResolveDoi: + def test_exact_match(self, resolver_with_index): + ws = resolver_with_index.resolve_doi("10.1234/tgf.2024") + assert ws is not None + assert ws.key == "ABC12345" + + def test_case_insensitive(self, resolver_with_index): + ws = resolver_with_index.resolve_doi("10.1234/TGF.2024") + assert ws is not None + assert ws.key == "ABC12345" + + def test_with_url_prefix(self, resolver_with_index): + ws = resolver_with_index.resolve_doi("https://doi.org/10.1234/tgf.2024") + assert ws is not None + assert ws.key == "ABC12345" + + def test_with_dx_prefix(self, resolver_with_index): + ws = resolver_with_index.resolve_doi("http://dx.doi.org/10.1234/tgf.2024") + assert ws is not None + assert ws.key == "ABC12345" + + def test_no_match(self, resolver_with_index): + ws = resolver_with_index.resolve_doi("10.0000/nonexistent") + assert ws is None + + def test_empty_doi_entries_handled(self, resolver_with_index): + items = resolver_with_index._items + for item in items: + if item["zotero_key"] == "GHI11111": + break + ws = resolver_with_index.resolve_doi("10.9999/bone.2024") + assert ws is not None + assert ws.key == "GHI11111" + + +# --------------------------------------------------------------------------- +# search tests +# --------------------------------------------------------------------------- + + +class TestSearch: + def test_search_by_title_exact(self, resolver_with_index): + results = resolver_with_index.search(title="TGF-beta in Bone Regeneration") + assert len(results) == 1 + assert results[0].key == "ABC12345" + + def test_search_by_title_substring(self, resolver_with_index): + results = resolver_with_index.search(title="Bone") + assert len(results) == 2 # ABC12345 + GHI11111 + keys = {r.key for r in results} + assert "ABC12345" in keys + assert "GHI11111" in keys + + def test_search_by_author(self, resolver_with_index): + results = resolver_with_index.search(author="Smith") + assert len(results) == 2 # ABC12345 + GHI11111 + + def test_search_by_year(self, resolver_with_index): + results = resolver_with_index.search(year=2023) + assert len(results) == 1 + assert results[0].key == "DEF67890" + + def test_search_by_domain(self, resolver_with_index): + results = resolver_with_index.search(domain="运动医学") + assert len(results) == 1 + assert results[0].key == "DEF67890" + + def test_search_combined_and_logic(self, resolver_with_index): + results = resolver_with_index.search(title="Bone", author="Smith", year=2024) + assert len(results) == 2 # Both ABC and GHI match all three + + def test_search_no_match_combined(self, resolver_with_index): + results = resolver_with_index.search(title="Bone", year=2023) + assert len(results) == 0 # No bone papers in 2023 + + def test_search_ranked_by_relevance(self, resolver_with_index): + results = resolver_with_index.search(title="Bone") + assert len(results) >= 1 + # Exact match (TGF-beta in Bone Regeneration) should rank higher + # than substring match (Another Bone Study) + assert results[0].key in ("ABC12345", "GHI11111") + + def test_search_returns_workspace_fields(self, resolver_with_index): + results = resolver_with_index.search(title="TGF-beta") + ws = results[0] + assert isinstance(ws, PaperWorkspace) + assert ws.key == "ABC12345" + assert ws.formal_note_path + assert ws.frontmatter + assert ws.frontmatter["zotero_key"] == "ABC12345" + + +# --------------------------------------------------------------------------- +# PaperWorkspace tests +# --------------------------------------------------------------------------- + + +class TestPaperWorkspace: + def test_has_all_fields(self, resolver_with_index): + ws = resolver_with_index.resolve_key("ABC12345") + assert ws is not None + assert ws.key == "ABC12345" + assert ws.title + assert ws.domain + assert ws.year + assert ws.authors + assert ws.doi + assert ws.journal + assert ws.formal_note_path + assert ws.ocr_status == "done" + assert ws.deep_reading_status == "pending" + assert ws.analyze is True + assert ws.do_ocr is True + assert ws.has_pdf is True + + def test_ocr_path_when_ocr_dir_exists(self, resolver_with_index): + ws = resolver_with_index.resolve_key("ABC12345") + assert ws is not None + assert ws.ocr_path + assert "ABC12345" in ws.ocr_path + + def test_frontmatter_loaded(self, resolver_with_index): + ws = resolver_with_index.resolve_key("ABC12345") + assert ws is not None + fm = ws.frontmatter + assert fm["zotero_key"] == "ABC12345" + assert fm["domain"] == "骨科" + assert fm["title"] == "TGF-beta in Bone Regeneration" + + +# --------------------------------------------------------------------------- +# Edge cases +# --------------------------------------------------------------------------- + + +class TestEdgeCases: + def test_empty_index(self, empty_resolver): + empty_resolver._ensure_loaded() + assert empty_resolver.resolve_key("ABC12345") is None + assert empty_resolver.resolve_doi("10.1234/test") is None + assert empty_resolver.search(title="anything") == [] + + def test_authors_as_list(self, resolver_with_index): + ws = resolver_with_index.resolve_key("ABC12345") + assert ws is not None + assert "Smith" in ws.authors + assert "Lee" in ws.authors + + def test_missing_optional_fields(self, resolver_with_index): + items = resolver_with_index._items + extra = _make_index_entry( + key="XXX00000", + title="Minimal Paper", + domain="测试", + year="", + authors="", + doi="", + note_path="Literature/测试/XXX00000 - Minimal Paper.md", + ) + items.append(extra) + ws = resolver_with_index.resolve_key("XXX00000") + assert ws is not None + assert ws.year == "" + assert ws.doi == "" From 78a8ed312e0d3cd5a275e0ee41aa519e89a5a43a Mon Sep 17 00:00:00 2001 From: Research Assistant <research@example.com> Date: Sun, 10 May 2026 20:46:04 +0800 Subject: [PATCH 31/37] fix(skills): narrow description triggers to avoid false matches --- paperforge/skills/literature-qa/SKILL.md | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/paperforge/skills/literature-qa/SKILL.md b/paperforge/skills/literature-qa/SKILL.md index 83728fbf..63d4618e 100644 --- a/paperforge/skills/literature-qa/SKILL.md +++ b/paperforge/skills/literature-qa/SKILL.md @@ -1,16 +1,11 @@ --- name: literature-qa description: > - 学术文献精读与问答。MUST trigger when user types /pf-deep /pf-paper /pf-end - or says 精读 深度阅读 读一下这篇 读文献 带我读 组会讲这篇 帮我精读 讲讲这篇论文 - 查一下这篇论文 帮我看看这篇文章 这篇文章讲了什么 这篇讲了什么 论文问答 做这篇的问答 - 保存讨论 结束讨论 保存记录 完成讨论 精读队列 有哪些该读了 - or uses natural language like 那篇关于XX的文章 去年那篇XX 找一下XX的文献 - or any phrase about reading analyzing deep-reading papers in their Zotero library. - 支持 Zotero key DOI 标题 作者 年份 自然语言描述定位论文. - Routes intent internally via routing table—no separate sub-skills needed. + 学术文献精读与问答。Triggered by: /pf-deep /pf-paper /pf-end, + "精读 <key/title>", "文献问答 <key/title>", "结束讨论", "保存记录", + "精读队列". Uses Zotero key, DOI, or title to locate papers. license: Apache-2.0 -compatibility: opencode +compatibility: all --- # Literature QA — 学术文献精读与问答 From 7abb90b91bc7662af1ccc784a5b45c75124101f2 Mon Sep 17 00:00:00 2001 From: Research Assistant <research@example.com> Date: Sun, 10 May 2026 21:05:05 +0800 Subject: [PATCH 32/37] test: update bootstrap test to match fixed agents_src code --- tests/test_plugin_install_bootstrap.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_plugin_install_bootstrap.py b/tests/test_plugin_install_bootstrap.py index 6232f860..671c9265 100644 --- a/tests/test_plugin_install_bootstrap.py +++ b/tests/test_plugin_install_bootstrap.py @@ -46,7 +46,8 @@ def test_setup_wizard_flat_command_falls_back_to_packaged_command_files() -> Non def test_setup_wizard_does_not_fail_if_agents_md_missing() -> None: """AGENTS.md is optional for pip-installed package deployments.""" source = (REPO_ROOT / "paperforge" / "setup_wizard.py").read_text(encoding="utf-8") - assert 'True if not agents_src.exists() else agents_dst.exists()' in source + assert 'True if not agents_src.exists() else agents_dst.exists()' not in source + assert '(vault / "AGENTS.md").exists()' in source def test_setup_wizard_uses_manual_zotero_path_for_checks() -> None: From 0c31f89d4d2d75a38db674781d30af01a3031804 Mon Sep 17 00:00:00 2001 From: Research Assistant <research@example.com> Date: Sun, 10 May 2026 21:50:27 +0800 Subject: [PATCH 33/37] =?UTF-8?q?feat:=20add=20deep-finalize=20command=20?= =?UTF-8?q?=E2=80=94=20signal=20dashboard=20on=20/pf-deep=20completion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - new command paperforge deep-finalize <key>: sets deep_reading_status=done in frontmatter + refreshes index - dashboard now only auto-refreshes on formal-library.json changes (removed per-note modify handler) - pf-deep reference updated with Post-Processing section instructing agent to call deep-finalize at end --- paperforge/cli.py | 10 +++ paperforge/command_files/pf-deep.md | 16 +++++ paperforge/commands/__init__.py | 1 + paperforge/commands/finalize.py | 95 +++++++++++++++++++++++++++++ paperforge/plugin/main.js | 7 +-- 5 files changed, 123 insertions(+), 6 deletions(-) create mode 100644 paperforge/commands/finalize.py diff --git a/paperforge/cli.py b/paperforge/cli.py index 1cb7c790..d46b5e93 100644 --- a/paperforge/cli.py +++ b/paperforge/cli.py @@ -197,6 +197,11 @@ def build_parser() -> argparse.ArgumentParser: p_dr = sub.add_parser("deep-reading", help="Check deep-reading queue status") p_dr.add_argument("--json", action="store_true", help="Output as PFResult JSON") + # deep-finalize + p_df = sub.add_parser("deep-finalize", help="Mark deep reading done and notify dashboard") + p_df.add_argument("zotero_key", help="Zotero citation key") + p_df.add_argument("--json", action="store_true", help="Output as PFResult JSON") + # repair p_repair = sub.add_parser("repair", help="Repair divergent literature notes") p_repair.add_argument("--fix", action="store_true", help="Actually apply repairs instead of dry-run") @@ -445,6 +450,11 @@ def main(argv: list[str] | None = None) -> int: return deep.run(args) + if args.command == "deep-finalize": + from paperforge.commands import finalize + + return finalize.run(args) + if args.command == "context": from paperforge.commands import context diff --git a/paperforge/command_files/pf-deep.md b/paperforge/command_files/pf-deep.md index 32658f9d..535ff591 100644 --- a/paperforge/command_files/pf-deep.md +++ b/paperforge/command_files/pf-deep.md @@ -83,6 +83,21 @@ Agent 在正式笔记中创建或更新 `## 精读` 区域,包含: - **Pass 2: 精读还原** — Figure-by-Figure 解析、Table-by-Table 解析、关键方法补课、主要发现与新意 - **Pass 3: 深度理解** — 假设挑战与隐藏缺陷、结论扎实性评估、Discussion 解读、个人启发、遗留问题 +## Post-Processing (必须执行) + +精读内容全部写完后,Agent **必须**调用以下命令完成收尾: + +```bash +paperforge deep-finalize <zotero_key> +``` + +该命令会: +1. 将正式笔记 frontmatter 中的 `deep_reading_status` 设为 `done` +2. 刷新 canonical index(写入 `formal-library.json`) +3. Dashboard 检测到 index 变更后自动刷新 `文章概览` 卡片 + +> `deep_reading_status` 只在精读全部完成(内容填写 + 验证通过)后才标记为 `done`。不要在准备阶段或中途设置此状态。 + ## Error Handling ### OCR 未完成 @@ -148,6 +163,7 @@ Task( 2. 用 Bash tool 预跑 `python {{SCRIPT}} figure-index {{FULLTEXT_MD}}` 确认 OCR 存在 3. 四个 Task 并行启动,每篇独立 4. 等待所有 Task 完成,收集各篇的写入行数和验证结果 +5. 对每篇已完成精读的论文,调用 `paperforge deep-finalize <zotero_key>` 标记完成并触发 Dashboard 刷新 **预检(必须)**: diff --git a/paperforge/commands/__init__.py b/paperforge/commands/__init__.py index 4a570f2f..63dc3add 100644 --- a/paperforge/commands/__init__.py +++ b/paperforge/commands/__init__.py @@ -9,6 +9,7 @@ _COMMAND_REGISTRY: dict[str, str] = { "status": "paperforge.commands.status", "context": "paperforge.commands.context", "dashboard": "paperforge.commands.dashboard", + "finalize": "paperforge.commands.finalize", } diff --git a/paperforge/commands/finalize.py b/paperforge/commands/finalize.py new file mode 100644 index 00000000..f4ab74e8 --- /dev/null +++ b/paperforge/commands/finalize.py @@ -0,0 +1,95 @@ +"""deep-finalize — mark deep reading as done and signal dashboard to refresh.""" + +from __future__ import annotations + +import argparse +import re +import sys +from pathlib import Path + +from paperforge import __version__ +from paperforge.core.result import PFResult + + +def _update_frontmatter(text: str, key: str, value: str) -> str: + """Replace or insert a frontmatter field value.""" + pattern = re.compile(rf"^({re.escape(key)}:\s*)(.*?)$", re.MULTILINE) + if pattern.search(text): + return pattern.sub(rf"\g<1>{value}", text) + # Insert after the first '---' separator + fm_end = text.find("---\n", 3) + if fm_end != -1: + return text[:fm_end] + f"{key}: {value}\n" + text[fm_end:] + return text + + +def run(args: argparse.Namespace) -> int: + """Mark deep reading as done and refresh the canonical index. + + Called by the AI agent at the end of /pf-deep to signal completion. + The dashboard watches formal-library.json and will refresh on change. + """ + vault: Path = getattr(args, "vault_path", None) + if vault is None: + from paperforge.config import resolve_vault + + vault = resolve_vault(cli_vault=getattr(args, "vault", None)) + + zotero_key: str | None = getattr(args, "zotero_key", None) + if not zotero_key: + print("[ERROR] zotero_key is required", file=sys.stderr) + if getattr(args, "json", False): + pf = PFResult(ok=False, command="deep-finalize", version=__version__, error="zotero_key is required") + print(pf.to_json()) + return 1 + + # 1. Find and update the formal note frontmatter + from paperforge.worker._utils import pipeline_paths + + paths = pipeline_paths(vault) + lit_root = paths["literature"] + note_updated = False + + if lit_root.exists(): + for note_file in lit_root.rglob("*.md"): + if note_file.name in ("fulltext.md", "deep-reading.md", "discussion.md"): + continue + try: + text = note_file.read_text(encoding="utf-8") + except Exception: + continue + if re.search(rf'^\s*zotero_key:\s*"{re.escape(zotero_key)}"', text, re.MULTILINE): + new_text = _update_frontmatter(text, "deep_reading_status", "done") + note_file.write_text(new_text, encoding="utf-8") + note_updated = True + break + + # 2. Refresh the canonical index entry (writes formal-library.json) + from paperforge.worker.asset_index import refresh_index_entry + + index_ok = refresh_index_entry(vault, zotero_key) + + msg_parts = [] + if note_updated: + msg_parts.append("frontmatter updated") + else: + msg_parts.append("frontmatter not found (index-only refresh)") + msg_parts.append("index refreshed" if index_ok else "full index rebuild triggered") + + message = f"[OK] deep-finalize {zotero_key}: " + ", ".join(msg_parts) + print(message) + + if getattr(args, "json", False): + pf = PFResult( + ok=True, + command="deep-finalize", + version=__version__, + data={ + "zotero_key": zotero_key, + "note_updated": note_updated, + "index_refreshed": index_ok, + }, + ) + print(pf.to_json()) + + return 0 diff --git a/paperforge/plugin/main.js b/paperforge/plugin/main.js index d1446470..e610303a 100644 --- a/paperforge/plugin/main.js +++ b/paperforge/plugin/main.js @@ -2110,16 +2110,11 @@ class PaperForgeStatusView extends ItemView { }); this._modeSubscribers.push({ event: 'active-leaf-change', ref: leafHandler }); - // D-09: File modification -- filter to formal-library.json only + // D-09: File modification -- formal-library.json only (deep-finalize signals completion) const modifyHandler = this.app.vault.on('modify', (file) => { if (file && file.path && file.path.endsWith('formal-library.json')) { this._invalidateIndex(); // D-14: invalidate cache this._refreshCurrentMode(); - return; - } - if (file && this._currentPaperEntry && file.path === this._currentPaperEntry.note_path) { - this._currentPaperEntry = this._findEntry(this._currentPaperKey); - this._refreshCurrentMode(); } }); this._modeSubscribers.push({ event: 'modify', ref: modifyHandler }); From f8c05959edc35c4ee48c5f451c2082c57e7e05b9 Mon Sep 17 00:00:00 2001 From: Research Assistant <research@example.com> Date: Sun, 10 May 2026 22:22:31 +0800 Subject: [PATCH 34/37] style: align collection workflow overview with global library snapshot - Match outer frame padding/background/shadow to library-snapshot - Match inner stage card padding/background/layout to snapshot-pill - Use align-items: stretch on funnel (pill parity), arrow stays centered - Reduce collection-header bottom gap for tighter domain-to-card spacing - Synchronize stage-label font-size (12px -> 11px) --- paperforge/plugin/styles.css | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/paperforge/plugin/styles.css b/paperforge/plugin/styles.css index 1ba83d44..31f0725b 100644 --- a/paperforge/plugin/styles.css +++ b/paperforge/plugin/styles.css @@ -2085,7 +2085,8 @@ /* Collection mode */ .paperforge-collection-header { - padding: 14px 0 0 0; + padding: 10px 0 0 0; + margin-bottom: -12px; } .paperforge-collection-title { @@ -2112,25 +2113,24 @@ } .paperforge-workflow-overview { - margin: 10px 0; - padding: 18px; - background: color-mix(in srgb, var(--background-primary) 94%, var(--background-primary-alt) 6%); + padding: 20px; + background: color-mix(in srgb, var(--background-primary) 92%, var(--background-secondary) 8%); border: 1px solid color-mix(in srgb, var(--pf-warm-line) 20%, var(--pf-border)); border-radius: calc(var(--pf-radius) + 2px); - box-shadow: inset 3px 0 0 color-mix(in srgb, var(--pf-collection-accent) 12%, transparent); + box-shadow: 0 1px 2px rgba(20, 18, 14, 0.04), 0 10px 24px rgba(20, 18, 14, 0.04); transition: border-color 0.2s, box-shadow 0.2s; } .paperforge-workflow-overview:hover { border-color: color-mix(in srgb, var(--pf-warm-line) 42%, var(--pf-border)); - box-shadow: inset 3px 0 0 color-mix(in srgb, var(--pf-collection-accent) 17%, transparent), 0 2px 10px rgba(20, 18, 14, 0.08); + box-shadow: 0 2px 10px rgba(20, 18, 14, 0.08), 0 14px 28px rgba(20, 18, 14, 0.06); } .paperforge-workflow-funnel { display: grid; grid-template-columns: 1fr auto 1fr auto 1fr auto 1fr; - gap: 8px 6px; - align-items: center; + gap: 10px 6px; + align-items: stretch; } @container pfpanel (max-width: 380px) { @@ -2163,8 +2163,9 @@ display: flex; flex-direction: column; align-items: center; - padding: 12px 6px; - background: color-mix(in srgb, var(--background-primary-alt) 88%, var(--background-primary) 12%); + justify-content: center; + padding: 14px 16px; + background: color-mix(in srgb, var(--background-primary-alt) 86%, var(--background-primary) 14%); border: 1px solid color-mix(in srgb, var(--pf-collection-accent) 22%, var(--pf-border)); border-radius: 8px; min-width: 0; @@ -2182,7 +2183,7 @@ } .paperforge-workflow-stage-label { - font-size: 12px; + font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.3px; @@ -2195,6 +2196,7 @@ color: var(--text-faint); font-size: 11px; flex-shrink: 0; + align-self: center; } .paperforge-collection-ocr-header { From a5dd71bb13a71417552801bdfcce81fd146c7c1b Mon Sep 17 00:00:00 2001 From: Research Assistant <research@example.com> Date: Sun, 10 May 2026 22:31:16 +0800 Subject: [PATCH 35/37] fix(sync): freeze workspace slug + frontmatter-only update to prevent deep-reading data loss MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Slug freeze: reuse existing workspace dir when title slug changes in Zotero - Frontmatter-only: for existing notes, replace only YAML block, never touch body - extract_preserved_deep_reading: match both ##精读 and ##🔍精读 + skip placeholder-only sections --- paperforge/adapters/obsidian_frontmatter.py | 20 +++++++++++-- paperforge/worker/asset_index.py | 33 +++++++++++++++++---- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/paperforge/adapters/obsidian_frontmatter.py b/paperforge/adapters/obsidian_frontmatter.py index 5f84bc0d..69723df1 100644 --- a/paperforge/adapters/obsidian_frontmatter.py +++ b/paperforge/adapters/obsidian_frontmatter.py @@ -226,19 +226,35 @@ def generate_review(candidates: list[dict]) -> str: def extract_preserved_deep_reading(text: str) -> str: if not text: return "" - match = re.search("^## 🔍 精读\\s*$", text, re.MULTILINE) + # Match "## 🔍 精读" or "## 精读" (emoji is optional) + match = re.search(r"^##\s*🔍?\s*精读\s*$", text, re.MULTILINE) if not match: return "" start = match.start() preserved = text[start:].strip() + # Skip if section body is empty or only contains placeholder text + body = re.sub(r"^##\s*🔍?\s*精读\s*$", "", preserved, flags=re.MULTILINE).strip() + if not body or _is_placeholder_only(body): + return "" return preserved +_PLACEHOLDER_PATTERN = re.compile(r"(待补充)|\(待补充\)|\(TODO\)|\(TBD\)") + + +def _is_placeholder_only(body: str) -> bool: + """Check if the deep reading body is only placeholder text (no real content).""" + cleaned = _PLACEHOLDER_PATTERN.sub("", body).strip() + cleaned = re.sub(r"^[-*]\s*$", "", cleaned, flags=re.MULTILINE).strip() + return not cleaned + + def has_deep_reading_content(text: str) -> bool: preserved = extract_preserved_deep_reading(text) if not preserved: return False - body = preserved.replace(DEEP_READING_HEADER, "").strip() + # Strip both "## 🔍 精读" and "## 精读" header variants + body = re.sub(r"^##\s*🔍?\s*精读\s*$", "", preserved, flags=re.MULTILINE).strip() if not body: return False diff --git a/paperforge/worker/asset_index.py b/paperforge/worker/asset_index.py index a36406ff..7fc28df6 100644 --- a/paperforge/worker/asset_index.py +++ b/paperforge/worker/asset_index.py @@ -262,13 +262,22 @@ def _build_entry(item: dict, vault: Path, paths: dict, domain: str, zotero_dir: write_json(meta_path, meta) title_slug = slugify_filename(item["title"]) note_path = paths["literature"] / domain / f"{key} - {title_slug}.md" + + # --- Freeze slug: reuse existing workspace if it exists under a different slug --- + workspace_dir = paths["literature"] / domain / f"{key} - {title_slug}" + if not workspace_dir.exists(): + for candidate in (paths["literature"] / domain).glob(f"{key} - *"): + if candidate.is_dir(): + workspace_dir = candidate + title_slug = workspace_dir.name.split(" - ", 1)[1] if " - " in workspace_dir.name else title_slug + break + if note_path.parent.exists(): for stale_note in note_path.parent.glob(f"{key} - *.md"): if stale_note != note_path: stale_note.unlink() # Workspace paths (Phase 26: flat-to-workspace migration) - workspace_dir = paths["literature"] / domain / f"{key} - {title_slug}" main_note_path = workspace_dir / f"{key} - {title_slug}.md" deep_reading_file = workspace_dir / "deep-reading.md" target_fulltext = workspace_dir / "fulltext.md" @@ -363,10 +372,24 @@ def _build_entry(item: dict, vault: Path, paths: dict, domain: str, zotero_dir: entry["maturity"] = compute_maturity(entry) entry["next_step"] = compute_next_step(entry) - # --- Workspace: always create and write to workspace path (Phase 38: no flat fallback) --- - existing_text = main_note_path.read_text(encoding="utf-8") if main_note_path.exists() else "" - - main_note_path.write_text(frontmatter_note(entry, existing_text), encoding="utf-8") + # Slug already frozen above — for existing notes, update frontmatter only (preserve body) + if main_note_path.exists(): + text = main_note_path.read_text(encoding="utf-8") + fm_close = text.find("---\n", 4) # closing --- after opening --- + if fm_close != -1: + body = text[fm_close + 4:] # everything after frontmatter + new_full = frontmatter_note(entry, "") + new_fm_close = new_full.find("---\n", 4) + if new_fm_close != -1: + new_fm = new_full[: new_fm_close + 4] # new frontmatter block with closing ---\n + main_note_path.write_text(new_fm + body, encoding="utf-8") + else: + main_note_path.write_text(new_full, encoding="utf-8") + else: + main_note_path.write_text(frontmatter_note(entry, text), encoding="utf-8") + else: + existing_text = note_path.read_text(encoding="utf-8") if note_path.exists() else "" + main_note_path.write_text(frontmatter_note(entry, existing_text), encoding="utf-8") # Write per-workspace paper-meta.json (Phase 37: internal state outside frontmatter) write_paper_meta(workspace_dir, entry, paperforge_version=PAPERFORGE_VERSION) From d50c2ff4c05fa00844e3b332ecd9ba32bb3ed4b4 Mon Sep 17 00:00:00 2001 From: Research Assistant <research@example.com> Date: Sun, 10 May 2026 23:05:22 +0800 Subject: [PATCH 36/37] =?UTF-8?q?fix(plugin):=20remove=20all=20control=5Fd?= =?UTF-8?q?ir=20references=20=E2=80=94=20CLI=20argument=20was=20removed=20?= =?UTF-8?q?in=20v1.4.17?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove --control-dir from setup args (CLI no longer accepts it) - Remove control_dir from DEFAULTS, config parser, valid path keys - Remove control_dir from settings assignment and JSDoc - Remove dir_index display and validate_index check --- paperforge/plugin/main.js | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/paperforge/plugin/main.js b/paperforge/plugin/main.js index 67083316..2e89a168 100644 --- a/paperforge/plugin/main.js +++ b/paperforge/plugin/main.js @@ -2338,7 +2338,6 @@ class PaperForgeSettingTab extends PluginSettingTab { { label: t('dir_vault'), val: vaultPath }, { label: t('dir_resources'), val: `${vaultPath}/${pf.resources_dir}` }, { label: ' ' + t('dir_notes'), val: `${vaultPath}/${pf.resources_dir}/${pf.literature_dir}` }, - { label: ' ' + t('dir_index'), val: `${vaultPath}/${pf.resources_dir}/${pf.control_dir}` }, { label: t('dir_base'), val: `${vaultPath}/${pf.base_dir}` }, { label: t('dir_system'), val: `${vaultPath}/${pf.system_dir}` }, { label: 'API Key', val: s.paddleocr_api_key ? t('api_key_set') : t('api_key_missing') }, @@ -2769,7 +2768,6 @@ class PaperForgeSetupModal extends Modal { const previewItems = [ { label: t('dir_resources'), val: `${vault}/${s.resources_dir || ''}` }, { label: t('dir_notes'), val: `${vault}/${s.resources_dir || ''}/${s.literature_dir || ''}` }, - { label: t('dir_index'), val: `${vault}/${s.resources_dir || ''}/${s.control_dir || ''}` }, { label: t('dir_system'), val: `${vault}/${s.system_dir || ''}` }, { label: t('dir_base'), val: `${vault}/${s.base_dir || ''}` }, ]; @@ -3017,7 +3015,6 @@ class PaperForgeSetupModal extends Modal { '--system-dir', s.system_dir.trim(), '--resources-dir', s.resources_dir.trim(), '--literature-dir', s.literature_dir.trim(), - '--control-dir', s.control_dir.trim(), '--base-dir', s.base_dir.trim(), '--agent', s.agent_platform || 'opencode', ]; @@ -3102,7 +3099,6 @@ class PaperForgeSetupModal extends Modal { if (!s.vault_path || !s.vault_path.trim()) errors.push(t('validate_vault')); if (!s.resources_dir || !s.resources_dir.trim()) errors.push(t('validate_resources')); if (!s.literature_dir || !s.literature_dir.trim()) errors.push(t('validate_notes')); - if (!s.control_dir || !s.control_dir.trim()) errors.push(t('validate_index')); if (!s.base_dir || !s.base_dir.trim()) errors.push(t('validate_base')); if (!s.paddleocr_api_key || !s.paddleocr_api_key.trim()) errors.push(t('validate_key')); if (!s.zotero_data_dir || !s.zotero_data_dir.trim()) errors.push(t('validate_zotero')); @@ -3159,7 +3155,6 @@ class PaperForgeSetupModal extends Modal { { label: t('dir_vault'), val: vault }, { label: t('dir_resources'), val: `${vault}/${s.resources_dir}` }, { label: t('dir_notes'), val: `${vault}/${s.resources_dir}/${s.literature_dir}` }, - { label: t('dir_index'), val: `${vault}/${s.resources_dir}/${s.control_dir}` }, { label: t('dir_base'), val: `${vault}/${s.base_dir}` }, { label: t('dir_system'), val: `${vault}/${s.system_dir}` }, { label: 'API Key', val: s.paddleocr_api_key ? t('api_key_set') : t('api_key_missing') }, @@ -3285,7 +3280,7 @@ module.exports = class PaperForgePlugin extends Plugin { /** * Read path configuration from the canonical paperforge.json file. * Falls back to Python-level DEFAULT_CONFIG values if file does not exist. - * Returns {system_dir, resources_dir, literature_dir, control_dir, base_dir}. + * Returns {system_dir, resources_dir, literature_dir, base_dir}. */ readPaperforgeJson() { const fs = require('fs'); @@ -3298,7 +3293,6 @@ module.exports = class PaperForgePlugin extends Plugin { system_dir: 'System', resources_dir: 'Resources', literature_dir: 'Literature', - control_dir: 'LiteratureControl', base_dir: 'Bases', }; @@ -3315,7 +3309,6 @@ module.exports = class PaperForgePlugin extends Plugin { system_dir: vc.system_dir || data.system_dir || DEFAULTS.system_dir, resources_dir: vc.resources_dir || data.resources_dir || DEFAULTS.resources_dir, literature_dir: vc.literature_dir || data.literature_dir || DEFAULTS.literature_dir, - control_dir: vc.control_dir || data.control_dir || DEFAULTS.control_dir, base_dir: vc.base_dir || data.base_dir || DEFAULTS.base_dir, }; } catch (e) { @@ -3350,7 +3343,7 @@ module.exports = class PaperForgePlugin extends Plugin { } // Update vault_config with new path values - const validPathKeys = ['system_dir', 'resources_dir', 'literature_dir', 'control_dir', 'base_dir']; + const validPathKeys = ['system_dir', 'resources_dir', 'literature_dir', 'base_dir']; for (const key of validPathKeys) { if (pathConfig[key] !== undefined) { data.vault_config[key] = pathConfig[key]; @@ -3375,7 +3368,6 @@ module.exports = class PaperForgePlugin extends Plugin { this.settings.system_dir = pfConfig.system_dir; this.settings.resources_dir = pfConfig.resources_dir; this.settings.literature_dir = pfConfig.literature_dir; - this.settings.control_dir = pfConfig.control_dir; this.settings.base_dir = pfConfig.base_dir; } } catch (e) { @@ -3395,7 +3387,6 @@ module.exports = class PaperForgePlugin extends Plugin { this.settings.system_dir = pfConfig.system_dir; this.settings.resources_dir = pfConfig.resources_dir; this.settings.literature_dir = pfConfig.literature_dir; - this.settings.control_dir = pfConfig.control_dir; this.settings.base_dir = pfConfig.base_dir; // Re-validate saved python_path override From c0cc05ab329f680e04723654e804ff993bb56b79 Mon Sep 17 00:00:00 2001 From: Research Assistant <research@example.com> Date: Sun, 10 May 2026 23:22:42 +0800 Subject: [PATCH 37/37] =?UTF-8?q?refactor(skills):=20simplify=20skill=20de?= =?UTF-8?q?ployment=20=E2=80=94=20single=20copytree=20for=20all=20platform?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove format dispatch (flat_command/skill_directory/rules_file) — all platforms unified - skill_deploy.py: 257→81 lines, only AGENT_SKILL_DIRS + copytree + AGENTS.md - AgentInstaller switches to vault-local paths, removes deploy_commands step - Remove _deploy_skills dead code in update.py --- paperforge/services/skill_deploy.py | 259 +++++----------------------- paperforge/setup/agent.py | 68 ++------ paperforge/worker/update.py | 41 +---- 3 files changed, 57 insertions(+), 311 deletions(-) diff --git a/paperforge/services/skill_deploy.py b/paperforge/services/skill_deploy.py index f821baf3..66076b09 100644 --- a/paperforge/services/skill_deploy.py +++ b/paperforge/services/skill_deploy.py @@ -1,4 +1,4 @@ -"""Skill deployment service — single source of truth for agent skill installation and updates. +"""Skill deployment service — single copytree for all platforms. Used by both setup wizard (install) and update worker (update). All deployments are vault-local only. @@ -9,253 +9,82 @@ from __future__ import annotations import shutil from pathlib import Path -# ── Agent Platform Configurations ── -# Canonical source of truth. setup_wizard.py imports from here. - -AGENT_CONFIGS = { - "opencode": { - "name": "OpenCode", - "skill_dir": ".opencode/skills", - "command_dir": ".opencode/command", - "format": "flat_command", - "prefix": "/", - "config_file": None, - }, - "claude": { - "name": "Claude Code", - "skill_dir": ".claude/skills", - "format": "skill_directory", - "prefix": "/", - "config_file": ".claude/skills.json", - }, - "codex": { - "name": "Codex", - "skill_dir": ".codex/skills", - "format": "skill_directory", - "prefix": "$", - "config_file": None, - }, - "cursor": { - "name": "Cursor", - "skill_dir": ".cursor/skills", - "format": "skill_directory", - "prefix": "/", - "config_file": ".cursor/settings.json", - }, - "windsurf": { - "name": "Windsurf", - "skill_dir": ".windsurf/skills", - "format": "skill_directory", - "prefix": "/", - "config_file": None, - }, - "github_copilot": { - "name": "GitHub Copilot", - "skill_dir": ".github/skills", - "format": "skill_directory", - "prefix": "/", - "config_file": ".github/copilot-instructions.md", - }, - "cline": { - "name": "Cline", - "skill_dir": ".clinerules", - "format": "rules_file", - "prefix": "/", - "config_file": ".clinerules", - }, - "augment": { - "name": "Augment", - "skill_dir": ".augment/skills", - "format": "skill_directory", - "prefix": "/", - "config_file": None, - }, - "trae": { - "name": "Trae", - "skill_dir": ".trae/skills", - "format": "skill_directory", - "prefix": "/", - "config_file": None, - }, +# ── Agent platform → vault-local skill directory ── +AGENT_SKILL_DIRS: dict[str, str] = { + "opencode": ".opencode/skills", + "claude": ".claude/skills", + "codex": ".codex/skills", + "cursor": ".cursor/skills", + "windsurf": ".windsurf/skills", + "github_copilot": ".github/skills", + "cline": ".clinerules", + "augment": ".augment/skills", + "trae": ".trae/skills", } def _resolve_source_root() -> Path: """Resolve the paperforge package root (where skills/ lives).""" import paperforge + return Path(paperforge.__file__).parent -def _substitute_vars(text: str, system_dir: str, resources_dir: str, literature_dir: str, base_dir: str, skill_dir: str, prefix: str = "/") -> str: - for old, new in [ - ("<system_dir>", system_dir), - ("<resources_dir>", resources_dir), - ("<literature_dir>", literature_dir), - ("<base_dir>", base_dir), - ("<skill_dir>", skill_dir), - ("<prefix>", prefix), - ]: - text = text.replace(old, new) - return text - - -# ── Deploy helpers ── - -def _deploy_skill_directory(vault: Path, skill_dir: str, source_root: Path, system_dir: str, resources_dir: str, literature_dir: str, base_dir: str, prefix: str = "/", overwrite: bool = False) -> list[str]: - """Deploy pf-* skills as independent SKILL.md directories (Claude Code, Codex, Cursor, etc.).""" - imported = [] - src_scripts = source_root / "skills" / "literature-qa" / "scripts" - src_charts = source_root / "skills" / "literature-qa" / "chart-reading" - src_prompt = source_root / "skills" / "literature-qa" / "prompt_deep_subagent.md" - - for skill_file in sorted(src_scripts.glob("pf-*.md")): - skill_name = skill_file.stem - skill_dst = vault / skill_dir / skill_name - skill_dst.mkdir(parents=True, exist_ok=True) - text = skill_file.read_text(encoding="utf-8") - text = _substitute_vars(text, system_dir, resources_dir, literature_dir, base_dir, skill_dir, prefix) - dst_file = skill_dst / "SKILL.md" - if overwrite or not dst_file.exists(): - dst_file.write_text(text, encoding="utf-8") - imported.append(skill_name) - - # pf-deep extras: scripts, chart-reading, subagent prompt - pf_deep_dst = vault / skill_dir / "pf-deep" - pf_deep_dst.mkdir(parents=True, exist_ok=True) - ld_dst = pf_deep_dst / "scripts" / "ld_deep.py" - ld_src = src_scripts / "ld_deep.py" - if ld_src.exists() and (overwrite or not ld_dst.exists()): - ld_dst.parent.mkdir(parents=True, exist_ok=True) - shutil.copy2(ld_src, ld_dst) - prompt_dst = pf_deep_dst / "prompt_deep_subagent.md" - if src_prompt.exists() and (overwrite or not prompt_dst.exists()): - shutil.copy2(src_prompt, prompt_dst) - if src_charts.exists() and src_charts.is_dir(): - chart_dst = pf_deep_dst / "chart-reading" - if overwrite and chart_dst.exists(): - shutil.rmtree(chart_dst) - chart_dst.mkdir(parents=True, exist_ok=True) - for f in src_charts.glob("*.md"): - if overwrite or not (chart_dst / f.name).exists(): - shutil.copy2(f, chart_dst / f.name) - - return imported - - -def _deploy_flat_command(vault: Path, command_dir: str, source_root: Path, system_dir: str, resources_dir: str, literature_dir: str, base_dir: str, skill_dir: str, overwrite: bool = False) -> list[str]: - """Deploy skills in flat .md command format (OpenCode).""" - imported = [] - command_src = source_root / "skills" / "literature-qa" / "scripts" - command_dst = vault / command_dir - if not (command_src.exists() and command_src.is_dir()): - return imported - - command_dst.mkdir(parents=True, exist_ok=True) - for f in command_src.glob("pf-*.md"): - text = f.read_text(encoding="utf-8") - text = _substitute_vars(text, system_dir, resources_dir, literature_dir, base_dir, skill_dir) - dst_file = command_dst / f.name - if overwrite or not dst_file.exists(): - dst_file.write_text(text, encoding="utf-8") - imported.append(f.stem) - - return imported - - -def _deploy_rules_file(vault: Path, skill_dir: str, source_root: Path, system_dir: str, resources_dir: str, literature_dir: str, base_dir: str, overwrite: bool = False) -> list[str]: - """Deploy skills as .clinerules directory (Cline).""" - imported = [] - src_scripts = source_root / "skills" / "literature-qa" / "scripts" - src_charts = source_root / "skills" / "literature-qa" / "chart-reading" - src_prompt = source_root / "skills" / "literature-qa" / "prompt_deep_subagent.md" - - pf_deep_dst = vault / skill_dir / "pf-deep" - pf_deep_dst.mkdir(parents=True, exist_ok=True) - ld_src = src_scripts / "ld_deep.py" - ld_dst = pf_deep_dst / "scripts" / "ld_deep.py" - if ld_src.exists() and (overwrite or not ld_dst.exists()): - ld_dst.parent.mkdir(parents=True, exist_ok=True) - shutil.copy2(ld_src, ld_dst) - prompt_dst = pf_deep_dst / "prompt_deep_subagent.md" - if src_prompt.exists() and (overwrite or not prompt_dst.exists()): - shutil.copy2(src_prompt, prompt_dst) - if src_charts.exists() and src_charts.is_dir(): - chart_dst = pf_deep_dst / "chart-reading" - if overwrite and chart_dst.exists(): - shutil.rmtree(chart_dst) - chart_dst.mkdir(parents=True, exist_ok=True) - for f in src_charts.glob("*.md"): - if overwrite or not (chart_dst / f.name).exists(): - shutil.copy2(f, chart_dst / f.name) - - imported.append("clinerules") - return imported - - -# ── Main entry point ── - def deploy_skills( vault: Path, agent_key: str = "opencode", - system_dir: str = "System", - resources_dir: str = "Resources", - literature_dir: str = "Literature", - base_dir: str = "Bases", overwrite: bool = False, ) -> dict: - """Deploy skills, commands, and AGENTS.md for a given agent platform. + """Deploy literature-qa skill and AGENTS.md to the vault. Args: - vault: Obsidian vault root - agent_key: Agent platform key (opencode, cursor, claude, etc.) - overwrite: True for update (overwrite existing), False for install (skip) + vault: Obsidian vault root. + agent_key: Agent platform key (opencode, claude, etc.). + overwrite: If True, overwrite existing files (used by update). Returns: - dict with 'skills', 'commands', 'agents_md', 'errors' keys + dict with 'skill_deployed', 'agents_md', 'errors' keys. """ - agent_config = AGENT_CONFIGS.get(agent_key) - if not agent_config: - return {"skills": [], "commands": [], "agents_md": False, "errors": [f"Unknown agent: {agent_key}"]} - - source_root = _resolve_source_root() - if not (source_root / "skills" / "literature-qa").exists(): - return {"skills": [], "commands": [], "agents_md": False, "errors": ["Skills source not found in package"]} - - skill_dir = agent_config.get("skill_dir", ".opencode/skills") - fmt = agent_config.get("format", "skill_directory") - prefix = agent_config.get("prefix", "/") - imported_skills: list[str] = [] errors: list[str] = [] - # Deploy skills by format - try: - if fmt == "flat_command": - imported_skills = _deploy_flat_command(vault, agent_config["command_dir"], source_root, system_dir, resources_dir, literature_dir, base_dir, skill_dir, overwrite) - imported_skills += _deploy_skill_directory(vault, skill_dir, source_root, system_dir, resources_dir, literature_dir, base_dir, prefix, overwrite) - elif fmt == "rules_file": - imported_skills = _deploy_rules_file(vault, agent_config["skill_dir"], source_root, system_dir, resources_dir, literature_dir, base_dir, overwrite) - else: - imported_skills = _deploy_skill_directory(vault, skill_dir, source_root, system_dir, resources_dir, literature_dir, base_dir, prefix, overwrite) - except Exception as e: - errors.append(f"Skill deploy failed: {e}") + # ── Deploy literature-qa skill ── + skill_deployed = False + source_root = _resolve_source_root() + src_skill = source_root / "skills" / "literature-qa" - # Deploy AGENTS.md + if src_skill.exists(): + skill_dir_name = AGENT_SKILL_DIRS.get(agent_key) + if skill_dir_name: + dst_skill = vault / skill_dir_name / "literature-qa" + try: + if overwrite and dst_skill.exists(): + shutil.rmtree(dst_skill, ignore_errors=True) + dst_skill.parent.mkdir(parents=True, exist_ok=True) + shutil.copytree(src_skill, dst_skill, dirs_exist_ok=True) + skill_deployed = True + except Exception as e: + errors.append(f"Skill deploy failed: {e}") + else: + errors.append(f"Unknown agent: {agent_key}") + else: + errors.append("Skills source not found in package") + + # ── Deploy AGENTS.md ── agents_ok = False agents_src = source_root.parent / "AGENTS.md" if agents_src.exists(): try: agents_dst = vault / "AGENTS.md" - text = agents_src.read_text(encoding="utf-8") - text = _substitute_vars(text, system_dir, resources_dir, literature_dir, base_dir, skill_dir, prefix) + if overwrite and agents_dst.exists(): + agents_dst.unlink() if overwrite or not agents_dst.exists(): - agents_dst.write_text(text, encoding="utf-8") + shutil.copy2(agents_src, agents_dst) agents_ok = True except Exception as e: errors.append(f"AGENTS.md deploy failed: {e}") return { - "skills": imported_skills, - "commands": imported_skills, + "skill_deployed": skill_deployed, "agents_md": agents_ok, "errors": errors, } diff --git a/paperforge/setup/agent.py b/paperforge/setup/agent.py index 156e7d5f..f6b4bd57 100644 --- a/paperforge/setup/agent.py +++ b/paperforge/setup/agent.py @@ -1,15 +1,16 @@ -"""AgentInstaller — deploys skill files and agent configs.""" +"""AgentInstaller — deploys literature-qa skill to vault-local agent config.""" from __future__ import annotations import shutil from pathlib import Path +from paperforge.services.skill_deploy import AGENT_SKILL_DIRS from paperforge.setup import SetupStepResult class AgentInstaller: - """Deploy agent skill files, command files, and rules.""" + """Deploy literature-qa skill directory to vault-local agent skills path.""" def __init__(self, vault: Path, agent_type: str = "opencode"): self.vault = vault @@ -17,19 +18,12 @@ class AgentInstaller: self._script_dir = Path(__file__).resolve().parent.parent def _get_skills_dir(self) -> Path: - """Get the target skills directory based on agent type.""" - if self.agent_type == "opencode": - base = Path.home() / ".config" / "opencode" - elif self.agent_type == "claude": - base = Path.home() / ".claude" - elif self.agent_type == "codex": - base = Path.home() / ".codex" - else: - base = self.vault / ".agents" - return base / "skills" + """Get the vault-local target skills directory.""" + skill_dir_name = AGENT_SKILL_DIRS.get(self.agent_type, ".agents/skills") + return self.vault / skill_dir_name def deploy_skills(self) -> SetupStepResult: - """Deploy literature-qa skill directory to agent config.""" + """Deploy literature-qa skill as a single directory.""" source_skills = self._script_dir / "skills" / "literature-qa" if not source_skills.exists(): return SetupStepResult( @@ -43,11 +37,7 @@ class AgentInstaller: target_dir.mkdir(parents=True, exist_ok=True) try: - if source_skills.is_dir(): - shutil.copytree(source_skills, target_dir, dirs_exist_ok=True) - else: - shutil.copy2(source_skills, target_dir) - + shutil.copytree(source_skills, target_dir, dirs_exist_ok=True) return SetupStepResult( step="agent_installer", ok=True, @@ -62,42 +52,8 @@ class AgentInstaller: error=str(e), ) - def deploy_commands(self) -> SetupStepResult: - """Deploy command files to vault agent config dir.""" - source_commands = self._script_dir / "command_files" - if not source_commands.exists(): - return SetupStepResult( - step="agent_installer", - ok=True, - message="No command files to deploy", - details={"skipped": True}, - ) - - agent_dir = self.vault / ".agents" - target_dir = agent_dir / "command_files" - target_dir.mkdir(parents=True, exist_ok=True) - - try: - for f in source_commands.iterdir(): - if f.is_file(): - shutil.copy2(f, target_dir / f.name) - - return SetupStepResult( - step="agent_installer", - ok=True, - message=f"Deployed command files to {target_dir}", - details={"source": str(source_commands), "target": str(target_dir)}, - ) - except Exception as e: - return SetupStepResult( - step="agent_installer", - ok=False, - message="Failed to deploy command files", - error=str(e), - ) - def deploy_agent_config(self) -> SetupStepResult: - """Deploy AGENTS.md and other agent config files.""" + """Deploy AGENTS.md to vault root.""" source_agents = self._script_dir.parent / "AGENTS.md" if not source_agents.exists(): return SetupStepResult( @@ -126,8 +82,4 @@ class AgentInstaller: def run_all(self) -> list[SetupStepResult]: """Run all deployment steps.""" - results = [] - results.append(self.deploy_skills()) - results.append(self.deploy_commands()) - results.append(self.deploy_agent_config()) - return results + return [self.deploy_skills(), self.deploy_agent_config()] diff --git a/paperforge/worker/update.py b/paperforge/worker/update.py index 0f4bcfe9..a92742cf 100644 --- a/paperforge/worker/update.py +++ b/paperforge/worker/update.py @@ -34,31 +34,6 @@ def _sync_obsidian_plugin(vault: Path) -> None: _pf_utils.install_obsidian_plugin(vault) -def _deploy_skills(vault: Path) -> None: - """Copy the literature-qa skill from the package to the vault's agent dir.""" - import paperforge - - src = Path(paperforge.__file__).parent / "skills" / "literature-qa" - if not src.exists(): - logger.debug("Skills source not found: %s", src) - return - - raw = read_paperforge_json(vault) - agent = raw.get("agent_platform", "opencode") - agent_dirs = { - "opencode": ".opencode/skills/literature-qa", - "claude": ".claude/skills/literature-qa", - "cursor": ".cursor/skills/literature-qa", - "copilot": ".github/skills/literature-qa", - "windsurf": ".windsurf/skills/literature-qa", - "codex": ".codex/skills/literature-qa", - } - target_rel = agent_dirs.get(agent, agent_dirs["opencode"]) - dst = vault / target_rel - shutil.copytree(src, dst, dirs_exist_ok=True) - logger.info("Skills deployed: %s -> %s", src, dst) - - def protected_paths(vault: Path) -> set[str]: cfg = load_vault_config(vault) pf = f"{cfg['system_dir']}/PaperForge" @@ -269,20 +244,10 @@ def _deploy_all_skills(vault: Path) -> None: from paperforge.config import load_vault_config config = load_vault_config(vault) - # Agent platform is a user preference, not a system default. - # Fall back to opencode if not configured. agent_key = config.get("agent_platform") or "opencode" - result = deploy_skills( - vault=vault, - agent_key=agent_key, - system_dir=config.get("system_dir", "System"), - resources_dir=config.get("resources_dir", "Resources"), - literature_dir=config.get("literature_dir", "Literature"), - base_dir=config.get("base_dir", "Bases"), - overwrite=True, - ) - if result["skills"]: - logger.info("已部署 %d 个 skill: %s", len(result["skills"]), ", ".join(result["skills"])) + result = deploy_skills(vault=vault, agent_key=agent_key, overwrite=True) + if result["skill_deployed"]: + logger.info("已部署 literature-qa skill") if result["agents_md"]: logger.info("已更新 AGENTS.md") for err in result.get("errors", []):