├── .dockerignore
├── .env
├── .gitignore
├── .vscode
└── launch.json
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── LICENSE-3rd-party.txt
├── README.md
├── SECURITY.md
├── __tests__
└── utils
│ └── app
│ └── importExports.test.ts
├── components
├── .DS_Store
├── Avatar
│ ├── AgentAvatar.tsx
│ ├── BotAvatar.tsx
│ ├── SystemAvatar.tsx
│ └── UserAvatar.tsx
├── Buttons
│ └── SidebarActionButton
│ │ ├── SidebarActionButton.tsx
│ │ └── index.ts
├── Chat
│ ├── Chat.tsx
│ ├── ChatHeader.tsx
│ ├── ChatInput.tsx
│ ├── ChatInteractionMessage.tsx
│ ├── ChatLoader.tsx
│ ├── ChatMessage.tsx
│ ├── ErrorMessageDiv.tsx
│ ├── MemoizedChatMessage.tsx
│ └── Regenerate.tsx
├── Chatbar
│ ├── Chatbar.context.tsx
│ ├── Chatbar.state.tsx
│ ├── Chatbar.tsx
│ └── components
│ │ ├── ChatFolders.tsx
│ │ ├── ChatbarSettings.tsx
│ │ ├── ClearConversations.tsx
│ │ ├── Conversation.tsx
│ │ └── Conversations.tsx
├── Folder
│ ├── Folder.tsx
│ └── index.ts
├── Markdown
│ ├── Chart.tsx
│ ├── CodeBlock.tsx
│ ├── CustomComponents.tsx
│ ├── CustomDetails.tsx
│ ├── CustomSummary.tsx
│ ├── Image.tsx
│ ├── Loading.tsx
│ ├── MemoizedReactMarkdown.tsx
│ └── Video.tsx
├── Mobile
│ └── Navbar.tsx
├── Search
│ ├── Search.tsx
│ └── index.ts
├── Settings
│ ├── Import.tsx
│ └── SettingDialog.tsx
├── Sidebar
│ ├── Sidebar.tsx
│ ├── SidebarButton.tsx
│ ├── components
│ │ └── OpenCloseButton.tsx
│ └── index.ts
└── Spinner
│ ├── Spinner.tsx
│ └── index.ts
├── config.json
├── constants
└── constants.tsx
├── hooks
├── useConversationOperations.ts
├── useCreateReducer.ts
└── useFolderOperations.ts
├── next-env.d.ts
├── next-i18next.config.js
├── next.config.js
├── package-lock.json
├── package.json
├── pages
├── _app.tsx
├── _document.tsx
├── aiq-auth.tsx
├── api
│ ├── chat.ts
│ └── home
│ │ ├── home.context.tsx
│ │ ├── home.state.tsx
│ │ ├── home.tsx
│ │ └── index.ts
└── index.tsx
├── postcss.config.js
├── prettier.config.js
├── public
├── audio
│ └── recording.wav
├── favicon.jpg
├── favicon1.png
├── locales
│ └── en
│ │ └── common.json
├── nvidia.jpg
└── screenshots
│ ├── hitl_prompt.png
│ ├── hitl_settings.png
│ ├── ui_generate_example.png
│ ├── ui_generate_example_settings.png
│ └── ui_home_page.png
├── styles
└── globals.css
├── tailwind.config.js
├── tsconfig.json
├── types
├── chat.ts
├── data.ts
├── env.ts
├── error.ts
├── export.ts
├── folder.ts
├── index.ts
├── settings.ts
└── storage.ts
├── utils
├── app
│ ├── api.ts
│ ├── clean.ts
│ ├── codeblock.ts
│ ├── const.ts
│ ├── conversation.ts
│ ├── folders.ts
│ ├── helper.ts
│ ├── importExport.ts
│ ├── prompts.ts
│ └── settings.ts
└── data
│ └── throttle.ts
└── vitest.config.ts
/.dockerignore:
--------------------------------------------------------------------------------
1 | Dockerfile
2 | .gitignore
3 | .git
4 | tests/
5 | *.md
6 | .idea
7 | out/
8 | k8s
9 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_WORKFLOW=AIQ Toolkit
2 | NEXT_PUBLIC_WEBSOCKET_CHAT_COMPLETION_URL=ws://127.0.0.1:8000/websocket
3 | NEXT_PUBLIC_HTTP_CHAT_COMPLETION_URL=http://127.0.0.1:8000/chat/stream
4 | NEXT_PUBLIC_WEB_SOCKET_DEFAULT_ON=false
5 | NEXT_PUBLIC_CHAT_HISTORY_DEFAULT_ON=false
6 | NEXT_PUBLIC_RIGHT_MENU_OPEN=false
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | /node_modules
3 | /.pnp
4 | .pnp.js
5 | package-lock.json
6 |
7 | # testing
8 | /coverage
9 |
10 | # production
11 | /build
12 |
13 | # next
14 | /.next
15 | /out
16 |
17 | # misc
18 | .DS_Store
19 | .env.local
20 | .env.development.local
21 | .env.test.local
22 | .env.production.local
23 |
24 | # environment files
25 | public/__ENV.js
26 |
27 | npm-debug.log*
28 | yarn-debug.log*
29 | yarn-error.log*
30 |
31 | yarn.lock
32 | *.pem
33 | .vscode
34 |
35 | # PyCharm build files
36 | .idea/
37 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | // {
8 | // "name": "Attach to Chrome",
9 | // "port": 3000,
10 | // "request": "attach",
11 | // "type": "chrome",
12 | // "webRoot": "${workspaceFolder}"
13 | // },
14 | {
15 | "name": "Debug1",
16 | "type": "node-terminal",
17 | "request": "launch",
18 | "command": "npm run dev",
19 | "cwd": "${workspaceFolder}",
20 | "serverReadyAction": {
21 | "pattern": "started server on .+, url: (https?://.+)",
22 | "uriFormat": "%s",
23 | "action": "debugWithChrome"
24 | },
25 | "debugStdLib": true
26 | },
27 | {
28 | "name": "Debug2",
29 | "type": "node-terminal",
30 | "request": "launch",
31 | "command": "NEXT_PRIVATE_TURBOPACK=true npm run dev",
32 | "cwd": "${workspaceFolder}",
33 | "env": {
34 | "NODE_ENV": "development"
35 | },
36 | "serverReadyAction": {
37 | "pattern": "started server on .+, url: (https?://.+)",
38 | "uriFormat": "%s",
39 | "action": "debugWithChrome"
40 | },
41 | "console": "integratedTerminal",
42 | "outputCapture": "std"
43 | }
44 | ]
45 | }
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing Guidelines
2 |
3 | **Welcome to AIQ Toolkit-UI**
4 |
5 | We appreciate your interest in contributing to our project.
6 |
7 | Before you get started, please read our guidelines for contributing.
8 |
9 | ## Types of Contributions
10 |
11 | We welcome the following types of contributions:
12 |
13 | - Bug fixes
14 | - New features
15 | - Documentation improvements
16 | - Code optimizations
17 | - Translations
18 | - Tests
19 |
20 | ## Get Started
21 |
22 | To get started, fork the project on GitHub and clone it locally on your machine. Then, create a new branch to work on your changes.
23 |
24 | ```bash
25 | git clone https://github.com/NVIDIA/AIQ Toolkit-UI.git
26 | cd AIQ Toolkit-UI
27 | git checkout -b my-branch-name
28 | ```
29 |
30 | Before submitting your pull request, please make sure your changes pass our automated tests and adhere to our code style guidelines.
31 |
32 | ## Pull Request Process
33 |
34 | 1. Fork the project on GitHub.
35 | 2. Clone your forked repository locally on your machine.
36 | 3. Create a new branch from the main branch.
37 | 4. Make your changes on the new branch.
38 | 5. Ensure that your changes adhere to our code style guidelines and pass our automated tests.
39 | 6. Commit your changes and push them to your forked repository.
40 | 7. Submit a pull request to the main branch of the main repository.
41 |
42 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:18-alpine AS base
2 |
3 | # Install dependencies only when needed
4 | FROM base AS deps
5 | # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
6 | RUN apk add --no-cache libc6-compat
7 |
8 |
9 | WORKDIR /app
10 |
11 | # Install dependencies based on the preferred package manager
12 | COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
13 |
14 |
15 | RUN npm i
16 |
17 | # Rebuild the source code only when needed
18 | FROM base AS builder
19 | WORKDIR /app
20 |
21 |
22 | COPY --from=deps /app/node_modules ./node_modules
23 | COPY . .
24 |
25 | RUN apk update
26 |
27 | # Set working directory
28 | WORKDIR /app
29 | # install node modules
30 | COPY package.json /app/package.json
31 | RUN npm install
32 | # Copy all files from current directory to working dir in image
33 | COPY . .
34 | # Build the assets
35 | RUN yarn build
36 |
37 | # Next.js collects completely anonymous telemetry data about general usage.
38 | # Learn more here: https://nextjs.org/telemetry
39 | # Uncomment the following line in case you want to disable telemetry during the build.
40 | # ENV NEXT_TELEMETRY_DISABLED 1
41 |
42 | RUN npm run build
43 |
44 | # Production image, copy all the files and run next
45 | FROM base AS runner
46 | WORKDIR /app
47 |
48 | ENV NODE_ENV production
49 | # Uncomment the following line in case you want to disable telemetry during runtime.
50 | # ENV NEXT_TELEMETRY_DISABLED 1
51 |
52 | RUN addgroup --system --gid 1001 nodejs
53 | RUN adduser --system --uid 1001 nextjs
54 |
55 | COPY --from=builder /app/public ./public
56 |
57 | # Set the correct permission for prerender cache
58 | RUN mkdir .next
59 | RUN chown nextjs:nodejs .next
60 |
61 | # Automatically leverage output traces to reduce image size
62 | # https://nextjs.org/docs/advanced-features/output-file-tracing
63 | COPY --from=builder --chown=nextjs:nodejs /app/next.config.js ./
64 | COPY --from=builder --chown=nextjs:nodejs /app/public ./public
65 | COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
66 | COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
67 | COPY --from=builder --chown=nextjs:nodejs /app/package.json ./package.json
68 |
69 | USER nextjs
70 |
71 | EXPOSE 3000
72 |
73 | ENV PORT 3000
74 | # set hostname to localhost
75 | ENV HOSTNAME "0.0.0.0"
76 |
77 | # server.js is created by next build from the standalone output
78 | # https://nextjs.org/docs/pages/api-reference/next-config-js/output
79 | CMD ["node", "server.js"]
80 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2 | # SPDX-License-Identifier: MIT
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a
5 | # copy of this software and associated documentation files (the "Software"),
6 | # to deal in the Software without restriction, including without limitation
7 | # the rights to use, copy, modify, merge, publish, distribute, sublicense,
8 | # and/or sell copies of the Software, and to permit persons to whom the
9 | # Software is furnished to do so, subject to the following conditions:
10 | #
11 | # The above copyright notice and this permission notice shall be included in
12 | # all copies or substantial portions of the Software.
13 | #
14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
17 | # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
20 | # DEALINGS IN THE SOFTWARE.
21 | /*
22 | * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
23 | * SPDX-License-Identifier: MIT
24 | *
25 | * Permission is hereby granted, free of charge, to any person obtaining a
26 | * copy of this software and associated documentation files (the "Software"),
27 | * to deal in the Software without restriction, including without limitation
28 | * the rights to use, copy, modify, merge, publish, distribute, sublicense,
29 | * and/or sell copies of the Software, and to permit persons to whom the
30 | * Software is furnished to do so, subject to the following conditions:
31 | *
32 | * The above copyright notice and this permission notice shall be included in
33 | * all copies or substantial portions of the Software.
34 | *
35 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
36 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
37 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
38 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
39 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
40 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
41 | * DEALINGS IN THE SOFTWARE.
42 | */
43 |
44 | MIT License
45 |
46 | Copyright (c) 2024 Ivan Fioravanti
47 |
48 | Permission is hereby granted, free of charge, to any person obtaining a copy
49 | of this software and associated documentation files (the "Software"), to deal
50 | in the Software without restriction, including without limitation the rights
51 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
52 | copies of the Software, and to permit persons to whom the Software is
53 | furnished to do so, subject to the following conditions:
54 |
55 | The above copyright notice and this permission notice shall be included in all
56 | copies or substantial portions of the Software.
57 |
58 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
59 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
60 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
61 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
62 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
63 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
64 | SOFTWARE.
65 |
66 | MIT License
67 |
68 | Copyright (c) 2024 Mckay Wrigley
69 |
70 | Permission is hereby granted, free of charge, to any person obtaining a copy
71 | of this software and associated documentation files (the "Software"), to deal
72 | in the Software without restriction, including without limitation the rights
73 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
74 | copies of the Software, and to permit persons to whom the Software is
75 | furnished to do so, subject to the following conditions:
76 |
77 | The above copyright notice and this permission notice shall be included in all
78 | copies or substantial portions of the Software.
79 |
80 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
81 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
82 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
83 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
84 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
85 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
86 | SOFTWARE.
87 |
--------------------------------------------------------------------------------
/LICENSE-3rd-party.txt:
--------------------------------------------------------------------------------
1 | AIQ Toolkit-UI Third-Party Licenses
2 | =================================
3 |
4 | This file contains third-party license information for software packages used in this project.
5 | The licenses below apply to one or more packages included in this project. For each license,
6 | we list the packages that are distributed under it and include the full license text.
7 |
8 | ------------------------------------------------------------
9 | MIT License
10 | ------------------------------------------------------------
11 | The MIT License is a permissive free software license. Many of the packages used in this
12 | project are distributed under the MIT License. The full text of the MIT License is provided
13 | below.
14 |
15 | Packages under the MIT License:
16 | - @dqbd/tiktoken @ ^1.0.2
17 | - @radix-ui/react-select @ ^2.1.2
18 | - @tabler/icons-react @ ^2.9.0
19 | - @types/jwt-decode @ ^3.1.0
20 | - axios @ ^1.6.8
21 | - chart.js @ ^4.4.1
22 | - eventsource-parser @ ^0.1.0
23 | - file-saver @ ^2.0.5
24 | - html-to-image @ ^1.11.11
25 | - i18next @ ^22.4.13
26 | - jsonwebtoken @ ^9.0.2
27 | - jwt-decode @ ^4.0.0
28 | - lodash @ ^4.17.21
29 | - lucide-react @ ^0.454.0
30 | - next @ 13.5.3
31 | - next-auth @ ^4.24.7
32 | - next-i18next @ ^13.2.2
33 | - pptxgenjs @ ^3.12.0
34 | - react @ 18.2.0
35 | - react-bootstrap-modal @ ^4.2.0
36 | - react-chartjs-2 @ ^5.2.0
37 | - react-dom @ 18.2.0
38 | - react-force-graph-2d @ ^1.25.5
39 | - react-hot-toast @ ^2.4.0
40 | - react-i18next @ ^12.2.0
41 | - react-markdown @ ^8.0.5
42 | - react-query @ ^3.39.3
43 | - react-syntax-highlighter @ ^15.5.0
44 | - recharts @ ^2.12.7
45 | - rehype-mathjax @ ^4.0.2
46 | - rehype-raw @ ^7.0.0
47 | - remark-gfm @ ^3.0.1
48 | - remark-math @ ^5.1.1
49 | - remark-unwrap-images @ ^4.0.0
50 | - uuid @ ^9.0.1
51 |
52 | Dev Dependencies under the MIT License:
53 | - @tailwindcss/typography @ ^0.5.9
54 | - @trivago/prettier-plugin-sort-imports @ ^1.4.4
55 | - @types/jsdom @ ^21.1.1
56 | - @types/node @ 18.15.0
57 | - @types/react @ 18.0.28
58 | - @types/react-dom @ 18.0.11
59 | - @types/react-syntax-highlighter @ ^15.5.6
60 | - @types/uuid @ ^9.0.1
61 | - @vitest/coverage-c8 @ ^0.29.7
62 | - autoprefixer @ ^10.4.14
63 | - endent @ ^2.1.0
64 | - eslint @ 8.36.0
65 | - eslint-config-next @ 13.2.4
66 | - gpt-3-encoder @ ^1.1.4
67 | - jsdom @ ^21.1.1
68 | - postcss @ ^8.4.21
69 | - prettier @ ^2.8.7
70 | - prettier-plugin-tailwindcss @ ^0.2.5
71 | - tailwindcss @ ^3.3.3
72 | - vitest @ ^0.29.7
73 | - next-runtime-env @ ^1.3.0
74 |
75 | Full MIT License Text:
76 | --------------------------------------------------
77 | MIT License
78 |
79 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software
80 | and associated documentation files (the "Software"), to deal in the Software without restriction,
81 | including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
82 | and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
83 | subject to the following conditions:
84 |
85 | The above copyright notice and this permission notice shall be included in all copies or
86 | substantial portions of the Software.
87 |
88 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
89 | BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
90 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
91 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
92 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
93 | --------------------------------------------------
94 |
95 | ------------------------------------------------------------
96 | Apache License, Version 2.0
97 | ------------------------------------------------------------
98 | The Apache License, Version 2.0 is a permissive license that also provides an express grant of patent rights.
99 |
100 | Packages under the Apache License, Version 2.0:
101 | - @datadog/browser-rum @ ^5.11.0
102 | - @mozilla/readability @ ^0.4.4
103 | - typescript @ 4.9.5
104 |
105 | Full Apache License, Version 2.0 Text:
106 | --------------------------------------------------
107 | Apache License
108 | Version 2.0, January 2004
109 | http://www.apache.org/licenses/
110 |
111 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
112 |
113 | 1. Definitions.
114 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined in this document.
115 | "Licensor" shall mean the copyright owner or entity
116 | authorized by the copyright owner that is granting the License.
117 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity.
118 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
119 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
120 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
121 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work.
122 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the modifications represent, as a whole, an original work of authorship.
123 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work.
124 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
125 |
126 | 2. Grant of Copyright License.
127 | Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive,
128 | no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of,
129 | publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works.
130 |
131 | 3. Grant of Patent License.
132 | Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive,
133 | no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell,
134 | sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor
135 | that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work.
136 |
137 | 4. Redistribution.
138 | You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications,
139 | and in Source or Object form, provided that You meet the following conditions:
140 | (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
141 | (b) You must cause any modified files to carry prominent notices stating that You changed the files; and
142 | (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark,
143 | and attribution notices from the Source form of the Work; and
144 | (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute
145 | must include a readable copy of the attribution notices contained within such NOTICE file.
146 |
147 | 5. Submission of Contributions.
148 | Unless You explicitly state otherwise, any Contribution submitted for inclusion in the Work shall be under the terms and conditions of this License.
149 |
150 | 6. Trademarks.
151 | This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor.
152 |
153 | 7. Disclaimer of Warranty.
154 | The Work is provided on an "AS IS" basis, without warranties or conditions of any kind, either express or implied.
155 |
156 | 8. Limitation of Liability.
157 | In no event shall any Contributor be liable for any damages arising from the use of the Work.
158 |
159 | END OF TERMS AND CONDITIONS
160 | --------------------------------------------------
161 |
162 | END OF THIRD-PARTY LICENSES
163 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AIQ Toolkit - UI
2 |
3 | [](LICENSE)
4 | [](https://github.com/NVIDIA/AIQToolkit/tree/main)
5 |
6 | This is the official frontend user interface component for [AIQ Toolkit](https://github.com/NVIDIA/AIQToolkit/tree/main), an open-source library for building AI agents and workflows.
7 |
8 | This project builds upon the work of:
9 | - [chatbot-ui](https://github.com/mckaywrigley/chatbot-ui) by Mckay Wrigley
10 | - [chatbot-ollama](https://github.com/ivanfioravanti/chatbot-ollama) by Ivan Fioravanti
11 |
12 | ## Features
13 | - 🎨 Modern and responsive user interface
14 | - 🔄 Real-time streaming responses
15 | - 🤝 Human-in-the-loop workflow support
16 | - 🌙 Light/Dark theme
17 | - 🔌 WebSocket and HTTP API integration
18 | - 🐳 Docker support
19 |
20 | ## Getting Started
21 |
22 | ### Prerequisites
23 | - [AIQ Toolkit](https://github.com/NVIDIA/AIQToolkit/tree/main) installed and configured
24 | - Git
25 | - Node.js (v18 or higher)
26 | - npm or Docker
27 |
28 | ### Installation
29 |
30 | Clone the repository:
31 | ```bash
32 | git clone git@github.com:NVIDIA/AIQToolkit.git
33 | cd AIQToolkit
34 | ```
35 |
36 | Install dependencies:
37 | ```bash
38 | npm ci
39 | ```
40 |
41 | ### Running the Application
42 |
43 | #### Local Development
44 | ```bash
45 | npm run dev
46 | ```
47 | The application will be available at `http://localhost:3000`
48 |
49 | #### Docker Deployment
50 | ```bash
51 | # Build the Docker image
52 | docker build -t AIQ Toolkit-UI .
53 |
54 | # Run the container with environment variables from .env
55 | # Ensure the .env file is present before running this command.
56 | # Skip --env-file .env if no overrides are needed.
57 | docker run --env-file .env -p 3000:3000 AIQ Toolkit-UI
58 | ```
59 |
60 | 
61 |
62 | ## Configuration
63 |
64 | ### HTTP API Connection
65 | Settings can be configured by selecting the `Settings` icon located on the bottom left corner of the home page.
66 |
67 | 
68 |
69 | ### Settings Options
70 | NOTE: Most of the time, you will want to select /chat/stream for intermediate results streaming.
71 |
72 | - `Theme`: Light or Dark Theme
73 | - `HTTP URL for Chat Completion`: REST API endpoint
74 | - /generate - Single response generation
75 | - /generate/stream - Streaming response generation
76 | - /chat - Single response chat completion
77 | - /chat/stream - Streaming chat completion
78 | - `WebSocket URL for Completion`: WebSocket URL to connect to running AIQ Toolkit server
79 | - `WebSocket Schema`: Workflow schema type over WebSocket connection
80 |
81 | ## Usage Examples
82 |
83 | ### Simple Calculator Example
84 |
85 | #### Setup and Configuration
86 | 1. Set up [AIQ Toolkit Get Started ](https://github.com/NVIDIA/AIQToolkit/blob/main/docs/source/intro/get-started.md)
87 | 2. Start workflow by following the [Simple Calculator Example](https://github.com/NVIDIA/AIQToolkit/blob/main/examples/simple_calculator/README.md)
88 | ```bash
89 | aiq serve --config_file=examples/simple_calculator/configs/config.yml
90 | ```
91 |
92 | #### Testing the Calculator
93 | Interact with the chat interface by prompting the agent with the message:
94 | ```
95 | Is 4 + 4 greater than the current hour of the day?
96 | ```
97 |
98 | 
99 |
100 | ## API Integration
101 |
102 | ### Server Communication
103 | The UI supports both HTTP requests (OpenAI compatible) and WebSocket connections for server communication. For detailed information about WebSocket messaging integration, please refer to the [WebSocket Documentation](https://github.com/NVIDIA/AIQToolkit/blob/main/docs/source/references/websockets.md) in the AIQ Toolkit documentation.
104 |
105 |
106 |
107 | ## License
108 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. The project includes code from [chatbot-ui](https://github.com/mckaywrigley/chatbot-ui) and [chatbot-ollama](https://github.com/ivanfioravanti/chatbot-ollama), which are also MIT licensed.
109 |
110 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | This security policy outlines the process for reporting vulnerabilities and secrets found within this GitHub repository. It is essential that all contributors and users adhere to this policy in order to maintain a secure and stable environment.
4 |
5 | ## Reporting a Vulnerability
6 |
7 | If you discover a vulnerability within the code, dependencies, or any other component of this repository, please follow these steps:
8 |
9 | 1. **Do not disclose the vulnerability publicly.** Publicly disclosing a vulnerability may put the project at risk and could potentially harm other users.
10 |
11 | 2. **Contact the repository maintainer(s) privately.** Send a private message or email to the maintainer(s) with a detailed description of the vulnerability. Include the following information:
12 |
13 | - The affected component(s)
14 | - Steps to reproduce the issue
15 | - Potential impact of the vulnerability
16 | - Any possible mitigations or workarounds
17 |
18 | 3. **Wait for a response from the maintainer(s).** Please be patient, as they may need time to investigate and verify the issue. The maintainer(s) should acknowledge receipt of your report and provide an estimated time frame for addressing the vulnerability.
19 |
20 | 4. **Cooperate with the maintainer(s).** If requested, provide additional information or assistance to help resolve the issue.
21 |
22 | 5. **Do not disclose the vulnerability until the maintainer(s) have addressed it.** Once the issue has been resolved, the maintainer(s) may choose to publicly disclose the vulnerability and credit you for the discovery.
23 |
24 | ## Reporting Secrets
25 |
26 | If you discover any secrets, such as API keys or passwords, within the repository, follow these steps:
27 |
28 | 1. **Do not share the secret or use it for unauthorized purposes.** Misusing a secret could have severe consequences for the project and its users.
29 |
30 | 2. **Contact the repository maintainer(s) privately.** Notify them of the discovered secret, its location, and any potential risks associated with it.
31 |
32 | 3. **Wait for a response and further instructions.**
33 |
34 | ## Responsible Disclosure
35 |
36 | We encourage responsible disclosure of vulnerabilities and secrets. If you follow the steps outlined in this policy, we will work with you to understand and address the issue. We will not take legal action against individuals who discover and report vulnerabilities or secrets in accordance with this policy.
37 |
38 | ## Patching and Updates
39 |
40 | We are committed to maintaining the security of our project. When vulnerabilities are reported and confirmed, we will:
41 |
42 | 1. Work diligently to develop and apply a patch or implement a mitigation strategy.
43 | 2. Keep the reporter informed about the progress of the fix.
44 | 3. Update the repository with the necessary patches and document the changes in the release notes or changelog.
45 | 4. Credit the reporter for the discovery, if they wish to be acknowledged.
46 |
47 | ## Contributing to Security
48 |
49 | We welcome contributions that help improve the security of our project. If you have suggestions or want to contribute code to address security issues, please follow the standard contribution guidelines for this repository. When submitting a pull request related to security, please mention that it addresses a security issue and provide any necessary context.
50 |
51 | By adhering to this security policy, you contribute to the overall security and stability of the project. Thank you for your cooperation and responsible handling of vulnerabilities and secrets.
52 |
--------------------------------------------------------------------------------
/__tests__/utils/app/importExports.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | cleanData,
3 | isExportFormatV1,
4 | isExportFormatV2,
5 | isExportFormatV3,
6 | isExportFormatV4,
7 | isLatestExportFormat,
8 | } from '@/utils/app/importExport';
9 |
10 | import { ExportFormatV1, ExportFormatV2, ExportFormatV4 } from '@/types/export';
11 |
12 |
13 | import { describe, expect, it } from 'vitest';
14 |
15 | describe('Export Format Functions', () => {
16 | describe('isExportFormatV1', () => {
17 | it('should return true for v1 format', () => {
18 | const obj = [{ id: 1 }];
19 | expect(isExportFormatV1(obj)).toBe(true);
20 | });
21 |
22 | it('should return false for non-v1 formats', () => {
23 | const obj = { version: 3, history: [], folders: [] };
24 | expect(isExportFormatV1(obj)).toBe(false);
25 | });
26 | });
27 |
28 | describe('isExportFormatV2', () => {
29 | it('should return true for v2 format', () => {
30 | const obj = { history: [], folders: [] };
31 | expect(isExportFormatV2(obj)).toBe(true);
32 | });
33 |
34 | it('should return false for non-v2 formats', () => {
35 | const obj = { version: 3, history: [], folders: [] };
36 | expect(isExportFormatV2(obj)).toBe(false);
37 | });
38 | });
39 |
40 | describe('isExportFormatV3', () => {
41 | it('should return true for v3 format', () => {
42 | const obj = { version: 3, history: [], folders: [] };
43 | expect(isExportFormatV3(obj)).toBe(true);
44 | });
45 |
46 | it('should return false for non-v3 formats', () => {
47 | const obj = { version: 4, history: [], folders: [] };
48 | expect(isExportFormatV3(obj)).toBe(false);
49 | });
50 | });
51 |
52 | describe('isExportFormatV4', () => {
53 | it('should return true for v4 format', () => {
54 | const obj = { version: 4, history: [], folders: [], prompts: [] };
55 | expect(isExportFormatV4(obj)).toBe(true);
56 | });
57 |
58 | it('should return false for non-v4 formats', () => {
59 | const obj = { version: 5, history: [], folders: [], prompts: [] };
60 | expect(isExportFormatV4(obj)).toBe(false);
61 | });
62 | });
63 | });
64 |
65 | describe('cleanData Functions', () => {
66 | describe('cleaning v1 data', () => {
67 | it('should return the latest format', () => {
68 | const data = [
69 | {
70 | id: 1,
71 | name: 'conversation 1',
72 | messages: [
73 | {
74 | role: 'user',
75 | content: "what's up ?",
76 | },
77 | {
78 | role: 'assistant',
79 | content: 'Hi',
80 | },
81 | ],
82 | },
83 | ] as ExportFormatV1;
84 | const obj = cleanData(data);
85 | expect(isLatestExportFormat(obj)).toBe(true);
86 | expect(obj).toEqual({
87 | version: 4,
88 | history: [
89 | {
90 | id: 1,
91 | name: 'conversation 1',
92 | messages: [
93 | {
94 | role: 'user',
95 | content: "what's up ?",
96 | },
97 | {
98 | role: 'assistant',
99 | content: 'Hi',
100 | },
101 | ],
102 | folderId: null,
103 | },
104 | ],
105 | folders: [],
106 | prompts: [],
107 | });
108 | });
109 | });
110 |
111 | describe('cleaning v2 data', () => {
112 | it('should return the latest format', () => {
113 | const data = {
114 | history: [
115 | {
116 | id: '1',
117 | name: 'conversation 1',
118 | messages: [
119 | {
120 | role: 'user',
121 | content: "what's up ?",
122 | },
123 | {
124 | role: 'assistant',
125 | content: 'Hi',
126 | },
127 | ],
128 | },
129 | ],
130 | folders: [
131 | {
132 | id: 1,
133 | name: 'folder 1',
134 | },
135 | ],
136 | } as ExportFormatV2;
137 | const obj = cleanData(data);
138 | expect(isLatestExportFormat(obj)).toBe(true);
139 | expect(obj).toEqual({
140 | version: 4,
141 | history: [
142 | {
143 | id: '1',
144 | name: 'conversation 1',
145 | messages: [
146 | {
147 | role: 'user',
148 | content: "what's up ?",
149 | },
150 | {
151 | role: 'assistant',
152 | content: 'Hi',
153 | },
154 | ],
155 | folderId: null,
156 | },
157 | ],
158 | folders: [
159 | {
160 | id: '1',
161 | name: 'folder 1',
162 | type: 'chat',
163 | },
164 | ],
165 | prompts: [],
166 | });
167 | });
168 | });
169 |
170 | describe('cleaning v4 data', () => {
171 | it('should return the latest format', () => {
172 | const data = {
173 | version: 4,
174 | history: [
175 | {
176 | id: '1',
177 | name: 'conversation 1',
178 | messages: [
179 | {
180 | role: 'user',
181 | content: "what's up ?",
182 | },
183 | {
184 | role: 'assistant',
185 | content: 'Hi',
186 | },
187 | ],
188 | folderId: null,
189 | },
190 | ],
191 | folders: [
192 | {
193 | id: '1',
194 | name: 'folder 1',
195 | type: 'chat',
196 | },
197 | ],
198 | } as ExportFormatV4;
199 |
200 | const obj = cleanData(data);
201 | expect(isLatestExportFormat(obj)).toBe(true);
202 | expect(obj).toEqual({
203 | version: 4,
204 | history: [
205 | {
206 | id: '1',
207 | name: 'conversation 1',
208 | messages: [
209 | {
210 | role: 'user',
211 | content: "what's up ?",
212 | },
213 | {
214 | role: 'assistant',
215 | content: 'Hi',
216 | },
217 | ],
218 | folderId: null,
219 | },
220 | ],
221 | folders: [
222 | {
223 | id: '1',
224 | name: 'folder 1',
225 | type: 'chat',
226 | },
227 | ]
228 | });
229 | });
230 | });
231 | });
232 |
--------------------------------------------------------------------------------
/components/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NVIDIA/NeMo-Agent-Toolkit-UI/9816cd9801e782324f48a0a77c299c78406f0919/components/.DS_Store
--------------------------------------------------------------------------------
/components/Avatar/AgentAvatar.tsx:
--------------------------------------------------------------------------------
1 | import { IconUserPentagon } from '@tabler/icons-react';
2 | import React from 'react';
3 | export const AgentAvatar = ({height = 7, width = 7}) => {
4 | return (
5 |
6 |
7 |
8 | )
9 | }
--------------------------------------------------------------------------------
/components/Avatar/BotAvatar.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const BotAvatar = ({height = 30, width = 30, src= ''}) => {
4 |
5 | const onError = (event: { target: { src: string; }; }) => {
6 | console.error('error loading bot avatar');
7 | event.target.src = `nvidia.jpg`;
8 | };
9 |
10 | return
18 | }
--------------------------------------------------------------------------------
/components/Avatar/SystemAvatar.tsx:
--------------------------------------------------------------------------------
1 | import { IconPasswordUser, IconUserPentagon } from '@tabler/icons-react';
2 | import React from 'react';
3 | export const SystemAgentAvatar = ({height = 7, width = 7}) => {
4 | return (
5 |
6 |
7 |
8 | )
9 | }
--------------------------------------------------------------------------------
/components/Avatar/UserAvatar.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { getInitials } from '@/utils/app/helper';
3 |
4 | export const UserAvatar = ({src = '', height = 30, width = 30}) => {
5 | const profilePicUrl = src || ``
6 |
7 | const onError = (event: { target: { src: string; }; }) => {
8 | const svg = `
9 |
10 |
11 | user
12 |
13 | `;
14 | event.target.src = `data:image/svg+xml;base64,${window.btoa(svg)}`;
15 | };
16 |
17 | return
26 |
27 | }
--------------------------------------------------------------------------------
/components/Buttons/SidebarActionButton/SidebarActionButton.tsx:
--------------------------------------------------------------------------------
1 | import { MouseEventHandler, ReactElement } from 'react';
2 |
3 | interface Props {
4 | handleClick: MouseEventHandler;
5 | children: ReactElement;
6 | }
7 |
8 | const SidebarActionButton = ({ handleClick, children }: Props) => (
9 |
13 | {children}
14 |
15 | );
16 |
17 | export default SidebarActionButton;
18 |
--------------------------------------------------------------------------------
/components/Buttons/SidebarActionButton/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './SidebarActionButton';
2 |
--------------------------------------------------------------------------------
/components/Chat/ChatHeader.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { env } from 'next-runtime-env'
4 |
5 | import React, { useContext, useState, useRef, useEffect } from 'react';
6 | import {
7 | IconArrowsSort,
8 | IconMobiledataOff,
9 | IconSun,
10 | IconMoonFilled,
11 | IconUserFilled,
12 | IconChevronLeft,
13 | IconChevronRight
14 | } from '@tabler/icons-react';
15 | import HomeContext from '@/pages/api/home/home.context';
16 | import { getWorkflowName } from '@/utils/app/helper';
17 |
18 | export const ChatHeader = ({ webSocketModeRef = {} }) => {
19 | const [isMenuOpen, setIsMenuOpen] = useState(false);
20 | const [isExpanded, setIsExpanded] = useState(env('NEXT_PUBLIC_RIGHT_MENU_OPEN') === 'true' || process?.env?.NEXT_PUBLIC_RIGHT_MENU_OPEN === 'true' ? true : false);
21 | const menuRef = useRef(null);
22 |
23 | const workflow = getWorkflowName()
24 |
25 | const {
26 | state: {
27 | chatHistory,
28 | webSocketMode,
29 | webSocketConnected,
30 | lightMode,
31 | selectedConversation
32 | },
33 | dispatch: homeDispatch,
34 | } = useContext(HomeContext);
35 |
36 |
37 | const handleLogin = () => {
38 | console.log('Login clicked');
39 | setIsMenuOpen(false);
40 | };
41 |
42 | useEffect(() => {
43 | const handleClickOutside = (event) => {
44 | if (menuRef.current && !menuRef.current.contains(event.target)) {
45 | setIsMenuOpen(false);
46 | }
47 | };
48 |
49 | document.addEventListener('mousedown', handleClickOutside);
50 | return () => document.removeEventListener('mousedown', handleClickOutside);
51 | }, []);
52 |
53 | return (
54 |
55 | {
56 | selectedConversation?.messages?.length > 0 ?
57 |
58 | {workflow}
59 |
60 | :
61 |
62 |
63 | Hi, I'm {workflow}
64 |
65 |
66 | How can I assist you today?
67 |
68 |
69 | }
70 |
71 | {/* Collapsible Menu */}
72 |
73 |
{
75 | setIsExpanded(!isExpanded)}
76 | }
77 | className="flex p-1 text-black dark:text-white transition-colors"
78 | >
79 | {isExpanded ? : }
80 |
81 |
82 |
83 | {/* Chat History Toggle */}
84 |
85 |
86 | Chat History
87 | {
89 | homeDispatch({
90 | field: 'chatHistory',
91 | value: !chatHistory,
92 | });
93 | }}
94 | className={`relative inline-flex h-5 w-10 items-center cursor-pointer rounded-full transition-colors duration-300 ease-in-out ${
95 | chatHistory ? 'bg-black dark:bg-[#76b900]' : 'bg-gray-200'
96 | }`}
97 | >
98 |
101 |
102 |
103 |
104 |
105 | {/* WebSocket Mode Toggle */}
106 |
107 |
108 |
109 | WebSocket{' '}
110 | {webSocketModeRef?.current && (
111 | webSocketConnected ? :
112 | )}
113 |
114 | {
116 | const newWebSocketMode = !webSocketModeRef.current;
117 | sessionStorage.setItem('webSocketMode', String(newWebSocketMode));
118 | webSocketModeRef.current = newWebSocketMode;
119 | homeDispatch({
120 | field: 'webSocketMode',
121 | value: !webSocketMode,
122 | });
123 | }}
124 | className={`relative inline-flex h-5 w-10 items-center cursor-pointer rounded-full transition-colors duration-300 ease-in-out ${
125 | webSocketModeRef.current ? 'bg-black dark:bg-[#76b900]' : 'bg-gray-200'
126 | }`}
127 | >
128 |
131 |
132 |
133 |
134 |
135 | {/* Theme Toggle Button */}
136 |
137 | {
139 | const newMode = lightMode === 'dark' ? 'light' : 'dark';
140 | homeDispatch({
141 | field: 'lightMode',
142 | value: newMode,
143 | });
144 | }}
145 | className="rounded-full flex items-center justify-center bg-none dark:bg-gray-700 transition-colors duration-300 focus:outline-none"
146 | >
147 | {lightMode === 'dark' ? (
148 |
149 | ) : (
150 |
151 | )}
152 |
153 |
154 |
155 | {/* User Icon with Dropdown Menu */}
156 |
157 |
setIsMenuOpen(!isMenuOpen)}
159 | className="flex items-center dark:text-white text-black cursor-pointer"
160 | >
161 |
162 |
163 | {isMenuOpen && (
164 |
165 |
166 |
170 | Login
171 |
172 |
173 |
174 | )}
175 |
176 |
177 |
178 |
179 | );
180 | };
--------------------------------------------------------------------------------
/components/Chat/ChatInteractionMessage.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { IconInfoCircle, IconX } from "@tabler/icons-react";
3 | import { useState } from "react";
4 | import { toast } from "react-hot-toast";
5 |
6 | export const InteractionModal = ({ isOpen, interactionMessage, onClose, onSubmit }) => {
7 | if (!isOpen || !interactionMessage) return null;
8 |
9 | const { content } = interactionMessage;
10 | const [userInput, setUserInput] = useState("");
11 | const [error, setError] = useState("");
12 |
13 | // Validation for Text Input
14 | const handleTextSubmit = () => {
15 | if (content?.required && !userInput.trim()) {
16 | setError("This field is required.");
17 | return;
18 | }
19 | setError("");
20 | onSubmit({interactionMessage, userResponse: userInput});
21 | onClose();
22 | };
23 |
24 | // Handle Choice Selection
25 | const handleChoiceSubmit = (option = '') => {
26 | if (content?.required && !option) {
27 | setError("Please select an option.");
28 | return;
29 | }
30 | setError("");
31 | onSubmit({interactionMessage, userResponse: option});
32 | onClose();
33 | };
34 |
35 | // Handle Radio Selection
36 | const handleRadioSubmit = () => {
37 | if (content?.required && !userInput) {
38 | setError("Please select an option.");
39 | return;
40 | }
41 | setError("");
42 | onSubmit({interactionMessage, userResponse: userInput});
43 | onClose();
44 | };
45 |
46 | if(content.input_type === 'notification'){
47 | toast.custom((t) => (
48 |
51 |
52 | {content?.text || 'No content found for this notification'}
53 | toast.dismiss(t.id)}
55 | className="text-slate-800 dark:bg-slate-800 dark:text-slate-100 ml-3 hover:bg-slate-300 rounded-full p-1"
56 | >
57 |
58 |
59 |
60 | ), {
61 | position: 'top-right',
62 | duration: Infinity,
63 | id: 'notification-toast'
64 | })
65 | return null
66 | }
67 |
68 | return (
69 |
70 |
71 |
{content?.text}
72 |
73 | {content.input_type === "text" && (
74 |
91 | )}
92 |
93 | {content.input_type === "binary_choice" && (
94 |
95 |
96 | {content.options.map((option) => (
97 | handleChoiceSubmit(option.value)}
101 | >
102 | {option.label}
103 |
104 | ))}
105 |
106 |
107 | )}
108 |
109 | {content.input_type === "radio" && (
110 |
111 |
112 | {content.options.map((option) => (
113 |
114 | setUserInput(option.value)}
121 | className="mr-2 text-[#76b900] focus:ring-[#76b900]"
122 | />
123 |
124 | {option.label}
125 | {/* {option.description && (
126 |
127 | {option?.description}
128 |
129 | )} */}
130 |
131 |
132 | ))}
133 |
134 | {error &&
{error}
}
135 |
136 |
137 | Cancel
138 |
139 |
143 | Submit
144 |
145 |
146 |
147 | )}
148 |
149 |
150 | );
151 | }
--------------------------------------------------------------------------------
/components/Chat/ChatLoader.tsx:
--------------------------------------------------------------------------------
1 | import { FC, useEffect, useState } from 'react';
2 | import { BotAvatar } from '@/components/Avatar/BotAvatar';
3 |
4 | interface Props {
5 | statusUpdateText: string;
6 | }
7 |
8 | export const ChatLoader: FC = ({ statusUpdateText = '' }) => {
9 | const config = {
10 | initialDelay: 500,
11 | delayMultiplier: 6000,
12 | statusMessages: [statusUpdateText],
13 | };
14 |
15 | const [currentMessage, setCurrentMessage] = useState(''); // Initialize with empty string
16 |
17 | useEffect(() => {
18 | const timers = config.statusMessages.map((message, index) => {
19 | const delay = index === 0 ? config.initialDelay : config.initialDelay + (index * config.delayMultiplier);
20 | return setTimeout(() => {
21 | setCurrentMessage(message);
22 | }, delay);
23 | });
24 |
25 | return () => {
26 | timers.forEach((timer) => clearTimeout(timer));
27 | };
28 | }, []);
29 |
30 | return (
31 |
35 |
36 |
37 |
38 |
39 |
40 | {/* Status Update Text with Green Blinking Caret */}
41 | {currentMessage}▍
42 |
43 |
44 |
45 | );
46 | };
--------------------------------------------------------------------------------
/components/Chat/ErrorMessageDiv.tsx:
--------------------------------------------------------------------------------
1 | import { IconCircleX } from '@tabler/icons-react';
2 | import { FC } from 'react';
3 |
4 | import { ErrorMessage } from '@/types/error';
5 |
6 | interface Props {
7 | error: ErrorMessage;
8 | }
9 |
10 | export const ErrorMessageDiv: FC = ({ error }) => {
11 | return (
12 |
13 |
14 |
15 |
16 |
{error.title}
17 | {error.messageLines.map((line, index) => (
18 |
19 | {' '}
20 | {line}{' '}
21 |
22 | ))}
23 |
24 | {error.code ? Code: {error.code} : ''}
25 |
26 |
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/components/Chat/MemoizedChatMessage.tsx:
--------------------------------------------------------------------------------
1 | import { FC, memo } from "react";
2 | import isEqual from "lodash/isEqual";
3 | import { ChatMessage, Props } from "./ChatMessage";
4 |
5 | export const MemoizedChatMessage: FC = memo(
6 | ChatMessage,
7 | (prevProps, nextProps) => {
8 | // componenent will render if new props are only different than previous props (to prevent unnecessary re-rendering)
9 | const shouldRender = isEqual(prevProps.message, nextProps.message);
10 | return shouldRender;
11 | }
12 | );
13 |
--------------------------------------------------------------------------------
/components/Chat/Regenerate.tsx:
--------------------------------------------------------------------------------
1 | import { IconRefresh } from '@tabler/icons-react';
2 | import { FC } from 'react';
3 |
4 | import { useTranslation } from 'next-i18next';
5 |
6 | interface Props {
7 | onRegenerate: () => void;
8 | }
9 |
10 | export const Regenerate: FC = ({ onRegenerate }) => {
11 | const { t } = useTranslation('chat');
12 | return (
13 |
14 |
15 | {t('Sorry, there was an error.')}
16 |
17 |
21 |
22 | {t('Regenerate response')}
23 |
24 |
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/components/Chatbar/Chatbar.context.tsx:
--------------------------------------------------------------------------------
1 | import { Dispatch, createContext } from 'react';
2 |
3 | import { ActionType } from '@/hooks/useCreateReducer';
4 |
5 | import { Conversation } from '@/types/chat';
6 | import { SupportedExportFormats } from '@/types/export';
7 |
8 | import { ChatbarInitialState } from './Chatbar.state';
9 |
10 | export interface ChatbarContextProps {
11 | state: ChatbarInitialState;
12 | dispatch: Dispatch>;
13 | handleDeleteConversation: (conversation: Conversation) => void;
14 | handleClearConversations: () => void;
15 | handleExportData: () => void;
16 | handleImportConversations: (data: SupportedExportFormats) => void;
17 | }
18 |
19 | const ChatbarContext = createContext(undefined!);
20 |
21 | export default ChatbarContext;
22 |
--------------------------------------------------------------------------------
/components/Chatbar/Chatbar.state.tsx:
--------------------------------------------------------------------------------
1 | import { Conversation } from '@/types/chat';
2 |
3 | export interface ChatbarInitialState {
4 | searchTerm: string;
5 | filteredConversations: Conversation[];
6 | }
7 |
8 | export const initialState: ChatbarInitialState = {
9 | searchTerm: '',
10 | filteredConversations: [],
11 | };
12 |
--------------------------------------------------------------------------------
/components/Chatbar/Chatbar.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useContext, useEffect } from 'react';
2 |
3 | import { useTranslation } from 'next-i18next';
4 |
5 | import { useCreateReducer } from '@/hooks/useCreateReducer';
6 |
7 | import { saveConversation, saveConversations } from '@/utils/app/conversation';
8 | import { saveFolders } from '@/utils/app/folders';
9 | import { exportData, importData } from '@/utils/app/importExport';
10 |
11 | import { Conversation } from '@/types/chat';
12 | import { LatestExportFormat, SupportedExportFormats } from '@/types/export';
13 |
14 | import HomeContext from '@/pages/api/home/home.context';
15 |
16 | import { ChatFolders } from './components/ChatFolders';
17 | import { ChatbarSettings } from './components/ChatbarSettings';
18 | import { Conversations } from './components/Conversations';
19 |
20 | import Sidebar from '../Sidebar';
21 | import ChatbarContext from './Chatbar.context';
22 | import { ChatbarInitialState, initialState } from './Chatbar.state';
23 |
24 | import { v4 as uuidv4 } from 'uuid';
25 |
26 | export const Chatbar = () => {
27 | const { t } = useTranslation('sidebar');
28 |
29 | const chatBarContextValue = useCreateReducer({
30 | initialState,
31 | });
32 |
33 | const {
34 | state: { conversations, showChatbar, folders },
35 | dispatch: homeDispatch,
36 | handleCreateFolder,
37 | handleNewConversation,
38 | handleUpdateConversation,
39 | } = useContext(HomeContext);
40 |
41 | const {
42 | state: { searchTerm, filteredConversations },
43 | dispatch: chatDispatch,
44 | } = chatBarContextValue;
45 |
46 | const handleExportData = () => {
47 | exportData();
48 | };
49 |
50 | const handleImportConversations = (data: SupportedExportFormats) => {
51 | const { history, folders, prompts }: LatestExportFormat = importData(data);
52 | homeDispatch({ field: 'conversations', value: history });
53 | homeDispatch({
54 | field: 'selectedConversation',
55 | value: history[history.length - 1],
56 | });
57 | homeDispatch({ field: 'folders', value: folders });
58 | homeDispatch({ field: 'prompts', value: prompts });
59 |
60 | window.location.reload();
61 | };
62 |
63 | const handleClearConversations = () => {
64 | homeDispatch({
65 | field: 'selectedConversation',
66 | value: {
67 | id: uuidv4(),
68 | name: t('New Conversation'),
69 | messages: [],
70 | folderId: null,
71 | },
72 | });
73 |
74 | homeDispatch({ field: 'conversations', value: [] });
75 |
76 | sessionStorage.removeItem('conversationHistory');
77 | sessionStorage.removeItem('selectedConversation');
78 |
79 | const updatedFolders = folders.filter((f) => f.type !== 'chat');
80 |
81 | homeDispatch({ field: 'folders', value: updatedFolders });
82 | saveFolders(updatedFolders);
83 | };
84 |
85 | const handleDeleteConversation = (conversation: Conversation) => {
86 | const updatedConversations = conversations.filter(
87 | (c) => c.id !== conversation.id,
88 | );
89 |
90 | homeDispatch({ field: 'conversations', value: updatedConversations });
91 | chatDispatch({ field: 'searchTerm', value: '' });
92 | saveConversations(updatedConversations);
93 |
94 | if (updatedConversations.length > 0) {
95 | homeDispatch({
96 | field: 'selectedConversation',
97 | value: updatedConversations[updatedConversations.length - 1],
98 | });
99 |
100 | saveConversation(updatedConversations[updatedConversations.length - 1]);
101 | } else {
102 | homeDispatch({
103 | field: 'selectedConversation',
104 | value: {
105 | id: uuidv4(),
106 | name: t('New Conversation'),
107 | messages: [],
108 | folderId: null,
109 | },
110 | });
111 |
112 | sessionStorage.removeItem('selectedConversation');
113 | }
114 | };
115 |
116 | const handleToggleChatbar = () => {
117 | homeDispatch({ field: 'showChatbar', value: !showChatbar });
118 | sessionStorage.setItem('showChatbar', JSON.stringify(!showChatbar));
119 | };
120 |
121 | const handleDrop = (e: any) => {
122 | if (e.dataTransfer) {
123 | const conversation = JSON.parse(e.dataTransfer.getData('conversation'));
124 | handleUpdateConversation(conversation, { key: 'folderId', value: 0 });
125 | chatDispatch({ field: 'searchTerm', value: '' });
126 | e.target.style.background = 'none';
127 | }
128 | };
129 |
130 | useEffect(() => {
131 | if (searchTerm) {
132 | chatDispatch({
133 | field: 'filteredConversations',
134 | value: conversations.filter((conversation) => {
135 | const searchable =
136 | conversation.name.toLocaleLowerCase() +
137 | ' ' +
138 | conversation.messages.map((message) => message.content).join(' ');
139 | return searchable.toLowerCase().includes(searchTerm.toLowerCase());
140 | }),
141 | });
142 | } else {
143 | chatDispatch({
144 | field: 'filteredConversations',
145 | value: conversations,
146 | });
147 | }
148 | }, [searchTerm, conversations, chatDispatch]);
149 |
150 | return (
151 |
160 |
161 | side={'left'}
162 | isOpen={showChatbar}
163 | addItemButtonTitle={t('New chat')}
164 | itemComponent={ }
165 | folderComponent={ }
166 | items={filteredConversations}
167 | searchTerm={searchTerm}
168 | handleSearchTerm={(searchTerm: string) =>
169 | chatDispatch({ field: 'searchTerm', value: searchTerm })
170 | }
171 | toggleOpen={handleToggleChatbar}
172 | handleCreateItem={handleNewConversation}
173 | handleCreateFolder={() => handleCreateFolder(t('New folder'), 'chat')}
174 | handleDrop={handleDrop}
175 | footerComponent={ }
176 | />
177 |
178 | );
179 | };
180 |
--------------------------------------------------------------------------------
/components/Chatbar/components/ChatFolders.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 |
3 | import { FolderInterface } from '@/types/folder';
4 |
5 | import HomeContext from '@/pages/api/home/home.context';
6 |
7 | import Folder from '@/components/Folder';
8 |
9 | import { ConversationComponent } from './Conversation';
10 |
11 | interface Props {
12 | searchTerm: string;
13 | }
14 |
15 | export const ChatFolders = ({ searchTerm }: Props) => {
16 | const {
17 | state: { folders, conversations },
18 | handleUpdateConversation,
19 | } = useContext(HomeContext);
20 |
21 | const handleDrop = (e: any, folder: FolderInterface) => {
22 | if (e.dataTransfer) {
23 | const conversation = JSON.parse(e.dataTransfer.getData('conversation'));
24 | handleUpdateConversation(conversation, {
25 | key: 'folderId',
26 | value: folder.id,
27 | });
28 | }
29 | };
30 |
31 | const ChatFolders = (currentFolder: FolderInterface) => {
32 | return (
33 | conversations &&
34 | conversations
35 | .filter((conversation) => conversation.folderId)
36 | .map((conversation, index) => {
37 | if (conversation.folderId === currentFolder.id) {
38 | return (
39 |
40 |
41 |
42 | );
43 | }
44 | })
45 | );
46 | };
47 |
48 | return (
49 |
50 | {folders
51 | .filter((folder) => folder.type === 'chat')
52 | .sort((a, b) => a.name.localeCompare(b.name))
53 | .map((folder, index) => (
54 |
61 | ))}
62 |
63 | );
64 | };
65 |
--------------------------------------------------------------------------------
/components/Chatbar/components/ChatbarSettings.tsx:
--------------------------------------------------------------------------------
1 | import { IconFileExport, IconSettings } from '@tabler/icons-react';
2 | import { useContext, useState } from 'react';
3 |
4 | import { useTranslation } from 'next-i18next';
5 |
6 | import HomeContext from '@/pages/api/home/home.context';
7 |
8 | import { SettingDialog } from '@/components/Settings/SettingDialog';
9 |
10 | import { Import } from '../../Settings/Import';
11 | import { SidebarButton } from '../../Sidebar/SidebarButton';
12 | import ChatbarContext from '../Chatbar.context';
13 | import { ClearConversations } from './ClearConversations';
14 |
15 | export const ChatbarSettings = () => {
16 | const { t } = useTranslation('sidebar');
17 | const [isSettingDialogOpen, setIsSettingDialog] = useState(false);
18 |
19 | const {
20 | state: {
21 | lightMode,
22 | conversations,
23 | },
24 | dispatch: homeDispatch,
25 | } = useContext(HomeContext);
26 |
27 | const {
28 | handleClearConversations,
29 | handleImportConversations,
30 | handleExportData,
31 | } = useContext(ChatbarContext);
32 |
33 | return (
34 |
35 | {conversations.length > 0 ? (
36 |
37 | ) : null}
38 |
39 |
40 |
41 | }
44 | onClick={() => handleExportData()}
45 | />
46 |
47 | }
50 | onClick={() => setIsSettingDialog(true)}
51 | />
52 |
53 | {
56 | setIsSettingDialog(false);
57 | }}
58 | />
59 |
60 | );
61 | };
62 |
--------------------------------------------------------------------------------
/components/Chatbar/components/ClearConversations.tsx:
--------------------------------------------------------------------------------
1 | import { IconCheck, IconTrash, IconX } from '@tabler/icons-react';
2 | import { FC, useState } from 'react';
3 |
4 | import { useTranslation } from 'next-i18next';
5 |
6 | import { SidebarButton } from '@/components/Sidebar/SidebarButton';
7 |
8 | interface Props {
9 | onClearConversations: () => void;
10 | }
11 |
12 | export const ClearConversations: FC = ({ onClearConversations }) => {
13 | const [isConfirming, setIsConfirming] = useState(false);
14 |
15 | const { t } = useTranslation('sidebar');
16 |
17 | const handleClearConversations = () => {
18 | onClearConversations();
19 | setIsConfirming(false);
20 | };
21 |
22 | return isConfirming ? (
23 |
24 |
25 |
26 |
27 | {t('Are you sure?')}
28 |
29 |
30 |
31 | {
35 | e.stopPropagation();
36 | handleClearConversations();
37 | }}
38 | />
39 |
40 | {
44 | e.stopPropagation();
45 | setIsConfirming(false);
46 | }}
47 | />
48 |
49 |
50 | ) : (
51 | }
54 | onClick={() => setIsConfirming(true)}
55 | />
56 | );
57 | };
58 |
--------------------------------------------------------------------------------
/components/Chatbar/components/Conversation.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | IconCheck,
3 | IconMessage,
4 | IconPencil,
5 | IconTrash,
6 | IconX,
7 | } from '@tabler/icons-react';
8 | import {
9 | DragEvent,
10 | KeyboardEvent,
11 | MouseEventHandler,
12 | useContext,
13 | useEffect,
14 | useState,
15 | } from 'react';
16 |
17 | import { Conversation } from '@/types/chat';
18 |
19 | import HomeContext from '@/pages/api/home/home.context';
20 |
21 | import SidebarActionButton from '@/components/Buttons/SidebarActionButton';
22 | import ChatbarContext from '@/components/Chatbar/Chatbar.context';
23 |
24 | interface Props {
25 | conversation: Conversation;
26 | }
27 |
28 | export const ConversationComponent = ({ conversation }: Props) => {
29 | const {
30 | state: { selectedConversation, messageIsStreaming },
31 | handleSelectConversation,
32 | handleUpdateConversation,
33 | } = useContext(HomeContext);
34 |
35 | const { handleDeleteConversation } = useContext(ChatbarContext);
36 |
37 | const [isDeleting, setIsDeleting] = useState(false);
38 | const [isRenaming, setIsRenaming] = useState(false);
39 | const [renameValue, setRenameValue] = useState('');
40 |
41 | const handleEnterDown = (e: KeyboardEvent) => {
42 | if (e.key === 'Enter') {
43 | e.preventDefault();
44 | selectedConversation && handleRename(selectedConversation);
45 | }
46 | };
47 |
48 | const handleDragStart = (
49 | e: DragEvent,
50 | conversation: Conversation,
51 | ) => {
52 | if (e.dataTransfer) {
53 | e.dataTransfer.setData('conversation', JSON.stringify(conversation));
54 | }
55 | };
56 |
57 | const handleRename = (conversation: Conversation) => {
58 | if (renameValue.trim().length > 0) {
59 | handleUpdateConversation(conversation, {
60 | key: 'name',
61 | value: renameValue,
62 | });
63 | setRenameValue('');
64 | setIsRenaming(false);
65 | }
66 | };
67 |
68 | const handleConfirm: MouseEventHandler = (e) => {
69 | e.stopPropagation();
70 | if (isDeleting) {
71 | handleDeleteConversation(conversation);
72 | } else if (isRenaming) {
73 | handleRename(conversation);
74 | }
75 | setIsDeleting(false);
76 | setIsRenaming(false);
77 | };
78 |
79 | const handleCancel: MouseEventHandler = (e) => {
80 | e.stopPropagation();
81 | setIsDeleting(false);
82 | setIsRenaming(false);
83 | };
84 |
85 | const handleOpenRenameModal: MouseEventHandler = (e) => {
86 | e.stopPropagation();
87 | setIsRenaming(true);
88 | selectedConversation && setRenameValue(selectedConversation.name);
89 | };
90 | const handleOpenDeleteModal: MouseEventHandler = (e) => {
91 | e.stopPropagation();
92 | setIsDeleting(true);
93 | };
94 |
95 | useEffect(() => {
96 | if (isRenaming) {
97 | setIsDeleting(false);
98 | } else if (isDeleting) {
99 | setIsRenaming(false);
100 | }
101 | }, [isRenaming, isDeleting]);
102 |
103 | return (
104 |
105 | {isRenaming && selectedConversation?.id === conversation.id ? (
106 |
107 |
108 | setRenameValue(e.target.value)}
113 | onKeyDown={handleEnterDown}
114 | autoFocus
115 | />
116 |
117 | ) : (
118 |
handleSelectConversation(conversation)}
127 | disabled={messageIsStreaming}
128 | draggable="true"
129 | onDragStart={(e) => handleDragStart(e, conversation)}
130 | >
131 |
132 |
137 | {conversation.name}
138 |
139 |
140 | )}
141 |
142 | {(isDeleting || isRenaming) &&
143 | selectedConversation?.id === conversation.id && (
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 | )}
153 |
154 | {selectedConversation?.id === conversation.id &&
155 | !isDeleting &&
156 | !isRenaming && (
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 | )}
166 |
167 | );
168 | };
169 |
--------------------------------------------------------------------------------
/components/Chatbar/components/Conversations.tsx:
--------------------------------------------------------------------------------
1 | import { Conversation } from '@/types/chat';
2 |
3 | import { ConversationComponent } from './Conversation';
4 |
5 | interface Props {
6 | conversations: Conversation[];
7 | }
8 |
9 | export const Conversations = ({ conversations }: Props) => {
10 | return (
11 |
12 | {conversations
13 | .filter((conversation) => !conversation.folderId)
14 | .slice()
15 | .reverse()
16 | .map((conversation, index) => (
17 |
18 | ))}
19 |
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/components/Folder/Folder.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | IconCaretDown,
3 | IconCaretRight,
4 | IconCheck,
5 | IconPencil,
6 | IconTrash,
7 | IconX,
8 | } from '@tabler/icons-react';
9 | import {
10 | KeyboardEvent,
11 | ReactElement,
12 | useContext,
13 | useEffect,
14 | useState,
15 | } from 'react';
16 |
17 | import { FolderInterface } from '@/types/folder';
18 |
19 | import HomeContext from '@/pages/api/home/home.context';
20 |
21 | import SidebarActionButton from '@/components/Buttons/SidebarActionButton';
22 |
23 | interface Props {
24 | currentFolder: FolderInterface;
25 | searchTerm: string;
26 | handleDrop: (e: any, folder: FolderInterface) => void;
27 | folderComponent: (ReactElement | undefined)[];
28 | }
29 |
30 | const Folder = ({
31 | currentFolder,
32 | searchTerm,
33 | handleDrop,
34 | folderComponent,
35 | }: Props) => {
36 | const { handleDeleteFolder, handleUpdateFolder } = useContext(HomeContext);
37 |
38 | const [isDeleting, setIsDeleting] = useState(false);
39 | const [isRenaming, setIsRenaming] = useState(false);
40 | const [renameValue, setRenameValue] = useState('');
41 | const [isOpen, setIsOpen] = useState(false);
42 |
43 | const handleEnterDown = (e: KeyboardEvent) => {
44 | if (e.key === 'Enter') {
45 | e.preventDefault();
46 | handleRename();
47 | }
48 | };
49 |
50 | const handleRename = () => {
51 | handleUpdateFolder(currentFolder.id, renameValue);
52 | setRenameValue('');
53 | setIsRenaming(false);
54 | };
55 |
56 | const dropHandler = (e: any) => {
57 | if (e.dataTransfer) {
58 | setIsOpen(true);
59 |
60 | handleDrop(e, currentFolder);
61 |
62 | e.target.style.background = 'none';
63 | }
64 | };
65 |
66 | const allowDrop = (e: any) => {
67 | e.preventDefault();
68 | };
69 |
70 | const highlightDrop = (e: any) => {
71 | e.target.style.background = '#343541';
72 | };
73 |
74 | const removeHighlight = (e: any) => {
75 | e.target.style.background = 'none';
76 | };
77 |
78 | useEffect(() => {
79 | if (isRenaming) {
80 | setIsDeleting(false);
81 | } else if (isDeleting) {
82 | setIsRenaming(false);
83 | }
84 | }, [isRenaming, isDeleting]);
85 |
86 | useEffect(() => {
87 | if (searchTerm) {
88 | setIsOpen(true);
89 | } else {
90 | setIsOpen(false);
91 | }
92 | }, [searchTerm]);
93 |
94 | return (
95 | <>
96 |
97 | {isRenaming ? (
98 |
99 | {isOpen ? (
100 |
101 | ) : (
102 |
103 | )}
104 | setRenameValue(e.target.value)}
109 | onKeyDown={handleEnterDown}
110 | autoFocus
111 | />
112 |
113 | ) : (
114 |
setIsOpen(!isOpen)}
117 | onDrop={(e) => dropHandler(e)}
118 | onDragOver={allowDrop}
119 | onDragEnter={highlightDrop}
120 | onDragLeave={removeHighlight}
121 | >
122 | {isOpen ? (
123 |
124 | ) : (
125 |
126 | )}
127 |
128 |
129 | {currentFolder.name}
130 |
131 |
132 | )}
133 |
134 | {(isDeleting || isRenaming) && (
135 |
136 | {
138 | e.stopPropagation();
139 |
140 | if (isDeleting) {
141 | handleDeleteFolder(currentFolder.id);
142 | } else if (isRenaming) {
143 | handleRename();
144 | }
145 |
146 | setIsDeleting(false);
147 | setIsRenaming(false);
148 | }}
149 | >
150 |
151 |
152 | {
154 | e.stopPropagation();
155 | setIsDeleting(false);
156 | setIsRenaming(false);
157 | }}
158 | >
159 |
160 |
161 |
162 | )}
163 |
164 | {!isDeleting && !isRenaming && (
165 |
166 | {
168 | e.stopPropagation();
169 | setIsRenaming(true);
170 | setRenameValue(currentFolder.name);
171 | }}
172 | >
173 |
174 |
175 | {
177 | e.stopPropagation();
178 | setIsDeleting(true);
179 | }}
180 | >
181 |
182 |
183 |
184 | )}
185 |
186 |
187 | {isOpen ? folderComponent : null}
188 | >
189 | );
190 | };
191 |
192 | export default Folder;
193 |
--------------------------------------------------------------------------------
/components/Folder/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Folder';
2 |
--------------------------------------------------------------------------------
/components/Markdown/Chart.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from "react";
2 | import {
3 | BarChart, Bar, LineChart, Line, PieChart, Pie, AreaChart, Area,
4 | RadarChart, Radar, PolarGrid, PolarAngleAxis, PolarRadiusAxis,
5 | ScatterChart, Scatter, CartesianGrid, XAxis, YAxis, Tooltip, Legend, ResponsiveContainer,
6 | ComposedChart, Cell
7 | } from 'recharts';
8 | import dynamic from 'next/dynamic'; // Import dynamic from Next.js
9 | import HomeContext from "@/pages/api/home/home.context";
10 | import * as htmlToImage from 'html-to-image'; // Import html-to-image for generating images
11 | import { IconDownload } from "@tabler/icons-react";
12 | import toast from "react-hot-toast";
13 |
14 | // Dynamically import the ForceGraph2D component with SSR disabled
15 | const ForceGraph2D = dynamic(() => import('react-force-graph-2d'), { ssr: false });
16 |
17 | // Utility function to generate a random color
18 | const getRandomColor = () => {
19 | const letters = '0123456789ABCDEF';
20 | let color = '#';
21 | for (let i = 0; i < 6; i++) {
22 | color += letters[Math.floor(Math.random() * 16)];
23 | }
24 | return color;
25 | };
26 |
27 | const Chart = (props: any) => {
28 | const data = props?.payload;
29 | const {
30 | Label = '',
31 | ChartType = '',
32 | Data = [],
33 | XAxisKey = '',
34 | YAxisKey = '',
35 | ValueKey = '',
36 | NameKey = '',
37 | PolarAngleKey = '',
38 | PolarValueKey = '',
39 | BarKey = '',
40 | LineKey = '',
41 | Nodes = [],
42 | Links = []
43 | } = data;
44 |
45 | const {
46 | state: { selectedConversation, conversations },
47 | dispatch,
48 | } = useContext(HomeContext);
49 |
50 | const colors = {
51 | fill: '#76b900',
52 | stroke: 'black',
53 | };
54 |
55 | const handleDownload = async () => {
56 | try {
57 | const chartElement = document.getElementById(`chart-${Label}`);
58 | if (chartElement) {
59 | console.log('Generating image to download...');
60 | const chartBackground = chartElement.style.background;
61 | // Set the chart background to white before capturing the image
62 | chartElement.style.background = 'white';
63 | // Capture the image
64 | const dataUrl = await htmlToImage.toPng(chartElement);
65 | const link = document.createElement('a');
66 | link.href = dataUrl;
67 | link.download = `${Label}-${ChartType}.png`;
68 | link.click();
69 | // Reset the chart background
70 | chartElement.style.background = chartBackground;
71 | console.log('Image downloaded successfully.');
72 | toast.success('Downloaded successfully.');
73 | }
74 | } catch (error) {
75 | console.error('Error generating download image:', error);
76 | }
77 | };
78 |
79 |
80 | const renderChart = () => {
81 | switch (ChartType) {
82 | case 'BarChart':
83 | return (
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | );
95 |
96 | case 'LineChart':
97 | return (
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 | );
109 |
110 | case 'PieChart':
111 | return (
112 |
113 |
114 |
115 |
116 |
123 | {Data.map((entry, index) => (
124 | |
125 | ))}
126 |
127 |
128 |
129 | );
130 |
131 | case 'AreaChart':
132 | return (
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 | );
144 |
145 | case 'RadarChart':
146 | return (
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 | );
157 |
158 | case 'ScatterChart':
159 | return (
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 | );
171 |
172 | case 'ComposedChart':
173 | return (
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 | );
186 |
187 | case 'GraphPlot':
188 | return (
189 |
190 | ({ id: node.id, name: node.label })),
194 | links: Links.map((link: any) => ({
195 | source: link.source,
196 | target: link.target,
197 | label: link.label
198 | }))
199 | }}
200 | nodeLabel="name"
201 | linkLabel="label"
202 | nodeAutoColorBy="id"
203 | width={window.innerWidth * 0.9} // Adjust width to fit container
204 | height={500} // Set height to fit container
205 | // zoom={0.5} // Set zoom level (e.g., 2 for zoomed in)
206 | />
207 |
208 | );
209 |
210 | default:
211 | return No chart type found
;
212 | }
213 | };
214 |
215 | return (
216 |
217 |
218 |
219 |
{Label}
220 | {renderChart()}
221 |
222 |
223 | );
224 | };
225 |
226 | export default Chart;
227 |
--------------------------------------------------------------------------------
/components/Markdown/CodeBlock.tsx:
--------------------------------------------------------------------------------
1 | import { IconCheck, IconClipboard, IconDownload } from '@tabler/icons-react';
2 | import { FC, memo, MouseEvent, useState } from 'react';
3 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
4 | import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
5 |
6 | import { useTranslation } from 'next-i18next';
7 |
8 | import {
9 | generateRandomString,
10 | programmingLanguages,
11 | } from '@/utils/app/codeblock';
12 |
13 | interface Props {
14 | language: string;
15 | value: string;
16 | }
17 |
18 | export const CodeBlock: FC = memo(({ language, value }) => {
19 | const { t } = useTranslation('markdown');
20 | const [isCopied, setIsCopied] = useState(false);
21 |
22 | // Ensure value is a valid JSON string
23 | if (language === 'json') {
24 | try {
25 | value = value.replaceAll("'", '"');
26 | } catch (error) {
27 | console.log(error);
28 | }
29 | }
30 |
31 | const formattedValue = (() => {
32 | try {
33 | return JSON.stringify(JSON.parse(value), null, 2);
34 | } catch {
35 | return value; // Return the original value if parsing fails
36 | }
37 | })()
38 |
39 |
40 | const copyToClipboard = (e) => {
41 | e?.preventDefault();
42 | e?.stopPropagation();
43 | if (!navigator.clipboard || !navigator.clipboard.writeText) {
44 | return;
45 | }
46 |
47 | navigator.clipboard.writeText(formattedValue).then(() => {
48 | setIsCopied(true);
49 |
50 | setTimeout(() => {
51 | setIsCopied(false);
52 | }, 2000);
53 | });
54 | };
55 |
56 | const downloadAsFile = (e) => {
57 | e?.preventDefault();
58 | e?.stopPropagation();
59 | const fileExtension = programmingLanguages[language] || '.file';
60 | const suggestedFileName = `file-${generateRandomString(
61 | 3,
62 | true,
63 | )}${fileExtension}`;
64 | // const fileName = window.prompt(
65 | // t('Enter file name') || '',
66 | // suggestedFileName,
67 | // );
68 |
69 | if (!suggestedFileName) {
70 | return; // User pressed cancel on prompt
71 | }
72 |
73 | const blob = new Blob([formattedValue], { type: 'text/plain' });
74 | const url = URL.createObjectURL(blob);
75 | const link = document.createElement('a');
76 | link.download = suggestedFileName;
77 | link.href = url;
78 | link.style.display = 'none';
79 | document.body.appendChild(link);
80 | link.click();
81 | document.body.removeChild(link);
82 | URL.revokeObjectURL(url);
83 | };
84 |
85 | return (
86 |
87 |
88 |
{language}
89 |
90 |
91 | copyToClipboard(e)}
94 | >
95 | {isCopied ? : }
96 | {isCopied ? t('Copied!') : t('Copy code')}
97 |
98 | downloadAsFile(e)}
101 | >
102 |
103 |
104 |
105 |
106 |
123 | {formattedValue}
124 |
125 |
126 | );
127 | });
128 | CodeBlock.displayName = 'CodeBlock';
129 |
--------------------------------------------------------------------------------
/components/Markdown/CustomComponents.tsx:
--------------------------------------------------------------------------------
1 |
2 | import { memo, useMemo } from 'react';
3 | import { isEqual } from 'lodash';
4 | import { CodeBlock } from '@/components/Markdown/CodeBlock';
5 | import Chart from '@/components/Markdown/Chart';
6 | import { CustomDetails } from '@/components/Markdown/CustomDetails';
7 | import { CustomSummary } from '@/components/Markdown/CustomSummary';
8 | import { Video } from '@/components/Markdown/Video';
9 | import { Image } from '@/components/Markdown/Image';
10 |
11 |
12 | export const getReactMarkDownCustomComponents = (messageIndex = 0, messageId = '') => {
13 | return useMemo(() => ({
14 | code: memo(
15 | ({ node, inline, className, children, ...props }: { children: React.ReactNode; [key: string]: any }) => {
16 | // if (children?.length) {
17 | // if (children[0] === '▍') {
18 | // return ▍ ;
19 | // }
20 | // children[0] = children.length > 0 ? (children[0] as string)?.replace("`▍`", "▍") : '';
21 | // }
22 |
23 | const match = /language-(\w+)/.exec(className || '');
24 |
25 | return 1 && match[1]) || ''}
28 | value={String(children).replace(/\n$/, '')}
29 | {...props}
30 | />
31 | },
32 | (prevProps, nextProps) => {
33 | return isEqual(prevProps.children, nextProps.children);
34 | }
35 | ),
36 |
37 | chart: memo(({ children }) => {
38 | try {
39 | const payload = JSON.parse(children[0].replaceAll("\n", ""));
40 | return payload ? : null;
41 | } catch (error) {
42 | console.error(error);
43 | return null;
44 | }
45 | }, (prevProps, nextProps) => isEqual(prevProps.children, nextProps.children)),
46 |
47 | table: memo(({ children }) => (
48 |
51 | ), (prevProps, nextProps) => isEqual(prevProps.children, nextProps.children)),
52 |
53 | th: memo(({ children }) => (
54 |
55 | {children}
56 |
57 | ), (prevProps, nextProps) => isEqual(prevProps.children, nextProps.children)),
58 |
59 | td: memo(({ children }) => (
60 |
61 | {children}
62 |
63 | ), (prevProps, nextProps) => isEqual(prevProps.children, nextProps.children)),
64 |
65 | a: memo(({ href, children, ...props }) => (
66 |
67 | {children}
68 |
69 | ), (prevProps, nextProps) => isEqual(prevProps.children, nextProps.children)),
70 |
71 | li: memo(({ children, ...props }) => (
72 |
73 | {children}
74 |
75 | ), (prevProps, nextProps) => isEqual(prevProps.children, nextProps.children)),
76 |
77 | sup: memo(({ children, ...props }) => {
78 | const validContent = Array.isArray(children)
79 | ? children.filter(child => typeof child === 'string' && child.trim() && child.trim() !== ",").join("")
80 | : typeof children === 'string' && children.trim() && children.trim() !== "," ? children : null;
81 |
82 | return validContent ? (
83 |
93 | {validContent}
94 |
95 | ) : null;
96 | }, (prevProps, nextProps) => isEqual(prevProps.children, nextProps.children)),
97 |
98 | p: memo(({ children, ...props }: { children: React.ReactNode; [key: string]: any }) => {
99 | return {children}
;
100 | }, (prevProps, nextProps) => {
101 | return isEqual(prevProps.children, nextProps.children);
102 | }),
103 | img: memo((props) => , (prevProps, nextProps) => isEqual(prevProps, nextProps)),
104 | video: memo((props) => , (prevProps, nextProps) => isEqual(prevProps, nextProps)),
105 | details: memo((props) => , (prevProps, nextProps) => isEqual(prevProps, nextProps)),
106 | summary: memo((props) => , (prevProps, nextProps) => isEqual(prevProps, nextProps))
107 | }),[messageIndex, messageId]);
108 | };
109 |
--------------------------------------------------------------------------------
/components/Markdown/CustomDetails.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useContext, useEffect, useMemo, useRef, useState } from "react";
4 | import HomeContext from "@/pages/api/home/home.context";
5 | import { IconChevronCompactDown } from "@tabler/icons-react";
6 | import { fetchLastMessage } from "@/utils/app/helper";
7 |
8 | export const CustomDetails = ({ children, id, messageIndex, index}) => {
9 |
10 | let parsedIndex = index
11 | try {
12 | // index === -1 for top level
13 | parsedIndex = parseInt(index)
14 | }
15 | catch (error) {
16 | console.log('error - parsing index')
17 | }
18 |
19 | const {
20 | state,
21 | } = useContext(HomeContext);
22 |
23 | // Memoize only the values used in rendering to prevent unnecessary re-renders
24 | const { messageIsStreaming, selectedConversation, expandIntermediateSteps, autoScroll } = useMemo(() => ({
25 | messageIsStreaming: state?.messageIsStreaming,
26 | selectedConversation: state?.selectedConversation,
27 | expandIntermediateSteps: state?.expandIntermediateSteps,
28 | autoScroll: state?.autoScroll
29 | }), [state?.messageIsStreaming, state?.selectedConversation, state?.expandIntermediateSteps, state?.autoScroll]);
30 |
31 | const numberTotalMessages = selectedConversation?.messages?.length || 0;
32 | const lastAssistantMessage = fetchLastMessage({messages: selectedConversation?.messages, role: 'assistant'})
33 | const numberIntermediateMessages = lastAssistantMessage?.intermediateSteps?.length || 0
34 | const isLastMessage = messageIndex === numberTotalMessages - 1;
35 | const isLastIntermediateMessage = parsedIndex === numberIntermediateMessages - 1;
36 |
37 | const shouldOpen = () => {
38 | let isOpen = false
39 | const savedState = sessionStorage.getItem(`details-${id}`);
40 |
41 | // user saved state by toggling
42 | if(savedState) {
43 | isOpen = savedState === "true"
44 | }
45 |
46 | // expand if steps setting is set to true
47 | // expand for top level
48 | // expand for last intermediate message while streaming, it streaming is completed then close it
49 | else {
50 | isOpen = (expandIntermediateSteps && isLastMessage) || parsedIndex === -1 || (isLastMessage && isLastIntermediateMessage && messageIsStreaming)
51 | }
52 | return isOpen
53 | }
54 |
55 | // Initialize the open state based on sessionStorage or default from context
56 | const [isOpen, setIsOpen] = useState(shouldOpen());
57 | const detailsRef = useRef(null);
58 |
59 | useEffect(() => {
60 | setIsOpen(shouldOpen());
61 | autoScroll && detailsRef?.current?.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "nearest" });
62 | }, [isLastIntermediateMessage, messageIsStreaming]);
63 |
64 |
65 | // Handle manual toggling (optional if you want more control)
66 | const handleToggle = () => {
67 | setIsOpen((prev) => {
68 | sessionStorage.setItem(`details-${id}`, !prev);
69 | return !prev
70 | })
71 | };
72 |
73 | return (
74 | <>
75 | {
86 | e.preventDefault(); // Prevent default toggle if needed
87 | e.stopPropagation(); // Prevent event from bubbling to parent
88 | handleToggle();
89 | }}
90 | >
91 | {children}
92 |
93 |
94 | {isLastMessage && messageIsStreaming && parsedIndex === -1 && (
95 |
100 | )}
101 |
102 | >
103 | );
104 | };
105 |
--------------------------------------------------------------------------------
/components/Markdown/CustomSummary.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useState, useEffect } from 'react';
3 | import { IconCheck, IconCpu, IconTool, IconLoader, IconChevronDown, IconChevronUp } from "@tabler/icons-react"; // Import IconLoader for the loading state
4 |
5 | // Custom summary with additional props
6 | export const CustomSummary = ({ children, id }) => {
7 | const [isLoading, setIsLoading] = useState(true)
8 | const [checkOpen, setCheckOpen] = useState(false)
9 |
10 | const shouldOpen = () => {
11 | const savedState = sessionStorage.getItem(`details-${id}`)
12 | const open = savedState === "true"
13 | return open
14 | }
15 |
16 | // Simulate an artificial delay of 1 second
17 | useEffect(() => {
18 | const timer = setTimeout(() => {
19 | setIsLoading(false); // After 1 second, change the state to stop loading
20 | }, 10);
21 |
22 | // Cleanup the timer on component unmount
23 | return () => clearTimeout(timer);
24 | }, []);
25 |
26 | return (
27 | {
40 | e.preventDefault();
41 | setCheckOpen(!checkOpen);
42 | }}
43 | >
44 |
45 | {children?.toString().toLowerCase()?.includes('tool') ? (
46 |
47 | ) : (
48 |
49 | )}
50 | {children}
51 |
52 |
53 | {/* Right-side icons */}
54 |
55 | {isLoading ? (
56 |
57 | ) :
58 | //
59 | null
60 | }
61 | {shouldOpen() ? (
62 |
63 | ) : (
64 |
65 | )}
66 |
67 |
68 | );
69 | };
70 |
--------------------------------------------------------------------------------
/components/Markdown/Image.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useMemo, useRef, useState, useCallback } from "react";
2 | import Loading from "./Loading";
3 | import { IconExclamationCircle, IconMaximize, IconX } from "@tabler/icons-react";
4 |
5 | export const Image = memo(({ src, alt, ...props }) => {
6 | const imgRef = useRef(null);
7 | const [error, setError] = useState(false);
8 | const [isFullscreen, setIsFullscreen] = useState(false);
9 |
10 | const handleImageError = () => {
11 | console.error(`Image failed to load: ${src}`);
12 | setError(true);
13 | };
14 |
15 | const toggleFullscreen = useCallback(() => {
16 | setIsFullscreen((prev) => !prev);
17 | }, []);
18 |
19 | const imageElement = useMemo(() => {
20 | if (src === "loading") {
21 | return ;
22 | }
23 |
24 | return (
25 | <>
26 | {/* Image Container */}
27 |
28 | {error ? (
29 |
30 |
31 |
32 | Failed to load image with src: {src.slice(0, 50) + (src.length > 50 ? "..." : "")}
33 |
34 |
35 | ) : (
36 |
37 | {/* Image */}
38 |
47 | {/* Fullscreen Mode */}
48 | {isFullscreen && !error && (
49 |
53 |
54 |
59 |
60 |
61 | )}
62 |
63 | )}
64 |
65 | >
66 | );
67 | }, [src, alt, error, isFullscreen, toggleFullscreen]);
68 |
69 | return imageElement;
70 | }, (prevProps, nextProps) => prevProps.src === nextProps.src);
71 |
--------------------------------------------------------------------------------
/components/Markdown/Loading.tsx:
--------------------------------------------------------------------------------
1 | import { IconLoader } from '@tabler/icons-react';
2 | import React from 'react';
3 |
4 | const Loading = ({ message = "Loading", type = 'text' }) => {
5 | return (
6 | <>
7 | { type === 'text'
8 | ?
9 |
10 |
11 |
18 |
22 |
26 |
27 | {message &&
{message} }
28 |
{message}
29 |
30 |
31 | :
32 |
33 |
34 | Loading
35 |
40 |
41 |
42 | }
43 | >
44 | );
45 | };
46 |
47 | export default Loading;
--------------------------------------------------------------------------------
/components/Markdown/MemoizedReactMarkdown.tsx:
--------------------------------------------------------------------------------
1 | import { FC, memo } from 'react';
2 | import ReactMarkdown, { Options } from 'react-markdown';
3 |
4 | export const MemoizedReactMarkdown: FC = memo(
5 | (props) => ,
6 | (prevProps, nextProps) => (
7 | prevProps.children === nextProps.children
8 | )
9 | );
10 |
--------------------------------------------------------------------------------
/components/Markdown/Video.tsx:
--------------------------------------------------------------------------------
1 | // First, define the Video component at module level
2 | 'use client'
3 |
4 | import { memo, useMemo, useRef } from "react";
5 | import Loading from "@/components/Markdown/Loading";
6 |
7 | export const Video = memo(({
8 | src,
9 | controls = true,
10 | muted = false,
11 | ...props
12 | }) => {
13 | // Use ref to maintain stable reference for video element
14 | const videoRef = useRef(null);
15 |
16 | // Memoize the video element to prevent re-renders from context changes
17 | const videoElement = useMemo(() => {
18 | if (src === 'loading') {
19 | return ;
20 | }
21 |
22 | return (
23 |
34 | Your browser does not support the video tag.
35 |
36 | );
37 | }, [src, controls, muted]); // Only dependencies that should cause a re-render
38 |
39 | return videoElement;
40 | }, (prevProps, nextProps) => {
41 | return prevProps.src === nextProps.src;
42 | });
--------------------------------------------------------------------------------
/components/Mobile/Navbar.tsx:
--------------------------------------------------------------------------------
1 | import { IconPlus } from '@tabler/icons-react';
2 | import { FC } from 'react';
3 |
4 | import { Conversation } from '@/types/chat';
5 |
6 | interface Props {
7 | selectedConversation: Conversation;
8 | onNewConversation: () => void;
9 | }
10 |
11 | export const Navbar: FC = ({
12 | selectedConversation,
13 | onNewConversation,
14 | }) => {
15 | return (
16 |
17 |
18 |
19 |
20 | {selectedConversation.name}
21 |
22 |
23 |
27 |
28 | );
29 | };
30 |
--------------------------------------------------------------------------------
/components/Search/Search.tsx:
--------------------------------------------------------------------------------
1 | import { IconX } from '@tabler/icons-react';
2 | import { FC } from 'react';
3 |
4 | import { useTranslation } from 'next-i18next';
5 |
6 | interface Props {
7 | placeholder: string;
8 | searchTerm: string;
9 | onSearch: (searchTerm: string) => void;
10 | }
11 | const Search: FC = ({ placeholder, searchTerm, onSearch }) => {
12 | const { t } = useTranslation('sidebar');
13 |
14 | const handleSearchChange = (e: React.ChangeEvent) => {
15 | onSearch(e.target.value);
16 | };
17 |
18 | const clearSearch = () => {
19 | onSearch('');
20 | };
21 |
22 | return (
23 |
24 |
31 |
32 | {searchTerm && (
33 |
38 | )}
39 |
40 | );
41 | };
42 |
43 | export default Search;
44 |
--------------------------------------------------------------------------------
/components/Search/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Search';
2 |
--------------------------------------------------------------------------------
/components/Settings/Import.tsx:
--------------------------------------------------------------------------------
1 | import { IconFileImport } from '@tabler/icons-react';
2 | import { FC } from 'react';
3 |
4 | import { useTranslation } from 'next-i18next';
5 |
6 | import { SupportedExportFormats } from '@/types/export';
7 |
8 | import { SidebarButton } from '../Sidebar/SidebarButton';
9 |
10 | interface Props {
11 | onImport: (data: SupportedExportFormats) => void;
12 | }
13 |
14 | export const Import: FC = ({ onImport }) => {
15 | const { t } = useTranslation('sidebar');
16 | return (
17 | <>
18 | {
25 | if (!e.target.files?.length) return;
26 |
27 | const file = e.target.files[0];
28 | const reader = new FileReader();
29 | reader.onload = (e) => {
30 | let json = JSON.parse(e.target?.result as string);
31 | onImport(json);
32 | };
33 | reader.readAsText(file);
34 | }}
35 | />
36 |
37 | }
40 | onClick={() => {
41 | const importFile = document.querySelector(
42 | '#import-file',
43 | ) as HTMLInputElement;
44 | if (importFile) {
45 | importFile.click();
46 | }
47 | }}
48 | />
49 | >
50 | );
51 | };
52 |
--------------------------------------------------------------------------------
/components/Settings/SettingDialog.tsx:
--------------------------------------------------------------------------------
1 | import { FC, useContext, useEffect, useRef, useState } from 'react';
2 | import { useTranslation } from 'next-i18next';
3 | import HomeContext from '@/pages/api/home/home.context';
4 | import toast from 'react-hot-toast';
5 |
6 | interface Props {
7 | open: boolean;
8 | onClose: () => void;
9 | }
10 |
11 | export const SettingDialog: FC = ({ open, onClose }) => {
12 | const { t } = useTranslation('settings');
13 | const modalRef = useRef(null);
14 | const {
15 | state: { lightMode, chatCompletionURL, webSocketURL, webSocketSchema: schema, expandIntermediateSteps, intermediateStepOverride, enableIntermediateSteps, webSocketSchemas },
16 | dispatch: homeDispatch,
17 | } = useContext(HomeContext);
18 |
19 | const [theme, setTheme] = useState(lightMode);
20 | const [chatCompletionEndPoint, setChatCompletionEndPoint] = useState(sessionStorage.getItem('chatCompletionURL') || chatCompletionURL);
21 | const [webSocketEndPoint, setWebSocketEndPoint] = useState( sessionStorage.getItem('webSocketURL') || webSocketURL);
22 | const [webSocketSchema, setWebSocketSchema] = useState( sessionStorage.getItem('webSocketSchema') || schema);
23 | const [isIntermediateStepsEnabled, setIsIntermediateStepsEnabled] = useState(sessionStorage.getItem('enableIntermediateSteps') ? sessionStorage.getItem('enableIntermediateSteps') === 'true' : enableIntermediateSteps);
24 | const [detailsToggle, setDetailsToggle] = useState( sessionStorage.getItem('expandIntermediateSteps') === 'true' ? true : expandIntermediateSteps);
25 | const [intermediateStepOverrideToggle, setIntermediateStepOverrideToggle] = useState( sessionStorage.getItem('intermediateStepOverride') === 'false' ? false : intermediateStepOverride);
26 |
27 | useEffect(() => {
28 | const handleClickOutside = (e: MouseEvent) => {
29 | if (modalRef.current && !modalRef.current.contains(e.target as Node)) {
30 | onClose();
31 | }
32 | };
33 | if (open) {
34 | window.addEventListener('mousedown', handleClickOutside);
35 | }
36 | return () => {
37 | window.removeEventListener('mousedown', handleClickOutside);
38 | };
39 | }, [open, onClose]);
40 |
41 | const handleSave = () => {
42 | if(!chatCompletionEndPoint || !webSocketEndPoint) {
43 | toast.error('Please fill all the fields to save settings');
44 | return;
45 | }
46 |
47 | homeDispatch({ field: 'lightMode', value: theme });
48 | homeDispatch({ field: 'chatCompletionURL', value: chatCompletionEndPoint });
49 | homeDispatch({ field: 'webSocketURL', value: webSocketEndPoint });
50 | homeDispatch({ field: 'webSocketSchema', value: webSocketSchema });
51 | homeDispatch({ field: 'expandIntermediateSteps', value: detailsToggle });
52 | homeDispatch({ field: 'intermediateStepOverride', value: intermediateStepOverrideToggle });
53 | homeDispatch({ field: 'enableIntermediateSteps', value: isIntermediateStepsEnabled });
54 |
55 | sessionStorage.setItem('chatCompletionURL', chatCompletionEndPoint);
56 | sessionStorage.setItem('webSocketURL', webSocketEndPoint);
57 | sessionStorage.setItem('webSocketSchema', webSocketSchema);
58 | sessionStorage.setItem('expandIntermediateSteps', String(detailsToggle));
59 | sessionStorage.setItem('intermediateStepOverride', String(intermediateStepOverrideToggle));
60 | sessionStorage.setItem('enableIntermediateSteps', String(isIntermediateStepsEnabled));
61 |
62 | toast.success('Settings saved successfully');
63 | onClose();
64 | };
65 |
66 | if (!open) return null;
67 |
68 | return (
69 |
70 |
74 |
{t('Settings')}
75 |
76 |
{t('Theme')}
77 |
setTheme(e.target.value)}
81 | >
82 | {t('Dark mode')}
83 | {t('Light mode')}
84 |
85 |
86 |
{t('HTTP URL for Chat Completion')}
87 |
setChatCompletionEndPoint(e.target.value)}
91 | className="w-full mt-1 p-2 rounded-lg bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none"
92 | />
93 |
94 |
{t('WebSocket URL for Chat Completion')}
95 |
setWebSocketEndPoint(e.target.value)}
99 | className="w-full mt-1 p-2 rounded-lg bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none"
100 | />
101 |
102 |
{t('WebSocket Schema')}
103 |
{
107 | setWebSocketSchema(e.target.value)}
108 | }
109 | >
110 | {webSocketSchemas?.map((schema) => (
111 |
112 | {schema}
113 |
114 | ))}
115 |
116 |
117 |
118 | {
123 | setIsIntermediateStepsEnabled(!isIntermediateStepsEnabled)
124 | }}
125 | className="mr-2"
126 | />
127 |
131 | Enable Intermediate Steps
132 |
133 |
134 |
135 |
136 | {
141 | setDetailsToggle(!detailsToggle)
142 | }}
143 | disabled={!isIntermediateStepsEnabled}
144 | className="mr-2"
145 | />
146 |
150 | Expand Intermediate Steps by default
151 |
152 |
153 |
154 |
155 | {
160 | setIntermediateStepOverrideToggle(!intermediateStepOverrideToggle)
161 | }}
162 | disabled={!isIntermediateStepsEnabled}
163 | className="mr-2"
164 | />
165 |
169 | Override intermediate Steps with same Id
170 |
171 |
172 |
173 |
174 |
178 | {t('Cancel')}
179 |
180 |
184 | {t('Save')}
185 |
186 |
187 |
188 |
189 | );
190 | };
191 |
--------------------------------------------------------------------------------
/components/Sidebar/Sidebar.tsx:
--------------------------------------------------------------------------------
1 | import { IconFolderPlus, IconMistOff, IconPlus } from '@tabler/icons-react';
2 | import { ReactNode } from 'react';
3 | import { useTranslation } from 'react-i18next';
4 |
5 | import {
6 | CloseSidebarButton,
7 | OpenSidebarButton,
8 | } from './components/OpenCloseButton';
9 |
10 | import Search from '../Search';
11 |
12 | interface Props {
13 | isOpen: boolean;
14 | addItemButtonTitle: string;
15 | side: 'left' | 'right';
16 | items: T[];
17 | itemComponent: ReactNode;
18 | folderComponent: ReactNode;
19 | footerComponent?: ReactNode;
20 | searchTerm: string;
21 | handleSearchTerm: (searchTerm: string) => void;
22 | toggleOpen: () => void;
23 | handleCreateItem: () => void;
24 | handleCreateFolder: () => void;
25 | handleDrop: (e: any) => void;
26 | }
27 |
28 | const Sidebar = ({
29 | isOpen,
30 | addItemButtonTitle,
31 | side,
32 | items,
33 | itemComponent,
34 | folderComponent,
35 | footerComponent,
36 | searchTerm,
37 | handleSearchTerm,
38 | toggleOpen,
39 | handleCreateItem,
40 | handleCreateFolder,
41 | handleDrop,
42 | }: Props) => {
43 | const { t } = useTranslation('promptbar');
44 |
45 | const allowDrop = (e: any) => {
46 | e.preventDefault();
47 | };
48 |
49 | const highlightDrop = (e: any) => {
50 | e.target.style.background = '#343541';
51 | };
52 |
53 | const removeHighlight = (e: any) => {
54 | e.target.style.background = 'none';
55 | };
56 |
57 | return isOpen ? (
58 |
59 |
60 |
61 |
64 |
65 | {
68 | handleCreateItem();
69 | handleSearchTerm('');
70 | }}
71 | >
72 |
73 | {addItemButtonTitle}
74 |
75 |
76 |
80 |
81 |
82 |
83 |
88 |
89 |
90 | {items?.length > 0 && (
91 |
92 | {folderComponent}
93 |
94 | )}
95 |
96 | {items?.length > 0 ? (
97 |
104 | {itemComponent}
105 |
106 | ) : (
107 |
108 |
109 |
110 | {t('No data.')}
111 |
112 |
113 | )}
114 |
115 | {footerComponent}
116 |
117 |
118 |
119 |
120 | ) : (
121 |
122 | );
123 | };
124 |
125 | export default Sidebar;
126 |
--------------------------------------------------------------------------------
/components/Sidebar/SidebarButton.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react';
2 |
3 | interface Props {
4 | text: string;
5 | icon: JSX.Element;
6 | onClick: () => void;
7 | }
8 |
9 | export const SidebarButton: FC = ({ text, icon, onClick }) => {
10 | return (
11 |
15 | {icon}
16 | {text}
17 |
18 | );
19 | };
20 |
--------------------------------------------------------------------------------
/components/Sidebar/components/OpenCloseButton.tsx:
--------------------------------------------------------------------------------
1 | import { IconMenu2 } from '@tabler/icons-react';
2 |
3 | interface Props {
4 | onClick: any;
5 | side: 'left' | 'right';
6 | }
7 |
8 | export const CloseSidebarButton = ({ onClick, side }: Props) => {
9 | return (
10 |
18 |
19 |
20 | );
21 | };
22 |
23 | export const OpenSidebarButton = ({ onClick, side }: Props) => {
24 | return (
25 |
33 |
34 |
35 | );
36 | };
--------------------------------------------------------------------------------
/components/Sidebar/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Sidebar';
2 |
--------------------------------------------------------------------------------
/components/Spinner/Spinner.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react';
2 |
3 | interface Props {
4 | size?: string;
5 | className?: string;
6 | }
7 |
8 | const Spinner = ({ size = '1em', className = '' }: Props) => {
9 | return (
10 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | );
32 | };
33 |
34 | export default Spinner;
35 |
--------------------------------------------------------------------------------
/components/Spinner/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Spinner';
2 |
--------------------------------------------------------------------------------
/config.json:
--------------------------------------------------------------------------------
1 | {
2 | //todo - add specific config if needed
3 | }
--------------------------------------------------------------------------------
/constants/constants.tsx:
--------------------------------------------------------------------------------
1 | export const botHeader = 'Scout Bot'
--------------------------------------------------------------------------------
/hooks/useConversationOperations.ts:
--------------------------------------------------------------------------------
1 | import { saveConversation, saveConversations, updateConversation} from '@/utils/app/conversation';
2 | import { v4 as uuidv4 } from 'uuid';
3 |
4 | export const useConversationOperations = ({ conversations, dispatch, t, appConfig }) => {
5 | const handleSelectConversation = (conversation) => {
6 | dispatch({
7 | field: 'selectedConversation',
8 | value: conversation,
9 | });
10 |
11 | // updating the session id based on the selcted conversation
12 | sessionStorage.setItem('sessionId', conversation?.id);
13 | saveConversation(conversation);
14 | };
15 |
16 | const handleNewConversation = () => {
17 | const lastConversation = conversations[conversations.length - 1];
18 |
19 | const newConversation = {
20 | id: uuidv4(),
21 | name: t('New Conversation'),
22 | messages: [],
23 | folderId: null,
24 | };
25 |
26 | // setting new the session id for new chat conversation
27 | sessionStorage.setItem('sessionId', newConversation.id);
28 | const updatedConversations = [...conversations, newConversation];
29 |
30 | dispatch({ field: 'selectedConversation', value: newConversation });
31 | dispatch({ field: 'conversations', value: updatedConversations });
32 |
33 | saveConversations(updatedConversations);
34 |
35 | dispatch({ field: 'loading', value: false });
36 | };
37 |
38 | const handleUpdateConversation = (conversation, data) => {
39 | const updatedConversation = {
40 | ...conversation,
41 | [data.key]: data.value,
42 | };
43 |
44 | const { single, all } = updateConversation(updatedConversation, conversations);
45 |
46 | dispatch({ field: 'selectedConversation', value: single });
47 | dispatch({ field: 'conversations', value: all });
48 |
49 | saveConversations(all);
50 | };
51 |
52 | return { handleSelectConversation, handleNewConversation, handleUpdateConversation };
53 | };
54 |
--------------------------------------------------------------------------------
/hooks/useCreateReducer.ts:
--------------------------------------------------------------------------------
1 | import { useMemo, useReducer } from 'react';
2 |
3 | // Extracts property names from initial state of reducer to allow typesafe dispatch objects
4 | export type FieldNames = {
5 | [K in keyof T]: T[K] extends string ? K : K;
6 | }[keyof T];
7 |
8 | // Returns the Action Type for the dispatch object to be used for typing in things like context
9 | export type ActionType =
10 | | { type: 'reset' }
11 | | { type?: 'change'; field: FieldNames; value: any };
12 |
13 | // Returns a typed dispatch and state
14 | export const useCreateReducer = ({ initialState }: { initialState: T }) => {
15 | type Action =
16 | | { type: 'reset' }
17 | | { type?: 'change'; field: FieldNames; value: any };
18 |
19 | const reducer = (state: T, action: Action) => {
20 | if (!action.type) return { ...state, [action.field]: action.value };
21 |
22 | if (action.type === 'reset') return initialState;
23 |
24 | throw new Error();
25 | };
26 |
27 | const [state, dispatch] = useReducer(reducer, initialState);
28 |
29 | return useMemo(() => ({ state, dispatch }), [state, dispatch]);
30 | };
31 |
--------------------------------------------------------------------------------
/hooks/useFolderOperations.ts:
--------------------------------------------------------------------------------
1 | import { saveFolders } from '@/utils/app/folders'; // Adjust according to your utility functions' locations
2 | import { v4 as uuidv4 } from 'uuid';
3 |
4 | export const useFolderOperations = ({folders, dispatch}) => {
5 |
6 | const handleCreateFolder = (name, type) => {
7 | const newFolder = {
8 | id: uuidv4(), // Ensure you have uuid imported or an alternative way to generate unique ids
9 | name,
10 | type,
11 | };
12 |
13 | const updatedFolders = [...folders, newFolder];
14 | dispatch({ field: 'folders', value: updatedFolders });
15 | saveFolders(updatedFolders); // Assuming you have a utility function to persist folders change
16 | };
17 |
18 | const handleDeleteFolder = (folderId) => {
19 | const updatedFolders = folders.filter(folder => folder.id !== folderId);
20 | dispatch({ field: 'folders', value: updatedFolders });
21 | saveFolders(updatedFolders); // Persist the updated list after deletion
22 | };
23 |
24 | const handleUpdateFolder = (folderId, name) => {
25 | const updatedFolders = folders.map(folder =>
26 | folder.id === folderId ? { ...folder, name } : folder
27 | );
28 | dispatch({ field: 'folders', value: updatedFolders });
29 | saveFolders(updatedFolders); // Persist the updated list
30 | };
31 |
32 | return { handleCreateFolder, handleDeleteFolder, handleUpdateFolder };
33 | };
34 |
35 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.
6 |
--------------------------------------------------------------------------------
/next-i18next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | i18n: {
3 | defaultLocale: 'en',
4 | locales: [
5 | "bn",
6 | "de",
7 | "en",
8 | "es",
9 | "fr",
10 | "he",
11 | "id",
12 | "it",
13 | "ja",
14 | "ko",
15 | "pl",
16 | "pt",
17 | "ru",
18 | "ro",
19 | "sv",
20 | "te",
21 | "vi",
22 | "zh",
23 | "ar",
24 | "tr",
25 | "ca",
26 | "fi",
27 | ],
28 | },
29 | localePath:
30 | typeof window === 'undefined'
31 | ? require('path').resolve('./public/locales')
32 | : '/public/locales',
33 | };
34 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | const { configureRuntimeEnv } = require('next-runtime-env/build/configure');
2 |
3 | const nextConfig = {
4 | env: {
5 | ...configureRuntimeEnv(),
6 | },
7 | output: 'standalone',
8 | typescript: {
9 | // !! WARN !!
10 | // Dangerously allow production builds to successfully complete even if
11 | // your project has type errors.
12 | // !! WARN !!
13 | ignoreBuildErrors: true,
14 | },
15 | experimental: {
16 | serverActions: {
17 | bodySizeLimit: "5mb",
18 | },
19 | },
20 | webpack(config, { isServer, dev }) {
21 | config.experiments = {
22 | asyncWebAssembly: true,
23 | layers: true,
24 | };
25 |
26 | return config;
27 | },
28 | async redirects() {
29 | return [
30 | ]
31 | },
32 | };
33 |
34 | module.exports = nextConfig;
35 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "AIQ Toolkit-UI",
3 | "version": "0.1.1",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "format": "prettier --write .",
11 | "test": "vitest",
12 | "coverage": "vitest run --coverage"
13 | },
14 | "dependencies": {
15 | "@datadog/browser-rum": "^5.11.0",
16 | "@dqbd/tiktoken": "^1.0.2",
17 | "@radix-ui/react-select": "^2.1.2",
18 | "@tabler/icons-react": "^2.9.0",
19 | "@types/jwt-decode": "^3.1.0",
20 | "chart.js": "^4.4.1",
21 | "eventsource-parser": "^0.1.0",
22 | "file-saver": "^2.0.5",
23 | "html-to-image": "^1.11.11",
24 | "i18next": "^22.4.13",
25 | "jsonwebtoken": "^9.0.2",
26 | "jwt-decode": "^4.0.0",
27 | "lodash": "^4.17.21",
28 | "lucide-react": "^0.454.0",
29 | "next": "14.2.25",
30 | "next-auth": "^4.24.7",
31 | "next-i18next": "^13.2.2",
32 | "pptxgenjs": "^3.12.0",
33 | "react": "^18.2.0",
34 | "react-bootstrap-modal": "^4.2.0",
35 | "react-chartjs-2": "^5.2.0",
36 | "react-dom": "^18.2.0",
37 | "react-force-graph-2d": "^1.25.5",
38 | "react-hot-toast": "^2.4.0",
39 | "react-i18next": "^12.2.0",
40 | "react-markdown": "^8.0.5",
41 | "react-query": "^3.39.3",
42 | "react-syntax-highlighter": "^15.5.0",
43 | "recharts": "^2.12.7",
44 | "rehype-mathjax": "^4.0.2",
45 | "rehype-raw": "^7.0.0",
46 | "remark-gfm": "^3.0.1",
47 | "remark-math": "^5.1.1",
48 | "remark-unwrap-images": "^4.0.0",
49 | "uuid": "^9.0.1"
50 | },
51 | "devDependencies": {
52 | "@mozilla/readability": "^0.4.4",
53 | "@tailwindcss/typography": "^0.5.9",
54 | "@trivago/prettier-plugin-sort-imports": "^1.4.4",
55 | "@types/jsdom": "^21.1.1",
56 | "@types/node": "18.15.0",
57 | "@types/react": "18.0.28",
58 | "@types/react-dom": "18.0.11",
59 | "@types/react-syntax-highlighter": "^15.5.6",
60 | "@types/uuid": "^9.0.1",
61 | "@vitest/coverage-c8": "^0.29.7",
62 | "autoprefixer": "^10.4.14",
63 | "endent": "^2.1.0",
64 | "eslint": "8.36.0",
65 | "eslint-config-next": "^14.2.10",
66 | "gpt-3-encoder": "^1.1.4",
67 | "jsdom": "^21.1.1",
68 | "postcss": "^8.4.21",
69 | "prettier": "^2.8.7",
70 | "prettier-plugin-tailwindcss": "^0.2.5",
71 | "tailwindcss": "^3.3.3",
72 | "typescript": "4.9.5",
73 | "vitest": "^0.29.7",
74 | "next-runtime-env": "^1.3.0"
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import { Toaster } from 'react-hot-toast';
2 | import { QueryClient, QueryClientProvider } from 'react-query';
3 |
4 | import { appWithTranslation } from 'next-i18next';
5 | import type { AppProps } from 'next/app';
6 | import { Inter } from 'next/font/google';
7 |
8 | import '@/styles/globals.css';
9 |
10 | const inter = Inter({ subsets: ['latin'] });
11 |
12 | function App({ Component, pageProps }: AppProps<{}>) {
13 |
14 | const queryClient = new QueryClient();
15 |
16 | return (
17 |
18 |
26 |
27 |
28 |
29 |
30 | );
31 | }
32 |
33 | export default appWithTranslation(App);
--------------------------------------------------------------------------------
/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import { DocumentProps, Head, Html, Main, NextScript } from 'next/document';
2 | import i18nextConfig from '../next-i18next.config';
3 | type Props = DocumentProps & {
4 | // add custom document props
5 | };
6 |
7 | export default function Document(props: Props) {
8 | const currentLocale =
9 | props.__NEXT_DATA__.locale ?? i18nextConfig.i18n.defaultLocale;
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/pages/aiq-auth.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import type { FormEvent } from 'react';
3 |
4 | interface PromptModalProps {
5 | onSubmit: (value: string, domain: string, setError: (error: string | null) => void) => void;
6 | initialValue: string;
7 | initialDomain: string;
8 | responseData: any;
9 | clientError: string | null;
10 | serverError: string | null;
11 | }
12 |
13 | function PromptModal({ onSubmit, initialValue, initialDomain, responseData, clientError, serverError }: PromptModalProps) {
14 | const [value, setValue] = useState(initialValue);
15 | const [domain, setDomain] = useState(initialDomain);
16 | const [error, setError] = useState(null);
17 | const [isSubmitting, setIsSubmitting] = useState(false);
18 |
19 | const handleSubmit = (e: FormEvent) => {
20 | e.preventDefault();
21 | setError(null);
22 | setIsSubmitting(true);
23 | onSubmit(value, domain, (errorMessage) => {
24 | setError(errorMessage);
25 | setIsSubmitting(false);
26 | });
27 | };
28 |
29 | return (
30 |
31 |
32 |
Enter Authentication Details
33 |
106 |
107 |
108 | );
109 | }
110 |
111 | export default function AIQAuthPage() {
112 | const [showPrompt, setShowPrompt] = useState(true);
113 | const [isProcessing, setIsProcessing] = useState(false);
114 | const [error, setError] = useState(null);
115 | const [responseError, setResponseError] = useState(null);
116 | const [responseData, setResponseData] = useState(null);
117 |
118 | const [authSuccess, setAuthSuccess] = useState(false);
119 | const [currentPopup, setCurrentPopup] = useState(null);
120 | const [authProviderName, setAuthProviderName] = useState(null);
121 |
122 | React.useEffect(() => {
123 | const handleMessage = (event: MessageEvent) => {
124 | if (currentPopup && !currentPopup.closed) {
125 | try {
126 | currentPopup.document.title = `Authentication was successfully granted for authentication provider ${authProviderName}`;
127 | } catch (e) {
128 | console.log('Could not update popup title due to cross-origin restrictions');
129 | }
130 | }
131 |
132 | setShowPrompt(false);
133 | setAuthSuccess(true);
134 | setIsProcessing(false);
135 | };
136 |
137 | window.addEventListener('message', handleMessage);
138 | return () => window.removeEventListener('message', handleMessage);
139 | }, [currentPopup, authProviderName]);
140 |
141 | const handleSubmit = async (key: string, domain: string, setModalError: (error: string | null) => void) => {
142 | setIsProcessing(true);
143 | setError(null);
144 | setResponseError(null);
145 | setResponseData(null);
146 | setAuthSuccess(false);
147 | setAuthProviderName(null);
148 |
149 | try {
150 | const response = await fetch(`${domain}/auth/prompt-uri`, {
151 | method: 'POST',
152 | headers: {
153 | 'Content-Type': 'application/json'
154 | },
155 | body: JSON.stringify({ "consent_prompt_key": key }),
156 | });
157 |
158 | const contentType = response.headers.get('content-type');
159 |
160 | if (contentType && contentType.includes('application/json')) {
161 | const data = await response.json();
162 |
163 | if (!response.ok) {
164 | setResponseData(data);
165 | const errorMessage = data.detail || data.message || data.error || `Error: ${response.status} - ${response.statusText}`;
166 | setModalError(errorMessage);
167 | setIsProcessing(false);
168 | return;
169 | }
170 |
171 | setResponseData(data);
172 |
173 | if (data.redirect_url) {
174 | setAuthProviderName(data.auth_provider_name);
175 | const popup = window.open(
176 | data.redirect_url,
177 | 'auth-popup',
178 | 'width=600,height=700,scrollbars=yes,resizable=yes'
179 | );
180 | setCurrentPopup(popup);
181 | setModalError(null);
182 | return;
183 | }
184 | }
185 |
186 | if (!response.ok) {
187 | const errorMessage = `Error: ${response.status} - ${response.statusText}`;
188 | setModalError(errorMessage);
189 | setIsProcessing(false);
190 | return;
191 | }
192 |
193 | } catch (error) {
194 | const errorMessage = error instanceof Error ? error.message : 'An unexpected error occurred';
195 | setModalError(errorMessage);
196 | } finally {
197 | setIsProcessing(false);
198 | }
199 | };
200 |
201 | return (
202 |
203 | {/* Green Banner */}
204 |
205 |
NeMo-Agent-Toolkit
206 |
207 |
208 |
209 | {showPrompt && !authSuccess && (
210 |
218 | )}
219 |
220 |
221 | {authSuccess && (
222 |
223 |
224 |
225 |
226 |
231 |
232 | Authentication was successfully granted for authentication provider {authProviderName}
233 |
234 |
235 |
236 |
{
238 | setShowPrompt(true);
239 | setAuthSuccess(false);
240 | setError(null);
241 | setResponseError(null);
242 | setResponseData(null);
243 | setAuthProviderName(null);
244 | }}
245 | className="w-full mt-3 px-4 py-2 text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 transition-colors duration-200"
246 | >
247 | Authenticate Again
248 |
249 |
250 |
251 |
252 | )}
253 |
254 | );
255 | }
--------------------------------------------------------------------------------
/pages/api/chat.ts:
--------------------------------------------------------------------------------
1 | import { ChatBody } from '@/types/chat';
2 | import { delay } from '@/utils/app/helper';
3 | export const config = {
4 | runtime: 'edge',
5 | api: {
6 | bodyParser: {
7 | sizeLimit: '5mb',
8 | },
9 | },
10 | };
11 |
12 |
13 | const handler = async (req: Request): Promise => {
14 |
15 | // extract the request body
16 | let {
17 | chatCompletionURL = '',
18 | messages = [],
19 | additionalProps = {
20 | enableIntermediateSteps: true
21 | }
22 | } = (await req.json()) as ChatBody;
23 |
24 | try {
25 | let payload
26 | // for generate end point the request schema is {input_message: "user question"}
27 | if(chatCompletionURL.includes('generate')) {
28 | if (messages?.length > 0 && messages[messages.length - 1]?.role === 'user') {
29 | payload = {
30 | input_message: messages[messages.length - 1]?.content ?? ''
31 | };
32 | } else {
33 | throw new Error('User message not found: messages array is empty or invalid.');
34 | }
35 | }
36 |
37 | // for chat end point it is openAI compatible schema
38 | else {
39 | payload = {
40 | messages,
41 | model: "string",
42 | temperature: 0,
43 | max_tokens: 0,
44 | top_p: 0,
45 | use_knowledge_base: true,
46 | top_k: 0,
47 | collection_name: "string",
48 | stop: true,
49 | additionalProp1: {}
50 | }
51 | }
52 |
53 | console.log('aiq - making request to', { url: chatCompletionURL });
54 |
55 | let response = await fetch(chatCompletionURL, {
56 | method: 'POST',
57 | headers: {
58 | 'Content-Type': 'application/json',
59 | 'Conversation-Id': req.headers.get('Conversation-Id') || '',
60 | },
61 | body: JSON.stringify(payload),
62 | });
63 |
64 | console.log('aiq - received response from server', response.status);
65 |
66 | if (!response.ok) {
67 | let errorMessage = await response.text();
68 |
69 | if(errorMessage.includes('')) {
70 | if(errorMessage.includes('404')) {
71 | errorMessage = '404 - Page not found'
72 | }
73 | else {
74 | errorMessage = 'HTML response received from server, which cannot be parsed.'
75 | }
76 |
77 | }
78 | console.log('aiq - received error response from server', errorMessage);
79 | // For other errors, return a Response object with the error message
80 | const formattedError = `Something went wrong. Please try again. \n\nDetails Error Message: ${errorMessage || 'Unknown error'} `
81 | return new Response(formattedError, {
82 | status: 200, // Return 200 status
83 | headers: { 'Content-Type': 'text/plain' }, // Set appropriate content type
84 | });
85 | }
86 |
87 |
88 | // response handling for streaming schema
89 | if (chatCompletionURL.includes('stream')) {
90 | console.log('aiq - processing streaming response');
91 | const encoder = new TextEncoder();
92 | const decoder = new TextDecoder();
93 |
94 | const responseStream = new ReadableStream({
95 | async start(controller) {
96 | const reader = response?.body?.getReader();
97 | let buffer = '';
98 | let counter = 0
99 | try {
100 | while (true) {
101 | const { done, value } = await reader?.read();
102 | if (done) break;
103 |
104 | buffer += decoder.decode(value, { stream: true });
105 | const lines = buffer.split('\n');
106 | buffer = lines.pop() || '';
107 |
108 | for (const line of lines) {
109 | if (line.startsWith('data: ')) {
110 | const data = line.slice(5);
111 | if (data.trim() === '[DONE]') {
112 | controller.close();
113 | return;
114 | }
115 | try {
116 | const parsed = JSON.parse(data);
117 | const content = parsed.choices[0]?.message?.content || parsed.choices[0]?.delta?.content || '';
118 | if (content) {
119 | // console.log(`aiq - stream response received from server with length`, content?.length)
120 | controller.enqueue(encoder.encode(content));
121 | }
122 | } catch (error) {
123 | console.log('aiq - error parsing JSON:', error);
124 | }
125 | }
126 | // TODO - fix or remove this and use websocket to support intermediate data
127 | if (line.startsWith('intermediate_data: ')) {
128 |
129 | if(additionalProps.enableIntermediateSteps === true) {
130 | const data = line.split('intermediate_data: ')[1];
131 | if (data.trim() === '[DONE]') {
132 | controller.close();
133 | return;
134 | }
135 | try {
136 | const payload = JSON.parse(data);
137 | let details = payload?.payload || 'No details';
138 | let name = payload?.name || 'Step';
139 | let id = payload?.id || '';
140 | let status = payload?.status || 'in_progress';
141 | let error = payload?.error || '';
142 | let type = 'system_intermediate';
143 | let parent_id = payload?.parent_id || 'default';
144 | let intermediate_parent_id = payload?.intermediate_parent_id || 'default';
145 | let time_stamp = payload?.time_stamp || 'default';
146 |
147 | const intermediate_message = {
148 | id,
149 | status,
150 | error,
151 | type,
152 | parent_id,
153 | intermediate_parent_id,
154 | content: {
155 | name: name,
156 | payload: details,
157 | },
158 | time_stamp,
159 | index: counter++
160 | };
161 | const messageString = `${JSON.stringify(intermediate_message)} `;
162 | // console.log('intermediate step counter', counter ++ , messageString.length)
163 | controller.enqueue(encoder.encode(messageString));
164 | // await delay(1000)
165 | } catch (error) {
166 | controller.enqueue(encoder.encode('Error parsing intermediate data: ' + error));
167 | console.log('aiq - error parsing JSON:', error);
168 | }
169 | }
170 | else {
171 | console.log('aiq - intermediate data is not enabled');
172 | }
173 | }
174 | }
175 | }
176 | } catch (error) {
177 | console.log('aiq - stream reading error, closing stream', error);
178 | controller.close();
179 | } finally {
180 | console.log('aiq - response processing is completed, closing stream');
181 | controller.close();
182 | reader?.releaseLock();
183 | }
184 | },
185 | });
186 |
187 | return new Response(responseStream);
188 | }
189 |
190 | // response handling for non straming schema
191 | else {
192 | console.log('aiq - processing non streaming response');
193 | const data = await response.text();
194 | let parsed = null;
195 |
196 | try {
197 | parsed = JSON.parse(data);
198 | } catch (error) {
199 | console.log('aiq - error parsing JSON response', error);
200 | }
201 |
202 | // Safely extract content with proper checks
203 | const content =
204 | parsed?.output || // Check for `output`
205 | parsed?.answer || // Check for `answer`
206 | parsed?.value || // Check for `value`
207 | (Array.isArray(parsed?.choices) ? parsed.choices[0]?.message?.content : null) || // Safely check `choices[0]`
208 | parsed || // Fallback to the entire `parsed` object
209 | data; // Final fallback to raw `data`
210 |
211 | if (content) {
212 | console.log('aiq - response processing is completed');
213 | return new Response(content);
214 | } else {
215 | console.log('aiq - error parsing response');
216 | return new Response(response.body || data);
217 | }
218 | }
219 |
220 | } catch (error) {
221 | console.log('error - while making request', error);
222 | const formattedError = `Something went wrong. Please try again. \n\nDetails Error Message: ${error?.message || 'Unknown error'} `
223 | return new Response(formattedError, { status: 200 })
224 | }
225 | };
226 |
227 | export default handler;
228 |
--------------------------------------------------------------------------------
/pages/api/home/home.context.tsx:
--------------------------------------------------------------------------------
1 | import { Dispatch, createContext } from 'react';
2 |
3 | import { ActionType } from '@/hooks/useCreateReducer';
4 |
5 | import { Conversation } from '@/types/chat';
6 | import { KeyValuePair } from '@/types/data';
7 | import { FolderType } from '@/types/folder';
8 |
9 | import { HomeInitialState } from './home.state';
10 |
11 | export interface HomeContextProps {
12 | state: HomeInitialState;
13 | dispatch: Dispatch>;
14 | handleNewConversation: () => void;
15 | handleCreateFolder: (name: string, type: FolderType) => void;
16 | handleDeleteFolder: (folderId: string) => void;
17 | handleUpdateFolder: (folderId: string, name: string) => void;
18 | handleSelectConversation: (conversation: Conversation) => void;
19 | handleUpdateConversation: (
20 | conversation: Conversation,
21 | data: KeyValuePair,
22 | ) => void;
23 | }
24 |
25 | const HomeContext = createContext(undefined!);
26 |
27 | export default HomeContext;
28 |
--------------------------------------------------------------------------------
/pages/api/home/home.state.tsx:
--------------------------------------------------------------------------------
1 | import { Conversation, Message } from '@/types/chat';
2 | import { FolderInterface } from '@/types/folder';
3 | import { t } from 'i18next';
4 | import { env } from 'next-runtime-env'
5 |
6 | export interface HomeInitialState {
7 | loading: boolean;
8 | lightMode: 'light' | 'dark';
9 | messageIsStreaming: boolean;
10 | folders: FolderInterface[];
11 | conversations: Conversation[];
12 | selectedConversation: Conversation | undefined;
13 | currentMessage: Message | undefined;
14 | showChatbar: boolean;
15 | currentFolder: FolderInterface | undefined;
16 | messageError: boolean;
17 | searchTerm: string;
18 | chatHistory: boolean;
19 | chatCompletionURL?: string;
20 | webSocketMode?: boolean;
21 | webSocketConnected?: boolean;
22 | webSocketURL?: string;
23 | webSocketSchema?: string;
24 | webSocketSchemas?: string[];
25 | enableIntermediateSteps?: boolean;
26 | expandIntermediateSteps?: boolean;
27 | intermediateStepOverride?: boolean;
28 | autoScroll?: boolean;
29 | additionalConfig: any;
30 | }
31 |
32 | export const initialState: HomeInitialState = {
33 | loading: false,
34 | lightMode: 'light',
35 | messageIsStreaming: false,
36 | folders: [],
37 | conversations: [],
38 | selectedConversation: undefined,
39 | currentMessage: undefined,
40 | showChatbar: true,
41 | currentFolder: undefined,
42 | messageError: false,
43 | searchTerm: '',
44 | chatHistory: env('NEXT_PUBLIC_CHAT_HISTORY_DEFAULT_ON') === 'true' || process?.env?.NEXT_PUBLIC_CHAT_HISTORY_DEFAULT_ON === 'true' ? true : false,
45 | chatCompletionURL: env('NEXT_PUBLIC_HTTP_CHAT_COMPLETION_URL') || process?.env?.NEXT_PUBLIC_HTTP_CHAT_COMPLETION_URL || 'http://127.0.0.1:8000/chat/stream',
46 | webSocketMode: env('NEXT_PUBLIC_WEB_SOCKET_DEFAULT_ON') === 'true' || process?.env?.NEXT_PUBLIC_WEB_SOCKET_DEFAULT_ON === 'true' ? true : false,
47 | webSocketConnected: false,
48 | webSocketURL: env('NEXT_PUBLIC_WEBSOCKET_CHAT_COMPLETION_URL') || process?.env?.NEXT_PUBLIC_WEBSOCKET_CHAT_COMPLETION_URL || 'ws://127.0.0.1:8000/websocket',
49 | webSocketSchema: 'chat_stream',
50 | webSocketSchemas: ['chat_stream', 'chat', 'generate_stream', 'generate'],
51 | enableIntermediateSteps: env('NEXT_PUBLIC_ENABLE_INTERMEDIATE_STEPS') === 'true' || process?.env?.NEXT_PUBLIC_ENABLE_INTERMEDIATE_STEPS === 'true' ? true : false,
52 | expandIntermediateSteps: false,
53 | intermediateStepOverride: true,
54 | autoScroll: true,
55 | additionalConfig: {},
56 | };
57 |
--------------------------------------------------------------------------------
/pages/api/home/home.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { useEffect, useRef } from 'react';
3 |
4 | import { GetServerSideProps } from 'next';
5 | import { useTranslation } from 'next-i18next';
6 | import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
7 | import Head from 'next/head';
8 |
9 | import { useCreateReducer } from '@/hooks/useCreateReducer';
10 |
11 |
12 | import {
13 | cleanConversationHistory,
14 | cleanSelectedConversation,
15 | } from '@/utils/app/clean';
16 | import {
17 | saveConversation,
18 | saveConversations,
19 | updateConversation,
20 | } from '@/utils/app/conversation';
21 | import { saveFolders } from '@/utils/app/folders';
22 | import { getSettings } from '@/utils/app/settings';
23 |
24 | import { Conversation } from '@/types/chat';
25 | import { KeyValuePair } from '@/types/data';
26 | import { FolderInterface, FolderType } from '@/types/folder';
27 |
28 | import { Chat } from '@/components/Chat/Chat';
29 | import { Chatbar } from '@/components/Chatbar/Chatbar';
30 | import { Navbar } from '@/components/Mobile/Navbar';
31 |
32 | import HomeContext from './home.context';
33 | import { HomeInitialState, initialState } from './home.state';
34 |
35 | import { v4 as uuidv4 } from 'uuid';
36 | import { getWorkflowName } from '@/utils/app/helper';
37 |
38 | const Home = (props: any) => {
39 | const { t } = useTranslation('chat');
40 |
41 | const contextValue = useCreateReducer({
42 | initialState,
43 | });
44 |
45 | let workflow = 'AIQ Toolkit';
46 |
47 | const {
48 | state: {
49 | lightMode,
50 | folders,
51 | conversations,
52 | selectedConversation,
53 | },
54 | dispatch,
55 | } = contextValue;
56 |
57 | const stopConversationRef = useRef(false);
58 |
59 | const handleSelectConversation = (conversation: Conversation) => {
60 | dispatch({
61 | field: 'selectedConversation',
62 | value: conversation,
63 | });
64 |
65 | saveConversation(conversation);
66 | };
67 |
68 | // FOLDER OPERATIONS --------------------------------------------
69 |
70 | const handleCreateFolder = (name: string, type: FolderType) => {
71 | const newFolder: FolderInterface = {
72 | id: uuidv4(),
73 | name,
74 | type,
75 | };
76 |
77 | const updatedFolders = [...folders, newFolder];
78 |
79 | dispatch({ field: 'folders', value: updatedFolders });
80 | saveFolders(updatedFolders);
81 | };
82 |
83 | const handleDeleteFolder = (folderId: string) => {
84 | const updatedFolders = folders.filter((f) => f.id !== folderId);
85 | dispatch({ field: 'folders', value: updatedFolders });
86 | saveFolders(updatedFolders);
87 |
88 | const updatedConversations: Conversation[] = conversations.map((c) => {
89 | if (c.folderId === folderId) {
90 | return {
91 | ...c,
92 | folderId: null,
93 | };
94 | }
95 |
96 | return c;
97 | });
98 |
99 | dispatch({ field: 'conversations', value: updatedConversations });
100 | saveConversations(updatedConversations);;
101 | };
102 |
103 | const handleUpdateFolder = (folderId: string, name: string) => {
104 | const updatedFolders = folders.map((f) => {
105 | if (f.id === folderId) {
106 | return {
107 | ...f,
108 | name,
109 | };
110 | }
111 |
112 | return f;
113 | });
114 |
115 | dispatch({ field: 'folders', value: updatedFolders });
116 |
117 | saveFolders(updatedFolders);
118 | };
119 |
120 | // CONVERSATION OPERATIONS --------------------------------------------
121 |
122 | const handleNewConversation = () => {
123 | const lastConversation = conversations[conversations.length - 1];
124 |
125 | const newConversation: Conversation = {
126 | id: uuidv4(),
127 | name: t('New Conversation'),
128 | messages: [],
129 | folderId: null,
130 | };
131 |
132 | const updatedConversations = [...conversations, newConversation];
133 |
134 | dispatch({ field: 'selectedConversation', value: newConversation });
135 | dispatch({ field: 'conversations', value: updatedConversations });
136 |
137 | saveConversation(newConversation);
138 | saveConversations(updatedConversations);
139 |
140 | dispatch({ field: 'loading', value: false });
141 | };
142 |
143 | const handleUpdateConversation = (
144 | conversation: Conversation,
145 | data: KeyValuePair,
146 | ) => {
147 | const updatedConversation = {
148 | ...conversation,
149 | [data.key]: data.value,
150 | };
151 |
152 | const { single, all } = updateConversation(
153 | updatedConversation,
154 | conversations,
155 | );
156 |
157 | dispatch({ field: 'selectedConversation', value: single });
158 | dispatch({ field: 'conversations', value: all });
159 | };
160 |
161 | // EFFECTS --------------------------------------------
162 |
163 | useEffect(() => {
164 | if (window.innerWidth < 640) {
165 | dispatch({ field: 'showChatbar', value: false });
166 | }
167 | }, [selectedConversation, dispatch]);
168 |
169 | useEffect(() => {
170 | workflow = getWorkflowName()
171 | const settings = getSettings();
172 | if (settings.theme) {
173 | dispatch({
174 | field: 'lightMode',
175 | value: settings.theme,
176 | });
177 | }
178 |
179 | const showChatbar = sessionStorage.getItem('showChatbar');
180 | if (showChatbar) {
181 | dispatch({ field: 'showChatbar', value: showChatbar === 'true' });
182 | }
183 |
184 | const folders = sessionStorage.getItem('folders');
185 | if (folders) {
186 | dispatch({ field: 'folders', value: JSON.parse(folders) });
187 | }
188 |
189 | const conversationHistory = sessionStorage.getItem('conversationHistory');
190 | if (conversationHistory) {
191 | const parsedConversationHistory: Conversation[] =
192 | JSON.parse(conversationHistory);
193 | const cleanedConversationHistory = cleanConversationHistory(
194 | parsedConversationHistory,
195 | );
196 |
197 | dispatch({ field: 'conversations', value: cleanedConversationHistory });
198 | }
199 |
200 | const selectedConversation = sessionStorage.getItem('selectedConversation');
201 | if (selectedConversation) {
202 | const parsedSelectedConversation: Conversation =
203 | JSON.parse(selectedConversation);
204 | const cleanedSelectedConversation = cleanSelectedConversation(
205 | parsedSelectedConversation,
206 | );
207 |
208 | dispatch({
209 | field: 'selectedConversation',
210 | value: cleanedSelectedConversation,
211 | });
212 | } else {
213 | const lastConversation = conversations[conversations.length - 1];
214 | dispatch({
215 | field: 'selectedConversation',
216 | value: {
217 | id: uuidv4(),
218 | name: t('New Conversation'),
219 | messages: [],
220 | folderId: null,
221 | },
222 | });
223 | }
224 | }, [dispatch, t]);
225 |
226 | return (
227 |
238 |
239 | {workflow}
240 |
241 |
245 |
246 |
247 | {selectedConversation && (
248 |
251 |
252 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 | )}
267 |
268 | );
269 | };
270 | export default Home;
271 |
272 | export const getServerSideProps: GetServerSideProps = async ({ locale }) => {
273 | const defaultModelId =
274 | process.env.DEFAULT_MODEL || '';
275 |
276 | return {
277 | props: {
278 | defaultModelId,
279 | ...(await serverSideTranslations(locale ?? 'en', [
280 | 'common',
281 | 'chat',
282 | 'sidebar',
283 | 'markdown',
284 | 'promptbar',
285 | 'settings',
286 | ])),
287 | },
288 | };
289 | };
290 |
--------------------------------------------------------------------------------
/pages/api/home/index.ts:
--------------------------------------------------------------------------------
1 | export { default, getServerSideProps } from './home';
--------------------------------------------------------------------------------
/pages/index.tsx:
--------------------------------------------------------------------------------
1 | export { default, getServerSideProps } from './api/home';
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | trailingComma: 'all',
3 | singleQuote: true,
4 | plugins: [
5 | 'prettier-plugin-tailwindcss',
6 | '@trivago/prettier-plugin-sort-imports',
7 | ],
8 | importOrder: [
9 | 'react', // React
10 | '^react-.*$', // React-related imports
11 | '^next', // Next-related imports
12 | '^next-.*$', // Next-related imports
13 | '^next/.*$', // Next-related imports
14 | '^.*/hooks/.*$', // Hooks
15 | '^.*/services/.*$', // Services
16 | '^.*/utils/.*$', // Utils
17 | '^.*/types/.*$', // Types
18 | '^.*/pages/.*$', // Components
19 | '^.*/components/.*$', // Components
20 | '^[./]', // Other imports
21 | '.*', // Any uncaught imports
22 | ],
23 | importOrderSeparation: true,
24 | importOrderSortSpecifiers: true,
25 | };
26 |
--------------------------------------------------------------------------------
/public/audio/recording.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NVIDIA/NeMo-Agent-Toolkit-UI/9816cd9801e782324f48a0a77c299c78406f0919/public/audio/recording.wav
--------------------------------------------------------------------------------
/public/favicon.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NVIDIA/NeMo-Agent-Toolkit-UI/9816cd9801e782324f48a0a77c299c78406f0919/public/favicon.jpg
--------------------------------------------------------------------------------
/public/favicon1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NVIDIA/NeMo-Agent-Toolkit-UI/9816cd9801e782324f48a0a77c299c78406f0919/public/favicon1.png
--------------------------------------------------------------------------------
/public/locales/en/common.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/public/nvidia.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NVIDIA/NeMo-Agent-Toolkit-UI/9816cd9801e782324f48a0a77c299c78406f0919/public/nvidia.jpg
--------------------------------------------------------------------------------
/public/screenshots/hitl_prompt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NVIDIA/NeMo-Agent-Toolkit-UI/9816cd9801e782324f48a0a77c299c78406f0919/public/screenshots/hitl_prompt.png
--------------------------------------------------------------------------------
/public/screenshots/hitl_settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NVIDIA/NeMo-Agent-Toolkit-UI/9816cd9801e782324f48a0a77c299c78406f0919/public/screenshots/hitl_settings.png
--------------------------------------------------------------------------------
/public/screenshots/ui_generate_example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NVIDIA/NeMo-Agent-Toolkit-UI/9816cd9801e782324f48a0a77c299c78406f0919/public/screenshots/ui_generate_example.png
--------------------------------------------------------------------------------
/public/screenshots/ui_generate_example_settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NVIDIA/NeMo-Agent-Toolkit-UI/9816cd9801e782324f48a0a77c299c78406f0919/public/screenshots/ui_generate_example_settings.png
--------------------------------------------------------------------------------
/public/screenshots/ui_home_page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NVIDIA/NeMo-Agent-Toolkit-UI/9816cd9801e782324f48a0a77c299c78406f0919/public/screenshots/ui_home_page.png
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | ::-webkit-scrollbar-track {
6 | background-color: transparent;
7 | }
8 |
9 | ::-webkit-scrollbar-thumb {
10 | background-color: #ccc;
11 | border-radius: 10px;
12 | }
13 |
14 | ::-webkit-scrollbar-thumb:hover {
15 | background-color: #aaa;
16 | }
17 |
18 | ::-webkit-scrollbar-track:hover {
19 | background-color: #f2f2f2;
20 | }
21 |
22 | ::-webkit-scrollbar-corner {
23 | background-color: transparent;
24 | }
25 |
26 | ::-webkit-scrollbar {
27 | width: 6px;
28 | height: 6px;
29 | }
30 |
31 | html {
32 | background: #f4f7f2;
33 | font-family: NVIDIA-NALA,Arial,Helvetica,Sans-Serif;
34 | }
35 |
36 | @media (max-width: 720px) {
37 | pre {
38 | width: calc(100vw - 110px);
39 | }
40 | }
41 |
42 | pre:has(div.codeblock) {
43 | padding: 0;
44 | }
45 |
46 | /* add the code bellow */
47 | @layer utilities {
48 | /* Hide scrollbar for Chrome, Safari and Opera */
49 | .no-scrollbar::-webkit-scrollbar {
50 | display: none;
51 | }
52 | /* Hide scrollbar for IE, Edge and Firefox */
53 | .no-scrollbar {
54 | -ms-overflow-style: none; /* IE and Edge */
55 | scrollbar-width: none; /* Firefox */
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | './app/**/*.{js,ts,jsx,tsx}',
5 | './pages/**/*.{js,ts,jsx,tsx}',
6 | './components/**/*.{js,ts,jsx,tsx}',
7 | ],
8 | darkMode: 'class',
9 | theme: {
10 | extend: {
11 | screens: {
12 | 'xs': '320px', // Extra small screen breakpoint
13 | 'sm': '344px', // Small screen breakpoint
14 | 'base': '768px',
15 | 'md': '960px',
16 | 'lg': '1440px',
17 | },
18 | fontSize: {
19 | 'xs': ['0.6rem', { lineHeight: '1rem' }], // Extra small screen font size
20 | 'sm': ['0.875rem', { lineHeight: '1.25rem' }], // Small screen font size
21 | 'base': ['0.9rem', { lineHeight: '1.5rem' }], // Base font size
22 | 'lg': ['1.125rem', { lineHeight: '1.75rem' }], // Large screen font size
23 | 'xl': ['1.25rem', { lineHeight: '1.75rem' }], // Extra large screen font size
24 | },
25 | keyframes: {
26 | blink: {
27 | '0%, 100%': { opacity: 1 },
28 | '50%': { opacity: 0 },
29 | },
30 | flicker: {
31 | '0%, 100%': { opacity: '1' },
32 | '50%': { opacity: '0.4' },
33 | },
34 | glitch: {
35 | '0%': { transform: 'translate(0)' },
36 | '20%': { transform: 'translate(-2px, 2px)' },
37 | '40%': { transform: 'translate(2px, -2px)' },
38 | '60%': { transform: 'translate(-2px, -2px)' },
39 | '80%': { transform: 'translate(2px, 2px)' },
40 | '100%': { transform: 'translate(0)' },
41 | },
42 | ghost: {
43 | '0%': { opacity: '0' },
44 | '50%': { opacity: '1' },
45 | '100%': { opacity: '0' },
46 | },
47 | flash: {
48 | '0%': { backgroundColor: 'rgba(255, 255, 255, 0)' },
49 | '50%': { backgroundColor: 'rgba(255, 255, 255, 0.5)' },
50 | '100%': { backgroundColor: 'rgba(255, 255, 255, 0)' },
51 | },
52 | crack1: {
53 | '0%': {
54 | transform: 'scale(1)',
55 | opacity: '1',
56 | },
57 | '20%': {
58 | transform: 'scale(1.05)',
59 | opacity: '0.8',
60 | },
61 | '40%': {
62 | transform: 'scale(1)',
63 | opacity: '0.6',
64 | },
65 | '60%': {
66 | transform: 'scale(0.95)',
67 | opacity: '0.4',
68 | },
69 | '80%': {
70 | transform: 'scale(1)',
71 | opacity: '0.2',
72 | },
73 | '100%': {
74 | transform: 'scale(1)',
75 | opacity: '0',
76 | },
77 | },
78 | darken: {
79 | '0%': { backgroundColor: 'rgba(0, 0, 0, 0)' },
80 | '100%': { backgroundColor: 'rgba(0, 0, 0, 0.7)' },
81 | },
82 | crack: {
83 | '0%': { backgroundSize: '100%', opacity: '1' },
84 | '50%': { backgroundSize: '120%', opacity: '1' },
85 | '100%': { backgroundSize: '100%', opacity: '0' },
86 | },
87 | loadingBar: {
88 | '0%': { transform: 'translateX(-100%)' },
89 | '50%': { transform: 'translateX(0%)' },
90 | '100%': { transform: 'translateX(100%)' },
91 | }
92 | },
93 | animation: {
94 | blink: 'blink 1s step-start infinite',
95 | flicker: 'flicker 1.5s infinite',
96 | glitch: 'glitch 1s infinite',
97 | ghost: 'ghost 3s ease-in-out infinite',
98 | flash: 'flash 0.5s ease-in-out', // Add your flash animation here
99 | crack: 'crack 0.6s ease-in-out forwards',
100 | darken: 'darken 1s forwards',
101 | loadingBar: 'loadingBar 2s ease-in-out infinite',
102 | },
103 | },
104 | },
105 |
106 | variants: {
107 | extend: {
108 | visibility: ['group-hover'],
109 | },
110 | },
111 | plugins: [require('@tailwindcss/typography')],
112 | };
113 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "types": ["vitest/globals"],
18 | "paths": {
19 | "@/*": ["./*"]
20 | }
21 | },
22 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
23 | "exclude": ["node_modules"]
24 | }
25 |
--------------------------------------------------------------------------------
/types/chat.ts:
--------------------------------------------------------------------------------
1 | export interface Message {
2 | id?: string;
3 | role: Role;
4 | content: string;
5 | intermediateSteps?: any;
6 | humanInteractionMessages?: any;
7 | errorMessages?: any;
8 | }
9 |
10 | export type Role = 'assistant' | 'user' | 'agent' | 'system';
11 |
12 | export interface ChatBody {
13 | chatCompletionURL?: string,
14 | messages?: Message[],
15 | additionalProps?: any
16 | }
17 |
18 | export interface Conversation {
19 | id: string;
20 | name: string;
21 | messages: Message[];
22 | folderId: string | null;
23 | }
24 |
--------------------------------------------------------------------------------
/types/data.ts:
--------------------------------------------------------------------------------
1 | export interface KeyValuePair {
2 | key: string;
3 | value: any;
4 | }
5 |
--------------------------------------------------------------------------------
/types/env.ts:
--------------------------------------------------------------------------------
1 | export interface ProcessEnv {
2 |
3 | }
4 |
--------------------------------------------------------------------------------
/types/error.ts:
--------------------------------------------------------------------------------
1 | export interface ErrorMessage {
2 | code: String | null;
3 | title: String;
4 | messageLines: String[];
5 | }
6 |
--------------------------------------------------------------------------------
/types/export.ts:
--------------------------------------------------------------------------------
1 | import { Conversation, Message } from './chat';
2 | import { FolderInterface } from './folder';
3 | import { Prompt } from './prompt';
4 |
5 | export type SupportedExportFormats =
6 | | ExportFormatV1
7 | | ExportFormatV2
8 | | ExportFormatV3
9 | | ExportFormatV4;
10 | export type LatestExportFormat = ExportFormatV4;
11 |
12 | ////////////////////////////////////////////////////////////////////////////////////////////
13 | interface ConversationV1 {
14 | id: number;
15 | name: string;
16 | messages: Message[];
17 | }
18 |
19 | export type ExportFormatV1 = ConversationV1[];
20 |
21 | ////////////////////////////////////////////////////////////////////////////////////////////
22 | interface ChatFolder {
23 | id: number;
24 | name: string;
25 | }
26 |
27 | export interface ExportFormatV2 {
28 | history: Conversation[] | null;
29 | folders: ChatFolder[] | null;
30 | }
31 |
32 | ////////////////////////////////////////////////////////////////////////////////////////////
33 | export interface ExportFormatV3 {
34 | version: 3;
35 | history: Conversation[];
36 | folders: FolderInterface[];
37 | }
38 |
39 | export interface ExportFormatV4 {
40 | version: 4;
41 | history: Conversation[];
42 | folders: FolderInterface[];
43 | prompts: Prompt[];
44 | }
45 |
--------------------------------------------------------------------------------
/types/folder.ts:
--------------------------------------------------------------------------------
1 | export interface FolderInterface {
2 | id: string;
3 | name: string;
4 | type: FolderType;
5 | }
6 |
7 | export type FolderType = 'chat' | 'prompt';
8 |
--------------------------------------------------------------------------------
/types/index.ts:
--------------------------------------------------------------------------------
1 | export {};
2 |
--------------------------------------------------------------------------------
/types/settings.ts:
--------------------------------------------------------------------------------
1 | export interface Settings {
2 | theme: 'light' | 'dark';
3 | }
4 |
--------------------------------------------------------------------------------
/types/storage.ts:
--------------------------------------------------------------------------------
1 | import { Conversation } from './chat';
2 | import { FolderInterface } from './folder';
3 | import { Prompt } from './prompt';
4 |
5 | // keep track of local storage schema
6 | export interface LocalStorage {
7 | conversationHistory: Conversation[];
8 | selectedConversation: Conversation;
9 | theme: 'light' | 'dark';
10 | // added folders (3/23/23)
11 | folders: FolderInterface[];
12 | // added prompts (3/26/23)
13 | prompts: Prompt[];
14 | // added showChatbar and showPromptbar (3/26/23)
15 | showChatbar: boolean;
16 | showPromptbar: boolean;
17 | }
18 |
--------------------------------------------------------------------------------
/utils/app/api.ts:
--------------------------------------------------------------------------------
1 | import { nextEndPoints } from "./const";
2 |
3 | export const getEndpoint = ({ service = 'chat'}) => {
4 | return nextEndPoints[service]
5 | };
6 |
7 |
--------------------------------------------------------------------------------
/utils/app/clean.ts:
--------------------------------------------------------------------------------
1 | import { Conversation } from '@/types/chat';
2 |
3 | export const cleanSelectedConversation = (conversation: Conversation) => {
4 | let updatedConversation = conversation;
5 |
6 | if (!updatedConversation.folderId) {
7 | updatedConversation = {
8 | ...updatedConversation,
9 | folderId: updatedConversation.folderId || null,
10 | };
11 | }
12 |
13 | if (!updatedConversation.messages) {
14 | updatedConversation = {
15 | ...updatedConversation,
16 | messages: updatedConversation.messages || [],
17 | };
18 | }
19 |
20 | return updatedConversation;
21 | };
22 |
23 | export const cleanConversationHistory = (history: any[]): Conversation[] => {
24 |
25 | if (!Array.isArray(history)) {
26 | console.warn('history is not an array. Returning an empty array.');
27 | return [];
28 | }
29 |
30 | return history.reduce((acc: any[], conversation) => {
31 | try {
32 | if (!conversation.folderId) {
33 | conversation.folderId = null;
34 | }
35 |
36 | if (!conversation.messages) {
37 | conversation.messages = [];
38 | }
39 |
40 | acc.push(conversation);
41 | return acc;
42 | } catch (error) {
43 | console.warn(
44 | `error while cleaning conversations' history. Removing culprit`,
45 | error,
46 | );
47 | }
48 | return acc;
49 | }, []);
50 | };
51 |
--------------------------------------------------------------------------------
/utils/app/codeblock.ts:
--------------------------------------------------------------------------------
1 | interface languageMap {
2 | [key: string]: string | undefined;
3 | }
4 |
5 | export const programmingLanguages: languageMap = {
6 | javascript: '.js',
7 | python: '.py',
8 | java: '.java',
9 | c: '.c',
10 | cpp: '.cpp',
11 | 'c++': '.cpp',
12 | 'c#': '.cs',
13 | ruby: '.rb',
14 | php: '.php',
15 | swift: '.swift',
16 | 'objective-c': '.m',
17 | kotlin: '.kt',
18 | typescript: '.ts',
19 | go: '.go',
20 | perl: '.pl',
21 | rust: '.rs',
22 | scala: '.scala',
23 | haskell: '.hs',
24 | lua: '.lua',
25 | shell: '.sh',
26 | sql: '.sql',
27 | html: '.html',
28 | css: '.css',
29 | // add more file extensions here, make sure the key is same as language prop in CodeBlock.tsx component
30 | };
31 |
32 | export const generateRandomString = (length: number, lowercase = false) => {
33 | const chars = 'ABCDEFGHJKLMNPQRSTUVWXY3456789'; // excluding similar looking characters like Z, 2, I, 1, O, 0
34 | let result = '';
35 | for (let i = 0; i < length; i++) {
36 | result += chars.charAt(Math.floor(Math.random() * chars.length));
37 | }
38 | return lowercase ? result.toLowerCase() : result;
39 | };
40 |
--------------------------------------------------------------------------------
/utils/app/const.ts:
--------------------------------------------------------------------------------
1 | export const nextEndPoints = {
2 | chat: 'api/chat',
3 | };
4 |
5 | export const webSocketMessageTypes = {
6 | userMessage: 'user_message',
7 | userInteractionMessage: 'user_interaction_message',
8 | systemResponseMessage: 'system_response_message',
9 | systemIntermediateMessage: 'system_intermediate_message',
10 | systemInteractionMessage: 'system_interaction_message'
11 | }
12 |
13 | export const appConfig = {
14 | fileUploadEnabled : false
15 | }
--------------------------------------------------------------------------------
/utils/app/conversation.ts:
--------------------------------------------------------------------------------
1 | import { Conversation, Role } from '@/types/chat';
2 | import toast from 'react-hot-toast';
3 |
4 | export const updateConversation = (
5 | updatedConversation: Conversation,
6 | allConversations: Conversation[],
7 | ) => {
8 | const updatedConversations = allConversations.map((c) => {
9 | if (c.id === updatedConversation.id) {
10 | return updatedConversation;
11 | }
12 |
13 | return c;
14 | });
15 |
16 | saveConversation(updatedConversation);
17 | saveConversations(updatedConversations);
18 |
19 | return {
20 | single: updatedConversation,
21 | all: updatedConversations,
22 | };
23 | };
24 |
25 | export const saveConversation = (conversation: Conversation) => {
26 | try {
27 | sessionStorage.setItem('selectedConversation', JSON.stringify(conversation));
28 | } catch (error) {
29 | if (error instanceof DOMException && error.name === 'QuotaExceededError') {
30 | console.log('Storage quota exceeded, cannot save conversation.');
31 | toast.error('Storage quota exceeded, cannot save conversation.');
32 | }
33 | }
34 | };
35 |
36 | export const saveConversations = (conversations: Conversation[]) => {
37 | try {
38 | sessionStorage.setItem('conversationHistory', JSON.stringify(conversations));
39 | } catch (error) {
40 | if (error instanceof DOMException && error.name === 'QuotaExceededError') {
41 | console.log('Storage quota exceeded, cannot save conversations.');
42 | toast.error('Storage quota exceeded, cannot save conversation.');
43 | }
44 | }
45 | };
46 |
47 |
--------------------------------------------------------------------------------
/utils/app/folders.ts:
--------------------------------------------------------------------------------
1 | import { FolderInterface } from '@/types/folder';
2 |
3 | export const saveFolders = (folders: FolderInterface[]) => {
4 | sessionStorage.setItem('folders', JSON.stringify(folders));
5 | };
6 |
--------------------------------------------------------------------------------
/utils/app/helper.ts:
--------------------------------------------------------------------------------
1 | import { v4 as uuidv4 } from 'uuid';
2 | import { env } from 'next-runtime-env'
3 | export const getInitials = (fullName = '') => {
4 | if (!fullName) {
5 | return "";
6 | }
7 | const initials = fullName.split(' ').map(name => name[0]).join('').toUpperCase();
8 | return initials;
9 | }
10 | export const compressImage = (base64: string, mimeType: string | undefined, shouldCompress: boolean, callback: { (compressedBase64: string): void; (arg0: string): void; }) => {
11 | const MAX_SIZE = 200 * 1024; // 200 KB maximum size
12 | const MIN_SIZE = 100 * 1024; // 100 KB minimum size, to avoid under compression
13 | const canvas = document.createElement('canvas');
14 | const ctx = canvas.getContext('2d');
15 | const img = new Image();
16 |
17 | img.onload = () => {
18 | let width = img.width;
19 | let height = img.height;
20 | const maxSize = 800; // Start with a larger size for initial scaling
21 |
22 | if (width > maxSize || height > maxSize) {
23 | if (width > height) {
24 | height *= maxSize / width;
25 | width = maxSize;
26 | } else {
27 | width *= maxSize / height;
28 | height = maxSize;
29 | }
30 | }
31 |
32 | canvas.width = width;
33 | canvas.height = height;
34 | ctx.drawImage(img, 0, 0, width, height);
35 |
36 | let quality = 0.9; // Start with high quality
37 | let newDataUrl = canvas.toDataURL(mimeType, quality);
38 |
39 | if (shouldCompress) {
40 | while (newDataUrl.length > MAX_SIZE && quality > 0.1) {
41 | quality -= 0.05; // Gradually reduce quality
42 | newDataUrl = canvas.toDataURL(mimeType, quality);
43 | }
44 |
45 | // Check if overly compressed, then adjust quality slightly back up
46 | while (newDataUrl.length < MIN_SIZE && quality <= 0.9) {
47 | quality += 0.05; // Increment quality slightly
48 | newDataUrl = canvas.toDataURL(mimeType, quality);
49 | }
50 |
51 | // Further dimension reduction if still too large
52 | while (newDataUrl.length > MAX_SIZE && (width > 50 || height > 50)) {
53 | width *= 0.75; // Reduce dimensions
54 | height *= 0.75;
55 | canvas.width = width;
56 | canvas.height = height;
57 | ctx.drawImage(img, 0, 0, width, height);
58 | newDataUrl = canvas.toDataURL(mimeType, quality);
59 | }
60 | }
61 |
62 | // console.log(`Original Base64 Size: ${base64.length / 1024} KB`);
63 | // console.log(`Compressed Base64 Size: ${newDataUrl.length / 1024} KB`);
64 | callback(newDataUrl);
65 | };
66 |
67 | img.src = base64;
68 | }
69 |
70 | export const getURLQueryParam = ({ param = '' }) => {
71 | // Get the URL query parameters safely
72 | const urlParams = new URLSearchParams(window?.location?.search);
73 |
74 | if (param) {
75 | // Get the value of a specific query parameter
76 | return urlParams.get(param);
77 | } else {
78 | // Get all query params safely
79 | const paramsObject = Object.create(null); // Prevent prototype pollution
80 | for (const [key, value] of urlParams?.entries()) {
81 | if (Object.prototype.hasOwnProperty.call(paramsObject, key)) continue; // Extra safety check
82 | paramsObject[key] = value;
83 | }
84 | return paramsObject;
85 | }
86 | };
87 |
88 |
89 | export const getWorkflowName = () => {
90 | const workflow = getURLQueryParam({ param: 'workflow' }) || env('NEXT_PUBLIC_WORKFLOW') || process?.env?.NEXT_PUBLIC_WORKFLOW || 'AIQ Toolkit';
91 | return workflow
92 | }
93 |
94 | export const setSessionError = (message = 'unknown error') => {
95 | sessionStorage.setItem('error', 'true');
96 | sessionStorage.setItem('errorMessage', message);
97 | }
98 |
99 | export const removeSessionError = () => {
100 | sessionStorage.removeItem('error');
101 | sessionStorage.removeItem('errorMessage');
102 | }
103 |
104 | export const isInsideIframe = () => {
105 | try {
106 | return window?.self !== window?.top;
107 | } catch (e) {
108 | // If a security error occurs (cross-origin), assume it's in an iframe
109 | return true;
110 | }
111 | };
112 |
113 | export const fetchLastMessage = ({messages = [], role = 'user'}) => {
114 | // Loop from the end to find the last message with the role "user"
115 | for (let i = messages.length - 1; i >= 0; i--) {
116 | if (messages[i]?.role === role) {
117 | return messages[i]; // Return the content of the last user message
118 | }
119 | }
120 | return null; // Return null if no user message is found
121 | }
122 |
123 | export const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
124 |
125 | interface IntermediateStep {
126 | id: string;
127 | parent_id?: string;
128 | index?: number;
129 | content?: any;
130 | intermediate_steps?: IntermediateStep[];
131 | [key: string]: any; // For any additional properties
132 | }
133 |
134 | export const processIntermediateMessage = (
135 | existingSteps: IntermediateStep[] = [],
136 | newMessage: IntermediateStep = {} as IntermediateStep,
137 | intermediateStepOverride = true
138 | ): IntermediateStep[] => {
139 |
140 | if (!newMessage.id) {
141 | console.log('Skipping message processing - no message ID provided');
142 | return existingSteps;
143 | }
144 |
145 | // Helper function to find and replace a message in the steps tree
146 | const replaceMessage = (steps: IntermediateStep[]): boolean => {
147 | for (let i = 0; i < steps.length; i++) {
148 | if (steps[i].id === newMessage.id && steps[i].content?.name === newMessage.content?.name) {
149 | // Preserve the index when overriding
150 | steps[i] = {
151 | ...newMessage,
152 | index: steps[i].index
153 | };
154 | return true;
155 | }
156 |
157 | // Recursively check intermediate steps
158 | const intermediateSteps = steps[i].intermediate_steps;
159 | if (intermediateSteps && intermediateSteps.length > 0) {
160 | if (replaceMessage(intermediateSteps)) {
161 | return true;
162 | }
163 | }
164 | }
165 | return false;
166 | };
167 |
168 | // Helper function to find a parent step by ID
169 | const findParentStep = (steps: IntermediateStep[], parentId: string): IntermediateStep | null => {
170 | for (const step of steps) {
171 | if (step.id === parentId) {
172 | return step;
173 | }
174 | const intermediateSteps = step.intermediate_steps;
175 | if (intermediateSteps && intermediateSteps.length > 0) {
176 | const found = findParentStep(intermediateSteps, parentId);
177 | if (found) return found;
178 | }
179 | }
180 | return null;
181 | };
182 |
183 | try {
184 | // If override is enabled and message exists, try to replace it
185 | if (intermediateStepOverride) {
186 | const wasReplaced = replaceMessage(existingSteps);
187 | if (wasReplaced) {
188 | return existingSteps;
189 | }
190 | }
191 |
192 | // If message wasn't replaced or override is disabled, add it to the appropriate place
193 | if (newMessage.parent_id) {
194 | const parentStep = findParentStep(existingSteps, newMessage.parent_id);
195 | if (parentStep) {
196 | // Initialize intermediate_steps array if it doesn't exist
197 | if (!parentStep.intermediate_steps) {
198 | parentStep.intermediate_steps = [];
199 | }
200 | parentStep.intermediate_steps.push(newMessage);
201 | return existingSteps;
202 | }
203 | }
204 |
205 | // If no parent found or no parent_id, add to root level
206 | existingSteps.push(newMessage);
207 | return existingSteps;
208 |
209 | } catch (error) {
210 | console.log('Error in processIntermediateMessage:', {
211 | error,
212 | messageId: newMessage.id,
213 | parentId: newMessage.parent_id
214 | });
215 | return existingSteps;
216 | }
217 | };
218 |
219 | export const escapeHtml = (str: string): string => {
220 | try {
221 | if (typeof str !== 'string') {
222 | throw new TypeError('Input must be a string');
223 | }
224 | return str.replace(/&/g, '&')
225 | .replace(//g, '>')
227 | .replace(/"/g, '"')
228 | .replace(/'/g, ''');
229 | } catch (error) {
230 | console.error('Error in escapeHtml:', error);
231 | return ''; // Return an empty string in case of error
232 | }
233 | };
234 |
235 | export const convertBackticksToPreCode = (markdown = '') => {
236 | try {
237 | if (typeof markdown !== 'string') {
238 | throw new TypeError('Input must be a string');
239 | }
240 |
241 | // Step 1: Convert code blocks first
242 | markdown = markdown.replace(
243 | /```(\w+)?\n([\s\S]*?)\n```/g,
244 | (_, lang, code) => {
245 | const languageClass = lang ? ` class="language-${lang}"` : '';
246 | const escapedCode = escapeHtml(code);
247 | return `\n${escapedCode}
\n`;
248 | }
249 | );
250 |
251 | // Step 2: Convert bold text **bold**
252 | markdown = markdown.replace(/\*\*(.*?)\*\*/g, '$1 ');
253 |
254 | return markdown;
255 | } catch (error) {
256 | console.error('Error in convertBackticksToPreCode:', error);
257 | return markdown;
258 | }
259 | };
260 |
261 | export const generateContentIntermediate = (intermediateSteps: IntermediateStep[] = []): string => {
262 | const generateDetails = (data: IntermediateStep[]): string => {
263 | try {
264 | if (!Array.isArray(data)) {
265 | throw new TypeError('Input must be an array');
266 | }
267 | return data.map((item) => {
268 | const currentId = item.id;
269 | const currentIndex = item.index;
270 | const sanitizedPayload = convertBackticksToPreCode(item.content?.payload || '');
271 | let details = `\n`;
272 | details += ` ${item.content?.name || ''} \n`;
273 |
274 | details += `\n${sanitizedPayload}\n`;
275 |
276 | if (item.intermediate_steps && item.intermediate_steps.length > 0) {
277 | details += generateDetails(item.intermediate_steps);
278 | }
279 |
280 | details += ` \n`;
281 | return details;
282 | }).join('');
283 | } catch (error) {
284 | console.error('error in generateDetails:', error);
285 | return ''; // Return an empty string in case of error
286 | }
287 | };
288 |
289 | try {
290 | if (!Array.isArray(intermediateSteps) || intermediateSteps.length === 0) {
291 | return '';
292 | }
293 | let intermediateContent = generateDetails(intermediateSteps);
294 | const firstStep = intermediateSteps[0];
295 | if (firstStep && firstStep.parent_id) {
296 | intermediateContent = `Intermediate Steps \n${intermediateContent} `;
297 | }
298 | if (/(?:\\)?```/.test(intermediateContent)) {
299 | intermediateContent = intermediateContent.replace(/\n{2,}/g, '\n');
300 | }
301 | return intermediateContent;
302 | } catch (error) {
303 | console.error('error in generateIntermediateMarkdown:', error);
304 | return '';
305 | }
306 | };
307 |
308 | export const replaceMalformedMarkdownImages = (str = '') => {
309 | return str.replace(/!\[.*?\]\(([^)]*)$/, (match) => {
310 | return ` `;
311 | });
312 | }
313 |
314 | export const replaceMalformedHTMLImages = (str = '') => {
315 | return str.replace(/ ]*$/, (match) => {
316 | return ` `;
317 | });
318 | }
319 |
320 | export const replaceMalformedHTMLVideos = (str = '') => {
321 | return str.replace(/]*$/, (match) => {
322 | return `
323 |
324 | Your browser does not support the video tag.
325 | `;
326 | });
327 | }
328 |
329 |
330 | export const fixMalformedHtml = (content = '') => {
331 | try {
332 |
333 | let fixed = replaceMalformedHTMLImages(content);
334 | fixed = replaceMalformedHTMLVideos(fixed);
335 | fixed = replaceMalformedMarkdownImages(fixed);
336 | return fixed;
337 |
338 | // Sanitize content
339 | // let sanitizedContent = DOMPurify.sanitize(content);
340 |
341 | // // Fallback for empty or fully stripped content
342 | // if (!sanitizedContent) {
343 | // return sanitizedContent = ` `;
344 | // }
345 |
346 | // const fixed = replaceMalformedMarkdownImages(sanitizedContent);
347 | // return fixed;
348 |
349 | // let dirtyHtml = marked(content);
350 | // // remove and
tags to reveal malformed img or other html tags
351 | // dirtyHtml = dirtyHtml.replace(//g, "\n");
352 | // dirtyHtml = dirtyHtml.replace(/<\/p>/g, "");
353 | // console.log(dirtyHtml);
354 | // const cleanHtml = DOMPurify.sanitize(dirtyHtml);
355 | // if(!cleanHtml) {
356 | // return ` `
357 | // }
358 | // return sanitizedContent;
359 | }
360 | catch (e) {
361 | console.log("error - sanitizing content", e);
362 | return content; // Return original if fixing fails
363 | }
364 | };
365 |
366 |
367 |
368 |
--------------------------------------------------------------------------------
/utils/app/importExport.ts:
--------------------------------------------------------------------------------
1 | import { Conversation } from '@/types/chat';
2 | import {
3 | ExportFormatV1,
4 | ExportFormatV2,
5 | ExportFormatV3,
6 | ExportFormatV4,
7 | LatestExportFormat,
8 | SupportedExportFormats,
9 | } from '@/types/export';
10 | import { FolderInterface } from '@/types/folder';
11 | import { Prompt } from '@/types/prompt';
12 |
13 | import { cleanConversationHistory } from './clean';
14 |
15 | export function isExportFormatV1(obj: any): obj is ExportFormatV1 {
16 | return Array.isArray(obj);
17 | }
18 |
19 | export function isExportFormatV2(obj: any): obj is ExportFormatV2 {
20 | return !('version' in obj) && 'folders' in obj && 'history' in obj;
21 | }
22 |
23 | export function isExportFormatV3(obj: any): obj is ExportFormatV3 {
24 | return obj.version === 3;
25 | }
26 |
27 | export function isExportFormatV4(obj: any): obj is ExportFormatV4 {
28 | return obj.version === 4;
29 | }
30 |
31 | export const isLatestExportFormat = isExportFormatV4;
32 |
33 | export function cleanData(data: SupportedExportFormats): LatestExportFormat {
34 | if (isExportFormatV1(data)) {
35 | return {
36 | version: 4,
37 | history: cleanConversationHistory(data),
38 | folders: [],
39 | prompts: [],
40 | };
41 | }
42 |
43 | if (isExportFormatV2(data)) {
44 | return {
45 | version: 4,
46 | history: cleanConversationHistory(data.history || []),
47 | folders: (data.folders || []).map((chatFolder) => ({
48 | id: chatFolder.id.toString(),
49 | name: chatFolder.name,
50 | type: 'chat',
51 | })),
52 | prompts: [],
53 | };
54 | }
55 |
56 | if (isExportFormatV3(data)) {
57 | return { ...data, version: 4, prompts: [] };
58 | }
59 |
60 | if (isExportFormatV4(data)) {
61 | return data;
62 | }
63 |
64 | throw new Error('Unsupported data format');
65 | }
66 |
67 | function currentDate() {
68 | const date = new Date();
69 | const month = date.getMonth() + 1;
70 | const day = date.getDate();
71 | return `${month}-${day}`;
72 | }
73 |
74 | export const exportData = () => {
75 | let history = sessionStorage.getItem('conversationHistory');
76 | let folders = sessionStorage.getItem('folders');
77 | let prompts = sessionStorage.getItem('prompts');
78 |
79 | if (history) {
80 | history = JSON.parse(history);
81 | }
82 |
83 | if (folders) {
84 | folders = JSON.parse(folders);
85 | }
86 |
87 | if (prompts) {
88 | prompts = JSON.parse(prompts);
89 | }
90 |
91 | const data = {
92 | version: 4,
93 | history: history || [],
94 | folders: folders || [],
95 | prompts: prompts || [],
96 | } as LatestExportFormat;
97 |
98 | const blob = new Blob([JSON.stringify(data, null, 2)], {
99 | type: 'application/json',
100 | });
101 | const url = URL.createObjectURL(blob);
102 | const link = document.createElement('a');
103 | link.download = `chatbot_ui_history_${currentDate()}.json`;
104 | link.href = url;
105 | link.style.display = 'none';
106 | document.body.appendChild(link);
107 | link.click();
108 | document.body.removeChild(link);
109 | URL.revokeObjectURL(url);
110 | };
111 |
112 | export const importData = (
113 | data: SupportedExportFormats,
114 | ): LatestExportFormat => {
115 | const { history, folders, prompts } = cleanData(data);
116 |
117 | const oldConversations = sessionStorage.getItem('conversationHistory');
118 | const oldConversationsParsed = oldConversations
119 | ? JSON.parse(oldConversations)
120 | : [];
121 |
122 | const newHistory: Conversation[] = [
123 | ...oldConversationsParsed,
124 | ...history,
125 | ].filter(
126 | (conversation, index, self) =>
127 | index === self.findIndex((c) => c.id === conversation.id),
128 | );
129 | sessionStorage.setItem('conversationHistory', JSON.stringify(newHistory));
130 | if (newHistory.length > 0) {
131 | sessionStorage.setItem(
132 | 'selectedConversation',
133 | JSON.stringify(newHistory[newHistory.length - 1]),
134 | );
135 | } else {
136 | sessionStorage.removeItem('selectedConversation');
137 | }
138 |
139 | const oldFolders = sessionStorage.getItem('folders');
140 | const oldFoldersParsed = oldFolders ? JSON.parse(oldFolders) : [];
141 | const newFolders: FolderInterface[] = [
142 | ...oldFoldersParsed,
143 | ...folders,
144 | ].filter(
145 | (folder, index, self) =>
146 | index === self.findIndex((f) => f.id === folder.id),
147 | );
148 | sessionStorage.setItem('folders', JSON.stringify(newFolders));
149 |
150 | const oldPrompts = sessionStorage.getItem('prompts');
151 | const oldPromptsParsed = oldPrompts ? JSON.parse(oldPrompts) : [];
152 | const newPrompts: Prompt[] = [...oldPromptsParsed, ...prompts].filter(
153 | (prompt, index, self) =>
154 | index === self.findIndex((p) => p.id === prompt.id),
155 | );
156 | sessionStorage.setItem('prompts', JSON.stringify(newPrompts));
157 |
158 | return {
159 | version: 4,
160 | history: newHistory,
161 | folders: newFolders,
162 | prompts: newPrompts,
163 | };
164 | };
165 |
--------------------------------------------------------------------------------
/utils/app/prompts.ts:
--------------------------------------------------------------------------------
1 | import { Prompt } from '@/types/prompt';
2 |
3 | export const updatePrompt = (updatedPrompt: Prompt, allPrompts: Prompt[]) => {
4 | const updatedPrompts = allPrompts.map((c) => {
5 | if (c.id === updatedPrompt.id) {
6 | return updatedPrompt;
7 | }
8 |
9 | return c;
10 | });
11 |
12 | savePrompts(updatedPrompts);
13 |
14 | return {
15 | single: updatedPrompt,
16 | all: updatedPrompts,
17 | };
18 | };
19 |
20 | export const savePrompts = (prompts: Prompt[]) => {
21 | sessionStorage.setItem('prompts', JSON.stringify(prompts));
22 | };
23 |
--------------------------------------------------------------------------------
/utils/app/settings.ts:
--------------------------------------------------------------------------------
1 | import { Settings } from '@/types/settings';
2 |
3 | const STORAGE_KEY = 'settings';
4 |
5 | export const getSettings = (): Settings => {
6 | let settings: Settings = {
7 | theme: 'light',
8 | };
9 | const settingsJson = sessionStorage.getItem(STORAGE_KEY);
10 | if (settingsJson) {
11 | try {
12 | let savedSettings = JSON.parse(settingsJson) as Settings;
13 | settings = Object.assign(settings, savedSettings);
14 | } catch (e) {
15 | console.error(e);
16 | }
17 | }
18 | return settings;
19 | };
20 |
21 | export const saveSettings = (settings: Settings) => {
22 | sessionStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
23 | };
24 |
--------------------------------------------------------------------------------
/utils/data/throttle.ts:
--------------------------------------------------------------------------------
1 | export function throttle any>(
2 | func: T,
3 | limit: number,
4 | ): T {
5 | let lastFunc: ReturnType;
6 | let lastRan: number;
7 |
8 | return ((...args) => {
9 | if (!lastRan) {
10 | func(...args);
11 | lastRan = Date.now();
12 | } else {
13 | clearTimeout(lastFunc);
14 | lastFunc = setTimeout(() => {
15 | if (Date.now() - lastRan >= limit) {
16 | func(...args);
17 | lastRan = Date.now();
18 | }
19 | }, limit - (Date.now() - lastRan));
20 | }
21 | }) as T;
22 | }
23 |
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import { defineConfig } from 'vite';
3 |
4 | export default defineConfig({
5 | resolve: {
6 | alias: {
7 | '@': path.resolve(__dirname, './'),
8 | },
9 | },
10 | });
11 |
--------------------------------------------------------------------------------