diff --git a/.env.local b/.env.local
new file mode 100644
index 0000000000..8144cbad6f
--- /dev/null
+++ b/.env.local
@@ -0,0 +1,36 @@
+# Supabase Public
+NEXT_PUBLIC_SUPABASE_URL=https://xfvdikreoaorkfbhibgu.supabase.co
+NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0
+
+# Supabase Private
+SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InhmdmRpa3Jlb2FvcmtmYmhpYmd1Iiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTcyODIwNzk1MywiZXhwIjoyMDQzNzgzOTUzfQ.LN1n_e2DTmDiw4WRibdTpGbHKB5SwGKEzkoh_lAUUek
+
+# Ollama
+NEXT_PUBLIC_OLLAMA_URL=http://localhost:11434
+
+# API Keys (Optional: Entering an API key here overrides the API keys globally for all users.)
+OPENAI_API_KEY=
+ANTHROPIC_API_KEY=
+GOOGLE_GEMINI_API_KEY=
+MISTRAL_API_KEY=
+GROQ_API_KEY=
+PERPLEXITY_API_KEY=
+OPENROUTER_API_KEY=
+
+# OpenAI API Information
+NEXT_PUBLIC_OPENAI_ORGANIZATION_ID=
+
+# Azure API Information
+AZURE_OPENAI_API_KEY=
+AZURE_OPENAI_ENDPOINT=
+AZURE_GPT_35_TURBO_NAME=
+AZURE_GPT_45_VISION_NAME=
+AZURE_GPT_45_TURBO_NAME=
+AZURE_EMBEDDINGS_NAME=
+
+# General Configuration (Optional)
+EMAIL_DOMAIN_WHITELIST=
+EMAIL_WHITELIST=
+
+# File size limit for uploads in bytes
+NEXT_PUBLIC_USER_FILE_SIZE_LIMIT=10485760
\ No newline at end of file
diff --git a/chatbot-ui-main/.env.local.example b/chatbot-ui-main/.env.local.example
new file mode 100644
index 0000000000..7cff1a7118
--- /dev/null
+++ b/chatbot-ui-main/.env.local.example
@@ -0,0 +1,36 @@
+# Supabase Public
+NEXT_PUBLIC_SUPABASE_URL=
+NEXT_PUBLIC_SUPABASE_ANON_KEY=
+
+# Supabase Private
+SUPABASE_SERVICE_ROLE_KEY=
+
+# Ollama
+NEXT_PUBLIC_OLLAMA_URL=http://localhost:11434
+
+# API Keys (Optional: Entering an API key here overrides the API keys globally for all users.)
+OPENAI_API_KEY=
+ANTHROPIC_API_KEY=
+GOOGLE_GEMINI_API_KEY=
+MISTRAL_API_KEY=
+GROQ_API_KEY=
+PERPLEXITY_API_KEY=
+OPENROUTER_API_KEY=
+
+# OpenAI API Information
+NEXT_PUBLIC_OPENAI_ORGANIZATION_ID=
+
+# Azure API Information
+AZURE_OPENAI_API_KEY=
+AZURE_OPENAI_ENDPOINT=
+AZURE_GPT_35_TURBO_NAME=
+AZURE_GPT_45_VISION_NAME=
+AZURE_GPT_45_TURBO_NAME=
+AZURE_EMBEDDINGS_NAME=
+
+# General Configuration (Optional)
+EMAIL_DOMAIN_WHITELIST=
+EMAIL_WHITELIST=
+
+# File size limit for uploads in bytes
+NEXT_PUBLIC_USER_FILE_SIZE_LIMIT=10485760
\ No newline at end of file
diff --git a/chatbot-ui-main/.eslintrc.json b/chatbot-ui-main/.eslintrc.json
new file mode 100644
index 0000000000..6ec5479fec
--- /dev/null
+++ b/chatbot-ui-main/.eslintrc.json
@@ -0,0 +1,25 @@
+{
+ "$schema": "https://json.schemastore.org/eslintrc",
+ "root": true,
+ "extends": [
+ "next/core-web-vitals",
+ "prettier",
+ "plugin:tailwindcss/recommended"
+ ],
+ "plugins": ["tailwindcss"],
+ "rules": {
+ "tailwindcss/no-custom-classname": "off"
+ },
+ "settings": {
+ "tailwindcss": {
+ "callees": ["cn", "cva"],
+ "config": "tailwind.config.js"
+ }
+ },
+ "overrides": [
+ {
+ "files": ["*.ts", "*.tsx"],
+ "parser": "@typescript-eslint/parser"
+ }
+ ]
+}
diff --git a/chatbot-ui-main/.github/funding.yaml b/chatbot-ui-main/.github/funding.yaml
new file mode 100644
index 0000000000..d6494ef5b2
--- /dev/null
+++ b/chatbot-ui-main/.github/funding.yaml
@@ -0,0 +1,3 @@
+# If you find my open-source work helpful, please consider sponsoring me!
+
+github: mckaywrigley
diff --git a/chatbot-ui-main/.gitignore b/chatbot-ui-main/.gitignore
new file mode 100644
index 0000000000..f56e22be97
--- /dev/null
+++ b/chatbot-ui-main/.gitignore
@@ -0,0 +1,46 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+.yarn/install-state.gz
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env
+.env*.local
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
+
+.VSCodeCounter
+tool-schemas
+custom-prompts
+
+sw.js
+sw.js.map
+workbox-*.js
+workbox-*.js.map
diff --git a/chatbot-ui-main/.husky/pre-commit b/chatbot-ui-main/.husky/pre-commit
new file mode 100644
index 0000000000..3ebdff6310
--- /dev/null
+++ b/chatbot-ui-main/.husky/pre-commit
@@ -0,0 +1,5 @@
+#!/usr/bin/env sh
+
+. "$(dirname -- "$0")/_/husky.sh"
+
+npm run lint:fix && npm run format:write && git add .
diff --git a/chatbot-ui-main/.nvmrc b/chatbot-ui-main/.nvmrc
new file mode 100644
index 0000000000..7ea6a59d34
--- /dev/null
+++ b/chatbot-ui-main/.nvmrc
@@ -0,0 +1 @@
+v20.11.0
diff --git a/chatbot-ui-main/README.md b/chatbot-ui-main/README.md
new file mode 100644
index 0000000000..1d50749974
--- /dev/null
+++ b/chatbot-ui-main/README.md
@@ -0,0 +1,292 @@
+# Chatbot UI
+
+The open-source AI chat app for everyone.
+
+
+
+## Demo
+
+View the latest demo [here](https://x.com/mckaywrigley/status/1738273242283151777?s=20).
+
+## Updates
+
+Hey everyone! I've heard your feedback and am working hard on a big update.
+
+Things like simpler deployment, better backend compatibility, and improved mobile layouts are on their way.
+
+Be back soon.
+
+-- Mckay
+
+## Official Hosted Version
+
+Use Chatbot UI without having to host it yourself!
+
+Find the official hosted version of Chatbot UI [here](https://chatbotui.com).
+
+## Sponsor
+
+If you find Chatbot UI useful, please consider [sponsoring](https://github.com/sponsors/mckaywrigley) me to support my open-source work :)
+
+## Issues
+
+We restrict "Issues" to actual issues related to the codebase.
+
+We're getting excessive amounts of issues that amount to things like feature requests, cloud provider issues, etc.
+
+If you are having issues with things like setup, please refer to the "Help" section in the "Discussions" tab above.
+
+Issues unrelated to the codebase will likely be closed immediately.
+
+## Discussions
+
+We highly encourage you to participate in the "Discussions" tab above!
+
+Discussions are a great place to ask questions, share ideas, and get help.
+
+Odds are if you have a question, someone else has the same question.
+
+## Legacy Code
+
+Chatbot UI was recently updated to its 2.0 version.
+
+The code for 1.0 can be found on the `legacy` branch.
+
+## Updating
+
+In your terminal at the root of your local Chatbot UI repository, run:
+
+```bash
+npm run update
+```
+
+If you run a hosted instance you'll also need to run:
+
+```bash
+npm run db-push
+```
+
+to apply the latest migrations to your live database.
+
+## Local Quickstart
+
+Follow these steps to get your own Chatbot UI instance running locally.
+
+You can watch the full video tutorial [here](https://www.youtube.com/watch?v=9Qq3-7-HNgw).
+
+### 1. Clone the Repo
+
+```bash
+git clone https://github.com/mckaywrigley/chatbot-ui.git
+```
+
+### 2. Install Dependencies
+
+Open a terminal in the root directory of your local Chatbot UI repository and run:
+
+```bash
+npm install
+```
+
+### 3. Install Supabase & Run Locally
+
+#### Why Supabase?
+
+Previously, we used local browser storage to store data. However, this was not a good solution for a few reasons:
+
+- Security issues
+- Limited storage
+- Limits multi-modal use cases
+
+We now use Supabase because it's easy to use, it's open-source, it's Postgres, and it has a free tier for hosted instances.
+
+We will support other providers in the future to give you more options.
+
+#### 1. Install Docker
+
+You will need to install Docker to run Supabase locally. You can download it [here](https://docs.docker.com/get-docker) for free.
+
+#### 2. Install Supabase CLI
+
+**MacOS/Linux**
+
+```bash
+brew install supabase/tap/supabase
+```
+
+**Windows**
+
+```bash
+scoop bucket add supabase https://github.com/supabase/scoop-bucket.git
+scoop install supabase
+```
+
+#### 3. Start Supabase
+
+In your terminal at the root of your local Chatbot UI repository, run:
+
+```bash
+supabase start
+```
+
+### 4. Fill in Secrets
+
+#### 1. Environment Variables
+
+In your terminal at the root of your local Chatbot UI repository, run:
+
+```bash
+cp .env.local.example .env.local
+```
+
+Get the required values by running:
+
+```bash
+supabase status
+```
+
+Note: Use `API URL` from `supabase status` for `NEXT_PUBLIC_SUPABASE_URL`
+
+Now go to your `.env.local` file and fill in the values.
+
+If the environment variable is set, it will disable the input in the user settings.
+
+#### 2. SQL Setup
+
+In the 1st migration file `supabase/migrations/20240108234540_setup.sql` you will need to replace 2 values with the values you got above:
+
+- `project_url` (line 53): `http://supabase_kong_chatbotui:8000` (default) can remain unchanged if you don't change your `project_id` in the `config.toml` file
+- `service_role_key` (line 54): You got this value from running `supabase status`
+
+This prevents issues with storage files not being deleted properly.
+
+### 5. Install Ollama (optional for local models)
+
+Follow the instructions [here](https://github.com/jmorganca/ollama#macos).
+
+### 6. Run app locally
+
+In your terminal at the root of your local Chatbot UI repository, run:
+
+```bash
+npm run chat
+```
+
+Your local instance of Chatbot UI should now be running at [http://localhost:3000](http://localhost:3000). Be sure to use a compatible node version (i.e. v18).
+
+You can view your backend GUI at [http://localhost:54323/project/default/editor](http://localhost:54323/project/default/editor).
+
+## Hosted Quickstart
+
+Follow these steps to get your own Chatbot UI instance running in the cloud.
+
+Video tutorial coming soon.
+
+### 1. Follow Local Quickstart
+
+Repeat steps 1-4 in "Local Quickstart" above.
+
+You will want separate repositories for your local and hosted instances.
+
+Create a new repository for your hosted instance of Chatbot UI on GitHub and push your code to it.
+
+### 2. Setup Backend with Supabase
+
+#### 1. Create a new project
+
+Go to [Supabase](https://supabase.com/) and create a new project.
+
+#### 2. Get Project Values
+
+Once you are in the project dashboard, click on the "Project Settings" icon tab on the far bottom left.
+
+Here you will get the values for the following environment variables:
+
+- `Project Ref`: Found in "General settings" as "Reference ID"
+
+- `Project ID`: Found in the URL of your project dashboard (Ex: https://supabase.com/dashboard/project//settings/general)
+
+While still in "Settings" click on the "API" text tab on the left.
+
+Here you will get the values for the following environment variables:
+
+- `Project URL`: Found in "API Settings" as "Project URL"
+
+- `Anon key`: Found in "Project API keys" as "anon public"
+
+- `Service role key`: Found in "Project API keys" as "service_role" (Reminder: Treat this like a password!)
+
+#### 3. Configure Auth
+
+Next, click on the "Authentication" icon tab on the far left.
+
+In the text tabs, click on "Providers" and make sure "Email" is enabled.
+
+We recommend turning off "Confirm email" for your own personal instance.
+
+#### 4. Connect to Hosted DB
+
+Open up your repository for your hosted instance of Chatbot UI.
+
+In the 1st migration file `supabase/migrations/20240108234540_setup.sql` you will need to replace 2 values with the values you got above:
+
+- `project_url` (line 53): Use the `Project URL` value from above
+- `service_role_key` (line 54): Use the `Service role key` value from above
+
+Now, open a terminal in the root directory of your local Chatbot UI repository. We will execute a few commands here.
+
+Login to Supabase by running:
+
+```bash
+supabase login
+```
+
+Next, link your project by running the following command with the "Project ID" you got above:
+
+```bash
+supabase link --project-ref
+```
+
+Your project should now be linked.
+
+Finally, push your database to Supabase by running:
+
+```bash
+supabase db push
+```
+
+Your hosted database should now be set up!
+
+### 3. Setup Frontend with Vercel
+
+Go to [Vercel](https://vercel.com/) and create a new project.
+
+In the setup page, import your GitHub repository for your hosted instance of Chatbot UI. Within the project Settings, in the "Build & Development Settings" section, switch Framework Preset to "Next.js".
+
+In environment variables, add the following from the values you got above:
+
+- `NEXT_PUBLIC_SUPABASE_URL`
+- `NEXT_PUBLIC_SUPABASE_ANON_KEY`
+- `SUPABASE_SERVICE_ROLE_KEY`
+- `NEXT_PUBLIC_OLLAMA_URL` (only needed when using local Ollama models; default: `http://localhost:11434`)
+
+You can also add API keys as environment variables.
+
+- `OPENAI_API_KEY`
+- `AZURE_OPENAI_API_KEY`
+- `AZURE_OPENAI_ENDPOINT`
+- `AZURE_GPT_45_VISION_NAME`
+
+For the full list of environment variables, refer to the '.env.local.example' file. If the environment variables are set for API keys, it will disable the input in the user settings.
+
+Click "Deploy" and wait for your frontend to deploy.
+
+Once deployed, you should be able to use your hosted instance of Chatbot UI via the URL Vercel gives you.
+
+## Contributing
+
+We are working on a guide for contributing.
+
+## Contact
+
+Message Mckay on [Twitter/X](https://twitter.com/mckaywrigley)
diff --git a/chatbot-ui-main/__tests__/lib/openapi-conversion.test.ts b/chatbot-ui-main/__tests__/lib/openapi-conversion.test.ts
new file mode 100644
index 0000000000..7505590faf
--- /dev/null
+++ b/chatbot-ui-main/__tests__/lib/openapi-conversion.test.ts
@@ -0,0 +1,369 @@
+import { openapiToFunctions } from "@/lib/openapi-conversion"
+
+const validSchemaURL = JSON.stringify({
+ openapi: "3.1.0",
+ info: {
+ title: "Get weather data",
+ description: "Retrieves current weather data for a location.",
+ version: "v1.0.0"
+ },
+ servers: [
+ {
+ url: "https://weather.example.com"
+ }
+ ],
+ paths: {
+ "/location": {
+ get: {
+ description: "Get temperature for a specific location",
+ operationId: "GetCurrentWeather",
+ parameters: [
+ {
+ name: "location",
+ in: "query",
+ description: "The city and state to retrieve the weather for",
+ required: true,
+ schema: {
+ type: "string"
+ }
+ }
+ ]
+ }
+ },
+ "/summary": {
+ get: {
+ description: "Get description of weather for a specific location",
+ operationId: "GetWeatherSummary",
+ parameters: [
+ {
+ name: "location",
+ in: "query",
+ description: "The city and state to retrieve the summary for",
+ required: true,
+ schema: {
+ type: "string"
+ }
+ }
+ ]
+ }
+ }
+ }
+})
+
+describe("extractOpenapiData for url", () => {
+ it("should parse a valid OpenAPI url schema", async () => {
+ const { info, routes, functions } = await openapiToFunctions(
+ JSON.parse(validSchemaURL)
+ )
+
+ expect(info.title).toBe("Get weather data")
+ expect(info.description).toBe(
+ "Retrieves current weather data for a location."
+ )
+ expect(info.server).toBe("https://weather.example.com")
+
+ expect(routes).toHaveLength(2)
+
+ expect(functions).toHaveLength(2)
+ expect(functions[0].function.name).toBe("GetCurrentWeather")
+ expect(functions[1].function.name).toBe("GetWeatherSummary")
+ })
+})
+
+const validSchemaBody = JSON.stringify({
+ openapi: "3.1.0",
+ info: {
+ title: "Get weather data",
+ description: "Retrieves current weather data for a location.",
+ version: "v1.0.0"
+ },
+ servers: [
+ {
+ url: "https://weather.example.com"
+ }
+ ],
+ paths: {
+ "/location": {
+ post: {
+ description: "Get temperature for a specific location",
+ operationId: "GetCurrentWeather",
+ requestBody: {
+ required: true,
+ content: {
+ "application/json": {
+ schema: {
+ type: "object",
+ properties: {
+ location: {
+ type: "string",
+ description:
+ "The city and state to retrieve the weather for",
+ example: "New York, NY"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+})
+
+describe("extractOpenapiData for body", () => {
+ it("should parse a valid OpenAPI body schema", async () => {
+ const { info, routes, functions } = await openapiToFunctions(
+ JSON.parse(validSchemaBody)
+ )
+
+ expect(info.title).toBe("Get weather data")
+ expect(info.description).toBe(
+ "Retrieves current weather data for a location."
+ )
+ expect(info.server).toBe("https://weather.example.com")
+
+ expect(routes).toHaveLength(1)
+ expect(routes[0].path).toBe("/location")
+ expect(routes[0].method).toBe("post")
+ expect(routes[0].operationId).toBe("GetCurrentWeather")
+
+ expect(functions).toHaveLength(1)
+ expect(
+ functions[0].function.parameters.properties.requestBody.properties
+ .location.type
+ ).toBe("string")
+ expect(
+ functions[0].function.parameters.properties.requestBody.properties
+ .location.description
+ ).toBe("The city and state to retrieve the weather for")
+ })
+})
+
+const validSchemaBody2 = JSON.stringify({
+ openapi: "3.1.0",
+ info: {
+ title: "Polygon.io Stock and Crypto Data API",
+ description:
+ "API schema for accessing stock and crypto data from Polygon.io.",
+ version: "1.0.0"
+ },
+ servers: [
+ {
+ url: "https://api.polygon.io"
+ }
+ ],
+ paths: {
+ "/v1/open-close/{stocksTicker}/{date}": {
+ get: {
+ summary: "Get Stock Daily Open and Close",
+ description: "Get the daily open and close for a specific stock.",
+ operationId: "getStockDailyOpenClose",
+ parameters: [
+ {
+ name: "stocksTicker",
+ in: "path",
+ required: true,
+ schema: {
+ type: "string"
+ }
+ },
+ {
+ name: "date",
+ in: "path",
+ required: true,
+ schema: {
+ type: "string",
+ format: "date"
+ }
+ }
+ ]
+ }
+ },
+ "/v2/aggs/ticker/{stocksTicker}/prev": {
+ get: {
+ summary: "Get Stock Previous Close",
+ description: "Get the previous closing data for a specific stock.",
+ operationId: "getStockPreviousClose",
+ parameters: [
+ {
+ name: "stocksTicker",
+ in: "path",
+ required: true,
+ schema: {
+ type: "string"
+ }
+ }
+ ]
+ }
+ },
+ "/v3/trades/{stockTicker}": {
+ get: {
+ summary: "Get Stock Trades",
+ description: "Retrieve trades for a specific stock.",
+ operationId: "getStockTrades",
+ parameters: [
+ {
+ name: "stockTicker",
+ in: "path",
+ required: true,
+ schema: {
+ type: "string"
+ }
+ }
+ ]
+ }
+ },
+ "/v3/trades/{optionsTicker}": {
+ get: {
+ summary: "Get Options Trades",
+ description: "Retrieve trades for a specific options ticker.",
+ operationId: "getOptionsTrades",
+ parameters: [
+ {
+ name: "optionsTicker",
+ in: "path",
+ required: true,
+ schema: {
+ type: "string"
+ }
+ }
+ ]
+ }
+ },
+ "/v2/last/trade/{optionsTicker}": {
+ get: {
+ summary: "Get Last Options Trade",
+ description: "Get the last trade for a specific options ticker.",
+ operationId: "getLastOptionsTrade",
+ parameters: [
+ {
+ name: "optionsTicker",
+ in: "path",
+ required: true,
+ schema: {
+ type: "string"
+ }
+ }
+ ]
+ }
+ },
+ "/v1/open-close/crypto/{from}/{to}/{date}": {
+ get: {
+ summary: "Get Crypto Daily Open and Close",
+ description:
+ "Get daily open and close data for a specific cryptocurrency.",
+ operationId: "getCryptoDailyOpenClose",
+ parameters: [
+ {
+ name: "from",
+ in: "path",
+ required: true,
+ schema: {
+ type: "string"
+ }
+ },
+ {
+ name: "to",
+ in: "path",
+ required: true,
+ schema: {
+ type: "string"
+ }
+ },
+ {
+ name: "date",
+ in: "path",
+ required: true,
+ schema: {
+ type: "string",
+ format: "date"
+ }
+ }
+ ]
+ }
+ },
+ "/v2/aggs/ticker/{cryptoTicker}/prev": {
+ get: {
+ summary: "Get Crypto Previous Close",
+ description:
+ "Get the previous closing data for a specific cryptocurrency.",
+ operationId: "getCryptoPreviousClose",
+ parameters: [
+ {
+ name: "cryptoTicker",
+ in: "path",
+ required: true,
+ schema: {
+ type: "string"
+ }
+ }
+ ]
+ }
+ }
+ },
+ components: {
+ securitySchemes: {
+ BearerAuth: {
+ type: "http",
+ scheme: "bearer",
+ bearerFormat: "API Key"
+ }
+ }
+ },
+ security: [
+ {
+ BearerAuth: []
+ }
+ ]
+})
+
+describe("extractOpenapiData for body 2", () => {
+ it("should parse a valid OpenAPI body schema for body 2", async () => {
+ const { info, routes, functions } = await openapiToFunctions(
+ JSON.parse(validSchemaBody2)
+ )
+
+ expect(info.title).toBe("Polygon.io Stock and Crypto Data API")
+ expect(info.description).toBe(
+ "API schema for accessing stock and crypto data from Polygon.io."
+ )
+ expect(info.server).toBe("https://api.polygon.io")
+
+ expect(routes).toHaveLength(7)
+ expect(routes[0].path).toBe("/v1/open-close/{stocksTicker}/{date}")
+ expect(routes[0].method).toBe("get")
+ expect(routes[0].operationId).toBe("getStockDailyOpenClose")
+
+ expect(functions[0].function.parameters.properties).toHaveProperty(
+ "stocksTicker"
+ )
+ expect(functions[0].function.parameters.properties.stocksTicker.type).toBe(
+ "string"
+ )
+ expect(
+ functions[0].function.parameters.properties.stocksTicker
+ ).toHaveProperty("required", true)
+ expect(functions[0].function.parameters.properties).toHaveProperty("date")
+ expect(functions[0].function.parameters.properties.date.type).toBe("string")
+ expect(functions[0].function.parameters.properties.date).toHaveProperty(
+ "format",
+ "date"
+ )
+ expect(functions[0].function.parameters.properties.date).toHaveProperty(
+ "required",
+ true
+ )
+ expect(routes[1].path).toBe("/v2/aggs/ticker/{stocksTicker}/prev")
+ expect(routes[1].method).toBe("get")
+ expect(routes[1].operationId).toBe("getStockPreviousClose")
+ expect(functions[1].function.parameters.properties).toHaveProperty(
+ "stocksTicker"
+ )
+ expect(functions[1].function.parameters.properties.stocksTicker.type).toBe(
+ "string"
+ )
+ expect(
+ functions[1].function.parameters.properties.stocksTicker
+ ).toHaveProperty("required", true)
+ })
+})
diff --git a/chatbot-ui-main/__tests__/playwright-test/.gitignore b/chatbot-ui-main/__tests__/playwright-test/.gitignore
new file mode 100644
index 0000000000..68c5d18f00
--- /dev/null
+++ b/chatbot-ui-main/__tests__/playwright-test/.gitignore
@@ -0,0 +1,5 @@
+node_modules/
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
diff --git a/chatbot-ui-main/__tests__/playwright-test/package-lock.json b/chatbot-ui-main/__tests__/playwright-test/package-lock.json
new file mode 100644
index 0000000000..6b2675df8d
--- /dev/null
+++ b/chatbot-ui-main/__tests__/playwright-test/package-lock.json
@@ -0,0 +1,91 @@
+{
+ "name": "playwright-test",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "playwright-test",
+ "version": "1.0.0",
+ "license": "ISC",
+ "devDependencies": {
+ "@playwright/test": "^1.41.2",
+ "@types/node": "^20.11.20"
+ }
+ },
+ "node_modules/@playwright/test": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.2.tgz",
+ "integrity": "sha512-qQB9h7KbibJzrDpkXkYvsmiDJK14FULCCZgEcoe2AvFAS64oCirWTwzTlAYEbKaRxWs5TFesE1Na6izMv3HfGg==",
+ "dev": true,
+ "dependencies": {
+ "playwright": "1.41.2"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "20.11.20",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz",
+ "integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==",
+ "dev": true,
+ "dependencies": {
+ "undici-types": "~5.26.4"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/playwright": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.41.2.tgz",
+ "integrity": "sha512-v0bOa6H2GJChDL8pAeLa/LZC4feoAMbSQm1/jF/ySsWWoaNItvrMP7GEkvEEFyCTUYKMxjQKaTSg5up7nR6/8A==",
+ "dev": true,
+ "dependencies": {
+ "playwright-core": "1.41.2"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.41.2.tgz",
+ "integrity": "sha512-VaTvwCA4Y8kxEe+kfm2+uUUw5Lubf38RxF7FpBxLPmGe5sdNkSg5e3ChEigaGrX7qdqT3pt2m/98LiyvU2x6CA==",
+ "dev": true,
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "5.26.5",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
+ "dev": true
+ }
+ }
+}
diff --git a/chatbot-ui-main/__tests__/playwright-test/package.json b/chatbot-ui-main/__tests__/playwright-test/package.json
new file mode 100644
index 0000000000..286e30dd1b
--- /dev/null
+++ b/chatbot-ui-main/__tests__/playwright-test/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "playwright-test",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "integration": "playwright test",
+ "integration:open": "playwright test --ui",
+ "integration:codegen": "playwright codegen"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "devDependencies": {
+ "@playwright/test": "^1.41.2",
+ "@types/node": "^20.11.20"
+ }
+}
diff --git a/chatbot-ui-main/__tests__/playwright-test/playwright.config.ts b/chatbot-ui-main/__tests__/playwright-test/playwright.config.ts
new file mode 100644
index 0000000000..301801ee1d
--- /dev/null
+++ b/chatbot-ui-main/__tests__/playwright-test/playwright.config.ts
@@ -0,0 +1,77 @@
+import { defineConfig, devices } from '@playwright/test';
+
+/**
+ * Read environment variables from file.
+ * https://github.com/motdotla/dotenv
+ */
+// require('dotenv').config();
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ testDir: './tests',
+ /* Run tests in files in parallel */
+ fullyParallel: true,
+ /* Fail the build on CI if you accidentally left test.only in the source code. */
+ forbidOnly: !!process.env.CI,
+ /* Retry on CI only */
+ retries: process.env.CI ? 2 : 0,
+ /* Opt out of parallel tests on CI. */
+ workers: process.env.CI ? 1 : undefined,
+ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
+ reporter: 'html',
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+ use: {
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ // baseURL: 'http://127.0.0.1:3000',
+
+ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+ trace: 'on-first-retry',
+ },
+
+ /* Configure projects for major browsers */
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+
+ {
+ name: 'firefox',
+ use: { ...devices['Desktop Firefox'] },
+ },
+
+ {
+ name: 'webkit',
+ use: { ...devices['Desktop Safari'] },
+ },
+
+ /* Test against mobile viewports. */
+ // {
+ // name: 'Mobile Chrome',
+ // use: { ...devices['Pixel 5'] },
+ // },
+ // {
+ // name: 'Mobile Safari',
+ // use: { ...devices['iPhone 12'] },
+ // },
+
+ /* Test against branded browsers. */
+ // {
+ // name: 'Microsoft Edge',
+ // use: { ...devices['Desktop Edge'], channel: 'msedge' },
+ // },
+ // {
+ // name: 'Google Chrome',
+ // use: { ...devices['Desktop Chrome'], channel: 'chrome' },
+ // },
+ ],
+
+ /* Run your local dev server before starting the tests */
+ // webServer: {
+ // command: 'npm run start',
+ // url: 'http://127.0.0.1:3000',
+ // reuseExistingServer: !process.env.CI,
+ // },
+});
diff --git a/chatbot-ui-main/__tests__/playwright-test/tests/login.spec.ts b/chatbot-ui-main/__tests__/playwright-test/tests/login.spec.ts
new file mode 100644
index 0000000000..0b69a70273
--- /dev/null
+++ b/chatbot-ui-main/__tests__/playwright-test/tests/login.spec.ts
@@ -0,0 +1,46 @@
+import { test, expect } from '@playwright/test';
+
+test('start chatting is displayed', async ({ page }) => {
+ await page.goto('http://localhost:3000/');
+
+ //expect the start chatting link to be visible
+ await expect (page.getByRole('link', { name: 'Start Chatting' })).toBeVisible();
+});
+
+test('No password error message', async ({ page }) => {
+ await page.goto('http://localhost:3000/login');
+ //fill in dummy email
+ await page.getByPlaceholder('you@example.com').fill('dummyemail@gmail.com');
+ await page.getByRole('button', { name: 'Login' }).click();
+ //wait for netwrok to be idle
+ await page.waitForLoadState('networkidle');
+ //validate that correct message is shown to the user
+ await expect(page.getByText('Invalid login credentials')).toBeVisible();
+
+});
+test('No password for signup', async ({ page }) => {
+ await page.goto('http://localhost:3000/login');
+
+ await page.getByPlaceholder('you@example.com').fill('dummyEmail@Gmail.com');
+ await page.getByRole('button', { name: 'Sign Up' }).click();
+ //validate appropriate error is thrown for missing password when signing up
+ await expect(page.getByText('Signup requires a valid')).toBeVisible();
+});
+test('invalid username for signup', async ({ page }) => {
+ await page.goto('http://localhost:3000/login');
+
+ await page.getByPlaceholder('you@example.com').fill('dummyEmail');
+ await page.getByPlaceholder('••••••••').fill('dummypassword');
+ await page.getByRole('button', { name: 'Sign Up' }).click();
+ //validate appropriate error is thrown for invalid username when signing up
+ await expect(page.getByText('Unable to validate email')).toBeVisible();
+});
+test('password reset message', async ({ page }) => {
+ await page.goto('http://localhost:3000/login');
+ await page.getByPlaceholder('you@example.com').fill('demo@gmail.com');
+ await page.getByRole('button', { name: 'Reset' }).click();
+ //validate appropriate message is shown
+ await expect(page.getByText('Check email to reset password')).toBeVisible();
+});
+
+//more tests can be added here
\ No newline at end of file
diff --git a/chatbot-ui-main/app/[locale]/[workspaceid]/chat/[chatid]/page.tsx b/chatbot-ui-main/app/[locale]/[workspaceid]/chat/[chatid]/page.tsx
new file mode 100644
index 0000000000..30d082e82d
--- /dev/null
+++ b/chatbot-ui-main/app/[locale]/[workspaceid]/chat/[chatid]/page.tsx
@@ -0,0 +1,7 @@
+"use client"
+
+import { ChatUI } from "@/components/chat/chat-ui"
+
+export default function ChatIDPage() {
+ return
+}
diff --git a/chatbot-ui-main/app/[locale]/[workspaceid]/chat/page.tsx b/chatbot-ui-main/app/[locale]/[workspaceid]/chat/page.tsx
new file mode 100644
index 0000000000..15e189e691
--- /dev/null
+++ b/chatbot-ui-main/app/[locale]/[workspaceid]/chat/page.tsx
@@ -0,0 +1,58 @@
+"use client"
+
+import { ChatHelp } from "@/components/chat/chat-help"
+import { useChatHandler } from "@/components/chat/chat-hooks/use-chat-handler"
+import { ChatInput } from "@/components/chat/chat-input"
+import { ChatSettings } from "@/components/chat/chat-settings"
+import { ChatUI } from "@/components/chat/chat-ui"
+import { QuickSettings } from "@/components/chat/quick-settings"
+import { Brand } from "@/components/ui/brand"
+import { ChatbotUIContext } from "@/context/context"
+import useHotkey from "@/lib/hooks/use-hotkey"
+import { useTheme } from "next-themes"
+import { useContext } from "react"
+
+export default function ChatPage() {
+ useHotkey("o", () => handleNewChat())
+ useHotkey("l", () => {
+ handleFocusChatInput()
+ })
+
+ const { chatMessages } = useContext(ChatbotUIContext)
+
+ const { handleNewChat, handleFocusChatInput } = useChatHandler()
+
+ const { theme } = useTheme()
+
+ return (
+ <>
+ {chatMessages.length === 0 ? (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ) : (
+
+ )}
+ >
+ )
+}
diff --git a/chatbot-ui-main/app/[locale]/[workspaceid]/layout.tsx b/chatbot-ui-main/app/[locale]/[workspaceid]/layout.tsx
new file mode 100644
index 0000000000..227a6e7903
--- /dev/null
+++ b/chatbot-ui-main/app/[locale]/[workspaceid]/layout.tsx
@@ -0,0 +1,183 @@
+"use client"
+
+import { Dashboard } from "@/components/ui/dashboard"
+import { ChatbotUIContext } from "@/context/context"
+import { getAssistantWorkspacesByWorkspaceId } from "@/db/assistants"
+import { getChatsByWorkspaceId } from "@/db/chats"
+import { getCollectionWorkspacesByWorkspaceId } from "@/db/collections"
+import { getFileWorkspacesByWorkspaceId } from "@/db/files"
+import { getFoldersByWorkspaceId } from "@/db/folders"
+import { getModelWorkspacesByWorkspaceId } from "@/db/models"
+import { getPresetWorkspacesByWorkspaceId } from "@/db/presets"
+import { getPromptWorkspacesByWorkspaceId } from "@/db/prompts"
+import { getAssistantImageFromStorage } from "@/db/storage/assistant-images"
+import { getToolWorkspacesByWorkspaceId } from "@/db/tools"
+import { getWorkspaceById } from "@/db/workspaces"
+import { convertBlobToBase64 } from "@/lib/blob-to-b64"
+import { supabase } from "@/lib/supabase/browser-client"
+import { LLMID } from "@/types"
+import { useParams, useRouter, useSearchParams } from "next/navigation"
+import { ReactNode, useContext, useEffect, useState } from "react"
+import Loading from "../loading"
+
+interface WorkspaceLayoutProps {
+ children: ReactNode
+}
+
+export default function WorkspaceLayout({ children }: WorkspaceLayoutProps) {
+ const router = useRouter()
+
+ const params = useParams()
+ const searchParams = useSearchParams()
+ const workspaceId = params.workspaceid as string
+
+ const {
+ setChatSettings,
+ setAssistants,
+ setAssistantImages,
+ setChats,
+ setCollections,
+ setFolders,
+ setFiles,
+ setPresets,
+ setPrompts,
+ setTools,
+ setModels,
+ selectedWorkspace,
+ setSelectedWorkspace,
+ setSelectedChat,
+ setChatMessages,
+ setUserInput,
+ setIsGenerating,
+ setFirstTokenReceived,
+ setChatFiles,
+ setChatImages,
+ setNewMessageFiles,
+ setNewMessageImages,
+ setShowFilesDisplay
+ } = useContext(ChatbotUIContext)
+
+ const [loading, setLoading] = useState(true)
+
+ useEffect(() => {
+ ;(async () => {
+ const session = (await supabase.auth.getSession()).data.session
+
+ if (!session) {
+ return router.push("/login")
+ } else {
+ await fetchWorkspaceData(workspaceId)
+ }
+ })()
+ }, [])
+
+ useEffect(() => {
+ ;(async () => await fetchWorkspaceData(workspaceId))()
+
+ setUserInput("")
+ setChatMessages([])
+ setSelectedChat(null)
+
+ setIsGenerating(false)
+ setFirstTokenReceived(false)
+
+ setChatFiles([])
+ setChatImages([])
+ setNewMessageFiles([])
+ setNewMessageImages([])
+ setShowFilesDisplay(false)
+ }, [workspaceId])
+
+ const fetchWorkspaceData = async (workspaceId: string) => {
+ setLoading(true)
+
+ const workspace = await getWorkspaceById(workspaceId)
+ setSelectedWorkspace(workspace)
+
+ const assistantData = await getAssistantWorkspacesByWorkspaceId(workspaceId)
+ setAssistants(assistantData.assistants)
+
+ for (const assistant of assistantData.assistants) {
+ let url = ""
+
+ if (assistant.image_path) {
+ url = (await getAssistantImageFromStorage(assistant.image_path)) || ""
+ }
+
+ if (url) {
+ const response = await fetch(url)
+ const blob = await response.blob()
+ const base64 = await convertBlobToBase64(blob)
+
+ setAssistantImages(prev => [
+ ...prev,
+ {
+ assistantId: assistant.id,
+ path: assistant.image_path,
+ base64,
+ url
+ }
+ ])
+ } else {
+ setAssistantImages(prev => [
+ ...prev,
+ {
+ assistantId: assistant.id,
+ path: assistant.image_path,
+ base64: "",
+ url
+ }
+ ])
+ }
+ }
+
+ const chats = await getChatsByWorkspaceId(workspaceId)
+ setChats(chats)
+
+ const collectionData =
+ await getCollectionWorkspacesByWorkspaceId(workspaceId)
+ setCollections(collectionData.collections)
+
+ const folders = await getFoldersByWorkspaceId(workspaceId)
+ setFolders(folders)
+
+ const fileData = await getFileWorkspacesByWorkspaceId(workspaceId)
+ setFiles(fileData.files)
+
+ const presetData = await getPresetWorkspacesByWorkspaceId(workspaceId)
+ setPresets(presetData.presets)
+
+ const promptData = await getPromptWorkspacesByWorkspaceId(workspaceId)
+ setPrompts(promptData.prompts)
+
+ const toolData = await getToolWorkspacesByWorkspaceId(workspaceId)
+ setTools(toolData.tools)
+
+ const modelData = await getModelWorkspacesByWorkspaceId(workspaceId)
+ setModels(modelData.models)
+
+ setChatSettings({
+ model: (searchParams.get("model") ||
+ workspace?.default_model ||
+ "gpt-4-1106-preview") as LLMID,
+ prompt:
+ workspace?.default_prompt ||
+ "You are a friendly, helpful AI assistant.",
+ temperature: workspace?.default_temperature || 0.5,
+ contextLength: workspace?.default_context_length || 4096,
+ includeProfileContext: workspace?.include_profile_context || true,
+ includeWorkspaceInstructions:
+ workspace?.include_workspace_instructions || true,
+ embeddingsProvider:
+ (workspace?.embeddings_provider as "openai" | "local") || "openai"
+ })
+
+ setLoading(false)
+ }
+
+ if (loading) {
+ return
+ }
+
+ return {children}
+}
diff --git a/chatbot-ui-main/app/[locale]/[workspaceid]/page.tsx b/chatbot-ui-main/app/[locale]/[workspaceid]/page.tsx
new file mode 100644
index 0000000000..b43e8d5eb8
--- /dev/null
+++ b/chatbot-ui-main/app/[locale]/[workspaceid]/page.tsx
@@ -0,0 +1,14 @@
+"use client"
+
+import { ChatbotUIContext } from "@/context/context"
+import { useContext } from "react"
+
+export default function WorkspacePage() {
+ const { selectedWorkspace } = useContext(ChatbotUIContext)
+
+ return (
+
+
{selectedWorkspace?.name}
+
+ )
+}
diff --git a/chatbot-ui-main/app/[locale]/globals.css b/chatbot-ui-main/app/[locale]/globals.css
new file mode 100644
index 0000000000..c0d1efcb5a
--- /dev/null
+++ b/chatbot-ui-main/app/[locale]/globals.css
@@ -0,0 +1,104 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+::-webkit-scrollbar-track {
+ background-color: transparent;
+}
+
+::-webkit-scrollbar-thumb {
+ background-color: #ccc;
+ border-radius: 10px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background-color: #aaa;
+}
+
+::-webkit-scrollbar-track:hover {
+ background-color: #f2f2f2;
+}
+
+::-webkit-scrollbar-corner {
+ background-color: transparent;
+}
+
+::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
+}
+
+@layer base {
+ :root {
+ --background: 0 0% 100%;
+ --foreground: 0 0% 3.9%;
+
+ --muted: 0 0% 96.1%;
+ --muted-foreground: 0 0% 45.1%;
+
+ --popover: 0 0% 100%;
+ --popover-foreground: 0 0% 3.9%;
+
+ --card: 0 0% 100%;
+ --card-foreground: 0 0% 3.9%;
+
+ --border: 0 0% 89.8%;
+ --input: 0 0% 89.8%;
+
+ --primary: 0 0% 9%;
+ --primary-foreground: 0 0% 98%;
+
+ --secondary: 0 0% 96.1%;
+ --secondary-foreground: 0 0% 9%;
+
+ --accent: 0 0% 96.1%;
+ --accent-foreground: 0 0% 9%;
+
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 0 0% 98%;
+
+ --ring: 0 0% 63.9%;
+
+ --radius: 0.5rem;
+ }
+
+ .dark {
+ --background: 0 0% 3.9%;
+ --foreground: 0 0% 98%;
+
+ --muted: 0 0% 14.9%;
+ --muted-foreground: 0 0% 63.9%;
+
+ --popover: 0 0% 3.9%;
+ --popover-foreground: 0 0% 98%;
+
+ --card: 0 0% 3.9%;
+ --card-foreground: 0 0% 98%;
+
+ --border: 0 0% 14.9%;
+ --input: 0 0% 14.9%;
+
+ --primary: 0 0% 98%;
+ --primary-foreground: 0 0% 9%;
+
+ --secondary: 0 0% 14.9%;
+ --secondary-foreground: 0 0% 98%;
+
+ --accent: 0 0% 14.9%;
+ --accent-foreground: 0 0% 98%;
+
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 0 85.7% 97.3%;
+
+ --ring: 0 0% 14.9%;
+ }
+}
+
+@layer base {
+ * {
+ @apply border-border;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+}
diff --git a/chatbot-ui-main/app/[locale]/help/page.tsx b/chatbot-ui-main/app/[locale]/help/page.tsx
new file mode 100644
index 0000000000..c1753f460b
--- /dev/null
+++ b/chatbot-ui-main/app/[locale]/help/page.tsx
@@ -0,0 +1,7 @@
+export default function HelpPage() {
+ return (
+
+
Help under construction.
+
+ )
+}
diff --git a/chatbot-ui-main/app/[locale]/layout.tsx b/chatbot-ui-main/app/[locale]/layout.tsx
new file mode 100644
index 0000000000..474058a910
--- /dev/null
+++ b/chatbot-ui-main/app/[locale]/layout.tsx
@@ -0,0 +1,107 @@
+import { Toaster } from "@/components/ui/sonner"
+import { GlobalState } from "@/components/utility/global-state"
+import { Providers } from "@/components/utility/providers"
+import TranslationsProvider from "@/components/utility/translations-provider"
+import initTranslations from "@/lib/i18n"
+import { Database } from "@/supabase/types"
+import { createServerClient } from "@supabase/ssr"
+import { Metadata, Viewport } from "next"
+import { Inter } from "next/font/google"
+import { cookies } from "next/headers"
+import { ReactNode } from "react"
+import "./globals.css"
+
+const inter = Inter({ subsets: ["latin"] })
+const APP_NAME = "Chatbot UI"
+const APP_DEFAULT_TITLE = "Chatbot UI"
+const APP_TITLE_TEMPLATE = "%s - Chatbot UI"
+const APP_DESCRIPTION = "Chabot UI PWA!"
+
+interface RootLayoutProps {
+ children: ReactNode
+ params: {
+ locale: string
+ }
+}
+
+export const metadata: Metadata = {
+ applicationName: APP_NAME,
+ title: {
+ default: APP_DEFAULT_TITLE,
+ template: APP_TITLE_TEMPLATE
+ },
+ description: APP_DESCRIPTION,
+ manifest: "/manifest.json",
+ appleWebApp: {
+ capable: true,
+ statusBarStyle: "black",
+ title: APP_DEFAULT_TITLE
+ // startUpImage: [],
+ },
+ formatDetection: {
+ telephone: false
+ },
+ openGraph: {
+ type: "website",
+ siteName: APP_NAME,
+ title: {
+ default: APP_DEFAULT_TITLE,
+ template: APP_TITLE_TEMPLATE
+ },
+ description: APP_DESCRIPTION
+ },
+ twitter: {
+ card: "summary",
+ title: {
+ default: APP_DEFAULT_TITLE,
+ template: APP_TITLE_TEMPLATE
+ },
+ description: APP_DESCRIPTION
+ }
+}
+
+export const viewport: Viewport = {
+ themeColor: "#000000"
+}
+
+const i18nNamespaces = ["translation"]
+
+export default async function RootLayout({
+ children,
+ params: { locale }
+}: RootLayoutProps) {
+ const cookieStore = cookies()
+ const supabase = createServerClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
+ {
+ cookies: {
+ get(name: string) {
+ return cookieStore.get(name)?.value
+ }
+ }
+ }
+ )
+ const session = (await supabase.auth.getSession()).data.session
+
+ const { t, resources } = await initTranslations(locale, i18nNamespaces)
+
+ return (
+
+
+
+
+
+
+ {session ? {children} : children}
+
+
+
+
+
+ )
+}
diff --git a/chatbot-ui-main/app/[locale]/loading.tsx b/chatbot-ui-main/app/[locale]/loading.tsx
new file mode 100644
index 0000000000..4cfc63fdec
--- /dev/null
+++ b/chatbot-ui-main/app/[locale]/loading.tsx
@@ -0,0 +1,9 @@
+import { IconLoader2 } from "@tabler/icons-react"
+
+export default function Loading() {
+ return (
+
+
+
+ )
+}
diff --git a/chatbot-ui-main/app/[locale]/login/page.tsx b/chatbot-ui-main/app/[locale]/login/page.tsx
new file mode 100644
index 0000000000..527422f65a
--- /dev/null
+++ b/chatbot-ui-main/app/[locale]/login/page.tsx
@@ -0,0 +1,221 @@
+import { Brand } from "@/components/ui/brand"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { SubmitButton } from "@/components/ui/submit-button"
+import { createClient } from "@/lib/supabase/server"
+import { Database } from "@/supabase/types"
+import { createServerClient } from "@supabase/ssr"
+import { get } from "@vercel/edge-config"
+import { Metadata } from "next"
+import { cookies, headers } from "next/headers"
+import { redirect } from "next/navigation"
+
+export const metadata: Metadata = {
+ title: "Login"
+}
+
+export default async function Login({
+ searchParams
+}: {
+ searchParams: { message: string }
+}) {
+ const cookieStore = cookies()
+ const supabase = createServerClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
+ {
+ cookies: {
+ get(name: string) {
+ return cookieStore.get(name)?.value
+ }
+ }
+ }
+ )
+ const session = (await supabase.auth.getSession()).data.session
+
+ if (session) {
+ const { data: homeWorkspace, error } = await supabase
+ .from("workspaces")
+ .select("*")
+ .eq("user_id", session.user.id)
+ .eq("is_home", true)
+ .single()
+
+ if (!homeWorkspace) {
+ throw new Error(error.message)
+ }
+
+ return redirect(`/${homeWorkspace.id}/chat`)
+ }
+
+ const signIn = async (formData: FormData) => {
+ "use server"
+
+ const email = formData.get("email") as string
+ const password = formData.get("password") as string
+ const cookieStore = cookies()
+ const supabase = createClient(cookieStore)
+
+ const { data, error } = await supabase.auth.signInWithPassword({
+ email,
+ password
+ })
+
+ if (error) {
+ return redirect(`/login?message=${error.message}`)
+ }
+
+ const { data: homeWorkspace, error: homeWorkspaceError } = await supabase
+ .from("workspaces")
+ .select("*")
+ .eq("user_id", data.user.id)
+ .eq("is_home", true)
+ .single()
+
+ if (!homeWorkspace) {
+ throw new Error(
+ homeWorkspaceError?.message || "An unexpected error occurred"
+ )
+ }
+
+ return redirect(`/${homeWorkspace.id}/chat`)
+ }
+
+ const getEnvVarOrEdgeConfigValue = async (name: string) => {
+ "use server"
+ if (process.env.EDGE_CONFIG) {
+ return await get(name)
+ }
+
+ return process.env[name]
+ }
+
+ const signUp = async (formData: FormData) => {
+ "use server"
+
+ const email = formData.get("email") as string
+ const password = formData.get("password") as string
+
+ const emailDomainWhitelistPatternsString = await getEnvVarOrEdgeConfigValue(
+ "EMAIL_DOMAIN_WHITELIST"
+ )
+ const emailDomainWhitelist = emailDomainWhitelistPatternsString?.trim()
+ ? emailDomainWhitelistPatternsString?.split(",")
+ : []
+ const emailWhitelistPatternsString =
+ await getEnvVarOrEdgeConfigValue("EMAIL_WHITELIST")
+ const emailWhitelist = emailWhitelistPatternsString?.trim()
+ ? emailWhitelistPatternsString?.split(",")
+ : []
+
+ // If there are whitelist patterns, check if the email is allowed to sign up
+ if (emailDomainWhitelist.length > 0 || emailWhitelist.length > 0) {
+ const domainMatch = emailDomainWhitelist?.includes(email.split("@")[1])
+ const emailMatch = emailWhitelist?.includes(email)
+ if (!domainMatch && !emailMatch) {
+ return redirect(
+ `/login?message=Email ${email} is not allowed to sign up.`
+ )
+ }
+ }
+
+ const cookieStore = cookies()
+ const supabase = createClient(cookieStore)
+
+ const { error } = await supabase.auth.signUp({
+ email,
+ password,
+ options: {
+ // USE IF YOU WANT TO SEND EMAIL VERIFICATION, ALSO CHANGE TOML FILE
+ // emailRedirectTo: `${origin}/auth/callback`
+ }
+ })
+
+ if (error) {
+ console.error(error)
+ return redirect(`/login?message=${error.message}`)
+ }
+
+ return redirect("/setup")
+
+ // USE IF YOU WANT TO SEND EMAIL VERIFICATION, ALSO CHANGE TOML FILE
+ // return redirect("/login?message=Check email to continue sign in process")
+ }
+
+ const handleResetPassword = async (formData: FormData) => {
+ "use server"
+
+ const origin = headers().get("origin")
+ const email = formData.get("email") as string
+ const cookieStore = cookies()
+ const supabase = createClient(cookieStore)
+
+ const { error } = await supabase.auth.resetPasswordForEmail(email, {
+ redirectTo: `${origin}/auth/callback?next=/login/password`
+ })
+
+ if (error) {
+ return redirect(`/login?message=${error.message}`)
+ }
+
+ return redirect("/login?message=Check email to reset password")
+ }
+
+ return (
+
+ )
+}
diff --git a/chatbot-ui-main/app/[locale]/login/password/page.tsx b/chatbot-ui-main/app/[locale]/login/password/page.tsx
new file mode 100644
index 0000000000..e00cca3f65
--- /dev/null
+++ b/chatbot-ui-main/app/[locale]/login/password/page.tsx
@@ -0,0 +1,30 @@
+"use client"
+
+import { ChangePassword } from "@/components/utility/change-password"
+import { supabase } from "@/lib/supabase/browser-client"
+import { useRouter } from "next/navigation"
+import { useEffect, useState } from "react"
+
+export default function ChangePasswordPage() {
+ const [loading, setLoading] = useState(true)
+
+ const router = useRouter()
+
+ useEffect(() => {
+ ;(async () => {
+ const session = (await supabase.auth.getSession()).data.session
+
+ if (!session) {
+ router.push("/login")
+ } else {
+ setLoading(false)
+ }
+ })()
+ }, [])
+
+ if (loading) {
+ return null
+ }
+
+ return
+}
diff --git a/chatbot-ui-main/app/[locale]/page.tsx b/chatbot-ui-main/app/[locale]/page.tsx
new file mode 100644
index 0000000000..a5a0fc7daf
--- /dev/null
+++ b/chatbot-ui-main/app/[locale]/page.tsx
@@ -0,0 +1,28 @@
+"use client"
+
+import { ChatbotUISVG } from "@/components/icons/chatbotui-svg"
+import { IconArrowRight } from "@tabler/icons-react"
+import { useTheme } from "next-themes"
+import Link from "next/link"
+
+export default function HomePage() {
+ const { theme } = useTheme()
+
+ return (
+
+
+
+
+
+
Chatbot UI
+
+
+ Start Chatting
+
+
+
+ )
+}
diff --git a/chatbot-ui-main/app/[locale]/setup/page.tsx b/chatbot-ui-main/app/[locale]/setup/page.tsx
new file mode 100644
index 0000000000..700e8e94f1
--- /dev/null
+++ b/chatbot-ui-main/app/[locale]/setup/page.tsx
@@ -0,0 +1,256 @@
+"use client"
+
+import { ChatbotUIContext } from "@/context/context"
+import { getProfileByUserId, updateProfile } from "@/db/profile"
+import {
+ getHomeWorkspaceByUserId,
+ getWorkspacesByUserId
+} from "@/db/workspaces"
+import {
+ fetchHostedModels,
+ fetchOpenRouterModels
+} from "@/lib/models/fetch-models"
+import { supabase } from "@/lib/supabase/browser-client"
+import { TablesUpdate } from "@/supabase/types"
+import { useRouter } from "next/navigation"
+import { useContext, useEffect, useState } from "react"
+import { APIStep } from "../../../components/setup/api-step"
+import { FinishStep } from "../../../components/setup/finish-step"
+import { ProfileStep } from "../../../components/setup/profile-step"
+import {
+ SETUP_STEP_COUNT,
+ StepContainer
+} from "../../../components/setup/step-container"
+
+export default function SetupPage() {
+ const {
+ profile,
+ setProfile,
+ setWorkspaces,
+ setSelectedWorkspace,
+ setEnvKeyMap,
+ setAvailableHostedModels,
+ setAvailableOpenRouterModels
+ } = useContext(ChatbotUIContext)
+
+ const router = useRouter()
+
+ const [loading, setLoading] = useState(true)
+
+ const [currentStep, setCurrentStep] = useState(1)
+
+ // Profile Step
+ const [displayName, setDisplayName] = useState("")
+ const [username, setUsername] = useState(profile?.username || "")
+ const [usernameAvailable, setUsernameAvailable] = useState(true)
+
+ // API Step
+ const [useAzureOpenai, setUseAzureOpenai] = useState(false)
+ const [openaiAPIKey, setOpenaiAPIKey] = useState("")
+ const [openaiOrgID, setOpenaiOrgID] = useState("")
+ const [azureOpenaiAPIKey, setAzureOpenaiAPIKey] = useState("")
+ const [azureOpenaiEndpoint, setAzureOpenaiEndpoint] = useState("")
+ const [azureOpenai35TurboID, setAzureOpenai35TurboID] = useState("")
+ const [azureOpenai45TurboID, setAzureOpenai45TurboID] = useState("")
+ const [azureOpenai45VisionID, setAzureOpenai45VisionID] = useState("")
+ const [azureOpenaiEmbeddingsID, setAzureOpenaiEmbeddingsID] = useState("")
+ const [anthropicAPIKey, setAnthropicAPIKey] = useState("")
+ const [googleGeminiAPIKey, setGoogleGeminiAPIKey] = useState("")
+ const [mistralAPIKey, setMistralAPIKey] = useState("")
+ const [groqAPIKey, setGroqAPIKey] = useState("")
+ const [perplexityAPIKey, setPerplexityAPIKey] = useState("")
+ const [openrouterAPIKey, setOpenrouterAPIKey] = useState("")
+
+ useEffect(() => {
+ ;(async () => {
+ const session = (await supabase.auth.getSession()).data.session
+
+ if (!session) {
+ return router.push("/login")
+ } else {
+ const user = session.user
+
+ const profile = await getProfileByUserId(user.id)
+ setProfile(profile)
+ setUsername(profile.username)
+
+ if (!profile.has_onboarded) {
+ setLoading(false)
+ } else {
+ const data = await fetchHostedModels(profile)
+
+ if (!data) return
+
+ setEnvKeyMap(data.envKeyMap)
+ setAvailableHostedModels(data.hostedModels)
+
+ if (profile["openrouter_api_key"] || data.envKeyMap["openrouter"]) {
+ const openRouterModels = await fetchOpenRouterModels()
+ if (!openRouterModels) return
+ setAvailableOpenRouterModels(openRouterModels)
+ }
+
+ const homeWorkspaceId = await getHomeWorkspaceByUserId(
+ session.user.id
+ )
+ return router.push(`/${homeWorkspaceId}/chat`)
+ }
+ }
+ })()
+ }, [])
+
+ const handleShouldProceed = (proceed: boolean) => {
+ if (proceed) {
+ if (currentStep === SETUP_STEP_COUNT) {
+ handleSaveSetupSetting()
+ } else {
+ setCurrentStep(currentStep + 1)
+ }
+ } else {
+ setCurrentStep(currentStep - 1)
+ }
+ }
+
+ const handleSaveSetupSetting = async () => {
+ const session = (await supabase.auth.getSession()).data.session
+ if (!session) {
+ return router.push("/login")
+ }
+
+ const user = session.user
+ const profile = await getProfileByUserId(user.id)
+
+ const updateProfilePayload: TablesUpdate<"profiles"> = {
+ ...profile,
+ has_onboarded: true,
+ display_name: displayName,
+ username,
+ openai_api_key: openaiAPIKey,
+ openai_organization_id: openaiOrgID,
+ anthropic_api_key: anthropicAPIKey,
+ google_gemini_api_key: googleGeminiAPIKey,
+ mistral_api_key: mistralAPIKey,
+ groq_api_key: groqAPIKey,
+ perplexity_api_key: perplexityAPIKey,
+ openrouter_api_key: openrouterAPIKey,
+ use_azure_openai: useAzureOpenai,
+ azure_openai_api_key: azureOpenaiAPIKey,
+ azure_openai_endpoint: azureOpenaiEndpoint,
+ azure_openai_35_turbo_id: azureOpenai35TurboID,
+ azure_openai_45_turbo_id: azureOpenai45TurboID,
+ azure_openai_45_vision_id: azureOpenai45VisionID,
+ azure_openai_embeddings_id: azureOpenaiEmbeddingsID
+ }
+
+ const updatedProfile = await updateProfile(profile.id, updateProfilePayload)
+ setProfile(updatedProfile)
+
+ const workspaces = await getWorkspacesByUserId(profile.user_id)
+ const homeWorkspace = workspaces.find(w => w.is_home)
+
+ // There will always be a home workspace
+ setSelectedWorkspace(homeWorkspace!)
+ setWorkspaces(workspaces)
+
+ return router.push(`/${homeWorkspace?.id}/chat`)
+ }
+
+ const renderStep = (stepNum: number) => {
+ switch (stepNum) {
+ // Profile Step
+ case 1:
+ return (
+
+
+
+ )
+
+ // API Step
+ case 2:
+ return (
+
+
+
+ )
+
+ // Finish Step
+ case 3:
+ return (
+
+
+
+ )
+ default:
+ return null
+ }
+ }
+
+ if (loading) {
+ return null
+ }
+
+ return (
+
+ {renderStep(currentStep)}
+
+ )
+}
diff --git a/chatbot-ui-main/app/api/assistants/openai/route.ts b/chatbot-ui-main/app/api/assistants/openai/route.ts
new file mode 100644
index 0000000000..7e16e52b5e
--- /dev/null
+++ b/chatbot-ui-main/app/api/assistants/openai/route.ts
@@ -0,0 +1,32 @@
+import { checkApiKey, getServerProfile } from "@/lib/server/server-chat-helpers"
+import { ServerRuntime } from "next"
+import OpenAI from "openai"
+
+export const runtime: ServerRuntime = "edge"
+
+export async function GET() {
+ try {
+ const profile = await getServerProfile()
+
+ checkApiKey(profile.openai_api_key, "OpenAI")
+
+ const openai = new OpenAI({
+ apiKey: profile.openai_api_key || "",
+ organization: profile.openai_organization_id
+ })
+
+ const myAssistants = await openai.beta.assistants.list({
+ limit: 100
+ })
+
+ return new Response(JSON.stringify({ assistants: myAssistants.data }), {
+ status: 200
+ })
+ } catch (error: any) {
+ const errorMessage = error.error?.message || "An unexpected error occurred"
+ const errorCode = error.status || 500
+ return new Response(JSON.stringify({ message: errorMessage }), {
+ status: errorCode
+ })
+ }
+}
diff --git a/chatbot-ui-main/app/api/chat/anthropic/route.ts b/chatbot-ui-main/app/api/chat/anthropic/route.ts
new file mode 100644
index 0000000000..4f6242f0b1
--- /dev/null
+++ b/chatbot-ui-main/app/api/chat/anthropic/route.ts
@@ -0,0 +1,111 @@
+import { CHAT_SETTING_LIMITS } from "@/lib/chat-setting-limits"
+import { checkApiKey, getServerProfile } from "@/lib/server/server-chat-helpers"
+import { getBase64FromDataURL, getMediaTypeFromDataURL } from "@/lib/utils"
+import { ChatSettings } from "@/types"
+import Anthropic from "@anthropic-ai/sdk"
+import { AnthropicStream, StreamingTextResponse } from "ai"
+import { NextRequest, NextResponse } from "next/server"
+
+export const runtime = "edge"
+
+export async function POST(request: NextRequest) {
+ const json = await request.json()
+ const { chatSettings, messages } = json as {
+ chatSettings: ChatSettings
+ messages: any[]
+ }
+
+ try {
+ const profile = await getServerProfile()
+
+ checkApiKey(profile.anthropic_api_key, "Anthropic")
+
+ let ANTHROPIC_FORMATTED_MESSAGES: any = messages.slice(1)
+
+ ANTHROPIC_FORMATTED_MESSAGES = ANTHROPIC_FORMATTED_MESSAGES?.map(
+ (message: any) => {
+ const messageContent =
+ typeof message?.content === "string"
+ ? [message.content]
+ : message?.content
+
+ return {
+ ...message,
+ content: messageContent.map((content: any) => {
+ if (typeof content === "string") {
+ // Handle the case where content is a string
+ return { type: "text", text: content }
+ } else if (
+ content?.type === "image_url" &&
+ content?.image_url?.url?.length
+ ) {
+ return {
+ type: "image",
+ source: {
+ type: "base64",
+ media_type: getMediaTypeFromDataURL(content.image_url.url),
+ data: getBase64FromDataURL(content.image_url.url)
+ }
+ }
+ } else {
+ return content
+ }
+ })
+ }
+ }
+ )
+
+ const anthropic = new Anthropic({
+ apiKey: profile.anthropic_api_key || ""
+ })
+
+ try {
+ const response = await anthropic.messages.create({
+ model: chatSettings.model,
+ messages: ANTHROPIC_FORMATTED_MESSAGES,
+ temperature: chatSettings.temperature,
+ system: messages[0].content,
+ max_tokens:
+ CHAT_SETTING_LIMITS[chatSettings.model].MAX_TOKEN_OUTPUT_LENGTH,
+ stream: true
+ })
+
+ try {
+ const stream = AnthropicStream(response)
+ return new StreamingTextResponse(stream)
+ } catch (error: any) {
+ console.error("Error parsing Anthropic API response:", error)
+ return new NextResponse(
+ JSON.stringify({
+ message:
+ "An error occurred while parsing the Anthropic API response"
+ }),
+ { status: 500 }
+ )
+ }
+ } catch (error: any) {
+ console.error("Error calling Anthropic API:", error)
+ return new NextResponse(
+ JSON.stringify({
+ message: "An error occurred while calling the Anthropic API"
+ }),
+ { status: 500 }
+ )
+ }
+ } catch (error: any) {
+ let errorMessage = error.message || "An unexpected error occurred"
+ const errorCode = error.status || 500
+
+ if (errorMessage.toLowerCase().includes("api key not found")) {
+ errorMessage =
+ "Anthropic API Key not found. Please set it in your profile settings."
+ } else if (errorCode === 401) {
+ errorMessage =
+ "Anthropic API Key is incorrect. Please fix it in your profile settings."
+ }
+
+ return new NextResponse(JSON.stringify({ message: errorMessage }), {
+ status: errorCode
+ })
+ }
+}
diff --git a/chatbot-ui-main/app/api/chat/azure/route.ts b/chatbot-ui-main/app/api/chat/azure/route.ts
new file mode 100644
index 0000000000..642eb771fe
--- /dev/null
+++ b/chatbot-ui-main/app/api/chat/azure/route.ts
@@ -0,0 +1,72 @@
+import { checkApiKey, getServerProfile } from "@/lib/server/server-chat-helpers"
+import { ChatAPIPayload } from "@/types"
+import { OpenAIStream, StreamingTextResponse } from "ai"
+import OpenAI from "openai"
+import { ChatCompletionCreateParamsBase } from "openai/resources/chat/completions.mjs"
+
+export const runtime = "edge"
+
+export async function POST(request: Request) {
+ const json = await request.json()
+ const { chatSettings, messages } = json as ChatAPIPayload
+
+ try {
+ const profile = await getServerProfile()
+
+ checkApiKey(profile.azure_openai_api_key, "Azure OpenAI")
+
+ const ENDPOINT = profile.azure_openai_endpoint
+ const KEY = profile.azure_openai_api_key
+
+ let DEPLOYMENT_ID = ""
+ switch (chatSettings.model) {
+ case "gpt-3.5-turbo":
+ DEPLOYMENT_ID = profile.azure_openai_35_turbo_id || ""
+ break
+ case "gpt-4-turbo-preview":
+ DEPLOYMENT_ID = profile.azure_openai_45_turbo_id || ""
+ break
+ case "gpt-4-vision-preview":
+ DEPLOYMENT_ID = profile.azure_openai_45_vision_id || ""
+ break
+ default:
+ return new Response(JSON.stringify({ message: "Model not found" }), {
+ status: 400
+ })
+ }
+
+ if (!ENDPOINT || !KEY || !DEPLOYMENT_ID) {
+ return new Response(
+ JSON.stringify({ message: "Azure resources not found" }),
+ {
+ status: 400
+ }
+ )
+ }
+
+ const azureOpenai = new OpenAI({
+ apiKey: KEY,
+ baseURL: `${ENDPOINT}/openai/deployments/${DEPLOYMENT_ID}`,
+ defaultQuery: { "api-version": "2023-12-01-preview" },
+ defaultHeaders: { "api-key": KEY }
+ })
+
+ const response = await azureOpenai.chat.completions.create({
+ model: DEPLOYMENT_ID as ChatCompletionCreateParamsBase["model"],
+ messages: messages as ChatCompletionCreateParamsBase["messages"],
+ temperature: chatSettings.temperature,
+ max_tokens: chatSettings.model === "gpt-4-vision-preview" ? 4096 : null, // TODO: Fix
+ stream: true
+ })
+
+ const stream = OpenAIStream(response)
+
+ return new StreamingTextResponse(stream)
+ } catch (error: any) {
+ const errorMessage = error.error?.message || "An unexpected error occurred"
+ const errorCode = error.status || 500
+ return new Response(JSON.stringify({ message: errorMessage }), {
+ status: errorCode
+ })
+ }
+}
diff --git a/chatbot-ui-main/app/api/chat/custom/route.ts b/chatbot-ui-main/app/api/chat/custom/route.ts
new file mode 100644
index 0000000000..2c8e7c850b
--- /dev/null
+++ b/chatbot-ui-main/app/api/chat/custom/route.ts
@@ -0,0 +1,66 @@
+import { Database } from "@/supabase/types"
+import { ChatSettings } from "@/types"
+import { createClient } from "@supabase/supabase-js"
+import { OpenAIStream, StreamingTextResponse } from "ai"
+import { ServerRuntime } from "next"
+import OpenAI from "openai"
+import { ChatCompletionCreateParamsBase } from "openai/resources/chat/completions.mjs"
+
+export const runtime: ServerRuntime = "edge"
+
+export async function POST(request: Request) {
+ const json = await request.json()
+ const { chatSettings, messages, customModelId } = json as {
+ chatSettings: ChatSettings
+ messages: any[]
+ customModelId: string
+ }
+
+ try {
+ const supabaseAdmin = createClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.SUPABASE_SERVICE_ROLE_KEY!
+ )
+
+ const { data: customModel, error } = await supabaseAdmin
+ .from("models")
+ .select("*")
+ .eq("id", customModelId)
+ .single()
+
+ if (!customModel) {
+ throw new Error(error.message)
+ }
+
+ const custom = new OpenAI({
+ apiKey: customModel.api_key || "",
+ baseURL: customModel.base_url
+ })
+
+ const response = await custom.chat.completions.create({
+ model: chatSettings.model as ChatCompletionCreateParamsBase["model"],
+ messages: messages as ChatCompletionCreateParamsBase["messages"],
+ temperature: chatSettings.temperature,
+ stream: true
+ })
+
+ const stream = OpenAIStream(response)
+
+ return new StreamingTextResponse(stream)
+ } catch (error: any) {
+ let errorMessage = error.message || "An unexpected error occurred"
+ const errorCode = error.status || 500
+
+ if (errorMessage.toLowerCase().includes("api key not found")) {
+ errorMessage =
+ "Custom API Key not found. Please set it in your profile settings."
+ } else if (errorMessage.toLowerCase().includes("incorrect api key")) {
+ errorMessage =
+ "Custom API Key is incorrect. Please fix it in your profile settings."
+ }
+
+ return new Response(JSON.stringify({ message: errorMessage }), {
+ status: errorCode
+ })
+ }
+}
diff --git a/chatbot-ui-main/app/api/chat/google/route.ts b/chatbot-ui-main/app/api/chat/google/route.ts
new file mode 100644
index 0000000000..ad79139646
--- /dev/null
+++ b/chatbot-ui-main/app/api/chat/google/route.ts
@@ -0,0 +1,64 @@
+import { checkApiKey, getServerProfile } from "@/lib/server/server-chat-helpers"
+import { ChatSettings } from "@/types"
+import { GoogleGenerativeAI } from "@google/generative-ai"
+
+export const runtime = "edge"
+
+export async function POST(request: Request) {
+ const json = await request.json()
+ const { chatSettings, messages } = json as {
+ chatSettings: ChatSettings
+ messages: any[]
+ }
+
+ try {
+ const profile = await getServerProfile()
+
+ checkApiKey(profile.google_gemini_api_key, "Google")
+
+ const genAI = new GoogleGenerativeAI(profile.google_gemini_api_key || "")
+ const googleModel = genAI.getGenerativeModel({ model: chatSettings.model })
+
+ const lastMessage = messages.pop()
+
+ const chat = googleModel.startChat({
+ history: messages,
+ generationConfig: {
+ temperature: chatSettings.temperature
+ }
+ })
+
+ const response = await chat.sendMessageStream(lastMessage.parts)
+
+ const encoder = new TextEncoder()
+ const readableStream = new ReadableStream({
+ async start(controller) {
+ for await (const chunk of response.stream) {
+ const chunkText = chunk.text()
+ controller.enqueue(encoder.encode(chunkText))
+ }
+ controller.close()
+ }
+ })
+
+ return new Response(readableStream, {
+ headers: { "Content-Type": "text/plain" }
+ })
+
+ } catch (error: any) {
+ let errorMessage = error.message || "An unexpected error occurred"
+ const errorCode = error.status || 500
+
+ if (errorMessage.toLowerCase().includes("api key not found")) {
+ errorMessage =
+ "Google Gemini API Key not found. Please set it in your profile settings."
+ } else if (errorMessage.toLowerCase().includes("api key not valid")) {
+ errorMessage =
+ "Google Gemini API Key is incorrect. Please fix it in your profile settings."
+ }
+
+ return new Response(JSON.stringify({ message: errorMessage }), {
+ status: errorCode
+ })
+ }
+}
diff --git a/chatbot-ui-main/app/api/chat/groq/route.ts b/chatbot-ui-main/app/api/chat/groq/route.ts
new file mode 100644
index 0000000000..653de00d6e
--- /dev/null
+++ b/chatbot-ui-main/app/api/chat/groq/route.ts
@@ -0,0 +1,55 @@
+import { CHAT_SETTING_LIMITS } from "@/lib/chat-setting-limits"
+import { checkApiKey, getServerProfile } from "@/lib/server/server-chat-helpers"
+import { ChatSettings } from "@/types"
+import { OpenAIStream, StreamingTextResponse } from "ai"
+import OpenAI from "openai"
+
+export const runtime = "edge"
+export async function POST(request: Request) {
+ const json = await request.json()
+ const { chatSettings, messages } = json as {
+ chatSettings: ChatSettings
+ messages: any[]
+ }
+
+ try {
+ const profile = await getServerProfile()
+
+ checkApiKey(profile.groq_api_key, "G")
+
+ // Groq is compatible with the OpenAI SDK
+ const groq = new OpenAI({
+ apiKey: profile.groq_api_key || "",
+ baseURL: "https://api.groq.com/openai/v1"
+ })
+
+ const response = await groq.chat.completions.create({
+ model: chatSettings.model,
+ messages,
+ max_tokens:
+ CHAT_SETTING_LIMITS[chatSettings.model].MAX_TOKEN_OUTPUT_LENGTH,
+ stream: true
+ })
+
+ // Convert the response into a friendly text-stream.
+ const stream = OpenAIStream(response)
+
+ // Respond with the stream
+ return new StreamingTextResponse(stream)
+ } catch (error: any) {
+ let errorMessage = error.message || "An unexpected error occurred"
+ const errorCode = error.status || 500
+
+ if (errorMessage.toLowerCase().includes("api key not found")) {
+ errorMessage =
+ "Groq API Key not found. Please set it in your profile settings."
+ } else if (errorCode === 401) {
+ errorMessage =
+ "Groq API Key is incorrect. Please fix it in your profile settings."
+ }
+
+ return new Response(JSON.stringify({ message: errorMessage }), {
+ status: errorCode
+ })
+ }
+}
diff --git a/chatbot-ui-main/app/api/chat/mistral/route.ts b/chatbot-ui-main/app/api/chat/mistral/route.ts
new file mode 100644
index 0000000000..5153ca6c46
--- /dev/null
+++ b/chatbot-ui-main/app/api/chat/mistral/route.ts
@@ -0,0 +1,56 @@
+import { CHAT_SETTING_LIMITS } from "@/lib/chat-setting-limits"
+import { checkApiKey, getServerProfile } from "@/lib/server/server-chat-helpers"
+import { ChatSettings } from "@/types"
+import { OpenAIStream, StreamingTextResponse } from "ai"
+import OpenAI from "openai"
+
+export const runtime = "edge"
+
+export async function POST(request: Request) {
+ const json = await request.json()
+ const { chatSettings, messages } = json as {
+ chatSettings: ChatSettings
+ messages: any[]
+ }
+
+ try {
+ const profile = await getServerProfile()
+
+ checkApiKey(profile.mistral_api_key, "Mistral")
+
+ // Mistral is compatible the OpenAI SDK
+ const mistral = new OpenAI({
+ apiKey: profile.mistral_api_key || "",
+ baseURL: "https://api.mistral.ai/v1"
+ })
+
+ const response = await mistral.chat.completions.create({
+ model: chatSettings.model,
+ messages,
+ max_tokens:
+ CHAT_SETTING_LIMITS[chatSettings.model].MAX_TOKEN_OUTPUT_LENGTH,
+ stream: true
+ })
+
+ // Convert the response into a friendly text-stream.
+ const stream = OpenAIStream(response)
+
+ // Respond with the stream
+ return new StreamingTextResponse(stream)
+ } catch (error: any) {
+ let errorMessage = error.message || "An unexpected error occurred"
+ const errorCode = error.status || 500
+
+ if (errorMessage.toLowerCase().includes("api key not found")) {
+ errorMessage =
+ "Mistral API Key not found. Please set it in your profile settings."
+ } else if (errorCode === 401) {
+ errorMessage =
+ "Mistral API Key is incorrect. Please fix it in your profile settings."
+ }
+
+ return new Response(JSON.stringify({ message: errorMessage }), {
+ status: errorCode
+ })
+ }
+}
diff --git a/chatbot-ui-main/app/api/chat/openai/route.ts b/chatbot-ui-main/app/api/chat/openai/route.ts
new file mode 100644
index 0000000000..a0f8ad0c93
--- /dev/null
+++ b/chatbot-ui-main/app/api/chat/openai/route.ts
@@ -0,0 +1,58 @@
+import { checkApiKey, getServerProfile } from "@/lib/server/server-chat-helpers"
+import { ChatSettings } from "@/types"
+import { OpenAIStream, StreamingTextResponse } from "ai"
+import { ServerRuntime } from "next"
+import OpenAI from "openai"
+import { ChatCompletionCreateParamsBase } from "openai/resources/chat/completions.mjs"
+
+export const runtime: ServerRuntime = "edge"
+
+export async function POST(request: Request) {
+ const json = await request.json()
+ const { chatSettings, messages } = json as {
+ chatSettings: ChatSettings
+ messages: any[]
+ }
+
+ try {
+ const profile = await getServerProfile()
+
+ checkApiKey(profile.openai_api_key, "OpenAI")
+
+ const openai = new OpenAI({
+ apiKey: profile.openai_api_key || "",
+ organization: profile.openai_organization_id
+ })
+
+ const response = await openai.chat.completions.create({
+ model: chatSettings.model as ChatCompletionCreateParamsBase["model"],
+ messages: messages as ChatCompletionCreateParamsBase["messages"],
+ temperature: chatSettings.temperature,
+ max_tokens:
+ chatSettings.model === "gpt-4-vision-preview" ||
+ chatSettings.model === "gpt-4o"
+ ? 4096
+ : null, // TODO: Fix
+ stream: true
+ })
+
+ const stream = OpenAIStream(response)
+
+ return new StreamingTextResponse(stream)
+ } catch (error: any) {
+ let errorMessage = error.message || "An unexpected error occurred"
+ const errorCode = error.status || 500
+
+ if (errorMessage.toLowerCase().includes("api key not found")) {
+ errorMessage =
+ "OpenAI API Key not found. Please set it in your profile settings."
+ } else if (errorMessage.toLowerCase().includes("incorrect api key")) {
+ errorMessage =
+ "OpenAI API Key is incorrect. Please fix it in your profile settings."
+ }
+
+ return new Response(JSON.stringify({ message: errorMessage }), {
+ status: errorCode
+ })
+ }
+}
diff --git a/chatbot-ui-main/app/api/chat/openrouter/route.ts b/chatbot-ui-main/app/api/chat/openrouter/route.ts
new file mode 100644
index 0000000000..34a8a74c07
--- /dev/null
+++ b/chatbot-ui-main/app/api/chat/openrouter/route.ts
@@ -0,0 +1,51 @@
+import { checkApiKey, getServerProfile } from "@/lib/server/server-chat-helpers"
+import { ChatSettings } from "@/types"
+import { OpenAIStream, StreamingTextResponse } from "ai"
+import { ServerRuntime } from "next"
+import OpenAI from "openai"
+import { ChatCompletionCreateParamsBase } from "openai/resources/chat/completions.mjs"
+
+export const runtime: ServerRuntime = "edge"
+
+export async function POST(request: Request) {
+ const json = await request.json()
+ const { chatSettings, messages } = json as {
+ chatSettings: ChatSettings
+ messages: any[]
+ }
+
+ try {
+ const profile = await getServerProfile()
+
+ checkApiKey(profile.openrouter_api_key, "OpenRouter")
+
+ const openai = new OpenAI({
+ apiKey: profile.openrouter_api_key || "",
+ baseURL: "https://openrouter.ai/api/v1"
+ })
+
+ const response = await openai.chat.completions.create({
+ model: chatSettings.model as ChatCompletionCreateParamsBase["model"],
+ messages: messages as ChatCompletionCreateParamsBase["messages"],
+ temperature: chatSettings.temperature,
+ max_tokens: undefined,
+ stream: true
+ })
+
+ const stream = OpenAIStream(response)
+
+ return new StreamingTextResponse(stream)
+ } catch (error: any) {
+ let errorMessage = error.message || "An unexpected error occurred"
+ const errorCode = error.status || 500
+
+ if (errorMessage.toLowerCase().includes("api key not found")) {
+ errorMessage =
+ "OpenRouter API Key not found. Please set it in your profile settings."
+ }
+
+ return new Response(JSON.stringify({ message: errorMessage }), {
+ status: errorCode
+ })
+ }
+}
diff --git a/chatbot-ui-main/app/api/chat/perplexity/route.ts b/chatbot-ui-main/app/api/chat/perplexity/route.ts
new file mode 100644
index 0000000000..db700a2136
--- /dev/null
+++ b/chatbot-ui-main/app/api/chat/perplexity/route.ts
@@ -0,0 +1,51 @@
+import { checkApiKey, getServerProfile } from "@/lib/server/server-chat-helpers"
+import { ChatSettings } from "@/types"
+import { OpenAIStream, StreamingTextResponse } from "ai"
+import OpenAI from "openai"
+
+export const runtime = "edge"
+
+export async function POST(request: Request) {
+ const json = await request.json()
+ const { chatSettings, messages } = json as {
+ chatSettings: ChatSettings
+ messages: any[]
+ }
+
+ try {
+ const profile = await getServerProfile()
+
+ checkApiKey(profile.perplexity_api_key, "Perplexity")
+
+ // Perplexity is compatible the OpenAI SDK
+ const perplexity = new OpenAI({
+ apiKey: profile.perplexity_api_key || "",
+ baseURL: "https://api.perplexity.ai/"
+ })
+
+ const response = await perplexity.chat.completions.create({
+ model: chatSettings.model,
+ messages,
+ stream: true
+ })
+
+ const stream = OpenAIStream(response)
+
+ return new StreamingTextResponse(stream)
+ } catch (error: any) {
+ let errorMessage = error.message || "An unexpected error occurred"
+ const errorCode = error.status || 500
+
+ if (errorMessage.toLowerCase().includes("api key not found")) {
+ errorMessage =
+ "Perplexity API Key not found. Please set it in your profile settings."
+ } else if (errorCode === 401) {
+ errorMessage =
+ "Perplexity API Key is incorrect. Please fix it in your profile settings."
+ }
+
+ return new Response(JSON.stringify({ message: errorMessage }), {
+ status: errorCode
+ })
+ }
+}
diff --git a/chatbot-ui-main/app/api/chat/tools/route.ts b/chatbot-ui-main/app/api/chat/tools/route.ts
new file mode 100644
index 0000000000..752df25110
--- /dev/null
+++ b/chatbot-ui-main/app/api/chat/tools/route.ts
@@ -0,0 +1,218 @@
+import { openapiToFunctions } from "@/lib/openapi-conversion"
+import { checkApiKey, getServerProfile } from "@/lib/server/server-chat-helpers"
+import { Tables } from "@/supabase/types"
+import { ChatSettings } from "@/types"
+import { OpenAIStream, StreamingTextResponse } from "ai"
+import OpenAI from "openai"
+import { ChatCompletionCreateParamsBase } from "openai/resources/chat/completions.mjs"
+
+export async function POST(request: Request) {
+ const json = await request.json()
+ const { chatSettings, messages, selectedTools } = json as {
+ chatSettings: ChatSettings
+ messages: any[]
+ selectedTools: Tables<"tools">[]
+ }
+
+ try {
+ const profile = await getServerProfile()
+
+ checkApiKey(profile.openai_api_key, "OpenAI")
+
+ const openai = new OpenAI({
+ apiKey: profile.openai_api_key || "",
+ organization: profile.openai_organization_id
+ })
+
+ let allTools: OpenAI.Chat.Completions.ChatCompletionTool[] = []
+ let allRouteMaps = {}
+ let schemaDetails = []
+
+ for (const selectedTool of selectedTools) {
+ try {
+ const convertedSchema = await openapiToFunctions(
+ JSON.parse(selectedTool.schema as string)
+ )
+ const tools = convertedSchema.functions || []
+ allTools = allTools.concat(tools)
+
+ const routeMap = convertedSchema.routes.reduce(
+ (map: Record, route) => {
+ map[route.path.replace(/{(\w+)}/g, ":$1")] = route.operationId
+ return map
+ },
+ {}
+ )
+
+ allRouteMaps = { ...allRouteMaps, ...routeMap }
+
+ schemaDetails.push({
+ title: convertedSchema.info.title,
+ description: convertedSchema.info.description,
+ url: convertedSchema.info.server,
+ headers: selectedTool.custom_headers,
+ routeMap,
+ requestInBody: convertedSchema.routes[0].requestInBody
+ })
+ } catch (error: any) {
+ console.error("Error converting schema", error)
+ }
+ }
+
+ const firstResponse = await openai.chat.completions.create({
+ model: chatSettings.model as ChatCompletionCreateParamsBase["model"],
+ messages,
+ tools: allTools.length > 0 ? allTools : undefined
+ })
+
+ const message = firstResponse.choices[0].message
+ messages.push(message)
+ const toolCalls = message.tool_calls || []
+
+ if (toolCalls.length === 0) {
+ return new Response(message.content, {
+ headers: {
+ "Content-Type": "application/json"
+ }
+ })
+ }
+
+ if (toolCalls.length > 0) {
+ for (const toolCall of toolCalls) {
+ const functionCall = toolCall.function
+ const functionName = functionCall.name
+ const argumentsString = toolCall.function.arguments.trim()
+ const parsedArgs = JSON.parse(argumentsString)
+
+ // Find the schema detail that contains the function name
+ const schemaDetail = schemaDetails.find(detail =>
+ Object.values(detail.routeMap).includes(functionName)
+ )
+
+ if (!schemaDetail) {
+ throw new Error(`Function ${functionName} not found in any schema`)
+ }
+
+ const pathTemplate = Object.keys(schemaDetail.routeMap).find(
+ key => schemaDetail.routeMap[key] === functionName
+ )
+
+ if (!pathTemplate) {
+ throw new Error(`Path for function ${functionName} not found`)
+ }
+
+ const path = pathTemplate.replace(/:(\w+)/g, (_, paramName) => {
+ const value = parsedArgs.parameters[paramName]
+ if (!value) {
+ throw new Error(
+ `Parameter ${paramName} not found for function ${functionName}`
+ )
+ }
+ return encodeURIComponent(value)
+ })
+
+ if (!path) {
+ throw new Error(`Path for function ${functionName} not found`)
+ }
+
+ // Determine if the request should be in the body or as a query
+ const isRequestInBody = schemaDetail.requestInBody
+ let data = {}
+
+ if (isRequestInBody) {
+ // If the type is set to body
+ let headers = {
+ "Content-Type": "application/json"
+ }
+
+ // Check if custom headers are set
+ const customHeaders = schemaDetail.headers // Moved this line up to the loop
+ // Check if custom headers are set and are of type string
+ if (customHeaders && typeof customHeaders === "string") {
+ let parsedCustomHeaders = JSON.parse(customHeaders) as Record<
+ string,
+ string
+ >
+
+ headers = {
+ ...headers,
+ ...parsedCustomHeaders
+ }
+ }
+
+ const fullUrl = schemaDetail.url + path
+
+ const bodyContent = parsedArgs.requestBody || parsedArgs
+
+ const requestInit = {
+ method: "POST",
+ headers,
+ body: JSON.stringify(bodyContent) // Use the extracted requestBody or the entire parsedArgs
+ }
+
+ const response = await fetch(fullUrl, requestInit)
+
+ if (!response.ok) {
+ data = {
+ error: response.statusText
+ }
+ } else {
+ data = await response.json()
+ }
+ } else {
+ // If the type is set to query
+ const queryParams = new URLSearchParams(
+ parsedArgs.parameters
+ ).toString()
+ const fullUrl =
+ schemaDetail.url + path + (queryParams ? "?" + queryParams : "")
+
+ let headers = {}
+
+ // Check if custom headers are set
+ const customHeaders = schemaDetail.headers
+ if (customHeaders && typeof customHeaders === "string") {
+ headers = JSON.parse(customHeaders)
+ }
+
+ const response = await fetch(fullUrl, {
+ method: "GET",
+ headers: headers
+ })
+
+ if (!response.ok) {
+ data = {
+ error: response.statusText
+ }
+ } else {
+ data = await response.json()
+ }
+ }
+
+ messages.push({
+ tool_call_id: toolCall.id,
+ role: "tool",
+ name: functionName,
+ content: JSON.stringify(data)
+ })
+ }
+ }
+
+ const secondResponse = await openai.chat.completions.create({
+ model: chatSettings.model as ChatCompletionCreateParamsBase["model"],
+ messages,
+ stream: true
+ })
+
+ const stream = OpenAIStream(secondResponse)
+
+ return new StreamingTextResponse(stream)
+ } catch (error: any) {
+ console.error(error)
+ const errorMessage = error.error?.message || "An unexpected error occurred"
+ const errorCode = error.status || 500
+ return new Response(JSON.stringify({ message: errorMessage }), {
+ status: errorCode
+ })
+ }
+}
diff --git a/chatbot-ui-main/app/api/command/route.ts b/chatbot-ui-main/app/api/command/route.ts
new file mode 100644
index 0000000000..b10df7bd17
--- /dev/null
+++ b/chatbot-ui-main/app/api/command/route.ts
@@ -0,0 +1,54 @@
+import { CHAT_SETTING_LIMITS } from "@/lib/chat-setting-limits"
+import { checkApiKey, getServerProfile } from "@/lib/server/server-chat-helpers"
+import OpenAI from "openai"
+
+export const runtime = "edge"
+
+export async function POST(request: Request) {
+ const json = await request.json()
+ const { input } = json as {
+ input: string
+ }
+
+ try {
+ const profile = await getServerProfile()
+
+ checkApiKey(profile.openai_api_key, "OpenAI")
+
+ const openai = new OpenAI({
+ apiKey: profile.openai_api_key || "",
+ organization: profile.openai_organization_id
+ })
+
+ const response = await openai.chat.completions.create({
+ model: "gpt-4-1106-preview",
+ messages: [
+ {
+ role: "system",
+ content: "Respond to the user."
+ },
+ {
+ role: "user",
+ content: input
+ }
+ ],
+ temperature: 0,
+ max_tokens:
+ CHAT_SETTING_LIMITS["gpt-4-turbo-preview"].MAX_TOKEN_OUTPUT_LENGTH
+ // response_format: { type: "json_object" }
+ // stream: true
+ })
+
+ const content = response.choices[0].message.content
+
+ return new Response(JSON.stringify({ content }), {
+ status: 200
+ })
+ } catch (error: any) {
+ const errorMessage = error.error?.message || "An unexpected error occurred"
+ const errorCode = error.status || 500
+ return new Response(JSON.stringify({ message: errorMessage }), {
+ status: errorCode
+ })
+ }
+}
diff --git a/chatbot-ui-main/app/api/keys/route.ts b/chatbot-ui-main/app/api/keys/route.ts
new file mode 100644
index 0000000000..c0f8e38b80
--- /dev/null
+++ b/chatbot-ui-main/app/api/keys/route.ts
@@ -0,0 +1,38 @@
+import { isUsingEnvironmentKey } from "@/lib/envs"
+import { createResponse } from "@/lib/server/server-utils"
+import { EnvKey } from "@/types/key-type"
+import { VALID_ENV_KEYS } from "@/types/valid-keys"
+
+export async function GET() {
+ const envKeyMap: Record = {
+ azure: VALID_ENV_KEYS.AZURE_OPENAI_API_KEY,
+ openai: VALID_ENV_KEYS.OPENAI_API_KEY,
+ google: VALID_ENV_KEYS.GOOGLE_GEMINI_API_KEY,
+ anthropic: VALID_ENV_KEYS.ANTHROPIC_API_KEY,
+ mistral: VALID_ENV_KEYS.MISTRAL_API_KEY,
+ groq: VALID_ENV_KEYS.GROQ_API_KEY,
+ perplexity: VALID_ENV_KEYS.PERPLEXITY_API_KEY,
+ openrouter: VALID_ENV_KEYS.OPENROUTER_API_KEY,
+
+ openai_organization_id: VALID_ENV_KEYS.OPENAI_ORGANIZATION_ID,
+
+ azure_openai_endpoint: VALID_ENV_KEYS.AZURE_OPENAI_ENDPOINT,
+ azure_gpt_35_turbo_name: VALID_ENV_KEYS.AZURE_GPT_35_TURBO_NAME,
+ azure_gpt_45_vision_name: VALID_ENV_KEYS.AZURE_GPT_45_VISION_NAME,
+ azure_gpt_45_turbo_name: VALID_ENV_KEYS.AZURE_GPT_45_TURBO_NAME,
+ azure_embeddings_name: VALID_ENV_KEYS.AZURE_EMBEDDINGS_NAME
+ }
+
+ const isUsingEnvKeyMap = Object.keys(envKeyMap).reduce<
+ Record
+ >((acc, provider) => {
+ const key = envKeyMap[provider]
+
+ if (key) {
+ acc[provider] = isUsingEnvironmentKey(key as EnvKey)
+ }
+ return acc
+ }, {})
+
+ return createResponse({ isUsingEnvKeyMap }, 200)
+}
diff --git a/chatbot-ui-main/app/api/retrieval/process/docx/route.ts b/chatbot-ui-main/app/api/retrieval/process/docx/route.ts
new file mode 100644
index 0000000000..cea3d7a610
--- /dev/null
+++ b/chatbot-ui-main/app/api/retrieval/process/docx/route.ts
@@ -0,0 +1,121 @@
+import { generateLocalEmbedding } from "@/lib/generate-local-embedding"
+import { processDocX } from "@/lib/retrieval/processing"
+import { checkApiKey, getServerProfile } from "@/lib/server/server-chat-helpers"
+import { Database } from "@/supabase/types"
+import { FileItemChunk } from "@/types"
+import { createClient } from "@supabase/supabase-js"
+import { NextResponse } from "next/server"
+import OpenAI from "openai"
+
+export async function POST(req: Request) {
+ const json = await req.json()
+ const { text, fileId, embeddingsProvider, fileExtension } = json as {
+ text: string
+ fileId: string
+ embeddingsProvider: "openai" | "local"
+ fileExtension: string
+ }
+
+ try {
+ const supabaseAdmin = createClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.SUPABASE_SERVICE_ROLE_KEY!
+ )
+
+ const profile = await getServerProfile()
+
+ if (embeddingsProvider === "openai") {
+ if (profile.use_azure_openai) {
+ checkApiKey(profile.azure_openai_api_key, "Azure OpenAI")
+ } else {
+ checkApiKey(profile.openai_api_key, "OpenAI")
+ }
+ }
+
+ let chunks: FileItemChunk[] = []
+
+ switch (fileExtension) {
+ case "docx":
+ chunks = await processDocX(text)
+ break
+ default:
+ return new NextResponse("Unsupported file type", {
+ status: 400
+ })
+ }
+
+ let embeddings: any = []
+
+ let openai
+ if (profile.use_azure_openai) {
+ openai = new OpenAI({
+ apiKey: profile.azure_openai_api_key || "",
+ baseURL: `${profile.azure_openai_endpoint}/openai/deployments/${profile.azure_openai_embeddings_id}`,
+ defaultQuery: { "api-version": "2023-12-01-preview" },
+ defaultHeaders: { "api-key": profile.azure_openai_api_key }
+ })
+ } else {
+ openai = new OpenAI({
+ apiKey: profile.openai_api_key || "",
+ organization: profile.openai_organization_id
+ })
+ }
+
+ if (embeddingsProvider === "openai") {
+ const response = await openai.embeddings.create({
+ model: "text-embedding-3-small",
+ input: chunks.map(chunk => chunk.content)
+ })
+
+ embeddings = response.data.map((item: any) => {
+ return item.embedding
+ })
+ } else if (embeddingsProvider === "local") {
+ const embeddingPromises = chunks.map(async chunk => {
+ try {
+ return await generateLocalEmbedding(chunk.content)
+ } catch (error) {
+ console.error(`Error generating embedding for chunk: ${chunk}`, error)
+ return null
+ }
+ })
+
+ embeddings = await Promise.all(embeddingPromises)
+ }
+
+ const file_items = chunks.map((chunk, index) => ({
+ file_id: fileId,
+ user_id: profile.user_id,
+ content: chunk.content,
+ tokens: chunk.tokens,
+ openai_embedding:
+ embeddingsProvider === "openai"
+ ? ((embeddings[index] || null) as any)
+ : null,
+ local_embedding:
+ embeddingsProvider === "local"
+ ? ((embeddings[index] || null) as any)
+ : null
+ }))
+
+ await supabaseAdmin.from("file_items").upsert(file_items)
+
+ const totalTokens = file_items.reduce((acc, item) => acc + item.tokens, 0)
+
+ await supabaseAdmin
+ .from("files")
+ .update({ tokens: totalTokens })
+ .eq("id", fileId)
+
+ return new NextResponse("Embed Successful", {
+ status: 200
+ })
+ } catch (error: any) {
+ console.error(error)
+ const errorMessage = error.error?.message || "An unexpected error occurred"
+ const errorCode = error.status || 500
+ return new Response(JSON.stringify({ message: errorMessage }), {
+ status: errorCode
+ })
+ }
+}
diff --git a/chatbot-ui-main/app/api/retrieval/process/route.ts b/chatbot-ui-main/app/api/retrieval/process/route.ts
new file mode 100644
index 0000000000..f0221aa4ed
--- /dev/null
+++ b/chatbot-ui-main/app/api/retrieval/process/route.ts
@@ -0,0 +1,175 @@
+import { generateLocalEmbedding } from "@/lib/generate-local-embedding"
+import {
+ processCSV,
+ processJSON,
+ processMarkdown,
+ processPdf,
+ processTxt
+} from "@/lib/retrieval/processing"
+import { checkApiKey, getServerProfile } from "@/lib/server/server-chat-helpers"
+import { Database } from "@/supabase/types"
+import { FileItemChunk } from "@/types"
+import { createClient } from "@supabase/supabase-js"
+import { NextResponse } from "next/server"
+import OpenAI from "openai"
+
+export async function POST(req: Request) {
+ try {
+ const supabaseAdmin = createClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.SUPABASE_SERVICE_ROLE_KEY!
+ )
+
+ const profile = await getServerProfile()
+
+ const formData = await req.formData()
+
+ const file_id = formData.get("file_id") as string
+ const embeddingsProvider = formData.get("embeddingsProvider") as string
+
+ const { data: fileMetadata, error: metadataError } = await supabaseAdmin
+ .from("files")
+ .select("*")
+ .eq("id", file_id)
+ .single()
+
+ if (metadataError) {
+ throw new Error(
+ `Failed to retrieve file metadata: ${metadataError.message}`
+ )
+ }
+
+ if (!fileMetadata) {
+ throw new Error("File not found")
+ }
+
+ if (fileMetadata.user_id !== profile.user_id) {
+ throw new Error("Unauthorized")
+ }
+
+ const { data: file, error: fileError } = await supabaseAdmin.storage
+ .from("files")
+ .download(fileMetadata.file_path)
+
+ if (fileError)
+ throw new Error(`Failed to retrieve file: ${fileError.message}`)
+
+ const fileBuffer = Buffer.from(await file.arrayBuffer())
+ const blob = new Blob([fileBuffer])
+ const fileExtension = fileMetadata.name.split(".").pop()?.toLowerCase()
+
+ if (embeddingsProvider === "openai") {
+ try {
+ if (profile.use_azure_openai) {
+ checkApiKey(profile.azure_openai_api_key, "Azure OpenAI")
+ } else {
+ checkApiKey(profile.openai_api_key, "OpenAI")
+ }
+ } catch (error: any) {
+ error.message =
+ error.message +
+ ", make sure it is configured or else use local embeddings"
+ throw error
+ }
+ }
+
+ let chunks: FileItemChunk[] = []
+
+ switch (fileExtension) {
+ case "csv":
+ chunks = await processCSV(blob)
+ break
+ case "json":
+ chunks = await processJSON(blob)
+ break
+ case "md":
+ chunks = await processMarkdown(blob)
+ break
+ case "pdf":
+ chunks = await processPdf(blob)
+ break
+ case "txt":
+ chunks = await processTxt(blob)
+ break
+ default:
+ return new NextResponse("Unsupported file type", {
+ status: 400
+ })
+ }
+
+ let embeddings: any = []
+
+ let openai
+ if (profile.use_azure_openai) {
+ openai = new OpenAI({
+ apiKey: profile.azure_openai_api_key || "",
+ baseURL: `${profile.azure_openai_endpoint}/openai/deployments/${profile.azure_openai_embeddings_id}`,
+ defaultQuery: { "api-version": "2023-12-01-preview" },
+ defaultHeaders: { "api-key": profile.azure_openai_api_key }
+ })
+ } else {
+ openai = new OpenAI({
+ apiKey: profile.openai_api_key || "",
+ organization: profile.openai_organization_id
+ })
+ }
+
+ if (embeddingsProvider === "openai") {
+ const response = await openai.embeddings.create({
+ model: "text-embedding-3-small",
+ input: chunks.map(chunk => chunk.content)
+ })
+
+ embeddings = response.data.map((item: any) => {
+ return item.embedding
+ })
+ } else if (embeddingsProvider === "local") {
+ const embeddingPromises = chunks.map(async chunk => {
+ try {
+ return await generateLocalEmbedding(chunk.content)
+ } catch (error) {
+ console.error(`Error generating embedding for chunk: ${chunk}`, error)
+
+ return null
+ }
+ })
+
+ embeddings = await Promise.all(embeddingPromises)
+ }
+
+ const file_items = chunks.map((chunk, index) => ({
+ file_id,
+ user_id: profile.user_id,
+ content: chunk.content,
+ tokens: chunk.tokens,
+ openai_embedding:
+ embeddingsProvider === "openai"
+ ? ((embeddings[index] || null) as any)
+ : null,
+ local_embedding:
+ embeddingsProvider === "local"
+ ? ((embeddings[index] || null) as any)
+ : null
+ }))
+
+ await supabaseAdmin.from("file_items").upsert(file_items)
+
+ const totalTokens = file_items.reduce((acc, item) => acc + item.tokens, 0)
+
+ await supabaseAdmin
+ .from("files")
+ .update({ tokens: totalTokens })
+ .eq("id", file_id)
+
+ return new NextResponse("Embed Successful", {
+ status: 200
+ })
+ } catch (error: any) {
+ console.log(`Error in retrieval/process: ${error.stack}`)
+ const errorMessage = error?.message || "An unexpected error occurred"
+ const errorCode = error.status || 500
+ return new Response(JSON.stringify({ message: errorMessage }), {
+ status: errorCode
+ })
+ }
+}
diff --git a/chatbot-ui-main/app/api/retrieval/retrieve/route.ts b/chatbot-ui-main/app/api/retrieval/retrieve/route.ts
new file mode 100644
index 0000000000..9c2755a84e
--- /dev/null
+++ b/chatbot-ui-main/app/api/retrieval/retrieve/route.ts
@@ -0,0 +1,102 @@
+import { generateLocalEmbedding } from "@/lib/generate-local-embedding"
+import { checkApiKey, getServerProfile } from "@/lib/server/server-chat-helpers"
+import { Database } from "@/supabase/types"
+import { createClient } from "@supabase/supabase-js"
+import OpenAI from "openai"
+
+export async function POST(request: Request) {
+ const json = await request.json()
+ const { userInput, fileIds, embeddingsProvider, sourceCount } = json as {
+ userInput: string
+ fileIds: string[]
+ embeddingsProvider: "openai" | "local"
+ sourceCount: number
+ }
+
+ const uniqueFileIds = [...new Set(fileIds)]
+
+ try {
+ const supabaseAdmin = createClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.SUPABASE_SERVICE_ROLE_KEY!
+ )
+
+ const profile = await getServerProfile()
+
+ if (embeddingsProvider === "openai") {
+ if (profile.use_azure_openai) {
+ checkApiKey(profile.azure_openai_api_key, "Azure OpenAI")
+ } else {
+ checkApiKey(profile.openai_api_key, "OpenAI")
+ }
+ }
+
+ let chunks: any[] = []
+
+ let openai
+ if (profile.use_azure_openai) {
+ openai = new OpenAI({
+ apiKey: profile.azure_openai_api_key || "",
+ baseURL: `${profile.azure_openai_endpoint}/openai/deployments/${profile.azure_openai_embeddings_id}`,
+ defaultQuery: { "api-version": "2023-12-01-preview" },
+ defaultHeaders: { "api-key": profile.azure_openai_api_key }
+ })
+ } else {
+ openai = new OpenAI({
+ apiKey: profile.openai_api_key || "",
+ organization: profile.openai_organization_id
+ })
+ }
+
+ if (embeddingsProvider === "openai") {
+ const response = await openai.embeddings.create({
+ model: "text-embedding-3-small",
+ input: userInput
+ })
+
+ const openaiEmbedding = response.data.map(item => item.embedding)[0]
+
+ const { data: openaiFileItems, error: openaiError } =
+ await supabaseAdmin.rpc("match_file_items_openai", {
+ query_embedding: openaiEmbedding as any,
+ match_count: sourceCount,
+ file_ids: uniqueFileIds
+ })
+
+ if (openaiError) {
+ throw openaiError
+ }
+
+ chunks = openaiFileItems
+ } else if (embeddingsProvider === "local") {
+ const localEmbedding = await generateLocalEmbedding(userInput)
+
+ const { data: localFileItems, error: localFileItemsError } =
+ await supabaseAdmin.rpc("match_file_items_local", {
+ query_embedding: localEmbedding as any,
+ match_count: sourceCount,
+ file_ids: uniqueFileIds
+ })
+
+ if (localFileItemsError) {
+ throw localFileItemsError
+ }
+
+ chunks = localFileItems
+ }
+
+ const mostSimilarChunks = chunks?.sort(
+ (a, b) => b.similarity - a.similarity
+ )
+
+ return new Response(JSON.stringify({ results: mostSimilarChunks }), {
+ status: 200
+ })
+ } catch (error: any) {
+ const errorMessage = error.error?.message || "An unexpected error occurred"
+ const errorCode = error.status || 500
+ return new Response(JSON.stringify({ message: errorMessage }), {
+ status: errorCode
+ })
+ }
+}
diff --git a/chatbot-ui-main/app/api/username/available/route.ts b/chatbot-ui-main/app/api/username/available/route.ts
new file mode 100644
index 0000000000..bf00ee07f6
--- /dev/null
+++ b/chatbot-ui-main/app/api/username/available/route.ts
@@ -0,0 +1,37 @@
+import { Database } from "@/supabase/types"
+import { createClient } from "@supabase/supabase-js"
+
+export const runtime = "edge"
+
+export async function POST(request: Request) {
+ const json = await request.json()
+ const { username } = json as {
+ username: string
+ }
+
+ try {
+ const supabaseAdmin = createClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.SUPABASE_SERVICE_ROLE_KEY!
+ )
+
+ const { data: usernames, error } = await supabaseAdmin
+ .from("profiles")
+ .select("username")
+ .eq("username", username)
+
+ if (!usernames) {
+ throw new Error(error.message)
+ }
+
+ return new Response(JSON.stringify({ isAvailable: !usernames.length }), {
+ status: 200
+ })
+ } catch (error: any) {
+ const errorMessage = error.error?.message || "An unexpected error occurred"
+ const errorCode = error.status || 500
+ return new Response(JSON.stringify({ message: errorMessage }), {
+ status: errorCode
+ })
+ }
+}
diff --git a/chatbot-ui-main/app/api/username/get/route.ts b/chatbot-ui-main/app/api/username/get/route.ts
new file mode 100644
index 0000000000..d3cd158021
--- /dev/null
+++ b/chatbot-ui-main/app/api/username/get/route.ts
@@ -0,0 +1,38 @@
+import { Database } from "@/supabase/types"
+import { createClient } from "@supabase/supabase-js"
+
+export const runtime = "edge"
+
+export async function POST(request: Request) {
+ const json = await request.json()
+ const { userId } = json as {
+ userId: string
+ }
+
+ try {
+ const supabaseAdmin = createClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.SUPABASE_SERVICE_ROLE_KEY!
+ )
+
+ const { data, error } = await supabaseAdmin
+ .from("profiles")
+ .select("username")
+ .eq("user_id", userId)
+ .single()
+
+ if (!data) {
+ throw new Error(error.message)
+ }
+
+ return new Response(JSON.stringify({ username: data.username }), {
+ status: 200
+ })
+ } catch (error: any) {
+ const errorMessage = error.error?.message || "An unexpected error occurred"
+ const errorCode = error.status || 500
+ return new Response(JSON.stringify({ message: errorMessage }), {
+ status: errorCode
+ })
+ }
+}
diff --git a/chatbot-ui-main/app/auth/callback/route.ts b/chatbot-ui-main/app/auth/callback/route.ts
new file mode 100644
index 0000000000..acf1c65daa
--- /dev/null
+++ b/chatbot-ui-main/app/auth/callback/route.ts
@@ -0,0 +1,21 @@
+import { createClient } from "@/lib/supabase/server"
+import { cookies } from "next/headers"
+import { NextResponse } from "next/server"
+
+export async function GET(request: Request) {
+ const requestUrl = new URL(request.url)
+ const code = requestUrl.searchParams.get("code")
+ const next = requestUrl.searchParams.get("next")
+
+ if (code) {
+ const cookieStore = cookies()
+ const supabase = createClient(cookieStore)
+ await supabase.auth.exchangeCodeForSession(code)
+ }
+
+ if (next) {
+ return NextResponse.redirect(requestUrl.origin + next)
+ } else {
+ return NextResponse.redirect(requestUrl.origin)
+ }
+}
diff --git a/chatbot-ui-main/components.json b/chatbot-ui-main/components.json
new file mode 100644
index 0000000000..433a5ad510
--- /dev/null
+++ b/chatbot-ui-main/components.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "default",
+ "rsc": true,
+ "tsx": true,
+ "tailwind": {
+ "config": "tailwind.config.js",
+ "css": "app/globals.css",
+ "baseColor": "gray",
+ "cssVariables": true
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils"
+ }
+}
diff --git a/chatbot-ui-main/components/chat/assistant-picker.tsx b/chatbot-ui-main/components/chat/assistant-picker.tsx
new file mode 100644
index 0000000000..04e2bdded0
--- /dev/null
+++ b/chatbot-ui-main/components/chat/assistant-picker.tsx
@@ -0,0 +1,128 @@
+import { ChatbotUIContext } from "@/context/context"
+import { Tables } from "@/supabase/types"
+import { IconRobotFace } from "@tabler/icons-react"
+import Image from "next/image"
+import { FC, useContext, useEffect, useRef } from "react"
+import { usePromptAndCommand } from "./chat-hooks/use-prompt-and-command"
+
+interface AssistantPickerProps {}
+
+export const AssistantPicker: FC = ({}) => {
+ const {
+ assistants,
+ assistantImages,
+ focusAssistant,
+ atCommand,
+ isAssistantPickerOpen,
+ setIsAssistantPickerOpen
+ } = useContext(ChatbotUIContext)
+
+ const { handleSelectAssistant } = usePromptAndCommand()
+
+ const itemsRef = useRef<(HTMLDivElement | null)[]>([])
+
+ useEffect(() => {
+ if (focusAssistant && itemsRef.current[0]) {
+ itemsRef.current[0].focus()
+ }
+ }, [focusAssistant])
+
+ const filteredAssistants = assistants.filter(assistant =>
+ assistant.name.toLowerCase().includes(atCommand.toLowerCase())
+ )
+
+ const handleOpenChange = (isOpen: boolean) => {
+ setIsAssistantPickerOpen(isOpen)
+ }
+
+ const callSelectAssistant = (assistant: Tables<"assistants">) => {
+ handleSelectAssistant(assistant)
+ handleOpenChange(false)
+ }
+
+ const getKeyDownHandler =
+ (index: number) => (e: React.KeyboardEvent) => {
+ if (e.key === "Backspace") {
+ e.preventDefault()
+ handleOpenChange(false)
+ } else if (e.key === "Enter") {
+ e.preventDefault()
+ callSelectAssistant(filteredAssistants[index])
+ } else if (
+ (e.key === "Tab" || e.key === "ArrowDown") &&
+ !e.shiftKey &&
+ index === filteredAssistants.length - 1
+ ) {
+ e.preventDefault()
+ itemsRef.current[0]?.focus()
+ } else if (e.key === "ArrowUp" && !e.shiftKey && index === 0) {
+ // go to last element if arrow up is pressed on first element
+ e.preventDefault()
+ itemsRef.current[itemsRef.current.length - 1]?.focus()
+ } else if (e.key === "ArrowUp") {
+ e.preventDefault()
+ const prevIndex =
+ index - 1 >= 0 ? index - 1 : itemsRef.current.length - 1
+ itemsRef.current[prevIndex]?.focus()
+ } else if (e.key === "ArrowDown") {
+ e.preventDefault()
+ const nextIndex = index + 1 < itemsRef.current.length ? index + 1 : 0
+ itemsRef.current[nextIndex]?.focus()
+ }
+ }
+
+ return (
+ <>
+ {isAssistantPickerOpen && (
+
+ {filteredAssistants.length === 0 ? (
+
+ No matching assistants.
+
+ ) : (
+ <>
+ {filteredAssistants.map((item, index) => (
+
{
+ itemsRef.current[index] = ref
+ }}
+ tabIndex={0}
+ className="hover:bg-accent focus:bg-accent flex cursor-pointer items-center rounded p-2 focus:outline-none"
+ onClick={() =>
+ callSelectAssistant(item as Tables<"assistants">)
+ }
+ onKeyDown={getKeyDownHandler(index)}
+ >
+ {item.image_path ? (
+
image.path === item.image_path
+ )?.url || ""
+ }
+ alt={item.name}
+ width={32}
+ height={32}
+ className="rounded"
+ />
+ ) : (
+
+ )}
+
+
+
{item.name}
+
+
+ {item.description || "No description."}
+
+
+
+ ))}
+ >
+ )}
+
+ )}
+ >
+ )
+}
diff --git a/chatbot-ui-main/components/chat/chat-command-input.tsx b/chatbot-ui-main/components/chat/chat-command-input.tsx
new file mode 100644
index 0000000000..49afb0b4af
--- /dev/null
+++ b/chatbot-ui-main/components/chat/chat-command-input.tsx
@@ -0,0 +1,48 @@
+import { ChatbotUIContext } from "@/context/context"
+import { FC, useContext } from "react"
+import { AssistantPicker } from "./assistant-picker"
+import { usePromptAndCommand } from "./chat-hooks/use-prompt-and-command"
+import { FilePicker } from "./file-picker"
+import { PromptPicker } from "./prompt-picker"
+import { ToolPicker } from "./tool-picker"
+
+interface ChatCommandInputProps {}
+
+export const ChatCommandInput: FC = ({}) => {
+ const {
+ newMessageFiles,
+ chatFiles,
+ slashCommand,
+ isFilePickerOpen,
+ setIsFilePickerOpen,
+ hashtagCommand,
+ focusPrompt,
+ focusFile
+ } = useContext(ChatbotUIContext)
+
+ const { handleSelectUserFile, handleSelectUserCollection } =
+ usePromptAndCommand()
+
+ return (
+ <>
+
+
+ file.id
+ )}
+ selectedCollectionIds={[]}
+ onSelectFile={handleSelectUserFile}
+ onSelectCollection={handleSelectUserCollection}
+ isFocused={focusFile}
+ />
+
+
+
+
+ >
+ )
+}
diff --git a/chatbot-ui-main/components/chat/chat-files-display.tsx b/chatbot-ui-main/components/chat/chat-files-display.tsx
new file mode 100644
index 0000000000..a0675053c8
--- /dev/null
+++ b/chatbot-ui-main/components/chat/chat-files-display.tsx
@@ -0,0 +1,283 @@
+import { ChatbotUIContext } from "@/context/context"
+import { getFileFromStorage } from "@/db/storage/files"
+import useHotkey from "@/lib/hooks/use-hotkey"
+import { cn } from "@/lib/utils"
+import { ChatFile, MessageImage } from "@/types"
+import {
+ IconCircleFilled,
+ IconFileFilled,
+ IconFileTypeCsv,
+ IconFileTypeDocx,
+ IconFileTypePdf,
+ IconFileTypeTxt,
+ IconJson,
+ IconLoader2,
+ IconMarkdown,
+ IconX
+} from "@tabler/icons-react"
+import Image from "next/image"
+import { FC, useContext, useState } from "react"
+import { Button } from "../ui/button"
+import { FilePreview } from "../ui/file-preview"
+import { WithTooltip } from "../ui/with-tooltip"
+import { ChatRetrievalSettings } from "./chat-retrieval-settings"
+
+interface ChatFilesDisplayProps {}
+
+export const ChatFilesDisplay: FC = ({}) => {
+ useHotkey("f", () => setShowFilesDisplay(prev => !prev))
+ useHotkey("e", () => setUseRetrieval(prev => !prev))
+
+ const {
+ files,
+ newMessageImages,
+ setNewMessageImages,
+ newMessageFiles,
+ setNewMessageFiles,
+ setShowFilesDisplay,
+ showFilesDisplay,
+ chatFiles,
+ chatImages,
+ setChatImages,
+ setChatFiles,
+ setUseRetrieval
+ } = useContext(ChatbotUIContext)
+
+ const [selectedFile, setSelectedFile] = useState(null)
+ const [selectedImage, setSelectedImage] = useState(null)
+ const [showPreview, setShowPreview] = useState(false)
+
+ const messageImages = [
+ ...newMessageImages.filter(
+ image =>
+ !chatImages.some(chatImage => chatImage.messageId === image.messageId)
+ )
+ ]
+
+ const combinedChatFiles = [
+ ...newMessageFiles.filter(
+ file => !chatFiles.some(chatFile => chatFile.id === file.id)
+ ),
+ ...chatFiles
+ ]
+
+ const combinedMessageFiles = [...messageImages, ...combinedChatFiles]
+
+ const getLinkAndView = async (file: ChatFile) => {
+ const fileRecord = files.find(f => f.id === file.id)
+
+ if (!fileRecord) return
+
+ const link = await getFileFromStorage(fileRecord.file_path)
+ window.open(link, "_blank")
+ }
+
+ return showFilesDisplay && combinedMessageFiles.length > 0 ? (
+ <>
+ {showPreview && selectedImage && (
+ {
+ setShowPreview(isOpen)
+ setSelectedImage(null)
+ }}
+ />
+ )}
+
+ {showPreview && selectedFile && (
+ {
+ setShowPreview(isOpen)
+ setSelectedFile(null)
+ }}
+ />
+ )}
+
+
+
+
setShowFilesDisplay(false)}
+ >
+
+
+ Hide files
+
+ e.stopPropagation()}>
+
+
+
+
+
+
+
+ {messageImages.map((image, index) => (
+
+ {
+ setSelectedImage(image)
+ setShowPreview(true)
+ }}
+ />
+
+ {
+ e.stopPropagation()
+ setNewMessageImages(
+ newMessageImages.filter(
+ f => f.messageId !== image.messageId
+ )
+ )
+ setChatImages(
+ chatImages.filter(f => f.messageId !== image.messageId)
+ )
+ }}
+ />
+
+ ))}
+
+ {combinedChatFiles.map((file, index) =>
+ file.id === "loading" ? (
+
+
+
+
+
+
+
{file.name}
+
{file.type}
+
+
+ ) : (
+
getLinkAndView(file)}
+ >
+
+ {(() => {
+ let fileExtension = file.type.includes("/")
+ ? file.type.split("/")[1]
+ : file.type
+
+ switch (fileExtension) {
+ case "pdf":
+ return
+ case "markdown":
+ return
+ case "txt":
+ return
+ case "json":
+ return
+ case "csv":
+ return
+ case "docx":
+ return
+ default:
+ return
+ }
+ })()}
+
+
+
+
+
{
+ e.stopPropagation()
+ setNewMessageFiles(
+ newMessageFiles.filter(f => f.id !== file.id)
+ )
+ setChatFiles(chatFiles.filter(f => f.id !== file.id))
+ }}
+ />
+
+ )
+ )}
+
+
+
+ >
+ ) : (
+ combinedMessageFiles.length > 0 && (
+
+
setShowFilesDisplay(true)}
+ >
+
+
+
+ {" "}
+ View {combinedMessageFiles.length} file
+ {combinedMessageFiles.length > 1 ? "s" : ""}
+
+
+ e.stopPropagation()}>
+
+
+
+
+ )
+ )
+}
+
+const RetrievalToggle = ({}) => {
+ const { useRetrieval, setUseRetrieval } = useContext(ChatbotUIContext)
+
+ return (
+
+
+ {useRetrieval
+ ? "File retrieval is enabled on the selected files for this message. Click the indicator to disable."
+ : "Click the indicator to enable file retrieval for this message."}
+
+ }
+ trigger={
+ {
+ e.stopPropagation()
+ setUseRetrieval(prev => !prev)
+ }}
+ />
+ }
+ />
+
+ )
+}
diff --git a/chatbot-ui-main/components/chat/chat-help.tsx b/chatbot-ui-main/components/chat/chat-help.tsx
new file mode 100644
index 0000000000..4895ea4eeb
--- /dev/null
+++ b/chatbot-ui-main/components/chat/chat-help.tsx
@@ -0,0 +1,208 @@
+import useHotkey from "@/lib/hooks/use-hotkey"
+import {
+ IconBrandGithub,
+ IconBrandX,
+ IconHelpCircle,
+ IconQuestionMark
+} from "@tabler/icons-react"
+import Link from "next/link"
+import { FC, useState } from "react"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger
+} from "../ui/dropdown-menu"
+import { Announcements } from "../utility/announcements"
+
+interface ChatHelpProps {}
+
+export const ChatHelp: FC = ({}) => {
+ useHotkey("/", () => setIsOpen(prevState => !prevState))
+
+ const [isOpen, setIsOpen] = useState(false)
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Show Help
+
+
+ ⌘
+
+
+ Shift
+
+
+ /
+
+
+
+
+
+ Show Workspaces
+
+
+ ⌘
+
+
+ Shift
+
+
+ ;
+
+
+
+
+
+ New Chat
+
+
+ ⌘
+
+
+ Shift
+
+
+ O
+
+
+
+
+
+ Focus Chat
+
+
+ ⌘
+
+
+ Shift
+
+
+ L
+
+
+
+
+
+ Toggle Files
+
+
+ ⌘
+
+
+ Shift
+
+
+ F
+
+
+
+
+
+ Toggle Retrieval
+
+
+ ⌘
+
+
+ Shift
+
+
+ E
+
+
+
+
+
+ Open Settings
+
+
+ ⌘
+
+
+ Shift
+
+
+ I
+
+
+
+
+
+ Open Quick Settings
+
+
+ ⌘
+
+
+ Shift
+
+
+ P
+
+
+
+
+
+ Toggle Sidebar
+
+
+ ⌘
+
+
+ Shift
+
+
+ S
+
+
+
+
+
+ )
+}
diff --git a/chatbot-ui-main/components/chat/chat-helpers/index.ts b/chatbot-ui-main/components/chat/chat-helpers/index.ts
new file mode 100644
index 0000000000..17a2089638
--- /dev/null
+++ b/chatbot-ui-main/components/chat/chat-helpers/index.ts
@@ -0,0 +1,511 @@
+// Only used in use-chat-handler.tsx to keep it clean
+
+import { createChatFiles } from "@/db/chat-files"
+import { createChat } from "@/db/chats"
+import { createMessageFileItems } from "@/db/message-file-items"
+import { createMessages, updateMessage } from "@/db/messages"
+import { uploadMessageImage } from "@/db/storage/message-images"
+import {
+ buildFinalMessages,
+ adaptMessagesForGoogleGemini
+} from "@/lib/build-prompt"
+import { consumeReadableStream } from "@/lib/consume-stream"
+import { Tables, TablesInsert } from "@/supabase/types"
+import {
+ ChatFile,
+ ChatMessage,
+ ChatPayload,
+ ChatSettings,
+ LLM,
+ MessageImage
+} from "@/types"
+import React from "react"
+import { toast } from "sonner"
+import { v4 as uuidv4 } from "uuid"
+
+export const validateChatSettings = (
+ chatSettings: ChatSettings | null,
+ modelData: LLM | undefined,
+ profile: Tables<"profiles"> | null,
+ selectedWorkspace: Tables<"workspaces"> | null,
+ messageContent: string
+) => {
+ if (!chatSettings) {
+ throw new Error("Chat settings not found")
+ }
+
+ if (!modelData) {
+ throw new Error("Model not found")
+ }
+
+ if (!profile) {
+ throw new Error("Profile not found")
+ }
+
+ if (!selectedWorkspace) {
+ throw new Error("Workspace not found")
+ }
+
+ if (!messageContent) {
+ throw new Error("Message content not found")
+ }
+}
+
+export const handleRetrieval = async (
+ userInput: string,
+ newMessageFiles: ChatFile[],
+ chatFiles: ChatFile[],
+ embeddingsProvider: "openai" | "local",
+ sourceCount: number
+) => {
+ const response = await fetch("/api/retrieval/retrieve", {
+ method: "POST",
+ body: JSON.stringify({
+ userInput,
+ fileIds: [...newMessageFiles, ...chatFiles].map(file => file.id),
+ embeddingsProvider,
+ sourceCount
+ })
+ })
+
+ if (!response.ok) {
+ console.error("Error retrieving:", response)
+ }
+
+ const { results } = (await response.json()) as {
+ results: Tables<"file_items">[]
+ }
+
+ return results
+}
+
+export const createTempMessages = (
+ messageContent: string,
+ chatMessages: ChatMessage[],
+ chatSettings: ChatSettings,
+ b64Images: string[],
+ isRegeneration: boolean,
+ setChatMessages: React.Dispatch>,
+ selectedAssistant: Tables<"assistants"> | null
+) => {
+ let tempUserChatMessage: ChatMessage = {
+ message: {
+ chat_id: "",
+ assistant_id: null,
+ content: messageContent,
+ created_at: "",
+ id: uuidv4(),
+ image_paths: b64Images,
+ model: chatSettings.model,
+ role: "user",
+ sequence_number: chatMessages.length,
+ updated_at: "",
+ user_id: ""
+ },
+ fileItems: []
+ }
+
+ let tempAssistantChatMessage: ChatMessage = {
+ message: {
+ chat_id: "",
+ assistant_id: selectedAssistant?.id || null,
+ content: "",
+ created_at: "",
+ id: uuidv4(),
+ image_paths: [],
+ model: chatSettings.model,
+ role: "assistant",
+ sequence_number: chatMessages.length + 1,
+ updated_at: "",
+ user_id: ""
+ },
+ fileItems: []
+ }
+
+ let newMessages = []
+
+ if (isRegeneration) {
+ const lastMessageIndex = chatMessages.length - 1
+ chatMessages[lastMessageIndex].message.content = ""
+ newMessages = [...chatMessages]
+ } else {
+ newMessages = [
+ ...chatMessages,
+ tempUserChatMessage,
+ tempAssistantChatMessage
+ ]
+ }
+
+ setChatMessages(newMessages)
+
+ return {
+ tempUserChatMessage,
+ tempAssistantChatMessage
+ }
+}
+
+export const handleLocalChat = async (
+ payload: ChatPayload,
+ profile: Tables<"profiles">,
+ chatSettings: ChatSettings,
+ tempAssistantMessage: ChatMessage,
+ isRegeneration: boolean,
+ newAbortController: AbortController,
+ setIsGenerating: React.Dispatch>,
+ setFirstTokenReceived: React.Dispatch>,
+ setChatMessages: React.Dispatch>,
+ setToolInUse: React.Dispatch>
+) => {
+ const formattedMessages = await buildFinalMessages(payload, profile, [])
+
+ // Ollama API: https://github.com/jmorganca/ollama/blob/main/docs/api.md
+ const response = await fetchChatResponse(
+ process.env.NEXT_PUBLIC_OLLAMA_URL + "/api/chat",
+ {
+ model: chatSettings.model,
+ messages: formattedMessages,
+ options: {
+ temperature: payload.chatSettings.temperature
+ }
+ },
+ false,
+ newAbortController,
+ setIsGenerating,
+ setChatMessages
+ )
+
+ return await processResponse(
+ response,
+ isRegeneration
+ ? payload.chatMessages[payload.chatMessages.length - 1]
+ : tempAssistantMessage,
+ false,
+ newAbortController,
+ setFirstTokenReceived,
+ setChatMessages,
+ setToolInUse
+ )
+}
+
+export const handleHostedChat = async (
+ payload: ChatPayload,
+ profile: Tables<"profiles">,
+ modelData: LLM,
+ tempAssistantChatMessage: ChatMessage,
+ isRegeneration: boolean,
+ newAbortController: AbortController,
+ newMessageImages: MessageImage[],
+ chatImages: MessageImage[],
+ setIsGenerating: React.Dispatch>,
+ setFirstTokenReceived: React.Dispatch>,
+ setChatMessages: React.Dispatch>,
+ setToolInUse: React.Dispatch>
+) => {
+ const provider =
+ modelData.provider === "openai" && profile.use_azure_openai
+ ? "azure"
+ : modelData.provider
+
+ let draftMessages = await buildFinalMessages(payload, profile, chatImages)
+
+ let formattedMessages : any[] = []
+ if (provider === "google") {
+ formattedMessages = await adaptMessagesForGoogleGemini(payload, draftMessages)
+ } else {
+ formattedMessages = draftMessages
+ }
+
+ const apiEndpoint =
+ provider === "custom" ? "/api/chat/custom" : `/api/chat/${provider}`
+
+ const requestBody = {
+ chatSettings: payload.chatSettings,
+ messages: formattedMessages,
+ customModelId: provider === "custom" ? modelData.hostedId : ""
+ }
+
+ const response = await fetchChatResponse(
+ apiEndpoint,
+ requestBody,
+ true,
+ newAbortController,
+ setIsGenerating,
+ setChatMessages
+ )
+
+ return await processResponse(
+ response,
+ isRegeneration
+ ? payload.chatMessages[payload.chatMessages.length - 1]
+ : tempAssistantChatMessage,
+ true,
+ newAbortController,
+ setFirstTokenReceived,
+ setChatMessages,
+ setToolInUse
+ )
+}
+
+export const fetchChatResponse = async (
+ url: string,
+ body: object,
+ isHosted: boolean,
+ controller: AbortController,
+ setIsGenerating: React.Dispatch>,
+ setChatMessages: React.Dispatch>
+) => {
+ const response = await fetch(url, {
+ method: "POST",
+ body: JSON.stringify(body),
+ signal: controller.signal
+ })
+
+ if (!response.ok) {
+ if (response.status === 404 && !isHosted) {
+ toast.error(
+ "Model not found. Make sure you have it downloaded via Ollama."
+ )
+ }
+
+ const errorData = await response.json()
+
+ toast.error(errorData.message)
+
+ setIsGenerating(false)
+ setChatMessages(prevMessages => prevMessages.slice(0, -2))
+ }
+
+ return response
+}
+
+export const processResponse = async (
+ response: Response,
+ lastChatMessage: ChatMessage,
+ isHosted: boolean,
+ controller: AbortController,
+ setFirstTokenReceived: React.Dispatch>,
+ setChatMessages: React.Dispatch>,
+ setToolInUse: React.Dispatch>
+) => {
+ let fullText = ""
+ let contentToAdd = ""
+
+ if (response.body) {
+ await consumeReadableStream(
+ response.body,
+ chunk => {
+ setFirstTokenReceived(true)
+ setToolInUse("none")
+
+ try {
+ contentToAdd = isHosted
+ ? chunk
+ : // Ollama's streaming endpoint returns new-line separated JSON
+ // objects. A chunk may have more than one of these objects, so we
+ // need to split the chunk by new-lines and handle each one
+ // separately.
+ chunk
+ .trimEnd()
+ .split("\n")
+ .reduce(
+ (acc, line) => acc + JSON.parse(line).message.content,
+ ""
+ )
+ fullText += contentToAdd
+ } catch (error) {
+ console.error("Error parsing JSON:", error)
+ }
+
+ setChatMessages(prev =>
+ prev.map(chatMessage => {
+ if (chatMessage.message.id === lastChatMessage.message.id) {
+ const updatedChatMessage: ChatMessage = {
+ message: {
+ ...chatMessage.message,
+ content: fullText
+ },
+ fileItems: chatMessage.fileItems
+ }
+
+ return updatedChatMessage
+ }
+
+ return chatMessage
+ })
+ )
+ },
+ controller.signal
+ )
+
+ return fullText
+ } else {
+ throw new Error("Response body is null")
+ }
+}
+
+export const handleCreateChat = async (
+ chatSettings: ChatSettings,
+ profile: Tables<"profiles">,
+ selectedWorkspace: Tables<"workspaces">,
+ messageContent: string,
+ selectedAssistant: Tables<"assistants">,
+ newMessageFiles: ChatFile[],
+ setSelectedChat: React.Dispatch | null>>,
+ setChats: React.Dispatch[]>>,
+ setChatFiles: React.Dispatch>
+) => {
+ const createdChat = await createChat({
+ user_id: profile.user_id,
+ workspace_id: selectedWorkspace.id,
+ assistant_id: selectedAssistant?.id || null,
+ context_length: chatSettings.contextLength,
+ include_profile_context: chatSettings.includeProfileContext,
+ include_workspace_instructions: chatSettings.includeWorkspaceInstructions,
+ model: chatSettings.model,
+ name: messageContent.substring(0, 100),
+ prompt: chatSettings.prompt,
+ temperature: chatSettings.temperature,
+ embeddings_provider: chatSettings.embeddingsProvider
+ })
+
+ setSelectedChat(createdChat)
+ setChats(chats => [createdChat, ...chats])
+
+ await createChatFiles(
+ newMessageFiles.map(file => ({
+ user_id: profile.user_id,
+ chat_id: createdChat.id,
+ file_id: file.id
+ }))
+ )
+
+ setChatFiles(prev => [...prev, ...newMessageFiles])
+
+ return createdChat
+}
+
+export const handleCreateMessages = async (
+ chatMessages: ChatMessage[],
+ currentChat: Tables<"chats">,
+ profile: Tables<"profiles">,
+ modelData: LLM,
+ messageContent: string,
+ generatedText: string,
+ newMessageImages: MessageImage[],
+ isRegeneration: boolean,
+ retrievedFileItems: Tables<"file_items">[],
+ setChatMessages: React.Dispatch>,
+ setChatFileItems: React.Dispatch<
+ React.SetStateAction[]>
+ >,
+ setChatImages: React.Dispatch>,
+ selectedAssistant: Tables<"assistants"> | null
+) => {
+ const finalUserMessage: TablesInsert<"messages"> = {
+ chat_id: currentChat.id,
+ assistant_id: null,
+ user_id: profile.user_id,
+ content: messageContent,
+ model: modelData.modelId,
+ role: "user",
+ sequence_number: chatMessages.length,
+ image_paths: []
+ }
+
+ const finalAssistantMessage: TablesInsert<"messages"> = {
+ chat_id: currentChat.id,
+ assistant_id: selectedAssistant?.id || null,
+ user_id: profile.user_id,
+ content: generatedText,
+ model: modelData.modelId,
+ role: "assistant",
+ sequence_number: chatMessages.length + 1,
+ image_paths: []
+ }
+
+ let finalChatMessages: ChatMessage[] = []
+
+ if (isRegeneration) {
+ const lastStartingMessage = chatMessages[chatMessages.length - 1].message
+
+ const updatedMessage = await updateMessage(lastStartingMessage.id, {
+ ...lastStartingMessage,
+ content: generatedText
+ })
+
+ chatMessages[chatMessages.length - 1].message = updatedMessage
+
+ finalChatMessages = [...chatMessages]
+
+ setChatMessages(finalChatMessages)
+ } else {
+ const createdMessages = await createMessages([
+ finalUserMessage,
+ finalAssistantMessage
+ ])
+
+ // Upload each image (stored in newMessageImages) for the user message to message_images bucket
+ const uploadPromises = newMessageImages
+ .filter(obj => obj.file !== null)
+ .map(obj => {
+ let filePath = `${profile.user_id}/${currentChat.id}/${
+ createdMessages[0].id
+ }/${uuidv4()}`
+
+ return uploadMessageImage(filePath, obj.file as File).catch(error => {
+ console.error(`Failed to upload image at ${filePath}:`, error)
+ return null
+ })
+ })
+
+ const paths = (await Promise.all(uploadPromises)).filter(
+ Boolean
+ ) as string[]
+
+ setChatImages(prevImages => [
+ ...prevImages,
+ ...newMessageImages.map((obj, index) => ({
+ ...obj,
+ messageId: createdMessages[0].id,
+ path: paths[index]
+ }))
+ ])
+
+ const updatedMessage = await updateMessage(createdMessages[0].id, {
+ ...createdMessages[0],
+ image_paths: paths
+ })
+
+ const createdMessageFileItems = await createMessageFileItems(
+ retrievedFileItems.map(fileItem => {
+ return {
+ user_id: profile.user_id,
+ message_id: createdMessages[1].id,
+ file_item_id: fileItem.id
+ }
+ })
+ )
+
+ finalChatMessages = [
+ ...chatMessages,
+ {
+ message: updatedMessage,
+ fileItems: []
+ },
+ {
+ message: createdMessages[1],
+ fileItems: retrievedFileItems.map(fileItem => fileItem.id)
+ }
+ ]
+
+ setChatFileItems(prevFileItems => {
+ const newFileItems = retrievedFileItems.filter(
+ fileItem => !prevFileItems.some(prevItem => prevItem.id === fileItem.id)
+ )
+
+ return [...prevFileItems, ...newFileItems]
+ })
+
+ setChatMessages(finalChatMessages)
+ }
+}
diff --git a/chatbot-ui-main/components/chat/chat-hooks/use-chat-handler.tsx b/chatbot-ui-main/components/chat/chat-hooks/use-chat-handler.tsx
new file mode 100644
index 0000000000..f5ab04a25a
--- /dev/null
+++ b/chatbot-ui-main/components/chat/chat-hooks/use-chat-handler.tsx
@@ -0,0 +1,422 @@
+import { ChatbotUIContext } from "@/context/context"
+import { getAssistantCollectionsByAssistantId } from "@/db/assistant-collections"
+import { getAssistantFilesByAssistantId } from "@/db/assistant-files"
+import { getAssistantToolsByAssistantId } from "@/db/assistant-tools"
+import { updateChat } from "@/db/chats"
+import { getCollectionFilesByCollectionId } from "@/db/collection-files"
+import { deleteMessagesIncludingAndAfter } from "@/db/messages"
+import { buildFinalMessages } from "@/lib/build-prompt"
+import { Tables } from "@/supabase/types"
+import { ChatMessage, ChatPayload, LLMID, ModelProvider } from "@/types"
+import { useRouter } from "next/navigation"
+import { useContext, useEffect, useRef } from "react"
+import { LLM_LIST } from "../../../lib/models/llm/llm-list"
+import {
+ createTempMessages,
+ handleCreateChat,
+ handleCreateMessages,
+ handleHostedChat,
+ handleLocalChat,
+ handleRetrieval,
+ processResponse,
+ validateChatSettings
+} from "../chat-helpers"
+
+export const useChatHandler = () => {
+ const router = useRouter()
+
+ const {
+ userInput,
+ chatFiles,
+ setUserInput,
+ setNewMessageImages,
+ profile,
+ setIsGenerating,
+ setChatMessages,
+ setFirstTokenReceived,
+ selectedChat,
+ selectedWorkspace,
+ setSelectedChat,
+ setChats,
+ setSelectedTools,
+ availableLocalModels,
+ availableOpenRouterModels,
+ abortController,
+ setAbortController,
+ chatSettings,
+ newMessageImages,
+ selectedAssistant,
+ chatMessages,
+ chatImages,
+ setChatImages,
+ setChatFiles,
+ setNewMessageFiles,
+ setShowFilesDisplay,
+ newMessageFiles,
+ chatFileItems,
+ setChatFileItems,
+ setToolInUse,
+ useRetrieval,
+ sourceCount,
+ setIsPromptPickerOpen,
+ setIsFilePickerOpen,
+ selectedTools,
+ selectedPreset,
+ setChatSettings,
+ models,
+ isPromptPickerOpen,
+ isFilePickerOpen,
+ isToolPickerOpen
+ } = useContext(ChatbotUIContext)
+
+ const chatInputRef = useRef(null)
+
+ useEffect(() => {
+ if (!isPromptPickerOpen || !isFilePickerOpen || !isToolPickerOpen) {
+ chatInputRef.current?.focus()
+ }
+ }, [isPromptPickerOpen, isFilePickerOpen, isToolPickerOpen])
+
+ const handleNewChat = async () => {
+ if (!selectedWorkspace) return
+
+ setUserInput("")
+ setChatMessages([])
+ setSelectedChat(null)
+ setChatFileItems([])
+
+ setIsGenerating(false)
+ setFirstTokenReceived(false)
+
+ setChatFiles([])
+ setChatImages([])
+ setNewMessageFiles([])
+ setNewMessageImages([])
+ setShowFilesDisplay(false)
+ setIsPromptPickerOpen(false)
+ setIsFilePickerOpen(false)
+
+ setSelectedTools([])
+ setToolInUse("none")
+
+ if (selectedAssistant) {
+ setChatSettings({
+ model: selectedAssistant.model as LLMID,
+ prompt: selectedAssistant.prompt,
+ temperature: selectedAssistant.temperature,
+ contextLength: selectedAssistant.context_length,
+ includeProfileContext: selectedAssistant.include_profile_context,
+ includeWorkspaceInstructions:
+ selectedAssistant.include_workspace_instructions,
+ embeddingsProvider: selectedAssistant.embeddings_provider as
+ | "openai"
+ | "local"
+ })
+
+ let allFiles = []
+
+ const assistantFiles = (
+ await getAssistantFilesByAssistantId(selectedAssistant.id)
+ ).files
+ allFiles = [...assistantFiles]
+ const assistantCollections = (
+ await getAssistantCollectionsByAssistantId(selectedAssistant.id)
+ ).collections
+ for (const collection of assistantCollections) {
+ const collectionFiles = (
+ await getCollectionFilesByCollectionId(collection.id)
+ ).files
+ allFiles = [...allFiles, ...collectionFiles]
+ }
+ const assistantTools = (
+ await getAssistantToolsByAssistantId(selectedAssistant.id)
+ ).tools
+
+ setSelectedTools(assistantTools)
+ setChatFiles(
+ allFiles.map(file => ({
+ id: file.id,
+ name: file.name,
+ type: file.type,
+ file: null
+ }))
+ )
+
+ if (allFiles.length > 0) setShowFilesDisplay(true)
+ } else if (selectedPreset) {
+ setChatSettings({
+ model: selectedPreset.model as LLMID,
+ prompt: selectedPreset.prompt,
+ temperature: selectedPreset.temperature,
+ contextLength: selectedPreset.context_length,
+ includeProfileContext: selectedPreset.include_profile_context,
+ includeWorkspaceInstructions:
+ selectedPreset.include_workspace_instructions,
+ embeddingsProvider: selectedPreset.embeddings_provider as
+ | "openai"
+ | "local"
+ })
+ } else if (selectedWorkspace) {
+ // setChatSettings({
+ // model: (selectedWorkspace.default_model ||
+ // "gpt-4-1106-preview") as LLMID,
+ // prompt:
+ // selectedWorkspace.default_prompt ||
+ // "You are a friendly, helpful AI assistant.",
+ // temperature: selectedWorkspace.default_temperature || 0.5,
+ // contextLength: selectedWorkspace.default_context_length || 4096,
+ // includeProfileContext:
+ // selectedWorkspace.include_profile_context || true,
+ // includeWorkspaceInstructions:
+ // selectedWorkspace.include_workspace_instructions || true,
+ // embeddingsProvider:
+ // (selectedWorkspace.embeddings_provider as "openai" | "local") ||
+ // "openai"
+ // })
+ }
+
+ return router.push(`/${selectedWorkspace.id}/chat`)
+ }
+
+ const handleFocusChatInput = () => {
+ chatInputRef.current?.focus()
+ }
+
+ const handleStopMessage = () => {
+ if (abortController) {
+ abortController.abort()
+ }
+ }
+
+ const handleSendMessage = async (
+ messageContent: string,
+ chatMessages: ChatMessage[],
+ isRegeneration: boolean
+ ) => {
+ const startingInput = messageContent
+
+ try {
+ setUserInput("")
+ setIsGenerating(true)
+ setIsPromptPickerOpen(false)
+ setIsFilePickerOpen(false)
+ setNewMessageImages([])
+
+ const newAbortController = new AbortController()
+ setAbortController(newAbortController)
+
+ const modelData = [
+ ...models.map(model => ({
+ modelId: model.model_id as LLMID,
+ modelName: model.name,
+ provider: "custom" as ModelProvider,
+ hostedId: model.id,
+ platformLink: "",
+ imageInput: false
+ })),
+ ...LLM_LIST,
+ ...availableLocalModels,
+ ...availableOpenRouterModels
+ ].find(llm => llm.modelId === chatSettings?.model)
+
+ validateChatSettings(
+ chatSettings,
+ modelData,
+ profile,
+ selectedWorkspace,
+ messageContent
+ )
+
+ let currentChat = selectedChat ? { ...selectedChat } : null
+
+ const b64Images = newMessageImages.map(image => image.base64)
+
+ let retrievedFileItems: Tables<"file_items">[] = []
+
+ if (
+ (newMessageFiles.length > 0 || chatFiles.length > 0) &&
+ useRetrieval
+ ) {
+ setToolInUse("retrieval")
+
+ retrievedFileItems = await handleRetrieval(
+ userInput,
+ newMessageFiles,
+ chatFiles,
+ chatSettings!.embeddingsProvider,
+ sourceCount
+ )
+ }
+
+ const { tempUserChatMessage, tempAssistantChatMessage } =
+ createTempMessages(
+ messageContent,
+ chatMessages,
+ chatSettings!,
+ b64Images,
+ isRegeneration,
+ setChatMessages,
+ selectedAssistant
+ )
+
+ let payload: ChatPayload = {
+ chatSettings: chatSettings!,
+ workspaceInstructions: selectedWorkspace!.instructions || "",
+ chatMessages: isRegeneration
+ ? [...chatMessages]
+ : [...chatMessages, tempUserChatMessage],
+ assistant: selectedChat?.assistant_id ? selectedAssistant : null,
+ messageFileItems: retrievedFileItems,
+ chatFileItems: chatFileItems
+ }
+
+ let generatedText = ""
+
+ if (selectedTools.length > 0) {
+ setToolInUse("Tools")
+
+ const formattedMessages = await buildFinalMessages(
+ payload,
+ profile!,
+ chatImages
+ )
+
+ const response = await fetch("/api/chat/tools", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json"
+ },
+ body: JSON.stringify({
+ chatSettings: payload.chatSettings,
+ messages: formattedMessages,
+ selectedTools
+ })
+ })
+
+ setToolInUse("none")
+
+ generatedText = await processResponse(
+ response,
+ isRegeneration
+ ? payload.chatMessages[payload.chatMessages.length - 1]
+ : tempAssistantChatMessage,
+ true,
+ newAbortController,
+ setFirstTokenReceived,
+ setChatMessages,
+ setToolInUse
+ )
+ } else {
+ if (modelData!.provider === "ollama") {
+ generatedText = await handleLocalChat(
+ payload,
+ profile!,
+ chatSettings!,
+ tempAssistantChatMessage,
+ isRegeneration,
+ newAbortController,
+ setIsGenerating,
+ setFirstTokenReceived,
+ setChatMessages,
+ setToolInUse
+ )
+ } else {
+ generatedText = await handleHostedChat(
+ payload,
+ profile!,
+ modelData!,
+ tempAssistantChatMessage,
+ isRegeneration,
+ newAbortController,
+ newMessageImages,
+ chatImages,
+ setIsGenerating,
+ setFirstTokenReceived,
+ setChatMessages,
+ setToolInUse
+ )
+ }
+ }
+
+ if (!currentChat) {
+ currentChat = await handleCreateChat(
+ chatSettings!,
+ profile!,
+ selectedWorkspace!,
+ messageContent,
+ selectedAssistant!,
+ newMessageFiles,
+ setSelectedChat,
+ setChats,
+ setChatFiles
+ )
+ } else {
+ const updatedChat = await updateChat(currentChat.id, {
+ updated_at: new Date().toISOString()
+ })
+
+ setChats(prevChats => {
+ const updatedChats = prevChats.map(prevChat =>
+ prevChat.id === updatedChat.id ? updatedChat : prevChat
+ )
+
+ return updatedChats
+ })
+ }
+
+ await handleCreateMessages(
+ chatMessages,
+ currentChat,
+ profile!,
+ modelData!,
+ messageContent,
+ generatedText,
+ newMessageImages,
+ isRegeneration,
+ retrievedFileItems,
+ setChatMessages,
+ setChatFileItems,
+ setChatImages,
+ selectedAssistant
+ )
+
+ setIsGenerating(false)
+ setFirstTokenReceived(false)
+ } catch (error) {
+ setIsGenerating(false)
+ setFirstTokenReceived(false)
+ setUserInput(startingInput)
+ }
+ }
+
+ const handleSendEdit = async (
+ editedContent: string,
+ sequenceNumber: number
+ ) => {
+ if (!selectedChat) return
+
+ await deleteMessagesIncludingAndAfter(
+ selectedChat.user_id,
+ selectedChat.id,
+ sequenceNumber
+ )
+
+ const filteredMessages = chatMessages.filter(
+ chatMessage => chatMessage.message.sequence_number < sequenceNumber
+ )
+
+ setChatMessages(filteredMessages)
+
+ handleSendMessage(editedContent, filteredMessages, false)
+ }
+
+ return {
+ chatInputRef,
+ prompt,
+ handleNewChat,
+ handleSendMessage,
+ handleFocusChatInput,
+ handleStopMessage,
+ handleSendEdit
+ }
+}
diff --git a/chatbot-ui-main/components/chat/chat-hooks/use-chat-history.tsx b/chatbot-ui-main/components/chat/chat-hooks/use-chat-history.tsx
new file mode 100644
index 0000000000..cbb376eb06
--- /dev/null
+++ b/chatbot-ui-main/components/chat/chat-hooks/use-chat-history.tsx
@@ -0,0 +1,77 @@
+import { ChatbotUIContext } from "@/context/context"
+import { useContext, useEffect, useState } from "react"
+
+/**
+ * Custom hook for handling chat history in the chat component.
+ * It provides functions to set the new message content to the previous or next user message in the chat history.
+ *
+ * @returns An object containing the following functions:
+ * - setNewMessageContentToPreviousUserMessage: Sets the new message content to the previous user message.
+ * - setNewMessageContentToNextUserMessage: Sets the new message content to the next user message in the chat history.
+ */
+export const useChatHistoryHandler = () => {
+ const { setUserInput, chatMessages, isGenerating } =
+ useContext(ChatbotUIContext)
+ const userRoleString = "user"
+
+ const [messageHistoryIndex, setMessageHistoryIndex] = useState(
+ chatMessages.length
+ )
+
+ useEffect(() => {
+ // If messages get deleted the history index pointed could be out of bounds
+ if (!isGenerating && messageHistoryIndex > chatMessages.length)
+ setMessageHistoryIndex(chatMessages.length)
+ }, [chatMessages, isGenerating, messageHistoryIndex])
+
+ /**
+ * Sets the new message content to the previous user message.
+ */
+ const setNewMessageContentToPreviousUserMessage = () => {
+ let tempIndex = messageHistoryIndex
+ while (
+ tempIndex > 0 &&
+ chatMessages[tempIndex - 1].message.role !== userRoleString
+ ) {
+ tempIndex--
+ }
+
+ const previousUserMessage =
+ chatMessages.length > 0 && tempIndex > 0
+ ? chatMessages[tempIndex - 1]
+ : null
+ if (previousUserMessage) {
+ setUserInput(previousUserMessage.message.content)
+ setMessageHistoryIndex(tempIndex - 1)
+ }
+ }
+
+ /**
+ * Sets the new message content to the next user message in the chat history.
+ * If there is a next user message, it updates the user input and message history index accordingly.
+ * If there is no next user message, it resets the user input and sets the message history index to the end of the chat history.
+ */
+ const setNewMessageContentToNextUserMessage = () => {
+ let tempIndex = messageHistoryIndex
+ while (
+ tempIndex < chatMessages.length - 1 &&
+ chatMessages[tempIndex + 1].message.role !== userRoleString
+ ) {
+ tempIndex++
+ }
+
+ const nextUserMessage =
+ chatMessages.length > 0 && tempIndex < chatMessages.length - 1
+ ? chatMessages[tempIndex + 1]
+ : null
+ setUserInput(nextUserMessage?.message.content || "")
+ setMessageHistoryIndex(
+ nextUserMessage ? tempIndex + 1 : chatMessages.length
+ )
+ }
+
+ return {
+ setNewMessageContentToPreviousUserMessage,
+ setNewMessageContentToNextUserMessage
+ }
+}
diff --git a/chatbot-ui-main/components/chat/chat-hooks/use-prompt-and-command.tsx b/chatbot-ui-main/components/chat/chat-hooks/use-prompt-and-command.tsx
new file mode 100644
index 0000000000..aaa19250e5
--- /dev/null
+++ b/chatbot-ui-main/components/chat/chat-hooks/use-prompt-and-command.tsx
@@ -0,0 +1,190 @@
+import { ChatbotUIContext } from "@/context/context"
+import { getAssistantCollectionsByAssistantId } from "@/db/assistant-collections"
+import { getAssistantFilesByAssistantId } from "@/db/assistant-files"
+import { getAssistantToolsByAssistantId } from "@/db/assistant-tools"
+import { getCollectionFilesByCollectionId } from "@/db/collection-files"
+import { Tables } from "@/supabase/types"
+import { LLMID } from "@/types"
+import { useContext } from "react"
+
+export const usePromptAndCommand = () => {
+ const {
+ chatFiles,
+ setNewMessageFiles,
+ userInput,
+ setUserInput,
+ setShowFilesDisplay,
+ setIsPromptPickerOpen,
+ setIsFilePickerOpen,
+ setSlashCommand,
+ setHashtagCommand,
+ setUseRetrieval,
+ setToolCommand,
+ setIsToolPickerOpen,
+ setSelectedTools,
+ setAtCommand,
+ setIsAssistantPickerOpen,
+ setSelectedAssistant,
+ setChatSettings,
+ setChatFiles
+ } = useContext(ChatbotUIContext)
+
+ const handleInputChange = (value: string) => {
+ const atTextRegex = /@([^ ]*)$/
+ const slashTextRegex = /\/([^ ]*)$/
+ const hashtagTextRegex = /#([^ ]*)$/
+ const toolTextRegex = /!([^ ]*)$/
+ const atMatch = value.match(atTextRegex)
+ const slashMatch = value.match(slashTextRegex)
+ const hashtagMatch = value.match(hashtagTextRegex)
+ const toolMatch = value.match(toolTextRegex)
+
+ if (atMatch) {
+ setIsAssistantPickerOpen(true)
+ setAtCommand(atMatch[1])
+ } else if (slashMatch) {
+ setIsPromptPickerOpen(true)
+ setSlashCommand(slashMatch[1])
+ } else if (hashtagMatch) {
+ setIsFilePickerOpen(true)
+ setHashtagCommand(hashtagMatch[1])
+ } else if (toolMatch) {
+ setIsToolPickerOpen(true)
+ setToolCommand(toolMatch[1])
+ } else {
+ setIsPromptPickerOpen(false)
+ setIsFilePickerOpen(false)
+ setIsToolPickerOpen(false)
+ setIsAssistantPickerOpen(false)
+ setSlashCommand("")
+ setHashtagCommand("")
+ setToolCommand("")
+ setAtCommand("")
+ }
+
+ setUserInput(value)
+ }
+
+ const handleSelectPrompt = (prompt: Tables<"prompts">) => {
+ setIsPromptPickerOpen(false)
+ setUserInput(userInput.replace(/\/[^ ]*$/, "") + prompt.content)
+ }
+
+ const handleSelectUserFile = async (file: Tables<"files">) => {
+ setShowFilesDisplay(true)
+ setIsFilePickerOpen(false)
+ setUseRetrieval(true)
+
+ setNewMessageFiles(prev => {
+ const fileAlreadySelected =
+ prev.some(prevFile => prevFile.id === file.id) ||
+ chatFiles.some(chatFile => chatFile.id === file.id)
+
+ if (!fileAlreadySelected) {
+ return [
+ ...prev,
+ {
+ id: file.id,
+ name: file.name,
+ type: file.type,
+ file: null
+ }
+ ]
+ }
+ return prev
+ })
+
+ setUserInput(userInput.replace(/#[^ ]*$/, ""))
+ }
+
+ const handleSelectUserCollection = async (
+ collection: Tables<"collections">
+ ) => {
+ setShowFilesDisplay(true)
+ setIsFilePickerOpen(false)
+ setUseRetrieval(true)
+
+ const collectionFiles = await getCollectionFilesByCollectionId(
+ collection.id
+ )
+
+ setNewMessageFiles(prev => {
+ const newFiles = collectionFiles.files
+ .filter(
+ file =>
+ !prev.some(prevFile => prevFile.id === file.id) &&
+ !chatFiles.some(chatFile => chatFile.id === file.id)
+ )
+ .map(file => ({
+ id: file.id,
+ name: file.name,
+ type: file.type,
+ file: null
+ }))
+
+ return [...prev, ...newFiles]
+ })
+
+ setUserInput(userInput.replace(/#[^ ]*$/, ""))
+ }
+
+ const handleSelectTool = (tool: Tables<"tools">) => {
+ setIsToolPickerOpen(false)
+ setUserInput(userInput.replace(/![^ ]*$/, ""))
+ setSelectedTools(prev => [...prev, tool])
+ }
+
+ const handleSelectAssistant = async (assistant: Tables<"assistants">) => {
+ setIsAssistantPickerOpen(false)
+ setUserInput(userInput.replace(/@[^ ]*$/, ""))
+ setSelectedAssistant(assistant)
+
+ setChatSettings({
+ model: assistant.model as LLMID,
+ prompt: assistant.prompt,
+ temperature: assistant.temperature,
+ contextLength: assistant.context_length,
+ includeProfileContext: assistant.include_profile_context,
+ includeWorkspaceInstructions: assistant.include_workspace_instructions,
+ embeddingsProvider: assistant.embeddings_provider as "openai" | "local"
+ })
+
+ let allFiles = []
+
+ const assistantFiles = (await getAssistantFilesByAssistantId(assistant.id))
+ .files
+ allFiles = [...assistantFiles]
+ const assistantCollections = (
+ await getAssistantCollectionsByAssistantId(assistant.id)
+ ).collections
+ for (const collection of assistantCollections) {
+ const collectionFiles = (
+ await getCollectionFilesByCollectionId(collection.id)
+ ).files
+ allFiles = [...allFiles, ...collectionFiles]
+ }
+ const assistantTools = (await getAssistantToolsByAssistantId(assistant.id))
+ .tools
+
+ setSelectedTools(assistantTools)
+ setChatFiles(
+ allFiles.map(file => ({
+ id: file.id,
+ name: file.name,
+ type: file.type,
+ file: null
+ }))
+ )
+
+ if (allFiles.length > 0) setShowFilesDisplay(true)
+ }
+
+ return {
+ handleInputChange,
+ handleSelectPrompt,
+ handleSelectUserFile,
+ handleSelectUserCollection,
+ handleSelectTool,
+ handleSelectAssistant
+ }
+}
diff --git a/chatbot-ui-main/components/chat/chat-hooks/use-scroll.tsx b/chatbot-ui-main/components/chat/chat-hooks/use-scroll.tsx
new file mode 100644
index 0000000000..9c6aea0d8d
--- /dev/null
+++ b/chatbot-ui-main/components/chat/chat-hooks/use-scroll.tsx
@@ -0,0 +1,87 @@
+import { ChatbotUIContext } from "@/context/context"
+import {
+ type UIEventHandler,
+ useCallback,
+ useContext,
+ useEffect,
+ useRef,
+ useState
+} from "react"
+
+export const useScroll = () => {
+ const { isGenerating, chatMessages } = useContext(ChatbotUIContext)
+
+ const messagesStartRef = useRef(null)
+ const messagesEndRef = useRef(null)
+ const isAutoScrolling = useRef(false)
+
+ const [isAtTop, setIsAtTop] = useState(false)
+ const [isAtBottom, setIsAtBottom] = useState(true)
+ const [userScrolled, setUserScrolled] = useState(false)
+ const [isOverflowing, setIsOverflowing] = useState(false)
+
+ useEffect(() => {
+ setUserScrolled(false)
+
+ if (!isGenerating && userScrolled) {
+ setUserScrolled(false)
+ }
+ }, [isGenerating])
+
+ useEffect(() => {
+ if (isGenerating && !userScrolled) {
+ scrollToBottom()
+ }
+ }, [chatMessages])
+
+ const handleScroll: UIEventHandler = useCallback(e => {
+ const target = e.target as HTMLDivElement
+ const bottom =
+ Math.round(target.scrollHeight) - Math.round(target.scrollTop) ===
+ Math.round(target.clientHeight)
+ setIsAtBottom(bottom)
+
+ const top = target.scrollTop === 0
+ setIsAtTop(top)
+
+ if (!bottom && !isAutoScrolling.current) {
+ setUserScrolled(true)
+ } else {
+ setUserScrolled(false)
+ }
+
+ const isOverflow = target.scrollHeight > target.clientHeight
+ setIsOverflowing(isOverflow)
+ }, [])
+
+ const scrollToTop = useCallback(() => {
+ if (messagesStartRef.current) {
+ messagesStartRef.current.scrollIntoView({ behavior: "instant" })
+ }
+ }, [])
+
+ const scrollToBottom = useCallback(() => {
+ isAutoScrolling.current = true
+
+ setTimeout(() => {
+ if (messagesEndRef.current) {
+ messagesEndRef.current.scrollIntoView({ behavior: "instant" })
+ }
+
+ isAutoScrolling.current = false
+ }, 100)
+ }, [])
+
+ return {
+ messagesStartRef,
+ messagesEndRef,
+ isAtTop,
+ isAtBottom,
+ userScrolled,
+ isOverflowing,
+ handleScroll,
+ scrollToTop,
+ scrollToBottom,
+ setIsAtBottom
+ }
+}
diff --git a/chatbot-ui-main/components/chat/chat-hooks/use-select-file-handler.tsx b/chatbot-ui-main/components/chat/chat-hooks/use-select-file-handler.tsx
new file mode 100644
index 0000000000..103ce6f0a8
--- /dev/null
+++ b/chatbot-ui-main/components/chat/chat-hooks/use-select-file-handler.tsx
@@ -0,0 +1,204 @@
+import { ChatbotUIContext } from "@/context/context"
+import { createDocXFile, createFile } from "@/db/files"
+import { LLM_LIST } from "@/lib/models/llm/llm-list"
+import mammoth from "mammoth"
+import { useContext, useEffect, useState } from "react"
+import { toast } from "sonner"
+
+export const ACCEPTED_FILE_TYPES = [
+ "text/csv",
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+ "application/json",
+ "text/markdown",
+ "application/pdf",
+ "text/plain"
+].join(",")
+
+export const useSelectFileHandler = () => {
+ const {
+ selectedWorkspace,
+ profile,
+ chatSettings,
+ setNewMessageImages,
+ setNewMessageFiles,
+ setShowFilesDisplay,
+ setFiles,
+ setUseRetrieval
+ } = useContext(ChatbotUIContext)
+
+ const [filesToAccept, setFilesToAccept] = useState(ACCEPTED_FILE_TYPES)
+
+ useEffect(() => {
+ handleFilesToAccept()
+ }, [chatSettings?.model])
+
+ const handleFilesToAccept = () => {
+ const model = chatSettings?.model
+ const FULL_MODEL = LLM_LIST.find(llm => llm.modelId === model)
+
+ if (!FULL_MODEL) return
+
+ setFilesToAccept(
+ FULL_MODEL.imageInput
+ ? `${ACCEPTED_FILE_TYPES},image/*`
+ : ACCEPTED_FILE_TYPES
+ )
+ }
+
+ const handleSelectDeviceFile = async (file: File) => {
+ if (!profile || !selectedWorkspace || !chatSettings) return
+
+ setShowFilesDisplay(true)
+ setUseRetrieval(true)
+
+ if (file) {
+ let simplifiedFileType = file.type.split("/")[1]
+
+ let reader = new FileReader()
+
+ if (file.type.includes("image")) {
+ reader.readAsDataURL(file)
+ } else if (ACCEPTED_FILE_TYPES.split(",").includes(file.type)) {
+ if (simplifiedFileType.includes("vnd.adobe.pdf")) {
+ simplifiedFileType = "pdf"
+ } else if (
+ simplifiedFileType.includes(
+ "vnd.openxmlformats-officedocument.wordprocessingml.document" ||
+ "docx"
+ )
+ ) {
+ simplifiedFileType = "docx"
+ }
+
+ setNewMessageFiles(prev => [
+ ...prev,
+ {
+ id: "loading",
+ name: file.name,
+ type: simplifiedFileType,
+ file: file
+ }
+ ])
+
+ // Handle docx files
+ if (
+ file.type.includes(
+ "vnd.openxmlformats-officedocument.wordprocessingml.document" ||
+ "docx"
+ )
+ ) {
+ const arrayBuffer = await file.arrayBuffer()
+ const result = await mammoth.extractRawText({
+ arrayBuffer
+ })
+
+ const createdFile = await createDocXFile(
+ result.value,
+ file,
+ {
+ user_id: profile.user_id,
+ description: "",
+ file_path: "",
+ name: file.name,
+ size: file.size,
+ tokens: 0,
+ type: simplifiedFileType
+ },
+ selectedWorkspace.id,
+ chatSettings.embeddingsProvider
+ )
+
+ setFiles(prev => [...prev, createdFile])
+
+ setNewMessageFiles(prev =>
+ prev.map(item =>
+ item.id === "loading"
+ ? {
+ id: createdFile.id,
+ name: createdFile.name,
+ type: createdFile.type,
+ file: file
+ }
+ : item
+ )
+ )
+
+ reader.onloadend = null
+
+ return
+ } else {
+ // Use readAsArrayBuffer for PDFs and readAsText for other types
+ file.type.includes("pdf")
+ ? reader.readAsArrayBuffer(file)
+ : reader.readAsText(file)
+ }
+ } else {
+ throw new Error("Unsupported file type")
+ }
+
+ reader.onloadend = async function () {
+ try {
+ if (file.type.includes("image")) {
+ // Create a temp url for the image file
+ const imageUrl = URL.createObjectURL(file)
+
+ // This is a temporary image for display purposes in the chat input
+ setNewMessageImages(prev => [
+ ...prev,
+ {
+ messageId: "temp",
+ path: "",
+ base64: reader.result, // base64 image
+ url: imageUrl,
+ file
+ }
+ ])
+ } else {
+ const createdFile = await createFile(
+ file,
+ {
+ user_id: profile.user_id,
+ description: "",
+ file_path: "",
+ name: file.name,
+ size: file.size,
+ tokens: 0,
+ type: simplifiedFileType
+ },
+ selectedWorkspace.id,
+ chatSettings.embeddingsProvider
+ )
+
+ setFiles(prev => [...prev, createdFile])
+
+ setNewMessageFiles(prev =>
+ prev.map(item =>
+ item.id === "loading"
+ ? {
+ id: createdFile.id,
+ name: createdFile.name,
+ type: createdFile.type,
+ file: file
+ }
+ : item
+ )
+ )
+ }
+ } catch (error: any) {
+ toast.error("Failed to upload. " + error?.message, {
+ duration: 10000
+ })
+ setNewMessageImages(prev =>
+ prev.filter(img => img.messageId !== "temp")
+ )
+ setNewMessageFiles(prev => prev.filter(file => file.id !== "loading"))
+ }
+ }
+ }
+ }
+
+ return {
+ handleSelectDeviceFile,
+ filesToAccept
+ }
+}
diff --git a/chatbot-ui-main/components/chat/chat-input.tsx b/chatbot-ui-main/components/chat/chat-input.tsx
new file mode 100644
index 0000000000..761c6cdcf0
--- /dev/null
+++ b/chatbot-ui-main/components/chat/chat-input.tsx
@@ -0,0 +1,281 @@
+import { ChatbotUIContext } from "@/context/context"
+import useHotkey from "@/lib/hooks/use-hotkey"
+import { LLM_LIST } from "@/lib/models/llm/llm-list"
+import { cn } from "@/lib/utils"
+import {
+ IconBolt,
+ IconCirclePlus,
+ IconPlayerStopFilled,
+ IconSend
+} from "@tabler/icons-react"
+import Image from "next/image"
+import { FC, useContext, useEffect, useRef, useState } from "react"
+import { useTranslation } from "react-i18next"
+import { toast } from "sonner"
+import { Input } from "../ui/input"
+import { TextareaAutosize } from "../ui/textarea-autosize"
+import { ChatCommandInput } from "./chat-command-input"
+import { ChatFilesDisplay } from "./chat-files-display"
+import { useChatHandler } from "./chat-hooks/use-chat-handler"
+import { useChatHistoryHandler } from "./chat-hooks/use-chat-history"
+import { usePromptAndCommand } from "./chat-hooks/use-prompt-and-command"
+import { useSelectFileHandler } from "./chat-hooks/use-select-file-handler"
+
+interface ChatInputProps {}
+
+export const ChatInput: FC = ({}) => {
+ const { t } = useTranslation()
+
+ useHotkey("l", () => {
+ handleFocusChatInput()
+ })
+
+ const [isTyping, setIsTyping] = useState(false)
+
+ const {
+ isAssistantPickerOpen,
+ focusAssistant,
+ setFocusAssistant,
+ userInput,
+ chatMessages,
+ isGenerating,
+ selectedPreset,
+ selectedAssistant,
+ focusPrompt,
+ setFocusPrompt,
+ focusFile,
+ focusTool,
+ setFocusTool,
+ isToolPickerOpen,
+ isPromptPickerOpen,
+ setIsPromptPickerOpen,
+ isFilePickerOpen,
+ setFocusFile,
+ chatSettings,
+ selectedTools,
+ setSelectedTools,
+ assistantImages
+ } = useContext(ChatbotUIContext)
+
+ const {
+ chatInputRef,
+ handleSendMessage,
+ handleStopMessage,
+ handleFocusChatInput
+ } = useChatHandler()
+
+ const { handleInputChange } = usePromptAndCommand()
+
+ const { filesToAccept, handleSelectDeviceFile } = useSelectFileHandler()
+
+ const {
+ setNewMessageContentToNextUserMessage,
+ setNewMessageContentToPreviousUserMessage
+ } = useChatHistoryHandler()
+
+ const fileInputRef = useRef(null)
+
+ useEffect(() => {
+ setTimeout(() => {
+ handleFocusChatInput()
+ }, 200) // FIX: hacky
+ }, [selectedPreset, selectedAssistant])
+
+ const handleKeyDown = (event: React.KeyboardEvent) => {
+ if (!isTyping && event.key === "Enter" && !event.shiftKey) {
+ event.preventDefault()
+ setIsPromptPickerOpen(false)
+ handleSendMessage(userInput, chatMessages, false)
+ }
+
+ // Consolidate conditions to avoid TypeScript error
+ if (
+ isPromptPickerOpen ||
+ isFilePickerOpen ||
+ isToolPickerOpen ||
+ isAssistantPickerOpen
+ ) {
+ if (
+ event.key === "Tab" ||
+ event.key === "ArrowUp" ||
+ event.key === "ArrowDown"
+ ) {
+ event.preventDefault()
+ // Toggle focus based on picker type
+ if (isPromptPickerOpen) setFocusPrompt(!focusPrompt)
+ if (isFilePickerOpen) setFocusFile(!focusFile)
+ if (isToolPickerOpen) setFocusTool(!focusTool)
+ if (isAssistantPickerOpen) setFocusAssistant(!focusAssistant)
+ }
+ }
+
+ if (event.key === "ArrowUp" && event.shiftKey && event.ctrlKey) {
+ event.preventDefault()
+ setNewMessageContentToPreviousUserMessage()
+ }
+
+ if (event.key === "ArrowDown" && event.shiftKey && event.ctrlKey) {
+ event.preventDefault()
+ setNewMessageContentToNextUserMessage()
+ }
+
+ //use shift+ctrl+up and shift+ctrl+down to navigate through chat history
+ if (event.key === "ArrowUp" && event.shiftKey && event.ctrlKey) {
+ event.preventDefault()
+ setNewMessageContentToPreviousUserMessage()
+ }
+
+ if (event.key === "ArrowDown" && event.shiftKey && event.ctrlKey) {
+ event.preventDefault()
+ setNewMessageContentToNextUserMessage()
+ }
+
+ if (
+ isAssistantPickerOpen &&
+ (event.key === "Tab" ||
+ event.key === "ArrowUp" ||
+ event.key === "ArrowDown")
+ ) {
+ event.preventDefault()
+ setFocusAssistant(!focusAssistant)
+ }
+ }
+
+ const handlePaste = (event: React.ClipboardEvent) => {
+ const imagesAllowed = LLM_LIST.find(
+ llm => llm.modelId === chatSettings?.model
+ )?.imageInput
+
+ const items = event.clipboardData.items
+ for (const item of items) {
+ if (item.type.indexOf("image") === 0) {
+ if (!imagesAllowed) {
+ toast.error(
+ `Images are not supported for this model. Use models like GPT-4 Vision instead.`
+ )
+ return
+ }
+ const file = item.getAsFile()
+ if (!file) return
+ handleSelectDeviceFile(file)
+ }
+ }
+ }
+
+ return (
+ <>
+
+
+
+ {selectedTools &&
+ selectedTools.map((tool, index) => (
+
+ setSelectedTools(
+ selectedTools.filter(
+ selectedTool => selectedTool.id !== tool.id
+ )
+ )
+ }
+ >
+
+
+ ))}
+
+ {selectedAssistant && (
+
+ {selectedAssistant.image_path && (
+
img.path === selectedAssistant.image_path
+ )?.base64
+ }
+ width={28}
+ height={28}
+ alt={selectedAssistant.name}
+ />
+ )}
+
+
+ Talking to {selectedAssistant.name}
+
+
+ )}
+
+
+
+
+
+
+
+ <>
+
fileInputRef.current?.click()}
+ />
+
+ {/* Hidden input to select files from device */}
+ {
+ if (!e.target.files) return
+ handleSelectDeviceFile(e.target.files[0])
+ }}
+ accept={filesToAccept}
+ />
+ >
+
+ setIsTyping(true)}
+ onCompositionEnd={() => setIsTyping(false)}
+ />
+
+
+ {isGenerating ? (
+
+ ) : (
+ {
+ if (!userInput) return
+
+ handleSendMessage(userInput, chatMessages, false)
+ }}
+ size={30}
+ />
+ )}
+
+
+ >
+ )
+}
diff --git a/chatbot-ui-main/components/chat/chat-messages.tsx b/chatbot-ui-main/components/chat/chat-messages.tsx
new file mode 100644
index 0000000000..af13a88cac
--- /dev/null
+++ b/chatbot-ui-main/components/chat/chat-messages.tsx
@@ -0,0 +1,38 @@
+import { useChatHandler } from "@/components/chat/chat-hooks/use-chat-handler"
+import { ChatbotUIContext } from "@/context/context"
+import { Tables } from "@/supabase/types"
+import { FC, useContext, useState } from "react"
+import { Message } from "../messages/message"
+
+interface ChatMessagesProps {}
+
+export const ChatMessages: FC = ({}) => {
+ const { chatMessages, chatFileItems } = useContext(ChatbotUIContext)
+
+ const { handleSendEdit } = useChatHandler()
+
+ const [editingMessage, setEditingMessage] = useState>()
+
+ return chatMessages
+ .sort((a, b) => a.message.sequence_number - b.message.sequence_number)
+ .map((chatMessage, index, array) => {
+ const messageFileItems = chatFileItems.filter(
+ (chatFileItem, _, self) =>
+ chatMessage.fileItems.includes(chatFileItem.id) &&
+ self.findIndex(item => item.id === chatFileItem.id) === _
+ )
+
+ return (
+ setEditingMessage(undefined)}
+ onSubmitEdit={handleSendEdit}
+ />
+ )
+ })
+}
diff --git a/chatbot-ui-main/components/chat/chat-retrieval-settings.tsx b/chatbot-ui-main/components/chat/chat-retrieval-settings.tsx
new file mode 100644
index 0000000000..2a47521721
--- /dev/null
+++ b/chatbot-ui-main/components/chat/chat-retrieval-settings.tsx
@@ -0,0 +1,65 @@
+import { ChatbotUIContext } from "@/context/context"
+import { IconAdjustmentsHorizontal } from "@tabler/icons-react"
+import { FC, useContext, useState } from "react"
+import { Button } from "../ui/button"
+import {
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogTrigger
+} from "../ui/dialog"
+import { Label } from "../ui/label"
+import { Slider } from "../ui/slider"
+import { WithTooltip } from "../ui/with-tooltip"
+
+interface ChatRetrievalSettingsProps {}
+
+export const ChatRetrievalSettings: FC = ({}) => {
+ const { sourceCount, setSourceCount } = useContext(ChatbotUIContext)
+
+ const [isOpen, setIsOpen] = useState(false)
+
+ return (
+
+
+ Adjust retrieval settings.}
+ trigger={
+
+ }
+ />
+
+
+
+
+
+ Source Count:
+
+ {sourceCount}
+
+
+
{
+ setSourceCount(values[0])
+ }}
+ min={1}
+ max={10}
+ step={1}
+ />
+
+
+
+ setIsOpen(false)}>
+ Save & Close
+
+
+
+
+ )
+}
diff --git a/chatbot-ui-main/components/chat/chat-scroll-buttons.tsx b/chatbot-ui-main/components/chat/chat-scroll-buttons.tsx
new file mode 100644
index 0000000000..3eb6f2d643
--- /dev/null
+++ b/chatbot-ui-main/components/chat/chat-scroll-buttons.tsx
@@ -0,0 +1,41 @@
+import {
+ IconCircleArrowDownFilled,
+ IconCircleArrowUpFilled
+} from "@tabler/icons-react"
+import { FC } from "react"
+
+interface ChatScrollButtonsProps {
+ isAtTop: boolean
+ isAtBottom: boolean
+ isOverflowing: boolean
+ scrollToTop: () => void
+ scrollToBottom: () => void
+}
+
+export const ChatScrollButtons: FC = ({
+ isAtTop,
+ isAtBottom,
+ isOverflowing,
+ scrollToTop,
+ scrollToBottom
+}) => {
+ return (
+ <>
+ {!isAtTop && isOverflowing && (
+
+ )}
+
+ {!isAtBottom && isOverflowing && (
+
+ )}
+ >
+ )
+}
diff --git a/chatbot-ui-main/components/chat/chat-secondary-buttons.tsx b/chatbot-ui-main/components/chat/chat-secondary-buttons.tsx
new file mode 100644
index 0000000000..780f7654e4
--- /dev/null
+++ b/chatbot-ui-main/components/chat/chat-secondary-buttons.tsx
@@ -0,0 +1,78 @@
+import { useChatHandler } from "@/components/chat/chat-hooks/use-chat-handler"
+import { ChatbotUIContext } from "@/context/context"
+import { IconInfoCircle, IconMessagePlus } from "@tabler/icons-react"
+import { FC, useContext } from "react"
+import { WithTooltip } from "../ui/with-tooltip"
+
+interface ChatSecondaryButtonsProps {}
+
+export const ChatSecondaryButtons: FC = ({}) => {
+ const { selectedChat } = useContext(ChatbotUIContext)
+
+ const { handleNewChat } = useChatHandler()
+
+ return (
+ <>
+ {selectedChat && (
+ <>
+
+ Chat Info
+
+
+
Model: {selectedChat.model}
+
Prompt: {selectedChat.prompt}
+
+
Temperature: {selectedChat.temperature}
+
Context Length: {selectedChat.context_length}
+
+
+ Profile Context:{" "}
+ {selectedChat.include_profile_context
+ ? "Enabled"
+ : "Disabled"}
+
+
+ {" "}
+ Workspace Instructions:{" "}
+ {selectedChat.include_workspace_instructions
+ ? "Enabled"
+ : "Disabled"}
+
+
+
+ Embeddings Provider: {selectedChat.embeddings_provider}
+
+
+
+ }
+ trigger={
+
+
+
+ }
+ />
+
+ Start a new chat}
+ trigger={
+
+
+
+ }
+ />
+ >
+ )}
+ >
+ )
+}
diff --git a/chatbot-ui-main/components/chat/chat-settings.tsx b/chatbot-ui-main/components/chat/chat-settings.tsx
new file mode 100644
index 0000000000..8230d5f4d4
--- /dev/null
+++ b/chatbot-ui-main/components/chat/chat-settings.tsx
@@ -0,0 +1,94 @@
+import { ChatbotUIContext } from "@/context/context"
+import { CHAT_SETTING_LIMITS } from "@/lib/chat-setting-limits"
+import useHotkey from "@/lib/hooks/use-hotkey"
+import { LLMID, ModelProvider } from "@/types"
+import { IconAdjustmentsHorizontal } from "@tabler/icons-react"
+import { FC, useContext, useEffect, useRef } from "react"
+import { Button } from "../ui/button"
+import { ChatSettingsForm } from "../ui/chat-settings-form"
+import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"
+
+interface ChatSettingsProps {}
+
+export const ChatSettings: FC = ({}) => {
+ useHotkey("i", () => handleClick())
+
+ const {
+ chatSettings,
+ setChatSettings,
+ models,
+ availableHostedModels,
+ availableLocalModels,
+ availableOpenRouterModels
+ } = useContext(ChatbotUIContext)
+
+ const buttonRef = useRef(null)
+
+ const handleClick = () => {
+ if (buttonRef.current) {
+ buttonRef.current.click()
+ }
+ }
+
+ useEffect(() => {
+ if (!chatSettings) return
+
+ setChatSettings({
+ ...chatSettings,
+ temperature: Math.min(
+ chatSettings.temperature,
+ CHAT_SETTING_LIMITS[chatSettings.model]?.MAX_TEMPERATURE || 1
+ ),
+ contextLength: Math.min(
+ chatSettings.contextLength,
+ CHAT_SETTING_LIMITS[chatSettings.model]?.MAX_CONTEXT_LENGTH || 4096
+ )
+ })
+ }, [chatSettings?.model])
+
+ if (!chatSettings) return null
+
+ const allModels = [
+ ...models.map(model => ({
+ modelId: model.model_id as LLMID,
+ modelName: model.name,
+ provider: "custom" as ModelProvider,
+ hostedId: model.id,
+ platformLink: "",
+ imageInput: false
+ })),
+ ...availableHostedModels,
+ ...availableLocalModels,
+ ...availableOpenRouterModels
+ ]
+
+ const fullModel = allModels.find(llm => llm.modelId === chatSettings.model)
+
+ return (
+
+
+
+
+ {fullModel?.modelName || chatSettings.model}
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/chatbot-ui-main/components/chat/chat-ui.tsx b/chatbot-ui-main/components/chat/chat-ui.tsx
new file mode 100644
index 0000000000..ac9f13d3c2
--- /dev/null
+++ b/chatbot-ui-main/components/chat/chat-ui.tsx
@@ -0,0 +1,230 @@
+import Loading from "@/app/[locale]/loading"
+import { useChatHandler } from "@/components/chat/chat-hooks/use-chat-handler"
+import { ChatbotUIContext } from "@/context/context"
+import { getAssistantToolsByAssistantId } from "@/db/assistant-tools"
+import { getChatFilesByChatId } from "@/db/chat-files"
+import { getChatById } from "@/db/chats"
+import { getMessageFileItemsByMessageId } from "@/db/message-file-items"
+import { getMessagesByChatId } from "@/db/messages"
+import { getMessageImageFromStorage } from "@/db/storage/message-images"
+import { convertBlobToBase64 } from "@/lib/blob-to-b64"
+import useHotkey from "@/lib/hooks/use-hotkey"
+import { LLMID, MessageImage } from "@/types"
+import { useParams } from "next/navigation"
+import { FC, useContext, useEffect, useState } from "react"
+import { ChatHelp } from "./chat-help"
+import { useScroll } from "./chat-hooks/use-scroll"
+import { ChatInput } from "./chat-input"
+import { ChatMessages } from "./chat-messages"
+import { ChatScrollButtons } from "./chat-scroll-buttons"
+import { ChatSecondaryButtons } from "./chat-secondary-buttons"
+
+interface ChatUIProps {}
+
+export const ChatUI: FC = ({}) => {
+ useHotkey("o", () => handleNewChat())
+
+ const params = useParams()
+
+ const {
+ setChatMessages,
+ selectedChat,
+ setSelectedChat,
+ setChatSettings,
+ setChatImages,
+ assistants,
+ setSelectedAssistant,
+ setChatFileItems,
+ setChatFiles,
+ setShowFilesDisplay,
+ setUseRetrieval,
+ setSelectedTools
+ } = useContext(ChatbotUIContext)
+
+ const { handleNewChat, handleFocusChatInput } = useChatHandler()
+
+ const {
+ messagesStartRef,
+ messagesEndRef,
+ handleScroll,
+ scrollToBottom,
+ setIsAtBottom,
+ isAtTop,
+ isAtBottom,
+ isOverflowing,
+ scrollToTop
+ } = useScroll()
+
+ const [loading, setLoading] = useState(true)
+
+ useEffect(() => {
+ const fetchData = async () => {
+ await fetchMessages()
+ await fetchChat()
+
+ scrollToBottom()
+ setIsAtBottom(true)
+ }
+
+ if (params.chatid) {
+ fetchData().then(() => {
+ handleFocusChatInput()
+ setLoading(false)
+ })
+ } else {
+ setLoading(false)
+ }
+ }, [])
+
+ const fetchMessages = async () => {
+ const fetchedMessages = await getMessagesByChatId(params.chatid as string)
+
+ const imagePromises: Promise[] = fetchedMessages.flatMap(
+ message =>
+ message.image_paths
+ ? message.image_paths.map(async imagePath => {
+ const url = await getMessageImageFromStorage(imagePath)
+
+ if (url) {
+ const response = await fetch(url)
+ const blob = await response.blob()
+ const base64 = await convertBlobToBase64(blob)
+
+ return {
+ messageId: message.id,
+ path: imagePath,
+ base64,
+ url,
+ file: null
+ }
+ }
+
+ return {
+ messageId: message.id,
+ path: imagePath,
+ base64: "",
+ url,
+ file: null
+ }
+ })
+ : []
+ )
+
+ const images: MessageImage[] = await Promise.all(imagePromises.flat())
+ setChatImages(images)
+
+ const messageFileItemPromises = fetchedMessages.map(
+ async message => await getMessageFileItemsByMessageId(message.id)
+ )
+
+ const messageFileItems = await Promise.all(messageFileItemPromises)
+
+ const uniqueFileItems = messageFileItems.flatMap(item => item.file_items)
+ setChatFileItems(uniqueFileItems)
+
+ const chatFiles = await getChatFilesByChatId(params.chatid as string)
+
+ setChatFiles(
+ chatFiles.files.map(file => ({
+ id: file.id,
+ name: file.name,
+ type: file.type,
+ file: null
+ }))
+ )
+
+ setUseRetrieval(true)
+ setShowFilesDisplay(true)
+
+ const fetchedChatMessages = fetchedMessages.map(message => {
+ return {
+ message,
+ fileItems: messageFileItems
+ .filter(messageFileItem => messageFileItem.id === message.id)
+ .flatMap(messageFileItem =>
+ messageFileItem.file_items.map(fileItem => fileItem.id)
+ )
+ }
+ })
+
+ setChatMessages(fetchedChatMessages)
+ }
+
+ const fetchChat = async () => {
+ const chat = await getChatById(params.chatid as string)
+ if (!chat) return
+
+ if (chat.assistant_id) {
+ const assistant = assistants.find(
+ assistant => assistant.id === chat.assistant_id
+ )
+
+ if (assistant) {
+ setSelectedAssistant(assistant)
+
+ const assistantTools = (
+ await getAssistantToolsByAssistantId(assistant.id)
+ ).tools
+ setSelectedTools(assistantTools)
+ }
+ }
+
+ setSelectedChat(chat)
+ setChatSettings({
+ model: chat.model as LLMID,
+ prompt: chat.prompt,
+ temperature: chat.temperature,
+ contextLength: chat.context_length,
+ includeProfileContext: chat.include_profile_context,
+ includeWorkspaceInstructions: chat.include_workspace_instructions,
+ embeddingsProvider: chat.embeddings_provider as "openai" | "local"
+ })
+ }
+
+ if (loading) {
+ return
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {selectedChat?.name || "Chat"}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/chatbot-ui-main/components/chat/file-picker.tsx b/chatbot-ui-main/components/chat/file-picker.tsx
new file mode 100644
index 0000000000..00c4a7372d
--- /dev/null
+++ b/chatbot-ui-main/components/chat/file-picker.tsx
@@ -0,0 +1,158 @@
+import { ChatbotUIContext } from "@/context/context"
+import { Tables } from "@/supabase/types"
+import { IconBooks } from "@tabler/icons-react"
+import { FC, useContext, useEffect, useRef } from "react"
+import { FileIcon } from "../ui/file-icon"
+
+interface FilePickerProps {
+ isOpen: boolean
+ searchQuery: string
+ onOpenChange: (isOpen: boolean) => void
+ selectedFileIds: string[]
+ selectedCollectionIds: string[]
+ onSelectFile: (file: Tables<"files">) => void
+ onSelectCollection: (collection: Tables<"collections">) => void
+ isFocused: boolean
+}
+
+export const FilePicker: FC = ({
+ isOpen,
+ searchQuery,
+ onOpenChange,
+ selectedFileIds,
+ selectedCollectionIds,
+ onSelectFile,
+ onSelectCollection,
+ isFocused
+}) => {
+ const { files, collections, setIsFilePickerOpen } =
+ useContext(ChatbotUIContext)
+
+ const itemsRef = useRef<(HTMLDivElement | null)[]>([])
+
+ useEffect(() => {
+ if (isFocused && itemsRef.current[0]) {
+ itemsRef.current[0].focus()
+ }
+ }, [isFocused])
+
+ const filteredFiles = files.filter(
+ file =>
+ file.name.toLowerCase().includes(searchQuery.toLowerCase()) &&
+ !selectedFileIds.includes(file.id)
+ )
+
+ const filteredCollections = collections.filter(
+ collection =>
+ collection.name.toLowerCase().includes(searchQuery.toLowerCase()) &&
+ !selectedCollectionIds.includes(collection.id)
+ )
+
+ const handleOpenChange = (isOpen: boolean) => {
+ onOpenChange(isOpen)
+ }
+
+ const handleSelectFile = (file: Tables<"files">) => {
+ onSelectFile(file)
+ handleOpenChange(false)
+ }
+
+ const handleSelectCollection = (collection: Tables<"collections">) => {
+ onSelectCollection(collection)
+ handleOpenChange(false)
+ }
+
+ const getKeyDownHandler =
+ (index: number, type: "file" | "collection", item: any) =>
+ (e: React.KeyboardEvent) => {
+ if (e.key === "Escape") {
+ e.preventDefault()
+ setIsFilePickerOpen(false)
+ } else if (e.key === "Backspace") {
+ e.preventDefault()
+ } else if (e.key === "Enter") {
+ e.preventDefault()
+
+ if (type === "file") {
+ handleSelectFile(item)
+ } else {
+ handleSelectCollection(item)
+ }
+ } else if (
+ (e.key === "Tab" || e.key === "ArrowDown") &&
+ !e.shiftKey &&
+ index === filteredFiles.length + filteredCollections.length - 1
+ ) {
+ e.preventDefault()
+ itemsRef.current[0]?.focus()
+ } else if (e.key === "ArrowUp" && !e.shiftKey && index === 0) {
+ // go to last element if arrow up is pressed on first element
+ e.preventDefault()
+ itemsRef.current[itemsRef.current.length - 1]?.focus()
+ } else if (e.key === "ArrowUp") {
+ e.preventDefault()
+ const prevIndex =
+ index - 1 >= 0 ? index - 1 : itemsRef.current.length - 1
+ itemsRef.current[prevIndex]?.focus()
+ } else if (e.key === "ArrowDown") {
+ e.preventDefault()
+ const nextIndex = index + 1 < itemsRef.current.length ? index + 1 : 0
+ itemsRef.current[nextIndex]?.focus()
+ }
+ }
+
+ return (
+ <>
+ {isOpen && (
+
+ {filteredFiles.length === 0 && filteredCollections.length === 0 ? (
+
+ No matching files.
+
+ ) : (
+ <>
+ {[...filteredFiles, ...filteredCollections].map((item, index) => (
+
{
+ itemsRef.current[index] = ref
+ }}
+ tabIndex={0}
+ className="hover:bg-accent focus:bg-accent flex cursor-pointer items-center rounded p-2 focus:outline-none"
+ onClick={() => {
+ if ("type" in item) {
+ handleSelectFile(item as Tables<"files">)
+ } else {
+ handleSelectCollection(item)
+ }
+ }}
+ onKeyDown={e =>
+ getKeyDownHandler(
+ index,
+ "type" in item ? "file" : "collection",
+ item
+ )(e)
+ }
+ >
+ {"type" in item ? (
+
).type} size={32} />
+ ) : (
+
+ )}
+
+
+
{item.name}
+
+
+ {item.description || "No description."}
+
+
+
+ ))}
+ >
+ )}
+
+ )}
+ >
+ )
+}
diff --git a/chatbot-ui-main/components/chat/prompt-picker.tsx b/chatbot-ui-main/components/chat/prompt-picker.tsx
new file mode 100644
index 0000000000..55592e37a9
--- /dev/null
+++ b/chatbot-ui-main/components/chat/prompt-picker.tsx
@@ -0,0 +1,215 @@
+import { ChatbotUIContext } from "@/context/context"
+import { Tables } from "@/supabase/types"
+import { FC, useContext, useEffect, useRef, useState } from "react"
+import { Button } from "../ui/button"
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../ui/dialog"
+import { Label } from "../ui/label"
+import { TextareaAutosize } from "../ui/textarea-autosize"
+import { usePromptAndCommand } from "./chat-hooks/use-prompt-and-command"
+
+interface PromptPickerProps {}
+
+export const PromptPicker: FC = ({}) => {
+ const {
+ prompts,
+ isPromptPickerOpen,
+ setIsPromptPickerOpen,
+ focusPrompt,
+ slashCommand
+ } = useContext(ChatbotUIContext)
+
+ const { handleSelectPrompt } = usePromptAndCommand()
+
+ const itemsRef = useRef<(HTMLDivElement | null)[]>([])
+
+ const [promptVariables, setPromptVariables] = useState<
+ {
+ promptId: string
+ name: string
+ value: string
+ }[]
+ >([])
+ const [showPromptVariables, setShowPromptVariables] = useState(false)
+
+ useEffect(() => {
+ if (focusPrompt && itemsRef.current[0]) {
+ itemsRef.current[0].focus()
+ }
+ }, [focusPrompt])
+
+ const [isTyping, setIsTyping] = useState(false)
+
+ const filteredPrompts = prompts.filter(prompt =>
+ prompt.name.toLowerCase().includes(slashCommand.toLowerCase())
+ )
+
+ const handleOpenChange = (isOpen: boolean) => {
+ setIsPromptPickerOpen(isOpen)
+ }
+
+ const callSelectPrompt = (prompt: Tables<"prompts">) => {
+ const regex = /\{\{.*?\}\}/g
+ const matches = prompt.content.match(regex)
+
+ if (matches) {
+ const newPromptVariables = matches.map(match => ({
+ promptId: prompt.id,
+ name: match.replace(/\{\{|\}\}/g, ""),
+ value: ""
+ }))
+
+ setPromptVariables(newPromptVariables)
+ setShowPromptVariables(true)
+ } else {
+ handleSelectPrompt(prompt)
+ handleOpenChange(false)
+ }
+ }
+
+ const getKeyDownHandler =
+ (index: number) => (e: React.KeyboardEvent) => {
+ if (e.key === "Backspace") {
+ e.preventDefault()
+ handleOpenChange(false)
+ } else if (e.key === "Enter") {
+ e.preventDefault()
+ callSelectPrompt(filteredPrompts[index])
+ } else if (
+ (e.key === "Tab" || e.key === "ArrowDown") &&
+ !e.shiftKey &&
+ index === filteredPrompts.length - 1
+ ) {
+ e.preventDefault()
+ itemsRef.current[0]?.focus()
+ } else if (e.key === "ArrowUp" && !e.shiftKey && index === 0) {
+ // go to last element if arrow up is pressed on first element
+ e.preventDefault()
+ itemsRef.current[itemsRef.current.length - 1]?.focus()
+ } else if (e.key === "ArrowUp") {
+ e.preventDefault()
+ const prevIndex =
+ index - 1 >= 0 ? index - 1 : itemsRef.current.length - 1
+ itemsRef.current[prevIndex]?.focus()
+ } else if (e.key === "ArrowDown") {
+ e.preventDefault()
+ const nextIndex = index + 1 < itemsRef.current.length ? index + 1 : 0
+ itemsRef.current[nextIndex]?.focus()
+ }
+ }
+
+ const handleSubmitPromptVariables = () => {
+ const newPromptContent = promptVariables.reduce(
+ (prevContent, variable) =>
+ prevContent.replace(
+ new RegExp(`\\{\\{${variable.name}\\}\\}`, "g"),
+ variable.value
+ ),
+ prompts.find(prompt => prompt.id === promptVariables[0].promptId)
+ ?.content || ""
+ )
+
+ const newPrompt: any = {
+ ...prompts.find(prompt => prompt.id === promptVariables[0].promptId),
+ content: newPromptContent
+ }
+
+ handleSelectPrompt(newPrompt)
+ handleOpenChange(false)
+ setShowPromptVariables(false)
+ setPromptVariables([])
+ }
+
+ const handleCancelPromptVariables = () => {
+ setShowPromptVariables(false)
+ setPromptVariables([])
+ }
+
+ const handleKeydownPromptVariables = (
+ e: React.KeyboardEvent
+ ) => {
+ if (!isTyping && e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault()
+ handleSubmitPromptVariables()
+ }
+ }
+
+ return (
+ <>
+ {isPromptPickerOpen && (
+
+ {showPromptVariables ? (
+
+
+
+ Enter Prompt Variables
+
+
+
+ {promptVariables.map((variable, index) => (
+
+ {variable.name}
+
+ {
+ const newPromptVariables = [...promptVariables]
+ newPromptVariables[index].value = value
+ setPromptVariables(newPromptVariables)
+ }}
+ minRows={3}
+ maxRows={5}
+ onCompositionStart={() => setIsTyping(true)}
+ onCompositionEnd={() => setIsTyping(false)}
+ />
+
+ ))}
+
+
+
+
+ Cancel
+
+
+
+ Submit
+
+
+
+
+ ) : filteredPrompts.length === 0 ? (
+
+ No matching prompts.
+
+ ) : (
+ filteredPrompts.map((prompt, index) => (
+
{
+ itemsRef.current[index] = ref
+ }}
+ tabIndex={0}
+ className="hover:bg-accent focus:bg-accent flex cursor-pointer flex-col rounded p-2 focus:outline-none"
+ onClick={() => callSelectPrompt(prompt)}
+ onKeyDown={getKeyDownHandler(index)}
+ >
+
{prompt.name}
+
+
+ {prompt.content}
+
+
+ ))
+ )}
+
+ )}
+ >
+ )
+}
diff --git a/chatbot-ui-main/components/chat/quick-setting-option.tsx b/chatbot-ui-main/components/chat/quick-setting-option.tsx
new file mode 100644
index 0000000000..6ddd48b3bd
--- /dev/null
+++ b/chatbot-ui-main/components/chat/quick-setting-option.tsx
@@ -0,0 +1,71 @@
+import { LLM_LIST } from "@/lib/models/llm/llm-list"
+import { Tables } from "@/supabase/types"
+import { IconCircleCheckFilled, IconRobotFace } from "@tabler/icons-react"
+import Image from "next/image"
+import { FC } from "react"
+import { ModelIcon } from "../models/model-icon"
+import { DropdownMenuItem } from "../ui/dropdown-menu"
+
+interface QuickSettingOptionProps {
+ contentType: "presets" | "assistants"
+ isSelected: boolean
+ item: Tables<"presets"> | Tables<"assistants">
+ onSelect: () => void
+ image: string
+}
+
+export const QuickSettingOption: FC = ({
+ contentType,
+ isSelected,
+ item,
+ onSelect,
+ image
+}) => {
+ const modelDetails = LLM_LIST.find(model => model.modelId === item.model)
+
+ return (
+
+
+ {contentType === "presets" ? (
+
+ ) : image ? (
+
+ ) : (
+
+ )}
+
+
+
+
{item.name}
+
+ {item.description && (
+
{item.description}
+ )}
+
+
+
+ {isSelected ? (
+
+ ) : null}
+
+
+ )
+}
diff --git a/chatbot-ui-main/components/chat/quick-settings.tsx b/chatbot-ui-main/components/chat/quick-settings.tsx
new file mode 100644
index 0000000000..6eb5cabef4
--- /dev/null
+++ b/chatbot-ui-main/components/chat/quick-settings.tsx
@@ -0,0 +1,308 @@
+import { ChatbotUIContext } from "@/context/context"
+import { getAssistantCollectionsByAssistantId } from "@/db/assistant-collections"
+import { getAssistantFilesByAssistantId } from "@/db/assistant-files"
+import { getAssistantToolsByAssistantId } from "@/db/assistant-tools"
+import { getCollectionFilesByCollectionId } from "@/db/collection-files"
+import useHotkey from "@/lib/hooks/use-hotkey"
+import { LLM_LIST } from "@/lib/models/llm/llm-list"
+import { Tables } from "@/supabase/types"
+import { LLMID } from "@/types"
+import { IconChevronDown, IconRobotFace } from "@tabler/icons-react"
+import Image from "next/image"
+import { FC, useContext, useEffect, useRef, useState } from "react"
+import { useTranslation } from "react-i18next"
+import { ModelIcon } from "../models/model-icon"
+import { Button } from "../ui/button"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuTrigger
+} from "../ui/dropdown-menu"
+import { Input } from "../ui/input"
+import { QuickSettingOption } from "./quick-setting-option"
+import { set } from "date-fns"
+
+interface QuickSettingsProps {}
+
+export const QuickSettings: FC = ({}) => {
+ const { t } = useTranslation()
+
+ useHotkey("p", () => setIsOpen(prevState => !prevState))
+
+ const {
+ presets,
+ assistants,
+ selectedAssistant,
+ selectedPreset,
+ chatSettings,
+ setSelectedPreset,
+ setSelectedAssistant,
+ setChatSettings,
+ assistantImages,
+ setChatFiles,
+ setSelectedTools,
+ setShowFilesDisplay,
+ selectedWorkspace
+ } = useContext(ChatbotUIContext)
+
+ const inputRef = useRef(null)
+
+ const [isOpen, setIsOpen] = useState(false)
+ const [search, setSearch] = useState("")
+ const [loading, setLoading] = useState(false)
+
+ useEffect(() => {
+ if (isOpen) {
+ setTimeout(() => {
+ inputRef.current?.focus()
+ }, 100) // FIX: hacky
+ }
+ }, [isOpen])
+
+ const handleSelectQuickSetting = async (
+ item: Tables<"presets"> | Tables<"assistants"> | null,
+ contentType: "presets" | "assistants" | "remove"
+ ) => {
+ console.log({ item, contentType })
+ if (contentType === "assistants" && item) {
+ setSelectedAssistant(item as Tables<"assistants">)
+ setLoading(true)
+ let allFiles = []
+ const assistantFiles = (await getAssistantFilesByAssistantId(item.id))
+ .files
+ allFiles = [...assistantFiles]
+ const assistantCollections = (
+ await getAssistantCollectionsByAssistantId(item.id)
+ ).collections
+ for (const collection of assistantCollections) {
+ const collectionFiles = (
+ await getCollectionFilesByCollectionId(collection.id)
+ ).files
+ allFiles = [...allFiles, ...collectionFiles]
+ }
+ const assistantTools = (await getAssistantToolsByAssistantId(item.id))
+ .tools
+ setSelectedTools(assistantTools)
+ setChatFiles(
+ allFiles.map(file => ({
+ id: file.id,
+ name: file.name,
+ type: file.type,
+ file: null
+ }))
+ )
+ if (allFiles.length > 0) setShowFilesDisplay(true)
+ setLoading(false)
+ setSelectedPreset(null)
+ } else if (contentType === "presets" && item) {
+ setSelectedPreset(item as Tables<"presets">)
+ setSelectedAssistant(null)
+ setChatFiles([])
+ setSelectedTools([])
+ } else {
+ setSelectedPreset(null)
+ setSelectedAssistant(null)
+ setChatFiles([])
+ setSelectedTools([])
+ if (selectedWorkspace) {
+ setChatSettings({
+ model: selectedWorkspace.default_model as LLMID,
+ prompt: selectedWorkspace.default_prompt,
+ temperature: selectedWorkspace.default_temperature,
+ contextLength: selectedWorkspace.default_context_length,
+ includeProfileContext: selectedWorkspace.include_profile_context,
+ includeWorkspaceInstructions:
+ selectedWorkspace.include_workspace_instructions,
+ embeddingsProvider: selectedWorkspace.embeddings_provider as
+ | "openai"
+ | "local"
+ })
+ }
+ return
+ }
+
+ setChatSettings({
+ model: item.model as LLMID,
+ prompt: item.prompt,
+ temperature: item.temperature,
+ contextLength: item.context_length,
+ includeProfileContext: item.include_profile_context,
+ includeWorkspaceInstructions: item.include_workspace_instructions,
+ embeddingsProvider: item.embeddings_provider as "openai" | "local"
+ })
+ }
+
+ const checkIfModified = () => {
+ if (!chatSettings) return false
+
+ if (selectedPreset) {
+ return (
+ selectedPreset.include_profile_context !==
+ chatSettings?.includeProfileContext ||
+ selectedPreset.include_workspace_instructions !==
+ chatSettings.includeWorkspaceInstructions ||
+ selectedPreset.context_length !== chatSettings.contextLength ||
+ selectedPreset.model !== chatSettings.model ||
+ selectedPreset.prompt !== chatSettings.prompt ||
+ selectedPreset.temperature !== chatSettings.temperature
+ )
+ } else if (selectedAssistant) {
+ return (
+ selectedAssistant.include_profile_context !==
+ chatSettings.includeProfileContext ||
+ selectedAssistant.include_workspace_instructions !==
+ chatSettings.includeWorkspaceInstructions ||
+ selectedAssistant.context_length !== chatSettings.contextLength ||
+ selectedAssistant.model !== chatSettings.model ||
+ selectedAssistant.prompt !== chatSettings.prompt ||
+ selectedAssistant.temperature !== chatSettings.temperature
+ )
+ }
+
+ return false
+ }
+
+ const isModified = checkIfModified()
+
+ const items = [
+ ...presets.map(preset => ({ ...preset, contentType: "presets" })),
+ ...assistants.map(assistant => ({
+ ...assistant,
+ contentType: "assistants"
+ }))
+ ]
+
+ const selectedAssistantImage = selectedPreset
+ ? ""
+ : assistantImages.find(
+ image => image.path === selectedAssistant?.image_path
+ )?.base64 || ""
+
+ const modelDetails = LLM_LIST.find(
+ model => model.modelId === selectedPreset?.model
+ )
+
+ return (
+ {
+ setIsOpen(isOpen)
+ setSearch("")
+ }}
+ >
+
+
+ {selectedPreset && (
+
+ )}
+
+ {selectedAssistant &&
+ (selectedAssistantImage ? (
+
+ ) : (
+
+ ))}
+
+ {loading ? (
+ Loading assistant...
+ ) : (
+ <>
+
+ {isModified &&
+ (selectedPreset || selectedAssistant) &&
+ "Modified "}
+
+ {selectedPreset?.name ||
+ selectedAssistant?.name ||
+ t("Quick Settings")}
+
+
+
+ >
+ )}
+
+
+
+
+ {presets.length === 0 && assistants.length === 0 ? (
+ No items found.
+ ) : (
+ <>
+ setSearch(e.target.value)}
+ onKeyDown={e => e.stopPropagation()}
+ />
+
+ {!!(selectedPreset || selectedAssistant) && (
+
+ | Tables<"assistants">)
+ }
+ onSelect={() => {
+ handleSelectQuickSetting(null, "remove")
+ }}
+ image={selectedPreset ? "" : selectedAssistantImage}
+ />
+ )}
+
+ {items
+ .filter(
+ item =>
+ item.name.toLowerCase().includes(search.toLowerCase()) &&
+ item.id !== selectedPreset?.id &&
+ item.id !== selectedAssistant?.id
+ )
+ .map(({ contentType, ...item }) => (
+
+ handleSelectQuickSetting(
+ item,
+ contentType as "presets" | "assistants"
+ )
+ }
+ image={
+ contentType === "assistants"
+ ? assistantImages.find(
+ image =>
+ image.path ===
+ (item as Tables<"assistants">).image_path
+ )?.base64 || ""
+ : ""
+ }
+ />
+ ))}
+ >
+ )}
+
+
+ )
+}
diff --git a/chatbot-ui-main/components/chat/tool-picker.tsx b/chatbot-ui-main/components/chat/tool-picker.tsx
new file mode 100644
index 0000000000..10887379fa
--- /dev/null
+++ b/chatbot-ui-main/components/chat/tool-picker.tsx
@@ -0,0 +1,110 @@
+import { ChatbotUIContext } from "@/context/context"
+import { Tables } from "@/supabase/types"
+import { IconBolt } from "@tabler/icons-react"
+import { FC, useContext, useEffect, useRef } from "react"
+import { usePromptAndCommand } from "./chat-hooks/use-prompt-and-command"
+
+interface ToolPickerProps {}
+
+export const ToolPicker: FC = ({}) => {
+ const {
+ tools,
+ focusTool,
+ toolCommand,
+ isToolPickerOpen,
+ setIsToolPickerOpen
+ } = useContext(ChatbotUIContext)
+
+ const { handleSelectTool } = usePromptAndCommand()
+
+ const itemsRef = useRef<(HTMLDivElement | null)[]>([])
+
+ useEffect(() => {
+ if (focusTool && itemsRef.current[0]) {
+ itemsRef.current[0].focus()
+ }
+ }, [focusTool])
+
+ const filteredTools = tools.filter(tool =>
+ tool.name.toLowerCase().includes(toolCommand.toLowerCase())
+ )
+
+ const handleOpenChange = (isOpen: boolean) => {
+ setIsToolPickerOpen(isOpen)
+ }
+
+ const callSelectTool = (tool: Tables<"tools">) => {
+ handleSelectTool(tool)
+ handleOpenChange(false)
+ }
+
+ const getKeyDownHandler =
+ (index: number) => (e: React.KeyboardEvent) => {
+ if (e.key === "Backspace") {
+ e.preventDefault()
+ handleOpenChange(false)
+ } else if (e.key === "Enter") {
+ e.preventDefault()
+ callSelectTool(filteredTools[index])
+ } else if (
+ (e.key === "Tab" || e.key === "ArrowDown") &&
+ !e.shiftKey &&
+ index === filteredTools.length - 1
+ ) {
+ e.preventDefault()
+ itemsRef.current[0]?.focus()
+ } else if (e.key === "ArrowUp" && !e.shiftKey && index === 0) {
+ // go to last element if arrow up is pressed on first element
+ e.preventDefault()
+ itemsRef.current[itemsRef.current.length - 1]?.focus()
+ } else if (e.key === "ArrowUp") {
+ e.preventDefault()
+ const prevIndex =
+ index - 1 >= 0 ? index - 1 : itemsRef.current.length - 1
+ itemsRef.current[prevIndex]?.focus()
+ } else if (e.key === "ArrowDown") {
+ e.preventDefault()
+ const nextIndex = index + 1 < itemsRef.current.length ? index + 1 : 0
+ itemsRef.current[nextIndex]?.focus()
+ }
+ }
+
+ return (
+ <>
+ {isToolPickerOpen && (
+
+ {filteredTools.length === 0 ? (
+
+ No matching tools.
+
+ ) : (
+ <>
+ {filteredTools.map((item, index) => (
+
{
+ itemsRef.current[index] = ref
+ }}
+ tabIndex={0}
+ className="hover:bg-accent focus:bg-accent flex cursor-pointer items-center rounded p-2 focus:outline-none"
+ onClick={() => callSelectTool(item as Tables<"tools">)}
+ onKeyDown={getKeyDownHandler(index)}
+ >
+
+
+
+
{item.name}
+
+
+ {item.description || "No description."}
+
+
+
+ ))}
+ >
+ )}
+
+ )}
+ >
+ )
+}
diff --git a/chatbot-ui-main/components/icons/anthropic-svg.tsx b/chatbot-ui-main/components/icons/anthropic-svg.tsx
new file mode 100644
index 0000000000..27b2cd18f4
--- /dev/null
+++ b/chatbot-ui-main/components/icons/anthropic-svg.tsx
@@ -0,0 +1,44 @@
+import { FC } from "react"
+
+interface AnthropicSVGProps {
+ height?: number
+ width?: number
+ className?: string
+}
+
+export const AnthropicSVG: FC = ({
+ height = 40,
+ width = 40,
+ className
+}) => {
+ return (
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/chatbot-ui-main/components/icons/chatbotui-svg.tsx b/chatbot-ui-main/components/icons/chatbotui-svg.tsx
new file mode 100644
index 0000000000..4c29cf66f6
--- /dev/null
+++ b/chatbot-ui-main/components/icons/chatbotui-svg.tsx
@@ -0,0 +1,37 @@
+import { FC } from "react"
+
+interface ChatbotUISVGProps {
+ theme: "dark" | "light"
+ scale?: number
+}
+
+export const ChatbotUISVG: FC = ({ theme, scale = 1 }) => {
+ return (
+
+
+
+
+
+ )
+}
diff --git a/chatbot-ui-main/components/icons/google-svg.tsx b/chatbot-ui-main/components/icons/google-svg.tsx
new file mode 100644
index 0000000000..8a86709ce7
--- /dev/null
+++ b/chatbot-ui-main/components/icons/google-svg.tsx
@@ -0,0 +1,42 @@
+import { FC } from "react"
+
+interface GoogleSVGProps {
+ height?: number
+ width?: number
+ className?: string
+}
+
+export const GoogleSVG: FC = ({
+ height = 40,
+ width = 40,
+ className
+}) => {
+ return (
+
+
+
+
+
+
+ )
+}
diff --git a/chatbot-ui-main/components/icons/openai-svg.tsx b/chatbot-ui-main/components/icons/openai-svg.tsx
new file mode 100644
index 0000000000..670c613598
--- /dev/null
+++ b/chatbot-ui-main/components/icons/openai-svg.tsx
@@ -0,0 +1,31 @@
+import { FC } from "react"
+
+interface OpenAISVGProps {
+ height?: number
+ width?: number
+ className?: string
+}
+
+export const OpenAISVG: FC = ({
+ height = 40,
+ width = 40,
+ className
+}) => {
+ return (
+
+
+
+ )
+}
diff --git a/chatbot-ui-main/components/messages/message-actions.tsx b/chatbot-ui-main/components/messages/message-actions.tsx
new file mode 100644
index 0000000000..0e1c8c78ea
--- /dev/null
+++ b/chatbot-ui-main/components/messages/message-actions.tsx
@@ -0,0 +1,117 @@
+import { ChatbotUIContext } from "@/context/context"
+import { IconCheck, IconCopy, IconEdit, IconRepeat } from "@tabler/icons-react"
+import { FC, useContext, useEffect, useState } from "react"
+import { WithTooltip } from "../ui/with-tooltip"
+
+export const MESSAGE_ICON_SIZE = 18
+
+interface MessageActionsProps {
+ isAssistant: boolean
+ isLast: boolean
+ isEditing: boolean
+ isHovering: boolean
+ onCopy: () => void
+ onEdit: () => void
+ onRegenerate: () => void
+}
+
+export const MessageActions: FC = ({
+ isAssistant,
+ isLast,
+ isEditing,
+ isHovering,
+ onCopy,
+ onEdit,
+ onRegenerate
+}) => {
+ const { isGenerating } = useContext(ChatbotUIContext)
+
+ const [showCheckmark, setShowCheckmark] = useState(false)
+
+ const handleCopy = () => {
+ onCopy()
+ setShowCheckmark(true)
+ }
+
+ const handleForkChat = async () => {}
+
+ useEffect(() => {
+ if (showCheckmark) {
+ const timer = setTimeout(() => {
+ setShowCheckmark(false)
+ }, 2000)
+
+ return () => clearTimeout(timer)
+ }
+ }, [showCheckmark])
+
+ return (isLast && isGenerating) || isEditing ? null : (
+
+ {/* {((isAssistant && isHovering) || isLast) && (
+ Fork Chat
}
+ trigger={
+
+ }
+ />
+ )} */}
+
+ {!isAssistant && isHovering && (
+ Edit}
+ trigger={
+
+ }
+ />
+ )}
+
+ {(isHovering || isLast) && (
+ Copy}
+ trigger={
+ showCheckmark ? (
+
+ ) : (
+
+ )
+ }
+ />
+ )}
+
+ {isLast && (
+ Regenerate}
+ trigger={
+
+ }
+ />
+ )}
+
+ {/* {1 > 0 && isAssistant && } */}
+
+ )
+}
diff --git a/chatbot-ui-main/components/messages/message-codeblock.tsx b/chatbot-ui-main/components/messages/message-codeblock.tsx
new file mode 100644
index 0000000000..2b8d79552d
--- /dev/null
+++ b/chatbot-ui-main/components/messages/message-codeblock.tsx
@@ -0,0 +1,135 @@
+import { Button } from "@/components/ui/button"
+import { useCopyToClipboard } from "@/lib/hooks/use-copy-to-clipboard"
+import { IconCheck, IconCopy, IconDownload } from "@tabler/icons-react"
+import { FC, memo } from "react"
+import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
+import { oneDark } from "react-syntax-highlighter/dist/cjs/styles/prism"
+
+interface MessageCodeBlockProps {
+ language: string
+ value: string
+}
+
+interface languageMap {
+ [key: string]: string | undefined
+}
+
+export const programmingLanguages: languageMap = {
+ javascript: ".js",
+ python: ".py",
+ java: ".java",
+ c: ".c",
+ cpp: ".cpp",
+ "c++": ".cpp",
+ "c#": ".cs",
+ ruby: ".rb",
+ php: ".php",
+ swift: ".swift",
+ "objective-c": ".m",
+ kotlin: ".kt",
+ typescript: ".ts",
+ go: ".go",
+ perl: ".pl",
+ rust: ".rs",
+ scala: ".scala",
+ haskell: ".hs",
+ lua: ".lua",
+ shell: ".sh",
+ sql: ".sql",
+ html: ".html",
+ css: ".css"
+}
+
+export const generateRandomString = (length: number, lowercase = false) => {
+ const chars = "ABCDEFGHJKLMNPQRSTUVWXY3456789" // excluding similar looking characters like Z, 2, I, 1, O, 0
+ let result = ""
+ for (let i = 0; i < length; i++) {
+ result += chars.charAt(Math.floor(Math.random() * chars.length))
+ }
+ return lowercase ? result.toLowerCase() : result
+}
+
+export const MessageCodeBlock: FC = memo(
+ ({ language, value }) => {
+ const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 })
+
+ const downloadAsFile = () => {
+ if (typeof window === "undefined") {
+ return
+ }
+ const fileExtension = programmingLanguages[language] || ".file"
+ const suggestedFileName = `file-${generateRandomString(
+ 3,
+ true
+ )}${fileExtension}`
+ const fileName = window.prompt("Enter file name" || "", suggestedFileName)
+
+ if (!fileName) {
+ return
+ }
+
+ const blob = new Blob([value], { type: "text/plain" })
+ const url = URL.createObjectURL(blob)
+ const link = document.createElement("a")
+ link.download = fileName
+ link.href = url
+ link.style.display = "none"
+ document.body.appendChild(link)
+ link.click()
+ document.body.removeChild(link)
+ URL.revokeObjectURL(url)
+ }
+
+ const onCopy = () => {
+ if (isCopied) return
+ copyToClipboard(value)
+ }
+
+ return (
+
+
+
{language}
+
+
+
+
+
+
+ {isCopied ? : }
+
+
+
+
+ {value}
+
+
+ )
+ }
+)
+
+MessageCodeBlock.displayName = "MessageCodeBlock"
diff --git a/chatbot-ui-main/components/messages/message-markdown-memoized.tsx b/chatbot-ui-main/components/messages/message-markdown-memoized.tsx
new file mode 100644
index 0000000000..2fc2106500
--- /dev/null
+++ b/chatbot-ui-main/components/messages/message-markdown-memoized.tsx
@@ -0,0 +1,9 @@
+import { FC, memo } from "react"
+import ReactMarkdown, { Options } from "react-markdown"
+
+export const MessageMarkdownMemoized: FC = memo(
+ ReactMarkdown,
+ (prevProps, nextProps) =>
+ prevProps.children === nextProps.children &&
+ prevProps.className === nextProps.className
+)
diff --git a/chatbot-ui-main/components/messages/message-markdown.tsx b/chatbot-ui-main/components/messages/message-markdown.tsx
new file mode 100644
index 0000000000..88be7e9339
--- /dev/null
+++ b/chatbot-ui-main/components/messages/message-markdown.tsx
@@ -0,0 +1,65 @@
+import React, { FC } from "react"
+import remarkGfm from "remark-gfm"
+import remarkMath from "remark-math"
+import { MessageCodeBlock } from "./message-codeblock"
+import { MessageMarkdownMemoized } from "./message-markdown-memoized"
+
+interface MessageMarkdownProps {
+ content: string
+}
+
+export const MessageMarkdown: FC = ({ content }) => {
+ return (
+ {children}
+ },
+ img({ node, ...props }) {
+ return
+ },
+ code({ node, className, children, ...props }) {
+ const childArray = React.Children.toArray(children)
+ const firstChild = childArray[0] as React.ReactElement
+ const firstChildAsString = React.isValidElement(firstChild)
+ ? (firstChild as React.ReactElement).props.children
+ : firstChild
+
+ if (firstChildAsString === "▍") {
+ return ▍
+ }
+
+ if (typeof firstChildAsString === "string") {
+ childArray[0] = firstChildAsString.replace("`▍`", "▍")
+ }
+
+ const match = /language-(\w+)/.exec(className || "")
+
+ if (
+ typeof firstChildAsString === "string" &&
+ !firstChildAsString.includes("\n")
+ ) {
+ return (
+
+ {childArray}
+
+ )
+ }
+
+ return (
+
+ )
+ }
+ }}
+ >
+ {content}
+
+ )
+}
diff --git a/chatbot-ui-main/components/messages/message-replies.tsx b/chatbot-ui-main/components/messages/message-replies.tsx
new file mode 100644
index 0000000000..e9dd75b67d
--- /dev/null
+++ b/chatbot-ui-main/components/messages/message-replies.tsx
@@ -0,0 +1,51 @@
+import { IconMessage } from "@tabler/icons-react"
+import { FC, useState } from "react"
+import {
+ Sheet,
+ SheetContent,
+ SheetDescription,
+ SheetHeader,
+ SheetTitle,
+ SheetTrigger
+} from "../ui/sheet"
+import { WithTooltip } from "../ui/with-tooltip"
+import { MESSAGE_ICON_SIZE } from "./message-actions"
+
+interface MessageRepliesProps {}
+
+export const MessageReplies: FC = ({}) => {
+ const [isOpen, setIsOpen] = useState(false)
+
+ return (
+
+
+ View Replies}
+ trigger={
+ setIsOpen(true)}
+ >
+
+
+ {1}
+
+
+ }
+ />
+
+
+
+
+ Are you sure absolutely sure?
+
+ This action cannot be undone. This will permanently delete your
+ account and remove your data from our servers.
+
+
+
+
+ )
+}
diff --git a/chatbot-ui-main/components/messages/message.tsx b/chatbot-ui-main/components/messages/message.tsx
new file mode 100644
index 0000000000..d0867d68ab
--- /dev/null
+++ b/chatbot-ui-main/components/messages/message.tsx
@@ -0,0 +1,445 @@
+import { useChatHandler } from "@/components/chat/chat-hooks/use-chat-handler"
+import { ChatbotUIContext } from "@/context/context"
+import { LLM_LIST } from "@/lib/models/llm/llm-list"
+import { cn } from "@/lib/utils"
+import { Tables } from "@/supabase/types"
+import { LLM, LLMID, MessageImage, ModelProvider } from "@/types"
+import {
+ IconBolt,
+ IconCaretDownFilled,
+ IconCaretRightFilled,
+ IconCircleFilled,
+ IconFileText,
+ IconMoodSmile,
+ IconPencil
+} from "@tabler/icons-react"
+import Image from "next/image"
+import { FC, useContext, useEffect, useRef, useState } from "react"
+import { ModelIcon } from "../models/model-icon"
+import { Button } from "../ui/button"
+import { FileIcon } from "../ui/file-icon"
+import { FilePreview } from "../ui/file-preview"
+import { TextareaAutosize } from "../ui/textarea-autosize"
+import { WithTooltip } from "../ui/with-tooltip"
+import { MessageActions } from "./message-actions"
+import { MessageMarkdown } from "./message-markdown"
+
+const ICON_SIZE = 32
+
+interface MessageProps {
+ message: Tables<"messages">
+ fileItems: Tables<"file_items">[]
+ isEditing: boolean
+ isLast: boolean
+ onStartEdit: (message: Tables<"messages">) => void
+ onCancelEdit: () => void
+ onSubmitEdit: (value: string, sequenceNumber: number) => void
+}
+
+export const Message: FC = ({
+ message,
+ fileItems,
+ isEditing,
+ isLast,
+ onStartEdit,
+ onCancelEdit,
+ onSubmitEdit
+}) => {
+ const {
+ assistants,
+ profile,
+ isGenerating,
+ setIsGenerating,
+ firstTokenReceived,
+ availableLocalModels,
+ availableOpenRouterModels,
+ chatMessages,
+ selectedAssistant,
+ chatImages,
+ assistantImages,
+ toolInUse,
+ files,
+ models
+ } = useContext(ChatbotUIContext)
+
+ const { handleSendMessage } = useChatHandler()
+
+ const editInputRef = useRef(null)
+
+ const [isHovering, setIsHovering] = useState(false)
+ const [editedMessage, setEditedMessage] = useState(message.content)
+
+ const [showImagePreview, setShowImagePreview] = useState(false)
+ const [selectedImage, setSelectedImage] = useState(null)
+
+ const [showFileItemPreview, setShowFileItemPreview] = useState(false)
+ const [selectedFileItem, setSelectedFileItem] =
+ useState | null>(null)
+
+ const [viewSources, setViewSources] = useState(false)
+
+ const handleCopy = () => {
+ if (navigator.clipboard) {
+ navigator.clipboard.writeText(message.content)
+ } else {
+ const textArea = document.createElement("textarea")
+ textArea.value = message.content
+ document.body.appendChild(textArea)
+ textArea.focus()
+ textArea.select()
+ document.execCommand("copy")
+ document.body.removeChild(textArea)
+ }
+ }
+
+ const handleSendEdit = () => {
+ onSubmitEdit(editedMessage, message.sequence_number)
+ onCancelEdit()
+ }
+
+ const handleKeyDown = (event: React.KeyboardEvent) => {
+ if (isEditing && event.key === "Enter" && event.metaKey) {
+ handleSendEdit()
+ }
+ }
+
+ const handleRegenerate = async () => {
+ setIsGenerating(true)
+ await handleSendMessage(
+ editedMessage || chatMessages[chatMessages.length - 2].message.content,
+ chatMessages,
+ true
+ )
+ }
+
+ const handleStartEdit = () => {
+ onStartEdit(message)
+ }
+
+ useEffect(() => {
+ setEditedMessage(message.content)
+
+ if (isEditing && editInputRef.current) {
+ const input = editInputRef.current
+ input.focus()
+ input.setSelectionRange(input.value.length, input.value.length)
+ }
+ }, [isEditing])
+
+ const MODEL_DATA = [
+ ...models.map(model => ({
+ modelId: model.model_id as LLMID,
+ modelName: model.name,
+ provider: "custom" as ModelProvider,
+ hostedId: model.id,
+ platformLink: "",
+ imageInput: false
+ })),
+ ...LLM_LIST,
+ ...availableLocalModels,
+ ...availableOpenRouterModels
+ ].find(llm => llm.modelId === message.model) as LLM
+
+ const messageAssistantImage = assistantImages.find(
+ image => image.assistantId === message.assistant_id
+ )?.base64
+
+ const selectedAssistantImage = assistantImages.find(
+ image => image.path === selectedAssistant?.image_path
+ )?.base64
+
+ const modelDetails = LLM_LIST.find(model => model.modelId === message.model)
+
+ const fileAccumulator: Record<
+ string,
+ {
+ id: string
+ name: string
+ count: number
+ type: string
+ description: string
+ }
+ > = {}
+
+ const fileSummary = fileItems.reduce((acc, fileItem) => {
+ const parentFile = files.find(file => file.id === fileItem.file_id)
+ if (parentFile) {
+ if (!acc[parentFile.id]) {
+ acc[parentFile.id] = {
+ id: parentFile.id,
+ name: parentFile.name,
+ count: 1,
+ type: parentFile.type,
+ description: parentFile.description
+ }
+ } else {
+ acc[parentFile.id].count += 1
+ }
+ }
+ return acc
+ }, fileAccumulator)
+
+ return (
+ setIsHovering(true)}
+ onMouseLeave={() => setIsHovering(false)}
+ onKeyDown={handleKeyDown}
+ >
+
+
+
+
+
+ {message.role === "system" ? (
+
+ ) : (
+
+ {message.role === "assistant" ? (
+ messageAssistantImage ? (
+
+ ) : (
+ {MODEL_DATA?.modelName}
}
+ trigger={
+
+ }
+ />
+ )
+ ) : profile?.image_url ? (
+
+ ) : (
+
+ )}
+
+
+ {message.role === "assistant"
+ ? message.assistant_id
+ ? assistants.find(
+ assistant => assistant.id === message.assistant_id
+ )?.name
+ : selectedAssistant
+ ? selectedAssistant?.name
+ : MODEL_DATA?.modelName
+ : profile?.display_name ?? profile?.username}
+
+
+ )}
+ {!firstTokenReceived &&
+ isGenerating &&
+ isLast &&
+ message.role === "assistant" ? (
+ <>
+ {(() => {
+ switch (toolInUse) {
+ case "none":
+ return (
+
+ )
+ case "retrieval":
+ return (
+
+
+
+
Searching files...
+
+ )
+ default:
+ return (
+
+
+
+
Using {toolInUse}...
+
+ )
+ }
+ })()}
+ >
+ ) : isEditing ? (
+
+ ) : (
+
+ )}
+
+
+ {fileItems.length > 0 && (
+
+ {!viewSources ? (
+
setViewSources(true)}
+ >
+ {fileItems.length}
+ {fileItems.length > 1 ? " Sources " : " Source "}
+ from {Object.keys(fileSummary).length}{" "}
+ {Object.keys(fileSummary).length > 1 ? "Files" : "File"}{" "}
+
+
+ ) : (
+ <>
+
setViewSources(false)}
+ >
+ {fileItems.length}
+ {fileItems.length > 1 ? " Sources " : " Source "}
+ from {Object.keys(fileSummary).length}{" "}
+ {Object.keys(fileSummary).length > 1 ? "Files" : "File"}{" "}
+
+
+
+
+ {Object.values(fileSummary).map((file, index) => (
+
+
+
+ {fileItems
+ .filter(fileItem => {
+ const parentFile = files.find(
+ parentFile => parentFile.id === fileItem.file_id
+ )
+ return parentFile?.id === file.id
+ })
+ .map((fileItem, index) => (
+
{
+ setSelectedFileItem(fileItem)
+ setShowFileItemPreview(true)
+ }}
+ >
+
+ - {" "}
+ {fileItem.content.substring(0, 200)}...
+
+
+ ))}
+
+ ))}
+
+ >
+ )}
+
+ )}
+
+
+ {message.image_paths.map((path, index) => {
+ const item = chatImages.find(image => image.path === path)
+
+ return (
+ {
+ setSelectedImage({
+ messageId: message.id,
+ path,
+ base64: path.startsWith("data") ? path : item?.base64 || "",
+ url: path.startsWith("data") ? "" : item?.url || "",
+ file: null
+ })
+
+ setShowImagePreview(true)
+ }}
+ loading="lazy"
+ />
+ )
+ })}
+
+ {isEditing && (
+
+
+ Save & Send
+
+
+
+ Cancel
+
+
+ )}
+
+
+ {showImagePreview && selectedImage && (
+ {
+ setShowImagePreview(isOpen)
+ setSelectedImage(null)
+ }}
+ />
+ )}
+
+ {showFileItemPreview && selectedFileItem && (
+ {
+ setShowFileItemPreview(isOpen)
+ setSelectedFileItem(null)
+ }}
+ />
+ )}
+
+ )
+}
diff --git a/chatbot-ui-main/components/models/model-icon.tsx b/chatbot-ui-main/components/models/model-icon.tsx
new file mode 100644
index 0000000000..27ca7b42c4
--- /dev/null
+++ b/chatbot-ui-main/components/models/model-icon.tsx
@@ -0,0 +1,107 @@
+import { cn } from "@/lib/utils"
+import mistral from "@/public/providers/mistral.png"
+import groq from "@/public/providers/groq.png"
+import perplexity from "@/public/providers/perplexity.png"
+import { ModelProvider } from "@/types"
+import { IconSparkles } from "@tabler/icons-react"
+import { useTheme } from "next-themes"
+import Image from "next/image"
+import { FC, HTMLAttributes } from "react"
+import { AnthropicSVG } from "../icons/anthropic-svg"
+import { GoogleSVG } from "../icons/google-svg"
+import { OpenAISVG } from "../icons/openai-svg"
+
+interface ModelIconProps extends HTMLAttributes {
+ provider: ModelProvider
+ height: number
+ width: number
+}
+
+export const ModelIcon: FC = ({
+ provider,
+ height,
+ width,
+ ...props
+}) => {
+ const { theme } = useTheme()
+
+ switch (provider as ModelProvider) {
+ case "openai":
+ return (
+
+ )
+ case "mistral":
+ return (
+
+ )
+ case "groq":
+ return (
+
+ )
+ case "anthropic":
+ return (
+
+ )
+ case "google":
+ return (
+
+ )
+ case "perplexity":
+ return (
+
+ )
+ default:
+ return
+ }
+}
diff --git a/chatbot-ui-main/components/models/model-option.tsx b/chatbot-ui-main/components/models/model-option.tsx
new file mode 100644
index 0000000000..2344d3dc7d
--- /dev/null
+++ b/chatbot-ui-main/components/models/model-option.tsx
@@ -0,0 +1,49 @@
+import { LLM } from "@/types"
+import { FC } from "react"
+import { ModelIcon } from "./model-icon"
+import { IconInfoCircle } from "@tabler/icons-react"
+import { WithTooltip } from "../ui/with-tooltip"
+
+interface ModelOptionProps {
+ model: LLM
+ onSelect: () => void
+}
+
+export const ModelOption: FC = ({ model, onSelect }) => {
+ return (
+
+ {model.provider !== "ollama" && model.pricing && (
+
+
+ Input Cost: {" "}
+ {model.pricing.inputCost} {model.pricing.currency} per{" "}
+ {model.pricing.unit}
+
+ {model.pricing.outputCost && (
+
+ Output Cost: {" "}
+ {model.pricing.outputCost} {model.pricing.currency} per{" "}
+ {model.pricing.unit}
+
+ )}
+
+ )}
+
+ }
+ side="bottom"
+ trigger={
+
+ }
+ />
+ )
+}
diff --git a/chatbot-ui-main/components/models/model-select.tsx b/chatbot-ui-main/components/models/model-select.tsx
new file mode 100644
index 0000000000..d25d6de507
--- /dev/null
+++ b/chatbot-ui-main/components/models/model-select.tsx
@@ -0,0 +1,203 @@
+import { ChatbotUIContext } from "@/context/context"
+import { LLM, LLMID, ModelProvider } from "@/types"
+import { IconCheck, IconChevronDown } from "@tabler/icons-react"
+import { FC, useContext, useEffect, useRef, useState } from "react"
+import { Button } from "../ui/button"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuTrigger
+} from "../ui/dropdown-menu"
+import { Input } from "../ui/input"
+import { Tabs, TabsList, TabsTrigger } from "../ui/tabs"
+import { ModelIcon } from "./model-icon"
+import { ModelOption } from "./model-option"
+
+interface ModelSelectProps {
+ selectedModelId: string
+ onSelectModel: (modelId: LLMID) => void
+}
+
+export const ModelSelect: FC = ({
+ selectedModelId,
+ onSelectModel
+}) => {
+ const {
+ profile,
+ models,
+ availableHostedModels,
+ availableLocalModels,
+ availableOpenRouterModels
+ } = useContext(ChatbotUIContext)
+
+ const inputRef = useRef(null)
+ const triggerRef = useRef(null)
+
+ const [isOpen, setIsOpen] = useState(false)
+ const [search, setSearch] = useState("")
+ const [tab, setTab] = useState<"hosted" | "local">("hosted")
+
+ useEffect(() => {
+ if (isOpen) {
+ setTimeout(() => {
+ inputRef.current?.focus()
+ }, 100) // FIX: hacky
+ }
+ }, [isOpen])
+
+ const handleSelectModel = (modelId: LLMID) => {
+ onSelectModel(modelId)
+ setIsOpen(false)
+ }
+
+ const allModels = [
+ ...models.map(model => ({
+ modelId: model.model_id as LLMID,
+ modelName: model.name,
+ provider: "custom" as ModelProvider,
+ hostedId: model.id,
+ platformLink: "",
+ imageInput: false
+ })),
+ ...availableHostedModels,
+ ...availableLocalModels,
+ ...availableOpenRouterModels
+ ]
+
+ const groupedModels = allModels.reduce>(
+ (groups, model) => {
+ const key = model.provider
+ if (!groups[key]) {
+ groups[key] = []
+ }
+ groups[key].push(model)
+ return groups
+ },
+ {}
+ )
+
+ const selectedModel = allModels.find(
+ model => model.modelId === selectedModelId
+ )
+
+ if (!profile) return null
+
+ return (
+ {
+ setIsOpen(isOpen)
+ setSearch("")
+ }}
+ >
+
+ {allModels.length === 0 ? (
+
+ Unlock models by entering API keys in your profile settings.
+
+ ) : (
+
+
+ {selectedModel ? (
+ <>
+
+
+ {selectedModel?.modelName}
+
+ >
+ ) : (
+
Select a model
+ )}
+
+
+
+
+ )}
+
+
+
+ setTab(value)}>
+ {availableLocalModels.length > 0 && (
+
+ Hosted
+
+ Local
+
+ )}
+
+
+ setSearch(e.target.value)}
+ />
+
+
+ {Object.entries(groupedModels).map(([provider, models]) => {
+ const filteredModels = models
+ .filter(model => {
+ if (tab === "hosted") return model.provider !== "ollama"
+ if (tab === "local") return model.provider === "ollama"
+ if (tab === "openrouter") return model.provider === "openrouter"
+ })
+ .filter(model =>
+ model.modelName.toLowerCase().includes(search.toLowerCase())
+ )
+ .sort((a, b) => a.provider.localeCompare(b.provider))
+
+ if (filteredModels.length === 0) return null
+
+ return (
+
+
+ {provider === "openai" && profile.use_azure_openai
+ ? "AZURE OPENAI"
+ : provider.toLocaleUpperCase()}
+
+
+
+ {filteredModels.map(model => {
+ return (
+
+ {selectedModelId === model.modelId && (
+
+ )}
+
+ handleSelectModel(model.modelId)}
+ />
+
+ )
+ })}
+
+
+ )
+ })}
+
+
+
+ )
+}
diff --git a/chatbot-ui-main/components/setup/api-step.tsx b/chatbot-ui-main/components/setup/api-step.tsx
new file mode 100644
index 0000000000..16393be500
--- /dev/null
+++ b/chatbot-ui-main/components/setup/api-step.tsx
@@ -0,0 +1,243 @@
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { FC } from "react"
+import { Button } from "../ui/button"
+
+interface APIStepProps {
+ openaiAPIKey: string
+ openaiOrgID: string
+ azureOpenaiAPIKey: string
+ azureOpenaiEndpoint: string
+ azureOpenai35TurboID: string
+ azureOpenai45TurboID: string
+ azureOpenai45VisionID: string
+ azureOpenaiEmbeddingsID: string
+ anthropicAPIKey: string
+ googleGeminiAPIKey: string
+ mistralAPIKey: string
+ groqAPIKey: string
+ perplexityAPIKey: string
+ useAzureOpenai: boolean
+ openrouterAPIKey: string
+ onOpenrouterAPIKeyChange: (value: string) => void
+ onOpenaiAPIKeyChange: (value: string) => void
+ onOpenaiOrgIDChange: (value: string) => void
+ onAzureOpenaiAPIKeyChange: (value: string) => void
+ onAzureOpenaiEndpointChange: (value: string) => void
+ onAzureOpenai35TurboIDChange: (value: string) => void
+ onAzureOpenai45TurboIDChange: (value: string) => void
+ onAzureOpenai45VisionIDChange: (value: string) => void
+ onAzureOpenaiEmbeddingsIDChange: (value: string) => void
+ onAnthropicAPIKeyChange: (value: string) => void
+ onGoogleGeminiAPIKeyChange: (value: string) => void
+ onMistralAPIKeyChange: (value: string) => void
+ onGroqAPIKeyChange: (value: string) => void
+ onPerplexityAPIKeyChange: (value: string) => void
+ onUseAzureOpenaiChange: (value: boolean) => void
+}
+
+export const APIStep: FC = ({
+ openaiAPIKey,
+ openaiOrgID,
+ azureOpenaiAPIKey,
+ azureOpenaiEndpoint,
+ azureOpenai35TurboID,
+ azureOpenai45TurboID,
+ azureOpenai45VisionID,
+ azureOpenaiEmbeddingsID,
+ anthropicAPIKey,
+ googleGeminiAPIKey,
+ mistralAPIKey,
+ groqAPIKey,
+ perplexityAPIKey,
+ openrouterAPIKey,
+ useAzureOpenai,
+ onOpenaiAPIKeyChange,
+ onOpenaiOrgIDChange,
+ onAzureOpenaiAPIKeyChange,
+ onAzureOpenaiEndpointChange,
+ onAzureOpenai35TurboIDChange,
+ onAzureOpenai45TurboIDChange,
+ onAzureOpenai45VisionIDChange,
+ onAzureOpenaiEmbeddingsIDChange,
+ onAnthropicAPIKeyChange,
+ onGoogleGeminiAPIKeyChange,
+ onMistralAPIKeyChange,
+ onGroqAPIKeyChange,
+ onPerplexityAPIKeyChange,
+ onUseAzureOpenaiChange,
+ onOpenrouterAPIKeyChange
+}) => {
+ return (
+ <>
+
+
+
+ {useAzureOpenai ? "Azure OpenAI API Key" : "OpenAI API Key"}
+
+
+ onUseAzureOpenaiChange(!useAzureOpenai)}
+ >
+ {useAzureOpenai
+ ? "Switch To Standard OpenAI"
+ : "Switch To Azure OpenAI"}
+
+
+
+
+ useAzureOpenai
+ ? onAzureOpenaiAPIKeyChange(e.target.value)
+ : onOpenaiAPIKeyChange(e.target.value)
+ }
+ />
+
+
+
+
+
+ Anthropic API Key
+
+ onAnthropicAPIKeyChange(e.target.value)}
+ />
+
+
+
+ Google Gemini API Key
+
+ onGoogleGeminiAPIKeyChange(e.target.value)}
+ />
+
+
+
+ Mistral API Key
+
+ onMistralAPIKeyChange(e.target.value)}
+ />
+
+
+
+ Groq API Key
+
+ onGroqAPIKeyChange(e.target.value)}
+ />
+
+
+
+ Perplexity API Key
+
+ onPerplexityAPIKeyChange(e.target.value)}
+ />
+
+
+ OpenRouter API Key
+
+ onOpenrouterAPIKeyChange(e.target.value)}
+ />
+
+ >
+ )
+}
diff --git a/chatbot-ui-main/components/setup/finish-step.tsx b/chatbot-ui-main/components/setup/finish-step.tsx
new file mode 100644
index 0000000000..d0747d32e4
--- /dev/null
+++ b/chatbot-ui-main/components/setup/finish-step.tsx
@@ -0,0 +1,18 @@
+import { FC } from "react"
+
+interface FinishStepProps {
+ displayName: string
+}
+
+export const FinishStep: FC = ({ displayName }) => {
+ return (
+
+
+ Welcome to Chatbot UI
+ {displayName.length > 0 ? `, ${displayName.split(" ")[0]}` : null}!
+
+
+
Click next to start chatting.
+
+ )
+}
diff --git a/chatbot-ui-main/components/setup/profile-step.tsx b/chatbot-ui-main/components/setup/profile-step.tsx
new file mode 100644
index 0000000000..eaf8faf813
--- /dev/null
+++ b/chatbot-ui-main/components/setup/profile-step.tsx
@@ -0,0 +1,149 @@
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import {
+ PROFILE_DISPLAY_NAME_MAX,
+ PROFILE_USERNAME_MAX,
+ PROFILE_USERNAME_MIN
+} from "@/db/limits"
+import {
+ IconCircleCheckFilled,
+ IconCircleXFilled,
+ IconLoader2
+} from "@tabler/icons-react"
+import { FC, useCallback, useState } from "react"
+import { LimitDisplay } from "../ui/limit-display"
+import { toast } from "sonner"
+
+interface ProfileStepProps {
+ username: string
+ usernameAvailable: boolean
+ displayName: string
+ onUsernameAvailableChange: (isAvailable: boolean) => void
+ onUsernameChange: (username: string) => void
+ onDisplayNameChange: (name: string) => void
+}
+
+export const ProfileStep: FC = ({
+ username,
+ usernameAvailable,
+ displayName,
+ onUsernameAvailableChange,
+ onUsernameChange,
+ onDisplayNameChange
+}) => {
+ const [loading, setLoading] = useState(false)
+
+ const debounce = (func: (...args: any[]) => void, wait: number) => {
+ let timeout: NodeJS.Timeout | null
+
+ return (...args: any[]) => {
+ const later = () => {
+ if (timeout) clearTimeout(timeout)
+ func(...args)
+ }
+
+ if (timeout) clearTimeout(timeout)
+ timeout = setTimeout(later, wait)
+ }
+ }
+
+ const checkUsernameAvailability = useCallback(
+ debounce(async (username: string) => {
+ if (!username) return
+
+ if (username.length < PROFILE_USERNAME_MIN) {
+ onUsernameAvailableChange(false)
+ return
+ }
+
+ if (username.length > PROFILE_USERNAME_MAX) {
+ onUsernameAvailableChange(false)
+ return
+ }
+
+ const usernameRegex = /^[a-zA-Z0-9_]+$/
+ if (!usernameRegex.test(username)) {
+ onUsernameAvailableChange(false)
+ toast.error(
+ "Username must be letters, numbers, or underscores only - no other characters or spacing allowed."
+ )
+ return
+ }
+
+ setLoading(true)
+
+ const response = await fetch(`/api/username/available`, {
+ method: "POST",
+ body: JSON.stringify({ username })
+ })
+
+ const data = await response.json()
+ const isAvailable = data.isAvailable
+
+ onUsernameAvailableChange(isAvailable)
+
+ setLoading(false)
+ }, 500),
+ []
+ )
+
+ return (
+ <>
+
+
+
Username
+
+
+ {usernameAvailable ? (
+
AVAILABLE
+ ) : (
+
UNAVAILABLE
+ )}
+
+
+
+
+
{
+ onUsernameChange(e.target.value)
+ checkUsernameAvailability(e.target.value)
+ }}
+ minLength={PROFILE_USERNAME_MIN}
+ maxLength={PROFILE_USERNAME_MAX}
+ />
+
+
+ {loading ? (
+
+ ) : usernameAvailable ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+ Chat Display Name
+
+ onDisplayNameChange(e.target.value)}
+ maxLength={PROFILE_DISPLAY_NAME_MAX}
+ />
+
+
+
+ >
+ )
+}
diff --git a/chatbot-ui-main/components/setup/step-container.tsx b/chatbot-ui-main/components/setup/step-container.tsx
new file mode 100644
index 0000000000..2a9ed6972a
--- /dev/null
+++ b/chatbot-ui-main/components/setup/step-container.tsx
@@ -0,0 +1,89 @@
+import { Button } from "@/components/ui/button"
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle
+} from "@/components/ui/card"
+import { FC, useRef } from "react"
+
+export const SETUP_STEP_COUNT = 3
+
+interface StepContainerProps {
+ stepDescription: string
+ stepNum: number
+ stepTitle: string
+ onShouldProceed: (shouldProceed: boolean) => void
+ children?: React.ReactNode
+ showBackButton?: boolean
+ showNextButton?: boolean
+}
+
+export const StepContainer: FC = ({
+ stepDescription,
+ stepNum,
+ stepTitle,
+ onShouldProceed,
+ children,
+ showBackButton = false,
+ showNextButton = true
+}) => {
+ const buttonRef = useRef(null)
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter" && !e.shiftKey) {
+ if (buttonRef.current) {
+ buttonRef.current.click()
+ }
+ }
+ }
+
+ return (
+
+
+
+ {stepTitle}
+
+
+ {stepNum} / {SETUP_STEP_COUNT}
+
+
+
+ {stepDescription}
+
+
+ {children}
+
+
+
+ {showBackButton && (
+ onShouldProceed(false)}
+ >
+ Back
+
+ )}
+
+
+
+ {showNextButton && (
+ onShouldProceed(true)}
+ >
+ Next
+
+ )}
+
+
+
+ )
+}
diff --git a/chatbot-ui-main/components/sidebar/items/all/sidebar-create-item.tsx b/chatbot-ui-main/components/sidebar/items/all/sidebar-create-item.tsx
new file mode 100644
index 0000000000..18d08ff4b1
--- /dev/null
+++ b/chatbot-ui-main/components/sidebar/items/all/sidebar-create-item.tsx
@@ -0,0 +1,254 @@
+import { Button } from "@/components/ui/button"
+import {
+ Sheet,
+ SheetContent,
+ SheetFooter,
+ SheetHeader,
+ SheetTitle
+} from "@/components/ui/sheet"
+import { ChatbotUIContext } from "@/context/context"
+import { createAssistantCollections } from "@/db/assistant-collections"
+import { createAssistantFiles } from "@/db/assistant-files"
+import { createAssistantTools } from "@/db/assistant-tools"
+import { createAssistant, updateAssistant } from "@/db/assistants"
+import { createChat } from "@/db/chats"
+import { createCollectionFiles } from "@/db/collection-files"
+import { createCollection } from "@/db/collections"
+import { createFileBasedOnExtension } from "@/db/files"
+import { createModel } from "@/db/models"
+import { createPreset } from "@/db/presets"
+import { createPrompt } from "@/db/prompts"
+import {
+ getAssistantImageFromStorage,
+ uploadAssistantImage
+} from "@/db/storage/assistant-images"
+import { createTool } from "@/db/tools"
+import { convertBlobToBase64 } from "@/lib/blob-to-b64"
+import { Tables, TablesInsert } from "@/supabase/types"
+import { ContentType } from "@/types"
+import { FC, useContext, useRef, useState } from "react"
+import { toast } from "sonner"
+
+interface SidebarCreateItemProps {
+ isOpen: boolean
+ isTyping: boolean
+ onOpenChange: (isOpen: boolean) => void
+ contentType: ContentType
+ renderInputs: () => JSX.Element
+ createState: any
+}
+
+export const SidebarCreateItem: FC = ({
+ isOpen,
+ onOpenChange,
+ contentType,
+ renderInputs,
+ createState,
+ isTyping
+}) => {
+ const {
+ selectedWorkspace,
+ setChats,
+ setPresets,
+ setPrompts,
+ setFiles,
+ setCollections,
+ setAssistants,
+ setAssistantImages,
+ setTools,
+ setModels
+ } = useContext(ChatbotUIContext)
+
+ const buttonRef = useRef(null)
+
+ const [creating, setCreating] = useState(false)
+
+ const createFunctions = {
+ chats: createChat,
+ presets: createPreset,
+ prompts: createPrompt,
+ files: async (
+ createState: { file: File } & TablesInsert<"files">,
+ workspaceId: string
+ ) => {
+ if (!selectedWorkspace) return
+
+ const { file, ...rest } = createState
+
+ const createdFile = await createFileBasedOnExtension(
+ file,
+ rest,
+ workspaceId,
+ selectedWorkspace.embeddings_provider as "openai" | "local"
+ )
+
+ return createdFile
+ },
+ collections: async (
+ createState: {
+ image: File
+ collectionFiles: TablesInsert<"collection_files">[]
+ } & Tables<"collections">,
+ workspaceId: string
+ ) => {
+ const { collectionFiles, ...rest } = createState
+
+ const createdCollection = await createCollection(rest, workspaceId)
+
+ const finalCollectionFiles = collectionFiles.map(collectionFile => ({
+ ...collectionFile,
+ collection_id: createdCollection.id
+ }))
+
+ await createCollectionFiles(finalCollectionFiles)
+
+ return createdCollection
+ },
+ assistants: async (
+ createState: {
+ image: File
+ files: Tables<"files">[]
+ collections: Tables<"collections">[]
+ tools: Tables<"tools">[]
+ } & Tables<"assistants">,
+ workspaceId: string
+ ) => {
+ const { image, files, collections, tools, ...rest } = createState
+
+ const createdAssistant = await createAssistant(rest, workspaceId)
+
+ let updatedAssistant = createdAssistant
+
+ if (image) {
+ const filePath = await uploadAssistantImage(createdAssistant, image)
+
+ updatedAssistant = await updateAssistant(createdAssistant.id, {
+ image_path: filePath
+ })
+
+ const url = (await getAssistantImageFromStorage(filePath)) || ""
+
+ if (url) {
+ const response = await fetch(url)
+ const blob = await response.blob()
+ const base64 = await convertBlobToBase64(blob)
+
+ setAssistantImages(prev => [
+ ...prev,
+ {
+ assistantId: updatedAssistant.id,
+ path: filePath,
+ base64,
+ url
+ }
+ ])
+ }
+ }
+
+ const assistantFiles = files.map(file => ({
+ user_id: rest.user_id,
+ assistant_id: createdAssistant.id,
+ file_id: file.id
+ }))
+
+ const assistantCollections = collections.map(collection => ({
+ user_id: rest.user_id,
+ assistant_id: createdAssistant.id,
+ collection_id: collection.id
+ }))
+
+ const assistantTools = tools.map(tool => ({
+ user_id: rest.user_id,
+ assistant_id: createdAssistant.id,
+ tool_id: tool.id
+ }))
+
+ await createAssistantFiles(assistantFiles)
+ await createAssistantCollections(assistantCollections)
+ await createAssistantTools(assistantTools)
+
+ return updatedAssistant
+ },
+ tools: createTool,
+ models: createModel
+ }
+
+ const stateUpdateFunctions = {
+ chats: setChats,
+ presets: setPresets,
+ prompts: setPrompts,
+ files: setFiles,
+ collections: setCollections,
+ assistants: setAssistants,
+ tools: setTools,
+ models: setModels
+ }
+
+ const handleCreate = async () => {
+ try {
+ if (!selectedWorkspace) return
+ if (isTyping) return // Prevent creation while typing
+
+ const createFunction = createFunctions[contentType]
+ const setStateFunction = stateUpdateFunctions[contentType]
+
+ if (!createFunction || !setStateFunction) return
+
+ setCreating(true)
+
+ const newItem = await createFunction(createState, selectedWorkspace.id)
+
+ setStateFunction((prevItems: any) => [...prevItems, newItem])
+
+ onOpenChange(false)
+ setCreating(false)
+ } catch (error) {
+ toast.error(`Error creating ${contentType.slice(0, -1)}. ${error}.`)
+ setCreating(false)
+ }
+ }
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (!isTyping && e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault()
+ buttonRef.current?.click()
+ }
+ }
+
+ return (
+
+
+
+
+
+ Create{" "}
+ {contentType.charAt(0).toUpperCase() + contentType.slice(1, -1)}
+
+
+
+
{renderInputs()}
+
+
+
+
+ onOpenChange(false)}
+ >
+ Cancel
+
+
+
+ {creating ? "Creating..." : "Create"}
+
+
+
+
+
+ )
+}
diff --git a/chatbot-ui-main/components/sidebar/items/all/sidebar-delete-item.tsx b/chatbot-ui-main/components/sidebar/items/all/sidebar-delete-item.tsx
new file mode 100644
index 0000000000..5066bdfc8f
--- /dev/null
+++ b/chatbot-ui-main/components/sidebar/items/all/sidebar-delete-item.tsx
@@ -0,0 +1,142 @@
+import { Button } from "@/components/ui/button"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger
+} from "@/components/ui/dialog"
+import { ChatbotUIContext } from "@/context/context"
+import { deleteAssistant } from "@/db/assistants"
+import { deleteChat } from "@/db/chats"
+import { deleteCollection } from "@/db/collections"
+import { deleteFile } from "@/db/files"
+import { deleteModel } from "@/db/models"
+import { deletePreset } from "@/db/presets"
+import { deletePrompt } from "@/db/prompts"
+import { deleteFileFromStorage } from "@/db/storage/files"
+import { deleteTool } from "@/db/tools"
+import { Tables } from "@/supabase/types"
+import { ContentType, DataItemType } from "@/types"
+import { FC, useContext, useRef, useState } from "react"
+
+interface SidebarDeleteItemProps {
+ item: DataItemType
+ contentType: ContentType
+}
+
+export const SidebarDeleteItem: FC = ({
+ item,
+ contentType
+}) => {
+ const {
+ setChats,
+ setPresets,
+ setPrompts,
+ setFiles,
+ setCollections,
+ setAssistants,
+ setTools,
+ setModels
+ } = useContext(ChatbotUIContext)
+
+ const buttonRef = useRef(null)
+
+ const [showDialog, setShowDialog] = useState(false)
+
+ const deleteFunctions = {
+ chats: async (chat: Tables<"chats">) => {
+ await deleteChat(chat.id)
+ },
+ presets: async (preset: Tables<"presets">) => {
+ await deletePreset(preset.id)
+ },
+ prompts: async (prompt: Tables<"prompts">) => {
+ await deletePrompt(prompt.id)
+ },
+ files: async (file: Tables<"files">) => {
+ await deleteFileFromStorage(file.file_path)
+ await deleteFile(file.id)
+ },
+ collections: async (collection: Tables<"collections">) => {
+ await deleteCollection(collection.id)
+ },
+ assistants: async (assistant: Tables<"assistants">) => {
+ await deleteAssistant(assistant.id)
+ setChats(prevState =>
+ prevState.filter(chat => chat.assistant_id !== assistant.id)
+ )
+ },
+ tools: async (tool: Tables<"tools">) => {
+ await deleteTool(tool.id)
+ },
+ models: async (model: Tables<"models">) => {
+ await deleteModel(model.id)
+ }
+ }
+
+ const stateUpdateFunctions = {
+ chats: setChats,
+ presets: setPresets,
+ prompts: setPrompts,
+ files: setFiles,
+ collections: setCollections,
+ assistants: setAssistants,
+ tools: setTools,
+ models: setModels
+ }
+
+ const handleDelete = async () => {
+ const deleteFunction = deleteFunctions[contentType]
+ const setStateFunction = stateUpdateFunctions[contentType]
+
+ if (!deleteFunction || !setStateFunction) return
+
+ await deleteFunction(item as any)
+
+ setStateFunction((prevItems: any) =>
+ prevItems.filter((prevItem: any) => prevItem.id !== item.id)
+ )
+
+ setShowDialog(false)
+ }
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") {
+ e.stopPropagation()
+ buttonRef.current?.click()
+ }
+ }
+
+ return (
+
+
+
+ Delete
+
+
+
+
+
+ Delete {contentType.slice(0, -1)}
+
+
+ Are you sure you want to delete {item.name}?
+
+
+
+
+ setShowDialog(false)}>
+ Cancel
+
+
+
+ Delete
+
+
+
+
+ )
+}
diff --git a/chatbot-ui-main/components/sidebar/items/all/sidebar-display-item.tsx b/chatbot-ui-main/components/sidebar/items/all/sidebar-display-item.tsx
new file mode 100644
index 0000000000..d8f0864036
--- /dev/null
+++ b/chatbot-ui-main/components/sidebar/items/all/sidebar-display-item.tsx
@@ -0,0 +1,127 @@
+import { ChatbotUIContext } from "@/context/context"
+import { createChat } from "@/db/chats"
+import { cn } from "@/lib/utils"
+import { Tables } from "@/supabase/types"
+import { ContentType, DataItemType } from "@/types"
+import { useRouter } from "next/navigation"
+import { FC, useContext, useRef, useState } from "react"
+import { SidebarUpdateItem } from "./sidebar-update-item"
+
+interface SidebarItemProps {
+ item: DataItemType
+ isTyping: boolean
+ contentType: ContentType
+ icon: React.ReactNode
+ updateState: any
+ renderInputs: (renderState: any) => JSX.Element
+}
+
+export const SidebarItem: FC = ({
+ item,
+ contentType,
+ updateState,
+ renderInputs,
+ icon,
+ isTyping
+}) => {
+ const { selectedWorkspace, setChats, setSelectedAssistant } =
+ useContext(ChatbotUIContext)
+
+ const router = useRouter()
+
+ const itemRef = useRef(null)
+
+ const [isHovering, setIsHovering] = useState(false)
+
+ const actionMap = {
+ chats: async (item: any) => {},
+ presets: async (item: any) => {},
+ prompts: async (item: any) => {},
+ files: async (item: any) => {},
+ collections: async (item: any) => {},
+ assistants: async (assistant: Tables<"assistants">) => {
+ if (!selectedWorkspace) return
+
+ const createdChat = await createChat({
+ user_id: assistant.user_id,
+ workspace_id: selectedWorkspace.id,
+ assistant_id: assistant.id,
+ context_length: assistant.context_length,
+ include_profile_context: assistant.include_profile_context,
+ include_workspace_instructions:
+ assistant.include_workspace_instructions,
+ model: assistant.model,
+ name: `Chat with ${assistant.name}`,
+ prompt: assistant.prompt,
+ temperature: assistant.temperature,
+ embeddings_provider: assistant.embeddings_provider
+ })
+
+ setChats(prevState => [createdChat, ...prevState])
+ setSelectedAssistant(assistant)
+
+ return router.push(`/${selectedWorkspace.id}/chat/${createdChat.id}`)
+ },
+ tools: async (item: any) => {},
+ models: async (item: any) => {}
+ }
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") {
+ e.stopPropagation()
+ itemRef.current?.click()
+ }
+ }
+
+ // const handleClickAction = async (
+ // e: React.MouseEvent
+ // ) => {
+ // e.stopPropagation()
+
+ // const action = actionMap[contentType]
+
+ // await action(item as any)
+ // }
+
+ return (
+
+ setIsHovering(true)}
+ onMouseLeave={() => setIsHovering(false)}
+ >
+ {icon}
+
+
+ {item.name}
+
+
+ {/* TODO */}
+ {/* {isHovering && (
+
Start chat with {contentType.slice(0, -1)} }
+ trigger={
+
+ }
+ />
+ )} */}
+
+
+ )
+}
diff --git a/chatbot-ui-main/components/sidebar/items/all/sidebar-update-item.tsx b/chatbot-ui-main/components/sidebar/items/all/sidebar-update-item.tsx
new file mode 100644
index 0000000000..1f8b346107
--- /dev/null
+++ b/chatbot-ui-main/components/sidebar/items/all/sidebar-update-item.tsx
@@ -0,0 +1,680 @@
+import { Button } from "@/components/ui/button"
+import { Label } from "@/components/ui/label"
+import {
+ Sheet,
+ SheetContent,
+ SheetFooter,
+ SheetHeader,
+ SheetTitle,
+ SheetTrigger
+} from "@/components/ui/sheet"
+import { AssignWorkspaces } from "@/components/workspace/assign-workspaces"
+import { ChatbotUIContext } from "@/context/context"
+import {
+ createAssistantCollection,
+ deleteAssistantCollection,
+ getAssistantCollectionsByAssistantId
+} from "@/db/assistant-collections"
+import {
+ createAssistantFile,
+ deleteAssistantFile,
+ getAssistantFilesByAssistantId
+} from "@/db/assistant-files"
+import {
+ createAssistantTool,
+ deleteAssistantTool,
+ getAssistantToolsByAssistantId
+} from "@/db/assistant-tools"
+import {
+ createAssistantWorkspaces,
+ deleteAssistantWorkspace,
+ getAssistantWorkspacesByAssistantId,
+ updateAssistant
+} from "@/db/assistants"
+import { updateChat } from "@/db/chats"
+import {
+ createCollectionFile,
+ deleteCollectionFile,
+ getCollectionFilesByCollectionId
+} from "@/db/collection-files"
+import {
+ createCollectionWorkspaces,
+ deleteCollectionWorkspace,
+ getCollectionWorkspacesByCollectionId,
+ updateCollection
+} from "@/db/collections"
+import {
+ createFileWorkspaces,
+ deleteFileWorkspace,
+ getFileWorkspacesByFileId,
+ updateFile
+} from "@/db/files"
+import {
+ createModelWorkspaces,
+ deleteModelWorkspace,
+ getModelWorkspacesByModelId,
+ updateModel
+} from "@/db/models"
+import {
+ createPresetWorkspaces,
+ deletePresetWorkspace,
+ getPresetWorkspacesByPresetId,
+ updatePreset
+} from "@/db/presets"
+import {
+ createPromptWorkspaces,
+ deletePromptWorkspace,
+ getPromptWorkspacesByPromptId,
+ updatePrompt
+} from "@/db/prompts"
+import {
+ getAssistantImageFromStorage,
+ uploadAssistantImage
+} from "@/db/storage/assistant-images"
+import {
+ createToolWorkspaces,
+ deleteToolWorkspace,
+ getToolWorkspacesByToolId,
+ updateTool
+} from "@/db/tools"
+import { convertBlobToBase64 } from "@/lib/blob-to-b64"
+import { Tables, TablesUpdate } from "@/supabase/types"
+import { CollectionFile, ContentType, DataItemType } from "@/types"
+import { FC, useContext, useEffect, useRef, useState } from "react"
+import profile from "react-syntax-highlighter/dist/esm/languages/hljs/profile"
+import { toast } from "sonner"
+import { SidebarDeleteItem } from "./sidebar-delete-item"
+
+interface SidebarUpdateItemProps {
+ isTyping: boolean
+ item: DataItemType
+ contentType: ContentType
+ children: React.ReactNode
+ renderInputs: (renderState: any) => JSX.Element
+ updateState: any
+}
+
+export const SidebarUpdateItem: FC = ({
+ item,
+ contentType,
+ children,
+ renderInputs,
+ updateState,
+ isTyping
+}) => {
+ const {
+ workspaces,
+ selectedWorkspace,
+ setChats,
+ setPresets,
+ setPrompts,
+ setFiles,
+ setCollections,
+ setAssistants,
+ setTools,
+ setModels,
+ setAssistantImages
+ } = useContext(ChatbotUIContext)
+
+ const buttonRef = useRef(null)
+
+ const [isOpen, setIsOpen] = useState(false)
+ const [startingWorkspaces, setStartingWorkspaces] = useState<
+ Tables<"workspaces">[]
+ >([])
+ const [selectedWorkspaces, setSelectedWorkspaces] = useState<
+ Tables<"workspaces">[]
+ >([])
+
+ // Collections Render State
+ const [startingCollectionFiles, setStartingCollectionFiles] = useState<
+ CollectionFile[]
+ >([])
+ const [selectedCollectionFiles, setSelectedCollectionFiles] = useState<
+ CollectionFile[]
+ >([])
+
+ // Assistants Render State
+ const [startingAssistantFiles, setStartingAssistantFiles] = useState<
+ Tables<"files">[]
+ >([])
+ const [startingAssistantCollections, setStartingAssistantCollections] =
+ useState[]>([])
+ const [startingAssistantTools, setStartingAssistantTools] = useState<
+ Tables<"tools">[]
+ >([])
+ const [selectedAssistantFiles, setSelectedAssistantFiles] = useState<
+ Tables<"files">[]
+ >([])
+ const [selectedAssistantCollections, setSelectedAssistantCollections] =
+ useState[]>([])
+ const [selectedAssistantTools, setSelectedAssistantTools] = useState<
+ Tables<"tools">[]
+ >([])
+
+ useEffect(() => {
+ if (isOpen) {
+ const fetchData = async () => {
+ if (workspaces.length > 1) {
+ const workspaces = await fetchSelectedWorkspaces()
+ setStartingWorkspaces(workspaces)
+ setSelectedWorkspaces(workspaces)
+ }
+
+ const fetchDataFunction = fetchDataFunctions[contentType]
+ if (!fetchDataFunction) return
+ await fetchDataFunction(item.id)
+ }
+
+ fetchData()
+ }
+ }, [isOpen])
+
+ const renderState = {
+ chats: null,
+ presets: null,
+ prompts: null,
+ files: null,
+ collections: {
+ startingCollectionFiles,
+ setStartingCollectionFiles,
+ selectedCollectionFiles,
+ setSelectedCollectionFiles
+ },
+ assistants: {
+ startingAssistantFiles,
+ setStartingAssistantFiles,
+ startingAssistantCollections,
+ setStartingAssistantCollections,
+ startingAssistantTools,
+ setStartingAssistantTools,
+ selectedAssistantFiles,
+ setSelectedAssistantFiles,
+ selectedAssistantCollections,
+ setSelectedAssistantCollections,
+ selectedAssistantTools,
+ setSelectedAssistantTools
+ },
+ tools: null,
+ models: null
+ }
+
+ const fetchDataFunctions = {
+ chats: null,
+ presets: null,
+ prompts: null,
+ files: null,
+ collections: async (collectionId: string) => {
+ const collectionFiles =
+ await getCollectionFilesByCollectionId(collectionId)
+ setStartingCollectionFiles(collectionFiles.files)
+ setSelectedCollectionFiles([])
+ },
+ assistants: async (assistantId: string) => {
+ const assistantFiles = await getAssistantFilesByAssistantId(assistantId)
+ setStartingAssistantFiles(assistantFiles.files)
+
+ const assistantCollections =
+ await getAssistantCollectionsByAssistantId(assistantId)
+ setStartingAssistantCollections(assistantCollections.collections)
+
+ const assistantTools = await getAssistantToolsByAssistantId(assistantId)
+ setStartingAssistantTools(assistantTools.tools)
+
+ setSelectedAssistantFiles([])
+ setSelectedAssistantCollections([])
+ setSelectedAssistantTools([])
+ },
+ tools: null,
+ models: null
+ }
+
+ const fetchWorkpaceFunctions = {
+ chats: null,
+ presets: async (presetId: string) => {
+ const item = await getPresetWorkspacesByPresetId(presetId)
+ return item.workspaces
+ },
+ prompts: async (promptId: string) => {
+ const item = await getPromptWorkspacesByPromptId(promptId)
+ return item.workspaces
+ },
+ files: async (fileId: string) => {
+ const item = await getFileWorkspacesByFileId(fileId)
+ return item.workspaces
+ },
+ collections: async (collectionId: string) => {
+ const item = await getCollectionWorkspacesByCollectionId(collectionId)
+ return item.workspaces
+ },
+ assistants: async (assistantId: string) => {
+ const item = await getAssistantWorkspacesByAssistantId(assistantId)
+ return item.workspaces
+ },
+ tools: async (toolId: string) => {
+ const item = await getToolWorkspacesByToolId(toolId)
+ return item.workspaces
+ },
+ models: async (modelId: string) => {
+ const item = await getModelWorkspacesByModelId(modelId)
+ return item.workspaces
+ }
+ }
+
+ const fetchSelectedWorkspaces = async () => {
+ const fetchFunction = fetchWorkpaceFunctions[contentType]
+
+ if (!fetchFunction) return []
+
+ const workspaces = await fetchFunction(item.id)
+
+ return workspaces
+ }
+
+ const handleWorkspaceUpdates = async (
+ startingWorkspaces: Tables<"workspaces">[],
+ selectedWorkspaces: Tables<"workspaces">[],
+ itemId: string,
+ deleteWorkspaceFn: (
+ itemId: string,
+ workspaceId: string
+ ) => Promise,
+ createWorkspaceFn: (
+ workspaces: { user_id: string; item_id: string; workspace_id: string }[]
+ ) => Promise,
+ itemIdKey: string
+ ) => {
+ if (!selectedWorkspace) return
+
+ const deleteList = startingWorkspaces.filter(
+ startingWorkspace =>
+ !selectedWorkspaces.some(
+ selectedWorkspace => selectedWorkspace.id === startingWorkspace.id
+ )
+ )
+
+ for (const workspace of deleteList) {
+ await deleteWorkspaceFn(itemId, workspace.id)
+ }
+
+ if (deleteList.map(w => w.id).includes(selectedWorkspace.id)) {
+ const setStateFunction = stateUpdateFunctions[contentType]
+
+ if (setStateFunction) {
+ setStateFunction((prevItems: any) =>
+ prevItems.filter((prevItem: any) => prevItem.id !== item.id)
+ )
+ }
+ }
+
+ const createList = selectedWorkspaces.filter(
+ selectedWorkspace =>
+ !startingWorkspaces.some(
+ startingWorkspace => startingWorkspace.id === selectedWorkspace.id
+ )
+ )
+
+ await createWorkspaceFn(
+ createList.map(workspace => {
+ return {
+ user_id: workspace.user_id,
+ [itemIdKey]: itemId,
+ workspace_id: workspace.id
+ } as any
+ })
+ )
+ }
+
+ const updateFunctions = {
+ chats: updateChat,
+ presets: async (presetId: string, updateState: TablesUpdate<"presets">) => {
+ const updatedPreset = await updatePreset(presetId, updateState)
+
+ await handleWorkspaceUpdates(
+ startingWorkspaces,
+ selectedWorkspaces,
+ presetId,
+ deletePresetWorkspace,
+ createPresetWorkspaces as any,
+ "preset_id"
+ )
+
+ return updatedPreset
+ },
+ prompts: async (promptId: string, updateState: TablesUpdate<"prompts">) => {
+ const updatedPrompt = await updatePrompt(promptId, updateState)
+
+ await handleWorkspaceUpdates(
+ startingWorkspaces,
+ selectedWorkspaces,
+ promptId,
+ deletePromptWorkspace,
+ createPromptWorkspaces as any,
+ "prompt_id"
+ )
+
+ return updatedPrompt
+ },
+ files: async (fileId: string, updateState: TablesUpdate<"files">) => {
+ const updatedFile = await updateFile(fileId, updateState)
+
+ await handleWorkspaceUpdates(
+ startingWorkspaces,
+ selectedWorkspaces,
+ fileId,
+ deleteFileWorkspace,
+ createFileWorkspaces as any,
+ "file_id"
+ )
+
+ return updatedFile
+ },
+ collections: async (
+ collectionId: string,
+ updateState: TablesUpdate<"assistants">
+ ) => {
+ if (!profile) return
+
+ const { ...rest } = updateState
+
+ const filesToAdd = selectedCollectionFiles.filter(
+ selectedFile =>
+ !startingCollectionFiles.some(
+ startingFile => startingFile.id === selectedFile.id
+ )
+ )
+
+ const filesToRemove = startingCollectionFiles.filter(startingFile =>
+ selectedCollectionFiles.some(
+ selectedFile => selectedFile.id === startingFile.id
+ )
+ )
+
+ for (const file of filesToAdd) {
+ await createCollectionFile({
+ user_id: item.user_id,
+ collection_id: collectionId,
+ file_id: file.id
+ })
+ }
+
+ for (const file of filesToRemove) {
+ await deleteCollectionFile(collectionId, file.id)
+ }
+
+ const updatedCollection = await updateCollection(collectionId, rest)
+
+ await handleWorkspaceUpdates(
+ startingWorkspaces,
+ selectedWorkspaces,
+ collectionId,
+ deleteCollectionWorkspace,
+ createCollectionWorkspaces as any,
+ "collection_id"
+ )
+
+ return updatedCollection
+ },
+ assistants: async (
+ assistantId: string,
+ updateState: {
+ assistantId: string
+ image: File
+ } & TablesUpdate<"assistants">
+ ) => {
+ const { image, ...rest } = updateState
+
+ const filesToAdd = selectedAssistantFiles.filter(
+ selectedFile =>
+ !startingAssistantFiles.some(
+ startingFile => startingFile.id === selectedFile.id
+ )
+ )
+
+ const filesToRemove = startingAssistantFiles.filter(startingFile =>
+ selectedAssistantFiles.some(
+ selectedFile => selectedFile.id === startingFile.id
+ )
+ )
+
+ for (const file of filesToAdd) {
+ await createAssistantFile({
+ user_id: item.user_id,
+ assistant_id: assistantId,
+ file_id: file.id
+ })
+ }
+
+ for (const file of filesToRemove) {
+ await deleteAssistantFile(assistantId, file.id)
+ }
+
+ const collectionsToAdd = selectedAssistantCollections.filter(
+ selectedCollection =>
+ !startingAssistantCollections.some(
+ startingCollection =>
+ startingCollection.id === selectedCollection.id
+ )
+ )
+
+ const collectionsToRemove = startingAssistantCollections.filter(
+ startingCollection =>
+ selectedAssistantCollections.some(
+ selectedCollection =>
+ selectedCollection.id === startingCollection.id
+ )
+ )
+
+ for (const collection of collectionsToAdd) {
+ await createAssistantCollection({
+ user_id: item.user_id,
+ assistant_id: assistantId,
+ collection_id: collection.id
+ })
+ }
+
+ for (const collection of collectionsToRemove) {
+ await deleteAssistantCollection(assistantId, collection.id)
+ }
+
+ const toolsToAdd = selectedAssistantTools.filter(
+ selectedTool =>
+ !startingAssistantTools.some(
+ startingTool => startingTool.id === selectedTool.id
+ )
+ )
+
+ const toolsToRemove = startingAssistantTools.filter(startingTool =>
+ selectedAssistantTools.some(
+ selectedTool => selectedTool.id === startingTool.id
+ )
+ )
+
+ for (const tool of toolsToAdd) {
+ await createAssistantTool({
+ user_id: item.user_id,
+ assistant_id: assistantId,
+ tool_id: tool.id
+ })
+ }
+
+ for (const tool of toolsToRemove) {
+ await deleteAssistantTool(assistantId, tool.id)
+ }
+
+ let updatedAssistant = await updateAssistant(assistantId, rest)
+
+ if (image) {
+ const filePath = await uploadAssistantImage(updatedAssistant, image)
+
+ updatedAssistant = await updateAssistant(assistantId, {
+ image_path: filePath
+ })
+
+ const url = (await getAssistantImageFromStorage(filePath)) || ""
+
+ if (url) {
+ const response = await fetch(url)
+ const blob = await response.blob()
+ const base64 = await convertBlobToBase64(blob)
+
+ setAssistantImages(prev => [
+ ...prev,
+ {
+ assistantId: updatedAssistant.id,
+ path: filePath,
+ base64,
+ url
+ }
+ ])
+ }
+ }
+
+ await handleWorkspaceUpdates(
+ startingWorkspaces,
+ selectedWorkspaces,
+ assistantId,
+ deleteAssistantWorkspace,
+ createAssistantWorkspaces as any,
+ "assistant_id"
+ )
+
+ return updatedAssistant
+ },
+ tools: async (toolId: string, updateState: TablesUpdate<"tools">) => {
+ const updatedTool = await updateTool(toolId, updateState)
+
+ await handleWorkspaceUpdates(
+ startingWorkspaces,
+ selectedWorkspaces,
+ toolId,
+ deleteToolWorkspace,
+ createToolWorkspaces as any,
+ "tool_id"
+ )
+
+ return updatedTool
+ },
+ models: async (modelId: string, updateState: TablesUpdate<"models">) => {
+ const updatedModel = await updateModel(modelId, updateState)
+
+ await handleWorkspaceUpdates(
+ startingWorkspaces,
+ selectedWorkspaces,
+ modelId,
+ deleteModelWorkspace,
+ createModelWorkspaces as any,
+ "model_id"
+ )
+
+ return updatedModel
+ }
+ }
+
+ const stateUpdateFunctions = {
+ chats: setChats,
+ presets: setPresets,
+ prompts: setPrompts,
+ files: setFiles,
+ collections: setCollections,
+ assistants: setAssistants,
+ tools: setTools,
+ models: setModels
+ }
+
+ const handleUpdate = async () => {
+ try {
+ const updateFunction = updateFunctions[contentType]
+ const setStateFunction = stateUpdateFunctions[contentType]
+
+ if (!updateFunction || !setStateFunction) return
+ if (isTyping) return // Prevent update while typing
+
+ const updatedItem = await updateFunction(item.id, updateState)
+
+ setStateFunction((prevItems: any) =>
+ prevItems.map((prevItem: any) =>
+ prevItem.id === item.id ? updatedItem : prevItem
+ )
+ )
+
+ setIsOpen(false)
+
+ toast.success(`${contentType.slice(0, -1)} updated successfully`)
+ } catch (error) {
+ toast.error(`Error updating ${contentType.slice(0, -1)}. ${error}`)
+ }
+ }
+
+ const handleSelectWorkspace = (workspace: Tables<"workspaces">) => {
+ setSelectedWorkspaces(prevState => {
+ const isWorkspaceAlreadySelected = prevState.find(
+ selectedWorkspace => selectedWorkspace.id === workspace.id
+ )
+
+ if (isWorkspaceAlreadySelected) {
+ return prevState.filter(
+ selectedWorkspace => selectedWorkspace.id !== workspace.id
+ )
+ } else {
+ return [...prevState, workspace]
+ }
+ })
+ }
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (!isTyping && e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault()
+ buttonRef.current?.click()
+ }
+ }
+
+ return (
+
+ {children}
+
+
+
+
+
+ Edit {contentType.slice(0, -1)}
+
+
+
+
+ {workspaces.length > 1 && (
+
+
Assigned Workspaces
+
+
+
+ )}
+
+ {renderInputs(renderState[contentType])}
+
+
+
+
+
+
+
+ setIsOpen(false)}>
+ Cancel
+
+
+
+ Save
+
+
+
+
+
+ )
+}
diff --git a/chatbot-ui-main/components/sidebar/items/assistants/assistant-item.tsx b/chatbot-ui-main/components/sidebar/items/assistants/assistant-item.tsx
new file mode 100644
index 0000000000..de807fdc98
--- /dev/null
+++ b/chatbot-ui-main/components/sidebar/items/assistants/assistant-item.tsx
@@ -0,0 +1,303 @@
+import { ChatSettingsForm } from "@/components/ui/chat-settings-form"
+import ImagePicker from "@/components/ui/image-picker"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { ChatbotUIContext } from "@/context/context"
+import { ASSISTANT_DESCRIPTION_MAX, ASSISTANT_NAME_MAX } from "@/db/limits"
+import { Tables } from "@/supabase/types"
+import { IconRobotFace } from "@tabler/icons-react"
+import Image from "next/image"
+import { FC, useContext, useEffect, useState } from "react"
+import profile from "react-syntax-highlighter/dist/esm/languages/hljs/profile"
+import { SidebarItem } from "../all/sidebar-display-item"
+import { AssistantRetrievalSelect } from "./assistant-retrieval-select"
+import { AssistantToolSelect } from "./assistant-tool-select"
+
+interface AssistantItemProps {
+ assistant: Tables<"assistants">
+}
+
+export const AssistantItem: FC = ({ assistant }) => {
+ const { selectedWorkspace, assistantImages } = useContext(ChatbotUIContext)
+
+ const [name, setName] = useState(assistant.name)
+ const [isTyping, setIsTyping] = useState(false)
+ const [description, setDescription] = useState(assistant.description)
+ const [assistantChatSettings, setAssistantChatSettings] = useState({
+ model: assistant.model,
+ prompt: assistant.prompt,
+ temperature: assistant.temperature,
+ contextLength: assistant.context_length,
+ includeProfileContext: assistant.include_profile_context,
+ includeWorkspaceInstructions: assistant.include_workspace_instructions
+ })
+ const [selectedImage, setSelectedImage] = useState(null)
+ const [imageLink, setImageLink] = useState("")
+
+ useEffect(() => {
+ const assistantImage =
+ assistantImages.find(image => image.path === assistant.image_path)
+ ?.base64 || ""
+ setImageLink(assistantImage)
+ }, [assistant, assistantImages])
+
+ const handleFileSelect = (
+ file: Tables<"files">,
+ setSelectedAssistantFiles: React.Dispatch<
+ React.SetStateAction[]>
+ >
+ ) => {
+ setSelectedAssistantFiles(prevState => {
+ const isFileAlreadySelected = prevState.find(
+ selectedFile => selectedFile.id === file.id
+ )
+
+ if (isFileAlreadySelected) {
+ return prevState.filter(selectedFile => selectedFile.id !== file.id)
+ } else {
+ return [...prevState, file]
+ }
+ })
+ }
+
+ const handleCollectionSelect = (
+ collection: Tables<"collections">,
+ setSelectedAssistantCollections: React.Dispatch<
+ React.SetStateAction[]>
+ >
+ ) => {
+ setSelectedAssistantCollections(prevState => {
+ const isCollectionAlreadySelected = prevState.find(
+ selectedCollection => selectedCollection.id === collection.id
+ )
+
+ if (isCollectionAlreadySelected) {
+ return prevState.filter(
+ selectedCollection => selectedCollection.id !== collection.id
+ )
+ } else {
+ return [...prevState, collection]
+ }
+ })
+ }
+
+ const handleToolSelect = (
+ tool: Tables<"tools">,
+ setSelectedAssistantTools: React.Dispatch<
+ React.SetStateAction[]>
+ >
+ ) => {
+ setSelectedAssistantTools(prevState => {
+ const isToolAlreadySelected = prevState.find(
+ selectedTool => selectedTool.id === tool.id
+ )
+
+ if (isToolAlreadySelected) {
+ return prevState.filter(selectedTool => selectedTool.id !== tool.id)
+ } else {
+ return [...prevState, tool]
+ }
+ })
+ }
+
+ if (!profile) return null
+ if (!selectedWorkspace) return null
+
+ return (
+
+ ) : (
+
+ )
+ }
+ updateState={{
+ image: selectedImage,
+ user_id: assistant.user_id,
+ name,
+ description,
+ include_profile_context: assistantChatSettings.includeProfileContext,
+ include_workspace_instructions:
+ assistantChatSettings.includeWorkspaceInstructions,
+ context_length: assistantChatSettings.contextLength,
+ model: assistantChatSettings.model,
+ image_path: assistant.image_path,
+ prompt: assistantChatSettings.prompt,
+ temperature: assistantChatSettings.temperature
+ }}
+ renderInputs={(renderState: {
+ startingAssistantFiles: Tables<"files">[]
+ setStartingAssistantFiles: React.Dispatch<
+ React.SetStateAction[]>
+ >
+ selectedAssistantFiles: Tables<"files">[]
+ setSelectedAssistantFiles: React.Dispatch<
+ React.SetStateAction[]>
+ >
+ startingAssistantCollections: Tables<"collections">[]
+ setStartingAssistantCollections: React.Dispatch<
+ React.SetStateAction[]>
+ >
+ selectedAssistantCollections: Tables<"collections">[]
+ setSelectedAssistantCollections: React.Dispatch<
+ React.SetStateAction[]>
+ >
+ startingAssistantTools: Tables<"tools">[]
+ setStartingAssistantTools: React.Dispatch<
+ React.SetStateAction[]>
+ >
+ selectedAssistantTools: Tables<"tools">[]
+ setSelectedAssistantTools: React.Dispatch<
+ React.SetStateAction[]>
+ >
+ }) => (
+ <>
+
+ Name
+
+ setName(e.target.value)}
+ maxLength={ASSISTANT_NAME_MAX}
+ />
+
+
+
+ Description
+
+ setDescription(e.target.value)}
+ maxLength={ASSISTANT_DESCRIPTION_MAX}
+ />
+
+
+
+ Image
+
+
+
+
+
+
+
+
Files & Collections
+
+
+ ![
+ ...renderState.selectedAssistantFiles,
+ ...renderState.selectedAssistantCollections
+ ].some(
+ selectedFile => selectedFile.id === startingFile.id
+ )
+ ),
+ ...renderState.selectedAssistantFiles.filter(
+ selectedFile =>
+ !renderState.startingAssistantFiles.some(
+ startingFile => startingFile.id === selectedFile.id
+ )
+ ),
+ ...renderState.startingAssistantCollections.filter(
+ startingCollection =>
+ ![
+ ...renderState.selectedAssistantFiles,
+ ...renderState.selectedAssistantCollections
+ ].some(
+ selectedCollection =>
+ selectedCollection.id === startingCollection.id
+ )
+ ),
+ ...renderState.selectedAssistantCollections.filter(
+ selectedCollection =>
+ !renderState.startingAssistantCollections.some(
+ startingCollection =>
+ startingCollection.id === selectedCollection.id
+ )
+ )
+ ]
+ }
+ onAssistantRetrievalItemsSelect={item =>
+ "type" in item
+ ? handleFileSelect(
+ item,
+ renderState.setSelectedAssistantFiles
+ )
+ : handleCollectionSelect(
+ item,
+ renderState.setSelectedAssistantCollections
+ )
+ }
+ />
+
+
+
+
Tools
+
+
+ !renderState.selectedAssistantTools.some(
+ selectedTool => selectedTool.id === startingTool.id
+ )
+ ),
+ ...renderState.selectedAssistantTools.filter(
+ selectedTool =>
+ !renderState.startingAssistantTools.some(
+ startingTool => startingTool.id === selectedTool.id
+ )
+ )
+ ]
+ }
+ onAssistantToolsSelect={tool =>
+ handleToolSelect(tool, renderState.setSelectedAssistantTools)
+ }
+ />
+
+ >
+ )}
+ />
+ )
+}
diff --git a/chatbot-ui-main/components/sidebar/items/assistants/assistant-retrieval-select.tsx b/chatbot-ui-main/components/sidebar/items/assistants/assistant-retrieval-select.tsx
new file mode 100644
index 0000000000..fc69de7b26
--- /dev/null
+++ b/chatbot-ui-main/components/sidebar/items/assistants/assistant-retrieval-select.tsx
@@ -0,0 +1,197 @@
+import { Button } from "@/components/ui/button"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuTrigger
+} from "@/components/ui/dropdown-menu"
+import { Input } from "@/components/ui/input"
+import { ChatbotUIContext } from "@/context/context"
+import { Tables } from "@/supabase/types"
+import {
+ IconBooks,
+ IconChevronDown,
+ IconCircleCheckFilled
+} from "@tabler/icons-react"
+import { FileIcon } from "lucide-react"
+import { FC, useContext, useEffect, useRef, useState } from "react"
+
+interface AssistantRetrievalSelectProps {
+ selectedAssistantRetrievalItems: Tables<"files">[] | Tables<"collections">[]
+ onAssistantRetrievalItemsSelect: (
+ item: Tables<"files"> | Tables<"collections">
+ ) => void
+}
+
+export const AssistantRetrievalSelect: FC = ({
+ selectedAssistantRetrievalItems,
+ onAssistantRetrievalItemsSelect
+}) => {
+ const { files, collections } = useContext(ChatbotUIContext)
+
+ const inputRef = useRef(null)
+ const triggerRef = useRef(null)
+
+ const [isOpen, setIsOpen] = useState(false)
+ const [search, setSearch] = useState("")
+
+ useEffect(() => {
+ if (isOpen) {
+ setTimeout(() => {
+ inputRef.current?.focus()
+ }, 100) // FIX: hacky
+ }
+ }, [isOpen])
+
+ const handleItemSelect = (item: Tables<"files"> | Tables<"collections">) => {
+ onAssistantRetrievalItemsSelect(item)
+ }
+
+ if (!files || !collections) return null
+
+ return (
+ {
+ setIsOpen(isOpen)
+ setSearch("")
+ }}
+ >
+
+
+
+
+ {selectedAssistantRetrievalItems.length} files selected
+
+
+
+
+
+
+
+
+ setSearch(e.target.value)}
+ onKeyDown={e => e.stopPropagation()}
+ />
+
+ {selectedAssistantRetrievalItems
+ .filter(item =>
+ item.name.toLowerCase().includes(search.toLowerCase())
+ )
+ .map(item => (
+ | Tables<"collections">}
+ selected={selectedAssistantRetrievalItems.some(
+ selectedAssistantRetrieval =>
+ selectedAssistantRetrieval.id === item.id
+ )}
+ onSelect={handleItemSelect}
+ />
+ ))}
+
+ {files
+ .filter(
+ file =>
+ !selectedAssistantRetrievalItems.some(
+ selectedAssistantRetrieval =>
+ selectedAssistantRetrieval.id === file.id
+ ) && file.name.toLowerCase().includes(search.toLowerCase())
+ )
+ .map(file => (
+
+ selectedAssistantRetrieval.id === file.id
+ )}
+ onSelect={handleItemSelect}
+ />
+ ))}
+
+ {collections
+ .filter(
+ collection =>
+ !selectedAssistantRetrievalItems.some(
+ selectedAssistantRetrieval =>
+ selectedAssistantRetrieval.id === collection.id
+ ) && collection.name.toLowerCase().includes(search.toLowerCase())
+ )
+ .map(collection => (
+
+ selectedAssistantRetrieval.id === collection.id
+ )}
+ onSelect={handleItemSelect}
+ />
+ ))}
+
+
+ )
+}
+
+interface AssistantRetrievalOptionItemProps {
+ contentType: "files" | "collections"
+ item: Tables<"files"> | Tables<"collections">
+ selected: boolean
+ onSelect: (item: Tables<"files"> | Tables<"collections">) => void
+}
+
+const AssistantRetrievalItemOption: FC = ({
+ contentType,
+ item,
+ selected,
+ onSelect
+}) => {
+ const handleSelect = () => {
+ onSelect(item)
+ }
+
+ return (
+
+
+ {contentType === "files" ? (
+
+ ).type} size={24} />
+
+ ) : (
+
+
+
+ )}
+
+
{item.name}
+
+
+ {selected && (
+
+ )}
+
+ )
+}
diff --git a/chatbot-ui-main/components/sidebar/items/assistants/assistant-tool-select.tsx b/chatbot-ui-main/components/sidebar/items/assistants/assistant-tool-select.tsx
new file mode 100644
index 0000000000..2efba48cc5
--- /dev/null
+++ b/chatbot-ui-main/components/sidebar/items/assistants/assistant-tool-select.tsx
@@ -0,0 +1,161 @@
+import { Button } from "@/components/ui/button"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuTrigger
+} from "@/components/ui/dropdown-menu"
+import { Input } from "@/components/ui/input"
+import { ChatbotUIContext } from "@/context/context"
+import { Tables } from "@/supabase/types"
+import {
+ IconBolt,
+ IconChevronDown,
+ IconCircleCheckFilled
+} from "@tabler/icons-react"
+import { FC, useContext, useEffect, useRef, useState } from "react"
+
+interface AssistantToolSelectProps {
+ selectedAssistantTools: Tables<"tools">[]
+ onAssistantToolsSelect: (tool: Tables<"tools">) => void
+}
+
+export const AssistantToolSelect: FC = ({
+ selectedAssistantTools,
+ onAssistantToolsSelect
+}) => {
+ const { tools } = useContext(ChatbotUIContext)
+
+ const inputRef = useRef(null)
+ const triggerRef = useRef(null)
+
+ const [isOpen, setIsOpen] = useState(false)
+ const [search, setSearch] = useState("")
+
+ useEffect(() => {
+ if (isOpen) {
+ setTimeout(() => {
+ inputRef.current?.focus()
+ }, 100) // FIX: hacky
+ }
+ }, [isOpen])
+
+ const handleToolSelect = (tool: Tables<"tools">) => {
+ onAssistantToolsSelect(tool)
+ }
+
+ if (!tools) return null
+
+ return (
+ {
+ setIsOpen(isOpen)
+ setSearch("")
+ }}
+ >
+
+
+
+
+ {selectedAssistantTools.length} tools selected
+
+
+
+
+
+
+
+
+ setSearch(e.target.value)}
+ onKeyDown={e => e.stopPropagation()}
+ />
+
+ {selectedAssistantTools
+ .filter(item =>
+ item.name.toLowerCase().includes(search.toLowerCase())
+ )
+ .map(tool => (
+
+ selectedAssistantRetrieval.id === tool.id
+ )}
+ onSelect={handleToolSelect}
+ />
+ ))}
+
+ {tools
+ .filter(
+ tool =>
+ !selectedAssistantTools.some(
+ selectedAssistantRetrieval =>
+ selectedAssistantRetrieval.id === tool.id
+ ) && tool.name.toLowerCase().includes(search.toLowerCase())
+ )
+ .map(tool => (
+
+ selectedAssistantRetrieval.id === tool.id
+ )}
+ onSelect={handleToolSelect}
+ />
+ ))}
+
+
+ )
+}
+
+interface AssistantToolItemProps {
+ tool: Tables<"tools">
+ selected: boolean
+ onSelect: (tool: Tables<"tools">) => void
+}
+
+const AssistantToolItem: FC = ({
+ tool,
+ selected,
+ onSelect
+}) => {
+ const handleSelect = () => {
+ onSelect(tool)
+ }
+
+ return (
+
+
+
+ {selected && (
+
+ )}
+
+ )
+}
diff --git a/chatbot-ui-main/components/sidebar/items/assistants/create-assistant.tsx b/chatbot-ui-main/components/sidebar/items/assistants/create-assistant.tsx
new file mode 100644
index 0000000000..754736661a
--- /dev/null
+++ b/chatbot-ui-main/components/sidebar/items/assistants/create-assistant.tsx
@@ -0,0 +1,211 @@
+import { SidebarCreateItem } from "@/components/sidebar/items/all/sidebar-create-item"
+import { ChatSettingsForm } from "@/components/ui/chat-settings-form"
+import ImagePicker from "@/components/ui/image-picker"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { ChatbotUIContext } from "@/context/context"
+import { ASSISTANT_DESCRIPTION_MAX, ASSISTANT_NAME_MAX } from "@/db/limits"
+import { Tables, TablesInsert } from "@/supabase/types"
+import { FC, useContext, useEffect, useState } from "react"
+import { AssistantRetrievalSelect } from "./assistant-retrieval-select"
+import { AssistantToolSelect } from "./assistant-tool-select"
+
+interface CreateAssistantProps {
+ isOpen: boolean
+ onOpenChange: (isOpen: boolean) => void
+}
+
+export const CreateAssistant: FC = ({
+ isOpen,
+ onOpenChange
+}) => {
+ const { profile, selectedWorkspace } = useContext(ChatbotUIContext)
+
+ const [name, setName] = useState("")
+ const [isTyping, setIsTyping] = useState(false)
+ const [description, setDescription] = useState("")
+ const [assistantChatSettings, setAssistantChatSettings] = useState({
+ model: selectedWorkspace?.default_model,
+ prompt: selectedWorkspace?.default_prompt,
+ temperature: selectedWorkspace?.default_temperature,
+ contextLength: selectedWorkspace?.default_context_length,
+ includeProfileContext: false,
+ includeWorkspaceInstructions: false,
+ embeddingsProvider: selectedWorkspace?.embeddings_provider
+ })
+ const [selectedImage, setSelectedImage] = useState(null)
+ const [imageLink, setImageLink] = useState("")
+ const [selectedAssistantRetrievalItems, setSelectedAssistantRetrievalItems] =
+ useState[] | Tables<"collections">[]>([])
+ const [selectedAssistantToolItems, setSelectedAssistantToolItems] = useState<
+ Tables<"tools">[]
+ >([])
+
+ useEffect(() => {
+ setAssistantChatSettings(prevSettings => {
+ const previousPrompt = prevSettings.prompt || ""
+ const previousPromptParts = previousPrompt.split(". ")
+
+ previousPromptParts[0] = name ? `You are ${name}` : ""
+
+ return {
+ ...prevSettings,
+ prompt: previousPromptParts.join(". ")
+ }
+ })
+ }, [name])
+
+ const handleRetrievalItemSelect = (
+ item: Tables<"files"> | Tables<"collections">
+ ) => {
+ setSelectedAssistantRetrievalItems(prevState => {
+ const isItemAlreadySelected = prevState.find(
+ selectedItem => selectedItem.id === item.id
+ )
+
+ if (isItemAlreadySelected) {
+ return prevState.filter(selectedItem => selectedItem.id !== item.id)
+ } else {
+ return [...prevState, item]
+ }
+ })
+ }
+
+ const handleToolSelect = (item: Tables<"tools">) => {
+ setSelectedAssistantToolItems(prevState => {
+ const isItemAlreadySelected = prevState.find(
+ selectedItem => selectedItem.id === item.id
+ )
+
+ if (isItemAlreadySelected) {
+ return prevState.filter(selectedItem => selectedItem.id !== item.id)
+ } else {
+ return [...prevState, item]
+ }
+ })
+ }
+
+ const checkIfModelIsToolCompatible = () => {
+ if (!assistantChatSettings.model) return false
+
+ const compatibleModels = [
+ "gpt-4-turbo-preview",
+ "gpt-4-vision-preview",
+ "gpt-3.5-turbo-1106",
+ "gpt-4"
+ ]
+ const isModelCompatible = compatibleModels.includes(
+ assistantChatSettings.model
+ )
+
+ return isModelCompatible
+ }
+
+ if (!profile) return null
+ if (!selectedWorkspace) return null
+
+ return (
+
+ item.hasOwnProperty("type")
+ ) as Tables<"files">[],
+ collections: selectedAssistantRetrievalItems.filter(
+ item => !item.hasOwnProperty("type")
+ ) as Tables<"collections">[],
+ tools: selectedAssistantToolItems
+ } as TablesInsert<"assistants">
+ }
+ isOpen={isOpen}
+ isTyping={isTyping}
+ renderInputs={() => (
+ <>
+
+ Name
+
+ setName(e.target.value)}
+ maxLength={ASSISTANT_NAME_MAX}
+ />
+
+
+
+ Description
+
+ setDescription(e.target.value)}
+ maxLength={ASSISTANT_DESCRIPTION_MAX}
+ />
+
+
+
+
+ Image
+
+ (optional)
+
+
+
+
+
+
+
+
+
Files & Collections
+
+
+
+
+ {checkIfModelIsToolCompatible() ? (
+
+ ) : (
+
+ Model is not compatible with tools.
+
+ )}
+ >
+ )}
+ onOpenChange={onOpenChange}
+ />
+ )
+}
diff --git a/chatbot-ui-main/components/sidebar/items/chat/chat-item.tsx b/chatbot-ui-main/components/sidebar/items/chat/chat-item.tsx
new file mode 100644
index 0000000000..62c2d98c54
--- /dev/null
+++ b/chatbot-ui-main/components/sidebar/items/chat/chat-item.tsx
@@ -0,0 +1,110 @@
+import { ModelIcon } from "@/components/models/model-icon"
+import { WithTooltip } from "@/components/ui/with-tooltip"
+import { ChatbotUIContext } from "@/context/context"
+import { LLM_LIST } from "@/lib/models/llm/llm-list"
+import { cn } from "@/lib/utils"
+import { Tables } from "@/supabase/types"
+import { LLM } from "@/types"
+import { IconRobotFace } from "@tabler/icons-react"
+import Image from "next/image"
+import { useParams, useRouter } from "next/navigation"
+import { FC, useContext, useRef } from "react"
+import { DeleteChat } from "./delete-chat"
+import { UpdateChat } from "./update-chat"
+
+interface ChatItemProps {
+ chat: Tables<"chats">
+}
+
+export const ChatItem: FC = ({ chat }) => {
+ const {
+ selectedWorkspace,
+ selectedChat,
+ availableLocalModels,
+ assistantImages,
+ availableOpenRouterModels
+ } = useContext(ChatbotUIContext)
+
+ const router = useRouter()
+ const params = useParams()
+ const isActive = params.chatid === chat.id || selectedChat?.id === chat.id
+
+ const itemRef = useRef(null)
+
+ const handleClick = () => {
+ if (!selectedWorkspace) return
+ return router.push(`/${selectedWorkspace.id}/chat/${chat.id}`)
+ }
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") {
+ e.stopPropagation()
+ itemRef.current?.click()
+ }
+ }
+
+ const MODEL_DATA = [
+ ...LLM_LIST,
+ ...availableLocalModels,
+ ...availableOpenRouterModels
+ ].find(llm => llm.modelId === chat.model) as LLM
+
+ const assistantImage = assistantImages.find(
+ image => image.assistantId === chat.assistant_id
+ )?.base64
+
+ return (
+
+ {chat.assistant_id ? (
+ assistantImage ? (
+
+ ) : (
+
+ )
+ ) : (
+ {MODEL_DATA?.modelName}
}
+ trigger={
+
+ }
+ />
+ )}
+
+
+ {chat.name}
+
+
+ {
+ e.stopPropagation()
+ e.preventDefault()
+ }}
+ className={`ml-2 flex space-x-2 ${!isActive && "w-11 opacity-0 group-hover:opacity-100"}`}
+ >
+
+
+
+
+
+ )
+}
diff --git a/chatbot-ui-main/components/sidebar/items/chat/delete-chat.tsx b/chatbot-ui-main/components/sidebar/items/chat/delete-chat.tsx
new file mode 100644
index 0000000000..868e38a34d
--- /dev/null
+++ b/chatbot-ui-main/components/sidebar/items/chat/delete-chat.tsx
@@ -0,0 +1,80 @@
+import { useChatHandler } from "@/components/chat/chat-hooks/use-chat-handler"
+import { Button } from "@/components/ui/button"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger
+} from "@/components/ui/dialog"
+import { ChatbotUIContext } from "@/context/context"
+import { deleteChat } from "@/db/chats"
+import useHotkey from "@/lib/hooks/use-hotkey"
+import { Tables } from "@/supabase/types"
+import { IconTrash } from "@tabler/icons-react"
+import { FC, useContext, useRef, useState } from "react"
+
+interface DeleteChatProps {
+ chat: Tables<"chats">
+}
+
+export const DeleteChat: FC = ({ chat }) => {
+ useHotkey("Backspace", () => setShowChatDialog(true))
+
+ const { setChats } = useContext(ChatbotUIContext)
+ const { handleNewChat } = useChatHandler()
+
+ const buttonRef = useRef(null)
+
+ const [showChatDialog, setShowChatDialog] = useState(false)
+
+ const handleDeleteChat = async () => {
+ await deleteChat(chat.id)
+
+ setChats(prevState => prevState.filter(c => c.id !== chat.id))
+
+ setShowChatDialog(false)
+
+ handleNewChat()
+ }
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") {
+ buttonRef.current?.click()
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+ Delete {chat.name}
+
+
+ Are you sure you want to delete this chat?
+
+
+
+
+ setShowChatDialog(false)}>
+ Cancel
+
+
+
+ Delete
+
+
+
+
+ )
+}
diff --git a/chatbot-ui-main/components/sidebar/items/chat/update-chat.tsx b/chatbot-ui-main/components/sidebar/items/chat/update-chat.tsx
new file mode 100644
index 0000000000..e5893009a2
--- /dev/null
+++ b/chatbot-ui-main/components/sidebar/items/chat/update-chat.tsx
@@ -0,0 +1,76 @@
+import { Button } from "@/components/ui/button"
+import {
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger
+} from "@/components/ui/dialog"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { ChatbotUIContext } from "@/context/context"
+import { updateChat } from "@/db/chats"
+import { Tables } from "@/supabase/types"
+import { IconEdit } from "@tabler/icons-react"
+import { FC, useContext, useRef, useState } from "react"
+
+interface UpdateChatProps {
+ chat: Tables<"chats">
+}
+
+export const UpdateChat: FC = ({ chat }) => {
+ const { setChats } = useContext(ChatbotUIContext)
+
+ const buttonRef = useRef(null)
+
+ const [showChatDialog, setShowChatDialog] = useState(false)
+ const [name, setName] = useState(chat.name)
+
+ const handleUpdateChat = async (e: React.MouseEvent) => {
+ const updatedChat = await updateChat(chat.id, {
+ name
+ })
+ setChats(prevState =>
+ prevState.map(c => (c.id === chat.id ? updatedChat : c))
+ )
+
+ setShowChatDialog(false)
+ }
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") {
+ buttonRef.current?.click()
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+ Edit Chat
+
+
+
+ Name
+
+ setName(e.target.value)} />
+
+
+
+ setShowChatDialog(false)}>
+ Cancel
+
+
+
+ Save
+
+
+
+
+ )
+}
diff --git a/chatbot-ui-main/components/sidebar/items/collections/collection-file-select.tsx b/chatbot-ui-main/components/sidebar/items/collections/collection-file-select.tsx
new file mode 100644
index 0000000000..7f2f0de888
--- /dev/null
+++ b/chatbot-ui-main/components/sidebar/items/collections/collection-file-select.tsx
@@ -0,0 +1,155 @@
+import { Button } from "@/components/ui/button"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuTrigger
+} from "@/components/ui/dropdown-menu"
+import { FileIcon } from "@/components/ui/file-icon"
+import { Input } from "@/components/ui/input"
+import { ChatbotUIContext } from "@/context/context"
+import { CollectionFile } from "@/types"
+import { IconChevronDown, IconCircleCheckFilled } from "@tabler/icons-react"
+import { FC, useContext, useEffect, useRef, useState } from "react"
+
+interface CollectionFileSelectProps {
+ selectedCollectionFiles: CollectionFile[]
+ onCollectionFileSelect: (file: CollectionFile) => void
+}
+
+export const CollectionFileSelect: FC = ({
+ selectedCollectionFiles,
+ onCollectionFileSelect
+}) => {
+ const { files } = useContext(ChatbotUIContext)
+
+ const inputRef = useRef(null)
+ const triggerRef = useRef(null)
+
+ const [isOpen, setIsOpen] = useState(false)
+ const [search, setSearch] = useState("")
+
+ useEffect(() => {
+ if (isOpen) {
+ setTimeout(() => {
+ inputRef.current?.focus()
+ }, 100) // FIX: hacky
+ }
+ }, [isOpen])
+
+ const handleFileSelect = (file: CollectionFile) => {
+ onCollectionFileSelect(file)
+ }
+
+ if (!files) return null
+
+ return (
+ {
+ setIsOpen(isOpen)
+ setSearch("")
+ }}
+ >
+
+
+
+
+ {selectedCollectionFiles.length} files selected
+
+
+
+
+
+
+
+
+ setSearch(e.target.value)}
+ onKeyDown={e => e.stopPropagation()}
+ />
+
+ {selectedCollectionFiles
+ .filter(file =>
+ file.name.toLowerCase().includes(search.toLowerCase())
+ )
+ .map(file => (
+ selectedCollectionFile.id === file.id
+ )}
+ onSelect={handleFileSelect}
+ />
+ ))}
+
+ {files
+ .filter(
+ file =>
+ !selectedCollectionFiles.some(
+ selectedCollectionFile => selectedCollectionFile.id === file.id
+ ) && file.name.toLowerCase().includes(search.toLowerCase())
+ )
+ .map(file => (
+ selectedCollectionFile.id === file.id
+ )}
+ onSelect={handleFileSelect}
+ />
+ ))}
+
+
+ )
+}
+
+interface CollectionFileItemProps {
+ file: CollectionFile
+ selected: boolean
+ onSelect: (file: CollectionFile) => void
+}
+
+const CollectionFileItem: FC = ({
+ file,
+ selected,
+ onSelect
+}) => {
+ const handleSelect = () => {
+ onSelect(file)
+ }
+
+ return (
+
+
+
+ {selected && (
+
+ )}
+
+ )
+}
diff --git a/chatbot-ui-main/components/sidebar/items/collections/collection-item.tsx b/chatbot-ui-main/components/sidebar/items/collections/collection-item.tsx
new file mode 100644
index 0000000000..e6b1faa4a1
--- /dev/null
+++ b/chatbot-ui-main/components/sidebar/items/collections/collection-item.tsx
@@ -0,0 +1,117 @@
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { COLLECTION_DESCRIPTION_MAX, COLLECTION_NAME_MAX } from "@/db/limits"
+import { Tables } from "@/supabase/types"
+import { CollectionFile } from "@/types"
+import { IconBooks } from "@tabler/icons-react"
+import { FC, useState } from "react"
+import { SidebarItem } from "../all/sidebar-display-item"
+import { CollectionFileSelect } from "./collection-file-select"
+
+interface CollectionItemProps {
+ collection: Tables<"collections">
+}
+
+export const CollectionItem: FC = ({ collection }) => {
+ const [name, setName] = useState(collection.name)
+ const [isTyping, setIsTyping] = useState(false)
+ const [description, setDescription] = useState(collection.description)
+
+ const handleFileSelect = (
+ file: CollectionFile,
+ setSelectedCollectionFiles: React.Dispatch<
+ React.SetStateAction
+ >
+ ) => {
+ setSelectedCollectionFiles(prevState => {
+ const isFileAlreadySelected = prevState.find(
+ selectedFile => selectedFile.id === file.id
+ )
+
+ if (isFileAlreadySelected) {
+ return prevState.filter(selectedFile => selectedFile.id !== file.id)
+ } else {
+ return [...prevState, file]
+ }
+ })
+ }
+
+ return (
+ }
+ updateState={{
+ name,
+ description
+ }}
+ renderInputs={(renderState: {
+ startingCollectionFiles: CollectionFile[]
+ setStartingCollectionFiles: React.Dispatch<
+ React.SetStateAction
+ >
+ selectedCollectionFiles: CollectionFile[]
+ setSelectedCollectionFiles: React.Dispatch<
+ React.SetStateAction
+ >
+ }) => {
+ return (
+ <>
+
+ Files
+
+
+ !renderState.selectedCollectionFiles.some(
+ selectedFile =>
+ selectedFile.id === startingFile.id
+ )
+ ),
+ ...renderState.selectedCollectionFiles.filter(
+ selectedFile =>
+ !renderState.startingCollectionFiles.some(
+ startingFile =>
+ startingFile.id === selectedFile.id
+ )
+ )
+ ]
+ }
+ onCollectionFileSelect={file =>
+ handleFileSelect(file, renderState.setSelectedCollectionFiles)
+ }
+ />
+
+
+
+ Name
+
+ setName(e.target.value)}
+ maxLength={COLLECTION_NAME_MAX}
+ />
+
+
+
+ Description
+
+ setDescription(e.target.value)}
+ maxLength={COLLECTION_DESCRIPTION_MAX}
+ />
+
+ >
+ )
+ }}
+ />
+ )
+}
diff --git a/chatbot-ui-main/components/sidebar/items/collections/create-collection.tsx b/chatbot-ui-main/components/sidebar/items/collections/create-collection.tsx
new file mode 100644
index 0000000000..d2c24370ee
--- /dev/null
+++ b/chatbot-ui-main/components/sidebar/items/collections/create-collection.tsx
@@ -0,0 +1,100 @@
+import { SidebarCreateItem } from "@/components/sidebar/items/all/sidebar-create-item"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { ChatbotUIContext } from "@/context/context"
+import { COLLECTION_DESCRIPTION_MAX, COLLECTION_NAME_MAX } from "@/db/limits"
+import { TablesInsert } from "@/supabase/types"
+import { CollectionFile } from "@/types"
+import { FC, useContext, useState } from "react"
+import { CollectionFileSelect } from "./collection-file-select"
+
+interface CreateCollectionProps {
+ isOpen: boolean
+ onOpenChange: (isOpen: boolean) => void
+}
+
+export const CreateCollection: FC = ({
+ isOpen,
+ onOpenChange
+}) => {
+ const { profile, selectedWorkspace } = useContext(ChatbotUIContext)
+
+ const [name, setName] = useState("")
+ const [isTyping, setIsTyping] = useState(false)
+ const [description, setDescription] = useState("")
+ const [selectedCollectionFiles, setSelectedCollectionFiles] = useState<
+ CollectionFile[]
+ >([])
+
+ const handleFileSelect = (file: CollectionFile) => {
+ setSelectedCollectionFiles(prevState => {
+ const isFileAlreadySelected = prevState.find(
+ selectedFile => selectedFile.id === file.id
+ )
+
+ if (isFileAlreadySelected) {
+ return prevState.filter(selectedFile => selectedFile.id !== file.id)
+ } else {
+ return [...prevState, file]
+ }
+ })
+ }
+
+ if (!profile) return null
+ if (!selectedWorkspace) return null
+
+ return (
+ ({
+ user_id: profile.user_id,
+ collection_id: "",
+ file_id: file.id
+ })),
+ user_id: profile.user_id,
+ name,
+ description
+ } as TablesInsert<"collections">
+ }
+ isOpen={isOpen}
+ isTyping={isTyping}
+ onOpenChange={onOpenChange}
+ renderInputs={() => (
+ <>
+
+ Files
+
+
+
+
+
+ Name
+
+ setName(e.target.value)}
+ maxLength={COLLECTION_NAME_MAX}
+ />
+
+
+
+ Description
+
+ setDescription(e.target.value)}
+ maxLength={COLLECTION_DESCRIPTION_MAX}
+ />
+
+ >
+ )}
+ />
+ )
+}
diff --git a/chatbot-ui-main/components/sidebar/items/files/create-file.tsx b/chatbot-ui-main/components/sidebar/items/files/create-file.tsx
new file mode 100644
index 0000000000..20220d3bce
--- /dev/null
+++ b/chatbot-ui-main/components/sidebar/items/files/create-file.tsx
@@ -0,0 +1,93 @@
+import { ACCEPTED_FILE_TYPES } from "@/components/chat/chat-hooks/use-select-file-handler"
+import { SidebarCreateItem } from "@/components/sidebar/items/all/sidebar-create-item"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { ChatbotUIContext } from "@/context/context"
+import { FILE_DESCRIPTION_MAX, FILE_NAME_MAX } from "@/db/limits"
+import { TablesInsert } from "@/supabase/types"
+import { FC, useContext, useState } from "react"
+
+interface CreateFileProps {
+ isOpen: boolean
+ onOpenChange: (isOpen: boolean) => void
+}
+
+export const CreateFile: FC = ({ isOpen, onOpenChange }) => {
+ const { profile, selectedWorkspace } = useContext(ChatbotUIContext)
+
+ const [name, setName] = useState("")
+ const [isTyping, setIsTyping] = useState(false)
+ const [description, setDescription] = useState("")
+ const [selectedFile, setSelectedFile] = useState(null)
+
+ const handleSelectedFile = async (e: React.ChangeEvent) => {
+ if (!e.target.files) return
+
+ const file = e.target.files[0]
+
+ if (!file) return
+
+ setSelectedFile(file)
+ const fileNameWithoutExtension = file.name.split(".").slice(0, -1).join(".")
+ setName(fileNameWithoutExtension)
+ }
+
+ if (!profile) return null
+ if (!selectedWorkspace) return null
+
+ return (
+
+ }
+ isOpen={isOpen}
+ isTyping={isTyping}
+ onOpenChange={onOpenChange}
+ renderInputs={() => (
+ <>
+
+ File
+
+
+
+
+
+ Name
+
+ setName(e.target.value)}
+ maxLength={FILE_NAME_MAX}
+ />
+
+
+
+ Description
+
+ setDescription(e.target.value)}
+ maxLength={FILE_DESCRIPTION_MAX}
+ />
+
+ >
+ )}
+ />
+ )
+}
diff --git a/chatbot-ui-main/components/sidebar/items/files/file-item.tsx b/chatbot-ui-main/components/sidebar/items/files/file-item.tsx
new file mode 100644
index 0000000000..7941e2e7b3
--- /dev/null
+++ b/chatbot-ui-main/components/sidebar/items/files/file-item.tsx
@@ -0,0 +1,95 @@
+import { FileIcon } from "@/components/ui/file-icon"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { FILE_DESCRIPTION_MAX, FILE_NAME_MAX } from "@/db/limits"
+import { getFileFromStorage } from "@/db/storage/files"
+import { Tables } from "@/supabase/types"
+import { FC, useState } from "react"
+import { SidebarItem } from "../all/sidebar-display-item"
+
+interface FileItemProps {
+ file: Tables<"files">
+}
+
+export const FileItem: FC = ({ file }) => {
+ const [name, setName] = useState(file.name)
+ const [isTyping, setIsTyping] = useState(false)
+ const [description, setDescription] = useState(file.description)
+
+ const getLinkAndView = async () => {
+ const link = await getFileFromStorage(file.file_path)
+ window.open(link, "_blank")
+ }
+
+ return (
+ }
+ updateState={{ name, description }}
+ renderInputs={() => (
+ <>
+
+ View {file.name}
+
+
+
+
{file.type}
+
+
{formatFileSize(file.size)}
+
+
{file.tokens.toLocaleString()} tokens
+
+
+
+ Name
+
+ setName(e.target.value)}
+ maxLength={FILE_NAME_MAX}
+ />
+
+
+
+ Description
+
+ setDescription(e.target.value)}
+ maxLength={FILE_DESCRIPTION_MAX}
+ />
+
+ >
+ )}
+ />
+ )
+}
+
+export const formatFileSize = (sizeInBytes: number): string => {
+ let size = sizeInBytes
+ let unit = "bytes"
+
+ if (size >= 1024) {
+ size /= 1024
+ unit = "KB"
+ }
+
+ if (size >= 1024) {
+ size /= 1024
+ unit = "MB"
+ }
+
+ if (size >= 1024) {
+ size /= 1024
+ unit = "GB"
+ }
+
+ return `${size.toFixed(2)} ${unit}`
+}
diff --git a/chatbot-ui-main/components/sidebar/items/folders/delete-folder.tsx b/chatbot-ui-main/components/sidebar/items/folders/delete-folder.tsx
new file mode 100644
index 0000000000..8a6fb0fae3
--- /dev/null
+++ b/chatbot-ui-main/components/sidebar/items/folders/delete-folder.tsx
@@ -0,0 +1,141 @@
+import { Button } from "@/components/ui/button"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger
+} from "@/components/ui/dialog"
+import { ChatbotUIContext } from "@/context/context"
+import { deleteFolder } from "@/db/folders"
+import { supabase } from "@/lib/supabase/browser-client"
+import { Tables } from "@/supabase/types"
+import { ContentType } from "@/types"
+import { IconTrash } from "@tabler/icons-react"
+import { FC, useContext, useRef, useState } from "react"
+import { toast } from "sonner"
+
+interface DeleteFolderProps {
+ folder: Tables<"folders">
+ contentType: ContentType
+}
+
+export const DeleteFolder: FC = ({
+ folder,
+ contentType
+}) => {
+ const {
+ setChats,
+ setFolders,
+ setPresets,
+ setPrompts,
+ setFiles,
+ setCollections,
+ setAssistants,
+ setTools,
+ setModels
+ } = useContext(ChatbotUIContext)
+
+ const buttonRef = useRef(null)
+
+ const [showFolderDialog, setShowFolderDialog] = useState(false)
+
+ const stateUpdateFunctions = {
+ chats: setChats,
+ presets: setPresets,
+ prompts: setPrompts,
+ files: setFiles,
+ collections: setCollections,
+ assistants: setAssistants,
+ tools: setTools,
+ models: setModels
+ }
+
+ const handleDeleteFolderOnly = async () => {
+ await deleteFolder(folder.id)
+
+ setFolders(prevState => prevState.filter(c => c.id !== folder.id))
+
+ setShowFolderDialog(false)
+
+ const setStateFunction = stateUpdateFunctions[contentType]
+
+ if (!setStateFunction) return
+
+ setStateFunction((prevItems: any) =>
+ prevItems.map((item: any) => {
+ if (item.folder_id === folder.id) {
+ return {
+ ...item,
+ folder_id: null
+ }
+ }
+
+ return item
+ })
+ )
+ }
+
+ const handleDeleteFolderAndItems = async () => {
+ const setStateFunction = stateUpdateFunctions[contentType]
+
+ if (!setStateFunction) return
+
+ const { error } = await supabase
+ .from(contentType)
+ .delete()
+ .eq("folder_id", folder.id)
+
+ if (error) {
+ toast.error(error.message)
+ }
+
+ setStateFunction((prevItems: any) =>
+ prevItems.filter((item: any) => item.folder_id !== folder.id)
+ )
+
+ handleDeleteFolderOnly()
+ }
+
+ return (
+
+
+
+
+
+
+
+ Delete {folder.name}
+
+
+ Are you sure you want to delete this folder?
+
+
+
+
+ setShowFolderDialog(false)}>
+ Cancel
+
+
+
+ Delete Folder & Included Items
+
+
+
+ Delete Folder Only
+
+
+
+
+ )
+}
diff --git a/chatbot-ui-main/components/sidebar/items/folders/folder-item.tsx b/chatbot-ui-main/components/sidebar/items/folders/folder-item.tsx
new file mode 100644
index 0000000000..6768f21d44
--- /dev/null
+++ b/chatbot-ui-main/components/sidebar/items/folders/folder-item.tsx
@@ -0,0 +1,114 @@
+import { cn } from "@/lib/utils"
+import { Tables } from "@/supabase/types"
+import { ContentType } from "@/types"
+import { IconChevronDown, IconChevronRight } from "@tabler/icons-react"
+import { FC, useRef, useState } from "react"
+import { DeleteFolder } from "./delete-folder"
+import { UpdateFolder } from "./update-folder"
+
+interface FolderProps {
+ folder: Tables<"folders">
+ contentType: ContentType
+ children: React.ReactNode
+ onUpdateFolder: (itemId: string, folderId: string | null) => void
+}
+
+export const Folder: FC = ({
+ folder,
+ contentType,
+ children,
+ onUpdateFolder
+}) => {
+ const itemRef = useRef(null)
+
+ const [isDragOver, setIsDragOver] = useState(false)
+ const [isExpanded, setIsExpanded] = useState(false)
+ const [isHovering, setIsHovering] = useState(false)
+
+ const handleDragEnter = (e: React.DragEvent) => {
+ e.preventDefault()
+ setIsDragOver(true)
+ }
+
+ const handleDragLeave = (e: React.DragEvent) => {
+ e.preventDefault()
+ setIsDragOver(false)
+ }
+
+ const handleDragOver = (e: React.DragEvent) => {
+ e.preventDefault()
+ setIsDragOver(true)
+ }
+
+ const handleDrop = (e: React.DragEvent) => {
+ e.preventDefault()
+
+ setIsDragOver(false)
+ const itemId = e.dataTransfer.getData("text/plain")
+ onUpdateFolder(itemId, folder.id)
+ }
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") {
+ e.stopPropagation()
+ itemRef.current?.click()
+ }
+ }
+
+ const handleClick = (e: React.MouseEvent) => {
+ setIsExpanded(!isExpanded)
+ }
+
+ return (
+ setIsHovering(true)}
+ onMouseLeave={() => setIsHovering(false)}
+ >
+
+
+
+ {isExpanded ? (
+
+ ) : (
+
+ )}
+
+
{folder.name}
+
+
+ {isHovering && (
+
{
+ e.stopPropagation()
+ e.preventDefault()
+ }}
+ className="ml-2 flex space-x-2"
+ >
+
+
+
+
+ )}
+
+
+
+ {isExpanded && (
+
{children}
+ )}
+
+ )
+}
diff --git a/chatbot-ui-main/components/sidebar/items/folders/update-folder.tsx b/chatbot-ui-main/components/sidebar/items/folders/update-folder.tsx
new file mode 100644
index 0000000000..d98833e2f7
--- /dev/null
+++ b/chatbot-ui-main/components/sidebar/items/folders/update-folder.tsx
@@ -0,0 +1,76 @@
+import { Button } from "@/components/ui/button"
+import {
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger
+} from "@/components/ui/dialog"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { ChatbotUIContext } from "@/context/context"
+import { updateFolder } from "@/db/folders"
+import { Tables } from "@/supabase/types"
+import { IconEdit } from "@tabler/icons-react"
+import { FC, useContext, useRef, useState } from "react"
+
+interface UpdateFolderProps {
+ folder: Tables<"folders">
+}
+
+export const UpdateFolder: FC = ({ folder }) => {
+ const { setFolders } = useContext(ChatbotUIContext)
+
+ const buttonRef = useRef(null)
+
+ const [showFolderDialog, setShowFolderDialog] = useState(false)
+ const [name, setName] = useState(folder.name)
+
+ const handleUpdateFolder = async (e: React.MouseEvent) => {
+ const updatedFolder = await updateFolder(folder.id, {
+ name
+ })
+ setFolders(prevState =>
+ prevState.map(c => (c.id === folder.id ? updatedFolder : c))
+ )
+
+ setShowFolderDialog(false)
+ }
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") {
+ buttonRef.current?.click()
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+ Edit Folder
+
+
+
+ Name
+
+ setName(e.target.value)} />
+
+
+
+ setShowFolderDialog(false)}>
+ Cancel
+
+
+
+ Save
+
+
+
+
+ )
+}
diff --git a/chatbot-ui-main/components/sidebar/items/models/create-model.tsx b/chatbot-ui-main/components/sidebar/items/models/create-model.tsx
new file mode 100644
index 0000000000..c50ea63dc3
--- /dev/null
+++ b/chatbot-ui-main/components/sidebar/items/models/create-model.tsx
@@ -0,0 +1,117 @@
+import { SidebarCreateItem } from "@/components/sidebar/items/all/sidebar-create-item"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { ChatbotUIContext } from "@/context/context"
+import { MODEL_NAME_MAX } from "@/db/limits"
+import { TablesInsert } from "@/supabase/types"
+import { FC, useContext, useState } from "react"
+
+interface CreateModelProps {
+ isOpen: boolean
+ onOpenChange: (isOpen: boolean) => void
+}
+
+export const CreateModel: FC = ({ isOpen, onOpenChange }) => {
+ const { profile, selectedWorkspace } = useContext(ChatbotUIContext)
+
+ const [isTyping, setIsTyping] = useState(false)
+
+ const [apiKey, setApiKey] = useState("")
+ const [baseUrl, setBaseUrl] = useState("")
+ const [description, setDescription] = useState("")
+ const [modelId, setModelId] = useState("")
+ const [name, setName] = useState("")
+ const [contextLength, setContextLength] = useState(4096)
+
+ if (!profile || !selectedWorkspace) return null
+
+ return (
+
+ }
+ renderInputs={() => (
+ <>
+
+
Create a custom model.
+
+
+ Your API *must* be compatible
+ with the OpenAI SDK.
+
+
+
+
+ Name
+
+ setName(e.target.value)}
+ maxLength={MODEL_NAME_MAX}
+ />
+
+
+
+ Model ID
+
+ setModelId(e.target.value)}
+ />
+
+
+
+
Base URL
+
+
setBaseUrl(e.target.value)}
+ />
+
+
+ Your API must be compatible with the OpenAI SDK.
+
+
+
+
+ API Key
+
+ setApiKey(e.target.value)}
+ />
+
+
+
+ Max Context Length
+
+ setContextLength(parseInt(e.target.value))}
+ />
+
+ >
+ )}
+ />
+ )
+}
diff --git a/chatbot-ui-main/components/sidebar/items/models/model-item.tsx b/chatbot-ui-main/components/sidebar/items/models/model-item.tsx
new file mode 100644
index 0000000000..b6f6f74d1b
--- /dev/null
+++ b/chatbot-ui-main/components/sidebar/items/models/model-item.tsx
@@ -0,0 +1,102 @@
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { MODEL_NAME_MAX } from "@/db/limits"
+import { Tables, TablesUpdate } from "@/supabase/types"
+import { IconSparkles } from "@tabler/icons-react"
+import { FC, useState } from "react"
+import { SidebarItem } from "../all/sidebar-display-item"
+
+interface ModelItemProps {
+ model: Tables<"models">
+}
+
+export const ModelItem: FC = ({ model }) => {
+ const [isTyping, setIsTyping] = useState(false)
+
+ const [apiKey, setApiKey] = useState(model.api_key)
+ const [baseUrl, setBaseUrl] = useState(model.base_url)
+ const [description, setDescription] = useState(model.description)
+ const [modelId, setModelId] = useState(model.model_id)
+ const [name, setName] = useState(model.name)
+ const [contextLength, setContextLength] = useState(model.context_length)
+
+ return (
+ }
+ updateState={
+ {
+ api_key: apiKey,
+ base_url: baseUrl,
+ description,
+ context_length: contextLength,
+ model_id: modelId,
+ name
+ } as TablesUpdate<"models">
+ }
+ renderInputs={() => (
+ <>
+
+ Name
+
+ setName(e.target.value)}
+ maxLength={MODEL_NAME_MAX}
+ />
+
+
+
+ Model ID
+
+ setModelId(e.target.value)}
+ />
+
+
+
+
Base URL
+
+
setBaseUrl(e.target.value)}
+ />
+
+
+ Your API must be compatible with the OpenAI SDK.
+
+
+
+
+ API Key
+
+ setApiKey(e.target.value)}
+ />
+
+
+
+ Max Context Length
+
+ setContextLength(parseInt(e.target.value))}
+ />
+
+ >
+ )}
+ />
+ )
+}
diff --git a/chatbot-ui-main/components/sidebar/items/presets/create-preset.tsx b/chatbot-ui-main/components/sidebar/items/presets/create-preset.tsx
new file mode 100644
index 0000000000..aa3b6c8b6b
--- /dev/null
+++ b/chatbot-ui-main/components/sidebar/items/presets/create-preset.tsx
@@ -0,0 +1,81 @@
+import { SidebarCreateItem } from "@/components/sidebar/items/all/sidebar-create-item"
+import { ChatSettingsForm } from "@/components/ui/chat-settings-form"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { ChatbotUIContext } from "@/context/context"
+import { PRESET_NAME_MAX } from "@/db/limits"
+import { TablesInsert } from "@/supabase/types"
+import { FC, useContext, useState } from "react"
+
+interface CreatePresetProps {
+ isOpen: boolean
+ onOpenChange: (isOpen: boolean) => void
+}
+
+export const CreatePreset: FC = ({
+ isOpen,
+ onOpenChange
+}) => {
+ const { profile, selectedWorkspace } = useContext(ChatbotUIContext)
+
+ const [name, setName] = useState("")
+ const [isTyping, setIsTyping] = useState(false)
+ const [description, setDescription] = useState("")
+ const [presetChatSettings, setPresetChatSettings] = useState({
+ model: selectedWorkspace?.default_model,
+ prompt: selectedWorkspace?.default_prompt,
+ temperature: selectedWorkspace?.default_temperature,
+ contextLength: selectedWorkspace?.default_context_length,
+ includeProfileContext: selectedWorkspace?.include_profile_context,
+ includeWorkspaceInstructions:
+ selectedWorkspace?.include_workspace_instructions,
+ embeddingsProvider: selectedWorkspace?.embeddings_provider
+ })
+
+ if (!profile) return null
+ if (!selectedWorkspace) return null
+
+ return (
+
+ }
+ renderInputs={() => (
+ <>
+
+ Name
+
+ setName(e.target.value)}
+ maxLength={PRESET_NAME_MAX}
+ />
+
+
+
+ >
+ )}
+ />
+ )
+}
diff --git a/chatbot-ui-main/components/sidebar/items/presets/preset-item.tsx b/chatbot-ui-main/components/sidebar/items/presets/preset-item.tsx
new file mode 100644
index 0000000000..7ad71ffb15
--- /dev/null
+++ b/chatbot-ui-main/components/sidebar/items/presets/preset-item.tsx
@@ -0,0 +1,75 @@
+import { ModelIcon } from "@/components/models/model-icon"
+import { ChatSettingsForm } from "@/components/ui/chat-settings-form"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { PRESET_NAME_MAX } from "@/db/limits"
+import { LLM_LIST } from "@/lib/models/llm/llm-list"
+import { Tables } from "@/supabase/types"
+import { FC, useState } from "react"
+import { SidebarItem } from "../all/sidebar-display-item"
+
+interface PresetItemProps {
+ preset: Tables<"presets">
+}
+
+export const PresetItem: FC = ({ preset }) => {
+ const [name, setName] = useState(preset.name)
+ const [isTyping, setIsTyping] = useState(false)
+ const [description, setDescription] = useState(preset.description)
+ const [presetChatSettings, setPresetChatSettings] = useState({
+ model: preset.model,
+ prompt: preset.prompt,
+ temperature: preset.temperature,
+ contextLength: preset.context_length,
+ includeProfileContext: preset.include_profile_context,
+ includeWorkspaceInstructions: preset.include_workspace_instructions
+ })
+
+ const modelDetails = LLM_LIST.find(model => model.modelId === preset.model)
+
+ return (
+
+ }
+ updateState={{
+ name,
+ description,
+ include_profile_context: presetChatSettings.includeProfileContext,
+ include_workspace_instructions:
+ presetChatSettings.includeWorkspaceInstructions,
+ context_length: presetChatSettings.contextLength,
+ model: presetChatSettings.model,
+ prompt: presetChatSettings.prompt,
+ temperature: presetChatSettings.temperature
+ }}
+ renderInputs={() => (
+ <>
+
+ Name
+
+ setName(e.target.value)}
+ maxLength={PRESET_NAME_MAX}
+ />
+
+
+
+ >
+ )}
+ />
+ )
+}
diff --git a/chatbot-ui-main/components/sidebar/items/prompts/create-prompt.tsx b/chatbot-ui-main/components/sidebar/items/prompts/create-prompt.tsx
new file mode 100644
index 0000000000..2b785c0a49
--- /dev/null
+++ b/chatbot-ui-main/components/sidebar/items/prompts/create-prompt.tsx
@@ -0,0 +1,72 @@
+import { SidebarCreateItem } from "@/components/sidebar/items/all/sidebar-create-item"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { TextareaAutosize } from "@/components/ui/textarea-autosize"
+import { ChatbotUIContext } from "@/context/context"
+import { PROMPT_NAME_MAX } from "@/db/limits"
+import { TablesInsert } from "@/supabase/types"
+import { FC, useContext, useState } from "react"
+
+interface CreatePromptProps {
+ isOpen: boolean
+ onOpenChange: (isOpen: boolean) => void
+}
+
+export const CreatePrompt: FC = ({
+ isOpen,
+ onOpenChange
+}) => {
+ const { profile, selectedWorkspace } = useContext(ChatbotUIContext)
+ const [isTyping, setIsTyping] = useState(false)
+ const [name, setName] = useState("")
+ const [content, setContent] = useState("")
+
+ if (!profile) return null
+ if (!selectedWorkspace) return null
+
+ return (
+
+ }
+ renderInputs={() => (
+ <>
+
+ Name
+
+ setName(e.target.value)}
+ maxLength={PROMPT_NAME_MAX}
+ onCompositionStart={() => setIsTyping(true)}
+ onCompositionEnd={() => setIsTyping(false)}
+ />
+
+
+
+ Prompt
+
+ setIsTyping(true)}
+ onCompositionEnd={() => setIsTyping(false)}
+ />
+
+ >
+ )}
+ />
+ )
+}
diff --git a/chatbot-ui-main/components/sidebar/items/prompts/prompt-item.tsx b/chatbot-ui-main/components/sidebar/items/prompts/prompt-item.tsx
new file mode 100644
index 0000000000..921e3157f8
--- /dev/null
+++ b/chatbot-ui-main/components/sidebar/items/prompts/prompt-item.tsx
@@ -0,0 +1,57 @@
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { TextareaAutosize } from "@/components/ui/textarea-autosize"
+import { PROMPT_NAME_MAX } from "@/db/limits"
+import { Tables } from "@/supabase/types"
+import { IconPencil } from "@tabler/icons-react"
+import { FC, useState } from "react"
+import { SidebarItem } from "../all/sidebar-display-item"
+
+interface PromptItemProps {
+ prompt: Tables<"prompts">
+}
+
+export const PromptItem: FC = ({ prompt }) => {
+ const [name, setName] = useState(prompt.name)
+ const [content, setContent] = useState(prompt.content)
+ const [isTyping, setIsTyping] = useState(false)
+ return (
+ }
+ updateState={{ name, content }}
+ renderInputs={() => (
+ <>
+
+ Name
+
+ setName(e.target.value)}
+ maxLength={PROMPT_NAME_MAX}
+ onCompositionStart={() => setIsTyping(true)}
+ onCompositionEnd={() => setIsTyping(false)}
+ />
+
+
+
+ Prompt
+
+ setIsTyping(true)}
+ onCompositionEnd={() => setIsTyping(false)}
+ />
+
+ >
+ )}
+ />
+ )
+}
diff --git a/chatbot-ui-main/components/sidebar/items/tools/create-tool.tsx b/chatbot-ui-main/components/sidebar/items/tools/create-tool.tsx
new file mode 100644
index 0000000000..b134246a1a
--- /dev/null
+++ b/chatbot-ui-main/components/sidebar/items/tools/create-tool.tsx
@@ -0,0 +1,172 @@
+import { SidebarCreateItem } from "@/components/sidebar/items/all/sidebar-create-item"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { TextareaAutosize } from "@/components/ui/textarea-autosize"
+import { ChatbotUIContext } from "@/context/context"
+import { TOOL_DESCRIPTION_MAX, TOOL_NAME_MAX } from "@/db/limits"
+import { validateOpenAPI } from "@/lib/openapi-conversion"
+import { TablesInsert } from "@/supabase/types"
+import { FC, useContext, useState } from "react"
+
+interface CreateToolProps {
+ isOpen: boolean
+ onOpenChange: (isOpen: boolean) => void
+}
+
+export const CreateTool: FC = ({ isOpen, onOpenChange }) => {
+ const { profile, selectedWorkspace } = useContext(ChatbotUIContext)
+
+ const [name, setName] = useState("")
+ const [isTyping, setIsTyping] = useState(false)
+ const [description, setDescription] = useState("")
+ const [url, setUrl] = useState("")
+ const [customHeaders, setCustomHeaders] = useState("")
+ const [schema, setSchema] = useState("")
+ const [schemaError, setSchemaError] = useState("")
+
+ if (!profile || !selectedWorkspace) return null
+
+ return (
+
+ }
+ isOpen={isOpen}
+ isTyping={isTyping}
+ renderInputs={() => (
+ <>
+
+ Name
+
+ setName(e.target.value)}
+ maxLength={TOOL_NAME_MAX}
+ />
+
+
+
+ Description
+
+ setDescription(e.target.value)}
+ maxLength={TOOL_DESCRIPTION_MAX}
+ />
+
+
+ {/*
+ URL
+
+ setUrl(e.target.value)}
+ />
+
*/}
+
+ {/*
+
+
+
+ Web Browsing
+
+
+
+
+
+ Image Generation
+
+
+
+
+
+ Code Interpreter
+
+
*/}
+
+
+ Custom Headers
+
+
+
+
+
+
Schema
+
+
{
+ setSchema(value)
+
+ try {
+ const parsedSchema = JSON.parse(value)
+ validateOpenAPI(parsedSchema)
+ .then(() => setSchemaError("")) // Clear error if validation is successful
+ .catch(error => setSchemaError(error.message)) // Set specific validation error message
+ } catch (error) {
+ setSchemaError("Invalid JSON format") // Set error for invalid JSON format
+ }
+ }}
+ minRows={15}
+ />
+
+ {schemaError}
+
+ >
+ )}
+ onOpenChange={onOpenChange}
+ />
+ )
+}
diff --git a/chatbot-ui-main/components/sidebar/items/tools/tool-item.tsx b/chatbot-ui-main/components/sidebar/items/tools/tool-item.tsx
new file mode 100644
index 0000000000..fb8d54a442
--- /dev/null
+++ b/chatbot-ui-main/components/sidebar/items/tools/tool-item.tsx
@@ -0,0 +1,166 @@
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { TextareaAutosize } from "@/components/ui/textarea-autosize"
+import { TOOL_DESCRIPTION_MAX, TOOL_NAME_MAX } from "@/db/limits"
+import { validateOpenAPI } from "@/lib/openapi-conversion"
+import { Tables } from "@/supabase/types"
+import { IconBolt } from "@tabler/icons-react"
+import { FC, useState } from "react"
+import { SidebarItem } from "../all/sidebar-display-item"
+
+interface ToolItemProps {
+ tool: Tables<"tools">
+}
+
+export const ToolItem: FC = ({ tool }) => {
+ const [name, setName] = useState(tool.name)
+ const [isTyping, setIsTyping] = useState(false)
+ const [description, setDescription] = useState(tool.description)
+ const [url, setUrl] = useState(tool.url)
+ const [customHeaders, setCustomHeaders] = useState(
+ tool.custom_headers as string
+ )
+ const [schema, setSchema] = useState(tool.schema as string)
+ const [schemaError, setSchemaError] = useState("")
+
+ return (
+ }
+ updateState={{
+ name,
+ description,
+ url,
+ custom_headers: customHeaders,
+ schema
+ }}
+ renderInputs={() => (
+ <>
+
+ Name
+
+ setName(e.target.value)}
+ maxLength={TOOL_NAME_MAX}
+ />
+
+
+
+ Description
+
+ setDescription(e.target.value)}
+ maxLength={TOOL_DESCRIPTION_MAX}
+ />
+
+
+ {/*
+ URL
+
+ setUrl(e.target.value)}
+ />
+
*/}
+
+ {/*
+
+
+
+ Web Browsing
+
+
+
+
+
+ Image Generation
+
+
+
+
+
+ Code Interpreter
+
+
*/}
+
+
+ Custom Headers
+
+
+
+
+
+
Schema
+
+
{
+ setSchema(value)
+
+ try {
+ const parsedSchema = JSON.parse(value)
+ validateOpenAPI(parsedSchema)
+ .then(() => setSchemaError("")) // Clear error if validation is successful
+ .catch(error => setSchemaError(error.message)) // Set specific validation error message
+ } catch (error) {
+ setSchemaError("Invalid JSON format") // Set error for invalid JSON format
+ }
+ }}
+ minRows={15}
+ />
+
+ {schemaError}
+
+ >
+ )}
+ />
+ )
+}
diff --git a/chatbot-ui-main/components/sidebar/sidebar-content.tsx b/chatbot-ui-main/components/sidebar/sidebar-content.tsx
new file mode 100644
index 0000000000..1c5bd0860c
--- /dev/null
+++ b/chatbot-ui-main/components/sidebar/sidebar-content.tsx
@@ -0,0 +1,50 @@
+import { Tables } from "@/supabase/types"
+import { ContentType, DataListType } from "@/types"
+import { FC, useState } from "react"
+import { SidebarCreateButtons } from "./sidebar-create-buttons"
+import { SidebarDataList } from "./sidebar-data-list"
+import { SidebarSearch } from "./sidebar-search"
+
+interface SidebarContentProps {
+ contentType: ContentType
+ data: DataListType
+ folders: Tables<"folders">[]
+}
+
+export const SidebarContent: FC = ({
+ contentType,
+ data,
+ folders
+}) => {
+ const [searchTerm, setSearchTerm] = useState("")
+
+ const filteredData: any = data.filter(item =>
+ item.name.toLowerCase().includes(searchTerm.toLowerCase())
+ )
+
+ return (
+ // Subtract 50px for the height of the workspace settings
+
+
+ 0}
+ />
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/chatbot-ui-main/components/sidebar/sidebar-create-buttons.tsx b/chatbot-ui-main/components/sidebar/sidebar-create-buttons.tsx
new file mode 100644
index 0000000000..934365471f
--- /dev/null
+++ b/chatbot-ui-main/components/sidebar/sidebar-create-buttons.tsx
@@ -0,0 +1,157 @@
+import { useChatHandler } from "@/components/chat/chat-hooks/use-chat-handler"
+import { ChatbotUIContext } from "@/context/context"
+import { createFolder } from "@/db/folders"
+import { ContentType } from "@/types"
+import { IconFolderPlus, IconPlus } from "@tabler/icons-react"
+import { FC, useContext, useState } from "react"
+import { Button } from "../ui/button"
+import { CreateAssistant } from "./items/assistants/create-assistant"
+import { CreateCollection } from "./items/collections/create-collection"
+import { CreateFile } from "./items/files/create-file"
+import { CreateModel } from "./items/models/create-model"
+import { CreatePreset } from "./items/presets/create-preset"
+import { CreatePrompt } from "./items/prompts/create-prompt"
+import { CreateTool } from "./items/tools/create-tool"
+
+interface SidebarCreateButtonsProps {
+ contentType: ContentType
+ hasData: boolean
+}
+
+export const SidebarCreateButtons: FC = ({
+ contentType,
+ hasData
+}) => {
+ const { profile, selectedWorkspace, folders, setFolders } =
+ useContext(ChatbotUIContext)
+ const { handleNewChat } = useChatHandler()
+
+ const [isCreatingPrompt, setIsCreatingPrompt] = useState(false)
+ const [isCreatingPreset, setIsCreatingPreset] = useState(false)
+ const [isCreatingFile, setIsCreatingFile] = useState(false)
+ const [isCreatingCollection, setIsCreatingCollection] = useState(false)
+ const [isCreatingAssistant, setIsCreatingAssistant] = useState(false)
+ const [isCreatingTool, setIsCreatingTool] = useState(false)
+ const [isCreatingModel, setIsCreatingModel] = useState(false)
+
+ const handleCreateFolder = async () => {
+ if (!profile) return
+ if (!selectedWorkspace) return
+
+ const createdFolder = await createFolder({
+ user_id: profile.user_id,
+ workspace_id: selectedWorkspace.id,
+ name: "New Folder",
+ description: "",
+ type: contentType
+ })
+ setFolders([...folders, createdFolder])
+ }
+
+ const getCreateFunction = () => {
+ switch (contentType) {
+ case "chats":
+ return async () => {
+ handleNewChat()
+ }
+
+ case "presets":
+ return async () => {
+ setIsCreatingPreset(true)
+ }
+
+ case "prompts":
+ return async () => {
+ setIsCreatingPrompt(true)
+ }
+
+ case "files":
+ return async () => {
+ setIsCreatingFile(true)
+ }
+
+ case "collections":
+ return async () => {
+ setIsCreatingCollection(true)
+ }
+
+ case "assistants":
+ return async () => {
+ setIsCreatingAssistant(true)
+ }
+
+ case "tools":
+ return async () => {
+ setIsCreatingTool(true)
+ }
+
+ case "models":
+ return async () => {
+ setIsCreatingModel(true)
+ }
+
+ default:
+ break
+ }
+ }
+
+ return (
+
+
+
+ New{" "}
+ {contentType.charAt(0).toUpperCase() +
+ contentType.slice(1, contentType.length - 1)}
+
+
+ {hasData && (
+
+
+
+ )}
+
+ {isCreatingPrompt && (
+
+ )}
+
+ {isCreatingPreset && (
+
+ )}
+
+ {isCreatingFile && (
+
+ )}
+
+ {isCreatingCollection && (
+
+ )}
+
+ {isCreatingAssistant && (
+
+ )}
+
+ {isCreatingTool && (
+
+ )}
+
+ {isCreatingModel && (
+
+ )}
+
+ )
+}
diff --git a/chatbot-ui-main/components/sidebar/sidebar-data-list.tsx b/chatbot-ui-main/components/sidebar/sidebar-data-list.tsx
new file mode 100644
index 0000000000..ee9ba44e0f
--- /dev/null
+++ b/chatbot-ui-main/components/sidebar/sidebar-data-list.tsx
@@ -0,0 +1,344 @@
+import { ChatbotUIContext } from "@/context/context"
+import { updateAssistant } from "@/db/assistants"
+import { updateChat } from "@/db/chats"
+import { updateCollection } from "@/db/collections"
+import { updateFile } from "@/db/files"
+import { updateModel } from "@/db/models"
+import { updatePreset } from "@/db/presets"
+import { updatePrompt } from "@/db/prompts"
+import { updateTool } from "@/db/tools"
+import { cn } from "@/lib/utils"
+import { Tables } from "@/supabase/types"
+import { ContentType, DataItemType, DataListType } from "@/types"
+import { FC, useContext, useEffect, useRef, useState } from "react"
+import { Separator } from "../ui/separator"
+import { AssistantItem } from "./items/assistants/assistant-item"
+import { ChatItem } from "./items/chat/chat-item"
+import { CollectionItem } from "./items/collections/collection-item"
+import { FileItem } from "./items/files/file-item"
+import { Folder } from "./items/folders/folder-item"
+import { ModelItem } from "./items/models/model-item"
+import { PresetItem } from "./items/presets/preset-item"
+import { PromptItem } from "./items/prompts/prompt-item"
+import { ToolItem } from "./items/tools/tool-item"
+
+interface SidebarDataListProps {
+ contentType: ContentType
+ data: DataListType
+ folders: Tables<"folders">[]
+}
+
+export const SidebarDataList: FC = ({
+ contentType,
+ data,
+ folders
+}) => {
+ const {
+ setChats,
+ setPresets,
+ setPrompts,
+ setFiles,
+ setCollections,
+ setAssistants,
+ setTools,
+ setModels
+ } = useContext(ChatbotUIContext)
+
+ const divRef = useRef(null)
+
+ const [isOverflowing, setIsOverflowing] = useState(false)
+ const [isDragOver, setIsDragOver] = useState(false)
+
+ const getDataListComponent = (
+ contentType: ContentType,
+ item: DataItemType
+ ) => {
+ switch (contentType) {
+ case "chats":
+ return } />
+
+ case "presets":
+ return } />
+
+ case "prompts":
+ return } />
+
+ case "files":
+ return } />
+
+ case "collections":
+ return (
+ }
+ />
+ )
+
+ case "assistants":
+ return (
+ }
+ />
+ )
+
+ case "tools":
+ return } />
+
+ case "models":
+ return } />
+
+ default:
+ return null
+ }
+ }
+
+ const getSortedData = (
+ data: any,
+ dateCategory: "Today" | "Yesterday" | "Previous Week" | "Older"
+ ) => {
+ const now = new Date()
+ const todayStart = new Date(now.setHours(0, 0, 0, 0))
+ const yesterdayStart = new Date(
+ new Date().setDate(todayStart.getDate() - 1)
+ )
+ const oneWeekAgoStart = new Date(
+ new Date().setDate(todayStart.getDate() - 7)
+ )
+
+ return data
+ .filter((item: any) => {
+ const itemDate = new Date(item.updated_at || item.created_at)
+ switch (dateCategory) {
+ case "Today":
+ return itemDate >= todayStart
+ case "Yesterday":
+ return itemDate >= yesterdayStart && itemDate < todayStart
+ case "Previous Week":
+ return itemDate >= oneWeekAgoStart && itemDate < yesterdayStart
+ case "Older":
+ return itemDate < oneWeekAgoStart
+ default:
+ return true
+ }
+ })
+ .sort(
+ (
+ a: { updated_at: string; created_at: string },
+ b: { updated_at: string; created_at: string }
+ ) =>
+ new Date(b.updated_at || b.created_at).getTime() -
+ new Date(a.updated_at || a.created_at).getTime()
+ )
+ }
+
+ const updateFunctions = {
+ chats: updateChat,
+ presets: updatePreset,
+ prompts: updatePrompt,
+ files: updateFile,
+ collections: updateCollection,
+ assistants: updateAssistant,
+ tools: updateTool,
+ models: updateModel
+ }
+
+ const stateUpdateFunctions = {
+ chats: setChats,
+ presets: setPresets,
+ prompts: setPrompts,
+ files: setFiles,
+ collections: setCollections,
+ assistants: setAssistants,
+ tools: setTools,
+ models: setModels
+ }
+
+ const updateFolder = async (itemId: string, folderId: string | null) => {
+ const item: any = data.find(item => item.id === itemId)
+
+ if (!item) return null
+
+ const updateFunction = updateFunctions[contentType]
+ const setStateFunction = stateUpdateFunctions[contentType]
+
+ if (!updateFunction || !setStateFunction) return
+
+ const updatedItem = await updateFunction(item.id, {
+ folder_id: folderId
+ })
+
+ setStateFunction((items: any) =>
+ items.map((item: any) =>
+ item.id === updatedItem.id ? updatedItem : item
+ )
+ )
+ }
+
+ const handleDragEnter = (e: React.DragEvent) => {
+ e.preventDefault()
+ setIsDragOver(true)
+ }
+
+ const handleDragLeave = (e: React.DragEvent) => {
+ e.preventDefault()
+ setIsDragOver(false)
+ }
+
+ const handleDragStart = (e: React.DragEvent, id: string) => {
+ e.dataTransfer.setData("text/plain", id)
+ }
+
+ const handleDragOver = (e: React.DragEvent) => {
+ e.preventDefault()
+ }
+
+ const handleDrop = (e: React.DragEvent) => {
+ e.preventDefault()
+
+ const target = e.target as Element
+
+ if (!target.closest("#folder")) {
+ const itemId = e.dataTransfer.getData("text/plain")
+ updateFolder(itemId, null)
+ }
+
+ setIsDragOver(false)
+ }
+
+ useEffect(() => {
+ if (divRef.current) {
+ setIsOverflowing(
+ divRef.current.scrollHeight > divRef.current.clientHeight
+ )
+ }
+ }, [data])
+
+ const dataWithFolders = data.filter(item => item.folder_id)
+ const dataWithoutFolders = data.filter(item => item.folder_id === null)
+
+ return (
+ <>
+
+ {data.length === 0 && (
+
+
+ No {contentType}.
+
+
+ )}
+
+ {(dataWithFolders.length > 0 || dataWithoutFolders.length > 0) && (
+
+ {folders.map(folder => (
+
+ {dataWithFolders
+ .filter(item => item.folder_id === folder.id)
+ .map(item => (
+ handleDragStart(e, item.id)}
+ >
+ {getDataListComponent(contentType, item)}
+
+ ))}
+
+ ))}
+
+ {folders.length > 0 &&
}
+
+ {contentType === "chats" ? (
+ <>
+ {["Today", "Yesterday", "Previous Week", "Older"].map(
+ dateCategory => {
+ const sortedData = getSortedData(
+ dataWithoutFolders,
+ dateCategory as
+ | "Today"
+ | "Yesterday"
+ | "Previous Week"
+ | "Older"
+ )
+
+ return (
+ sortedData.length > 0 && (
+
+
+ {dateCategory}
+
+
+
+ {sortedData.map((item: any) => (
+
handleDragStart(e, item.id)}
+ >
+ {getDataListComponent(contentType, item)}
+
+ ))}
+
+
+ )
+ )
+ }
+ )}
+ >
+ ) : (
+
+ {dataWithoutFolders.map(item => {
+ return (
+
handleDragStart(e, item.id)}
+ >
+ {getDataListComponent(contentType, item)}
+
+ )
+ })}
+
+ )}
+
+ )}
+
+
+
+ >
+ )
+}
diff --git a/chatbot-ui-main/components/sidebar/sidebar-search.tsx b/chatbot-ui-main/components/sidebar/sidebar-search.tsx
new file mode 100644
index 0000000000..b451bc8442
--- /dev/null
+++ b/chatbot-ui-main/components/sidebar/sidebar-search.tsx
@@ -0,0 +1,23 @@
+import { ContentType } from "@/types"
+import { FC } from "react"
+import { Input } from "../ui/input"
+
+interface SidebarSearchProps {
+ contentType: ContentType
+ searchTerm: string
+ setSearchTerm: Function
+}
+
+export const SidebarSearch: FC = ({
+ contentType,
+ searchTerm,
+ setSearchTerm
+}) => {
+ return (
+ setSearchTerm(e.target.value)}
+ />
+ )
+}
diff --git a/chatbot-ui-main/components/sidebar/sidebar-switch-item.tsx b/chatbot-ui-main/components/sidebar/sidebar-switch-item.tsx
new file mode 100644
index 0000000000..2ccc92f440
--- /dev/null
+++ b/chatbot-ui-main/components/sidebar/sidebar-switch-item.tsx
@@ -0,0 +1,33 @@
+import { ContentType } from "@/types"
+import { FC } from "react"
+import { TabsTrigger } from "../ui/tabs"
+import { WithTooltip } from "../ui/with-tooltip"
+
+interface SidebarSwitchItemProps {
+ contentType: ContentType
+ icon: React.ReactNode
+ onContentTypeChange: (contentType: ContentType) => void
+}
+
+export const SidebarSwitchItem: FC = ({
+ contentType,
+ icon,
+ onContentTypeChange
+}) => {
+ return (
+ {contentType[0].toUpperCase() + contentType.substring(1)}
+ }
+ trigger={
+ onContentTypeChange(contentType as ContentType)}
+ >
+ {icon}
+
+ }
+ />
+ )
+}
diff --git a/chatbot-ui-main/components/sidebar/sidebar-switcher.tsx b/chatbot-ui-main/components/sidebar/sidebar-switcher.tsx
new file mode 100644
index 0000000000..6e1caa1838
--- /dev/null
+++ b/chatbot-ui-main/components/sidebar/sidebar-switcher.tsx
@@ -0,0 +1,93 @@
+import { ContentType } from "@/types"
+import {
+ IconAdjustmentsHorizontal,
+ IconBolt,
+ IconBooks,
+ IconFile,
+ IconMessage,
+ IconPencil,
+ IconRobotFace,
+ IconSparkles
+} from "@tabler/icons-react"
+import { FC } from "react"
+import { TabsList } from "../ui/tabs"
+import { WithTooltip } from "../ui/with-tooltip"
+import { ProfileSettings } from "../utility/profile-settings"
+import { SidebarSwitchItem } from "./sidebar-switch-item"
+
+export const SIDEBAR_ICON_SIZE = 28
+
+interface SidebarSwitcherProps {
+ onContentTypeChange: (contentType: ContentType) => void
+}
+
+export const SidebarSwitcher: FC = ({
+ onContentTypeChange
+}) => {
+ return (
+
+
+ }
+ contentType="chats"
+ onContentTypeChange={onContentTypeChange}
+ />
+
+ }
+ contentType="presets"
+ onContentTypeChange={onContentTypeChange}
+ />
+
+ }
+ contentType="prompts"
+ onContentTypeChange={onContentTypeChange}
+ />
+
+ }
+ contentType="models"
+ onContentTypeChange={onContentTypeChange}
+ />
+
+ }
+ contentType="files"
+ onContentTypeChange={onContentTypeChange}
+ />
+
+ }
+ contentType="collections"
+ onContentTypeChange={onContentTypeChange}
+ />
+
+ }
+ contentType="assistants"
+ onContentTypeChange={onContentTypeChange}
+ />
+
+ }
+ contentType="tools"
+ onContentTypeChange={onContentTypeChange}
+ />
+
+
+
+ {/* TODO */}
+ {/* Import
} trigger={
} /> */}
+
+ {/* TODO */}
+ {/*
*/}
+
+
Profile Settings }
+ trigger={ }
+ />
+
+
+ )
+}
diff --git a/chatbot-ui-main/components/sidebar/sidebar.tsx b/chatbot-ui-main/components/sidebar/sidebar.tsx
new file mode 100644
index 0000000000..69a228e6b2
--- /dev/null
+++ b/chatbot-ui-main/components/sidebar/sidebar.tsx
@@ -0,0 +1,111 @@
+import { ChatbotUIContext } from "@/context/context"
+import { Tables } from "@/supabase/types"
+import { ContentType } from "@/types"
+import { FC, useContext } from "react"
+import { SIDEBAR_WIDTH } from "../ui/dashboard"
+import { TabsContent } from "../ui/tabs"
+import { WorkspaceSwitcher } from "../utility/workspace-switcher"
+import { WorkspaceSettings } from "../workspace/workspace-settings"
+import { SidebarContent } from "./sidebar-content"
+
+interface SidebarProps {
+ contentType: ContentType
+ showSidebar: boolean
+}
+
+export const Sidebar: FC = ({ contentType, showSidebar }) => {
+ const {
+ folders,
+ chats,
+ presets,
+ prompts,
+ files,
+ collections,
+ assistants,
+ tools,
+ models
+ } = useContext(ChatbotUIContext)
+
+ const chatFolders = folders.filter(folder => folder.type === "chats")
+ const presetFolders = folders.filter(folder => folder.type === "presets")
+ const promptFolders = folders.filter(folder => folder.type === "prompts")
+ const filesFolders = folders.filter(folder => folder.type === "files")
+ const collectionFolders = folders.filter(
+ folder => folder.type === "collections"
+ )
+ const assistantFolders = folders.filter(
+ folder => folder.type === "assistants"
+ )
+ const toolFolders = folders.filter(folder => folder.type === "tools")
+ const modelFolders = folders.filter(folder => folder.type === "models")
+
+ const renderSidebarContent = (
+ contentType: ContentType,
+ data: any[],
+ folders: Tables<"folders">[]
+ ) => {
+ return (
+
+ )
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ {(() => {
+ switch (contentType) {
+ case "chats":
+ return renderSidebarContent("chats", chats, chatFolders)
+
+ case "presets":
+ return renderSidebarContent("presets", presets, presetFolders)
+
+ case "prompts":
+ return renderSidebarContent("prompts", prompts, promptFolders)
+
+ case "files":
+ return renderSidebarContent("files", files, filesFolders)
+
+ case "collections":
+ return renderSidebarContent(
+ "collections",
+ collections,
+ collectionFolders
+ )
+
+ case "assistants":
+ return renderSidebarContent(
+ "assistants",
+ assistants,
+ assistantFolders
+ )
+
+ case "tools":
+ return renderSidebarContent("tools", tools, toolFolders)
+
+ case "models":
+ return renderSidebarContent("models", models, modelFolders)
+
+ default:
+ return null
+ }
+ })()}
+
+
+ )
+}
diff --git a/chatbot-ui-main/components/ui/accordion.tsx b/chatbot-ui-main/components/ui/accordion.tsx
new file mode 100644
index 0000000000..791ca2c78c
--- /dev/null
+++ b/chatbot-ui-main/components/ui/accordion.tsx
@@ -0,0 +1,58 @@
+"use client"
+
+import * as React from "react"
+import * as AccordionPrimitive from "@radix-ui/react-accordion"
+import { ChevronDown } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Accordion = AccordionPrimitive.Root
+
+const AccordionItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AccordionItem.displayName = "AccordionItem"
+
+const AccordionTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ svg]:rotate-180",
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+))
+AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
+
+const AccordionContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ {children}
+
+))
+
+AccordionContent.displayName = AccordionPrimitive.Content.displayName
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
diff --git a/chatbot-ui-main/components/ui/advanced-settings.tsx b/chatbot-ui-main/components/ui/advanced-settings.tsx
new file mode 100644
index 0000000000..0c2bf85960
--- /dev/null
+++ b/chatbot-ui-main/components/ui/advanced-settings.tsx
@@ -0,0 +1,40 @@
+import {
+ Collapsible,
+ CollapsibleContent,
+ CollapsibleTrigger
+} from "@/components/ui/collapsible"
+import { IconChevronDown, IconChevronRight } from "@tabler/icons-react"
+import { FC, useState } from "react"
+
+interface AdvancedSettingsProps {
+ children: React.ReactNode
+}
+
+export const AdvancedSettings: FC = ({ children }) => {
+ const [isOpen, setIsOpen] = useState(
+ false
+ // localStorage.getItem("advanced-settings-open") === "true"
+ )
+
+ const handleOpenChange = (isOpen: boolean) => {
+ setIsOpen(isOpen)
+ // localStorage.setItem("advanced-settings-open", String(isOpen))
+ }
+
+ return (
+
+
+
+
Advanced Settings
+ {isOpen ? (
+
+ ) : (
+
+ )}
+
+
+
+ {children}
+
+ )
+}
diff --git a/chatbot-ui-main/components/ui/alert-dialog.tsx b/chatbot-ui-main/components/ui/alert-dialog.tsx
new file mode 100644
index 0000000000..d468c39290
--- /dev/null
+++ b/chatbot-ui-main/components/ui/alert-dialog.tsx
@@ -0,0 +1,141 @@
+"use client"
+
+import * as React from "react"
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+
+const AlertDialog = AlertDialogPrimitive.Root
+
+const AlertDialogTrigger = AlertDialogPrimitive.Trigger
+
+const AlertDialogPortal = AlertDialogPrimitive.Portal
+
+const AlertDialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
+
+const AlertDialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+))
+AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
+
+const AlertDialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogHeader.displayName = "AlertDialogHeader"
+
+const AlertDialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogFooter.displayName = "AlertDialogFooter"
+
+const AlertDialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
+
+const AlertDialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogDescription.displayName =
+ AlertDialogPrimitive.Description.displayName
+
+const AlertDialogAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
+
+const AlertDialogCancel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
+
+export {
+ AlertDialog,
+ AlertDialogPortal,
+ AlertDialogOverlay,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogFooter,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogAction,
+ AlertDialogCancel
+}
diff --git a/chatbot-ui-main/components/ui/alert.tsx b/chatbot-ui-main/components/ui/alert.tsx
new file mode 100644
index 0000000000..588ee66fe4
--- /dev/null
+++ b/chatbot-ui-main/components/ui/alert.tsx
@@ -0,0 +1,59 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const alertVariants = cva(
+ "[&>svg]:text-foreground relative w-full rounded-lg border p-4 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg~*]:pl-7",
+ {
+ variants: {
+ variant: {
+ default: "bg-background text-foreground",
+ destructive:
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive"
+ }
+ },
+ defaultVariants: {
+ variant: "default"
+ }
+ }
+)
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & VariantProps
+>(({ className, variant, ...props }, ref) => (
+
+))
+Alert.displayName = "Alert"
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertTitle.displayName = "AlertTitle"
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertDescription.displayName = "AlertDescription"
+
+export { Alert, AlertTitle, AlertDescription }
diff --git a/chatbot-ui-main/components/ui/aspect-ratio.tsx b/chatbot-ui-main/components/ui/aspect-ratio.tsx
new file mode 100644
index 0000000000..d6a5226f5e
--- /dev/null
+++ b/chatbot-ui-main/components/ui/aspect-ratio.tsx
@@ -0,0 +1,7 @@
+"use client"
+
+import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
+
+const AspectRatio = AspectRatioPrimitive.Root
+
+export { AspectRatio }
diff --git a/chatbot-ui-main/components/ui/avatar.tsx b/chatbot-ui-main/components/ui/avatar.tsx
new file mode 100644
index 0000000000..1cf1283528
--- /dev/null
+++ b/chatbot-ui-main/components/ui/avatar.tsx
@@ -0,0 +1,50 @@
+"use client"
+
+import * as React from "react"
+import * as AvatarPrimitive from "@radix-ui/react-avatar"
+
+import { cn } from "@/lib/utils"
+
+const Avatar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+Avatar.displayName = AvatarPrimitive.Root.displayName
+
+const AvatarImage = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarImage.displayName = AvatarPrimitive.Image.displayName
+
+const AvatarFallback = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
+
+export { Avatar, AvatarImage, AvatarFallback }
diff --git a/chatbot-ui-main/components/ui/badge.tsx b/chatbot-ui-main/components/ui/badge.tsx
new file mode 100644
index 0000000000..56f0ea4b42
--- /dev/null
+++ b/chatbot-ui-main/components/ui/badge.tsx
@@ -0,0 +1,36 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const badgeVariants = cva(
+ "focus:ring-ring inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2",
+ {
+ variants: {
+ variant: {
+ default:
+ "bg-primary text-primary-foreground hover:bg-primary/80 border-transparent",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80 border-transparent",
+ destructive:
+ "bg-destructive text-destructive-foreground hover:bg-destructive/80 border-transparent",
+ outline: "text-foreground"
+ }
+ },
+ defaultVariants: {
+ variant: "default"
+ }
+ }
+)
+
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return (
+
+ )
+}
+
+export { Badge, badgeVariants }
diff --git a/chatbot-ui-main/components/ui/brand.tsx b/chatbot-ui-main/components/ui/brand.tsx
new file mode 100644
index 0000000000..ae42e07372
--- /dev/null
+++ b/chatbot-ui-main/components/ui/brand.tsx
@@ -0,0 +1,26 @@
+"use client"
+
+import Link from "next/link"
+import { FC } from "react"
+import { ChatbotUISVG } from "../icons/chatbotui-svg"
+
+interface BrandProps {
+ theme?: "dark" | "light"
+}
+
+export const Brand: FC = ({ theme = "dark" }) => {
+ return (
+
+
+
+
+
+ Chatbot UI
+
+ )
+}
diff --git a/chatbot-ui-main/components/ui/button.tsx b/chatbot-ui-main/components/ui/button.tsx
new file mode 100644
index 0000000000..fb4fc2fabb
--- /dev/null
+++ b/chatbot-ui-main/components/ui/button.tsx
@@ -0,0 +1,56 @@
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "ring-offset-background focus-visible:ring-ring inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors hover:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ outline:
+ "border-input bg-background hover:bg-accent hover:text-accent-foreground border",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline"
+ },
+ size: {
+ default: "h-10 px-4 py-2",
+ sm: "h-9 rounded-md px-3",
+ lg: "h-11 rounded-md px-8",
+ icon: "size-10"
+ }
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default"
+ }
+ }
+)
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+ return (
+
+ )
+ }
+)
+Button.displayName = "Button"
+
+export { Button, buttonVariants }
diff --git a/chatbot-ui-main/components/ui/calendar.tsx b/chatbot-ui-main/components/ui/calendar.tsx
new file mode 100644
index 0000000000..07c8aa48c3
--- /dev/null
+++ b/chatbot-ui-main/components/ui/calendar.tsx
@@ -0,0 +1,66 @@
+"use client"
+
+import * as React from "react"
+import { ChevronLeft, ChevronRight } from "lucide-react"
+import { DayPicker } from "react-day-picker"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+
+export type CalendarProps = React.ComponentProps
+
+function Calendar({
+ className,
+ classNames,
+ showOutsideDays = true,
+ ...props
+}: CalendarProps) {
+ return (
+ ,
+ IconRight: ({ ...props }) =>
+ }}
+ {...props}
+ />
+ )
+}
+Calendar.displayName = "Calendar"
+
+export { Calendar }
diff --git a/chatbot-ui-main/components/ui/card.tsx b/chatbot-ui-main/components/ui/card.tsx
new file mode 100644
index 0000000000..a26fd5d7b7
--- /dev/null
+++ b/chatbot-ui-main/components/ui/card.tsx
@@ -0,0 +1,79 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+Card.displayName = "Card"
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardHeader.displayName = "CardHeader"
+
+const CardTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardTitle.displayName = "CardTitle"
+
+const CardDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardDescription.displayName = "CardDescription"
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardContent.displayName = "CardContent"
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardFooter.displayName = "CardFooter"
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
diff --git a/chatbot-ui-main/components/ui/chat-settings-form.tsx b/chatbot-ui-main/components/ui/chat-settings-form.tsx
new file mode 100644
index 0000000000..60d6b510da
--- /dev/null
+++ b/chatbot-ui-main/components/ui/chat-settings-form.tsx
@@ -0,0 +1,253 @@
+"use client"
+
+import { ChatbotUIContext } from "@/context/context"
+import { CHAT_SETTING_LIMITS } from "@/lib/chat-setting-limits"
+import { ChatSettings } from "@/types"
+import { IconInfoCircle } from "@tabler/icons-react"
+import { FC, useContext } from "react"
+import { ModelSelect } from "../models/model-select"
+import { AdvancedSettings } from "./advanced-settings"
+import { Checkbox } from "./checkbox"
+import { Label } from "./label"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue
+} from "./select"
+import { Slider } from "./slider"
+import { TextareaAutosize } from "./textarea-autosize"
+import { WithTooltip } from "./with-tooltip"
+
+interface ChatSettingsFormProps {
+ chatSettings: ChatSettings
+ onChangeChatSettings: (value: ChatSettings) => void
+ useAdvancedDropdown?: boolean
+ showTooltip?: boolean
+}
+
+export const ChatSettingsForm: FC = ({
+ chatSettings,
+ onChangeChatSettings,
+ useAdvancedDropdown = true,
+ showTooltip = true
+}) => {
+ const { profile, models } = useContext(ChatbotUIContext)
+
+ if (!profile) return null
+
+ return (
+
+
+ Model
+
+ {
+ onChangeChatSettings({ ...chatSettings, model })
+ }}
+ />
+
+
+
+ Prompt
+
+ {
+ onChangeChatSettings({ ...chatSettings, prompt })
+ }}
+ value={chatSettings.prompt}
+ minRows={3}
+ maxRows={6}
+ />
+
+
+ {useAdvancedDropdown ? (
+
+
+
+ ) : (
+
+ )}
+
+ )
+}
+
+interface AdvancedContentProps {
+ chatSettings: ChatSettings
+ onChangeChatSettings: (value: ChatSettings) => void
+ showTooltip: boolean
+}
+
+const AdvancedContent: FC = ({
+ chatSettings,
+ onChangeChatSettings,
+ showTooltip
+}) => {
+ const { profile, selectedWorkspace, availableOpenRouterModels, models } =
+ useContext(ChatbotUIContext)
+
+ const isCustomModel = models.some(
+ model => model.model_id === chatSettings.model
+ )
+
+ function findOpenRouterModel(modelId: string) {
+ return availableOpenRouterModels.find(model => model.modelId === modelId)
+ }
+
+ const MODEL_LIMITS = CHAT_SETTING_LIMITS[chatSettings.model] || {
+ MIN_TEMPERATURE: 0,
+ MAX_TEMPERATURE: 1,
+ MAX_CONTEXT_LENGTH:
+ findOpenRouterModel(chatSettings.model)?.maxContext || 4096
+ }
+
+ return (
+
+
+
+ Temperature:
+
+ {chatSettings.temperature}
+
+
+
{
+ onChangeChatSettings({
+ ...chatSettings,
+ temperature: temperature[0]
+ })
+ }}
+ min={MODEL_LIMITS.MIN_TEMPERATURE}
+ max={MODEL_LIMITS.MAX_TEMPERATURE}
+ step={0.01}
+ />
+
+
+
+
+ Context Length:
+
+ {chatSettings.contextLength}
+
+
+
{
+ onChangeChatSettings({
+ ...chatSettings,
+ contextLength: contextLength[0]
+ })
+ }}
+ min={0}
+ max={
+ isCustomModel
+ ? models.find(model => model.model_id === chatSettings.model)
+ ?.context_length
+ : MODEL_LIMITS.MAX_CONTEXT_LENGTH
+ }
+ step={1}
+ />
+
+
+
+
+ onChangeChatSettings({
+ ...chatSettings,
+ includeProfileContext: value
+ })
+ }
+ />
+
+ Chats Include Profile Context
+
+ {showTooltip && (
+
+ {profile?.profile_context || "No profile context."}
+
+ }
+ trigger={
+
+ }
+ />
+ )}
+
+
+
+
+ onChangeChatSettings({
+ ...chatSettings,
+ includeWorkspaceInstructions: value
+ })
+ }
+ />
+
+ Chats Include Workspace Instructions
+
+ {showTooltip && (
+
+ {selectedWorkspace?.instructions ||
+ "No workspace instructions."}
+
+ }
+ trigger={
+
+ }
+ />
+ )}
+
+
+
+ Embeddings Provider
+
+ {
+ onChangeChatSettings({
+ ...chatSettings,
+ embeddingsProvider
+ })
+ }}
+ >
+
+
+
+
+
+
+ {profile?.use_azure_openai ? "Azure OpenAI" : "OpenAI"}
+
+
+ {window.location.hostname === "localhost" && (
+ Local
+ )}
+
+
+
+
+ )
+}
diff --git a/chatbot-ui-main/components/ui/checkbox.tsx b/chatbot-ui-main/components/ui/checkbox.tsx
new file mode 100644
index 0000000000..6abd7f87e7
--- /dev/null
+++ b/chatbot-ui-main/components/ui/checkbox.tsx
@@ -0,0 +1,30 @@
+"use client"
+
+import * as React from "react"
+import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
+import { Check } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Checkbox = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+
+))
+Checkbox.displayName = CheckboxPrimitive.Root.displayName
+
+export { Checkbox }
diff --git a/chatbot-ui-main/components/ui/collapsible.tsx b/chatbot-ui-main/components/ui/collapsible.tsx
new file mode 100644
index 0000000000..9fa48946af
--- /dev/null
+++ b/chatbot-ui-main/components/ui/collapsible.tsx
@@ -0,0 +1,11 @@
+"use client"
+
+import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
+
+const Collapsible = CollapsiblePrimitive.Root
+
+const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
+
+const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
+
+export { Collapsible, CollapsibleTrigger, CollapsibleContent }
diff --git a/chatbot-ui-main/components/ui/command.tsx b/chatbot-ui-main/components/ui/command.tsx
new file mode 100644
index 0000000000..bf47537f5c
--- /dev/null
+++ b/chatbot-ui-main/components/ui/command.tsx
@@ -0,0 +1,155 @@
+"use client"
+
+import * as React from "react"
+import { type DialogProps } from "@radix-ui/react-dialog"
+import { Command as CommandPrimitive } from "cmdk"
+import { Search } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+import { Dialog, DialogContent } from "@/components/ui/dialog"
+
+const Command = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+Command.displayName = CommandPrimitive.displayName
+
+interface CommandDialogProps extends DialogProps {}
+
+const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
+ return (
+
+
+
+ {children}
+
+
+
+ )
+}
+
+const CommandInput = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+))
+
+CommandInput.displayName = CommandPrimitive.Input.displayName
+
+const CommandList = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+
+CommandList.displayName = CommandPrimitive.List.displayName
+
+const CommandEmpty = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>((props, ref) => (
+
+))
+
+CommandEmpty.displayName = CommandPrimitive.Empty.displayName
+
+const CommandGroup = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+
+CommandGroup.displayName = CommandPrimitive.Group.displayName
+
+const CommandSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+CommandSeparator.displayName = CommandPrimitive.Separator.displayName
+
+const CommandItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+
+CommandItem.displayName = CommandPrimitive.Item.displayName
+
+const CommandShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => {
+ return (
+
+ )
+}
+CommandShortcut.displayName = "CommandShortcut"
+
+export {
+ Command,
+ CommandDialog,
+ CommandInput,
+ CommandList,
+ CommandEmpty,
+ CommandGroup,
+ CommandItem,
+ CommandShortcut,
+ CommandSeparator
+}
diff --git a/chatbot-ui-main/components/ui/context-menu.tsx b/chatbot-ui-main/components/ui/context-menu.tsx
new file mode 100644
index 0000000000..05b7f098a9
--- /dev/null
+++ b/chatbot-ui-main/components/ui/context-menu.tsx
@@ -0,0 +1,200 @@
+"use client"
+
+import * as React from "react"
+import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
+import { Check, ChevronRight, Circle } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const ContextMenu = ContextMenuPrimitive.Root
+
+const ContextMenuTrigger = ContextMenuPrimitive.Trigger
+
+const ContextMenuGroup = ContextMenuPrimitive.Group
+
+const ContextMenuPortal = ContextMenuPrimitive.Portal
+
+const ContextMenuSub = ContextMenuPrimitive.Sub
+
+const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
+
+const ContextMenuSubTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, children, ...props }, ref) => (
+
+ {children}
+
+
+))
+ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
+
+const ContextMenuSubContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
+
+const ContextMenuContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
+
+const ContextMenuItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, ...props }, ref) => (
+
+))
+ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
+
+const ContextMenuCheckboxItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, checked, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+ContextMenuCheckboxItem.displayName =
+ ContextMenuPrimitive.CheckboxItem.displayName
+
+const ContextMenuRadioItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
+
+const ContextMenuLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, ...props }, ref) => (
+
+))
+ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
+
+const ContextMenuSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
+
+const ContextMenuShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => {
+ return (
+
+ )
+}
+ContextMenuShortcut.displayName = "ContextMenuShortcut"
+
+export {
+ ContextMenu,
+ ContextMenuTrigger,
+ ContextMenuContent,
+ ContextMenuItem,
+ ContextMenuCheckboxItem,
+ ContextMenuRadioItem,
+ ContextMenuLabel,
+ ContextMenuSeparator,
+ ContextMenuShortcut,
+ ContextMenuGroup,
+ ContextMenuPortal,
+ ContextMenuSub,
+ ContextMenuSubContent,
+ ContextMenuSubTrigger,
+ ContextMenuRadioGroup
+}
diff --git a/chatbot-ui-main/components/ui/dashboard.tsx b/chatbot-ui-main/components/ui/dashboard.tsx
new file mode 100644
index 0000000000..b5fbbe9aec
--- /dev/null
+++ b/chatbot-ui-main/components/ui/dashboard.tsx
@@ -0,0 +1,133 @@
+"use client"
+
+import { Sidebar } from "@/components/sidebar/sidebar"
+import { SidebarSwitcher } from "@/components/sidebar/sidebar-switcher"
+import { Button } from "@/components/ui/button"
+import { Tabs } from "@/components/ui/tabs"
+import useHotkey from "@/lib/hooks/use-hotkey"
+import { cn } from "@/lib/utils"
+import { ContentType } from "@/types"
+import { IconChevronCompactRight } from "@tabler/icons-react"
+import { usePathname, useRouter, useSearchParams } from "next/navigation"
+import { FC, useState } from "react"
+import { useSelectFileHandler } from "../chat/chat-hooks/use-select-file-handler"
+import { CommandK } from "../utility/command-k"
+
+export const SIDEBAR_WIDTH = 350
+
+interface DashboardProps {
+ children: React.ReactNode
+}
+
+export const Dashboard: FC = ({ children }) => {
+ useHotkey("s", () => setShowSidebar(prevState => !prevState))
+
+ const pathname = usePathname()
+ const router = useRouter()
+ const searchParams = useSearchParams()
+ const tabValue = searchParams.get("tab") || "chats"
+
+ const { handleSelectDeviceFile } = useSelectFileHandler()
+
+ const [contentType, setContentType] = useState(
+ tabValue as ContentType
+ )
+ const [showSidebar, setShowSidebar] = useState(
+ localStorage.getItem("showSidebar") === "true"
+ )
+ const [isDragging, setIsDragging] = useState(false)
+
+ const onFileDrop = (event: React.DragEvent) => {
+ event.preventDefault()
+
+ const files = event.dataTransfer.files
+ const file = files[0]
+
+ handleSelectDeviceFile(file)
+
+ setIsDragging(false)
+ }
+
+ const handleDragEnter = (event: React.DragEvent) => {
+ event.preventDefault()
+ setIsDragging(true)
+ }
+
+ const handleDragLeave = (event: React.DragEvent) => {
+ event.preventDefault()
+ setIsDragging(false)
+ }
+
+ const onDragOver = (event: React.DragEvent) => {
+ event.preventDefault()
+ }
+
+ const handleToggleSidebar = () => {
+ setShowSidebar(prevState => !prevState)
+ localStorage.setItem("showSidebar", String(!showSidebar))
+ }
+
+ return (
+
+
+
+
+ {showSidebar && (
+ {
+ setContentType(tabValue as ContentType)
+ router.replace(`${pathname}?tab=${tabValue}`)
+ }}
+ >
+
+
+
+
+ )}
+
+
+
+ {isDragging ? (
+
+ drop file here
+
+ ) : (
+ children
+ )}
+
+
+
+
+
+
+ )
+}
diff --git a/chatbot-ui-main/components/ui/dialog.tsx b/chatbot-ui-main/components/ui/dialog.tsx
new file mode 100644
index 0000000000..5c01896f87
--- /dev/null
+++ b/chatbot-ui-main/components/ui/dialog.tsx
@@ -0,0 +1,121 @@
+"use client"
+
+import * as DialogPrimitive from "@radix-ui/react-dialog"
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Dialog = DialogPrimitive.Root
+
+const DialogTrigger = DialogPrimitive.Trigger
+
+const DialogPortal = DialogPrimitive.Portal
+
+const DialogClose = DialogPrimitive.Close
+
+const DialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
+
+const DialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+ {children}
+ {/*
+
+ Close
+ */}
+
+
+))
+DialogContent.displayName = DialogPrimitive.Content.displayName
+
+const DialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+DialogHeader.displayName = "DialogHeader"
+
+const DialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+DialogFooter.displayName = "DialogFooter"
+
+const DialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogTitle.displayName = DialogPrimitive.Title.displayName
+
+const DialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogDescription.displayName = DialogPrimitive.Description.displayName
+
+export {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogOverlay,
+ DialogPortal,
+ DialogTitle,
+ DialogTrigger
+}
diff --git a/chatbot-ui-main/components/ui/dropdown-menu.tsx b/chatbot-ui-main/components/ui/dropdown-menu.tsx
new file mode 100644
index 0000000000..0519d6561e
--- /dev/null
+++ b/chatbot-ui-main/components/ui/dropdown-menu.tsx
@@ -0,0 +1,200 @@
+"use client"
+
+import * as React from "react"
+import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
+import { Check, ChevronRight, Circle } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const DropdownMenu = DropdownMenuPrimitive.Root
+
+const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
+
+const DropdownMenuGroup = DropdownMenuPrimitive.Group
+
+const DropdownMenuPortal = DropdownMenuPrimitive.Portal
+
+const DropdownMenuSub = DropdownMenuPrimitive.Sub
+
+const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
+
+const DropdownMenuSubTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, children, ...props }, ref) => (
+
+ {children}
+
+
+))
+DropdownMenuSubTrigger.displayName =
+ DropdownMenuPrimitive.SubTrigger.displayName
+
+const DropdownMenuSubContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DropdownMenuSubContent.displayName =
+ DropdownMenuPrimitive.SubContent.displayName
+
+const DropdownMenuContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+
+
+))
+DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
+
+const DropdownMenuItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, ...props }, ref) => (
+
+))
+DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
+
+const DropdownMenuCheckboxItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, checked, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+DropdownMenuCheckboxItem.displayName =
+ DropdownMenuPrimitive.CheckboxItem.displayName
+
+const DropdownMenuRadioItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
+
+const DropdownMenuLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, ...props }, ref) => (
+
+))
+DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
+
+const DropdownMenuSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
+
+const DropdownMenuShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => {
+ return (
+
+ )
+}
+DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
+
+export {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuGroup,
+ DropdownMenuPortal,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuRadioGroup
+}
diff --git a/chatbot-ui-main/components/ui/file-icon.tsx b/chatbot-ui-main/components/ui/file-icon.tsx
new file mode 100644
index 0000000000..814dda3e31
--- /dev/null
+++ b/chatbot-ui-main/components/ui/file-icon.tsx
@@ -0,0 +1,36 @@
+import {
+ IconFile,
+ IconFileText,
+ IconFileTypeCsv,
+ IconFileTypeDocx,
+ IconFileTypePdf,
+ IconJson,
+ IconMarkdown,
+ IconPhoto
+} from "@tabler/icons-react"
+import { FC } from "react"
+
+interface FileIconProps {
+ type: string
+ size?: number
+}
+
+export const FileIcon: FC = ({ type, size = 32 }) => {
+ if (type.includes("image")) {
+ return
+ } else if (type.includes("pdf")) {
+ return
+ } else if (type.includes("csv")) {
+ return
+ } else if (type.includes("docx")) {
+ return
+ } else if (type.includes("plain")) {
+ return
+ } else if (type.includes("json")) {
+ return
+ } else if (type.includes("markdown")) {
+ return
+ } else {
+ return
+ }
+}
diff --git a/chatbot-ui-main/components/ui/file-preview.tsx b/chatbot-ui-main/components/ui/file-preview.tsx
new file mode 100644
index 0000000000..2bfbc70378
--- /dev/null
+++ b/chatbot-ui-main/components/ui/file-preview.tsx
@@ -0,0 +1,68 @@
+import { cn } from "@/lib/utils"
+import { Tables } from "@/supabase/types"
+import { ChatFile, MessageImage } from "@/types"
+import { IconFileFilled } from "@tabler/icons-react"
+import Image from "next/image"
+import { FC } from "react"
+import { DrawingCanvas } from "../utility/drawing-canvas"
+import { Dialog, DialogContent } from "./dialog"
+
+interface FilePreviewProps {
+ type: "image" | "file" | "file_item"
+ item: ChatFile | MessageImage | Tables<"file_items">
+ isOpen: boolean
+ onOpenChange: (isOpen: boolean) => void
+}
+
+export const FilePreview: FC = ({
+ type,
+ item,
+ isOpen,
+ onOpenChange
+}) => {
+ return (
+
+
+ {(() => {
+ if (type === "image") {
+ const imageItem = item as MessageImage
+
+ return imageItem.file ? (
+
+ ) : (
+
+ )
+ } else if (type === "file_item") {
+ const fileItem = item as Tables<"file_items">
+ return (
+
+ )
+ } else if (type === "file") {
+ return (
+
+