mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
feat(web): add bot context menu with details link and pin/unpin
Add a dropdown menu to each bot item in the chat sidebar with: - "Details" option to navigate to the bot's settings page - "Pin/Unpin" option to pin bots to the top of the list, persisted via localStorage
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
>
|
||||
<button
|
||||
:class="[
|
||||
'flex items-center gap-2.5 w-full h-[38px] px-2.5 rounded-lg transition-colors',
|
||||
'group/bot flex items-center gap-2.5 w-full h-[38px] px-2.5 rounded-lg transition-colors',
|
||||
isActive
|
||||
? 'bg-background'
|
||||
: bot.status === 'error'
|
||||
@@ -29,9 +29,45 @@
|
||||
{{ avatarFallback }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="truncate text-xs font-medium text-foreground leading-[18px]">
|
||||
<span class="truncate text-xs font-medium text-foreground leading-[18px] flex-1 text-left">
|
||||
{{ bot.display_name || bot.id }}
|
||||
</span>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
as-child
|
||||
@click.stop
|
||||
>
|
||||
<span
|
||||
class="shrink-0 size-6 flex items-center justify-center rounded text-muted-foreground opacity-0 group-hover/bot:opacity-100 hover:text-foreground hover:bg-accent transition-opacity"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'ellipsis']"
|
||||
class="size-3"
|
||||
/>
|
||||
</span>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
side="bottom"
|
||||
@click.stop
|
||||
>
|
||||
<DropdownMenuItem @click.stop="handleTogglePin">
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'thumbtack']"
|
||||
class="size-3 mr-2"
|
||||
/>
|
||||
{{ pinned ? $t('common.unpin') : $t('common.pin') }}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem @click.stop="handleDetails">
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'gear']"
|
||||
class="size-3 mr-2"
|
||||
/>
|
||||
{{ $t('common.details') }}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</button>
|
||||
</SidebarMenuButton>
|
||||
</template>
|
||||
@@ -43,22 +79,39 @@ import { useRouter } from 'vue-router'
|
||||
import type { BotsBot } from '@memohai/sdk'
|
||||
import { useChatStore } from '@/store/chat-list'
|
||||
import { useAvatarInitials } from '@/composables/useAvatarInitials'
|
||||
import { SidebarMenuButton } from '@memohai/ui'
|
||||
import { usePinnedBots } from '@/composables/usePinnedBots'
|
||||
import {
|
||||
SidebarMenuButton,
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
} from '@memohai/ui'
|
||||
|
||||
const props = defineProps<{ bot: BotsBot }>()
|
||||
|
||||
const router = useRouter()
|
||||
const chatStore = useChatStore()
|
||||
const { currentBotId } = storeToRefs(chatStore)
|
||||
const { isPinned, togglePin } = usePinnedBots()
|
||||
|
||||
const displayName = computed(() => props.bot.display_name || props.bot.id || '')
|
||||
const avatarFallback = useAvatarInitials(() => displayName.value, 'B')
|
||||
|
||||
const isActive = computed(() => currentBotId.value === props.bot.id)
|
||||
const pinned = computed(() => isPinned(props.bot.id ?? ''))
|
||||
|
||||
function handleSelect() {
|
||||
if (props.bot.status === 'error') return
|
||||
chatStore.selectBot(props.bot.id ?? '')
|
||||
router.push({ name: 'chat', params: { botId: props.bot.id } })
|
||||
}
|
||||
|
||||
function handleDetails() {
|
||||
router.push({ name: 'bot-detail', params: { botId: props.bot.id } })
|
||||
}
|
||||
|
||||
function handleTogglePin() {
|
||||
togglePin(props.bot.id ?? '')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -122,14 +122,16 @@ import {
|
||||
SidebarMenuItem,
|
||||
} from '@memohai/ui'
|
||||
import BotItem from './bot-item.vue'
|
||||
import { usePinnedBots } from '@/composables/usePinnedBots'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const { t } = useI18n()
|
||||
// const { userInfo } = useUserStore()
|
||||
const { sortBots } = usePinnedBots()
|
||||
|
||||
const { data: botData, isLoading } = useQuery(getBotsQuery())
|
||||
const bots = computed<BotsBot[]>(() => botData.value?.items ?? [])
|
||||
const bots = computed<BotsBot[]>(() => sortBots(botData.value?.items ?? []))
|
||||
|
||||
const isSettingsActive = computed(() => route.path.startsWith('/settings'))
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useStorage } from '@vueuse/core'
|
||||
|
||||
const pinnedBotIds = useStorage<string[]>('pinned-bot-ids', [])
|
||||
|
||||
export function usePinnedBots() {
|
||||
function isPinned(botId: string) {
|
||||
return pinnedBotIds.value.includes(botId)
|
||||
}
|
||||
|
||||
function togglePin(botId: string) {
|
||||
const idx = pinnedBotIds.value.indexOf(botId)
|
||||
if (idx >= 0) {
|
||||
pinnedBotIds.value.splice(idx, 1)
|
||||
} else {
|
||||
pinnedBotIds.value.push(botId)
|
||||
}
|
||||
}
|
||||
|
||||
function sortBots<T extends { id?: string }>(bots: T[]): T[] {
|
||||
return [...bots].sort((a, b) => {
|
||||
const aPinned = isPinned(a.id ?? '')
|
||||
const bPinned = isPinned(b.id ?? '')
|
||||
if (aPinned && !bPinned) return -1
|
||||
if (!aPinned && bPinned) return 1
|
||||
return 0
|
||||
})
|
||||
}
|
||||
|
||||
return { pinnedBotIds, isPinned, togglePin, sortBots }
|
||||
}
|
||||
@@ -36,7 +36,10 @@
|
||||
"createdAt": "Created at",
|
||||
"none": "None",
|
||||
"yes": "Yes",
|
||||
"no": "No"
|
||||
"no": "No",
|
||||
"details": "Details",
|
||||
"pin": "Pin",
|
||||
"unpin": "Unpin"
|
||||
},
|
||||
"auth": {
|
||||
"welcome": "Welcome Back",
|
||||
|
||||
@@ -36,7 +36,10 @@
|
||||
"createdAt": "创建时间",
|
||||
"none": "无",
|
||||
"yes": "是",
|
||||
"no": "否"
|
||||
"no": "否",
|
||||
"details": "详情",
|
||||
"pin": "置顶",
|
||||
"unpin": "取消置顶"
|
||||
},
|
||||
"auth": {
|
||||
"welcome": "欢迎回来",
|
||||
|
||||
Reference in New Issue
Block a user