mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
feat(web): add resizable session sidebar and inline file manager panel
Replace fixed-width session sidebar with a draggable resize handle on its right edge (180–480px, persisted to localStorage). Convert the file manager from a Sheet overlay to an embedded right-side panel with a left-edge resize handle (320–800px, also persisted).
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div class="flex-1 flex flex-col h-full">
|
||||
<div class="flex-1 flex h-full min-w-0">
|
||||
<div class="flex-1 flex flex-col h-full min-w-0">
|
||||
<!-- No bot selected -->
|
||||
<div
|
||||
v-if="!currentBotId"
|
||||
@@ -196,19 +197,37 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- File manager sheet -->
|
||||
<Sheet v-model:open="fileManagerOpen">
|
||||
<SheetContent
|
||||
side="right"
|
||||
class="sm:max-w-2xl w-full p-0 flex flex-col"
|
||||
<!-- File manager panel -->
|
||||
<div
|
||||
v-if="fileManagerOpen"
|
||||
class="flex shrink-0 h-full relative"
|
||||
:style="{ width: `${fileManagerWidth}px` }"
|
||||
>
|
||||
<SheetHeader class="px-4 pt-4 pb-0">
|
||||
<SheetTitle>{{ $t('chat.files') }}</SheetTitle>
|
||||
<SheetDescription class="sr-only">
|
||||
{{ $t('chat.files') }}
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div
|
||||
class="absolute top-0 left-0 w-1 h-full cursor-col-resize z-10 group"
|
||||
@mousedown="onFmResizeStart"
|
||||
>
|
||||
<div
|
||||
class="w-full h-full transition-colors group-hover:bg-primary/20"
|
||||
:class="{ 'bg-primary/30': isFmResizing }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col h-full flex-1 min-w-0 border-l border-border bg-sidebar">
|
||||
<div class="flex items-center justify-between px-4 h-12 shrink-0">
|
||||
<span class="text-sm font-medium text-foreground">{{ $t('chat.files') }}</span>
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
class="size-6"
|
||||
@click="fileManagerOpen = false"
|
||||
>
|
||||
<X class="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex-1 min-h-0 relative">
|
||||
<FileManager
|
||||
v-if="currentBotId"
|
||||
@@ -217,15 +236,16 @@
|
||||
:sync-url="false"
|
||||
/>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, nextTick, onMounted, provide, useTemplateRef, watchEffect} from 'vue'
|
||||
import { ref, computed, nextTick, onMounted, onBeforeUnmount, provide, useTemplateRef, watchEffect } from 'vue'
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
import { LoaderCircle, Image as ImageIcon, File as FileIcon, X, Paperclip, FolderOpen, Send } from 'lucide-vue-next'
|
||||
import { ScrollArea, Button, InputGroup, InputGroupAddon, InputGroupTextarea, Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription } from '@memohai/ui'
|
||||
import { ScrollArea, Button, InputGroup, InputGroupAddon, InputGroupTextarea } from '@memohai/ui'
|
||||
import { useChatStore } from '@/store/chat-list'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import MessageItem from './message-item.vue'
|
||||
@@ -242,6 +262,43 @@ const pendingFiles = ref<File[]>([])
|
||||
const fileManagerOpen = ref(false)
|
||||
const fileManagerRef = ref<InstanceType<typeof FileManager> | null>(null)
|
||||
|
||||
const FM_MIN_WIDTH = 320
|
||||
const FM_MAX_WIDTH = 800
|
||||
const FM_DEFAULT_WIDTH = 520
|
||||
|
||||
const fileManagerWidth = useLocalStorage('file-manager-panel-width', FM_DEFAULT_WIDTH)
|
||||
const isFmResizing = ref(false)
|
||||
|
||||
function onFmResizeStart(e: MouseEvent) {
|
||||
e.preventDefault()
|
||||
isFmResizing.value = true
|
||||
const startX = e.clientX
|
||||
const startWidth = fileManagerWidth.value
|
||||
|
||||
function onMouseMove(ev: MouseEvent) {
|
||||
const delta = startX - ev.clientX
|
||||
fileManagerWidth.value = Math.min(FM_MAX_WIDTH, Math.max(FM_MIN_WIDTH, startWidth + delta))
|
||||
}
|
||||
|
||||
function onMouseUp() {
|
||||
isFmResizing.value = false
|
||||
document.removeEventListener('mousemove', onMouseMove)
|
||||
document.removeEventListener('mouseup', onMouseUp)
|
||||
document.body.style.cursor = ''
|
||||
document.body.style.userSelect = ''
|
||||
}
|
||||
|
||||
document.body.style.cursor = 'col-resize'
|
||||
document.body.style.userSelect = 'none'
|
||||
document.addEventListener('mousemove', onMouseMove)
|
||||
document.addEventListener('mouseup', onMouseUp)
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.body.style.cursor = ''
|
||||
document.body.style.userSelect = ''
|
||||
})
|
||||
|
||||
const FILE_MANAGER_ROOT = '/data'
|
||||
|
||||
function normalizeFileManagerPath(path: string): string {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-full w-[223px] shrink-0 bg-sidebar border-r border-border">
|
||||
<div
|
||||
class="flex shrink-0 h-full relative"
|
||||
:style="{ width: `${sidebarWidth}px` }"
|
||||
>
|
||||
<div class="flex flex-col h-full flex-1 min-w-0 bg-sidebar border-r border-border">
|
||||
<!-- <div class="h-[53px] flex items-center px-2 shrink-0">
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'comment-dots']"
|
||||
@@ -127,10 +131,22 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="absolute top-0 right-0 w-1 h-full cursor-col-resize z-10 group"
|
||||
@mousedown="onResizeStart"
|
||||
>
|
||||
<div
|
||||
class="w-full h-full transition-colors group-hover:bg-primary/20"
|
||||
:class="{ 'bg-primary/30': isResizing }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, onBeforeUnmount } from 'vue'
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
import { Search, Plus, Globe, ChevronDown, Check, LoaderCircle } from 'lucide-vue-next'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -155,6 +171,43 @@ const router = useRouter()
|
||||
const chatStore = useChatStore()
|
||||
const { sessions, sessionId, currentBotId, loadingChats } = storeToRefs(chatStore)
|
||||
|
||||
const MIN_WIDTH = 180
|
||||
const MAX_WIDTH = 480
|
||||
const DEFAULT_WIDTH = 223
|
||||
|
||||
const sidebarWidth = useLocalStorage('session-sidebar-width', DEFAULT_WIDTH)
|
||||
const isResizing = ref(false)
|
||||
|
||||
function onResizeStart(e: MouseEvent) {
|
||||
e.preventDefault()
|
||||
isResizing.value = true
|
||||
const startX = e.clientX
|
||||
const startWidth = sidebarWidth.value
|
||||
|
||||
function onMouseMove(ev: MouseEvent) {
|
||||
const delta = ev.clientX - startX
|
||||
sidebarWidth.value = Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, startWidth + delta))
|
||||
}
|
||||
|
||||
function onMouseUp() {
|
||||
isResizing.value = false
|
||||
document.removeEventListener('mousemove', onMouseMove)
|
||||
document.removeEventListener('mouseup', onMouseUp)
|
||||
document.body.style.cursor = ''
|
||||
document.body.style.userSelect = ''
|
||||
}
|
||||
|
||||
document.body.style.cursor = 'col-resize'
|
||||
document.body.style.userSelect = 'none'
|
||||
document.addEventListener('mousemove', onMouseMove)
|
||||
document.addEventListener('mouseup', onMouseUp)
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.body.style.cursor = ''
|
||||
document.body.style.userSelect = ''
|
||||
})
|
||||
|
||||
const searchQuery = ref('')
|
||||
const filterType = ref<string>('chat')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user