diff --git a/docs/design-version-history-panel.excalidraw b/docs/design-version-history-panel.excalidraw new file mode 100644 index 00000000..a98e0883 --- /dev/null +++ b/docs/design-version-history-panel.excalidraw @@ -0,0 +1,910 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "claude-code", + "elements": [ + { + "id": "panel_bg", + "type": "rectangle", + "x": 0, "y": 0, + "width": 700, "height": 520, + "angle": 0, + "strokeColor": "#475569", + "backgroundColor": "#f1f5f9", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100001, + "boundElements": null + }, + { + "id": "title", + "type": "text", + "x": 20, "y": 16, + "width": 280, + "height": 30, + "angle": 0, + "strokeColor": "#1e293b", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100002, + "fontSize": 24, + "fontFamily": 2, + "text": "Version History", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null + }, + { + "id": "paper_count", + "type": "text", + "x": 280, "y": 24, + "width": 100, + "height": 20, + "angle": 0, + "strokeColor": "#64748b", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100003, + "fontSize": 14, + "fontFamily": 2, + "text": "(12 papers)", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null + }, + { + "id": "back_btn", + "type": "rectangle", + "x": 620, "y": 12, + "width": 60, "height": 28, + "angle": 0, + "strokeColor": "#475569", + "backgroundColor": "#e0f2fe", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100004, + "boundElements": [ + { "id": "back_label", "type": "text" } + ] + }, + { + "id": "back_label", + "type": "text", + "x": 632, "y": 16, + "width": 36, + "height": 20, + "angle": 0, + "strokeColor": "#334155", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100005, + "fontSize": 14, + "fontFamily": 2, + "text": "Back", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "back_btn" + }, + { + "id": "left_panel_bg", + "type": "rectangle", + "x": 12, "y": 52, + "width": 200, "height": 420, + "angle": 0, + "strokeColor": "#475569", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100010, + "boundElements": null + }, + { + "id": "filter_label", + "type": "text", + "x": 24, "y": 60, + "width": 60, + "height": 18, + "angle": 0, + "strokeColor": "#64748b", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100011, + "fontSize": 12, + "fontFamily": 2, + "text": "Filter", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null + }, + { + "id": "filter_box", + "type": "rectangle", + "x": 24, "y": 82, + "width": 176, "height": 24, + "angle": 0, + "strokeColor": "#cbd5e1", + "backgroundColor": "#f8fafc", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100012, + "boundElements": null + }, + { + "id": "filter_placeholder", + "type": "text", + "x": 30, "y": 85, + "width": 120, + "height": 16, + "angle": 0, + "strokeColor": "#94a3b8", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100013, + "fontSize": 12, + "fontFamily": 2, + "text": "Search papers...", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null + }, + { + "id": "paper_item_1_bg", + "type": "rectangle", + "x": 14, "y": 116, + "width": 196, "height": 60, + "angle": 0, + "strokeColor": "#dbeafe", + "backgroundColor": "#eff6ff", + "fillStyle": "solid", + "strokeWidth": 2, + "roughness": 0, + "opacity": 100, + "seed": 100020, + "boundElements": null + }, + { + "id": "paper_item_1_title", + "type": "text", + "x": 20, "y": 120, + "width": 180, + "height": 16, + "angle": 0, + "strokeColor": "#1e293b", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100021, + "fontSize": 12, + "fontFamily": 2, + "text": "Glueck_2005_Osteolysis", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null + }, + { + "id": "paper_item_1_versions", + "type": "text", + "x": 20, "y": 140, + "width": 180, + "height": 14, + "angle": 0, + "strokeColor": "#64748b", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100022, + "fontSize": 11, + "fontFamily": 2, + "text": "v1 \u2192 v2 \u2192 v3 \u25cf 2.1 MB", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null + }, + { + "id": "paper_item_1_badge", + "type": "ellipse", + "x": 190, "y": 122, + "width": 16, "height": 16, + "angle": 0, + "strokeColor": "#dc2626", + "backgroundColor": "#fecaca", + "fillStyle": "solid", + "strokeWidth": 2, + "roughness": 0, + "opacity": 100, + "seed": 100023, + "boundElements": null + }, + { + "id": "paper_item_2_bg", + "type": "rectangle", + "x": 14, "y": 180, + "width": 196, "height": 60, + "angle": 0, + "strokeColor": "#e2e8f0", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100024, + "boundElements": null + }, + { + "id": "paper_item_2_title", + "type": "text", + "x": 20, "y": 184, + "width": 180, + "height": 16, + "angle": 0, + "strokeColor": "#334155", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100025, + "fontSize": 12, + "fontFamily": 2, + "text": "Gao_2020_Bioelectronic", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null + }, + { + "id": "paper_item_2_versions", + "type": "text", + "x": 20, "y": 204, + "width": 180, + "height": 14, + "angle": 0, + "strokeColor": "#64748b", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100026, + "fontSize": 11, + "fontFamily": 2, + "text": "v1 \u25cf 0.8 MB", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null + }, + { + "id": "paper_item_3_bg", + "type": "rectangle", + "x": 14, "y": 244, + "width": 196, "height": 60, + "angle": 0, + "strokeColor": "#e2e8f0", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100027, + "boundElements": null + }, + { + "id": "paper_item_3_title", + "type": "text", + "x": 20, "y": 248, + "width": 180, + "height": 16, + "angle": 0, + "strokeColor": "#334155", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100028, + "fontSize": 12, + "fontFamily": 2, + "text": "Lyu_AI_VTE_Prevention", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null + }, + { + "id": "paper_item_3_versions", + "type": "text", + "x": 20, "y": 268, + "width": 180, + "height": 14, + "angle": 0, + "strokeColor": "#64748b", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100029, + "fontSize": 11, + "fontFamily": 2, + "text": "v1 \u2192 v2 \u25cf 1.5 MB", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null + }, + { + "id": "right_panel_bg", + "type": "rectangle", + "x": 220, "y": 52, + "width": 468, "height": 420, + "angle": 0, + "strokeColor": "#475569", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100030, + "boundElements": null + }, + { + "id": "detail_title", + "type": "text", + "x": 232, "y": 60, + "width": 440, + "height": 20, + "angle": 0, + "strokeColor": "#1e293b", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100031, + "fontSize": 16, + "fontFamily": 2, + "text": "Glueck_2005_Osteolysis_CaseReport", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null + }, + { + "id": "timeline_line", + "type": "line", + "x": 252, "y": 108, + "width": 0, "height": 240, + "angle": 0, + "strokeColor": "#cbd5e1", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "roughness": 0, + "opacity": 100, + "seed": 100032, + "points": [[0, 0], [0, 240]], + "boundElements": null + }, + { + "id": "timeline_dot_v3", + "type": "ellipse", + "x": 246, "y": 108, + "width": 12, "height": 12, + "angle": 0, + "strokeColor": "#166534", + "backgroundColor": "#bbf7d0", + "fillStyle": "solid", + "strokeWidth": 2, + "roughness": 0, + "opacity": 100, + "seed": 100040, + "boundElements": null + }, + { + "id": "timeline_label_v3", + "type": "text", + "x": 272, "y": 106, + "width": 400, + "height": 16, + "angle": 0, + "strokeColor": "#166534", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100041, + "fontSize": 13, + "fontFamily": 2, + "text": "v3 (current) \u2014 2026-07-10 10:00 \u2014 43KB \u2014 renderer 2.1.0", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null + }, + { + "id": "timeline_dot_v2", + "type": "ellipse", + "x": 246, "y": 170, + "width": 12, "height": 12, + "angle": 0, + "strokeColor": "#475569", + "backgroundColor": "#e2e8f0", + "fillStyle": "solid", + "strokeWidth": 2, + "roughness": 0, + "opacity": 100, + "seed": 100042, + "boundElements": null + }, + { + "id": "timeline_label_v2", + "type": "text", + "x": 272, "y": 168, + "width": 400, + "height": 16, + "angle": 0, + "strokeColor": "#334155", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100043, + "fontSize": 13, + "fontFamily": 2, + "text": "v2 (rebuild) \u2014 2026-07-09 14:30 \u2014 42KB \u2014 renderer 2.0.0", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null + }, + { + "id": "restore_v2_btn", + "type": "rectangle", + "x": 280, "y": 190, + "width": 64, "height": 22, + "angle": 0, + "strokeColor": "#1e40af", + "backgroundColor": "#dbeafe", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100044, + "boundElements": [ + { "id": "restore_v2_label", "type": "text" } + ] + }, + { + "id": "restore_v2_label", + "type": "text", + "x": 290, "y": 192, + "width": 44, + "height": 16, + "angle": 0, + "strokeColor": "#334155", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100045, + "fontSize": 11, + "fontFamily": 2, + "text": "Restore", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "restore_v2_btn" + }, + { + "id": "compare_v2_btn", + "type": "rectangle", + "x": 350, "y": 190, + "width": 64, "height": 22, + "angle": 0, + "strokeColor": "#475569", + "backgroundColor": "#f1f5f9", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100046, + "boundElements": [ + { "id": "compare_v2_label", "type": "text" } + ] + }, + { + "id": "compare_v2_label", + "type": "text", + "x": 358, "y": 192, + "width": 48, + "height": 16, + "angle": 0, + "strokeColor": "#334155", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100047, + "fontSize": 11, + "fontFamily": 2, + "text": "Compare", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "compare_v2_btn" + }, + { + "id": "timeline_dot_v1", + "type": "ellipse", + "x": 246, "y": 232, + "width": 12, "height": 12, + "angle": 0, + "strokeColor": "#475569", + "backgroundColor": "#e2e8f0", + "fillStyle": "solid", + "strokeWidth": 2, + "roughness": 0, + "opacity": 100, + "seed": 100048, + "boundElements": null + }, + { + "id": "timeline_label_v1", + "type": "text", + "x": 272, "y": 230, + "width": 400, + "height": 16, + "angle": 0, + "strokeColor": "#64748b", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100049, + "fontSize": 13, + "fontFamily": 2, + "text": "v1 (original OCR) \u2014 2026-06-01 09:00 \u2014 38KB", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null + }, + { + "id": "restore_v1_btn", + "type": "rectangle", + "x": 280, "y": 252, + "width": 64, "height": 22, + "angle": 0, + "strokeColor": "#1e40af", + "backgroundColor": "#dbeafe", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100050, + "boundElements": [ + { "id": "restore_v1_label", "type": "text" } + ] + }, + { + "id": "restore_v1_label", + "type": "text", + "x": 290, "y": 254, + "width": 44, + "height": 16, + "angle": 0, + "strokeColor": "#334155", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100051, + "fontSize": 11, + "fontFamily": 2, + "text": "Restore", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "restore_v1_btn" + }, + { + "id": "compare_v1_btn", + "type": "rectangle", + "x": 350, "y": 252, + "width": 64, "height": 22, + "angle": 0, + "strokeColor": "#475569", + "backgroundColor": "#f1f5f9", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100052, + "boundElements": [ + { "id": "compare_v1_label", "type": "text" } + ] + }, + { + "id": "compare_v1_label", + "type": "text", + "x": 358, "y": 254, + "width": 48, + "height": 16, + "angle": 0, + "strokeColor": "#334155", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100053, + "fontSize": 11, + "fontFamily": 2, + "text": "Compare", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "compare_v1_btn" + }, + { + "id": "compare_preview_section", + "type": "rectangle", + "x": 232, "y": 290, + "width": 444, "height": 120, + "angle": 0, + "strokeColor": "#e2e8f0", + "backgroundColor": "#f8fafc", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100060, + "boundElements": null + }, + { + "id": "compare_preview_title", + "type": "text", + "x": 240, "y": 296, + "width": 200, + "height": 16, + "angle": 0, + "strokeColor": "#334155", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100061, + "fontSize": 12, + "fontFamily": 2, + "text": "v2 vs current \u2014 2 paragraphs changed", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null + }, + { + "id": "diff_item_1", + "type": "rectangle", + "x": 240, "y": 318, + "width": 428, "height": 18, + "angle": 0, + "strokeColor": "#fef9c3", + "backgroundColor": "#fefce8", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100062, + "boundElements": null + }, + { + "id": "diff_item_1_text", + "type": "text", + "x": 246, "y": 319, + "width": 400, + "height": 14, + "angle": 0, + "strokeColor": "#854d0e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100063, + "fontSize": 11, + "fontFamily": 2, + "text": "Introduction: background paragraph restructured", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null + }, + { + "id": "diff_item_2", + "type": "rectangle", + "x": 240, "y": 340, + "width": 428, "height": 18, + "angle": 0, + "strokeColor": "#dcfce7", + "backgroundColor": "#f0fdf4", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100064, + "boundElements": null + }, + { + "id": "diff_item_2_text", + "type": "text", + "x": 246, "y": 341, + "width": 400, + "height": 14, + "angle": 0, + "strokeColor": "#166534", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100065, + "fontSize": 11, + "fontFamily": 2, + "text": "Methods: statistical paragraph added", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null + }, + { + "id": "diff_item_3", + "type": "rectangle", + "x": 240, "y": 362, + "width": 428, "height": 18, + "angle": 0, + "strokeColor": "#e2e8f0", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100066, + "boundElements": null + }, + { + "id": "diff_item_3_text", + "type": "text", + "x": 246, "y": 363, + "width": 400, + "height": 14, + "angle": 0, + "strokeColor": "#64748b", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100067, + "fontSize": 11, + "fontFamily": 2, + "text": "Results: unchanged (2/3 tables reformatted)", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null + }, + { + "id": "action_bar_bg", + "type": "rectangle", + "x": 12, "y": 478, + "width": 676, "height": 36, + "angle": 0, + "strokeColor": "#e2e8f0", + "backgroundColor": "#f8fafc", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100070, + "boundElements": null + }, + { + "id": "restore_all_btn", + "type": "rectangle", + "x": 22, "y": 482, + "width": 120, "height": 28, + "angle": 0, + "strokeColor": "#1e40af", + "backgroundColor": "#dbeafe", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100071, + "boundElements": [ + { "id": "restore_all_label", "type": "text" } + ] + }, + { + "id": "restore_all_label", + "type": "text", + "x": 28, "y": 488, + "width": 108, + "height": 16, + "angle": 0, + "strokeColor": "#334155", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100072, + "fontSize": 11, + "fontFamily": 2, + "text": "Restore selected", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "restore_all_btn" + }, + { + "id": "clear_btn", + "type": "rectangle", + "x": 150, "y": 482, + "width": 180, "height": 28, + "angle": 0, + "strokeColor": "#475569", + "backgroundColor": "#f1f5f9", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100073, + "boundElements": [ + { "id": "clear_label", "type": "text" } + ] + }, + { + "id": "clear_label", + "type": "text", + "x": 160, "y": 488, + "width": 160, + "height": 16, + "angle": 0, + "strokeColor": "#334155", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "roughness": 0, + "opacity": 100, + "seed": 100074, + "fontSize": 11, + "fontFamily": 2, + "text": "Clear old versions (free 4.4 MB)", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "clear_btn" + } + ], + "appState": { + "viewBackgroundColor": "#ffffff" + } +} diff --git a/docs/design-version-history-panel.svg b/docs/design-version-history-panel.svg new file mode 100644 index 00000000..d31e07ec --- /dev/null +++ b/docs/design-version-history-panel.svg @@ -0,0 +1,2 @@ +Version History(12 papers)BackFilterSearch papers...Glueck_2005_Osteolysisv1 → v2 → v3 ● 2.1 MBGao_2020_Bioelectronicv1 ● 0.8 MBLyu_AI_VTE_Preventionv1 → v2 ● 1.5 MBGlueck_2005_Osteolysis_CaseReportv3 (current) — 2026-07-10 10:00 — 43KB — renderer 2.1.0v2 (rebuild) — 2026-07-09 14:30 — 42KB — renderer 2.0.0RestoreComparev1 (original OCR) — 2026-06-01 09:00 — 38KBRestoreComparev2 vs current — 2 paragraphs changedIntroduction: background paragraph restructuredMethods: statistical paragraph addedResults: unchanged (2/3 tables reformatted)Restore selectedClear old versions (free 4.4 MB) \ No newline at end of file diff --git a/paperforge/cli.py b/paperforge/cli.py index 34953528..b323abe7 100644 --- a/paperforge/cli.py +++ b/paperforge/cli.py @@ -302,6 +302,7 @@ def build_parser() -> argparse.ArgumentParser: p_retrieve.add_argument("query", help="Search query") p_retrieve.add_argument("--json", action="store_true") p_retrieve.add_argument("--limit", type=int, default=5) + p_retrieve.add_argument("--deep", action="store_true", help="Enable @ Deep Search mode with query rewrite + hybrid retrieval (BM25 + vec0)") p_retrieve.add_argument("--expand", action="store_true", default=True) p_qp = sub.add_parser("query-plan", help="Classify a literature query and recommend the first retrieval command") diff --git a/paperforge/commands/retrieve.py b/paperforge/commands/retrieve.py index fb00f37b..f162c188 100644 --- a/paperforge/commands/retrieve.py +++ b/paperforge/commands/retrieve.py @@ -6,7 +6,7 @@ import sys from paperforge import __version__ as PF_VERSION from paperforge.core.errors import ErrorCode from paperforge.core.result import PFError, PFResult -from paperforge.embedding import merge_retrieve, retrieve_chunks +from paperforge.embedding import hybrid_search, merge_retrieve, retrieve_chunks from paperforge.memory.db import get_connection, get_memory_db_path from paperforge.query_planning import build_query_plan, enrich_query_plan_with_runtime @@ -43,8 +43,49 @@ def run(args: argparse.Namespace) -> int: vault = args.vault_path query = args.query limit = args.limit or 5 + deep = getattr(args, "deep", False) - # Check if vector index exists + # ── @ Deep Search mode: query rewrite + hybrid retrieval ────── + if deep: + try: + chunks = hybrid_search(vault, query, limit=limit) + except Exception as e: + result = PFResult( + ok=False, + command="retrieve", + version=PF_VERSION, + error=PFError(code=ErrorCode.INTERNAL_ERROR, message=str(e)), + ) + print(result.to_json() if args.json else result.error.message, file=sys.stderr if not args.json else sys.stdout) + return 1 + + data = { + "query": query, + "chunks": chunks, + "count": len(chunks), + "deep": True, + "route_explanation": { + "primary_arm": "deep_search", + "query_rewrite": True, + "hybrid": True, + }, + } + warnings: list[str] = [] + next_actions: list[dict] = [] + if len(chunks) == 0: + warnings.append("Deep search returned no results for the query.") + result = PFResult( + ok=True, command="retrieve", version=PF_VERSION, data=data, warnings=warnings, next_actions=next_actions + ) + if args.json: + print(result.to_json()) + else: + print(f"{len(chunks)} deep search results for: {query}") + for c in chunks: + print(f" [{c.get('source', '?')}] {c.get('title', c.get('paper_id', '?'))} ({c.get('year', '?')}): {c.get('text', '')[:80]}...") + return 0 + + # ── Standard vector retrieve ────────────────────────────────── from paperforge.embedding import get_embed_status status = get_embed_status(vault) diff --git a/paperforge/embedding/__init__.py b/paperforge/embedding/__init__.py index ebaf5a8a..cc7fbd8b 100644 --- a/paperforge/embedding/__init__.py +++ b/paperforge/embedding/__init__.py @@ -19,7 +19,7 @@ from paperforge.embedding.build_state import ( ) from paperforge.embedding.builder import embed_body_units, embed_paper, get_body_units_for_embedding, get_object_units_for_embedding, embed_object_units from paperforge.embedding.preflight import _preflight_check -from paperforge.embedding.search import merge_retrieve, retrieve_chunks +from paperforge.embedding.search import hybrid_search, merge_retrieve, retrieve_chunks from paperforge.embedding.status import get_embed_status __all__ = [ @@ -37,8 +37,8 @@ __all__ = [ "get_vector_build_state_path", "get_vector_db_path", "mark_vector_build_state", + "hybrid_search", "merge_retrieve", - "migrate_chroma_to_vec0", "read_vector_build_state", "retrieve_chunks", "write_vector_build_state", diff --git a/paperforge/embedding/query_rewrite.py b/paperforge/embedding/query_rewrite.py new file mode 100644 index 00000000..56ccfb56 --- /dev/null +++ b/paperforge/embedding/query_rewrite.py @@ -0,0 +1,77 @@ +"""Query expansion for @ Deep Search — abbreviation expansion and synonym mapping.""" + +from __future__ import annotations + +import re + +# Common orthopedic/medical abbreviations used in literature +ABBREVIATIONS: dict[str, str] = { + "ACL": "anterior cruciate ligament", + "PCL": "posterior cruciate ligament", + "MCL": "medial collateral ligament", + "LCL": "lateral collateral ligament", + "VTE": "venous thromboembolism", + "DVT": "deep vein thrombosis", + "RCT": "rotator cuff", + "OA": "osteoarthritis", + "THA": "total hip arthroplasty", + "TKA": "total knee arthroplasty", + "ROM": "range of motion", + "BMI": "body mass index", + "NSAID": "nonsteroidal anti-inflammatory drug", + "RSA": "reverse shoulder arthroplasty", + "TSA": "total shoulder arthroplasty", + "ORIF": "open reduction internal fixation", + "TFCC": "triangular fibrocartilage complex", + "CCI": "charlson comorbidity index", + "ASA": "american society of anesthesiologists", + "PROM": "patient reported outcome measure", + "PROMIS": "patient reported outcomes measurement information system", +} + +MEDICAL_SYNONYMS: dict[str, list[str]] = { + "knee": ["knee joint", "genu"], + "shoulder": ["glenohumeral joint", "shoulder joint"], + "hip": ["hip joint", "acetabulofemoral joint"], + "elbow": ["elbow joint"], + "ankle": ["ankle joint", "talocrural joint"], + "wrist": ["wrist joint", "radiocarpal joint"], + "spine": ["vertebral column", "spinal column"], + "fracture": ["fracture", "break"], + "repair": ["repair", "reconstruction", "fixation"], + "replacement": ["arthroplasty", "replacement"], + "complication": ["complication", "adverse event", "comorbidity"], + "pain": ["pain", "analgesia", "pain management"], +} +def expand_query(query: str) -> list[str]: + """Expand query by replacing known abbreviations with full forms. + + Returns a list of query variants with the original query first, + followed by expanded versions (one per abbreviation expanded). + """ + variants = [query] + seen: set[str] = set() + + # Try expanding all abbreviations + expanded = query + for abbr, full in ABBREVIATIONS.items(): + pattern = re.compile(r"\b" + re.escape(abbr) + r"\b", re.IGNORECASE) + if pattern.search(query): + candidate = pattern.sub(full, expanded) + if candidate != expanded and candidate not in seen: + seen.add(candidate) + variants.append(candidate) + + # If no abbreviation was expanded, try synonym expansion + if len(variants) == 1: + for term, synonyms in MEDICAL_SYNONYMS.items(): + pattern = re.compile(r"\b" + re.escape(term) + r"\b", re.IGNORECASE) + if pattern.search(query): + for syn in synonyms: + candidate = pattern.sub(syn, query) + if candidate != query and candidate not in seen: + seen.add(candidate) + variants.append(candidate) + break # one synonym expansion is enough + + return variants diff --git a/paperforge/embedding/search.py b/paperforge/embedding/search.py index 03462f69..6e3d47fb 100644 --- a/paperforge/embedding/search.py +++ b/paperforge/embedding/search.py @@ -125,3 +125,232 @@ def merge_retrieve(vault: Path, query: str, limit: int = 5, expand: bool = True) return merged finally: conn.close() + + +def hybrid_search(vault: Path, query: str, limit: int = 10) -> list[dict]: + """Hybrid search: BM25 FTS5 + vec0 k-NN with query rewrite. + + Runs on body_units_fts (BM25) and vec_body/vec_objects (k-NN), + fuses scores (0.3 BM25 + 0.7 vec), and returns deduplicated results + with text snippets and source types. + + Falls back to BM25-only when vec0 tables are unavailable or empty. + """ + logger = logging.getLogger(__name__) + from paperforge.embedding.query_rewrite import expand_query as do_expand + + query_variants = do_expand(query) + db_path = get_memory_db_path(vault) + conn = get_connection(db_path, read_only=True) + try: + ensure_vec_extension(conn) + + # ── Step 2: BM25 FTS5 ────────────────────────────────────────── + bm25_results: list[dict] = _bm25_search(conn, query_variants, limit * 2) + + # ── Step 3: vec0 k-NN ───────────────────────────────────────── + vec_results: list[dict] = _vec_search(conn, vault, query, limit * 2) + + # ── Step 4: Score fusion ──────────────────────────────────────── + fused = _fuse_results(bm25_results, vec_results, limit) + + return fused + finally: + conn.close() + + +def _bm25_search( + conn: sqlite3.Connection, query_variants: list[str], limit: int +) -> list[dict]: + """Run BM25 (FTS5) search across body_units_fts for each query variant. + + Returns results with BM25 scores normalized to [0, 1]. + """ + seen: set[str] = set() + results: list[dict] = [] + + for qv in query_variants: + try: + rows = conn.execute( + """SELECT + bu.unit_id, + bu.paper_id, + bu.section_path, + bu.section_title, + bu.unit_text, + p.title, + p.first_author, + p.year, + p.journal, + p.domain, + rank as bm25_raw + FROM body_units_fts bu_fts + JOIN body_units bu ON bu_fts.unit_id = bu.unit_id + JOIN papers p ON bu.paper_id = p.zotero_key + WHERE body_units_fts MATCH ? + ORDER BY rank + LIMIT ?""", + (qv, limit), + ).fetchall() + except Exception as exc: + logger = logging.getLogger(__name__) + logger.warning("BM25 query failed for %r: %s", qv, exc) + continue + + for row in rows: + key = (row["unit_id"], row["paper_id"]) + if key in seen: + continue + seen.add(key) + + bm25_raw = float(row["bm25_raw"]) + # Normalize: invert sign so positive = better, then squash to [0, 1] + bm25_norm = 1.0 - (1.0 / (1.0 + abs(bm25_raw))) if bm25_raw != 0 else 0.0 + + results.append( + { + "unit_id": row["unit_id"], + "paper_id": row["paper_id"], + "title": row["title"], + "first_author": row["first_author"], + "year": row["year"], + "journal": row["journal"], + "domain": row["domain"], + "source": "body_unit", + "text": row["unit_text"], + "heading": row["section_title"], + "bm25_score": round(bm25_norm, 4), + "vec_score": 0.0, + } + ) + + results.sort(key=lambda r: r["bm25_score"], reverse=True) + return results[:limit] + + +def _vec_search( + conn: sqlite3.Connection, vault: Path, query: str, limit: int +) -> list[dict]: + """Run vec0 k-NN on vec_body and vec_objects. + + Gracefully returns empty list when vec0 extension or tables are missing. + """ + logger = logging.getLogger(__name__) + provider = OpenAICompatibleProvider(vault) + q_emb = provider.encode_single(query) + q_emb_json = json.dumps(q_emb) + + results: list[dict] = [] + for vec_table, source in _VEC_SOURCE_MAP.items(): + if vec_table not in ("vec_body", "vec_objects"): + continue + meta_table = _VEC_META_MAP[vec_table] + try: + rows = conn.execute( + f"""SELECT m.paper_id, m.chunk_index, m.text, v.distance + FROM {vec_table} v + JOIN {meta_table} m ON v.rowid = m.rowid + WHERE v.embedding MATCH ? AND v.k = ?""", + (q_emb_json, limit), + ).fetchall() + except Exception as exc: + logger.warning("vec0 query failed for %s: %s", vec_table, exc) + continue + + for row in rows: + vec_sim = round(1.0 - row[3], 4) # convert distance to similarity + results.append( + { + "paper_id": row[0], + "source": source, + "text": row[2] or "", + "vec_score": vec_sim, + } + ) + + return results + + +def _fuse_results( + bm25_results: list[dict], vec_results: list[dict], limit: int +) -> list[dict]: + """Fuse BM25 and vec0 results with combined score. + + Fusion formula: combined = 0.3 * bm25_norm + 0.7 * vec_norm. + When vec0 is unavailable, uses BM25 scores directly. + + Returns list deduplicated by (paper_id, text) with combined score. + """ + # Build vec lookup: (paper_id, text) -> vec_score + vec_lookup: dict[tuple[str, str], float] = {} + for vr in vec_results: + key = (vr["paper_id"], vr["text"]) + # Keep the highest vec score per unique paper_id+text + if key not in vec_lookup or vr["vec_score"] > vec_lookup[key]: + vec_lookup[key] = vr["vec_score"] + + has_vec = bool(vec_results) + + for br in bm25_results: + bm25_norm = br["bm25_score"] + key = (br["paper_id"], br.get("text", "")) + + if has_vec and key in vec_lookup: + vec_raw = vec_lookup[key] + # Normalize vec score similarly + vec_norm = 1.0 - (1.0 / (1.0 + vec_raw)) if vec_raw > 0 else 0.0 + br["vec_score"] = round(vec_raw, 4) + combined = 0.3 * bm25_norm + 0.7 * vec_norm + br["score"] = round(combined, 4) + else: + # No vec data for this item — use BM25 alone + br["vec_score"] = 0.0 + br["score"] = round(bm25_norm, 4) + + # Sort by combined score descending + bm25_results.sort(key=lambda r: r["score"], reverse=True) + + # Final dedup and cap + seen: set[tuple[str, str]] = set() + out: list[dict] = [] + for r in bm25_results: + key = (r["paper_id"], r.get("text", "")) + if key in seen: + continue + seen.add(key) + # Include matched terms hint + r["matched_terms"] = "" + out.append(r) + if len(out) >= limit: + break + + # If BM25 was empty but vec had results, synthesize entries + if not out and has_vec: + seen_text: set[tuple[str, str]] = set() + for vr in vec_results: + key = (vr["paper_id"], vr.get("text", "")) + if key in seen_text: + continue + seen_text.add(key) + vec_raw = vr["vec_score"] + vec_norm = 1.0 - (1.0 / (1.0 + vec_raw)) if vec_raw > 0 else 0.0 + out.append( + { + "paper_id": vr["paper_id"], + "source": vr["source"], + "text": vr.get("text", ""), + "vec_score": round(vec_raw, 4), + "score": round(vec_norm, 4), + "matched_terms": "", + "heading": "", + "title": "", + "first_author": "", + "year": "", + "journal": "", + "domain": "", + } + ) + if len(out) >= limit: + break + + return out diff --git a/paperforge/plugin/main.js b/paperforge/plugin/main.js index e9323f0f..81273183 100644 --- a/paperforge/plugin/main.js +++ b/paperforge/plugin/main.js @@ -1,37 +1,37 @@ "use strict"; -var Et = Object.create; -var Be = Object.defineProperty; -var kt = Object.getOwnPropertyDescriptor; -var wt = Object.getOwnPropertyNames; -var St = Object.getPrototypeOf, - Pt = Object.prototype.hasOwnProperty; -var Ct = (g, h) => () => (h || g((h = { exports: {} }).exports, h), h.exports), - Ft = (g, h) => { - for (var e in h) Be(g, e, { get: h[e], enumerable: !0 }); +var Rt = Object.create; +var Me = Object.defineProperty; +var Tt = Object.getOwnPropertyDescriptor; +var Dt = Object.getOwnPropertyNames; +var Bt = Object.getPrototypeOf, + At = Object.prototype.hasOwnProperty; +var Ot = (f, u) => () => (u || f((u = { exports: {} }).exports, u), u.exports), + Mt = (f, u) => { + for (var e in u) Me(f, e, { get: u[e], enumerable: !0 }); }, - Ye = (g, h, e, t) => { - if ((h && typeof h == "object") || typeof h == "function") - for (let r of wt(h)) - !Pt.call(g, r) && + rt = (f, u, e, t) => { + if ((u && typeof u == "object") || typeof u == "function") + for (let r of Dt(u)) + !At.call(f, r) && r !== e && - Be(g, r, { - get: () => h[r], - enumerable: !(t = kt(h, r)) || t.enumerable, + Me(f, r, { + get: () => u[r], + enumerable: !(t = Tt(u, r)) || t.enumerable, }); - return g; + return f; }; -var H = (g, h, e) => ( - (e = g != null ? Et(St(g)) : {}), - Ye( - h || !g || !g.__esModule - ? Be(e, "default", { value: g, enumerable: !0 }) +var $ = (f, u, e) => ( + (e = f != null ? Rt(Bt(f)) : {}), + rt( + u || !f || !f.__esModule + ? Me(e, "default", { value: f, enumerable: !0 }) : e, - g + f ) ), - Rt = (g) => Ye(Be({}, "__esModule", { value: !0 }), g); -var Ke = Ct((Nt, Dt) => { - Dt.exports = { + Lt = (f) => rt(Me({}, "__esModule", { value: !0 }), f); +var qe = Ot((qt, Nt) => { + Nt.exports = { versions: [ { version: "1.5.15", @@ -63,18 +63,18 @@ var Ke = Ct((Nt, Dt) => { ], }; }); -var Ot = {}; -Ft(Ot, { default: () => $e }); -module.exports = Rt(Ot); -var V = require("obsidian"), - $ = H(require("fs")), - xe = H(require("path")), - ae = require("child_process"); -var ge = "paperforge-status", - Se = "paperforge", - Qe = +var Vt = {}; +Mt(Vt, { default: () => He }); +module.exports = Lt(Vt); +var K = require("obsidian"), + H = $(require("fs")), + ke = $(require("path")), + ie = require("child_process"); +var _e = "paperforge-status", + Ce = "paperforge", + st = '', - G = [ + Y = [ { id: "paperforge-sync", title: "Sync Library", @@ -118,7 +118,7 @@ var ge = "paperforge-status", okMsg: "OCR redo started", }, ], - Pe = { + Fe = { vault_path: "", setup_complete: !1, auto_update: !0, @@ -140,14 +140,14 @@ var ge = "paperforge-status", base_dir: "", last_seen_version: "", }; -function et(g, h) { - if (!h || !h.note_path) return h; - let e = g.vault.getAbstractFileByPath(h.note_path); - if (!e) return h; - let t = g.metadataCache.getFileCache(e), +function nt(f, u) { + if (!u || !u.note_path) return u; + let e = f.vault.getAbstractFileByPath(u.note_path); + if (!e) return u; + let t = f.metadataCache.getFileCache(e), r = t && t.frontmatter; - if (!r) return h; - let n = { ...h }; + if (!r) return u; + let n = { ...u }; for (let a of [ "do_ocr", "analyze", @@ -158,10 +158,10 @@ function et(g, h) { Object.prototype.hasOwnProperty.call(r, a) && (n[a] = r[a]); return n; } -function je(g, h) { - return g && { ...g, ...h }; +function Ke(f, u) { + return f && { ...f, ...u }; } -var He = { +var We = { en: { action_running: "Running ", api_key_missing: "Missing", @@ -432,6 +432,21 @@ var He = { maintenance_refresh_spinning: "Updating\u2026", maintenance_all_good: "\u2705 All good \u2014 no action needed", maintenance_n_pending: "{n} need attention", + version_panel_title: "Version History", + version_panel_back: "Back", + version_filter_placeholder: "Filter papers...", + version_papers_count: "{n} papers", + version_current: "current", + version_restore_btn: "Restore", + version_compare_btn: "Compare", + version_restore_selected: "Restore selected", + version_clear_old: "Clear old versions (free {size})", + version_no_backups: "No version history available", + version_restore_confirm: "Restore {label} for {paper}?", + version_restore_done: "Restored {label}", + version_compare_title: "{vA} vs {vB}", + version_compare_paragraphs: "{n} paragraphs changed", + version_error_read: "Cannot read version data", }, zh: { action_running: "\u6B63\u5728\u6267\u884C ", @@ -718,107 +733,124 @@ var He = { maintenance_refresh_spinning: "\u6B63\u5728\u66F4\u65B0\u2026", maintenance_all_good: "\u2705 \u5168\u90E8\u6B63\u5E38", maintenance_n_pending: "{n} \u7BC7\u9700\u8981\u5904\u7406", + version_panel_title: "\u7248\u672C\u5386\u53F2", + version_panel_back: "\u8FD4\u56DE", + version_filter_placeholder: "\u641C\u7D22\u8BBA\u6587...", + version_papers_count: "{n} \u7BC7\u8BBA\u6587", + version_current: "\u5F53\u524D", + version_restore_btn: "\u6062\u590D", + version_compare_btn: "\u5BF9\u6BD4", + version_restore_selected: "\u6062\u590D\u9009\u4E2D\u7248\u672C", + version_clear_old: "\u6E05\u9664\u65E7\u7248\u672C (\u91CA\u653E {size})", + version_no_backups: + "\u6CA1\u6709\u53EF\u6062\u590D\u7684\u5386\u53F2\u7248\u672C", + version_restore_confirm: + "\u786E\u8BA4\u5C06 {paper} \u6062\u590D\u5230 {label}\uFF1F", + version_restore_done: "\u5DF2\u6062\u590D\u5230 {label}", + version_compare_title: "{vA} vs {vB}", + version_compare_paragraphs: "{n} \u6BB5\u6709\u53D8\u5316", + version_error_read: "\u65E0\u6CD5\u8BFB\u53D6\u7248\u672C\u6570\u636E", }, }, - Ve = null; -function Tt(g) { + Ze = null; +function It(f) { try { - let h = g.vault; - if (typeof h.getConfig == "function") { - let e = h.getConfig("language"); + let u = f.vault; + if (typeof u.getConfig == "function") { + let e = u.getConfig("language"); if (e && String(e).startsWith("zh")) return "zh"; } - } catch (h) {} + } catch (u) {} try { if (typeof localStorage != "undefined") { - let h = localStorage.getItem("language"); - if (h && String(h).startsWith("zh")) return "zh"; + let u = localStorage.getItem("language"); + if (u && String(u).startsWith("zh")) return "zh"; } - } catch (h) {} + } catch (u) {} return "en"; } -function tt(g) { - Ve = Tt(g) === "zh" ? He.zh : He.en; +function at(f) { + Ze = It(f) === "zh" ? We.zh : We.en; } -function l(g) { - return (Ve && Ve[g]) || He.en[g] || g; +function o(f) { + return (Ze && Ze[f]) || We.en[f] || f; } -var w = require("obsidian"), - I = H(require("fs")), - Z = H(require("path")), - bt = H(require("os")), - N = require("child_process"); -var vt = H(Ke()); -var re = H(require("fs")), - te = H(require("path")), - st = H(require("os")), - oe = require("child_process"), - Ze = null, - rt = !1; -function M(g, h, e, t) { - let r = e || re, - n = t || oe.execFileSync; - if (h && h.python_path && h.python_path.trim()) { - let o = h.python_path.trim(); - if (r.existsSync(o)) return { path: o, source: "manual", extraArgs: [] }; +var C = require("obsidian"), + N = $(require("fs")), + Z = $(require("path")), + kt = $(require("os")), + j = require("child_process"); +var wt = $(qe()); +var ne = $(require("fs")), + se = $(require("path")), + ot = $(require("os")), + le = require("child_process"), + Je = null, + it = !1; +function M(f, u, e, t) { + let r = e || ne, + n = t || le.execFileSync; + if (u && u.python_path && u.python_path.trim()) { + let l = u.python_path.trim(); + if (r.existsSync(l)) return { path: l, source: "manual", extraArgs: [] }; } let a = [ - te.join(g, ".paperforge-test-venv", "Scripts", "python.exe"), - te.join(g, ".venv", "Scripts", "python.exe"), - te.join(g, "venv", "Scripts", "python.exe"), + se.join(f, ".paperforge-test-venv", "Scripts", "python.exe"), + se.join(f, ".venv", "Scripts", "python.exe"), + se.join(f, "venv", "Scripts", "python.exe"), ]; - for (let o of a) + for (let l of a) try { - if (r.existsSync(o)) - return { path: o, source: "auto-detected", extraArgs: [] }; + if (r.existsSync(l)) + return { path: l, source: "auto-detected", extraArgs: [] }; } catch (i) {} let s = [ { path: "py", extraArgs: ["-3"] }, { path: "python", extraArgs: [] }, { path: "python3", extraArgs: [] }, ]; - for (let o of s) + for (let l of s) try { - let i = n(o.path, [...o.extraArgs, "--version"], { + let i = n(l.path, [...l.extraArgs, "--version"], { encoding: "utf-8", timeout: 5e3, windowsHide: !0, }); if (i && i.toLowerCase().includes("python")) return { - path: o.path, + path: l.path, source: "auto-detected", - extraArgs: o.extraArgs, + extraArgs: l.extraArgs, }; } catch (i) {} return { path: "python", source: "auto-detected", extraArgs: [] }; } -function nt(g, h, e, t, r) { +function lt(f, u, e, t, r) { t === void 0 && (t = 1e4); - let n = r || oe.execFile; + let n = r || le.execFile; return new Promise((a) => { n( - g, + f, ["-c", "import paperforge; print(paperforge.__version__)"], { cwd: e, timeout: t }, - (s, o) => { + (s, l) => { if (s) { a({ status: "not-installed", pyVersion: null, - pluginVersion: h, + pluginVersion: u, error: s.message, }); return; } - let i = (o && o.trim()) || null; + let i = (l && l.trim()) || null; a( - i === h - ? { status: "match", pyVersion: i, pluginVersion: h, error: null } + i === u + ? { status: "match", pyVersion: i, pluginVersion: u, error: null } : { status: "mismatch", pyVersion: i, - pluginVersion: h, + pluginVersion: u, error: null, } ); @@ -826,134 +858,134 @@ function nt(g, h, e, t, r) { ); }); } -function at(g, h, e) { +function ct(f, u, e) { e === void 0 && (e = []); - let t = `paperforge==${h}`, - r = `git+https://github.com/LLLin000/PaperForge.git@${h}`, + let t = `paperforge==${u}`, + r = `git+https://github.com/LLLin000/PaperForge.git@${u}`, n = [...e, "-m", "pip", "install", "--upgrade", t], a = [...e, "-m", "pip", "install", "--upgrade", r]; - return { cmd: g, url: r, args: a, pypiArgs: n, gitArgs: a, timeout: 12e4 }; + return { cmd: f, url: r, args: a, pypiArgs: n, gitArgs: a, timeout: 12e4 }; } -function it(g, h, e, t, r, n) { - let a = r || oe.spawn; +function pt(f, u, e, t, r, n) { + let a = r || le.spawn; return new Promise((s) => { - let o = Date.now(), + let l = Date.now(), i = { cwd: e, timeout: t, windowsHide: !0 }; n && (i.env = n); - let c = a(g, h, i), + let c = a(f, u, i), d = [], - u = []; + h = []; (c.stdout.on("data", (p) => { d.push(p.toString("utf-8")); }), c.stderr.on("data", (p) => { - u.push(p.toString("utf-8")); + h.push(p.toString("utf-8")); }), c.on("close", (p) => { s({ stdout: d.join(""), - stderr: u.join(""), + stderr: h.join(""), exitCode: p, - elapsed: Date.now() - o, + elapsed: Date.now() - l, }); }), c.on("error", (p) => { s({ stdout: d.join(""), stderr: - u.join("") + + h.join("") + ` ` + p.message, exitCode: -1, - elapsed: Date.now() - o, + elapsed: Date.now() - l, }); })); }); } -function We() { - if (rt) return Ze; - rt = !0; +function Ue() { + if (it) return Je; + it = !0; try { - let g; + let f; if (process.platform === "win32") { - let h = process.env.ComSpec || "C:\\Windows\\System32\\cmd.exe"; - g = (0, oe.execFileSync)(h, ["/c", "where", "git"], { + let u = process.env.ComSpec || "C:\\Windows\\System32\\cmd.exe"; + f = (0, le.execFileSync)(u, ["/c", "where", "git"], { timeout: 5e3, windowsHide: !0, encoding: "utf-8", }); } else - g = (0, oe.execFileSync)("which", ["git"], { + f = (0, le.execFileSync)("which", ["git"], { timeout: 5e3, encoding: "utf-8", }); - if (g) { - let h = g + if (f) { + let u = f .split( ` ` )[0] .trim(); - h && (Ze = te.dirname(h)); + u && (Je = se.dirname(u)); } - } catch (g) {} - return Ze; + } catch (f) {} + return Je; } -function fe() { - let g = { ...process.env }, - h = process.platform, - e = st.homedir(), +function me() { + let f = { ...process.env }, + u = process.platform, + e = ot.homedir(), t = [], - r = We(); + r = Ue(); (r && t.push(r), - h === "darwin" + u === "darwin" ? t.push( "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", `${e}/.local/bin` ) - : h === "linux" && + : u === "linux" && t.push("/usr/local/bin", "/usr/bin", `${e}/.local/bin`)); - let n = g.PATH || ""; - return ((g.PATH = [...t, n].filter(Boolean).join(te.delimiter)), g); + let n = f.PATH || ""; + return ((f.PATH = [...t, n].filter(Boolean).join(se.delimiter)), f); } -function ot(g) { - return String(g) +function dt(f) { + return String(f) .toLowerCase() .replace(/[^a-z0-9]/g, "") .includes("betterbibtex"); } -function qe(g) { - if (!g) return !1; +function Ge(f) { + if (!f) return !1; try { - if (!re.existsSync(g)) return !1; - for (let h of re.readdirSync(g)) if (ot(h)) return !0; - } catch (h) {} + if (!ne.existsSync(f)) return !1; + for (let u of ne.readdirSync(f)) if (dt(u)) return !0; + } catch (u) {} return !1; } -function Ae(g) { - if (!g) return !1; +function Le(f) { + if (!f) return !1; try { - if (!re.existsSync(g)) return !1; - for (let h of re.readdirSync(g)) { - let e = te.join(g, h, "extensions"); + if (!ne.existsSync(f)) return !1; + for (let u of ne.readdirSync(f)) { + let e = se.join(f, u, "extensions"); try { - if (!re.existsSync(e)) continue; - for (let t of re.readdirSync(e)) if (ot(t)) return !0; + if (!ne.existsSync(e)) continue; + for (let t of ne.readdirSync(e)) if (dt(t)) return !0; } catch (t) {} } - } catch (h) {} + } catch (u) {} return !1; } -var _e = H(require("fs")), - K = H(require("path")), - lt = require("child_process"), - se = null; -function Bt(g, h) { - let e = h || _e, - t = K.join(g, "paperforge.json"), +var ye = $(require("fs")), + W = $(require("path")), + ut = require("child_process"), + ae = null; +function zt(f, u) { + let e = u || ye, + t = W.join(f, "paperforge.json"), r = { system_dir: "System", resources_dir: "Resources", @@ -983,96 +1015,96 @@ function Bt(g, h) { ); } } -function ne(g, h) { - let e = Bt(g, h), - t = K.join(g, e.system_dir, "PaperForge"); +function Q(f, u) { + let e = zt(f, u), + t = W.join(f, e.system_dir, "PaperForge"); return { - vault: g, + vault: f, systemDir: t, - indexesDir: K.join(t, "indexes"), - logsDir: K.join(t, "logs"), - dbPath: K.join(t, "indexes", "paperforge.db"), - memoryStatePath: K.join(t, "indexes", "memory-runtime-state.json"), - vectorStatePath: K.join(t, "indexes", "vector-runtime-state.json"), - healthStatePath: K.join(t, "indexes", "runtime-health.json"), - buildStatePath: K.join(t, "indexes", "vector-build-state.json"), - orphanStatePath: K.join(t, "indexes", "sync-orphan-state.json"), - exportsDir: K.join(t, "exports"), - ocrDir: K.join(t, "ocr"), - pluginDataPath: K.join( - g, + indexesDir: W.join(t, "indexes"), + logsDir: W.join(t, "logs"), + dbPath: W.join(t, "indexes", "paperforge.db"), + memoryStatePath: W.join(t, "indexes", "memory-runtime-state.json"), + vectorStatePath: W.join(t, "indexes", "vector-runtime-state.json"), + healthStatePath: W.join(t, "indexes", "runtime-health.json"), + buildStatePath: W.join(t, "indexes", "vector-build-state.json"), + orphanStatePath: W.join(t, "indexes", "sync-orphan-state.json"), + exportsDir: W.join(t, "exports"), + ocrDir: W.join(t, "ocr"), + pluginDataPath: W.join( + f, ".obsidian", "plugins", "paperforge", "data.json" ), - pfJsonPath: K.join(g, "paperforge.json"), + pfJsonPath: W.join(f, "paperforge.json"), configWarning: e._warning, }; } -function Je(g) { +function Xe(f) { try { - return _e.existsSync(g) ? JSON.parse(_e.readFileSync(g, "utf-8")) : null; - } catch (h) { + return ye.existsSync(f) ? JSON.parse(ye.readFileSync(f, "utf-8")) : null; + } catch (u) { return null; } } -function At(g) { - let h = ne(g); - return Je(h.memoryStatePath); +function $t(f) { + let u = Q(f); + return Xe(u.memoryStatePath); } -function Ce(g) { - let h = ne(g); - return Je(h.vectorStatePath); +function Re(f) { + let u = Q(f); + return Xe(u.vectorStatePath); } -function Ue(g) { - let h = ne(g); - return Je(h.healthStatePath); +function Ye(f) { + let u = Q(f); + return Xe(u.healthStatePath); } -function ct(g) { +function ft(f) { var e; - let h = Ue(g); - return !!(h && ((e = h.summary) == null ? void 0 : e.status) === "ok"); + let u = Ye(f); + return !!(u && ((e = u.summary) == null ? void 0 : e.status) === "ok"); } -function Oe(g) { - let h = At(g); - return !h || h.paper_count_db === 0 +function Ie(f) { + let u = $t(f); + return !u || u.paper_count_db === 0 ? "DB not found. Run paperforge memory build." - : "Papers: " + h.paper_count_db + " | " + (h.fresh ? "fresh" : "stale"); + : "Papers: " + u.paper_count_db + " | " + (u.fresh ? "fresh" : "stale"); } -function ye(g) { +function be(f) { var t, r, n; - let h = Ce(g); - return h - ? h.healthy === !1 + let u = Re(f); + return u + ? u.healthy === !1 ? "Vector index unreadable - rebuild required" : "Chunks: " + - (((t = h.chunk_count) != null ? t : 0) + - ((r = h.body_chunk_count) != null ? r : 0) + - ((n = h.object_chunk_count) != null ? n : 0)) + + (((t = u.chunk_count) != null ? t : 0) + + ((r = u.body_chunk_count) != null ? r : 0) + + ((n = u.object_chunk_count) != null ? n : 0)) + " | " + - h.model + + u.model + " | " + - h.mode + u.mode : "Status unavailable"; } -function le(g, h) { - if (se) return se; - if (h && h.python_path && h.python_path.trim()) { - let r = h.python_path.trim(); - if (_e.existsSync(r)) - return ((se = { path: r, source: "manual", extraArgs: [] }), se); +function ce(f, u) { + if (ae) return ae; + if (u && u.python_path && u.python_path.trim()) { + let r = u.python_path.trim(); + if (ye.existsSync(r)) + return ((ae = { path: r, source: "manual", extraArgs: [] }), ae); } let e = [ - K.join(g, ".paperforge-test-venv", "Scripts", "python.exe"), - K.join(g, ".venv", "Scripts", "python.exe"), - K.join(g, "venv", "Scripts", "python.exe"), + W.join(f, ".paperforge-test-venv", "Scripts", "python.exe"), + W.join(f, ".venv", "Scripts", "python.exe"), + W.join(f, "venv", "Scripts", "python.exe"), ]; for (let r = 0; r < e.length; r++) - if (_e.existsSync(e[r])) + if (ye.existsSync(e[r])) return ( - (se = { path: e[r], source: "auto-detected", extraArgs: [] }), - se + (ae = { path: e[r], source: "auto-detected", extraArgs: [] }), + ae ); let t = [ { path: "py", extraArgs: ["-3"] }, @@ -1082,50 +1114,50 @@ function le(g, h) { for (let r = 0; r < t.length; r++) try { let n = t[r], - a = (0, lt.execFileSync)(n.path, n.extraArgs.concat(["--version"]), { + a = (0, ut.execFileSync)(n.path, n.extraArgs.concat(["--version"]), { encoding: "utf-8", timeout: 5e3, windowsHide: !0, }); if (a && a.toLowerCase().indexOf("python") !== -1) return ( - (se = { + (ae = { path: n.path, source: "auto-detected", extraArgs: n.extraArgs, }), - se + ae ); } catch (n) {} return ( - (se = { path: "python", source: "auto-detected", extraArgs: [] }), - se + (ae = { path: "python", source: "auto-detected", extraArgs: [] }), + ae ); } -function Ge(g, h, e) { - return !g || - typeof g != "object" || - !Object.prototype.hasOwnProperty.call(g, h) +function Qe(f, u, e) { + return !f || + typeof f != "object" || + !Object.prototype.hasOwnProperty.call(f, u) ? !!e - : !!g[h]; + : !!f[u]; } -function pt(g, h, e) { - let t = !Ge(g, h, e); - return (g && typeof g == "object" && (g[h] = t), t); +function ht(f, u, e) { + let t = !Qe(f, u, e); + return (f && typeof f == "object" && (f[u] = t), t); } -var J = require("obsidian"), - Q = H(require("fs")), - ut = H(require("path")), - ht = H(require("https")), - Re = require("child_process"); -function dt(g, h) { - return !h || !h.trim() +var G = require("obsidian"), + te = $(require("fs")), + _t = $(require("path")), + mt = $(require("https")), + De = require("child_process"); +function gt(f, u) { + return !u || !u.trim() ? { blocked: !0, reason: "zotero" } - : g + : f ? { blocked: !1 } : { blocked: !0, reason: "ocr" }; } -var Xe = class extends J.Modal { +var et = class extends G.Modal { constructor(e, t, r, n) { super(e); this._rowEls = []; @@ -1136,12 +1168,12 @@ var Xe = class extends J.Modal { _updateUI() { let e = this.orphans.filter((t) => t._selected); (this._countEl.setText( - l("orphan_delete_selected").replace("{count}", String(e.length)) + o("orphan_delete_selected").replace("{count}", String(e.length)) ), this._selectAllBtn.setText( e.length === this.orphans.length - ? l("orphan_deselect_all") - : l("orphan_select_all") + ? o("orphan_deselect_all") + : o("orphan_select_all") )); for (let t of this.orphans) { let r = this._rowEls[t._idx]; @@ -1152,9 +1184,9 @@ var Xe = class extends J.Modal { let { contentEl: e } = this; (e.addClass("paperforge-modal"), e.createEl("h2", { - text: l("orphan_title").replace("{count}", String(this.orphans.length)), + text: o("orphan_title").replace("{count}", String(this.orphans.length)), }), - e.createEl("p", { cls: "paperforge-modal-desc", text: l("orphan_desc") }), + e.createEl("p", { cls: "paperforge-modal-desc", text: o("orphan_desc") }), (this._rowEls = [])); let t = e.createEl("div", { cls: "paperforge-orphan-list" }); for (let n of this.orphans) { @@ -1165,12 +1197,12 @@ var Xe = class extends J.Modal { }); this._rowEls.push(a); let s = a.createEl("div", { cls: "paperforge-orphan-info" }), - o = s.createEl("div", { cls: "paperforge-orphan-header" }); - o.createEl("span", { + l = s.createEl("div", { cls: "paperforge-orphan-header" }); + l.createEl("span", { cls: "paperforge-orphan-key", text: n.citation_key || n.key, }); - let i = o.createEl("span", { cls: "paperforge-orphan-tags" }); + let i = l.createEl("span", { cls: "paperforge-orphan-tags" }); (i.createEl("span", { cls: "paperforge-tag " + (n.has_pdf ? "tag-pdf" : "tag-nopdf"), text: n.has_pdf ? "PDF" : "no PDF", @@ -1192,7 +1224,7 @@ var Xe = class extends J.Modal { }), s.createEl("div", { cls: "paperforge-orphan-explain", - text: l("orphan_explain"), + text: o("orphan_explain"), }), a.addEventListener("click", () => { ((n._selected = !n._selected), this._updateUI()); @@ -1218,7 +1250,7 @@ var Xe = class extends J.Modal { this._countEl.addEventListener("click", () => { let n = this.orphans.filter((s) => s._selected); if (n.length === 0) { - new J.Notice(l("orphan_none_selected")); + new G.Notice(o("orphan_none_selected")); return; } if ( @@ -1227,11 +1259,11 @@ var Xe = class extends J.Modal { this._selectAllBtn.setAttr("disabled", ""), !this.py || !this.py.path) ) { - (new J.Notice("PaperForge: Python not found"), this.close()); + (new G.Notice("PaperForge: Python not found"), this.close()); return; } let a = n.map((s) => s.key); - (0, Re.execFile)( + (0, De.execFile)( this.py.path, [ ...this.py.extraArgs, @@ -1245,17 +1277,17 @@ var Xe = class extends J.Modal { ...a, ], { cwd: this.vaultPath, timeout: 6e4 }, - (s, o) => { + (s, l) => { if (s) { - (new J.Notice("PaperForge: prune failed"), this.close()); + (new G.Notice("PaperForge: prune failed"), this.close()); return; } try { - let i = JSON.parse(o), + let i = JSON.parse(l), c = (i.data && i.data.deleted) || []; - new J.Notice("Deleted " + c.length + " orphan workspace(s)"); + new G.Notice("Deleted " + c.length + " orphan workspace(s)"); } catch (i) { - new J.Notice("PaperForge: prune done"); + new G.Notice("PaperForge: prune done"); } this.close(); } @@ -1266,28 +1298,28 @@ var Xe = class extends J.Modal { this.contentEl.empty(); } }; -function Me(g, h, e) { +function Ne(f, u, e) { console.log("[PF] checkOrphanState called"); try { - let r = ne(e).orphanStatePath; - if (!Q.existsSync(r)) { + let r = Q(e).orphanStatePath; + if (!te.existsSync(r)) { console.log("[PF] orphan file NOT FOUND"); return; } console.log("[PF] orphan file FOUND"); - let n = Q.readFileSync(r, "utf-8"), + let n = te.readFileSync(r, "utf-8"), s = JSON.parse(n).orphans || []; if ((console.log("[PF] orphans count:", s.length), s.length === 0)) return; - let o = le(e, h.settings); - (console.log("[PF] py.path:", o ? o.path : "null"), - new Xe(g, s, e, o).open(), - Q.unlinkSync(r), + let l = ce(e, u.settings); + (console.log("[PF] py.path:", l ? l.path : "null"), + new et(f, s, e, l).open(), + te.unlinkSync(r), console.log("[PF] orphan file cleaned")); } catch (t) { console.log("[PF] checkOrphanState exception:", t.message || t); } } -var Fe = class extends J.Modal { +var Te = class extends G.Modal { constructor(e, t) { super(e); this._pendingSave = null; @@ -1310,11 +1342,11 @@ var Fe = class extends J.Modal { } _renderStepIndicator() { let e = [ - l("wizard_step1"), - l("wizard_step2"), - l("wizard_step3"), - l("wizard_step4"), - l("wizard_step5"), + o("wizard_step1"), + o("wizard_step2"), + o("wizard_step3"), + o("wizard_step4"), + o("wizard_step5"), ], t = this.contentEl.createEl("div", { cls: "paperforge-step-bar" }); e.forEach((r, n) => { @@ -1350,7 +1382,7 @@ var Fe = class extends J.Modal { let e = this.contentEl.createEl("div", { cls: "paperforge-step-nav" }); (this._step > 1 && e - .createEl("button", { cls: "paperforge-step-btn", text: l("nav_prev") }) + .createEl("button", { cls: "paperforge-step-btn", text: o("nav_prev") }) .addEventListener("click", () => { (this._step--, (this._showSkipConfirm = !1), this._render()); }), @@ -1358,7 +1390,7 @@ var Fe = class extends J.Modal { ? e .createEl("button", { cls: "paperforge-step-btn mod-cta", - text: l("nav_next"), + text: o("nav_next"), }) .addEventListener("click", () => { if (this._step === 3) { @@ -1376,56 +1408,56 @@ var Fe = class extends J.Modal { : e .createEl("button", { cls: "paperforge-step-btn", - text: l("nav_close"), + text: o("nav_close"), }) .addEventListener("click", () => this.close())); } _validateStep3() { let e = this.plugin.settings, - t = dt(this._apiKeyValidated, e.zotero_data_dir); + t = gt(this._apiKeyValidated, e.zotero_data_dir); if (t.reason === "ocr") return t; let r = (e.zotero_data_dir || "").trim(); if (!r) return ( - new J.Notice( + new G.Notice( "Zotero \u6570\u636E\u76EE\u5F55\u4E3A\u5FC5\u586B\u9879\uFF0C\u8BF7\u586B\u5199\u8DEF\u5F84" ), { blocked: !0, reason: "zotero" } ); - if (!Q.existsSync(r)) + if (!te.existsSync(r)) return ( - new J.Notice( + new G.Notice( "Zotero \u6570\u636E\u76EE\u5F55\u8DEF\u5F84\u4E0D\u5B58\u5728" ), { blocked: !0, reason: "zotero" } ); - if (!Q.statSync(r).isDirectory()) + if (!te.statSync(r).isDirectory()) return ( - new J.Notice( + new G.Notice( "Zotero \u6570\u636E\u76EE\u5F55\u8DEF\u5F84\u4E0D\u662F\u4E00\u4E2A\u76EE\u5F55" ), { blocked: !0, reason: "zotero" } ); - let n = ut.join(r, "storage"); - return !Q.existsSync(n) || !Q.statSync(n).isDirectory() - ? (new J.Notice( + let n = _t.join(r, "storage"); + return !te.existsSync(n) || !te.statSync(n).isDirectory() + ? (new G.Notice( "Zotero \u6570\u636E\u76EE\u5F55\u4E2D\u672A\u627E\u5230 storage/ \u5B50\u76EE\u5F55" ), { blocked: !0, reason: "zotero" }) : { blocked: !1 }; } _stepOverview(e) { - (e.createEl("h2", { text: l("wizard_title") }), - e.createEl("p", { text: l("wizard_intro") })); + (e.createEl("h2", { text: o("wizard_title") }), + e.createEl("p", { text: o("wizard_intro") })); let t = this.plugin.settings, r = this.app.vault.adapter.basePath, n = e.createEl("div", { cls: "paperforge-dir-tree" }), a = n.createEl("div", { cls: "paperforge-dir-node root" }); a.textContent = `\u{1F4C1} Vault (${r})`; let s = n.createEl("div", { cls: "paperforge-dir-children" }), - o = s.createEl("div", { cls: "paperforge-dir-node folder" }); - ((o.textContent = `\u{1F4C1} ${t.resources_dir || "Resources"}/ \u2014 \u6587\u732E\u5361\u7247\u76EE\u5F55\uFF08Base \u6570\u636E\u6765\u6E90\uFF09`), - o + l = s.createEl("div", { cls: "paperforge-dir-node folder" }); + ((l.textContent = `\u{1F4C1} ${t.resources_dir || "Resources"}/ \u2014 \u6587\u732E\u5361\u7247\u76EE\u5F55\uFF08Base \u6570\u636E\u6765\u6E90\uFF09`), + l .createEl("div", { cls: "paperforge-dir-children" }) .createEl("div", { cls: "paperforge-dir-node file", @@ -1440,40 +1472,40 @@ var Fe = class extends J.Modal { text: `\u{1F4C1} ${t.system_dir || "System"}/ \u2014 Zotero \u8F6F\u94FE\u63A5 + PaperForge \u7CFB\u7EDF\u6587\u4EF6\u5939`, }), e.createEl("p", { - text: l("wizard_preview"), + text: o("wizard_preview"), cls: "paperforge-modal-hint", }), e.createEl("p", { - text: l("wizard_safety"), + text: o("wizard_safety"), cls: "paperforge-modal-hint", })); let c = e.createEl("div", { cls: "paperforge-summary" }), d = [ { - label: l("dir_resources"), + label: o("dir_resources"), val: `${r}/${t.resources_dir || "Resources"}`, }, { - label: l("dir_notes"), + label: o("dir_notes"), val: `${r}/${t.resources_dir || "Resources"}/${t.literature_dir || "Literature"}`, }, - { label: l("dir_base"), val: `${r}/${t.base_dir || "Bases"}` }, - { label: l("dir_system"), val: `${r}/${t.system_dir || "System"}` }, + { label: o("dir_base"), val: `${r}/${t.base_dir || "Bases"}` }, + { label: o("dir_system"), val: `${r}/${t.system_dir || "System"}` }, ]; - for (let u of d) { + for (let h of d) { let p = c.createEl("div", { cls: "paperforge-summary-row" }); - (p.createEl("span", { cls: "paperforge-summary-label", text: u.label }), - p.createEl("span", { cls: "paperforge-summary-value", text: u.val })); + (p.createEl("span", { cls: "paperforge-summary-label", text: h.label }), + p.createEl("span", { cls: "paperforge-summary-value", text: h.val })); } } _stepDirectories(e) { - (e.createEl("h2", { text: l("wizard_step2") }), - e.createEl("p", { text: l("wizard_intro") })); + (e.createEl("h2", { text: o("wizard_step2") }), + e.createEl("p", { text: o("wizard_intro") })); let t = this.plugin.settings, r = this.app.vault.adapter.basePath; - (this._modalField(e, l("dir_vault"), r, !0), + (this._modalField(e, o("dir_vault"), r, !0), e.createEl("p", { - text: l("wizard_dir_hint"), + text: o("wizard_dir_hint"), cls: "paperforge-modal-hint", }), this._modalInput( @@ -1484,7 +1516,7 @@ var Fe = class extends J.Modal { "Resources" ), e.createEl("p", { - text: l("wizard_dir_sub_hint"), + text: o("wizard_dir_sub_hint"), cls: "paperforge-modal-hint", }), this._modalInput( @@ -1495,7 +1527,7 @@ var Fe = class extends J.Modal { "Literature" ), e.createEl("p", { - text: l("wizard_sys_hint"), + text: o("wizard_sys_hint"), cls: "paperforge-modal-hint", }), this._modalInput( @@ -1513,35 +1545,35 @@ var Fe = class extends J.Modal { "Bases" ), e.createEl("p", { - text: l("wizard_safety"), + text: o("wizard_safety"), cls: "paperforge-modal-hint", })); let n = e.createEl("div", { cls: "paperforge-summary" }), a = [ - { label: l("dir_resources"), val: `${r}/${t.resources_dir || ""}` }, + { label: o("dir_resources"), val: `${r}/${t.resources_dir || ""}` }, { - label: l("dir_notes"), + label: o("dir_notes"), val: `${r}/${t.resources_dir || ""}/${t.literature_dir || ""}`, }, - { label: l("dir_system"), val: `${r}/${t.system_dir || ""}` }, - { label: l("dir_base"), val: `${r}/${t.base_dir || ""}` }, + { label: o("dir_system"), val: `${r}/${t.system_dir || ""}` }, + { label: o("dir_base"), val: `${r}/${t.base_dir || ""}` }, ]; for (let s of a) { - let o = n.createEl("div", { cls: "paperforge-summary-row" }); - (o.createEl("span", { cls: "paperforge-summary-label", text: s.label }), - o.createEl("span", { cls: "paperforge-summary-value", text: s.val })); + let l = n.createEl("div", { cls: "paperforge-summary-row" }); + (l.createEl("span", { cls: "paperforge-summary-label", text: s.label }), + l.createEl("span", { cls: "paperforge-summary-value", text: s.val })); } } _stepKeys(e) { if ( - (e.createEl("h2", { text: l("wizard_step3") }), this._showSkipConfirm) + (e.createEl("h2", { text: o("wizard_step3") }), this._showSkipConfirm) ) { this._renderSkipConfirm(e); return; } let t = this.plugin.settings; e.createEl("p", { - text: l("wizard_agent_hint"), + text: o("wizard_agent_hint"), cls: "paperforge-modal-hint", }); let r = [ @@ -1557,12 +1589,12 @@ var Fe = class extends J.Modal { n = e.createEl("div", { cls: "paperforge-modal-field" }); n.createEl("label", { cls: "paperforge-modal-label", - text: l("label_agent"), + text: o("label_agent"), }); let a = n.createEl("select", { cls: "paperforge-modal-select" }); - for (let u of r) { - let p = a.createEl("option", { text: u.name, attr: { value: u.key } }); - u.key === (t.agent_platform || "opencode") && (p.selected = !0); + for (let h of r) { + let p = a.createEl("option", { text: h.name, attr: { value: h.key } }); + h.key === (t.agent_platform || "opencode") && (p.selected = !0); } (a.addEventListener("change", () => { ((t.agent_platform = a.value), @@ -1572,19 +1604,19 @@ var Fe = class extends J.Modal { }, 500))); }), e.createEl("p", { - text: l("wizard_keys_hint"), + text: o("wizard_keys_hint"), cls: "paperforge-modal-hint", })); let s = e.createEl("div", { cls: "paperforge-modal-field" }); s.createEl("label", { cls: "paperforge-modal-label", - text: l("field_paddleocr"), + text: o("field_paddleocr"), }); - let o = s.createEl("input", { + let l = s.createEl("input", { cls: "paperforge-modal-input", attr: { type: "password", placeholder: "API Key" }, }); - ((o.value = t.paddleocr_api_key || ""), + ((l.value = t.paddleocr_api_key || ""), (this._apiKeyValidated = !1), (this._apiKeyStatus = s.createEl("span", { cls: "paperforge-apikey-status", @@ -1594,9 +1626,9 @@ var Fe = class extends J.Modal { cls: "paperforge-step-btn", text: "\u9A8C\u8BC1", }); - (i.addEventListener("click", () => this._validateApiKey(o.value, i)), - o.addEventListener("input", () => { - ((t.paddleocr_api_key = o.value), + (i.addEventListener("click", () => this._validateApiKey(l.value, i)), + l.addEventListener("input", () => { + ((t.paddleocr_api_key = l.value), (this._apiKeyValidated = !1), (this._apiKeyStatus.textContent = ""), (this._apiKeyStatus.className = "paperforge-apikey-status")); @@ -1606,17 +1638,17 @@ var Fe = class extends J.Modal { (this.plugin.saveSettings(), (this._pendingSave = null)); }, 500)), e.createEl("p", { - text: l("wizard_api_hint_skip"), + text: o("wizard_api_hint_skip"), cls: "paperforge-modal-hint", })); let c = e.createEl("div", { cls: "paperforge-modal-field" }); c.createEl("label", { cls: "paperforge-modal-label", - text: l("field_zotero_data"), + text: o("field_zotero_data"), }); let d = c.createEl("input", { cls: "paperforge-modal-input", - attr: { type: "text", placeholder: l("field_zotero_placeholder") }, + attr: { type: "text", placeholder: o("field_zotero_placeholder") }, }); ((d.value = t.zotero_data_dir || ""), d.addEventListener("input", () => { @@ -1650,13 +1682,13 @@ var Fe = class extends J.Modal { }, timeout: 1e4, }, - a = ht.request(n, (s) => { + a = mt.request(n, (s) => { ((t.disabled = !1), (t.textContent = "\u9A8C\u8BC1")); - let o = ""; - (s.on("data", (i) => (o += i)), + let l = ""; + (s.on("data", (i) => (l += i)), s.on("end", () => { try { - let i = JSON.parse(o); + let i = JSON.parse(l); s.statusCode === 400 && i.code === 10001 ? ((this._apiKeyStatus.textContent = "\u2713 \u5BC6\u94A5\u6709\u6548"), @@ -1700,14 +1732,14 @@ var Fe = class extends J.Modal { } _renderSkipConfirm(e) { e.createEl("p", { - text: l("wizard_skip_ocr_desc"), + text: o("wizard_skip_ocr_desc"), cls: "paperforge-modal-desc", }); let t = e.createEl("div", { cls: "paperforge-modal-actions" }); (t .createEl("button", { cls: "paperforge-step-btn mod-cta", - text: l("wizard_skip_ocr_continue"), + text: o("wizard_skip_ocr_continue"), }) .addEventListener("click", () => { ((this._showSkipConfirm = !1), this._step++, this._render()); @@ -1715,7 +1747,7 @@ var Fe = class extends J.Modal { t .createEl("button", { cls: "paperforge-step-btn", - text: l("wizard_skip_ocr_back"), + text: o("wizard_skip_ocr_back"), }) .addEventListener("click", () => { ((this._showSkipConfirm = !1), this._render()); @@ -1733,14 +1765,14 @@ var Fe = class extends J.Modal { _modalInput(e, t, r, n, a) { let s = e.createEl("div", { cls: "paperforge-modal-field" }); s.createEl("label", { cls: "paperforge-modal-label", text: t }); - let o = s.createEl("input", { + let l = s.createEl("input", { cls: "paperforge-modal-input", attr: { type: "text", placeholder: a || "" }, }); - o.value = n; + l.value = n; let i = this.plugin.settings; - o.addEventListener("input", () => { - ((i[r] = o.value), + l.addEventListener("input", () => { + ((i[r] = l.value), this._pendingSave && clearTimeout(this._pendingSave), (this._pendingSave = setTimeout(() => { (this.plugin.saveSettings(), (this._pendingSave = null)); @@ -1750,14 +1782,14 @@ var Fe = class extends J.Modal { _modalSecret(e, t, r, n, a) { let s = e.createEl("div", { cls: "paperforge-modal-field" }); s.createEl("label", { cls: "paperforge-modal-label", text: t }); - let o = s.createEl("input", { + let l = s.createEl("input", { cls: "paperforge-modal-input", attr: { type: "password", placeholder: a || "" }, }); - o.value = n; + l.value = n; let i = this.plugin.settings; - o.addEventListener("input", () => { - ((i[r] = o.value), + l.addEventListener("input", () => { + ((i[r] = l.value), this._pendingSave && clearTimeout(this._pendingSave), (this._pendingSave = setTimeout(() => { (this.plugin.saveSettings(), (this._pendingSave = null)); @@ -1765,65 +1797,65 @@ var Fe = class extends J.Modal { }); } _stepInstall(e) { - (e.createEl("h2", { text: l("wizard_step4") }), + (e.createEl("h2", { text: o("wizard_step4") }), (this._installLog = e.createEl("div", { cls: "paperforge-install-log", }))); let t = e.createEl("button", { cls: "paperforge-step-btn mod-cta", - text: l("install_btn"), + text: o("install_btn"), }); t.addEventListener("click", () => this._runInstall(t)); } async _runInstall(e) { - var s, o, i, c, d, u; + var s, l, i, c, d, h; ((e.disabled = !0), - (e.textContent = l("install_btn_running")), + (e.textContent = o("install_btn_running")), this._installLog.setText( - l("install_validating") + + o("install_validating") + ` ` ), - this._log(l("install_validating"))); + this._log(o("install_validating"))); let t = this.plugin.settings, r = this._validate(); if (r.length > 0) { - (this._log(l("validate_fail") + ":"), + (this._log(o("validate_fail") + ":"), r.forEach((p) => this._log(" \u2717 " + p)), (e.disabled = !1), - (e.textContent = l("install_btn_retry"))); + (e.textContent = o("install_btn_retry"))); return; } - let n = (p, _ = {}) => - new Promise((m, E) => { - let { path: f, extraArgs: v = [] } = M( + let n = (p, m = {}) => + new Promise((_, x) => { + let { path: y, extraArgs: b = [] } = M( t.vault_path.trim(), this.plugin.settings, void 0, void 0 ), - S = (0, Re.spawn)(f, [...v, ...p], { + w = (0, De.spawn)(y, [...b, ...p], { cwd: t.vault_path.trim(), - env: fe(), + env: me(), timeout: 12e4, - ..._, + ...m, }), - x = "", - y = ""; - (S.stdout.on("data", (b) => { - let k = b.toString("utf-8"); - ((x += k), _.logStdout && this._processSetupOutput(k)); + E = "", + g = ""; + (w.stdout.on("data", (v) => { + let k = v.toString("utf-8"); + ((E += k), m.logStdout && this._processSetupOutput(k)); }), - S.stderr.on("data", (b) => { - let k = b.toString("utf-8"); - ((y += k), this._log("[stderr] " + k.trim())); + w.stderr.on("data", (v) => { + let k = v.toString("utf-8"); + ((g += k), this._log("[stderr] " + k.trim())); }), - S.on("close", (b) => { - b === 0 - ? m({ stdout: x, stderr: y }) - : E(new Error(y.trim() || x.trim() || `exit code ${b}`)); + w.on("close", (v) => { + v === 0 + ? _({ stdout: E, stderr: g }) + : x(new Error(g.trim() || E.trim() || `exit code ${v}`)); }), - S.on("error", (b) => E(b))); + w.on("error", (v) => x(v))); }), a = [ "-m", @@ -1853,34 +1885,34 @@ var Fe = class extends J.Modal { let p = !0; try { await n(["-c", "import paperforge"]); - } catch (_) { + } catch (m) { p = !1; } if (!p) { - this._log(l("install_bootstrapping")); - let _ = this.plugin.manifest.version; - this._log(`[install] Trying PyPI: pip install paperforge==${_}`); - let m = ["-m", "pip", "install", "--upgrade"]; - (process.platform !== "win32" && m.push("--user"), - m.push(`paperforge==${_}`)); + this._log(o("install_bootstrapping")); + let m = this.plugin.manifest.version; + this._log(`[install] Trying PyPI: pip install paperforge==${m}`); + let _ = ["-m", "pip", "install", "--upgrade"]; + (process.platform !== "win32" && _.push("--user"), + _.push(`paperforge==${m}`)); try { - await n(m, { logStdout: !0 }); - } catch (E) { + await n(_, { logStdout: !0 }); + } catch (x) { (this._log( - `[install] PyPI failed, falling back to git: git+https://...@v${_}` + `[install] PyPI failed, falling back to git: git+https://...@v${m}` ), console.warn( "[PaperForge] PyPI install failed, falling back to git:", - (s = E.message) == null ? void 0 : s.slice(0, 200) + (s = x.message) == null ? void 0 : s.slice(0, 200) )); - let f = ["-m", "pip", "install", "--upgrade"]; - (process.platform !== "win32" && f.push("--user"), - f.push(`git+https://github.com/LLLin000/PaperForge.git@v${_}`), - await n(f, { logStdout: !0 })); + let y = ["-m", "pip", "install", "--upgrade"]; + (process.platform !== "win32" && y.push("--user"), + y.push(`git+https://github.com/LLLin000/PaperForge.git@v${m}`), + await n(y, { logStdout: !0 })); } } - (await n(a, { logStdout: !0, env: fe() }), - this._log(l("install_complete")), + (await n(a, { logStdout: !0, env: me() }), + this._log(o("install_complete")), (t.setup_complete = !0), await this.plugin.saveSettings(), setTimeout(() => { @@ -1888,71 +1920,71 @@ var Fe = class extends J.Modal { }, 800)); } catch (p) { console.error("PaperForge setup failed:", p.message); - let _ = this._formatSetupError(p.message); - this._log(l("install_failed") + _); - let m = - (o = this._installLog.parentElement) == null + let m = this._formatSetupError(p.message); + this._log(o("install_failed") + m); + let _ = + (l = this._installLog.parentElement) == null ? void 0 - : o.createEl("button", { + : l.createEl("button", { cls: "paperforge-copy-diag-btn", - text: l("error_copy_diagnostic") || "Copy diagnostic", + text: o("error_copy_diagnostic") || "Copy diagnostic", }); - if (m) { - let E = p.message, - f = + if (_) { + let x = p.message, + y = ((c = (i = this.plugin) == null ? void 0 : i.settings) == null ? void 0 : c.python_path) || "auto", - v = - ((u = (d = this.plugin) == null ? void 0 : d.manifest) == null + b = + ((h = (d = this.plugin) == null ? void 0 : d.manifest) == null ? void 0 - : u.version) || "?", - S = process.platform + " " + process.arch, - x, - y; + : h.version) || "?", + w = process.platform + " " + process.arch, + E, + g; try { - x = We() || "(not found)"; - } catch (T) { - x = "(error)"; + E = Ue() || "(not found)"; + } catch (P) { + E = "(error)"; } try { - y = M(t.vault_path.trim(), this.plugin.settings, void 0, void 0); - } catch (T) { - y = null; + g = M(t.vault_path.trim(), this.plugin.settings, void 0, void 0); + } catch (P) { + g = null; } - let b = (process.env.PATH || "").length, + let v = (process.env.PATH || "").length, k = (process.env.PATH || "").toLowerCase().includes("git"), - C = [ + S = [ "[PaperForge Diagnostic]", - "Category: " + _, - "Plugin version: " + v, - "Python: " + f, - "Resolved Python: " + ((y == null ? void 0 : y.path) || "?"), - "OS: " + S, + "Category: " + m, + "Plugin version: " + b, + "Python: " + y, + "Resolved Python: " + ((g == null ? void 0 : g.path) || "?"), + "OS: " + w, "Vault path: " + (t.vault_path || "?"), "--- Git ---", - "Git dir (resolved): " + x, - "PATH length: " + b + " chars", + "Git dir (resolved): " + E, + "PATH length: " + v + " chars", "PATH contains git: " + k, "--- Raw error ---", - E.slice(0, 2e3), + x.slice(0, 2e3), ].join(` `); - m.addEventListener("click", () => { + _.addEventListener("click", () => { navigator.clipboard - .writeText(C) + .writeText(S) .then(() => { - (m.setText(l("error_copied") || "Copied!"), + (_.setText(o("error_copied") || "Copied!"), setTimeout(() => { - m.setText(l("error_copy_diagnostic") || "Copy diagnostic"); + _.setText(o("error_copy_diagnostic") || "Copy diagnostic"); }, 3e3)); }) .catch(() => { - new J.Notice("[!!] Clipboard write failed", 6e3); + new G.Notice("[!!] Clipboard write failed", 6e3); }); }); } - ((e.disabled = !1), (e.textContent = l("install_btn_retry"))); + ((e.disabled = !1), (e.textContent = o("install_btn_retry"))); } } _log(e) { @@ -1968,16 +2000,16 @@ var Fe = class extends J.Modal { let e = [], t = this.plugin.settings; return ( - (!t.vault_path || !t.vault_path.trim()) && e.push(l("validate_vault")), + (!t.vault_path || !t.vault_path.trim()) && e.push(o("validate_vault")), (!t.resources_dir || !t.resources_dir.trim()) && - e.push(l("validate_resources")), + e.push(o("validate_resources")), (!t.literature_dir || !t.literature_dir.trim()) && - e.push(l("validate_notes")), - (!t.base_dir || !t.base_dir.trim()) && e.push(l("validate_base")), + e.push(o("validate_notes")), + (!t.base_dir || !t.base_dir.trim()) && e.push(o("validate_base")), (!t.paddleocr_api_key || !t.paddleocr_api_key.trim()) && - this._log(" ! " + l("validate_key") + " " + l("optional_later")), + this._log(" ! " + o("validate_key") + " " + o("optional_later")), (!t.zotero_data_dir || !t.zotero_data_dir.trim()) && - this._log(" ! " + l("validate_zotero") + " " + l("optional_later")), + this._log(" ! " + o("validate_zotero") + " " + o("optional_later")), e ); } @@ -2046,148 +2078,148 @@ var Fe = class extends J.Modal { ); } _stepComplete(e) { - e.createEl("h2", { text: l("complete_title") }); + e.createEl("h2", { text: o("complete_title") }); let t = e.createEl("div", { cls: "paperforge-summary" }); t.createEl("div", { cls: "paperforge-summary-title", - text: l("complete_summary"), + text: o("complete_summary"), }); let r = this.plugin.settings, n = this.app.vault.adapter.basePath, a = [ - { label: l("dir_vault"), val: n }, - { label: l("dir_resources"), val: `${n}/${r.resources_dir}` }, + { label: o("dir_vault"), val: n }, + { label: o("dir_resources"), val: `${n}/${r.resources_dir}` }, { - label: l("dir_notes"), + label: o("dir_notes"), val: `${n}/${r.resources_dir}/${r.literature_dir}`, }, - { label: l("dir_base"), val: `${n}/${r.base_dir}` }, - { label: l("dir_system"), val: `${n}/${r.system_dir}` }, + { label: o("dir_base"), val: `${n}/${r.base_dir}` }, + { label: o("dir_system"), val: `${n}/${r.system_dir}` }, { label: "API Key", - val: r.paddleocr_api_key ? l("api_key_set") : l("api_key_missing"), + val: r.paddleocr_api_key ? o("api_key_set") : o("api_key_missing"), }, { - label: l("field_zotero_data"), - val: r.zotero_data_dir || l("not_set"), + label: o("field_zotero_data"), + val: r.zotero_data_dir || o("not_set"), }, ]; for (let d of a) { - let u = t.createEl("div", { cls: "paperforge-summary-row" }); - (u.createEl("span", { cls: "paperforge-summary-label", text: d.label }), - u.createEl("span", { cls: "paperforge-summary-value", text: d.val })); + let h = t.createEl("div", { cls: "paperforge-summary-row" }); + (h.createEl("span", { cls: "paperforge-summary-label", text: d.label }), + h.createEl("span", { cls: "paperforge-summary-value", text: d.val })); } let s = t.createEl("div", { cls: "paperforge-summary-row" }); s.createEl("span", { cls: "paperforge-summary-label", text: "PaperForge" }); - let o = s.createEl("span", { + let l = s.createEl("span", { cls: "paperforge-summary-value", text: "\u2014", }); { let d = n, - { path: u, extraArgs: p = [] } = M( + { path: h, extraArgs: p = [] } = M( d, this.plugin.settings, void 0, void 0 ); - (0, Re.execFile)( - u, + (0, De.execFile)( + h, [...p, "-c", "import paperforge; print(paperforge.__version__)"], { cwd: d, timeout: 1e4 }, - (_, m) => { - !_ && m && (o.textContent = "v" + m.trim()); + (m, _) => { + !m && _ && (l.textContent = "v" + _.trim()); } ); } for (let d of a) { - let u = t.createEl("div", { cls: "paperforge-summary-row" }); - (u.createEl("span", { cls: "paperforge-summary-label", text: d.label }), - u.createEl("span", { cls: "paperforge-summary-value", text: d.val })); + let h = t.createEl("div", { cls: "paperforge-summary-row" }); + (h.createEl("span", { cls: "paperforge-summary-label", text: d.label }), + h.createEl("span", { cls: "paperforge-summary-value", text: d.val })); } - e.createEl("h3", { text: l("complete_next") }); + e.createEl("h3", { text: o("complete_next") }); let i = e.createEl("div", { cls: "paperforge-nextsteps" }), c = [ - [l("complete_step4"), l("complete_step4_desc")], + [o("complete_step4"), o("complete_step4_desc")], [ "", - `${l("complete_export_path")} ${n}/${r.system_dir}/PaperForge/exports/`, + `${o("complete_export_path")} ${n}/${r.system_dir}/PaperForge/exports/`, ], - [l("complete_step1"), l("complete_step1_desc")], - [l("complete_step2"), l("complete_step2_desc")], - [l("complete_step3"), l("complete_step3_desc")], + [o("complete_step1"), o("complete_step1_desc")], + [o("complete_step2"), o("complete_step2_desc")], + [o("complete_step3"), o("complete_step3_desc")], ]; - for (let [d, u] of c) { + for (let [d, h] of c) { let p = i.createEl("div", { cls: "paperforge-nextstep-item" }); - (d && p.createEl("strong", { text: d }), p.createEl("span", { text: u })); + (d && p.createEl("strong", { text: d }), p.createEl("span", { text: h })); } } }; -var be = H(require("fs")), - Le = H(require("path")), - ft = require("child_process"); -function _t(g) { - return Le.join(g, "System", "PaperForge", "cache", "ocr_maintenance.json"); +var xe = $(require("fs")), + ze = $(require("path")), + vt = require("child_process"); +function bt(f) { + return ze.join(f, "System", "PaperForge", "cache", "ocr_maintenance.json"); } -function mt(g) { +function xt(f) { try { - let h = _t(g), - e = be.readFileSync(h, "utf-8"); + let u = bt(f), + e = xe.readFileSync(u, "utf-8"); return JSON.parse(e); - } catch (h) { + } catch (u) { return null; } } -function Ie(g, h) { - let e = _t(g), - t = Le.dirname(e); - (be.mkdirSync(t, { recursive: !0 }), - be.writeFileSync(e, JSON.stringify(h, null, 2), "utf-8")); +function $e(f, u) { + let e = bt(f), + t = ze.dirname(e); + (xe.mkdirSync(t, { recursive: !0 }), + xe.writeFileSync(e, JSON.stringify(u, null, 2), "utf-8")); } -function gt(g, h, e) { +function yt(f, u, e) { return new Promise((t, r) => { - (0, ft.execFile)(g, h, e, (n, a) => { + (0, vt.execFile)(f, u, e, (n, a) => { n ? r(n) : t(a); }); }); } -async function yt(g, h, e, t) { - let r = await gt(h, [...e, "-m", "paperforge", "ocr", "list", "--manifest"], { - cwd: g, +async function Et(f, u, e, t) { + let r = await yt(u, [...e, "-m", "paperforge", "ocr", "list", "--manifest"], { + cwd: f, timeout: 3e4, }), n = JSON.parse(r); if (t) { let d = Object.keys(t.manifest), - u = Object.keys(n); - if (d.length === u.length && d.every((_) => t.manifest[_] === n[_])) + h = Object.keys(n); + if (d.length === h.length && d.every((m) => t.manifest[m] === n[m])) return { - data: Object.values(t.papers).filter((m) => m.visible_in_maintenance), + data: Object.values(t.papers).filter((_) => _.visible_in_maintenance), changed: !1, }; } let a = Object.keys(n).filter( (d) => !(t != null && t.manifest[d]) || t.manifest[d] !== n[d] ), - s = await gt( - h, + s = await yt( + u, [...e, "-m", "paperforge", "ocr", "list", "--json", "--keys", ...a], - { cwd: g, timeout: 3e4 } + { cwd: f, timeout: 3e4 } ), - o = JSON.parse(s), + l = JSON.parse(s), i = { manifest: n, papers: {}, cached_at: new Date().toISOString() }; if (t != null && t.papers) for (let d of Object.keys(n)) t.papers[d] && (i.papers[d] = t.papers[d]); - for (let d of o) i.papers[d.key] = d; + for (let d of l) i.papers[d.key] = d; return ( - Ie(g, i), + $e(f, i), { data: Object.values(i.papers).filter((d) => d.visible_in_maintenance), changed: !0, } ); } -var Ne = class extends w.PluginSettingTab { +var je = class extends C.PluginSettingTab { constructor(e, t) { super(e, t); this._saveTimeout = null; @@ -2247,9 +2279,9 @@ var Ne = class extends w.PluginSettingTab { } let t = e.createDiv({ cls: "paperforge-settings-tabs" }), r = [ - { id: "setup", label: l("tab_setup") || "\u5B89\u88C5" }, - { id: "features", label: l("tab_features") || "\u529F\u80FD" }, - { id: "maintenance", label: l("tab_maintenance") || "\u7EF4\u62A4" }, + { id: "setup", label: o("tab_setup") || "\u5B89\u88C5" }, + { id: "features", label: o("tab_features") || "\u529F\u80FD" }, + { id: "maintenance", label: o("tab_maintenance") || "\u7EF4\u62A4" }, { id: "release-notes", label: "\u66F4\u65B0\u4E0E\u624B\u518C" }, ], n = {}; @@ -2283,196 +2315,195 @@ var Ne = class extends w.PluginSettingTab { (this.plugin.settings.vault_path || ((this.plugin.settings.vault_path = t), this._debouncedSave()), this.plugin.settings.setup_complete && - (I.existsSync(Z.join(t, "paperforge.json")) || + (N.existsSync(Z.join(t, "paperforge.json")) || ((this.plugin.settings.setup_complete = !1), this._debouncedSave())), - e.createEl("h2", { text: l("header_title") || "PaperForge" }), - e.createEl("p", { text: l("desc"), cls: "paperforge-settings-desc" })); + e.createEl("h2", { text: o("header_title") || "PaperForge" }), + e.createEl("p", { text: o("desc"), cls: "paperforge-settings-desc" })); let n = e .createEl("div", { cls: "paperforge-setup-bar" }) .createEl("span", { cls: "paperforge-setup-label" }); this.plugin.settings.setup_complete - ? (n.setText(l("setup_done")), n.addClass("paperforge-setup-done")) - : (n.setText(l("setup_pending")), n.addClass("paperforge-setup-pending")); + ? (n.setText(o("setup_done")), n.addClass("paperforge-setup-done")) + : (n.setText(o("setup_pending")), n.addClass("paperforge-setup-pending")); let a = this.app.vault.adapter.basePath, s = M(a, this.plugin.settings, void 0, void 0), - o = s.path, + l = s.path, i = this.plugin.settings._python_path_stale ? "stale" : s.source, - c = new w.Setting(e) - .setName(l("field_python_interp")) - .setDesc(this._getPythonDesc(o, i)); + c = new C.Setting(e) + .setName(o("field_python_interp")) + .setDesc(this._getPythonDesc(l, i)); this._pythonInterpDescEl = c.descEl; - let d = new w.Setting(e).setName(l("field_python_custom")).setDesc(""); + let d = new C.Setting(e).setName(o("field_python_custom")).setDesc(""); ((this._customPathDescEl = d.descEl), - d.addText((x) => { - x.setPlaceholder("e.g. C:\\Python310\\python.exe") + d.addText((E) => { + E.setPlaceholder("e.g. C:\\Python310\\python.exe") .setValue(this.plugin.settings.python_path || "") - .onChange((y) => { + .onChange((g) => { if ( - ((this.plugin.settings.python_path = y), + ((this.plugin.settings.python_path = g), this.plugin.saveSettings(), - y && y.trim()) + g && g.trim()) ) { - let C = I.existsSync(y.trim()); - this.plugin.settings._python_path_stale = !C; + let S = N.existsSync(g.trim()); + this.plugin.settings._python_path_stale = !S; } else this.plugin.settings._python_path_stale = !1; - let b = M( + let v = M( this.app.vault.adapter.basePath, this.plugin.settings, void 0, void 0 ), - k = this.plugin.settings._python_path_stale ? "stale" : b.source; + k = this.plugin.settings._python_path_stale ? "stale" : v.source; this._pythonInterpDescEl && (this._pythonInterpDescEl.textContent = this._getPythonDesc( - b.path, + v.path, k )); }); }), - d.addButton((x) => { - x.setButtonText(l("btn_validate")).onClick(() => + d.addButton((E) => { + E.setButtonText(o("btn_validate")).onClick(() => this._validatePythonOverride() ); }), - e.createEl("h3", { text: l("runtime_health") }), + e.createEl("h3", { text: o("runtime_health") }), e.createEl("p", { - text: l("runtime_health_desc"), + text: o("runtime_health_desc"), cls: "paperforge-settings-desc", })); - let u = new w.Setting(e) + let h = new C.Setting(e) .setName("PaperForge") - .setDesc(l("runtime_health_checking")), - p = u.descEl.createEl("span", { cls: "paperforge-runtime-badge" }), - _ = null; - u.addButton((x) => { - ((_ = x), - x - .setButtonText(l("runtime_health_sync")) + .setDesc(o("runtime_health_checking")), + p = h.descEl.createEl("span", { cls: "paperforge-runtime-badge" }), + m = null; + h.addButton((E) => { + ((m = E), + E.setButtonText(o("runtime_health_sync")) .setDisabled(!0) - .onClick(() => this._syncRuntime(x))); + .onClick(() => this._syncRuntime(E))); }); { - let x = this.app.vault.adapter.basePath, - { path: y, extraArgs: b = [] } = M( - x, + let E = this.app.vault.adapter.basePath, + { path: g, extraArgs: v = [] } = M( + E, this.plugin.settings, void 0, void 0 ), k = this.plugin.manifest.version || "?"; - (0, N.execFile)( - y, - [...b, "-c", "import paperforge; print(paperforge.__version__)"], - { cwd: x, timeout: 1e4 }, - (C, T) => { - let O = this.plugin.settings.setup_complete, - D = !C && T ? T.trim() : null, - z = D - ? `${l("runtime_health_plugin_ver").replace("{0}", k)} \u2192 ${l("runtime_health_package_ver").replace("{0}", D)}` - : O + (0, j.execFile)( + g, + [...v, "-c", "import paperforge; print(paperforge.__version__)"], + { cwd: E, timeout: 1e4 }, + (S, P) => { + let A = this.plugin.settings.setup_complete, + T = !S && P ? P.trim() : null, + I = T + ? `${o("runtime_health_plugin_ver").replace("{0}", k)} \u2192 ${o("runtime_health_package_ver").replace("{0}", T)}` + : A ? `Plugin v${k} \u2192 Python package not installed. Click "Sync Runtime" to install.` : `Plugin v${k} \u2192 Not configured. Please open the setup wizard first.`; - (u.setDesc(z), - D === k - ? (p.setText(l("runtime_health_match")), + (h.setDesc(I), + T === k + ? (p.setText(o("runtime_health_match")), (p.className = "paperforge-runtime-badge match"), - _ && _.setDisabled(!0)) - : D - ? (p.setText(l("runtime_health_mismatch")), + m && m.setDisabled(!0)) + : T + ? (p.setText(o("runtime_health_mismatch")), (p.className = "paperforge-runtime-badge mismatch"), - _ && _.setDisabled(!1)) - : (p.setText(O ? "Not installed" : "Setup needed"), + m && m.setDisabled(!1)) + : (p.setText(A ? "Not installed" : "Setup needed"), (p.className = "paperforge-runtime-badge missing"), - _ && _.setDisabled(!1))); + m && m.setDisabled(!1))); } ); } - (e.createEl("h3", { text: l("section_prep") }), + (e.createEl("h3", { text: o("section_prep") }), e.createEl("p", { - text: l("section_prep_desc"), + text: o("section_prep_desc"), cls: "paperforge-settings-desc", })); - let m = e.createEl("div", { cls: "paperforge-guide" }), - E = [ + let _ = e.createEl("div", { cls: "paperforge-guide" }), + x = [ ["prep_python", "prep_python_desc"], ["prep_zotero", "prep_zotero_desc"], ["prep_bbt", "prep_bbt_desc"], ["prep_key", "prep_key_desc"], ]; - for (let [x, y] of E) { - let b = m.createEl("div", { cls: "paperforge-guide-item" }); - (b.createEl("strong", { text: l(x) }), - b.createEl("span", { text: " \u2014 " + l(y) })); + for (let [E, g] of x) { + let v = _.createEl("div", { cls: "paperforge-guide-item" }); + (v.createEl("strong", { text: o(E) }), + v.createEl("span", { text: " \u2014 " + o(g) })); } this._checkEl = e.createEl("div", { cls: "paperforge-message" }); - let f = !this.plugin.settings.setup_complete; - (new w.Setting(e) - .setName(l(f ? "btn_install" : "btn_reconfig")) - .setDesc(l(f ? "btn_install_desc" : "btn_reconfig_desc")) - .addButton((x) => { - x.setButtonText(l(f ? "btn_install" : "btn_reconfig")) + let y = !this.plugin.settings.setup_complete; + (new C.Setting(e) + .setName(o(y ? "btn_install" : "btn_reconfig")) + .setDesc(o(y ? "btn_install_desc" : "btn_reconfig_desc")) + .addButton((E) => { + E.setButtonText(o(y ? "btn_install" : "btn_reconfig")) .setCta() .onClick(() => { - f + y ? this._preCheck(() => { - new Fe(this.app, this.plugin).open(); + new Te(this.app, this.plugin).open(); }) - : new Fe(this.app, this.plugin).open(); + : new Te(this.app, this.plugin).open(); }); }), - e.createEl("h3", { text: l("section_guide") })); - let v = e.createEl("div", { cls: "paperforge-guide" }), - S = [ + e.createEl("h3", { text: o("section_guide") })); + let b = e.createEl("div", { cls: "paperforge-guide" }), + w = [ ["guide_open", "guide_open_desc"], ["guide_sync", "guide_sync_desc"], ["guide_ocr", "guide_ocr_desc"], ]; - for (let [x, y] of S) { - let b = v.createEl("div", { cls: "paperforge-guide-item" }); - (b.createEl("strong", { text: l(x) }), - b.createEl("span", { text: " \u2014 " + l(y) })); + for (let [E, g] of w) { + let v = b.createEl("div", { cls: "paperforge-guide-item" }); + (v.createEl("strong", { text: o(E) }), + v.createEl("span", { text: " \u2014 " + o(g) })); } if (this.plugin.settings.setup_complete) { - e.createEl("h3", { text: l("section_config") }); - let x = e.createEl("div", { cls: "paperforge-summary" }), - y = this.plugin.settings, - b = this._pfConfig, + e.createEl("h3", { text: o("section_config") }); + let E = e.createEl("div", { cls: "paperforge-summary" }), + g = this.plugin.settings, + v = this._pfConfig, k = [ - { label: l("dir_vault"), val: t }, + { label: o("dir_vault"), val: t }, { - label: l("dir_resources"), - val: `${t}/${b == null ? void 0 : b.resources_dir}`, + label: o("dir_resources"), + val: `${t}/${v == null ? void 0 : v.resources_dir}`, }, { - label: " " + l("dir_notes"), - val: `${t}/${b == null ? void 0 : b.resources_dir}/${b == null ? void 0 : b.literature_dir}`, + label: " " + o("dir_notes"), + val: `${t}/${v == null ? void 0 : v.resources_dir}/${v == null ? void 0 : v.literature_dir}`, }, { - label: l("dir_base"), - val: `${t}/${b == null ? void 0 : b.base_dir}`, + label: o("dir_base"), + val: `${t}/${v == null ? void 0 : v.base_dir}`, }, { - label: l("dir_system"), - val: `${t}/${b == null ? void 0 : b.system_dir}`, + label: o("dir_system"), + val: `${t}/${v == null ? void 0 : v.system_dir}`, }, { label: "API Key", - val: y.paddleocr_api_key ? l("api_key_set") : l("api_key_missing"), + val: g.paddleocr_api_key ? o("api_key_set") : o("api_key_missing"), }, { - label: l("field_zotero_data"), - val: y.zotero_data_dir || l("not_set"), + label: o("field_zotero_data"), + val: g.zotero_data_dir || o("not_set"), }, ]; - for (let C of k) { - let T = x.createEl("div", { cls: "paperforge-summary-row" }); - (T.createEl("span", { cls: "paperforge-summary-label", text: C.label }), - T.createEl("span", { cls: "paperforge-summary-value", text: C.val })); + for (let S of k) { + let P = E.createEl("div", { cls: "paperforge-summary-row" }); + (P.createEl("span", { cls: "paperforge-summary-label", text: S.label }), + P.createEl("span", { cls: "paperforge-summary-value", text: S.val })); } } } _execMemoryStatus(e, t, r) { - (0, N.exec)( + (0, j.exec)( `"${e}" -m paperforge --vault "${t}" memory status --json`, { encoding: "utf-8", timeout: 15e3 }, (n, a) => { @@ -2483,10 +2514,10 @@ var Ne = class extends w.PluginSettingTab { try { let s = JSON.parse(a); if (s.ok) { - let o = s.data, - i = o.fresh ? "fresh" : "stale"; + let l = s.data, + i = l.fresh ? "fresh" : "stale"; r( - `Papers: ${o.paper_count_db} | ${i}${o.needs_rebuild ? " - needs rebuild" : ""}` + `Papers: ${l.paper_count_db} | ${i}${l.needs_rebuild ? " - needs rebuild" : ""}` ); } else r("DB not found. Run paperforge memory build."); } catch (s) { @@ -2496,7 +2527,7 @@ var Ne = class extends w.PluginSettingTab { ); } _execEmbedStatus(e, t, r) { - (0, N.exec)( + (0, j.exec)( `"${e}" -m paperforge --vault "${t}" embed status --json`, { encoding: "utf-8", timeout: 15e3 }, (n, a) => { @@ -2519,10 +2550,10 @@ var Ne = class extends w.PluginSettingTab { } _callPython(e, t) { let r = this.app.vault.adapter.basePath, - n = le(r, this.plugin.settings), + n = ce(r, this.plugin.settings), a = [...n.extraArgs, "-m", "paperforge", "--vault", r, ...e]; if (t && t.stream) { - let s = (0, N.spawn)(n.path, a, { + let s = (0, j.spawn)(n.path, a, { cwd: r, env: t.env || process.env, windowsHide: !0, @@ -2536,12 +2567,12 @@ var Ne = class extends w.PluginSettingTab { ); } return ( - (0, N.execFile)( + (0, j.execFile)( n.path, a, { cwd: r, timeout: (t && t.timeout) || 6e4 }, - (s, o, i) => { - t && t.onClose && t.onClose(s ? 1 : 0, o, i); + (s, l, i) => { + t && t.onClose && t.onClose(s ? 1 : 0, l, i); } ), null @@ -2558,18 +2589,18 @@ var Ne = class extends w.PluginSettingTab { : r && e.createEl("span", { text: r, cls: "paperforge-sync-status" })); let n = e.createEl("button", { cls: "paperforge-rebuild-btn", - text: l("feat_memory_rebuild_btn"), + text: o("feat_memory_rebuild_btn"), }); ((n.title = "Rebuild memory database"), (n.onclick = () => { let s = this.app.vault.adapter.basePath, - o = le(s, this.plugin.settings); - if (!o.path) { - new w.Notice(l("feat_no_python")); + l = ce(s, this.plugin.settings); + if (!l.path) { + new C.Notice(o("feat_no_python")); return; } - (console.log("[PaperForge] Rebuilding memory:", o.path), - n.setText(l("feat_memory_rebuilding")), + (console.log("[PaperForge] Rebuilding memory:", l.path), + n.setText(o("feat_memory_rebuilding")), n.setAttr("disabled", ""), this._callPython(["memory", "build"], { timeout: 6e4, @@ -2580,15 +2611,15 @@ var Ne = class extends w.PluginSettingTab { (c || "").slice(0, 200), (d || "").slice(0, 200) ), - n.setText(l("feat_memory_rebuild_btn")), + n.setText(o("feat_memory_rebuild_btn")), n.removeAttribute("disabled"), i === 0 - ? new w.Notice(l("feat_memory_rebuild_done")) - : new w.Notice( - l("feat_memory_rebuild_failed") + + ? new C.Notice(o("feat_memory_rebuild_done")) + : new C.Notice( + o("feat_memory_rebuild_failed") + (d ? " " + d.slice(0, 80) : "") ), - (this._memoryStatusText = Oe(s)), + (this._memoryStatusText = Ie(s)), this._refreshSnapshots(s)); }, })); @@ -2609,7 +2640,7 @@ var Ne = class extends w.PluginSettingTab { } _runManualSync() { let e = this.app.vault.adapter.basePath; - if (!le(e, this.plugin.settings).path) return; + if (!ce(e, this.plugin.settings).path) return; let r = document.querySelector(".paperforge-memory-status"); (r && this._renderMemoryStatusText(r, "Checking...", "syncing"), (this.plugin._autoSyncRunning = !0), @@ -2623,12 +2654,12 @@ var Ne = class extends w.PluginSettingTab { (this.plugin._lastSyncTime = this._lastSyncTime)), this.display(), this._refreshSnapshots(e), - Me(this.app, this.plugin, e)); + Ne(this.app, this.plugin, e)); }, })); } _refreshSnapshots(e) { - let t = le(e, this.plugin.settings), + let t = ce(e, this.plugin.settings), r = [ ...t.extraArgs, "-m", @@ -2639,14 +2670,14 @@ var Ne = class extends w.PluginSettingTab { "--json", ]; ((this._refreshPending = !0), - (0, N.execFile)( + (0, j.execFile)( t.path, r, { cwd: e, timeout: 3e4, windowsHide: !0 }, (n, a, s) => { ((this._refreshPending = !1), - (this._memoryStatusText = Oe(e)), - (this._embedStatusText = ye(e)), + (this._memoryStatusText = Ie(e)), + (this._embedStatusText = be(e)), this.display()); } )); @@ -2654,9 +2685,9 @@ var Ne = class extends w.PluginSettingTab { _renderFeaturesTab(e) { e.createEl("h3", { text: "Skills" }); let t = e.createEl("div", { cls: "paperforge-desc-box" }); - (t.setText(l("feat_skills_desc")), + (t.setText(o("feat_skills_desc")), t.createEl("br"), - t.createEl("span", { text: l("feat_skills_system") })); + t.createEl("span", { text: o("feat_skills_system") })); let r = { opencode: "OpenCode", claude: "Claude Code", @@ -2677,124 +2708,124 @@ var Ne = class extends w.PluginSettingTab { }, a = this.app.vault.adapter.basePath, s = this.plugin.settings.selected_skill_platform || "opencode"; - new w.Setting(e) - .setName(l("feat_agent_platform")) - .setDesc(l("feat_agent_platform_desc")) - .addDropdown((y) => { - (Object.entries(r).forEach(([b, k]) => y.addOption(b, k)), - y.setValue(s).onChange((b) => { - ((this.plugin.settings.selected_skill_platform = b), + new C.Setting(e) + .setName(o("feat_agent_platform")) + .setDesc(o("feat_agent_platform_desc")) + .addDropdown((g) => { + (Object.entries(r).forEach(([v, k]) => g.addOption(v, k)), + g.setValue(s).onChange((v) => { + ((this.plugin.settings.selected_skill_platform = v), this.plugin.saveSettings(), this.display()); })); }) - .addExtraButton((y) => { - y.setIcon("folder") + .addExtraButton((g) => { + g.setIcon("folder") .setTooltip("Open skills folder") .onClick(() => { - let b = n[s] || ".opencode/skills", - k = Z.join(a, b); - I.existsSync(k) - ? (0, N.exec)(`start "" "${k}"`) - : new w.Notice(`Skills folder not found: ${b}`); + let v = n[s] || ".opencode/skills", + k = Z.join(a, v); + N.existsSync(k) + ? (0, j.exec)(`start "" "${k}"`) + : new C.Notice(`Skills folder not found: ${v}`); }); }); - let o = Z.join(a, n[s]), + let l = Z.join(a, n[s]), i = [], c = []; - I.existsSync(o) && - I.readdirSync(o, { withFileTypes: !0 }).forEach((y) => { - if (!y.isDirectory()) return; - let b = Z.join(o, y.name, "SKILL.md"); - if (!I.existsSync(b)) return; - let k = I.readFileSync(b, "utf-8"), - C = k.match(/^name:\s*(.+)$/m), - T = k.split(` + N.existsSync(l) && + N.readdirSync(l, { withFileTypes: !0 }).forEach((g) => { + if (!g.isDirectory()) return; + let v = Z.join(l, g.name, "SKILL.md"); + if (!N.existsSync(v)) return; + let k = N.readFileSync(v, "utf-8"), + S = k.match(/^name:\s*(.+)$/m), + P = k.split(` `), - O = T.findIndex((W) => /^description:/.test(W)), - D = ""; - if (O >= 0) { - let W = T[O].match(/^description:\s*(.+)$/); - if (W && W[1] && W[1] !== ">" && W[1] !== "|-" && W[1] !== "|") - D = W[1].trim(); + A = P.findIndex((q) => /^description:/.test(q)), + T = ""; + if (A >= 0) { + let q = P[A].match(/^description:\s*(.+)$/); + if (q && q[1] && q[1] !== ">" && q[1] !== "|-" && q[1] !== "|") + T = q[1].trim(); else { for ( - let P = O + 1; - P < T.length && (/^\s{2,}/.test(T[P]) || T[P].trim() === ""); - P++ + let F = A + 1; + F < P.length && (/^\s{2,}/.test(P[F]) || P[F].trim() === ""); + F++ ) - D += T[P].trim() + " "; - D = D.trim(); + T += P[F].trim() + " "; + T = T.trim(); } } - let z = k.match(/^source:\s*(.+)$/m), - j = k.match(/^disable-model-invocation:\s*(.+)$/m), - B = k.match(/^version:\s*(.+)$/m), - X = { - name: C ? C[1].trim() : y.name, - desc: D, - source: z ? z[1].trim() : "user", - disabled: j && j[1].trim() === "true", - version: B ? B[1].trim() : "", - path: b, + let I = k.match(/^source:\s*(.+)$/m), + z = k.match(/^disable-model-invocation:\s*(.+)$/m), + O = k.match(/^version:\s*(.+)$/m), + U = { + name: S ? S[1].trim() : g.name, + desc: T, + source: I ? I[1].trim() : "user", + disabled: z && z[1].trim() === "true", + version: O ? O[1].trim() : "", + path: v, content: k, - dirName: y.name, + dirName: g.name, }; - X.source === "paperforge" ? i.push(X) : c.push(X); + U.source === "paperforge" ? i.push(U) : c.push(U); }); let d = e.createEl("div", { cls: "paperforge-skills-box" }), - u = (y, b, k) => { - if (b.length === 0) return; - let C = d.createEl("div", { cls: "paperforge-skills-group" }), - T = C.createEl("div", { cls: "paperforge-skills-collapse-header" }), - O = C.createEl("div", { cls: "paperforge-skills-collapse-content" }), - D = T.createEl("span", { + h = (g, v, k) => { + if (v.length === 0) return; + let S = d.createEl("div", { cls: "paperforge-skills-group" }), + P = S.createEl("div", { cls: "paperforge-skills-collapse-header" }), + A = S.createEl("div", { cls: "paperforge-skills-collapse-content" }), + T = P.createEl("span", { text: "\u25BC", cls: "paperforge-skills-arrow", }); - (T.createEl("h4", { - text: `${y} (${b.length})`, + (P.createEl("h4", { + text: `${g} (${v.length})`, cls: "paperforge-skills-subheader", }), - b.forEach((B) => { - let X = B.name + (B.version ? " v" + B.version : ""), - W = k ? " [system]" : " [user]", - P = B.desc || "", - R = new w.Setting(O).setName(X + W).setDesc(P); - ((R.settingEl.style.opacity = B.disabled ? "0.4" : "1"), - R.addToggle((U) => { - U.setValue(!B.disabled).onChange((Y) => { - let ie = !Y, - pe = B.content.match(/^disable-model-invocation:\s*(.+)$/m) - ? B.content.replace( + v.forEach((O) => { + let U = O.name + (O.version ? " v" + O.version : ""), + q = k ? " [system]" : " [user]", + F = O.desc || "", + D = new C.Setting(A).setName(U + q).setDesc(F); + ((D.settingEl.style.opacity = O.disabled ? "0.4" : "1"), + D.addToggle((X) => { + X.setValue(!O.disabled).onChange((ee) => { + let oe = !ee, + de = O.content.match(/^disable-model-invocation:\s*(.+)$/m) + ? O.content.replace( /^disable-model-invocation:\s*.+$/m, - `disable-model-invocation: ${ie}` + `disable-model-invocation: ${oe}` ) - : B.content.replace( + : O.content.replace( /^(---\r?\n)/, - `$1disable-model-invocation: ${ie} + `$1disable-model-invocation: ${oe} ` ); - (I.writeFileSync(B.path, pe, "utf-8"), - (B.disabled = ie), - (B.content = pe), - (R.settingEl.style.opacity = B.disabled ? "0.4" : "1")); + (N.writeFileSync(O.path, de, "utf-8"), + (O.disabled = oe), + (O.content = de), + (D.settingEl.style.opacity = O.disabled ? "0.4" : "1")); }); })); })); - let z = k ? "system" : "user"; - ((this._skillsCollapsed[z] || !1) && - ((O.style.display = "none"), (D.style.transform = "rotate(-90deg)")), - T.addEventListener("click", () => { - (O.style.display !== "none" - ? ((O.style.display = "none"), - (D.style.transform = "rotate(-90deg)")) - : ((O.style.display = ""), (D.style.transform = "rotate(0deg)")), - (this._skillsCollapsed[z] = O.style.display === "none")); + let I = k ? "system" : "user"; + ((this._skillsCollapsed[I] || !1) && + ((A.style.display = "none"), (T.style.transform = "rotate(-90deg)")), + P.addEventListener("click", () => { + (A.style.display !== "none" + ? ((A.style.display = "none"), + (T.style.transform = "rotate(-90deg)")) + : ((A.style.display = ""), (T.style.transform = "rotate(0deg)")), + (this._skillsCollapsed[I] = A.style.display === "none")); })); }; - (u("System Skills", i, !0), - u("User Skills", c, !1), + (h("System Skills", i, !0), + h("User Skills", c, !1), i.length === 0 && c.length === 0 && d.createEl("p", { @@ -2803,44 +2834,44 @@ var Ne = class extends w.PluginSettingTab { }), this._advCollapsed === void 0 && (this._advCollapsed = !0)); let p = e.createEl("div", { cls: "paperforge-collapsible-header" }), - _ = p.createEl("span", { + m = p.createEl("span", { text: "\u25B6", cls: "paperforge-collapsible-arrow", }); - _.style.transform = this._advCollapsed ? "rotate(0deg)" : "rotate(90deg)"; - let m = p.createEl("span", { + m.style.transform = this._advCollapsed ? "rotate(0deg)" : "rotate(90deg)"; + let _ = p.createEl("span", { cls: "paperforge-collapsible-title", text: "Advanced", }), - E = p.createEl("span", { + x = p.createEl("span", { cls: "paperforge-collapsible-sub", text: "Memory + Vector DB + Embedding", }), - f = e.createEl("div", { cls: "paperforge-collapsible-content" }); - ((f.style.display = this._advCollapsed ? "none" : ""), + y = e.createEl("div", { cls: "paperforge-collapsible-content" }); + ((y.style.display = this._advCollapsed ? "none" : ""), p.addEventListener("click", () => { ((this._advCollapsed = !this._advCollapsed), - (f.style.display = this._advCollapsed ? "none" : ""), - (_.style.transform = this._advCollapsed + (y.style.display = this._advCollapsed ? "none" : ""), + (m.style.transform = this._advCollapsed ? "rotate(0deg)" : "rotate(90deg)")); }), - f.createEl("h4", { text: "Memory Layer" }), - f + y.createEl("h4", { text: "Memory Layer" }), + y .createEl("div", { cls: "paperforge-desc-box" }) - .setText(l("feat_memory_desc"))); - let S = f.createEl("div", { cls: "paperforge-memory-status" }), - x = this.app.vault.adapter.basePath; + .setText(o("feat_memory_desc"))); + let w = y.createEl("div", { cls: "paperforge-memory-status" }), + E = this.app.vault.adapter.basePath; (this.plugin._lastSyncTime && !this._lastSyncTime && (this._lastSyncTime = this.plugin._lastSyncTime), - this._memoryStatusText === null && (this._memoryStatusText = Oe(x)), + this._memoryStatusText === null && (this._memoryStatusText = Ie(E)), this._renderMemoryStatusText( - S, + w, this._memoryStatusText, this._lastSyncTime ), - this._renderVectorSection(f)); + this._renderVectorSection(y)); } _renderVectorSection(e) { var i; @@ -2850,10 +2881,10 @@ var Ne = class extends w.PluginSettingTab { (this.plugin.settings.features = { memory_layer: !0, vector_db: !1 }), e .createEl("div", { cls: "paperforge-desc-box" }) - .setText(l("feat_vector_desc")), - new w.Setting(e) - .setName(l("feat_vector_enable")) - .setDesc(l("feat_vector_enable_desc")) + .setText(o("feat_vector_desc")), + new C.Setting(e) + .setName(o("feat_vector_enable")) + .setDesc(o("feat_vector_enable_desc")) .addToggle((c) => { c.setValue(!!this.plugin.settings.features.vector_db).onChange( (d) => { @@ -2876,18 +2907,18 @@ var Ne = class extends w.PluginSettingTab { }); n.createEl("span", { cls: "paperforge-vec-header-label", - text: l("feat_vector_config_label"), + text: o("feat_vector_config_label"), }); let s = e.createEl("div", { cls: "paperforge-vector-config" }), - o = (c) => { + l = (c) => { ((s.style.display = c ? "none" : ""), (a.style.transform = c ? "rotate(-90deg)" : "rotate(0deg)")); }; if ( - (o(Ge(this._featurePanelsCollapsed, "vectorConfig", !1)), + (l(Qe(this._featurePanelsCollapsed, "vectorConfig", !1)), n.addEventListener("click", () => { - let c = pt(this._featurePanelsCollapsed, "vectorConfig", !1); - o(c); + let c = ht(this._featurePanelsCollapsed, "vectorConfig", !1); + l(c); }), this._vectorDepsOk === !0) ) { @@ -2899,16 +2930,16 @@ var Ne = class extends w.PluginSettingTab { return; } if (this._vectorDepsOk === null) { - let c = Ce(r); + let c = Re(r); ((this._vectorDepsOk = c && (i = c.deps_installed) != null ? i : !1), - this._vectorDepsOk && (this._embedStatusText = ye(r)), + this._vectorDepsOk && (this._embedStatusText = be(r)), this.display()); } } _renderApiConfig(e) { - (new w.Setting(e) - .setName(l("feat_openai_key")) - .setDesc(l("feat_openai_key_desc")) + (new C.Setting(e) + .setName(o("feat_openai_key")) + .setDesc(o("feat_openai_key_desc")) .addText((t) => { t.setPlaceholder("sk-...") .setValue(this.plugin.settings.vector_db_api_key || "") @@ -2917,9 +2948,9 @@ var Ne = class extends w.PluginSettingTab { this.plugin.saveSettings()); }); }), - new w.Setting(e) - .setName(l("feat_api_base_url")) - .setDesc(l("feat_api_base_url_desc")) + new C.Setting(e) + .setName(o("feat_api_base_url")) + .setDesc(o("feat_api_base_url_desc")) .addText((t) => { t.setPlaceholder("https://api.openai.com/v1") .setValue(this.plugin.settings.vector_db_api_base || "") @@ -2928,9 +2959,9 @@ var Ne = class extends w.PluginSettingTab { this.plugin.saveSettings()); }); }), - new w.Setting(e) - .setName(l("feat_api_model")) - .setDesc(l("feat_api_model_desc")) + new C.Setting(e) + .setName(o("feat_api_model")) + .setDesc(o("feat_api_model_desc")) .addText((t) => { t.setPlaceholder("text-embedding-3-small") .setValue( @@ -2946,24 +2977,24 @@ var Ne = class extends w.PluginSettingTab { _renderVectorNoDeps(e) { (e .createEl("div", { cls: "paperforge-desc-box" }) - .setText(l("feat_deps_missing")), - new w.Setting(e) - .setName(l("feat_install_deps")) - .setDesc(l("feat_install_deps_desc")) + .setText(o("feat_deps_missing")), + new C.Setting(e) + .setName(o("feat_install_deps")) + .setDesc(o("feat_install_deps_desc")) .addButton((r) => { - r.setButtonText(l("feat_install_btn")) + r.setButtonText(o("feat_install_btn")) .setCta() .onClick(async () => { let n = this.app.vault.adapter.basePath, - a = le(n, this.plugin.settings); + a = ce(n, this.plugin.settings); if (!a.path) { - new w.Notice(l("feat_no_python")); + new C.Notice(o("feat_no_python")); return; } - (r.setButtonText(l("feat_installing")), r.setDisabled(!0)); + (r.setButtonText(o("feat_installing")), r.setDisabled(!0)); let s = "chromadb openai", - o = new w.Notice( - l("feat_installing_pkgs").replace("{pkgs}", s), + l = new C.Notice( + o("feat_installing_pkgs").replace("{pkgs}", s), 0 ); try { @@ -2972,46 +3003,46 @@ var Ne = class extends w.PluginSettingTab { PYTHONUTF8: "1", }), c = s.split(" "); - (await new Promise((d, u) => { - (0, N.execFile)( + (await new Promise((d, h) => { + (0, j.execFile)( a.path, [...a.extraArgs, "-m", "pip", "install", ...c], { cwd: n, timeout: 3e5, env: i, windowsHide: !0 }, (p) => { - p ? u(p) : d(); + p ? h(p) : d(); } ); }), - o.hide(), - new w.Notice(l("feat_install_done")), + l.hide(), + new C.Notice(o("feat_install_done")), (this._vectorDepsOk = !0), - (this._embedStatusText = ye(n)), + (this._embedStatusText = be(n)), this.display()); } catch (i) { - (o.hide(), - new w.Notice( - l("feat_install_failed") + (i.stderr || i.message || i) + (l.hide(), + new C.Notice( + o("feat_install_failed") + (i.stderr || i.message || i) ), - r.setButtonText(l("feat_retry_btn")), + r.setButtonText(o("feat_retry_btn")), r.setDisabled(!1)); } }); })); } _renderVectorReady(e, t) { - (e.createEl("div", { cls: "paperforge-desc-box" }).setText(ye(t)), + (e.createEl("div", { cls: "paperforge-desc-box" }).setText(be(t)), this._renderApiConfig(e)); let n = e.createEl("div", { cls: "paperforge-embed-section" }); n.createEl("div", { cls: "paperforge-embed-header" }).createEl("span", { - text: l("feat_rebuild_vectors"), + text: o("feat_rebuild_vectors"), cls: "setting-item-name", }); let s = n.createEl("div", { cls: "paperforge-embed-controls" }), - o = n.createEl("div", { cls: "paperforge-embed-status-text" }); + l = n.createEl("div", { cls: "paperforge-embed-status-text" }); (() => { - var m, E, f; - (s.empty(), o.empty()); - let c = (Ce(t) || {}).build_state || {}; + var _, x, y; + (s.empty(), l.empty()); + let c = (Re(t) || {}).build_state || {}; ((this.plugin._embedProgress = this.plugin._embedProgress || { current: 0, total: 0, @@ -3024,32 +3055,32 @@ var Ne = class extends w.PluginSettingTab { total: c.total || 1, key: c.paper_id || "", })); - let { current: d, total: u, key: p } = this.plugin._embedProgress; + let { current: d, total: h, key: p } = this.plugin._embedProgress; if (!!this.plugin._embedProcess || c.status === "running") { - let v = s.createEl("div", { cls: "paperforge-progress-track" }); - v.style.cssText = "flex:1;"; - let S = u > 0 ? ((d / u) * 100).toFixed(1) : "0", - x = v.createEl("div", { cls: "paperforge-progress-seg done" }); + let b = s.createEl("div", { cls: "paperforge-progress-track" }); + b.style.cssText = "flex:1;"; + let w = h > 0 ? ((d / h) * 100).toFixed(1) : "0", + E = b.createEl("div", { cls: "paperforge-progress-seg done" }); if ( - ((x.style.cssText = `width:${S}%; min-width:${d > 0 ? "2px" : "0"};`), - d < u) + ((E.style.cssText = `width:${w}%; min-width:${d > 0 ? "2px" : "0"};`), + d < h) ) { - let b = v.createEl("div", { cls: "paperforge-progress-seg pending" }); - b.style.cssText = `width:${(100 - parseFloat(S)).toFixed(1)}%;`; + let v = b.createEl("div", { cls: "paperforge-progress-seg pending" }); + v.style.cssText = `width:${(100 - parseFloat(w)).toFixed(1)}%;`; } - (o.createEl("span", { + (l.createEl("span", { cls: "paperforge-embed-progress-text", - text: `${d}/${u} papers`, + text: `${d}/${h} papers`, }), p && - o.createEl("span", { + l.createEl("span", { cls: "paperforge-embed-progress-key", text: ` (${p})`, })); - let y = s.createEl("button"); - (y.setText("Stop"), - (y.className = "mod-warning"), - y.addEventListener("click", () => { + let g = s.createEl("button"); + (g.setText("Stop"), + (g.className = "mod-warning"), + g.addEventListener("click", () => { (this._callPython(["embed", "stop", "--json"], { timeout: 8e3 }), this.plugin._embedProcess && (this.plugin._embedProcess.kill(), @@ -3057,19 +3088,19 @@ var Ne = class extends w.PluginSettingTab { this.display()); })); } else { - let v = Ce(t), - S = - ((m = v == null ? void 0 : v.chunk_count) != null ? m : 0) + - ((E = v == null ? void 0 : v.body_chunk_count) != null ? E : 0) + - ((f = v == null ? void 0 : v.object_chunk_count) != null ? f : 0), - x = S > 0, - y = v ? !!v.corrupted : !1, - b = (C) => { - if (!le(t, this.plugin.settings).path) { - new w.Notice(l("feat_no_python")); + let b = Re(t), + w = + ((_ = b == null ? void 0 : b.chunk_count) != null ? _ : 0) + + ((x = b == null ? void 0 : b.body_chunk_count) != null ? x : 0) + + ((y = b == null ? void 0 : b.object_chunk_count) != null ? y : 0), + E = w > 0, + g = b ? !!b.corrupted : !1, + v = (S) => { + if (!ce(t, this.plugin.settings).path) { + new C.Notice(o("feat_no_python")); return; } - let O = Object.assign({}, process.env, { + let A = Object.assign({}, process.env, { PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1", VECTOR_DB_API_KEY: this.plugin.settings.vector_db_api_key || "", @@ -3080,53 +3111,53 @@ var Ne = class extends w.PluginSettingTab { ((this.plugin._embedStderr = ""), (this.plugin._embedProgress = { current: 0, total: 0, key: "" }), (this.plugin._embedProcess = this._callPython( - ["embed", "build", C], + ["embed", "build", S], { stream: !0, - env: O, - onData: (D) => { - let z = D.toString("utf-8").split(` + env: A, + onData: (T) => { + let I = T.toString("utf-8").split(` `); - for (let j of z) - if (j.startsWith("EMBED_START:")) + for (let z of I) + if (z.startsWith("EMBED_START:")) this.plugin._embedProgress.total = - parseInt(j.split(":")[1]) || 0; - else if (j.startsWith("EMBED_PROGRESS:")) { - let B = j.split(":"); + parseInt(z.split(":")[1]) || 0; + else if (z.startsWith("EMBED_PROGRESS:")) { + let O = z.split(":"); ((this.plugin._embedProgress.current = - parseInt(B[1]) || 0), - (this.plugin._embedProgress.key = B[3] || "")); + parseInt(O[1]) || 0), + (this.plugin._embedProgress.key = O[3] || "")); } else - j.startsWith("EMBED_DONE") && + z.startsWith("EMBED_DONE") && ((this.plugin._embedProcess = null), (this.plugin._embedProgress.current = this.plugin._embedProgress.total)); this.display(); }, - onStderr: (D) => { + onStderr: (T) => { (this.plugin._embedStderr || (this.plugin._embedStderr = ""), - (this.plugin._embedStderr += D.toString("utf-8"))); + (this.plugin._embedStderr += T.toString("utf-8"))); }, - onError: (D) => { + onError: (T) => { ((this.plugin._embedProcess = null), - new w.Notice( - l("feat_build_failed") + ": " + (D.message || D) + new C.Notice( + o("feat_build_failed") + ": " + (T.message || T) ), this.display()); }, - onClose: (D) => { - if (((this.plugin._embedProcess = null), D === 0)) + onClose: (T) => { + if (((this.plugin._embedProcess = null), T === 0)) ((this.plugin._embedProgress.current = this.plugin._embedProgress.total), this.plugin.saveSettings(), - (this._embedStatusText = ye(t)), - new w.Notice(l("feat_build_complete"))); + (this._embedStatusText = be(t)), + new C.Notice(o("feat_build_complete"))); else { this._embedStatusText = null; - let z = (this.plugin._embedStderr || "").slice(0, 200); - new w.Notice( - l("feat_build_failed") + (z ? ": " + z : ""), + let I = (this.plugin._embedStderr || "").slice(0, 200); + new C.Notice( + o("feat_build_failed") + (I ? ": " + I : ""), 8e3 ); } @@ -3138,34 +3169,34 @@ var Ne = class extends w.PluginSettingTab { )), this.display()); }; - if (y) { - let C = n.createEl("div"); - ((C.style.cssText = + if (g) { + let S = n.createEl("div"); + ((S.style.cssText = "padding:8px 12px; margin:8px 0; background:var(--background-modifier-warning); border-radius:4px; font-size:12px; display:flex; align-items:center; justify-content:space-between;"), - C.createEl("span", { text: l("feat_vector_corrupted") })); - let T = C.createEl("button", { - text: l("feat_vector_rebuild_force_btn"), + S.createEl("span", { text: o("feat_vector_corrupted") })); + let P = S.createEl("button", { + text: o("feat_vector_rebuild_force_btn"), }); - ((T.className = "mod-cta"), - T.addEventListener("click", () => b("--force"))); + ((P.className = "mod-cta"), + P.addEventListener("click", () => v("--force"))); } - x && - !y && + E && + !g && s.createEl("span", { - text: S + " chunks embedded", + text: w + " chunks embedded", cls: "setting-item-description", }); let k = s.createEl("button"); if ( - (k.setText(x ? l("feat_rebuild_btn") : l("feat_build_btn")), + (k.setText(E ? o("feat_rebuild_btn") : o("feat_build_btn")), k.addClass("mod-cta"), - k.addEventListener("click", () => b("--resume")), - !y && x) + k.addEventListener("click", () => v("--resume")), + !g && E) ) { - let C = s.createEl("button"); - (C.setText(l("feat_vector_rebuild_force_btn")), - (C.style.marginLeft = "6px"), - C.addEventListener("click", () => b("--force"))); + let S = s.createEl("button"); + (S.setText(o("feat_vector_rebuild_force_btn")), + (S.style.marginLeft = "6px"), + S.addEventListener("click", () => v("--force"))); } } })(); @@ -3217,31 +3248,31 @@ var Ne = class extends w.PluginSettingTab { let r = "\u8BF7\u8F93\u5165\u8DEF\u5F84 / Enter a path first"; (t && (t.innerHTML = `\u2717 ${r}`), - new w.Notice(r)); + new C.Notice(r)); return; } - if (!I.existsSync(e)) { + if (!N.existsSync(e)) { let r = "\u8DEF\u5F84\u4E0D\u5B58\u5728 / Path does not exist"; (t && (t.innerHTML = `\u2717 ${r}`), - new w.Notice(r, 4e3)); + new C.Notice(r, 4e3)); return; } try { - I.accessSync(e, I.constants.X_OK); + N.accessSync(e, N.constants.X_OK); } catch (r) { let n = "\u4E0D\u53EF\u6267\u884C / Not executable"; (t && (t.innerHTML = `\u2717 ${n}`), - new w.Notice(n, 4e3)); + new C.Notice(n, 4e3)); return; } - (0, N.execFile)(e, ["--version"], { timeout: 8e3 }, (r, n) => { + (0, j.execFile)(e, ["--version"], { timeout: 8e3 }, (r, n) => { if (r || !n) { let i = "\u65E0\u6CD5\u8FD0\u884C / Cannot run"; (t && (t.innerHTML = `\u2717 ${i}`), - new w.Notice(i, 4e3)); + new C.Notice(i, 4e3)); return; } let a = n.match(/Python (\d+)\.(\d+)/); @@ -3249,30 +3280,30 @@ var Ne = class extends w.PluginSettingTab { let i = "\u65E0\u6CD5\u89E3\u6790\u7248\u672C / Cannot parse version"; (t && (t.innerHTML = `\u2717 ${i}`), - new w.Notice(i, 4e3)); + new C.Notice(i, 4e3)); return; } let s = parseInt(a[1], 10), - o = parseInt(a[2], 10); - if (s < 3 || (s === 3 && o < 10)) { + l = parseInt(a[2], 10); + if (s < 3 || (s === 3 && l < 10)) { let i = "Python \u7248\u672C\u8FC7\u4F4E\uFF0C\u9700\u8981 3.10+ / Python version too low, need 3.10+"; (t && (t.innerHTML = `\u2717 ${i}`), - new w.Notice(i, 4e3)); + new C.Notice(i, 4e3)); return; } - (0, N.execFile)(e, ["-m", "pip", "--version"], { timeout: 8e3 }, (i) => { + (0, j.execFile)(e, ["-m", "pip", "--version"], { timeout: 8e3 }, (i) => { if (i) { - let c = `\u2713 Python ${s}.${o} \u6709\u6548\uFF0C\u4F46\u672A\u68C0\u6D4B\u5230 pip / Valid, but pip not found`; + let c = `\u2713 Python ${s}.${l} \u6709\u6548\uFF0C\u4F46\u672A\u68C0\u6D4B\u5230 pip / Valid, but pip not found`; (t && (t.innerHTML = `\u26A0 ${c}`), - new w.Notice(c, 4e3)); + new C.Notice(c, 4e3)); } else { - let c = `\u2713 Python ${s}.${o} \u6709\u6548 / Valid`; + let c = `\u2713 Python ${s}.${l} \u6709\u6548 / Valid`; (t && (t.innerHTML = `${c}`), - new w.Notice(c, 4e3)); + new C.Notice(c, 4e3)); } }); }); @@ -3286,19 +3317,19 @@ var Ne = class extends w.PluginSettingTab { void 0 ), a = this.plugin.manifest.version, - s = at(r, a, n); - (e.setDisabled(!0), e.setButtonText(l("runtime_health_syncing"))); - let o = (c, d) => ( + s = ct(r, a, n); + (e.setDisabled(!0), e.setButtonText(o("runtime_health_syncing"))); + let l = (c, d) => ( console.log(`[PaperForge] Sync Runtime: trying ${d}`), - it(s.cmd, c, t, s.timeout, void 0, fe()) + pt(s.cmd, c, t, s.timeout, void 0, me()) ), i = () => { let c = "opencode"; try { - let _ = I.readFileSync(Z.join(t, "paperforge.json"), "utf-8"), - m = JSON.parse(_); - m.agent_key && (c = m.agent_key); - } catch (_) {} + let m = N.readFileSync(Z.join(t, "paperforge.json"), "utf-8"), + _ = JSON.parse(m); + _.agent_key && (c = _.agent_key); + } catch (m) {} let d = [ ...n, "-c", @@ -3308,40 +3339,40 @@ var Ne = class extends w.PluginSettingTab { c + '", overwrite=True); print("skills deployed" if r["skill_deployed"] else "skills skipped", flush=True)', ], - u = (0, N.spawn)(r, d, { cwd: t, timeout: 3e4, windowsHide: !0 }), + h = (0, j.spawn)(r, d, { cwd: t, timeout: 3e4, windowsHide: !0 }), p = ""; - (u.stdout.on("data", (_) => { - p += _.toString("utf-8"); + (h.stdout.on("data", (m) => { + p += m.toString("utf-8"); }), - u.on("close", (_) => { - console.log(`[PaperForge] Skill deploy: ${p.trim()} (exit ${_})`); + h.on("close", (m) => { + console.log(`[PaperForge] Skill deploy: ${p.trim()} (exit ${m})`); })); }; - o(s.pypiArgs, "PyPI").then((c) => { + l(s.pypiArgs, "PyPI").then((c) => { if (c.exitCode === 0) { (console.log("[PaperForge] Sync Runtime: installed via PyPI"), i(), - new w.Notice(l("runtime_health_sync_done").replace("{0}", a), 5e3), + new C.Notice(o("runtime_health_sync_done").replace("{0}", a), 5e3), this.display()); return; } (console.warn( "[PaperForge] Sync Runtime: PyPI failed, falling back to git..." ), - o(s.gitArgs, "git").then((d) => { + l(s.gitArgs, "git").then((d) => { d.exitCode === 0 ? (console.log("[PaperForge] Sync Runtime: installed via git"), i(), - new w.Notice( - l("runtime_health_sync_done").replace("{0}", a), + new C.Notice( + o("runtime_health_sync_done").replace("{0}", a), 5e3 ), this.display()) : (e.setDisabled(!1), - e.setButtonText(l("runtime_health_sync")), + e.setButtonText(o("runtime_health_sync")), console.error("[PaperForge] git fallback stderr:", d.stderr), - new w.Notice( - l("runtime_health_sync_fail").replace( + new C.Notice( + o("runtime_health_sync_fail").replace( "{0}", "pip exit code " + d.exitCode ), @@ -3363,41 +3394,41 @@ var Ne = class extends w.PluginSettingTab { void 0, void 0 ); - (0, N.execFile)(r, [...n, "--version"], { timeout: 8e3 }, (s, o) => { + (0, j.execFile)(r, [...n, "--version"], { timeout: 8e3 }, (s, l) => { let i = []; i.push({ label: "Python", ok: !s, - detail: s ? l("check_python_fail") : o.trim(), + detail: s ? o("check_python_fail") : l.trim(), }); let c = !1, - d = process.env.HOME || process.env.USERPROFILE || bt.homedir() || ""; + d = process.env.HOME || process.env.USERPROFILE || kt.homedir() || ""; if (process.platform === "darwin") c = [ "/Applications/Zotero.app", Z.join(d, "Applications", "Zotero.app"), - ].some((v) => { + ].some((b) => { try { - return I.existsSync(v); - } catch (S) { + return N.existsSync(b); + } catch (w) { return !1; } }); else if (process.platform === "win32") { - let f = process.env.ProgramFiles || "", - v = process.env.LOCALAPPDATA || ""; + let y = process.env.ProgramFiles || "", + b = process.env.LOCALAPPDATA || ""; c = [ - Z.join(f, "Zotero"), - Z.join(f, "(x86)", "Zotero"), - Z.join(v, "Programs", "Zotero"), - Z.join(v, "Zotero"), + Z.join(y, "Zotero"), + Z.join(y, "(x86)", "Zotero"), + Z.join(b, "Programs", "Zotero"), + Z.join(b, "Zotero"), Z.join(d, "AppData", "Local", "Programs", "Zotero"), ] .filter(Boolean) - .some((x) => { + .some((E) => { try { - return I.existsSync(x); - } catch (y) { + return N.existsSync(E); + } catch (g) { return !1; } }); @@ -3406,82 +3437,82 @@ var Ne = class extends w.PluginSettingTab { Z.join(d, ".local", "share", "zotero", "zotero"), "/usr/bin/zotero", "/usr/local/bin/zotero", - ].some((v) => { + ].some((b) => { try { - return I.existsSync(v); - } catch (S) { + return N.existsSync(b); + } catch (w) { return !1; } }); - let u = this.plugin.settings.zotero_data_dir; - if (!c && u) + let h = this.plugin.settings.zotero_data_dir; + if (!c && h) try { - c = I.existsSync(u); - } catch (f) {} + c = N.existsSync(h); + } catch (y) {} i.push({ label: "Zotero", ok: c, - detail: c ? l("check_zotero_ok") : l("check_zotero_fail"), + detail: c ? o("check_zotero_ok") : o("check_zotero_fail"), }); let p = !1, - _ = process.env.APPDATA || ""; + m = process.env.APPDATA || ""; (process.platform === "win32" && - _ && - (p = Ae(Z.join(_, "Zotero", "Zotero", "Profiles"))), + m && + (p = Le(Z.join(m, "Zotero", "Zotero", "Profiles"))), !p && process.platform === "darwin" && d && - (p = Ae( + (p = Le( Z.join(d, "Library", "Application Support", "Zotero", "Profiles") )), !p && process.platform !== "win32" && process.platform !== "darwin" && d && - (p = Ae(Z.join(d, ".zotero", "zotero", "Profiles"))), - !p && u && String(u).trim() && (p = qe(u.trim())), - !p && d && (p = qe(Z.join(d, "Zotero"))), + (p = Le(Z.join(d, ".zotero", "zotero", "Profiles"))), + !p && h && String(h).trim() && (p = Ge(h.trim())), + !p && d && (p = Ge(Z.join(d, "Zotero"))), i.push({ label: "Better BibTeX", ok: p, - detail: p ? l("check_bbt_ok") : l("check_bbt_fail"), + detail: p ? o("check_bbt_ok") : o("check_bbt_fail"), })); - let m = { true: "\u2713", false: "\u2717" }; + let _ = { true: "\u2713", false: "\u2717" }; if (this._checkEl) { this._checkEl.setText( - i.map((v) => `${m[String(v.ok)]} ${v.label}: ${v.detail}`).join(` + i.map((b) => `${_[String(b.ok)]} ${b.label}: ${b.detail}`).join(` `) ); - let f = i.some((v) => !v.ok); - this._checkEl.className = `paperforge-message msg-${f ? "error" : "ok"}`; + let y = i.some((b) => !b.ok); + this._checkEl.className = `paperforge-message msg-${y ? "error" : "ok"}`; } - let E = i.filter((f) => !f.ok); - (E.length > 0 && - new w.Notice( - `[!!] \u672A\u901A\u8FC7: ${E.map((f) => f.label).join(", ")}`, + let x = i.filter((y) => !y.ok); + (x.length > 0 && + new C.Notice( + `[!!] \u672A\u901A\u8FC7: ${x.map((y) => y.label).join(", ")}`, 6e3 ), e()); }); } _renderMaintenanceTab(e) { - e.createEl("h2", { text: l("tab_maintenance") || "\u7EF4\u62A4" }); + e.createEl("h2", { text: o("tab_maintenance") || "\u7EF4\u62A4" }); let t = this.app.vault.adapter.basePath, r = e.createEl("div"), - n = (u, p) => + n = (h, p) => p === "retry_ocr" || p === "upgrade_legacy" - ? { cmd: ["-m", "paperforge", "ocr", "redo", ...u], timeout: 3e5 } + ? { cmd: ["-m", "paperforge", "ocr", "redo", ...h], timeout: 3e5 } : p === "rebuild_result" ? { - cmd: ["-m", "paperforge", "ocr", "rebuild", ...u], + cmd: ["-m", "paperforge", "ocr", "rebuild", ...h], timeout: 12e4, } : null, a = null; try { - a = mt(t); - } catch (u) {} - let s = M(t, this.plugin.settings, I, N.execFileSync); + a = xt(t); + } catch (h) {} + let s = M(t, this.plugin.settings, N, j.execFileSync); if (!s.path) { r.createEl("p", { text: "\u26A0 Python \u672A\u914D\u7F6E\uFF0C\u8BF7\u5148\u5728\u300C\u5B89\u88C5\u300D\u6807\u7B7E\u9875\u914D\u7F6E\u3002", @@ -3489,112 +3520,113 @@ var Ne = class extends w.PluginSettingTab { }); return; } - let o = (u) => { + let l = (h) => { r.empty(); - let p = u.filter((f) => f.visible_in_maintenance); + let p = h.filter((y) => y.visible_in_maintenance); if (p.length === 0) { r.createEl("p", { - text: l("maintenance_all_good") || "\u2705 \u5168\u90E8\u6B63\u5E38", + text: o("maintenance_all_good") || "\u2705 \u5168\u90E8\u6B63\u5E38", }); return; } - let _ = s.path, - m = s.extraArgs || [], - E = [ + let m = s.path, + _ = s.extraArgs || [], + x = [ { key: "retry", - title: l("maintenance_group_retry") || "\u9700\u8981\u91CD\u8BD5", + title: o("maintenance_group_retry") || "\u9700\u8981\u91CD\u8BD5", items: [], }, { key: "rebuild", title: - l("maintenance_group_rebuild") || + o("maintenance_group_rebuild") || "\u53EF\u91CD\u5EFA\u7ED3\u679C", items: [], }, { key: "legacy_optional", title: - l("maintenance_group_legacy") || + o("maintenance_group_legacy") || "\u53EF\u5347\u7EA7\u65E7\u7ED3\u679C\uFF08\u53EF\u9009\uFF09", items: [], }, ]; - for (let f of p) { - let v = E.find((S) => S.key === f.display_group); - v && v.items.push(f); + for (let y of p) { + let b = x.find((w) => w.key === y.display_group); + b && b.items.push(y); } - for (let f of E) { - if (f.items.length === 0) continue; - let v = f.key === "legacy_optional", - S = v ? r.createEl("details") : r.createEl("div"); - v - ? S.createEl("summary").createEl("strong", { - text: f.title + " (" + f.items.length + ")", - }) - : S.createEl("h3", { text: f.title + " (" + f.items.length + ")" }); - let x = new Map(); - for (let P of f.items) x.set(P.key, !1); - let y = S.createEl("div", { cls: "pf-maint-toolbar" }), - b = y.createEl("button", { text: "\u5168\u9009" }), - k = y.createEl("button", { text: "\u53D6\u6D88\u5168\u9009" }), - C = y.createEl("button", { + for (let y of x) { + if (y.items.length === 0) continue; + let b = y.key === "legacy_optional", + w = b ? r.createEl("details") : r.createEl("div"); + b + ? w + .createEl("summary") + .createEl("strong", { + text: y.title + " (" + y.items.length + ")", + }) + : w.createEl("h3", { text: y.title + " (" + y.items.length + ")" }); + let E = new Map(); + for (let F of y.items) E.set(F.key, !1); + let g = w.createEl("div", { cls: "pf-maint-toolbar" }), + v = g.createEl("button", { text: "\u5168\u9009" }), + k = g.createEl("button", { text: "\u53D6\u6D88\u5168\u9009" }), + S = g.createEl("button", { text: "\u25B6 \u6267\u884C\u5DF2\u9009", cls: "mod-cta", }), - T = y.createEl("span", { cls: "pf-maint-exec-label" }), - O = () => { - let P = f.items.filter((R) => x.get(R.key)).length; - T.setText("\u5DF2\u9009 " + P + " \u7BC7"); + P = g.createEl("span", { cls: "pf-maint-exec-label" }), + A = () => { + let F = y.items.filter((D) => E.get(D.key)).length; + P.setText("\u5DF2\u9009 " + F + " \u7BC7"); }; - (O(), - b.addEventListener("click", () => { - for (let R of f.items) x.set(R.key, !0); - (O(), - S.querySelectorAll("input[type=checkbox].pf-maint-sel").forEach( - (R) => { - R.checked = !0; - } - )); + (A(), + v.addEventListener("click", () => { + for (let D of y.items) E.set(D.key, !0); + (A(), + w + .querySelectorAll("input[type=checkbox].pf-maint-sel") + .forEach((D) => { + D.checked = !0; + })); }), k.addEventListener("click", () => { - for (let R of f.items) x.set(R.key, !1); - (O(), - S.querySelectorAll("input[type=checkbox].pf-maint-sel").forEach( - (R) => { - R.checked = !1; - } - )); + for (let D of y.items) E.set(D.key, !1); + (A(), + w + .querySelectorAll("input[type=checkbox].pf-maint-sel") + .forEach((D) => { + D.checked = !1; + })); }), - C.addEventListener("click", () => { - let P = f.items.filter((R) => x.get(R.key)); - if (P.length === 0) { - new w.Notice( + S.addEventListener("click", () => { + let F = y.items.filter((D) => E.get(D.key)); + if (F.length === 0) { + new C.Notice( "\u8BF7\u5148\u9009\u62E9\u8981\u5904\u7406\u7684\u8BBA\u6587\u3002" ); return; } - for (let R of P) { - let U = n([R.key], R.display_action); - U && - (0, N.execFile)( - _, - [...m, ...U.cmd], - { cwd: t, timeout: U.timeout, windowsHide: !0 }, + for (let D of F) { + let X = n([D.key], D.display_action); + X && + (0, j.execFile)( + m, + [..._, ...X.cmd], + { cwd: t, timeout: X.timeout, windowsHide: !0 }, () => { - new w.Notice(R.display_label + " \u2014 " + R.key); + new C.Notice(D.display_label + " \u2014 " + D.key); } ); } })); - let z = S.createEl("div", { cls: "pf-maint-table-wrap" }).createEl( - "table", - { cls: "pf-maint-table" } - ), - j = z.createEl("thead"), - B = z.createEl("tbody"), - X = j.insertRow(); + let I = w + .createEl("div", { cls: "pf-maint-table-wrap" }) + .createEl("table", { cls: "pf-maint-table" }), + z = I.createEl("thead"), + O = I.createEl("tbody"), + U = z.insertRow(); [ "", "Key", @@ -3602,88 +3634,88 @@ var Ne = class extends w.PluginSettingTab { "\u5EFA\u8BAE\u64CD\u4F5C", "\u539F\u56E0", "\u64CD\u4F5C", - ].forEach((P) => { - let R = document.createElement("th"); - ((R.textContent = P), X.appendChild(R)); + ].forEach((F) => { + let D = document.createElement("th"); + ((D.textContent = F), U.appendChild(D)); }); - let W = (P) => - P === "retry_ocr" - ? l("maintenance_btn_retry") || "\u91CD\u8BD5" - : P === "rebuild_result" - ? l("maintenance_btn_rebuild") || "\u91CD\u5EFA" - : P === "upgrade_legacy" - ? l("maintenance_btn_upgrade") || "\u5347\u7EA7" + let q = (F) => + F === "retry_ocr" + ? o("maintenance_btn_retry") || "\u91CD\u8BD5" + : F === "rebuild_result" + ? o("maintenance_btn_rebuild") || "\u91CD\u5EFA" + : F === "upgrade_legacy" + ? o("maintenance_btn_upgrade") || "\u5347\u7EA7" : ""; - for (let P of f.items) { - let R = B.insertRow(), - U = R.insertCell(); - U.style.cssText = "padding:3px 4px;text-align:center;"; - let Y = document.createElement("input"); - ((Y.type = "checkbox"), - (Y.className = "pf-maint-sel"), - (Y.checked = x.get(P.key) || !1), - Y.addEventListener("change", () => { - (x.set(P.key, Y.checked), O()); + for (let F of y.items) { + let D = O.insertRow(), + X = D.insertCell(); + X.style.cssText = "padding:3px 4px;text-align:center;"; + let ee = document.createElement("input"); + ((ee.type = "checkbox"), + (ee.className = "pf-maint-sel"), + (ee.checked = E.get(F.key) || !1), + ee.addEventListener("change", () => { + (E.set(F.key, ee.checked), A()); }), - U.appendChild(Y)); - let ie = R.insertCell(); - ((ie.style.cssText = + X.appendChild(ee)); + let oe = D.insertCell(); + ((oe.style.cssText = "padding:3px 4px;white-space:nowrap;font-size:11px;max-width:90px;overflow:hidden;text-overflow:ellipsis;"), - (ie.textContent = P.key)); - let me = R.insertCell(); - ((me.style.cssText = + (oe.textContent = F.key)); + let ve = D.insertCell(); + ((ve.style.cssText = "padding:3px 4px;white-space:nowrap;max-width:220px;overflow:hidden;text-overflow:ellipsis;"), - (me.textContent = P.title || P.key)); - let pe = R.insertCell(); - ((pe.style.cssText = "padding:3px 4px;white-space:nowrap;"), - (pe.textContent = P.display_label)); - let Ee = R.insertCell(); - ((Ee.style.cssText = + (ve.textContent = F.title || F.key)); + let de = D.insertCell(); + ((de.style.cssText = "padding:3px 4px;white-space:nowrap;"), + (de.textContent = F.display_label)); + let we = D.insertCell(); + ((we.style.cssText = "padding:3px 4px;white-space:nowrap;max-width:160px;overflow:hidden;text-overflow:ellipsis;font-size:11px;color:var(--text-muted);"), - (Ee.textContent = P.display_reason || "")); - let ke = R.insertCell(); - ke.style.cssText = + (we.textContent = F.display_reason || "")); + let Se = D.insertCell(); + Se.style.cssText = "padding:3px 4px;text-align:center;white-space:nowrap;"; - let he = document.createElement("button"); - ((he.textContent = W(P.display_action)), - he.textContent && - (he.addEventListener("click", () => { - let F = n([P.key], P.display_action); - F && - (0, N.execFile)( - _, - [...m, ...F.cmd], - { cwd: t, timeout: F.timeout, windowsHide: !0 }, + let ge = document.createElement("button"); + ((ge.textContent = q(F.display_action)), + ge.textContent && + (ge.addEventListener("click", () => { + let R = n([F.key], F.display_action); + R && + (0, j.execFile)( + m, + [..._, ...R.cmd], + { cwd: t, timeout: R.timeout, windowsHide: !0 }, () => { - new w.Notice(P.display_label + " \u2014 " + P.key); + new C.Notice(F.display_label + " \u2014 " + F.key); } ); }), - ke.appendChild(he))); + Se.appendChild(ge))); } } }; if (a) { - let u = Object.values(a.papers); - o(u); + let h = Object.values(a.papers); + l(h); } else r.createEl("p", { text: "\u6B63\u5728\u52A0\u8F7D OCR \u7EF4\u62A4\u6570\u636E\u2026", }); - (yt(t, s.path, s.extraArgs || [], a || null) - .then((u) => { - u.changed - ? (o(u.data), - Ie(t, { + (Et(t, s.path, s.extraArgs || [], a || null) + .then((h) => { + h.changed + ? (l(h.data), + $e(t, { manifest: {}, - papers: Object.fromEntries(u.data.map((p) => [p.key, p])), + papers: Object.fromEntries(h.data.map((p) => [p.key, p])), cached_at: new Date().toISOString(), })) : a || - (o(u.data), - Ie(t, { + (l(h.data), + $e(t, { manifest: {}, - papers: Object.fromEntries(u.data.map((p) => [p.key, p])), + papers: Object.fromEntries(h.data.map((p) => [p.key, p])), cached_at: new Date().toISOString(), })); }) @@ -3701,8 +3733,8 @@ var Ne = class extends w.PluginSettingTab { (i .createEl("button", { text: "\u91CD\u5EFA\u641C\u7D22\u7D22\u5F15" }) .addEventListener("click", () => { - (new w.Notice("\u6B63\u5728\u91CD\u5EFA\u641C\u7D22\u7D22\u5F15\u2026"), - (0, N.execFile)( + (new C.Notice("\u6B63\u5728\u91CD\u5EFA\u641C\u7D22\u7D22\u5F15\u2026"), + (0, j.execFile)( s.path, [ ...(s.extraArgs || []), @@ -3714,7 +3746,7 @@ var Ne = class extends w.PluginSettingTab { ], { cwd: t, timeout: 3e5, windowsHide: !0 }, () => { - new w.Notice( + new C.Notice( "\u641C\u7D22\u7D22\u5F15\u91CD\u5EFA\u5B8C\u6210\u3002" ); } @@ -3723,13 +3755,13 @@ var Ne = class extends w.PluginSettingTab { i .createEl("button", { text: "\u91CD\u5EFA\u8BB0\u5FC6\u5E93" }) .addEventListener("click", () => { - (new w.Notice("\u6B63\u5728\u91CD\u5EFA\u8BB0\u5FC6\u5E93\u2026"), - (0, N.execFile)( + (new C.Notice("\u6B63\u5728\u91CD\u5EFA\u8BB0\u5FC6\u5E93\u2026"), + (0, j.execFile)( s.path, [...(s.extraArgs || []), "-m", "paperforge", "repair", "--fix"], { cwd: t, timeout: 12e4, windowsHide: !0 }, () => { - new w.Notice( + new C.Notice( "\u8BB0\u5FC6\u5E93\u91CD\u5EFA\u5B8C\u6210\u3002" ); } @@ -3739,13 +3771,13 @@ var Ne = class extends w.PluginSettingTab { _renderReleaseNotesTab(e) { (e.createEl("h2", { text: "\u66F4\u65B0\u4E0E\u624B\u518C" }), e.createEl("h3", { text: "\u7248\u672C\u66F4\u65B0\u8BB0\u5F55" })); - let t = vt.default.versions || []; + let t = wt.default.versions || []; for (let a of t) { let s = e.createEl("div", { cls: "paperforge-release-card" }), - o = s.createEl("div", { cls: "paperforge-release-header" }); + l = s.createEl("div", { cls: "paperforge-release-header" }); if ( - (o.createEl("strong", { text: `v${a.version} \u2014 ${a.title}` }), - o.createEl("span", { + (l.createEl("strong", { text: `v${a.version} \u2014 ${a.title}` }), + l.createEl("span", { cls: "paperforge-release-date", text: ` (${a.date})`, }), @@ -3811,26 +3843,179 @@ var Ne = class extends w.PluginSettingTab { .setAttr("target", "_blank")); } }; -var A = require("obsidian"), - ce = H(require("fs")), - De = H(require("path")), - ue = require("child_process"); -var Te = H(require("path")); -function xt(g) { - if (!g) return null; - let h = Te.dirname(g); +var B = require("obsidian"), + pe = $(require("fs")), + Oe = $(require("path")), + he = require("child_process"); +var Be = $(require("path")); +function St(f) { + if (!f) return null; + let u = Be.dirname(f); for (;;) { - let e = Te.basename(h); + let e = Be.basename(u); if (!e || e === ".") break; let t = e.match(/^([A-Z0-9]{8})(?:\s*-\s*.*)?$/i); if (t) return t[1]; - let r = Te.dirname(h); - if (r === h) break; - h = r; + let r = Be.dirname(u); + if (r === u) break; + u = r; } return null; } -var ve = class extends A.ItemView { +var V = $(require("fs")), + fe = $(require("path")); +function Ae(f) { + return Q(f).ocrDir; +} +function jt(f, u) { + let e = fe.join(Ae(f), u, "versions", "manifest.json"); + try { + if (!V.existsSync(e)) return null; + let t = V.readFileSync(e, "utf-8"), + r = JSON.parse(t); + if (r && typeof r == "object" && "versions" in r && "current" in r) { + let n = r, + a = n.versions, + s = n.current; + if (Array.isArray(a) && s && typeof s == "object" && "label" in s) + return r; + } + return null; + } catch (t) { + return null; + } +} +function Ht(f) { + let u = Ae(f); + try { + return V.existsSync(u) + ? V.readdirSync(u, { withFileTypes: !0 }) + .filter((e) => e.isDirectory()) + .map((e) => e.name) + : []; + } catch (e) { + return []; + } +} +function tt(f) { + let u = Ht(f), + e = []; + for (let t of u) { + let r = jt(f, t); + if (!r) continue; + let n = r.versions.map((s) => s.label), + a = 0; + for (let s of n) { + let l = fe.join(Ae(f), t, "versions", s, "fulltext.md"); + try { + V.existsSync(l) && (a += V.statSync(l).size); + } catch (i) {} + } + e.push({ + key: t, + title: t.replace(/_/g, " "), + versions: r.versions, + currentLabel: r.current.label, + totalSize: a, + }); + } + return (e.sort((t, r) => t.title.localeCompare(r.title)), e); +} +function Ct(f, u, e) { + let t = Ae(f), + r = fe.join(t, u, "versions", e, "fulltext.md"), + n = fe.join(t, u, "render"), + a = fe.join(n, "fulltext.md"); + try { + return V.existsSync(r) + ? (V.existsSync(n) || V.mkdirSync(n, { recursive: !0 }), + V.copyFileSync(r, a), + !0) + : !1; + } catch (s) { + return !1; + } +} +function Ft(f, u, e, t) { + var p; + let r = Ae(f), + n = fe.join(r, u, "versions", e, "fulltext.md"), + a = fe.join(r, u, "versions", t, "fulltext.md"), + s = "", + l = ""; + try { + V.existsSync(n) && (s = V.readFileSync(n, "utf-8")); + } catch (m) {} + try { + V.existsSync(a) && (l = V.readFileSync(a, "utf-8")); + } catch (m) {} + let i = Pt(s), + c = Pt(l), + d = Math.max(i.length, c.length), + h = []; + for (let m = 0; m < d; m++) { + let _ = m < i.length ? i[m] : "", + x = m < c.length ? c[m] : "", + y = + (p = (_ || x).split(` +`)[0]) != null + ? p + : "", + b = y.startsWith("## ") ? y.replace(/^##\s+/, "") : "", + w = "unchanged"; + (!_ && x + ? (w = "added") + : _ && !x + ? (w = "removed") + : _ !== x && (w = "changed"), + w !== "unchanged" && + h.push({ + paragraphIndex: m, + heading: b, + type: w, + oldText: _ || void 0, + newText: x || void 0, + })); + } + return h; +} +function Pt(f) { + let u = f.split(` +`), + e = [], + t = []; + for (let r of u) + if (r.startsWith("## ") && t.length > 0) + (e.push( + t + .join( + ` +` + ) + .trim() + ), + (t = [r])); + else if (r.trim() === "" && t.length > 0) { + let n = t + .join( + ` +` + ) + .trim(); + n && (e.push(n), (t = [])); + } else t.push(r); + if (t.length > 0) { + let r = t + .join( + ` +` + ) + .trim(); + r && e.push(r); + } + return e; +} +var Ee = class extends B.ItemView { constructor(e) { super(e); this._currentMode = null; @@ -3856,6 +4041,8 @@ var ve = class extends A.ItemView { this._ocrTrack = null; this._ocrCounts = null; this._driftBannerEl = null; + this._versionPapers = null; + this._versionFilter = ""; this._searchContainer = null; this._searchInput = null; this._searchResultsEl = null; @@ -3871,13 +4058,13 @@ var ve = class extends A.ItemView { (this._ocrPrivacyShown = !1)); } getViewType() { - return ge; + return _e; } getDisplayText() { return "PaperForge"; } getIcon() { - return Se; + return Ce; } async onOpen() { (this._buildPanel(), @@ -3931,7 +4118,7 @@ var ve = class extends A.ItemView { }))); } _fetchVersion() { - var s, o; + var s, l; let e = this.app.vault.adapter.basePath, t = this.app.plugins.plugins.paperforge, r = @@ -3939,11 +4126,11 @@ var ve = class extends A.ItemView { "?", { path: n, extraArgs: a = [] } = M( e, - (o = t == null ? void 0 : t.settings) != null ? o : null, + (l = t == null ? void 0 : t.settings) != null ? l : null, void 0, void 0 ); - nt(n, r, e, 1e4, void 0).then((i) => { + lt(n, r, e, 1e4, void 0).then((i) => { if (i.status === "not-installed") return; let c = i.pyVersion || ""; ((this._paperforgeVersion = c.startsWith("v") ? c : "v" + c), @@ -3954,7 +4141,7 @@ var ve = class extends A.ItemView { this._paperforgeVersion !== "v" + r.replace(/^v/, "") ? ((this._driftBannerEl.style.display = "block"), this._driftBannerEl.setText( - l("dashboard_drift_warning") + o("dashboard_drift_warning") .replace("{0}", this._paperforgeVersion) .replace("{1}", "v" + r.replace(/^v/, "")) )) @@ -3980,12 +4167,12 @@ var ve = class extends A.ItemView { void 0, void 0 ); - (0, ue.execFile)( + (0, he.execFile)( n, [...a, "-m", "paperforge", "dashboard", "--json"], { cwd: t, timeout: 3e4 }, - (o, i) => { - if (!o) + (l, i) => { + if (!l) try { let c = JSON.parse(i); if (c.ok && c.data) { @@ -4029,64 +4216,64 @@ var ve = class extends A.ItemView { }; } _fallbackFetchStats(e, t, r) { - var s, o, i; + var s, l, i; let n = ((s = r == null ? void 0 : r.settings) == null ? void 0 : s.system_dir) || "System", - a = De.join(t, n, "PaperForge", "indexes", "formal-library.json"); + a = Oe.join(t, n, "PaperForge", "indexes", "formal-library.json"); try { - let c = ce.readFileSync(a, "utf-8"), + let c = pe.readFileSync(a, "utf-8"), d = JSON.parse(c), - u = d.items || [], + h = d.items || [], p = {}, - _ = { + m = { pdf_health: { healthy: 0, unhealthy: 0 }, ocr_health: { healthy: 0, unhealthy: 0 }, note_health: { healthy: 0, unhealthy: 0 }, asset_health: { healthy: 0, unhealthy: 0 }, }, - m = 0, - E = 0, - f = 0, - v = 0, - S = 0, - x = 0; - for (let y of u) { - y.note_path && x++; - let b = y.lifecycle || "pdf_ready"; - p[b] = (p[b] || 0) + 1; - let k = y.health || {}; - for (let T of [ + _ = 0, + x = 0, + y = 0, + b = 0, + w = 0, + E = 0; + for (let g of h) { + g.note_path && E++; + let v = g.lifecycle || "pdf_ready"; + p[v] = (p[v] || 0) + 1; + let k = g.health || {}; + for (let P of [ "pdf_health", "ocr_health", "note_health", "asset_health", ]) - (k[T] || "healthy") === "healthy" ? _[T].healthy++ : _[T].unhealthy++; - let C = y.ocr_status || ""; - (m++, - C === "done" - ? E++ - : C === "pending" - ? f++ - : C === "processing" || C === "queued" || C === "running" - ? v++ - : S++); + (k[P] || "healthy") === "healthy" ? m[P].healthy++ : m[P].unhealthy++; + let S = g.ocr_status || ""; + (_++, + S === "done" + ? x++ + : S === "pending" + ? y++ + : S === "processing" || S === "queued" || S === "running" + ? b++ + : w++); } ((this._cachedStats = { version: d.paperforge_version || - ((o = this._cachedStats) == null ? void 0 : o.version) || + ((l = this._cachedStats) == null ? void 0 : l.version) || "\u2014", - total_papers: u.length, - formal_notes: x, + total_papers: h.length, + formal_notes: E, exports: 0, bases: 0, - ocr: { total: m, pending: f, processing: v, done: E, failed: S }, + ocr: { total: _, pending: y, processing: b, done: x, failed: w }, path_errors: 0, lifecycle_level_counts: p, - health_aggregate: _, + health_aggregate: m, }), this._metricsEl.empty(), this._renderStats(this._cachedStats), @@ -4098,17 +4285,17 @@ var ve = class extends A.ItemView { cls: "paperforge-status-loading", text: "No index \u2014 trying CLI...", }); - let { path: d, extraArgs: u = [] } = M( + let { path: d, extraArgs: h = [] } = M( t, (i = r == null ? void 0 : r.settings) != null ? i : null, void 0, void 0 ); - (0, ue.execFile)( + (0, he.execFile)( d, - [...u, "-m", "paperforge", "status", "--json"], + [...h, "-m", "paperforge", "status", "--json"], { cwd: t, timeout: 3e4 }, - (p, _) => { + (p, m) => { if (p) { if (this._cachedStats) return; this._metricsEl.createEl("div", { @@ -4119,12 +4306,12 @@ Make sure paperforge is installed and in your PATH.`, return; } try { - let m = JSON.parse(_); - ((this._cachedStats = m), + let _ = JSON.parse(m); + ((this._cachedStats = _), this._metricsEl.empty(), - this._renderStats(m), - this._renderOcr(m)); - } catch (m) { + this._renderStats(_), + this._renderOcr(_)); + } catch (_) { this._cachedStats || this._metricsEl.createEl("div", { cls: "paperforge-status-error", @@ -4157,9 +4344,9 @@ Make sure paperforge is installed and in your PATH.`, ((a = t == null ? void 0 : t.settings) == null ? void 0 : a.system_dir) || "System", - n = De.join(e, r, "PaperForge", "indexes", "formal-library.json"); + n = Oe.join(e, r, "PaperForge", "indexes", "formal-library.json"); try { - let s = ce.readFileSync(n, "utf-8"); + let s = pe.readFileSync(n, "utf-8"); return JSON.parse(s); } catch (s) { return null; @@ -4175,12 +4362,12 @@ Make sure paperforge is installed and in your PATH.`, _findEntry(e) { if (!e) return null; let t = this._getCachedIndex().find((r) => r.zotero_key === e) || null; - return et(this.app, t); + return nt(this.app, t); } _patchCachedEntry(e, t) { if (!e || !this._cachedItems) return; let r = this._cachedItems.findIndex((n) => n.zotero_key === e); - r !== -1 && (this._cachedItems[r] = je(this._cachedItems[r], t)); + r !== -1 && (this._cachedItems[r] = Ke(this._cachedItems[r], t)); } _filterByDomain(e) { return e ? this._getCachedIndex().filter((t) => t.domain === e) : []; @@ -4216,34 +4403,34 @@ Make sure paperforge is installed and in your PATH.`, barMax: 0, }, ]; - for (let o of n) { + for (let l of n) { let i = this._metricsEl.createEl("div", { cls: "paperforge-metric-card", }); - (i.style.setProperty("--metric-color", o.color), + (i.style.setProperty("--metric-color", l.color), i.createEl("div", { cls: "paperforge-metric-value", - text: ((s = o.value) == null ? void 0 : s.toString()) || "\u2014", + text: ((s = l.value) == null ? void 0 : s.toString()) || "\u2014", }), - i.createEl("div", { cls: "paperforge-metric-label", text: o.label }), - o.barMax > 0 && this._buildMetricBar(i, o.value, o.barMax)); + i.createEl("div", { cls: "paperforge-metric-label", text: l.label }), + l.barMax > 0 && this._buildMetricBar(i, l.value, l.barMax)); } let a = e.ocr_version_state || {}; if ( a.total_papers > 0 && (a.derived_stale_count > 0 || a.raw_upgradable_count > 0) ) { - let o = []; - (a.derived_stale_count > 0 && o.push(`${a.derived_stale_count} stale`), + let l = []; + (a.derived_stale_count > 0 && l.push(`${a.derived_stale_count} stale`), a.raw_upgradable_count > 0 && - o.push(`${a.raw_upgradable_count} upgradable`)); + l.push(`${a.raw_upgradable_count} upgradable`)); let i = this._metricsEl.createEl("div", { cls: "paperforge-metric-card", }); (i.style.setProperty("--metric-color", "var(--color-yellow)"), i.createEl("div", { cls: "paperforge-metric-value", - text: o.join(", "), + text: l.join(", "), }), i.createEl("div", { cls: "paperforge-metric-label", @@ -4264,7 +4451,7 @@ Make sure paperforge is installed and in your PATH.`, let n = t.done || 0, a = t.pending || 0, s = t.processing || 0, - o = t.failed || 0; + l = t.failed || 0; if ( (this._ocrBadge && (this._ocrBadge.removeClass("active", "idle"), @@ -4286,7 +4473,7 @@ Make sure paperforge is installed and in your PATH.`, { cls: "pending", count: a }, { cls: "active", count: s }, { cls: "done", count: n }, - { cls: "failed", count: o }, + { cls: "failed", count: l }, ]; for (let c of i) if (c.count > 0) { @@ -4303,7 +4490,7 @@ Make sure paperforge is installed and in your PATH.`, { cls: "pending", value: a, label: "Pending" }, { cls: "active", value: s, label: "Processing" }, { cls: "done", value: n, label: "Done" }, - { cls: "failed", value: o, label: "Failed" }, + { cls: "failed", value: l, label: "Failed" }, ]; for (let c of i) { let d = this._ocrCounts.createEl("div", { @@ -4333,11 +4520,11 @@ Make sure paperforge is installed and in your PATH.`, ], a = e.createEl("div", { cls: "paperforge-lifecycle-stepper" }), s = !1; - for (let o of n) { + for (let l of n) { let i = a.createEl("div", { cls: "step" }); (i.createEl("div", { cls: "step-indicator" }), - i.createEl("div", { cls: "step-label", text: o.label }), - o.key === r + i.createEl("div", { cls: "step-label", text: l.label }), + l.key === r ? (i.addClass("current"), (s = !0)) : s ? i.addClass("pending") @@ -4382,7 +4569,7 @@ Make sure paperforge is installed and in your PATH.`, n = e.createEl("div", { cls: "paperforge-health-matrix" }); for (let a of r) { let s = t[a.key] || "healthy", - o = n.createEl("div", { cls: "paperforge-health-cell" }), + l = n.createEl("div", { cls: "paperforge-health-cell" }), i, c, d; @@ -4393,10 +4580,10 @@ Make sure paperforge is installed and in your PATH.`, (c = "warn"), (d = `${a.label}: Needs Attention`)) : ((i = a.iconFail), (c = "fail"), (d = `${a.label}: Failed`)), - o.addClass(c), - o.setAttribute("title", d), - o.createEl("div", { cls: "paperforge-health-cell-icon", text: i }), - o.createEl("div", { + l.addClass(c), + l.setAttribute("title", d), + l.createEl("div", { cls: "paperforge-health-cell-icon", text: i }), + l.createEl("div", { cls: "paperforge-health-cell-label", text: a.label, })); @@ -4410,14 +4597,14 @@ Make sure paperforge is installed and in your PATH.`, let n = e.createEl("div", { cls: "paperforge-maturity-gauge" }), a = n.createEl("div", { cls: "gauge-track" }), s = 4, - o = Math.max(1, Math.min(s, Math.round(t))); + l = Math.max(1, Math.min(s, Math.round(t))); for (let i = 1; i <= s; i++) { let c = a.createEl("div", { cls: "gauge-segment" }); - i <= o && (c.addClass("filled"), c.addClass(`level-${i}`)); + i <= l && (c.addClass("filled"), c.addClass(`level-${i}`)); } if ( - (n.createEl("div", { cls: "gauge-level", text: `Level ${o} / ${s}` }), - o < s && r) + (n.createEl("div", { cls: "gauge-level", text: `Level ${l} / ${s}` }), + l < s && r) ) { let i = typeof r == "string" ? [r] : r; if (i.length > 0) { @@ -4444,8 +4631,8 @@ Make sure paperforge is installed and in your PATH.`, n = e.createEl("div", { cls: "paperforge-bar-chart" }), a = Math.max(1, ...r.map((s) => t[s.key] || 0)); for (let s of r) { - let o = t[s.key] || 0, - i = (o / a) * 100, + let l = t[s.key] || 0, + i = (l / a) * 100, c = n.createEl("div", { cls: "bar-row" }); (c.createEl("div", { cls: "bar-label", text: s.label }), c @@ -4454,14 +4641,14 @@ Make sure paperforge is installed and in your PATH.`, cls: `bar-fill ${s.cls}`, attr: { style: `width:${i.toFixed(1)}%` }, }), - c.createEl("div", { cls: "bar-count", text: o.toString() })); + c.createEl("div", { cls: "bar-count", text: l.toString() })); } } _invalidateIndex() { this._cachedItems = null; } _extractZoteroKeyFromPath(e) { - return xt(e); + return St(e); } _resolveModeForFile(e) { if (!e) return { mode: "global", filePath: null, key: null, domain: null }; @@ -4482,8 +4669,8 @@ Make sure paperforge is installed and in your PATH.`, if (t === "pdf") { let a = this._getCachedIndex(); for (let s of a) { - let o = (s.pdf_path || "").match(/\[\[([^\]]+)\]\]/); - if ((o ? o[1] : s.pdf_path) === r) + let l = (s.pdf_path || "").match(/\[\[([^\]]+)\]\]/); + if ((l ? l[1] : s.pdf_path) === r) return { mode: "paper", filePath: r, @@ -4530,10 +4717,13 @@ Make sure paperforge is installed and in your PATH.`, case "collection": this._renderCollectionMode(); break; + case "versions": + this._renderVersionMode(); + break; } } _renderGlobalMode() { - var Y, ie, me, pe, Ee, ke, he; + var ee, oe, ve, de, we, Se, ge; if (!this._contentEl) return; let e = this._contentEl.createEl("div", { cls: "paperforge-global-view" }); ((this._driftBannerEl = e.createEl("div", { @@ -4545,31 +4735,31 @@ Make sure paperforge is installed and in your PATH.`, n = 0, a = 0, s = 0; - for (let F of t) - (F.has_pdf && n++, - F.ocr_status === "done" && a++, - F.deep_reading_status === "done" && s++); - let o = e.createEl("div", { cls: "paperforge-library-snapshot" }); - o.createEl("div", { + for (let R of t) + (R.has_pdf && n++, + R.ocr_status === "done" && a++, + R.deep_reading_status === "done" && s++); + let l = e.createEl("div", { cls: "paperforge-library-snapshot" }); + l.createEl("div", { cls: "paperforge-section-label", text: "Library Snapshot", }); - let i = o.createEl("div", { cls: "paperforge-snapshot-pills" }), + let i = l.createEl("div", { cls: "paperforge-snapshot-pills" }), c = [ { value: r, label: "papers" }, { value: n, label: "PDFs ready" }, { value: a, label: "OCR done" }, { value: s, label: "deep-read done" }, ]; - for (let F of c) { + for (let R of c) { let L = i.createEl("div", { cls: "paperforge-snapshot-pill" }); (L.createEl("span", { cls: "paperforge-snapshot-value", - text: String(F.value), + text: String(R.value), }), L.createEl("span", { cls: "paperforge-snapshot-label", - text: " " + F.label, + text: " " + R.label, })); } let d = e.createEl("div", { cls: "paperforge-system-status" }); @@ -4577,84 +4767,85 @@ Make sure paperforge is installed and in your PATH.`, cls: "paperforge-section-label", text: "System Status", }); - let u = d.createEl("div", { cls: "paperforge-status-grid" }), + let h = d.createEl("div", { cls: "paperforge-status-grid" }), p = this.app.plugins.plugins.paperforge, - _ = - ((Y = p == null ? void 0 : p.manifest) == null ? void 0 : Y.version) || - "?", - m = this._paperforgeVersion; - if (!m) + m = + ((ee = p == null ? void 0 : p.manifest) == null + ? void 0 + : ee.version) || "?", + _ = this._paperforgeVersion; + if (!_) try { - let F = this.app.vault.adapter.basePath, - { path: L, extraArgs: ee = [] } = M( - F, - (ie = p == null ? void 0 : p.settings) != null ? ie : null, + let R = this.app.vault.adapter.basePath, + { path: L, extraArgs: re = [] } = M( + R, + (oe = p == null ? void 0 : p.settings) != null ? oe : null, void 0, void 0 ), - q = (0, ue.execFileSync)( + J = (0, he.execFileSync)( L, - [...ee, "-c", "import paperforge; print(paperforge.__version__)"], - { cwd: F, timeout: 5e3, encoding: "utf-8", windowsHide: !0 } + [...re, "-c", "import paperforge; print(paperforge.__version__)"], + { cwd: R, timeout: 5e3, encoding: "utf-8", windowsHide: !0 } ).trim(); - q && - ((m = q.startsWith("v") ? q : "v" + q), - (this._paperforgeVersion = m)); - } catch (F) {} - m = m || "\u2014"; - let E = m === "v" + _; + J && + ((_ = J.startsWith("v") ? J : "v" + J), + (this._paperforgeVersion = _)); + } catch (R) {} + _ = _ || "\u2014"; + let x = _ === "v" + m; this._renderSystemStatusRow( - u, + h, "Runtime", - E ? "healthy" : "mismatch", - E ? "v" + _ : "plugin v" + _ + " \u2260 CLI " + m + x ? "healthy" : "mismatch", + x ? "v" + m : "plugin v" + m + " \u2260 CLI " + _ ); - let f = this._loadIndex(), - v = f && f.items && f.items.length > 0; + let y = this._loadIndex(), + b = y && y.items && y.items.length > 0; this._renderSystemStatusRow( - u, + h, "Index", - v ? "healthy" : "missing", - v ? f.items.length + " entries" : "formal-library.json not found" + b ? "healthy" : "missing", + b ? y.items.length + " entries" : "formal-library.json not found" ); - let S = - ((me = p == null ? void 0 : p.settings) == null + let w = + ((ve = p == null ? void 0 : p.settings) == null ? void 0 - : me.system_dir) || "System", - x = this.app.vault.adapter.basePath, - y = !1, - b = "No exports found"; + : ve.system_dir) || "System", + E = this.app.vault.adapter.basePath, + g = !1, + v = "No exports found"; try { - let F = De.join(x, S, "PaperForge", "exports"); - if (ce.existsSync(F)) { - let L = ce.readdirSync(F).filter((ee) => ee.endsWith(".json")); - ((y = L.length > 0), - (b = y ? L.length + " export(s)" : "No JSON exports")); + let R = Oe.join(E, w, "PaperForge", "exports"); + if (pe.existsSync(R)) { + let L = pe.readdirSync(R).filter((re) => re.endsWith(".json")); + ((g = L.length > 0), + (v = g ? L.length + " export(s)" : "No JSON exports")); } - } catch (F) {} + } catch (R) {} this._renderSystemStatusRow( - u, + h, "Zotero Export", - y ? "healthy" : "missing", - b + g ? "healthy" : "missing", + v ); let k = !!( - (pe = p == null ? void 0 : p.settings) != null && pe.paddleocr_api_key + (de = p == null ? void 0 : p.settings) != null && de.paddleocr_api_key ); if (!k) try { - let F = - ((Ee = p == null ? void 0 : p.settings) == null + let R = + ((we = p == null ? void 0 : p.settings) == null ? void 0 - : Ee.system_dir) || "System", - L = De.join(x, F, "PaperForge", ".env"); - if (ce.existsSync(L)) { - let q = ce + : we.system_dir) || "System", + L = Oe.join(E, R, "PaperForge", ".env"); + if (pe.existsSync(L)) { + let J = pe .readFileSync(L, "utf-8") .match(/^PADDLEOCR_API_TOKEN\s*=\s*(.+)$/m); - k = !!(q && q[1] && q[1].trim()); + k = !!(J && J[1] && J[1].trim()); } - } catch (F) {} + } catch (R) {} (k || (k = !!( process.env.PADDLEOCR_API_TOKEN || @@ -4662,45 +4853,45 @@ Make sure paperforge is installed and in your PATH.`, process.env.OCR_TOKEN )), this._renderSystemStatusRow( - u, + h, "OCR Token", k ? "configured" : "missing", k ? "Configured" : "Not set" )); - let C = !1, - T = "", - O = this.app.vault.adapter.basePath, - D = Ue(O); - ((C = ct(O)), - (T = - (D && ((ke = D.summary) == null ? void 0 : ke.reason)) || - (D && ((he = D.summary) == null ? void 0 : he.status)) || + let S = !1, + P = "", + A = this.app.vault.adapter.basePath, + T = Ye(A); + ((S = ft(A)), + (P = + (T && ((Se = T.summary) == null ? void 0 : Se.reason)) || + (T && ((ge = T.summary) == null ? void 0 : ge.status)) || "Unknown"), this._renderSystemStatusRow( - u, + h, "Memory Layer", - C ? "healthy" : "fail", - T + S ? "healthy" : "fail", + P )); - let z = !E && m !== "\u2014"; - if (z || !v || !y || !k) { - let F = e.createEl("div", { cls: "paperforge-issue-summary" }); - F.createEl("div", { + let I = !x && _ !== "\u2014"; + if (I || !b || !g || !k) { + let R = e.createEl("div", { cls: "paperforge-issue-summary" }); + R.createEl("div", { cls: "paperforge-section-label", text: "\u9700\u8981\u5904\u7406", }); - let L = F.createEl("div", { cls: "paperforge-issue-list" }); - (z && + let L = R.createEl("div", { cls: "paperforge-issue-list" }); + (I && L.createEl("div", { cls: "paperforge-issue-item", text: "Runtime version mismatch", }), - v || + b || L.createEl("div", { cls: "paperforge-issue-item", text: "Index missing or corrupted", }), - y || + g || L.createEl("div", { cls: "paperforge-issue-item", text: "No Zotero export found", @@ -4710,80 +4901,80 @@ Make sure paperforge is installed and in your PATH.`, cls: "paperforge-issue-item", text: "PaddleOCR API key not configured", })); - let ee = F.createEl("div", { cls: "paperforge-issue-actions" }), - q = ee.createEl("button", { cls: "paperforge-contextual-btn" }); - (q.createEl("span", { text: "Run Doctor" }), - q.addEventListener("click", () => { - let we = G.find((ze) => ze.id === "paperforge-doctor"); - we && this._runAction(we, q); + let re = R.createEl("div", { cls: "paperforge-issue-actions" }), + J = re.createEl("button", { cls: "paperforge-contextual-btn" }); + (J.createEl("span", { text: "Run Doctor" }), + J.addEventListener("click", () => { + let Pe = Y.find((Ve) => Ve.id === "paperforge-doctor"); + Pe && this._runAction(Pe, J); })); - let de = ee.createEl("button", { cls: "paperforge-contextual-btn" }); - (de.createEl("span", { text: "Repair Issues" }), - de.addEventListener("click", () => { - let we = G.find((ze) => ze.id === "paperforge-repair"); - we && this._runAction(we, de); + let ue = re.createEl("button", { cls: "paperforge-contextual-btn" }); + (ue.createEl("span", { text: "Repair Issues" }), + ue.addEventListener("click", () => { + let Pe = Y.find((Ve) => Ve.id === "paperforge-repair"); + Pe && this._runAction(Pe, ue); })); } - let B = e.createEl("div", { cls: "paperforge-global-actions" }); - B.createEl("div", { + let O = e.createEl("div", { cls: "paperforge-global-actions" }); + O.createEl("div", { cls: "paperforge-section-label", text: "Start Working", }); - let X = B.createEl("div", { cls: "paperforge-global-actions-row" }), - W = X.createEl("button", { cls: "paperforge-contextual-btn primary" }); - (W.createEl("span", { + let U = O.createEl("div", { cls: "paperforge-global-actions-row" }), + q = U.createEl("button", { cls: "paperforge-contextual-btn primary" }); + (q.createEl("span", { cls: "paperforge-contextual-btn-icon", text: "\u{1F4C1}", }), - W.createEl("span", { text: "Open Literature Hub" }), - W.addEventListener("click", () => { - var ee; - let F = - ((ee = p == null ? void 0 : p.settings) == null + q.createEl("span", { text: "Open Literature Hub" }), + q.addEventListener("click", () => { + var re; + let R = + ((re = p == null ? void 0 : p.settings) == null ? void 0 - : ee.base_dir) || "Bases", - L = this.app.vault.getAbstractFileByPath(F); + : re.base_dir) || "Bases", + L = this.app.vault.getAbstractFileByPath(R); if (L) { - let q = null; + let J = null; if ( (L.children && - (q = L.children.find((de) => de.extension === "base")), - q) + (J = L.children.find((ue) => ue.extension === "base")), + J) ) { - let de = this.app.workspace.getLeaf(!1); - de && de.openFile(q); - } else new A.Notice("[!!] No .base file found in " + F, 6e3); - } else new A.Notice("[!!] Base directory not found: " + F, 6e3); + let ue = this.app.workspace.getLeaf(!1); + ue && ue.openFile(J); + } else new B.Notice("[!!] No .base file found in " + R, 6e3); + } else new B.Notice("[!!] Base directory not found: " + R, 6e3); })); - let P = X.createEl("button", { cls: "paperforge-contextual-btn" }); - (P.createEl("span", { + let F = U.createEl("button", { cls: "paperforge-contextual-btn" }); + (F.createEl("span", { cls: "paperforge-contextual-btn-icon", text: "\u21BB", }), - P.createEl("span", { text: "Sync Library" }), - P.addEventListener("click", () => { - let F = G.find((L) => L.id === "paperforge-sync"); - F && this._runAction(F, P); + F.createEl("span", { text: "Sync Library" }), + F.addEventListener("click", () => { + let R = Y.find((L) => L.id === "paperforge-sync"); + R && this._runAction(R, F); })); - let R = X.createEl("button", { cls: "paperforge-contextual-btn" }); - (R.createEl("span", { + let D = U.createEl("button", { cls: "paperforge-contextual-btn" }); + (D.createEl("span", { cls: "paperforge-contextual-btn-icon", text: "\u229E", }), - R.createEl("span", { text: "Run OCR" }), - R.addEventListener("click", () => { - let F = G.find((L) => L.id === "paperforge-ocr"); - F && this._runAction(F, R); + D.createEl("span", { text: "Run OCR" }), + D.addEventListener("click", () => { + let R = Y.find((L) => L.id === "paperforge-ocr"); + R && this._runAction(R, D); })); - let U = X.createEl("button", { cls: "paperforge-contextual-btn warn" }); - (U.createEl("span", { + let X = U.createEl("button", { cls: "paperforge-contextual-btn warn" }); + (X.createEl("span", { cls: "paperforge-contextual-btn-icon", text: "\u21BA", }), - U.createEl("span", { text: "Redo OCR" }), - U.addEventListener("click", () => { - let F = G.find((L) => L.id === "paperforge-ocr-redo"); - F && this._runAction(F, U); + X.createEl("span", { text: "Redo OCR" }), + X.addEventListener("click", () => { + let R = Y.find((L) => L.id === "paperforge-ocr-redo"); + R && this._runAction(R, X); })); } _renderSystemStatusRow(e, t, r, n) { @@ -4816,7 +5007,7 @@ Make sure paperforge is installed and in your PATH.`, text: e.title || "Untitled", }).addEventListener("click", () => { (navigator.clipboard.writeText(e.title || ""), - new A.Notice("Title copied")); + new B.Notice("Title copied")); }); let s = n.createEl("div", { cls: "paperforge-paper-meta" }); (e.authors && @@ -4830,9 +5021,9 @@ Make sure paperforge is installed and in your PATH.`, cls: "paperforge-paper-year", text: String(e.year), })); - let o = r.createEl("div", { cls: "paperforge-status-strip" }), - i = o.createEl("div", { cls: "paperforge-status-strip-left" }), - c = o.createEl("div", { cls: "paperforge-status-strip-right" }), + let l = r.createEl("div", { cls: "paperforge-status-strip" }), + i = l.createEl("div", { cls: "paperforge-status-strip-left" }), + c = l.createEl("div", { cls: "paperforge-status-strip-right" }), d = [ { key: "pdf", label: "PDF", ok: e.has_pdf === !0 }, { @@ -4852,46 +5043,51 @@ Make sure paperforge is installed and in your PATH.`, ok: e.deep_reading_status === "done", }, ]; - for (let u of d) { - let p = i.createEl("span", { cls: "paperforge-status-pill" }), + for (let p of d) { + let m = i.createEl("span", { cls: "paperforge-status-pill" }), _ = "pending"; - (u.ok ? (_ = "ok") : u.fail ? (_ = "fail") : u.pending && (_ = "pending"), - p.addClass(_)); - let m = u.ok ? "\u2713" : u.fail ? "\u2717" : "\u25CB"; - (p.createEl("span", { cls: "paperforge-status-pill-icon", text: m }), - p.createEl("span", { text: " " + u.label })); + (p.ok ? (_ = "ok") : p.fail ? (_ = "fail") : p.pending && (_ = "pending"), + m.addClass(_)); + let x = p.ok ? "\u2713" : p.fail ? "\u2717" : "\u25CB"; + (m.createEl("span", { cls: "paperforge-status-pill-icon", text: x }), + m.createEl("span", { text: " " + p.label })); } if (e.pdf_path) { - let u = c.createEl("button", { cls: "paperforge-contextual-btn" }); - (u.createEl("span", { + let p = c.createEl("button", { cls: "paperforge-contextual-btn" }); + (p.createEl("span", { cls: "paperforge-contextual-btn-icon", text: "\u{1F4C4}", }), - u.createEl("span", { text: "\u6253\u5F00 PDF" }), - u.addEventListener("click", () => { - let p = e.pdf_path.match(/\[\[([^\]]+)\]\]/), - _ = p ? p[1] : e.pdf_path; + p.createEl("span", { text: "\u6253\u5F00 PDF" }), + p.addEventListener("click", () => { + let m = e.pdf_path.match(/\[\[([^\]]+)\]\]/), + _ = m ? m[1] : e.pdf_path; this.app.vault.getAbstractFileByPath(_) ? this.app.workspace.openLinkText(_, "") - : new A.Notice("[!!] PDF not found: " + _, 6e3); + : new B.Notice("[!!] PDF not found: " + _, 6e3); })); } if (e.fulltext_path) { - let u = c.createEl("button", { cls: "paperforge-contextual-btn" }); - (u.createEl("span", { + let p = c.createEl("button", { cls: "paperforge-contextual-btn" }); + (p.createEl("span", { cls: "paperforge-contextual-btn-icon", text: "\u{1F4DD}", }), - u.createEl("span", { text: "\u6253\u5F00\u5168\u6587" }), - u.addEventListener("click", () => this._openFulltext(e.fulltext_path))); + p.createEl("span", { text: "\u6253\u5F00\u5168\u6587" }), + p.addEventListener("click", () => this._openFulltext(e.fulltext_path))); } + let h = c.createEl("button", { cls: "paperforge-contextual-btn" }); if ( - (this._renderPaperOverviewCard(r, e), + (h.createEl("span", { text: o("version_panel_title") }), + h.addEventListener("click", () => { + this._switchToVersionMode(t); + }), + this._renderPaperOverviewCard(r, e), e.next_step === "ready" && e.deep_reading_status === "done") ) { - let u = r.createEl("div", { cls: "paperforge-complete-row" }); - (u.createEl("span", { text: "\u2713" }), - u.createEl("span", { + let p = r.createEl("div", { cls: "paperforge-complete-row" }); + (p.createEl("span", { text: "\u2713" }), + p.createEl("span", { text: "\u5DF2\u5B8C\u6210\uFF0C\u53EF\u76F4\u63A5\u4F7F\u7528", })); } else this._renderNextStepCard(r, e, t); @@ -4913,31 +5109,31 @@ Make sure paperforge is installed and in your PATH.`, text: "\u52A0\u8F7D\u4E2D...", }); if (t.note_path) { - let o = this.app.vault.getAbstractFileByPath(t.note_path); - o + let l = this.app.vault.getAbstractFileByPath(t.note_path); + l ? this.app.vault - .read(o) + .read(l) .then((i) => { let c = this._extractOverviewFromNote(i); if (c) { let d = c.length > 200 ? c.slice(0, 200) + "..." : c; if ((s.setText(d), c.length > 200)) { - let u = a.createEl("div", { + let h = a.createEl("div", { cls: "paperforge-expand-container", }), - p = u.createEl("button", { + p = h.createEl("button", { cls: "paperforge-expand-icon", title: "\u5C55\u5F00/\u6536\u8D77", }); p.innerHTML = ''; - let _ = !1; - u.addEventListener("click", () => { - (s.setText(_ ? d : c), - (p.innerHTML = _ + let m = !1; + h.addEventListener("click", () => { + (s.setText(m ? d : c), + (p.innerHTML = m ? '' : ''), - (_ = !_)); + (m = !m)); }); } } else @@ -4962,25 +5158,25 @@ Make sure paperforge is installed and in your PATH.`, "**\u6587\u7AE0\u6458\u8981:**", "**\u6587\u7AE0\u6458\u8981**", ]; - for (let o of n) { - let i = r.indexOf(o); + for (let l of n) { + let i = r.indexOf(l); if (i !== -1) { - let c = r.slice(i + o.length), + let c = r.slice(i + l.length), d = ["**5 Cs", "**Figure", "**\u8BC1\u636E", "### Pass 2", "## "], - u = c.length; - for (let m of d) { - let E = c.indexOf(m); - E !== -1 && E < u && (u = E); + h = c.length; + for (let _ of d) { + let x = c.indexOf(_); + x !== -1 && x < h && (h = x); } let p = c.indexOf(` `); - p !== -1 && p < u && (u = p); - let _ = c.slice(0, u).trim(); + p !== -1 && p < h && (h = p); + let m = c.slice(0, h).trim(); return ( - _.startsWith("**") && (_ = _.slice(2)), - _.endsWith("**") && (_ = _.slice(0, -2)), - _ || null + m.startsWith("**") && (m = m.slice(2)), + m.endsWith("**") && (m = m.slice(0, -2)), + m || null ); } } @@ -5008,12 +5204,12 @@ Make sure paperforge is installed and in your PATH.`, s = (n !== -1 ? t.note_path.substring(0, n) : ".") + "/ai/discussion.md"; this.app.vault.adapter .exists(s) - .then((o) => { - if (o) return this.app.vault.adapter.read(s); + .then((l) => { + if (l) return this.app.vault.adapter.read(s); }) - .then(async (o) => { - if (!o) return; - let i = this._parseDiscussionMD(o); + .then(async (l) => { + if (!l) return; + let i = this._parseDiscussionMD(l); if (!i || i.length === 0) return; ((r.style.display = "block"), r @@ -5022,55 +5218,55 @@ Make sure paperforge is installed and in your PATH.`, cls: "paperforge-discussion-title", text: "\u6700\u8FD1\u8BA8\u8BBA", })); - for (let u of i) { + for (let h of i) { let p = r.createEl("div", { cls: "paperforge-discussion-item" }), - _ = p.createEl("div", { cls: "paperforge-discussion-q" }); - (_.createEl("span", { + m = p.createEl("div", { cls: "paperforge-discussion-q" }); + (m.createEl("span", { cls: "paperforge-discussion-q-label", text: "\u63D0\u95EE\uFF1A", }), - _.createEl("span", { + m.createEl("span", { cls: "paperforge-discussion-q-text", - text: u.question, + text: h.question, })); - let m = p.createEl("div", { cls: "paperforge-discussion-a" }), - E = !1; + let _ = p.createEl("div", { cls: "paperforge-discussion-a" }), + x = !1; if ( - (u.answer && - u.answer.length > 500 && - ((E = !0), m.classList.add("paperforge-discussion-a-collapsed")), - await A.MarkdownRenderer.render( + (h.answer && + h.answer.length > 500 && + ((x = !0), _.classList.add("paperforge-discussion-a-collapsed")), + await B.MarkdownRenderer.render( this.app, - u.answer || "", - m, + h.answer || "", + _, s, this ), - E) + x) ) { - let f = !1; + let y = !1; ((p.style.cursor = "pointer"), p.addEventListener("click", () => { - ((f = !f), - m.classList.toggle("paperforge-discussion-a-collapsed", !f), - m.classList.toggle("paperforge-discussion-a-expanded", f)); + ((y = !y), + _.classList.toggle("paperforge-discussion-a-collapsed", !y), + _.classList.toggle("paperforge-discussion-a-expanded", y)); })); } } r.createEl("a", { cls: "paperforge-discussion-viewall", text: "\u67E5\u770B\u5168\u90E8\u8BA8\u8BBA \u2192", - }).addEventListener("click", (u) => { - (u.preventDefault(), + }).addEventListener("click", (h) => { + (h.preventDefault(), this.app.vault.getAbstractFileByPath(s) ? this.app.workspace.openLinkText(s, "") - : new A.Notice( + : new B.Notice( "\u8BA8\u8BBA\u6587\u4EF6\u5C1A\u672A\u751F\u6210" )); }); }) - .catch((o) => { - console.error("PaperForge: discussion.md read error", s, o.message); + .catch((l) => { + console.error("PaperForge: discussion.md read error", s, l.message); }); } _parseDiscussionMD(e) { @@ -5080,10 +5276,10 @@ Make sure paperforge is installed and in your PATH.`, n = [], a = r.split(/\*\*\u95EE\u9898:\*\*/).slice(1); for (let s of a) { - let o = s.match(/\*\*\u89E3\u7B54:\*\*/); - if (!o) continue; - let i = s.substring(0, o.index).trim(), - c = s.substring(o.index + 3 + 4).trim(); + let l = s.match(/\*\*\u89E3\u7B54:\*\*/); + if (!l) continue; + let i = s.substring(0, l.index).trim(), + c = s.substring(l.index + 3 + 4).trim(); n.push({ question: i, answer: c }); } return n.slice(-3); @@ -5108,7 +5304,7 @@ Make sure paperforge is installed and in your PATH.`, ), (this._techDetailsExpanded = !p)); })); - let o = s.createEl("div", { cls: "paperforge-workflow-toggles" }), + let l = s.createEl("div", { cls: "paperforge-workflow-toggles" }), i = [ { key: "do_ocr", label: "OCR", hint: "\u52A0\u5165 OCR" }, { @@ -5118,35 +5314,35 @@ Make sure paperforge is installed and in your PATH.`, }, ]; for (let p of i) { - let _ = o.createEl("label", { cls: "paperforge-workflow-toggle" }), - m = _.createEl("input", { + let m = l.createEl("label", { cls: "paperforge-workflow-toggle" }), + _ = m.createEl("input", { type: "checkbox", cls: "paperforge-workflow-checkbox", }); - ((m.checked = t[p.key] === !0), - _.createEl("span", { + ((_.checked = t[p.key] === !0), + m.createEl("span", { cls: "paperforge-workflow-toggle-label", text: p.label, }), - _.createEl("span", { + m.createEl("span", { cls: "paperforge-workflow-toggle-hint", text: p.hint, }), - m.addEventListener("change", async () => { - let E = t.note_path + _.addEventListener("change", async () => { + let x = t.note_path ? this.app.vault.getAbstractFileByPath(t.note_path) : null; - if (!E) { - new A.Notice("[!!] Note file not found", 6e3); + if (!x) { + new B.Notice("[!!] Note file not found", 6e3); return; } - let f = m.checked; - (await this.app.fileManager.processFrontMatter(E, (v) => { - v[p.key] = f; + let y = _.checked; + (await this.app.fileManager.processFrontMatter(x, (b) => { + b[p.key] = y; }), - this._patchCachedEntry(r, { [p.key]: f }), - (this._currentPaperEntry = je(this._currentPaperEntry, { - [p.key]: f, + this._patchCachedEntry(r, { [p.key]: y }), + (this._currentPaperEntry = Ke(this._currentPaperEntry, { + [p.key]: y, }))); })); } @@ -5158,20 +5354,20 @@ Make sure paperforge is installed and in your PATH.`, ["Note Path", t.note_path || "\u2014"], ["Fulltext Path", t.fulltext_path || "\u2014"], ], - u = new Set(["Note Path", "Fulltext Path", "Key"]); - for (let [p, _] of d) { - let m = s.createEl("div", { cls: "paperforge-technical-row" }); - m.createEl("span", { cls: "paperforge-technical-label", text: p }); - let E = m.createEl("span", { + h = new Set(["Note Path", "Fulltext Path", "Key"]); + for (let [p, m] of d) { + let _ = s.createEl("div", { cls: "paperforge-technical-row" }); + _.createEl("span", { cls: "paperforge-technical-label", text: p }); + let x = _.createEl("span", { cls: "paperforge-technical-value", - text: String(_), + text: String(m), }); - u.has(p) && - _ && - _ !== "\u2014" && - (E.addClass("pf-copy"), - E.addEventListener("click", () => { - (navigator.clipboard.writeText(_), new A.Notice(p + " copied")); + h.has(p) && + m && + m !== "\u2014" && + (x.addClass("pf-copy"), + x.addEventListener("click", () => { + (navigator.clipboard.writeText(m), new B.Notice(p + " copied")); })); } } @@ -5217,45 +5413,45 @@ Make sure paperforge is installed and in your PATH.`, }, }, s = a[n] || a.ready, - o = e.createEl("div", { cls: "paperforge-next-step-card" }); + l = e.createEl("div", { cls: "paperforge-next-step-card" }); if ( - (n === "ready" && o.addClass("ready"), - o.createEl("div", { + (n === "ready" && l.addClass("ready"), + l.createEl("div", { cls: "paperforge-next-step-label", text: "Recommended Next Step", }), - o.createEl("div", { cls: "paperforge-next-step-text", text: s.text }), + l.createEl("div", { cls: "paperforge-next-step-text", text: s.text }), s.cmd && s.cmd !== "ready") ) { - let d = o.createEl("button", { cls: "paperforge-next-step-trigger" }); + let d = l.createEl("button", { cls: "paperforge-next-step-trigger" }); (d.createEl("span", { text: s.icon + " " + s.label }), d.addEventListener("click", () => { - let u = G.find((p) => p.cmd === s.cmd); - u && this._runAction(u, d); + let h = Y.find((p) => p.cmd === s.cmd); + h && this._runAction(h, d); })); } else if (n === "/pf-deep") { - let d = o.createEl("button", { cls: "paperforge-next-step-trigger" }); - (d.createEl("span", { text: "\u{1F4CB} " + l("copy_pf_deep_cmd") }), + let d = l.createEl("button", { cls: "paperforge-next-step-trigger" }); + (d.createEl("span", { text: "\u{1F4CB} " + o("copy_pf_deep_cmd") }), d.addEventListener("click", () => { - let E = "/pf-deep " + r; + let x = "/pf-deep " + r; navigator.clipboard - .writeText(E) + .writeText(x) .then(() => { - (d.setText("\u2713 " + l("copied")), - new A.Notice(E + " copied")); + (d.setText("\u2713 " + o("copied")), + new B.Notice(x + " copied")); }) .catch(() => { - new A.Notice("[!!] Clipboard write failed", 6e3); + new B.Notice("[!!] Clipboard write failed", 6e3); }); })); - let u = + let h = ((c = (i = this.app.plugins.plugins.paperforge) == null ? void 0 : i.settings) == null ? void 0 : c.agent_platform) || "opencode", - _ = + m = { opencode: "OpenCode", claude: "Claude Code", @@ -5265,25 +5461,25 @@ Make sure paperforge is installed and in your PATH.`, codex: "Codex", gemini: "Gemini CLI", cline: "Cline", - }[u] || u; - o.createEl("div", { cls: "paperforge-agent-platform-label" }).setText( - l("run_in_agent").replace("{0}", _) + }[h] || h; + l.createEl("div", { cls: "paperforge-agent-platform-label" }).setText( + o("run_in_agent").replace("{0}", m) ); } else n === "ready" && - o + l .createEl("button", { cls: "paperforge-next-step-trigger" }) .createEl("span", { text: "\u2713 " + s.label }); } _openFulltext(e) { if (!e) { - new A.Notice("[!!] No fulltext path available for this paper", 6e3); + new B.Notice("[!!] No fulltext path available for this paper", 6e3); return; } let t = this.app.vault.getAbstractFileByPath(e); t ? this.app.workspace.openLinkText(t.path, "") - : new A.Notice("[!!] Fulltext file not found: " + e, 6e3); + : new B.Notice("[!!] Fulltext file not found: " + e, 6e3); } _renderCollectionMode() { let e = this._currentDomain || "Unknown", @@ -5299,135 +5495,135 @@ Make sure paperforge is installed and in your PATH.`, n = t.length, a = 0, s = 0, - o = 0, + l = 0, i = 0, c = 0, d = 0, - u = 0; - for (let y of t) { - (y.has_pdf && a++, - y.ocr_status === "done" && s++, - y.ocr_status === "done" && y.analyze === !0 && o++, - y.deep_reading_status === "done" && i++); - let b = y.ocr_status || ""; - b === "pending" || b === "queued" + h = 0; + for (let g of t) { + (g.has_pdf && a++, + g.ocr_status === "done" && s++, + g.ocr_status === "done" && g.analyze === !0 && l++, + g.deep_reading_status === "done" && i++); + let v = g.ocr_status || ""; + v === "pending" || v === "queued" ? c++ - : b === "processing" + : v === "processing" ? d++ - : (b === "failed" || - b === "blocked" || - b === "done_incomplete" || - b === "nopdf") && - u++; + : (v === "failed" || + v === "blocked" || + v === "done_incomplete" || + v === "nopdf") && + h++; } r.createEl("div", { cls: "paperforge-collection-header" }).createEl("div", { cls: "paperforge-collection-title", text: e, }); - let _ = r.createEl("div", { cls: "paperforge-workflow-overview" }); - _.createEl("div", { + let m = r.createEl("div", { cls: "paperforge-workflow-overview" }); + m.createEl("div", { cls: "paperforge-section-label", text: "Workflow Overview", }); - let m = _.createEl("div", { cls: "paperforge-workflow-funnel" }), - E = [ + let _ = m.createEl("div", { cls: "paperforge-workflow-funnel" }), + x = [ { value: n, label: "Total" }, { value: a, label: "PDF Ready" }, { value: s, label: "OCR Done" }, { value: i, label: "Deep Read" }, ]; - for (let y = 0; y < E.length; y++) { - let b = m.createEl("div", { cls: "paperforge-workflow-stage" }); - (b.createEl("div", { + for (let g = 0; g < x.length; g++) { + let v = _.createEl("div", { cls: "paperforge-workflow-stage" }); + (v.createEl("div", { cls: "paperforge-workflow-stage-value", - text: String(E[y].value), + text: String(x[g].value), }), - b.createEl("div", { + v.createEl("div", { cls: "paperforge-workflow-stage-label", - text: E[y].label, + text: x[g].label, }), - y < E.length - 1 && - m.createEl("div", { + g < x.length - 1 && + _.createEl("div", { cls: "paperforge-workflow-arrow", text: "\u2192", })); } - if (c + d + s + u > 0) { - let y = r.createEl("div", { cls: "paperforge-ocr-section" }), - b = y.createEl("div", { cls: "paperforge-collection-ocr-header" }); - b.createEl("h4", { cls: "paperforge-ocr-title", text: "OCR Pipeline" }); - let k = b.createEl("span", { cls: "paperforge-ocr-badge idle" }); + if (c + d + s + h > 0) { + let g = r.createEl("div", { cls: "paperforge-ocr-section" }), + v = g.createEl("div", { cls: "paperforge-collection-ocr-header" }); + v.createEl("h4", { cls: "paperforge-ocr-title", text: "OCR Pipeline" }); + let k = v.createEl("span", { cls: "paperforge-ocr-badge idle" }); d > 0 ? (k.addClass("active"), k.setText("Processing")) : c > 0 ? k.setText("Pending") : (k.addClass("idle"), k.setText("Idle")); - let C = y.createEl("div", { cls: "paperforge-progress-track" }); - d > 0 && C.addClass("paperforge-processing"); - let T = c + d + s + u, - O = [ + let S = g.createEl("div", { cls: "paperforge-progress-track" }); + d > 0 && S.addClass("paperforge-processing"); + let P = c + d + s + h, + A = [ { cls: "pending", count: c }, { cls: "active", count: d }, { cls: "done", count: s }, - { cls: "failed", count: u }, + { cls: "failed", count: h }, ]; - for (let j of O) - if (j.count > 0) { - let B = ((j.count / T) * 100).toFixed(1); - C.createEl("div", { - cls: `paperforge-progress-seg ${j.cls}`, - attr: { style: `width:${B}%` }, + for (let z of A) + if (z.count > 0) { + let O = ((z.count / P) * 100).toFixed(1); + S.createEl("div", { + cls: `paperforge-progress-seg ${z.cls}`, + attr: { style: `width:${O}%` }, }); } - let D = y.createEl("div", { cls: "paperforge-ocr-counts" }), - z = [ + let T = g.createEl("div", { cls: "paperforge-ocr-counts" }), + I = [ { cls: "pending", value: c, label: "Pending" }, { cls: "active", value: d, label: "Processing" }, { cls: "done", value: s, label: "Done" }, - { cls: "failed", value: u, label: "Attention" }, + { cls: "failed", value: h, label: "Attention" }, ]; - for (let j of z) { - let B = D.createEl("div", { cls: "paperforge-ocr-count" }); - (B.createEl("div", { + for (let z of I) { + let O = T.createEl("div", { cls: "paperforge-ocr-count" }); + (O.createEl("div", { cls: "paperforge-ocr-count-value", - text: j.value.toString(), + text: z.value.toString(), }), - B.createEl("div", { + O.createEl("div", { cls: "paperforge-ocr-count-label", - text: j.label, + text: z.label, })); } } - let f = r.createEl("div", { cls: "paperforge-collection-actions" }), - v = f.createEl("button", { cls: "paperforge-contextual-btn primary" }); - (v.createEl("span", { + let y = r.createEl("div", { cls: "paperforge-collection-actions" }), + b = y.createEl("button", { cls: "paperforge-contextual-btn primary" }); + (b.createEl("span", { cls: "paperforge-contextual-btn-icon", text: "\u229E", }), - v.createEl("span", { text: "Run OCR" }), - v.addEventListener("click", () => { - let y = G.find((b) => b.id === "paperforge-ocr"); - y && this._runAction(y, v); + b.createEl("span", { text: "Run OCR" }), + b.addEventListener("click", () => { + let g = Y.find((v) => v.id === "paperforge-ocr"); + g && this._runAction(g, b); })); - let S = f.createEl("button", { cls: "paperforge-contextual-btn" }); - (S.createEl("span", { + let w = y.createEl("button", { cls: "paperforge-contextual-btn" }); + (w.createEl("span", { cls: "paperforge-contextual-btn-icon", text: "\u21BB", }), - S.createEl("span", { text: "Sync Library" }), - S.addEventListener("click", () => { - let y = G.find((b) => b.id === "paperforge-sync"); - y && this._runAction(y, S); + w.createEl("span", { text: "Sync Library" }), + w.addEventListener("click", () => { + let g = Y.find((v) => v.id === "paperforge-sync"); + g && this._runAction(g, w); })); - let x = f.createEl("button", { cls: "paperforge-contextual-btn warn" }); - (x.createEl("span", { + let E = y.createEl("button", { cls: "paperforge-contextual-btn warn" }); + (E.createEl("span", { cls: "paperforge-contextual-btn-icon", text: "\u21BA", }), - x.createEl("span", { text: "Redo OCR" }), - x.addEventListener("click", () => { - let y = G.find((b) => b.id === "paperforge-ocr-redo"); - y && this._runAction(y, x); + E.createEl("span", { text: "Redo OCR" }), + E.addEventListener("click", () => { + let g = Y.find((v) => v.id === "paperforge-ocr-redo"); + g && this._runAction(g, E); }), this.renderSearchSection(r)); } @@ -5451,6 +5647,9 @@ Make sure paperforge is installed and in your PATH.`, case "collection": this._renderCollectionMode(); break; + case "versions": + this._renderVersionMode(); + break; } } finally { setTimeout(() => { @@ -5459,6 +5658,221 @@ Make sure paperforge is installed and in your PATH.`, } } } + _switchToVersionMode(e) { + let r = this.app.vault.adapter.basePath, + n = typeof r == "string" ? r : ""; + if (!n) { + new B.Notice("Cannot determine vault path"); + return; + } + ((this._versionPapers = tt(n)), + (this._versionFilter = ""), + (this._currentMode = "versions"), + (this._currentFilePath = null), + (this._techDetailsExpanded = !1), + this._contentEl && + (this._contentEl.empty(), + this._contentEl.removeClass("switching"), + this._renderModeHeader("versions"), + this._renderVersionMode())); + } + _renderVersionMode() { + if (!this._contentEl) return; + let e = this._contentEl.createEl("div", { + cls: "paperforge-version-panel", + }), + r = this.app.vault.adapter.basePath, + n = typeof r == "string" ? r : ""; + if (!n) { + e.createEl("div", { + cls: "paperforge-status-error", + text: "Could not determine vault path", + }); + return; + } + (!this._versionPapers || this._versionPapers.length === 0) && + (this._versionPapers = tt(n)); + let a = e.createEl("div", { cls: "paperforge-version-left" }), + s = e.createEl("div", { cls: "paperforge-version-right" }), + l = a.createEl("input", { + cls: "paperforge-version-filter", + attr: { type: "text", placeholder: o("version_filter_placeholder") }, + }); + l.value = this._versionFilter; + let i = a.createEl("div", { cls: "paperforge-version-paper-list" }), + c = () => { + i.empty(); + let b = this._versionFilter.toLowerCase(), + w = this._versionPapers + ? this._versionPapers.filter( + (g) => + !b || + g.key.toLowerCase().includes(b) || + g.title.toLowerCase().includes(b) + ) + : []; + if (w.length === 0) { + i.createEl("div", { + cls: "paperforge-meta", + text: o("version_no_backups"), + }); + return; + } + let E = i.createEl("div", { + cls: "paperforge-meta", + text: o("version_papers_count").replace("{n}", String(w.length)), + }); + for (let g of w) { + let v = i.createEl("div", { cls: "paperforge-version-paper-item" }), + k = v.createEl("span", { + cls: "paperforge-version-paper-title", + text: g.title, + }), + S = v.createEl("span", { + cls: "paperforge-version-paper-versions", + text: g.versions.map((P) => P.label).join(" "), + }); + v.addEventListener("click", () => { + (i + .querySelectorAll(".paperforge-version-paper-item.selected") + .forEach((P) => P.removeClass("selected")), + v.addClass("selected"), + h(g)); + }); + } + }; + l.addEventListener("input", () => { + ((this._versionFilter = l.value), c()); + }); + let d = s.createEl("div", { cls: "paperforge-version-timeline-area" }), + h = (b) => { + if ( + (d.empty(), + d + .createEl("div", { cls: "paperforge-version-timeline-header" }) + .createEl("span", { cls: "pf-title", text: b.title }), + b.versions.length === 0) + ) { + d.createEl("div", { + cls: "paperforge-meta", + text: o("version_no_backups"), + }); + return; + } + let E = d.createEl("div", { cls: "paperforge-version-timeline" }); + for (let g of b.versions) { + let v = g.label === b.currentLabel, + k = E.createEl("div", { + cls: + "paperforge-version-entry" + + (v ? " paperforge-version-current" : ""), + }), + S = k.createEl("div", { cls: "paperforge-version-dot" }), + P = k.createEl("div", { cls: "paperforge-version-content" }), + A = P.createEl("div", { cls: "paperforge-version-label-row" }); + (A.createEl("span", { + cls: "paperforge-version-label", + text: g.label, + }), + v && + A.createEl("span", { + cls: "paperforge-version-current-tag", + text: o("version_current"), + })); + let T = g.created_at ? g.created_at.slice(0, 10) : ""; + P.createEl("div", { + cls: "paperforge-meta", + text: T + " \u2014 " + g.source, + }); + let I = g.fulltext_size + ? g.fulltext_size > 1024 + ? (g.fulltext_size / 1024).toFixed(0) + "KB" + : g.fulltext_size + "B" + : ""; + I && P.createEl("div", { cls: "paperforge-meta", text: I }); + let z = P.createEl("div", { cls: "paperforge-version-actions" }); + (z + .createEl("button", { + cls: "pf-btn-primary", + text: o("version_restore_btn"), + }) + .addEventListener("click", () => { + Ct(n, b.key, g.label) + ? new B.Notice( + o("version_restore_done").replace("{label}", g.label) + ) + : new B.Notice("Restore failed", 6e3); + }), + b.versions.length > 1 && + !v && + z + .createEl("button", { + cls: "pf-btn-secondary", + text: o("version_compare_btn"), + }) + .addEventListener("click", () => { + m(b, g.label, b.currentLabel); + })); + } + }, + p = s.createEl("div", { cls: "paperforge-version-compare" }); + p.style.display = "none"; + let m = (b, w, E) => { + let g = Ft(n, b.key, w, E); + ((p.style.display = "block"), p.empty()); + let v = p.createEl("div", { cls: "paperforge-version-compare-header" }); + if ( + (v.createEl("span", { + cls: "pf-title", + text: o("version_compare_title") + .replace("{vA}", w) + .replace("{vB}", E), + }), + v.createEl("span", { + cls: "paperforge-meta", + text: o("version_compare_paragraphs").replace( + "{n}", + String(g.length) + ), + }), + g.length === 0) + ) { + p.createEl("div", { cls: "paperforge-meta", text: "No changes" }); + return; + } + let k = p.createEl("div", { cls: "paperforge-version-diff-list" }); + for (let S of g) { + let P = k.createEl("div", { cls: "paperforge-version-diff-row" }), + A = + S.type === "added" ? "[+]" : S.type === "removed" ? "[-]" : "[~]", + T = S.heading || "paragraph " + (S.paragraphIndex + 1); + (P.createEl("span", { + cls: "paperforge-version-diff-label", + text: A + " " + T, + }), + S.oldText && + P.createEl("pre", { + cls: "paperforge-version-diff-old", + text: S.oldText.slice(0, 200), + }), + S.newText && + P.createEl("pre", { + cls: "paperforge-version-diff-new", + text: S.newText.slice(0, 200), + })); + } + }, + _ = e.createEl("div", { cls: "paperforge-version-actions-bar" }), + x = _.createEl("button", { + cls: "pf-btn-primary", + text: o("version_restore_selected"), + }), + y = _.createEl("button", { + cls: "pf-btn-secondary", + text: o("version_clear_old").replace("{size}", ""), + }); + c(); + } renderSearchSection(e) { ((this._searchContainer = e.createEl("div", { cls: "paperforge-search-section", @@ -5507,67 +5921,67 @@ Make sure paperforge is installed and in your PATH.`, let a = this.app.vault.adapter, s = ""; if (a && typeof a == "object" && "basePath" in a) { - let m = a.basePath; - s = typeof m == "string" ? m : ""; + let _ = a.basePath; + s = typeof _ == "string" ? _ : ""; } if (!s) { this._renderSearchError("Could not determine vault path"); return; } - let o = null, + let l = null, c = this.app.plugins; if (c && typeof c == "object" && "plugins" in c) { - let m = c.plugins; - if (m && typeof m == "object" && "paperforge" in m) { - let E = m.paperforge; - E && typeof E == "object" && "settings" in E && (o = E.settings); + let _ = c.plugins; + if (_ && typeof _ == "object" && "paperforge" in _) { + let x = _.paperforge; + x && typeof x == "object" && "settings" in x && (l = x.settings); } } - let { path: d, extraArgs: u = [] } = M(s, o, void 0, void 0), - p = (0, ue.spawn)(d, [...u, "-m", "paperforge", n, r, "--json"], { + let { path: d, extraArgs: h = [] } = M(s, l, void 0, void 0), + p = (0, he.spawn)(d, [...h, "-m", "paperforge", n, r, "--json"], { cwd: s, timeout: 3e4, }), - _ = []; - (p.stdout.on("data", (m) => { - _.push(m.toString("utf-8")); + m = []; + (p.stdout.on("data", (_) => { + m.push(_.toString("utf-8")); }), p.stderr.on("data", () => {}), - p.on("close", (m) => { - if (m !== 0) { - this._renderSearchError(`Search failed (exit ${m})`); + p.on("close", (_) => { + if (_ !== 0) { + this._renderSearchError(`Search failed (exit ${_})`); return; } - let E = _.join(""), - f = E.indexOf("{"), - v = E.lastIndexOf("}"), - S = ""; - if (f !== -1 && v > f) S = E.slice(f, v + 1); + let x = m.join(""), + y = x.indexOf("{"), + b = x.lastIndexOf("}"), + w = ""; + if (y !== -1 && b > y) w = x.slice(y, b + 1); else { - let x = E.indexOf("["), - y = E.lastIndexOf("]"); - x !== -1 && y > x && (S = E.slice(x, y + 1)); + let E = x.indexOf("["), + g = x.lastIndexOf("]"); + E !== -1 && g > E && (w = x.slice(E, g + 1)); } - if (!S) { + if (!w) { this._renderSearchError("No JSON output from CLI"); return; } try { - let x = JSON.parse(S), - y = []; - if (Array.isArray(x)) y = x; - else if (x && typeof x == "object" && "results" in x) { - let b = x.results; - y = Array.isArray(b) ? b : []; + let E = JSON.parse(w), + g = []; + if (Array.isArray(E)) g = E; + else if (E && typeof E == "object" && "results" in E) { + let v = E.results; + g = Array.isArray(v) ? v : []; } - this.renderSearchResults(y, t); - } catch (x) { - let y = x instanceof Error ? x.message : String(x); - this._renderSearchError("Failed to parse results: " + y); + this.renderSearchResults(g, t); + } catch (E) { + let g = E instanceof Error ? E.message : String(E); + this._renderSearchError("Failed to parse results: " + g); } }), - p.on("error", (m) => { - this._renderSearchError("Process error: " + m.message); + p.on("error", (_) => { + this._renderSearchError("Process error: " + _.message); })); } renderSearchResults(e, t) { @@ -5595,7 +6009,7 @@ Make sure paperforge is installed and in your PATH.`, s = this._searchResultsEl.createEl("div", { cls: "paperforge-search-result-card", }), - o = + l = typeof a.title == "string" ? a.title : typeof a.file_name == "string" @@ -5603,13 +6017,13 @@ Make sure paperforge is installed and in your PATH.`, : "(untitled)", i = s.createEl("div", { cls: "paperforge-search-result-title", - text: o, + text: l, }), c = typeof a.file_path == "string" ? a.file_path : null; c && i.addEventListener("click", () => { - let u = this.app.vault.getAbstractFileByPath(c); - u instanceof A.TFile && this.app.workspace.getLeaf(!1).openFile(u); + let h = this.app.vault.getAbstractFileByPath(c); + h instanceof B.TFile && this.app.workspace.getLeaf(!1).openFile(h); }); let d = s.createEl("div", { cls: "paperforge-search-result-meta" }); if ( @@ -5636,8 +6050,8 @@ Make sure paperforge is installed and in your PATH.`, }), a.score !== void 0) ) { - let u = a.score, - p = typeof u == "number" ? u.toFixed(3) : String(u); + let h = a.score, + p = typeof h == "number" ? h.toFixed(3) : String(h); d.createEl("span", { cls: "paperforge-search-result-score", text: "Score: " + p, @@ -5652,17 +6066,17 @@ Make sure paperforge is installed and in your PATH.`, }), typeof a.abstract == "string" && a.abstract) ) { - let u = a.abstract; + let h = a.abstract; s.createEl("div", { cls: "paperforge-search-result-abstract", - text: u.length > 200 ? u.slice(0, 200) + "..." : u, + text: h.length > 200 ? h.slice(0, 200) + "..." : h, }); } if (t && typeof a.matched_text == "string" && a.matched_text) { - let u = a.matched_text; + let h = a.matched_text; s.createEl("div", { cls: "paperforge-search-result-source", - text: u.length > 300 ? u.slice(0, 300) + "..." : u, + text: h.length > 300 ? h.slice(0, 300) + "..." : h, }); } } @@ -5676,9 +6090,9 @@ Make sure paperforge is installed and in your PATH.`, })); } _runAction(e, t) { - var p, _; + var p, m; if (e.disabled) { - new A.Notice( + new B.Notice( `[i] ${e.disabledMsg || "This action is not yet available."}`, 6e3 ); @@ -5690,23 +6104,23 @@ Make sure paperforge is installed and in your PATH.`, this._showMessage("Processing...", "running"); let n = Array.isArray(e.args) ? [...e.args] : []; if (e.needsKey) { - let m = this.app.workspace.getActiveFile(), - E = null; - if (m) { - let f = this.app.metadataCache.getFileCache(m); + let _ = this.app.workspace.getActiveFile(), + x = null; + if (_) { + let y = this.app.metadataCache.getFileCache(_); if ( - (f && f.frontmatter && f.frontmatter.zotero_key - ? (E = f.frontmatter.zotero_key) - : (E = this._extractZoteroKeyFromPath(m.path)), - E) + (y && y.frontmatter && y.frontmatter.zotero_key + ? (x = y.frontmatter.zotero_key) + : (x = this._extractZoteroKeyFromPath(_.path)), + x) ) - n = [...n, E]; - else if (f && f.frontmatter) { + n = [...n, x]; + else if (y && y.frontmatter) { (this._showMessage( "[!!] No zotero_key in active note frontmatter", "error" ), - new A.Notice( + new B.Notice( "[!!] Open a paper note with a zotero_key in its frontmatter first", 6e3 ), @@ -5714,7 +6128,7 @@ Make sure paperforge is installed and in your PATH.`, return; } else { (this._showMessage("[!!] No frontmatter in active note", "error"), - new A.Notice( + new B.Notice( "[!!] The active note has no frontmatter with a zotero_key", 6e3 ), @@ -5723,7 +6137,7 @@ Make sure paperforge is installed and in your PATH.`, } } else { (this._showMessage("[!!] No active note open", "error"), - new A.Notice( + new B.Notice( "[!!] Open a paper note with a zotero_key in its frontmatter first", 6e3 ), @@ -5733,36 +6147,35 @@ Make sure paperforge is installed and in your PATH.`, } e.needsFilter && (n = [...n, "--all"]); let a = e.needsFilter ? 6e4 : e.needsKey ? 3e4 : 6e5, - { path: s, extraArgs: o = [] } = M( + { path: s, extraArgs: l = [] } = M( r, - (_ = + (m = (p = this.app.plugins.plugins.paperforge) == null ? void 0 : p.settings) != null - ? _ + ? m : null, void 0, void 0 ), - i = (0, ue.spawn)(s, [...o, "-m", "paperforge", e.cmd, ...n], { + i = (0, he.spawn)(s, [...l, "-m", "paperforge", e.cmd, ...n], { cwd: r, timeout: a, }), c = [], d = Date.now(), - u = setInterval(() => this._fetchStats(!0), 4e3); - (i.stdout.on("data", (m) => { - let E = m - .toString("utf-8") + h = setInterval(() => this._fetchStats(!0), 4e3); + (i.stdout.on("data", (_) => { + let x = _.toString("utf-8") .split( ` ` ) .filter(Boolean); - for (let f of E) { - let v = f.trim(); - v && - (c.push(v), + for (let y of x) { + let b = y.trim(); + b && + (c.push(b), this._showMessage( c.slice(-8).join(` `), @@ -5770,21 +6183,20 @@ Make sure paperforge is installed and in your PATH.`, )); } }), - i.stderr.on("data", (m) => { - let E = m - .toString("utf-8") + i.stderr.on("data", (_) => { + let x = _.toString("utf-8") .split( ` ` ) .filter(Boolean); - for (let f of E) { - if (f.includes("\r") || f.includes("%") || f.includes("\u2588")) + for (let y of x) { + if (y.includes("\r") || y.includes("%") || y.includes("\u2588")) continue; - let v = f.trim(); - v && - !v.match(/^\d+%|^\|/) && - (c.push(v), + let b = y.trim(); + b && + !b.match(/^\d+%|^\|/) && + (c.push(b), this._showMessage( c.slice(-8).join(` `), @@ -5792,78 +6204,78 @@ Make sure paperforge is installed and in your PATH.`, )); } }), - i.on("close", (m) => { - (clearInterval(u), t.removeClass("running")); - let E = ((Date.now() - d) / 1e3).toFixed(1); - if (m !== 0) { - let f = c.slice(-3).join(" | ") || "exit code " + m; - (e.cmd === "repair" || e.cmd === "ocr") && m === 1 - ? (this._showMessage("[WARN] " + f, "running"), - new A.Notice("[WARN] " + e.cmd + " partial: " + f, 8e3), + i.on("close", (_) => { + (clearInterval(h), t.removeClass("running")); + let x = ((Date.now() - d) / 1e3).toFixed(1); + if (_ !== 0) { + let y = c.slice(-3).join(" | ") || "exit code " + _; + (e.cmd === "repair" || e.cmd === "ocr") && _ === 1 + ? (this._showMessage("[WARN] " + y, "running"), + new B.Notice("[WARN] " + e.cmd + " partial: " + y, 8e3), this._fetchStats(!0)) - : (this._showMessage("[!!] " + f, "error"), - new A.Notice("[!!] " + e.cmd + " failed: " + f, 8e3)); + : (this._showMessage("[!!] " + y, "error"), + new B.Notice("[!!] " + e.cmd + " failed: " + y, 8e3)); } else if (e.needsKey || e.needsFilter) { - let f = c.join(` + let y = c.join(` `); - if (f.trim()) + if (y.trim()) try { - (JSON.parse(f), + (JSON.parse(y), navigator.clipboard - .writeText(f) + .writeText(y) .then(() => { - let v = `${E}s \u2014 ${f.length} chars copied`; - (this._showMessage("[OK] " + e.title + ": " + v, "ok"), - new A.Notice( - "[OK] " + e.okMsg + " \u2014 " + f.length + " chars" + let b = `${x}s \u2014 ${y.length} chars copied`; + (this._showMessage("[OK] " + e.title + ": " + b, "ok"), + new B.Notice( + "[OK] " + e.okMsg + " \u2014 " + y.length + " chars" )); }) - .catch((v) => { + .catch((b) => { (this._showMessage( - "[!!] Clipboard write failed: " + v.message, + "[!!] Clipboard write failed: " + b.message, "error" ), - new A.Notice("[!!] Clipboard error", 6e3)); + new B.Notice("[!!] Clipboard error", 6e3)); })); - } catch (v) { + } catch (b) { (this._showMessage("[!!] Invalid JSON from " + e.title, "error"), - new A.Notice( + new B.Notice( "[!!] " + e.title + " returned invalid JSON: " + - v.message.slice(0, 100), + b.message.slice(0, 100), 8e3 )); } else (this._showMessage("[!!] No output from context command", "error"), - new A.Notice("[!!] Context command returned empty output", 8e3)); + new B.Notice("[!!] Context command returned empty output", 8e3)); this._fetchStats(!0); } else { - let v = - c.filter((x) => x.match(/updated \d+/)).pop() || + let b = + c.filter((E) => E.match(/updated \d+/)).pop() || c[c.length - 1] || "", - S = `${E}s \u2014 ${v}`; - (this._showMessage("[OK] " + e.title + ": " + S, "ok"), - new A.Notice("[OK] " + e.okMsg), + w = `${x}s \u2014 ${b}`; + (this._showMessage("[OK] " + e.title + ": " + w, "ok"), + new B.Notice("[OK] " + e.okMsg), this._contentEl && this._contentEl.removeClass("switching"), (this._cachedStats = null)); try { this._fetchStats(!1); - } catch (x) { - console.log("[PF] fetchStats error:", x); + } catch (E) { + console.log("[PF] fetchStats error:", E); } (console.log("[PF] close cmd=" + e.cmd + " id=" + e.id), e.cmd === "sync" && - Me(this.app, this.app.plugins.plugins.paperforge, r)); + Ne(this.app, this.app.plugins.plugins.paperforge, r)); } }), - i.on("error", (m) => { + i.on("error", (_) => { (t.removeClass("running"), this._contentEl && this._contentEl.removeClass("switching"), - this._showMessage("[!!] " + m.message, "error"), - new A.Notice("[!!] Cannot start: " + m.message, 8e3)); + this._showMessage("[!!] " + _.message, "error"), + new B.Notice("[!!] Cannot start: " + _.message, 8e3)); })); } _showMessage(e, t) { @@ -5904,6 +6316,12 @@ Make sure paperforge is installed and in your PATH.`, this._headerTitle && this._headerTitle.setText("Collection"), (r = this._currentDomain || "Unknown Domain")); break; + case "versions": + (t.addClass("versions"), + t.setText(o("version_panel_title")), + this._headerTitle && + this._headerTitle.setText(o("version_panel_title"))); + break; } r && this._modeContextEl.createEl("span", { @@ -5932,18 +6350,18 @@ Make sure paperforge is installed and in your PATH.`, this._modeSubscribers.push({ event: "modify", ref: t }); } static async open(e) { - let t = e.app.workspace.getLeavesOfType(ge); + let t = e.app.workspace.getLeavesOfType(_e); if (t.length > 0) { e.app.workspace.revealLeaf(t[0]); return; } let r = e.app.workspace.getRightLeaf(!1); r && - (await r.setViewState({ type: ge, active: !0 }), + (await r.setViewState({ type: _e, active: !0 }), e.app.workspace.revealLeaf(r)); } }; -var $e = class extends V.Plugin { +var He = class extends K.Plugin { constructor() { super(...arguments); this._lastExportMtime = 0; @@ -5959,50 +6377,50 @@ var $e = class extends V.Plugin { async onload() { (await this.loadSettings(), this.saveSettings(), - tt(this.app), - this.registerView(ge, (t) => new ve(t))); + at(this.app), + this.registerView(_e, (t) => new Ee(t))); try { - (0, V.addIcon)(Se, Qe); + (0, K.addIcon)(Ce, st); } catch (t) {} - (this.addRibbonIcon(Se, "PaperForge Dashboard", () => ve.open(this)), - G.find((t) => t.id === "paperforge-ocr-redo") && + (this.addRibbonIcon(Ce, "PaperForge Dashboard", () => Ee.open(this)), + Y.find((t) => t.id === "paperforge-ocr-redo") && this.addRibbonIcon("reset", "PaperForge: Redo OCR", () => { let t = this.app.vault.adapter.basePath; - new V.Notice("PaperForge: Redo OCR starting..."); + new K.Notice("PaperForge: Redo OCR starting..."); let { path: r, extraArgs: n } = M(t, this.settings, void 0, void 0); - (0, ae.execFile)( + (0, ie.execFile)( r, [...n, "-m", "paperforge", "ocr", "redo"], { cwd: t, timeout: 6e5 }, - (a, s, o) => { + (a, s, l) => { if (a) { - new V.Notice("PaperForge: Redo OCR failed"); + new K.Notice("PaperForge: Redo OCR failed"); return; } - new V.Notice("PaperForge: Redo OCR done"); + new K.Notice("PaperForge: Redo OCR done"); } ); }), - this.addSettingTab(new Ne(this.app, this)), + this.addSettingTab(new je(this.app, this)), this.addCommand({ id: "paperforge-status-panel", - name: `PaperForge: ${l("guide_open")}`, - callback: () => ve.open(this), + name: `PaperForge: ${o("guide_open")}`, + callback: () => Ee.open(this), })); - for (let t of G) + for (let t of Y) this.addCommand({ id: t.id, name: `PaperForge: ${t.title}`, callback: () => { if (t.disabled) { - new V.Notice( + new K.Notice( `[i] ${t.disabledMsg || "This action is not yet available."}`, 6e3 ); return; } let r = this.app.vault.adapter.basePath; - new V.Notice(`PaperForge: running ${t.cmd}...`); + new K.Notice(`PaperForge: running ${t.cmd}...`); let { path: n, extraArgs: a = [] } = M( r, this.settings, @@ -6010,19 +6428,19 @@ var $e = class extends V.Plugin { void 0 ), s = Array.isArray(t.args) ? [...t.args] : []; - (0, ae.execFile)( + (0, ie.execFile)( n, [...a, "-m", "paperforge", t.cmd, ...s], { cwd: r, timeout: 3e5 }, - (o, i, c) => { - if (o) { - new V.Notice( - `[!!] ${t.cmd} failed: ${(c || o.message).slice(0, 120)}`, + (l, i, c) => { + if (l) { + new K.Notice( + `[!!] ${t.cmd} failed: ${(c || l.message).slice(0, 120)}`, 8e3 ); return; } - new V.Notice( + new K.Notice( `[OK] ${ t.okMsg || i @@ -6048,18 +6466,18 @@ var $e = class extends V.Plugin { _firstLaunchSnapshotMigration() { let e = this.app.vault.adapter.basePath; if (!e) return; - let r = ne(e).memoryStatePath; - if (!$.existsSync(r)) { + let r = Q(e).memoryStatePath; + if (!H.existsSync(r)) { let n = M(e, this.settings, void 0, void 0); [ ["runtime-health", "--json"], ["memory", "status", "--json"], ["embed", "status", "--json"], ].forEach((s) => { - let o = [...n.extraArgs, "-m", "paperforge", "--vault", e, ...s]; - (0, ae.execFile)( + let l = [...n.extraArgs, "-m", "paperforge", "--vault", e, ...s]; + (0, ie.execFile)( n.path, - o, + l, { cwd: e, timeout: 6e4, windowsHide: !0 }, () => {} ); @@ -6072,14 +6490,14 @@ var $e = class extends V.Plugin { n = this.manifest.version, a = `paperforge==${n}`, s = `git+https://github.com/LLLin000/PaperForge.git@${n}`, - o = (i, c) => { - (0, ae.spawn)(t, [...r, "-m", "pip", "install", "--upgrade", i], { + l = (i, c) => { + (0, ie.spawn)(t, [...r, "-m", "pip", "install", "--upgrade", i], { cwd: e, timeout: 12e4, - env: fe(), - }).on("close", (u) => c(u === 0)); + env: me(), + }).on("close", (h) => c(h === 0)); }; - (0, ae.execFile)( + (0, ie.execFile)( t, [...r, "-c", "import paperforge; print(paperforge.__version__)"], { cwd: e, timeout: 1e4 }, @@ -6088,19 +6506,19 @@ var $e = class extends V.Plugin { (console.log( `[PaperForge] Auto-update: trying PyPI (paperforge==${n})` ), - o(a, (_) => { - if (_) { + l(a, (m) => { + if (m) { (console.log("[PaperForge] Auto-update: installed via PyPI"), - new V.Notice(`[OK] PaperForge CLI ${p}`, 5e3)); + new K.Notice(`[OK] PaperForge CLI ${p}`, 5e3)); return; } (console.warn( "[PaperForge] Auto-update: PyPI failed, falling back to git..." ), - o(s, (m) => { - m && + l(s, (_) => { + _ && (console.log("[PaperForge] Auto-update: installed via git"), - new V.Notice(`[OK] PaperForge CLI ${p} (via git)`, 5e3)); + new K.Notice(`[OK] PaperForge CLI ${p} (via git)`, 5e3)); })); })); }; @@ -6108,8 +6526,8 @@ var $e = class extends V.Plugin { d("installed"); return; } - let u = c.trim(); - u !== n && d(`${u} -> ${n}`); + let h = c.trim(); + h !== n && d(`${h} -> ${n}`); } ); } @@ -6121,13 +6539,13 @@ var $e = class extends V.Plugin { } _checkExports(e) { if (this._autoSyncRunning) return; - let t = ne(e).exportsDir; - if (!$.existsSync(t)) return; + let t = Q(e).exportsDir; + if (!H.existsSync(t)) return; let r = 0; try { - $.readdirSync(t).forEach((n) => { + H.readdirSync(t).forEach((n) => { if (!n.endsWith(".json")) return; - let a = $.statSync(xe.join(t, n)); + let a = H.statSync(ke.join(t, n)); a.mtimeMs > r && (r = a.mtimeMs); }); } catch (n) { @@ -6145,31 +6563,31 @@ var $e = class extends V.Plugin { return; } let r = `"${t.path}" -m paperforge --vault "${e}" sync`; - (0, ae.exec)(r, { timeout: 12e4, encoding: "utf-8" }, (n, a, s) => { + (0, ie.exec)(r, { timeout: 12e4, encoding: "utf-8" }, (n, a, s) => { ((this._autoSyncRunning = !1), (this._memoryStatusText = null), n || (this._lastSyncTime = new Date().toLocaleTimeString())); try { - let o = ne(e).exportsDir, + let l = Q(e).exportsDir, i = 0; - ($.readdirSync(o).forEach((c) => { + (H.readdirSync(l).forEach((c) => { c.endsWith(".json") && - (i = Math.max(i, $.statSync(xe.join(o, c)).mtimeMs)); + (i = Math.max(i, H.statSync(ke.join(l, c)).mtimeMs)); }), (this._lastExportMtime = i)); - } catch (o) {} + } catch (l) {} }); } _checkOcr(e) { if (this._autoSyncRunning) return; - let t = ne(e).ocrDir; - if ($.existsSync(t)) + let t = Q(e).ocrDir; + if (H.existsSync(t)) try { - $.readdirSync(t, { withFileTypes: !0 }).forEach((r) => { + H.readdirSync(t, { withFileTypes: !0 }).forEach((r) => { if (!r.isDirectory()) return; - let n = xe.join(t, r.name, "meta.json"); - if (!$.existsSync(n)) return; - let a = $.statSync(n), + let n = ke.join(t, r.name, "meta.json"); + if (!H.existsSync(n)) return; + let a = H.statSync(n), s = this._lastOcrMtimes[r.name] || 0; if ( a.mtimeMs <= s || @@ -6177,13 +6595,13 @@ var $e = class extends V.Plugin { ) return; this._autoSyncRunning = !0; - let o = M(e, this.settings, void 0, void 0); - if (!o.path) { + let l = M(e, this.settings, void 0, void 0); + if (!l.path) { this._autoSyncRunning = !1; return; } - let i = `"${o.path}" -m paperforge --vault "${e}" sync`; - (0, ae.exec)(i, { timeout: 3e4, encoding: "utf-8" }, () => { + let i = `"${l.path}" -m paperforge --vault "${e}" sync`; + (0, ie.exec)(i, { timeout: 3e4, encoding: "utf-8" }, () => { ((this._autoSyncRunning = !1), (this._memoryStatusText = null)); }); }); @@ -6191,7 +6609,7 @@ var $e = class extends V.Plugin { } readPaperforgeJson() { let e = this.app.vault.adapter.basePath, - t = xe.join(e, "paperforge.json"), + t = ke.join(e, "paperforge.json"), r = { system_dir: "System", resources_dir: "Resources", @@ -6199,8 +6617,8 @@ var $e = class extends V.Plugin { base_dir: "Bases", }; try { - if (!$.existsSync(t)) return r; - let n = $.readFileSync(t, "utf-8"), + if (!H.existsSync(t)) return r; + let n = H.readFileSync(t, "utf-8"), a = JSON.parse(n), s = a.vault_config || {}; return { @@ -6222,10 +6640,10 @@ var $e = class extends V.Plugin { } savePaperforgeJson(e) { let t = this.app.vault.adapter.basePath, - r = xe.join(t, "paperforge.json"), + r = ke.join(t, "paperforge.json"), n = {}; try { - $.existsSync(r) && (n = JSON.parse($.readFileSync(r, "utf-8"))); + H.existsSync(r) && (n = JSON.parse(H.readFileSync(r, "utf-8"))); } catch (s) { console.warn("PaperForge: Failed to read paperforge.json for update", s); } @@ -6237,7 +6655,7 @@ var $e = class extends V.Plugin { for (let s of a) delete n[s]; try { if ( - ($.writeFileSync(r, JSON.stringify(n, null, 2), "utf-8"), this.settings) + (H.writeFileSync(r, JSON.stringify(n, null, 2), "utf-8"), this.settings) ) { let s = this.readPaperforgeJson(); ((this.settings.system_dir = s.system_dir), @@ -6247,22 +6665,22 @@ var $e = class extends V.Plugin { } } catch (s) { (console.error("PaperForge: Failed to write paperforge.json", s), - new V.Notice( + new K.Notice( "PaperForge: Failed to save configuration to paperforge.json" )); } } onunload() { (this._pollTimer && clearInterval(this._pollTimer), - this.app.workspace.detachLeavesOfType(ge)); + this.app.workspace.detachLeavesOfType(_e)); } async loadSettings() { - ((this.settings = Object.assign({}, Pe, await this.loadData())), + ((this.settings = Object.assign({}, Fe, await this.loadData())), this.settings.features && - Pe.features && + Fe.features && (this.settings.features = Object.assign( {}, - Pe.features, + Fe.features, this.settings.features || {} )), this.settings.frozen_skills || (this.settings.frozen_skills = {})); @@ -6275,7 +6693,7 @@ var $e = class extends V.Plugin { this.settings.python_path && this.settings.python_path.trim()) ) { let t = this.settings.python_path.trim(); - $.existsSync(t) + H.existsSync(t) ? (this.settings._python_path_stale = !1) : (console.warn( `PaperForge: Saved python_path "${t}" no longer exists - showing stale warning` @@ -6285,15 +6703,15 @@ var $e = class extends V.Plugin { } async saveSettings() { let e = {}; - for (let t of Object.keys(Pe)) + for (let t of Object.keys(Fe)) t in this.settings && (e[t] = this.settings[t]); await this.saveData(e); } _checkReleaseNotes() { let e = this.manifest.version; if (this.settings.last_seen_version === e) return; - let a = (Ke().versions || []).find((o) => o.version === e); - class s extends V.Modal { + let a = (qe().versions || []).find((l) => l.version === e); + class s extends K.Modal { constructor(i, c) { (super(i), (this._entry = c)); } @@ -6360,7 +6778,7 @@ var $e = class extends V.Plugin { e + "\uFF0C\u8BF7\u524D\u5F80\u8BBE\u7F6E \u2192 \u66F4\u65B0\u4E0E\u624B\u518C \u67E5\u770B\u5B8C\u6574\u66F4\u65B0\u8BB0\u5F55\u3002", }); - new V.Setting(i).addButton((c) => + new K.Setting(i).addButton((c) => c .setButtonText("\u77E5\u9053\u4E86") .setCta() diff --git a/paperforge/plugin/src/i18n.ts b/paperforge/plugin/src/i18n.ts index e30198da..1de673c9 100644 --- a/paperforge/plugin/src/i18n.ts +++ b/paperforge/plugin/src/i18n.ts @@ -268,6 +268,21 @@ const LANG: Record> = { maintenance_refresh_spinning: "Updating…", maintenance_all_good: "✅ All good — no action needed", maintenance_n_pending: "{n} need attention", + version_panel_title: "Version History", + version_panel_back: "Back", + version_filter_placeholder: "Filter papers...", + version_papers_count: "{n} papers", + version_current: "current", + version_restore_btn: "Restore", + version_compare_btn: "Compare", + version_restore_selected: "Restore selected", + version_clear_old: "Clear old versions (free {size})", + version_no_backups: "No version history available", + version_restore_confirm: "Restore {label} for {paper}?", + version_restore_done: "Restored {label}", + version_compare_title: "{vA} vs {vB}", + version_compare_paragraphs: "{n} paragraphs changed", + version_error_read: "Cannot read version data", }, zh: { @@ -513,6 +528,21 @@ const LANG: Record> = { maintenance_refresh_spinning: "正在更新…", maintenance_all_good: "✅ 全部正常", maintenance_n_pending: "{n} 篇需要处理", + version_panel_title: "版本历史", + version_panel_back: "返回", + version_filter_placeholder: "搜索论文...", + version_papers_count: "{n} 篇论文", + version_current: "当前", + version_restore_btn: "恢复", + version_compare_btn: "对比", + version_restore_selected: "恢复选中版本", + version_clear_old: "清除旧版本 (释放 {size})", + version_no_backups: "没有可恢复的历史版本", + version_restore_confirm: "确认将 {paper} 恢复到 {label}?", + version_restore_done: "已恢复到 {label}", + version_compare_title: "{vA} vs {vB}", + version_compare_paragraphs: "{n} 段有变化", + version_error_read: "无法读取版本数据", }, }; diff --git a/paperforge/plugin/src/services/version-history.ts b/paperforge/plugin/src/services/version-history.ts new file mode 100644 index 00000000..e620c6ae --- /dev/null +++ b/paperforge/plugin/src/services/version-history.ts @@ -0,0 +1,285 @@ +import * as fs from "fs"; +import * as path from "path"; +import { resolveVaultPaths } from "./memory-state"; + +// ── Types ── + +export interface VersionEntry { + label: string; + created_at: string; + source: string; + renderer_version?: string; + structured_content_hash?: string; + fulltext_size: number; +} + +export interface PaperVersionInfo { + key: string; + title: string; + versions: VersionEntry[]; + currentLabel: string; + totalSize: number; +} + +export interface DiffResult { + paragraphIndex: number; + heading: string; + type: "unchanged" | "added" | "removed" | "changed"; + oldText?: string; + newText?: string; +} + +// ── Helpers ── + +function ocrRoot(vaultPath: string): string { + const paths = resolveVaultPaths(vaultPath); + return paths.ocrDir; +} + +/** Read and parse manifest.json for a paper key. Returns null on any failure. */ +function readManifest( + vaultPath: string, + paperKey: string +): { versions: VersionEntry[]; current: { label: string } } | null { + const manifestPath = path.join( + ocrRoot(vaultPath), + paperKey, + "versions", + "manifest.json" + ); + try { + if (!fs.existsSync(manifestPath)) return null; + const raw = fs.readFileSync(manifestPath, "utf-8"); + const parsed: unknown = JSON.parse(raw); + if ( + parsed && + typeof parsed === "object" && + "versions" in parsed && + "current" in parsed + ) { + const obj = parsed as Record; + const versions = obj["versions"]; + const current = obj["current"]; + if ( + Array.isArray(versions) && + current && + typeof current === "object" && + "label" in current + ) { + return parsed as { + versions: VersionEntry[]; + current: { label: string }; + }; + } + } + return null; + } catch { + return null; + } +} + +/** List all directories under the OCR root (each is a paper key). */ +function listOcrPaperDirs(vaultPath: string): string[] { + const root = ocrRoot(vaultPath); + try { + if (!fs.existsSync(root)) return []; + return fs + .readdirSync(root, { withFileTypes: true }) + .filter((d) => d.isDirectory()) + .map((d) => d.name); + } catch { + return []; + } +} + +// ── Exported API ── + +/** + * Read version manifest for a single paper key. + * Returns the version list plus the current label, or null. + */ +export function scanVersions( + vaultPath: string, + paperKey: string +): { versions: VersionEntry[]; currentLabel: string } | null { + const manifest = readManifest(vaultPath, paperKey); + if (!manifest) return null; + return { + versions: manifest.versions, + currentLabel: manifest.current.label, + }; +} + +/** + * Scan all OCR paper directories for version manifests. + * Returns an array of PaperVersionInfo, one per paper that has backups. + */ +export function listPapersWithBackups(vaultPath: string): PaperVersionInfo[] { + const dirs = listOcrPaperDirs(vaultPath); + const results: PaperVersionInfo[] = []; + + for (const key of dirs) { + const manifest = readManifest(vaultPath, key); + if (!manifest) continue; + const labels = manifest.versions.map((v) => v.label); + + // Inline size estimate + let totalSize = 0; + for (const label of labels) { + const ftPath = path.join( + ocrRoot(vaultPath), + key, + "versions", + label, + "fulltext.md" + ); + try { + if (fs.existsSync(ftPath)) { + totalSize += fs.statSync(ftPath).size; + } + } catch { + // skip + } + } + + results.push({ + key, + title: key.replace(/_/g, " "), + versions: manifest.versions, + currentLabel: manifest.current.label, + totalSize, + }); + } + + results.sort((a, b) => a.title.localeCompare(b.title)); + return results; +} + +/** + * Restore a specific version's fulltext.md to the render/ directory. + * Returns true on success, false on failure. + */ +export function restoreVersion( + vaultPath: string, + paperKey: string, + label: string +): boolean { + const root = ocrRoot(vaultPath); + const sourcePath = path.join( + root, + paperKey, + "versions", + label, + "fulltext.md" + ); + const targetDir = path.join(root, paperKey, "render"); + const targetPath = path.join(targetDir, "fulltext.md"); + + try { + if (!fs.existsSync(sourcePath)) return false; + if (!fs.existsSync(targetDir)) { + fs.mkdirSync(targetDir, { recursive: true }); + } + fs.copyFileSync(sourcePath, targetPath); + return true; + } catch { + return false; + } +} + +/** + * Paragraph-level diff between two version fulltext files. + * Paragraphs are split by double-newline or `## `-prefixed headings. + * Returns a list of changed paragraphs. + */ +export function compareVersions( + vaultPath: string, + paperKey: string, + labelA: string, + labelB: string +): DiffResult[] { + const root = ocrRoot(vaultPath); + const pathA = path.join(root, paperKey, "versions", labelA, "fulltext.md"); + const pathB = path.join(root, paperKey, "versions", labelB, "fulltext.md"); + + let textA = ""; + let textB = ""; + try { + if (fs.existsSync(pathA)) textA = fs.readFileSync(pathA, "utf-8"); + } catch { + /* empty */ + } + try { + if (fs.existsSync(pathB)) textB = fs.readFileSync(pathB, "utf-8"); + } catch { + /* empty */ + } + + const paragraphsA = splitParagraphs(textA); + const paragraphsB = splitParagraphs(textB); + + const maxLen = Math.max(paragraphsA.length, paragraphsB.length); + const results: DiffResult[] = []; + + for (let i = 0; i < maxLen; i++) { + const oldText = i < paragraphsA.length ? paragraphsA[i] : ""; + const newText = i < paragraphsB.length ? paragraphsB[i] : ""; + const firstHdrLine = (oldText || newText).split("\n")[0] ?? ""; + const heading = firstHdrLine.startsWith("## ") + ? firstHdrLine.replace(/^##\s+/, "") + : ""; + + let type: DiffResult["type"] = "unchanged"; + if (!oldText && newText) { + type = "added"; + } else if (oldText && !newText) { + type = "removed"; + } else if (oldText !== newText) { + type = "changed"; + } + + if (type !== "unchanged") { + results.push({ + paragraphIndex: i, + heading, + type, + oldText: oldText || undefined, + newText: newText || undefined, + }); + } + } + + return results; +} + +// ── Internal diff helpers ── + +function splitParagraphs(text: string): string[] { + // Split by `## `-prefixed headings or double-newline blocks + const lines = text.split("\n"); + const blocks: string[] = []; + let current: string[] = []; + + for (const line of lines) { + if (line.startsWith("## ") && current.length > 0) { + blocks.push(current.join("\n").trim()); + current = [line]; + } else if (line.trim() === "" && current.length > 0) { + // Double-newline boundary: commit if the last committed is not empty + const joined = current.join("\n").trim(); + if (joined) { + blocks.push(joined); + current = []; + } + } else { + current.push(line); + } + } + + if (current.length > 0) { + const joined = current.join("\n").trim(); + if (joined) blocks.push(joined); + } + + return blocks; +} diff --git a/paperforge/plugin/src/views/dashboard.ts b/paperforge/plugin/src/views/dashboard.ts index e7ad5baf..9f0f072c 100644 --- a/paperforge/plugin/src/views/dashboard.ts +++ b/paperforge/plugin/src/views/dashboard.ts @@ -44,6 +44,13 @@ import { import { getDisclosureState, toggleDisclosureState } from "../utils/disclosure"; import { extractZoteroKeyFromPath } from "../utils/zotero-path"; import { checkOrphanState } from "./modals"; +import { + type PaperVersionInfo, + listPapersWithBackups, + scanVersions, + restoreVersion, + compareVersions, +} from "../services/version-history"; // ── Interface for plugin ref used by static open ── @@ -55,7 +62,7 @@ interface IPluginRef { } export class PaperForgeStatusView extends ItemView { - _currentMode: "global" | "paper" | "collection" | null = null; + _currentMode: "global" | "paper" | "collection" | "versions" | null = null; _currentDomain: string | null = null; _currentPaperKey: string | null = null; _currentPaperEntry: Record | null = null; @@ -81,6 +88,10 @@ export class PaperForgeStatusView extends ItemView { _ocrCounts: HTMLElement | null = null; _driftBannerEl: HTMLElement | null = null; // ── Search state ── + // ── Version state ── + _versionPapers: PaperVersionInfo[] | null = null; + _versionFilter: string = ""; + // ── Search state ── _searchContainer: HTMLElement | null = null; _searchInput: HTMLInputElement | null = null; _searchResultsEl: HTMLElement | null = null; @@ -919,6 +930,9 @@ export class PaperForgeStatusView extends ItemView { case "collection": this._renderCollectionMode(); break; + case "versions": + this._renderVersionMode(); + break; } } @@ -1353,6 +1367,14 @@ export class PaperForgeStatusView extends ItemView { this._openFulltext(entry.fulltext_path) ); } + // Version history button — always visible, versions mode handles empty state + const verBtn = stripRight.createEl("button", { + cls: "paperforge-contextual-btn", + }); + verBtn.createEl("span", { text: t("version_panel_title") }); + verBtn.addEventListener("click", () => { + this._switchToVersionMode(key!); + }); this._renderPaperOverviewCard(view, entry); if (entry.next_step === "ready" && entry.deep_reading_status === "done") { const complete = view.createEl("div", { cls: "paperforge-complete-row" }); @@ -2028,6 +2050,9 @@ export class PaperForgeStatusView extends ItemView { case "collection": this._renderCollectionMode(); break; + case "versions": + this._renderVersionMode(); + break; } } finally { setTimeout(() => { @@ -2036,6 +2061,303 @@ export class PaperForgeStatusView extends ItemView { } } + /* ── Switch to Version Mode ── */ + _switchToVersionMode(paperKey: string) { + const adapter = this.app.vault.adapter as unknown as Record< + string, + unknown + >; + const vp = adapter.basePath; + const vaultPath = typeof vp === "string" ? vp : ""; + if (!vaultPath) { + new Notice("Cannot determine vault path"); + return; + } + this._versionPapers = listPapersWithBackups(vaultPath); + this._versionFilter = ""; + this._currentMode = "versions"; + this._currentFilePath = null; + this._techDetailsExpanded = false; + if (!this._contentEl) return; + this._contentEl.empty(); + this._contentEl.removeClass("switching"); + this._renderModeHeader("versions"); + this._renderVersionMode(); + } + + /* ── Version Mode Render: File Recovery-style Panel ── */ + _renderVersionMode() { + if (!this._contentEl) return; + const view = this._contentEl.createEl("div", { + cls: "paperforge-version-panel", + }); + + const adapter = this.app.vault.adapter as unknown as Record< + string, + unknown + >; + const vp = adapter.basePath; + const vaultPath = typeof vp === "string" ? vp : ""; + if (!vaultPath) { + view.createEl("div", { + cls: "paperforge-status-error", + text: "Could not determine vault path", + }); + return; + } + + // Re-scan if null + if (!this._versionPapers || this._versionPapers.length === 0) { + this._versionPapers = listPapersWithBackups(vaultPath); + } + + // ── Left Panel: Filter + Paper List ── + const left = view.createEl("div", { cls: "paperforge-version-left" }); + const right = view.createEl("div", { cls: "paperforge-version-right" }); + + // Filter input + const filterInput = left.createEl("input", { + cls: "paperforge-version-filter", + attr: { type: "text", placeholder: t("version_filter_placeholder") }, + }) as HTMLInputElement; + filterInput.value = this._versionFilter; + + // Paper list container + const paperList = left.createEl("div", { + cls: "paperforge-version-paper-list", + }); + + const renderPaperList = () => { + paperList.empty(); + const filter = this._versionFilter.toLowerCase(); + const filtered = this._versionPapers + ? this._versionPapers.filter( + (p) => + !filter || + p.key.toLowerCase().includes(filter) || + p.title.toLowerCase().includes(filter) + ) + : []; + + if (filtered.length === 0) { + paperList.createEl("div", { + cls: "paperforge-meta", + text: t("version_no_backups"), + }); + return; + } + + const countLabel = paperList.createEl("div", { + cls: "paperforge-meta", + text: t("version_papers_count").replace("{n}", String(filtered.length)), + }); + + for (const paper of filtered) { + const row = paperList.createEl("div", { + cls: "paperforge-version-paper-item", + }); + const titleEl = row.createEl("span", { + cls: "paperforge-version-paper-title", + text: paper.title, + }); + const badge = row.createEl("span", { + cls: "paperforge-version-paper-versions", + text: paper.versions.map((v) => v.label).join(" "), + }); + row.addEventListener("click", () => { + // Highlight selected, show version timeline in right panel + paperList + .querySelectorAll(".paperforge-version-paper-item.selected") + .forEach((el) => el.removeClass("selected")); + row.addClass("selected"); + renderTimeline(paper); + }); + } + }; + + // Filter on input + filterInput.addEventListener("input", () => { + this._versionFilter = filterInput.value; + renderPaperList(); + }); + + // ── Right Panel: Timeline ── + const timelineArea = right.createEl("div", { + cls: "paperforge-version-timeline-area", + }); + + const renderTimeline = (paper: PaperVersionInfo) => { + timelineArea.empty(); + const header = timelineArea.createEl("div", { + cls: "paperforge-version-timeline-header", + }); + header.createEl("span", { cls: "pf-title", text: paper.title }); + + if (paper.versions.length === 0) { + timelineArea.createEl("div", { + cls: "paperforge-meta", + text: t("version_no_backups"), + }); + return; + } + + // Version list as timeline + const timeline = timelineArea.createEl("div", { + cls: "paperforge-version-timeline", + }); + + for (const ver of paper.versions) { + const isCurrent = ver.label === paper.currentLabel; + const entry = timeline.createEl("div", { + cls: + "paperforge-version-entry" + + (isCurrent ? " paperforge-version-current" : ""), + }); + const dot = entry.createEl("div", { cls: "paperforge-version-dot" }); + const content = entry.createEl("div", { + cls: "paperforge-version-content", + }); + const labelRow = content.createEl("div", { + cls: "paperforge-version-label-row", + }); + labelRow.createEl("span", { + cls: "paperforge-version-label", + text: ver.label, + }); + if (isCurrent) { + labelRow.createEl("span", { + cls: "paperforge-version-current-tag", + text: t("version_current"), + }); + } + const dateStr = ver.created_at ? ver.created_at.slice(0, 10) : ""; + content.createEl("div", { + cls: "paperforge-meta", + text: dateStr + " \u2014 " + ver.source, + }); + const sizeStr = ver.fulltext_size + ? ver.fulltext_size > 1024 + ? (ver.fulltext_size / 1024).toFixed(0) + "KB" + : ver.fulltext_size + "B" + : ""; + if (sizeStr) { + content.createEl("div", { cls: "paperforge-meta", text: sizeStr }); + } + + // Action buttons + const actions = content.createEl("div", { + cls: "paperforge-version-actions", + }); + const restoreBtn = actions.createEl("button", { + cls: "pf-btn-primary", + text: t("version_restore_btn"), + }); + restoreBtn.addEventListener("click", () => { + const ok = restoreVersion(vaultPath, paper.key, ver.label); + if (ok) { + new Notice(t("version_restore_done").replace("{label}", ver.label)); + } else { + new Notice("Restore failed", 6000); + } + }); + + if (paper.versions.length > 1 && !isCurrent) { + const compareBtn = actions.createEl("button", { + cls: "pf-btn-secondary", + text: t("version_compare_btn"), + }); + compareBtn.addEventListener("click", () => { + renderComparison(paper, ver.label, paper.currentLabel); + }); + } + } + }; + + // ── Comparison Area ── + const compareArea = right.createEl("div", { + cls: "paperforge-version-compare", + }); + compareArea.style.display = "none"; + + const renderComparison = ( + paper: PaperVersionInfo, + vA: string, + vB: string + ) => { + const diffs = compareVersions(vaultPath, paper.key, vA, vB); + compareArea.style.display = "block"; + compareArea.empty(); + const header = compareArea.createEl("div", { + cls: "paperforge-version-compare-header", + }); + header.createEl("span", { + cls: "pf-title", + text: t("version_compare_title") + .replace("{vA}", vA) + .replace("{vB}", vB), + }); + header.createEl("span", { + cls: "paperforge-meta", + text: t("version_compare_paragraphs").replace( + "{n}", + String(diffs.length) + ), + }); + + if (diffs.length === 0) { + compareArea.createEl("div", { + cls: "paperforge-meta", + text: "No changes", + }); + return; + } + + const diffList = compareArea.createEl("div", { + cls: "paperforge-version-diff-list", + }); + for (const d of diffs) { + const diffRow = diffList.createEl("div", { + cls: "paperforge-version-diff-row", + }); + const typeLabel = + d.type === "added" ? "[+]" : d.type === "removed" ? "[-]" : "[~]"; + const headingLabel = d.heading || "paragraph " + (d.paragraphIndex + 1); + diffRow.createEl("span", { + cls: "paperforge-version-diff-label", + text: typeLabel + " " + headingLabel, + }); + if (d.oldText) { + diffRow.createEl("pre", { + cls: "paperforge-version-diff-old", + text: d.oldText.slice(0, 200), + }); + } + if (d.newText) { + diffRow.createEl("pre", { + cls: "paperforge-version-diff-new", + text: d.newText.slice(0, 200), + }); + } + } + }; + + // ── Bottom Action Bar ── + const actionBar = view.createEl("div", { + cls: "paperforge-version-actions-bar", + }); + const restoreSelectedBtn = actionBar.createEl("button", { + cls: "pf-btn-primary", + text: t("version_restore_selected"), + }); + const clearOldBtn = actionBar.createEl("button", { + cls: "pf-btn-secondary", + text: t("version_clear_old").replace("{size}", ""), + }); + + // Initial render + renderPaperList(); + } + /* ── Search Section ── */ renderSearchSection(view: HTMLElement) { @@ -2557,6 +2879,12 @@ export class PaperForgeStatusView extends ItemView { if (this._headerTitle) this._headerTitle.setText("Collection"); modeName = this._currentDomain || "Unknown Domain"; break; + case "versions": + badge.addClass("versions"); + badge.setText(t("version_panel_title")); + if (this._headerTitle) + this._headerTitle.setText(t("version_panel_title")); + break; } if (modeName) { this._modeContextEl.createEl("span", { diff --git a/paperforge/plugin/styles.css b/paperforge/plugin/styles.css index d10ef0b3..d62306cb 100644 --- a/paperforge/plugin/styles.css +++ b/paperforge/plugin/styles.css @@ -4001,3 +4001,255 @@ color: var(--text-muted); font-size: 13px; } + +/* ========================================================================== + SECTION 9 — Version History Panel (File Recovery-style) + ========================================================================== */ + +.paperforge-version-panel { + display: flex; + flex-direction: row; + gap: 0; + height: 100%; + min-height: 300px; + border: 1px solid var(--pf-border); + border-radius: var(--pf-radius); + overflow: hidden; + background: var(--pf-surface); +} + +/* ── Left Panel ── */ +.paperforge-version-left { + width: 240px; + min-width: 200px; + border-right: 1px solid var(--pf-border); + display: flex; + flex-direction: column; + background: var(--background-primary); + overflow: hidden; +} + +.paperforge-version-filter { + margin: 8px; + padding: 6px 10px; + border: 1px solid var(--pf-border); + border-radius: var(--radius-s); + background: var(--background-primary); + color: var(--text-normal); + font-size: 13px; + outline: none; +} + +.paperforge-version-filter:focus { + border-color: var(--pf-accent); +} + +.paperforge-version-paper-list { + flex: 1; + overflow-y: auto; + padding: 4px 0; +} + +.paperforge-version-paper-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 12px; + cursor: pointer; + transition: background 0.12s; + border-left: 3px solid transparent; +} + +.paperforge-version-paper-item:hover { + background: var(--background-modifier-hover); +} + +.paperforge-version-paper-item.selected { + background: var(--background-modifier-hover); + border-left-color: var(--pf-accent); +} + +.paperforge-version-paper-title { + font-size: 13px; + font-weight: 500; + color: var(--text-normal); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex: 1; +} + +.paperforge-version-paper-versions { + font-size: 11px; + color: var(--text-muted); + margin-left: 6px; + white-space: nowrap; +} + +/* ── Right Panel ── */ +.paperforge-version-right { + flex: 1; + display: flex; + flex-direction: column; + overflow-y: auto; + padding: 12px; + gap: 12px; +} + +.paperforge-version-timeline-area { + flex: 1; +} + +.paperforge-version-timeline-header { + margin-bottom: 12px; +} + +/* ── Timeline ── */ +.paperforge-version-timeline { + display: flex; + flex-direction: column; + gap: 0; + position: relative; + padding-left: 20px; +} + +.paperforge-version-timeline::before { + content: ""; + position: absolute; + left: 6px; + top: 10px; + bottom: 10px; + width: 2px; + background: var(--pf-border); + border-radius: 1px; +} + +.paperforge-version-entry { + display: flex; + gap: 10px; + padding: 8px 0; + position: relative; +} + +.paperforge-version-dot { + position: relative; + z-index: 1; + width: 14px; + height: 14px; + min-width: 14px; + border-radius: 50%; + background: var(--pf-border); + border: 2px solid var(--pf-surface); + margin-top: 3px; +} + +.paperforge-version-current .paperforge-version-dot { + background: var(--text-success); + border-color: var(--pf-surface); +} + +.paperforge-version-content { + flex: 1; + display: flex; + flex-direction: column; + gap: 2px; +} + +.paperforge-version-label-row { + display: flex; + align-items: center; + gap: 6px; +} + +.paperforge-version-label { + font-size: 14px; + font-weight: 600; + color: var(--text-normal); +} + +.paperforge-version-current-tag { + font-size: 11px; + font-weight: 500; + color: var(--text-success); + background: color-mix(in srgb, var(--text-success) 12%, transparent); + padding: 1px 6px; + border-radius: 4px; +} + +.paperforge-version-actions { + display: flex; + gap: 6px; + margin-top: 4px; +} + +/* ── Bottom Action Bar ── */ +.paperforge-version-actions-bar { + display: flex; + gap: 8px; + padding: 10px 12px; + border-top: 1px solid var(--pf-border); + background: var(--pf-surface); +} + +/* ── Comparison / Diff Preview ── */ +.paperforge-version-compare { + border: 1px solid var(--pf-border); + border-radius: var(--pf-radius); + padding: 10px; + background: var(--pf-surface-alt); + max-height: 300px; + overflow-y: auto; +} + +.paperforge-version-compare-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; +} + +.paperforge-version-diff-list { + display: flex; + flex-direction: column; + gap: 6px; +} + +.paperforge-version-diff-row { + padding: 6px 8px; + border-radius: var(--radius-s); + background: var(--background-primary); +} + +.paperforge-version-diff-label { + font-size: 12px; + font-weight: 600; + color: var(--text-normal); + display: block; + margin-bottom: 2px; +} + +.paperforge-version-diff-old { + font-size: 12px; + color: var(--text-error); + margin: 2px 0; + padding: 2px 4px; + background: color-mix(in srgb, var(--text-error) 6%, transparent); + border-radius: 2px; + white-space: pre-wrap; + word-break: break-word; + max-height: 60px; + overflow-y: auto; +} + +.paperforge-version-diff-new { + font-size: 12px; + color: var(--text-success); + margin: 2px 0; + padding: 2px 4px; + background: color-mix(in srgb, var(--text-success) 6%, transparent); + border-radius: 2px; + white-space: pre-wrap; + word-break: break-word; + max-height: 60px; + overflow-y: auto; +} diff --git a/tests/test_deep_search.py b/tests/test_deep_search.py new file mode 100644 index 00000000..e66c39f4 --- /dev/null +++ b/tests/test_deep_search.py @@ -0,0 +1,381 @@ +"""Tests for @ Deep Search: query rewrite + hybrid retrieval.""" + +from __future__ import annotations + +import json +import sqlite3 +from pathlib import Path +from unittest.mock import patch + +import pytest + +import paperforge.config +from paperforge.worker._utils import pipeline_paths as _pp + +paperforge.config.pipeline_paths = _pp + +from paperforge.embedding.query_rewrite import expand_query, ABBREVIATIONS, MEDICAL_SYNONYMS +from paperforge.embedding.search import hybrid_search, _bm25_search, _fuse_results +from paperforge.memory.db import get_connection, get_memory_db_path +from paperforge.memory.schema import ( + CREATE_BODY_UNITS, + CREATE_BODY_UNITS_FTS, + CREATE_PAPERS, + CREATE_OBJECT_UNITS, + CREATE_VEC_BODY, + CREATE_VEC_BODY_META, + CREATE_VEC_OBJECTS, + CREATE_VEC_OBJECTS_META, + ensure_schema, +) + + +EMBEDDING_DIM = 1536 + + +# --------------------------------------------------------------------------- +# Mock provider — deterministic fixed embeddings +# --------------------------------------------------------------------------- +class FixedProvider: + """Provider that returns deterministic embeddings for any text.""" + + def __init__(self, vault: Path | None = None): + self.vault = vault + + def encode(self, texts: list[str]) -> list[list[float]]: + import hashlib + + result = [] + for t in texts: + h = hashlib.sha256(t.encode()).digest() + vec = [(h[i % 32] / 255.0) for i in range(EMBEDDING_DIM)] + result.append(vec) + return result + + def encode_single(self, text: str) -> list[float]: + return self.encode([text])[0] + + +@pytest.fixture +def mock_provider(): + return FixedProvider() + + +@pytest.fixture(autouse=True) +def _patch_providers(mock_provider): + """Replace OpenAICompatibleProvider with the fixed mock.""" + with patch("paperforge.embedding.search.OpenAICompatibleProvider", return_value=mock_provider): + yield + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- +def _create_tables(conn: sqlite3.Connection) -> None: + """Create tables needed for deep search tests.""" + conn.executescript(CREATE_PAPERS) + conn.executescript(CREATE_BODY_UNITS) + conn.executescript(CREATE_BODY_UNITS_FTS) + conn.executescript(CREATE_OBJECT_UNITS) + conn.commit() + + +def _seed_paper(conn: sqlite3.Connection, paper_id: str, title: str) -> None: + """Insert a paper row.""" + conn.execute( + """INSERT OR IGNORE INTO papers (zotero_key, title, year, first_author, journal, domain) + VALUES (?, ?, ?, ?, ?, ?)""", + (paper_id, title, "2023", "Author", "Test Journal", "ortho"), + ) + conn.commit() + + +def _seed_body_unit( + conn: sqlite3.Connection, + unit_id: str, + paper_id: str, + text: str, + section_title: str = "Methods", + section_path: str = "methods", + indexable: int = 1, +) -> None: + """Insert a body unit and its FTS index entry.""" + conn.execute( + """INSERT OR REPLACE INTO body_units + (unit_id, paper_id, section_path, section_path_json, section_level, section_title, + unit_text, unit_kind, part_ordinal, page_span_json, block_span_json, + token_estimate, indexable, veto_reason, quality_hints_json) + VALUES (?, ?, ?, '[]', 1, ?, ?, 'body', 0, '[]', '[]', 10, ?, '', '[]')""", + (unit_id, paper_id, section_path, section_title, text, indexable), + ) + # FTS5 external content table may raise on DELETE when index is empty + try: + conn.execute( + "DELETE FROM body_units_fts WHERE unit_id = ?", + (unit_id,), + ) + except sqlite3.DatabaseError: + pass + conn.execute( + """INSERT INTO body_units_fts(rowid, unit_id, paper_id, section_path, unit_text) + SELECT rowid, unit_id, paper_id, section_path, unit_text + FROM body_units WHERE unit_id = ?""", + (unit_id,), + ) + conn.commit() + + +def _create_vec_tables(conn: sqlite3.Connection) -> None: + """Create vec0 tables (if sqlite-vec is available).""" + try: + from paperforge.memory.db import ensure_vec_extension + + ensure_vec_extension(conn) + conn.executescript(CREATE_VEC_BODY) + conn.executescript(CREATE_VEC_BODY_META) + conn.executescript(CREATE_VEC_OBJECTS) + conn.executescript(CREATE_VEC_OBJECTS_META) + conn.commit() + return True + except Exception: + return False + + +# --------------------------------------------------------------------------- +# Tests: query rewrite +# --------------------------------------------------------------------------- + +class TestQueryRewrite: + def test_expand_abbreviation(self): + """VTE -> venous thromboembolism.""" + variants = expand_query("VTE prevention") + assert "VTE prevention" in variants + assert "venous thromboembolism prevention" in variants + + def test_expand_multiple_abbreviations(self): + """Each abbreviation produces a separate variant.""" + variants = expand_query("ACL VTE") + assert len(variants) >= 2 + + def test_no_abbreviation_no_expansion(self): + """No expansion when no abbreviations or synonyms are present.""" + variants = expand_query("randomized controlled trial design") + assert len(variants) == 1 + + def test_synonym_expansion_fallback(self): + """Synonym expansion when no abbreviations found.""" + variants = expand_query("knee fracture") + assert len(variants) >= 2 # original + at least one expanded form + + def test_abbreviations_dict_populated(self): + """The abbreviations dictionary has entries.""" + assert len(ABBREVIATIONS) > 5 + + def test_synonyms_dict_populated(self): + """The synonyms dictionary has entries.""" + assert len(MEDICAL_SYNONYMS) > 3 + + +# --------------------------------------------------------------------------- +# Tests: hybrid search +# --------------------------------------------------------------------------- + +class TestHybridSearch: + def test_bm25_only_fallback(self, tmp_path): + """hybrid_search returns BM25 results when vec0 is unavailable.""" + vault = tmp_path + db_path = get_memory_db_path(vault) + db_path.parent.mkdir(parents=True, exist_ok=True) + + conn = get_connection(db_path) + _create_tables(conn) + _seed_paper(conn, "p1", "ACL Reconstruction Study") + _seed_body_unit(conn, "p1:u1", "p1", "The anterior cruciate ligament reconstruction technique used patellar tendon graft.", section_title="Methods") + _seed_body_unit(conn, "p1:u2", "p1", "Patients were followed for 2 years post-operatively.", section_title="Results") + conn.close() + + results = hybrid_search(vault, "ACL reconstruction", limit=5) + assert isinstance(results, list) + # Should find BM25 content + assert len(results) >= 1 + assert results[0]["paper_id"] == "p1" + assert "source" in results[0] + + def test_hybrid_with_vec0(self, tmp_path, mock_provider): + """hybrid_search runs BM25 + vec0 when vec0 tables exist.""" + vault = tmp_path + db_path = get_memory_db_path(vault) + db_path.parent.mkdir(parents=True, exist_ok=True) + + conn = get_connection(db_path) + _create_tables(conn) + vec_ok = _create_vec_tables(conn) + _seed_paper(conn, "p1", "VTE Prophylaxis Study") + _seed_body_unit(conn, "p1:u1", "p1", "Venous thromboembolism prophylaxis after total knee arthroplasty.", section_title="Methods") + conn.close() + + results = hybrid_search(vault, "VTE prophylaxis", limit=5) + assert isinstance(results, list) + + def test_empty_results(self, tmp_path): + """hybrid_search returns empty list when nothing matches.""" + vault = tmp_path + db_path = get_memory_db_path(vault) + db_path.parent.mkdir(parents=True, exist_ok=True) + + conn = get_connection(db_path) + _create_tables(conn) + _seed_paper(conn, "p1", "Some Study") + _seed_body_unit(conn, "p1:u1", "p1", "Completely unrelated content about plants.", section_title="Intro") + conn.close() + + results = hybrid_search(vault, "zzzznotfoundzzzz", limit=5) + assert isinstance(results, list) + assert len(results) == 0 + + def test_result_fields(self, tmp_path): + """Each result has the expected fields.""" + vault = tmp_path + db_path = get_memory_db_path(vault) + db_path.parent.mkdir(parents=True, exist_ok=True) + + conn = get_connection(db_path) + _create_tables(conn) + _seed_paper(conn, "p1", "Knee OA Study") + _seed_body_unit(conn, "p1:u1", "p1", "Osteoarthritis of the knee joint is common.", section_title="Introduction") + conn.close() + + results = hybrid_search(vault, "knee osteoarthritis", limit=5) + if results: + r = results[0] + assert "paper_id" in r + assert "title" in r + assert "source" in r + assert "text" in r + assert "score" in r + assert "heading" in r + assert r["source"] == "body_unit" + + def test_query_rewrite_broadens_bm25(self, tmp_path): + """Query with abbreviation finds content via expanded form.""" + vault = tmp_path + db_path = get_memory_db_path(vault) + db_path.parent.mkdir(parents=True, exist_ok=True) + + conn = get_connection(db_path) + _create_tables(conn) + _seed_paper(conn, "p1", "ACL Study") + # Only contains the full form, not the abbreviation + _seed_body_unit(conn, "p1:u1", "p1", "We reviewed anterior cruciate ligament reconstruction outcomes.", section_title="Methods") + # Contains neither + _seed_body_unit(conn, "p1:u2", "p1", "Patients were evaluated at 12 months.", section_title="Results") + conn.close() + + results = hybrid_search(vault, "ACL reconstruction", limit=5) + # Should find the expanded form + matched = [r for r in results if "anterior cruciate ligament" in (r.get("text", "") or "")] + assert len(matched) >= 1 + + +# --------------------------------------------------------------------------- +# Tests: score fusion logic +# --------------------------------------------------------------------------- + +class TestScoreFusion: + def test_fuse_bm25_only(self): + """Fusion returns BM25 scores unchanged when vec results are empty.""" + bm25 = [ + {"paper_id": "p1", "text": "text1", "bm25_score": 0.8, "vec_score": 0.0, "source": "body_unit", + "title": "T1", "first_author": "A", "year": "2023", "journal": "J", "domain": "ortho", + "heading": "Methods", "unit_id": "p1:u1", "matched_terms": ""}, + ] + vec: list[dict] = [] + fused = _fuse_results(bm25, vec, limit=5) + assert len(fused) == 1 + assert fused[0]["score"] == 0.8 + assert fused[0]["vec_score"] == 0.0 + + def test_fuse_hybrid(self): + """Fusion combines BM25 and vec scores.""" + bm25 = [ + {"paper_id": "p1", "text": "text1", "bm25_score": 0.5, "vec_score": 0.0, "source": "body_unit", + "title": "T1", "first_author": "A", "year": "2023", "journal": "J", "domain": "ortho", + "heading": "Methods", "unit_id": "p1:u1", "matched_terms": ""}, + ] + vec = [ + {"paper_id": "p1", "text": "text1", "source": "body_unit", "vec_score": 0.8}, + ] + fused = _fuse_results(bm25, vec, limit=5) + assert len(fused) == 1 + # combined = 0.3 * bm25_norm + 0.7 * vec_norm + # bm25_norm = 0.5 (already normalized) + # vec_norm = 1 - 1/(1+0.8) = 0.4444 + # combined = 0.3*0.5 + 0.7*0.4444 = 0.15 + 0.3111 = 0.4611 + assert fused[0]["score"] > 0.4 + assert fused[0]["score"] < 0.5 + + def test_fuse_deduplicates(self): + """Fusion deduplicates by (paper_id, text).""" + bm25 = [ + {"paper_id": "p1", "text": "same text", "bm25_score": 0.8, "vec_score": 0.0, "source": "body_unit", + "title": "T1", "first_author": "A", "year": "2023", "journal": "J", "domain": "ortho", + "heading": "Methods", "unit_id": "p1:u1", "matched_terms": ""}, + {"paper_id": "p1", "text": "same text", "bm25_score": 0.6, "vec_score": 0.0, "source": "body_unit", + "title": "T1", "first_author": "A", "year": "2023", "journal": "J", "domain": "ortho", + "heading": "Results", "unit_id": "p1:u2", "matched_terms": ""}, + ] + vec: list[dict] = [] + fused = _fuse_results(bm25, vec, limit=5) + assert len(fused) == 1 # deduplicated by text + + def test_vec_only_fallback(self): + """When BM25 is empty, fusion falls back to vec results.""" + bm25: list[dict] = [] + vec = [ + {"paper_id": "p1", "text": "vec result text", "source": "body_unit", "vec_score": 0.75}, + ] + fused = _fuse_results(bm25, vec, limit=5) + assert len(fused) == 1 + assert fused[0]["paper_id"] == "p1" + assert fused[0]["vec_score"] > 0.7 + + +# --------------------------------------------------------------------------- +# Tests: BM25 search internals +# --------------------------------------------------------------------------- + +class TestBM25Search: + def test_bm25_finds_matching_content(self, tmp_path): + """_bm25_search finds matching body units.""" + db_path = get_memory_db_path(tmp_path) + db_path.parent.mkdir(parents=True, exist_ok=True) + conn = get_connection(db_path) + _create_tables(conn) + _seed_paper(conn, "p1", "Test Paper") + _seed_body_unit(conn, "p1:u1", "p1", "This text discusses knee replacement outcomes and complications.", section_title="Methods") + _seed_body_unit(conn, "p1:u2", "p1", "Introduction paragraph with general background.", section_title="Introduction") + conn.close() + + conn = get_connection(db_path, read_only=True) + try: + results = _bm25_search(conn, ["knee replacement"], limit=10) + assert len(results) >= 1 + assert results[0]["paper_id"] == "p1" + finally: + conn.close() + + +# --------------------------------------------------------------------------- +# Test CLI integration +# --------------------------------------------------------------------------- + +class TestDeepFlag: + def test_deep_flag_parses(self): + """--deep flag is accepted by the CLI parser.""" + import argparse + from paperforge.cli import build_parser + + parser = build_parser() + args = parser.parse_args(["retrieve", "VTE prevention", "--deep", "--json"]) + assert args.deep is True + assert args.command == "retrieve"