@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,1083 @@
1
+ import type { Adapter } from "@btst/db";
2
+ import { defineBackendPlugin } from "@btst/stack/plugins/api";
3
+ import { createEndpoint } from "@btst/stack/plugins/api";
4
+ import {
5
+ streamText,
6
+ convertToModelMessages,
7
+ stepCountIs,
8
+ type LanguageModel,
9
+ type UIMessage,
10
+ type Tool,
11
+ } from "ai";
12
+ import { aiChatSchema as dbSchema } from "../db";
13
+ import {
14
+ chatRequestSchema,
15
+ createConversationSchema,
16
+ updateConversationSchema,
17
+ } from "../schemas";
18
+ import type { Conversation, ConversationWithMessages, Message } from "../types";
19
+
20
+ /**
21
+ * Context passed to AI Chat API hooks
22
+ */
23
+ export interface ChatApiContext<TBody = any, TParams = any, TQuery = any> {
24
+ body?: TBody;
25
+ params?: TParams;
26
+ query?: TQuery;
27
+ request?: Request;
28
+ headers?: Headers;
29
+ [key: string]: any;
30
+ }
31
+
32
+ /**
33
+ * Configuration hooks for AI Chat backend plugin
34
+ * All hooks are optional and allow consumers to customize behavior
35
+ */
36
+ export interface AiChatBackendHooks {
37
+ // ============== Authorization Hooks ==============
38
+ // Return false to deny access
39
+
40
+ /**
41
+ * Called before processing a chat message. Return false to deny access.
42
+ * @param messages - Array of messages being sent
43
+ * @param context - Request context with headers, etc.
44
+ */
45
+ onBeforeChat?: (
46
+ messages: Array<{ role: string; content: string }>,
47
+ context: ChatApiContext,
48
+ ) => Promise<boolean> | boolean;
49
+
50
+ /**
51
+ * Called before listing conversations. Return false to deny access.
52
+ * @param context - Request context with headers, etc.
53
+ */
54
+ onBeforeListConversations?: (
55
+ context: ChatApiContext,
56
+ ) => Promise<boolean> | boolean;
57
+
58
+ /**
59
+ * Called before getting a single conversation. Return false to deny access.
60
+ * @param conversationId - ID of the conversation being accessed
61
+ * @param context - Request context with headers, etc.
62
+ */
63
+ onBeforeGetConversation?: (
64
+ conversationId: string,
65
+ context: ChatApiContext,
66
+ ) => Promise<boolean> | boolean;
67
+
68
+ /**
69
+ * Called before creating a conversation. Return false to deny access.
70
+ * @param data - Conversation data being created
71
+ * @param context - Request context with headers, etc.
72
+ */
73
+ onBeforeCreateConversation?: (
74
+ data: { id?: string; title?: string },
75
+ context: ChatApiContext,
76
+ ) => Promise<boolean> | boolean;
77
+
78
+ /**
79
+ * Called before updating a conversation. Return false to deny access.
80
+ * @param conversationId - ID of the conversation being updated
81
+ * @param data - Updated conversation data
82
+ * @param context - Request context with headers, etc.
83
+ */
84
+ onBeforeUpdateConversation?: (
85
+ conversationId: string,
86
+ data: { title?: string },
87
+ context: ChatApiContext,
88
+ ) => Promise<boolean> | boolean;
89
+
90
+ /**
91
+ * Called before deleting a conversation. Return false to deny access.
92
+ * @param conversationId - ID of the conversation being deleted
93
+ * @param context - Request context with headers, etc.
94
+ */
95
+ onBeforeDeleteConversation?: (
96
+ conversationId: string,
97
+ context: ChatApiContext,
98
+ ) => Promise<boolean> | boolean;
99
+
100
+ // ============== Lifecycle Hooks ==============
101
+
102
+ /**
103
+ * Called after a chat message is processed successfully
104
+ * @param conversationId - ID of the conversation
105
+ * @param messages - Array of messages in the conversation
106
+ * @param context - Request context
107
+ */
108
+ onAfterChat?: (
109
+ conversationId: string,
110
+ messages: Message[],
111
+ context: ChatApiContext,
112
+ ) => Promise<void> | void;
113
+
114
+ /**
115
+ * Called after conversations are read successfully
116
+ * @param conversations - Array of conversations that were read
117
+ * @param context - Request context
118
+ */
119
+ onConversationsRead?: (
120
+ conversations: Conversation[],
121
+ context: ChatApiContext,
122
+ ) => Promise<void> | void;
123
+
124
+ /**
125
+ * Called after a single conversation is read successfully
126
+ * @param conversation - The conversation with messages
127
+ * @param context - Request context
128
+ */
129
+ onConversationRead?: (
130
+ conversation: Conversation & { messages: Message[] },
131
+ context: ChatApiContext,
132
+ ) => Promise<void> | void;
133
+
134
+ /**
135
+ * Called after a conversation is created successfully
136
+ * @param conversation - The created conversation
137
+ * @param context - Request context
138
+ */
139
+ onConversationCreated?: (
140
+ conversation: Conversation,
141
+ context: ChatApiContext,
142
+ ) => Promise<void> | void;
143
+
144
+ /**
145
+ * Called after a conversation is updated successfully
146
+ * @param conversation - The updated conversation
147
+ * @param context - Request context
148
+ */
149
+ onConversationUpdated?: (
150
+ conversation: Conversation,
151
+ context: ChatApiContext,
152
+ ) => Promise<void> | void;
153
+
154
+ /**
155
+ * Called after a conversation is deleted successfully
156
+ * @param conversationId - ID of the deleted conversation
157
+ * @param context - Request context
158
+ */
159
+ onConversationDeleted?: (
160
+ conversationId: string,
161
+ context: ChatApiContext,
162
+ ) => Promise<void> | void;
163
+
164
+ // ============== Error Hooks ==============
165
+
166
+ /**
167
+ * Called when a chat operation fails
168
+ * @param error - The error that occurred
169
+ * @param context - Request context
170
+ */
171
+ onChatError?: (error: Error, context: ChatApiContext) => Promise<void> | void;
172
+
173
+ /**
174
+ * Called when listing conversations fails
175
+ * @param error - The error that occurred
176
+ * @param context - Request context
177
+ */
178
+ onListConversationsError?: (
179
+ error: Error,
180
+ context: ChatApiContext,
181
+ ) => Promise<void> | void;
182
+
183
+ /**
184
+ * Called when getting a conversation fails
185
+ * @param error - The error that occurred
186
+ * @param context - Request context
187
+ */
188
+ onGetConversationError?: (
189
+ error: Error,
190
+ context: ChatApiContext,
191
+ ) => Promise<void> | void;
192
+
193
+ /**
194
+ * Called when creating a conversation fails
195
+ * @param error - The error that occurred
196
+ * @param context - Request context
197
+ */
198
+ onCreateConversationError?: (
199
+ error: Error,
200
+ context: ChatApiContext,
201
+ ) => Promise<void> | void;
202
+
203
+ /**
204
+ * Called when updating a conversation fails
205
+ * @param error - The error that occurred
206
+ * @param context - Request context
207
+ */
208
+ onUpdateConversationError?: (
209
+ error: Error,
210
+ context: ChatApiContext,
211
+ ) => Promise<void> | void;
212
+
213
+ /**
214
+ * Called when deleting a conversation fails
215
+ * @param error - The error that occurred
216
+ * @param context - Request context
217
+ */
218
+ onDeleteConversationError?: (
219
+ error: Error,
220
+ context: ChatApiContext,
221
+ ) => Promise<void> | void;
222
+ }
223
+
224
+ /**
225
+ * Plugin mode for AI Chat
226
+ * - 'authenticated': Conversations persisted with userId (default)
227
+ * - 'public': Stateless chat, no persistence (ideal for public chatbots)
228
+ */
229
+ export type AiChatMode = "authenticated" | "public";
230
+
231
+ /**
232
+ * Configuration for AI Chat backend plugin
233
+ */
234
+ export interface AiChatBackendConfig {
235
+ /**
236
+ * The language model to use for chat completions.
237
+ * Supports any model from AI SDK providers (OpenAI, Anthropic, Google, etc.)
238
+ */
239
+ model: LanguageModel;
240
+
241
+ /**
242
+ * Plugin mode:
243
+ * - 'authenticated': Conversations persisted with userId (requires getUserId)
244
+ * - 'public': Stateless chat, no persistence (ideal for public chatbots)
245
+ * @default 'authenticated'
246
+ */
247
+ mode?: AiChatMode;
248
+
249
+ /**
250
+ * Extract userId from request context (authenticated mode only).
251
+ * Return null/undefined to deny access in authenticated mode.
252
+ * This function is called for all conversation operations.
253
+ * @example (ctx) => ctx.headers?.get('x-user-id')
254
+ */
255
+ getUserId?: (
256
+ context: ChatApiContext,
257
+ ) => string | null | undefined | Promise<string | null | undefined>;
258
+
259
+ /**
260
+ * Optional system prompt to prepend to all conversations
261
+ */
262
+ systemPrompt?: string;
263
+
264
+ /**
265
+ * Optional tools to make available to the model.
266
+ * Uses AI SDK v5 tool format.
267
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/tools-and-tool-calling
268
+ */
269
+ tools?: Record<string, Tool>;
270
+
271
+ /**
272
+ * Optional hooks for customizing plugin behavior
273
+ */
274
+ hooks?: AiChatBackendHooks;
275
+ }
276
+
277
+ /**
278
+ * AI Chat backend plugin
279
+ * Provides API endpoints for AI-powered chat with conversation history
280
+ * Uses AI SDK v5 for model interactions
281
+ *
282
+ * @param config - Configuration including model, tools, and optional hooks
283
+ */
284
+ export const aiChatBackendPlugin = (config: AiChatBackendConfig) =>
285
+ defineBackendPlugin({
286
+ name: "ai-chat",
287
+ // Always include db schema - in public mode we just don't use it
288
+ dbPlugin: dbSchema,
289
+ routes: (adapter: Adapter) => {
290
+ const mode = config.mode ?? "authenticated";
291
+ const isPublicMode = mode === "public";
292
+
293
+ // Helper to extract text content from UIMessage (for conversation titles, etc.)
294
+ const getMessageTextContent = (msg: UIMessage): string => {
295
+ if (msg.parts && Array.isArray(msg.parts)) {
296
+ return msg.parts
297
+ .filter((part: any) => part.type === "text")
298
+ .map((part: any) => part.text)
299
+ .join("");
300
+ }
301
+ return "";
302
+ };
303
+
304
+ // Helper to serialize message parts to JSON (preserves all files)
305
+ const serializeMessageParts = (msg: UIMessage): string => {
306
+ if (msg.parts && Array.isArray(msg.parts)) {
307
+ // Filter to only include text and file parts (images, PDFs, text files, etc.)
308
+ const serializableParts = msg.parts.filter(
309
+ (part: any) => part.type === "text" || part.type === "file",
310
+ );
311
+ return JSON.stringify(serializableParts);
312
+ }
313
+ return JSON.stringify([]);
314
+ };
315
+
316
+ // Helper to get userId in authenticated mode
317
+ // Returns null if no getUserId is configured, or the userId if available
318
+ // Throws if getUserId returns null/undefined (auth required but not provided)
319
+ const resolveUserId = async (
320
+ context: ChatApiContext,
321
+ throwOnMissing: () => never,
322
+ ): Promise<string | null> => {
323
+ if (isPublicMode) {
324
+ return null;
325
+ }
326
+ if (!config.getUserId) {
327
+ // If no getUserId is provided, conversations are not user-scoped
328
+ return null;
329
+ }
330
+ const userId = await config.getUserId(context);
331
+ if (!userId) {
332
+ throwOnMissing();
333
+ }
334
+ return userId;
335
+ };
336
+
337
+ // ============== Chat Endpoint ==============
338
+ const chat = createEndpoint(
339
+ "/chat",
340
+ {
341
+ method: "POST",
342
+ body: chatRequestSchema,
343
+ },
344
+ async (ctx) => {
345
+ const { messages: rawMessages, conversationId } = ctx.body;
346
+ const uiMessages = rawMessages as UIMessage[];
347
+
348
+ const context: ChatApiContext = {
349
+ body: ctx.body,
350
+ headers: ctx.headers,
351
+ request: ctx.request,
352
+ };
353
+
354
+ try {
355
+ // Authorization hook
356
+ if (config.hooks?.onBeforeChat) {
357
+ const messagesForHook = uiMessages.map((msg) => ({
358
+ role: msg.role,
359
+ content: getMessageTextContent(msg),
360
+ }));
361
+ const canChat = await config.hooks.onBeforeChat(
362
+ messagesForHook,
363
+ context,
364
+ );
365
+ if (!canChat) {
366
+ throw ctx.error(403, {
367
+ message: "Unauthorized: Cannot start chat",
368
+ });
369
+ }
370
+ }
371
+
372
+ const firstMessage = uiMessages[0];
373
+ if (!firstMessage) {
374
+ throw ctx.error(400, {
375
+ message: "At least one message is required",
376
+ });
377
+ }
378
+ const firstMessageContent = getMessageTextContent(firstMessage);
379
+
380
+ // Convert UIMessages to CoreMessages for streamText
381
+ const modelMessages = convertToModelMessages(uiMessages);
382
+
383
+ // Add system prompt if configured
384
+ const messagesWithSystem = config.systemPrompt
385
+ ? [
386
+ { role: "system" as const, content: config.systemPrompt },
387
+ ...modelMessages,
388
+ ]
389
+ : modelMessages;
390
+
391
+ // PUBLIC MODE: Stream without persistence
392
+ if (isPublicMode) {
393
+ const result = streamText({
394
+ model: config.model,
395
+ messages: messagesWithSystem,
396
+ tools: config.tools,
397
+ // Enable multi-step tool calls if tools are configured
398
+ ...(config.tools ? { stopWhen: stepCountIs(5) } : {}),
399
+ });
400
+
401
+ return result.toUIMessageStreamResponse({
402
+ originalMessages: uiMessages,
403
+ });
404
+ }
405
+
406
+ // AUTHENTICATED MODE: Persist conversations
407
+ // Get userId if getUserId is configured
408
+ let userId: string | null = null;
409
+ if (config.getUserId) {
410
+ const resolvedUserId = await config.getUserId(context);
411
+ if (!resolvedUserId) {
412
+ throw ctx.error(403, {
413
+ message: "Unauthorized: User authentication required",
414
+ });
415
+ }
416
+ userId = resolvedUserId;
417
+ }
418
+
419
+ let convId = conversationId;
420
+
421
+ // Create or verify conversation
422
+ if (!convId) {
423
+ const newConv = await adapter.create<Conversation>({
424
+ model: "conversation",
425
+ data: {
426
+ ...(userId ? { userId } : {}),
427
+ title: firstMessageContent.slice(0, 50) || "New Conversation",
428
+ createdAt: new Date(),
429
+ updatedAt: new Date(),
430
+ },
431
+ });
432
+ convId = newConv.id;
433
+ } else {
434
+ const existing = await adapter.findMany<Conversation>({
435
+ model: "conversation",
436
+ where: [{ field: "id", value: convId, operator: "eq" }],
437
+ limit: 1,
438
+ });
439
+ if (!existing.length) {
440
+ const newConv = await adapter.create<Conversation>({
441
+ model: "conversation",
442
+ data: {
443
+ id: convId,
444
+ ...(userId ? { userId } : {}),
445
+ title:
446
+ firstMessageContent.slice(0, 50) || "New Conversation",
447
+ createdAt: new Date(),
448
+ updatedAt: new Date(),
449
+ } as Conversation,
450
+ });
451
+ convId = newConv.id;
452
+ } else {
453
+ // Verify ownership if userId is set
454
+ const conv = existing[0]!;
455
+ if (userId && conv.userId && conv.userId !== userId) {
456
+ throw ctx.error(403, {
457
+ message: "Unauthorized: Cannot access this conversation",
458
+ });
459
+ }
460
+ }
461
+ }
462
+
463
+ // Sync database messages with client state
464
+ // The client sends all messages for the conversation
465
+ // We need to ensure the DB matches this state before adding the assistant response
466
+ const existingMessages = await adapter.findMany<Message>({
467
+ model: "message",
468
+ where: [
469
+ {
470
+ field: "conversationId",
471
+ value: convId as string,
472
+ operator: "eq",
473
+ },
474
+ ],
475
+ sortBy: { field: "createdAt", direction: "asc" },
476
+ });
477
+
478
+ const lastIncomingMessage = uiMessages[uiMessages.length - 1];
479
+ const isNewUserMessage = lastIncomingMessage?.role === "user";
480
+
481
+ // Determine the expected DB count before we add new messages:
482
+ // - If last incoming is a user message: it might be new or existing
483
+ // - Compare with what's in DB to determine if it's a new message or regenerate
484
+ let expectedDbCount: number;
485
+ let shouldAddUserMessage = false;
486
+
487
+ if (isNewUserMessage) {
488
+ // Check if this user message already exists in DB
489
+ // by comparing the last user message in each
490
+ const lastDbUserMessage = [...existingMessages]
491
+ .reverse()
492
+ .find((m) => m.role === "user");
493
+ const incomingUserContent =
494
+ serializeMessageParts(lastIncomingMessage);
495
+
496
+ if (
497
+ lastDbUserMessage &&
498
+ lastDbUserMessage.content === incomingUserContent
499
+ ) {
500
+ // The user message already exists - this is a regenerate
501
+ // DB should have all incoming messages (no new user message to add)
502
+ expectedDbCount = uiMessages.length;
503
+ shouldAddUserMessage = false;
504
+ } else {
505
+ // New user message - DB should have incoming count - 1
506
+ expectedDbCount = uiMessages.length - 1;
507
+ shouldAddUserMessage = true;
508
+ }
509
+ } else {
510
+ // Last message is not user (unusual case)
511
+ expectedDbCount = uiMessages.length;
512
+ shouldAddUserMessage = false;
513
+ }
514
+
515
+ // If DB has more messages than expected, delete the excess
516
+ // This handles both edit (truncated history) and retry (regenerating last response)
517
+ // Use a transaction to ensure atomicity - if create fails, deletions are rolled back
518
+ const actualDbCount = existingMessages.length;
519
+ const messagesToDelete =
520
+ actualDbCount > expectedDbCount
521
+ ? existingMessages.slice(expectedDbCount)
522
+ : [];
523
+
524
+ // Wrap deletion and creation in a transaction for atomicity
525
+ // This prevents data loss if the create operation fails after deletions
526
+ await adapter.transaction(async (tx) => {
527
+ // Delete excess messages
528
+ for (const msg of messagesToDelete) {
529
+ await tx.delete({
530
+ model: "message",
531
+ where: [{ field: "id", value: msg.id }],
532
+ });
533
+ }
534
+
535
+ // Save user message if it's new
536
+ if (shouldAddUserMessage && lastIncomingMessage) {
537
+ await tx.create<Message>({
538
+ model: "message",
539
+ data: {
540
+ conversationId: convId as string,
541
+ role: "user",
542
+ content: serializeMessageParts(lastIncomingMessage),
543
+ createdAt: new Date(),
544
+ },
545
+ });
546
+ }
547
+ });
548
+
549
+ const result = streamText({
550
+ model: config.model,
551
+ messages: messagesWithSystem,
552
+ tools: config.tools,
553
+ // Enable multi-step tool calls if tools are configured
554
+ ...(config.tools ? { stopWhen: stepCountIs(5) } : {}),
555
+ onFinish: async (completion: { text: string }) => {
556
+ // Wrap in try-catch since this runs after the response is sent
557
+ // and errors would otherwise become unhandled promise rejections
558
+ try {
559
+ // Save assistant message (serialize as parts for consistency)
560
+ const assistantParts = completion.text
561
+ ? [{ type: "text", text: completion.text }]
562
+ : [];
563
+ await adapter.create<Message>({
564
+ model: "message",
565
+ data: {
566
+ conversationId: convId as string,
567
+ role: "assistant",
568
+ content: JSON.stringify(assistantParts),
569
+ createdAt: new Date(),
570
+ },
571
+ });
572
+
573
+ // Update conversation timestamp
574
+ await adapter.update({
575
+ model: "conversation",
576
+ where: [{ field: "id", value: convId as string }],
577
+ update: { updatedAt: new Date() },
578
+ });
579
+
580
+ // Lifecycle hook
581
+ if (config.hooks?.onAfterChat) {
582
+ const messages = await adapter.findMany<Message>({
583
+ model: "message",
584
+ where: [
585
+ {
586
+ field: "conversationId",
587
+ value: convId as string,
588
+ operator: "eq",
589
+ },
590
+ ],
591
+ sortBy: { field: "createdAt", direction: "asc" },
592
+ });
593
+ await config.hooks.onAfterChat(
594
+ convId as string,
595
+ messages,
596
+ context,
597
+ );
598
+ }
599
+ } catch (error) {
600
+ // Log the error since the response is already sent
601
+ console.error("[ai-chat] Error in onFinish callback:", error);
602
+ // Call error hook if configured
603
+ if (config.hooks?.onChatError) {
604
+ try {
605
+ await config.hooks.onChatError(error as Error, context);
606
+ } catch (hookError) {
607
+ console.error(
608
+ "[ai-chat] Error in onChatError hook:",
609
+ hookError,
610
+ );
611
+ }
612
+ }
613
+ }
614
+ },
615
+ });
616
+
617
+ // Return the stream response with conversation ID header
618
+ // This allows the client to know which conversation was created/used
619
+ const response = result.toUIMessageStreamResponse({
620
+ originalMessages: uiMessages,
621
+ });
622
+
623
+ // Add the conversation ID header to the response
624
+ const headers = new Headers(response.headers);
625
+ headers.set("X-Conversation-Id", convId as string);
626
+
627
+ return new Response(response.body, {
628
+ status: response.status,
629
+ statusText: response.statusText,
630
+ headers,
631
+ });
632
+ } catch (error) {
633
+ if (config.hooks?.onChatError) {
634
+ await config.hooks.onChatError(error as Error, context);
635
+ }
636
+ throw error;
637
+ }
638
+ },
639
+ );
640
+
641
+ // ============== Create Conversation ==============
642
+ const createConversation = createEndpoint(
643
+ "/chat/conversations",
644
+ {
645
+ method: "POST",
646
+ body: createConversationSchema,
647
+ },
648
+ async (ctx) => {
649
+ // Public mode: conversations are not persisted
650
+ if (isPublicMode) {
651
+ throw ctx.error(404, {
652
+ message: "Conversations not available in public mode",
653
+ });
654
+ }
655
+
656
+ const { id, title } = ctx.body;
657
+ const context: ChatApiContext = {
658
+ body: ctx.body,
659
+ headers: ctx.headers,
660
+ };
661
+
662
+ try {
663
+ // Get userId if configured
664
+ const userId = await resolveUserId(context, () => {
665
+ throw ctx.error(403, {
666
+ message: "Unauthorized: User authentication required",
667
+ });
668
+ });
669
+
670
+ // Authorization hook
671
+ if (config.hooks?.onBeforeCreateConversation) {
672
+ const canCreate = await config.hooks.onBeforeCreateConversation(
673
+ { id, title },
674
+ context,
675
+ );
676
+ if (!canCreate) {
677
+ throw ctx.error(403, {
678
+ message: "Unauthorized: Cannot create conversation",
679
+ });
680
+ }
681
+ }
682
+
683
+ const newConv = await adapter.create<Conversation>({
684
+ model: "conversation",
685
+ data: {
686
+ ...(id ? { id } : {}),
687
+ ...(userId ? { userId } : {}),
688
+ title: title || "New Conversation",
689
+ createdAt: new Date(),
690
+ updatedAt: new Date(),
691
+ } as Conversation,
692
+ });
693
+
694
+ // Lifecycle hook
695
+ if (config.hooks?.onConversationCreated) {
696
+ await config.hooks.onConversationCreated(newConv, context);
697
+ }
698
+
699
+ return newConv;
700
+ } catch (error) {
701
+ if (config.hooks?.onCreateConversationError) {
702
+ await config.hooks.onCreateConversationError(
703
+ error as Error,
704
+ context,
705
+ );
706
+ }
707
+ throw error;
708
+ }
709
+ },
710
+ );
711
+
712
+ // ============== List Conversations ==============
713
+ const listConversations = createEndpoint(
714
+ "/chat/conversations",
715
+ {
716
+ method: "GET",
717
+ },
718
+ async (ctx) => {
719
+ // Public mode: return empty list
720
+ if (isPublicMode) {
721
+ return [];
722
+ }
723
+
724
+ const context: ChatApiContext = {
725
+ headers: ctx.headers,
726
+ };
727
+
728
+ try {
729
+ // Get userId if configured
730
+ const userId = await resolveUserId(context, () => {
731
+ throw ctx.error(403, {
732
+ message: "Unauthorized: User authentication required",
733
+ });
734
+ });
735
+
736
+ // Authorization hook
737
+ if (config.hooks?.onBeforeListConversations) {
738
+ const canList =
739
+ await config.hooks.onBeforeListConversations(context);
740
+ if (!canList) {
741
+ throw ctx.error(403, {
742
+ message: "Unauthorized: Cannot list conversations",
743
+ });
744
+ }
745
+ }
746
+
747
+ // Build where conditions - filter by userId if set
748
+ const whereConditions: Array<{
749
+ field: string;
750
+ value: string;
751
+ operator: "eq";
752
+ }> = [];
753
+ if (userId) {
754
+ whereConditions.push({
755
+ field: "userId",
756
+ value: userId,
757
+ operator: "eq",
758
+ });
759
+ }
760
+
761
+ const conversations = await adapter.findMany<Conversation>({
762
+ model: "conversation",
763
+ where: whereConditions.length > 0 ? whereConditions : undefined,
764
+ sortBy: { field: "updatedAt", direction: "desc" },
765
+ });
766
+
767
+ // Lifecycle hook
768
+ if (config.hooks?.onConversationsRead) {
769
+ await config.hooks.onConversationsRead(conversations, context);
770
+ }
771
+
772
+ return conversations;
773
+ } catch (error) {
774
+ if (config.hooks?.onListConversationsError) {
775
+ await config.hooks.onListConversationsError(
776
+ error as Error,
777
+ context,
778
+ );
779
+ }
780
+ throw error;
781
+ }
782
+ },
783
+ );
784
+
785
+ // ============== Get Conversation ==============
786
+ const getConversation = createEndpoint(
787
+ "/chat/conversations/:id",
788
+ {
789
+ method: "GET",
790
+ },
791
+ async (ctx) => {
792
+ // Public mode: conversations are not persisted
793
+ if (isPublicMode) {
794
+ throw ctx.error(404, {
795
+ message: "Conversations not available in public mode",
796
+ });
797
+ }
798
+
799
+ const { id } = ctx.params;
800
+ const context: ChatApiContext = {
801
+ params: ctx.params,
802
+ headers: ctx.headers,
803
+ };
804
+
805
+ try {
806
+ // Get userId if configured
807
+ const userId = await resolveUserId(context, () => {
808
+ throw ctx.error(403, {
809
+ message: "Unauthorized: User authentication required",
810
+ });
811
+ });
812
+
813
+ // Authorization hook
814
+ if (config.hooks?.onBeforeGetConversation) {
815
+ const canGet = await config.hooks.onBeforeGetConversation(
816
+ id,
817
+ context,
818
+ );
819
+ if (!canGet) {
820
+ throw ctx.error(403, {
821
+ message: "Unauthorized: Cannot get conversation",
822
+ });
823
+ }
824
+ }
825
+
826
+ // Fetch conversation with messages in a single query using join
827
+ const conversations =
828
+ await adapter.findMany<ConversationWithMessages>({
829
+ model: "conversation",
830
+ where: [{ field: "id", value: id, operator: "eq" }],
831
+ limit: 1,
832
+ join: {
833
+ message: true,
834
+ },
835
+ });
836
+
837
+ if (!conversations.length) {
838
+ throw ctx.error(404, { message: "Conversation not found" });
839
+ }
840
+
841
+ const conversation = conversations[0]!;
842
+
843
+ // Verify ownership if userId is set
844
+ if (
845
+ userId &&
846
+ conversation.userId &&
847
+ conversation.userId !== userId
848
+ ) {
849
+ throw ctx.error(403, {
850
+ message: "Unauthorized: Cannot access this conversation",
851
+ });
852
+ }
853
+
854
+ // Sort messages by createdAt and map to result format
855
+ const messages = (conversation.message || []).sort(
856
+ (a, b) =>
857
+ new Date(a.createdAt).getTime() -
858
+ new Date(b.createdAt).getTime(),
859
+ );
860
+
861
+ const { message: _, ...conversationWithoutJoin } = conversation;
862
+ const result = {
863
+ ...conversationWithoutJoin,
864
+ messages,
865
+ } as Conversation & { messages: Message[] };
866
+
867
+ // Lifecycle hook
868
+ if (config.hooks?.onConversationRead) {
869
+ await config.hooks.onConversationRead(result, context);
870
+ }
871
+
872
+ return result;
873
+ } catch (error) {
874
+ if (config.hooks?.onGetConversationError) {
875
+ await config.hooks.onGetConversationError(
876
+ error as Error,
877
+ context,
878
+ );
879
+ }
880
+ throw error;
881
+ }
882
+ },
883
+ );
884
+
885
+ // ============== Update Conversation ==============
886
+ const updateConversation = createEndpoint(
887
+ "/chat/conversations/:id",
888
+ {
889
+ method: "PUT",
890
+ body: updateConversationSchema,
891
+ },
892
+ async (ctx) => {
893
+ // Public mode: conversations are not persisted
894
+ if (isPublicMode) {
895
+ throw ctx.error(404, {
896
+ message: "Conversations not available in public mode",
897
+ });
898
+ }
899
+
900
+ const { id } = ctx.params;
901
+ const { title } = ctx.body;
902
+ const context: ChatApiContext = {
903
+ params: ctx.params,
904
+ body: ctx.body,
905
+ headers: ctx.headers,
906
+ };
907
+
908
+ try {
909
+ // Get userId if configured
910
+ const userId = await resolveUserId(context, () => {
911
+ throw ctx.error(403, {
912
+ message: "Unauthorized: User authentication required",
913
+ });
914
+ });
915
+
916
+ // Check ownership before update
917
+ const existing = await adapter.findMany<Conversation>({
918
+ model: "conversation",
919
+ where: [{ field: "id", value: id, operator: "eq" }],
920
+ limit: 1,
921
+ });
922
+
923
+ if (!existing.length) {
924
+ throw ctx.error(404, { message: "Conversation not found" });
925
+ }
926
+
927
+ const conversation = existing[0]!;
928
+ if (
929
+ userId &&
930
+ conversation.userId &&
931
+ conversation.userId !== userId
932
+ ) {
933
+ throw ctx.error(403, {
934
+ message: "Unauthorized: Cannot update this conversation",
935
+ });
936
+ }
937
+
938
+ // Authorization hook
939
+ if (config.hooks?.onBeforeUpdateConversation) {
940
+ const canUpdate = await config.hooks.onBeforeUpdateConversation(
941
+ id,
942
+ { title },
943
+ context,
944
+ );
945
+ if (!canUpdate) {
946
+ throw ctx.error(403, {
947
+ message: "Unauthorized: Cannot update conversation",
948
+ });
949
+ }
950
+ }
951
+
952
+ const updated = await adapter.update<Conversation>({
953
+ model: "conversation",
954
+ where: [{ field: "id", value: id }],
955
+ update: {
956
+ ...(title !== undefined ? { title } : {}),
957
+ updatedAt: new Date(),
958
+ },
959
+ });
960
+
961
+ if (!updated) {
962
+ throw ctx.error(404, { message: "Conversation not found" });
963
+ }
964
+
965
+ // Lifecycle hook
966
+ if (config.hooks?.onConversationUpdated) {
967
+ await config.hooks.onConversationUpdated(updated, context);
968
+ }
969
+
970
+ return updated;
971
+ } catch (error) {
972
+ if (config.hooks?.onUpdateConversationError) {
973
+ await config.hooks.onUpdateConversationError(
974
+ error as Error,
975
+ context,
976
+ );
977
+ }
978
+ throw error;
979
+ }
980
+ },
981
+ );
982
+
983
+ // ============== Delete Conversation ==============
984
+ const deleteConversation = createEndpoint(
985
+ "/chat/conversations/:id",
986
+ {
987
+ method: "DELETE",
988
+ },
989
+ async (ctx) => {
990
+ // Public mode: conversations are not persisted
991
+ if (isPublicMode) {
992
+ throw ctx.error(404, {
993
+ message: "Conversations not available in public mode",
994
+ });
995
+ }
996
+
997
+ const { id } = ctx.params;
998
+ const context: ChatApiContext = {
999
+ params: ctx.params,
1000
+ headers: ctx.headers,
1001
+ };
1002
+
1003
+ try {
1004
+ // Get userId if configured
1005
+ const userId = await resolveUserId(context, () => {
1006
+ throw ctx.error(403, {
1007
+ message: "Unauthorized: User authentication required",
1008
+ });
1009
+ });
1010
+
1011
+ // Check ownership before delete
1012
+ const existing = await adapter.findMany<Conversation>({
1013
+ model: "conversation",
1014
+ where: [{ field: "id", value: id, operator: "eq" }],
1015
+ limit: 1,
1016
+ });
1017
+
1018
+ if (!existing.length) {
1019
+ throw ctx.error(404, { message: "Conversation not found" });
1020
+ }
1021
+
1022
+ const conversation = existing[0]!;
1023
+ if (
1024
+ userId &&
1025
+ conversation.userId &&
1026
+ conversation.userId !== userId
1027
+ ) {
1028
+ throw ctx.error(403, {
1029
+ message: "Unauthorized: Cannot delete this conversation",
1030
+ });
1031
+ }
1032
+
1033
+ // Authorization hook
1034
+ if (config.hooks?.onBeforeDeleteConversation) {
1035
+ const canDelete = await config.hooks.onBeforeDeleteConversation(
1036
+ id,
1037
+ context,
1038
+ );
1039
+ if (!canDelete) {
1040
+ throw ctx.error(403, {
1041
+ message: "Unauthorized: Cannot delete conversation",
1042
+ });
1043
+ }
1044
+ }
1045
+
1046
+ // Messages are automatically deleted via cascade (onDelete: "cascade")
1047
+ await adapter.delete({
1048
+ model: "conversation",
1049
+ where: [{ field: "id", value: id }],
1050
+ });
1051
+
1052
+ // Lifecycle hook
1053
+ if (config.hooks?.onConversationDeleted) {
1054
+ await config.hooks.onConversationDeleted(id, context);
1055
+ }
1056
+
1057
+ return { success: true };
1058
+ } catch (error) {
1059
+ if (config.hooks?.onDeleteConversationError) {
1060
+ await config.hooks.onDeleteConversationError(
1061
+ error as Error,
1062
+ context,
1063
+ );
1064
+ }
1065
+ throw error;
1066
+ }
1067
+ },
1068
+ );
1069
+
1070
+ return {
1071
+ chat,
1072
+ createConversation,
1073
+ listConversations,
1074
+ getConversation,
1075
+ updateConversation,
1076
+ deleteConversation,
1077
+ };
1078
+ },
1079
+ });
1080
+
1081
+ export type AiChatApiRouter = ReturnType<
1082
+ ReturnType<typeof aiChatBackendPlugin>["routes"]
1083
+ >;