yao/agent/docs/pages.md
Max ebf3dcc870 Refactor Documentation for Backend API Method Naming and Iframe Communication
- Updated iframe documentation to remove token handling from the setup message, clarifying the focus on theme and locale.
- Added a new section in MCP documentation detailing HTTP transport configuration for API access, enhancing clarity on authorization.
- Revised frontend API documentation to standardize method naming by removing the 'Api' prefix in examples, improving consistency across backend calls.
- Enhanced event handling documentation by updating method names in examples, ensuring alignment with the new naming conventions.
2026-01-03 11:42:23 +08:00

16 KiB

Agent Pages

Agent Pages provide a built-in SUI (Simple User Interface) framework for building web interfaces for AI agents. Pages are automatically loaded from the /agent/template/ directory for global templates and /assistants/<name>/pages/ for individual assistant pages.

Directory Structure

<app>/
├── agent/
│   └── template/              # Global template directory
│       ├── __document.html    # Document template
│       ├── __data.json        # Global data
│       ├── __assets/          # Global assets
│       │   ├── css/
│       │   ├── js/
│       │   └── images/
│       ├── pages/             # Global pages (login, error, etc.)
│       │   └── login/
│       │       └── login.html
│       └── __locales/         # Internationalization
│
└── assistants/
    └── my-assistant/
        ├── package.yao
        └── pages/             # Assistant-specific pages
            ├── index/
            │   ├── index.html
            │   ├── index.css
            │   ├── index.ts
            │   └── index.backend.ts
            └── __assets/      # Optional assistant assets

Route Mapping

File Path Public URL
/agent/template/pages/login/login.html /agents/login
/assistants/demo/pages/index/index.html /agents/demo/index
/assistants/demo/pages/chat/chat.html /agents/demo/chat

Quick Start

1. Create Document Template

/agent/template/__document.html:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>{{ $global.title }}</title>
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="icon" href="/agents/assets/images/favicon.png" />
  </head>
  <body>
    <div class="container">{{ __page }}</div>
  </body>
</html>

2. Create Global Data

/agent/template/__data.json:

{
  "title": "AI Agent",
  "version": "1.0.0"
}

3. Create a Page

/assistants/my-assistant/pages/index/index.html:

<div id="chat-page" class="page">
  <h1>{{ title }}</h1>
  <div class="messages" s:for="{{ messages }}" s:for-item="msg">
    <div class="message {{ msg.role }}">{{ msg.content }}</div>
  </div>
  <input
    type="text"
    s:on-keypress="handleInput"
    placeholder="Type a message..."
  />
</div>

/assistants/my-assistant/pages/index/index.json:

{
  "title": "Chat",
  "messages": []
}

/assistants/my-assistant/pages/index/index.css:

.page {
  max-width: 800px;
  margin: 0 auto;
  padding: 24px;
}

