diff --git a/docs.json b/docs.json index 757829608..7e8a46c2d 100644 --- a/docs.json +++ b/docs.json @@ -321,7 +321,8 @@ "en/plugin/dev/components/command", "en/plugin/dev/components/tool", "en/plugin/dev/components/knowledge-engine", - "en/plugin/dev/components/parser" + "en/plugin/dev/components/parser", + "en/plugin/dev/components/page" ] }, "en/plugin/dev/style", @@ -528,7 +529,8 @@ "zh/plugin/dev/components/command", "zh/plugin/dev/components/tool", "zh/plugin/dev/components/knowledge-engine", - "zh/plugin/dev/components/parser" + "zh/plugin/dev/components/parser", + "zh/plugin/dev/components/page" ] }, "zh/plugin/dev/style", diff --git a/en/plugin/dev/components/add.mdx b/en/plugin/dev/components/add.mdx index 3325c390e..4bc82626a 100644 --- a/en/plugin/dev/components/add.mdx +++ b/en/plugin/dev/components/add.mdx @@ -9,6 +9,7 @@ Plugins consist of one or more components that provide different functionalities - Tools (Tool): Called by LLMs during execution of LangBot's built-in Local Agent. - Knowledge Engines (KnowledgeEngine): Provide knowledge base indexing and retrieval capabilities for LangBot, managing document ingestion, retrieval, and deletion. - Parsers (Parser): Parse binary files such as PDF, Word, etc. into structured text before document ingestion, for use by Knowledge Engines. +- Pages (Page): Register custom visual pages in the LangBot WebUI sidebar, providing admin panels, data displays, and interactive interfaces. ## Adding Components @@ -74,3 +75,4 @@ For detailed usage of each component, please refer to: - [Tool](./tool) - [KnowledgeEngine](./knowledge-engine) - [Parser](./parser) +- [Page](./page) diff --git a/en/plugin/dev/components/page.mdx b/en/plugin/dev/components/page.mdx new file mode 100644 index 000000000..13f6efbf8 --- /dev/null +++ b/en/plugin/dev/components/page.mdx @@ -0,0 +1,245 @@ +--- +title: "Component: Page" +--- + +The Page component allows plugins to register custom visual pages in the LangBot WebUI sidebar. Pages run inside an iframe sandbox, communicate with the host via the Page SDK, and can call the plugin's backend API. + +## Use Cases + +- **Admin panels**: Provide visual configuration and data management interfaces (e.g., FAQ management, analytics dashboards) +- **Data displays**: Show runtime statistics, logs, charts, etc. +- **Interactive tools**: Provide forms, editors, and other interactive interfaces + +## Adding a Page Component + +Run the following command in the plugin directory: + +```bash +lbp comp Page +``` + +After entering the page name, the CLI will generate page files under `components/pages/`: + +```bash +├── components +│ └── pages +│ └── dashboard +│ ├── dashboard.yaml # Page manifest +│ ├── dashboard.py # Backend handler +│ ├── index.html # Page entry point +│ └── i18n # Translation files (optional) +│ ├── en_US.json +│ └── zh_Hans.json +``` + +The plugin's `manifest.yaml` will also be updated with the component discovery config: + +```yaml +spec: + components: + Page: + fromDirs: + - path: components/pages/ + maxDepth: 2 +``` + +## Manifest File: Page + +```yaml +apiVersion: v1 # Do not modify +kind: Page # Do not modify +metadata: + name: dashboard # Unique page ID within the plugin + label: + en_US: Dashboard # Display name shown in the WebUI sidebar, supports i18n + zh_Hans: 仪表盘 +spec: + path: index.html # HTML entry file, relative to this YAML file's directory +execution: + python: + path: dashboard.py # Backend handler file + attr: DashboardPage # Handler class name +``` + +## Backend Handler + +The Page component's backend handler extends the `Page` base class and implements `handle_api` to process API requests from the frontend page. + +```python +from langbot_plugin.api.definition.components.page import Page, PageRequest, PageResponse + + +class DashboardPage(Page): + + async def handle_api(self, request: PageRequest) -> PageResponse: + # request.endpoint: API endpoint path, e.g. '/stats' + # request.method: HTTP method (GET, POST, PUT, DELETE) + # request.body: Request body (parsed JSON, or None) + + if request.endpoint == '/stats' and request.method == 'GET': + # Access shared plugin state via self.plugin + return PageResponse.ok({ + 'total': len(self.plugin.entries), + }) + + return PageResponse.fail(f'Unknown endpoint: {request.endpoint}') +``` + +### PageRequest Fields + +| Field | Type | Description | +|-------|------|-------------| +| `endpoint` | `str` | API endpoint path (e.g. `'/entries'`) | +| `method` | `str` | HTTP method (`GET`, `POST`, `PUT`, `DELETE`) | +| `body` | `Any` | Request body (parsed JSON, or `None`) | + +### PageResponse Construction + +| Method | Description | +|--------|-------------| +| `PageResponse.ok(data)` | Success response. `data` can be any JSON-serializable value | +| `PageResponse.fail(error)` | Error response. `error` is a human-readable error string | + +## Frontend Page Development + +Include the Page SDK in your HTML file to communicate with the plugin backend: + +```html + + + + + + + +

