Repository: mckaywrigley/chatbot-ui
Branch: main
Commit: 81328b61d2a4
Files: 306
Total size: 847.3 KB
Directory structure:
gitextract_faqfpv4r/
├── .eslintrc.json
├── .github/
│ └── funding.yaml
├── .gitignore
├── .husky/
│ └── pre-commit
├── .nvmrc
├── README.md
├── __tests__/
│ ├── lib/
│ │ └── openapi-conversion.test.ts
│ └── playwright-test/
│ ├── .gitignore
│ ├── package.json
│ ├── playwright.config.ts
│ └── tests/
│ └── login.spec.ts
├── app/
│ ├── [locale]/
│ │ ├── [workspaceid]/
│ │ │ ├── chat/
│ │ │ │ ├── [chatid]/
│ │ │ │ │ └── page.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ │ ├── globals.css
│ │ ├── help/
│ │ │ └── page.tsx
│ │ ├── layout.tsx
│ │ ├── loading.tsx
│ │ ├── login/
│ │ │ ├── page.tsx
│ │ │ └── password/
│ │ │ └── page.tsx
│ │ ├── page.tsx
│ │ └── setup/
│ │ └── page.tsx
│ ├── api/
│ │ ├── assistants/
│ │ │ └── openai/
│ │ │ └── route.ts
│ │ ├── chat/
│ │ │ ├── anthropic/
│ │ │ │ └── route.ts
│ │ │ ├── azure/
│ │ │ │ └── route.ts
│ │ │ ├── custom/
│ │ │ │ └── route.ts
│ │ │ ├── google/
│ │ │ │ └── route.ts
│ │ │ ├── groq/
│ │ │ │ └── route.ts
│ │ │ ├── mistral/
│ │ │ │ └── route.ts
│ │ │ ├── openai/
│ │ │ │ └── route.ts
│ │ │ ├── openrouter/
│ │ │ │ └── route.ts
│ │ │ ├── perplexity/
│ │ │ │ └── route.ts
│ │ │ └── tools/
│ │ │ └── route.ts
│ │ ├── command/
│ │ │ └── route.ts
│ │ ├── keys/
│ │ │ └── route.ts
│ │ ├── retrieval/
│ │ │ ├── process/
│ │ │ │ ├── docx/
│ │ │ │ │ └── route.ts
│ │ │ │ └── route.ts
│ │ │ └── retrieve/
│ │ │ └── route.ts
│ │ └── username/
│ │ ├── available/
│ │ │ └── route.ts
│ │ └── get/
│ │ └── route.ts
│ └── auth/
│ └── callback/
│ └── route.ts
├── components/
│ ├── chat/
│ │ ├── assistant-picker.tsx
│ │ ├── chat-command-input.tsx
│ │ ├── chat-files-display.tsx
│ │ ├── chat-help.tsx
│ │ ├── chat-helpers/
│ │ │ └── index.ts
│ │ ├── chat-hooks/
│ │ │ ├── use-chat-handler.tsx
│ │ │ ├── use-chat-history.tsx
│ │ │ ├── use-prompt-and-command.tsx
│ │ │ ├── use-scroll.tsx
│ │ │ └── use-select-file-handler.tsx
│ │ ├── chat-input.tsx
│ │ ├── chat-messages.tsx
│ │ ├── chat-retrieval-settings.tsx
│ │ ├── chat-scroll-buttons.tsx
│ │ ├── chat-secondary-buttons.tsx
│ │ ├── chat-settings.tsx
│ │ ├── chat-ui.tsx
│ │ ├── file-picker.tsx
│ │ ├── prompt-picker.tsx
│ │ ├── quick-setting-option.tsx
│ │ ├── quick-settings.tsx
│ │ └── tool-picker.tsx
│ ├── icons/
│ │ ├── anthropic-svg.tsx
│ │ ├── chatbotui-svg.tsx
│ │ ├── google-svg.tsx
│ │ └── openai-svg.tsx
│ ├── messages/
│ │ ├── message-actions.tsx
│ │ ├── message-codeblock.tsx
│ │ ├── message-markdown-memoized.tsx
│ │ ├── message-markdown.tsx
│ │ ├── message-replies.tsx
│ │ └── message.tsx
│ ├── models/
│ │ ├── model-icon.tsx
│ │ ├── model-option.tsx
│ │ └── model-select.tsx
│ ├── setup/
│ │ ├── api-step.tsx
│ │ ├── finish-step.tsx
│ │ ├── profile-step.tsx
│ │ └── step-container.tsx
│ ├── sidebar/
│ │ ├── items/
│ │ │ ├── all/
│ │ │ │ ├── sidebar-create-item.tsx
│ │ │ │ ├── sidebar-delete-item.tsx
│ │ │ │ ├── sidebar-display-item.tsx
│ │ │ │ └── sidebar-update-item.tsx
│ │ │ ├── assistants/
│ │ │ │ ├── assistant-item.tsx
│ │ │ │ ├── assistant-retrieval-select.tsx
│ │ │ │ ├── assistant-tool-select.tsx
│ │ │ │ └── create-assistant.tsx
│ │ │ ├── chat/
│ │ │ │ ├── chat-item.tsx
│ │ │ │ ├── delete-chat.tsx
│ │ │ │ └── update-chat.tsx
│ │ │ ├── collections/
│ │ │ │ ├── collection-file-select.tsx
│ │ │ │ ├── collection-item.tsx
│ │ │ │ └── create-collection.tsx
│ │ │ ├── files/
│ │ │ │ ├── create-file.tsx
│ │ │ │ └── file-item.tsx
│ │ │ ├── folders/
│ │ │ │ ├── delete-folder.tsx
│ │ │ │ ├── folder-item.tsx
│ │ │ │ └── update-folder.tsx
│ │ │ ├── models/
│ │ │ │ ├── create-model.tsx
│ │ │ │ └── model-item.tsx
│ │ │ ├── presets/
│ │ │ │ ├── create-preset.tsx
│ │ │ │ └── preset-item.tsx
│ │ │ ├── prompts/
│ │ │ │ ├── create-prompt.tsx
│ │ │ │ └── prompt-item.tsx
│ │ │ └── tools/
│ │ │ ├── create-tool.tsx
│ │ │ └── tool-item.tsx
│ │ ├── sidebar-content.tsx
│ │ ├── sidebar-create-buttons.tsx
│ │ ├── sidebar-data-list.tsx
│ │ ├── sidebar-search.tsx
│ │ ├── sidebar-switch-item.tsx
│ │ ├── sidebar-switcher.tsx
│ │ └── sidebar.tsx
│ ├── ui/
│ │ ├── accordion.tsx
│ │ ├── advanced-settings.tsx
│ │ ├── alert-dialog.tsx
│ │ ├── alert.tsx
│ │ ├── aspect-ratio.tsx
│ │ ├── avatar.tsx
│ │ ├── badge.tsx
│ │ ├── brand.tsx
│ │ ├── button.tsx
│ │ ├── calendar.tsx
│ │ ├── card.tsx
│ │ ├── chat-settings-form.tsx
│ │ ├── checkbox.tsx
│ │ ├── collapsible.tsx
│ │ ├── command.tsx
│ │ ├── context-menu.tsx
│ │ ├── dashboard.tsx
│ │ ├── dialog.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── file-icon.tsx
│ │ ├── file-preview.tsx
│ │ ├── form.tsx
│ │ ├── hover-card.tsx
│ │ ├── image-picker.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── limit-display.tsx
│ │ ├── menubar.tsx
│ │ ├── navigation-menu.tsx
│ │ ├── popover.tsx
│ │ ├── progress.tsx
│ │ ├── radio-group.tsx
│ │ ├── screen-loader.tsx
│ │ ├── scroll-area.tsx
│ │ ├── select.tsx
│ │ ├── separator.tsx
│ │ ├── sheet.tsx
│ │ ├── skeleton.tsx
│ │ ├── slider.tsx
│ │ ├── sonner.tsx
│ │ ├── submit-button.tsx
│ │ ├── switch.tsx
│ │ ├── table.tsx
│ │ ├── tabs.tsx
│ │ ├── textarea-autosize.tsx
│ │ ├── textarea.tsx
│ │ ├── toast.tsx
│ │ ├── toaster.tsx
│ │ ├── toggle-group.tsx
│ │ ├── toggle.tsx
│ │ ├── tooltip.tsx
│ │ ├── use-toast.ts
│ │ └── with-tooltip.tsx
│ ├── utility/
│ │ ├── alerts.tsx
│ │ ├── announcements.tsx
│ │ ├── change-password.tsx
│ │ ├── command-k.tsx
│ │ ├── drawing-canvas.tsx
│ │ ├── global-state.tsx
│ │ ├── import.tsx
│ │ ├── profile-settings.tsx
│ │ ├── providers.tsx
│ │ ├── theme-switcher.tsx
│ │ ├── translations-provider.tsx
│ │ └── workspace-switcher.tsx
│ └── workspace/
│ ├── assign-workspaces.tsx
│ ├── delete-workspace.tsx
│ └── workspace-settings.tsx
├── components.json
├── context/
│ └── context.tsx
├── db/
│ ├── assistant-collections.ts
│ ├── assistant-files.ts
│ ├── assistant-tools.ts
│ ├── assistants.ts
│ ├── chat-files.ts
│ ├── chats.ts
│ ├── collection-files.ts
│ ├── collections.ts
│ ├── files.ts
│ ├── folders.ts
│ ├── index.ts
│ ├── limits.ts
│ ├── message-file-items.ts
│ ├── messages.ts
│ ├── models.ts
│ ├── presets.ts
│ ├── profile.ts
│ ├── prompts.ts
│ ├── storage/
│ │ ├── assistant-images.ts
│ │ ├── files.ts
│ │ ├── message-images.ts
│ │ ├── profile-images.ts
│ │ └── workspace-images.ts
│ ├── tools.ts
│ └── workspaces.ts
├── i18nConfig.js
├── jest.config.ts
├── lib/
│ ├── blob-to-b64.ts
│ ├── build-prompt.ts
│ ├── chat-setting-limits.ts
│ ├── consume-stream.ts
│ ├── envs.ts
│ ├── export-old-data.ts
│ ├── generate-local-embedding.ts
│ ├── hooks/
│ │ ├── use-copy-to-clipboard.tsx
│ │ └── use-hotkey.tsx
│ ├── i18n.ts
│ ├── models/
│ │ ├── fetch-models.ts
│ │ └── llm/
│ │ ├── anthropic-llm-list.ts
│ │ ├── google-llm-list.ts
│ │ ├── groq-llm-list.ts
│ │ ├── llm-list.ts
│ │ ├── mistral-llm-list.ts
│ │ ├── openai-llm-list.ts
│ │ └── perplexity-llm-list.ts
│ ├── openapi-conversion.ts
│ ├── retrieval/
│ │ └── processing/
│ │ ├── csv.ts
│ │ ├── docx.ts
│ │ ├── index.ts
│ │ ├── json.ts
│ │ ├── md.ts
│ │ ├── pdf.ts
│ │ └── txt.ts
│ ├── server/
│ │ ├── server-chat-helpers.ts
│ │ └── server-utils.ts
│ ├── supabase/
│ │ ├── browser-client.ts
│ │ ├── client.ts
│ │ ├── middleware.ts
│ │ └── server.ts
│ └── utils.ts
├── license
├── middleware.ts
├── next.config.js
├── package.json
├── postcss.config.js
├── prettier.config.cjs
├── public/
│ ├── locales/
│ │ ├── de/
│ │ │ └── translation.json
│ │ └── en/
│ │ └── translation.json
│ ├── manifest.json
│ └── worker-development.js
├── supabase/
│ ├── .gitignore
│ ├── config.toml
│ ├── migrations/
│ │ ├── 20240108234540_setup.sql
│ │ ├── 20240108234541_add_profiles.sql
│ │ ├── 20240108234542_add_workspaces.sql
│ │ ├── 20240108234543_add_folders.sql
│ │ ├── 20240108234544_add_files.sql
│ │ ├── 20240108234545_add_file_items.sql
│ │ ├── 20240108234546_add_presets.sql
│ │ ├── 20240108234547_add_assistants.sql
│ │ ├── 20240108234548_add_chats.sql
│ │ ├── 20240108234549_add_messages.sql
│ │ ├── 20240108234550_add_prompts.sql
│ │ ├── 20240108234551_add_collections.sql
│ │ ├── 20240115135033_add_openrouter.sql
│ │ ├── 20240115171510_add_assistant_files.sql
│ │ ├── 20240115171524_add_tools.sql
│ │ ├── 20240115172125_add_assistant_tools.sql
│ │ ├── 20240118224049_add_azure_embeddings.sql
│ │ ├── 20240124234424_tool_improvements.sql
│ │ ├── 20240125192042_upgrade_openai_models.sql
│ │ ├── 20240125194719_add_custom_models.sql
│ │ ├── 20240129232644_add_workspace_images.sql
│ │ ├── 20240212063532_add_at_assistants.sql
│ │ ├── 20240213040255_remove_request_in_body_from_tools.sql
│ │ ├── 20240213085646_add_context_length_to_custom_models.sql
│ │ └── 20240302004845_add_groq.sql
│ ├── seed.sql
│ └── types.ts
├── tailwind.config.ts
├── tsconfig.json
├── types/
│ ├── announcement.ts
│ ├── assistant-retrieval-item.ts
│ ├── chat-file.tsx
│ ├── chat-message.ts
│ ├── chat.ts
│ ├── collection-file.ts
│ ├── content-type.ts
│ ├── error-response.ts
│ ├── file-item-chunk.ts
│ ├── images/
│ │ ├── assistant-image.ts
│ │ ├── message-image.ts
│ │ └── workspace-image.ts
│ ├── index.ts
│ ├── key-type.ts
│ ├── llms.ts
│ ├── models.ts
│ ├── sharing.ts
│ ├── sidebar-data.ts
│ └── valid-keys.ts
└── worker/
└── index.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .eslintrc.json
================================================
{
"$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"
}
]
}
================================================
FILE: .github/funding.yaml
================================================
# If you find my open-source work helpful, please consider sponsoring me!
github: mckaywrigley
================================================
FILE: .gitignore
================================================
# 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
================================================
FILE: .husky/pre-commit
================================================
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm run lint:fix && npm run format:write && git add .
================================================
FILE: .nvmrc
================================================
v20.11.0
================================================
FILE: README.md
================================================
# 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)
================================================
FILE: __tests__/lib/openapi-conversion.test.ts
================================================
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)
})
})
================================================
FILE: __tests__/playwright-test/.gitignore
================================================
node_modules/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
================================================
FILE: __tests__/playwright-test/package.json
================================================
{
"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"
}
}
================================================
FILE: __tests__/playwright-test/playwright.config.ts
================================================
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,
// },
});
================================================
FILE: __tests__/playwright-test/tests/login.spec.ts
================================================
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
================================================
FILE: app/[locale]/[workspaceid]/chat/[chatid]/page.tsx
================================================
"use client"
import { ChatUI } from "@/components/chat/chat-ui"
export default function ChatIDPage() {
return
}
================================================
FILE: app/[locale]/[workspaceid]/chat/page.tsx
================================================
"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 ? (
) : (
)}
>
)
}
================================================
FILE: app/[locale]/[workspaceid]/layout.tsx
================================================
"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}
}
================================================
FILE: app/[locale]/[workspaceid]/page.tsx
================================================
"use client"
import { ChatbotUIContext } from "@/context/context"
import { useContext } from "react"
export default function WorkspacePage() {
const { selectedWorkspace } = useContext(ChatbotUIContext)
return (
{selectedWorkspace?.name}
)
}
================================================
FILE: app/[locale]/globals.css
================================================
@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;
}
}
================================================
FILE: app/[locale]/help/page.tsx
================================================
export default function HelpPage() {
return (
)
}
================================================
FILE: app/[locale]/layout.tsx
================================================
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}
)
}
================================================
FILE: app/[locale]/loading.tsx
================================================
import { IconLoader2 } from "@tabler/icons-react"
export default function Loading() {
return (
)
}
================================================
FILE: app/[locale]/login/page.tsx
================================================
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 (
)
}
================================================
FILE: app/[locale]/login/password/page.tsx
================================================
"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
}
================================================
FILE: app/[locale]/page.tsx
================================================
"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
)
}
================================================
FILE: app/[locale]/setup/page.tsx
================================================
"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)}
)
}
================================================
FILE: app/api/assistants/openai/route.ts
================================================
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
})
}
}
================================================
FILE: app/api/chat/anthropic/route.ts
================================================
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
})
}
}
================================================
FILE: app/api/chat/azure/route.ts
================================================
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
})
}
}
================================================
FILE: app/api/chat/custom/route.ts
================================================
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
})
}
}
================================================
FILE: app/api/chat/google/route.ts
================================================
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
})
}
}
================================================
FILE: app/api/chat/groq/route.ts
================================================
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
})
}
}
================================================
FILE: app/api/chat/mistral/route.ts
================================================
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
})
}
}
================================================
FILE: app/api/chat/openai/route.ts
================================================
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
})
}
}
================================================
FILE: app/api/chat/openrouter/route.ts
================================================
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
})
}
}
================================================
FILE: app/api/chat/perplexity/route.ts
================================================
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
})
}
}
================================================
FILE: app/api/chat/tools/route.ts
================================================
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
})
}
}
================================================
FILE: app/api/command/route.ts
================================================
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
})
}
}
================================================
FILE: app/api/keys/route.ts
================================================
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)
}
================================================
FILE: app/api/retrieval/process/docx/route.ts
================================================
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
})
}
}
================================================
FILE: app/api/retrieval/process/route.ts
================================================
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
})
}
}
================================================
FILE: app/api/retrieval/retrieve/route.ts
================================================
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
})
}
}
================================================
FILE: app/api/username/available/route.ts
================================================
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
})
}
}
================================================
FILE: app/api/username/get/route.ts
================================================
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
})
}
}
================================================
FILE: app/auth/callback/route.ts
================================================
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)
}
}
================================================
FILE: components/chat/assistant-picker.tsx
================================================
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."}
))}
>
)}
)}
>
)
}
================================================
FILE: components/chat/chat-command-input.tsx
================================================
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}
/>
>
)
}
================================================
FILE: components/chat/chat-files-display.tsx
================================================
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" ? (
) : (
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)
}}
/>
}
/>
)
}
================================================
FILE: components/chat/chat-help.tsx
================================================
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
Show Workspaces
New Chat
Focus Chat
Toggle Files
Toggle Retrieval
Open Settings
Open Quick Settings
Toggle Sidebar
)
}
================================================
FILE: components/chat/chat-helpers/index.ts
================================================
// 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)
}
}
================================================
FILE: components/chat/chat-hooks/use-chat-handler.tsx
================================================
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
}
}
================================================
FILE: components/chat/chat-hooks/use-chat-history.tsx
================================================
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
}
}
================================================
FILE: components/chat/chat-hooks/use-prompt-and-command.tsx
================================================
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
}
}
================================================
FILE: components/chat/chat-hooks/use-scroll.tsx
================================================
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
}
}
================================================
FILE: components/chat/chat-hooks/use-select-file-handler.tsx
================================================
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
}
}
================================================
FILE: components/chat/chat-input.tsx
================================================
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}
/>
)}
>
)
}
================================================
FILE: components/chat/chat-messages.tsx
================================================
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}
/>
)
})
}
================================================
FILE: components/chat/chat-retrieval-settings.tsx
================================================
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
)
}
================================================
FILE: components/chat/chat-scroll-buttons.tsx
================================================
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 && (
)}
>
)
}
================================================
FILE: components/chat/chat-secondary-buttons.tsx
================================================
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={
}
/>
>
)}
>
)
}
================================================
FILE: components/chat/chat-settings.tsx
================================================
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}
)
}
================================================
FILE: components/chat/chat-ui.tsx
================================================
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"}
)
}
================================================
FILE: components/chat/file-picker.tsx
================================================
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."}
))}
>
)}
)}
>
)
}
================================================
FILE: components/chat/prompt-picker.tsx
================================================
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}
))
)}
)}
>
)
}
================================================
FILE: components/chat/quick-setting-option.tsx
================================================
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}
)
}
================================================
FILE: components/chat/quick-settings.tsx
================================================
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 || ""
: ""
}
/>
))}
>
)}
)
}
================================================
FILE: components/chat/tool-picker.tsx
================================================
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."}
))}
>
)}
)}
>
)
}
================================================
FILE: components/icons/anthropic-svg.tsx
================================================
import { FC } from "react"
interface AnthropicSVGProps {
height?: number
width?: number
className?: string
}
export const AnthropicSVG: FC = ({
height = 40,
width = 40,
className
}) => {
return (
)
}
================================================
FILE: components/icons/chatbotui-svg.tsx
================================================
import { FC } from "react"
interface ChatbotUISVGProps {
theme: "dark" | "light"
scale?: number
}
export const ChatbotUISVG: FC = ({ theme, scale = 1 }) => {
return (
)
}
================================================
FILE: components/icons/google-svg.tsx
================================================
import { FC } from "react"
interface GoogleSVGProps {
height?: number
width?: number
className?: string
}
export const GoogleSVG: FC = ({
height = 40,
width = 40,
className
}) => {
return (
)
}
================================================
FILE: components/icons/openai-svg.tsx
================================================
import { FC } from "react"
interface OpenAISVGProps {
height?: number
width?: number
className?: string
}
export const OpenAISVG: FC = ({
height = 40,
width = 40,
className
}) => {
return (
)
}
================================================
FILE: components/messages/message-actions.tsx
================================================
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 && } */}
)
}
================================================
FILE: components/messages/message-codeblock.tsx
================================================
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"
================================================
FILE: components/messages/message-markdown-memoized.tsx
================================================
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
)
================================================
FILE: components/messages/message-markdown.tsx
================================================
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}
)
}
================================================
FILE: components/messages/message-replies.tsx
================================================
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={
}
/>
Are you sure absolutely sure?
This action cannot be undone. This will permanently delete your
account and remove your data from our servers.
)
}
================================================
FILE: components/messages/message.tsx
================================================
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 (
)
default:
return (
)
}
})()}
>
) : 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)
}}
/>
)}
)
}
================================================
FILE: components/models/model-icon.tsx
================================================
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
}
}
================================================
FILE: components/models/model-option.tsx
================================================
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={
}
/>
)
}
================================================
FILE: components/models/model-select.tsx
================================================
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)}
/>
)
})}
)
})}
)
}
================================================
FILE: components/setup/api-step.tsx
================================================
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 (
<>
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)}
/>
>
)
}
================================================
FILE: components/setup/finish-step.tsx
================================================
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.
)
}
================================================
FILE: components/setup/profile-step.tsx
================================================
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
)}
Chat Display Name
onDisplayNameChange(e.target.value)}
maxLength={PROFILE_DISPLAY_NAME_MAX}
/>
>
)
}
================================================
FILE: components/setup/step-container.tsx
================================================
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
)}
)
}
================================================
FILE: components/sidebar/items/all/sidebar-create-item.tsx
================================================
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"}
)
}
================================================
FILE: components/sidebar/items/all/sidebar-delete-item.tsx
================================================
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
)
}
================================================
FILE: components/sidebar/items/all/sidebar-display-item.tsx
================================================
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={
}
/>
)} */}
)
}
================================================
FILE: components/sidebar/items/all/sidebar-update-item.tsx
================================================
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 && (
)}
{renderInputs(renderState[contentType])}
setIsOpen(false)}>
Cancel
Save
)
}
================================================
FILE: components/sidebar/items/assistants/assistant-item.tsx
================================================
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)
}
/>
>
)}
/>
)
}
================================================
FILE: components/sidebar/items/assistants/assistant-retrieval-select.tsx
================================================
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 && (
)}
)
}
================================================
FILE: components/sidebar/items/assistants/assistant-tool-select.tsx
================================================
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 (
)
}
================================================
FILE: components/sidebar/items/assistants/create-assistant.tsx
================================================
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}
/>
{checkIfModelIsToolCompatible() ? (
) : (
Model is not compatible with tools.
)}
>
)}
onOpenChange={onOpenChange}
/>
)
}
================================================
FILE: components/sidebar/items/chat/chat-item.tsx
================================================
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"}`}
>
)
}
================================================
FILE: components/sidebar/items/chat/delete-chat.tsx
================================================
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
)
}
================================================
FILE: components/sidebar/items/chat/update-chat.tsx
================================================
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
)
}
================================================
FILE: components/sidebar/items/collections/collection-file-select.tsx
================================================
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 (
)
}
================================================
FILE: components/sidebar/items/collections/collection-item.tsx
================================================
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}
/>
>
)
}}
/>
)
}
================================================
FILE: components/sidebar/items/collections/create-collection.tsx
================================================
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}
/>
>
)}
/>
)
}
================================================
FILE: components/sidebar/items/files/create-file.tsx
================================================
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}
/>
>
)}
/>
)
}
================================================
FILE: components/sidebar/items/files/file-item.tsx
================================================
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}`
}
================================================
FILE: components/sidebar/items/folders/delete-folder.tsx
================================================
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
)
}
================================================
FILE: components/sidebar/items/folders/folder-item.tsx
================================================
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}
)}
)
}
================================================
FILE: components/sidebar/items/folders/update-folder.tsx
================================================
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
)
}
================================================
FILE: components/sidebar/items/models/create-model.tsx
================================================
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)}
/>
API Key
setApiKey(e.target.value)}
/>
Max Context Length
setContextLength(parseInt(e.target.value))}
/>
>
)}
/>
)
}
================================================
FILE: components/sidebar/items/models/model-item.tsx
================================================
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)}
/>
API Key
setApiKey(e.target.value)}
/>
Max Context Length
setContextLength(parseInt(e.target.value))}
/>
>
)}
/>
)
}
================================================
FILE: components/sidebar/items/presets/create-preset.tsx
================================================
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}
/>
>
)}
/>
)
}
================================================
FILE: components/sidebar/items/presets/preset-item.tsx
================================================
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}
/>
>
)}
/>
)
}
================================================
FILE: components/sidebar/items/prompts/create-prompt.tsx
================================================
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)}
/>
>
)}
/>
)
}
================================================
FILE: components/sidebar/items/prompts/prompt-item.tsx
================================================
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)}
/>
>
)}
/>
)
}
================================================
FILE: components/sidebar/items/tools/create-tool.tsx
================================================
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}
/>
)
}
================================================
FILE: components/sidebar/items/tools/tool-item.tsx
================================================
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}
>
)}
/>
)
}
================================================
FILE: components/sidebar/sidebar-content.tsx
================================================
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
)
}
================================================
FILE: components/sidebar/sidebar-create-buttons.tsx
================================================
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 && (
)}
)
}
================================================
FILE: components/sidebar/sidebar-data-list.tsx
================================================
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 && (
)}
{(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)}
)
})}
)}
)}
>
)
}
================================================
FILE: components/sidebar/sidebar-search.tsx
================================================
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)}
/>
)
}
================================================
FILE: components/sidebar/sidebar-switch-item.tsx
================================================
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}
}
/>
)
}
================================================
FILE: components/sidebar/sidebar-switcher.tsx
================================================
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={ }
/>
)
}
================================================
FILE: components/sidebar/sidebar.tsx
================================================
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
}
})()}
)
}
================================================
FILE: components/ui/accordion.tsx
================================================
"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 }
================================================
FILE: components/ui/advanced-settings.tsx
================================================
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}
)
}
================================================
FILE: components/ui/alert-dialog.tsx
================================================
"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
}
================================================
FILE: components/ui/alert.tsx
================================================
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 }
================================================
FILE: components/ui/aspect-ratio.tsx
================================================
"use client"
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
const AspectRatio = AspectRatioPrimitive.Root
export { AspectRatio }
================================================
FILE: components/ui/avatar.tsx
================================================
"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 }
================================================
FILE: components/ui/badge.tsx
================================================
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 }
================================================
FILE: components/ui/brand.tsx
================================================
"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
)
}
================================================
FILE: components/ui/button.tsx
================================================
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 }
================================================
FILE: components/ui/calendar.tsx
================================================
"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 }
================================================
FILE: components/ui/card.tsx
================================================
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 }
================================================
FILE: components/ui/chat-settings-form.tsx
================================================
"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
)}
)
}
================================================
FILE: components/ui/checkbox.tsx
================================================
"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 }
================================================
FILE: components/ui/collapsible.tsx
================================================
"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 }
================================================
FILE: components/ui/command.tsx
================================================
"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
}
================================================
FILE: components/ui/context-menu.tsx
================================================
"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
}
================================================
FILE: components/ui/dashboard.tsx
================================================
"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
)}
)
}
================================================
FILE: components/ui/dialog.tsx
================================================
"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
}
================================================
FILE: components/ui/dropdown-menu.tsx
================================================
"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
}
================================================
FILE: components/ui/file-icon.tsx
================================================
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
}
}
================================================
FILE: components/ui/file-preview.tsx
================================================
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 (