diff --git a/src/App.vue b/src/App.vue index c811f87c..94f60628 100644 --- a/src/App.vue +++ b/src/App.vue @@ -41,12 +41,11 @@ const LockScreen = IS_ELECTRON ? defineAsyncComponent(() => import('@/components const DataDirCleanupNotice = defineAsyncComponent(() => import('@/components/common/DataDirCleanupNotice.vue')) const loadScreenCaptureModal = () => import('@/components/common/ScreenCaptureModal.vue') const loadSettingsModal = () => import('@/components/common/SettingsModal.vue') -const loadChatRecordDrawer = () => import('@/components/common/ChatRecord/ChatRecordDrawer.vue') +const loadChatRecordWorkspace = () => import('@/components/common/ChatRecord/ChatRecordWorkspace.vue') const loadGlobalTaskBar = () => import('@/components/AIChat/GlobalTaskBar.vue') const loadDebugToolsPanel = () => import('@/components/layout/DebugToolsPanel.vue') const ScreenCaptureModal = defineAsyncComponent(loadScreenCaptureModal) const SettingsModal = defineAsyncComponent(loadSettingsModal) -const ChatRecordDrawer = defineAsyncComponent(loadChatRecordDrawer) const GlobalTaskBar = defineAsyncComponent(loadGlobalTaskBar) const DebugToolsPanel = defineAsyncComponent(loadDebugToolsPanel) @@ -120,7 +119,7 @@ function scheduleNonCriticalUiPrefetch() { cancelNonCriticalUiPrefetch = null void Promise.allSettled([ loadSettingsModal(), - loadChatRecordDrawer(), + loadChatRecordWorkspace(), loadScreenCaptureModal(), loadDebugToolsPanel(), ]) @@ -359,7 +358,7 @@ useWindowsTitleBarOverlay([ () => route.fullPath, () => layoutStore.showSettings, () => layoutStore.showScreenCaptureModal, - () => layoutStore.showChatRecordDrawer, + () => layoutStore.sidePanelOpen, ]) onMounted(async () => { @@ -484,8 +483,6 @@ onUnmounted(() => { /> - - diff --git a/src/components/AIChat/chat/CrossChatEvidenceBlock.vue b/src/components/AIChat/chat/CrossChatEvidenceBlock.vue index e905ec3c..c3289aea 100644 --- a/src/components/AIChat/chat/CrossChatEvidenceBlock.vue +++ b/src/components/AIChat/chat/CrossChatEvidenceBlock.vue @@ -31,7 +31,7 @@ const sessionGroups = computed(() => { }) function viewSource(source: CrossChatEvidenceSource): void { - layoutStore.openChatRecordDrawer({ + layoutStore.openChatRecords({ sessionId: source.sessionId, scrollToMessageId: source.messageId, }) diff --git a/src/components/AIChat/chat/EvidenceBlock.vue b/src/components/AIChat/chat/EvidenceBlock.vue index 737e6515..b4b0a021 100644 --- a/src/components/AIChat/chat/EvidenceBlock.vue +++ b/src/components/AIChat/chat/EvidenceBlock.vue @@ -43,7 +43,7 @@ const statusDotClass: Record = { } function viewSource(messageId: number): void { - layoutStore.openChatRecordDrawer({ scrollToMessageId: messageId }) + layoutStore.openChatRecords({ scrollToMessageId: messageId }) } diff --git a/src/components/analysis/SQLLab/ResultTable.vue b/src/components/analysis/SQLLab/ResultTable.vue index ae9ddc97..f2223bfd 100644 --- a/src/components/analysis/SQLLab/ResultTable.vue +++ b/src/components/analysis/SQLLab/ResultTable.vue @@ -78,7 +78,7 @@ function getMessageIdColumnIndex(columns: string[]): number { * 查看消息上下文 */ function viewMessageContext(messageId: number) { - layoutStore.openChatRecordDrawer({ + layoutStore.openChatRecords({ scrollToMessageId: messageId, }) } diff --git a/src/components/analysis/quotes/CatchphraseTab.vue b/src/components/analysis/quotes/CatchphraseTab.vue index 170c35a3..a2e5781b 100644 --- a/src/components/analysis/quotes/CatchphraseTab.vue +++ b/src/components/analysis/quotes/CatchphraseTab.vue @@ -53,7 +53,7 @@ async function loadCatchphraseAnalysis() { } function handlePhraseClick(content: string) { - layoutStore.openChatRecordDrawer(buildCatchphraseRecordQuery(content, props.timeFilter)) + layoutStore.openChatRecords(buildCatchphraseRecordQuery(content, props.timeFilter)) } function getOverviewPhrases(member: MemberCatchphrase) { diff --git a/src/components/analysis/quotes/HotRepeatTab.vue b/src/components/analysis/quotes/HotRepeatTab.vue index 135bca8b..d1ad2ca1 100644 --- a/src/components/analysis/quotes/HotRepeatTab.vue +++ b/src/components/analysis/quotes/HotRepeatTab.vue @@ -48,7 +48,7 @@ async function loadRepeatAnalysis() { * 查看复读内容的聊天记录上下文 */ function viewRepeatContext(item: { content: string; firstMessageId: number }) { - layoutStore.openChatRecordDrawer({ + layoutStore.openChatRecords({ scrollToMessageId: item.firstMessageId, highlightKeywords: [item.content], }) diff --git a/src/components/analysis/quotes/LanguagePreferenceTab.vue b/src/components/analysis/quotes/LanguagePreferenceTab.vue index 3fffdacb..4e31a0da 100644 --- a/src/components/analysis/quotes/LanguagePreferenceTab.vue +++ b/src/components/analysis/quotes/LanguagePreferenceTab.vue @@ -43,7 +43,7 @@ async function loadData() { function handleWordClick(word: string) { if (!props.enableRecordNavigation) return - layoutStore.openChatRecordDrawer({ keywords: [word] }) + layoutStore.openChatRecords({ keywords: [word] }) } watch( diff --git a/src/components/analysis/quotes/WordcloudTab.vue b/src/components/analysis/quotes/WordcloudTab.vue index 27b792c7..f459cbc0 100644 --- a/src/components/analysis/quotes/WordcloudTab.vue +++ b/src/components/analysis/quotes/WordcloudTab.vue @@ -430,7 +430,7 @@ watch(locale, () => { // 点击词语,打开聊天记录查看器 function handleWordClick(word: string) { if (!props.enableRecordNavigation) return - layoutStore.openChatRecordDrawer({ + layoutStore.openChatRecords({ keywords: [word], }) } diff --git a/src/components/common/ChatRecord/ChatRecordDrawer.vue b/src/components/common/ChatRecord/ChatRecordDrawer.vue deleted file mode 100644 index 78a7ff44..00000000 --- a/src/components/common/ChatRecord/ChatRecordDrawer.vue +++ /dev/null @@ -1,146 +0,0 @@ - - - diff --git a/src/components/common/ChatRecord/FilterPanel.vue b/src/components/common/ChatRecord/FilterPanel.vue index abe04a1a..d223b483 100644 --- a/src/components/common/ChatRecord/FilterPanel.vue +++ b/src/components/common/ChatRecord/FilterPanel.vue @@ -124,8 +124,8 @@ function resetFilter() { diff --git a/src/pages/private-chat/index.vue b/src/pages/private-chat/index.vue index f87babc4..3f051e14 100644 --- a/src/pages/private-chat/index.vue +++ b/src/pages/private-chat/index.vue @@ -9,6 +9,7 @@ import { ChatExplorer } from '@/components/AIChat' import PrivateChatInsights from './components/insights/PrivateChatInsights.vue' import MemberList from '@/components/common/member/MemberList.vue' import SessionAnalysisHeader from '@/components/layout/session/SessionAnalysisHeader.vue' +import SidePanelLayout from '@/components/layout/SidePanelLayout.vue' import SemanticIndexSessionModal from '@/components/analysis/SemanticIndexSessionModal.vue' import OwnerPromptModal from '@/components/analysis/member/OwnerPromptModal.vue' import IncrementalImportModal from '@/components/analysis/IncrementalImportModal.vue' @@ -46,7 +47,7 @@ const showMemberManagementModal = ref(false) // 打开聊天记录查看器 function openChatRecordViewer() { - layoutStore.openChatRecordDrawer({}) + layoutStore.openChatRecords({}) } // Tab 配置 - 私聊包含洞察、AI 对话、记忆和更多工具 @@ -156,56 +157,57 @@ const otherMemberAvatar = computed(() => { :initial-time-state="initialTimeState" @open-incremental-import="showIncrementalImportModal = true" @open-member-management="showMemberManagementModal = true" - @open-chat-record="openChatRecordViewer" @update:full-range="fullTimeRange = $event" @time-range-initialized="handleTimeRangeInitialized" /> - -
- - + + +
+ + -
- - - - - - +
+ + + + + + +
-
+
{ :session-id="currentSessionId" :show-header="false" chat-type="private" + @view-records="showMemberManagementModal = false" @data-changed="invalidateAnalysisData" />
diff --git a/src/stores/layout-persistence.test.ts b/src/stores/layout-persistence.test.ts index c9b2abad..5b1e03a6 100644 --- a/src/stores/layout-persistence.test.ts +++ b/src/stores/layout-persistence.test.ts @@ -41,3 +41,48 @@ test('layout persistence upgrades older state and restores a saved drawer width' assert.equal(restoredStore.chatRecordDrawerWidth, 936) }) + +test('collapsing the side panel preserves the record target and reopening restores it', async () => { + const pinia = createPersistedPinia(createMemoryStorage()) + const { useLayoutStore } = await import('./layout') + const store = useLayoutStore(pinia) + store.setSidePanelContext('session-a') + store.openChatRecords({ scrollToMessageId: 42, keywords: ['hello'] }) + store.closeSidePanel() + assert.equal(store.sidePanelOpen, false) + assert.deepEqual(store.chatRecordQuery, { sessionId: 'session-a', scrollToMessageId: 42, keywords: ['hello'] }) + store.toggleSidePanel() + assert.equal(store.sidePanelOpen, true) + assert.equal(store.sidePanelView, 'records') +}) + +test('contact source records share one panel and can return to the selected contact', async () => { + const pinia = createPersistedPinia(createMemoryStorage()) + const { useLayoutStore } = await import('./layout') + const store = useLayoutStore(pinia) + store.setSidePanelContext(null) + store.openContactPanel() + store.openChatRecords({ sessionId: 'contact-source', scrollToMessageId: 7 }) + assert.equal(store.sidePanelView, 'records') + assert.equal(store.sidePanelHasContact, true) + store.returnToContactPanel() + assert.equal(store.sidePanelView, 'contact') + assert.equal(store.sidePanelOpen, true) +}) + +test('changing page context clears stale records and opens only the new session', async () => { + const pinia = createPersistedPinia(createMemoryStorage()) + const { useLayoutStore } = await import('./layout') + const store = useLayoutStore(pinia) + store.setSidePanelContext('session-a') + store.openChatRecords({ scrollToMessageId: 42 }) + store.setSidePanelContext('session-b') + assert.equal(store.sidePanelOpen, false) + assert.equal(store.chatRecordQuery, null) + store.toggleSidePanel() + assert.deepEqual(store.chatRecordQuery, { sessionId: 'session-b' }) + store.setSidePanelContext(null) + assert.equal(store.canOpenSidePanel, false) + store.openChatRecords({ scrollToMessageId: 42 }) + assert.equal(store.sidePanelOpen, false) +}) diff --git a/src/stores/layout.ts b/src/stores/layout.ts index 7a4b931e..278af876 100644 --- a/src/stores/layout.ts +++ b/src/stores/layout.ts @@ -14,9 +14,15 @@ export const useLayoutStore = defineStore( const isAIChatSidebarCollapsed = ref(false) const showScreenCaptureModal = ref(false) const screenCaptureImage = ref(null) - const showChatRecordDrawer = ref(false) + const sidePanelOpen = ref(false) + const sidePanelView = ref<'records' | 'contact' | null>(null) + const sidePanelSessionId = ref(null) + const sidePanelHasContact = ref(false) const chatRecordQuery = ref(null) + // Keep the existing persisted preference when replacing the drawer shell. const chatRecordDrawerWidth = ref(750) + const contactPanelWidth = ref(420) + const canOpenSidePanel = computed(() => Boolean(sidePanelView.value || sidePanelSessionId.value)) const isToolsPanelLocked = ref(false) const isToolsPanelMini = ref(false) @@ -60,22 +66,53 @@ export const useLayoutStore = defineStore( }, 300) } - /** - * 打开聊天记录抽屉并设置查询参数 - */ - function openChatRecordDrawer(query: ChatRecordQuery) { - chatRecordQuery.value = query - showChatRecordDrawer.value = true + function setSidePanelContext(sessionId: string | null) { + sidePanelOpen.value = false + sidePanelView.value = null + sidePanelHasContact.value = false + chatRecordQuery.value = null + sidePanelSessionId.value = sessionId } - /** - * 关闭聊天记录抽屉并重置查询 - */ - function closeChatRecordDrawer() { - showChatRecordDrawer.value = false - setTimeout(() => { - chatRecordQuery.value = null - }, 300) + function openChatRecords(query: ChatRecordQuery) { + const sessionId = query.sessionId || sidePanelSessionId.value + if (!sessionId) return + chatRecordQuery.value = { ...query, sessionId } + sidePanelView.value = 'records' + sidePanelOpen.value = true + } + + function openContactPanel() { + sidePanelHasContact.value = true + sidePanelView.value = 'contact' + sidePanelOpen.value = true + chatRecordQuery.value = null + } + + function returnToContactPanel() { + if (!sidePanelHasContact.value) return + sidePanelView.value = 'contact' + sidePanelOpen.value = true + } + + function clearContactPanel() { + sidePanelHasContact.value = false + if (sidePanelView.value !== 'contact') return + sidePanelView.value = null + sidePanelOpen.value = false + } + + function closeSidePanel() { + sidePanelOpen.value = false + } + + function toggleSidePanel() { + if (!canOpenSidePanel.value) return + if (!sidePanelView.value) { + openChatRecords({}) + return + } + sidePanelOpen.value = !sidePanelOpen.value } function toggleToolsPanelLock() { @@ -116,9 +153,13 @@ export const useLayoutStore = defineStore( isToolsPanelOpen, showScreenCaptureModal, screenCaptureImage, - showChatRecordDrawer, + sidePanelOpen, + sidePanelView, + sidePanelHasContact, + canOpenSidePanel, chatRecordQuery, chatRecordDrawerWidth, + contactPanelWidth, showSettings, settingsTab, settingsSubTab, @@ -129,8 +170,13 @@ export const useLayoutStore = defineStore( toggleToolsPanelMini, openScreenCaptureModal, closeScreenCaptureModal, - openChatRecordDrawer, - closeChatRecordDrawer, + setSidePanelContext, + openChatRecords, + openContactPanel, + returnToContactPanel, + clearContactPanel, + closeSidePanel, + toggleSidePanel, openSettings, closeSettings, } @@ -145,6 +191,7 @@ export const useLayoutStore = defineStore( 'isToolsPanelMini', 'toolsPanelPosition', 'chatRecordDrawerWidth', + 'contactPanelWidth', ], storage: localStorage, },