Dashboard

+

+ + + + + +``` + +### Page SDK API + +| Method | Description | +|--------|-------------| +| `langbot.onReady(callback)` | Fires when SDK is ready. `callback` receives `ctx` with `theme` and `language` | +| `langbot.api(endpoint, body?, method?)` | Calls the plugin's `handle_api`. Returns a Promise | +| `langbot.t(key, fallback?)` | Gets a translated string | +| `langbot.onThemeChange(callback)` | Fires when the theme changes | +| `langbot.onLanguageChange(callback)` | Fires when the language changes | +| `langbot.applyI18n()` | Manually re-apply `data-i18n` translations | + +### Dark Mode + +The SDK automatically sets CSS custom properties on the page. Use them directly: + +| CSS Variable | Purpose | +|-------------|---------| +| `--langbot-bg` | Page background | +| `--langbot-bg-card` | Card background | +| `--langbot-text` | Primary text color | +| `--langbot-text-muted` | Secondary text color | +| `--langbot-border` | Border color | +| `--langbot-accent` | Accent color | + +### Page i18n + +Create an `i18n/` directory inside your page directory with JSON translation files: + +``` +pages/dashboard/ +├── index.html +└── i18n/ + ├── en_US.json + └── zh_Hans.json +``` + +Translation files are flat JSON key-value pairs: + +```json +{ + "title": "Dashboard", + "totalEntries": "Total Entries" +} +``` + +Add the `data-i18n` attribute to HTML elements for automatic translation: + +```html +

Dashboard

