@digithings-ai/ui 0.0.0-stage → 0.1.0
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/README.md +48 -2
- package/package.json +143 -4
- package/src/components/NavShell.tsx +776 -0
- package/src/components/SocialRow.tsx +87 -0
- package/src/components/Spinner.tsx +27 -0
- package/src/components/StackLogo.tsx +40 -0
- package/src/components/Terminal.tsx +110 -0
- package/src/components/TerminalManifest.tsx +212 -0
- package/src/components/ThemeProvider.tsx +271 -0
- package/src/components/account/AuthCard.tsx +309 -0
- package/src/components/assistant-ui/elements/mermaid-diagram.aui.tsx +63 -0
- package/src/components/assistant-ui/elements/mermaid-diagram.tsx +340 -0
- package/src/components/brand/BrandWordmark.tsx +32 -0
- package/src/components/changelog/ReleaseRail.tsx +81 -0
- package/src/components/changelog/index.ts +1 -0
- package/src/components/chat/ChatCodeBlock.tsx +67 -0
- package/src/components/chat/ChatMarkdown.tsx +66 -0
- package/src/components/chat/ChatMarkdownSource.tsx +203 -0
- package/src/components/chat/ChatMermaidBlock.tsx +202 -0
- package/src/components/chat/ChatMessage.tsx +77 -0
- package/src/components/chat/ChatPlayback.tsx +422 -0
- package/src/components/chat/ChatStreamCursor.tsx +25 -0
- package/src/components/chat/ChatThinking.tsx +113 -0
- package/src/components/chat/ChatToolCall.tsx +190 -0
- package/src/components/chat/ChatTranscript.tsx +60 -0
- package/src/components/chat/ChatWidgetFrame.tsx +107 -0
- package/src/components/chat/DigichatBootLoader.tsx +683 -0
- package/src/components/chat/DigichatLauncher.tsx +361 -0
- package/src/components/chat/DigichatThread.tsx +118 -0
- package/src/components/chat/DigichatThreadList.tsx +54 -0
- package/src/components/chat/DigichatWordmark.tsx +24 -0
- package/src/components/chat/DotMatrix.tsx +650 -0
- package/src/components/chat/boot-lab.tsx +766 -0
- package/src/components/chat/boot-signal.ts +144 -0
- package/src/components/chat/chat-surfaces.ts +52 -0
- package/src/components/chat/connection-status.ts +40 -0
- package/src/components/chat/digichat-wordmark.ts +355 -0
- package/src/components/chat/disclosure-chevron.tsx +49 -0
- package/src/components/chat/followable-href.ts +9 -0
- package/src/components/chat/gallery-thread/action-icons.tsx +25 -0
- package/src/components/chat/gallery-thread/attachment.aui.tsx +262 -0
- package/src/components/chat/gallery-thread/block-caret.tsx +439 -0
- package/src/components/chat/gallery-thread/cn.ts +1 -0
- package/src/components/chat/gallery-thread/composer-keyboard.ts +106 -0
- package/src/components/chat/gallery-thread/composer-trigger-popover.aui.tsx +235 -0
- package/src/components/chat/gallery-thread/css.d.ts +6 -0
- package/src/components/chat/gallery-thread/file.tsx +238 -0
- package/src/components/chat/gallery-thread/follow-up-suggestions.aui.tsx +92 -0
- package/src/components/chat/gallery-thread/format-json-dump.ts +112 -0
- package/src/components/chat/gallery-thread/hooks/use-attachment-src.ts +46 -0
- package/src/components/chat/gallery-thread/hooks/use-copy-to-clipboard.ts +9 -0
- package/src/components/chat/gallery-thread/image.tsx +519 -0
- package/src/components/chat/gallery-thread/markdown-text.tsx +278 -0
- package/src/components/chat/gallery-thread/message-error.aui.tsx +105 -0
- package/src/components/chat/gallery-thread/message-timing.aui.tsx +143 -0
- package/src/components/chat/gallery-thread/private-work-step.tsx +188 -0
- package/src/components/chat/gallery-thread/reasoning.tsx +332 -0
- package/src/components/chat/gallery-thread/scroll-mode.ts +174 -0
- package/src/components/chat/gallery-thread/source.tsx +76 -0
- package/src/components/chat/gallery-thread/thread.aui.tsx +1619 -0
- package/src/components/chat/gallery-thread/tooltip-icon-button.tsx +53 -0
- package/src/components/chat/gallery-thread/typed-welcome-copy.tsx +18 -0
- package/src/components/chat/gallery-thread/ui/tooltip.tsx +22 -0
- package/src/components/chat/gallery-thread/work-chain-summary.tsx +48 -0
- package/src/components/chat/gallery-thread/work-chain.tsx +110 -0
- package/src/components/chat/gallery-thread/work-status.ts +66 -0
- package/src/components/chat/overlay-scrollbar.tsx +86 -0
- package/src/components/chat/sanitize-mermaid-svg.ts +38 -0
- package/src/components/chat/skins/SOURCE.md +36 -0
- package/src/components/chat/skins/base/attachment.aui.tsx +287 -0
- package/src/components/chat/skins/base/aui-default/README.md +45 -0
- package/src/components/chat/skins/base/aui-default/collapsible-root.tsx +19 -0
- package/src/components/chat/skins/base/aui-default/collapsible.tsx +21 -0
- package/src/components/chat/skins/base/aui-default/reasoning.aui.tsx +120 -0
- package/src/components/chat/skins/base/aui-default/reasoning.tsx +331 -0
- package/src/components/chat/skins/base/aui-default/surfaces.ts +7 -0
- package/src/components/chat/skins/base/aui-default/tool-fallback.aui.tsx +772 -0
- package/src/components/chat/skins/base/aui-default/tool-group.aui.tsx +241 -0
- package/src/components/chat/skins/base/docs-model-options.ts +6 -0
- package/src/components/chat/skins/base/elements/assistant-trace.aui.tsx +169 -0
- package/src/components/chat/skins/base/elements/composer-trigger-popover.aui.tsx +271 -0
- package/src/components/chat/skins/base/elements/directive-text.aui.tsx +29 -0
- package/src/components/chat/skins/base/elements/directive-text.tsx +76 -0
- package/src/components/chat/skins/base/elements/message-timing.aui.tsx +104 -0
- package/src/components/chat/skins/base/elements/model-selector.aui.tsx +140 -0
- package/src/components/chat/skins/base/elements/model-selector.tsx +720 -0
- package/src/components/chat/skins/base/elements/quote.aui.tsx +290 -0
- package/src/components/chat/skins/base/elements/work-chain.tsx +100 -0
- package/src/components/chat/skins/base/markdown-text.tsx +294 -0
- package/src/components/chat/skins/base/model.ts +1 -0
- package/src/components/chat/skins/base/thread.tsx +1050 -0
- package/src/components/chat/skins/base/tooltip-icon-button.tsx +56 -0
- package/src/components/chat/skins/base/ui/avatar.tsx +115 -0
- package/src/components/chat/skins/base/ui/badge.tsx +51 -0
- package/src/components/chat/skins/base/ui/button.tsx +67 -0
- package/src/components/chat/skins/base/ui/command.tsx +185 -0
- package/src/components/chat/skins/base/ui/dialog.tsx +168 -0
- package/src/components/chat/skins/base/ui/dot-matrix.tsx +267 -0
- package/src/components/chat/skins/base/ui/popover.tsx +97 -0
- package/src/components/chat/skins/base/ui/skeleton.tsx +20 -0
- package/src/components/chat/skins/base/ui/textarea.tsx +24 -0
- package/src/components/chat/skins/base/ui/tooltip.tsx +73 -0
- package/src/components/chat/skins/base-assistant-ui/config-provider.tsx +90 -0
- package/src/components/chat/skins/base-assistant-ui/defaults.ts +419 -0
- package/src/components/chat/skins/base-assistant-ui/index.tsx +44 -0
- package/src/components/chat/skins/chatgpt.tsx +448 -0
- package/src/components/chat/skins/claude.tsx +380 -0
- package/src/components/chat/skins/digichat-composer-toolbar.tsx +151 -0
- package/src/components/chat/skins/digichat-context-meter-shares.ts +158 -0
- package/src/components/chat/skins/digichat-context-meter.tsx +243 -0
- package/src/components/chat/skins/digichat-meter-tone.ts +26 -0
- package/src/components/chat/skins/digichat-usage-meter.tsx +103 -0
- package/src/components/chat/skins/digichat.tsx +144 -0
- package/src/components/chat/skins/expo-react-native.tsx +29 -0
- package/src/components/chat/skins/gemini.tsx +322 -0
- package/src/components/chat/skins/grok-icon.tsx +34 -0
- package/src/components/chat/skins/grok.tsx +369 -0
- package/src/components/chat/skins/index.ts +46 -0
- package/src/components/chat/skins/perplexity.tsx +462 -0
- package/src/components/chat/skins/product-page-assistant/assistant-modal.tsx +73 -0
- package/src/components/chat/skins/product-page-assistant/index.tsx +19 -0
- package/src/components/chat/skins/product-page-assistant/product-dashboard.tsx +164 -0
- package/src/components/chat/skins/react-ink.tsx +97 -0
- package/src/components/chat/skins/thread-skin-host-contract.ts +102 -0
- package/src/components/chat/skins/thread-skin-view.tsx +156 -0
- package/src/components/chat/skins/thread-skins.ts +143 -0
- package/src/components/chat/skins/tooltip-icon-button.tsx +35 -0
- package/src/components/chat/skins/ui/dropdown-menu.tsx +272 -0
- package/src/components/chat/skins/webpage-assistant/assistant-config.ts +238 -0
- package/src/components/chat/skins/webpage-assistant/assistant-modal.tsx +78 -0
- package/src/components/chat/skins/webpage-assistant/config-provider.tsx +132 -0
- package/src/components/chat/skins/webpage-assistant/docs-app.tsx +106 -0
- package/src/components/chat/skins/webpage-assistant/docs-article.tsx +109 -0
- package/src/components/chat/skins/webpage-assistant/docs-assistant.tsx +73 -0
- package/src/components/chat/skins/webpage-assistant/docs-nav.tsx +81 -0
- package/src/components/chat/skins/webpage-assistant/host-ui.ts +79 -0
- package/src/components/chat/skins/webpage-assistant/index.tsx +12 -0
- package/src/components/chat/skins/webpage-assistant/markdown-page.tsx +11 -0
- package/src/components/chat/skins/webpage-assistant/pages.ts +9 -0
- package/src/components/chat/skins/webpage-assistant/types.ts +178 -0
- package/src/components/chat/skins/webpage-assistant/use-mobile.ts +19 -0
- package/src/components/chat/stock/SOURCE.md +90 -0
- package/src/components/chat/stock/artifacts-panel.tsx +305 -0
- package/src/components/chat/stock/attachment.aui.tsx +280 -0
- package/src/components/chat/stock/chat-notice.tsx +143 -0
- package/src/components/chat/stock/chat-private-mode.tsx +64 -0
- package/src/components/chat/stock/deploy-ui-context.tsx +89 -0
- package/src/components/chat/stock/embed-chat-prefs.tsx +471 -0
- package/src/components/chat/stock/file.tsx +265 -0
- package/src/components/chat/stock/follow-up-suggestions.aui.tsx +83 -0
- package/src/components/chat/stock/image.tsx +531 -0
- package/src/components/chat/stock/index.ts +129 -0
- package/src/components/chat/stock/json-view.tsx +140 -0
- package/src/components/chat/stock/markdown-text.tsx +319 -0
- package/src/components/chat/stock/message-error.aui.tsx +96 -0
- package/src/components/chat/stock/message-export-menu.tsx +156 -0
- package/src/components/chat/stock/palette-row-preview.tsx +26 -0
- package/src/components/chat/stock/reasoning.aui.tsx +120 -0
- package/src/components/chat/stock/reasoning.tsx +335 -0
- package/src/components/chat/stock/skin-chrome.tsx +81 -0
- package/src/components/chat/stock/skin-credit.tsx +63 -0
- package/src/components/chat/stock/skin-palette-bridge.tsx +235 -0
- package/src/components/chat/stock/skin-palette-context.ts +16 -0
- package/src/components/chat/stock/skin-runtime.tsx +52 -0
- package/src/components/chat/stock/skin-slash-palette.tsx +393 -0
- package/src/components/chat/stock/skin-types.ts +217 -0
- package/src/components/chat/stock/stock-chat-prefs-host.tsx +501 -0
- package/src/components/chat/stock/stock-send-gate.tsx +94 -0
- package/src/components/chat/stock/stock-shape.tsx +39 -0
- package/src/components/chat/stock/thread.aui.tsx +645 -0
- package/src/components/chat/stock/tool-display.ts +6 -0
- package/src/components/chat/stock/tool-fallback.aui.tsx +866 -0
- package/src/components/chat/stock/tool-group.aui.tsx +244 -0
- package/src/components/chat/stock/tool-surface.ts +120 -0
- package/src/components/chat/stock/tooltip-icon-button.tsx +49 -0
- package/src/components/chat/stock/ui/avatar.tsx +108 -0
- package/src/components/chat/stock/ui/button.tsx +81 -0
- package/src/components/chat/stock/ui/collapsible.tsx +21 -0
- package/src/components/chat/stock/ui/dialog.tsx +163 -0
- package/src/components/chat/stock/ui/skeleton.tsx +13 -0
- package/src/components/chat/stock/ui/textarea.tsx +20 -0
- package/src/components/chat/stock/ui/tooltip.tsx +68 -0
- package/src/components/chat/stock/use-attachment-src.ts +42 -0
- package/src/components/chat/stock/use-copy-to-clipboard.ts +29 -0
- package/src/components/chat/strip-model-boilerplate.ts +42 -0
- package/src/components/chat/suggestion-cycle.tsx +230 -0
- package/src/components/chat/suggestion-scroll-row.tsx +81 -0
- package/src/components/chat/trace-grouping/chain-group-by.ts +44 -0
- package/src/components/chat/trace-grouping/chain-state.ts +141 -0
- package/src/components/chat/trace-grouping/index.ts +32 -0
- package/src/components/chat/trace-grouping/tool-row.ts +74 -0
- package/src/components/chat/trace-grouping/use-deploy-work-disclosure.ts +35 -0
- package/src/components/chat/trace-grouping/use-work-chain.ts +103 -0
- package/src/components/chat/trace-i18n/core.ts +48 -0
- package/src/components/chat/trace-i18n/index.tsx +23 -0
- package/src/components/chat/trace-i18n/locales/en.ts +24 -0
- package/src/components/chat/trace-i18n/locales/fr.ts +28 -0
- package/src/components/chat/trace-i18n/types.ts +35 -0
- package/src/components/chat/trace-spacing.ts +249 -0
- package/src/components/chat/transcript/activity-view.ts +375 -0
- package/src/components/chat/transcript/chat-export.ts +133 -0
- package/src/components/chat/transcript/index.ts +10 -0
- package/src/components/chat/transcript/save-file.ts +209 -0
- package/src/components/chat/transcript/transcript-markdown.ts +428 -0
- package/src/components/chat/transcript/types.ts +81 -0
- package/src/components/chat/turn-outcome.ts +52 -0
- package/src/components/chat/use-warmup-phase.ts +18 -0
- package/src/components/chat/work-status-lead.tsx +49 -0
- package/src/components/chrome.tsx +264 -0
- package/src/components/command-palette/CommandPalette.tsx +404 -0
- package/src/components/command-palette/index.ts +6 -0
- package/src/components/contact/ContactMailto.tsx +112 -0
- package/src/components/contact/index.ts +5 -0
- package/src/components/conviction/ConvictionMeter.tsx +40 -0
- package/src/components/conviction/SignedConvictionBadge.tsx +26 -0
- package/src/components/conviction/index.ts +7 -0
- package/src/components/data-layout/BentoGrid.tsx +52 -0
- package/src/components/data-layout/CardRail.tsx +150 -0
- package/src/components/data-layout/DotMatrixStat.tsx +65 -0
- package/src/components/data-layout/FeatureCell.tsx +80 -0
- package/src/components/data-layout/MediaFrame.tsx +125 -0
- package/src/components/data-layout/ModuleGrid.tsx +735 -0
- package/src/components/data-layout/Odometer.tsx +149 -0
- package/src/components/data-layout/ProductFrame.tsx +79 -0
- package/src/components/data-layout/TestimonialWall.tsx +139 -0
- package/src/components/data-layout/index.ts +19 -0
- package/src/components/data-layout/treemap.ts +368 -0
- package/src/components/deck/DeckStack.tsx +243 -0
- package/src/components/desk/DataTable.tsx +373 -0
- package/src/components/desk/KpiGrid.tsx +80 -0
- package/src/components/desk/Sparkline.tsx +63 -0
- package/src/components/desk/StateBlock.tsx +132 -0
- package/src/components/desk/index.ts +18 -0
- package/src/components/diagrams/ArchitectureDiagram.tsx +350 -0
- package/src/components/diagrams/ArchitectureTour.tsx +912 -0
- package/src/components/diagrams/TerminalSchematic.tsx +192 -0
- package/src/components/diagrams/architecture-svg.tsx +384 -0
- package/src/components/diagrams/index.ts +30 -0
- package/src/components/diagrams/mermaid-theme.ts +115 -0
- package/src/components/diagrams/tour-camera.ts +171 -0
- package/src/components/docs/CodeTabs.tsx +90 -0
- package/src/components/docs/CopyCommand.tsx +151 -0
- package/src/components/docs/DocsLayout.tsx +197 -0
- package/src/components/docs/DocsSearch.tsx +97 -0
- package/src/components/docs/Endpoint.tsx +135 -0
- package/src/components/document/DocumentFrame.tsx +43 -0
- package/src/components/document/GlyphList.tsx +51 -0
- package/src/components/document/PageTitle.tsx +44 -0
- package/src/components/document/Prose.tsx +38 -0
- package/src/components/document/Section.tsx +55 -0
- package/src/components/document/index.ts +13 -0
- package/src/components/effects-chrome/AnnouncementBar.tsx +88 -0
- package/src/components/effects-chrome/HorizontalScrollTrack.tsx +461 -0
- package/src/components/effects-chrome/Pipeline.tsx +226 -0
- package/src/components/effects-chrome/RotatingPrompts.tsx +116 -0
- package/src/components/effects-chrome/StackingPanels.tsx +101 -0
- package/src/components/effects-chrome/TabStrip.tsx +232 -0
- package/src/components/effects-chrome/ToastStack.tsx +201 -0
- package/src/components/effects-chrome/horizontal-track-core.ts +70 -0
- package/src/components/effects-chrome/index.ts +35 -0
- package/src/components/emblems.tsx +188 -0
- package/src/components/faviconTheme.ts +23 -0
- package/src/components/figure/Figure.tsx +38 -0
- package/src/components/figure/index.ts +2 -0
- package/src/components/finance-charts/DrawdownPlot.tsx +76 -0
- package/src/components/finance-charts/EquityCurve.tsx +71 -0
- package/src/components/finance-charts/PriceChart.tsx +119 -0
- package/src/components/finance-charts/chart-host.tsx +236 -0
- package/src/components/finance-charts/chart-scale.ts +93 -0
- package/src/components/finance-charts/demo-data.ts +88 -0
- package/src/components/finance-charts/index.ts +46 -0
- package/src/components/finance-charts/lw-chart.tsx +292 -0
- package/src/components/finance-composites/MarketBar.tsx +243 -0
- package/src/components/finance-composites/OrderBook.tsx +133 -0
- package/src/components/finance-composites/PerformanceDashboard.tsx +185 -0
- package/src/components/finance-composites/SortableTable.tsx +144 -0
- package/src/components/finance-composites/StockTicker.tsx +65 -0
- package/src/components/finance-composites/SyncedTearsheet.tsx +226 -0
- package/src/components/finance-composites/index.ts +28 -0
- package/src/components/finance-tearsheet/KpiStrip.tsx +49 -0
- package/src/components/finance-tearsheet/LiveBadge.tsx +29 -0
- package/src/components/finance-tearsheet/ReturnsMatrix.tsx +321 -0
- package/src/components/finance-tearsheet/StrategyTearsheet.tsx +99 -0
- package/src/components/finance-tearsheet/TearsheetCard.tsx +54 -0
- package/src/components/finance-tearsheet/TradeLogTable.tsx +73 -0
- package/src/components/finance-tearsheet/charts.tsx +2624 -0
- package/src/components/finance-tearsheet/demo-data.ts +177 -0
- package/src/components/finance-tearsheet/format.ts +41 -0
- package/src/components/finance-tearsheet/index.ts +120 -0
- package/src/components/finance-tearsheet/print.ts +52 -0
- package/src/components/finance-tearsheet/risk-bands.ts +48 -0
- package/src/components/finance-tearsheet/stats.ts +35 -0
- package/src/components/finance-tearsheet/strategy-sheet-model.ts +238 -0
- package/src/components/finance-tearsheet/types.ts +58 -0
- package/src/components/footer/FooterCells.tsx +109 -0
- package/src/components/footer/index.ts +6 -0
- package/src/components/graph.tsx +158 -0
- package/src/components/icons.tsx +98 -0
- package/src/components/landing/PixelWordmark.tsx +36 -0
- package/src/components/logos.ts +39 -0
- package/src/components/marquee/Marquee.tsx +204 -0
- package/src/components/metrics/PerfMetrics.tsx +68 -0
- package/src/components/metrics/StatCounter.tsx +104 -0
- package/src/components/page-geometry.tsx +177 -0
- package/src/components/pricing/Pricing.tsx +174 -0
- package/src/components/pricing/PricingMatrix.tsx +219 -0
- package/src/components/prose/index.ts +2 -0
- package/src/components/prose/prose.tsx +86 -0
- package/src/components/repo-activity/RepoActivity.tsx +599 -0
- package/src/components/repo-activity/RepoHeatmap.tsx +225 -0
- package/src/components/repo-activity/demo.ts +201 -0
- package/src/components/repo-activity/fetch.ts +211 -0
- package/src/components/repo-activity/heatmap.ts +123 -0
- package/src/components/repo-activity/index.ts +28 -0
- package/src/components/repo-activity/types.ts +99 -0
- package/src/components/stages/NumberedStages.tsx +116 -0
- package/src/components/symbols/marks.tsx +116 -0
- package/src/components/symbols/terminal-lockup.tsx +136 -0
- package/src/components/symbols/terminal-marks.tsx +226 -0
- package/src/components/terminal/ChatResponseLoader.tsx +67 -0
- package/src/components/terminal/ContainerBootLoader.tsx +82 -0
- package/src/components/terminal/TerminalStepCaret.tsx +202 -0
- package/src/components/themeInit.ts +24 -0
- package/src/components/typography/MotionReveal.tsx +108 -0
- package/src/components/typography/TypewriterReveal.tsx +101 -0
- package/src/components/typography/WordReveal.tsx +143 -0
- package/src/data/modules.ts +378 -0
- package/src/data/subsystems.ts +101 -0
- package/src/index.ts +587 -0
- package/src/lib/base-path.ts +60 -0
- package/src/lib/cx.ts +25 -0
- package/src/lib/gloomberb.ts +69 -0
- package/src/lib/record.ts +19 -0
- package/src/lib/stub-payload.ts +183 -0
- package/src/lib/useBodyScrollLock.ts +44 -0
- package/src/lib/utils.ts +6 -0
- package/src/motion/primitives.tsx +105 -0
- package/src/motion/scroll-glide.ts +62 -0
- package/src/motion/scrolly-core.ts +38 -0
- package/src/motion/scrolly.tsx +95 -0
- package/src/navigation/HashScrollManager.tsx +167 -0
- package/src/navigation/hashScroll.ts +78 -0
- package/src/replay/browser.ts +280 -0
- package/src/replay/chat-export-run.ts +121 -0
- package/src/replay/export-format.ts +25 -0
- package/src/replay/export-mount.ts +32 -0
- package/src/replay/filename.ts +54 -0
- package/src/replay/index.ts +21 -0
- package/src/replay/redact-dom.ts +178 -0
- package/src/replay/redact.ts +113 -0
- package/src/replay/snapshot.ts +659 -0
- package/src/styles/account-auth.css +339 -0
- package/src/styles/brand-wordmark.css +11 -0
- package/src/styles/card-rail.css +120 -0
- package/src/styles/chat-aui.css +1143 -0
- package/src/styles/chat-core.css +469 -0
- package/src/styles/chat-digichat.css +2531 -0
- package/src/styles/chat-math.css +70 -0
- package/src/styles/chat-skin-palette.css +138 -0
- package/src/styles/chat-stock-menu.css +37 -0
- package/src/styles/chat-widgets.css +190 -0
- package/src/styles/command-palette.css +372 -0
- package/src/styles/controls-core.css +2261 -0
- package/src/styles/controls-overlay.css +603 -0
- package/src/styles/data-layout.css +288 -0
- package/src/styles/deck.css +141 -0
- package/src/styles/diagrams.css +1860 -0
- package/src/styles/digichat-app-theme.css +250 -0
- package/src/styles/digichat-boot-loader.css +487 -0
- package/src/styles/digichat-launcher.css +377 -0
- package/src/styles/docs.css +165 -0
- package/src/styles/effects-chrome.css +888 -0
- package/src/styles/finance-composites.css +214 -0
- package/src/styles/finance-tearsheet-overlay.css +90 -0
- package/src/styles/finance-tearsheet.css +945 -0
- package/src/styles/marquee.css +168 -0
- package/src/styles/metrics.css +82 -0
- package/src/styles/nav-shell.css +551 -0
- package/src/styles/pricing.css +79 -0
- package/src/styles/repo-activity.css +575 -0
- package/src/styles/stages.css +69 -0
- package/src/styles/terminal-loaders.css +122 -0
- package/src/styles/terminal-manifest.css +228 -0
- package/src/styles/web-theme.css +1007 -0
- package/src/styles/word-reveal.css +204 -0
- package/src/theme/apply-theme.ts +214 -0
- package/src/theme/index.ts +27 -0
- package/src/theme/palette-registry.ts +855 -0
- package/src/theme/tui-prefs.ts +64 -0
- package/src/ui/alert.tsx +78 -0
- package/src/ui/avatar.tsx +104 -0
- package/src/ui/badge.tsx +75 -0
- package/src/ui/breadcrumb.tsx +61 -0
- package/src/ui/button.tsx +67 -0
- package/src/ui/card.tsx +139 -0
- package/src/ui/checkbox.tsx +27 -0
- package/src/ui/collapsible.tsx +32 -0
- package/src/ui/date-pager.tsx +325 -0
- package/src/ui/dialog.tsx +157 -0
- package/src/ui/dropdown-menu.tsx +267 -0
- package/src/ui/empty-state.tsx +165 -0
- package/src/ui/field.tsx +104 -0
- package/src/ui/form.tsx +36 -0
- package/src/ui/icon-button.tsx +36 -0
- package/src/ui/index.ts +34 -0
- package/src/ui/input.tsx +33 -0
- package/src/ui/kbd.tsx +30 -0
- package/src/ui/label.tsx +32 -0
- package/src/ui/pager.tsx +119 -0
- package/src/ui/pagination.tsx +126 -0
- package/src/ui/radio-group.tsx +51 -0
- package/src/ui/search-bar.tsx +85 -0
- package/src/ui/segmented-control.tsx +76 -0
- package/src/ui/select.tsx +257 -0
- package/src/ui/separator.tsx +24 -0
- package/src/ui/sheet.tsx +141 -0
- package/src/ui/skeleton.tsx +76 -0
- package/src/ui/slider.tsx +76 -0
- package/src/ui/switch.tsx +31 -0
- package/src/ui/table.tsx +179 -0
- package/src/ui/tabs.tsx +89 -0
- package/src/ui/tags-input.tsx +168 -0
- package/src/ui/textarea.tsx +17 -0
- package/src/ui/tooltip.tsx +71 -0
|
@@ -0,0 +1,1619 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ComposerAddAttachment,
|
|
5
|
+
ComposerAttachments,
|
|
6
|
+
HiddenAttachmentNamesProvider,
|
|
7
|
+
UserMessageAttachments,
|
|
8
|
+
useHiddenAttachmentNames,
|
|
9
|
+
} from "./attachment.aui";
|
|
10
|
+
import {
|
|
11
|
+
composerHasText,
|
|
12
|
+
dismissComposerKeyboard,
|
|
13
|
+
dismissKeyboardIfCoarse,
|
|
14
|
+
isCoarsePointer,
|
|
15
|
+
} from "./composer-keyboard";
|
|
16
|
+
import { File } from "./file";
|
|
17
|
+
import { ThreadFollowupSuggestions } from "./follow-up-suggestions.aui";
|
|
18
|
+
import { Image } from "./image";
|
|
19
|
+
import { MarkdownText } from "./markdown-text";
|
|
20
|
+
import {
|
|
21
|
+
Reasoning,
|
|
22
|
+
ReasoningContent,
|
|
23
|
+
ReasoningRoot,
|
|
24
|
+
ReasoningText,
|
|
25
|
+
ReasoningTrigger,
|
|
26
|
+
} from "../stock/reasoning.aui";
|
|
27
|
+
import { MessageTiming } from "./message-timing.aui";
|
|
28
|
+
import {
|
|
29
|
+
DEFAULT_SCROLL_MODE,
|
|
30
|
+
scrollModeProps,
|
|
31
|
+
useFollowStream,
|
|
32
|
+
type ScrollMode,
|
|
33
|
+
} from "./scroll-mode";
|
|
34
|
+
import { WorkChain } from "./work-chain";
|
|
35
|
+
import {
|
|
36
|
+
chainFollowedByText,
|
|
37
|
+
chainGroupBy,
|
|
38
|
+
chainHoldsWork,
|
|
39
|
+
chainToolCount,
|
|
40
|
+
messageHasContent,
|
|
41
|
+
messageHasWork,
|
|
42
|
+
reasoningGroupDisclosure,
|
|
43
|
+
toolGroupDisclosure,
|
|
44
|
+
} from "../trace-grouping";
|
|
45
|
+
import { WorkChainSummary } from "./work-chain-summary";
|
|
46
|
+
import { PrivateToolCallStep, PrivateWorkStep } from "./private-work-step";
|
|
47
|
+
import {
|
|
48
|
+
resolveWorkDisclosure,
|
|
49
|
+
type WorkVisibility,
|
|
50
|
+
} from "../stock/skin-types";
|
|
51
|
+
import { BackendNotice } from "../stock/chat-notice";
|
|
52
|
+
import { ToolFallback } from "../stock/tool-fallback.aui";
|
|
53
|
+
import {
|
|
54
|
+
ToolGroupContent,
|
|
55
|
+
ToolGroupRoot,
|
|
56
|
+
ToolGroupTrigger,
|
|
57
|
+
} from "../stock/tool-group.aui";
|
|
58
|
+
import {
|
|
59
|
+
CheckActionIcon,
|
|
60
|
+
CopyActionIcon,
|
|
61
|
+
} from "./action-icons";
|
|
62
|
+
import { TooltipIconButton } from "./tooltip-icon-button";
|
|
63
|
+
import { MessageExportMenu } from "../stock/message-export-menu";
|
|
64
|
+
import { Button } from "../../../ui/button";
|
|
65
|
+
import { DotMatrix } from "../DotMatrix";
|
|
66
|
+
import { connectionStatusText } from "../connection-status";
|
|
67
|
+
import { WorkStatusLead } from "../work-status-lead";
|
|
68
|
+
import { Skeleton } from "../../../ui/skeleton";
|
|
69
|
+
import { cn } from "./cn";
|
|
70
|
+
import { useSuggestionGroup } from "../suggestion-cycle";
|
|
71
|
+
import { SkinCredit } from "../stock/skin-credit";
|
|
72
|
+
import {
|
|
73
|
+
ActionBarPrimitive,
|
|
74
|
+
AuiIf,
|
|
75
|
+
type AssistantState,
|
|
76
|
+
BranchPickerPrimitive,
|
|
77
|
+
ComposerPrimitive,
|
|
78
|
+
MessagePrimitive,
|
|
79
|
+
SuggestionPrimitive,
|
|
80
|
+
ThreadPrimitive,
|
|
81
|
+
type FileMessagePartComponent,
|
|
82
|
+
type ImageMessagePartComponent,
|
|
83
|
+
type SourceMessagePartComponent,
|
|
84
|
+
type ToolCallMessagePartComponent,
|
|
85
|
+
useAui,
|
|
86
|
+
useAuiState,
|
|
87
|
+
} from "@assistant-ui/react";
|
|
88
|
+
import {
|
|
89
|
+
createContext,
|
|
90
|
+
useCallback,
|
|
91
|
+
useContext,
|
|
92
|
+
useEffect,
|
|
93
|
+
useMemo,
|
|
94
|
+
useRef,
|
|
95
|
+
useState,
|
|
96
|
+
type ComponentPropsWithoutRef,
|
|
97
|
+
type ComponentType,
|
|
98
|
+
type FC,
|
|
99
|
+
type FormEvent,
|
|
100
|
+
type MouseEvent as ReactMouseEvent,
|
|
101
|
+
type PropsWithChildren,
|
|
102
|
+
type ReactNode,
|
|
103
|
+
} from "react";
|
|
104
|
+
import { ComposerTriggerPopover } from "./composer-trigger-popover.aui";
|
|
105
|
+
import { MessageError } from "./message-error.aui";
|
|
106
|
+
import { ComposerBlockCaret } from "./block-caret";
|
|
107
|
+
import { TypedWelcomeCopy } from "./typed-welcome-copy";
|
|
108
|
+
import { useDigichatWarmupPhase } from "../use-warmup-phase";
|
|
109
|
+
import { OverlayScrollbar } from "../overlay-scrollbar";
|
|
110
|
+
|
|
111
|
+
export type ThreadGroupPart = MessagePrimitive.GroupedParts.GroupPart;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Disclosure behavior for a grouped chain of reasoning / tool-call steps.
|
|
115
|
+
* `off` hides the group entirely, `collapsed` (default) renders a closed
|
|
116
|
+
* dropdown, `balanced` opens while the group is still streaming and
|
|
117
|
+
* auto-collapses when it completes, `expanded` starts open, and
|
|
118
|
+
* `locked_open` pins it open.
|
|
119
|
+
*/
|
|
120
|
+
export type GroupDisclosureMode =
|
|
121
|
+
| "off"
|
|
122
|
+
| "collapsed"
|
|
123
|
+
| "balanced"
|
|
124
|
+
| "expanded"
|
|
125
|
+
| "locked_open";
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Optional component overrides for the thread. `AssistantMessage` and
|
|
129
|
+
* `Welcome` replace whole sections; the remaining slots override how the
|
|
130
|
+
* assistant message renders tool calls and part groups. Tool UIs registered
|
|
131
|
+
* by name (toolkit `render`, `useAssistantDataUI`) take precedence over
|
|
132
|
+
* `ToolFallback`.
|
|
133
|
+
*/
|
|
134
|
+
export type ThreadComponents = {
|
|
135
|
+
AssistantMessage?: ComponentType | undefined;
|
|
136
|
+
Welcome?: ComponentType | undefined;
|
|
137
|
+
ToolFallback?: ToolCallMessagePartComponent | undefined;
|
|
138
|
+
/** Citation row (`source` parts): provider web search, RAG documents (#4552). */
|
|
139
|
+
Source?: SourceMessagePartComponent | undefined;
|
|
140
|
+
/**
|
|
141
|
+
* Optional footer slot below the composer; absent means no footer.
|
|
142
|
+
*/
|
|
143
|
+
Footer?: ComponentType | undefined;
|
|
144
|
+
ToolGroup?:
|
|
145
|
+
| ComponentType<PropsWithChildren<{ group: ThreadGroupPart }>>
|
|
146
|
+
| undefined;
|
|
147
|
+
ReasoningGroup?:
|
|
148
|
+
| ComponentType<PropsWithChildren<{ group: ThreadGroupPart }>>
|
|
149
|
+
| undefined;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/** Expanded: input, then a row of attach + send. Compact: attach | input | send. */
|
|
153
|
+
export type ComposerLayout = "expanded" | "compact";
|
|
154
|
+
|
|
155
|
+
export type ThreadActions = {
|
|
156
|
+
/** Restore previous assistant branch. Default on. */
|
|
157
|
+
undo?: boolean | undefined;
|
|
158
|
+
/** Thumbs up / down. Off when absent. */
|
|
159
|
+
feedback?: boolean | undefined;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export type ThreadProps = {
|
|
163
|
+
components?: ThreadComponents | undefined;
|
|
164
|
+
autoFocus?: boolean | undefined;
|
|
165
|
+
/** `chrome.placeholder` in deploy YAML. */
|
|
166
|
+
placeholder?: string | undefined;
|
|
167
|
+
/** `chrome.composerLayout`. Default expanded (toolbar under the input). */
|
|
168
|
+
composerLayout?: ComposerLayout | undefined;
|
|
169
|
+
/** `features.undo` / `features.feedback`. */
|
|
170
|
+
actions?: ThreadActions | undefined;
|
|
171
|
+
/** `chrome.welcome.title`. Used by the default Welcome slot. */
|
|
172
|
+
welcome?: string | undefined;
|
|
173
|
+
/** `chrome.welcome.body`. */
|
|
174
|
+
welcomeBody?: readonly string[] | undefined;
|
|
175
|
+
/**
|
|
176
|
+
* `chrome.suggestions` in deploy YAML. Static prompts for the new-chat
|
|
177
|
+
* welcome state; when non-empty they take precedence over the runtime's own
|
|
178
|
+
* suggestion adapter, which is the fallback.
|
|
179
|
+
*/
|
|
180
|
+
suggestions?: readonly string[] | undefined;
|
|
181
|
+
className?: string | undefined;
|
|
182
|
+
/** Embed send-gate / system page-context attach. */
|
|
183
|
+
onComposerSubmit?: (event: FormEvent<HTMLFormElement>) => void;
|
|
184
|
+
/**
|
|
185
|
+
* Native assistant-ui slash adapter (`unstable_useSlashCommandAdapter`).
|
|
186
|
+
* Mounted when the product skin has session prefs (embed, modal, and app).
|
|
187
|
+
*/
|
|
188
|
+
slash?: ThreadSlashTrigger | undefined;
|
|
189
|
+
/** Native `@` mention adapter (`unstable_useMentionAdapter`). */
|
|
190
|
+
mention?: ThreadMentionTrigger | undefined;
|
|
191
|
+
/**
|
|
192
|
+
* Attachment names that render no chip in the composer or sent messages.
|
|
193
|
+
* The attachment still lives on the runtime (its file part reaches the
|
|
194
|
+
* model) — only the UI is suppressed. Generic; callers pass system names
|
|
195
|
+
* such as the embed's `page-context.html` in `silent` mode.
|
|
196
|
+
*/
|
|
197
|
+
hiddenAttachmentNames?: readonly string[] | undefined;
|
|
198
|
+
/** Disclosure mode for grouped reasoning runs. Default collapsed. */
|
|
199
|
+
reasoningMode?: GroupDisclosureMode | undefined;
|
|
200
|
+
/** Disclosure mode for grouped tool-call runs. Default collapsed. */
|
|
201
|
+
toolCallsMode?: GroupDisclosureMode | undefined;
|
|
202
|
+
/**
|
|
203
|
+
* How much of the agent's work the user sees: `quiet` (the titles only,
|
|
204
|
+
* with the folded header as a live status line), `balanced` (open while it
|
|
205
|
+
* runs, collapsed when it finishes) or `verbose` (stays open). The client's
|
|
206
|
+
* `features.view` picks the default; the user can change it per session.
|
|
207
|
+
*/
|
|
208
|
+
workVisibility?: WorkVisibility | undefined;
|
|
209
|
+
/**
|
|
210
|
+
* Private session. Reasoning text and tool-call payloads are user data, so
|
|
211
|
+
* the rows show the step (kind, tool name, state) in the normal chain but
|
|
212
|
+
* never open — while streaming as well as at rest. Default off; other
|
|
213
|
+
* hosts are unaffected.
|
|
214
|
+
*/
|
|
215
|
+
privateMode?: boolean | undefined;
|
|
216
|
+
/**
|
|
217
|
+
* Rendered as a row inside the composer border, under the input. The
|
|
218
|
+
* digichat skin passes its model / tools / search / view / context chips;
|
|
219
|
+
* other hosts leave it unset.
|
|
220
|
+
*/
|
|
221
|
+
composerToolbar?: ReactNode;
|
|
222
|
+
/** `always` reserves a scrollbar gutter; `auto` shows bars only on scroll. */
|
|
223
|
+
scrollbars?: "always" | "auto";
|
|
224
|
+
/** Worked-chain rhythm. `default` is the fixed spacing. */
|
|
225
|
+
density?: "compact" | "default" | "airy";
|
|
226
|
+
/**
|
|
227
|
+
* `follow` anchors the sent question at the top and chases the stream;
|
|
228
|
+
* `free` never moves the viewport. See `scroll-mode.ts`.
|
|
229
|
+
*/
|
|
230
|
+
scrollMode?: ScrollMode;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/** `{ adapter, action }` from `unstable_useSlashCommandAdapter`. */
|
|
234
|
+
type SlashPopoverProps = ComponentPropsWithoutRef<typeof ComposerTriggerPopover>;
|
|
235
|
+
export type ThreadSlashTrigger = {
|
|
236
|
+
adapter: SlashPopoverProps["adapter"];
|
|
237
|
+
action: NonNullable<SlashPopoverProps["action"]>;
|
|
238
|
+
iconMap?: SlashPopoverProps["iconMap"];
|
|
239
|
+
matcher?: SlashPopoverProps["matcher"];
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
export type ThreadMentionTrigger = {
|
|
243
|
+
adapter: SlashPopoverProps["adapter"];
|
|
244
|
+
directive: NonNullable<SlashPopoverProps["directive"]>;
|
|
245
|
+
iconMap?: SlashPopoverProps["iconMap"];
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const DEFAULT_THREAD_ACTIONS: Required<ThreadActions> = {
|
|
249
|
+
undo: true,
|
|
250
|
+
feedback: false,
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const ThreadActionsContext = createContext<Required<ThreadActions>>(
|
|
254
|
+
DEFAULT_THREAD_ACTIONS,
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
const EMPTY_COMPONENTS: ThreadComponents = {};
|
|
258
|
+
|
|
259
|
+
const ThreadComponentsContext =
|
|
260
|
+
createContext<ThreadComponents>(EMPTY_COMPONENTS);
|
|
261
|
+
|
|
262
|
+
type ThreadGroupDisclosure = {
|
|
263
|
+
reasoning: GroupDisclosureMode;
|
|
264
|
+
toolCalls: GroupDisclosureMode;
|
|
265
|
+
visibility: WorkVisibility;
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const DEFAULT_GROUP_DISCLOSURE: ThreadGroupDisclosure = {
|
|
269
|
+
reasoning: "collapsed",
|
|
270
|
+
toolCalls: "collapsed",
|
|
271
|
+
visibility: "balanced",
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const ThreadGroupDisclosureContext = createContext<ThreadGroupDisclosure>(
|
|
275
|
+
DEFAULT_GROUP_DISCLOSURE,
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Private session. The work renders exactly as in a normal session — the same
|
|
280
|
+
* "Worked" chain, the same "N tool calls" group, the same rows, labels and
|
|
281
|
+
* icons, under the same `reasoningMode` / `toolCallsMode` / `workVisibility`
|
|
282
|
+
* rules — except that each reasoning and tool row is a `PrivateWorkStep`,
|
|
283
|
+
* which cannot be opened: the reasoning text and tool payloads are stripped
|
|
284
|
+
* server side and must not mount here either. Default false, so every skin
|
|
285
|
+
* that does not pass `privateMode` renders exactly as before.
|
|
286
|
+
*/
|
|
287
|
+
const ThreadPrivateModeContext = createContext(false);
|
|
288
|
+
|
|
289
|
+
type ThreadChrome = {
|
|
290
|
+
welcome: string;
|
|
291
|
+
welcomeBody: readonly string[];
|
|
292
|
+
suggestions: readonly string[];
|
|
293
|
+
onComposerSubmit?: (event: FormEvent<HTMLFormElement>) => void;
|
|
294
|
+
className?: string;
|
|
295
|
+
slash?: ThreadSlashTrigger;
|
|
296
|
+
mention?: ThreadMentionTrigger;
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const EMPTY_SUGGESTIONS: readonly string[] = [];
|
|
300
|
+
|
|
301
|
+
const DEFAULT_CHROME: ThreadChrome = {
|
|
302
|
+
welcome: "How can I help you today?",
|
|
303
|
+
welcomeBody: [],
|
|
304
|
+
suggestions: EMPTY_SUGGESTIONS,
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const ThreadChromeContext = createContext<ThreadChrome>(DEFAULT_CHROME);
|
|
308
|
+
|
|
309
|
+
/** The scroll viewport element, for footer widgets that live outside it. */
|
|
310
|
+
const ThreadViewportElContext = createContext<HTMLElement | null>(null);
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Edit-in-composer: clicking edit on a user message loads its text into the
|
|
314
|
+
* main composer (the only place messages are typed or changed). Resubmitting
|
|
315
|
+
* replaces that message and regenerates from it; unchanged text is a no-op
|
|
316
|
+
* (treated as a misclick), as is an empty or whitespace-only composer.
|
|
317
|
+
* Escape or a click outside the composer cancels and restores whatever draft
|
|
318
|
+
* was there before. The composer itself carries no editing chrome: it looks
|
|
319
|
+
* like a normal text box while a message is being edited.
|
|
320
|
+
*/
|
|
321
|
+
type ComposerEditState = {
|
|
322
|
+
messageId: string;
|
|
323
|
+
original: string;
|
|
324
|
+
savedDraft: string;
|
|
325
|
+
};
|
|
326
|
+
type ComposerEditApi = {
|
|
327
|
+
editing: ComposerEditState | null;
|
|
328
|
+
begin: (messageId: string, text: string) => void;
|
|
329
|
+
cancel: () => void;
|
|
330
|
+
commit: (text: string) => void;
|
|
331
|
+
};
|
|
332
|
+
const ComposerEditContext = createContext<ComposerEditApi>({
|
|
333
|
+
editing: null,
|
|
334
|
+
begin: () => {},
|
|
335
|
+
cancel: () => {},
|
|
336
|
+
commit: () => {},
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
export function isUnchangedEdit(original: string, next: string): boolean {
|
|
340
|
+
return original.trim() === next.trim();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Should an in-composer edit be sent at all?
|
|
345
|
+
*
|
|
346
|
+
* No when the composer is empty or holds only whitespace (there is nothing to
|
|
347
|
+
* say), and no when the text is the message being edited (an edit that
|
|
348
|
+
* changes nothing is not worth a resend). Both cases end the edit without
|
|
349
|
+
* touching the message.
|
|
350
|
+
*/
|
|
351
|
+
export function shouldCommitEdit(original: string, next: string): boolean {
|
|
352
|
+
if (!next.trim()) return false;
|
|
353
|
+
return !isUnchangedEdit(original, next);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const ComposerEditProvider: FC<PropsWithChildren> = ({ children }) => {
|
|
357
|
+
const aui = useAui();
|
|
358
|
+
const [editing, setEditing] = useState<ComposerEditState | null>(null);
|
|
359
|
+
const editingRef = useRef<ComposerEditState | null>(null);
|
|
360
|
+
editingRef.current = editing;
|
|
361
|
+
|
|
362
|
+
const begin = useCallback(
|
|
363
|
+
(messageId: string, text: string) => {
|
|
364
|
+
const prev = editingRef.current;
|
|
365
|
+
const savedDraft = prev ? prev.savedDraft : aui.composer().getState().text;
|
|
366
|
+
aui.composer().setText(text);
|
|
367
|
+
setEditing({ messageId, original: text, savedDraft });
|
|
368
|
+
},
|
|
369
|
+
[aui],
|
|
370
|
+
);
|
|
371
|
+
const cancel = useCallback(() => {
|
|
372
|
+
const cur = editingRef.current;
|
|
373
|
+
if (!cur) return;
|
|
374
|
+
aui.composer().setText(cur.savedDraft);
|
|
375
|
+
setEditing(null);
|
|
376
|
+
}, [aui]);
|
|
377
|
+
const commit = useCallback(
|
|
378
|
+
(text: string) => {
|
|
379
|
+
const cur = editingRef.current;
|
|
380
|
+
if (!cur) return;
|
|
381
|
+
if (!shouldCommitEdit(cur.original, text)) {
|
|
382
|
+
cancel();
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const mc = aui.thread().message({ id: cur.messageId }).composer();
|
|
386
|
+
mc.beginEdit();
|
|
387
|
+
mc.setText(text);
|
|
388
|
+
mc.send();
|
|
389
|
+
aui.composer().setText(cur.savedDraft);
|
|
390
|
+
setEditing(null);
|
|
391
|
+
},
|
|
392
|
+
[aui, cancel],
|
|
393
|
+
);
|
|
394
|
+
const value = useMemo(
|
|
395
|
+
() => ({ editing, begin, cancel, commit }),
|
|
396
|
+
[editing, begin, cancel, commit],
|
|
397
|
+
);
|
|
398
|
+
return (
|
|
399
|
+
<ComposerEditContext.Provider value={value}>
|
|
400
|
+
{children}
|
|
401
|
+
</ComposerEditContext.Provider>
|
|
402
|
+
);
|
|
403
|
+
};
|
|
404
|
+
// Startup exposes a loading placeholder thread; treat it as a new chat so
|
|
405
|
+
// Welcome docks in the footer above the composer. Loads after startup keep
|
|
406
|
+
// the docked layout.
|
|
407
|
+
const isNewChatView = (s: AssistantState) =>
|
|
408
|
+
s.thread.messages.length === 0 &&
|
|
409
|
+
(!s.thread.isLoading || s.threads.isLoading);
|
|
410
|
+
|
|
411
|
+
// A switched thread that is still fetching its history: skeleton, not welcome.
|
|
412
|
+
const isHistoryLoadingView = (s: AssistantState) =>
|
|
413
|
+
s.thread.messages.length === 0 &&
|
|
414
|
+
s.thread.isLoading &&
|
|
415
|
+
!s.thread.isDisabled &&
|
|
416
|
+
!s.threads.isLoading;
|
|
417
|
+
|
|
418
|
+
const ThreadHistorySkeleton: FC = () => (
|
|
419
|
+
<div
|
|
420
|
+
data-slot="aui_thread-history-skeleton"
|
|
421
|
+
role="status"
|
|
422
|
+
className="animate-in fade-in fill-mode-both flex flex-col gap-y-6 [animation-delay:150ms] [animation-duration:200ms]"
|
|
423
|
+
>
|
|
424
|
+
<span className="sr-only">Loading conversation</span>
|
|
425
|
+
<Skeleton variant="block" className="ms-auto h-9 w-2/5 rounded-none motion-reduce:animate-none" />
|
|
426
|
+
<div className="flex flex-col gap-y-2">
|
|
427
|
+
<Skeleton variant="line" className="h-4 w-11/12 motion-reduce:animate-none" />
|
|
428
|
+
<Skeleton variant="line" className="h-4 w-4/5 motion-reduce:animate-none" />
|
|
429
|
+
<Skeleton variant="line" className="h-4 w-3/5 motion-reduce:animate-none" />
|
|
430
|
+
</div>
|
|
431
|
+
<Skeleton variant="block" className="ms-auto h-9 w-1/3 rounded-none motion-reduce:animate-none" />
|
|
432
|
+
<div className="flex flex-col gap-y-2">
|
|
433
|
+
<Skeleton variant="line" className="h-4 w-10/12 motion-reduce:animate-none" />
|
|
434
|
+
<Skeleton variant="line" className="h-4 w-2/3 motion-reduce:animate-none" />
|
|
435
|
+
</div>
|
|
436
|
+
</div>
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
const EMPTY_HIDDEN_ATTACHMENTS: readonly string[] = [];
|
|
440
|
+
|
|
441
|
+
export const Thread: FC<ThreadProps> = ({
|
|
442
|
+
components = EMPTY_COMPONENTS,
|
|
443
|
+
autoFocus = true,
|
|
444
|
+
placeholder,
|
|
445
|
+
composerLayout = "expanded",
|
|
446
|
+
actions,
|
|
447
|
+
welcome,
|
|
448
|
+
welcomeBody,
|
|
449
|
+
suggestions,
|
|
450
|
+
className,
|
|
451
|
+
onComposerSubmit,
|
|
452
|
+
slash,
|
|
453
|
+
mention,
|
|
454
|
+
hiddenAttachmentNames,
|
|
455
|
+
reasoningMode = "collapsed",
|
|
456
|
+
toolCallsMode = "collapsed",
|
|
457
|
+
workVisibility = "balanced",
|
|
458
|
+
privateMode = false,
|
|
459
|
+
composerToolbar,
|
|
460
|
+
scrollbars = "always",
|
|
461
|
+
density = "default",
|
|
462
|
+
scrollMode = DEFAULT_SCROLL_MODE,
|
|
463
|
+
}) => {
|
|
464
|
+
const resolvedActions: Required<ThreadActions> = {
|
|
465
|
+
undo: actions?.undo !== false,
|
|
466
|
+
feedback: actions?.feedback === true,
|
|
467
|
+
};
|
|
468
|
+
const chrome: ThreadChrome = {
|
|
469
|
+
welcome: welcome ?? DEFAULT_CHROME.welcome,
|
|
470
|
+
welcomeBody: welcomeBody ?? DEFAULT_CHROME.welcomeBody,
|
|
471
|
+
suggestions: suggestions ?? EMPTY_SUGGESTIONS,
|
|
472
|
+
onComposerSubmit,
|
|
473
|
+
className,
|
|
474
|
+
slash,
|
|
475
|
+
mention,
|
|
476
|
+
};
|
|
477
|
+
return (
|
|
478
|
+
<HiddenAttachmentNamesProvider names={hiddenAttachmentNames ?? EMPTY_HIDDEN_ATTACHMENTS}>
|
|
479
|
+
<ThreadChromeContext.Provider value={chrome}>
|
|
480
|
+
<ThreadActionsContext.Provider value={resolvedActions}>
|
|
481
|
+
<ThreadComponentsContext.Provider value={components}>
|
|
482
|
+
<ThreadGroupDisclosureContext.Provider
|
|
483
|
+
value={{
|
|
484
|
+
reasoning: reasoningMode,
|
|
485
|
+
toolCalls: toolCallsMode,
|
|
486
|
+
visibility: workVisibility,
|
|
487
|
+
}}
|
|
488
|
+
>
|
|
489
|
+
<ComposerEditProvider>
|
|
490
|
+
<ThreadPrivateModeContext.Provider value={privateMode}>
|
|
491
|
+
<ThreadRoot
|
|
492
|
+
autoFocus={autoFocus}
|
|
493
|
+
placeholder={placeholder}
|
|
494
|
+
composerLayout={composerLayout}
|
|
495
|
+
composerToolbar={composerToolbar}
|
|
496
|
+
scrollbars={scrollbars}
|
|
497
|
+
density={density}
|
|
498
|
+
scrollMode={scrollMode}
|
|
499
|
+
/>
|
|
500
|
+
</ThreadPrivateModeContext.Provider>
|
|
501
|
+
</ComposerEditProvider>
|
|
502
|
+
</ThreadGroupDisclosureContext.Provider>
|
|
503
|
+
</ThreadComponentsContext.Provider>
|
|
504
|
+
</ThreadActionsContext.Provider>
|
|
505
|
+
</ThreadChromeContext.Provider>
|
|
506
|
+
</HiddenAttachmentNamesProvider>
|
|
507
|
+
);
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Default branding footer: "powered by digichat" in normal flow below the
|
|
512
|
+
* composer in every state. This footer's `sticky bottom-0 mt-auto` docks it
|
|
513
|
+
* to the page bottom unconditionally, so in-flow is already bottom-pinned —
|
|
514
|
+
* an absolute overlay would paint over the composer (it did; the textbox
|
|
515
|
+
* cut the credit off in app, product, and embed modes). Hosts can still
|
|
516
|
+
* override the whole Footer slot.
|
|
517
|
+
*/
|
|
518
|
+
const ThreadFooter: FC = () => <SkinCredit />;
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Keeps the viewport on the stream while following. A component rather than a
|
|
522
|
+
* hook call in `ThreadRoot` because the message count lives in the thread
|
|
523
|
+
* store, which only exists inside `ThreadPrimitive.Root`.
|
|
524
|
+
*/
|
|
525
|
+
const ScrollFollower: FC<{ viewport: HTMLElement | null; enabled: boolean }> = ({
|
|
526
|
+
viewport,
|
|
527
|
+
enabled,
|
|
528
|
+
}) => {
|
|
529
|
+
const messageCount = useAuiState((s) => s.thread.messages.length);
|
|
530
|
+
useFollowStream(viewport, enabled, messageCount);
|
|
531
|
+
return null;
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
const ThreadRoot: FC<{
|
|
535
|
+
autoFocus: boolean;
|
|
536
|
+
placeholder?: string | undefined;
|
|
537
|
+
composerLayout: ComposerLayout;
|
|
538
|
+
composerToolbar?: ReactNode;
|
|
539
|
+
scrollbars: "always" | "auto";
|
|
540
|
+
density: "compact" | "default" | "airy";
|
|
541
|
+
scrollMode: ScrollMode;
|
|
542
|
+
}> = ({ autoFocus, placeholder, composerLayout, composerToolbar, scrollbars, density, scrollMode }) => {
|
|
543
|
+
const { Welcome = ThreadWelcome, Footer = ThreadFooter } =
|
|
544
|
+
useContext(ThreadComponentsContext);
|
|
545
|
+
const { className } = useContext(ThreadChromeContext);
|
|
546
|
+
const [viewportEl, setViewportEl] = useState<HTMLElement | null>(null);
|
|
547
|
+
const viewportRef = useRef<HTMLElement | null>(null);
|
|
548
|
+
const attachViewport = useCallback((node: HTMLElement | null) => {
|
|
549
|
+
viewportRef.current = node;
|
|
550
|
+
setViewportEl(node);
|
|
551
|
+
}, []);
|
|
552
|
+
|
|
553
|
+
const viewportProps = scrollModeProps(scrollMode);
|
|
554
|
+
|
|
555
|
+
return (
|
|
556
|
+
<ThreadPrimitive.Root
|
|
557
|
+
className={cn(
|
|
558
|
+
"aui-root aui-thread-root digichat-thread bg-surface @container flex h-full flex-col",
|
|
559
|
+
className,
|
|
560
|
+
)}
|
|
561
|
+
data-user-align="left"
|
|
562
|
+
// The skin scope marker: `chat-digichat.css` carries the whole
|
|
563
|
+
// first-party theme under `:is(.aui-theme-stage, [data-thread-skin="digichat"])`
|
|
564
|
+
// and the portal mirrors under `html:has([data-thread-skin="digichat"])`.
|
|
565
|
+
// Declaring it here means every mount path gets the same theme — the
|
|
566
|
+
// /baseline catalog, the embed shell, and the production shells — instead
|
|
567
|
+
// of each surface having to remember to wrap the Thread.
|
|
568
|
+
data-thread-skin="digichat"
|
|
569
|
+
data-scrollbars={scrollbars}
|
|
570
|
+
data-density={density}
|
|
571
|
+
style={{
|
|
572
|
+
["--thread-max-width" as string]: "44rem",
|
|
573
|
+
// `--card` (not `--color-card`) so a scoped palette wins: the
|
|
574
|
+
// Tailwind bridge declares `--color-card` at :root, so its var()
|
|
575
|
+
// resolves there and ignores `.digichat-thread`'s own `--card`.
|
|
576
|
+
["--composer-bg" as string]: "var(--card)",
|
|
577
|
+
["--composer-radius" as string]: "0",
|
|
578
|
+
// `--composer-padding` is a density token (chat-aui.css), so it is not
|
|
579
|
+
// pinned inline here — an inline value would win over the mode.
|
|
580
|
+
}}
|
|
581
|
+
>
|
|
582
|
+
{/* The footer (composer + credit) sits outside the scroll viewport so
|
|
583
|
+
messages end above the text box. ViewportProvider keeps the
|
|
584
|
+
scroll-to-bottom control wired to the viewport store. */}
|
|
585
|
+
<ThreadPrimitive.ViewportProvider>
|
|
586
|
+
<ThreadViewportElContext.Provider value={viewportEl}>
|
|
587
|
+
<div className="relative flex min-h-0 flex-1 flex-col">
|
|
588
|
+
<ThreadPrimitive.Viewport
|
|
589
|
+
ref={attachViewport}
|
|
590
|
+
{...viewportProps}
|
|
591
|
+
// The top-anchored message carries `padding-top: var(--density-gap-4)` (chat-aui.css, taken back as margin)
|
|
592
|
+
// so it does not sit flush against the viewport top. `tallerThan` scores
|
|
593
|
+
// the anchor's offsetHeight, which includes that padding, so it is
|
|
594
|
+
// raised by the same 1.5rem to keep the set of fully-pinned messages
|
|
595
|
+
// unchanged from before the padding was added.
|
|
596
|
+
topAnchorMessageClamp={{ tallerThan: "11.5em", visibleHeight: "6em" }}
|
|
597
|
+
data-slot="aui_thread-viewport"
|
|
598
|
+
className="relative flex min-h-0 flex-1 flex-col overflow-x-hidden overflow-y-scroll scroll-smooth digichat-thread__viewport"
|
|
599
|
+
>
|
|
600
|
+
<ScrollFollower viewport={viewportEl} enabled={scrollMode === "follow"} />
|
|
601
|
+
<div className="mx-auto flex w-full max-w-(--thread-max-width) flex-1 flex-col px-4 pt-4">
|
|
602
|
+
<AuiIf condition={isHistoryLoadingView}>
|
|
603
|
+
<ThreadHistorySkeleton />
|
|
604
|
+
</AuiIf>
|
|
605
|
+
|
|
606
|
+
<div
|
|
607
|
+
data-slot="aui_message-group"
|
|
608
|
+
className="flex flex-col empty:hidden"
|
|
609
|
+
>
|
|
610
|
+
<ThreadPrimitive.Messages>
|
|
611
|
+
{() => <ThreadMessage />}
|
|
612
|
+
</ThreadPrimitive.Messages>
|
|
613
|
+
</div>
|
|
614
|
+
</div>
|
|
615
|
+
</ThreadPrimitive.Viewport>
|
|
616
|
+
<OverlayScrollbar targetRef={viewportRef} width={10} />
|
|
617
|
+
</div>
|
|
618
|
+
<div className="mx-auto flex w-full max-w-(--thread-max-width) shrink-0 flex-col px-4">
|
|
619
|
+
<div className="aui-thread-viewport-footer digichat-thread__footer bg-surface relative flex flex-col gap-4 overflow-visible pt-2 pb-2 md:pb-3">
|
|
620
|
+
<ThreadScrollToBottom />
|
|
621
|
+
<ThreadFollowupSuggestions />
|
|
622
|
+
<AuiIf condition={isNewChatView}>
|
|
623
|
+
<div
|
|
624
|
+
data-slot="aui_thread-empty"
|
|
625
|
+
className="aui-thread-empty flex flex-col gap-3"
|
|
626
|
+
>
|
|
627
|
+
<Welcome />
|
|
628
|
+
<ThreadSuggestions />
|
|
629
|
+
</div>
|
|
630
|
+
</AuiIf>
|
|
631
|
+
<Composer
|
|
632
|
+
autoFocus={autoFocus}
|
|
633
|
+
placeholder={placeholder}
|
|
634
|
+
layout={composerLayout}
|
|
635
|
+
toolbar={composerToolbar}
|
|
636
|
+
/>
|
|
637
|
+
{Footer ? <Footer /> : null}
|
|
638
|
+
</div>
|
|
639
|
+
</div>
|
|
640
|
+
</ThreadViewportElContext.Provider>
|
|
641
|
+
</ThreadPrimitive.ViewportProvider>
|
|
642
|
+
</ThreadPrimitive.Root>
|
|
643
|
+
);
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
const ThreadMessage: FC = () => {
|
|
647
|
+
const { AssistantMessage: AssistantMessageComponent = AssistantMessage } =
|
|
648
|
+
useContext(ThreadComponentsContext);
|
|
649
|
+
const role = useAuiState((s) => s.message.role);
|
|
650
|
+
// Edits happen in the main composer (ComposerEditProvider); a message's own
|
|
651
|
+
// edit composer is only opened transiently to submit, so never render it.
|
|
652
|
+
if (role === "user") return <UserMessage />;
|
|
653
|
+
return <AssistantMessageComponent />;
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
const ThreadScrollToBottom: FC = () => {
|
|
657
|
+
const viewportEl = useContext(ThreadViewportElContext);
|
|
658
|
+
const [revealSettled, setRevealSettled] = useState(false);
|
|
659
|
+
const [scrolledAway, setScrolledAway] = useState(false);
|
|
660
|
+
|
|
661
|
+
// Arm only after the bottom-up entrance has finished - the button used to
|
|
662
|
+
// pop in mid-animation.
|
|
663
|
+
useEffect(() => {
|
|
664
|
+
const owner = viewportEl?.closest("[data-stock-product]");
|
|
665
|
+
if (!owner) {
|
|
666
|
+
setRevealSettled(true);
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
670
|
+
const arm = () => {
|
|
671
|
+
if (owner.getAttribute("data-boot-reveal") !== "true") return;
|
|
672
|
+
observer.disconnect();
|
|
673
|
+
timer = setTimeout(() => setRevealSettled(true), 950);
|
|
674
|
+
};
|
|
675
|
+
const observer = new MutationObserver(arm);
|
|
676
|
+
observer.observe(owner, {
|
|
677
|
+
attributes: true,
|
|
678
|
+
attributeFilter: ["data-boot-reveal"],
|
|
679
|
+
});
|
|
680
|
+
arm();
|
|
681
|
+
return () => {
|
|
682
|
+
observer.disconnect();
|
|
683
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
684
|
+
};
|
|
685
|
+
}, [viewportEl]);
|
|
686
|
+
|
|
687
|
+
// Show only when content is actually hidden under the composer: the message
|
|
688
|
+
// list's bottom must sit below the top edge of the composer shell. A scroll
|
|
689
|
+
// of a few pixels with everything still visible (e.g. after the entrance
|
|
690
|
+
// settle) must not surface it.
|
|
691
|
+
useEffect(() => {
|
|
692
|
+
if (!viewportEl) return;
|
|
693
|
+
let observedGroup: HTMLElement | null = null;
|
|
694
|
+
let observedComposer: HTMLElement | null = null;
|
|
695
|
+
const measure = () => {
|
|
696
|
+
// Re-query each run: neither node is guaranteed to be mounted yet (and
|
|
697
|
+
// either may remount), so a captured reference could go stale.
|
|
698
|
+
const group = viewportEl.querySelector<HTMLElement>(
|
|
699
|
+
'[data-slot="aui_message-group"]',
|
|
700
|
+
);
|
|
701
|
+
// The composer sits below the scroll viewport, on the thread root.
|
|
702
|
+
const composer = (
|
|
703
|
+
viewportEl.closest(".aui-thread-root") ?? viewportEl
|
|
704
|
+
).querySelector<HTMLElement>('[data-slot="aui_composer-shell"]');
|
|
705
|
+
if (!group || !composer) {
|
|
706
|
+
setScrolledAway(false);
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
if (group !== observedGroup) {
|
|
710
|
+
if (observedGroup) ro.unobserve(observedGroup);
|
|
711
|
+
ro.observe(group);
|
|
712
|
+
observedGroup = group;
|
|
713
|
+
}
|
|
714
|
+
if (composer !== observedComposer) {
|
|
715
|
+
if (observedComposer) ro.unobserve(observedComposer);
|
|
716
|
+
ro.observe(composer);
|
|
717
|
+
observedComposer = composer;
|
|
718
|
+
}
|
|
719
|
+
setScrolledAway(
|
|
720
|
+
group.getBoundingClientRect().bottom >
|
|
721
|
+
composer.getBoundingClientRect().top + 1,
|
|
722
|
+
);
|
|
723
|
+
};
|
|
724
|
+
const ro = new ResizeObserver(measure);
|
|
725
|
+
measure();
|
|
726
|
+
viewportEl.addEventListener("scroll", measure, { passive: true });
|
|
727
|
+
ro.observe(viewportEl);
|
|
728
|
+
return () => {
|
|
729
|
+
viewportEl.removeEventListener("scroll", measure);
|
|
730
|
+
ro.disconnect();
|
|
731
|
+
};
|
|
732
|
+
}, [viewportEl]);
|
|
733
|
+
|
|
734
|
+
if (!revealSettled || !scrolledAway) return null;
|
|
735
|
+
|
|
736
|
+
return (
|
|
737
|
+
<ThreadPrimitive.ScrollToBottom asChild>
|
|
738
|
+
<TooltipIconButton
|
|
739
|
+
tooltip="Scroll to bottom"
|
|
740
|
+
variant="outline"
|
|
741
|
+
className="aui-thread-scroll-to-bottom border-hair bg-surface hover:bg-surface-2 absolute -top-12 z-10 self-center rounded-none p-4 disabled:invisible"
|
|
742
|
+
>
|
|
743
|
+
<DotMatrix state="scroll" decorative className="size-3.5" />
|
|
744
|
+
</TooltipIconButton>
|
|
745
|
+
</ThreadPrimitive.ScrollToBottom>
|
|
746
|
+
);
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
const ThreadWelcome: FC = () => {
|
|
750
|
+
const { welcome, welcomeBody } = useContext(ThreadChromeContext);
|
|
751
|
+
return (
|
|
752
|
+
<div
|
|
753
|
+
data-slot="aui_thread-welcome"
|
|
754
|
+
className="aui-thread-welcome-root flex flex-col items-start text-start"
|
|
755
|
+
>
|
|
756
|
+
<h1 className="aui-thread-welcome-message-inner fade-in slide-in-from-bottom-1 animate-in fill-mode-both text-2xl font-medium tracking-tight duration-(--duration-hover)">
|
|
757
|
+
{welcome}
|
|
758
|
+
</h1>
|
|
759
|
+
<TypedWelcomeCopy lines={welcomeBody} />
|
|
760
|
+
</div>
|
|
761
|
+
);
|
|
762
|
+
};
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Shared look for a welcome prompt row. The runtime-backed item and the static
|
|
766
|
+
* fallback render the same markup so a skin's examples look identical whether
|
|
767
|
+
* they came from the runtime or from `chrome.suggestions`.
|
|
768
|
+
*/
|
|
769
|
+
const SUGGESTION_BUTTON_CLASS =
|
|
770
|
+
"aui-thread-welcome-suggestion fade-in slide-in-from-bottom-1 animate-in fill-mode-both grid w-full cursor-pointer grid-cols-[1.25rem_minmax(0,1fr)] items-start gap-x-[0.55rem] rounded-none border-0 bg-transparent px-0 py-0.5 text-start text-sm font-normal duration-(--duration-hover)";
|
|
771
|
+
|
|
772
|
+
const ThreadSuggestions: FC = () => {
|
|
773
|
+
const { suggestions } = useContext(ThreadChromeContext);
|
|
774
|
+
// Keyed by the cycle's group offset so a new group remounts and re-runs the
|
|
775
|
+
// chips' entrance fade; hover/focus handlers hold the cycle while it is read.
|
|
776
|
+
const { groupKey, handlers } = useSuggestionGroup();
|
|
777
|
+
return (
|
|
778
|
+
<div
|
|
779
|
+
key={groupKey}
|
|
780
|
+
className="aui-thread-welcome-suggestions flex w-full flex-col items-stretch gap-0.5"
|
|
781
|
+
{...handlers}
|
|
782
|
+
>
|
|
783
|
+
{suggestions.length > 0 ? (
|
|
784
|
+
suggestions.map((prompt, index) => (
|
|
785
|
+
<StaticSuggestionItem key={`${index}:${prompt}`} prompt={prompt} />
|
|
786
|
+
))
|
|
787
|
+
) : (
|
|
788
|
+
<ThreadPrimitive.Suggestions>
|
|
789
|
+
{() => <ThreadSuggestionItem />}
|
|
790
|
+
</ThreadPrimitive.Suggestions>
|
|
791
|
+
)}
|
|
792
|
+
</div>
|
|
793
|
+
);
|
|
794
|
+
};
|
|
795
|
+
|
|
796
|
+
/** Static `chrome.suggestions` prompt. Appends the text like a typed message. */
|
|
797
|
+
const StaticSuggestionItem: FC<{ prompt: string }> = ({ prompt }) => {
|
|
798
|
+
const aui = useAui();
|
|
799
|
+
return (
|
|
800
|
+
<button
|
|
801
|
+
type="button"
|
|
802
|
+
onClick={() => {
|
|
803
|
+
if (aui.thread.getState().isRunning) return;
|
|
804
|
+
// A tapped example is a send: on iOS a button does not take focus, so
|
|
805
|
+
// the composer would keep the keyboard up over the answer.
|
|
806
|
+
dismissKeyboardIfCoarse();
|
|
807
|
+
aui.thread.append({
|
|
808
|
+
content: [{ type: "text", text: prompt }],
|
|
809
|
+
runConfig: aui.composer.getState().runConfig,
|
|
810
|
+
});
|
|
811
|
+
}}
|
|
812
|
+
className={SUGGESTION_BUTTON_CLASS}
|
|
813
|
+
>
|
|
814
|
+
<span className="aui-msg-marker" aria-hidden="true">
|
|
815
|
+
<DotMatrix
|
|
816
|
+
state="example"
|
|
817
|
+
decorative
|
|
818
|
+
className="aui-thread-welcome-suggestion-mark size-3.5"
|
|
819
|
+
/>
|
|
820
|
+
</span>
|
|
821
|
+
<span className="aui-thread-welcome-suggestion-text min-w-0">
|
|
822
|
+
<span className="aui-thread-welcome-suggestion-text-1">{prompt}</span>
|
|
823
|
+
</span>
|
|
824
|
+
</button>
|
|
825
|
+
);
|
|
826
|
+
};
|
|
827
|
+
|
|
828
|
+
const ThreadSuggestionItem: FC = () => {
|
|
829
|
+
return (
|
|
830
|
+
<SuggestionPrimitive.Trigger send asChild>
|
|
831
|
+
<button
|
|
832
|
+
type="button"
|
|
833
|
+
className={SUGGESTION_BUTTON_CLASS}
|
|
834
|
+
onClick={() => dismissKeyboardIfCoarse()}
|
|
835
|
+
>
|
|
836
|
+
<span className="aui-msg-marker" aria-hidden="true">
|
|
837
|
+
<DotMatrix
|
|
838
|
+
state="example"
|
|
839
|
+
decorative
|
|
840
|
+
className="aui-thread-welcome-suggestion-mark size-3.5"
|
|
841
|
+
/>
|
|
842
|
+
</span>
|
|
843
|
+
<span className="aui-thread-welcome-suggestion-text min-w-0">
|
|
844
|
+
<SuggestionPrimitive.Title className="aui-thread-welcome-suggestion-text-1" />
|
|
845
|
+
<SuggestionPrimitive.Description className="aui-thread-welcome-suggestion-text-2 empty:hidden" />
|
|
846
|
+
</span>
|
|
847
|
+
</button>
|
|
848
|
+
</SuggestionPrimitive.Trigger>
|
|
849
|
+
);
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
const Composer: FC<{
|
|
853
|
+
autoFocus: boolean;
|
|
854
|
+
placeholder?: string | undefined;
|
|
855
|
+
layout: ComposerLayout;
|
|
856
|
+
toolbar?: ReactNode;
|
|
857
|
+
}> = ({ autoFocus, placeholder = "Send a message...", layout, toolbar }) => {
|
|
858
|
+
const compact = layout === "compact";
|
|
859
|
+
const { onComposerSubmit, slash, mention } = useContext(ThreadChromeContext);
|
|
860
|
+
const edit = useContext(ComposerEditContext);
|
|
861
|
+
const aui = useAui();
|
|
862
|
+
const inputWrapRef = useRef<HTMLDivElement | null>(null);
|
|
863
|
+
const composerRootRef = useRef<HTMLFormElement | null>(null);
|
|
864
|
+
const isEditingMessage = edit.editing !== null;
|
|
865
|
+
// Read after mount, not during render: the server has no matchMedia, and a
|
|
866
|
+
// phone reporting "coarse" on its first render would hydrate a different
|
|
867
|
+
// tree than the one that was sent.
|
|
868
|
+
const [coarsePointer, setCoarsePointer] = useState(false);
|
|
869
|
+
useEffect(() => {
|
|
870
|
+
setCoarsePointer(isCoarsePointer());
|
|
871
|
+
}, []);
|
|
872
|
+
|
|
873
|
+
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
|
|
874
|
+
if (edit.editing) {
|
|
875
|
+
event.preventDefault();
|
|
876
|
+
edit.commit(aui.composer().getState().text);
|
|
877
|
+
// An edit is a send: the corrected text goes off and the answer starts
|
|
878
|
+
// streaming. Leaving the keyboard up would hide it, so the same
|
|
879
|
+
// dismissal runs here as on a normal submit.
|
|
880
|
+
dismissKeyboardIfCoarse();
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
// Captured before the send clears the textarea — see composerHasText.
|
|
884
|
+
const sent = composerHasText(composerRootRef.current);
|
|
885
|
+
onComposerSubmit?.(event);
|
|
886
|
+
// On a phone the keyboard would otherwise stay up over the streaming
|
|
887
|
+
// answer; blurring retracts it and leaves the thread in full view. A
|
|
888
|
+
// no-op on a fine pointer, so a desktop keeps its caret for the next turn.
|
|
889
|
+
if (sent) dismissComposerKeyboard(composerRootRef.current);
|
|
890
|
+
};
|
|
891
|
+
|
|
892
|
+
// Click outside the composer cancels an in-progress edit. The edit button
|
|
893
|
+
// itself is excluded so re-clicking edit on another message switches target.
|
|
894
|
+
useEffect(() => {
|
|
895
|
+
if (!isEditingMessage) return;
|
|
896
|
+
const onPointerDown = (event: PointerEvent) => {
|
|
897
|
+
const target = event.target as Element | null;
|
|
898
|
+
if (!target) return;
|
|
899
|
+
if (composerRootRef.current?.contains(target)) return;
|
|
900
|
+
if (target.closest(".aui-user-action-edit, [data-radix-popper-content-wrapper]")) return;
|
|
901
|
+
edit.cancel();
|
|
902
|
+
};
|
|
903
|
+
document.addEventListener("pointerdown", onPointerDown, true);
|
|
904
|
+
return () => document.removeEventListener("pointerdown", onPointerDown, true);
|
|
905
|
+
}, [isEditingMessage, edit]);
|
|
906
|
+
|
|
907
|
+
// Focus the composer when an edit starts.
|
|
908
|
+
useEffect(() => {
|
|
909
|
+
if (!isEditingMessage) return;
|
|
910
|
+
const area = inputWrapRef.current?.querySelector<HTMLTextAreaElement>("textarea");
|
|
911
|
+
if (!area) return;
|
|
912
|
+
area.focus({ preventScroll: true });
|
|
913
|
+
const end = area.value.length;
|
|
914
|
+
area.setSelectionRange(end, end);
|
|
915
|
+
}, [isEditingMessage, edit.editing?.messageId]);
|
|
916
|
+
const focusComposerInput = () => {
|
|
917
|
+
inputWrapRef.current
|
|
918
|
+
?.querySelector<HTMLTextAreaElement>("textarea")
|
|
919
|
+
?.focus({ preventScroll: true });
|
|
920
|
+
};
|
|
921
|
+
|
|
922
|
+
// Focus the composer on mount. The runtime's own autoFocus effect can miss
|
|
923
|
+
// when the embed hydrates inside an iframe, and a retry covers engines that
|
|
924
|
+
// ignore focus() until the first paint.
|
|
925
|
+
useEffect(() => {
|
|
926
|
+
if (!autoFocus) return;
|
|
927
|
+
let cancelled = false;
|
|
928
|
+
let attempts = 0;
|
|
929
|
+
const focus = () => {
|
|
930
|
+
if (cancelled) return;
|
|
931
|
+
const area =
|
|
932
|
+
inputWrapRef.current?.querySelector<HTMLTextAreaElement>("textarea");
|
|
933
|
+
if (!area || document.activeElement === area) return;
|
|
934
|
+
const active = document.activeElement;
|
|
935
|
+
if (active instanceof HTMLElement && active !== document.body) return;
|
|
936
|
+
area.focus({ preventScroll: true });
|
|
937
|
+
if (document.activeElement !== area && attempts < 6) {
|
|
938
|
+
attempts += 1;
|
|
939
|
+
window.requestAnimationFrame(focus);
|
|
940
|
+
}
|
|
941
|
+
};
|
|
942
|
+
const frame = window.requestAnimationFrame(focus);
|
|
943
|
+
return () => {
|
|
944
|
+
cancelled = true;
|
|
945
|
+
window.cancelAnimationFrame(frame);
|
|
946
|
+
};
|
|
947
|
+
}, [autoFocus]);
|
|
948
|
+
|
|
949
|
+
const handleShellMouseDown = (event: ReactMouseEvent<HTMLDivElement>) => {
|
|
950
|
+
const target = event.target as HTMLElement | null;
|
|
951
|
+
if (
|
|
952
|
+
target?.closest(
|
|
953
|
+
"button, a, input, textarea, select, [contenteditable='true']",
|
|
954
|
+
)
|
|
955
|
+
) {
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
event.preventDefault();
|
|
959
|
+
focusComposerInput();
|
|
960
|
+
};
|
|
961
|
+
const bar = (
|
|
962
|
+
<ComposerPrimitive.Root
|
|
963
|
+
ref={composerRootRef}
|
|
964
|
+
className="aui-composer-root relative flex w-full flex-col"
|
|
965
|
+
onSubmit={handleSubmit}
|
|
966
|
+
onKeyDownCapture={(event) => {
|
|
967
|
+
if (isEditingMessage && event.key === "Escape") {
|
|
968
|
+
event.preventDefault();
|
|
969
|
+
event.stopPropagation();
|
|
970
|
+
edit.cancel();
|
|
971
|
+
}
|
|
972
|
+
}}
|
|
973
|
+
>
|
|
974
|
+
<ComposerPrimitive.AttachmentDropzone asChild>
|
|
975
|
+
<div
|
|
976
|
+
data-slot="aui_composer-shell"
|
|
977
|
+
data-layout={layout}
|
|
978
|
+
onMouseDown={handleShellMouseDown}
|
|
979
|
+
className="border-hair data-[dragging=true]:border-accent flex w-full cursor-text flex-col gap-2 rounded-(--composer-radius) border bg-(--composer-bg) p-(--composer-padding,0.5rem) data-[dragging=true]:border-dashed data-[dragging=true]:bg-[color-mix(in_oklab,var(--accent)_50%,var(--bg))]"
|
|
980
|
+
>
|
|
981
|
+
<ComposerAttachments />
|
|
982
|
+
{/* One row: attach, input, send. The toolbar used to sit on a second
|
|
983
|
+
row inside the same border, which read as a two-line text box. */}
|
|
984
|
+
<div className="aui-composer-main flex min-w-0 items-end gap-1">
|
|
985
|
+
<ComposerAddAttachment />
|
|
986
|
+
<div
|
|
987
|
+
ref={inputWrapRef}
|
|
988
|
+
className="aui-composer-input-wrap relative min-w-0 flex-1"
|
|
989
|
+
>
|
|
990
|
+
<ComposerPrimitive.Input
|
|
991
|
+
placeholder={placeholder}
|
|
992
|
+
className={cn(
|
|
993
|
+
"aui-composer-input placeholder:text-ink-mute w-full resize-none overflow-x-hidden overflow-y-auto break-words whitespace-pre-wrap bg-transparent outline-none",
|
|
994
|
+
compact
|
|
995
|
+
? "min-h-7 max-h-[8.75rem] px-1.5 py-0 text-sm leading-7"
|
|
996
|
+
: "min-h-[1.375rem] max-h-[7.75rem] px-2.5 py-0.5 text-base leading-6",
|
|
997
|
+
)}
|
|
998
|
+
rows={1}
|
|
999
|
+
maxRows={5}
|
|
1000
|
+
autoFocus={autoFocus}
|
|
1001
|
+
submitMode="enter"
|
|
1002
|
+
enterKeyHint="send"
|
|
1003
|
+
// assistant-ui refocuses the composer when a run starts and
|
|
1004
|
+
// when the viewport scrolls back to the bottom. On a phone
|
|
1005
|
+
// that reopens the keyboard over the answer we just revealed,
|
|
1006
|
+
// so both are off there; on a desktop the caret is welcome.
|
|
1007
|
+
unstable_focusOnRunStart={!coarsePointer}
|
|
1008
|
+
unstable_focusOnScrollToBottom={!coarsePointer}
|
|
1009
|
+
aria-label="Message"
|
|
1010
|
+
/>
|
|
1011
|
+
<ComposerBlockCaret containerRef={inputWrapRef} />
|
|
1012
|
+
</div>
|
|
1013
|
+
<ComposerSendControls />
|
|
1014
|
+
</div>
|
|
1015
|
+
</div>
|
|
1016
|
+
</ComposerPrimitive.AttachmentDropzone>
|
|
1017
|
+
{toolbar ? (
|
|
1018
|
+
<div data-slot="aui_composer-toolbar" className="aui-composer-toolbar">
|
|
1019
|
+
{toolbar}
|
|
1020
|
+
</div>
|
|
1021
|
+
) : null}
|
|
1022
|
+
{slash ? (
|
|
1023
|
+
<ComposerTriggerPopover
|
|
1024
|
+
char="/"
|
|
1025
|
+
adapter={slash.adapter}
|
|
1026
|
+
action={slash.action}
|
|
1027
|
+
iconMap={slash.iconMap}
|
|
1028
|
+
matcher={slash.matcher}
|
|
1029
|
+
emptyItemsLabel="No matching commands"
|
|
1030
|
+
/>
|
|
1031
|
+
) : null}
|
|
1032
|
+
{mention ? (
|
|
1033
|
+
<ComposerTriggerPopover
|
|
1034
|
+
char="@"
|
|
1035
|
+
adapter={mention.adapter}
|
|
1036
|
+
directive={mention.directive}
|
|
1037
|
+
iconMap={mention.iconMap}
|
|
1038
|
+
emptyItemsLabel="No matching tools"
|
|
1039
|
+
/>
|
|
1040
|
+
) : null}
|
|
1041
|
+
</ComposerPrimitive.Root>
|
|
1042
|
+
);
|
|
1043
|
+
if (!slash && !mention) return bar;
|
|
1044
|
+
return (
|
|
1045
|
+
<ComposerPrimitive.Unstable_TriggerPopoverRoot>
|
|
1046
|
+
{bar}
|
|
1047
|
+
</ComposerPrimitive.Unstable_TriggerPopoverRoot>
|
|
1048
|
+
);
|
|
1049
|
+
};
|
|
1050
|
+
|
|
1051
|
+
const ComposerSendControls: FC = () => {
|
|
1052
|
+
const aui = useAui();
|
|
1053
|
+
// Mirrors `composerSendDisabled` from `@assistant-ui/core`'s primitive
|
|
1054
|
+
// predicates. Inlined rather than imported: the predicate is not re-exported
|
|
1055
|
+
// from `@assistant-ui/react` (the only assistant-ui package this one depends
|
|
1056
|
+
// on), so a deep import would resolve only thanks to npm hoisting.
|
|
1057
|
+
const sendDisabled = useAuiState(
|
|
1058
|
+
(s) => !s.composer.canSend || (s.thread.isRunning && !s.thread.capabilities.queue),
|
|
1059
|
+
);
|
|
1060
|
+
// A `type="submit"` button already submits its form on activation, so the
|
|
1061
|
+
// click must not also do it: `preventDefault()` suppresses that implicit
|
|
1062
|
+
// submission, and the explicit `requestSubmit()` below replaces it with one
|
|
1063
|
+
// we control. Without this the form's `submit` fires twice per click, and the
|
|
1064
|
+
// free-turn gate mounted on `onSubmit` runs twice — holding or charging the
|
|
1065
|
+
// turn more than once.
|
|
1066
|
+
const submitComposer = (event: ReactMouseEvent<HTMLButtonElement>) => {
|
|
1067
|
+
event.preventDefault();
|
|
1068
|
+
const form = event.currentTarget.closest("form");
|
|
1069
|
+
if (form) {
|
|
1070
|
+
form.requestSubmit();
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
aui.composer.send();
|
|
1074
|
+
};
|
|
1075
|
+
|
|
1076
|
+
return (
|
|
1077
|
+
<div className="aui-composer-send-controls ml-auto flex shrink-0 items-center gap-1.5">
|
|
1078
|
+
<AuiIf condition={(s) => s.thread.capabilities.dictation}>
|
|
1079
|
+
<AuiIf condition={(s) => s.composer.dictation == null}>
|
|
1080
|
+
<ComposerPrimitive.Dictate asChild>
|
|
1081
|
+
<TooltipIconButton
|
|
1082
|
+
tooltip="Voice input"
|
|
1083
|
+
side="bottom"
|
|
1084
|
+
type="button"
|
|
1085
|
+
variant="ghost"
|
|
1086
|
+
size="icon"
|
|
1087
|
+
className="aui-composer-dictate text-ink-mute hover:text-ink size-7 rounded-none"
|
|
1088
|
+
aria-label="Start voice input"
|
|
1089
|
+
>
|
|
1090
|
+
<DotMatrix
|
|
1091
|
+
state="dictate"
|
|
1092
|
+
decorative
|
|
1093
|
+
className="aui-composer-dictate-icon size-3.5"
|
|
1094
|
+
/>
|
|
1095
|
+
</TooltipIconButton>
|
|
1096
|
+
</ComposerPrimitive.Dictate>
|
|
1097
|
+
</AuiIf>
|
|
1098
|
+
<AuiIf condition={(s) => s.composer.dictation != null}>
|
|
1099
|
+
<ComposerPrimitive.StopDictation asChild>
|
|
1100
|
+
<TooltipIconButton
|
|
1101
|
+
tooltip="Stop dictation"
|
|
1102
|
+
side="bottom"
|
|
1103
|
+
type="button"
|
|
1104
|
+
variant="ghost"
|
|
1105
|
+
size="icon"
|
|
1106
|
+
className="aui-composer-stop-dictation text-danger size-7 rounded-none"
|
|
1107
|
+
aria-label="Stop voice input"
|
|
1108
|
+
>
|
|
1109
|
+
<DotMatrix
|
|
1110
|
+
state="stop"
|
|
1111
|
+
decorative
|
|
1112
|
+
className="aui-composer-stop-dictation-icon size-3.5"
|
|
1113
|
+
/>
|
|
1114
|
+
</TooltipIconButton>
|
|
1115
|
+
</ComposerPrimitive.StopDictation>
|
|
1116
|
+
</AuiIf>
|
|
1117
|
+
</AuiIf>
|
|
1118
|
+
<AuiIf condition={(s) => !s.thread.isRunning}>
|
|
1119
|
+
{/* No tooltip/dropdown on send: the button submits on click and on
|
|
1120
|
+
Enter, so the "Send (Enter)" popover was noise. */}
|
|
1121
|
+
<Button
|
|
1122
|
+
dress="chat"
|
|
1123
|
+
type="submit"
|
|
1124
|
+
variant="ghost"
|
|
1125
|
+
size="icon"
|
|
1126
|
+
className="aui-composer-send size-7 rounded-none"
|
|
1127
|
+
aria-label="Send message"
|
|
1128
|
+
disabled={sendDisabled}
|
|
1129
|
+
onClick={submitComposer}
|
|
1130
|
+
>
|
|
1131
|
+
<DotMatrix
|
|
1132
|
+
state="send"
|
|
1133
|
+
decorative
|
|
1134
|
+
className="aui-composer-send-icon size-3.5"
|
|
1135
|
+
/>
|
|
1136
|
+
</Button>
|
|
1137
|
+
</AuiIf>
|
|
1138
|
+
<AuiIf condition={(s) => s.thread.isRunning}>
|
|
1139
|
+
<ComposerPrimitive.Cancel asChild>
|
|
1140
|
+
<Button dress="chat"
|
|
1141
|
+
type="button"
|
|
1142
|
+
variant="default"
|
|
1143
|
+
size="icon"
|
|
1144
|
+
className="aui-composer-cancel size-7 rounded-none"
|
|
1145
|
+
aria-label="Stop generating"
|
|
1146
|
+
>
|
|
1147
|
+
<DotMatrix
|
|
1148
|
+
state="stop"
|
|
1149
|
+
decorative
|
|
1150
|
+
className="aui-composer-cancel-icon size-3.5"
|
|
1151
|
+
/>
|
|
1152
|
+
</Button>
|
|
1153
|
+
</ComposerPrimitive.Cancel>
|
|
1154
|
+
</AuiIf>
|
|
1155
|
+
</div>
|
|
1156
|
+
);
|
|
1157
|
+
};
|
|
1158
|
+
|
|
1159
|
+
/** Thread-level cubes only when no tool is already animating its own matrix. */
|
|
1160
|
+
const AssistantWorkingIndicator: FC = () => {
|
|
1161
|
+
const toolRunning = useAuiState((s) =>
|
|
1162
|
+
s.message.parts.some(
|
|
1163
|
+
(part) => part.type === "tool-call" && part.status.type === "running",
|
|
1164
|
+
),
|
|
1165
|
+
);
|
|
1166
|
+
// Text only when the server sent a status message (DIG-4459): a bare
|
|
1167
|
+
// `connecting` is the plain wait for a response, so the row is the
|
|
1168
|
+
// animation alone. No client-side default copy.
|
|
1169
|
+
const statusText = useAuiState((s) => connectionStatusText(s.message.parts));
|
|
1170
|
+
const wakePhase = useDigichatWarmupPhase();
|
|
1171
|
+
if (toolRunning) return null;
|
|
1172
|
+
const statusLabel = wakePhase === "waking" ? "Waking up services…" : statusText;
|
|
1173
|
+
return (
|
|
1174
|
+
<span
|
|
1175
|
+
data-slot="aui_assistant-message-indicator"
|
|
1176
|
+
className="aui-work-status"
|
|
1177
|
+
>
|
|
1178
|
+
<WorkStatusLead
|
|
1179
|
+
marker="loading"
|
|
1180
|
+
markerLabel="Assistant is working"
|
|
1181
|
+
label={statusLabel}
|
|
1182
|
+
/>
|
|
1183
|
+
</span>
|
|
1184
|
+
);
|
|
1185
|
+
};
|
|
1186
|
+
|
|
1187
|
+
const AssistantMessage: FC = () => {
|
|
1188
|
+
const {
|
|
1189
|
+
ToolFallback: ToolFallbackComponent = ToolFallback,
|
|
1190
|
+
ToolGroup,
|
|
1191
|
+
ReasoningGroup,
|
|
1192
|
+
} = useContext(ThreadComponentsContext);
|
|
1193
|
+
const {
|
|
1194
|
+
reasoning: reasoningMode,
|
|
1195
|
+
toolCalls: toolCallsMode,
|
|
1196
|
+
visibility,
|
|
1197
|
+
} = useContext(ThreadGroupDisclosureContext);
|
|
1198
|
+
const privateMode = useContext(ThreadPrivateModeContext);
|
|
1199
|
+
const disclosure = resolveWorkDisclosure(visibility);
|
|
1200
|
+
// The whole turn, not just the group: the tools answer faster than the
|
|
1201
|
+
// chain can fold, and a group that closes between two of them flickers.
|
|
1202
|
+
const turnRunning = useAuiState((s) => s.thread.isRunning);
|
|
1203
|
+
// The message parts, to tell a chain that holds real work from one that
|
|
1204
|
+
// only holds a source or a status part.
|
|
1205
|
+
const messageParts = useAuiState((s) => s.message.parts);
|
|
1206
|
+
// Only text ends a work run: a chain with a text part after it has
|
|
1207
|
+
// answered, so its work is over. A chain without one is still the agent's
|
|
1208
|
+
// current work, even if the model streams another run of tool calls.
|
|
1209
|
+
const hasWork = messageHasWork(messageParts);
|
|
1210
|
+
// Anything real that arrived from the agent — reasoning, a tool call, the
|
|
1211
|
+
// answer itself. A status part (the connection state) is not content: it is
|
|
1212
|
+
// the pre-response phase, which is exactly when the loading row belongs.
|
|
1213
|
+
const hasContent = messageHasContent(messageParts);
|
|
1214
|
+
const followedByText = (indices: readonly number[]) =>
|
|
1215
|
+
chainFollowedByText(messageParts, indices);
|
|
1216
|
+
|
|
1217
|
+
const ACTION_BAR_PT = "pt-1.5";
|
|
1218
|
+
// Keep the action bar inside the contained root's paint box, then cancel its reserved space in flow.
|
|
1219
|
+
const ACTION_BAR_HEIGHT = `min-h-7.5 ${ACTION_BAR_PT}`;
|
|
1220
|
+
|
|
1221
|
+
return (
|
|
1222
|
+
<MessagePrimitive.Root
|
|
1223
|
+
data-slot="aui_assistant-message-root"
|
|
1224
|
+
data-role="assistant"
|
|
1225
|
+
className="aui-msg fade-in slide-in-from-bottom-1 animate-in relative grid grid-cols-[minmax(0,1fr)] items-start -mb-7.5 pb-7.5 duration-(--dur-fast)"
|
|
1226
|
+
>
|
|
1227
|
+
<div
|
|
1228
|
+
data-slot="aui_assistant-message-content"
|
|
1229
|
+
className="aui-assistant-message-content text-ink min-w-0 leading-relaxed wrap-break-word"
|
|
1230
|
+
>
|
|
1231
|
+
<MessagePrimitive.GroupedParts
|
|
1232
|
+
groupBy={chainGroupBy}
|
|
1233
|
+
>
|
|
1234
|
+
{({ part, children }) => {
|
|
1235
|
+
switch (part.type) {
|
|
1236
|
+
case "group-chainOfThought": {
|
|
1237
|
+
// A status part is not work. The grouping puts the
|
|
1238
|
+
// connection state into the chain, so without this guard the
|
|
1239
|
+
// chain header appears before the model has said anything —
|
|
1240
|
+
// right next to the loading row, which is the double
|
|
1241
|
+
// animation we do not want. No reasoning and no tool call in
|
|
1242
|
+
// the group means no work block: the loading row covers that
|
|
1243
|
+
// phase on its own.
|
|
1244
|
+
const holdsWork = chainHoldsWork(messageParts, part.indices);
|
|
1245
|
+
// The parts still render bare: a backend notice (DIG-4459)
|
|
1246
|
+
// rides a data part and must not vanish with the header.
|
|
1247
|
+
if (!holdsWork) return <>{children}</>;
|
|
1248
|
+
// A private session folds the chain exactly like a normal one:
|
|
1249
|
+
// the same header, the same view rules. Only the rows inside
|
|
1250
|
+
// differ (see `PrivateWorkStep`).
|
|
1251
|
+
// The chain is the one disclosure boundary: every work part
|
|
1252
|
+
// nests under it (see `chainGroupBy`), so the level's fold
|
|
1253
|
+
// rule here reaches a reasoning-only, a tool-only and a
|
|
1254
|
+
// combined turn alike. The rows inside take their own rule
|
|
1255
|
+
// from the same disclosure. See the decision note in
|
|
1256
|
+
// packages/ui/ARCHITECTURE.md.
|
|
1257
|
+
//
|
|
1258
|
+
// The `hidden` level this branch served is retired: the
|
|
1259
|
+
// `summaryOnly` field is always false now, and a quiet
|
|
1260
|
+
// client gets the folded chain below instead of a summary
|
|
1261
|
+
// line. The branch stays until the field goes.
|
|
1262
|
+
if (disclosure.summaryOnly) {
|
|
1263
|
+
return (
|
|
1264
|
+
<div data-slot="aui_chain-of-thought">
|
|
1265
|
+
<WorkChainSummary indices={part.indices} />
|
|
1266
|
+
</div>
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
// One header for the whole chain: the reasoning and the tool
|
|
1270
|
+
// calls collapse together.
|
|
1271
|
+
return (
|
|
1272
|
+
<div data-slot="aui_chain-of-thought">
|
|
1273
|
+
<WorkChain
|
|
1274
|
+
indices={part.indices}
|
|
1275
|
+
openAtRest={disclosure.openAtRest}
|
|
1276
|
+
foldOnAnswer={disclosure.foldOnAnswer}
|
|
1277
|
+
running={part.status.type === "running"}
|
|
1278
|
+
followedByText={followedByText(part.indices)}
|
|
1279
|
+
>
|
|
1280
|
+
{children}
|
|
1281
|
+
</WorkChain>
|
|
1282
|
+
</div>
|
|
1283
|
+
);
|
|
1284
|
+
}
|
|
1285
|
+
case "group-tool": {
|
|
1286
|
+
// A private session keeps the "N tool calls" group: opening it
|
|
1287
|
+
// lists the rows, and only the rows themselves stay shut.
|
|
1288
|
+
if (toolCallsMode === "off") return null;
|
|
1289
|
+
if (ToolGroup) {
|
|
1290
|
+
return <ToolGroup group={part}>{children}</ToolGroup>;
|
|
1291
|
+
}
|
|
1292
|
+
const running = part.status.type === "running";
|
|
1293
|
+
// The group can hold the source and status parts that sit
|
|
1294
|
+
// between two tool runs; the count is the tool calls only.
|
|
1295
|
+
const toolCount = chainToolCount(messageParts, part.indices);
|
|
1296
|
+
if (toolCount === 0) {
|
|
1297
|
+
return <div data-slot="aui_chain-of-thought">{children}</div>;
|
|
1298
|
+
}
|
|
1299
|
+
const group = toolGroupDisclosure(
|
|
1300
|
+
disclosure,
|
|
1301
|
+
toolCallsMode,
|
|
1302
|
+
running || turnRunning,
|
|
1303
|
+
);
|
|
1304
|
+
const locked = group.locked;
|
|
1305
|
+
return (
|
|
1306
|
+
<ToolGroupRoot
|
|
1307
|
+
variant="ghost"
|
|
1308
|
+
defaultOpen={group.defaultOpen}
|
|
1309
|
+
streamingOpen={group.streamingOpen}
|
|
1310
|
+
{...(locked ? { open: true, onOpenChange: () => {} } : {})}
|
|
1311
|
+
>
|
|
1312
|
+
<ToolGroupTrigger
|
|
1313
|
+
count={toolCount}
|
|
1314
|
+
active={part.status.type === "running"}
|
|
1315
|
+
disabled={locked}
|
|
1316
|
+
/>
|
|
1317
|
+
<ToolGroupContent>{children}</ToolGroupContent>
|
|
1318
|
+
</ToolGroupRoot>
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
case "group-reasoning": {
|
|
1322
|
+
if (reasoningMode === "off") return null;
|
|
1323
|
+
// Private session: the same "Reasoning" row, but not a
|
|
1324
|
+
// disclosure. The server sends no reasoning text, so there
|
|
1325
|
+
// is no body under it and the parts inside do not render.
|
|
1326
|
+
if (privateMode) {
|
|
1327
|
+
return (
|
|
1328
|
+
<PrivateWorkStep
|
|
1329
|
+
kind="reasoning"
|
|
1330
|
+
statusType={part.status.type === "running" ? "running" : "complete"}
|
|
1331
|
+
/>
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1334
|
+
if (ReasoningGroup) {
|
|
1335
|
+
return (
|
|
1336
|
+
<ReasoningGroup group={part}>{children}</ReasoningGroup>
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
const running = part.status.type === "running";
|
|
1340
|
+
const box = reasoningGroupDisclosure(
|
|
1341
|
+
disclosure,
|
|
1342
|
+
reasoningMode,
|
|
1343
|
+
running || turnRunning,
|
|
1344
|
+
);
|
|
1345
|
+
const locked = box.locked;
|
|
1346
|
+
return (
|
|
1347
|
+
<ReasoningRoot
|
|
1348
|
+
variant="ghost"
|
|
1349
|
+
streaming={box.streamingOpen}
|
|
1350
|
+
defaultOpen={box.defaultOpen}
|
|
1351
|
+
{...(locked ? { open: true, onOpenChange: () => {} } : {})}
|
|
1352
|
+
>
|
|
1353
|
+
{/* Upstream's structure: the reasoning is a sub-step with
|
|
1354
|
+
its own disclosure inside the chain. */}
|
|
1355
|
+
<ReasoningTrigger active={running} disabled={locked} />
|
|
1356
|
+
<ReasoningContent aria-busy={running}>
|
|
1357
|
+
<ReasoningText>{children}</ReasoningText>
|
|
1358
|
+
</ReasoningContent>
|
|
1359
|
+
</ReasoningRoot>
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
case "text":
|
|
1363
|
+
return <MarkdownText />;
|
|
1364
|
+
case "reasoning": {
|
|
1365
|
+
if (reasoningMode === "off") return null;
|
|
1366
|
+
// Private session: the group row stands for it; no body.
|
|
1367
|
+
if (privateMode) return null;
|
|
1368
|
+
return <Reasoning {...part} />;
|
|
1369
|
+
}
|
|
1370
|
+
case "tool-call":
|
|
1371
|
+
if (toolCallsMode === "off") return null;
|
|
1372
|
+
// Private session: the normal row's look, but it never
|
|
1373
|
+
// opens, and a tool's own renderer is skipped — it would
|
|
1374
|
+
// render the (stripped) arguments and result.
|
|
1375
|
+
if (privateMode) {
|
|
1376
|
+
return (
|
|
1377
|
+
<PrivateToolCallStep
|
|
1378
|
+
toolName={part.toolName}
|
|
1379
|
+
status={part.status}
|
|
1380
|
+
isError={part.isError}
|
|
1381
|
+
result={part.result}
|
|
1382
|
+
/>
|
|
1383
|
+
);
|
|
1384
|
+
}
|
|
1385
|
+
return part.toolUI ?? <ToolFallbackComponent {...part} />;
|
|
1386
|
+
case "data":
|
|
1387
|
+
// DIG-4459: a backend notice (data-notice, or a data-status
|
|
1388
|
+
// with a message) shows as a boxed status/error line.
|
|
1389
|
+
return part.dataRendererUI ?? <BackendNotice part={part} />;
|
|
1390
|
+
case "file":
|
|
1391
|
+
return (
|
|
1392
|
+
<div data-slot="aui_assistant-message-file" className="py-1">
|
|
1393
|
+
<File {...part} />
|
|
1394
|
+
</div>
|
|
1395
|
+
);
|
|
1396
|
+
case "image":
|
|
1397
|
+
return (
|
|
1398
|
+
<div data-slot="aui_assistant-message-image" className="py-1">
|
|
1399
|
+
<Image {...part} />
|
|
1400
|
+
</div>
|
|
1401
|
+
);
|
|
1402
|
+
case "indicator":
|
|
1403
|
+
// One loading affordance per turn. Before the agent answers,
|
|
1404
|
+
// the row stands alone (with the phase text when we know it:
|
|
1405
|
+
// waking, connecting, warming up). The moment anything real
|
|
1406
|
+
// arrives — reasoning, a tool call, the answer — the work
|
|
1407
|
+
// block takes over and this row is gone, so the two never
|
|
1408
|
+
// stack.
|
|
1409
|
+
return hasContent ? null : <AssistantWorkingIndicator />;
|
|
1410
|
+
case "source":
|
|
1411
|
+
// Citations are inline markdown links in the answer (DIG-1210);
|
|
1412
|
+
// rendering source parts here stacked duplicate rows between
|
|
1413
|
+
// tool groups on every retrieve span.
|
|
1414
|
+
return null;
|
|
1415
|
+
default:
|
|
1416
|
+
return null;
|
|
1417
|
+
}
|
|
1418
|
+
}}
|
|
1419
|
+
</MessagePrimitive.GroupedParts>
|
|
1420
|
+
<MessageError />
|
|
1421
|
+
</div>
|
|
1422
|
+
|
|
1423
|
+
<div
|
|
1424
|
+
data-slot="aui_assistant-message-footer"
|
|
1425
|
+
className={cn("col-start-1 flex items-center", ACTION_BAR_HEIGHT)}
|
|
1426
|
+
>
|
|
1427
|
+
<AuiIf
|
|
1428
|
+
condition={(s) =>
|
|
1429
|
+
!(
|
|
1430
|
+
s.message.status?.type === "incomplete" &&
|
|
1431
|
+
s.message.status.reason === "error"
|
|
1432
|
+
)
|
|
1433
|
+
}
|
|
1434
|
+
>
|
|
1435
|
+
<BranchPicker />
|
|
1436
|
+
<AssistantActionBar />
|
|
1437
|
+
</AuiIf>
|
|
1438
|
+
</div>
|
|
1439
|
+
</MessagePrimitive.Root>
|
|
1440
|
+
);
|
|
1441
|
+
};
|
|
1442
|
+
|
|
1443
|
+
const AssistantActionBar: FC = () => {
|
|
1444
|
+
const { undo, feedback } = useContext(ThreadActionsContext);
|
|
1445
|
+
const privateMode = useContext(ThreadPrivateModeContext);
|
|
1446
|
+
return (
|
|
1447
|
+
<ActionBarPrimitive.Root
|
|
1448
|
+
hideWhenRunning
|
|
1449
|
+
autohide="not-last"
|
|
1450
|
+
// Earlier turns: the bar is unmounted until the turn is hovered, then it
|
|
1451
|
+
// takes its footer row back in flow. It cannot float in the message gap —
|
|
1452
|
+
// the bar is 24px tall and the gap is 12/8/20px by density, so a float
|
|
1453
|
+
// paints over the next turn's first line. The empty-footer collapse in
|
|
1454
|
+
// chat-digichat.css keeps the un-hovered turns at one message gap.
|
|
1455
|
+
className="aui-assistant-action-bar-root text-ink-mute animate-in fade-in col-start-3 row-start-2 -ms-1 flex gap-1 duration-(--duration-hover)"
|
|
1456
|
+
>
|
|
1457
|
+
<ActionBarPrimitive.Copy asChild>
|
|
1458
|
+
<TooltipIconButton tooltip="Copy">
|
|
1459
|
+
<AuiIf condition={(s) => s.message.isCopied}>
|
|
1460
|
+
<CheckActionIcon className="size-3.5" />
|
|
1461
|
+
</AuiIf>
|
|
1462
|
+
<AuiIf condition={(s) => !s.message.isCopied}>
|
|
1463
|
+
<CopyActionIcon className="size-3.5" />
|
|
1464
|
+
</AuiIf>
|
|
1465
|
+
</TooltipIconButton>
|
|
1466
|
+
</ActionBarPrimitive.Copy>
|
|
1467
|
+
{undo ? (
|
|
1468
|
+
<AuiIf
|
|
1469
|
+
condition={(s) =>
|
|
1470
|
+
(s.message.branchCount ?? 1) > 1 && (s.message.branchNumber ?? 1) > 1
|
|
1471
|
+
}
|
|
1472
|
+
>
|
|
1473
|
+
<BranchPickerPrimitive.Previous asChild>
|
|
1474
|
+
<TooltipIconButton tooltip="Undo">
|
|
1475
|
+
<DotMatrix state="prev" decorative className="size-3.5" />
|
|
1476
|
+
</TooltipIconButton>
|
|
1477
|
+
</BranchPickerPrimitive.Previous>
|
|
1478
|
+
</AuiIf>
|
|
1479
|
+
) : null}
|
|
1480
|
+
<ActionBarPrimitive.Reload asChild>
|
|
1481
|
+
<TooltipIconButton tooltip="Redo">
|
|
1482
|
+
<DotMatrix state="refresh" decorative className="size-3.5" />
|
|
1483
|
+
</TooltipIconButton>
|
|
1484
|
+
</ActionBarPrimitive.Reload>
|
|
1485
|
+
{feedback ? (
|
|
1486
|
+
<>
|
|
1487
|
+
<ActionBarPrimitive.FeedbackPositive asChild>
|
|
1488
|
+
<TooltipIconButton tooltip="Good response">
|
|
1489
|
+
<DotMatrix state="thumbsUp" decorative className="size-3.5" />
|
|
1490
|
+
</TooltipIconButton>
|
|
1491
|
+
</ActionBarPrimitive.FeedbackPositive>
|
|
1492
|
+
<ActionBarPrimitive.FeedbackNegative asChild>
|
|
1493
|
+
<TooltipIconButton tooltip="Bad response">
|
|
1494
|
+
<DotMatrix state="thumbsDown" decorative className="size-3.5" />
|
|
1495
|
+
</TooltipIconButton>
|
|
1496
|
+
</ActionBarPrimitive.FeedbackNegative>
|
|
1497
|
+
</>
|
|
1498
|
+
) : null}
|
|
1499
|
+
{/* Private session: the stored messages still hold the real typed text,
|
|
1500
|
+
which the mode exists to keep out of a file someone else may see. */}
|
|
1501
|
+
{privateMode ? null : (
|
|
1502
|
+
<MessageExportMenu
|
|
1503
|
+
trigger={
|
|
1504
|
+
<TooltipIconButton tooltip="More">
|
|
1505
|
+
<DotMatrix state="more" decorative className="size-3.5" />
|
|
1506
|
+
</TooltipIconButton>
|
|
1507
|
+
}
|
|
1508
|
+
icon={<DotMatrix state="export" decorative className="size-3.5" />}
|
|
1509
|
+
contentClassName="aui-action-bar-more-content z-[110] min-w-[8rem] overflow-hidden border p-1"
|
|
1510
|
+
itemClassName="aui-action-bar-more-item flex cursor-pointer items-center gap-2 bg-transparent px-2 py-1.5 outline-none select-none hover:bg-transparent focus:bg-transparent data-highlighted:bg-transparent data-highlighted:text-ink"
|
|
1511
|
+
/>
|
|
1512
|
+
)}
|
|
1513
|
+
<MessageTiming />
|
|
1514
|
+
</ActionBarPrimitive.Root>
|
|
1515
|
+
);
|
|
1516
|
+
};
|
|
1517
|
+
|
|
1518
|
+
const UserFilePart: FileMessagePartComponent = (part) => {
|
|
1519
|
+
const hiddenNames = useHiddenAttachmentNames();
|
|
1520
|
+
if (part.filename && hiddenNames.includes(part.filename)) return null;
|
|
1521
|
+
return (
|
|
1522
|
+
<div data-slot="aui_user-message-file" className="py-1">
|
|
1523
|
+
<File {...part} />
|
|
1524
|
+
</div>
|
|
1525
|
+
);
|
|
1526
|
+
};
|
|
1527
|
+
|
|
1528
|
+
const UserImagePart: ImageMessagePartComponent = (part) => (
|
|
1529
|
+
<div data-slot="aui_user-message-image" className="py-1">
|
|
1530
|
+
<Image {...part} />
|
|
1531
|
+
</div>
|
|
1532
|
+
);
|
|
1533
|
+
|
|
1534
|
+
const UserMessage: FC = () => {
|
|
1535
|
+
return (
|
|
1536
|
+
<MessagePrimitive.Root
|
|
1537
|
+
data-slot="aui_user-message-root"
|
|
1538
|
+
className="aui-msg fade-in slide-in-from-bottom-1 animate-in grid grid-cols-[1.25rem_minmax(0,1fr)_1.75rem] items-start gap-x-[0.55rem] gap-y-2 duration-(--dur-fast)"
|
|
1539
|
+
data-role="user"
|
|
1540
|
+
>
|
|
1541
|
+
<span className="aui-msg-marker" aria-hidden="true">
|
|
1542
|
+
<DotMatrix state="user" label="User" className="size-3.5" />
|
|
1543
|
+
</span>
|
|
1544
|
+
<div className="aui-user-message-content-wrapper relative min-w-0">
|
|
1545
|
+
<UserMessageAttachments />
|
|
1546
|
+
<div className="aui-user-message-content text-ink wrap-break-word empty:hidden">
|
|
1547
|
+
<MessagePrimitive.Parts
|
|
1548
|
+
components={{ File: UserFilePart, Image: UserImagePart }}
|
|
1549
|
+
/>
|
|
1550
|
+
</div>
|
|
1551
|
+
</div>
|
|
1552
|
+
<div className="aui-user-action-bar-slot col-start-3 row-start-1 self-start justify-self-end">
|
|
1553
|
+
<UserActionBar />
|
|
1554
|
+
</div>
|
|
1555
|
+
|
|
1556
|
+
<BranchPicker
|
|
1557
|
+
data-slot="aui_user-branch-picker"
|
|
1558
|
+
className="col-start-2 -me-1 justify-start"
|
|
1559
|
+
/>
|
|
1560
|
+
</MessagePrimitive.Root>
|
|
1561
|
+
);
|
|
1562
|
+
};
|
|
1563
|
+
|
|
1564
|
+
const UserActionBar: FC = () => {
|
|
1565
|
+
const aui = useAui();
|
|
1566
|
+
const edit = useContext(ComposerEditContext);
|
|
1567
|
+
const onEdit = () => {
|
|
1568
|
+
const msg = aui.message().getState();
|
|
1569
|
+
const text = msg.parts
|
|
1570
|
+
.map((p) => (p.type === "text" ? p.text : ""))
|
|
1571
|
+
.join("");
|
|
1572
|
+
edit.begin(msg.id, text);
|
|
1573
|
+
};
|
|
1574
|
+
return (
|
|
1575
|
+
<ActionBarPrimitive.Root
|
|
1576
|
+
hideWhenRunning
|
|
1577
|
+
autohide="not-last"
|
|
1578
|
+
className="aui-user-action-bar-root flex flex-row items-center"
|
|
1579
|
+
>
|
|
1580
|
+
<TooltipIconButton
|
|
1581
|
+
tooltip="Edit"
|
|
1582
|
+
className="aui-user-action-edit"
|
|
1583
|
+
onClick={onEdit}
|
|
1584
|
+
>
|
|
1585
|
+
<DotMatrix state="edit" decorative className="size-3.5" />
|
|
1586
|
+
</TooltipIconButton>
|
|
1587
|
+
</ActionBarPrimitive.Root>
|
|
1588
|
+
);
|
|
1589
|
+
};
|
|
1590
|
+
|
|
1591
|
+
const BranchPicker: FC<BranchPickerPrimitive.Root.Props> = ({
|
|
1592
|
+
className,
|
|
1593
|
+
...rest
|
|
1594
|
+
}) => {
|
|
1595
|
+
return (
|
|
1596
|
+
<BranchPickerPrimitive.Root
|
|
1597
|
+
hideWhenSingleBranch
|
|
1598
|
+
className={cn(
|
|
1599
|
+
"aui-branch-picker-root text-ink-mute -ms-2 me-2 inline-flex items-center text-xs",
|
|
1600
|
+
className,
|
|
1601
|
+
)}
|
|
1602
|
+
{...rest}
|
|
1603
|
+
>
|
|
1604
|
+
<BranchPickerPrimitive.Previous asChild>
|
|
1605
|
+
<TooltipIconButton tooltip="Previous">
|
|
1606
|
+
<DotMatrix state="prev" decorative className="size-3.5" />
|
|
1607
|
+
</TooltipIconButton>
|
|
1608
|
+
</BranchPickerPrimitive.Previous>
|
|
1609
|
+
<span className="aui-branch-picker-state font-medium">
|
|
1610
|
+
<BranchPickerPrimitive.Number /> / <BranchPickerPrimitive.Count />
|
|
1611
|
+
</span>
|
|
1612
|
+
<BranchPickerPrimitive.Next asChild>
|
|
1613
|
+
<TooltipIconButton tooltip="Next">
|
|
1614
|
+
<DotMatrix state="next" decorative className="size-3.5" />
|
|
1615
|
+
</TooltipIconButton>
|
|
1616
|
+
</BranchPickerPrimitive.Next>
|
|
1617
|
+
</BranchPickerPrimitive.Root>
|
|
1618
|
+
);
|
|
1619
|
+
};
|