From a54357cddb357e10e8c167b6e16c7e6f21f0c72f Mon Sep 17 00:00:00 2001 From: Markus Ecker Date: Fri, 12 Dec 2025 19:33:12 +0100 Subject: [PATCH 01/10] Add A2UI composer --- a2ui-composer/.gitignore | 49 + a2ui-composer/AGENTS.md | 19 + a2ui-composer/README.md | 6 + .../apps/widget-builder/components.json | 22 + .../apps/widget-builder/eslint.config.mjs | 18 + .../apps/widget-builder/next-env.d.ts | 6 + .../apps/widget-builder/next.config.ts | 7 + .../apps/widget-builder/package.json | 46 + .../apps/widget-builder/postcss.config.mjs | 7 + .../apps/widget-builder/public/file.svg | 1 + .../apps/widget-builder/public/globe.svg | 1 + .../apps/widget-builder/public/next.svg | 1 + .../apps/widget-builder/public/vercel.svg | 1 + .../apps/widget-builder/public/window.svg | 1 + .../app/api/copilotkit/[[...slug]]/route.ts | 41 + .../src/app/api/copilotkit/a2ui-prompt.ts | 270 + .../src/app/components/layout.tsx | 13 + .../src/app/components/page.tsx | 190 + .../apps/widget-builder/src/app/favicon.ico | Bin 0 -> 25931 bytes .../widget-builder/src/app/gallery/layout.tsx | 13 + .../widget-builder/src/app/gallery/page.tsx | 60 + .../apps/widget-builder/src/app/globals.css | 141 + .../widget-builder/src/app/icons/layout.tsx | 13 + .../widget-builder/src/app/icons/page.tsx | 148 + .../apps/widget-builder/src/app/layout.tsx | 41 + .../apps/widget-builder/src/app/page.tsx | 14 + .../src/app/widget/[id]/layout.tsx | 7 + .../src/app/widget/[id]/page.tsx | 34 + .../src/components/editor/code-editor.tsx | 70 + .../src/components/editor/data-panel.tsx | 126 + .../src/components/editor/editor-header.tsx | 63 + .../src/components/editor/preview-pane.tsx | 43 + .../src/components/editor/widget-editor.tsx | 224 + .../src/components/gallery/gallery-widget.tsx | 39 + .../gallery/widget-preview-modal.tsx | 152 + .../src/components/layout/app-shell.tsx | 51 + .../src/components/layout/sidebar-header.tsx | 14 + .../src/components/layout/sidebar-nav.tsx | 62 + .../src/components/layout/sidebar-widgets.tsx | 212 + .../src/components/layout/sidebar.tsx | 49 + .../src/components/main/create-widget.tsx | 171 + .../src/components/main/preview-gallery.tsx | 27 + .../src/components/main/widget-input.tsx | 52 + .../src/components/ui/alert-dialog.tsx | 157 + .../src/components/ui/button.tsx | 60 + .../src/components/ui/dropdown-menu.tsx | 257 + .../src/components/ui/input.tsx | 21 + .../src/components/ui/resizable.tsx | 56 + .../src/components/ui/separator.tsx | 28 + .../src/contexts/widgets-context.tsx | 110 + .../src/data/gallery/account-balance.ts | 135 + .../src/data/gallery/calendar-day.ts | 218 + .../src/data/gallery/chat-message.ts | 228 + .../src/data/gallery/coffee-order.ts | 309 + .../src/data/gallery/contact-card.ts | 214 + .../src/data/gallery/countdown-timer.ts | 153 + .../src/data/gallery/credit-card.ts | 141 + .../src/data/gallery/email-compose.ts | 238 + .../src/data/gallery/event-detail.ts | 164 + .../src/data/gallery/flight-status.ts | 259 + .../widget-builder/src/data/gallery/index.ts | 96 + .../src/data/gallery/login-form.ts | 158 + .../src/data/gallery/movie-card.ts | 155 + .../src/data/gallery/music-player.ts | 196 + .../data/gallery/notification-permission.ts | 115 + .../src/data/gallery/podcast-episode.ts | 136 + .../src/data/gallery/product-card.ts | 168 + .../src/data/gallery/purchase-complete.ts | 194 + .../src/data/gallery/recipe-card.ts | 182 + .../src/data/gallery/restaurant-card.ts | 166 + .../src/data/gallery/shipping-status.ts | 228 + .../src/data/gallery/software-purchase.ts | 209 + .../src/data/gallery/sports-player.ts | 198 + .../src/data/gallery/stats-card.ts | 107 + .../src/data/gallery/step-counter.ts | 158 + .../src/data/gallery/task-card.ts | 109 + .../src/data/gallery/track-list.ts | 292 + .../src/data/gallery/user-profile.ts | 217 + .../src/data/gallery/weather-current.ts | 244 + .../src/data/gallery/workout-summary.ts | 177 + .../widget-builder/src/lib/components-data.ts | 1045 ++ .../apps/widget-builder/src/lib/storage.ts | 30 + .../apps/widget-builder/src/lib/utils.ts | 6 + .../apps/widget-builder/src/types/widget.ts | 17 + .../apps/widget-builder/tsconfig.json | 34 + a2ui-composer/package.json | 28 + .../packages/a2ui-renderer/package.json | 54 + .../a2ui-renderer/src/A2UIMessageRenderer.tsx | 294 + .../packages/a2ui-renderer/src/A2UIViewer.tsx | 250 + .../packages/a2ui-renderer/src/index.ts | 32 + .../a2ui-renderer/src/styles/components.ts | 6 + .../a2ui-renderer/src/styles/global.ts | 144 + .../packages/a2ui-renderer/src/theme/theme.ts | 410 + .../a2ui-renderer/src/themed-surface.ts | 74 + .../packages/a2ui-renderer/tsconfig.json | 31 + a2ui-composer/packages/a2ui/package.json | 96 + a2ui-composer/packages/a2ui/src/0.8/core.ts | 35 + .../packages/a2ui/src/0.8/data/guards.ts | 236 + .../a2ui/src/0.8/data/model-processor.ts | 855 ++ .../src/0.8/data/signal-model-processor.ts | 31 + .../packages/a2ui/src/0.8/events/a2ui.ts | 28 + .../packages/a2ui/src/0.8/events/base.ts | 19 + .../packages/a2ui/src/0.8/events/events.ts | 53 + a2ui-composer/packages/a2ui/src/0.8/index.ts | 18 + .../packages/a2ui/src/0.8/model.test.ts | 1337 ++ .../packages/a2ui/src/0.8/schemas/.gitignore | 4 + ...erver_to_client_with_standard_catalog.json | 827 ++ .../packages/a2ui/src/0.8/styles/behavior.ts | 55 + .../packages/a2ui/src/0.8/styles/border.ts | 42 + .../packages/a2ui/src/0.8/styles/colors.ts | 73 + .../packages/a2ui/src/0.8/styles/icons.ts | 60 + .../packages/a2ui/src/0.8/styles/index.ts | 37 + .../packages/a2ui/src/0.8/styles/layout.ts | 235 + .../packages/a2ui/src/0.8/styles/opacity.ts | 24 + .../packages/a2ui/src/0.8/styles/shared.ts | 17 + .../packages/a2ui/src/0.8/styles/type.ts | 148 + .../packages/a2ui/src/0.8/styles/utils.ts | 105 + .../a2ui/src/0.8/types/client-event.ts | 80 + .../packages/a2ui/src/0.8/types/colors.ts | 66 + .../packages/a2ui/src/0.8/types/components.ts | 211 + .../packages/a2ui/src/0.8/types/primitives.ts | 60 + .../packages/a2ui/src/0.8/types/types.ts | 521 + .../packages/a2ui/src/0.8/ui/audio.ts | 96 + .../packages/a2ui/src/0.8/ui/button.ts | 66 + .../packages/a2ui/src/0.8/ui/card.ts | 64 + .../packages/a2ui/src/0.8/ui/checkbox.ts | 139 + .../packages/a2ui/src/0.8/ui/column.ts | 104 + .../a2ui/src/0.8/ui/component-registry.ts | 53 + .../packages/a2ui/src/0.8/ui/context/theme.ts | 20 + .../src/0.8/ui/custom-components/index.ts | 6 + .../a2ui/src/0.8/ui/datetime-input.ts | 197 + .../a2ui/src/0.8/ui/directives/directives.ts | 17 + .../a2ui/src/0.8/ui/directives/markdown.ts | 159 + .../a2ui/src/0.8/ui/directives/sanitizer.ts | 40 + .../packages/a2ui/src/0.8/ui/divider.ts | 51 + .../packages/a2ui/src/0.8/ui/icon.ts | 98 + .../packages/a2ui/src/0.8/ui/image.ts | 118 + .../packages/a2ui/src/0.8/ui/list.ts | 72 + .../packages/a2ui/src/0.8/ui/modal.ts | 131 + .../a2ui/src/0.8/ui/multiple-choice.ts | 142 + .../packages/a2ui/src/0.8/ui/root.ts | 553 + a2ui-composer/packages/a2ui/src/0.8/ui/row.ts | 104 + .../packages/a2ui/src/0.8/ui/slider.ts | 159 + .../packages/a2ui/src/0.8/ui/styles.ts | 20 + .../packages/a2ui/src/0.8/ui/surface.ts | 112 + .../packages/a2ui/src/0.8/ui/tabs.ts | 132 + .../packages/a2ui/src/0.8/ui/text-field.ts | 131 + .../packages/a2ui/src/0.8/ui/text.ts | 121 + .../packages/a2ui/src/0.8/ui/theme/manager.ts | 42 + a2ui-composer/packages/a2ui/src/0.8/ui/ui.ts | 121 + .../packages/a2ui/src/0.8/ui/utils/utils.ts | 92 + .../packages/a2ui/src/0.8/ui/utils/youtube.ts | 77 + .../packages/a2ui/src/0.8/ui/video.ts | 96 + a2ui-composer/packages/a2ui/src/index.ts | 17 + a2ui-composer/packages/a2ui/tsconfig.json | 35 + .../packages/eslint-config/README.md | 3 + a2ui-composer/packages/eslint-config/base.js | 32 + a2ui-composer/packages/eslint-config/next.js | 49 + .../packages/eslint-config/package.json | 24 + .../packages/eslint-config/react-internal.js | 39 + .../packages/typescript-config/base.json | 19 + .../packages/typescript-config/nextjs.json | 12 + .../packages/typescript-config/package.json | 9 + .../typescript-config/react-library.json | 7 + a2ui-composer/pnpm-lock.yaml | 11792 ++++++++++++++++ a2ui-composer/pnpm-workspace.yaml | 3 + a2ui-composer/turbo.json | 21 + 167 files changed, 31901 insertions(+) create mode 100644 a2ui-composer/.gitignore create mode 100644 a2ui-composer/AGENTS.md create mode 100644 a2ui-composer/README.md create mode 100644 a2ui-composer/apps/widget-builder/components.json create mode 100644 a2ui-composer/apps/widget-builder/eslint.config.mjs create mode 100644 a2ui-composer/apps/widget-builder/next-env.d.ts create mode 100644 a2ui-composer/apps/widget-builder/next.config.ts create mode 100644 a2ui-composer/apps/widget-builder/package.json create mode 100644 a2ui-composer/apps/widget-builder/postcss.config.mjs create mode 100644 a2ui-composer/apps/widget-builder/public/file.svg create mode 100644 a2ui-composer/apps/widget-builder/public/globe.svg create mode 100644 a2ui-composer/apps/widget-builder/public/next.svg create mode 100644 a2ui-composer/apps/widget-builder/public/vercel.svg create mode 100644 a2ui-composer/apps/widget-builder/public/window.svg create mode 100644 a2ui-composer/apps/widget-builder/src/app/api/copilotkit/[[...slug]]/route.ts create mode 100644 a2ui-composer/apps/widget-builder/src/app/api/copilotkit/a2ui-prompt.ts create mode 100644 a2ui-composer/apps/widget-builder/src/app/components/layout.tsx create mode 100644 a2ui-composer/apps/widget-builder/src/app/components/page.tsx create mode 100644 a2ui-composer/apps/widget-builder/src/app/favicon.ico create mode 100644 a2ui-composer/apps/widget-builder/src/app/gallery/layout.tsx create mode 100644 a2ui-composer/apps/widget-builder/src/app/gallery/page.tsx create mode 100644 a2ui-composer/apps/widget-builder/src/app/globals.css create mode 100644 a2ui-composer/apps/widget-builder/src/app/icons/layout.tsx create mode 100644 a2ui-composer/apps/widget-builder/src/app/icons/page.tsx create mode 100644 a2ui-composer/apps/widget-builder/src/app/layout.tsx create mode 100644 a2ui-composer/apps/widget-builder/src/app/page.tsx create mode 100644 a2ui-composer/apps/widget-builder/src/app/widget/[id]/layout.tsx create mode 100644 a2ui-composer/apps/widget-builder/src/app/widget/[id]/page.tsx create mode 100644 a2ui-composer/apps/widget-builder/src/components/editor/code-editor.tsx create mode 100644 a2ui-composer/apps/widget-builder/src/components/editor/data-panel.tsx create mode 100644 a2ui-composer/apps/widget-builder/src/components/editor/editor-header.tsx create mode 100644 a2ui-composer/apps/widget-builder/src/components/editor/preview-pane.tsx create mode 100644 a2ui-composer/apps/widget-builder/src/components/editor/widget-editor.tsx create mode 100644 a2ui-composer/apps/widget-builder/src/components/gallery/gallery-widget.tsx create mode 100644 a2ui-composer/apps/widget-builder/src/components/gallery/widget-preview-modal.tsx create mode 100644 a2ui-composer/apps/widget-builder/src/components/layout/app-shell.tsx create mode 100644 a2ui-composer/apps/widget-builder/src/components/layout/sidebar-header.tsx create mode 100644 a2ui-composer/apps/widget-builder/src/components/layout/sidebar-nav.tsx create mode 100644 a2ui-composer/apps/widget-builder/src/components/layout/sidebar-widgets.tsx create mode 100644 a2ui-composer/apps/widget-builder/src/components/layout/sidebar.tsx create mode 100644 a2ui-composer/apps/widget-builder/src/components/main/create-widget.tsx create mode 100644 a2ui-composer/apps/widget-builder/src/components/main/preview-gallery.tsx create mode 100644 a2ui-composer/apps/widget-builder/src/components/main/widget-input.tsx create mode 100644 a2ui-composer/apps/widget-builder/src/components/ui/alert-dialog.tsx create mode 100644 a2ui-composer/apps/widget-builder/src/components/ui/button.tsx create mode 100644 a2ui-composer/apps/widget-builder/src/components/ui/dropdown-menu.tsx create mode 100644 a2ui-composer/apps/widget-builder/src/components/ui/input.tsx create mode 100644 a2ui-composer/apps/widget-builder/src/components/ui/resizable.tsx create mode 100644 a2ui-composer/apps/widget-builder/src/components/ui/separator.tsx create mode 100644 a2ui-composer/apps/widget-builder/src/contexts/widgets-context.tsx create mode 100644 a2ui-composer/apps/widget-builder/src/data/gallery/account-balance.ts create mode 100644 a2ui-composer/apps/widget-builder/src/data/gallery/calendar-day.ts create mode 100644 a2ui-composer/apps/widget-builder/src/data/gallery/chat-message.ts create mode 100644 a2ui-composer/apps/widget-builder/src/data/gallery/coffee-order.ts create mode 100644 a2ui-composer/apps/widget-builder/src/data/gallery/contact-card.ts create mode 100644 a2ui-composer/apps/widget-builder/src/data/gallery/countdown-timer.ts create mode 100644 a2ui-composer/apps/widget-builder/src/data/gallery/credit-card.ts create mode 100644 a2ui-composer/apps/widget-builder/src/data/gallery/email-compose.ts create mode 100644 a2ui-composer/apps/widget-builder/src/data/gallery/event-detail.ts create mode 100644 a2ui-composer/apps/widget-builder/src/data/gallery/flight-status.ts create mode 100644 a2ui-composer/apps/widget-builder/src/data/gallery/index.ts create mode 100644 a2ui-composer/apps/widget-builder/src/data/gallery/login-form.ts create mode 100644 a2ui-composer/apps/widget-builder/src/data/gallery/movie-card.ts create mode 100644 a2ui-composer/apps/widget-builder/src/data/gallery/music-player.ts create mode 100644 a2ui-composer/apps/widget-builder/src/data/gallery/notification-permission.ts create mode 100644 a2ui-composer/apps/widget-builder/src/data/gallery/podcast-episode.ts create mode 100644 a2ui-composer/apps/widget-builder/src/data/gallery/product-card.ts create mode 100644 a2ui-composer/apps/widget-builder/src/data/gallery/purchase-complete.ts create mode 100644 a2ui-composer/apps/widget-builder/src/data/gallery/recipe-card.ts create mode 100644 a2ui-composer/apps/widget-builder/src/data/gallery/restaurant-card.ts create mode 100644 a2ui-composer/apps/widget-builder/src/data/gallery/shipping-status.ts create mode 100644 a2ui-composer/apps/widget-builder/src/data/gallery/software-purchase.ts create mode 100644 a2ui-composer/apps/widget-builder/src/data/gallery/sports-player.ts create mode 100644 a2ui-composer/apps/widget-builder/src/data/gallery/stats-card.ts create mode 100644 a2ui-composer/apps/widget-builder/src/data/gallery/step-counter.ts create mode 100644 a2ui-composer/apps/widget-builder/src/data/gallery/task-card.ts create mode 100644 a2ui-composer/apps/widget-builder/src/data/gallery/track-list.ts create mode 100644 a2ui-composer/apps/widget-builder/src/data/gallery/user-profile.ts create mode 100644 a2ui-composer/apps/widget-builder/src/data/gallery/weather-current.ts create mode 100644 a2ui-composer/apps/widget-builder/src/data/gallery/workout-summary.ts create mode 100644 a2ui-composer/apps/widget-builder/src/lib/components-data.ts create mode 100644 a2ui-composer/apps/widget-builder/src/lib/storage.ts create mode 100644 a2ui-composer/apps/widget-builder/src/lib/utils.ts create mode 100644 a2ui-composer/apps/widget-builder/src/types/widget.ts create mode 100644 a2ui-composer/apps/widget-builder/tsconfig.json create mode 100644 a2ui-composer/package.json create mode 100644 a2ui-composer/packages/a2ui-renderer/package.json create mode 100644 a2ui-composer/packages/a2ui-renderer/src/A2UIMessageRenderer.tsx create mode 100644 a2ui-composer/packages/a2ui-renderer/src/A2UIViewer.tsx create mode 100644 a2ui-composer/packages/a2ui-renderer/src/index.ts create mode 100644 a2ui-composer/packages/a2ui-renderer/src/styles/components.ts create mode 100644 a2ui-composer/packages/a2ui-renderer/src/styles/global.ts create mode 100644 a2ui-composer/packages/a2ui-renderer/src/theme/theme.ts create mode 100644 a2ui-composer/packages/a2ui-renderer/src/themed-surface.ts create mode 100644 a2ui-composer/packages/a2ui-renderer/tsconfig.json create mode 100644 a2ui-composer/packages/a2ui/package.json create mode 100644 a2ui-composer/packages/a2ui/src/0.8/core.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/data/guards.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/data/model-processor.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/data/signal-model-processor.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/events/a2ui.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/events/base.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/events/events.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/index.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/model.test.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/schemas/.gitignore create mode 100644 a2ui-composer/packages/a2ui/src/0.8/schemas/server_to_client_with_standard_catalog.json create mode 100644 a2ui-composer/packages/a2ui/src/0.8/styles/behavior.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/styles/border.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/styles/colors.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/styles/icons.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/styles/index.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/styles/layout.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/styles/opacity.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/styles/shared.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/styles/type.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/styles/utils.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/types/client-event.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/types/colors.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/types/components.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/types/primitives.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/types/types.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/ui/audio.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/ui/button.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/ui/card.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/ui/checkbox.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/ui/column.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/ui/component-registry.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/ui/context/theme.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/ui/custom-components/index.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/ui/datetime-input.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/ui/directives/directives.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/ui/directives/markdown.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/ui/directives/sanitizer.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/ui/divider.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/ui/icon.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/ui/image.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/ui/list.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/ui/modal.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/ui/multiple-choice.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/ui/root.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/ui/row.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/ui/slider.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/ui/styles.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/ui/surface.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/ui/tabs.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/ui/text-field.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/ui/text.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/ui/theme/manager.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/ui/ui.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/ui/utils/utils.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/ui/utils/youtube.ts create mode 100644 a2ui-composer/packages/a2ui/src/0.8/ui/video.ts create mode 100644 a2ui-composer/packages/a2ui/src/index.ts create mode 100644 a2ui-composer/packages/a2ui/tsconfig.json create mode 100644 a2ui-composer/packages/eslint-config/README.md create mode 100644 a2ui-composer/packages/eslint-config/base.js create mode 100644 a2ui-composer/packages/eslint-config/next.js create mode 100644 a2ui-composer/packages/eslint-config/package.json create mode 100644 a2ui-composer/packages/eslint-config/react-internal.js create mode 100644 a2ui-composer/packages/typescript-config/base.json create mode 100644 a2ui-composer/packages/typescript-config/nextjs.json create mode 100644 a2ui-composer/packages/typescript-config/package.json create mode 100644 a2ui-composer/packages/typescript-config/react-library.json create mode 100644 a2ui-composer/pnpm-lock.yaml create mode 100644 a2ui-composer/pnpm-workspace.yaml create mode 100644 a2ui-composer/turbo.json diff --git a/a2ui-composer/.gitignore b/a2ui-composer/.gitignore new file mode 100644 index 00000000..e56c90ea --- /dev/null +++ b/a2ui-composer/.gitignore @@ -0,0 +1,49 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# Dependencies +node_modules +.pnp +.pnp.js +.pnpm-store +.corepack + +.wireit + +# Local env files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Testing +coverage + +# Turbo +.turbo + +# Vercel +.vercel + +# Build Outputs +.next/ +out/ +build +dist + + +# Debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Misc +.DS_Store +*.pem + +.venv +.venv_test +.env +__pycache__ + +.playwright-mcp diff --git a/a2ui-composer/AGENTS.md b/a2ui-composer/AGENTS.md new file mode 100644 index 00000000..b2d16bfb --- /dev/null +++ b/a2ui-composer/AGENTS.md @@ -0,0 +1,19 @@ +# Repository Guidelines + +## Project Structure & Module Organization +The monorepo uses pnpm workspaces managed by Turborepo. Front-end code lives in `apps/web` (Next.js 15 app with Tailwind assets in `app`, `public`, and `tailwind.config.ts`). Shared UI primitives are implemented in `packages/a2ui/src`; the compiled output in `packages/a2ui/dist` is generated and should not be edited directly. Python-based agent samples reside under `apps/a2a_samples`, orchestrated with `uv`, while `original_a2ui_source` preserves Google’s original A2UI dump—treat it as read-only documentation of their reference app, useful when comparing how we adapted pieces into this React monorepo. + +## Build, Test, and Development Commands +Run `pnpm install` once, then `pnpm dev` for all workspace dev servers (Next.js watches on port 3000). Use `pnpm --filter web dev` for an isolated UI session and `pnpm --filter a2a-samples dev` to launch the restaurant finder agent (requires `GEMINI_API_KEY` in `apps/a2a_samples/a2ui_restaurant_finder/.env`). Build artifacts with `pnpm build`; `pnpm --filter web lint` and `pnpm --filter web check-types` gate merges, while `pnpm sync:python` keeps the Python virtualenv aligned. + +## Coding Style & Naming Conventions +TypeScript and modern React are mandatory in `apps/web`; prefer function components and colocate UI logic in `app/components`. Follow Prettier's defaults (two-space indentation, single quotes) by running `pnpm format` before commits. Component files use PascalCase (e.g., `AgentPanel.tsx`); hooks and utilities use camelCase in `*.ts`. The shared ESLint preset (`packages/eslint-config`) enforces `turbo/no-undeclared-env-vars`, so surface new env variables via typed helpers. + +## Testing Guidelines +There is no bundled unit-test runner yet, so treat linting and type-checking as minimum CI gates. When adding logic, include targeted checks: for UI, add stories or smoke tests under `apps/web/app` and document manual verification steps in the PR; for agents, prefer lightweight contract tests that exercise handlers via `uv run python -m a2ui_restaurant_finder`. Keep test fixtures (JSON, mock responses) alongside the code they exercise to match existing samples. + +## Commit & Pull Request Guidelines +Existing history uses short imperative subjects ("Add themed a2ui surface"); continue that pattern and reference issues in the footer as needed. Each PR should state scope, testing evidence, and any UI screenshots or terminal output that prove the agent path. Link design docs and flag follow-ups; ensure CI (`pnpm build`) is green before requesting review. + +## Security & Configuration +Never commit `.env` files or API keys; use `.env.local` for web and `.env` inside sample agents. Document any new secrets in `README.md` and expose through `turbo.json` inputs if needed so pipelines remain deterministic. diff --git a/a2ui-composer/README.md b/a2ui-composer/README.md new file mode 100644 index 00000000..fcf16bd5 --- /dev/null +++ b/a2ui-composer/README.md @@ -0,0 +1,6 @@ +``` +pnpm i && pnpm build +pnpm dev +``` + +Needs a `GEMINI_API_KEY` or `OPENAI_API_KEY` fallback in `apps/widget-builder/.env.local` diff --git a/a2ui-composer/apps/widget-builder/components.json b/a2ui-composer/apps/widget-builder/components.json new file mode 100644 index 00000000..edcaef26 --- /dev/null +++ b/a2ui-composer/apps/widget-builder/components.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/a2ui-composer/apps/widget-builder/eslint.config.mjs b/a2ui-composer/apps/widget-builder/eslint.config.mjs new file mode 100644 index 00000000..05e726d1 --- /dev/null +++ b/a2ui-composer/apps/widget-builder/eslint.config.mjs @@ -0,0 +1,18 @@ +import { defineConfig, globalIgnores } from "eslint/config"; +import nextVitals from "eslint-config-next/core-web-vitals"; +import nextTs from "eslint-config-next/typescript"; + +const eslintConfig = defineConfig([ + ...nextVitals, + ...nextTs, + // Override default ignores of eslint-config-next. + globalIgnores([ + // Default ignores of eslint-config-next: + ".next/**", + "out/**", + "build/**", + "next-env.d.ts", + ]), +]); + +export default eslintConfig; diff --git a/a2ui-composer/apps/widget-builder/next-env.d.ts b/a2ui-composer/apps/widget-builder/next-env.d.ts new file mode 100644 index 00000000..c4b7818f --- /dev/null +++ b/a2ui-composer/apps/widget-builder/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +import "./.next/dev/types/routes.d.ts"; + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/a2ui-composer/apps/widget-builder/next.config.ts b/a2ui-composer/apps/widget-builder/next.config.ts new file mode 100644 index 00000000..e9ffa308 --- /dev/null +++ b/a2ui-composer/apps/widget-builder/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/a2ui-composer/apps/widget-builder/package.json b/a2ui-composer/apps/widget-builder/package.json new file mode 100644 index 00000000..007b67e0 --- /dev/null +++ b/a2ui-composer/apps/widget-builder/package.json @@ -0,0 +1,46 @@ +{ + "name": "widget-builder", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev --turbopack --port 3001", + "build": "next build", + "start": "next start", + "lint": "eslint" + }, + "dependencies": { + "@copilotkit/react-core": "^1.50.0", + "@copilotkit/react-ui": "^1.50.0", + "@copilotkit/runtime": "^1.50.0", + "@copilotkitnext/a2ui-renderer": "workspace:*", + "@monaco-editor/react": "^4.7.0", + "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slot": "^1.2.4", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "hono": "^4.6.18", + "localforage": "^1.10.0", + "lucide-react": "^0.556.0", + "next": "16.0.8", + "react": "19.2.1", + "react-dom": "19.2.1", + "react-resizable-panels": "^3.0.6", + "tailwind-merge": "^3.4.0", + "uuid": "^13.0.0", + "zod": "^3.25.75" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "19.2.3", + "@types/react-dom": "19.2.3", + "@types/uuid": "^11.0.0", + "eslint": "^9", + "eslint-config-next": "16.0.8", + "tailwindcss": "^4", + "tw-animate-css": "^1.4.0", + "typescript": "^5" + } +} diff --git a/a2ui-composer/apps/widget-builder/postcss.config.mjs b/a2ui-composer/apps/widget-builder/postcss.config.mjs new file mode 100644 index 00000000..61e36849 --- /dev/null +++ b/a2ui-composer/apps/widget-builder/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/a2ui-composer/apps/widget-builder/public/file.svg b/a2ui-composer/apps/widget-builder/public/file.svg new file mode 100644 index 00000000..004145cd --- /dev/null +++ b/a2ui-composer/apps/widget-builder/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/a2ui-composer/apps/widget-builder/public/globe.svg b/a2ui-composer/apps/widget-builder/public/globe.svg new file mode 100644 index 00000000..567f17b0 --- /dev/null +++ b/a2ui-composer/apps/widget-builder/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/a2ui-composer/apps/widget-builder/public/next.svg b/a2ui-composer/apps/widget-builder/public/next.svg new file mode 100644 index 00000000..5174b28c --- /dev/null +++ b/a2ui-composer/apps/widget-builder/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/a2ui-composer/apps/widget-builder/public/vercel.svg b/a2ui-composer/apps/widget-builder/public/vercel.svg new file mode 100644 index 00000000..77053960 --- /dev/null +++ b/a2ui-composer/apps/widget-builder/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/a2ui-composer/apps/widget-builder/public/window.svg b/a2ui-composer/apps/widget-builder/public/window.svg new file mode 100644 index 00000000..b2b2a44f --- /dev/null +++ b/a2ui-composer/apps/widget-builder/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/a2ui-composer/apps/widget-builder/src/app/api/copilotkit/[[...slug]]/route.ts b/a2ui-composer/apps/widget-builder/src/app/api/copilotkit/[[...slug]]/route.ts new file mode 100644 index 00000000..a1706b4a --- /dev/null +++ b/a2ui-composer/apps/widget-builder/src/app/api/copilotkit/[[...slug]]/route.ts @@ -0,0 +1,41 @@ +import { + CopilotRuntime, + createCopilotEndpoint, + InMemoryAgentRunner, + BasicAgent, +} from "@copilotkit/runtime/v2"; +import { handle } from "hono/vercel"; +import { A2UI_SYSTEM_PROMPT } from "../a2ui-prompt"; + +const determineModel = () => { + if (process.env.GEMINI_API_KEY?.trim()) { + return "google/gemini-3-pro-preview"; + } + if (process.env.OPENAI_API_KEY?.trim()) { + console.warn( + "[CopilotKit] GEMINI_API_KEY not found, falling back to OpenAI", + ); + return "openai/gpt-4o"; + } + console.warn("[CopilotKit] No GEMINI_API_KEY or OPENAI_API_KEY found"); + return "google/gemini-3-pro-preview"; +}; + +const agent = new BasicAgent({ + model: determineModel(), + prompt: A2UI_SYSTEM_PROMPT, + temperature: 0.7, +}); + +const runtime = new CopilotRuntime({ + agents: { default: agent }, + runner: new InMemoryAgentRunner(), +}); + +const app = createCopilotEndpoint({ + runtime, + basePath: "/api/copilotkit", +}); + +export const GET = handle(app); +export const POST = handle(app); diff --git a/a2ui-composer/apps/widget-builder/src/app/api/copilotkit/a2ui-prompt.ts b/a2ui-composer/apps/widget-builder/src/app/api/copilotkit/a2ui-prompt.ts new file mode 100644 index 00000000..ee8f2395 --- /dev/null +++ b/a2ui-composer/apps/widget-builder/src/app/api/copilotkit/a2ui-prompt.ts @@ -0,0 +1,270 @@ +// A2UI Component Catalog and Protocol Schema for LLM prompt +// Based on A2UI Protocol v0.8 + +export const A2UI_COMPONENT_CATALOG = { + "components": { + "Text": { + "description": "Displays text content with optional styling hints", + "properties": { + "text": "The text content - use { literalString: 'value' } for static text or { path: '/data/path' } for data binding", + "usageHint": "Style hint: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'caption' | 'body'" + }, + "required": ["text"] + }, + "Image": { + "description": "Displays an image", + "properties": { + "url": "Image URL - use { literalString: 'url' } or { path: '/data/path' }", + "fit": "'contain' | 'cover' | 'fill' | 'none' | 'scale-down'", + "usageHint": "'icon' | 'avatar' | 'smallFeature' | 'mediumFeature' | 'largeFeature' | 'header'" + }, + "required": ["url"] + }, + "Icon": { + "description": "Displays a named icon", + "properties": { + "name": "Icon name - use { literalString: 'iconName' }. Available: accountCircle, add, arrowBack, arrowForward, attachFile, calendarToday, call, camera, check, close, delete, download, edit, event, error, favorite, favoriteOff, folder, help, home, info, locationOn, lock, lockOpen, mail, menu, moreVert, moreHoriz, notificationsOff, notifications, payment, person, phone, photo, print, refresh, search, send, settings, share, shoppingCart, star, starHalf, starOff, upload, visibility, visibilityOff, warning" + }, + "required": ["name"] + }, + "Row": { + "description": "Horizontal container for children", + "properties": { + "children": "Use { explicitList: ['child-id-1', 'child-id-2'] } for static children", + "distribution": "'start' | 'center' | 'end' | 'spaceBetween' | 'spaceAround' | 'spaceEvenly' (horizontal alignment)", + "alignment": "'start' | 'center' | 'end' | 'stretch' (vertical alignment)" + }, + "required": ["children"] + }, + "Column": { + "description": "Vertical container for children", + "properties": { + "children": "Use { explicitList: ['child-id-1', 'child-id-2'] } for static children", + "distribution": "'start' | 'center' | 'end' | 'spaceBetween' | 'spaceAround' | 'spaceEvenly' (vertical alignment)", + "alignment": "'start' | 'center' | 'end' | 'stretch' (horizontal alignment)" + }, + "required": ["children"] + }, + "Card": { + "description": "A card container with a single child", + "properties": { + "child": "The ID of the component to render inside the card" + }, + "required": ["child"] + }, + "Button": { + "description": "A clickable button", + "properties": { + "child": "The ID of a Text component to display as the button label", + "primary": "boolean - whether this is a primary action button", + "action": "{ name: 'actionName', context: [{ key: 'key', value: { literalString: 'value' } }] }" + }, + "required": ["child", "action"] + }, + "TextField": { + "description": "A text input field", + "properties": { + "label": "Field label - use { literalString: 'Label' }", + "text": "Field value - use { path: '/form/fieldName' } for data binding", + "textFieldType": "'shortText' | 'longText' | 'number' | 'date' | 'obscured'" + }, + "required": ["label"] + }, + "CheckBox": { + "description": "A checkbox input", + "properties": { + "label": "Checkbox label - use { literalString: 'Label' }", + "value": "Checked state - use { literalBoolean: true } or { path: '/form/checked' }" + }, + "required": ["label", "value"] + }, + "Slider": { + "description": "A slider input for numeric values", + "properties": { + "value": "Current value - use { literalNumber: 50 } or { path: '/form/value' }", + "minValue": "Minimum value (number)", + "maxValue": "Maximum value (number)" + }, + "required": ["value"] + }, + "Divider": { + "description": "A visual separator line", + "properties": { + "axis": "'horizontal' | 'vertical'" + } + }, + "List": { + "description": "A scrollable list container", + "properties": { + "children": "Use { explicitList: ['id1', 'id2'] } or { template: { componentId: 'template-id', dataBinding: '/items' } }", + "direction": "'vertical' | 'horizontal'", + "alignment": "'start' | 'center' | 'end' | 'stretch'" + }, + "required": ["children"] + }, + "Tabs": { + "description": "A tabbed container", + "properties": { + "tabItems": "Array of { title: { literalString: 'Tab Name' }, child: 'content-component-id' }" + }, + "required": ["tabItems"] + }, + "Modal": { + "description": "A modal dialog", + "properties": { + "entryPointChild": "ID of the component that triggers the modal (e.g., a button)", + "contentChild": "ID of the component to display inside the modal" + }, + "required": ["entryPointChild", "contentChild"] + }, + "MultipleChoice": { + "description": "A multiple choice selection component", + "properties": { + "selections": "Selected values - use { literalArray: ['option1'] } or { path: '/form/selected' }", + "options": "Array of { label: { literalString: 'Option' }, value: 'option-value' }", + "maxAllowedSelections": "Maximum selections allowed (number)" + }, + "required": ["selections", "options"] + }, + "DateTimeInput": { + "description": "A date/time picker", + "properties": { + "value": "Current value - use { literalString: '' } or { path: '/form/date' }", + "enableDate": "boolean - enable date selection", + "enableTime": "boolean - enable time selection" + }, + "required": ["value"] + } + } +}; + +export const A2UI_SYSTEM_PROMPT = `You are an expert A2UI widget builder. A2UI is a protocol for defining platform-agnostic user interfaces using JSON. + +## IMPORTANT: Widget Format + +You are editing an A2UI widget that has TWO parts: +1. **components** - An array of component definitions (the UI structure) +2. **data** - A JSON object with the data model (the values) + +When using the editWidget tool, you can update either or both parts. + +## Component Structure + +Each component in the array has: +- \`id\`: A unique string identifier +- \`component\`: An object with exactly ONE key (the component type) containing its properties + +Example component: +\`\`\`json +{ + "id": "title", + "component": { + "Text": { + "text": { "literalString": "Hello World" }, + "usageHint": "h1" + } + } +} +\`\`\` + +## Available Components + +${JSON.stringify(A2UI_COMPONENT_CATALOG.components, null, 2)} + +## Key Concepts + +### 1. Literal Values vs Data Binding +- **Literal values**: Static values in the component + - \`{ literalString: "text" }\` + - \`{ literalNumber: 42 }\` + - \`{ literalBoolean: true }\` + - \`{ literalArray: ["a", "b"] }\` +- **Data binding**: Values from the data model using paths + - \`{ path: "/user/name" }\` - reads from data.user.name + +**IMPORTANT: These are MUTUALLY EXCLUSIVE.** Use EITHER a literal value OR a path, NEVER both together. +- ✅ Correct: \`{ literalString: "Hello" }\` +- ✅ Correct: \`{ path: "/user/name" }\` +- ❌ WRONG: \`{ literalString: "Hello", path: "/user/name" }\` - Never mix them! + +### 2. Parent-Child Relationships (Adjacency List) +Components reference children by ID, NOT by nesting. All components are in a flat array. + +**Correct** (flat list with ID references): +\`\`\`json +[ + { "id": "root", "component": { "Column": { "children": { "explicitList": ["title", "content"] } } } }, + { "id": "title", "component": { "Text": { "text": { "literalString": "Title" } } } }, + { "id": "content", "component": { "Text": { "text": { "literalString": "Content" } } } } +] +\`\`\` + +### 3. Root Component +Every widget needs a root component (typically "root" ID) that contains all other components. Usually a Column or Card. + +### 4. Data Model +The data object holds dynamic values that components can bind to: +\`\`\`json +{ + "user": { "name": "John", "email": "john@example.com" }, + "settings": { "darkMode": true } +} +\`\`\` + +Components bind to this using paths: \`{ path: "/user/name" }\` + +## Common Patterns + +### Simple Card with Text +\`\`\`json +{ + "components": [ + { "id": "root", "component": { "Card": { "child": "content" } } }, + { "id": "content", "component": { "Column": { "children": { "explicitList": ["title", "description"] } } } }, + { "id": "title", "component": { "Text": { "text": { "literalString": "Card Title" }, "usageHint": "h2" } } }, + { "id": "description", "component": { "Text": { "text": { "literalString": "Card description text" } } } } + ], + "data": {} +} +\`\`\` + +### Form with Data Binding +\`\`\`json +{ + "components": [ + { "id": "root", "component": { "Column": { "children": { "explicitList": ["nameField", "submitBtn", "submitBtnText"] } } } }, + { "id": "nameField", "component": { "TextField": { "label": { "literalString": "Name" }, "text": { "path": "/form/name" } } } }, + { "id": "submitBtn", "component": { "Button": { "child": "submitBtnText", "action": { "name": "submit" } } } }, + { "id": "submitBtnText", "component": { "Text": { "text": { "literalString": "Submit" } } } } + ], + "data": { "form": { "name": "" } } +} +\`\`\` + +### Button Component (Important!) +Buttons require a child Text component for their label: +\`\`\`json +{ "id": "btn", "component": { "Button": { "child": "btnText", "action": { "name": "click" } } } }, +{ "id": "btnText", "component": { "Text": { "text": { "literalString": "Click Me" } } } } +\`\`\` + +## Using the editWidget Tool + +When editing, always provide complete valid components and/or data. The tool accepts: +- \`data\`: The complete data object to replace the current data +- \`components\`: The complete components array to replace current components + +Example tool call to add a button: +\`\`\` +editWidget({ + components: [ + // ... all existing components plus the new ones + ] +}) +\`\`\` + +Remember: +1. Always include ALL components (it's a replacement, not a merge) +2. Keep component IDs unique +3. Ensure all referenced child IDs exist +4. Use proper data binding syntax for dynamic values`; \ No newline at end of file diff --git a/a2ui-composer/apps/widget-builder/src/app/components/layout.tsx b/a2ui-composer/apps/widget-builder/src/app/components/layout.tsx new file mode 100644 index 00000000..f30484c3 --- /dev/null +++ b/a2ui-composer/apps/widget-builder/src/app/components/layout.tsx @@ -0,0 +1,13 @@ +import { Metadata } from 'next'; + +export const metadata: Metadata = { + title: 'Components | A2UI Composer', +}; + +export default function ComponentsLayout({ + children, +}: { + children: React.ReactNode; +}) { + return children; +} diff --git a/a2ui-composer/apps/widget-builder/src/app/components/page.tsx b/a2ui-composer/apps/widget-builder/src/app/components/page.tsx new file mode 100644 index 00000000..1d35d0bd --- /dev/null +++ b/a2ui-composer/apps/widget-builder/src/app/components/page.tsx @@ -0,0 +1,190 @@ +'use client'; + +import { useState } from 'react'; +import { cn } from '@/lib/utils'; +import { COMPONENTS_DATA, ComponentDoc } from '@/lib/components-data'; +import { A2UIViewer } from '@copilotkitnext/a2ui-renderer'; + +function ComponentSidebar({ + selectedComponent, + onSelect, +}: { + selectedComponent: string; + onSelect: (name: string) => void; +}) { + return ( + + ); +} + +function UsageBlock({ code }: { code: string }) { + const [copied, setCopied] = useState(false); + + const handleCopy = async () => { + await navigator.clipboard.writeText(code); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
+
{code}
+ + {copied && ( + + Copied! + + )} +
+ ); +} + +function PropValues({ values }: { values: string[] }) { + return ( +
+ {values.map((value) => ( + + "{value}" + + ))} +
+ ); +} + +function PropsTable({ component }: { component: ComponentDoc }) { + return ( +
+ + + + + + + + + + {component.props.map((prop, index) => ( + + + + + + ))} + +
NameDescriptionDefault
+ + {prop.name} + + +
{prop.description}
+ {prop.values ? ( + + ) : ( + + {prop.type} + + )} +
+ {prop.default ? ( + + {prop.default} + + ) : ( + + )} +
+
+ ); +} + +function ComponentPreview({ component }: { component: ComponentDoc }) { + if (!component.preview) { + return null; + } + + return ( +
+

Preview

+
+ console.log('Component action:', action)} + /> +
+
+ ); +} + +function ComponentContent({ component }: { component: ComponentDoc }) { + return ( +
+

{component.name}

+

{component.description}

+ + + +

Usage

+ + +

Props

+ +
+ ); +} + +export default function ComponentsPage() { + const [selectedComponent, setSelectedComponent] = useState('Row'); + + // Find the selected component + const component = COMPONENTS_DATA + .flatMap((cat) => cat.components) + .find((c) => c.name === selectedComponent); + + return ( +
+ + {component && } +
+ ); +} diff --git a/a2ui-composer/apps/widget-builder/src/app/favicon.ico b/a2ui-composer/apps/widget-builder/src/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 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 LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/a2ui-composer/apps/widget-builder/src/app/gallery/layout.tsx b/a2ui-composer/apps/widget-builder/src/app/gallery/layout.tsx new file mode 100644 index 00000000..6f8b2430 --- /dev/null +++ b/a2ui-composer/apps/widget-builder/src/app/gallery/layout.tsx @@ -0,0 +1,13 @@ +import { Metadata } from 'next'; + +export const metadata: Metadata = { + title: 'Gallery | A2UI Composer', +}; + +export default function GalleryLayout({ + children, +}: { + children: React.ReactNode; +}) { + return children; +} diff --git a/a2ui-composer/apps/widget-builder/src/app/gallery/page.tsx b/a2ui-composer/apps/widget-builder/src/app/gallery/page.tsx new file mode 100644 index 00000000..72972ae3 --- /dev/null +++ b/a2ui-composer/apps/widget-builder/src/app/gallery/page.tsx @@ -0,0 +1,60 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { GalleryWidget } from '@/components/gallery/gallery-widget'; +import { WidgetPreviewModal } from '@/components/gallery/widget-preview-modal'; +import { Widget } from '@/types/widget'; +import { useWidgets } from '@/contexts/widgets-context'; +import { ALL_GALLERY_WIDGETS } from '@/data/gallery'; + +export default function GalleryPage() { + const [selectedWidget, setSelectedWidget] = useState(null); + const { addWidget } = useWidgets(); + const router = useRouter(); + + const handleOpenInEditor = async () => { + if (!selectedWidget) return; + + // Create a new widget with a unique ID but copy the content + const newWidget: Widget = { + ...selectedWidget, + id: crypto.randomUUID(), + name: `${selectedWidget.name} (Copy)`, + createdAt: new Date(), + updatedAt: new Date(), + }; + + // Save to storage + await addWidget(newWidget); + + // Close modal and navigate to editor + setSelectedWidget(null); + router.push(`/widget/${newWidget.id}`); + }; + + return ( +
+

Gallery

+
+ {ALL_GALLERY_WIDGETS.map((item) => ( +
+ setSelectedWidget(item.widget)} + /> +
+ ))} +
+ + {selectedWidget && ( + setSelectedWidget(null)} + onOpenInEditor={handleOpenInEditor} + /> + )} +
+ ); +} diff --git a/a2ui-composer/apps/widget-builder/src/app/globals.css b/a2ui-composer/apps/widget-builder/src/app/globals.css new file mode 100644 index 00000000..510aa92f --- /dev/null +++ b/a2ui-composer/apps/widget-builder/src/app/globals.css @@ -0,0 +1,141 @@ +/* CopilotKit CSS must load BEFORE Tailwind so our @layer base overrides theirs */ +@import "@copilotkit/react-core/v2/styles.css"; + +@import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-palette-surface-main: var(--palette-surface-main); + --color-palette-border-default: var(--palette-border-default); + --color-palette-grey-200: var(--palette-grey-200); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); +} + +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + + /* Glassy surface colors from dojo */ + --palette-surface-main: #dedee9; + --palette-border-default: #ffffff; + --palette-grey-200: #f0f0f4; + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} + +/* Monaco editor - use background highlight instead of border */ +.monaco-editor .current-line, +.monaco-editor .view-overlays .current-line { + border: none !important; + box-shadow: none !important; + background-color: #f5f5f5 !important; +} diff --git a/a2ui-composer/apps/widget-builder/src/app/icons/layout.tsx b/a2ui-composer/apps/widget-builder/src/app/icons/layout.tsx new file mode 100644 index 00000000..2d7b4b94 --- /dev/null +++ b/a2ui-composer/apps/widget-builder/src/app/icons/layout.tsx @@ -0,0 +1,13 @@ +import { Metadata } from 'next'; + +export const metadata: Metadata = { + title: 'Icons | A2UI Composer', +}; + +export default function IconsLayout({ + children, +}: { + children: React.ReactNode; +}) { + return children; +} diff --git a/a2ui-composer/apps/widget-builder/src/app/icons/page.tsx b/a2ui-composer/apps/widget-builder/src/app/icons/page.tsx new file mode 100644 index 00000000..5c2860f7 --- /dev/null +++ b/a2ui-composer/apps/widget-builder/src/app/icons/page.tsx @@ -0,0 +1,148 @@ +'use client'; + +import { useState } from 'react'; +import { cn } from '@/lib/utils'; +import { ExternalLink, Copy, Check } from 'lucide-react'; +import { A2UIViewer } from '@copilotkitnext/a2ui-renderer'; + +// 100 most important Material Icons for common UI patterns +const MATERIAL_ICONS = [ + // Navigation & Actions + 'home', 'menu', 'close', 'arrow_back', 'arrow_forward', 'chevron_left', 'chevron_right', + 'expand_more', 'expand_less', 'more_vert', 'more_horiz', 'refresh', 'search', 'settings', + + // Common Actions + 'add', 'remove', 'edit', 'delete', 'save', 'done', 'check', 'check_circle', 'cancel', + 'send', 'share', 'download', 'upload', 'print', 'copy', 'content_paste', + + // Communication + 'mail', 'email', 'message', 'chat', 'phone', 'call', 'notifications', 'notification_important', + + // Media + 'play_arrow', 'pause', 'stop', 'skip_next', 'skip_previous', 'volume_up', 'volume_off', + 'mic', 'videocam', 'photo_camera', 'image', 'music_note', + + // People & Account + 'person', 'people', 'group', 'account_circle', 'face', 'sentiment_satisfied', + + // Status & Info + 'info', 'help', 'warning', 'error', 'error_outline', 'report', 'verified', + 'star', 'star_border', 'favorite', 'favorite_border', 'thumb_up', 'thumb_down', + + // Content & Files + 'folder', 'folder_open', 'file_copy', 'description', 'article', 'note', 'attachment', + 'link', 'insert_link', 'cloud', 'cloud_upload', 'cloud_download', + + // Time & Date + 'schedule', 'access_time', 'today', 'event', 'calendar_today', 'alarm', + + // Location + 'place', 'location_on', 'map', 'directions', 'navigation', 'near_me', + + // Shopping & Commerce + 'shopping_cart', 'add_shopping_cart', 'store', 'payment', 'credit_card', 'receipt', + + // Device & Hardware + 'smartphone', 'laptop', 'desktop_windows', 'keyboard', 'mouse', 'bluetooth', 'wifi', + + // Misc UI + 'visibility', 'visibility_off', 'lock', 'lock_open', 'key', 'security', + 'dashboard', 'list', 'view_list', 'grid_view', 'table_chart', 'bar_chart', +]; + +function IconCard({ name, isSelected, onClick }: { name: string; isSelected: boolean; onClick: () => void }) { + return ( + + ); +} + +export default function IconsPage() { + const [selectedIcon, setSelectedIcon] = useState(null); + const [copied, setCopied] = useState(false); + + const handleCopy = async (name: string) => { + const code = `{ "Icon": { "name": { "literalString": "${name}" } } }`; + await navigator.clipboard.writeText(code); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
+
+
+

