From 1c7d885f0692c246ce333296de7b31cd7f1e740b Mon Sep 17 00:00:00 2001
From: YumemiDream <1803068130@qq.com>
Date: Sat, 6 Jun 2026 14:45:53 +0800
Subject: [PATCH 1/2] Remove misleading jargon occurrence sort
---
web_res/static/html/dashboard.html | 7 ++-----
1 file changed, 2 insertions(+), 5 deletions(-)
diff --git a/web_res/static/html/dashboard.html b/web_res/static/html/dashboard.html
index d61e32b4..6e047578 100644
--- a/web_res/static/html/dashboard.html
+++ b/web_res/static/html/dashboard.html
@@ -2530,7 +2530,6 @@
${label}
@@ -6635,7 +6632,7 @@
${escapeHtml(item.title)}
const meta = [
item.group_id ? `群组 ${item.group_id}` : null,
item.is_confirmed !== undefined ? (item.is_confirmed ? '已确认' : '待确认') : null,
- item.occurrences !== undefined ? `出现 ${formatCount(item.occurrences)}` : null,
+ item.occurrences !== undefined ? `命中 ${formatCount(item.occurrences)}` : null,
item.created_at ? `创建 ${formatTime(item.created_at)}` : null,
item.updated_at ? `更新 ${formatTime(item.updated_at)}` : null,
].filter(Boolean).join(' · ');
From d153989adf11c12e90dbf046d53f3fa78e3583d8 Mon Sep 17 00:00:00 2001
From: YumemiDream <1803068130@qq.com>
Date: Sat, 6 Jun 2026 14:51:55 +0800
Subject: [PATCH 2/2] refactor: rework review queue with internal sidebar
workspace
- Add a dedicated review workspace layout with sticky sidebar
containing 6 category buttons: persona / style / jargon /
persona-state / reviewed / batches
- Each button shows live count badges; counts update whenever
persona, style, jargon, persona-state, backup or batch data
is loaded
- Add setReviewTab(tab) / updateReviewNavCounts(counts) helpers
and bind button clicks + page-revisit restore (state.reviews.tab)
- Add new integration test test_dashboard_review_queue_uses_sidebar_categories
to lock in the new structure
---
tests/integration/test_webui_static_assets.py | 15 +
web_res/static/html/dashboard.html | 599 ++++++++++++++----
2 files changed, 495 insertions(+), 119 deletions(-)
diff --git a/tests/integration/test_webui_static_assets.py b/tests/integration/test_webui_static_assets.py
index e435fded..87658249 100644
--- a/tests/integration/test_webui_static_assets.py
+++ b/tests/integration/test_webui_static_assets.py
@@ -72,6 +72,21 @@ def test_dashboard_review_details_use_backend_structured_fields():
assert "renderContextExamples(item)" in text
+def test_dashboard_review_queue_uses_sidebar_categories():
+ text = (PLUGIN_ROOT / "web_res" / "static" / "html" / "dashboard.html").read_text(encoding="utf-8")
+
+ assert "review-sidebar" in text
+ assert "review-workspace" in text
+ assert 'data-review-tab="persona"' in text
+ assert 'data-review-tab="style"' in text
+ assert 'data-review-tab="jargon"' in text
+ assert 'data-review-tab="persona-state"' in text
+ assert 'data-review-tab="reviewed"' in text
+ assert 'data-review-tab="batches"' in text
+ assert "setReviewTab" in text
+ assert "updateReviewNavCounts" in text
+
+
def test_dashboard_review_deletes_use_inline_confirmation():
text = (PLUGIN_ROOT / "web_res" / "static" / "html" / "dashboard.html").read_text(encoding="utf-8")
diff --git a/web_res/static/html/dashboard.html b/web_res/static/html/dashboard.html
index 6e047578..fe671a20 100644
--- a/web_res/static/html/dashboard.html
+++ b/web_res/static/html/dashboard.html
@@ -769,6 +769,164 @@
gap: 16px;
}
+ .review-workspace {
+ display: grid;
+ grid-template-columns: 280px minmax(0, 1fr);
+ gap: 16px;
+ align-items: start;
+ }
+
+ .review-sidebar {
+ position: sticky;
+ top: 16px;
+ padding: 14px;
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+ background: var(--panel);
+ box-shadow: var(--shadow-sm);
+ }
+
+ .review-sidebar-head {
+ padding: 2px 4px 12px;
+ border-bottom: 1px solid var(--border);
+ margin-bottom: 12px;
+ }
+
+ .review-sidebar-kicker {
+ color: var(--muted);
+ font-size: 11px;
+ font-weight: 700;
+ }
+
+ .review-sidebar-head h3 {
+ margin: 4px 0 2px;
+ font-size: var(--text-lg);
+ line-height: 1.25;
+ }
+
+ .review-sidebar-head p {
+ margin: 0;
+ color: var(--muted);
+ font-size: 12px;
+ }
+
+ .review-nav {
+ display: grid;
+ gap: 8px;
+ }
+
+ .review-nav-group {
+ margin: 14px 4px 8px;
+ color: var(--muted);
+ font-size: 11px;
+ font-weight: 700;
+ }
+
+ .review-nav-btn {
+ width: 100%;
+ min-height: 58px;
+ display: grid;
+ grid-template-columns: 32px minmax(0, 1fr) auto;
+ align-items: center;
+ gap: 10px;
+ padding: 10px;
+ border: 1px solid transparent;
+ border-radius: 8px;
+ background: transparent;
+ color: var(--text);
+ cursor: pointer;
+ text-align: left;
+ }
+
+ .review-nav-btn:hover {
+ border-color: var(--border);
+ background: var(--surface-hover);
+ }
+
+ .review-nav-btn.active {
+ border-color: var(--accent);
+ background: var(--selection);
+ box-shadow: inset 3px 0 0 var(--accent);
+ }
+
+ .review-nav-icon {
+ width: 32px;
+ height: 32px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ background: var(--panel-soft);
+ color: var(--accent);
+ }
+
+ .review-nav-icon .material-icons {
+ font-size: 18px;
+ }
+
+ .review-nav-text {
+ min-width: 0;
+ }
+
+ .review-nav-title {
+ display: block;
+ font-size: 13px;
+ font-weight: 700;
+ line-height: 1.2;
+ }
+
+ .review-nav-desc {
+ display: block;
+ margin-top: 3px;
+ color: var(--muted);
+ font-size: 12px;
+ line-height: 1.25;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .review-nav-badge {
+ min-width: 28px;
+ height: 24px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0 8px;
+ border: 1px solid var(--border);
+ border-radius: var(--radius-full);
+ background: var(--panel-soft);
+ color: var(--muted);
+ font-size: 12px;
+ font-weight: 700;
+ }
+
+ .review-nav-btn.active .review-nav-badge {
+ border-color: transparent;
+ background: var(--accent);
+ color: #fff;
+ }
+
+ .review-main {
+ min-width: 0;
+ }
+
+ .review-tab-panel {
+ display: none;
+ gap: 16px;
+ }
+
+ .review-tab-panel.active {
+ display: grid;
+ }
+
+ .review-split-grid {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) minmax(300px, 0.85fr);
+ gap: 16px;
+ }
+
.panel {
background: var(--panel);
border: 1px solid var(--border);
@@ -2007,6 +2165,15 @@
grid-template-columns: 1fr;
}
+ .review-workspace,
+ .review-split-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .review-sidebar {
+ position: static;
+ }
+
.settings-head {
flex-direction: column;
align-items: stretch;
@@ -2046,6 +2213,14 @@
grid-template-columns: 1fr;
}
+ .review-sidebar {
+ padding: 12px;
+ }
+
+ .review-nav-btn {
+ min-height: 54px;
+ }
+
.panel-head {
flex-direction: column;
}
@@ -2456,134 +2631,210 @@
审查队列
审查操作就绪。
-
-
-
+
+
+
+
+
-
-
-
-
黑话与批次
- --
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
黑话候选
+ --
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
-
-
-
-
最近批次
- --
-
-
-
-
-
-
+
-
-
-
+
@@ -2872,6 +3123,9 @@ 手动安装依赖
mirror: localStorage.getItem('sl-pip-mirror') || 'default',
},
actionBusy: new Set(),
+ reviews: {
+ tab: 'persona',
+ },
personaState: {
groupId: 'default',
},
@@ -3867,6 +4121,15 @@ 手动安装依赖
return map[target] || 'home';
}
+ function getReviewTabForInsightTarget(target) {
+ const map = {
+ reviews: 'persona',
+ jargon: 'jargon',
+ batches: 'batches',
+ };
+ return map[target] || null;
+ }
+
function jumpToInsightTarget(target) {
const targetId = getInsightTargetId(target);
if (!targetId) {
@@ -3874,6 +4137,10 @@ 手动安装依赖
}
navigateToPage(getInsightTargetPage(target), { keepScroll: true });
+ const reviewTab = getReviewTabForInsightTarget(target);
+ if (reviewTab) {
+ setReviewTab(reviewTab);
+ }
window.setTimeout(() => {
const element = $(targetId);
if (!element) {
@@ -4078,6 +4345,72 @@ 手动安装依赖
return `置信 ${formatPercent(score <= 1 ? score * 100 : score)}`;
}
+ function setReviewTab(tab) {
+ const validTabs = ['persona', 'style', 'jargon', 'persona-state', 'reviewed', 'batches'];
+ const nextTab = validTabs.includes(tab) ? tab : 'persona';
+ state.reviews.tab = nextTab;
+
+ document.querySelectorAll('[data-review-tab]').forEach((button) => {
+ const active = button.dataset.reviewTab === nextTab;
+ button.classList.toggle('active', active);
+ button.setAttribute('aria-selected', active ? 'true' : 'false');
+ });
+
+ document.querySelectorAll('[data-review-panel]').forEach((panel) => {
+ const active = panel.dataset.reviewPanel === nextTab;
+ panel.classList.toggle('active', active);
+ panel.hidden = !active;
+ });
+ }
+
+ function updateReviewNavCounts(counts = {}) {
+ const setText = (id, value) => {
+ const target = $(id);
+ if (target) {
+ target.textContent = value;
+ }
+ };
+
+ const dashboardData = state.data || {};
+ const persona = dashboardData.persona || {};
+ const personaUpdates = safeArray(persona.updates);
+ const style = dashboardData.style || {};
+ const personaReviewed = dashboardData.personaReviewed || {};
+ const personaBackups = dashboardData.personaBackups || {};
+ const jargonStats = extractJargonStats(dashboardData.jargonStats);
+ const styleBacklogDefault = safeNumber(style.total);
+ const personaTotal = safeNumber(persona.total);
+ const personaIncludesStyle = personaUpdates.some((item) => item && item.review_source === 'style_learning');
+ const personaBacklogDefault = personaIncludesStyle ? Math.max(0, personaTotal - styleBacklogDefault) : personaTotal;
+ const pendingJargonDefault = Math.max(
+ 0,
+ safeNumber(jargonStats.total_candidates ?? jargonStats.totalCandidates)
+ - safeNumber(jargonStats.confirmed_jargon ?? jargonStats.confirmedJargon),
+ );
+ const backupTotalDefault = personaBackups.available === false
+ ? 0
+ : safeNumber(personaBackups.total || safeArray(personaBackups.backups).length);
+ const valueOrDefault = (value, fallback) => (
+ value === undefined || value === null ? fallback : safeNumber(value)
+ );
+
+ const personaBacklog = valueOrDefault(counts.personaBacklog, personaBacklogDefault);
+ const styleBacklog = valueOrDefault(counts.styleBacklog, styleBacklogDefault);
+ const pendingJargon = valueOrDefault(counts.pendingJargon, pendingJargonDefault);
+ const reviewedTotal = valueOrDefault(counts.reviewedTotal, safeNumber(personaReviewed.total));
+ const backupTotal = valueOrDefault(counts.backupTotal, backupTotalDefault);
+ const batchTotal = valueOrDefault(counts.batchTotal, state.batch.total);
+ const pendingTotal = personaBacklog + styleBacklog + pendingJargon;
+
+ setText('reviewSidebarSummary', `待处理 ${formatCount(pendingTotal)} 条`);
+ setText('reviewNavPersonaCount', formatCount(personaBacklog));
+ setText('reviewNavStyleCount', formatCount(styleBacklog));
+ setText('reviewNavJargonCount', formatCount(pendingJargon));
+ setText('reviewNavPersonaStateCount', formatCount(backupTotal));
+ setText('reviewNavReviewedCount', formatCount(reviewedTotal));
+ setText('reviewNavBatchCount', formatCount(batchTotal));
+ }
+
function actionButton(action, id, icon, label, tone = '') {
if (id === undefined || id === null || id === '') {
return '';
@@ -4354,6 +4687,9 @@ 手动安装依赖
const personaTotal = safeNumber(persona.total);
const personaPayloadIncludesStyle = personaUpdates.some((item) => item && item.review_source === 'style_learning');
const personaBacklog = personaPayloadIncludesStyle ? Math.max(0, personaTotal - styleBacklog) : personaTotal;
+ const totalJargonCandidates = safeNumber(jargonStats.total_candidates ?? jargonStats.totalCandidates);
+ const confirmedJargon = safeNumber(jargonStats.confirmed_jargon ?? jargonStats.confirmedJargon);
+ const pendingJargon = Math.max(0, totalJargonCandidates - confirmedJargon);
const backlog = personaBacklog + styleBacklog;
const overallHealth = textOrDash(health.overall || 'unknown');
@@ -4468,6 +4804,17 @@ 手动安装依赖
$('styleList').innerHTML = renderStyleQueue(styleUpdatesFromPersona.length ? styleUpdatesFromPersona : style.reviews);
$('reviewedPersonaList').innerHTML = renderReviewedPersonaHistory(personaReviewed.updates);
renderPersonaStatePanel(personaState, personaBackups);
+ updateReviewNavCounts({
+ personaBacklog,
+ styleBacklog,
+ pendingJargon,
+ reviewedTotal: personaReviewed.total || 0,
+ backupTotal: personaBackups.available === false
+ ? 0
+ : (personaBackups.total || safeArray(personaBackups.backups).length),
+ batchTotal: state.batch.total,
+ });
+ setReviewTab(state.reviews.tab);
$('trendHint').textContent = `最近 ${Object.keys(trends.daily_messages || {}).length || 7} 天`;
@@ -6690,6 +7037,13 @@ ${escapeHtml(item.title)}
$('jargonNextBtn').disabled = page >= totalPages;
$('jargonHint').textContent = `候选 ${formatCount((jargonStats.total_candidates ?? jargonStats.totalCandidates) || 0)} · 第 ${page}/${totalPages} 页`;
+ updateReviewNavCounts({
+ pendingJargon: Math.max(
+ 0,
+ safeNumber(jargonStats.total_candidates ?? jargonStats.totalCandidates)
+ - safeNumber(jargonStats.confirmed_jargon ?? jargonStats.confirmedJargon),
+ ),
+ });
}
function renderBatchPanel() {
@@ -6727,6 +7081,7 @@ ${escapeHtml(item.title)}
$('batchPrevBtn').disabled = page <= 1;
$('batchNextBtn').disabled = page >= totalPages;
$('batchHint').textContent = `共 ${formatCount(total)} 个批次`;
+ updateReviewNavCounts({ batchTotal: total });
}
function bindEvents() {
@@ -6869,6 +7224,12 @@ ${escapeHtml(item.title)}
return;
}
+ const reviewTab = event.target.closest('[data-review-tab]');
+ if (reviewTab) {
+ setReviewTab(reviewTab.dataset.reviewTab);
+ return;
+ }
+
const graphTab = event.target.closest('[data-graph-type]');
if (graphTab) {
state.graph.type = graphTab.dataset.graphType === 'knowledge' ? 'knowledge' : 'memory';