diff --git a/.agents/skills/.gitkeep b/.agents/skills/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/.github/workflows/deploy-github-pages.yml b/.github/workflows/deploy-github-pages.yml index 403e7b63..46a88dc6 100644 --- a/.github/workflows/deploy-github-pages.yml +++ b/.github/workflows/deploy-github-pages.yml @@ -46,7 +46,9 @@ jobs: VITE_CHAT_URL: ${{ vars.PLAYGROUND_BACKEND_URL }}/chat-genui VITE_GET_MODELS_URL: ${{ vars.PLAYGROUND_BACKEND_URL }}/get-models VITE_CHECK_MCP_URL: ${{ vars.PLAYGROUND_BACKEND_URL }}/check-mcp + VITE_FETCH_AGENT_CARD_URL: ${{ vars.PLAYGROUND_BACKEND_URL }}/fetch-agent-card VITE_CHAT_TEMPLATE_URL: ${{ vars.PLAYGROUND_BACKEND_URL }}/chat-template + VITE_CHECK_OPENAPI_TOOLS_URL: ${{ vars.PLAYGROUND_BACKEND_URL }}/check-openapi-tools run: node scripts/build-github-pages.mjs - name: Upload Pages artifact diff --git a/.github/workflows/deploy-obs-playground.yml b/.github/workflows/deploy-obs-playground.yml index 9553271f..6280751d 100644 --- a/.github/workflows/deploy-obs-playground.yml +++ b/.github/workflows/deploy-obs-playground.yml @@ -46,6 +46,7 @@ jobs: VITE_CHAT_URL: ${{ vars.GENUI_BACKEND_URL }}/chat-genui VITE_GET_MODELS_URL: ${{ vars.GENUI_BACKEND_URL }}/get-models VITE_CHECK_MCP_URL: ${{ vars.GENUI_BACKEND_URL }}/check-mcp + VITE_FETCH_AGENT_CARD_URL: ${{ vars.GENUI_BACKEND_URL }}/fetch-agent-card - name: Copy files run: | cp sites/playground/web/dist/index.html sites/playground/web/dist/404.html diff --git a/.github/workflows/publish-angular.yml b/.github/workflows/publish-angular.yml deleted file mode 100644 index d1e87865..00000000 --- a/.github/workflows/publish-angular.yml +++ /dev/null @@ -1,87 +0,0 @@ -name: Publish GenUI SDK Angular - -on: - workflow_dispatch: - inputs: - version: - description: '发布版本号(将写入 packages/frameworks/angular/package.json,例如 1.0.0-beta.2)' - required: true - type: string - npm_tag: - description: '发布标签(latest=正式版,beta/alpha=预发版,安装时如 npm install pkg@beta)' - required: true - default: latest - type: choice - options: - - latest - - beta - - alpha - -jobs: - publish: - runs-on: ubuntu-latest - permissions: - contents: write - id-token: write - steps: - - name: Check allowed publishers - run: | - ALLOWED="${{ vars.ALLOWED_PUBLISHERS }}" - if [ -z "$ALLOWED" ]; then - echo "::error::请在仓库 Settings → Secrets and variables → Actions → Variables 中配置 ALLOWED_PUBLISHERS(逗号分隔的 GitHub 用户名)" - exit 1 - fi - ACTOR="${{ github.actor }}" - if echo ",${ALLOWED}," | grep -q ",${ACTOR},"; then - echo "✓ 允许发布: $ACTOR" - else - echo "::error::无权限发布: $ACTOR 不在 ALLOWED_PUBLISHERS 列表中" - exit 1 - fi - - - name: Validate version (semver) - run: | - VERSION="${{ github.event.inputs.version }}" - if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]; then - echo "::error::版本号需符合语义化版本(如 1.0.0、1.0.0-beta.2),当前值: $VERSION" - exit 1 - fi - echo "✓ 版本号格式正确: $VERSION" - - - name: Checkout (with submodules) - uses: actions/checkout@v4 - with: - fetch-depth: 0 - submodules: true - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'pnpm' - - - name: Install dependencies - run: pnpm install - - - name: Update Angular package version - run: node ./scripts/update-package-version.js packages/frameworks/angular/package.json "${{ github.event.inputs.version }}" - - - name: Build Angular - run: pnpm --filter @opentiny/genui-sdk-angular build:lib:npm - - - name: Setup .npmrc for publish - run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc - - - name: Publish to npm (from angular package root) - working-directory: packages/frameworks/angular/output - run: pnpm publish --no-git-checks --access public --tag ${{ github.event.inputs.npm_tag }} - - - name: Create and push tag - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git tag "@opentiny/genui-sdk-angular@${{ github.event.inputs.version }}" - git push origin "@opentiny/genui-sdk-angular@${{ github.event.inputs.version }}" diff --git a/.github/workflows/publish-package.yml b/.github/workflows/publish-package.yml new file mode 100644 index 00000000..87e80657 --- /dev/null +++ b/.github/workflows/publish-package.yml @@ -0,0 +1,173 @@ +name: Publish GenUI SDK package + +on: + workflow_dispatch: + inputs: + package: + description: '要发布的 npm 包' + required: true + type: choice + options: + - '@opentiny/genui-sdk-server' + - '@opentiny/genui-sdk-vue' + - '@opentiny/genui-sdk-angular' + - '@opentiny/genui-sdk-core' + - '@opentiny/genui-sdk-materials-vue-opentiny-vue' + - '@opentiny/genui-sdk-materials-vue-element-plus' + - '@opentiny/genui-sdk-materials-angular-opentiny-ng' + version: + description: '发布版本号(将写入对应包的 package.json,例如 1.0.0-beta.3)' + required: true + type: string + npm_tag: + description: '发布标签(latest=正式版,beta/alpha=预发版,安装时如 npm install pkg@beta)' + required: true + default: latest + type: choice + options: + - latest + - beta + - alpha + fetch_npm_tag: + description: '发包前从 npm 拉取版本替换 workspace 依赖所使用的 tag(通常为 latest)' + required: true + default: latest + type: choice + options: + - latest + - beta + - alpha + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: write + id-token: write + steps: + - name: Check allowed publishers + run: | + ALLOWED="${{ vars.ALLOWED_PUBLISHERS }}" + if [ -z "$ALLOWED" ]; then + echo "::error::请在仓库 Settings → Secrets and variables → Actions → Variables 中配置 ALLOWED_PUBLISHERS(逗号分隔的 GitHub 用户名)" + exit 1 + fi + ACTOR="${{ github.actor }}" + if echo ",${ALLOWED}," | grep -q ",${ACTOR},"; then + echo "✓ 允许发布: $ACTOR" + else + echo "::error::无权限发布: $ACTOR 不在 ALLOWED_PUBLISHERS 列表中" + exit 1 + fi + + - name: Validate version (semver) + run: | + VERSION="${{ github.event.inputs.version }}" + if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]; then + echo "::error::版本号需符合语义化版本(如 1.0.0、1.0.0-beta.2),当前值: $VERSION" + exit 1 + fi + echo "✓ 版本号格式正确: $VERSION" + + - name: Resolve package config + id: pkg + run: | + case "${{ github.event.inputs.package }}" in + @opentiny/genui-sdk-server) + echo "pkg_json=packages/server/package.json" >> "$GITHUB_OUTPUT" + echo "sync_pkg_json=packages/server/output/package.json" >> "$GITHUB_OUTPUT" + echo "filter=@opentiny/genui-sdk-server" >> "$GITHUB_OUTPUT" + echo "build_script=build:lib:npm" >> "$GITHUB_OUTPUT" + echo "publish_dir=packages/server/output" >> "$GITHUB_OUTPUT" + ;; + @opentiny/genui-sdk-vue) + echo "pkg_json=packages/frameworks/vue/package.json" >> "$GITHUB_OUTPUT" + echo "sync_pkg_json=packages/frameworks/vue/package.json" >> "$GITHUB_OUTPUT" + echo "filter=@opentiny/genui-sdk-vue" >> "$GITHUB_OUTPUT" + echo "build_script=build:lib:npm" >> "$GITHUB_OUTPUT" + echo "publish_dir=packages/frameworks/vue" >> "$GITHUB_OUTPUT" + ;; + @opentiny/genui-sdk-angular) + echo "pkg_json=packages/frameworks/angular/package.json" >> "$GITHUB_OUTPUT" + echo "sync_pkg_json=packages/frameworks/angular/output/package.json" >> "$GITHUB_OUTPUT" + echo "filter=@opentiny/genui-sdk-angular" >> "$GITHUB_OUTPUT" + echo "build_script=build:lib:npm" >> "$GITHUB_OUTPUT" + echo "publish_dir=packages/frameworks/angular/output" >> "$GITHUB_OUTPUT" + ;; + @opentiny/genui-sdk-core) + echo "pkg_json=packages/core/package.json" >> "$GITHUB_OUTPUT" + echo "sync_pkg_json=packages/core/package.json" >> "$GITHUB_OUTPUT" + echo "filter=@opentiny/genui-sdk-core" >> "$GITHUB_OUTPUT" + echo "build_script=build" >> "$GITHUB_OUTPUT" + echo "publish_dir=packages/core" >> "$GITHUB_OUTPUT" + ;; + @opentiny/genui-sdk-materials-vue-opentiny-vue) + echo "pkg_json=packages/materials/vue-opentiny-vue/package.json" >> "$GITHUB_OUTPUT" + echo "sync_pkg_json=packages/materials/vue-opentiny-vue/package.json" >> "$GITHUB_OUTPUT" + echo "filter=@opentiny/genui-sdk-materials-vue-opentiny-vue" >> "$GITHUB_OUTPUT" + echo "build_script=build" >> "$GITHUB_OUTPUT" + echo "publish_dir=packages/materials/vue-opentiny-vue" >> "$GITHUB_OUTPUT" + ;; + @opentiny/genui-sdk-materials-vue-element-plus) + echo "pkg_json=packages/materials/vue-element-plus/package.json" >> "$GITHUB_OUTPUT" + echo "sync_pkg_json=packages/materials/vue-element-plus/package.json" >> "$GITHUB_OUTPUT" + echo "filter=@opentiny/genui-sdk-materials-vue-element-plus" >> "$GITHUB_OUTPUT" + echo "build_script=build" >> "$GITHUB_OUTPUT" + echo "publish_dir=packages/materials/vue-element-plus" >> "$GITHUB_OUTPUT" + ;; + @opentiny/genui-sdk-materials-angular-opentiny-ng) + echo "pkg_json=packages/materials/angular-opentiny-ng/package.json" >> "$GITHUB_OUTPUT" + echo "sync_pkg_json=packages/materials/angular-opentiny-ng/package.json" >> "$GITHUB_OUTPUT" + echo "filter=@opentiny/genui-sdk-materials-angular-opentiny-ng" >> "$GITHUB_OUTPUT" + echo "build_script=build" >> "$GITHUB_OUTPUT" + echo "publish_dir=packages/materials/angular-opentiny-ng" >> "$GITHUB_OUTPUT" + ;; + *) + echo "::error::未知包: ${{ github.event.inputs.package }}" + exit 1 + ;; + esac + + - name: Checkout (with submodules) + uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: true + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install + + - name: Update package version + run: node ./scripts/update-package-version.js "${{ steps.pkg.outputs.pkg_json }}" "${{ github.event.inputs.version }}" + + - name: Build package + run: pnpm --filter ${{ steps.pkg.outputs.filter }} ${{ steps.pkg.outputs.build_script }} + + - name: Sync workspace deps for publish + env: + PUBLISH_PKG_JSONS: ${{ steps.pkg.outputs.sync_pkg_json }} + FETCH_NPM_TAG: ${{ github.event.inputs.fetch_npm_tag }} + run: node ./scripts/ci-replace-workspace-deps.js + + - name: Setup .npmrc for publish + run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc + + - name: Publish to npm + working-directory: ${{ steps.pkg.outputs.publish_dir }} + run: pnpm publish --no-git-checks --access public --tag ${{ github.event.inputs.npm_tag }} + + - name: Create and push tag + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag "${{ github.event.inputs.package }}@${{ github.event.inputs.version }}" + git push origin "${{ github.event.inputs.package }}@${{ github.event.inputs.version }}" diff --git a/.github/workflows/publish-packages.yml b/.github/workflows/publish-packages.yml index 183ca8f9..64c4aeec 100644 --- a/.github/workflows/publish-packages.yml +++ b/.github/workflows/publish-packages.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: inputs: version: - description: '发布版本号(将写入 server / vue / angular 的 package.json,例如 1.0.0-beta.2)' + description: '发布版本号,如 1.0.0-beta.2' required: true type: string npm_tag: @@ -16,6 +16,20 @@ on: - latest - beta - alpha + fetch_npm_tag: + description: '发包前从 npm 拉取版本替换 workspace 依赖所使用的 tag(通常为 latest)' + required: true + default: latest + type: choice + options: + - latest + - beta + - alpha + publish_optional_packages: + description: '同时发布 core 与物料包(vue-opentiny / vue-element-plus / angular)' + required: false + default: false + type: boolean jobs: publish: @@ -72,6 +86,28 @@ jobs: node ./scripts/update-package-version.js packages/server/package.json "$VERSION" node ./scripts/update-package-version.js packages/frameworks/vue/package.json "$VERSION" node ./scripts/update-package-version.js packages/frameworks/angular/package.json "$VERSION" + if [ "${{ github.event.inputs.publish_optional_packages }}" = "true" ]; then + node ./scripts/update-package-version.js packages/core/package.json "$VERSION" + node ./scripts/update-package-version.js packages/materials/vue-opentiny-vue/package.json "$VERSION" + node ./scripts/update-package-version.js packages/materials/vue-element-plus/package.json "$VERSION" + node ./scripts/update-package-version.js packages/materials/angular-opentiny-ng/package.json "$VERSION" + fi + + - name: Build Core + if: github.event.inputs.publish_optional_packages == 'true' + run: pnpm --filter @opentiny/genui-sdk-core build + + - name: Build Vue materials + if: github.event.inputs.publish_optional_packages == 'true' + run: pnpm --filter @opentiny/genui-sdk-materials-vue-opentiny-vue build + + - name: Build Element Plus materials + if: github.event.inputs.publish_optional_packages == 'true' + run: pnpm --filter @opentiny/genui-sdk-materials-vue-element-plus build + + - name: Build Angular materials + if: github.event.inputs.publish_optional_packages == 'true' + run: pnpm --filter @opentiny/genui-sdk-materials-angular-opentiny-ng build - name: Build Server run: pnpm --filter @opentiny/genui-sdk-server build:lib:npm @@ -82,15 +118,47 @@ jobs: - name: Build Angular run: pnpm --filter @opentiny/genui-sdk-angular build:lib:npm + - name: Sync workspace deps for publish + env: + FETCH_NPM_TAG: ${{ github.event.inputs.fetch_npm_tag }} + PUBLISH_VERSION: ${{ github.event.inputs.version }} + run: | + PUBLISH_PKG_JSONS="packages/server/output/package.json,packages/frameworks/vue/package.json,packages/frameworks/angular/output/package.json" + if [ "${{ github.event.inputs.publish_optional_packages }}" = "true" ]; then + PUBLISH_PKG_JSONS="${PUBLISH_PKG_JSONS},packages/core/package.json,packages/materials/vue-opentiny-vue/package.json,packages/materials/vue-element-plus/package.json,packages/materials/angular-opentiny-ng/package.json" + fi + export PUBLISH_PKG_JSONS + node ./scripts/ci-replace-workspace-deps.js + - name: Setup .npmrc for publish run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc + - name: Publish Core to npm + if: github.event.inputs.publish_optional_packages == 'true' + working-directory: packages/core + run: pnpm publish --no-git-checks --access public --tag ${{ github.event.inputs.npm_tag }} + + - name: Publish Vue materials to npm + if: github.event.inputs.publish_optional_packages == 'true' + working-directory: packages/materials/vue-opentiny-vue + run: pnpm publish --no-git-checks --access public --tag ${{ github.event.inputs.npm_tag }} + + - name: Publish Element Plus materials to npm + if: github.event.inputs.publish_optional_packages == 'true' + working-directory: packages/materials/vue-element-plus + run: pnpm publish --no-git-checks --access public --tag ${{ github.event.inputs.npm_tag }} + + - name: Publish Angular materials to npm + if: github.event.inputs.publish_optional_packages == 'true' + working-directory: packages/materials/angular-opentiny-ng + run: pnpm publish --no-git-checks --access public --tag ${{ github.event.inputs.npm_tag }} + - name: Publish server to npm working-directory: packages/server/output run: pnpm publish --no-git-checks --access public --tag ${{ github.event.inputs.npm_tag }} - name: Publish Vue to npm - working-directory: packages/frameworks/vue/output + working-directory: packages/frameworks/vue run: pnpm publish --no-git-checks --access public --tag ${{ github.event.inputs.npm_tag }} - name: Publish Angular to npm diff --git a/.github/workflows/publish-server.yml b/.github/workflows/publish-server.yml deleted file mode 100644 index e9d88acb..00000000 --- a/.github/workflows/publish-server.yml +++ /dev/null @@ -1,87 +0,0 @@ -name: Publish GenUI SDK Server - -on: - workflow_dispatch: - inputs: - version: - description: '发布版本号(将写入 packages/server/package.json,例如 1.0.0-beta.3)' - required: true - type: string - npm_tag: - description: '发布标签(latest=正式版,beta/alpha=预发版,安装时如 npm install pkg@beta)' - required: true - default: latest - type: choice - options: - - latest - - beta - - alpha - -jobs: - publish: - runs-on: ubuntu-latest - permissions: - contents: write - id-token: write - steps: - - name: Check allowed publishers - run: | - ALLOWED="${{ vars.ALLOWED_PUBLISHERS }}" - if [ -z "$ALLOWED" ]; then - echo "::error::请在仓库 Settings → Secrets and variables → Actions → Variables 中配置 ALLOWED_PUBLISHERS(逗号分隔的 GitHub 用户名)" - exit 1 - fi - ACTOR="${{ github.actor }}" - if echo ",${ALLOWED}," | grep -q ",${ACTOR},"; then - echo "✓ 允许发布: $ACTOR" - else - echo "::error::无权限发布: $ACTOR 不在 ALLOWED_PUBLISHERS 列表中" - exit 1 - fi - - - name: Validate version (semver) - run: | - VERSION="${{ github.event.inputs.version }}" - if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]; then - echo "::error::版本号需符合语义化版本(如 1.0.0、1.0.0-beta.2),当前值: $VERSION" - exit 1 - fi - echo "✓ 版本号格式正确: $VERSION" - - - name: Checkout (with submodules) - uses: actions/checkout@v4 - with: - fetch-depth: 0 - submodules: true - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'pnpm' - - - name: Install dependencies - run: pnpm install - - - name: Update server package version - run: node ./scripts/update-package-version.js packages/server/package.json "${{ github.event.inputs.version }}" - - - name: Build server - run: pnpm --filter @opentiny/genui-sdk-server build:lib:npm - - - name: Setup .npmrc for publish - run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc - - - name: Publish to npm (from server output) - working-directory: packages/server/output - run: pnpm publish --no-git-checks --access public --tag ${{ github.event.inputs.npm_tag }} - - - name: Create and push tag - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git tag "@opentiny/genui-sdk-server@${{ github.event.inputs.version }}" - git push origin "@opentiny/genui-sdk-server@${{ github.event.inputs.version }}" diff --git a/.github/workflows/publish-vue.yml b/.github/workflows/publish-vue.yml deleted file mode 100644 index aaaf5b46..00000000 --- a/.github/workflows/publish-vue.yml +++ /dev/null @@ -1,87 +0,0 @@ -name: Publish GenUI SDK Vue - -on: - workflow_dispatch: - inputs: - version: - description: '发布版本号(将写入 packages/frameworks/vue/package.json,例如 1.0.0-beta.3)' - required: true - type: string - npm_tag: - description: '发布标签(latest=正式版,beta/alpha=预发版,安装时如 npm install pkg@beta)' - required: true - default: latest - type: choice - options: - - latest - - beta - - alpha - -jobs: - publish: - runs-on: ubuntu-latest - permissions: - contents: write - id-token: write - steps: - - name: Check allowed publishers - run: | - ALLOWED="${{ vars.ALLOWED_PUBLISHERS }}" - if [ -z "$ALLOWED" ]; then - echo "::error::请在仓库 Settings → Secrets and variables → Actions → Variables 中配置 ALLOWED_PUBLISHERS(逗号分隔的 GitHub 用户名)" - exit 1 - fi - ACTOR="${{ github.actor }}" - if echo ",${ALLOWED}," | grep -q ",${ACTOR},"; then - echo "✓ 允许发布: $ACTOR" - else - echo "::error::无权限发布: $ACTOR 不在 ALLOWED_PUBLISHERS 列表中" - exit 1 - fi - - - name: Validate version (semver) - run: | - VERSION="${{ github.event.inputs.version }}" - if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]; then - echo "::error::版本号需符合语义化版本(如 1.0.0、1.0.0-beta.2),当前值: $VERSION" - exit 1 - fi - echo "✓ 版本号格式正确: $VERSION" - - - name: Checkout (with submodules) - uses: actions/checkout@v4 - with: - fetch-depth: 0 - submodules: true - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'pnpm' - - - name: Install dependencies - run: pnpm install - - - name: Update Vue package version - run: node ./scripts/update-package-version.js packages/frameworks/vue/package.json "${{ github.event.inputs.version }}" - - - name: Build Vue - run: pnpm --filter @opentiny/genui-sdk-vue build:lib:npm - - - name: Setup .npmrc for publish - run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc - - - name: Publish to npm (from vue output) - working-directory: packages/frameworks/vue/output - run: pnpm publish --no-git-checks --access public --tag ${{ github.event.inputs.npm_tag }} - - - name: Create and push tag - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git tag "@opentiny/genui-sdk-vue@${{ github.event.inputs.version }}" - git push origin "@opentiny/genui-sdk-vue@${{ github.event.inputs.version }}" diff --git a/.gitignore b/.gitignore index 7eb941c7..1ddb8d7d 100644 --- a/.gitignore +++ b/.gitignore @@ -148,3 +148,13 @@ stats.html # GitHub Pages 本地合并输出 _site/ + +# AI platform skill projections. +# SSOT lives in .agents/skills/ and should be committed. +.claude/skills/ +.cursor/skills/ +.codex/skills/ +.gemini/skills/ +.hermes/skills/ +.openclaw/skills/ +.config/opencode/skills/ diff --git a/README.md b/README.md index 794629a4..fac73e78 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,17 @@ -## GenUI SDK +# GenUI SDK + +

+ + OpenTiny Logo + +

Language: English | [简体中文](README.zh-CN.md) GenUI SDK is a full‑stack development toolkit developed by OpenTiny for building **Generative UI**–based AI applications. It helps you quickly create AI apps and embed generative interfaces into existing products. +[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/opentiny/genui-sdk) + ### Instruction **GenUI SDK** is an open‑source solution built by the OpenTiny team around the Generative UI concept, providing integrated capabilities across both frontend and backend. @@ -56,4 +64,4 @@ You can also reach us via: ### License -[MIT](https://opensource.org/license/MIT) \ No newline at end of file +[MIT](https://opensource.org/license/MIT) diff --git a/README.zh-CN.md b/README.zh-CN.md index c74d1820..37b53dc6 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -1,9 +1,17 @@ # GenUI SDK +

+ + OpenTiny Logo + +

+ > 语言: [English](README.md) | 简体中文 GenUI SDK 是 OpenTiny 面向生成式 UI(Generative UI)场景的全栈开发套件,帮助你快速搭建 AI 应用。 +[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/opentiny/genui-sdk) + ## 简介 **GenUI SDK** 是 OpenTiny 团队基于生成式 UI 理念打造的开源解决方案,提供完整的前后端一体化集成能力。它遵循 OpenAI 接口规范,可无缝对接主流大模型服务;内置 Vue 与 Angular 双框架渲染器,支持自定义的组件库、交互行为与主题样式。无论是从零搭建一个 AI 对话应用,还是在现有业务系统中嵌入生成式界面能力,GenUI SDK 都能让开发者开箱即用、灵活扩展。 @@ -49,4 +57,4 @@ GenUI SDK 在设计上兼顾了”开箱即用“与”深度定制“,具备 ## 授权协议 -[MIT](https://opensource.org/license/MIT) \ No newline at end of file +[MIT](https://opensource.org/license/MIT) diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 6f4ec33c..89ed9577 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -1,154 +1,27 @@ import { defineConfig } from 'vitepress'; -import { vitepressDemoPlugin } from 'vitepress-demo-plugin'; -import { tabsMarkdownPlugin } from 'vitepress-plugin-tabs'; +import { sharedConfig } from './config/shared'; +import { zhThemeConfig } from './config/zh-theme'; +import { enThemeConfig } from './config/en-theme'; // https://vitepress.dev/reference/site-config export default defineConfig({ - title: 'GenUI SDK', - description: 'GenUI SDK Documentation', + ...sharedConfig, srcDir: 'src', - base: '/genui-sdk-docs/', - ignoreDeadLinks: true, - markdown: { - config(md) { - md.use(vitepressDemoPlugin); - md.use(tabsMarkdownPlugin); + locales: { + root: { + label: '中文', + lang: 'zh-CN', + title: 'GenUI SDK', + description: 'GenUI SDK 文档', + themeConfig: zhThemeConfig, }, - }, - vue:{ - template: { - compilerOptions: { - isCustomElement: (tag) => tag === 'genui-renderer-ng-element', - }, - }, - }, - vite: { - server: { - host: '0.0.0.0', // 允许外部访问 - open: true, // 开发时自动打开浏览器 - }, - }, - themeConfig: { - logo: '/logo.svg', - outline: { - level: [2, 3], // 显示

