From 7c81d19d3903459d24a561af3ac08781e6e11726 Mon Sep 17 00:00:00 2001 From: digua Date: Mon, 10 Aug 2026 11:38:36 +0800 Subject: [PATCH] fix(topics): synchronize progress and highlights --- .../src/services/topics/service.test.ts | 7 +-- .../src/services/topics/service.ts | 1 + .../common/ChatRecord/ChatTopicsPanel.vue | 33 ++++++-------- .../common/ChatRecord/topic-highlight.test.ts | 12 +++++- .../common/ChatRecord/topic-highlight.ts | 9 +++- .../common/ChatRecord/topic-progress.test.ts | 43 +++++++++++++++++++ .../common/ChatRecord/topic-progress.ts | 35 +++++++++++++++ 7 files changed, 114 insertions(+), 26 deletions(-) create mode 100644 src/components/common/ChatRecord/topic-progress.test.ts create mode 100644 src/components/common/ChatRecord/topic-progress.ts diff --git a/packages/node-runtime/src/services/topics/service.test.ts b/packages/node-runtime/src/services/topics/service.test.ts index b9fe5e39..ca8f7dac 100644 --- a/packages/node-runtime/src/services/topics/service.test.ts +++ b/packages/node-runtime/src/services/topics/service.test.ts @@ -586,14 +586,15 @@ test('a second runtime cannot recover or control a live topic execution', async now: () => Date.parse('2026-08-09T12:00:00.000Z'), generateId: () => 'second-runtime-run', }) + secondService = runtimeService - assert.equal(secondService.getRun('group', run.id)?.status, 'running') + assert.equal(runtimeService.getRun('group', run.id)?.status, 'running') assert.throws( - () => secondService.pause('group', run.id), + () => runtimeService.pause('group', run.id), (error: unknown) => (error as { statusCode?: number }).statusCode === 409 ) assert.throws( - () => secondService.cancel('group', run.id), + () => runtimeService.cancel('group', run.id), (error: unknown) => (error as { statusCode?: number }).statusCode === 409 ) assert.equal(firstService.getRun('group', run.id)?.status, 'running') diff --git a/packages/node-runtime/src/services/topics/service.ts b/packages/node-runtime/src/services/topics/service.ts index 8122652f..147aff54 100644 --- a/packages/node-runtime/src/services/topics/service.ts +++ b/packages/node-runtime/src/services/topics/service.ts @@ -471,6 +471,7 @@ export function createChatTopicService(deps: ChatTopicServiceDeps): ChatTopicSer } signal.throwIfAborted() + run = updateRun(run, { currentBlockIndex: null }) const finalPrompts = buildTopicFinalizationPrompt({ chatType: source.chatType, dayKey: source.dayKey, diff --git a/src/components/common/ChatRecord/ChatTopicsPanel.vue b/src/components/common/ChatRecord/ChatTopicsPanel.vue index 5a8af65b..6a8ab9ad 100644 --- a/src/components/common/ChatRecord/ChatTopicsPanel.vue +++ b/src/components/common/ChatRecord/ChatTopicsPanel.vue @@ -3,7 +3,13 @@ import { computed, onUnmounted, ref, watch } from 'vue' import dayjs from 'dayjs' import { useI18n } from 'vue-i18n' import type { ChatTopic } from '@openchatlab/shared-types' -import { CHAT_TOPIC_COLOR_STYLES, chatTopicColorStyle, type ChatTopicHighlight } from './topic-highlight' +import { + CHAT_TOPIC_COLOR_STYLES, + chatTopicColorStyle, + shouldClearChatTopicHighlight, + type ChatTopicHighlight, +} from './topic-highlight' +import { resolveChatTopicProgressDetail } from './topic-progress' import { useToast } from '@/composables/useToast' import { useChatTopicsService, @@ -51,25 +57,7 @@ const progressPercent = computed(() => { if (!run.value || run.value.totalBlocks === 0) return 0 return Math.min(100, Math.round((run.value.completedBlocks / run.value.totalBlocks) * 100)) }) -const activeProgressDetail = computed(() => { - const currentRun = run.value - if ( - !currentRun || - currentRun.status !== 'running' || - currentRun.currentBlockIndex === null || - currentRun.totalBlocks === 0 - ) { - return null - } - const seconds = Math.max(0, Math.floor((Date.now() - currentRun.updatedAt) / 1000)) - if (currentRun.currentBlockIndex >= currentRun.totalBlocks) { - return { key: 'records.topics.progressFinalizing', params: { seconds } } - } - return { - key: 'records.topics.progressBlock', - params: { current: currentRun.currentBlockIndex + 1, total: currentRun.totalBlocks, seconds }, - } -}) +const activeProgressDetail = computed(() => resolveChatTopicProgressDetail(run.value)) const todayDayKey = dayjs().format('YYYY-MM-DD') const rangeOptions = computed(() => [ @@ -133,7 +121,9 @@ async function refreshRun() { const statusChanged = next.status !== run.value.status || next.completedDays !== run.value.completedDays run.value = next if (statusChanged && props.dayKey) { - day.value = await useChatTopicsService().getDay(props.sessionId, props.dayKey, timezone) + const nextDay = await useChatTopicsService().getDay(props.sessionId, props.dayKey, timezone) + if (shouldClearChatTopicHighlight(day.value, nextDay)) clearTopicSelection() + day.value = nextDay } schedulePoll() } catch { @@ -211,6 +201,7 @@ async function regenerateDay() { actionLoading.value = true try { run.value = await useChatTopicsService().generateDay(props.sessionId, props.dayKey, timezone, locale.value) + clearTopicSelection() schedulePoll() } catch (error) { toast.fail(t('records.topics.startFailed'), { description: errorMessage(error) }) diff --git a/src/components/common/ChatRecord/topic-highlight.test.ts b/src/components/common/ChatRecord/topic-highlight.test.ts index 67edf935..6e54935b 100644 --- a/src/components/common/ChatRecord/topic-highlight.test.ts +++ b/src/components/common/ChatRecord/topic-highlight.test.ts @@ -1,6 +1,10 @@ import assert from 'node:assert/strict' import test from 'node:test' -import { isMessageInChatTopicHighlight, type ChatTopicHighlight } from './topic-highlight' +import { + isMessageInChatTopicHighlight, + shouldClearChatTopicHighlight, + type ChatTopicHighlight, +} from './topic-highlight' test('exact topic highlight excludes an interleaved message inside the same time range', () => { const highlight: ChatTopicHighlight = { @@ -25,3 +29,9 @@ test('legacy topic highlight keeps the minute-expanded range fallback', () => { assert.equal(isMessageInChatTopicHighlight(highlight, { id: 1, timestamp: 120 }), true) assert.equal(isMessageInChatTopicHighlight(highlight, { id: 3, timestamp: 180 }), false) }) + +test('clears a topic highlight only when the displayed snapshot is replaced', () => { + assert.equal(shouldClearChatTopicHighlight({ generatedAt: 100 }, { generatedAt: 100 }), false) + assert.equal(shouldClearChatTopicHighlight({ generatedAt: 100 }, { generatedAt: 200 }), true) + assert.equal(shouldClearChatTopicHighlight({ generatedAt: 100 }, null), true) +}) diff --git a/src/components/common/ChatRecord/topic-highlight.ts b/src/components/common/ChatRecord/topic-highlight.ts index d737cf8b..0e868a8e 100644 --- a/src/components/common/ChatRecord/topic-highlight.ts +++ b/src/components/common/ChatRecord/topic-highlight.ts @@ -1,4 +1,4 @@ -import type { ChatTopicAssignmentMode, ChatTopicTimeRange } from '@openchatlab/shared-types' +import type { ChatTopicAssignmentMode, ChatTopicDay, ChatTopicTimeRange } from '@openchatlab/shared-types' export interface ChatTopicHighlight { messageIds: number[] @@ -63,6 +63,13 @@ export function isMessageInChatTopicHighlight( }) } +export function shouldClearChatTopicHighlight( + currentDay: Pick | null, + nextDay: Pick | null +): boolean { + return currentDay !== null && currentDay.generatedAt !== nextDay?.generatedAt +} + function normalizeColorIndex(colorIndex: number): number { const length = CHAT_TOPIC_COLOR_STYLES.length return ((Math.trunc(colorIndex) % length) + length) % length diff --git a/src/components/common/ChatRecord/topic-progress.test.ts b/src/components/common/ChatRecord/topic-progress.test.ts new file mode 100644 index 00000000..f6c9943d --- /dev/null +++ b/src/components/common/ChatRecord/topic-progress.test.ts @@ -0,0 +1,43 @@ +import assert from 'node:assert/strict' +import test from 'node:test' +import { resolveChatTopicProgressDetail } from './topic-progress' + +test('reports run-wide block progress after advancing to a later day', () => { + assert.deepEqual( + resolveChatTopicProgressDetail( + { + status: 'running', + currentDay: '2026-08-10', + currentBlockIndex: 0, + completedBlocks: 2, + totalBlocks: 4, + updatedAt: 10_000, + }, + 13_000 + ), + { + key: 'records.topics.progressBlock', + params: { current: 3, total: 4, seconds: 3 }, + } + ) +}) + +test('reports daily finalization after all blocks for the current day finish', () => { + assert.deepEqual( + resolveChatTopicProgressDetail( + { + status: 'running', + currentDay: '2026-08-10', + currentBlockIndex: null, + completedBlocks: 2, + totalBlocks: 4, + updatedAt: 10_000, + }, + 13_000 + ), + { + key: 'records.topics.progressFinalizing', + params: { seconds: 3 }, + } + ) +}) diff --git a/src/components/common/ChatRecord/topic-progress.ts b/src/components/common/ChatRecord/topic-progress.ts new file mode 100644 index 00000000..e4ed0db7 --- /dev/null +++ b/src/components/common/ChatRecord/topic-progress.ts @@ -0,0 +1,35 @@ +import type { ChatTopicRun } from '@openchatlab/shared-types' + +export type ChatTopicProgressDetail = + | { + key: 'records.topics.progressBlock' + params: { current: number; total: number; seconds: number } + } + | { + key: 'records.topics.progressFinalizing' + params: { seconds: number } + } + +export function resolveChatTopicProgressDetail( + run: Pick< + ChatTopicRun, + 'status' | 'currentDay' | 'currentBlockIndex' | 'completedBlocks' | 'totalBlocks' | 'updatedAt' + > | null, + now = Date.now() +): ChatTopicProgressDetail | null { + if (!run || run.status !== 'running' || run.currentDay === null || run.totalBlocks === 0) return null + + const seconds = Math.max(0, Math.floor((now - run.updatedAt) / 1000)) + if (run.currentBlockIndex === null) { + return { key: 'records.topics.progressFinalizing', params: { seconds } } + } + + return { + key: 'records.topics.progressBlock', + params: { + current: Math.min(run.completedBlocks + 1, run.totalBlocks), + total: run.totalBlocks, + seconds, + }, + } +}