@btst/stack 1.3.1 → 1.4.1

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 (238) hide show
  1. package/README.md +85 -37
  2. package/dist/node_modules/.pnpm/@radix-ui_react-accordion@1.2.12_@types_react-dom@19.2.3_@types_react@19.2.6__@types_re_947719a27ff11ec6f09710dd9e85efc5/node_modules/@radix-ui/react-accordion/dist/index.cjs +321 -0
  3. package/dist/node_modules/.pnpm/@radix-ui_react-accordion@1.2.12_@types_react-dom@19.2.3_@types_react@19.2.6__@types_re_947719a27ff11ec6f09710dd9e85efc5/node_modules/@radix-ui/react-accordion/dist/index.mjs +306 -0
  4. package/dist/node_modules/.pnpm/{@radix-ui_react-alert-dialog@1.1.15_@types_react-dom@19.2.2_@types_react@19.2.2__@types_ec789942cd38340bd7362c09e7d34384 → @radix-ui_react-alert-dialog@1.1.15_@types_react-dom@19.2.3_@types_react@19.2.6__@types_4f58c757aa677e233cf96d60fda2f1da}/node_modules/@radix-ui/react-alert-dialog/dist/index.cjs +2 -2
  5. package/dist/node_modules/.pnpm/{@radix-ui_react-alert-dialog@1.1.15_@types_react-dom@19.2.2_@types_react@19.2.2__@types_ec789942cd38340bd7362c09e7d34384 → @radix-ui_react-alert-dialog@1.1.15_@types_react-dom@19.2.3_@types_react@19.2.6__@types_4f58c757aa677e233cf96d60fda2f1da}/node_modules/@radix-ui/react-alert-dialog/dist/index.mjs +2 -2
  6. package/dist/node_modules/.pnpm/{@radix-ui_react-arrow@1.1.7_@types_react-dom@19.2.2_@types_react@19.2.2__@types_react@1_9e04309f365863673e44407648bb0cb6 → @radix-ui_react-arrow@1.1.7_@types_react-dom@19.2.3_@types_react@19.2.6__@types_react@1_35df44f6d87656c1401686c91d770dbb}/node_modules/@radix-ui/react-arrow/dist/index.cjs +1 -1
  7. package/dist/node_modules/.pnpm/{@radix-ui_react-arrow@1.1.7_@types_react-dom@19.2.2_@types_react@19.2.2__@types_react@1_9e04309f365863673e44407648bb0cb6 → @radix-ui_react-arrow@1.1.7_@types_react-dom@19.2.3_@types_react@19.2.6__@types_react@1_35df44f6d87656c1401686c91d770dbb}/node_modules/@radix-ui/react-arrow/dist/index.mjs +1 -1
  8. package/dist/node_modules/.pnpm/@radix-ui_react-collapsible@1.1.12_@types_react-dom@19.2.3_@types_react@19.2.6__@types__d025a77f62ee83ca6bd8b0ea1f9de738/node_modules/@radix-ui/react-collapsible/dist/index.cjs +168 -0
  9. package/dist/node_modules/.pnpm/@radix-ui_react-collapsible@1.1.12_@types_react-dom@19.2.3_@types_react@19.2.6__@types__d025a77f62ee83ca6bd8b0ea1f9de738/node_modules/@radix-ui/react-collapsible/dist/index.mjs +146 -0
  10. package/dist/node_modules/.pnpm/{@radix-ui_react-collection@1.1.7_@types_react-dom@19.2.2_@types_react@19.2.2__@types_re_6d7c277722b3619c9ee7e64e9a822c45 → @radix-ui_react-collection@1.1.7_@types_react-dom@19.2.3_@types_react@19.2.6__@types_re_59aa5e150e70a3e7bfb1567148b021b5}/node_modules/@radix-ui/react-collection/dist/index.cjs +2 -2
  11. package/dist/node_modules/.pnpm/{@radix-ui_react-collection@1.1.7_@types_react-dom@19.2.2_@types_react@19.2.2__@types_re_6d7c277722b3619c9ee7e64e9a822c45 → @radix-ui_react-collection@1.1.7_@types_react-dom@19.2.3_@types_react@19.2.6__@types_re_59aa5e150e70a3e7bfb1567148b021b5}/node_modules/@radix-ui/react-collection/dist/index.mjs +2 -2
  12. package/dist/node_modules/.pnpm/{@radix-ui_react-dismissable-layer@1.1.11_@types_react-dom@19.2.2_@types_react@19.2.2__@_ca5522e5d45d4722cb9eb5ce53defa61 → @radix-ui_react-dismissable-layer@1.1.11_@types_react-dom@19.2.3_@types_react@19.2.6__@_9ee1db7daf927866cf505b31d40047ad}/node_modules/@radix-ui/react-dismissable-layer/dist/index.cjs +4 -4
  13. package/dist/node_modules/.pnpm/{@radix-ui_react-dismissable-layer@1.1.11_@types_react-dom@19.2.2_@types_react@19.2.2__@_ca5522e5d45d4722cb9eb5ce53defa61 → @radix-ui_react-dismissable-layer@1.1.11_@types_react-dom@19.2.3_@types_react@19.2.6__@_9ee1db7daf927866cf505b31d40047ad}/node_modules/@radix-ui/react-dismissable-layer/dist/index.mjs +4 -4
  14. package/dist/node_modules/.pnpm/@radix-ui_react-dropdown-menu@2.1.16_@types_react-dom@19.2.3_@types_react@19.2.6__@type_a50051c7210b6fbd5be09388bda08578/node_modules/@radix-ui/react-dropdown-menu/dist/index.cjs +282 -0
  15. package/dist/node_modules/.pnpm/@radix-ui_react-dropdown-menu@2.1.16_@types_react-dom@19.2.3_@types_react@19.2.6__@type_a50051c7210b6fbd5be09388bda08578/node_modules/@radix-ui/react-dropdown-menu/dist/index.mjs +247 -0
  16. package/dist/node_modules/.pnpm/{@radix-ui_react-focus-scope@1.1.7_@types_react-dom@19.2.2_@types_react@19.2.2__@types_r_93de389b3f622f9f764acc8e59ec80c0 → @radix-ui_react-focus-scope@1.1.7_@types_react-dom@19.2.3_@types_react@19.2.6__@types_r_0a31b7f987af9482d13505312e1a1be9}/node_modules/@radix-ui/react-focus-scope/dist/index.cjs +3 -3
  17. package/dist/node_modules/.pnpm/{@radix-ui_react-focus-scope@1.1.7_@types_react-dom@19.2.2_@types_react@19.2.2__@types_r_93de389b3f622f9f764acc8e59ec80c0 → @radix-ui_react-focus-scope@1.1.7_@types_react-dom@19.2.3_@types_react@19.2.6__@types_r_0a31b7f987af9482d13505312e1a1be9}/node_modules/@radix-ui/react-focus-scope/dist/index.mjs +3 -3
  18. package/dist/node_modules/.pnpm/{@radix-ui_react-id@1.1.1_@types_react@19.2.2_react@19.2.0 → @radix-ui_react-id@1.1.1_@types_react@19.2.6_react@19.2.0}/node_modules/@radix-ui/react-id/dist/index.cjs +1 -1
  19. package/dist/node_modules/.pnpm/{@radix-ui_react-id@1.1.1_@types_react@19.2.2_react@19.2.0 → @radix-ui_react-id@1.1.1_@types_react@19.2.6_react@19.2.0}/node_modules/@radix-ui/react-id/dist/index.mjs +1 -1
  20. package/dist/node_modules/.pnpm/@radix-ui_react-menu@2.1.16_@types_react-dom@19.2.3_@types_react@19.2.6__@types_react@1_82ed1fcd848b6984d9291a784d477cf4/node_modules/@radix-ui/react-menu/dist/index.cjs +845 -0
  21. package/dist/node_modules/.pnpm/@radix-ui_react-menu@2.1.16_@types_react-dom@19.2.3_@types_react@19.2.6__@types_react@1_82ed1fcd848b6984d9291a784d477cf4/node_modules/@radix-ui/react-menu/dist/index.mjs +799 -0
  22. package/dist/node_modules/.pnpm/{@radix-ui_react-popper@1.2.8_@types_react-dom@19.2.2_@types_react@19.2.2__@types_react@_d6285b8269ea5d6b59b300f5be279a0c → @radix-ui_react-popper@1.2.8_@types_react-dom@19.2.3_@types_react@19.2.6__@types_react@_7ac2caae1e39f9ba93d5015614525161}/node_modules/@radix-ui/react-popper/dist/index.cjs +7 -7
  23. package/dist/node_modules/.pnpm/{@radix-ui_react-popper@1.2.8_@types_react-dom@19.2.2_@types_react@19.2.2__@types_react@_d6285b8269ea5d6b59b300f5be279a0c → @radix-ui_react-popper@1.2.8_@types_react-dom@19.2.3_@types_react@19.2.6__@types_react@_7ac2caae1e39f9ba93d5015614525161}/node_modules/@radix-ui/react-popper/dist/index.mjs +7 -7
  24. package/dist/node_modules/.pnpm/{@radix-ui_react-portal@1.1.9_@types_react-dom@19.2.2_@types_react@19.2.2__@types_react@_478b3d5dd4afab1a3dcce7ed1748cb95 → @radix-ui_react-portal@1.1.9_@types_react-dom@19.2.3_@types_react@19.2.6__@types_react@_1bb4e0f97f86496802d28a2e74e2a8b9}/node_modules/@radix-ui/react-portal/dist/index.cjs +2 -2
  25. package/dist/node_modules/.pnpm/{@radix-ui_react-portal@1.1.9_@types_react-dom@19.2.2_@types_react@19.2.2__@types_react@_478b3d5dd4afab1a3dcce7ed1748cb95 → @radix-ui_react-portal@1.1.9_@types_react-dom@19.2.3_@types_react@19.2.6__@types_react@_1bb4e0f97f86496802d28a2e74e2a8b9}/node_modules/@radix-ui/react-portal/dist/index.mjs +2 -2
  26. package/dist/node_modules/.pnpm/@radix-ui_react-presence@1.1.5_@types_react-dom@19.2.3_@types_react@19.2.6__@types_reac_90f8e5c12233caef3399d5fd66452a13/node_modules/@radix-ui/react-presence/dist/index.cjs +147 -0
  27. package/dist/node_modules/.pnpm/@radix-ui_react-presence@1.1.5_@types_react-dom@19.2.3_@types_react@19.2.6__@types_reac_90f8e5c12233caef3399d5fd66452a13/node_modules/@radix-ui/react-presence/dist/index.mjs +131 -0
  28. package/dist/node_modules/.pnpm/@radix-ui_react-roving-focus@1.1.11_@types_react-dom@19.2.3_@types_react@19.2.6__@types_fe1151d1f393bbc1072b24a86dff3a23/node_modules/@radix-ui/react-roving-focus/dist/index.cjs +244 -0
  29. package/dist/node_modules/.pnpm/@radix-ui_react-roving-focus@1.1.11_@types_react-dom@19.2.3_@types_react@19.2.6__@types_fe1151d1f393bbc1072b24a86dff3a23/node_modules/@radix-ui/react-roving-focus/dist/index.mjs +224 -0
  30. package/dist/node_modules/.pnpm/@radix-ui_react-scroll-area@1.2.10_@types_react-dom@19.2.3_@types_react@19.2.6__@types__e3f7735e9b444a10b3bbfd9fe97d44d0/node_modules/@radix-ui/react-scroll-area/dist/index.cjs +742 -0
  31. package/dist/node_modules/.pnpm/@radix-ui_react-scroll-area@1.2.10_@types_react-dom@19.2.3_@types_react@19.2.6__@types__e3f7735e9b444a10b3bbfd9fe97d44d0/node_modules/@radix-ui/react-scroll-area/dist/index.mjs +719 -0
  32. package/dist/node_modules/.pnpm/{@radix-ui_react-select@2.2.6_@types_react-dom@19.2.2_@types_react@19.2.2__@types_react@_802c3d414c85063bee785fcc98a39c07 → @radix-ui_react-select@2.2.6_@types_react-dom@19.2.3_@types_react@19.2.6__@types_react@_38dc681bb1f2bcfeb5249d8ca2bc01f5}/node_modules/@radix-ui/react-select/dist/index.cjs +17 -17
  33. package/dist/node_modules/.pnpm/{@radix-ui_react-select@2.2.6_@types_react-dom@19.2.2_@types_react@19.2.2__@types_react@_802c3d414c85063bee785fcc98a39c07 → @radix-ui_react-select@2.2.6_@types_react-dom@19.2.3_@types_react@19.2.6__@types_react@_38dc681bb1f2bcfeb5249d8ca2bc01f5}/node_modules/@radix-ui/react-select/dist/index.mjs +17 -17
  34. package/dist/node_modules/.pnpm/{@radix-ui_react-use-controllable-state@1.2.2_@types_react@19.2.2_react@19.2.0 → @radix-ui_react-use-controllable-state@1.2.2_@types_react@19.2.6_react@19.2.0}/node_modules/@radix-ui/react-use-controllable-state/dist/index.cjs +1 -1
  35. package/dist/node_modules/.pnpm/{@radix-ui_react-use-controllable-state@1.2.2_@types_react@19.2.2_react@19.2.0 → @radix-ui_react-use-controllable-state@1.2.2_@types_react@19.2.6_react@19.2.0}/node_modules/@radix-ui/react-use-controllable-state/dist/index.mjs +1 -1
  36. package/dist/node_modules/.pnpm/{@radix-ui_react-use-escape-keydown@1.1.1_@types_react@19.2.2_react@19.2.0 → @radix-ui_react-use-escape-keydown@1.1.1_@types_react@19.2.6_react@19.2.0}/node_modules/@radix-ui/react-use-escape-keydown/dist/index.cjs +1 -1
  37. package/dist/node_modules/.pnpm/{@radix-ui_react-use-escape-keydown@1.1.1_@types_react@19.2.2_react@19.2.0 → @radix-ui_react-use-escape-keydown@1.1.1_@types_react@19.2.6_react@19.2.0}/node_modules/@radix-ui/react-use-escape-keydown/dist/index.mjs +1 -1
  38. package/dist/node_modules/.pnpm/{@radix-ui_react-use-size@1.1.1_@types_react@19.2.2_react@19.2.0 → @radix-ui_react-use-size@1.1.1_@types_react@19.2.6_react@19.2.0}/node_modules/@radix-ui/react-use-size/dist/index.cjs +1 -1
  39. package/dist/node_modules/.pnpm/{@radix-ui_react-use-size@1.1.1_@types_react@19.2.2_react@19.2.0 → @radix-ui_react-use-size@1.1.1_@types_react@19.2.6_react@19.2.0}/node_modules/@radix-ui/react-use-size/dist/index.mjs +1 -1
  40. package/dist/node_modules/.pnpm/{@radix-ui_react-visually-hidden@1.2.3_@types_react-dom@19.2.2_@types_react@19.2.2__@typ_a84e98a44624c31e835a98d4b8b0c30d → @radix-ui_react-visually-hidden@1.2.3_@types_react-dom@19.2.3_@types_react@19.2.6__@typ_f453379bbd5a4932ec515167f81be42a}/node_modules/@radix-ui/react-visually-hidden/dist/index.cjs +1 -1
  41. package/dist/node_modules/.pnpm/{@radix-ui_react-visually-hidden@1.2.3_@types_react-dom@19.2.2_@types_react@19.2.2__@typ_a84e98a44624c31e835a98d4b8b0c30d → @radix-ui_react-visually-hidden@1.2.3_@types_react-dom@19.2.3_@types_react@19.2.6__@typ_f453379bbd5a4932ec515167f81be42a}/node_modules/@radix-ui/react-visually-hidden/dist/index.mjs +1 -1
  42. package/dist/node_modules/.pnpm/{react-remove-scroll-bar@2.3.8_@types_react@19.2.2_react@19.2.0 → react-remove-scroll-bar@2.3.8_@types_react@19.2.6_react@19.2.0}/node_modules/react-remove-scroll-bar/dist/es2015/component.cjs +1 -1
  43. package/dist/node_modules/.pnpm/{react-remove-scroll-bar@2.3.8_@types_react@19.2.2_react@19.2.0 → react-remove-scroll-bar@2.3.8_@types_react@19.2.6_react@19.2.0}/node_modules/react-remove-scroll-bar/dist/es2015/component.mjs +1 -1
  44. package/dist/node_modules/.pnpm/{react-remove-scroll@2.7.1_@types_react@19.2.2_react@19.2.0 → react-remove-scroll@2.7.1_@types_react@19.2.6_react@19.2.0}/node_modules/react-remove-scroll/dist/es2015/SideEffect.cjs +2 -2
  45. package/dist/node_modules/.pnpm/{react-remove-scroll@2.7.1_@types_react@19.2.2_react@19.2.0 → react-remove-scroll@2.7.1_@types_react@19.2.6_react@19.2.0}/node_modules/react-remove-scroll/dist/es2015/SideEffect.mjs +2 -2
  46. package/dist/node_modules/.pnpm/{react-remove-scroll@2.7.1_@types_react@19.2.2_react@19.2.0 → react-remove-scroll@2.7.1_@types_react@19.2.6_react@19.2.0}/node_modules/react-remove-scroll/dist/es2015/UI.cjs +2 -2
  47. package/dist/node_modules/.pnpm/{react-remove-scroll@2.7.1_@types_react@19.2.2_react@19.2.0 → react-remove-scroll@2.7.1_@types_react@19.2.6_react@19.2.0}/node_modules/react-remove-scroll/dist/es2015/UI.mjs +2 -2
  48. package/dist/node_modules/.pnpm/{react-remove-scroll@2.7.1_@types_react@19.2.2_react@19.2.0 → react-remove-scroll@2.7.1_@types_react@19.2.6_react@19.2.0}/node_modules/react-remove-scroll/dist/es2015/medium.cjs +1 -1
  49. package/dist/node_modules/.pnpm/{react-remove-scroll@2.7.1_@types_react@19.2.2_react@19.2.0 → react-remove-scroll@2.7.1_@types_react@19.2.6_react@19.2.0}/node_modules/react-remove-scroll/dist/es2015/medium.mjs +1 -1
  50. package/dist/node_modules/.pnpm/react-remove-scroll@2.7.1_@types_react@19.2.6_react@19.2.0/node_modules/react-remove-scroll/dist/es2015/sidecar.cjs +9 -0
  51. package/dist/node_modules/.pnpm/{react-remove-scroll@2.7.1_@types_react@19.2.2_react@19.2.0 → react-remove-scroll@2.7.1_@types_react@19.2.6_react@19.2.0}/node_modules/react-remove-scroll/dist/es2015/sidecar.mjs +1 -1
  52. package/dist/packages/better-stack/src/plugins/ai-chat/api/plugin.cjs +610 -0
  53. package/dist/packages/better-stack/src/plugins/ai-chat/api/plugin.mjs +608 -0
  54. package/dist/packages/better-stack/src/plugins/ai-chat/client/components/chat-input.cjs +221 -0
  55. package/dist/packages/better-stack/src/plugins/ai-chat/client/components/chat-input.mjs +219 -0
  56. package/dist/packages/better-stack/src/plugins/ai-chat/client/components/chat-interface.cjs +341 -0
  57. package/dist/packages/better-stack/src/plugins/ai-chat/client/components/chat-interface.mjs +339 -0
  58. package/dist/packages/better-stack/src/plugins/ai-chat/client/components/chat-layout.cjs +135 -0
  59. package/dist/packages/better-stack/src/plugins/ai-chat/client/components/chat-layout.mjs +133 -0
  60. package/dist/packages/better-stack/src/plugins/ai-chat/client/components/chat-message.cjs +429 -0
  61. package/dist/packages/better-stack/src/plugins/ai-chat/client/components/chat-message.mjs +423 -0
  62. package/dist/packages/better-stack/src/plugins/ai-chat/client/components/chat-sidebar.cjs +227 -0
  63. package/dist/packages/better-stack/src/plugins/ai-chat/client/components/chat-sidebar.mjs +225 -0
  64. package/dist/packages/better-stack/src/plugins/ai-chat/client/components/loading/chat-page-skeleton.cjs +31 -0
  65. package/dist/packages/better-stack/src/plugins/ai-chat/client/components/loading/chat-page-skeleton.mjs +29 -0
  66. package/dist/packages/better-stack/src/plugins/ai-chat/client/components/loading/index.cjs +11 -0
  67. package/dist/packages/better-stack/src/plugins/ai-chat/client/components/loading/index.mjs +8 -0
  68. package/dist/packages/better-stack/src/plugins/ai-chat/client/components/pages/404-page.cjs +18 -0
  69. package/dist/packages/better-stack/src/plugins/ai-chat/client/components/pages/404-page.mjs +16 -0
  70. package/dist/packages/better-stack/src/plugins/ai-chat/client/components/pages/chat-page.cjs +39 -0
  71. package/dist/packages/better-stack/src/plugins/ai-chat/client/components/pages/chat-page.internal.cjs +22 -0
  72. package/dist/packages/better-stack/src/plugins/ai-chat/client/components/pages/chat-page.internal.mjs +20 -0
  73. package/dist/packages/better-stack/src/plugins/ai-chat/client/components/pages/chat-page.mjs +37 -0
  74. package/dist/packages/better-stack/src/plugins/ai-chat/client/components/shared/default-error.cjs +18 -0
  75. package/dist/packages/better-stack/src/plugins/ai-chat/client/components/shared/default-error.mjs +16 -0
  76. package/dist/packages/better-stack/src/plugins/ai-chat/client/components/shared/error-placeholder.cjs +26 -0
  77. package/dist/packages/better-stack/src/plugins/ai-chat/client/components/shared/error-placeholder.mjs +24 -0
  78. package/dist/packages/better-stack/src/plugins/ai-chat/client/components/tool-call-display.cjs +123 -0
  79. package/dist/packages/better-stack/src/plugins/ai-chat/client/components/tool-call-display.mjs +121 -0
  80. package/dist/packages/better-stack/src/plugins/ai-chat/client/hooks/chat-hooks.cjs +199 -0
  81. package/dist/packages/better-stack/src/plugins/ai-chat/client/hooks/chat-hooks.mjs +191 -0
  82. package/dist/packages/better-stack/src/plugins/ai-chat/client/localization/index.cjs +63 -0
  83. package/dist/packages/better-stack/src/plugins/ai-chat/client/localization/index.mjs +61 -0
  84. package/dist/packages/better-stack/src/plugins/ai-chat/client/overrides.cjs +14 -0
  85. package/dist/packages/better-stack/src/plugins/ai-chat/client/overrides.mjs +11 -0
  86. package/dist/packages/better-stack/src/plugins/ai-chat/client/plugin.cjs +241 -0
  87. package/dist/packages/better-stack/src/plugins/ai-chat/client/plugin.mjs +239 -0
  88. package/dist/packages/better-stack/src/plugins/ai-chat/db.cjs +65 -0
  89. package/dist/packages/better-stack/src/plugins/ai-chat/db.mjs +63 -0
  90. package/dist/packages/better-stack/src/plugins/ai-chat/schemas.cjs +42 -0
  91. package/dist/packages/better-stack/src/plugins/ai-chat/schemas.mjs +38 -0
  92. package/dist/packages/better-stack/src/plugins/blog/client/components/shared/markdown-content.cjs +12 -309
  93. package/dist/packages/better-stack/src/plugins/blog/client/components/shared/markdown-content.mjs +13 -303
  94. package/dist/packages/better-stack/src/plugins/blog/client/components/shared/page-wrapper.cjs +2 -2
  95. package/dist/packages/better-stack/src/plugins/blog/client/components/shared/page-wrapper.mjs +2 -2
  96. package/dist/packages/ui/src/components/accordion.cjs +67 -0
  97. package/dist/packages/ui/src/components/accordion.mjs +62 -0
  98. package/dist/packages/ui/src/components/alert-dialog.cjs +1 -1
  99. package/dist/packages/ui/src/components/alert-dialog.mjs +1 -1
  100. package/dist/packages/{better-stack/src/plugins/blog/client/components/shared/better-blog-attribution.cjs → ui/src/components/better-stack-attribution.cjs} +3 -3
  101. package/dist/packages/{better-stack/src/plugins/blog/client/components/shared/better-blog-attribution.mjs → ui/src/components/better-stack-attribution.mjs} +3 -3
  102. package/dist/packages/ui/src/components/dialog.cjs +14 -0
  103. package/dist/packages/ui/src/components/dialog.mjs +14 -1
  104. package/dist/packages/ui/src/components/dropdown-menu.cjs +67 -0
  105. package/dist/packages/ui/src/components/dropdown-menu.mjs +62 -0
  106. package/dist/packages/ui/src/components/markdown-content.cjs +306 -0
  107. package/dist/packages/ui/src/components/markdown-content.mjs +297 -0
  108. package/dist/packages/ui/src/components/scroll-area.cjs +63 -0
  109. package/dist/packages/ui/src/components/scroll-area.mjs +60 -0
  110. package/dist/packages/ui/src/components/select.cjs +1 -1
  111. package/dist/packages/ui/src/components/select.mjs +1 -1
  112. package/dist/packages/ui/src/components/sheet.cjs +87 -0
  113. package/dist/packages/ui/src/components/sheet.mjs +69 -0
  114. package/dist/plugins/ai-chat/api/index.cjs +9 -0
  115. package/dist/plugins/ai-chat/api/index.d.cts +9 -0
  116. package/dist/plugins/ai-chat/api/index.d.mts +9 -0
  117. package/dist/plugins/ai-chat/api/index.d.ts +9 -0
  118. package/dist/plugins/ai-chat/api/index.mjs +2 -0
  119. package/dist/plugins/ai-chat/client/components/index.cjs +29 -0
  120. package/dist/plugins/ai-chat/client/components/index.d.cts +30 -0
  121. package/dist/plugins/ai-chat/client/components/index.d.mts +30 -0
  122. package/dist/plugins/ai-chat/client/components/index.d.ts +30 -0
  123. package/dist/plugins/ai-chat/client/components/index.mjs +12 -0
  124. package/dist/plugins/ai-chat/client/hooks/index.cjs +13 -0
  125. package/dist/plugins/ai-chat/client/hooks/index.d.cts +98 -0
  126. package/dist/plugins/ai-chat/client/hooks/index.d.mts +98 -0
  127. package/dist/plugins/ai-chat/client/hooks/index.d.ts +98 -0
  128. package/dist/plugins/ai-chat/client/hooks/index.mjs +1 -0
  129. package/dist/plugins/ai-chat/client/index.cjs +21 -0
  130. package/dist/plugins/ai-chat/client/index.d.cts +156 -0
  131. package/dist/plugins/ai-chat/client/index.d.mts +156 -0
  132. package/dist/plugins/ai-chat/client/index.d.ts +156 -0
  133. package/dist/plugins/ai-chat/client/index.mjs +8 -0
  134. package/dist/plugins/ai-chat/client.css +6 -0
  135. package/dist/plugins/ai-chat/query-keys.cjs +60 -0
  136. package/dist/plugins/ai-chat/query-keys.d.cts +478 -0
  137. package/dist/plugins/ai-chat/query-keys.d.mts +478 -0
  138. package/dist/plugins/ai-chat/query-keys.d.ts +478 -0
  139. package/dist/plugins/ai-chat/query-keys.mjs +58 -0
  140. package/dist/plugins/ai-chat/style.css +19 -0
  141. package/dist/plugins/blog/api/index.d.cts +1 -1
  142. package/dist/plugins/blog/api/index.d.mts +1 -1
  143. package/dist/plugins/blog/api/index.d.ts +1 -1
  144. package/dist/plugins/blog/client/components/shared/markdown-content-styles.css +91 -62
  145. package/dist/plugins/blog/client/hooks/index.d.cts +4 -4
  146. package/dist/plugins/blog/client/hooks/index.d.mts +4 -4
  147. package/dist/plugins/blog/client/hooks/index.d.ts +4 -4
  148. package/dist/plugins/blog/client/index.d.cts +1 -1
  149. package/dist/plugins/blog/client/index.d.mts +1 -1
  150. package/dist/plugins/blog/client/index.d.ts +1 -1
  151. package/dist/plugins/blog/query-keys.d.cts +7 -7
  152. package/dist/plugins/blog/query-keys.d.mts +7 -7
  153. package/dist/plugins/blog/query-keys.d.ts +7 -7
  154. package/dist/shared/stack.Be1QIHEn.d.cts +23 -0
  155. package/dist/shared/stack.Be1QIHEn.d.mts +23 -0
  156. package/dist/shared/stack.Be1QIHEn.d.ts +23 -0
  157. package/dist/shared/stack.DaOcgmrM.d.cts +323 -0
  158. package/dist/shared/stack.DaOcgmrM.d.mts +323 -0
  159. package/dist/shared/stack.DaOcgmrM.d.ts +323 -0
  160. package/package.json +59 -1
  161. package/src/plugins/ai-chat/api/index.ts +2 -0
  162. package/src/plugins/ai-chat/api/plugin.ts +1083 -0
  163. package/src/plugins/ai-chat/client/components/chat-input.tsx +295 -0
  164. package/src/plugins/ai-chat/client/components/chat-interface.tsx +494 -0
  165. package/src/plugins/ai-chat/client/components/chat-layout.tsx +175 -0
  166. package/src/plugins/ai-chat/client/components/chat-message.tsx +561 -0
  167. package/src/plugins/ai-chat/client/components/chat-sidebar.tsx +296 -0
  168. package/src/plugins/ai-chat/client/components/index.ts +18 -0
  169. package/src/plugins/ai-chat/client/components/loading/chat-page-skeleton.tsx +57 -0
  170. package/src/plugins/ai-chat/client/components/loading/index.tsx +11 -0
  171. package/src/plugins/ai-chat/client/components/pages/404-page.tsx +27 -0
  172. package/src/plugins/ai-chat/client/components/pages/chat-page.internal.tsx +31 -0
  173. package/src/plugins/ai-chat/client/components/pages/chat-page.tsx +46 -0
  174. package/src/plugins/ai-chat/client/components/shared/default-error.tsx +28 -0
  175. package/src/plugins/ai-chat/client/components/shared/error-placeholder.tsx +22 -0
  176. package/src/plugins/ai-chat/client/components/tool-call-display.tsx +197 -0
  177. package/src/plugins/ai-chat/client/hooks/chat-hooks.tsx +349 -0
  178. package/src/plugins/ai-chat/client/hooks/index.tsx +1 -0
  179. package/src/plugins/ai-chat/client/index.ts +25 -0
  180. package/src/plugins/ai-chat/client/localization/index.ts +156 -0
  181. package/src/plugins/ai-chat/client/overrides.ts +241 -0
  182. package/src/plugins/ai-chat/client/plugin.tsx +449 -0
  183. package/src/plugins/ai-chat/client.css +6 -0
  184. package/src/plugins/ai-chat/db.ts +65 -0
  185. package/src/plugins/ai-chat/query-keys.ts +87 -0
  186. package/src/plugins/ai-chat/schemas.ts +40 -0
  187. package/src/plugins/ai-chat/style.css +19 -0
  188. package/src/plugins/ai-chat/types.ts +29 -0
  189. package/src/plugins/blog/client/components/shared/markdown-content-styles.css +91 -62
  190. package/src/plugins/blog/client/components/shared/markdown-content.tsx +19 -427
  191. package/src/plugins/blog/client/components/shared/page-wrapper.tsx +2 -2
  192. package/dist/node_modules/.pnpm/react-remove-scroll@2.7.1_@types_react@19.2.2_react@19.2.0/node_modules/react-remove-scroll/dist/es2015/sidecar.cjs +0 -9
  193. package/src/plugins/blog/client/components/shared/better-blog-attribution.tsx +0 -19
  194. package/dist/node_modules/.pnpm/{@radix-ui_react-compose-refs@1.1.2_@types_react@19.2.2_react@19.2.0 → @radix-ui_react-compose-refs@1.1.2_@types_react@19.2.6_react@19.2.0}/node_modules/@radix-ui/react-compose-refs/dist/index.cjs +0 -0
  195. package/dist/node_modules/.pnpm/{@radix-ui_react-compose-refs@1.1.2_@types_react@19.2.2_react@19.2.0 → @radix-ui_react-compose-refs@1.1.2_@types_react@19.2.6_react@19.2.0}/node_modules/@radix-ui/react-compose-refs/dist/index.mjs +0 -0
  196. package/dist/node_modules/.pnpm/{@radix-ui_react-context@1.1.2_@types_react@19.2.2_react@19.2.0 → @radix-ui_react-context@1.1.2_@types_react@19.2.6_react@19.2.0}/node_modules/@radix-ui/react-context/dist/index.cjs +0 -0
  197. package/dist/node_modules/.pnpm/{@radix-ui_react-context@1.1.2_@types_react@19.2.2_react@19.2.0 → @radix-ui_react-context@1.1.2_@types_react@19.2.6_react@19.2.0}/node_modules/@radix-ui/react-context/dist/index.mjs +0 -0
  198. package/dist/node_modules/.pnpm/{@radix-ui_react-direction@1.1.1_@types_react@19.2.2_react@19.2.0 → @radix-ui_react-direction@1.1.1_@types_react@19.2.6_react@19.2.0}/node_modules/@radix-ui/react-direction/dist/index.cjs +0 -0
  199. package/dist/node_modules/.pnpm/{@radix-ui_react-direction@1.1.1_@types_react@19.2.2_react@19.2.0 → @radix-ui_react-direction@1.1.1_@types_react@19.2.6_react@19.2.0}/node_modules/@radix-ui/react-direction/dist/index.mjs +0 -0
  200. package/dist/node_modules/.pnpm/{@radix-ui_react-focus-guards@1.1.3_@types_react@19.2.2_react@19.2.0 → @radix-ui_react-focus-guards@1.1.3_@types_react@19.2.6_react@19.2.0}/node_modules/@radix-ui/react-focus-guards/dist/index.cjs +0 -0
  201. package/dist/node_modules/.pnpm/{@radix-ui_react-focus-guards@1.1.3_@types_react@19.2.2_react@19.2.0 → @radix-ui_react-focus-guards@1.1.3_@types_react@19.2.6_react@19.2.0}/node_modules/@radix-ui/react-focus-guards/dist/index.mjs +0 -0
  202. package/dist/node_modules/.pnpm/{@radix-ui_react-primitive@2.1.3_@types_react-dom@19.2.2_@types_react@19.2.2__@types_rea_bdc15f10281778271ffcbe8dd3cd491e → @radix-ui_react-primitive@2.1.3_@types_react-dom@19.2.3_@types_react@19.2.6__@types_rea_a92a69cb1cb39305138539e4fa72f596}/node_modules/@radix-ui/react-primitive/dist/index.cjs +0 -0
  203. package/dist/node_modules/.pnpm/{@radix-ui_react-primitive@2.1.3_@types_react-dom@19.2.2_@types_react@19.2.2__@types_rea_bdc15f10281778271ffcbe8dd3cd491e → @radix-ui_react-primitive@2.1.3_@types_react-dom@19.2.3_@types_react@19.2.6__@types_rea_a92a69cb1cb39305138539e4fa72f596}/node_modules/@radix-ui/react-primitive/dist/index.mjs +0 -0
  204. package/dist/node_modules/.pnpm/{@radix-ui_react-use-callback-ref@1.1.1_@types_react@19.2.2_react@19.2.0 → @radix-ui_react-use-callback-ref@1.1.1_@types_react@19.2.6_react@19.2.0}/node_modules/@radix-ui/react-use-callback-ref/dist/index.cjs +0 -0
  205. package/dist/node_modules/.pnpm/{@radix-ui_react-use-callback-ref@1.1.1_@types_react@19.2.2_react@19.2.0 → @radix-ui_react-use-callback-ref@1.1.1_@types_react@19.2.6_react@19.2.0}/node_modules/@radix-ui/react-use-callback-ref/dist/index.mjs +0 -0
  206. package/dist/node_modules/.pnpm/{@radix-ui_react-use-layout-effect@1.1.1_@types_react@19.2.2_react@19.2.0 → @radix-ui_react-use-layout-effect@1.1.1_@types_react@19.2.6_react@19.2.0}/node_modules/@radix-ui/react-use-layout-effect/dist/index.cjs +0 -0
  207. package/dist/node_modules/.pnpm/{@radix-ui_react-use-layout-effect@1.1.1_@types_react@19.2.2_react@19.2.0 → @radix-ui_react-use-layout-effect@1.1.1_@types_react@19.2.6_react@19.2.0}/node_modules/@radix-ui/react-use-layout-effect/dist/index.mjs +0 -0
  208. package/dist/node_modules/.pnpm/{@radix-ui_react-use-previous@1.1.1_@types_react@19.2.2_react@19.2.0 → @radix-ui_react-use-previous@1.1.1_@types_react@19.2.6_react@19.2.0}/node_modules/@radix-ui/react-use-previous/dist/index.cjs +0 -0
  209. package/dist/node_modules/.pnpm/{@radix-ui_react-use-previous@1.1.1_@types_react@19.2.2_react@19.2.0 → @radix-ui_react-use-previous@1.1.1_@types_react@19.2.6_react@19.2.0}/node_modules/@radix-ui/react-use-previous/dist/index.mjs +0 -0
  210. package/dist/node_modules/.pnpm/{react-remove-scroll-bar@2.3.8_@types_react@19.2.2_react@19.2.0 → react-remove-scroll-bar@2.3.8_@types_react@19.2.6_react@19.2.0}/node_modules/react-remove-scroll-bar/dist/es2015/constants.cjs +0 -0
  211. package/dist/node_modules/.pnpm/{react-remove-scroll-bar@2.3.8_@types_react@19.2.2_react@19.2.0 → react-remove-scroll-bar@2.3.8_@types_react@19.2.6_react@19.2.0}/node_modules/react-remove-scroll-bar/dist/es2015/constants.mjs +0 -0
  212. package/dist/node_modules/.pnpm/{react-remove-scroll-bar@2.3.8_@types_react@19.2.2_react@19.2.0 → react-remove-scroll-bar@2.3.8_@types_react@19.2.6_react@19.2.0}/node_modules/react-remove-scroll-bar/dist/es2015/utils.cjs +0 -0
  213. package/dist/node_modules/.pnpm/{react-remove-scroll-bar@2.3.8_@types_react@19.2.2_react@19.2.0 → react-remove-scroll-bar@2.3.8_@types_react@19.2.6_react@19.2.0}/node_modules/react-remove-scroll-bar/dist/es2015/utils.mjs +0 -0
  214. package/dist/node_modules/.pnpm/{react-remove-scroll@2.7.1_@types_react@19.2.2_react@19.2.0 → react-remove-scroll@2.7.1_@types_react@19.2.6_react@19.2.0}/node_modules/react-remove-scroll/dist/es2015/Combination.cjs +0 -0
  215. package/dist/node_modules/.pnpm/{react-remove-scroll@2.7.1_@types_react@19.2.2_react@19.2.0 → react-remove-scroll@2.7.1_@types_react@19.2.6_react@19.2.0}/node_modules/react-remove-scroll/dist/es2015/Combination.mjs +0 -0
  216. package/dist/node_modules/.pnpm/{react-remove-scroll@2.7.1_@types_react@19.2.2_react@19.2.0 → react-remove-scroll@2.7.1_@types_react@19.2.6_react@19.2.0}/node_modules/react-remove-scroll/dist/es2015/aggresiveCapture.cjs +0 -0
  217. package/dist/node_modules/.pnpm/{react-remove-scroll@2.7.1_@types_react@19.2.2_react@19.2.0 → react-remove-scroll@2.7.1_@types_react@19.2.6_react@19.2.0}/node_modules/react-remove-scroll/dist/es2015/aggresiveCapture.mjs +0 -0
  218. package/dist/node_modules/.pnpm/{react-remove-scroll@2.7.1_@types_react@19.2.2_react@19.2.0 → react-remove-scroll@2.7.1_@types_react@19.2.6_react@19.2.0}/node_modules/react-remove-scroll/dist/es2015/handleScroll.cjs +0 -0
  219. package/dist/node_modules/.pnpm/{react-remove-scroll@2.7.1_@types_react@19.2.2_react@19.2.0 → react-remove-scroll@2.7.1_@types_react@19.2.6_react@19.2.0}/node_modules/react-remove-scroll/dist/es2015/handleScroll.mjs +0 -0
  220. package/dist/node_modules/.pnpm/{react-style-singleton@2.2.3_@types_react@19.2.2_react@19.2.0 → react-style-singleton@2.2.3_@types_react@19.2.6_react@19.2.0}/node_modules/react-style-singleton/dist/es2015/component.cjs +0 -0
  221. package/dist/node_modules/.pnpm/{react-style-singleton@2.2.3_@types_react@19.2.2_react@19.2.0 → react-style-singleton@2.2.3_@types_react@19.2.6_react@19.2.0}/node_modules/react-style-singleton/dist/es2015/component.mjs +0 -0
  222. package/dist/node_modules/.pnpm/{react-style-singleton@2.2.3_@types_react@19.2.2_react@19.2.0 → react-style-singleton@2.2.3_@types_react@19.2.6_react@19.2.0}/node_modules/react-style-singleton/dist/es2015/hook.cjs +0 -0
  223. package/dist/node_modules/.pnpm/{react-style-singleton@2.2.3_@types_react@19.2.2_react@19.2.0 → react-style-singleton@2.2.3_@types_react@19.2.6_react@19.2.0}/node_modules/react-style-singleton/dist/es2015/hook.mjs +0 -0
  224. package/dist/node_modules/.pnpm/{react-style-singleton@2.2.3_@types_react@19.2.2_react@19.2.0 → react-style-singleton@2.2.3_@types_react@19.2.6_react@19.2.0}/node_modules/react-style-singleton/dist/es2015/singleton.cjs +0 -0
  225. package/dist/node_modules/.pnpm/{react-style-singleton@2.2.3_@types_react@19.2.2_react@19.2.0 → react-style-singleton@2.2.3_@types_react@19.2.6_react@19.2.0}/node_modules/react-style-singleton/dist/es2015/singleton.mjs +0 -0
  226. package/dist/node_modules/.pnpm/{use-callback-ref@1.3.3_@types_react@19.2.2_react@19.2.0 → use-callback-ref@1.3.3_@types_react@19.2.6_react@19.2.0}/node_modules/use-callback-ref/dist/es2015/assignRef.cjs +0 -0
  227. package/dist/node_modules/.pnpm/{use-callback-ref@1.3.3_@types_react@19.2.2_react@19.2.0 → use-callback-ref@1.3.3_@types_react@19.2.6_react@19.2.0}/node_modules/use-callback-ref/dist/es2015/assignRef.mjs +0 -0
  228. package/dist/node_modules/.pnpm/{use-callback-ref@1.3.3_@types_react@19.2.2_react@19.2.0 → use-callback-ref@1.3.3_@types_react@19.2.6_react@19.2.0}/node_modules/use-callback-ref/dist/es2015/useMergeRef.cjs +0 -0
  229. package/dist/node_modules/.pnpm/{use-callback-ref@1.3.3_@types_react@19.2.2_react@19.2.0 → use-callback-ref@1.3.3_@types_react@19.2.6_react@19.2.0}/node_modules/use-callback-ref/dist/es2015/useMergeRef.mjs +0 -0
  230. package/dist/node_modules/.pnpm/{use-callback-ref@1.3.3_@types_react@19.2.2_react@19.2.0 → use-callback-ref@1.3.3_@types_react@19.2.6_react@19.2.0}/node_modules/use-callback-ref/dist/es2015/useRef.cjs +0 -0
  231. package/dist/node_modules/.pnpm/{use-callback-ref@1.3.3_@types_react@19.2.2_react@19.2.0 → use-callback-ref@1.3.3_@types_react@19.2.6_react@19.2.0}/node_modules/use-callback-ref/dist/es2015/useRef.mjs +0 -0
  232. package/dist/node_modules/.pnpm/{use-sidecar@1.1.3_@types_react@19.2.2_react@19.2.0 → use-sidecar@1.1.3_@types_react@19.2.6_react@19.2.0}/node_modules/use-sidecar/dist/es2015/exports.cjs +0 -0
  233. package/dist/node_modules/.pnpm/{use-sidecar@1.1.3_@types_react@19.2.2_react@19.2.0 → use-sidecar@1.1.3_@types_react@19.2.6_react@19.2.0}/node_modules/use-sidecar/dist/es2015/exports.mjs +0 -0
  234. package/dist/node_modules/.pnpm/{use-sidecar@1.1.3_@types_react@19.2.2_react@19.2.0 → use-sidecar@1.1.3_@types_react@19.2.6_react@19.2.0}/node_modules/use-sidecar/dist/es2015/medium.cjs +0 -0
  235. package/dist/node_modules/.pnpm/{use-sidecar@1.1.3_@types_react@19.2.2_react@19.2.0 → use-sidecar@1.1.3_@types_react@19.2.6_react@19.2.0}/node_modules/use-sidecar/dist/es2015/medium.mjs +0 -0
  236. package/dist/shared/{stack.CbuN2zVV.d.ts → stack.CcI4sYJP.d.cts} +3 -3
  237. package/dist/shared/{stack.CbuN2zVV.d.cts → stack.CcI4sYJP.d.mts} +3 -3
  238. package/dist/shared/{stack.CbuN2zVV.d.mts → stack.CcI4sYJP.d.ts} +3 -3
