mirror of
https://github.com/lllin000/PaperForge.git
synced 2026-07-22 06:50:53 +00:00
docs: close #71/#72 prototypes, set #73 as active frontier
Record the completed six-module control-center and maintenance-inbox prototype wave. Both prototype pairs passed independent Critical/Important PASS review and direct browser verification at 768px without production code changes. Update executive summary, active queue, decision log, session timeline, and milestone checkpoint. - #71 resolved: six-module control-center HTML prototype (5 scenarios x 6 modules, plain-button switcher, responsive 768px) - #72 resolved: actionable-only maintenance inbox prototype (single-action rows, inline issue-draft, confirmation-first report) - #73 promoted to active frontier: integrate accepted model into production control-center plugin
This commit is contained in:
parent
c68b671e0c
commit
b97e82ab7c
6 changed files with 2334 additions and 14 deletions
|
|
@ -1,11 +1,13 @@
|
|||
> **Branch:** `feat/ocr-rebuild-ux` | **Last Updated:** 2026-07-14
|
||||
> **Active work:** [Wayfinder: Make the PaperForge control center self-explanatory](https://github.com/LLLin000/PaperForge/issues/65). The capability vocabulary (#69) and managed-runtime architecture (#70) are resolved; the six-module control-center prototype (#71) and actionable maintenance design (#72) are the live frontier.
|
||||
> **Branch:** `feat/ocr-rebuild-ux` | **Last Updated:** 2026-07-15
|
||||
> **Active work:** [Wayfinder: Make the PaperForge control center self-explanatory](https://github.com/LLLin000/PaperForge/issues/65). The capability vocabulary (#69), managed-runtime architecture (#70), six-module control-center prototype (#71), and actionable maintenance design (#72) are resolved; integrating the accepted model (#73) is the frontier.
|
||||
>
|
||||
> ---
|
||||
>
|
||||
> **Current state:** Retrieval recovery is merged and live. OCR multi-key rebuild/redo now streams progress, supports cooperative stop between papers, and refreshes canonical maintenance state. The control-plane audit found four critical contract gaps; the orthogonal capability/activity/attention model and managed-runtime immutable-slot architecture are chosen across #69 and #70. OCR maintenance now uses a canonical per-row action model with confirmation gates.
|
||||
>
|
||||
> Next: prototype the six-module control center [#71](https://github.com/LLLin000/PaperForge/issues/71), design actionable-only maintenance with user-reviewed issue drafts [#72](https://github.com/LLLin000/PaperForge/issues/72), then integrate the accepted model in [#73](https://github.com/LLLin000/PaperForge/issues/73). Do not start production implementation before #73 closes.
|
||||
> #71 and #72 prototypes are complete: the six-module control-center HTML prototype covers 5 scenarios × 6 modules with independent capability probes, a primary attention zone, responsive single-column layout at 768px, and a plain-button scenario switcher. The maintenance inbox prototype demonstrates actionable-only filtering, single-action rows, inline issue-draft review with local redacted export, and a confirmation-first report flow. Both prototypes were independently reviewed (Critical/Important PASS) and browser-verified at 768px; design decisions documented in `docs/prototypes/`. No production plugin code was changed.
|
||||
>
|
||||
> Next: integrate the accepted model into a production control-center plugin [#73](https://github.com/LLLin000/PaperForge/issues/73).
|
||||
## 1. Architecture
|
||||
|
||||
### 1.1 The problem (pre-v2)
|
||||
|
|
@ -151,12 +153,12 @@ The OCR maintenance slice has a canonical All/Recommended state model, selected
|
|||
Remaining legacy OCR issues (carried forward):
|
||||
## 4. Active Queue
|
||||
|
||||
1. 🔴 **[Control-center Wayfinder](https://github.com/LLLin000/PaperForge/issues/65)** — map charted, audit and research gates resolved, capability model and runtime architecture chosen. Prototypes pending.
|
||||
1. 🔴 **[Control-center Wayfinder](https://github.com/LLLin000/PaperForge/issues/65)** — map charted, audit and research gates resolved, capability model and runtime architecture chosen. Prototypes completed (#71/#72); integration (#73) is the active frontier.
|
||||
2. ✅ **[Capability-state vocabulary](https://github.com/LLLin000/PaperForge/issues/69)** — resolved at `issuecomment-4971161072`. Orthogonal availability/activity/attention axes, 6-state capability ordinal, 12 canonical verbs, backend-owned severity and primary actions, maintenance projection.
|
||||
3. ✅ **[Managed runtime](https://github.com/LLLin000/PaperForge/issues/70)** — resolved at `issuecomment-4971239398`. Plugin-managed immutable runtime slots, system-Python bootstrap with validated-triplet fallback, single `active-runtime.json` pointer, `ManagedRuntime` class with `current()`/`status()`/`ensure()`, fail-closed command resolution.
|
||||
4. 🔴 **[Control-center prototype](https://github.com/LLLin000/PaperForge/issues/71)** — design six-module information hierarchy and interaction model. No production implementation before #73.
|
||||
5. 🔴 **[Maintenance prototype](https://github.com/LLLin000/PaperForge/issues/72)** — design actionable-only stale/failed/corrupt inbox with user-reviewed GitHub Issue draft reporting.
|
||||
6. ⏳ **Control-center integration** — [#73](https://github.com/LLLin000/PaperForge/issues/73) remains blocked on #71/#72.
|
||||
4. ✅ **[Control-center prototype](https://github.com/LLLin000/PaperForge/issues/71)** — resolved with independent Critical/Important PASS review and browser verification at 768px. Six-module control-center HTML prototype covers 5 scenarios with plain-button switcher, primary attention zone, responsive layout, and capability-gated actions. Design decisions recorded in `docs/prototypes/2026-07-14-six-module-control-center.html/.md`. No production implementation before #73.
|
||||
5. ✅ **[Maintenance prototype](https://github.com/LLLin000/PaperForge/issues/72)** — resolved with independent Critical/Important PASS review and browser verification at 768px. Actionable-only inbox with single-action rows, inline issue-draft review, local redacted export, and confirmation-first report flow. Design decisions recorded in `docs/prototypes/2026-07-14-maintenance-issue-reporting.html/.md`.
|
||||
6. 🔴 **Control-center integration** — [#73](https://github.com/LLLin000/PaperForge/issues/73) — integrate the accepted prototype model into a production control-center plugin. Build one or more Obsidian plugin settings tabs using the capability-state-action model, managed-runtime contracts, and actionable-only maintenance pattern from #71/#72.
|
||||
7. 🟡 **Downstream tooling** — section-aware chunking and separate figure/table handling.
|
||||
8. ⏳ **Compatibility naming cleanup** — deferred post-release.
|
||||
### 4.1 Immediate Next Steps
|
||||
|
|
@ -172,8 +174,9 @@ Remaining legacy OCR issues (carried forward):
|
|||
- [x] Study desktop installation/health/recovery patterns ([#68](https://github.com/LLLin000/PaperForge/issues/68))
|
||||
- [x] Define capability-state vocabulary and managed-runtime architecture ([#69](https://github.com/LLLin000/PaperForge/issues/69), [#70](https://github.com/LLLin000/PaperForge/issues/70))
|
||||
- [x] Add canonical per-row action routing and redo confirmation gates
|
||||
- [ ] Prototype six-module control center ([#71](https://github.com/LLLin000/PaperForge/issues/71))
|
||||
- [ ] Design actionable-only maintenance inbox ([#72](https://github.com/LLLin000/PaperForge/issues/72))
|
||||
- [x] Prototype six-module control center ([#71](https://github.com/LLLin000/PaperForge/issues/71))
|
||||
- [x] Design actionable-only maintenance inbox ([#72](https://github.com/LLLin000/PaperForge/issues/72))
|
||||
- [ ] Integrate accepted prototype model ([#73](https://github.com/LLLin000/PaperForge/issues/73))
|
||||
---
|
||||
|
||||
## 5. Key File Map
|
||||
|
|
@ -322,6 +325,11 @@ Remaining legacy OCR issues (carried forward):
|
|||
| 2026-07-14 | OCR maintenance actions route through a canonical verb model | `maintenanceActionForRow()` maps backend `display_action` → `"rebuild"` / `"redo"`. Batch command filtering now uses this verb rather than twin booleans. `maintenanceActionRequiresConfirmation()` gates destructive `redo` behind a browser `confirm()` with localized text. |
|
||||
| 2026-07-14 | Keep normal readiness in the control center, not permanent status-bar chrome | Obsidian status bars work for active operations and exceptional attention; duplicating six-module health there creates noise and conflicts with the actionable-only maintenance model. |
|
||||
| 2026-07-14 | Diagnostics stay local until the user reviews an issue draft | Docker-style opaque uploads require a support service and hide bundle contents; PaperForge instead exports redacted data locally and opens a prefilled GitHub draft without token storage or automatic submission. |
|
||||
| 2026-07-15 | Six-module control center: plain-button scenario switcher, not ARIA tabpanel | Scenario switcher uses `<button>` elements with `aria-current="true"`. No `role="tab"`/`"tablist"`/`"tabpanel"` — clicking a scenario changes the entire page state, inconsistent with tabpanel pattern. |
|
||||
| 2026-07-15 | Primary attention zone exposes one concrete action, never a generic link | Hero action is the single highest-priority verb (e.g. `rebuild_derived` for OCR), never a "View Details" link. Normal state says "一切正常" with a less prominent refresh action. |
|
||||
| 2026-07-15 | Maintenance inbox is actionable-only; normal and quality-ok items are absent | Routine quality scores create permanent, non-actionable noise. Maintenance shows only stale/failed/corrupt work with a concrete action; unacceptable OCR is reported through a user-reviewed GitHub Issue draft. |
|
||||
| 2026-07-15 | Issue-draft flow is confirmation-first, inline redacted, never auto-submitted | User reviews a prefilled GitHub draft panel inline, edits freely, then clicks "Open GitHub" to create. No token storage, no automatic submission. Docker-style opaque upload rejected. |
|
||||
| 2026-07-15 | Prototype reviews use independent Critical/Important pass/fail gates | Both #71 and #72 prototypes were reviewed by independent reviewer subagents on two dimensions: Critical items (correctness/safety) and Important items (readability/maintainability). All items passed before acceptance. |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -422,6 +430,7 @@ python -m ruff check paperforge/worker/ocr_*.py
|
|||
| 2026-07-14 | Capability model, runtime architecture, and maintenance regression fixes | Closed #69 (orthogonal capability/activity/attention model) and #70 (plugin-managed immutable runtime slots). Added canonical per-row maintenance action routing, redo confirmation gate, and cache-manifest preservation across 5 plugin files. Verification: 19/19 maintenance tests pass. | [Capability contract](docs/research/2026-07-14-capability-state-action-contract.md), [Runtime architecture](docs/research/2026-07-14-managed-runtime-architecture.md) |
|
||||
| 2026-07-14 | Control-center current-contract audit | Inventoried setup, configuration, runtime, readiness, persistence, cache, failure, and recovery contracts across plugin and Python. Closed #66 with a ranked contradiction matrix and preserve/migrate/remove plan; unblocked #69 while #70 remains blocked on desktop/runtime evidence. | [Contract audit](https://github.com/LLLin000/PaperForge/issues/66#issuecomment-4968837257) |
|
||||
| 2026-07-14 | Obsidian and desktop recovery research | Closed #67 and #68 with primary-source pattern reports. Preserved the six-module IA, independent capability probes, Obsidian-managed plugin updates, module-scoped recovery, local diagnostics, and user-reviewed issue drafts; unblocked #70. | [#67](https://github.com/LLLin000/PaperForge/issues/67#issuecomment-4970653461), [#68](https://github.com/LLLin000/PaperForge/issues/68#issuecomment-4970660288) |
|
||||
| 2026-07-15 | Six-module control-center + maintenance inbox prototypes completed | Closed #71 (six-module HTML prototype, 5 scenarios, plain-button switcher, responsive 768px) with independent Critical/Important PASS review and browser verification. Closed #72 (actionable-only inbox, inline issue-draft review, confirmation-first report) with same review gate. Both prototype pairs passed all review dimensions. No production code changed. | `docs/prototypes/2026-07-14-six-module-control-center.{html,md}`, `docs/prototypes/2026-07-14-maintenance-issue-reporting.{html,md}` |
|
||||
|
||||
## 9. Historical Detail Archive
|
||||
|
||||
|
|
|
|||
831
docs/prototypes/2026-07-14-maintenance-issue-reporting.html
Normal file
831
docs/prototypes/2026-07-14-maintenance-issue-reporting.html
Normal file
|
|
@ -0,0 +1,831 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>PaperForge — Maintenance Inbox 原型</title>
|
||||
<style>
|
||||
[hidden]{display:none!important}
|
||||
/* ── Reset & base ── */
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
html{font-size:16px;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}
|
||||
body{
|
||||
font-family:system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
|
||||
color:#171717;background:#fff;line-height:1.5;
|
||||
min-height:100vh;display:flex;flex-direction:column;
|
||||
}
|
||||
|
||||
/* ── Tokens ── */
|
||||
:root{
|
||||
--ink:#171717;--body:#4d4d4d;--mute:#888;--hairline:#ebebeb;--hairline-strong:#a1a1a1;
|
||||
--canvas:#fff;--soft:#fafafa;--soft-2:#f5f5f5;
|
||||
--link:#0070f3;--link-deep:#0761d1;--link-bg:#d3e5ff;
|
||||
--success:#0070f3;--error:#ee0000;--error-soft:#f7d4d6;--error-deep:#c50000;
|
||||
--warning:#f5a623;--warning-soft:#ffefcf;--warning-deep:#ab570a;
|
||||
--radius-sm:4px;--radius-md:8px;--radius-lg:12px;
|
||||
--space-xxs:4px;--space-xs:8px;--space-sm:12px;--space-md:16px;--space-lg:24px;--space-xl:32px;--space-2xl:40px;
|
||||
--font-body:system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
|
||||
--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,monospace;
|
||||
--shadow-sm:0 1px 2px rgba(0,0,0,.04);
|
||||
--shadow-md:0 4px 12px rgba(0,0,0,.08);
|
||||
}
|
||||
|
||||
/* ── Typography ── */
|
||||
h1{font-size:24px;font-weight:600;letter-spacing:-.96px;line-height:32px}
|
||||
h2{font-size:18px;font-weight:500;letter-spacing:-.28px;line-height:24px;color:var(--body)}
|
||||
h3{font-size:14px;font-weight:500;letter-spacing:-.28px;line-height:20px}
|
||||
label{font-size:13px;font-weight:500;color:var(--body);display:block;margin-bottom:var(--space-xxs)}
|
||||
p,body,button,input,select,textarea{font-family:var(--font-body)}
|
||||
|
||||
/* ── Layout ── */
|
||||
.page-wrap{max-width:840px;margin:0 auto;padding:var(--space-xl) var(--space-lg);width:100%}
|
||||
|
||||
/* ── Header ── */
|
||||
.header{display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--space-xl);flex-wrap:wrap;gap:var(--space-sm)}
|
||||
.header-left{display:flex;align-items:center;gap:var(--space-sm)}
|
||||
.header-badge{
|
||||
display:inline-flex;align-items:center;justify-content:center;
|
||||
background:var(--ink);color:#fff;font-size:12px;font-weight:500;
|
||||
min-width:24px;height:24px;border-radius:100px;padding:0 var(--space-xs);
|
||||
letter-spacing:-.28px;
|
||||
}
|
||||
|
||||
/* ── Scenario bar ── */
|
||||
.scenario-bar{
|
||||
display:flex;flex-wrap:wrap;gap:var(--space-xs);
|
||||
padding:var(--space-sm) var(--space-md);
|
||||
background:var(--soft);border-radius:var(--radius-md);
|
||||
margin-bottom:var(--space-lg);
|
||||
align-items:center;
|
||||
}
|
||||
.scenario-bar-label{font-size:12px;font-weight:500;color:var(--mute);letter-spacing:-.28px;margin-right:var(--space-xxs);white-space:nowrap}
|
||||
.scenario-btn{
|
||||
font-size:12px;font-weight:500;letter-spacing:-.28px;
|
||||
padding:var(--space-xxs) var(--space-sm);
|
||||
border:1px solid var(--hairline);border-radius:100px;
|
||||
background:var(--canvas);color:var(--body);cursor:pointer;
|
||||
transition:background .15s,border-color .15s,color .15s;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.scenario-btn:hover{background:var(--soft-2);border-color:var(--hairline-strong)}
|
||||
.scenario-btn:focus-visible{outline:2px solid var(--link);outline-offset:2px;border-color:var(--link)}
|
||||
.scenario-btn.reset-btn{background:var(--ink);color:#fff;border-color:var(--ink)}
|
||||
.scenario-btn.reset-btn:hover{background:var(--body)}
|
||||
.scenario-btn.s-resolve{color:var(--success);border-color:var(--link-bg)}
|
||||
|
||||
/* ── Inbox ── */
|
||||
.inbox{display:flex;flex-direction:column;gap:var(--space-xxs)}
|
||||
|
||||
/* ── Item card ── */
|
||||
.item{
|
||||
display:grid;grid-template-columns:1fr auto auto;
|
||||
gap:var(--space-sm);align-items:center;
|
||||
padding:var(--space-md);
|
||||
background:var(--canvas);border:1px solid var(--hairline);border-radius:var(--radius-md);
|
||||
transition:opacity .3s,transform .3s,max-height .4s,padding .3s,margin .3s,border-color .3s;
|
||||
overflow:hidden;
|
||||
}
|
||||
.item.running{
|
||||
background:var(--soft);
|
||||
}
|
||||
.item.removing{
|
||||
opacity:0;transform:translateX(-16px);max-height:0;padding:0 var(--space-md);margin:0;border-color:transparent;
|
||||
pointer-events:none;
|
||||
}
|
||||
.item.info-area{grid-column:1/-1;border:0;padding:var(--space-sm) 0 0;background:transparent;overflow:visible}
|
||||
.item.with-border-red{border-color:var(--error-soft)}
|
||||
.item.with-border-yellow{border-color:var(--warning-soft)}
|
||||
|
||||
.item-info{min-width:0}
|
||||
.item-title{font-size:14px;font-weight:500;margin-bottom:2px}
|
||||
.item-meta{font-size:12px;color:var(--mute)}
|
||||
.item-reason{font-size:12px;color:var(--body);margin-top:4px}
|
||||
.item-actions{display:flex;gap:var(--space-xxs)}
|
||||
.action-btn{
|
||||
font-size:12px;font-weight:500;letter-spacing:-.28px;
|
||||
padding:var(--space-xxs) var(--space-sm);
|
||||
border:1px solid var(--hairline);border-radius:100px;
|
||||
background:var(--canvas);color:var(--ink);cursor:pointer;
|
||||
white-space:nowrap;transition:background .15s,color .15s,border-color .15s;
|
||||
}
|
||||
.action-btn:hover{background:var(--soft-2)}
|
||||
.action-btn:focus-visible{outline:2px solid var(--link);outline-offset:2px}
|
||||
.action-btn.primary{background:var(--link);color:#fff;border-color:var(--link)}
|
||||
.action-btn.primary:hover{background:var(--link-deep);border-color:var(--link-deep)}
|
||||
.action-btn.destructive{color:var(--error);border-color:var(--error-soft)}
|
||||
.action-btn.destructive:hover{background:var(--error-soft)}
|
||||
.action-btn.destructive.primary{background:var(--error);color:#fff;border-color:var(--error)}
|
||||
.action-btn.destructive.primary:hover{background:var(--error-deep);border-color:var(--error-deep)}
|
||||
.action-btn:disabled{opacity:.4;cursor:not-allowed}
|
||||
|
||||
/* ── Running progress ── */
|
||||
.progress-row{display:flex;align-items:center;gap:var(--space-sm);margin-top:var(--space-xs)}
|
||||
.progress-bar{
|
||||
flex:1;height:4px;border-radius:100px;
|
||||
background:var(--soft-2);overflow:hidden;
|
||||
}
|
||||
.progress-fill{
|
||||
height:100%;border-radius:100px;
|
||||
background:var(--link);transition:width .5s;
|
||||
}
|
||||
.progress-label{font-size:11px;color:var(--mute);white-space:nowrap}
|
||||
|
||||
/* ── Badge per action type ── */
|
||||
.item-badge{
|
||||
display:inline-flex;align-items:center;gap:var(--space-xxs);
|
||||
font-size:11px;font-weight:500;letter-spacing:-.28px;
|
||||
padding:2px var(--space-xs);border-radius:100px;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.item-badge.retry{background:var(--error-soft);color:var(--error-deep)}
|
||||
.item-badge.rebuild{background:var(--warning-soft);color:var(--warning-deep)}
|
||||
.item-badge.corrupt{background:var(--error-soft);color:var(--error-deep)}
|
||||
.item-badge.running-indicator{background:var(--link-bg);color:var(--link-deep)}
|
||||
.item-badge.draft{background:var(--link-bg);color:var(--link-deep)}
|
||||
|
||||
/* ── Confirmation modal ── */
|
||||
.modal-overlay{
|
||||
display:none;position:fixed;inset:0;
|
||||
background:rgba(23,23,23,.4);
|
||||
z-index:100;align-items:center;justify-content:center;
|
||||
padding:var(--space-md);
|
||||
}
|
||||
.modal-overlay.open{display:flex}
|
||||
.modal{
|
||||
background:var(--canvas);border-radius:var(--radius-lg);
|
||||
padding:var(--space-xl);max-width:480px;width:100%;
|
||||
box-shadow:var(--shadow-md);
|
||||
}
|
||||
.modal h2{font-size:18px;font-weight:600;margin-bottom:var(--space-sm);color:var(--ink)}
|
||||
.modal p{font-size:14px;color:var(--body);margin-bottom:var(--space-md);line-height:1.6}
|
||||
.modal .destructive-scope{
|
||||
background:var(--error-soft);color:var(--error-deep);
|
||||
font-size:13px;font-weight:500;padding:var(--space-sm) var(--space-md);
|
||||
border-radius:var(--radius-sm);margin-bottom:var(--space-md);
|
||||
}
|
||||
.modal-actions{display:flex;gap:var(--space-sm);justify-content:flex-end}
|
||||
|
||||
/* ── Issue Draft panel ── */
|
||||
.draft-overlay{
|
||||
display:none;position:fixed;inset:0;
|
||||
background:rgba(23,23,23,.4);
|
||||
z-index:100;align-items:center;justify-content:center;
|
||||
padding:var(--space-md);
|
||||
}
|
||||
.draft-overlay.open{display:flex}
|
||||
.draft-panel{
|
||||
background:var(--canvas);border-radius:var(--radius-lg);
|
||||
padding:var(--space-xl);max-width:600px;width:100%;max-height:85vh;
|
||||
overflow-y:auto;box-shadow:var(--shadow-md);
|
||||
}
|
||||
.draft-panel h2{font-size:18px;font-weight:600;margin-bottom:var(--space-sm)}
|
||||
.draft-panel .draft-subtitle{font-size:13px;color:var(--mute);margin-bottom:var(--space-lg)}
|
||||
.draft-section{margin-bottom:var(--space-lg)}
|
||||
.draft-section h3{font-size:13px;font-weight:500;color:var(--body);margin-bottom:var(--space-xs);text-transform:uppercase;letter-spacing:.4px}
|
||||
.field-table{width:100%;font-size:13px;border-collapse:collapse}
|
||||
.field-table th{padding:var(--space-xxs) var(--space-xs);text-align:left;color:var(--mute);font-weight:500;border-bottom:1px solid var(--hairline);width:120px}
|
||||
.field-table td{padding:var(--space-xxs) var(--space-xs);border-bottom:1px solid var(--hairline)}
|
||||
.field-table td.redacted{color:var(--mute);font-family:var(--font-mono);font-size:11px}
|
||||
.field-table td.included{color:var(--body)}
|
||||
.draft-panel textarea{
|
||||
width:100%;min-height:140px;padding:var(--space-sm);
|
||||
font-size:13px;font-family:var(--font-mono);
|
||||
border:1px solid var(--hairline);border-radius:var(--radius-sm);
|
||||
resize:vertical;color:var(--ink);background:var(--soft);
|
||||
}
|
||||
.draft-panel textarea:focus{outline:none;border-color:var(--link);box-shadow:0 0 0 2px var(--link-bg)}
|
||||
.draft-panel input[type="text"]{
|
||||
width:100%;padding:var(--space-sm);
|
||||
font-size:14px;font-weight:500;font-family:var(--font-body);
|
||||
border:1px solid var(--hairline);border-radius:var(--radius-sm);
|
||||
color:var(--ink);background:var(--soft);
|
||||
}
|
||||
.draft-panel input[type="text"]:focus{outline:none;border-color:var(--link);box-shadow:0 0 0 2px var(--link-bg)}
|
||||
.draft-notice{
|
||||
background:var(--warning-soft);color:var(--warning-deep);
|
||||
font-size:12px;padding:var(--space-sm) var(--space-md);
|
||||
border-radius:var(--radius-sm);margin-bottom:var(--space-md);
|
||||
}
|
||||
|
||||
/* ── Empty state ── */
|
||||
.empty-state{
|
||||
text-align:center;padding:var(--space-2xl) var(--space-lg);
|
||||
}
|
||||
.empty-icon{
|
||||
display:inline-flex;align-items:center;justify-content:center;
|
||||
width:48px;height:48px;border-radius:100px;
|
||||
background:var(--soft-2);margin-bottom:var(--space-md);
|
||||
font-size:20px;
|
||||
}
|
||||
.empty-state h3{font-size:16px;font-weight:500;margin-bottom:var(--space-xxs);color:var(--ink)}
|
||||
.empty-state p{font-size:14px;color:var(--mute);max-width:360px;margin:0 auto}
|
||||
|
||||
/* ── Separator ── */
|
||||
.inbox-sep{font-size:11px;font-weight:500;color:var(--mute);padding:var(--space-sm) 0;letter-spacing:.4px;text-transform:uppercase}
|
||||
|
||||
/* ── Footer ── */
|
||||
.footer-notes{margin-top:var(--space-lg);font-size:11px;color:var(--mute);border-top:1px solid var(--hairline);padding-top:var(--space-sm)}
|
||||
|
||||
/* ── Responsive ── */
|
||||
@media(max-width:768px){
|
||||
.page-wrap{padding:var(--space-md)}
|
||||
.header{flex-direction:column;align-items:flex-start}
|
||||
.scenario-bar{flex-direction:column;align-items:stretch}
|
||||
.item{grid-template-columns:1fr;gap:var(--space-xs)}
|
||||
.item-actions{justify-content:flex-end;margin-top:var(--space-xxs)}
|
||||
.modal{padding:var(--space-md)}
|
||||
.draft-panel{padding:var(--space-md)}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="page-wrap">
|
||||
|
||||
<!-- ─── HEADER ─── -->
|
||||
<div class="header">
|
||||
<div class="header-left">
|
||||
<h1>维护</h1>
|
||||
<span class="header-badge" id="badge">0</span>
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--mute)">
|
||||
<span id="status-label">全部正常</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ─── SCENARIO BAR ─── -->
|
||||
<div class="scenario-bar">
|
||||
<span class="scenario-bar-label">场景</span>
|
||||
<button class="scenario-btn" data-scenario="stale-ocr">过期 OCR 结果</button>
|
||||
<button class="scenario-btn" data-scenario="failed-ocr">OCR 失败</button>
|
||||
<button class="scenario-btn" data-scenario="corrupt-index">索引损坏</button>
|
||||
<button class="scenario-btn" data-scenario="running" id="scn-running">正在重建</button>
|
||||
<button class="scenario-btn s-resolve" data-scenario="resolve">→ 解决一项</button>
|
||||
<button class="scenario-btn" data-scenario="unacceptable">质量不合格</button>
|
||||
<button class="scenario-btn" data-scenario="empty">全部正常</button>
|
||||
<button class="scenario-btn reset-btn" id="reset-all">重置</button>
|
||||
</div>
|
||||
|
||||
<!-- ─── INBOX ─── -->
|
||||
<div class="inbox" id="inbox">
|
||||
<!-- Items rendered by JS -->
|
||||
</div>
|
||||
|
||||
<!-- ─── FOOTER ─── -->
|
||||
<div class="footer-notes">
|
||||
PaperForge · 维护标签页原型 · 设计语言 Vercel 灰阶
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ─── CONFIRMATION MODAL ─── -->
|
||||
<div class="modal-overlay" id="confirm-overlay">
|
||||
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="confirm-title">
|
||||
<h2 id="confirm-title">确认操作</h2>
|
||||
<p id="confirm-body">此操作将执行变更。</p>
|
||||
<div class="destructive-scope" id="confirm-scope" hidden></div>
|
||||
<div class="modal-actions">
|
||||
<button class="action-btn" id="confirm-cancel">取消</button>
|
||||
<button class="action-btn destructive primary" id="confirm-ok">确认执行</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ─── ISSUE DRAFT OVERLAY ─── -->
|
||||
<div class="draft-overlay" id="draft-overlay">
|
||||
<div class="draft-panel" role="dialog" aria-modal="true" aria-labelledby="draft-title">
|
||||
<h2 id="draft-title">提交 Issue</h2>
|
||||
<p class="draft-subtitle">隐私安全模式 · 预览将包含和排除的字段</p>
|
||||
|
||||
<div class="draft-section">
|
||||
<h3>标题(可编辑)</h3>
|
||||
<input type="text" id="draft-title-input" value="[PaperForge] OCR quality unacceptable — 2BB8VM5W" aria-label="Issue 标题">
|
||||
</div>
|
||||
|
||||
<div class="draft-section">
|
||||
<h3>包含的字段</h3>
|
||||
<table class="field-table">
|
||||
<tr><th>模块</th><td class="included">ocr</td></tr>
|
||||
<tr><th>reason.code</th><td class="included">ocr.quality_unacceptable</td></tr>
|
||||
<tr><th>reason.text</th><td class="included"><span id="draft-reason-text">OCR quality assessment failed</span></td></tr>
|
||||
<tr><th>error_summary</th><td class="included"><span id="draft-error">character error rate above threshold</span></td></tr>
|
||||
<tr><th>key/ID</th><td class="included"><span id="draft-key">5T8JX2</span></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="draft-section">
|
||||
<h3>排除的字段(已脱敏)</h3>
|
||||
<table class="field-table">
|
||||
<tr><th>论文标题</th><td class="redacted">[redacted — contains identifiable content]</td></tr>
|
||||
<tr><th>vault path</th><td class="redacted">[redacted — local filesystem path]</td></tr>
|
||||
<tr><th>zotero path</th><td class="redacted">[redacted — local filesystem path]</td></tr>
|
||||
<tr><th>config values</th><td class="redacted">[redacted — may contain secrets]</td></tr>
|
||||
<tr><th>API key</th><td class="redacted">[redacted — credential]</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="draft-section">
|
||||
<h3>Issue 草稿(可编辑)</h3>
|
||||
<div class="draft-notice">⚠ 草稿仅本地展示,不会自动提交。编辑完成后,点击下方按钮再 Github 新建 Issue。</div>
|
||||
<textarea id="draft-body" aria-label="Issue 草稿内容">## Bug: OCR quality assessment failed
|
||||
|
||||
**Module:** ocr
|
||||
**reason.code:** ocr.quality_unacceptable
|
||||
**reason.text:** OCR quality assessment failed
|
||||
**error:** character error rate above threshold
|
||||
**paper key:** 5T8JX2
|
||||
|
||||
### Redacted fields
|
||||
- title: [redacted]
|
||||
- vault path: [redacted]
|
||||
- zotero path: [redacted]
|
||||
- config values: [redacted]
|
||||
- API key: [redacted]
|
||||
|
||||
### Steps to reproduce
|
||||
1. Open the paper in PaperForge
|
||||
2. Review OCR output
|
||||
3. Character error rate exceeds acceptable threshold
|
||||
|
||||
### Expected behavior
|
||||
OCR quality should meet the configured threshold.
|
||||
|
||||
### Environment
|
||||
[User fills in manually — none collected]</textarea>
|
||||
</div>
|
||||
|
||||
<div class="modal-actions">
|
||||
<button class="action-btn" id="draft-close">关闭</button>
|
||||
<button class="action-btn primary" id="draft-open-github">在 GitHub 新建 Issue</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* ─── State ─── */
|
||||
/*
|
||||
Each item represents a maintenance inbox entry backed by the capability model.
|
||||
Fields following the envelope spec from research/2026-07-14-capability-state-action-contract.md
|
||||
*/
|
||||
const SCENARIOS = {
|
||||
'stale-ocr': {
|
||||
items: [{
|
||||
id:'S1',module:'ocr',title:'Efficient Retrieval-Augmented Generation',
|
||||
key:'5T8JX2',capability_state:'needs_action',activity_state:'idle',
|
||||
severity:'warning',reason_code:'ocr.artifacts_stale',
|
||||
reason_text:'12 篇论文的派生 OCR 结果已过期',
|
||||
action_verb:'rebuild_derived',action_label:'重建结果',
|
||||
destructive:false,destructive_scope:null,destructive_effect:null,
|
||||
badge_label:'重建',badge_class:'rebuild',
|
||||
}]
|
||||
},
|
||||
'failed-ocr': {
|
||||
items: [{
|
||||
id:'F1',module:'ocr',title:'Deep Learning for Medical Imaging — 2024 Review',
|
||||
key:'9KM2P',capability_state:'needs_action',activity_state:'idle',
|
||||
severity:'warning',reason_code:'ocr.retryable_error',
|
||||
reason_text:'OCR 处理中断 — 可重试',
|
||||
action_verb:'redo',action_label:'重试 OCR',
|
||||
destructive:true,destructive_scope:'paper',destructive_effect:'删除该论文的现有 OCR 结果并重新处理',
|
||||
confirmation_prompt:'将删除该论文的所有 OCR 派生数据并重新处理。源代码 PDF 不受影响。',
|
||||
badge_label:'重试',badge_class:'retry',
|
||||
}]
|
||||
},
|
||||
'corrupt-index': {
|
||||
items: [{
|
||||
id:'M1',module:'memory',title:'Memory Index',
|
||||
key:'——',capability_state:'unavailable',activity_state:'idle',
|
||||
severity:'error',reason_code:'memory.db_corrupt',
|
||||
reason_text:'记忆库索引文件损坏,搜索功能不可用',
|
||||
action_verb:'restore_backup',action_label:'恢复备份',
|
||||
destructive:true,destructive_scope:'module',destructive_effect:'替换当前索引文件为最近的备份副本',
|
||||
confirmation_prompt:'将替换损坏的 memory.db 索引文件为最近备份。原始 PDF 来源、OCR 派生数据、论文元数据和设置配置均不受影响。如备份也损坏,后续需完全重建搜索索引。',
|
||||
badge_label:'损坏',badge_class:'corrupt',
|
||||
}]
|
||||
},
|
||||
'running': {
|
||||
items: [
|
||||
{
|
||||
id:'R1',module:'ocr',title:'Advances in Transformer Architecture',
|
||||
key:'R3X7H',capability_state:'needs_action',activity_state:'running',
|
||||
severity:'warning',reason_code:'ocr.artifacts_stale',
|
||||
reason_text:'正在重建派生结果…',
|
||||
action_verb:'rebuild_derived',action_label:'重建中',
|
||||
destructive:false,destructive_scope:null,destructive_effect:null,
|
||||
badge_label:'运行中',badge_class:'running-indicator',
|
||||
running:true,progress_label:'3 / 12 篇已完成',
|
||||
},
|
||||
{
|
||||
id:'R2',module:'ocr',title:'Graph Neural Networks in Drug Discovery',
|
||||
key:'X8Y4K',capability_state:'needs_action',activity_state:'idle',
|
||||
severity:'warning',reason_code:'ocr.artifacts_stale',
|
||||
reason_text:'派生 OCR 结果已过期',
|
||||
action_verb:'rebuild_derived',action_label:'重建结果',
|
||||
destructive:false,destructive_scope:null,destructive_effect:null,
|
||||
badge_label:'重建',badge_class:'rebuild',
|
||||
}
|
||||
]
|
||||
},
|
||||
'unacceptable': {
|
||||
items: [
|
||||
{
|
||||
id:'U1',module:'ocr',title:'Medical Image Analysis — A Systematic Review',
|
||||
key:'2BB8VM5W',capability_state:'needs_action',activity_state:'idle',
|
||||
severity:'warning',reason_code:'ocr.quality_unacceptable',
|
||||
reason_text:'OCR 质量低于阈值,可手动报告',
|
||||
action_verb:'investigate',action_label:'报告 Issue',
|
||||
destructive:false,destructive_scope:null,destructive_effect:null,
|
||||
badge_label:'质量不合格',badge_class:'draft',
|
||||
issue_reportable:true,
|
||||
issue_error:'character error rate above threshold (CER 0.23 vs max 0.05)',
|
||||
issue_title:'Medical Image Analysis — A Systematic Review',
|
||||
issue_path:'D:\\Vault\\Literature\\Papers\\2BB8VM5W',
|
||||
issue_zotero:'D:\\Zotero\\storage\\2BB8VM5W',
|
||||
},
|
||||
{
|
||||
id:'U2',module:'ocr',title:'Towards Explainable AI in Radiology',
|
||||
key:'6M9QN',capability_state:'needs_action',activity_state:'idle',
|
||||
severity:'warning',reason_code:'ocr.artifacts_stale',
|
||||
reason_text:'派生 OCR 结果已过期',
|
||||
action_verb:'rebuild_derived',action_label:'重建结果',
|
||||
destructive:false,destructive_scope:null,destructive_effect:null,
|
||||
badge_label:'重建',badge_class:'rebuild',
|
||||
}
|
||||
]
|
||||
},
|
||||
'empty': { items: [] },
|
||||
'resolve': null, /* handled specially — resolves the first item */
|
||||
};
|
||||
|
||||
let currentItems = [];
|
||||
let removedIds = new Set();
|
||||
|
||||
/* ─── Render ─── */
|
||||
function render() {
|
||||
const inbox = document.getElementById('inbox');
|
||||
const visible = currentItems.filter(i => !removedIds.has(i.id));
|
||||
const badge = document.getElementById('badge');
|
||||
const label = document.getElementById('status-label');
|
||||
|
||||
badge.textContent = visible.length;
|
||||
|
||||
if (visible.length === 0) {
|
||||
label.textContent = '全部正常';
|
||||
inbox.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">✓</div>
|
||||
<h3>全部正常</h3>
|
||||
<p>所有模块运行正常,无需维护操作。</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
label.textContent = `${visible.length} 项需要处理`;
|
||||
|
||||
let html = '';
|
||||
visible.forEach((item, idx) => {
|
||||
const infoStyle = item.running ? ' style="background:var(--soft);"' : '';
|
||||
const borderCls = item.severity === 'error' ? ' with-border-red' : (item.severity === 'warning' ? ' with-border-yellow' : '');
|
||||
|
||||
html += `<div class="item${borderCls}" data-id="${item.id}"${infoStyle}>`;
|
||||
html += `<div class="item-info">`;
|
||||
html += `<div class="item-title">${escHtml(item.title)}</div>`;
|
||||
html += `<div class="item-meta"><span class="item-badge ${item.badge_class}">${escHtml(item.badge_label)}</span> · ${escHtml(item.key)} · ${escHtml(moduleLabel(item.module))}</div>`;
|
||||
html += `<div class="item-reason">${escHtml(item.reason_text)}</div>`;
|
||||
|
||||
if (item.running) {
|
||||
html += `<div class="progress-row">`;
|
||||
html += `<div class="progress-bar"><div class="progress-fill" style="width:${item.progress_pct || 30}%"></div></div>`;
|
||||
html += `<span class="progress-label">${escHtml(item.progress_label || '')}</span>`;
|
||||
html += `</div>`;
|
||||
}
|
||||
|
||||
html += `</div>`; /* info */
|
||||
|
||||
html += `<div class="item-actions">`;
|
||||
if (item.activity_state !== 'running' && !item.running) {
|
||||
if (item.destructive) {
|
||||
html += `<button class="action-btn destructive" data-action="destructive" data-idx="${idx}">${escHtml(item.action_label)}</button>`;
|
||||
} else if (item.issue_reportable) {
|
||||
html += `<button class="action-btn" data-action="issue-draft" data-idx="${idx}">${escHtml(item.action_label)}</button>`;
|
||||
} else {
|
||||
html += `<button class="action-btn primary" data-action="resolve" data-idx="${idx}">${escHtml(item.action_label)}</button>`;
|
||||
}
|
||||
} else {
|
||||
html += `<button class="action-btn" disabled>处理中</button>`;
|
||||
}
|
||||
html += `</div>`;
|
||||
|
||||
html += `</div>`; /* item */
|
||||
});
|
||||
|
||||
inbox.innerHTML = html;
|
||||
updateBadge();
|
||||
}
|
||||
|
||||
function moduleLabel(m) {
|
||||
switch(m) {
|
||||
case 'ocr': return 'OCR';
|
||||
case 'memory': return '记忆';
|
||||
case 'library': return '文献库';
|
||||
default: return m;
|
||||
}
|
||||
}
|
||||
|
||||
function escHtml(s) {
|
||||
const d = document.createElement('div');
|
||||
d.textContent = s;
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
function updateBadge() {
|
||||
const visible = currentItems.filter(i => !removedIds.has(i.id));
|
||||
const badge = document.getElementById('badge');
|
||||
badge.textContent = visible.length;
|
||||
}
|
||||
|
||||
/* ─── Scenario loading ─── */
|
||||
function loadScenario(name) {
|
||||
if (name === 'resolve') {
|
||||
resolveOne();
|
||||
return;
|
||||
}
|
||||
const data = SCENARIOS[name];
|
||||
if (!data) return;
|
||||
removedIds.clear();
|
||||
// deep clone
|
||||
currentItems = data.items.map(i => ({
|
||||
...i,
|
||||
running: i.running || false,
|
||||
progress_pct: i.progress_pct || 0,
|
||||
progress_label: i.progress_label || '',
|
||||
}));
|
||||
render();
|
||||
}
|
||||
|
||||
function resolveOne() {
|
||||
const visible = currentItems.filter(i => !removedIds.has(i.id));
|
||||
if (visible.length === 0) return;
|
||||
// Resolve the first non-running item
|
||||
const target = visible.find(i => i.activity_state !== 'running' && !i.running);
|
||||
if (!target) return;
|
||||
removedIds.add(target.id);
|
||||
render();
|
||||
focusAfterRemoval();
|
||||
}
|
||||
|
||||
/* ─── Modal helpers: focus trap + inert + restoration ─── */
|
||||
let lastFocusedElement = null;
|
||||
let activeFocusTrapOverlay = null;
|
||||
|
||||
function openModal(overlay) {
|
||||
// Ensure other overlay is hidden — only one modal at a time
|
||||
document.getElementById('confirm-overlay').classList.remove('open');
|
||||
document.getElementById('draft-overlay').classList.remove('open');
|
||||
// Inert background content
|
||||
document.querySelector('.page-wrap').inert = true;
|
||||
// Track previously focused element for restoration
|
||||
lastFocusedElement = document.activeElement;
|
||||
overlay.classList.add('open');
|
||||
activeFocusTrapOverlay = overlay;
|
||||
}
|
||||
|
||||
function closeModal(overlay, opts) {
|
||||
opts = opts || {};
|
||||
overlay.classList.remove('open');
|
||||
// Restore inert
|
||||
document.querySelector('.page-wrap').inert = false;
|
||||
activeFocusTrapOverlay = null;
|
||||
// Restore focus to element that opened the modal (skip when caller manages it)
|
||||
if (!opts.skipRestore && lastFocusedElement && lastFocusedElement.focus && typeof lastFocusedElement.focus === 'function') {
|
||||
try { lastFocusedElement.focus(); } catch (_) {}
|
||||
}
|
||||
if (!opts.skipRestore) lastFocusedElement = null;
|
||||
}
|
||||
|
||||
function focusAfterRemoval() {
|
||||
const visible = currentItems.filter(i => !removedIds.has(i.id));
|
||||
// Focus deterministically: actionable button > heading
|
||||
if (visible.length > 0) {
|
||||
// Try first non-running item's action button
|
||||
const targetIdx = visible.findIndex(i => i.activity_state !== 'running' && !i.running);
|
||||
if (targetIdx >= 0) {
|
||||
const btn = document.querySelector(`.item:not(.removing) [data-action][data-idx="${targetIdx}"]`);
|
||||
if (btn && typeof btn.focus === 'function') { btn.focus(); return; }
|
||||
}
|
||||
// Fallback: any actionable button (skip disabled)
|
||||
const anyBtn = document.querySelector('.item:not(.removing) .action-btn:not(:disabled):not([hidden])');
|
||||
if (anyBtn && typeof anyBtn.focus === 'function') { anyBtn.focus(); return; }
|
||||
}
|
||||
// Empty/no-actionable state — focus the heading
|
||||
const h1 = document.querySelector('h1');
|
||||
if (h1) { h1.setAttribute('tabindex', '-1'); h1.focus(); }
|
||||
}
|
||||
|
||||
// Focus trap: cycle Tab within the active overlay
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key !== 'Tab' || !activeFocusTrapOverlay) return;
|
||||
const focusable = activeFocusTrapOverlay.querySelectorAll(
|
||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||
);
|
||||
if (focusable.length === 0) return;
|
||||
const first = focusable[0];
|
||||
const last = focusable[focusable.length - 1];
|
||||
if (e.shiftKey && document.activeElement === first) {
|
||||
e.preventDefault();
|
||||
last.focus();
|
||||
} else if (!e.shiftKey && document.activeElement === last) {
|
||||
e.preventDefault();
|
||||
first.focus();
|
||||
}
|
||||
});
|
||||
|
||||
/* ─── Confirmation flow ─── */
|
||||
let pendingDestructive = null;
|
||||
|
||||
function showConfirmation(item) {
|
||||
const overlay = document.getElementById('confirm-overlay');
|
||||
document.getElementById('confirm-title').textContent = '确认 ' + item.action_label;
|
||||
document.getElementById('confirm-body').textContent = item.confirmation_prompt || '此操作将执行变更。';
|
||||
const scopeEl = document.getElementById('confirm-scope');
|
||||
if (item.destructive_scope && item.destructive_effect) {
|
||||
scopeEl.hidden = false;
|
||||
scopeEl.textContent = '范围: ' + item.destructive_scope + ' · ' + item.destructive_effect;
|
||||
} else {
|
||||
scopeEl.hidden = true;
|
||||
}
|
||||
pendingDestructive = item;
|
||||
openModal(overlay);
|
||||
document.getElementById('confirm-ok').focus();
|
||||
}
|
||||
|
||||
function closeAnyOverlay() {
|
||||
const confirmEl = document.getElementById('confirm-overlay');
|
||||
const draftEl = document.getElementById('draft-overlay');
|
||||
if (confirmEl.classList.contains('open')) {
|
||||
confirmEl.classList.remove('open');
|
||||
pendingDestructive = null;
|
||||
}
|
||||
if (draftEl.classList.contains('open')) {
|
||||
draftEl.classList.remove('open');
|
||||
draftItem = null;
|
||||
}
|
||||
document.querySelector('.page-wrap').inert = false;
|
||||
activeFocusTrapOverlay = null;
|
||||
if (lastFocusedElement && lastFocusedElement.focus && typeof lastFocusedElement.focus === 'function') {
|
||||
try { lastFocusedElement.focus(); } catch (_) {}
|
||||
}
|
||||
lastFocusedElement = null;
|
||||
}
|
||||
|
||||
document.getElementById('confirm-cancel').addEventListener('click', () => {
|
||||
closeModal(document.getElementById('confirm-overlay'));
|
||||
pendingDestructive = null;
|
||||
});
|
||||
|
||||
document.getElementById('confirm-overlay').addEventListener('click', (e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
closeModal(document.getElementById('confirm-overlay'));
|
||||
pendingDestructive = null;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('confirm-ok').addEventListener('click', () => {
|
||||
const item = pendingDestructive;
|
||||
if (!item) return;
|
||||
// Mark confirmed — remove from inbox
|
||||
removedIds.add(item.id);
|
||||
closeModal(document.getElementById('confirm-overlay'), { skipRestore: true });
|
||||
pendingDestructive = null;
|
||||
lastFocusedElement = null;
|
||||
render();
|
||||
focusAfterRemoval();
|
||||
});
|
||||
|
||||
/* ─── Issue Draft flow ─── */
|
||||
let draftItem = null;
|
||||
|
||||
function showIssueDraft(item) {
|
||||
draftItem = item;
|
||||
const overlay = document.getElementById('draft-overlay');
|
||||
|
||||
document.getElementById('draft-key').textContent = item.key;
|
||||
document.getElementById('draft-error').textContent = item.issue_error || 'N/A';
|
||||
document.getElementById('draft-reason-text').textContent = item.reason_text;
|
||||
|
||||
const titleInput = document.getElementById('draft-title-input');
|
||||
titleInput.value = '[PaperForge] OCR quality unacceptable — ' + item.key;
|
||||
|
||||
const body = document.getElementById('draft-body');
|
||||
body.value = `## Bug: OCR quality assessment failed
|
||||
|
||||
**Module:** ocr
|
||||
**reason.code:** ocr.quality_unacceptable
|
||||
**reason.text:** ${item.reason_text}
|
||||
**error:** ${item.issue_error || 'N/A'}
|
||||
**paper key:** ${item.key}
|
||||
|
||||
### Redacted fields
|
||||
- title: [redacted]
|
||||
- vault path: [redacted]
|
||||
- zotero path: [redacted]
|
||||
- config values: [redacted]
|
||||
- API key: [redacted]
|
||||
|
||||
### Steps to reproduce
|
||||
1. Open the paper in PaperForge
|
||||
2. Review OCR output
|
||||
3. Character error rate exceeds acceptable threshold
|
||||
|
||||
### Expected behavior
|
||||
OCR quality should meet the configured threshold.
|
||||
|
||||
### Environment
|
||||
[User fills in manually — none collected]`;
|
||||
|
||||
openModal(overlay);
|
||||
titleInput.focus();
|
||||
titleInput.select();
|
||||
}
|
||||
|
||||
document.getElementById('draft-close').addEventListener('click', () => {
|
||||
closeModal(document.getElementById('draft-overlay'));
|
||||
draftItem = null;
|
||||
});
|
||||
|
||||
document.getElementById('draft-overlay').addEventListener('click', (e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
closeModal(document.getElementById('draft-overlay'));
|
||||
draftItem = null;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('draft-open-github').addEventListener('click', () => {
|
||||
const title = document.getElementById('draft-title-input').value.trim();
|
||||
const body = document.getElementById('draft-body').value.trim();
|
||||
|
||||
// User-edited title and body are included
|
||||
// We NEVER auto-submit — only open a pre-filled GitHub new-issue URL
|
||||
const params = new URLSearchParams();
|
||||
params.set('title', title || '[PaperForge] OCR quality issue');
|
||||
params.set('body', body);
|
||||
|
||||
// Open in new window — user must click "Submit new issue" themselves
|
||||
const url = 'https://github.com/LLLin000/PaperForge/issues/new?' + params.toString();
|
||||
window.open(url, '_blank');
|
||||
|
||||
// Do NOT close the draft — user may want to continue editing
|
||||
});
|
||||
|
||||
|
||||
/* ─── Inbox event delegation ─── */
|
||||
document.getElementById('inbox').addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('button[data-action]');
|
||||
if (!btn) return;
|
||||
const action = btn.dataset.action;
|
||||
const idx = parseInt(btn.dataset.idx, 10);
|
||||
const visible = currentItems.filter(i => !removedIds.has(i.id));
|
||||
const item = visible[idx];
|
||||
if (!item) return;
|
||||
|
||||
switch (action) {
|
||||
case 'resolve':
|
||||
removedIds.add(item.id);
|
||||
render();
|
||||
focusAfterRemoval();
|
||||
break;
|
||||
case 'destructive':
|
||||
showConfirmation(item);
|
||||
break;
|
||||
case 'issue-draft':
|
||||
showIssueDraft(item);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
/* ─── Scenario button wiring ─── */
|
||||
document.querySelectorAll('[data-scenario]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
loadScenario(btn.dataset.scenario);
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('reset-all').addEventListener('click', () => {
|
||||
removedIds.clear();
|
||||
currentItems = [];
|
||||
render();
|
||||
});
|
||||
/* ─── Keyboard support ─── */
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
closeAnyOverlay();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
/* ─── Init ─── */
|
||||
loadScenario('empty');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
297
docs/prototypes/2026-07-14-maintenance-issue-reporting.md
Normal file
297
docs/prototypes/2026-07-14-maintenance-issue-reporting.md
Normal file
|
|
@ -0,0 +1,297 @@
|
|||
# Maintenance Issue Reporting — 原型设计说明
|
||||
|
||||
**日期:** 2026-07-15
|
||||
**方式:** Issue #72
|
||||
**配套原型:** `2026-07-14-maintenance-issue-reporting.html`
|
||||
**涉及方式:** #69 (能力状态模型), #70 (托管运行时架构), #71 (Control Center 原型)
|
||||
**文件路径:** `docs/prototypes/2026-07-14-maintenance-issue-reporting.html`
|
||||
|
||||
---
|
||||
|
||||
## 1. 设计目标
|
||||
|
||||
原生原型用于探索和维护导航 **actionable-only 维护收件箱** 概念验证:
|
||||
|
||||
1. 只显示用户需要关心的维护项(不显示"正常"项,不制造焦虑)
|
||||
2. 每个维护项提供明确的可操作入口按钮
|
||||
3. 展示能力状态模型(availability × activity × attention)
|
||||
4. 提供隐私安全的 Issue 报告流程(可编辑草稿 + 显式打开 GitHub)
|
||||
5. 确定性的场景控制方便评审
|
||||
|
||||
### 非目标
|
||||
|
||||
- 不是插件代码,不涉及真实数据加载
|
||||
- 不替代生产维护标签页的实现
|
||||
- 不涉及缓存策略、TTL 刷新、后端探测
|
||||
- 不涉及批量操作(全选/执行已选)
|
||||
|
||||
---
|
||||
|
||||
## 2. 对应的能力状态合同关系(#69)
|
||||
|
||||
本原型严格按照 [2026-07-14-capability-state-action-contract.md](../research/2026-07-14-capability-state-action-contract.md) 定义的三维能力模型:
|
||||
|
||||
### 2.1 可用性轴 (Availability)
|
||||
|
||||
| 状态 | 原型场景 | 显示含义 |
|
||||
|------|---------|---------|
|
||||
| `needs_action` | 过期 OCR、OCR 失败、不合格 OCR | 黄色提示,可操作 |
|
||||
| `unavailable` | 索引损坏 | 红色提示,需要修复 |
|
||||
| `ready` | 全部正常 | 不显示任何维护项 |
|
||||
|
||||
### 2.2 活动轴 (Activity)
|
||||
|
||||
`running` 状态下,物品保持可见但按钮变为 `disabled` 并显示"处理中"。
|
||||
进展栏和进度文本也一同显示。**Running 不隐藏 attention:** 即使在运行也保持显示。
|
||||
|
||||
### 2.3 注意力轴 (Attention)
|
||||
|
||||
- 普通状态保持安静,底部和徽章显示计数
|
||||
- `running` 从不掩盖 warning/error 徽章(合同 §2 明确)
|
||||
|
||||
### 2.4 单一主要行动不变量
|
||||
|
||||
每个模块至多一个 `action.primary`。原型中:
|
||||
- `needs_action` → `rebuild_derived` / `redo` / `investigate`
|
||||
- `unavailable` → `restore_backup`
|
||||
- `ready` → 无操作,收件箱为空
|
||||
|
||||
---
|
||||
|
||||
## 3. 维护投影规则(#69 §8)
|
||||
|
||||
维护是派生视图(Derived View),不是独立模块。本原型通过汇总各模块状态来模拟:
|
||||
|
||||
- **入口条件:** 模块从 `ready` / `unknown` 转换成 `unavailable` / `needs_action`
|
||||
- **出口条件:** 用户执行操作 → 项消失 → 如果所有项都消失,回到`全部正常`
|
||||
- **空状态:** `0 项需要处理` → 显示空状态引导
|
||||
|
||||
---
|
||||
|
||||
## 4. 破坏性操作确认流程(#69 §5, #68)
|
||||
|
||||
继承自 Desktop Runtime Recovery Patterns 研究中的三阶恢复模型(§4.2.1):
|
||||
|
||||
| 操作 | 场景 | 确认要求 | 生效范围 |
|
||||
|------|------|---------|---------|
|
||||
| `rebuild_derived` | 过期 OCR | 不需要(非破坏性) | selection |
|
||||
| `redo` | OCR 失败 | 需要确认 | paper |
|
||||
| `restore_backup` | 索引损坏 | 需要确认 | module |
|
||||
|
||||
确认弹窗显示:
|
||||
- **标题:** `确认 <操作名>`(保留用户看到的操作描述)
|
||||
- **确认提示:** 描述后果
|
||||
- **破坏范围:** `范围: <scope> · <effect>`
|
||||
|
||||
---
|
||||
|
||||
## 5. 隐私安全 Issue 报告流程(#42, #69 §9)
|
||||
|
||||
直接继承 VS Code(预填 Issue Report)和 Docker Desktop(诊断导出供本地检查再提交)的模式。
|
||||
|
||||
### 流程
|
||||
|
||||
1. 用户点击 "报告 Issue"
|
||||
2. 弹窗显示 **包含的字段**(模块、reason.code、reason.text、error_summary、key)
|
||||
3. 弹窗显示 **排除的字段**(论文标题、vault 路径、zotero 路径、配置值、API Key),全部标记为 `[redacted]`
|
||||
4. 用户可以在文本框中任意编辑草稿
|
||||
5. **⚠ 显式警告:** "草稿仅本地展示,不会自动提交。编辑完成后,点击下方按钮再 Github 新建 Issue。"
|
||||
6. 用户点击"在 GitHub 新建 Issue" → `window.open(github.com/LLLin000/PaperForge/issues/new?...)` → 用户仍需在 GitHub 页面手动点击 "Submit new issue"
|
||||
7. 点击后不关闭弹窗(方便用户继续编辑)
|
||||
8. 草稿不上传、不提交 API、不包含 token
|
||||
|
||||
### 不变量
|
||||
|
||||
- ❌ NEVER 自动打开 GitHub(用户必须显式点击按钮)
|
||||
- ❌ NEVER 自动提交 Issue
|
||||
- ❌ NEVER 上传任何数据
|
||||
- ❌ 不包含 token、API key、本地路径、环境变量
|
||||
|
||||
---
|
||||
|
||||
## 6. UI 设计决策
|
||||
|
||||
### 6.1 设计语言
|
||||
|
||||
继承 Vercel 灰阶设计体系(来自 DESIGN.md):
|
||||
|
||||
| Token | 使用位置 | 值 |
|
||||
|-------|---------|-----|
|
||||
| ink | 标题、主文本 | #171717 |
|
||||
| body | 辅助文本、原因 | #4d4d4d |
|
||||
| mute | 元信息、徽章数字 | #888888 |
|
||||
| hairline | 卡片描边 | #ebebeb |
|
||||
| link | 主要操作按钮 | #0070f3 |
|
||||
| error | 破坏性操作/严重状态 | #ee0000 |
|
||||
| warning | 警告状态 | #f5a623 |
|
||||
|
||||
### 6.2 组件映射
|
||||
|
||||
- **场景工具栏:** pill 样式按钮(符合 nav-link 组件定义)
|
||||
- **物品卡片:** card-marketing-large 变体(圆角 8px, padding 16px, hairline 描边)
|
||||
- **空状态:** ex-empty-state-card(48px 圆形图标 + 标题 + 描述)
|
||||
- **确认弹窗:** ex-modal-card(圆角 12px, shadow-md, 最大宽 480px)
|
||||
- **浮动层:** 半透明 ink 遮罩(rgba(23,23,23,.4))
|
||||
|
||||
### 6.3 响应式
|
||||
|
||||
- 768px 断点:折成单列堆叠布局
|
||||
- 标题改为纵向排列
|
||||
- 场景栏改为堆叠
|
||||
- 物品卡片:信息区域和按钮纵向排列
|
||||
|
||||
### 6.4 无状态
|
||||
|
||||
- 没有外部依赖(vanilla HTML/CSS/JS)
|
||||
- 直接打开 HTML 文件即可完整工作
|
||||
- 不需要服务端、WebSocket、localStorage
|
||||
|
||||
---
|
||||
|
||||
## 7. 场景设计理由
|
||||
|
||||
| 场景 | 测试的能力模型 | 覆盖的操作类型 |
|
||||
|------|---------------|---------------|
|
||||
| 过期 OCR 结果 | `needs_action` + `idle` | 非破坏性的 `rebuild_derived` |
|
||||
| OCR 失败 | `needs_action` + `idle` | 破坏性的 `redo`(需确认) |
|
||||
| 索引损坏 | `unavailable` + `idle` | 破坏性的 `restore_backup`(模块级) |
|
||||
| 正在重建 | `needs_action` + `running` | 混合状态(一个在运行 + 一个待处理) |
|
||||
| → 解决一项 | 从收件箱移除项 | 跳过 running 项,只解决 idle 项 |
|
||||
| 质量不合格 | `needs_action` + 可报告 Issue | Issue 草稿流(包含/排除预览) |
|
||||
| 全部正常 | `ready` | 空状态 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 从 #69/#70 到本原型的关键路径
|
||||
|
||||
```
|
||||
#69 capability contract #70 managed runtime
|
||||
│ │
|
||||
│ ┌──────────────────────────────────┘
|
||||
│ │
|
||||
│ ▼
|
||||
│ Action verbs & reason codes
|
||||
│ Maintenance projection rules
|
||||
│ Destructive confirmation contract
|
||||
│ Issue draft privacy rules
|
||||
│
|
||||
│ ┌──────────────────────────────────┐
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ #71 Control Center Prototype │
|
||||
│ (module cards from envelope) │
|
||||
│ │
|
||||
│ ▼ │
|
||||
│ #72 Maintenance Issue Reporting │
|
||||
│ (actionable inbox + Issue draft) │
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 浏览器验证步骤
|
||||
|
||||
### 9.1 快速验证(桌面端,~1200px 宽)
|
||||
|
||||
1. 打开 `docs/prototypes/2026-07-14-maintenance-issue-reporting.html`
|
||||
2. **初始状态:** 看到 "全部正常" 空状态,徽章显示 "0"
|
||||
3. **点击 "过期 OCR 结果":** 看到 1 条物品,有 "重建" 徽章和 "重建结果" 按钮(蓝色 primary)
|
||||
4. **点击 "重建结果":** 物品消失,回到空状态
|
||||
5. **点击 "OCR 失败":** 看到 "重试" 徽章和 "重试 OCR" 按钮
|
||||
6. **点击 "重试 OCR":** 弹出确认弹窗(显示范围:"paper"级破坏性)
|
||||
7. **按 Esc** 关闭弹窗。再次点击 "重试 OCR" → **点击 "确认执行"** → 物品消失
|
||||
8. **点击 "索引损坏":** 看到 "损坏" 徽章、"恢复备份" 按钮。点击 → 看到模块级确认
|
||||
9. **点击 "正在重建":** 看到 2 条物品。第一条标记 "运行中" 有进度条和 "处理中" 禁用按钮;第二条有 "重建结果" 按钮
|
||||
10. **点击 "→ 解决一项":** 运行中物品保留,非运行中的物品被移除
|
||||
11. **点击 "质量不合格":** 看到 "质量不合格" 徽章和 "报告 Issue" 按钮
|
||||
12. **点击 "报告 Issue":** 弹出 Issue 草稿面板
|
||||
13. **查看"包含的字段"和"排除的字段":** 标题、路径、配置等已脱敏
|
||||
14. **点击 "在 GitHub 新建 Issue":** 新标签页打开 GitHub Issues,不自动提交,草稿面板保持打开
|
||||
15. **按 Esc** 关闭草稿面板
|
||||
16. **点击 "重置":** 回到空状态
|
||||
|
||||
### 9.2 响应式验证(768px 宽)
|
||||
|
||||
1. 调整浏览器窗口到 768px 或使用 DevTools 的设备仿真
|
||||
2. 验证场景栏折为纵向布局
|
||||
3. 验证物品卡片堆叠排列
|
||||
4. 确认所有操作按钮仍可点击
|
||||
5. 确认弹窗仍可读(文本自适应)
|
||||
|
||||
### 9.3 键盘验证
|
||||
|
||||
1. `Tab` 导航所有场景按钮和操作按钮
|
||||
2. 确认所有 `:focus-visible` 出现蓝色(`--link`)光环
|
||||
3. `Enter` 激活焦点按钮
|
||||
4. `Esc` 关闭弹窗和草稿面板
|
||||
5. 弹出确认弹窗时焦点自动移到"确认执行"按钮
|
||||
|
||||
### 9.4 隐私安全验证
|
||||
|
||||
1. 点击 "报告 Issue" → 确认草稿面板没有用户路径、文件内容、API key
|
||||
2. 确认"排除的字段"区域显示 `[redacted]`
|
||||
3. 确认草稿面板有关于"不会自动提交"的警告
|
||||
4. 确认 "在 GitHub 新建 Issue" 按钮行为:在真实浏览器中打开新标签页,不在当前标签页导航
|
||||
5. 确认 URL 参数仅包含 `title` 和 `body`(不包含 token/secret)
|
||||
|
||||
---
|
||||
|
||||
## 10. 已知限制
|
||||
|
||||
| 限制 | 描述 | 计划解决方案 |
|
||||
|------|------|------------|
|
||||
| 数据完全模拟 | 没有真正的后端探测 | 集成时替换为真实 envelope |
|
||||
| 无缓存层 | 每次重新加载都重置状态 | #71 阶段加入缓存 |
|
||||
| 无批量操作 | 不支持全选/执行已选 | #73 或后续迭代 |
|
||||
| 仅限桌面焦点 | 没有针对移动端布局优化 | 后续可选 |
|
||||
| 无 i18n | 全部中文硬编码 | 后续接入 `t()` 系统 |
|
||||
| 无动画 | 移除物品时使用过渡过度动画 | 后续添加入/出动画 |
|
||||
|
||||
## 11. 评审修复记录
|
||||
|
||||
### 11.1 Fix 1 — 模态框焦点陷阱 + 背景 inert + 焦点恢复
|
||||
|
||||
**问题:** 弹窗打开后没有焦点陷阱,Tab 可以跳出到背景内容;没有 inert 阻断,键盘用户(屏幕阅读器)可以访问背景元素;关闭后焦点没有回到触发元素。
|
||||
|
||||
**方案:**
|
||||
- `openModal(overlay)`:设置 `.page-wrap` 的 `inert = true`、保存 `lastFocusedElement`、打开弹窗
|
||||
- `closeModal(overlay)`:移除 `.open`、恢复 `inert = false`、将焦点恢复到 `lastFocusedElement`
|
||||
- `closeAnyOverlay()`:一次性关闭所有可能的弹窗并恢复 inert 和焦点
|
||||
- 全局 `keydown` 监听 Tab 键:当焦点在弹窗第一个/最后一个可聚焦元素时,循环回到另一端的 `preventDefault` + `focus()`
|
||||
- 使用 `openModal` 时主动关闭另一个弹窗,确保任何时候只有一个弹窗可见
|
||||
|
||||
**验证结果:** inert 在弹窗打开时设置,关闭时恢复;Escape 调用 `closeAnyOverlay()` 正确关闭所有弹窗并恢复焦点。
|
||||
|
||||
### 11.2 Fix 2 — Issue 标题可编辑 + 在 URL 中使用编辑后标题
|
||||
|
||||
**问题:** 原标题在 draft-body 中硬编码,用户无法在标题区域编辑,GitHub URL 中 title 参数是固定的。
|
||||
|
||||
**方案:**
|
||||
- 在 Issue 草稿面板头部添加可编辑的 `<input type="text" id="draft-title-input">`
|
||||
- `showIssueDraft()` 时预填入 `[PaperForge] OCR quality unacceptable — <key>`
|
||||
- 用户可自由编辑标题文本
|
||||
- `draft-open-github` 点击时从 `draft-title-input.value` 读取标题,传入 URL 参数
|
||||
- 焦点默认移到标题输入框,用户可以直接开始编辑
|
||||
|
||||
**验证结果:** 标题输入框存在(type="text"),预填入正确值,URL 使用编辑后标题。
|
||||
|
||||
### 11.3 Fix 3 — 索引损坏确认文本明确替换内容与保留数据
|
||||
|
||||
**问题:** 原确认提示 "用最近的备份替换当前的索引" 没有说明哪些数据会替换、哪些保留。
|
||||
|
||||
**方案:**
|
||||
- 新确认文本:*"将替换损坏的 memory.db 索引文件为最近备份。原始 PDF 来源、OCR 派生数据、论文元数据和设置配置均不受影响。如备份也损坏,后续需完全重建搜索索引。"*
|
||||
- 明确列出被替换的内容:`memory.db` 索引文件
|
||||
- 明确列出保留的内容:原始 PDF 来源、OCR 派生数据、论文元数据、设置配置
|
||||
- 描述后续故障模式:如备份也损坏,需完全重建
|
||||
|
||||
**验证结果:** 确认文本和范围文本均已包含上述信息。
|
||||
|
||||
### 11.4 Fix 4 — `[hidden]` 样式权重修复
|
||||
|
||||
**问题:** 浏览发现两个弹窗的遮罩同时可见。原因是 `.open { display: flex }` 选择器权重 > HTML `[hidden]` 属性,导致 `hidden` 被覆盖。
|
||||
|
||||
**方案:**
|
||||
- 在 CSS 顶部添加 `[hidden]{display:none!important}`
|
||||
- `openModal()` 主动关闭另一个弹窗(双重保险)
|
||||
|
||||
**验证结果:** 打开草稿面板时确认弹窗不可见,打开确认弹窗时草稿面板不可见。两个弹窗不会同时出现。
|
||||
964
docs/prototypes/2026-07-14-six-module-control-center.html
Normal file
964
docs/prototypes/2026-07-14-six-module-control-center.html
Normal file
|
|
@ -0,0 +1,964 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PaperForge 控制中心 · 原型</title>
|
||||
<style>
|
||||
/* ===== Tokens (Vercel-inspired grayscale) ===== */
|
||||
:root {
|
||||
--ink: #171717;
|
||||
--body: #4d4d4d;
|
||||
--mute: #888888;
|
||||
--hairline: #ebebeb;
|
||||
--hairline-strong: #a1a1a1;
|
||||
--canvas: #ffffff;
|
||||
--canvas-soft: #fafafa;
|
||||
--canvas-soft-2: #f5f5f5;
|
||||
--link: #0070f3;
|
||||
--success: #0070f3;
|
||||
--error: #ee0000;
|
||||
--error-soft: #f7d4d6;
|
||||
--warning: #f5a623;
|
||||
--warning-soft: #ffefcf;
|
||||
--warning-deep: #ab570a;
|
||||
--gray-ok: #22a67e;
|
||||
--gray-ok-soft: #e0f5ed;
|
||||
|
||||
--radius-sm: 6px;
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 12px;
|
||||
--radius-pill: 100px;
|
||||
|
||||
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
|
||||
|
||||
--shadow-sm: 0 1px 2px rgba(0,0,0,0.04);
|
||||
--shadow-md: 0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04);
|
||||
--focus-ring: 0 0 0 2px var(--link);
|
||||
}
|
||||
|
||||
/* ===== Reset ===== */
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html { font-size: 16px; -webkit-font-smoothing: antialiased; }
|
||||
body {
|
||||
font-family: var(--font);
|
||||
color: var(--body);
|
||||
background: var(--canvas-soft);
|
||||
line-height: 1.5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* ===== Focus ===== */
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--link);
|
||||
outline-offset: 2px;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
button:focus-visible, a:focus-visible, [tabindex]:focus-visible {
|
||||
outline-offset: 2px;
|
||||
}
|
||||
button:focus:not(:focus-visible) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* ===== Layout ===== */
|
||||
.app {
|
||||
max-width: 1120px;
|
||||
margin: 0 auto;
|
||||
padding: 0 24px 48px;
|
||||
}
|
||||
|
||||
/* ===== Header ===== */
|
||||
.header {
|
||||
padding: 40px 0 8px;
|
||||
border-bottom: 1px solid var(--hairline);
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.header h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--ink);
|
||||
letter-spacing: -0.5px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.header p {
|
||||
font-size: 14px;
|
||||
color: var(--mute);
|
||||
}
|
||||
|
||||
/* ===== Scenario Switcher ===== */
|
||||
.scenarios {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 28px;
|
||||
padding-top: 20px;
|
||||
}
|
||||
.scenario-btn {
|
||||
font-family: var(--font);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
padding: 6px 16px;
|
||||
border-radius: var(--radius-pill);
|
||||
border: 1px solid var(--hairline);
|
||||
background: var(--canvas);
|
||||
color: var(--body);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
letter-spacing: -0.1px;
|
||||
}
|
||||
.scenario-btn:hover {
|
||||
border-color: var(--hairline-strong);
|
||||
color: var(--ink);
|
||||
background: var(--canvas-soft);
|
||||
}
|
||||
.scenario-btn.active {
|
||||
background: var(--ink);
|
||||
border-color: var(--ink);
|
||||
color: var(--canvas);
|
||||
}
|
||||
|
||||
/* ===== Primary Attention Zone ===== */
|
||||
.primary-zone {
|
||||
background: var(--canvas);
|
||||
border: 1px solid var(--hairline);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
.primary-zone.error-primary {
|
||||
border-left: 3px solid var(--error);
|
||||
}
|
||||
.primary-zone.warning-primary {
|
||||
border-left: 3px solid var(--warning);
|
||||
}
|
||||
.primary-zone.ok-primary {
|
||||
border-left: 3px solid var(--gray-ok);
|
||||
}
|
||||
.primary-zone.unknown-primary {
|
||||
border-left: 3px solid var(--hairline-strong);
|
||||
}
|
||||
.primary-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--mute);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.primary-headline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.primary-headline h2 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--ink);
|
||||
letter-spacing: -0.4px;
|
||||
flex: 1;
|
||||
}
|
||||
.primary-action-btn {
|
||||
font-family: var(--font);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
padding: 8px 20px;
|
||||
border-radius: var(--radius-pill);
|
||||
border: none;
|
||||
background: var(--ink);
|
||||
color: var(--canvas);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
.primary-action-btn:hover { opacity: 0.85; }
|
||||
.primary-action-btn:active { opacity: 0.7; }
|
||||
.primary-description {
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
color: var(--body);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ===== Module Grid ===== */
|
||||
.module-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.module-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.module-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
/* ===== Module Card ===== */
|
||||
.module-card {
|
||||
background: var(--canvas);
|
||||
border: 1px solid var(--hairline);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 20px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: box-shadow 0.15s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.module-card:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
.module-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* ===== State Badge ===== */
|
||||
.state-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
padding: 2px 10px;
|
||||
border-radius: var(--radius-pill);
|
||||
line-height: 1.4;
|
||||
}
|
||||
.state-badge .dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.state-badge.ok { background: var(--gray-ok-soft); color: #1a7a5c; }
|
||||
.state-badge.ok .dot { background: var(--gray-ok); }
|
||||
.state-badge.warning { background: var(--warning-soft); color: var(--warning-deep); }
|
||||
.state-badge.warning .dot { background: var(--warning); }
|
||||
.state-badge.error { background: var(--error-soft); color: var(--error); }
|
||||
.state-badge.error .dot { background: var(--error); }
|
||||
.state-badge.unknown { background: var(--canvas-soft-2); color: var(--mute); }
|
||||
.state-badge.unknown .dot { background: var(--hairline-strong); }
|
||||
|
||||
.module-name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--ink);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.module-reason {
|
||||
font-size: 13px;
|
||||
color: var(--mute);
|
||||
line-height: 1.4;
|
||||
}
|
||||
.module-reason strong {
|
||||
color: var(--body);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ===== Progress ===== */
|
||||
.activity-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: var(--body);
|
||||
}
|
||||
.spinner {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 2px solid var(--hairline);
|
||||
border-top-color: var(--link);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
.progress-bar {
|
||||
flex: 1;
|
||||
height: 4px;
|
||||
background: var(--canvas-soft-2);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: var(--link);
|
||||
border-radius: 2px;
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
|
||||
/* ===== Action Button ===== */
|
||||
.module-action-btn {
|
||||
font-family: var(--font);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
padding: 6px 14px;
|
||||
border-radius: var(--radius-pill);
|
||||
border: 1px solid var(--hairline);
|
||||
background: var(--canvas);
|
||||
color: var(--ink);
|
||||
cursor: pointer;
|
||||
align-self: flex-start;
|
||||
transition: all 0.12s ease;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.module-action-btn:hover {
|
||||
background: var(--canvas-soft-2);
|
||||
border-color: var(--hairline-strong);
|
||||
}
|
||||
.module-action-btn.primary-action {
|
||||
background: var(--ink);
|
||||
border-color: var(--ink);
|
||||
color: var(--canvas);
|
||||
}
|
||||
.module-action-btn.primary-action:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
/* ===== Advanced Diagnostics ===== */
|
||||
.advanced-toggle {
|
||||
font-family: var(--font);
|
||||
font-size: 12px;
|
||||
color: var(--link);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 4px 0;
|
||||
align-self: flex-start;
|
||||
text-decoration: underline 1px dotted transparent;
|
||||
text-underline-offset: 3px;
|
||||
transition: text-decoration-color 0.15s;
|
||||
}
|
||||
.advanced-toggle:hover {
|
||||
text-decoration-color: var(--link);
|
||||
}
|
||||
.advanced-details {
|
||||
display: none;
|
||||
font-size: 12px;
|
||||
color: var(--mute);
|
||||
padding: 10px 12px;
|
||||
background: var(--canvas-soft);
|
||||
border-radius: var(--radius-sm);
|
||||
line-height: 1.6;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
.advanced-details.open {
|
||||
display: block;
|
||||
}
|
||||
.advanced-details dt {
|
||||
color: var(--body);
|
||||
font-weight: 500;
|
||||
}
|
||||
.advanced-details dd {
|
||||
margin-left: 0;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
/* ===== Snackbar ===== */
|
||||
.snackbar {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(80px);
|
||||
background: var(--ink);
|
||||
color: var(--canvas);
|
||||
font-size: 14px;
|
||||
padding: 10px 20px;
|
||||
border-radius: var(--radius-pill);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
opacity: 0;
|
||||
transition: all 0.3s ease;
|
||||
pointer-events: none;
|
||||
z-index: 100;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.snackbar.show {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="app">
|
||||
|
||||
<!-- Header -->
|
||||
<header class="header">
|
||||
<h1>PaperForge 控制中心</h1>
|
||||
<p>安装 · 文献库 · OCR · 记忆 · 维护 · 帮助</p>
|
||||
</header>
|
||||
|
||||
<!-- Scenario Switcher -->
|
||||
<nav class="scenarios" aria-label="场景切换">
|
||||
<button class="scenario-btn" data-scenario="first-run" aria-current="true">首次运行</button>
|
||||
<button class="scenario-btn" data-scenario="partial">部分就绪</button>
|
||||
<button class="scenario-btn" data-scenario="active-ocr">OCR 进行中</button>
|
||||
<button class="scenario-btn" data-scenario="stale">快照过期</button>
|
||||
<button class="scenario-btn" data-scenario="all-ready">一切正常</button>
|
||||
</nav>
|
||||
|
||||
<!-- Primary Attention Zone -->
|
||||
<div class="primary-zone" id="primaryZone" role="alert" aria-live="polite">
|
||||
<div class="primary-label">需要注意</div>
|
||||
<div class="primary-headline">
|
||||
<h2 id="primaryTitle">PaperForge 尚未安装</h2>
|
||||
<button class="primary-action-btn" id="primaryBtn">开始安装</button>
|
||||
</div>
|
||||
<p class="primary-description" id="primaryDesc">完成安装后即可管理文献、运行 OCR 和构建记忆库。</p>
|
||||
</div>
|
||||
|
||||
<!-- Module Grid -->
|
||||
<div class="module-grid" id="moduleGrid" role="list">
|
||||
<!-- Each card generated by JS -->
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Snackbar -->
|
||||
<div class="snackbar" id="snackbar" role="status" aria-live="polite"></div>
|
||||
|
||||
<script>
|
||||
// ===== Module Definitions =====
|
||||
const MODULES = [
|
||||
{ id: 'installation', label: '安装', icon: '⚙' },
|
||||
{ id: 'library', label: '文献库', icon: '▤' },
|
||||
{ id: 'ocr', label: 'OCR', icon: '◐' },
|
||||
{ id: 'memory', label: '记忆', icon: '◇' },
|
||||
{ id: 'maintenance', label: '维护', icon: '△' },
|
||||
{ id: 'help', label: '帮助', icon: '?' },
|
||||
];
|
||||
|
||||
// ===== Scenario Data =====
|
||||
const SCENARIOS = {
|
||||
'first-run': {
|
||||
primary: {
|
||||
severity: 'warning',
|
||||
title: 'PaperForge 尚未安装',
|
||||
action: { label: '开始安装', verb: 'setup' },
|
||||
desc: '完成安装后即可管理文献、运行 OCR 和构建记忆库。'
|
||||
},
|
||||
modules: {
|
||||
installation: {
|
||||
state: 'missing_input', severity: 'warning',
|
||||
reason: '需要配置',
|
||||
detail: '安装路径、Python 运行环境和 API 密钥均未设置。',
|
||||
action: { label: '开始安装', verb: 'setup', primary: true },
|
||||
diag: { '状态': '未安装', '配置文件': '不存在', '运行环境': '未检测' }
|
||||
},
|
||||
library: {
|
||||
state: 'missing_input', severity: 'warning',
|
||||
reason: 'Zotero 导出路径未设置',
|
||||
detail: '需要指定 Zotero 的 Better BibTeX 导出文件路径才能同步文献。',
|
||||
action: { label: '配置路径', verb: 'set_config', primary: false },
|
||||
diag: { '同步状态': '未同步', '文献数量': '--', '导出文件': '未指定' }
|
||||
},
|
||||
ocr: {
|
||||
state: 'missing_input', severity: 'warning',
|
||||
reason: 'API 密钥未配置',
|
||||
detail: 'OCR 需要有效的 PaddleOCR API 密钥才能处理论文图片。',
|
||||
action: { label: '配置密钥', verb: 'set_config', primary: false },
|
||||
diag: { 'API 状态': '未配置', '可用额度': '--', '模型版本': '--' }
|
||||
},
|
||||
memory: {
|
||||
state: 'missing_input', severity: 'warning',
|
||||
reason: '记忆库尚未创建',
|
||||
detail: '需要先完成安装并同步文献后才能创建文献记忆库。',
|
||||
action: { label: '配置记忆库', verb: 'set_config', primary: false },
|
||||
diag: { '数据库状态': '未创建', '索引状态': '--', '检索就绪': '--' }
|
||||
},
|
||||
maintenance: {
|
||||
state: 'needs_action', severity: 'warning',
|
||||
reason: '4 个模块需要配置',
|
||||
detail: '安装、文献库、OCR、记忆四个模块均处于未就绪状态。可点击各模块的按钮单独处理。',
|
||||
action: null,
|
||||
diag: { '待处理项目': '4', '影响模块': '安装 / 文献库 / OCR / 记忆' }
|
||||
},
|
||||
help: {
|
||||
state: 'ready', severity: 'ok',
|
||||
reason: '随时可用',
|
||||
detail: '帮助文档已就绪,可以通过命令面板或右键菜单访问。',
|
||||
action: null,
|
||||
diag: { '文档版本': '2026-07', '文档数量': '12 篇', '最后更新': '2026-07-14' }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
'partial': {
|
||||
primary: {
|
||||
severity: 'warning',
|
||||
title: 'OCR 处理结果已过期,记忆库索引需要更新',
|
||||
action: { label: '重建衍生文件', verb: 'rebuild_derived' },
|
||||
desc: '12 篇论文的 OCR 衍生文件已过期。同时记忆库索引需要更新(5 篇待索引)。点击"重建衍生文件"优先处理过期项。'
|
||||
},
|
||||
modules: {
|
||||
installation: {
|
||||
state: 'ready', severity: 'ok',
|
||||
reason: '运行正常',
|
||||
detail: 'PaperForge 已安装,Python 运行环境版本 3.11,配置文件完整。',
|
||||
action: null,
|
||||
diag: { '版本': '1.3.0', '运行环境': 'Python 3.11.4 (venv)', '安装方式': 'pip' }
|
||||
},
|
||||
library: {
|
||||
state: 'ready', severity: 'ok',
|
||||
reason: '已同步',
|
||||
detail: '文献库已于 2 分钟前自动同步,共 247 篇论文,无新增。',
|
||||
action: null,
|
||||
diag: { '文献总数': '247', '同步时间': '2 分钟前', 'Zotero 连接': '正常' }
|
||||
},
|
||||
ocr: {
|
||||
state: 'needs_action', severity: 'warning',
|
||||
reason: '12 篇论文的衍生文件已过期',
|
||||
detail: 'OCR 版本更新后,这些论文的解析结果需要重新生成,不会覆盖原始识别结果。',
|
||||
action: { label: '重建衍生文件', verb: 'rebuild_derived', primary: true },
|
||||
diag: { '待处理': '12 篇', '原始识别': '已完成', '衍生版本': '过期 (v2→v3)' }
|
||||
},
|
||||
memory: {
|
||||
state: 'needs_action', severity: 'warning',
|
||||
reason: '索引需要更新',
|
||||
detail: '有 5 篇新论文已同步,全文索引和向量索引需要重建才能搜索到这些文献。',
|
||||
action: { label: '重建索引', verb: 'rebuild_index', primary: true },
|
||||
diag: { '待索引': '5 篇', '当前索引': '242 篇', '向量维度': '768' }
|
||||
},
|
||||
maintenance: {
|
||||
state: 'needs_action', severity: 'warning',
|
||||
reason: '2 个模块需要注意',
|
||||
detail: 'OCR 衍生文件过期,记忆库索引需要更新。可点击各模块的按钮单独处理。',
|
||||
action: null,
|
||||
items: ['OCR: 12 篇衍生文件过期', '记忆: 5 篇待索引'],
|
||||
diag: { '待处理': '2 项', '影响模块': 'OCR / 记忆' }
|
||||
},
|
||||
help: {
|
||||
state: 'ready', severity: 'ok',
|
||||
reason: '随时可用',
|
||||
detail: '帮助文档已就绪,可以通过命令面板或右键菜单访问。',
|
||||
action: null,
|
||||
diag: { '文档版本': '2026-07', '文档数量': '12 篇', '最后更新': '2026-07-14' }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
'active-ocr': {
|
||||
primary: {
|
||||
severity: 'warning',
|
||||
title: 'OCR 正在处理 3/12 篇论文',
|
||||
action: { label: '查看进度', verb: 'investigate' },
|
||||
desc: 'OCR 批处理正在进行中。已处理 3 篇,预计剩余 4 分钟。同时记忆库索引需要更新。'
|
||||
},
|
||||
modules: {
|
||||
installation: {
|
||||
state: 'ready', severity: 'ok',
|
||||
reason: '运行正常',
|
||||
detail: 'PaperForge 已安装,运行环境版本 1.3.0。',
|
||||
action: null,
|
||||
diag: { '版本': '1.3.0', '运行环境': 'Python 3.11.4', '安装方式': 'pip' }
|
||||
},
|
||||
library: {
|
||||
state: 'ready', severity: 'ok',
|
||||
reason: '已同步',
|
||||
detail: '文献库同步正常。',
|
||||
action: null,
|
||||
diag: { '文献总数': '247', '同步时间': '10 分钟前', 'Zotero 连接': '正常' }
|
||||
},
|
||||
ocr: {
|
||||
state: 'needs_action', severity: 'warning',
|
||||
reason: 'OCR 批处理进行中',
|
||||
activity: { label: '处理中', progress: { current: 3, total: 12 } },
|
||||
detail: '正在处理第 3 篇论文的 OCR 识别,预计剩余 4 分钟。',
|
||||
action: { label: '查看详情', verb: 'investigate', primary: false },
|
||||
diag: { '进度': '3/12', '已处理': '3 篇', '队列中': '9 篇', '模型': 'PaddleOCR v4' }
|
||||
},
|
||||
memory: {
|
||||
state: 'needs_action', severity: 'warning',
|
||||
reason: '索引需要更新',
|
||||
detail: 'OCR 完成后需要更新全文索引和向量索引。',
|
||||
action: { label: '稍后重建', verb: 'rebuild_index', primary: false },
|
||||
diag: { '待索引': '5 篇', '当前索引': '242 篇' }
|
||||
},
|
||||
maintenance: {
|
||||
state: 'needs_action', severity: 'warning',
|
||||
reason: 'OCR 进行中,记忆库待索引',
|
||||
detail: 'OCR 批处理运行中(3/12)。记忆库索引需要更新。各模块均可独立操作。',
|
||||
action: null,
|
||||
items: ['OCR: 批处理进行中', '记忆: 5 篇待索引'],
|
||||
diag: { '待处理': '2 项', '影响模块': 'OCR / 记忆' }
|
||||
},
|
||||
help: {
|
||||
state: 'ready', severity: 'ok',
|
||||
reason: '随时可用',
|
||||
detail: '帮助文档已就绪。',
|
||||
action: null,
|
||||
diag: { '文档版本': '2026-07', '文档数量': '12 篇' }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
'stale': {
|
||||
primary: {
|
||||
severity: 'warning',
|
||||
label: '缓存过期',
|
||||
title: '部分模块状态已过期',
|
||||
action: { label: '刷新过期模块', verb: 'probe' },
|
||||
desc: 'OCR、文献库、记忆库的缓存快照已超过有效期。安装状态和帮助文档仍为最新(有效期 3600 秒)。点击"刷新过期模块"重新检测。'
|
||||
},
|
||||
modules: {
|
||||
installation: {
|
||||
state: 'ready', severity: 'ok',
|
||||
reason: '运行正常(缓存有效)',
|
||||
detail: '安装配置缓存 3600 秒有效,上次检测 6 分钟前,仍在有效期内。',
|
||||
action: null,
|
||||
diag: { '版本': '1.3.0', '运行环境': 'Python 3.11.4', '有效期': '3600 秒', '下次过期': '54 分钟后' }
|
||||
},
|
||||
library: {
|
||||
state: 'unknown', severity: 'unknown',
|
||||
reason: '状态未知(缓存已过期)',
|
||||
detail: '文献库缓存有效期 300 秒,上次检测 6 分钟前(已过期)。需要重新检测。',
|
||||
action: { label: '检测', verb: 'probe', primary: false },
|
||||
diag: { '状态': '未知', '上次检测': '6 分钟前', '有效期': '300 秒', '已超时': '3 分钟' }
|
||||
},
|
||||
ocr: {
|
||||
state: 'unknown', severity: 'unknown',
|
||||
reason: '状态未知(缓存已过期)',
|
||||
detail: 'OCR 缓存有效期 60 秒,上次检测 6 分钟前(已过期)。需要重新检测。',
|
||||
action: { label: '检测', verb: 'probe', primary: false },
|
||||
diag: { '状态': '未知', '上次检测': '6 分钟前', '有效期': '60 秒', '已超时': '5 分钟' }
|
||||
},
|
||||
memory: {
|
||||
state: 'unknown', severity: 'unknown',
|
||||
reason: '状态未知(缓存已过期)',
|
||||
detail: '记忆库缓存有效期 300 秒,上次检测 6 分钟前(已过期)。需要重新检测。',
|
||||
action: { label: '检测', verb: 'probe', primary: false },
|
||||
diag: { '状态': '未知', '上次检测': '6 分钟前', '有效期': '300 秒', '已超时': '3 分钟' }
|
||||
},
|
||||
maintenance: {
|
||||
state: 'unknown', severity: 'unknown',
|
||||
reason: '状态未知(依赖模块已过期)',
|
||||
detail: '维护视图依赖的底层模块缓存已过期,无法投影有效状态。需要重新检测。',
|
||||
action: { label: '检测', verb: 'probe', primary: false },
|
||||
diag: { '状态': '未知', '依赖模块': 'OCR / 文献库 / 记忆', '缓存': '已过期' }
|
||||
},
|
||||
help: {
|
||||
state: 'ready', severity: 'ok',
|
||||
reason: '随时可用(缓存有效)',
|
||||
detail: '帮助文档缓存 3600 秒有效,上次检测 6 分钟前,仍在有效期内。',
|
||||
action: null,
|
||||
diag: { '文档版本': '2026-07', '有效期': '3600 秒', '下次过期': '54 分钟后' }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
'all-ready': {
|
||||
primary: {
|
||||
severity: 'ok',
|
||||
title: '一切正常',
|
||||
action: { label: '刷新状态', verb: 'probe' },
|
||||
desc: '所有模块均已就绪。PaperForge 正常运行,文献库已同步,OCR 已就绪,记忆库已索引。'
|
||||
},
|
||||
modules: {
|
||||
installation: {
|
||||
state: 'ready', severity: 'ok',
|
||||
reason: '运行正常',
|
||||
detail: 'PaperForge 已安装并运行正常。',
|
||||
action: null,
|
||||
diag: { '版本': '1.3.0', '运行环境': 'Python 3.11.4', '安装方式': 'pip' }
|
||||
},
|
||||
library: {
|
||||
state: 'ready', severity: 'ok',
|
||||
reason: '已同步',
|
||||
detail: '文献库同步正常,共 247 篇论文。',
|
||||
action: null,
|
||||
diag: { '文献总数': '247', '同步时间': '刚刚', 'Zotero 连接': '正常' }
|
||||
},
|
||||
ocr: {
|
||||
state: 'ready', severity: 'ok',
|
||||
reason: '已就绪',
|
||||
detail: 'OCR 服务正常运行,API 密钥有效。',
|
||||
action: null,
|
||||
diag: { 'API 状态': '有效', '可用额度': '充足', '模型版本': 'PaddleOCR v4' }
|
||||
},
|
||||
memory: {
|
||||
state: 'ready', severity: 'ok',
|
||||
reason: '已索引',
|
||||
detail: '全文索引和向量索引均已就绪。',
|
||||
action: null,
|
||||
diag: { '已索引': '247 篇', '全文索引': '已构建', '向量索引': '已构建' }
|
||||
},
|
||||
maintenance: {
|
||||
state: 'ready', severity: 'ok',
|
||||
reason: '无待处理事项',
|
||||
detail: '所有模块运行正常,无需处理。',
|
||||
action: null,
|
||||
items: [],
|
||||
diag: { '待处理': '0 项', '最后检查': '刚刚' }
|
||||
},
|
||||
help: {
|
||||
state: 'ready', severity: 'ok',
|
||||
reason: '随时可用',
|
||||
detail: '帮助文档已就绪。',
|
||||
action: null,
|
||||
diag: { '文档版本': '2026-07', '文档数量': '12 篇' }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ===== Render =====
|
||||
let currentScenario = 'first-run';
|
||||
|
||||
function showSnackbar(text) {
|
||||
const el = document.getElementById('snackbar');
|
||||
el.textContent = text;
|
||||
el.classList.add('show');
|
||||
setTimeout(() => el.classList.remove('show'), 2500);
|
||||
}
|
||||
|
||||
function renderScenario(scenarioId) {
|
||||
currentScenario = scenarioId;
|
||||
const data = SCENARIOS[scenarioId];
|
||||
const grid = document.getElementById('moduleGrid');
|
||||
const zone = document.getElementById('primaryZone');
|
||||
|
||||
// Update active button
|
||||
document.querySelectorAll('.scenario-btn').forEach(btn => {
|
||||
const isActive = btn.dataset.scenario === scenarioId;
|
||||
btn.classList.toggle('active', isActive);
|
||||
btn.setAttribute('aria-current', isActive ? 'true' : 'false');
|
||||
});
|
||||
|
||||
// Primary zone
|
||||
const prim = data.primary;
|
||||
zone.className = 'primary-zone';
|
||||
if (prim.severity === 'error') zone.classList.add('error-primary');
|
||||
else if (prim.severity === 'warning') zone.classList.add('warning-primary');
|
||||
else if (prim.severity === 'ok') zone.classList.add('ok-primary');
|
||||
else if (prim.severity === 'unknown') zone.classList.add('unknown-primary');
|
||||
|
||||
const primaryLabelEl = zone.querySelector('.primary-label');
|
||||
if (prim.severity === 'ok') primaryLabelEl.textContent = '总览';
|
||||
else if (prim.severity === 'unknown') primaryLabelEl.textContent = '状态未知';
|
||||
else if (prim.label) primaryLabelEl.textContent = prim.label;
|
||||
else primaryLabelEl.textContent = '需要注意';
|
||||
|
||||
document.getElementById('primaryTitle').textContent = prim.title;
|
||||
document.getElementById('primaryDesc').textContent = prim.desc;
|
||||
const primaryBtn = document.getElementById('primaryBtn');
|
||||
primaryBtn.textContent = prim.action.label;
|
||||
primaryBtn.dataset.verb = prim.action.verb;
|
||||
primaryBtn.style.display = '';
|
||||
|
||||
// Module cards
|
||||
grid.innerHTML = '';
|
||||
MODULES.forEach(mod => {
|
||||
const info = data.modules[mod.id];
|
||||
if (!info) return;
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'module-card';
|
||||
card.setAttribute('role', 'listitem');
|
||||
|
||||
// Header
|
||||
const header = document.createElement('div');
|
||||
header.className = 'module-card-header';
|
||||
|
||||
const nameEl = document.createElement('span');
|
||||
nameEl.className = 'module-name';
|
||||
nameEl.textContent = mod.label;
|
||||
|
||||
// Severity badge
|
||||
const badge = document.createElement('span');
|
||||
badge.className = `state-badge ${severityClass(info.severity)}`;
|
||||
badge.innerHTML = `<span class="dot"></span>${stateLabel(info.state)}`;
|
||||
if (info.state === 'ready') badge.title = '正常工作';
|
||||
else if (info.state === 'unknown') badge.title = '缓存已过期,无法确定实际状态';
|
||||
else if (info.state === 'needs_action') badge.title = '需要操作';
|
||||
else if (info.state === 'missing_input') badge.title = '缺少配置';
|
||||
else badge.title = info.reason;
|
||||
|
||||
header.appendChild(nameEl);
|
||||
header.appendChild(badge);
|
||||
card.appendChild(header);
|
||||
|
||||
// Reason
|
||||
const reasonEl = document.createElement('p');
|
||||
reasonEl.className = 'module-reason';
|
||||
reasonEl.textContent = info.reason;
|
||||
card.appendChild(reasonEl);
|
||||
|
||||
// Activity (if running)
|
||||
if (info.activity) {
|
||||
const act = document.createElement('div');
|
||||
act.className = 'activity-row';
|
||||
act.setAttribute('aria-label', info.activity.label);
|
||||
|
||||
const spinner = document.createElement('div');
|
||||
spinner.className = 'spinner';
|
||||
spinner.setAttribute('aria-hidden', 'true');
|
||||
act.appendChild(spinner);
|
||||
|
||||
const label = document.createElement('span');
|
||||
label.textContent = info.activity.label;
|
||||
act.appendChild(label);
|
||||
|
||||
if (info.activity.progress) {
|
||||
const bar = document.createElement('div');
|
||||
bar.className = 'progress-bar';
|
||||
bar.setAttribute('role', 'progressbar');
|
||||
bar.setAttribute('aria-valuenow', info.activity.progress.current);
|
||||
bar.setAttribute('aria-valuemin', '0');
|
||||
bar.setAttribute('aria-valuemax', info.activity.progress.total);
|
||||
|
||||
const fill = document.createElement('div');
|
||||
fill.className = 'progress-fill';
|
||||
const pct = info.activity.progress.current / info.activity.progress.total * 100;
|
||||
fill.style.width = pct + '%';
|
||||
bar.appendChild(fill);
|
||||
act.appendChild(bar);
|
||||
|
||||
const count = document.createElement('span');
|
||||
count.textContent = `${info.activity.progress.current}/${info.activity.progress.total}`;
|
||||
act.appendChild(count);
|
||||
}
|
||||
|
||||
card.appendChild(act);
|
||||
}
|
||||
|
||||
// Detail text
|
||||
if (info.detail) {
|
||||
const detailEl = document.createElement('p');
|
||||
detailEl.className = 'module-reason';
|
||||
detailEl.style.color = 'var(--body)';
|
||||
detailEl.textContent = info.detail;
|
||||
card.appendChild(detailEl);
|
||||
}
|
||||
|
||||
// Maintenance items list
|
||||
if (info.items && info.items.length > 0) {
|
||||
const itemsList = document.createElement('ul');
|
||||
itemsList.style.cssText = 'font-size:13px;color:var(--body);list-style:none;padding:0;margin:2px 0 0;';
|
||||
info.items.forEach(item => {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = '· ' + item;
|
||||
itemsList.appendChild(li);
|
||||
});
|
||||
card.appendChild(itemsList);
|
||||
}
|
||||
|
||||
// Action button
|
||||
if (info.action) {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'module-action-btn' + (info.action.primary ? ' primary-action' : '');
|
||||
btn.textContent = info.action.label;
|
||||
btn.dataset.verb = info.action.verb;
|
||||
btn.dataset.module = mod.id;
|
||||
btn.addEventListener('click', () => handleAction(info.action.verb, mod.label));
|
||||
card.appendChild(btn);
|
||||
}
|
||||
|
||||
// Advanced diagnostics toggle
|
||||
if (info.diag) {
|
||||
const toggle = document.createElement('button');
|
||||
toggle.className = 'advanced-toggle';
|
||||
toggle.textContent = '诊断信息';
|
||||
toggle.setAttribute('aria-expanded', 'false');
|
||||
toggle.addEventListener('click', () => {
|
||||
const panel = card.querySelector('.advanced-details');
|
||||
const expanded = panel.classList.toggle('open');
|
||||
toggle.setAttribute('aria-expanded', expanded);
|
||||
toggle.textContent = expanded ? '收起诊断信息' : '诊断信息';
|
||||
});
|
||||
card.appendChild(toggle);
|
||||
|
||||
const diagPanel = document.createElement('dl');
|
||||
diagPanel.className = 'advanced-details';
|
||||
Object.entries(info.diag).forEach(([key, val]) => {
|
||||
const dt = document.createElement('dt');
|
||||
dt.textContent = key;
|
||||
const dd = document.createElement('dd');
|
||||
dd.textContent = val;
|
||||
diagPanel.appendChild(dt);
|
||||
diagPanel.appendChild(dd);
|
||||
});
|
||||
card.appendChild(diagPanel);
|
||||
}
|
||||
|
||||
grid.appendChild(card);
|
||||
});
|
||||
|
||||
// Focus management: focus the primary action button on scenario change
|
||||
primaryBtn.focus();
|
||||
}
|
||||
|
||||
function severityClass(sev) {
|
||||
if (sev === 'ok') return 'ok';
|
||||
if (sev === 'warning') return 'warning';
|
||||
if (sev === 'error') return 'error';
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function stateLabel(state) {
|
||||
const labels = {
|
||||
'ready': '就绪',
|
||||
'unknown': '未知',
|
||||
'unavailable': '不可用',
|
||||
'missing_input': '待配置',
|
||||
'needs_action': '需处理',
|
||||
'limited': '受限'
|
||||
};
|
||||
return labels[state] || state;
|
||||
}
|
||||
|
||||
function handleAction(verb, moduleLabel) {
|
||||
const messages = {
|
||||
'setup': '正在启动安装流程...',
|
||||
'set_config': '正在打开配置界面...',
|
||||
'sync': '正在同步文献库...',
|
||||
'rebuild_derived': '正在重建衍生文件...',
|
||||
'rebuild_index': '正在重建索引...',
|
||||
'redo': '需要确认:此操作将删除现有结果',
|
||||
'restore_backup': '需要确认:将从备份恢复数据库',
|
||||
'run': '正在处理...',
|
||||
'migrate': '正在执行数据库迁移...',
|
||||
'update': '正在检查更新...',
|
||||
'investigate': '正在收集诊断信息...',
|
||||
'probe': '正在检测状态...'
|
||||
};
|
||||
const msg = messages[verb] || `正在执行: ${verb}`;
|
||||
showSnackbar(`${moduleLabel}: ${msg}`);
|
||||
}
|
||||
|
||||
// ===== Event Wiring =====
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Scenario buttons
|
||||
document.querySelectorAll('.scenario-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
renderScenario(e.currentTarget.dataset.scenario);
|
||||
});
|
||||
btn.addEventListener('keydown', (e) => {
|
||||
const btns = Array.from(document.querySelectorAll('.scenario-btn'));
|
||||
const idx = btns.indexOf(e.currentTarget);
|
||||
let nextIdx = -1;
|
||||
if (e.key === 'ArrowRight') nextIdx = (idx + 1) % btns.length;
|
||||
else if (e.key === 'ArrowLeft') nextIdx = (idx - 1 + btns.length) % btns.length;
|
||||
if (nextIdx >= 0) {
|
||||
e.preventDefault();
|
||||
btns[nextIdx].click();
|
||||
btns[nextIdx].focus();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Primary action
|
||||
document.getElementById('primaryBtn').addEventListener('click', () => {
|
||||
const data = SCENARIOS[currentScenario];
|
||||
if (data) handleAction(data.primary.action.verb, 'PaperForge');
|
||||
});
|
||||
|
||||
// Initial render
|
||||
renderScenario('first-run');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
212
docs/prototypes/2026-07-14-six-module-control-center.md
Normal file
212
docs/prototypes/2026-07-14-six-module-control-center.md
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
# Issue #71 — Six-Module Control Center Prototype
|
||||
|
||||
**Date:** 2026-07-15
|
||||
**Author:** ControlCenterPrototype
|
||||
**Prototype:** `docs/prototypes/2026-07-14-six-module-control-center.html`
|
||||
**Parent:** Wayfinder #65 — Make the PaperForge control center self-explanatory
|
||||
**Inputs:** #69 capability state/action contract, #66 contract audit, #67 Obsidian patterns, #68 desktop runtime patterns, DESIGN.md
|
||||
|
||||
---
|
||||
|
||||
## 1. Design Overview
|
||||
|
||||
A first-time and returning-user control center for six modules: 安装 / 文献库 / OCR / 记忆 / 维护 / 帮助. The layout uses a two-zone structure:
|
||||
|
||||
- **Primary Attention Zone** (top): shows the single module needing most urgent attention, with its primary action as a dark pill button. Normal state says "一切正常" with a less prominent refresh action.
|
||||
- **Module Grid** (3-column, responsive to 1-column at 768px): six independent cards, each showing module name, capability state badge, reason text, actionable details, optional progress/activity, a single action button (only when non-ready), and an expandable "诊断信息" diagnostic disclosure.
|
||||
|
||||
### Design language
|
||||
|
||||
Vercel-inspired grayscale palette from `DESIGN.md`: `#171717` ink, `#4d4d4d` body, `#888888` mute, `#ebebeb` hairline, `#ffffff` canvas, `#fafafa` canvas-soft, `#f5f5f5` canvas-soft-2. Green (`#22a67e`) for ready states uses the same chroma-to-value ratio as the existing warning (`#f5a623`) and error (`#ee0000`) signals, not a saturated green that would clash with the grayscale system. No emojis, no glassmorphism, no gradient text, no cyan-on-black.
|
||||
|
||||
---
|
||||
|
||||
## 2. State Model (from #69 capability contract)
|
||||
|
||||
Three orthogonal axes per module:
|
||||
|
||||
```
|
||||
Availability x Activity x Attention
|
||||
```
|
||||
|
||||
| Axis | Values | Notes |
|
||||
|------|--------|-------|
|
||||
| Availability | `unknown < unavailable < missing_input < needs_action < limited < ready` | Ordinal, worst-to-best |
|
||||
| Activity | `idle` / `running` | Never masks availability badge |
|
||||
| Attention | Derived from availability | One primary action per module, `null` when ready |
|
||||
|
||||
### Contract rules implemented in prototype
|
||||
|
||||
| Rule | Implementation |
|
||||
|------|---------------|
|
||||
| One primary action per module | Action button shown only when state is not `ready`. Button label maps to verb table ($\leq 12$ verbs). |
|
||||
| Running never hides warning | Activity spinner + progress bar shown alongside the warning badge. OCR with `needs_action` + progress bar keeps yellow badge. |
|
||||
| Stale → unknown by TTL | Short-TTL modules (60–300s) render as `unknown` (grey) when TTL expired. Long-TTL modules (3600s: installation, help) remain `ready` when still within TTL. Never leaks last-known state. |
|
||||
| Maintenance is derived view, no batch action | 维护 card has `action: null` in all scenarios. No batch "全部处理" or "run" button. Lists constituent module issues as inline bullets; users act on individual modules. |
|
||||
| Primary zone exposes one concrete action | Hero action is the single highest-priority verb (e.g. `rebuild_derived` for OCR), never a generic "查看详情". |
|
||||
| Plain buttons, not ARIA tabpanel | Scenario switcher uses `<button>` elements with `aria-current="true"`. No `role="tab"`, `role="tablist"`, or `role="tabpanel"`. Clicking a scenario changes the entire page state — inconsistent with tabpanel pattern. |
|
||||
| Quiet ready states | `ready` modules show green "就绪" badge with no action button, just a brief reason text. |
|
||||
| Advanced details without JSON | Expandable "诊断信息" section uses `dl`/`dt`/`dd` pairs, never raw JSON or Python traceback. |
|
||||
---
|
||||
|
||||
## 3. Scenario State Definitions
|
||||
|
||||
### First Run (`first-run`)
|
||||
|
||||
| Module | State | Action |
|
||||
|--------|-------|--------|
|
||||
| 安装 | `missing_input` — 需要配置 | 开始安装 (primary) |
|
||||
| 文献库 | `missing_input` — Zotero 导出路径未设置 | 配置路径 |
|
||||
| OCR | `missing_input` — API 密钥未配置 | 配置密钥 |
|
||||
| 记忆 | `missing_input` — 记忆库尚未创建 | 配置记忆库 |
|
||||
| 维护 | `needs_action` — 4 个模块需要注意 | null (派生视图,无批量操作) |
|
||||
| 帮助 | `ready` — 随时可用 | null |
|
||||
|
||||
Primary zone: "PaperForge 尚未安装" (warning, yellow left border).
|
||||
|
||||
### Partially Ready (`partial`)
|
||||
|
||||
| Module | State | Action |
|
||||
|--------|-------|--------|
|
||||
| 安装 | `ready` — 运行正常 | null |
|
||||
| 文献库 | `ready` — 已同步 | null |
|
||||
| OCR | `needs_action` — 12 篇论文衍生文件过期 | **重建衍生文件** (primary) |
|
||||
| 记忆 | `needs_action` — 索引需要更新 | 重建索引 |
|
||||
| 维护 | `needs_action` — 2 个模块需要注意 | null (派生视图,无批量操作) |
|
||||
| 帮助 | `ready` — 随时可用 | null |
|
||||
|
||||
Primary zone: "OCR 处理结果已过期,记忆库索引需要更新" (warning, yellow left border). Hero action is **"重建衍生文件"** (`rebuild_derived`) — the single highest-priority concrete action. The description also notes the second item: memory index rebuild. Demonstrates **one concrete primary action** when multiple modules need attention.
|
||||
|
||||
### Stale Snapshot (`stale`)
|
||||
|
||||
Simulates a state where **short-TTL probes have expired** (library 300s, OCR 60s, memory 300s) while **long-TTL probes remain fresh** (installation 3600s, help 3600s). Last probe was 6 minutes ago.
|
||||
|
||||
| Module | State | Action |
|
||||
|--------|-------|--------|
|
||||
| 安装 | `ready` — 运行正常(缓存有效) | null |
|
||||
| 文献库 | `unknown` — 状态未知(缓存已过期) | 检测 |
|
||||
| OCR | `unknown` — 状态未知(缓存已过期) | 检测 |
|
||||
| 记忆 | `unknown` — 状态未知(缓存已过期) | 检测 |
|
||||
| 维护 | `unknown` — 状态未知(依赖模块已过期) | 检测 |
|
||||
| 帮助 | `ready` — 随时可用(缓存有效) | null |
|
||||
|
||||
Primary zone: "部分模块状态已过期" (warning, yellow left border, label "缓存过期"). Cards: 2 green, 4 grey. Action is "刷新过期模块" (`probe`). Demonstrates **TTL-respecting fail-closed state**: fresh modules never go unknown when their TTL has not expired.
|
||||
|
||||
---
|
||||
|
||||
## 4. Interaction & UX Design
|
||||
|
||||
### Primary action per attention item (contract rule)
|
||||
|
||||
Each non-ready card has **exactly one action button**. The button label maps to a verb from the $12$-verb table. When the module is `ready`, no button appears. This satisfies the #69 invariant: `action.primary` is `null` when ready.
|
||||
|
||||
### Active operation feedback
|
||||
|
||||
When activity is `running`, the card shows:
|
||||
- CSS spinner (animated border rotation, `--link` blue to signal process health)
|
||||
- Activity label text ("处理中")
|
||||
- Progress bar with `role="progressbar"` and `aria-valuenow`/`aria-valuemin`/`aria-valuemax`
|
||||
- Fractional counter ("3/12")
|
||||
- Badge remains visible (yellow warning badge coexists with spinner in "active-ocr" scenario)
|
||||
|
||||
### Advanced diagnostic disclosure
|
||||
|
||||
The "诊断信息" toggle expands a `dl` panel with key-value diagnostic data. No JSON, no Python error messages, no raw tracebacks. Example entries:
|
||||
- "版本: 1.3.0"
|
||||
- "运行环境: Python 3.11.4"
|
||||
- "API 状态: 有效"
|
||||
- "全文索引: 已构建"
|
||||
- "待处理: 2 项"
|
||||
|
||||
The toggle is a text-only link (`<button>` styled as underlined text, `--link` blue) that says "诊断信息" when collapsed and "收起诊断信息" when expanded.
|
||||
|
||||
### Snackbar feedback
|
||||
|
||||
Every action button click shows a translucent snackbar at the bottom of the viewport with a Chinese message describing the action being taken ("PaperForge: 正在启动安装流程...", "OCR: 正在重建衍生文件..."). The snackbar auto-dismisses after 2.5 seconds.
|
||||
|
||||
### Keyboard navigation
|
||||
|
||||
- Scenario tabs are `role="tablist"` with arrow key navigation (Left/Right arrow switches tab)
|
||||
- All buttons are natively focusable `<button>` elements
|
||||
- Focus moves to the primary action button on scenario switch
|
||||
- `:focus-visible` uses `--link` blue outline (`#0070f3`) at 2px with offset
|
||||
- Diagnostics toggle reports `aria-expanded` state
|
||||
|
||||
### Responsive design
|
||||
|
||||
- Default: 3-column grid
|
||||
- `< 900px`: 2-column grid
|
||||
- `< 768px`: single-column grid (verified: `720px` card width within `768px` viewport, no overflow, no clipped controls)
|
||||
- All controls visible and functional at 768px (verified: 17 buttons, 0 clipped)
|
||||
|
||||
---
|
||||
|
||||
## 5. Adopt/Reject Decisions
|
||||
|
||||
| Decision | Adopt/Reject | Rationale |
|
||||
|----------|-------------|-----------|
|
||||
| Primary zone with one dominant module | **Adopt** | Follows #69 "one primary action per attention" and #68 "per-module actionable maintenance, never global OK" pattern (VS Code, Docker Desktop). |
|
||||
| Unknown state as solid grey, never last-known color | **Adopt** | Directly from #69 contract: "Stale is never ready". Grey conveys "no information" without implying readiness or failure. |
|
||||
| Expandable diagnostics without JSON | **Adopt** | Does not expose XML/JSON structure; uses native `dl`/`dt`/`dd` for definition list. From #68 "local diagnostic export with user review before issue creation" (VS Code pattern). |
|
||||
| Chinese-first copy with English only behind disclosure | **Adopt** | Target audience reads zh-CN. Diagnostic panel uses same Chinese labels for field names; only version identifiers and technical codes use English when unavoidable ("Python 3.11.4", "PaddleOCR v4"). |
|
||||
| Maintenance as derived view with item list | **Adopt** | From #69: maintenance aggregates non-ready states; has `items[]` array referencing constituent modules. Shown as bullet list in "首次运行" and "部分就绪". |
|
||||
| No emoji module icons | **Adopt** | Contract says no emojis. Used monochrome geometric symbols (`◐`, `▤`, `◇`, `△`, `⚙`) as CSS content instead. |
|
||||
| Scenario switches as styled pills | **Adopt** | Follows Vercel muted pill tab pattern from DESIGN.md (`.tab-ghost`). Active state inverts to solid `--ink` background. |
|
||||
| Action buttons: dark pill for primary, outlined for secondary | **Adopt** | Matches `button-primary` / `button-secondary` from DESIGN.md. Primary scenario action uses solid `--ink`; module-level primary actions also solid; secondary actions use outlined style. |
|
||||
| Activity spinner in CSS only (no JS animation frame) | **Adopt** | Simpler, lower energy, no JS dependency. Uses `@keyframes spin` with `border-top-color` trick. |
|
||||
| Progress bar with ARIA progressbar role | **Adopt** | Accessibility requirement; screen readers get exact numeric progress. |
|
||||
| Snackbar for action feedback | **Adopt** | From #67 Obsidian Notice pattern: transient message at bottom of viewport with 2.5s auto-dismiss. Used instead of a true Notice to stay in vanilla HTML/JS. |
|
||||
| Focus management on scenario switch | **Adopt** | MVP of #67 "`checkCallback` probe + re-evaluate" pattern: switching scenario focuses the primary action button so keyboard users don't lose context. |
|
||||
| No error state (red badge) in current demo | **Reject** (deferred) | The `unavailable` state with `error` severity exists in the contract but is not exercised by the five demo scenarios. A future prototype or #72 maintenance view should demonstrate `unavailable` (e.g., "Python runtime not found"). |
|
||||
| Maintenance item list as inline bullets | **Adopt** | Clearer than links or hidden details. Each item shows module name + issue type ("OCR: 12 篇衍生文件过期"). Unobtrusive at `13px` body color. |
|
||||
| TTL values hardcoded in diagnostic text | **Adopt** | Matches #69 note: "actual TTLs should be calibrated during the #71 prototype phase". Each `unknown` card shows both `lastProbe` and `ttl` as plain text under diagnostics. This makes TTL calibration observable. |
|
||||
| Plain buttons instead of ARIA tabpanel | **Adopt** (after review) | Original used `role="tablist"`/`role="tab"`/`aria-selected` with keyboard arrow navigation. Changed to plain `<button>` with `aria-current` because clicking a scenario changes the entire page state — inconsistent with the tabpanel pattern where each tab reveals a panel with `aria-labelledby`. Arrow key navigation preserved. |
|
||||
| Maintenance card has no action button | **Adopt** (after review) | Removed all maintenance card `action` fields (were `probe` or `run` in various scenarios). Maintenance is a derived view per #69 contract — no independent action. Users act on individual modules. Item list shows constituent issues. |
|
||||
| Stale scenario preserves long-TTL modules as ready | **Adopt** (after review) | Original stale scenario set all modules to `unknown`. Fixed: installation and help (3600s TTL) remain `ready` because 6 minutes < 60 minutes. Library/OCR/memory (60–300s TTL) render as `unknown`. Maintenance depends on the expired modules so also `unknown`. |
|
||||
| Partial hero shows concrete action, not generic | **Adopt** (after review) | Primary zone action changed from `investigate` ("查看详情") to `rebuild_derived` ("重建衍生文件"). The priority algorithm selects OCR's action as the most urgent concrete verb across all non-ready modules. |
|
||||
|
||||
---
|
||||
|
||||
| Test | Result | Detail |
|
||||
|------|--------|--------|
|
||||
| First-run scenario | Pass | Primary: "PaperForge 尚未安装". Badges: 1 ready, 5 warning. Maintenance has no action button. |
|
||||
| Partial scenario | Pass | Primary: "OCR 处理结果已过期,记忆库索引需要更新". Hero action: "重建衍生文件". Badges: 3 ready, 3 warning. Maintenance has no batch action. |
|
||||
| Active OCR scenario | Pass | Primary: "OCR 正在处理 3/12 篇论文". OCR card has spinner + progress bar. Warning badge visible despite running activity. Maintenance has no action. |
|
||||
| Stale scenario | Pass | Primary: "部分模块状态已过期" with label "缓存过期". Badges: 2 ready (install+help), 4 unknown (library/OCR/memory/maintenance). Install/help preserved as ready under 3600s TTL. |
|
||||
| All-ready scenario | Pass | Primary: "一切正常". All 6 badges: ready (green). No action buttons on any card. |
|
||||
| Diagnostic toggle | Pass | Click opens `dl` panel, toggles label to "收起诊断信息", sets `aria-expanded="true"`. Multiple panels open simultaneously. |
|
||||
| Action button feedback | Pass | Each action click shows snackbar with module-specific Chinese message. Primary: "PaperForge: 正在启动安装流程...". OCR: "OCR: 正在重建衍生文件...". |
|
||||
| 768px responsive | Pass | Single column grid (720px cards within 768px viewport). No overflow. All 16 buttons visible and unclipped. Diagnostics fits within card width. |
|
||||
| Keyboard navigation | Pass | Arrow keys switch scenario buttons. Focus moves to primary action on scenario switch. |
|
||||
| Plain buttons (no ARIA tab role) | Pass | No `role="tab"`, `role="tablist"`, or `role="tabpanel"`. Active button uses `aria-current="true"`. Arrow key navigation preserved via JS. |
|
||||
| No external resources | Pass | Zero network requests. System font stack. No external CSS/JS. |
|
||||
| 768px responsive | Pass | Single column grid ($720\text{ px}$ cards within $768\text{ px}$ viewport). No overflow. All 16 buttons visible and unclipped. Diagnostics fits within card width. Keyboard focusable at this width. |
|
||||
| Keyboard tab navigation | Pass | Arrow keys switch scenario buttons. Focus moves to primary action on scenario switch. |
|
||||
---
|
||||
|
||||
## 7. Relationship to Issue #72
|
||||
|
||||
This prototype is scoped to #71 (control center information hierarchy and interaction model). It does not implement:
|
||||
|
||||
- Actionable stale/failed/corrupt work filtering (maintenance inbox)
|
||||
- Successful update disappearance
|
||||
- Privacy-safe GitHub Issue draft generation and review
|
||||
|
||||
These are the #72 scope and will be addressed in a separate prototype (`docs/prototypes/2026-07-14-maintenance-inbox.html`).
|
||||
|
||||
The two prototypes share:
|
||||
- Same CSS token set and design language
|
||||
- Same capability state model (`unknown/unavailable/missing_input/needs_action/limited/ready`)
|
||||
- Same action verb table (`setup/set_config/sync/rebuild_derived/rebuild_index/probe/investigate/run/migrate/update/restore_backup/redo`)
|
||||
- Same "stale is never ready" fail-closed contract
|
||||
|
||||
---
|
||||
|
||||
## 8. Open Questions for #79 Design Review
|
||||
|
||||
1. **TTL calibration**: Current values (60–3600s, from contract) are placeholders. Should installation really have a 3600s TTL if config changes at runtime are rare but the consequences of stale config are high?
|
||||
2. **Error state (`unavailable`, red)**: Not exercised in any scenario. Should a "breakage" scenario be added showing a missing runtime or corrupt database?
|
||||
3. **Action confirmation**: Destructive actions (`redo`, `restore_backup`) need a confirmation modal in production that this prototype does not mock.
|
||||
4. **First-run vs Returning-user differentiation**: The "首次运行" scenario shows all missing input. How does the system distinguish true first-run (never probed) from all-modules-unknown (TTL expired)? The contract says `unknown` for both; the banner label could differ.
|
||||
5. **Primary zone priority algorithm**: Currently hardcoded per scenario. The production system needs a deterministic algorithm: worst severity first, then earliest `updated_at`, then alphabetical by module id.
|
||||
6. **Help module probes**: The contract TTL is 3600s, but what does probing help actually check? File existence? File content integrity? Version match with plugin?
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# OCR-v2 Active Queue
|
||||
> Status: OCR-v2 is stable; OCR maintenance streaming with canonical per-row actions is implemented; the capability vocabulary and managed-runtime architecture are chosen.
|
||||
> Last updated: 2026-07-14
|
||||
> Status: OCR-v2 is stable; OCR maintenance streaming with canonical per-row actions is implemented; the capability vocabulary, managed-runtime architecture, control-center prototype (#71), and maintenance inbox prototype (#72) are all resolved.
|
||||
> Last updated: 2026-07-15
|
||||
|
||||
## Current checkpoint
|
||||
|
||||
|
|
@ -18,6 +18,9 @@
|
|||
- **[#69](https://github.com/LLLin000/PaperForge/issues/69) resolved** at `issuecomment-4971161072`: orthogonal availability/activity/attention axes, 6-state capability ordinal, 12 canonical verbs, backend-owned severity and primary actions, maintenance projection.
|
||||
- **[#70](https://github.com/LLLin000/PaperForge/issues/70) resolved** at `issuecomment-4971239398`: plugin-managed immutable runtime slots, system-Python bootstrap with validated-triplet fallback, single `active-runtime.json` pointer, `ManagedRuntime` class with `current()`/`status()`/`ensure()`, fail-closed command resolution.
|
||||
|
||||
- **[#71](https://github.com/LLLin000/PaperForge/issues/71) resolved**: six-module control-center HTML prototype with 5 scenarios, plain-button switcher, primary attention zone, responsive layout (768px breakpoint), and capability-gated actions. Independent Critical/Important PASS review. Design decisions recorded in `docs/prototypes/2026-07-14-six-module-control-center.{html,md}`.
|
||||
- **[#72](https://github.com/LLLin000/PaperForge/issues/72) resolved**: actionable-only maintenance inbox prototype with single-action rows, inline issue-draft review, local redacted export, and confirmation-first report flow. Independent Critical/Important PASS review. Design decisions recorded in `docs/prototypes/2026-07-14-maintenance-issue-reporting.{html,md}`.
|
||||
|
||||
## Verification status
|
||||
|
||||
- Focused Python OCR paths: **99 passed, 1 Windows SIGINT test skipped, 1 unrelated empty-result regression deselected**.
|
||||
|
|
@ -25,13 +28,17 @@
|
|||
- Maintenance regression tests: **19/19 passed** (canonical action routing, confirmation gate, cache manifest preservation).
|
||||
- Live Obsidian verification: PaperForge 1.5.15 loaded without captured errors; maintenance rendered **734 All** rows and **700 Recommended** rows from the canonical backend flag.
|
||||
- Live progress-state harness showed the floating progress bar, current key, Stop control, and disabled row actions.
|
||||
- Prototype #71 (control center): **Critical PASS (5/5), Important PASS (11/11)** — independent reviewer dimensions confirmed.
|
||||
- Prototype #72 (maintenance inbox): **Critical PASS (4/4), Important PASS (6/6)** — independent reviewer dimensions confirmed.
|
||||
- Both prototypes browser-verified at 768px viewport with scenario-switching, action-button interactions, expand/collapse diagnostics, and issue-draft flow.
|
||||
- No production plugin code was modified during prototype work.
|
||||
- The repository-wide Python suite remains blocked during collection by the pre-existing `test_pr9a_resume_rebuild.py` import of removed `_assert_collections_healthy`.
|
||||
|
||||
## Frontier
|
||||
|
||||
- [ ] Prototype the six-module control center ([#71](https://github.com/LLLin000/PaperForge/issues/71)) — design information hierarchy and interaction model for 安装 / 文献库 / OCR / 记忆 / 维护 / 帮助.
|
||||
- [ ] Design actionable-only maintenance inbox with user-reviewed GitHub Issue draft reporting ([#72](https://github.com/LLLin000/PaperForge/issues/72)).
|
||||
- [ ] Integrate the accepted model ([#73](https://github.com/LLLin000/PaperForge/issues/73)).
|
||||
- [x] Prototype the six-module control center ([#71](https://github.com/LLLin000/PaperForge/issues/71)) — design information hierarchy and interaction model for 安装 / 文献库 / OCR / 记忆 / 维护 / 帮助.
|
||||
- [x] Design actionable-only maintenance inbox with user-reviewed GitHub Issue draft reporting ([#72](https://github.com/LLLin000/PaperForge/issues/72)).
|
||||
- [ ] Integrate the accepted prototype model into a production control-center plugin ([#73](https://github.com/LLLin000/PaperForge/issues/73)).
|
||||
- [ ] Keep routine OCR quality outside maintenance; successful updates leave the queue, while unacceptable results use a user-reviewed GitHub Issue draft.
|
||||
|
||||
## Deferred
|
||||
|
|
|
|||
Loading…
Reference in a new issue