Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/renderer/shell/components/AppBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@
<Icon v-else-if="themeStore.themeMode === 'light'" icon="lucide:sun" class="w-4 h-4" />
<Icon v-else icon="lucide:monitor" class="w-4 h-4" />
</Button>
<Button
variant="ghost"
class="text-xs font-medium px-2 h-7 bg-transparent rounded-md flex items-center justify-center hover:bg-zinc-500/20"
@click="toggleThreadView"
>
<Icon icon="lucide:history" class="w-4 h-4" />
</Button>
<Button
variant="ghost"
class="text-xs font-medium px-2 h-7 bg-transparent rounded-md flex items-center justify-center hover:bg-zinc-500/20"
Expand Down Expand Up @@ -129,6 +136,7 @@ import { useThemeStore } from '@/stores/theme'
import { useElementSize } from '@vueuse/core'
import { useLanguageStore } from '@/stores/language'
import { useI18n } from 'vue-i18n'
import { THREAD_VIEW_EVENTS } from '@/events'
const tabStore = useTabStore()
const langStore = useLanguageStore()
const windowPresenter = usePresenter('windowPresenter')
Expand Down Expand Up @@ -162,6 +170,22 @@ const onTabContainerWrapperScroll = () => {
})
}

const toggleThreadView = async () => {
try {
const windowId = window.api.getWindowId()
if (windowId == null) {
console.warn('Failed to toggle thread view: unable to determine window id.')
return
}
const success = await windowPresenter.sendToActiveTab(windowId, THREAD_VIEW_EVENTS.TOGGLE)
if (!success) {
console.warn('Failed to toggle thread view: no active tab found.')
}
} catch (error) {
console.warn('Failed to toggle thread view via windowPresenter.', error)
}
}

const isTabContainerOverflowingLeft = computed(() => {
return (
tabContainerWrapperSize.width.value < tabContainerSize.width.value &&
Expand Down
19 changes: 18 additions & 1 deletion src/renderer/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ import { usePresenter } from './composables/usePresenter'
import SelectedTextContextMenu from './components/message/SelectedTextContextMenu.vue'
import { useArtifactStore } from './stores/artifact'
import { useChatStore } from '@/stores/chat'
import { NOTIFICATION_EVENTS, SHORTCUT_EVENTS } from './events'
import { NOTIFICATION_EVENTS, SHORTCUT_EVENTS, THREAD_VIEW_EVENTS } from './events'
import { Toaster } from '@shadcn/components/ui/sonner'
import { useToast } from '@/components/use-toast'
import { useSettingsStore } from '@/stores/settings'
import { useThemeStore } from '@/stores/theme'
import { useLanguageStore } from '@/stores/language'
import { useI18n } from 'vue-i18n'
import TranslatePopup from '@/components/popup/TranslatePopup.vue'
import ThreadView from '@/components/ThreadView.vue'
import ModelCheckDialog from '@/components/settings/ModelCheckDialog.vue'
import { useModelCheckStore } from '@/stores/modelCheck'
import MessageDialog from './components/ui/MessageDialog.vue'
Expand Down Expand Up @@ -165,6 +166,15 @@ const handleCreateNewConversation = () => {
}
}

const handleThreadViewToggle = () => {
if (router.currentRoute.value.name !== 'chat') {
void router.push({ name: 'chat' })
chatStore.isSidebarOpen = true
return
}
chatStore.isSidebarOpen = !chatStore.isSidebarOpen
}

// Removed GO_SETTINGS handler; now handled in main via tab logic

// Handle ESC key - close floating chat window
Expand Down Expand Up @@ -224,6 +234,8 @@ onMounted(() => {
})
})

window.electron.ipcRenderer.on(THREAD_VIEW_EVENTS.TOGGLE, handleThreadViewToggle)

