From dec4b30e0282a10420bcbdd520cdb482b60a80d8 Mon Sep 17 00:00:00 2001 From: kientv Date: Tue, 26 Aug 2025 11:44:43 +0700 Subject: [PATCH 01/33] AI Chatbot --- .env.example | 4 +- .gitignore | 3 +- README.md | 66 ++++---- dist/cacanhthuysinhtrungtin.htm | 151 +++++++++++++++++ dist/components/Bot.d.ts | 4 +- dist/components/Bot.d.ts.map | 2 +- .../bubbles/AgentReasoningBubble.d.ts | 2 +- .../bubbles/AgentReasoningBubble.d.ts.map | 2 +- dist/components/bubbles/BotBubble.d.ts | 2 +- dist/components/bubbles/BotBubble.d.ts.map | 2 +- dist/components/bubbles/GuestBubble.d.ts | 2 +- dist/components/bubbles/GuestBubble.d.ts.map | 2 +- .../components/bubbles/LeadCaptureBubble.d.ts | 2 +- .../bubbles/LeadCaptureBubble.d.ts.map | 2 +- dist/gemini.htm | 151 +++++++++++++++++ dist/queries/sendMessageQuery.d.ts | 20 +-- dist/queries/sendMessageQuery.d.ts.map | 2 +- dist/test.htm | 158 ++++++++++++++++++ dist/trolydieuhanh.htm | 151 +++++++++++++++++ dist/utils/index.d.ts | 6 +- dist/utils/index.d.ts.map | 2 +- dist/web.d.ts | 8 +- dist/web.js | 2 +- dist/web.umd.js | 2 +- dist/window.d.ts | 4 +- dist/window.d.ts.map | 2 +- dist/xtsoauth2.htm | 142 ++++++++++++++++ public/index.html | 4 +- server.js | 38 ++--- src/components/Bot.tsx | 114 +++++++++---- .../bubbles/AgentReasoningBubble.tsx | 4 +- src/components/bubbles/BotBubble.tsx | 25 +-- src/components/bubbles/GuestBubble.tsx | 6 +- src/components/bubbles/LeadCaptureBubble.tsx | 8 +- src/components/buttons/SendButton.tsx | 4 +- src/constants.ts | 4 +- src/features/bubble/components/Bubble.tsx | 4 +- .../bubble/components/BubbleButton.tsx | 4 +- src/features/full/components/Full.tsx | 4 +- src/queries/sendMessageQuery.ts | 38 ++--- src/register.tsx | 4 +- src/utils/embedScript.js | 6 +- src/utils/index.ts | 24 +-- src/window.ts | 10 +- 44 files changed, 996 insertions(+), 201 deletions(-) create mode 100755 dist/cacanhthuysinhtrungtin.htm create mode 100755 dist/gemini.htm create mode 100755 dist/test.htm create mode 100755 dist/trolydieuhanh.htm create mode 100644 dist/xtsoauth2.htm diff --git a/.env.example b/.env.example index 6fefdb0..368d68a 100644 --- a/.env.example +++ b/.env.example @@ -16,11 +16,11 @@ FLOWISE_API_KEY= # CHATFLOWS CONFIGURATION (required) # ============================================== -# Format: [identifier]=[chatflowId],[allowedDomain1],[allowedDomain2],... +# Format: [identifier]=[chatbot],[allowedDomain1],[allowedDomain2],... # # Each entry consists of: # - identifier: Any name you choose (e.g., agent1, support, salesbot) -# - chatflowId: The UUID of your Flowise chatflow +# - chatbot: The chatbot name # - allowedDomains: Comma-separated list of domains where this chat can be embedded # Note: Wildcard domains (*) are not supported for security reasons # diff --git a/.gitignore b/.gitignore index 1af2f6e..dc8f1c5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules .idea -.env \ No newline at end of file +.env +dist diff --git a/README.md b/README.md index 7f11d5e..472392b 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,6 @@ -# Flowise Embed - -Javascript library to display flowise chatbot on your website - -![Flowise](https://github.com/FlowiseAI/FlowiseChatEmbed/blob/main/images/ChatEmbed.gif?raw=true) +# aichatbot.com.vn Embed Install: @@ -18,15 +14,15 @@ Dev: yarn dev ``` -A development server will be running on http://localhost:5678 automatically. Update `public/index.html` to connect directly to Flowise: +A development server will be running on http://localhost:5678 automatically. Update `public/index.html` to connect directly to aichatbot.com.vn: ```html ``` @@ -43,9 +39,9 @@ yarn build ```html @@ -55,13 +51,13 @@ yarn build ```html - + ``` To enable full screen, add `margin: 0` to body style, and confirm you don't set height and width @@ -69,9 +65,9 @@ To enable full screen, add `margin: 0` to body style, and confirm y ```html @@ -328,11 +324,11 @@ For full page testing, use this configuration instead: ```html - + diff --git a/dist/cacanhthuysinhtrungtin.htm b/dist/cacanhthuysinhtrungtin.htm new file mode 100755 index 0000000..f3808e1 --- /dev/null +++ b/dist/cacanhthuysinhtrungtin.htm @@ -0,0 +1,151 @@ + + + + + + +cacanhthuysinhtrungtin! + + + +
+ + +
+ + + + + diff --git a/dist/components/Bot.d.ts b/dist/components/Bot.d.ts index 9041c25..da464c3 100644 --- a/dist/components/Bot.d.ts +++ b/dist/components/Bot.d.ts @@ -81,10 +81,10 @@ export type MessageType = { type observerConfigType = (accessor: string | boolean | object | MessageType[]) => void; export type observersConfigType = Record<'observeUserInput' | 'observeLoading' | 'observeMessages', observerConfigType>; export type BotProps = { - chatflowid: string; + chatbot: string; apiHost?: string; onRequest?: (request: RequestInit) => Promise; - chatflowConfig?: Record; + config?: Record; backgroundColor?: string; welcomeMessage?: string; errorMessage?: string; diff --git a/dist/components/Bot.d.ts.map b/dist/components/Bot.d.ts.map index 37b93d3..9f5c260 100644 --- a/dist/components/Bot.d.ts.map +++ b/dist/components/Bot.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"Bot.d.ts","sourceRoot":"","sources":["../../src/components/Bot.tsx"],"names":[],"mappings":"AAEA,OAAO,EAML,kBAAkB,EAEnB,MAAM,4BAA4B,CAAC;AAMpC,OAAO,EACL,eAAe,EACf,WAAW,EACX,cAAc,EACd,gBAAgB,EAChB,aAAa,EACb,oBAAoB,EACpB,mBAAmB,EACpB,MAAM,yBAAyB,CAAC;AAKjC,OAAO,EAAE,WAAW,EAAE,MAAM,sDAAsD,CAAC;AAUnF,MAAM,MAAM,SAAS,CAAC,CAAC,GAAG,WAAW,IAAI;IACvC,MAAM,EAAE,CAAC,CAAC;CACX,CAAC;AAEF,MAAM,MAAM,SAAS,CAAC,CAAC,GAAG,WAAW,IAAI;IACvC,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,aAAa,EAAE,CAAC,CAAC;CAClB,CAAC;AAEF,KAAK,iBAAiB,GAAG;IACvB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,qBAAqB,EAAE,iBAAiB,EAAE,CAAC;IAC3C,sBAAsB,EAAE,iBAAiB,EAAE,CAAC;IAC5C,oBAAoB,EAAE,OAAO,CAAC;IAC9B,qBAAqB,EAAE,OAAO,CAAC;IAC/B,sBAAsB,EAAE,OAAO,CAAC;CACjC,CAAC;AAEF,KAAK,eAAe,GAAG,MAAM,GAAG,WAAW,CAAC;AAE5C,KAAK,WAAW,GAAG;IACjB,IAAI,EAAE,eAAe,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,KAAK,WAAW,GAAG,YAAY,GAAG,aAAa,GAAG,oBAAoB,GAAG,oBAAoB,CAAC;AAC9F,KAAK,cAAc,GAAG,YAAY,GAAG,UAAU,GAAG,OAAO,GAAG,YAAY,GAAG,SAAS,GAAG,SAAS,CAAC;AAEjG,MAAM,MAAM,eAAe,GAAG;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,SAAS,CAAC,EAAE,GAAG,EAAE,CAAC;IAClB,SAAS,CAAC,EAAE,UAAU,EAAE,CAAC;IACzB,eAAe,CAAC,EAAE,GAAG,EAAE,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,OAAO,GAAG;IACpB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,QAAQ,CAAC,EAAE,KAAK,CAAC;QACf,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;KACf,CAAC,CAAC;IACH,OAAO,CAAC,EAAE;QACR,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,GAAG,EAAE,CAAC;KAClB,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,IAAI,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;AAEtD,MAAM,MAAM,qBAAqB,GAAG;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,GAAG,CAAC;IACV,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,MAAM,CAAC,EAAE,cAAc,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,WAAW,CAAC;IAClB,eAAe,CAAC,EAAE,GAAG,CAAC;IACtB,eAAe,CAAC,EAAE,GAAG,CAAC;IACtB,WAAW,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC;IACpC,SAAS,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC;IAClC,cAAc,CAAC,EAAE,eAAe,EAAE,CAAC;IACnC,SAAS,CAAC,EAAE,GAAG,CAAC;IAChB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,qBAAqB,CAAC,EAAE,GAAG,CAAC;IAC5B,SAAS,CAAC,EAAE,GAAG,EAAE,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IACxB,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AASF,KAAK,kBAAkB,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,WAAW,EAAE,KAAK,IAAI,CAAC;AACxF,MAAM,MAAM,mBAAmB,GAAG,MAAM,CAAC,kBAAkB,GAAG,gBAAgB,GAAG,iBAAiB,EAAE,kBAAkB,CAAC,CAAC;AAExH,MAAM,MAAM,QAAQ,GAAG;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACzC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,eAAe,CAAC;IAC7B,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAC/B,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,eAAe,CAAC,EAAE,mBAAmB,CAAC;IACtC,cAAc,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/D,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,UAAU,CAAC,EAAE,oBAAoB,CAAC;IAClC,cAAc,CAAC,EAAE,mBAAmB,CAAC;IACrC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AA+QF,eAAO,MAAM,GAAG,aAAc,QAAQ,GAAG;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,mCAyjD1D,CAAC"} \ No newline at end of file +{"version":3,"file":"Bot.d.ts","sourceRoot":"","sources":["../../src/components/Bot.tsx"],"names":[],"mappings":"AAEA,OAAO,EAML,kBAAkB,EAEnB,MAAM,4BAA4B,CAAC;AAMpC,OAAO,EACL,eAAe,EACf,WAAW,EACX,cAAc,EACd,gBAAgB,EAChB,aAAa,EACb,oBAAoB,EACpB,mBAAmB,EACpB,MAAM,yBAAyB,CAAC;AAKjC,OAAO,EAAE,WAAW,EAAE,MAAM,sDAAsD,CAAC;AAWnF,MAAM,MAAM,SAAS,CAAC,CAAC,GAAG,WAAW,IAAI;IACvC,MAAM,EAAE,CAAC,CAAC;CACX,CAAC;AAEF,MAAM,MAAM,SAAS,CAAC,CAAC,GAAG,WAAW,IAAI;IACvC,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,aAAa,EAAE,CAAC,CAAC;CAClB,CAAC;AAEF,KAAK,iBAAiB,GAAG;IACvB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,qBAAqB,EAAE,iBAAiB,EAAE,CAAC;IAC3C,sBAAsB,EAAE,iBAAiB,EAAE,CAAC;IAC5C,oBAAoB,EAAE,OAAO,CAAC;IAC9B,qBAAqB,EAAE,OAAO,CAAC;IAC/B,sBAAsB,EAAE,OAAO,CAAC;CACjC,CAAC;AAEF,KAAK,eAAe,GAAG,MAAM,GAAG,WAAW,CAAC;AAE5C,KAAK,WAAW,GAAG;IACjB,IAAI,EAAE,eAAe,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,KAAK,WAAW,GAAG,YAAY,GAAG,aAAa,GAAG,oBAAoB,GAAG,oBAAoB,CAAC;AAC9F,KAAK,cAAc,GAAG,YAAY,GAAG,UAAU,GAAG,OAAO,GAAG,YAAY,GAAG,SAAS,GAAG,SAAS,CAAC;AAEjG,MAAM,MAAM,eAAe,GAAG;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,SAAS,CAAC,EAAE,GAAG,EAAE,CAAC;IAClB,SAAS,CAAC,EAAE,UAAU,EAAE,CAAC;IACzB,eAAe,CAAC,EAAE,GAAG,EAAE,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,OAAO,GAAG;IACpB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,QAAQ,CAAC,EAAE,KAAK,CAAC;QACf,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;KACf,CAAC,CAAC;IACH,OAAO,CAAC,EAAE;QACR,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,GAAG,EAAE,CAAC;KAClB,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,IAAI,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;AAEtD,MAAM,MAAM,qBAAqB,GAAG;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,GAAG,CAAC;IACV,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,MAAM,CAAC,EAAE,cAAc,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,WAAW,CAAC;IAClB,eAAe,CAAC,EAAE,GAAG,CAAC;IACtB,eAAe,CAAC,EAAE,GAAG,CAAC;IACtB,WAAW,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC;IACpC,SAAS,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC;IAClC,cAAc,CAAC,EAAE,eAAe,EAAE,CAAC;IACnC,SAAS,CAAC,EAAE,GAAG,CAAC;IAChB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,qBAAqB,CAAC,EAAE,GAAG,CAAC;IAC5B,SAAS,CAAC,EAAE,GAAG,EAAE,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IACxB,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AASF,KAAK,kBAAkB,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,WAAW,EAAE,KAAK,IAAI,CAAC;AACxF,MAAM,MAAM,mBAAmB,GAAG,MAAM,CAAC,kBAAkB,GAAG,gBAAgB,GAAG,iBAAiB,EAAE,kBAAkB,CAAC,CAAC;AAExH,MAAM,MAAM,QAAQ,GAAG;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,eAAe,CAAC;IAC7B,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAC/B,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,eAAe,CAAC,EAAE,mBAAmB,CAAC;IACtC,cAAc,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/D,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,UAAU,CAAC,EAAE,oBAAoB,CAAC;IAClC,cAAc,CAAC,EAAE,mBAAmB,CAAC;IACrC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AA+QF,eAAO,MAAM,GAAG,aAAc,QAAQ,GAAG;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,mCAomD1D,CAAC"} \ No newline at end of file diff --git a/dist/components/bubbles/AgentReasoningBubble.d.ts b/dist/components/bubbles/AgentReasoningBubble.d.ts index eb97a5f..1ec0ef0 100644 --- a/dist/components/bubbles/AgentReasoningBubble.d.ts +++ b/dist/components/bubbles/AgentReasoningBubble.d.ts @@ -1,7 +1,7 @@ import { FileUpload } from '../Bot'; type Props = { apiHost?: string; - chatflowid: string; + chatbot: string; chatId: string; agentName: string; agentMessage: string; diff --git a/dist/components/bubbles/AgentReasoningBubble.d.ts.map b/dist/components/bubbles/AgentReasoningBubble.d.ts.map index 4696154..3738ef0 100644 --- a/dist/components/bubbles/AgentReasoningBubble.d.ts.map +++ b/dist/components/bubbles/AgentReasoningBubble.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"AgentReasoningBubble.d.ts","sourceRoot":"","sources":["../../../src/components/bubbles/AgentReasoningBubble.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAGpC,KAAK,KAAK,GAAG;IACX,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,UAAU,EAAE,CAAC;IAC9B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AAMF,eAAO,MAAM,oBAAoB,UAAW,KAAK,mCAoFhD,CAAC"} \ No newline at end of file +{"version":3,"file":"AgentReasoningBubble.d.ts","sourceRoot":"","sources":["../../../src/components/bubbles/AgentReasoningBubble.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAGpC,KAAK,KAAK,GAAG;IACX,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,UAAU,EAAE,CAAC;IAC9B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AAMF,eAAO,MAAM,oBAAoB,UAAW,KAAK,mCAoFhD,CAAC"} \ No newline at end of file diff --git a/dist/components/bubbles/BotBubble.d.ts b/dist/components/bubbles/BotBubble.d.ts index 2dbac1f..1129450 100644 --- a/dist/components/bubbles/BotBubble.d.ts +++ b/dist/components/bubbles/BotBubble.d.ts @@ -2,7 +2,7 @@ import { IAction, MessageType } from '../Bot'; import { DateTimeToggleTheme } from '@/features/bubble/types'; type Props = { message: MessageType; - chatflowid: string; + chatbot: string; chatId: string; apiHost?: string; onRequest?: (request: RequestInit) => Promise; diff --git a/dist/components/bubbles/BotBubble.d.ts.map b/dist/components/bubbles/BotBubble.d.ts.map index 01a96f1..e8d89ec 100644 --- a/dist/components/bubbles/BotBubble.d.ts.map +++ b/dist/components/bubbles/BotBubble.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"BotBubble.d.ts","sourceRoot":"","sources":["../../../src/components/bubbles/BotBubble.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAc,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAM1D,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAG9D,KAAK,KAAK,GAAG;IACX,OAAO,EAAE,WAAW,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,eAAe,CAAC,EAAE,GAAG,CAAC;IACtB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,OAAO,CAAC;IACnB,cAAc,CAAC,EAAE,mBAAmB,CAAC;IACrC,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,iBAAiB,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,GAAG,SAAS,GAAG,IAAI,KAAK,IAAI,CAAC;IAC3E,0BAA0B,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAC;CAChD,CAAC;AAOF,eAAO,MAAM,SAAS,UAAW,KAAK,mCA0gBrC,CAAC"} \ No newline at end of file +{"version":3,"file":"BotBubble.d.ts","sourceRoot":"","sources":["../../../src/components/bubbles/BotBubble.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAc,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAM1D,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAG9D,KAAK,KAAK,GAAG;IACX,OAAO,EAAE,WAAW,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,eAAe,CAAC,EAAE,GAAG,CAAC;IACtB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,OAAO,CAAC;IACnB,cAAc,CAAC,EAAE,mBAAmB,CAAC;IACrC,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,iBAAiB,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,GAAG,SAAS,GAAG,IAAI,KAAK,IAAI,CAAC;IAC3E,0BAA0B,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAC;CAChD,CAAC;AAOF,eAAO,MAAM,SAAS,UAAW,KAAK,mCA2gBrC,CAAC"} \ No newline at end of file diff --git a/dist/components/bubbles/GuestBubble.d.ts b/dist/components/bubbles/GuestBubble.d.ts index 3cad860..36b516f 100644 --- a/dist/components/bubbles/GuestBubble.d.ts +++ b/dist/components/bubbles/GuestBubble.d.ts @@ -2,7 +2,7 @@ import { MessageType } from '../Bot'; type Props = { message: MessageType; apiHost?: string; - chatflowid: string; + chatbot: string; chatId: string; showAvatar?: boolean; avatarSrc?: string; diff --git a/dist/components/bubbles/GuestBubble.d.ts.map b/dist/components/bubbles/GuestBubble.d.ts.map index 052abe8..8cbc1c2 100644 --- a/dist/components/bubbles/GuestBubble.d.ts.map +++ b/dist/components/bubbles/GuestBubble.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"GuestBubble.d.ts","sourceRoot":"","sources":["../../../src/components/bubbles/GuestBubble.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAc,WAAW,EAAE,MAAM,QAAQ,CAAC;AAGjD,KAAK,KAAK,GAAG;IACX,OAAO,EAAE,WAAW,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AAMF,eAAO,MAAM,WAAW,UAAW,KAAK,mCAgGvC,CAAC"} \ No newline at end of file +{"version":3,"file":"GuestBubble.d.ts","sourceRoot":"","sources":["../../../src/components/bubbles/GuestBubble.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAc,WAAW,EAAE,MAAM,QAAQ,CAAC;AAGjD,KAAK,KAAK,GAAG;IACX,OAAO,EAAE,WAAW,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AAMF,eAAO,MAAM,WAAW,UAAW,KAAK,mCAgGvC,CAAC"} \ No newline at end of file diff --git a/dist/components/bubbles/LeadCaptureBubble.d.ts b/dist/components/bubbles/LeadCaptureBubble.d.ts index 05e8b8d..abbb6dc 100644 --- a/dist/components/bubbles/LeadCaptureBubble.d.ts +++ b/dist/components/bubbles/LeadCaptureBubble.d.ts @@ -1,7 +1,7 @@ import { LeadsConfig, MessageType } from '@/components/Bot'; type Props = { message: MessageType; - chatflowid: string; + chatbot: string; chatId: string; leadsConfig?: LeadsConfig; apiHost?: string; diff --git a/dist/components/bubbles/LeadCaptureBubble.d.ts.map b/dist/components/bubbles/LeadCaptureBubble.d.ts.map index e50d482..e06f518 100644 --- a/dist/components/bubbles/LeadCaptureBubble.d.ts.map +++ b/dist/components/bubbles/LeadCaptureBubble.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"LeadCaptureBubble.d.ts","sourceRoot":"","sources":["../../../src/components/bubbles/LeadCaptureBubble.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAa,WAAW,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAMvE,KAAK,KAAK,GAAG;IACX,OAAO,EAAE,WAAW,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,OAAO,CAAC;IACrB,cAAc,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACzC,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACvC,CAAC;AAaF,eAAO,MAAM,iBAAiB,UAAW,KAAK,mCAgI7C,CAAC"} \ No newline at end of file +{"version":3,"file":"LeadCaptureBubble.d.ts","sourceRoot":"","sources":["../../../src/components/bubbles/LeadCaptureBubble.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAa,WAAW,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAMvE,KAAK,KAAK,GAAG;IACX,OAAO,EAAE,WAAW,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,OAAO,CAAC;IACrB,cAAc,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACzC,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACvC,CAAC;AAaF,eAAO,MAAM,iBAAiB,UAAW,KAAK,mCAgI7C,CAAC"} \ No newline at end of file diff --git a/dist/gemini.htm b/dist/gemini.htm new file mode 100755 index 0000000..ff2cd03 --- /dev/null +++ b/dist/gemini.htm @@ -0,0 +1,151 @@ + + + + + + +OpenAI Test! + + + +
+ + +
+ + + + + diff --git a/dist/queries/sendMessageQuery.d.ts b/dist/queries/sendMessageQuery.d.ts index 4cc5ab3..84b5696 100644 --- a/dist/queries/sendMessageQuery.d.ts +++ b/dist/queries/sendMessageQuery.d.ts @@ -16,7 +16,7 @@ type BaseRequest = { onRequest?: (request: RequestInit) => Promise; }; export type MessageRequest = BaseRequest & { - chatflowid?: string; + chatbot?: string; body?: IncomingInput; }; export type FeedbackRatingType = 'THUMBS_UP' | 'THUMBS_DOWN'; @@ -27,7 +27,7 @@ export type FeedbackInput = { content?: string; }; export type CreateFeedbackRequest = BaseRequest & { - chatflowid?: string; + chatbot?: string; body?: FeedbackInput; }; export type UpdateFeedbackRequest = BaseRequest & { @@ -35,12 +35,12 @@ export type UpdateFeedbackRequest = BaseRequest & { body?: Partial; }; export type UpsertRequest = BaseRequest & { - chatflowid: string; + chatbot: string; apiHost?: string; formData: FormData; }; export type LeadCaptureInput = { - chatflowid: string; + chatbot: string; chatId: string; name?: string; email?: string; @@ -49,7 +49,7 @@ export type LeadCaptureInput = { export type LeadCaptureRequest = BaseRequest & { body: Partial; }; -export declare const sendFeedbackQuery: ({ chatflowid, apiHost, body, onRequest }: CreateFeedbackRequest) => Promise<{ +export declare const sendFeedbackQuery: ({ chatbot, apiHost, body, onRequest }: CreateFeedbackRequest) => Promise<{ data?: unknown; error?: Error | undefined; }>; @@ -57,23 +57,23 @@ export declare const updateFeedbackQuery: ({ id, apiHost, body, onRequest }: Upd data?: unknown; error?: Error | undefined; }>; -export declare const sendMessageQuery: ({ chatflowid, apiHost, body, onRequest }: MessageRequest) => Promise<{ +export declare const sendMessageQuery: ({ chatbot, apiHost, body, onRequest }: MessageRequest) => Promise<{ data?: any; error?: Error | undefined; }>; -export declare const createAttachmentWithFormData: ({ chatflowid, apiHost, formData, onRequest }: UpsertRequest) => Promise<{ +export declare const createAttachmentWithFormData: ({ chatbot, apiHost, formData, onRequest }: UpsertRequest) => Promise<{ data?: unknown; error?: Error | undefined; }>; -export declare const upsertVectorStoreWithFormData: ({ chatflowid, apiHost, formData, onRequest }: UpsertRequest) => Promise<{ +export declare const upsertVectorStoreWithFormData: ({ chatbot, apiHost, formData, onRequest }: UpsertRequest) => Promise<{ data?: unknown; error?: Error | undefined; }>; -export declare const getChatbotConfig: ({ chatflowid, apiHost, onRequest }: MessageRequest) => Promise<{ +export declare const getChatbotConfig: ({ chatbot, apiHost, onRequest }: MessageRequest) => Promise<{ data?: any; error?: Error | undefined; }>; -export declare const isStreamAvailableQuery: ({ chatflowid, apiHost, onRequest }: MessageRequest) => Promise<{ +export declare const isStreamAvailableQuery: ({ chatbot, apiHost, onRequest }: MessageRequest) => Promise<{ data?: any; error?: Error | undefined; }>; diff --git a/dist/queries/sendMessageQuery.d.ts.map b/dist/queries/sendMessageQuery.d.ts.map index cee85e7..f78ae25 100644 --- a/dist/queries/sendMessageQuery.d.ts.map +++ b/dist/queries/sendMessageQuery.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"sendMessageQuery.d.ts","sourceRoot":"","sources":["../../src/queries/sendMessageQuery.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAGvD,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,OAAO,CAAC,EAAE,UAAU,EAAE,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACzC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC,CAAC;AAEF,KAAK,WAAW,GAAG;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACrD,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG,WAAW,GAAG;IACzC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,aAAa,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,aAAa,CAAC;AAE7D,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG,WAAW,GAAG;IAChD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,aAAa,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG,WAAW,GAAG;IAChD,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG,WAAW,GAAG;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,QAAQ,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG;IAC7C,IAAI,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAC;CACjC,CAAC;AAEF,eAAO,MAAM,iBAAiB,6CAAwE,qBAAqB;;;EAMvH,CAAC;AAEL,eAAO,MAAM,mBAAmB,qCAAgE,qBAAqB;;;EAMjH,CAAC;AAEL,eAAO,MAAM,gBAAgB,6CAAwE,cAAc;;;EAM/G,CAAC;AAEL,eAAO,MAAM,4BAA4B,iDAA4E,aAAa;;;EAS9H,CAAC;AAEL,eAAO,MAAM,6BAA6B,iDAA4E,aAAa;;;EAS/H,CAAC;AAEL,eAAO,MAAM,gBAAgB,uCAAkE,cAAc;;;EAKzG,CAAC;AAEL,eAAO,MAAM,sBAAsB,uCAAkE,cAAc;;;EAK/G,CAAC;AAEL,eAAO,MAAM,qBAAqB,iCAA4D,cAAc;;;EAOxG,CAAC;AAEL,eAAO,MAAM,YAAY,iCAA4D,kBAAkB;;;EAMnG,CAAC"} \ No newline at end of file +{"version":3,"file":"sendMessageQuery.d.ts","sourceRoot":"","sources":["../../src/queries/sendMessageQuery.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAGvD,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,OAAO,CAAC,EAAE,UAAU,EAAE,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACzC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC,CAAC;AAEF,KAAK,WAAW,GAAG;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACrD,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG,WAAW,GAAG;IACzC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,aAAa,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,aAAa,CAAC;AAE7D,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG,WAAW,GAAG;IAChD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,aAAa,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG,WAAW,GAAG;IAChD,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG,WAAW,GAAG;IACxC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,QAAQ,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG;IAC7C,IAAI,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAC;CACjC,CAAC;AAEF,eAAO,MAAM,iBAAiB,0CAAqE,qBAAqB;;;EAMpH,CAAC;AAEL,eAAO,MAAM,mBAAmB,qCAAgE,qBAAqB;;;EAMjH,CAAC;AAEL,eAAO,MAAM,gBAAgB,0CAAqE,cAAc;;;EAM5G,CAAC;AAEL,eAAO,MAAM,4BAA4B,8CAAyE,aAAa;;;EAS3H,CAAC;AAEL,eAAO,MAAM,6BAA6B,8CAAyE,aAAa;;;EAS5H,CAAC;AAEL,eAAO,MAAM,gBAAgB,oCAA+D,cAAc;;;EAKtG,CAAC;AAEL,eAAO,MAAM,sBAAsB,oCAA+D,cAAc;;;EAK5G,CAAC;AAEL,eAAO,MAAM,qBAAqB,iCAA4D,cAAc;;;EAOxG,CAAC;AAEL,eAAO,MAAM,YAAY,iCAA4D,kBAAkB;;;EAMnG,CAAC"} \ No newline at end of file diff --git a/dist/test.htm b/dist/test.htm new file mode 100755 index 0000000..323d61b --- /dev/null +++ b/dist/test.htm @@ -0,0 +1,158 @@ + + + + + + +OpenAI Test! + + + +
+ + +
+ + + + + diff --git a/dist/trolydieuhanh.htm b/dist/trolydieuhanh.htm new file mode 100755 index 0000000..9a75474 --- /dev/null +++ b/dist/trolydieuhanh.htm @@ -0,0 +1,151 @@ + + + + + + +OpenAI Test! + + + +
+ + +
+ + + + + diff --git a/dist/utils/index.d.ts b/dist/utils/index.d.ts index 2e3b93d..da3f797 100644 --- a/dist/utils/index.d.ts +++ b/dist/utils/index.d.ts @@ -14,9 +14,9 @@ export declare const sendRequest: (params: string | { data?: ResponseData | undefined; error?: Error | undefined; }>; -export declare const setLocalStorageChatflow: (chatflowid: string, chatId: string, saveObj?: Record) => void; -export declare const getLocalStorageChatflow: (chatflowid: string) => any; -export declare const removeLocalStorageChatHistory: (chatflowid: string) => void; +export declare const setLocalStorageChatflow: (chatbot: string, chatId: string, saveObj?: Record) => void; +export declare const getLocalStorageChatflow: (chatbot: string) => any; +export declare const removeLocalStorageChatHistory: (chatbot: string) => void; export declare const getBubbleButtonSize: (size: 'small' | 'medium' | 'large' | number | undefined) => number; export declare const setCookie: (cname: string, cvalue: string, exdays: number) => void; export declare const getCookie: (cname: string) => string; diff --git a/dist/utils/index.d.ts.map b/dist/utils/index.d.ts.map index 4c8d47a..92c56f5 100644 --- a/dist/utils/index.d.ts.map +++ b/dist/utils/index.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,YAAY,+DAAuG,CAAC;AAEjI,eAAO,MAAM,SAAS,6DAAqG,CAAC;AAE5H,eAAO,MAAM,OAAO,UAAW,MAAM,GAAG,SAAS,GAAG,IAAI,uBAA8E,CAAC;AAEvI,eAAO,MAAM,UAAU,UAAW,MAAM,GAAG,SAAS,GAAG,IAAI,oBAA2E,CAAC;AAEvI,eAAO,MAAM,WAAW;SAGX,MAAM;YACH,MAAM;;;;;2BAKQ,WAAW,KAAK,QAAQ,IAAI,CAAC;;;;EAuD1D,CAAC;AAEF,eAAO,MAAM,uBAAuB,eAAgB,MAAM,UAAU,MAAM,YAAW,OAAO,MAAM,EAAE,GAAG,CAAC,SAiBvG,CAAC;AAEF,eAAO,MAAM,uBAAuB,eAAgB,MAAM,QAQzD,CAAC;AAEF,eAAO,MAAM,6BAA6B,eAAgB,MAAM,SAgB/D,CAAC;AAEF,eAAO,MAAM,mBAAmB,SAAU,OAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,WAO1F,CAAC;AAEF,eAAO,MAAM,SAAS,UAAW,MAAM,UAAU,MAAM,UAAU,MAAM,SAKtE,CAAC;AAEF,eAAO,MAAM,SAAS,UAAW,MAAM,KAAG,MAczC,CAAC"} \ No newline at end of file +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,YAAY,+DAAuG,CAAC;AAEjI,eAAO,MAAM,SAAS,6DAAqG,CAAC;AAE5H,eAAO,MAAM,OAAO,UAAW,MAAM,GAAG,SAAS,GAAG,IAAI,uBAA8E,CAAC;AAEvI,eAAO,MAAM,UAAU,UAAW,MAAM,GAAG,SAAS,GAAG,IAAI,oBAA2E,CAAC;AAEvI,eAAO,MAAM,WAAW;SAGX,MAAM;YACH,MAAM;;;;;2BAKQ,WAAW,KAAK,QAAQ,IAAI,CAAC;;;;EAuD1D,CAAC;AAEF,eAAO,MAAM,uBAAuB,YAAa,MAAM,UAAU,MAAM,YAAW,OAAO,MAAM,EAAE,GAAG,CAAC,SAiBpG,CAAC;AAEF,eAAO,MAAM,uBAAuB,YAAa,MAAM,QAQtD,CAAC;AAEF,eAAO,MAAM,6BAA6B,YAAa,MAAM,SAgB5D,CAAC;AAEF,eAAO,MAAM,mBAAmB,SAAU,OAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,WAO1F,CAAC;AAEF,eAAO,MAAM,SAAS,UAAW,MAAM,UAAU,MAAM,UAAU,MAAM,SAKtE,CAAC;AAEF,eAAO,MAAM,SAAS,UAAW,MAAM,KAAG,MAczC,CAAC"} \ No newline at end of file diff --git a/dist/web.d.ts b/dist/web.d.ts index a395dac..309be3b 100644 --- a/dist/web.d.ts +++ b/dist/web.d.ts @@ -1,19 +1,19 @@ declare const chatbot: { initFull: (props: { - chatflowid: string; + chatbot: string; apiHost?: string | undefined; onRequest?: ((request: RequestInit) => Promise) | undefined; - chatflowConfig?: Record | undefined; + config?: Record | undefined; observersConfig?: import("./components/Bot").observersConfigType | undefined; theme?: import("./features/bubble/types").BubbleTheme | undefined; } & { id?: string | undefined; }) => void; init: (props: { - chatflowid: string; + chatbot: string; apiHost?: string | undefined; onRequest?: ((request: RequestInit) => Promise) | undefined; - chatflowConfig?: Record | undefined; + config?: Record | undefined; observersConfig?: import("./components/Bot").observersConfigType | undefined; theme?: import("./features/bubble/types").BubbleTheme | undefined; }) => void; diff --git a/dist/web.js b/dist/web.js index 5930a19..28dccb7 100644 --- a/dist/web.js +++ b/dist/web.js @@ -1 +1 @@ -function e(e){return Object.keys(e).reduce(((r,o)=>{var a=e[o];return r[o]=Object.assign({},a),!t(a.value)||function(e){return"[object Function]"===Object.prototype.toString.call(e)}(a.value)||Array.isArray(a.value)||(r[o].value=Object.assign({},a.value)),Array.isArray(a.value)&&(r[o].value=a.value.slice(0)),r}),{})}function r(e){if(e)try{return JSON.parse(e)}catch(r){return e}}function o(e,r,o){if(null==o||!1===o)return e.removeAttribute(r);let t=JSON.stringify(o);e.__updating[r]=!0,"true"===t&&(t=""),e.setAttribute(r,t),Promise.resolve().then((()=>delete e.__updating[r]))}function t(e){return null!=e&&("object"==typeof e||"function"==typeof e)}let a;function l(t,l){const i=Object.keys(l);return class extends t{static get observedAttributes(){return i.map((e=>l[e].attribute))}constructor(){super(),this.__initialized=!1,this.__released=!1,this.__releaseCallbacks=[],this.__propertyChangedCallbacks=[],this.__updating={},this.props={}}connectedCallback(){if(!this.__initialized){this.__releaseCallbacks=[],this.__propertyChangedCallbacks=[],this.__updating={},this.props=function(t,a){const l=e(a);return Object.keys(a).forEach((e=>{const a=l[e],i=t.getAttribute(a.attribute),n=t[e];i&&(a.value=a.parse?r(i):i),null!=n&&(a.value=Array.isArray(n)?n.slice(0):n),a.reflect&&o(t,a.attribute,a.value),Object.defineProperty(t,e,{get:()=>a.value,set(r){var t=a.value;a.value=r,a.reflect&&o(this,a.attribute,a.value);for(let o=0,a=this.__propertyChangedCallbacks.length;o(r[o]=e[o].value,r)),{})}(this.props),i=this.Component,n=a;try{(a=this).__initialized=!0,function(e){return"function"==typeof e&&0===e.toString().indexOf("class")}(i)?new i(t,{element:this}):i(t,{element:this})}finally{a=n}}}async disconnectedCallback(){if(await Promise.resolve(),!this.isConnected){this.__propertyChangedCallbacks.length=0;for(var e=null;e=this.__releaseCallbacks.pop();)e(this);delete this.__initialized,this.__released=!0}}attributeChangedCallback(e,o,t){!this.__initialized||this.__updating[e]||(e=this.lookupProp(e))in l&&(null==t&&!this[e]||(this[e]=l[e].parse?r(t):t))}lookupProp(e){if(l)return i.find((r=>e===r||e===l[r].attribute))}get renderRoot(){return this.shadowRoot||this.attachShadow({mode:"open"})}addReleaseCallback(e){this.__releaseCallbacks.push(e)}addPropertyChangedCallback(e){this.__propertyChangedCallbacks.push(e)}}}function i(e,r={},o={}){const{BaseElement:a=HTMLElement,extension:i}=o;return o=>{if(!e)throw new Error("tag is required to register a Component");let n=customElements.get(e);return n?n.prototype.Component=o:((n=l(a,function(e){return e?Object.keys(e).reduce(((r,o)=>{var a=e[o];return r[o]=t(a)&&"value"in a?a:{value:a},r[o].attribute||(r[o].attribute=function(e){return e.replace(/\.?([A-Z]+)/g,((e,r)=>"-"+r.toLowerCase())).replace("_","-").replace(/^-/,"")}(o)),r[o].parse="parse"in r[o]?r[o].parse:"string"!=typeof r[o].value,r}),{}):{}}(r))).prototype.Component=o,n.prototype.registeredTag=e,customElements.define(e,n,i)),n}}const n=Symbol("solid-proxy"),d=Symbol("solid-track"),s=Symbol("solid-dev-component"),m={equals:(e,r)=>e===r};let g=X;const x=1,c=2,u={owned:null,cleanups:null,context:null,owner:null};var p=null;let h=null,S=null,v=null,$=null,A=0;function b(e,r){const o=S,t=p,a=0===e.length,l=a?u:{owned:null,cleanups:null,context:null,owner:void 0===r?t:r},i=a?e:()=>e((()=>_((()=>V(l)))));p=l,S=null;try{return k(i,!0)}finally{S=o,p=t}}function M(e,r){const o={value:e,observers:null,observerSlots:null,comparator:(r=r?Object.assign({},m,r):m).equals||void 0};return[B.bind(o),e=>("function"==typeof e&&(e=e(o.value)),E(o,e))]}function P(e,r,o){N(O(e,r,!1,x))}function f(e,r,o){g=D,(e=O(e,r,!1,x)).user=!0,$?$.push(e):N(e)}function T(e,r,o){return o=o?Object.assign({},m,o):m,(e=O(e,r,!0,0)).observers=null,e.observerSlots=null,e.comparator=o.equals||void 0,N(e),B.bind(e)}function _(e){if(null===S)return e();var r=S;S=null;try{return e()}finally{S=r}}function y(e){f((()=>_(e)))}function w(e){return null!==p&&(null===p.cleanups?p.cleanups=[e]:p.cleanups.push(e)),e}function G(){return S}function L(e){var r;return void 0!==(r=U(p,e.id))?r:e.defaultValue}function C(e){const r=T(e),o=T((()=>K(r())));return o.toArray=()=>{var e=o();return Array.isArray(e)?e:null!=e?[e]:[]},o}function B(){var e;return this.sources&&this.state&&(this.state===x?N(this):(e=v,v=null,k((()=>R(this)),!1),v=e)),S&&(e=this.observers?this.observers.length:0,S.sources?(S.sources.push(this),S.sourceSlots.push(e)):(S.sources=[this],S.sourceSlots=[e]),this.observers?(this.observers.push(S),this.observerSlots.push(S.sources.length-1)):(this.observers=[S],this.observerSlots=[S.sources.length-1])),this.value}function E(e,r,o){var t=e.value;return e.comparator&&e.comparator(t,r)||(e.value=r,e.observers&&e.observers.length&&k((()=>{for(let t=0;tR(e,o[0])),!1),v=r)}}}function k(e,r){if(v)return e();let o=!1;r||(v=[]),$?o=!0:$=[],A++;try{var t=e();return function(e){if(v&&(X(v),v=null),!e){const e=$;$=null,e.length&&k((()=>g(e)),!1)}}(o),t}catch(e){o||($=null),v=null,H(e)}}function X(e){for(let r=0;ro=_((()=>(p.context={[e]:r.value},C((()=>r.children)))))),void 0),o}}const Z=Symbol("fallback");function Q(e){for(let r=0;re(r||{})))}function Y(){return!0}const J={get:(e,r,o)=>r===n?o:e.get(r),has:(e,r)=>r===n||e.has(r),set:Y,deleteProperty:Y,getOwnPropertyDescriptor:(e,r)=>({configurable:!0,enumerable:!0,get:()=>e.get(r),set:Y,deleteProperty:Y}),ownKeys:e=>e.keys()};function j(e){return(e="function"==typeof e?e():e)||{}}function q(...e){let r=!1;for(let t=0;tnew Proxy({get:o=>r.includes(o)?e[o]:void 0,has:o=>r.includes(o)&&o in e,keys:()=>r.filter((r=>r in e))},J)))).push(new Proxy({get:r=>o.has(r)?void 0:e[r],has:r=>!o.has(r)&&r in e,keys:()=>Object.keys(e).filter((e=>!o.has(e)))},J)),t;const a=Object.getOwnPropertyDescriptors(e);return r.push(Object.keys(a).filter((e=>!o.has(e)))),r.map((r=>{var o={};for(let t=0;te[l],set:()=>!0,enumerable:!0})}return o}))}function re(e){var r="fallback"in e&&{fallback:()=>e.fallback};return T(function(e,r,o={}){let t=[],a=[],l=[],i=0,n=1Q(l))),()=>{let s,m,g=e()||[];return g[d],_((()=>{let e,r,d,c,u,p,h,S,v,$=g.length;if(0===$)0!==i&&(Q(l),l=[],t=[],a=[],i=0,n=n&&[]),o.fallback&&(t=[Z],a[0]=b((e=>(l[0]=e,o.fallback()))),i=1);else if(0===i){for(a=new Array($),m=0;m<$;m++)t[m]=g[m],a[m]=b(x);i=$}else{for(d=new Array($),c=new Array($),n&&(u=new Array($)),p=0,h=Math.min(i,$);p=p&&S>=p&&t[h]===g[S];h--,S--)d[S]=a[h],c[S]=l[h],n&&(u[S]=n[h]);for(e=new Map,r=new Array(S+1),m=S;m>=p;m--)v=g[m],s=e.get(v),r[m]=void 0===s?-1:s,e.set(v,m);for(s=p;s<=h;s++)v=t[s],void 0!==(m=e.get(v))&&-1!==m?(d[m]=a[s],c[m]=l[s],n&&(u[m]=n[s]),m=r[m],e.set(v,m)):l[s]();for(m=p;m<$;m++)m in d?(a[m]=d[m],l[m]=c[m],n&&(n[m]=u[m],n[m](m))):a[m]=b(x);a=a.slice(0,i=$),t=g.slice(0)}return a}));function x(e){var o;return l[m]=e,n?([e,o]=M(m),n[m]=o,r(g[m],e)):r(g[m])}}}((()=>e.each),e.children,r||void 0))}function oe(e){const r=e.keyed,o=T((()=>e.when),void 0,{equals:(e,o)=>r?e===o:!e==!o});return T((()=>{const t=o();if(t){const a=e.children;return"function"==typeof a&&0a(r?t:()=>{if(_(o))return e.when;throw(e=>`Stale read from <${e}>.`)("Show")}))):a}return e.fallback}),void 0,void 0)}const te=new Set(["className","value","readOnly","formNoValidate","isMap","noModule","playsInline","allowfullscreen","async","autofocus","autoplay","checked","controls","default","disabled","formnovalidate","hidden","indeterminate","ismap","loop","multiple","muted","nomodule","novalidate","open","playsinline","readonly","required","reversed","seamless","selected"]),ae=new Set(["innerHTML","textContent","innerText","children"]),le=Object.assign(Object.create(null),{className:"class",htmlFor:"for"}),ie=Object.assign(Object.create(null),{class:"className",formnovalidate:{$:"formNoValidate",BUTTON:1,INPUT:1},ismap:{$:"isMap",IMG:1},nomodule:{$:"noModule",SCRIPT:1},playsinline:{$:"playsInline",VIDEO:1},readonly:{$:"readOnly",INPUT:1,TEXTAREA:1}});const ne=new Set(["beforeinput","click","dblclick","contextmenu","focusin","focusout","input","keydown","keyup","mousedown","mousemove","mouseout","mouseover","mouseup","pointerdown","pointermove","pointerout","pointerover","pointerup","touchend","touchmove","touchstart"]),de=new Set(["altGlyph","altGlyphDef","altGlyphItem","animate","animateColor","animateMotion","animateTransform","circle","clipPath","color-profile","cursor","defs","desc","ellipse","feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence","filter","font","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","foreignObject","g","glyph","glyphRef","hkern","image","line","linearGradient","marker","mask","metadata","missing-glyph","mpath","path","pattern","polygon","polyline","radialGradient","rect","set","stop","svg","switch","symbol","text","textPath","tref","tspan","use","view","vkern"]),se={xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace"};const me="_$DX_DELEGATE";function ge(e,r,o){let t;const a=()=>{var r=document.createElement("template");return r.innerHTML=e,(o?r.content.firstChild:r.content).firstChild};return(r=r?()=>(t=t||a()).cloneNode(!0):()=>_((()=>document.importNode(t=t||a(),!0)))).cloneNode=r}function xe(e,r=window.document){var o=r[me]||(r[me]=new Set);for(let a=0,l=e.length;at.call(e,o[1],r))}else e.addEventListener(r,o)}function he(e,r={},o,t){const a={};return t||P((()=>a.children=Me(e,r.children,a.children))),P((()=>r.ref&&r.ref(e))),P((()=>function(e,r,o,t,a={},l=!1){r=r||{};for(const t in a)t in r||"children"!==t&&(a[t]=Ae(e,t,null,a[t],o,l));for(const n in r){var i;"children"===n?t||Me(e,r.children):(i=r[n],a[n]=Ae(e,n,i,a[n],o,l))}}(e,r,o,!0,a,!0))),a}function Se(e,r,o){return _((()=>e(r,o)))}function ve(e,r,o,t){if(void 0!==o&&(t=t||[]),"function"!=typeof r)return Me(e,r,t,o);P((t=>Me(e,r(),t,o)),t)}function $e(e,r,o){var t=r.trim().split(/\s+/);for(let r=0,a=t.length;rr.toUpperCase()))}(r)]=o):(t=a&&-1o||document});o;){var t=o[r];if(t&&!o.disabled){var a=o[r+"Data"];if(void 0!==a?t.call(o,a,e):t.call(o,e),e.cancelBubble)return}o=o._$host||o.parentNode||o.host}}function Me(e,r,o,t,a){for(;"function"==typeof o;)o=o();if(r!==o){var l=typeof r,i=void 0!==t;if(e=i&&o[0]&&o[0].parentNode||e,"string"==l||"number"==l)if("number"==l&&(r=r.toString()),i){let a=o[0];a&&3===a.nodeType?a.data=r:a=document.createTextNode(r),o=Te(e,o,t,a)}else o=""!==o&&"string"==typeof o?e.firstChild.data=r:e.textContent=r;else if(null==r||"boolean"==l)o=Te(e,o,t);else{if("function"==l)return P((()=>{let a=r();for(;"function"==typeof a;)a=a();o=Me(e,a,o,t)})),()=>o;if(Array.isArray(r)){const n=[];if(l=o&&Array.isArray(o),Pe(n,r,o,a))return P((()=>o=Me(e,n,o,t,!0))),()=>o;if(0===n.length){if(o=Te(e,o,t),i)return o}else l?0===o.length?fe(e,n,t):function(e,r,o){let t=o.length,a=r.length,l=t,i=0,n=0,d=r[a-1].nextSibling,s=null;for(;ix-n)for(var c=r[i];nr.component));return T((()=>{const e=t();switch(typeof e){case"function":return Object.assign(e,{[s]:!0}),_((()=>e(o)));case"string":var r=de.has(e),a=function(e,r=!1){return r?document.createElementNS(_e,e):document.createElement(e)}(e,r);return he(a,o,r),a}}))}function we(e){return(r,o)=>{const t=o.element;return b((a=>{const l=function(e){var r=Object.keys(e),o={};for(let t=0;te))}})}return o}(r);t.addPropertyChangedCallback(((e,r)=>l[e]=r)),t.addReleaseCallback((()=>{t.renderRoot.textContent="",a()}));var i=e(l,o);return ve(t.renderRoot,i)}),function(e){if(e.assignedSlot&&e.assignedSlot._$owner)return e.assignedSlot._$owner;let r=e.parentNode;for(;r&&!r._$owner&&(!r.assignedSlot||!r.assignedSlot._$owner);)r=r.parentNode;return(r&&r.assignedSlot?r.assignedSlot:e)._$owner}(t))}}function Ge(e,r,o){return 2===arguments.length&&(o=r,r={}),i(e,r)(we(o))}const Le={chatflowid:"",apiHost:void 0,onRequest:void 0,chatflowConfig:void 0,theme:void 0,observersConfig:void 0};var Ce='/*! tailwindcss v3.3.1 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}html{-webkit-text-size-adjust:100%;font-feature-settings:normal;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-variation-settings:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]{display:none}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.prose{color:var(--tw-prose-body);max-width:65ch}.prose :where(p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em;margin-top:1.25em}.prose :where([class~=lead]):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-lead);font-size:1.25em;line-height:1.6;margin-bottom:1.2em;margin-top:1.2em}.prose :where(a):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-links);font-weight:500;text-decoration:underline}.prose :where(strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-bold);font-weight:600}.prose :where(a strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(blockquote strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(thead th strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:decimal;margin-bottom:1.25em;margin-top:1.25em;padding-left:1.625em}.prose :where(ol[type=A]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=A s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=I]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type=I s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type="1"]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:decimal}.prose :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:disc;margin-bottom:1.25em;margin-top:1.25em;padding-left:1.625em}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-counters);font-weight:400}.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-bullets)}.prose :where(dt):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;margin-top:1.25em}.prose :where(hr):not(:where([class~=not-prose],[class~=not-prose] *)){border-color:var(--tw-prose-hr);border-top-width:1px;margin-bottom:3em;margin-top:3em}.prose :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)){border-left-color:var(--tw-prose-quote-borders);border-left-width:.25rem;color:var(--tw-prose-quotes);font-style:italic;font-weight:500;margin-bottom:1.6em;margin-top:1.6em;padding-left:1em;quotes:"\\201C""\\201D""\\2018""\\2019"}.prose :where(blockquote p:first-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:open-quote}.prose :where(blockquote p:last-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:close-quote}.prose :where(h1):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-size:2.25em;font-weight:800;line-height:1.1111111;margin-bottom:.8888889em;margin-top:0}.prose :where(h1 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:900}.prose :where(h2):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-size:1.5em;font-weight:700;line-height:1.3333333;margin-bottom:1em;margin-top:2em}.prose :where(h2 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:800}.prose :where(h3):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-size:1.25em;font-weight:600;line-height:1.6;margin-bottom:.6em;margin-top:1.6em}.prose :where(h3 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:700}.prose :where(h4):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;line-height:1.5;margin-bottom:.5em;margin-top:1.5em}.prose :where(h4 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:700}.prose :where(img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:2em;margin-top:2em}.prose :where(picture):not(:where([class~=not-prose],[class~=not-prose] *)){display:block;margin-bottom:2em;margin-top:2em}.prose :where(kbd):not(:where([class~=not-prose],[class~=not-prose] *)){border-radius:.3125rem;box-shadow:0 0 0 1px rgb(var(--tw-prose-kbd-shadows)/10%),0 3px 0 rgb(var(--tw-prose-kbd-shadows)/10%);color:var(--tw-prose-kbd);font-family:inherit;font-size:.875em;font-weight:500;padding:.1875em .375em}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-code);font-size:.875em;font-weight:600}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:"`"}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:"`"}.prose :where(a code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(h1 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(h2 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.875em}.prose :where(h3 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.9em}.prose :where(h4 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(blockquote code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(thead th code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(pre):not(:where([class~=not-prose],[class~=not-prose] *)){background-color:var(--tw-prose-pre-bg);border-radius:.375rem;color:var(--tw-prose-pre-code);font-size:.875em;font-weight:400;line-height:1.7142857;margin-bottom:1.7142857em;margin-top:1.7142857em;overflow-x:auto;padding:.8571429em 1.1428571em}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)){background-color:transparent;border-radius:0;border-width:0;color:inherit;font-family:inherit;font-size:inherit;font-weight:inherit;line-height:inherit;padding:0}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:none}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:none}.prose :where(table):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:.875em;line-height:1.7142857;margin-bottom:2em;margin-top:2em;table-layout:auto;text-align:left;width:100%}.prose :where(thead):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-color:var(--tw-prose-th-borders);border-bottom-width:1px}.prose :where(thead th):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;padding-bottom:.5714286em;padding-left:.5714286em;padding-right:.5714286em;vertical-align:bottom}.prose :where(tbody tr):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-color:var(--tw-prose-td-borders);border-bottom-width:1px}.prose :where(tbody tr:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:0}.prose :where(tbody td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:baseline}.prose :where(tfoot):not(:where([class~=not-prose],[class~=not-prose] *)){border-top-color:var(--tw-prose-th-borders);border-top-width:1px}.prose :where(tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:top}.prose :where(figure>*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0;margin-top:0}.prose :where(figcaption):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-captions);font-size:.875em;line-height:1.4285714;margin-top:.8571429em}.prose{--tw-prose-body:#374151;--tw-prose-headings:#111827;--tw-prose-lead:#4b5563;--tw-prose-links:#111827;--tw-prose-bold:#111827;--tw-prose-counters:#6b7280;--tw-prose-bullets:#d1d5db;--tw-prose-hr:#e5e7eb;--tw-prose-quotes:#111827;--tw-prose-quote-borders:#e5e7eb;--tw-prose-captions:#6b7280;--tw-prose-kbd:#111827;--tw-prose-kbd-shadows:17 24 39;--tw-prose-code:#111827;--tw-prose-pre-code:#e5e7eb;--tw-prose-pre-bg:#1f2937;--tw-prose-th-borders:#d1d5db;--tw-prose-td-borders:#e5e7eb;--tw-prose-invert-body:#d1d5db;--tw-prose-invert-headings:#fff;--tw-prose-invert-lead:#9ca3af;--tw-prose-invert-links:#fff;--tw-prose-invert-bold:#fff;--tw-prose-invert-counters:#9ca3af;--tw-prose-invert-bullets:#4b5563;--tw-prose-invert-hr:#374151;--tw-prose-invert-quotes:#f3f4f6;--tw-prose-invert-quote-borders:#374151;--tw-prose-invert-captions:#9ca3af;--tw-prose-invert-kbd:#fff;--tw-prose-invert-kbd-shadows:255 255 255;--tw-prose-invert-code:#fff;--tw-prose-invert-pre-code:#d1d5db;--tw-prose-invert-pre-bg:rgba(0,0,0,.5);--tw-prose-invert-th-borders:#4b5563;--tw-prose-invert-td-borders:#374151;font-size:1rem;line-height:1.75}.prose :where(picture>img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0;margin-top:0}.prose :where(video):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:2em;margin-top:2em}.prose :where(li):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:.5em;margin-top:.5em}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-left:.375em}.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-left:.375em}.prose :where(.prose>ul>li p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:.75em;margin-top:.75em}.prose :where(.prose>ul>li>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ul>li>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(.prose>ol>li>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ol>li>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:.75em;margin-top:.75em}.prose :where(dl):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em;margin-top:1.25em}.prose :where(dd):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;padding-left:1.625em}.prose :where(hr+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(h2+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(h3+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(h4+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(thead th:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-left:0}.prose :where(thead th:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-right:0}.prose :where(tbody td,tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){padding:.5714286em}.prose :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-left:0}.prose :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-right:0}.prose :where(figure):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:2em;margin-top:2em}.prose :where(.prose>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(.prose>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.bottom-0{bottom:0}.left-0{left:0}.right-0{right:0}.right-\\[-8px\\]{right:-8px}.top-0{top:0}.z-0{z-index:0}.z-10{z-index:10}.z-40{z-index:40}.z-50{z-index:50}.z-\\[1001\\]{z-index:1001}.z-\\[1002\\]{z-index:1002}.float-right{float:right}.m-0{margin:0}.m-\\[6px\\]{margin:6px}.m-auto{margin:auto}.mx-4{margin-left:16px;margin-right:16px}.my-2{margin-bottom:8px;margin-top:8px}.my-6{margin-bottom:24px;margin-top:24px}.-ml-1{margin-left:-4px}.mb-1{margin-bottom:4px}.mb-2{margin-bottom:8px}.mb-3{margin-bottom:12px}.mb-4{margin-bottom:16px}.mb-6{margin-bottom:24px}.ml-1{margin-left:4px}.ml-1\\.5{margin-left:6px}.ml-10{margin-left:40px}.ml-2{margin-left:8px}.ml-auto{margin-left:auto}.mr-1{margin-right:4px}.mr-2{margin-right:8px}.mr-3{margin-right:12px}.mr-\\[10px\\]{margin-right:10px}.mt-2{margin-top:8px}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.hidden{display:none}.h-10{height:40px}.h-12{height:48px}.h-14{height:56px}.h-2{height:8px}.h-4{height:16px}.h-5{height:20px}.h-6{height:24px}.h-7{height:28px}.h-\\[50px\\]{height:50px}.h-\\[58px\\]{height:58px}.h-auto{height:auto}.h-full{height:100%}.max-h-60{max-height:240px}.max-h-\\[128px\\]{max-height:128px}.max-h-\\[192px\\]{max-height:192px}.max-h-\\[704px\\]{max-height:704px}.min-h-\\[56px\\]{min-height:56px}.min-h-full{min-height:100%}.w-10{width:40px}.w-12{width:48px}.w-2{width:8px}.w-4{width:16px}.w-5{width:20px}.w-6{width:24px}.w-64{width:256px}.w-7{width:28px}.w-\\[200px\\]{width:200px}.w-full{width:100%}.min-w-full{min-width:100%}.max-w-3xl{max-width:768px}.max-w-\\[128px\\]{max-width:128px}.max-w-full{max-width:100%}.max-w-max{max-width:-moz-max-content;max-width:max-content}.max-w-md{max-width:448px}.flex-1{flex:1 1 0%}.flex-auto{flex:1 1 auto}.flex-none{flex:none}.flex-shrink-0{flex-shrink:0}.flex-grow{flex-grow:1}.flex-grow-0{flex-grow:0}.basis-auto{flex-basis:auto}.-rotate-180{--tw-rotate:-180deg}.-rotate-180,.rotate-0{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-0{--tw-rotate:0deg}.scale-0{--tw-scale-x:0;--tw-scale-y:0}.scale-0,.scale-100{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-100{--tw-scale-x:1;--tw-scale-y:1}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes fade-in{0%{opacity:0}to{opacity:1}}.animate-fade-in{animation:fade-in .3s ease-out}.animate-spin{animation:spin 1s linear infinite}.cursor-pointer{cursor:pointer}.resize{resize:both}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:4px}.gap-2{gap:8px}.gap-3{gap:12px}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(8px*(1 - var(--tw-space-x-reverse)));margin-right:calc(8px*var(--tw-space-x-reverse))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(16px*(1 - var(--tw-space-x-reverse)));margin-right:calc(16px*var(--tw-space-x-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(8px*var(--tw-space-y-reverse));margin-top:calc(8px*(1 - var(--tw-space-y-reverse)))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(16px*var(--tw-space-y-reverse));margin-top:calc(16px*(1 - var(--tw-space-y-reverse)))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.overflow-x-hidden{overflow-x:hidden}.overflow-y-scroll{overflow-y:scroll}.scroll-smooth{scroll-behavior:smooth}.whitespace-pre-wrap{white-space:pre-wrap}.rounded{border-radius:4px}.rounded-\\[10px\\]{border-radius:10px}.rounded-\\[6px\\]{border-radius:6px}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:8px}.rounded-md{border-radius:6px}.rounded-none{border-radius:0}.rounded-xl{border-radius:12px}.rounded-b{border-bottom-left-radius:4px;border-bottom-right-radius:4px}.rounded-t{border-top-left-radius:4px;border-top-right-radius:4px}.border{border-width:1px}.border-0{border-width:0}.border-2{border-width:2px}.border-4{border-width:4px}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-t-4{border-top-width:4px}.border-solid{border-style:solid}.border-dashed{border-style:dashed}.border-\\[\\#eeeeee\\]{--tw-border-opacity:1;border-color:rgb(238 238 238/var(--tw-border-opacity))}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.border-green-600{--tw-border-opacity:1;border-color:rgb(22 163 74/var(--tw-border-opacity))}.border-red-300{--tw-border-opacity:1;border-color:rgb(252 165 165/var(--tw-border-opacity))}.border-red-600{--tw-border-opacity:1;border-color:rgb(220 38 38/var(--tw-border-opacity))}.border-yellow-300{--tw-border-opacity:1;border-color:rgb(253 224 71/var(--tw-border-opacity))}.border-t-white{--tw-border-opacity:1;border-top-color:rgb(255 255 255/var(--tw-border-opacity))}.bg-\\[rgba\\(0\\2c 0\\2c 0\\2c 0\\.3\\)\\]{background-color:rgba(0,0,0,.3)}.bg-\\[rgba\\(0\\2c 0\\2c 0\\2c 0\\.4\\)\\]{background-color:rgba(0,0,0,.4)}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity))}.bg-black\\/10{background-color:rgba(0,0,0,.1)}.bg-black\\/60{background-color:rgba(0,0,0,.6)}.bg-emerald-500{--tw-bg-opacity:1;background-color:rgb(16 185 129/var(--tw-bg-opacity))}.bg-gray-500{--tw-bg-opacity:1;background-color:rgb(107 114 128/var(--tw-bg-opacity))}.bg-red-100{--tw-bg-opacity:1;background-color:rgb(254 226 226/var(--tw-bg-opacity))}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity))}.bg-transparent{background-color:transparent}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-yellow-100{--tw-bg-opacity:1;background-color:rgb(254 249 195/var(--tw-bg-opacity))}.bg-opacity-50{--tw-bg-opacity:0.5}.bg-cover{background-size:cover}.bg-center{background-position:50%}.fill-transparent{fill:transparent}.stroke-2{stroke-width:2}.object-cover{-o-object-fit:cover;object-fit:cover}.p-0{padding:0}.p-1{padding:4px}.p-10{padding:40px}.p-2{padding:8px}.p-2\\.5{padding:10px}.p-3{padding:12px}.p-4{padding:16px}.p-5{padding:20px}.p-6{padding:24px}.px-1{padding-left:4px;padding-right:4px}.px-12{padding-left:48px;padding-right:48px}.px-2{padding-left:8px;padding-right:8px}.px-3{padding-left:12px;padding-right:12px}.px-4{padding-left:16px;padding-right:16px}.px-5{padding-left:20px;padding-right:20px}.px-6{padding-left:24px;padding-right:24px}.px-\\[10px\\]{padding-left:10px;padding-right:10px}.py-1{padding-bottom:4px;padding-top:4px}.py-2{padding-bottom:8px;padding-top:8px}.py-4{padding-bottom:16px;padding-top:16px}.py-8{padding-bottom:32px;padding-top:32px}.py-\\[10px\\]{padding-bottom:10px;padding-top:10px}.pb-1{padding-bottom:4px}.pb-2{padding-bottom:8px}.pb-\\[10px\\]{padding-bottom:10px}.pl-4{padding-left:16px}.pr-0{padding-right:0}.pr-3{padding-right:12px}.pt-2{padding-top:8px}.pt-4{padding-top:16px}.pt-\\[6px\\]{padding-top:6px}.pt-\\[70px\\]{padding-top:70px}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.font-sans{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}.text-2xl{font-size:24px;line-height:32px}.text-\\[13px\\]{font-size:13px}.text-base{font-size:16px;line-height:24px}.text-sm{font-size:14px;line-height:20px}.text-xl{font-size:20px;line-height:28px}.text-xs{font-size:12px;line-height:16px}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.italic{font-style:italic}.leading-none{line-height:1}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity))}.text-blue-500{--tw-text-opacity:1;color:rgb(59 130 246/var(--tw-text-opacity))}.text-blue-600{--tw-text-opacity:1;color:rgb(37 99 235/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.text-green-600{--tw-text-opacity:1;color:rgb(22 163 74/var(--tw-text-opacity))}.text-inherit{color:inherit}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity))}.text-red-600{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity))}.text-red-700{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity))}.text-transparent{color:transparent}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-yellow-700{--tw-text-opacity:1;color:rgb(161 98 7/var(--tw-text-opacity))}.opacity-0{opacity:0}.opacity-100{opacity:1}.opacity-25{opacity:.25}.opacity-75{opacity:.75}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-lg{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-md{--tw-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.shadow-md,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.shadow-xl{--tw-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.blur-\\[2px\\]{--tw-blur:blur(2px)}.blur-\\[2px\\],.blur-none{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.blur-none{--tw-blur:blur(0)}.drop-shadow{--tw-drop-shadow:drop-shadow(0 1px 2px rgba(0,0,0,.1)) drop-shadow(0 1px 1px rgba(0,0,0,.06))}.drop-shadow,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-sm{--tw-backdrop-blur:blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-all{transition-duration:.15s;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-colors{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-opacity{transition-duration:.15s;transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-150,.transition-transform{transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-linear{transition-timing-function:linear}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}:host{--chatbot-container-bg-image:none;--chatbot-container-bg-color:transparent;--chatbot-container-font-family:"Open Sans";--chatbot-button-bg-color:#0042da;--chatbot-button-color:#fff;--chatbot-host-bubble-bg-color:#f7f8ff;--chatbot-host-bubble-color:#303235;--chatbot-guest-bubble-bg-color:#3b81f6;--chatbot-guest-bubble-color:#fff;--chatbot-input-bg-color:#fff;--chatbot-input-color:#303235;--chatbot-input-placeholder-color:#9095a0;--chatbot-header-bg-color:#fff;--chatbot-header-color:#303235;--chatbot-border-radius:6px;--PhoneInputCountryFlag-borderColor:transparent;--PhoneInput-color--focus:transparent}a{color:#16bed7;font-weight:500}a:hover{text-decoration:underline}pre{word-wrap:break-word;font-size:13px;margin:5px;overflow:auto;padding:5px;white-space:pre-wrap;white-space:-moz-pre-wrap;white-space:-pre-wrap;white-space:-o-pre-wrap;width:auto}.string{color:green}.number{color:#ff8c00}.boolean{color:blue}.null{color:#f0f}.key{color:#002b36}.scrollable-container::-webkit-scrollbar{display:none}.scrollable-container{-ms-overflow-style:none;scrollbar-width:none}.text-fade-in{transition:opacity .4s ease-in .2s}.bubble-typing{transition:width .4s ease-out,height .4s ease-out}.bubble1,.bubble2,.bubble3{background-color:var(--chatbot-host-bubble-color);opacity:.5}.bubble1,.bubble2{animation:chatBubbles 1s ease-in-out infinite}.bubble2{animation-delay:.3s}.bubble3{animation:chatBubbles 1s ease-in-out infinite;animation-delay:.5s}@keyframes chatBubbles{0%{transform:translateY(0)}50%{transform:translateY(-5px)}to{transform:translateY(0)}}button,input,textarea{font-weight:300}.slate-a{text-decoration:underline}.slate-html-container>div{min-height:24px}.slate-bold{font-weight:700}.slate-italic{font-style:oblique}.slate-underline{text-decoration:underline}.text-input::-moz-placeholder{color:#9095a0!important;opacity:1!important}.text-input::placeholder{color:#9095a0!important;opacity:1!important}.chatbot-container{background-color:var(--chatbot-container-bg-color);background-image:var(--chatbot-container-bg-image);font-family:Open Sans,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol}.file-annotation-button{background-color:#02a0a0c2;border:1px solid #02a0a0c2;border-radius:var(--chatbot-border-radius);color:var(--chatbot-button-color)}.chatbot-button{background-color:#0042da;border:1px solid #0042da;border-radius:var(--chatbot-border-radius);color:var(--chatbot-button-color)}.chatbot-button.selectable{border:1px solid #0042da}.chatbot-button.selectable,.chatbot-host-bubble{background-color:#f7f8ff;color:var(--chatbot-host-bubble-color)}.chatbot-host-bubble{word-wrap:break-word;overflow-wrap:break-word;white-space:normal;word-break:break-word}.chatbot-host-bubble>.bubble-typing{background-color:#f7f8ff;border:var(--chatbot-host-bubble-border);border-radius:6px}.chatbot-host-bubble iframe,.chatbot-host-bubble img,.chatbot-host-bubble video{border-radius:var(--chatbot-border-radius)}.chatbot-guest-bubble{word-wrap:break-word;background-color:#3b81f6;border-radius:6px;color:var(--chatbot-guest-bubble-color);overflow-wrap:break-word;white-space:normal;word-break:break-word}.chatbot-input,.feedback-input{background-color:#fff;border-radius:var(--chatbot-border-radius);box-shadow:0 2px 6px -1px rgba(0,0,0,.1);color:#303235}.chatbot-input-error-message{color:#303235}.chatbot-button>.send-icon{fill:var(--chatbot-button-color);stroke:var(--chatbot-button-color)}.chatbot-chat-view{max-width:800px}.ping span{background-color:#0042da}.rating-icon-container svg{stroke:#0042da;fill:#f7f8ff;height:42px;transition:fill .1s ease-out;width:42px}.rating-icon-container.selected svg{fill:#0042da}.rating-icon-container:hover svg{filter:brightness(.9)}.rating-icon-container:active svg{filter:brightness(.75)}.upload-progress-bar{background-color:#0042da;border-radius:var(--chatbot-border-radius)}.total-files-indicator{background-color:#0042da;color:var(--chatbot-button-color);font-size:10px}.chatbot-upload-input{transition:border-color .1s ease-out}.chatbot-upload-input.dragging-over{border-color:#0042da}.secondary-button{background-color:#f7f8ff;border-radius:var(--chatbot-border-radius);color:var(--chatbot-host-bubble-color)}.chatbot-country-select{color:#303235}.chatbot-country-select,.chatbot-date-input{background-color:#fff;border-radius:var(--chatbot-border-radius)}.chatbot-date-input{color:#303235;color-scheme:light}.chatbot-popup-blocked-toast{border-radius:var(--chatbot-border-radius)}.messagelist{border-radius:.5rem;height:100%;overflow-y:scroll;width:100%}.messagelistloading{display:flex;justify-content:center;margin-top:1rem;width:100%}.usermessage{padding:1rem 1.5rem}.usermessagewaiting-light{background:linear-gradient(270deg,#ede7f6,#e3f2fd,#ede7f6);background-position:-100% 0;background-size:200% 200%}.usermessagewaiting-dark,.usermessagewaiting-light{animation:loading-gradient 2s ease-in-out infinite;animation-direction:alternate;animation-name:loading-gradient;padding:1rem 1.5rem}.usermessagewaiting-dark{background:linear-gradient(270deg,#2e2352,#1d3d60,#2e2352);background-position:-100% 0;background-size:200% 200%;color:#ececf1}@keyframes loading-gradient{0%{background-position:-100% 0}to{background-position:100% 0}}.apimessage{animation:fadein .5s;padding:1rem 1.5rem}@keyframes fadein{0%{opacity:0}to{opacity:1}}.apimessage,.usermessage,.usermessagewaiting{display:flex}.markdownanswer{line-height:1.75}.markdownanswer a:hover{opacity:.8}.markdownanswer a{color:#16bed7;font-weight:500}.markdownanswer code{color:#15cb19;font-weight:500;white-space:pre-wrap!important}.markdownanswer ol,.markdownanswer ul{margin:1rem}.boticon,.usericon{border-radius:1rem;margin-right:1rem}.markdownanswer h1,.markdownanswer h2,.markdownanswer h3{font-size:inherit}.center{flex-direction:column;padding:10px;position:relative}.center,.cloud{align-items:center;display:flex;justify-content:center}.cloud{border-radius:.5rem;height:calc(100% - 50px);width:400px}input,textarea{background-color:transparent;border:none;font-family:Poppins,sans-serif;padding:10px}@media (max-width:640px){div[part=bot]{height:100%!important;left:0!important;max-height:unset!important;max-width:unset!important;overflow:auto;overflow-x:hidden;position:fixed!important;top:0!important;width:100%!important}.chatbot-container,.rounded-lg,div[class="flex flex-row items-center w-full h-[50px] absolute top-0 left-0 z-10"],div[part=button]{border-radius:0!important}button{cursor:default!important}}.tooltip{background:var(--tooltip-background-color,#000);border-radius:5px;color:var(--tooltip-text-color,#fff);font-size:var(--tooltip-font-size,12px);max-width:calc(100vw - 20px);padding:5px 10px;position:fixed;transition:opacity .3s ease-in-out;white-space:pre-wrap;word-break:break-word;z-index:42424242}@keyframes spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.spinner{animation:spin 1s linear infinite;border:4px solid hsla(0,0%,100%,.3);border-radius:50%;border-top-color:#fff;height:24px;width:24px}.hover\\:scale-110:hover{--tw-scale-x:1.1;--tw-scale-y:1.1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.hover\\:bg-green-600:hover{--tw-bg-opacity:1;background-color:rgb(22 163 74/var(--tw-bg-opacity))}.hover\\:bg-red-600:hover{--tw-bg-opacity:1;background-color:rgb(220 38 38/var(--tw-bg-opacity))}.hover\\:bg-transparent:hover{background-color:transparent}.hover\\:text-gray-700:hover{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.hover\\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.hover\\:shadow-lg:hover{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.hover\\:brightness-90:hover{--tw-brightness:brightness(.9);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.focus\\:border-blue-500:focus{--tw-border-opacity:1;border-color:rgb(59 130 246/var(--tw-border-opacity))}.focus\\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\\:ring-blue-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(59 130 246/var(--tw-ring-opacity))}.active\\:scale-95:active{--tw-scale-x:.95;--tw-scale-y:.95;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.active\\:bg-emerald-600:active{--tw-bg-opacity:1;background-color:rgb(5 150 105/var(--tw-bg-opacity))}.active\\:brightness-75:active{--tw-brightness:brightness(.75);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.disabled\\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\\:opacity-50:disabled{opacity:.5}.disabled\\:brightness-100:disabled{--tw-brightness:brightness(1);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.group:hover .group-hover\\:flex{display:flex}@media (min-width:640px){.sm\\:right-5{right:20px}.sm\\:my-8{margin-bottom:32px;margin-top:32px}.sm\\:w-\\[400px\\]{width:400px}.sm\\:w-full{width:100%}.sm\\:max-w-lg{max-width:512px}.sm\\:p-0{padding:0}}@media (min-width:768px){.md\\:mb-0{margin-bottom:0}.md\\:flex-row{flex-direction:row}}';const Be=e=>null==e,Ee=e=>null!=e,Ne=async e=>{try{var r="string"==typeof e?e:e.url,o="string"!=typeof e&&Ee(e.body)?{"Content-Type":"application/json",...e.headers}:void 0;let i="string"!=typeof e&&Ee(e.body)?JSON.stringify(e.body):void 0;"string"!=typeof e&&e.formData&&(i=e.formData);var t={method:"string"==typeof e?"GET":e.method,mode:"cors",headers:o,body:i},a=("string"!=typeof e&&e.onRequest&&await e.onRequest(t),await fetch(r,t));let n;var l=a.headers.get("Content-Type");if(n=l&&l.includes("application/json")?await a.json():"string"!=typeof e&&"blob"===e.type?await a.blob():await a.text(),a.ok)return{data:n};{let e;throw e="object"==typeof n&&"error"in n?n.error:n||a.statusText}}catch(e){return console.error(e),{error:e}}},Oe=(e,r,o={})=>{var t=localStorage.getItem(e+"_EXTERNAL");o={...o};if(r&&(o.chatId=r),t)try{var a=JSON.parse(t);localStorage.setItem(e+"_EXTERNAL",JSON.stringify({...a,...o}))}catch(a){const r=t;o.chatId=r,localStorage.setItem(e+"_EXTERNAL",JSON.stringify(o))}else localStorage.setItem(e+"_EXTERNAL",JSON.stringify(o))},Ie=e=>{if(!(e=localStorage.getItem(e+"_EXTERNAL")))return{};try{return JSON.parse(e)}catch(e){return{}}},ke=e=>e?"number"==typeof e?e:"small"===e?32:"medium"!==e&&"large"===e?64:48:48,Xe=ge(''),De=ge('Bubble button icon'),Re=ge('
+ + + +
+ ) +} + +ChatMessage.propTypes = { + open: PropTypes.bool, + chatflowid: PropTypes.string, + isAgentCanvas: PropTypes.bool, + isDialog: PropTypes.bool, + previews: PropTypes.array, + setPreviews: PropTypes.func +} + +export default memo(ChatMessage) diff --git a/src/components/bubbles/BotBubble.tsx b/src/components/bubbles/BotBubble.tsx index f57e2e8..ca75fd1 100644 --- a/src/components/bubbles/BotBubble.tsx +++ b/src/components/bubbles/BotBubble.tsx @@ -396,11 +396,6 @@ export const BotBubble = (props: Props) => { } }; - createEffect(() => { - console.log('messageId', props.message.id || props.message.messageId); - console.log('isTTSLoading', props.isTTSLoading); - }); - return (
diff --git a/src/queries/sendMessageQuery.ts b/src/queries/sendMessageQuery.ts index 08f8862..a1a3ce3 100644 --- a/src/queries/sendMessageQuery.ts +++ b/src/queries/sendMessageQuery.ts @@ -71,6 +71,14 @@ export type GenerateTTSRequest = BaseRequest & { signal?: AbortSignal; }; +export type AbortTTSRequest = BaseRequest & { + body: { + chatbot: string; + chatId: string; + chatMessageId: string; + }; +}; + export const sendFeedbackQuery = ({ chatbot, apiHost = 'http://localhost:3000', body, onRequest }: CreateFeedbackRequest) => sendRequest({ method: 'POST', From 678245661ad2286a31426e861289e807ad781b3a Mon Sep 17 00:00:00 2001 From: Ilango Rajagopal Date: Fri, 26 Sep 2025 13:48:52 +0530 Subject: [PATCH 14/33] Apply prettier --- src/components/ChatMessage.jsx | 5860 ++++++++++++++++---------------- 1 file changed, 2869 insertions(+), 2991 deletions(-) diff --git a/src/components/ChatMessage.jsx b/src/components/ChatMessage.jsx index a70fa57..7baa627 100644 --- a/src/components/ChatMessage.jsx +++ b/src/components/ChatMessage.jsx @@ -1,3068 +1,2946 @@ -import { useState, useRef, useEffect, useCallback, Fragment, useContext, memo } from 'react' -import { useSelector, useDispatch } from 'react-redux' -import PropTypes from 'prop-types' -import { cloneDeep } from 'lodash' -import axios from 'axios' -import { v4 as uuidv4 } from 'uuid' -import { EventStreamContentType, fetchEventSource } from '@microsoft/fetch-event-source' +import { useState, useRef, useEffect, useCallback, Fragment, useContext, memo } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import PropTypes from 'prop-types'; +import { cloneDeep } from 'lodash'; +import axios from 'axios'; +import { v4 as uuidv4 } from 'uuid'; +import { EventStreamContentType, fetchEventSource } from '@microsoft/fetch-event-source'; import { - Box, - Button, - Card, - CardMedia, - Chip, - CircularProgress, - Divider, - IconButton, - InputAdornment, - OutlinedInput, - Typography, - Stack, - Dialog, - DialogTitle, - DialogContent, - DialogActions, - TextField -} from '@mui/material' -import { darken, useTheme } from '@mui/material/styles' + Box, + Button, + Card, + CardMedia, + Chip, + CircularProgress, + Divider, + IconButton, + InputAdornment, + OutlinedInput, + Typography, + Stack, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + TextField, +} from '@mui/material'; +import { darken, useTheme } from '@mui/material/styles'; import { - IconCircleDot, - IconDownload, - IconSend, - IconMicrophone, - IconPhotoPlus, - IconTrash, - IconX, - IconTool, - IconSquareFilled, - IconCheck, - IconPaperclip, - IconSparkles, - IconVolume -} from '@tabler/icons-react' -import robotPNG from '@/assets/images/robot.png' -import userPNG from '@/assets/images/account.png' -import multiagent_supervisorPNG from '@/assets/images/multiagent_supervisor.png' -import multiagent_workerPNG from '@/assets/images/multiagent_worker.png' -import audioUploadSVG from '@/assets/images/wave-sound.jpg' + IconCircleDot, + IconDownload, + IconSend, + IconMicrophone, + IconPhotoPlus, + IconTrash, + IconX, + IconTool, + IconSquareFilled, + IconCheck, + IconPaperclip, + IconSparkles, + IconVolume, +} from '@tabler/icons-react'; +import robotPNG from '@/assets/images/robot.png'; +import userPNG from '@/assets/images/account.png'; +import multiagent_supervisorPNG from '@/assets/images/multiagent_supervisor.png'; +import multiagent_workerPNG from '@/assets/images/multiagent_worker.png'; +import audioUploadSVG from '@/assets/images/wave-sound.jpg'; // project import -import NodeInputHandler from '@/views/canvas/NodeInputHandler' -import { MemoizedReactMarkdown } from '@/ui-component/markdown/MemoizedReactMarkdown' -import { SafeHTML } from '@/ui-component/safe/SafeHTML' -import SourceDocDialog from '@/ui-component/dialog/SourceDocDialog' -import ChatFeedbackContentDialog from '@/ui-component/dialog/ChatFeedbackContentDialog' -import StarterPromptsCard from '@/ui-component/cards/StarterPromptsCard' -import AgentReasoningCard from './AgentReasoningCard' -import AgentExecutedDataCard from './AgentExecutedDataCard' -import { ImageButton, ImageSrc, ImageBackdrop, ImageMarked } from '@/ui-component/button/ImageButton' -import CopyToClipboardButton from '@/ui-component/button/CopyToClipboardButton' -import ThumbsUpButton from '@/ui-component/button/ThumbsUpButton' -import ThumbsDownButton from '@/ui-component/button/ThumbsDownButton' -import { cancelAudioRecording, startAudioRecording, stopAudioRecording } from './audio-recording' -import './audio-recording.css' -import './ChatMessage.css' +import NodeInputHandler from '@/views/canvas/NodeInputHandler'; +import { MemoizedReactMarkdown } from '@/ui-component/markdown/MemoizedReactMarkdown'; +import { SafeHTML } from '@/ui-component/safe/SafeHTML'; +import SourceDocDialog from '@/ui-component/dialog/SourceDocDialog'; +import ChatFeedbackContentDialog from '@/ui-component/dialog/ChatFeedbackContentDialog'; +import StarterPromptsCard from '@/ui-component/cards/StarterPromptsCard'; +import AgentReasoningCard from './AgentReasoningCard'; +import AgentExecutedDataCard from './AgentExecutedDataCard'; +import { ImageButton, ImageSrc, ImageBackdrop, ImageMarked } from '@/ui-component/button/ImageButton'; +import CopyToClipboardButton from '@/ui-component/button/CopyToClipboardButton'; +import ThumbsUpButton from '@/ui-component/button/ThumbsUpButton'; +import ThumbsDownButton from '@/ui-component/button/ThumbsDownButton'; +import { cancelAudioRecording, startAudioRecording, stopAudioRecording } from './audio-recording'; +import './audio-recording.css'; +import './ChatMessage.css'; // api -import chatmessageApi from '@/api/chatmessage' -import chatflowsApi from '@/api/chatflows' -import predictionApi from '@/api/prediction' -import vectorstoreApi from '@/api/vectorstore' -import attachmentsApi from '@/api/attachments' -import chatmessagefeedbackApi from '@/api/chatmessagefeedback' -import leadsApi from '@/api/lead' -import executionsApi from '@/api/executions' -import ttsApi from '@/api/tts' +import chatmessageApi from '@/api/chatmessage'; +import chatflowsApi from '@/api/chatflows'; +import predictionApi from '@/api/prediction'; +import vectorstoreApi from '@/api/vectorstore'; +import attachmentsApi from '@/api/attachments'; +import chatmessagefeedbackApi from '@/api/chatmessagefeedback'; +import leadsApi from '@/api/lead'; +import executionsApi from '@/api/executions'; +import ttsApi from '@/api/tts'; // Hooks -import useApi from '@/hooks/useApi' -import { flowContext } from '@/store/context/ReactFlowContext' +import useApi from '@/hooks/useApi'; +import { flowContext } from '@/store/context/ReactFlowContext'; // Const -import { baseURL, maxScroll } from '@/store/constant' -import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions' +import { baseURL, maxScroll } from '@/store/constant'; +import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions'; // Utils -import { isValidURL, removeDuplicateURL, setLocalStorageChatflow, getLocalStorageChatflow } from '@/utils/genericHelper' -import useNotifier from '@/utils/useNotifier' -import FollowUpPromptsCard from '@/ui-component/cards/FollowUpPromptsCard' +import { isValidURL, removeDuplicateURL, setLocalStorageChatflow, getLocalStorageChatflow } from '@/utils/genericHelper'; +import useNotifier from '@/utils/useNotifier'; +import FollowUpPromptsCard from '@/ui-component/cards/FollowUpPromptsCard'; // History -import { ChatInputHistory } from './ChatInputHistory' +import { ChatInputHistory } from './ChatInputHistory'; const messageImageStyle = { - width: '128px', - height: '128px', - objectFit: 'cover' -} + width: '128px', + height: '128px', + objectFit: 'cover', +}; const CardWithDeleteOverlay = ({ item, disabled, customization, onDelete }) => { - const [isHovered, setIsHovered] = useState(false) - const defaultBackgroundColor = customization.isDarkMode ? 'rgba(0, 0, 0, 0.3)' : 'transparent' - - return ( -
setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - style={{ position: 'relative', display: 'inline-block' }} - > - - - - {item.name} - - - {isHovered && !disabled && ( - - )} -
- ) -} - -CardWithDeleteOverlay.propTypes = { - item: PropTypes.object, - customization: PropTypes.object, - disabled: PropTypes.bool, - onDelete: PropTypes.func -} - -const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, previews, setPreviews }) => { - const theme = useTheme() - const customization = useSelector((state) => state.customization) - - const ps = useRef() - - const dispatch = useDispatch() - const { onAgentflowNodeStatusUpdate, clearAgentflowNodeStatus } = useContext(flowContext) - - useNotifier() - const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) - const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) - - const [userInput, setUserInput] = useState('') - const [loading, setLoading] = useState(false) - const [messages, setMessages] = useState([ - { - message: 'Hi there! How can I help?', - type: 'apiMessage' - } - ]) - const [isChatFlowAvailableToStream, setIsChatFlowAvailableToStream] = useState(false) - const [isChatFlowAvailableForSpeech, setIsChatFlowAvailableForSpeech] = useState(false) - const [sourceDialogOpen, setSourceDialogOpen] = useState(false) - const [sourceDialogProps, setSourceDialogProps] = useState({}) - const [chatId, setChatId] = useState(uuidv4()) - const [isMessageStopping, setIsMessageStopping] = useState(false) - const [uploadedFiles, setUploadedFiles] = useState([]) - const [imageUploadAllowedTypes, setImageUploadAllowedTypes] = useState('') - const [fileUploadAllowedTypes, setFileUploadAllowedTypes] = useState('') - const [inputHistory] = useState(new ChatInputHistory(10)) - - const inputRef = useRef(null) - const getChatmessageApi = useApi(chatmessageApi.getInternalChatmessageFromChatflow) - const getAllExecutionsApi = useApi(executionsApi.getAllExecutions) - const getIsChatflowStreamingApi = useApi(chatflowsApi.getIsChatflowStreaming) - const getAllowChatFlowUploads = useApi(chatflowsApi.getAllowChatflowUploads) - const getChatflowConfig = useApi(chatflowsApi.getSpecificChatflow) - - const [starterPrompts, setStarterPrompts] = useState([]) - - // full file upload - const [fullFileUpload, setFullFileUpload] = useState(false) - const [fullFileUploadAllowedTypes, setFullFileUploadAllowedTypes] = useState('*') - - // feedback - const [chatFeedbackStatus, setChatFeedbackStatus] = useState(false) - const [feedbackId, setFeedbackId] = useState('') - const [showFeedbackContentDialog, setShowFeedbackContentDialog] = useState(false) - - // leads - const [leadsConfig, setLeadsConfig] = useState(null) - const [leadName, setLeadName] = useState('') - const [leadEmail, setLeadEmail] = useState('') - const [leadPhone, setLeadPhone] = useState('') - const [isLeadSaving, setIsLeadSaving] = useState(false) - const [isLeadSaved, setIsLeadSaved] = useState(false) - - // follow-up prompts - const [followUpPromptsStatus, setFollowUpPromptsStatus] = useState(false) - const [followUpPrompts, setFollowUpPrompts] = useState([]) - - // drag & drop and file input - const imgUploadRef = useRef(null) - const fileUploadRef = useRef(null) - const [isChatFlowAvailableForImageUploads, setIsChatFlowAvailableForImageUploads] = useState(false) - const [isChatFlowAvailableForFileUploads, setIsChatFlowAvailableForFileUploads] = useState(false) - const [isChatFlowAvailableForRAGFileUploads, setIsChatFlowAvailableForRAGFileUploads] = useState(false) - const [isDragActive, setIsDragActive] = useState(false) - - // recording - const [isRecording, setIsRecording] = useState(false) - const [recordingNotSupported, setRecordingNotSupported] = useState(false) - const [isLoadingRecording, setIsLoadingRecording] = useState(false) - - const [openFeedbackDialog, setOpenFeedbackDialog] = useState(false) - const [feedback, setFeedback] = useState('') - const [pendingActionData, setPendingActionData] = useState(null) - const [feedbackType, setFeedbackType] = useState('') - - // start input type - const [startInputType, setStartInputType] = useState('') - const [formTitle, setFormTitle] = useState('') - const [formDescription, setFormDescription] = useState('') - const [formInputsData, setFormInputsData] = useState({}) - const [formInputParams, setFormInputParams] = useState([]) - - const [isConfigLoading, setIsConfigLoading] = useState(true) - - // TTS state - const [isTTSLoading, setIsTTSLoading] = useState({}) - const [isTTSPlaying, setIsTTSPlaying] = useState({}) - const [ttsAudio, setTtsAudio] = useState({}) - const [isTTSEnabled, setIsTTSEnabled] = useState(false) - - // TTS streaming state - const [ttsStreamingState, setTtsStreamingState] = useState({ - mediaSource: null, - sourceBuffer: null, - audio: null, - chunkQueue: [], - isBuffering: false, - audioFormat: null, - abortController: null - }) - - // Ref to prevent auto-scroll during TTS actions (using ref to avoid re-renders) - const isTTSActionRef = useRef(false) - const ttsTimeoutRef = useRef(null) - - const isFileAllowedForUpload = (file) => { - const constraints = getAllowChatFlowUploads.data - /** - * {isImageUploadAllowed: boolean, imgUploadSizeAndTypes: Array<{ fileTypes: string[], maxUploadSize: number }>} - */ - let acceptFile = false - - // Early return if constraints are not available yet - if (!constraints) { - console.warn('Upload constraints not loaded yet') - return false - } - - if (constraints.isImageUploadAllowed) { - const fileType = file.type - const sizeInMB = file.size / 1024 / 1024 - if (constraints.imgUploadSizeAndTypes && Array.isArray(constraints.imgUploadSizeAndTypes)) { - constraints.imgUploadSizeAndTypes.forEach((allowed) => { - if (allowed.fileTypes && allowed.fileTypes.includes(fileType) && sizeInMB <= allowed.maxUploadSize) { - acceptFile = true - } - }) - } - } - - if (fullFileUpload) { - return true - } else if (constraints.isRAGFileUploadAllowed) { - const fileExt = file.name.split('.').pop() - if (fileExt && constraints.fileUploadSizeAndTypes && Array.isArray(constraints.fileUploadSizeAndTypes)) { - constraints.fileUploadSizeAndTypes.forEach((allowed) => { - if (allowed.fileTypes && allowed.fileTypes.length === 1 && allowed.fileTypes[0] === '*') { - acceptFile = true - } else if (allowed.fileTypes && allowed.fileTypes.includes(`.${fileExt}`)) { - acceptFile = true - } - }) - } - } - if (!acceptFile) { - alert(`Cannot upload file. Kindly check the allowed file types and maximum allowed size.`) - } - return acceptFile - } - - const handleDrop = async (e) => { - if (!isChatFlowAvailableForImageUploads && !isChatFlowAvailableForFileUploads) { - return - } - e.preventDefault() - setIsDragActive(false) - let files = [] - let uploadedFiles = [] - - if (e.dataTransfer.files.length > 0) { - for (const file of e.dataTransfer.files) { - if (isFileAllowedForUpload(file) === false) { - return - } - const reader = new FileReader() - const { name } = file - // Only add files - if (!file.type || !imageUploadAllowedTypes.includes(file.type)) { - uploadedFiles.push({ file, type: fullFileUpload ? 'file:full' : 'file:rag' }) - } - files.push( - new Promise((resolve) => { - reader.onload = (evt) => { - if (!evt?.target?.result) { - return - } - const { result } = evt.target - let previewUrl - if (file.type.startsWith('audio/')) { - previewUrl = audioUploadSVG - } else { - previewUrl = URL.createObjectURL(file) - } - resolve({ - data: result, - preview: previewUrl, - type: 'file', - name: name, - mime: file.type - }) - } - reader.readAsDataURL(file) - }) - ) - } - - const newFiles = await Promise.all(files) - setUploadedFiles(uploadedFiles) - setPreviews((prevPreviews) => [...prevPreviews, ...newFiles]) - } - - if (e.dataTransfer.items) { - //TODO set files - for (const item of e.dataTransfer.items) { - if (item.kind === 'string' && item.type.match('^text/uri-list')) { - item.getAsString((s) => { - let upload = { - data: s, - preview: s, - type: 'url', - name: s ? s.substring(s.lastIndexOf('/') + 1) : '' - } - setPreviews((prevPreviews) => [...prevPreviews, upload]) - }) - } else if (item.kind === 'string' && item.type.match('^text/html')) { - item.getAsString((s) => { - if (s.indexOf('href') === -1) return - //extract href - let start = s ? s.substring(s.indexOf('href') + 6) : '' - let hrefStr = start.substring(0, start.indexOf('"')) - - let upload = { - data: hrefStr, - preview: hrefStr, - type: 'url', - name: hrefStr ? hrefStr.substring(hrefStr.lastIndexOf('/') + 1) : '' - } - setPreviews((prevPreviews) => [...prevPreviews, upload]) - }) - } - } - } - } - - const handleFileChange = async (event) => { - const fileObj = event.target.files && event.target.files[0] - if (!fileObj) { - return - } - let files = [] - let uploadedFiles = [] - for (const file of event.target.files) { - if (isFileAllowedForUpload(file) === false) { - return - } - // Only add files - if (!file.type || !imageUploadAllowedTypes.includes(file.type)) { - uploadedFiles.push({ file, type: fullFileUpload ? 'file:full' : 'file:rag' }) - } - const reader = new FileReader() - const { name } = file - files.push( - new Promise((resolve) => { - reader.onload = (evt) => { - if (!evt?.target?.result) { - return - } - const { result } = evt.target - resolve({ - data: result, - preview: URL.createObjectURL(file), - type: 'file', - name: name, - mime: file.type - }) - } - reader.readAsDataURL(file) - }) - ) - } - - const newFiles = await Promise.all(files) - setUploadedFiles(uploadedFiles) - setPreviews((prevPreviews) => [...prevPreviews, ...newFiles]) - // 👇️ reset file input - event.target.value = null - } - - const addRecordingToPreviews = (blob) => { - let mimeType = '' - const pos = blob.type.indexOf(';') - if (pos === -1) { - mimeType = blob.type - } else { - mimeType = blob.type ? blob.type.substring(0, pos) : '' - } - // read blob and add to previews - const reader = new FileReader() - reader.readAsDataURL(blob) - reader.onloadend = () => { - const base64data = reader.result - const upload = { - data: base64data, - preview: audioUploadSVG, - type: 'audio', - name: `audio_${Date.now()}.wav`, - mime: mimeType - } - setPreviews((prevPreviews) => [...prevPreviews, upload]) - } - } - - const handleDrag = (e) => { - if (isChatFlowAvailableForImageUploads || isChatFlowAvailableForFileUploads) { - e.preventDefault() - e.stopPropagation() - if (e.type === 'dragenter' || e.type === 'dragover') { - setIsDragActive(true) - } else if (e.type === 'dragleave') { - setIsDragActive(false) - } - } - } - - const handleAbort = async () => { - setIsMessageStopping(true) - try { - // Stop all TTS streams first - stopAllTTS() - - // Abort TTS for any active streams - const activeTTSMessages = Object.keys(isTTSLoading).concat(Object.keys(isTTSPlaying)) - for (const messageId of activeTTSMessages) { - await ttsApi.abortTTS({ chatflowId: chatflowid, chatId, chatMessageId: messageId }) - } - - await chatmessageApi.abortMessage(chatflowid, chatId) - } catch (error) { - setIsMessageStopping(false) - enqueueSnackbar({ - message: typeof error.response.data === 'object' ? error.response.data.message : error.response.data, - options: { - key: new Date().getTime() + Math.random(), - variant: 'error', - persist: true, - action: (key) => ( - - ) - } - }) - } - } - - const handleDeletePreview = (itemToDelete) => { - if (itemToDelete.type === 'file') { - URL.revokeObjectURL(itemToDelete.preview) // Clean up for file - } - setPreviews(previews.filter((item) => item !== itemToDelete)) - } - - const handleFileUploadClick = () => { - // 👇️ open file input box on click of another element - fileUploadRef.current.click() - } - - const handleImageUploadClick = () => { - // 👇️ open file input box on click of another element - imgUploadRef.current.click() - } - - const clearPreviews = () => { - // Revoke the data uris to avoid memory leaks - previews.forEach((file) => URL.revokeObjectURL(file.preview)) - setPreviews([]) - } - - const onMicrophonePressed = () => { - setIsRecording(true) - startAudioRecording(setIsRecording, setRecordingNotSupported) - } - - const onRecordingCancelled = () => { - if (!recordingNotSupported) cancelAudioRecording() - setIsRecording(false) - setRecordingNotSupported(false) - } - - const onRecordingStopped = async () => { - setIsLoadingRecording(true) - stopAudioRecording(addRecordingToPreviews) - } - - const onSourceDialogClick = (data, title) => { - setSourceDialogProps({ data, title }) - setSourceDialogOpen(true) - } - - const onURLClick = (data) => { - window.open(data, '_blank') - } - - const scrollToBottom = () => { - if (ps.current) { - ps.current.scrollTo({ top: maxScroll }) - } - } - - // Helper function to manage TTS action flag - const setTTSAction = (isActive) => { - isTTSActionRef.current = isActive - if (ttsTimeoutRef.current) { - clearTimeout(ttsTimeoutRef.current) - ttsTimeoutRef.current = null - } - if (isActive) { - // Reset the flag after a longer delay to ensure all state changes are complete - ttsTimeoutRef.current = setTimeout(() => { - isTTSActionRef.current = false - ttsTimeoutRef.current = null - }, 300) - } - } - - const onChange = useCallback((e) => setUserInput(e.target.value), [setUserInput]) - - const updateLastMessage = (text) => { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)] - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages - allMessages[allMessages.length - 1].message += text - allMessages[allMessages.length - 1].feedback = null - return allMessages - }) - } - - const updateErrorMessage = (errorMessage) => { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)] - allMessages.push({ message: errorMessage, type: 'apiMessage' }) - return allMessages - }) - } - - const updateLastMessageSourceDocuments = (sourceDocuments) => { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)] - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages - allMessages[allMessages.length - 1].sourceDocuments = sourceDocuments - return allMessages - }) - } - - const updateLastMessageAgentReasoning = (agentReasoning) => { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)] - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages - allMessages[allMessages.length - 1].agentReasoning = agentReasoning - return allMessages - }) - } - - const updateAgentFlowEvent = (event) => { - if (event === 'INPROGRESS') { - setMessages((prevMessages) => [...prevMessages, { message: '', type: 'apiMessage', agentFlowEventStatus: event }]) - } else { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)] - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages - allMessages[allMessages.length - 1].agentFlowEventStatus = event - return allMessages - }) - } - } - - const updateAgentFlowExecutedData = (agentFlowExecutedData) => { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)] - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages - allMessages[allMessages.length - 1].agentFlowExecutedData = agentFlowExecutedData - return allMessages - }) - } - - const updateLastMessageAction = (action) => { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)] - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages - allMessages[allMessages.length - 1].action = action - return allMessages - }) - } - - const updateLastMessageArtifacts = (artifacts) => { - artifacts.forEach((artifact) => { - if (artifact.type === 'png' || artifact.type === 'jpeg') { - artifact.data = `${baseURL}/api/v1/get-upload-file?chatflowId=${chatflowid}&chatId=${chatId}&fileName=${artifact.data.replace( - 'FILE-STORAGE::', - '' - )}` - } - }) - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)] - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages - allMessages[allMessages.length - 1].artifacts = artifacts - return allMessages - }) - } - - const updateLastMessageNextAgent = (nextAgent) => { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)] - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages - const lastAgentReasoning = allMessages[allMessages.length - 1].agentReasoning - if (lastAgentReasoning && lastAgentReasoning.length > 0) { - lastAgentReasoning.push({ nextAgent }) - } - allMessages[allMessages.length - 1].agentReasoning = lastAgentReasoning - return allMessages - }) - } - - const updateLastMessageNextAgentFlow = (nextAgentFlow) => { - onAgentflowNodeStatusUpdate(nextAgentFlow) - } - - const updateLastMessageUsedTools = (usedTools) => { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)] - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages - allMessages[allMessages.length - 1].usedTools = usedTools - return allMessages - }) - } - - const updateLastMessageFileAnnotations = (fileAnnotations) => { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)] - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages - allMessages[allMessages.length - 1].fileAnnotations = fileAnnotations - return allMessages - }) - } - - const abortMessage = () => { - setIsMessageStopping(false) - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)] - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages - const lastAgentReasoning = allMessages[allMessages.length - 1].agentReasoning - if (lastAgentReasoning && lastAgentReasoning.length > 0) { - allMessages[allMessages.length - 1].agentReasoning = lastAgentReasoning.filter((reasoning) => !reasoning.nextAgent) - } - return allMessages - }) - setTimeout(() => { - inputRef.current?.focus() - }, 100) - enqueueSnackbar({ - message: 'Message stopped', - options: { - key: new Date().getTime() + Math.random(), - variant: 'success', - action: (key) => ( - - ) - } - }) - } - - const handleError = (message = 'Oops! There seems to be an error. Please try again.') => { - message = message.replace(`Unable to parse JSON response from chat agent.\n\n`, '') - setMessages((prevMessages) => [...prevMessages, { message, type: 'apiMessage' }]) - setLoading(false) - setUserInput('') - setUploadedFiles([]) - setTimeout(() => { - inputRef.current?.focus() - }, 100) - } - - const handlePromptClick = async (promptStarterInput) => { - setUserInput(promptStarterInput) - handleSubmit(undefined, promptStarterInput) - } - - const handleFollowUpPromptClick = async (promptStarterInput) => { - setUserInput(promptStarterInput) - setFollowUpPrompts([]) - handleSubmit(undefined, promptStarterInput) - } - - const onSubmitResponse = (actionData, feedback = '', type = '') => { - let fbType = feedbackType - if (type) { - fbType = type - } - const question = feedback ? feedback : fbType.charAt(0).toUpperCase() + fbType.slice(1) - handleSubmit(undefined, question, undefined, { - type: fbType, - startNodeId: actionData?.nodeId, - feedback - }) - } - - const handleSubmitFeedback = () => { - if (pendingActionData) { - onSubmitResponse(pendingActionData, feedback) - setOpenFeedbackDialog(false) - setFeedback('') - setPendingActionData(null) - setFeedbackType('') - } - } - - const handleActionClick = async (elem, action) => { - setUserInput(elem.label) - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)] - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages - allMessages[allMessages.length - 1].action = null - return allMessages - }) - if (elem.type.includes('agentflowv2')) { - const type = elem.type.includes('approve') ? 'proceed' : 'reject' - setFeedbackType(type) - - if (action.data && action.data.input && action.data.input.humanInputEnableFeedback) { - setPendingActionData(action.data) - setOpenFeedbackDialog(true) - } else { - onSubmitResponse(action.data, '', type) - } - } else { - handleSubmit(undefined, elem.label, action) - } - } - - const updateMetadata = (data, input) => { - // set message id that is needed for feedback - if (data.chatMessageId) { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)] - if (allMessages[allMessages.length - 1].type === 'apiMessage') { - allMessages[allMessages.length - 1].id = data.chatMessageId - } - return allMessages - }) - } - - if (data.chatId) { - setChatId(data.chatId) - } - - if (input === '' && data.question) { - // the response contains the question even if it was in an audio format - // so if input is empty but the response contains the question, update the user message to show the question - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)] - if (allMessages[allMessages.length - 2].type === 'apiMessage') return allMessages - allMessages[allMessages.length - 2].message = data.question - return allMessages - }) - } - - if (data.followUpPrompts) { - const followUpPrompts = JSON.parse(data.followUpPrompts) - if (typeof followUpPrompts === 'string') { - setFollowUpPrompts(JSON.parse(followUpPrompts)) - } else { - setFollowUpPrompts(followUpPrompts) - } - } - } - - const handleFileUploads = async (uploads) => { - if (!uploadedFiles.length) return uploads - - if (fullFileUpload) { - const filesWithFullUploadType = uploadedFiles.filter((file) => file.type === 'file:full') - if (filesWithFullUploadType.length > 0) { - const formData = new FormData() - for (const file of filesWithFullUploadType) { - formData.append('files', file.file) - } - formData.append('chatId', chatId) - - const response = await attachmentsApi.createAttachment(chatflowid, chatId, formData) - const data = response.data - - for (const extractedFileData of data) { - const content = extractedFileData.content - const fileName = extractedFileData.name - - // find matching name in previews and replace data with content - const uploadIndex = uploads.findIndex((upload) => upload.name === fileName) - - if (uploadIndex !== -1) { - uploads[uploadIndex] = { - ...uploads[uploadIndex], - data: content, - name: fileName, - type: 'file:full' - } - } - } - } - } else if (isChatFlowAvailableForRAGFileUploads) { - const filesWithRAGUploadType = uploadedFiles.filter((file) => file.type === 'file:rag') - - if (filesWithRAGUploadType.length > 0) { - const formData = new FormData() - for (const file of filesWithRAGUploadType) { - formData.append('files', file.file) - } - formData.append('chatId', chatId) - - await vectorstoreApi.upsertVectorStoreWithFormData(chatflowid, formData) - - // delay for vector store to be updated - const delay = (delayInms) => { - return new Promise((resolve) => setTimeout(resolve, delayInms)) - } - await delay(2500) //TODO: check if embeddings can be retrieved using file name as metadata filter - - uploads = uploads.map((upload) => { - return { - ...upload, - type: 'file:rag' - } - }) - } - } - return uploads - } - - // Handle form submission - const handleSubmit = async (e, selectedInput, action, humanInput) => { - if (e) e.preventDefault() - - if (!selectedInput && userInput.trim() === '') { - const containsFile = previews.filter((item) => !item.mime.startsWith('image') && item.type !== 'audio').length > 0 - if (!previews.length || (previews.length && containsFile)) { - return - } - } - - let input = userInput - - if (typeof selectedInput === 'string') { - if (selectedInput !== undefined && selectedInput.trim() !== '') input = selectedInput - - if (input.trim()) { - inputHistory.addToHistory(input) - } - } else if (typeof selectedInput === 'object') { - input = Object.entries(selectedInput) - .map(([key, value]) => `${key}: ${value}`) - .join('\n') - } - - setLoading(true) - clearAgentflowNodeStatus() - - let uploads = previews.map((item) => { - return { - data: item.data, - type: item.type, - name: item.name, - mime: item.mime - } - }) - - try { - uploads = await handleFileUploads(uploads) - } catch (error) { - handleError('Unable to upload documents') - return - } - - clearPreviews() - setMessages((prevMessages) => [...prevMessages, { message: input, type: 'userMessage', fileUploads: uploads }]) - - // Send user question to Prediction Internal API - try { - const params = { - question: input, - chatId - } - if (typeof selectedInput === 'object') { - params.form = selectedInput - delete params.question - } - if (uploads && uploads.length > 0) params.uploads = uploads - if (leadEmail) params.leadEmail = leadEmail - if (action) params.action = action - if (humanInput) params.humanInput = humanInput - - if (isChatFlowAvailableToStream) { - fetchResponseFromEventStream(chatflowid, params) - } else { - const response = await predictionApi.sendMessageAndGetPrediction(chatflowid, params) - if (response.data) { - const data = response.data - - updateMetadata(data, input) - - let text = '' - if (data.text) text = data.text - else if (data.json) text = '```json\n' + JSON.stringify(data.json, null, 2) - else text = JSON.stringify(data, null, 2) - - setMessages((prevMessages) => [ - ...prevMessages, - { - message: text, - id: data?.chatMessageId, - sourceDocuments: data?.sourceDocuments, - usedTools: data?.usedTools, - calledTools: data?.calledTools, - fileAnnotations: data?.fileAnnotations, - agentReasoning: data?.agentReasoning, - agentFlowExecutedData: data?.agentFlowExecutedData, - action: data?.action, - artifacts: data?.artifacts, - type: 'apiMessage', - feedback: null - } - ]) - - setLocalStorageChatflow(chatflowid, data.chatId) - setLoading(false) - setUserInput('') - setUploadedFiles([]) - - setTimeout(() => { - inputRef.current?.focus() - scrollToBottom() - }, 100) - } - } - } catch (error) { - handleError(error.response.data.message) - return - } - } - - const fetchResponseFromEventStream = async (chatflowid, params) => { - const chatId = params.chatId - const input = params.question - params.streaming = true - await fetchEventSource(`${baseURL}/api/v1/internal-prediction/${chatflowid}`, { - openWhenHidden: true, - method: 'POST', - body: JSON.stringify(params), - headers: { - 'Content-Type': 'application/json', - 'x-request-from': 'internal' - }, - async onopen(response) { - if (response.ok && response.headers.get('content-type') === EventStreamContentType) { - //console.log('EventSource Open') - } - }, - async onmessage(ev) { - const payload = JSON.parse(ev.data) - switch (payload.event) { - case 'start': - setMessages((prevMessages) => [...prevMessages, { message: '', type: 'apiMessage' }]) - break - case 'token': - updateLastMessage(payload.data) - break - case 'sourceDocuments': - updateLastMessageSourceDocuments(payload.data) - break - case 'usedTools': - updateLastMessageUsedTools(payload.data) - break - case 'fileAnnotations': - updateLastMessageFileAnnotations(payload.data) - break - case 'agentReasoning': - updateLastMessageAgentReasoning(payload.data) - break - case 'agentFlowEvent': - updateAgentFlowEvent(payload.data) - break - case 'agentFlowExecutedData': - updateAgentFlowExecutedData(payload.data) - break - case 'artifacts': - updateLastMessageArtifacts(payload.data) - break - case 'action': - updateLastMessageAction(payload.data) - break - case 'nextAgent': - updateLastMessageNextAgent(payload.data) - break - case 'nextAgentFlow': - updateLastMessageNextAgentFlow(payload.data) - break - case 'metadata': - updateMetadata(payload.data, input) - break - case 'error': - updateErrorMessage(payload.data) - break - case 'abort': - abortMessage(payload.data) - closeResponse() - break - case 'tts_start': - handleTTSStart(payload.data) - break - case 'tts_data': - handleTTSDataChunk(payload.data.audioChunk) - break - case 'tts_end': - handleTTSEnd() - break - case 'tts_abort': - handleTTSAbort(payload.data) - break - case 'end': - setLocalStorageChatflow(chatflowid, chatId) - closeResponse() - break - } - }, - async onclose() { - closeResponse() - }, - async onerror(err) { - console.error('EventSource Error: ', err) - closeResponse() - throw err - } - }) - } - - const closeResponse = () => { - setLoading(false) - setUserInput('') - setUploadedFiles([]) - setTimeout(() => { - inputRef.current?.focus() - scrollToBottom() - }, 100) - } - // Prevent blank submissions and allow for multiline input - const handleEnter = (e) => { - // Check if IME composition is in progress - const isIMEComposition = e.isComposing || e.keyCode === 229 - if (e.key === 'ArrowUp' && !isIMEComposition) { - e.preventDefault() - const previousInput = inputHistory.getPreviousInput(userInput) - setUserInput(previousInput) - } else if (e.key === 'ArrowDown' && !isIMEComposition) { - e.preventDefault() - const nextInput = inputHistory.getNextInput() - setUserInput(nextInput) - } else if (e.key === 'Enter' && userInput && !isIMEComposition) { - if (!e.shiftKey && userInput) { - handleSubmit(e) - } - } else if (e.key === 'Enter') { - e.preventDefault() - } - } - - const getLabel = (URL, source) => { - if (URL && typeof URL === 'object') { - if (URL.pathname && typeof URL.pathname === 'string') { - if (URL.pathname.substring(0, 15) === '/') { - return URL.host || '' - } else { - return `${URL.pathname.substring(0, 15)}...` - } - } else if (URL.host) { - return URL.host - } - } - - if (source && source.pageContent && typeof source.pageContent === 'string') { - return `${source.pageContent.substring(0, 15)}...` - } - - return '' - } - - const getFileUploadAllowedTypes = () => { - if (fullFileUpload) { - return fullFileUploadAllowedTypes === '' ? '*' : fullFileUploadAllowedTypes - } - return fileUploadAllowedTypes.includes('*') ? '*' : fileUploadAllowedTypes || '*' - } - - const downloadFile = async (fileAnnotation) => { - try { - const response = await axios.post( - `${baseURL}/api/v1/openai-assistants-file/download`, - { fileName: fileAnnotation.fileName, chatflowId: chatflowid, chatId: chatId }, - { responseType: 'blob' } - ) - const blob = new Blob([response.data], { type: response.headers['content-type'] }) - const downloadUrl = window.URL.createObjectURL(blob) - const link = document.createElement('a') - link.href = downloadUrl - link.download = fileAnnotation.fileName - document.body.appendChild(link) - link.click() - link.remove() - } catch (error) { - console.error('Download failed:', error) - } - } - - const getAgentIcon = (nodeName, instructions) => { - if (nodeName) { - return `${baseURL}/api/v1/node-icon/${nodeName}` - } else if (instructions) { - return multiagent_supervisorPNG - } else { - return multiagent_workerPNG - } - } - - // Get chatmessages successful - useEffect(() => { - if (getChatmessageApi.data?.length) { - const chatId = getChatmessageApi.data[0]?.chatId - setChatId(chatId) - const loadedMessages = getChatmessageApi.data.map((message) => { - const obj = { - id: message.id, - message: message.content, - feedback: message.feedback, - type: message.role - } - if (message.sourceDocuments) obj.sourceDocuments = message.sourceDocuments - if (message.usedTools) obj.usedTools = message.usedTools - if (message.fileAnnotations) obj.fileAnnotations = message.fileAnnotations - if (message.agentReasoning) obj.agentReasoning = message.agentReasoning - if (message.action) obj.action = message.action - if (message.artifacts) { - obj.artifacts = message.artifacts - obj.artifacts.forEach((artifact) => { - if (artifact.type === 'png' || artifact.type === 'jpeg') { - artifact.data = `${baseURL}/api/v1/get-upload-file?chatflowId=${chatflowid}&chatId=${chatId}&fileName=${artifact.data.replace( - 'FILE-STORAGE::', - '' - )}` - } - }) - } - if (message.fileUploads) { - obj.fileUploads = message.fileUploads - obj.fileUploads.forEach((file) => { - if (file.type === 'stored-file') { - file.data = `${baseURL}/api/v1/get-upload-file?chatflowId=${chatflowid}&chatId=${chatId}&fileName=${file.name}` - } - }) - } - if (message.followUpPrompts) obj.followUpPrompts = JSON.parse(message.followUpPrompts) - if (message.role === 'apiMessage' && message.execution && message.execution.executionData) - obj.agentFlowExecutedData = JSON.parse(message.execution.executionData) - return obj - }) - setMessages((prevMessages) => [...prevMessages, ...loadedMessages]) - setLocalStorageChatflow(chatflowid, chatId) - } - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [getChatmessageApi.data]) - - useEffect(() => { - if (getAllExecutionsApi.data?.length) { - const chatId = getAllExecutionsApi.data[0]?.sessionId - setChatId(chatId) - const loadedMessages = getAllExecutionsApi.data.map((execution) => { - const executionData = - typeof execution.executionData === 'string' ? JSON.parse(execution.executionData) : execution.executionData - const obj = { - id: execution.id, - agentFlow: executionData - } - return obj - }) - setMessages((prevMessages) => [...prevMessages, ...loadedMessages]) - setLocalStorageChatflow(chatflowid, chatId) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [getAllExecutionsApi.data]) - - // Get chatflow streaming capability - useEffect(() => { - if (getIsChatflowStreamingApi.data) { - setIsChatFlowAvailableToStream(getIsChatflowStreamingApi.data?.isStreaming ?? false) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [getIsChatflowStreamingApi.data]) - - // Get chatflow uploads capability - useEffect(() => { - if (getAllowChatFlowUploads.data) { - setIsChatFlowAvailableForImageUploads(getAllowChatFlowUploads.data?.isImageUploadAllowed ?? false) - setIsChatFlowAvailableForRAGFileUploads(getAllowChatFlowUploads.data?.isRAGFileUploadAllowed ?? false) - setIsChatFlowAvailableForSpeech(getAllowChatFlowUploads.data?.isSpeechToTextEnabled ?? false) - setImageUploadAllowedTypes(getAllowChatFlowUploads.data?.imgUploadSizeAndTypes.map((allowed) => allowed.fileTypes).join(',')) - setFileUploadAllowedTypes(getAllowChatFlowUploads.data?.fileUploadSizeAndTypes.map((allowed) => allowed.fileTypes).join(',')) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [getAllowChatFlowUploads.data]) - - useEffect(() => { - if (getChatflowConfig.data) { - setIsConfigLoading(false) - if (getChatflowConfig.data?.flowData) { - let nodes = JSON.parse(getChatflowConfig.data?.flowData).nodes ?? [] - const startNode = nodes.find((node) => node.data.name === 'startAgentflow') - if (startNode) { - const startInputType = startNode.data.inputs?.startInputType - setStartInputType(startInputType) - - const formInputTypes = startNode.data.inputs?.formInputTypes - if (startInputType === 'formInput' && formInputTypes && formInputTypes.length > 0) { - for (const formInputType of formInputTypes) { - if (formInputType.type === 'options') { - formInputType.options = formInputType.addOptions.map((option) => ({ - label: option.option, - name: option.option - })) - } - } - setFormInputParams(formInputTypes) - setFormInputsData({ - id: 'formInput', - inputs: {}, - inputParams: formInputTypes - }) - setFormTitle(startNode.data.inputs?.formTitle) - setFormDescription(startNode.data.inputs?.formDescription) - } - - getAllExecutionsApi.request({ agentflowId: chatflowid }) - } - } - - if (getChatflowConfig.data?.chatbotConfig && JSON.parse(getChatflowConfig.data?.chatbotConfig)) { - let config = JSON.parse(getChatflowConfig.data?.chatbotConfig) - if (config.starterPrompts) { - let inputFields = [] - Object.getOwnPropertyNames(config.starterPrompts).forEach((key) => { - if (config.starterPrompts[key]) { - inputFields.push(config.starterPrompts[key]) - } - }) - setStarterPrompts(inputFields.filter((field) => field.prompt !== '')) - } - if (config.chatFeedback) { - setChatFeedbackStatus(config.chatFeedback.status) - } - - if (config.leads) { - setLeadsConfig(config.leads) - if (config.leads.status && !getLocalStorageChatflow(chatflowid).lead) { - setMessages((prevMessages) => { - const leadCaptureMessage = { - message: '', - type: 'leadCaptureMessage' - } - - return [...prevMessages, leadCaptureMessage] - }) - } - } - - if (config.followUpPrompts) { - setFollowUpPromptsStatus(config.followUpPrompts.status) - } - - if (config.fullFileUpload) { - setFullFileUpload(config.fullFileUpload.status) - if (config.fullFileUpload?.allowedUploadFileTypes) { - setFullFileUploadAllowedTypes(config.fullFileUpload?.allowedUploadFileTypes) - } - } - } - } - - // Check if TTS is configured - if (getChatflowConfig.data && getChatflowConfig.data.textToSpeech) { - try { - const ttsConfig = - typeof getChatflowConfig.data.textToSpeech === 'string' - ? JSON.parse(getChatflowConfig.data.textToSpeech) - : getChatflowConfig.data.textToSpeech - - let isEnabled = false - if (ttsConfig) { - Object.keys(ttsConfig).forEach((provider) => { - if (provider !== 'none' && ttsConfig?.[provider]?.status) { - isEnabled = true - } - }) - } - setIsTTSEnabled(isEnabled) - } catch (error) { - setIsTTSEnabled(false) - } - } else { - setIsTTSEnabled(false) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [getChatflowConfig.data]) - - useEffect(() => { - if (getChatflowConfig.error) { - setIsConfigLoading(false) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [getChatflowConfig.error]) - - useEffect(() => { - if (fullFileUpload) { - setIsChatFlowAvailableForFileUploads(true) - } else if (isChatFlowAvailableForRAGFileUploads) { - setIsChatFlowAvailableForFileUploads(true) - } else { - setIsChatFlowAvailableForFileUploads(false) - } - }, [isChatFlowAvailableForRAGFileUploads, fullFileUpload]) - - // Auto scroll chat to bottom (but not during TTS actions) - useEffect(() => { - if (!isTTSActionRef.current) { - scrollToBottom() - } - }, [messages]) - - useEffect(() => { - if (isDialog && inputRef) { - setTimeout(() => { - inputRef.current?.focus() - }, 100) - } - }, [isDialog, inputRef]) - - useEffect(() => { - if (open && chatflowid) { - // API request - getChatmessageApi.request(chatflowid) - getIsChatflowStreamingApi.request(chatflowid) - getAllowChatFlowUploads.request(chatflowid) - getChatflowConfig.request(chatflowid) - - // Add a small delay to ensure content is rendered before scrolling - setTimeout(() => { - scrollToBottom() - }, 100) - - setIsRecording(false) - setIsConfigLoading(true) - - // leads - const savedLead = getLocalStorageChatflow(chatflowid)?.lead - if (savedLead) { - setIsLeadSaved(!!savedLead) - setLeadEmail(savedLead.email) - } - } - - return () => { - setUserInput('') - setUploadedFiles([]) - setLoading(false) - setMessages([ - { - message: 'Hi there! How can I help?', - type: 'apiMessage' - } - ]) - } - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [open, chatflowid]) - - useEffect(() => { - // wait for audio recording to load and then send - const containsAudio = previews.filter((item) => item.type === 'audio').length > 0 - if (previews.length >= 1 && containsAudio) { - setIsRecording(false) - setRecordingNotSupported(false) - handlePromptClick('') - } - // eslint-disable-next-line - }, [previews]) - - useEffect(() => { - if (followUpPromptsStatus && messages.length > 0) { - const lastMessage = messages[messages.length - 1] - if (lastMessage.type === 'apiMessage' && lastMessage.followUpPrompts) { - if (Array.isArray(lastMessage.followUpPrompts)) { - setFollowUpPrompts(lastMessage.followUpPrompts) - } - if (typeof lastMessage.followUpPrompts === 'string') { - const followUpPrompts = JSON.parse(lastMessage.followUpPrompts) - setFollowUpPrompts(followUpPrompts) - } - } else if (lastMessage.type === 'userMessage') { - setFollowUpPrompts([]) - } - } - }, [followUpPromptsStatus, messages]) - - const copyMessageToClipboard = async (text) => { - try { - await navigator.clipboard.writeText(text || '') - } catch (error) { - console.error('Error copying to clipboard:', error) - } - } - - const onThumbsUpClick = async (messageId) => { - const body = { - chatflowid, - chatId, - messageId, - rating: 'THUMBS_UP', - content: '' - } - const result = await chatmessagefeedbackApi.addFeedback(chatflowid, body) - if (result.data) { - const data = result.data - let id = '' - if (data && data.id) id = data.id - setMessages((prevMessages) => { - const allMessages = [...cloneDeep(prevMessages)] - return allMessages.map((message) => { - if (message.id === messageId) { - message.feedback = { - rating: 'THUMBS_UP' - } - } - return message - }) - }) - setFeedbackId(id) - setShowFeedbackContentDialog(true) - } - } + const [isHovered, setIsHovered] = useState(false); + const defaultBackgroundColor = customization.isDarkMode ? 'rgba(0, 0, 0, 0.3)' : 'transparent'; + + return ( +
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} style={{ position: 'relative', display: 'inline-block' }}> + + + + {item.name} + + + {isHovered && !disabled && ( + + )} +
+ ); +}; - const onThumbsDownClick = async (messageId) => { - const body = { - chatflowid, - chatId, - messageId, - rating: 'THUMBS_DOWN', - content: '' - } - const result = await chatmessagefeedbackApi.addFeedback(chatflowid, body) - if (result.data) { - const data = result.data - let id = '' - if (data && data.id) id = data.id - setMessages((prevMessages) => { - const allMessages = [...cloneDeep(prevMessages)] - return allMessages.map((message) => { - if (message.id === messageId) { - message.feedback = { - rating: 'THUMBS_DOWN' - } - } - return message - }) - }) - setFeedbackId(id) - setShowFeedbackContentDialog(true) - } - } +CardWithDeleteOverlay.propTypes = { + item: PropTypes.object, + customization: PropTypes.object, + disabled: PropTypes.bool, + onDelete: PropTypes.func, +}; - const submitFeedbackContent = async (text) => { - const body = { - content: text +const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, previews, setPreviews }) => { + const theme = useTheme(); + const customization = useSelector((state) => state.customization); + + const ps = useRef(); + + const dispatch = useDispatch(); + const { onAgentflowNodeStatusUpdate, clearAgentflowNodeStatus } = useContext(flowContext); + + useNotifier(); + const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)); + const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)); + + const [userInput, setUserInput] = useState(''); + const [loading, setLoading] = useState(false); + const [messages, setMessages] = useState([ + { + message: 'Hi there! How can I help?', + type: 'apiMessage', + }, + ]); + const [isChatFlowAvailableToStream, setIsChatFlowAvailableToStream] = useState(false); + const [isChatFlowAvailableForSpeech, setIsChatFlowAvailableForSpeech] = useState(false); + const [sourceDialogOpen, setSourceDialogOpen] = useState(false); + const [sourceDialogProps, setSourceDialogProps] = useState({}); + const [chatId, setChatId] = useState(uuidv4()); + const [isMessageStopping, setIsMessageStopping] = useState(false); + const [uploadedFiles, setUploadedFiles] = useState([]); + const [imageUploadAllowedTypes, setImageUploadAllowedTypes] = useState(''); + const [fileUploadAllowedTypes, setFileUploadAllowedTypes] = useState(''); + const [inputHistory] = useState(new ChatInputHistory(10)); + + const inputRef = useRef(null); + const getChatmessageApi = useApi(chatmessageApi.getInternalChatmessageFromChatflow); + const getAllExecutionsApi = useApi(executionsApi.getAllExecutions); + const getIsChatflowStreamingApi = useApi(chatflowsApi.getIsChatflowStreaming); + const getAllowChatFlowUploads = useApi(chatflowsApi.getAllowChatflowUploads); + const getChatflowConfig = useApi(chatflowsApi.getSpecificChatflow); + + const [starterPrompts, setStarterPrompts] = useState([]); + + // full file upload + const [fullFileUpload, setFullFileUpload] = useState(false); + const [fullFileUploadAllowedTypes, setFullFileUploadAllowedTypes] = useState('*'); + + // feedback + const [chatFeedbackStatus, setChatFeedbackStatus] = useState(false); + const [feedbackId, setFeedbackId] = useState(''); + const [showFeedbackContentDialog, setShowFeedbackContentDialog] = useState(false); + + // leads + const [leadsConfig, setLeadsConfig] = useState(null); + const [leadName, setLeadName] = useState(''); + const [leadEmail, setLeadEmail] = useState(''); + const [leadPhone, setLeadPhone] = useState(''); + const [isLeadSaving, setIsLeadSaving] = useState(false); + const [isLeadSaved, setIsLeadSaved] = useState(false); + + // follow-up prompts + const [followUpPromptsStatus, setFollowUpPromptsStatus] = useState(false); + const [followUpPrompts, setFollowUpPrompts] = useState([]); + + // drag & drop and file input + const imgUploadRef = useRef(null); + const fileUploadRef = useRef(null); + const [isChatFlowAvailableForImageUploads, setIsChatFlowAvailableForImageUploads] = useState(false); + const [isChatFlowAvailableForFileUploads, setIsChatFlowAvailableForFileUploads] = useState(false); + const [isChatFlowAvailableForRAGFileUploads, setIsChatFlowAvailableForRAGFileUploads] = useState(false); + const [isDragActive, setIsDragActive] = useState(false); + + // recording + const [isRecording, setIsRecording] = useState(false); + const [recordingNotSupported, setRecordingNotSupported] = useState(false); + const [isLoadingRecording, setIsLoadingRecording] = useState(false); + + const [openFeedbackDialog, setOpenFeedbackDialog] = useState(false); + const [feedback, setFeedback] = useState(''); + const [pendingActionData, setPendingActionData] = useState(null); + const [feedbackType, setFeedbackType] = useState(''); + + // start input type + const [startInputType, setStartInputType] = useState(''); + const [formTitle, setFormTitle] = useState(''); + const [formDescription, setFormDescription] = useState(''); + const [formInputsData, setFormInputsData] = useState({}); + const [formInputParams, setFormInputParams] = useState([]); + + const [isConfigLoading, setIsConfigLoading] = useState(true); + + // TTS state + const [isTTSLoading, setIsTTSLoading] = useState({}); + const [isTTSPlaying, setIsTTSPlaying] = useState({}); + const [ttsAudio, setTtsAudio] = useState({}); + const [isTTSEnabled, setIsTTSEnabled] = useState(false); + + // TTS streaming state + const [ttsStreamingState, setTtsStreamingState] = useState({ + mediaSource: null, + sourceBuffer: null, + audio: null, + chunkQueue: [], + isBuffering: false, + audioFormat: null, + abortController: null, + }); + + // Ref to prevent auto-scroll during TTS actions (using ref to avoid re-renders) + const isTTSActionRef = useRef(false); + const ttsTimeoutRef = useRef(null); + + const isFileAllowedForUpload = (file) => { + const constraints = getAllowChatFlowUploads.data; + /** + * {isImageUploadAllowed: boolean, imgUploadSizeAndTypes: Array<{ fileTypes: string[], maxUploadSize: number }>} + */ + let acceptFile = false; + + // Early return if constraints are not available yet + if (!constraints) { + console.warn('Upload constraints not loaded yet'); + return false; + } + + if (constraints.isImageUploadAllowed) { + const fileType = file.type; + const sizeInMB = file.size / 1024 / 1024; + if (constraints.imgUploadSizeAndTypes && Array.isArray(constraints.imgUploadSizeAndTypes)) { + constraints.imgUploadSizeAndTypes.forEach((allowed) => { + if (allowed.fileTypes && allowed.fileTypes.includes(fileType) && sizeInMB <= allowed.maxUploadSize) { + acceptFile = true; + } + }); + } + } + + if (fullFileUpload) { + return true; + } else if (constraints.isRAGFileUploadAllowed) { + const fileExt = file.name.split('.').pop(); + if (fileExt && constraints.fileUploadSizeAndTypes && Array.isArray(constraints.fileUploadSizeAndTypes)) { + constraints.fileUploadSizeAndTypes.forEach((allowed) => { + if (allowed.fileTypes && allowed.fileTypes.length === 1 && allowed.fileTypes[0] === '*') { + acceptFile = true; + } else if (allowed.fileTypes && allowed.fileTypes.includes(`.${fileExt}`)) { + acceptFile = true; + } + }); + } + } + if (!acceptFile) { + alert(`Cannot upload file. Kindly check the allowed file types and maximum allowed size.`); + } + return acceptFile; + }; + + const handleDrop = async (e) => { + if (!isChatFlowAvailableForImageUploads && !isChatFlowAvailableForFileUploads) { + return; + } + e.preventDefault(); + setIsDragActive(false); + let files = []; + let uploadedFiles = []; + + if (e.dataTransfer.files.length > 0) { + for (const file of e.dataTransfer.files) { + if (isFileAllowedForUpload(file) === false) { + return; + } + const reader = new FileReader(); + const { name } = file; + // Only add files + if (!file.type || !imageUploadAllowedTypes.includes(file.type)) { + uploadedFiles.push({ file, type: fullFileUpload ? 'file:full' : 'file:rag' }); + } + files.push( + new Promise((resolve) => { + reader.onload = (evt) => { + if (!evt?.target?.result) { + return; + } + const { result } = evt.target; + let previewUrl; + if (file.type.startsWith('audio/')) { + previewUrl = audioUploadSVG; + } else { + previewUrl = URL.createObjectURL(file); + } + resolve({ + data: result, + preview: previewUrl, + type: 'file', + name: name, + mime: file.type, + }); + }; + reader.readAsDataURL(file); + }), + ); + } + + const newFiles = await Promise.all(files); + setUploadedFiles(uploadedFiles); + setPreviews((prevPreviews) => [...prevPreviews, ...newFiles]); + } + + if (e.dataTransfer.items) { + //TODO set files + for (const item of e.dataTransfer.items) { + if (item.kind === 'string' && item.type.match('^text/uri-list')) { + item.getAsString((s) => { + let upload = { + data: s, + preview: s, + type: 'url', + name: s ? s.substring(s.lastIndexOf('/') + 1) : '', + }; + setPreviews((prevPreviews) => [...prevPreviews, upload]); + }); + } else if (item.kind === 'string' && item.type.match('^text/html')) { + item.getAsString((s) => { + if (s.indexOf('href') === -1) return; + //extract href + let start = s ? s.substring(s.indexOf('href') + 6) : ''; + let hrefStr = start.substring(0, start.indexOf('"')); + + let upload = { + data: hrefStr, + preview: hrefStr, + type: 'url', + name: hrefStr ? hrefStr.substring(hrefStr.lastIndexOf('/') + 1) : '', + }; + setPreviews((prevPreviews) => [...prevPreviews, upload]); + }); + } + } + } + }; + + const handleFileChange = async (event) => { + const fileObj = event.target.files && event.target.files[0]; + if (!fileObj) { + return; + } + let files = []; + let uploadedFiles = []; + for (const file of event.target.files) { + if (isFileAllowedForUpload(file) === false) { + return; + } + // Only add files + if (!file.type || !imageUploadAllowedTypes.includes(file.type)) { + uploadedFiles.push({ file, type: fullFileUpload ? 'file:full' : 'file:rag' }); + } + const reader = new FileReader(); + const { name } = file; + files.push( + new Promise((resolve) => { + reader.onload = (evt) => { + if (!evt?.target?.result) { + return; + } + const { result } = evt.target; + resolve({ + data: result, + preview: URL.createObjectURL(file), + type: 'file', + name: name, + mime: file.type, + }); + }; + reader.readAsDataURL(file); + }), + ); + } + + const newFiles = await Promise.all(files); + setUploadedFiles(uploadedFiles); + setPreviews((prevPreviews) => [...prevPreviews, ...newFiles]); + // 👇️ reset file input + event.target.value = null; + }; + + const addRecordingToPreviews = (blob) => { + let mimeType = ''; + const pos = blob.type.indexOf(';'); + if (pos === -1) { + mimeType = blob.type; + } else { + mimeType = blob.type ? blob.type.substring(0, pos) : ''; + } + // read blob and add to previews + const reader = new FileReader(); + reader.readAsDataURL(blob); + reader.onloadend = () => { + const base64data = reader.result; + const upload = { + data: base64data, + preview: audioUploadSVG, + type: 'audio', + name: `audio_${Date.now()}.wav`, + mime: mimeType, + }; + setPreviews((prevPreviews) => [...prevPreviews, upload]); + }; + }; + + const handleDrag = (e) => { + if (isChatFlowAvailableForImageUploads || isChatFlowAvailableForFileUploads) { + e.preventDefault(); + e.stopPropagation(); + if (e.type === 'dragenter' || e.type === 'dragover') { + setIsDragActive(true); + } else if (e.type === 'dragleave') { + setIsDragActive(false); + } + } + }; + + const handleAbort = async () => { + setIsMessageStopping(true); + try { + // Stop all TTS streams first + stopAllTTS(); + + // Abort TTS for any active streams + const activeTTSMessages = Object.keys(isTTSLoading).concat(Object.keys(isTTSPlaying)); + for (const messageId of activeTTSMessages) { + await ttsApi.abortTTS({ chatflowId: chatflowid, chatId, chatMessageId: messageId }); + } + + await chatmessageApi.abortMessage(chatflowid, chatId); + } catch (error) { + setIsMessageStopping(false); + enqueueSnackbar({ + message: typeof error.response.data === 'object' ? error.response.data.message : error.response.data, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ), + }, + }); + } + }; + + const handleDeletePreview = (itemToDelete) => { + if (itemToDelete.type === 'file') { + URL.revokeObjectURL(itemToDelete.preview); // Clean up for file + } + setPreviews(previews.filter((item) => item !== itemToDelete)); + }; + + const handleFileUploadClick = () => { + // 👇️ open file input box on click of another element + fileUploadRef.current.click(); + }; + + const handleImageUploadClick = () => { + // 👇️ open file input box on click of another element + imgUploadRef.current.click(); + }; + + const clearPreviews = () => { + // Revoke the data uris to avoid memory leaks + previews.forEach((file) => URL.revokeObjectURL(file.preview)); + setPreviews([]); + }; + + const onMicrophonePressed = () => { + setIsRecording(true); + startAudioRecording(setIsRecording, setRecordingNotSupported); + }; + + const onRecordingCancelled = () => { + if (!recordingNotSupported) cancelAudioRecording(); + setIsRecording(false); + setRecordingNotSupported(false); + }; + + const onRecordingStopped = async () => { + setIsLoadingRecording(true); + stopAudioRecording(addRecordingToPreviews); + }; + + const onSourceDialogClick = (data, title) => { + setSourceDialogProps({ data, title }); + setSourceDialogOpen(true); + }; + + const onURLClick = (data) => { + window.open(data, '_blank'); + }; + + const scrollToBottom = () => { + if (ps.current) { + ps.current.scrollTo({ top: maxScroll }); + } + }; + + // Helper function to manage TTS action flag + const setTTSAction = (isActive) => { + isTTSActionRef.current = isActive; + if (ttsTimeoutRef.current) { + clearTimeout(ttsTimeoutRef.current); + ttsTimeoutRef.current = null; + } + if (isActive) { + // Reset the flag after a longer delay to ensure all state changes are complete + ttsTimeoutRef.current = setTimeout(() => { + isTTSActionRef.current = false; + ttsTimeoutRef.current = null; + }, 300); + } + }; + + const onChange = useCallback((e) => setUserInput(e.target.value), [setUserInput]); + + const updateLastMessage = (text) => { + setMessages((prevMessages) => { + let allMessages = [...cloneDeep(prevMessages)]; + if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; + allMessages[allMessages.length - 1].message += text; + allMessages[allMessages.length - 1].feedback = null; + return allMessages; + }); + }; + + const updateErrorMessage = (errorMessage) => { + setMessages((prevMessages) => { + let allMessages = [...cloneDeep(prevMessages)]; + allMessages.push({ message: errorMessage, type: 'apiMessage' }); + return allMessages; + }); + }; + + const updateLastMessageSourceDocuments = (sourceDocuments) => { + setMessages((prevMessages) => { + let allMessages = [...cloneDeep(prevMessages)]; + if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; + allMessages[allMessages.length - 1].sourceDocuments = sourceDocuments; + return allMessages; + }); + }; + + const updateLastMessageAgentReasoning = (agentReasoning) => { + setMessages((prevMessages) => { + let allMessages = [...cloneDeep(prevMessages)]; + if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; + allMessages[allMessages.length - 1].agentReasoning = agentReasoning; + return allMessages; + }); + }; + + const updateAgentFlowEvent = (event) => { + if (event === 'INPROGRESS') { + setMessages((prevMessages) => [...prevMessages, { message: '', type: 'apiMessage', agentFlowEventStatus: event }]); + } else { + setMessages((prevMessages) => { + let allMessages = [...cloneDeep(prevMessages)]; + if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; + allMessages[allMessages.length - 1].agentFlowEventStatus = event; + return allMessages; + }); + } + }; + + const updateAgentFlowExecutedData = (agentFlowExecutedData) => { + setMessages((prevMessages) => { + let allMessages = [...cloneDeep(prevMessages)]; + if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; + allMessages[allMessages.length - 1].agentFlowExecutedData = agentFlowExecutedData; + return allMessages; + }); + }; + + const updateLastMessageAction = (action) => { + setMessages((prevMessages) => { + let allMessages = [...cloneDeep(prevMessages)]; + if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; + allMessages[allMessages.length - 1].action = action; + return allMessages; + }); + }; + + const updateLastMessageArtifacts = (artifacts) => { + artifacts.forEach((artifact) => { + if (artifact.type === 'png' || artifact.type === 'jpeg') { + artifact.data = `${baseURL}/api/v1/get-upload-file?chatflowId=${chatflowid}&chatId=${chatId}&fileName=${artifact.data.replace( + 'FILE-STORAGE::', + '', + )}`; + } + }); + setMessages((prevMessages) => { + let allMessages = [...cloneDeep(prevMessages)]; + if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; + allMessages[allMessages.length - 1].artifacts = artifacts; + return allMessages; + }); + }; + + const updateLastMessageNextAgent = (nextAgent) => { + setMessages((prevMessages) => { + let allMessages = [...cloneDeep(prevMessages)]; + if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; + const lastAgentReasoning = allMessages[allMessages.length - 1].agentReasoning; + if (lastAgentReasoning && lastAgentReasoning.length > 0) { + lastAgentReasoning.push({ nextAgent }); + } + allMessages[allMessages.length - 1].agentReasoning = lastAgentReasoning; + return allMessages; + }); + }; + + const updateLastMessageNextAgentFlow = (nextAgentFlow) => { + onAgentflowNodeStatusUpdate(nextAgentFlow); + }; + + const updateLastMessageUsedTools = (usedTools) => { + setMessages((prevMessages) => { + let allMessages = [...cloneDeep(prevMessages)]; + if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; + allMessages[allMessages.length - 1].usedTools = usedTools; + return allMessages; + }); + }; + + const updateLastMessageFileAnnotations = (fileAnnotations) => { + setMessages((prevMessages) => { + let allMessages = [...cloneDeep(prevMessages)]; + if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; + allMessages[allMessages.length - 1].fileAnnotations = fileAnnotations; + return allMessages; + }); + }; + + const abortMessage = () => { + setIsMessageStopping(false); + setMessages((prevMessages) => { + let allMessages = [...cloneDeep(prevMessages)]; + if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; + const lastAgentReasoning = allMessages[allMessages.length - 1].agentReasoning; + if (lastAgentReasoning && lastAgentReasoning.length > 0) { + allMessages[allMessages.length - 1].agentReasoning = lastAgentReasoning.filter((reasoning) => !reasoning.nextAgent); + } + return allMessages; + }); + setTimeout(() => { + inputRef.current?.focus(); + }, 100); + enqueueSnackbar({ + message: 'Message stopped', + options: { + key: new Date().getTime() + Math.random(), + variant: 'success', + action: (key) => ( + + ), + }, + }); + }; + + const handleError = (message = 'Oops! There seems to be an error. Please try again.') => { + message = message.replace(`Unable to parse JSON response from chat agent.\n\n`, ''); + setMessages((prevMessages) => [...prevMessages, { message, type: 'apiMessage' }]); + setLoading(false); + setUserInput(''); + setUploadedFiles([]); + setTimeout(() => { + inputRef.current?.focus(); + }, 100); + }; + + const handlePromptClick = async (promptStarterInput) => { + setUserInput(promptStarterInput); + handleSubmit(undefined, promptStarterInput); + }; + + const handleFollowUpPromptClick = async (promptStarterInput) => { + setUserInput(promptStarterInput); + setFollowUpPrompts([]); + handleSubmit(undefined, promptStarterInput); + }; + + const onSubmitResponse = (actionData, feedback = '', type = '') => { + let fbType = feedbackType; + if (type) { + fbType = type; + } + const question = feedback ? feedback : fbType.charAt(0).toUpperCase() + fbType.slice(1); + handleSubmit(undefined, question, undefined, { + type: fbType, + startNodeId: actionData?.nodeId, + feedback, + }); + }; + + const handleSubmitFeedback = () => { + if (pendingActionData) { + onSubmitResponse(pendingActionData, feedback); + setOpenFeedbackDialog(false); + setFeedback(''); + setPendingActionData(null); + setFeedbackType(''); + } + }; + + const handleActionClick = async (elem, action) => { + setUserInput(elem.label); + setMessages((prevMessages) => { + let allMessages = [...cloneDeep(prevMessages)]; + if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; + allMessages[allMessages.length - 1].action = null; + return allMessages; + }); + if (elem.type.includes('agentflowv2')) { + const type = elem.type.includes('approve') ? 'proceed' : 'reject'; + setFeedbackType(type); + + if (action.data && action.data.input && action.data.input.humanInputEnableFeedback) { + setPendingActionData(action.data); + setOpenFeedbackDialog(true); + } else { + onSubmitResponse(action.data, '', type); + } + } else { + handleSubmit(undefined, elem.label, action); + } + }; + + const updateMetadata = (data, input) => { + // set message id that is needed for feedback + if (data.chatMessageId) { + setMessages((prevMessages) => { + let allMessages = [...cloneDeep(prevMessages)]; + if (allMessages[allMessages.length - 1].type === 'apiMessage') { + allMessages[allMessages.length - 1].id = data.chatMessageId; + } + return allMessages; + }); + } + + if (data.chatId) { + setChatId(data.chatId); + } + + if (input === '' && data.question) { + // the response contains the question even if it was in an audio format + // so if input is empty but the response contains the question, update the user message to show the question + setMessages((prevMessages) => { + let allMessages = [...cloneDeep(prevMessages)]; + if (allMessages[allMessages.length - 2].type === 'apiMessage') return allMessages; + allMessages[allMessages.length - 2].message = data.question; + return allMessages; + }); + } + + if (data.followUpPrompts) { + const followUpPrompts = JSON.parse(data.followUpPrompts); + if (typeof followUpPrompts === 'string') { + setFollowUpPrompts(JSON.parse(followUpPrompts)); + } else { + setFollowUpPrompts(followUpPrompts); + } + } + }; + + const handleFileUploads = async (uploads) => { + if (!uploadedFiles.length) return uploads; + + if (fullFileUpload) { + const filesWithFullUploadType = uploadedFiles.filter((file) => file.type === 'file:full'); + if (filesWithFullUploadType.length > 0) { + const formData = new FormData(); + for (const file of filesWithFullUploadType) { + formData.append('files', file.file); + } + formData.append('chatId', chatId); + + const response = await attachmentsApi.createAttachment(chatflowid, chatId, formData); + const data = response.data; + + for (const extractedFileData of data) { + const content = extractedFileData.content; + const fileName = extractedFileData.name; + + // find matching name in previews and replace data with content + const uploadIndex = uploads.findIndex((upload) => upload.name === fileName); + + if (uploadIndex !== -1) { + uploads[uploadIndex] = { + ...uploads[uploadIndex], + data: content, + name: fileName, + type: 'file:full', + }; + } + } + } + } else if (isChatFlowAvailableForRAGFileUploads) { + const filesWithRAGUploadType = uploadedFiles.filter((file) => file.type === 'file:rag'); + + if (filesWithRAGUploadType.length > 0) { + const formData = new FormData(); + for (const file of filesWithRAGUploadType) { + formData.append('files', file.file); + } + formData.append('chatId', chatId); + + await vectorstoreApi.upsertVectorStoreWithFormData(chatflowid, formData); + + // delay for vector store to be updated + const delay = (delayInms) => { + return new Promise((resolve) => setTimeout(resolve, delayInms)); + }; + await delay(2500); //TODO: check if embeddings can be retrieved using file name as metadata filter + + uploads = uploads.map((upload) => { + return { + ...upload, + type: 'file:rag', + }; + }); + } + } + return uploads; + }; + + // Handle form submission + const handleSubmit = async (e, selectedInput, action, humanInput) => { + if (e) e.preventDefault(); + + if (!selectedInput && userInput.trim() === '') { + const containsFile = previews.filter((item) => !item.mime.startsWith('image') && item.type !== 'audio').length > 0; + if (!previews.length || (previews.length && containsFile)) { + return; + } + } + + let input = userInput; + + if (typeof selectedInput === 'string') { + if (selectedInput !== undefined && selectedInput.trim() !== '') input = selectedInput; + + if (input.trim()) { + inputHistory.addToHistory(input); + } + } else if (typeof selectedInput === 'object') { + input = Object.entries(selectedInput) + .map(([key, value]) => `${key}: ${value}`) + .join('\n'); + } + + setLoading(true); + clearAgentflowNodeStatus(); + + let uploads = previews.map((item) => { + return { + data: item.data, + type: item.type, + name: item.name, + mime: item.mime, + }; + }); + + try { + uploads = await handleFileUploads(uploads); + } catch (error) { + handleError('Unable to upload documents'); + return; + } + + clearPreviews(); + setMessages((prevMessages) => [...prevMessages, { message: input, type: 'userMessage', fileUploads: uploads }]); + + // Send user question to Prediction Internal API + try { + const params = { + question: input, + chatId, + }; + if (typeof selectedInput === 'object') { + params.form = selectedInput; + delete params.question; + } + if (uploads && uploads.length > 0) params.uploads = uploads; + if (leadEmail) params.leadEmail = leadEmail; + if (action) params.action = action; + if (humanInput) params.humanInput = humanInput; + + if (isChatFlowAvailableToStream) { + fetchResponseFromEventStream(chatflowid, params); + } else { + const response = await predictionApi.sendMessageAndGetPrediction(chatflowid, params); + if (response.data) { + const data = response.data; + + updateMetadata(data, input); + + let text = ''; + if (data.text) text = data.text; + else if (data.json) text = '```json\n' + JSON.stringify(data.json, null, 2); + else text = JSON.stringify(data, null, 2); + + setMessages((prevMessages) => [ + ...prevMessages, + { + message: text, + id: data?.chatMessageId, + sourceDocuments: data?.sourceDocuments, + usedTools: data?.usedTools, + calledTools: data?.calledTools, + fileAnnotations: data?.fileAnnotations, + agentReasoning: data?.agentReasoning, + agentFlowExecutedData: data?.agentFlowExecutedData, + action: data?.action, + artifacts: data?.artifacts, + type: 'apiMessage', + feedback: null, + }, + ]); + + setLocalStorageChatflow(chatflowid, data.chatId); + setLoading(false); + setUserInput(''); + setUploadedFiles([]); + + setTimeout(() => { + inputRef.current?.focus(); + scrollToBottom(); + }, 100); + } + } + } catch (error) { + handleError(error.response.data.message); + return; + } + }; + + const fetchResponseFromEventStream = async (chatflowid, params) => { + const chatId = params.chatId; + const input = params.question; + params.streaming = true; + await fetchEventSource(`${baseURL}/api/v1/internal-prediction/${chatflowid}`, { + openWhenHidden: true, + method: 'POST', + body: JSON.stringify(params), + headers: { + 'Content-Type': 'application/json', + 'x-request-from': 'internal', + }, + async onopen(response) { + if (response.ok && response.headers.get('content-type') === EventStreamContentType) { + //console.log('EventSource Open') + } + }, + async onmessage(ev) { + const payload = JSON.parse(ev.data); + switch (payload.event) { + case 'start': + setMessages((prevMessages) => [...prevMessages, { message: '', type: 'apiMessage' }]); + break; + case 'token': + updateLastMessage(payload.data); + break; + case 'sourceDocuments': + updateLastMessageSourceDocuments(payload.data); + break; + case 'usedTools': + updateLastMessageUsedTools(payload.data); + break; + case 'fileAnnotations': + updateLastMessageFileAnnotations(payload.data); + break; + case 'agentReasoning': + updateLastMessageAgentReasoning(payload.data); + break; + case 'agentFlowEvent': + updateAgentFlowEvent(payload.data); + break; + case 'agentFlowExecutedData': + updateAgentFlowExecutedData(payload.data); + break; + case 'artifacts': + updateLastMessageArtifacts(payload.data); + break; + case 'action': + updateLastMessageAction(payload.data); + break; + case 'nextAgent': + updateLastMessageNextAgent(payload.data); + break; + case 'nextAgentFlow': + updateLastMessageNextAgentFlow(payload.data); + break; + case 'metadata': + updateMetadata(payload.data, input); + break; + case 'error': + updateErrorMessage(payload.data); + break; + case 'abort': + abortMessage(payload.data); + closeResponse(); + break; + case 'tts_start': + handleTTSStart(payload.data); + break; + case 'tts_data': + handleTTSDataChunk(payload.data.audioChunk); + break; + case 'tts_end': + handleTTSEnd(); + break; + case 'tts_abort': + handleTTSAbort(payload.data); + break; + case 'end': + setLocalStorageChatflow(chatflowid, chatId); + closeResponse(); + break; + } + }, + async onclose() { + closeResponse(); + }, + async onerror(err) { + console.error('EventSource Error: ', err); + closeResponse(); + throw err; + }, + }); + }; + + const closeResponse = () => { + setLoading(false); + setUserInput(''); + setUploadedFiles([]); + setTimeout(() => { + inputRef.current?.focus(); + scrollToBottom(); + }, 100); + }; + // Prevent blank submissions and allow for multiline input + const handleEnter = (e) => { + // Check if IME composition is in progress + const isIMEComposition = e.isComposing || e.keyCode === 229; + if (e.key === 'ArrowUp' && !isIMEComposition) { + e.preventDefault(); + const previousInput = inputHistory.getPreviousInput(userInput); + setUserInput(previousInput); + } else if (e.key === 'ArrowDown' && !isIMEComposition) { + e.preventDefault(); + const nextInput = inputHistory.getNextInput(); + setUserInput(nextInput); + } else if (e.key === 'Enter' && userInput && !isIMEComposition) { + if (!e.shiftKey && userInput) { + handleSubmit(e); + } + } else if (e.key === 'Enter') { + e.preventDefault(); + } + }; + + const getLabel = (URL, source) => { + if (URL && typeof URL === 'object') { + if (URL.pathname && typeof URL.pathname === 'string') { + if (URL.pathname.substring(0, 15) === '/') { + return URL.host || ''; + } else { + return `${URL.pathname.substring(0, 15)}...`; + } + } else if (URL.host) { + return URL.host; + } + } + + if (source && source.pageContent && typeof source.pageContent === 'string') { + return `${source.pageContent.substring(0, 15)}...`; + } + + return ''; + }; + + const getFileUploadAllowedTypes = () => { + if (fullFileUpload) { + return fullFileUploadAllowedTypes === '' ? '*' : fullFileUploadAllowedTypes; + } + return fileUploadAllowedTypes.includes('*') ? '*' : fileUploadAllowedTypes || '*'; + }; + + const downloadFile = async (fileAnnotation) => { + try { + const response = await axios.post( + `${baseURL}/api/v1/openai-assistants-file/download`, + { fileName: fileAnnotation.fileName, chatflowId: chatflowid, chatId: chatId }, + { responseType: 'blob' }, + ); + const blob = new Blob([response.data], { type: response.headers['content-type'] }); + const downloadUrl = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = downloadUrl; + link.download = fileAnnotation.fileName; + document.body.appendChild(link); + link.click(); + link.remove(); + } catch (error) { + console.error('Download failed:', error); + } + }; + + const getAgentIcon = (nodeName, instructions) => { + if (nodeName) { + return `${baseURL}/api/v1/node-icon/${nodeName}`; + } else if (instructions) { + return multiagent_supervisorPNG; + } else { + return multiagent_workerPNG; + } + }; + + // Get chatmessages successful + useEffect(() => { + if (getChatmessageApi.data?.length) { + const chatId = getChatmessageApi.data[0]?.chatId; + setChatId(chatId); + const loadedMessages = getChatmessageApi.data.map((message) => { + const obj = { + id: message.id, + message: message.content, + feedback: message.feedback, + type: message.role, + }; + if (message.sourceDocuments) obj.sourceDocuments = message.sourceDocuments; + if (message.usedTools) obj.usedTools = message.usedTools; + if (message.fileAnnotations) obj.fileAnnotations = message.fileAnnotations; + if (message.agentReasoning) obj.agentReasoning = message.agentReasoning; + if (message.action) obj.action = message.action; + if (message.artifacts) { + obj.artifacts = message.artifacts; + obj.artifacts.forEach((artifact) => { + if (artifact.type === 'png' || artifact.type === 'jpeg') { + artifact.data = `${baseURL}/api/v1/get-upload-file?chatflowId=${chatflowid}&chatId=${chatId}&fileName=${artifact.data.replace( + 'FILE-STORAGE::', + '', + )}`; + } + }); } - const result = await chatmessagefeedbackApi.updateFeedback(feedbackId, body) - if (result.data) { - setFeedbackId('') - setShowFeedbackContentDialog(false) + if (message.fileUploads) { + obj.fileUploads = message.fileUploads; + obj.fileUploads.forEach((file) => { + if (file.type === 'stored-file') { + file.data = `${baseURL}/api/v1/get-upload-file?chatflowId=${chatflowid}&chatId=${chatId}&fileName=${file.name}`; + } + }); + } + if (message.followUpPrompts) obj.followUpPrompts = JSON.parse(message.followUpPrompts); + if (message.role === 'apiMessage' && message.execution && message.execution.executionData) + obj.agentFlowExecutedData = JSON.parse(message.execution.executionData); + return obj; + }); + setMessages((prevMessages) => [...prevMessages, ...loadedMessages]); + setLocalStorageChatflow(chatflowid, chatId); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getChatmessageApi.data]); + + useEffect(() => { + if (getAllExecutionsApi.data?.length) { + const chatId = getAllExecutionsApi.data[0]?.sessionId; + setChatId(chatId); + const loadedMessages = getAllExecutionsApi.data.map((execution) => { + const executionData = typeof execution.executionData === 'string' ? JSON.parse(execution.executionData) : execution.executionData; + const obj = { + id: execution.id, + agentFlow: executionData, + }; + return obj; + }); + setMessages((prevMessages) => [...prevMessages, ...loadedMessages]); + setLocalStorageChatflow(chatflowid, chatId); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getAllExecutionsApi.data]); + + // Get chatflow streaming capability + useEffect(() => { + if (getIsChatflowStreamingApi.data) { + setIsChatFlowAvailableToStream(getIsChatflowStreamingApi.data?.isStreaming ?? false); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getIsChatflowStreamingApi.data]); + + // Get chatflow uploads capability + useEffect(() => { + if (getAllowChatFlowUploads.data) { + setIsChatFlowAvailableForImageUploads(getAllowChatFlowUploads.data?.isImageUploadAllowed ?? false); + setIsChatFlowAvailableForRAGFileUploads(getAllowChatFlowUploads.data?.isRAGFileUploadAllowed ?? false); + setIsChatFlowAvailableForSpeech(getAllowChatFlowUploads.data?.isSpeechToTextEnabled ?? false); + setImageUploadAllowedTypes(getAllowChatFlowUploads.data?.imgUploadSizeAndTypes.map((allowed) => allowed.fileTypes).join(',')); + setFileUploadAllowedTypes(getAllowChatFlowUploads.data?.fileUploadSizeAndTypes.map((allowed) => allowed.fileTypes).join(',')); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getAllowChatFlowUploads.data]); + + useEffect(() => { + if (getChatflowConfig.data) { + setIsConfigLoading(false); + if (getChatflowConfig.data?.flowData) { + let nodes = JSON.parse(getChatflowConfig.data?.flowData).nodes ?? []; + const startNode = nodes.find((node) => node.data.name === 'startAgentflow'); + if (startNode) { + const startInputType = startNode.data.inputs?.startInputType; + setStartInputType(startInputType); + + const formInputTypes = startNode.data.inputs?.formInputTypes; + if (startInputType === 'formInput' && formInputTypes && formInputTypes.length > 0) { + for (const formInputType of formInputTypes) { + if (formInputType.type === 'options') { + formInputType.options = formInputType.addOptions.map((option) => ({ + label: option.option, + name: option.option, + })); + } + } + setFormInputParams(formInputTypes); + setFormInputsData({ + id: 'formInput', + inputs: {}, + inputParams: formInputTypes, + }); + setFormTitle(startNode.data.inputs?.formTitle); + setFormDescription(startNode.data.inputs?.formDescription); + } + + getAllExecutionsApi.request({ agentflowId: chatflowid }); + } + } + + if (getChatflowConfig.data?.chatbotConfig && JSON.parse(getChatflowConfig.data?.chatbotConfig)) { + let config = JSON.parse(getChatflowConfig.data?.chatbotConfig); + if (config.starterPrompts) { + let inputFields = []; + Object.getOwnPropertyNames(config.starterPrompts).forEach((key) => { + if (config.starterPrompts[key]) { + inputFields.push(config.starterPrompts[key]); + } + }); + setStarterPrompts(inputFields.filter((field) => field.prompt !== '')); } - } - - const handleLeadCaptureSubmit = async (event) => { - if (event) event.preventDefault() - setIsLeadSaving(true) - - const body = { - chatflowid, - chatId, - name: leadName, - email: leadEmail, - phone: leadPhone + if (config.chatFeedback) { + setChatFeedbackStatus(config.chatFeedback.status); } - const result = await leadsApi.addLead(body) - if (result.data) { - const data = result.data - setChatId(data.chatId) - setLocalStorageChatflow(chatflowid, data.chatId, { lead: { name: leadName, email: leadEmail, phone: leadPhone } }) - setIsLeadSaved(true) - setLeadEmail(leadEmail) + if (config.leads) { + setLeadsConfig(config.leads); + if (config.leads.status && !getLocalStorageChatflow(chatflowid).lead) { setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)] - if (allMessages[allMessages.length - 1].type !== 'leadCaptureMessage') return allMessages - allMessages[allMessages.length - 1].message = - leadsConfig.successMessage || 'Thank you for submitting your contact information.' - return allMessages - }) - } - - setIsLeadSaving(false) - } - - const cleanupTTSForMessage = (messageId) => { - if (ttsAudio[messageId]) { - ttsAudio[messageId].pause() - ttsAudio[messageId].currentTime = 0 - setTtsAudio((prev) => { - const newState = { ...prev } - delete newState[messageId] - return newState - }) - } + const leadCaptureMessage = { + message: '', + type: 'leadCaptureMessage', + }; - if (ttsStreamingState.audio) { - ttsStreamingState.audio.pause() - cleanupTTSStreaming() + return [...prevMessages, leadCaptureMessage]; + }); + } } - setIsTTSPlaying((prev) => { - const newState = { ...prev } - delete newState[messageId] - return newState - }) - - setIsTTSLoading((prev) => { - const newState = { ...prev } - delete newState[messageId] - return newState - }) - } - - const handleTTSStop = async (messageId) => { - setTTSAction(true) - await ttsApi.abortTTS({ chatflowId: chatflowid, chatId, chatMessageId: messageId }) - cleanupTTSForMessage(messageId) - } - - const stopAllTTS = () => { - Object.keys(ttsAudio).forEach((messageId) => { - if (ttsAudio[messageId]) { - ttsAudio[messageId].pause() - ttsAudio[messageId].currentTime = 0 - } - }) - setTtsAudio({}) - - if (ttsStreamingState.abortController) { - ttsStreamingState.abortController.abort() + if (config.followUpPrompts) { + setFollowUpPromptsStatus(config.followUpPrompts.status); } - if (ttsStreamingState.audio) { - ttsStreamingState.audio.pause() - cleanupTTSStreaming() + if (config.fullFileUpload) { + setFullFileUpload(config.fullFileUpload.status); + if (config.fullFileUpload?.allowedUploadFileTypes) { + setFullFileUploadAllowedTypes(config.fullFileUpload?.allowedUploadFileTypes); + } } - - setIsTTSPlaying({}) - setIsTTSLoading({}) + } } - const handleTTSClick = async (messageId, messageText) => { - if (isTTSLoading[messageId]) return - - if (isTTSPlaying[messageId] || ttsAudio[messageId]) { - handleTTSStop(messageId) - return - } - - setTTSAction(true) - stopAllTTS() - - handleTTSStart({ chatMessageId: messageId, format: 'mp3' }) - try { - const abortController = new AbortController() - setTtsStreamingState((prev) => ({ ...prev, abortController })) - - const response = await fetch('/api/v1/text-to-speech/generate', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'x-request-from': 'internal' - }, - credentials: 'include', - signal: abortController.signal, - body: JSON.stringify({ - chatflowId: chatflowid, - chatId: chatId, - chatMessageId: messageId, - text: messageText - }) - }) - - if (!response.ok) { - throw new Error(`TTS request failed: ${response.status}`) - } - - const reader = response.body.getReader() - const decoder = new TextDecoder() - let buffer = '' - - let done = false - while (!done) { - if (abortController.signal.aborted) { - break - } + // Check if TTS is configured + if (getChatflowConfig.data && getChatflowConfig.data.textToSpeech) { + try { + const ttsConfig = + typeof getChatflowConfig.data.textToSpeech === 'string' + ? JSON.parse(getChatflowConfig.data.textToSpeech) + : getChatflowConfig.data.textToSpeech; - const result = await reader.read() - done = result.done - if (done) { - break - } - const value = result.value - const chunk = decoder.decode(value, { stream: true }) - buffer += chunk - - const lines = buffer.split('\n\n') - buffer = lines.pop() || '' - - for (const eventBlock of lines) { - if (eventBlock.trim()) { - const event = parseSSEEvent(eventBlock) - if (event) { - switch (event.event) { - case 'tts_start': - break - case 'tts_data': - if (!abortController.signal.aborted) { - handleTTSDataChunk(event.data.audioChunk) - } - break - case 'tts_end': - if (!abortController.signal.aborted) { - handleTTSEnd() - } - break - } - } - } - } - } - } catch (error) { - if (error.name === 'AbortError') { - console.error('TTS request was aborted') - } else { - console.error('Error with TTS:', error) - enqueueSnackbar({ - message: `TTS failed: ${error.message}`, - options: { variant: 'error' } - }) + let isEnabled = false; + if (ttsConfig) { + Object.keys(ttsConfig).forEach((provider) => { + if (provider !== 'none' && ttsConfig?.[provider]?.status) { + isEnabled = true; } - } finally { - setIsTTSLoading((prev) => { - const newState = { ...prev } - delete newState[messageId] - return newState - }) - } - } - - const parseSSEEvent = (eventBlock) => { - const lines = eventBlock.split('\n') - const event = {} - - for (const line of lines) { - if (line.startsWith('event:')) { - event.event = line.substring(6).trim() - } else if (line.startsWith('data:')) { - const dataStr = line.substring(5).trim() - try { - const parsed = JSON.parse(dataStr) - if (parsed.data) { - event.data = parsed.data - } - } catch (e) { - console.error('Error parsing SSE data:', e, 'Raw data:', dataStr) - } + }); + } + setIsTTSEnabled(isEnabled); + } catch (error) { + setIsTTSEnabled(false); + } + } else { + setIsTTSEnabled(false); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getChatflowConfig.data]); + + useEffect(() => { + if (getChatflowConfig.error) { + setIsConfigLoading(false); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getChatflowConfig.error]); + + useEffect(() => { + if (fullFileUpload) { + setIsChatFlowAvailableForFileUploads(true); + } else if (isChatFlowAvailableForRAGFileUploads) { + setIsChatFlowAvailableForFileUploads(true); + } else { + setIsChatFlowAvailableForFileUploads(false); + } + }, [isChatFlowAvailableForRAGFileUploads, fullFileUpload]); + + // Auto scroll chat to bottom (but not during TTS actions) + useEffect(() => { + if (!isTTSActionRef.current) { + scrollToBottom(); + } + }, [messages]); + + useEffect(() => { + if (isDialog && inputRef) { + setTimeout(() => { + inputRef.current?.focus(); + }, 100); + } + }, [isDialog, inputRef]); + + useEffect(() => { + if (open && chatflowid) { + // API request + getChatmessageApi.request(chatflowid); + getIsChatflowStreamingApi.request(chatflowid); + getAllowChatFlowUploads.request(chatflowid); + getChatflowConfig.request(chatflowid); + + // Add a small delay to ensure content is rendered before scrolling + setTimeout(() => { + scrollToBottom(); + }, 100); + + setIsRecording(false); + setIsConfigLoading(true); + + // leads + const savedLead = getLocalStorageChatflow(chatflowid)?.lead; + if (savedLead) { + setIsLeadSaved(!!savedLead); + setLeadEmail(savedLead.email); + } + } + + return () => { + setUserInput(''); + setUploadedFiles([]); + setLoading(false); + setMessages([ + { + message: 'Hi there! How can I help?', + type: 'apiMessage', + }, + ]); + }; + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [open, chatflowid]); + + useEffect(() => { + // wait for audio recording to load and then send + const containsAudio = previews.filter((item) => item.type === 'audio').length > 0; + if (previews.length >= 1 && containsAudio) { + setIsRecording(false); + setRecordingNotSupported(false); + handlePromptClick(''); + } + // eslint-disable-next-line + }, [previews]); + + useEffect(() => { + if (followUpPromptsStatus && messages.length > 0) { + const lastMessage = messages[messages.length - 1]; + if (lastMessage.type === 'apiMessage' && lastMessage.followUpPrompts) { + if (Array.isArray(lastMessage.followUpPrompts)) { + setFollowUpPrompts(lastMessage.followUpPrompts); + } + if (typeof lastMessage.followUpPrompts === 'string') { + const followUpPrompts = JSON.parse(lastMessage.followUpPrompts); + setFollowUpPrompts(followUpPrompts); + } + } else if (lastMessage.type === 'userMessage') { + setFollowUpPrompts([]); + } + } + }, [followUpPromptsStatus, messages]); + + const copyMessageToClipboard = async (text) => { + try { + await navigator.clipboard.writeText(text || ''); + } catch (error) { + console.error('Error copying to clipboard:', error); + } + }; + + const onThumbsUpClick = async (messageId) => { + const body = { + chatflowid, + chatId, + messageId, + rating: 'THUMBS_UP', + content: '', + }; + const result = await chatmessagefeedbackApi.addFeedback(chatflowid, body); + if (result.data) { + const data = result.data; + let id = ''; + if (data && data.id) id = data.id; + setMessages((prevMessages) => { + const allMessages = [...cloneDeep(prevMessages)]; + return allMessages.map((message) => { + if (message.id === messageId) { + message.feedback = { + rating: 'THUMBS_UP', + }; + } + return message; + }); + }); + setFeedbackId(id); + setShowFeedbackContentDialog(true); + } + }; + + const onThumbsDownClick = async (messageId) => { + const body = { + chatflowid, + chatId, + messageId, + rating: 'THUMBS_DOWN', + content: '', + }; + const result = await chatmessagefeedbackApi.addFeedback(chatflowid, body); + if (result.data) { + const data = result.data; + let id = ''; + if (data && data.id) id = data.id; + setMessages((prevMessages) => { + const allMessages = [...cloneDeep(prevMessages)]; + return allMessages.map((message) => { + if (message.id === messageId) { + message.feedback = { + rating: 'THUMBS_DOWN', + }; + } + return message; + }); + }); + setFeedbackId(id); + setShowFeedbackContentDialog(true); + } + }; + + const submitFeedbackContent = async (text) => { + const body = { + content: text, + }; + const result = await chatmessagefeedbackApi.updateFeedback(feedbackId, body); + if (result.data) { + setFeedbackId(''); + setShowFeedbackContentDialog(false); + } + }; + + const handleLeadCaptureSubmit = async (event) => { + if (event) event.preventDefault(); + setIsLeadSaving(true); + + const body = { + chatflowid, + chatId, + name: leadName, + email: leadEmail, + phone: leadPhone, + }; + + const result = await leadsApi.addLead(body); + if (result.data) { + const data = result.data; + setChatId(data.chatId); + setLocalStorageChatflow(chatflowid, data.chatId, { lead: { name: leadName, email: leadEmail, phone: leadPhone } }); + setIsLeadSaved(true); + setLeadEmail(leadEmail); + setMessages((prevMessages) => { + let allMessages = [...cloneDeep(prevMessages)]; + if (allMessages[allMessages.length - 1].type !== 'leadCaptureMessage') return allMessages; + allMessages[allMessages.length - 1].message = leadsConfig.successMessage || 'Thank you for submitting your contact information.'; + return allMessages; + }); + } + + setIsLeadSaving(false); + }; + + const cleanupTTSForMessage = (messageId) => { + if (ttsAudio[messageId]) { + ttsAudio[messageId].pause(); + ttsAudio[messageId].currentTime = 0; + setTtsAudio((prev) => { + const newState = { ...prev }; + delete newState[messageId]; + return newState; + }); + } + + if (ttsStreamingState.audio) { + ttsStreamingState.audio.pause(); + cleanupTTSStreaming(); + } + + setIsTTSPlaying((prev) => { + const newState = { ...prev }; + delete newState[messageId]; + return newState; + }); + + setIsTTSLoading((prev) => { + const newState = { ...prev }; + delete newState[messageId]; + return newState; + }); + }; + + const handleTTSStop = async (messageId) => { + setTTSAction(true); + await ttsApi.abortTTS({ chatflowId: chatflowid, chatId, chatMessageId: messageId }); + cleanupTTSForMessage(messageId); + }; + + const stopAllTTS = () => { + Object.keys(ttsAudio).forEach((messageId) => { + if (ttsAudio[messageId]) { + ttsAudio[messageId].pause(); + ttsAudio[messageId].currentTime = 0; + } + }); + setTtsAudio({}); + + if (ttsStreamingState.abortController) { + ttsStreamingState.abortController.abort(); + } + + if (ttsStreamingState.audio) { + ttsStreamingState.audio.pause(); + cleanupTTSStreaming(); + } + + setIsTTSPlaying({}); + setIsTTSLoading({}); + }; + + const handleTTSClick = async (messageId, messageText) => { + if (isTTSLoading[messageId]) return; + + if (isTTSPlaying[messageId] || ttsAudio[messageId]) { + handleTTSStop(messageId); + return; + } + + setTTSAction(true); + stopAllTTS(); + + handleTTSStart({ chatMessageId: messageId, format: 'mp3' }); + try { + const abortController = new AbortController(); + setTtsStreamingState((prev) => ({ ...prev, abortController })); + + const response = await fetch('/api/v1/text-to-speech/generate', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-request-from': 'internal', + }, + credentials: 'include', + signal: abortController.signal, + body: JSON.stringify({ + chatflowId: chatflowid, + chatId: chatId, + chatMessageId: messageId, + text: messageText, + }), + }); + + if (!response.ok) { + throw new Error(`TTS request failed: ${response.status}`); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + + let done = false; + while (!done) { + if (abortController.signal.aborted) { + break; + } + + const result = await reader.read(); + done = result.done; + if (done) { + break; + } + const value = result.value; + const chunk = decoder.decode(value, { stream: true }); + buffer += chunk; + + const lines = buffer.split('\n\n'); + buffer = lines.pop() || ''; + + for (const eventBlock of lines) { + if (eventBlock.trim()) { + const event = parseSSEEvent(eventBlock); + if (event) { + switch (event.event) { + case 'tts_start': + break; + case 'tts_data': + if (!abortController.signal.aborted) { + handleTTSDataChunk(event.data.audioChunk); + } + break; + case 'tts_end': + if (!abortController.signal.aborted) { + handleTTSEnd(); + } + break; + } } - } - - return event.event ? event : null - } - - const initializeTTSStreaming = (data) => { + } + } + } + } catch (error) { + if (error.name === 'AbortError') { + console.error('TTS request was aborted'); + } else { + console.error('Error with TTS:', error); + enqueueSnackbar({ + message: `TTS failed: ${error.message}`, + options: { variant: 'error' }, + }); + } + } finally { + setIsTTSLoading((prev) => { + const newState = { ...prev }; + delete newState[messageId]; + return newState; + }); + } + }; + + const parseSSEEvent = (eventBlock) => { + const lines = eventBlock.split('\n'); + const event = {}; + + for (const line of lines) { + if (line.startsWith('event:')) { + event.event = line.substring(6).trim(); + } else if (line.startsWith('data:')) { + const dataStr = line.substring(5).trim(); try { - const mediaSource = new MediaSource() - const audio = new Audio() - audio.src = URL.createObjectURL(mediaSource) - - mediaSource.addEventListener('sourceopen', () => { - try { - const mimeType = data.format === 'mp3' ? 'audio/mpeg' : 'audio/mpeg' - const sourceBuffer = mediaSource.addSourceBuffer(mimeType) - - setTtsStreamingState((prevState) => ({ - ...prevState, - mediaSource, - sourceBuffer, - audio - })) - - audio.play().catch((playError) => { - console.error('Error starting audio playback:', playError) - }) - } catch (error) { - console.error('Error setting up source buffer:', error) - console.error('MediaSource readyState:', mediaSource.readyState) - console.error('Requested MIME type:', mimeType) - } - }) - - audio.addEventListener('playing', () => { - setIsTTSLoading((prevState) => { - const newState = { ...prevState } - newState[data.chatMessageId] = false - return newState - }) - setIsTTSPlaying((prevState) => ({ - ...prevState, - [data.chatMessageId]: true - })) - }) - - audio.addEventListener('ended', () => { - setIsTTSPlaying((prevState) => { - const newState = { ...prevState } - delete newState[data.chatMessageId] - return newState - }) - cleanupTTSStreaming() - }) - } catch (error) { - console.error('Error initializing TTS streaming:', error) + const parsed = JSON.parse(dataStr); + if (parsed.data) { + event.data = parsed.data; + } + } catch (e) { + console.error('Error parsing SSE data:', e, 'Raw data:', dataStr); } + } } - const cleanupTTSStreaming = () => { - setTtsStreamingState((prevState) => { - if (prevState.abortController) { - prevState.abortController.abort() - } - - if (prevState.audio) { - prevState.audio.pause() - prevState.audio.removeAttribute('src') - if (prevState.audio.src) { - URL.revokeObjectURL(prevState.audio.src) - } - } - - if (prevState.mediaSource) { - if (prevState.mediaSource.readyState === 'open') { - try { - prevState.mediaSource.endOfStream() - } catch (e) { - // Ignore errors during cleanup - } - } - prevState.mediaSource.removeEventListener('sourceopen', () => {}) - } - - return { - mediaSource: null, - sourceBuffer: null, - audio: null, - chunkQueue: [], - isBuffering: false, - audioFormat: null, - abortController: null - } - }) - } - - const processChunkQueue = () => { - setTtsStreamingState((prevState) => { - if (!prevState.sourceBuffer || prevState.sourceBuffer.updating || prevState.chunkQueue.length === 0) { - return prevState - } - - const chunk = prevState.chunkQueue.shift() - - try { - prevState.sourceBuffer.appendBuffer(chunk) - return { - ...prevState, - chunkQueue: [...prevState.chunkQueue], - isBuffering: true - } - } catch (error) { - console.error('Error appending chunk to buffer:', error) - return prevState - } - }) - } + return event.event ? event : null; + }; - const handleTTSStart = (data) => { - setTTSAction(true) + const initializeTTSStreaming = (data) => { + try { + const mediaSource = new MediaSource(); + const audio = new Audio(); + audio.src = URL.createObjectURL(mediaSource); - // Stop all existing TTS audio before starting new stream - stopAllTTS() + mediaSource.addEventListener('sourceopen', () => { + try { + const mimeType = data.format === 'mp3' ? 'audio/mpeg' : 'audio/mpeg'; + const sourceBuffer = mediaSource.addSourceBuffer(mimeType); - setIsTTSLoading((prevState) => ({ + setTtsStreamingState((prevState) => ({ ...prevState, - [data.chatMessageId]: true - })) - setMessages((prevMessages) => { - const allMessages = [...cloneDeep(prevMessages)] - const lastMessage = allMessages[allMessages.length - 1] - if (lastMessage.type === 'userMessage') return allMessages - if (lastMessage.id) return allMessages - allMessages[allMessages.length - 1].id = data.chatMessageId - return allMessages - }) - setTtsStreamingState({ - mediaSource: null, - sourceBuffer: null, - audio: null, - chunkQueue: [], - isBuffering: false, - audioFormat: data.format, - abortController: null - }) - - setTimeout(() => initializeTTSStreaming(data), 0) - } - - const handleTTSDataChunk = (base64Data) => { + mediaSource, + sourceBuffer, + audio, + })); + + audio.play().catch((playError) => { + console.error('Error starting audio playback:', playError); + }); + } catch (error) { + console.error('Error setting up source buffer:', error); + console.error('MediaSource readyState:', mediaSource.readyState); + console.error('Requested MIME type:', mimeType); + } + }); + + audio.addEventListener('playing', () => { + setIsTTSLoading((prevState) => { + const newState = { ...prevState }; + newState[data.chatMessageId] = false; + return newState; + }); + setIsTTSPlaying((prevState) => ({ + ...prevState, + [data.chatMessageId]: true, + })); + }); + + audio.addEventListener('ended', () => { + setIsTTSPlaying((prevState) => { + const newState = { ...prevState }; + delete newState[data.chatMessageId]; + return newState; + }); + cleanupTTSStreaming(); + }); + } catch (error) { + console.error('Error initializing TTS streaming:', error); + } + }; + + const cleanupTTSStreaming = () => { + setTtsStreamingState((prevState) => { + if (prevState.abortController) { + prevState.abortController.abort(); + } + + if (prevState.audio) { + prevState.audio.pause(); + prevState.audio.removeAttribute('src'); + if (prevState.audio.src) { + URL.revokeObjectURL(prevState.audio.src); + } + } + + if (prevState.mediaSource) { + if (prevState.mediaSource.readyState === 'open') { + try { + prevState.mediaSource.endOfStream(); + } catch (e) { + // Ignore errors during cleanup + } + } + prevState.mediaSource.removeEventListener('sourceopen', () => {}); + } + + return { + mediaSource: null, + sourceBuffer: null, + audio: null, + chunkQueue: [], + isBuffering: false, + audioFormat: null, + abortController: null, + }; + }); + }; + + const processChunkQueue = () => { + setTtsStreamingState((prevState) => { + if (!prevState.sourceBuffer || prevState.sourceBuffer.updating || prevState.chunkQueue.length === 0) { + return prevState; + } + + const chunk = prevState.chunkQueue.shift(); + + try { + prevState.sourceBuffer.appendBuffer(chunk); + return { + ...prevState, + chunkQueue: [...prevState.chunkQueue], + isBuffering: true, + }; + } catch (error) { + console.error('Error appending chunk to buffer:', error); + return prevState; + } + }); + }; + + const handleTTSStart = (data) => { + setTTSAction(true); + + // Stop all existing TTS audio before starting new stream + stopAllTTS(); + + setIsTTSLoading((prevState) => ({ + ...prevState, + [data.chatMessageId]: true, + })); + setMessages((prevMessages) => { + const allMessages = [...cloneDeep(prevMessages)]; + const lastMessage = allMessages[allMessages.length - 1]; + if (lastMessage.type === 'userMessage') return allMessages; + if (lastMessage.id) return allMessages; + allMessages[allMessages.length - 1].id = data.chatMessageId; + return allMessages; + }); + setTtsStreamingState({ + mediaSource: null, + sourceBuffer: null, + audio: null, + chunkQueue: [], + isBuffering: false, + audioFormat: data.format, + abortController: null, + }); + + setTimeout(() => initializeTTSStreaming(data), 0); + }; + + const handleTTSDataChunk = (base64Data) => { + try { + const audioBuffer = Uint8Array.from(atob(base64Data), (c) => c.charCodeAt(0)); + + setTtsStreamingState((prevState) => { + const newState = { + ...prevState, + chunkQueue: [...prevState.chunkQueue, audioBuffer], + }; + + if (prevState.sourceBuffer && !prevState.sourceBuffer.updating) { + setTimeout(() => processChunkQueue(), 0); + } + + return newState; + }); + } catch (error) { + console.error('Error handling TTS data chunk:', error); + } + }; + + const handleTTSEnd = () => { + setTtsStreamingState((prevState) => { + if (prevState.mediaSource && prevState.mediaSource.readyState === 'open') { try { - const audioBuffer = Uint8Array.from(atob(base64Data), (c) => c.charCodeAt(0)) - - setTtsStreamingState((prevState) => { - const newState = { - ...prevState, - chunkQueue: [...prevState.chunkQueue, audioBuffer] - } - + if (prevState.sourceBuffer && prevState.chunkQueue.length > 0 && !prevState.sourceBuffer.updating) { + const remainingChunks = [...prevState.chunkQueue]; + remainingChunks.forEach((chunk, index) => { + setTimeout(() => { if (prevState.sourceBuffer && !prevState.sourceBuffer.updating) { - setTimeout(() => processChunkQueue(), 0) - } - - return newState - }) - } catch (error) { - console.error('Error handling TTS data chunk:', error) - } - } - - const handleTTSEnd = () => { - setTtsStreamingState((prevState) => { - if (prevState.mediaSource && prevState.mediaSource.readyState === 'open') { - try { - if (prevState.sourceBuffer && prevState.chunkQueue.length > 0 && !prevState.sourceBuffer.updating) { - const remainingChunks = [...prevState.chunkQueue] - remainingChunks.forEach((chunk, index) => { - setTimeout(() => { - if (prevState.sourceBuffer && !prevState.sourceBuffer.updating) { - try { - prevState.sourceBuffer.appendBuffer(chunk) - if (index === remainingChunks.length - 1) { - setTimeout(() => { - if (prevState.mediaSource && prevState.mediaSource.readyState === 'open') { - prevState.mediaSource.endOfStream() - } - }, 100) - } - } catch (error) { - console.error('Error appending remaining chunk:', error) - } - } - }, index * 50) - }) - return { - ...prevState, - chunkQueue: [] + try { + prevState.sourceBuffer.appendBuffer(chunk); + if (index === remainingChunks.length - 1) { + setTimeout(() => { + if (prevState.mediaSource && prevState.mediaSource.readyState === 'open') { + prevState.mediaSource.endOfStream(); } + }, 100); } - - if (prevState.sourceBuffer && !prevState.sourceBuffer.updating) { - prevState.mediaSource.endOfStream() - } else if (prevState.sourceBuffer) { - prevState.sourceBuffer.addEventListener( - 'updateend', - () => { - if (prevState.mediaSource && prevState.mediaSource.readyState === 'open') { - prevState.mediaSource.endOfStream() - } - }, - { once: true } - ) - } - } catch (error) { - console.error('Error ending TTS stream:', error) + } catch (error) { + console.error('Error appending remaining chunk:', error); + } } - } - return prevState - }) - } - - const handleTTSAbort = (data) => { - const messageId = data.chatMessageId - cleanupTTSForMessage(messageId) + }, index * 50); + }); + return { + ...prevState, + chunkQueue: [], + }; + } + + if (prevState.sourceBuffer && !prevState.sourceBuffer.updating) { + prevState.mediaSource.endOfStream(); + } else if (prevState.sourceBuffer) { + prevState.sourceBuffer.addEventListener( + 'updateend', + () => { + if (prevState.mediaSource && prevState.mediaSource.readyState === 'open') { + prevState.mediaSource.endOfStream(); + } + }, + { once: true }, + ); + } + } catch (error) { + console.error('Error ending TTS stream:', error); + } + } + return prevState; + }); + }; + + const handleTTSAbort = (data) => { + const messageId = data.chatMessageId; + cleanupTTSForMessage(messageId); + }; + + useEffect(() => { + if (ttsStreamingState.sourceBuffer) { + const sourceBuffer = ttsStreamingState.sourceBuffer; + + const handleUpdateEnd = () => { + setTtsStreamingState((prevState) => ({ + ...prevState, + isBuffering: false, + })); + setTimeout(() => processChunkQueue(), 0); + }; + + sourceBuffer.addEventListener('updateend', handleUpdateEnd); + + return () => { + sourceBuffer.removeEventListener('updateend', handleUpdateEnd); + }; + } + }, [ttsStreamingState.sourceBuffer]); + + useEffect(() => { + return () => { + cleanupTTSStreaming(); + // Cleanup TTS timeout on unmount + if (ttsTimeoutRef.current) { + clearTimeout(ttsTimeoutRef.current); + ttsTimeoutRef.current = null; + } + }; + }, []); + + const getInputDisabled = () => { + return ( + loading || + !chatflowid || + (leadsConfig?.status && !isLeadSaved) || + (messages[messages.length - 1].action && Object.keys(messages[messages.length - 1].action).length > 0) + ); + }; + + const previewDisplay = (item) => { + if (item.mime.startsWith('image/')) { + return ( + handleDeletePreview(item)} + > + + + + + + + ); + } else if (item.mime.startsWith('audio/')) { + return ( + + + handleDeletePreview(item)} size="small"> + + + + ); + } else { + return ( + handleDeletePreview(item)} /> + ); + } + }; + + const renderFileUploads = (item, index) => { + if (item?.mime?.startsWith('image/')) { + return ( + + + + ); + } else if (item?.mime?.startsWith('audio/')) { + return ( + /* eslint-disable jsx-a11y/media-has-caption */ + + ); + } else { + return ( + + + + {item.name} + + + ); + } + }; + + const agentReasoningArtifacts = (artifacts) => { + const newArtifacts = cloneDeep(artifacts); + for (let i = 0; i < newArtifacts.length; i++) { + const artifact = newArtifacts[i]; + if (artifact && (artifact.type === 'png' || artifact.type === 'jpeg')) { + const data = artifact.data; + newArtifacts[i].data = `${baseURL}/api/v1/get-upload-file?chatflowId=${chatflowid}&chatId=${chatId}&fileName=${data.replace( + 'FILE-STORAGE::', + '', + )}`; + } + } + return newArtifacts; + }; + + const renderArtifacts = (item, index, isAgentReasoning) => { + if (item.type === 'png' || item.type === 'jpeg') { + return ( + + + + ); + } else if (item.type === 'html') { + return ( +
+ +
+ ); + } else { + return ( + + {item.data} + + ); } + }; - useEffect(() => { - if (ttsStreamingState.sourceBuffer) { - const sourceBuffer = ttsStreamingState.sourceBuffer - - const handleUpdateEnd = () => { - setTtsStreamingState((prevState) => ({ - ...prevState, - isBuffering: false - })) - setTimeout(() => processChunkQueue(), 0) - } - - sourceBuffer.addEventListener('updateend', handleUpdateEnd) - - return () => { - sourceBuffer.removeEventListener('updateend', handleUpdateEnd) - } - } - }, [ttsStreamingState.sourceBuffer]) - - useEffect(() => { - return () => { - cleanupTTSStreaming() - // Cleanup TTS timeout on unmount - if (ttsTimeoutRef.current) { - clearTimeout(ttsTimeoutRef.current) - ttsTimeoutRef.current = null - } - } - }, []) - - const getInputDisabled = () => { - return ( - loading || - !chatflowid || - (leadsConfig?.status && !isLeadSaved) || - (messages[messages.length - 1].action && Object.keys(messages[messages.length - 1].action).length > 0) - ) - } + if (isConfigLoading) { + return ( + + + + + + ); + } - const previewDisplay = (item) => { - if (item.mime.startsWith('image/')) { - return ( - handleDeletePreview(item)} - > - - - - - - - ) - } else if (item.mime.startsWith('audio/')) { - return ( - - - handleDeletePreview(item)} size='small'> - - - - ) - } else { - return ( - handleDeletePreview(item)} - /> - ) - } - } + if (startInputType === 'formInput' && messages.length === 1) { + return ( + + + + + {formTitle || 'Please Fill Out The Form'} + + + {formDescription || 'Complete all fields below to continue'} + + + {/* Form inputs */} + + {formInputParams && + formInputParams.map((inputParam, index) => ( + + { + setFormInputsData((prev) => ({ + ...prev, + inputs: { + ...prev.inputs, + [inputParam.name]: newValue, + }, + })); + }} + /> + + ))} + - const renderFileUploads = (item, index) => { - if (item?.mime?.startsWith('image/')) { + + + + + ); + } + + return ( +
+ {isDragActive && ( +
+ )} + {isDragActive && (getAllowChatFlowUploads.data?.isImageUploadAllowed || getAllowChatFlowUploads.data?.isRAGFileUploadAllowed) && ( + + Drop here to upload + {[...getAllowChatFlowUploads.data.imgUploadSizeAndTypes, ...getAllowChatFlowUploads.data.fileUploadSizeAndTypes].map((allowed) => { return ( - + {allowed.fileTypes?.join(', ')} + {allowed.maxUploadSize && Max Allowed Size: {allowed.maxUploadSize} MB} + + ); + })} + + )} +
+
+ {messages && + messages.map((message, index) => { + return ( + // The latest message sent by the user will be animated while waiting for a response + - - - ) - } else if (item?.mime?.startsWith('audio/')) { - return ( - /* eslint-disable jsx-a11y/media-has-caption */ - - ) - } else { - return ( - + ) : ( + Me + )} +
- - + {message.fileUploads && message.fileUploads.length > 0 && ( +
- {item.name} - - - ) - } - } - - const agentReasoningArtifacts = (artifacts) => { - const newArtifacts = cloneDeep(artifacts) - for (let i = 0; i < newArtifacts.length; i++) { - const artifact = newArtifacts[i] - if (artifact && (artifact.type === 'png' || artifact.type === 'jpeg')) { - const data = artifact.data - newArtifacts[i].data = `${baseURL}/api/v1/get-upload-file?chatflowId=${chatflowid}&chatId=${chatId}&fileName=${data.replace( - 'FILE-STORAGE::', - '' - )}` - } - } - return newArtifacts - } - - const renderArtifacts = (item, index, isAgentReasoning) => { - if (item.type === 'png' || item.type === 'jpeg') { - return ( - - + {message.fileUploads.map((item, index) => { + return <>{renderFileUploads(item, index)}; + })} +
+ )} + {message.agentReasoning && message.agentReasoning.length > 0 && ( +
+ {message.agentReasoning.map((agent, index) => ( + + ))} +
+ )} + {message.agentFlowExecutedData && Array.isArray(message.agentFlowExecutedData) && message.agentFlowExecutedData.length > 0 && ( + + )} + {message.usedTools && ( +
- - ) - } else if (item.type === 'html') { - return ( -
- -
- ) - } else { - return ( - - {item.data} - - ) - } - } - - if (isConfigLoading) { - return ( - - - - - - ) - } - - if (startInputType === 'formInput' && messages.length === 1) { - return ( - - - + {message.usedTools.map((tool, index) => { + return tool ? ( + } + onClick={() => onSourceDialogClick(tool, 'Used Tools')} + /> + ) : null; + })} +
+ )} + {message.artifacts && ( +
- - {formTitle || 'Please Fill Out The Form'} - - - {formDescription || 'Complete all fields below to continue'} - - - {/* Form inputs */} - - {formInputParams && - formInputParams.map((inputParam, index) => ( - - { - setFormInputsData((prev) => ({ - ...prev, - inputs: { - ...prev.inputs, - [inputParam.name]: newValue - } - })) - }} - /> - - ))} - - -
+ )} +
+ {message.type === 'leadCaptureMessage' && !getLocalStorageChatflow(chatflowid)?.lead && leadsConfig.status ? ( + - {loading ? 'Submitting...' : 'Submit'} - - - - - ) - } - - return ( -
- {isDragActive && ( -
- )} - {isDragActive && - (getAllowChatFlowUploads.data?.isImageUploadAllowed || getAllowChatFlowUploads.data?.isRAGFileUploadAllowed) && ( - - Drop here to upload - {[ - ...getAllowChatFlowUploads.data.imgUploadSizeAndTypes, - ...getAllowChatFlowUploads.data.fileUploadSizeAndTypes - ].map((allowed) => { - return ( - <> - {allowed.fileTypes?.join(', ')} - {allowed.maxUploadSize && ( - Max Allowed Size: {allowed.maxUploadSize} MB - )} - - ) + + {leadsConfig.title || 'Let us know where we can reach you:'} + +
+ {leadsConfig.name && ( + setLeadName(e.target.value)} + /> + )} + {leadsConfig.email && ( + setLeadEmail(e.target.value)} + /> + )} + {leadsConfig.phone && ( + setLeadPhone(e.target.value)} + /> + )} + + + + +
+ ) : ( + <> + + {message.message} + + + )} +
+ {message.fileAnnotations && ( +
+ {message.fileAnnotations.map((fileAnnotation, index) => { + return ( + + ); + })} +
+ )} + {message.sourceDocuments && ( +
+ {removeDuplicateURL(message).map((source, index) => { + const URL = source.metadata && source.metadata.source ? isValidURL(source.metadata.source) : undefined; + return ( + (URL ? onURLClick(source.metadata.source) : onSourceDialogClick(source))} + /> + ); })} - - )} -
-
- {messages && - messages.map((message, index) => { - return ( - // The latest message sent by the user will be animated while waiting for a response - + )} + {message.action && ( +
+ {(message.action.elements || []).map((elem, index) => { + return ( + <> + {(elem.type === 'approve-button' && elem.label === 'Yes') || elem.type === 'agentflowv2-approve-button' ? ( + + ) : (elem.type === 'reject-button' && elem.label === 'No') || elem.type === 'agentflowv2-reject-button' ? ( + + ) : ( + - - - - ) : ( - <> - - {message.message} - - - )} -
- {message.fileAnnotations && ( -
- {message.fileAnnotations.map((fileAnnotation, index) => { - return ( - - ) - })} -
- )} - {message.sourceDocuments && ( -
- {removeDuplicateURL(message).map((source, index) => { - const URL = - source.metadata && source.metadata.source - ? isValidURL(source.metadata.source) - : undefined - return ( - - URL ? onURLClick(source.metadata.source) : onSourceDialogClick(source) - } - /> - ) - })} -
- )} - {message.action && ( -
- {(message.action.elements || []).map((elem, index) => { - return ( - <> - {(elem.type === 'approve-button' && elem.label === 'Yes') || - elem.type === 'agentflowv2-approve-button' ? ( - - ) : (elem.type === 'reject-button' && elem.label === 'No') || - elem.type === 'agentflowv2-reject-button' ? ( - - ) : ( - - )} - - ) - })} -
- )} - {message.type === 'apiMessage' && message.id ? ( - <> - - {isTTSEnabled && ( - - isTTSPlaying[message.id] - ? handleTTSStop(message.id) - : handleTTSClick(message.id, message.message) - } - disabled={isTTSLoading[message.id]} - sx={{ - backgroundColor: ttsAudio[message.id] ? 'primary.main' : 'transparent', - color: ttsAudio[message.id] ? 'white' : 'inherit', - '&:hover': { - backgroundColor: ttsAudio[message.id] ? 'primary.dark' : 'action.hover' - } - }} - > - {isTTSLoading[message.id] ? ( - - ) : isTTSPlaying[message.id] ? ( - - ) : ( - - )} - - )} - {chatFeedbackStatus && ( - <> - copyMessageToClipboard(message.message)} - /> - {!message.feedback || - message.feedback.rating === '' || - message.feedback.rating === 'THUMBS_UP' ? ( - onThumbsUpClick(message.id)} - /> - ) : null} - {!message.feedback || - message.feedback.rating === '' || - message.feedback.rating === 'THUMBS_DOWN' ? ( - onThumbsDownClick(message.id)} - /> - ) : null} - - )} - - - ) : null} -
- - ) + {elem.label} + + )} + + ); })} +
+ )} + {message.type === 'apiMessage' && message.id ? ( + <> + + {isTTSEnabled && ( + (isTTSPlaying[message.id] ? handleTTSStop(message.id) : handleTTSClick(message.id, message.message))} + disabled={isTTSLoading[message.id]} + sx={{ + backgroundColor: ttsAudio[message.id] ? 'primary.main' : 'transparent', + color: ttsAudio[message.id] ? 'white' : 'inherit', + '&:hover': { + backgroundColor: ttsAudio[message.id] ? 'primary.dark' : 'action.hover', + }, + }} + > + {isTTSLoading[message.id] ? ( + + ) : isTTSPlaying[message.id] ? ( + + ) : ( + + )} + + )} + {chatFeedbackStatus && ( + <> + copyMessageToClipboard(message.message)} /> + {!message.feedback || message.feedback.rating === '' || message.feedback.rating === 'THUMBS_UP' ? ( + onThumbsUpClick(message.id)} + /> + ) : null} + {!message.feedback || message.feedback.rating === '' || message.feedback.rating === 'THUMBS_DOWN' ? ( + onThumbsDownClick(message.id)} + /> + ) : null} + + )} + + + ) : null} +
+ + ); + })} +
+
+ + {messages && messages.length === 1 && starterPrompts.length > 0 && ( +
+ 0 ? 70 : 0 }} + starterPrompts={starterPrompts || []} + onPromptClick={handlePromptClick} + isGrid={isDialog} + /> +
+ )} + + {messages && messages.length > 2 && followUpPromptsStatus && followUpPrompts.length > 0 && ( + <> + + + + + + Try these prompts + + + 0 ? 70 : 0 }} + followUpPrompts={followUpPrompts || []} + onPromptClick={handleFollowUpPromptClick} + isGrid={isDialog} + /> + + + )} + + + +
+ {previews && previews.length > 0 && ( + + {previews.map((item, index) => ( + {previewDisplay(item)} + ))} + + )} + {isRecording ? ( + <> + {recordingNotSupported ? ( +
+
+ To record audio, use modern browsers like Chrome or Firefox that support audio recording. +
-
- - {messages && messages.length === 1 && starterPrompts.length > 0 && ( -
- 0 ? 70 : 0 }} - starterPrompts={starterPrompts || []} - onPromptClick={handlePromptClick} - isGrid={isDialog} - /> +
+ ) : ( + +
+ + + + 00:00 + {isLoadingRecording && Sending...}
+
+ + + + + + +
+
)} - - {messages && messages.length > 2 && followUpPromptsStatus && followUpPrompts.length > 0 && ( + + ) : ( +
+ - - - - - - Try these prompts - - - 0 ? 70 : 0 }} - followUpPrompts={followUpPrompts || []} - onPromptClick={handleFollowUpPromptClick} - isGrid={isDialog} - /> - + {isChatFlowAvailableForImageUploads && !isChatFlowAvailableForFileUploads && ( + + + + + + )} + {!isChatFlowAvailableForImageUploads && isChatFlowAvailableForFileUploads && ( + + + + + + )} + {isChatFlowAvailableForImageUploads && isChatFlowAvailableForFileUploads && ( + + + + + + + + + )} + {!isChatFlowAvailableForImageUploads && !isChatFlowAvailableForFileUploads && } - )} - - - -
- {previews && previews.length > 0 && ( - - {previews.map((item, index) => ( - {previewDisplay(item)} - ))} - - )} - {isRecording ? ( - <> - {recordingNotSupported ? ( -
-
- - To record audio, use modern browsers like Chrome or Firefox that support audio recording. - - -
-
+ } + endAdornment={ + <> + {isChatFlowAvailableForSpeech && ( + + onMicrophonePressed()} type="button" disabled={getInputDisabled()} edge="end"> + + + + )} + {!isAgentCanvas && ( + + + {loading ? ( +
+ +
) : ( - -
- - - - 00:00 - {isLoadingRecording && Sending...} -
-
- - - - - - -
-
+ // Send icon SVG in input field + )} +
+
+ )} + {isAgentCanvas && ( + <> + {!loading && ( + + + + + + )} + {loading && ( + + handleAbort()} + disabled={isMessageStopping} + > + {isMessageStopping ? ( +
+ +
+ ) : ( + + )} +
+
+ )} - ) : ( - - - {isChatFlowAvailableForImageUploads && !isChatFlowAvailableForFileUploads && ( - - - - - - )} - {!isChatFlowAvailableForImageUploads && isChatFlowAvailableForFileUploads && ( - - - - - - )} - {isChatFlowAvailableForImageUploads && isChatFlowAvailableForFileUploads && ( - - - - - - - - - )} - {!isChatFlowAvailableForImageUploads && !isChatFlowAvailableForFileUploads && } - - } - endAdornment={ - <> - {isChatFlowAvailableForSpeech && ( - - onMicrophonePressed()} - type='button' - disabled={getInputDisabled()} - edge='end' - > - - - - )} - {!isAgentCanvas && ( - - - {loading ? ( -
- -
- ) : ( - // Send icon SVG in input field - - )} -
-
- )} - {isAgentCanvas && ( - <> - {!loading && ( - - - - - - )} - {loading && ( - - handleAbort()} - disabled={isMessageStopping} - > - {isMessageStopping ? ( -
- -
- ) : ( - - )} -
-
- )} - - )} - - } - /> - {isChatFlowAvailableForImageUploads && ( - - )} - {isChatFlowAvailableForFileUploads && ( - - )} - - )} -
- setSourceDialogOpen(false)} /> - setShowFeedbackContentDialog(false)} - onConfirm={submitFeedbackContent} + )} + + } /> - { - setOpenFeedbackDialog(false) - setPendingActionData(null) - setFeedback('') - }} - > - Provide Feedback - - setFeedback(e.target.value)} - /> - - - - - - -
- ) -} + {isChatFlowAvailableForImageUploads && ( + + )} + {isChatFlowAvailableForFileUploads && ( + + )} + + )} +
+ setSourceDialogOpen(false)} /> + setShowFeedbackContentDialog(false)} + onConfirm={submitFeedbackContent} + /> + { + setOpenFeedbackDialog(false); + setPendingActionData(null); + setFeedback(''); + }} + > + Provide Feedback + + setFeedback(e.target.value)} + /> + + + + + + +
+ ); +}; ChatMessage.propTypes = { - open: PropTypes.bool, - chatflowid: PropTypes.string, - isAgentCanvas: PropTypes.bool, - isDialog: PropTypes.bool, - previews: PropTypes.array, - setPreviews: PropTypes.func -} - -export default memo(ChatMessage) + open: PropTypes.bool, + chatflowid: PropTypes.string, + isAgentCanvas: PropTypes.bool, + isDialog: PropTypes.bool, + previews: PropTypes.array, + setPreviews: PropTypes.func, +}; + +export default memo(ChatMessage); From 520162d54bd074d89929d13189e230b9318d9076 Mon Sep 17 00:00:00 2001 From: Ilango Rajagopal Date: Fri, 26 Sep 2025 14:51:45 +0530 Subject: [PATCH 15/33] Remove react component --- src/components/ChatMessage.jsx | 2946 -------------------------------- 1 file changed, 2946 deletions(-) delete mode 100644 src/components/ChatMessage.jsx diff --git a/src/components/ChatMessage.jsx b/src/components/ChatMessage.jsx deleted file mode 100644 index 7baa627..0000000 --- a/src/components/ChatMessage.jsx +++ /dev/null @@ -1,2946 +0,0 @@ -import { useState, useRef, useEffect, useCallback, Fragment, useContext, memo } from 'react'; -import { useSelector, useDispatch } from 'react-redux'; -import PropTypes from 'prop-types'; -import { cloneDeep } from 'lodash'; -import axios from 'axios'; -import { v4 as uuidv4 } from 'uuid'; -import { EventStreamContentType, fetchEventSource } from '@microsoft/fetch-event-source'; - -import { - Box, - Button, - Card, - CardMedia, - Chip, - CircularProgress, - Divider, - IconButton, - InputAdornment, - OutlinedInput, - Typography, - Stack, - Dialog, - DialogTitle, - DialogContent, - DialogActions, - TextField, -} from '@mui/material'; -import { darken, useTheme } from '@mui/material/styles'; -import { - IconCircleDot, - IconDownload, - IconSend, - IconMicrophone, - IconPhotoPlus, - IconTrash, - IconX, - IconTool, - IconSquareFilled, - IconCheck, - IconPaperclip, - IconSparkles, - IconVolume, -} from '@tabler/icons-react'; -import robotPNG from '@/assets/images/robot.png'; -import userPNG from '@/assets/images/account.png'; -import multiagent_supervisorPNG from '@/assets/images/multiagent_supervisor.png'; -import multiagent_workerPNG from '@/assets/images/multiagent_worker.png'; -import audioUploadSVG from '@/assets/images/wave-sound.jpg'; - -// project import -import NodeInputHandler from '@/views/canvas/NodeInputHandler'; -import { MemoizedReactMarkdown } from '@/ui-component/markdown/MemoizedReactMarkdown'; -import { SafeHTML } from '@/ui-component/safe/SafeHTML'; -import SourceDocDialog from '@/ui-component/dialog/SourceDocDialog'; -import ChatFeedbackContentDialog from '@/ui-component/dialog/ChatFeedbackContentDialog'; -import StarterPromptsCard from '@/ui-component/cards/StarterPromptsCard'; -import AgentReasoningCard from './AgentReasoningCard'; -import AgentExecutedDataCard from './AgentExecutedDataCard'; -import { ImageButton, ImageSrc, ImageBackdrop, ImageMarked } from '@/ui-component/button/ImageButton'; -import CopyToClipboardButton from '@/ui-component/button/CopyToClipboardButton'; -import ThumbsUpButton from '@/ui-component/button/ThumbsUpButton'; -import ThumbsDownButton from '@/ui-component/button/ThumbsDownButton'; -import { cancelAudioRecording, startAudioRecording, stopAudioRecording } from './audio-recording'; -import './audio-recording.css'; -import './ChatMessage.css'; - -// api -import chatmessageApi from '@/api/chatmessage'; -import chatflowsApi from '@/api/chatflows'; -import predictionApi from '@/api/prediction'; -import vectorstoreApi from '@/api/vectorstore'; -import attachmentsApi from '@/api/attachments'; -import chatmessagefeedbackApi from '@/api/chatmessagefeedback'; -import leadsApi from '@/api/lead'; -import executionsApi from '@/api/executions'; -import ttsApi from '@/api/tts'; - -// Hooks -import useApi from '@/hooks/useApi'; -import { flowContext } from '@/store/context/ReactFlowContext'; - -// Const -import { baseURL, maxScroll } from '@/store/constant'; -import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions'; - -// Utils -import { isValidURL, removeDuplicateURL, setLocalStorageChatflow, getLocalStorageChatflow } from '@/utils/genericHelper'; -import useNotifier from '@/utils/useNotifier'; -import FollowUpPromptsCard from '@/ui-component/cards/FollowUpPromptsCard'; - -// History -import { ChatInputHistory } from './ChatInputHistory'; - -const messageImageStyle = { - width: '128px', - height: '128px', - objectFit: 'cover', -}; - -const CardWithDeleteOverlay = ({ item, disabled, customization, onDelete }) => { - const [isHovered, setIsHovered] = useState(false); - const defaultBackgroundColor = customization.isDarkMode ? 'rgba(0, 0, 0, 0.3)' : 'transparent'; - - return ( -
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} style={{ position: 'relative', display: 'inline-block' }}> - - - - {item.name} - - - {isHovered && !disabled && ( - - )} -
- ); -}; - -CardWithDeleteOverlay.propTypes = { - item: PropTypes.object, - customization: PropTypes.object, - disabled: PropTypes.bool, - onDelete: PropTypes.func, -}; - -const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, previews, setPreviews }) => { - const theme = useTheme(); - const customization = useSelector((state) => state.customization); - - const ps = useRef(); - - const dispatch = useDispatch(); - const { onAgentflowNodeStatusUpdate, clearAgentflowNodeStatus } = useContext(flowContext); - - useNotifier(); - const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)); - const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)); - - const [userInput, setUserInput] = useState(''); - const [loading, setLoading] = useState(false); - const [messages, setMessages] = useState([ - { - message: 'Hi there! How can I help?', - type: 'apiMessage', - }, - ]); - const [isChatFlowAvailableToStream, setIsChatFlowAvailableToStream] = useState(false); - const [isChatFlowAvailableForSpeech, setIsChatFlowAvailableForSpeech] = useState(false); - const [sourceDialogOpen, setSourceDialogOpen] = useState(false); - const [sourceDialogProps, setSourceDialogProps] = useState({}); - const [chatId, setChatId] = useState(uuidv4()); - const [isMessageStopping, setIsMessageStopping] = useState(false); - const [uploadedFiles, setUploadedFiles] = useState([]); - const [imageUploadAllowedTypes, setImageUploadAllowedTypes] = useState(''); - const [fileUploadAllowedTypes, setFileUploadAllowedTypes] = useState(''); - const [inputHistory] = useState(new ChatInputHistory(10)); - - const inputRef = useRef(null); - const getChatmessageApi = useApi(chatmessageApi.getInternalChatmessageFromChatflow); - const getAllExecutionsApi = useApi(executionsApi.getAllExecutions); - const getIsChatflowStreamingApi = useApi(chatflowsApi.getIsChatflowStreaming); - const getAllowChatFlowUploads = useApi(chatflowsApi.getAllowChatflowUploads); - const getChatflowConfig = useApi(chatflowsApi.getSpecificChatflow); - - const [starterPrompts, setStarterPrompts] = useState([]); - - // full file upload - const [fullFileUpload, setFullFileUpload] = useState(false); - const [fullFileUploadAllowedTypes, setFullFileUploadAllowedTypes] = useState('*'); - - // feedback - const [chatFeedbackStatus, setChatFeedbackStatus] = useState(false); - const [feedbackId, setFeedbackId] = useState(''); - const [showFeedbackContentDialog, setShowFeedbackContentDialog] = useState(false); - - // leads - const [leadsConfig, setLeadsConfig] = useState(null); - const [leadName, setLeadName] = useState(''); - const [leadEmail, setLeadEmail] = useState(''); - const [leadPhone, setLeadPhone] = useState(''); - const [isLeadSaving, setIsLeadSaving] = useState(false); - const [isLeadSaved, setIsLeadSaved] = useState(false); - - // follow-up prompts - const [followUpPromptsStatus, setFollowUpPromptsStatus] = useState(false); - const [followUpPrompts, setFollowUpPrompts] = useState([]); - - // drag & drop and file input - const imgUploadRef = useRef(null); - const fileUploadRef = useRef(null); - const [isChatFlowAvailableForImageUploads, setIsChatFlowAvailableForImageUploads] = useState(false); - const [isChatFlowAvailableForFileUploads, setIsChatFlowAvailableForFileUploads] = useState(false); - const [isChatFlowAvailableForRAGFileUploads, setIsChatFlowAvailableForRAGFileUploads] = useState(false); - const [isDragActive, setIsDragActive] = useState(false); - - // recording - const [isRecording, setIsRecording] = useState(false); - const [recordingNotSupported, setRecordingNotSupported] = useState(false); - const [isLoadingRecording, setIsLoadingRecording] = useState(false); - - const [openFeedbackDialog, setOpenFeedbackDialog] = useState(false); - const [feedback, setFeedback] = useState(''); - const [pendingActionData, setPendingActionData] = useState(null); - const [feedbackType, setFeedbackType] = useState(''); - - // start input type - const [startInputType, setStartInputType] = useState(''); - const [formTitle, setFormTitle] = useState(''); - const [formDescription, setFormDescription] = useState(''); - const [formInputsData, setFormInputsData] = useState({}); - const [formInputParams, setFormInputParams] = useState([]); - - const [isConfigLoading, setIsConfigLoading] = useState(true); - - // TTS state - const [isTTSLoading, setIsTTSLoading] = useState({}); - const [isTTSPlaying, setIsTTSPlaying] = useState({}); - const [ttsAudio, setTtsAudio] = useState({}); - const [isTTSEnabled, setIsTTSEnabled] = useState(false); - - // TTS streaming state - const [ttsStreamingState, setTtsStreamingState] = useState({ - mediaSource: null, - sourceBuffer: null, - audio: null, - chunkQueue: [], - isBuffering: false, - audioFormat: null, - abortController: null, - }); - - // Ref to prevent auto-scroll during TTS actions (using ref to avoid re-renders) - const isTTSActionRef = useRef(false); - const ttsTimeoutRef = useRef(null); - - const isFileAllowedForUpload = (file) => { - const constraints = getAllowChatFlowUploads.data; - /** - * {isImageUploadAllowed: boolean, imgUploadSizeAndTypes: Array<{ fileTypes: string[], maxUploadSize: number }>} - */ - let acceptFile = false; - - // Early return if constraints are not available yet - if (!constraints) { - console.warn('Upload constraints not loaded yet'); - return false; - } - - if (constraints.isImageUploadAllowed) { - const fileType = file.type; - const sizeInMB = file.size / 1024 / 1024; - if (constraints.imgUploadSizeAndTypes && Array.isArray(constraints.imgUploadSizeAndTypes)) { - constraints.imgUploadSizeAndTypes.forEach((allowed) => { - if (allowed.fileTypes && allowed.fileTypes.includes(fileType) && sizeInMB <= allowed.maxUploadSize) { - acceptFile = true; - } - }); - } - } - - if (fullFileUpload) { - return true; - } else if (constraints.isRAGFileUploadAllowed) { - const fileExt = file.name.split('.').pop(); - if (fileExt && constraints.fileUploadSizeAndTypes && Array.isArray(constraints.fileUploadSizeAndTypes)) { - constraints.fileUploadSizeAndTypes.forEach((allowed) => { - if (allowed.fileTypes && allowed.fileTypes.length === 1 && allowed.fileTypes[0] === '*') { - acceptFile = true; - } else if (allowed.fileTypes && allowed.fileTypes.includes(`.${fileExt}`)) { - acceptFile = true; - } - }); - } - } - if (!acceptFile) { - alert(`Cannot upload file. Kindly check the allowed file types and maximum allowed size.`); - } - return acceptFile; - }; - - const handleDrop = async (e) => { - if (!isChatFlowAvailableForImageUploads && !isChatFlowAvailableForFileUploads) { - return; - } - e.preventDefault(); - setIsDragActive(false); - let files = []; - let uploadedFiles = []; - - if (e.dataTransfer.files.length > 0) { - for (const file of e.dataTransfer.files) { - if (isFileAllowedForUpload(file) === false) { - return; - } - const reader = new FileReader(); - const { name } = file; - // Only add files - if (!file.type || !imageUploadAllowedTypes.includes(file.type)) { - uploadedFiles.push({ file, type: fullFileUpload ? 'file:full' : 'file:rag' }); - } - files.push( - new Promise((resolve) => { - reader.onload = (evt) => { - if (!evt?.target?.result) { - return; - } - const { result } = evt.target; - let previewUrl; - if (file.type.startsWith('audio/')) { - previewUrl = audioUploadSVG; - } else { - previewUrl = URL.createObjectURL(file); - } - resolve({ - data: result, - preview: previewUrl, - type: 'file', - name: name, - mime: file.type, - }); - }; - reader.readAsDataURL(file); - }), - ); - } - - const newFiles = await Promise.all(files); - setUploadedFiles(uploadedFiles); - setPreviews((prevPreviews) => [...prevPreviews, ...newFiles]); - } - - if (e.dataTransfer.items) { - //TODO set files - for (const item of e.dataTransfer.items) { - if (item.kind === 'string' && item.type.match('^text/uri-list')) { - item.getAsString((s) => { - let upload = { - data: s, - preview: s, - type: 'url', - name: s ? s.substring(s.lastIndexOf('/') + 1) : '', - }; - setPreviews((prevPreviews) => [...prevPreviews, upload]); - }); - } else if (item.kind === 'string' && item.type.match('^text/html')) { - item.getAsString((s) => { - if (s.indexOf('href') === -1) return; - //extract href - let start = s ? s.substring(s.indexOf('href') + 6) : ''; - let hrefStr = start.substring(0, start.indexOf('"')); - - let upload = { - data: hrefStr, - preview: hrefStr, - type: 'url', - name: hrefStr ? hrefStr.substring(hrefStr.lastIndexOf('/') + 1) : '', - }; - setPreviews((prevPreviews) => [...prevPreviews, upload]); - }); - } - } - } - }; - - const handleFileChange = async (event) => { - const fileObj = event.target.files && event.target.files[0]; - if (!fileObj) { - return; - } - let files = []; - let uploadedFiles = []; - for (const file of event.target.files) { - if (isFileAllowedForUpload(file) === false) { - return; - } - // Only add files - if (!file.type || !imageUploadAllowedTypes.includes(file.type)) { - uploadedFiles.push({ file, type: fullFileUpload ? 'file:full' : 'file:rag' }); - } - const reader = new FileReader(); - const { name } = file; - files.push( - new Promise((resolve) => { - reader.onload = (evt) => { - if (!evt?.target?.result) { - return; - } - const { result } = evt.target; - resolve({ - data: result, - preview: URL.createObjectURL(file), - type: 'file', - name: name, - mime: file.type, - }); - }; - reader.readAsDataURL(file); - }), - ); - } - - const newFiles = await Promise.all(files); - setUploadedFiles(uploadedFiles); - setPreviews((prevPreviews) => [...prevPreviews, ...newFiles]); - // 👇️ reset file input - event.target.value = null; - }; - - const addRecordingToPreviews = (blob) => { - let mimeType = ''; - const pos = blob.type.indexOf(';'); - if (pos === -1) { - mimeType = blob.type; - } else { - mimeType = blob.type ? blob.type.substring(0, pos) : ''; - } - // read blob and add to previews - const reader = new FileReader(); - reader.readAsDataURL(blob); - reader.onloadend = () => { - const base64data = reader.result; - const upload = { - data: base64data, - preview: audioUploadSVG, - type: 'audio', - name: `audio_${Date.now()}.wav`, - mime: mimeType, - }; - setPreviews((prevPreviews) => [...prevPreviews, upload]); - }; - }; - - const handleDrag = (e) => { - if (isChatFlowAvailableForImageUploads || isChatFlowAvailableForFileUploads) { - e.preventDefault(); - e.stopPropagation(); - if (e.type === 'dragenter' || e.type === 'dragover') { - setIsDragActive(true); - } else if (e.type === 'dragleave') { - setIsDragActive(false); - } - } - }; - - const handleAbort = async () => { - setIsMessageStopping(true); - try { - // Stop all TTS streams first - stopAllTTS(); - - // Abort TTS for any active streams - const activeTTSMessages = Object.keys(isTTSLoading).concat(Object.keys(isTTSPlaying)); - for (const messageId of activeTTSMessages) { - await ttsApi.abortTTS({ chatflowId: chatflowid, chatId, chatMessageId: messageId }); - } - - await chatmessageApi.abortMessage(chatflowid, chatId); - } catch (error) { - setIsMessageStopping(false); - enqueueSnackbar({ - message: typeof error.response.data === 'object' ? error.response.data.message : error.response.data, - options: { - key: new Date().getTime() + Math.random(), - variant: 'error', - persist: true, - action: (key) => ( - - ), - }, - }); - } - }; - - const handleDeletePreview = (itemToDelete) => { - if (itemToDelete.type === 'file') { - URL.revokeObjectURL(itemToDelete.preview); // Clean up for file - } - setPreviews(previews.filter((item) => item !== itemToDelete)); - }; - - const handleFileUploadClick = () => { - // 👇️ open file input box on click of another element - fileUploadRef.current.click(); - }; - - const handleImageUploadClick = () => { - // 👇️ open file input box on click of another element - imgUploadRef.current.click(); - }; - - const clearPreviews = () => { - // Revoke the data uris to avoid memory leaks - previews.forEach((file) => URL.revokeObjectURL(file.preview)); - setPreviews([]); - }; - - const onMicrophonePressed = () => { - setIsRecording(true); - startAudioRecording(setIsRecording, setRecordingNotSupported); - }; - - const onRecordingCancelled = () => { - if (!recordingNotSupported) cancelAudioRecording(); - setIsRecording(false); - setRecordingNotSupported(false); - }; - - const onRecordingStopped = async () => { - setIsLoadingRecording(true); - stopAudioRecording(addRecordingToPreviews); - }; - - const onSourceDialogClick = (data, title) => { - setSourceDialogProps({ data, title }); - setSourceDialogOpen(true); - }; - - const onURLClick = (data) => { - window.open(data, '_blank'); - }; - - const scrollToBottom = () => { - if (ps.current) { - ps.current.scrollTo({ top: maxScroll }); - } - }; - - // Helper function to manage TTS action flag - const setTTSAction = (isActive) => { - isTTSActionRef.current = isActive; - if (ttsTimeoutRef.current) { - clearTimeout(ttsTimeoutRef.current); - ttsTimeoutRef.current = null; - } - if (isActive) { - // Reset the flag after a longer delay to ensure all state changes are complete - ttsTimeoutRef.current = setTimeout(() => { - isTTSActionRef.current = false; - ttsTimeoutRef.current = null; - }, 300); - } - }; - - const onChange = useCallback((e) => setUserInput(e.target.value), [setUserInput]); - - const updateLastMessage = (text) => { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)]; - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; - allMessages[allMessages.length - 1].message += text; - allMessages[allMessages.length - 1].feedback = null; - return allMessages; - }); - }; - - const updateErrorMessage = (errorMessage) => { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)]; - allMessages.push({ message: errorMessage, type: 'apiMessage' }); - return allMessages; - }); - }; - - const updateLastMessageSourceDocuments = (sourceDocuments) => { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)]; - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; - allMessages[allMessages.length - 1].sourceDocuments = sourceDocuments; - return allMessages; - }); - }; - - const updateLastMessageAgentReasoning = (agentReasoning) => { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)]; - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; - allMessages[allMessages.length - 1].agentReasoning = agentReasoning; - return allMessages; - }); - }; - - const updateAgentFlowEvent = (event) => { - if (event === 'INPROGRESS') { - setMessages((prevMessages) => [...prevMessages, { message: '', type: 'apiMessage', agentFlowEventStatus: event }]); - } else { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)]; - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; - allMessages[allMessages.length - 1].agentFlowEventStatus = event; - return allMessages; - }); - } - }; - - const updateAgentFlowExecutedData = (agentFlowExecutedData) => { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)]; - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; - allMessages[allMessages.length - 1].agentFlowExecutedData = agentFlowExecutedData; - return allMessages; - }); - }; - - const updateLastMessageAction = (action) => { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)]; - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; - allMessages[allMessages.length - 1].action = action; - return allMessages; - }); - }; - - const updateLastMessageArtifacts = (artifacts) => { - artifacts.forEach((artifact) => { - if (artifact.type === 'png' || artifact.type === 'jpeg') { - artifact.data = `${baseURL}/api/v1/get-upload-file?chatflowId=${chatflowid}&chatId=${chatId}&fileName=${artifact.data.replace( - 'FILE-STORAGE::', - '', - )}`; - } - }); - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)]; - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; - allMessages[allMessages.length - 1].artifacts = artifacts; - return allMessages; - }); - }; - - const updateLastMessageNextAgent = (nextAgent) => { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)]; - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; - const lastAgentReasoning = allMessages[allMessages.length - 1].agentReasoning; - if (lastAgentReasoning && lastAgentReasoning.length > 0) { - lastAgentReasoning.push({ nextAgent }); - } - allMessages[allMessages.length - 1].agentReasoning = lastAgentReasoning; - return allMessages; - }); - }; - - const updateLastMessageNextAgentFlow = (nextAgentFlow) => { - onAgentflowNodeStatusUpdate(nextAgentFlow); - }; - - const updateLastMessageUsedTools = (usedTools) => { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)]; - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; - allMessages[allMessages.length - 1].usedTools = usedTools; - return allMessages; - }); - }; - - const updateLastMessageFileAnnotations = (fileAnnotations) => { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)]; - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; - allMessages[allMessages.length - 1].fileAnnotations = fileAnnotations; - return allMessages; - }); - }; - - const abortMessage = () => { - setIsMessageStopping(false); - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)]; - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; - const lastAgentReasoning = allMessages[allMessages.length - 1].agentReasoning; - if (lastAgentReasoning && lastAgentReasoning.length > 0) { - allMessages[allMessages.length - 1].agentReasoning = lastAgentReasoning.filter((reasoning) => !reasoning.nextAgent); - } - return allMessages; - }); - setTimeout(() => { - inputRef.current?.focus(); - }, 100); - enqueueSnackbar({ - message: 'Message stopped', - options: { - key: new Date().getTime() + Math.random(), - variant: 'success', - action: (key) => ( - - ), - }, - }); - }; - - const handleError = (message = 'Oops! There seems to be an error. Please try again.') => { - message = message.replace(`Unable to parse JSON response from chat agent.\n\n`, ''); - setMessages((prevMessages) => [...prevMessages, { message, type: 'apiMessage' }]); - setLoading(false); - setUserInput(''); - setUploadedFiles([]); - setTimeout(() => { - inputRef.current?.focus(); - }, 100); - }; - - const handlePromptClick = async (promptStarterInput) => { - setUserInput(promptStarterInput); - handleSubmit(undefined, promptStarterInput); - }; - - const handleFollowUpPromptClick = async (promptStarterInput) => { - setUserInput(promptStarterInput); - setFollowUpPrompts([]); - handleSubmit(undefined, promptStarterInput); - }; - - const onSubmitResponse = (actionData, feedback = '', type = '') => { - let fbType = feedbackType; - if (type) { - fbType = type; - } - const question = feedback ? feedback : fbType.charAt(0).toUpperCase() + fbType.slice(1); - handleSubmit(undefined, question, undefined, { - type: fbType, - startNodeId: actionData?.nodeId, - feedback, - }); - }; - - const handleSubmitFeedback = () => { - if (pendingActionData) { - onSubmitResponse(pendingActionData, feedback); - setOpenFeedbackDialog(false); - setFeedback(''); - setPendingActionData(null); - setFeedbackType(''); - } - }; - - const handleActionClick = async (elem, action) => { - setUserInput(elem.label); - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)]; - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; - allMessages[allMessages.length - 1].action = null; - return allMessages; - }); - if (elem.type.includes('agentflowv2')) { - const type = elem.type.includes('approve') ? 'proceed' : 'reject'; - setFeedbackType(type); - - if (action.data && action.data.input && action.data.input.humanInputEnableFeedback) { - setPendingActionData(action.data); - setOpenFeedbackDialog(true); - } else { - onSubmitResponse(action.data, '', type); - } - } else { - handleSubmit(undefined, elem.label, action); - } - }; - - const updateMetadata = (data, input) => { - // set message id that is needed for feedback - if (data.chatMessageId) { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)]; - if (allMessages[allMessages.length - 1].type === 'apiMessage') { - allMessages[allMessages.length - 1].id = data.chatMessageId; - } - return allMessages; - }); - } - - if (data.chatId) { - setChatId(data.chatId); - } - - if (input === '' && data.question) { - // the response contains the question even if it was in an audio format - // so if input is empty but the response contains the question, update the user message to show the question - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)]; - if (allMessages[allMessages.length - 2].type === 'apiMessage') return allMessages; - allMessages[allMessages.length - 2].message = data.question; - return allMessages; - }); - } - - if (data.followUpPrompts) { - const followUpPrompts = JSON.parse(data.followUpPrompts); - if (typeof followUpPrompts === 'string') { - setFollowUpPrompts(JSON.parse(followUpPrompts)); - } else { - setFollowUpPrompts(followUpPrompts); - } - } - }; - - const handleFileUploads = async (uploads) => { - if (!uploadedFiles.length) return uploads; - - if (fullFileUpload) { - const filesWithFullUploadType = uploadedFiles.filter((file) => file.type === 'file:full'); - if (filesWithFullUploadType.length > 0) { - const formData = new FormData(); - for (const file of filesWithFullUploadType) { - formData.append('files', file.file); - } - formData.append('chatId', chatId); - - const response = await attachmentsApi.createAttachment(chatflowid, chatId, formData); - const data = response.data; - - for (const extractedFileData of data) { - const content = extractedFileData.content; - const fileName = extractedFileData.name; - - // find matching name in previews and replace data with content - const uploadIndex = uploads.findIndex((upload) => upload.name === fileName); - - if (uploadIndex !== -1) { - uploads[uploadIndex] = { - ...uploads[uploadIndex], - data: content, - name: fileName, - type: 'file:full', - }; - } - } - } - } else if (isChatFlowAvailableForRAGFileUploads) { - const filesWithRAGUploadType = uploadedFiles.filter((file) => file.type === 'file:rag'); - - if (filesWithRAGUploadType.length > 0) { - const formData = new FormData(); - for (const file of filesWithRAGUploadType) { - formData.append('files', file.file); - } - formData.append('chatId', chatId); - - await vectorstoreApi.upsertVectorStoreWithFormData(chatflowid, formData); - - // delay for vector store to be updated - const delay = (delayInms) => { - return new Promise((resolve) => setTimeout(resolve, delayInms)); - }; - await delay(2500); //TODO: check if embeddings can be retrieved using file name as metadata filter - - uploads = uploads.map((upload) => { - return { - ...upload, - type: 'file:rag', - }; - }); - } - } - return uploads; - }; - - // Handle form submission - const handleSubmit = async (e, selectedInput, action, humanInput) => { - if (e) e.preventDefault(); - - if (!selectedInput && userInput.trim() === '') { - const containsFile = previews.filter((item) => !item.mime.startsWith('image') && item.type !== 'audio').length > 0; - if (!previews.length || (previews.length && containsFile)) { - return; - } - } - - let input = userInput; - - if (typeof selectedInput === 'string') { - if (selectedInput !== undefined && selectedInput.trim() !== '') input = selectedInput; - - if (input.trim()) { - inputHistory.addToHistory(input); - } - } else if (typeof selectedInput === 'object') { - input = Object.entries(selectedInput) - .map(([key, value]) => `${key}: ${value}`) - .join('\n'); - } - - setLoading(true); - clearAgentflowNodeStatus(); - - let uploads = previews.map((item) => { - return { - data: item.data, - type: item.type, - name: item.name, - mime: item.mime, - }; - }); - - try { - uploads = await handleFileUploads(uploads); - } catch (error) { - handleError('Unable to upload documents'); - return; - } - - clearPreviews(); - setMessages((prevMessages) => [...prevMessages, { message: input, type: 'userMessage', fileUploads: uploads }]); - - // Send user question to Prediction Internal API - try { - const params = { - question: input, - chatId, - }; - if (typeof selectedInput === 'object') { - params.form = selectedInput; - delete params.question; - } - if (uploads && uploads.length > 0) params.uploads = uploads; - if (leadEmail) params.leadEmail = leadEmail; - if (action) params.action = action; - if (humanInput) params.humanInput = humanInput; - - if (isChatFlowAvailableToStream) { - fetchResponseFromEventStream(chatflowid, params); - } else { - const response = await predictionApi.sendMessageAndGetPrediction(chatflowid, params); - if (response.data) { - const data = response.data; - - updateMetadata(data, input); - - let text = ''; - if (data.text) text = data.text; - else if (data.json) text = '```json\n' + JSON.stringify(data.json, null, 2); - else text = JSON.stringify(data, null, 2); - - setMessages((prevMessages) => [ - ...prevMessages, - { - message: text, - id: data?.chatMessageId, - sourceDocuments: data?.sourceDocuments, - usedTools: data?.usedTools, - calledTools: data?.calledTools, - fileAnnotations: data?.fileAnnotations, - agentReasoning: data?.agentReasoning, - agentFlowExecutedData: data?.agentFlowExecutedData, - action: data?.action, - artifacts: data?.artifacts, - type: 'apiMessage', - feedback: null, - }, - ]); - - setLocalStorageChatflow(chatflowid, data.chatId); - setLoading(false); - setUserInput(''); - setUploadedFiles([]); - - setTimeout(() => { - inputRef.current?.focus(); - scrollToBottom(); - }, 100); - } - } - } catch (error) { - handleError(error.response.data.message); - return; - } - }; - - const fetchResponseFromEventStream = async (chatflowid, params) => { - const chatId = params.chatId; - const input = params.question; - params.streaming = true; - await fetchEventSource(`${baseURL}/api/v1/internal-prediction/${chatflowid}`, { - openWhenHidden: true, - method: 'POST', - body: JSON.stringify(params), - headers: { - 'Content-Type': 'application/json', - 'x-request-from': 'internal', - }, - async onopen(response) { - if (response.ok && response.headers.get('content-type') === EventStreamContentType) { - //console.log('EventSource Open') - } - }, - async onmessage(ev) { - const payload = JSON.parse(ev.data); - switch (payload.event) { - case 'start': - setMessages((prevMessages) => [...prevMessages, { message: '', type: 'apiMessage' }]); - break; - case 'token': - updateLastMessage(payload.data); - break; - case 'sourceDocuments': - updateLastMessageSourceDocuments(payload.data); - break; - case 'usedTools': - updateLastMessageUsedTools(payload.data); - break; - case 'fileAnnotations': - updateLastMessageFileAnnotations(payload.data); - break; - case 'agentReasoning': - updateLastMessageAgentReasoning(payload.data); - break; - case 'agentFlowEvent': - updateAgentFlowEvent(payload.data); - break; - case 'agentFlowExecutedData': - updateAgentFlowExecutedData(payload.data); - break; - case 'artifacts': - updateLastMessageArtifacts(payload.data); - break; - case 'action': - updateLastMessageAction(payload.data); - break; - case 'nextAgent': - updateLastMessageNextAgent(payload.data); - break; - case 'nextAgentFlow': - updateLastMessageNextAgentFlow(payload.data); - break; - case 'metadata': - updateMetadata(payload.data, input); - break; - case 'error': - updateErrorMessage(payload.data); - break; - case 'abort': - abortMessage(payload.data); - closeResponse(); - break; - case 'tts_start': - handleTTSStart(payload.data); - break; - case 'tts_data': - handleTTSDataChunk(payload.data.audioChunk); - break; - case 'tts_end': - handleTTSEnd(); - break; - case 'tts_abort': - handleTTSAbort(payload.data); - break; - case 'end': - setLocalStorageChatflow(chatflowid, chatId); - closeResponse(); - break; - } - }, - async onclose() { - closeResponse(); - }, - async onerror(err) { - console.error('EventSource Error: ', err); - closeResponse(); - throw err; - }, - }); - }; - - const closeResponse = () => { - setLoading(false); - setUserInput(''); - setUploadedFiles([]); - setTimeout(() => { - inputRef.current?.focus(); - scrollToBottom(); - }, 100); - }; - // Prevent blank submissions and allow for multiline input - const handleEnter = (e) => { - // Check if IME composition is in progress - const isIMEComposition = e.isComposing || e.keyCode === 229; - if (e.key === 'ArrowUp' && !isIMEComposition) { - e.preventDefault(); - const previousInput = inputHistory.getPreviousInput(userInput); - setUserInput(previousInput); - } else if (e.key === 'ArrowDown' && !isIMEComposition) { - e.preventDefault(); - const nextInput = inputHistory.getNextInput(); - setUserInput(nextInput); - } else if (e.key === 'Enter' && userInput && !isIMEComposition) { - if (!e.shiftKey && userInput) { - handleSubmit(e); - } - } else if (e.key === 'Enter') { - e.preventDefault(); - } - }; - - const getLabel = (URL, source) => { - if (URL && typeof URL === 'object') { - if (URL.pathname && typeof URL.pathname === 'string') { - if (URL.pathname.substring(0, 15) === '/') { - return URL.host || ''; - } else { - return `${URL.pathname.substring(0, 15)}...`; - } - } else if (URL.host) { - return URL.host; - } - } - - if (source && source.pageContent && typeof source.pageContent === 'string') { - return `${source.pageContent.substring(0, 15)}...`; - } - - return ''; - }; - - const getFileUploadAllowedTypes = () => { - if (fullFileUpload) { - return fullFileUploadAllowedTypes === '' ? '*' : fullFileUploadAllowedTypes; - } - return fileUploadAllowedTypes.includes('*') ? '*' : fileUploadAllowedTypes || '*'; - }; - - const downloadFile = async (fileAnnotation) => { - try { - const response = await axios.post( - `${baseURL}/api/v1/openai-assistants-file/download`, - { fileName: fileAnnotation.fileName, chatflowId: chatflowid, chatId: chatId }, - { responseType: 'blob' }, - ); - const blob = new Blob([response.data], { type: response.headers['content-type'] }); - const downloadUrl = window.URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = downloadUrl; - link.download = fileAnnotation.fileName; - document.body.appendChild(link); - link.click(); - link.remove(); - } catch (error) { - console.error('Download failed:', error); - } - }; - - const getAgentIcon = (nodeName, instructions) => { - if (nodeName) { - return `${baseURL}/api/v1/node-icon/${nodeName}`; - } else if (instructions) { - return multiagent_supervisorPNG; - } else { - return multiagent_workerPNG; - } - }; - - // Get chatmessages successful - useEffect(() => { - if (getChatmessageApi.data?.length) { - const chatId = getChatmessageApi.data[0]?.chatId; - setChatId(chatId); - const loadedMessages = getChatmessageApi.data.map((message) => { - const obj = { - id: message.id, - message: message.content, - feedback: message.feedback, - type: message.role, - }; - if (message.sourceDocuments) obj.sourceDocuments = message.sourceDocuments; - if (message.usedTools) obj.usedTools = message.usedTools; - if (message.fileAnnotations) obj.fileAnnotations = message.fileAnnotations; - if (message.agentReasoning) obj.agentReasoning = message.agentReasoning; - if (message.action) obj.action = message.action; - if (message.artifacts) { - obj.artifacts = message.artifacts; - obj.artifacts.forEach((artifact) => { - if (artifact.type === 'png' || artifact.type === 'jpeg') { - artifact.data = `${baseURL}/api/v1/get-upload-file?chatflowId=${chatflowid}&chatId=${chatId}&fileName=${artifact.data.replace( - 'FILE-STORAGE::', - '', - )}`; - } - }); - } - if (message.fileUploads) { - obj.fileUploads = message.fileUploads; - obj.fileUploads.forEach((file) => { - if (file.type === 'stored-file') { - file.data = `${baseURL}/api/v1/get-upload-file?chatflowId=${chatflowid}&chatId=${chatId}&fileName=${file.name}`; - } - }); - } - if (message.followUpPrompts) obj.followUpPrompts = JSON.parse(message.followUpPrompts); - if (message.role === 'apiMessage' && message.execution && message.execution.executionData) - obj.agentFlowExecutedData = JSON.parse(message.execution.executionData); - return obj; - }); - setMessages((prevMessages) => [...prevMessages, ...loadedMessages]); - setLocalStorageChatflow(chatflowid, chatId); - } - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [getChatmessageApi.data]); - - useEffect(() => { - if (getAllExecutionsApi.data?.length) { - const chatId = getAllExecutionsApi.data[0]?.sessionId; - setChatId(chatId); - const loadedMessages = getAllExecutionsApi.data.map((execution) => { - const executionData = typeof execution.executionData === 'string' ? JSON.parse(execution.executionData) : execution.executionData; - const obj = { - id: execution.id, - agentFlow: executionData, - }; - return obj; - }); - setMessages((prevMessages) => [...prevMessages, ...loadedMessages]); - setLocalStorageChatflow(chatflowid, chatId); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [getAllExecutionsApi.data]); - - // Get chatflow streaming capability - useEffect(() => { - if (getIsChatflowStreamingApi.data) { - setIsChatFlowAvailableToStream(getIsChatflowStreamingApi.data?.isStreaming ?? false); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [getIsChatflowStreamingApi.data]); - - // Get chatflow uploads capability - useEffect(() => { - if (getAllowChatFlowUploads.data) { - setIsChatFlowAvailableForImageUploads(getAllowChatFlowUploads.data?.isImageUploadAllowed ?? false); - setIsChatFlowAvailableForRAGFileUploads(getAllowChatFlowUploads.data?.isRAGFileUploadAllowed ?? false); - setIsChatFlowAvailableForSpeech(getAllowChatFlowUploads.data?.isSpeechToTextEnabled ?? false); - setImageUploadAllowedTypes(getAllowChatFlowUploads.data?.imgUploadSizeAndTypes.map((allowed) => allowed.fileTypes).join(',')); - setFileUploadAllowedTypes(getAllowChatFlowUploads.data?.fileUploadSizeAndTypes.map((allowed) => allowed.fileTypes).join(',')); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [getAllowChatFlowUploads.data]); - - useEffect(() => { - if (getChatflowConfig.data) { - setIsConfigLoading(false); - if (getChatflowConfig.data?.flowData) { - let nodes = JSON.parse(getChatflowConfig.data?.flowData).nodes ?? []; - const startNode = nodes.find((node) => node.data.name === 'startAgentflow'); - if (startNode) { - const startInputType = startNode.data.inputs?.startInputType; - setStartInputType(startInputType); - - const formInputTypes = startNode.data.inputs?.formInputTypes; - if (startInputType === 'formInput' && formInputTypes && formInputTypes.length > 0) { - for (const formInputType of formInputTypes) { - if (formInputType.type === 'options') { - formInputType.options = formInputType.addOptions.map((option) => ({ - label: option.option, - name: option.option, - })); - } - } - setFormInputParams(formInputTypes); - setFormInputsData({ - id: 'formInput', - inputs: {}, - inputParams: formInputTypes, - }); - setFormTitle(startNode.data.inputs?.formTitle); - setFormDescription(startNode.data.inputs?.formDescription); - } - - getAllExecutionsApi.request({ agentflowId: chatflowid }); - } - } - - if (getChatflowConfig.data?.chatbotConfig && JSON.parse(getChatflowConfig.data?.chatbotConfig)) { - let config = JSON.parse(getChatflowConfig.data?.chatbotConfig); - if (config.starterPrompts) { - let inputFields = []; - Object.getOwnPropertyNames(config.starterPrompts).forEach((key) => { - if (config.starterPrompts[key]) { - inputFields.push(config.starterPrompts[key]); - } - }); - setStarterPrompts(inputFields.filter((field) => field.prompt !== '')); - } - if (config.chatFeedback) { - setChatFeedbackStatus(config.chatFeedback.status); - } - - if (config.leads) { - setLeadsConfig(config.leads); - if (config.leads.status && !getLocalStorageChatflow(chatflowid).lead) { - setMessages((prevMessages) => { - const leadCaptureMessage = { - message: '', - type: 'leadCaptureMessage', - }; - - return [...prevMessages, leadCaptureMessage]; - }); - } - } - - if (config.followUpPrompts) { - setFollowUpPromptsStatus(config.followUpPrompts.status); - } - - if (config.fullFileUpload) { - setFullFileUpload(config.fullFileUpload.status); - if (config.fullFileUpload?.allowedUploadFileTypes) { - setFullFileUploadAllowedTypes(config.fullFileUpload?.allowedUploadFileTypes); - } - } - } - } - - // Check if TTS is configured - if (getChatflowConfig.data && getChatflowConfig.data.textToSpeech) { - try { - const ttsConfig = - typeof getChatflowConfig.data.textToSpeech === 'string' - ? JSON.parse(getChatflowConfig.data.textToSpeech) - : getChatflowConfig.data.textToSpeech; - - let isEnabled = false; - if (ttsConfig) { - Object.keys(ttsConfig).forEach((provider) => { - if (provider !== 'none' && ttsConfig?.[provider]?.status) { - isEnabled = true; - } - }); - } - setIsTTSEnabled(isEnabled); - } catch (error) { - setIsTTSEnabled(false); - } - } else { - setIsTTSEnabled(false); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [getChatflowConfig.data]); - - useEffect(() => { - if (getChatflowConfig.error) { - setIsConfigLoading(false); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [getChatflowConfig.error]); - - useEffect(() => { - if (fullFileUpload) { - setIsChatFlowAvailableForFileUploads(true); - } else if (isChatFlowAvailableForRAGFileUploads) { - setIsChatFlowAvailableForFileUploads(true); - } else { - setIsChatFlowAvailableForFileUploads(false); - } - }, [isChatFlowAvailableForRAGFileUploads, fullFileUpload]); - - // Auto scroll chat to bottom (but not during TTS actions) - useEffect(() => { - if (!isTTSActionRef.current) { - scrollToBottom(); - } - }, [messages]); - - useEffect(() => { - if (isDialog && inputRef) { - setTimeout(() => { - inputRef.current?.focus(); - }, 100); - } - }, [isDialog, inputRef]); - - useEffect(() => { - if (open && chatflowid) { - // API request - getChatmessageApi.request(chatflowid); - getIsChatflowStreamingApi.request(chatflowid); - getAllowChatFlowUploads.request(chatflowid); - getChatflowConfig.request(chatflowid); - - // Add a small delay to ensure content is rendered before scrolling - setTimeout(() => { - scrollToBottom(); - }, 100); - - setIsRecording(false); - setIsConfigLoading(true); - - // leads - const savedLead = getLocalStorageChatflow(chatflowid)?.lead; - if (savedLead) { - setIsLeadSaved(!!savedLead); - setLeadEmail(savedLead.email); - } - } - - return () => { - setUserInput(''); - setUploadedFiles([]); - setLoading(false); - setMessages([ - { - message: 'Hi there! How can I help?', - type: 'apiMessage', - }, - ]); - }; - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [open, chatflowid]); - - useEffect(() => { - // wait for audio recording to load and then send - const containsAudio = previews.filter((item) => item.type === 'audio').length > 0; - if (previews.length >= 1 && containsAudio) { - setIsRecording(false); - setRecordingNotSupported(false); - handlePromptClick(''); - } - // eslint-disable-next-line - }, [previews]); - - useEffect(() => { - if (followUpPromptsStatus && messages.length > 0) { - const lastMessage = messages[messages.length - 1]; - if (lastMessage.type === 'apiMessage' && lastMessage.followUpPrompts) { - if (Array.isArray(lastMessage.followUpPrompts)) { - setFollowUpPrompts(lastMessage.followUpPrompts); - } - if (typeof lastMessage.followUpPrompts === 'string') { - const followUpPrompts = JSON.parse(lastMessage.followUpPrompts); - setFollowUpPrompts(followUpPrompts); - } - } else if (lastMessage.type === 'userMessage') { - setFollowUpPrompts([]); - } - } - }, [followUpPromptsStatus, messages]); - - const copyMessageToClipboard = async (text) => { - try { - await navigator.clipboard.writeText(text || ''); - } catch (error) { - console.error('Error copying to clipboard:', error); - } - }; - - const onThumbsUpClick = async (messageId) => { - const body = { - chatflowid, - chatId, - messageId, - rating: 'THUMBS_UP', - content: '', - }; - const result = await chatmessagefeedbackApi.addFeedback(chatflowid, body); - if (result.data) { - const data = result.data; - let id = ''; - if (data && data.id) id = data.id; - setMessages((prevMessages) => { - const allMessages = [...cloneDeep(prevMessages)]; - return allMessages.map((message) => { - if (message.id === messageId) { - message.feedback = { - rating: 'THUMBS_UP', - }; - } - return message; - }); - }); - setFeedbackId(id); - setShowFeedbackContentDialog(true); - } - }; - - const onThumbsDownClick = async (messageId) => { - const body = { - chatflowid, - chatId, - messageId, - rating: 'THUMBS_DOWN', - content: '', - }; - const result = await chatmessagefeedbackApi.addFeedback(chatflowid, body); - if (result.data) { - const data = result.data; - let id = ''; - if (data && data.id) id = data.id; - setMessages((prevMessages) => { - const allMessages = [...cloneDeep(prevMessages)]; - return allMessages.map((message) => { - if (message.id === messageId) { - message.feedback = { - rating: 'THUMBS_DOWN', - }; - } - return message; - }); - }); - setFeedbackId(id); - setShowFeedbackContentDialog(true); - } - }; - - const submitFeedbackContent = async (text) => { - const body = { - content: text, - }; - const result = await chatmessagefeedbackApi.updateFeedback(feedbackId, body); - if (result.data) { - setFeedbackId(''); - setShowFeedbackContentDialog(false); - } - }; - - const handleLeadCaptureSubmit = async (event) => { - if (event) event.preventDefault(); - setIsLeadSaving(true); - - const body = { - chatflowid, - chatId, - name: leadName, - email: leadEmail, - phone: leadPhone, - }; - - const result = await leadsApi.addLead(body); - if (result.data) { - const data = result.data; - setChatId(data.chatId); - setLocalStorageChatflow(chatflowid, data.chatId, { lead: { name: leadName, email: leadEmail, phone: leadPhone } }); - setIsLeadSaved(true); - setLeadEmail(leadEmail); - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)]; - if (allMessages[allMessages.length - 1].type !== 'leadCaptureMessage') return allMessages; - allMessages[allMessages.length - 1].message = leadsConfig.successMessage || 'Thank you for submitting your contact information.'; - return allMessages; - }); - } - - setIsLeadSaving(false); - }; - - const cleanupTTSForMessage = (messageId) => { - if (ttsAudio[messageId]) { - ttsAudio[messageId].pause(); - ttsAudio[messageId].currentTime = 0; - setTtsAudio((prev) => { - const newState = { ...prev }; - delete newState[messageId]; - return newState; - }); - } - - if (ttsStreamingState.audio) { - ttsStreamingState.audio.pause(); - cleanupTTSStreaming(); - } - - setIsTTSPlaying((prev) => { - const newState = { ...prev }; - delete newState[messageId]; - return newState; - }); - - setIsTTSLoading((prev) => { - const newState = { ...prev }; - delete newState[messageId]; - return newState; - }); - }; - - const handleTTSStop = async (messageId) => { - setTTSAction(true); - await ttsApi.abortTTS({ chatflowId: chatflowid, chatId, chatMessageId: messageId }); - cleanupTTSForMessage(messageId); - }; - - const stopAllTTS = () => { - Object.keys(ttsAudio).forEach((messageId) => { - if (ttsAudio[messageId]) { - ttsAudio[messageId].pause(); - ttsAudio[messageId].currentTime = 0; - } - }); - setTtsAudio({}); - - if (ttsStreamingState.abortController) { - ttsStreamingState.abortController.abort(); - } - - if (ttsStreamingState.audio) { - ttsStreamingState.audio.pause(); - cleanupTTSStreaming(); - } - - setIsTTSPlaying({}); - setIsTTSLoading({}); - }; - - const handleTTSClick = async (messageId, messageText) => { - if (isTTSLoading[messageId]) return; - - if (isTTSPlaying[messageId] || ttsAudio[messageId]) { - handleTTSStop(messageId); - return; - } - - setTTSAction(true); - stopAllTTS(); - - handleTTSStart({ chatMessageId: messageId, format: 'mp3' }); - try { - const abortController = new AbortController(); - setTtsStreamingState((prev) => ({ ...prev, abortController })); - - const response = await fetch('/api/v1/text-to-speech/generate', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'x-request-from': 'internal', - }, - credentials: 'include', - signal: abortController.signal, - body: JSON.stringify({ - chatflowId: chatflowid, - chatId: chatId, - chatMessageId: messageId, - text: messageText, - }), - }); - - if (!response.ok) { - throw new Error(`TTS request failed: ${response.status}`); - } - - const reader = response.body.getReader(); - const decoder = new TextDecoder(); - let buffer = ''; - - let done = false; - while (!done) { - if (abortController.signal.aborted) { - break; - } - - const result = await reader.read(); - done = result.done; - if (done) { - break; - } - const value = result.value; - const chunk = decoder.decode(value, { stream: true }); - buffer += chunk; - - const lines = buffer.split('\n\n'); - buffer = lines.pop() || ''; - - for (const eventBlock of lines) { - if (eventBlock.trim()) { - const event = parseSSEEvent(eventBlock); - if (event) { - switch (event.event) { - case 'tts_start': - break; - case 'tts_data': - if (!abortController.signal.aborted) { - handleTTSDataChunk(event.data.audioChunk); - } - break; - case 'tts_end': - if (!abortController.signal.aborted) { - handleTTSEnd(); - } - break; - } - } - } - } - } - } catch (error) { - if (error.name === 'AbortError') { - console.error('TTS request was aborted'); - } else { - console.error('Error with TTS:', error); - enqueueSnackbar({ - message: `TTS failed: ${error.message}`, - options: { variant: 'error' }, - }); - } - } finally { - setIsTTSLoading((prev) => { - const newState = { ...prev }; - delete newState[messageId]; - return newState; - }); - } - }; - - const parseSSEEvent = (eventBlock) => { - const lines = eventBlock.split('\n'); - const event = {}; - - for (const line of lines) { - if (line.startsWith('event:')) { - event.event = line.substring(6).trim(); - } else if (line.startsWith('data:')) { - const dataStr = line.substring(5).trim(); - try { - const parsed = JSON.parse(dataStr); - if (parsed.data) { - event.data = parsed.data; - } - } catch (e) { - console.error('Error parsing SSE data:', e, 'Raw data:', dataStr); - } - } - } - - return event.event ? event : null; - }; - - const initializeTTSStreaming = (data) => { - try { - const mediaSource = new MediaSource(); - const audio = new Audio(); - audio.src = URL.createObjectURL(mediaSource); - - mediaSource.addEventListener('sourceopen', () => { - try { - const mimeType = data.format === 'mp3' ? 'audio/mpeg' : 'audio/mpeg'; - const sourceBuffer = mediaSource.addSourceBuffer(mimeType); - - setTtsStreamingState((prevState) => ({ - ...prevState, - mediaSource, - sourceBuffer, - audio, - })); - - audio.play().catch((playError) => { - console.error('Error starting audio playback:', playError); - }); - } catch (error) { - console.error('Error setting up source buffer:', error); - console.error('MediaSource readyState:', mediaSource.readyState); - console.error('Requested MIME type:', mimeType); - } - }); - - audio.addEventListener('playing', () => { - setIsTTSLoading((prevState) => { - const newState = { ...prevState }; - newState[data.chatMessageId] = false; - return newState; - }); - setIsTTSPlaying((prevState) => ({ - ...prevState, - [data.chatMessageId]: true, - })); - }); - - audio.addEventListener('ended', () => { - setIsTTSPlaying((prevState) => { - const newState = { ...prevState }; - delete newState[data.chatMessageId]; - return newState; - }); - cleanupTTSStreaming(); - }); - } catch (error) { - console.error('Error initializing TTS streaming:', error); - } - }; - - const cleanupTTSStreaming = () => { - setTtsStreamingState((prevState) => { - if (prevState.abortController) { - prevState.abortController.abort(); - } - - if (prevState.audio) { - prevState.audio.pause(); - prevState.audio.removeAttribute('src'); - if (prevState.audio.src) { - URL.revokeObjectURL(prevState.audio.src); - } - } - - if (prevState.mediaSource) { - if (prevState.mediaSource.readyState === 'open') { - try { - prevState.mediaSource.endOfStream(); - } catch (e) { - // Ignore errors during cleanup - } - } - prevState.mediaSource.removeEventListener('sourceopen', () => {}); - } - - return { - mediaSource: null, - sourceBuffer: null, - audio: null, - chunkQueue: [], - isBuffering: false, - audioFormat: null, - abortController: null, - }; - }); - }; - - const processChunkQueue = () => { - setTtsStreamingState((prevState) => { - if (!prevState.sourceBuffer || prevState.sourceBuffer.updating || prevState.chunkQueue.length === 0) { - return prevState; - } - - const chunk = prevState.chunkQueue.shift(); - - try { - prevState.sourceBuffer.appendBuffer(chunk); - return { - ...prevState, - chunkQueue: [...prevState.chunkQueue], - isBuffering: true, - }; - } catch (error) { - console.error('Error appending chunk to buffer:', error); - return prevState; - } - }); - }; - - const handleTTSStart = (data) => { - setTTSAction(true); - - // Stop all existing TTS audio before starting new stream - stopAllTTS(); - - setIsTTSLoading((prevState) => ({ - ...prevState, - [data.chatMessageId]: true, - })); - setMessages((prevMessages) => { - const allMessages = [...cloneDeep(prevMessages)]; - const lastMessage = allMessages[allMessages.length - 1]; - if (lastMessage.type === 'userMessage') return allMessages; - if (lastMessage.id) return allMessages; - allMessages[allMessages.length - 1].id = data.chatMessageId; - return allMessages; - }); - setTtsStreamingState({ - mediaSource: null, - sourceBuffer: null, - audio: null, - chunkQueue: [], - isBuffering: false, - audioFormat: data.format, - abortController: null, - }); - - setTimeout(() => initializeTTSStreaming(data), 0); - }; - - const handleTTSDataChunk = (base64Data) => { - try { - const audioBuffer = Uint8Array.from(atob(base64Data), (c) => c.charCodeAt(0)); - - setTtsStreamingState((prevState) => { - const newState = { - ...prevState, - chunkQueue: [...prevState.chunkQueue, audioBuffer], - }; - - if (prevState.sourceBuffer && !prevState.sourceBuffer.updating) { - setTimeout(() => processChunkQueue(), 0); - } - - return newState; - }); - } catch (error) { - console.error('Error handling TTS data chunk:', error); - } - }; - - const handleTTSEnd = () => { - setTtsStreamingState((prevState) => { - if (prevState.mediaSource && prevState.mediaSource.readyState === 'open') { - try { - if (prevState.sourceBuffer && prevState.chunkQueue.length > 0 && !prevState.sourceBuffer.updating) { - const remainingChunks = [...prevState.chunkQueue]; - remainingChunks.forEach((chunk, index) => { - setTimeout(() => { - if (prevState.sourceBuffer && !prevState.sourceBuffer.updating) { - try { - prevState.sourceBuffer.appendBuffer(chunk); - if (index === remainingChunks.length - 1) { - setTimeout(() => { - if (prevState.mediaSource && prevState.mediaSource.readyState === 'open') { - prevState.mediaSource.endOfStream(); - } - }, 100); - } - } catch (error) { - console.error('Error appending remaining chunk:', error); - } - } - }, index * 50); - }); - return { - ...prevState, - chunkQueue: [], - }; - } - - if (prevState.sourceBuffer && !prevState.sourceBuffer.updating) { - prevState.mediaSource.endOfStream(); - } else if (prevState.sourceBuffer) { - prevState.sourceBuffer.addEventListener( - 'updateend', - () => { - if (prevState.mediaSource && prevState.mediaSource.readyState === 'open') { - prevState.mediaSource.endOfStream(); - } - }, - { once: true }, - ); - } - } catch (error) { - console.error('Error ending TTS stream:', error); - } - } - return prevState; - }); - }; - - const handleTTSAbort = (data) => { - const messageId = data.chatMessageId; - cleanupTTSForMessage(messageId); - }; - - useEffect(() => { - if (ttsStreamingState.sourceBuffer) { - const sourceBuffer = ttsStreamingState.sourceBuffer; - - const handleUpdateEnd = () => { - setTtsStreamingState((prevState) => ({ - ...prevState, - isBuffering: false, - })); - setTimeout(() => processChunkQueue(), 0); - }; - - sourceBuffer.addEventListener('updateend', handleUpdateEnd); - - return () => { - sourceBuffer.removeEventListener('updateend', handleUpdateEnd); - }; - } - }, [ttsStreamingState.sourceBuffer]); - - useEffect(() => { - return () => { - cleanupTTSStreaming(); - // Cleanup TTS timeout on unmount - if (ttsTimeoutRef.current) { - clearTimeout(ttsTimeoutRef.current); - ttsTimeoutRef.current = null; - } - }; - }, []); - - const getInputDisabled = () => { - return ( - loading || - !chatflowid || - (leadsConfig?.status && !isLeadSaved) || - (messages[messages.length - 1].action && Object.keys(messages[messages.length - 1].action).length > 0) - ); - }; - - const previewDisplay = (item) => { - if (item.mime.startsWith('image/')) { - return ( - handleDeletePreview(item)} - > - - - - - - - ); - } else if (item.mime.startsWith('audio/')) { - return ( - - - handleDeletePreview(item)} size="small"> - - - - ); - } else { - return ( - handleDeletePreview(item)} /> - ); - } - }; - - const renderFileUploads = (item, index) => { - if (item?.mime?.startsWith('image/')) { - return ( - - - - ); - } else if (item?.mime?.startsWith('audio/')) { - return ( - /* eslint-disable jsx-a11y/media-has-caption */ - - ); - } else { - return ( - - - - {item.name} - - - ); - } - }; - - const agentReasoningArtifacts = (artifacts) => { - const newArtifacts = cloneDeep(artifacts); - for (let i = 0; i < newArtifacts.length; i++) { - const artifact = newArtifacts[i]; - if (artifact && (artifact.type === 'png' || artifact.type === 'jpeg')) { - const data = artifact.data; - newArtifacts[i].data = `${baseURL}/api/v1/get-upload-file?chatflowId=${chatflowid}&chatId=${chatId}&fileName=${data.replace( - 'FILE-STORAGE::', - '', - )}`; - } - } - return newArtifacts; - }; - - const renderArtifacts = (item, index, isAgentReasoning) => { - if (item.type === 'png' || item.type === 'jpeg') { - return ( - - - - ); - } else if (item.type === 'html') { - return ( -
- -
- ); - } else { - return ( - - {item.data} - - ); - } - }; - - if (isConfigLoading) { - return ( - - - - - - ); - } - - if (startInputType === 'formInput' && messages.length === 1) { - return ( - - - - - {formTitle || 'Please Fill Out The Form'} - - - {formDescription || 'Complete all fields below to continue'} - - - {/* Form inputs */} - - {formInputParams && - formInputParams.map((inputParam, index) => ( - - { - setFormInputsData((prev) => ({ - ...prev, - inputs: { - ...prev.inputs, - [inputParam.name]: newValue, - }, - })); - }} - /> - - ))} - - - - - - - ); - } - - return ( -
- {isDragActive && ( -
- )} - {isDragActive && (getAllowChatFlowUploads.data?.isImageUploadAllowed || getAllowChatFlowUploads.data?.isRAGFileUploadAllowed) && ( - - Drop here to upload - {[...getAllowChatFlowUploads.data.imgUploadSizeAndTypes, ...getAllowChatFlowUploads.data.fileUploadSizeAndTypes].map((allowed) => { - return ( - <> - {allowed.fileTypes?.join(', ')} - {allowed.maxUploadSize && Max Allowed Size: {allowed.maxUploadSize} MB} - - ); - })} - - )} -
-
- {messages && - messages.map((message, index) => { - return ( - // The latest message sent by the user will be animated while waiting for a response - - {/* Display the correct icon depending on the message type */} - {message.type === 'apiMessage' || message.type === 'leadCaptureMessage' ? ( - AI - ) : ( - Me - )} -
- {message.fileUploads && message.fileUploads.length > 0 && ( -
- {message.fileUploads.map((item, index) => { - return <>{renderFileUploads(item, index)}; - })} -
- )} - {message.agentReasoning && message.agentReasoning.length > 0 && ( -
- {message.agentReasoning.map((agent, index) => ( - - ))} -
- )} - {message.agentFlowExecutedData && Array.isArray(message.agentFlowExecutedData) && message.agentFlowExecutedData.length > 0 && ( - - )} - {message.usedTools && ( -
- {message.usedTools.map((tool, index) => { - return tool ? ( - } - onClick={() => onSourceDialogClick(tool, 'Used Tools')} - /> - ) : null; - })} -
- )} - {message.artifacts && ( -
- {message.artifacts.map((item, index) => { - return item !== null ? <>{renderArtifacts(item, index)} : null; - })} -
- )} -
- {message.type === 'leadCaptureMessage' && !getLocalStorageChatflow(chatflowid)?.lead && leadsConfig.status ? ( - - - {leadsConfig.title || 'Let us know where we can reach you:'} - -
- {leadsConfig.name && ( - setLeadName(e.target.value)} - /> - )} - {leadsConfig.email && ( - setLeadEmail(e.target.value)} - /> - )} - {leadsConfig.phone && ( - setLeadPhone(e.target.value)} - /> - )} - - - - -
- ) : ( - <> - - {message.message} - - - )} -
- {message.fileAnnotations && ( -
- {message.fileAnnotations.map((fileAnnotation, index) => { - return ( - - ); - })} -
- )} - {message.sourceDocuments && ( -
- {removeDuplicateURL(message).map((source, index) => { - const URL = source.metadata && source.metadata.source ? isValidURL(source.metadata.source) : undefined; - return ( - (URL ? onURLClick(source.metadata.source) : onSourceDialogClick(source))} - /> - ); - })} -
- )} - {message.action && ( -
- {(message.action.elements || []).map((elem, index) => { - return ( - <> - {(elem.type === 'approve-button' && elem.label === 'Yes') || elem.type === 'agentflowv2-approve-button' ? ( - - ) : (elem.type === 'reject-button' && elem.label === 'No') || elem.type === 'agentflowv2-reject-button' ? ( - - ) : ( - - )} - - ); - })} -
- )} - {message.type === 'apiMessage' && message.id ? ( - <> - - {isTTSEnabled && ( - (isTTSPlaying[message.id] ? handleTTSStop(message.id) : handleTTSClick(message.id, message.message))} - disabled={isTTSLoading[message.id]} - sx={{ - backgroundColor: ttsAudio[message.id] ? 'primary.main' : 'transparent', - color: ttsAudio[message.id] ? 'white' : 'inherit', - '&:hover': { - backgroundColor: ttsAudio[message.id] ? 'primary.dark' : 'action.hover', - }, - }} - > - {isTTSLoading[message.id] ? ( - - ) : isTTSPlaying[message.id] ? ( - - ) : ( - - )} - - )} - {chatFeedbackStatus && ( - <> - copyMessageToClipboard(message.message)} /> - {!message.feedback || message.feedback.rating === '' || message.feedback.rating === 'THUMBS_UP' ? ( - onThumbsUpClick(message.id)} - /> - ) : null} - {!message.feedback || message.feedback.rating === '' || message.feedback.rating === 'THUMBS_DOWN' ? ( - onThumbsDownClick(message.id)} - /> - ) : null} - - )} - - - ) : null} -
-
- ); - })} -
-
- - {messages && messages.length === 1 && starterPrompts.length > 0 && ( -
- 0 ? 70 : 0 }} - starterPrompts={starterPrompts || []} - onPromptClick={handlePromptClick} - isGrid={isDialog} - /> -
- )} - - {messages && messages.length > 2 && followUpPromptsStatus && followUpPrompts.length > 0 && ( - <> - - - - - - Try these prompts - - - 0 ? 70 : 0 }} - followUpPrompts={followUpPrompts || []} - onPromptClick={handleFollowUpPromptClick} - isGrid={isDialog} - /> - - - )} - - - -
- {previews && previews.length > 0 && ( - - {previews.map((item, index) => ( - {previewDisplay(item)} - ))} - - )} - {isRecording ? ( - <> - {recordingNotSupported ? ( -
-
- To record audio, use modern browsers like Chrome or Firefox that support audio recording. - -
-
- ) : ( - -
- - - - 00:00 - {isLoadingRecording && Sending...} -
-
- - - - - - -
-
- )} - - ) : ( -
- - {isChatFlowAvailableForImageUploads && !isChatFlowAvailableForFileUploads && ( - - - - - - )} - {!isChatFlowAvailableForImageUploads && isChatFlowAvailableForFileUploads && ( - - - - - - )} - {isChatFlowAvailableForImageUploads && isChatFlowAvailableForFileUploads && ( - - - - - - - - - )} - {!isChatFlowAvailableForImageUploads && !isChatFlowAvailableForFileUploads && } - - } - endAdornment={ - <> - {isChatFlowAvailableForSpeech && ( - - onMicrophonePressed()} type="button" disabled={getInputDisabled()} edge="end"> - - - - )} - {!isAgentCanvas && ( - - - {loading ? ( -
- -
- ) : ( - // Send icon SVG in input field - - )} -
-
- )} - {isAgentCanvas && ( - <> - {!loading && ( - - - - - - )} - {loading && ( - - handleAbort()} - disabled={isMessageStopping} - > - {isMessageStopping ? ( -
- -
- ) : ( - - )} -
-
- )} - - )} - - } - /> - {isChatFlowAvailableForImageUploads && ( - - )} - {isChatFlowAvailableForFileUploads && ( - - )} - - )} -
- setSourceDialogOpen(false)} /> - setShowFeedbackContentDialog(false)} - onConfirm={submitFeedbackContent} - /> - { - setOpenFeedbackDialog(false); - setPendingActionData(null); - setFeedback(''); - }} - > - Provide Feedback - - setFeedback(e.target.value)} - /> - - - - - - -
- ); -}; - -ChatMessage.propTypes = { - open: PropTypes.bool, - chatflowid: PropTypes.string, - isAgentCanvas: PropTypes.bool, - isDialog: PropTypes.bool, - previews: PropTypes.array, - setPreviews: PropTypes.func, -}; - -export default memo(ChatMessage); From 31997235e3bd2fcf4f0adc7d653d6a111f3c82c2 Mon Sep 17 00:00:00 2001 From: Ilango Rajagopal Date: Tue, 30 Sep 2025 18:22:28 +0530 Subject: [PATCH 16/33] Fix audio overlap due to malfunctioning abort --- src/components/Bot.tsx | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/src/components/Bot.tsx b/src/components/Bot.tsx index 45f8183..fe99bc2 100644 --- a/src/components/Bot.tsx +++ b/src/components/Bot.tsx @@ -1150,7 +1150,7 @@ export const Bot = (botProps: BotProps & { class?: string }) => { setIsMessageStopping(false); // Stop all TTS when aborting message - stopAllTTSLocally(); + stopAllTTS(); setMessages((prevMessages) => { const allMessages = [...cloneDeep(prevMessages)]; @@ -2047,7 +2047,7 @@ export const Bot = (botProps: BotProps & { class?: string }) => { setTTSAction(true); // Ensure complete cleanup before starting new TTS - stopAllTTSLocally(); + stopAllTTS(); setIsTTSLoading((prevState) => ({ ...prevState, @@ -2058,8 +2058,12 @@ export const Bot = (botProps: BotProps & { class?: string }) => { const allMessages = [...cloneDeep(prevMessages)]; const lastMessage = allMessages[allMessages.length - 1]; if (lastMessage.type === 'userMessage') return allMessages; - if (lastMessage.id) return allMessages; - allMessages[allMessages.length - 1].id = data.chatMessageId; + const existingId = lastMessage.id || lastMessage.messageId; + if (!existingId) { + allMessages[allMessages.length - 1].id = data.chatMessageId; + } else if (!lastMessage.id) { + allMessages[allMessages.length - 1].id = existingId; + } return allMessages; }); @@ -2194,7 +2198,7 @@ export const Bot = (botProps: BotProps & { class?: string }) => { const playingHandler = () => { setIsTTSLoading((prevState) => { const newState = { ...prevState }; - newState[data.chatMessageId] = false; + delete newState[data.chatMessageId]; return newState; }); setIsTTSPlaying((prevState) => ({ @@ -2372,7 +2376,7 @@ export const Bot = (botProps: BotProps & { class?: string }) => { cleanupTTSForMessage(messageId); }; - const stopAllTTSLocally = () => { + const stopAllTTS = () => { const audioElements = ttsAudio(); Object.keys(audioElements).forEach((messageId) => { if (audioElements[messageId]) { @@ -2397,11 +2401,7 @@ export const Bot = (botProps: BotProps & { class?: string }) => { setIsTTSLoading({}); }; - const stopAllTTS = async () => { - // First do local cleanup - stopAllTTSLocally(); - - // Then abort any active TTS requests on the server + const handleTTSAbortAll = async () => { const activeTTSMessages = Object.keys(isTTSLoading()).concat(Object.keys(isTTSPlaying())); for (const messageId of activeTTSMessages) { try { @@ -2434,10 +2434,8 @@ export const Bot = (botProps: BotProps & { class?: string }) => { setTTSAction(true); // Ensure complete cleanup before starting new TTS - await stopAllTTS(); - - // Wait for cleanup to complete - await new Promise((resolve) => setTimeout(resolve, 150)); + await handleTTSAbortAll(); + stopAllTTS(); handleTTSStart({ chatMessageId: messageId, format: 'mp3' }); @@ -2511,7 +2509,6 @@ export const Bot = (botProps: BotProps & { class?: string }) => { } } catch (error: any) { if (error.name === 'AbortError') { - console.log('TTS request was aborted'); cleanupTTSForMessage(messageId); } else { console.error('Error with TTS:', error); From 5521d37d6dbeaebdd44a496fecdbd4dc4d817049 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 3 Oct 2025 13:48:38 +0100 Subject: [PATCH 17/33] flowise-embed@3.0.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 281b445..b742cc0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flowise-embed", - "version": "3.0.4", + "version": "3.0.5", "description": "Javascript library to display flowise chatbot on your website", "type": "module", "main": "dist/index.js", From 62521830d34c9b88b04f7c820a56090853b8a1dc Mon Sep 17 00:00:00 2001 From: kientv Date: Tue, 26 Aug 2025 11:44:43 +0700 Subject: [PATCH 18/33] AI Chatbot --- public/index.html | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/public/index.html b/public/index.html index 262ada5..6bf9c42 100644 --- a/public/index.html +++ b/public/index.html @@ -19,7 +19,7 @@
From a6f7370f8c4abc66b022758912d9aacd98df80be Mon Sep 17 00:00:00 2001 From: PhucZuu Date: Mon, 13 Oct 2025 15:34:44 +0700 Subject: [PATCH 19/33] Authorization chatbot --- src/components/Bot.tsx | 1 + src/utils/auth.ts | 18 +++++++++++++----- src/utils/pkce.ts | 5 ++++- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/components/Bot.tsx b/src/components/Bot.tsx index fe99bc2..d40388c 100644 --- a/src/components/Bot.tsx +++ b/src/components/Bot.tsx @@ -48,6 +48,7 @@ import { getOAuthState, clearOAuthState, } from '@/utils/pkce'; + import { refreshAccessToken as refreshToken, handleOIDCLogin as initiateOIDCLogin, diff --git a/src/utils/auth.ts b/src/utils/auth.ts index 31e3f8f..22addf0 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -35,7 +35,10 @@ export const storeOAuthState = (state: string) => { /** * Refresh access token using refresh_token */ -export const refreshAccessToken = async (config: OIDCConfig, onTokenUpdate?: (token: string) => void): Promise => { +export const refreshAccessToken = async ( + config: OIDCConfig, + onTokenUpdate?: (token: string) => void +): Promise => { const refreshToken = getCookie('oidc_refresh_token'); if (!refreshToken) { return null; @@ -83,7 +86,7 @@ export const refreshAccessToken = async (config: OIDCConfig, onTokenUpdate?: (to } if (tokens.expires_in) { - const expiresAt = Date.now() + parseInt(tokens.expires_in) * 1000; + const expiresAt = Date.now() + (parseInt(tokens.expires_in) * 1000); setCookie('oidc_token_expires_at', expiresAt.toString(), expiresInSeconds); } @@ -100,7 +103,11 @@ export const refreshAccessToken = async (config: OIDCConfig, onTokenUpdate?: (to /** * Handle OIDC login with PKCE flow (redirect method) */ -export const handleOIDCLogin = async (config: OIDCConfig, chatbot: string, onError?: (message: string) => void): Promise => { +export const handleOIDCLogin = async ( + config: OIDCConfig, + chatbot: string, + onError?: (message: string) => void +): Promise => { try { const { authorizationEndpoint, redirectUri, scopes, audience } = config; @@ -141,6 +148,7 @@ export const handleOIDCLogin = async (config: OIDCConfig, chatbot: string, onErr // Redirect to authorization endpoint (page will reload) window.location.href = authUrl; + } catch (error) { console.error('[OIDC] Login error:', error); if (onError) { @@ -152,10 +160,10 @@ export const handleOIDCLogin = async (config: OIDCConfig, chatbot: string, onErr /** * Check if token is expired or expiring soon */ -export const isTokenExpired = (bufferSeconds = 0): boolean => { +export const isTokenExpired = (bufferSeconds: number = 0): boolean => { const expiresAt = getCookie('oidc_token_expires_at'); if (!expiresAt) return false; - return Date.now() >= parseInt(expiresAt) - bufferSeconds * 1000; + return Date.now() >= (parseInt(expiresAt) - bufferSeconds * 1000); }; /** diff --git a/src/utils/pkce.ts b/src/utils/pkce.ts index 833a314..d6f0b2a 100644 --- a/src/utils/pkce.ts +++ b/src/utils/pkce.ts @@ -30,7 +30,10 @@ export async function generateCodeChallenge(verifier: string): Promise { */ function base64URLEncode(buffer: Uint8Array): string { const base64 = btoa(String.fromCharCode(...buffer)); - return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); + return base64 + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); } /** From 8829f0e2e7a9be87cceba438b9aa7b6c6ee3ff20 Mon Sep 17 00:00:00 2001 From: kientv Date: Tue, 2 Dec 2025 16:42:31 +0700 Subject: [PATCH 20/33] git rm -r --cached dist --- src/components/Bot.tsx | 13 ++++++++++++- src/utils/auth.ts | 28 +++++++++++++++------------- src/utils/pkce.ts | 5 +---- 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/src/components/Bot.tsx b/src/components/Bot.tsx index d40388c..924e11a 100644 --- a/src/components/Bot.tsx +++ b/src/components/Bot.tsx @@ -48,7 +48,6 @@ import { getOAuthState, clearOAuthState, } from '@/utils/pkce'; - import { refreshAccessToken as refreshToken, handleOIDCLogin as initiateOIDCLogin, @@ -57,6 +56,7 @@ import { clearAllTokens, deleteCookie as removeOIDCCookie, getCookie as getOIDCCookie, + getCookie as getOIDCCookie, } from '@/utils/auth'; const EventStreamContentType = 'text/event-stream; charset=utf-8'; @@ -524,6 +524,7 @@ export const Bot = (botProps: BotProps & { class?: string }) => { const [leadEmail, setLeadEmail] = createSignal(''); const [disclaimerPopupOpen, setDisclaimerPopupOpen] = createSignal(false); + // OIDC authentication state const [oidcConfig, setOidcConfig] = createSignal(null); const [oidcToken, setOidcToken] = createSignal(null); @@ -606,6 +607,7 @@ export const Bot = (botProps: BotProps & { class?: string }) => { const config = oidcConfig(); if (!config) return; + await initiateOIDCLogin(config, props.chatbot, (errorMsg) => { handleError(errorMsg); }); @@ -999,17 +1001,21 @@ export const Bot = (botProps: BotProps & { class?: string }) => { const config = oidcConfig()!; const grantType = config.grantType; + // OAuth flows: get token from cookie let token = oidcToken() || getOIDCCookie('oidc_access_token'); + // Check if token is expired or about to expire (within 5 minutes) const expiresAt = getOIDCCookie('oidc_token_expires_at'); const isExpired = token && expiresAt && Date.now() >= parseInt(expiresAt); + if (isExpired || !token) { // Try to refresh token first const refreshedToken = await refreshAccessToken(); + if (refreshedToken) { token = refreshedToken; } else { @@ -1020,6 +1026,7 @@ export const Bot = (botProps: BotProps & { class?: string }) => { } } + // Add Bearer token to headers if (token && request.headers instanceof Headers) { request.headers.set('Authorization', `Bearer ${token}`); @@ -1677,6 +1684,7 @@ export const Bot = (botProps: BotProps & { class?: string }) => { // Transform API response (snake_case) to OIDCConfig (camelCase) const oidcData = chatbotConfig.oidc as any; + const transformedConfig: OIDCConfig = { clientId: oidcData.client_id, authorizationEndpoint: oidcData.authorization_endpoint, @@ -1689,12 +1697,15 @@ export const Bot = (botProps: BotProps & { class?: string }) => { grantType: oidcData.scheme, }; + setOidcConfig(transformedConfig); + // Check for existing token (callback already handled in web.ts) if (!hasOnRequest()) { const savedToken = getOIDCCookie('oidc_access_token'); + if (savedToken) { setOidcToken(savedToken); } else if (oidcConfig()) { diff --git a/src/utils/auth.ts b/src/utils/auth.ts index 22addf0..7bd7ca7 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -35,10 +35,7 @@ export const storeOAuthState = (state: string) => { /** * Refresh access token using refresh_token */ -export const refreshAccessToken = async ( - config: OIDCConfig, - onTokenUpdate?: (token: string) => void -): Promise => { +export const refreshAccessToken = async (config: OIDCConfig, onTokenUpdate?: (token: string) => void): Promise => { const refreshToken = getCookie('oidc_refresh_token'); if (!refreshToken) { return null; @@ -71,28 +68,34 @@ export const refreshAccessToken = async ( const tokens = await response.json(); + if (tokens.access_token) { const expiresInSeconds = tokens.expires_in ? parseInt(tokens.expires_in) : 3600; setCookie('oidc_access_token', tokens.access_token, expiresInSeconds); + // Notify parent component of token update if (onTokenUpdate) { onTokenUpdate(tokens.access_token); } + // Some providers rotate refresh tokens if (tokens.refresh_token) { setCookie('oidc_refresh_token', tokens.refresh_token, 30 * 24 * 3600); } + if (tokens.expires_in) { - const expiresAt = Date.now() + (parseInt(tokens.expires_in) * 1000); + const expiresAt = Date.now() + parseInt(tokens.expires_in) * 1000; setCookie('oidc_token_expires_at', expiresAt.toString(), expiresInSeconds); } + return tokens.access_token; } + return null; } catch (error) { console.error('[OIDC] Token refresh error:', error); @@ -103,18 +106,16 @@ export const refreshAccessToken = async ( /** * Handle OIDC login with PKCE flow (redirect method) */ -export const handleOIDCLogin = async ( - config: OIDCConfig, - chatbot: string, - onError?: (message: string) => void -): Promise => { +export const handleOIDCLogin = async (config: OIDCConfig, chatbot: string, onError?: (message: string) => void): Promise => { try { const { authorizationEndpoint, redirectUri, scopes, audience } = config; + // Generate PKCE code_verifier and code_challenge const codeVerifier = generateCodeVerifier(); const codeChallenge = await generateCodeChallenge(codeVerifier); + // Store code_verifier in sessionStorage for callback storePKCEVerifier(codeVerifier); @@ -126,12 +127,14 @@ export const handleOIDCLogin = async ( }; const state = btoa(JSON.stringify(stateData)); + // Save state to sessionStorage for validation storeOAuthState(state); // Save OIDC config to sessionStorage for early callback handling sessionStorage.setItem('oidc_config_temp', JSON.stringify(config)); + // Build authorization URL with PKCE parameters const params = new URLSearchParams({ client_id: config.clientId, @@ -148,7 +151,6 @@ export const handleOIDCLogin = async ( // Redirect to authorization endpoint (page will reload) window.location.href = authUrl; - } catch (error) { console.error('[OIDC] Login error:', error); if (onError) { @@ -160,10 +162,10 @@ export const handleOIDCLogin = async ( /** * Check if token is expired or expiring soon */ -export const isTokenExpired = (bufferSeconds: number = 0): boolean => { +export const isTokenExpired = (bufferSeconds = 0): boolean => { const expiresAt = getCookie('oidc_token_expires_at'); if (!expiresAt) return false; - return Date.now() >= (parseInt(expiresAt) - bufferSeconds * 1000); + return Date.now() >= parseInt(expiresAt) - bufferSeconds * 1000; }; /** diff --git a/src/utils/pkce.ts b/src/utils/pkce.ts index d6f0b2a..833a314 100644 --- a/src/utils/pkce.ts +++ b/src/utils/pkce.ts @@ -30,10 +30,7 @@ export async function generateCodeChallenge(verifier: string): Promise { */ function base64URLEncode(buffer: Uint8Array): string { const base64 = btoa(String.fromCharCode(...buffer)); - return base64 - .replace(/\+/g, '-') - .replace(/\//g, '_') - .replace(/=/g, ''); + return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); } /** From 95f8e08ebfff8dd6fa0a21fc87b047f37bd5b7e9 Mon Sep 17 00:00:00 2001 From: kientv Date: Wed, 3 Dec 2025 12:07:33 +0700 Subject: [PATCH 21/33] TTS for chatbot --- src/components/Bot.tsx | 90 ++++++++++++++------------ src/components/bubbles/GuestBubble.tsx | 4 +- src/queries/sendMessageQuery.ts | 2 +- src/utils/auth.ts | 10 --- 4 files changed, 51 insertions(+), 55 deletions(-) diff --git a/src/components/Bot.tsx b/src/components/Bot.tsx index 924e11a..060734e 100644 --- a/src/components/Bot.tsx +++ b/src/components/Bot.tsx @@ -56,7 +56,6 @@ import { clearAllTokens, deleteCookie as removeOIDCCookie, getCookie as getOIDCCookie, - getCookie as getOIDCCookie, } from '@/utils/auth'; const EventStreamContentType = 'text/event-stream; charset=utf-8'; @@ -524,7 +523,6 @@ export const Bot = (botProps: BotProps & { class?: string }) => { const [leadEmail, setLeadEmail] = createSignal(''); const [disclaimerPopupOpen, setDisclaimerPopupOpen] = createSignal(false); - // OIDC authentication state const [oidcConfig, setOidcConfig] = createSignal(null); const [oidcToken, setOidcToken] = createSignal(null); @@ -607,7 +605,6 @@ export const Bot = (botProps: BotProps & { class?: string }) => { const config = oidcConfig(); if (!config) return; - await initiateOIDCLogin(config, props.chatbot, (errorMsg) => { handleError(errorMsg); }); @@ -901,7 +898,11 @@ export const Bot = (botProps: BotProps & { class?: string }) => { setMessages((prevMessages) => { const allMessages = [...cloneDeep(prevMessages)]; if (allMessages[allMessages.length - 1].type === 'apiMessage') { + const lastMessage = allMessages[allMessages.length - 1]; + + // Set both messageId and id (replace temp ID with real ID) allMessages[allMessages.length - 1].messageId = data.chatMessageId; + allMessages[allMessages.length - 1].id = data.chatMessageId; } addChatMessage(allMessages); return allMessages; @@ -1001,21 +1002,17 @@ export const Bot = (botProps: BotProps & { class?: string }) => { const config = oidcConfig()!; const grantType = config.grantType; - // OAuth flows: get token from cookie let token = oidcToken() || getOIDCCookie('oidc_access_token'); - // Check if token is expired or about to expire (within 5 minutes) const expiresAt = getOIDCCookie('oidc_token_expires_at'); const isExpired = token && expiresAt && Date.now() >= parseInt(expiresAt); - if (isExpired || !token) { // Try to refresh token first const refreshedToken = await refreshAccessToken(); - if (refreshedToken) { token = refreshedToken; } else { @@ -1026,7 +1023,6 @@ export const Bot = (botProps: BotProps & { class?: string }) => { } } - // Add Bearer token to headers if (token && request.headers instanceof Headers) { request.headers.set('Authorization', `Bearer ${token}`); @@ -1072,9 +1068,18 @@ export const Bot = (botProps: BotProps & { class?: string }) => { async onmessage(ev) { const payload = JSON.parse(ev.data); switch (payload.event) { - case 'start': - setMessages((prevMessages) => [...prevMessages, { message: '', type: 'apiMessage' }]); + case 'start': { + const tempMessageId = uuidv4(); + setMessages((prevMessages) => [ + ...prevMessages, + { + message: '', + type: 'apiMessage', + id: tempMessageId, + }, + ]); break; + } case 'token': updateLastMessage(payload.data); break; @@ -1536,28 +1541,29 @@ export const Bot = (botProps: BotProps & { class?: string }) => { const loadedMessages: MessageType[] = chatMessage?.chatHistory?.length > 0 ? chatMessage.chatHistory?.map((message: MessageType) => { - const chatHistory: MessageType = { - messageId: message?.messageId, - message: message.message, - type: message.type, - rating: message.rating, - dateTime: message.dateTime, - }; - if (message.sourceDocuments) chatHistory.sourceDocuments = message.sourceDocuments; - if (message.fileAnnotations) chatHistory.fileAnnotations = message.fileAnnotations; - if (message.fileUploads) chatHistory.fileUploads = message.fileUploads; - if (message.agentReasoning) chatHistory.agentReasoning = message.agentReasoning; - if (message.action) chatHistory.action = message.action; - if (message.artifacts) chatHistory.artifacts = message.artifacts; - if (message.followUpPrompts) chatHistory.followUpPrompts = message.followUpPrompts; - if (message.execution && message.execution.executionData) - chatHistory.agentFlowExecutedData = - typeof message.execution.executionData === 'string' ? JSON.parse(message.execution.executionData) : message.execution.executionData; - if (message.agentFlowExecutedData) - chatHistory.agentFlowExecutedData = - typeof message.agentFlowExecutedData === 'string' ? JSON.parse(message.agentFlowExecutedData) : message.agentFlowExecutedData; - return chatHistory; - }) + const chatHistory: MessageType = { + messageId: message?.messageId, + id: message?.messageId, + message: message.message, + type: message.type, + rating: message.rating, + dateTime: message.dateTime, + }; + if (message.sourceDocuments) chatHistory.sourceDocuments = message.sourceDocuments; + if (message.fileAnnotations) chatHistory.fileAnnotations = message.fileAnnotations; + if (message.fileUploads) chatHistory.fileUploads = message.fileUploads; + if (message.agentReasoning) chatHistory.agentReasoning = message.agentReasoning; + if (message.action) chatHistory.action = message.action; + if (message.artifacts) chatHistory.artifacts = message.artifacts; + if (message.followUpPrompts) chatHistory.followUpPrompts = message.followUpPrompts; + if (message.execution && message.execution.executionData) + chatHistory.agentFlowExecutedData = + typeof message.execution.executionData === 'string' ? JSON.parse(message.execution.executionData) : message.execution.executionData; + if (message.agentFlowExecutedData) + chatHistory.agentFlowExecutedData = + typeof message.agentFlowExecutedData === 'string' ? JSON.parse(message.agentFlowExecutedData) : message.agentFlowExecutedData; + return chatHistory; + }) : [{ message: props.welcomeMessage ?? defaultWelcomeMessage, type: 'apiMessage' }]; const filteredMessages = loadedMessages.filter((message) => message.type !== 'leadCaptureMessage'); @@ -1675,8 +1681,14 @@ export const Bot = (botProps: BotProps & { class?: string }) => { setFullFileUploadAllowedTypes(chatbotConfig.fullFileUpload?.allowedUploadFileTypes); } } - if (chatbotConfig.isTTSEnabled) { - setIsTTSEnabled(chatbotConfig.isTTSEnabled); + + // Convert to boolean in case API returns string "true" instead of boolean true + const isTTSEnabledValue = chatbotConfig.isTTSEnabled === true || chatbotConfig.isTTSEnabled === 'true'; + + if (isTTSEnabledValue) { + setIsTTSEnabled(true); + } else { + setIsTTSEnabled(false); } // Get OIDC config ONLY from API publicConfig @@ -1684,7 +1696,6 @@ export const Bot = (botProps: BotProps & { class?: string }) => { // Transform API response (snake_case) to OIDCConfig (camelCase) const oidcData = chatbotConfig.oidc as any; - const transformedConfig: OIDCConfig = { clientId: oidcData.client_id, authorizationEndpoint: oidcData.authorization_endpoint, @@ -1697,15 +1708,12 @@ export const Bot = (botProps: BotProps & { class?: string }) => { grantType: oidcData.scheme, }; - setOidcConfig(transformedConfig); - // Check for existing token (callback already handled in web.ts) if (!hasOnRequest()) { const savedToken = getOIDCCookie('oidc_access_token'); - if (savedToken) { setOidcToken(savedToken); } else if (oidcConfig()) { @@ -2375,7 +2383,7 @@ export const Bot = (botProps: BotProps & { class?: string }) => { await abortTTSQuery({ apiHost: props.apiHost, body: { - chatflowId: props.chatflowid, + chatbot: props.chatbot, chatId: chatId(), chatMessageId: messageId, }, @@ -2420,7 +2428,7 @@ export const Bot = (botProps: BotProps & { class?: string }) => { await abortTTSQuery({ apiHost: props.apiHost, body: { - chatflowId: props.chatflowid, + chatbot: props.chatbot, chatId: chatId(), chatMessageId: messageId, }, @@ -2459,7 +2467,7 @@ export const Bot = (botProps: BotProps & { class?: string }) => { apiHost: props.apiHost, body: { chatId: chatId(), - chatflowId: props.chatflowid, + chatbot: props.chatbot, chatMessageId: messageId, text: messageText, }, diff --git a/src/components/bubbles/GuestBubble.tsx b/src/components/bubbles/GuestBubble.tsx index aec47ad..696e110 100644 --- a/src/components/bubbles/GuestBubble.tsx +++ b/src/components/bubbles/GuestBubble.tsx @@ -84,9 +84,7 @@ export const GuestBubble = (props: Props) => { } }; - const hasAudio = props.message.fileUploads?.some( - (item) => item?.mime?.startsWith('audio/') - ); + const hasAudio = props.message.fileUploads?.some((item) => item?.mime?.startsWith('audio/')); return (
diff --git a/src/queries/sendMessageQuery.ts b/src/queries/sendMessageQuery.ts index a1a3ce3..1b6489e 100644 --- a/src/queries/sendMessageQuery.ts +++ b/src/queries/sendMessageQuery.ts @@ -64,7 +64,7 @@ export type LeadCaptureRequest = BaseRequest & { export type GenerateTTSRequest = BaseRequest & { body: { chatId: string; - chatflowId: string; + chatbot: string; chatMessageId: string; text: string; }; diff --git a/src/utils/auth.ts b/src/utils/auth.ts index 7bd7ca7..31e3f8f 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -68,34 +68,28 @@ export const refreshAccessToken = async (config: OIDCConfig, onTokenUpdate?: (to const tokens = await response.json(); - if (tokens.access_token) { const expiresInSeconds = tokens.expires_in ? parseInt(tokens.expires_in) : 3600; setCookie('oidc_access_token', tokens.access_token, expiresInSeconds); - // Notify parent component of token update if (onTokenUpdate) { onTokenUpdate(tokens.access_token); } - // Some providers rotate refresh tokens if (tokens.refresh_token) { setCookie('oidc_refresh_token', tokens.refresh_token, 30 * 24 * 3600); } - if (tokens.expires_in) { const expiresAt = Date.now() + parseInt(tokens.expires_in) * 1000; setCookie('oidc_token_expires_at', expiresAt.toString(), expiresInSeconds); } - return tokens.access_token; } - return null; } catch (error) { console.error('[OIDC] Token refresh error:', error); @@ -110,12 +104,10 @@ export const handleOIDCLogin = async (config: OIDCConfig, chatbot: string, onErr try { const { authorizationEndpoint, redirectUri, scopes, audience } = config; - // Generate PKCE code_verifier and code_challenge const codeVerifier = generateCodeVerifier(); const codeChallenge = await generateCodeChallenge(codeVerifier); - // Store code_verifier in sessionStorage for callback storePKCEVerifier(codeVerifier); @@ -127,14 +119,12 @@ export const handleOIDCLogin = async (config: OIDCConfig, chatbot: string, onErr }; const state = btoa(JSON.stringify(stateData)); - // Save state to sessionStorage for validation storeOAuthState(state); // Save OIDC config to sessionStorage for early callback handling sessionStorage.setItem('oidc_config_temp', JSON.stringify(config)); - // Build authorization URL with PKCE parameters const params = new URLSearchParams({ client_id: config.clientId, From 23adc833c38a863680de885889b6d8f84d2c9387 Mon Sep 17 00:00:00 2001 From: PhucZuu Date: Thu, 4 Dec 2025 10:15:18 +0700 Subject: [PATCH 22/33] Customize message date and time and don't automatically reopen the chat window when reloading the page --- src/components/Bot.tsx | 5 +- src/components/bubbles/BotBubble.tsx | 59 ++++++++++--------- src/components/bubbles/GuestBubble.tsx | 50 +++++++++++++++- src/components/bubbles/LoadingBubble.tsx | 4 +- .../inputs/textInput/components/TextInput.tsx | 2 +- src/features/bubble/components/Bubble.tsx | 24 +++++++- 6 files changed, 108 insertions(+), 36 deletions(-) diff --git a/src/components/Bot.tsx b/src/components/Bot.tsx index 060734e..4626872 100644 --- a/src/components/Bot.tsx +++ b/src/components/Bot.tsx @@ -864,7 +864,7 @@ export const Bot = (botProps: BotProps & { class?: string }) => { errMessage = props.errorMessage; } setMessages((prevMessages) => { - const messages: MessageType[] = [...prevMessages, { message: errMessage, type: 'apiMessage' }]; + const messages: MessageType[] = [...prevMessages, { message: errMessage, type: 'apiMessage', dateTime: new Date().toISOString() }]; addChatMessage(messages); return messages; }); @@ -1292,7 +1292,7 @@ export const Bot = (botProps: BotProps & { class?: string }) => { clearPreviews(); setMessages((prevMessages) => { - const messages: MessageType[] = [...prevMessages, { message: value as string, type: 'userMessage', fileUploads: uploads }]; + const messages: MessageType[] = [...prevMessages, { message: value as string, type: 'userMessage', fileUploads: uploads, dateTime: new Date().toISOString() }]; addChatMessage(messages); return messages; }); @@ -2708,6 +2708,7 @@ export const Bot = (botProps: BotProps & { class?: string }) => { showAvatar={props.userMessage?.showAvatar} avatarSrc={props.userMessage?.avatarSrc} fontSize={props.fontSize} + dateTimeToggle={props.dateTimeToggle} renderHTML={props.renderHTML} /> )} diff --git a/src/components/bubbles/BotBubble.tsx b/src/components/bubbles/BotBubble.tsx index ca75fd1..0ad15f5 100644 --- a/src/components/bubbles/BotBubble.tsx +++ b/src/components/bubbles/BotBubble.tsx @@ -365,31 +365,24 @@ export const BotBubble = (props: Props) => { return ''; } - let formatted = ''; + const pad = (n: number) => n.toString().padStart(2, '0'); + + const parts: string[] = []; if (showDate) { - const dateFormatter = new Intl.DateTimeFormat('en-US', { - year: 'numeric', - month: 'short', - day: 'numeric', - timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, - }); - const [{ value: month }, , { value: day }, , { value: year }] = dateFormatter.formatToParts(date); - formatted = `${month.charAt(0).toUpperCase() + month.slice(1)} ${day}, ${year}`; + const year = date.getFullYear(); + const month = pad(date.getMonth() + 1); + const day = pad(date.getDate()); + parts.push(`${year}/${month}/${day}`); } if (showTime) { - const timeFormatter = new Intl.DateTimeFormat('en-US', { - hour: 'numeric', - minute: '2-digit', - hour12: true, - timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, - }); - const timeString = timeFormatter.format(date).toLowerCase(); - formatted = formatted ? `${formatted}, ${timeString}` : timeString; + const hour = pad(date.getHours()); + const minute = pad(date.getMinutes()); + parts.push(`${hour}:${minute}`); } - return formatted; + return parts.join(' '); } catch (error) { console.error('Error formatting date:', error); return ''; @@ -450,17 +443,32 @@ export const BotBubble = (props: Props) => {
)} {props.message.message && ( - + > + + + +
+ {formatDateTime( + props.message.dateTime, + props?.dateTimeToggle?.date, + props?.dateTimeToggle?.time + )} +
+
+
)} {props.message.action && (
@@ -576,11 +584,6 @@ export const BotBubble = (props: Props) => { onClick={onThumbsDownClick} /> ) : null} - -
- {formatDateTime(props.message.dateTime, props?.dateTimeToggle?.date, props?.dateTimeToggle?.time)} -
-
)}
diff --git a/src/components/bubbles/GuestBubble.tsx b/src/components/bubbles/GuestBubble.tsx index 696e110..099bf63 100644 --- a/src/components/bubbles/GuestBubble.tsx +++ b/src/components/bubbles/GuestBubble.tsx @@ -14,6 +14,10 @@ type Props = { backgroundColor?: string; textColor?: string; fontSize?: number; + dateTimeToggle?: { + date?: boolean; + time?: boolean; + }; renderHTML?: boolean; }; @@ -84,12 +88,49 @@ export const GuestBubble = (props: Props) => { } }; - const hasAudio = props.message.fileUploads?.some((item) => item?.mime?.startsWith('audio/')); + const hasAudio = props.message.fileUploads?.some( + (item) => item?.mime?.startsWith('audio/') + ); + + const formatDateTime = (dateTimeString: string | undefined, showDate: boolean | undefined, showTime: boolean | undefined) => { + if (!dateTimeString) return ''; + + try { + const date = new Date(dateTimeString); + if (isNaN(date.getTime())) { + console.error('Invalid ISO date string:', dateTimeString); + return ''; + } + + const pad = (n: number) => n.toString().padStart(2, '0'); + let formatted = ''; + + if (showDate) { + const y = date.getFullYear(); + const m = pad(date.getMonth() + 1); + const d = pad(date.getDate()); + formatted = `${y}/${m}/${d}`; + } + + if (showTime) { + const h = pad(date.getHours()); + const min = pad(date.getMinutes()); + formatted = formatted ? `${formatted} ${h}:${min}` : `${h}:${min}`; + } + + return formatted; + } catch (error) { + console.error('Error formatting date:', error); + return ''; + } + }; + + const dateTimeText = props.message.dateTime ? formatDateTime(props.message.dateTime, props.dateTimeToggle?.date !== false, props.dateTimeToggle?.time !== false) : ''; return (
{ style={{ 'font-size': props.fontSize ? `${props.fontSize}px` : `${defaultFontSize}px` }} /> )} + {dateTimeText && ( +
+ {dateTimeText} +
+ )}
diff --git a/src/components/bubbles/LoadingBubble.tsx b/src/components/bubbles/LoadingBubble.tsx index c1fdeac..5f21701 100644 --- a/src/components/bubbles/LoadingBubble.tsx +++ b/src/components/bubbles/LoadingBubble.tsx @@ -1,8 +1,8 @@ import { TypingBubble } from '@/components'; export const LoadingBubble = () => ( -
- +
+
diff --git a/src/components/inputs/textInput/components/TextInput.tsx b/src/components/inputs/textInput/components/TextInput.tsx index 6541dc1..516c6b2 100644 --- a/src/components/inputs/textInput/components/TextInput.tsx +++ b/src/components/inputs/textInput/components/TextInput.tsx @@ -140,7 +140,7 @@ export const TextInput = (props: TextInputProps) => { return (
{ right: bubbleProps.theme?.button?.right ?? 20, }); + const ONE_WEEK_SECONDS = 7 * 24 * 60 * 60; + + const getCookieKey = (chatbot: string) => + `chatbot_auto_closed_${chatbot}`; + + const setClosedCookie = (chatbot: string) => { + const key = getCookieKey(chatbot); + document.cookie = `${key}=1; max-age=${ONE_WEEK_SECONDS}; path=/`; + }; + + const hasClosedCookie = (chatbot: string): boolean => { + const key = getCookieKey(chatbot) + "="; + return document.cookie.split("; ").some(c => c.startsWith(key)); + }; + // State for window dimensions const [windowSize, setWindowSize] = createSignal({ width: window.innerWidth, @@ -34,6 +49,11 @@ export const Bubble = (props: BubbleProps) => { const closeBot = () => { setIsBotOpened(false); + try { + if (props.chatbot && !hasClosedCookie(props.chatbot)) { + setClosedCookie(props.chatbot); + } + } catch (e) {} }; const toggleBot = () => { @@ -58,6 +78,8 @@ export const Bubble = (props: BubbleProps) => { }); const buttonSize = getBubbleButtonSize(props.theme?.button?.size); // Default to 48px if size is not provided + const isClosedBefore = props.chatbot ? hasClosedCookie(props.chatbot) : false; + const shouldAutoOpen = bubbleProps.theme?.button?.autoWindowOpen?.autoOpen && !isClosedBefore; const buttonBottom = props.theme?.button?.bottom ?? 20; const chatWindowBottom = buttonBottom + buttonSize + 10; // Adjust the offset here for slight shift @@ -96,7 +118,7 @@ export const Bubble = (props: BubbleProps) => { isBotOpened={isBotOpened()} setButtonPosition={setButtonPosition} dragAndDrop={bubbleProps.theme?.button?.dragAndDrop ?? false} - autoOpen={bubbleProps.theme?.button?.autoWindowOpen?.autoOpen ?? false} + autoOpen={shouldAutoOpen} openDelay={bubbleProps.theme?.button?.autoWindowOpen?.openDelay} autoOpenOnMobile={bubbleProps.theme?.button?.autoWindowOpen?.autoOpenOnMobile ?? false} /> From 4dfde5ecaf340b754b7116cba603362f4ad9dc73 Mon Sep 17 00:00:00 2001 From: kientv Date: Fri, 5 Dec 2025 10:03:44 +0700 Subject: [PATCH 23/33] Improve tts endpoint --- src/features/bubble/components/Bubble.tsx | 4 +++- src/queries/sendMessageQuery.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/features/bubble/components/Bubble.tsx b/src/features/bubble/components/Bubble.tsx index 0c5fffc..5bf46b3 100644 --- a/src/features/bubble/components/Bubble.tsx +++ b/src/features/bubble/components/Bubble.tsx @@ -53,7 +53,9 @@ export const Bubble = (props: BubbleProps) => { if (props.chatbot && !hasClosedCookie(props.chatbot)) { setClosedCookie(props.chatbot); } - } catch (e) {} + } catch (e) { + console.error('Error handling cookie:', e); + } }; const toggleBot = () => { diff --git a/src/queries/sendMessageQuery.ts b/src/queries/sendMessageQuery.ts index 1b6489e..5b8e999 100644 --- a/src/queries/sendMessageQuery.ts +++ b/src/queries/sendMessageQuery.ts @@ -173,7 +173,7 @@ export const generateTTSQuery = async ({ apiHost = 'http://localhost:3000', body await onRequest(requestInfo); } - return fetch(`${apiHost}/api/v1/text-to-speech/generate`, requestInfo); + return fetch(`${apiHost}/api/v2/tts`, requestInfo); }; export const abortTTSQuery = ({ apiHost = 'http://localhost:3000', body, onRequest }: AbortTTSRequest) => From 3f9802d041e5455771bd3853bc19dc534f97b67f Mon Sep 17 00:00:00 2001 From: PhucZuu Date: Mon, 8 Dec 2025 15:36:31 +0700 Subject: [PATCH 24/33] Fix bug speaker --- src/components/Bot.tsx | 26 ++++++++++++------------- src/components/avatars/Avatar.tsx | 2 +- src/components/bubbles/BotBubble.tsx | 27 +++++++++++++------------- src/components/bubbles/GuestBubble.tsx | 9 ++++++--- src/components/buttons/TTSButton.tsx | 2 +- 5 files changed, 35 insertions(+), 31 deletions(-) diff --git a/src/components/Bot.tsx b/src/components/Bot.tsx index 4626872..7c1fe06 100644 --- a/src/components/Bot.tsx +++ b/src/components/Bot.tsx @@ -2379,19 +2379,19 @@ export const Bot = (botProps: BotProps & { class?: string }) => { setTTSAction(true); // Abort TTS request if active - try { - await abortTTSQuery({ - apiHost: props.apiHost, - body: { - chatbot: props.chatbot, - chatId: chatId(), - chatMessageId: messageId, - }, - onRequest: props.onRequest, - }); - } catch (error) { - console.warn(`Error aborting TTS for message ${messageId}:`, error); - } + // try { + // await abortTTSQuery({ + // apiHost: props.apiHost, + // body: { + // chatbot: props.chatbot, + // chatId: chatId(), + // chatMessageId: messageId, + // }, + // onRequest: props.onRequest, + // }); + // } catch (error) { + // console.warn(`Error aborting TTS for message ${messageId}:`, error); + // } cleanupTTSForMessage(messageId); }; diff --git a/src/components/avatars/Avatar.tsx b/src/components/avatars/Avatar.tsx index 69910a6..640d1b7 100644 --- a/src/components/avatars/Avatar.tsx +++ b/src/components/avatars/Avatar.tsx @@ -14,7 +14,7 @@ export const Avatar = (props: { initialAvatarSrc?: string }) => { }>
Bot avatar diff --git a/src/components/bubbles/BotBubble.tsx b/src/components/bubbles/BotBubble.tsx index 0ad15f5..0e35ec0 100644 --- a/src/components/bubbles/BotBubble.tsx +++ b/src/components/bubbles/BotBubble.tsx @@ -379,7 +379,8 @@ export const BotBubble = (props: Props) => { if (showTime) { const hour = pad(date.getHours()); const minute = pad(date.getMinutes()); - parts.push(`${hour}:${minute}`); + const sec = pad(date.getSeconds()); + parts.push(`${hour}:${minute}:${sec}`); } return parts.join(' '); @@ -458,16 +459,6 @@ export const BotBubble = (props: Props) => { 'font-size': props.fontSize ? `${props.fontSize}px` : `${defaultFontSize}px`, }} /> - - -
- {formatDateTime( - props.message.dateTime, - props?.dateTimeToggle?.date, - props?.dateTimeToggle?.time - )} -
-
)} {props.message.action && ( @@ -537,8 +528,8 @@ export const BotBubble = (props: Props) => { )}
-
- +
+ { @@ -565,6 +556,16 @@ export const BotBubble = (props: Props) => { }} /> + + +
+ {formatDateTime( + props.message.dateTime, + props?.dateTimeToggle?.date, + props?.dateTimeToggle?.time + )} +
+
{props.chatFeedbackStatus && props.message.messageId && ( <> copyMessageToClipboard()} /> diff --git a/src/components/bubbles/GuestBubble.tsx b/src/components/bubbles/GuestBubble.tsx index 099bf63..cb5f599 100644 --- a/src/components/bubbles/GuestBubble.tsx +++ b/src/components/bubbles/GuestBubble.tsx @@ -115,7 +115,8 @@ export const GuestBubble = (props: Props) => { if (showTime) { const h = pad(date.getHours()); const min = pad(date.getMinutes()); - formatted = formatted ? `${formatted} ${h}:${min}` : `${h}:${min}`; + const sec = pad(date.getSeconds()); + formatted = formatted ? `${formatted} ${h}:${min}:${sec}` : `${h}:${min}:${sec}`; } return formatted; @@ -128,7 +129,8 @@ export const GuestBubble = (props: Props) => { const dateTimeText = props.message.dateTime ? formatDateTime(props.message.dateTime, props.dateTimeToggle?.date !== false, props.dateTimeToggle?.time !== false) : ''; return ( -
+
+
{ style={{ 'font-size': props.fontSize ? `${props.fontSize}px` : `${defaultFontSize}px` }} /> )} +
{dateTimeText && ( -
+
{dateTimeText}
)} diff --git a/src/components/buttons/TTSButton.tsx b/src/components/buttons/TTSButton.tsx index 02d9e9e..37a549e 100644 --- a/src/components/buttons/TTSButton.tsx +++ b/src/components/buttons/TTSButton.tsx @@ -45,7 +45,7 @@ export const TTSButton = (props: Props) => { return (
); diff --git a/src/components/bubbles/GuestBubble.tsx b/src/components/bubbles/GuestBubble.tsx index cb5f599..dd71944 100644 --- a/src/components/bubbles/GuestBubble.tsx +++ b/src/components/bubbles/GuestBubble.tsx @@ -1,8 +1,9 @@ -import { For, Show } from 'solid-js'; +import { For, Show, createSignal } from 'solid-js'; import { Avatar } from '../avatars/Avatar'; import { Marked } from '@ts-stack/markdown'; import { FileUpload, MessageType } from '../Bot'; import { AttachmentIcon } from '../icons'; +import { ImageModal } from './ImageModal'; type Props = { message: MessageType; @@ -28,6 +29,8 @@ const defaultFontSize = 16; export const GuestBubble = (props: Props) => { Marked.setOptions({ isNoP: true, sanitize: props.renderHTML !== undefined ? !props.renderHTML : true }); + const [enlargedImage,setEnlargedImage] = createSignal(null); + // Callback ref to set innerHTML and apply text color to all Markdown elements const setUserMessageRef = (el: HTMLSpanElement) => { if (el) { @@ -57,6 +60,17 @@ export const GuestBubble = (props: Props) => { el.querySelectorAll('a').forEach((link) => { link.target = '_blank'; }); + + el.querySelectorAll('img').forEach((img) => { + img.style.cursor = 'pointer'; + img.classList.add('hover:opacity-90', 'transition-opacity', 'rounded-lg', 'my-2'); + img.addEventListener('click', () => { + const src = img.getAttribute('src'); + if (src) { + setEnlargedImage(src); + } + }); + }); } }; @@ -166,6 +180,13 @@ export const GuestBubble = (props: Props) => { + + + setEnlargedImage(null)} + /> +
); }; diff --git a/src/components/bubbles/ImageModal.tsx b/src/components/bubbles/ImageModal.tsx new file mode 100644 index 0000000..6a2c1ec --- /dev/null +++ b/src/components/bubbles/ImageModal.tsx @@ -0,0 +1,226 @@ +import { createSignal, onCleanup, onMount, Show } from 'solid-js'; +import { XIcon } from '../icons'; + +type Props = { + imageUrl: string; + onClose: () => void; +}; + +export const ImageModal = (props: Props) => { + const [isLoading, setIsLoading] = createSignal(true); + const [imageError, setImageError] = createSignal(false); + const [scale, setScale] = createSignal(1); + const [position, setPosition] = createSignal({ x: 0, y: 0 }); + let modalContainer: HTMLDivElement | undefined; + let imageContainer: HTMLDivElement | undefined; + + const handleImageLoad = () => { + setIsLoading(false); + setImageError(false); + }; + + const handleImageError = () => { + setIsLoading(false); + setImageError(true); + console.error('Failed to load image:', props.imageUrl); + }; + + const handleBackdropClick = (e: MouseEvent) => { + if (e.target === e.currentTarget) { + props.onClose(); + } + }; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + props.onClose(); + } + }; + + const handleWheel = (e: WheelEvent) => { + e.preventDefault(); + e.stopPropagation(); + + const delta = e.deltaY > 0 ? -0.1 : 0.1; + const newScale = Math.min(Math.max(0.5, scale() + delta), 5); + + setScale(newScale); + + if (newScale === 1) { + setPosition({ x: 0, y: 0 }); + } + }; + + let isDragging = false; + let dragStart = { x: 0, y: 0 }; + + const handleMouseDown = (e: MouseEvent) => { + if (scale() > 1) { + isDragging = true; + dragStart = { + x: e.clientX - position().x, + y: e.clientY - position().y + }; + e.preventDefault(); + } + }; + + const handleMouseMove = (e: MouseEvent) => { + if (isDragging && scale() > 1) { + setPosition({ + x: e.clientX - dragStart.x, + y: e.clientY - dragStart.y + }); + } + }; + + const handleMouseUp = () => { + isDragging = false; + }; + + const handleDoubleClick = () => { + if (scale() === 1) { + setScale(2); + } else { + setScale(1); + setPosition({ x: 0, y: 0 }); + } + }; + + onMount(() => { + document.addEventListener('keydown', handleKeyDown); + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + document.body.style.overflow = 'hidden'; + + if (imageContainer) { + imageContainer.addEventListener('wheel', handleWheel as any, { passive: false }); + } + + if (modalContainer) { + document.body.appendChild(modalContainer); + } + + onCleanup(() => { + document.removeEventListener('keydown', handleKeyDown); + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + if (imageContainer) { + imageContainer.removeEventListener('wheel', handleWheel as any); + } + document.body.style.overflow = ''; + if (modalContainer && modalContainer.parentNode === document.body) { + document.body.removeChild(modalContainer); + } + }); + }); + + return ( +
+
+ + + +
+
+
+
+ + +
+
Failed to load image
+
Please try again later
+
+
+ + Enlarged image 1 ? (isDragging ? 'grabbing' : 'grab') : 'zoom-in' + }} + onLoad={handleImageLoad} + onError={handleImageError} + onMouseDown={handleMouseDown} + onDblClick={handleDoubleClick} + /> +
+
+ ); +}; \ No newline at end of file From 632a2271fa659435f434c4895c2c03abb875cd1d Mon Sep 17 00:00:00 2001 From: haviet4012 Date: Tue, 6 Jan 2026 14:30:19 +0700 Subject: [PATCH 29/33] hide text and icon --- src/components/Bot.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Bot.tsx b/src/components/Bot.tsx index de327d2..592b2bd 100644 --- a/src/components/Bot.tsx +++ b/src/components/Bot.tsx @@ -2814,10 +2814,10 @@ export const Bot = (botProps: BotProps & { class?: string }) => { 2 && followUpPromptsStatus()}> 0}> <> -
+ {/*
Try these prompts -
+
*/}
{(prompt, index) => ( From 54d04d65cd0c13542e1a5819a4636bd76978e0f0 Mon Sep 17 00:00:00 2001 From: PhucZuu Date: Thu, 8 Jan 2026 15:09:04 +0700 Subject: [PATCH 30/33] Reduce the height by less on a 768px screen --- src/features/bubble/components/Bubble.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/bubble/components/Bubble.tsx b/src/features/bubble/components/Bubble.tsx index 5bf46b3..3890e2d 100644 --- a/src/features/bubble/components/Bubble.tsx +++ b/src/features/bubble/components/Bubble.tsx @@ -128,7 +128,7 @@ export const Bubble = (props: BubbleProps) => { part="bot" style={{ height: bubbleProps.theme?.chatWindow?.height - ? `${Math.min(bubbleProps.theme.chatWindow.height, windowSize().height - 240)}px` + ? `${Math.min(bubbleProps.theme.chatWindow.height, windowSize().height - (windowSize().height < 768 ? 180 : 240))}px` : 'calc(100% - 150px)', width: bubbleProps.theme?.chatWindow?.width ? `${Math.min(bubbleProps.theme.chatWindow.width, windowSize().width - 120)}px` : undefined, 'max-height': 'calc(100vh - 20px)', From c90362332b52dd49905d4b716337491103a18559 Mon Sep 17 00:00:00 2001 From: PhucZuu Date: Tue, 6 Jan 2026 15:53:04 +0700 Subject: [PATCH 31/33] Feature onUpload --- src/components/Bot.tsx | 74 ++++++++++++++++++++++- src/constants.ts | 1 + src/features/bubble/components/Bubble.tsx | 1 + src/features/full/components/Full.tsx | 1 + src/window.ts | 1 + 5 files changed, 75 insertions(+), 3 deletions(-) diff --git a/src/components/Bot.tsx b/src/components/Bot.tsx index 592b2bd..7b91f66 100644 --- a/src/components/Bot.tsx +++ b/src/components/Bot.tsx @@ -178,6 +178,7 @@ export type BotProps = { chatbot: string; apiHost?: string; onRequest?: (request: RequestInit) => Promise; + onUpload?: (files: IUploads, context: any) => Promise; config?: Record; backgroundColor?: string; welcomeMessage?: string; @@ -1308,17 +1309,85 @@ export const Bot = (botProps: BotProps & { class?: string }) => { }; }); + let customUpload:boolean = false; try { - uploads = await handleFileUploads(uploads); + if (uploads.length > 0) { + // Check if the user has onUpload enabled. + let hasCustomUpload = 'onUpload' in props && typeof props.onUpload === 'function'; + customUpload = hasCustomUpload ? true : false; + + if (hasCustomUpload) { + try { + const files = uploads + .filter(u => u?.data && u?.name && u?.mime) + .map(u => { + if (typeof u.data !== 'string') { + throw new Error('Upload data is not base64 string'); + } + const base64 = u.data.split(',')[1]; + const binary = window.atob(base64); + const bytes = new Uint8Array(binary.length); + + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + + return new File([bytes], u.name, { type: u.mime }); + }); + + const imageUrls = await props.onUpload?.(files as unknown as IUploads,{}); + + if (!imageUrls || !Array.isArray(imageUrls)) { + throw new Error('Custom onUpload must return an array of image URLs'); + } + + // Convert image URLs to markdown format + const imageMarkdown = imageUrls + .map(url => `\n![image](${url})`) + .join(''); + + value = value + imageMarkdown; + } catch (customUploadError) { + console.error('Custom upload failed:', customUploadError); + handleError( + customUploadError instanceof Error + ? customUploadError.message + : 'Custom upload processing failed', + true + ); + setLoading(false); + return; + } + } else { + uploads = await handleFileUploads(uploads); + } + } } catch (error) { + console.error('Upload error:', error); handleError('Unable to upload documents', true); + setLoading(false); return; } clearPreviews(); + // Attach image URLs to user messages when using customUpload + let userMessage = value as string; + if (customUpload && uploads?.length) { + const imageUrls = (uploads as any[]) + .filter(upload => upload.data?.url && upload.data?.mime?.startsWith('image')) + .map(upload => `\n![${upload.data.name}](${upload.data.url})`) + .join(''); + + if (imageUrls) { + userMessage += imageUrls; + } + + uploads = []; + } + setMessages((prevMessages) => { - const messages: MessageType[] = [...prevMessages, { message: value as string, type: 'userMessage', fileUploads: uploads, dateTime: new Date().toISOString() }]; + const messages: MessageType[] = [...prevMessages, { message: userMessage, type: 'userMessage', fileUploads: uploads, dateTime: new Date().toISOString() }]; addChatMessage(messages); return messages; }); @@ -1409,7 +1478,6 @@ export const Bot = (botProps: BotProps & { class?: string }) => { } } - // Update last question to avoid saving base64 data to localStorage if (uploads && uploads.length > 0) { setMessages((data) => { const messages = data.map((item, i) => { diff --git a/src/constants.ts b/src/constants.ts index 701eca9..62263c5 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -4,6 +4,7 @@ export const defaultBotProps: BubbleProps = { chatbot: '', apiHost: undefined, onRequest: undefined, + onUpload: undefined, config: undefined, theme: undefined, observersConfig: undefined, diff --git a/src/features/bubble/components/Bubble.tsx b/src/features/bubble/components/Bubble.tsx index 3890e2d..6999b05 100644 --- a/src/features/bubble/components/Bubble.tsx +++ b/src/features/bubble/components/Bubble.tsx @@ -198,6 +198,7 @@ export const Bubble = (props: BubbleProps) => { config={props.config} apiHost={props.apiHost} onRequest={props.onRequest} + onUpload={props.onUpload} observersConfig={props.observersConfig} clearChatOnReload={bubbleProps.theme?.chatWindow?.clearChatOnReload} disclaimer={bubbleProps.theme?.disclaimer} diff --git a/src/features/full/components/Full.tsx b/src/features/full/components/Full.tsx index 4c481ec..e4946fb 100644 --- a/src/features/full/components/Full.tsx +++ b/src/features/full/components/Full.tsx @@ -86,6 +86,7 @@ export const Full = (props: FullProps, { element }: { element: HTMLElement }) => config={props.config} apiHost={props.apiHost} onRequest={props.onRequest} + onUpload={props.onUpload} isFullPage={true} observersConfig={props.observersConfig} starterPromptFontSize={props.theme?.chatWindow?.starterPromptFontSize} diff --git a/src/window.ts b/src/window.ts index eb79511..2212009 100644 --- a/src/window.ts +++ b/src/window.ts @@ -6,6 +6,7 @@ type BotProps = { chatbot: string; apiHost?: string; onRequest?: (request: RequestInit) => Promise; + onUpload?: (files: any, context: any) => Promise; config?: Record; observersConfig?: observersConfigType; theme?: BubbleTheme; From eed7b7690cac0c201b61280f6b29190688625b46 Mon Sep 17 00:00:00 2001 From: PhucZuu Date: Mon, 12 Jan 2026 16:27:45 +0700 Subject: [PATCH 32/33] Fix QuotaExceededError when using TTS --- src/components/Bot.tsx | 187 +++++++++++++++++++++-------------------- 1 file changed, 98 insertions(+), 89 deletions(-) diff --git a/src/components/Bot.tsx b/src/components/Bot.tsx index 7b91f66..e2786fb 100644 --- a/src/components/Bot.tsx +++ b/src/components/Bot.tsx @@ -572,6 +572,7 @@ export const Bot = (botProps: BotProps & { class?: string }) => { isBuffering: false, audioFormat: null as string | null, abortController: null as AbortController | null, + isEndedRequested: false as boolean, }); // TTS auto-scroll prevention refs @@ -1151,7 +1152,7 @@ export const Bot = (botProps: BotProps & { class?: string }) => { handleTTSDataChunk(payload.data.audioChunk); break; case 'tts_end': - handleTTSEnd(); + handleTTSEnd(payload.data?.chatMessageId); break; case 'tts_abort': handleTTSAbort(payload.data); @@ -2138,15 +2139,35 @@ export const Bot = (botProps: BotProps & { class?: string }) => { // TTS Functions const processChunkQueue = () => { const currentState = ttsStreamingState(); - if (!currentState.sourceBuffer || currentState.sourceBuffer.updating || currentState.chunkQueue.length === 0) { + const { mediaSource, sourceBuffer, chunkQueue, isEndedRequested } = currentState; + + // Require a valid, open MediaSource and SourceBuffer + if (!mediaSource || mediaSource.readyState !== 'open' || !sourceBuffer) { + return; + } + + // If there are no queued chunks, we may be ready to end the stream + if (chunkQueue.length === 0) { + if (isEndedRequested && !sourceBuffer.updating) { + try { + mediaSource.endOfStream(); + } catch (e) { + console.error('Error calling endOfStream when queue empty:', e); + } + } + return; + } + + // If buffer is busy, wait for updateend listener to retry + if (sourceBuffer.updating) { return; } - const chunk = currentState.chunkQueue[0]; - if (!chunk) return; + const nextChunk = chunkQueue[0]; + if (!nextChunk) return; try { - currentState.sourceBuffer.appendBuffer(chunk); + sourceBuffer.appendBuffer(nextChunk); setTtsStreamingState((prevState) => ({ ...prevState, chunkQueue: prevState.chunkQueue.slice(1), @@ -2189,9 +2210,11 @@ export const Bot = (botProps: BotProps & { class?: string }) => { isBuffering: false, audioFormat: data.format, abortController: null, + isEndedRequested: false }); - setTimeout(() => initializeTTSStreaming(data), 100); + // Increased timeout to ensure cleanup is fully completed + setTimeout(() => initializeTTSStreaming(data), 200); }; const handleTTSDataChunk = (base64Data: string) => { @@ -2204,10 +2227,8 @@ export const Bot = (botProps: BotProps & { class?: string }) => { chunkQueue: [...prevState.chunkQueue, audioBuffer], }; - // Schedule processing after state update - if (prevState.sourceBuffer && !prevState.sourceBuffer.updating) { - setTimeout(() => processChunkQueue(), 0); - } + // Always process the queue, processChunkQueue() handles buffer and MediaSource state checks + setTimeout(() => processChunkQueue(), 0); return newState; }); @@ -2216,53 +2237,30 @@ export const Bot = (botProps: BotProps & { class?: string }) => { } }; - const handleTTSEnd = () => { + const handleTTSEnd = (chatMessageId?: string) => { const currentState = ttsStreamingState(); - if (currentState.mediaSource && currentState.mediaSource.readyState === 'open') { - try { - // Process any remaining chunks first - if (currentState.sourceBuffer && currentState.chunkQueue.length > 0 && !currentState.sourceBuffer.updating) { - const remainingChunks = [...currentState.chunkQueue]; - remainingChunks.forEach((chunk, index) => { - setTimeout(() => { - const state = ttsStreamingState(); - if (state.sourceBuffer && !state.sourceBuffer.updating) { - try { - state.sourceBuffer.appendBuffer(chunk); - if (index === remainingChunks.length - 1) { - setTimeout(() => { - const finalState = ttsStreamingState(); - if (finalState.mediaSource && finalState.mediaSource.readyState === 'open') { - finalState.mediaSource.endOfStream(); - } - }, 100); - } - } catch (error) { - console.error('Error appending remaining chunk:', error); - } - } - }, index * 50); - }); - setTtsStreamingState((prevState) => ({ - ...prevState, - chunkQueue: [], - })); - } else if (currentState.sourceBuffer && !currentState.sourceBuffer.updating) { - currentState.mediaSource.endOfStream(); - } else if (currentState.sourceBuffer) { - const handleFinalUpdateEnd = () => { - const finalState = ttsStreamingState(); - if (finalState.mediaSource && finalState.mediaSource.readyState === 'open') { - finalState.mediaSource.endOfStream(); - } - }; - currentState.sourceBuffer.addEventListener('updateend', handleFinalUpdateEnd, { once: true }); - } - } catch (error) { - console.error('Error ending TTS stream:', error); - } + // Clear loading/playing flags for this message if id is provided + if (chatMessageId) { + setIsTTSLoading((prev) => { + const next = { ...prev } as Record; + delete next[chatMessageId]; + return next; + }); + setIsTTSPlaying((prev) => { + const next = { ...prev } as Record; + delete next[chatMessageId]; + return next; + }); } + // Request stream end, endOfStream() runs after queued chunks are processed. + setTtsStreamingState((prev) => ({ + ...prev, + isEndedRequested: true, + })); + + // Trigger a processing attempt in case the queue is already empty + setTimeout(() => processChunkQueue(), 0); }; const initializeTTSStreaming = (data: { chatMessageId: string; format: string }) => { @@ -2283,28 +2281,38 @@ export const Bot = (botProps: BotProps & { class?: string }) => { // Check if MediaSource supports the MIME type if (!MediaSource.isTypeSupported(mimeType)) { console.error('MediaSource does not support MIME type:', mimeType); + cleanupTTSStreaming(); return; } const sourceBuffer = mediaSource.addSourceBuffer(mimeType); + // Whenever a chunk finishes appending, try to process the next one + sourceBuffer.addEventListener('updateend', () => { + processChunkQueue(); + }); + setTtsStreamingState((prevState) => ({ ...prevState, mediaSource, sourceBuffer, audio, + isEndedRequested: false, })); // Start audio playback - audio.play().catch((playError) => { - console.error('Error starting audio playback:', playError); - // Cleanup on play error - cleanupTTSStreaming(); - }); + const playPromise = audio.play(); + if (playPromise !== undefined) { + playPromise.catch((playError) => { + console.error('Error starting audio playback:', playError); + if (playError.name !== 'AbortError') { + cleanupTTSStreaming(); + } + }); + } } catch (error) { console.error('Error setting up source buffer:', error); console.error('MediaSource readyState:', mediaSource.readyState); - // Cleanup on error cleanupTTSStreaming(); } }; @@ -2367,61 +2375,59 @@ export const Bot = (botProps: BotProps & { class?: string }) => { currentState.abortController.abort(); } + let objectUrlToRevoke: string | null = null; if (currentState.audio) { + if (currentState.audio.src) { + objectUrlToRevoke = currentState.audio.src; + } currentState.audio.pause(); currentState.audio.currentTime = 0; currentState.audio.removeAttribute('src'); - currentState.audio.load(); // Force reload to clear buffer - if (currentState.audio.src) { - URL.revokeObjectURL(currentState.audio.src); - } - // Remove all event listeners - currentState.audio.removeEventListener('playing', () => console.log('Playing')); - currentState.audio.removeEventListener('ended', () => console.log('Ended')); + currentState.audio.load(); } if (currentState.sourceBuffer) { - // Clear any pending data in the source buffer if (currentState.sourceBuffer.updating) { try { currentState.sourceBuffer.abort(); - } catch (e) { - // Ignore abort errors + } catch { + // ignore } } - // Remove buffered data if possible try { if (currentState.sourceBuffer.buffered.length > 0) { const start = currentState.sourceBuffer.buffered.start(0); - const end = currentState.sourceBuffer.buffered.end(currentState.sourceBuffer.buffered.length - 1); + const end = currentState.sourceBuffer.buffered.end( + currentState.sourceBuffer.buffered.length - 1, + ); currentState.sourceBuffer.remove(start, end); } - } catch (e) { - // Ignore remove errors during cleanup - } - - // Remove update listeners - if (currentState.sourceBuffer.onupdateend) { - currentState.sourceBuffer.removeEventListener('updateend', currentState.sourceBuffer.onupdateend); - currentState.sourceBuffer.onupdateend = null; + } catch { + // ignore } } if (currentState.mediaSource) { if (currentState.mediaSource.readyState === 'open') { try { - // Remove source buffers before ending stream - if (currentState.sourceBuffer && currentState.mediaSource.sourceBuffers.length > 0) { - currentState.mediaSource.removeSourceBuffer(currentState.sourceBuffer); + const sourceBuffers = currentState.mediaSource.sourceBuffers; + for (let i = sourceBuffers.length - 1; i >= 0; i--) { + currentState.mediaSource.removeSourceBuffer(sourceBuffers[i]); } currentState.mediaSource.endOfStream(); - } catch (e) { - // Ignore errors during cleanup + } catch { + // ignore } } - // Remove source open event listeners - currentState.mediaSource.removeEventListener('sourceopen', () => console.log('removed source open event listener')); + } + + if (objectUrlToRevoke) { + try { + URL.revokeObjectURL(objectUrlToRevoke); + } catch { + // ignore + } } setTtsStreamingState({ @@ -2432,6 +2438,7 @@ export const Bot = (botProps: BotProps & { class?: string }) => { isBuffering: false, audioFormat: null, abortController: null, + isEndedRequested: false }); }; @@ -2612,7 +2619,7 @@ export const Bot = (botProps: BotProps & { class?: string }) => { break; case 'tts_end': if (!abortController.signal.aborted) { - handleTTSEnd(); + handleTTSEnd(event.data?.chatMessageId); } break; } @@ -2766,7 +2773,9 @@ export const Bot = (botProps: BotProps & { class?: string }) => { <>
- +
+ +
From c62db0cfb6bbe84b5a235a370414c35940e27106 Mon Sep 17 00:00:00 2001 From: PhucZuu Date: Mon, 19 Jan 2026 09:46:21 +0700 Subject: [PATCH 33/33] Display a chatbot service expiration message --- src/components/Bot.tsx | 66 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/src/components/Bot.tsx b/src/components/Bot.tsx index e2786fb..bab9ba6 100644 --- a/src/components/Bot.tsx +++ b/src/components/Bot.tsx @@ -523,7 +523,9 @@ export const Bot = (botProps: BotProps & { class?: string }) => { const [isLeadSaved, setIsLeadSaved] = createSignal(false); const [leadEmail, setLeadEmail] = createSignal(''); const [disclaimerPopupOpen, setDisclaimerPopupOpen] = createSignal(false); - + const [botConfig, setBotConfig] = createSignal({}); + const [isExpired, setIsExpired] = createSignal(false); + // OIDC authentication state const [oidcConfig, setOidcConfig] = createSignal(null); const [oidcToken, setOidcToken] = createSignal(null); @@ -1274,6 +1276,26 @@ export const Bot = (botProps: BotProps & { class?: string }) => { return uploads; }; + // Check if the chatbot has expired + createEffect(() => { + const configExpiredAt = botConfig()?.expired_at || props.config?.expired_at; + + if (configExpiredAt) { + const expiredAt = new Date(configExpiredAt); + + if (!isNaN(expiredAt.getTime())) { + const currentDate = new Date(); + const isChatbotExpired = expiredAt < currentDate; + + setIsExpired(isChatbotExpired); + } else { + setIsExpired(false); + } + } else { + setIsExpired(false); + } + }); + // Handle form submission const handleSubmit = async (value: string | object, action?: IAction | undefined | null, humanInput?: any) => { if (typeof value === 'string' && value.trim() === '') { @@ -1687,6 +1709,7 @@ export const Bot = (botProps: BotProps & { class?: string }) => { if (result.data) { const chatbotConfig = result.data; + setBotConfig(chatbotConfig); if (chatbotConfig.flowData) { const nodes = JSON.parse(chatbotConfig.flowData).nodes ?? []; @@ -2711,8 +2734,49 @@ export const Bot = (botProps: BotProps & { class?: string }) => { } }; + // Expiration Overlay Component + const ExpirationOverlay = () => { + return ( +
+
+ {/* Icon */} +
+ + + + + +
+ + {/* Title */} +

+ Service Expired +

+ + {/* Message */} +

+ This AI chatbot is unavailable because the service plan has expired.
+ Please renew your subscription. +

+
+
+ ); + }; + return ( <> + + + {startInputType() === 'formInput' && messages().length === 1 ? (