@@ -53,6 +55,14 @@ const emit = defineEmits<{
const notes = reactive>({})
const visibleLoops = computed(() => props.payload.items.slice(0, 4))
const overflowCount = computed(() => Math.max(props.payload.items.length - visibleLoops.value.length, 0))
+const priorityNote = computed(() => {
+ const nextCount = props.payload.summary.by_state.next || 0
+ const activeCount = props.payload.summary.by_state.active || 0
+ if (nextCount) return `${nextCount} 个问题已经标成 next,适合进入下一轮行动。`
+ if (activeCount) return `${activeCount} 个问题仍然 active,先判断它们是否还值得继续。`
+ if (props.payload.summary.total) return '剩余问题大多已经处理,可以展开确认是否需要恢复。'
+ return '当前没有悬而未决的问题。'
+})
function setState(loopId: string, state: TrustOpenLoopState) {
emit('updateLoop', loopId, {
diff --git a/frontend/src/components/TrustReviewDetail.vue b/frontend/src/components/TrustReviewDetail.vue
index 96c8d83..5a3c16f 100644
--- a/frontend/src/components/TrustReviewDetail.vue
+++ b/frontend/src/components/TrustReviewDetail.vue
@@ -22,6 +22,10 @@
+ 证据路径 @@ -65,24 +69,22 @@
-
-
-
-
-
- 从左侧队列选择一条材料查看证据。
diff --git a/frontend/src/components/TrustReviewInbox.vue b/frontend/src/components/TrustReviewInbox.vue index 0ce8c2b..a1079ba 100644 --- a/frontend/src/components/TrustReviewInbox.vue +++ b/frontend/src/components/TrustReviewInbox.vue @@ -53,6 +53,7 @@
{{ actionHint(item) }}
+
{{ boundaryLabel(item.why_saved_status) }}
{{ item.space_name || '未分配空间' }}
@@ -67,6 +68,7 @@
diff --git a/frontend/src/components/TrustReviewSignals.vue b/frontend/src/components/TrustReviewSignals.vue
new file mode 100644
index 0000000..7dc6be8
--- /dev/null
+++ b/frontend/src/components/TrustReviewSignals.vue
@@ -0,0 +1,26 @@
+
+
+
+
+
+
diff --git a/frontend/src/components/TrustSessionPlanner.vue b/frontend/src/components/TrustSessionPlanner.vue
new file mode 100644
index 0000000..8b58101
--- /dev/null
+++ b/frontend/src/components/TrustSessionPlanner.vue
@@ -0,0 +1,91 @@
+
+
+
+
+
+
diff --git a/frontend/src/components/trustCenterCopy.ts b/frontend/src/components/trustCenterCopy.ts
new file mode 100644
index 0000000..4ed7b19
--- /dev/null
+++ b/frontend/src/components/trustCenterCopy.ts
@@ -0,0 +1,62 @@
+import type {
+ TrustReviewSessionMode,
+ TrustReviewStatus,
+ TrustRiskLevel,
+} from './trustCenterTypes'
+
+export function riskToneLabel(level: TrustRiskLevel) {
+ if (level === 'critical') return '必须先看'
+ if (level === 'high') return '高优先级'
+ if (level === 'medium') return '需要复核'
+ return '低压力'
+}
+
+export function boundaryLabel(status: string) {
+ if (status === 'user-stated') return '用户原话'
+ if (status === 'AI-inferred') return 'AI 推断'
+ return status || '边界未知'
+}
+
+export function reviewStatusLabel(status: TrustReviewStatus | string) {
+ if (status === 'unreviewed') return '未审查'
+ if (status === 'confirmed') return '已确认'
+ if (status === 'rewritten') return '已改写'
+ if (status === 'rejected') return '已拒绝'
+ if (status === 'deferred') return '稍后处理'
+ return status || '未审查'
+}
+
+export function decisionActionLabel(action: TrustReviewStatus | string) {
+ if (action === 'confirmed') return '确认'
+ if (action === 'rewritten') return '改写'
+ if (action === 'rejected') return '拒绝'
+ if (action === 'deferred') return '稍后'
+ return reviewStatusLabel(action)
+}
+
+export function sessionModeLabel(mode: TrustReviewSessionMode) {
+ if (mode === 'high-risk') return '高风险优先'
+ if (mode === 'quick-clear') return '快速清理'
+ if (mode === 'open-loops') return 'Open loop'
+ return '全部'
+}
+
+export function sessionModeDescription(mode: TrustReviewSessionMode) {
+ if (mode === 'high-risk') return '先处理最可能影响回答可信度的 AI 推断。'
+ if (mode === 'quick-clear') return '优先扫掉低压力项目,减少队列噪音。'
+ if (mode === 'open-loops') return '把悬而未决的问题整理成下一步。'
+ return '保留完整队列,适合做系统性检查。'
+}
+
+export function confidenceLabel(confidence: number) {
+ const value = Math.round((Number.isFinite(confidence) ? confidence : 0) * 100)
+ if (value >= 80) return `${value}% 稳定`
+ if (value >= 55) return `${value}% 需确认`
+ return `${value}% 不稳`
+}
+
+export function shortTrustText(text: string, limit = 96) {
+ const clean = (text || '').trim()
+ if (!clean) return ''
+ return clean.length > limit ? `${clean.slice(0, limit)}...` : clean
+}
diff --git a/frontend/src/components/trustCenterTypes.ts b/frontend/src/components/trustCenterTypes.ts
index fab8e62..7d5626a 100644
--- a/frontend/src/components/trustCenterTypes.ts
+++ b/frontend/src/components/trustCenterTypes.ts
@@ -3,6 +3,7 @@ import type { GraphSpace } from '../types'
export type TrustRiskLevel = 'critical' | 'high' | 'medium' | 'low'
export type TrustReviewStatus = 'unreviewed' | 'confirmed' | 'rewritten' | 'rejected' | 'deferred'
export type TrustOpenLoopState = 'active' | 'next' | 'resolved' | 'dismissed'
+export type TrustReviewSessionMode = 'high-risk' | 'quick-clear' | 'open-loops' | 'all'
export type TrustReviewFilters = {
status?: string
@@ -20,6 +21,37 @@ export type TrustTopicRef = {
role: string
}
+export type TrustDecisionOption = {
+ action: Exclude
+ label: string
+ tone: 'positive' | 'constructive' | 'danger' | 'neutral'
+ description: string
+ requires_note: boolean
+ requires_rewrite: boolean
+}
+
+export type TrustReviewFocus = {
+ headline: string
+ detail: string
+ primary_action: string
+ evidence_prompt: string
+}
+
+export type TrustSignals = {
+ risk_level: TrustRiskLevel
+ confidence: number
+ confidence_percent: number
+ boundary: string
+ review_status: TrustReviewStatus
+ evidence_count: number
+ topic_count: number
+ open_loop_count: number
+ future_question_count: number
+ has_open_loops: boolean
+ has_review_note: boolean
+ needs_user_decision: boolean
+}
+
export type TrustReviewItem = {
source_id: string
title: string
@@ -40,6 +72,10 @@ export type TrustReviewItem = {
reviewed_at: string
risk_level: TrustRiskLevel
recommended_action: string
+ risk_reasons: string[]
+ decision_options: TrustDecisionOption[]
+ review_focus: TrustReviewFocus
+ trust_signals: TrustSignals
evidence_count: number
topic_refs: TrustTopicRef[]
has_open_loops: boolean
diff --git a/frontend/src/styles.css b/frontend/src/styles.css
index 032c0fa..d938718 100644
--- a/frontend/src/styles.css
+++ b/frontend/src/styles.css
@@ -7109,6 +7109,103 @@ button:not(:disabled):hover {
min-width: 0;
}
+.trust-session-planner {
+ display: grid;
+ grid-template-columns: minmax(220px, 0.8fr) minmax(360px, 1.4fr) minmax(220px, 0.8fr);
+ gap: 10px;
+ align-items: stretch;
+ border: 1px solid rgba(44, 53, 68, 0.12);
+ border-radius: 8px;
+ background: rgba(255, 255, 255, 0.74);
+ padding: 10px;
+}
+
+.trust-session-copy,
+.trust-session-next-step {
+ display: grid;
+ align-content: center;
+ gap: 5px;
+ min-width: 0;
+}
+
+.trust-session-copy h2,
+.trust-session-next-step strong {
+ margin: 0;
+ font-family: var(--snap-title);
+ font-size: 1.03rem;
+ font-weight: 560;
+ letter-spacing: 0;
+}
+
+.trust-session-copy p,
+.trust-session-next-step p,
+.trust-review-progress p {
+ margin: 0;
+ color: var(--muted-foreground);
+ line-height: 1.45;
+}
+
+.trust-session-modes {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 7px;
+ min-width: 0;
+}
+
+.trust-session-modes button {
+ border: 1px solid rgba(44, 53, 68, 0.12);
+ border-radius: 8px;
+ background: color-mix(in srgb, var(--paper) 88%, white);
+ color: var(--foreground);
+ padding: 9px;
+ display: grid;
+ gap: 3px;
+ text-align: left;
+ cursor: pointer;
+ min-width: 0;
+}
+
+.trust-session-modes button.active {
+ border-color: rgba(53, 99, 91, 0.44);
+ background: color-mix(in srgb, var(--secondary) 78%, white);
+ box-shadow: inset 0 0 0 1px rgba(53, 99, 91, 0.14);
+}
+
+.trust-session-modes strong {
+ font-size: 0.88rem;
+ line-height: 1.2;
+}
+
+.trust-session-modes span,
+.trust-review-progress span,
+.trust-session-next-step span {
+ color: var(--muted-foreground);
+ font-size: 0.74rem;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+}
+
+.trust-review-progress {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 10px;
+}
+
+.trust-review-progress > div {
+ border: 1px solid rgba(44, 53, 68, 0.1);
+ border-radius: 8px;
+ background: color-mix(in srgb, var(--paper) 86%, white);
+ padding: 10px;
+ display: grid;
+ gap: 4px;
+ min-width: 0;
+}
+
+.trust-review-progress strong {
+ font-size: 1.1rem;
+ line-height: 1.2;
+}
+
.trust-summary-grid article {
min-height: 96px;
border: 1px solid var(--line);
@@ -7186,6 +7283,15 @@ button:not(:disabled):hover {
margin-top: 2px;
}
+.trust-batch-guidance,
+.trust-loop-priority-note,
+.trust-diagnostics-guidance {
+ margin: 4px 0 0;
+ color: var(--muted-foreground);
+ font-size: 0.82rem;
+ line-height: 1.35;
+}
+
.trust-batch-bar input,
.trust-filter-row input,
.trust-filter-row select,
@@ -7369,6 +7475,50 @@ button:not(:disabled):hover {
gap: 6px;
}
+.trust-review-signals {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ align-items: center;
+}
+
+.trust-review-signals span {
+ border: 1px solid rgba(44, 53, 68, 0.12);
+ border-radius: 999px;
+ background: rgba(255, 255, 255, 0.82);
+ color: var(--muted-foreground);
+ font-size: 0.74rem;
+ line-height: 1.2;
+ padding: 4px 8px;
+}
+
+.trust-review-signals[data-compact="false"] {
+ border: 1px solid rgba(44, 53, 68, 0.1);
+ border-radius: 8px;
+ background: color-mix(in srgb, var(--secondary) 48%, white);
+ padding: 8px;
+}
+
+.trust-review-signals .risk-critical {
+ border-color: rgba(160, 48, 39, 0.36);
+ color: #8c2b24;
+}
+
+.trust-review-signals .risk-high {
+ border-color: rgba(176, 80, 30, 0.34);
+ color: #974613;
+}
+
+.trust-review-signals .risk-medium {
+ border-color: rgba(106, 127, 61, 0.34);
+ color: #5d7135;
+}
+
+.trust-review-signals .risk-low {
+ border-color: rgba(58, 105, 97, 0.34);
+ color: #315f58;
+}
+
.trust-detail-panel {
position: sticky;
top: 14px;
@@ -7401,6 +7551,145 @@ button:not(:disabled):hover {
line-height: 1.5;
}
+.trust-risk-lens,
+.trust-decision-coach {
+ border: 1px solid rgba(44, 53, 68, 0.12);
+ border-radius: 8px;
+ background: color-mix(in srgb, var(--paper) 88%, white);
+ padding: 10px;
+ display: grid;
+ gap: 9px;
+}
+
+.trust-risk-lens[data-risk="critical"] {
+ border-color: rgba(160, 48, 39, 0.28);
+ background: color-mix(in srgb, #fff7f2 70%, white);
+}
+
+.trust-risk-lens[data-risk="high"] {
+ border-color: rgba(176, 80, 30, 0.26);
+ background: color-mix(in srgb, #fff9ec 70%, white);
+}
+
+.trust-risk-lens-head,
+.trust-decision-coach-head {
+ display: flex;
+ justify-content: space-between;
+ gap: 10px;
+ align-items: flex-start;
+}
+
+.trust-risk-lens h4,
+.trust-decision-coach h4 {
+ margin: 2px 0 0;
+ font-size: 0.98rem;
+}
+
+.trust-risk-lens p {
+ margin: 0;
+ color: var(--muted-foreground);
+ line-height: 1.45;
+}
+
+.trust-risk-reasons {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+}
+
+.trust-risk-reasons span {
+ border: 1px solid rgba(44, 53, 68, 0.12);
+ border-radius: 999px;
+ background: rgba(255, 255, 255, 0.78);
+ color: var(--foreground);
+ font-size: 0.74rem;
+ padding: 4px 8px;
+}
+
+.trust-risk-evidence-prompt {
+ border-left: 3px solid rgba(53, 99, 91, 0.36);
+ display: grid;
+ gap: 3px;
+ padding-left: 9px;
+}
+
+.trust-risk-evidence-prompt span,
+.trust-decision-field span {
+ color: var(--muted-foreground);
+ font-size: 0.74rem;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+}
+
+.trust-decision-options {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 8px;
+}
+
+.trust-decision-options button {
+ border: 1px solid rgba(44, 53, 68, 0.12);
+ border-radius: 8px;
+ background: rgba(255, 255, 255, 0.78);
+ color: var(--foreground);
+ display: grid;
+ gap: 4px;
+ min-width: 0;
+ padding: 9px;
+ text-align: left;
+ cursor: pointer;
+}
+
+.trust-decision-options button.active {
+ border-color: rgba(53, 99, 91, 0.44);
+ background: color-mix(in srgb, var(--secondary) 76%, white);
+}
+
+.trust-decision-options button span {
+ color: var(--muted-foreground);
+ font-size: 0.78rem;
+ line-height: 1.35;
+}
+
+.trust-decision-field {
+ display: grid;
+ gap: 5px;
+}
+
+.trust-decision-field textarea {
+ width: 100%;
+ min-height: 72px;
+ box-sizing: border-box;
+ resize: vertical;
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ background: white;
+ padding: 8px;
+ font: inherit;
+}
+
+.trust-decision-field[data-required="true"] textarea {
+ border-color: rgba(176, 80, 30, 0.42);
+}
+
+.trust-decision-validation {
+ margin: 0;
+ border: 1px solid rgba(176, 80, 30, 0.24);
+ border-radius: 8px;
+ background: #fff9ec;
+ color: #8a4518;
+ font-size: 0.82rem;
+ line-height: 1.35;
+ padding: 8px;
+}
+
+.trust-decision-submit-row {
+ display: flex;
+ gap: 8px;
+ justify-content: flex-end;
+ flex-wrap: wrap;
+}
+
.trust-detail-disclosure {
border-top: 1px solid var(--line);
margin-top: 10px;
@@ -7479,6 +7768,12 @@ button:not(:disabled):hover {
margin-bottom: 10px;
}
+.trust-diagnostics-guidance {
+ border-left: 3px solid rgba(53, 99, 91, 0.34);
+ padding-left: 8px;
+ margin-bottom: 10px;
+}
+
.trust-open-loop-card {
display: grid;
grid-template-columns: minmax(0, 1fr);
@@ -7497,6 +7792,14 @@ button:not(:disabled):hover {
background: rgba(250, 250, 246, 0.68);
}
+.trust-loop-priority-note {
+ border: 1px dashed rgba(44, 53, 68, 0.18);
+ border-radius: 8px;
+ background: rgba(250, 250, 246, 0.72);
+ padding: 8px;
+ margin-bottom: 10px;
+}
+
.trust-empty {
color: var(--muted-foreground);
margin: 0;
@@ -7526,6 +7829,24 @@ button:not(:disabled):hover {
}
}
+@media (max-width: 760px) {
+ .trust-session-planner,
+ .trust-review-progress {
+ grid-template-columns: 1fr;
+ }
+
+ .trust-session-modes,
+ .trust-decision-options {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+
+ .trust-review-signals[data-compact="false"],
+ .trust-risk-lens,
+ .trust-decision-coach {
+ padding: 9px;
+ }
+}
+
@media (max-width: 560px) {
.trust-center-header {
flex-direction: column;
diff --git a/snapgraph/trust_center.py b/snapgraph/trust_center.py
index d2a4ffb..652d724 100644
--- a/snapgraph/trust_center.py
+++ b/snapgraph/trust_center.py
@@ -365,6 +365,8 @@ def _source_rows(workspace: Workspace) -> list[_SourceProjection]:
def _review_item(source: _SourceProjection, evidence_count: int, topic_refs: list[dict]) -> dict:
risk = _risk_level(source, evidence_count=evidence_count)
+ risk_reasons = _risk_reasons(source, risk, evidence_count)
+ trust_signals = _trust_signals(source, risk, evidence_count, topic_refs)
return {
"source_id": source.source_id,
"title": source.title,
@@ -385,6 +387,10 @@ def _review_item(source: _SourceProjection, evidence_count: int, topic_refs: lis
"reviewed_at": source.reviewed_at,
"risk_level": risk,
"recommended_action": _recommended_action(source, risk),
+ "risk_reasons": risk_reasons,
+ "decision_options": _decision_options(source, risk),
+ "review_focus": _review_focus(source, risk, evidence_count, risk_reasons),
+ "trust_signals": trust_signals,
"evidence_count": evidence_count,
"topic_refs": topic_refs,
"has_open_loops": bool(source.open_loops),
@@ -416,6 +422,146 @@ def _recommended_action(source: _SourceProjection, risk: str) -> str:
return "monitor"
+def _risk_reasons(source: _SourceProjection, risk: str, evidence_count: int) -> list[str]:
+ reasons: list[str] = []
+ if source.why_saved_status == "AI-inferred":
+ reasons.append("saved reason is AI-inferred")
+ elif source.why_saved_status == "user-stated":
+ reasons.append("saved reason was user-stated")
+ else:
+ reasons.append("saved reason boundary is unknown")
+ if source.review_status == "unreviewed":
+ reasons.append("review status is still unreviewed")
+ elif source.review_status == "deferred":
+ reasons.append("previous review was deferred")
+ elif source.review_status == "rejected":
+ reasons.append("previous review rejected this inference")
+ if source.confidence < 0.55:
+ reasons.append("confidence is below 55 percent")
+ elif source.confidence < 0.75:
+ reasons.append("confidence is moderate")
+ if source.open_loops:
+ reasons.append(f"{len(source.open_loops)} open loop remains connected")
+ if evidence_count == 0:
+ reasons.append("no graph evidence path is attached")
+ elif evidence_count == 1:
+ reasons.append("one graph evidence path is attached")
+ else:
+ reasons.append(f"{evidence_count} graph evidence paths are attached")
+ if risk in {"critical", "high"} and not source.review_note:
+ reasons.append("no user review note has been recorded")
+ return reasons
+
+
+def _decision_options(source: _SourceProjection, risk: str) -> list[dict]:
+ confirm_description = "Accept the current saved reason as representing the user's intent."
+ if source.why_saved_status == "AI-inferred":
+ confirm_description = "Confirm that this AI-inferred reason matches the user's real intent."
+ rewrite_description = "Replace the current reason with a user-approved explanation."
+ reject_description = "Mark this inference as unsafe to use as the user's intent."
+ defer_description = "Keep it in the queue because more evidence or user context is needed."
+ if risk == "critical":
+ defer_description = "Defer only if the user cannot decide now; this item should stay visible."
+ return [
+ {
+ "action": "confirmed",
+ "label": "Confirm",
+ "tone": "positive",
+ "description": confirm_description,
+ "requires_note": False,
+ "requires_rewrite": False,
+ },
+ {
+ "action": "rewritten",
+ "label": "Rewrite",
+ "tone": "constructive",
+ "description": rewrite_description,
+ "requires_note": False,
+ "requires_rewrite": True,
+ },
+ {
+ "action": "rejected",
+ "label": "Reject",
+ "tone": "danger",
+ "description": reject_description,
+ "requires_note": risk in {"critical", "high"},
+ "requires_rewrite": False,
+ },
+ {
+ "action": "deferred",
+ "label": "Defer",
+ "tone": "neutral",
+ "description": defer_description,
+ "requires_note": risk == "critical",
+ "requires_rewrite": False,
+ },
+ ]
+
+
+def _review_focus(
+ source: _SourceProjection,
+ risk: str,
+ evidence_count: int,
+ risk_reasons: list[str],
+) -> dict:
+ if source.review_status == "deferred":
+ return {
+ "headline": "Resume the deferred decision",
+ "detail": "This item was already postponed, so the next useful step is to decide whether new context is enough.",
+ "primary_action": "resume_review",
+ "evidence_prompt": "Compare the previous note with the current evidence before deciding.",
+ }
+ if source.why_saved_status == "AI-inferred" and source.review_status == "unreviewed":
+ return {
+ "headline": "Validate the AI-inferred reason",
+ "detail": risk_reasons[0] if risk_reasons else "The item needs a user-trust decision.",
+ "primary_action": "review_ai_inference",
+ "evidence_prompt": "Check whether the evidence path supports the stated intent.",
+ }
+ if source.open_loops:
+ return {
+ "headline": "Turn the open loop into a next step",
+ "detail": "The saved material still carries unresolved follow-up work.",
+ "primary_action": "inspect_open_loops",
+ "evidence_prompt": "Review the loop text and decide whether it is still active.",
+ }
+ if evidence_count == 0:
+ return {
+ "headline": "Check traceability before trusting",
+ "detail": "This item does not yet expose a graph evidence path.",
+ "primary_action": "inspect_evidence",
+ "evidence_prompt": "Confirm whether the source itself is enough or more links are needed.",
+ }
+ return {
+ "headline": "Monitor the trusted context",
+ "detail": "No urgent trust action is required, but the context remains traceable.",
+ "primary_action": "monitor",
+ "evidence_prompt": "Keep the evidence path available for future recall.",
+ }
+
+
+def _trust_signals(
+ source: _SourceProjection,
+ risk: str,
+ evidence_count: int,
+ topic_refs: list[dict],
+) -> dict:
+ return {
+ "risk_level": risk,
+ "confidence": source.confidence,
+ "confidence_percent": round(source.confidence * 100),
+ "boundary": source.why_saved_status,
+ "review_status": source.review_status,
+ "evidence_count": evidence_count,
+ "topic_count": len(topic_refs),
+ "open_loop_count": len(source.open_loops),
+ "future_question_count": len(source.future_recall_questions),
+ "has_open_loops": bool(source.open_loops),
+ "has_review_note": bool(source.review_note.strip()),
+ "needs_user_decision": source.why_saved_status == "AI-inferred" and source.review_status in {"unreviewed", "deferred"},
+ }
+
+
def _matches_filters(item: dict, filters: dict) -> bool:
if filters.get("source_id") and item["source_id"] != filters["source_id"]:
return False
diff --git a/tests/test_trust_center.py b/tests/test_trust_center.py
index 4625538..51d06a6 100644
--- a/tests/test_trust_center.py
+++ b/tests/test_trust_center.py
@@ -24,6 +24,33 @@ def test_trust_review_queue_prioritizes_unreviewed_ai_context(
assert "recommended_action" in payload["items"][0]
+def test_trust_review_queue_exposes_session_decision_fields(
+ tmp_path: Path,
+ monkeypatch,
+) -> None:
+ monkeypatch.chdir(tmp_path)
+ client = TestClient(app)
+ client.post("/api/demo/load")
+
+ payload = client.get("/api/trust/review").json()
+ item = payload["items"][0]
+
+ assert item["risk_reasons"]
+ assert item["review_focus"]["headline"]
+ assert item["review_focus"]["primary_action"]
+ assert item["trust_signals"]["evidence_count"] == item["evidence_count"]
+ assert item["trust_signals"]["has_open_loops"] == item["has_open_loops"]
+ assert item["trust_signals"]["topic_count"] == len(item["topic_refs"])
+ assert item["decision_options"]
+ assert {option["action"] for option in item["decision_options"]} == {
+ "confirmed",
+ "rewritten",
+ "rejected",
+ "deferred",
+ }
+ assert any(option["requires_rewrite"] for option in item["decision_options"])
+
+
def test_trust_review_detail_exposes_traceability(tmp_path: Path, monkeypatch) -> None:
monkeypatch.chdir(tmp_path)
client = TestClient(app)
diff --git a/tests/trust_operations_ui.test.ts b/tests/trust_operations_ui.test.ts
index 30ba720..173001c 100644
--- a/tests/trust_operations_ui.test.ts
+++ b/tests/trust_operations_ui.test.ts
@@ -97,3 +97,85 @@ test('Trust Center reduces information pressure with compact focus and progressi
assert.match(styles, /\.trust-review-compact-meta/)
assert.match(styles, /\.trust-diagnostics-summary/)
})
+
+test('Trust Center defines session types and shared decision copy helpers', () => {
+ const types = read('frontend/src/components/trustCenterTypes.ts')
+ const copy = read('frontend/src/components/trustCenterCopy.ts')
+
+ assert.match(types, /TrustReviewSessionMode/)
+ assert.match(types, /TrustDecisionOption/)
+ assert.match(types, /TrustReviewFocus/)
+ assert.match(types, /TrustSignals/)
+ assert.match(copy, /riskToneLabel/)
+ assert.match(copy, /boundaryLabel/)
+ assert.match(copy, /reviewStatusLabel/)
+ assert.match(copy, /decisionActionLabel/)
+ assert.match(copy, /sessionModeLabel/)
+ assert.match(copy, /sessionModeDescription/)
+ assert.match(copy, /confidenceLabel/)
+})
+
+test('Trust Center adds a guided review session planner and progress strip', () => {
+ const view = read('frontend/src/components/TrustCenterView.vue')
+ const planner = read('frontend/src/components/TrustSessionPlanner.vue')
+ const progress = read('frontend/src/components/TrustReviewProgress.vue')
+ const styles = read('frontend/src/styles.css')
+
+ assert.match(view, /TrustSessionPlanner/)
+ assert.match(view, /TrustReviewProgress/)
+ assert.match(view, /TrustReviewSessionMode/)
+ assert.match(view, /selectedSessionMode/)
+ assert.match(view, /sessionItems/)
+ assert.match(planner, /trust-session-planner/)
+ assert.match(planner, /sessionModeLabel/)
+ assert.match(planner, /sessionModeDescription/)
+ assert.match(progress, /trust-review-progress/)
+ assert.match(progress, /selectedCount/)
+ assert.match(progress, /nextStep/)
+ assert.match(styles, /\.trust-session-planner/)
+ assert.match(styles, /\.trust-review-progress/)
+ assert.match(styles, /\.trust-session-modes/)
+})
+
+test('Trust Center adds reusable signals, risk lens, and decision coach', () => {
+ const inbox = read('frontend/src/components/TrustReviewInbox.vue')
+ const detail = read('frontend/src/components/TrustReviewDetail.vue')
+ const signals = read('frontend/src/components/TrustReviewSignals.vue')
+ const lens = read('frontend/src/components/TrustRiskLens.vue')
+ const coach = read('frontend/src/components/TrustDecisionCoach.vue')
+ const styles = read('frontend/src/styles.css')
+
+ assert.match(inbox, /TrustReviewSignals/)
+ assert.match(detail, /TrustReviewSignals/)
+ assert.match(detail, /TrustRiskLens/)
+ assert.match(detail, /TrustDecisionCoach/)
+ assert.match(signals, /trust-review-signals/)
+ assert.match(signals, /confidenceLabel/)
+ assert.match(lens, /trust-risk-lens/)
+ assert.match(lens, /riskReasons/)
+ assert.match(coach, /trust-decision-coach/)
+ assert.match(coach, /submitRewrite/)
+ assert.match(coach, /validationMessage/)
+ assert.match(styles, /\.trust-review-signals/)
+ assert.match(styles, /\.trust-risk-lens/)
+ assert.match(styles, /\.trust-decision-coach/)
+})
+
+test('Trust Center secondary panels include guidance and responsive session layout', () => {
+ const batch = read('frontend/src/components/TrustBatchActionBar.vue')
+ const loops = read('frontend/src/components/TrustOpenLoopPanel.vue')
+ const diagnostics = read('frontend/src/components/TrustDiagnosticsPanel.vue')
+ const styles = read('frontend/src/styles.css')
+
+ assert.match(batch, /trust-batch-guidance/)
+ assert.match(batch, /batchGuidance/)
+ assert.match(loops, /trust-loop-priority-note/)
+ assert.match(loops, /priorityNote/)
+ assert.match(diagnostics, /trust-diagnostics-guidance/)
+ assert.match(diagnostics, /diagnosticsGuidance/)
+ assert.match(styles, /@media \(max-width: 760px\)[\s\S]*\.trust-session-planner/)
+ assert.match(styles, /@media \(max-width: 760px\)[\s\S]*\.trust-decision-options/)
+ assert.match(styles, /\.trust-batch-guidance/)
+ assert.match(styles, /\.trust-loop-priority-note/)
+ assert.match(styles, /\.trust-diagnostics-guidance/)
+})
+ {{ riskToneLabel(item.risk_level) }}
+ {{ boundaryLabel(item.why_saved_status) }}
+ {{ confidenceLabel(item.confidence) }}
+ {{ item.evidence_count }} 条证据
+ {{ item.trust_signals.open_loop_count }} 个 open loop
+ {{ item.trust_signals.topic_count }} 个主题
+ 有审查备注
+
+
+
+
diff --git a/frontend/src/components/TrustRiskLens.vue b/frontend/src/components/TrustRiskLens.vue
new file mode 100644
index 0000000..de57ed0
--- /dev/null
+++ b/frontend/src/components/TrustRiskLens.vue
@@ -0,0 +1,49 @@
+
+
+
+
+
+ Risk Lens
+
+ {{ riskToneLabel(item.risk_level) }}
+ {{ item.review_focus.headline }}
+{{ item.review_focus.detail }}
+ +
+ {{ translateReason(reason) }}
+
+
+
+ 看证据时先问
+ {{ item.review_focus.evidence_prompt }}
+
+
+ Review Session
+
+
+ {{ sessionModeLabel(mode) }}
+{{ sessionModeDescription(mode) }}
+
+
+
+
+
+ 建议下一步
+ {{ nextStepHeadline }}
+
+ {{ nextStepDetail }}
+