From ab26e914aa6c62860df7609e7825ea9147300dc9 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Fri, 2 Oct 2026 12:33:51 -0700 Subject: [PATCH] feat: add interactive app and agent architecture explorer --- AGENTS.md | 4 + README.md | 3 +- README.zh-CN.md | 3 +- astro.config.mjs | 2 + package-lock.json | 361 +++++++++++++++++- package.json | 8 +- src/components/AgentTools.astro | 2 +- src/components/Guide.astro | 16 +- .../architecture/ArchitectureFlow.tsx | 138 +++++++ src/components/architecture/architecture.css | 55 +++ src/components/architecture/model.ts | 94 +++++ src/data/content.ts | 4 +- src/data/docs.ts | 2 +- tests/architecture.spec.ts | 94 +++++ tsconfig.json | 4 +- 15 files changed, 776 insertions(+), 14 deletions(-) create mode 100644 src/components/architecture/ArchitectureFlow.tsx create mode 100644 src/components/architecture/architecture.css create mode 100644 src/components/architecture/model.ts create mode 100644 tests/architecture.spec.ts diff --git a/AGENTS.md b/AGENTS.md index 1fd242f..f6d855c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,6 +15,10 @@ Read [README.md](README.md) and the bilingual app guides under - Preserve source provenance: corrections to checked-in upstream snapshots require updated hashes and an explicit correction record in `sources.json`. The consuming app's runtime and Cargo locks select its actual implementation. +- The React Flow explorer lives in `src/components/architecture/`; keep node + ownership, edge meanings and both locales aligned with the prose. Preserve + keyboard selection, readable mobile focus, page scrolling, themes and the + static text version. Run `tests/architecture.spec.ts` for graph changes. - Keep browser sample state and historical design examples labelled. Preserve demo behavior and the prebuilt WASM package unless the task changes them. - For content/navigation changes run `npm run test:unit`, `npm run build`, diff --git a/README.md b/README.md index f405b8f..2beaa7c 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ The independent bilingual website for [Octoscript](https://github.com/OctoSense- The site covers the DSL's motivation, syntax, host authority, L0–L3 rendering layers, UI capability levels, Splash, native composition and agent tooling. Nine guides in each language explain the contracts on the page. A searchable component catalog and a real Makepad/WebAssembly lab accompany the design-to-component examples. Light and dark themes are supported. -The [app and agent guide](src/content/guides/octosense-apps.en.md) follows native Rust, Splash and L0 apps into OctoSense, including human/system conversations, app data, tool grants and Tokio tasks. Contributor instructions are in [AGENTS.md](AGENTS.md). +The [app and agent guide](src/content/guides/octosense-apps.en.md) follows native Rust, Splash and L0 apps into OctoSense, including human/system conversations, app data, tool grants and Tokio tasks. Its interactive React Flow map separates app rendering from agent requests, with node details, keyboard controls and an HTML text version. Contributor instructions are in [AGENTS.md](AGENTS.md). ## Run locally @@ -40,6 +40,7 @@ The build checks Astro/TypeScript and writes the static site to `dist/`. Playwri - `src/data/docs.ts`: combines the guides and snapshots into the documentation routes. - `scripts/doc-links.mjs`: maps relative Markdown links to local guides; optional upstream source references still point to the language repository. - `src/components/`: homepage sections, component catalog, WASM panel and recorded design-flow inspector. +- `src/components/architecture/`: React Flow explorer, bilingual graph data and scoped styles. `Guide.astro` places it after the original guide introduction; `tests/architecture.spec.ts` checks interaction, accessibility and fallback behavior. - `src/styles/`: typography, responsive layout and light/dark theme tokens. - `public/examples/provenance.json`: image source hashes and the archived design review's status. diff --git a/README.zh-CN.md b/README.zh-CN.md index 351b78c..97b2db5 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -8,7 +8,7 @@ 网站介绍这门 DSL 的设计动机、语法、宿主权限、L0–L3 渲染分层、UI 能力等级、Splash、原生组合以及 Agent 工具链。每种语言各有九篇指南,逐一讲解页面上的各项约定。设计到组件的示例配有可搜索的组件目录和一个真实的 Makepad/WebAssembly 实验室。支持浅色和深色主题。 -[应用与 Agent 指南](src/content/guides/octosense-apps.cn.md)追踪原生 Rust、Splash 和 L0 应用如何进入 OctoSense,并说明用户/系统对话、应用数据、工具授权与 Tokio 任务。协作规则见 [AGENTS.md](AGENTS.md)。 +[应用与 Agent 指南](src/content/guides/octosense-apps.cn.md)追踪原生 Rust、Splash 和 L0 应用如何进入 OctoSense,并说明用户/系统对话、应用数据、工具授权与 Tokio 任务。交互式 React Flow 图把应用渲染与 Agent 请求分开,提供节点详情、键盘控制与 HTML 文字版。协作规则见 [AGENTS.md](AGENTS.md)。 ## 本地运行 @@ -40,6 +40,7 @@ npm run test:e2e - `src/data/docs.ts`:把指南和快照组合成文档路由。 - `scripts/doc-links.mjs`:把 Markdown 相对链接映射到本地指南;可选的上游源码引用仍指向语言仓库。 - `src/components/`:首页各板块、组件目录、WASM 面板以及录制的设计流程查看器。 +- `src/components/architecture/`:React Flow 查看器、双语图数据与局部样式。`Guide.astro` 把它放在原指南引言之后;`tests/architecture.spec.ts` 检查交互、可访问性与无 JavaScript 的文字阅读。 - `src/styles/`:字体排印、响应式布局以及浅色/深色主题 token。 - `public/examples/provenance.json`:图片来源哈希以及已归档设计评审的状态。 diff --git a/astro.config.mjs b/astro.config.mjs index c3832b5..66a63a8 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -1,4 +1,5 @@ import { defineConfig } from 'astro/config'; +import react from '@astrojs/react'; import { unified } from '@astrojs/markdown-remark'; import docLinks from './scripts/doc-links.mjs'; @@ -6,6 +7,7 @@ export default defineConfig({ site: process.env.SITE_URL || 'http://localhost:4325', base: process.env.BASE_PATH || '/', output: 'static', + integrations: [react()], trailingSlash: 'always', devToolbar: { enabled: false }, markdown: { diff --git a/package-lock.json b/package-lock.json index 30fd0c7..fd4591c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,19 +9,25 @@ "version": "1.0.0", "dependencies": { "@astrojs/markdown-remark": "7.3.1", + "@astrojs/react": "^7.0.0", "@fontsource-variable/cormorant-garamond": "^5.3.0", "@fontsource-variable/manrope": "^5.3.0", "@fontsource-variable/noto-sans-sc": "^5.3.0", "@fontsource-variable/noto-serif-sc": "^5.3.0", "@fontsource-variable/space-grotesk": "^5.3.0", "@fontsource/ibm-plex-mono": "^5.3.0", - "astro": "^7.3.2" + "@xyflow/react": "^12.12.0", + "astro": "^7.3.2", + "react": "^19.3.0", + "react-dom": "^19.3.0" }, "devDependencies": { "@astrojs/check": "^0.9.10", "@axe-core/playwright": "^4.13.0", "@playwright/test": "^1.63.0", "@types/node": "^22.20.2", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", "typescript": "^6.0.3" }, "engines": { @@ -372,6 +378,34 @@ "node": ">=22.12.0" } }, + "node_modules/@astrojs/react": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@astrojs/react/-/react-7.0.0.tgz", + "integrity": "sha512-BUs2jVvuUnPBA/vtp9y1PvvoYeZZtjmwphu0TJX0wSQ6xzR31/OyflQ9Gmm4rn9IRZNFObCJG7wWsZSqr4OMUA==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.11.0", + "@vitejs/plugin-react": "^6.1.1", + "devalue": "^5.9.4", + "ultrahtml": "^1.7.0", + "vite": "^8.3.0" + }, + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "@types/react": "^17.0.50 || ^18.0.21 || ^19.0.0", + "@types/react-dom": "^17.0.17 || ^18.0.6 || ^19.0.0", + "oxc-transform-react": "^0.145.0", + "react": "^17.0.2 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.2 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "oxc-transform-react": { + "optional": true + } + } + }, "node_modules/@astrojs/telemetry": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.3.3.tgz", @@ -2166,6 +2200,55 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-selection": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.12.tgz", + "integrity": "sha512-Qe/KWYhEiIIxGs7HrAAjMfShxKldx19SJtr5zu53f3afPsdZNz7HHtdTLXo/kqeiWNXVycI24kSnfzBYkTzpgw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.9.tgz", + "integrity": "sha512-0sE1406XBYJGiqD3AusTl9ZqC//2mIXix51tbom25gDCA8ri4xnSZg28CaSE8Srl6FClqABUKfsn0qghgdepMA==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.13", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", @@ -2239,6 +2322,24 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -2251,6 +2352,35 @@ "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==", "license": "ISC" }, + "node_modules/@vitejs/plugin-react": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.1.tgz", + "integrity": "sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw==", + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "oxc-transform-react": "^0.145.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "oxc-transform-react": { + "optional": true + } + } + }, "node_modules/@volar/kit": { "version": "2.4.28", "resolved": "https://registry.npmjs.org/@volar/kit/-/kit-2.4.28.tgz", @@ -2356,6 +2486,48 @@ "dev": true, "license": "MIT" }, + "node_modules/@xyflow/react": { + "version": "12.12.0", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.12.0.tgz", + "integrity": "sha512-74oXI0Rgm1Eu33cHQAd74J6oVgCH9mbFyTVQbsGgp2kmWwh0HdtBr2VP04z8mbPD+M/xO69nqG5dDEt7/EN2rQ==", + "license": "MIT", + "dependencies": { + "@xyflow/system": "0.0.83", + "classcat": "^5.0.3", + "zustand": "^4.4.0" + }, + "peerDependencies": { + "@types/react": ">=17", + "@types/react-dom": ">=17", + "react": ">=17", + "react-dom": ">=17" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@xyflow/system": { + "version": "0.0.83", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.83.tgz", + "integrity": "sha512-a//eDjez3WZF47aaPTfVUDD0OINOq+WVwBZCRTqG+m0a5euX8EwlJ4jMY1Wxz0Ocj4xOeekJ70V22Pq0ll8RJw==", + "license": "MIT", + "dependencies": { + "@types/d3-drag": "^3.0.7", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-selection": "^3.0.10", + "@types/d3-transition": "^3.0.8", + "@types/d3-zoom": "^3.0.8", + "d3-drag": "^3.0.0", + "d3-interpolate": "^3.0.1", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + } + }, "node_modules/acorn": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", @@ -2715,6 +2887,12 @@ "node": ">=8" } }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "license": "MIT" + }, "node_modules/cliui": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", @@ -2897,6 +3075,117 @@ "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", "license": "CC0-1.0" }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2958,9 +3247,9 @@ } }, "node_modules/devalue": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.9.2.tgz", - "integrity": "sha512-po4PAY5c53tw5XMocSnf8A/5OHhbbUftpr93aEN6BBoAdntUmK7vu7wOATqvt7cXO7m1Cl4gMVn6p7n6n4mj0w==", + "version": "5.9.4", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.9.4.tgz", + "integrity": "sha512-sPAT4pztbu6586/hrhOnMKS17IJrvg12mXiSPSS3W5qDeN2RGgvZ0diZCm31dBbnevfVmujNO3IM2wrS4Y2Rhg==", "license": "MIT" }, "node_modules/devlop": { @@ -5584,6 +5873,27 @@ "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", "license": "MIT" }, + "node_modules/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", + "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.0" + } + }, "node_modules/readdirp": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", @@ -5980,6 +6290,12 @@ "node": ">=11.0.0" } }, + "node_modules/scheduler": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "license": "MIT" + }, "node_modules/semver": { "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", @@ -6591,6 +6907,15 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.7.0.tgz", + "integrity": "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", @@ -7185,6 +7510,34 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/package.json b/package.json index 63b85dc..de68df9 100644 --- a/package.json +++ b/package.json @@ -16,19 +16,25 @@ }, "dependencies": { "@astrojs/markdown-remark": "7.3.1", + "@astrojs/react": "^7.0.0", "@fontsource-variable/cormorant-garamond": "^5.3.0", "@fontsource-variable/manrope": "^5.3.0", "@fontsource-variable/noto-sans-sc": "^5.3.0", "@fontsource-variable/noto-serif-sc": "^5.3.0", "@fontsource-variable/space-grotesk": "^5.3.0", "@fontsource/ibm-plex-mono": "^5.3.0", - "astro": "^7.3.2" + "@xyflow/react": "^12.12.0", + "astro": "^7.3.2", + "react": "^19.3.0", + "react-dom": "^19.3.0" }, "devDependencies": { "@astrojs/check": "^0.9.10", "@axe-core/playwright": "^4.13.0", "@playwright/test": "^1.63.0", "@types/node": "^22.20.2", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", "typescript": "^6.0.3" }, "repository": { diff --git a/src/components/AgentTools.astro b/src/components/AgentTools.astro index c65682b..e4e81d9 100644 --- a/src/components/AgentTools.astro +++ b/src/components/AgentTools.astro @@ -21,6 +21,6 @@ const rows = cn ? [
app --remote127.0.0.1:PORT / HTTP
    {rows.map(([endpoint,title,body],i) =>
  1. 0{i+1}

    {title}

    {body}

    {endpoint}
  2. )}

