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": "欢迎回来",