.messages {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.message.user {
  align-self: flex-end;
  background: #007bff;
  color: white;
}

.message.assistant {
  align-self: flex-start;
  background: #f0f0f0;
}

4. Add Backend Script

/assistants/my-assistant/pages/index/index.backend.ts:

function BeforeRender(request: Request): Record<string, any> {
  const chatId = request.query.chat_id;
  return {
    messages: chatId ? Process("scripts.chat.GetHistory", chatId) : [],
    user: request.authorized?.user_id,
  };
}

function ApiGetData(request: Request): any {
  const { id } = request.payload;
  return Process("models.data.Find", id, {});
}

5. Add Frontend Script

/assistants/my-assistant/pages/index/index.ts:

Frontend scripts can be written in two styles:

Style 1: Direct Code (Simple Pages)

// 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)

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("GetData", 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("Submit", Object.fromEntries(formData));
};

Using Backend API:

import { $Backend, Yao } from "@yao/sui";

// Call backend method
const data = await $Backend().Call("MethodName", 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

# Build pages
yao sui build agent

# Or watch for changes
yao sui watch agent

# Start server
yao start

Access at: http://localhost:5099/agents/my-assistant/index

Template Syntax

Data Binding

<!-- Simple binding -->
<h1>{{ title }}</h1>

<!-- Object properties -->
<p>{{ user.name }}</p>

<!-- With default value -->
<p>{{ description || "No description" }}</p>

Conditionals

<div s:if="{{ isLoggedIn }}">Welcome, {{ user.name }}!</div>
<div s:elif="{{ isGuest }}">Welcome, Guest!</div>
<div s:else>Please log in</div>

Loops

<ul>
  <li s:for="{{ items }}" s:for-item="item" s:for-index="i">
    {{ i + 1 }}. {{ item.name }}
  </li>
</ul>

Events

<button s:on-click="handleClick">Click Me</button>
<input s:on-change="handleChange" s:on-keypress="handleKeypress" />

Components

Pages can use other pages as components:

<import s:as="Header" s:from="/shared/header" />
<import s:as="Footer" s:from="/shared/footer" />

<div class="page">
  <header title="My Page" />
  <main>Content</main>
  <footer />
</div>

Built-in Variables

Variable Description
$global Global data from __data.json
$query URL query parameters
$param URL path parameters
$payload POST request body
$cookie Cookie values
$url Current URL info
$theme Current theme
$locale Current locale
$auth OAuth authorization info (if authenticated)

Page Configuration

Create <page>.config for page settings:

{
  "title": "Page Title",
  "guard": "bearer-jwt",
  "cache": 3600,
  "data": {
    "key": "value"
  }
}

Asset Paths

  • Global assets: /agents/assets/.../agent/template/__assets/...
  • Assistant assets: /agents/<id>/assets/.../assistants/<id>/pages/__assets/...

Build Output

<app>/public/agents/
├── assets/
│   ├── libsui.min.js      # SUI frontend SDK
│   ├── css/               # Global CSS
│   ├── js/                # Global JS
│   └── images/            # Global images
│
├── login.sui              # Global page
├── login.cfg
│
└── my-assistant/
    ├── index.sui          # Assistant page
    └── index.cfg

Authentication

Pages default to public access. To require authentication:

/assistants/my-assistant/pages/dashboard/dashboard.config:

{
  "guard": "bearer-jwt"
}

Available guards:

Guard Description
- No authentication (default)
bearer-jwt JWT token in Authorization header
cookie-jwt JWT token in cookie
oauth OAuth 2.0 authentication

Triggering Pages from Hooks

Use action messages to open pages in the sidebar during conversation:

// Navigate to a page in sidebar
ctx.Send({
  type: "action",
  props: {
    name: "navigate",
    payload: {
      route: "/agents/my-assistant/result", // Page route
      title: "Query Results", // Sidebar title
      query: { id: "123" }, // Passed as $query in page
    },
  },
});

// Open in new tab
ctx.Send({
  type: "action",
  props: {
    name: "navigate",
    payload: {
      route: "/agents/my-assistant/detail",
      target: "_blank",
    },
  },
});

Action Reference

Navigate

Open a route in the sidebar or new window.

Payload:

Field Type Required Description
route string Target route or URL
title string - Page title (shows custom title bar with back button)
icon string - Tab icon (e.g., material-folder)
query Record<string, string> - Query parameters (passed as $query in page)
target '_self' | '_blank' - _self (sidebar, default) or _blank (new window)

Route Types:

Prefix Type Description
$dashboard/ CUI Page Dashboard pages (e.g., $dashboard/kb/kb)
/ SUI Page Custom pages (e.g., /agents/demo/result)
http://https:// External External URL (loaded in iframe)

Examples:

// Open agent page in sidebar with title
ctx.Send({
  type: "action",
  props: {
    name: "navigate",
    payload: {
      route: "/agents/my-assistant/result",
      title: "Query Results",
      icon: "material-table_chart",
      query: { id: "123" },
    },
  },
});

// Open CUI dashboard page
ctx.Send({
  type: "action",
  props: {
    name: "navigate",
    payload: { route: "$dashboard/users" },
  },
});

// Open external URL in new tab
ctx.Send({
  type: "action",
  props: {
    name: "navigate",
    payload: {
      route: "https://docs.example.com",
      target: "_blank",
    },
  },
});

Navigate Back

Navigate back in history.

ctx.Send({
  type: "action",
  props: { name: "navigate.back" },
});

Notify

Show notification messages.

Actions:

Action Description
notify.success Success notification (green)
notify.error Error notification (red)
notify.warning Warning notification (yellow)
notify.info Info notification (blue)

Payload:

Field Type Required Description
message string Notification message
duration number - Auto-close seconds (default: 3, 0 = keep open)
icon string - Custom icon (overrides default)
closable boolean - Show close button (default: false)

Examples:

// Success notification
ctx.Send({
  type: "action",
  props: {
    name: "notify.success",
    payload: { message: "Data saved successfully!" },
  },
});

// Error with custom duration
ctx.Send({
  type: "action",
  props: {
    name: "notify.error",
    payload: {
      message: "Operation failed",
      duration: 5,
      closable: true,
    },
  },
});

App Menu

Refresh application menu/navigation.

ctx.Send({
  type: "action",
  props: { name: "app.menu.reload" },
});

All Actions

Category Action Description
Navigate navigate Open page in sidebar or new tab
navigate.back Navigate back in history
Notify notify.success Show success notification
notify.error Show error notification
notify.warning Show warning notification
notify.info Show info notification
App app.menu.reload Refresh application menu
Modal modal.open Open content in modal dialog
modal.close Close modal
Table table.search Trigger table search
table.refresh Refresh table data
table.save Save table row data
table.delete Delete table row(s)
Form form.find Load form data by ID
form.submit Submit form data
form.reset Reset form to initial state
form.setFields Set form field values
MCP mcp.tool.call Execute MCP tool (client-side)
mcp.resource.read Read MCP resource
Event event.emit Emit custom event
Confirm confirm Show confirmation dialog

Frontend API

Backend Calls

import { $Backend, Yao } from "@yao/sui";

// Call backend method defined in .backend.ts
const data = await $Backend().Call("MethodName", arg1, arg2);

// 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

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)

// 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/<assistant-id>/<page>, they can communicate with the host:

Receiving Context

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

// 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 for complete documentation.