+``` + +## Full Example: FAQ Manager + +Here is a complete Page component example that implements CRUD operations for FAQ entries. + +**Backend handler** (`components/pages/manager/manager.py`): + +```python +from langbot_plugin.api.definition.components.page import Page, PageRequest, PageResponse + + +class ManagerPage(Page): + + async def handle_api(self, request: PageRequest) -> PageResponse: + plugin = self.plugin + + if request.endpoint == '/entries' and request.method == 'GET': + return PageResponse.ok({'entries': plugin.entries}) + + if request.endpoint == '/entries' and request.method == 'POST': + question = (request.body or {}).get('question', '').strip() + answer = (request.body or {}).get('answer', '').strip() + if not question or not answer: + return PageResponse.fail('question and answer are required') + entry = plugin.add_entry(question, answer) + await plugin.persist() + return PageResponse.ok({'entry': entry}) + + if request.endpoint == '/entries' and request.method == 'DELETE': + entry_id = (request.body or {}).get('id', '') + if plugin.delete_entry(entry_id): + await plugin.persist() + return PageResponse.ok({'deleted': entry_id}) + return PageResponse.fail('entry not found') + + return PageResponse.fail(f'Unknown: {request.method} {request.endpoint}') +``` + +**Frontend page** (`components/pages/manager/index.html`) calls the backend via `langbot.api()`: + +```javascript +// Load entries +var data = await langbot.api('/entries', null, 'GET'); + +// Add entry +await langbot.api('/entries', { question: '...', answer: '...' }, 'POST'); + +// Delete entry +await langbot.api('/entries', { id: '...' }, 'DELETE'); +``` + +Full example code is available at [FAQManager plugin](https://github.com/langbot-app/langbot-plugin-demo/tree/main/FAQManager). + +## Notes + +- Pages run inside a `sandbox="allow-scripts allow-forms"` iframe and cannot open popups or navigate the parent page +- Access shared plugin state via `self.plugin` — Page and Tool components can share data +- Use `PageResponse.ok()` and `PageResponse.fail()` to construct responses for consistent formatting +- The Page SDK ` + + + +``` + +### Page SDK API + +| 方法 | 说明 | +|------|------| +| `langbot.onReady(callback)` | SDK 就绪后触发回调,`callback` 接收 `ctx` 参数(包含 `theme` 和 `language`) | +| `langbot.api(endpoint, body?, method?)` | 调用插件后端 `handle_api`,返回 Promise | +| `langbot.t(key, fallback?)` | 获取翻译字符串 | +| `langbot.onThemeChange(callback)` | 主题变更回调 | +| `langbot.onLanguageChange(callback)` | 语言变更回调 | +| `langbot.applyI18n()` | 手动重新应用 `data-i18n` 翻译 | + +### 暗色模式 + +SDK 会自动在页面上设置 CSS 自定义属性,直接使用即可: + +| CSS 变量 | 用途 | +|------|------| +| `--langbot-bg` | 页面背景色 | +| `--langbot-bg-card` | 卡片背景色 | +| `--langbot-text` | 主文字色 | +| `--langbot-text-muted` | 次要文字色 | +| `--langbot-border` | 边框色 | +| `--langbot-accent` | 强调色 | + +### 页面 i18n + +在页面目录下创建 `i18n/` 目录,放入 JSON 翻译文件: + +``` +pages/dashboard/ +├── index.html +└── i18n/ + ├── en_US.json + └── zh_Hans.json +``` + +翻译文件格式为扁平 JSON 键值对: + +```json +{ + "title": "仪表盘", + "totalEntries": "条目总数" +} +``` + +在 HTML 元素上添加 `data-i18n` 属性,SDK 会自动替换文本: + +```html +

Dashboard

+``` + +## 完整示例:FAQ 管理器 + +以下是一个完整的 Page 组件示例,实现了 FAQ 条目的增删改查。 + +**后端处理程序**(`components/pages/manager/manager.py`): + +```python +from langbot_plugin.api.definition.components.page import Page, PageRequest, PageResponse + + +class ManagerPage(Page): + + async def handle_api(self, request: PageRequest) -> PageResponse: + plugin = self.plugin + + if request.endpoint == '/entries' and request.method == 'GET': + return PageResponse.ok({'entries': plugin.entries}) + + if request.endpoint == '/entries' and request.method == 'POST': + question = (request.body or {}).get('question', '').strip() + answer = (request.body or {}).get('answer', '').strip() + if not question or not answer: + return PageResponse.fail('question and answer are required') + entry = plugin.add_entry(question, answer) + await plugin.persist() + return PageResponse.ok({'entry': entry}) + + if request.endpoint == '/entries' and request.method == 'DELETE': + entry_id = (request.body or {}).get('id', '') + if plugin.delete_entry(entry_id): + await plugin.persist() + return PageResponse.ok({'deleted': entry_id}) + return PageResponse.fail('entry not found') + + return PageResponse.fail(f'Unknown: {request.method} {request.endpoint}') +``` + +**前端页面**(`components/pages/manager/index.html`)中通过 `langbot.api()` 调用后端: + +```javascript +// 加载条目 +var data = await langbot.api('/entries', null, 'GET'); + +// 添加条目 +await langbot.api('/entries', { question: '...', answer: '...' }, 'POST'); + +// 删除条目 +await langbot.api('/entries', { id: '...' }, 'DELETE'); +``` + +完整示例代码请参考 [FAQManager 插件](https://github.com/langbot-app/langbot-plugin-demo/tree/main/FAQManager)。 + +## 注意事项 + +- 页面运行在 `sandbox="allow-scripts allow-forms"` 的 iframe 中,不能弹窗或导航父页面 +- 通过 `self.plugin` 访问插件实例的共享状态,Page 和 Tool 组件可以共享数据 +- 使用 `PageResponse.ok()` 和 `PageResponse.fail()` 构造响应,确保格式一致 +- Page SDK 的 `