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. Chatbot UI ## Demo View the latest demo [here](https://x.com/mckaywrigley/status/1738273242283151777?s=20). ## Updates Hey everyone! I've heard your feedback and am working hard on a big update. Things like simpler deployment, better backend compatibility, and improved mobile layouts are on their way. Be back soon. -- Mckay ## Official Hosted Version Use Chatbot UI without having to host it yourself! Find the official hosted version of Chatbot UI [here](https://chatbotui.com). ## Sponsor If you find Chatbot UI useful, please consider [sponsoring](https://github.com/sponsors/mckaywrigley) me to support my open-source work :) ## Issues We restrict "Issues" to actual issues related to the codebase. We're getting excessive amounts of issues that amount to things like feature requests, cloud provider issues, etc. If you are having issues with things like setup, please refer to the "Help" section in the "Discussions" tab above. Issues unrelated to the codebase will likely be closed immediately. ## Discussions We highly encourage you to participate in the "Discussions" tab above! Discussions are a great place to ask questions, share ideas, and get help. Odds are if you have a question, someone else has the same question. ## Legacy Code Chatbot UI was recently updated to its 2.0 version. The code for 1.0 can be found on the `legacy` branch. ## Updating In your terminal at the root of your local Chatbot UI repository, run: ```bash npm run update ``` If you run a hosted instance you'll also need to run: ```bash npm run db-push ``` to apply the latest migrations to your live database. ## Local Quickstart Follow these steps to get your own Chatbot UI instance running locally. You can watch the full video tutorial [here](https://www.youtube.com/watch?v=9Qq3-7-HNgw). ### 1. Clone the Repo ```bash git clone https://github.com/mckaywrigley/chatbot-ui.git ``` ### 2. Install Dependencies Open a terminal in the root directory of your local Chatbot UI repository and run: ```bash npm install ``` ### 3. Install Supabase & Run Locally #### Why Supabase? Previously, we used local browser storage to store data. However, this was not a good solution for a few reasons: - Security issues - Limited storage - Limits multi-modal use cases We now use Supabase because it's easy to use, it's open-source, it's Postgres, and it has a free tier for hosted instances. We will support other providers in the future to give you more options. #### 1. Install Docker You will need to install Docker to run Supabase locally. You can download it [here](https://docs.docker.com/get-docker) for free. #### 2. Install Supabase CLI **MacOS/Linux** ```bash brew install supabase/tap/supabase ``` **Windows** ```bash scoop bucket add supabase https://github.com/supabase/scoop-bucket.git scoop install supabase ``` #### 3. Start Supabase In your terminal at the root of your local Chatbot UI repository, run: ```bash supabase start ``` ### 4. Fill in Secrets #### 1. Environment Variables In your terminal at the root of your local Chatbot UI repository, run: ```bash cp .env.local.example .env.local ``` Get the required values by running: ```bash supabase status ``` Note: Use `API URL` from `supabase status` for `NEXT_PUBLIC_SUPABASE_URL` Now go to your `.env.local` file and fill in the values. If the environment variable is set, it will disable the input in the user settings. #### 2. SQL Setup In the 1st migration file `supabase/migrations/20240108234540_setup.sql` you will need to replace 2 values with the values you got above: - `project_url` (line 53): `http://supabase_kong_chatbotui:8000` (default) can remain unchanged if you don't change your `project_id` in the `config.toml` file - `service_role_key` (line 54): You got this value from running `supabase status` This prevents issues with storage files not being deleted properly. ### 5. Install Ollama (optional for local models) Follow the instructions [here](https://github.com/jmorganca/ollama#macos). ### 6. Run app locally In your terminal at the root of your local Chatbot UI repository, run: ```bash npm run chat ``` Your local instance of Chatbot UI should now be running at [http://localhost:3000](http://localhost:3000). Be sure to use a compatible node version (i.e. v18). You can view your backend GUI at [http://localhost:54323/project/default/editor](http://localhost:54323/project/default/editor). ## Hosted Quickstart Follow these steps to get your own Chatbot UI instance running in the cloud. Video tutorial coming soon. ### 1. Follow Local Quickstart Repeat steps 1-4 in "Local Quickstart" above. You will want separate repositories for your local and hosted instances. Create a new repository for your hosted instance of Chatbot UI on GitHub and push your code to it. ### 2. Setup Backend with Supabase #### 1. Create a new project Go to [Supabase](https://supabase.com/) and create a new project. #### 2. Get Project Values Once you are in the project dashboard, click on the "Project Settings" icon tab on the far bottom left. Here you will get the values for the following environment variables: - `Project Ref`: Found in "General settings" as "Reference ID" - `Project ID`: Found in the URL of your project dashboard (Ex: https://supabase.com/dashboard/project//settings/general) While still in "Settings" click on the "API" text tab on the left. Here you will get the values for the following environment variables: - `Project URL`: Found in "API Settings" as "Project URL" - `Anon key`: Found in "Project API keys" as "anon public" - `Service role key`: Found in "Project API keys" as "service_role" (Reminder: Treat this like a password!) #### 3. Configure Auth Next, click on the "Authentication" icon tab on the far left. In the text tabs, click on "Providers" and make sure "Email" is enabled. We recommend turning off "Confirm email" for your own personal instance. #### 4. Connect to Hosted DB Open up your repository for your hosted instance of Chatbot UI. In the 1st migration file `supabase/migrations/20240108234540_setup.sql` you will need to replace 2 values with the values you got above: - `project_url` (line 53): Use the `Project URL` value from above - `service_role_key` (line 54): Use the `Service role key` value from above Now, open a terminal in the root directory of your local Chatbot UI repository. We will execute a few commands here. Login to Supabase by running: ```bash supabase login ``` Next, link your project by running the following command with the "Project ID" you got above: ```bash supabase link --project-ref ``` Your project should now be linked. Finally, push your database to Supabase by running: ```bash supabase db push ``` Your hosted database should now be set up! ### 3. Setup Frontend with Vercel Go to [Vercel](https://vercel.com/) and create a new project. In the setup page, import your GitHub repository for your hosted instance of Chatbot UI. Within the project Settings, in the "Build & Development Settings" section, switch Framework Preset to "Next.js". In environment variables, add the following from the values you got above: - `NEXT_PUBLIC_SUPABASE_URL` - `NEXT_PUBLIC_SUPABASE_ANON_KEY` - `SUPABASE_SERVICE_ROLE_KEY` - `NEXT_PUBLIC_OLLAMA_URL` (only needed when using local Ollama models; default: `http://localhost:11434`) You can also add API keys as environment variables. - `OPENAI_API_KEY` - `AZURE_OPENAI_API_KEY` - `AZURE_OPENAI_ENDPOINT` - `AZURE_GPT_45_VISION_NAME` For the full list of environment variables, refer to the '.env.local.example' file. If the environment variables are set for API keys, it will disable the input in the user settings. Click "Deploy" and wait for your frontend to deploy. Once deployed, you should be able to use your hosted instance of Chatbot UI via the URL Vercel gives you. ## Contributing We are working on a guide for contributing. ## Contact Message Mckay on [Twitter/X](https://twitter.com/mckaywrigley) ================================================ FILE: __tests__/lib/openapi-conversion.test.ts ================================================ import { openapiToFunctions } from "@/lib/openapi-conversion" const validSchemaURL = JSON.stringify({ openapi: "3.1.0", info: { title: "Get weather data", description: "Retrieves current weather data for a location.", version: "v1.0.0" }, servers: [ { url: "https://weather.example.com" } ], paths: { "/location": { get: { description: "Get temperature for a specific location", operationId: "GetCurrentWeather", parameters: [ { name: "location", in: "query", description: "The city and state to retrieve the weather for", required: true, schema: { type: "string" } } ] } }, "/summary": { get: { description: "Get description of weather for a specific location", operationId: "GetWeatherSummary", parameters: [ { name: "location", in: "query", description: "The city and state to retrieve the summary for", required: true, schema: { type: "string" } } ] } } } }) describe("extractOpenapiData for url", () => { it("should parse a valid OpenAPI url schema", async () => { const { info, routes, functions } = await openapiToFunctions( JSON.parse(validSchemaURL) ) expect(info.title).toBe("Get weather data") expect(info.description).toBe( "Retrieves current weather data for a location." ) expect(info.server).toBe("https://weather.example.com") expect(routes).toHaveLength(2) expect(functions).toHaveLength(2) expect(functions[0].function.name).toBe("GetCurrentWeather") expect(functions[1].function.name).toBe("GetWeatherSummary") }) }) const validSchemaBody = JSON.stringify({ openapi: "3.1.0", info: { title: "Get weather data", description: "Retrieves current weather data for a location.", version: "v1.0.0" }, servers: [ { url: "https://weather.example.com" } ], paths: { "/location": { post: { description: "Get temperature for a specific location", operationId: "GetCurrentWeather", requestBody: { required: true, content: { "application/json": { schema: { type: "object", properties: { location: { type: "string", description: "The city and state to retrieve the weather for", example: "New York, NY" } } } } } } } } } }) describe("extractOpenapiData for body", () => { it("should parse a valid OpenAPI body schema", async () => { const { info, routes, functions } = await openapiToFunctions( JSON.parse(validSchemaBody) ) expect(info.title).toBe("Get weather data") expect(info.description).toBe( "Retrieves current weather data for a location." ) expect(info.server).toBe("https://weather.example.com") expect(routes).toHaveLength(1) expect(routes[0].path).toBe("/location") expect(routes[0].method).toBe("post") expect(routes[0].operationId).toBe("GetCurrentWeather") expect(functions).toHaveLength(1) expect( functions[0].function.parameters.properties.requestBody.properties .location.type ).toBe("string") expect( functions[0].function.parameters.properties.requestBody.properties .location.description ).toBe("The city and state to retrieve the weather for") }) }) const validSchemaBody2 = JSON.stringify({ openapi: "3.1.0", info: { title: "Polygon.io Stock and Crypto Data API", description: "API schema for accessing stock and crypto data from Polygon.io.", version: "1.0.0" }, servers: [ { url: "https://api.polygon.io" } ], paths: { "/v1/open-close/{stocksTicker}/{date}": { get: { summary: "Get Stock Daily Open and Close", description: "Get the daily open and close for a specific stock.", operationId: "getStockDailyOpenClose", parameters: [ { name: "stocksTicker", in: "path", required: true, schema: { type: "string" } }, { name: "date", in: "path", required: true, schema: { type: "string", format: "date" } } ] } }, "/v2/aggs/ticker/{stocksTicker}/prev": { get: { summary: "Get Stock Previous Close", description: "Get the previous closing data for a specific stock.", operationId: "getStockPreviousClose", parameters: [ { name: "stocksTicker", in: "path", required: true, schema: { type: "string" } } ] } }, "/v3/trades/{stockTicker}": { get: { summary: "Get Stock Trades", description: "Retrieve trades for a specific stock.", operationId: "getStockTrades", parameters: [ { name: "stockTicker", in: "path", required: true, schema: { type: "string" } } ] } }, "/v3/trades/{optionsTicker}": { get: { summary: "Get Options Trades", description: "Retrieve trades for a specific options ticker.", operationId: "getOptionsTrades", parameters: [ { name: "optionsTicker", in: "path", required: true, schema: { type: "string" } } ] } }, "/v2/last/trade/{optionsTicker}": { get: { summary: "Get Last Options Trade", description: "Get the last trade for a specific options ticker.", operationId: "getLastOptionsTrade", parameters: [ { name: "optionsTicker", in: "path", required: true, schema: { type: "string" } } ] } }, "/v1/open-close/crypto/{from}/{to}/{date}": { get: { summary: "Get Crypto Daily Open and Close", description: "Get daily open and close data for a specific cryptocurrency.", operationId: "getCryptoDailyOpenClose", parameters: [ { name: "from", in: "path", required: true, schema: { type: "string" } }, { name: "to", in: "path", required: true, schema: { type: "string" } }, { name: "date", in: "path", required: true, schema: { type: "string", format: "date" } } ] } }, "/v2/aggs/ticker/{cryptoTicker}/prev": { get: { summary: "Get Crypto Previous Close", description: "Get the previous closing data for a specific cryptocurrency.", operationId: "getCryptoPreviousClose", parameters: [ { name: "cryptoTicker", in: "path", required: true, schema: { type: "string" } } ] } } }, components: { securitySchemes: { BearerAuth: { type: "http", scheme: "bearer", bearerFormat: "API Key" } } }, security: [ { BearerAuth: [] } ] }) describe("extractOpenapiData for body 2", () => { it("should parse a valid OpenAPI body schema for body 2", async () => { const { info, routes, functions } = await openapiToFunctions( JSON.parse(validSchemaBody2) ) expect(info.title).toBe("Polygon.io Stock and Crypto Data API") expect(info.description).toBe( "API schema for accessing stock and crypto data from Polygon.io." ) expect(info.server).toBe("https://api.polygon.io") expect(routes).toHaveLength(7) expect(routes[0].path).toBe("/v1/open-close/{stocksTicker}/{date}") expect(routes[0].method).toBe("get") expect(routes[0].operationId).toBe("getStockDailyOpenClose") expect(functions[0].function.parameters.properties).toHaveProperty( "stocksTicker" ) expect(functions[0].function.parameters.properties.stocksTicker.type).toBe( "string" ) expect( functions[0].function.parameters.properties.stocksTicker ).toHaveProperty("required", true) expect(functions[0].function.parameters.properties).toHaveProperty("date") expect(functions[0].function.parameters.properties.date.type).toBe("string") expect(functions[0].function.parameters.properties.date).toHaveProperty( "format", "date" ) expect(functions[0].function.parameters.properties.date).toHaveProperty( "required", true ) expect(routes[1].path).toBe("/v2/aggs/ticker/{stocksTicker}/prev") expect(routes[1].method).toBe("get") expect(routes[1].operationId).toBe("getStockPreviousClose") expect(functions[1].function.parameters.properties).toHaveProperty( "stocksTicker" ) expect(functions[1].function.parameters.properties.stocksTicker.type).toBe( "string" ) expect( functions[1].function.parameters.properties.stocksTicker ).toHaveProperty("required", true) }) }) ================================================ FILE: __tests__/playwright-test/.gitignore ================================================ node_modules/ /test-results/ /playwright-report/ /blob-report/ /playwright/.cache/ ================================================ FILE: __tests__/playwright-test/package.json ================================================ { "name": "playwright-test", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "integration": "playwright test", "integration:open": "playwright test --ui", "integration:codegen": "playwright codegen" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "@playwright/test": "^1.41.2", "@types/node": "^20.11.20" } } ================================================ FILE: __tests__/playwright-test/playwright.config.ts ================================================ import { defineConfig, devices } from '@playwright/test'; /** * Read environment variables from file. * https://github.com/motdotla/dotenv */ // require('dotenv').config(); /** * See https://playwright.dev/docs/test-configuration. */ export default defineConfig({ testDir: './tests', /* Run tests in files in parallel */ fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, /* Retry on CI only */ retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: 'html', /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ // baseURL: 'http://127.0.0.1:3000', /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry', }, /* Configure projects for major browsers */ projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, { name: 'firefox', use: { ...devices['Desktop Firefox'] }, }, { name: 'webkit', use: { ...devices['Desktop Safari'] }, }, /* Test against mobile viewports. */ // { // name: 'Mobile Chrome', // use: { ...devices['Pixel 5'] }, // }, // { // name: 'Mobile Safari', // use: { ...devices['iPhone 12'] }, // }, /* Test against branded browsers. */ // { // name: 'Microsoft Edge', // use: { ...devices['Desktop Edge'], channel: 'msedge' }, // }, // { // name: 'Google Chrome', // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, // }, ], /* Run your local dev server before starting the tests */ // webServer: { // command: 'npm run start', // url: 'http://127.0.0.1:3000', // reuseExistingServer: !process.env.CI, // }, }); ================================================ FILE: __tests__/playwright-test/tests/login.spec.ts ================================================ import { test, expect } from '@playwright/test'; test('start chatting is displayed', async ({ page }) => { await page.goto('http://localhost:3000/'); //expect the start chatting link to be visible await expect (page.getByRole('link', { name: 'Start Chatting' })).toBeVisible(); }); test('No password error message', async ({ page }) => { await page.goto('http://localhost:3000/login'); //fill in dummy email await page.getByPlaceholder('you@example.com').fill('dummyemail@gmail.com'); await page.getByRole('button', { name: 'Login' }).click(); //wait for netwrok to be idle await page.waitForLoadState('networkidle'); //validate that correct message is shown to the user await expect(page.getByText('Invalid login credentials')).toBeVisible(); }); test('No password for signup', async ({ page }) => { await page.goto('http://localhost:3000/login'); await page.getByPlaceholder('you@example.com').fill('dummyEmail@Gmail.com'); await page.getByRole('button', { name: 'Sign Up' }).click(); //validate appropriate error is thrown for missing password when signing up await expect(page.getByText('Signup requires a valid')).toBeVisible(); }); test('invalid username for signup', async ({ page }) => { await page.goto('http://localhost:3000/login'); await page.getByPlaceholder('you@example.com').fill('dummyEmail'); await page.getByPlaceholder('••••••••').fill('dummypassword'); await page.getByRole('button', { name: 'Sign Up' }).click(); //validate appropriate error is thrown for invalid username when signing up await expect(page.getByText('Unable to validate email')).toBeVisible(); }); test('password reset message', async ({ page }) => { await page.goto('http://localhost:3000/login'); await page.getByPlaceholder('you@example.com').fill('demo@gmail.com'); await page.getByRole('button', { name: 'Reset' }).click(); //validate appropriate message is shown await expect(page.getByText('Check email to reset password')).toBeVisible(); }); //more tests can be added here ================================================ FILE: app/[locale]/[workspaceid]/chat/[chatid]/page.tsx ================================================ "use client" import { ChatUI } from "@/components/chat/chat-ui" export default function ChatIDPage() { return } ================================================ FILE: app/[locale]/[workspaceid]/chat/page.tsx ================================================ "use client" import { ChatHelp } from "@/components/chat/chat-help" import { useChatHandler } from "@/components/chat/chat-hooks/use-chat-handler" import { ChatInput } from "@/components/chat/chat-input" import { ChatSettings } from "@/components/chat/chat-settings" import { ChatUI } from "@/components/chat/chat-ui" import { QuickSettings } from "@/components/chat/quick-settings" import { Brand } from "@/components/ui/brand" import { ChatbotUIContext } from "@/context/context" import useHotkey from "@/lib/hooks/use-hotkey" import { useTheme } from "next-themes" import { useContext } from "react" export default function ChatPage() { useHotkey("o", () => handleNewChat()) useHotkey("l", () => { handleFocusChatInput() }) const { chatMessages } = useContext(ChatbotUIContext) const { handleNewChat, handleFocusChatInput } = useChatHandler() const { theme } = useTheme() return ( <> {chatMessages.length === 0 ? (
) : ( )} ) } ================================================ FILE: app/[locale]/[workspaceid]/layout.tsx ================================================ "use client" import { Dashboard } from "@/components/ui/dashboard" import { ChatbotUIContext } from "@/context/context" import { getAssistantWorkspacesByWorkspaceId } from "@/db/assistants" import { getChatsByWorkspaceId } from "@/db/chats" import { getCollectionWorkspacesByWorkspaceId } from "@/db/collections" import { getFileWorkspacesByWorkspaceId } from "@/db/files" import { getFoldersByWorkspaceId } from "@/db/folders" import { getModelWorkspacesByWorkspaceId } from "@/db/models" import { getPresetWorkspacesByWorkspaceId } from "@/db/presets" import { getPromptWorkspacesByWorkspaceId } from "@/db/prompts" import { getAssistantImageFromStorage } from "@/db/storage/assistant-images" import { getToolWorkspacesByWorkspaceId } from "@/db/tools" import { getWorkspaceById } from "@/db/workspaces" import { convertBlobToBase64 } from "@/lib/blob-to-b64" import { supabase } from "@/lib/supabase/browser-client" import { LLMID } from "@/types" import { useParams, useRouter, useSearchParams } from "next/navigation" import { ReactNode, useContext, useEffect, useState } from "react" import Loading from "../loading" interface WorkspaceLayoutProps { children: ReactNode } export default function WorkspaceLayout({ children }: WorkspaceLayoutProps) { const router = useRouter() const params = useParams() const searchParams = useSearchParams() const workspaceId = params.workspaceid as string const { setChatSettings, setAssistants, setAssistantImages, setChats, setCollections, setFolders, setFiles, setPresets, setPrompts, setTools, setModels, selectedWorkspace, setSelectedWorkspace, setSelectedChat, setChatMessages, setUserInput, setIsGenerating, setFirstTokenReceived, setChatFiles, setChatImages, setNewMessageFiles, setNewMessageImages, setShowFilesDisplay } = useContext(ChatbotUIContext) const [loading, setLoading] = useState(true) useEffect(() => { ;(async () => { const session = (await supabase.auth.getSession()).data.session if (!session) { return router.push("/login") } else { await fetchWorkspaceData(workspaceId) } })() }, []) useEffect(() => { ;(async () => await fetchWorkspaceData(workspaceId))() setUserInput("") setChatMessages([]) setSelectedChat(null) setIsGenerating(false) setFirstTokenReceived(false) setChatFiles([]) setChatImages([]) setNewMessageFiles([]) setNewMessageImages([]) setShowFilesDisplay(false) }, [workspaceId]) const fetchWorkspaceData = async (workspaceId: string) => { setLoading(true) const workspace = await getWorkspaceById(workspaceId) setSelectedWorkspace(workspace) const assistantData = await getAssistantWorkspacesByWorkspaceId(workspaceId) setAssistants(assistantData.assistants) for (const assistant of assistantData.assistants) { let url = "" if (assistant.image_path) { url = (await getAssistantImageFromStorage(assistant.image_path)) || "" } if (url) { const response = await fetch(url) const blob = await response.blob() const base64 = await convertBlobToBase64(blob) setAssistantImages(prev => [ ...prev, { assistantId: assistant.id, path: assistant.image_path, base64, url } ]) } else { setAssistantImages(prev => [ ...prev, { assistantId: assistant.id, path: assistant.image_path, base64: "", url } ]) } } const chats = await getChatsByWorkspaceId(workspaceId) setChats(chats) const collectionData = await getCollectionWorkspacesByWorkspaceId(workspaceId) setCollections(collectionData.collections) const folders = await getFoldersByWorkspaceId(workspaceId) setFolders(folders) const fileData = await getFileWorkspacesByWorkspaceId(workspaceId) setFiles(fileData.files) const presetData = await getPresetWorkspacesByWorkspaceId(workspaceId) setPresets(presetData.presets) const promptData = await getPromptWorkspacesByWorkspaceId(workspaceId) setPrompts(promptData.prompts) const toolData = await getToolWorkspacesByWorkspaceId(workspaceId) setTools(toolData.tools) const modelData = await getModelWorkspacesByWorkspaceId(workspaceId) setModels(modelData.models) setChatSettings({ model: (searchParams.get("model") || workspace?.default_model || "gpt-4-1106-preview") as LLMID, prompt: workspace?.default_prompt || "You are a friendly, helpful AI assistant.", temperature: workspace?.default_temperature || 0.5, contextLength: workspace?.default_context_length || 4096, includeProfileContext: workspace?.include_profile_context || true, includeWorkspaceInstructions: workspace?.include_workspace_instructions || true, embeddingsProvider: (workspace?.embeddings_provider as "openai" | "local") || "openai" }) setLoading(false) } if (loading) { return } return {children} } ================================================ FILE: app/[locale]/[workspaceid]/page.tsx ================================================ "use client" import { ChatbotUIContext } from "@/context/context" import { useContext } from "react" export default function WorkspacePage() { const { selectedWorkspace } = useContext(ChatbotUIContext) return (
{selectedWorkspace?.name}
) } ================================================ FILE: app/[locale]/globals.css ================================================ @tailwind base; @tailwind components; @tailwind utilities; ::-webkit-scrollbar-track { background-color: transparent; } ::-webkit-scrollbar-thumb { background-color: #ccc; border-radius: 10px; } ::-webkit-scrollbar-thumb:hover { background-color: #aaa; } ::-webkit-scrollbar-track:hover { background-color: #f2f2f2; } ::-webkit-scrollbar-corner { background-color: transparent; } ::-webkit-scrollbar { width: 6px; height: 6px; } @layer base { :root { --background: 0 0% 100%; --foreground: 0 0% 3.9%; --muted: 0 0% 96.1%; --muted-foreground: 0 0% 45.1%; --popover: 0 0% 100%; --popover-foreground: 0 0% 3.9%; --card: 0 0% 100%; --card-foreground: 0 0% 3.9%; --border: 0 0% 89.8%; --input: 0 0% 89.8%; --primary: 0 0% 9%; --primary-foreground: 0 0% 98%; --secondary: 0 0% 96.1%; --secondary-foreground: 0 0% 9%; --accent: 0 0% 96.1%; --accent-foreground: 0 0% 9%; --destructive: 0 84.2% 60.2%; --destructive-foreground: 0 0% 98%; --ring: 0 0% 63.9%; --radius: 0.5rem; } .dark { --background: 0 0% 3.9%; --foreground: 0 0% 98%; --muted: 0 0% 14.9%; --muted-foreground: 0 0% 63.9%; --popover: 0 0% 3.9%; --popover-foreground: 0 0% 98%; --card: 0 0% 3.9%; --card-foreground: 0 0% 98%; --border: 0 0% 14.9%; --input: 0 0% 14.9%; --primary: 0 0% 98%; --primary-foreground: 0 0% 9%; --secondary: 0 0% 14.9%; --secondary-foreground: 0 0% 98%; --accent: 0 0% 14.9%; --accent-foreground: 0 0% 98%; --destructive: 0 62.8% 30.6%; --destructive-foreground: 0 85.7% 97.3%; --ring: 0 0% 14.9%; } } @layer base { * { @apply border-border; } body { @apply bg-background text-foreground; } } ================================================ FILE: app/[locale]/help/page.tsx ================================================ export default function HelpPage() { return (
Help under construction.
) } ================================================ FILE: app/[locale]/layout.tsx ================================================ import { Toaster } from "@/components/ui/sonner" import { GlobalState } from "@/components/utility/global-state" import { Providers } from "@/components/utility/providers" import TranslationsProvider from "@/components/utility/translations-provider" import initTranslations from "@/lib/i18n" import { Database } from "@/supabase/types" import { createServerClient } from "@supabase/ssr" import { Metadata, Viewport } from "next" import { Inter } from "next/font/google" import { cookies } from "next/headers" import { ReactNode } from "react" import "./globals.css" const inter = Inter({ subsets: ["latin"] }) const APP_NAME = "Chatbot UI" const APP_DEFAULT_TITLE = "Chatbot UI" const APP_TITLE_TEMPLATE = "%s - Chatbot UI" const APP_DESCRIPTION = "Chabot UI PWA!" interface RootLayoutProps { children: ReactNode params: { locale: string } } export const metadata: Metadata = { applicationName: APP_NAME, title: { default: APP_DEFAULT_TITLE, template: APP_TITLE_TEMPLATE }, description: APP_DESCRIPTION, manifest: "/manifest.json", appleWebApp: { capable: true, statusBarStyle: "black", title: APP_DEFAULT_TITLE // startUpImage: [], }, formatDetection: { telephone: false }, openGraph: { type: "website", siteName: APP_NAME, title: { default: APP_DEFAULT_TITLE, template: APP_TITLE_TEMPLATE }, description: APP_DESCRIPTION }, twitter: { card: "summary", title: { default: APP_DEFAULT_TITLE, template: APP_TITLE_TEMPLATE }, description: APP_DESCRIPTION } } export const viewport: Viewport = { themeColor: "#000000" } const i18nNamespaces = ["translation"] export default async function RootLayout({ children, params: { locale } }: RootLayoutProps) { const cookieStore = cookies() const supabase = createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { cookies: { get(name: string) { return cookieStore.get(name)?.value } } } ) const session = (await supabase.auth.getSession()).data.session const { t, resources } = await initTranslations(locale, i18nNamespaces) return (
{session ? {children} : children}
) } ================================================ FILE: app/[locale]/loading.tsx ================================================ import { IconLoader2 } from "@tabler/icons-react" export default function Loading() { return (
) } ================================================ FILE: app/[locale]/login/page.tsx ================================================ import { Brand } from "@/components/ui/brand" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { SubmitButton } from "@/components/ui/submit-button" import { createClient } from "@/lib/supabase/server" import { Database } from "@/supabase/types" import { createServerClient } from "@supabase/ssr" import { get } from "@vercel/edge-config" import { Metadata } from "next" import { cookies, headers } from "next/headers" import { redirect } from "next/navigation" export const metadata: Metadata = { title: "Login" } export default async function Login({ searchParams }: { searchParams: { message: string } }) { const cookieStore = cookies() const supabase = createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { cookies: { get(name: string) { return cookieStore.get(name)?.value } } } ) const session = (await supabase.auth.getSession()).data.session if (session) { const { data: homeWorkspace, error } = await supabase .from("workspaces") .select("*") .eq("user_id", session.user.id) .eq("is_home", true) .single() if (!homeWorkspace) { throw new Error(error.message) } return redirect(`/${homeWorkspace.id}/chat`) } const signIn = async (formData: FormData) => { "use server" const email = formData.get("email") as string const password = formData.get("password") as string const cookieStore = cookies() const supabase = createClient(cookieStore) const { data, error } = await supabase.auth.signInWithPassword({ email, password }) if (error) { return redirect(`/login?message=${error.message}`) } const { data: homeWorkspace, error: homeWorkspaceError } = await supabase .from("workspaces") .select("*") .eq("user_id", data.user.id) .eq("is_home", true) .single() if (!homeWorkspace) { throw new Error( homeWorkspaceError?.message || "An unexpected error occurred" ) } return redirect(`/${homeWorkspace.id}/chat`) } const getEnvVarOrEdgeConfigValue = async (name: string) => { "use server" if (process.env.EDGE_CONFIG) { return await get(name) } return process.env[name] } const signUp = async (formData: FormData) => { "use server" const email = formData.get("email") as string const password = formData.get("password") as string const emailDomainWhitelistPatternsString = await getEnvVarOrEdgeConfigValue( "EMAIL_DOMAIN_WHITELIST" ) const emailDomainWhitelist = emailDomainWhitelistPatternsString?.trim() ? emailDomainWhitelistPatternsString?.split(",") : [] const emailWhitelistPatternsString = await getEnvVarOrEdgeConfigValue("EMAIL_WHITELIST") const emailWhitelist = emailWhitelistPatternsString?.trim() ? emailWhitelistPatternsString?.split(",") : [] // If there are whitelist patterns, check if the email is allowed to sign up if (emailDomainWhitelist.length > 0 || emailWhitelist.length > 0) { const domainMatch = emailDomainWhitelist?.includes(email.split("@")[1]) const emailMatch = emailWhitelist?.includes(email) if (!domainMatch && !emailMatch) { return redirect( `/login?message=Email ${email} is not allowed to sign up.` ) } } const cookieStore = cookies() const supabase = createClient(cookieStore) const { error } = await supabase.auth.signUp({ email, password, options: { // USE IF YOU WANT TO SEND EMAIL VERIFICATION, ALSO CHANGE TOML FILE // emailRedirectTo: `${origin}/auth/callback` } }) if (error) { console.error(error) return redirect(`/login?message=${error.message}`) } return redirect("/setup") // USE IF YOU WANT TO SEND EMAIL VERIFICATION, ALSO CHANGE TOML FILE // return redirect("/login?message=Check email to continue sign in process") } const handleResetPassword = async (formData: FormData) => { "use server" const origin = headers().get("origin") const email = formData.get("email") as string const cookieStore = cookies() const supabase = createClient(cookieStore) const { error } = await supabase.auth.resetPasswordForEmail(email, { redirectTo: `${origin}/auth/callback?next=/login/password` }) if (error) { return redirect(`/login?message=${error.message}`) } return redirect("/login?message=Check email to reset password") } return (
Login Sign Up
Forgot your password?
{searchParams?.message && (

{searchParams.message}

)}
) } ================================================ FILE: app/[locale]/login/password/page.tsx ================================================ "use client" import { ChangePassword } from "@/components/utility/change-password" import { supabase } from "@/lib/supabase/browser-client" import { useRouter } from "next/navigation" import { useEffect, useState } from "react" export default function ChangePasswordPage() { const [loading, setLoading] = useState(true) const router = useRouter() useEffect(() => { ;(async () => { const session = (await supabase.auth.getSession()).data.session if (!session) { router.push("/login") } else { setLoading(false) } })() }, []) if (loading) { return null } return } ================================================ FILE: app/[locale]/page.tsx ================================================ "use client" import { ChatbotUISVG } from "@/components/icons/chatbotui-svg" import { IconArrowRight } from "@tabler/icons-react" import { useTheme } from "next-themes" import Link from "next/link" export default function HomePage() { const { theme } = useTheme() return (
Chatbot UI
Start Chatting
) } ================================================ FILE: app/[locale]/setup/page.tsx ================================================ "use client" import { ChatbotUIContext } from "@/context/context" import { getProfileByUserId, updateProfile } from "@/db/profile" import { getHomeWorkspaceByUserId, getWorkspacesByUserId } from "@/db/workspaces" import { fetchHostedModels, fetchOpenRouterModels } from "@/lib/models/fetch-models" import { supabase } from "@/lib/supabase/browser-client" import { TablesUpdate } from "@/supabase/types" import { useRouter } from "next/navigation" import { useContext, useEffect, useState } from "react" import { APIStep } from "../../../components/setup/api-step" import { FinishStep } from "../../../components/setup/finish-step" import { ProfileStep } from "../../../components/setup/profile-step" import { SETUP_STEP_COUNT, StepContainer } from "../../../components/setup/step-container" export default function SetupPage() { const { profile, setProfile, setWorkspaces, setSelectedWorkspace, setEnvKeyMap, setAvailableHostedModels, setAvailableOpenRouterModels } = useContext(ChatbotUIContext) const router = useRouter() const [loading, setLoading] = useState(true) const [currentStep, setCurrentStep] = useState(1) // Profile Step const [displayName, setDisplayName] = useState("") const [username, setUsername] = useState(profile?.username || "") const [usernameAvailable, setUsernameAvailable] = useState(true) // API Step const [useAzureOpenai, setUseAzureOpenai] = useState(false) const [openaiAPIKey, setOpenaiAPIKey] = useState("") const [openaiOrgID, setOpenaiOrgID] = useState("") const [azureOpenaiAPIKey, setAzureOpenaiAPIKey] = useState("") const [azureOpenaiEndpoint, setAzureOpenaiEndpoint] = useState("") const [azureOpenai35TurboID, setAzureOpenai35TurboID] = useState("") const [azureOpenai45TurboID, setAzureOpenai45TurboID] = useState("") const [azureOpenai45VisionID, setAzureOpenai45VisionID] = useState("") const [azureOpenaiEmbeddingsID, setAzureOpenaiEmbeddingsID] = useState("") const [anthropicAPIKey, setAnthropicAPIKey] = useState("") const [googleGeminiAPIKey, setGoogleGeminiAPIKey] = useState("") const [mistralAPIKey, setMistralAPIKey] = useState("") const [groqAPIKey, setGroqAPIKey] = useState("") const [perplexityAPIKey, setPerplexityAPIKey] = useState("") const [openrouterAPIKey, setOpenrouterAPIKey] = useState("") useEffect(() => { ;(async () => { const session = (await supabase.auth.getSession()).data.session if (!session) { return router.push("/login") } else { const user = session.user const profile = await getProfileByUserId(user.id) setProfile(profile) setUsername(profile.username) if (!profile.has_onboarded) { setLoading(false) } else { const data = await fetchHostedModels(profile) if (!data) return setEnvKeyMap(data.envKeyMap) setAvailableHostedModels(data.hostedModels) if (profile["openrouter_api_key"] || data.envKeyMap["openrouter"]) { const openRouterModels = await fetchOpenRouterModels() if (!openRouterModels) return setAvailableOpenRouterModels(openRouterModels) } const homeWorkspaceId = await getHomeWorkspaceByUserId( session.user.id ) return router.push(`/${homeWorkspaceId}/chat`) } } })() }, []) const handleShouldProceed = (proceed: boolean) => { if (proceed) { if (currentStep === SETUP_STEP_COUNT) { handleSaveSetupSetting() } else { setCurrentStep(currentStep + 1) } } else { setCurrentStep(currentStep - 1) } } const handleSaveSetupSetting = async () => { const session = (await supabase.auth.getSession()).data.session if (!session) { return router.push("/login") } const user = session.user const profile = await getProfileByUserId(user.id) const updateProfilePayload: TablesUpdate<"profiles"> = { ...profile, has_onboarded: true, display_name: displayName, username, openai_api_key: openaiAPIKey, openai_organization_id: openaiOrgID, anthropic_api_key: anthropicAPIKey, google_gemini_api_key: googleGeminiAPIKey, mistral_api_key: mistralAPIKey, groq_api_key: groqAPIKey, perplexity_api_key: perplexityAPIKey, openrouter_api_key: openrouterAPIKey, use_azure_openai: useAzureOpenai, azure_openai_api_key: azureOpenaiAPIKey, azure_openai_endpoint: azureOpenaiEndpoint, azure_openai_35_turbo_id: azureOpenai35TurboID, azure_openai_45_turbo_id: azureOpenai45TurboID, azure_openai_45_vision_id: azureOpenai45VisionID, azure_openai_embeddings_id: azureOpenaiEmbeddingsID } const updatedProfile = await updateProfile(profile.id, updateProfilePayload) setProfile(updatedProfile) const workspaces = await getWorkspacesByUserId(profile.user_id) const homeWorkspace = workspaces.find(w => w.is_home) // There will always be a home workspace setSelectedWorkspace(homeWorkspace!) setWorkspaces(workspaces) return router.push(`/${homeWorkspace?.id}/chat`) } const renderStep = (stepNum: number) => { switch (stepNum) { // Profile Step case 1: return ( ) // API Step case 2: return ( ) // Finish Step case 3: return ( ) default: return null } } if (loading) { return null } return (
{renderStep(currentStep)}
) } ================================================ FILE: app/api/assistants/openai/route.ts ================================================ import { checkApiKey, getServerProfile } from "@/lib/server/server-chat-helpers" import { ServerRuntime } from "next" import OpenAI from "openai" export const runtime: ServerRuntime = "edge" export async function GET() { try { const profile = await getServerProfile() checkApiKey(profile.openai_api_key, "OpenAI") const openai = new OpenAI({ apiKey: profile.openai_api_key || "", organization: profile.openai_organization_id }) const myAssistants = await openai.beta.assistants.list({ limit: 100 }) return new Response(JSON.stringify({ assistants: myAssistants.data }), { status: 200 }) } catch (error: any) { const errorMessage = error.error?.message || "An unexpected error occurred" const errorCode = error.status || 500 return new Response(JSON.stringify({ message: errorMessage }), { status: errorCode }) } } ================================================ FILE: app/api/chat/anthropic/route.ts ================================================ import { CHAT_SETTING_LIMITS } from "@/lib/chat-setting-limits" import { checkApiKey, getServerProfile } from "@/lib/server/server-chat-helpers" import { getBase64FromDataURL, getMediaTypeFromDataURL } from "@/lib/utils" import { ChatSettings } from "@/types" import Anthropic from "@anthropic-ai/sdk" import { AnthropicStream, StreamingTextResponse } from "ai" import { NextRequest, NextResponse } from "next/server" export const runtime = "edge" export async function POST(request: NextRequest) { const json = await request.json() const { chatSettings, messages } = json as { chatSettings: ChatSettings messages: any[] } try { const profile = await getServerProfile() checkApiKey(profile.anthropic_api_key, "Anthropic") let ANTHROPIC_FORMATTED_MESSAGES: any = messages.slice(1) ANTHROPIC_FORMATTED_MESSAGES = ANTHROPIC_FORMATTED_MESSAGES?.map( (message: any) => { const messageContent = typeof message?.content === "string" ? [message.content] : message?.content return { ...message, content: messageContent.map((content: any) => { if (typeof content === "string") { // Handle the case where content is a string return { type: "text", text: content } } else if ( content?.type === "image_url" && content?.image_url?.url?.length ) { return { type: "image", source: { type: "base64", media_type: getMediaTypeFromDataURL(content.image_url.url), data: getBase64FromDataURL(content.image_url.url) } } } else { return content } }) } } ) const anthropic = new Anthropic({ apiKey: profile.anthropic_api_key || "" }) try { const response = await anthropic.messages.create({ model: chatSettings.model, messages: ANTHROPIC_FORMATTED_MESSAGES, temperature: chatSettings.temperature, system: messages[0].content, max_tokens: CHAT_SETTING_LIMITS[chatSettings.model].MAX_TOKEN_OUTPUT_LENGTH, stream: true }) try { const stream = AnthropicStream(response) return new StreamingTextResponse(stream) } catch (error: any) { console.error("Error parsing Anthropic API response:", error) return new NextResponse( JSON.stringify({ message: "An error occurred while parsing the Anthropic API response" }), { status: 500 } ) } } catch (error: any) { console.error("Error calling Anthropic API:", error) return new NextResponse( JSON.stringify({ message: "An error occurred while calling the Anthropic API" }), { status: 500 } ) } } catch (error: any) { let errorMessage = error.message || "An unexpected error occurred" const errorCode = error.status || 500 if (errorMessage.toLowerCase().includes("api key not found")) { errorMessage = "Anthropic API Key not found. Please set it in your profile settings." } else if (errorCode === 401) { errorMessage = "Anthropic API Key is incorrect. Please fix it in your profile settings." } return new NextResponse(JSON.stringify({ message: errorMessage }), { status: errorCode }) } } ================================================ FILE: app/api/chat/azure/route.ts ================================================ import { checkApiKey, getServerProfile } from "@/lib/server/server-chat-helpers" import { ChatAPIPayload } from "@/types" import { OpenAIStream, StreamingTextResponse } from "ai" import OpenAI from "openai" import { ChatCompletionCreateParamsBase } from "openai/resources/chat/completions.mjs" export const runtime = "edge" export async function POST(request: Request) { const json = await request.json() const { chatSettings, messages } = json as ChatAPIPayload try { const profile = await getServerProfile() checkApiKey(profile.azure_openai_api_key, "Azure OpenAI") const ENDPOINT = profile.azure_openai_endpoint const KEY = profile.azure_openai_api_key let DEPLOYMENT_ID = "" switch (chatSettings.model) { case "gpt-3.5-turbo": DEPLOYMENT_ID = profile.azure_openai_35_turbo_id || "" break case "gpt-4-turbo-preview": DEPLOYMENT_ID = profile.azure_openai_45_turbo_id || "" break case "gpt-4-vision-preview": DEPLOYMENT_ID = profile.azure_openai_45_vision_id || "" break default: return new Response(JSON.stringify({ message: "Model not found" }), { status: 400 }) } if (!ENDPOINT || !KEY || !DEPLOYMENT_ID) { return new Response( JSON.stringify({ message: "Azure resources not found" }), { status: 400 } ) } const azureOpenai = new OpenAI({ apiKey: KEY, baseURL: `${ENDPOINT}/openai/deployments/${DEPLOYMENT_ID}`, defaultQuery: { "api-version": "2023-12-01-preview" }, defaultHeaders: { "api-key": KEY } }) const response = await azureOpenai.chat.completions.create({ model: DEPLOYMENT_ID as ChatCompletionCreateParamsBase["model"], messages: messages as ChatCompletionCreateParamsBase["messages"], temperature: chatSettings.temperature, max_tokens: chatSettings.model === "gpt-4-vision-preview" ? 4096 : null, // TODO: Fix stream: true }) const stream = OpenAIStream(response) return new StreamingTextResponse(stream) } catch (error: any) { const errorMessage = error.error?.message || "An unexpected error occurred" const errorCode = error.status || 500 return new Response(JSON.stringify({ message: errorMessage }), { status: errorCode }) } } ================================================ FILE: app/api/chat/custom/route.ts ================================================ import { Database } from "@/supabase/types" import { ChatSettings } from "@/types" import { createClient } from "@supabase/supabase-js" import { OpenAIStream, StreamingTextResponse } from "ai" import { ServerRuntime } from "next" import OpenAI from "openai" import { ChatCompletionCreateParamsBase } from "openai/resources/chat/completions.mjs" export const runtime: ServerRuntime = "edge" export async function POST(request: Request) { const json = await request.json() const { chatSettings, messages, customModelId } = json as { chatSettings: ChatSettings messages: any[] customModelId: string } try { const supabaseAdmin = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY! ) const { data: customModel, error } = await supabaseAdmin .from("models") .select("*") .eq("id", customModelId) .single() if (!customModel) { throw new Error(error.message) } const custom = new OpenAI({ apiKey: customModel.api_key || "", baseURL: customModel.base_url }) const response = await custom.chat.completions.create({ model: chatSettings.model as ChatCompletionCreateParamsBase["model"], messages: messages as ChatCompletionCreateParamsBase["messages"], temperature: chatSettings.temperature, stream: true }) const stream = OpenAIStream(response) return new StreamingTextResponse(stream) } catch (error: any) { let errorMessage = error.message || "An unexpected error occurred" const errorCode = error.status || 500 if (errorMessage.toLowerCase().includes("api key not found")) { errorMessage = "Custom API Key not found. Please set it in your profile settings." } else if (errorMessage.toLowerCase().includes("incorrect api key")) { errorMessage = "Custom API Key is incorrect. Please fix it in your profile settings." } return new Response(JSON.stringify({ message: errorMessage }), { status: errorCode }) } } ================================================ FILE: app/api/chat/google/route.ts ================================================ import { checkApiKey, getServerProfile } from "@/lib/server/server-chat-helpers" import { ChatSettings } from "@/types" import { GoogleGenerativeAI } from "@google/generative-ai" export const runtime = "edge" export async function POST(request: Request) { const json = await request.json() const { chatSettings, messages } = json as { chatSettings: ChatSettings messages: any[] } try { const profile = await getServerProfile() checkApiKey(profile.google_gemini_api_key, "Google") const genAI = new GoogleGenerativeAI(profile.google_gemini_api_key || "") const googleModel = genAI.getGenerativeModel({ model: chatSettings.model }) const lastMessage = messages.pop() const chat = googleModel.startChat({ history: messages, generationConfig: { temperature: chatSettings.temperature } }) const response = await chat.sendMessageStream(lastMessage.parts) const encoder = new TextEncoder() const readableStream = new ReadableStream({ async start(controller) { for await (const chunk of response.stream) { const chunkText = chunk.text() controller.enqueue(encoder.encode(chunkText)) } controller.close() } }) return new Response(readableStream, { headers: { "Content-Type": "text/plain" } }) } catch (error: any) { let errorMessage = error.message || "An unexpected error occurred" const errorCode = error.status || 500 if (errorMessage.toLowerCase().includes("api key not found")) { errorMessage = "Google Gemini API Key not found. Please set it in your profile settings." } else if (errorMessage.toLowerCase().includes("api key not valid")) { errorMessage = "Google Gemini API Key is incorrect. Please fix it in your profile settings." } return new Response(JSON.stringify({ message: errorMessage }), { status: errorCode }) } } ================================================ FILE: app/api/chat/groq/route.ts ================================================ import { CHAT_SETTING_LIMITS } from "@/lib/chat-setting-limits" import { checkApiKey, getServerProfile } from "@/lib/server/server-chat-helpers" import { ChatSettings } from "@/types" import { OpenAIStream, StreamingTextResponse } from "ai" import OpenAI from "openai" export const runtime = "edge" export async function POST(request: Request) { const json = await request.json() const { chatSettings, messages } = json as { chatSettings: ChatSettings messages: any[] } try { const profile = await getServerProfile() checkApiKey(profile.groq_api_key, "G") // Groq is compatible with the OpenAI SDK const groq = new OpenAI({ apiKey: profile.groq_api_key || "", baseURL: "https://api.groq.com/openai/v1" }) const response = await groq.chat.completions.create({ model: chatSettings.model, messages, max_tokens: CHAT_SETTING_LIMITS[chatSettings.model].MAX_TOKEN_OUTPUT_LENGTH, stream: true }) // Convert the response into a friendly text-stream. const stream = OpenAIStream(response) // Respond with the stream return new StreamingTextResponse(stream) } catch (error: any) { let errorMessage = error.message || "An unexpected error occurred" const errorCode = error.status || 500 if (errorMessage.toLowerCase().includes("api key not found")) { errorMessage = "Groq API Key not found. Please set it in your profile settings." } else if (errorCode === 401) { errorMessage = "Groq API Key is incorrect. Please fix it in your profile settings." } return new Response(JSON.stringify({ message: errorMessage }), { status: errorCode }) } } ================================================ FILE: app/api/chat/mistral/route.ts ================================================ import { CHAT_SETTING_LIMITS } from "@/lib/chat-setting-limits" import { checkApiKey, getServerProfile } from "@/lib/server/server-chat-helpers" import { ChatSettings } from "@/types" import { OpenAIStream, StreamingTextResponse } from "ai" import OpenAI from "openai" export const runtime = "edge" export async function POST(request: Request) { const json = await request.json() const { chatSettings, messages } = json as { chatSettings: ChatSettings messages: any[] } try { const profile = await getServerProfile() checkApiKey(profile.mistral_api_key, "Mistral") // Mistral is compatible the OpenAI SDK const mistral = new OpenAI({ apiKey: profile.mistral_api_key || "", baseURL: "https://api.mistral.ai/v1" }) const response = await mistral.chat.completions.create({ model: chatSettings.model, messages, max_tokens: CHAT_SETTING_LIMITS[chatSettings.model].MAX_TOKEN_OUTPUT_LENGTH, stream: true }) // Convert the response into a friendly text-stream. const stream = OpenAIStream(response) // Respond with the stream return new StreamingTextResponse(stream) } catch (error: any) { let errorMessage = error.message || "An unexpected error occurred" const errorCode = error.status || 500 if (errorMessage.toLowerCase().includes("api key not found")) { errorMessage = "Mistral API Key not found. Please set it in your profile settings." } else if (errorCode === 401) { errorMessage = "Mistral API Key is incorrect. Please fix it in your profile settings." } return new Response(JSON.stringify({ message: errorMessage }), { status: errorCode }) } } ================================================ FILE: app/api/chat/openai/route.ts ================================================ import { checkApiKey, getServerProfile } from "@/lib/server/server-chat-helpers" import { ChatSettings } from "@/types" import { OpenAIStream, StreamingTextResponse } from "ai" import { ServerRuntime } from "next" import OpenAI from "openai" import { ChatCompletionCreateParamsBase } from "openai/resources/chat/completions.mjs" export const runtime: ServerRuntime = "edge" export async function POST(request: Request) { const json = await request.json() const { chatSettings, messages } = json as { chatSettings: ChatSettings messages: any[] } try { const profile = await getServerProfile() checkApiKey(profile.openai_api_key, "OpenAI") const openai = new OpenAI({ apiKey: profile.openai_api_key || "", organization: profile.openai_organization_id }) const response = await openai.chat.completions.create({ model: chatSettings.model as ChatCompletionCreateParamsBase["model"], messages: messages as ChatCompletionCreateParamsBase["messages"], temperature: chatSettings.temperature, max_tokens: chatSettings.model === "gpt-4-vision-preview" || chatSettings.model === "gpt-4o" ? 4096 : null, // TODO: Fix stream: true }) const stream = OpenAIStream(response) return new StreamingTextResponse(stream) } catch (error: any) { let errorMessage = error.message || "An unexpected error occurred" const errorCode = error.status || 500 if (errorMessage.toLowerCase().includes("api key not found")) { errorMessage = "OpenAI API Key not found. Please set it in your profile settings." } else if (errorMessage.toLowerCase().includes("incorrect api key")) { errorMessage = "OpenAI API Key is incorrect. Please fix it in your profile settings." } return new Response(JSON.stringify({ message: errorMessage }), { status: errorCode }) } } ================================================ FILE: app/api/chat/openrouter/route.ts ================================================ import { checkApiKey, getServerProfile } from "@/lib/server/server-chat-helpers" import { ChatSettings } from "@/types" import { OpenAIStream, StreamingTextResponse } from "ai" import { ServerRuntime } from "next" import OpenAI from "openai" import { ChatCompletionCreateParamsBase } from "openai/resources/chat/completions.mjs" export const runtime: ServerRuntime = "edge" export async function POST(request: Request) { const json = await request.json() const { chatSettings, messages } = json as { chatSettings: ChatSettings messages: any[] } try { const profile = await getServerProfile() checkApiKey(profile.openrouter_api_key, "OpenRouter") const openai = new OpenAI({ apiKey: profile.openrouter_api_key || "", baseURL: "https://openrouter.ai/api/v1" }) const response = await openai.chat.completions.create({ model: chatSettings.model as ChatCompletionCreateParamsBase["model"], messages: messages as ChatCompletionCreateParamsBase["messages"], temperature: chatSettings.temperature, max_tokens: undefined, stream: true }) const stream = OpenAIStream(response) return new StreamingTextResponse(stream) } catch (error: any) { let errorMessage = error.message || "An unexpected error occurred" const errorCode = error.status || 500 if (errorMessage.toLowerCase().includes("api key not found")) { errorMessage = "OpenRouter API Key not found. Please set it in your profile settings." } return new Response(JSON.stringify({ message: errorMessage }), { status: errorCode }) } } ================================================ FILE: app/api/chat/perplexity/route.ts ================================================ import { checkApiKey, getServerProfile } from "@/lib/server/server-chat-helpers" import { ChatSettings } from "@/types" import { OpenAIStream, StreamingTextResponse } from "ai" import OpenAI from "openai" export const runtime = "edge" export async function POST(request: Request) { const json = await request.json() const { chatSettings, messages } = json as { chatSettings: ChatSettings messages: any[] } try { const profile = await getServerProfile() checkApiKey(profile.perplexity_api_key, "Perplexity") // Perplexity is compatible the OpenAI SDK const perplexity = new OpenAI({ apiKey: profile.perplexity_api_key || "", baseURL: "https://api.perplexity.ai/" }) const response = await perplexity.chat.completions.create({ model: chatSettings.model, messages, stream: true }) const stream = OpenAIStream(response) return new StreamingTextResponse(stream) } catch (error: any) { let errorMessage = error.message || "An unexpected error occurred" const errorCode = error.status || 500 if (errorMessage.toLowerCase().includes("api key not found")) { errorMessage = "Perplexity API Key not found. Please set it in your profile settings." } else if (errorCode === 401) { errorMessage = "Perplexity API Key is incorrect. Please fix it in your profile settings." } return new Response(JSON.stringify({ message: errorMessage }), { status: errorCode }) } } ================================================ FILE: app/api/chat/tools/route.ts ================================================ import { openapiToFunctions } from "@/lib/openapi-conversion" import { checkApiKey, getServerProfile } from "@/lib/server/server-chat-helpers" import { Tables } from "@/supabase/types" import { ChatSettings } from "@/types" import { OpenAIStream, StreamingTextResponse } from "ai" import OpenAI from "openai" import { ChatCompletionCreateParamsBase } from "openai/resources/chat/completions.mjs" export async function POST(request: Request) { const json = await request.json() const { chatSettings, messages, selectedTools } = json as { chatSettings: ChatSettings messages: any[] selectedTools: Tables<"tools">[] } try { const profile = await getServerProfile() checkApiKey(profile.openai_api_key, "OpenAI") const openai = new OpenAI({ apiKey: profile.openai_api_key || "", organization: profile.openai_organization_id }) let allTools: OpenAI.Chat.Completions.ChatCompletionTool[] = [] let allRouteMaps = {} let schemaDetails = [] for (const selectedTool of selectedTools) { try { const convertedSchema = await openapiToFunctions( JSON.parse(selectedTool.schema as string) ) const tools = convertedSchema.functions || [] allTools = allTools.concat(tools) const routeMap = convertedSchema.routes.reduce( (map: Record, route) => { map[route.path.replace(/{(\w+)}/g, ":$1")] = route.operationId return map }, {} ) allRouteMaps = { ...allRouteMaps, ...routeMap } schemaDetails.push({ title: convertedSchema.info.title, description: convertedSchema.info.description, url: convertedSchema.info.server, headers: selectedTool.custom_headers, routeMap, requestInBody: convertedSchema.routes[0].requestInBody }) } catch (error: any) { console.error("Error converting schema", error) } } const firstResponse = await openai.chat.completions.create({ model: chatSettings.model as ChatCompletionCreateParamsBase["model"], messages, tools: allTools.length > 0 ? allTools : undefined }) const message = firstResponse.choices[0].message messages.push(message) const toolCalls = message.tool_calls || [] if (toolCalls.length === 0) { return new Response(message.content, { headers: { "Content-Type": "application/json" } }) } if (toolCalls.length > 0) { for (const toolCall of toolCalls) { const functionCall = toolCall.function const functionName = functionCall.name const argumentsString = toolCall.function.arguments.trim() const parsedArgs = JSON.parse(argumentsString) // Find the schema detail that contains the function name const schemaDetail = schemaDetails.find(detail => Object.values(detail.routeMap).includes(functionName) ) if (!schemaDetail) { throw new Error(`Function ${functionName} not found in any schema`) } const pathTemplate = Object.keys(schemaDetail.routeMap).find( key => schemaDetail.routeMap[key] === functionName ) if (!pathTemplate) { throw new Error(`Path for function ${functionName} not found`) } const path = pathTemplate.replace(/:(\w+)/g, (_, paramName) => { const value = parsedArgs.parameters[paramName] if (!value) { throw new Error( `Parameter ${paramName} not found for function ${functionName}` ) } return encodeURIComponent(value) }) if (!path) { throw new Error(`Path for function ${functionName} not found`) } // Determine if the request should be in the body or as a query const isRequestInBody = schemaDetail.requestInBody let data = {} if (isRequestInBody) { // If the type is set to body let headers = { "Content-Type": "application/json" } // Check if custom headers are set const customHeaders = schemaDetail.headers // Moved this line up to the loop // Check if custom headers are set and are of type string if (customHeaders && typeof customHeaders === "string") { let parsedCustomHeaders = JSON.parse(customHeaders) as Record< string, string > headers = { ...headers, ...parsedCustomHeaders } } const fullUrl = schemaDetail.url + path const bodyContent = parsedArgs.requestBody || parsedArgs const requestInit = { method: "POST", headers, body: JSON.stringify(bodyContent) // Use the extracted requestBody or the entire parsedArgs } const response = await fetch(fullUrl, requestInit) if (!response.ok) { data = { error: response.statusText } } else { data = await response.json() } } else { // If the type is set to query const queryParams = new URLSearchParams( parsedArgs.parameters ).toString() const fullUrl = schemaDetail.url + path + (queryParams ? "?" + queryParams : "") let headers = {} // Check if custom headers are set const customHeaders = schemaDetail.headers if (customHeaders && typeof customHeaders === "string") { headers = JSON.parse(customHeaders) } const response = await fetch(fullUrl, { method: "GET", headers: headers }) if (!response.ok) { data = { error: response.statusText } } else { data = await response.json() } } messages.push({ tool_call_id: toolCall.id, role: "tool", name: functionName, content: JSON.stringify(data) }) } } const secondResponse = await openai.chat.completions.create({ model: chatSettings.model as ChatCompletionCreateParamsBase["model"], messages, stream: true }) const stream = OpenAIStream(secondResponse) return new StreamingTextResponse(stream) } catch (error: any) { console.error(error) const errorMessage = error.error?.message || "An unexpected error occurred" const errorCode = error.status || 500 return new Response(JSON.stringify({ message: errorMessage }), { status: errorCode }) } } ================================================ FILE: app/api/command/route.ts ================================================ import { CHAT_SETTING_LIMITS } from "@/lib/chat-setting-limits" import { checkApiKey, getServerProfile } from "@/lib/server/server-chat-helpers" import OpenAI from "openai" export const runtime = "edge" export async function POST(request: Request) { const json = await request.json() const { input } = json as { input: string } try { const profile = await getServerProfile() checkApiKey(profile.openai_api_key, "OpenAI") const openai = new OpenAI({ apiKey: profile.openai_api_key || "", organization: profile.openai_organization_id }) const response = await openai.chat.completions.create({ model: "gpt-4-1106-preview", messages: [ { role: "system", content: "Respond to the user." }, { role: "user", content: input } ], temperature: 0, max_tokens: CHAT_SETTING_LIMITS["gpt-4-turbo-preview"].MAX_TOKEN_OUTPUT_LENGTH // response_format: { type: "json_object" } // stream: true }) const content = response.choices[0].message.content return new Response(JSON.stringify({ content }), { status: 200 }) } catch (error: any) { const errorMessage = error.error?.message || "An unexpected error occurred" const errorCode = error.status || 500 return new Response(JSON.stringify({ message: errorMessage }), { status: errorCode }) } } ================================================ FILE: app/api/keys/route.ts ================================================ import { isUsingEnvironmentKey } from "@/lib/envs" import { createResponse } from "@/lib/server/server-utils" import { EnvKey } from "@/types/key-type" import { VALID_ENV_KEYS } from "@/types/valid-keys" export async function GET() { const envKeyMap: Record = { azure: VALID_ENV_KEYS.AZURE_OPENAI_API_KEY, openai: VALID_ENV_KEYS.OPENAI_API_KEY, google: VALID_ENV_KEYS.GOOGLE_GEMINI_API_KEY, anthropic: VALID_ENV_KEYS.ANTHROPIC_API_KEY, mistral: VALID_ENV_KEYS.MISTRAL_API_KEY, groq: VALID_ENV_KEYS.GROQ_API_KEY, perplexity: VALID_ENV_KEYS.PERPLEXITY_API_KEY, openrouter: VALID_ENV_KEYS.OPENROUTER_API_KEY, openai_organization_id: VALID_ENV_KEYS.OPENAI_ORGANIZATION_ID, azure_openai_endpoint: VALID_ENV_KEYS.AZURE_OPENAI_ENDPOINT, azure_gpt_35_turbo_name: VALID_ENV_KEYS.AZURE_GPT_35_TURBO_NAME, azure_gpt_45_vision_name: VALID_ENV_KEYS.AZURE_GPT_45_VISION_NAME, azure_gpt_45_turbo_name: VALID_ENV_KEYS.AZURE_GPT_45_TURBO_NAME, azure_embeddings_name: VALID_ENV_KEYS.AZURE_EMBEDDINGS_NAME } const isUsingEnvKeyMap = Object.keys(envKeyMap).reduce< Record >((acc, provider) => { const key = envKeyMap[provider] if (key) { acc[provider] = isUsingEnvironmentKey(key as EnvKey) } return acc }, {}) return createResponse({ isUsingEnvKeyMap }, 200) } ================================================ FILE: app/api/retrieval/process/docx/route.ts ================================================ import { generateLocalEmbedding } from "@/lib/generate-local-embedding" import { processDocX } from "@/lib/retrieval/processing" import { checkApiKey, getServerProfile } from "@/lib/server/server-chat-helpers" import { Database } from "@/supabase/types" import { FileItemChunk } from "@/types" import { createClient } from "@supabase/supabase-js" import { NextResponse } from "next/server" import OpenAI from "openai" export async function POST(req: Request) { const json = await req.json() const { text, fileId, embeddingsProvider, fileExtension } = json as { text: string fileId: string embeddingsProvider: "openai" | "local" fileExtension: string } try { const supabaseAdmin = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY! ) const profile = await getServerProfile() if (embeddingsProvider === "openai") { if (profile.use_azure_openai) { checkApiKey(profile.azure_openai_api_key, "Azure OpenAI") } else { checkApiKey(profile.openai_api_key, "OpenAI") } } let chunks: FileItemChunk[] = [] switch (fileExtension) { case "docx": chunks = await processDocX(text) break default: return new NextResponse("Unsupported file type", { status: 400 }) } let embeddings: any = [] let openai if (profile.use_azure_openai) { openai = new OpenAI({ apiKey: profile.azure_openai_api_key || "", baseURL: `${profile.azure_openai_endpoint}/openai/deployments/${profile.azure_openai_embeddings_id}`, defaultQuery: { "api-version": "2023-12-01-preview" }, defaultHeaders: { "api-key": profile.azure_openai_api_key } }) } else { openai = new OpenAI({ apiKey: profile.openai_api_key || "", organization: profile.openai_organization_id }) } if (embeddingsProvider === "openai") { const response = await openai.embeddings.create({ model: "text-embedding-3-small", input: chunks.map(chunk => chunk.content) }) embeddings = response.data.map((item: any) => { return item.embedding }) } else if (embeddingsProvider === "local") { const embeddingPromises = chunks.map(async chunk => { try { return await generateLocalEmbedding(chunk.content) } catch (error) { console.error(`Error generating embedding for chunk: ${chunk}`, error) return null } }) embeddings = await Promise.all(embeddingPromises) } const file_items = chunks.map((chunk, index) => ({ file_id: fileId, user_id: profile.user_id, content: chunk.content, tokens: chunk.tokens, openai_embedding: embeddingsProvider === "openai" ? ((embeddings[index] || null) as any) : null, local_embedding: embeddingsProvider === "local" ? ((embeddings[index] || null) as any) : null })) await supabaseAdmin.from("file_items").upsert(file_items) const totalTokens = file_items.reduce((acc, item) => acc + item.tokens, 0) await supabaseAdmin .from("files") .update({ tokens: totalTokens }) .eq("id", fileId) return new NextResponse("Embed Successful", { status: 200 }) } catch (error: any) { console.error(error) const errorMessage = error.error?.message || "An unexpected error occurred" const errorCode = error.status || 500 return new Response(JSON.stringify({ message: errorMessage }), { status: errorCode }) } } ================================================ FILE: app/api/retrieval/process/route.ts ================================================ import { generateLocalEmbedding } from "@/lib/generate-local-embedding" import { processCSV, processJSON, processMarkdown, processPdf, processTxt } from "@/lib/retrieval/processing" import { checkApiKey, getServerProfile } from "@/lib/server/server-chat-helpers" import { Database } from "@/supabase/types" import { FileItemChunk } from "@/types" import { createClient } from "@supabase/supabase-js" import { NextResponse } from "next/server" import OpenAI from "openai" export async function POST(req: Request) { try { const supabaseAdmin = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY! ) const profile = await getServerProfile() const formData = await req.formData() const file_id = formData.get("file_id") as string const embeddingsProvider = formData.get("embeddingsProvider") as string const { data: fileMetadata, error: metadataError } = await supabaseAdmin .from("files") .select("*") .eq("id", file_id) .single() if (metadataError) { throw new Error( `Failed to retrieve file metadata: ${metadataError.message}` ) } if (!fileMetadata) { throw new Error("File not found") } if (fileMetadata.user_id !== profile.user_id) { throw new Error("Unauthorized") } const { data: file, error: fileError } = await supabaseAdmin.storage .from("files") .download(fileMetadata.file_path) if (fileError) throw new Error(`Failed to retrieve file: ${fileError.message}`) const fileBuffer = Buffer.from(await file.arrayBuffer()) const blob = new Blob([fileBuffer]) const fileExtension = fileMetadata.name.split(".").pop()?.toLowerCase() if (embeddingsProvider === "openai") { try { if (profile.use_azure_openai) { checkApiKey(profile.azure_openai_api_key, "Azure OpenAI") } else { checkApiKey(profile.openai_api_key, "OpenAI") } } catch (error: any) { error.message = error.message + ", make sure it is configured or else use local embeddings" throw error } } let chunks: FileItemChunk[] = [] switch (fileExtension) { case "csv": chunks = await processCSV(blob) break case "json": chunks = await processJSON(blob) break case "md": chunks = await processMarkdown(blob) break case "pdf": chunks = await processPdf(blob) break case "txt": chunks = await processTxt(blob) break default: return new NextResponse("Unsupported file type", { status: 400 }) } let embeddings: any = [] let openai if (profile.use_azure_openai) { openai = new OpenAI({ apiKey: profile.azure_openai_api_key || "", baseURL: `${profile.azure_openai_endpoint}/openai/deployments/${profile.azure_openai_embeddings_id}`, defaultQuery: { "api-version": "2023-12-01-preview" }, defaultHeaders: { "api-key": profile.azure_openai_api_key } }) } else { openai = new OpenAI({ apiKey: profile.openai_api_key || "", organization: profile.openai_organization_id }) } if (embeddingsProvider === "openai") { const response = await openai.embeddings.create({ model: "text-embedding-3-small", input: chunks.map(chunk => chunk.content) }) embeddings = response.data.map((item: any) => { return item.embedding }) } else if (embeddingsProvider === "local") { const embeddingPromises = chunks.map(async chunk => { try { return await generateLocalEmbedding(chunk.content) } catch (error) { console.error(`Error generating embedding for chunk: ${chunk}`, error) return null } }) embeddings = await Promise.all(embeddingPromises) } const file_items = chunks.map((chunk, index) => ({ file_id, user_id: profile.user_id, content: chunk.content, tokens: chunk.tokens, openai_embedding: embeddingsProvider === "openai" ? ((embeddings[index] || null) as any) : null, local_embedding: embeddingsProvider === "local" ? ((embeddings[index] || null) as any) : null })) await supabaseAdmin.from("file_items").upsert(file_items) const totalTokens = file_items.reduce((acc, item) => acc + item.tokens, 0) await supabaseAdmin .from("files") .update({ tokens: totalTokens }) .eq("id", file_id) return new NextResponse("Embed Successful", { status: 200 }) } catch (error: any) { console.log(`Error in retrieval/process: ${error.stack}`) const errorMessage = error?.message || "An unexpected error occurred" const errorCode = error.status || 500 return new Response(JSON.stringify({ message: errorMessage }), { status: errorCode }) } } ================================================ FILE: app/api/retrieval/retrieve/route.ts ================================================ import { generateLocalEmbedding } from "@/lib/generate-local-embedding" import { checkApiKey, getServerProfile } from "@/lib/server/server-chat-helpers" import { Database } from "@/supabase/types" import { createClient } from "@supabase/supabase-js" import OpenAI from "openai" export async function POST(request: Request) { const json = await request.json() const { userInput, fileIds, embeddingsProvider, sourceCount } = json as { userInput: string fileIds: string[] embeddingsProvider: "openai" | "local" sourceCount: number } const uniqueFileIds = [...new Set(fileIds)] try { const supabaseAdmin = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY! ) const profile = await getServerProfile() if (embeddingsProvider === "openai") { if (profile.use_azure_openai) { checkApiKey(profile.azure_openai_api_key, "Azure OpenAI") } else { checkApiKey(profile.openai_api_key, "OpenAI") } } let chunks: any[] = [] let openai if (profile.use_azure_openai) { openai = new OpenAI({ apiKey: profile.azure_openai_api_key || "", baseURL: `${profile.azure_openai_endpoint}/openai/deployments/${profile.azure_openai_embeddings_id}`, defaultQuery: { "api-version": "2023-12-01-preview" }, defaultHeaders: { "api-key": profile.azure_openai_api_key } }) } else { openai = new OpenAI({ apiKey: profile.openai_api_key || "", organization: profile.openai_organization_id }) } if (embeddingsProvider === "openai") { const response = await openai.embeddings.create({ model: "text-embedding-3-small", input: userInput }) const openaiEmbedding = response.data.map(item => item.embedding)[0] const { data: openaiFileItems, error: openaiError } = await supabaseAdmin.rpc("match_file_items_openai", { query_embedding: openaiEmbedding as any, match_count: sourceCount, file_ids: uniqueFileIds }) if (openaiError) { throw openaiError } chunks = openaiFileItems } else if (embeddingsProvider === "local") { const localEmbedding = await generateLocalEmbedding(userInput) const { data: localFileItems, error: localFileItemsError } = await supabaseAdmin.rpc("match_file_items_local", { query_embedding: localEmbedding as any, match_count: sourceCount, file_ids: uniqueFileIds }) if (localFileItemsError) { throw localFileItemsError } chunks = localFileItems } const mostSimilarChunks = chunks?.sort( (a, b) => b.similarity - a.similarity ) return new Response(JSON.stringify({ results: mostSimilarChunks }), { status: 200 }) } catch (error: any) { const errorMessage = error.error?.message || "An unexpected error occurred" const errorCode = error.status || 500 return new Response(JSON.stringify({ message: errorMessage }), { status: errorCode }) } } ================================================ FILE: app/api/username/available/route.ts ================================================ import { Database } from "@/supabase/types" import { createClient } from "@supabase/supabase-js" export const runtime = "edge" export async function POST(request: Request) { const json = await request.json() const { username } = json as { username: string } try { const supabaseAdmin = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY! ) const { data: usernames, error } = await supabaseAdmin .from("profiles") .select("username") .eq("username", username) if (!usernames) { throw new Error(error.message) } return new Response(JSON.stringify({ isAvailable: !usernames.length }), { status: 200 }) } catch (error: any) { const errorMessage = error.error?.message || "An unexpected error occurred" const errorCode = error.status || 500 return new Response(JSON.stringify({ message: errorMessage }), { status: errorCode }) } } ================================================ FILE: app/api/username/get/route.ts ================================================ import { Database } from "@/supabase/types" import { createClient } from "@supabase/supabase-js" export const runtime = "edge" export async function POST(request: Request) { const json = await request.json() const { userId } = json as { userId: string } try { const supabaseAdmin = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY! ) const { data, error } = await supabaseAdmin .from("profiles") .select("username") .eq("user_id", userId) .single() if (!data) { throw new Error(error.message) } return new Response(JSON.stringify({ username: data.username }), { status: 200 }) } catch (error: any) { const errorMessage = error.error?.message || "An unexpected error occurred" const errorCode = error.status || 500 return new Response(JSON.stringify({ message: errorMessage }), { status: errorCode }) } } ================================================ FILE: app/auth/callback/route.ts ================================================ import { createClient } from "@/lib/supabase/server" import { cookies } from "next/headers" import { NextResponse } from "next/server" export async function GET(request: Request) { const requestUrl = new URL(request.url) const code = requestUrl.searchParams.get("code") const next = requestUrl.searchParams.get("next") if (code) { const cookieStore = cookies() const supabase = createClient(cookieStore) await supabase.auth.exchangeCodeForSession(code) } if (next) { return NextResponse.redirect(requestUrl.origin + next) } else { return NextResponse.redirect(requestUrl.origin) } } ================================================ FILE: components/chat/assistant-picker.tsx ================================================ import { ChatbotUIContext } from "@/context/context" import { Tables } from "@/supabase/types" import { IconRobotFace } from "@tabler/icons-react" import Image from "next/image" import { FC, useContext, useEffect, useRef } from "react" import { usePromptAndCommand } from "./chat-hooks/use-prompt-and-command" interface AssistantPickerProps {} export const AssistantPicker: FC = ({}) => { const { assistants, assistantImages, focusAssistant, atCommand, isAssistantPickerOpen, setIsAssistantPickerOpen } = useContext(ChatbotUIContext) const { handleSelectAssistant } = usePromptAndCommand() const itemsRef = useRef<(HTMLDivElement | null)[]>([]) useEffect(() => { if (focusAssistant && itemsRef.current[0]) { itemsRef.current[0].focus() } }, [focusAssistant]) const filteredAssistants = assistants.filter(assistant => assistant.name.toLowerCase().includes(atCommand.toLowerCase()) ) const handleOpenChange = (isOpen: boolean) => { setIsAssistantPickerOpen(isOpen) } const callSelectAssistant = (assistant: Tables<"assistants">) => { handleSelectAssistant(assistant) handleOpenChange(false) } const getKeyDownHandler = (index: number) => (e: React.KeyboardEvent) => { if (e.key === "Backspace") { e.preventDefault() handleOpenChange(false) } else if (e.key === "Enter") { e.preventDefault() callSelectAssistant(filteredAssistants[index]) } else if ( (e.key === "Tab" || e.key === "ArrowDown") && !e.shiftKey && index === filteredAssistants.length - 1 ) { e.preventDefault() itemsRef.current[0]?.focus() } else if (e.key === "ArrowUp" && !e.shiftKey && index === 0) { // go to last element if arrow up is pressed on first element e.preventDefault() itemsRef.current[itemsRef.current.length - 1]?.focus() } else if (e.key === "ArrowUp") { e.preventDefault() const prevIndex = index - 1 >= 0 ? index - 1 : itemsRef.current.length - 1 itemsRef.current[prevIndex]?.focus() } else if (e.key === "ArrowDown") { e.preventDefault() const nextIndex = index + 1 < itemsRef.current.length ? index + 1 : 0 itemsRef.current[nextIndex]?.focus() } } return ( <> {isAssistantPickerOpen && (
{filteredAssistants.length === 0 ? (
No matching assistants.
) : ( <> {filteredAssistants.map((item, index) => (
{ itemsRef.current[index] = ref }} tabIndex={0} className="hover:bg-accent focus:bg-accent flex cursor-pointer items-center rounded p-2 focus:outline-none" onClick={() => callSelectAssistant(item as Tables<"assistants">) } onKeyDown={getKeyDownHandler(index)} > {item.image_path ? ( image.path === item.image_path )?.url || "" } alt={item.name} width={32} height={32} className="rounded" /> ) : ( )}
{item.name}
{item.description || "No description."}
))} )}
)} ) } ================================================ FILE: components/chat/chat-command-input.tsx ================================================ import { ChatbotUIContext } from "@/context/context" import { FC, useContext } from "react" import { AssistantPicker } from "./assistant-picker" import { usePromptAndCommand } from "./chat-hooks/use-prompt-and-command" import { FilePicker } from "./file-picker" import { PromptPicker } from "./prompt-picker" import { ToolPicker } from "./tool-picker" interface ChatCommandInputProps {} export const ChatCommandInput: FC = ({}) => { const { newMessageFiles, chatFiles, slashCommand, isFilePickerOpen, setIsFilePickerOpen, hashtagCommand, focusPrompt, focusFile } = useContext(ChatbotUIContext) const { handleSelectUserFile, handleSelectUserCollection } = usePromptAndCommand() return ( <> file.id )} selectedCollectionIds={[]} onSelectFile={handleSelectUserFile} onSelectCollection={handleSelectUserCollection} isFocused={focusFile} /> ) } ================================================ FILE: components/chat/chat-files-display.tsx ================================================ import { ChatbotUIContext } from "@/context/context" import { getFileFromStorage } from "@/db/storage/files" import useHotkey from "@/lib/hooks/use-hotkey" import { cn } from "@/lib/utils" import { ChatFile, MessageImage } from "@/types" import { IconCircleFilled, IconFileFilled, IconFileTypeCsv, IconFileTypeDocx, IconFileTypePdf, IconFileTypeTxt, IconJson, IconLoader2, IconMarkdown, IconX } from "@tabler/icons-react" import Image from "next/image" import { FC, useContext, useState } from "react" import { Button } from "../ui/button" import { FilePreview } from "../ui/file-preview" import { WithTooltip } from "../ui/with-tooltip" import { ChatRetrievalSettings } from "./chat-retrieval-settings" interface ChatFilesDisplayProps {} export const ChatFilesDisplay: FC = ({}) => { useHotkey("f", () => setShowFilesDisplay(prev => !prev)) useHotkey("e", () => setUseRetrieval(prev => !prev)) const { files, newMessageImages, setNewMessageImages, newMessageFiles, setNewMessageFiles, setShowFilesDisplay, showFilesDisplay, chatFiles, chatImages, setChatImages, setChatFiles, setUseRetrieval } = useContext(ChatbotUIContext) const [selectedFile, setSelectedFile] = useState(null) const [selectedImage, setSelectedImage] = useState(null) const [showPreview, setShowPreview] = useState(false) const messageImages = [ ...newMessageImages.filter( image => !chatImages.some(chatImage => chatImage.messageId === image.messageId) ) ] const combinedChatFiles = [ ...newMessageFiles.filter( file => !chatFiles.some(chatFile => chatFile.id === file.id) ), ...chatFiles ] const combinedMessageFiles = [...messageImages, ...combinedChatFiles] const getLinkAndView = async (file: ChatFile) => { const fileRecord = files.find(f => f.id === file.id) if (!fileRecord) return const link = await getFileFromStorage(fileRecord.file_path) window.open(link, "_blank") } return showFilesDisplay && combinedMessageFiles.length > 0 ? ( <> {showPreview && selectedImage && ( { setShowPreview(isOpen) setSelectedImage(null) }} /> )} {showPreview && selectedFile && ( { setShowPreview(isOpen) setSelectedFile(null) }} /> )}
{messageImages.map((image, index) => (
File image { setSelectedImage(image) setShowPreview(true) }} /> { e.stopPropagation() setNewMessageImages( newMessageImages.filter( f => f.messageId !== image.messageId ) ) setChatImages( chatImages.filter(f => f.messageId !== image.messageId) ) }} />
))} {combinedChatFiles.map((file, index) => file.id === "loading" ? (
{file.name}
{file.type}
) : (
getLinkAndView(file)} >
{(() => { let fileExtension = file.type.includes("/") ? file.type.split("/")[1] : file.type switch (fileExtension) { case "pdf": return case "markdown": return case "txt": return case "json": return case "csv": return case "docx": return default: return } })()}
{file.name}
{ e.stopPropagation() setNewMessageFiles( newMessageFiles.filter(f => f.id !== file.id) ) setChatFiles(chatFiles.filter(f => f.id !== file.id)) }} />
) )}
) : ( combinedMessageFiles.length > 0 && (
) ) } const RetrievalToggle = ({}) => { const { useRetrieval, setUseRetrieval } = useContext(ChatbotUIContext) return (
{useRetrieval ? "File retrieval is enabled on the selected files for this message. Click the indicator to disable." : "Click the indicator to enable file retrieval for this message."}
} trigger={ { e.stopPropagation() setUseRetrieval(prev => !prev) }} /> } />
) } ================================================ FILE: components/chat/chat-help.tsx ================================================ import useHotkey from "@/lib/hooks/use-hotkey" import { IconBrandGithub, IconBrandX, IconHelpCircle, IconQuestionMark } from "@tabler/icons-react" import Link from "next/link" import { FC, useState } from "react" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "../ui/dropdown-menu" import { Announcements } from "../utility/announcements" interface ChatHelpProps {} export const ChatHelp: FC = ({}) => { useHotkey("/", () => setIsOpen(prevState => !prevState)) const [isOpen, setIsOpen] = useState(false) return (
Show Help
Shift
/
Show Workspaces
Shift
;
New Chat
Shift
O
Focus Chat
Shift
L
Toggle Files
Shift
F
Toggle Retrieval
Shift
E
Open Settings
Shift
I
Open Quick Settings
Shift
P
Toggle Sidebar
Shift
S
) } ================================================ FILE: components/chat/chat-helpers/index.ts ================================================ // Only used in use-chat-handler.tsx to keep it clean import { createChatFiles } from "@/db/chat-files" import { createChat } from "@/db/chats" import { createMessageFileItems } from "@/db/message-file-items" import { createMessages, updateMessage } from "@/db/messages" import { uploadMessageImage } from "@/db/storage/message-images" import { buildFinalMessages, adaptMessagesForGoogleGemini } from "@/lib/build-prompt" import { consumeReadableStream } from "@/lib/consume-stream" import { Tables, TablesInsert } from "@/supabase/types" import { ChatFile, ChatMessage, ChatPayload, ChatSettings, LLM, MessageImage } from "@/types" import React from "react" import { toast } from "sonner" import { v4 as uuidv4 } from "uuid" export const validateChatSettings = ( chatSettings: ChatSettings | null, modelData: LLM | undefined, profile: Tables<"profiles"> | null, selectedWorkspace: Tables<"workspaces"> | null, messageContent: string ) => { if (!chatSettings) { throw new Error("Chat settings not found") } if (!modelData) { throw new Error("Model not found") } if (!profile) { throw new Error("Profile not found") } if (!selectedWorkspace) { throw new Error("Workspace not found") } if (!messageContent) { throw new Error("Message content not found") } } export const handleRetrieval = async ( userInput: string, newMessageFiles: ChatFile[], chatFiles: ChatFile[], embeddingsProvider: "openai" | "local", sourceCount: number ) => { const response = await fetch("/api/retrieval/retrieve", { method: "POST", body: JSON.stringify({ userInput, fileIds: [...newMessageFiles, ...chatFiles].map(file => file.id), embeddingsProvider, sourceCount }) }) if (!response.ok) { console.error("Error retrieving:", response) } const { results } = (await response.json()) as { results: Tables<"file_items">[] } return results } export const createTempMessages = ( messageContent: string, chatMessages: ChatMessage[], chatSettings: ChatSettings, b64Images: string[], isRegeneration: boolean, setChatMessages: React.Dispatch>, selectedAssistant: Tables<"assistants"> | null ) => { let tempUserChatMessage: ChatMessage = { message: { chat_id: "", assistant_id: null, content: messageContent, created_at: "", id: uuidv4(), image_paths: b64Images, model: chatSettings.model, role: "user", sequence_number: chatMessages.length, updated_at: "", user_id: "" }, fileItems: [] } let tempAssistantChatMessage: ChatMessage = { message: { chat_id: "", assistant_id: selectedAssistant?.id || null, content: "", created_at: "", id: uuidv4(), image_paths: [], model: chatSettings.model, role: "assistant", sequence_number: chatMessages.length + 1, updated_at: "", user_id: "" }, fileItems: [] } let newMessages = [] if (isRegeneration) { const lastMessageIndex = chatMessages.length - 1 chatMessages[lastMessageIndex].message.content = "" newMessages = [...chatMessages] } else { newMessages = [ ...chatMessages, tempUserChatMessage, tempAssistantChatMessage ] } setChatMessages(newMessages) return { tempUserChatMessage, tempAssistantChatMessage } } export const handleLocalChat = async ( payload: ChatPayload, profile: Tables<"profiles">, chatSettings: ChatSettings, tempAssistantMessage: ChatMessage, isRegeneration: boolean, newAbortController: AbortController, setIsGenerating: React.Dispatch>, setFirstTokenReceived: React.Dispatch>, setChatMessages: React.Dispatch>, setToolInUse: React.Dispatch> ) => { const formattedMessages = await buildFinalMessages(payload, profile, []) // Ollama API: https://github.com/jmorganca/ollama/blob/main/docs/api.md const response = await fetchChatResponse( process.env.NEXT_PUBLIC_OLLAMA_URL + "/api/chat", { model: chatSettings.model, messages: formattedMessages, options: { temperature: payload.chatSettings.temperature } }, false, newAbortController, setIsGenerating, setChatMessages ) return await processResponse( response, isRegeneration ? payload.chatMessages[payload.chatMessages.length - 1] : tempAssistantMessage, false, newAbortController, setFirstTokenReceived, setChatMessages, setToolInUse ) } export const handleHostedChat = async ( payload: ChatPayload, profile: Tables<"profiles">, modelData: LLM, tempAssistantChatMessage: ChatMessage, isRegeneration: boolean, newAbortController: AbortController, newMessageImages: MessageImage[], chatImages: MessageImage[], setIsGenerating: React.Dispatch>, setFirstTokenReceived: React.Dispatch>, setChatMessages: React.Dispatch>, setToolInUse: React.Dispatch> ) => { const provider = modelData.provider === "openai" && profile.use_azure_openai ? "azure" : modelData.provider let draftMessages = await buildFinalMessages(payload, profile, chatImages) let formattedMessages : any[] = [] if (provider === "google") { formattedMessages = await adaptMessagesForGoogleGemini(payload, draftMessages) } else { formattedMessages = draftMessages } const apiEndpoint = provider === "custom" ? "/api/chat/custom" : `/api/chat/${provider}` const requestBody = { chatSettings: payload.chatSettings, messages: formattedMessages, customModelId: provider === "custom" ? modelData.hostedId : "" } const response = await fetchChatResponse( apiEndpoint, requestBody, true, newAbortController, setIsGenerating, setChatMessages ) return await processResponse( response, isRegeneration ? payload.chatMessages[payload.chatMessages.length - 1] : tempAssistantChatMessage, true, newAbortController, setFirstTokenReceived, setChatMessages, setToolInUse ) } export const fetchChatResponse = async ( url: string, body: object, isHosted: boolean, controller: AbortController, setIsGenerating: React.Dispatch>, setChatMessages: React.Dispatch> ) => { const response = await fetch(url, { method: "POST", body: JSON.stringify(body), signal: controller.signal }) if (!response.ok) { if (response.status === 404 && !isHosted) { toast.error( "Model not found. Make sure you have it downloaded via Ollama." ) } const errorData = await response.json() toast.error(errorData.message) setIsGenerating(false) setChatMessages(prevMessages => prevMessages.slice(0, -2)) } return response } export const processResponse = async ( response: Response, lastChatMessage: ChatMessage, isHosted: boolean, controller: AbortController, setFirstTokenReceived: React.Dispatch>, setChatMessages: React.Dispatch>, setToolInUse: React.Dispatch> ) => { let fullText = "" let contentToAdd = "" if (response.body) { await consumeReadableStream( response.body, chunk => { setFirstTokenReceived(true) setToolInUse("none") try { contentToAdd = isHosted ? chunk : // Ollama's streaming endpoint returns new-line separated JSON // objects. A chunk may have more than one of these objects, so we // need to split the chunk by new-lines and handle each one // separately. chunk .trimEnd() .split("\n") .reduce( (acc, line) => acc + JSON.parse(line).message.content, "" ) fullText += contentToAdd } catch (error) { console.error("Error parsing JSON:", error) } setChatMessages(prev => prev.map(chatMessage => { if (chatMessage.message.id === lastChatMessage.message.id) { const updatedChatMessage: ChatMessage = { message: { ...chatMessage.message, content: fullText }, fileItems: chatMessage.fileItems } return updatedChatMessage } return chatMessage }) ) }, controller.signal ) return fullText } else { throw new Error("Response body is null") } } export const handleCreateChat = async ( chatSettings: ChatSettings, profile: Tables<"profiles">, selectedWorkspace: Tables<"workspaces">, messageContent: string, selectedAssistant: Tables<"assistants">, newMessageFiles: ChatFile[], setSelectedChat: React.Dispatch | null>>, setChats: React.Dispatch[]>>, setChatFiles: React.Dispatch> ) => { const createdChat = await createChat({ user_id: profile.user_id, workspace_id: selectedWorkspace.id, assistant_id: selectedAssistant?.id || null, context_length: chatSettings.contextLength, include_profile_context: chatSettings.includeProfileContext, include_workspace_instructions: chatSettings.includeWorkspaceInstructions, model: chatSettings.model, name: messageContent.substring(0, 100), prompt: chatSettings.prompt, temperature: chatSettings.temperature, embeddings_provider: chatSettings.embeddingsProvider }) setSelectedChat(createdChat) setChats(chats => [createdChat, ...chats]) await createChatFiles( newMessageFiles.map(file => ({ user_id: profile.user_id, chat_id: createdChat.id, file_id: file.id })) ) setChatFiles(prev => [...prev, ...newMessageFiles]) return createdChat } export const handleCreateMessages = async ( chatMessages: ChatMessage[], currentChat: Tables<"chats">, profile: Tables<"profiles">, modelData: LLM, messageContent: string, generatedText: string, newMessageImages: MessageImage[], isRegeneration: boolean, retrievedFileItems: Tables<"file_items">[], setChatMessages: React.Dispatch>, setChatFileItems: React.Dispatch< React.SetStateAction[]> >, setChatImages: React.Dispatch>, selectedAssistant: Tables<"assistants"> | null ) => { const finalUserMessage: TablesInsert<"messages"> = { chat_id: currentChat.id, assistant_id: null, user_id: profile.user_id, content: messageContent, model: modelData.modelId, role: "user", sequence_number: chatMessages.length, image_paths: [] } const finalAssistantMessage: TablesInsert<"messages"> = { chat_id: currentChat.id, assistant_id: selectedAssistant?.id || null, user_id: profile.user_id, content: generatedText, model: modelData.modelId, role: "assistant", sequence_number: chatMessages.length + 1, image_paths: [] } let finalChatMessages: ChatMessage[] = [] if (isRegeneration) { const lastStartingMessage = chatMessages[chatMessages.length - 1].message const updatedMessage = await updateMessage(lastStartingMessage.id, { ...lastStartingMessage, content: generatedText }) chatMessages[chatMessages.length - 1].message = updatedMessage finalChatMessages = [...chatMessages] setChatMessages(finalChatMessages) } else { const createdMessages = await createMessages([ finalUserMessage, finalAssistantMessage ]) // Upload each image (stored in newMessageImages) for the user message to message_images bucket const uploadPromises = newMessageImages .filter(obj => obj.file !== null) .map(obj => { let filePath = `${profile.user_id}/${currentChat.id}/${ createdMessages[0].id }/${uuidv4()}` return uploadMessageImage(filePath, obj.file as File).catch(error => { console.error(`Failed to upload image at ${filePath}:`, error) return null }) }) const paths = (await Promise.all(uploadPromises)).filter( Boolean ) as string[] setChatImages(prevImages => [ ...prevImages, ...newMessageImages.map((obj, index) => ({ ...obj, messageId: createdMessages[0].id, path: paths[index] })) ]) const updatedMessage = await updateMessage(createdMessages[0].id, { ...createdMessages[0], image_paths: paths }) const createdMessageFileItems = await createMessageFileItems( retrievedFileItems.map(fileItem => { return { user_id: profile.user_id, message_id: createdMessages[1].id, file_item_id: fileItem.id } }) ) finalChatMessages = [ ...chatMessages, { message: updatedMessage, fileItems: [] }, { message: createdMessages[1], fileItems: retrievedFileItems.map(fileItem => fileItem.id) } ] setChatFileItems(prevFileItems => { const newFileItems = retrievedFileItems.filter( fileItem => !prevFileItems.some(prevItem => prevItem.id === fileItem.id) ) return [...prevFileItems, ...newFileItems] }) setChatMessages(finalChatMessages) } } ================================================ FILE: components/chat/chat-hooks/use-chat-handler.tsx ================================================ import { ChatbotUIContext } from "@/context/context" import { getAssistantCollectionsByAssistantId } from "@/db/assistant-collections" import { getAssistantFilesByAssistantId } from "@/db/assistant-files" import { getAssistantToolsByAssistantId } from "@/db/assistant-tools" import { updateChat } from "@/db/chats" import { getCollectionFilesByCollectionId } from "@/db/collection-files" import { deleteMessagesIncludingAndAfter } from "@/db/messages" import { buildFinalMessages } from "@/lib/build-prompt" import { Tables } from "@/supabase/types" import { ChatMessage, ChatPayload, LLMID, ModelProvider } from "@/types" import { useRouter } from "next/navigation" import { useContext, useEffect, useRef } from "react" import { LLM_LIST } from "../../../lib/models/llm/llm-list" import { createTempMessages, handleCreateChat, handleCreateMessages, handleHostedChat, handleLocalChat, handleRetrieval, processResponse, validateChatSettings } from "../chat-helpers" export const useChatHandler = () => { const router = useRouter() const { userInput, chatFiles, setUserInput, setNewMessageImages, profile, setIsGenerating, setChatMessages, setFirstTokenReceived, selectedChat, selectedWorkspace, setSelectedChat, setChats, setSelectedTools, availableLocalModels, availableOpenRouterModels, abortController, setAbortController, chatSettings, newMessageImages, selectedAssistant, chatMessages, chatImages, setChatImages, setChatFiles, setNewMessageFiles, setShowFilesDisplay, newMessageFiles, chatFileItems, setChatFileItems, setToolInUse, useRetrieval, sourceCount, setIsPromptPickerOpen, setIsFilePickerOpen, selectedTools, selectedPreset, setChatSettings, models, isPromptPickerOpen, isFilePickerOpen, isToolPickerOpen } = useContext(ChatbotUIContext) const chatInputRef = useRef(null) useEffect(() => { if (!isPromptPickerOpen || !isFilePickerOpen || !isToolPickerOpen) { chatInputRef.current?.focus() } }, [isPromptPickerOpen, isFilePickerOpen, isToolPickerOpen]) const handleNewChat = async () => { if (!selectedWorkspace) return setUserInput("") setChatMessages([]) setSelectedChat(null) setChatFileItems([]) setIsGenerating(false) setFirstTokenReceived(false) setChatFiles([]) setChatImages([]) setNewMessageFiles([]) setNewMessageImages([]) setShowFilesDisplay(false) setIsPromptPickerOpen(false) setIsFilePickerOpen(false) setSelectedTools([]) setToolInUse("none") if (selectedAssistant) { setChatSettings({ model: selectedAssistant.model as LLMID, prompt: selectedAssistant.prompt, temperature: selectedAssistant.temperature, contextLength: selectedAssistant.context_length, includeProfileContext: selectedAssistant.include_profile_context, includeWorkspaceInstructions: selectedAssistant.include_workspace_instructions, embeddingsProvider: selectedAssistant.embeddings_provider as | "openai" | "local" }) let allFiles = [] const assistantFiles = ( await getAssistantFilesByAssistantId(selectedAssistant.id) ).files allFiles = [...assistantFiles] const assistantCollections = ( await getAssistantCollectionsByAssistantId(selectedAssistant.id) ).collections for (const collection of assistantCollections) { const collectionFiles = ( await getCollectionFilesByCollectionId(collection.id) ).files allFiles = [...allFiles, ...collectionFiles] } const assistantTools = ( await getAssistantToolsByAssistantId(selectedAssistant.id) ).tools setSelectedTools(assistantTools) setChatFiles( allFiles.map(file => ({ id: file.id, name: file.name, type: file.type, file: null })) ) if (allFiles.length > 0) setShowFilesDisplay(true) } else if (selectedPreset) { setChatSettings({ model: selectedPreset.model as LLMID, prompt: selectedPreset.prompt, temperature: selectedPreset.temperature, contextLength: selectedPreset.context_length, includeProfileContext: selectedPreset.include_profile_context, includeWorkspaceInstructions: selectedPreset.include_workspace_instructions, embeddingsProvider: selectedPreset.embeddings_provider as | "openai" | "local" }) } else if (selectedWorkspace) { // setChatSettings({ // model: (selectedWorkspace.default_model || // "gpt-4-1106-preview") as LLMID, // prompt: // selectedWorkspace.default_prompt || // "You are a friendly, helpful AI assistant.", // temperature: selectedWorkspace.default_temperature || 0.5, // contextLength: selectedWorkspace.default_context_length || 4096, // includeProfileContext: // selectedWorkspace.include_profile_context || true, // includeWorkspaceInstructions: // selectedWorkspace.include_workspace_instructions || true, // embeddingsProvider: // (selectedWorkspace.embeddings_provider as "openai" | "local") || // "openai" // }) } return router.push(`/${selectedWorkspace.id}/chat`) } const handleFocusChatInput = () => { chatInputRef.current?.focus() } const handleStopMessage = () => { if (abortController) { abortController.abort() } } const handleSendMessage = async ( messageContent: string, chatMessages: ChatMessage[], isRegeneration: boolean ) => { const startingInput = messageContent try { setUserInput("") setIsGenerating(true) setIsPromptPickerOpen(false) setIsFilePickerOpen(false) setNewMessageImages([]) const newAbortController = new AbortController() setAbortController(newAbortController) const modelData = [ ...models.map(model => ({ modelId: model.model_id as LLMID, modelName: model.name, provider: "custom" as ModelProvider, hostedId: model.id, platformLink: "", imageInput: false })), ...LLM_LIST, ...availableLocalModels, ...availableOpenRouterModels ].find(llm => llm.modelId === chatSettings?.model) validateChatSettings( chatSettings, modelData, profile, selectedWorkspace, messageContent ) let currentChat = selectedChat ? { ...selectedChat } : null const b64Images = newMessageImages.map(image => image.base64) let retrievedFileItems: Tables<"file_items">[] = [] if ( (newMessageFiles.length > 0 || chatFiles.length > 0) && useRetrieval ) { setToolInUse("retrieval") retrievedFileItems = await handleRetrieval( userInput, newMessageFiles, chatFiles, chatSettings!.embeddingsProvider, sourceCount ) } const { tempUserChatMessage, tempAssistantChatMessage } = createTempMessages( messageContent, chatMessages, chatSettings!, b64Images, isRegeneration, setChatMessages, selectedAssistant ) let payload: ChatPayload = { chatSettings: chatSettings!, workspaceInstructions: selectedWorkspace!.instructions || "", chatMessages: isRegeneration ? [...chatMessages] : [...chatMessages, tempUserChatMessage], assistant: selectedChat?.assistant_id ? selectedAssistant : null, messageFileItems: retrievedFileItems, chatFileItems: chatFileItems } let generatedText = "" if (selectedTools.length > 0) { setToolInUse("Tools") const formattedMessages = await buildFinalMessages( payload, profile!, chatImages ) const response = await fetch("/api/chat/tools", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ chatSettings: payload.chatSettings, messages: formattedMessages, selectedTools }) }) setToolInUse("none") generatedText = await processResponse( response, isRegeneration ? payload.chatMessages[payload.chatMessages.length - 1] : tempAssistantChatMessage, true, newAbortController, setFirstTokenReceived, setChatMessages, setToolInUse ) } else { if (modelData!.provider === "ollama") { generatedText = await handleLocalChat( payload, profile!, chatSettings!, tempAssistantChatMessage, isRegeneration, newAbortController, setIsGenerating, setFirstTokenReceived, setChatMessages, setToolInUse ) } else { generatedText = await handleHostedChat( payload, profile!, modelData!, tempAssistantChatMessage, isRegeneration, newAbortController, newMessageImages, chatImages, setIsGenerating, setFirstTokenReceived, setChatMessages, setToolInUse ) } } if (!currentChat) { currentChat = await handleCreateChat( chatSettings!, profile!, selectedWorkspace!, messageContent, selectedAssistant!, newMessageFiles, setSelectedChat, setChats, setChatFiles ) } else { const updatedChat = await updateChat(currentChat.id, { updated_at: new Date().toISOString() }) setChats(prevChats => { const updatedChats = prevChats.map(prevChat => prevChat.id === updatedChat.id ? updatedChat : prevChat ) return updatedChats }) } await handleCreateMessages( chatMessages, currentChat, profile!, modelData!, messageContent, generatedText, newMessageImages, isRegeneration, retrievedFileItems, setChatMessages, setChatFileItems, setChatImages, selectedAssistant ) setIsGenerating(false) setFirstTokenReceived(false) } catch (error) { setIsGenerating(false) setFirstTokenReceived(false) setUserInput(startingInput) } } const handleSendEdit = async ( editedContent: string, sequenceNumber: number ) => { if (!selectedChat) return await deleteMessagesIncludingAndAfter( selectedChat.user_id, selectedChat.id, sequenceNumber ) const filteredMessages = chatMessages.filter( chatMessage => chatMessage.message.sequence_number < sequenceNumber ) setChatMessages(filteredMessages) handleSendMessage(editedContent, filteredMessages, false) } return { chatInputRef, prompt, handleNewChat, handleSendMessage, handleFocusChatInput, handleStopMessage, handleSendEdit } } ================================================ FILE: components/chat/chat-hooks/use-chat-history.tsx ================================================ import { ChatbotUIContext } from "@/context/context" import { useContext, useEffect, useState } from "react" /** * Custom hook for handling chat history in the chat component. * It provides functions to set the new message content to the previous or next user message in the chat history. * * @returns An object containing the following functions: * - setNewMessageContentToPreviousUserMessage: Sets the new message content to the previous user message. * - setNewMessageContentToNextUserMessage: Sets the new message content to the next user message in the chat history. */ export const useChatHistoryHandler = () => { const { setUserInput, chatMessages, isGenerating } = useContext(ChatbotUIContext) const userRoleString = "user" const [messageHistoryIndex, setMessageHistoryIndex] = useState( chatMessages.length ) useEffect(() => { // If messages get deleted the history index pointed could be out of bounds if (!isGenerating && messageHistoryIndex > chatMessages.length) setMessageHistoryIndex(chatMessages.length) }, [chatMessages, isGenerating, messageHistoryIndex]) /** * Sets the new message content to the previous user message. */ const setNewMessageContentToPreviousUserMessage = () => { let tempIndex = messageHistoryIndex while ( tempIndex > 0 && chatMessages[tempIndex - 1].message.role !== userRoleString ) { tempIndex-- } const previousUserMessage = chatMessages.length > 0 && tempIndex > 0 ? chatMessages[tempIndex - 1] : null if (previousUserMessage) { setUserInput(previousUserMessage.message.content) setMessageHistoryIndex(tempIndex - 1) } } /** * Sets the new message content to the next user message in the chat history. * If there is a next user message, it updates the user input and message history index accordingly. * If there is no next user message, it resets the user input and sets the message history index to the end of the chat history. */ const setNewMessageContentToNextUserMessage = () => { let tempIndex = messageHistoryIndex while ( tempIndex < chatMessages.length - 1 && chatMessages[tempIndex + 1].message.role !== userRoleString ) { tempIndex++ } const nextUserMessage = chatMessages.length > 0 && tempIndex < chatMessages.length - 1 ? chatMessages[tempIndex + 1] : null setUserInput(nextUserMessage?.message.content || "") setMessageHistoryIndex( nextUserMessage ? tempIndex + 1 : chatMessages.length ) } return { setNewMessageContentToPreviousUserMessage, setNewMessageContentToNextUserMessage } } ================================================ FILE: components/chat/chat-hooks/use-prompt-and-command.tsx ================================================ import { ChatbotUIContext } from "@/context/context" import { getAssistantCollectionsByAssistantId } from "@/db/assistant-collections" import { getAssistantFilesByAssistantId } from "@/db/assistant-files" import { getAssistantToolsByAssistantId } from "@/db/assistant-tools" import { getCollectionFilesByCollectionId } from "@/db/collection-files" import { Tables } from "@/supabase/types" import { LLMID } from "@/types" import { useContext } from "react" export const usePromptAndCommand = () => { const { chatFiles, setNewMessageFiles, userInput, setUserInput, setShowFilesDisplay, setIsPromptPickerOpen, setIsFilePickerOpen, setSlashCommand, setHashtagCommand, setUseRetrieval, setToolCommand, setIsToolPickerOpen, setSelectedTools, setAtCommand, setIsAssistantPickerOpen, setSelectedAssistant, setChatSettings, setChatFiles } = useContext(ChatbotUIContext) const handleInputChange = (value: string) => { const atTextRegex = /@([^ ]*)$/ const slashTextRegex = /\/([^ ]*)$/ const hashtagTextRegex = /#([^ ]*)$/ const toolTextRegex = /!([^ ]*)$/ const atMatch = value.match(atTextRegex) const slashMatch = value.match(slashTextRegex) const hashtagMatch = value.match(hashtagTextRegex) const toolMatch = value.match(toolTextRegex) if (atMatch) { setIsAssistantPickerOpen(true) setAtCommand(atMatch[1]) } else if (slashMatch) { setIsPromptPickerOpen(true) setSlashCommand(slashMatch[1]) } else if (hashtagMatch) { setIsFilePickerOpen(true) setHashtagCommand(hashtagMatch[1]) } else if (toolMatch) { setIsToolPickerOpen(true) setToolCommand(toolMatch[1]) } else { setIsPromptPickerOpen(false) setIsFilePickerOpen(false) setIsToolPickerOpen(false) setIsAssistantPickerOpen(false) setSlashCommand("") setHashtagCommand("") setToolCommand("") setAtCommand("") } setUserInput(value) } const handleSelectPrompt = (prompt: Tables<"prompts">) => { setIsPromptPickerOpen(false) setUserInput(userInput.replace(/\/[^ ]*$/, "") + prompt.content) } const handleSelectUserFile = async (file: Tables<"files">) => { setShowFilesDisplay(true) setIsFilePickerOpen(false) setUseRetrieval(true) setNewMessageFiles(prev => { const fileAlreadySelected = prev.some(prevFile => prevFile.id === file.id) || chatFiles.some(chatFile => chatFile.id === file.id) if (!fileAlreadySelected) { return [ ...prev, { id: file.id, name: file.name, type: file.type, file: null } ] } return prev }) setUserInput(userInput.replace(/#[^ ]*$/, "")) } const handleSelectUserCollection = async ( collection: Tables<"collections"> ) => { setShowFilesDisplay(true) setIsFilePickerOpen(false) setUseRetrieval(true) const collectionFiles = await getCollectionFilesByCollectionId( collection.id ) setNewMessageFiles(prev => { const newFiles = collectionFiles.files .filter( file => !prev.some(prevFile => prevFile.id === file.id) && !chatFiles.some(chatFile => chatFile.id === file.id) ) .map(file => ({ id: file.id, name: file.name, type: file.type, file: null })) return [...prev, ...newFiles] }) setUserInput(userInput.replace(/#[^ ]*$/, "")) } const handleSelectTool = (tool: Tables<"tools">) => { setIsToolPickerOpen(false) setUserInput(userInput.replace(/![^ ]*$/, "")) setSelectedTools(prev => [...prev, tool]) } const handleSelectAssistant = async (assistant: Tables<"assistants">) => { setIsAssistantPickerOpen(false) setUserInput(userInput.replace(/@[^ ]*$/, "")) setSelectedAssistant(assistant) setChatSettings({ model: assistant.model as LLMID, prompt: assistant.prompt, temperature: assistant.temperature, contextLength: assistant.context_length, includeProfileContext: assistant.include_profile_context, includeWorkspaceInstructions: assistant.include_workspace_instructions, embeddingsProvider: assistant.embeddings_provider as "openai" | "local" }) let allFiles = [] const assistantFiles = (await getAssistantFilesByAssistantId(assistant.id)) .files allFiles = [...assistantFiles] const assistantCollections = ( await getAssistantCollectionsByAssistantId(assistant.id) ).collections for (const collection of assistantCollections) { const collectionFiles = ( await getCollectionFilesByCollectionId(collection.id) ).files allFiles = [...allFiles, ...collectionFiles] } const assistantTools = (await getAssistantToolsByAssistantId(assistant.id)) .tools setSelectedTools(assistantTools) setChatFiles( allFiles.map(file => ({ id: file.id, name: file.name, type: file.type, file: null })) ) if (allFiles.length > 0) setShowFilesDisplay(true) } return { handleInputChange, handleSelectPrompt, handleSelectUserFile, handleSelectUserCollection, handleSelectTool, handleSelectAssistant } } ================================================ FILE: components/chat/chat-hooks/use-scroll.tsx ================================================ import { ChatbotUIContext } from "@/context/context" import { type UIEventHandler, useCallback, useContext, useEffect, useRef, useState } from "react" export const useScroll = () => { const { isGenerating, chatMessages } = useContext(ChatbotUIContext) const messagesStartRef = useRef(null) const messagesEndRef = useRef(null) const isAutoScrolling = useRef(false) const [isAtTop, setIsAtTop] = useState(false) const [isAtBottom, setIsAtBottom] = useState(true) const [userScrolled, setUserScrolled] = useState(false) const [isOverflowing, setIsOverflowing] = useState(false) useEffect(() => { setUserScrolled(false) if (!isGenerating && userScrolled) { setUserScrolled(false) } }, [isGenerating]) useEffect(() => { if (isGenerating && !userScrolled) { scrollToBottom() } }, [chatMessages]) const handleScroll: UIEventHandler = useCallback(e => { const target = e.target as HTMLDivElement const bottom = Math.round(target.scrollHeight) - Math.round(target.scrollTop) === Math.round(target.clientHeight) setIsAtBottom(bottom) const top = target.scrollTop === 0 setIsAtTop(top) if (!bottom && !isAutoScrolling.current) { setUserScrolled(true) } else { setUserScrolled(false) } const isOverflow = target.scrollHeight > target.clientHeight setIsOverflowing(isOverflow) }, []) const scrollToTop = useCallback(() => { if (messagesStartRef.current) { messagesStartRef.current.scrollIntoView({ behavior: "instant" }) } }, []) const scrollToBottom = useCallback(() => { isAutoScrolling.current = true setTimeout(() => { if (messagesEndRef.current) { messagesEndRef.current.scrollIntoView({ behavior: "instant" }) } isAutoScrolling.current = false }, 100) }, []) return { messagesStartRef, messagesEndRef, isAtTop, isAtBottom, userScrolled, isOverflowing, handleScroll, scrollToTop, scrollToBottom, setIsAtBottom } } ================================================ FILE: components/chat/chat-hooks/use-select-file-handler.tsx ================================================ import { ChatbotUIContext } from "@/context/context" import { createDocXFile, createFile } from "@/db/files" import { LLM_LIST } from "@/lib/models/llm/llm-list" import mammoth from "mammoth" import { useContext, useEffect, useState } from "react" import { toast } from "sonner" export const ACCEPTED_FILE_TYPES = [ "text/csv", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/json", "text/markdown", "application/pdf", "text/plain" ].join(",") export const useSelectFileHandler = () => { const { selectedWorkspace, profile, chatSettings, setNewMessageImages, setNewMessageFiles, setShowFilesDisplay, setFiles, setUseRetrieval } = useContext(ChatbotUIContext) const [filesToAccept, setFilesToAccept] = useState(ACCEPTED_FILE_TYPES) useEffect(() => { handleFilesToAccept() }, [chatSettings?.model]) const handleFilesToAccept = () => { const model = chatSettings?.model const FULL_MODEL = LLM_LIST.find(llm => llm.modelId === model) if (!FULL_MODEL) return setFilesToAccept( FULL_MODEL.imageInput ? `${ACCEPTED_FILE_TYPES},image/*` : ACCEPTED_FILE_TYPES ) } const handleSelectDeviceFile = async (file: File) => { if (!profile || !selectedWorkspace || !chatSettings) return setShowFilesDisplay(true) setUseRetrieval(true) if (file) { let simplifiedFileType = file.type.split("/")[1] let reader = new FileReader() if (file.type.includes("image")) { reader.readAsDataURL(file) } else if (ACCEPTED_FILE_TYPES.split(",").includes(file.type)) { if (simplifiedFileType.includes("vnd.adobe.pdf")) { simplifiedFileType = "pdf" } else if ( simplifiedFileType.includes( "vnd.openxmlformats-officedocument.wordprocessingml.document" || "docx" ) ) { simplifiedFileType = "docx" } setNewMessageFiles(prev => [ ...prev, { id: "loading", name: file.name, type: simplifiedFileType, file: file } ]) // Handle docx files if ( file.type.includes( "vnd.openxmlformats-officedocument.wordprocessingml.document" || "docx" ) ) { const arrayBuffer = await file.arrayBuffer() const result = await mammoth.extractRawText({ arrayBuffer }) const createdFile = await createDocXFile( result.value, file, { user_id: profile.user_id, description: "", file_path: "", name: file.name, size: file.size, tokens: 0, type: simplifiedFileType }, selectedWorkspace.id, chatSettings.embeddingsProvider ) setFiles(prev => [...prev, createdFile]) setNewMessageFiles(prev => prev.map(item => item.id === "loading" ? { id: createdFile.id, name: createdFile.name, type: createdFile.type, file: file } : item ) ) reader.onloadend = null return } else { // Use readAsArrayBuffer for PDFs and readAsText for other types file.type.includes("pdf") ? reader.readAsArrayBuffer(file) : reader.readAsText(file) } } else { throw new Error("Unsupported file type") } reader.onloadend = async function () { try { if (file.type.includes("image")) { // Create a temp url for the image file const imageUrl = URL.createObjectURL(file) // This is a temporary image for display purposes in the chat input setNewMessageImages(prev => [ ...prev, { messageId: "temp", path: "", base64: reader.result, // base64 image url: imageUrl, file } ]) } else { const createdFile = await createFile( file, { user_id: profile.user_id, description: "", file_path: "", name: file.name, size: file.size, tokens: 0, type: simplifiedFileType }, selectedWorkspace.id, chatSettings.embeddingsProvider ) setFiles(prev => [...prev, createdFile]) setNewMessageFiles(prev => prev.map(item => item.id === "loading" ? { id: createdFile.id, name: createdFile.name, type: createdFile.type, file: file } : item ) ) } } catch (error: any) { toast.error("Failed to upload. " + error?.message, { duration: 10000 }) setNewMessageImages(prev => prev.filter(img => img.messageId !== "temp") ) setNewMessageFiles(prev => prev.filter(file => file.id !== "loading")) } } } } return { handleSelectDeviceFile, filesToAccept } } ================================================ FILE: components/chat/chat-input.tsx ================================================ import { ChatbotUIContext } from "@/context/context" import useHotkey from "@/lib/hooks/use-hotkey" import { LLM_LIST } from "@/lib/models/llm/llm-list" import { cn } from "@/lib/utils" import { IconBolt, IconCirclePlus, IconPlayerStopFilled, IconSend } from "@tabler/icons-react" import Image from "next/image" import { FC, useContext, useEffect, useRef, useState } from "react" import { useTranslation } from "react-i18next" import { toast } from "sonner" import { Input } from "../ui/input" import { TextareaAutosize } from "../ui/textarea-autosize" import { ChatCommandInput } from "./chat-command-input" import { ChatFilesDisplay } from "./chat-files-display" import { useChatHandler } from "./chat-hooks/use-chat-handler" import { useChatHistoryHandler } from "./chat-hooks/use-chat-history" import { usePromptAndCommand } from "./chat-hooks/use-prompt-and-command" import { useSelectFileHandler } from "./chat-hooks/use-select-file-handler" interface ChatInputProps {} export const ChatInput: FC = ({}) => { const { t } = useTranslation() useHotkey("l", () => { handleFocusChatInput() }) const [isTyping, setIsTyping] = useState(false) const { isAssistantPickerOpen, focusAssistant, setFocusAssistant, userInput, chatMessages, isGenerating, selectedPreset, selectedAssistant, focusPrompt, setFocusPrompt, focusFile, focusTool, setFocusTool, isToolPickerOpen, isPromptPickerOpen, setIsPromptPickerOpen, isFilePickerOpen, setFocusFile, chatSettings, selectedTools, setSelectedTools, assistantImages } = useContext(ChatbotUIContext) const { chatInputRef, handleSendMessage, handleStopMessage, handleFocusChatInput } = useChatHandler() const { handleInputChange } = usePromptAndCommand() const { filesToAccept, handleSelectDeviceFile } = useSelectFileHandler() const { setNewMessageContentToNextUserMessage, setNewMessageContentToPreviousUserMessage } = useChatHistoryHandler() const fileInputRef = useRef(null) useEffect(() => { setTimeout(() => { handleFocusChatInput() }, 200) // FIX: hacky }, [selectedPreset, selectedAssistant]) const handleKeyDown = (event: React.KeyboardEvent) => { if (!isTyping && event.key === "Enter" && !event.shiftKey) { event.preventDefault() setIsPromptPickerOpen(false) handleSendMessage(userInput, chatMessages, false) } // Consolidate conditions to avoid TypeScript error if ( isPromptPickerOpen || isFilePickerOpen || isToolPickerOpen || isAssistantPickerOpen ) { if ( event.key === "Tab" || event.key === "ArrowUp" || event.key === "ArrowDown" ) { event.preventDefault() // Toggle focus based on picker type if (isPromptPickerOpen) setFocusPrompt(!focusPrompt) if (isFilePickerOpen) setFocusFile(!focusFile) if (isToolPickerOpen) setFocusTool(!focusTool) if (isAssistantPickerOpen) setFocusAssistant(!focusAssistant) } } if (event.key === "ArrowUp" && event.shiftKey && event.ctrlKey) { event.preventDefault() setNewMessageContentToPreviousUserMessage() } if (event.key === "ArrowDown" && event.shiftKey && event.ctrlKey) { event.preventDefault() setNewMessageContentToNextUserMessage() } //use shift+ctrl+up and shift+ctrl+down to navigate through chat history if (event.key === "ArrowUp" && event.shiftKey && event.ctrlKey) { event.preventDefault() setNewMessageContentToPreviousUserMessage() } if (event.key === "ArrowDown" && event.shiftKey && event.ctrlKey) { event.preventDefault() setNewMessageContentToNextUserMessage() } if ( isAssistantPickerOpen && (event.key === "Tab" || event.key === "ArrowUp" || event.key === "ArrowDown") ) { event.preventDefault() setFocusAssistant(!focusAssistant) } } const handlePaste = (event: React.ClipboardEvent) => { const imagesAllowed = LLM_LIST.find( llm => llm.modelId === chatSettings?.model )?.imageInput const items = event.clipboardData.items for (const item of items) { if (item.type.indexOf("image") === 0) { if (!imagesAllowed) { toast.error( `Images are not supported for this model. Use models like GPT-4 Vision instead.` ) return } const file = item.getAsFile() if (!file) return handleSelectDeviceFile(file) } } } return ( <>
{selectedTools && selectedTools.map((tool, index) => (
setSelectedTools( selectedTools.filter( selectedTool => selectedTool.id !== tool.id ) ) } >
{tool.name}
))} {selectedAssistant && (
{selectedAssistant.image_path && ( img.path === selectedAssistant.image_path )?.base64 } width={28} height={28} alt={selectedAssistant.name} /> )}
Talking to {selectedAssistant.name}
)}
<> fileInputRef.current?.click()} /> {/* Hidden input to select files from device */} { if (!e.target.files) return handleSelectDeviceFile(e.target.files[0]) }} accept={filesToAccept} /> setIsTyping(true)} onCompositionEnd={() => setIsTyping(false)} />
{isGenerating ? ( ) : ( { if (!userInput) return handleSendMessage(userInput, chatMessages, false) }} size={30} /> )}
) } ================================================ FILE: components/chat/chat-messages.tsx ================================================ import { useChatHandler } from "@/components/chat/chat-hooks/use-chat-handler" import { ChatbotUIContext } from "@/context/context" import { Tables } from "@/supabase/types" import { FC, useContext, useState } from "react" import { Message } from "../messages/message" interface ChatMessagesProps {} export const ChatMessages: FC = ({}) => { const { chatMessages, chatFileItems } = useContext(ChatbotUIContext) const { handleSendEdit } = useChatHandler() const [editingMessage, setEditingMessage] = useState>() return chatMessages .sort((a, b) => a.message.sequence_number - b.message.sequence_number) .map((chatMessage, index, array) => { const messageFileItems = chatFileItems.filter( (chatFileItem, _, self) => chatMessage.fileItems.includes(chatFileItem.id) && self.findIndex(item => item.id === chatFileItem.id) === _ ) return ( setEditingMessage(undefined)} onSubmitEdit={handleSendEdit} /> ) }) } ================================================ FILE: components/chat/chat-retrieval-settings.tsx ================================================ import { ChatbotUIContext } from "@/context/context" import { IconAdjustmentsHorizontal } from "@tabler/icons-react" import { FC, useContext, useState } from "react" import { Button } from "../ui/button" import { Dialog, DialogContent, DialogFooter, DialogTrigger } from "../ui/dialog" import { Label } from "../ui/label" import { Slider } from "../ui/slider" import { WithTooltip } from "../ui/with-tooltip" interface ChatRetrievalSettingsProps {} export const ChatRetrievalSettings: FC = ({}) => { const { sourceCount, setSourceCount } = useContext(ChatbotUIContext) const [isOpen, setIsOpen] = useState(false) return ( Adjust retrieval settings.} trigger={ } />
{ setSourceCount(values[0]) }} min={1} max={10} step={1} />
) } ================================================ FILE: components/chat/chat-scroll-buttons.tsx ================================================ import { IconCircleArrowDownFilled, IconCircleArrowUpFilled } from "@tabler/icons-react" import { FC } from "react" interface ChatScrollButtonsProps { isAtTop: boolean isAtBottom: boolean isOverflowing: boolean scrollToTop: () => void scrollToBottom: () => void } export const ChatScrollButtons: FC = ({ isAtTop, isAtBottom, isOverflowing, scrollToTop, scrollToBottom }) => { return ( <> {!isAtTop && isOverflowing && ( )} {!isAtBottom && isOverflowing && ( )} ) } ================================================ FILE: components/chat/chat-secondary-buttons.tsx ================================================ import { useChatHandler } from "@/components/chat/chat-hooks/use-chat-handler" import { ChatbotUIContext } from "@/context/context" import { IconInfoCircle, IconMessagePlus } from "@tabler/icons-react" import { FC, useContext } from "react" import { WithTooltip } from "../ui/with-tooltip" interface ChatSecondaryButtonsProps {} export const ChatSecondaryButtons: FC = ({}) => { const { selectedChat } = useContext(ChatbotUIContext) const { handleNewChat } = useChatHandler() return ( <> {selectedChat && ( <>
Chat Info
Model: {selectedChat.model}
Prompt: {selectedChat.prompt}
Temperature: {selectedChat.temperature}
Context Length: {selectedChat.context_length}
Profile Context:{" "} {selectedChat.include_profile_context ? "Enabled" : "Disabled"}
{" "} Workspace Instructions:{" "} {selectedChat.include_workspace_instructions ? "Enabled" : "Disabled"}
Embeddings Provider: {selectedChat.embeddings_provider}
} trigger={
} /> Start a new chat} trigger={
} /> )} ) } ================================================ FILE: components/chat/chat-settings.tsx ================================================ import { ChatbotUIContext } from "@/context/context" import { CHAT_SETTING_LIMITS } from "@/lib/chat-setting-limits" import useHotkey from "@/lib/hooks/use-hotkey" import { LLMID, ModelProvider } from "@/types" import { IconAdjustmentsHorizontal } from "@tabler/icons-react" import { FC, useContext, useEffect, useRef } from "react" import { Button } from "../ui/button" import { ChatSettingsForm } from "../ui/chat-settings-form" import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover" interface ChatSettingsProps {} export const ChatSettings: FC = ({}) => { useHotkey("i", () => handleClick()) const { chatSettings, setChatSettings, models, availableHostedModels, availableLocalModels, availableOpenRouterModels } = useContext(ChatbotUIContext) const buttonRef = useRef(null) const handleClick = () => { if (buttonRef.current) { buttonRef.current.click() } } useEffect(() => { if (!chatSettings) return setChatSettings({ ...chatSettings, temperature: Math.min( chatSettings.temperature, CHAT_SETTING_LIMITS[chatSettings.model]?.MAX_TEMPERATURE || 1 ), contextLength: Math.min( chatSettings.contextLength, CHAT_SETTING_LIMITS[chatSettings.model]?.MAX_CONTEXT_LENGTH || 4096 ) }) }, [chatSettings?.model]) if (!chatSettings) return null const allModels = [ ...models.map(model => ({ modelId: model.model_id as LLMID, modelName: model.name, provider: "custom" as ModelProvider, hostedId: model.id, platformLink: "", imageInput: false })), ...availableHostedModels, ...availableLocalModels, ...availableOpenRouterModels ] const fullModel = allModels.find(llm => llm.modelId === chatSettings.model) return ( ) } ================================================ FILE: components/chat/chat-ui.tsx ================================================ import Loading from "@/app/[locale]/loading" import { useChatHandler } from "@/components/chat/chat-hooks/use-chat-handler" import { ChatbotUIContext } from "@/context/context" import { getAssistantToolsByAssistantId } from "@/db/assistant-tools" import { getChatFilesByChatId } from "@/db/chat-files" import { getChatById } from "@/db/chats" import { getMessageFileItemsByMessageId } from "@/db/message-file-items" import { getMessagesByChatId } from "@/db/messages" import { getMessageImageFromStorage } from "@/db/storage/message-images" import { convertBlobToBase64 } from "@/lib/blob-to-b64" import useHotkey from "@/lib/hooks/use-hotkey" import { LLMID, MessageImage } from "@/types" import { useParams } from "next/navigation" import { FC, useContext, useEffect, useState } from "react" import { ChatHelp } from "./chat-help" import { useScroll } from "./chat-hooks/use-scroll" import { ChatInput } from "./chat-input" import { ChatMessages } from "./chat-messages" import { ChatScrollButtons } from "./chat-scroll-buttons" import { ChatSecondaryButtons } from "./chat-secondary-buttons" interface ChatUIProps {} export const ChatUI: FC = ({}) => { useHotkey("o", () => handleNewChat()) const params = useParams() const { setChatMessages, selectedChat, setSelectedChat, setChatSettings, setChatImages, assistants, setSelectedAssistant, setChatFileItems, setChatFiles, setShowFilesDisplay, setUseRetrieval, setSelectedTools } = useContext(ChatbotUIContext) const { handleNewChat, handleFocusChatInput } = useChatHandler() const { messagesStartRef, messagesEndRef, handleScroll, scrollToBottom, setIsAtBottom, isAtTop, isAtBottom, isOverflowing, scrollToTop } = useScroll() const [loading, setLoading] = useState(true) useEffect(() => { const fetchData = async () => { await fetchMessages() await fetchChat() scrollToBottom() setIsAtBottom(true) } if (params.chatid) { fetchData().then(() => { handleFocusChatInput() setLoading(false) }) } else { setLoading(false) } }, []) const fetchMessages = async () => { const fetchedMessages = await getMessagesByChatId(params.chatid as string) const imagePromises: Promise[] = fetchedMessages.flatMap( message => message.image_paths ? message.image_paths.map(async imagePath => { const url = await getMessageImageFromStorage(imagePath) if (url) { const response = await fetch(url) const blob = await response.blob() const base64 = await convertBlobToBase64(blob) return { messageId: message.id, path: imagePath, base64, url, file: null } } return { messageId: message.id, path: imagePath, base64: "", url, file: null } }) : [] ) const images: MessageImage[] = await Promise.all(imagePromises.flat()) setChatImages(images) const messageFileItemPromises = fetchedMessages.map( async message => await getMessageFileItemsByMessageId(message.id) ) const messageFileItems = await Promise.all(messageFileItemPromises) const uniqueFileItems = messageFileItems.flatMap(item => item.file_items) setChatFileItems(uniqueFileItems) const chatFiles = await getChatFilesByChatId(params.chatid as string) setChatFiles( chatFiles.files.map(file => ({ id: file.id, name: file.name, type: file.type, file: null })) ) setUseRetrieval(true) setShowFilesDisplay(true) const fetchedChatMessages = fetchedMessages.map(message => { return { message, fileItems: messageFileItems .filter(messageFileItem => messageFileItem.id === message.id) .flatMap(messageFileItem => messageFileItem.file_items.map(fileItem => fileItem.id) ) } }) setChatMessages(fetchedChatMessages) } const fetchChat = async () => { const chat = await getChatById(params.chatid as string) if (!chat) return if (chat.assistant_id) { const assistant = assistants.find( assistant => assistant.id === chat.assistant_id ) if (assistant) { setSelectedAssistant(assistant) const assistantTools = ( await getAssistantToolsByAssistantId(assistant.id) ).tools setSelectedTools(assistantTools) } } setSelectedChat(chat) setChatSettings({ model: chat.model as LLMID, prompt: chat.prompt, temperature: chat.temperature, contextLength: chat.context_length, includeProfileContext: chat.include_profile_context, includeWorkspaceInstructions: chat.include_workspace_instructions, embeddingsProvider: chat.embeddings_provider as "openai" | "local" }) } if (loading) { return } return (
{selectedChat?.name || "Chat"}
) } ================================================ FILE: components/chat/file-picker.tsx ================================================ import { ChatbotUIContext } from "@/context/context" import { Tables } from "@/supabase/types" import { IconBooks } from "@tabler/icons-react" import { FC, useContext, useEffect, useRef } from "react" import { FileIcon } from "../ui/file-icon" interface FilePickerProps { isOpen: boolean searchQuery: string onOpenChange: (isOpen: boolean) => void selectedFileIds: string[] selectedCollectionIds: string[] onSelectFile: (file: Tables<"files">) => void onSelectCollection: (collection: Tables<"collections">) => void isFocused: boolean } export const FilePicker: FC = ({ isOpen, searchQuery, onOpenChange, selectedFileIds, selectedCollectionIds, onSelectFile, onSelectCollection, isFocused }) => { const { files, collections, setIsFilePickerOpen } = useContext(ChatbotUIContext) const itemsRef = useRef<(HTMLDivElement | null)[]>([]) useEffect(() => { if (isFocused && itemsRef.current[0]) { itemsRef.current[0].focus() } }, [isFocused]) const filteredFiles = files.filter( file => file.name.toLowerCase().includes(searchQuery.toLowerCase()) && !selectedFileIds.includes(file.id) ) const filteredCollections = collections.filter( collection => collection.name.toLowerCase().includes(searchQuery.toLowerCase()) && !selectedCollectionIds.includes(collection.id) ) const handleOpenChange = (isOpen: boolean) => { onOpenChange(isOpen) } const handleSelectFile = (file: Tables<"files">) => { onSelectFile(file) handleOpenChange(false) } const handleSelectCollection = (collection: Tables<"collections">) => { onSelectCollection(collection) handleOpenChange(false) } const getKeyDownHandler = (index: number, type: "file" | "collection", item: any) => (e: React.KeyboardEvent) => { if (e.key === "Escape") { e.preventDefault() setIsFilePickerOpen(false) } else if (e.key === "Backspace") { e.preventDefault() } else if (e.key === "Enter") { e.preventDefault() if (type === "file") { handleSelectFile(item) } else { handleSelectCollection(item) } } else if ( (e.key === "Tab" || e.key === "ArrowDown") && !e.shiftKey && index === filteredFiles.length + filteredCollections.length - 1 ) { e.preventDefault() itemsRef.current[0]?.focus() } else if (e.key === "ArrowUp" && !e.shiftKey && index === 0) { // go to last element if arrow up is pressed on first element e.preventDefault() itemsRef.current[itemsRef.current.length - 1]?.focus() } else if (e.key === "ArrowUp") { e.preventDefault() const prevIndex = index - 1 >= 0 ? index - 1 : itemsRef.current.length - 1 itemsRef.current[prevIndex]?.focus() } else if (e.key === "ArrowDown") { e.preventDefault() const nextIndex = index + 1 < itemsRef.current.length ? index + 1 : 0 itemsRef.current[nextIndex]?.focus() } } return ( <> {isOpen && (
{filteredFiles.length === 0 && filteredCollections.length === 0 ? (
No matching files.
) : ( <> {[...filteredFiles, ...filteredCollections].map((item, index) => (
{ itemsRef.current[index] = ref }} tabIndex={0} className="hover:bg-accent focus:bg-accent flex cursor-pointer items-center rounded p-2 focus:outline-none" onClick={() => { if ("type" in item) { handleSelectFile(item as Tables<"files">) } else { handleSelectCollection(item) } }} onKeyDown={e => getKeyDownHandler( index, "type" in item ? "file" : "collection", item )(e) } > {"type" in item ? ( ).type} size={32} /> ) : ( )}
{item.name}
{item.description || "No description."}
))} )}
)} ) } ================================================ FILE: components/chat/prompt-picker.tsx ================================================ import { ChatbotUIContext } from "@/context/context" import { Tables } from "@/supabase/types" import { FC, useContext, useEffect, useRef, useState } from "react" import { Button } from "../ui/button" import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../ui/dialog" import { Label } from "../ui/label" import { TextareaAutosize } from "../ui/textarea-autosize" import { usePromptAndCommand } from "./chat-hooks/use-prompt-and-command" interface PromptPickerProps {} export const PromptPicker: FC = ({}) => { const { prompts, isPromptPickerOpen, setIsPromptPickerOpen, focusPrompt, slashCommand } = useContext(ChatbotUIContext) const { handleSelectPrompt } = usePromptAndCommand() const itemsRef = useRef<(HTMLDivElement | null)[]>([]) const [promptVariables, setPromptVariables] = useState< { promptId: string name: string value: string }[] >([]) const [showPromptVariables, setShowPromptVariables] = useState(false) useEffect(() => { if (focusPrompt && itemsRef.current[0]) { itemsRef.current[0].focus() } }, [focusPrompt]) const [isTyping, setIsTyping] = useState(false) const filteredPrompts = prompts.filter(prompt => prompt.name.toLowerCase().includes(slashCommand.toLowerCase()) ) const handleOpenChange = (isOpen: boolean) => { setIsPromptPickerOpen(isOpen) } const callSelectPrompt = (prompt: Tables<"prompts">) => { const regex = /\{\{.*?\}\}/g const matches = prompt.content.match(regex) if (matches) { const newPromptVariables = matches.map(match => ({ promptId: prompt.id, name: match.replace(/\{\{|\}\}/g, ""), value: "" })) setPromptVariables(newPromptVariables) setShowPromptVariables(true) } else { handleSelectPrompt(prompt) handleOpenChange(false) } } const getKeyDownHandler = (index: number) => (e: React.KeyboardEvent) => { if (e.key === "Backspace") { e.preventDefault() handleOpenChange(false) } else if (e.key === "Enter") { e.preventDefault() callSelectPrompt(filteredPrompts[index]) } else if ( (e.key === "Tab" || e.key === "ArrowDown") && !e.shiftKey && index === filteredPrompts.length - 1 ) { e.preventDefault() itemsRef.current[0]?.focus() } else if (e.key === "ArrowUp" && !e.shiftKey && index === 0) { // go to last element if arrow up is pressed on first element e.preventDefault() itemsRef.current[itemsRef.current.length - 1]?.focus() } else if (e.key === "ArrowUp") { e.preventDefault() const prevIndex = index - 1 >= 0 ? index - 1 : itemsRef.current.length - 1 itemsRef.current[prevIndex]?.focus() } else if (e.key === "ArrowDown") { e.preventDefault() const nextIndex = index + 1 < itemsRef.current.length ? index + 1 : 0 itemsRef.current[nextIndex]?.focus() } } const handleSubmitPromptVariables = () => { const newPromptContent = promptVariables.reduce( (prevContent, variable) => prevContent.replace( new RegExp(`\\{\\{${variable.name}\\}\\}`, "g"), variable.value ), prompts.find(prompt => prompt.id === promptVariables[0].promptId) ?.content || "" ) const newPrompt: any = { ...prompts.find(prompt => prompt.id === promptVariables[0].promptId), content: newPromptContent } handleSelectPrompt(newPrompt) handleOpenChange(false) setShowPromptVariables(false) setPromptVariables([]) } const handleCancelPromptVariables = () => { setShowPromptVariables(false) setPromptVariables([]) } const handleKeydownPromptVariables = ( e: React.KeyboardEvent ) => { if (!isTyping && e.key === "Enter" && !e.shiftKey) { e.preventDefault() handleSubmitPromptVariables() } } return ( <> {isPromptPickerOpen && (
{showPromptVariables ? ( Enter Prompt Variables
{promptVariables.map((variable, index) => (
{ const newPromptVariables = [...promptVariables] newPromptVariables[index].value = value setPromptVariables(newPromptVariables) }} minRows={3} maxRows={5} onCompositionStart={() => setIsTyping(true)} onCompositionEnd={() => setIsTyping(false)} />
))}
) : filteredPrompts.length === 0 ? (
No matching prompts.
) : ( filteredPrompts.map((prompt, index) => (
{ itemsRef.current[index] = ref }} tabIndex={0} className="hover:bg-accent focus:bg-accent flex cursor-pointer flex-col rounded p-2 focus:outline-none" onClick={() => callSelectPrompt(prompt)} onKeyDown={getKeyDownHandler(index)} >
{prompt.name}
{prompt.content}
)) )}
)} ) } ================================================ FILE: components/chat/quick-setting-option.tsx ================================================ import { LLM_LIST } from "@/lib/models/llm/llm-list" import { Tables } from "@/supabase/types" import { IconCircleCheckFilled, IconRobotFace } from "@tabler/icons-react" import Image from "next/image" import { FC } from "react" import { ModelIcon } from "../models/model-icon" import { DropdownMenuItem } from "../ui/dropdown-menu" interface QuickSettingOptionProps { contentType: "presets" | "assistants" isSelected: boolean item: Tables<"presets"> | Tables<"assistants"> onSelect: () => void image: string } export const QuickSettingOption: FC = ({ contentType, isSelected, item, onSelect, image }) => { const modelDetails = LLM_LIST.find(model => model.modelId === item.model) return (
{contentType === "presets" ? ( ) : image ? ( Assistant ) : ( )}
{item.name}
{item.description && (
{item.description}
)}
{isSelected ? ( ) : null}
) } ================================================ FILE: components/chat/quick-settings.tsx ================================================ import { ChatbotUIContext } from "@/context/context" import { getAssistantCollectionsByAssistantId } from "@/db/assistant-collections" import { getAssistantFilesByAssistantId } from "@/db/assistant-files" import { getAssistantToolsByAssistantId } from "@/db/assistant-tools" import { getCollectionFilesByCollectionId } from "@/db/collection-files" import useHotkey from "@/lib/hooks/use-hotkey" import { LLM_LIST } from "@/lib/models/llm/llm-list" import { Tables } from "@/supabase/types" import { LLMID } from "@/types" import { IconChevronDown, IconRobotFace } from "@tabler/icons-react" import Image from "next/image" import { FC, useContext, useEffect, useRef, useState } from "react" import { useTranslation } from "react-i18next" import { ModelIcon } from "../models/model-icon" import { Button } from "../ui/button" import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "../ui/dropdown-menu" import { Input } from "../ui/input" import { QuickSettingOption } from "./quick-setting-option" import { set } from "date-fns" interface QuickSettingsProps {} export const QuickSettings: FC = ({}) => { const { t } = useTranslation() useHotkey("p", () => setIsOpen(prevState => !prevState)) const { presets, assistants, selectedAssistant, selectedPreset, chatSettings, setSelectedPreset, setSelectedAssistant, setChatSettings, assistantImages, setChatFiles, setSelectedTools, setShowFilesDisplay, selectedWorkspace } = useContext(ChatbotUIContext) const inputRef = useRef(null) const [isOpen, setIsOpen] = useState(false) const [search, setSearch] = useState("") const [loading, setLoading] = useState(false) useEffect(() => { if (isOpen) { setTimeout(() => { inputRef.current?.focus() }, 100) // FIX: hacky } }, [isOpen]) const handleSelectQuickSetting = async ( item: Tables<"presets"> | Tables<"assistants"> | null, contentType: "presets" | "assistants" | "remove" ) => { console.log({ item, contentType }) if (contentType === "assistants" && item) { setSelectedAssistant(item as Tables<"assistants">) setLoading(true) let allFiles = [] const assistantFiles = (await getAssistantFilesByAssistantId(item.id)) .files allFiles = [...assistantFiles] const assistantCollections = ( await getAssistantCollectionsByAssistantId(item.id) ).collections for (const collection of assistantCollections) { const collectionFiles = ( await getCollectionFilesByCollectionId(collection.id) ).files allFiles = [...allFiles, ...collectionFiles] } const assistantTools = (await getAssistantToolsByAssistantId(item.id)) .tools setSelectedTools(assistantTools) setChatFiles( allFiles.map(file => ({ id: file.id, name: file.name, type: file.type, file: null })) ) if (allFiles.length > 0) setShowFilesDisplay(true) setLoading(false) setSelectedPreset(null) } else if (contentType === "presets" && item) { setSelectedPreset(item as Tables<"presets">) setSelectedAssistant(null) setChatFiles([]) setSelectedTools([]) } else { setSelectedPreset(null) setSelectedAssistant(null) setChatFiles([]) setSelectedTools([]) if (selectedWorkspace) { setChatSettings({ model: selectedWorkspace.default_model as LLMID, prompt: selectedWorkspace.default_prompt, temperature: selectedWorkspace.default_temperature, contextLength: selectedWorkspace.default_context_length, includeProfileContext: selectedWorkspace.include_profile_context, includeWorkspaceInstructions: selectedWorkspace.include_workspace_instructions, embeddingsProvider: selectedWorkspace.embeddings_provider as | "openai" | "local" }) } return } setChatSettings({ model: item.model as LLMID, prompt: item.prompt, temperature: item.temperature, contextLength: item.context_length, includeProfileContext: item.include_profile_context, includeWorkspaceInstructions: item.include_workspace_instructions, embeddingsProvider: item.embeddings_provider as "openai" | "local" }) } const checkIfModified = () => { if (!chatSettings) return false if (selectedPreset) { return ( selectedPreset.include_profile_context !== chatSettings?.includeProfileContext || selectedPreset.include_workspace_instructions !== chatSettings.includeWorkspaceInstructions || selectedPreset.context_length !== chatSettings.contextLength || selectedPreset.model !== chatSettings.model || selectedPreset.prompt !== chatSettings.prompt || selectedPreset.temperature !== chatSettings.temperature ) } else if (selectedAssistant) { return ( selectedAssistant.include_profile_context !== chatSettings.includeProfileContext || selectedAssistant.include_workspace_instructions !== chatSettings.includeWorkspaceInstructions || selectedAssistant.context_length !== chatSettings.contextLength || selectedAssistant.model !== chatSettings.model || selectedAssistant.prompt !== chatSettings.prompt || selectedAssistant.temperature !== chatSettings.temperature ) } return false } const isModified = checkIfModified() const items = [ ...presets.map(preset => ({ ...preset, contentType: "presets" })), ...assistants.map(assistant => ({ ...assistant, contentType: "assistants" })) ] const selectedAssistantImage = selectedPreset ? "" : assistantImages.find( image => image.path === selectedAssistant?.image_path )?.base64 || "" const modelDetails = LLM_LIST.find( model => model.modelId === selectedPreset?.model ) return ( { setIsOpen(isOpen) setSearch("") }} > {presets.length === 0 && assistants.length === 0 ? (
No items found.
) : ( <> setSearch(e.target.value)} onKeyDown={e => e.stopPropagation()} /> {!!(selectedPreset || selectedAssistant) && ( | Tables<"assistants">) } onSelect={() => { handleSelectQuickSetting(null, "remove") }} image={selectedPreset ? "" : selectedAssistantImage} /> )} {items .filter( item => item.name.toLowerCase().includes(search.toLowerCase()) && item.id !== selectedPreset?.id && item.id !== selectedAssistant?.id ) .map(({ contentType, ...item }) => ( handleSelectQuickSetting( item, contentType as "presets" | "assistants" ) } image={ contentType === "assistants" ? assistantImages.find( image => image.path === (item as Tables<"assistants">).image_path )?.base64 || "" : "" } /> ))} )}
) } ================================================ FILE: components/chat/tool-picker.tsx ================================================ import { ChatbotUIContext } from "@/context/context" import { Tables } from "@/supabase/types" import { IconBolt } from "@tabler/icons-react" import { FC, useContext, useEffect, useRef } from "react" import { usePromptAndCommand } from "./chat-hooks/use-prompt-and-command" interface ToolPickerProps {} export const ToolPicker: FC = ({}) => { const { tools, focusTool, toolCommand, isToolPickerOpen, setIsToolPickerOpen } = useContext(ChatbotUIContext) const { handleSelectTool } = usePromptAndCommand() const itemsRef = useRef<(HTMLDivElement | null)[]>([]) useEffect(() => { if (focusTool && itemsRef.current[0]) { itemsRef.current[0].focus() } }, [focusTool]) const filteredTools = tools.filter(tool => tool.name.toLowerCase().includes(toolCommand.toLowerCase()) ) const handleOpenChange = (isOpen: boolean) => { setIsToolPickerOpen(isOpen) } const callSelectTool = (tool: Tables<"tools">) => { handleSelectTool(tool) handleOpenChange(false) } const getKeyDownHandler = (index: number) => (e: React.KeyboardEvent) => { if (e.key === "Backspace") { e.preventDefault() handleOpenChange(false) } else if (e.key === "Enter") { e.preventDefault() callSelectTool(filteredTools[index]) } else if ( (e.key === "Tab" || e.key === "ArrowDown") && !e.shiftKey && index === filteredTools.length - 1 ) { e.preventDefault() itemsRef.current[0]?.focus() } else if (e.key === "ArrowUp" && !e.shiftKey && index === 0) { // go to last element if arrow up is pressed on first element e.preventDefault() itemsRef.current[itemsRef.current.length - 1]?.focus() } else if (e.key === "ArrowUp") { e.preventDefault() const prevIndex = index - 1 >= 0 ? index - 1 : itemsRef.current.length - 1 itemsRef.current[prevIndex]?.focus() } else if (e.key === "ArrowDown") { e.preventDefault() const nextIndex = index + 1 < itemsRef.current.length ? index + 1 : 0 itemsRef.current[nextIndex]?.focus() } } return ( <> {isToolPickerOpen && (
{filteredTools.length === 0 ? (
No matching tools.
) : ( <> {filteredTools.map((item, index) => (
{ itemsRef.current[index] = ref }} tabIndex={0} className="hover:bg-accent focus:bg-accent flex cursor-pointer items-center rounded p-2 focus:outline-none" onClick={() => callSelectTool(item as Tables<"tools">)} onKeyDown={getKeyDownHandler(index)} >
{item.name}
{item.description || "No description."}
))} )}
)} ) } ================================================ FILE: components/icons/anthropic-svg.tsx ================================================ import { FC } from "react" interface AnthropicSVGProps { height?: number width?: number className?: string } export const AnthropicSVG: FC = ({ height = 40, width = 40, className }) => { return ( ) } ================================================ FILE: components/icons/chatbotui-svg.tsx ================================================ import { FC } from "react" interface ChatbotUISVGProps { theme: "dark" | "light" scale?: number } export const ChatbotUISVG: FC = ({ theme, scale = 1 }) => { return ( ) } ================================================ FILE: components/icons/google-svg.tsx ================================================ import { FC } from "react" interface GoogleSVGProps { height?: number width?: number className?: string } export const GoogleSVG: FC = ({ height = 40, width = 40, className }) => { return ( ) } ================================================ FILE: components/icons/openai-svg.tsx ================================================ import { FC } from "react" interface OpenAISVGProps { height?: number width?: number className?: string } export const OpenAISVG: FC = ({ height = 40, width = 40, className }) => { return ( ) } ================================================ FILE: components/messages/message-actions.tsx ================================================ import { ChatbotUIContext } from "@/context/context" import { IconCheck, IconCopy, IconEdit, IconRepeat } from "@tabler/icons-react" import { FC, useContext, useEffect, useState } from "react" import { WithTooltip } from "../ui/with-tooltip" export const MESSAGE_ICON_SIZE = 18 interface MessageActionsProps { isAssistant: boolean isLast: boolean isEditing: boolean isHovering: boolean onCopy: () => void onEdit: () => void onRegenerate: () => void } export const MessageActions: FC = ({ isAssistant, isLast, isEditing, isHovering, onCopy, onEdit, onRegenerate }) => { const { isGenerating } = useContext(ChatbotUIContext) const [showCheckmark, setShowCheckmark] = useState(false) const handleCopy = () => { onCopy() setShowCheckmark(true) } const handleForkChat = async () => {} useEffect(() => { if (showCheckmark) { const timer = setTimeout(() => { setShowCheckmark(false) }, 2000) return () => clearTimeout(timer) } }, [showCheckmark]) return (isLast && isGenerating) || isEditing ? null : (
{/* {((isAssistant && isHovering) || isLast) && ( Fork Chat
} trigger={ } /> )} */} {!isAssistant && isHovering && ( Edit
} trigger={ } /> )} {(isHovering || isLast) && ( Copy
} trigger={ showCheckmark ? ( ) : ( ) } /> )} {isLast && ( Regenerate} trigger={ } /> )} {/* {1 > 0 && isAssistant && } */} ) } ================================================ FILE: components/messages/message-codeblock.tsx ================================================ import { Button } from "@/components/ui/button" import { useCopyToClipboard } from "@/lib/hooks/use-copy-to-clipboard" import { IconCheck, IconCopy, IconDownload } from "@tabler/icons-react" import { FC, memo } from "react" import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" import { oneDark } from "react-syntax-highlighter/dist/cjs/styles/prism" interface MessageCodeBlockProps { language: string value: string } interface languageMap { [key: string]: string | undefined } export const programmingLanguages: languageMap = { javascript: ".js", python: ".py", java: ".java", c: ".c", cpp: ".cpp", "c++": ".cpp", "c#": ".cs", ruby: ".rb", php: ".php", swift: ".swift", "objective-c": ".m", kotlin: ".kt", typescript: ".ts", go: ".go", perl: ".pl", rust: ".rs", scala: ".scala", haskell: ".hs", lua: ".lua", shell: ".sh", sql: ".sql", html: ".html", css: ".css" } export const generateRandomString = (length: number, lowercase = false) => { const chars = "ABCDEFGHJKLMNPQRSTUVWXY3456789" // excluding similar looking characters like Z, 2, I, 1, O, 0 let result = "" for (let i = 0; i < length; i++) { result += chars.charAt(Math.floor(Math.random() * chars.length)) } return lowercase ? result.toLowerCase() : result } export const MessageCodeBlock: FC = memo( ({ language, value }) => { const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 }) const downloadAsFile = () => { if (typeof window === "undefined") { return } const fileExtension = programmingLanguages[language] || ".file" const suggestedFileName = `file-${generateRandomString( 3, true )}${fileExtension}` const fileName = window.prompt("Enter file name" || "", suggestedFileName) if (!fileName) { return } const blob = new Blob([value], { type: "text/plain" }) const url = URL.createObjectURL(blob) const link = document.createElement("a") link.download = fileName link.href = url link.style.display = "none" document.body.appendChild(link) link.click() document.body.removeChild(link) URL.revokeObjectURL(url) } const onCopy = () => { if (isCopied) return copyToClipboard(value) } return (
{language}
{value}
) } ) MessageCodeBlock.displayName = "MessageCodeBlock" ================================================ FILE: components/messages/message-markdown-memoized.tsx ================================================ import { FC, memo } from "react" import ReactMarkdown, { Options } from "react-markdown" export const MessageMarkdownMemoized: FC = memo( ReactMarkdown, (prevProps, nextProps) => prevProps.children === nextProps.children && prevProps.className === nextProps.className ) ================================================ FILE: components/messages/message-markdown.tsx ================================================ import React, { FC } from "react" import remarkGfm from "remark-gfm" import remarkMath from "remark-math" import { MessageCodeBlock } from "./message-codeblock" import { MessageMarkdownMemoized } from "./message-markdown-memoized" interface MessageMarkdownProps { content: string } export const MessageMarkdown: FC = ({ content }) => { return ( {children}

}, img({ node, ...props }) { return }, code({ node, className, children, ...props }) { const childArray = React.Children.toArray(children) const firstChild = childArray[0] as React.ReactElement const firstChildAsString = React.isValidElement(firstChild) ? (firstChild as React.ReactElement).props.children : firstChild if (firstChildAsString === "▍") { return } if (typeof firstChildAsString === "string") { childArray[0] = firstChildAsString.replace("`▍`", "▍") } const match = /language-(\w+)/.exec(className || "") if ( typeof firstChildAsString === "string" && !firstChildAsString.includes("\n") ) { return ( {childArray} ) } return ( ) } }} > {content}
) } ================================================ FILE: components/messages/message-replies.tsx ================================================ import { IconMessage } from "@tabler/icons-react" import { FC, useState } from "react" import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from "../ui/sheet" import { WithTooltip } from "../ui/with-tooltip" import { MESSAGE_ICON_SIZE } from "./message-actions" interface MessageRepliesProps {} export const MessageReplies: FC = ({}) => { const [isOpen, setIsOpen] = useState(false) return ( View Replies} trigger={
setIsOpen(true)} >
{1}
} />
Are you sure absolutely sure? This action cannot be undone. This will permanently delete your account and remove your data from our servers.
) } ================================================ FILE: components/messages/message.tsx ================================================ import { useChatHandler } from "@/components/chat/chat-hooks/use-chat-handler" import { ChatbotUIContext } from "@/context/context" import { LLM_LIST } from "@/lib/models/llm/llm-list" import { cn } from "@/lib/utils" import { Tables } from "@/supabase/types" import { LLM, LLMID, MessageImage, ModelProvider } from "@/types" import { IconBolt, IconCaretDownFilled, IconCaretRightFilled, IconCircleFilled, IconFileText, IconMoodSmile, IconPencil } from "@tabler/icons-react" import Image from "next/image" import { FC, useContext, useEffect, useRef, useState } from "react" import { ModelIcon } from "../models/model-icon" import { Button } from "../ui/button" import { FileIcon } from "../ui/file-icon" import { FilePreview } from "../ui/file-preview" import { TextareaAutosize } from "../ui/textarea-autosize" import { WithTooltip } from "../ui/with-tooltip" import { MessageActions } from "./message-actions" import { MessageMarkdown } from "./message-markdown" const ICON_SIZE = 32 interface MessageProps { message: Tables<"messages"> fileItems: Tables<"file_items">[] isEditing: boolean isLast: boolean onStartEdit: (message: Tables<"messages">) => void onCancelEdit: () => void onSubmitEdit: (value: string, sequenceNumber: number) => void } export const Message: FC = ({ message, fileItems, isEditing, isLast, onStartEdit, onCancelEdit, onSubmitEdit }) => { const { assistants, profile, isGenerating, setIsGenerating, firstTokenReceived, availableLocalModels, availableOpenRouterModels, chatMessages, selectedAssistant, chatImages, assistantImages, toolInUse, files, models } = useContext(ChatbotUIContext) const { handleSendMessage } = useChatHandler() const editInputRef = useRef(null) const [isHovering, setIsHovering] = useState(false) const [editedMessage, setEditedMessage] = useState(message.content) const [showImagePreview, setShowImagePreview] = useState(false) const [selectedImage, setSelectedImage] = useState(null) const [showFileItemPreview, setShowFileItemPreview] = useState(false) const [selectedFileItem, setSelectedFileItem] = useState | null>(null) const [viewSources, setViewSources] = useState(false) const handleCopy = () => { if (navigator.clipboard) { navigator.clipboard.writeText(message.content) } else { const textArea = document.createElement("textarea") textArea.value = message.content document.body.appendChild(textArea) textArea.focus() textArea.select() document.execCommand("copy") document.body.removeChild(textArea) } } const handleSendEdit = () => { onSubmitEdit(editedMessage, message.sequence_number) onCancelEdit() } const handleKeyDown = (event: React.KeyboardEvent) => { if (isEditing && event.key === "Enter" && event.metaKey) { handleSendEdit() } } const handleRegenerate = async () => { setIsGenerating(true) await handleSendMessage( editedMessage || chatMessages[chatMessages.length - 2].message.content, chatMessages, true ) } const handleStartEdit = () => { onStartEdit(message) } useEffect(() => { setEditedMessage(message.content) if (isEditing && editInputRef.current) { const input = editInputRef.current input.focus() input.setSelectionRange(input.value.length, input.value.length) } }, [isEditing]) const MODEL_DATA = [ ...models.map(model => ({ modelId: model.model_id as LLMID, modelName: model.name, provider: "custom" as ModelProvider, hostedId: model.id, platformLink: "", imageInput: false })), ...LLM_LIST, ...availableLocalModels, ...availableOpenRouterModels ].find(llm => llm.modelId === message.model) as LLM const messageAssistantImage = assistantImages.find( image => image.assistantId === message.assistant_id )?.base64 const selectedAssistantImage = assistantImages.find( image => image.path === selectedAssistant?.image_path )?.base64 const modelDetails = LLM_LIST.find(model => model.modelId === message.model) const fileAccumulator: Record< string, { id: string name: string count: number type: string description: string } > = {} const fileSummary = fileItems.reduce((acc, fileItem) => { const parentFile = files.find(file => file.id === fileItem.file_id) if (parentFile) { if (!acc[parentFile.id]) { acc[parentFile.id] = { id: parentFile.id, name: parentFile.name, count: 1, type: parentFile.type, description: parentFile.description } } else { acc[parentFile.id].count += 1 } } return acc }, fileAccumulator) return (
setIsHovering(true)} onMouseLeave={() => setIsHovering(false)} onKeyDown={handleKeyDown} >
{message.role === "system" ? (
Prompt
) : (
{message.role === "assistant" ? ( messageAssistantImage ? ( assistant image ) : ( {MODEL_DATA?.modelName}
} trigger={ } /> ) ) : profile?.image_url ? ( user image ) : ( )}
{message.role === "assistant" ? message.assistant_id ? assistants.find( assistant => assistant.id === message.assistant_id )?.name : selectedAssistant ? selectedAssistant?.name : MODEL_DATA?.modelName : profile?.display_name ?? profile?.username}
)} {!firstTokenReceived && isGenerating && isLast && message.role === "assistant" ? ( <> {(() => { switch (toolInUse) { case "none": return ( ) case "retrieval": return (
Searching files...
) default: return (
Using {toolInUse}...
) } })()} ) : isEditing ? ( ) : ( )}
{fileItems.length > 0 && (
{!viewSources ? (
setViewSources(true)} > {fileItems.length} {fileItems.length > 1 ? " Sources " : " Source "} from {Object.keys(fileSummary).length}{" "} {Object.keys(fileSummary).length > 1 ? "Files" : "File"}{" "}
) : ( <>
setViewSources(false)} > {fileItems.length} {fileItems.length > 1 ? " Sources " : " Source "} from {Object.keys(fileSummary).length}{" "} {Object.keys(fileSummary).length > 1 ? "Files" : "File"}{" "}
{Object.values(fileSummary).map((file, index) => (
{file.name}
{fileItems .filter(fileItem => { const parentFile = files.find( parentFile => parentFile.id === fileItem.file_id ) return parentFile?.id === file.id }) .map((fileItem, index) => (
{ setSelectedFileItem(fileItem) setShowFileItemPreview(true) }} >
-{" "} {fileItem.content.substring(0, 200)}...
))}
))}
)}
)}
{message.image_paths.map((path, index) => { const item = chatImages.find(image => image.path === path) return ( message image { setSelectedImage({ messageId: message.id, path, base64: path.startsWith("data") ? path : item?.base64 || "", url: path.startsWith("data") ? "" : item?.url || "", file: null }) setShowImagePreview(true) }} loading="lazy" /> ) })}
{isEditing && (
)}
{showImagePreview && selectedImage && ( { setShowImagePreview(isOpen) setSelectedImage(null) }} /> )} {showFileItemPreview && selectedFileItem && ( { setShowFileItemPreview(isOpen) setSelectedFileItem(null) }} /> )} ) } ================================================ FILE: components/models/model-icon.tsx ================================================ import { cn } from "@/lib/utils" import mistral from "@/public/providers/mistral.png" import groq from "@/public/providers/groq.png" import perplexity from "@/public/providers/perplexity.png" import { ModelProvider } from "@/types" import { IconSparkles } from "@tabler/icons-react" import { useTheme } from "next-themes" import Image from "next/image" import { FC, HTMLAttributes } from "react" import { AnthropicSVG } from "../icons/anthropic-svg" import { GoogleSVG } from "../icons/google-svg" import { OpenAISVG } from "../icons/openai-svg" interface ModelIconProps extends HTMLAttributes { provider: ModelProvider height: number width: number } export const ModelIcon: FC = ({ provider, height, width, ...props }) => { const { theme } = useTheme() switch (provider as ModelProvider) { case "openai": return ( ) case "mistral": return ( Mistral ) case "groq": return ( Groq ) case "anthropic": return ( ) case "google": return ( ) case "perplexity": return ( Mistral ) default: return } } ================================================ FILE: components/models/model-option.tsx ================================================ import { LLM } from "@/types" import { FC } from "react" import { ModelIcon } from "./model-icon" import { IconInfoCircle } from "@tabler/icons-react" import { WithTooltip } from "../ui/with-tooltip" interface ModelOptionProps { model: LLM onSelect: () => void } export const ModelOption: FC = ({ model, onSelect }) => { return ( {model.provider !== "ollama" && model.pricing && (
Input Cost:{" "} {model.pricing.inputCost} {model.pricing.currency} per{" "} {model.pricing.unit}
{model.pricing.outputCost && (
Output Cost:{" "} {model.pricing.outputCost} {model.pricing.currency} per{" "} {model.pricing.unit}
)}
)} } side="bottom" trigger={
{model.modelName}
} /> ) } ================================================ FILE: components/models/model-select.tsx ================================================ import { ChatbotUIContext } from "@/context/context" import { LLM, LLMID, ModelProvider } from "@/types" import { IconCheck, IconChevronDown } from "@tabler/icons-react" import { FC, useContext, useEffect, useRef, useState } from "react" import { Button } from "../ui/button" import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "../ui/dropdown-menu" import { Input } from "../ui/input" import { Tabs, TabsList, TabsTrigger } from "../ui/tabs" import { ModelIcon } from "./model-icon" import { ModelOption } from "./model-option" interface ModelSelectProps { selectedModelId: string onSelectModel: (modelId: LLMID) => void } export const ModelSelect: FC = ({ selectedModelId, onSelectModel }) => { const { profile, models, availableHostedModels, availableLocalModels, availableOpenRouterModels } = useContext(ChatbotUIContext) const inputRef = useRef(null) const triggerRef = useRef(null) const [isOpen, setIsOpen] = useState(false) const [search, setSearch] = useState("") const [tab, setTab] = useState<"hosted" | "local">("hosted") useEffect(() => { if (isOpen) { setTimeout(() => { inputRef.current?.focus() }, 100) // FIX: hacky } }, [isOpen]) const handleSelectModel = (modelId: LLMID) => { onSelectModel(modelId) setIsOpen(false) } const allModels = [ ...models.map(model => ({ modelId: model.model_id as LLMID, modelName: model.name, provider: "custom" as ModelProvider, hostedId: model.id, platformLink: "", imageInput: false })), ...availableHostedModels, ...availableLocalModels, ...availableOpenRouterModels ] const groupedModels = allModels.reduce>( (groups, model) => { const key = model.provider if (!groups[key]) { groups[key] = [] } groups[key].push(model) return groups }, {} ) const selectedModel = allModels.find( model => model.modelId === selectedModelId ) if (!profile) return null return ( { setIsOpen(isOpen) setSearch("") }} > {allModels.length === 0 ? (
Unlock models by entering API keys in your profile settings.
) : ( )}
setTab(value)}> {availableLocalModels.length > 0 && ( Hosted Local )} setSearch(e.target.value)} />
{Object.entries(groupedModels).map(([provider, models]) => { const filteredModels = models .filter(model => { if (tab === "hosted") return model.provider !== "ollama" if (tab === "local") return model.provider === "ollama" if (tab === "openrouter") return model.provider === "openrouter" }) .filter(model => model.modelName.toLowerCase().includes(search.toLowerCase()) ) .sort((a, b) => a.provider.localeCompare(b.provider)) if (filteredModels.length === 0) return null return (
{provider === "openai" && profile.use_azure_openai ? "AZURE OPENAI" : provider.toLocaleUpperCase()}
{filteredModels.map(model => { return (
{selectedModelId === model.modelId && ( )} handleSelectModel(model.modelId)} />
) })}
) })}
) } ================================================ FILE: components/setup/api-step.tsx ================================================ import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { FC } from "react" import { Button } from "../ui/button" interface APIStepProps { openaiAPIKey: string openaiOrgID: string azureOpenaiAPIKey: string azureOpenaiEndpoint: string azureOpenai35TurboID: string azureOpenai45TurboID: string azureOpenai45VisionID: string azureOpenaiEmbeddingsID: string anthropicAPIKey: string googleGeminiAPIKey: string mistralAPIKey: string groqAPIKey: string perplexityAPIKey: string useAzureOpenai: boolean openrouterAPIKey: string onOpenrouterAPIKeyChange: (value: string) => void onOpenaiAPIKeyChange: (value: string) => void onOpenaiOrgIDChange: (value: string) => void onAzureOpenaiAPIKeyChange: (value: string) => void onAzureOpenaiEndpointChange: (value: string) => void onAzureOpenai35TurboIDChange: (value: string) => void onAzureOpenai45TurboIDChange: (value: string) => void onAzureOpenai45VisionIDChange: (value: string) => void onAzureOpenaiEmbeddingsIDChange: (value: string) => void onAnthropicAPIKeyChange: (value: string) => void onGoogleGeminiAPIKeyChange: (value: string) => void onMistralAPIKeyChange: (value: string) => void onGroqAPIKeyChange: (value: string) => void onPerplexityAPIKeyChange: (value: string) => void onUseAzureOpenaiChange: (value: boolean) => void } export const APIStep: FC = ({ openaiAPIKey, openaiOrgID, azureOpenaiAPIKey, azureOpenaiEndpoint, azureOpenai35TurboID, azureOpenai45TurboID, azureOpenai45VisionID, azureOpenaiEmbeddingsID, anthropicAPIKey, googleGeminiAPIKey, mistralAPIKey, groqAPIKey, perplexityAPIKey, openrouterAPIKey, useAzureOpenai, onOpenaiAPIKeyChange, onOpenaiOrgIDChange, onAzureOpenaiAPIKeyChange, onAzureOpenaiEndpointChange, onAzureOpenai35TurboIDChange, onAzureOpenai45TurboIDChange, onAzureOpenai45VisionIDChange, onAzureOpenaiEmbeddingsIDChange, onAnthropicAPIKeyChange, onGoogleGeminiAPIKeyChange, onMistralAPIKeyChange, onGroqAPIKeyChange, onPerplexityAPIKeyChange, onUseAzureOpenaiChange, onOpenrouterAPIKeyChange }) => { return ( <>
useAzureOpenai ? onAzureOpenaiAPIKeyChange(e.target.value) : onOpenaiAPIKeyChange(e.target.value) } />
{useAzureOpenai ? ( <>
onAzureOpenaiEndpointChange(e.target.value)} />
onAzureOpenai35TurboIDChange(e.target.value)} />
onAzureOpenai45TurboIDChange(e.target.value)} />
onAzureOpenai45VisionIDChange(e.target.value)} />
onAzureOpenaiEmbeddingsIDChange(e.target.value)} />
) : ( <>
onOpenaiOrgIDChange(e.target.value)} />
)}
onAnthropicAPIKeyChange(e.target.value)} />
onGoogleGeminiAPIKeyChange(e.target.value)} />
onMistralAPIKeyChange(e.target.value)} />
onGroqAPIKeyChange(e.target.value)} />
onPerplexityAPIKeyChange(e.target.value)} />
onOpenrouterAPIKeyChange(e.target.value)} />
) } ================================================ FILE: components/setup/finish-step.tsx ================================================ import { FC } from "react" interface FinishStepProps { displayName: string } export const FinishStep: FC = ({ displayName }) => { return (
Welcome to Chatbot UI {displayName.length > 0 ? `, ${displayName.split(" ")[0]}` : null}!
Click next to start chatting.
) } ================================================ FILE: components/setup/profile-step.tsx ================================================ import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { PROFILE_DISPLAY_NAME_MAX, PROFILE_USERNAME_MAX, PROFILE_USERNAME_MIN } from "@/db/limits" import { IconCircleCheckFilled, IconCircleXFilled, IconLoader2 } from "@tabler/icons-react" import { FC, useCallback, useState } from "react" import { LimitDisplay } from "../ui/limit-display" import { toast } from "sonner" interface ProfileStepProps { username: string usernameAvailable: boolean displayName: string onUsernameAvailableChange: (isAvailable: boolean) => void onUsernameChange: (username: string) => void onDisplayNameChange: (name: string) => void } export const ProfileStep: FC = ({ username, usernameAvailable, displayName, onUsernameAvailableChange, onUsernameChange, onDisplayNameChange }) => { const [loading, setLoading] = useState(false) const debounce = (func: (...args: any[]) => void, wait: number) => { let timeout: NodeJS.Timeout | null return (...args: any[]) => { const later = () => { if (timeout) clearTimeout(timeout) func(...args) } if (timeout) clearTimeout(timeout) timeout = setTimeout(later, wait) } } const checkUsernameAvailability = useCallback( debounce(async (username: string) => { if (!username) return if (username.length < PROFILE_USERNAME_MIN) { onUsernameAvailableChange(false) return } if (username.length > PROFILE_USERNAME_MAX) { onUsernameAvailableChange(false) return } const usernameRegex = /^[a-zA-Z0-9_]+$/ if (!usernameRegex.test(username)) { onUsernameAvailableChange(false) toast.error( "Username must be letters, numbers, or underscores only - no other characters or spacing allowed." ) return } setLoading(true) const response = await fetch(`/api/username/available`, { method: "POST", body: JSON.stringify({ username }) }) const data = await response.json() const isAvailable = data.isAvailable onUsernameAvailableChange(isAvailable) setLoading(false) }, 500), [] ) return ( <>
{usernameAvailable ? (
AVAILABLE
) : (
UNAVAILABLE
)}
{ onUsernameChange(e.target.value) checkUsernameAvailability(e.target.value) }} minLength={PROFILE_USERNAME_MIN} maxLength={PROFILE_USERNAME_MAX} />
{loading ? ( ) : usernameAvailable ? ( ) : ( )}
onDisplayNameChange(e.target.value)} maxLength={PROFILE_DISPLAY_NAME_MAX} />
) } ================================================ FILE: components/setup/step-container.tsx ================================================ import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" import { FC, useRef } from "react" export const SETUP_STEP_COUNT = 3 interface StepContainerProps { stepDescription: string stepNum: number stepTitle: string onShouldProceed: (shouldProceed: boolean) => void children?: React.ReactNode showBackButton?: boolean showNextButton?: boolean } export const StepContainer: FC = ({ stepDescription, stepNum, stepTitle, onShouldProceed, children, showBackButton = false, showNextButton = true }) => { const buttonRef = useRef(null) const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { if (buttonRef.current) { buttonRef.current.click() } } } return (
{stepTitle}
{stepNum} / {SETUP_STEP_COUNT}
{stepDescription}
{children}
{showBackButton && ( )}
{showNextButton && ( )}
) } ================================================ FILE: components/sidebar/items/all/sidebar-create-item.tsx ================================================ import { Button } from "@/components/ui/button" import { Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle } from "@/components/ui/sheet" import { ChatbotUIContext } from "@/context/context" import { createAssistantCollections } from "@/db/assistant-collections" import { createAssistantFiles } from "@/db/assistant-files" import { createAssistantTools } from "@/db/assistant-tools" import { createAssistant, updateAssistant } from "@/db/assistants" import { createChat } from "@/db/chats" import { createCollectionFiles } from "@/db/collection-files" import { createCollection } from "@/db/collections" import { createFileBasedOnExtension } from "@/db/files" import { createModel } from "@/db/models" import { createPreset } from "@/db/presets" import { createPrompt } from "@/db/prompts" import { getAssistantImageFromStorage, uploadAssistantImage } from "@/db/storage/assistant-images" import { createTool } from "@/db/tools" import { convertBlobToBase64 } from "@/lib/blob-to-b64" import { Tables, TablesInsert } from "@/supabase/types" import { ContentType } from "@/types" import { FC, useContext, useRef, useState } from "react" import { toast } from "sonner" interface SidebarCreateItemProps { isOpen: boolean isTyping: boolean onOpenChange: (isOpen: boolean) => void contentType: ContentType renderInputs: () => JSX.Element createState: any } export const SidebarCreateItem: FC = ({ isOpen, onOpenChange, contentType, renderInputs, createState, isTyping }) => { const { selectedWorkspace, setChats, setPresets, setPrompts, setFiles, setCollections, setAssistants, setAssistantImages, setTools, setModels } = useContext(ChatbotUIContext) const buttonRef = useRef(null) const [creating, setCreating] = useState(false) const createFunctions = { chats: createChat, presets: createPreset, prompts: createPrompt, files: async ( createState: { file: File } & TablesInsert<"files">, workspaceId: string ) => { if (!selectedWorkspace) return const { file, ...rest } = createState const createdFile = await createFileBasedOnExtension( file, rest, workspaceId, selectedWorkspace.embeddings_provider as "openai" | "local" ) return createdFile }, collections: async ( createState: { image: File collectionFiles: TablesInsert<"collection_files">[] } & Tables<"collections">, workspaceId: string ) => { const { collectionFiles, ...rest } = createState const createdCollection = await createCollection(rest, workspaceId) const finalCollectionFiles = collectionFiles.map(collectionFile => ({ ...collectionFile, collection_id: createdCollection.id })) await createCollectionFiles(finalCollectionFiles) return createdCollection }, assistants: async ( createState: { image: File files: Tables<"files">[] collections: Tables<"collections">[] tools: Tables<"tools">[] } & Tables<"assistants">, workspaceId: string ) => { const { image, files, collections, tools, ...rest } = createState const createdAssistant = await createAssistant(rest, workspaceId) let updatedAssistant = createdAssistant if (image) { const filePath = await uploadAssistantImage(createdAssistant, image) updatedAssistant = await updateAssistant(createdAssistant.id, { image_path: filePath }) const url = (await getAssistantImageFromStorage(filePath)) || "" if (url) { const response = await fetch(url) const blob = await response.blob() const base64 = await convertBlobToBase64(blob) setAssistantImages(prev => [ ...prev, { assistantId: updatedAssistant.id, path: filePath, base64, url } ]) } } const assistantFiles = files.map(file => ({ user_id: rest.user_id, assistant_id: createdAssistant.id, file_id: file.id })) const assistantCollections = collections.map(collection => ({ user_id: rest.user_id, assistant_id: createdAssistant.id, collection_id: collection.id })) const assistantTools = tools.map(tool => ({ user_id: rest.user_id, assistant_id: createdAssistant.id, tool_id: tool.id })) await createAssistantFiles(assistantFiles) await createAssistantCollections(assistantCollections) await createAssistantTools(assistantTools) return updatedAssistant }, tools: createTool, models: createModel } const stateUpdateFunctions = { chats: setChats, presets: setPresets, prompts: setPrompts, files: setFiles, collections: setCollections, assistants: setAssistants, tools: setTools, models: setModels } const handleCreate = async () => { try { if (!selectedWorkspace) return if (isTyping) return // Prevent creation while typing const createFunction = createFunctions[contentType] const setStateFunction = stateUpdateFunctions[contentType] if (!createFunction || !setStateFunction) return setCreating(true) const newItem = await createFunction(createState, selectedWorkspace.id) setStateFunction((prevItems: any) => [...prevItems, newItem]) onOpenChange(false) setCreating(false) } catch (error) { toast.error(`Error creating ${contentType.slice(0, -1)}. ${error}.`) setCreating(false) } } const handleKeyDown = (e: React.KeyboardEvent) => { if (!isTyping && e.key === "Enter" && !e.shiftKey) { e.preventDefault() buttonRef.current?.click() } } return (
Create{" "} {contentType.charAt(0).toUpperCase() + contentType.slice(1, -1)}
{renderInputs()}
) } ================================================ FILE: components/sidebar/items/all/sidebar-delete-item.tsx ================================================ import { Button } from "@/components/ui/button" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog" import { ChatbotUIContext } from "@/context/context" import { deleteAssistant } from "@/db/assistants" import { deleteChat } from "@/db/chats" import { deleteCollection } from "@/db/collections" import { deleteFile } from "@/db/files" import { deleteModel } from "@/db/models" import { deletePreset } from "@/db/presets" import { deletePrompt } from "@/db/prompts" import { deleteFileFromStorage } from "@/db/storage/files" import { deleteTool } from "@/db/tools" import { Tables } from "@/supabase/types" import { ContentType, DataItemType } from "@/types" import { FC, useContext, useRef, useState } from "react" interface SidebarDeleteItemProps { item: DataItemType contentType: ContentType } export const SidebarDeleteItem: FC = ({ item, contentType }) => { const { setChats, setPresets, setPrompts, setFiles, setCollections, setAssistants, setTools, setModels } = useContext(ChatbotUIContext) const buttonRef = useRef(null) const [showDialog, setShowDialog] = useState(false) const deleteFunctions = { chats: async (chat: Tables<"chats">) => { await deleteChat(chat.id) }, presets: async (preset: Tables<"presets">) => { await deletePreset(preset.id) }, prompts: async (prompt: Tables<"prompts">) => { await deletePrompt(prompt.id) }, files: async (file: Tables<"files">) => { await deleteFileFromStorage(file.file_path) await deleteFile(file.id) }, collections: async (collection: Tables<"collections">) => { await deleteCollection(collection.id) }, assistants: async (assistant: Tables<"assistants">) => { await deleteAssistant(assistant.id) setChats(prevState => prevState.filter(chat => chat.assistant_id !== assistant.id) ) }, tools: async (tool: Tables<"tools">) => { await deleteTool(tool.id) }, models: async (model: Tables<"models">) => { await deleteModel(model.id) } } const stateUpdateFunctions = { chats: setChats, presets: setPresets, prompts: setPrompts, files: setFiles, collections: setCollections, assistants: setAssistants, tools: setTools, models: setModels } const handleDelete = async () => { const deleteFunction = deleteFunctions[contentType] const setStateFunction = stateUpdateFunctions[contentType] if (!deleteFunction || !setStateFunction) return await deleteFunction(item as any) setStateFunction((prevItems: any) => prevItems.filter((prevItem: any) => prevItem.id !== item.id) ) setShowDialog(false) } const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") { e.stopPropagation() buttonRef.current?.click() } } return ( Delete {contentType.slice(0, -1)} Are you sure you want to delete {item.name}? ) } ================================================ FILE: components/sidebar/items/all/sidebar-display-item.tsx ================================================ import { ChatbotUIContext } from "@/context/context" import { createChat } from "@/db/chats" import { cn } from "@/lib/utils" import { Tables } from "@/supabase/types" import { ContentType, DataItemType } from "@/types" import { useRouter } from "next/navigation" import { FC, useContext, useRef, useState } from "react" import { SidebarUpdateItem } from "./sidebar-update-item" interface SidebarItemProps { item: DataItemType isTyping: boolean contentType: ContentType icon: React.ReactNode updateState: any renderInputs: (renderState: any) => JSX.Element } export const SidebarItem: FC = ({ item, contentType, updateState, renderInputs, icon, isTyping }) => { const { selectedWorkspace, setChats, setSelectedAssistant } = useContext(ChatbotUIContext) const router = useRouter() const itemRef = useRef(null) const [isHovering, setIsHovering] = useState(false) const actionMap = { chats: async (item: any) => {}, presets: async (item: any) => {}, prompts: async (item: any) => {}, files: async (item: any) => {}, collections: async (item: any) => {}, assistants: async (assistant: Tables<"assistants">) => { if (!selectedWorkspace) return const createdChat = await createChat({ user_id: assistant.user_id, workspace_id: selectedWorkspace.id, assistant_id: assistant.id, context_length: assistant.context_length, include_profile_context: assistant.include_profile_context, include_workspace_instructions: assistant.include_workspace_instructions, model: assistant.model, name: `Chat with ${assistant.name}`, prompt: assistant.prompt, temperature: assistant.temperature, embeddings_provider: assistant.embeddings_provider }) setChats(prevState => [createdChat, ...prevState]) setSelectedAssistant(assistant) return router.push(`/${selectedWorkspace.id}/chat/${createdChat.id}`) }, tools: async (item: any) => {}, models: async (item: any) => {} } const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") { e.stopPropagation() itemRef.current?.click() } } // const handleClickAction = async ( // e: React.MouseEvent // ) => { // e.stopPropagation() // const action = actionMap[contentType] // await action(item as any) // } return (
setIsHovering(true)} onMouseLeave={() => setIsHovering(false)} > {icon}
{item.name}
{/* TODO */} {/* {isHovering && ( Start chat with {contentType.slice(0, -1)}
} trigger={ } /> )} */}
) } ================================================ FILE: components/sidebar/items/all/sidebar-update-item.tsx ================================================ import { Button } from "@/components/ui/button" import { Label } from "@/components/ui/label" import { Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet" import { AssignWorkspaces } from "@/components/workspace/assign-workspaces" import { ChatbotUIContext } from "@/context/context" import { createAssistantCollection, deleteAssistantCollection, getAssistantCollectionsByAssistantId } from "@/db/assistant-collections" import { createAssistantFile, deleteAssistantFile, getAssistantFilesByAssistantId } from "@/db/assistant-files" import { createAssistantTool, deleteAssistantTool, getAssistantToolsByAssistantId } from "@/db/assistant-tools" import { createAssistantWorkspaces, deleteAssistantWorkspace, getAssistantWorkspacesByAssistantId, updateAssistant } from "@/db/assistants" import { updateChat } from "@/db/chats" import { createCollectionFile, deleteCollectionFile, getCollectionFilesByCollectionId } from "@/db/collection-files" import { createCollectionWorkspaces, deleteCollectionWorkspace, getCollectionWorkspacesByCollectionId, updateCollection } from "@/db/collections" import { createFileWorkspaces, deleteFileWorkspace, getFileWorkspacesByFileId, updateFile } from "@/db/files" import { createModelWorkspaces, deleteModelWorkspace, getModelWorkspacesByModelId, updateModel } from "@/db/models" import { createPresetWorkspaces, deletePresetWorkspace, getPresetWorkspacesByPresetId, updatePreset } from "@/db/presets" import { createPromptWorkspaces, deletePromptWorkspace, getPromptWorkspacesByPromptId, updatePrompt } from "@/db/prompts" import { getAssistantImageFromStorage, uploadAssistantImage } from "@/db/storage/assistant-images" import { createToolWorkspaces, deleteToolWorkspace, getToolWorkspacesByToolId, updateTool } from "@/db/tools" import { convertBlobToBase64 } from "@/lib/blob-to-b64" import { Tables, TablesUpdate } from "@/supabase/types" import { CollectionFile, ContentType, DataItemType } from "@/types" import { FC, useContext, useEffect, useRef, useState } from "react" import profile from "react-syntax-highlighter/dist/esm/languages/hljs/profile" import { toast } from "sonner" import { SidebarDeleteItem } from "./sidebar-delete-item" interface SidebarUpdateItemProps { isTyping: boolean item: DataItemType contentType: ContentType children: React.ReactNode renderInputs: (renderState: any) => JSX.Element updateState: any } export const SidebarUpdateItem: FC = ({ item, contentType, children, renderInputs, updateState, isTyping }) => { const { workspaces, selectedWorkspace, setChats, setPresets, setPrompts, setFiles, setCollections, setAssistants, setTools, setModels, setAssistantImages } = useContext(ChatbotUIContext) const buttonRef = useRef(null) const [isOpen, setIsOpen] = useState(false) const [startingWorkspaces, setStartingWorkspaces] = useState< Tables<"workspaces">[] >([]) const [selectedWorkspaces, setSelectedWorkspaces] = useState< Tables<"workspaces">[] >([]) // Collections Render State const [startingCollectionFiles, setStartingCollectionFiles] = useState< CollectionFile[] >([]) const [selectedCollectionFiles, setSelectedCollectionFiles] = useState< CollectionFile[] >([]) // Assistants Render State const [startingAssistantFiles, setStartingAssistantFiles] = useState< Tables<"files">[] >([]) const [startingAssistantCollections, setStartingAssistantCollections] = useState[]>([]) const [startingAssistantTools, setStartingAssistantTools] = useState< Tables<"tools">[] >([]) const [selectedAssistantFiles, setSelectedAssistantFiles] = useState< Tables<"files">[] >([]) const [selectedAssistantCollections, setSelectedAssistantCollections] = useState[]>([]) const [selectedAssistantTools, setSelectedAssistantTools] = useState< Tables<"tools">[] >([]) useEffect(() => { if (isOpen) { const fetchData = async () => { if (workspaces.length > 1) { const workspaces = await fetchSelectedWorkspaces() setStartingWorkspaces(workspaces) setSelectedWorkspaces(workspaces) } const fetchDataFunction = fetchDataFunctions[contentType] if (!fetchDataFunction) return await fetchDataFunction(item.id) } fetchData() } }, [isOpen]) const renderState = { chats: null, presets: null, prompts: null, files: null, collections: { startingCollectionFiles, setStartingCollectionFiles, selectedCollectionFiles, setSelectedCollectionFiles }, assistants: { startingAssistantFiles, setStartingAssistantFiles, startingAssistantCollections, setStartingAssistantCollections, startingAssistantTools, setStartingAssistantTools, selectedAssistantFiles, setSelectedAssistantFiles, selectedAssistantCollections, setSelectedAssistantCollections, selectedAssistantTools, setSelectedAssistantTools }, tools: null, models: null } const fetchDataFunctions = { chats: null, presets: null, prompts: null, files: null, collections: async (collectionId: string) => { const collectionFiles = await getCollectionFilesByCollectionId(collectionId) setStartingCollectionFiles(collectionFiles.files) setSelectedCollectionFiles([]) }, assistants: async (assistantId: string) => { const assistantFiles = await getAssistantFilesByAssistantId(assistantId) setStartingAssistantFiles(assistantFiles.files) const assistantCollections = await getAssistantCollectionsByAssistantId(assistantId) setStartingAssistantCollections(assistantCollections.collections) const assistantTools = await getAssistantToolsByAssistantId(assistantId) setStartingAssistantTools(assistantTools.tools) setSelectedAssistantFiles([]) setSelectedAssistantCollections([]) setSelectedAssistantTools([]) }, tools: null, models: null } const fetchWorkpaceFunctions = { chats: null, presets: async (presetId: string) => { const item = await getPresetWorkspacesByPresetId(presetId) return item.workspaces }, prompts: async (promptId: string) => { const item = await getPromptWorkspacesByPromptId(promptId) return item.workspaces }, files: async (fileId: string) => { const item = await getFileWorkspacesByFileId(fileId) return item.workspaces }, collections: async (collectionId: string) => { const item = await getCollectionWorkspacesByCollectionId(collectionId) return item.workspaces }, assistants: async (assistantId: string) => { const item = await getAssistantWorkspacesByAssistantId(assistantId) return item.workspaces }, tools: async (toolId: string) => { const item = await getToolWorkspacesByToolId(toolId) return item.workspaces }, models: async (modelId: string) => { const item = await getModelWorkspacesByModelId(modelId) return item.workspaces } } const fetchSelectedWorkspaces = async () => { const fetchFunction = fetchWorkpaceFunctions[contentType] if (!fetchFunction) return [] const workspaces = await fetchFunction(item.id) return workspaces } const handleWorkspaceUpdates = async ( startingWorkspaces: Tables<"workspaces">[], selectedWorkspaces: Tables<"workspaces">[], itemId: string, deleteWorkspaceFn: ( itemId: string, workspaceId: string ) => Promise, createWorkspaceFn: ( workspaces: { user_id: string; item_id: string; workspace_id: string }[] ) => Promise, itemIdKey: string ) => { if (!selectedWorkspace) return const deleteList = startingWorkspaces.filter( startingWorkspace => !selectedWorkspaces.some( selectedWorkspace => selectedWorkspace.id === startingWorkspace.id ) ) for (const workspace of deleteList) { await deleteWorkspaceFn(itemId, workspace.id) } if (deleteList.map(w => w.id).includes(selectedWorkspace.id)) { const setStateFunction = stateUpdateFunctions[contentType] if (setStateFunction) { setStateFunction((prevItems: any) => prevItems.filter((prevItem: any) => prevItem.id !== item.id) ) } } const createList = selectedWorkspaces.filter( selectedWorkspace => !startingWorkspaces.some( startingWorkspace => startingWorkspace.id === selectedWorkspace.id ) ) await createWorkspaceFn( createList.map(workspace => { return { user_id: workspace.user_id, [itemIdKey]: itemId, workspace_id: workspace.id } as any }) ) } const updateFunctions = { chats: updateChat, presets: async (presetId: string, updateState: TablesUpdate<"presets">) => { const updatedPreset = await updatePreset(presetId, updateState) await handleWorkspaceUpdates( startingWorkspaces, selectedWorkspaces, presetId, deletePresetWorkspace, createPresetWorkspaces as any, "preset_id" ) return updatedPreset }, prompts: async (promptId: string, updateState: TablesUpdate<"prompts">) => { const updatedPrompt = await updatePrompt(promptId, updateState) await handleWorkspaceUpdates( startingWorkspaces, selectedWorkspaces, promptId, deletePromptWorkspace, createPromptWorkspaces as any, "prompt_id" ) return updatedPrompt }, files: async (fileId: string, updateState: TablesUpdate<"files">) => { const updatedFile = await updateFile(fileId, updateState) await handleWorkspaceUpdates( startingWorkspaces, selectedWorkspaces, fileId, deleteFileWorkspace, createFileWorkspaces as any, "file_id" ) return updatedFile }, collections: async ( collectionId: string, updateState: TablesUpdate<"assistants"> ) => { if (!profile) return const { ...rest } = updateState const filesToAdd = selectedCollectionFiles.filter( selectedFile => !startingCollectionFiles.some( startingFile => startingFile.id === selectedFile.id ) ) const filesToRemove = startingCollectionFiles.filter(startingFile => selectedCollectionFiles.some( selectedFile => selectedFile.id === startingFile.id ) ) for (const file of filesToAdd) { await createCollectionFile({ user_id: item.user_id, collection_id: collectionId, file_id: file.id }) } for (const file of filesToRemove) { await deleteCollectionFile(collectionId, file.id) } const updatedCollection = await updateCollection(collectionId, rest) await handleWorkspaceUpdates( startingWorkspaces, selectedWorkspaces, collectionId, deleteCollectionWorkspace, createCollectionWorkspaces as any, "collection_id" ) return updatedCollection }, assistants: async ( assistantId: string, updateState: { assistantId: string image: File } & TablesUpdate<"assistants"> ) => { const { image, ...rest } = updateState const filesToAdd = selectedAssistantFiles.filter( selectedFile => !startingAssistantFiles.some( startingFile => startingFile.id === selectedFile.id ) ) const filesToRemove = startingAssistantFiles.filter(startingFile => selectedAssistantFiles.some( selectedFile => selectedFile.id === startingFile.id ) ) for (const file of filesToAdd) { await createAssistantFile({ user_id: item.user_id, assistant_id: assistantId, file_id: file.id }) } for (const file of filesToRemove) { await deleteAssistantFile(assistantId, file.id) } const collectionsToAdd = selectedAssistantCollections.filter( selectedCollection => !startingAssistantCollections.some( startingCollection => startingCollection.id === selectedCollection.id ) ) const collectionsToRemove = startingAssistantCollections.filter( startingCollection => selectedAssistantCollections.some( selectedCollection => selectedCollection.id === startingCollection.id ) ) for (const collection of collectionsToAdd) { await createAssistantCollection({ user_id: item.user_id, assistant_id: assistantId, collection_id: collection.id }) } for (const collection of collectionsToRemove) { await deleteAssistantCollection(assistantId, collection.id) } const toolsToAdd = selectedAssistantTools.filter( selectedTool => !startingAssistantTools.some( startingTool => startingTool.id === selectedTool.id ) ) const toolsToRemove = startingAssistantTools.filter(startingTool => selectedAssistantTools.some( selectedTool => selectedTool.id === startingTool.id ) ) for (const tool of toolsToAdd) { await createAssistantTool({ user_id: item.user_id, assistant_id: assistantId, tool_id: tool.id }) } for (const tool of toolsToRemove) { await deleteAssistantTool(assistantId, tool.id) } let updatedAssistant = await updateAssistant(assistantId, rest) if (image) { const filePath = await uploadAssistantImage(updatedAssistant, image) updatedAssistant = await updateAssistant(assistantId, { image_path: filePath }) const url = (await getAssistantImageFromStorage(filePath)) || "" if (url) { const response = await fetch(url) const blob = await response.blob() const base64 = await convertBlobToBase64(blob) setAssistantImages(prev => [ ...prev, { assistantId: updatedAssistant.id, path: filePath, base64, url } ]) } } await handleWorkspaceUpdates( startingWorkspaces, selectedWorkspaces, assistantId, deleteAssistantWorkspace, createAssistantWorkspaces as any, "assistant_id" ) return updatedAssistant }, tools: async (toolId: string, updateState: TablesUpdate<"tools">) => { const updatedTool = await updateTool(toolId, updateState) await handleWorkspaceUpdates( startingWorkspaces, selectedWorkspaces, toolId, deleteToolWorkspace, createToolWorkspaces as any, "tool_id" ) return updatedTool }, models: async (modelId: string, updateState: TablesUpdate<"models">) => { const updatedModel = await updateModel(modelId, updateState) await handleWorkspaceUpdates( startingWorkspaces, selectedWorkspaces, modelId, deleteModelWorkspace, createModelWorkspaces as any, "model_id" ) return updatedModel } } const stateUpdateFunctions = { chats: setChats, presets: setPresets, prompts: setPrompts, files: setFiles, collections: setCollections, assistants: setAssistants, tools: setTools, models: setModels } const handleUpdate = async () => { try { const updateFunction = updateFunctions[contentType] const setStateFunction = stateUpdateFunctions[contentType] if (!updateFunction || !setStateFunction) return if (isTyping) return // Prevent update while typing const updatedItem = await updateFunction(item.id, updateState) setStateFunction((prevItems: any) => prevItems.map((prevItem: any) => prevItem.id === item.id ? updatedItem : prevItem ) ) setIsOpen(false) toast.success(`${contentType.slice(0, -1)} updated successfully`) } catch (error) { toast.error(`Error updating ${contentType.slice(0, -1)}. ${error}`) } } const handleSelectWorkspace = (workspace: Tables<"workspaces">) => { setSelectedWorkspaces(prevState => { const isWorkspaceAlreadySelected = prevState.find( selectedWorkspace => selectedWorkspace.id === workspace.id ) if (isWorkspaceAlreadySelected) { return prevState.filter( selectedWorkspace => selectedWorkspace.id !== workspace.id ) } else { return [...prevState, workspace] } }) } const handleKeyDown = (e: React.KeyboardEvent) => { if (!isTyping && e.key === "Enter" && !e.shiftKey) { e.preventDefault() buttonRef.current?.click() } } return ( {children}
Edit {contentType.slice(0, -1)}
{workspaces.length > 1 && (
)} {renderInputs(renderState[contentType])}
) } ================================================ FILE: components/sidebar/items/assistants/assistant-item.tsx ================================================ import { ChatSettingsForm } from "@/components/ui/chat-settings-form" import ImagePicker from "@/components/ui/image-picker" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { ChatbotUIContext } from "@/context/context" import { ASSISTANT_DESCRIPTION_MAX, ASSISTANT_NAME_MAX } from "@/db/limits" import { Tables } from "@/supabase/types" import { IconRobotFace } from "@tabler/icons-react" import Image from "next/image" import { FC, useContext, useEffect, useState } from "react" import profile from "react-syntax-highlighter/dist/esm/languages/hljs/profile" import { SidebarItem } from "../all/sidebar-display-item" import { AssistantRetrievalSelect } from "./assistant-retrieval-select" import { AssistantToolSelect } from "./assistant-tool-select" interface AssistantItemProps { assistant: Tables<"assistants"> } export const AssistantItem: FC = ({ assistant }) => { const { selectedWorkspace, assistantImages } = useContext(ChatbotUIContext) const [name, setName] = useState(assistant.name) const [isTyping, setIsTyping] = useState(false) const [description, setDescription] = useState(assistant.description) const [assistantChatSettings, setAssistantChatSettings] = useState({ model: assistant.model, prompt: assistant.prompt, temperature: assistant.temperature, contextLength: assistant.context_length, includeProfileContext: assistant.include_profile_context, includeWorkspaceInstructions: assistant.include_workspace_instructions }) const [selectedImage, setSelectedImage] = useState(null) const [imageLink, setImageLink] = useState("") useEffect(() => { const assistantImage = assistantImages.find(image => image.path === assistant.image_path) ?.base64 || "" setImageLink(assistantImage) }, [assistant, assistantImages]) const handleFileSelect = ( file: Tables<"files">, setSelectedAssistantFiles: React.Dispatch< React.SetStateAction[]> > ) => { setSelectedAssistantFiles(prevState => { const isFileAlreadySelected = prevState.find( selectedFile => selectedFile.id === file.id ) if (isFileAlreadySelected) { return prevState.filter(selectedFile => selectedFile.id !== file.id) } else { return [...prevState, file] } }) } const handleCollectionSelect = ( collection: Tables<"collections">, setSelectedAssistantCollections: React.Dispatch< React.SetStateAction[]> > ) => { setSelectedAssistantCollections(prevState => { const isCollectionAlreadySelected = prevState.find( selectedCollection => selectedCollection.id === collection.id ) if (isCollectionAlreadySelected) { return prevState.filter( selectedCollection => selectedCollection.id !== collection.id ) } else { return [...prevState, collection] } }) } const handleToolSelect = ( tool: Tables<"tools">, setSelectedAssistantTools: React.Dispatch< React.SetStateAction[]> > ) => { setSelectedAssistantTools(prevState => { const isToolAlreadySelected = prevState.find( selectedTool => selectedTool.id === tool.id ) if (isToolAlreadySelected) { return prevState.filter(selectedTool => selectedTool.id !== tool.id) } else { return [...prevState, tool] } }) } if (!profile) return null if (!selectedWorkspace) return null return ( ) : ( ) } updateState={{ image: selectedImage, user_id: assistant.user_id, name, description, include_profile_context: assistantChatSettings.includeProfileContext, include_workspace_instructions: assistantChatSettings.includeWorkspaceInstructions, context_length: assistantChatSettings.contextLength, model: assistantChatSettings.model, image_path: assistant.image_path, prompt: assistantChatSettings.prompt, temperature: assistantChatSettings.temperature }} renderInputs={(renderState: { startingAssistantFiles: Tables<"files">[] setStartingAssistantFiles: React.Dispatch< React.SetStateAction[]> > selectedAssistantFiles: Tables<"files">[] setSelectedAssistantFiles: React.Dispatch< React.SetStateAction[]> > startingAssistantCollections: Tables<"collections">[] setStartingAssistantCollections: React.Dispatch< React.SetStateAction[]> > selectedAssistantCollections: Tables<"collections">[] setSelectedAssistantCollections: React.Dispatch< React.SetStateAction[]> > startingAssistantTools: Tables<"tools">[] setStartingAssistantTools: React.Dispatch< React.SetStateAction[]> > selectedAssistantTools: Tables<"tools">[] setSelectedAssistantTools: React.Dispatch< React.SetStateAction[]> > }) => ( <>
setName(e.target.value)} maxLength={ASSISTANT_NAME_MAX} />
setDescription(e.target.value)} maxLength={ASSISTANT_DESCRIPTION_MAX} />
![ ...renderState.selectedAssistantFiles, ...renderState.selectedAssistantCollections ].some( selectedFile => selectedFile.id === startingFile.id ) ), ...renderState.selectedAssistantFiles.filter( selectedFile => !renderState.startingAssistantFiles.some( startingFile => startingFile.id === selectedFile.id ) ), ...renderState.startingAssistantCollections.filter( startingCollection => ![ ...renderState.selectedAssistantFiles, ...renderState.selectedAssistantCollections ].some( selectedCollection => selectedCollection.id === startingCollection.id ) ), ...renderState.selectedAssistantCollections.filter( selectedCollection => !renderState.startingAssistantCollections.some( startingCollection => startingCollection.id === selectedCollection.id ) ) ] } onAssistantRetrievalItemsSelect={item => "type" in item ? handleFileSelect( item, renderState.setSelectedAssistantFiles ) : handleCollectionSelect( item, renderState.setSelectedAssistantCollections ) } />
!renderState.selectedAssistantTools.some( selectedTool => selectedTool.id === startingTool.id ) ), ...renderState.selectedAssistantTools.filter( selectedTool => !renderState.startingAssistantTools.some( startingTool => startingTool.id === selectedTool.id ) ) ] } onAssistantToolsSelect={tool => handleToolSelect(tool, renderState.setSelectedAssistantTools) } />
)} /> ) } ================================================ FILE: components/sidebar/items/assistants/assistant-retrieval-select.tsx ================================================ import { Button } from "@/components/ui/button" import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" import { Input } from "@/components/ui/input" import { ChatbotUIContext } from "@/context/context" import { Tables } from "@/supabase/types" import { IconBooks, IconChevronDown, IconCircleCheckFilled } from "@tabler/icons-react" import { FileIcon } from "lucide-react" import { FC, useContext, useEffect, useRef, useState } from "react" interface AssistantRetrievalSelectProps { selectedAssistantRetrievalItems: Tables<"files">[] | Tables<"collections">[] onAssistantRetrievalItemsSelect: ( item: Tables<"files"> | Tables<"collections"> ) => void } export const AssistantRetrievalSelect: FC = ({ selectedAssistantRetrievalItems, onAssistantRetrievalItemsSelect }) => { const { files, collections } = useContext(ChatbotUIContext) const inputRef = useRef(null) const triggerRef = useRef(null) const [isOpen, setIsOpen] = useState(false) const [search, setSearch] = useState("") useEffect(() => { if (isOpen) { setTimeout(() => { inputRef.current?.focus() }, 100) // FIX: hacky } }, [isOpen]) const handleItemSelect = (item: Tables<"files"> | Tables<"collections">) => { onAssistantRetrievalItemsSelect(item) } if (!files || !collections) return null return ( { setIsOpen(isOpen) setSearch("") }} > setSearch(e.target.value)} onKeyDown={e => e.stopPropagation()} /> {selectedAssistantRetrievalItems .filter(item => item.name.toLowerCase().includes(search.toLowerCase()) ) .map(item => ( | Tables<"collections">} selected={selectedAssistantRetrievalItems.some( selectedAssistantRetrieval => selectedAssistantRetrieval.id === item.id )} onSelect={handleItemSelect} /> ))} {files .filter( file => !selectedAssistantRetrievalItems.some( selectedAssistantRetrieval => selectedAssistantRetrieval.id === file.id ) && file.name.toLowerCase().includes(search.toLowerCase()) ) .map(file => ( selectedAssistantRetrieval.id === file.id )} onSelect={handleItemSelect} /> ))} {collections .filter( collection => !selectedAssistantRetrievalItems.some( selectedAssistantRetrieval => selectedAssistantRetrieval.id === collection.id ) && collection.name.toLowerCase().includes(search.toLowerCase()) ) .map(collection => ( selectedAssistantRetrieval.id === collection.id )} onSelect={handleItemSelect} /> ))} ) } interface AssistantRetrievalOptionItemProps { contentType: "files" | "collections" item: Tables<"files"> | Tables<"collections"> selected: boolean onSelect: (item: Tables<"files"> | Tables<"collections">) => void } const AssistantRetrievalItemOption: FC = ({ contentType, item, selected, onSelect }) => { const handleSelect = () => { onSelect(item) } return (
{contentType === "files" ? (
).type} size={24} />
) : (
)}
{item.name}
{selected && ( )}
) } ================================================ FILE: components/sidebar/items/assistants/assistant-tool-select.tsx ================================================ import { Button } from "@/components/ui/button" import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" import { Input } from "@/components/ui/input" import { ChatbotUIContext } from "@/context/context" import { Tables } from "@/supabase/types" import { IconBolt, IconChevronDown, IconCircleCheckFilled } from "@tabler/icons-react" import { FC, useContext, useEffect, useRef, useState } from "react" interface AssistantToolSelectProps { selectedAssistantTools: Tables<"tools">[] onAssistantToolsSelect: (tool: Tables<"tools">) => void } export const AssistantToolSelect: FC = ({ selectedAssistantTools, onAssistantToolsSelect }) => { const { tools } = useContext(ChatbotUIContext) const inputRef = useRef(null) const triggerRef = useRef(null) const [isOpen, setIsOpen] = useState(false) const [search, setSearch] = useState("") useEffect(() => { if (isOpen) { setTimeout(() => { inputRef.current?.focus() }, 100) // FIX: hacky } }, [isOpen]) const handleToolSelect = (tool: Tables<"tools">) => { onAssistantToolsSelect(tool) } if (!tools) return null return ( { setIsOpen(isOpen) setSearch("") }} > setSearch(e.target.value)} onKeyDown={e => e.stopPropagation()} /> {selectedAssistantTools .filter(item => item.name.toLowerCase().includes(search.toLowerCase()) ) .map(tool => ( selectedAssistantRetrieval.id === tool.id )} onSelect={handleToolSelect} /> ))} {tools .filter( tool => !selectedAssistantTools.some( selectedAssistantRetrieval => selectedAssistantRetrieval.id === tool.id ) && tool.name.toLowerCase().includes(search.toLowerCase()) ) .map(tool => ( selectedAssistantRetrieval.id === tool.id )} onSelect={handleToolSelect} /> ))} ) } interface AssistantToolItemProps { tool: Tables<"tools"> selected: boolean onSelect: (tool: Tables<"tools">) => void } const AssistantToolItem: FC = ({ tool, selected, onSelect }) => { const handleSelect = () => { onSelect(tool) } return (
{tool.name}
{selected && ( )}
) } ================================================ FILE: components/sidebar/items/assistants/create-assistant.tsx ================================================ import { SidebarCreateItem } from "@/components/sidebar/items/all/sidebar-create-item" import { ChatSettingsForm } from "@/components/ui/chat-settings-form" import ImagePicker from "@/components/ui/image-picker" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { ChatbotUIContext } from "@/context/context" import { ASSISTANT_DESCRIPTION_MAX, ASSISTANT_NAME_MAX } from "@/db/limits" import { Tables, TablesInsert } from "@/supabase/types" import { FC, useContext, useEffect, useState } from "react" import { AssistantRetrievalSelect } from "./assistant-retrieval-select" import { AssistantToolSelect } from "./assistant-tool-select" interface CreateAssistantProps { isOpen: boolean onOpenChange: (isOpen: boolean) => void } export const CreateAssistant: FC = ({ isOpen, onOpenChange }) => { const { profile, selectedWorkspace } = useContext(ChatbotUIContext) const [name, setName] = useState("") const [isTyping, setIsTyping] = useState(false) const [description, setDescription] = useState("") const [assistantChatSettings, setAssistantChatSettings] = useState({ model: selectedWorkspace?.default_model, prompt: selectedWorkspace?.default_prompt, temperature: selectedWorkspace?.default_temperature, contextLength: selectedWorkspace?.default_context_length, includeProfileContext: false, includeWorkspaceInstructions: false, embeddingsProvider: selectedWorkspace?.embeddings_provider }) const [selectedImage, setSelectedImage] = useState(null) const [imageLink, setImageLink] = useState("") const [selectedAssistantRetrievalItems, setSelectedAssistantRetrievalItems] = useState[] | Tables<"collections">[]>([]) const [selectedAssistantToolItems, setSelectedAssistantToolItems] = useState< Tables<"tools">[] >([]) useEffect(() => { setAssistantChatSettings(prevSettings => { const previousPrompt = prevSettings.prompt || "" const previousPromptParts = previousPrompt.split(". ") previousPromptParts[0] = name ? `You are ${name}` : "" return { ...prevSettings, prompt: previousPromptParts.join(". ") } }) }, [name]) const handleRetrievalItemSelect = ( item: Tables<"files"> | Tables<"collections"> ) => { setSelectedAssistantRetrievalItems(prevState => { const isItemAlreadySelected = prevState.find( selectedItem => selectedItem.id === item.id ) if (isItemAlreadySelected) { return prevState.filter(selectedItem => selectedItem.id !== item.id) } else { return [...prevState, item] } }) } const handleToolSelect = (item: Tables<"tools">) => { setSelectedAssistantToolItems(prevState => { const isItemAlreadySelected = prevState.find( selectedItem => selectedItem.id === item.id ) if (isItemAlreadySelected) { return prevState.filter(selectedItem => selectedItem.id !== item.id) } else { return [...prevState, item] } }) } const checkIfModelIsToolCompatible = () => { if (!assistantChatSettings.model) return false const compatibleModels = [ "gpt-4-turbo-preview", "gpt-4-vision-preview", "gpt-3.5-turbo-1106", "gpt-4" ] const isModelCompatible = compatibleModels.includes( assistantChatSettings.model ) return isModelCompatible } if (!profile) return null if (!selectedWorkspace) return null return ( item.hasOwnProperty("type") ) as Tables<"files">[], collections: selectedAssistantRetrievalItems.filter( item => !item.hasOwnProperty("type") ) as Tables<"collections">[], tools: selectedAssistantToolItems } as TablesInsert<"assistants"> } isOpen={isOpen} isTyping={isTyping} renderInputs={() => ( <>
setName(e.target.value)} maxLength={ASSISTANT_NAME_MAX} />
setDescription(e.target.value)} maxLength={ASSISTANT_DESCRIPTION_MAX} />
{checkIfModelIsToolCompatible() ? (
) : (
Model is not compatible with tools.
)} )} onOpenChange={onOpenChange} /> ) } ================================================ FILE: components/sidebar/items/chat/chat-item.tsx ================================================ import { ModelIcon } from "@/components/models/model-icon" import { WithTooltip } from "@/components/ui/with-tooltip" import { ChatbotUIContext } from "@/context/context" import { LLM_LIST } from "@/lib/models/llm/llm-list" import { cn } from "@/lib/utils" import { Tables } from "@/supabase/types" import { LLM } from "@/types" import { IconRobotFace } from "@tabler/icons-react" import Image from "next/image" import { useParams, useRouter } from "next/navigation" import { FC, useContext, useRef } from "react" import { DeleteChat } from "./delete-chat" import { UpdateChat } from "./update-chat" interface ChatItemProps { chat: Tables<"chats"> } export const ChatItem: FC = ({ chat }) => { const { selectedWorkspace, selectedChat, availableLocalModels, assistantImages, availableOpenRouterModels } = useContext(ChatbotUIContext) const router = useRouter() const params = useParams() const isActive = params.chatid === chat.id || selectedChat?.id === chat.id const itemRef = useRef(null) const handleClick = () => { if (!selectedWorkspace) return return router.push(`/${selectedWorkspace.id}/chat/${chat.id}`) } const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") { e.stopPropagation() itemRef.current?.click() } } const MODEL_DATA = [ ...LLM_LIST, ...availableLocalModels, ...availableOpenRouterModels ].find(llm => llm.modelId === chat.model) as LLM const assistantImage = assistantImages.find( image => image.assistantId === chat.assistant_id )?.base64 return (
{chat.assistant_id ? ( assistantImage ? ( Assistant image ) : ( ) ) : ( {MODEL_DATA?.modelName}
} trigger={ } /> )}
{chat.name}
{ e.stopPropagation() e.preventDefault() }} className={`ml-2 flex space-x-2 ${!isActive && "w-11 opacity-0 group-hover:opacity-100"}`} >
) } ================================================ FILE: components/sidebar/items/chat/delete-chat.tsx ================================================ import { useChatHandler } from "@/components/chat/chat-hooks/use-chat-handler" import { Button } from "@/components/ui/button" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog" import { ChatbotUIContext } from "@/context/context" import { deleteChat } from "@/db/chats" import useHotkey from "@/lib/hooks/use-hotkey" import { Tables } from "@/supabase/types" import { IconTrash } from "@tabler/icons-react" import { FC, useContext, useRef, useState } from "react" interface DeleteChatProps { chat: Tables<"chats"> } export const DeleteChat: FC = ({ chat }) => { useHotkey("Backspace", () => setShowChatDialog(true)) const { setChats } = useContext(ChatbotUIContext) const { handleNewChat } = useChatHandler() const buttonRef = useRef(null) const [showChatDialog, setShowChatDialog] = useState(false) const handleDeleteChat = async () => { await deleteChat(chat.id) setChats(prevState => prevState.filter(c => c.id !== chat.id)) setShowChatDialog(false) handleNewChat() } const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") { buttonRef.current?.click() } } return ( Delete {chat.name} Are you sure you want to delete this chat? ) } ================================================ FILE: components/sidebar/items/chat/update-chat.tsx ================================================ import { Button } from "@/components/ui/button" import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { ChatbotUIContext } from "@/context/context" import { updateChat } from "@/db/chats" import { Tables } from "@/supabase/types" import { IconEdit } from "@tabler/icons-react" import { FC, useContext, useRef, useState } from "react" interface UpdateChatProps { chat: Tables<"chats"> } export const UpdateChat: FC = ({ chat }) => { const { setChats } = useContext(ChatbotUIContext) const buttonRef = useRef(null) const [showChatDialog, setShowChatDialog] = useState(false) const [name, setName] = useState(chat.name) const handleUpdateChat = async (e: React.MouseEvent) => { const updatedChat = await updateChat(chat.id, { name }) setChats(prevState => prevState.map(c => (c.id === chat.id ? updatedChat : c)) ) setShowChatDialog(false) } const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") { buttonRef.current?.click() } } return ( Edit Chat
setName(e.target.value)} />
) } ================================================ FILE: components/sidebar/items/collections/collection-file-select.tsx ================================================ import { Button } from "@/components/ui/button" import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" import { FileIcon } from "@/components/ui/file-icon" import { Input } from "@/components/ui/input" import { ChatbotUIContext } from "@/context/context" import { CollectionFile } from "@/types" import { IconChevronDown, IconCircleCheckFilled } from "@tabler/icons-react" import { FC, useContext, useEffect, useRef, useState } from "react" interface CollectionFileSelectProps { selectedCollectionFiles: CollectionFile[] onCollectionFileSelect: (file: CollectionFile) => void } export const CollectionFileSelect: FC = ({ selectedCollectionFiles, onCollectionFileSelect }) => { const { files } = useContext(ChatbotUIContext) const inputRef = useRef(null) const triggerRef = useRef(null) const [isOpen, setIsOpen] = useState(false) const [search, setSearch] = useState("") useEffect(() => { if (isOpen) { setTimeout(() => { inputRef.current?.focus() }, 100) // FIX: hacky } }, [isOpen]) const handleFileSelect = (file: CollectionFile) => { onCollectionFileSelect(file) } if (!files) return null return ( { setIsOpen(isOpen) setSearch("") }} > setSearch(e.target.value)} onKeyDown={e => e.stopPropagation()} /> {selectedCollectionFiles .filter(file => file.name.toLowerCase().includes(search.toLowerCase()) ) .map(file => ( selectedCollectionFile.id === file.id )} onSelect={handleFileSelect} /> ))} {files .filter( file => !selectedCollectionFiles.some( selectedCollectionFile => selectedCollectionFile.id === file.id ) && file.name.toLowerCase().includes(search.toLowerCase()) ) .map(file => ( selectedCollectionFile.id === file.id )} onSelect={handleFileSelect} /> ))} ) } interface CollectionFileItemProps { file: CollectionFile selected: boolean onSelect: (file: CollectionFile) => void } const CollectionFileItem: FC = ({ file, selected, onSelect }) => { const handleSelect = () => { onSelect(file) } return (
{file.name}
{selected && ( )}
) } ================================================ FILE: components/sidebar/items/collections/collection-item.tsx ================================================ import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { COLLECTION_DESCRIPTION_MAX, COLLECTION_NAME_MAX } from "@/db/limits" import { Tables } from "@/supabase/types" import { CollectionFile } from "@/types" import { IconBooks } from "@tabler/icons-react" import { FC, useState } from "react" import { SidebarItem } from "../all/sidebar-display-item" import { CollectionFileSelect } from "./collection-file-select" interface CollectionItemProps { collection: Tables<"collections"> } export const CollectionItem: FC = ({ collection }) => { const [name, setName] = useState(collection.name) const [isTyping, setIsTyping] = useState(false) const [description, setDescription] = useState(collection.description) const handleFileSelect = ( file: CollectionFile, setSelectedCollectionFiles: React.Dispatch< React.SetStateAction > ) => { setSelectedCollectionFiles(prevState => { const isFileAlreadySelected = prevState.find( selectedFile => selectedFile.id === file.id ) if (isFileAlreadySelected) { return prevState.filter(selectedFile => selectedFile.id !== file.id) } else { return [...prevState, file] } }) } return ( } updateState={{ name, description }} renderInputs={(renderState: { startingCollectionFiles: CollectionFile[] setStartingCollectionFiles: React.Dispatch< React.SetStateAction > selectedCollectionFiles: CollectionFile[] setSelectedCollectionFiles: React.Dispatch< React.SetStateAction > }) => { return ( <>
!renderState.selectedCollectionFiles.some( selectedFile => selectedFile.id === startingFile.id ) ), ...renderState.selectedCollectionFiles.filter( selectedFile => !renderState.startingCollectionFiles.some( startingFile => startingFile.id === selectedFile.id ) ) ] } onCollectionFileSelect={file => handleFileSelect(file, renderState.setSelectedCollectionFiles) } />
setName(e.target.value)} maxLength={COLLECTION_NAME_MAX} />
setDescription(e.target.value)} maxLength={COLLECTION_DESCRIPTION_MAX} />
) }} /> ) } ================================================ FILE: components/sidebar/items/collections/create-collection.tsx ================================================ import { SidebarCreateItem } from "@/components/sidebar/items/all/sidebar-create-item" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { ChatbotUIContext } from "@/context/context" import { COLLECTION_DESCRIPTION_MAX, COLLECTION_NAME_MAX } from "@/db/limits" import { TablesInsert } from "@/supabase/types" import { CollectionFile } from "@/types" import { FC, useContext, useState } from "react" import { CollectionFileSelect } from "./collection-file-select" interface CreateCollectionProps { isOpen: boolean onOpenChange: (isOpen: boolean) => void } export const CreateCollection: FC = ({ isOpen, onOpenChange }) => { const { profile, selectedWorkspace } = useContext(ChatbotUIContext) const [name, setName] = useState("") const [isTyping, setIsTyping] = useState(false) const [description, setDescription] = useState("") const [selectedCollectionFiles, setSelectedCollectionFiles] = useState< CollectionFile[] >([]) const handleFileSelect = (file: CollectionFile) => { setSelectedCollectionFiles(prevState => { const isFileAlreadySelected = prevState.find( selectedFile => selectedFile.id === file.id ) if (isFileAlreadySelected) { return prevState.filter(selectedFile => selectedFile.id !== file.id) } else { return [...prevState, file] } }) } if (!profile) return null if (!selectedWorkspace) return null return ( ({ user_id: profile.user_id, collection_id: "", file_id: file.id })), user_id: profile.user_id, name, description } as TablesInsert<"collections"> } isOpen={isOpen} isTyping={isTyping} onOpenChange={onOpenChange} renderInputs={() => ( <>
setName(e.target.value)} maxLength={COLLECTION_NAME_MAX} />
setDescription(e.target.value)} maxLength={COLLECTION_DESCRIPTION_MAX} />
)} /> ) } ================================================ FILE: components/sidebar/items/files/create-file.tsx ================================================ import { ACCEPTED_FILE_TYPES } from "@/components/chat/chat-hooks/use-select-file-handler" import { SidebarCreateItem } from "@/components/sidebar/items/all/sidebar-create-item" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { ChatbotUIContext } from "@/context/context" import { FILE_DESCRIPTION_MAX, FILE_NAME_MAX } from "@/db/limits" import { TablesInsert } from "@/supabase/types" import { FC, useContext, useState } from "react" interface CreateFileProps { isOpen: boolean onOpenChange: (isOpen: boolean) => void } export const CreateFile: FC = ({ isOpen, onOpenChange }) => { const { profile, selectedWorkspace } = useContext(ChatbotUIContext) const [name, setName] = useState("") const [isTyping, setIsTyping] = useState(false) const [description, setDescription] = useState("") const [selectedFile, setSelectedFile] = useState(null) const handleSelectedFile = async (e: React.ChangeEvent) => { if (!e.target.files) return const file = e.target.files[0] if (!file) return setSelectedFile(file) const fileNameWithoutExtension = file.name.split(".").slice(0, -1).join(".") setName(fileNameWithoutExtension) } if (!profile) return null if (!selectedWorkspace) return null return ( } isOpen={isOpen} isTyping={isTyping} onOpenChange={onOpenChange} renderInputs={() => ( <>
setName(e.target.value)} maxLength={FILE_NAME_MAX} />
setDescription(e.target.value)} maxLength={FILE_DESCRIPTION_MAX} />
)} /> ) } ================================================ FILE: components/sidebar/items/files/file-item.tsx ================================================ import { FileIcon } from "@/components/ui/file-icon" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { FILE_DESCRIPTION_MAX, FILE_NAME_MAX } from "@/db/limits" import { getFileFromStorage } from "@/db/storage/files" import { Tables } from "@/supabase/types" import { FC, useState } from "react" import { SidebarItem } from "../all/sidebar-display-item" interface FileItemProps { file: Tables<"files"> } export const FileItem: FC = ({ file }) => { const [name, setName] = useState(file.name) const [isTyping, setIsTyping] = useState(false) const [description, setDescription] = useState(file.description) const getLinkAndView = async () => { const link = await getFileFromStorage(file.file_path) window.open(link, "_blank") } return ( } updateState={{ name, description }} renderInputs={() => ( <>
View {file.name}
{file.type}
{formatFileSize(file.size)}
{file.tokens.toLocaleString()} tokens
setName(e.target.value)} maxLength={FILE_NAME_MAX} />
setDescription(e.target.value)} maxLength={FILE_DESCRIPTION_MAX} />
)} /> ) } export const formatFileSize = (sizeInBytes: number): string => { let size = sizeInBytes let unit = "bytes" if (size >= 1024) { size /= 1024 unit = "KB" } if (size >= 1024) { size /= 1024 unit = "MB" } if (size >= 1024) { size /= 1024 unit = "GB" } return `${size.toFixed(2)} ${unit}` } ================================================ FILE: components/sidebar/items/folders/delete-folder.tsx ================================================ import { Button } from "@/components/ui/button" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog" import { ChatbotUIContext } from "@/context/context" import { deleteFolder } from "@/db/folders" import { supabase } from "@/lib/supabase/browser-client" import { Tables } from "@/supabase/types" import { ContentType } from "@/types" import { IconTrash } from "@tabler/icons-react" import { FC, useContext, useRef, useState } from "react" import { toast } from "sonner" interface DeleteFolderProps { folder: Tables<"folders"> contentType: ContentType } export const DeleteFolder: FC = ({ folder, contentType }) => { const { setChats, setFolders, setPresets, setPrompts, setFiles, setCollections, setAssistants, setTools, setModels } = useContext(ChatbotUIContext) const buttonRef = useRef(null) const [showFolderDialog, setShowFolderDialog] = useState(false) const stateUpdateFunctions = { chats: setChats, presets: setPresets, prompts: setPrompts, files: setFiles, collections: setCollections, assistants: setAssistants, tools: setTools, models: setModels } const handleDeleteFolderOnly = async () => { await deleteFolder(folder.id) setFolders(prevState => prevState.filter(c => c.id !== folder.id)) setShowFolderDialog(false) const setStateFunction = stateUpdateFunctions[contentType] if (!setStateFunction) return setStateFunction((prevItems: any) => prevItems.map((item: any) => { if (item.folder_id === folder.id) { return { ...item, folder_id: null } } return item }) ) } const handleDeleteFolderAndItems = async () => { const setStateFunction = stateUpdateFunctions[contentType] if (!setStateFunction) return const { error } = await supabase .from(contentType) .delete() .eq("folder_id", folder.id) if (error) { toast.error(error.message) } setStateFunction((prevItems: any) => prevItems.filter((item: any) => item.folder_id !== folder.id) ) handleDeleteFolderOnly() } return ( Delete {folder.name} Are you sure you want to delete this folder? ) } ================================================ FILE: components/sidebar/items/folders/folder-item.tsx ================================================ import { cn } from "@/lib/utils" import { Tables } from "@/supabase/types" import { ContentType } from "@/types" import { IconChevronDown, IconChevronRight } from "@tabler/icons-react" import { FC, useRef, useState } from "react" import { DeleteFolder } from "./delete-folder" import { UpdateFolder } from "./update-folder" interface FolderProps { folder: Tables<"folders"> contentType: ContentType children: React.ReactNode onUpdateFolder: (itemId: string, folderId: string | null) => void } export const Folder: FC = ({ folder, contentType, children, onUpdateFolder }) => { const itemRef = useRef(null) const [isDragOver, setIsDragOver] = useState(false) const [isExpanded, setIsExpanded] = useState(false) const [isHovering, setIsHovering] = useState(false) const handleDragEnter = (e: React.DragEvent) => { e.preventDefault() setIsDragOver(true) } const handleDragLeave = (e: React.DragEvent) => { e.preventDefault() setIsDragOver(false) } const handleDragOver = (e: React.DragEvent) => { e.preventDefault() setIsDragOver(true) } const handleDrop = (e: React.DragEvent) => { e.preventDefault() setIsDragOver(false) const itemId = e.dataTransfer.getData("text/plain") onUpdateFolder(itemId, folder.id) } const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") { e.stopPropagation() itemRef.current?.click() } } const handleClick = (e: React.MouseEvent) => { setIsExpanded(!isExpanded) } return (
setIsHovering(true)} onMouseLeave={() => setIsHovering(false)} >
{isExpanded ? ( ) : ( )}
{folder.name}
{isHovering && (
{ e.stopPropagation() e.preventDefault() }} className="ml-2 flex space-x-2" >
)}
{isExpanded && (
{children}
)}
) } ================================================ FILE: components/sidebar/items/folders/update-folder.tsx ================================================ import { Button } from "@/components/ui/button" import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { ChatbotUIContext } from "@/context/context" import { updateFolder } from "@/db/folders" import { Tables } from "@/supabase/types" import { IconEdit } from "@tabler/icons-react" import { FC, useContext, useRef, useState } from "react" interface UpdateFolderProps { folder: Tables<"folders"> } export const UpdateFolder: FC = ({ folder }) => { const { setFolders } = useContext(ChatbotUIContext) const buttonRef = useRef(null) const [showFolderDialog, setShowFolderDialog] = useState(false) const [name, setName] = useState(folder.name) const handleUpdateFolder = async (e: React.MouseEvent) => { const updatedFolder = await updateFolder(folder.id, { name }) setFolders(prevState => prevState.map(c => (c.id === folder.id ? updatedFolder : c)) ) setShowFolderDialog(false) } const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") { buttonRef.current?.click() } } return ( Edit Folder
setName(e.target.value)} />
) } ================================================ FILE: components/sidebar/items/models/create-model.tsx ================================================ import { SidebarCreateItem } from "@/components/sidebar/items/all/sidebar-create-item" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { ChatbotUIContext } from "@/context/context" import { MODEL_NAME_MAX } from "@/db/limits" import { TablesInsert } from "@/supabase/types" import { FC, useContext, useState } from "react" interface CreateModelProps { isOpen: boolean onOpenChange: (isOpen: boolean) => void } export const CreateModel: FC = ({ isOpen, onOpenChange }) => { const { profile, selectedWorkspace } = useContext(ChatbotUIContext) const [isTyping, setIsTyping] = useState(false) const [apiKey, setApiKey] = useState("") const [baseUrl, setBaseUrl] = useState("") const [description, setDescription] = useState("") const [modelId, setModelId] = useState("") const [name, setName] = useState("") const [contextLength, setContextLength] = useState(4096) if (!profile || !selectedWorkspace) return null return ( } renderInputs={() => ( <>
Create a custom model.
Your API *must* be compatible with the OpenAI SDK.
setName(e.target.value)} maxLength={MODEL_NAME_MAX} />
setModelId(e.target.value)} />
setBaseUrl(e.target.value)} />
Your API must be compatible with the OpenAI SDK.
setApiKey(e.target.value)} />
setContextLength(parseInt(e.target.value))} />
)} /> ) } ================================================ FILE: components/sidebar/items/models/model-item.tsx ================================================ import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { MODEL_NAME_MAX } from "@/db/limits" import { Tables, TablesUpdate } from "@/supabase/types" import { IconSparkles } from "@tabler/icons-react" import { FC, useState } from "react" import { SidebarItem } from "../all/sidebar-display-item" interface ModelItemProps { model: Tables<"models"> } export const ModelItem: FC = ({ model }) => { const [isTyping, setIsTyping] = useState(false) const [apiKey, setApiKey] = useState(model.api_key) const [baseUrl, setBaseUrl] = useState(model.base_url) const [description, setDescription] = useState(model.description) const [modelId, setModelId] = useState(model.model_id) const [name, setName] = useState(model.name) const [contextLength, setContextLength] = useState(model.context_length) return ( } updateState={ { api_key: apiKey, base_url: baseUrl, description, context_length: contextLength, model_id: modelId, name } as TablesUpdate<"models"> } renderInputs={() => ( <>
setName(e.target.value)} maxLength={MODEL_NAME_MAX} />
setModelId(e.target.value)} />
setBaseUrl(e.target.value)} />
Your API must be compatible with the OpenAI SDK.
setApiKey(e.target.value)} />
setContextLength(parseInt(e.target.value))} />
)} /> ) } ================================================ FILE: components/sidebar/items/presets/create-preset.tsx ================================================ import { SidebarCreateItem } from "@/components/sidebar/items/all/sidebar-create-item" import { ChatSettingsForm } from "@/components/ui/chat-settings-form" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { ChatbotUIContext } from "@/context/context" import { PRESET_NAME_MAX } from "@/db/limits" import { TablesInsert } from "@/supabase/types" import { FC, useContext, useState } from "react" interface CreatePresetProps { isOpen: boolean onOpenChange: (isOpen: boolean) => void } export const CreatePreset: FC = ({ isOpen, onOpenChange }) => { const { profile, selectedWorkspace } = useContext(ChatbotUIContext) const [name, setName] = useState("") const [isTyping, setIsTyping] = useState(false) const [description, setDescription] = useState("") const [presetChatSettings, setPresetChatSettings] = useState({ model: selectedWorkspace?.default_model, prompt: selectedWorkspace?.default_prompt, temperature: selectedWorkspace?.default_temperature, contextLength: selectedWorkspace?.default_context_length, includeProfileContext: selectedWorkspace?.include_profile_context, includeWorkspaceInstructions: selectedWorkspace?.include_workspace_instructions, embeddingsProvider: selectedWorkspace?.embeddings_provider }) if (!profile) return null if (!selectedWorkspace) return null return ( } renderInputs={() => ( <>
setName(e.target.value)} maxLength={PRESET_NAME_MAX} />
)} /> ) } ================================================ FILE: components/sidebar/items/presets/preset-item.tsx ================================================ import { ModelIcon } from "@/components/models/model-icon" import { ChatSettingsForm } from "@/components/ui/chat-settings-form" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { PRESET_NAME_MAX } from "@/db/limits" import { LLM_LIST } from "@/lib/models/llm/llm-list" import { Tables } from "@/supabase/types" import { FC, useState } from "react" import { SidebarItem } from "../all/sidebar-display-item" interface PresetItemProps { preset: Tables<"presets"> } export const PresetItem: FC = ({ preset }) => { const [name, setName] = useState(preset.name) const [isTyping, setIsTyping] = useState(false) const [description, setDescription] = useState(preset.description) const [presetChatSettings, setPresetChatSettings] = useState({ model: preset.model, prompt: preset.prompt, temperature: preset.temperature, contextLength: preset.context_length, includeProfileContext: preset.include_profile_context, includeWorkspaceInstructions: preset.include_workspace_instructions }) const modelDetails = LLM_LIST.find(model => model.modelId === preset.model) return ( } updateState={{ name, description, include_profile_context: presetChatSettings.includeProfileContext, include_workspace_instructions: presetChatSettings.includeWorkspaceInstructions, context_length: presetChatSettings.contextLength, model: presetChatSettings.model, prompt: presetChatSettings.prompt, temperature: presetChatSettings.temperature }} renderInputs={() => ( <>
setName(e.target.value)} maxLength={PRESET_NAME_MAX} />
)} /> ) } ================================================ FILE: components/sidebar/items/prompts/create-prompt.tsx ================================================ import { SidebarCreateItem } from "@/components/sidebar/items/all/sidebar-create-item" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { TextareaAutosize } from "@/components/ui/textarea-autosize" import { ChatbotUIContext } from "@/context/context" import { PROMPT_NAME_MAX } from "@/db/limits" import { TablesInsert } from "@/supabase/types" import { FC, useContext, useState } from "react" interface CreatePromptProps { isOpen: boolean onOpenChange: (isOpen: boolean) => void } export const CreatePrompt: FC = ({ isOpen, onOpenChange }) => { const { profile, selectedWorkspace } = useContext(ChatbotUIContext) const [isTyping, setIsTyping] = useState(false) const [name, setName] = useState("") const [content, setContent] = useState("") if (!profile) return null if (!selectedWorkspace) return null return ( } renderInputs={() => ( <>
setName(e.target.value)} maxLength={PROMPT_NAME_MAX} onCompositionStart={() => setIsTyping(true)} onCompositionEnd={() => setIsTyping(false)} />
setIsTyping(true)} onCompositionEnd={() => setIsTyping(false)} />
)} /> ) } ================================================ FILE: components/sidebar/items/prompts/prompt-item.tsx ================================================ import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { TextareaAutosize } from "@/components/ui/textarea-autosize" import { PROMPT_NAME_MAX } from "@/db/limits" import { Tables } from "@/supabase/types" import { IconPencil } from "@tabler/icons-react" import { FC, useState } from "react" import { SidebarItem } from "../all/sidebar-display-item" interface PromptItemProps { prompt: Tables<"prompts"> } export const PromptItem: FC = ({ prompt }) => { const [name, setName] = useState(prompt.name) const [content, setContent] = useState(prompt.content) const [isTyping, setIsTyping] = useState(false) return ( } updateState={{ name, content }} renderInputs={() => ( <>
setName(e.target.value)} maxLength={PROMPT_NAME_MAX} onCompositionStart={() => setIsTyping(true)} onCompositionEnd={() => setIsTyping(false)} />
setIsTyping(true)} onCompositionEnd={() => setIsTyping(false)} />
)} /> ) } ================================================ FILE: components/sidebar/items/tools/create-tool.tsx ================================================ import { SidebarCreateItem } from "@/components/sidebar/items/all/sidebar-create-item" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { TextareaAutosize } from "@/components/ui/textarea-autosize" import { ChatbotUIContext } from "@/context/context" import { TOOL_DESCRIPTION_MAX, TOOL_NAME_MAX } from "@/db/limits" import { validateOpenAPI } from "@/lib/openapi-conversion" import { TablesInsert } from "@/supabase/types" import { FC, useContext, useState } from "react" interface CreateToolProps { isOpen: boolean onOpenChange: (isOpen: boolean) => void } export const CreateTool: FC = ({ isOpen, onOpenChange }) => { const { profile, selectedWorkspace } = useContext(ChatbotUIContext) const [name, setName] = useState("") const [isTyping, setIsTyping] = useState(false) const [description, setDescription] = useState("") const [url, setUrl] = useState("") const [customHeaders, setCustomHeaders] = useState("") const [schema, setSchema] = useState("") const [schemaError, setSchemaError] = useState("") if (!profile || !selectedWorkspace) return null return ( } isOpen={isOpen} isTyping={isTyping} renderInputs={() => ( <>
setName(e.target.value)} maxLength={TOOL_NAME_MAX} />
setDescription(e.target.value)} maxLength={TOOL_DESCRIPTION_MAX} />
{/*
setUrl(e.target.value)} />
*/} {/*
*/}
{ setSchema(value) try { const parsedSchema = JSON.parse(value) validateOpenAPI(parsedSchema) .then(() => setSchemaError("")) // Clear error if validation is successful .catch(error => setSchemaError(error.message)) // Set specific validation error message } catch (error) { setSchemaError("Invalid JSON format") // Set error for invalid JSON format } }} minRows={15} />
{schemaError}
)} onOpenChange={onOpenChange} /> ) } ================================================ FILE: components/sidebar/items/tools/tool-item.tsx ================================================ import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { TextareaAutosize } from "@/components/ui/textarea-autosize" import { TOOL_DESCRIPTION_MAX, TOOL_NAME_MAX } from "@/db/limits" import { validateOpenAPI } from "@/lib/openapi-conversion" import { Tables } from "@/supabase/types" import { IconBolt } from "@tabler/icons-react" import { FC, useState } from "react" import { SidebarItem } from "../all/sidebar-display-item" interface ToolItemProps { tool: Tables<"tools"> } export const ToolItem: FC = ({ tool }) => { const [name, setName] = useState(tool.name) const [isTyping, setIsTyping] = useState(false) const [description, setDescription] = useState(tool.description) const [url, setUrl] = useState(tool.url) const [customHeaders, setCustomHeaders] = useState( tool.custom_headers as string ) const [schema, setSchema] = useState(tool.schema as string) const [schemaError, setSchemaError] = useState("") return ( } updateState={{ name, description, url, custom_headers: customHeaders, schema }} renderInputs={() => ( <>
setName(e.target.value)} maxLength={TOOL_NAME_MAX} />
setDescription(e.target.value)} maxLength={TOOL_DESCRIPTION_MAX} />
{/*
setUrl(e.target.value)} />
*/} {/*
*/}
{ setSchema(value) try { const parsedSchema = JSON.parse(value) validateOpenAPI(parsedSchema) .then(() => setSchemaError("")) // Clear error if validation is successful .catch(error => setSchemaError(error.message)) // Set specific validation error message } catch (error) { setSchemaError("Invalid JSON format") // Set error for invalid JSON format } }} minRows={15} />
{schemaError}
)} /> ) } ================================================ FILE: components/sidebar/sidebar-content.tsx ================================================ import { Tables } from "@/supabase/types" import { ContentType, DataListType } from "@/types" import { FC, useState } from "react" import { SidebarCreateButtons } from "./sidebar-create-buttons" import { SidebarDataList } from "./sidebar-data-list" import { SidebarSearch } from "./sidebar-search" interface SidebarContentProps { contentType: ContentType data: DataListType folders: Tables<"folders">[] } export const SidebarContent: FC = ({ contentType, data, folders }) => { const [searchTerm, setSearchTerm] = useState("") const filteredData: any = data.filter(item => item.name.toLowerCase().includes(searchTerm.toLowerCase()) ) return ( // Subtract 50px for the height of the workspace settings
0} />
) } ================================================ FILE: components/sidebar/sidebar-create-buttons.tsx ================================================ import { useChatHandler } from "@/components/chat/chat-hooks/use-chat-handler" import { ChatbotUIContext } from "@/context/context" import { createFolder } from "@/db/folders" import { ContentType } from "@/types" import { IconFolderPlus, IconPlus } from "@tabler/icons-react" import { FC, useContext, useState } from "react" import { Button } from "../ui/button" import { CreateAssistant } from "./items/assistants/create-assistant" import { CreateCollection } from "./items/collections/create-collection" import { CreateFile } from "./items/files/create-file" import { CreateModel } from "./items/models/create-model" import { CreatePreset } from "./items/presets/create-preset" import { CreatePrompt } from "./items/prompts/create-prompt" import { CreateTool } from "./items/tools/create-tool" interface SidebarCreateButtonsProps { contentType: ContentType hasData: boolean } export const SidebarCreateButtons: FC = ({ contentType, hasData }) => { const { profile, selectedWorkspace, folders, setFolders } = useContext(ChatbotUIContext) const { handleNewChat } = useChatHandler() const [isCreatingPrompt, setIsCreatingPrompt] = useState(false) const [isCreatingPreset, setIsCreatingPreset] = useState(false) const [isCreatingFile, setIsCreatingFile] = useState(false) const [isCreatingCollection, setIsCreatingCollection] = useState(false) const [isCreatingAssistant, setIsCreatingAssistant] = useState(false) const [isCreatingTool, setIsCreatingTool] = useState(false) const [isCreatingModel, setIsCreatingModel] = useState(false) const handleCreateFolder = async () => { if (!profile) return if (!selectedWorkspace) return const createdFolder = await createFolder({ user_id: profile.user_id, workspace_id: selectedWorkspace.id, name: "New Folder", description: "", type: contentType }) setFolders([...folders, createdFolder]) } const getCreateFunction = () => { switch (contentType) { case "chats": return async () => { handleNewChat() } case "presets": return async () => { setIsCreatingPreset(true) } case "prompts": return async () => { setIsCreatingPrompt(true) } case "files": return async () => { setIsCreatingFile(true) } case "collections": return async () => { setIsCreatingCollection(true) } case "assistants": return async () => { setIsCreatingAssistant(true) } case "tools": return async () => { setIsCreatingTool(true) } case "models": return async () => { setIsCreatingModel(true) } default: break } } return (
{hasData && ( )} {isCreatingPrompt && ( )} {isCreatingPreset && ( )} {isCreatingFile && ( )} {isCreatingCollection && ( )} {isCreatingAssistant && ( )} {isCreatingTool && ( )} {isCreatingModel && ( )}
) } ================================================ FILE: components/sidebar/sidebar-data-list.tsx ================================================ import { ChatbotUIContext } from "@/context/context" import { updateAssistant } from "@/db/assistants" import { updateChat } from "@/db/chats" import { updateCollection } from "@/db/collections" import { updateFile } from "@/db/files" import { updateModel } from "@/db/models" import { updatePreset } from "@/db/presets" import { updatePrompt } from "@/db/prompts" import { updateTool } from "@/db/tools" import { cn } from "@/lib/utils" import { Tables } from "@/supabase/types" import { ContentType, DataItemType, DataListType } from "@/types" import { FC, useContext, useEffect, useRef, useState } from "react" import { Separator } from "../ui/separator" import { AssistantItem } from "./items/assistants/assistant-item" import { ChatItem } from "./items/chat/chat-item" import { CollectionItem } from "./items/collections/collection-item" import { FileItem } from "./items/files/file-item" import { Folder } from "./items/folders/folder-item" import { ModelItem } from "./items/models/model-item" import { PresetItem } from "./items/presets/preset-item" import { PromptItem } from "./items/prompts/prompt-item" import { ToolItem } from "./items/tools/tool-item" interface SidebarDataListProps { contentType: ContentType data: DataListType folders: Tables<"folders">[] } export const SidebarDataList: FC = ({ contentType, data, folders }) => { const { setChats, setPresets, setPrompts, setFiles, setCollections, setAssistants, setTools, setModels } = useContext(ChatbotUIContext) const divRef = useRef(null) const [isOverflowing, setIsOverflowing] = useState(false) const [isDragOver, setIsDragOver] = useState(false) const getDataListComponent = ( contentType: ContentType, item: DataItemType ) => { switch (contentType) { case "chats": return } /> case "presets": return } /> case "prompts": return } /> case "files": return } /> case "collections": return ( } /> ) case "assistants": return ( } /> ) case "tools": return } /> case "models": return } /> default: return null } } const getSortedData = ( data: any, dateCategory: "Today" | "Yesterday" | "Previous Week" | "Older" ) => { const now = new Date() const todayStart = new Date(now.setHours(0, 0, 0, 0)) const yesterdayStart = new Date( new Date().setDate(todayStart.getDate() - 1) ) const oneWeekAgoStart = new Date( new Date().setDate(todayStart.getDate() - 7) ) return data .filter((item: any) => { const itemDate = new Date(item.updated_at || item.created_at) switch (dateCategory) { case "Today": return itemDate >= todayStart case "Yesterday": return itemDate >= yesterdayStart && itemDate < todayStart case "Previous Week": return itemDate >= oneWeekAgoStart && itemDate < yesterdayStart case "Older": return itemDate < oneWeekAgoStart default: return true } }) .sort( ( a: { updated_at: string; created_at: string }, b: { updated_at: string; created_at: string } ) => new Date(b.updated_at || b.created_at).getTime() - new Date(a.updated_at || a.created_at).getTime() ) } const updateFunctions = { chats: updateChat, presets: updatePreset, prompts: updatePrompt, files: updateFile, collections: updateCollection, assistants: updateAssistant, tools: updateTool, models: updateModel } const stateUpdateFunctions = { chats: setChats, presets: setPresets, prompts: setPrompts, files: setFiles, collections: setCollections, assistants: setAssistants, tools: setTools, models: setModels } const updateFolder = async (itemId: string, folderId: string | null) => { const item: any = data.find(item => item.id === itemId) if (!item) return null const updateFunction = updateFunctions[contentType] const setStateFunction = stateUpdateFunctions[contentType] if (!updateFunction || !setStateFunction) return const updatedItem = await updateFunction(item.id, { folder_id: folderId }) setStateFunction((items: any) => items.map((item: any) => item.id === updatedItem.id ? updatedItem : item ) ) } const handleDragEnter = (e: React.DragEvent) => { e.preventDefault() setIsDragOver(true) } const handleDragLeave = (e: React.DragEvent) => { e.preventDefault() setIsDragOver(false) } const handleDragStart = (e: React.DragEvent, id: string) => { e.dataTransfer.setData("text/plain", id) } const handleDragOver = (e: React.DragEvent) => { e.preventDefault() } const handleDrop = (e: React.DragEvent) => { e.preventDefault() const target = e.target as Element if (!target.closest("#folder")) { const itemId = e.dataTransfer.getData("text/plain") updateFolder(itemId, null) } setIsDragOver(false) } useEffect(() => { if (divRef.current) { setIsOverflowing( divRef.current.scrollHeight > divRef.current.clientHeight ) } }, [data]) const dataWithFolders = data.filter(item => item.folder_id) const dataWithoutFolders = data.filter(item => item.folder_id === null) return ( <>
{data.length === 0 && (
No {contentType}.
)} {(dataWithFolders.length > 0 || dataWithoutFolders.length > 0) && (
{folders.map(folder => ( {dataWithFolders .filter(item => item.folder_id === folder.id) .map(item => (
handleDragStart(e, item.id)} > {getDataListComponent(contentType, item)}
))}
))} {folders.length > 0 && } {contentType === "chats" ? ( <> {["Today", "Yesterday", "Previous Week", "Older"].map( dateCategory => { const sortedData = getSortedData( dataWithoutFolders, dateCategory as | "Today" | "Yesterday" | "Previous Week" | "Older" ) return ( sortedData.length > 0 && (
{dateCategory}
{sortedData.map((item: any) => (
handleDragStart(e, item.id)} > {getDataListComponent(contentType, item)}
))}
) ) } )} ) : (
{dataWithoutFolders.map(item => { return (
handleDragStart(e, item.id)} > {getDataListComponent(contentType, item)}
) })}
)}
)}
) } ================================================ FILE: components/sidebar/sidebar-search.tsx ================================================ import { ContentType } from "@/types" import { FC } from "react" import { Input } from "../ui/input" interface SidebarSearchProps { contentType: ContentType searchTerm: string setSearchTerm: Function } export const SidebarSearch: FC = ({ contentType, searchTerm, setSearchTerm }) => { return ( setSearchTerm(e.target.value)} /> ) } ================================================ FILE: components/sidebar/sidebar-switch-item.tsx ================================================ import { ContentType } from "@/types" import { FC } from "react" import { TabsTrigger } from "../ui/tabs" import { WithTooltip } from "../ui/with-tooltip" interface SidebarSwitchItemProps { contentType: ContentType icon: React.ReactNode onContentTypeChange: (contentType: ContentType) => void } export const SidebarSwitchItem: FC = ({ contentType, icon, onContentTypeChange }) => { return ( {contentType[0].toUpperCase() + contentType.substring(1)}
} trigger={ onContentTypeChange(contentType as ContentType)} > {icon} } /> ) } ================================================ FILE: components/sidebar/sidebar-switcher.tsx ================================================ import { ContentType } from "@/types" import { IconAdjustmentsHorizontal, IconBolt, IconBooks, IconFile, IconMessage, IconPencil, IconRobotFace, IconSparkles } from "@tabler/icons-react" import { FC } from "react" import { TabsList } from "../ui/tabs" import { WithTooltip } from "../ui/with-tooltip" import { ProfileSettings } from "../utility/profile-settings" import { SidebarSwitchItem } from "./sidebar-switch-item" export const SIDEBAR_ICON_SIZE = 28 interface SidebarSwitcherProps { onContentTypeChange: (contentType: ContentType) => void } export const SidebarSwitcher: FC = ({ onContentTypeChange }) => { return (
} contentType="chats" onContentTypeChange={onContentTypeChange} /> } contentType="presets" onContentTypeChange={onContentTypeChange} /> } contentType="prompts" onContentTypeChange={onContentTypeChange} /> } contentType="models" onContentTypeChange={onContentTypeChange} /> } contentType="files" onContentTypeChange={onContentTypeChange} /> } contentType="collections" onContentTypeChange={onContentTypeChange} /> } contentType="assistants" onContentTypeChange={onContentTypeChange} /> } contentType="tools" onContentTypeChange={onContentTypeChange} />
{/* TODO */} {/* Import
} trigger={} /> */} {/* TODO */} {/* */} Profile Settings
} trigger={} /> ) } ================================================ FILE: components/sidebar/sidebar.tsx ================================================ import { ChatbotUIContext } from "@/context/context" import { Tables } from "@/supabase/types" import { ContentType } from "@/types" import { FC, useContext } from "react" import { SIDEBAR_WIDTH } from "../ui/dashboard" import { TabsContent } from "../ui/tabs" import { WorkspaceSwitcher } from "../utility/workspace-switcher" import { WorkspaceSettings } from "../workspace/workspace-settings" import { SidebarContent } from "./sidebar-content" interface SidebarProps { contentType: ContentType showSidebar: boolean } export const Sidebar: FC = ({ contentType, showSidebar }) => { const { folders, chats, presets, prompts, files, collections, assistants, tools, models } = useContext(ChatbotUIContext) const chatFolders = folders.filter(folder => folder.type === "chats") const presetFolders = folders.filter(folder => folder.type === "presets") const promptFolders = folders.filter(folder => folder.type === "prompts") const filesFolders = folders.filter(folder => folder.type === "files") const collectionFolders = folders.filter( folder => folder.type === "collections" ) const assistantFolders = folders.filter( folder => folder.type === "assistants" ) const toolFolders = folders.filter(folder => folder.type === "tools") const modelFolders = folders.filter(folder => folder.type === "models") const renderSidebarContent = ( contentType: ContentType, data: any[], folders: Tables<"folders">[] ) => { return ( ) } return (
{(() => { switch (contentType) { case "chats": return renderSidebarContent("chats", chats, chatFolders) case "presets": return renderSidebarContent("presets", presets, presetFolders) case "prompts": return renderSidebarContent("prompts", prompts, promptFolders) case "files": return renderSidebarContent("files", files, filesFolders) case "collections": return renderSidebarContent( "collections", collections, collectionFolders ) case "assistants": return renderSidebarContent( "assistants", assistants, assistantFolders ) case "tools": return renderSidebarContent("tools", tools, toolFolders) case "models": return renderSidebarContent("models", models, modelFolders) default: return null } })()}
) } ================================================ FILE: components/ui/accordion.tsx ================================================ "use client" import * as React from "react" import * as AccordionPrimitive from "@radix-ui/react-accordion" import { ChevronDown } from "lucide-react" import { cn } from "@/lib/utils" const Accordion = AccordionPrimitive.Root const AccordionItem = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) AccordionItem.displayName = "AccordionItem" const AccordionTrigger = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, children, ...props }, ref) => ( svg]:rotate-180", className )} {...props} > {children} )) AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName const AccordionContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, children, ...props }, ref) => (
{children}
)) AccordionContent.displayName = AccordionPrimitive.Content.displayName export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } ================================================ FILE: components/ui/advanced-settings.tsx ================================================ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible" import { IconChevronDown, IconChevronRight } from "@tabler/icons-react" import { FC, useState } from "react" interface AdvancedSettingsProps { children: React.ReactNode } export const AdvancedSettings: FC = ({ children }) => { const [isOpen, setIsOpen] = useState( false // localStorage.getItem("advanced-settings-open") === "true" ) const handleOpenChange = (isOpen: boolean) => { setIsOpen(isOpen) // localStorage.setItem("advanced-settings-open", String(isOpen)) } return (
Advanced Settings
{isOpen ? ( ) : ( )}
{children}
) } ================================================ FILE: components/ui/alert-dialog.tsx ================================================ "use client" import * as React from "react" import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" import { cn } from "@/lib/utils" import { buttonVariants } from "@/components/ui/button" const AlertDialog = AlertDialogPrimitive.Root const AlertDialogTrigger = AlertDialogPrimitive.Trigger const AlertDialogPortal = AlertDialogPrimitive.Portal const AlertDialogOverlay = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName const AlertDialogContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes) => (
) AlertDialogHeader.displayName = "AlertDialogHeader" const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes) => (
) AlertDialogFooter.displayName = "AlertDialogFooter" const AlertDialogTitle = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName const AlertDialogDescription = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName const AlertDialogAction = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName const AlertDialogCancel = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName export { AlertDialog, AlertDialogPortal, AlertDialogOverlay, AlertDialogTrigger, AlertDialogContent, AlertDialogHeader, AlertDialogFooter, AlertDialogTitle, AlertDialogDescription, AlertDialogAction, AlertDialogCancel } ================================================ FILE: components/ui/alert.tsx ================================================ import * as React from "react" import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" const alertVariants = cva( "[&>svg]:text-foreground relative w-full rounded-lg border p-4 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg~*]:pl-7", { variants: { variant: { default: "bg-background text-foreground", destructive: "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive" } }, defaultVariants: { variant: "default" } } ) const Alert = React.forwardRef< HTMLDivElement, React.HTMLAttributes & VariantProps >(({ className, variant, ...props }, ref) => (
)) Alert.displayName = "Alert" const AlertTitle = React.forwardRef< HTMLParagraphElement, React.HTMLAttributes >(({ className, ...props }, ref) => (
)) AlertTitle.displayName = "AlertTitle" const AlertDescription = React.forwardRef< HTMLParagraphElement, React.HTMLAttributes >(({ className, ...props }, ref) => (
)) AlertDescription.displayName = "AlertDescription" export { Alert, AlertTitle, AlertDescription } ================================================ FILE: components/ui/aspect-ratio.tsx ================================================ "use client" import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" const AspectRatio = AspectRatioPrimitive.Root export { AspectRatio } ================================================ FILE: components/ui/avatar.tsx ================================================ "use client" import * as React from "react" import * as AvatarPrimitive from "@radix-ui/react-avatar" import { cn } from "@/lib/utils" const Avatar = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) Avatar.displayName = AvatarPrimitive.Root.displayName const AvatarImage = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) AvatarImage.displayName = AvatarPrimitive.Image.displayName const AvatarFallback = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName export { Avatar, AvatarImage, AvatarFallback } ================================================ FILE: components/ui/badge.tsx ================================================ import * as React from "react" import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" const badgeVariants = cva( "focus:ring-ring inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2", { variants: { variant: { default: "bg-primary text-primary-foreground hover:bg-primary/80 border-transparent", secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 border-transparent", destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/80 border-transparent", outline: "text-foreground" } }, defaultVariants: { variant: "default" } } ) export interface BadgeProps extends React.HTMLAttributes, VariantProps {} function Badge({ className, variant, ...props }: BadgeProps) { return (
) } export { Badge, badgeVariants } ================================================ FILE: components/ui/brand.tsx ================================================ "use client" import Link from "next/link" import { FC } from "react" import { ChatbotUISVG } from "../icons/chatbotui-svg" interface BrandProps { theme?: "dark" | "light" } export const Brand: FC = ({ theme = "dark" }) => { return (
Chatbot UI
) } ================================================ FILE: components/ui/button.tsx ================================================ import { Slot } from "@radix-ui/react-slot" import { cva, type VariantProps } from "class-variance-authority" import * as React from "react" import { cn } from "@/lib/utils" const buttonVariants = cva( "ring-offset-background focus-visible:ring-ring inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors hover:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", { variants: { variant: { default: "bg-primary text-primary-foreground hover:bg-primary/90", destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", outline: "border-input bg-background hover:bg-accent hover:text-accent-foreground border", secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground", link: "text-primary underline-offset-4 hover:underline" }, size: { default: "h-10 px-4 py-2", sm: "h-9 rounded-md px-3", lg: "h-11 rounded-md px-8", icon: "size-10" } }, defaultVariants: { variant: "default", size: "default" } } ) export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { asChild?: boolean } const Button = React.forwardRef( ({ className, variant, size, asChild = false, ...props }, ref) => { const Comp = asChild ? Slot : "button" return ( ) } ) Button.displayName = "Button" export { Button, buttonVariants } ================================================ FILE: components/ui/calendar.tsx ================================================ "use client" import * as React from "react" import { ChevronLeft, ChevronRight } from "lucide-react" import { DayPicker } from "react-day-picker" import { cn } from "@/lib/utils" import { buttonVariants } from "@/components/ui/button" export type CalendarProps = React.ComponentProps function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) { return ( , IconRight: ({ ...props }) => }} {...props} /> ) } Calendar.displayName = "Calendar" export { Calendar } ================================================ FILE: components/ui/card.tsx ================================================ import * as React from "react" import { cn } from "@/lib/utils" const Card = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => (
)) Card.displayName = "Card" const CardHeader = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => (
)) CardHeader.displayName = "CardHeader" const CardTitle = React.forwardRef< HTMLParagraphElement, React.HTMLAttributes >(({ className, ...props }, ref) => (

)) CardTitle.displayName = "CardTitle" const CardDescription = React.forwardRef< HTMLParagraphElement, React.HTMLAttributes >(({ className, ...props }, ref) => (

)) CardDescription.displayName = "CardDescription" const CardContent = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => (

)) CardContent.displayName = "CardContent" const CardFooter = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => (
)) CardFooter.displayName = "CardFooter" export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } ================================================ FILE: components/ui/chat-settings-form.tsx ================================================ "use client" import { ChatbotUIContext } from "@/context/context" import { CHAT_SETTING_LIMITS } from "@/lib/chat-setting-limits" import { ChatSettings } from "@/types" import { IconInfoCircle } from "@tabler/icons-react" import { FC, useContext } from "react" import { ModelSelect } from "../models/model-select" import { AdvancedSettings } from "./advanced-settings" import { Checkbox } from "./checkbox" import { Label } from "./label" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select" import { Slider } from "./slider" import { TextareaAutosize } from "./textarea-autosize" import { WithTooltip } from "./with-tooltip" interface ChatSettingsFormProps { chatSettings: ChatSettings onChangeChatSettings: (value: ChatSettings) => void useAdvancedDropdown?: boolean showTooltip?: boolean } export const ChatSettingsForm: FC = ({ chatSettings, onChangeChatSettings, useAdvancedDropdown = true, showTooltip = true }) => { const { profile, models } = useContext(ChatbotUIContext) if (!profile) return null return (
{ onChangeChatSettings({ ...chatSettings, model }) }} />
{ onChangeChatSettings({ ...chatSettings, prompt }) }} value={chatSettings.prompt} minRows={3} maxRows={6} />
{useAdvancedDropdown ? ( ) : (
)}
) } interface AdvancedContentProps { chatSettings: ChatSettings onChangeChatSettings: (value: ChatSettings) => void showTooltip: boolean } const AdvancedContent: FC = ({ chatSettings, onChangeChatSettings, showTooltip }) => { const { profile, selectedWorkspace, availableOpenRouterModels, models } = useContext(ChatbotUIContext) const isCustomModel = models.some( model => model.model_id === chatSettings.model ) function findOpenRouterModel(modelId: string) { return availableOpenRouterModels.find(model => model.modelId === modelId) } const MODEL_LIMITS = CHAT_SETTING_LIMITS[chatSettings.model] || { MIN_TEMPERATURE: 0, MAX_TEMPERATURE: 1, MAX_CONTEXT_LENGTH: findOpenRouterModel(chatSettings.model)?.maxContext || 4096 } return (
{ onChangeChatSettings({ ...chatSettings, temperature: temperature[0] }) }} min={MODEL_LIMITS.MIN_TEMPERATURE} max={MODEL_LIMITS.MAX_TEMPERATURE} step={0.01} />
{ onChangeChatSettings({ ...chatSettings, contextLength: contextLength[0] }) }} min={0} max={ isCustomModel ? models.find(model => model.model_id === chatSettings.model) ?.context_length : MODEL_LIMITS.MAX_CONTEXT_LENGTH } step={1} />
onChangeChatSettings({ ...chatSettings, includeProfileContext: value }) } /> {showTooltip && ( {profile?.profile_context || "No profile context."}
} trigger={ } /> )}
onChangeChatSettings({ ...chatSettings, includeWorkspaceInstructions: value }) } /> {showTooltip && ( {selectedWorkspace?.instructions || "No workspace instructions."}
} trigger={ } /> )}
) } ================================================ FILE: components/ui/checkbox.tsx ================================================ "use client" import * as React from "react" import * as CheckboxPrimitive from "@radix-ui/react-checkbox" import { Check } from "lucide-react" import { cn } from "@/lib/utils" const Checkbox = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) Checkbox.displayName = CheckboxPrimitive.Root.displayName export { Checkbox } ================================================ FILE: components/ui/collapsible.tsx ================================================ "use client" import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" const Collapsible = CollapsiblePrimitive.Root const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent export { Collapsible, CollapsibleTrigger, CollapsibleContent } ================================================ FILE: components/ui/command.tsx ================================================ "use client" import * as React from "react" import { type DialogProps } from "@radix-ui/react-dialog" import { Command as CommandPrimitive } from "cmdk" import { Search } from "lucide-react" import { cn } from "@/lib/utils" import { Dialog, DialogContent } from "@/components/ui/dialog" const Command = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) Command.displayName = CommandPrimitive.displayName interface CommandDialogProps extends DialogProps {} const CommandDialog = ({ children, ...props }: CommandDialogProps) => { return ( {children} ) } const CommandInput = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => (
)) CommandInput.displayName = CommandPrimitive.Input.displayName const CommandList = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) CommandList.displayName = CommandPrimitive.List.displayName const CommandEmpty = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >((props, ref) => ( )) CommandEmpty.displayName = CommandPrimitive.Empty.displayName const CommandGroup = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) CommandGroup.displayName = CommandPrimitive.Group.displayName const CommandSeparator = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) CommandSeparator.displayName = CommandPrimitive.Separator.displayName const CommandItem = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) CommandItem.displayName = CommandPrimitive.Item.displayName const CommandShortcut = ({ className, ...props }: React.HTMLAttributes) => { return ( ) } CommandShortcut.displayName = "CommandShortcut" export { Command, CommandDialog, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandShortcut, CommandSeparator } ================================================ FILE: components/ui/context-menu.tsx ================================================ "use client" import * as React from "react" import * as ContextMenuPrimitive from "@radix-ui/react-context-menu" import { Check, ChevronRight, Circle } from "lucide-react" import { cn } from "@/lib/utils" const ContextMenu = ContextMenuPrimitive.Root const ContextMenuTrigger = ContextMenuPrimitive.Trigger const ContextMenuGroup = ContextMenuPrimitive.Group const ContextMenuPortal = ContextMenuPrimitive.Portal const ContextMenuSub = ContextMenuPrimitive.Sub const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup const ContextMenuSubTrigger = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { inset?: boolean } >(({ className, inset, children, ...props }, ref) => ( {children} )) ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName const ContextMenuSubContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName const ContextMenuContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName const ContextMenuItem = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { inset?: boolean } >(({ className, inset, ...props }, ref) => ( )) ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName const ContextMenuCheckboxItem = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, children, checked, ...props }, ref) => ( {children} )) ContextMenuCheckboxItem.displayName = ContextMenuPrimitive.CheckboxItem.displayName const ContextMenuRadioItem = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, children, ...props }, ref) => ( {children} )) ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName const ContextMenuLabel = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { inset?: boolean } >(({ className, inset, ...props }, ref) => ( )) ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName const ContextMenuSeparator = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName const ContextMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => { return ( ) } ContextMenuShortcut.displayName = "ContextMenuShortcut" export { ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuItem, ContextMenuCheckboxItem, ContextMenuRadioItem, ContextMenuLabel, ContextMenuSeparator, ContextMenuShortcut, ContextMenuGroup, ContextMenuPortal, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuRadioGroup } ================================================ FILE: components/ui/dashboard.tsx ================================================ "use client" import { Sidebar } from "@/components/sidebar/sidebar" import { SidebarSwitcher } from "@/components/sidebar/sidebar-switcher" import { Button } from "@/components/ui/button" import { Tabs } from "@/components/ui/tabs" import useHotkey from "@/lib/hooks/use-hotkey" import { cn } from "@/lib/utils" import { ContentType } from "@/types" import { IconChevronCompactRight } from "@tabler/icons-react" import { usePathname, useRouter, useSearchParams } from "next/navigation" import { FC, useState } from "react" import { useSelectFileHandler } from "../chat/chat-hooks/use-select-file-handler" import { CommandK } from "../utility/command-k" export const SIDEBAR_WIDTH = 350 interface DashboardProps { children: React.ReactNode } export const Dashboard: FC = ({ children }) => { useHotkey("s", () => setShowSidebar(prevState => !prevState)) const pathname = usePathname() const router = useRouter() const searchParams = useSearchParams() const tabValue = searchParams.get("tab") || "chats" const { handleSelectDeviceFile } = useSelectFileHandler() const [contentType, setContentType] = useState( tabValue as ContentType ) const [showSidebar, setShowSidebar] = useState( localStorage.getItem("showSidebar") === "true" ) const [isDragging, setIsDragging] = useState(false) const onFileDrop = (event: React.DragEvent) => { event.preventDefault() const files = event.dataTransfer.files const file = files[0] handleSelectDeviceFile(file) setIsDragging(false) } const handleDragEnter = (event: React.DragEvent) => { event.preventDefault() setIsDragging(true) } const handleDragLeave = (event: React.DragEvent) => { event.preventDefault() setIsDragging(false) } const onDragOver = (event: React.DragEvent) => { event.preventDefault() } const handleToggleSidebar = () => { setShowSidebar(prevState => !prevState) localStorage.setItem("showSidebar", String(!showSidebar)) } return (
{showSidebar && ( { setContentType(tabValue as ContentType) router.replace(`${pathname}?tab=${tabValue}`) }} > )}
{isDragging ? (
drop file here
) : ( children )}
) } ================================================ FILE: components/ui/dialog.tsx ================================================ "use client" import * as DialogPrimitive from "@radix-ui/react-dialog" import * as React from "react" import { cn } from "@/lib/utils" const Dialog = DialogPrimitive.Root const DialogTrigger = DialogPrimitive.Trigger const DialogPortal = DialogPrimitive.Portal const DialogClose = DialogPrimitive.Close const DialogOverlay = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) DialogOverlay.displayName = DialogPrimitive.Overlay.displayName const DialogContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, children, ...props }, ref) => ( {children} {/* Close */} )) DialogContent.displayName = DialogPrimitive.Content.displayName const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => (
) DialogHeader.displayName = "DialogHeader" const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => (
) DialogFooter.displayName = "DialogFooter" const DialogTitle = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) DialogTitle.displayName = DialogPrimitive.Title.displayName const DialogDescription = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) DialogDescription.displayName = DialogPrimitive.Description.displayName export { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger } ================================================ FILE: components/ui/dropdown-menu.tsx ================================================ "use client" import * as React from "react" import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" import { Check, ChevronRight, Circle } from "lucide-react" import { cn } from "@/lib/utils" const DropdownMenu = DropdownMenuPrimitive.Root const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger const DropdownMenuGroup = DropdownMenuPrimitive.Group const DropdownMenuPortal = DropdownMenuPrimitive.Portal const DropdownMenuSub = DropdownMenuPrimitive.Sub const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup const DropdownMenuSubTrigger = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { inset?: boolean } >(({ className, inset, children, ...props }, ref) => ( {children} )) DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName const DropdownMenuSubContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName const DropdownMenuContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, sideOffset = 4, ...props }, ref) => ( )) DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName const DropdownMenuItem = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { inset?: boolean } >(({ className, inset, ...props }, ref) => ( )) DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName const DropdownMenuCheckboxItem = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, children, checked, ...props }, ref) => ( {children} )) DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName const DropdownMenuRadioItem = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, children, ...props }, ref) => ( {children} )) DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName const DropdownMenuLabel = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { inset?: boolean } >(({ className, inset, ...props }, ref) => ( )) DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName const DropdownMenuSeparator = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => { return ( ) } DropdownMenuShortcut.displayName = "DropdownMenuShortcut" export { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuGroup, DropdownMenuPortal, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuRadioGroup } ================================================ FILE: components/ui/file-icon.tsx ================================================ import { IconFile, IconFileText, IconFileTypeCsv, IconFileTypeDocx, IconFileTypePdf, IconJson, IconMarkdown, IconPhoto } from "@tabler/icons-react" import { FC } from "react" interface FileIconProps { type: string size?: number } export const FileIcon: FC = ({ type, size = 32 }) => { if (type.includes("image")) { return } else if (type.includes("pdf")) { return } else if (type.includes("csv")) { return } else if (type.includes("docx")) { return } else if (type.includes("plain")) { return } else if (type.includes("json")) { return } else if (type.includes("markdown")) { return } else { return } } ================================================ FILE: components/ui/file-preview.tsx ================================================ import { cn } from "@/lib/utils" import { Tables } from "@/supabase/types" import { ChatFile, MessageImage } from "@/types" import { IconFileFilled } from "@tabler/icons-react" import Image from "next/image" import { FC } from "react" import { DrawingCanvas } from "../utility/drawing-canvas" import { Dialog, DialogContent } from "./dialog" interface FilePreviewProps { type: "image" | "file" | "file_item" item: ChatFile | MessageImage | Tables<"file_items"> isOpen: boolean onOpenChange: (isOpen: boolean) => void } export const FilePreview: FC = ({ type, item, isOpen, onOpenChange }) => { return ( {(() => { if (type === "image") { const imageItem = item as MessageImage return imageItem.file ? ( ) : ( File image ) } else if (type === "file_item") { const fileItem = item as Tables<"file_items"> return (
{fileItem.content}
) } else if (type === "file") { return (
) } })()}
) } ================================================ FILE: components/ui/form.tsx ================================================ import * as React from "react" import * as LabelPrimitive from "@radix-ui/react-label" import { Slot } from "@radix-ui/react-slot" import { Controller, ControllerProps, FieldPath, FieldValues, FormProvider, useFormContext } from "react-hook-form" import { cn } from "@/lib/utils" import { Label } from "@/components/ui/label" const Form = FormProvider type FormFieldContextValue< TFieldValues extends FieldValues = FieldValues, TName extends FieldPath = FieldPath > = { name: TName } const FormFieldContext = React.createContext( {} as FormFieldContextValue ) const FormField = < TFieldValues extends FieldValues = FieldValues, TName extends FieldPath = FieldPath >({ ...props }: ControllerProps) => { return ( ) } const useFormField = () => { const fieldContext = React.useContext(FormFieldContext) const itemContext = React.useContext(FormItemContext) const { getFieldState, formState } = useFormContext() const fieldState = getFieldState(fieldContext.name, formState) if (!fieldContext) { throw new Error("useFormField should be used within ") } const { id } = itemContext return { id, name: fieldContext.name, formItemId: `${id}-form-item`, formDescriptionId: `${id}-form-item-description`, formMessageId: `${id}-form-item-message`, ...fieldState } } type FormItemContextValue = { id: string } const FormItemContext = React.createContext( {} as FormItemContextValue ) const FormItem = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => { const id = React.useId() return (
) }) FormItem.displayName = "FormItem" const FormLabel = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => { const { error, formItemId } = useFormField() return (