{cn ? '开发实例显式启用仪器。这里的 headless 指不依赖 Studio GUI,视觉验证仍使用真实 GPU;各平台的检查与采集能力以宿主实现为准。' : 'Instrumentation is enabled explicitly for development. Headless means no Studio GUI is needed; visual checks still use the real GPU, with inspection and capture support determined by each host.'}

{cn ? '这里的 Agent 帮助开发和测试应用。运行时应用 Agent 则由 OctoSense 和 octos 管理,使用用户允许的账户工作区、工具与对话路由。独立 card-host 没有注册 Agent 服务。' : 'These instruments serve a coding agent. Runtime app agents are managed by OctoSense and octos, with permitted account workspaces, tools and conversation routes. Standalone card-host registers no agent services.'}

- {cn ? '运行应用并追踪它的 Agent' : 'Run an app and follow its agent'} + {cn ? '探索交互式应用与 Agent 架构图' : 'Explore the interactive app and agent architecture'} {cn ? '阅读语言、仪器与 A2App 的完整关系' : 'Read how language, instruments and A2App fit together'} diff --git a/src/components/Guide.astro b/src/components/Guide.astro index 91b9f8e..f65a2d3 100644 --- a/src/components/Guide.astro +++ b/src/components/Guide.astro @@ -2,6 +2,7 @@ import Layout from '../layouts/Layout.astro'; import Icon from './Icon.astro'; import ComponentCatalog from './ComponentCatalog.astro'; +import ArchitectureFlow from './architecture/ArchitectureFlow'; import ComponentLab from './ComponentLab.astro'; import { guidesFor } from '../data/docs'; import { content, route, repository, type Locale } from '../data/content'; @@ -11,16 +12,27 @@ const guides = guidesFor(locale); const guide = guides.find(item => item.slug === slug)!; const c = content[locale]; const Content = guide.Content; +// Keep the original Markdown h1, introduction and heading IDs. The graph gets +// a full-width row between the introduction and the first prose section. +const architectureHtml = slug === 'octosense-apps' ? await guide.compiledContent() : ''; +const firstSection = architectureHtml.indexOf('
+ {architectureHtml && <> +
{c.guideLabel} / {guide.tag}
+
+ } -
{c.guideLabel} / {guide.tag}
{slug === 'component-library' && <>}←{c.allDocs}
- +
{!architectureHtml && {c.guideLabel} / {guide.tag}}
{architectureHtml ? : }
{slug === 'component-library' && <>}←{c.allDocs}
+
diff --git a/src/components/architecture/ArchitectureFlow.tsx b/src/components/architecture/ArchitectureFlow.tsx new file mode 100644 index 0000000..d55e775 --- /dev/null +++ b/src/components/architecture/ArchitectureFlow.tsx @@ -0,0 +1,138 @@ +import { useEffect, useMemo, useRef, useState } from 'react'; +import { Background, Handle, MarkerType, Position, ReactFlow, type Node, type NodeProps, type ReactFlowInstance } from '@xyflow/react'; +import '@xyflow/react/dist/style.css'; +import './architecture.css'; +import { text, views, type Locale, type Mode, type Step } from './model'; + +type Data = { step: Step; locale: Locale }; +type ArchitectureNode = Node; +const sides = { top: Position.Top, right: Position.Right, bottom: Position.Bottom, left: Position.Left }; +function ArchitectureNodeView({ data }: NodeProps) { + return <> + {Object.entries(sides).map(([side, position]) => + + + )} + + {text(data.step.owner, data.locale)} + {text(data.step.title, data.locale)} + {text(data.step.summary, data.locale)} + ; +} +const nodeTypes = { architecture: ArchitectureNodeView }; + +export default function ArchitectureFlow({ locale }: { locale: Locale }) { + const cn = locale === 'cn'; + const [mode, setMode] = useState('render'); + const [selected, setSelected] = useState(views.render.start); + const [api, setApi] = useState | null>(null); + const [mobile, setMobile] = useState(false); + const [panning, setPanning] = useState(false); + const [zoom, setZoom] = useState(1); + const [theme, setTheme] = useState<'light' | 'dark'>('light'); + const canvas = useRef(null); + const view = views[mode]; + const step = view.steps.find(item => item.id === selected) ?? view.steps[0]; + const nodes: ArchitectureNode[] = useMemo(() => view.steps.map(item => ({ + id: item.id, type: 'architecture', position: item.position, + data: { step: item, locale }, width: 250, height: 104, + selected: item.id === selected, + ariaLabel: `${text(item.title, locale)} — ${text(item.owner, locale)}`, + })), [view, locale, selected]); + const edges = useMemo(() => view.links.map((link, i) => ({ + id: `${mode}-${i}`, source: link.from, target: link.to, type: 'smoothstep', + sourceHandle: `out-${link.fromSide ?? 'bottom'}`, targetHandle: `in-${link.toSide ?? 'top'}`, + label: text(link.label, locale), + markerEnd: { type: MarkerType.ArrowClosed, color: 'var(--accent)' }, + markerStart: link.both ? { type: MarkerType.ArrowClosed, color: 'var(--accent)' } : undefined, + className: link.ownership ? 'architecture-ownership-edge' : '', + style: { stroke: 'var(--accent)', strokeWidth: 1.6 }, + labelStyle: { fill: 'var(--text)', fontSize: 13 }, + labelBgStyle: { fill: 'var(--surface)' }, labelBgPadding: [6, 4] as [number, number], + })), [view, mode, locale]); + + useEffect(() => { + const syncTheme = () => setTheme(document.documentElement.dataset.theme === 'dark' ? 'dark' : 'light'); + syncTheme(); + const observer = new MutationObserver(syncTheme); + observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] }); + return () => observer.disconnect(); + }, []); + useEffect(() => { + if (!canvas.current) return; + const observer = new ResizeObserver(([entry]) => setMobile(entry.contentRect.width < 600)); + observer.observe(canvas.current); + return () => observer.disconnect(); + }, []); + useEffect(() => { + if (!api) return; + const frame = requestAnimationFrame(() => { + const first = views[mode].steps.find(item => item.id === views[mode].start)!; + if (mobile) void api.setCenter(first.position.x + 125, first.position.y + 52, { zoom: 1 }); + else void api.fitView({ padding: 0.12, maxZoom: 1 }); + }); + return () => cancelAnimationFrame(frame); + }, [api, mode, mobile]); + + function choose(id: string) { + setSelected(id); + const next = view.steps.find(item => item.id === id)!; + void api?.setCenter(next.position.x + 125, next.position.y + 52, { zoom: 1 }); + } + function pan(x: number, y: number) { + if (!api) return; + const current = api.getViewport(); + void api.setViewport({ ...current, x: current.x + x, y: current.y + y }); + } + const instruction = cn + ? '选择节点查看职责。开启“移动图”后拖动;滚轮仍滚动页面。聚焦画布后可用方向键平移。“全图”显示整体关系,“定位节点”恢复清晰的节点视图。' + : 'Select a node to read its role. Enable Move graph to drag; the wheel still scrolls the page. Focus the canvas and use arrow keys to pan. Fit view shows the whole map; Focus a node restores readable detail.'; + + return
+
{cn ? '交互式架构图' : 'INTERACTIVE ARCHITECTURE'}

