@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.
- package/README.md +85 -37
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- package/dist/packages/better-stack/src/plugins/ai-chat/api/plugin.cjs +610 -0
- package/dist/packages/better-stack/src/plugins/ai-chat/api/plugin.mjs +608 -0
- package/dist/packages/better-stack/src/plugins/ai-chat/client/components/chat-input.cjs +221 -0
- package/dist/packages/better-stack/src/plugins/ai-chat/client/components/chat-input.mjs +219 -0
- package/dist/packages/better-stack/src/plugins/ai-chat/client/components/chat-interface.cjs +341 -0
- package/dist/packages/better-stack/src/plugins/ai-chat/client/components/chat-interface.mjs +339 -0
- package/dist/packages/better-stack/src/plugins/ai-chat/client/components/chat-layout.cjs +135 -0
- package/dist/packages/better-stack/src/plugins/ai-chat/client/components/chat-layout.mjs +133 -0
- package/dist/packages/better-stack/src/plugins/ai-chat/client/components/chat-message.cjs +429 -0
- package/dist/packages/better-stack/src/plugins/ai-chat/client/components/chat-message.mjs +423 -0
- package/dist/packages/better-stack/src/plugins/ai-chat/client/components/chat-sidebar.cjs +227 -0
- package/dist/packages/better-stack/src/plugins/ai-chat/client/components/chat-sidebar.mjs +225 -0
- package/dist/packages/better-stack/src/plugins/ai-chat/client/components/loading/chat-page-skeleton.cjs +31 -0
- package/dist/packages/better-stack/src/plugins/ai-chat/client/components/loading/chat-page-skeleton.mjs +29 -0
- package/dist/packages/better-stack/src/plugins/ai-chat/client/components/loading/index.cjs +11 -0
- package/dist/packages/better-stack/src/plugins/ai-chat/client/components/loading/index.mjs +8 -0
- package/dist/packages/better-stack/src/plugins/ai-chat/client/components/pages/404-page.cjs +18 -0
- package/dist/packages/better-stack/src/plugins/ai-chat/client/components/pages/404-page.mjs +16 -0
- package/dist/packages/better-stack/src/plugins/ai-chat/client/components/pages/chat-page.cjs +39 -0
- package/dist/packages/better-stack/src/plugins/ai-chat/client/components/pages/chat-page.internal.cjs +22 -0
- package/dist/packages/better-stack/src/plugins/ai-chat/client/components/pages/chat-page.internal.mjs +20 -0
- package/dist/packages/better-stack/src/plugins/ai-chat/client/components/pages/chat-page.mjs +37 -0
- package/dist/packages/better-stack/src/plugins/ai-chat/client/components/shared/default-error.cjs +18 -0
- package/dist/packages/better-stack/src/plugins/ai-chat/client/components/shared/default-error.mjs +16 -0
- package/dist/packages/better-stack/src/plugins/ai-chat/client/components/shared/error-placeholder.cjs +26 -0
- package/dist/packages/better-stack/src/plugins/ai-chat/client/components/shared/error-placeholder.mjs +24 -0
- package/dist/packages/better-stack/src/plugins/ai-chat/client/components/tool-call-display.cjs +123 -0
- package/dist/packages/better-stack/src/plugins/ai-chat/client/components/tool-call-display.mjs +121 -0
- package/dist/packages/better-stack/src/plugins/ai-chat/client/hooks/chat-hooks.cjs +199 -0
- package/dist/packages/better-stack/src/plugins/ai-chat/client/hooks/chat-hooks.mjs +191 -0
- package/dist/packages/better-stack/src/plugins/ai-chat/client/localization/index.cjs +63 -0
- package/dist/packages/better-stack/src/plugins/ai-chat/client/localization/index.mjs +61 -0
- package/dist/packages/better-stack/src/plugins/ai-chat/client/overrides.cjs +14 -0
- package/dist/packages/better-stack/src/plugins/ai-chat/client/overrides.mjs +11 -0
- package/dist/packages/better-stack/src/plugins/ai-chat/client/plugin.cjs +241 -0
- package/dist/packages/better-stack/src/plugins/ai-chat/client/plugin.mjs +239 -0
- package/dist/packages/better-stack/src/plugins/ai-chat/db.cjs +65 -0
- package/dist/packages/better-stack/src/plugins/ai-chat/db.mjs +63 -0
- package/dist/packages/better-stack/src/plugins/ai-chat/schemas.cjs +42 -0
- package/dist/packages/better-stack/src/plugins/ai-chat/schemas.mjs +38 -0
- package/dist/packages/better-stack/src/plugins/blog/client/components/shared/markdown-content.cjs +12 -309
- package/dist/packages/better-stack/src/plugins/blog/client/components/shared/markdown-content.mjs +13 -303
- package/dist/packages/better-stack/src/plugins/blog/client/components/shared/page-wrapper.cjs +2 -2
- package/dist/packages/better-stack/src/plugins/blog/client/components/shared/page-wrapper.mjs +2 -2
- package/dist/packages/ui/src/components/accordion.cjs +67 -0
- package/dist/packages/ui/src/components/accordion.mjs +62 -0
- package/dist/packages/ui/src/components/alert-dialog.cjs +1 -1
- package/dist/packages/ui/src/components/alert-dialog.mjs +1 -1
- package/dist/packages/{better-stack/src/plugins/blog/client/components/shared/better-blog-attribution.cjs → ui/src/components/better-stack-attribution.cjs} +3 -3
- package/dist/packages/{better-stack/src/plugins/blog/client/components/shared/better-blog-attribution.mjs → ui/src/components/better-stack-attribution.mjs} +3 -3
- package/dist/packages/ui/src/components/dialog.cjs +14 -0
- package/dist/packages/ui/src/components/dialog.mjs +14 -1
- package/dist/packages/ui/src/components/dropdown-menu.cjs +67 -0
- package/dist/packages/ui/src/components/dropdown-menu.mjs +62 -0
- package/dist/packages/ui/src/components/markdown-content.cjs +306 -0
- package/dist/packages/ui/src/components/markdown-content.mjs +297 -0
- package/dist/packages/ui/src/components/scroll-area.cjs +63 -0
- package/dist/packages/ui/src/components/scroll-area.mjs +60 -0
- package/dist/packages/ui/src/components/select.cjs +1 -1
- package/dist/packages/ui/src/components/select.mjs +1 -1
- package/dist/packages/ui/src/components/sheet.cjs +87 -0
- package/dist/packages/ui/src/components/sheet.mjs +69 -0
- package/dist/plugins/ai-chat/api/index.cjs +9 -0
- package/dist/plugins/ai-chat/api/index.d.cts +9 -0
- package/dist/plugins/ai-chat/api/index.d.mts +9 -0
- package/dist/plugins/ai-chat/api/index.d.ts +9 -0
- package/dist/plugins/ai-chat/api/index.mjs +2 -0
- package/dist/plugins/ai-chat/client/components/index.cjs +29 -0
- package/dist/plugins/ai-chat/client/components/index.d.cts +30 -0
- package/dist/plugins/ai-chat/client/components/index.d.mts +30 -0
- package/dist/plugins/ai-chat/client/components/index.d.ts +30 -0
- package/dist/plugins/ai-chat/client/components/index.mjs +12 -0
- package/dist/plugins/ai-chat/client/hooks/index.cjs +13 -0
- package/dist/plugins/ai-chat/client/hooks/index.d.cts +98 -0
- package/dist/plugins/ai-chat/client/hooks/index.d.mts +98 -0
- package/dist/plugins/ai-chat/client/hooks/index.d.ts +98 -0
- package/dist/plugins/ai-chat/client/hooks/index.mjs +1 -0
- package/dist/plugins/ai-chat/client/index.cjs +21 -0
- package/dist/plugins/ai-chat/client/index.d.cts +156 -0
- package/dist/plugins/ai-chat/client/index.d.mts +156 -0
- package/dist/plugins/ai-chat/client/index.d.ts +156 -0
- package/dist/plugins/ai-chat/client/index.mjs +8 -0
- package/dist/plugins/ai-chat/client.css +6 -0
- package/dist/plugins/ai-chat/query-keys.cjs +60 -0
- package/dist/plugins/ai-chat/query-keys.d.cts +478 -0
- package/dist/plugins/ai-chat/query-keys.d.mts +478 -0
- package/dist/plugins/ai-chat/query-keys.d.ts +478 -0
- package/dist/plugins/ai-chat/query-keys.mjs +58 -0
- package/dist/plugins/ai-chat/style.css +19 -0
- package/dist/plugins/blog/api/index.d.cts +1 -1
- package/dist/plugins/blog/api/index.d.mts +1 -1
- package/dist/plugins/blog/api/index.d.ts +1 -1
- package/dist/plugins/blog/client/components/shared/markdown-content-styles.css +91 -62
- package/dist/plugins/blog/client/hooks/index.d.cts +4 -4
- package/dist/plugins/blog/client/hooks/index.d.mts +4 -4
- package/dist/plugins/blog/client/hooks/index.d.ts +4 -4
- package/dist/plugins/blog/client/index.d.cts +1 -1
- package/dist/plugins/blog/client/index.d.mts +1 -1
- package/dist/plugins/blog/client/index.d.ts +1 -1
- package/dist/plugins/blog/query-keys.d.cts +7 -7
- package/dist/plugins/blog/query-keys.d.mts +7 -7
- package/dist/plugins/blog/query-keys.d.ts +7 -7
- package/dist/shared/stack.Be1QIHEn.d.cts +23 -0
- package/dist/shared/stack.Be1QIHEn.d.mts +23 -0
- package/dist/shared/stack.Be1QIHEn.d.ts +23 -0
- package/dist/shared/stack.DaOcgmrM.d.cts +323 -0
- package/dist/shared/stack.DaOcgmrM.d.mts +323 -0
- package/dist/shared/stack.DaOcgmrM.d.ts +323 -0
- package/package.json +59 -1
- package/src/plugins/ai-chat/api/index.ts +2 -0
- package/src/plugins/ai-chat/api/plugin.ts +1083 -0
- package/src/plugins/ai-chat/client/components/chat-input.tsx +295 -0
- package/src/plugins/ai-chat/client/components/chat-interface.tsx +494 -0
- package/src/plugins/ai-chat/client/components/chat-layout.tsx +175 -0
- package/src/plugins/ai-chat/client/components/chat-message.tsx +561 -0
- package/src/plugins/ai-chat/client/components/chat-sidebar.tsx +296 -0
- package/src/plugins/ai-chat/client/components/index.ts +18 -0
- package/src/plugins/ai-chat/client/components/loading/chat-page-skeleton.tsx +57 -0
- package/src/plugins/ai-chat/client/components/loading/index.tsx +11 -0
- package/src/plugins/ai-chat/client/components/pages/404-page.tsx +27 -0
- package/src/plugins/ai-chat/client/components/pages/chat-page.internal.tsx +31 -0
- package/src/plugins/ai-chat/client/components/pages/chat-page.tsx +46 -0
- package/src/plugins/ai-chat/client/components/shared/default-error.tsx +28 -0
- package/src/plugins/ai-chat/client/components/shared/error-placeholder.tsx +22 -0
- package/src/plugins/ai-chat/client/components/tool-call-display.tsx +197 -0
- package/src/plugins/ai-chat/client/hooks/chat-hooks.tsx +349 -0
- package/src/plugins/ai-chat/client/hooks/index.tsx +1 -0
- package/src/plugins/ai-chat/client/index.ts +25 -0
- package/src/plugins/ai-chat/client/localization/index.ts +156 -0
- package/src/plugins/ai-chat/client/overrides.ts +241 -0
- package/src/plugins/ai-chat/client/plugin.tsx +449 -0
- package/src/plugins/ai-chat/client.css +6 -0
- package/src/plugins/ai-chat/db.ts +65 -0
- package/src/plugins/ai-chat/query-keys.ts +87 -0
- package/src/plugins/ai-chat/schemas.ts +40 -0
- package/src/plugins/ai-chat/style.css +19 -0
- package/src/plugins/ai-chat/types.ts +29 -0
- package/src/plugins/blog/client/components/shared/markdown-content-styles.css +91 -62
- package/src/plugins/blog/client/components/shared/markdown-content.tsx +19 -427
- package/src/plugins/blog/client/components/shared/page-wrapper.tsx +2 -2
- 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
- package/src/plugins/blog/client/components/shared/better-blog-attribution.tsx +0 -19
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- package/dist/shared/{stack.CbuN2zVV.d.ts → stack.CcI4sYJP.d.cts} +3 -3
- package/dist/shared/{stack.CbuN2zVV.d.cts → stack.CcI4sYJP.d.mts} +3 -3
- 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
|
+
>;
|