├── .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 | 24 | 25 | Adjust retrieval settings.
} 29 | trigger={ 30 | 34 | } 35 | /> 36 | 37 | 38 | 39 |
40 | 45 | 46 | { 49 | setSourceCount(values[0]) 50 | }} 51 | min={1} 52 | max={10} 53 | step={1} 54 | /> 55 |
56 | 57 | 58 | 61 | 62 |
63 | 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 | Assistant 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 | 22 | 28 | 32 | 33 | 37 | 41 | 42 | 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 | 17 | 27 | 31 | 35 | 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 | 24 | 28 | 32 | 36 | 40 | 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 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | Delete {chat.name} 58 | 59 | 60 | Are you sure you want to delete this chat? 61 | 62 | 63 | 64 | 65 | 68 | 69 | 76 | 77 | 78 | 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 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | Edit Chat 56 | 57 | 58 |
59 | 60 | 61 | setName(e.target.value)} /> 62 |
63 | 64 | 65 | 68 | 69 | 72 | 73 |
74 |
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 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | Edit Folder 56 | 57 | 58 |
59 | 60 | 61 | setName(e.target.value)} /> 62 |
63 | 64 | 65 | 68 | 69 | 72 | 73 |
74 |
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 | 25 | 31 | {(() => { 32 | if (type === "image") { 33 | const imageItem = item as MessageImage 34 | 35 | return imageItem.file ? ( 36 | 37 | ) : ( 38 | File image 49 | ) 50 | } else if (type === "file_item") { 51 | const fileItem = item as Tables<"file_items"> 52 | return ( 53 |
54 |
{fileItem.content}
55 |
56 | ) 57 | } else if (type === "file") { 58 | return ( 59 |
60 | 61 |
62 | ) 63 | } 64 | })()} 65 |
66 |
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 | {"Image"} 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