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:
Acbox
2026-03-28 19:44:36 +08:00
parent bca13a13fa
commit eb99f75c37
5 changed files with 97 additions and 6 deletions
+56 -3
View File
@@ -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>
+3 -1
View File
@@ -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'))
+30
View File
@@ -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 }
}
+4 -1
View File
@@ -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",
+4 -1
View File
@@ -36,7 +36,10 @@
"createdAt": "创建时间",
"none": "无",
"yes": "是",
"no": "否"
"no": "否",
"details": "详情",
"pin": "置顶",
"unpin": "取消置顶"
},
"auth": {
"welcome": "欢迎回来",