diff --git a/apps/web/src/components/sidebar/bot-item.vue b/apps/web/src/components/sidebar/bot-item.vue index c3ff354c..4d6ff9ce 100644 --- a/apps/web/src/components/sidebar/bot-item.vue +++ b/apps/web/src/components/sidebar/bot-item.vue @@ -5,7 +5,7 @@ > @@ -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 ?? '') +} diff --git a/apps/web/src/components/sidebar/index.vue b/apps/web/src/components/sidebar/index.vue index cf4e9146..864a6ba5 100644 --- a/apps/web/src/components/sidebar/index.vue +++ b/apps/web/src/components/sidebar/index.vue @@ -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(() => botData.value?.items ?? []) +const bots = computed(() => sortBots(botData.value?.items ?? [])) const isSettingsActive = computed(() => route.path.startsWith('/settings')) diff --git a/apps/web/src/composables/usePinnedBots.ts b/apps/web/src/composables/usePinnedBots.ts new file mode 100644 index 00000000..49113a4d --- /dev/null +++ b/apps/web/src/composables/usePinnedBots.ts @@ -0,0 +1,30 @@ +import { useStorage } from '@vueuse/core' + +const pinnedBotIds = useStorage('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(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 } +} diff --git a/apps/web/src/i18n/locales/en.json b/apps/web/src/i18n/locales/en.json index d5d5363c..c3dd808d 100644 --- a/apps/web/src/i18n/locales/en.json +++ b/apps/web/src/i18n/locales/en.json @@ -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", diff --git a/apps/web/src/i18n/locales/zh.json b/apps/web/src/i18n/locales/zh.json index a83e15a8..8efe79f9 100644 --- a/apps/web/src/i18n/locales/zh.json +++ b/apps/web/src/i18n/locales/zh.json @@ -36,7 +36,10 @@ "createdAt": "创建时间", "none": "无", "yes": "是", - "no": "否" + "no": "否", + "details": "详情", + "pin": "置顶", + "unpin": "取消置顶" }, "auth": { "welcome": "欢迎回来",