├── .env.local.example
├── .eslintrc.json
├── .github
└── funding.yaml
├── .gitignore
├── .husky
└── pre-commit
├── .nvmrc
├── README.md
├── __tests__
├── lib
│ └── openapi-conversion.test.ts
└── playwright-test
│ ├── .gitignore
│ ├── package-lock.json
│ ├── 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.json
├── 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
├── 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-lock.json
├── package.json
├── postcss.config.js
├── prettier.config.cjs
├── public
├── DARK_BRAND_LOGO.png
├── LIGHT_BRAND_LOGO.png
├── favicon.ico
├── icon-192x192.png
├── icon-256x256.png
├── icon-512x512.png
├── locales
│ ├── de
│ │ └── translation.json
│ └── en
│ │ └── translation.json
├── manifest.json
├── providers
│ ├── groq.png
│ ├── meta.png
│ ├── mistral.png
│ └── perplexity.png
├── readme
│ └── screenshot.png
└── 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
/.env.local.example:
--------------------------------------------------------------------------------
1 | # Supabase Public
2 | NEXT_PUBLIC_SUPABASE_URL=
3 | NEXT_PUBLIC_SUPABASE_ANON_KEY=
4 |
5 | # Supabase Private
6 | SUPABASE_SERVICE_ROLE_KEY=
7 |
8 | # Ollama
9 | NEXT_PUBLIC_OLLAMA_URL=http://localhost:11434
10 |
11 | # API Keys (Optional: Entering an API key here overrides the API keys globally for all users.)
12 | OPENAI_API_KEY=
13 | ANTHROPIC_API_KEY=
14 | GOOGLE_GEMINI_API_KEY=
15 | MISTRAL_API_KEY=
16 | GROQ_API_KEY=
17 | PERPLEXITY_API_KEY=
18 | OPENROUTER_API_KEY=
19 |
20 | # OpenAI API Information
21 | NEXT_PUBLIC_OPENAI_ORGANIZATION_ID=
22 |
23 | # Azure API Information
24 | AZURE_OPENAI_API_KEY=
25 | AZURE_OPENAI_ENDPOINT=
26 | AZURE_GPT_35_TURBO_NAME=
27 | AZURE_GPT_45_VISION_NAME=
28 | AZURE_GPT_45_TURBO_NAME=
29 | AZURE_EMBEDDINGS_NAME=
30 |
31 | # General Configuration (Optional)
32 | EMAIL_DOMAIN_WHITELIST=
33 | EMAIL_WHITELIST=
34 |
35 | # File size limit for uploads in bytes
36 | NEXT_PUBLIC_USER_FILE_SIZE_LIMIT=10485760
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/eslintrc",
3 | "root": true,
4 | "extends": [
5 | "next/core-web-vitals",
6 | "prettier",
7 | "plugin:tailwindcss/recommended"
8 | ],
9 | "plugins": ["tailwindcss"],
10 | "rules": {
11 | "tailwindcss/no-custom-classname": "off"
12 | },
13 | "settings": {
14 | "tailwindcss": {
15 | "callees": ["cn", "cva"],
16 | "config": "tailwind.config.js"
17 | }
18 | },
19 | "overrides": [
20 | {
21 | "files": ["*.ts", "*.tsx"],
22 | "parser": "@typescript-eslint/parser"
23 | }
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/.github/funding.yaml:
--------------------------------------------------------------------------------
1 | # If you find my open-source work helpful, please consider sponsoring me!
2 |
3 | github: mckaywrigley
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env
30 | .env*.local
31 |
32 | # vercel
33 | .vercel
34 |
35 | # typescript
36 | *.tsbuildinfo
37 | next-env.d.ts
38 |
39 | .VSCodeCounter
40 | tool-schemas
41 | custom-prompts
42 |
43 | sw.js
44 | sw.js.map
45 | workbox-*.js
46 | workbox-*.js.map
47 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | . "$(dirname -- "$0")/_/husky.sh"
4 |
5 | npm run lint:fix && npm run format:write && git add .
6 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v20.11.0
2 |
--------------------------------------------------------------------------------
/__tests__/playwright-test/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | /test-results/
3 | /playwright-report/
4 | /blob-report/
5 | /playwright/.cache/
6 |
--------------------------------------------------------------------------------
/__tests__/playwright-test/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "playwright-test",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "integration": "playwright test",
8 | "integration:open": "playwright test --ui",
9 | "integration:codegen": "playwright codegen"
10 | },
11 | "keywords": [],
12 | "author": "",
13 | "license": "ISC",
14 | "devDependencies": {
15 | "@playwright/test": "^1.41.2",
16 | "@types/node": "^20.11.20"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/__tests__/playwright-test/playwright.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig, devices } from '@playwright/test';
2 |
3 | /**
4 | * Read environment variables from file.
5 | * https://github.com/motdotla/dotenv
6 | */
7 | // require('dotenv').config();
8 |
9 | /**
10 | * See https://playwright.dev/docs/test-configuration.
11 | */
12 | export default defineConfig({
13 | testDir: './tests',
14 | /* Run tests in files in parallel */
15 | fullyParallel: true,
16 | /* Fail the build on CI if you accidentally left test.only in the source code. */
17 | forbidOnly: !!process.env.CI,
18 | /* Retry on CI only */
19 | retries: process.env.CI ? 2 : 0,
20 | /* Opt out of parallel tests on CI. */
21 | workers: process.env.CI ? 1 : undefined,
22 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */
23 | reporter: 'html',
24 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
25 | use: {
26 | /* Base URL to use in actions like `await page.goto('/')`. */
27 | // baseURL: 'http://127.0.0.1:3000',
28 |
29 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
30 | trace: 'on-first-retry',
31 | },
32 |
33 | /* Configure projects for major browsers */
34 | projects: [
35 | {
36 | name: 'chromium',
37 | use: { ...devices['Desktop Chrome'] },
38 | },
39 |
40 | {
41 | name: 'firefox',
42 | use: { ...devices['Desktop Firefox'] },
43 | },
44 |
45 | {
46 | name: 'webkit',
47 | use: { ...devices['Desktop Safari'] },
48 | },
49 |
50 | /* Test against mobile viewports. */
51 | // {
52 | // name: 'Mobile Chrome',
53 | // use: { ...devices['Pixel 5'] },
54 | // },
55 | // {
56 | // name: 'Mobile Safari',
57 | // use: { ...devices['iPhone 12'] },
58 | // },
59 |
60 | /* Test against branded browsers. */
61 | // {
62 | // name: 'Microsoft Edge',
63 | // use: { ...devices['Desktop Edge'], channel: 'msedge' },
64 | // },
65 | // {
66 | // name: 'Google Chrome',
67 | // use: { ...devices['Desktop Chrome'], channel: 'chrome' },
68 | // },
69 | ],
70 |
71 | /* Run your local dev server before starting the tests */
72 | // webServer: {
73 | // command: 'npm run start',
74 | // url: 'http://127.0.0.1:3000',
75 | // reuseExistingServer: !process.env.CI,
76 | // },
77 | });
78 |
--------------------------------------------------------------------------------
/__tests__/playwright-test/tests/login.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from '@playwright/test';
2 |
3 | test('start chatting is displayed', async ({ page }) => {
4 | await page.goto('http://localhost:3000/');
5 |
6 | //expect the start chatting link to be visible
7 | await expect (page.getByRole('link', { name: 'Start Chatting' })).toBeVisible();
8 | });
9 |
10 | test('No password error message', async ({ page }) => {
11 | await page.goto('http://localhost:3000/login');
12 | //fill in dummy email
13 | await page.getByPlaceholder('you@example.com').fill('dummyemail@gmail.com');
14 | await page.getByRole('button', { name: 'Login' }).click();
15 | //wait for netwrok to be idle
16 | await page.waitForLoadState('networkidle');
17 | //validate that correct message is shown to the user
18 | await expect(page.getByText('Invalid login credentials')).toBeVisible();
19 |
20 | });
21 | test('No password for signup', async ({ page }) => {
22 | await page.goto('http://localhost:3000/login');
23 |
24 | await page.getByPlaceholder('you@example.com').fill('dummyEmail@Gmail.com');
25 | await page.getByRole('button', { name: 'Sign Up' }).click();
26 | //validate appropriate error is thrown for missing password when signing up
27 | await expect(page.getByText('Signup requires a valid')).toBeVisible();
28 | });
29 | test('invalid username for signup', async ({ page }) => {
30 | await page.goto('http://localhost:3000/login');
31 |
32 | await page.getByPlaceholder('you@example.com').fill('dummyEmail');
33 | await page.getByPlaceholder('••••••••').fill('dummypassword');
34 | await page.getByRole('button', { name: 'Sign Up' }).click();
35 | //validate appropriate error is thrown for invalid username when signing up
36 | await expect(page.getByText('Unable to validate email')).toBeVisible();
37 | });
38 | test('password reset message', async ({ page }) => {
39 | await page.goto('http://localhost:3000/login');
40 | await page.getByPlaceholder('you@example.com').fill('demo@gmail.com');
41 | await page.getByRole('button', { name: 'Reset' }).click();
42 | //validate appropriate message is shown
43 | await expect(page.getByText('Check email to reset password')).toBeVisible();
44 | });
45 |
46 | //more tests can be added here
--------------------------------------------------------------------------------
/app/[locale]/[workspaceid]/chat/[chatid]/page.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { ChatUI } from "@/components/chat/chat-ui"
4 |
5 | export default function ChatIDPage() {
6 | return
7 | }
8 |
--------------------------------------------------------------------------------
/app/[locale]/[workspaceid]/chat/page.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { ChatHelp } from "@/components/chat/chat-help"
4 | import { useChatHandler } from "@/components/chat/chat-hooks/use-chat-handler"
5 | import { ChatInput } from "@/components/chat/chat-input"
6 | import { ChatSettings } from "@/components/chat/chat-settings"
7 | import { ChatUI } from "@/components/chat/chat-ui"
8 | import { QuickSettings } from "@/components/chat/quick-settings"
9 | import { Brand } from "@/components/ui/brand"
10 | import { ChatbotUIContext } from "@/context/context"
11 | import useHotkey from "@/lib/hooks/use-hotkey"
12 | import { useTheme } from "next-themes"
13 | import { useContext } from "react"
14 |
15 | export default function ChatPage() {
16 | useHotkey("o", () => handleNewChat())
17 | useHotkey("l", () => {
18 | handleFocusChatInput()
19 | })
20 |
21 | const { chatMessages } = useContext(ChatbotUIContext)
22 |
23 | const { handleNewChat, handleFocusChatInput } = useChatHandler()
24 |
25 | const { theme } = useTheme()
26 |
27 | return (
28 | <>
29 | {chatMessages.length === 0 ? (
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | ) : (
54 |
55 | )}
56 | >
57 | )
58 | }
59 |
--------------------------------------------------------------------------------
/app/[locale]/[workspaceid]/page.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { ChatbotUIContext } from "@/context/context"
4 | import { useContext } from "react"
5 |
6 | export default function WorkspacePage() {
7 | const { selectedWorkspace } = useContext(ChatbotUIContext)
8 |
9 | return (
10 |
11 |
{selectedWorkspace?.name}
12 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/app/[locale]/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | ::-webkit-scrollbar-track {
6 | background-color: transparent;
7 | }
8 |
9 | ::-webkit-scrollbar-thumb {
10 | background-color: #ccc;
11 | border-radius: 10px;
12 | }
13 |
14 | ::-webkit-scrollbar-thumb:hover {
15 | background-color: #aaa;
16 | }
17 |
18 | ::-webkit-scrollbar-track:hover {
19 | background-color: #f2f2f2;
20 | }
21 |
22 | ::-webkit-scrollbar-corner {
23 | background-color: transparent;
24 | }
25 |
26 | ::-webkit-scrollbar {
27 | width: 6px;
28 | height: 6px;
29 | }
30 |
31 | @layer base {
32 | :root {
33 | --background: 0 0% 100%;
34 | --foreground: 0 0% 3.9%;
35 |
36 | --muted: 0 0% 96.1%;
37 | --muted-foreground: 0 0% 45.1%;
38 |
39 | --popover: 0 0% 100%;
40 | --popover-foreground: 0 0% 3.9%;
41 |
42 | --card: 0 0% 100%;
43 | --card-foreground: 0 0% 3.9%;
44 |
45 | --border: 0 0% 89.8%;
46 | --input: 0 0% 89.8%;
47 |
48 | --primary: 0 0% 9%;
49 | --primary-foreground: 0 0% 98%;
50 |
51 | --secondary: 0 0% 96.1%;
52 | --secondary-foreground: 0 0% 9%;
53 |
54 | --accent: 0 0% 96.1%;
55 | --accent-foreground: 0 0% 9%;
56 |
57 | --destructive: 0 84.2% 60.2%;
58 | --destructive-foreground: 0 0% 98%;
59 |
60 | --ring: 0 0% 63.9%;
61 |
62 | --radius: 0.5rem;
63 | }
64 |
65 | .dark {
66 | --background: 0 0% 3.9%;
67 | --foreground: 0 0% 98%;
68 |
69 | --muted: 0 0% 14.9%;
70 | --muted-foreground: 0 0% 63.9%;
71 |
72 | --popover: 0 0% 3.9%;
73 | --popover-foreground: 0 0% 98%;
74 |
75 | --card: 0 0% 3.9%;
76 | --card-foreground: 0 0% 98%;
77 |
78 | --border: 0 0% 14.9%;
79 | --input: 0 0% 14.9%;
80 |
81 | --primary: 0 0% 98%;
82 | --primary-foreground: 0 0% 9%;
83 |
84 | --secondary: 0 0% 14.9%;
85 | --secondary-foreground: 0 0% 98%;
86 |
87 | --accent: 0 0% 14.9%;
88 | --accent-foreground: 0 0% 98%;
89 |
90 | --destructive: 0 62.8% 30.6%;
91 | --destructive-foreground: 0 85.7% 97.3%;
92 |
93 | --ring: 0 0% 14.9%;
94 | }
95 | }
96 |
97 | @layer base {
98 | * {
99 | @apply border-border;
100 | }
101 | body {
102 | @apply bg-background text-foreground;
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/app/[locale]/help/page.tsx:
--------------------------------------------------------------------------------
1 | export default function HelpPage() {
2 | return (
3 |
4 |
Help under construction.
5 |
6 | )
7 | }
8 |
--------------------------------------------------------------------------------
/app/[locale]/loading.tsx:
--------------------------------------------------------------------------------
1 | import { IconLoader2 } from "@tabler/icons-react"
2 |
3 | export default function Loading() {
4 | return (
5 |
6 |
7 |
8 | )
9 | }
10 |
--------------------------------------------------------------------------------
/app/[locale]/login/password/page.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { ChangePassword } from "@/components/utility/change-password"
4 | import { supabase } from "@/lib/supabase/browser-client"
5 | import { useRouter } from "next/navigation"
6 | import { useEffect, useState } from "react"
7 |
8 | export default function ChangePasswordPage() {
9 | const [loading, setLoading] = useState(true)
10 |
11 | const router = useRouter()
12 |
13 | useEffect(() => {
14 | ;(async () => {
15 | const session = (await supabase.auth.getSession()).data.session
16 |
17 | if (!session) {
18 | router.push("/login")
19 | } else {
20 | setLoading(false)
21 | }
22 | })()
23 | }, [])
24 |
25 | if (loading) {
26 | return null
27 | }
28 |
29 | return
30 | }
31 |
--------------------------------------------------------------------------------
/app/[locale]/page.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { ChatbotUISVG } from "@/components/icons/chatbotui-svg"
4 | import { IconArrowRight } from "@tabler/icons-react"
5 | import { useTheme } from "next-themes"
6 | import Link from "next/link"
7 |
8 | export default function HomePage() {
9 | const { theme } = useTheme()
10 |
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
Chatbot UI
18 |
19 |
23 | Start Chatting
24 |
25 |
26 |
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/app/api/assistants/openai/route.ts:
--------------------------------------------------------------------------------
1 | import { checkApiKey, getServerProfile } from "@/lib/server/server-chat-helpers"
2 | import { ServerRuntime } from "next"
3 | import OpenAI from "openai"
4 |
5 | export const runtime: ServerRuntime = "edge"
6 |
7 | export async function GET() {
8 | try {
9 | const profile = await getServerProfile()
10 |
11 | checkApiKey(profile.openai_api_key, "OpenAI")
12 |
13 | const openai = new OpenAI({
14 | apiKey: profile.openai_api_key || "",
15 | organization: profile.openai_organization_id
16 | })
17 |
18 | const myAssistants = await openai.beta.assistants.list({
19 | limit: 100
20 | })
21 |
22 | return new Response(JSON.stringify({ assistants: myAssistants.data }), {
23 | status: 200
24 | })
25 | } catch (error: any) {
26 | const errorMessage = error.error?.message || "An unexpected error occurred"
27 | const errorCode = error.status || 500
28 | return new Response(JSON.stringify({ message: errorMessage }), {
29 | status: errorCode
30 | })
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/app/api/chat/custom/route.ts:
--------------------------------------------------------------------------------
1 | import { Database } from "@/supabase/types"
2 | import { ChatSettings } from "@/types"
3 | import { createClient } from "@supabase/supabase-js"
4 | import { OpenAIStream, StreamingTextResponse } from "ai"
5 | import { ServerRuntime } from "next"
6 | import OpenAI from "openai"
7 | import { ChatCompletionCreateParamsBase } from "openai/resources/chat/completions.mjs"
8 |
9 | export const runtime: ServerRuntime = "edge"
10 |
11 | export async function POST(request: Request) {
12 | const json = await request.json()
13 | const { chatSettings, messages, customModelId } = json as {
14 | chatSettings: ChatSettings
15 | messages: any[]
16 | customModelId: string
17 | }
18 |
19 | try {
20 | const supabaseAdmin = createClient(
21 | process.env.NEXT_PUBLIC_SUPABASE_URL!,
22 | process.env.SUPABASE_SERVICE_ROLE_KEY!
23 | )
24 |
25 | const { data: customModel, error } = await supabaseAdmin
26 | .from("models")
27 | .select("*")
28 | .eq("id", customModelId)
29 | .single()
30 |
31 | if (!customModel) {
32 | throw new Error(error.message)
33 | }
34 |
35 | const custom = new OpenAI({
36 | apiKey: customModel.api_key || "",
37 | baseURL: customModel.base_url
38 | })
39 |
40 | const response = await custom.chat.completions.create({
41 | model: chatSettings.model as ChatCompletionCreateParamsBase["model"],
42 | messages: messages as ChatCompletionCreateParamsBase["messages"],
43 | temperature: chatSettings.temperature,
44 | stream: true
45 | })
46 |
47 | const stream = OpenAIStream(response)
48 |
49 | return new StreamingTextResponse(stream)
50 | } catch (error: any) {
51 | let errorMessage = error.message || "An unexpected error occurred"
52 | const errorCode = error.status || 500
53 |
54 | if (errorMessage.toLowerCase().includes("api key not found")) {
55 | errorMessage =
56 | "Custom API Key not found. Please set it in your profile settings."
57 | } else if (errorMessage.toLowerCase().includes("incorrect api key")) {
58 | errorMessage =
59 | "Custom API Key is incorrect. Please fix it in your profile settings."
60 | }
61 |
62 | return new Response(JSON.stringify({ message: errorMessage }), {
63 | status: errorCode
64 | })
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/app/api/chat/google/route.ts:
--------------------------------------------------------------------------------
1 | import { checkApiKey, getServerProfile } from "@/lib/server/server-chat-helpers"
2 | import { ChatSettings } from "@/types"
3 | import { GoogleGenerativeAI } from "@google/generative-ai"
4 |
5 | export const runtime = "edge"
6 |
7 | export async function POST(request: Request) {
8 | const json = await request.json()
9 | const { chatSettings, messages } = json as {
10 | chatSettings: ChatSettings
11 | messages: any[]
12 | }
13 |
14 | try {
15 | const profile = await getServerProfile()
16 |
17 | checkApiKey(profile.google_gemini_api_key, "Google")
18 |
19 | const genAI = new GoogleGenerativeAI(profile.google_gemini_api_key || "")
20 | const googleModel = genAI.getGenerativeModel({ model: chatSettings.model })
21 |
22 | const lastMessage = messages.pop()
23 |
24 | const chat = googleModel.startChat({
25 | history: messages,
26 | generationConfig: {
27 | temperature: chatSettings.temperature
28 | }
29 | })
30 |
31 | const response = await chat.sendMessageStream(lastMessage.parts)
32 |
33 | const encoder = new TextEncoder()
34 | const readableStream = new ReadableStream({
35 | async start(controller) {
36 | for await (const chunk of response.stream) {
37 | const chunkText = chunk.text()
38 | controller.enqueue(encoder.encode(chunkText))
39 | }
40 | controller.close()
41 | }
42 | })
43 |
44 | return new Response(readableStream, {
45 | headers: { "Content-Type": "text/plain" }
46 | })
47 |
48 | } catch (error: any) {
49 | let errorMessage = error.message || "An unexpected error occurred"
50 | const errorCode = error.status || 500
51 |
52 | if (errorMessage.toLowerCase().includes("api key not found")) {
53 | errorMessage =
54 | "Google Gemini API Key not found. Please set it in your profile settings."
55 | } else if (errorMessage.toLowerCase().includes("api key not valid")) {
56 | errorMessage =
57 | "Google Gemini API Key is incorrect. Please fix it in your profile settings."
58 | }
59 |
60 | return new Response(JSON.stringify({ message: errorMessage }), {
61 | status: errorCode
62 | })
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/app/api/chat/groq/route.ts:
--------------------------------------------------------------------------------
1 | import { CHAT_SETTING_LIMITS } from "@/lib/chat-setting-limits"
2 | import { checkApiKey, getServerProfile } from "@/lib/server/server-chat-helpers"
3 | import { ChatSettings } from "@/types"
4 | import { OpenAIStream, StreamingTextResponse } from "ai"
5 | import OpenAI from "openai"
6 |
7 | export const runtime = "edge"
8 | export async function POST(request: Request) {
9 | const json = await request.json()
10 | const { chatSettings, messages } = json as {
11 | chatSettings: ChatSettings
12 | messages: any[]
13 | }
14 |
15 | try {
16 | const profile = await getServerProfile()
17 |
18 | checkApiKey(profile.groq_api_key, "G")
19 |
20 | // Groq is compatible with the OpenAI SDK
21 | const groq = new OpenAI({
22 | apiKey: profile.groq_api_key || "",
23 | baseURL: "https://api.groq.com/openai/v1"
24 | })
25 |
26 | const response = await groq.chat.completions.create({
27 | model: chatSettings.model,
28 | messages,
29 | max_tokens:
30 | CHAT_SETTING_LIMITS[chatSettings.model].MAX_TOKEN_OUTPUT_LENGTH,
31 | stream: true
32 | })
33 |
34 | // Convert the response into a friendly text-stream.
35 | const stream = OpenAIStream(response)
36 |
37 | // Respond with the stream
38 | return new StreamingTextResponse(stream)
39 | } catch (error: any) {
40 | let errorMessage = error.message || "An unexpected error occurred"
41 | const errorCode = error.status || 500
42 |
43 | if (errorMessage.toLowerCase().includes("api key not found")) {
44 | errorMessage =
45 | "Groq API Key not found. Please set it in your profile settings."
46 | } else if (errorCode === 401) {
47 | errorMessage =
48 | "Groq API Key is incorrect. Please fix it in your profile settings."
49 | }
50 |
51 | return new Response(JSON.stringify({ message: errorMessage }), {
52 | status: errorCode
53 | })
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/app/api/chat/mistral/route.ts:
--------------------------------------------------------------------------------
1 | import { CHAT_SETTING_LIMITS } from "@/lib/chat-setting-limits"
2 | import { checkApiKey, getServerProfile } from "@/lib/server/server-chat-helpers"
3 | import { ChatSettings } from "@/types"
4 | import { OpenAIStream, StreamingTextResponse } from "ai"
5 | import OpenAI from "openai"
6 |
7 | export const runtime = "edge"
8 |
9 | export async function POST(request: Request) {
10 | const json = await request.json()
11 | const { chatSettings, messages } = json as {
12 | chatSettings: ChatSettings
13 | messages: any[]
14 | }
15 |
16 | try {
17 | const profile = await getServerProfile()
18 |
19 | checkApiKey(profile.mistral_api_key, "Mistral")
20 |
21 | // Mistral is compatible the OpenAI SDK
22 | const mistral = new OpenAI({
23 | apiKey: profile.mistral_api_key || "",
24 | baseURL: "https://api.mistral.ai/v1"
25 | })
26 |
27 | const response = await mistral.chat.completions.create({
28 | model: chatSettings.model,
29 | messages,
30 | max_tokens:
31 | CHAT_SETTING_LIMITS[chatSettings.model].MAX_TOKEN_OUTPUT_LENGTH,
32 | stream: true
33 | })
34 |
35 | // Convert the response into a friendly text-stream.
36 | const stream = OpenAIStream(response)
37 |
38 | // Respond with the stream
39 | return new StreamingTextResponse(stream)
40 | } catch (error: any) {
41 | let errorMessage = error.message || "An unexpected error occurred"
42 | const errorCode = error.status || 500
43 |
44 | if (errorMessage.toLowerCase().includes("api key not found")) {
45 | errorMessage =
46 | "Mistral API Key not found. Please set it in your profile settings."
47 | } else if (errorCode === 401) {
48 | errorMessage =
49 | "Mistral API Key is incorrect. Please fix it in your profile settings."
50 | }
51 |
52 | return new Response(JSON.stringify({ message: errorMessage }), {
53 | status: errorCode
54 | })
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/app/api/chat/openai/route.ts:
--------------------------------------------------------------------------------
1 | import { checkApiKey, getServerProfile } from "@/lib/server/server-chat-helpers"
2 | import { ChatSettings } from "@/types"
3 | import { OpenAIStream, StreamingTextResponse } from "ai"
4 | import { ServerRuntime } from "next"
5 | import OpenAI from "openai"
6 | import { ChatCompletionCreateParamsBase } from "openai/resources/chat/completions.mjs"
7 |
8 | export const runtime: ServerRuntime = "edge"
9 |
10 | export async function POST(request: Request) {
11 | const json = await request.json()
12 | const { chatSettings, messages } = json as {
13 | chatSettings: ChatSettings
14 | messages: any[]
15 | }
16 |
17 | try {
18 | const profile = await getServerProfile()
19 |
20 | checkApiKey(profile.openai_api_key, "OpenAI")
21 |
22 | const openai = new OpenAI({
23 | apiKey: profile.openai_api_key || "",
24 | organization: profile.openai_organization_id
25 | })
26 |
27 | const response = await openai.chat.completions.create({
28 | model: chatSettings.model as ChatCompletionCreateParamsBase["model"],
29 | messages: messages as ChatCompletionCreateParamsBase["messages"],
30 | temperature: chatSettings.temperature,
31 | max_tokens:
32 | chatSettings.model === "gpt-4-vision-preview" ||
33 | chatSettings.model === "gpt-4o"
34 | ? 4096
35 | : null, // TODO: Fix
36 | stream: true
37 | })
38 |
39 | const stream = OpenAIStream(response)
40 |
41 | return new StreamingTextResponse(stream)
42 | } catch (error: any) {
43 | let errorMessage = error.message || "An unexpected error occurred"
44 | const errorCode = error.status || 500
45 |
46 | if (errorMessage.toLowerCase().includes("api key not found")) {
47 | errorMessage =
48 | "OpenAI API Key not found. Please set it in your profile settings."
49 | } else if (errorMessage.toLowerCase().includes("incorrect api key")) {
50 | errorMessage =
51 | "OpenAI API Key is incorrect. Please fix it in your profile settings."
52 | }
53 |
54 | return new Response(JSON.stringify({ message: errorMessage }), {
55 | status: errorCode
56 | })
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/app/api/chat/openrouter/route.ts:
--------------------------------------------------------------------------------
1 | import { checkApiKey, getServerProfile } from "@/lib/server/server-chat-helpers"
2 | import { ChatSettings } from "@/types"
3 | import { OpenAIStream, StreamingTextResponse } from "ai"
4 | import { ServerRuntime } from "next"
5 | import OpenAI from "openai"
6 | import { ChatCompletionCreateParamsBase } from "openai/resources/chat/completions.mjs"
7 |
8 | export const runtime: ServerRuntime = "edge"
9 |
10 | export async function POST(request: Request) {
11 | const json = await request.json()
12 | const { chatSettings, messages } = json as {
13 | chatSettings: ChatSettings
14 | messages: any[]
15 | }
16 |
17 | try {
18 | const profile = await getServerProfile()
19 |
20 | checkApiKey(profile.openrouter_api_key, "OpenRouter")
21 |
22 | const openai = new OpenAI({
23 | apiKey: profile.openrouter_api_key || "",
24 | baseURL: "https://openrouter.ai/api/v1"
25 | })
26 |
27 | const response = await openai.chat.completions.create({
28 | model: chatSettings.model as ChatCompletionCreateParamsBase["model"],
29 | messages: messages as ChatCompletionCreateParamsBase["messages"],
30 | temperature: chatSettings.temperature,
31 | max_tokens: undefined,
32 | stream: true
33 | })
34 |
35 | const stream = OpenAIStream(response)
36 |
37 | return new StreamingTextResponse(stream)
38 | } catch (error: any) {
39 | let errorMessage = error.message || "An unexpected error occurred"
40 | const errorCode = error.status || 500
41 |
42 | if (errorMessage.toLowerCase().includes("api key not found")) {
43 | errorMessage =
44 | "OpenRouter API Key not found. Please set it in your profile settings."
45 | }
46 |
47 | return new Response(JSON.stringify({ message: errorMessage }), {
48 | status: errorCode
49 | })
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/api/chat/perplexity/route.ts:
--------------------------------------------------------------------------------
1 | import { checkApiKey, getServerProfile } from "@/lib/server/server-chat-helpers"
2 | import { ChatSettings } from "@/types"
3 | import { OpenAIStream, StreamingTextResponse } from "ai"
4 | import OpenAI from "openai"
5 |
6 | export const runtime = "edge"
7 |
8 | export async function POST(request: Request) {
9 | const json = await request.json()
10 | const { chatSettings, messages } = json as {
11 | chatSettings: ChatSettings
12 | messages: any[]
13 | }
14 |
15 | try {
16 | const profile = await getServerProfile()
17 |
18 | checkApiKey(profile.perplexity_api_key, "Perplexity")
19 |
20 | // Perplexity is compatible the OpenAI SDK
21 | const perplexity = new OpenAI({
22 | apiKey: profile.perplexity_api_key || "",
23 | baseURL: "https://api.perplexity.ai/"
24 | })
25 |
26 | const response = await perplexity.chat.completions.create({
27 | model: chatSettings.model,
28 | messages,
29 | stream: true
30 | })
31 |
32 | const stream = OpenAIStream(response)
33 |
34 | return new StreamingTextResponse(stream)
35 | } catch (error: any) {
36 | let errorMessage = error.message || "An unexpected error occurred"
37 | const errorCode = error.status || 500
38 |
39 | if (errorMessage.toLowerCase().includes("api key not found")) {
40 | errorMessage =
41 | "Perplexity API Key not found. Please set it in your profile settings."
42 | } else if (errorCode === 401) {
43 | errorMessage =
44 | "Perplexity API Key is incorrect. Please fix it in your profile settings."
45 | }
46 |
47 | return new Response(JSON.stringify({ message: errorMessage }), {
48 | status: errorCode
49 | })
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/api/command/route.ts:
--------------------------------------------------------------------------------
1 | import { CHAT_SETTING_LIMITS } from "@/lib/chat-setting-limits"
2 | import { checkApiKey, getServerProfile } from "@/lib/server/server-chat-helpers"
3 | import OpenAI from "openai"
4 |
5 | export const runtime = "edge"
6 |
7 | export async function POST(request: Request) {
8 | const json = await request.json()
9 | const { input } = json as {
10 | input: string
11 | }
12 |
13 | try {
14 | const profile = await getServerProfile()
15 |
16 | checkApiKey(profile.openai_api_key, "OpenAI")
17 |
18 | const openai = new OpenAI({
19 | apiKey: profile.openai_api_key || "",
20 | organization: profile.openai_organization_id
21 | })
22 |
23 | const response = await openai.chat.completions.create({
24 | model: "gpt-4-1106-preview",
25 | messages: [
26 | {
27 | role: "system",
28 | content: "Respond to the user."
29 | },
30 | {
31 | role: "user",
32 | content: input
33 | }
34 | ],
35 | temperature: 0,
36 | max_tokens:
37 | CHAT_SETTING_LIMITS["gpt-4-turbo-preview"].MAX_TOKEN_OUTPUT_LENGTH
38 | // response_format: { type: "json_object" }
39 | // stream: true
40 | })
41 |
42 | const content = response.choices[0].message.content
43 |
44 | return new Response(JSON.stringify({ content }), {
45 | status: 200
46 | })
47 | } catch (error: any) {
48 | const errorMessage = error.error?.message || "An unexpected error occurred"
49 | const errorCode = error.status || 500
50 | return new Response(JSON.stringify({ message: errorMessage }), {
51 | status: errorCode
52 | })
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/app/api/keys/route.ts:
--------------------------------------------------------------------------------
1 | import { isUsingEnvironmentKey } from "@/lib/envs"
2 | import { createResponse } from "@/lib/server/server-utils"
3 | import { EnvKey } from "@/types/key-type"
4 | import { VALID_ENV_KEYS } from "@/types/valid-keys"
5 |
6 | export async function GET() {
7 | const envKeyMap: Record = {
8 | azure: VALID_ENV_KEYS.AZURE_OPENAI_API_KEY,
9 | openai: VALID_ENV_KEYS.OPENAI_API_KEY,
10 | google: VALID_ENV_KEYS.GOOGLE_GEMINI_API_KEY,
11 | anthropic: VALID_ENV_KEYS.ANTHROPIC_API_KEY,
12 | mistral: VALID_ENV_KEYS.MISTRAL_API_KEY,
13 | groq: VALID_ENV_KEYS.GROQ_API_KEY,
14 | perplexity: VALID_ENV_KEYS.PERPLEXITY_API_KEY,
15 | openrouter: VALID_ENV_KEYS.OPENROUTER_API_KEY,
16 |
17 | openai_organization_id: VALID_ENV_KEYS.OPENAI_ORGANIZATION_ID,
18 |
19 | azure_openai_endpoint: VALID_ENV_KEYS.AZURE_OPENAI_ENDPOINT,
20 | azure_gpt_35_turbo_name: VALID_ENV_KEYS.AZURE_GPT_35_TURBO_NAME,
21 | azure_gpt_45_vision_name: VALID_ENV_KEYS.AZURE_GPT_45_VISION_NAME,
22 | azure_gpt_45_turbo_name: VALID_ENV_KEYS.AZURE_GPT_45_TURBO_NAME,
23 | azure_embeddings_name: VALID_ENV_KEYS.AZURE_EMBEDDINGS_NAME
24 | }
25 |
26 | const isUsingEnvKeyMap = Object.keys(envKeyMap).reduce<
27 | Record
28 | >((acc, provider) => {
29 | const key = envKeyMap[provider]
30 |
31 | if (key) {
32 | acc[provider] = isUsingEnvironmentKey(key as EnvKey)
33 | }
34 | return acc
35 | }, {})
36 |
37 | return createResponse({ isUsingEnvKeyMap }, 200)
38 | }
39 |
--------------------------------------------------------------------------------
/app/api/username/available/route.ts:
--------------------------------------------------------------------------------
1 | import { Database } from "@/supabase/types"
2 | import { createClient } from "@supabase/supabase-js"
3 |
4 | export const runtime = "edge"
5 |
6 | export async function POST(request: Request) {
7 | const json = await request.json()
8 | const { username } = json as {
9 | username: string
10 | }
11 |
12 | try {
13 | const supabaseAdmin = createClient(
14 | process.env.NEXT_PUBLIC_SUPABASE_URL!,
15 | process.env.SUPABASE_SERVICE_ROLE_KEY!
16 | )
17 |
18 | const { data: usernames, error } = await supabaseAdmin
19 | .from("profiles")
20 | .select("username")
21 | .eq("username", username)
22 |
23 | if (!usernames) {
24 | throw new Error(error.message)
25 | }
26 |
27 | return new Response(JSON.stringify({ isAvailable: !usernames.length }), {
28 | status: 200
29 | })
30 | } catch (error: any) {
31 | const errorMessage = error.error?.message || "An unexpected error occurred"
32 | const errorCode = error.status || 500
33 | return new Response(JSON.stringify({ message: errorMessage }), {
34 | status: errorCode
35 | })
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/app/api/username/get/route.ts:
--------------------------------------------------------------------------------
1 | import { Database } from "@/supabase/types"
2 | import { createClient } from "@supabase/supabase-js"
3 |
4 | export const runtime = "edge"
5 |
6 | export async function POST(request: Request) {
7 | const json = await request.json()
8 | const { userId } = json as {
9 | userId: string
10 | }
11 |
12 | try {
13 | const supabaseAdmin = createClient(
14 | process.env.NEXT_PUBLIC_SUPABASE_URL!,
15 | process.env.SUPABASE_SERVICE_ROLE_KEY!
16 | )
17 |
18 | const { data, error } = await supabaseAdmin
19 | .from("profiles")
20 | .select("username")
21 | .eq("user_id", userId)
22 | .single()
23 |
24 | if (!data) {
25 | throw new Error(error.message)
26 | }
27 |
28 | return new Response(JSON.stringify({ username: data.username }), {
29 | status: 200
30 | })
31 | } catch (error: any) {
32 | const errorMessage = error.error?.message || "An unexpected error occurred"
33 | const errorCode = error.status || 500
34 | return new Response(JSON.stringify({ message: errorMessage }), {
35 | status: errorCode
36 | })
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/auth/callback/route.ts:
--------------------------------------------------------------------------------
1 | import { createClient } from "@/lib/supabase/server"
2 | import { cookies } from "next/headers"
3 | import { NextResponse } from "next/server"
4 |
5 | export async function GET(request: Request) {
6 | const requestUrl = new URL(request.url)
7 | const code = requestUrl.searchParams.get("code")
8 | const next = requestUrl.searchParams.get("next")
9 |
10 | if (code) {
11 | const cookieStore = cookies()
12 | const supabase = createClient(cookieStore)
13 | await supabase.auth.exchangeCodeForSession(code)
14 | }
15 |
16 | if (next) {
17 | return NextResponse.redirect(requestUrl.origin + next)
18 | } else {
19 | return NextResponse.redirect(requestUrl.origin)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "app/globals.css",
9 | "baseColor": "gray",
10 | "cssVariables": true
11 | },
12 | "aliases": {
13 | "components": "@/components",
14 | "utils": "@/lib/utils"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/components/chat/chat-command-input.tsx:
--------------------------------------------------------------------------------
1 | import { ChatbotUIContext } from "@/context/context"
2 | import { FC, useContext } from "react"
3 | import { AssistantPicker } from "./assistant-picker"
4 | import { usePromptAndCommand } from "./chat-hooks/use-prompt-and-command"
5 | import { FilePicker } from "./file-picker"
6 | import { PromptPicker } from "./prompt-picker"
7 | import { ToolPicker } from "./tool-picker"
8 |
9 | interface ChatCommandInputProps {}
10 |
11 | export const ChatCommandInput: FC = ({}) => {
12 | const {
13 | newMessageFiles,
14 | chatFiles,
15 | slashCommand,
16 | isFilePickerOpen,
17 | setIsFilePickerOpen,
18 | hashtagCommand,
19 | focusPrompt,
20 | focusFile
21 | } = useContext(ChatbotUIContext)
22 |
23 | const { handleSelectUserFile, handleSelectUserCollection } =
24 | usePromptAndCommand()
25 |
26 | return (
27 | <>
28 |
29 |
30 | file.id
36 | )}
37 | selectedCollectionIds={[]}
38 | onSelectFile={handleSelectUserFile}
39 | onSelectCollection={handleSelectUserCollection}
40 | isFocused={focusFile}
41 | />
42 |
43 |
44 |
45 |
46 | >
47 | )
48 | }
49 |
--------------------------------------------------------------------------------
/components/chat/chat-hooks/use-scroll.tsx:
--------------------------------------------------------------------------------
1 | import { ChatbotUIContext } from "@/context/context"
2 | import {
3 | type UIEventHandler,
4 | useCallback,
5 | useContext,
6 | useEffect,
7 | useRef,
8 | useState
9 | } from "react"
10 |
11 | export const useScroll = () => {
12 | const { isGenerating, chatMessages } = useContext(ChatbotUIContext)
13 |
14 | const messagesStartRef = useRef(null)
15 | const messagesEndRef = useRef(null)
16 | const isAutoScrolling = useRef(false)
17 |
18 | const [isAtTop, setIsAtTop] = useState(false)
19 | const [isAtBottom, setIsAtBottom] = useState(true)
20 | const [userScrolled, setUserScrolled] = useState(false)
21 | const [isOverflowing, setIsOverflowing] = useState(false)
22 |
23 | useEffect(() => {
24 | setUserScrolled(false)
25 |
26 | if (!isGenerating && userScrolled) {
27 | setUserScrolled(false)
28 | }
29 | }, [isGenerating])
30 |
31 | useEffect(() => {
32 | if (isGenerating && !userScrolled) {
33 | scrollToBottom()
34 | }
35 | }, [chatMessages])
36 |
37 | const handleScroll: UIEventHandler = useCallback(e => {
38 | const target = e.target as HTMLDivElement
39 | const bottom =
40 | Math.round(target.scrollHeight) - Math.round(target.scrollTop) ===
41 | Math.round(target.clientHeight)
42 | setIsAtBottom(bottom)
43 |
44 | const top = target.scrollTop === 0
45 | setIsAtTop(top)
46 |
47 | if (!bottom && !isAutoScrolling.current) {
48 | setUserScrolled(true)
49 | } else {
50 | setUserScrolled(false)
51 | }
52 |
53 | const isOverflow = target.scrollHeight > target.clientHeight
54 | setIsOverflowing(isOverflow)
55 | }, [])
56 |
57 | const scrollToTop = useCallback(() => {
58 | if (messagesStartRef.current) {
59 | messagesStartRef.current.scrollIntoView({ behavior: "instant" })
60 | }
61 | }, [])
62 |
63 | const scrollToBottom = useCallback(() => {
64 | isAutoScrolling.current = true
65 |
66 | setTimeout(() => {
67 | if (messagesEndRef.current) {
68 | messagesEndRef.current.scrollIntoView({ behavior: "instant" })
69 | }
70 |
71 | isAutoScrolling.current = false
72 | }, 100)
73 | }, [])
74 |
75 | return {
76 | messagesStartRef,
77 | messagesEndRef,
78 | isAtTop,
79 | isAtBottom,
80 | userScrolled,
81 | isOverflowing,
82 | handleScroll,
83 | scrollToTop,
84 | scrollToBottom,
85 | setIsAtBottom
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/components/chat/chat-messages.tsx:
--------------------------------------------------------------------------------
1 | import { useChatHandler } from "@/components/chat/chat-hooks/use-chat-handler"
2 | import { ChatbotUIContext } from "@/context/context"
3 | import { Tables } from "@/supabase/types"
4 | import { FC, useContext, useState } from "react"
5 | import { Message } from "../messages/message"
6 |
7 | interface ChatMessagesProps {}
8 |
9 | export const ChatMessages: FC = ({}) => {
10 | const { chatMessages, chatFileItems } = useContext(ChatbotUIContext)
11 |
12 | const { handleSendEdit } = useChatHandler()
13 |
14 | const [editingMessage, setEditingMessage] = useState>()
15 |
16 | return chatMessages
17 | .sort((a, b) => a.message.sequence_number - b.message.sequence_number)
18 | .map((chatMessage, index, array) => {
19 | const messageFileItems = chatFileItems.filter(
20 | (chatFileItem, _, self) =>
21 | chatMessage.fileItems.includes(chatFileItem.id) &&
22 | self.findIndex(item => item.id === chatFileItem.id) === _
23 | )
24 |
25 | return (
26 | setEditingMessage(undefined)}
34 | onSubmitEdit={handleSendEdit}
35 | />
36 | )
37 | })
38 | }
39 |
--------------------------------------------------------------------------------
/components/chat/chat-retrieval-settings.tsx:
--------------------------------------------------------------------------------
1 | import { ChatbotUIContext } from "@/context/context"
2 | import { IconAdjustmentsHorizontal } from "@tabler/icons-react"
3 | import { FC, useContext, useState } from "react"
4 | import { Button } from "../ui/button"
5 | import {
6 | Dialog,
7 | DialogContent,
8 | DialogFooter,
9 | DialogTrigger
10 | } from "../ui/dialog"
11 | import { Label } from "../ui/label"
12 | import { Slider } from "../ui/slider"
13 | import { WithTooltip } from "../ui/with-tooltip"
14 |
15 | interface ChatRetrievalSettingsProps {}
16 |
17 | export const ChatRetrievalSettings: FC = ({}) => {
18 | const { sourceCount, setSourceCount } = useContext(ChatbotUIContext)
19 |
20 | const [isOpen, setIsOpen] = useState(false)
21 |
22 | return (
23 |
64 | )
65 | }
66 |
--------------------------------------------------------------------------------
/components/chat/chat-scroll-buttons.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | IconCircleArrowDownFilled,
3 | IconCircleArrowUpFilled
4 | } from "@tabler/icons-react"
5 | import { FC } from "react"
6 |
7 | interface ChatScrollButtonsProps {
8 | isAtTop: boolean
9 | isAtBottom: boolean
10 | isOverflowing: boolean
11 | scrollToTop: () => void
12 | scrollToBottom: () => void
13 | }
14 |
15 | export const ChatScrollButtons: FC = ({
16 | isAtTop,
17 | isAtBottom,
18 | isOverflowing,
19 | scrollToTop,
20 | scrollToBottom
21 | }) => {
22 | return (
23 | <>
24 | {!isAtTop && isOverflowing && (
25 |
30 | )}
31 |
32 | {!isAtBottom && isOverflowing && (
33 |
38 | )}
39 | >
40 | )
41 | }
42 |
--------------------------------------------------------------------------------
/components/chat/quick-setting-option.tsx:
--------------------------------------------------------------------------------
1 | import { LLM_LIST } from "@/lib/models/llm/llm-list"
2 | import { Tables } from "@/supabase/types"
3 | import { IconCircleCheckFilled, IconRobotFace } from "@tabler/icons-react"
4 | import Image from "next/image"
5 | import { FC } from "react"
6 | import { ModelIcon } from "../models/model-icon"
7 | import { DropdownMenuItem } from "../ui/dropdown-menu"
8 |
9 | interface QuickSettingOptionProps {
10 | contentType: "presets" | "assistants"
11 | isSelected: boolean
12 | item: Tables<"presets"> | Tables<"assistants">
13 | onSelect: () => void
14 | image: string
15 | }
16 |
17 | export const QuickSettingOption: FC = ({
18 | contentType,
19 | isSelected,
20 | item,
21 | onSelect,
22 | image
23 | }) => {
24 | const modelDetails = LLM_LIST.find(model => model.modelId === item.model)
25 |
26 | return (
27 |
32 |
33 | {contentType === "presets" ? (
34 |
39 | ) : image ? (
40 |
48 | ) : (
49 |
53 | )}
54 |
55 |
56 |
57 |
{item.name}
58 |
59 | {item.description && (
60 |
{item.description}
61 | )}
62 |
63 |
64 |
65 | {isSelected ? (
66 |
67 | ) : null}
68 |
69 |
70 | )
71 | }
72 |
--------------------------------------------------------------------------------
/components/icons/anthropic-svg.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from "react"
2 |
3 | interface AnthropicSVGProps {
4 | height?: number
5 | width?: number
6 | className?: string
7 | }
8 |
9 | export const AnthropicSVG: FC = ({
10 | height = 40,
11 | width = 40,
12 | className
13 | }) => {
14 | return (
15 |
43 | )
44 | }
45 |
--------------------------------------------------------------------------------
/components/icons/chatbotui-svg.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from "react"
2 |
3 | interface ChatbotUISVGProps {
4 | theme: "dark" | "light"
5 | scale?: number
6 | }
7 |
8 | export const ChatbotUISVG: FC = ({ theme, scale = 1 }) => {
9 | return (
10 |
36 | )
37 | }
38 |
--------------------------------------------------------------------------------
/components/icons/google-svg.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from "react"
2 |
3 | interface GoogleSVGProps {
4 | height?: number
5 | width?: number
6 | className?: string
7 | }
8 |
9 | export const GoogleSVG: FC = ({
10 | height = 40,
11 | width = 40,
12 | className
13 | }) => {
14 | return (
15 |
41 | )
42 | }
43 |
--------------------------------------------------------------------------------
/components/messages/message-markdown-memoized.tsx:
--------------------------------------------------------------------------------
1 | import { FC, memo } from "react"
2 | import ReactMarkdown, { Options } from "react-markdown"
3 |
4 | export const MessageMarkdownMemoized: FC = memo(
5 | ReactMarkdown,
6 | (prevProps, nextProps) =>
7 | prevProps.children === nextProps.children &&
8 | prevProps.className === nextProps.className
9 | )
10 |
--------------------------------------------------------------------------------
/components/messages/message-markdown.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from "react"
2 | import remarkGfm from "remark-gfm"
3 | import remarkMath from "remark-math"
4 | import { MessageCodeBlock } from "./message-codeblock"
5 | import { MessageMarkdownMemoized } from "./message-markdown-memoized"
6 |
7 | interface MessageMarkdownProps {
8 | content: string
9 | }
10 |
11 | export const MessageMarkdown: FC = ({ content }) => {
12 | return (
13 | {children}
19 | },
20 | img({ node, ...props }) {
21 | return
22 | },
23 | code({ node, className, children, ...props }) {
24 | const childArray = React.Children.toArray(children)
25 | const firstChild = childArray[0] as React.ReactElement
26 | const firstChildAsString = React.isValidElement(firstChild)
27 | ? (firstChild as React.ReactElement).props.children
28 | : firstChild
29 |
30 | if (firstChildAsString === "▍") {
31 | return ▍
32 | }
33 |
34 | if (typeof firstChildAsString === "string") {
35 | childArray[0] = firstChildAsString.replace("`▍`", "▍")
36 | }
37 |
38 | const match = /language-(\w+)/.exec(className || "")
39 |
40 | if (
41 | typeof firstChildAsString === "string" &&
42 | !firstChildAsString.includes("\n")
43 | ) {
44 | return (
45 |
46 | {childArray}
47 |
48 | )
49 | }
50 |
51 | return (
52 |
58 | )
59 | }
60 | }}
61 | >
62 | {content}
63 |
64 | )
65 | }
66 |
--------------------------------------------------------------------------------
/components/messages/message-replies.tsx:
--------------------------------------------------------------------------------
1 | import { IconMessage } from "@tabler/icons-react"
2 | import { FC, useState } from "react"
3 | import {
4 | Sheet,
5 | SheetContent,
6 | SheetDescription,
7 | SheetHeader,
8 | SheetTitle,
9 | SheetTrigger
10 | } from "../ui/sheet"
11 | import { WithTooltip } from "../ui/with-tooltip"
12 | import { MESSAGE_ICON_SIZE } from "./message-actions"
13 |
14 | interface MessageRepliesProps {}
15 |
16 | export const MessageReplies: FC = ({}) => {
17 | const [isOpen, setIsOpen] = useState(false)
18 |
19 | return (
20 |
21 |
22 | View Replies}
26 | trigger={
27 | setIsOpen(true)}
30 | >
31 |
32 |
33 | {1}
34 |
35 |
36 | }
37 | />
38 |
39 |
40 |
41 |
42 | Are you sure absolutely sure?
43 |
44 | This action cannot be undone. This will permanently delete your
45 | account and remove your data from our servers.
46 |
47 |
48 |
49 |
50 | )
51 | }
52 |
--------------------------------------------------------------------------------
/components/models/model-option.tsx:
--------------------------------------------------------------------------------
1 | import { LLM } from "@/types"
2 | import { FC } from "react"
3 | import { ModelIcon } from "./model-icon"
4 | import { IconInfoCircle } from "@tabler/icons-react"
5 | import { WithTooltip } from "../ui/with-tooltip"
6 |
7 | interface ModelOptionProps {
8 | model: LLM
9 | onSelect: () => void
10 | }
11 |
12 | export const ModelOption: FC = ({ model, onSelect }) => {
13 | return (
14 |
17 | {model.provider !== "ollama" && model.pricing && (
18 |
19 |
20 | Input Cost:{" "}
21 | {model.pricing.inputCost} {model.pricing.currency} per{" "}
22 | {model.pricing.unit}
23 |
24 | {model.pricing.outputCost && (
25 |
26 | Output Cost:{" "}
27 | {model.pricing.outputCost} {model.pricing.currency} per{" "}
28 | {model.pricing.unit}
29 |
30 | )}
31 |
32 | )}
33 |
34 | }
35 | side="bottom"
36 | trigger={
37 |
41 |
42 |
43 |
{model.modelName}
44 |
45 |
46 | }
47 | />
48 | )
49 | }
50 |
--------------------------------------------------------------------------------
/components/setup/finish-step.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from "react"
2 |
3 | interface FinishStepProps {
4 | displayName: string
5 | }
6 |
7 | export const FinishStep: FC = ({ displayName }) => {
8 | return (
9 |
10 |
11 | Welcome to Chatbot UI
12 | {displayName.length > 0 ? `, ${displayName.split(" ")[0]}` : null}!
13 |
14 |
15 |
Click next to start chatting.
16 |
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/components/setup/step-container.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button"
2 | import {
3 | Card,
4 | CardContent,
5 | CardDescription,
6 | CardFooter,
7 | CardHeader,
8 | CardTitle
9 | } from "@/components/ui/card"
10 | import { FC, useRef } from "react"
11 |
12 | export const SETUP_STEP_COUNT = 3
13 |
14 | interface StepContainerProps {
15 | stepDescription: string
16 | stepNum: number
17 | stepTitle: string
18 | onShouldProceed: (shouldProceed: boolean) => void
19 | children?: React.ReactNode
20 | showBackButton?: boolean
21 | showNextButton?: boolean
22 | }
23 |
24 | export const StepContainer: FC = ({
25 | stepDescription,
26 | stepNum,
27 | stepTitle,
28 | onShouldProceed,
29 | children,
30 | showBackButton = false,
31 | showNextButton = true
32 | }) => {
33 | const buttonRef = useRef(null)
34 |
35 | const handleKeyDown = (e: React.KeyboardEvent) => {
36 | if (e.key === "Enter" && !e.shiftKey) {
37 | if (buttonRef.current) {
38 | buttonRef.current.click()
39 | }
40 | }
41 | }
42 |
43 | return (
44 |
48 |
49 |
50 | {stepTitle}
51 |
52 |
53 | {stepNum} / {SETUP_STEP_COUNT}
54 |
55 |
56 |
57 | {stepDescription}
58 |
59 |
60 | {children}
61 |
62 |
63 |
64 | {showBackButton && (
65 |
72 | )}
73 |
74 |
75 |
76 | {showNextButton && (
77 |
84 | )}
85 |
86 |
87 |
88 | )
89 | }
90 |
--------------------------------------------------------------------------------
/components/sidebar/items/chat/delete-chat.tsx:
--------------------------------------------------------------------------------
1 | import { useChatHandler } from "@/components/chat/chat-hooks/use-chat-handler"
2 | import { Button } from "@/components/ui/button"
3 | import {
4 | Dialog,
5 | DialogContent,
6 | DialogDescription,
7 | DialogFooter,
8 | DialogHeader,
9 | DialogTitle,
10 | DialogTrigger
11 | } from "@/components/ui/dialog"
12 | import { ChatbotUIContext } from "@/context/context"
13 | import { deleteChat } from "@/db/chats"
14 | import useHotkey from "@/lib/hooks/use-hotkey"
15 | import { Tables } from "@/supabase/types"
16 | import { IconTrash } from "@tabler/icons-react"
17 | import { FC, useContext, useRef, useState } from "react"
18 |
19 | interface DeleteChatProps {
20 | chat: Tables<"chats">
21 | }
22 |
23 | export const DeleteChat: FC = ({ chat }) => {
24 | useHotkey("Backspace", () => setShowChatDialog(true))
25 |
26 | const { setChats } = useContext(ChatbotUIContext)
27 | const { handleNewChat } = useChatHandler()
28 |
29 | const buttonRef = useRef(null)
30 |
31 | const [showChatDialog, setShowChatDialog] = useState(false)
32 |
33 | const handleDeleteChat = async () => {
34 | await deleteChat(chat.id)
35 |
36 | setChats(prevState => prevState.filter(c => c.id !== chat.id))
37 |
38 | setShowChatDialog(false)
39 |
40 | handleNewChat()
41 | }
42 |
43 | const handleKeyDown = (e: React.KeyboardEvent) => {
44 | if (e.key === "Enter") {
45 | buttonRef.current?.click()
46 | }
47 | }
48 |
49 | return (
50 |
79 | )
80 | }
81 |
--------------------------------------------------------------------------------
/components/sidebar/items/chat/update-chat.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button"
2 | import {
3 | Dialog,
4 | DialogContent,
5 | DialogFooter,
6 | DialogHeader,
7 | DialogTitle,
8 | DialogTrigger
9 | } from "@/components/ui/dialog"
10 | import { Input } from "@/components/ui/input"
11 | import { Label } from "@/components/ui/label"
12 | import { ChatbotUIContext } from "@/context/context"
13 | import { updateChat } from "@/db/chats"
14 | import { Tables } from "@/supabase/types"
15 | import { IconEdit } from "@tabler/icons-react"
16 | import { FC, useContext, useRef, useState } from "react"
17 |
18 | interface UpdateChatProps {
19 | chat: Tables<"chats">
20 | }
21 |
22 | export const UpdateChat: FC = ({ chat }) => {
23 | const { setChats } = useContext(ChatbotUIContext)
24 |
25 | const buttonRef = useRef(null)
26 |
27 | const [showChatDialog, setShowChatDialog] = useState(false)
28 | const [name, setName] = useState(chat.name)
29 |
30 | const handleUpdateChat = async (e: React.MouseEvent) => {
31 | const updatedChat = await updateChat(chat.id, {
32 | name
33 | })
34 | setChats(prevState =>
35 | prevState.map(c => (c.id === chat.id ? updatedChat : c))
36 | )
37 |
38 | setShowChatDialog(false)
39 | }
40 |
41 | const handleKeyDown = (e: React.KeyboardEvent) => {
42 | if (e.key === "Enter") {
43 | buttonRef.current?.click()
44 | }
45 | }
46 |
47 | return (
48 |
75 | )
76 | }
77 |
--------------------------------------------------------------------------------
/components/sidebar/items/folders/update-folder.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button"
2 | import {
3 | Dialog,
4 | DialogContent,
5 | DialogFooter,
6 | DialogHeader,
7 | DialogTitle,
8 | DialogTrigger
9 | } from "@/components/ui/dialog"
10 | import { Input } from "@/components/ui/input"
11 | import { Label } from "@/components/ui/label"
12 | import { ChatbotUIContext } from "@/context/context"
13 | import { updateFolder } from "@/db/folders"
14 | import { Tables } from "@/supabase/types"
15 | import { IconEdit } from "@tabler/icons-react"
16 | import { FC, useContext, useRef, useState } from "react"
17 |
18 | interface UpdateFolderProps {
19 | folder: Tables<"folders">
20 | }
21 |
22 | export const UpdateFolder: FC = ({ folder }) => {
23 | const { setFolders } = useContext(ChatbotUIContext)
24 |
25 | const buttonRef = useRef(null)
26 |
27 | const [showFolderDialog, setShowFolderDialog] = useState(false)
28 | const [name, setName] = useState(folder.name)
29 |
30 | const handleUpdateFolder = async (e: React.MouseEvent) => {
31 | const updatedFolder = await updateFolder(folder.id, {
32 | name
33 | })
34 | setFolders(prevState =>
35 | prevState.map(c => (c.id === folder.id ? updatedFolder : c))
36 | )
37 |
38 | setShowFolderDialog(false)
39 | }
40 |
41 | const handleKeyDown = (e: React.KeyboardEvent) => {
42 | if (e.key === "Enter") {
43 | buttonRef.current?.click()
44 | }
45 | }
46 |
47 | return (
48 |
75 | )
76 | }
77 |
--------------------------------------------------------------------------------
/components/sidebar/items/prompts/create-prompt.tsx:
--------------------------------------------------------------------------------
1 | import { SidebarCreateItem } from "@/components/sidebar/items/all/sidebar-create-item"
2 | import { Input } from "@/components/ui/input"
3 | import { Label } from "@/components/ui/label"
4 | import { TextareaAutosize } from "@/components/ui/textarea-autosize"
5 | import { ChatbotUIContext } from "@/context/context"
6 | import { PROMPT_NAME_MAX } from "@/db/limits"
7 | import { TablesInsert } from "@/supabase/types"
8 | import { FC, useContext, useState } from "react"
9 |
10 | interface CreatePromptProps {
11 | isOpen: boolean
12 | onOpenChange: (isOpen: boolean) => void
13 | }
14 |
15 | export const CreatePrompt: FC = ({
16 | isOpen,
17 | onOpenChange
18 | }) => {
19 | const { profile, selectedWorkspace } = useContext(ChatbotUIContext)
20 | const [isTyping, setIsTyping] = useState(false)
21 | const [name, setName] = useState("")
22 | const [content, setContent] = useState("")
23 |
24 | if (!profile) return null
25 | if (!selectedWorkspace) return null
26 |
27 | return (
28 |
39 | }
40 | renderInputs={() => (
41 | <>
42 |
43 |
44 |
45 | setName(e.target.value)}
49 | maxLength={PROMPT_NAME_MAX}
50 | onCompositionStart={() => setIsTyping(true)}
51 | onCompositionEnd={() => setIsTyping(false)}
52 | />
53 |
54 |
55 |
56 |
57 |
58 | setIsTyping(true)}
65 | onCompositionEnd={() => setIsTyping(false)}
66 | />
67 |
68 | >
69 | )}
70 | />
71 | )
72 | }
73 |
--------------------------------------------------------------------------------
/components/sidebar/items/prompts/prompt-item.tsx:
--------------------------------------------------------------------------------
1 | import { Input } from "@/components/ui/input"
2 | import { Label } from "@/components/ui/label"
3 | import { TextareaAutosize } from "@/components/ui/textarea-autosize"
4 | import { PROMPT_NAME_MAX } from "@/db/limits"
5 | import { Tables } from "@/supabase/types"
6 | import { IconPencil } from "@tabler/icons-react"
7 | import { FC, useState } from "react"
8 | import { SidebarItem } from "../all/sidebar-display-item"
9 |
10 | interface PromptItemProps {
11 | prompt: Tables<"prompts">
12 | }
13 |
14 | export const PromptItem: FC = ({ prompt }) => {
15 | const [name, setName] = useState(prompt.name)
16 | const [content, setContent] = useState(prompt.content)
17 | const [isTyping, setIsTyping] = useState(false)
18 | return (
19 | }
24 | updateState={{ name, content }}
25 | renderInputs={() => (
26 | <>
27 |
28 |
29 |
30 | setName(e.target.value)}
34 | maxLength={PROMPT_NAME_MAX}
35 | onCompositionStart={() => setIsTyping(true)}
36 | onCompositionEnd={() => setIsTyping(false)}
37 | />
38 |
39 |
40 |
41 |
42 |
43 | setIsTyping(true)}
50 | onCompositionEnd={() => setIsTyping(false)}
51 | />
52 |
53 | >
54 | )}
55 | />
56 | )
57 | }
58 |
--------------------------------------------------------------------------------
/components/sidebar/sidebar-content.tsx:
--------------------------------------------------------------------------------
1 | import { Tables } from "@/supabase/types"
2 | import { ContentType, DataListType } from "@/types"
3 | import { FC, useState } from "react"
4 | import { SidebarCreateButtons } from "./sidebar-create-buttons"
5 | import { SidebarDataList } from "./sidebar-data-list"
6 | import { SidebarSearch } from "./sidebar-search"
7 |
8 | interface SidebarContentProps {
9 | contentType: ContentType
10 | data: DataListType
11 | folders: Tables<"folders">[]
12 | }
13 |
14 | export const SidebarContent: FC = ({
15 | contentType,
16 | data,
17 | folders
18 | }) => {
19 | const [searchTerm, setSearchTerm] = useState("")
20 |
21 | const filteredData: any = data.filter(item =>
22 | item.name.toLowerCase().includes(searchTerm.toLowerCase())
23 | )
24 |
25 | return (
26 | // Subtract 50px for the height of the workspace settings
27 |
28 |
29 | 0}
32 | />
33 |
34 |
35 |
36 |
41 |
42 |
43 |
48 |
49 | )
50 | }
51 |
--------------------------------------------------------------------------------
/components/sidebar/sidebar-search.tsx:
--------------------------------------------------------------------------------
1 | import { ContentType } from "@/types"
2 | import { FC } from "react"
3 | import { Input } from "../ui/input"
4 |
5 | interface SidebarSearchProps {
6 | contentType: ContentType
7 | searchTerm: string
8 | setSearchTerm: Function
9 | }
10 |
11 | export const SidebarSearch: FC = ({
12 | contentType,
13 | searchTerm,
14 | setSearchTerm
15 | }) => {
16 | return (
17 | setSearchTerm(e.target.value)}
21 | />
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/components/sidebar/sidebar-switch-item.tsx:
--------------------------------------------------------------------------------
1 | import { ContentType } from "@/types"
2 | import { FC } from "react"
3 | import { TabsTrigger } from "../ui/tabs"
4 | import { WithTooltip } from "../ui/with-tooltip"
5 |
6 | interface SidebarSwitchItemProps {
7 | contentType: ContentType
8 | icon: React.ReactNode
9 | onContentTypeChange: (contentType: ContentType) => void
10 | }
11 |
12 | export const SidebarSwitchItem: FC = ({
13 | contentType,
14 | icon,
15 | onContentTypeChange
16 | }) => {
17 | return (
18 | {contentType[0].toUpperCase() + contentType.substring(1)}
21 | }
22 | trigger={
23 | onContentTypeChange(contentType as ContentType)}
27 | >
28 | {icon}
29 |
30 | }
31 | />
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AccordionPrimitive from "@radix-ui/react-accordion"
5 | import { ChevronDown } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Accordion = AccordionPrimitive.Root
10 |
11 | const AccordionItem = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
20 | ))
21 | AccordionItem.displayName = "AccordionItem"
22 |
23 | const AccordionTrigger = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, children, ...props }, ref) => (
27 |
28 | svg]:rotate-180",
32 | className
33 | )}
34 | {...props}
35 | >
36 | {children}
37 |
38 |
39 |
40 | ))
41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
42 |
43 | const AccordionContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, children, ...props }, ref) => (
47 |
52 | {children}
53 |
54 | ))
55 |
56 | AccordionContent.displayName = AccordionPrimitive.Content.displayName
57 |
58 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
59 |
--------------------------------------------------------------------------------
/components/ui/advanced-settings.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Collapsible,
3 | CollapsibleContent,
4 | CollapsibleTrigger
5 | } from "@/components/ui/collapsible"
6 | import { IconChevronDown, IconChevronRight } from "@tabler/icons-react"
7 | import { FC, useState } from "react"
8 |
9 | interface AdvancedSettingsProps {
10 | children: React.ReactNode
11 | }
12 |
13 | export const AdvancedSettings: FC = ({ children }) => {
14 | const [isOpen, setIsOpen] = useState(
15 | false
16 | // localStorage.getItem("advanced-settings-open") === "true"
17 | )
18 |
19 | const handleOpenChange = (isOpen: boolean) => {
20 | setIsOpen(isOpen)
21 | // localStorage.setItem("advanced-settings-open", String(isOpen))
22 | }
23 |
24 | return (
25 |
26 |
27 |
28 |
Advanced Settings
29 | {isOpen ? (
30 |
31 | ) : (
32 |
33 | )}
34 |
35 |
36 |
37 | {children}
38 |
39 | )
40 | }
41 |
--------------------------------------------------------------------------------
/components/ui/alert.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const alertVariants = cva(
7 | "[&>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",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-background text-foreground",
12 | destructive:
13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive"
14 | }
15 | },
16 | defaultVariants: {
17 | variant: "default"
18 | }
19 | }
20 | )
21 |
22 | const Alert = React.forwardRef<
23 | HTMLDivElement,
24 | React.HTMLAttributes & VariantProps
25 | >(({ className, variant, ...props }, ref) => (
26 |
32 | ))
33 | Alert.displayName = "Alert"
34 |
35 | const AlertTitle = React.forwardRef<
36 | HTMLParagraphElement,
37 | React.HTMLAttributes
38 | >(({ className, ...props }, ref) => (
39 |
44 | ))
45 | AlertTitle.displayName = "AlertTitle"
46 |
47 | const AlertDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | AlertDescription.displayName = "AlertDescription"
58 |
59 | export { Alert, AlertTitle, AlertDescription }
60 |
--------------------------------------------------------------------------------
/components/ui/aspect-ratio.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
4 |
5 | const AspectRatio = AspectRatioPrimitive.Root
6 |
7 | export { AspectRatio }
8 |
--------------------------------------------------------------------------------
/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ))
21 | Avatar.displayName = AvatarPrimitive.Root.displayName
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ))
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ))
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
49 |
50 | export { Avatar, AvatarImage, AvatarFallback }
51 |
--------------------------------------------------------------------------------
/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const badgeVariants = cva(
7 | "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",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "bg-primary text-primary-foreground hover:bg-primary/80 border-transparent",
13 | secondary:
14 | "bg-secondary text-secondary-foreground hover:bg-secondary/80 border-transparent",
15 | destructive:
16 | "bg-destructive text-destructive-foreground hover:bg-destructive/80 border-transparent",
17 | outline: "text-foreground"
18 | }
19 | },
20 | defaultVariants: {
21 | variant: "default"
22 | }
23 | }
24 | )
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | )
34 | }
35 |
36 | export { Badge, badgeVariants }
37 |
--------------------------------------------------------------------------------
/components/ui/brand.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import Link from "next/link"
4 | import { FC } from "react"
5 | import { ChatbotUISVG } from "../icons/chatbotui-svg"
6 |
7 | interface BrandProps {
8 | theme?: "dark" | "light"
9 | }
10 |
11 | export const Brand: FC = ({ theme = "dark" }) => {
12 | return (
13 |
19 |
20 |
21 |
22 |
23 | Chatbot UI
24 |
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import { Slot } from "@radix-ui/react-slot"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 | import * as React from "react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "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",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border-input bg-background hover:bg-accent hover:text-accent-foreground border",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline"
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "size-10"
27 | }
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default"
32 | }
33 | }
34 | )
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button"
45 | return (
46 |
51 | )
52 | }
53 | )
54 | Button.displayName = "Button"
55 |
56 | export { Button, buttonVariants }
57 |
--------------------------------------------------------------------------------
/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ))
45 | CardTitle.displayName = "CardTitle"
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | CardDescription.displayName = "CardDescription"
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ))
65 | CardContent.displayName = "CardContent"
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ))
77 | CardFooter.displayName = "CardFooter"
78 |
79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
80 |
--------------------------------------------------------------------------------
/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
5 | import { Check } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Checkbox = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 |
24 |
25 |
26 |
27 | ))
28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName
29 |
30 | export { Checkbox }
31 |
--------------------------------------------------------------------------------
/components/ui/collapsible.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
4 |
5 | const Collapsible = CollapsiblePrimitive.Root
6 |
7 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
8 |
9 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
10 |
11 | export { Collapsible, CollapsibleTrigger, CollapsibleContent }
12 |
--------------------------------------------------------------------------------
/components/ui/file-icon.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | IconFile,
3 | IconFileText,
4 | IconFileTypeCsv,
5 | IconFileTypeDocx,
6 | IconFileTypePdf,
7 | IconJson,
8 | IconMarkdown,
9 | IconPhoto
10 | } from "@tabler/icons-react"
11 | import { FC } from "react"
12 |
13 | interface FileIconProps {
14 | type: string
15 | size?: number
16 | }
17 |
18 | export const FileIcon: FC = ({ type, size = 32 }) => {
19 | if (type.includes("image")) {
20 | return
21 | } else if (type.includes("pdf")) {
22 | return
23 | } else if (type.includes("csv")) {
24 | return
25 | } else if (type.includes("docx")) {
26 | return
27 | } else if (type.includes("plain")) {
28 | return
29 | } else if (type.includes("json")) {
30 | return
31 | } else if (type.includes("markdown")) {
32 | return
33 | } else {
34 | return
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/components/ui/file-preview.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 | import { Tables } from "@/supabase/types"
3 | import { ChatFile, MessageImage } from "@/types"
4 | import { IconFileFilled } from "@tabler/icons-react"
5 | import Image from "next/image"
6 | import { FC } from "react"
7 | import { DrawingCanvas } from "../utility/drawing-canvas"
8 | import { Dialog, DialogContent } from "./dialog"
9 |
10 | interface FilePreviewProps {
11 | type: "image" | "file" | "file_item"
12 | item: ChatFile | MessageImage | Tables<"file_items">
13 | isOpen: boolean
14 | onOpenChange: (isOpen: boolean) => void
15 | }
16 |
17 | export const FilePreview: FC = ({
18 | type,
19 | item,
20 | isOpen,
21 | onOpenChange
22 | }) => {
23 | return (
24 |
67 | )
68 | }
69 |
--------------------------------------------------------------------------------
/components/ui/hover-card.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const HoverCard = HoverCardPrimitive.Root
9 |
10 | const HoverCardTrigger = HoverCardPrimitive.Trigger
11 |
12 | const HoverCardContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
26 | ))
27 | HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
28 |
29 | export { HoverCard, HoverCardTrigger, HoverCardContent }
30 |
--------------------------------------------------------------------------------
/components/ui/image-picker.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image"
2 | import { ChangeEvent, FC, useState } from "react"
3 | import { toast } from "sonner"
4 | import { Input } from "./input"
5 |
6 | interface ImagePickerProps {
7 | src: string
8 | image: File | null
9 | onSrcChange: (src: string) => void
10 | onImageChange: (image: File) => void
11 | width?: number
12 | height?: number
13 | }
14 |
15 | const ImagePicker: FC = ({
16 | src,
17 | image,
18 | onSrcChange,
19 | onImageChange,
20 | width = 200,
21 | height = 200
22 | }) => {
23 | const [previewSrc, setPreviewSrc] = useState(src)
24 | const [previewImage, setPreviewImage] = useState(image)
25 |
26 | const handleImageSelect = (e: ChangeEvent) => {
27 | if (e.target.files) {
28 | const file = e.target.files[0]
29 |
30 | if (file.size > 6000000) {
31 | toast.error("Image must be less than 6MB!")
32 | return
33 | }
34 |
35 | const url = URL.createObjectURL(file)
36 |
37 | const img = new window.Image()
38 | img.src = url
39 |
40 | img.onload = () => {
41 | const canvas = document.createElement("canvas")
42 | const ctx = canvas.getContext("2d")
43 |
44 | if (!ctx) {
45 | toast.error("Unable to create canvas context.")
46 | return
47 | }
48 |
49 | const size = Math.min(img.width, img.height)
50 | canvas.width = size
51 | canvas.height = size
52 |
53 | ctx.drawImage(
54 | img,
55 | (img.width - size) / 2,
56 | (img.height - size) / 2,
57 | size,
58 | size,
59 | 0,
60 | 0,
61 | size,
62 | size
63 | )
64 |
65 | const squareUrl = canvas.toDataURL()
66 |
67 | setPreviewSrc(squareUrl)
68 | setPreviewImage(file)
69 | onSrcChange(squareUrl)
70 | onImageChange(file)
71 | }
72 | }
73 | }
74 |
75 | return (
76 |
77 | {previewSrc && (
78 |
86 | )}
87 |
88 |
94 |
95 | )
96 | }
97 |
98 | export default ImagePicker
99 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as LabelPrimitive from "@radix-ui/react-label"
4 | import { cva, type VariantProps } from "class-variance-authority"
5 | import * as React from "react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-semibold leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/components/ui/limit-display.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from "react"
2 |
3 | interface LimitDisplayProps {
4 | used: number
5 | limit: number
6 | }
7 |
8 | export const LimitDisplay: FC = ({ used, limit }) => {
9 | return (
10 |
11 | {used}/{limit}
12 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as PopoverPrimitive from "@radix-ui/react-popover"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Popover = PopoverPrimitive.Root
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ))
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
30 |
31 | export { Popover, PopoverTrigger, PopoverContent }
32 |
--------------------------------------------------------------------------------
/components/ui/progress.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ProgressPrimitive from "@radix-ui/react-progress"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Progress = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, value, ...props }, ref) => (
12 |
20 |
24 |
25 | ))
26 | Progress.displayName = ProgressPrimitive.Root.displayName
27 |
28 | export { Progress }
29 |
--------------------------------------------------------------------------------
/components/ui/radio-group.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
5 | import { Circle } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const RadioGroup = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => {
13 | return (
14 |
19 | )
20 | })
21 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
22 |
23 | const RadioGroupItem = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => {
27 | return (
28 |
36 |
37 |
38 |
39 |
40 | )
41 | })
42 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
43 |
44 | export { RadioGroup, RadioGroupItem }
45 |
--------------------------------------------------------------------------------
/components/ui/screen-loader.tsx:
--------------------------------------------------------------------------------
1 | import { IconLoader2 } from "@tabler/icons-react"
2 | import { FC } from "react"
3 |
4 | interface ScreenLoaderProps {}
5 |
6 | export const ScreenLoader: FC = () => {
7 | return (
8 |
9 |
10 |
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ScrollArea = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
17 |
18 | {children}
19 |
20 |
21 |
22 |
23 | ))
24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
25 |
26 | const ScrollBar = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, orientation = "vertical", ...props }, ref) => (
30 |
43 |
44 |
45 | ))
46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
47 |
48 | export { ScrollArea, ScrollBar }
49 |
--------------------------------------------------------------------------------
/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = "horizontal", decorative = true, ...props },
14 | ref
15 | ) => (
16 |
27 | )
28 | )
29 | Separator.displayName = SeparatorPrimitive.Root.displayName
30 |
31 | export { Separator }
32 |
--------------------------------------------------------------------------------
/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | )
13 | }
14 |
15 | export { Skeleton }
16 |
--------------------------------------------------------------------------------
/components/ui/slider.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as SliderPrimitive from "@radix-ui/react-slider"
4 | import * as React from "react"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Slider = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
21 |
22 |
23 |
24 |
25 | ))
26 | Slider.displayName = SliderPrimitive.Root.displayName
27 |
28 | export { Slider }
29 |
--------------------------------------------------------------------------------
/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useTheme } from "next-themes"
4 | import { Toaster as Sonner } from "sonner"
5 |
6 | type ToasterProps = React.ComponentProps
7 |
8 | const Toaster = ({ ...props }: ToasterProps) => {
9 | const { theme = "system" } = useTheme()
10 |
11 | return (
12 |
28 | )
29 | }
30 |
31 | export { Toaster }
32 |
--------------------------------------------------------------------------------
/components/ui/submit-button.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import React from "react"
4 | import { useFormStatus } from "react-dom"
5 | import { Button, ButtonProps } from "./button"
6 |
7 | const SubmitButton = React.forwardRef(
8 | (props, ref) => {
9 | const { pending } = useFormStatus()
10 |
11 | return
12 | }
13 | )
14 |
15 | SubmitButton.displayName = "SubmitButton"
16 |
17 | export { SubmitButton }
18 |
--------------------------------------------------------------------------------
/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SwitchPrimitives from "@radix-ui/react-switch"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Switch = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
25 |
26 | ))
27 | Switch.displayName = SwitchPrimitives.Root.displayName
28 |
29 | export { Switch }
30 |
--------------------------------------------------------------------------------
/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TabsPrimitive from "@radix-ui/react-tabs"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Tabs = TabsPrimitive.Root
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ))
23 | TabsList.displayName = TabsPrimitive.List.displayName
24 |
25 | const TabsTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
37 | ))
38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
39 |
40 | const TabsContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 | ))
53 | TabsContent.displayName = TabsPrimitive.Content.displayName
54 |
55 | export { Tabs, TabsList, TabsTrigger, TabsContent }
56 |
--------------------------------------------------------------------------------
/components/ui/textarea-autosize.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 | import { FC } from "react"
3 | import ReactTextareaAutosize from "react-textarea-autosize"
4 |
5 | interface TextareaAutosizeProps {
6 | value: string
7 | onValueChange: (value: string) => void
8 |
9 | textareaRef?: React.RefObject
10 | className?: string
11 |
12 | placeholder?: string
13 | minRows?: number
14 | maxRows?: number
15 | maxLength?: number
16 | onKeyDown?: (event: React.KeyboardEvent) => void
17 | onPaste?: (event: React.ClipboardEvent) => void
18 | onCompositionStart?: (event: React.CompositionEvent) => void
19 | onCompositionEnd?: (event: React.CompositionEvent) => void
20 | }
21 |
22 | export const TextareaAutosize: FC = ({
23 | value,
24 | onValueChange,
25 | textareaRef,
26 | className,
27 | placeholder = "",
28 | minRows = 1,
29 | maxRows = 6,
30 | maxLength,
31 | onKeyDown = () => {},
32 | onPaste = () => {},
33 | onCompositionStart = () => {},
34 | onCompositionEnd = () => {}
35 | }) => {
36 | return (
37 | maxRows ? minRows : maxRows}
45 | placeholder={placeholder}
46 | value={value}
47 | maxLength={maxLength}
48 | onChange={event => onValueChange(event.target.value)}
49 | onKeyDown={onKeyDown}
50 | onPaste={onPaste}
51 | onCompositionStart={onCompositionStart}
52 | onCompositionEnd={onCompositionEnd}
53 | />
54 | )
55 | }
56 |
--------------------------------------------------------------------------------
/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | )
20 | }
21 | )
22 | Textarea.displayName = "Textarea"
23 |
24 | export { Textarea }
25 |
--------------------------------------------------------------------------------
/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import {
4 | Toast,
5 | ToastClose,
6 | ToastDescription,
7 | ToastProvider,
8 | ToastTitle,
9 | ToastViewport
10 | } from "@/components/ui/toast"
11 | import { useToast } from "@/components/ui/use-toast"
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast()
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title}}
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | )
31 | })}
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/components/ui/toggle-group.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
5 | import { VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 | import { toggleVariants } from "@/components/ui/toggle"
9 |
10 | const ToggleGroupContext = React.createContext<
11 | VariantProps
12 | >({
13 | size: "default",
14 | variant: "default"
15 | })
16 |
17 | const ToggleGroup = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef &
20 | VariantProps
21 | >(({ className, variant, size, children, ...props }, ref) => (
22 |
27 |
28 | {children}
29 |
30 |
31 | ))
32 |
33 | ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName
34 |
35 | const ToggleGroupItem = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef &
38 | VariantProps
39 | >(({ className, children, variant, size, ...props }, ref) => {
40 | const context = React.useContext(ToggleGroupContext)
41 |
42 | return (
43 |
54 | {children}
55 |
56 | )
57 | })
58 |
59 | ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
60 |
61 | export { ToggleGroup, ToggleGroupItem }
62 |
--------------------------------------------------------------------------------
/components/ui/toggle.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TogglePrimitive from "@radix-ui/react-toggle"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const toggleVariants = cva(
10 | "ring-offset-background hover:bg-muted hover:text-muted-foreground focus-visible:ring-ring data-[state=on]:bg-accent data-[state=on]:text-accent-foreground inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
11 | {
12 | variants: {
13 | variant: {
14 | default: "bg-transparent",
15 | outline:
16 | "border-input hover:bg-accent hover:text-accent-foreground border bg-transparent"
17 | },
18 | size: {
19 | default: "h-10 px-3",
20 | sm: "h-9 px-2.5",
21 | lg: "h-11 px-5"
22 | }
23 | },
24 | defaultVariants: {
25 | variant: "default",
26 | size: "default"
27 | }
28 | }
29 | )
30 |
31 | const Toggle = React.forwardRef<
32 | React.ElementRef,
33 | React.ComponentPropsWithoutRef &
34 | VariantProps
35 | >(({ className, variant, size, ...props }, ref) => (
36 |
41 | ))
42 |
43 | Toggle.displayName = TogglePrimitive.Root.displayName
44 |
45 | export { Toggle, toggleVariants }
46 |
--------------------------------------------------------------------------------
/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider
9 |
10 | const Tooltip = TooltipPrimitive.Root
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ))
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
31 |
--------------------------------------------------------------------------------
/components/ui/with-tooltip.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from "react"
2 | import {
3 | Tooltip,
4 | TooltipContent,
5 | TooltipProvider,
6 | TooltipTrigger
7 | } from "./tooltip"
8 |
9 | interface WithTooltipProps {
10 | display: React.ReactNode
11 | trigger: React.ReactNode
12 |
13 | delayDuration?: number
14 | side?: "left" | "right" | "top" | "bottom"
15 | }
16 |
17 | export const WithTooltip: FC = ({
18 | display,
19 | trigger,
20 |
21 | delayDuration = 500,
22 | side = "right"
23 | }) => {
24 | return (
25 |
26 |
27 | {trigger}
28 |
29 | {display}
30 |
31 |
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/components/utility/alerts.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Popover,
3 | PopoverContent,
4 | PopoverTrigger
5 | } from "@/components/ui/popover"
6 | import { IconBell } from "@tabler/icons-react"
7 | import { FC } from "react"
8 | import { SIDEBAR_ICON_SIZE } from "../sidebar/sidebar-switcher"
9 |
10 | interface AlertsProps {}
11 |
12 | export const Alerts: FC = () => {
13 | return (
14 |
15 |
16 |
17 |
18 | {1 > 0 && (
19 |
20 | 1
21 |
22 | )}
23 |
24 |
25 |
26 | placeholder
27 |
28 |
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/components/utility/change-password.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { supabase } from "@/lib/supabase/browser-client"
4 | import { useRouter } from "next/navigation"
5 | import { FC, useState } from "react"
6 | import { Button } from "../ui/button"
7 | import {
8 | Dialog,
9 | DialogContent,
10 | DialogFooter,
11 | DialogHeader,
12 | DialogTitle
13 | } from "../ui/dialog"
14 | import { Input } from "../ui/input"
15 | import { toast } from "sonner"
16 |
17 | interface ChangePasswordProps {}
18 |
19 | export const ChangePassword: FC = () => {
20 | const router = useRouter()
21 |
22 | const [newPassword, setNewPassword] = useState("")
23 | const [confirmPassword, setConfirmPassword] = useState("")
24 |
25 | const handleResetPassword = async () => {
26 | if (!newPassword) return toast.info("Please enter your new password.")
27 |
28 | await supabase.auth.updateUser({ password: newPassword })
29 |
30 | toast.success("Password changed successfully.")
31 |
32 | return router.push("/login")
33 | }
34 |
35 | return (
36 |
63 | )
64 | }
65 |
--------------------------------------------------------------------------------
/components/utility/providers.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { TooltipProvider } from "@/components/ui/tooltip"
4 | import { ThemeProvider as NextThemesProvider } from "next-themes"
5 | import { ThemeProviderProps } from "next-themes/dist/types"
6 | import { FC } from "react"
7 |
8 | export const Providers: FC = ({ children, ...props }) => {
9 | return (
10 |
11 | {children}
12 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/components/utility/theme-switcher.tsx:
--------------------------------------------------------------------------------
1 | import { IconMoon, IconSun } from "@tabler/icons-react"
2 | import { useTheme } from "next-themes"
3 | import { FC } from "react"
4 | import { SIDEBAR_ICON_SIZE } from "../sidebar/sidebar-switcher"
5 | import { Button } from "../ui/button"
6 |
7 | interface ThemeSwitcherProps {}
8 |
9 | export const ThemeSwitcher: FC = () => {
10 | const { setTheme, theme } = useTheme()
11 |
12 | const handleChange = (theme: "dark" | "light") => {
13 | localStorage.setItem("theme", theme)
14 |
15 | setTheme(theme)
16 | }
17 |
18 | return (
19 |
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/components/utility/translations-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import initTranslations from "@/lib/i18n"
4 | import { createInstance } from "i18next"
5 | import { I18nextProvider } from "react-i18next"
6 |
7 | export default function TranslationsProvider({
8 | children,
9 | locale,
10 | namespaces,
11 | resources
12 | }: any) {
13 | const i18n = createInstance()
14 |
15 | initTranslations(locale, namespaces, i18n, resources)
16 |
17 | return {children}
18 | }
19 |
--------------------------------------------------------------------------------
/db/assistant-collections.ts:
--------------------------------------------------------------------------------
1 | import { supabase } from "@/lib/supabase/browser-client"
2 | import { TablesInsert } from "@/supabase/types"
3 |
4 | export const getAssistantCollectionsByAssistantId = async (
5 | assistantId: string
6 | ) => {
7 | const { data: assistantCollections, error } = await supabase
8 | .from("assistants")
9 | .select(
10 | `
11 | id,
12 | name,
13 | collections (*)
14 | `
15 | )
16 | .eq("id", assistantId)
17 | .single()
18 |
19 | if (!assistantCollections) {
20 | throw new Error(error.message)
21 | }
22 |
23 | return assistantCollections
24 | }
25 |
26 | export const createAssistantCollection = async (
27 | assistantCollection: TablesInsert<"assistant_collections">
28 | ) => {
29 | const { data: createdAssistantCollection, error } = await supabase
30 | .from("assistant_collections")
31 | .insert(assistantCollection)
32 | .select("*")
33 |
34 | if (!createdAssistantCollection) {
35 | throw new Error(error.message)
36 | }
37 |
38 | return createdAssistantCollection
39 | }
40 |
41 | export const createAssistantCollections = async (
42 | assistantCollections: TablesInsert<"assistant_collections">[]
43 | ) => {
44 | const { data: createdAssistantCollections, error } = await supabase
45 | .from("assistant_collections")
46 | .insert(assistantCollections)
47 | .select("*")
48 |
49 | if (!createdAssistantCollections) {
50 | throw new Error(error.message)
51 | }
52 |
53 | return createdAssistantCollections
54 | }
55 |
56 | export const deleteAssistantCollection = async (
57 | assistantId: string,
58 | collectionId: string
59 | ) => {
60 | const { error } = await supabase
61 | .from("assistant_collections")
62 | .delete()
63 | .eq("assistant_id", assistantId)
64 | .eq("collection_id", collectionId)
65 |
66 | if (error) throw new Error(error.message)
67 |
68 | return true
69 | }
70 |
--------------------------------------------------------------------------------
/db/assistant-files.ts:
--------------------------------------------------------------------------------
1 | import { supabase } from "@/lib/supabase/browser-client"
2 | import { TablesInsert } from "@/supabase/types"
3 |
4 | export const getAssistantFilesByAssistantId = async (assistantId: string) => {
5 | const { data: assistantFiles, error } = await supabase
6 | .from("assistants")
7 | .select(
8 | `
9 | id,
10 | name,
11 | files (*)
12 | `
13 | )
14 | .eq("id", assistantId)
15 | .single()
16 |
17 | if (!assistantFiles) {
18 | throw new Error(error.message)
19 | }
20 |
21 | return assistantFiles
22 | }
23 |
24 | export const createAssistantFile = async (
25 | assistantFile: TablesInsert<"assistant_files">
26 | ) => {
27 | const { data: createdAssistantFile, error } = await supabase
28 | .from("assistant_files")
29 | .insert(assistantFile)
30 | .select("*")
31 |
32 | if (!createdAssistantFile) {
33 | throw new Error(error.message)
34 | }
35 |
36 | return createdAssistantFile
37 | }
38 |
39 | export const createAssistantFiles = async (
40 | assistantFiles: TablesInsert<"assistant_files">[]
41 | ) => {
42 | const { data: createdAssistantFiles, error } = await supabase
43 | .from("assistant_files")
44 | .insert(assistantFiles)
45 | .select("*")
46 |
47 | if (!createdAssistantFiles) {
48 | throw new Error(error.message)
49 | }
50 |
51 | return createdAssistantFiles
52 | }
53 |
54 | export const deleteAssistantFile = async (
55 | assistantId: string,
56 | fileId: string
57 | ) => {
58 | const { error } = await supabase
59 | .from("assistant_files")
60 | .delete()
61 | .eq("assistant_id", assistantId)
62 | .eq("file_id", fileId)
63 |
64 | if (error) throw new Error(error.message)
65 |
66 | return true
67 | }
68 |
--------------------------------------------------------------------------------
/db/assistant-tools.ts:
--------------------------------------------------------------------------------
1 | import { supabase } from "@/lib/supabase/browser-client"
2 | import { TablesInsert } from "@/supabase/types"
3 |
4 | export const getAssistantToolsByAssistantId = async (assistantId: string) => {
5 | const { data: assistantTools, error } = await supabase
6 | .from("assistants")
7 | .select(
8 | `
9 | id,
10 | name,
11 | tools (*)
12 | `
13 | )
14 | .eq("id", assistantId)
15 | .single()
16 |
17 | if (!assistantTools) {
18 | throw new Error(error.message)
19 | }
20 |
21 | return assistantTools
22 | }
23 |
24 | export const createAssistantTool = async (
25 | assistantTool: TablesInsert<"assistant_tools">
26 | ) => {
27 | const { data: createdAssistantTool, error } = await supabase
28 | .from("assistant_tools")
29 | .insert(assistantTool)
30 | .select("*")
31 |
32 | if (!createdAssistantTool) {
33 | throw new Error(error.message)
34 | }
35 |
36 | return createdAssistantTool
37 | }
38 |
39 | export const createAssistantTools = async (
40 | assistantTools: TablesInsert<"assistant_tools">[]
41 | ) => {
42 | const { data: createdAssistantTools, error } = await supabase
43 | .from("assistant_tools")
44 | .insert(assistantTools)
45 | .select("*")
46 |
47 | if (!createdAssistantTools) {
48 | throw new Error(error.message)
49 | }
50 |
51 | return createdAssistantTools
52 | }
53 |
54 | export const deleteAssistantTool = async (
55 | assistantId: string,
56 | toolId: string
57 | ) => {
58 | const { error } = await supabase
59 | .from("assistant_tools")
60 | .delete()
61 | .eq("assistant_id", assistantId)
62 | .eq("tool_id", toolId)
63 |
64 | if (error) throw new Error(error.message)
65 |
66 | return true
67 | }
68 |
--------------------------------------------------------------------------------
/db/chat-files.ts:
--------------------------------------------------------------------------------
1 | import { supabase } from "@/lib/supabase/browser-client"
2 | import { TablesInsert } from "@/supabase/types"
3 |
4 | export const getChatFilesByChatId = async (chatId: string) => {
5 | const { data: chatFiles, error } = await supabase
6 | .from("chats")
7 | .select(
8 | `
9 | id,
10 | name,
11 | files (*)
12 | `
13 | )
14 | .eq("id", chatId)
15 | .single()
16 |
17 | if (!chatFiles) {
18 | throw new Error(error.message)
19 | }
20 |
21 | return chatFiles
22 | }
23 |
24 | export const createChatFile = async (chatFile: TablesInsert<"chat_files">) => {
25 | const { data: createdChatFile, error } = await supabase
26 | .from("chat_files")
27 | .insert(chatFile)
28 | .select("*")
29 |
30 | if (!createdChatFile) {
31 | throw new Error(error.message)
32 | }
33 |
34 | return createdChatFile
35 | }
36 |
37 | export const createChatFiles = async (
38 | chatFiles: TablesInsert<"chat_files">[]
39 | ) => {
40 | const { data: createdChatFiles, error } = await supabase
41 | .from("chat_files")
42 | .insert(chatFiles)
43 | .select("*")
44 |
45 | if (!createdChatFiles) {
46 | throw new Error(error.message)
47 | }
48 |
49 | return createdChatFiles
50 | }
51 |
--------------------------------------------------------------------------------
/db/chats.ts:
--------------------------------------------------------------------------------
1 | import { supabase } from "@/lib/supabase/browser-client"
2 | import { TablesInsert, TablesUpdate } from "@/supabase/types"
3 |
4 | export const getChatById = async (chatId: string) => {
5 | const { data: chat } = await supabase
6 | .from("chats")
7 | .select("*")
8 | .eq("id", chatId)
9 | .maybeSingle()
10 |
11 | return chat
12 | }
13 |
14 | export const getChatsByWorkspaceId = async (workspaceId: string) => {
15 | const { data: chats, error } = await supabase
16 | .from("chats")
17 | .select("*")
18 | .eq("workspace_id", workspaceId)
19 | .order("created_at", { ascending: false })
20 |
21 | if (!chats) {
22 | throw new Error(error.message)
23 | }
24 |
25 | return chats
26 | }
27 |
28 | export const createChat = async (chat: TablesInsert<"chats">) => {
29 | const { data: createdChat, error } = await supabase
30 | .from("chats")
31 | .insert([chat])
32 | .select("*")
33 | .single()
34 |
35 | if (error) {
36 | throw new Error(error.message)
37 | }
38 |
39 | return createdChat
40 | }
41 |
42 | export const createChats = async (chats: TablesInsert<"chats">[]) => {
43 | const { data: createdChats, error } = await supabase
44 | .from("chats")
45 | .insert(chats)
46 | .select("*")
47 |
48 | if (error) {
49 | throw new Error(error.message)
50 | }
51 |
52 | return createdChats
53 | }
54 |
55 | export const updateChat = async (
56 | chatId: string,
57 | chat: TablesUpdate<"chats">
58 | ) => {
59 | const { data: updatedChat, error } = await supabase
60 | .from("chats")
61 | .update(chat)
62 | .eq("id", chatId)
63 | .select("*")
64 | .single()
65 |
66 | if (error) {
67 | throw new Error(error.message)
68 | }
69 |
70 | return updatedChat
71 | }
72 |
73 | export const deleteChat = async (chatId: string) => {
74 | const { error } = await supabase.from("chats").delete().eq("id", chatId)
75 |
76 | if (error) {
77 | throw new Error(error.message)
78 | }
79 |
80 | return true
81 | }
82 |
--------------------------------------------------------------------------------
/db/collection-files.ts:
--------------------------------------------------------------------------------
1 | import { supabase } from "@/lib/supabase/browser-client"
2 | import { TablesInsert } from "@/supabase/types"
3 |
4 | export const getCollectionFilesByCollectionId = async (
5 | collectionId: string
6 | ) => {
7 | const { data: collectionFiles, error } = await supabase
8 | .from("collections")
9 | .select(
10 | `
11 | id,
12 | name,
13 | files ( id, name, type )
14 | `
15 | )
16 | .eq("id", collectionId)
17 | .single()
18 |
19 | if (!collectionFiles) {
20 | throw new Error(error.message)
21 | }
22 |
23 | return collectionFiles
24 | }
25 |
26 | export const createCollectionFile = async (
27 | collectionFile: TablesInsert<"collection_files">
28 | ) => {
29 | const { data: createdCollectionFile, error } = await supabase
30 | .from("collection_files")
31 | .insert(collectionFile)
32 | .select("*")
33 |
34 | if (!createdCollectionFile) {
35 | throw new Error(error.message)
36 | }
37 |
38 | return createdCollectionFile
39 | }
40 |
41 | export const createCollectionFiles = async (
42 | collectionFiles: TablesInsert<"collection_files">[]
43 | ) => {
44 | const { data: createdCollectionFiles, error } = await supabase
45 | .from("collection_files")
46 | .insert(collectionFiles)
47 | .select("*")
48 |
49 | if (!createdCollectionFiles) {
50 | throw new Error(error.message)
51 | }
52 |
53 | return createdCollectionFiles
54 | }
55 |
56 | export const deleteCollectionFile = async (
57 | collectionId: string,
58 | fileId: string
59 | ) => {
60 | const { error } = await supabase
61 | .from("collection_files")
62 | .delete()
63 | .eq("collection_id", collectionId)
64 | .eq("file_id", fileId)
65 |
66 | if (error) throw new Error(error.message)
67 |
68 | return true
69 | }
70 |
--------------------------------------------------------------------------------
/db/folders.ts:
--------------------------------------------------------------------------------
1 | import { supabase } from "@/lib/supabase/browser-client"
2 | import { TablesInsert, TablesUpdate } from "@/supabase/types"
3 |
4 | export const getFoldersByWorkspaceId = async (workspaceId: string) => {
5 | const { data: folders, error } = await supabase
6 | .from("folders")
7 | .select("*")
8 | .eq("workspace_id", workspaceId)
9 |
10 | if (!folders) {
11 | throw new Error(error.message)
12 | }
13 |
14 | return folders
15 | }
16 |
17 | export const createFolder = async (folder: TablesInsert<"folders">) => {
18 | const { data: createdFolder, error } = await supabase
19 | .from("folders")
20 | .insert([folder])
21 | .select("*")
22 | .single()
23 |
24 | if (error) {
25 | throw new Error(error.message)
26 | }
27 |
28 | return createdFolder
29 | }
30 |
31 | export const updateFolder = async (
32 | folderId: string,
33 | folder: TablesUpdate<"folders">
34 | ) => {
35 | const { data: updatedFolder, error } = await supabase
36 | .from("folders")
37 | .update(folder)
38 | .eq("id", folderId)
39 | .select("*")
40 | .single()
41 |
42 | if (error) {
43 | throw new Error(error.message)
44 | }
45 |
46 | return updatedFolder
47 | }
48 |
49 | export const deleteFolder = async (folderId: string) => {
50 | const { error } = await supabase.from("folders").delete().eq("id", folderId)
51 |
52 | if (error) {
53 | throw new Error(error.message)
54 | }
55 |
56 | return true
57 | }
58 |
--------------------------------------------------------------------------------
/db/index.ts:
--------------------------------------------------------------------------------
1 | import "./assistants"
2 | import "./chats"
3 | import "./file-items"
4 | import "./files"
5 | import "./folders"
6 | import "./messages"
7 | import "./presets"
8 | import "./profile"
9 | import "./prompts"
10 | import "./workspaces"
11 |
--------------------------------------------------------------------------------
/db/limits.ts:
--------------------------------------------------------------------------------
1 | // Profiles
2 | export const PROFILE_BIO_MAX = 500
3 | export const PROFILE_DISPLAY_NAME_MAX = 100
4 | export const PROFILE_CONTEXT_MAX = 1500
5 | export const PROFILE_USERNAME_MIN = 3
6 | export const PROFILE_USERNAME_MAX = 25
7 |
8 | // Workspaces
9 | export const WORKSPACE_NAME_MAX = 100
10 | export const WORKSPACE_DESCRIPTION_MAX = 500
11 | export const WORKSPACE_INSTRUCTIONS_MAX = 1500
12 |
13 | // Chats
14 |
15 | // Presets
16 | export const PRESET_NAME_MAX = 100
17 | export const PRESET_DESCRIPTION_MAX = 500
18 | export const PRESET_PROMPT_MAX = 100000
19 |
20 | // Prompts
21 | export const PROMPT_NAME_MAX = 100
22 | export const PROMPT_CONTENT_MAX = 100000
23 |
24 | // Files
25 | export const FILE_NAME_MAX = 100
26 | export const FILE_DESCRIPTION_MAX = 500
27 |
28 | // Collections
29 | export const COLLECTION_NAME_MAX = 100
30 | export const COLLECTION_DESCRIPTION_MAX = 500
31 |
32 | // Assistant
33 | export const ASSISTANT_NAME_MAX = 100
34 | export const ASSISTANT_DESCRIPTION_MAX = 500
35 | export const ASSISTANT_PROMPT_MAX = 100000
36 |
37 | // Tools
38 | export const TOOL_NAME_MAX = 100
39 | export const TOOL_DESCRIPTION_MAX = 500
40 |
41 | // Models
42 | export const MODEL_NAME_MAX = 100
43 | export const MODEL_DESCRIPTION_MAX = 500
44 |
--------------------------------------------------------------------------------
/db/message-file-items.ts:
--------------------------------------------------------------------------------
1 | import { supabase } from "@/lib/supabase/browser-client"
2 | import { TablesInsert } from "@/supabase/types"
3 |
4 | export const getMessageFileItemsByMessageId = async (messageId: string) => {
5 | const { data: messageFileItems, error } = await supabase
6 | .from("messages")
7 | .select(
8 | `
9 | id,
10 | file_items (*)
11 | `
12 | )
13 | .eq("id", messageId)
14 | .single()
15 |
16 | if (!messageFileItems) {
17 | throw new Error(error.message)
18 | }
19 |
20 | return messageFileItems
21 | }
22 |
23 | export const createMessageFileItems = async (
24 | messageFileItems: TablesInsert<"message_file_items">[]
25 | ) => {
26 | const { data: createdMessageFileItems, error } = await supabase
27 | .from("message_file_items")
28 | .insert(messageFileItems)
29 | .select("*")
30 |
31 | if (!createdMessageFileItems) {
32 | throw new Error(error.message)
33 | }
34 |
35 | return createdMessageFileItems
36 | }
37 |
--------------------------------------------------------------------------------
/db/profile.ts:
--------------------------------------------------------------------------------
1 | import { supabase } from "@/lib/supabase/browser-client"
2 | import { TablesInsert, TablesUpdate } from "@/supabase/types"
3 |
4 | export const getProfileByUserId = async (userId: string) => {
5 | const { data: profile, error } = await supabase
6 | .from("profiles")
7 | .select("*")
8 | .eq("user_id", userId)
9 | .single()
10 |
11 | if (!profile) {
12 | throw new Error(error.message)
13 | }
14 |
15 | return profile
16 | }
17 |
18 | export const getProfilesByUserId = async (userId: string) => {
19 | const { data: profiles, error } = await supabase
20 | .from("profiles")
21 | .select("*")
22 | .eq("user_id", userId)
23 |
24 | if (!profiles) {
25 | throw new Error(error.message)
26 | }
27 |
28 | return profiles
29 | }
30 |
31 | export const createProfile = async (profile: TablesInsert<"profiles">) => {
32 | const { data: createdProfile, error } = await supabase
33 | .from("profiles")
34 | .insert([profile])
35 | .select("*")
36 | .single()
37 |
38 | if (error) {
39 | throw new Error(error.message)
40 | }
41 |
42 | return createdProfile
43 | }
44 |
45 | export const updateProfile = async (
46 | profileId: string,
47 | profile: TablesUpdate<"profiles">
48 | ) => {
49 | const { data: updatedProfile, error } = await supabase
50 | .from("profiles")
51 | .update(profile)
52 | .eq("id", profileId)
53 | .select("*")
54 | .single()
55 |
56 | if (error) {
57 | throw new Error(error.message)
58 | }
59 |
60 | return updatedProfile
61 | }
62 |
63 | export const deleteProfile = async (profileId: string) => {
64 | const { error } = await supabase.from("profiles").delete().eq("id", profileId)
65 |
66 | if (error) {
67 | throw new Error(error.message)
68 | }
69 |
70 | return true
71 | }
72 |
--------------------------------------------------------------------------------
/db/storage/assistant-images.ts:
--------------------------------------------------------------------------------
1 | import { supabase } from "@/lib/supabase/browser-client"
2 | import { Tables } from "@/supabase/types"
3 |
4 | export const uploadAssistantImage = async (
5 | assistant: Tables<"assistants">,
6 | image: File
7 | ) => {
8 | const bucket = "assistant_images"
9 |
10 | const imageSizeLimit = 6000000 // 6MB
11 |
12 | if (image.size > imageSizeLimit) {
13 | throw new Error(`Image must be less than ${imageSizeLimit / 1000000}MB`)
14 | }
15 |
16 | const currentPath = assistant.image_path
17 | let filePath = `${assistant.user_id}/${assistant.id}/${Date.now()}`
18 |
19 | if (currentPath.length > 0) {
20 | const { error: deleteError } = await supabase.storage
21 | .from(bucket)
22 | .remove([currentPath])
23 |
24 | if (deleteError) {
25 | throw new Error("Error deleting old image")
26 | }
27 | }
28 |
29 | const { error } = await supabase.storage
30 | .from(bucket)
31 | .upload(filePath, image, {
32 | upsert: true
33 | })
34 |
35 | if (error) {
36 | throw new Error("Error uploading image")
37 | }
38 |
39 | return filePath
40 | }
41 |
42 | export const getAssistantImageFromStorage = async (filePath: string) => {
43 | try {
44 | const { data, error } = await supabase.storage
45 | .from("assistant_images")
46 | .createSignedUrl(filePath, 60 * 60 * 24) // 24hrs
47 |
48 | if (error) {
49 | throw new Error("Error downloading assistant image")
50 | }
51 |
52 | return data.signedUrl
53 | } catch (error) {
54 | console.error(error)
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/db/storage/files.ts:
--------------------------------------------------------------------------------
1 | import { supabase } from "@/lib/supabase/browser-client"
2 | import { toast } from "sonner"
3 |
4 | export const uploadFile = async (
5 | file: File,
6 | payload: {
7 | name: string
8 | user_id: string
9 | file_id: string
10 | }
11 | ) => {
12 | const SIZE_LIMIT = parseInt(
13 | process.env.NEXT_PUBLIC_USER_FILE_SIZE_LIMIT || "10000000"
14 | )
15 |
16 | if (file.size > SIZE_LIMIT) {
17 | throw new Error(
18 | `File must be less than ${Math.floor(SIZE_LIMIT / 1000000)}MB`
19 | )
20 | }
21 |
22 | const filePath = `${payload.user_id}/${Buffer.from(payload.file_id).toString("base64")}`
23 |
24 | const { error } = await supabase.storage
25 | .from("files")
26 | .upload(filePath, file, {
27 | upsert: true
28 | })
29 |
30 | if (error) {
31 | throw new Error("Error uploading file")
32 | }
33 |
34 | return filePath
35 | }
36 |
37 | export const deleteFileFromStorage = async (filePath: string) => {
38 | const { error } = await supabase.storage.from("files").remove([filePath])
39 |
40 | if (error) {
41 | toast.error("Failed to remove file!")
42 | return
43 | }
44 | }
45 |
46 | export const getFileFromStorage = async (filePath: string) => {
47 | const { data, error } = await supabase.storage
48 | .from("files")
49 | .createSignedUrl(filePath, 60 * 60 * 24) // 24hrs
50 |
51 | if (error) {
52 | console.error(`Error uploading file with path: ${filePath}`, error)
53 | throw new Error("Error downloading file")
54 | }
55 |
56 | return data.signedUrl
57 | }
58 |
--------------------------------------------------------------------------------
/db/storage/message-images.ts:
--------------------------------------------------------------------------------
1 | import { supabase } from "@/lib/supabase/browser-client"
2 |
3 | export const uploadMessageImage = async (path: string, image: File) => {
4 | const bucket = "message_images"
5 |
6 | const imageSizeLimit = 6000000 // 6MB
7 |
8 | if (image.size > imageSizeLimit) {
9 | throw new Error(`Image must be less than ${imageSizeLimit / 1000000}MB`)
10 | }
11 |
12 | const { error } = await supabase.storage.from(bucket).upload(path, image, {
13 | upsert: true
14 | })
15 |
16 | if (error) {
17 | throw new Error("Error uploading image")
18 | }
19 |
20 | return path
21 | }
22 |
23 | export const getMessageImageFromStorage = async (filePath: string) => {
24 | const { data, error } = await supabase.storage
25 | .from("message_images")
26 | .createSignedUrl(filePath, 60 * 60 * 24) // 24hrs
27 |
28 | if (error) {
29 | throw new Error("Error downloading message image")
30 | }
31 |
32 | return data.signedUrl
33 | }
34 |
--------------------------------------------------------------------------------
/db/storage/profile-images.ts:
--------------------------------------------------------------------------------
1 | import { supabase } from "@/lib/supabase/browser-client"
2 | import { Tables } from "@/supabase/types"
3 |
4 | export const uploadProfileImage = async (
5 | profile: Tables<"profiles">,
6 | image: File
7 | ) => {
8 | const bucket = "profile_images"
9 |
10 | const imageSizeLimit = 2000000 // 2MB
11 |
12 | if (image.size > imageSizeLimit) {
13 | throw new Error(`Image must be less than ${imageSizeLimit / 1000000}MB`)
14 | }
15 |
16 | const currentPath = profile.image_path
17 | let filePath = `${profile.user_id}/${Date.now()}`
18 |
19 | if (currentPath.length > 0) {
20 | const { error: deleteError } = await supabase.storage
21 | .from(bucket)
22 | .remove([currentPath])
23 |
24 | if (deleteError) {
25 | throw new Error("Error deleting old image")
26 | }
27 | }
28 |
29 | const { error } = await supabase.storage
30 | .from(bucket)
31 | .upload(filePath, image, {
32 | upsert: true
33 | })
34 |
35 | if (error) {
36 | throw new Error("Error uploading image")
37 | }
38 |
39 | const { data: getPublicUrlData } = supabase.storage
40 | .from(bucket)
41 | .getPublicUrl(filePath)
42 |
43 | return {
44 | path: filePath,
45 | url: getPublicUrlData.publicUrl
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/db/storage/workspace-images.ts:
--------------------------------------------------------------------------------
1 | import { supabase } from "@/lib/supabase/browser-client"
2 | import { Tables } from "@/supabase/types"
3 |
4 | export const uploadWorkspaceImage = async (
5 | workspace: Tables<"workspaces">,
6 | image: File
7 | ) => {
8 | const bucket = "workspace_images"
9 |
10 | const imageSizeLimit = 6000000 // 6MB
11 |
12 | if (image.size > imageSizeLimit) {
13 | throw new Error(`Image must be less than ${imageSizeLimit / 1000000}MB`)
14 | }
15 |
16 | const currentPath = workspace.image_path
17 | let filePath = `${workspace.user_id}/${workspace.id}/${Date.now()}`
18 |
19 | if (currentPath.length > 0) {
20 | const { error: deleteError } = await supabase.storage
21 | .from(bucket)
22 | .remove([currentPath])
23 |
24 | if (deleteError) {
25 | throw new Error("Error deleting old image")
26 | }
27 | }
28 |
29 | const { error } = await supabase.storage
30 | .from(bucket)
31 | .upload(filePath, image, {
32 | upsert: true
33 | })
34 |
35 | if (error) {
36 | throw new Error("Error uploading image")
37 | }
38 |
39 | return filePath
40 | }
41 |
42 | export const getWorkspaceImageFromStorage = async (filePath: string) => {
43 | try {
44 | const { data, error } = await supabase.storage
45 | .from("workspace_images")
46 | .createSignedUrl(filePath, 60 * 60 * 24) // 24hrs
47 |
48 | if (error) {
49 | throw new Error("Error downloading workspace image")
50 | }
51 |
52 | return data.signedUrl
53 | } catch (error) {
54 | console.error(error)
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/db/workspaces.ts:
--------------------------------------------------------------------------------
1 | import { supabase } from "@/lib/supabase/browser-client"
2 | import { TablesInsert, TablesUpdate } from "@/supabase/types"
3 |
4 | export const getHomeWorkspaceByUserId = async (userId: string) => {
5 | const { data: homeWorkspace, error } = await supabase
6 | .from("workspaces")
7 | .select("*")
8 | .eq("user_id", userId)
9 | .eq("is_home", true)
10 | .single()
11 |
12 | if (!homeWorkspace) {
13 | throw new Error(error.message)
14 | }
15 |
16 | return homeWorkspace.id
17 | }
18 |
19 | export const getWorkspaceById = async (workspaceId: string) => {
20 | const { data: workspace, error } = await supabase
21 | .from("workspaces")
22 | .select("*")
23 | .eq("id", workspaceId)
24 | .single()
25 |
26 | if (!workspace) {
27 | throw new Error(error.message)
28 | }
29 |
30 | return workspace
31 | }
32 |
33 | export const getWorkspacesByUserId = async (userId: string) => {
34 | const { data: workspaces, error } = await supabase
35 | .from("workspaces")
36 | .select("*")
37 | .eq("user_id", userId)
38 | .order("created_at", { ascending: false })
39 |
40 | if (!workspaces) {
41 | throw new Error(error.message)
42 | }
43 |
44 | return workspaces
45 | }
46 |
47 | export const createWorkspace = async (
48 | workspace: TablesInsert<"workspaces">
49 | ) => {
50 | const { data: createdWorkspace, error } = await supabase
51 | .from("workspaces")
52 | .insert([workspace])
53 | .select("*")
54 | .single()
55 |
56 | if (error) {
57 | throw new Error(error.message)
58 | }
59 |
60 | return createdWorkspace
61 | }
62 |
63 | export const updateWorkspace = async (
64 | workspaceId: string,
65 | workspace: TablesUpdate<"workspaces">
66 | ) => {
67 | const { data: updatedWorkspace, error } = await supabase
68 | .from("workspaces")
69 | .update(workspace)
70 | .eq("id", workspaceId)
71 | .select("*")
72 | .single()
73 |
74 | if (error) {
75 | throw new Error(error.message)
76 | }
77 |
78 | return updatedWorkspace
79 | }
80 |
81 | export const deleteWorkspace = async (workspaceId: string) => {
82 | const { error } = await supabase
83 | .from("workspaces")
84 | .delete()
85 | .eq("id", workspaceId)
86 |
87 | if (error) {
88 | throw new Error(error.message)
89 | }
90 |
91 | return true
92 | }
93 |
--------------------------------------------------------------------------------
/i18nConfig.js:
--------------------------------------------------------------------------------
1 | const i18nConfig = {
2 | defaultLocale: "en",
3 | locales: [
4 | "ar",
5 | "bn",
6 | "de",
7 | "en",
8 | "es",
9 | "fr",
10 | "he",
11 | "id",
12 | "it",
13 | "ja",
14 | "ko",
15 | "pt",
16 | "ru",
17 | "si",
18 | "sv",
19 | "te",
20 | "vi",
21 | "zh"
22 | ]
23 | }
24 |
25 | module.exports = i18nConfig
26 |
--------------------------------------------------------------------------------
/jest.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "jest"
2 | import nextJest from "next/jest.js"
3 |
4 | const createJestConfig = nextJest({
5 | // Provide the path to your Next.js app to load next.config.js and .env files in your test environment
6 | dir: "./"
7 | })
8 |
9 | // Add any custom config to be passed to Jest
10 | const config: Config = {
11 | coverageProvider: "v8",
12 | testEnvironment: "jsdom"
13 | // Add more setup options before each test is run
14 | // setupFilesAfterEnv: ['/jest.setup.ts'],
15 | }
16 |
17 | // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
18 | export default createJestConfig(config)
19 |
--------------------------------------------------------------------------------
/lib/blob-to-b64.ts:
--------------------------------------------------------------------------------
1 | export const convertBlobToBase64 = async (blob: Blob): Promise => {
2 | return new Promise((resolve, reject) => {
3 | const reader = new FileReader()
4 | reader.onloadend = () => resolve(reader.result as string)
5 | reader.onerror = reject
6 | reader.readAsDataURL(blob)
7 | })
8 | }
9 |
--------------------------------------------------------------------------------
/lib/consume-stream.ts:
--------------------------------------------------------------------------------
1 | export async function consumeReadableStream(
2 | stream: ReadableStream,
3 | callback: (chunk: string) => void,
4 | signal: AbortSignal
5 | ): Promise {
6 | const reader = stream.getReader()
7 | const decoder = new TextDecoder()
8 |
9 | signal.addEventListener("abort", () => reader.cancel(), { once: true })
10 |
11 | try {
12 | while (true) {
13 | const { done, value } = await reader.read()
14 |
15 | if (done) {
16 | break
17 | }
18 |
19 | if (value) {
20 | callback(decoder.decode(value, { stream: true }))
21 | }
22 | }
23 | } catch (error) {
24 | if (signal.aborted) {
25 | console.error("Stream reading was aborted:", error)
26 | } else {
27 | console.error("Error consuming stream:", error)
28 | }
29 | } finally {
30 | reader.releaseLock()
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/lib/envs.ts:
--------------------------------------------------------------------------------
1 | import { EnvKey } from "@/types/key-type"
2 |
3 | // returns true if the key is found in the environment variables
4 | export function isUsingEnvironmentKey(type: EnvKey) {
5 | return Boolean(process.env[type])
6 | }
7 |
--------------------------------------------------------------------------------
/lib/export-old-data.ts:
--------------------------------------------------------------------------------
1 | export function exportLocalStorageAsJSON() {
2 | const data: { [key: string]: string | null } = {}
3 | for (let i = 0; i < localStorage.length; i++) {
4 | const key = localStorage.key(i)
5 | if (key !== null) {
6 | data[key] = localStorage.getItem(key)
7 | }
8 | }
9 |
10 | const json = JSON.stringify(data)
11 | const blob = new Blob([json], { type: "application/json" })
12 | const url = URL.createObjectURL(blob)
13 |
14 | const a = document.createElement("a")
15 | a.href = url
16 | a.download = "chatbot-ui-data.json"
17 | document.body.appendChild(a)
18 | a.click()
19 | document.body.removeChild(a)
20 | URL.revokeObjectURL(url)
21 | }
22 |
--------------------------------------------------------------------------------
/lib/generate-local-embedding.ts:
--------------------------------------------------------------------------------
1 | import { pipeline } from "@xenova/transformers"
2 |
3 | export async function generateLocalEmbedding(content: string) {
4 | const generateEmbedding = await pipeline(
5 | "feature-extraction",
6 | "Xenova/all-MiniLM-L6-v2"
7 | )
8 |
9 | const output = await generateEmbedding(content, {
10 | pooling: "mean",
11 | normalize: true
12 | })
13 |
14 | const embedding = Array.from(output.data)
15 |
16 | return embedding
17 | }
18 |
--------------------------------------------------------------------------------
/lib/hooks/use-copy-to-clipboard.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react"
2 |
3 | export interface useCopyToClipboardProps {
4 | timeout?: number
5 | }
6 |
7 | export function useCopyToClipboard({
8 | timeout = 2000
9 | }: useCopyToClipboardProps) {
10 | const [isCopied, setIsCopied] = useState(false)
11 |
12 | const copyToClipboard = (value: string) => {
13 | if (typeof window === "undefined" || !navigator.clipboard?.writeText) {
14 | return
15 | }
16 |
17 | if (!value) {
18 | return
19 | }
20 |
21 | navigator.clipboard.writeText(value).then(() => {
22 | setIsCopied(true)
23 |
24 | setTimeout(() => {
25 | setIsCopied(false)
26 | }, timeout)
27 | })
28 | }
29 |
30 | return { isCopied, copyToClipboard }
31 | }
32 |
--------------------------------------------------------------------------------
/lib/hooks/use-hotkey.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react"
2 |
3 | const useHotkey = (key: string, callback: () => void): void => {
4 | useEffect(() => {
5 | const handleKeyDown = (event: KeyboardEvent): void => {
6 | if (event.metaKey && event.shiftKey && event.key === key) {
7 | event.preventDefault()
8 | callback()
9 | }
10 | }
11 |
12 | window.addEventListener("keydown", handleKeyDown)
13 |
14 | return () => {
15 | window.removeEventListener("keydown", handleKeyDown)
16 | }
17 | }, [key, callback])
18 | }
19 |
20 | export default useHotkey
21 |
--------------------------------------------------------------------------------
/lib/i18n.ts:
--------------------------------------------------------------------------------
1 | import i18nConfig from "@/i18nConfig"
2 | import { createInstance } from "i18next"
3 | import resourcesToBackend from "i18next-resources-to-backend"
4 | import { initReactI18next } from "react-i18next/initReactI18next"
5 |
6 | export default async function initTranslations(
7 | locale: any,
8 | namespaces: any,
9 | i18nInstance?: any,
10 | resources?: any
11 | ) {
12 | i18nInstance = i18nInstance || createInstance()
13 |
14 | i18nInstance.use(initReactI18next)
15 |
16 | if (!resources) {
17 | i18nInstance.use(
18 | resourcesToBackend(
19 | (language: string, namespace: string) =>
20 | import(`/public/locales/${language}/${namespace}.json`)
21 | )
22 | )
23 | }
24 |
25 | await i18nInstance.init({
26 | lng: locale,
27 | resources,
28 | fallbackLng: i18nConfig.defaultLocale,
29 | supportedLngs: i18nConfig.locales,
30 | defaultNS: namespaces[0],
31 | fallbackNS: namespaces[0],
32 | ns: namespaces,
33 | preload: resources ? [] : i18nConfig.locales
34 | })
35 |
36 | return {
37 | i18n: i18nInstance,
38 | resources: i18nInstance.services.resourceStore.data,
39 | t: i18nInstance.t
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/lib/models/llm/google-llm-list.ts:
--------------------------------------------------------------------------------
1 | import { LLM } from "@/types"
2 |
3 | const GOOGLE_PLATORM_LINK = "https://ai.google.dev/"
4 |
5 | // Google Models (UPDATED 12/22/23) -----------------------------
6 |
7 | // Gemini 1.5 Flash
8 | const GEMINI_1_5_FLASH: LLM = {
9 | modelId: "gemini-1.5-flash",
10 | modelName: "Gemini 1.5 Flash",
11 | provider: "google",
12 | hostedId: "gemini-1.5-flash",
13 | platformLink: GOOGLE_PLATORM_LINK,
14 | imageInput: true
15 | }
16 |
17 | // Gemini 1.5 Pro (UPDATED 05/28/24)
18 | const GEMINI_1_5_PRO: LLM = {
19 | modelId: "gemini-1.5-pro-latest",
20 | modelName: "Gemini 1.5 Pro",
21 | provider: "google",
22 | hostedId: "gemini-1.5-pro-latest",
23 | platformLink: GOOGLE_PLATORM_LINK,
24 | imageInput: true
25 | }
26 |
27 | // Gemini Pro (UPDATED 12/22/23)
28 | const GEMINI_PRO: LLM = {
29 | modelId: "gemini-pro",
30 | modelName: "Gemini Pro",
31 | provider: "google",
32 | hostedId: "gemini-pro",
33 | platformLink: GOOGLE_PLATORM_LINK,
34 | imageInput: false
35 | }
36 |
37 | // Gemini Pro Vision (UPDATED 12/22/23)
38 | const GEMINI_PRO_VISION: LLM = {
39 | modelId: "gemini-pro-vision",
40 | modelName: "Gemini Pro Vision",
41 | provider: "google",
42 | hostedId: "gemini-pro-vision",
43 | platformLink: GOOGLE_PLATORM_LINK,
44 | imageInput: true
45 | }
46 |
47 | export const GOOGLE_LLM_LIST: LLM[] = [GEMINI_PRO, GEMINI_PRO_VISION, GEMINI_1_5_PRO, GEMINI_1_5_FLASH]
48 |
--------------------------------------------------------------------------------
/lib/models/llm/groq-llm-list.ts:
--------------------------------------------------------------------------------
1 | import { LLM } from "@/types"
2 |
3 | const GROQ_PLATORM_LINK = "https://groq.com/"
4 |
5 | const LLaMA3_8B: LLM = {
6 | modelId: "llama3-8b-8192",
7 | modelName: "LLaMA3-8b-chat",
8 | provider: "groq",
9 | hostedId: "llama3-8b-8192",
10 | platformLink: GROQ_PLATORM_LINK,
11 | imageInput: false,
12 | pricing: {
13 | currency: "USD",
14 | unit: "1M tokens",
15 | inputCost: 0.05,
16 | outputCost: 0.1
17 | }
18 | }
19 |
20 | const LLaMA3_70B: LLM = {
21 | modelId: "llama3-70b-8192",
22 | modelName: "LLaMA3-70b-chat",
23 | provider: "groq",
24 | hostedId: "llama3-70b-4096",
25 | platformLink: GROQ_PLATORM_LINK,
26 | imageInput: false,
27 | pricing: {
28 | currency: "USD",
29 | unit: "1M tokens",
30 | inputCost: 0.59,
31 | outputCost: 0.79
32 | }
33 | }
34 |
35 | const MIXTRAL_8X7B: LLM = {
36 | modelId: "mixtral-8x7b-32768",
37 | modelName: "Mixtral-8x7b-Instruct-v0.1",
38 | provider: "groq",
39 | hostedId: "mixtral-8x7b-32768",
40 | platformLink: GROQ_PLATORM_LINK,
41 | imageInput: false,
42 | pricing: {
43 | currency: "USD",
44 | unit: "1M tokens",
45 | inputCost: 0.27,
46 | outputCost: 0.27
47 | }
48 | }
49 |
50 | const GEMMA_7B_IT: LLM = {
51 | modelId: "gemma-7b-it",
52 | modelName: "Gemma-7b-It",
53 | provider: "groq",
54 | hostedId: "gemma-7b-it",
55 | platformLink: GROQ_PLATORM_LINK,
56 | imageInput: false,
57 | pricing: {
58 | currency: "USD",
59 | unit: "1M tokens",
60 | inputCost: 0.15,
61 | outputCost: 0.15
62 | }
63 | }
64 |
65 | export const GROQ_LLM_LIST: LLM[] = [
66 | LLaMA3_8B,
67 | LLaMA3_70B,
68 | MIXTRAL_8X7B,
69 | GEMMA_7B_IT
70 | ]
71 |
--------------------------------------------------------------------------------
/lib/models/llm/llm-list.ts:
--------------------------------------------------------------------------------
1 | import { LLM } from "@/types"
2 | import { ANTHROPIC_LLM_LIST } from "./anthropic-llm-list"
3 | import { GOOGLE_LLM_LIST } from "./google-llm-list"
4 | import { MISTRAL_LLM_LIST } from "./mistral-llm-list"
5 | import { GROQ_LLM_LIST } from "./groq-llm-list"
6 | import { OPENAI_LLM_LIST } from "./openai-llm-list"
7 | import { PERPLEXITY_LLM_LIST } from "./perplexity-llm-list"
8 |
9 | export const LLM_LIST: LLM[] = [
10 | ...OPENAI_LLM_LIST,
11 | ...GOOGLE_LLM_LIST,
12 | ...MISTRAL_LLM_LIST,
13 | ...GROQ_LLM_LIST,
14 | ...PERPLEXITY_LLM_LIST,
15 | ...ANTHROPIC_LLM_LIST
16 | ]
17 |
18 | export const LLM_LIST_MAP: Record = {
19 | openai: OPENAI_LLM_LIST,
20 | azure: OPENAI_LLM_LIST,
21 | google: GOOGLE_LLM_LIST,
22 | mistral: MISTRAL_LLM_LIST,
23 | groq: GROQ_LLM_LIST,
24 | perplexity: PERPLEXITY_LLM_LIST,
25 | anthropic: ANTHROPIC_LLM_LIST
26 | }
27 |
--------------------------------------------------------------------------------
/lib/models/llm/mistral-llm-list.ts:
--------------------------------------------------------------------------------
1 | import { LLM } from "@/types"
2 |
3 | const MISTRAL_PLATORM_LINK = "https://docs.mistral.ai/"
4 |
5 | // Mistral Models (UPDATED 12/21/23) -----------------------------
6 |
7 | // Mistral 7B (UPDATED 12/21/23)
8 | const MISTRAL_7B: LLM = {
9 | modelId: "mistral-tiny",
10 | modelName: "Mistral Tiny",
11 | provider: "mistral",
12 | hostedId: "mistral-tiny",
13 | platformLink: MISTRAL_PLATORM_LINK,
14 | imageInput: false
15 | }
16 |
17 | // Mixtral (UPDATED 12/21/23)
18 | const MIXTRAL: LLM = {
19 | modelId: "mistral-small-latest",
20 | modelName: "Mistral Small",
21 | provider: "mistral",
22 | hostedId: "mistral-small-latest",
23 | platformLink: MISTRAL_PLATORM_LINK,
24 | imageInput: false,
25 | pricing: {
26 | currency: "USD",
27 | unit: "1M tokens",
28 | inputCost: 2,
29 | outputCost: 6
30 | }
31 | }
32 |
33 | // Mistral Medium (UPDATED 12/21/23)
34 | const MISTRAL_MEDIUM: LLM = {
35 | modelId: "mistral-medium-latest",
36 | modelName: "Mistral Medium",
37 | provider: "mistral",
38 | hostedId: "mistral-medium-latest",
39 | platformLink: MISTRAL_PLATORM_LINK,
40 | imageInput: false,
41 | pricing: {
42 | currency: "USD",
43 | unit: "1M tokens",
44 | inputCost: 2.7,
45 | outputCost: 8.1
46 | }
47 | }
48 |
49 | // Mistral Large (UPDATED 03/05/24)
50 | const MISTRAL_LARGE: LLM = {
51 | modelId: "mistral-large-latest",
52 | modelName: "Mistral Large",
53 | provider: "mistral",
54 | hostedId: "mistral-large-latest",
55 | platformLink: MISTRAL_PLATORM_LINK,
56 | imageInput: false,
57 | pricing: {
58 | currency: "USD",
59 | unit: "1M tokens",
60 | inputCost: 8,
61 | outputCost: 24
62 | }
63 | }
64 |
65 | export const MISTRAL_LLM_LIST: LLM[] = [
66 | MISTRAL_7B,
67 | MIXTRAL,
68 | MISTRAL_MEDIUM,
69 | MISTRAL_LARGE
70 | ]
71 |
--------------------------------------------------------------------------------
/lib/models/llm/openai-llm-list.ts:
--------------------------------------------------------------------------------
1 | import { LLM } from "@/types"
2 |
3 | const OPENAI_PLATORM_LINK = "https://platform.openai.com/docs/overview"
4 |
5 | // OpenAI Models (UPDATED 1/25/24) -----------------------------
6 | const GPT4o: LLM = {
7 | modelId: "gpt-4o",
8 | modelName: "GPT-4o",
9 | provider: "openai",
10 | hostedId: "gpt-4o",
11 | platformLink: OPENAI_PLATORM_LINK,
12 | imageInput: true,
13 | pricing: {
14 | currency: "USD",
15 | unit: "1M tokens",
16 | inputCost: 5,
17 | outputCost: 15
18 | }
19 | }
20 |
21 | // GPT-4 Turbo (UPDATED 1/25/24)
22 | const GPT4Turbo: LLM = {
23 | modelId: "gpt-4-turbo-preview",
24 | modelName: "GPT-4 Turbo",
25 | provider: "openai",
26 | hostedId: "gpt-4-turbo-preview",
27 | platformLink: OPENAI_PLATORM_LINK,
28 | imageInput: true,
29 | pricing: {
30 | currency: "USD",
31 | unit: "1M tokens",
32 | inputCost: 10,
33 | outputCost: 30
34 | }
35 | }
36 |
37 | // GPT-4 Vision (UPDATED 12/18/23)
38 | const GPT4Vision: LLM = {
39 | modelId: "gpt-4-vision-preview",
40 | modelName: "GPT-4 Vision",
41 | provider: "openai",
42 | hostedId: "gpt-4-vision-preview",
43 | platformLink: OPENAI_PLATORM_LINK,
44 | imageInput: true,
45 | pricing: {
46 | currency: "USD",
47 | unit: "1M tokens",
48 | inputCost: 10
49 | }
50 | }
51 |
52 | // GPT-4 (UPDATED 1/29/24)
53 | const GPT4: LLM = {
54 | modelId: "gpt-4",
55 | modelName: "GPT-4",
56 | provider: "openai",
57 | hostedId: "gpt-4",
58 | platformLink: OPENAI_PLATORM_LINK,
59 | imageInput: false,
60 | pricing: {
61 | currency: "USD",
62 | unit: "1M tokens",
63 | inputCost: 30,
64 | outputCost: 60
65 | }
66 | }
67 |
68 | // GPT-3.5 Turbo (UPDATED 1/25/24)
69 | const GPT3_5Turbo: LLM = {
70 | modelId: "gpt-3.5-turbo",
71 | modelName: "GPT-3.5 Turbo",
72 | provider: "openai",
73 | hostedId: "gpt-3.5-turbo",
74 | platformLink: OPENAI_PLATORM_LINK,
75 | imageInput: false,
76 | pricing: {
77 | currency: "USD",
78 | unit: "1M tokens",
79 | inputCost: 0.5,
80 | outputCost: 1.5
81 | }
82 | }
83 |
84 | export const OPENAI_LLM_LIST: LLM[] = [
85 | GPT4o,
86 | GPT4Turbo,
87 | GPT4Vision,
88 | GPT4,
89 | GPT3_5Turbo
90 | ]
91 |
--------------------------------------------------------------------------------
/lib/retrieval/processing/csv.ts:
--------------------------------------------------------------------------------
1 | import { FileItemChunk } from "@/types"
2 | import { encode } from "gpt-tokenizer"
3 | import { CSVLoader } from "langchain/document_loaders/fs/csv"
4 | import { RecursiveCharacterTextSplitter } from "langchain/text_splitter"
5 | import { CHUNK_OVERLAP, CHUNK_SIZE } from "."
6 |
7 | export const processCSV = async (csv: Blob): Promise => {
8 | const loader = new CSVLoader(csv)
9 | const docs = await loader.load()
10 | let completeText = docs.map(doc => doc.pageContent).join("\n\n")
11 |
12 | const splitter = new RecursiveCharacterTextSplitter({
13 | chunkSize: CHUNK_SIZE,
14 | chunkOverlap: CHUNK_OVERLAP,
15 | separators: ["\n\n"]
16 | })
17 | const splitDocs = await splitter.createDocuments([completeText])
18 |
19 | let chunks: FileItemChunk[] = []
20 |
21 | for (let i = 0; i < splitDocs.length; i++) {
22 | const doc = splitDocs[i]
23 |
24 | chunks.push({
25 | content: doc.pageContent,
26 | tokens: encode(doc.pageContent).length
27 | })
28 | }
29 |
30 | return chunks
31 | }
32 |
--------------------------------------------------------------------------------
/lib/retrieval/processing/docx.ts:
--------------------------------------------------------------------------------
1 | import { FileItemChunk } from "@/types"
2 | import { encode } from "gpt-tokenizer"
3 | import { RecursiveCharacterTextSplitter } from "langchain/text_splitter"
4 | import { CHUNK_OVERLAP, CHUNK_SIZE } from "."
5 |
6 | export const processDocX = async (text: string): Promise => {
7 | const splitter = new RecursiveCharacterTextSplitter({
8 | chunkSize: CHUNK_SIZE,
9 | chunkOverlap: CHUNK_OVERLAP
10 | })
11 | const splitDocs = await splitter.createDocuments([text])
12 |
13 | let chunks: FileItemChunk[] = []
14 |
15 | for (let i = 0; i < splitDocs.length; i++) {
16 | const doc = splitDocs[i]
17 |
18 | chunks.push({
19 | content: doc.pageContent,
20 | tokens: encode(doc.pageContent).length
21 | })
22 | }
23 |
24 | return chunks
25 | }
26 |
--------------------------------------------------------------------------------
/lib/retrieval/processing/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./csv"
2 | export * from "./docx"
3 | export * from "./json"
4 | export * from "./md"
5 | export * from "./pdf"
6 | export * from "./txt"
7 |
8 | export const CHUNK_SIZE = 4000
9 | export const CHUNK_OVERLAP = 200
10 |
--------------------------------------------------------------------------------
/lib/retrieval/processing/json.ts:
--------------------------------------------------------------------------------
1 | import { FileItemChunk } from "@/types"
2 | import { encode } from "gpt-tokenizer"
3 | import { JSONLoader } from "langchain/document_loaders/fs/json"
4 | import { RecursiveCharacterTextSplitter } from "langchain/text_splitter"
5 | import { CHUNK_OVERLAP, CHUNK_SIZE } from "."
6 |
7 | export const processJSON = async (json: Blob): Promise => {
8 | const loader = new JSONLoader(json)
9 | const docs = await loader.load()
10 | let completeText = docs.map(doc => doc.pageContent).join(" ")
11 |
12 | const splitter = new RecursiveCharacterTextSplitter({
13 | chunkSize: CHUNK_SIZE,
14 | chunkOverlap: CHUNK_OVERLAP
15 | })
16 | const splitDocs = await splitter.createDocuments([completeText])
17 |
18 | let chunks: FileItemChunk[] = []
19 |
20 | splitDocs.forEach(doc => {
21 | const docTokens = encode(doc.pageContent).length
22 | })
23 |
24 | for (let i = 0; i < splitDocs.length; i++) {
25 | const doc = splitDocs[i]
26 |
27 | chunks.push({
28 | content: doc.pageContent,
29 | tokens: encode(doc.pageContent).length
30 | })
31 | }
32 |
33 | return chunks
34 | }
35 |
--------------------------------------------------------------------------------
/lib/retrieval/processing/md.ts:
--------------------------------------------------------------------------------
1 | import { FileItemChunk } from "@/types"
2 | import { encode } from "gpt-tokenizer"
3 | import { RecursiveCharacterTextSplitter } from "langchain/text_splitter"
4 | import { CHUNK_OVERLAP, CHUNK_SIZE } from "."
5 |
6 | export const processMarkdown = async (
7 | markdown: Blob
8 | ): Promise => {
9 | const fileBuffer = Buffer.from(await markdown.arrayBuffer())
10 | const textDecoder = new TextDecoder("utf-8")
11 | const textContent = textDecoder.decode(fileBuffer)
12 |
13 | const splitter = RecursiveCharacterTextSplitter.fromLanguage("markdown", {
14 | chunkSize: CHUNK_SIZE,
15 | chunkOverlap: CHUNK_OVERLAP
16 | })
17 |
18 | const splitDocs = await splitter.createDocuments([textContent])
19 |
20 | let chunks: FileItemChunk[] = []
21 |
22 | for (let i = 0; i < splitDocs.length; i++) {
23 | const doc = splitDocs[i]
24 |
25 | chunks.push({
26 | content: doc.pageContent,
27 | tokens: encode(doc.pageContent).length
28 | })
29 | }
30 |
31 | return chunks
32 | }
33 |
--------------------------------------------------------------------------------
/lib/retrieval/processing/pdf.ts:
--------------------------------------------------------------------------------
1 | import { FileItemChunk } from "@/types"
2 | import { encode } from "gpt-tokenizer"
3 | import { PDFLoader } from "langchain/document_loaders/fs/pdf"
4 | import { RecursiveCharacterTextSplitter } from "langchain/text_splitter"
5 | import { CHUNK_OVERLAP, CHUNK_SIZE } from "."
6 |
7 | export const processPdf = async (pdf: Blob): Promise => {
8 | const loader = new PDFLoader(pdf)
9 | const docs = await loader.load()
10 | let completeText = docs.map(doc => doc.pageContent).join(" ")
11 |
12 | const splitter = new RecursiveCharacterTextSplitter({
13 | chunkSize: CHUNK_SIZE,
14 | chunkOverlap: CHUNK_OVERLAP
15 | })
16 | const splitDocs = await splitter.createDocuments([completeText])
17 |
18 | let chunks: FileItemChunk[] = []
19 |
20 | for (let i = 0; i < splitDocs.length; i++) {
21 | const doc = splitDocs[i]
22 |
23 | chunks.push({
24 | content: doc.pageContent,
25 | tokens: encode(doc.pageContent).length
26 | })
27 | }
28 |
29 | return chunks
30 | }
31 |
--------------------------------------------------------------------------------
/lib/retrieval/processing/txt.ts:
--------------------------------------------------------------------------------
1 | import { FileItemChunk } from "@/types"
2 | import { encode } from "gpt-tokenizer"
3 | import { RecursiveCharacterTextSplitter } from "langchain/text_splitter"
4 | import { CHUNK_OVERLAP, CHUNK_SIZE } from "."
5 |
6 | export const processTxt = async (txt: Blob): Promise => {
7 | const fileBuffer = Buffer.from(await txt.arrayBuffer())
8 | const textDecoder = new TextDecoder("utf-8")
9 | const textContent = textDecoder.decode(fileBuffer)
10 |
11 | const splitter = new RecursiveCharacterTextSplitter({
12 | chunkSize: CHUNK_SIZE,
13 | chunkOverlap: CHUNK_OVERLAP
14 | })
15 | const splitDocs = await splitter.createDocuments([textContent])
16 |
17 | let chunks: FileItemChunk[] = []
18 |
19 | for (let i = 0; i < splitDocs.length; i++) {
20 | const doc = splitDocs[i]
21 |
22 | chunks.push({
23 | content: doc.pageContent,
24 | tokens: encode(doc.pageContent).length
25 | })
26 | }
27 |
28 | return chunks
29 | }
30 |
--------------------------------------------------------------------------------
/lib/server/server-chat-helpers.ts:
--------------------------------------------------------------------------------
1 | import { Database, Tables } from "@/supabase/types"
2 | import { VALID_ENV_KEYS } from "@/types/valid-keys"
3 | import { createServerClient } from "@supabase/ssr"
4 | import { cookies } from "next/headers"
5 |
6 | export async function getServerProfile() {
7 | const cookieStore = cookies()
8 | const supabase = createServerClient(
9 | process.env.NEXT_PUBLIC_SUPABASE_URL!,
10 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
11 | {
12 | cookies: {
13 | get(name: string) {
14 | return cookieStore.get(name)?.value
15 | }
16 | }
17 | }
18 | )
19 |
20 | const user = (await supabase.auth.getUser()).data.user
21 | if (!user) {
22 | throw new Error("User not found")
23 | }
24 |
25 | const { data: profile } = await supabase
26 | .from("profiles")
27 | .select("*")
28 | .eq("user_id", user.id)
29 | .single()
30 |
31 | if (!profile) {
32 | throw new Error("Profile not found")
33 | }
34 |
35 | const profileWithKeys = addApiKeysToProfile(profile)
36 |
37 | return profileWithKeys
38 | }
39 |
40 | function addApiKeysToProfile(profile: Tables<"profiles">) {
41 | const apiKeys = {
42 | [VALID_ENV_KEYS.OPENAI_API_KEY]: "openai_api_key",
43 | [VALID_ENV_KEYS.ANTHROPIC_API_KEY]: "anthropic_api_key",
44 | [VALID_ENV_KEYS.GOOGLE_GEMINI_API_KEY]: "google_gemini_api_key",
45 | [VALID_ENV_KEYS.MISTRAL_API_KEY]: "mistral_api_key",
46 | [VALID_ENV_KEYS.GROQ_API_KEY]: "groq_api_key",
47 | [VALID_ENV_KEYS.PERPLEXITY_API_KEY]: "perplexity_api_key",
48 | [VALID_ENV_KEYS.AZURE_OPENAI_API_KEY]: "azure_openai_api_key",
49 | [VALID_ENV_KEYS.OPENROUTER_API_KEY]: "openrouter_api_key",
50 |
51 | [VALID_ENV_KEYS.OPENAI_ORGANIZATION_ID]: "openai_organization_id",
52 |
53 | [VALID_ENV_KEYS.AZURE_OPENAI_ENDPOINT]: "azure_openai_endpoint",
54 | [VALID_ENV_KEYS.AZURE_GPT_35_TURBO_NAME]: "azure_openai_35_turbo_id",
55 | [VALID_ENV_KEYS.AZURE_GPT_45_VISION_NAME]: "azure_openai_45_vision_id",
56 | [VALID_ENV_KEYS.AZURE_GPT_45_TURBO_NAME]: "azure_openai_45_turbo_id",
57 | [VALID_ENV_KEYS.AZURE_EMBEDDINGS_NAME]: "azure_openai_embeddings_id"
58 | }
59 |
60 | for (const [envKey, profileKey] of Object.entries(apiKeys)) {
61 | if (process.env[envKey]) {
62 | ;(profile as any)[profileKey] = process.env[envKey]
63 | }
64 | }
65 |
66 | return profile
67 | }
68 |
69 | export function checkApiKey(apiKey: string | null, keyName: string) {
70 | if (apiKey === null || apiKey === "") {
71 | throw new Error(`${keyName} API Key not found`)
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/lib/server/server-utils.ts:
--------------------------------------------------------------------------------
1 | export function createResponse(data: object, status: number): Response {
2 | return new Response(JSON.stringify(data), {
3 | status,
4 | headers: {
5 | "Content-Type": "application/json"
6 | }
7 | })
8 | }
9 |
--------------------------------------------------------------------------------
/lib/supabase/browser-client.ts:
--------------------------------------------------------------------------------
1 | import { Database } from "@/supabase/types"
2 | import { createBrowserClient } from "@supabase/ssr"
3 |
4 | export const supabase = createBrowserClient(
5 | process.env.NEXT_PUBLIC_SUPABASE_URL!,
6 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
7 | )
8 |
--------------------------------------------------------------------------------
/lib/supabase/client.ts:
--------------------------------------------------------------------------------
1 | import { createBrowserClient } from "@supabase/ssr"
2 |
3 | export const createClient = () =>
4 | createBrowserClient(
5 | process.env.NEXT_PUBLIC_SUPABASE_URL!,
6 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
7 | )
8 |
--------------------------------------------------------------------------------
/lib/supabase/middleware.ts:
--------------------------------------------------------------------------------
1 | import { createServerClient, type CookieOptions } from "@supabase/ssr"
2 | import { NextResponse, type NextRequest } from "next/server"
3 |
4 | export const createClient = (request: NextRequest) => {
5 | // Create an unmodified response
6 | let response = NextResponse.next({
7 | request: {
8 | headers: request.headers
9 | }
10 | })
11 |
12 | const supabase = createServerClient(
13 | process.env.NEXT_PUBLIC_SUPABASE_URL!,
14 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
15 | {
16 | cookies: {
17 | get(name: string) {
18 | return request.cookies.get(name)?.value
19 | },
20 | set(name: string, value: string, options: CookieOptions) {
21 | // If the cookie is updated, update the cookies for the request and response
22 | request.cookies.set({
23 | name,
24 | value,
25 | ...options
26 | })
27 | response = NextResponse.next({
28 | request: {
29 | headers: request.headers
30 | }
31 | })
32 | response.cookies.set({
33 | name,
34 | value,
35 | ...options
36 | })
37 | },
38 | remove(name: string, options: CookieOptions) {
39 | // If the cookie is removed, update the cookies for the request and response
40 | request.cookies.set({
41 | name,
42 | value: "",
43 | ...options
44 | })
45 | response = NextResponse.next({
46 | request: {
47 | headers: request.headers
48 | }
49 | })
50 | response.cookies.set({
51 | name,
52 | value: "",
53 | ...options
54 | })
55 | }
56 | }
57 | }
58 | )
59 |
60 | return { supabase, response }
61 | }
62 |
--------------------------------------------------------------------------------
/lib/supabase/server.ts:
--------------------------------------------------------------------------------
1 | import { createServerClient, type CookieOptions } from "@supabase/ssr"
2 | import { cookies } from "next/headers"
3 |
4 | export const createClient = (cookieStore: ReturnType) => {
5 | return createServerClient(
6 | process.env.NEXT_PUBLIC_SUPABASE_URL!,
7 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
8 | {
9 | cookies: {
10 | get(name: string) {
11 | return cookieStore.get(name)?.value
12 | },
13 | set(name: string, value: string, options: CookieOptions) {
14 | try {
15 | cookieStore.set({ name, value, ...options })
16 | } catch (error) {
17 | // The `set` method was called from a Server Component.
18 | // This can be ignored if you have middleware refreshing
19 | // user sessions.
20 | }
21 | },
22 | remove(name: string, options: CookieOptions) {
23 | try {
24 | cookieStore.set({ name, value: "", ...options })
25 | } catch (error) {
26 | // The `delete` method was called from a Server Component.
27 | // This can be ignored if you have middleware refreshing
28 | // user sessions.
29 | }
30 | }
31 | }
32 | }
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
8 | export function formatDate(input: string | number | Date): string {
9 | const date = new Date(input)
10 | return date.toLocaleDateString("en-US", {
11 | month: "long",
12 | day: "numeric",
13 | year: "numeric"
14 | })
15 | }
16 |
17 | export function getMediaTypeFromDataURL(dataURL: string): string | null {
18 | const matches = dataURL.match(/^data:([A-Za-z-+\/]+);base64/)
19 | return matches ? matches[1] : null
20 | }
21 |
22 | export function getBase64FromDataURL(dataURL: string): string | null {
23 | const matches = dataURL.match(/^data:[A-Za-z-+\/]+;base64,(.*)$/)
24 | return matches ? matches[1] : null
25 | }
26 |
--------------------------------------------------------------------------------
/license:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Mckay Wrigley
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/middleware.ts:
--------------------------------------------------------------------------------
1 | import { createClient } from "@/lib/supabase/middleware"
2 | import { i18nRouter } from "next-i18n-router"
3 | import { NextResponse, type NextRequest } from "next/server"
4 | import i18nConfig from "./i18nConfig"
5 |
6 | export async function middleware(request: NextRequest) {
7 | const i18nResult = i18nRouter(request, i18nConfig)
8 | if (i18nResult) return i18nResult
9 |
10 | try {
11 | const { supabase, response } = createClient(request)
12 |
13 | const session = await supabase.auth.getSession()
14 |
15 | const redirectToChat = session && request.nextUrl.pathname === "/"
16 |
17 | if (redirectToChat) {
18 | const { data: homeWorkspace, error } = await supabase
19 | .from("workspaces")
20 | .select("*")
21 | .eq("user_id", session.data.session?.user.id)
22 | .eq("is_home", true)
23 | .single()
24 |
25 | if (!homeWorkspace) {
26 | throw new Error(error?.message)
27 | }
28 |
29 | return NextResponse.redirect(
30 | new URL(`/${homeWorkspace.id}/chat`, request.url)
31 | )
32 | }
33 |
34 | return response
35 | } catch (e) {
36 | return NextResponse.next({
37 | request: {
38 | headers: request.headers
39 | }
40 | })
41 | }
42 | }
43 |
44 | export const config = {
45 | matcher: "/((?!api|static|.*\\..*|_next|auth).*)"
46 | }
47 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | const withBundleAnalyzer = require("@next/bundle-analyzer")({
2 | enabled: process.env.ANALYZE === "true"
3 | })
4 |
5 | const withPWA = require("next-pwa")({
6 | dest: "public"
7 | })
8 |
9 | module.exports = withBundleAnalyzer(
10 | withPWA({
11 | reactStrictMode: true,
12 | images: {
13 | remotePatterns: [
14 | {
15 | protocol: "http",
16 | hostname: "localhost"
17 | },
18 | {
19 | protocol: "http",
20 | hostname: "127.0.0.1"
21 | },
22 | {
23 | protocol: "https",
24 | hostname: "**"
25 | }
26 | ]
27 | },
28 | experimental: {
29 | serverComponentsExternalPackages: ["sharp", "onnxruntime-node"]
30 | }
31 | })
32 | )
33 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/prettier.config.cjs:
--------------------------------------------------------------------------------
1 | /** @type {import('prettier').Config} */
2 | module.exports = {
3 | endOfLine: 'lf',
4 | semi: false,
5 | useTabs: false,
6 | singleQuote: false,
7 | arrowParens: 'avoid',
8 | tabWidth: 2,
9 | trailingComma: 'none',
10 | importOrder: [
11 | '^.+\\.scss$',
12 | '^.+\\.css$',
13 | '^(react/(.*)$)|^(react$)',
14 | '^(next/(.*)$)|^(next$)',
15 | '',
16 | '',
17 | '^types$',
18 | '^@/types/(.*)$',
19 | '^@/config/(.*)$',
20 | '^@/lib/(.*)$',
21 | '^@/hooks/(.*)$',
22 | '^@/components/ui/(.*)$',
23 | '^@/components/(.*)$',
24 | '^@/registry/(.*)$',
25 | '^@/styles/(.*)$',
26 | '^@/app/(.*)$',
27 | '',
28 | '^[./]'
29 | ],
30 | importOrderSeparation: false,
31 | importOrderSortSpecifiers: true,
32 | importOrderBuiltinModulesToTop: true,
33 | importOrderParserPlugins: ['typescript', 'jsx', 'decorators-legacy'],
34 | importOrderMergeDuplicateImports: true,
35 | importOrderCombineTypeAndValueImports: true
36 | }
37 |
--------------------------------------------------------------------------------
/public/DARK_BRAND_LOGO.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mckaywrigley/chatbot-ui/81328b61d2a4ab597a7a057be70e785cf756d9f8/public/DARK_BRAND_LOGO.png
--------------------------------------------------------------------------------
/public/LIGHT_BRAND_LOGO.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mckaywrigley/chatbot-ui/81328b61d2a4ab597a7a057be70e785cf756d9f8/public/LIGHT_BRAND_LOGO.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mckaywrigley/chatbot-ui/81328b61d2a4ab597a7a057be70e785cf756d9f8/public/favicon.ico
--------------------------------------------------------------------------------
/public/icon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mckaywrigley/chatbot-ui/81328b61d2a4ab597a7a057be70e785cf756d9f8/public/icon-192x192.png
--------------------------------------------------------------------------------
/public/icon-256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mckaywrigley/chatbot-ui/81328b61d2a4ab597a7a057be70e785cf756d9f8/public/icon-256x256.png
--------------------------------------------------------------------------------
/public/icon-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mckaywrigley/chatbot-ui/81328b61d2a4ab597a7a057be70e785cf756d9f8/public/icon-512x512.png
--------------------------------------------------------------------------------
/public/locales/de/translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "Ask anything. Type \"/\" for prompts, \"@\" for files, and \"#\" for tools.": "Ask anything. Type \"/\" for prompts, \"@\" for files, and \"#\" for tools.",
3 | "Quick Settings": "Quick Settings"
4 | }
5 |
--------------------------------------------------------------------------------
/public/locales/en/translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "Ask anything. Type \"/\" for prompts, \"@\" for files, and \"#\" for tools.": "Ask anything. Type \"/\" for prompts, \"@\" for files, and \"#\" for tools."
3 | }
4 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Chatbot UI",
3 | "name": "Chatbot UI",
4 | "icons": [
5 | {
6 | "src": "/icon-192x192.png",
7 | "type": "image/ico",
8 | "sizes": "192x192"
9 | },
10 | {
11 | "src": "/icon-256x256.png",
12 | "type": "image/png",
13 | "sizes": "256x256"
14 | },
15 | {
16 | "src": "/icon-512x512.png",
17 | "type": "image/png",
18 | "sizes": "512x512",
19 | "purpose":"maskable"
20 | }
21 | ],
22 | "start_url": "/",
23 | "display": "standalone",
24 | "theme_color": "#000000",
25 | "background_color": "#000000"
26 | }
27 |
--------------------------------------------------------------------------------
/public/providers/groq.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mckaywrigley/chatbot-ui/81328b61d2a4ab597a7a057be70e785cf756d9f8/public/providers/groq.png
--------------------------------------------------------------------------------
/public/providers/meta.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mckaywrigley/chatbot-ui/81328b61d2a4ab597a7a057be70e785cf756d9f8/public/providers/meta.png
--------------------------------------------------------------------------------
/public/providers/mistral.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mckaywrigley/chatbot-ui/81328b61d2a4ab597a7a057be70e785cf756d9f8/public/providers/mistral.png
--------------------------------------------------------------------------------
/public/providers/perplexity.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mckaywrigley/chatbot-ui/81328b61d2a4ab597a7a057be70e785cf756d9f8/public/providers/perplexity.png
--------------------------------------------------------------------------------
/public/readme/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mckaywrigley/chatbot-ui/81328b61d2a4ab597a7a057be70e785cf756d9f8/public/readme/screenshot.png
--------------------------------------------------------------------------------
/public/worker-development.js:
--------------------------------------------------------------------------------
1 | /******/ (() => { // webpackBootstrap
2 | var __webpack_exports__ = {};
3 | self.__WB_DISABLE_DEV_LOGS = true;
4 | /******/ })()
5 | ;
--------------------------------------------------------------------------------
/supabase/.gitignore:
--------------------------------------------------------------------------------
1 | # Supabase
2 | .branches
3 | .temp
4 |
--------------------------------------------------------------------------------
/supabase/migrations/20240108234543_add_folders.sql:
--------------------------------------------------------------------------------
1 | --------------- FOLDERS ---------------
2 |
3 | -- TABLE --
4 |
5 | CREATE TABLE IF NOT EXISTS folders (
6 | -- ID
7 | id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
8 |
9 | -- RELATIONSHIPS
10 | user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
11 | workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
12 |
13 | -- METADATA
14 | created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
15 | updated_at TIMESTAMPTZ,
16 |
17 | -- REQUIRED
18 | name TEXT NOT NULL,
19 | description TEXT NOT NULL,
20 | type TEXT NOT NULL
21 | );
22 |
23 | -- INDEXES --
24 |
25 | CREATE INDEX folders_user_id_idx ON folders(user_id);
26 | CREATE INDEX folders_workspace_id_idx ON folders(workspace_id);
27 |
28 | -- RLS --
29 |
30 | ALTER TABLE folders ENABLE ROW LEVEL SECURITY;
31 |
32 | CREATE POLICY "Allow full access to own folders"
33 | ON folders
34 | USING (user_id = auth.uid())
35 | WITH CHECK (user_id = auth.uid());
36 |
37 | -- TRIGGERS --
38 |
39 | CREATE TRIGGER update_folders_updated_at
40 | BEFORE UPDATE ON folders
41 | FOR EACH ROW
42 | EXECUTE PROCEDURE update_updated_at_column();
--------------------------------------------------------------------------------
/supabase/migrations/20240115135033_add_openrouter.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE profiles
2 | ADD COLUMN openrouter_api_key TEXT CHECK (char_length(openrouter_api_key) <= 1000);
3 |
--------------------------------------------------------------------------------
/supabase/migrations/20240115172125_add_assistant_tools.sql:
--------------------------------------------------------------------------------
1 | --------------- ASSISTANT TOOLS ---------------
2 |
3 | -- TABLE --
4 |
5 | CREATE TABLE IF NOT EXISTS assistant_tools (
6 | -- REQUIRED RELATIONSHIPS
7 | user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
8 | assistant_id UUID NOT NULL REFERENCES assistants(id) ON DELETE CASCADE,
9 | tool_id UUID NOT NULL REFERENCES tools(id) ON DELETE CASCADE,
10 |
11 | PRIMARY KEY(assistant_id, tool_id),
12 |
13 | -- METADATA
14 | created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
15 | updated_at TIMESTAMPTZ
16 | );
17 |
18 | -- INDEXES --
19 |
20 | CREATE INDEX assistant_tools_user_id_idx ON assistant_tools(user_id);
21 | CREATE INDEX assistant_tools_assistant_id_idx ON assistant_tools(assistant_id);
22 | CREATE INDEX assistant_tools_tool_id_idx ON assistant_tools(tool_id);
23 |
24 | -- RLS --
25 |
26 | ALTER TABLE assistant_tools ENABLE ROW LEVEL SECURITY;
27 |
28 | CREATE POLICY "Allow full access to own assistant_tools"
29 | ON assistant_tools
30 | USING (user_id = auth.uid())
31 | WITH CHECK (user_id = auth.uid());
32 |
33 | -- TRIGGERS --
34 |
35 | CREATE TRIGGER update_assistant_tools_updated_at
36 | BEFORE UPDATE ON assistant_tools
37 | FOR EACH ROW
38 | EXECUTE PROCEDURE update_updated_at_column();
--------------------------------------------------------------------------------
/supabase/migrations/20240118224049_add_azure_embeddings.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE profiles
2 | ADD COLUMN azure_openai_embeddings_id TEXT CHECK (char_length(azure_openai_embeddings_id) <= 1000);
3 |
--------------------------------------------------------------------------------
/supabase/migrations/20240124234424_tool_improvements.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE tools
2 | ADD COLUMN custom_headers JSONB NOT NULL DEFAULT '{}',
3 | ADD COLUMN request_in_body BOOLEAN NOT NULL DEFAULT TRUE,
4 | ALTER COLUMN schema SET DEFAULT '{}';
5 |
--------------------------------------------------------------------------------
/supabase/migrations/20240129232644_add_workspace_images.sql:
--------------------------------------------------------------------------------
1 | -- ALTER TABLE --
2 |
3 | ALTER TABLE workspaces
4 | ADD COLUMN image_path TEXT DEFAULT '' NOT NULL CHECK (char_length(image_path) <= 1000);
5 |
6 | -- STORAGE --
7 |
8 | INSERT INTO storage.buckets (id, name, public) VALUES ('workspace_images', 'workspace_images', false);
9 |
10 | -- FUNCTIONS --
11 |
12 | CREATE OR REPLACE FUNCTION delete_old_workspace_image()
13 | RETURNS TRIGGER
14 | LANGUAGE 'plpgsql'
15 | SECURITY DEFINER
16 | AS $$
17 | DECLARE
18 | status INT;
19 | content TEXT;
20 | BEGIN
21 | IF TG_OP = 'DELETE' THEN
22 | SELECT
23 | INTO status, content
24 | result.status, result.content
25 | FROM public.delete_storage_object_from_bucket('workspace_images', OLD.image_path) AS result;
26 | IF status <> 200 THEN
27 | RAISE WARNING 'Could not delete workspace image: % %', status, content;
28 | END IF;
29 | END IF;
30 | IF TG_OP = 'DELETE' THEN
31 | RETURN OLD;
32 | END IF;
33 | RETURN NEW;
34 | END;
35 | $$;
36 |
37 | -- TRIGGERS --
38 |
39 | CREATE TRIGGER delete_old_workspace_image
40 | AFTER DELETE ON workspaces
41 | FOR EACH ROW
42 | EXECUTE PROCEDURE delete_old_workspace_image();
43 |
44 | -- POLICIES --
45 |
46 | CREATE OR REPLACE FUNCTION public.non_private_workspace_exists(p_name text)
47 | RETURNS boolean
48 | LANGUAGE sql
49 | SECURITY DEFINER
50 | AS $$
51 | SELECT EXISTS (
52 | SELECT 1
53 | FROM workspaces
54 | WHERE (id::text = (storage.filename(p_name))) AND sharing <> 'private'
55 | );
56 | $$;
57 |
58 | CREATE POLICY "Allow public read access on non-private workspace images"
59 | ON storage.objects FOR SELECT TO public
60 | USING (bucket_id = 'workspace_images' AND public.non_private_workspace_exists(name));
61 |
62 | CREATE POLICY "Allow insert access to own workspace images"
63 | ON storage.objects FOR INSERT TO authenticated
64 | WITH CHECK (bucket_id = 'workspace_images' AND (storage.foldername(name))[1] = auth.uid()::text);
65 |
66 | CREATE POLICY "Allow update access to own workspace images"
67 | ON storage.objects FOR UPDATE TO authenticated
68 | USING (bucket_id = 'workspace_images' AND (storage.foldername(name))[1] = auth.uid()::text);
69 |
70 | CREATE POLICY "Allow delete access to own workspace images"
71 | ON storage.objects FOR DELETE TO authenticated
72 | USING (bucket_id = 'workspace_images' AND (storage.foldername(name))[1] = auth.uid()::text);
73 |
--------------------------------------------------------------------------------
/supabase/migrations/20240212063532_add_at_assistants.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE messages ADD COLUMN assistant_id UUID REFERENCES assistants(id) ON DELETE CASCADE DEFAULT NULL;
2 |
--------------------------------------------------------------------------------
/supabase/migrations/20240213040255_remove_request_in_body_from_tools.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE tools
2 | DROP COLUMN request_in_body;
3 |
--------------------------------------------------------------------------------
/supabase/migrations/20240213085646_add_context_length_to_custom_models.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE models ADD COLUMN context_length INT NOT NULL DEFAULT 4096;
--------------------------------------------------------------------------------
/supabase/migrations/20240302004845_add_groq.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE profiles ADD COLUMN groq_api_key TEXT CHECK (char_length(groq_api_key) <= 1000);
2 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ['class'],
4 | content: [
5 | './pages/**/*.{ts,tsx}',
6 | './components/**/*.{ts,tsx}',
7 | './app/**/*.{ts,tsx}',
8 | './src/**/*.{ts,tsx}'
9 | ],
10 | theme: {
11 | container: {
12 | center: true,
13 | padding: '2rem',
14 | screens: {
15 | '2xl': '1400px'
16 | }
17 | },
18 | extend: {
19 | colors: {
20 | border: 'hsl(var(--border))',
21 | input: 'hsl(var(--input))',
22 | ring: 'hsl(var(--ring))',
23 | background: 'hsl(var(--background))',
24 | foreground: 'hsl(var(--foreground))',
25 | primary: {
26 | DEFAULT: 'hsl(var(--primary))',
27 | foreground: 'hsl(var(--primary-foreground))'
28 | },
29 | secondary: {
30 | DEFAULT: 'hsl(var(--secondary))',
31 | foreground: 'hsl(var(--secondary-foreground))'
32 | },
33 | destructive: {
34 | DEFAULT: 'hsl(var(--destructive))',
35 | foreground: 'hsl(var(--destructive-foreground))'
36 | },
37 | muted: {
38 | DEFAULT: 'hsl(var(--muted))',
39 | foreground: 'hsl(var(--muted-foreground))'
40 | },
41 | accent: {
42 | DEFAULT: 'hsl(var(--accent))',
43 | foreground: 'hsl(var(--accent-foreground))'
44 | },
45 | popover: {
46 | DEFAULT: 'hsl(var(--popover))',
47 | foreground: 'hsl(var(--popover-foreground))'
48 | },
49 | card: {
50 | DEFAULT: 'hsl(var(--card))',
51 | foreground: 'hsl(var(--card-foreground))'
52 | }
53 | },
54 | borderRadius: {
55 | lg: 'var(--radius)',
56 | md: 'calc(var(--radius) - 2px)',
57 | sm: 'calc(var(--radius) - 4px)'
58 | },
59 | keyframes: {
60 | 'accordion-down': {
61 | from: { height: 0 },
62 | to: { height: 'var(--radix-accordion-content-height)' }
63 | },
64 | 'accordion-up': {
65 | from: { height: 'var(--radix-accordion-content-height)' },
66 | to: { height: 0 }
67 | }
68 | },
69 | animation: {
70 | 'accordion-down': 'accordion-down 0.2s ease-out',
71 | 'accordion-up': 'accordion-up 0.2s ease-out'
72 | }
73 | }
74 | },
75 | plugins: [require('tailwindcss-animate'), require('@tailwindcss/typography')]
76 | }
77 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./*"]
23 | }
24 | },
25 | "include": [
26 | "next-env.d.ts",
27 | "**/*.ts",
28 | "**/*.tsx",
29 | ".next/types/**/*.ts",
30 | "./.next/types/**/*.ts",
31 | "i18nConfig.js"
32 | ],
33 | "exclude": ["node_modules"]
34 | }
35 |
--------------------------------------------------------------------------------
/types/announcement.ts:
--------------------------------------------------------------------------------
1 | export interface Announcement {
2 | id: string
3 | title: string
4 | content: string
5 | read: boolean
6 | link: string
7 | date: string
8 | }
9 |
--------------------------------------------------------------------------------
/types/assistant-retrieval-item.ts:
--------------------------------------------------------------------------------
1 | export interface AssistantRetrievalItem {
2 | id: string
3 | name: string
4 | type: string
5 | }
6 |
--------------------------------------------------------------------------------
/types/chat-file.tsx:
--------------------------------------------------------------------------------
1 | export interface ChatFile {
2 | id: string
3 | name: string
4 | type: string
5 | file: File | null
6 | }
7 |
--------------------------------------------------------------------------------
/types/chat-message.ts:
--------------------------------------------------------------------------------
1 | import { Tables } from "@/supabase/types"
2 |
3 | export interface ChatMessage {
4 | message: Tables<"messages">
5 | fileItems: string[]
6 | }
7 |
--------------------------------------------------------------------------------
/types/chat.ts:
--------------------------------------------------------------------------------
1 | import { Tables } from "@/supabase/types"
2 | import { ChatMessage, LLMID } from "."
3 |
4 | export interface ChatSettings {
5 | model: LLMID
6 | prompt: string
7 | temperature: number
8 | contextLength: number
9 | includeProfileContext: boolean
10 | includeWorkspaceInstructions: boolean
11 | embeddingsProvider: "openai" | "local"
12 | }
13 |
14 | export interface ChatPayload {
15 | chatSettings: ChatSettings
16 | workspaceInstructions: string
17 | chatMessages: ChatMessage[]
18 | assistant: Tables<"assistants"> | null
19 | messageFileItems: Tables<"file_items">[]
20 | chatFileItems: Tables<"file_items">[]
21 | }
22 |
23 | export interface ChatAPIPayload {
24 | chatSettings: ChatSettings
25 | messages: Tables<"messages">[]
26 | }
27 |
--------------------------------------------------------------------------------
/types/collection-file.ts:
--------------------------------------------------------------------------------
1 | export interface CollectionFile {
2 | id: string
3 | name: string
4 | type: string
5 | }
6 |
--------------------------------------------------------------------------------
/types/content-type.ts:
--------------------------------------------------------------------------------
1 | export type ContentType =
2 | | "chats"
3 | | "presets"
4 | | "prompts"
5 | | "files"
6 | | "collections"
7 | | "assistants"
8 | | "tools"
9 | | "models"
10 |
--------------------------------------------------------------------------------
/types/error-response.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod"
2 |
3 | export type ErrorResponse = {
4 | error: {
5 | code: number
6 | message: string
7 | }
8 | }
9 |
10 | export const ErrorResponseSchema = z.object({
11 | error: z.object({
12 | code: z.number({ coerce: true }).default(500),
13 | message: z.string().default("Internal Server Error")
14 | })
15 | })
16 |
--------------------------------------------------------------------------------
/types/file-item-chunk.ts:
--------------------------------------------------------------------------------
1 | export type FileItemChunk = {
2 | content: string
3 | tokens: number
4 | }
5 |
--------------------------------------------------------------------------------
/types/images/assistant-image.ts:
--------------------------------------------------------------------------------
1 | export interface AssistantImage {
2 | assistantId: string
3 | path: string
4 | base64: any // base64 image
5 | url: string
6 | }
7 |
--------------------------------------------------------------------------------
/types/images/message-image.ts:
--------------------------------------------------------------------------------
1 | export interface MessageImage {
2 | messageId: string
3 | path: string
4 | base64: any // base64 image
5 | url: string
6 | file: File | null
7 | }
8 |
--------------------------------------------------------------------------------
/types/images/workspace-image.ts:
--------------------------------------------------------------------------------
1 | export interface WorkspaceImage {
2 | workspaceId: string
3 | path: string
4 | base64: any // base64 image
5 | url: string
6 | }
7 |
--------------------------------------------------------------------------------
/types/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./announcement"
2 | export * from "./assistant-retrieval-item"
3 | export * from "./chat"
4 | export * from "./chat-file"
5 | export * from "./chat-message"
6 | export * from "./collection-file"
7 | export * from "./content-type"
8 | export * from "./file-item-chunk"
9 | export * from "./images/assistant-image"
10 | export * from "./images/message-image"
11 | export * from "./images/workspace-image"
12 | export * from "./llms"
13 | export * from "./models"
14 | export * from "./sharing"
15 | export * from "./sidebar-data"
16 |
--------------------------------------------------------------------------------
/types/key-type.ts:
--------------------------------------------------------------------------------
1 | export type EnvKey =
2 | | "OPENAI_API_KEY"
3 | | "ANTHROPIC_API_KEY"
4 | | "GOOGLE_GEMINI_API_KEY"
5 | | "MISTRAL_API_KEY"
6 | | "GROQ_API_KEY"
7 | | "PERPLEXITY_API_KEY"
8 | | "AZURE_OPENAI_API_KEY"
9 |
--------------------------------------------------------------------------------
/types/models.ts:
--------------------------------------------------------------------------------
1 | export type ModelProvider =
2 | | "openai"
3 | | "google"
4 | | "anthropic"
5 | | "mistral"
6 | | "groq"
7 | | "perplexity"
8 | | "ollama"
9 | | "openrouter"
10 | | "custom"
11 |
--------------------------------------------------------------------------------
/types/sharing.ts:
--------------------------------------------------------------------------------
1 | export type Sharing = "private" | "public" | "unlisted"
2 |
--------------------------------------------------------------------------------
/types/sidebar-data.ts:
--------------------------------------------------------------------------------
1 | import { Tables } from "@/supabase/types"
2 |
3 | export type DataListType =
4 | | Tables<"collections">[]
5 | | Tables<"chats">[]
6 | | Tables<"presets">[]
7 | | Tables<"prompts">[]
8 | | Tables<"files">[]
9 | | Tables<"assistants">[]
10 | | Tables<"tools">[]
11 | | Tables<"models">[]
12 |
13 | export type DataItemType =
14 | | Tables<"collections">
15 | | Tables<"chats">
16 | | Tables<"presets">
17 | | Tables<"prompts">
18 | | Tables<"files">
19 | | Tables<"assistants">
20 | | Tables<"tools">
21 | | Tables<"models">
22 |
--------------------------------------------------------------------------------
/types/valid-keys.ts:
--------------------------------------------------------------------------------
1 | export enum VALID_ENV_KEYS {
2 | OPENAI_API_KEY = "OPENAI_API_KEY",
3 | ANTHROPIC_API_KEY = "ANTHROPIC_API_KEY",
4 | GOOGLE_GEMINI_API_KEY = "GOOGLE_GEMINI_API_KEY",
5 | MISTRAL_API_KEY = "MISTRAL_API_KEY",
6 | GROQ_API_KEY = "GROQ_API_KEY",
7 | PERPLEXITY_API_KEY = "PERPLEXITY_API_KEY",
8 | AZURE_OPENAI_API_KEY = "AZURE_OPENAI_API_KEY",
9 | OPENROUTER_API_KEY = "OPENROUTER_API_KEY",
10 |
11 | OPENAI_ORGANIZATION_ID = "OPENAI_ORGANIZATION_ID",
12 |
13 | AZURE_OPENAI_ENDPOINT = "AZURE_OPENAI_ENDPOINT",
14 | AZURE_GPT_35_TURBO_NAME = "AZURE_GPT_35_TURBO_NAME",
15 | AZURE_GPT_45_VISION_NAME = "AZURE_GPT_45_VISION_NAME",
16 | AZURE_GPT_45_TURBO_NAME = "AZURE_GPT_45_TURBO_NAME",
17 | AZURE_EMBEDDINGS_NAME = "AZURE_EMBEDDINGS_NAME"
18 | }
19 |
--------------------------------------------------------------------------------
/worker/index.js:
--------------------------------------------------------------------------------
1 | self.__WB_DISABLE_DEV_LOGS = true
2 |
--------------------------------------------------------------------------------