@adia-ai/gen-ui 0.8.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +77 -0
- package/compose/CHANGELOG.md +1707 -0
- package/compose/README.md +215 -0
- package/compose/core/artifacts.js +262 -0
- package/compose/core/context-store.js +218 -0
- package/compose/core/generator.d.ts +4 -0
- package/compose/core/generator.js +602 -0
- package/compose/core/pipeline/engine.js +289 -0
- package/compose/core/pipeline/types.js +91 -0
- package/compose/core/plan-primitives.js +120 -0
- package/compose/core/plan-turn.js +145 -0
- package/compose/core/reference.js +150 -0
- package/compose/core/state.js +15 -0
- package/compose/evals/harness.d.ts +4 -0
- package/compose/evals/harness.mjs +193 -0
- package/compose/index.d.ts +4 -0
- package/compose/index.js +16 -0
- package/compose/shared/validate-and-repair.js +122 -0
- package/compose/strategies/_shared/chunk-loader.js +282 -0
- package/compose/strategies/cascade.d.ts +67 -0
- package/compose/strategies/cascade.js +165 -0
- package/compose/strategies/escalation.d.ts +13 -0
- package/compose/strategies/escalation.js +132 -0
- package/compose/strategies/free-form-composer/index.js +254 -0
- package/compose/strategies/free-form-composer/system-prompt.js +205 -0
- package/compose/strategies/free-form-composer/transpile.js +279 -0
- package/compose/strategies/monolithic/_shared.js +1619 -0
- package/compose/strategies/monolithic/generate-instant.js +228 -0
- package/compose/strategies/monolithic/generate-pro.js +490 -0
- package/compose/strategies/monolithic/generate-thinking.js +206 -0
- package/compose/strategies/registry.d.ts +20 -0
- package/compose/strategies/registry.js +495 -0
- package/compose/strategies/zettel/_smoke.js +27 -0
- package/compose/strategies/zettel/chunk-composer.js +214 -0
- package/compose/strategies/zettel/chunk-refiner.js +582 -0
- package/compose/strategies/zettel/chunk-synthesizer.js +458 -0
- package/compose/strategies/zettel/composer.js +78 -0
- package/compose/strategies/zettel/composition-library.js +418 -0
- package/compose/strategies/zettel/generate.js +15 -0
- package/compose/strategies/zettel/generator-adapter.d.ts +4 -0
- package/compose/strategies/zettel/generator-adapter.js +242 -0
- package/compose/strategies/zettel/issue-reporter.js +607 -0
- package/compose/strategies/zettel/session-store.js +121 -0
- package/compose/strategies/zettel/state-cache.js +158 -0
- package/compose/transpiler/transpiler-maps.js +675 -0
- package/compose/transpiler/transpiler.d.ts +4 -0
- package/compose/transpiler/transpiler.js +962 -0
- package/corpus/CHANGELOG.md +1957 -0
- package/corpus/README.md +279 -0
- package/corpus/catalog-a2ui_0_9.d.ts +4 -0
- package/corpus/catalog-a2ui_0_9.json +29571 -0
- package/corpus/catalog-a2ui_0_9_rules.txt +1215 -0
- package/corpus/chunk-embeddings.d.ts +4 -0
- package/corpus/chunk-embeddings.json +1 -0
- package/corpus/chunks/_index.json +16658 -0
- package/corpus/chunks/_source-hashes.json +571 -0
- package/corpus/chunks/access-denied-with-request.json +124 -0
- package/corpus/chunks/access-request-form.json +166 -0
- package/corpus/chunks/access-request-pending.json +133 -0
- package/corpus/chunks/accordion-settings.json +176 -0
- package/corpus/chunks/achievement-badges.json +413 -0
- package/corpus/chunks/achievement-unlock.json +104 -0
- package/corpus/chunks/action-list-record-actions.json +84 -0
- package/corpus/chunks/activity-feed.json +390 -0
- package/corpus/chunks/admin-page-header-flush.json +83 -0
- package/corpus/chunks/admin-page-header-title-description.json +80 -0
- package/corpus/chunks/admin-page-header-with-actions.json +107 -0
- package/corpus/chunks/admin-page-header-with-tabs.json +144 -0
- package/corpus/chunks/admin-shell-rounded-borderless.json +287 -0
- package/corpus/chunks/admin-shell-standard.json +310 -0
- package/corpus/chunks/admin-sidebar-brand-signin.json +158 -0
- package/corpus/chunks/admin-sidebar-brand-version.json +130 -0
- package/corpus/chunks/admin-sidebar-flat-nav.json +131 -0
- package/corpus/chunks/admin-sidebar-nav-dividers.json +150 -0
- package/corpus/chunks/admin-sidebar-nav-groups.json +171 -0
- package/corpus/chunks/admin-sidebar-workspace-switcher.json +139 -0
- package/corpus/chunks/admin-topbar-breadcrumb-actions.json +130 -0
- package/corpus/chunks/admin-topbar-breadcrumb-minimal.json +79 -0
- package/corpus/chunks/admin-topbar-deep-breadcrumb.json +129 -0
- package/corpus/chunks/admin-topbar-search.json +104 -0
- package/corpus/chunks/admin-topbar-status-indicator.json +122 -0
- package/corpus/chunks/admin-topbar-trailing-toggle.json +114 -0
- package/corpus/chunks/agent-activity-feed.json +163 -0
- package/corpus/chunks/agent-artifact-stub-card.json +115 -0
- package/corpus/chunks/agent-canvas-shell.json +305 -0
- package/corpus/chunks/agent-citations-source-chips.json +82 -0
- package/corpus/chunks/agent-command-palette-search.json +120 -0
- package/corpus/chunks/agent-cost-detailed.json +260 -0
- package/corpus/chunks/agent-cost-inline.json +78 -0
- package/corpus/chunks/agent-memory-detail.json +181 -0
- package/corpus/chunks/agent-memory-editor.json +173 -0
- package/corpus/chunks/agent-memory-list.json +207 -0
- package/corpus/chunks/agent-pipeline-status-stages.json +61 -0
- package/corpus/chunks/agent-prompt-card.json +179 -0
- package/corpus/chunks/agent-prompt-library-grid.json +341 -0
- package/corpus/chunks/agent-prompt-picker.json +282 -0
- package/corpus/chunks/agent-rate-limit.json +100 -0
- package/corpus/chunks/agent-reasoning-collapsed.json +128 -0
- package/corpus/chunks/agent-tool-call-accordion.json +109 -0
- package/corpus/chunks/agent-tool-call-collapsed.json +98 -0
- package/corpus/chunks/agent-tool-call-error.json +112 -0
- package/corpus/chunks/agent-tool-call-result.json +145 -0
- package/corpus/chunks/agent-tool-call-running.json +85 -0
- package/corpus/chunks/agent-tool-call-success.json +137 -0
- package/corpus/chunks/agent-trace-timeline.json +244 -0
- package/corpus/chunks/ai-streaming-response.json +147 -0
- package/corpus/chunks/alert-banner-stack.json +78 -0
- package/corpus/chunks/alert-banner.json +55 -0
- package/corpus/chunks/alert-dialog-simple.json +72 -0
- package/corpus/chunks/alert-dialog-typed.json +107 -0
- package/corpus/chunks/alert-dialog-unsaved.json +81 -0
- package/corpus/chunks/analytics-chart.json +213 -0
- package/corpus/chunks/analytics-sparklines-dashboard.json +259 -0
- package/corpus/chunks/anchor-bar-selection-bar.json +226 -0
- package/corpus/chunks/api-key-table.json +148 -0
- package/corpus/chunks/api-keys-create.json +140 -0
- package/corpus/chunks/api-keys-list.json +271 -0
- package/corpus/chunks/app-nav-badges-counts.json +77 -0
- package/corpus/chunks/app-nav-badges-group-rollup-badges.json +100 -0
- package/corpus/chunks/app-nav-badges-plan-tiers.json +76 -0
- package/corpus/chunks/app-nav-badges-status-callouts.json +75 -0
- package/corpus/chunks/app-nav-full-example.json +195 -0
- package/corpus/chunks/app-nav-groups.json +103 -0
- package/corpus/chunks/app-nav-items.json +74 -0
- package/corpus/chunks/app-nav-labels-and-dividers.json +92 -0
- package/corpus/chunks/app-nav-spacer.json +77 -0
- package/corpus/chunks/app-product-listing-card.json +214 -0
- package/corpus/chunks/approvals-detail.json +240 -0
- package/corpus/chunks/approvals-queue.json +321 -0
- package/corpus/chunks/audit-log-basic.json +184 -0
- package/corpus/chunks/audit-log.json +174 -0
- package/corpus/chunks/auth-account-deleted-actions.json +51 -0
- package/corpus/chunks/auth-account-deleted.json +202 -0
- package/corpus/chunks/auth-card-content.json +260 -0
- package/corpus/chunks/auth-card-header.json +205 -0
- package/corpus/chunks/auth-email-entry.json +65 -0
- package/corpus/chunks/auth-email-verify-confirm.json +52 -0
- package/corpus/chunks/auth-email-verify.json +196 -0
- package/corpus/chunks/auth-forbidden-actions.json +59 -0
- package/corpus/chunks/auth-forbidden.json +203 -0
- package/corpus/chunks/auth-invite-actions.json +49 -0
- package/corpus/chunks/auth-invite-team-card.json +86 -0
- package/corpus/chunks/auth-link-expired-form.json +65 -0
- package/corpus/chunks/auth-link-expired.json +196 -0
- package/corpus/chunks/auth-locked-account.json +211 -0
- package/corpus/chunks/auth-locked-recovery-options.json +60 -0
- package/corpus/chunks/auth-mfa-enrollment-submit.json +54 -0
- package/corpus/chunks/auth-mfa-enrollment.json +284 -0
- package/corpus/chunks/auth-mfa-fallback-actions.json +52 -0
- package/corpus/chunks/auth-mfa-recovery.json +61 -0
- package/corpus/chunks/auth-new-password-form.json +75 -0
- package/corpus/chunks/auth-new-password.json +217 -0
- package/corpus/chunks/auth-oauth-fallback-actions.json +52 -0
- package/corpus/chunks/auth-oauth-interstitial.json +160 -0
- package/corpus/chunks/auth-password-challenge.json +94 -0
- package/corpus/chunks/auth-password-reset-form.json +64 -0
- package/corpus/chunks/auth-password-reset.json +195 -0
- package/corpus/chunks/auth-profile-form.json +72 -0
- package/corpus/chunks/auth-profile-setup.json +194 -0
- package/corpus/chunks/auth-reset-sent.json +188 -0
- package/corpus/chunks/auth-session-expired-actions.json +52 -0
- package/corpus/chunks/auth-session-expired.json +201 -0
- package/corpus/chunks/auth-signin-card-email.json +245 -0
- package/corpus/chunks/auth-signin-card-magic-link.json +147 -0
- package/corpus/chunks/auth-signin-card-mfa.json +231 -0
- package/corpus/chunks/auth-signin-card-otp.json +205 -0
- package/corpus/chunks/auth-signin-card-password.json +264 -0
- package/corpus/chunks/auth-signin-card-recovery.json +218 -0
- package/corpus/chunks/auth-signout-actions.json +51 -0
- package/corpus/chunks/auth-signout.json +184 -0
- package/corpus/chunks/auth-signup-email-entry.json +66 -0
- package/corpus/chunks/auth-signup-entry.json +281 -0
- package/corpus/chunks/auth-signup-social-auths.json +54 -0
- package/corpus/chunks/auth-signup-verify.json +227 -0
- package/corpus/chunks/auth-social-auths.json +62 -0
- package/corpus/chunks/auth-sso-providers.json +55 -0
- package/corpus/chunks/auth-sso-required.json +213 -0
- package/corpus/chunks/auth-team-invite.json +288 -0
- package/corpus/chunks/avatar-group-overflow.json +137 -0
- package/corpus/chunks/avatar-stack-overflow.json +124 -0
- package/corpus/chunks/banner-announcement-beta.json +99 -0
- package/corpus/chunks/banner-announcement-promo.json +102 -0
- package/corpus/chunks/banner-live-update.json +102 -0
- package/corpus/chunks/banner-system-maintenance.json +101 -0
- package/corpus/chunks/banner-system-offline.json +85 -0
- package/corpus/chunks/banner-system-update.json +102 -0
- package/corpus/chunks/bell-menu-empty.json +107 -0
- package/corpus/chunks/bell-menu-unread.json +395 -0
- package/corpus/chunks/billing-cancel-subscription-danger.json +61 -0
- package/corpus/chunks/billing-invoice-history-table.json +75 -0
- package/corpus/chunks/billing-overview.json +202 -0
- package/corpus/chunks/billing-payment-method-card-on-file.json +157 -0
- package/corpus/chunks/billing-progress-label.json +101 -0
- package/corpus/chunks/blog-post-card.json +212 -0
- package/corpus/chunks/breadcrumb-navigation.json +72 -0
- package/corpus/chunks/bulk-action-toolbar-floating.json +199 -0
- package/corpus/chunks/bulk-action-toolbar-swap.json +158 -0
- package/corpus/chunks/bulk-selection-actions-bar.json +162 -0
- package/corpus/chunks/business-address-form.json +953 -0
- package/corpus/chunks/calendar-month-view.json +74 -0
- package/corpus/chunks/card-grid-images.json +459 -0
- package/corpus/chunks/card-header-with-description.json +47 -0
- package/corpus/chunks/card-with-empty-state.json +96 -0
- package/corpus/chunks/card-with-form.json +143 -0
- package/corpus/chunks/card-with-sparkline.json +90 -0
- package/corpus/chunks/card-with-stat-kpi.json +86 -0
- package/corpus/chunks/changelog-feed-basic.json +470 -0
- package/corpus/chunks/chart-in-card-chart-plus-breakdown.json +188 -0
- package/corpus/chunks/chart-in-card-chart-plus-stat.json +89 -0
- package/corpus/chunks/chart-in-card-chart-with-filter-pills.json +124 -0
- package/corpus/chunks/chart-in-card-full-bleed-legend.json +116 -0
- package/corpus/chunks/chart-in-card-full-chart.json +107 -0
- package/corpus/chunks/chart-in-card-sparkline-tile.json +93 -0
- package/corpus/chunks/chart-in-card-stat-with-sparkline.json +73 -0
- package/corpus/chunks/chart-in-card-trend-footer.json +115 -0
- package/corpus/chunks/chart-with-filter-pills.json +169 -0
- package/corpus/chunks/chat-empty-state-hero.json +102 -0
- package/corpus/chunks/chat-message-assistant-byline.json +85 -0
- package/corpus/chunks/chat-message-user-byline.json +84 -0
- package/corpus/chunks/chat-page-shell.json +379 -0
- package/corpus/chunks/chat-streaming-surface.json +169 -0
- package/corpus/chunks/checkout-order-summary-card.json +140 -0
- package/corpus/chunks/code-snippet.json +107 -0
- package/corpus/chunks/col-form-stack.json +93 -0
- package/corpus/chunks/col-heading-body-action.json +79 -0
- package/corpus/chunks/col-stat-label.json +63 -0
- package/corpus/chunks/color-picker-swatches.json +153 -0
- package/corpus/chunks/command-palette.json +327 -0
- package/corpus/chunks/comments-composer.json +158 -0
- package/corpus/chunks/comments-mention-menu.json +237 -0
- package/corpus/chunks/comments-thread.json +344 -0
- package/corpus/chunks/commerce-pricing-tiers.json +401 -0
- package/corpus/chunks/comparison-table.json +246 -0
- package/corpus/chunks/contact-form-card.json +197 -0
- package/corpus/chunks/contact-form.json +157 -0
- package/corpus/chunks/contact-support-form.json +702 -0
- package/corpus/chunks/conversion-funnel-4-step-funnel.json +139 -0
- package/corpus/chunks/conversion-funnel-6step.json +151 -0
- package/corpus/chunks/conversion-funnel-wide-summary.json +192 -0
- package/corpus/chunks/conversion-funnel-with-segment-compare.json +202 -0
- package/corpus/chunks/cta-banner-centered.json +102 -0
- package/corpus/chunks/dashboard-acquisition-panel.json +212 -0
- package/corpus/chunks/dashboard-admin-page.json +104 -0
- package/corpus/chunks/dashboard-audience-kpis.json +165 -0
- package/corpus/chunks/dashboard-audience-panel.json +756 -0
- package/corpus/chunks/dashboard-behavior-panel.json +114 -0
- package/corpus/chunks/dashboard-chart-recent.json +566 -0
- package/corpus/chunks/dashboard-cohort-retention.json +83 -0
- package/corpus/chunks/dashboard-conversion-panel.json +234 -0
- package/corpus/chunks/dashboard-country-list.json +304 -0
- package/corpus/chunks/dashboard-filter-bar.json +118 -0
- package/corpus/chunks/dashboard-funnel.json +142 -0
- package/corpus/chunks/dashboard-notifications-panel.json +423 -0
- package/corpus/chunks/dashboard-overview-panel.json +1644 -0
- package/corpus/chunks/dashboard-page-header.json +166 -0
- package/corpus/chunks/dashboard-pages-table.json +86 -0
- package/corpus/chunks/dashboard-quick-actions.json +116 -0
- package/corpus/chunks/dashboard-reports-panel.json +166 -0
- package/corpus/chunks/dashboard-reports-table.json +138 -0
- package/corpus/chunks/dashboard-storage-card.json +117 -0
- package/corpus/chunks/dashboard-tabs.json +86 -0
- package/corpus/chunks/dashboard-team-actions-storage.json +605 -0
- package/corpus/chunks/dashboard-team-list.json +334 -0
- package/corpus/chunks/dashboard-traffic-channels.json +184 -0
- package/corpus/chunks/dashboard-transactions-table.json +145 -0
- package/corpus/chunks/data-export-history.json +192 -0
- package/corpus/chunks/data-export-request.json +197 -0
- package/corpus/chunks/data-import-wizard.json +391 -0
- package/corpus/chunks/date-range-picker.json +156 -0
- package/corpus/chunks/date-time-picker-form.json +222 -0
- package/corpus/chunks/departments-checkbox-form.json +390 -0
- package/corpus/chunks/destructive-confirm-modal.json +149 -0
- package/corpus/chunks/diff-inline.json +84 -0
- package/corpus/chunks/diff-side-by-side.json +131 -0
- package/corpus/chunks/diff-structured.json +216 -0
- package/corpus/chunks/digest-email-preview.json +374 -0
- package/corpus/chunks/divider-text-label.json +87 -0
- package/corpus/chunks/doc-editor-shell.json +356 -0
- package/corpus/chunks/drawer-2fa-key.json +273 -0
- package/corpus/chunks/drawer-2fa-sms.json +261 -0
- package/corpus/chunks/drawer-2fa-totp.json +385 -0
- package/corpus/chunks/drawer-cancel-sub.json +636 -0
- package/corpus/chunks/drawer-change-plan.json +561 -0
- package/corpus/chunks/drawer-custom-roles.json +696 -0
- package/corpus/chunks/drawer-data-start.json +171 -0
- package/corpus/chunks/drawer-delete-account.json +341 -0
- package/corpus/chunks/drawer-delete-workspace.json +362 -0
- package/corpus/chunks/drawer-discord.json +434 -0
- package/corpus/chunks/drawer-figma.json +401 -0
- package/corpus/chunks/drawer-first-dashboard.json +220 -0
- package/corpus/chunks/drawer-gcal.json +475 -0
- package/corpus/chunks/drawer-github.json +589 -0
- package/corpus/chunks/drawer-invite.json +397 -0
- package/corpus/chunks/drawer-record-detail.json +44 -0
- package/corpus/chunks/drawer-report.json +528 -0
- package/corpus/chunks/drawer-revoke-session.json +331 -0
- package/corpus/chunks/drawer-role.json +190 -0
- package/corpus/chunks/drawer-slack.json +429 -0
- package/corpus/chunks/drawer-smtp.json +476 -0
- package/corpus/chunks/drawer-source.json +294 -0
- package/corpus/chunks/drawer-transaction.json +493 -0
- package/corpus/chunks/drawer-with-create-form.json +43 -0
- package/corpus/chunks/editor-code-pane.json +204 -0
- package/corpus/chunks/editor-page-shell.json +149 -0
- package/corpus/chunks/editor-preview-pane.json +136 -0
- package/corpus/chunks/editor-statusbar-ready.json +99 -0
- package/corpus/chunks/editor-toolbar-import-export.json +195 -0
- package/corpus/chunks/email-inbox-list.json +678 -0
- package/corpus/chunks/empty-state-danger.json +58 -0
- package/corpus/chunks/empty-state-minimal.json +44 -0
- package/corpus/chunks/empty-state-warning.json +53 -0
- package/corpus/chunks/empty-state.json +65 -0
- package/corpus/chunks/error-404-actions.json +54 -0
- package/corpus/chunks/error-404.json +204 -0
- package/corpus/chunks/error-500-actions.json +61 -0
- package/corpus/chunks/error-500.json +220 -0
- package/corpus/chunks/error-maintenance-actions.json +53 -0
- package/corpus/chunks/error-maintenance.json +203 -0
- package/corpus/chunks/error-page-shell.json +132 -0
- package/corpus/chunks/export-flow-async.json +210 -0
- package/corpus/chunks/export-flow-delivery.json +163 -0
- package/corpus/chunks/export-flow-sync.json +162 -0
- package/corpus/chunks/extended-profile-form.json +512 -0
- package/corpus/chunks/faq-accordion.json +123 -0
- package/corpus/chunks/feature-grid-icons.json +314 -0
- package/corpus/chunks/feature-grid-three-cards.json +187 -0
- package/corpus/chunks/feature-toggle-switch-grid.json +213 -0
- package/corpus/chunks/file-upload-dnd.json +166 -0
- package/corpus/chunks/file-upload-field.json +97 -0
- package/corpus/chunks/file-upload-form.json +251 -0
- package/corpus/chunks/filter-bar-active-filters.json +176 -0
- package/corpus/chunks/filter-bar-filter-bar-with-table.json +113 -0
- package/corpus/chunks/filter-bar-saved-views.json +117 -0
- package/corpus/chunks/filter-popover-form.json +137 -0
- package/corpus/chunks/footer-multi-column.json +270 -0
- package/corpus/chunks/footer-primary-only.json +41 -0
- package/corpus/chunks/form-checkbox-group.json +65 -0
- package/corpus/chunks/form-drawer-edit-form.json +178 -0
- package/corpus/chunks/form-drawer-simple-form.json +151 -0
- package/corpus/chunks/form-drawer-tall-form.json +325 -0
- package/corpus/chunks/form-field-email-hint.json +53 -0
- package/corpus/chunks/form-field-range.json +54 -0
- package/corpus/chunks/form-field-required.json +52 -0
- package/corpus/chunks/form-field-select.json +66 -0
- package/corpus/chunks/form-field-textarea.json +51 -0
- package/corpus/chunks/form-field-with-error.json +151 -0
- package/corpus/chunks/form-fields-two-column.json +69 -0
- package/corpus/chunks/form-page-shell.json +146 -0
- package/corpus/chunks/form-switch-toggle.json +41 -0
- package/corpus/chunks/form-system-live-demo.json +123 -0
- package/corpus/chunks/forms-address-autocomplete.json +277 -0
- package/corpus/chunks/forms-address-fields.json +221 -0
- package/corpus/chunks/forms-conditional-reveal.json +195 -0
- package/corpus/chunks/gallery-page-shell.json +444 -0
- package/corpus/chunks/grid-2col-entry-cards.json +140 -0
- package/corpus/chunks/grid-kpi-4col.json +164 -0
- package/corpus/chunks/grid-responsive-kpi.json +165 -0
- package/corpus/chunks/hero-cta-simple.json +107 -0
- package/corpus/chunks/hero-cta-split.json +183 -0
- package/corpus/chunks/hero-section-split.json +111 -0
- package/corpus/chunks/icon-text-row.json +60 -0
- package/corpus/chunks/image-caption-tile.json +99 -0
- package/corpus/chunks/image-carousel.json +156 -0
- package/corpus/chunks/image-thumbnail-grid.json +77 -0
- package/corpus/chunks/image-upload-preview.json +118 -0
- package/corpus/chunks/import-mapping-flow.json +972 -0
- package/corpus/chunks/inline-dialog-rename.json +128 -0
- package/corpus/chunks/inline-dialog-schedule.json +154 -0
- package/corpus/chunks/inline-dialog-status.json +179 -0
- package/corpus/chunks/integrations-api-keys-card-list.json +475 -0
- package/corpus/chunks/integrations-connect-form.json +550 -0
- package/corpus/chunks/integrations-grid.json +141 -0
- package/corpus/chunks/integrations-webhooks-empty-state.json +66 -0
- package/corpus/chunks/inventory-list-stock.json +137 -0
- package/corpus/chunks/invite-members.json +153 -0
- package/corpus/chunks/invoice-line-items.json +493 -0
- package/corpus/chunks/kanban-board-3col.json +713 -0
- package/corpus/chunks/kanban-page-shell.json +671 -0
- package/corpus/chunks/key-value-detail-list.json +242 -0
- package/corpus/chunks/keyboard-shortcut-display.json +250 -0
- package/corpus/chunks/labeled-textarea.json +45 -0
- package/corpus/chunks/leaderboard-table.json +140 -0
- package/corpus/chunks/library-browser-search-filter.json +109 -0
- package/corpus/chunks/linked-record-row.json +61 -0
- package/corpus/chunks/loading-skeleton-placeholder.json +387 -0
- package/corpus/chunks/login-form-card.json +207 -0
- package/corpus/chunks/logo-cloud-trusted-by.json +235 -0
- package/corpus/chunks/map-with-location-list.json +293 -0
- package/corpus/chunks/marketing-hero-cta.json +102 -0
- package/corpus/chunks/marketing-page-shell.json +352 -0
- package/corpus/chunks/masonry-gallery.json +154 -0
- package/corpus/chunks/member-edit-drawer.json +236 -0
- package/corpus/chunks/members-active-table-search-filter.json +184 -0
- package/corpus/chunks/members-pending-invites-cards.json +251 -0
- package/corpus/chunks/members-roles-permissions-list.json +146 -0
- package/corpus/chunks/menu-context-actions.json +113 -0
- package/corpus/chunks/metadata-description-list.json +178 -0
- package/corpus/chunks/modal-create-project-form.json +133 -0
- package/corpus/chunks/modal-preferences-tabs.json +147 -0
- package/corpus/chunks/multi-field-form-validation.json +394 -0
- package/corpus/chunks/nav-menu-icons.json +74 -0
- package/corpus/chunks/newsletter-subscription.json +190 -0
- package/corpus/chunks/notification-badges-feed.json +411 -0
- package/corpus/chunks/notification-preferences-panel.json +238 -0
- package/corpus/chunks/onb-completion.json +139 -0
- package/corpus/chunks/onb-extension-install.json +172 -0
- package/corpus/chunks/onb-hero-welcome.json +156 -0
- package/corpus/chunks/onb-import-picker.json +86 -0
- package/corpus/chunks/onb-mobile-handoff.json +121 -0
- package/corpus/chunks/onb-notification-prefs.json +83 -0
- package/corpus/chunks/onb-persona-picker.json +82 -0
- package/corpus/chunks/onb-provider-tiles.json +137 -0
- package/corpus/chunks/onb-settings-review.json +257 -0
- package/corpus/chunks/onb-step-footer.json +121 -0
- package/corpus/chunks/onb-step-header.json +121 -0
- package/corpus/chunks/onb-step-progress.json +140 -0
- package/corpus/chunks/onb-step-shell.json +380 -0
- package/corpus/chunks/onb-story-pane.json +121 -0
- package/corpus/chunks/onb-tutorial-steps.json +193 -0
- package/corpus/chunks/order-confirmation-success-card.json +177 -0
- package/corpus/chunks/order-tracking-timeline.json +131 -0
- package/corpus/chunks/pagination-controls.json +82 -0
- package/corpus/chunks/pane-settings-sidebar.json +204 -0
- package/corpus/chunks/patient-visit-shell.json +191 -0
- package/corpus/chunks/payment-checkout-form.json +341 -0
- package/corpus/chunks/payment-method-form.json +328 -0
- package/corpus/chunks/period-comparison.json +236 -0
- package/corpus/chunks/permission-matrix-table.json +438 -0
- package/corpus/chunks/permissions-matrix-grid.json +111 -0
- package/corpus/chunks/permissions-matrix-summary.json +157 -0
- package/corpus/chunks/permissions-role-picker-radio.json +107 -0
- package/corpus/chunks/permissions-role-picker-select.json +101 -0
- package/corpus/chunks/permissions-share-link.json +256 -0
- package/corpus/chunks/permissions-share-people.json +297 -0
- package/corpus/chunks/permissions-sharing-scopes.json +109 -0
- package/corpus/chunks/plan-selection-form.json +529 -0
- package/corpus/chunks/playground-a2ui.json +181 -0
- package/corpus/chunks/playground-agent-admin.json +117 -0
- package/corpus/chunks/playground-agent-chat.json +126 -0
- package/corpus/chunks/playground-app-shell.json +488 -0
- package/corpus/chunks/playground-chat.json +265 -0
- package/corpus/chunks/playground-construct-canvas.json +118 -0
- package/corpus/chunks/playground-css-channel.json +487 -0
- package/corpus/chunks/playground-gen-ui.json +224 -0
- package/corpus/chunks/playground-render-preview.json +369 -0
- package/corpus/chunks/playground-streams-bridge.json +437 -0
- package/corpus/chunks/playground-table-toolbar.json +38 -0
- package/corpus/chunks/popover-with-content.json +118 -0
- package/corpus/chunks/pricing-tiers.json +561 -0
- package/corpus/chunks/product-tour.json +158 -0
- package/corpus/chunks/profile-public.json +297 -0
- package/corpus/chunks/progress-tracker-milestones.json +294 -0
- package/corpus/chunks/radio-button-group.json +83 -0
- package/corpus/chunks/rating-stars-review-summary.json +259 -0
- package/corpus/chunks/reactions-bar.json +75 -0
- package/corpus/chunks/reactions-picker.json +245 -0
- package/corpus/chunks/real-time-metrics-dashboard.json +344 -0
- package/corpus/chunks/record-detail-drawer-simple-detail.json +296 -0
- package/corpus/chunks/record-detail-drawer-tabbed-detail.json +443 -0
- package/corpus/chunks/referral-share-link.json +128 -0
- package/corpus/chunks/referral-status.json +243 -0
- package/corpus/chunks/reg-address-form.json +825 -0
- package/corpus/chunks/reg-billing-card.json +210 -0
- package/corpus/chunks/reg-brand-scrape.json +599 -0
- package/corpus/chunks/reg-departments-toggle.json +259 -0
- package/corpus/chunks/reg-extended-profile.json +356 -0
- package/corpus/chunks/reg-final-done.json +457 -0
- package/corpus/chunks/reg-helpdesk-grid.json +571 -0
- package/corpus/chunks/reg-import-picker.json +112 -0
- package/corpus/chunks/reg-integrations-grid.json +418 -0
- package/corpus/chunks/reg-invite-form.json +245 -0
- package/corpus/chunks/reg-legal-entity.json +260 -0
- package/corpus/chunks/reg-org-chart-review.json +891 -0
- package/corpus/chunks/reg-profile-identity.json +188 -0
- package/corpus/chunks/reg-step-footer.json +201 -0
- package/corpus/chunks/reg-step-header.json +201 -0
- package/corpus/chunks/reg-step-progress.json +236 -0
- package/corpus/chunks/reg-step-shell.json +605 -0
- package/corpus/chunks/reg-story-pane.json +201 -0
- package/corpus/chunks/reg-success-summary.json +257 -0
- package/corpus/chunks/reg-team-size.json +128 -0
- package/corpus/chunks/reg-usecase-picker.json +104 -0
- package/corpus/chunks/reg-workspace-name.json +159 -0
- package/corpus/chunks/responsive-grid-pattern.json +71 -0
- package/corpus/chunks/responsive-kpi-grid.json +166 -0
- package/corpus/chunks/responsive-row-pattern.json +71 -0
- package/corpus/chunks/responsive-shell-sidebar.json +308 -0
- package/corpus/chunks/responsive-wrap-at-pattern.json +124 -0
- package/corpus/chunks/retention-cohort-default-retention.json +80 -0
- package/corpus/chunks/retention-cohort-stat-pair.json +163 -0
- package/corpus/chunks/retention-cohort-with-legend.json +135 -0
- package/corpus/chunks/revenue-heatmap.json +82 -0
- package/corpus/chunks/role-management.json +272 -0
- package/corpus/chunks/row-action-cluster.json +54 -0
- package/corpus/chunks/row-avatar-name-role.json +71 -0
- package/corpus/chunks/row-icon-label.json +52 -0
- package/corpus/chunks/row-spread-label-value.json +54 -0
- package/corpus/chunks/row-wrap-tags.json +72 -0
- package/corpus/chunks/schedule-meeting-form-card.json +229 -0
- package/corpus/chunks/search-recents-menu.json +195 -0
- package/corpus/chunks/search-results-grouped.json +357 -0
- package/corpus/chunks/search-results-list.json +202 -0
- package/corpus/chunks/search-saved-views.json +356 -0
- package/corpus/chunks/search-with-filters.json +311 -0
- package/corpus/chunks/searchable-picker-modal.json +164 -0
- package/corpus/chunks/section-with-stack.json +48 -0
- package/corpus/chunks/security-active-sessions-devices.json +287 -0
- package/corpus/chunks/security-change-password-form.json +102 -0
- package/corpus/chunks/security-delete-account-danger.json +79 -0
- package/corpus/chunks/segmented-control-billing-tabs.json +164 -0
- package/corpus/chunks/select-multiple-preselected.json +70 -0
- package/corpus/chunks/settings-2fa-setup.json +307 -0
- package/corpus/chunks/settings-admin-page.json +1205 -0
- package/corpus/chunks/settings-appearance-primitives.json +145 -0
- package/corpus/chunks/settings-appearance-theme-panel.json +55 -0
- package/corpus/chunks/settings-appearance.json +81 -0
- package/corpus/chunks/settings-danger-zone-delete-workspace.json +93 -0
- package/corpus/chunks/settings-general-form.json +69 -0
- package/corpus/chunks/settings-integrations.json +298 -0
- package/corpus/chunks/settings-members-invite.json +112 -0
- package/corpus/chunks/settings-notification-activity-checkboxes.json +108 -0
- package/corpus/chunks/settings-notification-channels-switches.json +101 -0
- package/corpus/chunks/settings-page-shell.json +190 -0
- package/corpus/chunks/settings-privacy.json +83 -0
- package/corpus/chunks/settings-profile-security.json +122 -0
- package/corpus/chunks/settings-toggle-switches.json +66 -0
- package/corpus/chunks/sidebar-nav.json +120 -0
- package/corpus/chunks/sidebar-navigation-panel.json +165 -0
- package/corpus/chunks/signup-form-card.json +218 -0
- package/corpus/chunks/signup-team-size-form.json +259 -0
- package/corpus/chunks/single-select-card-form.json +231 -0
- package/corpus/chunks/slider-range-controls.json +119 -0
- package/corpus/chunks/social-media-post-card.json +159 -0
- package/corpus/chunks/sortable-list.json +307 -0
- package/corpus/chunks/sortable-search-paginate-table.json +162 -0
- package/corpus/chunks/split-pane-editor.json +146 -0
- package/corpus/chunks/stack-avatar-badge-overlay.json +53 -0
- package/corpus/chunks/stack-image-shimmer.json +51 -0
- package/corpus/chunks/stat-cards-kpi-dashboard.json +178 -0
- package/corpus/chunks/stats-band-big-numbers.json +175 -0
- package/corpus/chunks/status-dashboard.json +282 -0
- package/corpus/chunks/stepper-form-wizard.json +165 -0
- package/corpus/chunks/subscription-status-renewal-card.json +285 -0
- package/corpus/chunks/survey-rating-form.json +165 -0
- package/corpus/chunks/swiper-product-carousel.json +349 -0
- package/corpus/chunks/swiper-testimonial-carousel.json +279 -0
- package/corpus/chunks/table-empty-state.json +116 -0
- package/corpus/chunks/table-inline-edit-row.json +269 -0
- package/corpus/chunks/table-spreadsheet-cells.json +334 -0
- package/corpus/chunks/table-toolbar-no-search.json +42 -0
- package/corpus/chunks/table-toolbar-with-table.json +208 -0
- package/corpus/chunks/table-with-toolbar-card.json +80 -0
- package/corpus/chunks/tabs-with-panels.json +187 -0
- package/corpus/chunks/tag-chips-filter.json +100 -0
- package/corpus/chunks/team-invite-form.json +380 -0
- package/corpus/chunks/team-members-list.json +341 -0
- package/corpus/chunks/testimonial-grid.json +269 -0
- package/corpus/chunks/testimonial-pull-quote.json +137 -0
- package/corpus/chunks/testimonials-avatar-cards.json +286 -0
- package/corpus/chunks/timeline-events.json +89 -0
- package/corpus/chunks/timezone-selector-basic.json +99 -0
- package/corpus/chunks/timezone-selector-preview.json +104 -0
- package/corpus/chunks/toolbar-icons.json +216 -0
- package/corpus/chunks/toolbar-rich-text-editor.json +230 -0
- package/corpus/chunks/toolbar-tooltips.json +191 -0
- package/corpus/chunks/tooltip-hover-button.json +106 -0
- package/corpus/chunks/tree-file-explorer.json +136 -0
- package/corpus/chunks/tree-table-basic.json +171 -0
- package/corpus/chunks/trial-banner.json +158 -0
- package/corpus/chunks/url-import-form.json +728 -0
- package/corpus/chunks/usage-quota-meter-card.json +217 -0
- package/corpus/chunks/user-identity-row.json +81 -0
- package/corpus/chunks/user-onboarding-checklist.json +301 -0
- package/corpus/chunks/user-profile-card.json +192 -0
- package/corpus/chunks/users-table-badge.json +135 -0
- package/corpus/chunks/verification-document-upload.json +179 -0
- package/corpus/chunks/verification-status.json +97 -0
- package/corpus/chunks/verification-stepper.json +111 -0
- package/corpus/chunks/video-player-controls.json +120 -0
- package/corpus/chunks/webhook-config.json +209 -0
- package/corpus/chunks/webhooks-list.json +204 -0
- package/corpus/chunks/wizard-drawer-three-step-wizard.json +443 -0
- package/corpus/manifest.d.ts +4 -0
- package/corpus/manifest.json +6 -0
- package/corpus/scripts/chunk-library.d.ts +4 -0
- package/corpus/scripts/chunk-library.js +335 -0
- package/node-shims.d.ts +48 -0
- package/package.json +215 -0
- package/retrieval/CHANGELOG.md +1073 -0
- package/retrieval/README.md +71 -0
- package/retrieval/anti-patterns.js +189 -0
- package/retrieval/authoring/index.d.ts +4 -0
- package/retrieval/authoring/index.js +9 -0
- package/retrieval/authoring/web-research.js +220 -0
- package/retrieval/catalog.js +250 -0
- package/retrieval/component-catalog.js +168 -0
- package/retrieval/component-entry.js +80 -0
- package/retrieval/context-assembler.js +174 -0
- package/retrieval/domain-router.js +417 -0
- package/retrieval/embedding/chunk-embedding-retriever.js +117 -0
- package/retrieval/embedding/embedding-provider.js +72 -0
- package/retrieval/embedding/index.d.ts +4 -0
- package/retrieval/embedding/index.js +2 -0
- package/retrieval/feedback/dialog-recorder.js +107 -0
- package/retrieval/feedback/feedback-analyzer.js +180 -0
- package/retrieval/feedback/feedback-store.js +194 -0
- package/retrieval/feedback/feedback.js +117 -0
- package/retrieval/feedback/gap-registry.js +80 -0
- package/retrieval/feedback/human-signal.js +113 -0
- package/retrieval/feedback/index.d.ts +4 -0
- package/retrieval/feedback/index.js +24 -0
- package/retrieval/feedback/submit-feedback.js +45 -0
- package/retrieval/index.d.ts +4 -0
- package/retrieval/index.js +53 -0
- package/retrieval/intent/clarity.js +139 -0
- package/retrieval/intent/decomposer.js +124 -0
- package/retrieval/intent/index.d.ts +4 -0
- package/retrieval/intent/index.js +18 -0
- package/retrieval/intent/intent-alignment.js +172 -0
- package/retrieval/intent/intent-categorizer.js +69 -0
- package/retrieval/intent/intent-gate.js +96 -0
- package/retrieval/intent/prompt-analyzer.js +162 -0
- package/retrieval/wiring-catalog.js +168 -0
- package/validate/catalog-validator.d.ts +4 -0
- package/validate/catalog-validator.js +327 -0
- package/validate/semantic/cache.js +54 -0
- package/validate/semantic/classify-intent.js +128 -0
- package/validate/semantic/index.js +163 -0
- package/validate/semantic/intent-specs.js +162 -0
- package/validate/semantic/judge.js +180 -0
|
@@ -0,0 +1,1619 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @adia-ai/gen-ui — monolithic engine, shared helpers.
|
|
3
|
+
*
|
|
4
|
+
* Pure helpers extracted from core/generator.js per spec §11 Phase 2:
|
|
5
|
+
* prompt builders, JSON parsers, canvas diff utilities, suggestion
|
|
6
|
+
* generators. All stateless (the only module-level state is the lazy
|
|
7
|
+
* component-catalog cache inside getComponentCatalog).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { listPatterns } from '../../core/reference.js';
|
|
11
|
+
import { resolveTag } from '@adia-ai/a2ui/registry';
|
|
12
|
+
import { checkAllAntiPatterns } from '../../../retrieval/anti-patterns.js';
|
|
13
|
+
import { store } from '../../core/state.js';
|
|
14
|
+
import { checkIntentAlignment } from '../../../retrieval/intent/intent-alignment.js';
|
|
15
|
+
import { composeSubtasks } from '../../../retrieval/intent/decomposer.js';
|
|
16
|
+
import { getWiringCatalog } from '../../../retrieval/wiring-catalog.js';
|
|
17
|
+
import { getComponentData } from '../../../retrieval/component-catalog.js';
|
|
18
|
+
|
|
19
|
+
// ── v0.9 .a2ui.json sidecar → prompt-catalog adapter ─────────────
|
|
20
|
+
//
|
|
21
|
+
// §66 (v0.4.6): the `.a2ui.json` sidecars produced by `npm run components`
|
|
22
|
+
// are v0.9 JSON Schemas with `{title, description, properties, x-adiaui}`.
|
|
23
|
+
// The legacy `_components.json` catalog this prompt builder originally
|
|
24
|
+
// read had a flatter `{tag, category, description, props, events, slots,
|
|
25
|
+
// examples, aliases, keywords}` shape. This adapter normalizes the v0.9
|
|
26
|
+
// sidecar into the legacy shape so the rest of the prompt builder can stay
|
|
27
|
+
// unchanged.
|
|
28
|
+
//
|
|
29
|
+
// Before §66, this code did `a2uiData.props` — which silently returned 0
|
|
30
|
+
// props for every component (the v0.9 shape uses `properties`, not
|
|
31
|
+
// `props`), causing every prompt to fall through to the `_components.json`
|
|
32
|
+
// catalog. After §66, the canonical sidecar drives the prompt directly
|
|
33
|
+
// and `_components.json` is retired (§64/§65).
|
|
34
|
+
function adaptV09Component(a2uiData) {
|
|
35
|
+
if (!a2uiData || typeof a2uiData !== 'object') return null;
|
|
36
|
+
const ext = a2uiData['x-adiaui'] || {};
|
|
37
|
+
const properties = a2uiData.properties || {};
|
|
38
|
+
|
|
39
|
+
// Map v0.9 property → legacy prop shape. Skip the always-present
|
|
40
|
+
// `component` (const PascalCase name) + `children` (slot container)
|
|
41
|
+
// fields since they aren't user-facing props.
|
|
42
|
+
const props = {};
|
|
43
|
+
for (const [name, spec] of Object.entries(properties)) {
|
|
44
|
+
if (name === 'component' || name === 'children') continue;
|
|
45
|
+
if (spec?.const) continue; // discriminator-only fields
|
|
46
|
+
const out = {};
|
|
47
|
+
// JSON Schema type names are lowercase; legacy catalog used Title-Case.
|
|
48
|
+
if (spec.type) {
|
|
49
|
+
out.type = spec.type === 'string' ? 'String'
|
|
50
|
+
: spec.type === 'boolean' ? 'Boolean'
|
|
51
|
+
: spec.type === 'number' || spec.type === 'integer' ? 'Number'
|
|
52
|
+
: spec.type === 'array' ? 'Array'
|
|
53
|
+
: spec.type === 'object' ? 'Object'
|
|
54
|
+
: String(spec.type);
|
|
55
|
+
}
|
|
56
|
+
if (spec.enum) out.enum = spec.enum;
|
|
57
|
+
if (spec.default !== undefined) out.default = spec.default;
|
|
58
|
+
if (spec.description) out.description = spec.description;
|
|
59
|
+
props[name] = out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Events in the legacy catalog were a flat string[]. The v0.9 sidecar
|
|
63
|
+
// stores them as an object: `{eventName: {description}}` (verified
|
|
64
|
+
// across all 94 sidecars on 2026-05-12). Older code paths accepted
|
|
65
|
+
// either shape; normalize to a string[] of event names.
|
|
66
|
+
const eventsRaw = ext.events;
|
|
67
|
+
let events = [];
|
|
68
|
+
if (Array.isArray(eventsRaw)) {
|
|
69
|
+
events = eventsRaw.map((e) => (typeof e === 'string' ? e : e?.name)).filter(Boolean);
|
|
70
|
+
} else if (eventsRaw && typeof eventsRaw === 'object') {
|
|
71
|
+
events = Object.keys(eventsRaw);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Slots — also object-shaped (`{slotName: {description, type}}`) in v0.9.
|
|
75
|
+
// Legacy catalog stored a string[]. Normalize.
|
|
76
|
+
const slotsRaw = ext.slots;
|
|
77
|
+
let slots = [];
|
|
78
|
+
if (Array.isArray(slotsRaw)) {
|
|
79
|
+
slots = slotsRaw.map((s) => (typeof s === 'string' ? s : s?.name)).filter(Boolean);
|
|
80
|
+
} else if (slotsRaw && typeof slotsRaw === 'object') {
|
|
81
|
+
slots = Object.keys(slotsRaw);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Aliases live under `x-adiaui.synonyms.tags` in v0.9. The synonyms
|
|
85
|
+
// field is always an object (verified across all 94 sidecars). Surface
|
|
86
|
+
// the `tags` array — the prompt renders them as "Button (also: SubmitButton)".
|
|
87
|
+
const synonyms = (ext.synonyms && typeof ext.synonyms === 'object') ? ext.synonyms : null;
|
|
88
|
+
const aliases = Array.isArray(synonyms?.tags) ? synonyms.tags : null;
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
tag: ext.tag || null,
|
|
92
|
+
category: ext.category || 'layout',
|
|
93
|
+
description: a2uiData.description || '',
|
|
94
|
+
props,
|
|
95
|
+
events,
|
|
96
|
+
slots,
|
|
97
|
+
examples: Array.isArray(ext.examples) ? ext.examples : [],
|
|
98
|
+
aliases,
|
|
99
|
+
keywords: Array.isArray(ext.keywords) ? ext.keywords : [],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Component prop catalog — loaded lazily for prompt injection.
|
|
104
|
+
//
|
|
105
|
+
// Since §65 (v0.4.7), reads from the canonical v0.9 catalog at
|
|
106
|
+
// `corpus/catalog-a2ui_0_9.json` (assembled from yamls by `npm run
|
|
107
|
+
// components`). Previously read the hand-maintained
|
|
108
|
+
// `corpus/patterns/_components.json` which was retired in §65.
|
|
109
|
+
// Aliases come from `x-adiaui.synonyms.tags` per the v0.9 sidecar shape;
|
|
110
|
+
// migrated in §65 step 1 — 17 yamls populated with synonyms.tags from
|
|
111
|
+
// the prior `_components.json` aliases field.
|
|
112
|
+
let _componentCatalog = null;
|
|
113
|
+
async function getComponentCatalog() {
|
|
114
|
+
if (_componentCatalog) return _componentCatalog;
|
|
115
|
+
try {
|
|
116
|
+
const IS_NODE = typeof process !== 'undefined' && process.versions?.node;
|
|
117
|
+
let catalogJson;
|
|
118
|
+
if (IS_NODE) {
|
|
119
|
+
const fs = await import(/* @vite-ignore */ 'node:fs/promises');
|
|
120
|
+
const path = await import(/* @vite-ignore */ 'node:path');
|
|
121
|
+
const url = await import(/* @vite-ignore */ 'node:url');
|
|
122
|
+
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
123
|
+
const raw = await fs.readFile(path.join(__dirname, '../../../corpus/catalog-a2ui_0_9.json'), 'utf8');
|
|
124
|
+
catalogJson = JSON.parse(raw);
|
|
125
|
+
} else {
|
|
126
|
+
const resp = await fetch(new URL('../../../corpus/catalog-a2ui_0_9.json', import.meta.url));
|
|
127
|
+
catalogJson = resp.ok ? await resp.json() : {};
|
|
128
|
+
}
|
|
129
|
+
// Adapt the v0.9 catalog (JSON Schema with `components: {Name: {x-adiaui: {...}}}`)
|
|
130
|
+
// into the legacy {Name: {aliases, ...}} shape getComponentCatalog consumers
|
|
131
|
+
// expect. Aliases live at `x-adiaui.synonyms.tags`.
|
|
132
|
+
const comps = catalogJson?.components || {};
|
|
133
|
+
_componentCatalog = {};
|
|
134
|
+
for (const [name, def] of Object.entries(comps)) {
|
|
135
|
+
const ext = def?.['x-adiaui'] || {};
|
|
136
|
+
const syns = (ext.synonyms && typeof ext.synonyms === 'object') ? ext.synonyms : null;
|
|
137
|
+
_componentCatalog[name] = {
|
|
138
|
+
aliases: Array.isArray(syns?.tags) ? syns.tags : [],
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
} catch {
|
|
142
|
+
_componentCatalog = {};
|
|
143
|
+
}
|
|
144
|
+
return _componentCatalog;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function buildSystemPrompt(context, patterns, researchContext = '', intent = '', composition = null, ontologyContext = null) {
|
|
148
|
+
const parts = [];
|
|
149
|
+
|
|
150
|
+
// ── Role + output format ──
|
|
151
|
+
parts.push(`You are an A2UI component generator for the AdiaUI design system.
|
|
152
|
+
Output ONLY a valid JSON array of A2UI messages. No markdown fences, no explanation.
|
|
153
|
+
|
|
154
|
+
Output format: [{ "type": "updateComponents", "surfaceId": "default", "components": [...] }]
|
|
155
|
+
|
|
156
|
+
Each component: { "id": "<unique>", "component": "<Type>", "children": ["<childId>", ...], ...props }
|
|
157
|
+
The root must have id "root". Use short, descriptive IDs (e.g., "hdr", "email-field", "submit-btn").`);
|
|
158
|
+
|
|
159
|
+
// ── Ontology Context Injection ──
|
|
160
|
+
if (ontologyContext) {
|
|
161
|
+
parts.push(`
|
|
162
|
+
CRITICAL CONSTRAINT: You must strictly adhere to the following Domain and Task models.
|
|
163
|
+
Do not hallucinate features, bounds, or data inputs that violate these structures.
|
|
164
|
+
- Domain Entities: ${(ontologyContext.domain?.entities || []).join(', ') || 'N/A'}
|
|
165
|
+
- Domain Metrics: ${(ontologyContext.domain?.metrics || []).join(', ') || 'N/A'}
|
|
166
|
+
- Primary Tasks: ${(ontologyContext.tasks?.primary || []).join(', ') || 'N/A'}
|
|
167
|
+
- Experience Mode: ${ontologyContext.experience?.mode || 'N/A'}
|
|
168
|
+
- Experience Shell: ${ontologyContext.experience?.shell || 'N/A'}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── Corpus context (§56, v0.4.5) ──
|
|
172
|
+
// Tells the LLM how the system surrounding it is wired so it doesn't
|
|
173
|
+
// hallucinate component names and so it understands what MATCHED PATTERN /
|
|
174
|
+
// STRUCTURAL REFERENCE blocks actually ARE. Keep this section tight — every
|
|
175
|
+
// token costs latency + money.
|
|
176
|
+
parts.push(`CORPUS CONTEXT (the system around you):
|
|
177
|
+
|
|
178
|
+
You are part of a retrieval-augmented pipeline. Before this prompt was built,
|
|
179
|
+
the pipeline searched two corpora for matches to the user's intent:
|
|
180
|
+
|
|
181
|
+
1. PATTERNS / COMPOSITIONS — full-canvas A2UI templates curated for the
|
|
182
|
+
AdiaUI design system. Examples: login-form, pricing-tiers,
|
|
183
|
+
dashboard-admin-page. When a strong match exists, you'll see a
|
|
184
|
+
"MATCHED PATTERN" block below with the canonical template.
|
|
185
|
+
|
|
186
|
+
2. ANNOTATED CHUNKS — real production HTML blocks harvested from the AdiaUI
|
|
187
|
+
apps (auth flows, dashboards, error pages, etc.) with metadata
|
|
188
|
+
(domain, keywords, description). When a strong chunk match exists,
|
|
189
|
+
you'll see a "STRUCTURAL REFERENCE" block with the production HTML.
|
|
190
|
+
Match its component palette + information density, but translate to
|
|
191
|
+
A2UI components — do not copy raw HTML.
|
|
192
|
+
|
|
193
|
+
Both corpora use ONLY components from the AVAILABLE COMPONENTS list further
|
|
194
|
+
down. If you would invent a component name not in that list, you're
|
|
195
|
+
hallucinating — use the closest canonical name instead.`);
|
|
196
|
+
|
|
197
|
+
// ── Card-N content model (critical for quality) ──
|
|
198
|
+
parts.push(`CARD-N CONTENT MODEL (mandatory for any card surface):
|
|
199
|
+
- Card > Header: for title/description. Use Text children with heading variants (h3, h4).
|
|
200
|
+
Card header uses a grid with slots: slot="icon" (leading icon), slot="heading" (title), slot="description" (subtitle), slot="action" (badge/button).
|
|
201
|
+
Card CSS auto-targets native h1-h6 and small/p for heading/description roles.
|
|
202
|
+
- Card > Section: for body content. Always wrap section children in a Column component.
|
|
203
|
+
Section > Column > [content children]
|
|
204
|
+
- Card > Footer: for action buttons. Buttons go here, not in Section.
|
|
205
|
+
|
|
206
|
+
LAYOUT PRIMITIVES:
|
|
207
|
+
- Column (col-ui): vertical stack. Use numeric gap: gap="2", gap="4", gap="6".
|
|
208
|
+
- Row (row-ui): horizontal. Use gap="2", justify="space-between"|"end"|"center".
|
|
209
|
+
- Grid (grid-ui): CSS grid. Use columns="2"|"3"|"4", gap="4".
|
|
210
|
+
|
|
211
|
+
TEXT TYPES (component="Text"):
|
|
212
|
+
- variant="display" / "title" / "heading" / "subsection" / "section" for headings (largest → smallest)
|
|
213
|
+
- variant="body" for paragraphs (default)
|
|
214
|
+
- variant="caption" / "label" / "kicker" / "code" for small / meta text
|
|
215
|
+
- variant="deck" / "metric" for lede / KPI text
|
|
216
|
+
- Use textContent prop for the display text
|
|
217
|
+
|
|
218
|
+
TABS (component="Tabs"):
|
|
219
|
+
- Tabs children MUST be Tab elements — the parent renders the button strip from each Tab's text + optional icon props.
|
|
220
|
+
- Each Tab needs a unique value prop; the Tab's CHILDREN are its panel content, auto-hidden by the parent when inactive.
|
|
221
|
+
- Never nest a Tab inside another Tab, and never place non-Tab children directly inside Tabs.
|
|
222
|
+
- WRONG: Column > [Tabs, Card, Card, Card] (panels as siblings render all visible at once)
|
|
223
|
+
- CORRECT: Tabs > [Tab(value=a) > Card, Tab(value=b) > Card] (each panel inside its Tab)
|
|
224
|
+
|
|
225
|
+
GLOBAL ATTRIBUTES (work on any element — span, p, h1-h6, div, and all *-n components):
|
|
226
|
+
- variant="heading|title|section|label|caption|body|kicker|deck|display|metric|code" — sets typography role
|
|
227
|
+
- weight="thin|light|normal|medium|semibold|bold" — overrides font weight
|
|
228
|
+
- color="text|text-strong|subtle|muted|accent|success|warning|danger|info" — sets text color
|
|
229
|
+
- nomargin — removes block margins
|
|
230
|
+
- truncate — single-line ellipsis
|
|
231
|
+
- grow — flex: 1 (fills remaining space in row/col)
|
|
232
|
+
|
|
233
|
+
PREFER <span> WITH GLOBAL ATTRIBUTES for styled text:
|
|
234
|
+
- CORRECT: { component: "Text", variant: "label", color: "muted", slot: "description", textContent: "Subtitle" }
|
|
235
|
+
- WRONG: wrapping in unnecessary component wrappers
|
|
236
|
+
The renderer maps Text components to <span> elements. Global attributes (variant, weight, color) are set as HTML attributes on the span. This is lighter than using dedicated text components.
|
|
237
|
+
|
|
238
|
+
KEY RULES:
|
|
239
|
+
- IMPORTANT: Generate comprehensive, production-quality UIs. A login form should have 10+ components. A dashboard should have 20+ components. Never return a minimal stub — include all the fields, labels, buttons, and chrome that a real UI would have.
|
|
240
|
+
- Never use raw HTML tags (div, form, input, span). Only A2UI component types.
|
|
241
|
+
- Use Input for text inputs (not "TextField" or "TextInput").
|
|
242
|
+
- Use CheckBox for checkboxes. Use Switch/Toggle for on/off toggles.
|
|
243
|
+
- Use Select for dropdowns (not "ChoicePicker").
|
|
244
|
+
- Use Stat for KPI/metric cards: { component: "Stat", label: "Revenue", value: "$48.2k", change: "+12%", trend: "up" }. Do NOT manually build stat cards with Header+Section+Text — use the Stat component directly.
|
|
245
|
+
- Use Table for tabular data (set data/columns via JS props, not Text children).
|
|
246
|
+
- Use EmptyState for no-content/error states (icon, heading, description, action slot).
|
|
247
|
+
- Use Skeleton for loading placeholders (variant: text|circle|rect).
|
|
248
|
+
- Button: use variant="primary" for the one main CTA that should stand out. Leave variant unset for secondary/default actions — the default (no attribute) already IS the secondary look; do NOT add variant="outline" just to mark something as non-primary. Reserve variant="ghost" for a genuinely lower-emphasis tertiary action.
|
|
249
|
+
- Button: use text prop for button label.
|
|
250
|
+
- NEVER use Text for clickable/interactive actions. "View logs", "Contact support", "Learn more" = Button (variant="ghost" or "link"), NOT Text. Text is for static content only.
|
|
251
|
+
- For error/status dialogs: use Alert for the error message, Button for recovery actions (retry, view logs, contact support). Don't render action labels as Text captions.
|
|
252
|
+
- For forms: wrap fields in Column with gap="3". Put submit button in a separate Row or Footer.
|
|
253
|
+
- Tab children must ONLY be Tab components. Never put Card, Column, or any content inside Tab.
|
|
254
|
+
- Every Header child MUST have a slot: slot="heading" for title, slot="description" for subtitle, slot="action" for badges/buttons, slot="icon" for leading icons.`);
|
|
255
|
+
|
|
256
|
+
// ── Component prop catalog (ground truth from _components.json) ──
|
|
257
|
+
const catalog = await getComponentCatalog();
|
|
258
|
+
if (catalog && Object.keys(catalog).length > 0) {
|
|
259
|
+
// Pick components most relevant to the intent — show top 20 most used
|
|
260
|
+
const relevantTypes = new Set();
|
|
261
|
+
// Always include core types
|
|
262
|
+
for (const t of ['Column', 'Row', 'Grid', 'Card', 'Header', 'Section', 'Footer',
|
|
263
|
+
'Text', 'Button', 'Input', 'Badge', 'Icon', 'Avatar', 'Image', 'Divider']) {
|
|
264
|
+
relevantTypes.add(t);
|
|
265
|
+
}
|
|
266
|
+
// §172 (v0.5.4): intent-keyword inclusion — add component families when
|
|
267
|
+
// the intent mentions their domain. Pre-§172 the LLM had to learn chat
|
|
268
|
+
// components via multi-turn context (unreliable) or pattern-match (which
|
|
269
|
+
// depends on retrieval). The 2026-05-14 redundant-send-button regression
|
|
270
|
+
// happened because the LLM saw `ChatInput` in the runtime registry but
|
|
271
|
+
// had no schema describing what `<chat-input-ui>` stamps. Now the chat
|
|
272
|
+
// family is force-included whenever the intent mentions chat/message/
|
|
273
|
+
// conversation.
|
|
274
|
+
const intentLower = (intent || '').toLowerCase();
|
|
275
|
+
const FAMILIES = {
|
|
276
|
+
chat: { keywords: ['chat', 'message', 'conversation', 'composer', 'thread'],
|
|
277
|
+
types: ['ChatInput', 'ChatThread', 'ChatComposer', 'ChatShell', 'ChatHeader', 'ChatSidebar'] },
|
|
278
|
+
table: { keywords: ['table', 'grid of rows', 'tabular', 'spreadsheet'],
|
|
279
|
+
types: ['Table', 'TableToolbar'] },
|
|
280
|
+
form: { keywords: ['form', 'sign in', 'sign up', 'register', 'login', 'signup'],
|
|
281
|
+
types: ['Field', 'Input', 'CheckBox', 'Switch', 'Select', 'TextArea', 'Slider'] },
|
|
282
|
+
chart: { keywords: ['chart', 'graph', 'metric', 'kpi', 'dashboard', 'trend'],
|
|
283
|
+
types: ['Chart', 'ChartLegend', 'Stat'] },
|
|
284
|
+
navigation: { keywords: ['nav', 'sidebar', 'breadcrumb', 'tabs', 'menu'],
|
|
285
|
+
types: ['Tabs', 'Tab', 'Menu', 'Breadcrumbs', 'NavGroup', 'NavItem'] },
|
|
286
|
+
};
|
|
287
|
+
for (const family of Object.values(FAMILIES)) {
|
|
288
|
+
if (family.keywords.some(kw => intentLower.includes(kw))) {
|
|
289
|
+
for (const t of family.types) relevantTypes.add(t);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// Add types from matched patterns
|
|
293
|
+
for (const p of patterns) {
|
|
294
|
+
for (const c of (p.components || [])) relevantTypes.add(c);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const catalogLines = [];
|
|
298
|
+
// Aliases live on the canonical catalog entry; surface them inline so the
|
|
299
|
+
// LLM knows it can write "Carousel" / "Slideshow" and the runtime registry
|
|
300
|
+
// will map to the right tag (verified 2026-04-19 — without this, the LLM
|
|
301
|
+
// emits Carousel only to have it ignored, swap-fallback breaks).
|
|
302
|
+
const aliasSuffix = (typeName) => {
|
|
303
|
+
const a = catalog[typeName]?.aliases;
|
|
304
|
+
return Array.isArray(a) && a.length ? ` (also: ${a.join(', ')})` : '';
|
|
305
|
+
};
|
|
306
|
+
for (const typeName of relevantTypes) {
|
|
307
|
+
// §66 (v0.4.6): .a2ui.json sidecar is the canonical source of truth.
|
|
308
|
+
// The raw v0.9 shape uses `properties` (JSON Schema); adapt to the
|
|
309
|
+
// legacy `props` shape this prompt builder was originally written
|
|
310
|
+
// for. Falls back to the (soon-retired) `_components.json` only
|
|
311
|
+
// when no sidecar exists.
|
|
312
|
+
const v09Raw = getComponentData(typeName);
|
|
313
|
+
const a2uiData = v09Raw ? adaptV09Component(v09Raw) : null;
|
|
314
|
+
if (a2uiData) {
|
|
315
|
+
const props = a2uiData.props ? Object.entries(a2uiData.props).map(([k, v]) => {
|
|
316
|
+
let desc = k;
|
|
317
|
+
if (v.enum) desc += `=(${v.enum.join('|')})`;
|
|
318
|
+
else if (v.type) desc += `:${v.type}`;
|
|
319
|
+
if (v.default !== undefined) desc += `[${v.default}]`;
|
|
320
|
+
// §167 (v0.5.4): surface yaml-declared deprecation to the LLM. Pre-§167
|
|
321
|
+
// the description field was captured into out.description (line ~56)
|
|
322
|
+
// but never read here, so deprecated props (Avatar.name, Field.persist
|
|
323
|
+
// alias, etc.) looked indistinguishable from canonical props. LLM
|
|
324
|
+
// would pick e.g. <avatar-ui name="..."> instead of text="...".
|
|
325
|
+
// §179 (v0.5.5): principled `deprecated:` field wins; falls back to §167a substring detection for back-compat.
|
|
326
|
+
if (v.deprecated === true || (v.description && /deprecated/i.test(v.description))) desc += ' (deprecated, do not use)';
|
|
327
|
+
return desc;
|
|
328
|
+
}).join(', ') : '';
|
|
329
|
+
const events = a2uiData.events?.length ? ` events: ${a2uiData.events.join(', ')}` : '';
|
|
330
|
+
catalogLines.push(`- ${typeName}${aliasSuffix(typeName)}: ${a2uiData.description || ''} props: ${props || 'none'}${events}`);
|
|
331
|
+
// Include a brief example snippet if the component is relevant. Pick
|
|
332
|
+
// the example whose NAME best matches words in the intent (so a chart
|
|
333
|
+
// prompt gets the chart-legend example, not the first one in the
|
|
334
|
+
// array). Falls back to examples[0] when no name overlap.
|
|
335
|
+
if (a2uiData.examples?.length > 0 && intent) {
|
|
336
|
+
const intentLower = intent.toLowerCase();
|
|
337
|
+
const keywords = a2uiData.keywords || [];
|
|
338
|
+
const isRelevant = keywords.some(kw => intentLower.includes(kw)) ||
|
|
339
|
+
intentLower.includes(typeName.toLowerCase());
|
|
340
|
+
if (isRelevant) {
|
|
341
|
+
const intentTokens = new Set(intentLower.split(/\W+/).filter(w => w.length > 2));
|
|
342
|
+
let bestExample = a2uiData.examples[0];
|
|
343
|
+
let bestScore = 0;
|
|
344
|
+
for (const ex of a2uiData.examples) {
|
|
345
|
+
const nameTokens = String(ex.name || '').toLowerCase().split(/[\s_-]+/);
|
|
346
|
+
const score = nameTokens.filter(t => intentTokens.has(t)).length;
|
|
347
|
+
if (score > bestScore) { bestScore = score; bestExample = ex; }
|
|
348
|
+
}
|
|
349
|
+
if (bestExample?.template) {
|
|
350
|
+
const exSnippet = JSON.stringify(bestExample.template.slice(0, 6));
|
|
351
|
+
catalogLines.push(` example (${bestExample.name || 'default'}): ${exSnippet}`);
|
|
352
|
+
if (bestExample.description) catalogLines.push(` guidance: ${bestExample.description}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
} else {
|
|
357
|
+
const comp = catalog[typeName];
|
|
358
|
+
if (!comp) continue;
|
|
359
|
+
const props = comp.props ? Object.entries(comp.props).map(([k, v]) => {
|
|
360
|
+
let desc = k;
|
|
361
|
+
if (v.enum) desc += `=(${v.enum.join('|')})`;
|
|
362
|
+
else if (v.type) desc += `:${v.type}`;
|
|
363
|
+
if (v.default !== undefined) desc += `[${v.default}]`;
|
|
364
|
+
// §167 (v0.5.4): surface yaml-declared deprecation to the LLM. Pre-§167
|
|
365
|
+
// the description field was captured into out.description (line ~56)
|
|
366
|
+
// but never read here, so deprecated props (Avatar.name, Field.persist
|
|
367
|
+
// alias, etc.) looked indistinguishable from canonical props. LLM
|
|
368
|
+
// would pick e.g. <avatar-ui name="..."> instead of text="...".
|
|
369
|
+
// §179 (v0.5.5): principled `deprecated:` field wins; falls back to §167a substring detection for back-compat.
|
|
370
|
+
if (v.deprecated === true || (v.description && /deprecated/i.test(v.description))) desc += ' (deprecated, do not use)';
|
|
371
|
+
return desc;
|
|
372
|
+
}).join(', ') : '';
|
|
373
|
+
const events = comp.events?.length ? ` events: ${comp.events.join(', ')}` : '';
|
|
374
|
+
catalogLines.push(`- ${typeName}${aliasSuffix(typeName)}: ${comp.description || ''} props: ${props || 'none'}${events}`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
parts.push(`COMPONENT CATALOG (typed props — use these, not guesses):\n${catalogLines.join('\n')}`);
|
|
378
|
+
} else if (context.components.length > 0) {
|
|
379
|
+
// Fallback to old-style catalog if _components.json not available
|
|
380
|
+
const oldCatalog = context.components.map(c => {
|
|
381
|
+
let props = '';
|
|
382
|
+
if (Array.isArray(c.properties)) {
|
|
383
|
+
props = c.properties.map(p => {
|
|
384
|
+
if (typeof p === 'string') return p;
|
|
385
|
+
if (p && typeof p === 'object') return p.name || p.attribute || JSON.stringify(p);
|
|
386
|
+
return String(p);
|
|
387
|
+
}).join(', ');
|
|
388
|
+
}
|
|
389
|
+
return `- ${c.type}: ${props || '(no props)'}`;
|
|
390
|
+
}).join('\n');
|
|
391
|
+
parts.push(`AVAILABLE COMPONENTS:\n${oldCatalog}`);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ── Anti-patterns ──
|
|
395
|
+
if (context.antiPatterns?.length > 0) {
|
|
396
|
+
const rules = context.antiPatterns.map(ap => `- ${ap.description}`).join('\n');
|
|
397
|
+
parts.push(`AVOID:\n${rules}`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ── Matched pattern (use as base, adapt content) ──
|
|
401
|
+
if (patterns.length > 0 && patterns[0].template) {
|
|
402
|
+
const best = patterns[0];
|
|
403
|
+
const full = JSON.stringify(best.template, null, 1);
|
|
404
|
+
parts.push(`MATCHED PATTERN: "${best.name}"
|
|
405
|
+
USE THIS AS YOUR BASE STRUCTURE. Adapt the text content and values to match the user's intent, but KEEP the structural patterns:
|
|
406
|
+
- Keep the same component hierarchy and nesting
|
|
407
|
+
- Keep slot attributes (slot="heading", slot="action", slot="description", slot="icon")
|
|
408
|
+
- Keep variant attributes on cards and buttons
|
|
409
|
+
- Adjust textContent, labels, and counts to match the user's request
|
|
410
|
+
|
|
411
|
+
Template:
|
|
412
|
+
${full}`);
|
|
413
|
+
} else if (patterns.length > 0) {
|
|
414
|
+
// No exact match — show top 3 partial matches so the LLM can compose from parts
|
|
415
|
+
const partials = patterns.slice(0, 3);
|
|
416
|
+
const partialList = partials.map(p => {
|
|
417
|
+
const comps = p.components?.join(', ') || '';
|
|
418
|
+
return `- "${p.name}" (${p.domain}): ${p.description}. Components: [${comps}]`;
|
|
419
|
+
}).join('\n');
|
|
420
|
+
parts.push(`NO EXACT PATTERN MATCH. Here are the closest patterns for reference:
|
|
421
|
+
${partialList}
|
|
422
|
+
|
|
423
|
+
COMPOSE from scratch using the AVAILABLE COMPONENTS list above. Follow these composition rules:
|
|
424
|
+
1. Start with a Card as the root container (or Column/Grid for multi-card layouts)
|
|
425
|
+
2. Card anatomy: Header (slot="heading" + slot="description") → Section > Column → content → Footer (actions)
|
|
426
|
+
3. Pick the most specific component for each UI element — don't default to Card+Text for everything
|
|
427
|
+
4. Use the component catalog to find the right type: Table for data, Timeline for sequences, Accordion for collapsible sections, etc.
|
|
428
|
+
5. Every Header child MUST have a slot attribute: slot="heading", slot="description", slot="action", or slot="icon"
|
|
429
|
+
6. Use Grid for equal-width columns, Row for content-sized horizontal layouts, Column for vertical stacks`);
|
|
430
|
+
} else {
|
|
431
|
+
parts.push(`NO MATCHING PATTERNS. Generate from scratch using the AVAILABLE COMPONENTS list and CARD-N CONTENT MODEL rules above.
|
|
432
|
+
Compose the UI by picking the most specific component types available — don't just use Card+Text for everything.`);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// ── Diverse pattern examples (always included when no exact match) ──
|
|
436
|
+
// Give the LLM structural examples even when keyword search returned nothing.
|
|
437
|
+
// This is the single biggest quality improvement for unmapped intents.
|
|
438
|
+
if (!patterns.length || !patterns[0]?.template) {
|
|
439
|
+
const allPatterns = listPatterns().filter(p => p.template && Array.isArray(p.template) && p.template.length >= 3);
|
|
440
|
+
if (allPatterns.length > 0) {
|
|
441
|
+
// Pick up to 3 diverse examples from different domains
|
|
442
|
+
const seen = new Set();
|
|
443
|
+
const diverse = [];
|
|
444
|
+
for (const p of allPatterns) {
|
|
445
|
+
const d = p.domain || 'general';
|
|
446
|
+
if (!seen.has(d) && diverse.length < 3) {
|
|
447
|
+
seen.add(d);
|
|
448
|
+
diverse.push(p);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
// If we didn't get 3 from unique domains, fill from remaining
|
|
452
|
+
if (diverse.length < 3) {
|
|
453
|
+
for (const p of allPatterns) {
|
|
454
|
+
if (!diverse.includes(p) && diverse.length < 3) diverse.push(p);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
const examples = diverse.map(p => {
|
|
458
|
+
const compact = JSON.stringify(p.template).slice(0, 500);
|
|
459
|
+
return `"${p.name}" (${p.domain || 'general'}): ${compact}${p.template.length > 500 ? '...' : ''}`;
|
|
460
|
+
}).join('\n\n');
|
|
461
|
+
parts.push(`REFERENCE EXAMPLES — diverse patterns from the library to show structural conventions:\n${examples}\n\nUse these as structural guidance, NOT to copy directly. Adapt to the user's intent.`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// ── HTML exemplar (golden output showing AdiaUI markup conventions) ──
|
|
466
|
+
parts.push(`GOLDEN HTML OUTPUT EXAMPLE — this is what correctly rendered AdiaUI markup looks like:
|
|
467
|
+
|
|
468
|
+
<section prose>
|
|
469
|
+
<col-ui gap="12">
|
|
470
|
+
<header align="center" size="lg">
|
|
471
|
+
<col-ui gap="4">
|
|
472
|
+
<span variant="kicker">Category</span>
|
|
473
|
+
<h1 variant="display" nomargin>Page Title</h1>
|
|
474
|
+
<p variant="deck" nomargin>Subtitle description text.</p>
|
|
475
|
+
</col-ui>
|
|
476
|
+
</header>
|
|
477
|
+
<grid-ui columns="3">
|
|
478
|
+
<card-ui>
|
|
479
|
+
<header>
|
|
480
|
+
<icon-ui name="lightning" slot="icon" color="accent"></icon-ui>
|
|
481
|
+
<h3 slot="heading" variant="section" nomargin>Card Title</h3>
|
|
482
|
+
<p slot="description" color="subtle" nomargin>Card subtitle</p>
|
|
483
|
+
</header>
|
|
484
|
+
<section>
|
|
485
|
+
<col-ui gap="2">
|
|
486
|
+
<p variant="body" color="subtle" nomargin>Body text content.</p>
|
|
487
|
+
</col-ui>
|
|
488
|
+
</section>
|
|
489
|
+
<footer>
|
|
490
|
+
<button-ui text="Action" variant="primary" stretch></button-ui>
|
|
491
|
+
</footer>
|
|
492
|
+
</card-ui>
|
|
493
|
+
</grid-ui>
|
|
494
|
+
</col-ui>
|
|
495
|
+
</section>
|
|
496
|
+
|
|
497
|
+
KEY CONVENTIONS from this example:
|
|
498
|
+
- Section content wrapped in col-ui (never bare text in section)
|
|
499
|
+
- Header uses slot="icon", slot="heading", slot="description", slot="action"
|
|
500
|
+
- Typography via variant attribute on semantic elements (h1, h3, p, span)
|
|
501
|
+
- color="subtle" and color="muted" for secondary text
|
|
502
|
+
- nomargin on all typography inside cards
|
|
503
|
+
- Numeric gap values (gap="2", gap="4", gap="12")
|
|
504
|
+
- icon-ui uses Phosphor names with color attribute
|
|
505
|
+
Your A2UI JSON output should produce HTML that follows these conventions.`);
|
|
506
|
+
|
|
507
|
+
// ── Styling & theming guidance ──
|
|
508
|
+
parts.push(`STYLING & THEMING:
|
|
509
|
+
You control visual appearance through component attributes and token overrides — NEVER generate raw CSS.
|
|
510
|
+
|
|
511
|
+
APPEARANCE ATTRIBUTES (set directly on component props):
|
|
512
|
+
- variant: "primary|outline|ghost|danger|link" (buttons), "success|warning|danger|info|primary" (badges; "primary" = brand emphasis), "success|warning|danger|info" (alerts)
|
|
513
|
+
- size: "sm|md|lg" — shifts component sizing + all typography
|
|
514
|
+
- color: "text|text-strong|subtle|muted|accent|success|warning|danger|info" — text/icon color
|
|
515
|
+
- weight: "thin|light|normal|medium|semibold|bold" — font weight
|
|
516
|
+
- density: "compact|spacious" — tightens or loosens spacing on a container
|
|
517
|
+
- radius: "sharp|rounded|round" — border radius presets on a container
|
|
518
|
+
- align: "left|center|right" — text/content alignment
|
|
519
|
+
|
|
520
|
+
THEMING (apply to any container to theme its entire subtree):
|
|
521
|
+
- { component: "Column", "theme": "ocean" } — named theme (ocean, forest, sunset, lavender, rose, slate, midnight)
|
|
522
|
+
- Or use the theme-ui provider: { component: "Theme", preset: "ocean", children: [...] }
|
|
523
|
+
|
|
524
|
+
TOKEN OVERRIDES (use style prop for precise control — sparingly):
|
|
525
|
+
- { component: "Card", style: "--card-bg: var(--md-sys-color-primary-container)" } — override card background
|
|
526
|
+
- { component: "Button", style: "--button-radius: 999px" } — pill-shaped button
|
|
527
|
+
- The renderer sets el.style.cssText for the style prop.
|
|
528
|
+
- Prefer semantic attributes (variant, size, color) over style overrides.
|
|
529
|
+
|
|
530
|
+
LAYOUT ATTRIBUTES:
|
|
531
|
+
- gap: "1" through "16" — spacing between children (numeric, maps to --a-space-*)
|
|
532
|
+
- columns: "2|3|4|5|6" — grid column count
|
|
533
|
+
- grow: true — flex: 1 (fill available space)
|
|
534
|
+
- block: true — full-width (buttons, inputs)
|
|
535
|
+
- prose: true — content-optimized spacing and typography
|
|
536
|
+
- nomargin: true — remove default margins on typography elements`);
|
|
537
|
+
|
|
538
|
+
// ── Composition rules (when composing from fragment patterns) ──
|
|
539
|
+
if (composition?.fragmentPatterns?.length >= 2) {
|
|
540
|
+
const { fragmentPatterns, decomposition } = composition;
|
|
541
|
+
const layoutComponent = decomposition?.layout?.component || 'Column';
|
|
542
|
+
const layoutChild = decomposition?.layout?.child || 'Card';
|
|
543
|
+
parts.push(`COMPOSITION RULES (multi-section intent detected):
|
|
544
|
+
You are composing ${fragmentPatterns.length} independent sections into a single cohesive UI.
|
|
545
|
+
|
|
546
|
+
LAYOUT: Use ${layoutComponent} as the root container${layoutComponent === 'Tabs' ? ' — Tab buttons are siblings of content panels, NOT parents' : ''}.
|
|
547
|
+
Each section becomes a ${layoutChild} child of the root.
|
|
548
|
+
|
|
549
|
+
COMPOSITION PRINCIPLES:
|
|
550
|
+
1. UNIQUE IDS: Prefix component IDs per section to avoid collisions (e.g., stats-root, table-hdr, form-submit)
|
|
551
|
+
2. COHESION: Add a page-level Header above all sections with the overall title
|
|
552
|
+
3. VISUAL HIERARCHY: Each section should be wrapped in a Card for visual separation (unless using Tabs)
|
|
553
|
+
4. CONSISTENCY: Use the same sizing, spacing tokens, and theme across all sections
|
|
554
|
+
5. CROSS-SECTION WIRING: If sections share data (e.g., stats + table both show user data), use shared data sources
|
|
555
|
+
|
|
556
|
+
SECTIONS TO COMPOSE:
|
|
557
|
+
${fragmentPatterns.map((f, i) => `${i + 1}. ${f.label} — based on "${f.pattern.name}" pattern (${f.pattern.components?.join(', ') || 'various components'})`).join('\n')}`);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// ── Web research context (if available) ──
|
|
561
|
+
if (researchContext) {
|
|
562
|
+
parts.push(`DESIGN RESEARCH:\n${researchContext}\nUse these insights to inform your component choices, layout, and content — but output must still be valid A2UI.`);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// ── Wiring capabilities — interactive behavior via wireComponents message ──
|
|
566
|
+
// Include when the intent suggests any interactivity, data binding, or user actions
|
|
567
|
+
const wiringKeywords = /\b(form|submit|save|data|api|fetch|navigate|login|checkout|filter|sort|crud|search|delete|edit|update|toggle|select|drag|drop|realtime|stream|websocket|poll|refresh|validate|upload|download|subscribe|notify|toast|modal|dialog|confirm|paginate|infinite.?scroll|load.?more)\b/i;
|
|
568
|
+
if (wiringKeywords.test(intent || '')) {
|
|
569
|
+
const wc = getWiringCatalog();
|
|
570
|
+
parts.push(`WIRING — Declarative Interactivity (5 docking points):
|
|
571
|
+
When the UI needs behavior beyond display, emit a wireComponents message. Each layer is optional — use only what the intent requires.
|
|
572
|
+
|
|
573
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
574
|
+
│ DATA — where the surface gets its data │
|
|
575
|
+
│ STATE — controllers that manage component behavior │
|
|
576
|
+
│ ACTIONS — trigger → handler chains (what happens on events) │
|
|
577
|
+
│ PROVIDES — context injection into subtrees │
|
|
578
|
+
│ LIFECYCLE — onMount, onUnmount, onModelChange hooks │
|
|
579
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
580
|
+
|
|
581
|
+
DATA LAYER:
|
|
582
|
+
"data": {
|
|
583
|
+
"params": { "userId": { "from": "route", "key": "id" } },
|
|
584
|
+
"sources": [
|
|
585
|
+
{ "id": "users", "uri": "resource://users", "path": "/users", "refresh": "once" },
|
|
586
|
+
{ "id": "feed", "uri": "sse://events/{userId}", "path": "/feed", "refresh": "stream" }
|
|
587
|
+
]
|
|
588
|
+
}
|
|
589
|
+
URI schemes: resource://, api://, mock://, ws://, sse://
|
|
590
|
+
Refresh: once, on-focus, interval:{ms}, stream
|
|
591
|
+
To re-fetch after an action, use refresh-source in onSuccess — not a refresh strategy.
|
|
592
|
+
|
|
593
|
+
STATE LAYER:
|
|
594
|
+
"state": {
|
|
595
|
+
"controllers": [
|
|
596
|
+
{ "id": "form1", "type": "FormController", "host": "form-col", "config": { "validateOn": "submit" },
|
|
597
|
+
"bind": { "values": "/form/values", "valid": "/form/valid" } }
|
|
598
|
+
],
|
|
599
|
+
"model": { "filter": "", "page": 1 }
|
|
600
|
+
}
|
|
601
|
+
Controllers: ${wc.controllers.map(c => c.type).join(', ')} (extensible via registerController)
|
|
602
|
+
"bind" maps controller state ↔ model paths for two-way sync.
|
|
603
|
+
|
|
604
|
+
ACTIONS LAYER — UIEvent → handler chains:
|
|
605
|
+
Each action binds a UIEvent to a handler. The event is a typed object, not a string.
|
|
606
|
+
|
|
607
|
+
UIEvent types: ${wc.adiaEvents.map(e => e.event).join(', ')}
|
|
608
|
+
Each carries a typed payload:
|
|
609
|
+
${wc.adiaEvents.filter(e => e.payload).map(e => ` ${e.event} → ${e.payload}`).join('\n')}
|
|
610
|
+
|
|
611
|
+
"actions": [
|
|
612
|
+
{
|
|
613
|
+
"event": { "event": "submit", "target": "save-btn" },
|
|
614
|
+
"handler": "submit-resource",
|
|
615
|
+
"config": { "uri": "api://users", "method": "POST" },
|
|
616
|
+
"onSuccess": [{ "handler": "notify", "config": { "message": "Saved!", "variant": "success" } }],
|
|
617
|
+
"onError": [{ "handler": "notify", "config": { "message": "Failed", "variant": "error" } }]
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
"event": { "event": "input", "target": "search-input", "debounce": 300 },
|
|
621
|
+
"handler": "update-model",
|
|
622
|
+
"config": { "path": "/filter" }
|
|
623
|
+
}
|
|
624
|
+
]
|
|
625
|
+
|
|
626
|
+
UIEvent shape: { "event": "<type>", "target": "<componentId>", "debounce"?: ms, "throttle"?: ms, "condition"?: { "path", "equals"|"notEquals"|"exists" } }
|
|
627
|
+
"target" scopes to a component id. Omit for surface-level events (mount, unmount).
|
|
628
|
+
"onSuccess" / "onError" are follow-up action arrays — NOT separate entries.
|
|
629
|
+
Handlers: ${wc.handlers.map(h => h.name).join(', ')} (extensible via registerHandler)
|
|
630
|
+
|
|
631
|
+
LIFECYCLE LAYER:
|
|
632
|
+
"lifecycle": {
|
|
633
|
+
"onMount": [{ "handler": "refresh-source", "config": { "sourceId": "users" } }],
|
|
634
|
+
"onModelChange": [{ "path": "/filter", "handler": "refresh-source", "config": { "sourceId": "search" }, "debounce": 300 }]
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
FULL EXAMPLE — Dashboard with data fetch + search + pagination:
|
|
638
|
+
[
|
|
639
|
+
{ "type": "updateComponents", "surfaceId": "default", "components": [
|
|
640
|
+
{ "id": "root", "component": "Column", "gap": "4", "children": ["toolbar", "table-card"] },
|
|
641
|
+
{ "id": "toolbar", "component": "Row", "gap": "2", "children": ["search", "refresh-btn"] },
|
|
642
|
+
{ "id": "search", "component": "Input", "type": "search", "placeholder": "Search...", "name": "q", "grow": true },
|
|
643
|
+
{ "id": "refresh-btn", "component": "Button", "icon": "arrows-clockwise", "variant": "ghost" },
|
|
644
|
+
{ "id": "table-card", "component": "Card", "children": ["table-sec", "table-ftr"] },
|
|
645
|
+
{ "id": "table-sec", "component": "Section", "children": ["tbl"] },
|
|
646
|
+
{ "id": "tbl", "component": "Table", "sortable": true },
|
|
647
|
+
{ "id": "table-ftr", "component": "Footer", "children": ["pager"] },
|
|
648
|
+
{ "id": "pager", "component": "Pagination", "total": 100, "pageSize": 10 }
|
|
649
|
+
]},
|
|
650
|
+
{ "type": "wireComponents", "surfaceId": "default",
|
|
651
|
+
"data": {
|
|
652
|
+
"sources": [
|
|
653
|
+
{ "id": "items", "uri": "resource://items?q={filter}&page={page}", "path": "/items", "refresh": "once" }
|
|
654
|
+
]
|
|
655
|
+
},
|
|
656
|
+
"state": {
|
|
657
|
+
"controllers": [
|
|
658
|
+
{ "id": "tbl-ctrl", "type": "SelectionController", "host": "tbl", "config": { "mode": "multi" },
|
|
659
|
+
"bind": { "selected": "/selection" } }
|
|
660
|
+
],
|
|
661
|
+
"model": { "filter": "", "page": 1 }
|
|
662
|
+
},
|
|
663
|
+
"actions": [
|
|
664
|
+
{ "event": { "event": "input", "target": "search", "debounce": 300 },
|
|
665
|
+
"handler": "update-model", "config": { "path": "/filter", "value": { "from": "event-target", "key": "value" } } },
|
|
666
|
+
{ "event": { "event": "press", "target": "refresh-btn" },
|
|
667
|
+
"handler": "refresh-source", "config": { "sourceId": "items" } },
|
|
668
|
+
{ "event": { "event": "change", "target": "pager" },
|
|
669
|
+
"handler": "update-model", "config": { "path": "/page", "value": { "from": "event-detail", "key": "page" } } }
|
|
670
|
+
],
|
|
671
|
+
"lifecycle": {
|
|
672
|
+
"onMount": [{ "handler": "refresh-source", "config": { "sourceId": "items" } }],
|
|
673
|
+
"onModelChange": [{ "path": "/filter", "handler": "refresh-source", "config": { "sourceId": "items" }, "debounce": 500 }]
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
]
|
|
677
|
+
|
|
678
|
+
WHEN TO INCLUDE wireComponents:
|
|
679
|
+
- Forms → FormController + submit-resource + navigate/notify
|
|
680
|
+
- Tables with data → DataStreamController or data sources + SelectionController
|
|
681
|
+
- Search/filter → update-model with debounce + refresh-source
|
|
682
|
+
- CRUD → submit-resource (POST/PUT/DELETE) + success/error notify
|
|
683
|
+
- Real-time → data source with refresh:"stream" or "interval:{ms}"
|
|
684
|
+
- Toggles/accordions → ToggleController or AccordionController
|
|
685
|
+
- Navigation → navigate handler
|
|
686
|
+
|
|
687
|
+
DO NOT include wireComponents for purely static/display UIs (stat cards, hero sections, profiles without edit).`);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
return parts.join('\n\n');
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
694
|
+
// MULTI-TURN (Feature 3: conversation history as context)
|
|
695
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Build chat messages for the LLM, including previous turn context for
|
|
699
|
+
* multi-turn iteration. If this is the first turn, just the user intent.
|
|
700
|
+
* If iterating, includes the previous A2UI output as assistant context.
|
|
701
|
+
*
|
|
702
|
+
* @param {string} intent — Current user intent
|
|
703
|
+
* @param {string} executionId — Execution ID (may have previous turns)
|
|
704
|
+
* @returns {{ role: string, content: string }[]}
|
|
705
|
+
*/
|
|
706
|
+
export function buildChatMessages(intent, executionId) {
|
|
707
|
+
const messages = [];
|
|
708
|
+
const prevTurns = store.getAll(executionId);
|
|
709
|
+
|
|
710
|
+
if (prevTurns && prevTurns.length > 0) {
|
|
711
|
+
// Include up to last 2 turns as context
|
|
712
|
+
const recentTurns = prevTurns.slice(-2);
|
|
713
|
+
for (const turn of recentTurns) {
|
|
714
|
+
// Previous user intent
|
|
715
|
+
if (turn.summary) {
|
|
716
|
+
messages.push({ role: 'user', content: turn.summary });
|
|
717
|
+
}
|
|
718
|
+
// Previous A2UI output (compact — just component tree, no full message wrapper)
|
|
719
|
+
if (turn.messages?.length > 0) {
|
|
720
|
+
const components = turn.messages.flatMap(m => m.components || []);
|
|
721
|
+
const compact = components.map(c => {
|
|
722
|
+
const { _surfaceId, ...rest } = c;
|
|
723
|
+
return rest;
|
|
724
|
+
});
|
|
725
|
+
messages.push({
|
|
726
|
+
role: 'assistant',
|
|
727
|
+
content: JSON.stringify(compact),
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Current turn
|
|
734
|
+
messages.push({ role: 'user', content: intent });
|
|
735
|
+
|
|
736
|
+
return messages;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
740
|
+
// STREAMING HELPERS (Feature 1)
|
|
741
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Try to parse a partial LLM text buffer as A2UI JSON.
|
|
745
|
+
* Handles incomplete JSON by attempting to close open brackets/braces.
|
|
746
|
+
* Returns null if not parseable yet.
|
|
747
|
+
*
|
|
748
|
+
* @param {string} text — Accumulated LLM output so far
|
|
749
|
+
* @returns {object[]|null}
|
|
750
|
+
*/
|
|
751
|
+
export function tryParsePartial(text) {
|
|
752
|
+
if (!text) return null;
|
|
753
|
+
|
|
754
|
+
// Strip markdown fences if present
|
|
755
|
+
let json = text.trim();
|
|
756
|
+
const fenceMatch = json.match(/```(?:json)?\s*\n?([\s\S]*)/);
|
|
757
|
+
if (fenceMatch) {
|
|
758
|
+
json = fenceMatch[1].trim();
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// Must start with [ or { to be JSON
|
|
762
|
+
if (!json.startsWith('[') && !json.startsWith('{')) return null;
|
|
763
|
+
|
|
764
|
+
// Try parsing as-is first (may be complete)
|
|
765
|
+
try {
|
|
766
|
+
const parsed = JSON.parse(json);
|
|
767
|
+
return wrapAsMessages(parsed);
|
|
768
|
+
} catch { /* expected for partial JSON */ }
|
|
769
|
+
|
|
770
|
+
// Try auto-closing: count open brackets and close them
|
|
771
|
+
let openBrackets = 0;
|
|
772
|
+
let openBraces = 0;
|
|
773
|
+
let inString = false;
|
|
774
|
+
let escaped = false;
|
|
775
|
+
|
|
776
|
+
for (const ch of json) {
|
|
777
|
+
if (escaped) { escaped = false; continue; }
|
|
778
|
+
if (ch === '\\') { escaped = true; continue; }
|
|
779
|
+
if (ch === '"') { inString = !inString; continue; }
|
|
780
|
+
if (inString) continue;
|
|
781
|
+
if (ch === '[') openBrackets++;
|
|
782
|
+
if (ch === ']') openBrackets--;
|
|
783
|
+
if (ch === '{') openBraces++;
|
|
784
|
+
if (ch === '}') openBraces--;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// Close any open strings, objects, arrays
|
|
788
|
+
let patched = json;
|
|
789
|
+
if (inString) patched += '"';
|
|
790
|
+
for (let i = 0; i < openBraces; i++) patched += '}';
|
|
791
|
+
for (let i = 0; i < openBrackets; i++) patched += ']';
|
|
792
|
+
|
|
793
|
+
try {
|
|
794
|
+
const parsed = JSON.parse(patched);
|
|
795
|
+
return wrapAsMessages(parsed);
|
|
796
|
+
} catch {
|
|
797
|
+
return null;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
802
|
+
// CANVAS DIFF HELPERS — compact summary + merge for multi-turn iteration
|
|
803
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Produce a compact summary of existing canvas components.
|
|
807
|
+
* Instead of full JSON (~7K tokens for 100+ components), this yields ~500 tokens.
|
|
808
|
+
* Format:
|
|
809
|
+
* 148 components in 5 sections: Task Summary, Recent Activity, ...
|
|
810
|
+
* root: Column
|
|
811
|
+
* hdr: Header
|
|
812
|
+
* title: Text(heading)
|
|
813
|
+
* ...
|
|
814
|
+
*/
|
|
815
|
+
function compactCanvasSummary(components) {
|
|
816
|
+
if (!components || components.length === 0) return '';
|
|
817
|
+
|
|
818
|
+
// Detect sections (Cards with Header children)
|
|
819
|
+
const sections = [];
|
|
820
|
+
const cardIds = new Set();
|
|
821
|
+
for (const c of components) {
|
|
822
|
+
if (c.component === 'Card') {
|
|
823
|
+
cardIds.add(c.id);
|
|
824
|
+
// Try to find the section name from a child Header or Text with heading variant
|
|
825
|
+
const headerChild = components.find(
|
|
826
|
+
h => h.parentId === c.id && (h.component === 'Header' || h.component === 'CardHeader')
|
|
827
|
+
);
|
|
828
|
+
if (headerChild) {
|
|
829
|
+
const titleChild = components.find(
|
|
830
|
+
t => t.parentId === headerChild.id && t.component === 'Text'
|
|
831
|
+
);
|
|
832
|
+
sections.push(titleChild?.text || titleChild?.children || headerChild.title || c.id);
|
|
833
|
+
} else {
|
|
834
|
+
sections.push(c.id);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
const lines = [];
|
|
840
|
+
const sectionSummary = sections.length > 0
|
|
841
|
+
? ` in ${sections.length} sections: ${sections.slice(0, 8).join(', ')}${sections.length > 8 ? `, ... +${sections.length - 8} more` : ''}`
|
|
842
|
+
: '';
|
|
843
|
+
lines.push(`${components.length} components${sectionSummary}`);
|
|
844
|
+
lines.push('');
|
|
845
|
+
|
|
846
|
+
// One-line-per-component summary: id: Component(variant) [slot]
|
|
847
|
+
// Layout containers get their structural props so the LLM preserves them
|
|
848
|
+
const LAYOUT_TYPES = new Set(['Grid', 'Row', 'Column', 'Stack']);
|
|
849
|
+
const LAYOUT_PROPS = ['columns', 'gap', 'justify', 'align', 'wrap', 'direction', 'responsive'];
|
|
850
|
+
for (const c of components) {
|
|
851
|
+
let desc = c.component;
|
|
852
|
+
if (c.variant) desc += `(${c.variant})`;
|
|
853
|
+
if (c.slot) desc += ` [slot=${c.slot}]`;
|
|
854
|
+
// Include layout-critical props for containers
|
|
855
|
+
if (LAYOUT_TYPES.has(c.component)) {
|
|
856
|
+
const lp = LAYOUT_PROPS.filter(p => c[p] != null).map(p => `${p}=${c[p]}`);
|
|
857
|
+
if (lp.length) desc += ` {${lp.join(', ')}}`;
|
|
858
|
+
}
|
|
859
|
+
if (c.text) desc += ` "${String(c.text).slice(0, 30)}"`;
|
|
860
|
+
else if (c.label) desc += ` "${String(c.label).slice(0, 30)}"`;
|
|
861
|
+
else if (c.title) desc += ` "${String(c.title).slice(0, 30)}"`;
|
|
862
|
+
if (c.parentId) desc += ` ^${c.parentId}`;
|
|
863
|
+
lines.push(` ${c.id}: ${desc}`);
|
|
864
|
+
}
|
|
865
|
+
return lines.join('\n');
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Merge an LLM-produced diff response with the prior canvas.
|
|
870
|
+
*
|
|
871
|
+
* The diff response contains:
|
|
872
|
+
* - New components (id not in prior): added to result
|
|
873
|
+
* - Modified components ({ id: 'existing', ...changed props }): merged with existing
|
|
874
|
+
* - Deleted components ({ id: 'existing', _delete: true }): removed
|
|
875
|
+
* - Unchanged components: carried forward from prior automatically
|
|
876
|
+
*
|
|
877
|
+
* @param {Array} priorComponents — full component array from last turn
|
|
878
|
+
* @param {Array} diffComponents — sparse diff array from LLM
|
|
879
|
+
* @returns {Array} merged full component array
|
|
880
|
+
*/
|
|
881
|
+
export function mergeCanvasDiff(priorComponents, diffComponents) {
|
|
882
|
+
if (!diffComponents || diffComponents.length === 0) return priorComponents;
|
|
883
|
+
if (!priorComponents || priorComponents.length === 0) return diffComponents;
|
|
884
|
+
|
|
885
|
+
// ── Detect full-replacement response ──
|
|
886
|
+
// On iteration turns the LLM is instructed to return DIFF OBJECTS referring
|
|
887
|
+
// to the prior canvas's ids. If the returned array shares almost no ids
|
|
888
|
+
// with the prior canvas AND starts from 'root', the LLM hallucinated a new
|
|
889
|
+
// UI instead of iterating — not a legitimate regeneration.
|
|
890
|
+
//
|
|
891
|
+
// Old behavior: silently accept the replacement (threshold <20% id match).
|
|
892
|
+
// This caused iteration turns to produce entirely unrelated canvases (see
|
|
893
|
+
// kanban "add draggable traits" case, 2026-04-19).
|
|
894
|
+
// New behavior: on iteration, a low-match-ratio replacement is a failure,
|
|
895
|
+
// not a feature. PRESERVE the prior canvas and warn — the user is better
|
|
896
|
+
// off with the unchanged canvas than with a hallucinated one. The ≥60%
|
|
897
|
+
// band still accepts valid whole-canvas rewrites (e.g. major restructures
|
|
898
|
+
// that re-id most components intentionally).
|
|
899
|
+
const priorIds = new Set(priorComponents.map(c => c.id));
|
|
900
|
+
const matchCount = diffComponents.filter(c => priorIds.has(c.id)).length;
|
|
901
|
+
const matchRatio = matchCount / Math.max(diffComponents.length, 1);
|
|
902
|
+
const hasRoot = diffComponents.some(c => c.id === 'root');
|
|
903
|
+
|
|
904
|
+
if (hasRoot && diffComponents.length >= 5 && matchRatio < 0.2) {
|
|
905
|
+
console.warn(`[mergeCanvasDiff] Hallucinated replacement rejected: ${diffComponents.length} new components, only ${matchCount} matching ids (${Math.round(matchRatio * 100)}%). Preserving prior canvas — LLM ignored iteration instructions.`);
|
|
906
|
+
return priorComponents;
|
|
907
|
+
}
|
|
908
|
+
if (hasRoot && diffComponents.length >= 5 && matchRatio < 0.6) {
|
|
909
|
+
// Middle band — could be a valid major rewrite, could be a partial
|
|
910
|
+
// hallucination. Accept but flag. If this turns out to be wrong too,
|
|
911
|
+
// tighten to return priorComponents here as well.
|
|
912
|
+
console.warn(`[mergeCanvasDiff] Low-match rewrite accepted: ${matchCount}/${diffComponents.length} ids match (${Math.round(matchRatio * 100)}%). Treating as a deliberate restructure.`);
|
|
913
|
+
return diffComponents;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const priorMap = new Map(priorComponents.map(c => [c.id, c]));
|
|
917
|
+
const deletedIds = new Set();
|
|
918
|
+
const modifiedIds = new Set();
|
|
919
|
+
const newComponents = [];
|
|
920
|
+
|
|
921
|
+
// §190b (v0.5.6) — Aggregate blocked-layout-type-change warnings.
|
|
922
|
+
// The for-loop below can fire `console.warn` once per blocked component,
|
|
923
|
+
// producing 5-10 line bursts when the LLM tries to restructure a form's
|
|
924
|
+
// fields (typical case: 5 Field→Column blocks for a 5-field form).
|
|
925
|
+
// Collect blocks into an array; emit a single aggregated warning after
|
|
926
|
+
// the loop. Closes the 2026-05-14 user-reported "buglabels" intent
|
|
927
|
+
// emitting 6 consecutive blocks.
|
|
928
|
+
const blockedLayoutChanges = [];
|
|
929
|
+
|
|
930
|
+
for (const dc of diffComponents) {
|
|
931
|
+
if (!dc || !dc.id) continue;
|
|
932
|
+
|
|
933
|
+
if (dc._delete) {
|
|
934
|
+
// Deletion marker
|
|
935
|
+
deletedIds.add(dc.id);
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
if (priorMap.has(dc.id)) {
|
|
940
|
+
// Modification: merge props onto existing component
|
|
941
|
+
const existing = priorMap.get(dc.id);
|
|
942
|
+
// GUARD: Prevent component type changes on layout containers —
|
|
943
|
+
// changing Grid→Column collapses multi-column layouts
|
|
944
|
+
const LAYOUT_CONTAINERS = new Set(['Grid', 'Row', 'Column', 'Stack']);
|
|
945
|
+
if (dc.component && dc.component !== existing.component &&
|
|
946
|
+
(LAYOUT_CONTAINERS.has(existing.component) || LAYOUT_CONTAINERS.has(dc.component))) {
|
|
947
|
+
blockedLayoutChanges.push({ id: dc.id, from: existing.component, to: dc.component });
|
|
948
|
+
delete dc.component;
|
|
949
|
+
}
|
|
950
|
+
const merged = { ...existing, ...dc };
|
|
951
|
+
// Remove internal _delete if somehow present
|
|
952
|
+
delete merged._delete;
|
|
953
|
+
priorMap.set(dc.id, merged);
|
|
954
|
+
modifiedIds.add(dc.id);
|
|
955
|
+
} else {
|
|
956
|
+
// New component
|
|
957
|
+
newComponents.push(dc);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// §190b — single aggregated warning for blocked layout changes.
|
|
962
|
+
if (blockedLayoutChanges.length > 0) {
|
|
963
|
+
const summary = blockedLayoutChanges
|
|
964
|
+
.map(b => `${b.from}→${b.to} on id="${b.id}"`)
|
|
965
|
+
.join(', ');
|
|
966
|
+
console.warn(`[mergeCanvasDiff] Blocked ${blockedLayoutChanges.length} layout type change${blockedLayoutChanges.length === 1 ? '' : 's'}: ${summary}. Preserving original types.`);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// Build result: prior components (minus deleted) + new components
|
|
970
|
+
const result = [];
|
|
971
|
+
for (const c of priorComponents) {
|
|
972
|
+
if (deletedIds.has(c.id)) continue;
|
|
973
|
+
result.push(priorMap.get(c.id));
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// Insert new components intelligently: after their parentId if specified, else at end
|
|
977
|
+
for (const nc of newComponents) {
|
|
978
|
+
if (nc.parentId) {
|
|
979
|
+
const parentIdx = result.findIndex(c => c.id === nc.parentId);
|
|
980
|
+
if (parentIdx !== -1) {
|
|
981
|
+
// Find the last sibling under the same parent to insert after
|
|
982
|
+
let insertIdx = parentIdx + 1;
|
|
983
|
+
while (insertIdx < result.length && result[insertIdx].parentId === nc.parentId) {
|
|
984
|
+
insertIdx++;
|
|
985
|
+
}
|
|
986
|
+
result.splice(insertIdx, 0, nc);
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
result.push(nc);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
return result;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* Build the diff-mode iteration prompt shared by all hasPriorCanvas branches.
|
|
998
|
+
* @param {string} intent — user's iteration request
|
|
999
|
+
* @param {Array} priorComponents — full prior canvas components
|
|
1000
|
+
* @param {object} [opts]
|
|
1001
|
+
* @param {string|null} [opts.originalIntent] — original design brief for drift anchoring
|
|
1002
|
+
* @returns {string} prompt text for the canvas diff
|
|
1003
|
+
*/
|
|
1004
|
+
/**
|
|
1005
|
+
* §169 (v0.5.4): structural rules block injected into iteration prompts.
|
|
1006
|
+
*
|
|
1007
|
+
* Pre-§169 the iteration prompts said "Preserve slot attributes and Card
|
|
1008
|
+
* anatomy rules" without defining what those rules ARE. The fresh-generation
|
|
1009
|
+
* prompt at buildSystemPrompt() defines them explicitly (Card > Header >
|
|
1010
|
+
* [Text slot=heading...], Card > Section > Column > [content], etc.) but
|
|
1011
|
+
* iteration mode relied on multi-turn context retention, which is unreliable.
|
|
1012
|
+
*
|
|
1013
|
+
* The 2026-05-14 chart-workflow ticket symptom: 32-component canvas + "add
|
|
1014
|
+
* a chart" intent → LLM emitted Chart at the wrong nesting depth, prompting
|
|
1015
|
+
* the user-friction complaint about "where chart data should live in the
|
|
1016
|
+
* tree." Empirically the corpus enforces Card > Section > Column > Chart
|
|
1017
|
+
* (233 Section→Column + 95 Card→Section across 230 chunks).
|
|
1018
|
+
*
|
|
1019
|
+
* This block echoes the fresh-generation contract's most-important
|
|
1020
|
+
* structural guidance into iteration mode. Keeps token cost negligible
|
|
1021
|
+
* (~150 tokens added to a 500-1500-token iteration prompt; well within
|
|
1022
|
+
* 32k max_tokens budget).
|
|
1023
|
+
*/
|
|
1024
|
+
const ITERATION_STRUCTURAL_RULES = `
|
|
1025
|
+
STRUCTURAL RULES (carry-over from fresh-generation contract — apply when ADDING new components):
|
|
1026
|
+
- Card content: Card > Header > [Text slot="heading", Text slot="description", Button slot="action"]
|
|
1027
|
+
Card > Section > Column > [content] (Section ALWAYS wraps children in a Column)
|
|
1028
|
+
Card > Footer > [Button, Button] (action buttons go in Footer, not Section)
|
|
1029
|
+
- Stats: use Stat component directly with {label, value, change, trend}; do NOT manually
|
|
1030
|
+
build stat cards from Card+Header+Section+Text — emit a Stat node.
|
|
1031
|
+
- Charts: place inside Section > Column. Chart props: type ("line"|"bar"|"area"), x (string),
|
|
1032
|
+
y (string or array). Charts go in Section, not directly inside Card.
|
|
1033
|
+
- Forms: wrap fields in Column with gap="3". Submit button in separate Row or Footer.
|
|
1034
|
+
- Tabs: children = ONLY Tab components; each Tab's own children are its panel content (parent auto-hides inactive).
|
|
1035
|
+
Tabs > [Tab(value=a) > Card, Tab(value=b) > Card] (each panel inside its Tab; NOT panels as Tabs siblings).
|
|
1036
|
+
- Header slots: every Header child MUST have a slot= attribute (heading|description|action|icon).
|
|
1037
|
+
- Layout types: Grid/Row/Column types MUST NOT change during iteration unless the user explicitly
|
|
1038
|
+
requests a layout change. To restructure, DELETE the old container + ADD a new one.
|
|
1039
|
+
`;
|
|
1040
|
+
|
|
1041
|
+
export function buildCanvasDiffPrompt(intent, priorComponents, { originalIntent = null } = {}) {
|
|
1042
|
+
// Design intent anchor — prevents drift from the original brief
|
|
1043
|
+
const intentAnchor = originalIntent && originalIntent !== intent
|
|
1044
|
+
? `\nORIGINAL DESIGN BRIEF (the user's first request — all changes must stay aligned with this): "${originalIntent}"\nThe current modification ("${intent}") should REFINE the original design, not replace its purpose.\n`
|
|
1045
|
+
: '';
|
|
1046
|
+
|
|
1047
|
+
// Full-JSON path for most canvases — the LLM gets the actual component
|
|
1048
|
+
// shape and can faithfully modify it. Compact-text summaries lose nesting
|
|
1049
|
+
// and props, which sends the LLM into "regenerate from memory" mode and
|
|
1050
|
+
// produces whole-new canvases instead of iterations (verified 2026-04-19).
|
|
1051
|
+
// The threshold is conservative — 300 components at ~40-60 bytes each is
|
|
1052
|
+
// ~15-20KB, well within 32k max_tokens output budget and 120k+ input.
|
|
1053
|
+
if (priorComponents.length < 300) {
|
|
1054
|
+
return `${intentAnchor}CURRENT CANVAS STATE (this is what the user sees right now — MODIFY it):
|
|
1055
|
+
${JSON.stringify(priorComponents, null, 2)}
|
|
1056
|
+
|
|
1057
|
+
The user is ITERATING on their existing canvas shown above. They want to MODIFY it, not replace it.
|
|
1058
|
+
${ITERATION_STRUCTURAL_RULES}
|
|
1059
|
+
Instructions:
|
|
1060
|
+
- Preserve ALL existing components unless explicitly asked to remove them
|
|
1061
|
+
- Add new components where they logically belong (following the STRUCTURAL RULES above)
|
|
1062
|
+
- Change text content, labels, icons to match the new intent
|
|
1063
|
+
- Preserve slot attributes and Card anatomy rules
|
|
1064
|
+
- CRITICAL: Preserve layout container types — do NOT change Grid to Column or Row to Column. If the canvas has a Grid with columns="3" for a card collection, keep it as Grid. Only change layout types when the user explicitly requests a layout change.
|
|
1065
|
+
- CONTENT CARRY-OVER on component swaps: when the user asks to upgrade a component to a richer variant (e.g. Image → Carousel, Text → Stat, Toggle → ToggleGroup, Input → Select), the original's content props (src, alt, textContent, text, value, label, name) MUST be carried into the equivalent prop on the new component. The original src becomes the first slide of a Carousel; the original textContent becomes the first item; the original value becomes the default. NEVER emit a swapped component with placeholder/lorem content when real content was present in the original.
|
|
1066
|
+
- You MUST return the COMPLETE component array including all existing components plus your modifications
|
|
1067
|
+
- Generate at least ${priorComponents.length} components (the existing ones plus any additions)
|
|
1068
|
+
- SCOPE GUARD: Only add sections/components that the user explicitly requested. Do NOT speculatively add features, panels, or sections not mentioned in the intent. If adding more than 3 new top-level sections, reconsider whether the user actually asked for them.
|
|
1069
|
+
- Output ONLY the JSON array, no explanation`;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// For large canvases, use compact diff format to save tokens
|
|
1073
|
+
const summary = compactCanvasSummary(priorComponents);
|
|
1074
|
+
return `CURRENT CANVAS SUMMARY (${priorComponents.length} components — do NOT regenerate unchanged ones):
|
|
1075
|
+
${summary}
|
|
1076
|
+
${intentAnchor}
|
|
1077
|
+
The user wants to MODIFY their existing canvas. Return ONLY the components you need to ADD, MODIFY, or DELETE:
|
|
1078
|
+
${ITERATION_STRUCTURAL_RULES}
|
|
1079
|
+
CRITICAL PRESERVATION RULES:
|
|
1080
|
+
- NEVER change layout container types (Grid→Column, Row→Column, Grid→Row) unless the user explicitly asks to change the layout structure.
|
|
1081
|
+
- If a Grid has columns="3", keep it as Grid with columns="3". Adding a toggle/filter/button to a toolbar does NOT change the content layout below.
|
|
1082
|
+
- Preserve parent-child relationships — adding a control to a toolbar row doesn't affect sibling containers.
|
|
1083
|
+
- CONTENT CARRY-OVER on component swaps: when upgrading a component to a richer variant (Image→Carousel, Text→Stat, Toggle→ToggleGroup, Input→Select), the original's content props (src, alt, textContent, text, value, label, name) MUST flow into the equivalent prop on the new component (or become its first item, where applicable). NEVER emit a swapped component with placeholder content when real content was present.
|
|
1084
|
+
|
|
1085
|
+
DIFF FORMAT:
|
|
1086
|
+
- To ADD a new component: include the full component JSON object (with a new unique id)
|
|
1087
|
+
- To MODIFY an existing component: { "id": "existing-id", ...only changed props } (e.g. { "id": "title", "text": "New Title" })
|
|
1088
|
+
- To DELETE a component: { "id": "existing-id", "_delete": true }
|
|
1089
|
+
- UNCHANGED components: do NOT include them — they are preserved automatically
|
|
1090
|
+
|
|
1091
|
+
CRITICAL: Do NOT change a component's "component" type in modifications (e.g. Grid→Column). Container types (Grid, Row, Column) define layout structure — changing them collapses layouts. To restructure, DELETE the old container and ADD a new one with the correct type and reparent its children.
|
|
1092
|
+
|
|
1093
|
+
Return a JSON array of ONLY the diff objects. Keep it minimal — typically 2-15 objects for an iteration.
|
|
1094
|
+
Output ONLY the JSON array, no explanation.`;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
/**
|
|
1098
|
+
* Normalize parsed JSON into A2UI message array.
|
|
1099
|
+
*/
|
|
1100
|
+
function wrapAsMessages(parsed) {
|
|
1101
|
+
if (Array.isArray(parsed)) {
|
|
1102
|
+
// Array of messages (may include updateComponents + wireComponents)
|
|
1103
|
+
const validTypes = new Set(['updateComponents', 'wireComponents', 'createSurface', 'updateDataModel', 'deleteSurface']);
|
|
1104
|
+
if (parsed.length > 0 && validTypes.has(parsed[0].type)) return parsed;
|
|
1105
|
+
// Bare components array
|
|
1106
|
+
if (parsed.length > 0 && parsed[0].id && parsed[0].component) {
|
|
1107
|
+
return [{ type: 'updateComponents', surfaceId: 'default', components: parsed }];
|
|
1108
|
+
}
|
|
1109
|
+
return parsed;
|
|
1110
|
+
}
|
|
1111
|
+
if (parsed && typeof parsed === 'object' && parsed.type === 'updateComponents') return [parsed];
|
|
1112
|
+
if (parsed && typeof parsed === 'object' && parsed.type === 'wireComponents') return [parsed];
|
|
1113
|
+
if (parsed && typeof parsed === 'object' && parsed.id && parsed.component) {
|
|
1114
|
+
return [{ type: 'updateComponents', surfaceId: 'default', components: [parsed] }];
|
|
1115
|
+
}
|
|
1116
|
+
return null;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1120
|
+
// EXISTING HELPERS
|
|
1121
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1122
|
+
|
|
1123
|
+
/**
|
|
1124
|
+
* Parse raw LLM output into A2UI messages.
|
|
1125
|
+
* Multi-strategy JSON extraction with repair for common LLM output issues.
|
|
1126
|
+
*
|
|
1127
|
+
* Strategies tried in order:
|
|
1128
|
+
* 1. Direct parse (clean JSON)
|
|
1129
|
+
* 2. Markdown code block extraction (```json ... ```)
|
|
1130
|
+
* 3. Outermost JSON extraction (find first [ or { to last ] or })
|
|
1131
|
+
* 4. Trailing comma / comment cleanup + re-parse
|
|
1132
|
+
* 5. Auto-close brackets (reuse tryParsePartial logic)
|
|
1133
|
+
*
|
|
1134
|
+
* @param {string} content — Raw LLM response text
|
|
1135
|
+
* @param {object} [ctx] — optional debug context forwarded to fallbackMessage
|
|
1136
|
+
* @returns {object[]} — Array of A2UI messages
|
|
1137
|
+
*/
|
|
1138
|
+
/**
|
|
1139
|
+
* Returns true if the given A2UI message array is a generation-failure
|
|
1140
|
+
* fallback surface (Alert + Try Again + debug payload). Stamped at fallback
|
|
1141
|
+
* construction via `_fallback: true` on the updateComponents message, so this
|
|
1142
|
+
* is a deterministic O(1) check — no fingerprinting heuristics required.
|
|
1143
|
+
*
|
|
1144
|
+
* Use this in validators, scorers, and downstream consumers that need to
|
|
1145
|
+
* distinguish a successful generation from a structurally-valid error surface.
|
|
1146
|
+
*/
|
|
1147
|
+
export function isFallbackResult(messages) {
|
|
1148
|
+
if (!Array.isArray(messages) || messages.length === 0) return false;
|
|
1149
|
+
return messages.some(m => m && m._fallback === true);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
export function parseA2UIResponse(content, ctx) {
|
|
1153
|
+
// Truncation short-circuit: if the LLM stopped because it hit max_tokens,
|
|
1154
|
+
// any remaining JSON is missing tail components. Don't try to auto-close —
|
|
1155
|
+
// surface the truncation explicitly so the validator and UI both know.
|
|
1156
|
+
if (ctx?.stopReason && ctx.stopReason !== 'end' && ctx.stopReason !== 'end_turn' && ctx.stopReason !== 'stop') {
|
|
1157
|
+
return [...fallbackMessage(`LLM response truncated (stop reason: ${ctx.stopReason}). Try a simpler intent or a model with larger output budget.`, { ...ctx, rawResponse: content })];
|
|
1158
|
+
}
|
|
1159
|
+
if (!content || typeof content !== 'string') {
|
|
1160
|
+
return [...fallbackMessage('Empty LLM response', ctx)];
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
const trimmed = content.trim();
|
|
1164
|
+
|
|
1165
|
+
// Strategy 1: Direct parse (cleanest case)
|
|
1166
|
+
const direct = _tryParseAndWrap(trimmed);
|
|
1167
|
+
if (direct) return direct;
|
|
1168
|
+
|
|
1169
|
+
// Strategy 2: Extract from markdown code fences (try all code blocks, prefer ```json)
|
|
1170
|
+
const fenceBlocks = [...trimmed.matchAll(/```(?:json)?\s*\n?([\s\S]*?)```/g)];
|
|
1171
|
+
for (const match of fenceBlocks) {
|
|
1172
|
+
const block = match[1].trim();
|
|
1173
|
+
const result = _tryParseAndWrap(block);
|
|
1174
|
+
if (result) return result;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// Strategy 3: Find outermost JSON — locate first [ or { and last ] or }
|
|
1178
|
+
const extracted = _extractOutermostJSON(trimmed);
|
|
1179
|
+
if (extracted) {
|
|
1180
|
+
const result = _tryParseAndWrap(extracted);
|
|
1181
|
+
if (result) return result;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// Strategy 4: Cleanup trailing commas, single-line comments, then re-parse
|
|
1185
|
+
const candidates = [trimmed, ...(fenceBlocks.map(m => m[1].trim())), extracted].filter(Boolean);
|
|
1186
|
+
for (const candidate of candidates) {
|
|
1187
|
+
const cleaned = _cleanupJSON(candidate);
|
|
1188
|
+
if (cleaned !== candidate) {
|
|
1189
|
+
const result = _tryParseAndWrap(cleaned);
|
|
1190
|
+
if (result) return result;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// Strategy 5: Auto-close brackets (LLM truncated mid-output)
|
|
1195
|
+
for (const candidate of candidates) {
|
|
1196
|
+
const cleaned = _cleanupJSON(candidate);
|
|
1197
|
+
const closed = _autoCloseBrackets(cleaned);
|
|
1198
|
+
if (closed !== cleaned) {
|
|
1199
|
+
const result = _tryParseAndWrap(closed);
|
|
1200
|
+
if (result) return result;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// All strategies failed — log raw content for debugging
|
|
1205
|
+
_logParseFailure(content);
|
|
1206
|
+
return [...fallbackMessage('Failed to parse LLM response as JSON', { ...ctx, rawResponse: content })];
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
/**
|
|
1210
|
+
* Try JSON.parse + wrapAsMessages. Returns result or null.
|
|
1211
|
+
*/
|
|
1212
|
+
function _tryParseAndWrap(text) {
|
|
1213
|
+
if (!text) return null;
|
|
1214
|
+
try {
|
|
1215
|
+
const parsed = JSON.parse(text);
|
|
1216
|
+
return wrapAsMessages(parsed) || null;
|
|
1217
|
+
} catch {
|
|
1218
|
+
return null;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
/**
|
|
1223
|
+
* Extract outermost JSON structure from text with surrounding prose.
|
|
1224
|
+
* Finds the first [ or { and the matching last ] or }.
|
|
1225
|
+
*/
|
|
1226
|
+
function _extractOutermostJSON(text) {
|
|
1227
|
+
// Find first JSON-start character
|
|
1228
|
+
const arrStart = text.indexOf('[');
|
|
1229
|
+
const objStart = text.indexOf('{');
|
|
1230
|
+
if (arrStart === -1 && objStart === -1) return null;
|
|
1231
|
+
|
|
1232
|
+
let startIdx;
|
|
1233
|
+
if (arrStart === -1) startIdx = objStart;
|
|
1234
|
+
else if (objStart === -1) startIdx = arrStart;
|
|
1235
|
+
else startIdx = Math.min(arrStart, objStart);
|
|
1236
|
+
|
|
1237
|
+
// Walk forward tracking nesting for both bracket types
|
|
1238
|
+
let brackets = 0;
|
|
1239
|
+
let braces = 0;
|
|
1240
|
+
let inString = false;
|
|
1241
|
+
let escaped = false;
|
|
1242
|
+
let endIdx = -1;
|
|
1243
|
+
|
|
1244
|
+
for (let i = startIdx; i < text.length; i++) {
|
|
1245
|
+
const ch = text[i];
|
|
1246
|
+
if (escaped) { escaped = false; continue; }
|
|
1247
|
+
if (ch === '\\' && inString) { escaped = true; continue; }
|
|
1248
|
+
if (ch === '"') { inString = !inString; continue; }
|
|
1249
|
+
if (inString) continue;
|
|
1250
|
+
if (ch === '[') brackets++;
|
|
1251
|
+
else if (ch === ']') brackets--;
|
|
1252
|
+
else if (ch === '{') braces++;
|
|
1253
|
+
else if (ch === '}') braces--;
|
|
1254
|
+
|
|
1255
|
+
// Balanced when we return to zero for the type we started with
|
|
1256
|
+
if (brackets === 0 && braces === 0) {
|
|
1257
|
+
endIdx = i;
|
|
1258
|
+
break;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
if (endIdx === -1) {
|
|
1263
|
+
// No balanced close found — return from start to end for auto-closing
|
|
1264
|
+
return text.slice(startIdx);
|
|
1265
|
+
}
|
|
1266
|
+
return text.slice(startIdx, endIdx + 1);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
/**
|
|
1270
|
+
* Clean up common LLM JSON issues: trailing commas, single-line comments.
|
|
1271
|
+
*/
|
|
1272
|
+
function _cleanupJSON(text) {
|
|
1273
|
+
if (!text) return text;
|
|
1274
|
+
let result = text;
|
|
1275
|
+
// Remove single-line comments (// ...) outside strings
|
|
1276
|
+
result = result.replace(/(?<="[^"]*".*?)\/\/[^\n]*/g, '');
|
|
1277
|
+
// Simpler: remove lines that are just comments
|
|
1278
|
+
result = result.replace(/^\s*\/\/.*$/gm, '');
|
|
1279
|
+
// Remove trailing commas before } or ]
|
|
1280
|
+
result = result.replace(/,\s*([}\]])/g, '$1');
|
|
1281
|
+
return result;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
/**
|
|
1285
|
+
* Auto-close unclosed brackets/braces (LLM truncated mid-output).
|
|
1286
|
+
*/
|
|
1287
|
+
function _autoCloseBrackets(text) {
|
|
1288
|
+
if (!text) return text;
|
|
1289
|
+
// Must start with [ or {
|
|
1290
|
+
const trimmed = text.trim();
|
|
1291
|
+
if (!trimmed.startsWith('[') && !trimmed.startsWith('{')) return text;
|
|
1292
|
+
|
|
1293
|
+
let openBrackets = 0;
|
|
1294
|
+
let openBraces = 0;
|
|
1295
|
+
let inString = false;
|
|
1296
|
+
let escaped = false;
|
|
1297
|
+
|
|
1298
|
+
for (const ch of trimmed) {
|
|
1299
|
+
if (escaped) { escaped = false; continue; }
|
|
1300
|
+
if (ch === '\\') { escaped = true; continue; }
|
|
1301
|
+
if (ch === '"') { inString = !inString; continue; }
|
|
1302
|
+
if (inString) continue;
|
|
1303
|
+
if (ch === '[') openBrackets++;
|
|
1304
|
+
if (ch === ']') openBrackets--;
|
|
1305
|
+
if (ch === '{') openBraces++;
|
|
1306
|
+
if (ch === '}') openBraces--;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
if (openBrackets === 0 && openBraces === 0) return text;
|
|
1310
|
+
|
|
1311
|
+
let patched = trimmed;
|
|
1312
|
+
if (inString) patched += '"';
|
|
1313
|
+
for (let i = 0; i < openBraces; i++) patched += '}';
|
|
1314
|
+
for (let i = 0; i < openBrackets; i++) patched += ']';
|
|
1315
|
+
return patched;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
/**
|
|
1319
|
+
* Log raw LLM content on parse failure for debugging.
|
|
1320
|
+
* Only logs in Node.js environment (not browser).
|
|
1321
|
+
*/
|
|
1322
|
+
function _logParseFailure(content) {
|
|
1323
|
+
try {
|
|
1324
|
+
const preview = content.length > 500 ? content.slice(0, 500) + '...' : content;
|
|
1325
|
+
console.warn(`[A2UI] All JSON parse strategies failed (${content.length} chars). Preview:\n${preview}`);
|
|
1326
|
+
} catch { /* ignore */ }
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
/**
|
|
1330
|
+
* Create a fallback updateComponents message with an error hint.
|
|
1331
|
+
* When debug metadata is available, renders execution context, mode, intent,
|
|
1332
|
+
* a collapsible raw-response preview, and a copy-debug-info button.
|
|
1333
|
+
*
|
|
1334
|
+
* @param {string} reason
|
|
1335
|
+
* @param {object} [opts]
|
|
1336
|
+
* @param {string} [opts.executionId] — Pipeline execution ID
|
|
1337
|
+
* @param {string} [opts.intent] — The user intent that triggered generation
|
|
1338
|
+
* @param {string} [opts.mode] — Generation mode (instant/pro/thinking/stream)
|
|
1339
|
+
* @param {string} [opts.rawResponse] — Raw LLM output (truncated for display)
|
|
1340
|
+
* @returns {object[]} Array of A2UI messages (updateComponents + wireComponents)
|
|
1341
|
+
*/
|
|
1342
|
+
function fallbackMessage(reason, opts = {}) {
|
|
1343
|
+
const { executionId, intent, mode, rawResponse } = opts;
|
|
1344
|
+
const children = ['sec', 'ftr'];
|
|
1345
|
+
const components = [
|
|
1346
|
+
{ id: 'sec', component: 'Section', children: ['alert'] },
|
|
1347
|
+
{ id: 'alert', component: 'Alert', variant: 'danger', title: 'Generation Error', description: reason },
|
|
1348
|
+
];
|
|
1349
|
+
|
|
1350
|
+
// ── Debug metadata: execution ID, mode, intent ──
|
|
1351
|
+
const hasMetadata = executionId || mode || intent;
|
|
1352
|
+
if (hasMetadata) {
|
|
1353
|
+
children.splice(1, 0, 'meta-sec');
|
|
1354
|
+
const metaChildren = [];
|
|
1355
|
+
if (executionId) {
|
|
1356
|
+
metaChildren.push('meta-exec');
|
|
1357
|
+
components.push({ id: 'meta-exec', component: 'Text', variant: 'caption', color: 'subtle', textContent: `Execution: ${executionId}` });
|
|
1358
|
+
}
|
|
1359
|
+
if (mode) {
|
|
1360
|
+
metaChildren.push('meta-mode');
|
|
1361
|
+
components.push({ id: 'meta-mode', component: 'Badge', text: mode, tone: 'muted' });
|
|
1362
|
+
}
|
|
1363
|
+
if (intent) {
|
|
1364
|
+
const truncIntent = intent.length > 120 ? intent.slice(0, 120) + '…' : intent;
|
|
1365
|
+
metaChildren.push('meta-intent');
|
|
1366
|
+
components.push({ id: 'meta-intent', component: 'Text', variant: 'caption', color: 'subtle', textContent: `Prompt: "${truncIntent}"` });
|
|
1367
|
+
}
|
|
1368
|
+
components.push({ id: 'meta-sec', component: 'Section', children: metaChildren });
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
// ── Collapsible raw response preview ──
|
|
1372
|
+
if (rawResponse && typeof rawResponse === 'string') {
|
|
1373
|
+
children.splice(children.indexOf('ftr'), 0, 'raw-acc');
|
|
1374
|
+
const preview = rawResponse.length > 2000 ? rawResponse.slice(0, 2000) + `\n… [truncated, ${rawResponse.length} chars total]` : rawResponse;
|
|
1375
|
+
components.push(
|
|
1376
|
+
{ id: 'raw-acc', component: 'Accordion', children: ['raw-item'] },
|
|
1377
|
+
{ id: 'raw-item', component: 'AccordionItem', text: 'Raw LLM Response', children: ['raw-code'] },
|
|
1378
|
+
{ id: 'raw-code', component: 'Code', language: 'text', textContent: preview },
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
// ── Footer: Try Again + Copy Debug Info ──
|
|
1383
|
+
const footerChildren = ['retry'];
|
|
1384
|
+
const debugPayload = JSON.stringify({
|
|
1385
|
+
error: reason,
|
|
1386
|
+
...(executionId && { executionId }),
|
|
1387
|
+
...(mode && { mode }),
|
|
1388
|
+
...(intent && { intent }),
|
|
1389
|
+
...(rawResponse && { rawResponsePreview: rawResponse.slice(0, 4000) }),
|
|
1390
|
+
}, null, 2);
|
|
1391
|
+
|
|
1392
|
+
if (executionId || rawResponse) {
|
|
1393
|
+
footerChildren.push('copy-btn');
|
|
1394
|
+
components.push({ id: 'copy-btn', component: 'Button', text: 'Copy Debug Info', icon: 'clipboard', 'data-clipboard': debugPayload });
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
components.push(
|
|
1398
|
+
{ id: 'ftr', component: 'Footer', children: footerChildren },
|
|
1399
|
+
{ id: 'retry', component: 'Button', text: 'Try Again', icon: 'arrow-clockwise', variant: 'primary' },
|
|
1400
|
+
);
|
|
1401
|
+
|
|
1402
|
+
const updateMsg = {
|
|
1403
|
+
type: 'updateComponents',
|
|
1404
|
+
surfaceId: 'default',
|
|
1405
|
+
// Marker so validators / scorers / consumers can detect that this is a
|
|
1406
|
+
// generation-failure surface (NOT a successful UI). Without this, the
|
|
1407
|
+
// validator was scoring fallbacks ~89/100 because they're structurally
|
|
1408
|
+
// a valid Card. See diagnosis report 2026-04-19.
|
|
1409
|
+
_fallback: true,
|
|
1410
|
+
_fallbackReason: reason,
|
|
1411
|
+
components: [
|
|
1412
|
+
{ id: 'root', component: 'Card', children },
|
|
1413
|
+
...components,
|
|
1414
|
+
],
|
|
1415
|
+
};
|
|
1416
|
+
|
|
1417
|
+
// ── Wiring: retry button emits a2ui-retry so consumers can re-run generation ──
|
|
1418
|
+
const actions = [
|
|
1419
|
+
{
|
|
1420
|
+
event: { event: 'press', target: 'retry' },
|
|
1421
|
+
handler: 'emit-event',
|
|
1422
|
+
config: {
|
|
1423
|
+
eventName: 'a2ui-retry',
|
|
1424
|
+
detail: { intent: intent || '', mode: mode || 'pro', executionId: executionId || '' },
|
|
1425
|
+
},
|
|
1426
|
+
},
|
|
1427
|
+
];
|
|
1428
|
+
|
|
1429
|
+
const wireMsg = {
|
|
1430
|
+
type: 'wireComponents',
|
|
1431
|
+
surfaceId: 'default',
|
|
1432
|
+
actions,
|
|
1433
|
+
};
|
|
1434
|
+
|
|
1435
|
+
return [updateMsg, wireMsg];
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
/**
|
|
1439
|
+
* Build a repair prompt from failed validation checks.
|
|
1440
|
+
* @param {object[]} failedChecks
|
|
1441
|
+
* @param {object[]} messages
|
|
1442
|
+
* @returns {string}
|
|
1443
|
+
*/
|
|
1444
|
+
export function buildRepairPrompt(failedChecks, messages, catalogFailures = []) {
|
|
1445
|
+
const blocks = [];
|
|
1446
|
+
|
|
1447
|
+
if (failedChecks.length > 0) {
|
|
1448
|
+
const lines = failedChecks.map(c => `- ${c.name}: ${c.detail}`).join('\n');
|
|
1449
|
+
blocks.push(`Heuristic validation errors:\n${lines}`);
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
if (catalogFailures.length > 0) {
|
|
1453
|
+
// Each entry: { id, component, errors: string[] }. Show at most 20 to
|
|
1454
|
+
// keep the prompt bounded; the LLM sees the pattern after a handful.
|
|
1455
|
+
const slice = catalogFailures.slice(0, 20);
|
|
1456
|
+
const lines = slice
|
|
1457
|
+
.map(f => `- ${f.component} id="${f.id}": ${f.errors.slice(0, 3).join('; ')}`)
|
|
1458
|
+
.join('\n');
|
|
1459
|
+
const overflow = catalogFailures.length > 20 ? `\n (+${catalogFailures.length - 20} more — same patterns apply)` : '';
|
|
1460
|
+
blocks.push(`Catalog (v0.9 schema) validation errors:\n${lines}${overflow}\n\nFix by:\n- Removing unknown properties (use declared props only)\n- Aligning enum values (e.g. variant="primary" not variant="main")\n- Using correct types (number fields get numbers, not strings)`);
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
return [
|
|
1464
|
+
`Fix these A2UI validation errors:\n`,
|
|
1465
|
+
blocks.join('\n\n'),
|
|
1466
|
+
`\n\nOriginal output:\n${JSON.stringify(messages)}`,
|
|
1467
|
+
].join('');
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
/**
|
|
1471
|
+
* Generate 2-4 actionable follow-up suggestions based on what's already on
|
|
1472
|
+
* the canvas. Canvas-aware (not domain-aware) so iteration turns don't drift
|
|
1473
|
+
* into unrelated patterns — a login form should never get "add a chart"
|
|
1474
|
+
* suggested, even when the LATEST turn's intent ("add status feedback")
|
|
1475
|
+
* trips the data-domain classifier.
|
|
1476
|
+
*
|
|
1477
|
+
* Selection logic:
|
|
1478
|
+
* 1. Prefer concept-specific extensions for the ORIGINAL canvas concepts
|
|
1479
|
+
* (e.g. "auth" → forgot-password, social sign-in, remember-me).
|
|
1480
|
+
* 2. Fall back to the original analyzer's impliedComponents that aren't
|
|
1481
|
+
* yet present (e.g. "Add Avatar" if the analysis expected one).
|
|
1482
|
+
* 3. Cap at 4. Never propose components that conflict with the canvas's
|
|
1483
|
+
* existing shape (no "data table" for a login form).
|
|
1484
|
+
*
|
|
1485
|
+
* @param {object} opts
|
|
1486
|
+
* @param {string} opts.intent — Current turn's intent (most-recent)
|
|
1487
|
+
* @param {object} opts.domain — Current turn's domain classification
|
|
1488
|
+
* @param {object[]} opts.messages — Current canvas messages
|
|
1489
|
+
* @param {object} [opts.originalAnalysis] — First turn's analyzer output (from store)
|
|
1490
|
+
* @param {string} [opts.originalIntent] — First turn's intent (from store)
|
|
1491
|
+
*/
|
|
1492
|
+
export function generateSuggestions({ intent, domain, messages, originalAnalysis = null, originalIntent = null }) {
|
|
1493
|
+
// Component types currently on canvas
|
|
1494
|
+
const types = new Set();
|
|
1495
|
+
for (const msg of messages || []) {
|
|
1496
|
+
if (msg?.type === 'updateComponents' && Array.isArray(msg.components)) {
|
|
1497
|
+
for (const c of msg.components) if (c?.component) types.add(c.component);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
// Concepts that anchor what kind of canvas this IS. Prefer the original
|
|
1502
|
+
// analysis (durable across iteration turns) over the current intent's
|
|
1503
|
+
// domain (which drifts with each chip click).
|
|
1504
|
+
const concepts = new Set((originalAnalysis?.concepts || []).map(c => c.toLowerCase()));
|
|
1505
|
+
const impliedComponents = new Set(originalAnalysis?.impliedComponents || []);
|
|
1506
|
+
const domainName = domain?.domain || 'layout';
|
|
1507
|
+
|
|
1508
|
+
// Concept-specific extension catalogues. Keys match the kind of tags the
|
|
1509
|
+
// analyzer produces (concepts like "auth", "commerce"). When a concept
|
|
1510
|
+
// matches, suggestions extend the canvas instead of drifting away.
|
|
1511
|
+
const CONCEPT_EXTENSIONS = {
|
|
1512
|
+
'auth': ['Add forgot password link', 'Add Sign in with Google', 'Add Remember me checkbox', 'Add Sign up link'],
|
|
1513
|
+
'authentication': ['Add forgot password link', 'Add Sign in with Google', 'Add Remember me checkbox', 'Add Sign up link'],
|
|
1514
|
+
'login': ['Add forgot password link', 'Add Sign in with Google', 'Add Remember me checkbox', 'Add Sign up link'],
|
|
1515
|
+
'commerce': ['Add price comparison', 'Add product variants picker', 'Add quantity stepper', 'Add Save for later button'],
|
|
1516
|
+
'product-display': ['Add price comparison', 'Add product variants picker', 'Add rating stars', 'Add Save for later button'],
|
|
1517
|
+
'settings': ['Add a Save / Cancel footer', 'Add an account section', 'Add a danger zone (delete account)', 'Add a billing section'],
|
|
1518
|
+
'preferences': ['Add a Save / Cancel footer', 'Add an account section', 'Add a notifications section'],
|
|
1519
|
+
'navigation': ['Add breadcrumbs', 'Add a sidebar', 'Add a search bar'],
|
|
1520
|
+
'data-display': ['Add filter controls', 'Add sort controls', 'Add a row-level action menu', 'Add a CSV export button'],
|
|
1521
|
+
'agent': ['Add a streaming response area', 'Add action buttons', 'Add a citation list'],
|
|
1522
|
+
};
|
|
1523
|
+
|
|
1524
|
+
const suggestions = [];
|
|
1525
|
+
|
|
1526
|
+
// 1. Concept-driven extensions (the high-quality path)
|
|
1527
|
+
for (const concept of concepts) {
|
|
1528
|
+
const ext = CONCEPT_EXTENSIONS[concept];
|
|
1529
|
+
if (ext) {
|
|
1530
|
+
for (const s of ext) if (!suggestions.includes(s)) suggestions.push(s);
|
|
1531
|
+
if (suggestions.length >= 4) break;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
// 2. Fill from analyzer's impliedComponents that aren't on canvas yet.
|
|
1536
|
+
// "The brief said you'd want an Avatar but you don't have one."
|
|
1537
|
+
if (suggestions.length < 4) {
|
|
1538
|
+
for (const c of impliedComponents) {
|
|
1539
|
+
if (!types.has(c)) {
|
|
1540
|
+
const label = `Add ${prettyComponentName(c)}`;
|
|
1541
|
+
if (!suggestions.includes(label)) suggestions.push(label);
|
|
1542
|
+
if (suggestions.length >= 4) break;
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
// 3. Last resort — domain heuristics, but ONLY when no concepts AND no
|
|
1548
|
+
// implied components landed. Original generic catalogue, intentionally
|
|
1549
|
+
// weak so it loses to the better paths above.
|
|
1550
|
+
if (suggestions.length === 0) {
|
|
1551
|
+
if (domainName === 'forms') suggestions.push('Add form validation', 'Add helper text');
|
|
1552
|
+
else if (domainName === 'data') suggestions.push('Add filter controls', 'Add a row-level action menu');
|
|
1553
|
+
else suggestions.push('Add a header', 'Add interactive controls');
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
return suggestions.slice(0, 4);
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
/** "AvatarGroup" → "an avatar group", "Button" → "a button" — friendly suggestion text. */
|
|
1560
|
+
function prettyComponentName(name) {
|
|
1561
|
+
const spaced = String(name).replace(/([a-z])([A-Z])/g, '$1 $2').toLowerCase();
|
|
1562
|
+
const article = /^[aeiou]/i.test(spaced) ? 'an' : 'a';
|
|
1563
|
+
return `${article} ${spaced}`;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
/**
|
|
1567
|
+
* Anti-pattern fold-in for generated output (the gen-UI contact-form
|
|
1568
|
+
* incident, 2026-06-10): validateSchema scores structure but never ran the
|
|
1569
|
+
* anti-pattern registry, so a generated form with a text-ui "label" inside
|
|
1570
|
+
* field-ui, display="none" attributes, and input-ui[type=textarea] badged
|
|
1571
|
+
* a clean score in the canvas.
|
|
1572
|
+
*
|
|
1573
|
+
* Projects the flat component messages into a light HTML sketch (open tags
|
|
1574
|
+
* + attrs, tree-ordered via children id refs) — enough surface for the
|
|
1575
|
+
* regex/function checks in retrieval/anti-patterns.js — runs
|
|
1576
|
+
* checkAllAntiPatterns over it, then folds violations into the validation
|
|
1577
|
+
* object: one warning per violation and a 10-point score dock each
|
|
1578
|
+
* (floored at 0). Mutates + returns `validation`.
|
|
1579
|
+
*
|
|
1580
|
+
* @param {object[]} messages — A2UI messages (flat adjacency components)
|
|
1581
|
+
* @param {{score: number, warnings?: string[]}} validation — from validateSchema
|
|
1582
|
+
* @returns {typeof validation}
|
|
1583
|
+
*/
|
|
1584
|
+
export function foldInAntiPatterns(messages, validation) {
|
|
1585
|
+
try {
|
|
1586
|
+
const components = (messages || []).flatMap((m) => m.components || []);
|
|
1587
|
+
if (!components.length || !validation) return validation;
|
|
1588
|
+
const byId = new Map(components.map((c) => [c.id, c]));
|
|
1589
|
+
const referenced = new Set(components.flatMap((c) => c.children || []));
|
|
1590
|
+
const roots = components.filter((c) => !referenced.has(c.id));
|
|
1591
|
+
const sketch = [];
|
|
1592
|
+
const emit = (c, depth) => {
|
|
1593
|
+
if (!c || depth > 12) return;
|
|
1594
|
+
const tag = resolveTag(c.type) || String(c.type || '').toLowerCase();
|
|
1595
|
+
const attrs = Object.entries(c.props || {})
|
|
1596
|
+
.filter(([, v]) => v !== false && v != null && typeof v !== 'object')
|
|
1597
|
+
.map(([k, v]) => ` ${k}="${String(v).replace(/"/g, '"')}"`)
|
|
1598
|
+
.join('');
|
|
1599
|
+
sketch.push(`<${tag}${attrs}>`);
|
|
1600
|
+
if (typeof (c.props || {}).textContent === 'string') sketch.push((c.props || {}).textContent);
|
|
1601
|
+
for (const id of c.children || []) emit(byId.get(id), depth + 1);
|
|
1602
|
+
sketch.push(`</${tag}>`);
|
|
1603
|
+
};
|
|
1604
|
+
for (const r of roots) emit(r, 0);
|
|
1605
|
+
const violations = checkAllAntiPatterns(sketch.join('\n'));
|
|
1606
|
+
if (violations.length) {
|
|
1607
|
+
validation.warnings = [
|
|
1608
|
+
...(validation.warnings || []),
|
|
1609
|
+
...violations.map((v) => `anti-pattern ${v.name}: ${v.description} Fix: ${v.fix}`),
|
|
1610
|
+
];
|
|
1611
|
+
validation.antiPatterns = violations.map((v) => v.name);
|
|
1612
|
+
validation.score = Math.max(0, (validation.score || 0) - 10 * violations.length);
|
|
1613
|
+
}
|
|
1614
|
+
return validation;
|
|
1615
|
+
} catch {
|
|
1616
|
+
// The fold-in must never break generation — fail open.
|
|
1617
|
+
return validation;
|
|
1618
|
+
}
|
|
1619
|
+
}
|