diff --git a/agent/README.md b/agent/README.md index 9f4abf38..74018c48 100644 --- a/agent/README.md +++ b/agent/README.md @@ -184,6 +184,7 @@ ctx.Send({ - [Models](docs/models.md) - Assistant-scoped data models - [Search](docs/search.md) - Web, knowledge base, and database search - [Pages](docs/pages.md) - Web UI for agents (SUI framework) +- [Iframe Integration](docs/iframe.md) - Iframe communication with CUI - [Internationalization](docs/i18n.md) - Multi-language support - [Testing](docs/testing.md) - Agent testing framework diff --git a/agent/docs/iframe.md b/agent/docs/iframe.md new file mode 100644 index 00000000..b8b74eba --- /dev/null +++ b/agent/docs/iframe.md @@ -0,0 +1,300 @@ +# Iframe Integration + +Agent Pages can be embedded in CUI via `/web/` routes. This document covers the iframe communication mechanism between embedded pages and the CUI host. + +## Route Mapping + +Pages are accessible via: + +``` +/web// +``` + +Example: + +| Page File | URL | +| -------------------------- | --------------------------------- | +| `pages/index/index.html` | `/web/my-assistant/index` | +| `pages/result/index.html` | `/web/my-assistant/result` | +| `pages/report/detail.html` | `/web/my-assistant/report/detail` | + +## URL Parameters + +CUI automatically injects context via URL parameters: + +| Parameter | Value | Description | +| ---------- | ---------------------- | ------------- | +| `__theme` | `light` / `dark` | Current theme | +| `__locale` | `en-us`, `zh-cn`, etc. | User locale | + +> **Note**: Authentication uses secure HTTP-only cookies, so `__token` parameter is not needed. + +**Usage in page URL:** + +``` +/web/my-assistant/result?theme=__theme&locale=__locale +``` + +CUI replaces `__theme`, `__locale` with actual values before loading. + +## Message Communication + +### Receiving Setup Message + +When the iframe loads, CUI sends a `setup` message: + +```typescript +// In your page script +window.addEventListener("message", (e) => { + if (e.data.type === "setup") { + const { theme, locale, token } = e.data.message; + // Apply theme, store token, set locale + document.documentElement.setAttribute("data-theme", theme); + } +}); +``` + +### Sending Actions to CUI + +Pages can trigger CUI actions via `postMessage` using the unified Action system: + +```typescript +// Send action to parent CUI +window.parent.postMessage( + { + type: "action", + message: { + name: "notify.success", + payload: { message: "Operation completed" }, + }, + }, + window.location.origin +); +``` + +### Action Types + +#### Navigate + +| Action | Description | Payload | +| --------------- | ------------------------------- | ------------------------------------------- | +| `navigate` | Open page in sidebar or new tab | `{ route, title?, icon?, query?, target? }` | +| `navigate.back` | Navigate back in history | - | + +**Navigate Payload:** + +| Field | Type | Required | Description | +| -------- | ------------------------ | -------- | ----------------------------------------------- | +| `route` | `string` | ✅ | Target route (`$dashboard/xxx`, `/xxx`, or URL) | +| `title` | `string` | - | Page title (shows title bar with back button) | +| `icon` | `string` | - | Tab icon (e.g., `material-folder`) | +| `query` | `Record` | - | Query parameters | +| `target` | `'_self'` \| `'_blank'` | - | `_self` (sidebar) or `_blank` (new window) | + +#### Notify + +| Action | Description | Payload | +| ---------------- | ------------------------- | ------------------------------------------ | +| `notify.success` | Show success notification | `{ message, duration?, icon?, closable? }` | +| `notify.error` | Show error notification | `{ message, duration?, icon?, closable? }` | +| `notify.warning` | Show warning notification | `{ message, duration?, icon?, closable? }` | +| `notify.info` | Show info notification | `{ message, duration?, icon?, closable? }` | + +#### App + +| Action | Description | +| ----------------- | ------------------------ | +| `app.menu.reload` | Refresh application menu | + +#### Modal + +| Action | Description | +| ------------- | ----------------- | +| `modal.open` | Open modal dialog | +| `modal.close` | Close modal | + +#### Table + +| Action | Description | +| --------------- | -------------------- | +| `table.search` | Trigger table search | +| `table.refresh` | Refresh table data | +| `table.save` | Save table row | +| `table.delete` | Delete table row(s) | + +#### Form + +| Action | Description | +| ----------------- | --------------------- | +| `form.find` | Load form data by ID | +| `form.submit` | Submit form | +| `form.reset` | Reset form | +| `form.setFields` | Set form field values | +| `form.fullscreen` | Toggle fullscreen | + +#### MCP (Client-side) + +| Action | Description | +| ------------------- | ------------------ | +| `mcp.tool.call` | Execute MCP tool | +| `mcp.resource.read` | Read MCP resource | +| `mcp.resource.list` | List MCP resources | +| `mcp.prompt.get` | Get MCP prompt | +| `mcp.prompt.list` | List MCP prompts | + +#### Event + +| Action | Description | +| ------------ | ----------------- | +| `event.emit` | Emit custom event | + +#### Confirm + +| Action | Description | +| --------- | ------------------------ | +| `confirm` | Show confirmation dialog | + +### Receiving Events from CUI + +CUI can send messages to iframe via `web/sendMessage` event: + +```typescript +// In your page script +window.addEventListener("message", (e) => { + const { type, message } = e.data; + + switch (type) { + case "setup": + // Initial setup with theme, locale, token + break; + case "refresh": + // CUI requests page refresh + location.reload(); + break; + case "data": + // CUI sends data update + handleDataUpdate(message); + break; + } +}); +``` + +## Complete Example + +### Page HTML (pages/result/index.html) + +```html + + + + Result Page + + + +
+ + +``` + +### Page Script (pages/result/result.ts) + +```typescript +import { $Backend, Component, EventData } from "@yao/sui"; + +const self = this as Component; + +// Helper: Send action to CUI parent +const sendAction = (name: string, payload?: any) => { + try { + window.parent.postMessage( + { type: "action", message: { name, payload } }, + window.location.origin + ); + } catch (err) { + console.error("Failed to send action to parent:", err); + } +}; + +// Initialize message listener +function init() { + window.addEventListener("message", (e) => { + if (e.origin !== window.location.origin) return; + + const { type, message } = e.data; + switch (type) { + case "setup": + // Apply theme, locale from CUI + document.documentElement.setAttribute("data-theme", message.theme); + break; + case "update": + // Handle data updates from CUI + console.log("Received update:", message); + break; + } + }); + + // Make helper available globally + (window as any).sendAction = sendAction; +} + +init(); + +// Event handler: Show success notification +self.HandleSuccess = (event: Event, data: EventData) => { + sendAction("notify.success", { message: data.message || "Success!" }); +}; + +// Event handler: Navigate to page +self.HandleNavigate = (event: Event, data: EventData) => { + sendAction("navigate", { + route: data.path, + title: data.title, + }); +}; + +// Event handler: Close sidebar +self.HandleClose = () => { + sendAction("event.emit", { key: "app/closeSidebar", value: {} }); +}; + +// Event handler: Call backend and display result +self.HandleQuery = async (event: Event, data: EventData) => { + try { + const result = await $Backend().Call("Query", data.id); + console.log(result); + } catch (error: any) { + sendAction("notify.error", { message: error.message }); + } +}; +``` + +## Triggering from Hooks + +Open page in sidebar from agent hooks: + +```typescript +function Next(ctx: agent.Context, payload: agent.Payload): agent.Next { + // Open result page in sidebar + ctx.Send({ + type: "action", + props: { + name: "navigate", + payload: { + route: `/agents/my-assistant/result`, + title: "Results", + query: { id: resultId }, + }, + }, + }); + + return null; +} +``` + +See [Pages](pages.md) for more details on triggering pages from hooks. + +## Security Notes + +1. **Same-origin only**: Messages are only processed from same-origin iframes +2. **Secure cookies**: Authentication uses HTTP-only cookies, no token in URL +3. **Validate messages**: Always validate message structure before processing diff --git a/agent/docs/pages.md b/agent/docs/pages.md index a0ffa475..c91e6ac6 100644 --- a/agent/docs/pages.md +++ b/agent/docs/pages.md @@ -148,16 +148,65 @@ function ApiGetData(request: Request): any { **`/assistants/my-assistant/pages/index/index.ts`**: -```typescript -function index(component: HTMLElement) { - this.root = component; - this.store = new __sui_store(component); +Frontend scripts can be written in two styles: - this.handleClick = async (event: Event) => { - const data = await this.backend.ApiGetData({ id: 1 }); - console.log(data); - }; -} +**Style 1: Direct Code (Simple Pages)** + +```typescript +// Runs immediately when script loads +document.addEventListener("DOMContentLoaded", () => { + const form = document.querySelector("#myForm") as HTMLFormElement; + + form.addEventListener("submit", async (e) => { + e.preventDefault(); + // Handle form submission + }); +}); + +// Smooth scrolling for navigation +document.querySelectorAll('a[href^="#"]').forEach((anchor) => { + anchor.addEventListener("click", function (e) { + e.preventDefault(); + const target = document.querySelector(this.getAttribute("href")); + target?.scrollIntoView({ behavior: "smooth" }); + }); +}); +``` + +**Style 2: Component Pattern (Interactive Pages)** + +```typescript +import { $Backend, Component, EventData } from "@yao/sui"; + +const self = this as Component; + +// Event handler bound to s:on-click="HandleClick" +self.HandleClick = async (event: Event, data: EventData) => { + const result = await $Backend().Call("ApiGetData", data.id); + console.log(result); +}; + +// Form submission handler +self.HandleSubmit = async (event: Event) => { + event.preventDefault(); + const form = event.target as HTMLFormElement; + const formData = new FormData(form); + await $Backend().Call("ApiSubmit", Object.fromEntries(formData)); +}; +``` + +**Using Backend API:** + +```typescript +import { $Backend, Yao } from "@yao/sui"; + +// Call backend method +const data = await $Backend().Call("ApiMethodName", arg1, arg2); + +// Direct API calls +const yao = new Yao(); +const res = await yao.Get("/api/endpoint", { param: "value" }); +await yao.Post("/api/endpoint", { data: "value" }); ``` ### 6. Build and Run @@ -495,23 +544,103 @@ ctx.Send({ ## Frontend API -The SUI frontend SDK provides: +### Backend Calls ```typescript -// Backend calls -const data = await this.backend.ApiMethodName(payload); +import { $Backend, Yao } from "@yao/sui"; -// State management -this.store.Set("key", value); -const value = this.store.Get("key"); +// Call backend method defined in .backend.ts +const data = await $Backend().Call("ApiMethodName", arg1, arg2); -// OpenAPI client (if using oauth guard) -const response = await OpenAPI.Get("/api/endpoint"); -await OpenAPI.Post("/api/endpoint", data); +// Direct API calls +const yao = new Yao(); +const res = await yao.Get("/api/endpoint", { query: "value" }); +await yao.Post("/api/endpoint", { body: "data" }); ``` +### State Management + +```typescript +import { Component } from "@yao/sui"; + +const self = this as Component; + +// Store values (per component instance) +self.store.Set("key", value); +const value = self.store.Get("key"); +``` + +### Parent Communication (Iframe) + +```typescript +// Helper: Send action to CUI parent +const sendAction = (name: string, payload?: any) => { + window.parent.postMessage( + { type: "action", message: { name, payload } }, + window.location.origin + ); +}; + +// Usage +sendAction("notify.success", { message: "Done!" }); +sendAction("navigate", { + route: "/agents/my-assistant/detail", + title: "Details", +}); + +// Receive messages from parent +window.addEventListener("message", (e) => { + if (e.origin !== window.location.origin) return; + const { type, message } = e.data; + if (type === "setup") { + document.documentElement.setAttribute("data-theme", message.theme); + } +}); +``` + +## Iframe Communication + +When pages are embedded in CUI via `/web//`, they can communicate with the host: + +### Receiving Context + +```javascript +window.addEventListener("message", (e) => { + if (e.origin !== window.location.origin) return; + if (e.data.type === "setup") { + const { theme, locale } = e.data.message; + // Apply theme, set locale + document.documentElement.setAttribute("data-theme", theme); + } +}); +``` + +### Sending Actions + +```javascript +// Helper function +const sendAction = (name, payload) => { + window.parent.postMessage( + { type: "action", message: { name, payload } }, + window.location.origin + ); +}; + +// Show notification +sendAction("notify.success", { message: "Done!" }); + +// Navigate to page +sendAction("navigate", { + route: "/agents/my-assistant/detail", + title: "Details", +}); +``` + +See [Iframe Integration](iframe.md) for complete documentation. + ## Related Documentation +- [Iframe Integration](iframe.md) - CUI iframe communication - [SUI Template Syntax](../../sui/docs/template-syntax.md) - [SUI Data Binding](../../sui/docs/data-binding.md) - [SUI Components](../../sui/docs/components.md)