Icons

+

+ A2UI uses Material Icons. Showing 100 most commonly used icons. +

+
+
+ {selectedIcon && ( + + )} + + Browse all icons + + +
+
+
+ {MATERIAL_ICONS.map((name) => ( + setSelectedIcon(name)} + /> + ))} +
+
+ ); +} diff --git a/a2ui-composer/apps/widget-builder/src/app/layout.tsx b/a2ui-composer/apps/widget-builder/src/app/layout.tsx new file mode 100644 index 00000000..cc1f1b0a --- /dev/null +++ b/a2ui-composer/apps/widget-builder/src/app/layout.tsx @@ -0,0 +1,41 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "./globals.css"; +import { AppShell } from "@/components/layout/app-shell"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "A2UI Composer", + description: "Create and compose A2UI widgets", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + + + + {children} + + + ); +} diff --git a/a2ui-composer/apps/widget-builder/src/app/page.tsx b/a2ui-composer/apps/widget-builder/src/app/page.tsx new file mode 100644 index 00000000..74cc68ea --- /dev/null +++ b/a2ui-composer/apps/widget-builder/src/app/page.tsx @@ -0,0 +1,14 @@ +import { Metadata } from 'next'; +import { CreateWidget } from '@/components/main/create-widget'; + +export const metadata: Metadata = { + title: 'Create | A2UI Composer', +}; + +export default function Home() { + return ( +
+ +
+ ); +} diff --git a/a2ui-composer/apps/widget-builder/src/app/widget/[id]/layout.tsx b/a2ui-composer/apps/widget-builder/src/app/widget/[id]/layout.tsx new file mode 100644 index 00000000..3698f9a8 --- /dev/null +++ b/a2ui-composer/apps/widget-builder/src/app/widget/[id]/layout.tsx @@ -0,0 +1,7 @@ +export default function WidgetEditorLayout({ + children, +}: { + children: React.ReactNode; +}) { + return children; +} diff --git a/a2ui-composer/apps/widget-builder/src/app/widget/[id]/page.tsx b/a2ui-composer/apps/widget-builder/src/app/widget/[id]/page.tsx new file mode 100644 index 00000000..650aeb00 --- /dev/null +++ b/a2ui-composer/apps/widget-builder/src/app/widget/[id]/page.tsx @@ -0,0 +1,34 @@ +'use client'; + +import { use } from 'react'; +import { WidgetEditor } from '@/components/editor/widget-editor'; +import { useWidgets } from '@/contexts/widgets-context'; + +interface WidgetPageProps { + params: Promise<{ id: string }>; +} + +export default function WidgetPage({ params }: WidgetPageProps) { + const { id } = use(params); + const { loading, getWidget } = useWidgets(); + + if (loading) { + return ( +
+
Loading...
+
+ ); + } + + const widget = getWidget(id); + + if (!widget) { + return ( +
+
Widget not found
+
+ ); + } + + return ; +} diff --git a/a2ui-composer/apps/widget-builder/src/components/editor/code-editor.tsx b/a2ui-composer/apps/widget-builder/src/components/editor/code-editor.tsx new file mode 100644 index 00000000..021f3097 --- /dev/null +++ b/a2ui-composer/apps/widget-builder/src/components/editor/code-editor.tsx @@ -0,0 +1,70 @@ +'use client'; + +import Editor, { type Monaco } from '@monaco-editor/react'; + +interface CodeEditorProps { + value: string; + onChange?: (code: string) => void; +} + +export function CodeEditor({ value, onChange }: CodeEditorProps) { + const handleBeforeMount = (monaco: Monaco) => { + // Disable all TypeScript/JavaScript diagnostics + monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({ + noSemanticValidation: true, + noSyntaxValidation: true, + }); + monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({ + noSemanticValidation: true, + noSyntaxValidation: true, + }); + + // Define custom theme with background line highlight instead of border + monaco.editor.defineTheme('custom-light', { + base: 'vs', + inherit: true, + rules: [], + colors: { + 'editor.lineHighlightBackground': '#f5f5f5', + 'editor.lineHighlightBorder': '#00000000', // transparent + }, + }); + }; + + return ( +
+ onChange?.(value ?? '')} + beforeMount={handleBeforeMount} + options={{ + minimap: { enabled: false }, + fontSize: 13, + lineNumbers: 'on', + scrollBeyondLastLine: false, + automaticLayout: true, + tabSize: 2, + wordWrap: 'on', + padding: { top: 16, bottom: 16 }, + cursorStyle: 'line', + renderLineHighlight: 'all', + renderLineHighlightOnlyWhenFocus: false, + guides: { + indentation: false, + bracketPairs: false, + highlightActiveIndentation: false, + }, + overviewRulerBorder: false, + overviewRulerLanes: 0, + hideCursorInOverviewRuler: true, + scrollbar: { + vertical: 'hidden', + horizontal: 'hidden', + }, + }} + /> +
+ ); +} diff --git a/a2ui-composer/apps/widget-builder/src/components/editor/data-panel.tsx b/a2ui-composer/apps/widget-builder/src/components/editor/data-panel.tsx new file mode 100644 index 00000000..e699f694 --- /dev/null +++ b/a2ui-composer/apps/widget-builder/src/components/editor/data-panel.tsx @@ -0,0 +1,126 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { Plus, X } from 'lucide-react'; +import Editor from '@monaco-editor/react'; +import { Button } from '@/components/ui/button'; +import { cn } from '@/lib/utils'; +import type { DataState } from '@/types/widget'; + +interface DataPanelProps { + dataStates: DataState[]; + activeIndex: number; + onActiveIndexChange: (index: number) => void; + onAddState: () => void; + onUpdateState: (index: number, data: Record) => void; + onRenameState: (index: number, name: string) => void; + onDeleteState: (index: number) => void; +} + +export function DataPanel({ + dataStates, + activeIndex, + onActiveIndexChange, + onAddState, + onUpdateState, + onRenameState, + onDeleteState, +}: DataPanelProps) { + const activeState = dataStates[activeIndex]; + const [jsonValue, setJsonValue] = useState(() => + JSON.stringify(activeState?.data ?? {}, null, 2) + ); + + // Update editor when active state changes + useEffect(() => { + setJsonValue(JSON.stringify(activeState?.data ?? {}, null, 2)); + }, [activeIndex, activeState]); + + const handleChange = (value: string | undefined) => { + if (value !== undefined) { + setJsonValue(value); + try { + const parsed = JSON.parse(value); + onUpdateState(activeIndex, parsed); + } catch { + // Invalid JSON, don't update + } + } + }; + + const handleTabDoubleClick = (index: number) => { + const newName = prompt('Rename state:', dataStates[index].name); + if (newName && newName.trim()) { + onRenameState(index, newName.trim()); + } + }; + + return ( +
+ {/* Tabs */} +
+ {dataStates.map((state, index) => ( +
onActiveIndexChange(index)} + onDoubleClick={() => handleTabDoubleClick(index)} + > + {state.name} + {dataStates.length > 1 && index !== 0 && ( + + )} +
+ ))} + +
+ + {/* JSON Editor */} +
+ +
+
+ ); +} diff --git a/a2ui-composer/apps/widget-builder/src/components/editor/editor-header.tsx b/a2ui-composer/apps/widget-builder/src/components/editor/editor-header.tsx new file mode 100644 index 00000000..91ff5318 --- /dev/null +++ b/a2ui-composer/apps/widget-builder/src/components/editor/editor-header.tsx @@ -0,0 +1,63 @@ +'use client'; + +import { useState } from 'react'; +import { Copy, Check, Download } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import type { Widget } from '@/types/widget'; + +interface EditorHeaderProps { + widget: Widget; +} + +export function EditorHeader({ widget }: EditorHeaderProps) { + const [copied, setCopied] = useState(false); + + const handleCopyJson = async () => { + const json = JSON.stringify(widget.components, null, 2); + await navigator.clipboard.writeText(json); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + const handleDownload = () => { + const json = JSON.stringify(widget.components, null, 2); + const blob = new Blob([json], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `a2ui-${widget.id}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }; + + return ( +
+

{widget.name}

+
+ + +
+
+ ); +} diff --git a/a2ui-composer/apps/widget-builder/src/components/editor/preview-pane.tsx b/a2ui-composer/apps/widget-builder/src/components/editor/preview-pane.tsx new file mode 100644 index 00000000..dd8da196 --- /dev/null +++ b/a2ui-composer/apps/widget-builder/src/components/editor/preview-pane.tsx @@ -0,0 +1,43 @@ +'use client'; + +import { useState } from 'react'; +import { Moon, Sun } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { A2UIViewer, type ComponentInstance } from '@copilotkitnext/a2ui-renderer'; + +interface PreviewPaneProps { + root: string; + components: ComponentInstance[]; + data: Record; +} + +export function PreviewPane({ root, components, data }: PreviewPaneProps) { + const [isDark, setIsDark] = useState(false); + + return ( +
+
+ +
+
+ console.log('Widget action:', action)} + /> +
+
+ ); +} diff --git a/a2ui-composer/apps/widget-builder/src/components/editor/widget-editor.tsx b/a2ui-composer/apps/widget-builder/src/components/editor/widget-editor.tsx new file mode 100644 index 00000000..c620999c --- /dev/null +++ b/a2ui-composer/apps/widget-builder/src/components/editor/widget-editor.tsx @@ -0,0 +1,224 @@ +"use client"; + +import { useState, useCallback } from "react"; +import { + CopilotChat, + useAgentContext, + useFrontendTool, +} from "@copilotkit/react-core/v2"; +import { z } from "zod"; +import { EditorHeader } from "./editor-header"; +import { CodeEditor } from "./code-editor"; +import { PreviewPane } from "./preview-pane"; +import { DataPanel } from "./data-panel"; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, +} from "@/components/ui/resizable"; +import { useWidgets } from "@/contexts/widgets-context"; +import type { Widget, DataState } from "@/types/widget"; +import type { ComponentInstance } from "@copilotkitnext/a2ui-renderer"; + +interface WidgetEditorProps { + widget: Widget; +} + +export function WidgetEditor({ widget }: WidgetEditorProps) { + const { updateWidget } = useWidgets(); + + // Local state for components (JSON string for editor) + const [componentsJson, setComponentsJson] = useState(() => + JSON.stringify(widget.components, null, 2), + ); + + // Local state for dataStates + const [dataStates, setDataStates] = useState(widget.dataStates); + const [activeDataStateIndex, setActiveDataStateIndex] = useState(0); + + // Parsed components for preview (null if invalid JSON) + const [components, setComponents] = useState( + widget.components, + ); + + const handleComponentsChange = useCallback( + (json: string) => { + setComponentsJson(json); + try { + const parsed = JSON.parse(json) as ComponentInstance[]; + setComponents(parsed); + updateWidget(widget.id, { components: parsed }); + } catch { + // Invalid JSON, don't update + } + }, + [widget.id, updateWidget], + ); + + const handleDataStatesChange = useCallback( + (newDataStates: DataState[]) => { + setDataStates(newDataStates); + updateWidget(widget.id, { dataStates: newDataStates }); + }, + [widget.id, updateWidget], + ); + + const handleAddDataState = useCallback(() => { + const newState: DataState = { + name: `state-${dataStates.length + 1}`, + data: dataStates[0]?.data ? { ...dataStates[0].data } : {}, + }; + const newDataStates = [...dataStates, newState]; + handleDataStatesChange(newDataStates); + setActiveDataStateIndex(newDataStates.length - 1); + }, [dataStates, handleDataStatesChange]); + + const handleUpdateDataState = useCallback( + (index: number, data: Record) => { + const newDataStates = dataStates.map((ds, i) => + i === index ? { ...ds, data } : ds, + ); + handleDataStatesChange(newDataStates); + }, + [dataStates, handleDataStatesChange], + ); + + const handleRenameDataState = useCallback( + (index: number, name: string) => { + const newDataStates = dataStates.map((ds, i) => + i === index ? { ...ds, name } : ds, + ); + handleDataStatesChange(newDataStates); + }, + [dataStates, handleDataStatesChange], + ); + + const handleDeleteDataState = useCallback( + (index: number) => { + if (dataStates.length <= 1) return; // Keep at least one state + const newDataStates = dataStates.filter((_, i) => i !== index); + handleDataStatesChange(newDataStates); + if (activeDataStateIndex >= newDataStates.length) { + setActiveDataStateIndex(newDataStates.length - 1); + } + }, + [dataStates, activeDataStateIndex, handleDataStatesChange], + ); + + const activeData = dataStates[activeDataStateIndex]?.data ?? {}; + + useAgentContext({ + description: "The current data", + value: activeData, + }); + + useAgentContext({ + description: "The current components", + value: components, + }); + + // Tool for AI to edit the widget + useFrontendTool({ + name: "editWidget", + description: + "Edit the widget by updating its data and/or components. Both parameters are optional - pass only what you want to change.", + parameters: z.object({ + data: z + .string() + .optional() + .describe("The new data object for the widget in JSON. Optional."), + components: z + .string() + .optional() + .describe("The new components array for the widget in JSON. Optional."), + }), + handler: async ({ data, components: newComponents }) => { + if (data !== undefined) { + handleUpdateDataState(activeDataStateIndex, JSON.parse(data)); + } + if (newComponents !== undefined) { + // Pretty-print the JSON for the editor + const prettyJson = JSON.stringify(JSON.parse(newComponents), null, 2); + handleComponentsChange(prettyJson); + } + return { + success: true, + updated: { + data: data !== undefined, + components: newComponents !== undefined, + }, + }; + }, + }); + + return ( +
+ {/* Main editor area */} +
+ + + + {/* Left: Code Editor + Data Panel */} + + + {/* Code Editor */} + + + + + + + {/* Data Panel */} + + + + + + + + + {/* Middle: Preview Pane */} + + + + +
+ + {/* Right: Chat column - styled like left sidebar */} +
+ ( +
+ Powered by 🪁 CopilotKit +
+ )} + /> +
+
+ ); +} diff --git a/a2ui-composer/apps/widget-builder/src/components/gallery/gallery-widget.tsx b/a2ui-composer/apps/widget-builder/src/components/gallery/gallery-widget.tsx new file mode 100644 index 00000000..56046d45 --- /dev/null +++ b/a2ui-composer/apps/widget-builder/src/components/gallery/gallery-widget.tsx @@ -0,0 +1,39 @@ +'use client'; + +import { Widget } from '@/types/widget'; +import { A2UIViewer } from '@copilotkitnext/a2ui-renderer'; + +interface GalleryWidgetProps { + widget: Widget; + height?: number; + onClick?: () => void; +} + +export function GalleryWidget({ widget, height = 200, onClick }: GalleryWidgetProps) { + // Get the first data state's data for preview + const previewData = widget.dataStates?.[0]?.data ?? {}; + + return ( + + ); +} diff --git a/a2ui-composer/apps/widget-builder/src/components/gallery/widget-preview-modal.tsx b/a2ui-composer/apps/widget-builder/src/components/gallery/widget-preview-modal.tsx new file mode 100644 index 00000000..33bd9d20 --- /dev/null +++ b/a2ui-composer/apps/widget-builder/src/components/gallery/widget-preview-modal.tsx @@ -0,0 +1,152 @@ +'use client'; + +import { useEffect } from 'react'; +import { X, RotateCcw, ExternalLink } from 'lucide-react'; +import { Widget } from '@/types/widget'; +import { Button } from '@/components/ui/button'; +import { A2UIViewer } from '@copilotkitnext/a2ui-renderer'; +import Editor from '@monaco-editor/react'; + +interface WidgetPreviewModalProps { + widget: Widget; + onClose: () => void; + onOpenInEditor?: () => void; +} + +export function WidgetPreviewModal({ widget, onClose, onOpenInEditor }: WidgetPreviewModalProps) { + // Get the actual A2UI JSON + const componentsJson = JSON.stringify(widget.components, null, 2); + const dataJson = JSON.stringify(widget.dataStates?.[0]?.data ?? {}, null, 2); + + // Get the first data state's data for preview + const previewData = widget.dataStates?.[0]?.data ?? {}; + + // Close on escape + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose(); + }; + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [onClose]); + + return ( +
+ {/* Backdrop */} +
+ + {/* Modal */} +
+ {/* Left side - Preview */} +
+ {/* Header */} +
+ + {widget.name} +
+ +
+ + {/* Preview area */} +
+ +
+
+ + {/* Right side - Code */} +
+ {/* Header for Components */} +
+ Components +
+ +
+ + {/* Components section */} +
+
+ +
+
+ + {/* Data section */} +
+
+ Data +
+
+ +
+
+
+
+
+ ); +} diff --git a/a2ui-composer/apps/widget-builder/src/components/layout/app-shell.tsx b/a2ui-composer/apps/widget-builder/src/components/layout/app-shell.tsx new file mode 100644 index 00000000..8eb2788d --- /dev/null +++ b/a2ui-composer/apps/widget-builder/src/components/layout/app-shell.tsx @@ -0,0 +1,51 @@ +'use client'; + +import { CopilotKitProvider } from '@copilotkit/react-core/v2'; +import { WidgetsProvider } from '@/contexts/widgets-context'; +import { Sidebar } from './sidebar'; + +interface AppShellProps { + children: React.ReactNode; +} + +export function AppShell({ children }: AppShellProps) { + return ( + + +
+ {/* Background blur circles - Glassy effect from dojo */} + {/* Ellipse 1351 - Orange glow top right */} +
+ + {/* Ellipse 1347 - Gray glow bottom right */} +
+ + {/* Ellipse 1350 - Gray glow top center */} +
+ + {/* Ellipse 1348 - Light purple glow center */} +
+ + {/* Ellipse 1346 - Yellow glow left */} +
+ + {/* Ellipse 1268 - Orange glow bottom left */} +
+ +
+ +
+ {children} +
+
+
+ + + ); +} diff --git a/a2ui-composer/apps/widget-builder/src/components/layout/sidebar-header.tsx b/a2ui-composer/apps/widget-builder/src/components/layout/sidebar-header.tsx new file mode 100644 index 00000000..7ef83c7e --- /dev/null +++ b/a2ui-composer/apps/widget-builder/src/components/layout/sidebar-header.tsx @@ -0,0 +1,14 @@ +'use client'; + +import { Sparkles } from 'lucide-react'; + +export function SidebarHeader() { + return ( +
+
+ +
+ A2UI Composer +
+ ); +} diff --git a/a2ui-composer/apps/widget-builder/src/components/layout/sidebar-nav.tsx b/a2ui-composer/apps/widget-builder/src/components/layout/sidebar-nav.tsx new file mode 100644 index 00000000..cadde688 --- /dev/null +++ b/a2ui-composer/apps/widget-builder/src/components/layout/sidebar-nav.tsx @@ -0,0 +1,62 @@ +'use client'; + +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import { SquarePlus, LayoutGrid, Box, Shapes, LucideIcon } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +interface NavItemProps { + icon: LucideIcon; + label: string; + href: string; + selected?: boolean; + onClick?: () => void; +} + +function NavItem({ icon: Icon, label, href, selected, onClick }: NavItemProps) { + return ( + + + {label} + + ); +} + +interface SidebarNavProps { + onNavigate?: () => void; +} + +export function SidebarNav({ onNavigate }: SidebarNavProps) { + const pathname = usePathname(); + + const navItems = [ + { icon: SquarePlus, label: 'Create', href: '/' }, + { icon: LayoutGrid, label: 'Gallery', href: '/gallery' }, + { icon: Box, label: 'Components', href: '/components' }, + { icon: Shapes, label: 'Icons', href: '/icons' }, + ]; + + return ( + + ); +} diff --git a/a2ui-composer/apps/widget-builder/src/components/layout/sidebar-widgets.tsx b/a2ui-composer/apps/widget-builder/src/components/layout/sidebar-widgets.tsx new file mode 100644 index 00000000..93f8134f --- /dev/null +++ b/a2ui-composer/apps/widget-builder/src/components/layout/sidebar-widgets.tsx @@ -0,0 +1,212 @@ +'use client'; + +import { useState, useRef, useEffect } from 'react'; +import Link from 'next/link'; +import { usePathname, useRouter } from 'next/navigation'; +import { MoreHorizontal, Pencil, Trash2 } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { useWidgets } from '@/contexts/widgets-context'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog'; + +interface WidgetItemProps { + id: string; + name: string; + isSelected: boolean; + onRename: (newName: string) => void; + onDelete: () => void; + onNavigate?: () => void; +} + +function WidgetItem({ id, name, isSelected, onRename, onDelete, onNavigate }: WidgetItemProps) { + const [isHovered, setIsHovered] = useState(false); + const [isMenuOpen, setIsMenuOpen] = useState(false); + const [isEditing, setIsEditing] = useState(false); + const [editValue, setEditValue] = useState(name); + const [showDeleteDialog, setShowDeleteDialog] = useState(false); + const inputRef = useRef(null); + + useEffect(() => { + if (isEditing && inputRef.current) { + inputRef.current.focus(); + inputRef.current.select(); + } + }, [isEditing]); + + const handleRename = () => { + setIsMenuOpen(false); + setIsEditing(true); + setEditValue(name); + }; + + const handleRenameSubmit = () => { + const newName = editValue.trim() || 'Untitled widget'; + onRename(newName); + setIsEditing(false); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + handleRenameSubmit(); + } else if (e.key === 'Escape') { + setIsEditing(false); + setEditValue(name); + } + }; + + if (isEditing) { + return ( +
+ setEditValue(e.target.value)} + onBlur={handleRenameSubmit} + onKeyDown={handleKeyDown} + placeholder="Widget name" + className="flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground" + /> +
+ ); + } + + return ( + <> + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + {name || 'Untitled widget'} + + + e.preventDefault()} + className={cn( + "flex h-6 w-6 shrink-0 items-center justify-center rounded hover:bg-black/5", + (isHovered || isMenuOpen) ? "opacity-100" : "opacity-0" + )} + > + + + + { e.preventDefault(); handleRename(); }}> + + Rename + + { e.preventDefault(); setShowDeleteDialog(true); }} + className="text-red-600 focus:text-red-600" + > + + Delete + + + + + + + + + Delete ? + + All widget studio is stored locally on your device so there is no backup. This action cannot be undone. + + + + + Delete + + + Cancel + + + + + + ); +} + +interface SidebarWidgetsProps { + onNavigate?: () => void; +} + +export function SidebarWidgets({ onNavigate }: SidebarWidgetsProps) { + const pathname = usePathname(); + const router = useRouter(); + const { widgets, loading, updateWidget, removeWidget } = useWidgets(); + + const handleRename = (id: string, newName: string) => { + updateWidget(id, { name: newName }); + }; + + const handleDelete = (id: string) => { + removeWidget(id); + // Navigate to home if we're on the deleted widget's page + if (pathname === `/widget/${id}`) { + router.push('/'); + } + }; + + // Extract widget ID from pathname if on a widget page + const currentWidgetId = pathname.startsWith('/widget/') + ? pathname.replace('/widget/', '') + : null; + + return ( +
+ + Widgets + +
+ {loading ? ( +
Loading...
+ ) : widgets.length === 0 ? ( +
No widgets yet
+ ) : ( + widgets.map(widget => ( + handleRename(widget.id, newName)} + onDelete={() => handleDelete(widget.id)} + onNavigate={onNavigate} + /> + )) + )} +
+
+ ); +} diff --git a/a2ui-composer/apps/widget-builder/src/components/layout/sidebar.tsx b/a2ui-composer/apps/widget-builder/src/components/layout/sidebar.tsx new file mode 100644 index 00000000..4375f996 --- /dev/null +++ b/a2ui-composer/apps/widget-builder/src/components/layout/sidebar.tsx @@ -0,0 +1,49 @@ +'use client'; + +import { useState } from 'react'; +import { Menu } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { SidebarHeader } from './sidebar-header'; +import { SidebarNav } from './sidebar-nav'; +import { SidebarWidgets } from './sidebar-widgets'; +import { Button } from '@/components/ui/button'; + +export function Sidebar() { + const [isOpen, setIsOpen] = useState(false); + + return ( + <> + {/* Mobile toggle button */} + {!isOpen && ( + + )} + + {/* Overlay for mobile */} + {isOpen && ( +
setIsOpen(false)} + /> + )} + + {/* Sidebar */} + + + ); +} diff --git a/a2ui-composer/apps/widget-builder/src/components/main/create-widget.tsx b/a2ui-composer/apps/widget-builder/src/components/main/create-widget.tsx new file mode 100644 index 00000000..b34fdd89 --- /dev/null +++ b/a2ui-composer/apps/widget-builder/src/components/main/create-widget.tsx @@ -0,0 +1,171 @@ +'use client'; + +import { useState, useRef } from 'react'; +import { useRouter } from 'next/navigation'; +import { v4 as uuidv4 } from 'uuid'; +import { + useAgent, + useCopilotKit, + useFrontendTool, +} from '@copilotkit/react-core/v2'; +import { z } from 'zod'; +import { WidgetInput } from './widget-input'; +import { useWidgets } from '@/contexts/widgets-context'; +import type { Widget } from '@/types/widget'; +import type { ComponentInstance } from '@copilotkitnext/a2ui-renderer'; + +const DEFAULT_COMPONENTS: ComponentInstance[] = [ + { + id: 'root', + component: { + Card: { + child: 'content', + }, + }, + }, + { + id: 'content', + component: { + Text: { + text: { path: '/title' }, + }, + }, + }, +]; + +const DEFAULT_DATA = { title: 'Hello World' }; + +export function CreateWidget() { + const router = useRouter(); + const { addWidget } = useWidgets(); + const { agent } = useAgent(); + const { copilotkit } = useCopilotKit(); + + const [inputValue, setInputValue] = useState(''); + const [isGenerating, setIsGenerating] = useState(false); + + // Refs to capture tool results + const generatedName = useRef(null); + const generatedComponents = useRef(null); + const generatedData = useRef | null>(null); + + // Frontend tool for creating new widgets - captures AI output + useFrontendTool({ + name: 'editWidget', + description: + 'Create a new widget with the specified name, data, and components.', + parameters: z.object({ + name: z + .string() + .describe('A short descriptive name for the widget (e.g. "User Profile Card", "Weather Widget").'), + data: z + .string() + .describe('The data object for the widget in JSON.'), + components: z + .string() + .describe('The components array for the widget in JSON.'), + }), + handler: async ({ name, data, components }) => { + generatedName.current = name; + generatedData.current = JSON.parse(data); + generatedComponents.current = JSON.parse(components); + return { success: true }; + }, + }); + + const handleCreate = async () => { + if (!inputValue.trim() || isGenerating) return; + + setIsGenerating(true); + + // Reset refs + generatedName.current = null; + generatedComponents.current = null; + generatedData.current = null; + + const widgetId = uuidv4(); + + try { + // Reset agent for fresh conversation + agent.setMessages([]); + agent.threadId = widgetId; + + // Add user message + agent.addMessage({ + id: crypto.randomUUID(), + role: 'user', + content: inputValue, + }); + + // Run agent (will call editWidget tool) + await copilotkit.runAgent({ agent }); + + // Create widget with generated content (or defaults if tool wasn't called) + const newWidget: Widget = { + id: widgetId, + name: generatedName.current ?? 'Untitled widget', + createdAt: new Date(), + updatedAt: new Date(), + root: 'root', + components: generatedComponents.current ?? DEFAULT_COMPONENTS, + dataStates: [ + { + name: 'default', + data: generatedData.current ?? DEFAULT_DATA, + }, + ], + }; + + await addWidget(newWidget); + router.push(`/widget/${widgetId}`); + } catch (error) { + console.error('Failed to generate widget:', error); + setIsGenerating(false); + } + }; + + const handleStartBlank = async () => { + const id = uuidv4(); + const newWidget: Widget = { + id, + name: 'Untitled widget', + createdAt: new Date(), + updatedAt: new Date(), + root: 'root', + components: DEFAULT_COMPONENTS, + dataStates: [ + { + name: 'default', + data: DEFAULT_DATA, + }, + ], + }; + await addWidget(newWidget); + router.push(`/widget/${id}`); + }; + + return ( +
+

What would you like to build?

+ + + Powered by 🪁 CopilotKit + + {isGenerating ? ( + Generating widget... + ) : ( + + )} +
+ ); +} diff --git a/a2ui-composer/apps/widget-builder/src/components/main/preview-gallery.tsx b/a2ui-composer/apps/widget-builder/src/components/main/preview-gallery.tsx new file mode 100644 index 00000000..f0363cbe --- /dev/null +++ b/a2ui-composer/apps/widget-builder/src/components/main/preview-gallery.tsx @@ -0,0 +1,27 @@ +const suggestions = [ + { icon: 'person', label: 'Profile card' }, + { icon: 'thermostat', label: 'Weather widget' }, + { icon: 'task_alt', label: 'Todo list' }, + { icon: 'music_note', label: 'Music player' }, +]; + +interface PreviewGalleryProps { + onSelect?: (label: string) => void; +} + +export function PreviewGallery({ onSelect }: PreviewGalleryProps) { + return ( +
+ {suggestions.map((s) => ( + + ))} +
+ ); +} diff --git a/a2ui-composer/apps/widget-builder/src/components/main/widget-input.tsx b/a2ui-composer/apps/widget-builder/src/components/main/widget-input.tsx new file mode 100644 index 00000000..94483979 --- /dev/null +++ b/a2ui-composer/apps/widget-builder/src/components/main/widget-input.tsx @@ -0,0 +1,52 @@ +'use client'; + +interface WidgetInputProps { + value: string; + onChange: (value: string) => void; + onSubmit?: () => void; + disabled?: boolean; +} + +export function WidgetInput({ value, onChange, onSubmit, disabled }: WidgetInputProps) { + const hasText = value.trim().length > 0; + const canSubmit = hasText && !disabled; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey && canSubmit) { + e.preventDefault(); + onSubmit?.(); + } + }; + + return ( +
+