From d865342ae2bfdee0c436eae015bb62ab99f1d780 Mon Sep 17 00:00:00 2001 From: weichen Date: Mon, 21 Sep 2026 05:19:43 +0000 Subject: [PATCH] feat(web): replace the standing trace panel with a chat card The requirements and impact summary no longer occupies the chat column. Open recheck marks arrive as an assistant card with a no-change action. Co-authored-by: Cursor --- web/src/App.tsx | 2 - web/src/api/projectTrace.ts | 9 ++- web/src/components/chat/InteractiveCard.tsx | 10 +++ .../components/workflow/ProjectTracePanel.tsx | 78 ------------------- .../workflow/tracePanelRetired.test.ts | 10 +++ web/src/hooks/useCardActions.ts | 27 +++++++ web/src/types/workbench.ts | 3 +- 7 files changed, 56 insertions(+), 83 deletions(-) delete mode 100644 web/src/components/workflow/ProjectTracePanel.tsx create mode 100644 web/src/components/workflow/tracePanelRetired.test.ts diff --git a/web/src/App.tsx b/web/src/App.tsx index abddb5ae..fa5d6f48 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -34,7 +34,6 @@ import { import { DocumentLineage } from './types/projectWorkflow'; import { StaleDocumentNotice } from './components/workflow/StaleDocumentNotice'; import { StageAdvanceBanner } from './components/workflow/StageAdvanceBanner'; -import { ProjectTracePanel } from './components/workflow/ProjectTracePanel'; import { OnDemand } from './components/lazy/OnDemand'; import { fetchProjectSettings } from './api/projectSettings'; import { LoginView } from './components/auth/LoginView'; @@ -435,7 +434,6 @@ export default function App() { isCreator={currentWfState.creatorId === currentUser.id} /> )} - {currentProjectId > 0 && } { diff --git a/web/src/api/projectTrace.ts b/web/src/api/projectTrace.ts index af0263d6..da4a1273 100644 --- a/web/src/api/projectTrace.ts +++ b/web/src/api/projectTrace.ts @@ -49,11 +49,16 @@ export async function fetchProjectTrace(projectId: number): Promise { +export async function clearRecheckMark( + projectId: number, + markId: number, + reason: 'no_change' | 'revision_started', + messageId?: number, +): Promise { const res = await fetch(`${API_BASE}/api/v1/projects/${projectId}/trace/recheck/${markId}/clear`, { method: 'POST', headers: getBearerAuthHeaders('application/json'), - body: JSON.stringify({ reason }), + body: JSON.stringify({ reason, message_id: messageId || 0 }), }); if (!res.ok) { throw new Error(`关闭待复核失败: ${res.status}`); diff --git a/web/src/components/chat/InteractiveCard.tsx b/web/src/components/chat/InteractiveCard.tsx index 97f8bf43..443648ee 100644 --- a/web/src/components/chat/InteractiveCard.tsx +++ b/web/src/components/chat/InteractiveCard.tsx @@ -82,6 +82,16 @@ export function getCardTypeMeta(type: InteractiveCardType) { badgeColor: 'var(--info)', badgeBorder: 'rgba(43, 108, 201, 0.2)', }; + case 'RECHECK_NOTICE': + return { + label: '待复核', + badgeText: '待复核', + icon: AlertTriangle, + accentColor: 'var(--amber-500)', + badgeBg: 'var(--amber-50)', + badgeColor: 'var(--amber-600)', + badgeBorder: 'rgba(242, 169, 59, 0.3)', + }; default: return { label: '协同卡片', diff --git a/web/src/components/workflow/ProjectTracePanel.tsx b/web/src/components/workflow/ProjectTracePanel.tsx deleted file mode 100644 index 8d4a3bf4..00000000 --- a/web/src/components/workflow/ProjectTracePanel.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React, { useCallback, useEffect, useState } from 'react'; -import { clearRecheckMark, fetchProjectTrace, ProjectTraceView } from '../../api/projectTrace'; - -interface ProjectTracePanelProps { - projectId: number; -} - -export const ProjectTracePanel: React.FC = ({ projectId }) => { - const [view, setView] = useState(null); - const [error, setError] = useState(null); - - const load = useCallback(async () => { - try { - setView(await fetchProjectTrace(projectId)); - setError(null); - } catch (err: unknown) { - setError(err instanceof Error ? err.message : '加载失败'); - } - }, [projectId]); - - useEffect(() => { - load(); - }, [load]); - - if (error) { - return
需求与影响暂时不可用:{error}
; - } - if (!view) return null; - - return ( -
-
需求与影响
-
- 已索引 {view.index.length} 份文档 · 条目 {view.items.length} - {view.hidden_count > 0 ? ` · 另有 ${view.hidden_count} 条不可见` : ''} -
- {view.recheck_marks.length > 0 && ( -
    - {view.recheck_marks.map((mark) => ( -
  • - 阶段 {mark.stage_key} 待复核({mark.reason || mark.source_doc_id}) - -
  • - ))} -
- )} - {view.items.length > 0 && ( -
    - {view.items.slice(0, 8).map((item) => ( -
  • - {item.business_key} - {item.statement ? ` · ${item.statement}` : ''} -
  • - ))} -
- )} -
- ); -}; - -const boxStyle: React.CSSProperties = { - margin: '8px 12px 0', - padding: '10px 12px', - borderRadius: 8, - border: '1px solid var(--border, #e2e8f0)', - background: 'var(--bg-subtle, #f8fafc)', - fontSize: 12, -}; diff --git a/web/src/components/workflow/tracePanelRetired.test.ts b/web/src/components/workflow/tracePanelRetired.test.ts new file mode 100644 index 00000000..3a26c2d8 --- /dev/null +++ b/web/src/components/workflow/tracePanelRetired.test.ts @@ -0,0 +1,10 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import test from 'node:test'; + +test('trace panel is not mounted above the chat stream', () => { + const app = readFileSync(new URL('../../App.tsx', import.meta.url), 'utf8'); + assert.equal(app.includes('ProjectTracePanel'), false); + const card = readFileSync(new URL('../chat/InteractiveCard.tsx', import.meta.url), 'utf8'); + assert.equal(card.includes("case 'RECHECK_NOTICE'"), true); +}); diff --git a/web/src/hooks/useCardActions.ts b/web/src/hooks/useCardActions.ts index 2fb78fe5..ae61d44b 100644 --- a/web/src/hooks/useCardActions.ts +++ b/web/src/hooks/useCardActions.ts @@ -1,4 +1,5 @@ import type { Dispatch, SetStateAction } from 'react'; +import { clearRecheckMark } from '../api/projectTrace'; import { fetchIntentPlan, submitIntentPlanDecision, @@ -69,6 +70,32 @@ export function useCardActions(input: CardActionsInput) { } } + if (!targetCard) return; + + if (targetCard.cardType === 'RECHECK_NOTICE' && actionId === 'recheck_no_change') { + const projectId = Number(payload?.project_id || currentProjectId); + const markId = Number(payload?.mark_id); + const serverId = messages.find((m) => m.interactiveCard?.cardId === cardId)?.serverId; + void clearRecheckMark(projectId, markId, 'no_change', serverId) + .then(() => { + setMessages((prev) => resolveCardInMessages(prev, cardId, '已标记为无需变更', currentUser.name)); + }) + .catch(() => { + setMessages((prev) => [ + ...prev, + { + id: 'recheck-fail-' + Date.now(), + sessionId: activeSessionId, + role: 'assistant', + authorName: '项目助手', + content: '没能关闭这条待复核,请稍后再试。', + timestamp: new Date().toTimeString().substring(0, 5), + }, + ]); + }); + return; + } + if (isPlanConfirmCard(targetCard, payload)) { const planId = String(payload?.plan_id || targetCard?.planId || ''); const expectedRevision = Number(payload?.expected_revision || targetCard?.planRevision || 0); diff --git a/web/src/types/workbench.ts b/web/src/types/workbench.ts index b83b0b0a..ba171e69 100644 --- a/web/src/types/workbench.ts +++ b/web/src/types/workbench.ts @@ -73,7 +73,8 @@ export type InteractiveCardType = | 'PLAN_CONFIRM' | 'SLOT_FILLING' | 'DIFF_PROPOSAL' - | 'REMINDER_NOTICE'; + | 'REMINDER_NOTICE' + | 'RECHECK_NOTICE'; export type CardActionStatus = | 'active'