Skip to content

Commit d3781ab

Browse files
committed
Fix report sources and card highlights
1 parent 7ce7ea2 commit d3781ab

4 files changed

Lines changed: 176 additions & 6 deletions

File tree

deployments/desktop/shared/agents.py

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,12 @@ def _reading_report_dirs() -> List[Path]:
467467
return [configured] if configured.exists() and configured.is_dir() else [configured]
468468
fallback = (PROJECT_ROOT / "data" / "reading_reports").resolve()
469469
legacy_exports = (PROJECT_ROOT / "data" / "exports").resolve()
470-
ordered = [configured, fallback, legacy_exports]
470+
ordered = [configured]
471+
notes_git_dir = _configured_reading_notes_git_dir()
472+
if notes_git_dir is not None:
473+
ordered.append(notes_git_dir.resolve())
474+
ordered.extend(path.resolve() for path in notes_git_dir.glob("Deep Reading - *") if path.is_dir())
475+
ordered.extend([fallback, legacy_exports])
471476
results: List[Path] = []
472477
seen: Set[str] = set()
473478
for candidate in ordered:
@@ -678,8 +683,42 @@ def _is_reading_report_record(report: Dict[str, Any]) -> bool:
678683
return source_type in {"arxiv", "pdf", "local_pdf", "feishu_file_key", "reading_report"}
679684

680685

686+
def _normalized_report_dedupe_key(report: Dict[str, Any]) -> str:
687+
arxiv_id = str(report.get("arxiv_id") or "").strip().lower()
688+
if arxiv_id:
689+
return f"arxiv:{arxiv_id}"
690+
doi = str((report.get("metadata") or {}).get("doi") or "").strip().lower()
691+
if doi:
692+
return f"doi:{doi}"
693+
title = re.sub(r"[^a-z0-9]+", " ", str(report.get("title") or "").lower()).strip()
694+
return f"title:{title}" if title else f"path:{report.get('report_path') or report.get('report_id')}"
695+
696+
697+
def _report_source_priority(path: Path) -> int:
698+
resolved = path.resolve()
699+
notes_git_dir = _configured_reading_notes_git_dir()
700+
if notes_git_dir is not None:
701+
try:
702+
resolved.relative_to(notes_git_dir.resolve())
703+
return 30
704+
except ValueError:
705+
pass
706+
notes_root = _configured_notes_root_dir()
707+
if notes_root is not None:
708+
try:
709+
resolved.relative_to(notes_root.resolve())
710+
return 20
711+
except ValueError:
712+
pass
713+
try:
714+
resolved.relative_to((PROJECT_ROOT / "data" / "exports").resolve())
715+
return 0
716+
except ValueError:
717+
return 10
718+
719+
681720
def _all_report_records() -> List[Dict[str, Any]]:
682-
reports: List[Dict[str, Any]] = []
721+
reports_by_key: Dict[str, Dict[str, Any]] = {}
683722
seen: Set[str] = set()
684723
for root in _reading_report_dirs():
685724
for path in root.rglob("*.md"):
@@ -692,8 +731,19 @@ def _all_report_records() -> List[Dict[str, Any]]:
692731
report = _report_record(path)
693732
if not _is_reading_report_record(report):
694733
continue
695-
reports.append(report)
696-
reports.sort(key=lambda item: item["_sort_ts"], reverse=True)
734+
report["_source_priority"] = _report_source_priority(path)
735+
dedupe_key = _normalized_report_dedupe_key(report)
736+
existing = reports_by_key.get(dedupe_key)
737+
if existing is None or (
738+
int(report.get("_source_priority") or 0),
739+
float(report.get("_sort_ts") or 0),
740+
) > (
741+
int(existing.get("_source_priority") or 0),
742+
float(existing.get("_sort_ts") or 0),
743+
):
744+
reports_by_key[dedupe_key] = report
745+
reports = list(reports_by_key.values())
746+
reports.sort(key=lambda item: float(item.get("_sort_ts") or 0), reverse=True)
697747
return reports
698748

699749

deployments/desktop/static/desktop.css

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1102,6 +1102,17 @@ body.chat-view #chat {
11021102
background: #f1f6ff;
11031103
}
11041104

1105+
.report-row.highlighted {
1106+
border-color: #f4c84a;
1107+
background: #fff8d8;
1108+
box-shadow: inset 3px 0 0 #d99a00;
1109+
}
1110+
1111+
.report-row.highlighted.active {
1112+
border-color: #d99a00;
1113+
background: #fff3bd;
1114+
}
1115+
11051116
.report-row h3 {
11061117
margin: 0 0 4px;
11071118
font-size: 12.5px;

deployments/desktop/static/desktop.js

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
const DEMO_MODE = new URLSearchParams(window.location.search).get("demo") === "1";
55
const LOCALE_KEY = "paperflow.desktop.locale";
6+
const REPORT_HIGHLIGHTS_KEY = "paperflow.report.cardHighlights";
67
const supportedLocales = new Set(["zh", "en"]);
78
const requestedLocale = new URLSearchParams(window.location.search).get("lang");
89
const savedLocale = window.localStorage.getItem(LOCALE_KEY);
@@ -24,6 +25,7 @@
2425
reports: [],
2526
currentReport: null,
2627
reportAnnotationRange: null,
28+
highlightedReports: new Set(),
2729
wikiNodes: [],
2830
wikiGraph: null,
2931
wikiMap: null,
@@ -2680,6 +2682,36 @@
26802682
return `paperflow.report.annotations.${reportId || ""}`;
26812683
}
26822684

