@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.
Files changed (424) hide show
  1. package/README.md +48 -2
  2. package/package.json +143 -4
  3. package/src/components/NavShell.tsx +776 -0
  4. package/src/components/SocialRow.tsx +87 -0
  5. package/src/components/Spinner.tsx +27 -0
  6. package/src/components/StackLogo.tsx +40 -0
  7. package/src/components/Terminal.tsx +110 -0
  8. package/src/components/TerminalManifest.tsx +212 -0
  9. package/src/components/ThemeProvider.tsx +271 -0
  10. package/src/components/account/AuthCard.tsx +309 -0
  11. package/src/components/assistant-ui/elements/mermaid-diagram.aui.tsx +63 -0
  12. package/src/components/assistant-ui/elements/mermaid-diagram.tsx +340 -0
  13. package/src/components/brand/BrandWordmark.tsx +32 -0
  14. package/src/components/changelog/ReleaseRail.tsx +81 -0
  15. package/src/components/changelog/index.ts +1 -0
  16. package/src/components/chat/ChatCodeBlock.tsx +67 -0
  17. package/src/components/chat/ChatMarkdown.tsx +66 -0
  18. package/src/components/chat/ChatMarkdownSource.tsx +203 -0
  19. package/src/components/chat/ChatMermaidBlock.tsx +202 -0
  20. package/src/components/chat/ChatMessage.tsx +77 -0
  21. package/src/components/chat/ChatPlayback.tsx +422 -0
  22. package/src/components/chat/ChatStreamCursor.tsx +25 -0
  23. package/src/components/chat/ChatThinking.tsx +113 -0
  24. package/src/components/chat/ChatToolCall.tsx +190 -0
  25. package/src/components/chat/ChatTranscript.tsx +60 -0
  26. package/src/components/chat/ChatWidgetFrame.tsx +107 -0
  27. package/src/components/chat/DigichatBootLoader.tsx +683 -0
  28. package/src/components/chat/DigichatLauncher.tsx +361 -0
  29. package/src/components/chat/DigichatThread.tsx +118 -0
  30. package/src/components/chat/DigichatThreadList.tsx +54 -0
  31. package/src/components/chat/DigichatWordmark.tsx +24 -0
  32. package/src/components/chat/DotMatrix.tsx +650 -0
  33. package/src/components/chat/boot-lab.tsx +766 -0
  34. package/src/components/chat/boot-signal.ts +144 -0
  35. package/src/components/chat/chat-surfaces.ts +52 -0
  36. package/src/components/chat/connection-status.ts +40 -0
  37. package/src/components/chat/digichat-wordmark.ts +355 -0
  38. package/src/components/chat/disclosure-chevron.tsx +49 -0
  39. package/src/components/chat/followable-href.ts +9 -0
  40. package/src/components/chat/gallery-thread/action-icons.tsx +25 -0
  41. package/src/components/chat/gallery-thread/attachment.aui.tsx +262 -0
  42. package/src/components/chat/gallery-thread/block-caret.tsx +439 -0
  43. package/src/components/chat/gallery-thread/cn.ts +1 -0
  44. package/src/components/chat/gallery-thread/composer-keyboard.ts +106 -0
  45. package/src/components/chat/gallery-thread/composer-trigger-popover.aui.tsx +235 -0
  46. package/src/components/chat/gallery-thread/css.d.ts +6 -0
  47. package/src/components/chat/gallery-thread/file.tsx +238 -0
  48. package/src/components/chat/gallery-thread/follow-up-suggestions.aui.tsx +92 -0
  49. package/src/components/chat/gallery-thread/format-json-dump.ts +112 -0
  50. package/src/components/chat/gallery-thread/hooks/use-attachment-src.ts +46 -0
  51. package/src/components/chat/gallery-thread/hooks/use-copy-to-clipboard.ts +9 -0
  52. package/src/components/chat/gallery-thread/image.tsx +519 -0
  53. package/src/components/chat/gallery-thread/markdown-text.tsx +278 -0
  54. package/src/components/chat/gallery-thread/message-error.aui.tsx +105 -0
  55. package/src/components/chat/gallery-thread/message-timing.aui.tsx +143 -0
  56. package/src/components/chat/gallery-thread/private-work-step.tsx +188 -0
  57. package/src/components/chat/gallery-thread/reasoning.tsx +332 -0
  58. package/src/components/chat/gallery-thread/scroll-mode.ts +174 -0
  59. package/src/components/chat/gallery-thread/source.tsx +76 -0
  60. package/src/components/chat/gallery-thread/thread.aui.tsx +1619 -0
  61. package/src/components/chat/gallery-thread/tooltip-icon-button.tsx +53 -0
  62. package/src/components/chat/gallery-thread/typed-welcome-copy.tsx +18 -0
  63. package/src/components/chat/gallery-thread/ui/tooltip.tsx +22 -0
  64. package/src/components/chat/gallery-thread/work-chain-summary.tsx +48 -0
  65. package/src/components/chat/gallery-thread/work-chain.tsx +110 -0
  66. package/src/components/chat/gallery-thread/work-status.ts +66 -0
  67. package/src/components/chat/overlay-scrollbar.tsx +86 -0
  68. package/src/components/chat/sanitize-mermaid-svg.ts +38 -0
  69. package/src/components/chat/skins/SOURCE.md +36 -0
  70. package/src/components/chat/skins/base/attachment.aui.tsx +287 -0
  71. package/src/components/chat/skins/base/aui-default/README.md +45 -0
  72. package/src/components/chat/skins/base/aui-default/collapsible-root.tsx +19 -0
  73. package/src/components/chat/skins/base/aui-default/collapsible.tsx +21 -0
  74. package/src/components/chat/skins/base/aui-default/reasoning.aui.tsx +120 -0
  75. package/src/components/chat/skins/base/aui-default/reasoning.tsx +331 -0
  76. package/src/components/chat/skins/base/aui-default/surfaces.ts +7 -0
  77. package/src/components/chat/skins/base/aui-default/tool-fallback.aui.tsx +772 -0
  78. package/src/components/chat/skins/base/aui-default/tool-group.aui.tsx +241 -0
  79. package/src/components/chat/skins/base/docs-model-options.ts +6 -0
  80. package/src/components/chat/skins/base/elements/assistant-trace.aui.tsx +169 -0
  81. package/src/components/chat/skins/base/elements/composer-trigger-popover.aui.tsx +271 -0
  82. package/src/components/chat/skins/base/elements/directive-text.aui.tsx +29 -0
  83. package/src/components/chat/skins/base/elements/directive-text.tsx +76 -0
  84. package/src/components/chat/skins/base/elements/message-timing.aui.tsx +104 -0
  85. package/src/components/chat/skins/base/elements/model-selector.aui.tsx +140 -0
  86. package/src/components/chat/skins/base/elements/model-selector.tsx +720 -0
  87. package/src/components/chat/skins/base/elements/quote.aui.tsx +290 -0
  88. package/src/components/chat/skins/base/elements/work-chain.tsx +100 -0
  89. package/src/components/chat/skins/base/markdown-text.tsx +294 -0
  90. package/src/components/chat/skins/base/model.ts +1 -0
  91. package/src/components/chat/skins/base/thread.tsx +1050 -0
  92. package/src/components/chat/skins/base/tooltip-icon-button.tsx +56 -0
  93. package/src/components/chat/skins/base/ui/avatar.tsx +115 -0
  94. package/src/components/chat/skins/base/ui/badge.tsx +51 -0
  95. package/src/components/chat/skins/base/ui/button.tsx +67 -0
  96. package/src/components/chat/skins/base/ui/command.tsx +185 -0
  97. package/src/components/chat/skins/base/ui/dialog.tsx +168 -0
  98. package/src/components/chat/skins/base/ui/dot-matrix.tsx +267 -0
  99. package/src/components/chat/skins/base/ui/popover.tsx +97 -0
  100. package/src/components/chat/skins/base/ui/skeleton.tsx +20 -0
  101. package/src/components/chat/skins/base/ui/textarea.tsx +24 -0
  102. package/src/components/chat/skins/base/ui/tooltip.tsx +73 -0
  103. package/src/components/chat/skins/base-assistant-ui/config-provider.tsx +90 -0
  104. package/src/components/chat/skins/base-assistant-ui/defaults.ts +419 -0
  105. package/src/components/chat/skins/base-assistant-ui/index.tsx +44 -0
  106. package/src/components/chat/skins/chatgpt.tsx +448 -0
  107. package/src/components/chat/skins/claude.tsx +380 -0
  108. package/src/components/chat/skins/digichat-composer-toolbar.tsx +151 -0
  109. package/src/components/chat/skins/digichat-context-meter-shares.ts +158 -0
  110. package/src/components/chat/skins/digichat-context-meter.tsx +243 -0
  111. package/src/components/chat/skins/digichat-meter-tone.ts +26 -0
  112. package/src/components/chat/skins/digichat-usage-meter.tsx +103 -0
  113. package/src/components/chat/skins/digichat.tsx +144 -0
  114. package/src/components/chat/skins/expo-react-native.tsx +29 -0
  115. package/src/components/chat/skins/gemini.tsx +322 -0
  116. package/src/components/chat/skins/grok-icon.tsx +34 -0
  117. package/src/components/chat/skins/grok.tsx +369 -0
  118. package/src/components/chat/skins/index.ts +46 -0
  119. package/src/components/chat/skins/perplexity.tsx +462 -0
  120. package/src/components/chat/skins/product-page-assistant/assistant-modal.tsx +73 -0
  121. package/src/components/chat/skins/product-page-assistant/index.tsx +19 -0
  122. package/src/components/chat/skins/product-page-assistant/product-dashboard.tsx +164 -0
  123. package/src/components/chat/skins/react-ink.tsx +97 -0
  124. package/src/components/chat/skins/thread-skin-host-contract.ts +102 -0
  125. package/src/components/chat/skins/thread-skin-view.tsx +156 -0
  126. package/src/components/chat/skins/thread-skins.ts +143 -0
  127. package/src/components/chat/skins/tooltip-icon-button.tsx +35 -0
  128. package/src/components/chat/skins/ui/dropdown-menu.tsx +272 -0
  129. package/src/components/chat/skins/webpage-assistant/assistant-config.ts +238 -0
  130. package/src/components/chat/skins/webpage-assistant/assistant-modal.tsx +78 -0
  131. package/src/components/chat/skins/webpage-assistant/config-provider.tsx +132 -0
  132. package/src/components/chat/skins/webpage-assistant/docs-app.tsx +106 -0
  133. package/src/components/chat/skins/webpage-assistant/docs-article.tsx +109 -0
  134. package/src/components/chat/skins/webpage-assistant/docs-assistant.tsx +73 -0
  135. package/src/components/chat/skins/webpage-assistant/docs-nav.tsx +81 -0
  136. package/src/components/chat/skins/webpage-assistant/host-ui.ts +79 -0
  137. package/src/components/chat/skins/webpage-assistant/index.tsx +12 -0
  138. package/src/components/chat/skins/webpage-assistant/markdown-page.tsx +11 -0
  139. package/src/components/chat/skins/webpage-assistant/pages.ts +9 -0
  140. package/src/components/chat/skins/webpage-assistant/types.ts +178 -0
  141. package/src/components/chat/skins/webpage-assistant/use-mobile.ts +19 -0
  142. package/src/components/chat/stock/SOURCE.md +90 -0
  143. package/src/components/chat/stock/artifacts-panel.tsx +305 -0
  144. package/src/components/chat/stock/attachment.aui.tsx +280 -0
  145. package/src/components/chat/stock/chat-notice.tsx +143 -0
  146. package/src/components/chat/stock/chat-private-mode.tsx +64 -0
  147. package/src/components/chat/stock/deploy-ui-context.tsx +89 -0
  148. package/src/components/chat/stock/embed-chat-prefs.tsx +471 -0
  149. package/src/components/chat/stock/file.tsx +265 -0
  150. package/src/components/chat/stock/follow-up-suggestions.aui.tsx +83 -0
  151. package/src/components/chat/stock/image.tsx +531 -0
  152. package/src/components/chat/stock/index.ts +129 -0
  153. package/src/components/chat/stock/json-view.tsx +140 -0
  154. package/src/components/chat/stock/markdown-text.tsx +319 -0
  155. package/src/components/chat/stock/message-error.aui.tsx +96 -0
  156. package/src/components/chat/stock/message-export-menu.tsx +156 -0
  157. package/src/components/chat/stock/palette-row-preview.tsx +26 -0
  158. package/src/components/chat/stock/reasoning.aui.tsx +120 -0
  159. package/src/components/chat/stock/reasoning.tsx +335 -0
  160. package/src/components/chat/stock/skin-chrome.tsx +81 -0
  161. package/src/components/chat/stock/skin-credit.tsx +63 -0
  162. package/src/components/chat/stock/skin-palette-bridge.tsx +235 -0
  163. package/src/components/chat/stock/skin-palette-context.ts +16 -0
  164. package/src/components/chat/stock/skin-runtime.tsx +52 -0
  165. package/src/components/chat/stock/skin-slash-palette.tsx +393 -0
  166. package/src/components/chat/stock/skin-types.ts +217 -0
  167. package/src/components/chat/stock/stock-chat-prefs-host.tsx +501 -0
  168. package/src/components/chat/stock/stock-send-gate.tsx +94 -0
  169. package/src/components/chat/stock/stock-shape.tsx +39 -0
  170. package/src/components/chat/stock/thread.aui.tsx +645 -0
  171. package/src/components/chat/stock/tool-display.ts +6 -0
  172. package/src/components/chat/stock/tool-fallback.aui.tsx +866 -0
  173. package/src/components/chat/stock/tool-group.aui.tsx +244 -0
  174. package/src/components/chat/stock/tool-surface.ts +120 -0
  175. package/src/components/chat/stock/tooltip-icon-button.tsx +49 -0
  176. package/src/components/chat/stock/ui/avatar.tsx +108 -0
  177. package/src/components/chat/stock/ui/button.tsx +81 -0
  178. package/src/components/chat/stock/ui/collapsible.tsx +21 -0
  179. package/src/components/chat/stock/ui/dialog.tsx +163 -0
  180. package/src/components/chat/stock/ui/skeleton.tsx +13 -0
  181. package/src/components/chat/stock/ui/textarea.tsx +20 -0
  182. package/src/components/chat/stock/ui/tooltip.tsx +68 -0
  183. package/src/components/chat/stock/use-attachment-src.ts +42 -0
  184. package/src/components/chat/stock/use-copy-to-clipboard.ts +29 -0
  185. package/src/components/chat/strip-model-boilerplate.ts +42 -0
  186. package/src/components/chat/suggestion-cycle.tsx +230 -0
  187. package/src/components/chat/suggestion-scroll-row.tsx +81 -0
  188. package/src/components/chat/trace-grouping/chain-group-by.ts +44 -0
  189. package/src/components/chat/trace-grouping/chain-state.ts +141 -0
  190. package/src/components/chat/trace-grouping/index.ts +32 -0
  191. package/src/components/chat/trace-grouping/tool-row.ts +74 -0
  192. package/src/components/chat/trace-grouping/use-deploy-work-disclosure.ts +35 -0
  193. package/src/components/chat/trace-grouping/use-work-chain.ts +103 -0
  194. package/src/components/chat/trace-i18n/core.ts +48 -0
  195. package/src/components/chat/trace-i18n/index.tsx +23 -0
  196. package/src/components/chat/trace-i18n/locales/en.ts +24 -0
  197. package/src/components/chat/trace-i18n/locales/fr.ts +28 -0
  198. package/src/components/chat/trace-i18n/types.ts +35 -0
  199. package/src/components/chat/trace-spacing.ts +249 -0
  200. package/src/components/chat/transcript/activity-view.ts +375 -0
  201. package/src/components/chat/transcript/chat-export.ts +133 -0
  202. package/src/components/chat/transcript/index.ts +10 -0
  203. package/src/components/chat/transcript/save-file.ts +209 -0
  204. package/src/components/chat/transcript/transcript-markdown.ts +428 -0
  205. package/src/components/chat/transcript/types.ts +81 -0
  206. package/src/components/chat/turn-outcome.ts +52 -0
  207. package/src/components/chat/use-warmup-phase.ts +18 -0
  208. package/src/components/chat/work-status-lead.tsx +49 -0
  209. package/src/components/chrome.tsx +264 -0
  210. package/src/components/command-palette/CommandPalette.tsx +404 -0
  211. package/src/components/command-palette/index.ts +6 -0
  212. package/src/components/contact/ContactMailto.tsx +112 -0
  213. package/src/components/contact/index.ts +5 -0
  214. package/src/components/conviction/ConvictionMeter.tsx +40 -0
  215. package/src/components/conviction/SignedConvictionBadge.tsx +26 -0
  216. package/src/components/conviction/index.ts +7 -0
  217. package/src/components/data-layout/BentoGrid.tsx +52 -0
  218. package/src/components/data-layout/CardRail.tsx +150 -0
  219. package/src/components/data-layout/DotMatrixStat.tsx +65 -0
  220. package/src/components/data-layout/FeatureCell.tsx +80 -0
  221. package/src/components/data-layout/MediaFrame.tsx +125 -0
  222. package/src/components/data-layout/ModuleGrid.tsx +735 -0
  223. package/src/components/data-layout/Odometer.tsx +149 -0
  224. package/src/components/data-layout/ProductFrame.tsx +79 -0
  225. package/src/components/data-layout/TestimonialWall.tsx +139 -0
  226. package/src/components/data-layout/index.ts +19 -0
  227. package/src/components/data-layout/treemap.ts +368 -0
  228. package/src/components/deck/DeckStack.tsx +243 -0
  229. package/src/components/desk/DataTable.tsx +373 -0
  230. package/src/components/desk/KpiGrid.tsx +80 -0
  231. package/src/components/desk/Sparkline.tsx +63 -0
  232. package/src/components/desk/StateBlock.tsx +132 -0
  233. package/src/components/desk/index.ts +18 -0
  234. package/src/components/diagrams/ArchitectureDiagram.tsx +350 -0
  235. package/src/components/diagrams/ArchitectureTour.tsx +912 -0
  236. package/src/components/diagrams/TerminalSchematic.tsx +192 -0
  237. package/src/components/diagrams/architecture-svg.tsx +384 -0
  238. package/src/components/diagrams/index.ts +30 -0
  239. package/src/components/diagrams/mermaid-theme.ts +115 -0
  240. package/src/components/diagrams/tour-camera.ts +171 -0
  241. package/src/components/docs/CodeTabs.tsx +90 -0
  242. package/src/components/docs/CopyCommand.tsx +151 -0
  243. package/src/components/docs/DocsLayout.tsx +197 -0
  244. package/src/components/docs/DocsSearch.tsx +97 -0
  245. package/src/components/docs/Endpoint.tsx +135 -0
  246. package/src/components/document/DocumentFrame.tsx +43 -0
  247. package/src/components/document/GlyphList.tsx +51 -0
  248. package/src/components/document/PageTitle.tsx +44 -0
  249. package/src/components/document/Prose.tsx +38 -0
  250. package/src/components/document/Section.tsx +55 -0
  251. package/src/components/document/index.ts +13 -0
  252. package/src/components/effects-chrome/AnnouncementBar.tsx +88 -0
  253. package/src/components/effects-chrome/HorizontalScrollTrack.tsx +461 -0
  254. package/src/components/effects-chrome/Pipeline.tsx +226 -0
  255. package/src/components/effects-chrome/RotatingPrompts.tsx +116 -0
  256. package/src/components/effects-chrome/StackingPanels.tsx +101 -0
  257. package/src/components/effects-chrome/TabStrip.tsx +232 -0
  258. package/src/components/effects-chrome/ToastStack.tsx +201 -0
  259. package/src/components/effects-chrome/horizontal-track-core.ts +70 -0
  260. package/src/components/effects-chrome/index.ts +35 -0
  261. package/src/components/emblems.tsx +188 -0
  262. package/src/components/faviconTheme.ts +23 -0
  263. package/src/components/figure/Figure.tsx +38 -0
  264. package/src/components/figure/index.ts +2 -0
  265. package/src/components/finance-charts/DrawdownPlot.tsx +76 -0
  266. package/src/components/finance-charts/EquityCurve.tsx +71 -0
  267. package/src/components/finance-charts/PriceChart.tsx +119 -0
  268. package/src/components/finance-charts/chart-host.tsx +236 -0
  269. package/src/components/finance-charts/chart-scale.ts +93 -0
  270. package/src/components/finance-charts/demo-data.ts +88 -0
  271. package/src/components/finance-charts/index.ts +46 -0
  272. package/src/components/finance-charts/lw-chart.tsx +292 -0
  273. package/src/components/finance-composites/MarketBar.tsx +243 -0
  274. package/src/components/finance-composites/OrderBook.tsx +133 -0
  275. package/src/components/finance-composites/PerformanceDashboard.tsx +185 -0
  276. package/src/components/finance-composites/SortableTable.tsx +144 -0
  277. package/src/components/finance-composites/StockTicker.tsx +65 -0
  278. package/src/components/finance-composites/SyncedTearsheet.tsx +226 -0
  279. package/src/components/finance-composites/index.ts +28 -0
  280. package/src/components/finance-tearsheet/KpiStrip.tsx +49 -0
  281. package/src/components/finance-tearsheet/LiveBadge.tsx +29 -0
  282. package/src/components/finance-tearsheet/ReturnsMatrix.tsx +321 -0
  283. package/src/components/finance-tearsheet/StrategyTearsheet.tsx +99 -0
  284. package/src/components/finance-tearsheet/TearsheetCard.tsx +54 -0
  285. package/src/components/finance-tearsheet/TradeLogTable.tsx +73 -0
  286. package/src/components/finance-tearsheet/charts.tsx +2624 -0
  287. package/src/components/finance-tearsheet/demo-data.ts +177 -0
  288. package/src/components/finance-tearsheet/format.ts +41 -0
  289. package/src/components/finance-tearsheet/index.ts +120 -0
  290. package/src/components/finance-tearsheet/print.ts +52 -0
  291. package/src/components/finance-tearsheet/risk-bands.ts +48 -0
  292. package/src/components/finance-tearsheet/stats.ts +35 -0
  293. package/src/components/finance-tearsheet/strategy-sheet-model.ts +238 -0
  294. package/src/components/finance-tearsheet/types.ts +58 -0
  295. package/src/components/footer/FooterCells.tsx +109 -0
  296. package/src/components/footer/index.ts +6 -0
  297. package/src/components/graph.tsx +158 -0
  298. package/src/components/icons.tsx +98 -0
  299. package/src/components/landing/PixelWordmark.tsx +36 -0
  300. package/src/components/logos.ts +39 -0
  301. package/src/components/marquee/Marquee.tsx +204 -0
  302. package/src/components/metrics/PerfMetrics.tsx +68 -0
  303. package/src/components/metrics/StatCounter.tsx +104 -0
  304. package/src/components/page-geometry.tsx +177 -0
  305. package/src/components/pricing/Pricing.tsx +174 -0
  306. package/src/components/pricing/PricingMatrix.tsx +219 -0
  307. package/src/components/prose/index.ts +2 -0
  308. package/src/components/prose/prose.tsx +86 -0
  309. package/src/components/repo-activity/RepoActivity.tsx +599 -0
  310. package/src/components/repo-activity/RepoHeatmap.tsx +225 -0
  311. package/src/components/repo-activity/demo.ts +201 -0
  312. package/src/components/repo-activity/fetch.ts +211 -0
  313. package/src/components/repo-activity/heatmap.ts +123 -0
  314. package/src/components/repo-activity/index.ts +28 -0
  315. package/src/components/repo-activity/types.ts +99 -0
  316. package/src/components/stages/NumberedStages.tsx +116 -0
  317. package/src/components/symbols/marks.tsx +116 -0
  318. package/src/components/symbols/terminal-lockup.tsx +136 -0
  319. package/src/components/symbols/terminal-marks.tsx +226 -0
  320. package/src/components/terminal/ChatResponseLoader.tsx +67 -0
  321. package/src/components/terminal/ContainerBootLoader.tsx +82 -0
  322. package/src/components/terminal/TerminalStepCaret.tsx +202 -0
  323. package/src/components/themeInit.ts +24 -0
  324. package/src/components/typography/MotionReveal.tsx +108 -0
  325. package/src/components/typography/TypewriterReveal.tsx +101 -0
  326. package/src/components/typography/WordReveal.tsx +143 -0
  327. package/src/data/modules.ts +378 -0
  328. package/src/data/subsystems.ts +101 -0
  329. package/src/index.ts +587 -0
  330. package/src/lib/base-path.ts +60 -0
  331. package/src/lib/cx.ts +25 -0
  332. package/src/lib/gloomberb.ts +69 -0
  333. package/src/lib/record.ts +19 -0
  334. package/src/lib/stub-payload.ts +183 -0
  335. package/src/lib/useBodyScrollLock.ts +44 -0
  336. package/src/lib/utils.ts +6 -0
  337. package/src/motion/primitives.tsx +105 -0
  338. package/src/motion/scroll-glide.ts +62 -0
  339. package/src/motion/scrolly-core.ts +38 -0
  340. package/src/motion/scrolly.tsx +95 -0
  341. package/src/navigation/HashScrollManager.tsx +167 -0
  342. package/src/navigation/hashScroll.ts +78 -0
  343. package/src/replay/browser.ts +280 -0
  344. package/src/replay/chat-export-run.ts +121 -0
  345. package/src/replay/export-format.ts +25 -0
  346. package/src/replay/export-mount.ts +32 -0
  347. package/src/replay/filename.ts +54 -0
  348. package/src/replay/index.ts +21 -0
  349. package/src/replay/redact-dom.ts +178 -0
  350. package/src/replay/redact.ts +113 -0
  351. package/src/replay/snapshot.ts +659 -0
  352. package/src/styles/account-auth.css +339 -0
  353. package/src/styles/brand-wordmark.css +11 -0
  354. package/src/styles/card-rail.css +120 -0
  355. package/src/styles/chat-aui.css +1143 -0
  356. package/src/styles/chat-core.css +469 -0
  357. package/src/styles/chat-digichat.css +2531 -0
  358. package/src/styles/chat-math.css +70 -0
  359. package/src/styles/chat-skin-palette.css +138 -0
  360. package/src/styles/chat-stock-menu.css +37 -0
  361. package/src/styles/chat-widgets.css +190 -0
  362. package/src/styles/command-palette.css +372 -0
  363. package/src/styles/controls-core.css +2261 -0
  364. package/src/styles/controls-overlay.css +603 -0
  365. package/src/styles/data-layout.css +288 -0
  366. package/src/styles/deck.css +141 -0
  367. package/src/styles/diagrams.css +1860 -0
  368. package/src/styles/digichat-app-theme.css +250 -0
  369. package/src/styles/digichat-boot-loader.css +487 -0
  370. package/src/styles/digichat-launcher.css +377 -0
  371. package/src/styles/docs.css +165 -0
  372. package/src/styles/effects-chrome.css +888 -0
  373. package/src/styles/finance-composites.css +214 -0
  374. package/src/styles/finance-tearsheet-overlay.css +90 -0
  375. package/src/styles/finance-tearsheet.css +945 -0
  376. package/src/styles/marquee.css +168 -0
  377. package/src/styles/metrics.css +82 -0
  378. package/src/styles/nav-shell.css +551 -0
  379. package/src/styles/pricing.css +79 -0
  380. package/src/styles/repo-activity.css +575 -0
  381. package/src/styles/stages.css +69 -0
  382. package/src/styles/terminal-loaders.css +122 -0
  383. package/src/styles/terminal-manifest.css +228 -0
  384. package/src/styles/web-theme.css +1007 -0
  385. package/src/styles/word-reveal.css +204 -0
  386. package/src/theme/apply-theme.ts +214 -0
  387. package/src/theme/index.ts +27 -0
  388. package/src/theme/palette-registry.ts +855 -0
  389. package/src/theme/tui-prefs.ts +64 -0
  390. package/src/ui/alert.tsx +78 -0
  391. package/src/ui/avatar.tsx +104 -0
  392. package/src/ui/badge.tsx +75 -0
  393. package/src/ui/breadcrumb.tsx +61 -0
  394. package/src/ui/button.tsx +67 -0
  395. package/src/ui/card.tsx +139 -0
  396. package/src/ui/checkbox.tsx +27 -0
  397. package/src/ui/collapsible.tsx +32 -0
  398. package/src/ui/date-pager.tsx +325 -0
  399. package/src/ui/dialog.tsx +157 -0
  400. package/src/ui/dropdown-menu.tsx +267 -0
  401. package/src/ui/empty-state.tsx +165 -0
  402. package/src/ui/field.tsx +104 -0
  403. package/src/ui/form.tsx +36 -0
  404. package/src/ui/icon-button.tsx +36 -0
  405. package/src/ui/index.ts +34 -0
  406. package/src/ui/input.tsx +33 -0
  407. package/src/ui/kbd.tsx +30 -0
  408. package/src/ui/label.tsx +32 -0
  409. package/src/ui/pager.tsx +119 -0
  410. package/src/ui/pagination.tsx +126 -0
  411. package/src/ui/radio-group.tsx +51 -0
  412. package/src/ui/search-bar.tsx +85 -0
  413. package/src/ui/segmented-control.tsx +76 -0
  414. package/src/ui/select.tsx +257 -0
  415. package/src/ui/separator.tsx +24 -0
  416. package/src/ui/sheet.tsx +141 -0
  417. package/src/ui/skeleton.tsx +76 -0
  418. package/src/ui/slider.tsx +76 -0
  419. package/src/ui/switch.tsx +31 -0
  420. package/src/ui/table.tsx +179 -0
  421. package/src/ui/tabs.tsx +89 -0
  422. package/src/ui/tags-input.tsx +168 -0
  423. package/src/ui/textarea.tsx +17 -0
  424. package/src/ui/tooltip.tsx +71 -0
