Full Code of mckaywrigley/chatbot-ui for AI

main 81328b61d2a4 cached
306 files
847.3 KB
217.3k tokens
379 symbols
1 requests
Download .txt
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
Download .txt
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
Download .txt
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.

Copied to clipboard!