标题 - label: '目录', // 可选,自定义目录标题 - }, - nav: [ - { text: '快速开始', link: '/guide/quick-start' }, - { text: '组件文档', link: '/components/renderer' }, - { text: '特性示例', link: '/examples/renderer/custom-actions' }, - { text: '协议规范', link: '/schema/protocol' }, - ], - sidebar: { - '/guide/': [ - { - text: 'GenUI SDK Vue 指引', - items: [ - { text: '快速开始', link: '/guide/quick-start' }, - { text: '使用 Renderer 组件', link: '/guide/start-with-renderer' }, - { text: '搭配 TinyRobot 使用', link: '/guide/renderer-with-tiny-robot' }, - ], - }, - { - text: 'GenUI SDK Angular 指引', - items: [ - { text: '安装与配置', link: '/guide/angular/install' }, - { text: '使用 Renderer 组件', link: '/guide/angular/start-with-renderer' }, - ], - }, - { - text: 'GenUI SDK Server 指引', - items: [ - { text: 'Server 包使用文档', link: '/guide/server-usage' }, - ], - }, - ], - '/components/': [ - { - text: 'Vue 组件文档', - items: [ - { text: 'GenuiRenderer', link: '/components/renderer' }, - { text: 'GenuiChat', link: '/components/chat' }, - { text: 'GenuiConfigProvider', link: '/components/config-provider' }, - ], - }, - { - text: 'Angular 组件文档', - items: [ - { text: 'GenuiRenderer', link: '/components/angular/renderer' } - ], - }, - { - text: 'Server 库文档', - items: [ - { text: 'API 参考', link: '/components/server/api' }, - { text: 'CLI', link: '/components/server/cli' } - ], - }, - ], - '/examples/': [ - { - text: 'Vue组件特性示例', - items: [ - { - text: 'Renderer 组件', - items: [ - { text: '自定义 Actions', link: '/examples/renderer/custom-actions' }, - { text: '自定义 Components', link: '/examples/renderer/custom-components' }, - { - text: '配置缓冲字段', - link: '/examples/renderer/required-complete-field-selectors', - }, - { text: '传递合并 State', link: '/examples/renderer/state' }, - ], - }, - { - text: 'Chat 组件', - items: [ - { text: '自定义 Actions', link: '/examples/chat/custom-actions' }, - { text: '自定义 Components', link: '/examples/chat/custom-components' }, - { text: '自定义 Snippets', link: '/examples/chat/custom-snippets' }, - { text: '自定义 Examples', link: '/examples/chat/custom-examples' }, - { text: '自定义底部工具栏', link: '/examples/chat/footer-toolbar' }, - { text: '自定义思考过程', link: '/examples/chat/thinking-process' }, - { text: '自定义 Fetch', link: '/examples/chat/custom-fetch' }, - { text: '上传图片', link: '/examples/chat/image-upload' }, - { text: '历史会话管理', link: '/examples/chat/history' }, - ], - }, - { - text: 'ConfigProvider 组件', - items: [ - { text: '切换主题', link: '/examples/config-provider/theme' }, - { text: '自定义主题', link: '/examples/config-provider/custom-theme' }, - { text: '国际化配置', link: '/examples/config-provider/i18n' }, - ], - }, - ], - }, - { - text: 'Angular 组件特性示例', - items: [ - { - text: 'Renderer 组件', - items: [ - { text: '自定义 Actions', link: '/examples/angular/renderer/custom-actions' }, - // { text: '自定义 Components/Directives', link: '/examples/angular/renderer/custom-components-directives' }, - { - text: '配置缓冲字段', - link: '/examples/angular/renderer/required-complete-field-selectors', - }, - { text: '传递合并 State', link: '/examples/angular/renderer/state' }, - ], - }, - ], - } - ], - }, - socialLinks: [{ icon: 'github', link: 'https://github.com/opentiny/genui-sdk' }], - search: { - provider: 'local', + en: { + label: 'English', + lang: 'en-US', + link: '/en/', + title: 'GenUI SDK', + description: 'GenUI SDK Documentation', + themeConfig: enThemeConfig, }, }, }); diff --git a/docs/.vitepress/config/en-theme.ts b/docs/.vitepress/config/en-theme.ts new file mode 100644 index 00000000..e120ba99 --- /dev/null +++ b/docs/.vitepress/config/en-theme.ts @@ -0,0 +1,130 @@ +import type { DefaultTheme } from 'vitepress'; + +export const enThemeConfig: DefaultTheme.Config = { + outline: { + level: [2, 3], + label: 'On this page', + }, + nav: [ + { text: 'Quick Start', link: '/en/guide/quick-start', activeMatch: '/en/guide/' }, + { text: 'Reference', link: '/en/components/renderer', activeMatch: '/en/components/' }, + { + text: 'Examples', + link: '/en/examples/renderer/custom-actions', + activeMatch: '/en/examples/', + }, + { text: 'Protocol', link: '/en/schema/protocol', activeMatch: '/en/schema/' }, + ], + sidebar: { + '/en/guide/': [ + { + text: 'GenUI SDK Vue Guide', + items: [ + { text: 'Quick Start', link: '/en/guide/quick-start' }, + { text: 'Using Renderer', link: '/en/guide/start-with-renderer' }, + ], + }, + { + text: 'GenUI SDK Angular Guide', + items: [ + { text: 'Install & Setup', link: '/en/guide/angular/install' }, + { text: 'Using Renderer', link: '/en/guide/angular/start-with-renderer' }, + ], + }, + { + text: 'GenUI SDK Server Guide', + items: [{ text: 'Server Usage', link: '/en/guide/server-usage' }], + }, + ], + '/en/components/': [ + { + text: 'Vue Components', + items: [ + { text: 'GenuiRenderer', link: '/en/components/renderer' }, + { text: 'GenuiChat', link: '/en/components/chat' }, + { text: 'GenuiConfigProvider', link: '/en/components/config-provider' }, + ], + }, + { + text: 'Angular Components', + items: [{ text: 'GenuiRenderer', link: '/en/components/angular/renderer' }], + }, + { + text: 'Server', + items: [ + { text: 'API Reference', link: '/en/components/server/api' }, + { text: 'CLI', link: '/en/components/server/cli' }, + ], + }, + { + text: 'Core', + items: [{ text: 'API Docs', link: '/en/components/core/api' }], + }, + { + text: 'Materials', + items: [ + { text: 'Vue OpenTiny Vue', link: '/en/components/materials/vue-opentiny-vue' }, + { text: 'Vue Element Plus', link: '/en/components/materials/vue-element-plus' }, + { text: 'Angular OpenTiny NG', link: '/en/components/materials/angular-opentiny-ng' }, + ], + }, + ], + '/en/examples/': [ + { + text: 'Vue Examples', + items: [ + { + text: 'Renderer', + items: [ + { text: 'Custom Actions', link: '/en/examples/renderer/custom-actions' }, + { text: 'Custom Components', link: '/en/examples/renderer/custom-components' }, + { + text: 'Buffer Field Selectors', + link: '/en/examples/renderer/required-complete-field-selectors', + }, + { text: 'Merged State', link: '/en/examples/renderer/state' }, + ], + }, + { + text: 'Chat', + items: [ + { text: 'Custom Actions', link: '/en/examples/chat/custom-actions' }, + { text: 'Custom Components', link: '/en/examples/chat/custom-components' }, + { text: 'Custom Snippets', link: '/en/examples/chat/custom-snippets' }, + { text: 'Custom Examples', link: '/en/examples/chat/custom-examples' }, + { text: 'Footer Toolbar', link: '/en/examples/chat/footer-toolbar' }, + { text: 'Thinking Process', link: '/en/examples/chat/thinking-process' }, + { text: 'Custom Fetch', link: '/en/examples/chat/custom-fetch' }, + { text: 'Image Upload', link: '/en/examples/chat/image-upload' }, + { text: 'Chat History', link: '/en/examples/chat/history' }, + ], + }, + { + text: 'ConfigProvider', + items: [ + { text: 'Theme Switch', link: '/en/examples/config-provider/theme' }, + { text: 'Custom Theme', link: '/en/examples/config-provider/custom-theme' }, + { text: 'i18n', link: '/en/examples/config-provider/i18n' }, + ], + }, + ], + }, + { + text: 'Angular Examples', + items: [ + { + text: 'Renderer', + items: [ + { text: 'Custom Actions', link: '/en/examples/angular/renderer/custom-actions' }, + { + text: 'Buffer Field Selectors', + link: '/en/examples/angular/renderer/required-complete-field-selectors', + }, + { text: 'Merged State', link: '/en/examples/angular/renderer/state' }, + ], + }, + ], + }, + ], + }, +}; diff --git a/docs/.vitepress/config/shared.ts b/docs/.vitepress/config/shared.ts new file mode 100644 index 00000000..78f5847c --- /dev/null +++ b/docs/.vitepress/config/shared.ts @@ -0,0 +1,55 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import type { UserConfig } from 'vitepress'; +import { vitepressDemoPlugin } from 'vitepress-demo-plugin'; +import { tabsMarkdownPlugin } from 'vitepress-plugin-tabs'; + +const docsRoot = path.dirname(fileURLToPath(import.meta.url)); +const srcDir = path.resolve(docsRoot, '../../src'); + +export const sharedConfig: UserConfig = { + base: '/genui-sdk-docs/', + ignoreDeadLinks: true, + transformPageData(pageData) { + if (pageData.isNotFound || !pageData.filePath) { + return; + } + + try { + const content = fs.readFileSync(path.join(srcDir, pageData.filePath), 'utf-8'); + return { + // Base64 避免 Markdown 中的 等字符破坏 VitePress 生成的 SFC + markdownSource: Buffer.from(content, 'utf-8').toString('base64'), + }; + } catch { + return; + } + }, + markdown: { + config(md) { + md.use(vitepressDemoPlugin); + md.use(tabsMarkdownPlugin); + }, + }, + vue: { + template: { + compilerOptions: { + isCustomElement: (tag) => tag === 'genui-renderer-ng-element', + }, + }, + }, + vite: { + server: { + host: '0.0.0.0', + open: true, + }, + }, + themeConfig: { + logo: '/logo.svg', + socialLinks: [{ icon: 'github', link: 'https://github.com/opentiny/genui-sdk' }], + search: { + provider: 'local', + }, + }, +}; diff --git a/docs/.vitepress/config/zh-theme.ts b/docs/.vitepress/config/zh-theme.ts new file mode 100644 index 00000000..14613e21 --- /dev/null +++ b/docs/.vitepress/config/zh-theme.ts @@ -0,0 +1,126 @@ +import type { DefaultTheme } from 'vitepress'; + +export const zhThemeConfig: DefaultTheme.Config = { + outline: { + level: [2, 3], + label: '目录', + }, + nav: [ + { text: '快速开始', link: '/guide/quick-start', activeMatch: '/guide/' }, + { text: '组件文档', link: '/components/renderer', activeMatch: '/components/' }, + { text: '特性示例', link: '/examples/renderer/custom-actions', activeMatch: '/examples/' }, + { text: '协议规范', link: '/schema/protocol', activeMatch: '/schema/' }, + ], + sidebar: { + '/guide/': [ + { + text: 'GenUI SDK Vue 指引', + items: [ + { text: '快速开始', link: '/guide/quick-start' }, + { text: '使用 Renderer 组件', link: '/guide/start-with-renderer' }, + ], + }, + { + text: 'GenUI SDK Angular 指引', + items: [ + { text: '安装与配置', link: '/guide/angular/install' }, + { text: '使用 Renderer 组件', link: '/guide/angular/start-with-renderer' }, + ], + }, + { + text: 'GenUI SDK Server 指引', + items: [{ text: 'Server 包使用文档', link: '/guide/server-usage' }], + }, + ], + '/components/': [ + { + text: 'Vue 组件文档', + items: [ + { text: 'GenuiRenderer', link: '/components/renderer' }, + { text: 'GenuiChat', link: '/components/chat' }, + { text: 'GenuiConfigProvider', link: '/components/config-provider' }, + ], + }, + { + text: 'Angular 组件文档', + items: [{ text: 'GenuiRenderer', link: '/components/angular/renderer' }], + }, + { + text: 'Server 库文档', + items: [ + { text: 'API 参考', link: '/components/server/api' }, + { text: 'CLI', link: '/components/server/cli' }, + ], + }, + { + text: 'Core 库文档', + items: [{ text: 'API 文档', link: '/components/core/api' }], + }, + { + text: '物料包文档', + items: [ + { text: 'Vue OpenTiny Vue', link: '/components/materials/vue-opentiny-vue' }, + { text: 'Vue Element Plus', link: '/components/materials/vue-element-plus' }, + { text: 'Angular OpenTiny NG', link: '/components/materials/angular-opentiny-ng' }, + ], + }, + ], + '/examples/': [ + { + text: 'Vue组件特性示例', + items: [ + { + text: 'Renderer 组件', + items: [ + { text: '自定义 Actions', link: '/examples/renderer/custom-actions' }, + { text: '自定义 Components', link: '/examples/renderer/custom-components' }, + { + text: '配置缓冲字段', + link: '/examples/renderer/required-complete-field-selectors', + }, + { text: '传递合并 State', link: '/examples/renderer/state' }, + ], + }, + { + text: 'Chat 组件', + items: [ + { text: '自定义 Actions', link: '/examples/chat/custom-actions' }, + { text: '自定义 Components', link: '/examples/chat/custom-components' }, + { text: '自定义 Snippets', link: '/examples/chat/custom-snippets' }, + { text: '自定义 Examples', link: '/examples/chat/custom-examples' }, + { text: '自定义底部工具栏', link: '/examples/chat/footer-toolbar' }, + { text: '自定义思考过程', link: '/examples/chat/thinking-process' }, + { text: '自定义 Fetch', link: '/examples/chat/custom-fetch' }, + { text: '上传图片', link: '/examples/chat/image-upload' }, + { text: '历史会话管理', link: '/examples/chat/history' }, + ], + }, + { + text: 'ConfigProvider 组件', + items: [ + { text: '切换主题', link: '/examples/config-provider/theme' }, + { text: '自定义主题', link: '/examples/config-provider/custom-theme' }, + { text: '国际化配置', link: '/examples/config-provider/i18n' }, + ], + }, + ], + }, + { + text: 'Angular 组件特性示例', + items: [ + { + text: 'Renderer 组件', + items: [ + { text: '自定义 Actions', link: '/examples/angular/renderer/custom-actions' }, + { + text: '配置缓冲字段', + link: '/examples/angular/renderer/required-complete-field-selectors', + }, + { text: '传递合并 State', link: '/examples/angular/renderer/state' }, + ], + }, + ], + }, + ], + }, +}; diff --git a/docs/.vitepress/theme/Layout.vue b/docs/.vitepress/theme/Layout.vue new file mode 100644 index 00000000..223abfd3 --- /dev/null +++ b/docs/.vitepress/theme/Layout.vue @@ -0,0 +1,14 @@ + + + diff --git a/docs/.vitepress/theme/copy-page/CopyMarkdownButton.vue b/docs/.vitepress/theme/copy-page/CopyMarkdownButton.vue new file mode 100644 index 00000000..4e9a37e3 --- /dev/null +++ b/docs/.vitepress/theme/copy-page/CopyMarkdownButton.vue @@ -0,0 +1,122 @@ + + + diff --git a/docs/.vitepress/theme/copy-page/copyPageMessages.ts b/docs/.vitepress/theme/copy-page/copyPageMessages.ts new file mode 100644 index 00000000..32addce0 --- /dev/null +++ b/docs/.vitepress/theme/copy-page/copyPageMessages.ts @@ -0,0 +1,20 @@ +export const copyPageMessages = { + 'zh-CN': { + copy: '复制页面', + copied: '已复制', + }, + 'en-US': { + copy: 'Copy page', + copied: 'Copied', + }, +} as const; + +export type CopyPageLocale = keyof typeof copyPageMessages; + +export function getCopyPageMessages(lang: string) { + if (lang in copyPageMessages) { + return copyPageMessages[lang as CopyPageLocale]; + } + + return copyPageMessages['zh-CN']; +} diff --git a/docs/.vitepress/theme/copy-page/index.ts b/docs/.vitepress/theme/copy-page/index.ts new file mode 100644 index 00000000..1ab1102a --- /dev/null +++ b/docs/.vitepress/theme/copy-page/index.ts @@ -0,0 +1,9 @@ +export { default as CopyMarkdownButton } from './CopyMarkdownButton.vue'; +export { getCopyPageMessages } from './copyPageMessages'; +export { + decodeMarkdownSource, + getPageMarkdownEncoded, + getPageMarkdownSource, + hasPageMarkdownSource, +} from './pageMarkdownSource'; +export { useTitleAnchor } from './useTitleAnchor'; diff --git a/docs/.vitepress/theme/copy-page/pageMarkdownSource.ts b/docs/.vitepress/theme/copy-page/pageMarkdownSource.ts new file mode 100644 index 00000000..25b37c30 --- /dev/null +++ b/docs/.vitepress/theme/copy-page/pageMarkdownSource.ts @@ -0,0 +1,32 @@ +import type { PageData } from 'vitepress'; + +interface DocPageData extends PageData { + markdownSource?: string; +} + +export function getPageMarkdownEncoded(page: PageData): string | undefined { + return (page as DocPageData).markdownSource; +} + +export function decodeMarkdownSource(encoded: string): string { + const binary = atob(encoded); + const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0)); + return new TextDecoder().decode(bytes); +} + +export function getPageMarkdownSource(page: PageData): string { + const encoded = getPageMarkdownEncoded(page); + if (!encoded) { + return ''; + } + + try { + return decodeMarkdownSource(encoded); + } catch { + return ''; + } +} + +export function hasPageMarkdownSource(page: PageData): boolean { + return Boolean(getPageMarkdownEncoded(page)); +} diff --git a/docs/.vitepress/theme/copy-page/style.css b/docs/.vitepress/theme/copy-page/style.css new file mode 100644 index 00000000..7cd7ed0a --- /dev/null +++ b/docs/.vitepress/theme/copy-page/style.css @@ -0,0 +1,80 @@ +.vp-doc-title-row { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + margin: 0 0 24px; +} + +.vp-doc-title-row > h1 { + flex: 1; + min-width: 0; + margin: 0 !important; + padding: 0; + border-top: none !important; +} + +.vp-doc-title-actions { + flex-shrink: 0; + padding-top: 0.35em; +} + +.copy-page-btn { + display: inline-flex; + align-items: center; + gap: 6px; + height: 32px; + padding: 0 10px; + border: 1px solid var(--vp-c-divider); + border-radius: 8px; + background-color: var(--vp-c-bg); + color: var(--vp-c-text-2); + font-size: 12px; + font-weight: 500; + line-height: 1; + white-space: nowrap; + cursor: pointer; + user-select: none; + transition: + color 0.15s ease, + border-color 0.15s ease, + background-color 0.15s ease; +} + +.copy-page-btn:hover { + color: var(--vp-c-text-1); + border-color: var(--vp-c-text-3); + background-color: var(--vp-c-bg-soft); +} + +.copy-page-btn.copied { + color: var(--vp-c-brand-1); + border-color: var(--vp-c-brand-soft); + background-color: var(--vp-c-brand-soft); +} + +.copy-page-btn-icon { + display: inline-flex; + align-items: center; + justify-content: center; +} + +.copy-page-btn-text { + display: inline-block; +} + +@media (max-width: 640px) { + .vp-doc-title-row { + flex-direction: column; + align-items: stretch; + gap: 12px; + } + + .vp-doc-title-actions { + padding-top: 0; + } + + .copy-page-btn { + align-self: flex-start; + } +} diff --git a/docs/.vitepress/theme/copy-page/useTitleAnchor.ts b/docs/.vitepress/theme/copy-page/useTitleAnchor.ts new file mode 100644 index 00000000..add6694a --- /dev/null +++ b/docs/.vitepress/theme/copy-page/useTitleAnchor.ts @@ -0,0 +1,63 @@ +import { onUnmounted, ref } from 'vue'; +import { inBrowser, onContentUpdated } from 'vitepress'; + +const COPY_ANCHOR_ID = 'vp-doc-copy-anchor'; + +function cleanupTitleRow(): void { + const row = document.querySelector('.vp-doc-title-row'); + if (!row?.parentElement) { + return; + } + + const h1 = row.querySelector('h1'); + if (h1) { + row.parentElement.insertBefore(h1, row); + } + + row.remove(); +} + +function mountTitleActions(): HTMLElement | null { + cleanupTitleRow(); + + const doc = document.querySelector('.VPDoc .vp-doc'); + const h1 = doc?.querySelector('h1'); + if (!h1?.parentElement) { + return null; + } + + const row = document.createElement('div'); + row.className = 'vp-doc-title-row'; + h1.parentElement.insertBefore(row, h1); + row.appendChild(h1); + + const actions = document.createElement('div'); + actions.className = 'vp-doc-title-actions'; + actions.id = COPY_ANCHOR_ID; + row.appendChild(actions); + + return actions; +} + +export function useTitleAnchor() { + const anchor = ref(null); + + function refreshAnchor(): void { + if (!inBrowser) { + return; + } + + anchor.value = mountTitleActions(); + } + + onContentUpdated(refreshAnchor); + + onUnmounted(() => { + if (inBrowser) { + cleanupTitleRow(); + } + anchor.value = null; + }); + + return { anchor }; +} diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts index 98e530c4..a327b5b2 100644 --- a/docs/.vitepress/theme/index.ts +++ b/docs/.vitepress/theme/index.ts @@ -1,9 +1,13 @@ +import type { Theme } from 'vitepress'; import DefaultTheme from 'vitepress/theme'; -import { enhanceAppWithTabs } from 'vitepress-plugin-tabs/client' +import { enhanceAppWithTabs } from 'vitepress-plugin-tabs/client'; +import Layout from './Layout.vue'; +import './copy-page/style.css'; export default { - ...DefaultTheme, + extends: DefaultTheme, + Layout, enhanceApp({ app }) { - enhanceAppWithTabs(app) + enhanceAppWithTabs(app); }, -}; +} satisfies Theme; diff --git a/docs/I18N.md b/docs/I18N.md new file mode 100644 index 00000000..15cf0176 --- /dev/null +++ b/docs/I18N.md @@ -0,0 +1,97 @@ +# 文档编写指南 + +## 目录约定 + +- 中文文档:`docs/src/`(不含 `en/`) +- 英文文档:`docs/src/en/`(与中文路径一一对应) +- 中文 Demo:`docs/demos/`(默认) +- 英文 Demo:`docs/demos/en/`(与中文路径一一对应) +- 图片:`docs/src/public/`(中英文共用) +- 中文导航:`docs/.vitepress/config/zh-theme.ts` +- 英文导航:`docs/.vitepress/config/en-theme.ts` + +## 新增文档 + +以新增 `docs/src/examples/chat/my-feature.md` 为例: + +1. 编写中文 `.md` +2. 有交互示例时,在 `docs/demos/` 新增 `.vue` + - 中文版本:`docs/demos/chat/my-feature.vue` + - 英文版本(如有中文内容):`docs/demos/en/chat/my-feature.vue` +3. 有图片时,放入 `docs/src/public/` +4. 在 `zh-theme.ts` 的 sidebar 中增加链接: + +```ts +{ text: '我的新特性', link: '/examples/chat/my-feature' }, +``` + +5. 创建并编写英文文档 `docs/src/en/examples/chat/my-feature.md` +6. 在 `en-theme.ts` 的 sidebar 中增加链接: + +```ts +{ text: 'My Feature', link: '/en/examples/chat/my-feature' }, +``` + +7. 本地预览:`cd docs && pnpm dev` + +## Demo 国际化 + +### 命名规则 + +- 中文版本:`docs/demos/xxx.vue`(默认) +- 英文版本:`docs/demos/en/xxx.vue`(路径与中文一一对应,文件名相同) + +### 使用方式 + +在 markdown 文件中引用对应的 demo: + +```markdown + + + + + +``` + +### 需要国际化的内容 + +Demo 文件中以下内容需要翻译: + +| 类型 | 示例 | 处理方式 | +|------|------|----------| +| UI 文案 | `` | 翻译为英文 | +| 代码注释 | `// 获取会话对象` | 翻译或保留 | +| Schema 内容 | `label: '姓名'` | 翻译为英文 | +| alert 消息 | `alert('复制成功')` | 翻译为英文 | + +### 无需国际化的 Demo + +以下 demo 无需创建英文版本,直接共用即可: + +- 纯英文内容的 demo +- 国际化示例 demo(如 `i18n.vue`,本身演示 i18n 功能) +- 无文本内容的 demo + +## 英文文档注意事项 + +从中文复制到英文时,`` 和图片的相对路径需**多加一层 `../`**,并将 demo 路径指向 `demos/en/`: + +```markdown + + + + + +``` + +文档内互相引用用相对路径即可,写法与中文相同。 +sidebar / nav 配置中,英文链接需带 `/en` 前缀。 + +## 修改或删除文档 + +| 操作 | 需同步 | +|------|--------| +| 改正文 | 更新 `src/en/` 下对应文件 | +| 重命名 / 移动 | 同步移动英文文件,更新两个 `theme` 配置 | +| 删除 | 删除英文镜像文件,更新两个 `theme` 配置 | +| 新增/修改 Demo | 同步更新 `demos/en/` 下对应文件(如有中文内容) | diff --git a/docs/demos/angular/renderer/custom-actions-open-page.ts b/docs/demos/angular/renderer/custom-actions-open-page.ts index 8cfeef00..2ff1a316 100644 --- a/docs/demos/angular/renderer/custom-actions-open-page.ts +++ b/docs/demos/angular/renderer/custom-actions-open-page.ts @@ -1,13 +1,17 @@ import { Component } from '@angular/core'; -import { GenuiRenderer } from '@opentiny/genui-sdk-angular'; +import { GenuiConfigProvider, GenuiRenderer } from '@opentiny/genui-sdk-angular'; +import { materials } from '@opentiny/genui-sdk-materials-angular-opentiny-ng/materials'; @Component({ - imports: [GenuiRenderer], + imports: [GenuiConfigProvider, GenuiRenderer], template: ` - + + + `, }) export class GenuiExample { + activeMaterials = materials; schemaContent = { componentName: 'Page', children: [ diff --git a/docs/demos/angular/renderer/required-complete-field-selectors.ts b/docs/demos/angular/renderer/required-complete-field-selectors.ts index 9fdca158..c6414949 100644 --- a/docs/demos/angular/renderer/required-complete-field-selectors.ts +++ b/docs/demos/angular/renderer/required-complete-field-selectors.ts @@ -1,5 +1,6 @@ import { Component } from '@angular/core'; -import { GenuiRenderer } from '@opentiny/genui-sdk-angular'; +import { GenuiConfigProvider, GenuiRenderer } from '@opentiny/genui-sdk-angular'; +import { materials } from '@opentiny/genui-sdk-materials-angular-opentiny-ng/materials'; const content = JSON.stringify({ componentName: 'Page', @@ -21,19 +22,21 @@ const content = JSON.stringify({ }); @Component({ - imports: [GenuiRenderer], + imports: [GenuiConfigProvider, GenuiRenderer], template: ` -
-
-
默认缓冲字段
- + +
+
+
默认缓冲字段
+ +
+
+
自定义缓冲字段: 拦截文本内容
+ +
-
-
自定义缓冲字段: 拦截文本内容
- -
-
+ `, styles: [ ` @@ -58,6 +61,7 @@ const content = JSON.stringify({ ] }) export class GenuiExample { + activeMaterials = materials; generating = false; requiredCompleteFieldSelectors = [ '[componentName=Text] > props > text', diff --git a/docs/demos/angular/renderer/state.ts b/docs/demos/angular/renderer/state.ts index 598e2582..69d352ce 100644 --- a/docs/demos/angular/renderer/state.ts +++ b/docs/demos/angular/renderer/state.ts @@ -1,13 +1,17 @@ import { Component } from '@angular/core'; -import { GenuiRenderer } from '@opentiny/genui-sdk-angular'; +import { GenuiConfigProvider, GenuiRenderer } from '@opentiny/genui-sdk-angular'; +import { materials } from '@opentiny/genui-sdk-materials-angular-opentiny-ng/materials'; @Component({ - imports: [GenuiRenderer], + imports: [GenuiConfigProvider, GenuiRenderer], template: ` - + + + `, }) export class GenuiExample { + activeMaterials = materials; schemaContent = { componentName: 'Page', state: { diff --git a/docs/demos/chat/custom-actions.vue b/docs/demos/chat/custom-actions.vue index 0883bada..16a4a739 100644 --- a/docs/demos/chat/custom-actions.vue +++ b/docs/demos/chat/custom-actions.vue @@ -1,9 +1,12 @@ diff --git a/docs/demos/en/angular/renderer/required-complete-field-selectors.vue b/docs/demos/en/angular/renderer/required-complete-field-selectors.vue new file mode 100644 index 00000000..7412a67e --- /dev/null +++ b/docs/demos/en/angular/renderer/required-complete-field-selectors.vue @@ -0,0 +1,79 @@ + + + + diff --git a/docs/demos/en/angular/renderer/state.vue b/docs/demos/en/angular/renderer/state.vue new file mode 100644 index 00000000..8b908ef9 --- /dev/null +++ b/docs/demos/en/angular/renderer/state.vue @@ -0,0 +1,110 @@ + + + diff --git a/docs/demos/en/chat/components/assistant-footer.vue b/docs/demos/en/chat/components/assistant-footer.vue new file mode 100644 index 00000000..3bf44f13 --- /dev/null +++ b/docs/demos/en/chat/components/assistant-footer.vue @@ -0,0 +1,110 @@ + + + + + + + diff --git a/docs/demos/en/chat/components/user-footer.vue b/docs/demos/en/chat/components/user-footer.vue new file mode 100644 index 00000000..d36faa39 --- /dev/null +++ b/docs/demos/en/chat/components/user-footer.vue @@ -0,0 +1,103 @@ + + + + + + + diff --git a/docs/demos/en/chat/custom-actions.vue b/docs/demos/en/chat/custom-actions.vue new file mode 100644 index 00000000..f2be34f4 --- /dev/null +++ b/docs/demos/en/chat/custom-actions.vue @@ -0,0 +1,138 @@ + + + diff --git a/docs/demos/en/chat/custom-components.vue b/docs/demos/en/chat/custom-components.vue new file mode 100644 index 00000000..c35769d3 --- /dev/null +++ b/docs/demos/en/chat/custom-components.vue @@ -0,0 +1,71 @@ + + + diff --git a/docs/demos/en/chat/custom-examples.vue b/docs/demos/en/chat/custom-examples.vue new file mode 100644 index 00000000..ca22bd16 --- /dev/null +++ b/docs/demos/en/chat/custom-examples.vue @@ -0,0 +1,205 @@ + + + diff --git a/docs/demos/en/chat/custom-fetch.vue b/docs/demos/en/chat/custom-fetch.vue new file mode 100644 index 00000000..c2d0cce9 --- /dev/null +++ b/docs/demos/en/chat/custom-fetch.vue @@ -0,0 +1,58 @@ + + + diff --git a/docs/demos/en/chat/custom-snippets.vue b/docs/demos/en/chat/custom-snippets.vue new file mode 100644 index 00000000..91449b74 --- /dev/null +++ b/docs/demos/en/chat/custom-snippets.vue @@ -0,0 +1,228 @@ + + + diff --git a/docs/demos/en/chat/footer-toolbar.vue b/docs/demos/en/chat/footer-toolbar.vue new file mode 100644 index 00000000..d6f4bacc --- /dev/null +++ b/docs/demos/en/chat/footer-toolbar.vue @@ -0,0 +1,124 @@ + + + diff --git a/docs/demos/en/chat/history.vue b/docs/demos/en/chat/history.vue new file mode 100644 index 00000000..dab2e4b2 --- /dev/null +++ b/docs/demos/en/chat/history.vue @@ -0,0 +1,172 @@ + + + + + diff --git a/docs/demos/en/chat/image-upload.vue b/docs/demos/en/chat/image-upload.vue new file mode 100644 index 00000000..47bcd070 --- /dev/null +++ b/docs/demos/en/chat/image-upload.vue @@ -0,0 +1,71 @@ + + + + + diff --git a/docs/demos/en/config-provider/custom-theme.vue b/docs/demos/en/config-provider/custom-theme.vue new file mode 100644 index 00000000..7699cf72 --- /dev/null +++ b/docs/demos/en/config-provider/custom-theme.vue @@ -0,0 +1,74 @@ + + + + + diff --git a/docs/demos/en/config-provider/schema-renderer-theme.vue b/docs/demos/en/config-provider/schema-renderer-theme.vue new file mode 100644 index 00000000..27a16bdc --- /dev/null +++ b/docs/demos/en/config-provider/schema-renderer-theme.vue @@ -0,0 +1,153 @@ + + + + + diff --git a/docs/demos/en/config-provider/theme.vue b/docs/demos/en/config-provider/theme.vue new file mode 100644 index 00000000..c0140696 --- /dev/null +++ b/docs/demos/en/config-provider/theme.vue @@ -0,0 +1,144 @@ + + + + + diff --git a/docs/demos/en/renderer/custom-actions-form.vue b/docs/demos/en/renderer/custom-actions-form.vue new file mode 100644 index 00000000..fb71b3b8 --- /dev/null +++ b/docs/demos/en/renderer/custom-actions-form.vue @@ -0,0 +1,118 @@ + + + diff --git a/docs/demos/en/renderer/custom-actions-open-page.vue b/docs/demos/en/renderer/custom-actions-open-page.vue new file mode 100644 index 00000000..cc07e773 --- /dev/null +++ b/docs/demos/en/renderer/custom-actions-open-page.vue @@ -0,0 +1,54 @@ + + + diff --git a/docs/demos/en/renderer/required-complete-field-selectors.vue b/docs/demos/en/renderer/required-complete-field-selectors.vue new file mode 100644 index 00000000..a6f18e36 --- /dev/null +++ b/docs/demos/en/renderer/required-complete-field-selectors.vue @@ -0,0 +1,35 @@ + + + diff --git a/docs/demos/en/renderer/state.vue b/docs/demos/en/renderer/state.vue new file mode 100644 index 00000000..903f344f --- /dev/null +++ b/docs/demos/en/renderer/state.vue @@ -0,0 +1,103 @@ + + + diff --git a/docs/demos/renderer/components/user-profile.vue b/docs/demos/renderer/components/user-profile.vue index 14e48152..597d376a 100644 --- a/docs/demos/renderer/components/user-profile.vue +++ b/docs/demos/renderer/components/user-profile.vue @@ -72,4 +72,3 @@ defineProps<{ color: #666; } - diff --git a/docs/demos/renderer/custom-actions-form.vue b/docs/demos/renderer/custom-actions-form.vue index a7ecd99d..54ff8c97 100644 --- a/docs/demos/renderer/custom-actions-form.vue +++ b/docs/demos/renderer/custom-actions-form.vue @@ -1,10 +1,13 @@ - diff --git a/docs/demos/renderer/state.vue b/docs/demos/renderer/state.vue index bdbebfd5..f0d8d704 100644 --- a/docs/demos/renderer/state.vue +++ b/docs/demos/renderer/state.vue @@ -1,10 +1,13 @@ -``` - -### 2. 使用异步组件(推荐) - -如果自定义 Renderer 体积较大或需要按需加载,可以使用异步组件: - -```vue - -``` - -## 跨框架集成示例 - -### 集成 Angular Renderer - -以下示例展示了如何在 Vue 应用中集成 Angular 渲染器, `tiny-schema-renderer-element-ng`是将 Angular 渲染器包装成了 webComponent - -```vue - - - - - -``` diff --git a/docs/src/components/angular/renderer.md b/docs/src/components/angular/renderer.md index 624c26ef..7a9674f3 100644 --- a/docs/src/components/angular/renderer.md +++ b/docs/src/components/angular/renderer.md @@ -2,6 +2,40 @@ `GenuiRenderer` 是 GenUI SDK 的核心渲染组件(Renderer),用于将大模型返回的结构化 JSON Schema 渲染为可交互的 UI 界面。 +::: warning 物料配置 +`GenuiRenderer` 本身不包含 UI 物料,需配合 `GenuiConfigProvider` 的 `materials` 使用,详见 [安装与配置](../../guide/angular/install#物料配置)。 + +若需保持旧版「开箱即用」行为,请改用 `GenuiLegacyRenderer`,Input 与内容投影与 `GenuiRenderer` 完全一致。 +::: + +## 兼容组件 GenuiLegacyRenderer + +`GenuiLegacyRenderer` 内置 OpenTiny NG 默认物料,适用于未配置 `GenuiConfigProvider` 的旧项目迁移,无需额外安装物料包。 + +```ts +import { Component } from '@angular/core'; +import { GenuiLegacyRenderer } from '@opentiny/genui-sdk-angular'; + +@Component({ + imports: [GenuiLegacyRenderer], + template: ` + + `, +}) +export class GenuiExample { + schemaContent = { + componentName: 'Page', + children: [ + { + componentName: 'TiButton', + props: { color: 'primary' }, + children: [{ componentName: 'Text', props: { text: '提交' } }], + }, + ], + }; +} +``` + ## Input ### content @@ -198,7 +232,7 @@ import { MyCustomDirective } from './my-custom-directive'; @Component({ imports: [GenuiRenderer], template: ` - + `, }) export class GenuiExample { @@ -237,7 +271,7 @@ import { GenuiRenderer } from '@opentiny/genui-sdk-angular'; @Component({ imports: [GenuiRenderer], template: ` - + `, }) export class GenuiExample { @@ -263,7 +297,7 @@ export class GenuiExample { }, }, showNotification: { - execute: (params,context) => { + execute: (params, context) => { console.log('通知:', params.message); }, }, @@ -385,8 +419,8 @@ import { GenuiRenderer } from '@opentiny/genui-sdk-angular'; ], template: ` - - 生成中 …… + + 生成中 …… 出错了! 卡片标题:{{ schema.componentName }} diff --git a/docs/src/components/chat.md b/docs/src/components/chat.md index a9c10527..ce8b0b6d 100644 --- a/docs/src/components/chat.md +++ b/docs/src/components/chat.md @@ -4,6 +4,33 @@ 仅使用 Chat 时可从 `@opentiny/genui-sdk-vue/chat` 按需引入,见 [快速开始 - 按需引入](../guide/quick-start#按需引入)。 +## 兼容组件 GenuiLegacyChat + +::: warning 物料配置(自 1.3.0 起) +`GenuiChat` 进行了物料解耦重构, 组件已不包含任何组件物料, 需要使用 `GenuiConfigProvider` 注入。 + +若需保持旧版行为,请改用 `GenuiLegacyChat`(`@opentiny/genui-sdk-vue/legacy-chat`)。 +::: + +`GenuiLegacyChat` 内置 OpenTiny 默认物料,适用于未配置 `GenuiConfigProvider` 的旧项目迁移。 + +```vue + + + +``` + ## Props ### url @@ -482,7 +509,29 @@ interface IChatConfig { interface ICustomComponentItem extends IGenPromptComponent { ref?: Component; // 组件引用,用于传给 GenuiRenderer } +``` +### ICustomActionItem + +```typescript +interface ICustomActionItem extends IGenPromptAction { + execute: (params: any, context: Record) => any; +} + +interface IGenPromptAction { + name: string; + description?: string; + parameters?: JSONSchema; + /** 返回值 JSON Schema 描述(可选,无返回值时可省略) */ + return?: JSONSchema; + /** 是否为异步 Action(可选,默认为 false) */ + async?: boolean; +} +``` + +### IGenPromptComponent + +```typescript interface IGenPromptComponent { component: string; // 组件名 schema: { diff --git a/docs/src/components/config-provider.md b/docs/src/components/config-provider.md index 53200dcf..3f49514f 100644 --- a/docs/src/components/config-provider.md +++ b/docs/src/components/config-provider.md @@ -1,6 +1,6 @@ # GenuiConfigProvider 组件 -`GenuiConfigProvider` 用于为渲染器提供主题能力,并将主题样式限定在特定作用域内。 +`GenuiConfigProvider` 用于为渲染器提供主题、国际化与物料配置能力,并将主题样式限定在特定作用域内。 仅使用 ConfigProvider 时可从 `@opentiny/genui-sdk-vue/config-provider` 按需引入,见 [快速开始 - 按需引入](../guide/quick-start#按需引入)。 @@ -97,6 +97,29 @@ const customI18n: I18nMessages = { 查看 [GenuiConfigProvider 组件 - 国际化配置](../examples/config-provider/i18n) 了解详细用法 +### materials + +- **类型**: `IMaterials` +- **必填**: 否(使用 `GenuiRenderer` / `GenuiChat` 时需要配置) +- **说明**: 渲染器使用的组件物料。通常传入物料包,例如 `@opentiny/genui-sdk-materials-vue-opentiny-vue` 提供的 `materials` 对象。 + +```vue + + + +``` + ## Slots `GenuiConfigProvider` 使用默认插槽包裹子组件。 diff --git a/docs/src/components/core/api.md b/docs/src/components/core/api.md new file mode 100644 index 00000000..f2529b4e --- /dev/null +++ b/docs/src/components/core/api.md @@ -0,0 +1,489 @@ +# API 文档 + +`@opentiny/genui-sdk-core` 提供 GenUI SDK 的核心能力:协议类型、Prompt 生成、流式 Schema 提取、增量 Patch、JSON 修复等,供 Vue / Angular / Server 等上层包依赖。 + +## 公共方法 + +### genPrompt() + +根据框架、物料元数据和自定义配置,拼接完整的 System Prompt。 + +- **类型** + +```typescript +function genPrompt( + framework: IGenPromptFramework | IGenPromptFrameworkConfig, + materialsMeta: IMaterialsMeta, + tgCustomConfig?: IGenPromptCustomConfig, + options?: IGenPromptOptions, +): string +``` + +- **参数** + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `framework` | `IGenPromptFramework` \| `IGenPromptFrameworkConfig` | 是 | 框架名(如 `'Vue'`)或自定义框架配置;传字符串时会合并该框架默认 rules | +| `materialsMeta` | [`IMaterialsMeta`](#imaterialsmeta) | 是 | 物料元数据,通常从物料包的 `meta` 入口引入 | +| `tgCustomConfig` | [`IGenPromptCustomConfig`](#igenpromptcustomconfig) | 否 | 自定义组件、Snippets、示例、Action | +| `options` | [`IGenPromptOptions`](#igenpromptoptions) | 否 | 控制 Prompt 各段落是否生成,以及额外 rules | + +- **返回值**: `string` — 拼接后的 System Prompt + +- **详细信息** + +Prompt 通常包含:前缀、可用组件、JSON Schema、示例、Snippets、About This、Actions、生成规则。各段落可通过 `options` 开关裁剪。 + +- **示例** + +```typescript +import { genPrompt } from '@opentiny/genui-sdk-core'; +import { materialsMeta } from '@opentiny/genui-sdk-materials-vue-opentiny-vue/meta'; + +const prompt = genPrompt( + 'Vue', + materialsMeta, + { + customActions: [ + { + name: 'openPage', + description: 'Open a page by path', + parameters: { + type: 'object', + properties: { + path: { type: 'string' }, + }, + required: ['path'], + }, + }, + ], + }, + { includeJsonSchema: false }, +); +``` + +### PatternExtractor + +基于正则状态机,将流式文本拆分为普通内容与被标记包裹的内容。 + +- **类型** + +```typescript +class PatternExtractor { + constructor(config: { + onNormalWrite: (value: string) => void; + onHandledWrite: (value: string) => void; + keepFlag?: false | 'handling' | 'normal'; + regExpMap?: Record>; + }) + + setState(state: 'handling' | 'normal'): void + reset(): void + handleContent(content: string): string +} +``` + +- **参数**(构造函数 `config`) + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `onNormalWrite` | `(value: string) => void` | 是 | 收到普通文本片段时的回调 | +| `onHandledWrite` | `(value: string) => void` | 是 | 收到标记内(如 schemaJson)文本片段时的回调 | +| `keepFlag` | `false` \| `'handling'` \| `'normal'` | 否 | 标记本身是否写入对应流;默认不写入 | +| `regExpMap` | `Record>` | 否 | 自定义起止正则;默认使用 [`SchemaJsonPattern`](#schemajsonpattern) | + +- **详细信息** + +`handleContent` 处理增量文本,并通过回调分别输出普通流与 handled 流。 + +- **示例** + +```typescript +import { PatternExtractor } from '@opentiny/genui-sdk-core'; + +const extractor = new PatternExtractor({ + onNormalWrite: (chunk) => console.log('markdown:', chunk), + onHandledWrite: (chunk) => console.log('schemaJson:', chunk), +}); + +extractor.handleContent('hello ```schemaJson\n{"componentName":"Page"'); +extractor.handleContent('\n}\n```'); +``` + +### SchemaJsonPattern + +提供 schemaJson 代码块的完整 / 部分匹配正则。 + +- **类型** + +```typescript +class SchemaJsonPattern { + get regExpMap(): { + start: { full: RegExp; partial: RegExp }; + end: { full: RegExp; partial: RegExp }; + } +} +``` + +默认 start 标记为 `` ```schemaJson ``,end 标记为 `` ``` ``。可将 `regExpMap` 注入到 `PatternExtractor`。另导出 `getPartialStartRegString(flag: string): string`,用于生成可能被截断的部分匹配正则。 + +### StreamPatternExtractor + +基于 Web Streams 的封装,将输入流拆成 `normalStream` 与 `handledStream`。 + +- **类型** + +```typescript +class StreamPatternExtractor { + static separate( + stream: ReadableStream, + ): [ReadableStream, ReadableStream] + + get normalStream(): ReadableStream + get handledStream(): ReadableStream + handleStream(stream: ReadableStream): Promise +} +``` + +- **示例** + +```typescript +import { StreamPatternExtractor } from '@opentiny/genui-sdk-core'; + +const [normal, handled] = StreamPatternExtractor.separate(inputStream); +``` + +### DeltaPatcher + +基于 `jsondiffpatch` 对 Schema 做增量合并,并结合缓冲字段选择器过滤不完整字段。 + +- **类型** + +```typescript +class DeltaPatcher { + constructor(options?: IPatchOptions) + patchWithDelta(oldValue: Object, newValue: Object, isCompleted: boolean): Object +} +``` + +- **参数** + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `options` | [`IPatchOptions`](#ipatchoptions) | 否 | 构造时传入缓冲字段选择器等配置 | +| `oldValue` | `Object` | 是 | 当前已合并的 Schema | +| `newValue` | `Object` | 是 | 新的 Schema 快照 | +| `isCompleted` | `boolean` | 是 | 流是否结束;为 `true` 时直接全量 patch,不再缓冲 | + +- **详细信息** + +流式未完成时,命中 `requiredCompleteFieldSelectors` 的路径会被缓冲,直到字段完整再写入。选择器语法与 [`matchJsonPath`](#matchjsonpath) / [`jsonSelectorMatcher`](#jsonselectormatcher) 一致。 + +查看 [Renderer 配置缓冲字段](../../examples/renderer/required-complete-field-selectors) 了解选择器语法与默认规则。 + +- **示例** + +```typescript +import { DeltaPatcher } from '@opentiny/genui-sdk-core'; + +const patcher = new DeltaPatcher({ + requiredCompleteFieldSelectors: [ + '[componentName=TinyForm] > props > labelPosition', + ], +}); + +const schema = {}; +patcher.patchWithDelta(schema, partialSchema, false); +patcher.patchWithDelta(schema, finalSchema, true); +``` + +### matchJsonPath() + +判断 JSON 某路径是否匹配 CSS-like 选择器。 + +- **类型** + +```typescript +function matchJsonPath( + json: Record, + selector: string, + path: string, +): boolean +``` + +- **参数** + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `json` | `Record` | 是 | 完整 JSON 对象,用于解析属性选择器等上下文 | +| `selector` | `string` | 是 | CSS-like 选择器 | +| `path` | `string` | 是 | 待匹配的 JSON 路径 | + +支持属性选择器 `[key=val]`、`^=` / `$=` / `*=`,伪类 `:empty` / `:object` / `:array` / `:string` / `:number` / `:boolean` / `:null`,以及子组合 `>`。 + +查看 [Renderer 配置缓冲字段](../../examples/renderer/required-complete-field-selectors) 了解详细用法和选择器语法。 + +### jsonSelectorMatcher() + +判断 delta 路径是否命中缓冲字段选择器,并返回最长匹配路径。 + +- **类型** + +```typescript +function jsonSelectorMatcher( + json: Record, + selector: string, + lastDeltaKeys: string, +): { isMatch: boolean; matchPath: string } +``` + +- **参数** + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `json` | `Record` | 是 | 当前 JSON 对象 | +| `selector` | `string` | 是 | 缓冲字段选择器 | +| `lastDeltaKeys` | `string` | 是 | 本次 delta 变更的路径 | + +主要由 `DeltaPatcher` 内部使用,也可在自定义 patch 逻辑中复用。另导出 `findGroupSelector`、`matchSelector`、`matchGroup`。 + +查看 [Renderer 配置缓冲字段](../../examples/renderer/required-complete-field-selectors) 了解选择器语法。 + +### repairJson() + +尝试解析或修复不完整 / 格式错误的 JSON 字符串。 + +- **类型** + +```typescript +function repairJson(jsonString: string | undefined): { + state: RepairJsonState; + value: any | undefined; +} + +function safeJsonParse(jsonString: string): any | undefined +``` + +- **参数** + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `jsonString` | `string` \| `undefined` | 是 | 待解析或修复的 JSON 文本 | + +- **返回值** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `state` | [`RepairJsonState`](#repairjsonstate) | 解析结果状态 | +| `value` | `any` \| `undefined` | 解析成功时的对象;失败为 `undefined` | + +- **详细信息** + +先 `JSON.parse`;失败则先进行结构修复再走 `jsonrepair`。`safeJsonParse` 仅做安全解析,失败返回 `undefined`。 + +- **示例** + +```typescript +import { repairJson, RepairJsonState } from '@opentiny/genui-sdk-core'; + +const { state, value } = repairJson('{"componentName":"Page"'); +if (state === RepairJsonState.SUCCESS || state === RepairJsonState.REPAIRED) { + console.log(value); +} +``` + +### buildMaterialDefaultValueMap() + +从物料元数据中提取组件属性默认值映射,供 Renderer 合并默认 Props。 + +- **类型** + +```typescript +function buildMaterialDefaultValueMap( + materialsMeta?: Partial, +): MaterialDefaultValueMap +``` + +- **参数** + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `materialsMeta` | `Partial` | 否 | 物料元数据,见 [`IMaterialsMeta`](#imaterialsmeta);缺省时返回空映射 | + +- **返回值**: [`MaterialDefaultValueMap`](#materialdefaultvaluemap) + +## 类型 + +### CardSchema + +```typescript +interface JSExpression { + type: 'JSExpression'; + value: string; + model?: boolean; +} + +interface JSFunction { + type: 'JSFunction'; + value: string; + params?: string[]; +} + +type Methods = Record; + +interface LifeCycles { + onMounted?: JSFunction; + onUnmounted?: JSFunction; +} + +interface Node { + id?: string; + componentName: string; + props?: Record; + children?: Node[] | string; + componentType?: 'Block' | 'PageStart' | 'PageSection'; + slot?: string | Record; + params?: string[]; + loop?: Record; + loopArgs?: string[]; + condition?: boolean | Record; +} + +type RootNode = Omit & { + css?: string; + state?: Record; + methods?: Methods; + lifeCycles?: LifeCycles; +}; + +type CardSchema = RootNode; +type NodeSchema = Node; +``` + +### IChatMessage + +```typescript +interface IStreamDelta { + reasoning_content?: string | null; + content?: string | null; + tool_calls?: any; + tool_calls_result?: any; +} + +type IMessageItem = + | { type: 'schema-card'; content: any; id?: string; state?: Record } + | { type: 'markdown'; content: string } + | { type: 'reasoning'; content: string; thinking?: boolean } + | { type: 'tool'; name: string; status: string; content?: any; [key: string]: any } + | { type: string; content: any; [customKey: string]: any }; + +interface IChatMessage { + role: 'assistant'; + content: string; + messages: IMessageItem[]; + [key: string]: any; +} +``` + +### IMaterials + +```typescript +interface IMaterials { + components?: Record; // 组件名 → 运行时组件 + requiredCompleteFieldSelectors?: string[]; // 缓冲字段选择器 + defaultPropsMap?: Record; // 组件默认 Props 映射 + [key: string]: any; +} +``` + +### IMaterialsMeta + +```typescript +interface IExample { + id?: string; + name: string; + description?: string; + schema: CardSchema; +} + +interface IMaterialsMeta { + materials: IMaterialsProtocol[]; // 物料协议数据 + examples: IExample[]; // Prompt 示例 + whiteList: string[]; // 组件白名单 + wrapperComponent?: string; // 包装组件名,默认如 TinyCard + rules?: string[]; // 物料侧生成规则 +} +``` + +### MaterialDefaultValueMap + +```typescript +type MaterialDefaultValueMap = Record>; // 组件名 → 属性默认值 +``` + +### IGenPromptFramework + +```typescript +type IGenPromptFramework = 'Vue' | 'React' | 'Angular' | string; +``` + +### IGenPromptFrameworkConfig + +```typescript +interface IGenPromptFrameworkConfig { + rules?: string[]; // 框架默认生成规则 +} +``` + +### IGenPromptCustomConfig + +```typescript +interface IGenPromptCustomConfig { + customComponents?: IGenPromptComponent[]; // 自定义组件描述 + customSnippets?: IGenPromptSnippet[]; // 自定义 Snippets + customExamples?: IGenPromptExample[]; // 自定义示例 + customActions?: IGenPromptAction[]; // 自定义 Action +} + +interface IGenPromptAction { + name: string; + description?: string; + parameters?: JSONSchema; + return?: JSONSchema; // 返回值 JSON Schema(可选) + async?: boolean; // 是否为异步 Action,默认 false +} +``` + +### IGenPromptOptions + +```typescript +interface IGenPromptOptions { + isSkill?: boolean; // 是否使用 Skill 模式前缀与规则,默认 false + includeJsonSchema?: boolean; // 是否包含 JSON Schema 段落,默认 true + includeSnippets?: boolean; // 是否包含 Snippets 段落,默认 true + includeExamples?: boolean; // 是否包含 Examples 段落,默认 true + includeActions?: boolean; // 是否包含 Actions 段落,默认 true + includeAboutThis?: boolean; // 是否包含 About This 段落,默认 true + includeBaseRules?: boolean; // 是否包含基础规则,默认 true + rules?: string[]; // 额外规则,会与物料 rules、框架默认 rules 合并 +} +``` + +### IPatchOptions + +```typescript +interface IPatchOptions { + requiredCompleteFieldSelectors?: string[]; // 缓冲字段选择器 +} +``` + +### RepairJsonState + +```typescript +enum RepairJsonState { + INVALID_INPUT = 'invalid-input', // 输入无效 + SUCCESS = 'success-parse', // 直接解析成功 + REPAIRED = 'repaired-parse', // 修复后解析成功 + FAILED = 'failed-repair', // 修复失败 +} +``` diff --git a/docs/src/components/materials/angular-opentiny-ng.md b/docs/src/components/materials/angular-opentiny-ng.md new file mode 100644 index 00000000..8841f4fe --- /dev/null +++ b/docs/src/components/materials/angular-opentiny-ng.md @@ -0,0 +1,40 @@ +# Angular OpenTiny NG + +`@opentiny/genui-sdk-materials-angular-opentiny-ng` 基于 [OpenTiny NG](https://opentiny.design/tiny-ng/) 的物料包,提供运行时组件映射与 Prompt 元数据。 + +类型定义见 [Core - IMaterials](../core/api#imaterials) / [IMaterialsMeta](../core/api#imaterialsmeta)。 + +## 导出 + +| 入口 | 导出 | +|------|------| +| `.` | `materials`、`materialsMeta` | +| `./materials` | `materials` | +| `./meta` | `materialsMeta` | + +## materials + +- **类型**: `IMaterials` +- **说明**: OpenTiny NG 组件映射,注入 ConfigProvider。 + +```typescript +import { materials } from '@opentiny/genui-sdk-materials-angular-opentiny-ng/materials'; +``` + +```html + + + +``` + +## materialsMeta + +- **类型**: `IMaterialsMeta` +- **说明**: 供服务端 [`genPrompt`](../core/api#genprompt) 使用。`wrapperComponent` 默认为 `TiCard`。 + +```typescript +import { genPrompt } from '@opentiny/genui-sdk-core'; +import { materialsMeta } from '@opentiny/genui-sdk-materials-angular-opentiny-ng/meta'; + +const systemPrompt = genPrompt('Angular', materialsMeta); +``` diff --git a/docs/src/components/materials/vue-element-plus.md b/docs/src/components/materials/vue-element-plus.md new file mode 100644 index 00000000..b0135b2d --- /dev/null +++ b/docs/src/components/materials/vue-element-plus.md @@ -0,0 +1,42 @@ +# Vue Element Plus + +`@opentiny/genui-sdk-materials-vue-element-plus` 基于 [Element Plus](https://element-plus.org/) 的物料包,提供运行时组件映射与 Prompt 元数据。 + +类型定义见 [Core - IMaterials](../core/api#imaterials) / [IMaterialsMeta](../core/api#imaterialsmeta)。 + +## 导出 + +| 入口 | 导出 | +|------|------| +| `.` | `materials`、`materialsMeta` | +| `./materials` | `materials` | +| `./meta` | `materialsMeta` | + +## materials + +- **类型**: `IMaterials` +- **说明**: Element Plus 组件映射,注入 [GenuiConfigProvider](../config-provider#materials)。 + +```typescript +import 'element-plus/dist/index.css'; +import { materials } from '@opentiny/genui-sdk-materials-vue-element-plus/materials'; +import { GenuiChat, GenuiConfigProvider } from '@opentiny/genui-sdk-vue'; +``` + +```vue + + + +``` + +## materialsMeta + +- **类型**: `IMaterialsMeta` +- **说明**: 供服务端 [`genPrompt`](../core/api#genprompt) 使用。`wrapperComponent` 默认为 `ElCard`。 + +```typescript +import { genPrompt } from '@opentiny/genui-sdk-core'; +import { materialsMeta } from '@opentiny/genui-sdk-materials-vue-element-plus/meta'; + +const systemPrompt = genPrompt('Vue', materialsMeta); +``` diff --git a/docs/src/components/materials/vue-opentiny-vue.md b/docs/src/components/materials/vue-opentiny-vue.md new file mode 100644 index 00000000..db2b4702 --- /dev/null +++ b/docs/src/components/materials/vue-opentiny-vue.md @@ -0,0 +1,41 @@ +# Vue OpenTiny Vue + +`@opentiny/genui-sdk-materials-vue-opentiny-vue` 基于 [OpenTiny Vue](https://opentiny.design/tiny-vue/) 的物料包,提供运行时组件映射与 Prompt 元数据。 + +类型定义见 [Core - IMaterials](../core/api#imaterials) / [IMaterialsMeta](../core/api#imaterialsmeta)。 + +## 导出 + +| 入口 | 导出 | +|------|------| +| `.` | `materials`、`miniMaterials`、`materialsMeta`、`miniMaterialsMeta` | +| `./materials` | `materials`、`miniMaterials` | +| `./meta` | `materialsMeta`、`miniMaterialsMeta` | + +## materials / miniMaterials + +- **类型**: `IMaterials` +- **说明**: OpenTiny Vue 组件映射,注入 [GenuiConfigProvider](../config-provider#materials)。`miniMaterials` 为精简组件集(不含图表等)。 + +```typescript +import { materials } from '@opentiny/genui-sdk-materials-vue-opentiny-vue/materials'; +import { GenuiChat, GenuiConfigProvider } from '@opentiny/genui-sdk-vue'; +``` + +```vue + + + +``` + +## materialsMeta / miniMaterialsMeta + +- **类型**: `IMaterialsMeta` +- **说明**: 供服务端 [`genPrompt`](../core/api#genprompt) 使用。`wrapperComponent` 默认为 `TinyCard`。`miniMaterialsMeta` 对应精简组件的物料。 + +```typescript +import { genPrompt } from '@opentiny/genui-sdk-core'; +import { materialsMeta } from '@opentiny/genui-sdk-materials-vue-opentiny-vue/meta'; + +const systemPrompt = genPrompt('Vue', materialsMeta); +``` diff --git a/docs/src/components/renderer.md b/docs/src/components/renderer.md index 6efc628f..26aa18c4 100644 --- a/docs/src/components/renderer.md +++ b/docs/src/components/renderer.md @@ -4,6 +4,36 @@ 仅使用 Renderer 时可从 `@opentiny/genui-sdk-vue/renderer` 按需引入,见 [快速开始 - 按需引入](../guide/quick-start#按需引入)。 +## 兼容组件 GenuiLegacyRenderer + +::: warning 物料配置(自 1.3.0 起) +`GenuiRenderer` 进行了物料解耦重构,组件已不包含任何组件物料,需要使用 `GenuiConfigProvider` 注入。 + +若需保持旧版行为,请改用 `GenuiLegacyRenderer`(`@opentiny/genui-sdk-vue/legacy-renderer`)。 +::: + +`GenuiLegacyRenderer` 内置 OpenTiny 默认物料,适用于未配置 `GenuiConfigProvider` 的旧项目迁移。 + +```vue + + + +``` + ## Props ### content diff --git a/docs/src/components/server/cli.md b/docs/src/components/server/cli.md index 44b1019a..4704a20d 100644 --- a/docs/src/components/server/cli.md +++ b/docs/src/components/server/cli.md @@ -32,7 +32,7 @@ genui-sdk-server 服务启动成功后,会输出服务器地址,例如: -``` +```text genui-sdk-server is running on http://localhost:3100 ``` diff --git a/docs/src/en/components/angular/renderer.md b/docs/src/en/components/angular/renderer.md new file mode 100644 index 00000000..7f232760 --- /dev/null +++ b/docs/src/en/components/angular/renderer.md @@ -0,0 +1,485 @@ +# GenuiRenderer Component + +`GenuiRenderer` is the core rendering component (Renderer) of GenUI SDK. It renders structured JSON Schema returned by large language models into interactive UI. + +::: warning Materials required +`GenuiRenderer` does not include UI materials. Use it with `GenuiConfigProvider`'s `materials` prop. See [Installation](../../guide/angular/install#materials-configuration). + +For the previous out-of-the-box behavior, use `GenuiLegacyRenderer` instead. Its inputs and content projection are identical to `GenuiRenderer`. +::: + +## Compatibility Component: GenuiLegacyRenderer + +`GenuiLegacyRenderer` bundles OpenTiny NG default materials. Use it when migrating old projects that did not configure `GenuiConfigProvider`. No extra materials package is required. + +```ts +import { Component } from '@angular/core'; +import { GenuiLegacyRenderer } from '@opentiny/genui-sdk-angular'; + +@Component({ + imports: [GenuiLegacyRenderer], + template: ` + + `, +}) +export class GenuiExample { + schemaContent = { + componentName: 'Page', + children: [ + { + componentName: 'TiButton', + props: { color: 'primary' }, + children: [{ componentName: 'Text', props: { text: 'Submit' } }], + }, + ], + }; +} +``` + +## Input + +### content + +- **Type**: `string | object` +- **Required**: Yes +- **Description**: Schema content as a string or object. When a string is passed, the component attempts to parse "partial JSON" and auto-complete it, supporting streaming updates. + +```ts +import { Component } from '@angular/core'; +import { GenuiRenderer } from '@opentiny/genui-sdk-angular'; + +@Component({ + imports: [GenuiRenderer], + template: ` + + `, +}) +export class GenuiExample { + schemaContent = { + componentName: 'Page', + children: [ + { + componentName: 'Text', + props: { + text: 'Hello World', + }, + }, + ], + }; +} +``` + +### isJsonComplete + +- **Type**: `boolean` +- **Required**: No +- **Description**: Applies only when `content` is a JSON object. Marks whether the current JSON is complete, helping the buffer logic determine whether values are complete. + +```ts +import { Component } from '@angular/core'; +import { GenuiRenderer } from '@opentiny/genui-sdk-angular'; + +@Component({ + imports: [GenuiRenderer], + template: ` + + `, +}) +export class GenuiExample { + schemaContent = { + componentName: 'Page', + children: [ + { + componentName: 'Text', + props: { + text: 'Hello World', + style: 'color:' + }, + }, + ], + }; + isJsonComplete = false; +} +``` + +### generating + +- **Type**: `boolean` +- **Required**: No +- **Description**: Indicates whether the current conversation is still generating. Used to control UI loading state. + +```ts + +import { Component } from '@angular/core'; +import { GenuiRenderer } from '@opentiny/genui-sdk-angular'; + +@Component({ + imports: [GenuiRenderer], + template: ` + + `, +}) +export class GenuiExample { + schemaContent = { + componentName: 'Page', + children: [ + { + componentName: 'Text', + props: { + text: 'Hello World', + }, + }, + ], + }; + isGenerating = true; +} +``` + +### customComponents + +- **Type**: `Record>` +- **Required**: No +- **Description**: Custom component map for extending the available component list. + +```ts +import { Component } from '@angular/core'; +import { GenuiRenderer } from '@opentiny/genui-sdk-angular'; +import { MyCustomComponent } from './my-custom-component'; + +@Component({ + imports: [GenuiRenderer], + template: ` + + `, +}) +export class GenuiExample { + schemaContent = { + componentName: 'Page', + children: [ + { + componentName: 'MyCustomComponent', + props: { + foo: 'bar', + }, + }, + ], + }; + customComponents = { + MyCustomComponent: MyCustomComponent, + // ... + }; +} +``` + +#### Notes + +- Non-`standalone` components must be used together with `customComponentsModule`. +- Component metadata must be sent to the backend service when calling the API so the model can generate matching protocol JSON for the component. +- ⚠️ Limitation: Dynamic rendering does not currently support components queried via `@ContentChild` or `@ContentChildren`. + + +### customComponentsModule + +- **Type**: `Record>` +- **Required**: No +- **Description**: Module map for custom components. Required when using non-`standalone` components. + +```ts +import { Component } from '@angular/core'; +import { GenuiRenderer } from '@opentiny/genui-sdk-angular'; +import { MyCustomModule, MyCustomComponent } from './my-custom-module'; + +@Component({ + imports: [GenuiRenderer], + template: ` + + `, +}) +export class GenuiExample { + schemaContent = { + componentName: 'Page', + children: [ + { + componentName: 'MyCustomComponent', + props: { + foo: 'bar', + }, + }, + ], + }; + customComponents = { + MyCustomComponent: MyCustomComponent, + // ... + }; + customComponentsModule = { + MyCustomComponent: MyCustomModule, + // ... + } +} +``` + +### customDirectives + +- **Type**: `Record>` +- **Required**: No +- **Description**: Directive map for extending the available directive list. + +```ts +import { Component } from '@angular/core'; +import { GenuiRenderer } from '@opentiny/genui-sdk-angular'; +import { MyCustomDirective } from './my-custom-directive'; + +@Component({ + imports: [GenuiRenderer], + template: ` + + `, +}) +export class GenuiExample { + schemaContent = { + componentName: 'Page', + children: [ + { + componentName: 'div', + directives: [ + { + directiveName: 'MyCustomDirective' + } + ] + }, + ], + }; + customDirectives = { + MyCustomDirective: MyCustomDirective, + // ... + }; +} +``` + +⚠️ Limitation: Due to the `ViewContainerRef.createComponent` API, only `standalone` directives are currently supported. + +### customActions + +- **Type**: `Record void }>` +- **Required**: No +- **Description**: Custom action map defining actions that can be invoked from components. + +```ts +import { Component } from '@angular/core'; +import { GenuiRenderer } from '@opentiny/genui-sdk-angular'; + +@Component({ + imports: [GenuiRenderer], + template: ` + + `, +}) +export class GenuiExample { + schemaContent = { + componentName: 'Page', + children: [ + { + componentName: 'Text', + props: { + text: 'Hello World', + onClick: { + type: 'JSFunction', + value: 'function() { this.callAction(\'showNotification\', { message: \'User clicked HelloWorld\'})}' + } + }, + }, + ], + }; + customActions = { + openPage: { + execute: (params, context) => { + window.open(params.url, params.target || '_self'); + }, + }, + showNotification: { + execute: (params, context) => { + console.log('Notification:', params.message); + }, + }, + }; +} + +``` + + +See [Renderer - Custom Actions](../../examples/angular/renderer/custom-actions) for detailed usage. + +### requiredCompleteFieldSelectors + +- **Type**: `string[]` +- **Required**: No +- **Description**: Specifies which field paths must be complete before updates are applied. Used to control buffering strategy during streaming updates. + +```ts +import { Component } from '@angular/core'; +import { GenuiRenderer } from '@opentiny/genui-sdk-angular'; + +@Component({ + imports: [GenuiRenderer], + template: ` + + `, +}) +export class GenuiExample { + schemaContent = { + componentName: 'Page', + children: [ + { + componentName: 'Text', + props: { + text: 'Hello World', + onClick: { + type: 'JSFunction', + value: 'function() { this.callAction(\'showNotification\', { message: \'User clicked HelloWorld\'})}' + } + }, + }, + ], + }; + + requiredCompleteFieldSelectors = [ + '[componentName=Text] > props > onClick' + ]; +} +``` + +See [Renderer - Buffer Field Configuration](../../examples/angular/renderer/required-complete-field-selectors) for detailed usage. + +### state + +- **Type**: `Record` +- **Required**: No +- **Description**: Global state passed to the renderer, accessible in components via context. + +```ts +import { Component } from '@angular/core'; +import { GenuiRenderer } from '@opentiny/genui-sdk-angular'; + +@Component({ + imports: [GenuiRenderer], + template: ` + + `, +}) +export class GenuiExample { + schemaContent = { + componentName: 'Page', + state: { + userId: null, + userName: '' + }, + children: [ + { + componentName: 'Text', + props: { + text: { + type: 'JSExpression', + value: 'this.state.userName' + } + }, + }, + ], + }; + + // Restore from some history record + state = this.getFromCache(); + getFromCache() { + return { + userId: 123, + userName: 'John' + } + } +} + +``` + +See [Renderer - Passing and Merging State](../../examples/angular/renderer/state) for detailed usage. + +## Template + +### header + +- **Context**: `{ schema: CardSchema, isError: boolean, isFinished: boolean }` +- **Description**: Custom renderer header content + +```ts +import { Component } from '@angular/core'; +import { CommonModule } from '@angular/common' +import { GenuiRenderer } from '@opentiny/genui-sdk-angular'; + +@Component({ + imports: [ + CommonModule, + GenuiRenderer + ], + template: ` + + + Generating... + Error! + Card title: {{ schema.componentName }} + + + `, +}) +export class GenuiExample { + schemaContent = { + componentName: 'Page', + children: [ + { + componentName: 'Text', + props: { + text: 'Hello World', + }, + }, + ], + }; +} +``` + +### footer + +- **Parameters**: `{ schema: CardSchema, isError: boolean, isFinished: boolean }` +- **Description**: Custom renderer footer content + +```ts +import { Component } from '@angular/core'; +import { CommonModule } from '@angular/common' +import { GenuiRenderer } from '@opentiny/genui-sdk-angular'; + +@Component({ + imports: [ + CommonModule, + GenuiRenderer + ], + template: ` + + + + + + `, +}) +export class GenuiExample { + schemaContent = { + componentName: 'Page', + children: [ + { + componentName: 'Text', + props: { + text: 'Hello World', + }, + }, + ], + }; + handlePrint(schema: any) { + console.log(schema); + } +} +``` + diff --git a/docs/src/en/components/chat.md b/docs/src/en/components/chat.md new file mode 100644 index 00000000..377ff8dd --- /dev/null +++ b/docs/src/en/components/chat.md @@ -0,0 +1,647 @@ +# GenuiChat Component + +`GenuiChat` is an integrated TinyRobot chat component that wraps session management, streaming responses, generation state, and more, providing an out-of-the-box chat experience. + +When using only Chat, you can import it on demand from `@opentiny/genui-sdk-vue/chat`. See [Quick Start - Subpath Imports](../guide/quick-start#subpath-imports). + +## Compatibility Component: GenuiLegacyChat + +::: warning Materials configuration (since v1.3.0) +`GenuiChat` has been refactored to decouple materials. The component no longer includes any UI materials; inject them via `GenuiConfigProvider`. + +For previous behavior, use `GenuiLegacyChat` (`@opentiny/genui-sdk-vue/legacy-chat`) instead. +::: + +`GenuiLegacyChat` bundles OpenTiny default materials. Use it when migrating old projects that did not configure `GenuiConfigProvider`. + +```vue + + + +``` + +## Props + +### url + +- **Type**: `string` +- **Required**: No +- **Description**: Backend service URL for requesting structured data from the large language model. + +```vue + +``` + +### model + +- **Type**: `string` +- **Required**: No +- **Description**: Large language model name. + +```vue + +``` + +### temperature + +- **Type**: `number` +- **Required**: No +- **Default**: `0.3` +- **Description**: Model temperature parameter controlling output randomness. + +```vue + +``` + +### messages + +- **Type**: `IMessage[]` +- **Required**: No +- **Default**: `[]` +- **Description**: Initial conversation context. + +```vue + + + +``` + +### chatConfig + +- **Type**: `IChatConfig` +- **Required**: No +- **Description**: Chat-related configuration. + +```vue + + + +``` + +### customComponents + +- **Type**: `ICustomComponentItem[]` +- **Required**: No +- **Description**: Custom component array. Each item includes a schema definition and an optional `ref` (component reference). + +```vue + + + +``` + +See [Chat - Custom Components](../examples/chat/custom-components) for detailed usage. + +### customSnippets + +- **Type**: `IGenPromptSnippet[]` +- **Required**: No +- **Description**: Custom snippet array providing common component composition patterns. + +```vue + + + +``` + +See [Chat - Custom Snippets](../examples/chat/custom-snippets) for detailed usage. + +### customExamples + +- **Type**: `IGenPromptExample[]` +- **Required**: No +- **Description**: Custom example array providing component usage examples. + +```vue + + + +``` + +See [Chat - Custom Examples](../examples/chat/custom-examples) for detailed usage. + +### customActions + +- **Type**: `any[]` +- **Required**: No +- **Description**: Custom action array defining actions that can be invoked from components. + +```vue + + + +``` + +See [Chat - Custom Actions](../examples/chat/custom-actions) for detailed usage. + +### rendererSlots + +- **Type**: `IRendererSlots` +- **Required**: No +- **Description**: Slots passed through to `GenuiRenderer`. + +```vue + + + +``` + +### thinkComponent + +- **Type**: `Component` +- **Required**: No +- **Description**: Custom thinking process component. + +```vue + + + +``` + +See [Chat - Custom Thinking Process](../examples/chat/thinking-process) for detailed usage and component prop types. + +### roles + +- **Type**: `IRolesConfig` +- **Required**: No +- **Description**: Custom role configuration including avatars and styles for user and assistant. + +```vue + + + +``` + +See [Chat - Custom Footer Toolbar](../examples/chat/footer-toolbar) for detailed usage. + +### features + +- **Type**: `ModelCapability` +- **Required**: No +- **Description**: Model capability configuration, such as image upload and function calling support. + +```vue + + + +``` + +See [Chat - Image Upload](../examples/chat/image-upload) for detailed usage. + +### customFetch + +- **Type**: `CustomFetch` +- **Required**: No +- **Description**: Custom fetch function for fully customizing HTTP request behavior. Useful for integrating third-party SDKs, adding authentication, handling tool calls, implementing custom streaming responses, and similar scenarios. + +```vue + + + +``` + +See [Chat - Custom Fetch](../examples/chat/custom-fetch) for detailed usage. + +### requiredCompleteFieldSelectors + +- **Type**: `string[]` +- **Required**: No +- **Description**: Buffer field selector array specifying which field paths must be complete before updates are applied. This prop is passed through to `GenuiRenderer` to prevent render errors caused by incomplete fields during streaming. + +```vue + + + +``` + +See [Renderer - Buffer Fields](../examples/renderer/required-complete-field-selectors) for detailed usage and selector syntax. + +## Slots + +### empty + +- **Description**: Custom empty state slot. Rendered when there is no conversation content, useful for welcome text, onboarding copy, or placeholder UI. +- **Slot parameters**: None + +```vue + + + +``` + +## Methods + +### getConversation + +- **Return type**: `UseConversationReturn` +- **Description**: Returns the conversation manager object for session APIs, including conversation list, current session, save/load, and related features. + +```vue + + + +``` + +See [Chat - Conversation History](../examples/chat/history) for detailed usage. + +## Types + +### IMessage + +```typescript +interface IMessage { + role: 'user' | 'assistant'; + content: string; + messages?: IMessageItem[]; // Custom display content +} + +interface IMessageItem { + type: string; + content: string; + [customKey: string]: any; +} +``` + +### IChatConfig + +```typescript +interface IChatConfig { + addToolCallContext?: boolean; // Whether to add tool call context + showThinkingResult?: boolean; // Whether to show thinking result +} +``` + +### ICustomComponentItem + +```typescript +interface ICustomComponentItem extends IGenPromptComponent { + ref?: Component; // Component reference passed to GenuiRenderer +} + +interface IGenPromptComponent { + component: string; // Component name + schema: { + properties?: IGenPromptComponentProperty[]; + events?: IGenPromptComponentEvent[]; + slots?: Record; + }; + name?: string; // Component label + description?: string; +} + +interface IGenPromptComponentProperty { + property: string; + description: string; + type: string; + required?: boolean; // Default false + defaultValue?: any; // Default empty + properties?: IGenPromptComponentProperty[]; +} + +interface IGenPromptComponentEvent { + type: string; + functionInfo?: IFunctionInfo; + defaultValue?: string; + description: string; +} + +interface IFunctionInfo { + params: IFunctionParam[]; + returns: Record; +} + +interface IFunctionParam { + name: string; + type: string; + defaultValue: string; + description: string; +} +``` + +### IGenPromptSnippet + +```typescript +type IGenPromptSnippet = NodeSchema; + +interface NodeSchema { + componentName: string; + props?: Record; + children?: NodeSchema[]; + [key: string]: any; +} +``` + +### IGenPromptExample + +```typescript +interface IGenPromptExample { + name: string; + description?: string; + schema: CardSchema; +} + +interface CardSchema { + componentName: 'Page'; + props?: Record; + children?: NodeSchema[]; + [key: string]: any; +} +``` + +### ModelCapability + +```typescript +interface ModelCapability { + supportImage?: ImageFeatures; + supportFunctionCalling?: boolean; + [key: string]: any; +} + +interface ImageFeatures { + enabled: boolean; + maxImageSize: number; // MB + maxFilesPerRequest: number; + supportedFileTypes: string[]; +} +``` + +### IRolesConfig + +```typescript +interface IRolesConfig { + user: BubbleRoleConfig; // User role config + assistant: BubbleRoleConfig; // Assistant role config +} + +interface BubbleRoleConfig { + placement?: 'start' | 'end'; // Message bubble placement + avatar?: Component | VNode; // Avatar component + maxWidth?: string; // Maximum message width + slots?: { + // Slot config, e.g. footer toolbar + trailer?: Component; + }; +} + +interface IBubbleSlotsProps { + index: number; + bubbleProps: BubbleProps; + isFinished: boolean; + messageManager: UseMessageReturn; + chatMessage: IMessage +} +``` + +### CustomFetch + +```typescript +type CustomFetch = ( + url: string, + options: { + method: string; + headers: Record; + body: string; + signal?: AbortSignal; + }, +) => Promise | Response; +``` + +See TinyRobot documentation for `BubbleProps`, `UseConversationReturn`, and `UseMessageReturn`. + +See [BubbleProps](https://docs.opentiny.design/tiny-robot/guide/bubble.html#props) for definitions and usage. + +See [UseConversationReturn](https://docs.opentiny.design/tiny-robot/guide/conversation.html#%E8%BF%94%E5%9B%9E%E5%80%BC) for definitions and usage. + +See [UseMessageReturn](https://docs.opentiny.design/tiny-robot/guide/message.html#%E8%BF%94%E5%9B%9E%E5%80%BC) for definitions and usage. diff --git a/docs/src/en/components/config-provider.md b/docs/src/en/components/config-provider.md new file mode 100644 index 00000000..79cc011a --- /dev/null +++ b/docs/src/en/components/config-provider.md @@ -0,0 +1,190 @@ +# GenuiConfigProvider Component + +`GenuiConfigProvider` provides theme, i18n, and materials configuration for the renderer, and scopes theme styles within a specific container. + +When using only ConfigProvider, you can import it on demand from `@opentiny/genui-sdk-vue/config-provider`. See [Quick Start - Subpath Imports](../guide/quick-start#subpath-imports). + +When used with `GenuiRenderer` or `GenuiChat`, you typically need to inject component materials via the `materials` prop. See [Materials Configuration](../guide/quick-start#materials-configuration). + +## Props + +### theme + +- **Type**: `'dark' | 'lite' | 'light' | 'auto'` +- **Required**: No +- **Default**: `'light'` +- **Description**: Theme mode. + - `'dark'`: Dark theme + - `'lite'`: Lite theme + - `'light'`: Light theme + - `'auto'`: Follow the browser preference automatically + +```vue + +``` + +See [GenuiConfigProvider - Theme Switching](../examples/config-provider/theme) for detailed usage. + +### id + +- **Type**: `string` +- **Required**: No +- **Default**: `'tiny-genui-config-provider'` +- **Description**: The container element id used for style scoping. When multiple `GenuiConfigProvider` instances exist on the page, set a different id for each. + +```vue + +``` + +See [GenuiConfigProvider - Custom Theme](../examples/config-provider/custom-theme) for detailed usage. + +### locale + +- **Type**: `string` +- **Required**: No +- **Default**: `'zh_CN'` +- **Description**: Sets the component locale. Supported language codes include `'zh_CN'` (Simplified Chinese) and `'en_US'` (English). + +```vue + +``` + +### i18n + +- **Type**: `I18nMessages` +- **Required**: No +- **Default**: `undefined` +- **Description**: Custom internationalization message object. Used to override or extend default i18n text. Format: `{ [lang: string]: { [key: string]: string | I18nMessageObject } }`. + +```vue + + + +``` + +See [GenuiConfigProvider - i18n Configuration](../examples/config-provider/i18n) for detailed usage. + +### materials + +- **Type**: `IMaterials` +- **Required**: No (required when using `GenuiRenderer` / `GenuiChat`) +- **Description**: Component materials for the renderer. Usually pass materials from a materials package, e.g. the `materials` object from `@opentiny/genui-sdk-materials-vue-opentiny-vue`. + +```vue + + + +``` + +## Slots + +`GenuiConfigProvider` uses the default slot to wrap child components. + +### Customizing theme for GenuiChat + +```vue + + + +``` + +### Customizing theme for GenuiRenderer + +```vue + + + +``` + +## Types + +### I18nMessages + +```typescript +type I18nMessages = { + [lang: string]: I18nMessageObject; +}; +``` + +Internationalization message object. Keys are language codes (e.g. `'zh_CN'`, `'en_US'`), and values are message objects for that language. + +### I18nMessageObject + +```typescript +type I18nMessageObject = { + [key: string]: string | I18nMessageObject; +}; +``` + +Internationalization message object structure supporting nested objects. Keys are message keys; values are strings or nested message objects. diff --git a/docs/src/en/components/core/api.md b/docs/src/en/components/core/api.md new file mode 100644 index 00000000..83299ccb --- /dev/null +++ b/docs/src/en/components/core/api.md @@ -0,0 +1,490 @@ +# API Reference + +`@opentiny/genui-sdk-core` provides the core capabilities of GenUI SDK: protocol types, prompt generation, streaming schema extraction, delta patching, JSON repair, and more. It is consumed by Vue / Angular / Server packages. + + +## Public APIs + +### genPrompt() + +Builds a full system prompt from framework, materials metadata, and optional custom config. + +- **Type** + +```typescript +function genPrompt( + framework: IGenPromptFramework | IGenPromptFrameworkConfig, + materialsMeta: IMaterialsMeta, + tgCustomConfig?: IGenPromptCustomConfig, + options?: IGenPromptOptions, +): string +``` + +- **Parameters** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `framework` | `IGenPromptFramework` \| `IGenPromptFrameworkConfig` | Yes | Framework name (e.g. `'Vue'`) or custom framework config; string form merges that framework’s default rules | +| `materialsMeta` | [`IMaterialsMeta`](#imaterialsmeta) | Yes | Materials metadata, usually imported from a materials package `meta` entry | +| `tgCustomConfig` | [`IGenPromptCustomConfig`](#igenpromptcustomconfig) | No | Custom components, snippets, examples, and actions | +| `options` | [`IGenPromptOptions`](#igenpromptoptions) | No | Toggle prompt sections and append extra rules | + +- **Returns**: `string` — the assembled system prompt + +- **Details** + +A prompt typically includes: prefix, available components, JSON Schema, examples, snippets, About This, actions, and generation rules. Use `options` to include or omit sections. + +- **Example** + +```typescript +import { genPrompt } from '@opentiny/genui-sdk-core'; +import { materialsMeta } from '@opentiny/genui-sdk-materials-vue-opentiny-vue/meta'; + +const prompt = genPrompt( + 'Vue', + materialsMeta, + { + customActions: [ + { + name: 'openPage', + description: 'Open a page by path', + parameters: { + type: 'object', + properties: { + path: { type: 'string' }, + }, + required: ['path'], + }, + }, + ], + }, + { includeJsonSchema: false }, +); +``` + +### PatternExtractor + +A regex state machine that splits streaming text into normal content and marked content. + +- **Type** + +```typescript +class PatternExtractor { + constructor(config: { + onNormalWrite: (value: string) => void; + onHandledWrite: (value: string) => void; + keepFlag?: false | 'handling' | 'normal'; + regExpMap?: Record>; + }) + + setState(state: 'handling' | 'normal'): void + reset(): void + handleContent(content: string): string +} +``` + +- **Parameters** (`config`) + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `onNormalWrite` | `(value: string) => void` | Yes | Callback for normal text chunks | +| `onHandledWrite` | `(value: string) => void` | Yes | Callback for text inside markers (e.g. schemaJson) | +| `keepFlag` | `false` \| `'handling'` \| `'normal'` | No | Whether markers themselves are written to a stream; default off | +| `regExpMap` | `Record>` | No | Custom start/end regexes; defaults to [`SchemaJsonPattern`](#schemajsonpattern) | + +- **Details** + +`handleContent` processes incremental text and emits chunks via callbacks. + +- **Example** + +```typescript +import { PatternExtractor } from '@opentiny/genui-sdk-core'; + +const extractor = new PatternExtractor({ + onNormalWrite: (chunk) => console.log('markdown:', chunk), + onHandledWrite: (chunk) => console.log('schemaJson:', chunk), +}); + +extractor.handleContent('hello ```schemaJson\n{"componentName":"Page"'); +extractor.handleContent('\n}\n```'); +``` + +### SchemaJsonPattern + +Provides full / partial match regexes for schemaJson fenced blocks. + +- **Type** + +```typescript +class SchemaJsonPattern { + get regExpMap(): { + start: { full: RegExp; partial: RegExp }; + end: { full: RegExp; partial: RegExp }; + } +} +``` + +Default start marker is `` ```schemaJson ``, end marker is `` ``` ``. Inject `regExpMap` into `PatternExtractor` when needed. Also exports `getPartialStartRegString(flag: string): string` for truncated prefix matching. + +### StreamPatternExtractor + +Web Streams wrapper that forks an input stream into `normalStream` and `handledStream`. + +- **Type** + +```typescript +class StreamPatternExtractor { + static separate( + stream: ReadableStream, + ): [ReadableStream, ReadableStream] + + get normalStream(): ReadableStream + get handledStream(): ReadableStream + handleStream(stream: ReadableStream): Promise +} +``` + +- **Example** + +```typescript +import { StreamPatternExtractor } from '@opentiny/genui-sdk-core'; + +const [normal, handled] = StreamPatternExtractor.separate(inputStream); +``` + +### DeltaPatcher + +Merges schema objects incrementally with `jsondiffpatch`, filtering incomplete fields via buffer-field selectors. + +- **Type** + +```typescript +class DeltaPatcher { + constructor(options?: IPatchOptions) + patchWithDelta(oldValue: Object, newValue: Object, isCompleted: boolean): Object +} +``` + +- **Parameters** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `options` | [`IPatchOptions`](#ipatchoptions) | No | Constructor options such as buffer-field selectors | +| `oldValue` | `Object` | Yes | Current merged schema | +| `newValue` | `Object` | Yes | New schema snapshot | +| `isCompleted` | `boolean` | Yes | Whether the stream has finished; `true` patches eagerly without buffering | + +- **Details** + +While streaming, paths matching `requiredCompleteFieldSelectors` are buffered until complete. Selector syntax matches [`matchJsonPath`](#matchjsonpath) / [`jsonSelectorMatcher`](#jsonselectormatcher). + +See [Renderer - Buffer Field Configuration](../../examples/renderer/required-complete-field-selectors) for selector syntax and defaults. + +- **Example** + +```typescript +import { DeltaPatcher } from '@opentiny/genui-sdk-core'; + +const patcher = new DeltaPatcher({ + requiredCompleteFieldSelectors: [ + '[componentName=TinyForm] > props > labelPosition', + ], +}); + +const schema = {}; +patcher.patchWithDelta(schema, partialSchema, false); +patcher.patchWithDelta(schema, finalSchema, true); +``` + +### matchJsonPath() + +Checks whether a JSON path matches a CSS-like selector. + +- **Type** + +```typescript +function matchJsonPath( + json: Record, + selector: string, + path: string, +): boolean +``` + +- **Parameters** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `json` | `Record` | Yes | Full JSON object for attribute-selector context | +| `selector` | `string` | Yes | CSS-like selector | +| `path` | `string` | Yes | JSON path to test | + +Supports attribute selectors `[key=val]`, `^=` / `$=` / `*=`, pseudos `:empty` / `:object` / `:array` / `:string` / `:number` / `:boolean` / `:null`, and child combinator `>`. + +See [Renderer - Buffer Field Configuration](../../examples/renderer/required-complete-field-selectors) for detailed usage and selector syntax. + +### jsonSelectorMatcher() + +Checks whether a delta path hits a buffer-field selector and returns the longest match path. + +- **Type** + +```typescript +function jsonSelectorMatcher( + json: Record, + selector: string, + lastDeltaKeys: string, +): { isMatch: boolean; matchPath: string } +``` + +- **Parameters** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `json` | `Record` | Yes | Current JSON object | +| `selector` | `string` | Yes | Buffer-field selector | +| `lastDeltaKeys` | `string` | Yes | Path of the current delta change | + +Used mainly by `DeltaPatcher`; reusable in custom patch logic. Also exports `findGroupSelector`, `matchSelector`, `matchGroup`. + +See [Renderer - Buffer Field Configuration](../../examples/renderer/required-complete-field-selectors) for selector syntax. + +### repairJson() + +Parses or repairs incomplete / malformed JSON strings. + +- **Type** + +```typescript +function repairJson(jsonString: string | undefined): { + state: RepairJsonState; + value: any | undefined; +} + +function safeJsonParse(jsonString: string): any | undefined +``` + +- **Parameters** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `jsonString` | `string` \| `undefined` | Yes | JSON text to parse or repair | + +- **Returns** + +| Field | Type | Description | +|-------|------|-------------| +| `state` | [`RepairJsonState`](#repairjsonstate) | Parse result state | +| `value` | `any` \| `undefined` | Parsed object on success; `undefined` on failure | + +- **Details** + +Tries `JSON.parse` first; on failure, applies structural fixes then `jsonrepair`. `safeJsonParse` only parses safely and returns `undefined` on failure. + +- **Example** + +```typescript +import { repairJson, RepairJsonState } from '@opentiny/genui-sdk-core'; + +const { state, value } = repairJson('{"componentName":"Page"'); +if (state === RepairJsonState.SUCCESS || state === RepairJsonState.REPAIRED) { + console.log(value); +} +``` + +### buildMaterialDefaultValueMap() + +Builds a component → default props map from materials metadata for Renderer default-prop merging. + +- **Type** + +```typescript +function buildMaterialDefaultValueMap( + materialsMeta?: Partial, +): MaterialDefaultValueMap +``` + +- **Parameters** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `materialsMeta` | `Partial` | No | Materials metadata, see [`IMaterialsMeta`](#imaterialsmeta); omitted yields an empty map | + +- **Returns**: [`MaterialDefaultValueMap`](#materialdefaultvaluemap) + +## Types + +### CardSchema + +```typescript +interface JSExpression { + type: 'JSExpression'; + value: string; + model?: boolean; +} + +interface JSFunction { + type: 'JSFunction'; + value: string; + params?: string[]; +} + +type Methods = Record; + +interface LifeCycles { + onMounted?: JSFunction; + onUnmounted?: JSFunction; +} + +interface Node { + id?: string; + componentName: string; + props?: Record; + children?: Node[] | string; + componentType?: 'Block' | 'PageStart' | 'PageSection'; + slot?: string | Record; + params?: string[]; + loop?: Record; + loopArgs?: string[]; + condition?: boolean | Record; +} + +type RootNode = Omit & { + css?: string; + state?: Record; + methods?: Methods; + lifeCycles?: LifeCycles; +}; + +type CardSchema = RootNode; +type NodeSchema = Node; +``` + +### IChatMessage + +```typescript +interface IStreamDelta { + reasoning_content?: string | null; + content?: string | null; + tool_calls?: any; + tool_calls_result?: any; +} + +type IMessageItem = + | { type: 'schema-card'; content: any; id?: string; state?: Record } + | { type: 'markdown'; content: string } + | { type: 'reasoning'; content: string; thinking?: boolean } + | { type: 'tool'; name: string; status: string; content?: any; [key: string]: any } + | { type: string; content: any; [customKey: string]: any }; + +interface IChatMessage { + role: 'assistant'; + content: string; + messages: IMessageItem[]; + [key: string]: any; +} +``` + +### IMaterials + +```typescript +interface IMaterials { + components?: Record; // component name → runtime component + requiredCompleteFieldSelectors?: string[]; // buffer-field selectors + defaultPropsMap?: Record; // default props map + [key: string]: any; +} +``` + +### IMaterialsMeta + +```typescript +interface IExample { + id?: string; + name: string; + description?: string; + schema: CardSchema; +} + +interface IMaterialsMeta { + materials: IMaterialsProtocol[]; // materials protocol payload + examples: IExample[]; // prompt examples + whiteList: string[]; // component whitelist + wrapperComponent?: string; // wrapper component name, e.g. TinyCard + rules?: string[]; // materials-side generation rules +} +``` + +### MaterialDefaultValueMap + +```typescript +type MaterialDefaultValueMap = Record>; // component name → prop defaults +``` + +### IGenPromptFramework + +```typescript +type IGenPromptFramework = 'Vue' | 'React' | 'Angular' | string; +``` + +### IGenPromptFrameworkConfig + +```typescript +interface IGenPromptFrameworkConfig { + rules?: string[]; // framework default generation rules +} +``` + +### IGenPromptCustomConfig + +```typescript +interface IGenPromptCustomConfig { + customComponents?: IGenPromptComponent[]; // custom component descriptions + customSnippets?: IGenPromptSnippet[]; // custom snippets + customExamples?: IGenPromptExample[]; // custom examples + customActions?: IGenPromptAction[]; // custom actions +} + +interface IGenPromptAction { + name: string; + description?: string; + parameters?: JSONSchema; + return?: JSONSchema; // return-value JSON Schema (optional) + async?: boolean; // whether the action is async, default false +} +``` + +### IGenPromptOptions + +```typescript +interface IGenPromptOptions { + isSkill?: boolean; // Skill-mode prefix and rules, default false + includeJsonSchema?: boolean; // include JSON Schema section, default true + includeSnippets?: boolean; // include Snippets section, default true + includeExamples?: boolean; // include Examples section, default true + includeActions?: boolean; // include Actions section, default true + includeAboutThis?: boolean; // include About This section, default true + includeBaseRules?: boolean; // include base rules, default true + rules?: string[]; // extra rules merged with materials and framework defaults +} +``` + +### IPatchOptions + +```typescript +interface IPatchOptions { + requiredCompleteFieldSelectors?: string[]; // buffer-field selectors +} +``` + +### RepairJsonState + +```typescript +enum RepairJsonState { + INVALID_INPUT = 'invalid-input', // invalid input + SUCCESS = 'success-parse', // parsed successfully + REPAIRED = 'repaired-parse', // parsed after repair + FAILED = 'failed-repair', // repair failed +} +``` diff --git a/docs/src/en/components/materials/angular-opentiny-ng.md b/docs/src/en/components/materials/angular-opentiny-ng.md new file mode 100644 index 00000000..08cf6d56 --- /dev/null +++ b/docs/src/en/components/materials/angular-opentiny-ng.md @@ -0,0 +1,40 @@ +# Angular OpenTiny NG + +`@opentiny/genui-sdk-materials-angular-opentiny-ng` is a materials package based on [OpenTiny NG](https://opentiny.design/tiny-ng/), providing a runtime component map and prompt metadata. + +See [Core - IMaterials](../core/api#imaterials) / [IMaterialsMeta](../core/api#imaterialsmeta) for types. + +## Exports + +| Entry | Exports | +|-------|---------| +| `.` | `materials`, `materialsMeta` | +| `./materials` | `materials` | +| `./meta` | `materialsMeta` | + +## materials + +- **Type**: `IMaterials` +- **Description**: OpenTiny NG component map for ConfigProvider. + +```typescript +import { materials } from '@opentiny/genui-sdk-materials-angular-opentiny-ng/materials'; +``` + +```html + + + +``` + +## materialsMeta + +- **Type**: `IMaterialsMeta` +- **Description**: For server-side [`genPrompt`](../core/api#genprompt). `wrapperComponent` defaults to `TiCard`. + +```typescript +import { genPrompt } from '@opentiny/genui-sdk-core'; +import { materialsMeta } from '@opentiny/genui-sdk-materials-angular-opentiny-ng/meta'; + +const systemPrompt = genPrompt('Angular', materialsMeta); +``` diff --git a/docs/src/en/components/materials/vue-element-plus.md b/docs/src/en/components/materials/vue-element-plus.md new file mode 100644 index 00000000..ab8c755a --- /dev/null +++ b/docs/src/en/components/materials/vue-element-plus.md @@ -0,0 +1,42 @@ +# Vue Element Plus + +`@opentiny/genui-sdk-materials-vue-element-plus` is a materials package based on [Element Plus](https://element-plus.org/), providing a runtime component map and prompt metadata. + +See [Core - IMaterials](../core/api#imaterials) / [IMaterialsMeta](../core/api#imaterialsmeta) for types. + +## Exports + +| Entry | Exports | +|-------|---------| +| `.` | `materials`, `materialsMeta` | +| `./materials` | `materials` | +| `./meta` | `materialsMeta` | + +## materials + +- **Type**: `IMaterials` +- **Description**: Element Plus component map for [GenuiConfigProvider](../config-provider#materials). + +```typescript +import 'element-plus/dist/index.css'; +import { materials } from '@opentiny/genui-sdk-materials-vue-element-plus/materials'; +import { GenuiChat, GenuiConfigProvider } from '@opentiny/genui-sdk-vue'; +``` + +```vue + + + +``` + +## materialsMeta + +- **Type**: `IMaterialsMeta` +- **Description**: For server-side [`genPrompt`](../core/api#genprompt). `wrapperComponent` defaults to `ElCard`. + +```typescript +import { genPrompt } from '@opentiny/genui-sdk-core'; +import { materialsMeta } from '@opentiny/genui-sdk-materials-vue-element-plus/meta'; + +const systemPrompt = genPrompt('Vue', materialsMeta); +``` diff --git a/docs/src/en/components/materials/vue-opentiny-vue.md b/docs/src/en/components/materials/vue-opentiny-vue.md new file mode 100644 index 00000000..06dd2671 --- /dev/null +++ b/docs/src/en/components/materials/vue-opentiny-vue.md @@ -0,0 +1,41 @@ +# Vue OpenTiny Vue + +`@opentiny/genui-sdk-materials-vue-opentiny-vue` is a materials package based on [OpenTiny Vue](https://opentiny.design/tiny-vue/), providing a runtime component map and prompt metadata. + +See [Core - IMaterials](../core/api#imaterials) / [IMaterialsMeta](../core/api#imaterialsmeta) for types. + +## Exports + +| Entry | Exports | +|-------|---------| +| `.` | `materials`, `miniMaterials`, `materialsMeta`, `miniMaterialsMeta` | +| `./materials` | `materials`, `miniMaterials` | +| `./meta` | `materialsMeta`, `miniMaterialsMeta` | + +## materials / miniMaterials + +- **Type**: `IMaterials` +- **Description**: OpenTiny Vue component map for [GenuiConfigProvider](../config-provider#materials). `miniMaterials` is a smaller set (without charts, etc.). + +```typescript +import { materials } from '@opentiny/genui-sdk-materials-vue-opentiny-vue/materials'; +import { GenuiChat, GenuiConfigProvider } from '@opentiny/genui-sdk-vue'; +``` + +```vue + + + +``` + +## materialsMeta / miniMaterialsMeta + +- **Type**: `IMaterialsMeta` +- **Description**: For server-side [`genPrompt`](../core/api#genprompt). `wrapperComponent` defaults to `TinyCard`. `miniMaterialsMeta` pairs with the mini whitelist and examples. + +```typescript +import { genPrompt } from '@opentiny/genui-sdk-core'; +import { materialsMeta } from '@opentiny/genui-sdk-materials-vue-opentiny-vue/meta'; + +const systemPrompt = genPrompt('Vue', materialsMeta); +``` diff --git a/docs/src/en/components/renderer.md b/docs/src/en/components/renderer.md new file mode 100644 index 00000000..f1321d96 --- /dev/null +++ b/docs/src/en/components/renderer.md @@ -0,0 +1,283 @@ +# GenuiRenderer Component + +`GenuiRenderer` is the core rendering component (Renderer) of GenUI SDK. It renders structured JSON Schema returned by large language models into interactive UI. + +When using only Renderer, you can import it on demand from `@opentiny/genui-sdk-vue/renderer`. See [Quick Start - Subpath Imports](../guide/quick-start#subpath-imports). + +## Compatibility Component: GenuiLegacyRenderer + +::: warning Materials configuration (since v1.3.0) +`GenuiRenderer` has been refactored to decouple materials. The component no longer includes any UI materials; inject them via `GenuiConfigProvider`. + +For previous behavior, use `GenuiLegacyRenderer` (`@opentiny/genui-sdk-vue/legacy-renderer`) instead. +::: + +`GenuiLegacyRenderer` bundles OpenTiny default materials. Use it when migrating old projects that did not configure `GenuiConfigProvider`. + +```vue + + + +``` + +## Props + +### content + +- **Type**: `string | object` +- **Required**: Yes +- **Description**: Schema content as a string or object. When a string is passed, the component attempts to parse "partial JSON" and auto-complete it, supporting streaming updates. + +```vue + + + +``` + +### isJsonComplete + +- **Type**: `boolean` +- **Required**: No +- **Description**: Applies only when `content` is a JSON object. Marks whether the current JSON is complete, helping the buffer logic determine whether values are complete. + +```vue + + + +``` + +### generating + +- **Type**: `boolean` +- **Required**: No +- **Description**: Indicates whether the current conversation is still generating. Used to control UI loading state. + +```vue + + + +``` + +### customComponents + +- **Type**: `Record` +- **Required**: No +- **Description**: Custom component map for extending the available component list. + +```vue + + + +``` + +See [Renderer - Custom Components](../examples/renderer/custom-components) for detailed usage. + +### customActions + +- **Type**: `Record void }>` +- **Required**: No +- **Description**: Custom action map defining actions that can be invoked from components. + +```vue + + + +``` + +See [Renderer - Custom Actions](../examples/renderer/custom-actions) for detailed usage. + +### requiredCompleteFieldSelectors + +- **Type**: `string[]` +- **Required**: No +- **Description**: Specifies which field paths must be complete before updates are applied. Used to control buffering strategy during streaming updates. + +```vue + +``` + +See [Renderer - Buffer Field Configuration](../examples/renderer/required-complete-field-selectors) for detailed usage. + +### state + +- **Type**: `Record` +- **Required**: No +- **Description**: Global state passed to the renderer, accessible in components via context. + +```vue + +``` + +See [Renderer - Passing and Merging State](../examples/renderer/state) for detailed usage. + +## Slots + +### header + +- **Parameters**: `{ schema: CardSchema, isError: boolean, isFinished: boolean }` +- **Description**: Custom renderer header content + +```vue + +``` + +### footer + +- **Parameters**: `{ schema: CardSchema, isError: boolean, isFinished: boolean }` +- **Description**: Custom renderer footer content + +```vue + +``` + +## Types + +### CardSchema + +```typescript +type CardSchema = { + id?: string; // Optional root node id + methods?: Methods; // Method collection + state?: Record; // Global state; required for two-way form binding + componentName: string; // Root component name, usually Page + props?: Record; // Root component props + componentType?: 'Block' | 'PageStart' | 'PageSection'; // Node type, usually omitted + children?: NodeSchema[]; // Root child nodes + slot?: string | JSSlot | Record; // Root slot content + loop?: Record; // Root loop render config + loopArgs?: string[]; // Root loop argument names + condition?: boolean | Record; // Root conditional render config + css?: string; // Global CSS string +}; + +type NodeSchema = { + id?: string; // Unique node id + componentName: string; // Component name + props?: Record; // Component props + children?: NodeSchema[] | string; // Child nodes or string (recursive) + componentType?: 'Block' | 'PageStart' | 'PageSection'; // Node type, usually omitted + slot?: string | JSSlot | Record; // Slot content + params?: string[]; // Parameter names + loop?: Record; // Loop render config + loopArgs?: string[]; // Loop argument names + condition?: boolean | Record; // Conditional render config +}; + +type PropValue = + | string // String + | number // Number + | boolean // Boolean + | null // null + | JSExpression // JS expression wrapper + | JSFunction // JS function wrapper + | JSSlot // Slot wrapper + | PropValue[] // Recursive array + | Record; // Recursive object + +type JSExpression = { type: 'JSExpression'; value: string; model?: boolean }; +type JSFunction = { type: 'JSFunction'; value: string; params?: string[] }; +type JSSlot = { type: 'JSSlot'; value: string | Record }; +``` + +### IRendererProps + +```typescript +interface IRendererProps { + content: string | { [prop: string]: any }; + isJsonComplete?: boolean; + generating?: boolean; + customComponents?: Record; + customActions?: Record< + string, + { + execute: (params: any, context: any) => void; + } + >; + requiredCompleteFieldSelectors?: string[]; + id?: string; + state?: Record; +} +``` diff --git a/docs/src/en/components/server/api.md b/docs/src/en/components/server/api.md new file mode 100644 index 00000000..e36ba432 --- /dev/null +++ b/docs/src/en/components/server/api.md @@ -0,0 +1,134 @@ +# API Reference + +`@opentiny/genui-sdk-server` provides component APIs for building large language model chat HTTP services. + +## startServer() + +Starts an Express HTTP server that provides chat completion services. + +- **Type** + +```typescript +function startServer(options: IStartServerOptions): void + +interface IStartServerOptions { + /** API base URL */ + baseURL: string; + /** API key */ + apiKey: string; + /** Server port, default 3100 */ + port?: number; + /** Max attempts when port is in use, default 10 */ + maxAttempts?: number; +} +``` + +- **Details** + +Creates an Express app with CORS enabled and registers the chat route (`/chat/completions`). If the specified port is in use, it automatically tries the next port (up to `maxAttempts` times). On success, the server address is printed to the console. + +- **Example** + +```typescript +import { startServer } from '@opentiny/genui-sdk-server'; + +startServer({ + port: 3100, + baseURL: 'https://api.openai.com/v1', + apiKey: '', + maxAttempts: 10, +}); +``` + +## equipChatCompletions() + +Equips an Express app with chat completion functionality by registering the route handler. + +- **Type** + +```typescript +function equipChatCompletions( + app: Express, + options: IEquipChatCompletionsOptions +): void + +interface IEquipChatCompletionsOptions { + /** Route path, e.g. '/chat/completions' */ + route: string; + /** API key */ + apiKey: string; + /** API base URL */ + baseURL: string; +} +``` + +- **Details** + +Creates a chat completion request instance, builds a request handler, and registers it on the specified route (POST). + +- **Example** + +```typescript +import express from 'express'; +import { equipChatCompletions } from '@opentiny/genui-sdk-server'; + +const app = express(); + +equipChatCompletions(app, { + route: '/chat/completions', + apiKey: '', + baseURL: 'https://api.openai.com/v1', +}); + +app.listen(3000); +``` + +## createChatCompletionHandler() + +Creates a chat completion request handler that processes HTTP requests and returns streaming responses. + +- **Type** + +```typescript +function createChatCompletionHandler( + config: IChatCompletionHandlerConfig +): { handler: (req: IncomingMessage, res: ServerResponse) => Promise } + +interface IChatCompletionHandlerConfig { + chatCompletions: ( + params: ChatCompletionCreateParamsBase, + options?: IRequestOptions + ) => Promise; +} + +interface IRequestOptions { + signal?: AbortSignal | undefined | null; +} +``` + +- **Details** + +Parses the request body (JSON), calls the chat completion API for a streaming response, converts the stream to SSE (Server-Sent Events), handles client disconnects (automatically aborting the request), and provides unified error handling and formatting. + +If the response is not streaming, an error is thrown. All errors are caught and formatted into a unified error response. If response headers have already been sent, errors are appended to the stream in SSE format. + +- **Example** + +```typescript +import { createChatCompletionHandler } from '@opentiny/genui-sdk-server'; +import { FetchChatCompletions } from '@opentiny/genui-sdk-chat-completions'; +import http from 'http'; + +const chatCompletion = new FetchChatCompletions({ + apiKey: '', + baseURL: 'https://api.openai.com/v1', +}); + +const { handler } = createChatCompletionHandler({ + chatCompletions: (params, options) => + chatCompletion.chatStream(params, options), +}); + +const server = http.createServer(handler); +server.listen(3000); +``` diff --git a/docs/src/en/components/server/cli.md b/docs/src/en/components/server/cli.md new file mode 100644 index 00000000..4c65f114 --- /dev/null +++ b/docs/src/en/components/server/cli.md @@ -0,0 +1,147 @@ +# CLI + +`genui-sdk-server` is a globally installed command-line tool for quickly starting a GenUI SDK chat completion HTTP service. It provides zero-config server startup with automatic environment variable loading and port conflict detection. + +## Installation + +Install globally via npm or yarn: + +```bash +npm install -g @opentiny/genui-sdk-server +# or +yarn global add @opentiny/genui-sdk-server +``` + +After installation, you can use the `genui-sdk-server` command in your terminal. + +## Quick Start + +1. Create a `.env` file and configure required environment variables: + +```env +BASE_URL=https://api.openai.com/v1 +API_KEY= +PORT=3100 +``` + +2. Run the command to start the service: + +```bash +genui-sdk-server +``` + +On success, the server address is printed, for example: + +```text +genui-sdk-server is running on http://localhost:3100 +``` + +## Commands + +### genui-sdk-server + +Starts the GenUI SDK chat HTTP service. + +#### Usage + +```bash +genui-sdk-server [options] +``` + +#### Options + +| Option | Short | Type | Description | +|:---|:---:|:---:|:---| +| `--envFile ` | `-e` | `string` | Custom environment file path (default: `.env` in the current directory) | +| `--port ` | `-p` | `number` | Server port (default: 3100 or `PORT` environment variable) | +| `--help` | `-h` | - | Show help information | + +#### Environment Variables + +The CLI requires the following environment variables (via `.env` file or system environment): + +| Variable | Required | Description | Default | +|:---|:---:|:---|:---| +| `BASE_URL` | Yes | API base URL, e.g. `https://api.openai.com/v1` | - | +| `API_KEY` | Yes | API key | - | +| `PORT` | No | Server port | `3100` | + +**Environment variable priority**: + +1. Command-line option `--port` (highest priority) +2. Environment variable `PORT` +3. Default value `3100` + +#### Features + +- **Automatic port detection**: If the specified port is in use, tries the next port automatically (up to 10 attempts) +- **Environment variable loading**: Supports loading configuration from `.env` files or system environment variables +- **CORS support**: CORS is enabled automatically for cross-origin requests +- **Streaming responses**: Supports SSE (Server-Sent Events) streaming format + +#### Examples + +**Basic usage** + +Start with default configuration (reads `.env` in the current directory, port 3100): + +```bash +genui-sdk-server +``` + +**Custom environment file** + +Use a custom environment file: + +```bash +genui-sdk-server -e /path/to/custom/.env +``` + +**Specify port** + +Set the server port via command-line option: + +```bash +genui-sdk-server -p 3000 +``` + +**Combined usage** + +Specify both environment file and port: + +```bash +genui-sdk-server -e /path/to/.env -p 3000 +``` + +**System environment variables** + +Use system environment variables without a `.env` file: + +```bash +# Set system environment variables +export BASE_URL=https://api.openai.com/v1 +export API_KEY= +export PORT=3000 + +# Start the service +genui-sdk-server +``` + +**Show help** + +```bash +genui-sdk-server --help +# or +genui-sdk-server -h +``` + +## Server Endpoints + +After startup, the service exposes the following endpoint: + +- **POST** `/chat/completions` - Chat completion endpoint with streaming response (SSE format) + +## Related Links + +- [Server API](./api) - Component API documentation +- [Usage Guide](../../guide/server-usage) - Detailed usage guide diff --git a/docs/src/en/examples/angular/renderer/custom-actions.md b/docs/src/en/examples/angular/renderer/custom-actions.md new file mode 100644 index 00000000..852e0761 --- /dev/null +++ b/docs/src/en/examples/angular/renderer/custom-actions.md @@ -0,0 +1,62 @@ +# Renderer - Custom Actions + +Custom actions let you implement complex interaction logic. Pair them with prompts so the LLM emits schema JSON that invokes those actions. + +## Passing customActions to the Renderer + +Pass actions via the `customActions` prop. Each action includes: + +- `name`: Action name +- `description`: Action description +- `execute`: Handler receiving `params` and `context` +- `parameters`: Optional parameter schema; the model uses it to fill `params` for `execute` + +### execute Parameters + +- `params`: Arguments passed when the action is invoked +- `context`: Renderer context (state and methods); use `context.state` for two-way bound global state + +### Example: Open a Page + + + +## Send Custom Actions to the Server + +After registering actions on the renderer, include them in chat requests so the model can generate correct action calls. + +```ts {9-31} +const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + messages: [{ role: 'user', content: userInput }], + model: 'deepseek-v3.2', + stream: true, + metadata: { + tinygenui: JSON.stringify({ + framework: 'Angular', + customActions: [ + { + name: 'openPage', + description: 'Open a page', + parameters: { + type: 'object', + properties: { + url: { + type: 'string', + description: 'URL to open', + }, + target: { + type: 'string', + description: 'Target window: _self (same tab) or _blank (new tab)', + }, + }, + required: ['url', 'target'], + }, + } + ] + }), + }, + }), +}); +``` diff --git a/docs/src/en/examples/angular/renderer/required-complete-field-selectors.md b/docs/src/en/examples/angular/renderer/required-complete-field-selectors.md new file mode 100644 index 00000000..ccf42a3b --- /dev/null +++ b/docs/src/en/examples/angular/renderer/required-complete-field-selectors.md @@ -0,0 +1,13 @@ +# Renderer - Buffered Fields + +See [Configure Buffered Fields](../../renderer/required-complete-field-selectors). + +## Custom Configuration + +### Example + + + +### Behavior + +Before configuring `requiredCompleteFieldSelectors`, `Text` content updates as the model streams partial output. After configuration, the full text is shown only when complete; text inside nested `div` elements is not affected. diff --git a/docs/src/en/examples/angular/renderer/state.md b/docs/src/en/examples/angular/renderer/state.md new file mode 100644 index 00000000..bead2151 --- /dev/null +++ b/docs/src/en/examples/angular/renderer/state.md @@ -0,0 +1,62 @@ +# Renderer - Passing and Merging State + +Use `state` to pass initial state to the renderer. It is merged into global state on init and accessible from component context. + +## Passing State to the Component + +State is merged when the component initializes and **does not update dynamically**. + +### Use Case + +`state` is mainly for **restoring history**: pass saved state from a past conversation so the renderer can rehydrate. + +### Basic Usage + +```ts +import { Component } from '@angular/core'; +import { GenuiRenderer } from '@opentiny/genui-sdk-angular'; + +@Component({ + imports: [GenuiRenderer], + template: ` + + `, +}) +export class GenuiExample { + generating = false; + content = {}; + + // State restored from history + savedState = { + formData: { + name: 'John Doe', + age: 30, + }, + }; +} +``` + +### Accessing State in Actions + +In custom actions, use `context.state`: + +```ts +const customActions = { + getState: { + execute: (params: any, context: Record) => { + const state = context.state; + alert(`Global state: ${JSON.stringify(state)}`); + }, + }, +}; +``` + +#### Full Example + + + +## Notes + +1. **Init-only merge**: State is merged only on init; later updates are ignored. +2. **History replay**: Intended for restoring saved conversation state. +3. **Serializable data**: Avoid functions, DOM nodes, and other non-serializable values. diff --git a/docs/src/en/examples/chat/custom-actions.md b/docs/src/en/examples/chat/custom-actions.md new file mode 100644 index 00000000..7f2efe48 --- /dev/null +++ b/docs/src/en/examples/chat/custom-actions.md @@ -0,0 +1,92 @@ +# Chat Component - Custom Actions + +In the `GenuiChat` component, a built-in continue-chat Action `continueChat` is included. You can pass custom Actions via the `customActions` prop so the AI can invoke them from generated UI. + +## Basic Usage + +```vue {14-36} + + + +``` + +## Action Definition Format + +Each Action must include the following fields: + +- `name`: Action name, invoked in Schema via `this.callAction(name, params)` +- `description`: Action description to help the AI understand when to use it +- `parameters`: Parameter definition as JSON Schema +- `execute`: Execution function (optional, used for frontend implementation) + +```typescript +interface CustomAction { + name: string; + description: string; + parameters: JSONSchema; + execute?: (params: any) => void; // frontend implementation +} +``` + +## Invoking Actions in Schema + +AI-generated Schema can invoke these Actions via `JSFunction`: + +```json +{ + "componentName": "TinyButton", + "props": { + "children": "Open New Page", + "onClick": { + "type": "JSFunction", + "value": "function() { this.callAction('openPage', { url: 'https://example.com', target: '_blank' }); }" + } + } +} +``` + +## Full Example + + diff --git a/docs/src/en/examples/chat/custom-components.md b/docs/src/en/examples/chat/custom-components.md new file mode 100644 index 00000000..d2aaf637 --- /dev/null +++ b/docs/src/en/examples/chat/custom-components.md @@ -0,0 +1,92 @@ +# Chat Component - Custom Components + +In the `GenuiChat` component, you can pass custom components via the `customComponents` prop to extend the component library available to the LLM. + +## Basic Usage + +```vue { 16-43 } + + + +``` + +## Example schemaJson returned by the LLM + +```json +{ + "type": "schema-card", + "componentName": "Page", + "children": [ + { + "componentName": "Text", + "props": { + "text": "Custom Component Example", + "style": "font-size: 20px; font-weight: bold; margin-bottom: 16px;" + } + }, + { + "componentName": "UserProfile", + "props": { + "name": "John Doe", + "email": "john@example.com", + "avatar": "/genui-sdk-docs/logo.svg" + } + } + ] +} +``` + +When the AI generates a schema, ensure `componentName` matches the component name registered in `customComponents`, and pass `props` according to the fields defined in `schema`. + +## Full Example + + diff --git a/docs/src/en/examples/chat/custom-examples.md b/docs/src/en/examples/chat/custom-examples.md new file mode 100644 index 00000000..b18b31d0 --- /dev/null +++ b/docs/src/en/examples/chat/custom-examples.md @@ -0,0 +1,211 @@ +# Chat Component - Custom Examples + +Custom Examples provide component usage samples to help the LLM learn how to compose more polished and rich UI. Use them together with prompts so the LLM outputs the corresponding schemaJson to apply these examples. + +## Example Definition Format + +Each Example must include the following fields: + +```typescript +interface IGenPromptExample { + name: string; + description?: string; + schema: CardSchema; +} +``` + +- `name`: Example name +- `description`: Example description to help the LLM understand the purpose +- `schema`: Component schema example demonstrating usage + +## Developer Profile Card Example + +The following example shows how to use the TinyCard component to create a developer profile card with a vibrant gradient background: + +```vue + + + +``` + +## Full Example + + diff --git a/docs/src/en/examples/chat/custom-fetch.md b/docs/src/en/examples/chat/custom-fetch.md new file mode 100644 index 00000000..f2b3f876 --- /dev/null +++ b/docs/src/en/examples/chat/custom-fetch.md @@ -0,0 +1,404 @@ +# Chat Component - Custom Fetch + +The `GenuiChat` component supports a custom fetch function, allowing you to fully customize HTTP request behavior. This is useful for integrating third-party SDKs, adding authentication, handling tool calls, implementing custom streaming responses, and more. + +## Parameters + +- `url`: Request URL +- `options.method`: HTTP method (usually `'POST'`) +- `options.headers`: Request headers object +- `options.body`: Request body (JSON string) containing `messages`, `model`, `temperature`, and `metadata` (with processed `customComponents`, `customSnippets`, `customExamples`, and `customActions` information) +- `options.signal`: AbortSignal used to cancel the request + +## Return Value + +Must return a `Response` object or `Promise`. The response should follow the OpenAI-compatible streaming format (SSE), or return a standard JSON response. + +## Use Cases + +### Adding Authentication Headers + +Pass a custom fetch function via the `customFetch` prop: + +```vue + + + +``` + +### Handling Tool Calls and Multi-turn Conversations + +Use `customFetch` to implement tool calling (Function Calling) and multi-turn conversations. The following complete example demonstrates how to do this. + +#### 1. Define Tools + +First, define the available tools. Each tool has two parts: `definition` (tool definition in OpenAI tool format) and `execute` (execution function). + +```typescript +import OpenAI from 'openai'; + +/** + * Tool to add two numbers + */ +export const addTwoNumbersTool = { + definition: { + type: 'function' as const, + function: { + name: 'add_two_numbers', + description: + 'Adds two numbers. This is a math tool for summing two numbers. You must provide parameters a and b. Example: if a=5, b=3, returns 8.', + parameters: { + type: 'object', + properties: { + a: { + type: 'number', + description: 'First number to add. Required. Must be a number. Example: 5', + }, + b: { + type: 'number', + description: 'Second number to add. Required. Must be a number. Example: 3', + }, + }, + required: ['a', 'b'], + }, + }, + }, + execute: async ({ a, b }: { a: number; b: number }) => { + return a + b; + }, +}; + +/** + * List of all available tools + */ +export const availableTools: Record< + string, + { + definition: OpenAI.Chat.Completions.ChatCompletionTool; + execute: (args: any) => Promise | any; + } +> = { + add_two_numbers: addTwoNumbersTool, +}; +``` + +#### 2. Implement CustomFetch for Tool Calls + +Next, implement the `customFetch` function to handle tool calls and multi-turn conversations: + +```typescript +import OpenAI from 'openai'; +import type { CustomFetch } from '@opentiny/genui-sdk-vue'; +import { availableTools } from './tools'; + +/** + * OpenAI SDK configuration + */ +export interface OpenAIConfig { + apiKey: string; + baseURL?: string; + organization?: string; +} + +/** + * Execute a tool call + */ +async function executeToolCall(toolName: string, args: any): Promise { + const tool = availableTools[toolName]; + if (!tool) { + throw new Error(`Tool ${toolName} not found`); + } + + try { + const result = await tool.execute(args); + return typeof result === 'string' ? result : JSON.stringify(result); + } catch (error) { + return JSON.stringify({ + error: error instanceof Error ? error.message : 'Unknown error', + }); + } +} + +/** + * Accumulate tool call data + * Accumulates incremental tool call deltas from streaming into complete tool call objects + */ +function accumulateToolCalls(toolCalls: any[], toolCallDeltas: any[]): void { + for (const delta of toolCallDeltas) { + const index = delta.index ?? 0; + const toolCall = (toolCalls[index] ??= { + id: delta.id ?? '', + type: 'function', + function: { name: '', arguments: '' }, + }); + if (delta.id) toolCall.id = delta.id; + if (delta.function?.name) toolCall.function.name += delta.function.name; + if (delta.function?.arguments) toolCall.function.arguments += delta.function.arguments; + } +} + +/** + * Execute a single tool call and return the result + */ +async function executeSingleToolCall(toolCall: any, currentMessages: any[]): Promise { + const createResult = (result: string) => { + currentMessages.push({ role: 'tool', tool_call_id: toolCall.id, content: result }); + return { + id: toolCall.id, + type: 'function', + function: { name: toolCall.function.name, arguments: toolCall.function.arguments, result }, + }; + }; + try { + const args = JSON.parse(toolCall.function.arguments); + const result = await executeToolCall(toolCall.function.name, args); + return createResult(result); + } catch (error) { + return createResult(JSON.stringify({ error: error instanceof Error ? error.message : 'Unknown error' })); + } +} + +/** + * Create a customRequest function using the OpenAI SDK (handles tool calls and multi-turn conversations) + * + * @param config OpenAI configuration + * @returns CustomFetch function + */ +export function createOpenAICustomFetch(config: OpenAIConfig): CustomFetch { + return async ( + url: string, + options: { + method: string; + headers: Record; + body: string; + signal?: AbortSignal; + }, + ): Promise => { + // Parse request body + const requestBody = JSON.parse(options.body); + const { messages, model, temperature } = requestBody; + + try { + const openai = new OpenAI({ + apiKey: config.apiKey, + baseURL: config.baseURL, + dangerouslyAllowBrowser: true, + }); + + const tools = Object.values(availableTools).map((tool) => tool.definition); + const maxSteps = 20; // Maximum number of tool call steps + + // Convert stream to SSE-format Response + const encoder = new TextEncoder(); + const readableStream = new ReadableStream({ + async start(controller) { + try { + let currentMessages = [...messages]; + let stepCount = 0; + + while (stepCount < maxSteps) { + // Create streaming request + const stream = await openai.chat.completions.create( + { + model, + messages: currentMessages, + temperature, + tools: tools.length > 0 ? tools : undefined, + tool_choice: tools.length > 0 ? 'auto' : undefined, + stream: true, + }, + { + signal: options.signal, + }, + ); + + let toolCalls: any[] = []; + let hasToolCalls = false; + + // Process streaming response + for await (const chunk of stream) { + const choice = chunk.choices?.[0]; + if (!choice) continue; + + const delta = choice.delta; + + // Accumulate tool call data + if (delta.tool_calls) { + hasToolCalls = true; + accumulateToolCalls(toolCalls, delta.tool_calls); + } + + // Pass through original chunk + controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`)); + + // Handle finish reason + if (choice.finish_reason === 'tool_calls' && toolCalls.length > 0) { + // Execute tool calls + currentMessages.push({ role: 'assistant', content: null, tool_calls: toolCalls }); + const toolResults = await Promise.all( + toolCalls.map((toolCall, i) => + executeSingleToolCall(toolCall, currentMessages).then((result) => ({ ...result, index: i })), + ), + ); + + // Send tool call results + if (toolResults.length > 0) { + const toolResultChunk = { + id: chunk.id, + object: 'chat.completion.chunk', + model: chunk.model || model, + created: chunk.created || Math.floor(Date.now() / 1000), + choices: [{ index: 0, delta: { tool_calls_result: toolResults }, finish_reason: 'tool_calls' }], + }; + controller.enqueue(encoder.encode(`data: ${JSON.stringify(toolResultChunk)}\n\n`)); + } + + stepCount++; + break; + } + + if (choice.finish_reason) { + // Normal completion, exit outer loop + break; + } + } + + // Exit loop if there are no tool calls + if (!hasToolCalls) { + break; + } + } + + // Send end marker + controller.enqueue(encoder.encode('data: [DONE]\n\n')); + controller.close(); + } catch (error) { + const errorData = { + error: { + message: error instanceof Error ? error.message : 'Unknown error', + type: 'stream_error', + }, + }; + controller.enqueue(encoder.encode(`data: ${JSON.stringify(errorData)}\n\n`)); + controller.error(error); + } + }, + }); + + return new Response(readableStream, { + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + }, + status: 200, + }); + } catch (error: any) { + console.error('[OpenAI SDK Error]', { + url, + error: error.message, + }); + + // Return error response + return new Response( + JSON.stringify({ + error: { + message: error.message, + type: error.type || 'unknown', + code: 500, + }, + }), + { + status: 500, + headers: { + 'Content-Type': 'application/json', + }, + }, + ); + } + }; +} + +/** + * Default customFetch implementation (configured via environment variables) + */ +export const defaultCustomFetch = createOpenAICustomFetch({ + apiKey: import.meta.env.VITE_OPENAI_API_KEY, + baseURL: 'https://your-chat-backend/api', +}); +``` + +Key implementation points: + +1. **Multi-turn conversation loop**: Use a `while` loop to handle multiple rounds of tool calls, up to `maxSteps` times +2. **Message history management**: Maintain a `currentMessages` array containing user messages, assistant messages, and tool call results +3. **Tool call handling**: + - When `finish_reason` is `tool_calls`, execute all tool calls + - Add tool call results to `currentMessages` and continue the next round of conversation +4. **Streaming response conversion**: Convert the OpenAI SDK streaming response to SSE format; otherwise the component cannot process it correctly +5. **Tool call result delivery**: Send tool execution results via the `tool_calls_result` delta field so the component can update tool call status and display results correctly + +#### 3. Use in the Application + +Finally, use the custom `customFetch` in a Vue component: + +```vue + + + + + +``` + +## Try Tool Calling + +After completing the steps above, you can try tool calling and view tool call arguments and results in the conversation: + +![Custom fetch](../../../public/custom-fetch.png) diff --git a/docs/src/en/examples/chat/custom-snippets.md b/docs/src/en/examples/chat/custom-snippets.md new file mode 100644 index 00000000..3b0025f5 --- /dev/null +++ b/docs/src/en/examples/chat/custom-snippets.md @@ -0,0 +1,89 @@ +# Chat Component - Custom Snippets + +Custom Snippets provide common component composition patterns to help the LLM quickly generate typical UI structures. Use them together with prompts so the LLM outputs the corresponding schemaJson for these snippets. + +## Snippet Definition Format + +Each Snippet must include the following fields: + +```typescript +type IGenPromptSnippet = NodeSchema; + + +interface NodeSchema { + componentName: string; + props?: Record; + children?: NodeSchema[]; + [key: string]: any; +} +``` + +- `componentName`: Component name +- `props`: Component properties +- `children`: Child component array used to define the composition structure + +## Tabs Composition Example + +The following example shows how to use `TinyTabs` and `TinyTabItem` to create a tab layout. `TinyTabs` serves as the container component and `TinyTabItem` as tab items; the two must be used together: + +```vue {10-51} + + + +``` + +## Full Example + + diff --git a/docs/src/en/examples/chat/footer-toolbar.md b/docs/src/en/examples/chat/footer-toolbar.md new file mode 100644 index 00000000..5a5a2b93 --- /dev/null +++ b/docs/src/en/examples/chat/footer-toolbar.md @@ -0,0 +1,73 @@ +# Chat Component - Custom Footer Toolbar + +The `GenuiChat` component supports custom footer toolbars for messages from different roles. You can add action buttons below each message (copy, like, dislike, refresh, etc.) to enhance the interaction experience. + +## Passing a Custom Footer Toolbar to GenuiChat + +Configure message footer toolbars for assistant and user roles via the `roles` prop. Under each role, specify the footer component with `slots.trailer`. + +```vue {12-23} + + + +``` + +## Slot Props + +```typescript +interface IBubbleSlotsProps { + index: number; + bubbleProps: BubbleProps; + isFinished: boolean; + messageManager: UseMessageReturn; +} +``` + +- `index`: Index of the current message in the message list (starting from 0) +- `bubbleProps`: Render props for the current bubble, including `content` and other fields +- `isFinished`: Whether the current reply has finished; typically used to control toolbar visibility +- `messageManager`: Message manager containing the current message list, send methods, and more + +See TinyRobot documentation for details on `BubbleProps` and `UseMessageReturn`. + +See [BubbleProps](https://docs.opentiny.design/tiny-robot/guide/bubble.html#props) for definition and usage. + +See [UseMessageReturn](https://docs.opentiny.design/tiny-robot/guide/message.html#%E8%BF%94%E5%9B%9E%E5%80%BC) for definition and usage. + +## Creating the User Footer Toolbar Component + +See `user-footer.vue` in the full example for detailed code. + +## Creating the Assistant Footer Toolbar Component + +See `assistant-footer.vue` in the full example for detailed code. + +## Full Example + +See the runnable demo below: + + diff --git a/docs/src/en/examples/chat/history.md b/docs/src/en/examples/chat/history.md new file mode 100644 index 00000000..526334dd --- /dev/null +++ b/docs/src/en/examples/chat/history.md @@ -0,0 +1,137 @@ +# Chat Component - Conversation History Management + +The `GenuiChat` component includes full conversation management with multi-conversation support, auto-save, and persistent storage. Use the exposed `getConversation()` method to access all conversation management APIs. + +## Basic Usage + +### Getting the Conversation Object + +Access conversation management methods via a component ref: + +```vue {12-19} + + + +``` + +## Conversation Management API + +Use the object returned by `getConversation()` to manage conversations: + +### Create a New Conversation + +```typescript +const conversation = chatRef.value?.getConversation(); +const newConversationId = conversation?.createConversation('New Conversation Title'); +``` + +### Switch Conversation + +```typescript +const conversation = chatRef.value?.getConversation(); +conversation?.switchConversation(conversationId); +``` + +### Delete Conversation + +```typescript +const conversation = chatRef.value?.getConversation(); +conversation?.deleteConversation(conversationId); +``` + +### Manually Save Conversations + +```typescript +const conversation = chatRef.value?.getConversation(); +await conversation?.saveConversations(); +``` + +## Managing History with a Sidebar + +The following example shows how to display and manage conversation history in a sidebar: + +```vue + + + +``` + +## Full Example + + diff --git a/docs/src/en/examples/chat/image-upload.md b/docs/src/en/examples/chat/image-upload.md new file mode 100644 index 00000000..21e99c1b --- /dev/null +++ b/docs/src/en/examples/chat/image-upload.md @@ -0,0 +1,60 @@ +# Chat Component - Image Upload + +The `GenuiChat` component supports image upload, allowing users to upload images in conversations. Configure image upload via `features.supportImage` and pair it with an LLM that supports image processing. + +## Configuration Options + +```typescript +interface ImageFeatures { + enabled: boolean; // Whether image upload is enabled + maxImageSize: number; // Maximum size per image (MB) + maxFilesPerRequest: number; // Maximum number of images per request + supportedFileTypes: string[]; // Supported image formats +} +``` + +## Basic Usage + +Set `supportImage.enabled` to `true` to enable and configure image upload: + +```vue {10-16} + + + +``` + +## Data Format Accepted by the LLM + +After upload, the data structure follows the OpenAI-compatible format. The messageItem passed to the LLM looks like this: + +```json +{ + "role": "user", + "content": [ + { "type": "image_url", "filename": "circle.png", "image_url": { "url": "data:image/png;base64,XXXXXXXXXXX" } }, + { "type": "text", "text": "Analyze this image" } + ] +} +``` + +## Full Example + + diff --git a/docs/src/en/examples/chat/thinking-process.md b/docs/src/en/examples/chat/thinking-process.md new file mode 100644 index 00000000..07304255 --- /dev/null +++ b/docs/src/en/examples/chat/thinking-process.md @@ -0,0 +1,249 @@ +# Chat Component - Custom Thinking Process + +The `GenuiChat` component supports a custom thinking-process display component so users can see the AI's real-time response status. + +## Basic Usage + +Use the `thinkComponent` prop to customize the thinking-process display component. + +```vue + + + +``` + +## Received Props + +The custom thinking component receives the following props: + +### `message: IChatMessage` + +The full message object for this turn of the conversation: + +```typescript +interface IChatMessage { + role: 'user' | 'assistant'; + content: string; + messages?: IMessageItem[]; +} + +interface IMessageItem { + type: string; + content: string; + [customKey: string]: any; +} +``` + +### `emitter: INotificationEventEmitter` + +Used to listen for notification events during streaming responses: + +```typescript +interface INotificationEventEmitter { + on(eventName: 'notification', callback: (payload: INotificationPayload) => void, once?: boolean): void; + off(eventName: 'notification', callback: (payload: INotificationPayload) => void): void; + once(eventName: 'notification', callback: (payload: INotificationPayload) => void): void; +} + +type INotificationPayload = + | { + type: 'markdown' | 'schema-card' | 'done'; + delta: IStreamDelta; + chatMessage: IChatMessage; + } + | { + type: 'tool'; + delta: IStreamDelta; + chatMessage: IChatMessage; + toolCallData: IMessageItem & { + type: 'tool'; + }; + }; + +interface IStreamDelta { + content?: string; + tool_calls?: Array<{ + id: string; + function: { + name: string; + arguments: string; + }; + }>; + tool_calls_result?: Array<{ + id: string; + function: { + arguments: any; + result: any; + }; + }>; +} +``` + +#### Payload Details + +##### 1. `type: 'markdown'` + +Triggered when the AI is generating Markdown content: + +```typescript +{ + type: 'markdown'; + delta: IStreamDelta; // incremental data, includes the content field + chatMessage: IChatMessage; // full message object +} +``` + +**Use case**: When the goal is to generate a card, Markdown can be shown as the thinking process. + +##### 2. `type: 'schema-card'` + +Triggered when the AI is generating a Schema Card (UI component): + +```typescript +{ + type: 'schema-card'; + delta: IStreamDelta; + chatMessage: IChatMessage; +} +``` + +**Use case**: Show prompts such as "Generating card...". + +##### 3. `type: 'tool'` + +Triggered when the AI is invoking a tool: + +```typescript +{ + type: 'tool'; + delta: IStreamDelta; + chatMessage: IChatMessage; + toolCallData: IToolMessageItem; // tool call data +} +``` + +`toolCallData` includes the following fields: + +- `name: string` - Tool name +- `status: 'running' | 'success' | 'failed' | 'cancelled'` - Tool call status +- `content: string` - Tool call arguments and result (JSON string) + +**Use case**: Display tool call status, e.g. "Calling getWeather...", "Called getWeather...". + +##### 4. `type: 'done'` + +Triggered when the streaming response completes: + +```typescript +{ + type: 'done'; + delta: IStreamDelta; + chatMessage: IChatMessage; +} +``` + +**Use case**: Hide loading state and clean up temporary data. + +## Example Implementation + +The following is a thinking component implementation based on production code: + +```vue + + + + + +``` + +## Code Explanation + +### Event Handling Logic + +1. **`done` event**: When streaming completes, clear `loadingText` and hide the component. + +2. **`schema-card` event**: When a UI component is being generated, show "Generating card...". + +3. **`showThinkingResult` is `true`**: If thinking result display is enabled, always show "Responding..." instead of specific tool call or content generation status. + +4. **`tool` event**: Display text based on tool call status (running, success, failed, cancelled), e.g. "Calling getWeather...". + +5. **`markdown` event**: Show a preview of the last message content, e.g. "Hello...". diff --git a/docs/src/en/examples/config-provider/custom-theme.md b/docs/src/en/examples/config-provider/custom-theme.md new file mode 100644 index 00000000..c76b74ad --- /dev/null +++ b/docs/src/en/examples/config-provider/custom-theme.md @@ -0,0 +1,69 @@ +# GenuiConfigProvider - Custom Theme + +`GenuiConfigProvider` supports custom themes via CSS variables. Override CSS variables from TinyRobot and the component library to customize visuals. + +## Basic Usage + +Use the `id` prop on `GenuiConfigProvider` to create a scoped container, then define custom variables in the matching CSS scope: + +```vue {22-43} + + + + + +``` + +## TinyRobot Theme Customization + +See the [TinyRobot theme configuration guide](https://docs.opentiny.design/tiny-robot/guide/theme-config.html) for more options. + +## TinyVue Component Theme Customization + +See [component design tokens](https://opentiny.design/tiny-vue/zh-CN/os-theme/components/button#token) for per-component theming. + +Or edit [base variables in vars.less](https://github.com/opentiny/tiny-vue/blob/dev/packages/theme/src/base/vars.less) to theme all components globally. + +## Full Example + + diff --git a/docs/src/en/examples/config-provider/i18n.md b/docs/src/en/examples/config-provider/i18n.md new file mode 100644 index 00000000..730705b9 --- /dev/null +++ b/docs/src/en/examples/config-provider/i18n.md @@ -0,0 +1,88 @@ +# GenuiConfigProvider - Internationalization + +`GenuiConfigProvider` supports i18n via the `locale` prop and custom messages via the `i18n` prop. + +## Dynamic Language Switching + +Switch language with a reactive variable: + +```vue + + + +``` + +## Custom Messages + +Use `i18n` to override or extend default strings. Keys are language codes; values are message objects for that locale. + +```vue + + + +``` + +## Full Message Catalog + +To customize more strings, inspect the full message catalog: + +```typescript +import { useI18n } from '@opentiny/genui-sdk-vue'; + +const i18n = useI18n(); + +console.log(i18n.messages) +``` + +## Full Example + + diff --git a/docs/src/en/examples/config-provider/theme.md b/docs/src/en/examples/config-provider/theme.md new file mode 100644 index 00000000..f460db1c --- /dev/null +++ b/docs/src/en/examples/config-provider/theme.md @@ -0,0 +1,11 @@ +# GenuiConfigProvider - Theme Switching + +`GenuiConfigProvider` supports runtime theme switching. + +## Theme for GenuiChat + + + +## Theme for GenuiRenderer + + diff --git a/docs/src/en/examples/renderer/custom-actions.md b/docs/src/en/examples/renderer/custom-actions.md new file mode 100644 index 00000000..63c70fb3 --- /dev/null +++ b/docs/src/en/examples/renderer/custom-actions.md @@ -0,0 +1,151 @@ +# Renderer - Custom Actions + +Custom actions let you implement complex interaction logic. Pair them with prompts so the LLM emits schema JSON that invokes those actions. + +## Passing customActions to the Renderer + +Pass actions via the `customActions` prop. Each action includes: + +- `name`: Action name +- `description`: Action description +- `execute`: Handler receiving `params` and `context` +- `parameters`: Optional parameter schema; the model uses it to fill `params` for `execute` + +### execute Parameters + +- `params`: Arguments passed when the action is invoked +- `context`: Renderer context (state and methods); use `context.state` for two-way bound global state + +### Example 1: Open a Page + +```vue {12-35} + + + +``` + +#### Full Example + + + +### Example 2: Show Live Form Binding + +```vue {12-33} + + + +``` + +#### Full Example + + + +## Send Custom Actions to the Server + +After registering actions on the renderer, include them in chat requests so the model can generate correct action calls. + +```ts {9-30} +const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + messages: [{ role: 'user', content: userInput }], + model: 'deepseek-v3.2', + stream: true, + metadata: { + tinygenui: JSON.stringify({ + customActions: [ + { + name: 'openPage', + description: 'Open a page', + parameters: { + type: 'object', + properties: { + url: { + type: 'string', + description: 'URL to open', + }, + target: { + type: 'string', + description: 'Target window: _self (same tab) or _blank (new tab)', + }, + }, + required: ['url', 'target'], + }, + } + ] + }), + }, + }), +}); +``` diff --git a/docs/src/en/examples/renderer/custom-components.md b/docs/src/en/examples/renderer/custom-components.md new file mode 100644 index 00000000..80ee0dba --- /dev/null +++ b/docs/src/en/examples/renderer/custom-components.md @@ -0,0 +1,132 @@ +# Renderer - Custom Components + +Custom components let you register your own components. With the right prompts, the LLM can output matching `componentName` values in schema JSON for `GenuiRenderer` to render. + +## Passing customComponents to the Renderer + +Pass a component map via `customComponents` on `GenuiRenderer`. + +### Example: Register a Custom Component + +```vue {8,13-15} + + + +``` + +### Create the Custom Component + +Use a standard Vue component: + +```vue + + + + + + +``` + +### Use Custom Components in Schema + +Generated schema can reference registered components: + +```json +{ + "componentName": "UserProfile", + "props": { + "name": "John Doe", + "email": "john@example.com", + "avatar": "https://example.com/avatar.jpg" + } +} +``` + +#### Full Example + + diff --git a/docs/src/en/examples/renderer/required-complete-field-selectors.md b/docs/src/en/examples/renderer/required-complete-field-selectors.md new file mode 100644 index 00000000..5d990520 --- /dev/null +++ b/docs/src/en/examples/renderer/required-complete-field-selectors.md @@ -0,0 +1,122 @@ +# Renderer - Buffered Fields + +Use `requiredCompleteFieldSelectors` to declare which field paths must be complete before updates apply. **This mainly prevents render errors when streaming partial JSON.** + +## Why Buffered Fields? + +During streaming, the LLM emits JSON in fragments. Rendering some fields too early can break the UI, for example: + +- **Incomplete JSFunction**: partial `value` under `[type=JSFunction]` causes parse errors +- **Incomplete componentName**: unknown or partial names fail to render +- **Incomplete image src**: `[componentName=img] > props > src` may load invalid URLs +- **Incomplete style**: partial CSS strings may fail to parse +- **Required component fields**: e.g. `name` on `TinyTabItem` + +Buffered selectors tell the framework to hold those values until complete, then apply them in one shot. + +## Selector Syntax + +Selectors resemble CSS: + +### Basics + +- **Field name**: `componentName` — any field named `componentName` +- **Attribute selector**: `[componentName=img]` — nodes where `componentName` is `img` +- **Child combinator**: `>` — direct child, e.g. `[componentName=img] > props > src` +- **Descendant**: space — any ancestor relationship +- **Wildcard**: `*` — any field name + +### Attribute Operators + +- `=` — exact: `[componentName=img]` +- `^=` — prefix: `[componentName^=TinyChart]` +- `$=` — suffix: `[componentName$=Item]` +- `*=` — contains: `[componentName*=Chart]` + +### Pseudo-classes + +- `:empty` — empty string, array, or object +- `:object` — object type +- `:array` — array type +- `:string` — string type +- `:number` — number type + +### Examples + +```typescript +// src on img nodes +'[componentName=img] > props > src'; + +// All JSFunction nodes +'[type=JSFunction]'; + +// All JSExpression nodes +'[type=JSExpression]'; + +// All props under components whose name starts with TinyChart +'[componentName^=TinyChart] > props > *'; + +// name prop on TinyTabItem +'[componentName=TinyTabItem] > props > name'; + +// Empty objects +':empty:object'; +``` + +## Defaults + +Built-in selectors cover common failure cases: + +```typescript +export const requiredCompleteFieldSelectors = [ + '[componentName=img] > props > src', + 'componentName', + 'style', + '[type=JSFunction]', + '[type=JSExpression]', + '[type=JSSlot][value=]', + 'type', + ':empty:object', +]; +``` + +## Custom Configuration + +Pass `requiredCompleteFieldSelectors`; custom rules are merged with defaults: + +```vue + + + +``` + +## Notes + +1. **Accurate selectors**: Invalid paths are ignored. +2. **Performance**: Too many selectors can slow updates; prefer critical fragile fields. diff --git a/docs/src/en/examples/renderer/state.md b/docs/src/en/examples/renderer/state.md new file mode 100644 index 00000000..932a6e09 --- /dev/null +++ b/docs/src/en/examples/renderer/state.md @@ -0,0 +1,64 @@ +# Renderer - Passing and Merging State + +Use `state` to pass initial state to the renderer. It is merged into global state on init and accessible from component context. + +## Passing State to the Renderer + +State is merged when `GenuiRenderer` initializes and **does not update dynamically**. + +### Use Case + +`state` is mainly for **restoring history**: pass saved state from a past conversation so the renderer can rehydrate. + +### Basic Usage + +```vue {12-18} + + + +``` + +### Accessing State in Actions + +In custom actions, use `context.state`: + +```vue + +``` + +#### Full Example + + + +## Notes + +1. **Init-only merge**: State is merged only on init; later updates are ignored. +2. **History replay**: Intended for restoring saved conversation state. +3. **Serializable data**: Avoid functions, DOM nodes, and other non-serializable values. diff --git a/docs/src/en/guide/angular/install.md b/docs/src/en/guide/angular/install.md new file mode 100644 index 00000000..14cd2a2f --- /dev/null +++ b/docs/src/en/guide/angular/install.md @@ -0,0 +1,143 @@ +# Installation and Configuration + +This guide helps you install GenUI SDK for Angular quickly. + +## Install dependencies + +Go to your project directory and install GenUI SDK and the official materials package: + +::: tabs +== npm +```bash +npm install @opentiny/genui-sdk-angular @opentiny/genui-sdk-materials-angular-opentiny-ng --force # Installs peerDependencies as well +``` +== pnpm +```bash +pnpm add @opentiny/genui-sdk-angular @opentiny/genui-sdk-materials-angular-opentiny-ng @opentiny/ng-themes +``` +== yarn +```bash +yarn add @opentiny/genui-sdk-angular @opentiny/genui-sdk-materials-angular-opentiny-ng @opentiny/ng-themes +``` +::: + +## Import styles + +### Update `style.css` + +Import the theme files required by the component library: + +```css +@import '@opentiny/ng-themes/styles.css'; +@import '@opentiny/ng-themes/theme-default.css'; +``` + +## Enable Zone and Animations + +The built-in component library requires Zone change detection and the animations module to work correctly. + +### Install zone.js + +Go to your project directory and install `zone.js`: + +::: tabs +== npm +```bash +npm install zone.js +``` +== pnpm +```bash +pnpm add zone.js +``` +== yarn +```bash +yarn add zone.js +``` +::: + +If your project already has Zone.js installed, you can skip this step. + +### Update `angular.json` + +Add `zone.js` to the polyfills array: + +```json +{ + // ... + "projects": { + "your-project-name": { + "projectType": "application", + // ... + "architect": { + "build": { + "builder": "@angular/build:application", + "options": { + // ... + "polyfills": ["zone.js"] // [!code ++] + }, + // ... + }, + // .... + } + }, + // other projects + } +} + +``` + +If your project already configures Zone.js, you can skip this step. + +### Update `app.config.ts` + +```ts +// ... +import { provideAnimations } from '@angular/platform-browser/animations'; // [!code ++] +import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core'; + +export const appConfig: ApplicationConfig = { + providers: [ + // ... + provideBrowserGlobalErrorListeners(), + provideZoneChangeDetection({ eventCoalescing: true }), // [!code ++] + provideAnimations(), // [!code ++] + ] +}; + +``` + +## Materials configuration + +`GenuiRenderer` no longer ships built-in materials. Inject them via `GenuiConfigProvider`'s `materials` prop so the SDK core stays decoupled from a specific component library. + +```ts +import { Component } from '@angular/core'; +import { GenuiConfigProvider, GenuiRenderer } from '@opentiny/genui-sdk-angular'; +import { materials } from '@opentiny/genui-sdk-materials-angular-opentiny-ng/materials'; + +@Component({ + imports: [GenuiConfigProvider, GenuiRenderer], + template: ` + + + + `, +}) +export class GenuiExample { + materials = materials; + schema = ''; +} +``` + +::: tip GenuiLegacyRenderer +For drop-in compatibility without configuring materials, see [GenuiRenderer Legacy compatibility](../../components/angular/renderer#compatibility-component-genuilegacyrenderer). +::: + +## Next steps + +You can now use `GenuiRenderer` to render generative UI. See the [Renderer usage guide](start-with-renderer). + +## Related documentation + +- See the [Renderer usage guide](start-with-renderer) to learn how to use `GenuiRenderer` with finer control +- See [feature examples](../../examples/angular/renderer/custom-actions) for usage examples diff --git a/docs/src/en/guide/angular/start-with-renderer.md b/docs/src/en/guide/angular/start-with-renderer.md new file mode 100644 index 00000000..77886495 --- /dev/null +++ b/docs/src/en/guide/angular/start-with-renderer.md @@ -0,0 +1,184 @@ +# Using the Renderer Component + +The core renderer component `GenuiRenderer` lets you compose logic more freely and control flows with finer granularity. This section shows a **minimal working example**: use the browser's native `fetch` to make a **streaming request**, then pass the streamed schema fragments to `GenuiRenderer` for rendering. + +## Fetch the service and handle streaming responses + +Create a file `fetch-schema-stream.ts`. Parse OpenAI-compatible SSE `delta.content`, then use core `PatternExtractor` to extract `` ```schemaJson `` chunks (default `SchemaJsonPattern`): + +````ts +// fetch-schema-stream.ts +import { PatternExtractor } from '@opentiny/genui-sdk-core'; + +export async function fetchSchemaStream( + url: string, + userInput: string, + onSchemaUpdate: (schemaChunk: string) => void, +): Promise { + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + messages: [{ role: 'user', content: userInput }], + model: 'deepseek-v3.2', + stream: true, + metadata: { + tinygenui: JSON.stringify({ + framework: 'Angular', + }), + }, + }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const reader = response.body!.getReader(); + const decoder = new TextDecoder('utf-8'); + let buffer = ''; + + const patternExtractor = new PatternExtractor({ + onNormalWrite: () => {}, + onHandledWrite: (value) => onSchemaUpdate(value), + }); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + + while (true) { + const lineEndIndex = buffer.indexOf('\n'); + if (lineEndIndex === -1) break; + + const line = buffer.slice(0, lineEndIndex).trim(); + buffer = buffer.slice(lineEndIndex + 1); + + if (!line.startsWith('data: ')) continue; + + const dataStr = line.slice(6); + + if (dataStr === '[DONE]') { + return; + } + + try { + const chunk = JSON.parse(dataStr); + const content = chunk.choices?.[0]?.delta?.content; + + if (!content) continue; + + patternExtractor.handleContent(content); + } catch (e) { + console.error('Failed to parse backend data:', e, dataStr); + } + } + } + } finally { + reader.releaseLock(); + } +} +```` + +## Use the Renderer component to accept streamed schemaJson + +Create a simple component with an input, send button, and render area. Configure an LLM service that can generate `schemaJson`: + +```ts {8, 15, 61-63} +import { Component } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { GenuiConfigProvider, GenuiRenderer } from '@opentiny/genui-sdk-angular'; +import { materials } from '@opentiny/genui-sdk-materials-angular-opentiny-ng/materials'; +import { fetchSchemaStream } from '../fetch-schema-stream'; + +@Component({ + selector: 'genui-example', + imports: [FormsModule, GenuiConfigProvider, GenuiRenderer], + template: ` +
+
+ + +
+ + + +
+ `, + styles: [` +.demo-container { + padding: 16px; + box-sizing: border-box; +} + +.input-group { + display: flex; + gap: 8px; + margin-bottom: 16px; +} + +input { + flex: 1; + padding: 8px 12px; + border: 1px solid #ddd; + border-radius: 4px; +} + +button { + padding: 8px 16px; + background: #1890ff; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; +} + `], +}) +export class GenuiExample { + inputText = ''; + schema = ''; + rendererKey = ''; + generating = false; + protected readonly activeMaterials = materials; + async handleSend() { + if (!this.inputText.trim() || this.generating) return; + + this.generating = true; + this.schema = ''; + const userInput = this.inputText; + this.inputText = ''; + + try { + await fetchSchemaStream('https:///chat/completions', userInput, (schemaChunk: string) => { + this.schema += schemaChunk; + }); + } catch (error) { + console.error('Request failed:', error); + } finally { + this.generating = false; + } + } + handlePrint(schema: any) { + console.log(schema); + } +} + +``` + +::: tip GenuiLegacyRenderer +For drop-in compatibility without configuring materials, see [GenuiRenderer Legacy compatibility](../../components/angular/renderer#compatibility-component-genuilegacyrenderer). +::: + +## Try it now + +Sample output: + +![Renderer component example](../../../public/start-with-renderer-ng.png) + +## Related documentation + +- See the [Renderer component docs](../../components/angular/renderer) for the full API +- See [custom actions example](../../examples/angular/renderer/custom-actions) to learn how to create custom actions diff --git a/docs/src/en/guide/quick-start.md b/docs/src/en/guide/quick-start.md new file mode 100644 index 00000000..b4802507 --- /dev/null +++ b/docs/src/en/guide/quick-start.md @@ -0,0 +1,256 @@ +# Quick Start + +This guide helps you get started with GenUI SDK quickly using the `GenuiChat` component. + +`GenuiChat` is an integrated chat component with built-in session management, streaming responses, and generation state. It is the simplest way to use generative UI. + +## Initialize Project + +Create a new Vue project: + +```bash +npm create vue@latest genui-chat +``` + +Follow the default prompts to initialize the project. + +## Install Dependencies + +Install GenUI SDK and the official materials package: + +::: tabs +== npm +```bash +cd genui-chat +npm install @opentiny/genui-sdk-vue @opentiny/genui-sdk-materials-vue-opentiny-vue +``` +== pnpm +```bash +cd genui-chat +pnpm add @opentiny/genui-sdk-vue @opentiny/genui-sdk-materials-vue-opentiny-vue +``` +== yarn +```bash +cd genui-chat +yarn add @opentiny/genui-sdk-vue @opentiny/genui-sdk-materials-vue-opentiny-vue +``` +::: + +## Modify Project + +### Update `src/main.js` or `src/main.ts` + +Remove the default Vue project styles: + +```js +import './assets/main.css'; // [!code --] + +import { createApp } from 'vue'; +import App from './App.vue'; + +createApp(App).mount('#app'); +``` + +### Update `src/App.vue` + +Inject materials via `GenuiConfigProvider` and render `GenuiChat`: + +```vue + + + + + +``` + +## Start Development + +Run the dev server: + +```bash +npm run dev +``` + +You should now see the GenUI Chat UI in your browser. + +## Configure GenuiChat + +Configure the LLM via `url`, `model`, and `temperature`: + +```vue + + + +``` + +## Materials and Theme with GenuiConfigProvider + +Both materials and theme are configured via `GenuiConfigProvider`: `materials` injects UI components, and `theme` controls the appearance. + +Built-in theme options: + +- `'dark'`: dark theme +- `'lite'`: fresh theme +- `'light'`: light theme (default) +- `'auto'`: follow system preference + +```vue + + + +``` + +## Empty Slot + +Use the `empty` slot for welcome text or suggested prompts when there is no conversation: + +```vue + +``` + +Add styles: + +```css +.empty-text { /* [!code ++] */ + height: 100%; /* [!code ++] */ + display: flex; /* [!code ++] */ + justify-content: center; /* [!code ++] */ + align-items: center; /* [!code ++] */ + font-size: 30px; /* [!code ++] */ +} /* [!code ++] */ +``` + +### Full Example + +```vue + + + + + +``` + +You are ready to try generative UI. + +![Quick start example](../../public/quick-start.png) + +## Subpath Imports + +Besides the main entry, `@opentiny/genui-sdk-vue` provides subpath exports. Import only Chat or Renderer when needed to avoid bundling unused modules when tree-shaking is limited. + +| Subpath | Use case | Main exports | +| --- | --- | --- | +| `@opentiny/genui-sdk-vue/chat` | Chat only | `GenuiChat` | +| `@opentiny/genui-sdk-vue/renderer` | Renderer only (custom chat UI) | `GenuiRenderer` | +| `@opentiny/genui-sdk-vue/config-provider` | Theme / i18n / materials container | `GenuiConfigProvider` | + +```ts +import { GenuiChat } from '@opentiny/genui-sdk-vue/chat'; +import { GenuiConfigProvider } from '@opentiny/genui-sdk-vue/config-provider'; +import { materials } from '@opentiny/genui-sdk-materials-vue-opentiny-vue/materials'; + +// Renderer only +import { GenuiRenderer } from '@opentiny/genui-sdk-vue/renderer'; +``` + +::: tip +In v1.3.0, materials were decoupled from the SDK. If you need the built-in TinyVue components without configuring materials separately, use `GenuiLegacyChat`. See [GenuiChat Legacy compatibility](../components/chat#compatibility-component-genuilegacychat). +::: + +## Related Docs + +- [GenuiChat API](../components/chat) +- [Using Renderer](start-with-renderer) +- [Examples](../examples/chat/custom-actions) diff --git a/docs/src/en/guide/renderer-with-tiny-robot.md b/docs/src/en/guide/renderer-with-tiny-robot.md new file mode 100644 index 00000000..eb63f332 --- /dev/null +++ b/docs/src/en/guide/renderer-with-tiny-robot.md @@ -0,0 +1,344 @@ +# Using Renderer with TinyRobot + +This guide explains how to use the `GenuiRenderer` component with a chat UI such as `TinyRobot`. It demonstrates how to combine a chat component to control message flow, UI rendering, and interaction logic. + +## Install dependencies + +:::: tabs +== npm +```bash +npm install @opentiny/genui-sdk-vue @opentiny/genui-sdk-materials-vue-opentiny-vue @opentiny/tiny-robot @opentiny/tiny-robot-kit +``` +== pnpm +```bash +pnpm add @opentiny/genui-sdk-vue @opentiny/genui-sdk-materials-vue-opentiny-vue @opentiny/tiny-robot @opentiny/tiny-robot-kit +``` +== yarn +```bash +yarn add @opentiny/genui-sdk-vue @opentiny/genui-sdk-materials-vue-opentiny-vue @opentiny/tiny-robot @opentiny/tiny-robot-kit +``` +:::: + +## Basic usage + +First, create a custom model provider to handle streaming responses. Below is the full `CustomModelProvider` implementation: + +````typescript +import { + BaseModelProvider, + type ChatCompletionRequest, + type ChatCompletionStreamResponse, +} from '@opentiny/tiny-robot-kit'; +import { PatternExtractor, type IChatMessage } from '@opentiny/genui-sdk-core'; +import { reactive } from 'vue'; + +function appendMarkdown(content: string, chatMessage: IChatMessage) { + const lastMessage = chatMessage.messages[chatMessage.messages.length - 1]; + if (lastMessage?.type === 'markdown') { + lastMessage.content += content; + } else { + chatMessage.messages.push({ type: 'markdown', content }); + } +} + +function appendSchemaCard(content: string, chatMessage: IChatMessage) { + const lastMessage = chatMessage.messages[chatMessage.messages.length - 1]; + if (lastMessage?.type === 'schema-card') { + lastMessage.content += content; + } else { + chatMessage.messages.push({ type: 'schema-card', content }); + } +} + +// Split markdown and schemaJson with PatternExtractor (default SchemaJsonPattern) +function useSchemaStream() { + let chatMessageRef: IChatMessage | null = null; + + const patternExtractor = new PatternExtractor({ + onNormalWrite: (value) => { + if (!chatMessageRef) return; + chatMessageRef.content += value; + appendMarkdown(value, chatMessageRef); + }, + onHandledWrite: (value) => { + if (!chatMessageRef) return; + chatMessageRef.content += value; + appendSchemaCard(value, chatMessageRef); + }, + }); + + const handleSchemaStream = (content: string, chatMessage: IChatMessage) => { + if (!content || typeof content !== 'string') return; + chatMessageRef = chatMessage; + patternExtractor.handleContent(content); + }; + + return { handleSchemaStream }; +} + +export class CustomModelProvider extends BaseModelProvider { + constructor(private url: string) { + super({ provider: 'custom' }); + } + + async chatStream( + request: ChatCompletionRequest, + handler: { + onData: (data: ChatCompletionStreamResponse) => void; + onDone: () => void; + onError: (error: any) => void; + }, + ) { + const { onDone, onData } = handler; + let response: Response; + + try { + response = await fetch(this.url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + messages: request.messages, + model: 'deepseek-v3.2', + stream: true, + }), + signal: request.options?.signal, + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + } catch (error) { + onDone({ type: 'error', error } as any); + return; + } + + const reader = response.body!.getReader(); + const decoder = new TextDecoder('utf-8'); + let buffer = ''; + const { handleSchemaStream } = useSchemaStream(); + + const chatMessage = reactive({ + role: 'assistant', + content: '', + messages: [], + }); + onData(chatMessage as any); + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + + while (true) { + const lineEnd = buffer.indexOf('\n'); + if (lineEnd === -1) break; + + const line = buffer.slice(0, lineEnd).trim(); + buffer = buffer.slice(lineEnd + 1); + + if (line.startsWith('data: ')) { + const data = line.slice(6); + if (data === '[DONE]') { + onDone(); + return; + } + + try { + const chunk = JSON.parse(data); + const delta = chunk.choices?.[0]?.delta; + const content = delta?.content; + + if (content) { + handleSchemaStream(content, chatMessage); + const lastMessage = chatMessage.messages[chatMessage.messages.length - 1]; + if (lastMessage && lastMessage.type === 'schema-card' && !lastMessage.id) { + // Demo only: use Math.random as key + lastMessage.id = Math.random().toString(36).substring(2, 15); + } + onData(chatMessage as any); + } + } catch (e) { + console.error('Parse error:', e); + } + } + } + } + + onDone(); + } +} +```` + +Then use it in your component: + +```vue + + + + + +``` + +::: tip GenuiRenderer +For drop-in compatibility without configuring materials, see [GenuiRenderer Legacy compatibility](../components/renderer#compatibility-component-genuilegacyrenderer). +::: + +## Related documentation + +- See the [Renderer component docs](../components/renderer) for the full API +- See [custom components example](../examples/renderer/custom-components) to learn how to create custom components +- See [custom actions example](../examples/renderer/custom-actions) to learn how to create custom actions diff --git a/docs/src/en/guide/server-usage.md b/docs/src/en/guide/server-usage.md new file mode 100644 index 00000000..05ef2035 --- /dev/null +++ b/docs/src/en/guide/server-usage.md @@ -0,0 +1,333 @@ +# GenUI SDK Server Usage + +The GenUI SDK chat server provides OpenAI-compatible HTTP endpoints with streaming responses and support for multiple AI providers. + +## Start the server + +### Installation + +Global installation: + +:::: tabs +== npm +```bash +npm install -g @opentiny/genui-sdk-server +``` +== pnpm +```bash +pnpm add -g @opentiny/genui-sdk-server +``` +== yarn +```bash +yarn global add @opentiny/genui-sdk-server +``` +:::: + +Project installation: + +:::: tabs +== npm +```bash +npm install @opentiny/genui-sdk-server +``` +== pnpm +```bash +pnpm add @opentiny/genui-sdk-server +``` +== yarn +```bash +yarn add @opentiny/genui-sdk-server +``` +:::: + +### Environment configuration + +Create a `.env` file: + +```env +BASE_URL=https://api.openai.com/v1 +API_KEY= +PORT=3100 +``` + +### Startup options + +#### Option 1: CLI command + +```bash +# Use default configuration +npx genui-sdk-server + +# Specify env file (shorthand: -e) +npx genui-sdk-server --envFile .env.production + +# Specify port (shorthand: -p) +npx genui-sdk-server --port 3000 + +# Start with environment variables (git bash) +export API_KEY= BASE_URL=https://your-llm-server.com/api && npx genui-sdk-server +``` + +#### Option 2: Programmatic startup + +```typescript +import { startServer } from '@opentiny/genui-sdk-server'; + +startServer({ + port: 3100, + baseURL: 'https://api.openai.com/v1', + apiKey: '', + maxAttempts: 10, // Max retries when port is in use +}); +``` + +#### Option 3: Integrate into an existing Express app + +```typescript +import express from 'express'; +import { equipChatCompletions } from '@opentiny/genui-sdk-server'; +import cors from 'cors'; + +const app = express(); +app.use(cors()); + +equipChatCompletions(app, { + route: '/chat/completions', + apiKey: '', + baseURL: 'https://api.openai.com/v1', +}); + +app.listen(3000); +``` + +## Client usage + +### Chat completions endpoint + +**Endpoint**: `POST /chat/completions` + +**Request format** (OpenAI-compatible): + +```jsonc +{ + "model": "gpt-4", + "messages": [ + { + "role": "system", + "content": "You are a helpful assistant." + }, + { + "role": "user", + "content": "Hello!" + } + ], + "stream": true, + "temperature": 0.7, + "metadata": { + "tinygenui": "{}" // See tinygenui configuration below + } +} +``` + +**Response format** (Server-Sent Events): + +```text +data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1694268190,"model":"gpt-4","choices":[{"index":0,"delta":{"content":"Hello"},"finish_reason":null}]} + +data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1694268190,"model":"gpt-4","choices":[{"index":0,"delta":{"content":"!"},"finish_reason":null}]} + +data: [DONE] +``` + +### tinygenui configuration + +Pass GenUI configuration via `metadata.tinygenui` to improve LLM generation (JSON string): + +```json +{ + "framework": "Vue", // or "Angular" + "strategy": "append", // "append" | "prepend" | "override" + "customComponents": [], // Custom component schema array + "customExamples": [], // Custom component usage examples + "customSnippets": [], // Custom component snippet schema array + "customActions": [] // Custom action definition array +} +``` + +- `framework`: Target frontend framework (Vue or Angular) for renderer configuration +- `strategy`: Prompt merge strategy + - `append`: Append to the existing system message (default) + - `prepend`: Prepend to the existing system message + - `override`: Replace the existing system message +- `customComponents`: Custom component schema array to extend the available component list +- `customExamples`: Custom component usage examples to guide the LLM in generating correct component usage +- `customSnippets`: Custom component snippet schema array for common component composition patterns +- `customActions`: Custom action definition array for actions callable from components (e.g. submit form, open a new page) + +#### Custom component configuration examples + +**customComponents** example: + +```js +const customComponents = [ + { + name: 'User Selector', + description: 'Select a user with fuzzy search by name', + component: 'TinyUser', + schema: { + properties: [ + { + property: 'name', + description: 'User name to search; supports fuzzy search', + type: 'string', + required: true, + }, + ], + }, + }, +]; +``` + +**customExamples** example: + +```js +const customExamples = [ + { + name: 'User selection example', + schema: { + componentName: 'Page', + children: [ + { + componentName: 'h3', + props: {}, + children: 'Enter a username to search and select a user', + }, + { + componentName: 'TinyUser', + props: { + name: 'Zhang San', + }, + }, + ], + }, + }, +]; +``` + +**customSnippets** example: + +```js +// Form composition example +const customSnippets = [ + { + componentName: 'TinyForm', + props: { + labelPosition: 'top', + labelWidth: '120px', + }, + children: [ + { + componentName: 'TinyFormItem', + props: { + label: 'Name', + prop: 'name', + required: true, + }, + children: [ + { + componentName: 'TinyInput', + props: { + placeholder: 'Enter your name', + }, + }, + ], + }, + { + componentName: 'TinyFormItem', + props: { + label: 'Email', + prop: 'email', + }, + children: [ + { + componentName: 'TinyInput', + props: { + placeholder: 'Enter your email', + }, + }, + ], + }, + { + componentName: 'TinyFormItem', + props: { + label: '', + }, + children: [ + { + componentName: 'TinyButton', + props: { + type: 'primary', + children: 'Submit', + }, + }, + ], + }, + ], + }, +]; +``` + +**customActions** example: + +```js +const customActions = [ + { + name: 'openPage', + description: 'Open a new page for navigation', + parameters: { + type: 'object', + properties: { + url: { + type: 'string', + description: 'Target page URL or path', + }, + target: { + type: 'string', + description: 'Open mode: _self (current window) or _blank (new window)', + }, + }, + required: ['url', 'target'], + }, + }, +]; +``` + +**Full configuration example**: + +```js +const requestParams = { + 'model': 'gpt-4', + 'messages': [ + { + 'role': 'system', + 'content': 'You are a helpful assistant.', + }, + { + 'role': 'user', + 'content': 'Hello!', + }, + ], + 'stream': true, + 'temperature': 0.7, + 'metadata': { + 'tinygenui': JSON.stringify({ + framework: 'Vue', + strategy: 'append', + customComponents, + customExamples, + customSnippets, + customActions, + }), + }, +}; +``` diff --git a/docs/src/en/guide/start-with-renderer.md b/docs/src/en/guide/start-with-renderer.md new file mode 100644 index 00000000..04161647 --- /dev/null +++ b/docs/src/en/guide/start-with-renderer.md @@ -0,0 +1,174 @@ +# Using the Renderer Component + +Besides the integrated Chat component, GenUI SDK provides the core renderer component `GenuiRenderer`, which lets you compose logic more freely and control flows with finer granularity. This section shows a **minimal working example**: use the browser's native `fetch` to make a **streaming request**, then pass the streamed schema fragments to `GenuiRenderer` for rendering. + +## Fetch the service and handle streaming responses + +Create a file `fetch-schema-stream.ts`. Parse OpenAI-compatible SSE `delta.content`, then use core `PatternExtractor` to extract `` ```schemaJson `` chunks (default `SchemaJsonPattern`): + +````typescript +import { PatternExtractor } from '@opentiny/genui-sdk-core'; + +export async function fetchSchemaStream( + url: string, + userInput: string, + onSchemaUpdate: (schemaChunk: string) => void, +): Promise { + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + messages: [{ role: 'user', content: userInput }], + model: 'deepseek-v3.2', + stream: true, + }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const reader = response.body!.getReader(); + const decoder = new TextDecoder('utf-8'); + let buffer = ''; + + const patternExtractor = new PatternExtractor({ + onNormalWrite: () => {}, + onHandledWrite: (value) => onSchemaUpdate(value), + }); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + + while (true) { + const lineEndIndex = buffer.indexOf('\n'); + if (lineEndIndex === -1) break; + + const line = buffer.slice(0, lineEndIndex).trim(); + buffer = buffer.slice(lineEndIndex + 1); + + if (!line.startsWith('data:')) continue; + + const dataStr = line.slice(5).trim(); + + if (dataStr === '[DONE]') { + return; + } + + try { + const chunk = JSON.parse(dataStr); + const content = chunk.choices?.[0]?.delta?.content; + + if (!content) continue; + + patternExtractor.handleContent(content); + } catch (e) { + console.error('Failed to parse backend data:', e, dataStr); + } + } + } + } finally { + reader.releaseLock(); + } +} +```` + +## Use the Renderer component to accept streamed schemaJson + +Create a simple Vue component with an input, send button, and render area. Configure an LLM service that can generate `schemaJson`: + +```vue + + + + + +``` + +::: tip GenuiRenderer +For drop-in compatibility without configuring materials, see [GenuiRenderer Legacy compatibility](../components/renderer#compatibility-component-genuilegacyrenderer). +::: + +## Try it now + +This example uses `deepseek-v3.2` for testing. Sample output: + +![Renderer component example](../../public/start-with-renderer.png) + +## Related documentation + +- See the [Renderer component docs](../components/renderer) for the full API +- See [custom components example](../examples/renderer/custom-components) to learn how to create custom components +- See [custom actions example](../examples/renderer/custom-actions) to learn how to create custom actions diff --git a/docs/src/en/index.md b/docs/src/en/index.md new file mode 100644 index 00000000..99cec1d3 --- /dev/null +++ b/docs/src/en/index.md @@ -0,0 +1,34 @@ +--- +layout: home + +hero: + name: 'GenUI-SDK' + text: 'Enhance LLM Display & Interaction' + + actions: + - theme: brand + text: Quick Start + link: /en/guide/quick-start + - theme: alt + text: Components + link: /en/components/renderer +features: + - title: AI Ecosystem Compatible + details: OpenAI format compatible, supports MCP service integration + - title: Customizable Themes + details: Theme switching, dark mode, and user-defined themes + - title: Custom Components + details: Pass custom components and descriptions to enhance generative UI capabilities + - title: Custom Interactions + details: Custom interaction behaviors such as opening pages and downloading attachments + - title: Multi-Stack Support + details: Built-in Vue and Angular renderers, with support for custom renderer extensions + - title: More Features + details: Custom examples and snippets to build better UIs for diverse visual preferences +--- + +## What is Generative UI? + +Generative UI is an innovative interaction pattern that renders structured LLM output into interactive user interfaces in real time. Unlike plain text chat, generative UI lets AI generate forms, buttons, charts, and other components so users can interact with AI more intuitively and efficiently. + +![Generative UI](../public/genui.gif) diff --git a/docs/src/en/schema/protocol.md b/docs/src/en/schema/protocol.md new file mode 100644 index 00000000..f75ce96c --- /dev/null +++ b/docs/src/en/schema/protocol.md @@ -0,0 +1,808 @@ + + +
+

Schema Protocol Specification

+
+ +The Schema protocol is a JSON-based declarative UI description protocol used to define and render user interfaces. It uses a tree structure to describe component hierarchy and supports dynamic data binding and event handling. + +## Overview + +The Schema protocol describes a complete UI structure through JSON objects, including: + +- **Component tree structure**: Describes component hierarchy through nested node objects +- **Component properties**: Each component can configure properties supporting primitive values, JS expressions, JS functions, and more +- **State management**: Manages page-level state data through the `state` field +- **Event handling**: Defines reusable methods through the `methods` field; component properties can bind to these methods + +### Design Principles + +1. **Declarative**: Uses declarative structures to describe UI rather than imperative operations +2. **Platform-agnostic**: The protocol itself is not tied to a specific framework; implementations are mapped through a component registry +3. **Type-safe**: Ensures Schema structure correctness through type definitions +4. **Extensible**: Supports custom components and property types + +## Core Concepts + +### Schema Object + +A Schema is a JSON object containing a complete page definition. It must include the `componentName` field, typically `"Page"`. + +### Node + +A node is the basic unit in the component tree; each node represents a UI component. A node includes: +- `componentName`: Component name (required) +- `id`: Unique node identifier (optional, but recommended) +- `props`: Component properties +- `children`: Child node array +- Other optional fields: `slot`, `loop`, `condition`, etc. + +### RootNode + +The root node is the top-level node of a Schema. In addition to all fields of a regular node, it includes page-level configuration: +- `state`: Global state +- `methods`: Method collection +- `css`: Global styles + +## Data Structures + +### RootNode Type Definition + +```typescript +type RootNode = Omit & { + id?: string; // Optional id for root node + css?: string; // Global CSS style string + fileName?: string; // File name + methods?: Methods; // Method collection + state?: Record; // Global state + schema?: any; // Embedded or external Schema +}; +``` + +### Node Type Definition + +```typescript +interface Node { + id?: string; // Unique node identifier (optional) + componentName: string; // Component name (required) + props?: Record & { + columns?: { slots?: Record }[] + }; // Component property collection + children?: Node[]; // Child node array + componentType?: 'Block' | 'PageStart' | 'PageSection'; // Node type + slot?: string | Record; // Slot content + params?: string[]; // Parameter name list + loop?: Record; // Loop rendering configuration + loopArgs?: string[]; // Loop parameter name list + condition?: boolean | Record; // Conditional rendering configuration +} +``` + +### Field Reference + +#### Required Fields + +- **componentName** (string): Component name; must match a component name in the client component registry + +#### Optional Fields + +- **id** (string): Unique node identifier; recommended for each node to aid debugging and event handling +- **props** (object): Component property object; keys are property names, values are property values (supports multiple types) +- **children** (Node[]): Child node array defining child components +- **componentType** ('Block' | 'PageStart' | 'PageSection'): Node type; usually omitted +- **slot** (string | object): Slot content; can be a string or object +- **params** (string[]): Parameter name list +- **loop** (object): Loop rendering configuration for list rendering +- **loopArgs** (string[]): Loop parameter name list, e.g. `["item", "index"]` +- **condition** (boolean | object): Conditional rendering configuration controlling whether the component renders + +#### RootNode-Specific Fields + +- **css** (string): Global CSS style string +- **fileName** (string): File name identifier +- **methods** (Methods): Method collection defining reusable functions +- **state** (Record): Global state object +- **schema** (any): Embedded or external Schema + +## Property Value Types + +Property values (PropValue) support the following types: + +### 1. Primitive Values + +- `string`: String +- `number`: Number +- `boolean`: Boolean +- `null`: null value + +### 2. JS Expression (JSExpression) + +Used for dynamically computing property values; supports accessing state and executing calculations. + +```typescript +interface JSExpression { + type: 'JSExpression'; // Fixed as 'JSExpression' + value: string; // Expression string + model?: boolean; // Whether this is a two-way binding model value + params?: string[]; // Parameters passed by scoped slots +} +``` + +**Example:** +```json +{ + "text": { + "type": "JSExpression", + "value": "this.state.userName + ' - ' + this.state.userHandle" + } +} +``` + +**Two-way binding example:** +```json +{ + "value": { + "type": "JSExpression", + "value": "this.state.inputValue", + "model": true + } +} +``` + +### 3. JS Function (JSFunction) + +Used to define event handler functions. + +```typescript +interface JSFunction { + type: 'JSFunction'; // Fixed as 'JSFunction' + value: string; // Function body string (serializable) +} +``` + +**Example:** +```json +{ + "onClick": { + "type": "JSFunction", + "value": "function() { alert('Button clicked'); }" + } +} +``` + +### 4. Slot (JSSlot) + +Used to define slot content. + +```typescript +interface JSSlot { + type: 'JSSlot'; // Fixed as 'JSSlot' + value: string | Record; // Slot content +} +``` + +### 5. Arrays and Objects + +Property values can be arrays or objects, supporting nested structures. + +```json +{ + "items": ["item1", "item2", "item3"], + "config": { + "key1": "value1", + "key2": { + "type": "JSExpression", + "value": "this.state.dynamicValue" + } + } +} +``` + +### 6. Special Structure: columns + +For table and similar components, `props` supports a special `columns` structure: + +```json +{ + "props": { + "columns": [ + { + "prop": "name", + "label": "Name", + "slots": { + "default": "custom-name-slot" + } + } + ] + } +} +``` + +## Component Rendering + +### Basic Rendering + +Components are specified via the `componentName` field; the client looks up the corresponding implementation in the component registry. + +```json +{ + "componentName": "Text", + "id": "text-1", + "props": { + "text": "Hello World" + } +} +``` + +### Nested Rendering + +Child components are defined through the `children` field, forming a component tree. + +```json +{ + "componentName": "CanvasFlexBox", + "id": "container", + "props": { + "flexDirection": "column" + }, + "children": [ + { + "componentName": "Text", + "id": "title", + "props": { + "text": "Title" + } + }, + { + "componentName": "Text", + "id": "content", + "props": { + "text": "Content" + } + } + ] +} +``` + +### Conditional Rendering + +The `condition` field controls whether a component renders. + +```json +{ + "componentName": "Text", + "id": "conditional-text", + "condition": { + "type": "JSExpression", + "value": "this.state.isVisible" + }, + "props": { + "text": "Conditionally rendered text" + } +} +``` + +Or use a boolean value: + +```json +{ + "componentName": "Text", + "id": "conditional-text", + "condition": true, + "props": { + "text": "Conditionally rendered text" + } +} +``` + +### Loop Rendering + +List rendering is implemented through the `loop` and `loopArgs` fields. + +```json +{ + "componentName": "div", + "id": "list-item", + "loop": { + "list": { + "type": "JSExpression", + "value": "this.state.items" + } + }, + "loopArgs": ["item", "index"], + "props": { + "style": "padding: 10px;" + }, + "children": [ + { + "componentName": "Text", + "id": "item-text", + "props": { + "text": { + "type": "JSExpression", + "value": "item.name" + } + } + } + ] +} +``` + +### Slot Rendering + +Slot content is defined through the `slot` field. + +```json +{ + "componentName": "Card", + "id": "card-1", + "slot": "Slot content text" +} +``` + +Or define multiple slots using an object: + +```json +{ + "componentName": "Card", + "id": "card-1", + "slot": { + "header": "Header content", + "footer": "Footer content" + } +} +``` + +## State Management + +### Defining State + +Define global state in the `state` field of the root node. + +```json +{ + "componentName": "Page", + "state": { + "userName": "John Doe", + "userAge": 25, + "isLoggedIn": true, + "userProfile": { + "name": "John Doe", + "email": "john@example.com" + } + } +} +``` + +### Using State + +Access state in component properties through JS expressions using `this.state`. + +```json +{ + "componentName": "Text", + "id": "user-name", + "props": { + "text": { + "type": "JSExpression", + "value": "this.state.userName" + } + } +} +``` + +### Two-Way Binding + +For form components, use `model: true` to implement two-way binding. + +```json +{ + "componentName": "Input", + "id": "user-input", + "props": { + "value": { + "type": "JSExpression", + "value": "this.state.inputValue", + "model": true + } + } +} +``` + +## Event Handling + +### Defining Methods + +Define reusable methods in the `methods` field of the root node. + +```json +{ + "componentName": "Page", + "state": { + "formData": { + "name": "", + "email": "" + } + }, + "methods": { + "handleSubmit": { + "type": "JSFunction", + "value": "function($event) { console.log('Triggered event object', $event); console.log('Submit data:', this.state.formData); }" + } + } +} +``` + +### Binding Events + +Bind event handler functions in component properties. + +**Option 1: Reference a method from methods** +```json +{ + "componentName": "TinyButton", + "id": "submit-btn", + "props": { + "text": "Submit", + "onClick": { + "type": "JSExpression", + "value": "this.handleSubmit" + } + } +} +``` + +**Option 2: Define JSFunction directly** +```json +{ + "componentName": "TinyButton", + "id": "submit-btn", + "props": { + "text": "Submit", + "onClick": { + "type": "JSFunction", + "value": "function() { console.log('Submit button clicked'); }" + } + } +} +``` + +## Complete Examples + +### Example 1: Simple Page + +```json +{ + "componentName": "Page", + "fileName": "SimplePage", + "css": ".page-base-style {\n padding: 24px;\n background: #FFFFFF;\n}", + "props": { + "className": "page-base-style" + }, + "children": [ + { + "componentName": "CanvasFlexBox", + "id": "container", + "props": { + "flexDirection": "column", + "justifyContent": "center", + "alignItems": "center" + }, + "children": [ + { + "componentName": "Text", + "id": "title", + "props": { + "text": "Welcome to the Schema Protocol", + "style": "font-size: 24px; font-weight: bold; margin-bottom: 20px;" + } + }, + { + "componentName": "Text", + "id": "subtitle", + "props": { + "text": "A declarative Schema-based UI rendering protocol", + "style": "font-size: 16px; color: #666;" + } + } + ] + } + ], + "state": {}, + "methods": {}, + "id": "body" +} +``` + +### Example 2: Page with State and Events + +```json +{ + "componentName": "Page", + "fileName": "UserProfile", + "css": ".page-base-style {\n padding: 24px;\n}", + "props": { + "className": "page-base-style" + }, + "state": { + "userName": "John Doe", + "userAvatar": "https://www.example.com/avatar.jpg", + "userBio": "Full-stack developer" + }, + "methods": { + "handleClick": { + "type": "JSFunction", + "value": "function() { alert('Button clicked!'); }" + } + }, + "children": [ + { + "componentName": "CanvasFlexBox", + "id": "profile-container", + "props": { + "flexDirection": "column", + "alignItems": "center", + "gap": "20px" + }, + "children": [ + { + "componentName": "img", + "id": "avatar", + "props": { + "src": { + "type": "JSExpression", + "value": "this.state.userAvatar" + }, + "style": "width: 100px; height: 100px; border-radius: 50%;" + } + }, + { + "componentName": "Text", + "id": "name", + "props": { + "text": { + "type": "JSExpression", + "value": "this.state.userName" + }, + "style": "font-size: 24px; font-weight: bold;" + } + }, + { + "componentName": "Text", + "id": "bio", + "props": { + "text": { + "type": "JSExpression", + "value": "this.state.userBio" + }, + "style": "font-size: 16px; color: #666;" + } + }, + { + "componentName": "TinyButton", + "id": "action-btn", + "props": { + "text": "Click me", + "onClick": { + "type": "JSExpression", + "value": "this.handleClick" + } + } + } + ] + } + ], + "id": "body" +} +``` + +### Example 3: List with Loop Rendering + +```json +{ + "componentName": "Page", + "fileName": "ProductList", + "state": { + "products": [ + { "id": 1, "name": "Product A", "price": 100 }, + { "id": 2, "name": "Product B", "price": 200 }, + { "id": 3, "name": "Product C", "price": 300 } + ] + }, + "children": [ + { + "componentName": "CanvasFlexBox", + "id": "product-list", + "props": { + "flexDirection": "column", + "gap": "10px" + }, + "children": [ + { + "componentName": "div", + "id": "product-item", + "loop": { + "list": { + "type": "JSExpression", + "value": "this.state.products" + } + }, + "loopArgs": ["item", "index"], + "props": { + "style": "padding: 10px; border: 1px solid #ddd; border-radius: 4px;" + }, + "children": [ + { + "componentName": "Text", + "id": "product-name", + "props": { + "text": { + "type": "JSExpression", + "value": "item.name" + }, + "style": "font-size: 18px; font-weight: bold;" + } + }, + { + "componentName": "Text", + "id": "product-price", + "props": { + "text": { + "type": "JSExpression", + "value": "'Price: ¥' + item.price" + }, + "style": "font-size: 16px; color: #666;" + } + } + ] + } + ] + } + ], + "methods": {}, + "id": "body" +} +``` + +## Type Definitions + +### Complete TypeScript Type Definitions + +```typescript +// JS expression +export type JSExpression = { + type: 'JSExpression'; + value: string; + model?: boolean; + params?: string[]; +}; + +// JS function +export type JSFunction = { + type: 'JSFunction'; + value: string; +}; + +// Slot +export type JSSlot = { + type: 'JSSlot'; + value: string | Record +}; + +// Method collection +export type Methods = Record; + +// Property value type (recursive) +export type PropValue = + | string + | number + | boolean + | null + | JSExpression + | JSFunction + | JSSlot + | PropValue[] + | Record; + +// Node interface +export interface Node { + id?: string; // Unique node identifier (optional) + componentName: string; // Component name (required) + props?: Record & { + columns?: { slots?: Record }[] + }; // Component property collection + children?: Node[]; // Child node array + componentType?: 'Block' | 'PageStart' | 'PageSection'; // Node type + slot?: string | Record; // Slot content + params?: string[]; // Parameter name list + loop?: Record; // Loop rendering configuration + loopArgs?: string[]; // Loop parameter name list + condition?: boolean | Record; // Conditional rendering configuration +} + +// Root node type +export type RootNode = Omit & { + id?: string; // Optional id for root node + css?: string; // Global CSS style string + fileName?: string; // File name + methods?: Methods; // Method collection + state?: Record; // Global state + schema?: any; // Embedded or external Schema +}; +``` + +## Common Components + +### Layout Components + +- **CanvasFlexBox**: Flexbox layout container + - `flexDirection`: Main axis direction ('row' | 'column') + - `justifyContent`: Main axis alignment + - `alignItems`: Cross axis alignment + - `wrap`: Whether to wrap + - `gap`: Spacing + +- **div**: Generic container + - `style`: Inline style string + - `className`: CSS class name + +### Basic Components + +- **Text**: Text component + - `text`: Text content + - `style`: Style string + +- **img**: Image component + - `src`: Image URL + - `alt`: Alternative text + - `style`: Style string + +### Business Components + +- **TinyTabs**: Tabs component + - `modelValue`: Currently active tab + - `className`: CSS class name + +- **TinyTabItem**: Tab item + - `title`: Tab title + - `name`: Tab name + +- **TinyCarousel**: Carousel component + - `height`: Height + - `autoplay`: Whether to autoplay + - `interval`: Switch interval (milliseconds) + +- **TinyCarouselItem**: Carousel item + - `title`: Item title + +- **TinyButton**: Button component + - `text`: Button text + - `onClick`: Click event handler + +## FAQ + +### Q: How do I pass data between components? + +A: Define global state through the `state` field; child components access state via JS expressions using `this.state`. + +### Q: How do I implement conditional rendering? + +A: Use the node's `condition` field, which can be a boolean value or a JS expression. + +### Q: How do I implement list rendering? + +A: Use the node's `loop` and `loopArgs` fields; `loop` specifies the data source, and `loopArgs` specifies loop variable names. + +### Q: How do I implement two-way binding? + +A: Set `model: true` in a JSExpression; applicable to form components. + +### Q: How do I define component event handlers? + +A: There are two approaches: +1. Define methods in the root node's `methods`, then reference them in component properties using `this.methodName` (e.g. `this.handleClick`) +2. Define JSFunction directly in component properties + +## References + +- [TinyEngine Protocol Specification](https://opentiny.design/tiny-engine#/protocol) diff --git a/docs/src/examples/angular/renderer/custom-actions.md b/docs/src/examples/angular/renderer/custom-actions.md index 8b3869d5..5175f1f5 100644 --- a/docs/src/examples/angular/renderer/custom-actions.md +++ b/docs/src/examples/angular/renderer/custom-actions.md @@ -8,8 +8,10 @@ - `name`: 动作名称 - `description`: 动作描述 +- `parameters`: 参数 JSON Schema 描述 +- `return`: (可选)返回值 JSON Schema 描述,无返回值时可省略 +- `async`: (可选)是否为异步 Action,默认为 `false`;为 `true` 时 `execute` 返回 Promise - `execute`: 执行函数,接收 `params` 和 `context` 两个参数 -- `params`: 参数定义数组(可选),用于描述动作接收的参数,大模型根据描述生成参数传递给`execute`第一个参数 ### execute 函数参数说明 diff --git a/docs/src/examples/chat/custom-actions.md b/docs/src/examples/chat/custom-actions.md index 9b5c7fcd..75603573 100644 --- a/docs/src/examples/chat/custom-actions.md +++ b/docs/src/examples/chat/custom-actions.md @@ -5,15 +5,18 @@ ## 基础用法 ```vue {14-36} ``` -完成以上步骤后,即可开始体验生成式 UI 了 +完成以上步骤后,即可开始体验生成式 UI 了。 + ![使用 Renderer 组件示例](../public/quick-start.png) +## 按需引入 + +`@opentiny/genui-sdk-vue` 除主入口外,还提供按功能拆分的子路径导出。只需 Chat 或只需 Renderer 时,可从对应子路径引入,在构建工具对摇树不友好时,避免打入未使用的模块。 + +| 子路径 | 适用场景 | 主要导出内容 | +| --- | --- | --- | +| `@opentiny/genui-sdk-vue/chat` | 仅需对话组件 | `GenuiChat` | +| `@opentiny/genui-sdk-vue/renderer` | 仅需渲染器(自建对话 UI) | `GenuiRenderer` | +| `@opentiny/genui-sdk-vue/config-provider` | 主题/国际化/物料配置容器 | `GenuiConfigProvider` | + +```ts +import { GenuiChat } from '@opentiny/genui-sdk-vue/chat'; +import { GenuiConfigProvider } from '@opentiny/genui-sdk-vue/config-provider'; +import { materials } from '@opentiny/genui-sdk-materials-vue-opentiny-vue/materials'; + +// 仅使用 Renderer +import { GenuiRenderer } from '@opentiny/genui-sdk-vue/renderer'; +``` + +::: tip +1.3.0 版本进行了物料解耦重构。若需使用内置 TinyVue 组件物料,可使用 `GenuiLegacyChat`,详见 [GenuiChat Legacy 兼容说明](../components/chat#兼容组件-genuilegacychat)。 +::: + ## 其他相关文档 - 查看 [组件文档](../components/chat) 了解 `GenuiChat` 的详细 API diff --git a/docs/src/guide/renderer-with-tiny-robot.md b/docs/src/guide/renderer-with-tiny-robot.md index f1baa591..3693cfc7 100644 --- a/docs/src/guide/renderer-with-tiny-robot.md +++ b/docs/src/guide/renderer-with-tiny-robot.md @@ -7,15 +7,15 @@ :::: tabs == npm ```bash -npm install @opentiny/genui-sdk-vue @opentiny/tiny-robot @opentiny/tiny-robot-kit +npm install @opentiny/genui-sdk-vue @opentiny/genui-sdk-materials-vue-opentiny-vue @opentiny/tiny-robot @opentiny/tiny-robot-kit ``` == pnpm ```bash -pnpm add @opentiny/genui-sdk-vue @opentiny/tiny-robot @opentiny/tiny-robot-kit +pnpm add @opentiny/genui-sdk-vue @opentiny/genui-sdk-materials-vue-opentiny-vue @opentiny/tiny-robot @opentiny/tiny-robot-kit ``` == yarn ```bash -yarn add @opentiny/genui-sdk-vue @opentiny/tiny-robot @opentiny/tiny-robot-kit +yarn add @opentiny/genui-sdk-vue @opentiny/genui-sdk-materials-vue-opentiny-vue @opentiny/tiny-robot @opentiny/tiny-robot-kit ``` :::: @@ -29,97 +29,48 @@ import { type ChatCompletionRequest, type ChatCompletionStreamResponse, } from '@opentiny/tiny-robot-kit'; +import { PatternExtractor, type IChatMessage } from '@opentiny/genui-sdk-core'; import { reactive } from 'vue'; -import type { IChatMessage } from '@opentiny/genui-sdk-vue'; -// 简化的 Schema 流式处理逻辑(只处理 schema-card 和 markdown) -function useSchemaStream() { - let inSchemaStream = false; - let bufferText = ''; - - const schemaFlag = '```schemaJson'; - const endFlag = '```'; - - const isSchemaJsonStart = (str: string): boolean => { - const index = str.indexOf('`'); - if (index === -1) return false; - return schemaFlag.startsWith(str.substring(index, index + schemaFlag.length)); - }; - - const isSchemaJsonEnd = (str: string): boolean => { - const index = str.lastIndexOf('\n'); - if (index === -1) return false; - if (str.includes(`\n${endFlag}`)) { - return true; - } - const newStr = str.slice(index).trim().substring(0, endFlag.length); - return endFlag.startsWith(newStr); - }; - - const handleSchemaStream = (content: string, chatMessage: IChatMessage): boolean => { - if (!content || typeof content !== 'string') return false; - - const deltaPart = bufferText + content; - - if ((!inSchemaStream && isSchemaJsonStart(deltaPart)) || (inSchemaStream && isSchemaJsonEnd(deltaPart))) { - const matchFlag = inSchemaStream ? /(\n\s*)```/ : schemaFlag; - const matchPart = deltaPart.match(matchFlag)?.[0]; - if (!matchPart) { - bufferText = deltaPart; - return true; - } - - chatMessage.content += deltaPart; - - if (inSchemaStream) { - const trimmedDelta = deltaPart.trim(); - const [schemaPart, markdownPart] = trimmedDelta.split(matchPart); - const lastMessage = chatMessage.messages[chatMessage.messages.length - 1]; - if (lastMessage?.type === 'schema-card') { - lastMessage.content += schemaPart; - } - if (markdownPart) { - chatMessage.messages.push({ type: 'markdown', content: markdownPart }); - } - } else { - const trimmedDelta = deltaPart.trim(); - const [markdownPart, schemaPart] = trimmedDelta.split(matchPart); - if (markdownPart) { - const lastMessage = chatMessage.messages[chatMessage.messages.length - 1]; - if (lastMessage && lastMessage.type === 'markdown') { - lastMessage.content += markdownPart; - } else { - chatMessage.messages.push({ type: 'markdown', content: markdownPart }); - } - } - chatMessage.messages.push({ type: 'schema-card', content: schemaPart }); - } - - inSchemaStream = !inSchemaStream; - bufferText = ''; - return true; - } +function appendMarkdown(content: string, chatMessage: IChatMessage) { + const lastMessage = chatMessage.messages[chatMessage.messages.length - 1]; + if (lastMessage?.type === 'markdown') { + lastMessage.content += content; + } else { + chatMessage.messages.push({ type: 'markdown', content }); + } +} - bufferText = ''; +function appendSchemaCard(content: string, chatMessage: IChatMessage) { + const lastMessage = chatMessage.messages[chatMessage.messages.length - 1]; + if (lastMessage?.type === 'schema-card') { + lastMessage.content += content; + } else { + chatMessage.messages.push({ type: 'schema-card', content }); + } +} - if (inSchemaStream) { - chatMessage.content += deltaPart; - const lastMessage = chatMessage.messages[chatMessage.messages.length - 1]; - if (lastMessage && lastMessage.type === 'schema-card') { - lastMessage.content += deltaPart; - } - return true; - } +// 用 PatternExtractor 拆分 markdown 与 schemaJson(默认 SchemaJsonPattern) +function useSchemaStream() { + let chatMessageRef: IChatMessage | null = null; - chatMessage.content += deltaPart; - const lastMessage = chatMessage.messages[chatMessage.messages.length - 1]; - if (lastMessage?.type === 'markdown') { - lastMessage.content += deltaPart; - } else { - chatMessage.messages.push({ type: 'markdown', content: deltaPart }); - } + const patternExtractor = new PatternExtractor({ + onNormalWrite: (value) => { + if (!chatMessageRef) return; + chatMessageRef.content += value; + appendMarkdown(value, chatMessageRef); + }, + onHandledWrite: (value) => { + if (!chatMessageRef) return; + chatMessageRef.content += value; + appendSchemaCard(value, chatMessageRef); + }, + }); - return false; + const handleSchemaStream = (content: string, chatMessage: IChatMessage) => { + if (!content || typeof content !== 'string') return; + chatMessageRef = chatMessage; + patternExtractor.handleContent(content); }; return { handleSchemaStream }; @@ -224,7 +175,9 @@ export class CustomModelProvider extends BaseModelProvider { ```vue ``` +::: tip GenuiRenderer +若无需单独配置物料、需兼容 1.3.0 之前用法,见 [GenuiRenderer Legacy 兼容说明](../components/renderer#兼容组件-genuilegacyrenderer)。 +::: + ## 其他相关文档 - 查看 [Renderer 组件文档](../components/renderer) 了解详细的 API diff --git a/docs/src/guide/server-usage.md b/docs/src/guide/server-usage.md index 5ee51e70..67138260 100644 --- a/docs/src/guide/server-usage.md +++ b/docs/src/guide/server-usage.md @@ -108,7 +108,7 @@ app.listen(3000); **请求格式**(OpenAI 兼容): -```json +```jsonc { "model": "gpt-4", "messages": [ @@ -131,7 +131,7 @@ app.listen(3000); **响应格式**(Server-Sent Events): -``` +```text data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1694268190,"model":"gpt-4","choices":[{"index":0,"delta":{"content":"Hello"},"finish_reason":null}]} data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1694268190,"model":"gpt-4","choices":[{"index":0,"delta":{"content":"!"},"finish_reason":null}]} diff --git a/docs/src/guide/start-with-renderer.md b/docs/src/guide/start-with-renderer.md index 5f71cd2c..577bc3fe 100644 --- a/docs/src/guide/start-with-renderer.md +++ b/docs/src/guide/start-with-renderer.md @@ -4,9 +4,11 @@ ## 使用 fetch 请求服务,处理流式返回 -创建一个文件 `fetch-schema-stream.ts`, 文件中的处理逻辑都是基于 OpenAI 兼容格式处理: +创建一个文件 `fetch-schema-stream.ts`。基于 OpenAI 兼容 SSE 解析 `delta.content`,再用 core 的 `PatternExtractor` 提取 `` ```schemaJson `` 片段(默认 `SchemaJsonPattern`): ````typescript +import { PatternExtractor } from '@opentiny/genui-sdk-core'; + export async function fetchSchemaStream( url: string, userInput: string, @@ -26,33 +28,14 @@ export async function fetchSchemaStream( throw new Error(`HTTP error! status: ${response.status}`); } - const reader = response.body.getReader(); + const reader = response.body!.getReader(); const decoder = new TextDecoder('utf-8'); let buffer = ''; - let inSchemaStream = false; - let bufferText = ''; - let schemaFinished = false; - const startFlag = '```schemaJson'; - const endFlag = '```'; - - // 检测 schema 开始标记 - const isSchemaJsonStart = (str: string): boolean => { - const index = str.indexOf('`'); - if (index === -1) return false; - return startFlag.startsWith(str.substring(index, index + startFlag.length)); - }; - - // 检测 schema 结束标记 - const isSchemaJsonEnd = (str: string): boolean => { - const index = str.lastIndexOf('\n'); - if (index === -1) return false; - if (str.includes(`\n${endFlag}`)) { - return true; - } - const newStr = str.slice(index).trim().substring(0, endFlag.length); - return endFlag.startsWith(newStr); - }; + const patternExtractor = new PatternExtractor({ + onNormalWrite: () => {}, + onHandledWrite: (value) => onSchemaUpdate(value), + }); try { while (true) { @@ -72,7 +55,7 @@ export async function fetchSchemaStream( const dataStr = line.slice(5).trim(); - if (dataStr === '[DONE]' || schemaFinished) { + if (dataStr === '[DONE]') { return; } @@ -82,43 +65,7 @@ export async function fetchSchemaStream( if (!content) continue; - const deltaPart = bufferText + content; - - // 检测是否进入或退出 schema 流 - if ((!inSchemaStream && isSchemaJsonStart(deltaPart)) || (inSchemaStream && isSchemaJsonEnd(deltaPart))) { - const matchFlag = inSchemaStream ? /(\n\s*)```/ : startFlag; - const matchPart = deltaPart.match(matchFlag)?.[0]; - - if (!matchPart) { - // 标记不完整,保留到下次 - bufferText = deltaPart; - continue; - } - - if (inSchemaStream) { - const trimmedDelta = deltaPart.trim(); - const [schemaPart] = trimmedDelta.split(matchPart); - if (schemaPart) { - onSchemaUpdate(schemaPart); - } - schemaFinished = true; - return; - } else { - const trimmedDelta = deltaPart.trim(); - const [, schemaPart] = trimmedDelta.split(matchPart); - inSchemaStream = true; - bufferText = ''; - if (schemaPart) { - onSchemaUpdate(schemaPart); - } - continue; - } - } - - bufferText = ''; - if (inSchemaStream) { - onSchemaUpdate(deltaPart); - } + patternExtractor.handleContent(content); } catch (e) { console.error('解析后端数据失败:', e, dataStr); } @@ -136,18 +83,22 @@ export async function fetchSchemaStream( ```vue ``` + ## Documentation * [quick-start](https://docs.opentiny.design/genui-sdk/guide/quick-start) @@ -34,4 +42,4 @@ import { GenuiChat, GenuiConfigProvider } from '@opentiny/genui-sdk-vue'; * [GenuiRender](https://docs.opentiny.design/genui-sdk/components/renderer) * [GenuiChat](https://docs.opentiny.design/genui-sdk/components/chat) -* [GenuiConfigProvider](https://docs.opentiny.design/genui-sdk/components/config-provider) \ No newline at end of file +* [GenuiConfigProvider](https://docs.opentiny.design/genui-sdk/components/config-provider) diff --git a/packages/frameworks/vue/package.json b/packages/frameworks/vue/package.json index a37414b6..1b0870c8 100644 --- a/packages/frameworks/vue/package.json +++ b/packages/frameworks/vue/package.json @@ -20,28 +20,36 @@ "generative-ui", "stream-ui" ], - "main": "output/dist/index.js", - "types": "output/dist/index.d.ts", + "main": "dist/index.js", + "types": "dist/index.d.ts", "exports": { ".": { - "types": "./output/dist/index.d.ts", - "import": "./output/dist/index.js" + "types": "./dist/index.d.ts", + "import": "./dist/index.js" }, "./chat": { - "types": "./output/dist/chat.d.ts", - "import": "./output/dist/chat.js" + "types": "./dist/chat.d.ts", + "import": "./dist/chat.js" + }, + "./legacy-chat": { + "types": "./dist/legacy-chat.d.ts", + "import": "./dist/legacy-chat.js" }, "./renderer": { - "types": "./output/dist/renderer.d.ts", - "import": "./output/dist/renderer.js" + "types": "./dist/renderer.d.ts", + "import": "./dist/renderer.js" + }, + "./legacy-renderer": { + "types": "./dist/legacy-renderer.d.ts", + "import": "./dist/legacy-renderer.js" }, "./config-provider": { - "types": "./output/dist/config-provider.d.ts", - "import": "./output/dist/config-provider.js" + "types": "./dist/config-provider.d.ts", + "import": "./dist/config-provider.js" }, "./transform-jsx": { - "types": "./output/dist/transform-jsx.d.ts", - "import": "./output/dist/transform-jsx.js" + "types": "./dist/transform-jsx.d.ts", + "import": "./dist/transform-jsx.js" } }, "type": "module", @@ -51,60 +59,27 @@ ], "scripts": { "build": "vite build", - "postbuild": "tsx scripts/postbuild.ts", "analyze": "vite build --mode analyze", "prebuild:lib:npm": "pnpm -F @opentiny/genui-sdk-core build && pnpm -F @opentiny/genui-sdk-materials-vue-opentiny-vue build && pnpm -F @opentiny/tiny-schema-renderer build", "build:lib:npm": "pnpm build", "test": "vitest" }, "dependencies": { + "@opentiny/genui-sdk-core": "workspace:*", + "@opentiny/genui-sdk-materials-vue-opentiny-vue": "workspace:*", "@opentiny/tiny-robot": "0.3.3", "@opentiny/tiny-robot-kit": "0.3.3", "@opentiny/tiny-robot-svgs": "0.3.3", - "@opentiny/vue": "^3.28.0", - "@opentiny/vue-renderless": "^3.28.2", - "@opentiny/vue-icon": "^3.28.0", - "@opentiny/vue-theme": "^3.28.0", + "@opentiny/vue": "~3.28.0", + "@opentiny/vue-renderless": "~3.28.2", + "@opentiny/vue-icon": "~3.28.0", + "@opentiny/vue-theme": "~3.28.0", "uuid": "^11.1.0", "vue": "^3.5.13", - "@opentiny/vue-chart-bar": "~3.14.0", - "@opentiny/vue-chart-histogram": "~3.14.0", - "@opentiny/vue-chart-line": "~3.14.0", - "@opentiny/vue-chart-pie": "~3.14.0", - "@opentiny/vue-chart-radar": "~3.14.0", - "@opentiny/vue-chart-ring": "3.14.0", - "@opentiny/vue-carousel": "^3.28.0", - "@opentiny/vue-carousel-item": "^3.28.0", - "@opentiny/vue-checkbox-button": "^3.28.0", - "@opentiny/vue-checkbox-group": "^3.28.0", - "@opentiny/vue-col": "^3.28.0", - "@opentiny/vue-date-picker": "^3.28.0", - "@opentiny/vue-layout": "^3.28.0", - "@opentiny/vue-row": "^3.28.0", - "@opentiny/vue-search": "^3.28.0", - "@opentiny/vue-select": "^3.28.0", - "@opentiny/vue-transfer": "^3.28.0", - "@opentiny/vue-button": "^3.28.0", - "@opentiny/vue-grid": "^3.28.0", - "@opentiny/vue-form": "^3.28.0", - "@opentiny/vue-form-item": "^3.28.0", - "@opentiny/vue-input": "^3.28.0", - "@opentiny/vue-notify": "^3.28.0", - "@opentiny/vue-card": "^3.28.0", - "@opentiny/vue-checkbox": "^3.28.0", - "@opentiny/vue-numeric": "^3.28.0", - "@opentiny/vue-radio": "^3.28.0", - "@opentiny/vue-switch": "^3.28.0", - "@opentiny/vue-tabs": "^3.28.0", - "@opentiny/vue-tab-item": "^3.28.0", - "@opentiny/vue-tree": "^3.28.0", - "@opentiny/vue-radio-group": "^3.28.0", "@opentiny/tiny-engine-builtin-component": "^2.6.0" }, "devDependencies": { "@vitejs/plugin-vue": "^5.0.0", - "@opentiny/genui-sdk-core": "workspace:*", - "@opentiny/genui-sdk-materials-vue-opentiny-vue": "workspace:*", "@opentiny/tiny-schema-renderer": "workspace:*", "rollup-plugin-visualizer": "^6.0.5", "vite-plugin-css-injected-by-js": "^3.5.2", diff --git a/packages/frameworks/vue/scripts/postbuild.ts b/packages/frameworks/vue/scripts/postbuild.ts deleted file mode 100644 index 43af7bb8..00000000 --- a/packages/frameworks/vue/scripts/postbuild.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { readFileSync, writeFileSync, copyFileSync } from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import { basename, dirname, join } from 'node:path'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -const packageJsonPath = join(__dirname, '../package.json'); -const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')); - -const outputDir = join(__dirname, '../output'); -const packageJsonOutputPrefix = `${basename(outputDir)}/`; - -// 创建新的 package.json 对象,只包含需要的字段 -const outputPackageJson: any = { - name: packageJson.name, - version: packageJson.version, - description: packageJson.description, - author: packageJson.author, - license: packageJson.license, - homepage: packageJson.homepage, - repository: packageJson.repository, - bugs: packageJson.bugs, - keywords: packageJson.keywords, - main: packageJson.main.replace(packageJsonOutputPrefix, ''), - types: packageJson.types.replace(packageJsonOutputPrefix, ''), - exports: normalizeExports(packageJson.exports, packageJsonOutputPrefix), - type: packageJson.type, - files: packageJson.files, - dependencies: { ...packageJson.dependencies }, -}; - -// 修改 @opentiny/genui-sdk-core 的版本(从 "workspace:*" 改为实际版本号) -if (outputPackageJson.dependencies && outputPackageJson.dependencies['@opentiny/genui-sdk-core'] === 'workspace:*') { - const corePackageJsonPath = join(__dirname, '../../../core/package.json'); - const corePackageJson = JSON.parse(readFileSync(corePackageJsonPath, 'utf-8')); - outputPackageJson.dependencies['@opentiny/genui-sdk-core'] = corePackageJson.version; -} - -const outputPackageJsonPath = join(outputDir, 'package.json'); -writeFileSync(outputPackageJsonPath, JSON.stringify(outputPackageJson, null, 2) + '\n', 'utf-8'); - -// 复制 packages/server/README.md 到 output 文件夹 -const serverReadmePath = join(__dirname, '../README.md'); -const outputReadmePath = join(outputDir, 'README.md'); -copyFileSync(serverReadmePath, outputReadmePath); - -function normalizeExports(exportsField: Record = {}, outputPathPrefix: string) { - return Object.fromEntries( - Object.entries(exportsField).map(([subpath, condition]) => { - if (typeof condition === 'string') { - return [subpath, condition.replace(outputPathPrefix, '')]; - } - - const normalizedCondition = Object.fromEntries( - Object.entries(condition || {}).map(([conditionName, target]) => [ - conditionName, - String(target).replace(outputPathPrefix, ''), - ]), - ); - - return [subpath, normalizedCondition]; - }), - ); -} diff --git a/packages/frameworks/vue/src/chat/CustomModelProvider.ts b/packages/frameworks/vue/src/chat/CustomModelProvider.ts index 1ea2220b..92139f7e 100644 --- a/packages/frameworks/vue/src/chat/CustomModelProvider.ts +++ b/packages/frameworks/vue/src/chat/CustomModelProvider.ts @@ -99,8 +99,7 @@ export class CustomModelProvider extends BaseModelProvider { const reader = bodyStream.getReader(); const context: any = {}; - const { chatConfig } = this.getChatOptions(); - context.chatConfig = chatConfig; + this.setupStreamContext(context, request); const signal = request.options?.signal; signal?.addEventListener('abort', @@ -120,6 +119,11 @@ export class CustomModelProvider extends BaseModelProvider { } + protected setupStreamContext(context: Record, _request: ChatCompletionRequest) { + const { chatConfig } = this.getChatOptions(); + context.chatConfig = chatConfig; + } + handlerChunk(rawData: string, context: any) { try { const streamData = JSON.parse(rawData) as IStreamData; diff --git a/packages/frameworks/vue/src/chat/GeneratingComponent.vue b/packages/frameworks/vue/src/chat/GeneratingComponent.vue index f0645273..37f2da8d 100644 --- a/packages/frameworks/vue/src/chat/GeneratingComponent.vue +++ b/packages/frameworks/vue/src/chat/GeneratingComponent.vue @@ -10,6 +10,8 @@ const { t } = useI18n(); const loadingText = ref(t('loading.response')); +const hasSchemaCard = ref(false); + const toolStatusTextMap = new Map([ ['running', { textKey: 'toolStatus.running' }], ['success', { textKey: 'toolStatus.success' }], @@ -43,7 +45,10 @@ const handleNotification = (payload: INotificationPayload) => { // type === 'markdown' const lastMessage = payload.chatMessage.messages[payload.chatMessage.messages.length - 1]; if (lastMessage) { - loadingText.value = `${lastMessage.content}...`; + if (!hasSchemaCard.value) { + hasSchemaCard.value = payload.chatMessage.messages?.some((item: any) => item.type.startsWith('schema-card')); + } + loadingText.value = hasSchemaCard.value ? `${lastMessage.content}...` : t('loading.response'); } }; @@ -58,7 +63,7 @@ onBeforeUnmount(() => { - - - \ No newline at end of file +} + + + diff --git a/packages/frameworks/vue/src/chat/response-handler.ts b/packages/frameworks/vue/src/chat/response-handler.ts index eff57010..3f16f1e4 100644 --- a/packages/frameworks/vue/src/chat/response-handler.ts +++ b/packages/frameworks/vue/src/chat/response-handler.ts @@ -1,6 +1,6 @@ -import { IChatMessage, IMessageItem, IStreamDelta, IStreamData, PatternExtractor } from "@opentiny/genui-sdk-core"; +import { IChatMessage, IMessageItem, IStreamDelta, IStreamData, PatternExtractor } from '@opentiny/genui-sdk-core'; import { ThinkTagWrapPattern } from './think-tag-wrap-pattern'; -import { reactive, toRaw, watch } from "vue"; +import { reactive, readonly, watch } from 'vue'; import { v4 as uuidv4 } from 'uuid'; import { emitter } from './event-emitter'; import { useI18n } from './i18n'; @@ -10,7 +10,10 @@ export interface IResponseHandler { match: (data: T, context: any) => boolean; handler: (data: T, context: any) => boolean; notMatchHandler?: (data: T, context: any) => boolean; - start?: (context: any, handlers: { onData: (data: IChatMessage) => void, onDone: () => void, onError: (error: Error) => void }) => void; + start?: ( + context: any, + handlers: { onData: (data: IChatMessage) => void; onDone: () => void; onError: (error: Error) => void }, + ) => void; end?: (context: any) => void; } @@ -18,7 +21,13 @@ const getStreamDelta = (data: IStreamData): IStreamDelta => { return data.choices?.[0]?.delta ?? {}; }; -function onToolResult(toolCallsResult: any[], delta: IStreamDelta, toolCallIdMap: Record, chatMessage: IChatMessage, addToolCallContext: boolean) { +function onToolResult( + toolCallsResult: any[], + delta: IStreamDelta, + toolCallIdMap: Record, + chatMessage: IChatMessage, + addToolCallContext: boolean, +) { const { id, function: { arguments: args, result }, @@ -31,8 +40,8 @@ function onToolResult(toolCallsResult: any[], delta: IStreamDelta, toolCallIdMap emitter.emit('notification', { type: 'tool', delta, - toolCallData: structuredClone(toRaw(toolCallItem)), - chatMessage: structuredClone(toRaw(chatMessage)), + toolCallData: readonly(toolCallItem), + chatMessage: readonly(chatMessage), }); if (addToolCallContext) { @@ -45,9 +54,15 @@ function onToolResult(toolCallsResult: any[], delta: IStreamDelta, toolCallIdMap }) + '\n\n'; } } -}; +} -function onToolCall(toolCalls: any[], delta: IStreamDelta, toolCallIdMap: Record, chatMessage: IChatMessage, toolCallStatus: { inProcessToolCallId: string | null }) { +function onToolCall( + toolCalls: any[], + delta: IStreamDelta, + toolCallIdMap: Record, + chatMessage: IChatMessage, + toolCallStatus: { inProcessToolCallId: string | null }, +) { toolCalls.forEach((toolCall) => { const { id, @@ -79,17 +94,15 @@ function onToolCall(toolCalls: any[], delta: IStreamDelta, toolCallIdMap: Record emitter.emit('notification', { type: 'tool', delta, - toolCallData: toolCallItem, - chatMessage: structuredClone(toRaw(chatMessage)), + toolCallData: readonly(toolCallItem), + chatMessage: readonly(chatMessage), }); - }); - -}; +} function onReasoningContent(reasoningContent: string, delta: IStreamDelta, chatMessage: IChatMessage) { const lastMessage = chatMessage.messages[chatMessage.messages.length - 1]; - let reasoningMessage = lastMessage + let reasoningMessage = lastMessage; if (reasoningMessage?.type === 'reasoning') { reasoningMessage.content += reasoningContent; } else { @@ -102,11 +115,11 @@ function onReasoningContent(reasoningContent: string, delta: IStreamDelta, chatM } emitNotification(delta, chatMessage); return reasoningMessage; -}; +} function onReasoningEnd(reasoningMessage: IMessageItem) { if (reasoningMessage?.type === 'reasoning') reasoningMessage.thinking = false; -}; +} function emitNotification(delta: IStreamDelta, chatMessage: IChatMessage) { const lastMessage = chatMessage.messages[chatMessage.messages.length - 1]; @@ -114,10 +127,10 @@ function emitNotification(delta: IStreamDelta, chatMessage: IChatMessage) { emitter.emit('notification', { type: lastMessage.type as 'markdown' | 'schema-card', delta, - chatMessage: structuredClone(toRaw(chatMessage)), + chatMessage: readonly(chatMessage), }); } -}; +} function onMarkdown(content: string, delta: IStreamDelta, chatMessage: IChatMessage) { if (chatMessage.messages.length > 0 && chatMessage.messages[chatMessage.messages.length - 1].type === 'markdown') { @@ -125,11 +138,11 @@ function onMarkdown(content: string, delta: IStreamDelta, chatMessage: IChatMess } else { chatMessage.messages.push({ type: 'markdown', - content: content + content: content, }); } emitNotification(delta, chatMessage); -}; +} function onSchemaJSON(content: string, delta: IStreamDelta, chatMessage: IChatMessage) { if (chatMessage.messages.length > 0 && chatMessage.messages[chatMessage.messages.length - 1].type === 'schema-card') { @@ -144,14 +157,18 @@ function onSchemaJSON(content: string, delta: IStreamDelta, chatMessage: IChatMe emitNotification(delta, chatMessage); } -function watchReasoningEnd (context: any) { - context.unWatchReasoning = watch(() => [...context.chatMessage.messages], (newVal) => { - if (context.handleReasoning && newVal[newVal.length - 1]?.type !== 'reasoning') { - context.handleReasoning = false; - onReasoningEnd(context.reasoningMessage); - context.unWatchReasoning?.(); - } - }, { flush: 'sync' }); +function watchReasoningEnd(context: any) { + context.unWatchReasoning = watch( + () => [...context.chatMessage.messages], + (newVal) => { + if (context.handleReasoning && newVal[newVal.length - 1]?.type !== 'reasoning') { + context.handleReasoning = false; + onReasoningEnd(context.reasoningMessage); + context.unWatchReasoning?.(); + } + }, + { flush: 'sync' }, + ); } export const defaultResponseHandlers: IResponseHandler[] = [ @@ -159,7 +176,10 @@ export const defaultResponseHandlers: IResponseHandler[] = [ name: 'init', match: (data: IStreamData, context: any) => false, handler: (data: IStreamData, context: any) => false, - start: (context: any, handlers: { onData: (data: IChatMessage) => void, onDone: () => void, onError: (error: Error) => void }) => { + start: ( + context: any, + handlers: { onData: (data: IChatMessage) => void; onDone: () => void; onError: (error: Error) => void }, + ) => { const chatMessage = reactive({ role: 'assistant', content: '', @@ -175,7 +195,7 @@ export const defaultResponseHandlers: IResponseHandler[] = [ emitter.emit('notification', { type: 'done', delta: {}, - chatMessage: structuredClone(toRaw(context.chatMessage)), + chatMessage: readonly(context.chatMessage), }); }, }, @@ -205,7 +225,10 @@ export const defaultResponseHandlers: IResponseHandler[] = [ } return true; }, - start: (context: any, handlers: { onData: (data: IChatMessage) => void, onDone: () => void, onError: (error: Error) => void }) => { + start: ( + context: any, + handlers: { onData: (data: IChatMessage) => void; onDone: () => void; onError: (error: Error) => void }, + ) => { context.handleReasoning = false; }, end: (context: any) => { @@ -214,7 +237,7 @@ export const defaultResponseHandlers: IResponseHandler[] = [ context.handleReasoning = false; onReasoningEnd(context.reasoningMessage); } - } + }, }, { name: 'toolCall', @@ -227,7 +250,10 @@ export const defaultResponseHandlers: IResponseHandler[] = [ onToolCall(delta.tool_calls, delta, context.toolCallIdMap, context.chatMessage, context.toolCallStatus); return true; }, - start: (context: any, handlers: { onData: (data: IChatMessage) => void, onDone: () => void, onError: (error: Error) => void }) => { + start: ( + context: any, + handlers: { onData: (data: IChatMessage) => void; onDone: () => void; onError: (error: Error) => void }, + ) => { context.toolCallIdMap = {}; context.toolCallStatus = { inProcessToolCallId: null }; }, @@ -240,7 +266,13 @@ export const defaultResponseHandlers: IResponseHandler[] = [ }, handler: (data: IStreamData, context: any) => { const delta = getStreamDelta(data); - onToolResult(delta.tool_calls_result, delta, context.toolCallIdMap, context.chatMessage, context.chatConfig.addToolCallContext); + onToolResult( + delta.tool_calls_result, + delta, + context.toolCallIdMap, + context.chatMessage, + context.chatConfig.addToolCallContext, + ); return true; }, }, @@ -253,15 +285,18 @@ export const defaultResponseHandlers: IResponseHandler[] = [ handler: (data: IStreamData, context: any) => { const delta = getStreamDelta(data); context.delta = delta; - context.patternExtractor.handleContent(delta.content) + context.patternExtractor.handleContent(delta.content); context.chatMessage.content += delta.content; return true; }, - start: (context: any, handlers: { onData: (data: IChatMessage) => void, onDone: () => void, onError: (error: Error) => void }) => { + start: ( + context: any, + handlers: { onData: (data: IChatMessage) => void; onDone: () => void; onError: (error: Error) => void }, + ) => { const thinkPatternExtractor = new PatternExtractor({ onNormalWrite: (value) => onMarkdown(value, context.delta, context.chatMessage), - onHandledWrite: (value) => { - context.reasoningMessage = onReasoningContent(value, context.delta, context.chatMessage) + onHandledWrite: (value) => { + context.reasoningMessage = onReasoningContent(value, context.delta, context.chatMessage); if (!context.handleReasoning) { watchReasoningEnd(context); context.handleReasoning = true; @@ -274,5 +309,5 @@ export const defaultResponseHandlers: IResponseHandler[] = [ onHandledWrite: (value) => onSchemaJSON(value, context.delta, context.chatMessage), }); }, - } + }, ]; diff --git a/packages/frameworks/vue/src/chat/think-tag-wrap-pattern.ts b/packages/frameworks/vue/src/chat/think-tag-wrap-pattern.ts index 0cabc8ec..c76a6c44 100644 --- a/packages/frameworks/vue/src/chat/think-tag-wrap-pattern.ts +++ b/packages/frameworks/vue/src/chat/think-tag-wrap-pattern.ts @@ -1,4 +1,4 @@ -import { getPartialStartRegString } from "@opentiny/genui-sdk-core"; +import { getPartialStartRegString } from '@opentiny/genui-sdk-core'; export class ThinkTagWrapPattern { protected thinkStartFlag: string = ''; @@ -18,6 +18,6 @@ export class ThinkTagWrapPattern { full: this.endRegex, partial: this.partialEndRegex, }, - } + }; } } diff --git a/packages/frameworks/vue/src/config-provider/ConfigProvider.vue b/packages/frameworks/vue/src/config-provider/ConfigProvider.vue index 7cbf0beb..e3e9f433 100644 --- a/packages/frameworks/vue/src/config-provider/ConfigProvider.vue +++ b/packages/frameworks/vue/src/config-provider/ConfigProvider.vue @@ -3,8 +3,9 @@ import { TinyConfigProvider } from '@opentiny/vue'; import { ThemeProvider } from '@opentiny/tiny-robot'; import ThemeTool, { tinyDarkTheme, tinyOldTheme } from '@opentiny/vue-theme/theme-tool'; import { watch, provide, computed, onMounted, ref } from 'vue'; +import type { IMaterials } from '@opentiny/genui-sdk-core'; import { I18nMessages, useI18n } from '../chat/i18n'; -import { GENUI_I18N, GENUI_CONFIG } from '../chat/injection-tokens'; +import { GENUI_I18N, GENUI_CONFIG, GENUI_MATERIALS } from './injection-tokens'; import { useMediaTheme } from './use-media-theme'; export interface ConfigProviderProps { @@ -12,6 +13,7 @@ export interface ConfigProviderProps { id?: string; locale?: string; i18n?: I18nMessages; + materials?: IMaterials; } interface IRobotProviderProps { @@ -59,10 +61,22 @@ const genuiConfig = computed(() => { provide(GENUI_CONFIG, genuiConfig); +const internalMaterials = {}; +watch(() => props.materials, (newVal) => { + Object.assign(internalMaterials, newVal); +}, { immediate: true }); + +provide( + GENUI_MATERIALS, + internalMaterials, +); + watch( () => [props.locale, props.i18n] as const, () => { - i18n.setLocale(props.locale); + if (props.locale && props.locale !== i18n.locale.value) { + i18n.setLocale(props.locale); + } props.i18n && i18n.mergeMessages(props.i18n); }, { immediate: true }, diff --git a/packages/frameworks/vue/src/config-provider/index.ts b/packages/frameworks/vue/src/config-provider/index.ts index ea703178..b04735eb 100644 --- a/packages/frameworks/vue/src/config-provider/index.ts +++ b/packages/frameworks/vue/src/config-provider/index.ts @@ -1,2 +1,3 @@ export { default as GenuiConfigProvider } from './ConfigProvider.vue'; export * from './use-media-theme'; +export * from './injection-tokens.js'; diff --git a/packages/frameworks/vue/src/config-provider/injection-tokens.ts b/packages/frameworks/vue/src/config-provider/injection-tokens.ts new file mode 100644 index 00000000..df4d7cb6 --- /dev/null +++ b/packages/frameworks/vue/src/config-provider/injection-tokens.ts @@ -0,0 +1,7 @@ +import type { InjectionKey } from 'vue'; +import type { IMaterials } from '@opentiny/genui-sdk-core'; + +export const GENUI_I18N = Symbol('GENUI_I18N'); +export const GENUI_CONFIG = Symbol('GENUI_CONFIG'); + +export const GENUI_MATERIALS: InjectionKey = Symbol('GENUI_MATERIALS'); diff --git a/packages/frameworks/vue/src/index.ts b/packages/frameworks/vue/src/index.ts index 6f4c5fbd..d7cb78e6 100644 --- a/packages/frameworks/vue/src/index.ts +++ b/packages/frameworks/vue/src/index.ts @@ -1,4 +1,6 @@ export * from './chat'; export * from './renderer'; export * from './config-provider'; +export * from './legacy-chat'; +export * from './legacy-renderer'; export { RENDERER_SETTINGS_KEY } from '@opentiny/tiny-schema-renderer'; diff --git a/packages/frameworks/vue/src/legacy-chat/GenuiChatWithMaterials.vue b/packages/frameworks/vue/src/legacy-chat/GenuiChatWithMaterials.vue new file mode 100644 index 00000000..e904fe8d --- /dev/null +++ b/packages/frameworks/vue/src/legacy-chat/GenuiChatWithMaterials.vue @@ -0,0 +1,31 @@ + + + diff --git a/packages/frameworks/vue/src/legacy-chat/index.ts b/packages/frameworks/vue/src/legacy-chat/index.ts new file mode 100644 index 00000000..8d3a8299 --- /dev/null +++ b/packages/frameworks/vue/src/legacy-chat/index.ts @@ -0,0 +1,8 @@ +export { default as GenuiLegacyChat } from './GenuiChatWithMaterials.vue'; +export * from '../chat/chat.types.js'; +export * from '../config-provider/injection-tokens.js'; +export * from '../chat/i18n/index.js'; +export * from '../chat/tiny-robot-patch/index.js'; +export * from '../chat/event-emitter.js'; +export * from '../chat/chat-utils.js'; +export * from '../chat/think-tag-wrap-pattern.js'; diff --git a/packages/frameworks/vue/src/legacy-renderer/GenuiRendererWithMaterials.vue b/packages/frameworks/vue/src/legacy-renderer/GenuiRendererWithMaterials.vue new file mode 100644 index 00000000..d4ccd179 --- /dev/null +++ b/packages/frameworks/vue/src/legacy-renderer/GenuiRendererWithMaterials.vue @@ -0,0 +1,22 @@ + + + diff --git a/packages/frameworks/vue/src/legacy-renderer/index.ts b/packages/frameworks/vue/src/legacy-renderer/index.ts new file mode 100644 index 00000000..008246d5 --- /dev/null +++ b/packages/frameworks/vue/src/legacy-renderer/index.ts @@ -0,0 +1,3 @@ +export { default as GenuiLegacyRenderer } from './GenuiRendererWithMaterials.vue'; +export * from '../renderer/config.js'; +export * from '../renderer/renderer.types.js'; diff --git a/packages/frameworks/vue/src/renderer/SchemaCardRenderer.vue b/packages/frameworks/vue/src/renderer/GenuiRenderer.vue similarity index 59% rename from packages/frameworks/vue/src/renderer/SchemaCardRenderer.vue rename to packages/frameworks/vue/src/renderer/GenuiRenderer.vue index 6b48332e..45f031b8 100644 --- a/packages/frameworks/vue/src/renderer/SchemaCardRenderer.vue +++ b/packages/frameworks/vue/src/renderer/GenuiRenderer.vue @@ -1,11 +1,10 @@