{cn ? '沿着一条路径,理解各层职责。' : 'Follow a path. See who owns each step.'}

{cn ? '源码中的逻辑关系示意,不连接运行中的应用或账户。' : 'A map of the implementation, not a connection to a live app or account.'}

+
+ {(['render', 'agents'] as const).map(value => )} +
+

{text(view.intro, locale)}

+
+
+
+ + + + + {Math.round(zoom * 100)}% +
+
+

{instruction}

+
{ + if (event.target !== event.currentTarget) return; + const directions: Record = { ArrowLeft: [70, 0], ArrowRight: [-70, 0], ArrowUp: [0, 70], ArrowDown: [0, -70] }; + if (directions[event.key]) { event.preventDefault(); pan(...directions[event.key]); } + }}> + nodes={nodes} edges={edges} nodeTypes={nodeTypes} onInit={setApi} + onNodesChange={changes => { const choice = changes.find(change => change.type === 'select' && change.selected); if (choice?.type === 'select') setSelected(choice.id); }} + onNodeClick={(_, node) => setSelected(node.id)} onMove={(_, viewport) => setZoom(viewport.zoom)} + nodesDraggable={false} nodesConnectable={false} edgesFocusable={false} edgesReconnectable={false} + deleteKeyCode={null} selectionKeyCode={null} multiSelectionKeyCode={null} + minZoom={0.2} maxZoom={1.6} panOnDrag={panning} panOnScroll={false} + zoomOnScroll={false} zoomOnPinch={panning} zoomOnDoubleClick={false} preventScrolling={false} + colorMode={theme} attributionPosition="bottom-right" + ariaLabelConfig={{ + 'node.a11yDescription.default': cn ? '按 Enter 或空格查看节点详情。节点位置固定。' : 'Press Enter or Space to read node details. Node positions are fixed.', + 'node.a11yDescription.keyboardDisabled': cn ? '按 Enter 或空格查看节点详情。' : 'Press Enter or Space to read node details.', + }}> + + +
+
{cn ? '实线:执行或请求路径' : 'Solid: execution or request path'}{cn ? '双箭头:请求与应答' : 'Two arrows: request and reply'}{mode === 'agents' && {cn ? '虚线:运行时归属' : 'Dashed: runtime ownership'}}
+
+
{text(step.owner, locale)}