window.electron.ipcRenderer.on(NOTIFICATION_EVENTS.SYS_NOTIFY_CLICKED, (_, msg) => {
let threadId: string | null = null

Expand Down Expand Up @@ -265,6 +277,9 @@ onMounted(() => {
}
// Close artifacts page when route changes
artifactStore.hideArtifact()
if (route.name !== 'chat') {
chatStore.isSidebarOpen = false
}
}
)

Expand Down Expand Up @@ -302,6 +317,7 @@ onBeforeUnmount(() => {
// GO_SETTINGS listener removed; handled in main
window.electron.ipcRenderer.removeAllListeners(NOTIFICATION_EVENTS.SYS_NOTIFY_CLICKED)
window.electron.ipcRenderer.removeAllListeners(NOTIFICATION_EVENTS.DATA_RESET_COMPLETE_DEV)
window.electron.ipcRenderer.removeListener(THREAD_VIEW_EVENTS.TOGGLE, handleThreadViewToggle)
})
</script>

Expand All @@ -320,6 +336,7 @@ onBeforeUnmount(() => {
<Toaster />
<SelectedTextContextMenu />
<TranslatePopup />
<ThreadView />
<!-- Global model check dialog -->
<ModelCheckDialog
:open="modelCheckStore.isDialogOpen"
Expand Down
60 changes: 60 additions & 0 deletions src/renderer/src/components/ThreadView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<template>
<Teleport to="body">
<Transition
enter-active-class="transition-opacity duration-200 ease-out"
leave-active-class="transition-opacity duration-150 ease-in"
enter-from-class="opacity-0"
leave-to-class="opacity-0"
>
<div v-if="chatStore.isSidebarOpen" class="fixed inset-0 z-50" :dir="langStore.dir">
<div class="absolute inset-0 bg-transparent" @click="closeSidebar"></div>
<div
class="relative h-full flex"
:class="langStore.dir === 'rtl' ? 'justify-end' : 'justify-start'"
>
<Transition
enter-active-class="transition-transform duration-200 ease-out"
leave-active-class="transition-transform duration-150 ease-in"
:enter-from-class="langStore.dir === 'rtl' ? 'translate-x-full' : '-translate-x-full'"
:leave-to-class="langStore.dir === 'rtl' ? 'translate-x-full' : '-translate-x-full'"
>
<div
v-if="chatStore.isSidebarOpen"
class="h-full w-60 max-w-60 shadow-lg border-r border-border bg-bg-card"
>
<ThreadsView class="h-full" />
</div>
</Transition>
</div>
</div>
</Transition>
</Teleport>
</template>

<script setup lang="ts">
import { onBeforeUnmount, onMounted } from 'vue'
import ThreadsView from './ThreadsView.vue'
import { useChatStore } from '@/stores/chat'
import { useLanguageStore } from '@/stores/language'

const chatStore = useChatStore()
const langStore = useLanguageStore()

const closeSidebar = () => {
chatStore.isSidebarOpen = false
}

const handleKeydown = (event: KeyboardEvent) => {
if (event.key === 'Escape' && chatStore.isSidebarOpen) {
closeSidebar()
}
}

onMounted(() => {
window.addEventListener('keydown', handleKeydown)
})

onBeforeUnmount(() => {
window.removeEventListener('keydown', handleKeydown)
})
</script>
1 change: 0 additions & 1 deletion src/renderer/src/components/ThreadsView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
<!-- 固定在顶部的"新会话"按钮 -->
<div class="flex-none flex flex-row gap-2">
<Button
v-if="windowSize.width.value < 1024"
variant="outline"
size="icon"
class="shrink-0 text-xs justify-center h-7 w-7"
Expand Down
38 changes: 10 additions & 28 deletions src/renderer/src/components/TitleView.vue
Original file line number Diff line number Diff line change
@@ -1,28 +1,14 @@
<template>
<div class="flex items-center justify-between w-full p-2">
<div class="flex flex-row gap-2 items-center">
<Button
class="w-7 h-7 rounded-md"
size="icon"
variant="outline"
@click="onSidebarButtonClick"
>
<Icon v-if="chatStore.isSidebarOpen" icon="lucide:panel-left-close" class="w-4 h-4" />
<Icon v-else icon="lucide:panel-left-open" class="w-4 h-4" />
</Button>
</div>

<div class="flex items-center gap-2">
<Button
class="w-7 h-7 rounded-md relative"
size="icon"
variant="outline"
:class="{ 'bg-accent': chatStore.isMessageNavigationOpen }"
@click="onMessageNavigationButtonClick"
>
<Icon icon="lucide:list" class="w-4 h-4" />
</Button>
</div>
<div class="flex items-center justify-end w-full p-2">
<Button
class="w-7 h-7 rounded-md relative"
size="icon"
variant="outline"
:class="{ 'bg-accent': chatStore.isMessageNavigationOpen }"
@click="onMessageNavigationButtonClick"
>
<Icon icon="lucide:list" class="w-4 h-4" />
</Button>
</div>
</template>

Expand All @@ -35,10 +21,6 @@ const emit = defineEmits(['messageNavigationToggle'])

const chatStore = useChatStore()

const onSidebarButtonClick = () => {
chatStore.isSidebarOpen = !chatStore.isSidebarOpen
}

// 新增的事件处理函数
const onMessageNavigationButtonClick = () => {
emit('messageNavigationToggle')
Expand Down
5 changes: 5 additions & 0 deletions src/renderer/src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ export const SHORTCUT_EVENTS = {
DELETE_CONVERSATION: 'shortcut:delete-conversation'
}

// Thread view related events
export const THREAD_VIEW_EVENTS = {
TOGGLE: 'thread-view:toggle'
}

// 标签页相关事件
export const TAB_EVENTS = {
TITLE_UPDATED: 'tab:title-updated', // 标签页标题更新
Expand Down
44 changes: 2 additions & 42 deletions src/renderer/src/views/ChatTabView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,6 @@
]"
>
<div class="flex h-full">
<!-- 会话列表 (根据语言方向自适应位置) -->
<Transition
enter-active-class="transition-all duration-300 ease-out"
leave-active-class="transition-all duration-300 ease-in"
:enter-from-class="
langStore.dir === 'rtl' ? 'translate-x-full opacity-0' : '-translate-x-full opacity-0'
"
:leave-to-class="
langStore.dir === 'rtl' ? 'translate-x-full opacity-0' : '-translate-x-full opacity-0'
"
>
<div
v-show="chatStore.isSidebarOpen"
ref="sidebarRef"
:class="[
'w-60 max-w-60 h-full fixed z-20 lg:relative',
langStore.dir === 'rtl' ? 'right-0' : 'left-0'
]"
:dir="langStore.dir"
>
<ThreadsView class="transform" />
</div>
</Transition>

<!-- 主聊天区域 -->
<div class="flex-1 flex flex-col w-0">
<!-- 新会话 -->
Expand Down Expand Up @@ -73,7 +49,7 @@
<div class="flex-1" @click="chatStore.isMessageNavigationOpen = false"></div>

<!-- 侧边栏 -->
<div ref="messageNavigationRef" class="w-80 max-w-80">
<div class="w-80 max-w-80">
<MessageNavigationSidebar
:messages="chatStore.variantAwareMessages"
:is-open="chatStore.isMessageNavigationOpen"
Expand All @@ -94,21 +70,18 @@
import { defineAsyncComponent } from 'vue'
import { useChatStore } from '@/stores/chat'
import { watch, ref } from 'vue'
import { onClickOutside, useTitle, useMediaQuery } from '@vueuse/core'
import { useTitle, useMediaQuery } from '@vueuse/core'
import { useArtifactStore } from '@/stores/artifact'
import ArtifactDialog from '@/components/artifacts/ArtifactDialog.vue'
import MessageNavigationSidebar from '@/components/MessageNavigationSidebar.vue'
import { useRoute } from 'vue-router'
import { useLanguageStore } from '@/stores/language'
const ThreadsView = defineAsyncComponent(() => import('@/components/ThreadsView.vue'))
const TitleView = defineAsyncComponent(() => import('@/components/TitleView.vue'))
const ChatView = defineAsyncComponent(() => import('@/components/ChatView.vue'))
const NewThread = defineAsyncComponent(() => import('@/components/NewThread.vue'))
const artifactStore = useArtifactStore()
const route = useRoute()
const chatStore = useChatStore()
const title = useTitle()
const langStore = useLanguageStore()
const chatViewRef = ref()
// 添加标题更新逻辑
const updateTitle = () => {
Expand Down Expand Up @@ -141,21 +114,8 @@ watch(
)

// 点击外部区域关闭侧边栏
const sidebarRef = ref<HTMLElement>()
const messageNavigationRef = ref<HTMLElement>()
const isLargeScreen = useMediaQuery('(min-width: 1024px)')

onClickOutside(sidebarRef, (event) => {
const isClickInMessageNavigation = messageNavigationRef.value?.contains(event.target as Node)

if (chatStore.isSidebarOpen && !isLargeScreen.value) {
chatStore.isSidebarOpen = false
}
if (chatStore.isMessageNavigationOpen && !isLargeScreen.value && !isClickInMessageNavigation) {
chatStore.isMessageNavigationOpen = false
}
})

const handleMessageNavigationToggle = () => {
if (artifactStore.isOpen) {
artifactStore.isOpen = false
Expand Down