# 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 |
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`, `settings`, `chat`, `models`, `provider`, `searchProvider`, `emailProvider`, `mcp`, `bots`, `home`
## 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 `