@@ -0,0 +1,494 @@
1
+ "use client";
2
+
3
+ import { useChat } from "@ai-sdk/react";
4
+ import { useEffect, useRef, useState, useMemo, useCallback } from "react";
5
+ import { useQueryClient } from "@tanstack/react-query";
6
+ import { ChatMessage } from "./chat-message";
7
+ import { ChatInput, type AttachedFile } from "./chat-input";
8
+ import { BetterStackAttribution } from "@workspace/ui/components/better-stack-attribution";
9
+ import { ScrollArea } from "@workspace/ui/components/scroll-area";
10
+ import { DefaultChatTransport, type UIMessage } from "ai";
11
+ import { cn } from "@workspace/ui/lib/utils";
12
+ import { usePluginOverrides, useBasePath } from "@btst/stack/context";
13
+ import type { AiChatPluginOverrides } from "../overrides";
14
+ import { AI_CHAT_LOCALIZATION } from "../localization";
15
+ import { createApiClient } from "@btst/stack/plugins/client";
16
+ import type { AiChatApiRouter } from "../../api/plugin";
17
+ import { createAiChatQueryKeys } from "../../query-keys";
18
+ import {
19
+ useConversation,
20
+ useConversations,
21
+ type SerializedConversation,
22
+ } from "../hooks/chat-hooks";
23
+
24
+ interface ChatInterfaceProps {
25
+ apiPath?: string;
26
+ initialMessages?: UIMessage[];
27
+ id?: string;
28
+ /** Variant: 'full' for full-page layout, 'widget' for embedded widget */
29
+ variant?: "full" | "widget";
30
+ className?: string;
31
+ /** Called whenever messages change (for persistence). Only fires in public mode. */
32
+ onMessagesChange?: (messages: UIMessage[]) => void;
33
+ }
34
+
35
+ export function ChatInterface({
36
+ apiPath = "/api/chat",
37
+ initialMessages,
38
+ id,
39
+ variant = "full",
40
+ className,
41
+ onMessagesChange,
42
+ }: ChatInterfaceProps) {
43
+ const {
44
+ navigate,
45
+ localization: customLocalization,
46
+ apiBaseURL,
47
+ apiBasePath,
48
+ headers,
49
+ mode,
50
+ showAttribution,
51
+ chatSuggestions,
52
+ } = usePluginOverrides<AiChatPluginOverrides, Partial<AiChatPluginOverrides>>(
53
+ "ai-chat",
54
+ { showAttribution: true },
55
+ );
56
+ const basePath = useBasePath();
57
+ const isPublicMode = mode === "public";
58
+
59
+ const localization = { ...AI_CHAT_LOCALIZATION, ...customLocalization };
60
+ const queryClient = useQueryClient();
61
+
62
+ const conversationsListQueryKey = useMemo(() => {
63
+ // In public mode, we don't need conversation queries
64
+ if (isPublicMode) return ["ai-chat", "disabled"];
65
+ const client = createApiClient<AiChatApiRouter>({
66
+ baseURL: apiBaseURL,
67
+ basePath: apiBasePath,
68
+ });
69
+ const queries = createAiChatQueryKeys(client, headers);
70
+ return queries.conversations.list().queryKey;
71
+ }, [apiBaseURL, apiBasePath, headers, isPublicMode]);
72
+
73
+ // Track the current conversation ID - initialized from prop, updated after first message
74
+ // In public mode, we don't track conversation IDs
75
+ const [currentConversationId, setCurrentConversationId] = useState<
76
+ string | undefined
77
+ >(isPublicMode ? undefined : id);
78
+ // Track if we've sent the first message on a new chat (to trigger navigation)
79
+ const isFirstMessageSentRef = useRef(false);
80
+ const hasNavigatedRef = useRef(false);
81
+
82
+ // Update currentConversationId when id prop changes (e.g., navigating to different conversation)
83
+ useEffect(() => {
84
+ if (!isPublicMode) {
85
+ setCurrentConversationId(id);
86
+ isFirstMessageSentRef.current = false;
87
+ hasNavigatedRef.current = false;
88
+ }
89
+ }, [id, isPublicMode]);
90
+
91
+ // Fetch existing conversation messages when id is provided (authenticated mode only)
92
+ const { conversation, isLoading: isLoadingConversation } = useConversation(
93
+ id,
94
+ { enabled: !!id && !isPublicMode },
95
+ );
96
+
97
+ // Fetch conversations list for navigation after first message (authenticated mode only)
98
+ const { conversations } = useConversations({ enabled: !isPublicMode });
99
+
100
+ // Use a ref to track the conversation ID for the transport body
101
+ // This ensures the transport always uses the latest value
102
+ // In public mode, always undefined
103
+ const conversationIdRef = useRef<string | undefined>(
104
+ isPublicMode ? undefined : id,
105
+ );
106
+ useEffect(() => {
107
+ if (!isPublicMode) {
108
+ conversationIdRef.current = currentConversationId;
109
+ }
110
+ }, [currentConversationId, isPublicMode]);
111
+
112
+ // Ref to track edit operation with messages to use
113
+ const editMessagesRef = useRef<UIMessage[] | null>(null);
114
+
115
+ // Track if we've finished initializing messages
116
+ // This prevents onMessagesChange from firing with an empty array before initialMessages are loaded
117
+ // Without this guard, the effect would fire on mount with [], overwriting any saved messages
118
+ const [isMessagesInitialized, setIsMessagesInitialized] = useState(
119
+ () =>
120
+ // Start as initialized if there are no initialMessages to load
121
+ !initialMessages || initialMessages.length === 0,
122
+ );
123
+
124
+ // Memoize the transport to prevent recreation on every render
125
+ const transport = useMemo(
126
+ () =>
127
+ new DefaultChatTransport({
128
+ api: apiPath,
129
+ // In public mode, don't send conversationId
130
+ body: isPublicMode
131
+ ? undefined
132
+ : () => ({ conversationId: conversationIdRef.current }),
133
+ // Handle edit operations by using truncated messages from the ref
134
+ prepareSendMessagesRequest: ({ messages: hookMessages }) => {
135
+ // If we're in an edit operation, use the truncated messages + new user message
136
+ if (editMessagesRef.current !== null) {
137
+ const newUserMessage = hookMessages[hookMessages.length - 1];
138
+ const messagesToSend = [...editMessagesRef.current];
139
+ if (newUserMessage) {
140
+ messagesToSend.push(newUserMessage);
141
+ }
142
+ // Clear the ref after use
143
+ editMessagesRef.current = null;
144
+ return {
145
+ body: {
146
+ messages: messagesToSend,
147
+ conversationId: conversationIdRef.current,
148
+ },
149
+ };
150
+ }
151
+ // Normal case - use the messages as-is
152
+ return {
153
+ body: {
154
+ messages: hookMessages,
155
+ conversationId: conversationIdRef.current,
156
+ },
157
+ };
158
+ },
159
+ }),
160
+ [apiPath, isPublicMode],
161
+ );
162
+
163
+ const { messages, sendMessage, status, error, setMessages, regenerate } =
164
+ useChat({
165
+ transport,
166
+ onError: (err) => {
167
+ console.error("useChat onError:", err);
168
+ },
169
+ onFinish: async () => {
170
+ // In public mode, skip all persistence-related operations
171
+ if (isPublicMode) return;
172
+
173
+ // Invalidate conversation list to show new/updated conversations
174
+ await queryClient.invalidateQueries({
175
+ queryKey: conversationsListQueryKey,
176
+ });
177
+
178
+ // If this was the first message on a new chat, update the URL without full navigation
179
+ // This avoids losing the in-memory messages during component remount
180
+ if (isFirstMessageSentRef.current && !id && !hasNavigatedRef.current) {
181
+ hasNavigatedRef.current = true;
182
+ // Wait for the invalidation to complete and refetch conversations
183
+ await queryClient.refetchQueries({
184
+ queryKey: conversationsListQueryKey,
185
+ });
186
+ // Get the updated conversations from cache
187
+ const cachedConversations = queryClient.getQueryData<
188
+ SerializedConversation[]
189
+ >(conversationsListQueryKey);
190
+ if (cachedConversations && cachedConversations.length > 0) {
191
+ // The most recently updated conversation should be the one we just created
192
+ const newConversation = cachedConversations[0];
193
+ if (newConversation) {
194
+ // Update our local state
195
+ setCurrentConversationId(newConversation.id);
196
+ conversationIdRef.current = newConversation.id;
197
+ // Update URL without navigation to preserve in-memory messages
198
+ // Use replaceState to avoid adding to history stack
199
+ const newUrl = `${basePath}/chat/${newConversation.id}`;
200
+ if (typeof window !== "undefined") {
201
+ window.history.replaceState(
202
+ { ...window.history.state },
203
+ "",
204
+ newUrl,
205
+ );
206
+ }
207
+ }
208
+ }
209
+ }
210
+ },
211
+ });
212
+
213
+ // Load existing conversation messages when navigating to a conversation
214
+ useEffect(() => {
215
+ if (
216
+ conversation?.messages &&
217
+ conversation.messages.length > 0 &&
218
+ messages.length === 0
219
+ ) {
220
+ // Filter out "data" role messages as UIMessage only accepts "user" | "assistant" | "system"
221
+ const uiMessages: UIMessage[] = conversation.messages
222
+ .filter((m) => m.role !== "data")
223
+ .map((m) => {
224
+ // Try to parse content as JSON parts (new format with images)
225
+ let parts: UIMessage["parts"];
226
+ try {
227
+ const parsed = JSON.parse(m.content);
228
+ if (Array.isArray(parsed)) {
229
+ parts = parsed;
230
+ } else {
231
+ // Fallback: wrap as text
232
+ parts = [{ type: "text" as const, text: m.content }];
233
+ }
234
+ } catch {
235
+ // Not JSON - legacy format, wrap as text
236
+ parts = [{ type: "text" as const, text: m.content }];
237
+ }
238
+ return {
239
+ id: m.id,
240
+ role: m.role as "user" | "assistant" | "system",
241
+ parts,
242
+ };
243
+ });
244
+ setMessages(uiMessages);
245
+ }
246
+ }, [conversation, messages.length, setMessages]);
247
+
248
+ // Set initial messages on mount (for SSR hydration)
249
+ useEffect(() => {
250
+ if (
251
+ initialMessages &&
252
+ initialMessages.length > 0 &&
253
+ messages.length === 0
254
+ ) {
255
+ setMessages(initialMessages);
256
+ // Mark as initialized - this is batched with setMessages so both take effect in the same render
257
+ setIsMessagesInitialized(true);
258
+ }
259
+ }, [initialMessages, setMessages, messages.length]);
260
+
261
+ const [input, setInput] = useState("");
262
+ const [attachedFiles, setAttachedFiles] = useState<AttachedFile[]>([]);
263
+ const scrollRef = useRef<HTMLDivElement>(null);
264
+
265
+ // Auto-scroll to bottom when messages change
266
+ useEffect(() => {
267
+ if (scrollRef.current) {
268
+ const scrollElement = scrollRef.current.querySelector(
269
+ "[data-radix-scroll-area-viewport]",
270
+ );
271
+ if (scrollElement) {
272
+ scrollElement.scrollTop = scrollElement.scrollHeight;
273
+ }
274
+ }
275
+ }, [messages]);
276
+
277
+ // Notify parent when messages change (for persistence in public mode)
278
+ // Only fire after initialization to prevent overwriting saved messages with an empty array
279
+ useEffect(() => {
280
+ if (isPublicMode && onMessagesChange && isMessagesInitialized) {
281
+ onMessagesChange(messages);
282
+ }
283
+ }, [messages, isPublicMode, onMessagesChange, isMessagesInitialized]);
284
+
285
+ const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
286
+ setInput(e.target.value);
287
+ };
288
+
289
+ const handleSubmit = async (
290
+ e: React.FormEvent<HTMLFormElement>,
291
+ files?: AttachedFile[],
292
+ ) => {
293
+ e.preventDefault();
294
+ const text = input.trim();
295
+ // Allow submit if there's text OR files
296
+ if (!text && (!files || files.length === 0)) return;
297
+
298
+ // Track if this is the first message on a new chat (authenticated mode only)
299
+ if (!isPublicMode && !id && messages.length === 0) {
300
+ isFirstMessageSentRef.current = true;
301
+ }
302
+
303
+ // Save current values before clearing - we'll restore them if send fails
304
+ const savedInput = input;
305
+ const savedFiles = files ? [...files] : [];
306
+
307
+ // Clear input immediately (optimistically) - the AI SDK renders messages optimistically,
308
+ // so we need to clear the input before the message appears to avoid duplicate text
309
+ setInput("");
310
+ setAttachedFiles([]);
311
+
312
+ try {
313
+ // Use AI SDK's file attachment format
314
+ // The SDK automatically converts supported file types (images, text) to the correct format
315
+ if (files && files.length > 0) {
316
+ // Convert AttachedFile[] to FileUIPart[] format expected by AI SDK
317
+ const fileUIParts = files.map((file) => ({
318
+ type: "file" as const,
319
+ mediaType: file.mediaType,
320
+ url: file.url,
321
+ filename: file.filename,
322
+ }));
323
+
324
+ await sendMessage({
325
+ text: text || "", // AI SDK requires text, even if empty
326
+ files: fileUIParts,
327
+ });
328
+ } else {
329
+ await sendMessage({ text });
330
+ }
331
+ } catch (error) {
332
+ // Restore input on failure so user can retry
333
+ setInput(savedInput);
334
+ setAttachedFiles(savedFiles);
335
+ console.error("Error sending message:", error);
336
+ }
337
+ };
338
+
339
+ const isLoading = status === "streaming" || status === "submitted";
340
+
341
+ // Handler for retrying/regenerating the last AI response
342
+ const handleRetry = useCallback(() => {
343
+ regenerate();
344
+ }, [regenerate]);
345
+
346
+ // Pending edit state - stores the text to send after messages are truncated
347
+ const [pendingEdit, setPendingEdit] = useState<{
348
+ text: string;
349
+ expectedLength: number;
350
+ } | null>(null);
351
+
352
+ // Effect to send the edited message after React has processed the truncation
353
+ useEffect(() => {
354
+ if (pendingEdit && messages.length === pendingEdit.expectedLength) {
355
+ const textToSend = pendingEdit.text;
356
+ setPendingEdit(null);
357
+ sendMessage({ text: textToSend });
358
+ }
359
+ }, [messages.length, pendingEdit, sendMessage]);
360
+
361
+ // Handler for editing a user message - replaces the message and all subsequent messages
362
+ const handleEditMessage = useCallback(
363
+ (messageId: string, newText: string) => {
364
+ const messageIndex = messages.findIndex((m) => m.id === messageId);
365
+ if (messageIndex === -1) return;
366
+
367
+ // Get the message to edit
368
+ const messageToEdit = messages[messageIndex];
369
+ if (!messageToEdit || messageToEdit.role !== "user") return;
370
+
371
+ // Truncate to BEFORE the edited message (remove it and all subsequent)
372
+ const truncatedMessages = messages.slice(0, messageIndex);
373
+
374
+ // Store the truncated messages in the ref for the transport to use
375
+ editMessagesRef.current = truncatedMessages;
376
+
377
+ // Set the pending edit - the useEffect will send after truncation is processed
378
+ setPendingEdit({ text: newText, expectedLength: messageIndex });
379
+
380
+ // Truncate the messages - React will batch this and the useEffect will fire after
381
+ setMessages(truncatedMessages);
382
+ },
383
+ [messages, setMessages],
384
+ );
385
+
386
+ const isWidget = variant === "widget";
387
+
388
+ return (
389
+ <>
390
+ <div className="flex-1 overflow-hidden">
391
+ <div
392
+ className={cn(
393
+ "flex flex-col h-full w-full bg-background",
394
+ isWidget && "rounded-xl",
395
+ className,
396
+ )}
397
+ data-testid="chat-interface"
398
+ >
399
+ {/* Messages Area */}
400
+ <ScrollArea ref={scrollRef} className="flex-1 h-full">
401
+ <div
402
+ className={cn(
403
+ "flex flex-col p-4",
404
+ isWidget ? "max-w-full" : "max-w-3xl mx-auto w-full",
405
+ )}
406
+ >
407
+ {messages.length === 0 ? (
408
+ <div className="flex flex-col h-full min-h-[300px]">
409
+ <div className="flex-1 flex items-center justify-center text-muted-foreground">
410
+ <p>{localization.CHAT_EMPTY_STATE}</p>
411
+ </div>
412
+ {chatSuggestions && chatSuggestions.length > 0 && (
413
+ <div className="flex flex-wrap justify-center gap-2 pb-4 max-w-md mx-auto">
414
+ {chatSuggestions.map((suggestion, index) => (
415
+ <button
416
+ key={index}
417
+ type="button"
418
+ onClick={() => setInput(suggestion)}
419
+ className="px-3 py-2 text-sm rounded-lg border border-border bg-background hover:bg-accent hover:text-accent-foreground transition-colors text-foreground"
420
+ >
421
+ {suggestion}
422
+ </button>
423
+ ))}
424
+ </div>
425
+ )}
426
+ </div>
427
+ ) : (
428
+ messages.map((m, index) => (
429
+ <ChatMessage
430
+ key={m.id || `msg-${index}`}
431
+ message={m}
432
+ isStreaming={
433
+ status === "streaming" &&
434
+ m.id === messages[messages.length - 1]?.id &&
435
+ m.role === "assistant"
436
+ }
437
+ variant={isWidget ? "compact" : "default"}
438
+ onRetry={
439
+ // Only show retry on the last assistant message
440
+ m.role === "assistant" && index === messages.length - 1
441
+ ? handleRetry
442
+ : undefined
443
+ }
444
+ onEdit={
445
+ // Allow editing user messages
446
+ m.role === "user"
447
+ ? (newText) => handleEditMessage(m.id, newText)
448
+ : undefined
449
+ }
450
+ isRetrying={isLoading && m.role === "assistant"}
451
+ />
452
+ ))
453
+ )}
454
+ {isLoading &&
455
+ messages[messages.length - 1]?.role !== "assistant" && (
456
+ <div className="flex items-center gap-2 text-muted-foreground text-sm py-4">
457
+ <div className="animate-pulse">
458
+ {localization.CHAT_LOADING}
459
+ </div>
460
+ </div>
461
+ )}
462
+ {error && (
463
+ <div className="flex items-center gap-2 text-destructive text-sm py-4 px-3 bg-destructive/10 rounded-md">
464
+ <span>{localization.CHAT_ERROR}</span>
465
+ </div>
466
+ )}
467
+ </div>
468
+ </ScrollArea>
469
+ </div>
470
+ </div>
471
+ {/* Input Area */}
472
+ <div
473
+ className={cn(
474
+ "border-t bg-background p-4",
475
+ isWidget ? "px-3 py-3" : "px-4",
476
+ )}
477
+ >
478
+ <div className={cn(!isWidget && "max-w-3xl mx-auto")}>
479
+ <ChatInput
480
+ input={input}
481
+ handleInputChange={handleInputChange}
482
+ handleSubmit={handleSubmit}
483
+ isLoading={isLoading}
484
+ placeholder={localization.CHAT_PLACEHOLDER}
485
+ variant={isWidget ? "compact" : "default"}
486
+ onFilesAttached={setAttachedFiles}
487
+ attachedFiles={attachedFiles}
488
+ />
489
+ {showAttribution && <BetterStackAttribution />}
490
+ </div>
491
+ </div>
492
+ </>
493
+ );
494
+ }
@@ -0,0 +1,175 @@
1
+ "use client";
2
+
3
+ import { useState, useCallback } from "react";
4
+ import { Button } from "@workspace/ui/components/button";
5
+ import {
6
+ Sheet,
7
+ SheetContent,
8
+ SheetTrigger,
9
+ } from "@workspace/ui/components/sheet";
10
+ import { Menu, PanelLeftClose, PanelLeft } from "lucide-react";
11
+ import { cn } from "@workspace/ui/lib/utils";
12
+ import { ChatSidebar } from "./chat-sidebar";
13
+ import { ChatInterface } from "./chat-interface";
14
+ import type { UIMessage } from "ai";
15
+
16
+ export interface ChatLayoutProps {
17
+ /** API base URL */
18
+ apiBaseURL: string;
19
+ /** API base path */
20
+ apiBasePath: string;
21
+ /** Current conversation ID (if viewing existing conversation) */
22
+ conversationId?: string;
23
+ /** Layout mode: 'full' for full page with sidebar, 'widget' for embeddable widget */
24
+ layout?: "full" | "widget";
25
+ /** Additional class name for the container */
26
+ className?: string;
27
+ /** Whether to show the sidebar (default: true for full layout) */
28
+ showSidebar?: boolean;
29
+ /** Height of the widget (only applies to widget layout) */
30
+ widgetHeight?: string | number;
31
+ /** Initial messages to populate the chat (useful for localStorage persistence in public mode) */
32
+ initialMessages?: UIMessage[];
33
+ /** Called whenever messages change (for persistence). Only fires in public mode. */
34
+ onMessagesChange?: (messages: UIMessage[]) => void;
35
+ }
36
+
37
+ /**
38
+ * ChatLayout component that provides a full-page chat experience with sidebar
39
+ * or a compact widget mode for embedding.
40
+ */
41
+ export function ChatLayout({
42
+ apiBaseURL,
43
+ apiBasePath,
44
+ conversationId,
45
+ layout = "full",
46
+ className,
47
+ showSidebar = true,
48
+ widgetHeight = "600px",
49
+ initialMessages,
50
+ onMessagesChange,
51
+ }: ChatLayoutProps) {
52
+ const [sidebarOpen, setSidebarOpen] = useState(true);
53
+ const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
54
+ // Key to force ChatInterface remount when starting a new chat
55
+ const [chatResetKey, setChatResetKey] = useState(0);
56
+
57
+ const apiPath = `${apiBaseURL}${apiBasePath}/chat`;
58
+
59
+ // Handler for "New chat" button - increments key to force remount
60
+ const handleNewChat = useCallback(() => {
61
+ // Only needed when we're already on the "new chat" route (/chat).
62
+ // If we're on /chat/:id, navigation to /chat will remount ChatLayout/ChatInterface anyway.
63
+ if (!conversationId) {
64
+ setChatResetKey((prev) => prev + 1);
65
+ }
66
+ }, [conversationId]);
67
+
68
+ if (layout === "widget") {
69
+ return (
70
+ <div
71
+ className={cn(
72
+ "flex flex-col w-full border rounded-xl overflow-hidden bg-background shadow-sm",
73
+ className,
74
+ )}
75
+ style={{ height: widgetHeight }}
76
+ >
77
+ <ChatInterface
78
+ apiPath={apiPath}
79
+ id={conversationId}
80
+ variant="widget"
81
+ initialMessages={initialMessages}
82
+ onMessagesChange={onMessagesChange}
83
+ />
84
+ </div>
85
+ );
86
+ }
87
+
88
+ // Full layout with sidebar
89
+ return (
90
+ <div
91
+ className={cn(
92
+ "flex h-[calc(100vh-4rem)] w-full overflow-hidden",
93
+ className,
94
+ )}
95
+ data-testid="chat-layout"
96
+ >
97
+ {/* Desktop Sidebar */}
98
+ {showSidebar && (
99
+ <div
100
+ className={cn(
101
+ "hidden md:flex transition-all duration-300 ease-in-out",
102
+ sidebarOpen ? "w-72" : "w-0",
103
+ )}
104
+ >
105
+ {sidebarOpen && (
106
+ <ChatSidebar
107
+ currentConversationId={conversationId}
108
+ onNewChat={handleNewChat}
109
+ className="w-72"
110
+ />
111
+ )}
112
+ </div>
113
+ )}
114
+
115
+ {/* Main Chat Area */}
116
+ <div className="flex-1 flex flex-col min-w-0">
117
+ {/* Header */}
118
+ <div className="flex items-center gap-2 p-2 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
119
+ {/* Mobile menu button */}
120
+ {showSidebar && (
121
+ <Sheet open={mobileSidebarOpen} onOpenChange={setMobileSidebarOpen}>
122
+ <SheetTrigger asChild>
123
+ <Button
124
+ variant="ghost"
125
+ size="icon"
126
+ className="md:hidden"
127
+ aria-label="Open menu"
128
+ >
129
+ <Menu className="h-5 w-5" />
130
+ </Button>
131
+ </SheetTrigger>
132
+ <SheetContent side="left" className="p-0 w-72">
133
+ <ChatSidebar
134
+ currentConversationId={conversationId}
135
+ onNewChat={() => {
136
+ handleNewChat();
137
+ setMobileSidebarOpen(false);
138
+ }}
139
+ />
140
+ </SheetContent>
141
+ </Sheet>
142
+ )}
143
+
144
+ {/* Desktop sidebar toggle */}
145
+ {showSidebar && (
146
+ <Button
147
+ variant="ghost"
148
+ size="icon"
149
+ className="hidden md:flex"
150
+ onClick={() => setSidebarOpen(!sidebarOpen)}
151
+ aria-label={sidebarOpen ? "Close sidebar" : "Open sidebar"}
152
+ >
153
+ {sidebarOpen ? (
154
+ <PanelLeftClose className="h-5 w-5" />
155
+ ) : (
156
+ <PanelLeft className="h-5 w-5" />
157
+ )}
158
+ </Button>
159
+ )}
160
+
161
+ <div className="flex-1" />
162
+ </div>
163
+
164
+ <ChatInterface
165
+ key={`chat-${conversationId ?? "new"}-${chatResetKey}`}
166
+ apiPath={apiPath}
167
+ id={conversationId}
168
+ variant="full"
169
+ initialMessages={initialMessages}
170
+ onMessagesChange={onMessagesChange}
171
+ />
172
+ </div>
173
+ </div>
174
+ );
175
+ }