{text(step.title, locale)}

+

{text(step.detail, locale)}

{cn ? '阅读对应源码导读 ↗' : 'Read the source walkthrough ↗'} +
+
{cn ? '文字版:两条路径与各节点职责' : 'Text version: both paths and their responsibilities'} + {(['render', 'agents'] as const).map(value =>

{text(views[value].title, locale)}

{text(views[value].intro, locale)}

    {views[value].steps.map(item =>
  1. {text(item.title, locale)} — {text(item.detail, locale)}
  2. )}
    {views[value].links.map((link, i) =>
  • {text(views[value].steps.find(item => item.id === link.from)!.title, locale)} {link.both ? '↔' : '→'} {text(views[value].steps.find(item => item.id === link.to)!.title, locale)}: {text(link.label, locale)}
  • )}
)} +
+
; +} diff --git a/src/components/architecture/architecture.css b/src/components/architecture/architecture.css new file mode 100644 index 0000000..e75b015 --- /dev/null +++ b/src/components/architecture/architecture.css @@ -0,0 +1,55 @@ +.architecture-lead, .architecture-row { grid-column: 1 / -1; min-width: 0; } +.architecture-lead { max-width: 950px; } +.architecture-row { margin-bottom: 24px; } +.architecture-explorer { border: 1px solid var(--line); background: var(--surface); border-radius: 8px; overflow: hidden; } +.architecture-heading { display: flex; align-items: start; justify-content: space-between; gap: 32px; padding: 28px 28px 16px; } +.architecture-heading h2 { font: 500 clamp(22px, 2.5vw, 30px)/1.25 var(--heading); letter-spacing: -.025em; margin: 10px 0; white-space: normal; } +.architecture-heading p { font-size: 13px; line-height: 1.7; color: var(--muted); max-width: 270px; margin: 5px 0; } +.architecture-modes { display: flex; flex-wrap: wrap; gap: 8px; padding: 0 28px; } +.architecture-explorer button, .architecture-explorer select { font: 600 13px/1.4 var(--body); color: var(--text); background: var(--surface); border: 1px solid var(--line); border-radius: 4px; min-height: 44px; padding: 9px 14px; } +.architecture-explorer button { cursor: pointer; } +.architecture-explorer button:hover { background: var(--raised); border-color: var(--accent); } +.architecture-explorer button[aria-pressed='true'] { background: var(--accent); color: var(--on-accent); border-color: var(--accent); } +.architecture-explorer :focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; } +.architecture-intro { font-size: 14px; line-height: 1.8; padding: 0 28px; color: var(--muted); } +.architecture-toolbar { display: flex; align-items: end; flex-wrap: wrap; gap: 14px; padding: 12px 28px 0; border-top: 1px solid var(--line); } +.architecture-select { display: grid; gap: 7px; } +.architecture-toolbar label { font-size: 12px; color: var(--muted); } +.architecture-toolbar select { min-width: 230px; max-width: 100%; } +.architecture-tools { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; } +.architecture-tools button:first-child, .architecture-tools button:nth-child(2) { font-size: 22px; padding: 4px; width: 44px; } +.architecture-zoom { color: var(--muted); font: 12px var(--mono); padding: 8px; } +.architecture-help { color: var(--muted); font-size: 12px; line-height: 1.7; margin: 12px 28px 16px; } +.architecture-canvas { height: 620px; width: 100%; background: var(--bg); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); } +.architecture-canvas:not(.can-pan) .react-flow__pane { touch-action: pan-y; cursor: default; } +.architecture-canvas .react-flow { --xy-background-color: var(--bg); --xy-edge-stroke: var(--accent); --xy-node-background-color: var(--surface); --xy-node-color: var(--text); font-family: var(--body); } +.architecture-canvas .react-flow__node-architecture { width: 250px; height: 104px; display: flex; flex-direction: column; justify-content: center; gap: 7px; padding: 13px 18px; color: var(--text); background: var(--surface); border: 1px solid var(--frame); border-radius: 7px; box-shadow: 0 3px 10px var(--shadow); cursor: pointer; } +.architecture-canvas .react-flow__node-architecture.selected { border: 2px solid var(--accent); background: var(--accent-soft); padding: 12px 17px; } +.architecture-canvas .react-flow__node-architecture strong { font-size: 16px; line-height: 1.25; font-weight: 600; } +.architecture-node-owner { color: var(--accent); font: 10px/1.2 var(--mono); text-transform: uppercase; letter-spacing: .04em; } +.architecture-node-summary { color: var(--muted); font-size: 12px; line-height: 1.35; } +.architecture-canvas .react-flow__handle { width: 5px; height: 5px; min-width: 5px; min-height: 5px; border-color: var(--surface); background: var(--accent); } +.architecture-canvas .architecture-ownership-edge path { stroke-dasharray: 5 5; } +.architecture-canvas .react-flow__attribution { font-size: 10px; background: var(--surface); padding: 3px 7px; } +.architecture-canvas .react-flow__attribution a { color: var(--muted); } +.architecture-legend { display: flex; flex-wrap: wrap; gap: 10px 24px; padding: 12px 28px; color: var(--muted); font-size: 11px; border-bottom: 1px solid var(--line); } +.architecture-detail { display: grid; grid-template-columns: 220px 1fr; gap: 8px 28px; padding: 24px 28px; min-height: 178px; } +.architecture-detail h3 { margin: 8px 0 0; font: 500 21px/1.3 var(--heading); } +.architecture-detail p { margin: 0; font-size: 14px; line-height: 1.9; color: var(--muted); } +.architecture-detail a { grid-column: 2; color: var(--accent); text-decoration: underline; font-size: 12px; } +.architecture-text { border-top: 1px solid var(--line); padding: 18px 28px; font-size: 14px; line-height: 1.8; } +.architecture-text summary { cursor: pointer; color: var(--accent); font-weight: 600; } +.architecture-text li { margin: 12px 0; } +@media (max-width: 700px) { + .architecture-heading { display: block; padding: 20px 16px 10px; } + .architecture-heading p { max-width: none; } + .architecture-modes, .architecture-toolbar { padding-left: 16px; padding-right: 16px; } + .architecture-intro { padding: 0 16px; } + .architecture-help { margin-left: 16px; margin-right: 16px; } + .architecture-select { width: 100%; } + .architecture-toolbar select { width: 100%; min-width: 0; } + .architecture-canvas { height: 440px; } + .architecture-detail { grid-template-columns: 1fr; padding: 20px 16px; min-height: 0; } + .architecture-detail a { grid-column: 1; } + .architecture-legend, .architecture-text { padding-left: 16px; padding-right: 16px; } +} diff --git a/src/components/architecture/model.ts b/src/components/architecture/model.ts new file mode 100644 index 0000000..e69c1d5 --- /dev/null +++ b/src/components/architecture/model.ts @@ -0,0 +1,94 @@ +export type Locale = 'en' | 'cn'; +export type Mode = 'render' | 'agents'; +type Words = readonly [string, string]; +export const text = (words: Words, locale: Locale) => words[locale === 'cn' ? 1 : 0]; +export type Step = { + id: string; + title: Words; + owner: Words; + summary: Words; + detail: Words; + position: { x: number; y: number }; + source: string; +}; +export type Link = { + from: string; + to: string; + label: Words; + fromSide?: 'bottom' | 'left' | 'right' | 'top'; + toSide?: 'bottom' | 'left' | 'right' | 'top' | 'top-right'; + both?: boolean; + ownership?: boolean; +}; +const root = 'https://github.com/OctoSense-org/'; +const sources = { + shell: `${root}OctoSense/blob/61c668279a7c38a0f8056134d8d29e42ed715806/docs/architecture-walkthrough.md`, + hub: `${root}OctoSense-App-Hub/blob/19bb52d402e80e89e085dea989615e3ec612d359/docs/CODE-WALKTHROUGH.md`, + renderer: `${root}OctoScript-Makepad/blob/fb29b6b1cb6e16f38d99a8aef60431a9565dfd59/docs/architecture-walkthrough.md`, + kernel: 'https://github.com/octos-org/octos/blob/82900bf149d3a53016c1c1492ffc075d2d4fb0ed/docs/octosense-integration-walkthrough.md', +}; +export const views: Record = { + render: { + title: ['App → native UI', '应用 → 原生 UI'], + intro: ['Choose the source you have. Follow its host and rendering path to native widgets.', '先选择源码类型,再沿着宿主与渲染路径走到原生组件。'], + start: 'splash', + steps: [ + { id: 'rust', title: ['Rust app module', 'Rust 应用模块'], owner: ['App author', '应用作者'], summary: ['Compiled AppModule', '编译后的 AppModule'], position: { x: 0, y: 0 }, source: sources.shell, + detail: ['A native Rust AppModule supplies compiled widgets, state and service code. It can also embed Splash. This path describes a module inside its host; a separate native executable has its own process and integration contract.', '原生 Rust AppModule 提供编译后的组件、状态与服务代码,也可嵌入 Splash。此路径描述宿主内的模块;独立原生程序另有进程与集成约定。'] }, + { id: 'splash', title: ['Splash app source', 'Splash 应用源码'], owner: ['App bundle', '应用包'], summary: ['main.splash', 'main.splash'], position: { x: 350, y: 0 }, source: sources.hub, + detail: ['A contained script app ships main.splash. Makepad Script evaluates it with the capabilities installed by the host. This is a UI entry point, separate from the standalone OctoScript workflow CLI.', '隔离脚本应用提供 main.splash,由 Makepad Script 使用宿主安装的能力求值。这是 UI 入口,与独立 OctoScript 工作流 CLI 分开。'] }, + { id: 'l0', title: ['L0 card + data + kit', 'L0 卡片 + 数据 + kit'], owner: ['App bundle', '应用包'], summary: ['page.card', 'page.card'], position: { x: 700, y: 0 }, source: sources.renderer, + detail: ['An L0 card declares data, state and components. Realization checks and expands these declarations without a VM. The card still needs its declared data and a compatible component kit; it does not start an agent or gain tool access by itself.', 'L0 卡片声明数据、状态与组件,实例化在不使用 VM 的情况下检查并展开这些声明。它仍需声明的数据与兼容的组件 kit;卡片不会自行启动 Agent 或获得工具访问权。'] }, + { id: 'native', title: ['Native module host', '原生模块宿主'], owner: ['Rust + Makepad', 'Rust + Makepad'], summary: ['Mount, events and state', '挂载、事件与状态'], position: { x: 0, y: 195 }, source: sources.hub, + detail: ['The compiled host registers the AppModule, mounts its UI and delivers events to its Rust handlers. It owns state and widget lifetime. A standalone host and the full shell have different service wiring; the shell adds the request broker and agent integration.', '编译后的宿主注册 AppModule、挂载 UI,并向 Rust 处理器交付事件。宿主持有状态并管理组件生命周期。独立宿主与完整 Shell 的服务接入不同;Shell 还提供请求 broker 与 Agent 集成。'] }, + { id: 'gate', title: ['Bundle admission', '应用包准入'], owner: ['App Hub', 'App Hub'], summary: ['Manifest → granted policy', 'Manifest → 授予的策略'], position: { x: 350, y: 180 }, source: sources.hub, + detail: ['The host checks the bundle and resolves its manifest against HostLimits before evaluation. Storage jail, network allowlist and resource budgets constrain the app. An admitted declaration still needs an executable service.', '宿主在求值前检查应用包,并用 HostLimits 解析 manifest 请求。存储隔离目录、网络允许列表与资源预算限制应用。声明获准后,仍需实际可执行的服务。'] }, + { id: 'lower', title: ['Realize, lower, translate', '实例化、转换、翻译'], owner: ['OctoScript + renderer', 'OctoScript + 渲染器'], summary: ['Card tree → UiNode → source', '卡片树 → UiNode → 源码'], position: { x: 700, y: 330 }, source: sources.renderer, + detail: ['OctoScript checks and realizes the semantic card tree. Lowering converts this tree into kit source; checked evaluation produces the renderer UiNode; Rust emits Makepad widget source. Native kits use the measured-design path. These are distinct tree representations and host stages.', 'OctoScript 检查并实例化语义卡片树。Lowering 把这棵树转换为 kit 源码;受检查的求值得到渲染器 UiNode,再由 Rust 生成 Makepad 组件源码。原生 kit 走保留设计几何的路径;这些是不同的树表示和宿主阶段。'] }, + { id: 'contained', title: ['Contained UI host', '隔离 UI 宿主'], owner: ['Card runner + Splash', 'Card runner + Splash'], summary: ['Evaluate and mount', '求值并挂载'], position: { x: 350, y: 390 }, source: sources.hub, + detail: ['The runner mounts script or lowered card source inside Splash and routes UI events. Standalone card-host registers no host services or agent runtime. In OctoSense, the shell supplies registered services and the app-agent bridge.', 'Runner 在 Splash 中挂载脚本或转换后的卡片源码,并路由 UI 事件。独立 card-host 没有注册宿主服务或 Agent 运行时;OctoSense Shell 提供已注册服务与应用 Agent 桥接。'] }, + { id: 'widgets', title: ['Native Makepad UI', '原生 Makepad UI'], owner: ['UI host', 'UI 宿主'], summary: ['Draw → input → update', '绘制 → 输入 → 更新'], position: { x: 0, y: 510 }, source: sources.renderer, + detail: ['Makepad draws widgets and delivers input through the host event loop. The host updates state and decides when to render again. A visible widget is not proof that a database, device API or agent operation is connected.', 'Makepad 绘制组件,并通过宿主事件循环交付输入。宿主更新状态并决定何时重绘。看见组件不等于已接通数据库、设备 API 或 Agent 操作。'] }, + ], + links: [ + { from: 'rust', to: 'native', label: ['compile / register', '编译/注册'] }, + { from: 'splash', to: 'gate', label: ['bundle', '应用包'] }, + { from: 'l0', to: 'gate', label: ['bundle', '应用包'], fromSide: 'bottom', toSide: 'top-right' }, + { from: 'gate', to: 'lower', label: ['card entry', '卡片入口'], fromSide: 'right' }, + { from: 'gate', to: 'contained', label: ['script entry', '脚本入口'] }, + { from: 'lower', to: 'contained', label: ['widget source', '组件源码'], fromSide: 'bottom', toSide: 'right' }, + { from: 'native', to: 'widgets', label: ['mount', '挂载'] }, + { from: 'contained', to: 'widgets', label: ['render', '渲染'], fromSide: 'bottom', toSide: 'right' }, + ], + }, + agents: { + title: ['Request → answer', '请求 → 答案'], + intro: ['The shell controls access. One shared kernel runs peer conversations; app data stays behind host tools.', 'Shell 控制访问,共享内核运行 Peer 会话;应用数据由宿主工具提供。'], + start: 'peer', + steps: [ + { id: 'person', title: ['Person in app chat', '应用聊天中的用户'], owner: ['Human conversation', '用户会话'], summary: ['Ask or app-owned chat', 'Ask 或应用聊天'], position: { x: 0, y: 0 }, source: sources.shell, + detail: ['A person can use desktop Ask or a supported app-owned chat surface. Trusted shell input carries person origin; a script’s person trigger is only its assertion, not an approval bypass. Answers return to the originating conversation with separate transcript and request IDs.', '用户可使用桌面 Ask 或受支持的应用聊天界面。可信 Shell 输入标记为 person 来源;脚本的 person 触发只是它自己的声明,不能绕过审批。答案返回发起请求的会话,并保留独立记录与请求 ID。'] }, + { id: 'system', title: ['System agent', '系统 Agent'], owner: ['octos · system role', 'octos · 系统角色'], summary: ['Delegate to a prepared peer', '委派给已准备的 Peer'], position: { x: 700, y: 0 }, source: sources.shell, + detail: ['agents.ask handles preparation and readiness. peer_send_input delegates to the actual peer identifier through the shell. System requests use the peer session; peer_gather collects their results. These routes do not grant a general app-to-system delegation API.', 'agents.ask 负责准备并等待就绪;peer_send_input 使用实际 Peer 标识,经 Shell 委派请求。系统请求使用 Peer session,peer_gather 收集结果。这些路由不会提供通用的应用到系统委派 API。'] }, + { id: 'broker', title: ['Shell app broker', 'Shell 应用 broker'], owner: ['OctoSense', 'OctoSense'], summary: ['Consent, sessions and routing', '同意、会话与路由'], position: { x: 350, y: 160 }, source: sources.shell, + detail: ['The broker is the shell’s request adapter. It prepares the allowed app/account peer, opens conversation sessions and routes input and output. Human sharing-context sessions and the system peer session keep separate transcripts while sharing bounded recent context.', 'Broker 是 Shell 的请求适配器,负责准备获准的应用/账户 Peer、打开会话并路由输入输出。用户共享上下文 session 与系统使用的 Peer session 分别保存记录,并共享有限的近期上下文。'] }, + { id: 'kernel', title: ['Shared agent kernel', '共享 Agent 内核'], owner: ['octos runtime', 'octos 运行时'], summary: ['Many peers, many tasks', '多个 Peer、多项任务'], position: { x: 700, y: 350 }, source: sources.kernel, + detail: ['The shell shares one kernel runtime across app peers. A turn orchestration task waits at a start barrier; admission, ownership and acceptance precede its release and agent processing. A peer is not one Tokio task or one UI VM. Desktop/Android use a child process; OpenHarmony embeds the runtime.', 'Shell 的应用 Peer 共享内核运行时。Turn 编排任务先等待启动屏障;准入、归属记录与接受响应完成后才释放屏障并进行 Agent 处理。Peer 不等于一个 Tokio 任务或一个 UI VM。桌面/Android 使用子进程,OpenHarmony 嵌入运行时。'] }, + { id: 'peer', title: ['App peer', '应用 Peer'], owner: ['octos · app/account identity', 'octos · 应用/账户身份'], summary: ['Separate human + system sessions', '独立的用户与系统会话'], position: { x: 350, y: 370 }, source: sources.kernel, + detail: ['A peer is a persistent app/account identity. Each session holds one conversation; the model processes its turn and requests only available tools. Host results return to that turn. Human answers go to their event receiver; results may also leave origin-labelled blackboard records (shared result records), without automatically waking the system agent for human turns.', 'Peer 是持久的应用/账户身份。每个 session 保存一段对话,模型处理其中的 turn 并仅请求可用工具。宿主结果返回该 turn。用户答案发给对应事件接收方;结果也可留下带来源标记的 blackboard(共享结果)记录,用户 turn 不会因此自动唤醒系统 Agent。'] }, + { id: 'tools', title: ['Authorized host tools', '获准的宿主工具'], owner: ['Shell service adapters', 'Shell 服务适配器'], summary: ['Declaration + grant + implementation', '声明 + 授权 + 实现'], position: { x: 0, y: 330 }, source: sources.hub, + detail: ['Host adapters enforce scope and approval before executing operations. Cross-app tools also need the owner’s shareable declaration and caller grant; default App Hub admission rejects arbitrary names such as mail.send. Store-app script tool declarations still lack a dispatcher.', '宿主适配器在执行操作前落实范围与确认策略。跨应用工具还需所有者的共享声明与调用方授权;App Hub 默认准入拒绝 mail.send 等任意名称。商店应用的脚本工具声明仍缺少分发器。'] }, + { id: 'data', title: ['App records / account API', '应用记录/账户 API'], owner: ['App and host services', '应用与宿主服务'], summary: ['Separate from agent memory', '与 Agent 记忆分开'], position: { x: 0, y: 540 }, source: sources.shell, + detail: ['The app’s business data is separate from transcripts and agent memory. Access needs an implemented tool. On Unix, consent plus an available exposed account workspace can enable bounded files.list/read/search; other app storage and host secrets are not automatically readable.', '应用业务数据与对话记录、Agent 记忆分开。访问需要已实现的工具。在 Unix 上,用户同意且开放的账户工作区可用时,可提供有界 files.list/read/search;其他应用存储与宿主密钥不会自动可读。'] }, + ], + links: [ + { from: 'person', to: 'broker', label: ['input / answer', '输入/答案'], both: true, toSide: 'left' }, + { from: 'system', to: 'broker', label: ['delegate / result', '委派/结果'], both: true, toSide: 'right' }, + { from: 'broker', to: 'peer', label: ['turn / events', 'turn/事件'], both: true }, + { from: 'kernel', to: 'system', label: ['runs', '运行'], ownership: true, fromSide: 'right', toSide: 'right' }, + { from: 'kernel', to: 'peer', label: ['hosts', '承载'], ownership: true, fromSide: 'left', toSide: 'right' }, + { from: 'peer', to: 'tools', label: ['tool / result', '工具/结果'], both: true, fromSide: 'left', toSide: 'right' }, + { from: 'tools', to: 'data', label: ['operation / data', '操作/数据'], both: true }, + ], + }, +}; diff --git a/src/data/content.ts b/src/data/content.ts index 4f2008c..e496ba2 100644 --- a/src/data/content.ts +++ b/src/data/content.ts @@ -49,7 +49,7 @@ export const content = { docsLabel: '10 / DOCUMENTATION', docsTitle: 'A clear next step', docsIntro: 'Start with the architecture. Go deeper when you need to.', guides: [ - { slug: 'octosense-apps', title: 'Run an app, follow its agent', description: 'Native Rust, Splash and L0 apps; human conversations, app data, tool grants and Tokio.', tag: 'APPS & AGENTS' }, + { slug: 'octosense-apps', title: 'Run an app, follow its agent', description: 'Explore an interactive map of native Rust, Splash and L0 apps, then follow agent requests, tools and data.', tag: 'APPS & AGENTS' }, { slug: 'why-octoscript', title: 'Why a DSL for agents?', description: 'Motivation, Python / JavaScript tradeoffs, execution boundaries and Splash lineage.', tag: 'MOTIVATION' }, { slug: 'splash-a2app', title: 'Splash → A2App', description: 'Language features, reusable Rust, native UI hosts and in-app agent instruments.', tag: 'LANGUAGE & HOSTS' }, { slug: 'design-to-app', title: 'Pixels → components', description: 'Two detailed flows: generated image atlases and structured Sketch documents.', tag: 'WORKED EXAMPLES' }, @@ -129,7 +129,7 @@ export const content = { stepTitles: ['获取源码', '校验并运行', '连接你的工具'], stepBodies: ['克隆 Octoscript 仓库。', '执行前,先通过校验器检查源码。', '在嵌入宿主中注册应用工具。'], toolsLink: '宿主工具目录', docsLabel: '10 / 开发文档', docsTitle: '下一步,清晰可见', docsIntro: '先了解架构,再按需深入。', guides: [ - { slug: 'octosense-apps', title: '运行应用,追踪 Agent', description: '原生 Rust、Splash 与 L0 应用;用户对话、应用数据、工具授权和 Tokio。', tag: '应用与 AGENT' }, + { slug: 'octosense-apps', title: '运行应用,追踪 Agent', description: '在交互图中探索原生 Rust、Splash 与 L0 应用,再追踪 Agent 请求、工具与数据。', tag: '应用与 AGENT' }, { slug: 'why-octoscript', title: '为何为 Agent 设计 DSL?', description: '设计动机、Python/JavaScript 的取舍、执行边界与 Splash 来源。', tag: '设计动机' }, { slug: 'splash-a2app', title: 'Splash → A2App', description: '语言特性、Rust 复用、原生 UI 宿主与应用内 Agent 仪器。', tag: '语言与宿主' }, { slug: 'design-to-app', title: '像素 → 组件', description: '生成图集与 Sketch 文档两条流程,以及详细的组件映射例证。', tag: '完整例证' }, diff --git a/src/data/docs.ts b/src/data/docs.ts index e204f01..17901e5 100644 --- a/src/data/docs.ts +++ b/src/data/docs.ts @@ -24,5 +24,5 @@ const modules = { }; export const guidesFor = (locale: Locale) => content[locale].guides.map(guide => { const module = modules[locale][guide.slug as keyof typeof modules.en]; - return {...guide, locale, Content: module.Content, headings: module.getHeadings()}; + return {...guide, locale, Content: module.Content, compiledContent: module.compiledContent, headings: module.getHeadings()}; }); diff --git a/tests/architecture.spec.ts b/tests/architecture.spec.ts new file mode 100644 index 0000000..9563d08 --- /dev/null +++ b/tests/architecture.spec.ts @@ -0,0 +1,94 @@ +import { test, expect } from '@playwright/test'; +import AxeBuilder from '@axe-core/playwright'; + +for (const locale of ['en', 'cn'] as const) { + const prefix = locale === 'cn' ? '/cn/' : '/'; + const cn = locale === 'cn'; + test(`${locale}: architecture modes, node details and keyboard navigation`, async ({ page }) => { + const errors: string[] = []; + page.on('pageerror', error => errors.push(error.message)); + await page.goto(prefix); + await page.locator(`a[href="${prefix}docs/octosense-apps/#architecture-explorer"]`).click(); + const graph = page.locator('#architecture-explorer'); + await expect(graph.locator('.react-flow__node')).toHaveCount(8); + await expect(page.getByRole('heading', { level: 1 })).toHaveCount(1); + expect(await page.evaluate(() => { + const h1 = document.querySelector('h1')!; + const graph = document.querySelector('#architecture-explorer')!; + const prose = document.querySelector('.doc-content article h2')!; + return !!(h1.compareDocumentPosition(graph) & Node.DOCUMENT_POSITION_FOLLOWING) + && !!(graph.compareDocumentPosition(prose) & Node.DOCUMENT_POSITION_FOLLOWING); + })).toBe(true); + await graph.getByLabel(cn ? '定位节点' : 'Focus a node', { exact: true }).selectOption('contained'); + await expect(graph.locator('[data-node-detail]')).toContainText(cn ? '没有注册宿主服务' : 'no host services'); + await graph.getByRole('button', { name: cn ? '请求 → 答案' : 'Request → answer', exact: true }).click(); + await expect(graph.locator('.react-flow__node')).toHaveCount(7); + await expect(graph.locator('[data-node-detail]')).toHaveAttribute('data-node-detail', 'peer'); + const toolNode = graph.locator('.react-flow__node[data-id="tools"]'); + await toolNode.focus(); + await page.keyboard.press('Enter'); + await expect(graph.locator('[data-node-detail]')).toHaveAttribute('data-node-detail', 'tools'); + await expect(graph.locator('[data-node-detail]')).toContainText('mail.send'); + await graph.getByLabel(cn ? '定位节点' : 'Focus a node', { exact: true }).selectOption('data'); + await expect(graph.locator('[data-node-detail]')).toContainText(cn ? '账户工作区' : 'account workspace'); + for (const theme of ['light', 'dark']) { + await page.evaluate(value => document.documentElement.dataset.theme = value, theme); + await expect(graph.locator('.react-flow')).toHaveClass(new RegExp(theme)); + const audit = await new AxeBuilder({ page }).include('#architecture-explorer').withTags(['wcag2a', 'wcag2aa', 'wcag21aa']).analyze(); + expect(audit.violations.map(v => ({ id: v.id, targets: v.nodes.map(n => n.target) }))).toEqual([]); + } + await page.locator('.language-link').click(); + await expect(page).toHaveURL(new RegExp(`${cn ? '/' : '/cn/'}docs/octosense-apps/$`)); + expect(errors).toEqual([]); + }); + + test(`${locale}: architecture zoom, fit, pan and mobile page scrolling`, async ({ page }) => { + await page.setViewportSize({ width: 390, height: 844 }); + await page.goto(`${prefix}docs/octosense-apps/#architecture-explorer`); + const graph = page.locator('#architecture-explorer'); + const canvas = graph.getByRole('region', { name: cn ? '架构画布' : 'Architecture canvas', exact: true }); + const viewport = graph.locator('.react-flow__viewport'); + await expect(graph.locator('.architecture-zoom')).toHaveText('100%'); + expect(await page.evaluate(() => document.documentElement.scrollWidth - innerWidth)).toBeLessThanOrEqual(1); + await graph.getByRole('button', { name: cn ? '缩小' : 'Zoom out', exact: true }).click(); + await expect(graph.locator('.architecture-zoom')).not.toHaveText('100%'); + await graph.getByRole('button', { name: cn ? '全图' : 'Fit view', exact: true }).click(); + await expect.poll(async () => Number((await graph.locator('.architecture-zoom').innerText()).replace('%', ''))).toBeLessThan(50); + await graph.getByLabel(cn ? '定位节点' : 'Focus a node', { exact: true }).selectOption('l0'); + await expect(graph.locator('.architecture-zoom')).toHaveText('100%'); + await expect(graph.locator('[data-node-detail]')).toHaveAttribute('data-node-detail', 'l0'); + const before = await viewport.getAttribute('style'); + await canvas.focus(); + await page.keyboard.press('ArrowRight'); + await expect(viewport).not.toHaveAttribute('style', before!); + await graph.getByRole('button', { name: cn ? '移动图' : 'Move graph', exact: true }).click(); + const bounds = await canvas.boundingBox(); + if (!bounds) throw new Error('Missing canvas'); + await canvas.scrollIntoViewIfNeeded(); + const visible = await canvas.boundingBox(); + if (!visible) throw new Error('Missing visible canvas'); + const priorDrag = await viewport.getAttribute('style'); + await page.mouse.move(visible.x + 20, visible.y + 25); + await page.mouse.down(); + await page.mouse.move(visible.x + 100, visible.y + 75, { steps: 5 }); + await page.mouse.up(); + await expect(viewport).not.toHaveAttribute('style', priorDrag!); + await page.mouse.move(visible.x + 30, visible.y + 40); + const scroll = await page.evaluate(() => scrollY); + await page.mouse.wheel(0, 300); + await expect.poll(() => page.evaluate(() => scrollY)).toBeGreaterThan(scroll); + }); +} + +test('architecture text and original guide remain usable without JavaScript', async ({ browser, baseURL }) => { + const context = await browser.newContext({ javaScriptEnabled: false, baseURL }); + const page = await context.newPage(); + await page.goto('/docs/octosense-apps/'); + await expect(page.getByRole('heading', { level: 1 })).toHaveCount(1); + await page.locator('.architecture-text summary').click(); + await expect(page.locator('.architecture-text')).toContainText('default App Hub admission rejects'); + await expect(page.locator('.doc-content article')).toContainText('tools/octo run'); + await page.locator('.language-link').click(); + await expect(page).toHaveURL(/\/cn\/docs\/octosense-apps\/$/); + await context.close(); +}); diff --git a/tsconfig.json b/tsconfig.json index 225416e..1b4ce74 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,9 @@ "compilerOptions": { "types": [ "node" - ] + ], + "jsx": "react-jsx", + "jsxImportSource": "react" }, "include": [ ".astro/types.d.ts",