mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
488 lines
28 KiB
Markdown
488 lines
28 KiB
Markdown
# Web Frontend (apps/web)
|
|
|
|
## Overview
|
|
|
|
`@memohai/web` is the management UI for Memoh, built with Vue 3 + Vite. It provides a chat interface for interacting with bots, plus visual configuration for bots, models, channels, memory, and more.
|
|
|
|
## Tech Stack
|
|
|
|
| Category | Technology |
|
|
|----------|-----------|
|
|
| Framework | Vue 3 (Composition API, `<script setup>`) |
|
|
| Build | Vite 8 + `@vitejs/plugin-vue` |
|
|
| CSS | Tailwind CSS 4 (CSS-based config, no `tailwind.config.*`) |
|
|
| UI Library | `@memohai/ui` (built on Reka UI + class-variance-authority) |
|
|
| State | Pinia 3 + `pinia-plugin-persistedstate` |
|
|
| Data Fetching | Pinia Colada (`@pinia/colada`) + `@memohai/sdk` |
|
|
| Forms | vee-validate + `@vee-validate/zod` + Zod |
|
|
| i18n | vue-i18n (en / zh) |
|
|
| Icons | lucide-vue-next (primary) + `@memohai/icon` (brand/provider icons) |
|
|
| Toast | vue-sonner |
|
|
| Tables | @tanstack/vue-table |
|
|
| Markdown | markstream-vue + Shiki + Mermaid + KaTeX |
|
|
| Charts | ECharts + vue-echarts |
|
|
| Terminal | @xterm/xterm + @xterm/addon-fit + @xterm/addon-serialize |
|
|
| Code Editor | Monaco Editor + stream-monaco |
|
|
| Utilities | @vueuse/core, @vueuse/integrations |
|
|
| Animation | animate.css + tw-animate-css |
|
|
| TypeScript | ~5.9 (strict mode) |
|
|
|
|
## Directory Structure
|
|
|
|
```
|
|
src/
|
|
├── App.vue # Root component (RouterView + Toaster + settings init)
|
|
├── main.ts # App entry (plugins, global components, API client setup)
|
|
├── router.ts # Route definitions, auth guard, chunk error recovery
|
|
├── style.css # Tailwind imports (delegates to @memohai/ui/style.css)
|
|
├── i18n.ts # vue-i18n configuration
|
|
├── assets/ # Static assets (logo.png)
|
|
├── components/ # Shared components
|
|
│ ├── sidebar/ # Bot list sidebar (collapsible, bot items, settings link)
|
|
│ │ ├── index.vue # Sidebar with bot list + settings gear footer
|
|
│ │ └── bot-item.vue # Individual bot entry in sidebar
|
|
│ ├── settings-sidebar/ # Settings section sidebar (back-to-chat + nav items)
|
|
│ ├── main-container/ # Main content area (KeepAlive RouterView)
|
|
│ ├── master-detail-sidebar-layout/ # Master-detail layout pattern
|
|
│ ├── chat-list/ # Chat list helpers
|
|
│ │ └── channel-badge/ # Channel badge component
|
|
│ ├── chat/ # Chat UI sub-components
|
|
│ │ ├── chat-status/ # Chat connection status indicator
|
|
│ │ └── chat-step/ # Chat processing step indicator
|
|
│ ├── file-manager/ # File browser (list + viewer + utils)
|
|
│ ├── terminal/ # Terminal emulator wrapper (xterm)
|
|
│ ├── monaco-editor/ # Monaco code editor wrapper
|
|
│ ├── model-capabilities/ # Model capabilities display
|
|
│ ├── context-window-badge/ # Context window size badge
|
|
│ ├── bot-select/ # Bot selection dropdown
|
|
│ ├── form-dialog-shell/ # Dialog wrapper for forms
|
|
│ ├── confirm-popover/ # Confirmation popover
|
|
│ ├── loading-button/ # Button with loading state
|
|
│ ├── status-dot/ # Status indicator dot
|
|
│ ├── channel-icon/ # Channel platform icon
|
|
│ ├── provider-icon/ # LLM provider icon (icons.ts + index.vue)
|
|
│ ├── search-provider-logo/ # Search provider icons (custom-icons.ts + index.vue)
|
|
│ ├── searchable-select-popover/ # Searchable dropdown
|
|
│ ├── timezone-select/ # Timezone selector
|
|
│ ├── key-value-editor/ # Key-value pair editor
|
|
│ ├── import-models-dialog/ # Bulk model import dialog
|
|
│ ├── add-platform/ # Add platform dialog
|
|
│ ├── add-provider/ # Add LLM provider dialog
|
|
│ └── create-model/ # Create model dialog
|
|
├── composables/ # Reusable composition functions
|
|
│ ├── api/ # API-related composables
|
|
│ │ ├── useChat.ts # Aggregated re-export of chat composables
|
|
│ │ ├── useChat.types.ts # Bot, Session, Message, StreamEvent types
|
|
│ │ ├── useChat.chat-api.ts # Bot/session CRUD (fetchBots, fetchSessions, etc.)
|
|
│ │ ├── useChat.message-api.ts # Message fetch, SSE streaming, local channel
|
|
│ │ ├── useChat.sse.ts # SSE stream reader and parser
|
|
│ │ ├── useChat.sse.test.ts # SSE parser tests
|
|
│ │ ├── useChat.ws.ts # WebSocket connection (send, abort, reconnect)
|
|
│ │ ├── useChat.ws.test.ts # WebSocket tests
|
|
│ │ ├── useChat.content.ts # Message content parsing (tool calls, text, reasoning)
|
|
│ │ ├── useContainerStream.ts # Container creation SSE stream
|
|
│ │ └── usePlatform.ts # Platform list query + create mutation
|
|
│ ├── useDialogMutation.ts # Mutation wrapper with toast error handling
|
|
│ ├── useRetryingStream.ts # SSE retry with exponential backoff
|
|
│ ├── useSyncedQueryParam.ts # URL query param sync
|
|
│ ├── useBotStatusMeta.ts # Bot status metadata
|
|
│ ├── useAvatarInitials.ts # Avatar initial generation
|
|
│ ├── useClipboard.ts # Clipboard utilities
|
|
│ ├── useKeyValueTags.ts # Tag management
|
|
│ ├── usePinnedBots.ts # Pinned bots management
|
|
│ ├── useShikiHighlighter.ts # Shiki syntax highlighter singleton
|
|
│ └── useTerminalCache.ts # Terminal output cache
|
|
├── constants/ # Constants
|
|
│ ├── client-types.ts # LLM client type definitions
|
|
│ ├── compatibilities.ts # Feature compatibility flags
|
|
│ └── acl-presets.ts # ACL preset configurations
|
|
├── i18n/locales/ # Translation files (en.json, zh.json)
|
|
├── layout/
|
|
│ └── main-layout/ # Top-level layout (SidebarProvider)
|
|
├── lib/
|
|
│ └── api-client.ts # SDK client setup (base URL, auth interceptor)
|
|
├── pages/ # Route page components
|
|
│ ├── login/ # Login page
|
|
│ ├── main-section/ # Chat section layout (bot sidebar + main container)
|
|
│ ├── settings-section/ # Settings section layout (settings sidebar + KeepAlive)
|
|
│ ├── home/ # Chat interface (used by both `/` and `/chat/:botId?/:sessionId?`)
|
|
│ │ ├── index.vue # Route ↔ store sync, session sidebar + chat area
|
|
│ │ ├── composables/ # Page-specific composables
|
|
│ │ │ ├── useFileManagerProvider.ts # File manager context
|
|
│ │ │ └── useMediaGallery.ts # Media gallery state
|
|
│ │ └── components/ # Chat UI components (28 files)
|
|
│ │ ├── chat-area.vue # Main chat area (messages, input, attachments)
|
|
│ │ ├── session-sidebar.vue # Session list sidebar (search, filter, CRUD)
|
|
│ │ ├── session-info-panel.vue # Session info panel
|
|
│ │ ├── chat-header.vue # Chat top bar (status, step indicator)
|
|
│ │ ├── message-item.vue # Single message (user/assistant, markdown, blocks)
|
|
│ │ ├── session-item.vue # Session list row (avatar, title, timestamp)
|
|
│ │ ├── thinking-block.vue # Collapsible thinking/reasoning block
|
|
│ │ ├── attachment-block.vue # Attachment grid (images, audio, files)
|
|
│ │ ├── media-gallery-lightbox.vue # Fullscreen media lightbox
|
|
│ │ ├── tool-call-block.vue # Generic tool call wrapper block
|
|
│ │ ├── tool-call-generic.vue # Generic tool call (name, status, JSON I/O)
|
|
│ │ ├── tool-call-list.vue # File listing tool display
|
|
│ │ ├── tool-call-read.vue # File read tool display
|
|
│ │ ├── tool-call-write.vue # File write tool display
|
|
│ │ ├── tool-call-edit.vue # File edit tool display
|
|
│ │ ├── tool-call-exec.vue # Command execution tool display
|
|
│ │ ├── tool-call-web-search.vue # Web search tool display
|
|
│ │ ├── tool-call-web-fetch.vue # Web fetch tool display
|
|
│ │ ├── tool-call-browser.vue # Browser action tool display
|
|
│ │ ├── tool-call-memory.vue # Memory read/write tool display
|
|
│ │ ├── tool-call-message.vue # Send message tool display
|
|
│ │ ├── tool-call-email.vue # Email tool display
|
|
│ │ ├── tool-call-schedule.vue # Schedule tool display
|
|
│ │ ├── tool-call-contacts.vue # Contacts tool display
|
|
│ │ ├── tool-call-subagent.vue # Sub-agent tool display
|
|
│ │ ├── tool-call-skill.vue # Skill activation tool display
|
|
│ │ ├── schedule-trigger-block.vue # Schedule trigger display
|
|
│ │ └── heartbeat-trigger-block.vue # Heartbeat trigger display
|
|
│ ├── bots/ # Bot list + detail (tabs: overview, memory, channels, etc.)
|
|
│ │ ├── index.vue # Bot grid with create dialog
|
|
│ │ ├── detail.vue # Bot detail with tabbed interface
|
|
│ │ └── components/ # Bot sub-components (27 files)
|
|
│ │ ├── bot-overview.vue # Bot overview tab
|
|
│ │ ├── bot-settings.vue # Bot settings tab
|
|
│ │ ├── bot-channels.vue # Channel configuration tab
|
|
│ │ ├── bot-memory.vue # Memory configuration tab
|
|
│ │ ├── bot-mcp.vue # MCP connections tab
|
|
│ │ ├── bot-schedule.vue # Schedule management tab
|
|
│ │ ├── bot-heartbeat.vue # Heartbeat configuration tab
|
|
│ │ ├── bot-email.vue # Email configuration tab
|
|
│ │ ├── bot-container.vue # Container management tab
|
|
│ │ ├── bot-files.vue # File browser tab
|
|
│ │ ├── bot-terminal.vue # Terminal tab
|
|
│ │ ├── bot-skills.vue # Skills tab
|
|
│ │ ├── bot-access.vue # Access control tab
|
|
│ │ ├── bot-compaction.vue # Compaction settings tab
|
|
│ │ ├── bot-card.vue # Bot card component
|
|
│ │ ├── create-bot.vue # Create bot dialog
|
|
│ │ ├── model-select.vue # Model selection dropdown
|
|
│ │ ├── model-options.vue # Model options configuration
|
|
│ │ ├── reasoning-effort-select.vue # Reasoning effort selector
|
|
│ │ ├── reasoning-effort.ts # Reasoning effort constants
|
|
│ │ ├── search-provider-select.vue # Search provider selector
|
|
│ │ ├── memory-provider-select.vue # Memory provider selector
|
|
│ │ ├── browser-context-select.vue # Browser context selector
|
|
│ │ ├── tts-model-select.vue # TTS model selector
|
|
│ │ ├── channel-settings-panel.vue # Channel settings panel
|
|
│ │ ├── container-create-progress.vue # Container creation progress
|
|
│ │ └── weixin-qr-login.vue # WeChat QR login
|
|
│ ├── providers/ # LLM provider & model management
|
|
│ ├── web-search/ # Web search provider management
|
|
│ ├── memory/ # Memory provider management
|
|
│ ├── speech/ # TTS / speech provider & model management
|
|
│ ├── email/ # Email provider management
|
|
│ ├── browser/ # Browser context management
|
|
│ ├── supermarket/ # Supermarket (template/skill marketplace)
|
|
│ ├── usage/ # Token usage statistics
|
|
│ ├── profile/ # User profile settings (password, bind codes)
|
|
│ ├── platform/ # Platform management
|
|
│ ├── about/ # About page
|
|
│ └── oauth/ # OAuth callback pages
|
|
│ └── mcp-callback.vue # MCP OAuth callback handler
|
|
├── store/ # Pinia stores
|
|
│ ├── user.ts # User state, JWT token, login/logout
|
|
│ ├── settings.ts # UI settings (theme, language)
|
|
│ ├── capabilities.ts # Server capabilities (container backend)
|
|
│ ├── chat-selection.ts # Current bot/session selection (localStorage persisted)
|
|
│ ├── chat-list.ts # Chat messages, streaming state, SSE/WS event processing
|
|
│ └── chat-list.utils.ts # Chat list utility functions (+ chat-list.utils.test.ts)
|
|
├── stores/ # Additional stores (non-core)
|
|
│ └── supermarket-mcp-draft.ts # Supermarket MCP draft state
|
|
└── utils/ # Utility functions
|
|
├── api-error.ts # API error message extraction
|
|
├── date-time.ts # Date/time formatting
|
|
├── date-time.test.ts # Date/time tests
|
|
├── channel-type-label.ts # Channel type label utilities
|
|
├── key-value-tags.ts # Tag ↔ Record conversion
|
|
├── key-value-tags.test.ts # Tag conversion tests
|
|
├── image-ref.ts # Image reference URL resolution
|
|
├── image-ref.test.ts # Image ref tests
|
|
├── timezones.ts # Timezone list and utilities
|
|
└── useControlVisibleStatus.ts # Visibility control utility
|
|
```
|
|
|
|
## Routes
|
|
|
|
The app uses a two-section layout architecture:
|
|
|
|
### Chat Section (`/`)
|
|
|
|
| Path | Name | Component | Description |
|
|
|------|------|-----------|-------------|
|
|
| `/` | home | `home/index.vue` | Home — empty state when no bot selected |
|
|
| `/chat/:botId?/:sessionId?` | chat | `home/index.vue` | Chat interface with bot + session params |
|
|
|
|
Both routes render the same `home/index.vue` component. The `home` route shows an empty state; the `chat` route auto-selects a bot and optionally a session based on URL params. URL and store state are bidirectionally synced.
|
|
|
|
### Settings Section (`/settings`)
|
|
|
|
| Path | Name | Component | Description |
|
|
|------|------|-----------|-------------|
|
|
| `/settings/bots` | bots | `bots/index.vue` | Bot list grid |
|
|
| `/settings/bots/:botId` | bot-detail | `bots/detail.vue` | Bot detail with tabs |
|
|
| `/settings/providers` | providers | `providers/index.vue` | LLM provider & model management |
|
|
| `/settings/web-search` | web-search | `web-search/index.vue` | Web search provider management |
|
|
| `/settings/memory` | memory | `memory/index.vue` | Memory provider management |
|
|
| `/settings/speech` | speech | `speech/index.vue` | TTS / speech provider & model management |
|
|
| `/settings/email` | email | `email/index.vue` | Email provider management |
|
|
| `/settings/browser` | browser | `browser/index.vue` | Browser context management |
|
|
| `/settings/supermarket` | supermarket | `supermarket/index.vue` | Template/skill marketplace |
|
|
| `/settings/usage` | usage | `usage/index.vue` | Token usage statistics |
|
|
| `/settings/profile` | profile | `profile/index.vue` | User profile settings |
|
|
| `/settings/platform` | platform | `platform/index.vue` | Platform management |
|
|
| `/settings/about` | about | `about/index.vue` | About page |
|
|
|
|
`/settings` redirects to `/settings/bots` by default.
|
|
|
|
### Standalone Routes
|
|
|
|
| Path | Name | Component | Description |
|
|
|------|------|-----------|-------------|
|
|
| `/login` | Login | `login/index.vue` | Login form (no auth required) |
|
|
| `/oauth/mcp/callback` | oauth-mcp-callback | `oauth/mcp-callback.vue` | MCP OAuth callback (no auth required) |
|
|
|
|
### Auth Guard
|
|
|
|
- All routes except `/login` and `/oauth/*` require `localStorage.getItem('token')`.
|
|
- Logged-in users accessing `/login` are redirected to `/`.
|
|
- Chunk load errors (dynamic import failures) trigger an automatic page reload.
|
|
- Tauri integration: `afterEach` hook calls `resize_for_route` via `@tauri-apps/api/core` when running inside Tauri.
|
|
|
|
## Layout System
|
|
|
|
Two-section layout architecture, both sharing the same `MainLayout` wrapper:
|
|
|
|
1. **MainLayout** (`layout/main-layout/`) — Top-level wrapper using `SidebarProvider` from `@memohai/ui`. Provides `#sidebar` and `#main` slots.
|
|
|
|
2. **Chat Section** (`pages/main-section/`) — Uses `MainLayout` with:
|
|
- **Sidebar** (`components/sidebar/`) — Bot list sidebar (collapsible). Header shows "Bots" label + create button. Body lists all bots as `BotItem` entries. Footer has a settings gear link to `/settings`.
|
|
- **MainContainer** (`components/main-container/`) — `<KeepAlive>` wrapped `<RouterView>` for chat pages.
|
|
|
|
3. **Settings Section** (`pages/settings-section/`) — Uses `MainLayout` with:
|
|
- **SettingsSidebar** (`components/settings-sidebar/`) — Collapsible settings navigation. Top has a "back to chat" button that restores the last selected bot/session. Menu items: Bots, Providers, Web Search, Memory, Speech, Email, Browser, Supermarket, Usage, Profile, About.
|
|
- **SidebarInset** — `<KeepAlive>` wrapped `<RouterView>` for settings pages.
|
|
|
|
4. **Home/Chat Page** (`pages/home/`) — Internal layout:
|
|
- **SessionSidebar** — Left panel: session search, source filter, new session button, session list.
|
|
- **ChatArea** — Center panel: message list with scroll, input area with attachments.
|
|
- **SessionInfoPanel** — Right panel: session info display.
|
|
|
|
Several settings pages use **MasterDetailSidebarLayout** (`components/master-detail-sidebar-layout/`) for left-sidebar + detail-panel patterns (providers, web search, email, memory, speech, browser).
|
|
|
|
## CSS & Theming
|
|
|
|
Design tokens, color palette, typography, elevation strategy, and component visual specs are defined in `packages/ui/DESIGN.md`. **Read that file before making any UI changes.**
|
|
|
|
### Tailwind CSS 4
|
|
|
|
CSS-based configuration (no `tailwind.config.*` file). All design tokens (CSS variables, `@theme inline` mapping, base styles) live in `packages/ui/src/style.css`. The web app imports them via:
|
|
|
|
```css
|
|
@import "@memohai/ui/style.css";
|
|
```
|
|
|
|
### Dark Mode
|
|
|
|
- Runtime: `useColorMode` from `@vueuse/core` in `store/settings.ts`
|
|
- Storage: theme preference persisted via `useStorage`
|
|
- Toggle: Available in Settings page and login page
|
|
- Usage: semantic tokens auto-switch; no `dark:` prefix needed
|
|
|
|
### Styling Rules
|
|
|
|
- Use Tailwind utility classes; avoid `<style>` blocks.
|
|
- Always use semantic color tokens (`text-foreground`, `bg-card`, `border-border`, etc.) — never hardcode raw colors (`gray-*`, `bg-white`, `text-black`).
|
|
- Follow the design system rules in `packages/ui/DESIGN.md`.
|
|
|
|
## UI Components (@memohai/ui)
|
|
|
|
All UI primitives are provided by `@memohai/ui` (43 component groups built on Reka UI). Do not import Reka UI directly. For the component design specification (variants, colors, elevation, spacing), see `packages/ui/DESIGN.md`.
|
|
|
|
- **Exception**: Physical UI knobs (Switch thumb, Slider thumb) may keep `bg-white` as they need to contrast against colored tracks regardless of theme.
|
|
- **No scoped CSS modules**: Styling is done inline via utility classes.
|
|
|
|
### CSS Imports (main.ts)
|
|
|
|
```
|
|
style.css — Tailwind + theme tokens
|
|
animate.css — Animation utilities
|
|
markstream-vue/index.css — Markdown rendering
|
|
katex/dist/katex.min.css — Math rendering
|
|
vue-sonner/style.css — Toast notifications (in App.vue)
|
|
```
|
|
|
|
`@memohai/ui` provides 43 component groups built on Reka UI primitives + Tailwind + class-variance-authority:
|
|
|
|
- **Form**: `Form`, `FormField`, `FormFieldArray`, `FormItem`, `FormControl`, `FormLabel`, `FormMessage`, `FormDescription`
|
|
- **Input**: `Input`, `Textarea`, `InputGroup` (Addon, Button, Input, Text, Textarea), `NativeSelect`, `Combobox`, `TagsInput`, `InputOTP` (Group, Slot, Separator)
|
|
- **Selection**: `Select`, `RadioGroup`, `Checkbox`, `Switch`, `Toggle`, `Slider`
|
|
- **Layout**: `Card`, `Separator`, `Sheet`, `Sidebar` (24 sub-components), `ScrollArea`, `Collapsible`, `Item` (10 sub-components)
|
|
- **Overlays**: `Dialog` (incl. `DialogScrollContent`), `Popover`, `Tooltip`, `DropdownMenu`, `ContextMenu`, `Command` (Dialog, Group, Input, Item, List)
|
|
- **Data**: `Table` (9 sub-components), `Badge`, `BadgeCount`, `Avatar`, `Skeleton`, `Empty` (5 sub-components)
|
|
- **Navigation**: `Breadcrumb`, `Tabs`, `Pagination`, `PinInput` (Group, Slot, Separator)
|
|
- **Feedback**: `Button`, `ButtonGroup` (Separator, Text), `Spinner`, `Alert`, `Toaster` (Sonner), `Kbd`
|
|
- **Effects**: `TextGenerateEffect`
|
|
|
|
### Form Pattern (vee-validate + Zod)
|
|
|
|
```vue
|
|
<script setup>
|
|
const form = useForm({
|
|
validationSchema: toTypedSchema(z.object({
|
|
name: z.string().min(1),
|
|
})),
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<FormField v-slot="{ componentField }" name="name">
|
|
<FormItem>
|
|
<Label>Name</Label>
|
|
<FormControl>
|
|
<Input v-bind="componentField" />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
</FormField>
|
|
</template>
|
|
```
|
|
|
|
### Icon Usage
|
|
|
|
- **Lucide** (primary): Direct component imports from `lucide-vue-next`. Example: `import { Plus, Search, Bot } from 'lucide-vue-next'` → `<Plus class="size-4" />`. Used for all UI icons (actions, navigation, status indicators, etc.).
|
|
- **`@memohai/icon`** (brand icons): Workspace package (`packages/icons/`) providing AI provider, search engine, and channel platform SVG icons as Vue components. Example: `import { Openai, Claude } from '@memohai/icon'`.
|
|
- **Do NOT use FontAwesome** for new code. Legacy FontAwesome usage remains only in commented-out code blocks. Always use Lucide for UI icons and `@memohai/icon` for brand logos.
|
|
|
|
### Notification Pattern
|
|
|
|
```typescript
|
|
import { toast } from 'vue-sonner'
|
|
toast.success(t('common.saved'))
|
|
toast.error(resolveApiErrorMessage(error, 'Failed'))
|
|
```
|
|
|
|
## Data Fetching
|
|
|
|
### API Client Setup (`lib/api-client.ts`)
|
|
|
|
- SDK: `@memohai/sdk` auto-generated from OpenAPI via `@hey-api/openapi-ts`
|
|
- Base URL: `VITE_API_URL` env var (defaults to `/api`, proxied by Vite dev server to backend)
|
|
- Auth: Request interceptor attaches `Authorization: Bearer ${token}` from localStorage
|
|
- 401 handling: Response interceptor removes token and redirects to `/login`
|
|
|
|
### Pinia Colada (Server State)
|
|
|
|
Primary data fetching mechanism for CRUD operations:
|
|
|
|
```typescript
|
|
// Query — auto-generated from SDK
|
|
const { data, isLoading } = useQuery(getBotsQuery())
|
|
|
|
// Custom query with dynamic key
|
|
const { data } = useQuery({
|
|
key: () => ['bot-settings', botId.value],
|
|
query: async () => {
|
|
const { data } = await getBotsByBotIdSettings({
|
|
path: { bot_id: botId.value },
|
|
throwOnError: true,
|
|
})
|
|
return data
|
|
},
|
|
enabled: () => !!botId.value,
|
|
})
|
|
|
|
// Mutation with cache invalidation
|
|
const queryCache = useQueryCache()
|
|
const { mutateAsync } = useMutation({
|
|
mutation: async (body) => {
|
|
const { data } = await putBotsByBotIdSettings({
|
|
path: { bot_id: botId.value },
|
|
body,
|
|
throwOnError: true,
|
|
})
|
|
return data
|
|
},
|
|
onSettled: () => queryCache.invalidateQueries({
|
|
key: ['bot-settings', botId.value],
|
|
}),
|
|
})
|
|
```
|
|
|
|
SDK also generates colada helpers: `getBotsQuery()`, `postBotsMutation()`, query key factories.
|
|
|
|
### Pinia Stores (Client State)
|
|
|
|
| Store | ID | Purpose |
|
|
|-------|----|---------|
|
|
| `user` | `user` | JWT token (`useLocalStorage`), user info (id, username, role, displayName, avatarUrl, timezone), login/logout |
|
|
| `settings` | `settings` | Theme (dark/light), language (en/zh), synced with `useColorMode` and vue-i18n locale |
|
|
| `capabilities` | `capabilities` | Server feature flags (container backend, snapshot support), loaded once from `getPing()` |
|
|
| `chat-selection` | `chat-selection` | Current bot ID and session ID, persisted via `useStorage` to localStorage |
|
|
| `chat-list` | `chat` | Chat messages, sessions, bots, streaming state, SSE/WS event processing. Depends on `chat-selection` store for current bot/session. Utility functions in `chat-list.utils.ts` |
|
|
|
|
Additional stores in `stores/`:
|
|
| Store | Purpose |
|
|
|-------|---------|
|
|
| `supermarket-mcp-draft` | Supermarket MCP draft state management |
|
|
|
|
Stores use Composition API style (`defineStore(() => { ... })`), with persistence via `pinia-plugin-persistedstate` or `useStorage`.
|
|
|
|
### Streaming (Chat)
|
|
|
|
Chat supports two transport modes: **Server-Sent Events (SSE)** and **WebSocket**.
|
|
|
|
#### SSE Streaming
|
|
- **Endpoints**: `/bots/{bot_id}/local/stream` (send + stream), `/messages/events` (real-time message updates)
|
|
- **Parsing**: `composables/api/useChat.sse.ts` reads `ReadableStream<Uint8Array>` and parses SSE `data:` lines
|
|
- **Events**: `text_delta`, `reasoning_delta`, `tool_call_start/end`, `attachment_delta`, `processing_completed/failed`
|
|
- **Retry**: `useRetryingStream` composable provides exponential backoff for reconnection
|
|
|
|
#### WebSocket
|
|
- **Endpoint**: `/bots/{bot_id}/local/ws` (with token query param)
|
|
- **Implementation**: `composables/api/useChat.ws.ts` wraps native `WebSocket` with send, abort, close, and auto-reconnect
|
|
- **State**: `store/chat-list.ts` processes streaming events from either transport into reactive message blocks in real-time
|
|
- **Abort**: Stream cancellation via `AbortSignal` (SSE) or close message (WS)
|
|
|
|
### Error Handling
|
|
|
|
- **Global**: `utils/api-error.ts` — `resolveApiErrorMessage()` extracts error from `message`, `error`, `detail` fields
|
|
- **Mutations**: `useDialogMutation` composable wraps mutations with automatic `toast.error()` on failure
|
|
- **SDK**: All calls use `throwOnError: true`; try/catch at component level
|
|
- **Streams**: `processing_failed` / `error` events appended to message blocks
|
|
|
|
## i18n
|
|
|
|
- Plugin: vue-i18n (Composition API, `legacy: false`)
|
|
- Locales: `en` (English, default), `zh` (Chinese)
|
|
- Files: `src/i18n/locales/en.json`, `src/i18n/locales/zh.json`
|
|
- Usage: `const { t } = useI18n()` → `t('bots.title')`
|
|
- Key namespaces: `common`, `auth`, `sidebar`, `breadcrumb`, `settings`, `about`, `chat`, `models`, `provider`, `webSearch`, `memory`, `speech`, `email`, `browser`, `mcp`, `home`, `bots`, `usage`, `supermarket`
|
|
|
|
## Vite Configuration
|
|
|
|
- Dev server port: 8082 (from `config.toml`)
|
|
- Proxy: `/api` → backend (default `http://localhost:8080`)
|
|
- Aliases: `@` → `./src`, `#` → `../ui/src`
|
|
- Config: reads from `MEMOH_CONFIG_PATH` / `CONFIG_PATH` when provided, otherwise `../../config.toml`, via `@memohai/config`
|
|
|
|
## Development Rules
|
|
|
|
- Use Vue 3 Composition API with `<script setup>` exclusively.
|
|
- Style with Tailwind utility classes; avoid `<style>` blocks. Follow the design system in `packages/ui/DESIGN.md`.
|
|
- **Always use semantic color tokens** (`text-foreground`, `bg-card`, `border-border`, `text-muted-foreground`, `bg-accent`, etc.) instead of raw colors (`gray-*`, `bg-white`, `text-black`). Never introduce hardcoded Tailwind color classes for themed elements — this breaks dark mode consistency.
|
|
- Use `@memohai/ui` components for all UI primitives; do not import Reka UI directly.
|
|
- Use `lucide-vue-next` for all UI icons. Use `@memohai/icon` for brand/provider logos. **Never use FontAwesome** — do not add `<FontAwesomeIcon>`, do not import from `@fortawesome/*`, do not use inline SVG or base64-encoded SVG in templates.
|
|
- Use Pinia Colada (`useQuery`/`useMutation`) for server state; use Pinia stores for client state only.
|
|
- API calls must go through `@memohai/sdk`; never call `fetch()` directly.
|
|
- All user-facing strings must use i18n keys (`t('key')`) — never hardcode text.
|
|
- Forms must use vee-validate + Zod schemas via `toTypedSchema()`.
|
|
- Error messages via `resolveApiErrorMessage()` + `toast.error()`.
|
|
- Page components go in `pages/{feature}/`; page-specific sub-components go in `pages/{feature}/components/`.
|
|
- Page-specific composables go in `pages/{feature}/composables/`.
|
|
- Shared components go in `components/`.
|
|
- Composables go in `composables/`; API-specific composables in `composables/api/`.
|