feat(web): 重构智能体文件侧边栏
This commit is contained in:
parent
84dc55ee74
commit
a8536a2e85
@ -1,6 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="chat-container">
|
<div class="chat-container">
|
||||||
<div class="chat">
|
<div
|
||||||
|
class="chat"
|
||||||
|
:class="{
|
||||||
|
'has-file-panel': isFilePanelOpen,
|
||||||
|
'is-resizing-file-panel': isResizing
|
||||||
|
}"
|
||||||
|
:style="{ '--file-panel-width': filePanelWidthStyle }"
|
||||||
|
>
|
||||||
<div class="chat-header">
|
<div class="chat-header">
|
||||||
<div class="header__left">
|
<div class="header__left">
|
||||||
<slot name="header-left"></slot>
|
<slot name="header-left"></slot>
|
||||||
@ -16,16 +23,32 @@
|
|||||||
v-if="showStateEntry"
|
v-if="showStateEntry"
|
||||||
type="button"
|
type="button"
|
||||||
class="agent-nav-btn agent-state-btn state-entry-btn"
|
class="agent-nav-btn agent-state-btn state-entry-btn"
|
||||||
:class="{ active: sideActive === 'state' }"
|
:class="{ active: statePanelOpen }"
|
||||||
title="查看状态"
|
title="查看状态"
|
||||||
|
:aria-expanded="statePanelOpen"
|
||||||
|
aria-controls="agent-state-panel"
|
||||||
@click.stop="toggleStatePanel"
|
@click.stop="toggleStatePanel"
|
||||||
>
|
>
|
||||||
<SquareCheck size="18" class="nav-btn-icon" />
|
<SquareCheck size="18" class="nav-btn-icon" />
|
||||||
<span class="hide-text">状态</span>
|
<span class="hide-text">状态</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="showFileEntry && !isFilePanelOpen"
|
||||||
|
type="button"
|
||||||
|
class="agent-nav-btn agent-state-btn file-entry-btn"
|
||||||
|
title="查看文件"
|
||||||
|
:aria-expanded="isFilePanelOpen"
|
||||||
|
aria-controls="agent-file-panel"
|
||||||
|
@click.stop="toggleAgentPanel"
|
||||||
|
>
|
||||||
|
<FolderKanban size="18" class="nav-btn-icon" />
|
||||||
|
<span class="hide-text">文件</span>
|
||||||
|
</button>
|
||||||
<slot
|
<slot
|
||||||
name="header-right"
|
name="header-right"
|
||||||
:side-active="sideActive"
|
:side-active="sideActive"
|
||||||
|
:is-file-panel-open="isFilePanelOpen"
|
||||||
|
:is-state-panel-open="statePanelOpen"
|
||||||
:has-active-thread="!!currentChatId"
|
:has-active-thread="!!currentChatId"
|
||||||
:toggle-agent-panel="toggleAgentPanel"
|
:toggle-agent-panel="toggleAgentPanel"
|
||||||
></slot>
|
></slot>
|
||||||
@ -36,8 +59,9 @@
|
|||||||
ref="chatContentContainerRef"
|
ref="chatContentContainerRef"
|
||||||
class="chat-content-container"
|
class="chat-content-container"
|
||||||
:class="{
|
:class="{
|
||||||
'has-file-panel': sideActive === 'file',
|
'has-file-panel': isFilePanelOpen,
|
||||||
'has-state-panel': sideActive === 'state'
|
'has-state-panel': statePanelDocked,
|
||||||
|
'has-floating-state-panel': statePanelFloating
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<!-- Main Chat Area -->
|
<!-- Main Chat Area -->
|
||||||
@ -163,46 +187,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Agent Panel Area -->
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="side-panel side-panel--file"
|
|
||||||
ref="panelWrapperRef"
|
|
||||||
:class="{
|
|
||||||
'is-visible': sideActive === 'file',
|
|
||||||
'no-transition': isResizing
|
|
||||||
}"
|
|
||||||
:style="{
|
|
||||||
flexBasis: sideActive === 'file' ? `${panelRatio * 100}%` : '0px'
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<AgentPanel
|
|
||||||
v-if="sideActive === 'file'"
|
|
||||||
:agent-state="currentAgentState"
|
|
||||||
:thread-id="currentChatId"
|
|
||||||
:panel-ratio="panelRatio"
|
|
||||||
:preview-tabs="agentPanelPreviewTabs"
|
|
||||||
:active-preview-path="agentPanelActivePreviewPath"
|
|
||||||
:view-mode="agentPanelViewMode"
|
|
||||||
@refresh="handleAgentStateRefresh"
|
|
||||||
@resize="handlePanelResize"
|
|
||||||
@resizing="handleResizingChange"
|
|
||||||
@open-preview="openPanelPreview"
|
|
||||||
@activate-preview="activatePanelPreview"
|
|
||||||
@close-preview-tab="closePanelPreviewTab"
|
|
||||||
@close-preview-path="closePanelPreviewPath"
|
|
||||||
@view-mode-change="setAgentPanelViewMode"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
id="agent-state-panel"
|
||||||
class="side-panel side-panel--state"
|
class="side-panel side-panel--state"
|
||||||
:class="{ 'is-visible': sideActive === 'state' }"
|
:class="{
|
||||||
|
'is-visible': statePanelOpen,
|
||||||
|
'is-docked': statePanelDocked,
|
||||||
|
'is-floating': statePanelFloating
|
||||||
|
}"
|
||||||
:style="{
|
:style="{
|
||||||
flexBasis: sideActive === 'state' ? '340px' : '0px'
|
flexBasis: statePanelDocked ? `${statePanelDockWidth}px` : '0px'
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div v-if="sideActive === 'state'" class="state-panel">
|
<div v-if="statePanelOpen" class="state-panel">
|
||||||
<div class="side-panel__header state-panel-header">
|
<div class="side-panel__header state-panel-header">
|
||||||
<span class="state-panel-title">状态</span>
|
<span class="state-panel-title">状态</span>
|
||||||
<div class="state-panel-header-actions">
|
<div class="state-panel-header-actions">
|
||||||
@ -442,6 +439,38 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
id="agent-file-panel"
|
||||||
|
class="side-panel side-panel--file"
|
||||||
|
ref="panelWrapperRef"
|
||||||
|
:class="{
|
||||||
|
'is-visible': isFilePanelOpen,
|
||||||
|
'no-transition': isResizing
|
||||||
|
}"
|
||||||
|
:style="{
|
||||||
|
width: filePanelWidthStyle
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<AgentPanel
|
||||||
|
v-if="isFilePanelOpen"
|
||||||
|
:agent-state="currentAgentState"
|
||||||
|
:thread-id="currentChatId"
|
||||||
|
:panel-ratio="panelRatio"
|
||||||
|
:preview-tabs="agentPanelPreviewTabs"
|
||||||
|
:active-preview-path="agentPanelActivePreviewPath"
|
||||||
|
:view-mode="agentPanelViewMode"
|
||||||
|
@close="closeFilePanel"
|
||||||
|
@refresh="handleAgentStateRefresh"
|
||||||
|
@resize="handlePanelResize"
|
||||||
|
@resizing="handleResizingChange"
|
||||||
|
@open-preview="openPanelPreview"
|
||||||
|
@activate-preview="activatePanelPreview"
|
||||||
|
@close-preview-tab="closePanelPreviewTab"
|
||||||
|
@close-preview-path="closePanelPreviewPath"
|
||||||
|
@view-mode-change="setAgentPanelViewMode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SubagentThreadModal
|
<SubagentThreadModal
|
||||||
@ -468,7 +497,7 @@ import {
|
|||||||
onDeactivated
|
onDeactivated
|
||||||
} from 'vue'
|
} from 'vue'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import { ChevronDown, RefreshCw, SquareCheck } from 'lucide-vue-next'
|
import { ChevronDown, FolderKanban, RefreshCw, SquareCheck } from 'lucide-vue-next'
|
||||||
import { formatFileSize } from '@/utils/file_utils'
|
import { formatFileSize } from '@/utils/file_utils'
|
||||||
import FileTypeIcon from '@/components/common/FileTypeIcon.vue'
|
import FileTypeIcon from '@/components/common/FileTypeIcon.vue'
|
||||||
import { generatePixelAvatar } from '@/utils/pixelAvatar'
|
import { generatePixelAvatar } from '@/utils/pixelAvatar'
|
||||||
@ -593,18 +622,30 @@ const configNoticeScrollVersion = ref(0)
|
|||||||
|
|
||||||
// 本地 UI 状态(仅在本组件使用)
|
// 本地 UI 状态(仅在本组件使用)
|
||||||
const localUIState = reactive({
|
const localUIState = reactive({
|
||||||
chatMainWidth: typeof window !== 'undefined' ? window.innerWidth : 0
|
chatMainWidth: typeof window !== 'undefined' ? window.innerWidth : 0,
|
||||||
|
chatContentWidth: typeof window !== 'undefined' ? window.innerWidth : 0
|
||||||
})
|
})
|
||||||
|
|
||||||
// Agent Panel State
|
// Agent Panel State
|
||||||
const sideActive = ref('')
|
const isFilePanelOpen = ref(false)
|
||||||
|
const statePanelOpen = ref(false)
|
||||||
|
const sideActive = computed(() => {
|
||||||
|
if (isFilePanelOpen.value) return 'file'
|
||||||
|
if (statePanelOpen.value) return 'state'
|
||||||
|
return ''
|
||||||
|
})
|
||||||
const isResizing = ref(false)
|
const isResizing = ref(false)
|
||||||
const defaultPanelRatio = 0.3
|
const defaultPanelRatio = 0.3
|
||||||
const previewPanelRatio = 0.65
|
const previewPanelRatio = 0.65
|
||||||
const minPanelRatio = 0.25
|
const minPanelRatio = 0.25
|
||||||
const maxPanelRatio = 0.75
|
const maxPanelRatio = 0.75
|
||||||
const minChatMainWidth = 350
|
const minChatMainWidth = 350
|
||||||
|
const filePanelGapWidth = 0
|
||||||
|
const mobilePanelBreakpoint = 768
|
||||||
|
const statePanelDockWidth = 340
|
||||||
|
const statePanelDockMinChatWidth = 800
|
||||||
const panelRatio = ref(defaultPanelRatio) // 面板宽度比例 (0-1)
|
const panelRatio = ref(defaultPanelRatio) // 面板宽度比例 (0-1)
|
||||||
|
const filePanelDragWidth = ref(null)
|
||||||
const agentPanelPreviewTabs = ref([])
|
const agentPanelPreviewTabs = ref([])
|
||||||
const agentPanelActivePreviewPath = ref('')
|
const agentPanelActivePreviewPath = ref('')
|
||||||
const agentPanelViewMode = ref('tree')
|
const agentPanelViewMode = ref('tree')
|
||||||
@ -627,11 +668,22 @@ const getPanelContainerWidth = () => {
|
|||||||
return container?.clientWidth || (typeof window !== 'undefined' ? window.innerWidth : 0)
|
return container?.clientWidth || (typeof window !== 'undefined' ? window.innerWidth : 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getFilePanelMaxWidth = (containerWidth = getPanelContainerWidth()) => {
|
||||||
|
if (!containerWidth) return 0
|
||||||
|
if (containerWidth <= mobilePanelBreakpoint) return Math.max(0, containerWidth - 16)
|
||||||
|
return Math.max(0, containerWidth - minChatMainWidth - filePanelGapWidth)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFilePanelMinWidth = (containerWidth, maxWidth = getFilePanelMaxWidth(containerWidth)) => {
|
||||||
|
const preferredMinWidth = containerWidth <= mobilePanelBreakpoint ? 280 : 320
|
||||||
|
return Math.min(preferredMinWidth, maxWidth)
|
||||||
|
}
|
||||||
|
|
||||||
const getMaxPanelRatio = (containerWidth = getPanelContainerWidth()) => {
|
const getMaxPanelRatio = (containerWidth = getPanelContainerWidth()) => {
|
||||||
if (!containerWidth) return maxPanelRatio
|
if (!containerWidth) return maxPanelRatio
|
||||||
return Math.max(
|
return Math.max(
|
||||||
minPanelRatio,
|
minPanelRatio,
|
||||||
Math.min(maxPanelRatio, (containerWidth - minChatMainWidth) / containerWidth)
|
Math.min(maxPanelRatio, getFilePanelMaxWidth(containerWidth) / containerWidth)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -639,20 +691,43 @@ const clampPanelRatio = (ratio, containerWidth = getPanelContainerWidth()) => {
|
|||||||
return Math.max(minPanelRatio, Math.min(ratio, getMaxPanelRatio(containerWidth)))
|
return Math.max(minPanelRatio, Math.min(ratio, getMaxPanelRatio(containerWidth)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const filePanelWidthStyle = computed(() => {
|
||||||
|
if (!isFilePanelOpen.value) return '0px'
|
||||||
|
if (filePanelDragWidth.value !== null) return `${filePanelDragWidth.value}px`
|
||||||
|
|
||||||
|
const containerWidth = localUIState.chatContentWidth || getPanelContainerWidth()
|
||||||
|
if (!containerWidth) return `${panelRatio.value * 100}%`
|
||||||
|
|
||||||
|
const maxWidth = getFilePanelMaxWidth(containerWidth)
|
||||||
|
const minWidth = getFilePanelMinWidth(containerWidth, maxWidth)
|
||||||
|
const preferredWidth = containerWidth * panelRatio.value
|
||||||
|
return `${Math.max(minWidth, Math.min(preferredWidth, maxWidth))}px`
|
||||||
|
})
|
||||||
|
|
||||||
|
const statePanelCanDock = computed(() => {
|
||||||
|
if (isFilePanelOpen.value) return false
|
||||||
|
const containerWidth = localUIState.chatContentWidth || getPanelContainerWidth()
|
||||||
|
return containerWidth - statePanelDockWidth > statePanelDockMinChatWidth
|
||||||
|
})
|
||||||
|
const statePanelDocked = computed(() => statePanelOpen.value && statePanelCanDock.value)
|
||||||
|
const statePanelFloating = computed(() => statePanelOpen.value && !statePanelDocked.value)
|
||||||
|
|
||||||
const setPanelRatioForViewMode = () => {
|
const setPanelRatioForViewMode = () => {
|
||||||
const hasPreview = Boolean(agentPanelActivePreviewPath.value)
|
const hasPreview = Boolean(agentPanelActivePreviewPath.value)
|
||||||
panelRatio.value = clampPanelRatio(hasPreview ? previewPanelRatio : defaultPanelRatio)
|
panelRatio.value = clampPanelRatio(hasPreview ? previewPanelRatio : defaultPanelRatio)
|
||||||
}
|
}
|
||||||
|
|
||||||
const showFilePanel = (mode = 'tree') => {
|
const showFilePanel = (mode = 'tree') => {
|
||||||
sideActive.value = 'file'
|
isFilePanelOpen.value = true
|
||||||
|
statePanelOpen.value = false
|
||||||
agentPanelViewMode.value =
|
agentPanelViewMode.value =
|
||||||
mode === 'preview' && agentPanelActivePreviewPath.value ? 'preview' : 'tree'
|
mode === 'preview' && agentPanelActivePreviewPath.value ? 'preview' : 'tree'
|
||||||
setPanelRatioForViewMode()
|
setPanelRatioForViewMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
const showFileTreePanel = () => {
|
const showFileTreePanel = () => {
|
||||||
sideActive.value = 'file'
|
isFilePanelOpen.value = true
|
||||||
|
statePanelOpen.value = false
|
||||||
agentPanelActivePreviewPath.value = ''
|
agentPanelActivePreviewPath.value = ''
|
||||||
agentPanelViewMode.value = 'tree'
|
agentPanelViewMode.value = 'tree'
|
||||||
setPanelRatioForViewMode()
|
setPanelRatioForViewMode()
|
||||||
@ -709,7 +784,8 @@ const isSameOrChildPanelPath = (path, targetPath) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const resetAgentPanelState = () => {
|
const resetAgentPanelState = () => {
|
||||||
sideActive.value = ''
|
isFilePanelOpen.value = false
|
||||||
|
statePanelOpen.value = false
|
||||||
panelRatio.value = defaultPanelRatio
|
panelRatio.value = defaultPanelRatio
|
||||||
agentPanelPreviewTabs.value = []
|
agentPanelPreviewTabs.value = []
|
||||||
agentPanelActivePreviewPath.value = ''
|
agentPanelActivePreviewPath.value = ''
|
||||||
@ -957,6 +1033,7 @@ const completedTodoCount = computed(
|
|||||||
() => currentTodos.value.filter((todo) => todo?.status === 'completed').length
|
() => currentTodos.value.filter((todo) => todo?.status === 'completed').length
|
||||||
)
|
)
|
||||||
const showStateEntry = computed(() => Boolean(currentChatId.value))
|
const showStateEntry = computed(() => Boolean(currentChatId.value))
|
||||||
|
const showFileEntry = computed(() => Boolean(currentChatId.value))
|
||||||
const todoProgress = computed(() => {
|
const todoProgress = computed(() => {
|
||||||
if (!totalTodoCount.value) return 0
|
if (!totalTodoCount.value) return 0
|
||||||
return Math.round((completedTodoCount.value / totalTodoCount.value) * 100)
|
return Math.round((completedTodoCount.value / totalTodoCount.value) * 100)
|
||||||
@ -1256,7 +1333,7 @@ const isStreaming = computed(() => {
|
|||||||
return threadState ? threadState.isStreaming : false
|
return threadState ? threadState.isStreaming : false
|
||||||
})
|
})
|
||||||
const shouldRefreshStateWhileStreaming = computed(
|
const shouldRefreshStateWhileStreaming = computed(
|
||||||
() => Boolean(currentChatId.value) && isStreaming.value && sideActive.value === 'state'
|
() => Boolean(currentChatId.value) && isStreaming.value && statePanelOpen.value
|
||||||
)
|
)
|
||||||
const isProcessing = computed(() => isStreaming.value)
|
const isProcessing = computed(() => isStreaming.value)
|
||||||
const isReplyLoading = computed(() => {
|
const isReplyLoading = computed(() => {
|
||||||
@ -1562,19 +1639,24 @@ const startChatMainResizeObserver = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
localUIState.chatMainWidth = chatMainRef.value.clientWidth || window.innerWidth
|
const syncLayoutWidths = () => {
|
||||||
|
localUIState.chatMainWidth = chatMainRef.value?.clientWidth || window.innerWidth
|
||||||
|
localUIState.chatContentWidth =
|
||||||
|
chatContentContainerRef.value?.clientWidth || localUIState.chatMainWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
syncLayoutWidths()
|
||||||
chatMainResizeObserver = new ResizeObserver((entries) => {
|
chatMainResizeObserver = new ResizeObserver((entries) => {
|
||||||
// 初始化期间跳过检查,等待 layout 稳定
|
// 初始化期间跳过检查,等待 layout 稳定
|
||||||
if (!isResizeObserverReady) return
|
if (!isResizeObserverReady) return
|
||||||
|
|
||||||
for (const entry of entries) {
|
if (!entries.length) return
|
||||||
const width = entry.contentRect.width
|
syncLayoutWidths()
|
||||||
if (!width) continue
|
|
||||||
|
|
||||||
localUIState.chatMainWidth = width
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
chatMainResizeObserver.observe(chatMainRef.value)
|
chatMainResizeObserver.observe(chatMainRef.value)
|
||||||
|
if (chatContentContainerRef.value) {
|
||||||
|
chatMainResizeObserver.observe(chatContentContainerRef.value)
|
||||||
|
}
|
||||||
armResizeObserver()
|
armResizeObserver()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2203,18 +2285,23 @@ const handleAgentStateRefresh = async (threadId = null) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const toggleStatePanel = async () => {
|
const toggleStatePanel = async () => {
|
||||||
const nextOpen = sideActive.value !== 'state'
|
const nextOpen = !statePanelOpen.value
|
||||||
sideActive.value = nextOpen ? 'state' : ''
|
statePanelOpen.value = nextOpen
|
||||||
if (nextOpen && currentChatId.value && !currentAgentState.value) {
|
if (nextOpen && currentChatId.value && !currentAgentState.value) {
|
||||||
await handleAgentStateRefresh()
|
await handleAgentStateRefresh()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const closeFilePanel = () => {
|
||||||
|
isFilePanelOpen.value = false
|
||||||
|
filePanelDragWidth.value = null
|
||||||
|
}
|
||||||
|
|
||||||
const toggleAgentPanel = async () => {
|
const toggleAgentPanel = async () => {
|
||||||
const nextOpen = sideActive.value !== 'file'
|
const nextOpen = !isFilePanelOpen.value
|
||||||
|
|
||||||
if (!nextOpen) {
|
if (!nextOpen) {
|
||||||
sideActive.value = ''
|
closeFilePanel()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2233,11 +2320,11 @@ const handlePanelResize = (clientX) => {
|
|||||||
|
|
||||||
const deltaX = clientX - resizeStartX
|
const deltaX = clientX - resizeStartX
|
||||||
const rawWidth = resizeStartWidth - deltaX
|
const rawWidth = resizeStartWidth - deltaX
|
||||||
const minWidth = minPanelRatio * panelContainerWidth
|
const maxWidth = getFilePanelMaxWidth(panelContainerWidth)
|
||||||
const maxWidth = getMaxPanelRatio(panelContainerWidth) * panelContainerWidth
|
const minWidth = getFilePanelMinWidth(panelContainerWidth, maxWidth)
|
||||||
const nextWidth = Math.max(minWidth, Math.min(rawWidth, maxWidth))
|
const nextWidth = Math.max(minWidth, Math.min(rawWidth, maxWidth))
|
||||||
|
|
||||||
panelWrapperRef.value.style.setProperty('flex', `0 0 ${nextWidth}px`, 'important')
|
filePanelDragWidth.value = nextWidth
|
||||||
|
|
||||||
if (nextWidth !== rawWidth) {
|
if (nextWidth !== rawWidth) {
|
||||||
resizeStartX = clientX
|
resizeStartX = clientX
|
||||||
@ -2252,6 +2339,7 @@ const handleResizingChange = (isResizingState, clientX = 0) => {
|
|||||||
if (isResizingState && panelWrapperRef.value) {
|
if (isResizingState && panelWrapperRef.value) {
|
||||||
resizeStartX = clientX
|
resizeStartX = clientX
|
||||||
resizeStartWidth = panelWrapperRef.value.offsetWidth
|
resizeStartWidth = panelWrapperRef.value.offsetWidth
|
||||||
|
filePanelDragWidth.value = resizeStartWidth
|
||||||
if (!panelContainerWidth) {
|
if (!panelContainerWidth) {
|
||||||
panelContainerWidth = getPanelContainerWidth()
|
panelContainerWidth = getPanelContainerWidth()
|
||||||
}
|
}
|
||||||
@ -2259,9 +2347,12 @@ const handleResizingChange = (isResizingState, clientX = 0) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!isResizingState && panelWrapperRef.value && panelContainerWidth) {
|
if (!isResizingState && panelWrapperRef.value && panelContainerWidth) {
|
||||||
const finalWidth = panelWrapperRef.value.offsetWidth
|
const finalWidth = filePanelDragWidth.value ?? panelWrapperRef.value.offsetWidth
|
||||||
panelRatio.value = clampPanelRatio(finalWidth / panelContainerWidth, panelContainerWidth)
|
panelRatio.value = clampPanelRatio(finalWidth / panelContainerWidth, panelContainerWidth)
|
||||||
panelWrapperRef.value.style.removeProperty('flex')
|
}
|
||||||
|
|
||||||
|
if (!isResizingState) {
|
||||||
|
filePanelDragWidth.value = null
|
||||||
resizeStartX = 0
|
resizeStartX = 0
|
||||||
resizeStartWidth = 0
|
resizeStartWidth = 0
|
||||||
panelContainerWidth = 0 // 重置,供下次使用
|
panelContainerWidth = 0 // 重置,供下次使用
|
||||||
@ -2374,8 +2465,14 @@ onMounted(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
watch(showStateEntry, (visible) => {
|
watch(showStateEntry, (visible) => {
|
||||||
if (!visible && sideActive.value === 'state') {
|
if (!visible && statePanelOpen.value) {
|
||||||
sideActive.value = ''
|
statePanelOpen.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(showFileEntry, (visible) => {
|
||||||
|
if (!visible && isFilePanelOpen.value) {
|
||||||
|
closeFilePanel()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -2507,6 +2604,7 @@ watch(currentChatId, (threadId, oldThreadId) => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 1rem 8px;
|
padding: 1rem 8px;
|
||||||
flex-shrink: 0; /* Prevent header from shrinking */
|
flex-shrink: 0; /* Prevent header from shrinking */
|
||||||
|
transition: padding-right 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
.header__left,
|
.header__left,
|
||||||
.header__right {
|
.header__right {
|
||||||
@ -2535,6 +2633,17 @@ watch(currentChatId, (threadId, oldThreadId) => {
|
|||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.has-file-panel .chat-header {
|
||||||
|
padding-right: calc(var(--file-panel-width) + 8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-resizing-file-panel {
|
||||||
|
.chat-header,
|
||||||
|
.chat-main {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-content-container {
|
.chat-content-container {
|
||||||
@ -2555,39 +2664,43 @@ watch(currentChatId, (threadId, oldThreadId) => {
|
|||||||
position: relative;
|
position: relative;
|
||||||
transition:
|
transition:
|
||||||
flex-basis 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
flex-basis 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||||
|
margin-right 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||||
width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
min-width: 0; /* Prevent flex item from overflowing */
|
min-width: 0; /* Prevent flex item from overflowing */
|
||||||
|
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-content-container.has-file-panel .chat-main {
|
||||||
|
margin-right: var(--file-panel-width);
|
||||||
|
}
|
||||||
|
|
||||||
.side-panel {
|
.side-panel {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: var(--gray-0);
|
background: var(--gray-0);
|
||||||
border: 1px solid var(--gray-150);
|
border: 1px solid var(--gray-150);
|
||||||
border-radius: 16px;
|
border-radius: 10px;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 16px 40px var(--shadow-1),
|
0 16px 40px var(--shadow-1),
|
||||||
0 2px 10px var(--shadow-0);
|
0 2px 10px var(--shadow-0);
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
margin: 0 8px 8px;
|
|
||||||
margin-left: -16px;
|
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
transform: translateX(10px);
|
transform: translateX(10px);
|
||||||
will-change: flex-basis;
|
will-change: width, flex-basis, opacity, transform;
|
||||||
transition:
|
transition:
|
||||||
|
width 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||||
flex-basis 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
flex-basis 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||||
opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||||
transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
margin-left 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.side-panel.is-visible {
|
.side-panel.is-visible {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
margin-left: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.side-panel.no-transition {
|
.side-panel.no-transition {
|
||||||
@ -2595,16 +2708,25 @@ watch(currentChatId, (threadId, oldThreadId) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.side-panel--file {
|
.side-panel--file {
|
||||||
align-self: stretch;
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 30;
|
||||||
|
display: flex;
|
||||||
height: auto;
|
height: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
border: none;
|
||||||
|
border-left: 1px solid var(--gray-150);
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.side-panel--file.is-visible {
|
.side-panel--file.is-visible {
|
||||||
min-width: 320px;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.side-panel--state {
|
.side-panel--state {
|
||||||
align-self: flex-start;
|
|
||||||
height: auto;
|
height: auto;
|
||||||
max-height: calc(100% - 8px);
|
max-height: calc(100% - 8px);
|
||||||
max-width: min(340px, calc(100vw - 24px));
|
max-width: min(340px, calc(100vw - 24px));
|
||||||
@ -2616,6 +2738,25 @@ watch(currentChatId, (threadId, oldThreadId) => {
|
|||||||
min-width: 300px;
|
min-width: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.side-panel--state.is-docked {
|
||||||
|
align-self: flex-start;
|
||||||
|
margin: 0 8px 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-panel--state.is-floating {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: calc(var(--file-panel-width) + 8px);
|
||||||
|
width: min(340px, calc(100% - var(--file-panel-width) - 24px));
|
||||||
|
min-width: 0;
|
||||||
|
max-height: calc(100% - 16px);
|
||||||
|
margin: 0;
|
||||||
|
z-index: 26;
|
||||||
|
box-shadow:
|
||||||
|
0 12px 28px var(--shadow-1),
|
||||||
|
0 2px 8px var(--shadow-0);
|
||||||
|
}
|
||||||
|
|
||||||
.state-panel {
|
.state-panel {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -2779,7 +2920,6 @@ watch(currentChatId, (threadId, oldThreadId) => {
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
padding: 1rem var(--page-padding);
|
padding: 1rem var(--page-padding);
|
||||||
padding-right: 30px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
@ -2931,25 +3071,39 @@ watch(currentChatId, (threadId, oldThreadId) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.side-panel--file.is-visible,
|
.side-panel--file.is-visible,
|
||||||
.side-panel--state.is-visible {
|
.side-panel--state.is-docked.is-visible {
|
||||||
max-width: calc(100% - 350px);
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
.chat.has-file-panel .chat-header {
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-content-container.has-file-panel .chat-main,
|
.chat-content-container.has-file-panel .chat-main,
|
||||||
.chat-content-container.has-state-panel .chat-main {
|
.chat-content-container.has-state-panel .chat-main {
|
||||||
|
margin-right: 0;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.side-panel--file {
|
||||||
|
top: calc(var(--header-height) + 4px);
|
||||||
|
}
|
||||||
|
|
||||||
.side-panel--file.is-visible {
|
.side-panel--file.is-visible {
|
||||||
min-width: 280px;
|
min-width: 0;
|
||||||
max-width: 80%;
|
max-width: calc(100% - 16px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.side-panel--state.is-visible {
|
.side-panel--state.is-visible {
|
||||||
min-width: 260px;
|
min-width: 0;
|
||||||
max-width: 80%;
|
max-width: calc(100% - 24px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-panel--state.is-floating {
|
||||||
|
right: 12px;
|
||||||
|
width: min(320px, calc(100% - 24px));
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-segment-wrapper {
|
.agent-segment-wrapper {
|
||||||
|
|||||||
@ -46,6 +46,14 @@
|
|||||||
<button class="header-action-btn" title="刷新" aria-label="刷新" @click="emitRefresh">
|
<button class="header-action-btn" title="刷新" aria-label="刷新" @click="emitRefresh">
|
||||||
<RefreshCw :size="15" />
|
<RefreshCw :size="15" />
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
class="header-action-btn"
|
||||||
|
title="关闭文件面板"
|
||||||
|
aria-label="关闭文件面板"
|
||||||
|
@click="emitClose"
|
||||||
|
>
|
||||||
|
<PanelRightClose :size="15" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -129,7 +137,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
import { Download, Folders, RefreshCw, Trash2, X } from 'lucide-vue-next'
|
import { Download, Folders, PanelRightClose, RefreshCw, Trash2, X } from 'lucide-vue-next'
|
||||||
import { Modal, message } from 'ant-design-vue'
|
import { Modal, message } from 'ant-design-vue'
|
||||||
import FileTreeComponent from '@/components/FileTreeComponent.vue'
|
import FileTreeComponent from '@/components/FileTreeComponent.vue'
|
||||||
import AgentFilePreview from '@/components/AgentFilePreview.vue'
|
import AgentFilePreview from '@/components/AgentFilePreview.vue'
|
||||||
@ -170,6 +178,7 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
|
'close',
|
||||||
'refresh',
|
'refresh',
|
||||||
'resize',
|
'resize',
|
||||||
'resizing',
|
'resizing',
|
||||||
@ -541,6 +550,10 @@ const emitRefresh = () => {
|
|||||||
emit('refresh', props.threadId)
|
emit('refresh', props.threadId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const emitClose = () => {
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
|
||||||
let resizePointerId = null
|
let resizePointerId = null
|
||||||
let pendingClientX = 0
|
let pendingClientX = 0
|
||||||
let resizeFrameId = 0
|
let resizeFrameId = 0
|
||||||
@ -663,7 +676,9 @@ watch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
.agent-panel {
|
.agent-panel {
|
||||||
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
min-width: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -675,8 +690,7 @@ watch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
.panel-header {
|
.panel-header {
|
||||||
border-bottom: none;
|
border-bottom: 1px solid var(--gray-100);
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.side-preview-shell) {
|
:deep(.side-preview-shell) {
|
||||||
|
|||||||
@ -78,20 +78,6 @@
|
|||||||
</template>
|
</template>
|
||||||
</a-dropdown>
|
</a-dropdown>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #header-right="{ sideActive, hasActiveThread, toggleAgentPanel }">
|
|
||||||
<button
|
|
||||||
v-if="hasActiveThread"
|
|
||||||
type="button"
|
|
||||||
class="agent-nav-btn agent-state-btn"
|
|
||||||
:class="{ active: sideActive === 'file' }"
|
|
||||||
title="查看文件"
|
|
||||||
@click.stop="toggleAgentPanel"
|
|
||||||
>
|
|
||||||
<FolderKanban size="18" class="nav-btn-icon" />
|
|
||||||
<span class="hide-text">文件</span>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
</AgentChatComponent>
|
</AgentChatComponent>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -101,7 +87,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import { Settings2, ChevronDown, Check, FolderKanban } from 'lucide-vue-next'
|
import { Settings2, ChevronDown, Check } from 'lucide-vue-next'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import AgentChatComponent from '@/components/AgentChatComponent.vue'
|
import AgentChatComponent from '@/components/AgentChatComponent.vue'
|
||||||
import { isBuiltinAgent, useAgentStore } from '@/stores/agent'
|
import { isBuiltinAgent, useAgentStore } from '@/stores/agent'
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user