Files
Memoh/apps/web/src/components/settings-sidebar/index.vue
T
zhangxx b308c27f74 feat(web): add sidebar collapse functionality (#314)
* feat: add Supermarket integration (MCP & Skill marketplace) (#309)

* feat: add Supermarket integration (MCP & Skill marketplace)

Backend:
- Add [supermarket] config section with base_url (default: supermarket.memoh.ai)
- Add SupermarketHandler with proxy endpoints for MCPs, Skills, and Tags
- Add install endpoints: POST /bots/:id/supermarket/install-mcp (creates MCP
  connection with env vars) and install-skill (downloads tar.gz, extracts to
  container via gRPC)
- Register handler in FX wiring, generate Swagger docs and TypeScript SDK

Frontend:
- Add /settings/supermarket route with Store icon in sidebar
- Create supermarket page with search, tag filtering, MCP and Skill sections
- Add MCP/Skill card components with tag badges and install buttons
- Add install dialogs: MCP (bot selector + env var form), Skill (bot selector)
- Add i18n entries for en.json and zh.json

* fix: improve supermarket install UX

- Create BotSelect component with avatar + name using UI Select
- Replace NativeSelect in install dialogs and usage page with BotSelect
- Change MCP install flow: navigate to bot detail MCP tab with pre-filled
  draft instead of direct install, letting users review before saving
- Move Supermarket sidebar entry between Browser and Usage

* web: remove supermarket page top tag selector bar

Drop the horizontal tag chips and getSupermarketTags fetch; keep
search and tag filter via card tag clicks with clearable badge.

* web: add homepage link to supermarket MCP and Skill cards

Show an external-link icon next to the card title when homepage is
available, opening in a new tab on click.

* refactor: move skills directory from .skills to skills and enrich prompt

- Change skills storage path from `/data/.skills` to `/data/skills`
- Add usage instructions and directory location to the Skills section
  in the system prompt

* feat(web): add Activity Bar and right sidebar panel to chat page

Replace the old file manager panel with a multi-tab right sidebar system:
- Activity Bar with Terminal, Files, and Info tabs
- Resizable right panel with tab switching
- Extract shared Terminal component from bot-terminal.vue
- Add bottom preview layout mode to FileManager
- Delete session button with confirmation dialog
- Fix FileManager scroll in flex column layout (min-h-0)

* feat(web): add session-type-aware UI for chat interface

- Make IM/heartbeat/schedule/subagent sessions read-only (hide input box)
- Render heartbeat user messages as info blocks with trigger metadata and
  link to heartbeat logs
- Render schedule user messages as info blocks with task metadata and
  link to schedule settings
- Render subagent user messages as full-width markdown boxes
- Add clickable spawn task results to navigate to subagent sessions

* feat(web): add sidebar collapse functionality

- Add SidebarRail for edge drag-to-collapse interaction in both sidebars
- Center align icons when collapsed in bot list and settings sidebar
- Hide text labels and dropdown menus in collapsed state
- Keep create bot button in header

* fix(web): ensure secondary sidebar remains visible after page refresh

When the primary sidebar was collapsed, refreshing the page would cause
the secondary sidebar (e.g., providers list) to disappear.

The MasterDetailSidebarLayout had its own SidebarProvider but didn't
set default-open, causing it to read the same sidebar_state cookie
and follow the primary sidebar's collapsed state.

Fix by explicitly setting :default-open="true" on the secondary
SidebarProvider to ensure it always stays expanded.

---------

Co-authored-by: Acbox Liu <acbox0328@gmail.com>
2026-04-03 01:17:32 +08:00

148 lines
4.0 KiB
Vue

<template>
<aside>
<Sidebar collapsible="icon">
<SidebarHeader class="p-0 border-0">
<button
class="h-[53px] flex items-center gap-2.5 px-3.5 w-full border-b border-border text-foreground hover:bg-accent/50 transition-colors group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:px-0"
@click="router.push(backToChatRoute)"
>
<ChevronLeft
class="size-3 shrink-0"
/>
<span class="text-xs font-semibold group-data-[collapsible=icon]:hidden">
{{ t('sidebar.settings') }}
</span>
</button>
</SidebarHeader>
<SidebarContent>
<SidebarGroup class="px-2 py-2.5">
<SidebarGroupContent>
<SidebarMenu class="gap-0.5">
<SidebarMenuItem
v-for="item in navItems"
:key="item.name"
>
<SidebarMenuButton
:tooltip="item.title"
:is-active="isItemActive(item.name)"
:aria-current="isItemActive(item.name) ? 'page' : undefined"
class="h-9 gap-2 relative before:absolute before:w-0.5 before:top-1.5 before:bottom-1.5 before:left-0 before:rounded-full data-[active=true]:before:bg-[#8B56E3] group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:px-0"
@click="router.push({ name: item.name })"
>
<component
:is="item.icon"
class="size-3.5 ml-1.5 group-data-[collapsible=icon]:ml-0"
/>
<span class="text-xs font-medium group-data-[collapsible=icon]:hidden">{{ item.title }}</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarRail />
</Sidebar>
</aside>
</template>
<script setup lang="ts">
import { computed, type Component } from 'vue'
import { storeToRefs } from 'pinia'
import { useRouter, useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { ChevronLeft, Bot, Boxes, Globe, Brain, Volume2, Mail, AppWindow, ChartLine, User, Store } from 'lucide-vue-next'
import { useChatSelectionStore } from '@/store/chat-selection'
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupContent,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarRail,
} from '@memohai/ui'
const router = useRouter()
const route = useRoute()
const { t } = useI18n()
const selectionStore = useChatSelectionStore()
const { currentBotId, sessionId } = storeToRefs(selectionStore)
const backToChatRoute = computed(() => {
const botId = (currentBotId.value ?? '').trim()
const targetSessionId = (sessionId.value ?? '').trim()
if (!botId) return { name: 'home' as const }
return {
name: 'chat' as const,
params: {
botId,
sessionId: targetSessionId || undefined,
},
}
})
function isItemActive(name: string): boolean {
if (name === 'bots') {
return route.path.startsWith('/settings/bots')
}
return route.name === name
}
const navItems = computed<{ title: string; name: string; icon: Component }[]>(() => [
{
title: t('sidebar.bots'),
name: 'bots',
icon: Bot,
},
{
title: t('sidebar.providers'),
name: 'providers',
icon: Boxes,
},
{
title: t('sidebar.webSearch'),
name: 'web-search',
icon: Globe,
},
{
title: t('sidebar.memory'),
name: 'memory',
icon: Brain,
},
{
title: t('sidebar.speech'),
name: 'speech',
icon: Volume2,
},
{
title: t('sidebar.email'),
name: 'email',
icon: Mail,
},
{
title: t('sidebar.browser'),
name: 'browser',
icon: AppWindow,
},
{
title: t('sidebar.supermarket'),
name: 'supermarket',
icon: Store,
},
{
title: t('sidebar.usage'),
name: 'usage',
icon: ChartLine,
},
{
title: t('sidebar.profile'),
name: 'profile',
icon: User,
},
])
</script>