# 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, ` ``` ### Icon Usage - **Lucide** (primary): Direct component imports from `lucide-vue-next`. Example: `import { Plus, Search, Bot } from 'lucide-vue-next'` → ``. 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` 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 `