@@ -0,0 +1,167 @@
1
+ "use client";
2
+
3
+ import { useEffect } from "react";
4
+ import {
5
+ hashIdFromHref,
6
+ instantScrollToHash,
7
+ instantScrollToId,
8
+ isSamePageHashHref,
9
+ readScrollAnchor,
10
+ scrollTopForAnchor,
11
+ type ScrollAnchor,
12
+ } from "./hashScroll";
13
+
14
+ const STORE = "dg-scroll:";
15
+ /** Late layout (fonts, measured grids) can still move content this long after load. */
16
+ const RESTORE_MS = 1500;
17
+ const RESIZE_SETTLE_MS = 300;
18
+
19
+ const storeKey = () => `${STORE}${window.location.pathname}${window.location.search}`;
20
+
21
+ function readSaved(): ScrollAnchor | null {
22
+ try {
23
+ const raw = window.sessionStorage.getItem(storeKey());
24
+ const saved = raw ? (JSON.parse(raw) as ScrollAnchor) : null;
25
+ return saved && typeof saved.y === "number" && typeof saved.frac === "number" ? saved : null;
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ function save(anchor: ScrollAnchor) {
32
+ try {
33
+ window.sessionStorage.setItem(storeKey(), JSON.stringify(anchor));
34
+ } catch {
35
+ /* storage full or blocked: position keeping is best-effort */
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Owns page scroll position:
41
+ * - same-page hash links jump instantly to their target section instead of
42
+ * smooth-scrolling through tall scroll-driven blocks (pipeline, card stack, etc.);
43
+ * - a reload or back/forward lands where the reader was, not on the URL hash;
44
+ * - a width change (browser zoom, rotation, window resize) keeps the reader on
45
+ * the same point of the same section. Height-only changes on touch devices
46
+ * are ignored — that is the mobile URL bar, and it moves under a live thumb.
47
+ */
48
+ export function HashScrollManager() {
49
+ useEffect(() => {
50
+ const nav = performance.getEntriesByType("navigation")[0] as
51
+ PerformanceNavigationTiming | undefined;
52
+ const returning = nav?.type === "reload" || nav?.type === "back_forward";
53
+ const saved = returning ? readSaved() : null;
54
+
55
+ let anchor: ScrollAnchor = saved ?? readScrollAnchor();
56
+ let holdUntil = 0;
57
+ let frame = 0;
58
+ let saveTimer = 0;
59
+ const timers: number[] = [];
60
+ let width = window.innerWidth;
61
+ let height = window.innerHeight;
62
+ const coarse = window.matchMedia("(pointer: coarse)").matches;
63
+
64
+ const place = () => {
65
+ if (performance.now() > holdUntil) return;
66
+ window.scrollTo({ top: Math.max(0, scrollTopForAnchor(anchor)), behavior: "instant" });
67
+ };
68
+ const hold = (ms: number) => {
69
+ holdUntil = Math.max(holdUntil, performance.now() + ms);
70
+ };
71
+ const release = () => {
72
+ holdUntil = 0;
73
+ if (window.history.scrollRestoration === "manual") window.history.scrollRestoration = "auto";
74
+ };
75
+
76
+ if (saved) {
77
+ window.history.scrollRestoration = "manual";
78
+ hold(RESTORE_MS);
79
+ requestAnimationFrame(place);
80
+ for (const ms of [250, 700, RESTORE_MS - 100]) timers.push(window.setTimeout(place, ms));
81
+ timers.push(window.setTimeout(release, RESTORE_MS));
82
+ void document.fonts?.ready.then(place);
83
+ } else if (window.location.hash) {
84
+ requestAnimationFrame(() => instantScrollToHash(window.location.hash));
85
+ }
86
+
87
+ const onScroll = () => {
88
+ if (performance.now() < holdUntil) return;
89
+ /* A width change (zoom/rotation/resize) is in flight: the scroll event it
90
+ produces must not overwrite the anchor, otherwise the resize handler
91
+ would re-place to a lower section and cut off the hero. */
92
+ if (window.innerWidth !== width) return;
93
+ cancelAnimationFrame(frame);
94
+ frame = requestAnimationFrame(() => {
95
+ anchor = readScrollAnchor();
96
+ window.clearTimeout(saveTimer);
97
+ saveTimer = window.setTimeout(() => save(anchor), 150);
98
+ });
99
+ };
100
+
101
+ const onResize = () => {
102
+ const w = window.innerWidth;
103
+ const h = window.innerHeight;
104
+ const moved = w !== width || (h !== height && !coarse);
105
+ width = w;
106
+ height = h;
107
+ if (!moved) return;
108
+ hold(RESIZE_SETTLE_MS + 50);
109
+ cancelAnimationFrame(frame);
110
+ frame = requestAnimationFrame(place);
111
+ timers.push(window.setTimeout(place, RESIZE_SETTLE_MS));
112
+ };
113
+
114
+ /* The reader takes over: stop steering. */
115
+ const onInput = () => {
116
+ if (holdUntil) release();
117
+ };
118
+
119
+ const onClick = (event: MouseEvent) => {
120
+ if (event.defaultPrevented) return;
121
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
122
+
123
+ const link = (event.target as Element | null)?.closest("a[href]");
124
+ if (!(link instanceof HTMLAnchorElement)) return;
125
+
126
+ const href = link.getAttribute("href");
127
+ if (!href || !isSamePageHashHref(href)) return;
128
+
129
+ const id = hashIdFromHref(href);
130
+ if (!id) return;
131
+
132
+ event.preventDefault();
133
+ const url = new URL(href, window.location.href);
134
+ window.history.pushState(null, "", `${url.pathname}${url.search}${url.hash}`);
135
+ instantScrollToId(id);
136
+ };
137
+
138
+ const onHashChange = () => {
139
+ if (!window.location.hash) return;
140
+ instantScrollToHash(window.location.hash);
141
+ };
142
+
143
+ const onHide = () => save(readScrollAnchor());
144
+
145
+ const inputs = ["wheel", "touchstart", "keydown", "pointerdown"] as const;
146
+ for (const type of inputs) window.addEventListener(type, onInput, { passive: true });
147
+ window.addEventListener("scroll", onScroll, { passive: true });
148
+ window.addEventListener("resize", onResize);
149
+ window.addEventListener("pagehide", onHide);
150
+ document.addEventListener("click", onClick);
151
+ window.addEventListener("hashchange", onHashChange);
152
+
153
+ return () => {
154
+ cancelAnimationFrame(frame);
155
+ window.clearTimeout(saveTimer);
156
+ for (const id of timers) window.clearTimeout(id);
157
+ for (const type of inputs) window.removeEventListener(type, onInput);
158
+ window.removeEventListener("scroll", onScroll);
159
+ window.removeEventListener("resize", onResize);
160
+ window.removeEventListener("pagehide", onHide);
161
+ document.removeEventListener("click", onClick);
162
+ window.removeEventListener("hashchange", onHashChange);
163
+ };
164
+ }, []);
165
+
166
+ return null;
167
+ }
@@ -0,0 +1,78 @@
1
+ /** Whether `href` targets a hash on the current document (same origin + path). */
2
+ export function isSamePageHashHref(href: string, locationHref = window.location.href): boolean {
3
+ if (!href || href.startsWith("mailto:") || href.startsWith("tel:")) return false;
4
+
5
+ let url: URL;
6
+ try {
7
+ url = new URL(href, locationHref);
8
+ } catch {
9
+ return false;
10
+ }
11
+
12
+ const current = new URL(locationHref);
13
+ if (url.origin !== current.origin || !url.hash) return false;
14
+
15
+ const normalize = (pathname: string) => {
16
+ if (pathname === "/index.html") return "/";
17
+ return pathname;
18
+ };
19
+
20
+ return normalize(url.pathname) === normalize(current.pathname);
21
+ }
22
+
23
+ export function hashIdFromHref(href: string, locationHref = window.location.href): string | null {
24
+ if (!isSamePageHashHref(href, locationHref)) return null;
25
+ const hash = new URL(href, locationHref).hash;
26
+ return hash.length > 1 ? hash.slice(1) : null;
27
+ }
28
+
29
+ /** Jump directly to a section — no smooth scroll through intermediate scroll-driven UI. */
30
+ export function instantScrollToId(id: string): boolean {
31
+ const el = document.getElementById(id);
32
+ if (!el) return false;
33
+ el.scrollIntoView({ behavior: "instant", block: "start" });
34
+ return true;
35
+ }
36
+
37
+ export function instantScrollToHash(hash: string): boolean {
38
+ const id = hash.replace(/^#/, "");
39
+ return id ? instantScrollToId(id) : false;
40
+ }
41
+
42
+ /**
43
+ * Where the reader is, as content rather than pixels: the innermost
44
+ * `main section[id]` under the line just below the fixed bar, and how far
45
+ * through it that line sits. Viewport-sized bands (pinned tours, scrolly
46
+ * grids) change height with the viewport, so a raw scrollY lands somewhere
47
+ * else after a zoom; the same fraction of the same section does not.
48
+ */
49
+ export interface ScrollAnchor {
50
+ id: string | null;
51
+ frac: number;
52
+ y: number;
53
+ }
54
+
55
+ function anchorLine(): number {
56
+ const pad = Number.parseFloat(getComputedStyle(document.documentElement).scrollPaddingTop);
57
+ return (Number.isFinite(pad) ? pad : 0) + 1;
58
+ }
59
+
60
+ export function readScrollAnchor(): ScrollAnchor {
61
+ const line = anchorLine();
62
+ let anchor: ScrollAnchor = { id: null, frac: 0, y: window.scrollY };
63
+ if (window.scrollY < 1) return anchor;
64
+ for (const el of document.querySelectorAll<HTMLElement>("main section[id]")) {
65
+ const box = el.getBoundingClientRect();
66
+ if (box.height > 0 && box.top <= line && box.bottom > line) {
67
+ anchor = { id: el.id, frac: (line - box.top) / box.height, y: window.scrollY };
68
+ }
69
+ }
70
+ return anchor;
71
+ }
72
+
73
+ export function scrollTopForAnchor(anchor: ScrollAnchor): number {
74
+ const el = anchor.id ? document.getElementById(anchor.id) : null;
75
+ if (!el) return anchor.y;
76
+ const box = el.getBoundingClientRect();
77
+ return window.scrollY + box.top + anchor.frac * box.height - anchorLine();
78
+ }
@@ -0,0 +1,280 @@
1
+ /**
2
+ * Download the active thread as one HTML file. Styles and same-origin images
3
+ * come from the live page. The rest of the page (other threads, the memory
4
+ * sidebar) is not included.
5
+ */
6
+
7
+ import { downloadTextFile } from "../components/chat/transcript";
8
+ import { setExportKeepMounted } from "./export-mount";
9
+ import { snapshotFilename } from "./filename";
10
+ import {
11
+ deploymentText,
12
+ keepAnswerOnly,
13
+ messageAnchor,
14
+ omitScrollLockCss,
15
+ questionText,
16
+ serializeSnapshot,
17
+ snapshotSource,
18
+ transformSnapshotDom,
19
+ } from "./snapshot";
20
+
21
+ // Bounded runs keep this linear on page CSS (CodeQL js/polynomial-redos). The
22
+ // value cannot start or end with whitespace, so the \s runs never compete with it.
23
+ const URL_RE =
24
+ /url\([ \t\n]{0,16}(['"]?)([^'"()\s](?:[^'"()]{0,4094}[^'"()\s])?)\1[ \t\n]{0,16}\)/g;
25
+
26
+ /** One slow asset must not hang the export; it is dropped instead. */
27
+ const ASSET_TIMEOUT_MS = 8000;
28
+
29
+ function fetchAsset(fetchImpl: typeof fetch, url: string): Promise<Response> {
30
+ const signal =
31
+ typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function"
32
+ ? AbortSignal.timeout(ASSET_TIMEOUT_MS)
33
+ : undefined;
34
+ return fetchImpl(url, signal ? { signal } : undefined);
35
+ }
36
+
37
+ export function liveSheetText(
38
+ doc: Document | null = typeof document === "undefined" ? null : document,
39
+ ): string {
40
+ if (!doc) return "";
41
+ const chunks: string[] = [];
42
+ for (const sheet of Array.from(doc.styleSheets)) {
43
+ try {
44
+ for (const rule of Array.from(sheet.cssRules)) {
45
+ const text = omitScrollLockCss(rule.cssText);
46
+ if (text.trim()) chunks.push(text);
47
+ }
48
+ } catch {
49
+ /* A cross-origin sheet cannot be read. Those rules are dropped. */
50
+ }
51
+ }
52
+ return chunks.join("\n");
53
+ }
54
+
55
+ export async function inlineAssetUrls(
56
+ css: string,
57
+ options: {
58
+ origin?: string;
59
+ href?: string;
60
+ fetchImpl?: typeof fetch;
61
+ } = {},
62
+ ): Promise<string> {
63
+ const origin = options.origin ?? (typeof location === "undefined" ? "" : location.origin);
64
+ const href = options.href ?? (typeof location === "undefined" ? "" : location.href);
65
+ const fetchImpl = options.fetchImpl ?? fetch;
66
+ URL_RE.lastIndex = 0;
67
+ if (!origin || origin === "null") return css;
68
+ const replacements = new Map<string, string>();
69
+ for (const match of css.matchAll(URL_RE)) {
70
+ const raw = match[2];
71
+ if (!raw || raw.startsWith("data:") || raw.startsWith("#")) continue;
72
+ if (replacements.has(raw)) continue;
73
+ const absolute = sameOriginUrl(raw, origin, href);
74
+ if (!absolute) {
75
+ replacements.set(raw, "");
76
+ continue;
77
+ }
78
+ try {
79
+ const res = await fetchAsset(fetchImpl, absolute);
80
+ if (!res.ok) {
81
+ replacements.set(raw, "");
82
+ continue;
83
+ }
84
+ const bytes = new Uint8Array(await res.arrayBuffer());
85
+ const mime = (res.headers.get("content-type") || mimeOf(absolute)).split(";")[0].trim();
86
+ if (!isSavableAsset(mime)) {
87
+ replacements.set(raw, "");
88
+ continue;
89
+ }
90
+ const encoded = bytesToBase64(bytes);
91
+ replacements.set(raw, `data:${mime || "application/octet-stream"};base64,${encoded}`);
92
+ } catch {
93
+ replacements.set(raw, "");
94
+ }
95
+ }
96
+ return css.replace(URL_RE, (full, _quote: string, raw: string) => {
97
+ if (!replacements.has(raw)) return full;
98
+ const data = replacements.get(raw);
99
+ return data ? `url("${data}")` : "none";
100
+ });
101
+ }
102
+
103
+ export async function inlineDomAssets(
104
+ root: ParentNode,
105
+ options: {
106
+ origin?: string;
107
+ href?: string;
108
+ fetchImpl?: typeof fetch;
109
+ } = {},
110
+ ): Promise<void> {
111
+ const origin = options.origin ?? (typeof location === "undefined" ? "" : location.origin);
112
+ const href = options.href ?? (typeof location === "undefined" ? "" : location.href);
113
+ const fetchImpl = options.fetchImpl ?? fetch;
114
+ const nodes = [...root.querySelectorAll<HTMLElement>("img, image, use, source, video")];
115
+ for (const node of nodes) {
116
+ node.removeAttribute("srcset");
117
+ for (const attr of ["src", "href", "poster"] as const) {
118
+ const raw = node.getAttribute(attr);
119
+ if (!raw || raw.startsWith("data:") || raw.startsWith("#")) continue;
120
+ const absolute = origin ? sameOriginUrl(raw, origin, href) : null;
121
+ if (!absolute) {
122
+ node.removeAttribute(attr);
123
+ continue;
124
+ }
125
+ try {
126
+ const res = await fetchAsset(fetchImpl, absolute);
127
+ if (!res.ok) {
128
+ node.removeAttribute(attr);
129
+ continue;
130
+ }
131
+ const bytes = new Uint8Array(await res.arrayBuffer());
132
+ const mime = (res.headers.get("content-type") || mimeOf(absolute)).split(";")[0].trim();
133
+ if (!isSavableAsset(mime)) {
134
+ node.removeAttribute(attr);
135
+ continue;
136
+ }
137
+ node.setAttribute(
138
+ attr,
139
+ `data:${mime || "application/octet-stream"};base64,${bytesToBase64(bytes)}`,
140
+ );
141
+ } catch {
142
+ node.removeAttribute(attr);
143
+ }
144
+ }
145
+ }
146
+ if (!origin) return;
147
+ for (const el of root.querySelectorAll<HTMLElement>("[style]")) {
148
+ const style = el.getAttribute("style");
149
+ if (!style || !style.includes("url(")) continue;
150
+ el.setAttribute("style", await inlineAssetUrls(style, { origin, href, fetchImpl }));
151
+ }
152
+ }
153
+
154
+ export const MISSING_THREAD_MESSAGE = "No open thread to export.";
155
+ export const MISSING_MESSAGE_MESSAGE = "Couldn't find that message to export.";
156
+
157
+ /** A failure the open page can show. The same slot is updated, not stacked. */
158
+ export function showSnapshotError(doc: Document, message: string): void {
159
+ const host = doc.body ?? doc.documentElement;
160
+ if (!host) return;
161
+ let banner = host.querySelector<HTMLElement>("[data-slot='snapshot-export-error']");
162
+ if (!banner) {
163
+ banner = doc.createElement("div");
164
+ banner.setAttribute("role", "alert");
165
+ banner.setAttribute("data-slot", "snapshot-export-error");
166
+ host.prepend(banner);
167
+ }
168
+ banner.textContent = message;
169
+ }
170
+
171
+ async function saveSnapshot(
172
+ doc: Document,
173
+ shell: HTMLElement,
174
+ exportedAt: string,
175
+ question = questionText(shell),
176
+ ): Promise<void> {
177
+ const css = await inlineAssetUrls(liveSheetText(doc));
178
+ transformSnapshotDom(shell);
179
+ await inlineDomAssets(shell);
180
+ const html = serializeSnapshot(shell, css);
181
+ const name = snapshotFilename(deploymentText(doc), exportedAt, question);
182
+ downloadTextFile(name, html, "text/html;charset=utf-8");
183
+ }
184
+
185
+ export async function downloadThreadSnapshot(
186
+ doc: Document | null = typeof document === "undefined" ? null : document,
187
+ exportedAt = new Date().toISOString(),
188
+ anchor?: HTMLElement | null,
189
+ ): Promise<void> {
190
+ if (!doc?.documentElement) throw new Error(MISSING_THREAD_MESSAGE);
191
+ // Closed reasoning and tool panels unmount on the live page. Mount them
192
+ // only for this read, then put the page back.
193
+ setExportKeepMounted(true);
194
+ try {
195
+ const shell = snapshotSource(doc, anchor);
196
+ if (!shell) {
197
+ const message = anchor ? MISSING_MESSAGE_MESSAGE : MISSING_THREAD_MESSAGE;
198
+ showSnapshotError(doc, message);
199
+ throw new Error(message);
200
+ }
201
+ await saveSnapshot(doc, shell, exportedAt);
202
+ } finally {
203
+ setExportKeepMounted(false);
204
+ }
205
+ }
206
+
207
+ /**
208
+ * The message-menu export. `trigger` is the menu button inside that
209
+ * assistant message. A miss shows an error and does not save the thread.
210
+ */
211
+ export async function downloadMessageSnapshot(
212
+ doc: Document | null = typeof document === "undefined" ? null : document,
213
+ exportedAt = new Date().toISOString(),
214
+ messageId = "",
215
+ trigger: HTMLElement | null = null,
216
+ options: { answerOnly?: boolean } = {},
217
+ ): Promise<void> {
218
+ if (!doc?.documentElement) throw new Error(MISSING_THREAD_MESSAGE);
219
+ setExportKeepMounted(true);
220
+ try {
221
+ const anchor = messageAnchor(trigger, messageId);
222
+ if (!anchor?.isConnected) {
223
+ showSnapshotError(doc, MISSING_MESSAGE_MESSAGE);
224
+ throw new Error(MISSING_MESSAGE_MESSAGE);
225
+ }
226
+ const shell = snapshotSource(doc, anchor);
227
+ if (!shell) {
228
+ showSnapshotError(doc, MISSING_MESSAGE_MESSAGE);
229
+ throw new Error(MISSING_MESSAGE_MESSAGE);
230
+ }
231
+ // The question names the file even when the file holds the answer only.
232
+ const question = questionText(shell);
233
+ if (options.answerOnly !== false) keepAnswerOnly(shell);
234
+ await saveSnapshot(doc, shell, exportedAt, question);
235
+ } finally {
236
+ setExportKeepMounted(false);
237
+ }
238
+ }
239
+
240
+ function sameOriginUrl(raw: string, origin: string, href: string): string | null {
241
+ try {
242
+ const url = new URL(raw, href || origin);
243
+ if (url.origin !== origin) return null;
244
+ return url.href;
245
+ } catch {
246
+ return null;
247
+ }
248
+ }
249
+
250
+ /** JSON and HTML bodies are not assets. Only images and fonts are inlined. */
251
+ function isSavableAsset(mime: string): boolean {
252
+ const type = mime.toLowerCase();
253
+ return (
254
+ type.startsWith("image/") ||
255
+ type.startsWith("font/") ||
256
+ type === "application/font-woff" ||
257
+ type === "application/font-woff2" ||
258
+ type === "application/x-font-woff"
259
+ );
260
+ }
261
+
262
+ function mimeOf(url: string): string {
263
+ const path = url.split("?")[0]?.toLowerCase() ?? "";
264
+ if (path.endsWith(".woff2")) return "font/woff2";
265
+ if (path.endsWith(".woff")) return "font/woff";
266
+ if (path.endsWith(".svg")) return "image/svg+xml";
267
+ if (path.endsWith(".png")) return "image/png";
268
+ if (path.endsWith(".css")) return "text/css";
269
+ return "application/octet-stream";
270
+ }
271
+
272
+ function bytesToBase64(bytes: Uint8Array): string {
273
+ if (typeof Buffer !== "undefined") return Buffer.from(bytes).toString("base64");
274
+ let binary = "";
275
+ const size = 0x8000;
276
+ for (let i = 0; i < bytes.length; i += size) {
277
+ binary += String.fromCharCode(...bytes.subarray(i, i + size));
278
+ }
279
+ return btoa(binary);
280
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Run an export: the conversation or one assistant message, as markdown or
3
+ * HTML. Every skin's message menu and the chat-level `/export` call these,
4
+ * so the formats, file names and errors are the same everywhere.
5
+ */
6
+
7
+ import {
8
+ chatExportStrings,
9
+ conversationMarkdown,
10
+ downloadMarkdown,
11
+ lastQuestion,
12
+ messageMarkdown,
13
+ transcriptTurn,
14
+ transcriptTurns,
15
+ type ChatExportFormat,
16
+ type ChatExportStrings,
17
+ } from "../components/chat/transcript";
18
+ import { isChatPrivateMode } from "../components/chat/stock/chat-private-mode";
19
+ import { downloadMessageSnapshot, downloadThreadSnapshot, showSnapshotError } from "./browser";
20
+ import { exportFilename } from "./filename";
21
+ import { deploymentText } from "./snapshot";
22
+
23
+ const EXPORT_FAILED = "Export failed.";
24
+
25
+ /**
26
+ * A private session exports nothing: the stored messages still hold the real
27
+ * typed text, and the file name carries a slug of the question. The menus
28
+ * already hide the rows; this is the guard behind them.
29
+ */
30
+ function exportBlocked(input: { privateMode?: boolean | undefined }): boolean {
31
+ return input.privateMode === true || isChatPrivateMode();
32
+ }
33
+
34
+ function liveDocument(): Document | null {
35
+ return typeof document === "undefined" ? null : document;
36
+ }
37
+
38
+ function reportError(doc: Document | null, error: unknown): void {
39
+ const message = error instanceof Error ? error.message : EXPORT_FAILED;
40
+ if (doc) showSnapshotError(doc, message);
41
+ console.error("chat export failed", error);
42
+ }
43
+
44
+ export type ConversationExportInput = {
45
+ format: ChatExportFormat;
46
+ /** Runtime thread messages, in order. */
47
+ messages: readonly unknown[];
48
+ strings?: ChatExportStrings;
49
+ doc?: Document | null;
50
+ now?: string;
51
+ /** The caller's private flag. The shared one is checked as well. */
52
+ privateMode?: boolean | undefined;
53
+ };
54
+
55
+ /**
56
+ * The whole conversation. Resolves after the file is handed to the browser.
57
+ * Saves nothing in a private session.
58
+ */
59
+ export async function exportConversation(input: ConversationExportInput): Promise<void> {
60
+ if (exportBlocked(input)) return;
61
+ const doc = input.doc === undefined ? liveDocument() : input.doc;
62
+ const now = input.now ?? new Date().toISOString();
63
+ try {
64
+ if (input.format === "html") {
65
+ await downloadThreadSnapshot(doc, now);
66
+ return;
67
+ }
68
+ const turns = transcriptTurns(input.messages);
69
+ const md = conversationMarkdown(turns, input.strings ?? chatExportStrings());
70
+ if (!md) return;
71
+ const deployment = doc ? deploymentText(doc) : "";
72
+ downloadMarkdown(exportFilename(deployment, now, lastQuestion(turns), "md"), md);
73
+ } catch (error) {
74
+ reportError(doc, error);
75
+ }
76
+ }
77
+
78
+ export type MessageExportInput = {
79
+ format: ChatExportFormat;
80
+ /** The assistant message's runtime state. */
81
+ message: unknown;
82
+ messageId: string;
83
+ /** The thread's messages, for the question that names the file. */
84
+ messages?: readonly unknown[];
85
+ /** The menu trigger inside that message. HTML needs it to find the message. */
86
+ trigger: HTMLElement | null;
87
+ doc?: Document | null;
88
+ now?: string;
89
+ /** The caller's private flag. The shared one is checked as well. */
90
+ privateMode?: boolean | undefined;
91
+ };
92
+
93
+ /**
94
+ * One assistant message, without the question before it: the answer text
95
+ * only, in both formats. Saves nothing in a private session.
96
+ */
97
+ export async function exportMessage(input: MessageExportInput): Promise<void> {
98
+ if (exportBlocked(input)) return;
99
+ const doc = input.doc === undefined ? liveDocument() : input.doc;
100
+ const now = input.now ?? new Date().toISOString();
101
+ try {
102
+ if (input.format === "html") {
103
+ await downloadMessageSnapshot(doc, now, input.messageId, input.trigger);
104
+ return;
105
+ }
106
+ const md = messageMarkdown(transcriptTurn(input.message));
107
+ if (!md) return;
108
+ const deployment = doc ? deploymentText(doc) : "";
109
+ downloadMarkdown(exportFilename(deployment, now, messageQuestion(input), "md"), md);
110
+ } catch (error) {
111
+ reportError(doc, error);
112
+ }
113
+ }
114
+
115
+ function messageQuestion(input: MessageExportInput): string {
116
+ const all = input.messages ?? [];
117
+ const index = all.findIndex(
118
+ (m) => !!m && typeof m === "object" && (m as { id?: unknown }).id === input.messageId,
119
+ );
120
+ return index < 0 ? "" : lastQuestion(transcriptTurns(all.slice(0, index)));
121
+ }
@@ -0,0 +1,25 @@
1
+ import { showSnapshotError } from "./browser";
2
+
3
+ /** Shown when `/export` is given a format other than markdown or html. */
4
+ export const UNSUPPORTED_EXPORT_FORMAT = "Unsupported format.";
5
+
6
+ /**
7
+ * `/export`, `/export md`, and `/export markdown` save markdown.
8
+ * `/export html` saves the thread. Anything else shows an error.
9
+ */
10
+ export function applyExportSlash(
11
+ format: string,
12
+ doc: Document,
13
+ actions: { html: () => void; markdown: () => void },
14
+ ): void {
15
+ const value = format.trim().toLowerCase();
16
+ if (value === "html") {
17
+ actions.html();
18
+ return;
19
+ }
20
+ if (value === "" || value === "md" || value === "markdown") {
21
+ actions.markdown();
22
+ return;
23
+ }
24
+ showSnapshotError(doc, UNSUPPORTED_EXPORT_FORMAT);
25
+ }