2685+
function loadReportCardHighlights() {
2686+
try {
2687+
const parsed = JSON.parse(window.localStorage.getItem(REPORT_HIGHLIGHTS_KEY) || "[]");
2688+
state.highlightedReports = new Set(Array.isArray(parsed) ? parsed.filter(Boolean).map(String) : []);
2689+
} catch (error) {
2690+
state.highlightedReports = new Set();
2691+
}
2692+
}
2693+
2694+
function saveReportCardHighlights() {
2695+
try {
2696+
window.localStorage.setItem(REPORT_HIGHLIGHTS_KEY, JSON.stringify([...state.highlightedReports]));
2697+
} catch (error) {
2698+
console.warn("Unable to save report card highlights", error);
2699+
}
2700+
}
2701+
2702+
function toggleReportCardHighlight(reportId) {
2703+
if (!reportId) return;
2704+
if (state.highlightedReports.has(reportId)) {
2705+
state.highlightedReports.delete(reportId);
2706+
showFeedbackToast("success", "已取消高亮", reportId);
2707+
} else {
2708+
state.highlightedReports.add(reportId);
2709+
showFeedbackToast("success", "已高亮报告", "右键可取消高亮");
2710+
}
2711+
saveReportCardHighlights();
2712+
renderReportList(state.reports, state.currentReport?.report_id || reportId);
2713+
}
2714+
26832715
function loadReportAnnotation(reportId) {
26842716
if (!reportId) return "";
26852717
try {
@@ -2790,7 +2822,7 @@
27902822
}
27912823
target.className = "reports-list";
27922824
target.innerHTML = reports.map((report) => `
2793-
<button class="report-row ${report.report_id === activeId ? "active" : ""}" data-report-id="${escapeHtml(report.report_id)}" type="button">
2825+
<button class="report-row ${report.report_id === activeId ? "active" : ""} ${state.highlightedReports.has(report.report_id) ? "highlighted" : ""}" data-report-id="${escapeHtml(report.report_id)}" type="button">
27942826
<h3>${escapeHtml(report.title || ui().reports.defaultTitle)}</h3>
27952827
<p>${escapeHtml(report.saved_at || "")} · ${escapeHtml(report.status || ui().reports.completed)}</p>
27962828
<p>${escapeHtml(report.snippet || report.report_path || "")}</p>
@@ -5346,6 +5378,12 @@
53465378
const row = event.target.closest("[data-report-id]");
53475379
if (row) runAction(() => loadReportContent(row.dataset.reportId), "打开报告");
53485380
});
5381+
$("reportsList").addEventListener("contextmenu", (event) => {
5382+
const row = event.target.closest("[data-report-id]");
5383+
if (!row) return;
5384+
event.preventDefault();
5385+
toggleReportCardHighlight(row.dataset.reportId);
5386+
});
53495387
["openReportAbsBtn", "openReportPdfBtn", "openReportDocBtn"].forEach((id) => {
53505388
$(id).addEventListener("click", () => {
53515389
const href = $(id).dataset.href;
@@ -5618,6 +5656,7 @@
56185656
}
56195657

56205658
async function init() {
5659+
loadReportCardHighlights();
56215660
bindEvents();
56225661
applyLocale();
56235662
renderChatSources();

tests/test_desktop_gui.py

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import threading
44
import time
55
import importlib
6+
import os
67
from datetime import timedelta
78
from pathlib import Path
89

@@ -1473,16 +1474,21 @@ def test_desktop_reading_agent_receives_derived_output_dirs(tmp_path, monkeypatc
14731474
def test_desktop_report_dirs_include_legacy_exports_for_recent_generated_reports(tmp_path, monkeypatch: pytest.MonkeyPatch) -> None:
14741475
project_root = tmp_path / "project"
14751476
configured = tmp_path / "Daily Note"
1477+
notes_year = configured / "Daily Note 2026"
1478+
deep_reading = notes_year / "Deep Reading - Jun 2026"
14761479
legacy_exports = project_root / "data" / "exports"
1477-
configured.mkdir()
1480+
deep_reading.mkdir(parents=True)
14781481
legacy_exports.mkdir(parents=True)
14791482
monkeypatch.setattr(agents, "PROJECT_ROOT", project_root)
14801483
monkeypatch.setattr(agents, "_configured_path", lambda _env_name, _default_relative: str(configured))
1484+
monkeypatch.setattr(agents, "_configured_reading_notes_git_dir", lambda: notes_year)
14811485
monkeypatch.setattr(agents, "_env_text", lambda _name, default="": "")
14821486

14831487
dirs = agents._reading_report_dirs() # noqa: SLF001 - report discovery contract
14841488

14851489
assert configured.resolve() in dirs
1490+
assert notes_year.resolve() in dirs
1491+
assert deep_reading.resolve() in dirs
14861492
assert legacy_exports.resolve() in dirs
14871493

14881494

@@ -1744,6 +1750,8 @@ def test_desktop_reports_view_uses_compact_reading_typography() -> None:
17441750
assert "position: fixed;" in css
17451751
assert ".annotation-toolbar.visible {" in css
17461752
assert ".annotation-swatch.bg-red {" in css
1753+
assert ".report-row.highlighted {" in css
1754+
assert "box-shadow: inset 3px 0 0 #d99a00;" in css
17471755

17481756

17491757
def test_desktop_report_viewer_renders_markdown_and_annotations() -> None:
@@ -1770,6 +1778,68 @@ def test_desktop_report_viewer_renders_markdown_and_annotations() -> None:
17701778
assert 'wrapper.style.fontWeight = "700"' in script
17711779
assert 'wrapper.style.fontStyle = "italic"' in script
17721780
assert "range.extractContents()" in script
1781+
assert "paperflow.report.cardHighlights" in script
1782+
assert "function toggleReportCardHighlight" in script
1783+
assert '.addEventListener("contextmenu"' in script
1784+
assert "state.highlightedReports.has(report.report_id)" in script
1785+
1786+
1787+
def test_report_list_dedupes_to_current_notes_but_sorts_by_time(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
1788+
project_root = tmp_path / "project"
1789+
notes_root = tmp_path / "Daily Note"
1790+
notes_year = notes_root / "Daily Note 2026"
1791+
deep_dir = notes_year / "Deep Reading - Jun 2026"
1792+
legacy_dir = project_root / "data" / "exports" / "Daily Note 2026" / "Deep Reading - Jun 2026"
1793+
other_dir = project_root / "data" / "reading_reports"
1794+
for path in (deep_dir, legacy_dir, other_dir):
1795+
path.mkdir(parents=True)
1796+
1797+
current = deep_dir / "Duplicate Current.md"
1798+
legacy = legacy_dir / "Duplicate Legacy.md"
1799+
unique = legacy_dir / "Unique Legacy.md"
1800+
current.write_text(
1801+
"---\n"
1802+
'title: "Duplicate Paper"\n'
1803+
'arxiv_id: "2606.00001v1"\n'
1804+
'report_version: "v-current"\n'
1805+
"---\n"
1806+
"# Duplicate Paper\n\nCurrent notes copy.",
1807+
encoding="utf-8",
1808+
)
1809+
legacy.write_text(
1810+
"---\n"
1811+
'title: "Duplicate Paper"\n'
1812+
'arxiv_id: "2606.00001v1"\n'
1813+
'report_version: "v-legacy"\n'
1814+
"---\n"
1815+
"# Duplicate Paper\n\nLegacy copy.",
1816+
encoding="utf-8",
1817+
)
1818+
unique.write_text(
1819+
"---\n"
1820+
'title: "Unique Legacy"\n'
1821+
'arxiv_id: "2606.00002v1"\n'
1822+
'report_version: "v-legacy"\n'
1823+
"---\n"
1824+
"# Unique Legacy\n",
1825+
encoding="utf-8",
1826+
)
1827+
os.utime(current, (100, 100))
1828+
os.utime(legacy, (300, 300))
1829+
os.utime(unique, (200, 200))
1830+
1831+
monkeypatch.setattr(agents, "PROJECT_ROOT", project_root)
1832+
monkeypatch.setattr(agents, "_configured_path", lambda _env_name, _default_relative: str(notes_root))
1833+
monkeypatch.setattr(agents, "_configured_notes_root_dir", lambda: notes_root)
1834+
monkeypatch.setattr(agents, "_configured_reading_notes_git_dir", lambda: notes_year)
1835+
monkeypatch.setattr(agents, "_env_text", lambda _name, default="": "")
1836+
1837+
reports = agents._all_report_records() # noqa: SLF001 - report list ordering contract
1838+
1839+
assert [report["title"] for report in reports] == ["Unique Legacy", "Duplicate Paper"]
1840+
duplicate = next(report for report in reports if report["title"] == "Duplicate Paper")
1841+
assert duplicate["report_path"] == str(current)
1842+
assert "Current notes copy" in duplicate["markdown"]
17731843

17741844

17751845
def test_report_record_derives_abs_url_and_patches_missing_institution(tmp_path: Path) -> None:

0 commit comments

Comments
 (0)