Showing preview only (921K chars total). Download the full file or copy to clipboard to get everything.
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.
<img src="./public/readme/screenshot.png" alt="Chatbot UI" width="600">
## 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/<YOUR_PROJECT_ID>/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 <project-id>
```
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 <ChatUI />
}
================================================
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 ? (
<div className="relative flex h-full flex-col items-center justify-center">
<div className="top-50% left-50% -translate-x-50% -translate-y-50% absolute mb-20">
<Brand theme={theme === "dark" ? "dark" : "light"} />
</div>
<div className="absolute left-2 top-2">
<QuickSettings />
</div>
<div className="absolute right-2 top-2">
<ChatSettings />
</div>
<div className="flex grow flex-col items-center justify-center" />
<div className="w-full min-w-[300px] items-end px-2 pb-3 pt-0 sm:w-[600px] sm:pb-8 sm:pt-5 md:w-[700px] lg:w-[700px] xl:w-[800px]">
<ChatInput />
</div>
<div className="absolute bottom-2 right-2 hidden md:block lg:bottom-4 lg:right-4">
<ChatHelp />
</div>
</div>
) : (
<ChatUI />
)}
</>
)
}
================================================
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 <Loading />
}
return <Dashboard>{children}</Dashboard>
}
================================================
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 (
<div className="flex h-screen w-full flex-col items-center justify-center">
<div className="text-4xl">{selectedWorkspace?.name}</div>
</div>
)
}
================================================
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 (
<div className="size-screen flex flex-col items-center justify-center">
<div className="text-4xl">Help under construction.</div>
</div>
)
}
================================================
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<Database>(
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 (
<html lang="en" suppressHydrationWarning>
<body className={inter.className}>
<Providers attribute="class" defaultTheme="dark">
<TranslationsProvider
namespaces={i18nNamespaces}
locale={locale}
resources={resources}
>
<Toaster richColors position="top-center" duration={3000} />
<div className="bg-background text-foreground flex h-dvh flex-col items-center overflow-x-auto">
{session ? <GlobalState>{children}</GlobalState> : children}
</div>
</TranslationsProvider>
</Providers>
</body>
</html>
)
}
================================================
FILE: app/[locale]/loading.tsx
================================================
import { IconLoader2 } from "@tabler/icons-react"
export default function Loading() {
return (
<div className="flex size-full flex-col items-center justify-center">
<IconLoader2 className="mt-4 size-12 animate-spin" />
</div>
)
}
================================================
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<Database>(
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<string>(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 (
<div className="flex w-full flex-1 flex-col justify-center gap-2 px-8 sm:max-w-md">
<form
className="animate-in text-foreground flex w-full flex-1 flex-col justify-center gap-2"
action={signIn}
>
<Brand />
<Label className="text-md mt-4" htmlFor="email">
Email
</Label>
<Input
className="mb-3 rounded-md border bg-inherit px-4 py-2"
name="email"
placeholder="you@example.com"
required
/>
<Label className="text-md" htmlFor="password">
Password
</Label>
<Input
className="mb-6 rounded-md border bg-inherit px-4 py-2"
type="password"
name="password"
placeholder="••••••••"
/>
<SubmitButton className="mb-2 rounded-md bg-blue-700 px-4 py-2 text-white">
Login
</SubmitButton>
<SubmitButton
formAction={signUp}
className="border-foreground/20 mb-2 rounded-md border px-4 py-2"
>
Sign Up
</SubmitButton>
<div className="text-muted-foreground mt-1 flex justify-center text-sm">
<span className="mr-1">Forgot your password?</span>
<button
formAction={handleResetPassword}
className="text-primary ml-1 underline hover:opacity-80"
>
Reset
</button>
</div>
{searchParams?.message && (
<p className="bg-foreground/10 text-foreground mt-4 p-4 text-center">
{searchParams.message}
</p>
)}
</form>
</div>
)
}
================================================
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 <ChangePassword />
}
================================================
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 (
<div className="flex size-full flex-col items-center justify-center">
<div>
<ChatbotUISVG theme={theme === "dark" ? "dark" : "light"} scale={0.3} />
</div>
<div className="mt-2 text-4xl font-bold">Chatbot UI</div>
<Link
className="mt-4 flex w-[200px] items-center justify-center rounded-md bg-blue-500 p-2 font-semibold"
href="/login"
>
Start Chatting
<IconArrowRight className="ml-1" size={20} />
</Link>
</div>
)
}
================================================
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 (
<StepContainer
stepDescription="Let's create your profile."
stepNum={currentStep}
stepTitle="Welcome to Chatbot UI"
onShouldProceed={handleShouldProceed}
showNextButton={!!(username && usernameAvailable)}
showBackButton={false}
>
<ProfileStep
username={username}
usernameAvailable={usernameAvailable}
displayName={displayName}
onUsernameAvailableChange={setUsernameAvailable}
onUsernameChange={setUsername}
onDisplayNameChange={setDisplayName}
/>
</StepContainer>
)
// API Step
case 2:
return (
<StepContainer
stepDescription="Enter API keys for each service you'd like to use."
stepNum={currentStep}
stepTitle="Set API Keys (optional)"
onShouldProceed={handleShouldProceed}
showNextButton={true}
showBackButton={true}
>
<APIStep
openaiAPIKey={openaiAPIKey}
openaiOrgID={openaiOrgID}
azureOpenaiAPIKey={azureOpenaiAPIKey}
azureOpenaiEndpoint={azureOpenaiEndpoint}
azureOpenai35TurboID={azureOpenai35TurboID}
azureOpenai45TurboID={azureOpenai45TurboID}
azureOpenai45VisionID={azureOpenai45VisionID}
azureOpenaiEmbeddingsID={azureOpenaiEmbeddingsID}
anthropicAPIKey={anthropicAPIKey}
googleGeminiAPIKey={googleGeminiAPIKey}
mistralAPIKey={mistralAPIKey}
groqAPIKey={groqAPIKey}
perplexityAPIKey={perplexityAPIKey}
useAzureOpenai={useAzureOpenai}
onOpenaiAPIKeyChange={setOpenaiAPIKey}
onOpenaiOrgIDChange={setOpenaiOrgID}
onAzureOpenaiAPIKeyChange={setAzureOpenaiAPIKey}
onAzureOpenaiEndpointChange={setAzureOpenaiEndpoint}
onAzureOpenai35TurboIDChange={setAzureOpenai35TurboID}
onAzureOpenai45TurboIDChange={setAzureOpenai45TurboID}
onAzureOpenai45VisionIDChange={setAzureOpenai45VisionID}
onAzureOpenaiEmbeddingsIDChange={setAzureOpenaiEmbeddingsID}
onAnthropicAPIKeyChange={setAnthropicAPIKey}
onGoogleGeminiAPIKeyChange={setGoogleGeminiAPIKey}
onMistralAPIKeyChange={setMistralAPIKey}
onGroqAPIKeyChange={setGroqAPIKey}
onPerplexityAPIKeyChange={setPerplexityAPIKey}
onUseAzureOpenaiChange={setUseAzureOpenai}
openrouterAPIKey={openrouterAPIKey}
onOpenrouterAPIKeyChange={setOpenrouterAPIKey}
/>
</StepContainer>
)
// Finish Step
case 3:
return (
<StepContainer
stepDescription="You are all set up!"
stepNum={currentStep}
stepTitle="Setup Complete"
onShouldProceed={handleShouldProceed}
showNextButton={true}
showBackButton={true}
>
<FinishStep displayName={displayName} />
</StepContainer>
)
default:
return null
}
}
if (loading) {
return null
}
return (
<div className="flex h-full items-center justify-center">
{renderStep(currentStep)}
</div>
)
}
================================================
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<Database>(
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<string, string>, 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<string, VALID_ENV_KEYS> = {
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<string, boolean>
>((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<Database>(
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<Database>(
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<Database>(
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<Database>(
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<Database>(
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<AssistantPickerProps> = ({}) => {
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<HTMLDivElement>) => {
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 && (
<div className="bg-background flex flex-col space-y-1 rounded-xl border-2 p-2 text-sm">
{filteredAssistants.length === 0 ? (
<div className="text-md flex h-14 cursor-pointer items-center justify-center italic hover:opacity-50">
No matching assistants.
</div>
) : (
<>
{filteredAssistants.map((item, index) => (
<div
key={item.id}
ref={ref => {
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
src={
assistantImages.find(
image => image.path === item.image_path
)?.url || ""
}
alt={item.name}
width={32}
height={32}
className="rounded"
/>
) : (
<IconRobotFace size={32} />
)}
<div className="ml-3 flex flex-col">
<div className="font-bold">{item.name}</div>
<div className="truncate text-sm opacity-80">
{item.description || "No description."}
</div>
</div>
</div>
))}
</>
)}
</div>
)}
</>
)
}
================================================
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<ChatCommandInputProps> = ({}) => {
const {
newMessageFiles,
chatFiles,
slashCommand,
isFilePickerOpen,
setIsFilePickerOpen,
hashtagCommand,
focusPrompt,
focusFile
} = useContext(ChatbotUIContext)
const { handleSelectUserFile, handleSelectUserCollection } =
usePromptAndCommand()
return (
<>
<PromptPicker />
<FilePicker
isOpen={isFilePickerOpen}
searchQuery={hashtagCommand}
onOpenChange={setIsFilePickerOpen}
selectedFileIds={[...newMessageFiles, ...chatFiles].map(
file => file.id
)}
selectedCollectionIds={[]}
onSelectFile={handleSelectUserFile}
onSelectCollection={handleSelectUserCollection}
isFocused={focusFile}
/>
<ToolPicker />
<AssistantPicker />
</>
)
}
================================================
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<ChatFilesDisplayProps> = ({}) => {
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<ChatFile | null>(null)
const [selectedImage, setSelectedImage] = useState<MessageImage | null>(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 && (
<FilePreview
type="image"
item={selectedImage}
isOpen={showPreview}
onOpenChange={(isOpen: boolean) => {
setShowPreview(isOpen)
setSelectedImage(null)
}}
/>
)}
{showPreview && selectedFile && (
<FilePreview
type="file"
item={selectedFile}
isOpen={showPreview}
onOpenChange={(isOpen: boolean) => {
setShowPreview(isOpen)
setSelectedFile(null)
}}
/>
)}
<div className="space-y-2">
<div className="flex w-full items-center justify-center">
<Button
className="flex h-[32px] w-[140px] space-x-2"
onClick={() => setShowFilesDisplay(false)}
>
<RetrievalToggle />
<div>Hide files</div>
<div onClick={e => e.stopPropagation()}>
<ChatRetrievalSettings />
</div>
</Button>
</div>
<div className="overflow-auto">
<div className="flex gap-2 overflow-auto pt-2">
{messageImages.map((image, index) => (
<div
key={index}
className="relative flex h-[64px] cursor-pointer items-center space-x-4 rounded-xl hover:opacity-50"
>
<Image
className="rounded"
// Force the image to be 56px by 56px
style={{
minWidth: "56px",
minHeight: "56px",
maxHeight: "56px",
maxWidth: "56px"
}}
src={image.base64} // Preview images will always be base64
alt="File image"
width={56}
height={56}
onClick={() => {
setSelectedImage(image)
setShowPreview(true)
}}
/>
<IconX
className="bg-muted-foreground border-primary absolute right-[-6px] top-[-2px] flex size-5 cursor-pointer items-center justify-center rounded-full border-DEFAULT text-[10px] hover:border-red-500 hover:bg-white hover:text-red-500"
onClick={e => {
e.stopPropagation()
setNewMessageImages(
newMessageImages.filter(
f => f.messageId !== image.messageId
)
)
setChatImages(
chatImages.filter(f => f.messageId !== image.messageId)
)
}}
/>
</div>
))}
{combinedChatFiles.map((file, index) =>
file.id === "loading" ? (
<div
key={index}
className="relative flex h-[64px] items-center space-x-4 rounded-xl border-2 px-4 py-3"
>
<div className="rounded bg-blue-500 p-2">
<IconLoader2 className="animate-spin" />
</div>
<div className="truncate text-sm">
<div className="truncate">{file.name}</div>
<div className="truncate opacity-50">{file.type}</div>
</div>
</div>
) : (
<div
key={file.id}
className="relative flex h-[64px] cursor-pointer items-center space-x-4 rounded-xl border-2 px-4 py-3 hover:opacity-50"
onClick={() => getLinkAndView(file)}
>
<div className="rounded bg-blue-500 p-2">
{(() => {
let fileExtension = file.type.includes("/")
? file.type.split("/")[1]
: file.type
switch (fileExtension) {
case "pdf":
return <IconFileTypePdf />
case "markdown":
return <IconMarkdown />
case "txt":
return <IconFileTypeTxt />
case "json":
return <IconJson />
case "csv":
return <IconFileTypeCsv />
case "docx":
return <IconFileTypeDocx />
default:
return <IconFileFilled />
}
})()}
</div>
<div className="truncate text-sm">
<div className="truncate">{file.name}</div>
</div>
<IconX
className="bg-muted-foreground border-primary absolute right-[-6px] top-[-6px] flex size-5 cursor-pointer items-center justify-center rounded-full border-DEFAULT text-[10px] hover:border-red-500 hover:bg-white hover:text-red-500"
onClick={e => {
e.stopPropagation()
setNewMessageFiles(
newMessageFiles.filter(f => f.id !== file.id)
)
setChatFiles(chatFiles.filter(f => f.id !== file.id))
}}
/>
</div>
)
)}
</div>
</div>
</div>
</>
) : (
combinedMessageFiles.length > 0 && (
<div className="flex w-full items-center justify-center space-x-2">
<Button
className="flex h-[32px] w-[140px] space-x-2"
onClick={() => setShowFilesDisplay(true)}
>
<RetrievalToggle />
<div>
{" "}
View {combinedMessageFiles.length} file
{combinedMessageFiles.length > 1 ? "s" : ""}
</div>
<div onClick={e => e.stopPropagation()}>
<ChatRetrievalSettings />
</div>
</Button>
</div>
)
)
}
const RetrievalToggle = ({}) => {
const { useRetrieval, setUseRetrieval } = useContext(ChatbotUIContext)
return (
<div className="flex items-center">
<WithTooltip
delayDuration={0}
side="top"
display={
<div>
{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."}
</div>
}
trigger={
<IconCircleFilled
className={cn(
"p-1",
useRetrieval ? "text-green-500" : "text-red-500",
useRetrieval ? "hover:text-green-200" : "hover:text-red-200"
)}
size={24}
onClick={e => {
e.stopPropagation()
setUseRetrieval(prev => !prev)
}}
/>
}
/>
</div>
)
}
================================================
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<ChatHelpProps> = ({}) => {
useHotkey("/", () => setIsOpen(prevState => !prevState))
const [isOpen, setIsOpen] = useState(false)
return (
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenuTrigger asChild>
<IconQuestionMark className="bg-primary text-secondary size-[24px] cursor-pointer rounded-full p-0.5 opacity-60 hover:opacity-50 lg:size-[30px] lg:p-1" />
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel className="flex items-center justify-between">
<div className="flex space-x-2">
<Link
className="cursor-pointer hover:opacity-50"
href="https://twitter.com/ChatbotUI"
target="_blank"
rel="noopener noreferrer"
>
<IconBrandX />
</Link>
<Link
className="cursor-pointer hover:opacity-50"
href="https://github.com/mckaywrigley/chatbot-ui"
target="_blank"
rel="noopener noreferrer"
>
<IconBrandGithub />
</Link>
</div>
<div className="flex space-x-2">
<Announcements />
<Link
className="cursor-pointer hover:opacity-50"
href="/help"
target="_blank"
rel="noopener noreferrer"
>
<IconHelpCircle size={24} />
</Link>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem className="flex justify-between">
<div>Show Help</div>
<div className="flex opacity-60">
<div className="min-w-[30px] rounded border-DEFAULT p-1 text-center">
⌘
</div>
<div className="min-w-[30px] rounded border-DEFAULT p-1 text-center">
Shift
</div>
<div className="min-w-[30px] rounded border-DEFAULT p-1 text-center">
/
</div>
</div>
</DropdownMenuItem>
<DropdownMenuItem className="flex justify-between">
<div>Show Workspaces</div>
<div className="flex opacity-60">
<div className="min-w-[30px] rounded border-DEFAULT p-1 text-center">
⌘
</div>
<div className="min-w-[30px] rounded border-DEFAULT p-1 text-center">
Shift
</div>
<div className="min-w-[30px] rounded border-DEFAULT p-1 text-center">
;
</div>
</div>
</DropdownMenuItem>
<DropdownMenuItem className="flex w-[300px] justify-between">
<div>New Chat</div>
<div className="flex opacity-60">
<div className="min-w-[30px] rounded border-DEFAULT p-1 text-center">
⌘
</div>
<div className="min-w-[30px] rounded border-DEFAULT p-1 text-center">
Shift
</div>
<div className="min-w-[30px] rounded border-DEFAULT p-1 text-center">
O
</div>
</div>
</DropdownMenuItem>
<DropdownMenuItem className="flex justify-between">
<div>Focus Chat</div>
<div className="flex opacity-60">
<div className="min-w-[30px] rounded border-DEFAULT p-1 text-center">
⌘
</div>
<div className="min-w-[30px] rounded border-DEFAULT p-1 text-center">
Shift
</div>
<div className="min-w-[30px] rounded border-DEFAULT p-1 text-center">
L
</div>
</div>
</DropdownMenuItem>
<DropdownMenuItem className="flex justify-between">
<div>Toggle Files</div>
<div className="flex opacity-60">
<div className="min-w-[30px] rounded border-DEFAULT p-1 text-center">
⌘
</div>
<div className="min-w-[30px] rounded border-DEFAULT p-1 text-center">
Shift
</div>
<div className="min-w-[30px] rounded border-DEFAULT p-1 text-center">
F
</div>
</div>
</DropdownMenuItem>
<DropdownMenuItem className="flex justify-between">
<div>Toggle Retrieval</div>
<div className="flex opacity-60">
<div className="min-w-[30px] rounded border-DEFAULT p-1 text-center">
⌘
</div>
<div className="min-w-[30px] rounded border-DEFAULT p-1 text-center">
Shift
</div>
<div className="min-w-[30px] rounded border-DEFAULT p-1 text-center">
E
</div>
</div>
</DropdownMenuItem>
<DropdownMenuItem className="flex justify-between">
<div>Open Settings</div>
<div className="flex opacity-60">
<div className="min-w-[30px] rounded border-DEFAULT p-1 text-center">
⌘
</div>
<div className="min-w-[30px] rounded border-DEFAULT p-1 text-center">
Shift
</div>
<div className="min-w-[30px] rounded border-DEFAULT p-1 text-center">
I
</div>
</div>
</DropdownMenuItem>
<DropdownMenuItem className="flex justify-between">
<div>Open Quick Settings</div>
<div className="flex opacity-60">
<div className="min-w-[30px] rounded border-DEFAULT p-1 text-center">
⌘
</div>
<div className="min-w-[30px] rounded border-DEFAULT p-1 text-center">
Shift
</div>
<div className="min-w-[30px] rounded border-DEFAULT p-1 text-center">
P
</div>
</div>
</DropdownMenuItem>
<DropdownMenuItem className="flex justify-between">
<div>Toggle Sidebar</div>
<div className="flex opacity-60">
<div className="min-w-[30px] rounded border-DEFAULT p-1 text-center">
⌘
</div>
<div className="min-w-[30px] rounded border-DEFAULT p-1 text-center">
Shift
</div>
<div className="min-w-[30px] rounded border-DEFAULT p-1 text-center">
S
</div>
</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
================================================
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<React.SetStateAction<ChatMessage[]>>,
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<React.SetStateAction<boolean>>,
setFirstTokenReceived: React.Dispatch<React.SetStateAction<boolean>>,
setChatMessages: React.Dispatch<React.SetStateAction<ChatMessage[]>>,
setToolInUse: React.Dispatch<React.SetStateAction<string>>
) => {
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<React.SetStateAction<boolean>>,
setFirstTokenReceived: React.Dispatch<React.SetStateAction<boolean>>,
setChatMessages: React.Dispatch<React.SetStateAction<ChatMessage[]>>,
setToolInUse: React.Dispatch<React.SetStateAction<string>>
) => {
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<React.SetStateAction<boolean>>,
setChatMessages: React.Dispatch<React.SetStateAction<ChatMessage[]>>
) => {
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<React.SetStateAction<boolean>>,
setChatMessages: React.Dispatch<React.SetStateAction<ChatMessage[]>>,
setToolInUse: React.Dispatch<React.SetStateAction<string>>
) => {
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<React.SetStateAction<Tables<"chats"> | null>>,
setChats: React.Dispatch<React.SetStateAction<Tables<"chats">[]>>,
setChatFiles: React.Dispatch<React.SetStateAction<ChatFile[]>>
) => {
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<React.SetStateAction<ChatMessage[]>>,
setChatFileItems: React.Dispatch<
React.SetStateAction<Tables<"file_items">[]>
>,
setChatImages: React.Dispatch<React.SetStateAction<MessageImage[]>>,
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<HTMLTextAreaElement>(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<number>(
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<HTMLDivElement>(null)
const messagesEndRef = useRef<HTMLDivElement>(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<HTMLDivElement> = 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<ChatInputProps> = ({}) => {
const { t } = useTranslation()
useHotkey("l", () => {
handleFocusChatInput()
})
const [isTyping, setIsTyping] = useState<boolean>(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<HTMLInputElement>(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 (
<>
<div className="flex flex-col flex-wrap justify-center gap-2">
<ChatFilesDisplay />
{selectedTools &&
selectedTools.map((tool, index) => (
<div
key={index}
className="flex justify-center"
onClick={() =>
setSelectedTools(
selectedTools.filter(
selectedTool => selectedTool.id !== tool.id
)
)
}
>
<div className="flex cursor-pointer items-center justify-center space-x-1 rounded-lg bg-purple-600 px-3 py-1 hover:opacity-50">
<IconBolt size={20} />
<div>{tool.name}</div>
</div>
</div>
))}
{selectedAssistant && (
<div className="border-primary mx-auto flex w-fit items-center space-x-2 rounded-lg border p-1.5">
{selectedAssistant.image_path && (
<Image
className="rounded"
src={
assistantImages.find(
img => img.path === selectedAssistant.image_path
)?.base64
}
width={28}
height={28}
alt={selectedAssistant.name}
/>
)}
<div className="text-sm font-bold">
Talking to {selectedAssistant.name}
</div>
</div>
)}
</div>
<div className="border-input relative mt-3 flex min-h-[60px] w-full items-center justify-center rounded-xl border-2">
<div className="absolute bottom-[76px] left-0 max-h-[300px] w-full overflow-auto rounded-xl dark:border-none">
<ChatCommandInput />
</div>
<>
<IconCirclePlus
className="absolute bottom-[12px] left-3 cursor-pointer p-1 hover:opacity-50"
size={32}
onClick={() => fileInputRef.current?.click()}
/>
{/* Hidden input to select files from device */}
<Input
ref={fileInputRef}
className="hidden"
type="file"
onChange={e => {
if (!e.target.files) return
handleSelectDeviceFile(e.target.files[0])
}}
accept={filesToAccept}
/>
</>
<TextareaAutosize
textareaRef={chatInputRef}
className="ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring text-md flex w-full resize-none rounded-md border-none bg-transparent px-14 py-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
placeholder={t(
// `Ask anything. Type "@" for assistants, "/" for prompts, "#" for files, and "!" for tools.`
`Ask anything. Type @ / # !`
)}
onValueChange={handleInputChange}
value={userInput}
minRows={1}
maxRows={18}
onKeyDown={handleKeyDown}
onPaste={handlePaste}
onCompositionStart={() => setIsTyping(true)}
onCompositionEnd={() => setIsTyping(false)}
/>
<div className="absolute bottom-[14px] right-3 cursor-pointer hover:opacity-50">
{isGenerating ? (
<IconPlayerStopFilled
className="hover:bg-background animate-pulse rounded bg-transparent p-1"
onClick={handleStopMessage}
size={30}
/>
) : (
<IconSend
className={cn(
"bg-primary text-secondary rounded p-1",
!userInput && "cursor-not-allowed opacity-50"
)}
onClick={() => {
if (!userInput) return
handleSendMessage(userInput, chatMessages, false)
}}
size={30}
/>
)}
</div>
</div>
</>
)
}
================================================
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<ChatMessagesProps> = ({}) => {
const { chatMessages, chatFileItems } = useContext(ChatbotUIContext)
const { handleSendEdit } = useChatHandler()
const [editingMessage, setEditingMessage] = useState<Tables<"messages">>()
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 (
<Message
key={chatMessage.message.sequence_number}
message={chatMessage.message}
fileItems={messageFileItems}
isEditing={editingMessage?.id === chatMessage.message.id}
isLast={index === array.length - 1}
onStartEdit={setEditingMessage}
onCancelEdit={() => 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<ChatRetrievalSettingsProps> = ({}) => {
const { sourceCount, setSourceCount } = useContext(ChatbotUIContext)
const [isOpen, setIsOpen] = useState(false)
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger>
<WithTooltip
delayDuration={0}
side="top"
display={<div>Adjust retrieval settings.</div>}
trigger={
<IconAdjustmentsHorizontal
className="cursor-pointer pt-[4px] hover:opacity-50"
size={24}
/>
}
/>
</DialogTrigger>
<DialogContent>
<div className="space-y-3">
<Label className="flex items-center space-x-1">
<div>Source Count:</div>
<div>{sourceCount}</div>
</Label>
<Slider
value={[sourceCount]}
onValueChange={values => {
setSourceCount(values[0])
}}
min={1}
max={10}
step={1}
/>
</div>
<DialogFooter>
<Button size="sm" onClick={() => setIsOpen(false)}>
Save & Close
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
================================================
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<ChatScrollButtonsProps> = ({
isAtTop,
isAtBottom,
isOverflowing,
scrollToTop,
scrollToBottom
}) => {
return (
<>
{!isAtTop && isOverflowing && (
<IconCircleArrowUpFilled
className="cursor-pointer opacity-50 hover:opacity-100"
size={32}
onClick={scrollToTop}
/>
)}
{!isAtBottom && isOverflowing && (
<IconCircleArrowDownFilled
className="cursor-pointer opacity-50 hover:opacity-100"
size={32}
onClick={scrollToBottom}
/>
)}
</>
)
}
================================================
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<ChatSecondaryButtonsProps> = ({}) => {
const { selectedChat } = useContext(ChatbotUIContext)
const { handleNewChat } = useChatHandler()
return (
<>
{selectedChat && (
<>
<WithTooltip
delayDuration={200}
display={
<div>
<div className="text-xl font-bold">Chat Info</div>
<div className="mx-auto mt-2 max-w-xs space-y-2 sm:max-w-sm md:max-w-md lg:max-w-lg">
<div>Model: {selectedChat.model}</div>
<div>Prompt: {selectedChat.prompt}</div>
<div>Temperature: {selectedChat.temperature}</div>
<div>Context Length: {selectedChat.context_length}</div>
<div>
Profile Context:{" "}
{selectedChat.include_profile_context
? "Enabled"
: "Disabled"}
</div>
<div>
{" "}
Workspace Instructions:{" "}
{selectedChat.include_workspace_instructions
? "Enabled"
: "Disabled"}
</div>
<div>
Embeddings Provider: {selectedChat.embeddings_provider}
</div>
</div>
</div>
}
trigger={
<div className="mt-1">
<IconInfoCircle
className="cursor-default hover:opacity-50"
size={24}
/>
</div>
}
/>
<WithTooltip
delayDuration={200}
display={<div>Start a new chat</div>}
trigger={
<div className="mt-1">
<IconMessagePlus
className="cursor-pointer hover:opacity-50"
size={24}
onClick={handleNewChat}
/>
</div>
}
/>
</>
)}
</>
)
}
================================================
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<ChatSettingsProps> = ({}) => {
useHotkey("i", () => handleClick())
const {
chatSettings,
setChatSettings,
models,
availableHostedModels,
availableLocalModels,
availableOpenRouterModels
} = useContext(ChatbotUIContext)
const buttonRef = useRef<HTMLButtonElement>(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 (
<Popover>
<PopoverTrigger>
<Button
ref={buttonRef}
className="flex items-center space-x-2"
variant="ghost"
>
<div className="max-w-[120px] truncate text-lg sm:max-w-[300px] lg:max-w-[500px]">
{fullModel?.modelName || chatSettings.model}
</div>
<IconAdjustmentsHorizontal size={28} />
</Button>
</PopoverTrigger>
<PopoverContent
className="bg-background border-input relative flex max-h-[calc(100vh-60px)] w-[300px] flex-col space-y-4 overflow-auto rounded-lg border-2 p-6 sm:w-[350px] md:w-[400px] lg:w-[500px] dark:border-none"
align="end"
>
<ChatSettingsForm
chatSettings={chatSettings}
onChangeChatSettings={setChatSettings}
/>
</PopoverContent>
</Popover>
)
}
================================================
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<ChatUIProps> = ({}) => {
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<MessageImage>[] = 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 <Loading />
}
return (
<div className="relative flex h-full flex-col items-center">
<div className="absolute left-4 top-2.5 flex justify-center">
<ChatScrollButtons
isAtTop={isAtTop}
isAtBottom={isAtBottom}
isOverflowing={isOverflowing}
scrollToTop={scrollToTop}
scrollToBottom={scrollToBottom}
/>
</div>
<div className="absolute right-4 top-1 flex h-[40px] items-center space-x-2">
<ChatSecondaryButtons />
</div>
<div className="bg-secondary flex max-h-[50px] min-h-[50px] w-full items-center justify-center border-b-2 font-bold">
<div className="max-w-[200px] truncate sm:max-w-[400px] md:max-w-[500px] lg:max-w-[600px] xl:max-w-[700px]">
{selectedChat?.name || "Chat"}
</div>
</div>
<div
className="flex size-full flex-col overflow-auto border-b"
onScroll={handleScroll}
>
<div ref={messagesStartRef} />
<ChatMessages />
<div ref={messagesEndRef} />
</div>
<div className="relative w-full min-w-[300px] items-end px-2 pb-3 pt-0 sm:w-[600px] sm:pb-8 sm:pt-5 md:w-[700px] lg:w-[700px] xl:w-[800px]">
<ChatInput />
</div>
<div className="absolute bottom-2 right-2 hidden md:block lg:bottom-4 lg:right-4">
<ChatHelp />
</div>
</div>
)
}
================================================
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<FilePickerProps> = ({
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<HTMLDivElement>) => {
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 && (
<div className="bg-background flex flex-col space-y-1 rounded-xl border-2 p-2 text-sm">
{filteredFiles.length === 0 && filteredCollections.length === 0 ? (
<div className="text-md flex h-14 cursor-pointer items-center justify-center italic hover:opacity-50">
No matching files.
</div>
) : (
<>
{[...filteredFiles, ...filteredCollections].map((item, index) => (
<div
key={item.id}
ref={ref => {
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 ? (
<FileIcon type={(item as Tables<"files">).type} size={32} />
) : (
<IconBooks size={32} />
)}
<div className="ml-3 flex flex-col">
<div className="font-bold">{item.name}</div>
<div className="truncate text-sm opacity-80">
{item.description || "No description."}
</div>
</div>
</div>
))}
</>
)}
</div>
)}
</>
)
}
================================================
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<PromptPickerProps> = ({}) => {
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<HTMLDivElement>) => {
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()
item
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
SYMBOL INDEX (379 symbols across 197 files)
FILE: app/[locale]/[workspaceid]/chat/[chatid]/page.tsx
function ChatIDPage (line 5) | function ChatIDPage() {
FILE: app/[locale]/[workspaceid]/chat/page.tsx
function ChatPage (line 15) | function ChatPage() {
FILE: app/[locale]/[workspaceid]/layout.tsx
type WorkspaceLayoutProps (line 23) | interface WorkspaceLayoutProps {
function WorkspaceLayout (line 27) | function WorkspaceLayout({ children }: WorkspaceLayoutProps) {
FILE: app/[locale]/[workspaceid]/page.tsx
function WorkspacePage (line 6) | function WorkspacePage() {
FILE: app/[locale]/help/page.tsx
function HelpPage (line 1) | function HelpPage() {
FILE: app/[locale]/layout.tsx
constant APP_NAME (line 15) | const APP_NAME = "Chatbot UI"
constant APP_DEFAULT_TITLE (line 16) | const APP_DEFAULT_TITLE = "Chatbot UI"
constant APP_TITLE_TEMPLATE (line 17) | const APP_TITLE_TEMPLATE = "%s - Chatbot UI"
constant APP_DESCRIPTION (line 18) | const APP_DESCRIPTION = "Chabot UI PWA!"
type RootLayoutProps (line 20) | interface RootLayoutProps {
function RootLayout (line 69) | async function RootLayout({
FILE: app/[locale]/loading.tsx
function Loading (line 3) | function Loading() {
FILE: app/[locale]/login/page.tsx
function Login (line 17) | async function Login({
FILE: app/[locale]/login/password/page.tsx
function ChangePasswordPage (line 8) | function ChangePasswordPage() {
FILE: app/[locale]/page.tsx
function HomePage (line 8) | function HomePage() {
FILE: app/[locale]/setup/page.tsx
function SetupPage (line 25) | function SetupPage() {
FILE: app/api/assistants/openai/route.ts
function GET (line 7) | async function GET() {
FILE: app/api/chat/anthropic/route.ts
function POST (line 11) | async function POST(request: NextRequest) {
FILE: app/api/chat/azure/route.ts
function POST (line 9) | async function POST(request: Request) {
FILE: app/api/chat/custom/route.ts
function POST (line 11) | async function POST(request: Request) {
FILE: app/api/chat/google/route.ts
function POST (line 7) | async function POST(request: Request) {
FILE: app/api/chat/groq/route.ts
function POST (line 8) | async function POST(request: Request) {
FILE: app/api/chat/mistral/route.ts
function POST (line 9) | async function POST(request: Request) {
FILE: app/api/chat/openai/route.ts
function POST (line 10) | async function POST(request: Request) {
FILE: app/api/chat/openrouter/route.ts
function POST (line 10) | async function POST(request: Request) {
FILE: app/api/chat/perplexity/route.ts
function POST (line 8) | async function POST(request: Request) {
FILE: app/api/chat/tools/route.ts
function POST (line 9) | async function POST(request: Request) {
FILE: app/api/command/route.ts
function POST (line 7) | async function POST(request: Request) {
FILE: app/api/keys/route.ts
function GET (line 6) | async function GET() {
FILE: app/api/retrieval/process/docx/route.ts
function POST (line 10) | async function POST(req: Request) {
FILE: app/api/retrieval/process/route.ts
function POST (line 16) | async function POST(req: Request) {
FILE: app/api/retrieval/retrieve/route.ts
function POST (line 7) | async function POST(request: Request) {
FILE: app/api/username/available/route.ts
function POST (line 6) | async function POST(request: Request) {
FILE: app/api/username/get/route.ts
function POST (line 6) | async function POST(request: Request) {
FILE: app/auth/callback/route.ts
function GET (line 5) | async function GET(request: Request) {
FILE: components/chat/assistant-picker.tsx
type AssistantPickerProps (line 8) | interface AssistantPickerProps {}
FILE: components/chat/chat-command-input.tsx
type ChatCommandInputProps (line 9) | interface ChatCommandInputProps {}
FILE: components/chat/chat-files-display.tsx
type ChatFilesDisplayProps (line 25) | interface ChatFilesDisplayProps {}
FILE: components/chat/chat-help.tsx
type ChatHelpProps (line 20) | interface ChatHelpProps {}
FILE: components/chat/chat-hooks/use-select-file-handler.tsx
constant ACCEPTED_FILE_TYPES (line 8) | const ACCEPTED_FILE_TYPES = [
FILE: components/chat/chat-input.tsx
type ChatInputProps (line 24) | interface ChatInputProps {}
FILE: components/chat/chat-messages.tsx
type ChatMessagesProps (line 7) | interface ChatMessagesProps {}
FILE: components/chat/chat-retrieval-settings.tsx
type ChatRetrievalSettingsProps (line 15) | interface ChatRetrievalSettingsProps {}
FILE: components/chat/chat-scroll-buttons.tsx
type ChatScrollButtonsProps (line 7) | interface ChatScrollButtonsProps {
FILE: components/chat/chat-secondary-buttons.tsx
type ChatSecondaryButtonsProps (line 7) | interface ChatSecondaryButtonsProps {}
FILE: components/chat/chat-settings.tsx
type ChatSettingsProps (line 11) | interface ChatSettingsProps {}
FILE: components/chat/chat-ui.tsx
type ChatUIProps (line 22) | interface ChatUIProps {}
FILE: components/chat/file-picker.tsx
type FilePickerProps (line 7) | interface FilePickerProps {
FILE: components/chat/prompt-picker.tsx
type PromptPickerProps (line 10) | interface PromptPickerProps {}
FILE: components/chat/quick-setting-option.tsx
type QuickSettingOptionProps (line 9) | interface QuickSettingOptionProps {
FILE: components/chat/quick-settings.tsx
type QuickSettingsProps (line 25) | interface QuickSettingsProps {}
FILE: components/chat/tool-picker.tsx
type ToolPickerProps (line 7) | interface ToolPickerProps {}
FILE: components/icons/anthropic-svg.tsx
type AnthropicSVGProps (line 3) | interface AnthropicSVGProps {
FILE: components/icons/chatbotui-svg.tsx
type ChatbotUISVGProps (line 3) | interface ChatbotUISVGProps {
FILE: components/icons/google-svg.tsx
type GoogleSVGProps (line 3) | interface GoogleSVGProps {
FILE: components/icons/openai-svg.tsx
type OpenAISVGProps (line 3) | interface OpenAISVGProps {
FILE: components/messages/message-actions.tsx
constant MESSAGE_ICON_SIZE (line 6) | const MESSAGE_ICON_SIZE = 18
type MessageActionsProps (line 8) | interface MessageActionsProps {
FILE: components/messages/message-codeblock.tsx
type MessageCodeBlockProps (line 8) | interface MessageCodeBlockProps {
type languageMap (line 13) | interface languageMap {
FILE: components/messages/message-markdown.tsx
type MessageMarkdownProps (line 7) | interface MessageMarkdownProps {
method p (line 17) | p({ children }) {
method img (line 20) | img({ node, ...props }) {
method code (line 23) | code({ node, className, children, ...props }) {
FILE: components/messages/message-replies.tsx
type MessageRepliesProps (line 14) | interface MessageRepliesProps {}
FILE: components/messages/message.tsx
constant ICON_SIZE (line 27) | const ICON_SIZE = 32
type MessageProps (line 29) | interface MessageProps {
FILE: components/models/model-icon.tsx
type ModelIconProps (line 14) | interface ModelIconProps extends HTMLAttributes<HTMLDivElement> {
FILE: components/models/model-option.tsx
type ModelOptionProps (line 7) | interface ModelOptionProps {
FILE: components/models/model-select.tsx
type ModelSelectProps (line 16) | interface ModelSelectProps {
FILE: components/setup/api-step.tsx
type APIStepProps (line 6) | interface APIStepProps {
FILE: components/setup/finish-step.tsx
type FinishStepProps (line 3) | interface FinishStepProps {
FILE: components/setup/profile-step.tsx
type ProfileStepProps (line 17) | interface ProfileStepProps {
FILE: components/setup/step-container.tsx
constant SETUP_STEP_COUNT (line 12) | const SETUP_STEP_COUNT = 3
type StepContainerProps (line 14) | interface StepContainerProps {
FILE: components/sidebar/items/all/sidebar-create-item.tsx
type SidebarCreateItemProps (line 32) | interface SidebarCreateItemProps {
FILE: components/sidebar/items/all/sidebar-delete-item.tsx
type SidebarDeleteItemProps (line 25) | interface SidebarDeleteItemProps {
FILE: components/sidebar/items/all/sidebar-display-item.tsx
type SidebarItemProps (line 10) | interface SidebarItemProps {
FILE: components/sidebar/items/all/sidebar-update-item.tsx
type SidebarUpdateItemProps (line 88) | interface SidebarUpdateItemProps {
FILE: components/sidebar/items/assistants/assistant-item.tsx
type AssistantItemProps (line 16) | interface AssistantItemProps {
FILE: components/sidebar/items/assistants/assistant-retrieval-select.tsx
type AssistantRetrievalSelectProps (line 18) | interface AssistantRetrievalSelectProps {
type AssistantRetrievalOptionItemProps (line 156) | interface AssistantRetrievalOptionItemProps {
FILE: components/sidebar/items/assistants/assistant-tool-select.tsx
type AssistantToolSelectProps (line 17) | interface AssistantToolSelectProps {
type AssistantToolItemProps (line 128) | interface AssistantToolItemProps {
FILE: components/sidebar/items/assistants/create-assistant.tsx
type CreateAssistantProps (line 13) | interface CreateAssistantProps {
FILE: components/sidebar/items/chat/chat-item.tsx
type ChatItemProps (line 15) | interface ChatItemProps {
FILE: components/sidebar/items/chat/delete-chat.tsx
type DeleteChatProps (line 19) | interface DeleteChatProps {
FILE: components/sidebar/items/chat/update-chat.tsx
type UpdateChatProps (line 18) | interface UpdateChatProps {
FILE: components/sidebar/items/collections/collection-file-select.tsx
type CollectionFileSelectProps (line 14) | interface CollectionFileSelectProps {
type CollectionFileItemProps (line 122) | interface CollectionFileItemProps {
FILE: components/sidebar/items/collections/collection-item.tsx
type CollectionItemProps (line 11) | interface CollectionItemProps {
FILE: components/sidebar/items/collections/create-collection.tsx
type CreateCollectionProps (line 11) | interface CreateCollectionProps {
FILE: components/sidebar/items/files/create-file.tsx
type CreateFileProps (line 10) | interface CreateFileProps {
FILE: components/sidebar/items/files/file-item.tsx
type FileItemProps (line 10) | interface FileItemProps {
FILE: components/sidebar/items/folders/delete-folder.tsx
type DeleteFolderProps (line 20) | interface DeleteFolderProps {
FILE: components/sidebar/items/folders/folder-item.tsx
type FolderProps (line 9) | interface FolderProps {
FILE: components/sidebar/items/folders/update-folder.tsx
type UpdateFolderProps (line 18) | interface UpdateFolderProps {
FILE: components/sidebar/items/models/create-model.tsx
type CreateModelProps (line 9) | interface CreateModelProps {
FILE: components/sidebar/items/models/model-item.tsx
type ModelItemProps (line 9) | interface ModelItemProps {
FILE: components/sidebar/items/presets/create-preset.tsx
type CreatePresetProps (line 10) | interface CreatePresetProps {
FILE: components/sidebar/items/presets/preset-item.tsx
type PresetItemProps (line 11) | interface PresetItemProps {
FILE: components/sidebar/items/prompts/create-prompt.tsx
type CreatePromptProps (line 10) | interface CreatePromptProps {
FILE: components/sidebar/items/prompts/prompt-item.tsx
type PromptItemProps (line 10) | interface PromptItemProps {
FILE: components/sidebar/items/tools/create-tool.tsx
type CreateToolProps (line 11) | interface CreateToolProps {
FILE: components/sidebar/items/tools/tool-item.tsx
type ToolItemProps (line 11) | interface ToolItemProps {
FILE: components/sidebar/sidebar-content.tsx
type SidebarContentProps (line 8) | interface SidebarContentProps {
FILE: components/sidebar/sidebar-create-buttons.tsx
type SidebarCreateButtonsProps (line 16) | interface SidebarCreateButtonsProps {
FILE: components/sidebar/sidebar-data-list.tsx
type SidebarDataListProps (line 25) | interface SidebarDataListProps {
FILE: components/sidebar/sidebar-search.tsx
type SidebarSearchProps (line 5) | interface SidebarSearchProps {
FILE: components/sidebar/sidebar-switch-item.tsx
type SidebarSwitchItemProps (line 6) | interface SidebarSwitchItemProps {
FILE: components/sidebar/sidebar-switcher.tsx
constant SIDEBAR_ICON_SIZE (line 18) | const SIDEBAR_ICON_SIZE = 28
type SidebarSwitcherProps (line 20) | interface SidebarSwitcherProps {
FILE: components/sidebar/sidebar.tsx
type SidebarProps (line 11) | interface SidebarProps {
FILE: components/ui/advanced-settings.tsx
type AdvancedSettingsProps (line 9) | interface AdvancedSettingsProps {
FILE: components/ui/badge.tsx
type BadgeProps (line 26) | interface BadgeProps
function Badge (line 30) | function Badge({ className, variant, ...props }: BadgeProps) {
FILE: components/ui/brand.tsx
type BrandProps (line 7) | interface BrandProps {
FILE: components/ui/button.tsx
type ButtonProps (line 36) | interface ButtonProps
FILE: components/ui/calendar.tsx
type CalendarProps (line 10) | type CalendarProps = React.ComponentProps<typeof DayPicker>
function Calendar (line 12) | function Calendar({
FILE: components/ui/chat-settings-form.tsx
type ChatSettingsFormProps (line 23) | interface ChatSettingsFormProps {
type AdvancedContentProps (line 89) | interface AdvancedContentProps {
function findOpenRouterModel (line 107) | function findOpenRouterModel(modelId: string) {
FILE: components/ui/command.tsx
type CommandDialogProps (line 26) | interface CommandDialogProps extends DialogProps {}
FILE: components/ui/dashboard.tsx
constant SIDEBAR_WIDTH (line 16) | const SIDEBAR_WIDTH = 350
type DashboardProps (line 18) | interface DashboardProps {
FILE: components/ui/file-icon.tsx
type FileIconProps (line 13) | interface FileIconProps {
FILE: components/ui/file-preview.tsx
type FilePreviewProps (line 10) | interface FilePreviewProps {
FILE: components/ui/form.tsx
type FormFieldContextValue (line 18) | type FormFieldContextValue<
type FormItemContextValue (line 65) | type FormItemContextValue = {
FILE: components/ui/image-picker.tsx
type ImagePickerProps (line 6) | interface ImagePickerProps {
FILE: components/ui/input.tsx
type InputProps (line 5) | interface InputProps
FILE: components/ui/limit-display.tsx
type LimitDisplayProps (line 3) | interface LimitDisplayProps {
FILE: components/ui/screen-loader.tsx
type ScreenLoaderProps (line 4) | interface ScreenLoaderProps {}
FILE: components/ui/sheet.tsx
type SheetContentProps (line 51) | interface SheetContentProps
FILE: components/ui/skeleton.tsx
function Skeleton (line 3) | function Skeleton({
FILE: components/ui/sonner.tsx
type ToasterProps (line 6) | type ToasterProps = React.ComponentProps<typeof Sonner>
FILE: components/ui/textarea-autosize.tsx
type TextareaAutosizeProps (line 5) | interface TextareaAutosizeProps {
FILE: components/ui/textarea.tsx
type TextareaProps (line 5) | interface TextareaProps
FILE: components/ui/toast.tsx
type ToastProps (line 113) | type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastActionElement (line 115) | type ToastActionElement = React.ReactElement<typeof ToastAction>
FILE: components/ui/toaster.tsx
function Toaster (line 13) | function Toaster() {
FILE: components/ui/use-toast.ts
constant TOAST_LIMIT (line 6) | const TOAST_LIMIT = 1
constant TOAST_REMOVE_DELAY (line 7) | const TOAST_REMOVE_DELAY = 1000000
type ToasterToast (line 9) | type ToasterToast = ToastProps & {
function genId (line 25) | function genId() {
type ActionType (line 30) | type ActionType = typeof actionTypes
type Action (line 32) | type Action =
type State (line 50) | interface State {
function dispatch (line 131) | function dispatch(action: Action) {
type Toast (line 138) | type Toast = Omit<ToasterToast, "id">
function toast (line 140) | function toast({ ...props }: Toast) {
function useToast (line 169) | function useToast() {
FILE: components/ui/with-tooltip.tsx
type WithTooltipProps (line 9) | interface WithTooltipProps {
FILE: components/utility/alerts.tsx
type AlertsProps (line 10) | interface AlertsProps {}
FILE: components/utility/announcements.tsx
type AnnouncementsProps (line 12) | interface AnnouncementsProps {}
FILE: components/utility/change-password.tsx
type ChangePasswordProps (line 17) | interface ChangePasswordProps {}
FILE: components/utility/command-k.tsx
type CommandKProps (line 8) | interface CommandKProps {}
FILE: components/utility/drawing-canvas.tsx
type DrawingCanvasProps (line 5) | interface DrawingCanvasProps {
FILE: components/utility/global-state.tsx
type GlobalStateProps (line 31) | interface GlobalStateProps {
FILE: components/utility/import.tsx
type ImportProps (line 24) | interface ImportProps {}
FILE: components/utility/profile-settings.tsx
type ProfileSettingsProps (line 46) | interface ProfileSettingsProps {}
FILE: components/utility/theme-switcher.tsx
type ThemeSwitcherProps (line 7) | interface ThemeSwitcherProps {}
FILE: components/utility/translations-provider.tsx
function TranslationsProvider (line 7) | function TranslationsProvider({
FILE: components/utility/workspace-switcher.tsx
type WorkspaceSwitcherProps (line 20) | interface WorkspaceSwitcherProps {}
FILE: components/workspace/assign-workspaces.tsx
type AssignWorkspaces (line 14) | interface AssignWorkspaces {
type WorkspaceItemProps (line 124) | interface WorkspaceItemProps {
FILE: components/workspace/delete-workspace.tsx
type DeleteWorkspaceProps (line 19) | interface DeleteWorkspaceProps {
FILE: components/workspace/workspace-settings.tsx
type WorkspaceSettingsProps (line 31) | interface WorkspaceSettingsProps {}
FILE: context/context.tsx
type ChatbotUIContext (line 15) | interface ChatbotUIContext {
FILE: db/limits.ts
constant PROFILE_BIO_MAX (line 2) | const PROFILE_BIO_MAX = 500
constant PROFILE_DISPLAY_NAME_MAX (line 3) | const PROFILE_DISPLAY_NAME_MAX = 100
constant PROFILE_CONTEXT_MAX (line 4) | const PROFILE_CONTEXT_MAX = 1500
constant PROFILE_USERNAME_MIN (line 5) | const PROFILE_USERNAME_MIN = 3
constant PROFILE_USERNAME_MAX (line 6) | const PROFILE_USERNAME_MAX = 25
constant WORKSPACE_NAME_MAX (line 9) | const WORKSPACE_NAME_MAX = 100
constant WORKSPACE_DESCRIPTION_MAX (line 10) | const WORKSPACE_DESCRIPTION_MAX = 500
constant WORKSPACE_INSTRUCTIONS_MAX (line 11) | const WORKSPACE_INSTRUCTIONS_MAX = 1500
constant PRESET_NAME_MAX (line 16) | const PRESET_NAME_MAX = 100
constant PRESET_DESCRIPTION_MAX (line 17) | const PRESET_DESCRIPTION_MAX = 500
constant PRESET_PROMPT_MAX (line 18) | const PRESET_PROMPT_MAX = 100000
constant PROMPT_NAME_MAX (line 21) | const PROMPT_NAME_MAX = 100
constant PROMPT_CONTENT_MAX (line 22) | const PROMPT_CONTENT_MAX = 100000
constant FILE_NAME_MAX (line 25) | const FILE_NAME_MAX = 100
constant FILE_DESCRIPTION_MAX (line 26) | const FILE_DESCRIPTION_MAX = 500
constant COLLECTION_NAME_MAX (line 29) | const COLLECTION_NAME_MAX = 100
constant COLLECTION_DESCRIPTION_MAX (line 30) | const COLLECTION_DESCRIPTION_MAX = 500
constant ASSISTANT_NAME_MAX (line 33) | const ASSISTANT_NAME_MAX = 100
constant ASSISTANT_DESCRIPTION_MAX (line 34) | const ASSISTANT_DESCRIPTION_MAX = 500
constant ASSISTANT_PROMPT_MAX (line 35) | const ASSISTANT_PROMPT_MAX = 100000
constant TOOL_NAME_MAX (line 38) | const TOOL_NAME_MAX = 100
constant TOOL_DESCRIPTION_MAX (line 39) | const TOOL_DESCRIPTION_MAX = 500
constant MODEL_NAME_MAX (line 42) | const MODEL_NAME_MAX = 100
constant MODEL_DESCRIPTION_MAX (line 43) | const MODEL_DESCRIPTION_MAX = 500
FILE: db/messages.ts
function deleteMessagesIncludingAndAfter (line 86) | async function deleteMessagesIncludingAndAfter(
FILE: lib/build-prompt.ts
function buildFinalMessages (line 33) | async function buildFinalMessages(
function buildRetrievalText (line 178) | function buildRetrievalText(fileItems: Tables<"file_items">[]) {
function adaptSingleMessageForGoogleGemini (line 186) | function adaptSingleMessageForGoogleGemini(message: any) {
function adaptMessagesForGeminiVision (line 225) | function adaptMessagesForGeminiVision(
function adaptMessagesForGoogleGemini (line 245) | async function adaptMessagesForGoogleGemini(
FILE: lib/chat-setting-limits.ts
type ChatSettingLimits (line 3) | type ChatSettingLimits = {
constant CHAT_SETTING_LIMITS (line 10) | const CHAT_SETTING_LIMITS: Record<LLMID, ChatSettingLimits> = {
FILE: lib/consume-stream.ts
function consumeReadableStream (line 1) | async function consumeReadableStream(
FILE: lib/envs.ts
function isUsingEnvironmentKey (line 4) | function isUsingEnvironmentKey(type: EnvKey) {
FILE: lib/export-old-data.ts
function exportLocalStorageAsJSON (line 1) | function exportLocalStorageAsJSON() {
FILE: lib/generate-local-embedding.ts
function generateLocalEmbedding (line 3) | async function generateLocalEmbedding(content: string) {
FILE: lib/hooks/use-copy-to-clipboard.tsx
type useCopyToClipboardProps (line 3) | interface useCopyToClipboardProps {
function useCopyToClipboard (line 7) | function useCopyToClipboard({
FILE: lib/i18n.ts
function initTranslations (line 6) | async function initTranslations(
FILE: lib/models/llm/anthropic-llm-list.ts
constant ANTHROPIC_PLATFORM_LINK (line 3) | const ANTHROPIC_PLATFORM_LINK =
constant CLAUDE_2 (line 9) | const CLAUDE_2: LLM = {
constant CLAUDE_INSTANT (line 25) | const CLAUDE_INSTANT: LLM = {
constant CLAUDE_3_HAIKU (line 41) | const CLAUDE_3_HAIKU: LLM = {
constant CLAUDE_3_SONNET (line 57) | const CLAUDE_3_SONNET: LLM = {
constant CLAUDE_3_OPUS (line 73) | const CLAUDE_3_OPUS: LLM = {
constant CLAUDE_3_5_SONNET (line 89) | const CLAUDE_3_5_SONNET: LLM = {
constant ANTHROPIC_LLM_LIST (line 104) | const ANTHROPIC_LLM_LIST: LLM[] = [
FILE: lib/models/llm/google-llm-list.ts
constant GOOGLE_PLATORM_LINK (line 3) | const GOOGLE_PLATORM_LINK = "https://ai.google.dev/"
constant GEMINI_1_5_FLASH (line 8) | const GEMINI_1_5_FLASH: LLM = {
constant GEMINI_1_5_PRO (line 18) | const GEMINI_1_5_PRO: LLM = {
constant GEMINI_PRO (line 28) | const GEMINI_PRO: LLM = {
constant GEMINI_PRO_VISION (line 38) | const GEMINI_PRO_VISION: LLM = {
constant GOOGLE_LLM_LIST (line 47) | const GOOGLE_LLM_LIST: LLM[] = [GEMINI_PRO, GEMINI_PRO_VISION, GEMINI_1_...
FILE: lib/models/llm/groq-llm-list.ts
constant GROQ_PLATORM_LINK (line 3) | const GROQ_PLATORM_LINK = "https://groq.com/"
constant MIXTRAL_8X7B (line 35) | const MIXTRAL_8X7B: LLM = {
constant GEMMA_7B_IT (line 50) | const GEMMA_7B_IT: LLM = {
constant GROQ_LLM_LIST (line 65) | const GROQ_LLM_LIST: LLM[] = [
FILE: lib/models/llm/llm-list.ts
constant LLM_LIST (line 9) | const LLM_LIST: LLM[] = [
constant LLM_LIST_MAP (line 18) | const LLM_LIST_MAP: Record<string, LLM[]> = {
FILE: lib/models/llm/mistral-llm-list.ts
constant MISTRAL_PLATORM_LINK (line 3) | const MISTRAL_PLATORM_LINK = "https://docs.mistral.ai/"
constant MISTRAL_7B (line 8) | const MISTRAL_7B: LLM = {
constant MIXTRAL (line 18) | const MIXTRAL: LLM = {
constant MISTRAL_MEDIUM (line 34) | const MISTRAL_MEDIUM: LLM = {
constant MISTRAL_LARGE (line 50) | const MISTRAL_LARGE: LLM = {
constant MISTRAL_LLM_LIST (line 65) | const MISTRAL_LLM_LIST: LLM[] = [
FILE: lib/models/llm/openai-llm-list.ts
constant OPENAI_PLATORM_LINK (line 3) | const OPENAI_PLATORM_LINK = "https://platform.openai.com/docs/overview"
constant GPT4 (line 53) | const GPT4: LLM = {
constant OPENAI_LLM_LIST (line 84) | const OPENAI_LLM_LIST: LLM[] = [
FILE: lib/models/llm/perplexity-llm-list.ts
constant PERPLEXITY_PLATORM_LINK (line 3) | const PERPLEXITY_PLATORM_LINK =
constant MIXTRAL_8X7B_INSTRUCT (line 11) | const MIXTRAL_8X7B_INSTRUCT: LLM = {
constant MISTRAL_7B_INSTRUCT (line 21) | const MISTRAL_7B_INSTRUCT: LLM = {
constant CODELLAMA_70B_INSTRUCT (line 31) | const CODELLAMA_70B_INSTRUCT: LLM = {
constant PERPLEXITY_SONAR_SMALL_CHAT_7B (line 41) | const PERPLEXITY_SONAR_SMALL_CHAT_7B: LLM = {
constant PERPLEXITY_SONAR_SMALL_ONLINE_7B (line 51) | const PERPLEXITY_SONAR_SMALL_ONLINE_7B: LLM = {
constant PERPLEXITY_LLM_LIST (line 80) | const PERPLEXITY_LLM_LIST: LLM[] = [
FILE: lib/openapi-conversion.ts
type OpenAPIData (line 3) | interface OpenAPIData {
FILE: lib/retrieval/processing/index.ts
constant CHUNK_SIZE (line 8) | const CHUNK_SIZE = 4000
constant CHUNK_OVERLAP (line 9) | const CHUNK_OVERLAP = 200
FILE: lib/server/server-chat-helpers.ts
function getServerProfile (line 6) | async function getServerProfile() {
function addApiKeysToProfile (line 40) | function addApiKeysToProfile(profile: Tables<"profiles">) {
function checkApiKey (line 69) | function checkApiKey(apiKey: string | null, keyName: string) {
FILE: lib/server/server-utils.ts
function createResponse (line 1) | function createResponse(data: object, status: number): Response {
FILE: lib/supabase/middleware.ts
method get (line 17) | get(name: string) {
method set (line 20) | set(name: string, value: string, options: CookieOptions) {
method remove (line 38) | remove(name: string, options: CookieOptions) {
FILE: lib/supabase/server.ts
method get (line 10) | get(name: string) {
method set (line 13) | set(name: string, value: string, options: CookieOptions) {
method remove (line 22) | remove(name: string, options: CookieOptions) {
FILE: lib/utils.ts
function cn (line 4) | function cn(...inputs: ClassValue[]) {
function formatDate (line 8) | function formatDate(input: string | number | Date): string {
function getMediaTypeFromDataURL (line 17) | function getMediaTypeFromDataURL(dataURL: string): string | null {
function getBase64FromDataURL (line 22) | function getBase64FromDataURL(dataURL: string): string | null {
FILE: middleware.ts
function middleware (line 6) | async function middleware(request: NextRequest) {
FILE: supabase/migrations/20240108234540_setup.sql
function update_updated_at_column (line 8) | CREATE OR REPLACE FUNCTION update_updated_at_column()
function delete_message_including_and_after (line 17) | CREATE OR REPLACE FUNCTION delete_message_including_and_after(
function create_duplicate_messages_for_new_chat (line 30) | CREATE OR REPLACE FUNCTION create_duplicate_messages_for_new_chat(old_ch...
function delete_storage_object (line 47) | CREATE OR REPLACE FUNCTION delete_storage_object(bucket TEXT, object TEX...
function delete_storage_object_from_bucket (line 70) | CREATE OR REPLACE FUNCTION delete_storage_object_from_bucket(bucket_name...
FILE: supabase/migrations/20240108234541_add_profiles.sql
type profiles (line 5) | CREATE TABLE IF NOT EXISTS profiles (
type idx_profiles_user_id (line 42) | CREATE INDEX idx_profiles_user_id ON profiles (user_id)
FILE: supabase/migrations/20240108234542_add_workspaces.sql
type workspaces (line 5) | CREATE TABLE IF NOT EXISTS workspaces (
type idx_workspaces_user_id (line 35) | CREATE INDEX idx_workspaces_user_id ON workspaces (user_id)
function prevent_home_field_update (line 53) | CREATE OR REPLACE FUNCTION prevent_home_field_update()
function prevent_home_workspace_deletion (line 71) | CREATE OR REPLACE FUNCTION prevent_home_workspace_deletion()
type idx_unique_home_workspace_per_user (line 89) | CREATE UNIQUE INDEX idx_unique_home_workspace_per_user
FILE: supabase/migrations/20240108234543_add_folders.sql
type folders (line 5) | CREATE TABLE IF NOT EXISTS folders (
type folders_user_id_idx (line 25) | CREATE INDEX folders_user_id_idx ON folders(user_id)
type folders_workspace_id_idx (line 26) | CREATE INDEX folders_workspace_id_idx ON folders(workspace_id)
FILE: supabase/migrations/20240108234544_add_files.sql
type files (line 5) | CREATE TABLE IF NOT EXISTS files (
type files_user_id_idx (line 33) | CREATE INDEX files_user_id_idx ON files(user_id)
function delete_old_file (line 51) | CREATE OR REPLACE FUNCTION delete_old_file()
function public (line 92) | CREATE OR REPLACE FUNCTION public.non_private_file_exists(p_name text)
FILE: supabase/migrations/20240108234545_add_file_items.sql
type file_items (line 3) | create table file_items (
type file_items_file_id_idx (line 27) | CREATE INDEX file_items_file_id_idx ON file_items(file_id)
type file_items_embedding_idx (line 29) | CREATE INDEX file_items_embedding_idx ON file_items
type file_items_local_embedding_idx (line 32) | CREATE INDEX file_items_local_embedding_idx ON file_items
function match_file_items_local (line 60) | create function match_file_items_local (
function match_file_items_openai (line 89) | create function match_file_items_openai (
FILE: supabase/migrations/20240108234546_add_presets.sql
type presets (line 5) | CREATE TABLE IF NOT EXISTS presets (
type presets_user_id_idx (line 36) | CREATE INDEX presets_user_id_idx ON presets(user_id)
type preset_workspaces (line 63) | CREATE TABLE IF NOT EXISTS preset_workspaces (
type preset_workspaces_user_id_idx (line 78) | CREATE INDEX preset_workspaces_user_id_idx ON preset_workspaces(user_id)
type preset_workspaces_preset_id_idx (line 79) | CREATE INDEX preset_workspaces_preset_id_idx ON preset_workspaces(preset...
type preset_workspaces_workspace_id_idx (line 80) | CREATE INDEX preset_workspaces_workspace_id_idx ON preset_workspaces(wor...
FILE: supabase/migrations/20240108234547_add_assistants.sql
type assistants (line 5) | CREATE TABLE IF NOT EXISTS assistants (
type assistants_user_id_idx (line 37) | CREATE INDEX assistants_user_id_idx ON assistants(user_id)
function delete_old_assistant_image (line 55) | CREATE OR REPLACE FUNCTION delete_old_assistant_image()
function public (line 96) | CREATE OR REPLACE FUNCTION public.non_private_assistant_exists(p_name text)
FILE: supabase/migrations/20240108234548_add_chats.sql
type chats (line 5) | CREATE TABLE IF NOT EXISTS chats (
type idx_chats_user_id (line 37) | CREATE INDEX idx_chats_user_id ON chats (user_id)
type idx_chats_workspace_id (line 38) | CREATE INDEX idx_chats_workspace_id ON chats (workspace_id)
type chat_files (line 65) | CREATE TABLE IF NOT EXISTS chat_files (
type idx_chat_files_chat_id (line 80) | CREATE INDEX idx_chat_files_chat_id ON chat_files (chat_id)
FILE: supabase/migrations/20240108234549_add_messages.sql
type messages (line 5) | CREATE TABLE IF NOT EXISTS messages (
type idx_messages_chat_id (line 30) | CREATE INDEX idx_messages_chat_id ON messages (chat_id)
function delete_old_message_images (line 50) | CREATE OR REPLACE FUNCTION delete_old_message_images()
type message_file_items (line 142) | CREATE TABLE IF NOT EXISTS message_file_items (
type idx_message_file_items_message_id (line 157) | CREATE INDEX idx_message_file_items_message_id ON message_file_items (me...
FILE: supabase/migrations/20240108234550_add_prompts.sql
type prompts (line 5) | CREATE TABLE IF NOT EXISTS prompts (
type prompts_user_id_idx (line 29) | CREATE INDEX prompts_user_id_idx ON prompts(user_id)
type prompt_workspaces (line 56) | CREATE TABLE IF NOT EXISTS prompt_workspaces (
type prompt_workspaces_user_id_idx (line 71) | CREATE INDEX prompt_workspaces_user_id_idx ON prompt_workspaces(user_id)
type prompt_workspaces_prompt_id_idx (line 72) | CREATE INDEX prompt_workspaces_prompt_id_idx ON prompt_workspaces(prompt...
type prompt_workspaces_workspace_id_idx (line 73) | CREATE INDEX prompt_workspaces_workspace_id_idx ON prompt_workspaces(wor...
FILE: supabase/migrations/20240108234551_add_collections.sql
type collections (line 5) | CREATE TABLE IF NOT EXISTS collections (
type collections_user_id_idx (line 29) | CREATE INDEX collections_user_id_idx ON collections(user_id)
type collection_workspaces (line 56) | CREATE TABLE IF NOT EXISTS collection_workspaces (
type collection_workspaces_user_id_idx (line 71) | CREATE INDEX collection_workspaces_user_id_idx ON collection_workspaces(...
type collection_workspaces_collection_id_idx (line 72) | CREATE INDEX collection_workspaces_collection_id_idx ON collection_works...
type collection_workspaces_workspace_id_idx (line 73) | CREATE INDEX collection_workspaces_workspace_id_idx ON collection_worksp...
FILE: supabase/migrations/20240115171510_add_assistant_files.sql
type assistant_files (line 5) | CREATE TABLE IF NOT EXISTS assistant_files (
type assistant_files_user_id_idx (line 20) | CREATE INDEX assistant_files_user_id_idx ON assistant_files(user_id)
type assistant_files_assistant_id_idx (line 21) | CREATE INDEX assistant_files_assistant_id_idx ON assistant_files(assista...
type assistant_files_file_id_idx (line 22) | CREATE INDEX assistant_files_file_id_idx ON assistant_files(file_id)
FILE: supabase/migrations/20240115171524_add_tools.sql
type tools (line 5) | CREATE TABLE IF NOT EXISTS tools (
type tools_user_id_idx (line 31) | CREATE INDEX tools_user_id_idx ON tools(user_id)
type tool_workspaces (line 58) | CREATE TABLE IF NOT EXISTS tool_workspaces (
type tool_workspaces_user_id_idx (line 73) | CREATE INDEX tool_workspaces_user_id_idx ON tool_workspaces(user_id)
type tool_workspaces_tool_id_idx (line 74) | CREATE INDEX tool_workspaces_tool_id_idx ON tool_workspaces(tool_id)
type tool_workspaces_workspace_id_idx (line 75) | CREATE INDEX tool_workspaces_workspace_id_idx ON tool_workspaces(workspa...
FILE: supabase/migrations/20240115172125_add_assistant_tools.sql
type assistant_tools (line 5) | CREATE TABLE IF NOT EXISTS assistant_tools (
type assistant_tools_user_id_idx (line 20) | CREATE INDEX assistant_tools_user_id_idx ON assistant_tools(user_id)
type assistant_tools_assistant_id_idx (line 21) | CREATE INDEX assistant_tools_assistant_id_idx ON assistant_tools(assista...
type assistant_tools_tool_id_idx (line 22) | CREATE INDEX assistant_tools_tool_id_idx ON assistant_tools(tool_id)
FILE: supabase/migrations/20240125194719_add_custom_models.sql
type models (line 5) | CREATE TABLE IF NOT EXISTS models (
type models_user_id_idx (line 32) | CREATE INDEX models_user_id_idx ON models(user_id)
type model_workspaces (line 59) | CREATE TABLE IF NOT EXISTS model_workspaces (
type model_workspaces_user_id_idx (line 74) | CREATE INDEX model_workspaces_user_id_idx ON model_workspaces(user_id)
type model_workspaces_model_id_idx (line 75) | CREATE INDEX model_workspaces_model_id_idx ON model_workspaces(model_id)
type model_workspaces_workspace_id_idx (line 76) | CREATE INDEX model_workspaces_workspace_id_idx ON model_workspaces(works...
FILE: supabase/migrations/20240129232644_add_workspace_images.sql
function delete_old_workspace_image (line 12) | CREATE OR REPLACE FUNCTION delete_old_workspace_image()
function public (line 46) | CREATE OR REPLACE FUNCTION public.non_private_workspace_exists(p_name text)
FILE: supabase/types.ts
type Json (line 1) | type Json =
type Database (line 9) | type Database = {
type PublicSchema (line 1742) | type PublicSchema = Database[Extract<keyof Database, "public">]
type Tables (line 1744) | type Tables<
type TablesInsert (line 1769) | type TablesInsert<
type TablesUpdate (line 1790) | type TablesUpdate<
type Enums (line 1811) | type Enums<
FILE: types/announcement.ts
type Announcement (line 1) | interface Announcement {
FILE: types/assistant-retrieval-item.ts
type AssistantRetrievalItem (line 1) | interface AssistantRetrievalItem {
FILE: types/chat-file.tsx
type ChatFile (line 1) | interface ChatFile {
FILE: types/chat-message.ts
type ChatMessage (line 3) | interface ChatMessage {
FILE: types/chat.ts
type ChatSettings (line 4) | interface ChatSettings {
type ChatPayload (line 14) | interface ChatPayload {
type ChatAPIPayload (line 23) | interface ChatAPIPayload {
FILE: types/collection-file.ts
type CollectionFile (line 1) | interface CollectionFile {
FILE: types/content-type.ts
type ContentType (line 1) | type ContentType =
FILE: types/error-response.ts
type ErrorResponse (line 3) | type ErrorResponse = {
FILE: types/file-item-chunk.ts
type FileItemChunk (line 1) | type FileItemChunk = {
FILE: types/images/assistant-image.ts
type AssistantImage (line 1) | interface AssistantImage {
FILE: types/images/message-image.ts
type MessageImage (line 1) | interface MessageImage {
FILE: types/images/workspace-image.ts
type WorkspaceImage (line 1) | interface WorkspaceImage {
FILE: types/key-type.ts
type EnvKey (line 1) | type EnvKey =
FILE: types/llms.ts
type LLMID (line 3) | type LLMID =
type OpenAILLMID (line 12) | type OpenAILLMID =
type GoogleLLMID (line 20) | type GoogleLLMID =
type AnthropicLLMID (line 27) | type AnthropicLLMID =
type MistralLLMID (line 36) | type MistralLLMID =
type GroqLLMID (line 42) | type GroqLLMID =
type PerplexityLLMID (line 49) | type PerplexityLLMID =
type LLM (line 64) | interface LLM {
type OpenRouterLLM (line 79) | interface OpenRouterLLM extends LLM {
FILE: types/models.ts
type ModelProvider (line 1) | type ModelProvider =
FILE: types/sharing.ts
type Sharing (line 1) | type Sharing = "private" | "public" | "unlisted"
FILE: types/sidebar-data.ts
type DataListType (line 3) | type DataListType =
type DataItemType (line 13) | type DataItemType =
FILE: types/valid-keys.ts
type VALID_ENV_KEYS (line 1) | enum VALID_ENV_KEYS {
Condensed preview — 306 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (928K chars).
[
{
"path": ".eslintrc.json",
"chars": 496,
"preview": "{\n \"$schema\": \"https://json.schemastore.org/eslintrc\",\n \"root\": true,\n \"extends\": [\n \"next/core-web-vitals\",\n \""
},
{
"path": ".github/funding.yaml",
"chars": 96,
"preview": "# If you find my open-source work helpful, please consider sponsoring me!\n\ngithub: mckaywrigley\n"
},
{
"path": ".gitignore",
"chars": 487,
"preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
},
{
"path": ".husky/pre-commit",
"chars": 108,
"preview": "#!/usr/bin/env sh\n\n. \"$(dirname -- \"$0\")/_/husky.sh\"\n\nnpm run lint:fix && npm run format:write && git add .\n"
},
{
"path": ".nvmrc",
"chars": 9,
"preview": "v20.11.0\n"
},
{
"path": "README.md",
"chars": 8173,
"preview": "# Chatbot UI\n\nThe open-source AI chat app for everyone.\n\n<img src=\"./public/readme/screenshot.png\" alt=\"Chatbot UI\" widt"
},
{
"path": "__tests__/lib/openapi-conversion.test.ts",
"chars": 9827,
"preview": "import { openapiToFunctions } from \"@/lib/openapi-conversion\"\n\nconst validSchemaURL = JSON.stringify({\n openapi: \"3.1.0"
},
{
"path": "__tests__/playwright-test/.gitignore",
"chars": 83,
"preview": "node_modules/\n/test-results/\n/playwright-report/\n/blob-report/\n/playwright/.cache/\n"
},
{
"path": "__tests__/playwright-test/package.json",
"chars": 400,
"preview": "{\n \"name\": \"playwright-test\",\n \"version\": \"1.0.0\",\n \"description\": \"\",\n \"main\": \"index.js\",\n \"scripts\": {\n \"inte"
},
{
"path": "__tests__/playwright-test/playwright.config.ts",
"chars": 2088,
"preview": "import { defineConfig, devices } from '@playwright/test';\n\n/**\n * Read environment variables from file.\n * https://githu"
},
{
"path": "__tests__/playwright-test/tests/login.spec.ts",
"chars": 2036,
"preview": "import { test, expect } from '@playwright/test';\n\ntest('start chatting is displayed', async ({ page }) => {\n await page"
},
{
"path": "app/[locale]/[workspaceid]/chat/[chatid]/page.tsx",
"chars": 127,
"preview": "\"use client\"\n\nimport { ChatUI } from \"@/components/chat/chat-ui\"\n\nexport default function ChatIDPage() {\n return <ChatU"
},
{
"path": "app/[locale]/[workspaceid]/chat/page.tsx",
"chars": 1878,
"preview": "\"use client\"\n\nimport { ChatHelp } from \"@/components/chat/chat-help\"\nimport { useChatHandler } from \"@/components/chat/c"
},
{
"path": "app/[locale]/[workspaceid]/layout.tsx",
"chars": 5287,
"preview": "\"use client\"\n\nimport { Dashboard } from \"@/components/ui/dashboard\"\nimport { ChatbotUIContext } from \"@/context/context\""
},
{
"path": "app/[locale]/[workspaceid]/page.tsx",
"chars": 379,
"preview": "\"use client\"\n\nimport { ChatbotUIContext } from \"@/context/context\"\nimport { useContext } from \"react\"\n\nexport default fu"
},
{
"path": "app/[locale]/globals.css",
"chars": 1798,
"preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n::-webkit-scrollbar-track {\n background-color: transparent;"
},
{
"path": "app/[locale]/help/page.tsx",
"chars": 204,
"preview": "export default function HelpPage() {\n return (\n <div className=\"size-screen flex flex-col items-center justify-cente"
},
{
"path": "app/[locale]/layout.tsx",
"chars": 2889,
"preview": "import { Toaster } from \"@/components/ui/sonner\"\nimport { GlobalState } from \"@/components/utility/global-state\"\nimport "
},
{
"path": "app/[locale]/loading.tsx",
"chars": 249,
"preview": "import { IconLoader2 } from \"@tabler/icons-react\"\n\nexport default function Loading() {\n return (\n <div className=\"fl"
},
{
"path": "app/[locale]/login/page.tsx",
"chars": 6314,
"preview": "import { Brand } from \"@/components/ui/brand\"\nimport { Input } from \"@/components/ui/input\"\nimport { Label } from \"@/com"
},
{
"path": "app/[locale]/login/password/page.tsx",
"chars": 654,
"preview": "\"use client\"\n\nimport { ChangePassword } from \"@/components/utility/change-password\"\nimport { supabase } from \"@/lib/supa"
},
{
"path": "app/[locale]/page.tsx",
"chars": 785,
"preview": "\"use client\"\n\nimport { ChatbotUISVG } from \"@/components/icons/chatbotui-svg\"\nimport { IconArrowRight } from \"@tabler/ic"
},
{
"path": "app/[locale]/setup/page.tsx",
"chars": 8783,
"preview": "\"use client\"\n\nimport { ChatbotUIContext } from \"@/context/context\"\nimport { getProfileByUserId, updateProfile } from \"@/"
},
{
"path": "app/api/assistants/openai/route.ts",
"chars": 904,
"preview": "import { checkApiKey, getServerProfile } from \"@/lib/server/server-chat-helpers\"\nimport { ServerRuntime } from \"next\"\nim"
},
{
"path": "app/api/chat/anthropic/route.ts",
"chars": 3538,
"preview": "import { CHAT_SETTING_LIMITS } from \"@/lib/chat-setting-limits\"\nimport { checkApiKey, getServerProfile } from \"@/lib/ser"
},
{
"path": "app/api/chat/azure/route.ts",
"chars": 2349,
"preview": "import { checkApiKey, getServerProfile } from \"@/lib/server/server-chat-helpers\"\nimport { ChatAPIPayload } from \"@/types"
},
{
"path": "app/api/chat/custom/route.ts",
"chars": 2069,
"preview": "import { Database } from \"@/supabase/types\"\nimport { ChatSettings } from \"@/types\"\nimport { createClient } from \"@supaba"
},
{
"path": "app/api/chat/google/route.ts",
"chars": 1957,
"preview": "import { checkApiKey, getServerProfile } from \"@/lib/server/server-chat-helpers\"\nimport { ChatSettings } from \"@/types\"\n"
},
{
"path": "app/api/chat/groq/route.ts",
"chars": 1708,
"preview": "import { CHAT_SETTING_LIMITS } from \"@/lib/chat-setting-limits\"\nimport { checkApiKey, getServerProfile } from \"@/lib/ser"
},
{
"path": "app/api/chat/mistral/route.ts",
"chars": 1726,
"preview": "import { CHAT_SETTING_LIMITS } from \"@/lib/chat-setting-limits\"\nimport { checkApiKey, getServerProfile } from \"@/lib/ser"
},
{
"path": "app/api/chat/openai/route.ts",
"chars": 1925,
"preview": "import { checkApiKey, getServerProfile } from \"@/lib/server/server-chat-helpers\"\nimport { ChatSettings } from \"@/types\"\n"
},
{
"path": "app/api/chat/openrouter/route.ts",
"chars": 1627,
"preview": "import { checkApiKey, getServerProfile } from \"@/lib/server/server-chat-helpers\"\nimport { ChatSettings } from \"@/types\"\n"
},
{
"path": "app/api/chat/perplexity/route.ts",
"chars": 1508,
"preview": "import { checkApiKey, getServerProfile } from \"@/lib/server/server-chat-helpers\"\nimport { ChatSettings } from \"@/types\"\n"
},
{
"path": "app/api/chat/tools/route.ts",
"chars": 6776,
"preview": "import { openapiToFunctions } from \"@/lib/openapi-conversion\"\nimport { checkApiKey, getServerProfile } from \"@/lib/serve"
},
{
"path": "app/api/command/route.ts",
"chars": 1450,
"preview": "import { CHAT_SETTING_LIMITS } from \"@/lib/chat-setting-limits\"\nimport { checkApiKey, getServerProfile } from \"@/lib/ser"
},
{
"path": "app/api/keys/route.ts",
"chars": 1383,
"preview": "import { isUsingEnvironmentKey } from \"@/lib/envs\"\nimport { createResponse } from \"@/lib/server/server-utils\"\nimport { E"
},
{
"path": "app/api/retrieval/process/docx/route.ts",
"chars": 3660,
"preview": "import { generateLocalEmbedding } from \"@/lib/generate-local-embedding\"\nimport { processDocX } from \"@/lib/retrieval/pro"
},
{
"path": "app/api/retrieval/process/route.ts",
"chars": 5063,
"preview": "import { generateLocalEmbedding } from \"@/lib/generate-local-embedding\"\nimport {\n processCSV,\n processJSON,\n processM"
},
{
"path": "app/api/retrieval/retrieve/route.ts",
"chars": 3132,
"preview": "import { generateLocalEmbedding } from \"@/lib/generate-local-embedding\"\nimport { checkApiKey, getServerProfile } from \"@"
},
{
"path": "app/api/username/available/route.ts",
"chars": 990,
"preview": "import { Database } from \"@/supabase/types\"\nimport { createClient } from \"@supabase/supabase-js\"\n\nexport const runtime ="
},
{
"path": "app/api/username/get/route.ts",
"chars": 976,
"preview": "import { Database } from \"@/supabase/types\"\nimport { createClient } from \"@supabase/supabase-js\"\n\nexport const runtime ="
},
{
"path": "app/auth/callback/route.ts",
"chars": 622,
"preview": "import { createClient } from \"@/lib/supabase/server\"\nimport { cookies } from \"next/headers\"\nimport { NextResponse } from"
},
{
"path": "components/chat/assistant-picker.tsx",
"chars": 4274,
"preview": "import { ChatbotUIContext } from \"@/context/context\"\nimport { Tables } from \"@/supabase/types\"\nimport { IconRobotFace } "
},
{
"path": "components/chat/chat-command-input.tsx",
"chars": 1259,
"preview": "import { ChatbotUIContext } from \"@/context/context\"\nimport { FC, useContext } from \"react\"\nimport { AssistantPicker } f"
},
{
"path": "components/chat/chat-files-display.tsx",
"chars": 9257,
"preview": "import { ChatbotUIContext } from \"@/context/context\"\nimport { getFileFromStorage } from \"@/db/storage/files\"\nimport useH"
},
{
"path": "components/chat/chat-help.tsx",
"chars": 6973,
"preview": "import useHotkey from \"@/lib/hooks/use-hotkey\"\nimport {\n IconBrandGithub,\n IconBrandX,\n IconHelpCircle,\n IconQuestio"
},
{
"path": "components/chat/chat-helpers/index.ts",
"chars": 13855,
"preview": "// Only used in use-chat-handler.tsx to keep it clean\n\nimport { createChatFiles } from \"@/db/chat-files\"\nimport { create"
},
{
"path": "components/chat/chat-hooks/use-chat-handler.tsx",
"chars": 11596,
"preview": "import { ChatbotUIContext } from \"@/context/context\"\nimport { getAssistantCollectionsByAssistantId } from \"@/db/assistan"
},
{
"path": "components/chat/chat-hooks/use-chat-history.tsx",
"chars": 2705,
"preview": "import { ChatbotUIContext } from \"@/context/context\"\nimport { useContext, useEffect, useState } from \"react\"\n\n/**\n * Cus"
},
{
"path": "components/chat/chat-hooks/use-prompt-and-command.tsx",
"chars": 5392,
"preview": "import { ChatbotUIContext } from \"@/context/context\"\nimport { getAssistantCollectionsByAssistantId } from \"@/db/assistan"
},
{
"path": "components/chat/chat-hooks/use-scroll.tsx",
"chars": 2109,
"preview": "import { ChatbotUIContext } from \"@/context/context\"\nimport {\n type UIEventHandler,\n useCallback,\n useContext,\n useE"
},
{
"path": "components/chat/chat-hooks/use-select-file-handler.tsx",
"chars": 5598,
"preview": "import { ChatbotUIContext } from \"@/context/context\"\nimport { createDocXFile, createFile } from \"@/db/files\"\nimport { LL"
},
{
"path": "components/chat/chat-input.tsx",
"chars": 8631,
"preview": "import { ChatbotUIContext } from \"@/context/context\"\nimport useHotkey from \"@/lib/hooks/use-hotkey\"\nimport { LLM_LIST } "
},
{
"path": "components/chat/chat-messages.tsx",
"chars": 1372,
"preview": "import { useChatHandler } from \"@/components/chat/chat-hooks/use-chat-handler\"\nimport { ChatbotUIContext } from \"@/conte"
},
{
"path": "components/chat/chat-retrieval-settings.tsx",
"chars": 1727,
"preview": "import { ChatbotUIContext } from \"@/context/context\"\nimport { IconAdjustmentsHorizontal } from \"@tabler/icons-react\"\nimp"
},
{
"path": "components/chat/chat-scroll-buttons.tsx",
"chars": 885,
"preview": "import {\n IconCircleArrowDownFilled,\n IconCircleArrowUpFilled\n} from \"@tabler/icons-react\"\nimport { FC } from \"react\"\n"
},
{
"path": "components/chat/chat-secondary-buttons.tsx",
"chars": 2436,
"preview": "import { useChatHandler } from \"@/components/chat/chat-hooks/use-chat-handler\"\nimport { ChatbotUIContext } from \"@/conte"
},
{
"path": "components/chat/chat-settings.tsx",
"chars": 2718,
"preview": "import { ChatbotUIContext } from \"@/context/context\"\nimport { CHAT_SETTING_LIMITS } from \"@/lib/chat-setting-limits\"\nimp"
},
{
"path": "components/chat/chat-ui.tsx",
"chars": 6595,
"preview": "import Loading from \"@/app/[locale]/loading\"\nimport { useChatHandler } from \"@/components/chat/chat-hooks/use-chat-handl"
},
{
"path": "components/chat/file-picker.tsx",
"chars": 5039,
"preview": "import { ChatbotUIContext } from \"@/context/context\"\nimport { Tables } from \"@/supabase/types\"\nimport { IconBooks } from"
},
{
"path": "components/chat/prompt-picker.tsx",
"chars": 7003,
"preview": "import { ChatbotUIContext } from \"@/context/context\"\nimport { Tables } from \"@/supabase/types\"\nimport { FC, useContext, "
},
{
"path": "components/chat/quick-setting-option.tsx",
"chars": 1931,
"preview": "import { LLM_LIST } from \"@/lib/models/llm/llm-list\"\nimport { Tables } from \"@/supabase/types\"\nimport { IconCircleCheckF"
},
{
"path": "components/chat/quick-settings.tsx",
"chars": 10008,
"preview": "import { ChatbotUIContext } from \"@/context/context\"\nimport { getAssistantCollectionsByAssistantId } from \"@/db/assistan"
},
{
"path": "components/chat/tool-picker.tsx",
"chars": 3548,
"preview": "import { ChatbotUIContext } from \"@/context/context\"\nimport { Tables } from \"@/supabase/types\"\nimport { IconBolt } from "
},
{
"path": "components/icons/anthropic-svg.tsx",
"chars": 2441,
"preview": "import { FC } from \"react\"\n\ninterface AnthropicSVGProps {\n height?: number\n width?: number\n className?: string\n}\n\nexp"
},
{
"path": "components/icons/chatbotui-svg.tsx",
"chars": 1621,
"preview": "import { FC } from \"react\"\n\ninterface ChatbotUISVGProps {\n theme: \"dark\" | \"light\"\n scale?: number\n}\n\nexport const Cha"
},
{
"path": "components/icons/google-svg.tsx",
"chars": 1457,
"preview": "import { FC } from \"react\"\n\ninterface GoogleSVGProps {\n height?: number\n width?: number\n className?: string\n}\n\nexport"
},
{
"path": "components/icons/openai-svg.tsx",
"chars": 4627,
"preview": "import { FC } from \"react\"\n\ninterface OpenAISVGProps {\n height?: number\n width?: number\n className?: string\n}\n\nexport"
},
{
"path": "components/messages/message-actions.tsx",
"chars": 2892,
"preview": "import { ChatbotUIContext } from \"@/context/context\"\nimport { IconCheck, IconCopy, IconEdit, IconRepeat } from \"@tabler/"
},
{
"path": "components/messages/message-codeblock.tsx",
"chars": 3874,
"preview": "import { Button } from \"@/components/ui/button\"\nimport { useCopyToClipboard } from \"@/lib/hooks/use-copy-to-clipboard\"\ni"
},
{
"path": "components/messages/message-markdown-memoized.tsx",
"chars": 292,
"preview": "import { FC, memo } from \"react\"\nimport ReactMarkdown, { Options } from \"react-markdown\"\n\nexport const MessageMarkdownMe"
},
{
"path": "components/messages/message-markdown.tsx",
"chars": 2072,
"preview": "import React, { FC } from \"react\"\nimport remarkGfm from \"remark-gfm\"\nimport remarkMath from \"remark-math\"\nimport { Messa"
},
{
"path": "components/messages/message-replies.tsx",
"chars": 1532,
"preview": "import { IconMessage } from \"@tabler/icons-react\"\nimport { FC, useState } from \"react\"\nimport {\n Sheet,\n SheetContent,"
},
{
"path": "components/messages/message.tsx",
"chars": 14312,
"preview": "import { useChatHandler } from \"@/components/chat/chat-hooks/use-chat-handler\"\nimport { ChatbotUIContext } from \"@/conte"
},
{
"path": "components/models/model-icon.tsx",
"chars": 2787,
"preview": "import { cn } from \"@/lib/utils\"\nimport mistral from \"@/public/providers/mistral.png\"\nimport groq from \"@/public/provide"
},
{
"path": "components/models/model-option.tsx",
"chars": 1567,
"preview": "import { LLM } from \"@/types\"\nimport { FC } from \"react\"\nimport { ModelIcon } from \"./model-icon\"\nimport { IconInfoCircl"
},
{
"path": "components/models/model-select.tsx",
"chars": 6050,
"preview": "import { ChatbotUIContext } from \"@/context/context\"\nimport { LLM, LLMID, ModelProvider } from \"@/types\"\nimport { IconCh"
},
{
"path": "components/setup/api-step.tsx",
"chars": 7182,
"preview": "import { Input } from \"@/components/ui/input\"\nimport { Label } from \"@/components/ui/label\"\nimport { FC } from \"react\"\ni"
},
{
"path": "components/setup/finish-step.tsx",
"chars": 390,
"preview": "import { FC } from \"react\"\n\ninterface FinishStepProps {\n displayName: string\n}\n\nexport const FinishStep: FC<FinishStepP"
},
{
"path": "components/setup/profile-step.tsx",
"chars": 3938,
"preview": "import { Input } from \"@/components/ui/input\"\nimport { Label } from \"@/components/ui/label\"\nimport {\n PROFILE_DISPLAY_N"
},
{
"path": "components/setup/step-container.tsx",
"chars": 2013,
"preview": "import { Button } from \"@/components/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardH"
},
{
"path": "components/sidebar/items/all/sidebar-create-item.tsx",
"chars": 7107,
"preview": "import { Button } from \"@/components/ui/button\"\nimport {\n Sheet,\n SheetContent,\n SheetFooter,\n SheetHeader,\n SheetT"
},
{
"path": "components/sidebar/items/all/sidebar-delete-item.tsx",
"chars": 3850,
"preview": "import { Button } from \"@/components/ui/button\"\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,"
},
{
"path": "components/sidebar/items/all/sidebar-display-item.tsx",
"chars": 3606,
"preview": "import { ChatbotUIContext } from \"@/context/context\"\nimport { createChat } from \"@/db/chats\"\nimport { cn } from \"@/lib/u"
},
{
"path": "components/sidebar/items/all/sidebar-update-item.tsx",
"chars": 18856,
"preview": "import { Button } from \"@/components/ui/button\"\nimport { Label } from \"@/components/ui/label\"\nimport {\n Sheet,\n SheetC"
},
{
"path": "components/sidebar/items/assistants/assistant-item.tsx",
"chars": 10722,
"preview": "import { ChatSettingsForm } from \"@/components/ui/chat-settings-form\"\nimport ImagePicker from \"@/components/ui/image-pic"
},
{
"path": "components/sidebar/items/assistants/assistant-retrieval-select.tsx",
"chars": 5837,
"preview": "import { Button } from \"@/components/ui/button\"\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuTrigger\n} "
},
{
"path": "components/sidebar/items/assistants/assistant-tool-select.tsx",
"chars": 4262,
"preview": "import { Button } from \"@/components/ui/button\"\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuTrigger\n} "
},
{
"path": "components/sidebar/items/assistants/create-assistant.tsx",
"chars": 6844,
"preview": "import { SidebarCreateItem } from \"@/components/sidebar/items/all/sidebar-create-item\"\nimport { ChatSettingsForm } from "
},
{
"path": "components/sidebar/items/chat/chat-item.tsx",
"chars": 3084,
"preview": "import { ModelIcon } from \"@/components/models/model-icon\"\nimport { WithTooltip } from \"@/components/ui/with-tooltip\"\nim"
},
{
"path": "components/sidebar/items/chat/delete-chat.tsx",
"chars": 2133,
"preview": "import { useChatHandler } from \"@/components/chat/chat-hooks/use-chat-handler\"\nimport { Button } from \"@/components/ui/b"
},
{
"path": "components/sidebar/items/chat/update-chat.tsx",
"chars": 2063,
"preview": "import { Button } from \"@/components/ui/button\"\nimport {\n Dialog,\n DialogContent,\n DialogFooter,\n DialogHeader,\n Di"
},
{
"path": "components/sidebar/items/collections/collection-file-select.tsx",
"chars": 4247,
"preview": "import { Button } from \"@/components/ui/button\"\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuTrigger\n} "
},
{
"path": "components/sidebar/items/collections/collection-item.tsx",
"chars": 3851,
"preview": "import { Input } from \"@/components/ui/input\"\nimport { Label } from \"@/components/ui/label\"\nimport { COLLECTION_DESCRIPT"
},
{
"path": "components/sidebar/items/collections/create-collection.tsx",
"chars": 2926,
"preview": "import { SidebarCreateItem } from \"@/components/sidebar/items/all/sidebar-create-item\"\nimport { Input } from \"@/componen"
},
{
"path": "components/sidebar/items/files/create-file.tsx",
"chars": 2684,
"preview": "import { ACCEPTED_FILE_TYPES } from \"@/components/chat/chat-hooks/use-select-file-handler\"\nimport { SidebarCreateItem } "
},
{
"path": "components/sidebar/items/files/file-item.tsx",
"chars": 2420,
"preview": "import { FileIcon } from \"@/components/ui/file-icon\"\nimport { Input } from \"@/components/ui/input\"\nimport { Label } from"
},
{
"path": "components/sidebar/items/folders/delete-folder.tsx",
"chars": 3371,
"preview": "import { Button } from \"@/components/ui/button\"\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,"
},
{
"path": "components/sidebar/items/folders/folder-item.tsx",
"chars": 3172,
"preview": "import { cn } from \"@/lib/utils\"\nimport { Tables } from \"@/supabase/types\"\nimport { ContentType } from \"@/types\"\nimport "
},
{
"path": "components/sidebar/items/folders/update-folder.tsx",
"chars": 2113,
"preview": "import { Button } from \"@/components/ui/button\"\nimport {\n Dialog,\n DialogContent,\n DialogFooter,\n DialogHeader,\n Di"
},
{
"path": "components/sidebar/items/models/create-model.tsx",
"chars": 3296,
"preview": "import { SidebarCreateItem } from \"@/components/sidebar/items/all/sidebar-create-item\"\nimport { Input } from \"@/componen"
},
{
"path": "components/sidebar/items/models/model-item.tsx",
"chars": 2868,
"preview": "import { Input } from \"@/components/ui/input\"\nimport { Label } from \"@/components/ui/label\"\nimport { MODEL_NAME_MAX } fr"
},
{
"path": "components/sidebar/items/presets/create-preset.tsx",
"chars": 2711,
"preview": "import { SidebarCreateItem } from \"@/components/sidebar/items/all/sidebar-create-item\"\nimport { ChatSettingsForm } from "
},
{
"path": "components/sidebar/items/presets/preset-item.tsx",
"chars": 2398,
"preview": "import { ModelIcon } from \"@/components/models/model-icon\"\nimport { ChatSettingsForm } from \"@/components/ui/chat-settin"
},
{
"path": "components/sidebar/items/prompts/create-prompt.tsx",
"chars": 2087,
"preview": "import { SidebarCreateItem } from \"@/components/sidebar/items/all/sidebar-create-item\"\nimport { Input } from \"@/componen"
},
{
"path": "components/sidebar/items/prompts/prompt-item.tsx",
"chars": 1744,
"preview": "import { Input } from \"@/components/ui/input\"\nimport { Label } from \"@/components/ui/label\"\nimport { TextareaAutosize } "
},
{
"path": "components/sidebar/items/tools/create-tool.tsx",
"chars": 5405,
"preview": "import { SidebarCreateItem } from \"@/components/sidebar/items/all/sidebar-create-item\"\nimport { Input } from \"@/componen"
},
{
"path": "components/sidebar/items/tools/tool-item.tsx",
"chars": 5156,
"preview": "import { Input } from \"@/components/ui/input\"\nimport { Label } from \"@/components/ui/label\"\nimport { TextareaAutosize } "
},
{
"path": "components/sidebar/sidebar-content.tsx",
"chars": 1316,
"preview": "import { Tables } from \"@/supabase/types\"\nimport { ContentType, DataListType } from \"@/types\"\nimport { FC, useState } fr"
},
{
"path": "components/sidebar/sidebar-create-buttons.tsx",
"chars": 4345,
"preview": "import { useChatHandler } from \"@/components/chat/chat-hooks/use-chat-handler\"\nimport { ChatbotUIContext } from \"@/conte"
},
{
"path": "components/sidebar/sidebar-data-list.tsx",
"chars": 10434,
"preview": "import { ChatbotUIContext } from \"@/context/context\"\nimport { updateAssistant } from \"@/db/assistants\"\nimport { updateCh"
},
{
"path": "components/sidebar/sidebar-search.tsx",
"chars": 478,
"preview": "import { ContentType } from \"@/types\"\nimport { FC } from \"react\"\nimport { Input } from \"../ui/input\"\n\ninterface SidebarS"
},
{
"path": "components/sidebar/sidebar-switch-item.tsx",
"chars": 800,
"preview": "import { ContentType } from \"@/types\"\nimport { FC } from \"react\"\nimport { TabsTrigger } from \"../ui/tabs\"\nimport { WithT"
},
{
"path": "components/sidebar/sidebar-switcher.tsx",
"chars": 2645,
"preview": "import { ContentType } from \"@/types\"\nimport {\n IconAdjustmentsHorizontal,\n IconBolt,\n IconBooks,\n IconFile,\n IconM"
},
{
"path": "components/sidebar/sidebar.tsx",
"chars": 3327,
"preview": "import { ChatbotUIContext } from \"@/context/context\"\nimport { Tables } from \"@/supabase/types\"\nimport { ContentType } fr"
},
{
"path": "components/ui/accordion.tsx",
"chars": 1990,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as AccordionPrimitive from \"@radix-ui/react-accordion\"\nimport { Ch"
},
{
"path": "components/ui/advanced-settings.tsx",
"chars": 1185,
"preview": "import {\n Collapsible,\n CollapsibleContent,\n CollapsibleTrigger\n} from \"@/components/ui/collapsible\"\nimport { IconChe"
},
{
"path": "components/ui/alert-dialog.tsx",
"chars": 4454,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as AlertDialogPrimitive from \"@radix-ui/react-alert-dialog\"\n\nimpor"
},
{
"path": "components/ui/alert.tsx",
"chars": 1580,
"preview": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/"
},
{
"path": "components/ui/aspect-ratio.tsx",
"chars": 154,
"preview": "\"use client\"\n\nimport * as AspectRatioPrimitive from \"@radix-ui/react-aspect-ratio\"\n\nconst AspectRatio = AspectRatioPrimi"
},
{
"path": "components/ui/avatar.tsx",
"chars": 1409,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as AvatarPrimitive from \"@radix-ui/react-avatar\"\n\nimport { cn } fr"
},
{
"path": "components/ui/badge.tsx",
"chars": 1124,
"preview": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/"
},
{
"path": "components/ui/brand.tsx",
"chars": 653,
"preview": "\"use client\"\n\nimport Link from \"next/link\"\nimport { FC } from \"react\"\nimport { ChatbotUISVG } from \"../icons/chatbotui-s"
},
{
"path": "components/ui/button.tsx",
"chars": 1845,
"preview": "import { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport * a"
},
{
"path": "components/ui/calendar.tsx",
"chars": 2617,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronLeft, ChevronRight } from \"lucide-react\"\nimport { DayPicker"
},
{
"path": "components/ui/card.tsx",
"chars": 1877,
"preview": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Card = React.forwardRef<\n HTMLDivElement,\n Rea"
},
{
"path": "components/ui/chat-settings-form.tsx",
"chars": 6907,
"preview": "\"use client\"\n\nimport { ChatbotUIContext } from \"@/context/context\"\nimport { CHAT_SETTING_LIMITS } from \"@/lib/chat-setti"
},
{
"path": "components/ui/checkbox.tsx",
"chars": 1068,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as CheckboxPrimitive from \"@radix-ui/react-checkbox\"\nimport { Chec"
},
{
"path": "components/ui/collapsible.tsx",
"chars": 329,
"preview": "\"use client\"\n\nimport * as CollapsiblePrimitive from \"@radix-ui/react-collapsible\"\n\nconst Collapsible = CollapsiblePrimit"
},
{
"path": "components/ui/command.tsx",
"chars": 4810,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport { type DialogProps } from \"@radix-ui/react-dialog\"\nimport { Command "
},
{
"path": "components/ui/context-menu.tsx",
"chars": 7242,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as ContextMenuPrimitive from \"@radix-ui/react-context-menu\"\nimport"
},
{
"path": "components/ui/dashboard.tsx",
"chars": 3963,
"preview": "\"use client\"\n\nimport { Sidebar } from \"@/components/sidebar/sidebar\"\nimport { SidebarSwitcher } from \"@/components/sideb"
},
{
"path": "components/ui/dialog.tsx",
"chars": 3868,
"preview": "\"use client\"\n\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\"\nimport * as React from \"react\"\n\nimport { cn } fr"
},
{
"path": "components/ui/dropdown-menu.tsx",
"chars": 7291,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\"\nimpo"
},
{
"path": "components/ui/file-icon.tsx",
"chars": 921,
"preview": "import {\n IconFile,\n IconFileText,\n IconFileTypeCsv,\n IconFileTypeDocx,\n IconFileTypePdf,\n IconJson,\n IconMarkdow"
},
{
"path": "components/ui/file-preview.tsx",
"chars": 2022,
"preview": "import { cn } from \"@/lib/utils\"\nimport { Tables } from \"@/supabase/types\"\nimport { ChatFile, MessageImage } from \"@/typ"
},
{
"path": "components/ui/form.tsx",
"chars": 4082,
"preview": "import * as React from \"react\"\nimport * as LabelPrimitive from \"@radix-ui/react-label\"\nimport { Slot } from \"@radix-ui/r"
},
{
"path": "components/ui/hover-card.tsx",
"chars": 1198,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as HoverCardPrimitive from \"@radix-ui/react-hover-card\"\n\nimport { "
},
{
"path": "components/ui/image-picker.tsx",
"chars": 2174,
"preview": "import Image from \"next/image\"\nimport { ChangeEvent, FC, useState } from \"react\"\nimport { toast } from \"sonner\"\nimport {"
},
{
"path": "components/ui/input.tsx",
"chars": 790,
"preview": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface InputProps\n extends React.InputHTMLA"
},
{
"path": "components/ui/label.tsx",
"chars": 726,
"preview": "\"use client\"\n\nimport * as LabelPrimitive from \"@radix-ui/react-label\"\nimport { cva, type VariantProps } from \"class-vari"
},
{
"path": "components/ui/limit-display.tsx",
"chars": 252,
"preview": "import { FC } from \"react\"\n\ninterface LimitDisplayProps {\n used: number\n limit: number\n}\n\nexport const LimitDisplay: F"
},
{
"path": "components/ui/menubar.tsx",
"chars": 7969,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as MenubarPrimitive from \"@radix-ui/react-menubar\"\nimport { Check,"
},
{
"path": "components/ui/navigation-menu.tsx",
"chars": 5040,
"preview": "import * as React from \"react\"\nimport * as NavigationMenuPrimitive from \"@radix-ui/react-navigation-menu\"\nimport { cva }"
},
{
"path": "components/ui/popover.tsx",
"chars": 1244,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as PopoverPrimitive from \"@radix-ui/react-popover\"\n\nimport { cn } "
},
{
"path": "components/ui/progress.tsx",
"chars": 787,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as ProgressPrimitive from \"@radix-ui/react-progress\"\n\nimport { cn "
},
{
"path": "components/ui/radio-group.tsx",
"chars": 1477,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as RadioGroupPrimitive from \"@radix-ui/react-radio-group\"\nimport {"
},
{
"path": "components/ui/screen-loader.tsx",
"chars": 331,
"preview": "import { IconLoader2 } from \"@tabler/icons-react\"\nimport { FC } from \"react\"\n\ninterface ScreenLoaderProps {}\n\nexport con"
},
{
"path": "components/ui/scroll-area.tsx",
"chars": 1646,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as ScrollAreaPrimitive from \"@radix-ui/react-scroll-area\"\n\nimport "
},
{
"path": "components/ui/select.tsx",
"chars": 5617,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SelectPrimitive from \"@radix-ui/react-select\"\nimport { Check, C"
},
{
"path": "components/ui/separator.tsx",
"chars": 764,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SeparatorPrimitive from \"@radix-ui/react-separator\"\n\nimport { c"
},
{
"path": "components/ui/sheet.tsx",
"chars": 4272,
"preview": "\"use client\"\n\nimport * as SheetPrimitive from \"@radix-ui/react-dialog\"\nimport { cva, type VariantProps } from \"class-var"
},
{
"path": "components/ui/skeleton.tsx",
"chars": 261,
"preview": "import { cn } from \"@/lib/utils\"\n\nfunction Skeleton({\n className,\n ...props\n}: React.HTMLAttributes<HTMLDivElement>) {"
},
{
"path": "components/ui/slider.tsx",
"chars": 1102,
"preview": "\"use client\"\n\nimport * as SliderPrimitive from \"@radix-ui/react-slider\"\nimport * as React from \"react\"\n\nimport { cn } fr"
},
{
"path": "components/ui/sonner.tsx",
"chars": 892,
"preview": "\"use client\"\n\nimport { useTheme } from \"next-themes\"\nimport { Toaster as Sonner } from \"sonner\"\n\ntype ToasterProps = Rea"
},
{
"path": "components/ui/submit-button.tsx",
"chars": 398,
"preview": "\"use client\"\n\nimport React from \"react\"\nimport { useFormStatus } from \"react-dom\"\nimport { Button, ButtonProps } from \"."
},
{
"path": "components/ui/switch.tsx",
"chars": 1152,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SwitchPrimitives from \"@radix-ui/react-switch\"\n\nimport { cn } f"
},
{
"path": "components/ui/table.tsx",
"chars": 2764,
"preview": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Table = React.forwardRef<\n HTMLTableElement,\n "
},
{
"path": "components/ui/tabs.tsx",
"chars": 1897,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\"\n\nimport { cn } from \""
},
{
"path": "components/ui/textarea-autosize.tsx",
"chars": 1618,
"preview": "import { cn } from \"@/lib/utils\"\nimport { FC } from \"react\"\nimport ReactTextareaAutosize from \"react-textarea-autosize\"\n"
},
{
"path": "components/ui/textarea.tsx",
"chars": 772,
"preview": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface TextareaProps\n extends React.Textare"
},
{
"path": "components/ui/toast.tsx",
"chars": 4839,
"preview": "import * as React from \"react\"\nimport * as ToastPrimitives from \"@radix-ui/react-toast\"\nimport { cva, type VariantProps "
},
{
"path": "components/ui/toaster.tsx",
"chars": 793,
"preview": "\"use client\"\n\nimport {\n Toast,\n ToastClose,\n ToastDescription,\n ToastProvider,\n ToastTitle,\n ToastViewport\n} from "
},
{
"path": "components/ui/toggle-group.tsx",
"chars": 1746,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as ToggleGroupPrimitive from \"@radix-ui/react-toggle-group\"\nimport"
},
{
"path": "components/ui/toggle.tsx",
"chars": 1444,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as TogglePrimitive from \"@radix-ui/react-toggle\"\nimport { cva, typ"
},
{
"path": "components/ui/tooltip.tsx",
"chars": 1159,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\"\n\nimport { cn } "
},
{
"path": "components/ui/use-toast.ts",
"chars": 3897,
"preview": "// Inspired by react-hot-toast library\nimport * as React from \"react\"\n\nimport type { ToastActionElement, ToastProps } fr"
},
{
"path": "components/ui/with-tooltip.tsx",
"chars": 641,
"preview": "import { FC } from \"react\"\nimport {\n Tooltip,\n TooltipContent,\n TooltipProvider,\n TooltipTrigger\n} from \"./tooltip\"\n"
},
{
"path": "components/utility/alerts.tsx",
"chars": 876,
"preview": "import {\n Popover,\n PopoverContent,\n PopoverTrigger\n} from \"@/components/ui/popover\"\nimport { IconBell } from \"@table"
},
{
"path": "components/utility/announcements.tsx",
"chars": 6162,
"preview": "import { Button } from \"@/components/ui/button\"\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger\n} from \"@/compone"
},
{
"path": "components/utility/change-password.tsx",
"chars": 1660,
"preview": "\"use client\"\n\nimport { supabase } from \"@/lib/supabase/browser-client\"\nimport { useRouter } from \"next/navigation\"\nimpor"
},
{
"path": "components/utility/command-k.tsx",
"chars": 2969,
"preview": "import { ChatbotUIContext } from \"@/context/context\"\nimport useHotkey from \"@/lib/hooks/use-hotkey\"\nimport { IconLoader2"
},
{
"path": "components/utility/drawing-canvas.tsx",
"chars": 2888,
"preview": "import { ChatbotUIContext } from \"@/context/context\"\nimport { MessageImage } from \"@/types\"\nimport { FC, MouseEvent, use"
},
{
"path": "components/utility/global-state.tsx",
"chars": 9945,
"preview": "// TODO: Separate into multiple contexts, keeping simple for now\n\n\"use client\"\n\nimport { ChatbotUIContext } from \"@/cont"
},
{
"path": "components/utility/import.tsx",
"chars": 8008,
"preview": "import { ChatbotUIContext } from \"@/context/context\"\nimport { createAssistants } from \"@/db/assistants\"\nimport { createC"
},
{
"path": "components/utility/profile-settings.tsx",
"chars": 25825,
"preview": "import { ChatbotUIContext } from \"@/context/context\"\nimport {\n PROFILE_CONTEXT_MAX,\n PROFILE_DISPLAY_NAME_MAX,\n PROFI"
},
{
"path": "components/utility/providers.tsx",
"chars": 436,
"preview": "\"use client\"\n\nimport { TooltipProvider } from \"@/components/ui/tooltip\"\nimport { ThemeProvider as NextThemesProvider } f"
},
{
"path": "components/utility/theme-switcher.tsx",
"chars": 832,
"preview": "import { IconMoon, IconSun } from \"@tabler/icons-react\"\nimport { useTheme } from \"next-themes\"\nimport { FC } from \"react"
},
{
"path": "components/utility/translations-provider.tsx",
"chars": 410,
"preview": "\"use client\"\n\nimport initTranslations from \"@/lib/i18n\"\nimport { createInstance } from \"i18next\"\nimport { I18nextProvide"
},
{
"path": "components/utility/workspace-switcher.tsx",
"chars": 7207,
"preview": "\"use client\"\n\nimport { useChatHandler } from \"@/components/chat/chat-hooks/use-chat-handler\"\nimport {\n Popover,\n Popov"
},
{
"path": "components/workspace/assign-workspaces.tsx",
"chars": 4453,
"preview": "import { ChatbotUIContext } from \"@/context/context\"\nimport { Tables } from \"@/supabase/types\"\nimport { IconChevronDown,"
},
{
"path": "components/workspace/delete-workspace.tsx",
"chars": 2836,
"preview": "import { useChatHandler } from \"@/components/chat/chat-hooks/use-chat-handler\"\nimport { Button } from \"@/components/ui/b"
},
{
"path": "components/workspace/workspace-settings.tsx",
"chars": 9437,
"preview": "import { ChatbotUIContext } from \"@/context/context\"\nimport { WORKSPACE_INSTRUCTIONS_MAX } from \"@/db/limits\"\nimport {\n "
},
{
"path": "components.json",
"chars": 323,
"preview": "{\n \"$schema\": \"https://ui.shadcn.com/schema.json\",\n \"style\": \"default\",\n \"rsc\": true,\n \"tsx\": true,\n \"tailwind\": {\n"
},
{
"path": "context/context.tsx",
"chars": 8065,
"preview": "import { Tables } from \"@/supabase/types\"\nimport {\n ChatFile,\n ChatMessage,\n ChatSettings,\n LLM,\n MessageImage,\n O"
},
{
"path": "db/assistant-collections.ts",
"chars": 1623,
"preview": "import { supabase } from \"@/lib/supabase/browser-client\"\nimport { TablesInsert } from \"@/supabase/types\"\n\nexport const g"
},
{
"path": "db/assistant-files.ts",
"chars": 1463,
"preview": "import { supabase } from \"@/lib/supabase/browser-client\"\nimport { TablesInsert } from \"@/supabase/types\"\n\nexport const g"
},
{
"path": "db/assistant-tools.ts",
"chars": 1463,
"preview": "import { supabase } from \"@/lib/supabase/browser-client\"\nimport { TablesInsert } from \"@/supabase/types\"\n\nexport const g"
},
{
"path": "db/assistants.ts",
"chars": 3747,
"preview": "import { supabase } from \"@/lib/supabase/browser-client\"\nimport { TablesInsert, TablesUpdate } from \"@/supabase/types\"\n\n"
},
{
"path": "db/chat-files.ts",
"chars": 1036,
"preview": "import { supabase } from \"@/lib/supabase/browser-client\"\nimport { TablesInsert } from \"@/supabase/types\"\n\nexport const g"
},
{
"path": "db/chats.ts",
"chars": 1670,
"preview": "import { supabase } from \"@/lib/supabase/browser-client\"\nimport { TablesInsert, TablesUpdate } from \"@/supabase/types\"\n\n"
},
{
"path": "db/collection-files.ts",
"chars": 1511,
"preview": "import { supabase } from \"@/lib/supabase/browser-client\"\nimport { TablesInsert } from \"@/supabase/types\"\n\nexport const g"
},
{
"path": "db/collections.ts",
"chars": 3816,
"preview": "import { supabase } from \"@/lib/supabase/browser-client\"\nimport { TablesInsert, TablesUpdate } from \"@/supabase/types\"\n\n"
},
{
"path": "db/files.ts",
"chars": 7041,
"preview": "import { supabase } from \"@/lib/supabase/browser-client\"\nimport { TablesInsert, TablesUpdate } from \"@/supabase/types\"\ni"
},
{
"path": "db/folders.ts",
"chars": 1221,
"preview": "import { supabase } from \"@/lib/supabase/browser-client\"\nimport { TablesInsert, TablesUpdate } from \"@/supabase/types\"\n\n"
},
{
"path": "db/index.ts",
"chars": 196,
"preview": "import \"./assistants\"\nimport \"./chats\"\nimport \"./file-items\"\nimport \"./files\"\nimport \"./folders\"\nimport \"./messages\"\nimp"
},
{
"path": "db/limits.ts",
"chars": 1073,
"preview": "// Profiles\nexport const PROFILE_BIO_MAX = 500\nexport const PROFILE_DISPLAY_NAME_MAX = 100\nexport const PROFILE_CONTEXT_"
},
{
"path": "db/message-file-items.ts",
"chars": 829,
"preview": "import { supabase } from \"@/lib/supabase/browser-client\"\nimport { TablesInsert } from \"@/supabase/types\"\n\nexport const g"
},
{
"path": "db/messages.ts",
"chars": 2164,
"preview": "import { supabase } from \"@/lib/supabase/browser-client\"\nimport { TablesInsert, TablesUpdate } from \"@/supabase/types\"\n\n"
},
{
"path": "db/models.ts",
"chars": 3455,
"preview": "import { supabase } from \"@/lib/supabase/browser-client\"\nimport { TablesInsert, TablesUpdate } from \"@/supabase/types\"\n\n"
}
]
// ... and 106 more files (download for full content)
About this extraction
This page contains the full source code of the mckaywrigley/chatbot-ui GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 306 files (847.3 KB), approximately 217.3k tokens, and a symbol index with 379 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.