From f4ec4df77f650fa650535c6b934d4a1775cf491a Mon Sep 17 00:00:00 2001
From: Mckay Wrigley
Date: Mon, 8 Jan 2024 17:11:48 -0700
Subject: [PATCH] Initial commit
---
.env.local.example | 9 +
.eslintrc.json | 25 +
.github/funding.yaml | 3 +
.gitignore | 40 +
.husky/pre-commit | 4 +
README.md | 82 +
app/api/chat/anthropic/route.ts | 47 +
app/api/chat/azure/route.ts | 74 +
app/api/chat/google/route.ts | 72 +
app/api/chat/mistral/route.ts | 71 +
app/api/chat/openai/route.ts | 46 +
app/api/chat/perplexity/route.ts | 68 +
app/api/command/route.ts | 54 +
app/api/localhost/ollama/route.ts | 39 +
app/api/retrieval/process/route.ts | 150 +
app/api/retrieval/retrieve/route.ts | 108 +
app/api/username/available/route.ts | 37 +
app/api/username/get/route.ts | 38 +
app/auth/callback/route.ts | 16 +
app/chat/[chatid]/page.tsx | 7 +
app/chat/layout.tsx | 34 +
app/chat/page.tsx | 55 +
app/globals.css | 104 +
app/help/page.tsx | 7 +
app/layout.tsx | 44 +
app/loading.tsx | 9 +
app/login/page.tsx | 130 +
app/page.tsx | 57 +
app/setup/page.tsx | 294 +
app/share/assistant/[id]/page.tsx | 65 +
app/share/chat/[id]/page.tsx | 73 +
app/share/collection/[id]/page.tsx | 75 +
app/share/file/[id]/page.tsx | 72 +
app/share/preset/[id]/page.tsx | 22 +
app/share/prompt/[id]/page.tsx | 22 +
components.json | 16 +
components/chat/chat-command-input.tsx | 43 +
components/chat/chat-files-display.tsx | 193 +
components/chat/chat-help.tsx | 178 +
components/chat/chat-helpers/index.ts | 472 +
.../chat/chat-hooks/use-chat-handler.tsx | 249 +
.../chat-hooks/use-prompt-and-command.tsx | 70 +
components/chat/chat-hooks/use-scroll.tsx | 79 +
.../chat-hooks/use-select-file-handler.tsx | 160 +
components/chat/chat-input.tsx | 171 +
components/chat/chat-messages.tsx | 34 +
components/chat/chat-scroll-buttons.tsx | 41 +
components/chat/chat-secondary-buttons.tsx | 35 +
components/chat/chat-settings.tsx | 73 +
components/chat/chat-ui.tsx | 207 +
components/chat/file-picker.tsx | 125 +
components/chat/prompt-picker.tsx | 193 +
components/chat/quick-setting-option.tsx | 63 +
components/chat/quick-settings.tsx | 233 +
components/icons/anthropic-svg.tsx | 44 +
components/icons/chatbotui-svg.tsx | 37 +
components/icons/google-svg.tsx | 42 +
components/icons/openai-svg.tsx | 31 +
components/messages/message-actions.tsx | 117 +
components/messages/message-codeblock.tsx | 135 +
.../messages/message-markdown-memoized.tsx | 9 +
components/messages/message-markdown.tsx | 62 +
components/messages/message-replies.tsx | 51 +
components/messages/message.tsx | 345 +
components/models/model-icon.tsx | 146 +
components/models/model-option.tsx | 50 +
components/models/model-select.tsx | 190 +
components/setup/api-step.tsx | 197 +
components/setup/finish-step.tsx | 15 +
components/setup/profile-step.tsx | 198 +
components/setup/step-container.tsx | 89 +
components/setup/workspace-step.tsx | 52 +
components/sharing/add-to-workspace.tsx | 209 +
components/sharing/share-assistant.tsx | 23 +
components/sharing/share-chat.tsx | 149 +
components/sharing/share-collection.tsx | 23 +
components/sharing/share-file.tsx | 44 +
components/sharing/share-header.tsx | 25 +
components/sharing/share-item.tsx | 196 +
components/sharing/share-message.tsx | 42 +
components/sharing/share-page.tsx | 71 +
components/sharing/share-preset.tsx | 73 +
components/sharing/share-prompt.tsx | 26 +
.../sidebar/items/all/sidebar-create-item.tsx | 198 +
.../sidebar/items/all/sidebar-delete-item.tsx | 129 +
.../items/all/sidebar-display-item.tsx | 122 +
.../sidebar/items/all/sidebar-update-item.tsx | 382 +
.../items/assistants/assistant-item.tsx | 137 +
.../items/assistants/create-assistant.tsx | 133 +
components/sidebar/items/chat/chat-item.tsx | 96 +
components/sidebar/items/chat/delete-chat.tsx | 80 +
components/sidebar/items/chat/update-chat.tsx | 76 +
.../collections/collection-file-picker.tsx | 155 +
.../items/collections/collection-item.tsx | 113 +
.../items/collections/create-collection.tsx | 115 +
.../sidebar/items/files/create-file.tsx | 92 +
components/sidebar/items/files/file-item.tsx | 93 +
.../sidebar/items/folders/delete-folder.tsx | 79 +
.../sidebar/items/folders/folder-item.tsx | 111 +
.../sidebar/items/folders/update-folder.tsx | 76 +
.../sidebar/items/presets/create-preset.tsx | 88 +
.../sidebar/items/presets/preset-item.tsx | 77 +
.../sidebar/items/prompts/create-prompt.tsx | 67 +
.../sidebar/items/prompts/prompt-item.tsx | 52 +
components/sidebar/sidebar-content.tsx | 47 +
components/sidebar/sidebar-create-buttons.tsx | 128 +
components/sidebar/sidebar-data-list.tsx | 241 +
components/sidebar/sidebar-search.tsx | 23 +
components/sidebar/sidebar-switch-item.tsx | 33 +
components/sidebar/sidebar-switcher.tsx | 79 +
components/sidebar/sidebar.tsx | 94 +
components/ui/accordion.tsx | 58 +
components/ui/advanced-settings.tsx | 40 +
components/ui/alert-dialog.tsx | 141 +
components/ui/alert.tsx | 59 +
components/ui/aspect-ratio.tsx | 7 +
components/ui/avatar.tsx | 50 +
components/ui/badge.tsx | 36 +
components/ui/brand.tsx | 26 +
components/ui/button.tsx | 56 +
components/ui/calendar.tsx | 66 +
components/ui/card.tsx | 79 +
components/ui/chat-settings-form.tsx | 239 +
components/ui/checkbox.tsx | 30 +
components/ui/collapsible.tsx | 11 +
components/ui/command.tsx | 155 +
components/ui/context-menu.tsx | 200 +
components/ui/dashboard.tsx | 131 +
components/ui/dialog.tsx | 121 +
components/ui/dropdown-menu.tsx | 200 +
components/ui/file-icon.tsx | 39 +
components/ui/file-preview.tsx | 65 +
components/ui/form.tsx | 176 +
components/ui/hover-card.tsx | 29 +
components/ui/image-picker.tsx | 200 +
components/ui/input.tsx | 25 +
components/ui/label.tsx | 26 +
components/ui/limit-display.tsx | 14 +
components/ui/menubar.tsx | 236 +
components/ui/navigation-menu.tsx | 128 +
components/ui/popover.tsx | 31 +
components/ui/progress.tsx | 28 +
components/ui/radio-group.tsx | 44 +
components/ui/screen-loader.tsx | 12 +
components/ui/scroll-area.tsx | 48 +
components/ui/select.tsx | 160 +
components/ui/separator.tsx | 31 +
components/ui/sheet.tsx | 139 +
components/ui/skeleton.tsx | 15 +
components/ui/slider.tsx | 28 +
components/ui/sonner.tsx | 31 +
components/ui/switch.tsx | 29 +
components/ui/table.tsx | 117 +
components/ui/tabs.tsx | 55 +
components/ui/textarea-autosize.tsx | 44 +
components/ui/textarea.tsx | 24 +
components/ui/toast.tsx | 127 +
components/ui/toaster.tsx | 35 +
components/ui/toggle-group.tsx | 61 +
components/ui/toggle.tsx | 45 +
components/ui/tooltip.tsx | 30 +
components/ui/use-toast.ts | 189 +
components/ui/with-tooltip.tsx | 33 +
components/utility/alerts.tsx | 30 +
components/utility/announcements.tsx | 177 +
components/utility/command-k.tsx | 87 +
components/utility/global-state.tsx | 350 +
components/utility/import.tsx | 283 +
components/utility/profile-settings.tsx | 501 +
components/utility/providers.tsx | 14 +
components/utility/share-menu.tsx | 148 +
components/utility/theme-switcher.tsx | 32 +
components/utility/workspace-switcher.tsx | 152 +
components/workspace/assign-workspaces.tsx | 159 +
components/workspace/delete-workspace.tsx | 100 +
components/workspace/workspace-settings.tsx | 223 +
context/context.tsx | 184 +
db/assistants.ts | 184 +
db/chat-files.ts | 50 +
db/chats.ts | 81 +
db/collection-files.ts | 54 +
db/collections.ts | 184 +
db/files.ts | 208 +
db/folders.ts | 57 +
db/index.ts | 10 +
db/limits.ts | 35 +
db/message-file-items.ts | 36 +
db/messages.ts | 104 +
db/presets.ts | 177 +
db/profile.ts | 71 +
db/prompts.ts | 177 +
db/storage/assistant-images.ts | 45 +
db/storage/files.ts | 52 +
db/storage/message-images.ts | 33 +
db/storage/profile-images.ts | 33 +
db/workspaces.ts | 77 +
lib/blob-to-b64.ts | 8 +
lib/build-prompt.ts | 267 +
lib/canvas-preview.ts | 58 +
lib/chat-setting-limits.ts | 94 +
lib/consume-stream.ts | 32 +
lib/effects/use-debounce.ts | 18 +
lib/generate-local-embedding.ts | 17 +
lib/hooks/use-copy-to-clipboard.tsx | 31 +
lib/hooks/use-hotkey.tsx | 20 +
lib/is-model-locked.ts | 24 +
lib/models/llm/anthropic-llm-list.ts | 28 +
lib/models/llm/google-llm-list.ts | 27 +
lib/models/llm/llm-list.ts | 15 +
lib/models/llm/mistral-llm-list.ts | 37 +
lib/models/llm/openai-llm-list.ts | 37 +
lib/models/llm/perplexity-llm-list.ts | 31 +
lib/retrieval/processing/csv.ts | 32 +
lib/retrieval/processing/doc.ts | 35 +
lib/retrieval/processing/docx.ts | 35 +
lib/retrieval/processing/html.ts | 35 +
lib/retrieval/processing/index.ts | 1 +
lib/retrieval/processing/json.ts | 35 +
lib/retrieval/processing/md.ts | 37 +
lib/retrieval/processing/pdf.ts | 50 +
lib/retrieval/processing/txt.ts | 34 +
lib/server-chat-helpers.ts | 41 +
lib/supabase/browser-client.ts | 7 +
lib/supabase/client.ts | 7 +
lib/supabase/middleware.ts | 61 +
lib/supabase/server.ts | 34 +
lib/utils.ts | 15 +
license | 21 +
middleware.ts | 18 +
next.config.js | 27 +
package-lock.json | 11318 ++++++++++++++++
package.json | 114 +
postcss.config.js | 6 +
prettier.config.cjs | 36 +
public/DARK_BRAND_LOGO.png | Bin 0 -> 7831 bytes
public/LIGHT_BRAND_LOGO.png | Bin 0 -> 7831 bytes
public/providers/meta.png | Bin 0 -> 16022 bytes
public/providers/mistral.png | Bin 0 -> 64929 bytes
public/providers/perplexity.png | Bin 0 -> 24298 bytes
public/readme/screenshot.png | Bin 0 -> 151168 bytes
supabase/.gitignore | 3 +
supabase/config.toml | 117 +
supabase/migrations/20240108234540_setup.sql | 81 +
.../20240108234541_add_profiles.sql | 171 +
.../20240108234542_add_workspaces.sql | 91 +
.../migrations/20240108234543_add_folders.sql | 42 +
.../migrations/20240108234544_add_files.sql | 157 +
.../20240108234545_add_file_items.sql | 117 +
.../migrations/20240108234546_add_presets.sql | 96 +
.../20240108234547_add_assistants.sql | 161 +
.../migrations/20240108234548_add_chats.sql | 96 +
.../20240108234549_add_messages.sql | 173 +
.../migrations/20240108234550_add_prompts.sql | 89 +
.../20240108234551_add_collections.sql | 145 +
supabase/types.ts | 1446 ++
tailwind.config.ts | 76 +
tsconfig.json | 27 +
types/announcement.ts | 8 +
types/assistant-image.ts | 6 +
types/chat-file.tsx | 6 +
types/chat-message.ts | 6 +
types/chat.ts | 25 +
types/collection-file.ts | 5 +
types/content-type.ts | 7 +
types/file-item-chunk.ts | 4 +
types/index.ts | 12 +
types/llms.ts | 44 +
types/message-image.ts | 7 +
types/models.ts | 8 +
types/sharing.ts | 1 +
types/sidebar-data.ts | 17 +
271 files changed, 34589 insertions(+)
create mode 100644 .env.local.example
create mode 100644 .eslintrc.json
create mode 100644 .github/funding.yaml
create mode 100644 .gitignore
create mode 100755 .husky/pre-commit
create mode 100644 README.md
create mode 100644 app/api/chat/anthropic/route.ts
create mode 100644 app/api/chat/azure/route.ts
create mode 100644 app/api/chat/google/route.ts
create mode 100644 app/api/chat/mistral/route.ts
create mode 100644 app/api/chat/openai/route.ts
create mode 100644 app/api/chat/perplexity/route.ts
create mode 100644 app/api/command/route.ts
create mode 100644 app/api/localhost/ollama/route.ts
create mode 100644 app/api/retrieval/process/route.ts
create mode 100644 app/api/retrieval/retrieve/route.ts
create mode 100644 app/api/username/available/route.ts
create mode 100644 app/api/username/get/route.ts
create mode 100644 app/auth/callback/route.ts
create mode 100644 app/chat/[chatid]/page.tsx
create mode 100644 app/chat/layout.tsx
create mode 100644 app/chat/page.tsx
create mode 100644 app/globals.css
create mode 100644 app/help/page.tsx
create mode 100644 app/layout.tsx
create mode 100644 app/loading.tsx
create mode 100644 app/login/page.tsx
create mode 100644 app/page.tsx
create mode 100644 app/setup/page.tsx
create mode 100644 app/share/assistant/[id]/page.tsx
create mode 100644 app/share/chat/[id]/page.tsx
create mode 100644 app/share/collection/[id]/page.tsx
create mode 100644 app/share/file/[id]/page.tsx
create mode 100644 app/share/preset/[id]/page.tsx
create mode 100644 app/share/prompt/[id]/page.tsx
create mode 100644 components.json
create mode 100644 components/chat/chat-command-input.tsx
create mode 100644 components/chat/chat-files-display.tsx
create mode 100644 components/chat/chat-help.tsx
create mode 100644 components/chat/chat-helpers/index.ts
create mode 100644 components/chat/chat-hooks/use-chat-handler.tsx
create mode 100644 components/chat/chat-hooks/use-prompt-and-command.tsx
create mode 100644 components/chat/chat-hooks/use-scroll.tsx
create mode 100644 components/chat/chat-hooks/use-select-file-handler.tsx
create mode 100644 components/chat/chat-input.tsx
create mode 100644 components/chat/chat-messages.tsx
create mode 100644 components/chat/chat-scroll-buttons.tsx
create mode 100644 components/chat/chat-secondary-buttons.tsx
create mode 100644 components/chat/chat-settings.tsx
create mode 100644 components/chat/chat-ui.tsx
create mode 100644 components/chat/file-picker.tsx
create mode 100644 components/chat/prompt-picker.tsx
create mode 100644 components/chat/quick-setting-option.tsx
create mode 100644 components/chat/quick-settings.tsx
create mode 100644 components/icons/anthropic-svg.tsx
create mode 100644 components/icons/chatbotui-svg.tsx
create mode 100644 components/icons/google-svg.tsx
create mode 100644 components/icons/openai-svg.tsx
create mode 100644 components/messages/message-actions.tsx
create mode 100644 components/messages/message-codeblock.tsx
create mode 100644 components/messages/message-markdown-memoized.tsx
create mode 100644 components/messages/message-markdown.tsx
create mode 100644 components/messages/message-replies.tsx
create mode 100644 components/messages/message.tsx
create mode 100644 components/models/model-icon.tsx
create mode 100644 components/models/model-option.tsx
create mode 100644 components/models/model-select.tsx
create mode 100644 components/setup/api-step.tsx
create mode 100644 components/setup/finish-step.tsx
create mode 100644 components/setup/profile-step.tsx
create mode 100644 components/setup/step-container.tsx
create mode 100644 components/setup/workspace-step.tsx
create mode 100644 components/sharing/add-to-workspace.tsx
create mode 100644 components/sharing/share-assistant.tsx
create mode 100644 components/sharing/share-chat.tsx
create mode 100644 components/sharing/share-collection.tsx
create mode 100644 components/sharing/share-file.tsx
create mode 100644 components/sharing/share-header.tsx
create mode 100644 components/sharing/share-item.tsx
create mode 100644 components/sharing/share-message.tsx
create mode 100644 components/sharing/share-page.tsx
create mode 100644 components/sharing/share-preset.tsx
create mode 100644 components/sharing/share-prompt.tsx
create mode 100644 components/sidebar/items/all/sidebar-create-item.tsx
create mode 100644 components/sidebar/items/all/sidebar-delete-item.tsx
create mode 100644 components/sidebar/items/all/sidebar-display-item.tsx
create mode 100644 components/sidebar/items/all/sidebar-update-item.tsx
create mode 100644 components/sidebar/items/assistants/assistant-item.tsx
create mode 100644 components/sidebar/items/assistants/create-assistant.tsx
create mode 100644 components/sidebar/items/chat/chat-item.tsx
create mode 100644 components/sidebar/items/chat/delete-chat.tsx
create mode 100644 components/sidebar/items/chat/update-chat.tsx
create mode 100644 components/sidebar/items/collections/collection-file-picker.tsx
create mode 100644 components/sidebar/items/collections/collection-item.tsx
create mode 100644 components/sidebar/items/collections/create-collection.tsx
create mode 100644 components/sidebar/items/files/create-file.tsx
create mode 100644 components/sidebar/items/files/file-item.tsx
create mode 100644 components/sidebar/items/folders/delete-folder.tsx
create mode 100644 components/sidebar/items/folders/folder-item.tsx
create mode 100644 components/sidebar/items/folders/update-folder.tsx
create mode 100644 components/sidebar/items/presets/create-preset.tsx
create mode 100644 components/sidebar/items/presets/preset-item.tsx
create mode 100644 components/sidebar/items/prompts/create-prompt.tsx
create mode 100644 components/sidebar/items/prompts/prompt-item.tsx
create mode 100644 components/sidebar/sidebar-content.tsx
create mode 100644 components/sidebar/sidebar-create-buttons.tsx
create mode 100644 components/sidebar/sidebar-data-list.tsx
create mode 100644 components/sidebar/sidebar-search.tsx
create mode 100644 components/sidebar/sidebar-switch-item.tsx
create mode 100644 components/sidebar/sidebar-switcher.tsx
create mode 100644 components/sidebar/sidebar.tsx
create mode 100644 components/ui/accordion.tsx
create mode 100644 components/ui/advanced-settings.tsx
create mode 100644 components/ui/alert-dialog.tsx
create mode 100644 components/ui/alert.tsx
create mode 100644 components/ui/aspect-ratio.tsx
create mode 100644 components/ui/avatar.tsx
create mode 100644 components/ui/badge.tsx
create mode 100644 components/ui/brand.tsx
create mode 100644 components/ui/button.tsx
create mode 100644 components/ui/calendar.tsx
create mode 100644 components/ui/card.tsx
create mode 100644 components/ui/chat-settings-form.tsx
create mode 100644 components/ui/checkbox.tsx
create mode 100644 components/ui/collapsible.tsx
create mode 100644 components/ui/command.tsx
create mode 100644 components/ui/context-menu.tsx
create mode 100644 components/ui/dashboard.tsx
create mode 100644 components/ui/dialog.tsx
create mode 100644 components/ui/dropdown-menu.tsx
create mode 100644 components/ui/file-icon.tsx
create mode 100644 components/ui/file-preview.tsx
create mode 100644 components/ui/form.tsx
create mode 100644 components/ui/hover-card.tsx
create mode 100644 components/ui/image-picker.tsx
create mode 100644 components/ui/input.tsx
create mode 100644 components/ui/label.tsx
create mode 100644 components/ui/limit-display.tsx
create mode 100644 components/ui/menubar.tsx
create mode 100644 components/ui/navigation-menu.tsx
create mode 100644 components/ui/popover.tsx
create mode 100644 components/ui/progress.tsx
create mode 100644 components/ui/radio-group.tsx
create mode 100644 components/ui/screen-loader.tsx
create mode 100644 components/ui/scroll-area.tsx
create mode 100644 components/ui/select.tsx
create mode 100644 components/ui/separator.tsx
create mode 100644 components/ui/sheet.tsx
create mode 100644 components/ui/skeleton.tsx
create mode 100644 components/ui/slider.tsx
create mode 100644 components/ui/sonner.tsx
create mode 100644 components/ui/switch.tsx
create mode 100644 components/ui/table.tsx
create mode 100644 components/ui/tabs.tsx
create mode 100644 components/ui/textarea-autosize.tsx
create mode 100644 components/ui/textarea.tsx
create mode 100644 components/ui/toast.tsx
create mode 100644 components/ui/toaster.tsx
create mode 100644 components/ui/toggle-group.tsx
create mode 100644 components/ui/toggle.tsx
create mode 100644 components/ui/tooltip.tsx
create mode 100644 components/ui/use-toast.ts
create mode 100644 components/ui/with-tooltip.tsx
create mode 100644 components/utility/alerts.tsx
create mode 100644 components/utility/announcements.tsx
create mode 100644 components/utility/command-k.tsx
create mode 100644 components/utility/global-state.tsx
create mode 100644 components/utility/import.tsx
create mode 100644 components/utility/profile-settings.tsx
create mode 100644 components/utility/providers.tsx
create mode 100644 components/utility/share-menu.tsx
create mode 100644 components/utility/theme-switcher.tsx
create mode 100644 components/utility/workspace-switcher.tsx
create mode 100644 components/workspace/assign-workspaces.tsx
create mode 100644 components/workspace/delete-workspace.tsx
create mode 100644 components/workspace/workspace-settings.tsx
create mode 100644 context/context.tsx
create mode 100644 db/assistants.ts
create mode 100644 db/chat-files.ts
create mode 100644 db/chats.ts
create mode 100644 db/collection-files.ts
create mode 100644 db/collections.ts
create mode 100644 db/files.ts
create mode 100644 db/folders.ts
create mode 100644 db/index.ts
create mode 100644 db/limits.ts
create mode 100644 db/message-file-items.ts
create mode 100644 db/messages.ts
create mode 100644 db/presets.ts
create mode 100644 db/profile.ts
create mode 100644 db/prompts.ts
create mode 100644 db/storage/assistant-images.ts
create mode 100644 db/storage/files.ts
create mode 100644 db/storage/message-images.ts
create mode 100644 db/storage/profile-images.ts
create mode 100644 db/workspaces.ts
create mode 100644 lib/blob-to-b64.ts
create mode 100644 lib/build-prompt.ts
create mode 100644 lib/canvas-preview.ts
create mode 100644 lib/chat-setting-limits.ts
create mode 100644 lib/consume-stream.ts
create mode 100644 lib/effects/use-debounce.ts
create mode 100644 lib/generate-local-embedding.ts
create mode 100644 lib/hooks/use-copy-to-clipboard.tsx
create mode 100644 lib/hooks/use-hotkey.tsx
create mode 100644 lib/is-model-locked.ts
create mode 100644 lib/models/llm/anthropic-llm-list.ts
create mode 100644 lib/models/llm/google-llm-list.ts
create mode 100644 lib/models/llm/llm-list.ts
create mode 100644 lib/models/llm/mistral-llm-list.ts
create mode 100644 lib/models/llm/openai-llm-list.ts
create mode 100644 lib/models/llm/perplexity-llm-list.ts
create mode 100644 lib/retrieval/processing/csv.ts
create mode 100644 lib/retrieval/processing/doc.ts
create mode 100644 lib/retrieval/processing/docx.ts
create mode 100644 lib/retrieval/processing/html.ts
create mode 100644 lib/retrieval/processing/index.ts
create mode 100644 lib/retrieval/processing/json.ts
create mode 100644 lib/retrieval/processing/md.ts
create mode 100644 lib/retrieval/processing/pdf.ts
create mode 100644 lib/retrieval/processing/txt.ts
create mode 100644 lib/server-chat-helpers.ts
create mode 100644 lib/supabase/browser-client.ts
create mode 100644 lib/supabase/client.ts
create mode 100644 lib/supabase/middleware.ts
create mode 100644 lib/supabase/server.ts
create mode 100644 lib/utils.ts
create mode 100644 license
create mode 100644 middleware.ts
create mode 100644 next.config.js
create mode 100644 package-lock.json
create mode 100644 package.json
create mode 100644 postcss.config.js
create mode 100644 prettier.config.cjs
create mode 100644 public/DARK_BRAND_LOGO.png
create mode 100644 public/LIGHT_BRAND_LOGO.png
create mode 100644 public/providers/meta.png
create mode 100644 public/providers/mistral.png
create mode 100644 public/providers/perplexity.png
create mode 100644 public/readme/screenshot.png
create mode 100644 supabase/.gitignore
create mode 100644 supabase/config.toml
create mode 100644 supabase/migrations/20240108234540_setup.sql
create mode 100644 supabase/migrations/20240108234541_add_profiles.sql
create mode 100644 supabase/migrations/20240108234542_add_workspaces.sql
create mode 100644 supabase/migrations/20240108234543_add_folders.sql
create mode 100644 supabase/migrations/20240108234544_add_files.sql
create mode 100644 supabase/migrations/20240108234545_add_file_items.sql
create mode 100644 supabase/migrations/20240108234546_add_presets.sql
create mode 100644 supabase/migrations/20240108234547_add_assistants.sql
create mode 100644 supabase/migrations/20240108234548_add_chats.sql
create mode 100644 supabase/migrations/20240108234549_add_messages.sql
create mode 100644 supabase/migrations/20240108234550_add_prompts.sql
create mode 100644 supabase/migrations/20240108234551_add_collections.sql
create mode 100644 supabase/types.ts
create mode 100644 tailwind.config.ts
create mode 100644 tsconfig.json
create mode 100644 types/announcement.ts
create mode 100644 types/assistant-image.ts
create mode 100644 types/chat-file.tsx
create mode 100644 types/chat-message.ts
create mode 100644 types/chat.ts
create mode 100644 types/collection-file.ts
create mode 100644 types/content-type.ts
create mode 100644 types/file-item-chunk.ts
create mode 100644 types/index.ts
create mode 100644 types/llms.ts
create mode 100644 types/message-image.ts
create mode 100644 types/models.ts
create mode 100644 types/sharing.ts
create mode 100644 types/sidebar-data.ts
diff --git a/.env.local.example b/.env.local.example
new file mode 100644
index 0000000000..375977fc85
--- /dev/null
+++ b/.env.local.example
@@ -0,0 +1,9 @@
+# Supabase Public
+NEXT_PUBLIC_SUPABASE_URL=
+NEXT_PUBLIC_SUPABASE_ANON_KEY=
+
+# Supabase Private
+SUPABASE_SERVICE_ROLE_KEY=
+
+# Ollama
+NEXT_PUBLIC_OLLAMA_URL=http://localhost:11434 # Default
diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000000..6ec5479fec
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,25 @@
+{
+ "$schema": "https://json.schemastore.org/eslintrc",
+ "root": true,
+ "extends": [
+ "next/core-web-vitals",
+ "prettier",
+ "plugin:tailwindcss/recommended"
+ ],
+ "plugins": ["tailwindcss"],
+ "rules": {
+ "tailwindcss/no-custom-classname": "off"
+ },
+ "settings": {
+ "tailwindcss": {
+ "callees": ["cn", "cva"],
+ "config": "tailwind.config.js"
+ }
+ },
+ "overrides": [
+ {
+ "files": ["*.ts", "*.tsx"],
+ "parser": "@typescript-eslint/parser"
+ }
+ ]
+}
diff --git a/.github/funding.yaml b/.github/funding.yaml
new file mode 100644
index 0000000000..d6494ef5b2
--- /dev/null
+++ b/.github/funding.yaml
@@ -0,0 +1,3 @@
+# If you find my open-source work helpful, please consider sponsoring me!
+
+github: mckaywrigley
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000..ebcb6f2436
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,40 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+.yarn/install-state.gz
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env
+.env*.local
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
+
+seed.sql
+.VSCodeCounter
\ No newline at end of file
diff --git a/.husky/pre-commit b/.husky/pre-commit
new file mode 100755
index 0000000000..d2eb6755b1
--- /dev/null
+++ b/.husky/pre-commit
@@ -0,0 +1,4 @@
+#!/usr/bin/env sh
+. "$(dirname -- "$0")/_/husky.sh"
+
+npm run lint:fix && npm run format:write && git add .
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000000..65c16f5318
--- /dev/null
+++ b/README.md
@@ -0,0 +1,82 @@
+# Chatbot UI
+
+The open-source AI chat app for everyone.
+
+
+
+## Demo
+
+View the latest demo [here](https://twitter.com/mckaywrigley).
+
+## Support
+
+If you find Chatbot UI useful, please consider [sponsoring](https://github.com/mckaywrigley?tab=sponsoring) me :)
+
+## Quickstart
+
+### 1. Clone the repo
+
+```bash
+git clone https://github.com/mckaywrigley/chatbot-ui.git
+```
+
+### 2. Install dependencies
+
+```bash
+npm install
+```
+
+### 3. Install Supabase & run locally
+
+1. Install Supabase CLI
+
+```bash
+brew install supabase/tap/supabase
+```
+
+2. Start Supabase
+
+```bash
+supabase start
+```
+
+### 4. Install Ollama (for local models)
+
+Follow the instructions [here](https://github.com/jmorganca/ollama#macos)
+
+### 5. Fill in secrets
+
+1. .env
+
+```bash
+cp .env.local.example .env.local
+```
+
+Get the required values by running:
+
+```bash
+supabase status
+```
+
+2. sql
+
+In the 1st migration file `20240108234540_setup.sql` you will need to replace 2 values:
+
+- `project_url` (line 53): This can remain unchanged if you don't change your `config.toml` file.
+- `service_role_key` (line 54): You got this value from running `supabase status` in step 5.1.
+
+You will also need to to fill in the values for project_url
+
+### 6. Run app locally
+
+```bash
+npm run chat
+```
+
+## Contributing
+
+We are working on a guide for contributing.
+
+## Contact
+
+Message Mckay on [Twitter/X](https://twitter.com/mckaywrigley)
diff --git a/app/api/chat/anthropic/route.ts b/app/api/chat/anthropic/route.ts
new file mode 100644
index 0000000000..ded55d0cf7
--- /dev/null
+++ b/app/api/chat/anthropic/route.ts
@@ -0,0 +1,47 @@
+import { CHAT_SETTING_LIMITS } from "@/lib/chat-setting-limits"
+import { checkApiKey, getServerProfile } from "@/lib/server-chat-helpers"
+import { ChatSettings } from "@/types"
+import Anthropic from "@anthropic-ai/sdk"
+import { AnthropicStream, StreamingTextResponse } from "ai"
+
+export const runtime = "edge"
+
+export async function POST(request: Request) {
+ const json = await request.json()
+ const { chatSettings, messages } = json as {
+ chatSettings: ChatSettings
+ messages: any[]
+ }
+
+ try {
+ const profile = await getServerProfile()
+
+ checkApiKey(profile.anthropic_api_key, "Anthropic")
+
+ let ANTHROPIC_FORMATTED_MESSAGES: any = messages.slice(1)
+
+ const anthropic = new Anthropic({
+ apiKey: profile.anthropic_api_key || ""
+ })
+
+ const response = await anthropic.beta.messages.create({
+ model: chatSettings.model,
+ messages: ANTHROPIC_FORMATTED_MESSAGES,
+ temperature: chatSettings.temperature,
+ system: messages[0].content,
+ max_tokens:
+ CHAT_SETTING_LIMITS[chatSettings.model].MAX_TOKEN_OUTPUT_LENGTH,
+ stream: true
+ })
+
+ const stream = AnthropicStream(response)
+
+ return new StreamingTextResponse(stream)
+ } catch (error: any) {
+ const errorMessage = error.message || "An unexpected error occurred"
+ const errorCode = error.status || 500
+ return new Response(JSON.stringify({ message: errorMessage }), {
+ status: errorCode
+ })
+ }
+}
diff --git a/app/api/chat/azure/route.ts b/app/api/chat/azure/route.ts
new file mode 100644
index 0000000000..1200335232
--- /dev/null
+++ b/app/api/chat/azure/route.ts
@@ -0,0 +1,74 @@
+import { CHAT_SETTING_LIMITS } from "@/lib/chat-setting-limits"
+import { checkApiKey, getServerProfile } from "@/lib/server-chat-helpers"
+import { ChatAPIPayload } from "@/types"
+import { OpenAIStream, StreamingTextResponse } from "ai"
+import OpenAI from "openai"
+import { ChatCompletionCreateParamsBase } from "openai/resources/chat/completions.mjs"
+
+export const runtime = "edge"
+
+export async function POST(request: Request) {
+ const json = await request.json()
+ const { chatSettings, messages } = json as ChatAPIPayload
+
+ try {
+ const profile = await getServerProfile()
+
+ checkApiKey(profile.azure_openai_api_key, "Azure")
+
+ const ENDPOINT = profile.azure_openai_endpoint
+ const KEY = profile.azure_openai_api_key
+
+ let DEPLOYMENT_ID = ""
+ switch (chatSettings.model) {
+ case "gpt-3.5-turbo-1106":
+ DEPLOYMENT_ID = profile.azure_openai_35_turbo_id || ""
+ break
+ case "gpt-4-1106-preview":
+ DEPLOYMENT_ID = profile.azure_openai_45_turbo_id || ""
+ break
+ case "gpt-4-vision-preview":
+ DEPLOYMENT_ID = profile.azure_openai_45_vision_id || ""
+ break
+ default:
+ return new Response(JSON.stringify({ message: "Model not found" }), {
+ status: 400
+ })
+ }
+
+ if (!ENDPOINT || !KEY || !DEPLOYMENT_ID) {
+ return new Response(
+ JSON.stringify({ message: "Azure resources not found" }),
+ {
+ status: 400
+ }
+ )
+ }
+
+ const azureOpenai = new OpenAI({
+ apiKey: KEY,
+ baseURL: `${ENDPOINT}/openai/deployments/${DEPLOYMENT_ID}`,
+ defaultQuery: { "api-version": "2023-07-01-preview" },
+ defaultHeaders: { "api-key": KEY }
+ })
+
+ const response = await azureOpenai.chat.completions.create({
+ model: DEPLOYMENT_ID as ChatCompletionCreateParamsBase["model"],
+ messages: messages as ChatCompletionCreateParamsBase["messages"],
+ temperature: chatSettings.temperature,
+ max_tokens:
+ CHAT_SETTING_LIMITS[chatSettings.model].MAX_TOKEN_OUTPUT_LENGTH,
+ stream: true
+ })
+
+ const stream = OpenAIStream(response)
+
+ return new StreamingTextResponse(stream)
+ } catch (error: any) {
+ const errorMessage = error.error?.message || "An unexpected error occurred"
+ const errorCode = error.status || 500
+ return new Response(JSON.stringify({ message: errorMessage }), {
+ status: errorCode
+ })
+ }
+}
diff --git a/app/api/chat/google/route.ts b/app/api/chat/google/route.ts
new file mode 100644
index 0000000000..225e2aca87
--- /dev/null
+++ b/app/api/chat/google/route.ts
@@ -0,0 +1,72 @@
+import { checkApiKey, getServerProfile } from "@/lib/server-chat-helpers"
+import { ChatSettings } from "@/types"
+import { GoogleGenerativeAI } from "@google/generative-ai"
+
+export const runtime = "edge"
+
+export async function POST(request: Request) {
+ const json = await request.json()
+ const { chatSettings, messages } = json as {
+ chatSettings: ChatSettings
+ messages: any[]
+ }
+
+ try {
+ const profile = await getServerProfile()
+
+ checkApiKey(profile.google_gemini_api_key, "Google")
+
+ const genAI = new GoogleGenerativeAI(profile.google_gemini_api_key || "")
+ const googleModel = genAI.getGenerativeModel({ model: chatSettings.model })
+
+ if (chatSettings.model === "gemini-pro") {
+ const lastMessage = messages.pop()
+
+ const chat = googleModel.startChat({
+ history: messages,
+ generationConfig: {
+ temperature: chatSettings.temperature
+ }
+ })
+
+ const response = await chat.sendMessageStream(lastMessage.parts)
+
+ const encoder = new TextEncoder()
+ const readableStream = new ReadableStream({
+ async start(controller) {
+ for await (const chunk of response.stream) {
+ const chunkText = chunk.text()
+ controller.enqueue(encoder.encode(chunkText))
+ }
+ controller.close()
+ }
+ })
+
+ return new Response(readableStream, {
+ headers: { "Content-Type": "text/plain" }
+ })
+ } else if (chatSettings.model === "gemini-pro-vision") {
+ // FIX: Hacky until chat messages are supported
+ const HACKY_MESSAGE = messages[messages.length - 1]
+
+ const result = await googleModel.generateContent([
+ HACKY_MESSAGE.prompt,
+ HACKY_MESSAGE.imageParts
+ ])
+
+ const response = result.response
+
+ const text = response.text()
+
+ return new Response(text, {
+ headers: { "Content-Type": "text/plain" }
+ })
+ }
+ } catch (error: any) {
+ const errorMessage = error.error?.message || "An unexpected error occurred"
+ const errorCode = error.status || 500
+ return new Response(JSON.stringify({ message: errorMessage }), {
+ status: errorCode
+ })
+ }
+}
diff --git a/app/api/chat/mistral/route.ts b/app/api/chat/mistral/route.ts
new file mode 100644
index 0000000000..76ad728ddc
--- /dev/null
+++ b/app/api/chat/mistral/route.ts
@@ -0,0 +1,71 @@
+import { checkApiKey, getServerProfile } from "@/lib/server-chat-helpers"
+import { ChatSettings } from "@/types"
+
+export const runtime = "edge"
+
+export async function POST(request: Request) {
+ const json = await request.json()
+ const { chatSettings, messages } = json as {
+ chatSettings: ChatSettings
+ messages: any[]
+ }
+
+ try {
+ const profile = await getServerProfile()
+
+ checkApiKey(profile.mistral_api_key, "Mistral")
+
+ const response = await fetch("https://api.mistral.ai/v1/chat/completions", {
+ method: "POST",
+ headers: {
+ Accept: "application/json",
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${profile.mistral_api_key}`
+ },
+ body: JSON.stringify({
+ model: chatSettings.model,
+ messages: messages,
+ temperature: chatSettings.temperature,
+ stream: true
+ })
+ })
+
+ const readableStream = new ReadableStream({
+ async start(controller) {
+ if (!response.body) {
+ throw new Error("No response body!")
+ }
+
+ const reader = response.body.getReader()
+ let isFirstChunk = true
+ while (true) {
+ const { done, value } = await reader.read()
+ if (done) {
+ controller.close()
+ break
+ }
+ const chunk = new TextDecoder("utf-8").decode(value)
+ const dataParts = chunk.split("data: ")
+ const data =
+ isFirstChunk && dataParts[2] ? dataParts[2] : dataParts[1]
+ if (data) {
+ const parsedData = JSON.parse(data)
+ const messageContent = parsedData.choices[0].delta.content
+ controller.enqueue(new TextEncoder().encode(messageContent))
+ }
+ isFirstChunk = false
+ }
+ }
+ })
+
+ return new Response(readableStream, {
+ headers: { "Content-Type": "text/plain" }
+ })
+ } catch (error: any) {
+ const errorMessage = error.error?.message || "An unexpected error occurred"
+ const errorCode = error.status || 500
+ return new Response(JSON.stringify({ message: errorMessage }), {
+ status: errorCode
+ })
+ }
+}
diff --git a/app/api/chat/openai/route.ts b/app/api/chat/openai/route.ts
new file mode 100644
index 0000000000..a2d5cf9ed2
--- /dev/null
+++ b/app/api/chat/openai/route.ts
@@ -0,0 +1,46 @@
+import { CHAT_SETTING_LIMITS } from "@/lib/chat-setting-limits"
+import { checkApiKey, getServerProfile } from "@/lib/server-chat-helpers"
+import { ChatSettings } from "@/types"
+import { OpenAIStream, StreamingTextResponse } from "ai"
+import OpenAI from "openai"
+import { ChatCompletionCreateParamsBase } from "openai/resources/chat/completions.mjs"
+
+export const runtime = "edge"
+
+export async function POST(request: Request) {
+ const json = await request.json()
+ const { chatSettings, messages } = json as {
+ chatSettings: ChatSettings
+ messages: any[]
+ }
+
+ try {
+ const profile = await getServerProfile()
+
+ checkApiKey(profile.openai_api_key, "OpenAI")
+
+ const openai = new OpenAI({
+ apiKey: profile.openai_api_key || "",
+ organization: profile.openai_organization_id
+ })
+
+ const response = await openai.chat.completions.create({
+ model: chatSettings.model as ChatCompletionCreateParamsBase["model"],
+ messages: messages as ChatCompletionCreateParamsBase["messages"],
+ temperature: chatSettings.temperature,
+ max_tokens:
+ CHAT_SETTING_LIMITS[chatSettings.model].MAX_TOKEN_OUTPUT_LENGTH,
+ stream: true
+ })
+
+ const stream = OpenAIStream(response)
+
+ return new StreamingTextResponse(stream)
+ } catch (error: any) {
+ const errorMessage = error.error?.message || "An unexpected error occurred"
+ const errorCode = error.status || 500
+ return new Response(JSON.stringify({ message: errorMessage }), {
+ status: errorCode
+ })
+ }
+}
diff --git a/app/api/chat/perplexity/route.ts b/app/api/chat/perplexity/route.ts
new file mode 100644
index 0000000000..6f29d03b65
--- /dev/null
+++ b/app/api/chat/perplexity/route.ts
@@ -0,0 +1,68 @@
+import { checkApiKey, getServerProfile } from "@/lib/server-chat-helpers"
+import { ChatSettings } from "@/types"
+
+export const runtime = "edge"
+
+export async function POST(request: Request) {
+ const json = await request.json()
+ const { chatSettings, messages } = json as {
+ chatSettings: ChatSettings
+ messages: any[]
+ }
+
+ try {
+ const profile = await getServerProfile()
+
+ checkApiKey(profile.anthropic_api_key, "Anthropic")
+
+ const response = await fetch("https://api.perplexity.ai/chat/completions", {
+ method: "POST",
+ headers: {
+ Accept: "application/json",
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${profile.perplexity_api_key}`
+ },
+ body: JSON.stringify({
+ model: chatSettings.model,
+ messages: messages,
+ temperature: chatSettings.temperature,
+ stream: true
+ })
+ })
+
+ const readableStream = new ReadableStream({
+ async start(controller) {
+ if (!response.body) {
+ throw new Error("No response body!")
+ }
+
+ const reader = response.body.getReader()
+ while (true) {
+ const { done, value } = await reader.read()
+ if (done) {
+ controller.close()
+ break
+ }
+ const chunk = new TextDecoder("utf-8").decode(value)
+
+ const data = chunk.split("data: ")[1]
+ if (data) {
+ const parsedData = JSON.parse(data)
+ const messageContent = parsedData.choices[0].delta.content
+ controller.enqueue(new TextEncoder().encode(messageContent))
+ }
+ }
+ }
+ })
+
+ return new Response(readableStream, {
+ headers: { "Content-Type": "text/plain" }
+ })
+ } catch (error: any) {
+ const errorMessage = error.error?.message || "An unexpected error occurred"
+ const errorCode = error.status || 500
+ return new Response(JSON.stringify({ message: errorMessage }), {
+ status: errorCode
+ })
+ }
+}
diff --git a/app/api/command/route.ts b/app/api/command/route.ts
new file mode 100644
index 0000000000..d317467d93
--- /dev/null
+++ b/app/api/command/route.ts
@@ -0,0 +1,54 @@
+import { CHAT_SETTING_LIMITS } from "@/lib/chat-setting-limits"
+import { checkApiKey, getServerProfile } from "@/lib/server-chat-helpers"
+import OpenAI from "openai"
+
+export const runtime = "edge"
+
+export async function POST(request: Request) {
+ const json = await request.json()
+ const { input } = json as {
+ input: string
+ }
+
+ try {
+ const profile = await getServerProfile()
+
+ checkApiKey(profile.openai_api_key, "OpenAI")
+
+ const openai = new OpenAI({
+ apiKey: profile.openai_api_key || "",
+ organization: profile.openai_organization_id
+ })
+
+ const response = await openai.chat.completions.create({
+ model: "gpt-4-1106-preview",
+ messages: [
+ {
+ role: "system",
+ content: "Respond to the user."
+ },
+ {
+ role: "user",
+ content: input
+ }
+ ],
+ temperature: 0,
+ max_tokens:
+ CHAT_SETTING_LIMITS["gpt-4-1106-preview"].MAX_TOKEN_OUTPUT_LENGTH
+ // response_format: { type: "json_object" }
+ // stream: true
+ })
+
+ const content = response.choices[0].message.content
+
+ return new Response(JSON.stringify({ content }), {
+ status: 200
+ })
+ } catch (error: any) {
+ const errorMessage = error.error?.message || "An unexpected error occurred"
+ const errorCode = error.status || 500
+ return new Response(JSON.stringify({ message: errorMessage }), {
+ status: errorCode
+ })
+ }
+}
diff --git a/app/api/localhost/ollama/route.ts b/app/api/localhost/ollama/route.ts
new file mode 100644
index 0000000000..9d7899e625
--- /dev/null
+++ b/app/api/localhost/ollama/route.ts
@@ -0,0 +1,39 @@
+import { LLM, LLMID } from "@/types"
+import { exec } from "child_process"
+
+export function GET(): Promise {
+ return new Promise((resolve, reject) => {
+ exec("ollama serve", (error: any, stdout: any, stderr: any) => {})
+
+ exec("ollama list", (error: any, stdout: any, stderr: any) => {
+ if (error) {
+ reject(error)
+ return
+ }
+
+ if (stderr) {
+ reject(new Error(stderr))
+ return
+ }
+
+ const rows = stdout.split("\n")
+ const dataRows = rows.slice(1, -1)
+ const modelNames = dataRows.map((row: string) => row.split(/\s+/)[0])
+
+ const localModels = modelNames.map((modelName: string) => {
+ const model: LLM = {
+ modelId: modelName as LLMID,
+ modelName: modelName,
+ provider: "ollama",
+ hostedId: modelName,
+ platformLink: "https://ollama.ai/library",
+ imageInput: false
+ }
+
+ return model
+ })
+
+ resolve(new Response(JSON.stringify({ localModels })))
+ })
+ })
+}
diff --git a/app/api/retrieval/process/route.ts b/app/api/retrieval/process/route.ts
new file mode 100644
index 0000000000..5f09d04d74
--- /dev/null
+++ b/app/api/retrieval/process/route.ts
@@ -0,0 +1,150 @@
+import { generateLocalEmbedding } from "@/lib/generate-local-embedding"
+import { processCSV } from "@/lib/retrieval/processing/csv"
+import { processDoc } from "@/lib/retrieval/processing/doc"
+import { processDocX } from "@/lib/retrieval/processing/docx"
+import { processHTML } from "@/lib/retrieval/processing/html"
+import { processJSON } from "@/lib/retrieval/processing/json"
+import { processMarkdown } from "@/lib/retrieval/processing/md"
+import { processPdf } from "@/lib/retrieval/processing/pdf"
+import { processTxt } from "@/lib/retrieval/processing/txt"
+import { checkApiKey, getServerProfile } from "@/lib/server-chat-helpers"
+import { Database } from "@/supabase/types"
+import { FileItemChunk } from "@/types"
+import { createClient } from "@supabase/supabase-js"
+import { NextResponse } from "next/server"
+import OpenAI from "openai"
+
+export async function POST(req: Request) {
+ try {
+ const supabaseAdmin = createClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.SUPABASE_SERVICE_ROLE_KEY!
+ )
+
+ const profile = await getServerProfile()
+
+ checkApiKey(profile.openai_api_key, "OpenAI")
+
+ const formData = await req.formData()
+
+ const file = formData.get("file") as File
+ const file_id = formData.get("file_id") as string
+ const embeddingsProvider = formData.get("embeddingsProvider") as string
+
+ const fileBuffer = Buffer.from(await file.arrayBuffer())
+ const blob = new Blob([fileBuffer])
+ const fileExtension = file.name.split(".").pop()?.toLowerCase()
+
+ let chunks: FileItemChunk[] = []
+
+ switch (fileExtension) {
+ case "csv":
+ chunks = await processCSV(blob)
+ break
+ case "doc":
+ chunks = await processDoc(blob)
+ break
+ case "docx":
+ console.log("docx")
+ chunks = await processDocX(blob)
+ break
+ case "html":
+ chunks = await processHTML(blob)
+ break
+ case "json":
+ chunks = await processJSON(blob)
+ break
+ case "md":
+ chunks = await processMarkdown(blob)
+ break
+ case "pdf":
+ console.log("pdf")
+ chunks = await processPdf(blob)
+ break
+ case "txt":
+ chunks = await processTxt(blob)
+ break
+ default:
+ return new NextResponse("Unsupported file type", {
+ status: 400
+ })
+ }
+
+ let embeddings: any = []
+
+ if (embeddingsProvider === "openai") {
+ const openai = new OpenAI({
+ apiKey: profile.openai_api_key || "",
+ organization: profile.openai_organization_id
+ })
+
+ const response = await openai.embeddings.create({
+ model: "text-embedding-ada-002",
+ input: chunks.map(chunk => chunk.content)
+ })
+
+ embeddings = response.data.map((item: any) => {
+ return item.embedding
+ })
+ } else if (embeddingsProvider === "local") {
+ const embeddingPromises = chunks.map(async chunk => {
+ try {
+ return await generateLocalEmbedding(chunk.content)
+ } catch (error) {
+ console.error(`Error generating embedding for chunk: ${chunk}`, error)
+ return null
+ }
+ })
+
+ embeddings = await Promise.all(embeddingPromises)
+ }
+
+ console.log(
+ embeddings[0],
+ "embeddings",
+ embeddingsProvider,
+ embeddings.length
+ )
+
+ const file_items = chunks.map((chunk, index) => ({
+ file_id,
+ user_id: profile.user_id,
+ content: chunk.content,
+ tokens: chunk.tokens,
+ openai_embedding:
+ embeddingsProvider === "openai"
+ ? ((embeddings[index] || null) as any)
+ : null,
+ local_embedding:
+ embeddingsProvider === "local"
+ ? ((embeddings[index] || null) as any)
+ : null
+ }))
+
+ const { data: fileItemData, error: fileItemError } = await supabaseAdmin
+ .from("file_items")
+ .upsert(file_items)
+
+ if (fileItemError) {
+ console.log("fileItemError", fileItemError)
+ }
+
+ const totalTokens = file_items.reduce((acc, item) => acc + item.tokens, 0)
+
+ await supabaseAdmin
+ .from("files")
+ .update({ tokens: totalTokens })
+ .eq("id", file_id)
+
+ return new NextResponse("Embed Successful", {
+ status: 200
+ })
+ } catch (error: any) {
+ console.error(error)
+ const errorMessage = error.error?.message || "An unexpected error occurred"
+ const errorCode = error.status || 500
+ return new Response(JSON.stringify({ message: errorMessage }), {
+ status: errorCode
+ })
+ }
+}
diff --git a/app/api/retrieval/retrieve/route.ts b/app/api/retrieval/retrieve/route.ts
new file mode 100644
index 0000000000..c101c971db
--- /dev/null
+++ b/app/api/retrieval/retrieve/route.ts
@@ -0,0 +1,108 @@
+import { generateLocalEmbedding } from "@/lib/generate-local-embedding"
+import { checkApiKey, getServerProfile } from "@/lib/server-chat-helpers"
+import { Database } from "@/supabase/types"
+import { createClient } from "@supabase/supabase-js"
+import OpenAI from "openai"
+
+export async function POST(request: Request) {
+ const json = await request.json()
+ const { userInput, fileIds, embeddingsProvider } = json as {
+ userInput: string
+ fileIds: string[]
+ embeddingsProvider: "openai" | "local"
+ }
+
+ try {
+ const supabaseAdmin = createClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.SUPABASE_SERVICE_ROLE_KEY!
+ )
+
+ const profile = await getServerProfile()
+
+ checkApiKey(profile.openai_api_key, "OpenAI")
+
+ console.log("userInput", userInput)
+ console.log("fileIds", fileIds)
+
+ let chunks: any[] = []
+
+ const MATCH_COUNT = 100
+
+ if (embeddingsProvider === "openai") {
+ console.log("openai")
+
+ const openai = new OpenAI({
+ apiKey: profile.openai_api_key || "",
+ organization: profile.openai_organization_id
+ })
+
+ const response = await openai.embeddings.create({
+ model: "text-embedding-ada-002",
+ input: userInput
+ })
+
+ const openaiEmbedding = response.data.map(item => item.embedding)[0]
+ console.log("openaiEmbedding", openaiEmbedding.length)
+
+ const { data: openaiFileItems, error: openaiError } =
+ await supabaseAdmin.rpc("match_file_items_openai", {
+ query_embedding: openaiEmbedding as any,
+ match_count: MATCH_COUNT,
+ file_ids: fileIds
+ })
+
+ if (openaiError) {
+ throw openaiError
+ }
+
+ chunks = openaiFileItems
+ } else if (embeddingsProvider === "local") {
+ console.log("local")
+
+ const localEmbedding = await generateLocalEmbedding(userInput)
+
+ const { data: localFileItems, error: localError } =
+ await supabaseAdmin.rpc("match_file_items_local", {
+ query_embedding: localEmbedding as any,
+ match_count: MATCH_COUNT,
+ file_ids: fileIds
+ })
+
+ if (localError) {
+ throw localError
+ }
+
+ chunks = localFileItems
+ }
+
+ const totalTokenCount = chunks?.reduce(
+ (total, chunk) => total + chunk.tokens,
+ 0
+ )
+
+ const topThreeSimilar = chunks
+ ?.sort((a, b) => b.similarity - a.similarity)
+ .slice(0, 3)
+ console.log(topThreeSimilar, "Top 3 most similar:")
+
+ const tokenCountTopThree = topThreeSimilar?.reduce(
+ (total, chunk) => total + chunk.tokens,
+ 0
+ )
+
+ console.log("Total token count of all 100 combined:", totalTokenCount)
+
+ console.log("Token count of top 3 most similar:", tokenCountTopThree)
+
+ return new Response(JSON.stringify({ results: topThreeSimilar }), {
+ status: 200
+ })
+ } catch (error: any) {
+ const errorMessage = error.error?.message || "An unexpected error occurred"
+ const errorCode = error.status || 500
+ return new Response(JSON.stringify({ message: errorMessage }), {
+ status: errorCode
+ })
+ }
+}
diff --git a/app/api/username/available/route.ts b/app/api/username/available/route.ts
new file mode 100644
index 0000000000..bf00ee07f6
--- /dev/null
+++ b/app/api/username/available/route.ts
@@ -0,0 +1,37 @@
+import { Database } from "@/supabase/types"
+import { createClient } from "@supabase/supabase-js"
+
+export const runtime = "edge"
+
+export async function POST(request: Request) {
+ const json = await request.json()
+ const { username } = json as {
+ username: string
+ }
+
+ try {
+ const supabaseAdmin = createClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.SUPABASE_SERVICE_ROLE_KEY!
+ )
+
+ const { data: usernames, error } = await supabaseAdmin
+ .from("profiles")
+ .select("username")
+ .eq("username", username)
+
+ if (!usernames) {
+ throw new Error(error.message)
+ }
+
+ return new Response(JSON.stringify({ isAvailable: !usernames.length }), {
+ status: 200
+ })
+ } catch (error: any) {
+ const errorMessage = error.error?.message || "An unexpected error occurred"
+ const errorCode = error.status || 500
+ return new Response(JSON.stringify({ message: errorMessage }), {
+ status: errorCode
+ })
+ }
+}
diff --git a/app/api/username/get/route.ts b/app/api/username/get/route.ts
new file mode 100644
index 0000000000..d3cd158021
--- /dev/null
+++ b/app/api/username/get/route.ts
@@ -0,0 +1,38 @@
+import { Database } from "@/supabase/types"
+import { createClient } from "@supabase/supabase-js"
+
+export const runtime = "edge"
+
+export async function POST(request: Request) {
+ const json = await request.json()
+ const { userId } = json as {
+ userId: string
+ }
+
+ try {
+ const supabaseAdmin = createClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.SUPABASE_SERVICE_ROLE_KEY!
+ )
+
+ const { data, error } = await supabaseAdmin
+ .from("profiles")
+ .select("username")
+ .eq("user_id", userId)
+ .single()
+
+ if (!data) {
+ throw new Error(error.message)
+ }
+
+ return new Response(JSON.stringify({ username: data.username }), {
+ status: 200
+ })
+ } catch (error: any) {
+ const errorMessage = error.error?.message || "An unexpected error occurred"
+ const errorCode = error.status || 500
+ return new Response(JSON.stringify({ message: errorMessage }), {
+ status: errorCode
+ })
+ }
+}
diff --git a/app/auth/callback/route.ts b/app/auth/callback/route.ts
new file mode 100644
index 0000000000..ece526325e
--- /dev/null
+++ b/app/auth/callback/route.ts
@@ -0,0 +1,16 @@
+import { createClient } from "@/lib/supabase/server"
+import { cookies } from "next/headers"
+import { NextResponse } from "next/server"
+
+export async function GET(request: Request) {
+ const requestUrl = new URL(request.url)
+ const code = requestUrl.searchParams.get("code")
+
+ if (code) {
+ const cookieStore = cookies()
+ const supabase = createClient(cookieStore)
+ await supabase.auth.exchangeCodeForSession(code)
+ }
+
+ return NextResponse.redirect(requestUrl.origin)
+}
diff --git a/app/chat/[chatid]/page.tsx b/app/chat/[chatid]/page.tsx
new file mode 100644
index 0000000000..30d082e82d
--- /dev/null
+++ b/app/chat/[chatid]/page.tsx
@@ -0,0 +1,7 @@
+"use client"
+
+import { ChatUI } from "@/components/chat/chat-ui"
+
+export default function ChatIDPage() {
+ return
+}
diff --git a/app/chat/layout.tsx b/app/chat/layout.tsx
new file mode 100644
index 0000000000..96931c73d8
--- /dev/null
+++ b/app/chat/layout.tsx
@@ -0,0 +1,34 @@
+"use client"
+
+import { Dashboard } from "@/components/ui/dashboard"
+import { supabase } from "@/lib/supabase/browser-client"
+import { useRouter } from "next/navigation"
+import { ReactNode, useEffect, useState } from "react"
+
+interface ChatLayoutProps {
+ children: ReactNode
+}
+
+export default function ChatLayout({ children }: ChatLayoutProps) {
+ const [loading, setLoading] = useState(true)
+
+ const router = useRouter()
+
+ useEffect(() => {
+ ;(async () => {
+ const session = (await supabase.auth.getSession()).data.session
+
+ if (!session) {
+ router.push("/login")
+ } else {
+ setLoading(false)
+ }
+ })()
+ }, [])
+
+ if (loading) {
+ return null
+ }
+
+ return {children}
+}
diff --git a/app/chat/page.tsx b/app/chat/page.tsx
new file mode 100644
index 0000000000..ef0b1c2165
--- /dev/null
+++ b/app/chat/page.tsx
@@ -0,0 +1,55 @@
+"use client"
+
+import { ChatHelp } from "@/components/chat/chat-help"
+import { useChatHandler } from "@/components/chat/chat-hooks/use-chat-handler"
+import { ChatInput } from "@/components/chat/chat-input"
+import { ChatSettings } from "@/components/chat/chat-settings"
+import { ChatUI } from "@/components/chat/chat-ui"
+import { QuickSettings } from "@/components/chat/quick-settings"
+import { Brand } from "@/components/ui/brand"
+import { ChatbotUIContext } from "@/context/context"
+import useHotkey from "@/lib/hooks/use-hotkey"
+import { useTheme } from "next-themes"
+import { useContext } from "react"
+
+export default function ChatPage() {
+ useHotkey("o", () => handleNewChat())
+
+ const { chatMessages } = useContext(ChatbotUIContext)
+
+ const { handleNewChat } = useChatHandler()
+
+ const { theme } = useTheme()
+
+ return (
+ <>
+ {chatMessages.length === 0 ? (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ) : (
+
+ )}
+ >
+ )
+}
diff --git a/app/globals.css b/app/globals.css
new file mode 100644
index 0000000000..c0d1efcb5a
--- /dev/null
+++ b/app/globals.css
@@ -0,0 +1,104 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+::-webkit-scrollbar-track {
+ background-color: transparent;
+}
+
+::-webkit-scrollbar-thumb {
+ background-color: #ccc;
+ border-radius: 10px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background-color: #aaa;
+}
+
+::-webkit-scrollbar-track:hover {
+ background-color: #f2f2f2;
+}
+
+::-webkit-scrollbar-corner {
+ background-color: transparent;
+}
+
+::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
+}
+
+@layer base {
+ :root {
+ --background: 0 0% 100%;
+ --foreground: 0 0% 3.9%;
+
+ --muted: 0 0% 96.1%;
+ --muted-foreground: 0 0% 45.1%;
+
+ --popover: 0 0% 100%;
+ --popover-foreground: 0 0% 3.9%;
+
+ --card: 0 0% 100%;
+ --card-foreground: 0 0% 3.9%;
+
+ --border: 0 0% 89.8%;
+ --input: 0 0% 89.8%;
+
+ --primary: 0 0% 9%;
+ --primary-foreground: 0 0% 98%;
+
+ --secondary: 0 0% 96.1%;
+ --secondary-foreground: 0 0% 9%;
+
+ --accent: 0 0% 96.1%;
+ --accent-foreground: 0 0% 9%;
+
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 0 0% 98%;
+
+ --ring: 0 0% 63.9%;
+
+ --radius: 0.5rem;
+ }
+
+ .dark {
+ --background: 0 0% 3.9%;
+ --foreground: 0 0% 98%;
+
+ --muted: 0 0% 14.9%;
+ --muted-foreground: 0 0% 63.9%;
+
+ --popover: 0 0% 3.9%;
+ --popover-foreground: 0 0% 98%;
+
+ --card: 0 0% 3.9%;
+ --card-foreground: 0 0% 98%;
+
+ --border: 0 0% 14.9%;
+ --input: 0 0% 14.9%;
+
+ --primary: 0 0% 98%;
+ --primary-foreground: 0 0% 9%;
+
+ --secondary: 0 0% 14.9%;
+ --secondary-foreground: 0 0% 98%;
+
+ --accent: 0 0% 14.9%;
+ --accent-foreground: 0 0% 98%;
+
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 0 85.7% 97.3%;
+
+ --ring: 0 0% 14.9%;
+ }
+}
+
+@layer base {
+ * {
+ @apply border-border;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+}
diff --git a/app/help/page.tsx b/app/help/page.tsx
new file mode 100644
index 0000000000..26a283e7b3
--- /dev/null
+++ b/app/help/page.tsx
@@ -0,0 +1,7 @@
+export default function HelpPage() {
+ return (
+
+
Help under construction.
+
+ )
+}
diff --git a/app/layout.tsx b/app/layout.tsx
new file mode 100644
index 0000000000..1ac5b1c53e
--- /dev/null
+++ b/app/layout.tsx
@@ -0,0 +1,44 @@
+import { Toaster } from "@/components/ui/sonner"
+import { GlobalState } from "@/components/utility/global-state"
+import { Providers } from "@/components/utility/providers"
+import { Database } from "@/supabase/types"
+import { createServerClient } from "@supabase/ssr"
+import { Inter } from "next/font/google"
+import { cookies } from "next/headers"
+import { ReactNode } from "react"
+import "./globals.css"
+
+const inter = Inter({ subsets: ["latin"] })
+
+interface RootLayoutProps {
+ children: ReactNode
+}
+
+export default async function RootLayout({ children }: RootLayoutProps) {
+ const cookieStore = cookies()
+ const supabase = createServerClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
+ {
+ cookies: {
+ get(name: string) {
+ return cookieStore.get(name)?.value
+ }
+ }
+ }
+ )
+ const session = (await supabase.auth.getSession()).data.session
+
+ return (
+
+
+
+
+
+ {session ? {children} : children}
+
+
+
+
+ )
+}
diff --git a/app/loading.tsx b/app/loading.tsx
new file mode 100644
index 0000000000..e88ca3e47c
--- /dev/null
+++ b/app/loading.tsx
@@ -0,0 +1,9 @@
+import { IconLoader2 } from "@tabler/icons-react"
+
+export default function Loading() {
+ return (
+
+
+
+ )
+}
diff --git a/app/login/page.tsx b/app/login/page.tsx
new file mode 100644
index 0000000000..3b3ca3e3a9
--- /dev/null
+++ b/app/login/page.tsx
@@ -0,0 +1,130 @@
+import { Brand } from "@/components/ui/brand"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { createClient } from "@/lib/supabase/server"
+import { Database } from "@/supabase/types"
+import { createServerClient } from "@supabase/ssr"
+import { cookies, headers } from "next/headers"
+import { redirect } from "next/navigation"
+
+export default async function Login({
+ searchParams
+}: {
+ searchParams: { message: string }
+}) {
+ const cookieStore = cookies()
+ const supabase = createServerClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
+ {
+ cookies: {
+ get(name: string) {
+ return cookieStore.get(name)?.value
+ }
+ }
+ }
+ )
+ const session = (await supabase.auth.getSession()).data.session
+
+ if (session) {
+ return redirect("/chat")
+ }
+
+ const signIn = async (formData: FormData) => {
+ "use server"
+
+ const email = formData.get("email") as string
+ const password = formData.get("password") as string
+ const cookieStore = cookies()
+ const supabase = createClient(cookieStore)
+
+ const { data, error } = await supabase.auth.signInWithPassword({
+ email,
+ password
+ })
+
+ if (error) {
+ return redirect("/login?message=Could not authenticate user")
+ }
+
+ return redirect("/chat")
+ }
+
+ const signUp = async (formData: FormData) => {
+ "use server"
+
+ const origin = headers().get("origin")
+ const email = formData.get("email") as string
+ const password = formData.get("password") as string
+ const cookieStore = cookies()
+ const supabase = createClient(cookieStore)
+
+ const { error } = await supabase.auth.signUp({
+ email,
+ password,
+ options: {
+ // TODO: USE IF YOU WANT TO SEND EMAIL VERIFICATION, ALSO CHANGE TOML FILE
+ // emailRedirectTo: `${origin}/auth/callback`
+ }
+ })
+
+ if (error) {
+ return redirect("/login?message=Could not authenticate user")
+ }
+
+ return redirect("/setup")
+
+ // TODO: USE IF YOU WANT TO SEND EMAIL VERIFICATION, ALSO CHANGE TOML FILE
+ // return redirect("/login?message=Check email to continue sign in process")
+ }
+
+ return (
+
+ )
+}
diff --git a/app/page.tsx b/app/page.tsx
new file mode 100644
index 0000000000..c06795b886
--- /dev/null
+++ b/app/page.tsx
@@ -0,0 +1,57 @@
+"use client"
+
+import { ChatbotUISVG } from "@/components/icons/chatbotui-svg"
+import { IconArrowRight } from "@tabler/icons-react"
+import { useTheme } from "next-themes"
+import Link from "next/link"
+import { useEffect, useState } from "react"
+
+export default function HomePage() {
+ const { theme } = useTheme()
+
+ const [stars, setStars] = useState(19000)
+
+ useEffect(() => {
+ getGitHubRepoStars()
+ }, [])
+
+ const getGitHubRepoStars = async () => {
+ const url = `https://api.github.com/repos/mckaywrigley/chatbot-ui`
+
+ try {
+ const response = await fetch(url, {
+ headers: {
+ Accept: "application/vnd.github.v3+json"
+ }
+ })
+
+ if (!response.ok) {
+ throw new Error(`Error: ${response.status}`)
+ }
+
+ const data = await response.json()
+ setStars(data.stargazers_count)
+ } catch (error) {
+ console.error("Failed to fetch GitHub stars:", error)
+ return 0
+ }
+ }
+
+ return (
+
+
+
+
+
+
Chatbot UI
+
+
+ Start Chatting
+
+
+
+ )
+}
diff --git a/app/setup/page.tsx b/app/setup/page.tsx
new file mode 100644
index 0000000000..607b0931ba
--- /dev/null
+++ b/app/setup/page.tsx
@@ -0,0 +1,294 @@
+"use client"
+
+import { ChatbotUIContext } from "@/context/context"
+import { getProfileByUserId, updateProfile } from "@/db/profile"
+import { uploadImage } from "@/db/storage/profile-images"
+import { getWorkspacesByUserId, updateWorkspace } from "@/db/workspaces"
+import { supabase } from "@/lib/supabase/browser-client"
+import { TablesUpdate } from "@/supabase/types"
+import { ChatSettings } from "@/types"
+import { useRouter } from "next/navigation"
+import { useContext, useEffect, useState } from "react"
+import { APIStep } from "../../components/setup/api-step"
+import { FinishStep } from "../../components/setup/finish-step"
+import { ProfileStep } from "../../components/setup/profile-step"
+import {
+ SETUP_STEP_COUNT,
+ StepContainer
+} from "../../components/setup/step-container"
+import { WorkspaceStep } from "../../components/setup/workspace-step"
+
+export default function SetupPage() {
+ const { profile, setProfile, setSelectedWorkspace, setWorkspaces } =
+ useContext(ChatbotUIContext)
+
+ const router = useRouter()
+
+ const [loading, setLoading] = useState(true)
+
+ const [currentStep, setCurrentStep] = useState(1)
+
+ // Profile Step
+ const [profileContext, setProfileContext] = useState("")
+ const [displayName, setDisplayName] = useState("")
+ const [username, setUsername] = useState(profile?.username || "")
+ const [usernameAvailable, setUsernameAvailable] = useState(true)
+ const [profileImageSrc, setProfileImageSrc] = useState("")
+ const [profileImage, setProfileImage] = useState(null)
+
+ // API Step
+ const [useAzureOpenai, setUseAzureOpenai] = useState(false)
+ const [openaiAPIKey, setOpenaiAPIKey] = useState("")
+ const [openaiOrgID, setOpenaiOrgID] = useState("")
+ const [azureOpenaiAPIKey, setAzureOpenaiAPIKey] = useState("")
+ const [azureOpenaiEndpoint, setAzureOpenaiEndpoint] = useState("")
+ const [azureOpenai35TurboID, setAzureOpenai35TurboID] = useState("")
+ const [azureOpenai45TurboID, setAzureOpenai45TurboID] = useState("")
+ const [azureOpenai45VisionID, setAzureOpenai45VisionID] = useState("")
+ const [anthropicAPIKey, setAnthropicAPIKey] = useState("")
+ const [googleGeminiAPIKey, setGoogleGeminiAPIKey] = useState("")
+ const [mistralAPIKey, setMistralAPIKey] = useState("")
+ const [perplexityAPIKey, setPerplexityAPIKey] = useState("")
+
+ // Workspace Step
+ const [workspaceInstructions, setWorkspaceInstructions] = useState("")
+ const [defaultChatSettings, setDefaultChatSettings] = useState({
+ model: "gpt-4-1106-preview",
+ prompt: "You are a friendly, helpful AI assistant.",
+ temperature: 0.5,
+ contextLength: 4096,
+ includeProfileContext: true,
+ includeWorkspaceInstructions: true,
+ embeddingsProvider: "openai"
+ })
+
+ const handleShouldProceed = (proceed: boolean) => {
+ if (proceed) {
+ if (currentStep === SETUP_STEP_COUNT) {
+ handleSaveSetupSetting()
+ } else {
+ setCurrentStep(currentStep + 1)
+ }
+ } else {
+ setCurrentStep(currentStep - 1)
+ }
+ }
+
+ const handleSaveSetupSetting = async () => {
+ if (!profile) return
+
+ let profileImageUrl = ""
+ let profileImagePath = ""
+
+ if (profileImage) {
+ const { path, url } = await uploadImage(profile, profileImage)
+ profileImageUrl = url
+ profileImagePath = path
+ }
+
+ const updateProfilePayload: TablesUpdate<"profiles"> = {
+ ...profile,
+ has_onboarded: true,
+ display_name: displayName,
+ username,
+ profile_context: profileContext,
+ image_url: profileImageUrl,
+ image_path: profileImageUrl,
+ openai_api_key: openaiAPIKey,
+ openai_organization_id: openaiOrgID,
+ anthropic_api_key: anthropicAPIKey,
+ google_gemini_api_key: googleGeminiAPIKey,
+ mistral_api_key: mistralAPIKey,
+ perplexity_api_key: perplexityAPIKey,
+ use_azure_openai: useAzureOpenai,
+ azure_openai_api_key: azureOpenaiAPIKey,
+ azure_openai_endpoint: azureOpenaiEndpoint,
+ azure_openai_35_turbo_id: azureOpenai35TurboID,
+ azure_openai_45_turbo_id: azureOpenai45TurboID,
+ azure_openai_45_vision_id: azureOpenai45VisionID
+ }
+
+ const updatedProfile = await updateProfile(profile.id, updateProfilePayload)
+
+ setProfile(updatedProfile)
+
+ const updateHomeWorkspacePayload: TablesUpdate<"workspaces"> = {
+ default_context_length: defaultChatSettings.contextLength,
+ default_model: defaultChatSettings.model,
+ default_prompt: defaultChatSettings.prompt,
+ default_temperature: defaultChatSettings.temperature,
+ include_profile_context: defaultChatSettings.includeProfileContext,
+ include_workspace_instructions:
+ defaultChatSettings.includeWorkspaceInstructions,
+ instructions: workspaceInstructions,
+ embeddings_provider: defaultChatSettings.embeddingsProvider
+ }
+
+ const workspaces = await getWorkspacesByUserId(profile.user_id)
+ const homeWorkspace = workspaces.find(w => w.is_home)
+
+ // There will always be a home workspace
+ const updatedWorkspace = await updateWorkspace(
+ homeWorkspace!.id,
+ updateHomeWorkspacePayload
+ )
+
+ setSelectedWorkspace(updatedWorkspace)
+ setWorkspaces(
+ workspaces.map(workspace =>
+ workspace.id === updatedWorkspace.id ? updatedWorkspace : workspace
+ )
+ )
+
+ router.push("/chat")
+ }
+
+ const renderStep = (stepNum: number) => {
+ switch (stepNum) {
+ // Profile Step
+ case 1:
+ return (
+
+
+
+ )
+
+ // API Step
+ case 2:
+ return (
+
+
+
+ )
+
+ // Workspace Step
+ case 3:
+ return (
+
+
+
+ )
+
+ // Finish Step
+ case 4:
+ return (
+
+
+
+ )
+ default:
+ return null
+ }
+ }
+
+ useEffect(() => {
+ ;(async () => {
+ const session = (await supabase.auth.getSession()).data.session
+
+ if (!session) {
+ router.push("/login")
+ } else {
+ const user = session.user
+
+ const profile = await getProfileByUserId(user.id)
+ setProfile(profile)
+
+ if (!profile.has_onboarded) {
+ setLoading(false)
+ } else {
+ router.push("/chat")
+ }
+ }
+ })()
+ }, [])
+
+ if (loading) {
+ return null
+ }
+
+ return (
+
+ {renderStep(currentStep)}
+
+ )
+}
diff --git a/app/share/assistant/[id]/page.tsx b/app/share/assistant/[id]/page.tsx
new file mode 100644
index 0000000000..c8c0064bee
--- /dev/null
+++ b/app/share/assistant/[id]/page.tsx
@@ -0,0 +1,65 @@
+"use client"
+
+import { ShareAssistant } from "@/components/sharing/share-assistant"
+import { ShareHeader } from "@/components/sharing/share-header"
+import { ScreenLoader } from "@/components/ui/screen-loader"
+import { getAssistantById } from "@/db/assistants"
+import { supabase } from "@/lib/supabase/browser-client"
+import { Tables } from "@/supabase/types"
+import { useEffect, useState } from "react"
+
+interface ShareAssistantPageProps {
+ params: {
+ id: string
+ }
+}
+
+export default function ShareAssistantPage({
+ params
+}: ShareAssistantPageProps) {
+ const [session, setSession] = useState(null)
+ const [loading, setLoading] = useState(true)
+ const [assistant, setAssistant] = useState | null>(null)
+
+ const onLoad = async () => {
+ const session = (await supabase.auth.getSession()).data.session
+
+ setSession(session)
+
+ const fetchedAssistant = await getAssistantById(params.id)
+ setAssistant(fetchedAssistant)
+
+ if (!fetchedAssistant) {
+ setLoading(false)
+ return
+ }
+
+ setLoading(false)
+ }
+
+ useEffect(() => {
+ onLoad()
+ }, [])
+
+ if (loading) {
+ return
+ }
+
+ if (!assistant) {
+ return (
+
+ Assistant not found.
+
+ )
+ }
+
+ return (
+
+ )
+}
diff --git a/app/share/chat/[id]/page.tsx b/app/share/chat/[id]/page.tsx
new file mode 100644
index 0000000000..613044e0cc
--- /dev/null
+++ b/app/share/chat/[id]/page.tsx
@@ -0,0 +1,73 @@
+"use client"
+
+import { ShareChat } from "@/components/sharing/share-chat"
+import { ShareHeader } from "@/components/sharing/share-header"
+import { ScreenLoader } from "@/components/ui/screen-loader"
+import { getChatById } from "@/db/chats"
+import { getMessagesByChatId } from "@/db/messages"
+import { supabase } from "@/lib/supabase/browser-client"
+import { Tables } from "@/supabase/types"
+import { useEffect, useState } from "react"
+
+interface ShareChatPageProps {
+ params: {
+ id: string
+ }
+}
+
+export default function ShareChatPage({ params }: ShareChatPageProps) {
+ const [session, setSession] = useState(null)
+ const [loading, setLoading] = useState(true)
+ const [chat, setChat] = useState | null>(null)
+ const [messages, setMessages] = useState[]>([])
+
+ const onLoad = async () => {
+ const session = (await supabase.auth.getSession()).data.session
+
+ setSession(session)
+
+ const fetchedChat = await getChatById(params.id)
+ setChat(fetchedChat)
+
+ if (!fetchedChat) {
+ setLoading(false)
+ return
+ }
+
+ const fetchedMessages = await getMessagesByChatId(fetchedChat.id)
+ setMessages(fetchedMessages)
+
+ setLoading(false)
+ }
+
+ useEffect(() => {
+ onLoad()
+ }, [])
+
+ if (loading) {
+ return
+ }
+
+ if (!chat) {
+ return (
+
+ Chat not found.
+
+ )
+ }
+
+ return (
+
+ )
+}
diff --git a/app/share/collection/[id]/page.tsx b/app/share/collection/[id]/page.tsx
new file mode 100644
index 0000000000..af4fc50ff9
--- /dev/null
+++ b/app/share/collection/[id]/page.tsx
@@ -0,0 +1,75 @@
+"use client"
+
+import { ShareCollection } from "@/components/sharing/share-collection"
+import { ShareHeader } from "@/components/sharing/share-header"
+import { ScreenLoader } from "@/components/ui/screen-loader"
+import { getCollectionFilesByCollectionId } from "@/db/collection-files"
+import { getCollectionById } from "@/db/collections"
+import { supabase } from "@/lib/supabase/browser-client"
+import { Tables } from "@/supabase/types"
+import { CollectionFile } from "@/types"
+import { useEffect, useState } from "react"
+
+interface ShareCollectionPageProps {
+ params: {
+ id: string
+ }
+}
+
+export default function ShareCollectionPage({
+ params
+}: ShareCollectionPageProps) {
+ const [session, setSession] = useState(null)
+ const [loading, setLoading] = useState(true)
+ const [collection, setCollection] = useState | null>(
+ null
+ )
+ const [collectionFiles, setCollectionFiles] = useState([])
+
+ const onLoad = async () => {
+ const session = (await supabase.auth.getSession()).data.session
+
+ setSession(session)
+
+ const fetchedCollection = await getCollectionById(params.id)
+ setCollection(fetchedCollection)
+
+ if (!fetchedCollection) {
+ setLoading(false)
+ return
+ }
+
+ const fetchedCollectionFiles = await getCollectionFilesByCollectionId(
+ fetchedCollection.id
+ )
+ setCollectionFiles(fetchedCollectionFiles.files)
+
+ setLoading(false)
+ }
+
+ useEffect(() => {
+ onLoad()
+ }, [])
+
+ if (loading) {
+ return
+ }
+
+ if (!collection) {
+ return (
+
+ Collection not found.
+
+ )
+ }
+
+ return (
+
+ )
+}
diff --git a/app/share/file/[id]/page.tsx b/app/share/file/[id]/page.tsx
new file mode 100644
index 0000000000..34dd8e49e2
--- /dev/null
+++ b/app/share/file/[id]/page.tsx
@@ -0,0 +1,72 @@
+"use client"
+
+import { ShareFile } from "@/components/sharing/share-file"
+import { ShareHeader } from "@/components/sharing/share-header"
+import { ScreenLoader } from "@/components/ui/screen-loader"
+import { getFileById } from "@/db/files"
+import { getFileFromStorage } from "@/db/storage/files"
+import { supabase } from "@/lib/supabase/browser-client"
+import { Tables } from "@/supabase/types"
+import { useEffect, useState } from "react"
+
+interface ShareFilePageProps {
+ params: {
+ id: string
+ }
+}
+
+export default function ShareFilePage({ params }: ShareFilePageProps) {
+ const [session, setSession] = useState(null)
+ const [loading, setLoading] = useState(true)
+ const [file, setFile] = useState | null>(null)
+ const [link, setLink] = useState("")
+
+ const onLoad = async () => {
+ try {
+ const session = (await supabase.auth.getSession()).data.session
+
+ setSession(session)
+
+ const fetchedFile = await getFileById(params.id)
+ setFile(fetchedFile)
+
+ if (!fetchedFile) {
+ setLoading(false)
+ return
+ }
+
+ const fileLink = await getFileFromStorage(fetchedFile.file_path)
+ setLink(fileLink)
+
+ setLoading(false)
+ } catch (error) {
+ setLoading(false)
+ }
+ }
+
+ useEffect(() => {
+ onLoad()
+ }, [])
+
+ if (loading) {
+ return
+ }
+
+ if (!file) {
+ return (
+
+ File not found.
+
+ )
+ }
+
+ return (
+
+ )
+}
diff --git a/app/share/preset/[id]/page.tsx b/app/share/preset/[id]/page.tsx
new file mode 100644
index 0000000000..5767ae1ce7
--- /dev/null
+++ b/app/share/preset/[id]/page.tsx
@@ -0,0 +1,22 @@
+"use client"
+
+import SharePage from "@/components/sharing/share-page"
+import { SharePreset } from "@/components/sharing/share-preset"
+import { getPresetById } from "@/db/presets"
+
+interface SharePresetPageProps {
+ params: {
+ id: string
+ }
+}
+
+export default function SharePresetPage({ params }: SharePresetPageProps) {
+ return (
+
+ )
+}
diff --git a/app/share/prompt/[id]/page.tsx b/app/share/prompt/[id]/page.tsx
new file mode 100644
index 0000000000..fecfa5c153
--- /dev/null
+++ b/app/share/prompt/[id]/page.tsx
@@ -0,0 +1,22 @@
+"use client"
+
+import SharePage from "@/components/sharing/share-page"
+import { SharePrompt } from "@/components/sharing/share-prompt"
+import { getPromptById } from "@/db/prompts"
+
+interface SharePromptPageProps {
+ params: {
+ id: string
+ }
+}
+
+export default function SharePromptPage({ params }: SharePromptPageProps) {
+ return (
+
+ )
+}
diff --git a/components.json b/components.json
new file mode 100644
index 0000000000..433a5ad510
--- /dev/null
+++ b/components.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "default",
+ "rsc": true,
+ "tsx": true,
+ "tailwind": {
+ "config": "tailwind.config.js",
+ "css": "app/globals.css",
+ "baseColor": "gray",
+ "cssVariables": true
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils"
+ }
+}
diff --git a/components/chat/chat-command-input.tsx b/components/chat/chat-command-input.tsx
new file mode 100644
index 0000000000..a2e498398a
--- /dev/null
+++ b/components/chat/chat-command-input.tsx
@@ -0,0 +1,43 @@
+import { ChatbotUIContext } from "@/context/context"
+import { FC, useContext } from "react"
+import { usePromptAndCommand } from "./chat-hooks/use-prompt-and-command"
+import { FilePicker } from "./file-picker"
+import { PromptPicker } from "./prompt-picker"
+
+interface ChatCommandInputProps {}
+
+export const ChatCommandInput: FC = ({}) => {
+ const {
+ chatFiles,
+ slashCommand,
+ isAtPickerOpen,
+ setIsAtPickerOpen,
+ atCommand,
+
+ focusPrompt,
+ focusFile
+ } = useContext(ChatbotUIContext)
+
+ const { handleSelectPrompt, handleSelectUserFile } = usePromptAndCommand()
+
+ return (
+ <>
+
+
+ file.id)}
+ selectedCollectionIds={[]}
+ onSelectFile={handleSelectUserFile}
+ onSelectCollection={() => {}}
+ isFocused={focusFile}
+ />
+ >
+ )
+}
diff --git a/components/chat/chat-files-display.tsx b/components/chat/chat-files-display.tsx
new file mode 100644
index 0000000000..8a999a96f0
--- /dev/null
+++ b/components/chat/chat-files-display.tsx
@@ -0,0 +1,193 @@
+import { ChatbotUIContext } from "@/context/context"
+import { getFileFromStorage } from "@/db/storage/files"
+import { ChatFile, MessageImage } from "@/types"
+import {
+ IconFileFilled,
+ IconFileTypePdf,
+ IconLoader2,
+ IconX
+} from "@tabler/icons-react"
+import Image from "next/image"
+import { FC, useContext, useState } from "react"
+import { Button } from "../ui/button"
+import { FilePreview } from "../ui/file-preview"
+
+interface ChatFilesDisplayProps {}
+
+export const ChatFilesDisplay: FC = ({}) => {
+ const {
+ files,
+ newMessageImages,
+ setNewMessageImages,
+ newMessageFiles,
+ setNewMessageFiles,
+ setShowFilesDisplay,
+ showFilesDisplay
+ } = useContext(ChatbotUIContext)
+
+ const [selectedFile, setSelectedFile] = useState(null)
+ const [selectedImage, setSelectedImage] = useState(null)
+ const [showPreview, setShowPreview] = useState(false)
+
+ const combinedNewMessageFiles = [...newMessageFiles, ...newMessageImages]
+
+ const getLinkAndView = async (file: ChatFile) => {
+ const fileRecord = files.find(f => f.id === file.id)
+
+ if (!fileRecord) return
+
+ const link = await getFileFromStorage(fileRecord.file_path)
+ window.open(link, "_blank")
+ }
+
+ return showFilesDisplay && combinedNewMessageFiles.length > 0 ? (
+ <>
+ {showPreview && selectedImage && (
+ {
+ setShowPreview(isOpen)
+ setSelectedImage(null)
+ }}
+ />
+ )}
+
+ {showPreview && selectedFile && (
+ {
+ setShowPreview(isOpen)
+ setSelectedFile(null)
+ }}
+ />
+ )}
+
+
+
+
+
+
+
+
+ {newMessageImages.map(image => (
+
+ {
+ setSelectedImage(image)
+ setShowPreview(true)
+ }}
+ />
+
+ {
+ e.stopPropagation()
+ setNewMessageImages(
+ newMessageImages.filter(
+ f => f.messageId !== image.messageId
+ )
+ )
+ }}
+ />
+
+ ))}
+
+ {newMessageFiles.map(file =>
+ file.id === "loading" ? (
+
+
+
+
+
+
+
{file.name}
+
{file.type}
+
+
+ ) : (
+
getLinkAndView(file)}
+ >
+
+ {(() => {
+ let fileExtension = file.type.includes("/")
+ ? file.type.split("/")[1]
+ : file.type
+
+ switch (fileExtension) {
+ case "pdf":
+ return
+ default:
+ return
+ }
+ })()}
+
+
+
+
{file.name}
+
+ {file.type.includes("/")
+ ? file.type.split("/")[1].toUpperCase()
+ : file.type.toUpperCase()}
+
+
+
+
{
+ e.stopPropagation()
+ setNewMessageFiles(
+ newMessageFiles.filter(f => f.id !== file.id)
+ )
+ }}
+ />
+
+ )
+ )}
+
+
+
+ >
+ ) : (
+ combinedNewMessageFiles.length > 0 && (
+
+
+
+ )
+ )
+}
diff --git a/components/chat/chat-help.tsx b/components/chat/chat-help.tsx
new file mode 100644
index 0000000000..377d48d0e6
--- /dev/null
+++ b/components/chat/chat-help.tsx
@@ -0,0 +1,178 @@
+import useHotkey from "@/lib/hooks/use-hotkey"
+import {
+ IconBrandGithub,
+ IconBrandX,
+ IconHelpCircle,
+ IconQuestionMark
+} from "@tabler/icons-react"
+import Link from "next/link"
+import { FC, useState } from "react"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger
+} from "../ui/dropdown-menu"
+import { Announcements } from "../utility/announcements"
+
+interface ChatHelpProps {}
+
+export const ChatHelp: FC = ({}) => {
+ useHotkey("/", () => setIsOpen(prevState => !prevState))
+
+ const [isOpen, setIsOpen] = useState(false)
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Show Help
+
+
+ ⌘
+
+
+ Shift
+
+
+ /
+
+
+
+
+
+ Show Workspaces
+
+
+ ⌘
+
+
+ Shift
+
+
+ ;
+
+
+
+
+
+ New Chat
+
+
+ ⌘
+
+
+ Shift
+
+
+ O
+
+
+
+
+
+ Focus Chat
+
+
+ ⌘
+
+
+ Shift
+
+
+ L
+
+
+
+
+
+ Open Settings
+
+
+ ⌘
+
+
+ Shift
+
+
+ I
+
+
+
+
+
+ Open Presets
+
+
+ ⌘
+
+
+ Shift
+
+
+ P
+
+
+
+
+
+ Toggle Sidebar
+
+
+ ⌘
+
+
+ Shift
+
+
+ S
+
+
+
+
+
+ )
+}
diff --git a/components/chat/chat-helpers/index.ts b/components/chat/chat-helpers/index.ts
new file mode 100644
index 0000000000..3ad15648c0
--- /dev/null
+++ b/components/chat/chat-helpers/index.ts
@@ -0,0 +1,472 @@
+// Only used in use-chat-handler.tsx to keep it clean
+
+import { createChatFiles } from "@/db/chat-files"
+import { createChat } from "@/db/chats"
+import { createMessageFileItems } from "@/db/message-file-items"
+import { createMessages, updateMessage } from "@/db/messages"
+import { uploadMessageImage } from "@/db/storage/message-images"
+import {
+ buildFinalMessages,
+ buildGoogleGeminiFinalMessages
+} from "@/lib/build-prompt"
+import { consumeReadableStream } from "@/lib/consume-stream"
+import { Tables, TablesInsert } from "@/supabase/types"
+import {
+ ChatFile,
+ ChatMessage,
+ ChatPayload,
+ ChatSettings,
+ LLM,
+ MessageImage
+} from "@/types"
+import React from "react"
+import { toast } from "sonner"
+import { v4 as uuidv4 } from "uuid"
+
+export const validateChatSettings = (
+ chatSettings: ChatSettings | null,
+ modelData: LLM | undefined,
+ profile: Tables<"profiles"> | null,
+ selectedWorkspace: Tables<"workspaces"> | null,
+ messageContent: string
+) => {
+ if (!chatSettings) {
+ throw new Error("Chat settings not found")
+ }
+
+ if (!modelData) {
+ throw new Error("Model not found")
+ }
+
+ if (!profile) {
+ throw new Error("Profile not found")
+ }
+
+ if (!selectedWorkspace) {
+ throw new Error("Workspace not found")
+ }
+
+ if (!messageContent) {
+ throw new Error("Message content not found")
+ }
+}
+
+export const handleRetrieval = async (
+ userInput: string,
+ newMessageFiles: ChatFile[],
+ chatFiles: ChatFile[],
+ embeddingsProvider: "openai" | "local"
+) => {
+ const response = await fetch("/api/retrieval/retrieve", {
+ method: "POST",
+ body: JSON.stringify({
+ userInput,
+ fileIds: [...newMessageFiles, ...chatFiles].map(file => file.id),
+ embeddingsProvider
+ })
+ })
+
+ if (!response.ok) {
+ console.error("Error retrieving:", response)
+ }
+
+ const { results } = (await response.json()) as {
+ results: Tables<"file_items">[]
+ }
+
+ return results
+}
+
+export const createTempMessages = (
+ messageContent: string,
+ chatMessages: ChatMessage[],
+ chatSettings: ChatSettings,
+ b64Images: string[],
+ isRegeneration: boolean,
+ setChatMessages: React.Dispatch>
+) => {
+ let tempUserChatMessage: ChatMessage = {
+ message: {
+ chat_id: "",
+ content: messageContent,
+ created_at: "",
+ id: uuidv4(),
+ image_paths: b64Images,
+ model: chatSettings.model,
+ role: "user",
+ sequence_number: chatMessages.length,
+ updated_at: "",
+ user_id: ""
+ },
+ fileItems: []
+ }
+
+ let tempAssistantChatMessage: ChatMessage = {
+ message: {
+ chat_id: "",
+ content: "",
+ created_at: "",
+ id: uuidv4(),
+ image_paths: [],
+ model: chatSettings.model,
+ role: "assistant",
+ sequence_number: chatMessages.length + 1,
+ updated_at: "",
+ user_id: ""
+ },
+ fileItems: []
+ }
+
+ let newMessages = []
+
+ if (isRegeneration) {
+ const lastMessageIndex = chatMessages.length - 1
+ chatMessages[lastMessageIndex].message.content = ""
+ newMessages = [...chatMessages]
+ } else {
+ newMessages = [
+ ...chatMessages,
+ tempUserChatMessage,
+ tempAssistantChatMessage
+ ]
+ }
+
+ setChatMessages(newMessages)
+
+ return {
+ tempUserChatMessage,
+ tempAssistantChatMessage
+ }
+}
+
+export const handleLocalChat = async (
+ payload: ChatPayload,
+ profile: Tables<"profiles">,
+ chatSettings: ChatSettings,
+ tempAssistantMessage: ChatMessage,
+ isRegeneration: boolean,
+ newAbortController: AbortController,
+ setIsGenerating: React.Dispatch>,
+ setFirstTokenReceived: React.Dispatch>,
+ setChatMessages: React.Dispatch>,
+ setToolInUse: React.Dispatch>
+) => {
+ const formattedMessages = await buildFinalMessages(payload, profile, [])
+
+ // Ollama API: https://github.com/jmorganca/ollama/blob/main/docs/api.md
+ const response = await fetchChatResponse(
+ process.env.NEXT_PUBLIC_OLLAMA_URL + "/api/chat",
+ {
+ model: chatSettings.model,
+ messages: formattedMessages,
+ options: {
+ temperature: payload.chatSettings.temperature
+ }
+ },
+ false,
+ newAbortController,
+ setIsGenerating,
+ setChatMessages
+ )
+
+ return await processResponse(
+ response,
+ isRegeneration
+ ? payload.chatMessages[payload.chatMessages.length - 1]
+ : tempAssistantMessage,
+ false,
+ newAbortController,
+ setFirstTokenReceived,
+ setChatMessages,
+ setToolInUse
+ )
+}
+
+export const handleHostedChat = async (
+ payload: ChatPayload,
+ profile: Tables<"profiles">,
+ modelData: LLM,
+ tempAssistantChatMessage: ChatMessage,
+ isRegeneration: boolean,
+ newAbortController: AbortController,
+ newMessageImages: MessageImage[],
+ chatImages: MessageImage[],
+ setIsGenerating: React.Dispatch>,
+ setFirstTokenReceived: React.Dispatch>,
+ setChatMessages: React.Dispatch>,
+ setToolInUse: React.Dispatch>
+) => {
+ const provider =
+ modelData.provider === "openai" && profile.use_azure_openai
+ ? "azure"
+ : modelData.provider
+
+ let formattedMessages = []
+
+ if (provider === "google") {
+ formattedMessages = await buildGoogleGeminiFinalMessages(
+ payload,
+ profile,
+ newMessageImages
+ )
+ } else {
+ formattedMessages = await buildFinalMessages(payload, profile, chatImages)
+ }
+
+ const response = await fetchChatResponse(
+ `/api/chat/${provider}`,
+ {
+ chatSettings: payload.chatSettings,
+ messages: formattedMessages
+ },
+ true,
+ newAbortController,
+ setIsGenerating,
+ setChatMessages
+ )
+
+ return await processResponse(
+ response,
+ isRegeneration
+ ? payload.chatMessages[payload.chatMessages.length - 1]
+ : tempAssistantChatMessage,
+ true,
+ newAbortController,
+ setFirstTokenReceived,
+ setChatMessages,
+ setToolInUse
+ )
+}
+
+export const fetchChatResponse = async (
+ url: string,
+ body: object,
+ isHosted: boolean,
+ controller: AbortController,
+ setIsGenerating: React.Dispatch>,
+ setChatMessages: React.Dispatch>
+) => {
+ const response = await fetch(url, {
+ method: "POST",
+ body: JSON.stringify(body),
+ signal: controller.signal
+ })
+
+ if (!response.ok) {
+ if (response.status === 404 && !isHosted) {
+ toast.error(
+ "Model not found. Make sure you have it downloaded via Ollama."
+ )
+ }
+
+ const errorData = await response.json()
+
+ toast.error(errorData.message)
+
+ setIsGenerating(false)
+ setChatMessages(prevMessages => prevMessages.slice(0, -2))
+ }
+
+ return response
+}
+
+export const processResponse = async (
+ response: Response,
+ lastChatMessage: ChatMessage,
+ isHosted: boolean,
+ controller: AbortController,
+ setFirstTokenReceived: React.Dispatch>,
+ setChatMessages: React.Dispatch>,
+ setToolInUse: React.Dispatch>
+) => {
+ let fullText = ""
+ let contentToAdd = ""
+
+ await consumeReadableStream(
+ response.body,
+ (chunk: any) => {
+ setFirstTokenReceived(true)
+ setToolInUse("none")
+
+ try {
+ contentToAdd = isHosted ? chunk : JSON.parse(chunk).message.content
+ fullText += contentToAdd
+ } catch (error) {
+ console.error("Error parsing JSON:", error)
+ }
+
+ setChatMessages(prev =>
+ prev.map(chatMessage => {
+ if (chatMessage.message.id === lastChatMessage.message.id) {
+ const updatedChatMessage: ChatMessage = {
+ message: {
+ ...chatMessage.message,
+ content: chatMessage.message.content + contentToAdd
+ },
+ fileItems: chatMessage.fileItems
+ }
+
+ return updatedChatMessage
+ }
+
+ return chatMessage
+ })
+ )
+ },
+ controller.signal
+ )
+
+ return fullText
+}
+
+export const handleCreateChat = async (
+ chatSettings: ChatSettings,
+ profile: Tables<"profiles">,
+ selectedWorkspace: Tables<"workspaces">,
+ messageContent: string,
+ selectedAssistant: Tables<"assistants">,
+ newMessageFiles: ChatFile[],
+ setSelectedChat: React.Dispatch | null>>,
+ setChats: React.Dispatch[]>>,
+ setChatFiles: React.Dispatch>
+) => {
+ const createdChat = await createChat({
+ user_id: profile.user_id,
+ workspace_id: selectedWorkspace.id,
+ assistant_id: selectedAssistant?.id || null,
+ context_length: chatSettings.contextLength,
+ include_profile_context: chatSettings.includeProfileContext,
+ include_workspace_instructions: chatSettings.includeWorkspaceInstructions,
+ model: chatSettings.model,
+ name: messageContent.substring(0, 100),
+ prompt: chatSettings.prompt,
+ temperature: chatSettings.temperature,
+ embeddings_provider: chatSettings.embeddingsProvider
+ })
+
+ setSelectedChat(createdChat)
+ setChats(chats => [createdChat, ...chats])
+
+ await createChatFiles(
+ newMessageFiles.map(file => ({
+ user_id: profile.user_id,
+ chat_id: createdChat.id,
+ file_id: file.id
+ }))
+ )
+
+ setChatFiles(prev => [...prev, ...newMessageFiles])
+
+ return createdChat
+}
+
+export const handleCreateMessages = async (
+ chatMessages: ChatMessage[],
+ currentChat: Tables<"chats">,
+ profile: Tables<"profiles">,
+ modelData: LLM,
+ messageContent: string,
+ generatedText: string,
+ newMessageImages: MessageImage[],
+ isRegeneration: boolean,
+ retrievedFileItems: Tables<"file_items">[],
+ setChatMessages: React.Dispatch>,
+ setChatFileItems: React.Dispatch[]>>
+) => {
+ const finalUserMessage: TablesInsert<"messages"> = {
+ chat_id: currentChat.id,
+ user_id: profile.user_id,
+ content: messageContent,
+ model: modelData.modelId,
+ role: "user",
+ sequence_number: chatMessages.length,
+ image_paths: []
+ }
+
+ const finalAssistantMessage: TablesInsert<"messages"> = {
+ chat_id: currentChat.id,
+ user_id: profile.user_id,
+ content: generatedText,
+ model: modelData.modelId,
+ role: "assistant",
+ sequence_number: chatMessages.length + 1,
+ image_paths: []
+ }
+
+ let finalChatMessages: ChatMessage[] = []
+
+ if (isRegeneration) {
+ const lastStartingMessage = chatMessages[chatMessages.length - 1].message
+
+ const updatedMessage = await updateMessage(lastStartingMessage.id, {
+ ...lastStartingMessage,
+ content: generatedText
+ })
+
+ chatMessages[chatMessages.length - 1].message = updatedMessage
+
+ finalChatMessages = [...chatMessages]
+
+ setChatMessages(finalChatMessages)
+ } else {
+ const createdMessages = await createMessages([
+ finalUserMessage,
+ finalAssistantMessage
+ ])
+
+ // Upload each image (stored in newMessageImages) for the user message to message_images bucket
+ const uploadPromises = newMessageImages
+ .filter(obj => obj.file !== null)
+ .map(obj => {
+ let filePath = `${profile.user_id}/${currentChat.id}/${
+ createdMessages[0].id
+ }/${uuidv4()}`
+
+ return uploadMessageImage(filePath, obj.file as File).catch(error => {
+ console.error(`Failed to upload image at ${filePath}:`, error)
+ return null
+ })
+ })
+
+ const paths = (await Promise.all(uploadPromises)).filter(
+ Boolean
+ ) as string[]
+
+ updateMessage(createdMessages[0].id, {
+ ...createdMessages[0],
+ image_paths: paths
+ })
+
+ const createdMessageFileItems = await createMessageFileItems(
+ retrievedFileItems.map(fileItem => {
+ return {
+ user_id: profile.user_id,
+ message_id: createdMessages[1].id,
+ file_item_id: fileItem.id
+ }
+ })
+ )
+
+ finalChatMessages = [
+ ...chatMessages,
+ {
+ message: createdMessages[0],
+ fileItems: []
+ },
+ {
+ message: createdMessages[1],
+ fileItems: retrievedFileItems.map(fileItem => fileItem.id)
+ }
+ ]
+
+ setChatFileItems(prevFileItems => {
+ const newFileItems = retrievedFileItems.filter(
+ fileItem => !prevFileItems.some(prevItem => prevItem.id === fileItem.id)
+ )
+
+ return [...prevFileItems, ...newFileItems]
+ })
+
+ setChatMessages(finalChatMessages)
+ }
+}
diff --git a/components/chat/chat-hooks/use-chat-handler.tsx b/components/chat/chat-hooks/use-chat-handler.tsx
new file mode 100644
index 0000000000..8b0783e55b
--- /dev/null
+++ b/components/chat/chat-hooks/use-chat-handler.tsx
@@ -0,0 +1,249 @@
+import { ChatbotUIContext } from "@/context/context"
+import { deleteMessagesIncludingAndAfter } from "@/db/messages"
+import { Tables } from "@/supabase/types"
+import { ChatMessage, ChatPayload } from "@/types"
+import { useRouter } from "next/navigation"
+import { useContext, useRef } from "react"
+import { LLM_LIST } from "../../../lib/models/llm/llm-list"
+import {
+ createTempMessages,
+ handleCreateChat,
+ handleCreateMessages,
+ handleHostedChat,
+ handleLocalChat,
+ handleRetrieval,
+ validateChatSettings
+} from "../chat-helpers"
+
+export const useChatHandler = () => {
+ const {
+ userInput,
+ chatFiles,
+ setUserInput,
+ setNewMessageImages,
+ profile,
+ setIsGenerating,
+ setChatMessages,
+ setFirstTokenReceived,
+ selectedChat,
+ selectedWorkspace,
+ setSelectedChat,
+ setChats,
+ availableLocalModels,
+ abortController,
+ setAbortController,
+ chatSettings,
+ newMessageImages,
+ selectedAssistant,
+ chatMessages,
+ chatImages,
+ setChatImages,
+ setChatFiles,
+ setNewMessageFiles,
+ setShowFilesDisplay,
+ newMessageFiles,
+ chatFileItems,
+ setChatFileItems,
+ setToolInUse
+ } = useContext(ChatbotUIContext)
+
+ const router = useRouter()
+
+ const chatInputRef = useRef(null)
+
+ const handleNewChat = () => {
+ setUserInput("")
+ setChatMessages([])
+ setSelectedChat(null)
+ setChatFileItems([])
+
+ setIsGenerating(false)
+ setFirstTokenReceived(false)
+
+ setChatFiles([])
+ setChatImages([])
+ setNewMessageFiles([])
+ setNewMessageImages([])
+ setShowFilesDisplay(false)
+
+ router.push("/chat")
+ }
+
+ const handleFocusChatInput = () => {
+ chatInputRef.current?.focus()
+ }
+
+ const handleStopMessage = () => {
+ if (abortController) {
+ abortController.abort()
+ }
+ }
+
+ const handleSendMessage = async (
+ messageContent: string,
+ chatMessages: ChatMessage[],
+ isRegeneration: boolean
+ ) => {
+ setIsGenerating(true)
+
+ const newAbortController = new AbortController()
+ setAbortController(newAbortController)
+
+ const modelData = [...LLM_LIST, ...availableLocalModels].find(
+ llm => llm.modelId === chatSettings?.model
+ )
+
+ try {
+ validateChatSettings(
+ chatSettings,
+ modelData,
+ profile,
+ selectedWorkspace,
+ messageContent
+ )
+ } catch (error) {
+ setIsGenerating(false)
+ return
+ }
+
+ let currentChat = selectedChat ? { ...selectedChat } : null
+
+ const b64Images = newMessageImages.map(image => image.base64)
+
+ let retrievedFileItems: Tables<"file_items">[] = []
+
+ if (newMessageFiles.length > 0 || chatFiles.length > 0) {
+ setToolInUse("retrieval")
+
+ retrievedFileItems = await handleRetrieval(
+ userInput,
+ newMessageFiles,
+ chatFiles,
+ chatSettings!.embeddingsProvider
+ )
+
+ console.log("retrievedFileItems", retrievedFileItems)
+ }
+
+ const { tempUserChatMessage, tempAssistantChatMessage } =
+ createTempMessages(
+ messageContent,
+ chatMessages,
+ chatSettings!,
+ b64Images,
+ isRegeneration,
+ setChatMessages
+ )
+
+ const payload: ChatPayload = {
+ chatSettings: chatSettings!,
+ workspaceInstructions: selectedWorkspace!.instructions || "",
+ chatMessages: isRegeneration
+ ? [...chatMessages]
+ : [...chatMessages, tempUserChatMessage],
+ assistant: selectedChat?.assistant_id ? selectedAssistant : null,
+ messageFileItems: retrievedFileItems
+ }
+
+ let generatedText = ""
+
+ if (modelData!.provider === "ollama") {
+ generatedText = await handleLocalChat(
+ payload,
+ profile!,
+ chatSettings!,
+ tempAssistantChatMessage,
+ isRegeneration,
+ newAbortController,
+ setIsGenerating,
+ setFirstTokenReceived,
+ setChatMessages,
+ setToolInUse
+ )
+ } else {
+ generatedText = await handleHostedChat(
+ payload,
+ profile!,
+ modelData!,
+ tempAssistantChatMessage,
+ isRegeneration,
+ newAbortController,
+ newMessageImages,
+ chatImages,
+ setIsGenerating,
+ setFirstTokenReceived,
+ setChatMessages,
+ setToolInUse
+ )
+ }
+
+ if (!currentChat) {
+ currentChat = await handleCreateChat(
+ chatSettings!,
+ profile!,
+ selectedWorkspace!,
+ messageContent,
+ selectedAssistant!,
+ newMessageFiles,
+ setSelectedChat,
+ setChats,
+ setChatFiles
+ )
+ }
+
+ if (!currentChat) {
+ throw new Error("Chat not found")
+ }
+
+ await handleCreateMessages(
+ chatMessages,
+ currentChat,
+ profile!,
+ modelData!,
+ messageContent,
+ generatedText,
+ newMessageImages,
+ isRegeneration,
+ retrievedFileItems,
+ setChatMessages,
+ setChatFileItems
+ )
+
+ setIsGenerating(false)
+ setFirstTokenReceived(false)
+ setUserInput("")
+ setNewMessageImages([])
+ setNewMessageFiles([])
+ }
+
+ const handleSendEdit = async (
+ editedContent: string,
+ sequenceNumber: number
+ ) => {
+ if (!selectedChat) return
+
+ await deleteMessagesIncludingAndAfter(
+ selectedChat.user_id,
+ selectedChat.id,
+ sequenceNumber
+ )
+
+ const filteredMessages = chatMessages.filter(
+ chatMessage => chatMessage.message.sequence_number < sequenceNumber
+ )
+
+ setChatMessages(filteredMessages)
+
+ handleSendMessage(editedContent, filteredMessages, false)
+ }
+
+ return {
+ chatInputRef,
+ prompt,
+ handleNewChat,
+ handleSendMessage,
+ handleFocusChatInput,
+ handleStopMessage,
+ handleSendEdit
+ }
+}
diff --git a/components/chat/chat-hooks/use-prompt-and-command.tsx b/components/chat/chat-hooks/use-prompt-and-command.tsx
new file mode 100644
index 0000000000..9b9c008812
--- /dev/null
+++ b/components/chat/chat-hooks/use-prompt-and-command.tsx
@@ -0,0 +1,70 @@
+import { ChatbotUIContext } from "@/context/context"
+import { Tables } from "@/supabase/types"
+import { useContext } from "react"
+
+export const usePromptAndCommand = () => {
+ const {
+ selectedChat,
+ chatFiles,
+ setChatFiles,
+ setNewMessageFiles,
+ userInput,
+ setUserInput,
+ setShowFilesDisplay,
+ setIsPromptPickerOpen,
+ setIsAtPickerOpen,
+ setSlashCommand,
+ setAtCommand
+ } = useContext(ChatbotUIContext)
+
+ const handleInputChange = (value: string) => {
+ const slashTextRegex = /\/([^ ]*)$/
+ const atTextRegex = /@([^ ]*)$/
+ const slashMatch = value.match(slashTextRegex)
+ const atMatch = value.match(atTextRegex)
+
+ if (slashMatch) {
+ setIsPromptPickerOpen(true)
+ setSlashCommand(slashMatch[1])
+ } else if (atMatch) {
+ setIsAtPickerOpen(true)
+ setAtCommand(atMatch[1])
+ } else {
+ setIsPromptPickerOpen(false)
+ setIsAtPickerOpen(false)
+ setSlashCommand("")
+ setAtCommand("")
+ }
+
+ setUserInput(value)
+ }
+
+ const handleSelectPrompt = (prompt: Tables<"prompts">) => {
+ setIsPromptPickerOpen(false)
+ setUserInput(userInput.replace(/\/[^ ]*$/, "") + prompt.content)
+ }
+
+ const handleSelectUserFile = async (file: Tables<"files">) => {
+ setShowFilesDisplay(true)
+ setIsAtPickerOpen(false)
+
+ setNewMessageFiles(prev => [
+ ...prev,
+ ...chatFiles,
+ {
+ id: file.id,
+ name: file.name,
+ type: file.type,
+ file: null
+ }
+ ])
+
+ setUserInput(userInput.replace(/@[^ ]*$/, ""))
+ }
+
+ return {
+ handleInputChange,
+ handleSelectPrompt,
+ handleSelectUserFile
+ }
+}
diff --git a/components/chat/chat-hooks/use-scroll.tsx b/components/chat/chat-hooks/use-scroll.tsx
new file mode 100644
index 0000000000..becb4c9218
--- /dev/null
+++ b/components/chat/chat-hooks/use-scroll.tsx
@@ -0,0 +1,79 @@
+import { ChatbotUIContext } from "@/context/context"
+import { useCallback, useContext, useEffect, useRef, useState } from "react"
+
+export const useScroll = () => {
+ const { isGenerating, chatMessages } = useContext(ChatbotUIContext)
+
+ const messagesStartRef = useRef(null)
+ const messagesEndRef = useRef(null)
+ const isAutoScrolling = useRef(false)
+
+ const [isAtTop, setIsAtTop] = useState(false)
+ const [isAtBottom, setIsAtBottom] = useState(true)
+ const [userScrolled, setUserScrolled] = useState(false)
+ const [isOverflowing, setIsOverflowing] = useState(false)
+
+ useEffect(() => {
+ setUserScrolled(false)
+
+ if (!isGenerating && userScrolled) {
+ setUserScrolled(false)
+ }
+ }, [isGenerating])
+
+ useEffect(() => {
+ if (isGenerating && !userScrolled) {
+ scrollToBottom()
+ }
+ }, [chatMessages])
+
+ const handleScroll = useCallback((e: any) => {
+ const bottom =
+ Math.round(e.target.scrollHeight) - Math.round(e.target.scrollTop) ===
+ Math.round(e.target.clientHeight)
+ setIsAtBottom(bottom)
+
+ const top = e.target.scrollTop === 0
+ setIsAtTop(top)
+
+ if (!bottom && !isAutoScrolling.current) {
+ setUserScrolled(true)
+ } else {
+ setUserScrolled(false)
+ }
+
+ const isOverflow = e.target.scrollHeight > e.target.clientHeight
+ setIsOverflowing(isOverflow)
+ }, [])
+
+ const scrollToTop = useCallback(() => {
+ if (messagesStartRef.current) {
+ messagesStartRef.current.scrollIntoView({ behavior: "instant" })
+ }
+ }, [])
+
+ const scrollToBottom = useCallback(() => {
+ isAutoScrolling.current = true
+
+ setTimeout(() => {
+ if (messagesEndRef.current) {
+ messagesEndRef.current.scrollIntoView({ behavior: "instant" })
+ }
+
+ isAutoScrolling.current = false
+ }, 0)
+ }, [])
+
+ return {
+ messagesStartRef,
+ messagesEndRef,
+ isAtTop,
+ isAtBottom,
+ userScrolled,
+ isOverflowing,
+ handleScroll,
+ scrollToTop,
+ scrollToBottom,
+ setIsAtBottom
+ }
+}
diff --git a/components/chat/chat-hooks/use-select-file-handler.tsx b/components/chat/chat-hooks/use-select-file-handler.tsx
new file mode 100644
index 0000000000..86cb87b26a
--- /dev/null
+++ b/components/chat/chat-hooks/use-select-file-handler.tsx
@@ -0,0 +1,160 @@
+import { ChatbotUIContext } from "@/context/context"
+import { createFile } from "@/db/files"
+import { LLM_LIST } from "@/lib/models/llm/llm-list"
+import { useContext, useEffect, useState } from "react"
+import { toast } from "sonner"
+
+export const ACCEPTED_FILE_TYPES = [
+ // "text/csv", // TODO: Add support for CSVs
+ // "application/msword", // TODO: Add support for DOCs
+ // "application/vnd.openxmlformats-officedocument.wordprocessingml.document", // TODO: Add support for DOCXs
+ // "text/html", // TODO: Add support for HTML
+ // "application/json", // TODO: Add support for JSON
+ // "text/markdown", // TODO: Add support for Markdown
+ "application/pdf"
+ // "text/plain" // TODO: Add support for TXT
+].join(",")
+
+export const useSelectFileHandler = () => {
+ const {
+ selectedWorkspace,
+ profile,
+ chatSettings,
+ setNewMessageImages,
+ setNewMessageFiles,
+ setShowFilesDisplay,
+ setFiles
+ } = useContext(ChatbotUIContext)
+
+ const [filesToAccept, setFilesToAccept] = useState(ACCEPTED_FILE_TYPES)
+
+ useEffect(() => {
+ handleFilesToAccept()
+ }, [chatSettings?.model])
+
+ const handleFilesToAccept = () => {
+ const model = chatSettings?.model
+ const FULL_MODEL = LLM_LIST.find(llm => llm.modelId === model)
+
+ if (!FULL_MODEL) return
+
+ setFilesToAccept(
+ FULL_MODEL.imageInput
+ ? `${ACCEPTED_FILE_TYPES},image/*`
+ : ACCEPTED_FILE_TYPES
+ )
+ }
+
+ const handleSelectDeviceFile = (file: File) => {
+ setShowFilesDisplay(true)
+
+ if (file) {
+ let reader = new FileReader()
+
+ if (file.type.includes("image")) {
+ reader.readAsDataURL(file)
+ } else if (`${file.type}`.includes("pdf")) {
+ reader.readAsArrayBuffer(file)
+ } else if (
+ ACCEPTED_FILE_TYPES.split(",").includes(file.type) ||
+ file.type.includes("pdf")
+ ) {
+ // Use readAsArrayBuffer for PDFs and readAsText for other types
+ file.type.includes("pdf")
+ ? reader.readAsArrayBuffer(file)
+ : reader.readAsText(file)
+ } else {
+ throw new Error("Unsupported file type")
+ }
+
+ reader.onloadend = async function () {
+ try {
+ if (file.type.includes("image")) {
+ // Create a temp url for the image file
+ const imageUrl = URL.createObjectURL(file)
+
+ // This is a temporary image for display purposes in the chat input
+ setNewMessageImages(prev => [
+ ...prev,
+ {
+ messageId: "temp",
+ path: "",
+ base64: reader.result, // base64 image
+ url: imageUrl,
+ file
+ }
+ ])
+ } else {
+ if (!profile || !selectedWorkspace || !chatSettings) return
+
+ let simplifiedFileType = file.type.split("/")[1]
+ if (
+ simplifiedFileType ===
+ "vnd.openxmlformats-officedocument.wordprocessingml.document"
+ ) {
+ simplifiedFileType = "docx"
+ } else if (simplifiedFileType === "msword") {
+ simplifiedFileType = "doc"
+ } else if (simplifiedFileType === "vnd.adobe.pdf") {
+ simplifiedFileType = "pdf"
+ }
+
+ setNewMessageFiles(prev => [
+ ...prev,
+ {
+ id: "loading",
+ name: file.name,
+ type: simplifiedFileType,
+ file: file
+ }
+ ])
+
+ const createdFile = await createFile(
+ file,
+ {
+ user_id: profile.user_id,
+ description: "",
+ file_path: "",
+ name: file.name,
+ size: file.size,
+ tokens: 0,
+ type: simplifiedFileType
+ },
+ selectedWorkspace.id,
+ chatSettings.embeddingsProvider
+ )
+ console.log("createdFile", createdFile)
+
+ setFiles(prev => [...prev, createdFile])
+
+ setNewMessageFiles(prev =>
+ prev.map(item =>
+ item.id === "loading"
+ ? {
+ id: createdFile.id,
+ name: createdFile.name,
+ type: createdFile.type,
+ file: file
+ }
+ : item
+ )
+ )
+ }
+ } catch (error) {
+ console.error(error)
+ toast.error("Failed to upload.")
+
+ setNewMessageImages(prev =>
+ prev.filter(img => img.messageId !== "temp")
+ )
+ setNewMessageFiles(prev => prev.filter(file => file.id !== "loading"))
+ }
+ }
+ }
+ }
+
+ return {
+ handleSelectDeviceFile,
+ filesToAccept
+ }
+}
diff --git a/components/chat/chat-input.tsx b/components/chat/chat-input.tsx
new file mode 100644
index 0000000000..d496bd9442
--- /dev/null
+++ b/components/chat/chat-input.tsx
@@ -0,0 +1,171 @@
+import { ChatbotUIContext } from "@/context/context"
+import { getChatFilesByChatId } from "@/db/chat-files"
+import useHotkey from "@/lib/hooks/use-hotkey"
+import { cn } from "@/lib/utils"
+import {
+ IconCirclePlus,
+ IconPlayerStopFilled,
+ IconSend
+} from "@tabler/icons-react"
+import { FC, useContext, useEffect, useRef } from "react"
+import { Input } from "../ui/input"
+import { TextareaAutosize } from "../ui/textarea-autosize"
+import { ChatCommandInput } from "./chat-command-input"
+import { ChatFilesDisplay } from "./chat-files-display"
+import { useChatHandler } from "./chat-hooks/use-chat-handler"
+import { usePromptAndCommand } from "./chat-hooks/use-prompt-and-command"
+import { useSelectFileHandler } from "./chat-hooks/use-select-file-handler"
+
+interface ChatInputProps {}
+
+export const ChatInput: FC = ({}) => {
+ useHotkey("l", () => {
+ handleFocusChatInput()
+ })
+
+ const {
+ selectedChat,
+ userInput,
+ setChatFiles,
+ chatMessages,
+ isGenerating,
+ selectedPreset,
+ selectedAssistant,
+ focusPrompt,
+ setFocusPrompt,
+ focusFile,
+ isPromptPickerOpen,
+ setIsPromptPickerOpen,
+ isAtPickerOpen,
+ setFocusFile
+ } = useContext(ChatbotUIContext)
+
+ const {
+ chatInputRef,
+ handleSendMessage,
+ handleStopMessage,
+ handleFocusChatInput
+ } = useChatHandler()
+
+ const { handleInputChange } = usePromptAndCommand()
+
+ const { filesToAccept, handleSelectDeviceFile } = useSelectFileHandler()
+
+ const fileInputRef = useRef(null)
+
+ useEffect(() => {
+ const fetchData = async () => {
+ await fetchChatFiles()
+ }
+
+ fetchData()
+ }, [selectedChat])
+
+ useEffect(() => {
+ setTimeout(() => {
+ handleFocusChatInput()
+ }, 200) // FIX: hacky
+ }, [selectedPreset, selectedAssistant])
+
+ const fetchChatFiles = async () => {
+ if (!selectedChat) return
+
+ const chatFiles = await getChatFilesByChatId(selectedChat.id)
+
+ setChatFiles(
+ chatFiles.files.map(file => {
+ return {
+ id: file.id,
+ name: file.name,
+ type: file.type,
+ data: "",
+ file: null
+ }
+ })
+ )
+ }
+
+ const handleKeyDown = (event: React.KeyboardEvent) => {
+ if (event.key === "Enter" && !event.shiftKey) {
+ event.preventDefault()
+ setIsPromptPickerOpen(false)
+ handleSendMessage(userInput, chatMessages, false)
+ }
+
+ if (event.key === "Tab" && isPromptPickerOpen) {
+ event.preventDefault()
+ setFocusPrompt(!focusPrompt)
+ }
+
+ if (event.key === "Tab" && isAtPickerOpen) {
+ event.preventDefault()
+ setFocusFile(!focusFile)
+ }
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+ <>
+
fileInputRef.current?.click()}
+ />
+
+ {/* Hidden input to select files from device */}
+ {
+ if (!e.target.files) return
+ handleSelectDeviceFile(e.target.files[0])
+ }}
+ accept={filesToAccept}
+ />
+ >
+
+
+
+
+ {isGenerating ? (
+
+ ) : (
+ {
+ if (!userInput) return
+
+ handleSendMessage(userInput, chatMessages, false)
+ }}
+ size={30}
+ />
+ )}
+
+
+ >
+ )
+}
diff --git a/components/chat/chat-messages.tsx b/components/chat/chat-messages.tsx
new file mode 100644
index 0000000000..b11c610b03
--- /dev/null
+++ b/components/chat/chat-messages.tsx
@@ -0,0 +1,34 @@
+import { useChatHandler } from "@/components/chat/chat-hooks/use-chat-handler"
+import { ChatbotUIContext } from "@/context/context"
+import { Tables } from "@/supabase/types"
+import { FC, useContext, useState } from "react"
+import { Message } from "../messages/message"
+
+interface ChatMessagesProps {}
+
+export const ChatMessages: FC = ({}) => {
+ const { chatMessages, chatFileItems } = useContext(ChatbotUIContext)
+
+ const { handleSendEdit } = useChatHandler()
+
+ const [editingMessage, setEditingMessage] = useState>()
+
+ return chatMessages
+ .sort((a, b) => a.message.sequence_number - b.message.sequence_number)
+ .map((chatMessage, index, array) => {
+ return (
+
+ chatMessage.fileItems.includes(chatFileItem.id)
+ )}
+ isEditing={editingMessage?.id === chatMessage.message.id}
+ isLast={index === array.length - 1}
+ onStartEdit={setEditingMessage}
+ onCancelEdit={() => setEditingMessage(undefined)}
+ onSubmitEdit={handleSendEdit}
+ />
+ )
+ })
+}
diff --git a/components/chat/chat-scroll-buttons.tsx b/components/chat/chat-scroll-buttons.tsx
new file mode 100644
index 0000000000..3eb6f2d643
--- /dev/null
+++ b/components/chat/chat-scroll-buttons.tsx
@@ -0,0 +1,41 @@
+import {
+ IconCircleArrowDownFilled,
+ IconCircleArrowUpFilled
+} from "@tabler/icons-react"
+import { FC } from "react"
+
+interface ChatScrollButtonsProps {
+ isAtTop: boolean
+ isAtBottom: boolean
+ isOverflowing: boolean
+ scrollToTop: () => void
+ scrollToBottom: () => void
+}
+
+export const ChatScrollButtons: FC = ({
+ isAtTop,
+ isAtBottom,
+ isOverflowing,
+ scrollToTop,
+ scrollToBottom
+}) => {
+ return (
+ <>
+ {!isAtTop && isOverflowing && (
+
+ )}
+
+ {!isAtBottom && isOverflowing && (
+
+ )}
+ >
+ )
+}
diff --git a/components/chat/chat-secondary-buttons.tsx b/components/chat/chat-secondary-buttons.tsx
new file mode 100644
index 0000000000..d5ab40ea12
--- /dev/null
+++ b/components/chat/chat-secondary-buttons.tsx
@@ -0,0 +1,35 @@
+import { useChatHandler } from "@/components/chat/chat-hooks/use-chat-handler"
+import { ChatbotUIContext } from "@/context/context"
+import { IconMessagePlus } from "@tabler/icons-react"
+import { FC, useContext } from "react"
+import { WithTooltip } from "../ui/with-tooltip"
+
+interface ChatSecondaryButtonsProps {}
+
+export const ChatSecondaryButtons: FC = ({}) => {
+ const { selectedChat } = useContext(ChatbotUIContext)
+
+ const { handleNewChat } = useChatHandler()
+
+ return (
+ <>
+ {selectedChat && (
+ Start a new chat}
+ trigger={
+
+
+
+ }
+ />
+ )}
+
+ {/* TODO */}
+ {/* */}
+ >
+ )
+}
diff --git a/components/chat/chat-settings.tsx b/components/chat/chat-settings.tsx
new file mode 100644
index 0000000000..72c2496458
--- /dev/null
+++ b/components/chat/chat-settings.tsx
@@ -0,0 +1,73 @@
+import { ChatbotUIContext } from "@/context/context"
+import { CHAT_SETTING_LIMITS } from "@/lib/chat-setting-limits"
+import useHotkey from "@/lib/hooks/use-hotkey"
+import { LLM_LIST } from "@/lib/models/llm/llm-list"
+import { IconAdjustmentsHorizontal } from "@tabler/icons-react"
+import { FC, useContext, useEffect, useRef } from "react"
+import { Button } from "../ui/button"
+import { ChatSettingsForm } from "../ui/chat-settings-form"
+import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"
+
+interface ChatSettingsProps {}
+
+export const ChatSettings: FC = ({}) => {
+ useHotkey("i", () => handleClick())
+
+ const { chatSettings, setChatSettings } = useContext(ChatbotUIContext)
+
+ const buttonRef = useRef(null)
+
+ const handleClick = () => {
+ if (buttonRef.current) {
+ buttonRef.current.click()
+ }
+ }
+
+ useEffect(() => {
+ if (!chatSettings) return
+
+ setChatSettings({
+ ...chatSettings,
+ temperature: Math.min(
+ chatSettings.temperature,
+ CHAT_SETTING_LIMITS[chatSettings.model]?.MAX_TEMPERATURE || 1
+ ),
+ contextLength: Math.min(
+ chatSettings.contextLength,
+ CHAT_SETTING_LIMITS[chatSettings.model]?.MAX_CONTEXT_LENGTH || 4096
+ )
+ })
+ }, [chatSettings?.model])
+
+ if (!chatSettings) return null
+
+ const fullModel = LLM_LIST.find(llm => llm.modelId === chatSettings.model)
+
+ return (
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/components/chat/chat-ui.tsx b/components/chat/chat-ui.tsx
new file mode 100644
index 0000000000..2cfd90b011
--- /dev/null
+++ b/components/chat/chat-ui.tsx
@@ -0,0 +1,207 @@
+import Loading from "@/app/loading"
+import { useChatHandler } from "@/components/chat/chat-hooks/use-chat-handler"
+import { ChatbotUIContext } from "@/context/context"
+import { getChatById } from "@/db/chats"
+import { getMessageFileItemsByMessageId } from "@/db/message-file-items"
+import { getMessagesByChatId } from "@/db/messages"
+import { getMessageImageFromStorage } from "@/db/storage/message-images"
+import { convertBlobToBase64 } from "@/lib/blob-to-b64"
+import useHotkey from "@/lib/hooks/use-hotkey"
+import { LLMID, MessageImage } from "@/types"
+import { useParams } from "next/navigation"
+import { FC, useContext, useEffect, useState } from "react"
+import { ChatHelp } from "./chat-help"
+import { useScroll } from "./chat-hooks/use-scroll"
+import { ChatInput } from "./chat-input"
+import { ChatMessages } from "./chat-messages"
+import { ChatScrollButtons } from "./chat-scroll-buttons"
+import { ChatSecondaryButtons } from "./chat-secondary-buttons"
+
+interface ChatUIProps {}
+
+export const ChatUI: FC = ({}) => {
+ useHotkey("o", () => handleNewChat())
+
+ const params = useParams()
+
+ const {
+ setChatMessages,
+ selectedChat,
+ setSelectedChat,
+ setChatSettings,
+ setChatImages,
+ assistants,
+ setSelectedAssistant,
+ setChatFileItems
+ } = useContext(ChatbotUIContext)
+
+ const { handleNewChat, handleFocusChatInput } = useChatHandler()
+
+ const {
+ messagesStartRef,
+ messagesEndRef,
+ handleScroll,
+ scrollToBottom,
+ setIsAtBottom,
+ isAtTop,
+ isAtBottom,
+ isOverflowing,
+ scrollToTop
+ } = useScroll()
+
+ const [loading, setLoading] = useState(true)
+
+ useEffect(() => {
+ const fetchData = async () => {
+ await fetchMessages()
+ await fetchChat()
+
+ scrollToBottom()
+ setIsAtBottom(true)
+ }
+
+ if (params.chatid) {
+ fetchData().then(() => {
+ handleFocusChatInput()
+ setLoading(false)
+ })
+ } else {
+ setLoading(false)
+ }
+ }, [])
+
+ const fetchMessages = async () => {
+ const fetchedMessages = await getMessagesByChatId(params.chatid as string)
+
+ const imagePromises: Promise[] = fetchedMessages.flatMap(
+ message =>
+ message.image_paths
+ ? message.image_paths.map(async imagePath => {
+ const url = await getMessageImageFromStorage(imagePath)
+
+ if (url) {
+ const response = await fetch(url)
+ const blob = await response.blob()
+ const base64 = await convertBlobToBase64(blob)
+
+ return {
+ messageId: message.id,
+ path: imagePath,
+ base64,
+ url,
+ file: null
+ }
+ }
+
+ return {
+ messageId: message.id,
+ path: imagePath,
+ base64: "",
+ url,
+ file: null
+ }
+ })
+ : []
+ )
+
+ const images: MessageImage[] = await Promise.all(imagePromises.flat())
+ setChatImages(images)
+
+ const messageFileItemPromises = fetchedMessages.map(
+ async message => await getMessageFileItemsByMessageId(message.id)
+ )
+
+ const messageFileItems = await Promise.all(messageFileItemPromises)
+
+ const uniqueFileItems = messageFileItems.flatMap(item => item.file_items)
+ console.log("uniqueFileItems", uniqueFileItems)
+ setChatFileItems(uniqueFileItems)
+
+ const fetchedChatMessages = fetchedMessages.map(message => {
+ return {
+ message,
+ fileItems: messageFileItems
+ .filter(messageFileItem => messageFileItem.id === message.id)
+ .flatMap(messageFileItem =>
+ messageFileItem.file_items.map(fileItem => fileItem.id)
+ )
+ }
+ })
+ console.log("fetchedChatMessages", fetchedChatMessages)
+
+ setChatMessages(fetchedChatMessages)
+ }
+
+ const fetchChat = async () => {
+ const chat = await getChatById(params.chatid as string)
+ if (!chat) return
+
+ if (chat.assistant_id) {
+ const assistant = assistants.find(
+ assistant => assistant.id === chat.assistant_id
+ )
+
+ if (assistant) {
+ setSelectedAssistant(assistant)
+ }
+ }
+
+ setSelectedChat(chat)
+ setChatSettings({
+ model: chat.model as LLMID,
+ prompt: chat.prompt,
+ temperature: chat.temperature,
+ contextLength: chat.context_length,
+ includeProfileContext: chat.include_profile_context,
+ includeWorkspaceInstructions: chat.include_workspace_instructions,
+ embeddingsProvider: chat.embeddings_provider as "openai" | "local"
+ })
+ }
+
+ if (loading) {
+ return
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {selectedChat?.name || "Chat"}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/components/chat/file-picker.tsx b/components/chat/file-picker.tsx
new file mode 100644
index 0000000000..81467f34b1
--- /dev/null
+++ b/components/chat/file-picker.tsx
@@ -0,0 +1,125 @@
+import { ChatbotUIContext } from "@/context/context"
+import { Tables } from "@/supabase/types"
+import { IconBooks } from "@tabler/icons-react"
+import { FC, useContext, useEffect, useRef } from "react"
+import { FileIcon } from "../ui/file-icon"
+
+interface FilePickerProps {
+ isOpen: boolean
+ searchQuery: string
+ onOpenChange: (isOpen: boolean) => void
+ selectedFileIds: string[]
+ selectedCollectionIds: string[]
+ onSelectFile: (file: Tables<"files">) => void
+ onSelectCollection: (collection: Tables<"collections">) => void
+ isFocused: boolean
+}
+
+export const FilePicker: FC = ({
+ isOpen,
+ searchQuery,
+ onOpenChange,
+ selectedFileIds,
+ selectedCollectionIds,
+ onSelectFile,
+ onSelectCollection,
+ isFocused
+}) => {
+ const { files, collections } = useContext(ChatbotUIContext)
+
+ useEffect(() => {
+ firstFileRef.current?.focus()
+ }, [isFocused])
+
+ const firstFileRef = useRef(null)
+
+ const filteredFiles = files.filter(
+ file =>
+ file.name.toLowerCase().includes(searchQuery.toLowerCase()) &&
+ !selectedFileIds.includes(file.id)
+ )
+
+ const filteredCollections = collections.filter(
+ collection =>
+ collection.name.toLowerCase().includes(searchQuery.toLowerCase()) &&
+ !selectedCollectionIds.includes(collection.id)
+ )
+
+ const handleOpenChange = (isOpen: boolean) => {
+ onOpenChange(isOpen)
+ }
+
+ const handleSelectFile = (file: Tables<"files">) => {
+ onSelectFile(file)
+ handleOpenChange(false)
+ }
+
+ const handleSelectCollection = (collection: Tables<"collections">) => {
+ onSelectCollection(collection)
+ handleOpenChange(false)
+ }
+
+ const getKeyDownHandler =
+ (index: number) => (e: React.KeyboardEvent) => {
+ if (e.key === "Backspace") {
+ e.preventDefault()
+ } else if (e.key === "Enter") {
+ e.preventDefault()
+ handleSelectFile(filteredFiles[index])
+ } else if (
+ e.key === "Tab" &&
+ !e.shiftKey &&
+ index === filteredFiles.length
+ ) {
+ e.preventDefault()
+ firstFileRef.current?.focus()
+ }
+ }
+
+ return (
+ <>
+ {isOpen && (
+
+ {filteredFiles.length === 0 && filteredCollections.length === 0 ? (
+
+ No matching files.
+
+ ) : (
+ <>
+ {[...filteredFiles, ...filteredCollections].map((item, index) => (
+
{
+ if ("type" in item) {
+ handleSelectFile(item as Tables<"files">)
+ } else {
+ handleSelectCollection(item)
+ }
+ }}
+ onKeyDown={getKeyDownHandler(index)}
+ >
+ {"type" in item ? (
+
).type} size={32} />
+ ) : (
+
+ )}
+
+
+
{item.name}
+
+
+ {item.description || "No description."}
+
+
+
+ ))}
+ >
+ )}
+
+ )}
+ >
+ )
+}
diff --git a/components/chat/prompt-picker.tsx b/components/chat/prompt-picker.tsx
new file mode 100644
index 0000000000..b4a6d7ab96
--- /dev/null
+++ b/components/chat/prompt-picker.tsx
@@ -0,0 +1,193 @@
+import { ChatbotUIContext } from "@/context/context"
+import { Tables } from "@/supabase/types"
+import { FC, useContext, useEffect, useRef, useState } from "react"
+import { Button } from "../ui/button"
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../ui/dialog"
+import { Label } from "../ui/label"
+import { TextareaAutosize } from "../ui/textarea-autosize"
+
+interface PromptPickerProps {
+ searchQuery: string
+ onSelect: (prompt: Tables<"prompts">) => void
+ isFocused: boolean
+}
+
+export const PromptPicker: FC = ({
+ searchQuery,
+ onSelect,
+ isFocused
+}) => {
+ const { prompts, isPromptPickerOpen, setIsPromptPickerOpen } =
+ useContext(ChatbotUIContext)
+
+ const firstPromptRef = useRef(null)
+
+ const [promptVariables, setPromptVariables] = useState<
+ {
+ promptId: string
+ name: string
+ value: string
+ }[]
+ >([])
+ const [showPromptVariables, setShowPromptVariables] = useState(false)
+
+ const filteredPrompts = prompts.filter(prompt =>
+ prompt.name.toLowerCase().includes(searchQuery.toLowerCase())
+ )
+
+ const handleOpenChange = (isOpen: boolean) => {
+ setIsPromptPickerOpen(isOpen)
+ }
+
+ const handleSelectPrompt = (prompt: Tables<"prompts">) => {
+ const regex = /\{\{.*?\}\}/g
+ const matches = prompt.content.match(regex)
+
+ if (matches) {
+ const newPromptVariables = matches.map(match => ({
+ promptId: prompt.id,
+ name: match.replace(/\{\{|\}\}/g, ""),
+ value: ""
+ }))
+
+ setPromptVariables(newPromptVariables)
+ setShowPromptVariables(true)
+ } else {
+ onSelect(prompt)
+ handleOpenChange(false)
+ }
+ }
+
+ const getKeyDownHandler =
+ (index: number) => (e: React.KeyboardEvent) => {
+ if (e.key === "Backspace") {
+ e.preventDefault()
+ handleOpenChange(false)
+ } else if (e.key === "Enter") {
+ e.preventDefault()
+ handleSelectPrompt(filteredPrompts[index])
+ } else if (
+ e.key === "Tab" &&
+ !e.shiftKey &&
+ index === filteredPrompts.length - 1
+ ) {
+ e.preventDefault()
+ firstPromptRef.current?.focus()
+ }
+ }
+
+ const handleSubmitPromptVariables = () => {
+ const newPromptContent = promptVariables.reduce(
+ (prevContent, variable) =>
+ prevContent.replace(
+ new RegExp(`\\{\\{${variable.name}\\}\\}`, "g"),
+ variable.value
+ ),
+ prompts.find(prompt => prompt.id === promptVariables[0].promptId)
+ ?.content || ""
+ )
+
+ const newPrompt: any = {
+ ...prompts.find(prompt => prompt.id === promptVariables[0].promptId),
+ content: newPromptContent
+ }
+
+ onSelect(newPrompt)
+ handleOpenChange(false)
+ setShowPromptVariables(false)
+ setPromptVariables([])
+ }
+
+ const handleCancelPromptVariables = () => {
+ setShowPromptVariables(false)
+ setPromptVariables([])
+ }
+
+ const handleKeydownPromptVariables = (
+ e: React.KeyboardEvent
+ ) => {
+ if (e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault()
+ handleSubmitPromptVariables()
+ }
+ }
+
+ useEffect(() => {
+ firstPromptRef.current?.focus()
+ }, [isFocused])
+
+ return (
+ <>
+ {isPromptPickerOpen && (
+
+ {showPromptVariables ? (
+
+ ) : filteredPrompts.length === 0 ? (
+
+ No matching prompts.
+
+ ) : (
+ filteredPrompts.map((prompt, index) => (
+
handleSelectPrompt(prompt)}
+ onKeyDown={getKeyDownHandler(index)}
+ >
+
{prompt.name}
+
+ {prompt.content}
+
+
+ ))
+ )}
+
+ )}
+ >
+ )
+}
diff --git a/components/chat/quick-setting-option.tsx b/components/chat/quick-setting-option.tsx
new file mode 100644
index 0000000000..61cc8ac644
--- /dev/null
+++ b/components/chat/quick-setting-option.tsx
@@ -0,0 +1,63 @@
+import { Tables } from "@/supabase/types"
+import { IconCircleCheckFilled, IconRobotFace } from "@tabler/icons-react"
+import Image from "next/image"
+import { FC } from "react"
+import { ModelIcon } from "../models/model-icon"
+import { DropdownMenuItem } from "../ui/dropdown-menu"
+
+interface QuickSettingOptionProps {
+ contentType: "presets" | "assistants"
+ isSelected: boolean
+ item: Tables<"presets"> | Tables<"assistants">
+ onSelect: () => void
+ image: string
+}
+
+export const QuickSettingOption: FC = ({
+ contentType,
+ isSelected,
+ item,
+ onSelect,
+ image
+}) => {
+ return (
+
+
+ {contentType === "presets" ? (
+
+ ) : image ? (
+
+ ) : (
+
+ )}
+
+
+
+
{item.name}
+
+
+ {item.description || "No description."}
+
+
+
+
+ {isSelected ? (
+
+ ) : null}
+
+
+ )
+}
diff --git a/components/chat/quick-settings.tsx b/components/chat/quick-settings.tsx
new file mode 100644
index 0000000000..89fc499769
--- /dev/null
+++ b/components/chat/quick-settings.tsx
@@ -0,0 +1,233 @@
+import { ChatbotUIContext } from "@/context/context"
+import useHotkey from "@/lib/hooks/use-hotkey"
+import { Tables } from "@/supabase/types"
+import { LLMID } from "@/types"
+import { IconChevronDown, IconRobotFace } from "@tabler/icons-react"
+import Image from "next/image"
+import { FC, useContext, useEffect, useRef, useState } from "react"
+import { ModelIcon } from "../models/model-icon"
+import { Button } from "../ui/button"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuTrigger
+} from "../ui/dropdown-menu"
+import { Input } from "../ui/input"
+import { QuickSettingOption } from "./quick-setting-option"
+
+interface QuickSettingsProps {}
+
+export const QuickSettings: FC = ({}) => {
+ useHotkey("p", () => setIsOpen(prevState => !prevState))
+
+ const {
+ presets,
+ assistants,
+ selectedAssistant,
+ selectedPreset,
+ chatSettings,
+ setSelectedPreset,
+ setSelectedAssistant,
+ setChatSettings,
+ assistantImages
+ } = useContext(ChatbotUIContext)
+
+ const inputRef = useRef(null)
+
+ const [isOpen, setIsOpen] = useState(false)
+ const [search, setSearch] = useState("")
+
+ useEffect(() => {
+ if (isOpen) {
+ setTimeout(() => {
+ inputRef.current?.focus()
+ }, 100) // FIX: hacky
+ }
+ }, [isOpen])
+
+ const handleSelectQuickSetting = (
+ item: Tables<"presets"> | Tables<"assistants">,
+ contentType: "presets" | "assistants"
+ ) => {
+ if (contentType === "assistants") {
+ setSelectedAssistant(item as Tables<"assistants">)
+ setSelectedPreset(null)
+ } else if (contentType === "presets") {
+ setSelectedPreset(item as Tables<"presets">)
+ setSelectedAssistant(null)
+ }
+
+ setChatSettings({
+ model: item.model as LLMID,
+ prompt: item.prompt,
+ temperature: item.temperature,
+ contextLength: item.context_length,
+ includeProfileContext: item.include_profile_context,
+ includeWorkspaceInstructions: item.include_workspace_instructions,
+ embeddingsProvider: item.embeddings_provider as "openai" | "local"
+ })
+ }
+
+ const checkIfModified = () => {
+ if (!chatSettings) return false
+
+ if (selectedPreset) {
+ if (
+ selectedPreset.include_profile_context !==
+ chatSettings.includeProfileContext ||
+ selectedPreset.include_workspace_instructions !==
+ chatSettings.includeWorkspaceInstructions ||
+ selectedPreset.context_length !== chatSettings.contextLength ||
+ selectedPreset.model !== chatSettings.model ||
+ selectedPreset.prompt !== chatSettings.prompt ||
+ selectedPreset.temperature !== chatSettings.temperature
+ ) {
+ return true
+ }
+ } else if (selectedAssistant) {
+ if (
+ selectedAssistant.include_profile_context !==
+ chatSettings.includeProfileContext ||
+ selectedAssistant.include_workspace_instructions !==
+ chatSettings.includeWorkspaceInstructions ||
+ selectedAssistant.context_length !== chatSettings.contextLength ||
+ selectedAssistant.model !== chatSettings.model ||
+ selectedAssistant.prompt !== chatSettings.prompt ||
+ selectedAssistant.temperature !== chatSettings.temperature
+ ) {
+ return true
+ }
+ }
+
+ return false
+ }
+
+ const isModified = checkIfModified()
+
+ const items = [
+ ...presets.map(preset => ({ ...preset, contentType: "presets" })),
+ ...assistants.map(assistant => ({
+ ...assistant,
+ contentType: "assistants"
+ }))
+ ]
+
+ const selectedAssistantImage = selectedPreset
+ ? ""
+ : assistantImages.find(
+ image => image.path === selectedAssistant?.image_path
+ )?.base64 || ""
+
+ return (
+ {
+ setIsOpen(isOpen)
+ setSearch("")
+ }}
+ >
+
+
+
+
+
+ {presets.length === 0 && assistants.length === 0 ? (
+ No items found.
+ ) : (
+ <>
+ setSearch(e.target.value)}
+ onKeyDown={e => e.stopPropagation()}
+ />
+
+ {!!(selectedPreset || selectedAssistant) && (
+
+ | Tables<"assistants">)
+ }
+ onSelect={() => {
+ setSelectedPreset(null)
+ setSelectedAssistant(null)
+ }}
+ image={selectedPreset ? "" : selectedAssistantImage}
+ />
+ )}
+
+ {items
+ .filter(
+ item =>
+ item.name.toLowerCase().includes(search.toLowerCase()) &&
+ item.id !== selectedPreset?.id &&
+ item.id !== selectedAssistant?.id
+ )
+ .map(({ contentType, ...item }) => (
+
+ handleSelectQuickSetting(
+ item,
+ contentType as "presets" | "assistants"
+ )
+ }
+ image={
+ contentType === "assistants"
+ ? assistantImages.find(
+ image =>
+ image.path ===
+ (item as Tables<"assistants">).image_path
+ )?.base64 || ""
+ : ""
+ }
+ />
+ ))}
+ >
+ )}
+
+
+ )
+}
diff --git a/components/icons/anthropic-svg.tsx b/components/icons/anthropic-svg.tsx
new file mode 100644
index 0000000000..27b2cd18f4
--- /dev/null
+++ b/components/icons/anthropic-svg.tsx
@@ -0,0 +1,44 @@
+import { FC } from "react"
+
+interface AnthropicSVGProps {
+ height?: number
+ width?: number
+ className?: string
+}
+
+export const AnthropicSVG: FC = ({
+ height = 40,
+ width = 40,
+ className
+}) => {
+ return (
+
+ )
+}
diff --git a/components/icons/chatbotui-svg.tsx b/components/icons/chatbotui-svg.tsx
new file mode 100644
index 0000000000..4c29cf66f6
--- /dev/null
+++ b/components/icons/chatbotui-svg.tsx
@@ -0,0 +1,37 @@
+import { FC } from "react"
+
+interface ChatbotUISVGProps {
+ theme: "dark" | "light"
+ scale?: number
+}
+
+export const ChatbotUISVG: FC = ({ theme, scale = 1 }) => {
+ return (
+
+ )
+}
diff --git a/components/icons/google-svg.tsx b/components/icons/google-svg.tsx
new file mode 100644
index 0000000000..8a86709ce7
--- /dev/null
+++ b/components/icons/google-svg.tsx
@@ -0,0 +1,42 @@
+import { FC } from "react"
+
+interface GoogleSVGProps {
+ height?: number
+ width?: number
+ className?: string
+}
+
+export const GoogleSVG: FC = ({
+ height = 40,
+ width = 40,
+ className
+}) => {
+ return (
+
+ )
+}
diff --git a/components/icons/openai-svg.tsx b/components/icons/openai-svg.tsx
new file mode 100644
index 0000000000..670c613598
--- /dev/null
+++ b/components/icons/openai-svg.tsx
@@ -0,0 +1,31 @@
+import { FC } from "react"
+
+interface OpenAISVGProps {
+ height?: number
+ width?: number
+ className?: string
+}
+
+export const OpenAISVG: FC = ({
+ height = 40,
+ width = 40,
+ className
+}) => {
+ return (
+
+ )
+}
diff --git a/components/messages/message-actions.tsx b/components/messages/message-actions.tsx
new file mode 100644
index 0000000000..0e1c8c78ea
--- /dev/null
+++ b/components/messages/message-actions.tsx
@@ -0,0 +1,117 @@
+import { ChatbotUIContext } from "@/context/context"
+import { IconCheck, IconCopy, IconEdit, IconRepeat } from "@tabler/icons-react"
+import { FC, useContext, useEffect, useState } from "react"
+import { WithTooltip } from "../ui/with-tooltip"
+
+export const MESSAGE_ICON_SIZE = 18
+
+interface MessageActionsProps {
+ isAssistant: boolean
+ isLast: boolean
+ isEditing: boolean
+ isHovering: boolean
+ onCopy: () => void
+ onEdit: () => void
+ onRegenerate: () => void
+}
+
+export const MessageActions: FC = ({
+ isAssistant,
+ isLast,
+ isEditing,
+ isHovering,
+ onCopy,
+ onEdit,
+ onRegenerate
+}) => {
+ const { isGenerating } = useContext(ChatbotUIContext)
+
+ const [showCheckmark, setShowCheckmark] = useState(false)
+
+ const handleCopy = () => {
+ onCopy()
+ setShowCheckmark(true)
+ }
+
+ const handleForkChat = async () => {}
+
+ useEffect(() => {
+ if (showCheckmark) {
+ const timer = setTimeout(() => {
+ setShowCheckmark(false)
+ }, 2000)
+
+ return () => clearTimeout(timer)
+ }
+ }, [showCheckmark])
+
+ return (isLast && isGenerating) || isEditing ? null : (
+
+ {/* {((isAssistant && isHovering) || isLast) && (
+ Fork Chat
}
+ trigger={
+
+ }
+ />
+ )} */}
+
+ {!isAssistant && isHovering && (
+ Edit}
+ trigger={
+
+ }
+ />
+ )}
+
+ {(isHovering || isLast) && (
+ Copy}
+ trigger={
+ showCheckmark ? (
+
+ ) : (
+
+ )
+ }
+ />
+ )}
+
+ {isLast && (
+ Regenerate}
+ trigger={
+
+ }
+ />
+ )}
+
+ {/* {1 > 0 && isAssistant && } */}
+
+ )
+}
diff --git a/components/messages/message-codeblock.tsx b/components/messages/message-codeblock.tsx
new file mode 100644
index 0000000000..2b8d79552d
--- /dev/null
+++ b/components/messages/message-codeblock.tsx
@@ -0,0 +1,135 @@
+import { Button } from "@/components/ui/button"
+import { useCopyToClipboard } from "@/lib/hooks/use-copy-to-clipboard"
+import { IconCheck, IconCopy, IconDownload } from "@tabler/icons-react"
+import { FC, memo } from "react"
+import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
+import { oneDark } from "react-syntax-highlighter/dist/cjs/styles/prism"
+
+interface MessageCodeBlockProps {
+ language: string
+ value: string
+}
+
+interface languageMap {
+ [key: string]: string | undefined
+}
+
+export const programmingLanguages: languageMap = {
+ javascript: ".js",
+ python: ".py",
+ java: ".java",
+ c: ".c",
+ cpp: ".cpp",
+ "c++": ".cpp",
+ "c#": ".cs",
+ ruby: ".rb",
+ php: ".php",
+ swift: ".swift",
+ "objective-c": ".m",
+ kotlin: ".kt",
+ typescript: ".ts",
+ go: ".go",
+ perl: ".pl",
+ rust: ".rs",
+ scala: ".scala",
+ haskell: ".hs",
+ lua: ".lua",
+ shell: ".sh",
+ sql: ".sql",
+ html: ".html",
+ css: ".css"
+}
+
+export const generateRandomString = (length: number, lowercase = false) => {
+ const chars = "ABCDEFGHJKLMNPQRSTUVWXY3456789" // excluding similar looking characters like Z, 2, I, 1, O, 0
+ let result = ""
+ for (let i = 0; i < length; i++) {
+ result += chars.charAt(Math.floor(Math.random() * chars.length))
+ }
+ return lowercase ? result.toLowerCase() : result
+}
+
+export const MessageCodeBlock: FC = memo(
+ ({ language, value }) => {
+ const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 })
+
+ const downloadAsFile = () => {
+ if (typeof window === "undefined") {
+ return
+ }
+ const fileExtension = programmingLanguages[language] || ".file"
+ const suggestedFileName = `file-${generateRandomString(
+ 3,
+ true
+ )}${fileExtension}`
+ const fileName = window.prompt("Enter file name" || "", suggestedFileName)
+
+ if (!fileName) {
+ return
+ }
+
+ const blob = new Blob([value], { type: "text/plain" })
+ const url = URL.createObjectURL(blob)
+ const link = document.createElement("a")
+ link.download = fileName
+ link.href = url
+ link.style.display = "none"
+ document.body.appendChild(link)
+ link.click()
+ document.body.removeChild(link)
+ URL.revokeObjectURL(url)
+ }
+
+ const onCopy = () => {
+ if (isCopied) return
+ copyToClipboard(value)
+ }
+
+ return (
+
+
+
{language}
+
+
+
+
+
+
+
+ {value}
+
+
+ )
+ }
+)
+
+MessageCodeBlock.displayName = "MessageCodeBlock"
diff --git a/components/messages/message-markdown-memoized.tsx b/components/messages/message-markdown-memoized.tsx
new file mode 100644
index 0000000000..2fc2106500
--- /dev/null
+++ b/components/messages/message-markdown-memoized.tsx
@@ -0,0 +1,9 @@
+import { FC, memo } from "react"
+import ReactMarkdown, { Options } from "react-markdown"
+
+export const MessageMarkdownMemoized: FC = memo(
+ ReactMarkdown,
+ (prevProps, nextProps) =>
+ prevProps.children === nextProps.children &&
+ prevProps.className === nextProps.className
+)
diff --git a/components/messages/message-markdown.tsx b/components/messages/message-markdown.tsx
new file mode 100644
index 0000000000..c00be87ee1
--- /dev/null
+++ b/components/messages/message-markdown.tsx
@@ -0,0 +1,62 @@
+import React, { FC } from "react"
+import remarkGfm from "remark-gfm"
+import remarkMath from "remark-math"
+import { MessageCodeBlock } from "./message-codeblock"
+import { MessageMarkdownMemoized } from "./message-markdown-memoized"
+
+interface MessageMarkdownProps {
+ content: string
+}
+
+export const MessageMarkdown: FC = ({ content }) => {
+ return (
+ {children}
+ },
+ code({ node, className, children, ...props }) {
+ const childArray = React.Children.toArray(children)
+ const firstChild = childArray[0] as React.ReactElement
+ const firstChildAsString = React.isValidElement(firstChild)
+ ? (firstChild as React.ReactElement).props.children
+ : firstChild
+
+ if (firstChildAsString === "▍") {
+ return ▍
+ }
+
+ if (typeof firstChildAsString === "string") {
+ childArray[0] = firstChildAsString.replace("`▍`", "▍")
+ }
+
+ const match = /language-(\w+)/.exec(className || "")
+
+ if (
+ typeof firstChildAsString === "string" &&
+ !firstChildAsString.includes("\n")
+ ) {
+ return (
+
+ {childArray}
+
+ )
+ }
+
+ return (
+
+ )
+ }
+ }}
+ >
+ {content}
+
+ )
+}
diff --git a/components/messages/message-replies.tsx b/components/messages/message-replies.tsx
new file mode 100644
index 0000000000..7daf2a6b1a
--- /dev/null
+++ b/components/messages/message-replies.tsx
@@ -0,0 +1,51 @@
+import { IconMessage } from "@tabler/icons-react"
+import { FC, useState } from "react"
+import {
+ Sheet,
+ SheetContent,
+ SheetDescription,
+ SheetHeader,
+ SheetTitle,
+ SheetTrigger
+} from "../ui/sheet"
+import { WithTooltip } from "../ui/with-tooltip"
+import { MESSAGE_ICON_SIZE } from "./message-actions"
+
+interface MessageRepliesProps {}
+
+export const MessageReplies: FC = ({}) => {
+ const [isOpen, setIsOpen] = useState(false)
+
+ return (
+
+
+ View Replies}
+ trigger={
+ setIsOpen(true)}
+ >
+
+
+ {1}
+
+
+ }
+ />
+
+
+
+
+ Are you sure absolutely sure?
+
+ This action cannot be undone. This will permanently delete your
+ account and remove your data from our servers.
+
+
+
+
+ )
+}
diff --git a/components/messages/message.tsx b/components/messages/message.tsx
new file mode 100644
index 0000000000..722668caf9
--- /dev/null
+++ b/components/messages/message.tsx
@@ -0,0 +1,345 @@
+import { useChatHandler } from "@/components/chat/chat-hooks/use-chat-handler"
+import { ChatbotUIContext } from "@/context/context"
+import { LLM_LIST } from "@/lib/models/llm/llm-list"
+import { cn } from "@/lib/utils"
+import { Tables } from "@/supabase/types"
+import { LLMID, MessageImage } from "@/types"
+import {
+ IconCircleFilled,
+ IconFileFilled,
+ IconFileText,
+ IconFileTypePdf,
+ IconMoodSmile,
+ IconPencil,
+ IconRobotFace
+} from "@tabler/icons-react"
+import Image from "next/image"
+import { FC, useContext, useEffect, useRef, useState } from "react"
+import { ModelIcon } from "../models/model-icon"
+import { Avatar, AvatarImage } from "../ui/avatar"
+import { Button } from "../ui/button"
+import { FilePreview } from "../ui/file-preview"
+import { TextareaAutosize } from "../ui/textarea-autosize"
+import { MessageActions } from "./message-actions"
+import { MessageMarkdown } from "./message-markdown"
+
+const ICON_SIZE = 28
+
+interface MessageProps {
+ message: Tables<"messages">
+ fileItems: Tables<"file_items">[]
+ isEditing: boolean
+ isLast: boolean
+ onStartEdit: (message: Tables<"messages">) => void
+ onCancelEdit: () => void
+ onSubmitEdit: (value: string, sequenceNumber: number) => void
+}
+
+export const Message: FC = ({
+ message,
+ fileItems,
+ isEditing,
+ isLast,
+ onStartEdit,
+ onCancelEdit,
+ onSubmitEdit
+}) => {
+ const {
+ profile,
+ isGenerating,
+ setIsGenerating,
+ firstTokenReceived,
+ availableLocalModels,
+ chatMessages,
+ selectedAssistant,
+ chatImages,
+ assistantImages,
+ toolInUse,
+ files
+ } = useContext(ChatbotUIContext)
+
+ const { handleSendMessage } = useChatHandler()
+
+ const editInputRef = useRef(null)
+
+ const [isHovering, setIsHovering] = useState(false)
+ const [editedMessage, setEditedMessage] = useState(message.content)
+
+ const [showImagePreview, setShowImagePreview] = useState(false)
+ const [selectedImage, setSelectedImage] = useState(null)
+
+ const [showFileItemPreview, setShowFileItemPreview] = useState(false)
+ const [selectedFileItem, setSelectedFileItem] =
+ useState | null>(null)
+
+ const handleCopy = () => {
+ navigator.clipboard.writeText(message.content)
+ }
+
+ const handleSendEdit = () => {
+ onSubmitEdit(editedMessage, message.sequence_number)
+ onCancelEdit()
+ }
+
+ const handleKeyDown = (event: React.KeyboardEvent) => {
+ if (isEditing && event.key === "Enter" && event.metaKey) {
+ handleSendEdit()
+ }
+ }
+
+ const handleRegenerate = async () => {
+ setIsGenerating(true)
+ await handleSendMessage(editedMessage, chatMessages, true)
+ }
+
+ const handleStartEdit = () => {
+ onStartEdit(message)
+ }
+
+ useEffect(() => {
+ setEditedMessage(message.content)
+
+ if (isEditing && editInputRef.current) {
+ const input = editInputRef.current
+ input.focus()
+ input.setSelectionRange(input.value.length, input.value.length)
+ }
+ }, [isEditing])
+
+ const MODEL_DATA = [...LLM_LIST, ...availableLocalModels].find(
+ llm => llm.modelId === message.model
+ )
+
+ const selectedAssistantImage = assistantImages.find(
+ image => image.path === selectedAssistant?.image_path
+ )?.base64
+
+ return (
+ setIsHovering(true)}
+ onMouseLeave={() => setIsHovering(false)}
+ onKeyDown={handleKeyDown}
+ >
+
+
+
+
+
+ {message.role === "system" ? (
+
+ ) : (
+
+ {message.role === "assistant" ? (
+ selectedAssistant ? (
+ selectedAssistantImage ? (
+
+ ) : (
+
+ )
+ ) : (
+
+ )
+ ) : profile?.image_url ? (
+
+
+
+ ) : (
+
+ )}
+
+
+ {message.role === "assistant"
+ ? selectedAssistant
+ ? selectedAssistant?.name
+ : MODEL_DATA?.modelName
+ : profile?.display_name ?? profile?.username}
+
+
+ )}
+ {!firstTokenReceived &&
+ isGenerating &&
+ isLast &&
+ message.role === "assistant" ? (
+ <>
+ {(() => {
+ switch (toolInUse) {
+ case "none":
+ return (
+
+ )
+ case "retrieval":
+ return (
+
+
+
+
Reading documents...
+
+ )
+ default:
+ return null
+ }
+ })()}
+ >
+ ) : isEditing ? (
+
+ ) : (
+
+ )}
+
+
+ {fileItems.length > 0 && (
+
+
Sources
+
+
+ {fileItems.map(fileItem => {
+ const parentFile = files.find(
+ file => file.id === fileItem.file_id
+ )
+
+ return (
+
{
+ setSelectedFileItem(fileItem)
+ setShowFileItemPreview(true)
+ }}
+ >
+
+ {(() => {
+ let fileExtension = parentFile?.type.includes("/")
+ ? parentFile.type.split("/")[1]
+ : parentFile?.type
+
+ switch (fileExtension) {
+ case "pdf":
+ return
+ default:
+ return
+ }
+ })()}
+
+
+
+
{parentFile?.name}
+
+
+ {fileItem.content.substring(0, 70)}...
+
+
+
+ )
+ })}
+
+
+ )}
+
+
+ {message.image_paths.map((path, index) => {
+ const item = chatImages.find(image => image.path === path)
+
+ return (
+ {
+ setSelectedImage({
+ messageId: message.id,
+ path,
+ base64: path.startsWith("data") ? path : item?.base64 || "",
+ url: path.startsWith("data") ? "" : item?.url || "",
+ file: null
+ })
+
+ setShowImagePreview(true)
+ }}
+ loading="lazy"
+ />
+ )
+ })}
+
+ {isEditing && (
+
+
+
+
+
+ )}
+
+
+ {showImagePreview && selectedImage && (
+
{
+ setShowImagePreview(isOpen)
+ setSelectedImage(null)
+ }}
+ />
+ )}
+
+ {showFileItemPreview && selectedFileItem && (
+ {
+ setShowFileItemPreview(isOpen)
+ setSelectedFileItem(null)
+ }}
+ />
+ )}
+
+ )
+}
diff --git a/components/models/model-icon.tsx b/components/models/model-icon.tsx
new file mode 100644
index 0000000000..464fd49c40
--- /dev/null
+++ b/components/models/model-icon.tsx
@@ -0,0 +1,146 @@
+import { cn } from "@/lib/utils"
+import meta from "@/public/providers/meta.png"
+import mistral from "@/public/providers/mistral.png"
+import perplexity from "@/public/providers/perplexity.png"
+import { LLMID } from "@/types"
+import { IconSparkles } from "@tabler/icons-react"
+import { useTheme } from "next-themes"
+import Image from "next/image"
+import { FC, HTMLAttributes } from "react"
+import { AnthropicSVG } from "../icons/anthropic-svg"
+import { GoogleSVG } from "../icons/google-svg"
+import { OpenAISVG } from "../icons/openai-svg"
+
+interface ModelIconProps extends HTMLAttributes {
+ modelId: LLMID | string
+ height: number
+ width: number
+}
+
+export const ModelIcon: FC = ({
+ modelId,
+ height,
+ width,
+ ...props
+}) => {
+ const { theme } = useTheme()
+
+ switch (modelId as string) {
+ case "gpt-4-1106-preview":
+ case "gpt-4-vision-preview":
+ case "gpt-3.5-turbo-1106":
+ return (
+
+ )
+ case "llama2-7b":
+ case "llama2-70b":
+ return (
+
+ )
+ case "mistral-tiny":
+ case "mistral-small":
+ case "mistral-medium":
+ return (
+
+ )
+ case "claude-2.1":
+ case "claude-instant-1.2":
+ return (
+
+ )
+ case "gemini-pro":
+ case "gemini-pro-vision":
+ return (
+
+ )
+ case "pplx-7b-online":
+ case "pplx-70b-online":
+ return (
+
+ )
+ default:
+ if (!modelId) {
+ return
+ } else if (modelId.includes("llama")) {
+ return (
+
+ )
+ } else if (modelId.includes("mistral") || modelId.includes("mixtral")) {
+ return (
+
+ )
+ } else {
+ return
+ }
+ }
+}
diff --git a/components/models/model-option.tsx b/components/models/model-option.tsx
new file mode 100644
index 0000000000..8a1572c14a
--- /dev/null
+++ b/components/models/model-option.tsx
@@ -0,0 +1,50 @@
+import { ChatbotUIContext } from "@/context/context"
+import { isModelLocked } from "@/lib/is-model-locked"
+import { LLM, LLMID } from "@/types"
+import { IconLock } from "@tabler/icons-react"
+import { FC, useContext } from "react"
+import { WithTooltip } from "../ui/with-tooltip"
+import { ModelIcon } from "./model-icon"
+
+interface ModelOptionProps {
+ model: LLM
+ onSelect: () => void
+}
+
+export const ModelOption: FC = ({ model, onSelect }) => {
+ const { profile } = useContext(ChatbotUIContext)
+
+ if (!profile) return null
+
+ const isLocked = isModelLocked(model.provider, profile)
+
+ const handleSelectModel = () => {
+ if (isLocked) return
+
+ onSelect()
+ }
+
+ return (
+
+
+ {isLocked ? (
+
+ Save {model.provider} API key in profile settings to unlock.
+
+ }
+ trigger={
}
+ />
+ ) : (
+
+ )}
+
+
{model.modelName}
+
+
+ )
+}
diff --git a/components/models/model-select.tsx b/components/models/model-select.tsx
new file mode 100644
index 0000000000..0bc4dbba3e
--- /dev/null
+++ b/components/models/model-select.tsx
@@ -0,0 +1,190 @@
+import { ChatbotUIContext } from "@/context/context"
+import { isModelLocked } from "@/lib/is-model-locked"
+import { LLM, LLMID } from "@/types"
+import { IconCheck, IconChevronDown, IconLock } from "@tabler/icons-react"
+import { FC, useContext, useEffect, useRef, useState } from "react"
+import { Button } from "../ui/button"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuTrigger
+} from "../ui/dropdown-menu"
+import { Input } from "../ui/input"
+import { Tabs, TabsList, TabsTrigger } from "../ui/tabs"
+import { WithTooltip } from "../ui/with-tooltip"
+import { ModelIcon } from "./model-icon"
+import { ModelOption } from "./model-option"
+
+interface ModelSelectProps {
+ hostedModelOptions: LLM[]
+ localModelOptions: LLM[]
+ selectedModelId: string
+ onSelectModel: (modelId: LLMID) => void
+}
+
+export const ModelSelect: FC = ({
+ hostedModelOptions,
+ localModelOptions,
+ selectedModelId,
+ onSelectModel
+}) => {
+ const { profile } = useContext(ChatbotUIContext)
+
+ const inputRef = useRef(null)
+ const triggerRef = useRef(null)
+
+ const [isOpen, setIsOpen] = useState(false)
+ const [search, setSearch] = useState("")
+ const [tab, setTab] = useState<"hosted" | "local">("hosted")
+
+ useEffect(() => {
+ if (isOpen) {
+ setTimeout(() => {
+ inputRef.current?.focus()
+ }, 100) // FIX: hacky
+ }
+ }, [isOpen])
+
+ const handleSelectModel = (modelId: LLMID) => {
+ onSelectModel(modelId)
+ setIsOpen(false)
+ }
+
+ const ALL_MODELS = [...hostedModelOptions, ...localModelOptions]
+
+ const groupedModels = ALL_MODELS.reduce>(
+ (groups, model) => {
+ const key = model.provider
+ if (!groups[key]) {
+ groups[key] = []
+ }
+ groups[key].push(model)
+ return groups
+ },
+ {}
+ )
+
+ const SELECTED_MODEL = ALL_MODELS.find(
+ model => model.modelId === selectedModelId
+ )
+
+ if (!SELECTED_MODEL) return null
+ if (!profile) return null
+
+ const isLocked = isModelLocked(SELECTED_MODEL.provider, profile)
+
+ return (
+ {
+ setIsOpen(isOpen)
+ setSearch("")
+ }}
+ >
+
+
+
+
+
+ setTab(value)}>
+
+ Hosted
+ Local
+
+
+
+ setSearch(e.target.value)}
+ />
+
+
+ {Object.entries(groupedModels).map(([provider, models]) => {
+ const filteredModels = models
+ .filter(model => {
+ if (tab === "hosted") return model.provider !== "ollama"
+ if (tab === "local") return model.provider === "ollama"
+ })
+ .filter(model =>
+ model.modelName.toLowerCase().includes(search.toLowerCase())
+ )
+ .sort((a, b) => a.provider.localeCompare(b.provider))
+
+ if (filteredModels.length === 0) return null
+
+ return (
+
+
+ {provider === "openai" && profile.use_azure_openai
+ ? "AZURE OPENAI"
+ : provider.toLocaleUpperCase()}
+
+
+
+ {filteredModels.map(model => {
+ return (
+
+ {selectedModelId === model.modelId && (
+
+ )}
+
+ handleSelectModel(model.modelId)}
+ />
+
+ )
+ })}
+
+
+ )
+ })}
+
+
+
+ )
+}
diff --git a/components/setup/api-step.tsx b/components/setup/api-step.tsx
new file mode 100644
index 0000000000..57ca0e5a64
--- /dev/null
+++ b/components/setup/api-step.tsx
@@ -0,0 +1,197 @@
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { FC } from "react"
+import { Button } from "../ui/button"
+
+interface APIStepProps {
+ openaiAPIKey: string
+ openaiOrgID: string
+ azureOpenaiAPIKey: string
+ azureOpenaiEndpoint: string
+ azureOpenai35TurboID: string
+ azureOpenai45TurboID: string
+ azureOpenai45VisionID: string
+ anthropicAPIKey: string
+ googleGeminiAPIKey: string
+ mistralAPIKey: string
+ perplexityAPIKey: string
+ useAzureOpenai: boolean
+ onOpenaiAPIKeyChange: (value: string) => void
+ onOpenaiOrgIDChange: (value: string) => void
+ onAzureOpenaiAPIKeyChange: (value: string) => void
+ onAzureOpenaiEndpointChange: (value: string) => void
+ onAzureOpenai35TurboIDChange: (value: string) => void
+ onAzureOpenai45TurboIDChange: (value: string) => void
+ onAzureOpenai45VisionIDChange: (value: string) => void
+ onAnthropicAPIKeyChange: (value: string) => void
+ onGoogleGeminiAPIKeyChange: (value: string) => void
+ onMistralAPIKeyChange: (value: string) => void
+ onPerplexityAPIKeyChange: (value: string) => void
+ onUseAzureOpenaiChange: (value: boolean) => void
+}
+
+export const APIStep: FC = ({
+ openaiAPIKey,
+ openaiOrgID,
+ azureOpenaiAPIKey,
+ azureOpenaiEndpoint,
+ azureOpenai35TurboID,
+ azureOpenai45TurboID,
+ azureOpenai45VisionID,
+ anthropicAPIKey,
+ googleGeminiAPIKey,
+ mistralAPIKey,
+ perplexityAPIKey,
+ useAzureOpenai,
+ onOpenaiAPIKeyChange,
+ onOpenaiOrgIDChange,
+ onAzureOpenaiAPIKeyChange,
+ onAzureOpenaiEndpointChange,
+ onAzureOpenai35TurboIDChange,
+ onAzureOpenai45TurboIDChange,
+ onAzureOpenai45VisionIDChange,
+ onAnthropicAPIKeyChange,
+ onGoogleGeminiAPIKeyChange,
+ onMistralAPIKeyChange,
+ onPerplexityAPIKeyChange,
+ onUseAzureOpenaiChange
+}) => {
+ return (
+ <>
+
+
+
+
+ useAzureOpenai
+ ? onAzureOpenaiAPIKeyChange(e.target.value)
+ : onOpenaiAPIKeyChange(e.target.value)
+ }
+ />
+
+
+
+
+
+
+
+ onAnthropicAPIKeyChange(e.target.value)}
+ />
+
+
+
+
+
+ onGoogleGeminiAPIKeyChange(e.target.value)}
+ />
+
+
+
+
+
+ onMistralAPIKeyChange(e.target.value)}
+ />
+
+
+
+
+
+ onPerplexityAPIKeyChange(e.target.value)}
+ />
+
+ >
+ )
+}
diff --git a/components/setup/finish-step.tsx b/components/setup/finish-step.tsx
new file mode 100644
index 0000000000..74994e1914
--- /dev/null
+++ b/components/setup/finish-step.tsx
@@ -0,0 +1,15 @@
+import { FC } from "react"
+
+interface FinishStepProps {
+ displayName: string
+}
+
+export const FinishStep: FC = ({ displayName }) => {
+ return (
+
+
Welcome to Chatbot UI, {displayName.split(" ")[0]}!
+
+
Click next to start chatting.
+
+ )
+}
diff --git a/components/setup/profile-step.tsx b/components/setup/profile-step.tsx
new file mode 100644
index 0000000000..ad750e042e
--- /dev/null
+++ b/components/setup/profile-step.tsx
@@ -0,0 +1,198 @@
+import ImagePicker from "@/components/ui/image-picker"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import {
+ PROFILE_CONTEXT_MAX,
+ PROFILE_DISPLAY_NAME_MAX,
+ PROFILE_USERNAME_MAX,
+ PROFILE_USERNAME_MIN
+} from "@/db/limits"
+import {
+ IconCircleCheckFilled,
+ IconCircleXFilled,
+ IconLoader2
+} from "@tabler/icons-react"
+import { FC, useCallback, useState } from "react"
+import ReactTextareaAutosize from "react-textarea-autosize"
+import { LimitDisplay } from "../ui/limit-display"
+
+interface ProfileStepProps {
+ profileContext: string
+ profileImage: File | null
+ profileImageSrc: string
+ username: string
+ usernameAvailable: boolean
+ displayName: string
+ onProfileContextChange: (profileContext: string) => void
+ onProfileImageChange: (image: File) => void
+ onProfileImageChangeSrc: (image: string) => void
+ onUsernameAvailableChange: (isAvailable: boolean) => void
+ onUsernameChange: (username: string) => void
+ onDisplayNameChange: (name: string) => void
+}
+
+export const ProfileStep: FC = ({
+ profileContext,
+ profileImage,
+ profileImageSrc,
+ username,
+ usernameAvailable,
+ displayName,
+ onProfileContextChange,
+ onProfileImageChange,
+ onProfileImageChangeSrc,
+ onUsernameAvailableChange,
+ onUsernameChange,
+ onDisplayNameChange
+}) => {
+ const [loading, setLoading] = useState(false)
+
+ const debounce = (func: (...args: any[]) => void, wait: number) => {
+ let timeout: NodeJS.Timeout | null
+
+ return (...args: any[]) => {
+ const later = () => {
+ if (timeout) clearTimeout(timeout)
+ func(...args)
+ }
+
+ if (timeout) clearTimeout(timeout)
+ timeout = setTimeout(later, wait)
+ }
+ }
+
+ const checkUsernameAvailability = useCallback(
+ debounce(async (username: string) => {
+ if (!username) return
+
+ if (username.length < PROFILE_USERNAME_MIN) {
+ onUsernameAvailableChange(false)
+ return
+ }
+
+ if (username.length > PROFILE_USERNAME_MAX) {
+ onUsernameAvailableChange(false)
+ return
+ }
+
+ const usernameRegex = /^[a-zA-Z0-9_]+$/
+ if (!usernameRegex.test(username)) {
+ onUsernameAvailableChange(false)
+ alert(
+ "Username must be letters, numbers, or underscores only - no other characters or spacing allowed."
+ )
+ return
+ }
+
+ setLoading(true)
+
+ const response = await fetch(`/api/username/available`, {
+ method: "POST",
+ body: JSON.stringify({ username })
+ })
+
+ const data = await response.json()
+ const isAvailable = data.isAvailable
+
+ onUsernameAvailableChange(isAvailable)
+
+ setLoading(false)
+ }, 500),
+ []
+ )
+
+ return (
+ <>
+
+
+
+
+
+ {usernameAvailable ? (
+
AVAILABLE
+ ) : (
+
UNAVAILABLE
+ )}
+
+
+
+
+
{
+ onUsernameChange(e.target.value)
+ checkUsernameAvailability(e.target.value)
+ }}
+ minLength={PROFILE_USERNAME_MIN}
+ maxLength={PROFILE_USERNAME_MAX}
+ />
+
+
+ {loading ? (
+
+ ) : usernameAvailable ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ onDisplayNameChange(e.target.value)}
+ maxLength={PROFILE_DISPLAY_NAME_MAX}
+ />
+
+
+
+
+
+
+
+ onProfileContextChange(e.target.value)}
+ />
+
+
+
+ >
+ )
+}
diff --git a/components/setup/step-container.tsx b/components/setup/step-container.tsx
new file mode 100644
index 0000000000..4e0ef30be6
--- /dev/null
+++ b/components/setup/step-container.tsx
@@ -0,0 +1,89 @@
+import { Button } from "@/components/ui/button"
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle
+} from "@/components/ui/card"
+import { FC, useRef } from "react"
+
+export const SETUP_STEP_COUNT = 4
+
+interface StepContainerProps {
+ stepDescription: string
+ stepNum: number
+ stepTitle: string
+ onShouldProceed: (shouldProceed: boolean) => void
+ children?: React.ReactNode
+ showBackButton?: boolean
+ showNextButton?: boolean
+}
+
+export const StepContainer: FC = ({
+ stepDescription,
+ stepNum,
+ stepTitle,
+ onShouldProceed,
+ children,
+ showBackButton = false,
+ showNextButton = true
+}) => {
+ const buttonRef = useRef(null)
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter" && !e.shiftKey) {
+ if (buttonRef.current) {
+ buttonRef.current.click()
+ }
+ }
+ }
+
+ return (
+
+
+
+ {stepTitle}
+
+
+ {stepNum} / {SETUP_STEP_COUNT}
+
+
+
+ {stepDescription}
+
+
+ {children}
+
+
+
+ {showBackButton && (
+
+ )}
+
+
+
+ {showNextButton && (
+
+ )}
+
+
+
+ )
+}
diff --git a/components/setup/workspace-step.tsx b/components/setup/workspace-step.tsx
new file mode 100644
index 0000000000..8931eb177a
--- /dev/null
+++ b/components/setup/workspace-step.tsx
@@ -0,0 +1,52 @@
+import { Label } from "@/components/ui/label"
+import { WORKSPACE_INSTRUCTIONS_MAX } from "@/db/limits"
+import { ChatSettings } from "@/types"
+import { FC } from "react"
+import ReactTextareaAutosize from "react-textarea-autosize"
+import { ChatSettingsForm } from "../ui/chat-settings-form"
+import { LimitDisplay } from "../ui/limit-display"
+
+interface WorkspaceStepProps {
+ chatSettings: ChatSettings
+ workspaceInstructions: string
+ onChatSettingsChange: (chatSettings: ChatSettings) => void
+ onWorkspaceInstructionsChange: (workspaceInstructions: string) => void
+}
+
+export const WorkspaceStep: FC = ({
+ chatSettings: defaultChatSettings,
+ workspaceInstructions,
+ onChatSettingsChange,
+ onWorkspaceInstructionsChange
+}) => {
+ return (
+ <>
+
+
+
+ onWorkspaceInstructionsChange(e.target.value)}
+ maxLength={WORKSPACE_INSTRUCTIONS_MAX}
+ />
+
+
+
+
+
+ >
+ )
+}
diff --git a/components/sharing/add-to-workspace.tsx b/components/sharing/add-to-workspace.tsx
new file mode 100644
index 0000000000..547036b212
--- /dev/null
+++ b/components/sharing/add-to-workspace.tsx
@@ -0,0 +1,209 @@
+import { ChatbotUIContext } from "@/context/context"
+import { createAssistant } from "@/db/assistants"
+import { createPreset } from "@/db/presets"
+import { createPrompt } from "@/db/prompts"
+import { getWorkspaceById } from "@/db/workspaces"
+import { Tables } from "@/supabase/types"
+import { ContentType, DataItemType } from "@/types"
+import { useRouter } from "next/navigation"
+import { FC, useContext } from "react"
+import { toast } from "sonner"
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList
+} from "../ui/command"
+
+interface AddToWorkspaceProps {
+ contentType: ContentType
+ item: DataItemType
+}
+
+export const AddToWorkspace: FC = ({
+ contentType,
+ item
+}) => {
+ const {
+ profile,
+ setSelectedWorkspace,
+ workspaces,
+ setChats,
+ setPresets,
+ setPrompts,
+ setFiles,
+ setCollections,
+ setAssistants
+ } = useContext(ChatbotUIContext)
+
+ const router = useRouter()
+
+ const createFunctions = {
+ chats: async (
+ item: Tables<"chats">,
+ workspaceId: string,
+ userId: string
+ ) => {
+ // TODO
+ // const createdChat = await createChat(
+ // {
+ // user_id: userId,
+ // folder_id: null,
+ // description: item.description,
+ // name: item.name,
+ // prompt: item.prompt,
+ // temperature: item.temperature
+ // },
+ // workspaceId
+ // )
+ // return createdChat
+ },
+ presets: async (
+ item: Tables<"presets">,
+ workspaceId: string,
+ userId: string
+ ) => {
+ const createdPreset = await createPreset(
+ {
+ user_id: userId,
+ folder_id: null,
+ context_length: item.context_length,
+ description: item.description,
+ include_profile_context: item.include_profile_context,
+ include_workspace_instructions: item.include_workspace_instructions,
+ model: item.model,
+ name: item.name,
+ prompt: item.prompt,
+ temperature: item.temperature,
+ embeddings_provider: item.embeddings_provider
+ },
+ workspaceId
+ )
+
+ return createdPreset
+ },
+ prompts: async (
+ item: Tables<"prompts">,
+ workspaceId: string,
+ userId: string
+ ) => {
+ const createdPrompt = await createPrompt(
+ {
+ user_id: userId,
+ folder_id: null,
+ content: item.content,
+ name: item.name
+ },
+ workspaceId
+ )
+
+ return createdPrompt
+ },
+ files: async (
+ item: Tables<"files">,
+ workspaceId: string,
+ userId: string
+ ) => {
+ // TODO also need file items to duplicate
+ // const createdFile = await createFile(
+ // {
+ // user_id: userId,
+ // folder_id: null,
+ // name: item.name
+ // },
+ // workspaceId
+ // )
+ // return createdFile
+ },
+ collections: async (
+ item: Tables<"collections">,
+ workspaceId: string,
+ userId: string
+ ) => {
+ // TODO also need to duplicate each file item in the collection and file items
+ },
+ assistants: async (
+ item: Tables<"assistants">,
+ workspaceId: string,
+ userId: string
+ ) => {
+ const createdAssistant = await createAssistant(
+ {
+ user_id: userId,
+ folder_id: null,
+ context_length: item.context_length,
+ description: item.description,
+ include_profile_context: item.include_profile_context,
+ include_workspace_instructions: item.include_workspace_instructions,
+ model: item.model,
+ name: item.name,
+ // TODO need to duplicate image
+ image_path: item.image_path,
+ prompt: item.prompt,
+ temperature: item.temperature,
+ embeddings_provider: item.embeddings_provider
+ },
+ workspaceId
+ )
+
+ return createdAssistant
+ }
+ }
+
+ const stateUpdateFunctions = {
+ chats: setChats,
+ presets: setPresets,
+ prompts: setPrompts,
+ files: setFiles,
+ collections: setCollections,
+ assistants: setAssistants
+ }
+
+ const handleAddToWorkspace = async (workspaceId: string) => {
+ if (!profile) return
+
+ const createFunction = createFunctions[contentType]
+ const setStateFunction = stateUpdateFunctions[contentType]
+
+ if (!createFunction || !setStateFunction) return
+
+ const newItem = await createFunction(
+ item as any,
+ workspaceId,
+ profile.user_id
+ )
+
+ setStateFunction((prevItems: any) => [...prevItems, newItem])
+
+ toast.success(`Added ${item.name} to workspace.`)
+
+ const workspace = await getWorkspaceById(workspaceId)
+ setSelectedWorkspace(workspace)
+
+ router.push(`/chat/?tab=${contentType}`)
+ }
+
+ return (
+
+
+
+
+ No workspaces found.
+
+
+ {workspaces.map(workspace => (
+ handleAddToWorkspace(workspace.id)}
+ >
+ Add to {workspace.name}
+
+ ))}
+
+
+
+ )
+}
diff --git a/components/sharing/share-assistant.tsx b/components/sharing/share-assistant.tsx
new file mode 100644
index 0000000000..02e272a279
--- /dev/null
+++ b/components/sharing/share-assistant.tsx
@@ -0,0 +1,23 @@
+import { Tables } from "@/supabase/types"
+import { User } from "@supabase/supabase-js"
+import { FC } from "react"
+import { ShareItem } from "./share-item"
+
+interface ShareAssistantProps {
+ user: User | null
+ assistant: Tables<"assistants">
+}
+
+export const ShareAssistant: FC = ({
+ user,
+ assistant
+}) => {
+ return (
+ {assistant.name}
}
+ />
+ )
+}
diff --git a/components/sharing/share-chat.tsx b/components/sharing/share-chat.tsx
new file mode 100644
index 0000000000..66127500ba
--- /dev/null
+++ b/components/sharing/share-chat.tsx
@@ -0,0 +1,149 @@
+import { ChatbotUIContext } from "@/context/context"
+import { Tables } from "@/supabase/types"
+import { User } from "@supabase/supabase-js"
+import { IconDownload, IconLock } from "@tabler/icons-react"
+import { FC, useContext, useState } from "react"
+import { Button } from "../ui/button"
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle
+} from "../ui/card"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuTrigger
+} from "../ui/dropdown-menu"
+import { WithTooltip } from "../ui/with-tooltip"
+import { AddToWorkspace } from "./add-to-workspace"
+import { ShareMessage } from "./share-message"
+
+interface ShareChatProps {
+ user: User | null
+ chat: Tables<"chats">
+ messages: Tables<"messages">[]
+ username: string
+}
+
+export const ShareChat: FC = ({
+ user,
+ chat,
+ messages,
+ username
+}) => {
+ const { profile } = useContext(ChatbotUIContext)
+
+ const [showWorkspaceMenu, setShowWorkspaceMenu] = useState(false)
+
+ const handleDownload = () => {
+ let chatData: any = {
+ contentType: "chats",
+ context_length: chat.context_length,
+ include_profile_context: chat.include_profile_context,
+ include_workspace_instructions: chat.include_workspace_instructions,
+ model: chat.model,
+ name: chat.name,
+ prompt: chat.prompt,
+ temperature: chat.temperature
+ }
+
+ let messagesData: any = messages.map(message => ({
+ id: message.id,
+ content: message.content,
+ model: message.model,
+ role: message.role,
+ sequence_number: message.sequence_number
+ }))
+
+ let data: any = {
+ contentType: "chats",
+ chat: chatData,
+ messages: messagesData
+ }
+
+ const element = document.createElement("a")
+ const file = new Blob([JSON.stringify(data)], {
+ type: "text/plain"
+ })
+ element.href = URL.createObjectURL(file)
+ element.download = `${data.name}.json`
+ document.body.appendChild(element)
+ element.click()
+ document.body.removeChild(element)
+ }
+
+ return (
+
+
+
+ {user?.id === profile?.user_id && user
+ ? `You created this chat`
+ : `Chat created by ${username || "Anonymous"}`}
+
+
+ {chat.name}
+
+
+
+ {messages.map(message => (
+
+ ))}
+
+
+
+ {(user?.id !== profile?.user_id || !user) && (
+ setShowWorkspaceMenu(open)}
+ >
+ setShowWorkspaceMenu(!showWorkspaceMenu)}
+ disabled={!user?.id}
+ >
+
+ {!user?.id
+ ? `Sign up for Chatbot UI to continue this chat.`
+ : "Continue this chat in a workspace."}
+
+ }
+ trigger={
+
+ }
+ />
+
+
+
+
+
+
+ )}
+
+
+ Export
}
+ trigger={
+
+ }
+ />
+
+
+
+ )
+}
diff --git a/components/sharing/share-collection.tsx b/components/sharing/share-collection.tsx
new file mode 100644
index 0000000000..42277af35b
--- /dev/null
+++ b/components/sharing/share-collection.tsx
@@ -0,0 +1,23 @@
+import { Tables } from "@/supabase/types"
+import { User } from "@supabase/supabase-js"
+import { FC } from "react"
+import { ShareItem } from "./share-item"
+
+interface ShareCollectionProps {
+ user: User | null
+ collection: Tables<"collections">
+}
+
+export const ShareCollection: FC = ({
+ user,
+ collection
+}) => {
+ return (
+ {collection.name}
}
+ />
+ )
+}
diff --git a/components/sharing/share-file.tsx b/components/sharing/share-file.tsx
new file mode 100644
index 0000000000..1ae04402e7
--- /dev/null
+++ b/components/sharing/share-file.tsx
@@ -0,0 +1,44 @@
+import { Tables } from "@/supabase/types"
+import { User } from "@supabase/supabase-js"
+import Link from "next/link"
+import { FC } from "react"
+import { formatFileSize } from "../sidebar/items/files/file-item"
+import { ShareItem } from "./share-item"
+
+interface ShareFileProps {
+ user: User | null
+ file: Tables<"files">
+ link: string
+}
+
+export const ShareFile: FC = ({ user, file, link }) => {
+ return (
+ (
+ <>
+ {file.name}
+
+
+ View
+
+
+
+
{file.type}
+
+
{formatFileSize(file.size)}
+
+
{file.tokens.toLocaleString()} tokens
+
+ >
+ )}
+ />
+ )
+}
diff --git a/components/sharing/share-header.tsx b/components/sharing/share-header.tsx
new file mode 100644
index 0000000000..dc28d480c2
--- /dev/null
+++ b/components/sharing/share-header.tsx
@@ -0,0 +1,25 @@
+import { Session } from "@supabase/supabase-js"
+import Link from "next/link"
+import { FC } from "react"
+import { Button } from "../ui/button"
+
+interface ShareHeaderProps {
+ session: Session
+}
+
+export const ShareHeader: FC = ({ session }) => {
+ return (
+
+
+ Chatbot UI
+
+
+
+
+
+
+ )
+}
diff --git a/components/sharing/share-item.tsx b/components/sharing/share-item.tsx
new file mode 100644
index 0000000000..16a6c9b57b
--- /dev/null
+++ b/components/sharing/share-item.tsx
@@ -0,0 +1,196 @@
+import Loading from "@/app/loading"
+import { ContentType } from "@/types"
+import { User } from "@supabase/supabase-js"
+import { IconDownload, IconLock } from "@tabler/icons-react"
+import { FC, ReactNode, useEffect, useState } from "react"
+import { Button } from "../ui/button"
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader
+} from "../ui/card"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuTrigger
+} from "../ui/dropdown-menu"
+import { WithTooltip } from "../ui/with-tooltip"
+import { AddToWorkspace } from "./add-to-workspace"
+
+interface ShareItemProps {
+ user: User | null
+ item: any // DataItemType
+ contentType: ContentType
+ renderContent: () => ReactNode
+}
+
+export const ShareItem: FC = ({
+ user,
+ item,
+ contentType,
+ renderContent
+}) => {
+ const [showWorkspaceMenu, setShowWorkspaceMenu] = useState(false)
+ const [username, setUsername] = useState("")
+ const [loading, setLoading] = useState(true)
+
+ useEffect(() => {
+ const fetchData = async () => {
+ setLoading(true)
+
+ const response = await fetch("/api/username/get", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json"
+ },
+ body: JSON.stringify({ userId: item.user_id })
+ })
+
+ const data = await response.json()
+ setUsername(data.username)
+
+ setLoading(false)
+ }
+
+ fetchData()
+ }, [])
+
+ const handleExport = () => {
+ let data: any = {
+ contentType: contentType
+ }
+
+ switch (contentType) {
+ case "presets":
+ data = {
+ ...data,
+ context_length: item.context_length,
+ description: item.description,
+ include_profile_context: item.include_profile_context,
+ include_workspace_instructions: item.include_workspace_instructions,
+ model: item.model,
+ name: item.name,
+ prompt: item.prompt,
+ temperature: item.temperature
+ }
+ break
+
+ case "prompts":
+ data = {
+ ...data,
+ content: item.content,
+ name: item.name
+ }
+ break
+
+ case "files":
+ data = {
+ ...data,
+ name: item.name
+ }
+ break
+
+ case "collections":
+ data = {
+ ...data,
+ name: item.name
+ }
+ break
+
+ case "assistants":
+ data = {
+ ...data,
+ name: item.name
+ }
+ break
+
+ default:
+ break
+ }
+
+ const element = document.createElement("a")
+ const file = new Blob([JSON.stringify(data)], {
+ type: "text/plain"
+ })
+ element.href = URL.createObjectURL(file)
+ element.download = `${data.name}.json`
+ document.body.appendChild(element)
+ element.click()
+ document.body.removeChild(element)
+ }
+
+ if (loading) {
+ return
+ }
+
+ return (
+
+
+
+ {user?.id === item?.user_id && user
+ ? `You created this ${contentType.slice(0, -1)}`
+ : `${
+ contentType.charAt(0).toUpperCase() + contentType.slice(1, -1)
+ } created by ${username || "Anonymous"}`}
+
+
+
+
+ {renderContent()}
+
+
+
+ {(user?.id !== item?.user_id || !user) && (
+ setShowWorkspaceMenu(open)}
+ >
+ setShowWorkspaceMenu(!showWorkspaceMenu)}
+ disabled={!user?.id}
+ >
+
+ {!user?.id
+ ? `Sign up for Chatbot UI to continue this ${contentType.slice(
+ 0,
+ -1
+ )}.`
+ : `Use this ${contentType.slice(0, -1)} in a workspace.`}
+
+ }
+ trigger={
+
+ }
+ />
+
+
+
+
+
+
+ )}
+
+
+ Export
}
+ trigger={
+
+ }
+ />
+
+
+
+ )
+}
diff --git a/components/sharing/share-message.tsx b/components/sharing/share-message.tsx
new file mode 100644
index 0000000000..4c1a36ac8f
--- /dev/null
+++ b/components/sharing/share-message.tsx
@@ -0,0 +1,42 @@
+import { LLM_LIST } from "@/lib/models/llm/llm-list"
+import { Tables } from "@/supabase/types"
+import { IconMoodSmile } from "@tabler/icons-react"
+import { FC } from "react"
+import { MessageMarkdown } from "../messages/message-markdown"
+import { ModelIcon } from "../models/model-icon"
+
+interface MessageProps {
+ username: string
+ message: Tables<"messages">
+}
+
+export const ShareMessage: FC = ({ username, message }) => {
+ const modelData = LLM_LIST.find(
+ llmModel => llmModel.modelId === message.model
+ )
+
+ const isUser = message.role === "user"
+ const ICON_SIZE = 28
+ const icon = isUser ? (
+
+ ) : (
+
+ )
+ const label = isUser ? "User" : modelData?.modelName
+ const bgClass = isUser ? "bg-muted/50" : "bg-muted"
+
+ return (
+
+ )
+}
diff --git a/components/sharing/share-page.tsx b/components/sharing/share-page.tsx
new file mode 100644
index 0000000000..7a58aefb66
--- /dev/null
+++ b/components/sharing/share-page.tsx
@@ -0,0 +1,71 @@
+"use client"
+
+import { ShareHeader } from "@/components/sharing/share-header"
+import { ScreenLoader } from "@/components/ui/screen-loader"
+import { supabase } from "@/lib/supabase/browser-client"
+import { ContentType, DataItemType } from "@/types"
+import { FC, useEffect, useState } from "react"
+
+interface SharePageProps {
+ contentType: ContentType
+ id: string
+ fetchById: (id: string) => Promise
+ ViewComponent: FC
+}
+
+export default function SharePage({
+ contentType,
+ id,
+ fetchById,
+ ViewComponent
+}: SharePageProps) {
+ const [session, setSession] = useState(null)
+ const [loading, setLoading] = useState(true)
+ const [item, setItem] = useState(null)
+
+ const onLoad = async () => {
+ const session = (await supabase.auth.getSession()).data.session
+
+ setSession(session)
+
+ const fetchedItem = await fetchById(id)
+ setItem(fetchedItem)
+
+ if (!fetchedItem) {
+ setLoading(false)
+ return
+ }
+
+ setLoading(false)
+ }
+
+ useEffect(() => {
+ onLoad()
+ }, [])
+
+ const itemProps = { [contentType.slice(0, -1)]: item }
+
+ if (loading) {
+ return
+ }
+
+ if (!item) {
+ return (
+
+ {contentType.slice(0, -1).charAt(0).toUpperCase() +
+ contentType.slice(1, -1)}{" "}
+ not found.
+
+ )
+ }
+
+ return (
+
+ )
+}
diff --git a/components/sharing/share-preset.tsx b/components/sharing/share-preset.tsx
new file mode 100644
index 0000000000..599f587077
--- /dev/null
+++ b/components/sharing/share-preset.tsx
@@ -0,0 +1,73 @@
+import { Tables } from "@/supabase/types"
+import { User } from "@supabase/supabase-js"
+import { FC } from "react"
+import { ModelIcon } from "../models/model-icon"
+import { Checkbox } from "../ui/checkbox"
+import { Label } from "../ui/label"
+import { ShareItem } from "./share-item"
+
+interface SharePresetProps {
+ user: User | null
+ preset: Tables<"presets">
+}
+
+export const SharePreset: FC = ({ user, preset }) => {
+ return (
+ (
+
+
{preset.name}
+
+
{preset.description}
+
+
+
+
+
+
+
+
+
+
{preset.model}
+
+
+
+
+
+
+
{preset.prompt}
+
+
+
+
+
+
{preset.temperature}
+
+
+
+
+
+
+ {preset.context_length.toLocaleString()}
+
+
+
+
+
+
+
Includes Profile Context
+
+
+
+
+
+
Includes Workspace Instructions
+
+
+ )}
+ />
+ )
+}
diff --git a/components/sharing/share-prompt.tsx b/components/sharing/share-prompt.tsx
new file mode 100644
index 0000000000..c4b8a1192d
--- /dev/null
+++ b/components/sharing/share-prompt.tsx
@@ -0,0 +1,26 @@
+import { Tables } from "@/supabase/types"
+import { User } from "@supabase/supabase-js"
+import { FC } from "react"
+import { ShareItem } from "./share-item"
+
+interface SharePromptProps {
+ user: User | null
+ prompt: Tables<"prompts">
+}
+
+export const SharePrompt: FC = ({ user, prompt }) => {
+ return (
+ (
+
+
{prompt.name}
+
+
{prompt.content}
+
+ )}
+ />
+ )
+}
diff --git a/components/sidebar/items/all/sidebar-create-item.tsx b/components/sidebar/items/all/sidebar-create-item.tsx
new file mode 100644
index 0000000000..21ca66efba
--- /dev/null
+++ b/components/sidebar/items/all/sidebar-create-item.tsx
@@ -0,0 +1,198 @@
+import { Button } from "@/components/ui/button"
+import {
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle
+} from "@/components/ui/dialog"
+import { ChatbotUIContext } from "@/context/context"
+import { createAssistant, updateAssistant } from "@/db/assistants"
+import { createChat } from "@/db/chats"
+import { createCollectionFiles } from "@/db/collection-files"
+import { createCollection } from "@/db/collections"
+import { createFile } from "@/db/files"
+import { createPreset } from "@/db/presets"
+import { createPrompt } from "@/db/prompts"
+import {
+ getAssistantImageFromStorage,
+ uploadAssistantImage
+} from "@/db/storage/assistant-images"
+import { convertBlobToBase64 } from "@/lib/blob-to-b64"
+import { Tables, TablesInsert } from "@/supabase/types"
+import { ContentType } from "@/types"
+import { FC, useContext, useRef, useState } from "react"
+
+interface SidebarCreateItemProps {
+ isOpen: boolean
+ onOpenChange: (isOpen: boolean) => void
+ contentType: ContentType
+ renderInputs: () => JSX.Element
+ createState: any
+}
+
+export const SidebarCreateItem: FC = ({
+ isOpen,
+ onOpenChange,
+ contentType,
+ renderInputs,
+ createState
+}) => {
+ const {
+ selectedWorkspace,
+ setChats,
+ setPresets,
+ setPrompts,
+ setFiles,
+ setCollections,
+ setAssistants,
+ setAssistantImages
+ } = useContext(ChatbotUIContext)
+
+ const buttonRef = useRef(null)
+
+ const [creating, setCreating] = useState(false)
+
+ const createFunctions = {
+ chats: createChat,
+ presets: createPreset,
+ prompts: createPrompt,
+ files: async (
+ createState: { file: File } & TablesInsert<"files">,
+ workspaceId: string
+ ) => {
+ if (!selectedWorkspace) return
+
+ const { file, ...rest } = createState
+
+ const createdFile = await createFile(
+ file,
+ rest,
+ workspaceId,
+ selectedWorkspace.embeddings_provider as "openai" | "local"
+ )
+
+ return createdFile
+ },
+ collections: async (
+ createState: {
+ image: File
+ collectionFiles: TablesInsert<"collection_files">[]
+ } & Tables<"collections">,
+ workspaceId: string
+ ) => {
+ const { collectionFiles, image, ...rest } = createState
+
+ const createdCollection = await createCollection(rest, workspaceId)
+
+ const finalCollectionFiles = collectionFiles.map(collectionFile => ({
+ ...collectionFile,
+ collection_id: createdCollection.id
+ }))
+
+ await createCollectionFiles(finalCollectionFiles)
+
+ return createdCollection
+ },
+ assistants: async (
+ createState: {
+ image: File
+ } & Tables<"assistants">,
+ workspaceId: string
+ ) => {
+ const { image, ...rest } = createState
+
+ const createdAssistant = await createAssistant(rest, workspaceId)
+
+ const filePath = await uploadAssistantImage(createdAssistant, image)
+
+ const updatedAssistant = await updateAssistant(createdAssistant.id, {
+ image_path: filePath
+ })
+
+ const url = (await getAssistantImageFromStorage(filePath)) || ""
+
+ if (url) {
+ const response = await fetch(url)
+ const blob = await response.blob()
+ const base64 = await convertBlobToBase64(blob)
+
+ setAssistantImages(prev => [
+ ...prev,
+ {
+ assistantId: updatedAssistant.id,
+ path: filePath,
+ base64,
+ url
+ }
+ ])
+ }
+
+ return updatedAssistant
+ }
+ }
+
+ const stateUpdateFunctions = {
+ chats: setChats,
+ presets: setPresets,
+ prompts: setPrompts,
+ files: setFiles,
+ collections: setCollections,
+ assistants: setAssistants
+ }
+
+ const handleCreate = async () => {
+ if (!selectedWorkspace) return
+
+ const createFunction = createFunctions[contentType]
+ const setStateFunction = stateUpdateFunctions[contentType]
+
+ if (!createFunction || !setStateFunction) return
+
+ setCreating(true)
+
+ const newItem = await createFunction(createState, selectedWorkspace.id)
+
+ setStateFunction((prevItems: any) => [...prevItems, newItem])
+
+ onOpenChange(false)
+ setCreating(false)
+ }
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault()
+ buttonRef.current?.click()
+ }
+ }
+
+ return (
+
+ )
+}
diff --git a/components/sidebar/items/all/sidebar-delete-item.tsx b/components/sidebar/items/all/sidebar-delete-item.tsx
new file mode 100644
index 0000000000..b56f8af7ff
--- /dev/null
+++ b/components/sidebar/items/all/sidebar-delete-item.tsx
@@ -0,0 +1,129 @@
+import { Button } from "@/components/ui/button"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger
+} from "@/components/ui/dialog"
+import { ChatbotUIContext } from "@/context/context"
+import { deleteAssistant } from "@/db/assistants"
+import { deleteChat } from "@/db/chats"
+import { deleteCollection } from "@/db/collections"
+import { deleteFile } from "@/db/files"
+import { deletePreset } from "@/db/presets"
+import { deletePrompt } from "@/db/prompts"
+import { deleteFileFromStorage } from "@/db/storage/files"
+import { Tables } from "@/supabase/types"
+import { ContentType, DataItemType } from "@/types"
+import { FC, useContext, useRef, useState } from "react"
+
+interface SidebarDeleteItemProps {
+ item: DataItemType
+ contentType: ContentType
+}
+
+export const SidebarDeleteItem: FC = ({
+ item,
+ contentType
+}) => {
+ const {
+ setChats,
+ setPresets,
+ setPrompts,
+ setFiles,
+ setCollections,
+ setAssistants
+ } = useContext(ChatbotUIContext)
+
+ const buttonRef = useRef(null)
+
+ const [showDialog, setShowDialog] = useState(false)
+
+ const deleteFunctions = {
+ chats: async (chat: Tables<"chats">) => {
+ await deleteChat(chat.id)
+ },
+ presets: async (preset: Tables<"presets">) => {
+ await deletePreset(preset.id)
+ },
+ prompts: async (prompt: Tables<"prompts">) => {
+ await deletePrompt(prompt.id)
+ },
+ files: async (file: Tables<"files">) => {
+ await deleteFileFromStorage(file.file_path)
+ await deleteFile(file.id)
+ },
+ collections: async (collection: Tables<"collections">) => {
+ await deleteCollection(collection.id)
+ },
+ assistants: async (assistant: Tables<"assistants">) => {
+ await deleteAssistant(assistant.id)
+ setChats(prevState =>
+ prevState.filter(chat => chat.assistant_id !== assistant.id)
+ )
+ }
+ }
+
+ const stateUpdateFunctions = {
+ chats: setChats,
+ presets: setPresets,
+ prompts: setPrompts,
+ files: setFiles,
+ collections: setCollections,
+ assistants: setAssistants
+ }
+
+ const handleDelete = async () => {
+ const deleteFunction = deleteFunctions[contentType]
+ const setStateFunction = stateUpdateFunctions[contentType]
+
+ if (!deleteFunction || !setStateFunction) return
+
+ await deleteFunction(item as any)
+
+ setStateFunction((prevItems: any) =>
+ prevItems.filter((prevItem: any) => prevItem.id !== item.id)
+ )
+
+ setShowDialog(false)
+ }
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") {
+ buttonRef.current?.click()
+ }
+ }
+
+ return (
+
+ )
+}
diff --git a/components/sidebar/items/all/sidebar-display-item.tsx b/components/sidebar/items/all/sidebar-display-item.tsx
new file mode 100644
index 0000000000..7312ae820b
--- /dev/null
+++ b/components/sidebar/items/all/sidebar-display-item.tsx
@@ -0,0 +1,122 @@
+import { ChatbotUIContext } from "@/context/context"
+import { createChat } from "@/db/chats"
+import { cn } from "@/lib/utils"
+import { Tables } from "@/supabase/types"
+import { ContentType, DataItemType } from "@/types"
+import { useRouter } from "next/navigation"
+import { FC, useContext, useRef, useState } from "react"
+import { SidebarUpdateItem } from "./sidebar-update-item"
+
+interface SidebarItemProps {
+ item: DataItemType
+ contentType: ContentType
+ icon: React.ReactNode
+ updateState: any
+ renderInputs: () => JSX.Element
+}
+
+export const SidebarItem: FC = ({
+ item,
+ contentType,
+ updateState,
+ renderInputs,
+ icon
+}) => {
+ const { selectedWorkspace, setChats, setSelectedAssistant } =
+ useContext(ChatbotUIContext)
+
+ const router = useRouter()
+
+ const itemRef = useRef(null)
+
+ const [isHovering, setIsHovering] = useState(false)
+
+ const actionMap = {
+ chats: async (item: any) => {},
+ presets: async (item: any) => {},
+ prompts: async (item: any) => {},
+ files: async (item: any) => {},
+ collections: async (item: any) => {},
+ assistants: async (assistant: Tables<"assistants">) => {
+ if (!selectedWorkspace) return
+
+ const createdChat = await createChat({
+ user_id: assistant.user_id,
+ workspace_id: selectedWorkspace.id,
+ assistant_id: assistant.id,
+ context_length: assistant.context_length,
+ include_profile_context: assistant.include_profile_context,
+ include_workspace_instructions:
+ assistant.include_workspace_instructions,
+ model: assistant.model,
+ name: `Chat with ${assistant.name}`,
+ prompt: assistant.prompt,
+ temperature: assistant.temperature,
+ embeddings_provider: assistant.embeddings_provider
+ })
+
+ setChats(prevState => [createdChat, ...prevState])
+ setSelectedAssistant(assistant)
+
+ router.push(`/chat/${createdChat.id}`)
+ }
+ }
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") {
+ e.stopPropagation()
+ itemRef.current?.click()
+ }
+ }
+
+ // const handleClickAction = async (
+ // e: React.MouseEvent
+ // ) => {
+ // e.stopPropagation()
+
+ // const action = actionMap[contentType]
+
+ // await action(item as any)
+ // }
+
+ return (
+
+ setIsHovering(true)}
+ onMouseLeave={() => setIsHovering(false)}
+ >
+ {icon}
+
+
+ {item.name}
+
+
+ {/* TODO */}
+ {/* {isHovering && (
+
Start chat with {contentType.slice(0, -1)} }
+ trigger={
+
+ }
+ />
+ )} */}
+
+
+ )
+}
diff --git a/components/sidebar/items/all/sidebar-update-item.tsx b/components/sidebar/items/all/sidebar-update-item.tsx
new file mode 100644
index 0000000000..0cc6e11e57
--- /dev/null
+++ b/components/sidebar/items/all/sidebar-update-item.tsx
@@ -0,0 +1,382 @@
+import { Button } from "@/components/ui/button"
+import {
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger
+} from "@/components/ui/dialog"
+import { Label } from "@/components/ui/label"
+import { AssignWorkspaces } from "@/components/workspace/assign-workspaces"
+import { ChatbotUIContext } from "@/context/context"
+import {
+ createAssistantWorkspaces,
+ deleteAssistantWorkspace,
+ getAssistantWorkspacesByAssistantId,
+ updateAssistant
+} from "@/db/assistants"
+import { updateChat } from "@/db/chats"
+import {
+ createCollectionWorkspaces,
+ deleteCollectionWorkspace,
+ getCollectionWorkspacesByCollectionId,
+ updateCollection
+} from "@/db/collections"
+import {
+ createFileWorkspaces,
+ deleteFileWorkspace,
+ getFileWorkspacesByFileId,
+ updateFile
+} from "@/db/files"
+import {
+ createPresetWorkspaces,
+ deletePresetWorkspace,
+ getPresetWorkspacesByPresetId,
+ updatePreset
+} from "@/db/presets"
+import {
+ createPromptWorkspaces,
+ deletePromptWorkspace,
+ getPromptWorkspacesByPromptId,
+ updatePrompt
+} from "@/db/prompts"
+import { uploadAssistantImage } from "@/db/storage/assistant-images"
+import { Tables, TablesUpdate } from "@/supabase/types"
+import { ContentType, DataItemType } from "@/types"
+import { FC, useContext, useEffect, useRef, useState } from "react"
+import { SidebarDeleteItem } from "./sidebar-delete-item"
+
+interface SidebarUpdateItemProps {
+ item: DataItemType
+ contentType: ContentType
+ children: React.ReactNode
+ renderInputs: () => JSX.Element
+ updateState: any
+}
+
+export const SidebarUpdateItem: FC = ({
+ item,
+ contentType,
+ children,
+ renderInputs,
+ updateState
+}) => {
+ const {
+ workspaces,
+ selectedWorkspace,
+ setChats,
+ setPresets,
+ setPrompts,
+ setFiles,
+ setCollections,
+ setAssistants
+ } = useContext(ChatbotUIContext)
+
+ const buttonRef = useRef(null)
+
+ const [isOpen, setIsOpen] = useState(false)
+ const [startingWorkspaces, setStartingWorkspaces] = useState<
+ Tables<"workspaces">[]
+ >([])
+ const [selectedWorkspaces, setSelectedWorkspaces] = useState<
+ Tables<"workspaces">[]
+ >([])
+
+ useEffect(() => {
+ if (isOpen) {
+ const fetchData = async () => {
+ const workspaces = await fetchSelectedWorkspaces()
+ setStartingWorkspaces(workspaces)
+ setSelectedWorkspaces(workspaces)
+ }
+
+ fetchData()
+ }
+ }, [isOpen])
+
+ const fetchWorkpaceFunctions = {
+ chats: null,
+ presets: async (presetId: string) => {
+ const item = await getPresetWorkspacesByPresetId(presetId)
+ return item.workspaces
+ },
+ prompts: async (promptId: string) => {
+ const item = await getPromptWorkspacesByPromptId(promptId)
+ return item.workspaces
+ },
+ files: async (fileId: string) => {
+ const item = await getFileWorkspacesByFileId(fileId)
+ return item.workspaces
+ },
+ collections: async (collectionId: string) => {
+ const item = await getCollectionWorkspacesByCollectionId(collectionId)
+ return item.workspaces
+ },
+ assistants: async (assistantId: string) => {
+ const item = await getAssistantWorkspacesByAssistantId(assistantId)
+ return item.workspaces
+ }
+ }
+
+ const fetchSelectedWorkspaces = async () => {
+ const fetchFunction = fetchWorkpaceFunctions[contentType]
+
+ if (!fetchFunction) return []
+
+ const workspaces = await fetchFunction(item.id)
+
+ return workspaces
+ }
+
+ const handleWorkspaceUpdates = async (
+ startingWorkspaces: Tables<"workspaces">[],
+ selectedWorkspaces: Tables<"workspaces">[],
+ itemId: string,
+ deleteWorkspaceFn: (
+ itemId: string,
+ workspaceId: string
+ ) => Promise,
+ createWorkspaceFn: (
+ workspaces: { user_id: string; item_id: string; workspace_id: string }[]
+ ) => Promise,
+ itemIdKey: string
+ ) => {
+ if (!selectedWorkspace) return
+
+ const deleteList = startingWorkspaces.filter(
+ startingWorkspace =>
+ !selectedWorkspaces.some(
+ selectedWorkspace => selectedWorkspace.id === startingWorkspace.id
+ )
+ )
+
+ for (const workspace of deleteList) {
+ await deleteWorkspaceFn(itemId, workspace.id)
+ }
+
+ if (deleteList.map(w => w.id).includes(selectedWorkspace.id)) {
+ const setStateFunction = stateUpdateFunctions[contentType]
+
+ if (setStateFunction) {
+ setStateFunction((prevItems: any) =>
+ prevItems.filter((prevItem: any) => prevItem.id !== item.id)
+ )
+ }
+ }
+
+ const createList = selectedWorkspaces.filter(
+ selectedWorkspace =>
+ !startingWorkspaces.some(
+ startingWorkspace => startingWorkspace.id === selectedWorkspace.id
+ )
+ )
+
+ await createWorkspaceFn(
+ createList.map(workspace => {
+ return {
+ user_id: workspace.user_id,
+ [itemIdKey]: itemId,
+ workspace_id: workspace.id
+ } as any
+ })
+ )
+ }
+
+ const updateFunctions = {
+ chats: updateChat,
+ presets: async (presetId: string, updateState: TablesUpdate<"presets">) => {
+ const updatedPreset = await updatePreset(presetId, updateState)
+
+ await handleWorkspaceUpdates(
+ startingWorkspaces,
+ selectedWorkspaces,
+ presetId,
+ deletePresetWorkspace,
+ createPresetWorkspaces as any,
+ "preset_id"
+ )
+
+ return updatedPreset
+ },
+ prompts: async (promptId: string, updateState: TablesUpdate<"prompts">) => {
+ const updatedPrompt = await updatePrompt(promptId, updateState)
+
+ await handleWorkspaceUpdates(
+ startingWorkspaces,
+ selectedWorkspaces,
+ promptId,
+ deletePromptWorkspace,
+ createPromptWorkspaces as any,
+ "prompt_id"
+ )
+
+ return updatedPrompt
+ },
+ files: async (fileId: string, updateState: TablesUpdate<"files">) => {
+ const updatedFile = await updateFile(fileId, updateState)
+
+ await handleWorkspaceUpdates(
+ startingWorkspaces,
+ selectedWorkspaces,
+ fileId,
+ deleteFileWorkspace,
+ createFileWorkspaces as any,
+ "file_id"
+ )
+
+ return updatedFile
+ },
+ collections: async (
+ collectionId: string,
+ updateState: {
+ image: File
+ collectionFilesToAdd: string[]
+ collectionFilesToRemove: string[]
+ } & TablesUpdate<"assistants">
+ ) => {
+ const { image, collectionFilesToAdd, collectionFilesToRemove, ...rest } =
+ updateState
+
+ // add files
+ // remove files
+
+ // TODO deletes image
+ const updatedCollection = await updateCollection(collectionId, rest)
+
+ await handleWorkspaceUpdates(
+ startingWorkspaces,
+ selectedWorkspaces,
+ collectionId,
+ deleteCollectionWorkspace,
+ createCollectionWorkspaces as any,
+ "collection_id"
+ )
+
+ return updatedCollection
+ },
+ assistants: async (
+ assistantId: string,
+ updateState: {
+ image: File
+ } & TablesUpdate<"assistants">
+ ) => {
+ const { image, ...rest } = updateState
+
+ const updatedAssistant = await updateAssistant(assistantId, rest)
+
+ // TODO deletes
+ if (image) {
+ await uploadAssistantImage(updatedAssistant, image)
+ }
+
+ await handleWorkspaceUpdates(
+ startingWorkspaces,
+ selectedWorkspaces,
+ assistantId,
+ deleteAssistantWorkspace,
+ createAssistantWorkspaces as any,
+ "assistant_id"
+ )
+
+ return updatedAssistant
+ }
+ }
+
+ const stateUpdateFunctions = {
+ chats: setChats,
+ presets: setPresets,
+ prompts: setPrompts,
+ files: setFiles,
+ collections: setCollections,
+ assistants: setAssistants
+ }
+
+ const handleUpdate = async () => {
+ const updateFunction = updateFunctions[contentType]
+ const setStateFunction = stateUpdateFunctions[contentType]
+
+ if (!updateFunction || !setStateFunction) return
+
+ const updatedItem = await updateFunction(item.id, updateState)
+
+ setStateFunction((prevItems: any) =>
+ prevItems.map((prevItem: any) =>
+ prevItem.id === item.id ? updatedItem : prevItem
+ )
+ )
+
+ setIsOpen(false)
+ }
+
+ const handleSelectWorkspace = (workspace: Tables<"workspaces">) => {
+ setSelectedWorkspaces(prevState => {
+ const isWorkspaceAlreadySelected = prevState.find(
+ selectedWorkspace => selectedWorkspace.id === workspace.id
+ )
+
+ if (isWorkspaceAlreadySelected) {
+ return prevState.filter(
+ selectedWorkspace => selectedWorkspace.id !== workspace.id
+ )
+ } else {
+ return [...prevState, workspace]
+ }
+ })
+ }
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault()
+ buttonRef.current?.click()
+ }
+ }
+
+ return (
+
+ )
+}
diff --git a/components/sidebar/items/assistants/assistant-item.tsx b/components/sidebar/items/assistants/assistant-item.tsx
new file mode 100644
index 0000000000..2587958dd2
--- /dev/null
+++ b/components/sidebar/items/assistants/assistant-item.tsx
@@ -0,0 +1,137 @@
+import { ChatSettingsForm } from "@/components/ui/chat-settings-form"
+import ImagePicker from "@/components/ui/image-picker"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { ChatbotUIContext } from "@/context/context"
+import { ASSISTANT_DESCRIPTION_MAX, ASSISTANT_NAME_MAX } from "@/db/limits"
+import { Tables } from "@/supabase/types"
+import { IconRobotFace } from "@tabler/icons-react"
+import Image from "next/image"
+import { FC, useContext, useEffect, useState } from "react"
+import profile from "react-syntax-highlighter/dist/esm/languages/hljs/profile"
+import { SidebarItem } from "../all/sidebar-display-item"
+
+interface AssistantItemProps {
+ assistant: Tables<"assistants">
+}
+
+export const AssistantItem: FC = ({ assistant }) => {
+ const { selectedWorkspace, assistantImages } = useContext(ChatbotUIContext)
+
+ const [name, setName] = useState(assistant.name)
+ const [description, setDescription] = useState(assistant.description)
+ const [assistantChatSettings, setAssistantChatSettings] = useState({
+ model: assistant.model,
+ prompt: assistant.prompt,
+ temperature: assistant.temperature,
+ contextLength: assistant.context_length,
+ includeProfileContext: assistant.include_profile_context,
+ includeWorkspaceInstructions: assistant.include_workspace_instructions
+ })
+ const [selectedImage, setSelectedImage] = useState(null)
+ const [imageLink, setImageLink] = useState("")
+
+ useEffect(() => {
+ const assistantImage =
+ assistantImages.find(image => image.path === assistant.image_path)
+ ?.base64 || ""
+
+ setImageLink(assistantImage)
+ }, [assistantImages])
+
+ if (!profile) return null
+ if (!selectedWorkspace) return null
+
+ return (
+
+ ) : (
+
+ )
+ }
+ updateState={{
+ image: selectedImage,
+ user_id: assistant.user_id,
+ name,
+ description,
+ include_profile_context: assistantChatSettings.includeProfileContext,
+ include_workspace_instructions:
+ assistantChatSettings.includeWorkspaceInstructions,
+ context_length: assistantChatSettings.contextLength,
+ model: assistantChatSettings.model,
+ image_path: assistant.image_path,
+ prompt: assistantChatSettings.prompt,
+ temperature: assistantChatSettings.temperature
+ }}
+ renderInputs={() => (
+ <>
+
+
+
+
+
+
+
+
+
+ setName(e.target.value)}
+ maxLength={ASSISTANT_NAME_MAX}
+ />
+
+
+
+
+
+ setDescription(e.target.value)}
+ maxLength={ASSISTANT_DESCRIPTION_MAX}
+ />
+
+
+
+
+
+
Coming soon...
+
+
+
+
+
+
Coming soon...
+
+
+
+ >
+ )}
+ />
+ )
+}
diff --git a/components/sidebar/items/assistants/create-assistant.tsx b/components/sidebar/items/assistants/create-assistant.tsx
new file mode 100644
index 0000000000..96473b579b
--- /dev/null
+++ b/components/sidebar/items/assistants/create-assistant.tsx
@@ -0,0 +1,133 @@
+import { SidebarCreateItem } from "@/components/sidebar/items/all/sidebar-create-item"
+import { ChatSettingsForm } from "@/components/ui/chat-settings-form"
+import ImagePicker from "@/components/ui/image-picker"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { ChatbotUIContext } from "@/context/context"
+import { ASSISTANT_DESCRIPTION_MAX, ASSISTANT_NAME_MAX } from "@/db/limits"
+import { TablesInsert } from "@/supabase/types"
+import { FC, useContext, useEffect, useState } from "react"
+
+interface CreateAssistantProps {
+ isOpen: boolean
+ onOpenChange: (isOpen: boolean) => void
+}
+
+export const CreateAssistant: FC = ({
+ isOpen,
+ onOpenChange
+}) => {
+ const { profile, selectedWorkspace } = useContext(ChatbotUIContext)
+
+ const [name, setName] = useState("")
+ const [description, setDescription] = useState("")
+ const [assistantChatSettings, setAssistantChatSettings] = useState({
+ model: selectedWorkspace?.default_model,
+ prompt: selectedWorkspace?.default_prompt,
+ temperature: selectedWorkspace?.default_temperature,
+ contextLength: selectedWorkspace?.default_context_length,
+ includeProfileContext: selectedWorkspace?.include_profile_context,
+ includeWorkspaceInstructions: false,
+ embeddingsProvider: selectedWorkspace?.embeddings_provider
+ })
+ const [selectedImage, setSelectedImage] = useState(null)
+ const [imageLink, setImageLink] = useState("")
+
+ useEffect(() => {
+ setAssistantChatSettings(prevSettings => {
+ const previousPrompt = prevSettings.prompt || ""
+ const previousPromptParts = previousPrompt.split(". ")
+
+ previousPromptParts[0] = name ? `You are ${name}` : ""
+
+ return {
+ ...prevSettings,
+ prompt: previousPromptParts.join(". ")
+ }
+ })
+ }, [name])
+
+ if (!profile) return null
+ if (!selectedWorkspace) return null
+
+ return (
+
+ }
+ isOpen={isOpen}
+ renderInputs={() => (
+ <>
+
+
+
+
+
+
+
+
+
+ setName(e.target.value)}
+ maxLength={ASSISTANT_NAME_MAX}
+ />
+
+
+
+
+
+ setDescription(e.target.value)}
+ maxLength={ASSISTANT_DESCRIPTION_MAX}
+ />
+
+
+
+
+
+
Coming soon...
+
+
+
+
+
+
Coming soon...
+
+
+
+ >
+ )}
+ onOpenChange={onOpenChange}
+ />
+ )
+}
diff --git a/components/sidebar/items/chat/chat-item.tsx b/components/sidebar/items/chat/chat-item.tsx
new file mode 100644
index 0000000000..c72243f402
--- /dev/null
+++ b/components/sidebar/items/chat/chat-item.tsx
@@ -0,0 +1,96 @@
+import { ModelIcon } from "@/components/models/model-icon"
+import { ChatbotUIContext } from "@/context/context"
+import { LLM_LIST } from "@/lib/models/llm/llm-list"
+import { cn } from "@/lib/utils"
+import { Tables } from "@/supabase/types"
+import { LLM } from "@/types"
+import { IconRobotFace } from "@tabler/icons-react"
+import Image from "next/image"
+import { useParams, useRouter } from "next/navigation"
+import { FC, useContext, useRef } from "react"
+import { DeleteChat } from "./delete-chat"
+import { UpdateChat } from "./update-chat"
+
+interface ChatItemProps {
+ chat: Tables<"chats">
+}
+
+export const ChatItem: FC = ({ chat }) => {
+ const { selectedChat, availableLocalModels, assistantImages } =
+ useContext(ChatbotUIContext)
+
+ const router = useRouter()
+ const params = useParams()
+ const isActive = params.chatid === chat.id || selectedChat?.id === chat.id
+
+ const itemRef = useRef(null)
+
+ const handleClick = () => {
+ router.push(`/chat/${chat.id}`)
+ }
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") {
+ e.stopPropagation()
+ itemRef.current?.click()
+ }
+ }
+
+ const MODEL_DATA = [...LLM_LIST, ...availableLocalModels].find(
+ llm => llm.modelId === chat.model
+ ) as LLM
+
+ const assistantImage = assistantImages.find(
+ image => image.assistantId === chat.assistant_id
+ )?.base64
+
+ return (
+
+ {chat.assistant_id ? (
+ assistantImage ? (
+
+ ) : (
+
+ )
+ ) : (
+
+ )}
+
+
+ {chat.name}
+
+
+ {isActive && (
+
{
+ e.stopPropagation()
+ e.preventDefault()
+ }}
+ className="ml-2 flex space-x-2"
+ >
+
+
+
+
+ )}
+
+ )
+}
diff --git a/components/sidebar/items/chat/delete-chat.tsx b/components/sidebar/items/chat/delete-chat.tsx
new file mode 100644
index 0000000000..868e38a34d
--- /dev/null
+++ b/components/sidebar/items/chat/delete-chat.tsx
@@ -0,0 +1,80 @@
+import { useChatHandler } from "@/components/chat/chat-hooks/use-chat-handler"
+import { Button } from "@/components/ui/button"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger
+} from "@/components/ui/dialog"
+import { ChatbotUIContext } from "@/context/context"
+import { deleteChat } from "@/db/chats"
+import useHotkey from "@/lib/hooks/use-hotkey"
+import { Tables } from "@/supabase/types"
+import { IconTrash } from "@tabler/icons-react"
+import { FC, useContext, useRef, useState } from "react"
+
+interface DeleteChatProps {
+ chat: Tables<"chats">
+}
+
+export const DeleteChat: FC = ({ chat }) => {
+ useHotkey("Backspace", () => setShowChatDialog(true))
+
+ const { setChats } = useContext(ChatbotUIContext)
+ const { handleNewChat } = useChatHandler()
+
+ const buttonRef = useRef(null)
+
+ const [showChatDialog, setShowChatDialog] = useState(false)
+
+ const handleDeleteChat = async () => {
+ await deleteChat(chat.id)
+
+ setChats(prevState => prevState.filter(c => c.id !== chat.id))
+
+ setShowChatDialog(false)
+
+ handleNewChat()
+ }
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") {
+ buttonRef.current?.click()
+ }
+ }
+
+ return (
+
+ )
+}
diff --git a/components/sidebar/items/chat/update-chat.tsx b/components/sidebar/items/chat/update-chat.tsx
new file mode 100644
index 0000000000..e5893009a2
--- /dev/null
+++ b/components/sidebar/items/chat/update-chat.tsx
@@ -0,0 +1,76 @@
+import { Button } from "@/components/ui/button"
+import {
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger
+} from "@/components/ui/dialog"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { ChatbotUIContext } from "@/context/context"
+import { updateChat } from "@/db/chats"
+import { Tables } from "@/supabase/types"
+import { IconEdit } from "@tabler/icons-react"
+import { FC, useContext, useRef, useState } from "react"
+
+interface UpdateChatProps {
+ chat: Tables<"chats">
+}
+
+export const UpdateChat: FC = ({ chat }) => {
+ const { setChats } = useContext(ChatbotUIContext)
+
+ const buttonRef = useRef(null)
+
+ const [showChatDialog, setShowChatDialog] = useState(false)
+ const [name, setName] = useState(chat.name)
+
+ const handleUpdateChat = async (e: React.MouseEvent) => {
+ const updatedChat = await updateChat(chat.id, {
+ name
+ })
+ setChats(prevState =>
+ prevState.map(c => (c.id === chat.id ? updatedChat : c))
+ )
+
+ setShowChatDialog(false)
+ }
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") {
+ buttonRef.current?.click()
+ }
+ }
+
+ return (
+
+ )
+}
diff --git a/components/sidebar/items/collections/collection-file-picker.tsx b/components/sidebar/items/collections/collection-file-picker.tsx
new file mode 100644
index 0000000000..7ed97f267b
--- /dev/null
+++ b/components/sidebar/items/collections/collection-file-picker.tsx
@@ -0,0 +1,155 @@
+import { Button } from "@/components/ui/button"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuTrigger
+} from "@/components/ui/dropdown-menu"
+import { FileIcon } from "@/components/ui/file-icon"
+import { Input } from "@/components/ui/input"
+import { ChatbotUIContext } from "@/context/context"
+import { CollectionFile } from "@/types"
+import { IconChevronDown, IconCircleCheckFilled } from "@tabler/icons-react"
+import { FC, useContext, useEffect, useRef, useState } from "react"
+
+interface CollectionFilePickerProps {
+ selectedCollectionFiles: CollectionFile[]
+ onCollectionFileSelect: (file: CollectionFile) => void
+}
+
+export const CollectionFilePicker: FC = ({
+ selectedCollectionFiles,
+ onCollectionFileSelect
+}) => {
+ const { files } = useContext(ChatbotUIContext)
+
+ const inputRef = useRef(null)
+ const triggerRef = useRef(null)
+
+ const [isOpen, setIsOpen] = useState(false)
+ const [search, setSearch] = useState("")
+
+ useEffect(() => {
+ if (isOpen) {
+ setTimeout(() => {
+ inputRef.current?.focus()
+ }, 100) // FIX: hacky
+ }
+ }, [isOpen])
+
+ const handleFileSelect = (file: CollectionFile) => {
+ onCollectionFileSelect(file)
+ }
+
+ if (!files) return null
+
+ return (
+ {
+ setIsOpen(isOpen)
+ setSearch("")
+ }}
+ >
+
+
+
+
+
+ setSearch(e.target.value)}
+ onKeyDown={e => e.stopPropagation()}
+ />
+
+ {selectedCollectionFiles
+ .filter(file =>
+ file.name.toLowerCase().includes(search.toLowerCase())
+ )
+ .map(file => (
+ selectedCollectionFile.id === file.id
+ )}
+ onSelect={handleFileSelect}
+ />
+ ))}
+
+ {files
+ .filter(
+ file =>
+ !selectedCollectionFiles.some(
+ selectedCollectionFile => selectedCollectionFile.id === file.id
+ ) && file.name.toLowerCase().includes(search.toLowerCase())
+ )
+ .map(file => (
+ selectedCollectionFile.id === file.id
+ )}
+ onSelect={handleFileSelect}
+ />
+ ))}
+
+
+ )
+}
+
+interface CollectionFileItemProps {
+ file: CollectionFile
+ selected: boolean
+ onSelect: (file: CollectionFile) => void
+}
+
+const CollectionFileItem: FC = ({
+ file,
+ selected,
+ onSelect
+}) => {
+ const handleSelect = () => {
+ onSelect(file)
+ }
+
+ return (
+
+
+
+ {selected && (
+
+ )}
+
+ )
+}
diff --git a/components/sidebar/items/collections/collection-item.tsx b/components/sidebar/items/collections/collection-item.tsx
new file mode 100644
index 0000000000..562268280c
--- /dev/null
+++ b/components/sidebar/items/collections/collection-item.tsx
@@ -0,0 +1,113 @@
+import ImagePicker from "@/components/ui/image-picker"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { getCollectionFilesByCollectionId } from "@/db/collection-files"
+import { COLLECTION_DESCRIPTION_MAX, COLLECTION_NAME_MAX } from "@/db/limits"
+import { Tables } from "@/supabase/types"
+import { CollectionFile } from "@/types"
+import { IconBooks } from "@tabler/icons-react"
+import { FC, useEffect, useState } from "react"
+import { SidebarItem } from "../all/sidebar-display-item"
+import { CollectionFilePicker } from "./collection-file-picker"
+
+interface CollectionItemProps {
+ collection: Tables<"collections">
+}
+
+export const CollectionItem: FC = ({ collection }) => {
+ const [name, setName] = useState(collection.name)
+ const [description, setDescription] = useState(collection.description)
+ const [selectedCollectionFiles, setSelectedCollectionFiles] = useState<
+ CollectionFile[]
+ >([])
+ const [selectedImage, setSelectedImage] = useState(null)
+ const [imageLink, setImageLink] = useState("")
+
+ useEffect(() => {
+ const fetchData = async () => {
+ const collectionFiles = await getCollectionFilesByCollectionId(
+ collection.id
+ )
+ console.log("collectionFiles", collectionFiles)
+ const files = collectionFiles.files
+ setSelectedCollectionFiles(files)
+ }
+
+ fetchData()
+ }, [])
+
+ const handleFileSelect = (file: CollectionFile) => {
+ setSelectedCollectionFiles(prevState => {
+ const isFileAlreadySelected = prevState.find(
+ selectedFile => selectedFile.id === file.id
+ )
+ if (isFileAlreadySelected) {
+ return prevState.filter(selectedFile => selectedFile.id !== file.id)
+ } else {
+ return [...prevState, file]
+ }
+ })
+ }
+
+ return (
+ }
+ updateState={{
+ image: selectedImage,
+ collectionFilesToAdd: [],
+ collectionFilesToRemove: [],
+ name,
+ description
+ }}
+ renderInputs={() => (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ setName(e.target.value)}
+ maxLength={COLLECTION_NAME_MAX}
+ />
+
+
+
+
+
+ setDescription(e.target.value)}
+ maxLength={COLLECTION_DESCRIPTION_MAX}
+ />
+
+ >
+ )}
+ />
+ )
+}
diff --git a/components/sidebar/items/collections/create-collection.tsx b/components/sidebar/items/collections/create-collection.tsx
new file mode 100644
index 0000000000..a465966b85
--- /dev/null
+++ b/components/sidebar/items/collections/create-collection.tsx
@@ -0,0 +1,115 @@
+import { SidebarCreateItem } from "@/components/sidebar/items/all/sidebar-create-item"
+import ImagePicker from "@/components/ui/image-picker"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { ChatbotUIContext } from "@/context/context"
+import { COLLECTION_DESCRIPTION_MAX, COLLECTION_NAME_MAX } from "@/db/limits"
+import { TablesInsert } from "@/supabase/types"
+import { CollectionFile } from "@/types"
+import { FC, useContext, useState } from "react"
+import { CollectionFilePicker } from "./collection-file-picker"
+
+interface CreateCollectionProps {
+ isOpen: boolean
+ onOpenChange: (isOpen: boolean) => void
+}
+
+export const CreateCollection: FC = ({
+ isOpen,
+ onOpenChange
+}) => {
+ const { profile, selectedWorkspace } = useContext(ChatbotUIContext)
+
+ const [name, setName] = useState("")
+ const [description, setDescription] = useState("")
+ const [selectedCollectionFiles, setSelectedCollectionFiles] = useState<
+ CollectionFile[]
+ >([])
+ const [selectedImage, setSelectedImage] = useState(null)
+ const [imageLink, setImageLink] = useState("")
+
+ const handleFileSelect = (file: CollectionFile) => {
+ setSelectedCollectionFiles(prevState => {
+ const isFileAlreadySelected = prevState.find(
+ selectedFile => selectedFile.id === file.id
+ )
+ if (isFileAlreadySelected) {
+ return prevState.filter(selectedFile => selectedFile.id !== file.id)
+ } else {
+ return [...prevState, file]
+ }
+ })
+ }
+
+ if (!profile) return null
+ if (!selectedWorkspace) return null
+
+ return (
+ ({
+ user_id: profile.user_id,
+ collection_id: "",
+ file_id: file.id
+ })),
+ user_id: profile.user_id,
+ image_path: "",
+ name,
+ description
+ } as TablesInsert<"collections">
+ }
+ isOpen={isOpen}
+ onOpenChange={onOpenChange}
+ renderInputs={() => (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ setName(e.target.value)}
+ maxLength={COLLECTION_NAME_MAX}
+ />
+
+
+
+
+
+ setDescription(e.target.value)}
+ maxLength={COLLECTION_DESCRIPTION_MAX}
+ />
+
+ >
+ )}
+ />
+ )
+}
diff --git a/components/sidebar/items/files/create-file.tsx b/components/sidebar/items/files/create-file.tsx
new file mode 100644
index 0000000000..c68d5348ae
--- /dev/null
+++ b/components/sidebar/items/files/create-file.tsx
@@ -0,0 +1,92 @@
+import { SidebarCreateItem } from "@/components/sidebar/items/all/sidebar-create-item"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { ChatbotUIContext } from "@/context/context"
+import { FILE_DESCRIPTION_MAX, FILE_NAME_MAX } from "@/db/limits"
+import { TablesInsert } from "@/supabase/types"
+import { FC, useContext, useState } from "react"
+
+interface CreateFileProps {
+ isOpen: boolean
+ onOpenChange: (isOpen: boolean) => void
+}
+
+export const CreateFile: FC = ({ isOpen, onOpenChange }) => {
+ const { profile, selectedWorkspace } = useContext(ChatbotUIContext)
+
+ const [name, setName] = useState("")
+ const [description, setDescription] = useState("")
+ const [selectedFile, setSelectedFile] = useState(null)
+
+ const ALLOWED_FILES = ["pdf"]
+
+ const handleSelectedFile = async (e: React.ChangeEvent) => {
+ if (!e.target.files) return
+
+ const file = e.target.files[0]
+
+ if (!file) return
+
+ setSelectedFile(file)
+ const fileNameWithoutExtension = file.name.split(".").slice(0, -1).join(".")
+ setName(fileNameWithoutExtension)
+ }
+
+ if (!profile) return null
+ if (!selectedWorkspace) return null
+
+ return (
+
+ }
+ isOpen={isOpen}
+ onOpenChange={onOpenChange}
+ renderInputs={() => (
+ <>
+
+
+
+ `.${file}`).join(",")}
+ />
+
+
+
+
+
+ setName(e.target.value)}
+ maxLength={FILE_NAME_MAX}
+ />
+
+
+
+
+
+ setDescription(e.target.value)}
+ maxLength={FILE_DESCRIPTION_MAX}
+ />
+
+ >
+ )}
+ />
+ )
+}
diff --git a/components/sidebar/items/files/file-item.tsx b/components/sidebar/items/files/file-item.tsx
new file mode 100644
index 0000000000..c1b18a95f0
--- /dev/null
+++ b/components/sidebar/items/files/file-item.tsx
@@ -0,0 +1,93 @@
+import { FileIcon } from "@/components/ui/file-icon"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { FILE_DESCRIPTION_MAX, FILE_NAME_MAX } from "@/db/limits"
+import { getFileFromStorage } from "@/db/storage/files"
+import { Tables } from "@/supabase/types"
+import { FC, useState } from "react"
+import { SidebarItem } from "../all/sidebar-display-item"
+
+interface FileItemProps {
+ file: Tables<"files">
+}
+
+export const FileItem: FC = ({ file }) => {
+ const [name, setName] = useState(file.name)
+ const [description, setDescription] = useState(file.description)
+
+ const getLinkAndView = async () => {
+ const link = await getFileFromStorage(file.file_path)
+ window.open(link, "_blank")
+ }
+
+ return (
+ }
+ updateState={{ name }}
+ renderInputs={() => (
+ <>
+
+ View {file.name}
+
+
+
+
{file.type}
+
+
{formatFileSize(file.size)}
+
+
{file.tokens.toLocaleString()} tokens
+
+
+
+
+
+ setName(e.target.value)}
+ maxLength={FILE_NAME_MAX}
+ />
+
+
+
+
+
+ setDescription(e.target.value)}
+ maxLength={FILE_DESCRIPTION_MAX}
+ />
+
+ >
+ )}
+ />
+ )
+}
+
+export const formatFileSize = (sizeInBytes: number): string => {
+ let size = sizeInBytes
+ let unit = "bytes"
+
+ if (size >= 1024) {
+ size /= 1024
+ unit = "KB"
+ }
+
+ if (size >= 1024) {
+ size /= 1024
+ unit = "MB"
+ }
+
+ if (size >= 1024) {
+ size /= 1024
+ unit = "GB"
+ }
+
+ return `${size.toFixed(2)} ${unit}`
+}
diff --git a/components/sidebar/items/folders/delete-folder.tsx b/components/sidebar/items/folders/delete-folder.tsx
new file mode 100644
index 0000000000..b2d17d1e17
--- /dev/null
+++ b/components/sidebar/items/folders/delete-folder.tsx
@@ -0,0 +1,79 @@
+import { Button } from "@/components/ui/button"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger
+} from "@/components/ui/dialog"
+import { ChatbotUIContext } from "@/context/context"
+import { deleteFolder } from "@/db/folders"
+import { Tables } from "@/supabase/types"
+import { IconTrash } from "@tabler/icons-react"
+import { FC, useContext, useRef, useState } from "react"
+
+interface DeleteFolderProps {
+ folder: Tables<"folders">
+}
+
+export const DeleteFolder: FC = ({ folder }) => {
+ const { setFolders, setChats } = useContext(ChatbotUIContext)
+
+ const buttonRef = useRef(null)
+
+ const [showFolderDialog, setShowFolderDialog] = useState(false)
+
+ const handleDeleteFolder = async () => {
+ await deleteFolder(folder.id)
+
+ setFolders(prevState => prevState.filter(c => c.id !== folder.id))
+
+ setShowFolderDialog(false)
+
+ setChats(prevState =>
+ prevState.map(c =>
+ c.folder_id === folder.id ? { ...c, folder_id: null } : c
+ )
+ )
+ }
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") {
+ buttonRef.current?.click()
+ }
+ }
+
+ return (
+
+ )
+}
diff --git a/components/sidebar/items/folders/folder-item.tsx b/components/sidebar/items/folders/folder-item.tsx
new file mode 100644
index 0000000000..3100850663
--- /dev/null
+++ b/components/sidebar/items/folders/folder-item.tsx
@@ -0,0 +1,111 @@
+import { cn } from "@/lib/utils"
+import { Tables } from "@/supabase/types"
+import { IconChevronDown, IconChevronRight } from "@tabler/icons-react"
+import { FC, useRef, useState } from "react"
+import { DeleteFolder } from "./delete-folder"
+import { UpdateFolder } from "./update-folder"
+
+interface FolderProps {
+ folder: Tables<"folders">
+ children: React.ReactNode
+ onUpdateFolder: (itemId: string, folderId: string | null) => void
+}
+
+export const Folder: FC = ({
+ folder,
+ children,
+ onUpdateFolder
+}) => {
+ const itemRef = useRef(null)
+
+ const [isDragOver, setIsDragOver] = useState(false)
+ const [isExpanded, setIsExpanded] = useState(false)
+ const [isHovering, setIsHovering] = useState(false)
+
+ const handleDragEnter = (e: React.DragEvent) => {
+ e.preventDefault()
+ setIsDragOver(true)
+ }
+
+ const handleDragLeave = (e: React.DragEvent) => {
+ e.preventDefault()
+ setIsDragOver(false)
+ }
+
+ const handleDragOver = (e: React.DragEvent) => {
+ e.preventDefault()
+ setIsDragOver(true)
+ }
+
+ const handleDrop = (e: React.DragEvent) => {
+ e.preventDefault()
+
+ setIsDragOver(false)
+ const itemId = e.dataTransfer.getData("text/plain")
+ onUpdateFolder(itemId, folder.id)
+ }
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") {
+ e.stopPropagation()
+ itemRef.current?.click()
+ }
+ }
+
+ const handleClick = (e: React.MouseEvent) => {
+ setIsExpanded(!isExpanded)
+ }
+
+ return (
+ setIsHovering(true)}
+ onMouseLeave={() => setIsHovering(false)}
+ >
+
+
+
+ {isExpanded ? (
+
+ ) : (
+
+ )}
+
+
{folder.name}
+
+
+ {isHovering && (
+
{
+ e.stopPropagation()
+ e.preventDefault()
+ }}
+ className="ml-2 flex space-x-2"
+ >
+
+
+
+
+ )}
+
+
+
+ {isExpanded && (
+
{children}
+ )}
+
+ )
+}
diff --git a/components/sidebar/items/folders/update-folder.tsx b/components/sidebar/items/folders/update-folder.tsx
new file mode 100644
index 0000000000..d98833e2f7
--- /dev/null
+++ b/components/sidebar/items/folders/update-folder.tsx
@@ -0,0 +1,76 @@
+import { Button } from "@/components/ui/button"
+import {
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger
+} from "@/components/ui/dialog"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { ChatbotUIContext } from "@/context/context"
+import { updateFolder } from "@/db/folders"
+import { Tables } from "@/supabase/types"
+import { IconEdit } from "@tabler/icons-react"
+import { FC, useContext, useRef, useState } from "react"
+
+interface UpdateFolderProps {
+ folder: Tables<"folders">
+}
+
+export const UpdateFolder: FC = ({ folder }) => {
+ const { setFolders } = useContext(ChatbotUIContext)
+
+ const buttonRef = useRef(null)
+
+ const [showFolderDialog, setShowFolderDialog] = useState(false)
+ const [name, setName] = useState(folder.name)
+
+ const handleUpdateFolder = async (e: React.MouseEvent) => {
+ const updatedFolder = await updateFolder(folder.id, {
+ name
+ })
+ setFolders(prevState =>
+ prevState.map(c => (c.id === folder.id ? updatedFolder : c))
+ )
+
+ setShowFolderDialog(false)
+ }
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") {
+ buttonRef.current?.click()
+ }
+ }
+
+ return (
+
+ )
+}
diff --git a/components/sidebar/items/presets/create-preset.tsx b/components/sidebar/items/presets/create-preset.tsx
new file mode 100644
index 0000000000..33bf436b27
--- /dev/null
+++ b/components/sidebar/items/presets/create-preset.tsx
@@ -0,0 +1,88 @@
+import { SidebarCreateItem } from "@/components/sidebar/items/all/sidebar-create-item"
+import { ChatSettingsForm } from "@/components/ui/chat-settings-form"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { ChatbotUIContext } from "@/context/context"
+import { PRESET_DESCRIPTION_MAX, PRESET_NAME_MAX } from "@/db/limits"
+import { TablesInsert } from "@/supabase/types"
+import { FC, useContext, useState } from "react"
+
+interface CreatePresetProps {
+ isOpen: boolean
+ onOpenChange: (isOpen: boolean) => void
+}
+
+export const CreatePreset: FC = ({
+ isOpen,
+ onOpenChange
+}) => {
+ const { profile, selectedWorkspace } = useContext(ChatbotUIContext)
+
+ const [name, setName] = useState("")
+ const [description, setDescription] = useState("")
+ const [presetChatSettings, setPresetChatSettings] = useState({
+ model: selectedWorkspace?.default_model,
+ prompt: selectedWorkspace?.default_prompt,
+ temperature: selectedWorkspace?.default_temperature,
+ contextLength: selectedWorkspace?.default_context_length,
+ includeProfileContext: selectedWorkspace?.include_profile_context,
+ includeWorkspaceInstructions:
+ selectedWorkspace?.include_workspace_instructions
+ })
+
+ if (!profile) return null
+ if (!selectedWorkspace) return null
+
+ return (
+
+ }
+ renderInputs={() => (
+ <>
+
+
+
+ setName(e.target.value)}
+ maxLength={PRESET_NAME_MAX}
+ />
+
+
+
+
+
+ setDescription(e.target.value)}
+ maxLength={PRESET_DESCRIPTION_MAX}
+ />
+
+
+
+ >
+ )}
+ />
+ )
+}
diff --git a/components/sidebar/items/presets/preset-item.tsx b/components/sidebar/items/presets/preset-item.tsx
new file mode 100644
index 0000000000..79350fed7a
--- /dev/null
+++ b/components/sidebar/items/presets/preset-item.tsx
@@ -0,0 +1,77 @@
+import { ModelIcon } from "@/components/models/model-icon"
+import { ChatSettingsForm } from "@/components/ui/chat-settings-form"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { PRESET_DESCRIPTION_MAX, PRESET_NAME_MAX } from "@/db/limits"
+import { Tables } from "@/supabase/types"
+import { FC, useState } from "react"
+import { SidebarItem } from "../all/sidebar-display-item"
+
+interface PresetItemProps {
+ preset: Tables<"presets">
+}
+
+export const PresetItem: FC = ({ preset }) => {
+ const [name, setName] = useState(preset.name)
+ const [description, setDescription] = useState(preset.description)
+ const [presetChatSettings, setPresetChatSettings] = useState({
+ model: preset.model,
+ prompt: preset.prompt,
+ temperature: preset.temperature,
+ contextLength: preset.context_length,
+ includeProfileContext: preset.include_profile_context,
+ includeWorkspaceInstructions: preset.include_workspace_instructions
+ })
+
+ return (
+
+ }
+ updateState={{
+ name,
+ description,
+ include_profile_context: presetChatSettings.includeProfileContext,
+ include_workspace_instructions:
+ presetChatSettings.includeWorkspaceInstructions,
+ context_length: presetChatSettings.contextLength,
+ model: presetChatSettings.model,
+ prompt: presetChatSettings.prompt,
+ temperature: presetChatSettings.temperature
+ }}
+ renderInputs={() => (
+ <>
+
+
+
+ setName(e.target.value)}
+ maxLength={PRESET_NAME_MAX}
+ />
+
+
+
+
+
+ setDescription(e.target.value)}
+ maxLength={PRESET_DESCRIPTION_MAX}
+ />
+
+
+
+ >
+ )}
+ />
+ )
+}
diff --git a/components/sidebar/items/prompts/create-prompt.tsx b/components/sidebar/items/prompts/create-prompt.tsx
new file mode 100644
index 0000000000..58015664f9
--- /dev/null
+++ b/components/sidebar/items/prompts/create-prompt.tsx
@@ -0,0 +1,67 @@
+import { SidebarCreateItem } from "@/components/sidebar/items/all/sidebar-create-item"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { TextareaAutosize } from "@/components/ui/textarea-autosize"
+import { ChatbotUIContext } from "@/context/context"
+import { PROMPT_NAME_MAX } from "@/db/limits"
+import { TablesInsert } from "@/supabase/types"
+import { FC, useContext, useState } from "react"
+
+interface CreatePromptProps {
+ isOpen: boolean
+ onOpenChange: (isOpen: boolean) => void
+}
+
+export const CreatePrompt: FC = ({
+ isOpen,
+ onOpenChange
+}) => {
+ const { profile, selectedWorkspace } = useContext(ChatbotUIContext)
+
+ const [name, setName] = useState("")
+ const [content, setContent] = useState("")
+
+ if (!profile) return null
+ if (!selectedWorkspace) return null
+
+ return (
+
+ }
+ renderInputs={() => (
+ <>
+
+
+
+ setName(e.target.value)}
+ maxLength={PROMPT_NAME_MAX}
+ />
+
+
+
+
+
+
+
+ >
+ )}
+ />
+ )
+}
diff --git a/components/sidebar/items/prompts/prompt-item.tsx b/components/sidebar/items/prompts/prompt-item.tsx
new file mode 100644
index 0000000000..bf74f746d7
--- /dev/null
+++ b/components/sidebar/items/prompts/prompt-item.tsx
@@ -0,0 +1,52 @@
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { TextareaAutosize } from "@/components/ui/textarea-autosize"
+import { PROMPT_NAME_MAX } from "@/db/limits"
+import { Tables } from "@/supabase/types"
+import { IconPencil } from "@tabler/icons-react"
+import { FC, useState } from "react"
+import { SidebarItem } from "../all/sidebar-display-item"
+
+interface PromptItemProps {
+ prompt: Tables<"prompts">
+}
+
+export const PromptItem: FC = ({ prompt }) => {
+ const [name, setName] = useState(prompt.name)
+ const [content, setContent] = useState(prompt.content)
+
+ return (
+ }
+ updateState={{ name, content }}
+ renderInputs={() => (
+ <>
+
+
+
+ setName(e.target.value)}
+ maxLength={PROMPT_NAME_MAX}
+ />
+
+
+
+
+
+
+
+ >
+ )}
+ />
+ )
+}
diff --git a/components/sidebar/sidebar-content.tsx b/components/sidebar/sidebar-content.tsx
new file mode 100644
index 0000000000..1e826557c2
--- /dev/null
+++ b/components/sidebar/sidebar-content.tsx
@@ -0,0 +1,47 @@
+import { Tables } from "@/supabase/types"
+import { ContentType, DataListType } from "@/types"
+import { FC, useState } from "react"
+import { SidebarCreateButtons } from "./sidebar-create-buttons"
+import { SidebarDataList } from "./sidebar-data-list"
+import { SidebarSearch } from "./sidebar-search"
+
+interface SidebarContentProps {
+ contentType: ContentType
+ data: DataListType
+ folders: Tables<"folders">[]
+}
+
+export const SidebarContent: FC = ({
+ contentType,
+ data,
+ folders
+}) => {
+ const [searchTerm, setSearchTerm] = useState("")
+
+ const filteredData: any = data.filter(item =>
+ item.name.toLowerCase().includes(searchTerm.toLowerCase())
+ )
+
+ return (
+ // Subtract 50px for the height of the workspace settings
+
+ )
+}
diff --git a/components/sidebar/sidebar-create-buttons.tsx b/components/sidebar/sidebar-create-buttons.tsx
new file mode 100644
index 0000000000..45b22d7776
--- /dev/null
+++ b/components/sidebar/sidebar-create-buttons.tsx
@@ -0,0 +1,128 @@
+import { useChatHandler } from "@/components/chat/chat-hooks/use-chat-handler"
+import { ChatbotUIContext } from "@/context/context"
+import { createFolder } from "@/db/folders"
+import { ContentType } from "@/types"
+import { IconFolderPlus, IconPlus } from "@tabler/icons-react"
+import { FC, useContext, useState } from "react"
+import { Button } from "../ui/button"
+import { CreateAssistant } from "./items/assistants/create-assistant"
+import { CreateCollection } from "./items/collections/create-collection"
+import { CreateFile } from "./items/files/create-file"
+import { CreatePreset } from "./items/presets/create-preset"
+import { CreatePrompt } from "./items/prompts/create-prompt"
+
+interface SidebarCreateButtonsProps {
+ contentType: ContentType
+}
+
+export const SidebarCreateButtons: FC = ({
+ contentType
+}) => {
+ const { profile, selectedWorkspace, folders, setFolders } =
+ useContext(ChatbotUIContext)
+ const { handleNewChat } = useChatHandler()
+
+ const [isCreatingPrompt, setIsCreatingPrompt] = useState(false)
+ const [isCreatingPreset, setIsCreatingPreset] = useState(false)
+ const [isCreatingFile, setIsCreatingFile] = useState(false)
+ const [isCreatingCollection, setIsCreatingCollection] = useState(false)
+ const [isCreatingAssistant, setIsCreatingAssistant] = useState(false)
+
+ const handleCreateFolder = async () => {
+ if (!profile) return
+ if (!selectedWorkspace) return
+
+ const createdFolder = await createFolder({
+ user_id: profile.user_id,
+ workspace_id: selectedWorkspace.id,
+ name: "New Folder",
+ description: "",
+ type: contentType
+ })
+ setFolders([...folders, createdFolder])
+ }
+
+ const getCreateFunction = () => {
+ switch (contentType) {
+ case "chats":
+ return async () => {
+ handleNewChat()
+ }
+
+ case "presets":
+ return async () => {
+ setIsCreatingPreset(true)
+ }
+
+ case "prompts":
+ return async () => {
+ setIsCreatingPrompt(true)
+ }
+
+ case "files":
+ return async () => {
+ setIsCreatingFile(true)
+ }
+
+ case "collections":
+ return async () => {
+ setIsCreatingCollection(true)
+ }
+
+ case "assistants":
+ return async () => {
+ setIsCreatingAssistant(true)
+ }
+
+ default:
+ break
+ }
+ }
+
+ return (
+
+
+
+
+
+ {isCreatingPrompt && (
+
+ )}
+
+ {isCreatingPreset && (
+
+ )}
+
+ {isCreatingFile && (
+
+ )}
+
+ {isCreatingCollection && (
+
+ )}
+
+ {isCreatingAssistant && (
+
+ )}
+
+ )
+}
diff --git a/components/sidebar/sidebar-data-list.tsx b/components/sidebar/sidebar-data-list.tsx
new file mode 100644
index 0000000000..a6e9922380
--- /dev/null
+++ b/components/sidebar/sidebar-data-list.tsx
@@ -0,0 +1,241 @@
+import { ChatbotUIContext } from "@/context/context"
+import { updateAssistant } from "@/db/assistants"
+import { updateChat } from "@/db/chats"
+import { updateCollection } from "@/db/collections"
+import { updateFile } from "@/db/files"
+import { updatePreset } from "@/db/presets"
+import { updatePrompt } from "@/db/prompts"
+import { cn } from "@/lib/utils"
+import { Tables } from "@/supabase/types"
+import { ContentType, DataItemType, DataListType } from "@/types"
+import { FC, useContext, useEffect, useRef, useState } from "react"
+import { Separator } from "../ui/separator"
+import { AssistantItem } from "./items/assistants/assistant-item"
+import { ChatItem } from "./items/chat/chat-item"
+import { CollectionItem } from "./items/collections/collection-item"
+import { FileItem } from "./items/files/file-item"
+import { Folder } from "./items/folders/folder-item"
+import { PresetItem } from "./items/presets/preset-item"
+import { PromptItem } from "./items/prompts/prompt-item"
+
+interface SidebarDataListProps {
+ contentType: ContentType
+ data: DataListType
+ folders: Tables<"folders">[]
+}
+
+export const SidebarDataList: FC = ({
+ contentType,
+ data,
+ folders
+}) => {
+ const {
+ setChats,
+ setPresets,
+ setPrompts,
+ setFiles,
+ setCollections,
+ setAssistants
+ } = useContext(ChatbotUIContext)
+
+ const divRef = useRef(null)
+
+ const [isOverflowing, setIsOverflowing] = useState(false)
+ const [isDragOver, setIsDragOver] = useState(false)
+
+ const getDataListComponent = (
+ contentType: ContentType,
+ item: DataItemType
+ ) => {
+ switch (contentType) {
+ case "chats":
+ return } />
+
+ case "presets":
+ return } />
+
+ case "prompts":
+ return } />
+
+ case "files":
+ return } />
+
+ case "collections":
+ return (
+ }
+ />
+ )
+
+ case "assistants":
+ return (
+ }
+ />
+ )
+
+ default:
+ return null
+ }
+ }
+
+ const updateFunctions = {
+ chats: updateChat,
+ presets: updatePreset,
+ prompts: updatePrompt,
+ files: updateFile,
+ collections: updateCollection,
+ assistants: updateAssistant
+ }
+
+ const stateUpdateFunctions = {
+ chats: setChats,
+ presets: setPresets,
+ prompts: setPrompts,
+ files: setFiles,
+ collections: setCollections,
+ assistants: setAssistants
+ }
+
+ const updateFolder = async (itemId: string, folderId: string | null) => {
+ const item: any = data.find(item => item.id === itemId)
+
+ if (!item) return null
+
+ const updateFunction = updateFunctions[contentType]
+ const setStateFunction = stateUpdateFunctions[contentType]
+
+ if (!updateFunction || !setStateFunction) return
+
+ const updatedItem = await updateFunction(item.id, {
+ folder_id: folderId
+ })
+
+ setStateFunction((items: any) =>
+ items.map((item: any) =>
+ item.id === updatedItem.id ? updatedItem : item
+ )
+ )
+ }
+
+ const handleDragEnter = (e: React.DragEvent) => {
+ e.preventDefault()
+ setIsDragOver(true)
+ }
+
+ const handleDragLeave = (e: React.DragEvent) => {
+ e.preventDefault()
+ setIsDragOver(false)
+ }
+
+ const handleDragStart = (e: React.DragEvent, id: string) => {
+ e.dataTransfer.setData("text/plain", id)
+ }
+
+ const handleDragOver = (e: React.DragEvent) => {
+ e.preventDefault()
+ }
+
+ const handleDrop = (e: React.DragEvent) => {
+ e.preventDefault()
+
+ const target = e.target as Element
+
+ if (!target.closest("#folder")) {
+ const itemId = e.dataTransfer.getData("text/plain")
+ updateFolder(itemId, null)
+ }
+
+ setIsDragOver(false)
+ }
+
+ useEffect(() => {
+ if (divRef.current) {
+ setIsOverflowing(
+ divRef.current.scrollHeight > divRef.current.clientHeight
+ )
+ }
+ }, [data])
+
+ const dataWithFolders = data.filter(item => item.folder_id)
+
+ const dataWithoutFolders = data.filter(item => item.folder_id === null)
+
+ return (
+ <>
+
+ {data.length === 0 && (
+
+
+ No {contentType}.
+
+
+ )}
+
+ {(dataWithFolders.length > 0 || dataWithoutFolders.length > 0) && (
+
+ {folders.map(folder => (
+
+ {dataWithFolders
+ .filter(item => item.folder_id === folder.id)
+ .map(item => (
+ handleDragStart(e, item.id)}
+ >
+ {getDataListComponent(contentType, item)}
+
+ ))}
+
+ ))}
+
+ {folders.length > 0 &&
}
+
+
+ {dataWithoutFolders.map(item => {
+ return (
+
handleDragStart(e, item.id)}
+ >
+ {getDataListComponent(contentType, item)}
+
+ )
+ })}
+
+
+ )}
+
+
+
+ >
+ )
+}
diff --git a/components/sidebar/sidebar-search.tsx b/components/sidebar/sidebar-search.tsx
new file mode 100644
index 0000000000..b451bc8442
--- /dev/null
+++ b/components/sidebar/sidebar-search.tsx
@@ -0,0 +1,23 @@
+import { ContentType } from "@/types"
+import { FC } from "react"
+import { Input } from "../ui/input"
+
+interface SidebarSearchProps {
+ contentType: ContentType
+ searchTerm: string
+ setSearchTerm: Function
+}
+
+export const SidebarSearch: FC = ({
+ contentType,
+ searchTerm,
+ setSearchTerm
+}) => {
+ return (
+ setSearchTerm(e.target.value)}
+ />
+ )
+}
diff --git a/components/sidebar/sidebar-switch-item.tsx b/components/sidebar/sidebar-switch-item.tsx
new file mode 100644
index 0000000000..2ccc92f440
--- /dev/null
+++ b/components/sidebar/sidebar-switch-item.tsx
@@ -0,0 +1,33 @@
+import { ContentType } from "@/types"
+import { FC } from "react"
+import { TabsTrigger } from "../ui/tabs"
+import { WithTooltip } from "../ui/with-tooltip"
+
+interface SidebarSwitchItemProps {
+ contentType: ContentType
+ icon: React.ReactNode
+ onContentTypeChange: (contentType: ContentType) => void
+}
+
+export const SidebarSwitchItem: FC = ({
+ contentType,
+ icon,
+ onContentTypeChange
+}) => {
+ return (
+ {contentType[0].toUpperCase() + contentType.substring(1)}
+ }
+ trigger={
+ onContentTypeChange(contentType as ContentType)}
+ >
+ {icon}
+
+ }
+ />
+ )
+}
diff --git a/components/sidebar/sidebar-switcher.tsx b/components/sidebar/sidebar-switcher.tsx
new file mode 100644
index 0000000000..5947a46bc4
--- /dev/null
+++ b/components/sidebar/sidebar-switcher.tsx
@@ -0,0 +1,79 @@
+import { ContentType } from "@/types"
+import {
+ IconAdjustmentsHorizontal,
+ IconBooks,
+ IconFile,
+ IconMessage,
+ IconPencil,
+ IconRobotFace
+} from "@tabler/icons-react"
+import { FC } from "react"
+import { TabsList } from "../ui/tabs"
+import { WithTooltip } from "../ui/with-tooltip"
+import { ProfileSettings } from "../utility/profile-settings"
+import { SidebarSwitchItem } from "./sidebar-switch-item"
+
+export const SIDEBAR_ICON_SIZE = 28
+
+interface SidebarSwitcherProps {
+ onContentTypeChange: (contentType: ContentType) => void
+}
+
+export const SidebarSwitcher: FC = ({
+ onContentTypeChange
+}) => {
+ return (
+
+
+ }
+ contentType="chats"
+ onContentTypeChange={onContentTypeChange}
+ />
+
+ }
+ contentType="presets"
+ onContentTypeChange={onContentTypeChange}
+ />
+
+ }
+ contentType="prompts"
+ onContentTypeChange={onContentTypeChange}
+ />
+
+ }
+ contentType="files"
+ onContentTypeChange={onContentTypeChange}
+ />
+
+ }
+ contentType="collections"
+ onContentTypeChange={onContentTypeChange}
+ />
+
+ }
+ contentType="assistants"
+ onContentTypeChange={onContentTypeChange}
+ />
+
+
+
+ {/* TODO */}
+ {/* Import
} trigger={
} /> */}
+
+ {/* TODO */}
+ {/*
*/}
+
+
Profile Settings }
+ trigger={}
+ />
+
+
+ )
+}
diff --git a/components/sidebar/sidebar.tsx b/components/sidebar/sidebar.tsx
new file mode 100644
index 0000000000..01aac5a9b8
--- /dev/null
+++ b/components/sidebar/sidebar.tsx
@@ -0,0 +1,94 @@
+import { ChatbotUIContext } from "@/context/context"
+import { Tables } from "@/supabase/types"
+import { ContentType } from "@/types"
+import { FC, useContext } from "react"
+import { SIDEBAR_WIDTH } from "../ui/dashboard"
+import { TabsContent } from "../ui/tabs"
+import { WorkspaceSwitcher } from "../utility/workspace-switcher"
+import { WorkspaceSettings } from "../workspace/workspace-settings"
+import { SidebarContent } from "./sidebar-content"
+
+interface SidebarProps {
+ contentType: ContentType
+ showSidebar: boolean
+}
+
+export const Sidebar: FC = ({ contentType, showSidebar }) => {
+ const { folders, chats, presets, prompts, files, collections, assistants } =
+ useContext(ChatbotUIContext)
+
+ const chatFolders = folders.filter(folder => folder.type === "chats")
+ const presetFolders = folders.filter(folder => folder.type === "presets")
+ const promptFolders = folders.filter(folder => folder.type === "prompts")
+ const filesFolders = folders.filter(folder => folder.type === "files")
+ const collectionFolders = folders.filter(
+ folder => folder.type === "collection"
+ )
+ const assistantFolders = folders.filter(
+ folder => folder.type === "assistants"
+ )
+
+ const renderSidebarContent = (
+ contentType: ContentType,
+ data: any[],
+ folders: Tables<"folders">[]
+ ) => {
+ return (
+
+ )
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ {(() => {
+ switch (contentType) {
+ case "chats":
+ return renderSidebarContent("chats", chats, chatFolders)
+
+ case "presets":
+ return renderSidebarContent("presets", presets, presetFolders)
+
+ case "prompts":
+ return renderSidebarContent("prompts", prompts, promptFolders)
+
+ case "files":
+ return renderSidebarContent("files", files, filesFolders)
+
+ case "collections":
+ return renderSidebarContent(
+ "collections",
+ collections,
+ collectionFolders
+ )
+
+ case "assistants":
+ return renderSidebarContent(
+ "assistants",
+ assistants,
+ assistantFolders
+ )
+
+ default:
+ return null
+ }
+ })()}
+
+
+ )
+}
diff --git a/components/ui/accordion.tsx b/components/ui/accordion.tsx
new file mode 100644
index 0000000000..d360b22470
--- /dev/null
+++ b/components/ui/accordion.tsx
@@ -0,0 +1,58 @@
+"use client"
+
+import * as React from "react"
+import * as AccordionPrimitive from "@radix-ui/react-accordion"
+import { ChevronDown } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Accordion = AccordionPrimitive.Root
+
+const AccordionItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AccordionItem.displayName = "AccordionItem"
+
+const AccordionTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ svg]:rotate-180",
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+))
+AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
+
+const AccordionContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ {children}
+
+))
+
+AccordionContent.displayName = AccordionPrimitive.Content.displayName
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
diff --git a/components/ui/advanced-settings.tsx b/components/ui/advanced-settings.tsx
new file mode 100644
index 0000000000..0c2bf85960
--- /dev/null
+++ b/components/ui/advanced-settings.tsx
@@ -0,0 +1,40 @@
+import {
+ Collapsible,
+ CollapsibleContent,
+ CollapsibleTrigger
+} from "@/components/ui/collapsible"
+import { IconChevronDown, IconChevronRight } from "@tabler/icons-react"
+import { FC, useState } from "react"
+
+interface AdvancedSettingsProps {
+ children: React.ReactNode
+}
+
+export const AdvancedSettings: FC = ({ children }) => {
+ const [isOpen, setIsOpen] = useState(
+ false
+ // localStorage.getItem("advanced-settings-open") === "true"
+ )
+
+ const handleOpenChange = (isOpen: boolean) => {
+ setIsOpen(isOpen)
+ // localStorage.setItem("advanced-settings-open", String(isOpen))
+ }
+
+ return (
+
+
+
+
Advanced Settings
+ {isOpen ? (
+
+ ) : (
+
+ )}
+
+
+
+ {children}
+
+ )
+}
diff --git a/components/ui/alert-dialog.tsx b/components/ui/alert-dialog.tsx
new file mode 100644
index 0000000000..d468c39290
--- /dev/null
+++ b/components/ui/alert-dialog.tsx
@@ -0,0 +1,141 @@
+"use client"
+
+import * as React from "react"
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+
+const AlertDialog = AlertDialogPrimitive.Root
+
+const AlertDialogTrigger = AlertDialogPrimitive.Trigger
+
+const AlertDialogPortal = AlertDialogPrimitive.Portal
+
+const AlertDialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
+
+const AlertDialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+))
+AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
+
+const AlertDialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogHeader.displayName = "AlertDialogHeader"
+
+const AlertDialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogFooter.displayName = "AlertDialogFooter"
+
+const AlertDialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
+
+const AlertDialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogDescription.displayName =
+ AlertDialogPrimitive.Description.displayName
+
+const AlertDialogAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
+
+const AlertDialogCancel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
+
+export {
+ AlertDialog,
+ AlertDialogPortal,
+ AlertDialogOverlay,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogFooter,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogAction,
+ AlertDialogCancel
+}
diff --git a/components/ui/alert.tsx b/components/ui/alert.tsx
new file mode 100644
index 0000000000..588ee66fe4
--- /dev/null
+++ b/components/ui/alert.tsx
@@ -0,0 +1,59 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const alertVariants = cva(
+ "[&>svg]:text-foreground relative w-full rounded-lg border p-4 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg~*]:pl-7",
+ {
+ variants: {
+ variant: {
+ default: "bg-background text-foreground",
+ destructive:
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive"
+ }
+ },
+ defaultVariants: {
+ variant: "default"
+ }
+ }
+)
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & VariantProps
+>(({ className, variant, ...props }, ref) => (
+
+))
+Alert.displayName = "Alert"
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertTitle.displayName = "AlertTitle"
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertDescription.displayName = "AlertDescription"
+
+export { Alert, AlertTitle, AlertDescription }
diff --git a/components/ui/aspect-ratio.tsx b/components/ui/aspect-ratio.tsx
new file mode 100644
index 0000000000..d6a5226f5e
--- /dev/null
+++ b/components/ui/aspect-ratio.tsx
@@ -0,0 +1,7 @@
+"use client"
+
+import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
+
+const AspectRatio = AspectRatioPrimitive.Root
+
+export { AspectRatio }
diff --git a/components/ui/avatar.tsx b/components/ui/avatar.tsx
new file mode 100644
index 0000000000..e6a93ce078
--- /dev/null
+++ b/components/ui/avatar.tsx
@@ -0,0 +1,50 @@
+"use client"
+
+import * as React from "react"
+import * as AvatarPrimitive from "@radix-ui/react-avatar"
+
+import { cn } from "@/lib/utils"
+
+const Avatar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+Avatar.displayName = AvatarPrimitive.Root.displayName
+
+const AvatarImage = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarImage.displayName = AvatarPrimitive.Image.displayName
+
+const AvatarFallback = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
+
+export { Avatar, AvatarImage, AvatarFallback }
diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx
new file mode 100644
index 0000000000..56f0ea4b42
--- /dev/null
+++ b/components/ui/badge.tsx
@@ -0,0 +1,36 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const badgeVariants = cva(
+ "focus:ring-ring inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2",
+ {
+ variants: {
+ variant: {
+ default:
+ "bg-primary text-primary-foreground hover:bg-primary/80 border-transparent",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80 border-transparent",
+ destructive:
+ "bg-destructive text-destructive-foreground hover:bg-destructive/80 border-transparent",
+ outline: "text-foreground"
+ }
+ },
+ defaultVariants: {
+ variant: "default"
+ }
+ }
+)
+
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return (
+
+ )
+}
+
+export { Badge, badgeVariants }
diff --git a/components/ui/brand.tsx b/components/ui/brand.tsx
new file mode 100644
index 0000000000..ae42e07372
--- /dev/null
+++ b/components/ui/brand.tsx
@@ -0,0 +1,26 @@
+"use client"
+
+import Link from "next/link"
+import { FC } from "react"
+import { ChatbotUISVG } from "../icons/chatbotui-svg"
+
+interface BrandProps {
+ theme?: "dark" | "light"
+}
+
+export const Brand: FC = ({ theme = "dark" }) => {
+ return (
+
+
+
+
+
+ Chatbot UI
+
+ )
+}
diff --git a/components/ui/button.tsx b/components/ui/button.tsx
new file mode 100644
index 0000000000..0652e0347b
--- /dev/null
+++ b/components/ui/button.tsx
@@ -0,0 +1,56 @@
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "ring-offset-background focus-visible:ring-ring inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors hover:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ outline:
+ "border-input bg-background hover:bg-accent hover:text-accent-foreground border",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline"
+ },
+ size: {
+ default: "h-10 px-4 py-2",
+ sm: "h-9 rounded-md px-3",
+ lg: "h-11 rounded-md px-8",
+ icon: "h-10 w-10"
+ }
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default"
+ }
+ }
+)
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+ return (
+
+ )
+ }
+)
+Button.displayName = "Button"
+
+export { Button, buttonVariants }
diff --git a/components/ui/calendar.tsx b/components/ui/calendar.tsx
new file mode 100644
index 0000000000..65e98ce528
--- /dev/null
+++ b/components/ui/calendar.tsx
@@ -0,0 +1,66 @@
+"use client"
+
+import * as React from "react"
+import { ChevronLeft, ChevronRight } from "lucide-react"
+import { DayPicker } from "react-day-picker"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+
+export type CalendarProps = React.ComponentProps
+
+function Calendar({
+ className,
+ classNames,
+ showOutsideDays = true,
+ ...props
+}: CalendarProps) {
+ return (
+ ,
+ IconRight: ({ ...props }) =>
+ }}
+ {...props}
+ />
+ )
+}
+Calendar.displayName = "Calendar"
+
+export { Calendar }
diff --git a/components/ui/card.tsx b/components/ui/card.tsx
new file mode 100644
index 0000000000..a26fd5d7b7
--- /dev/null
+++ b/components/ui/card.tsx
@@ -0,0 +1,79 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+Card.displayName = "Card"
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardHeader.displayName = "CardHeader"
+
+const CardTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardTitle.displayName = "CardTitle"
+
+const CardDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardDescription.displayName = "CardDescription"
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardContent.displayName = "CardContent"
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardFooter.displayName = "CardFooter"
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
diff --git a/components/ui/chat-settings-form.tsx b/components/ui/chat-settings-form.tsx
new file mode 100644
index 0000000000..2b0e4ef19e
--- /dev/null
+++ b/components/ui/chat-settings-form.tsx
@@ -0,0 +1,239 @@
+"use client"
+
+import { ChatbotUIContext } from "@/context/context"
+import { CHAT_SETTING_LIMITS } from "@/lib/chat-setting-limits"
+import { LLM_LIST } from "@/lib/models/llm/llm-list"
+import { ChatSettings } from "@/types"
+import { IconInfoCircle } from "@tabler/icons-react"
+import { FC, useContext } from "react"
+import { ModelSelect } from "../models/model-select"
+import { AdvancedSettings } from "./advanced-settings"
+import { Checkbox } from "./checkbox"
+import { Label } from "./label"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue
+} from "./select"
+import { Slider } from "./slider"
+import { TextareaAutosize } from "./textarea-autosize"
+import { WithTooltip } from "./with-tooltip"
+
+interface ChatSettingsFormProps {
+ chatSettings: ChatSettings
+ onChangeChatSettings: (value: ChatSettings) => void
+ useAdvancedDropdown?: boolean
+ showTooltip?: boolean
+}
+
+export const ChatSettingsForm: FC = ({
+ chatSettings,
+ onChangeChatSettings,
+ useAdvancedDropdown = true,
+ showTooltip = true
+}) => {
+ const { profile, availableLocalModels } = useContext(ChatbotUIContext)
+
+ if (!profile) return null
+
+ return (
+
+
+
+
+ {
+ onChangeChatSettings({ ...chatSettings, model })
+ }}
+ />
+
+
+
+
+
+ {
+ onChangeChatSettings({ ...chatSettings, prompt })
+ }}
+ value={chatSettings.prompt}
+ minRows={3}
+ maxRows={6}
+ />
+
+
+ {useAdvancedDropdown ? (
+
+
+
+ ) : (
+
+ )}
+
+ )
+}
+
+interface AdvancedContentProps {
+ chatSettings: ChatSettings
+ onChangeChatSettings: (value: ChatSettings) => void
+ showTooltip: boolean
+}
+
+const AdvancedContent: FC = ({
+ chatSettings,
+ onChangeChatSettings,
+ showTooltip
+}) => {
+ const { profile, selectedWorkspace } = useContext(ChatbotUIContext)
+
+ const MODEL_LIMITS = CHAT_SETTING_LIMITS[chatSettings.model] || {
+ MIN_TEMPERATURE: 0,
+ MAX_TEMPERATURE: 1,
+ MAX_CONTEXT_LENGTH: 4096
+ }
+
+ return (
+
+
+
+
+
{
+ onChangeChatSettings({
+ ...chatSettings,
+ temperature: temperature[0]
+ })
+ }}
+ min={MODEL_LIMITS.MIN_TEMPERATURE}
+ max={MODEL_LIMITS.MAX_TEMPERATURE}
+ step={0.01}
+ />
+
+
+
+
+
+
{
+ onChangeChatSettings({
+ ...chatSettings,
+ contextLength: contextLength[0]
+ })
+ }}
+ min={0}
+ max={MODEL_LIMITS.MAX_CONTEXT_LENGTH - 200} // 200 is a minimum buffer for token output
+ step={1}
+ />
+
+
+
+
+ onChangeChatSettings({
+ ...chatSettings,
+ includeProfileContext: value
+ })
+ }
+ />
+
+
+
+ {showTooltip && (
+
+ {profile?.profile_context || "No profile context."}
+
+ }
+ trigger={
+
+ }
+ />
+ )}
+
+
+
+
+ onChangeChatSettings({
+ ...chatSettings,
+ includeWorkspaceInstructions: value
+ })
+ }
+ />
+
+
+
+ {showTooltip && (
+
+ {selectedWorkspace?.instructions ||
+ "No workspace instructions."}
+
+ }
+ trigger={
+
+ }
+ />
+ )}
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/components/ui/checkbox.tsx b/components/ui/checkbox.tsx
new file mode 100644
index 0000000000..ff148f2d23
--- /dev/null
+++ b/components/ui/checkbox.tsx
@@ -0,0 +1,30 @@
+"use client"
+
+import * as React from "react"
+import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
+import { Check } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Checkbox = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+
+))
+Checkbox.displayName = CheckboxPrimitive.Root.displayName
+
+export { Checkbox }
diff --git a/components/ui/collapsible.tsx b/components/ui/collapsible.tsx
new file mode 100644
index 0000000000..9fa48946af
--- /dev/null
+++ b/components/ui/collapsible.tsx
@@ -0,0 +1,11 @@
+"use client"
+
+import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
+
+const Collapsible = CollapsiblePrimitive.Root
+
+const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
+
+const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
+
+export { Collapsible, CollapsibleTrigger, CollapsibleContent }
diff --git a/components/ui/command.tsx b/components/ui/command.tsx
new file mode 100644
index 0000000000..ea9f817bca
--- /dev/null
+++ b/components/ui/command.tsx
@@ -0,0 +1,155 @@
+"use client"
+
+import * as React from "react"
+import { type DialogProps } from "@radix-ui/react-dialog"
+import { Command as CommandPrimitive } from "cmdk"
+import { Search } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+import { Dialog, DialogContent } from "@/components/ui/dialog"
+
+const Command = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+Command.displayName = CommandPrimitive.displayName
+
+interface CommandDialogProps extends DialogProps {}
+
+const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
+ return (
+
+ )
+}
+
+const CommandInput = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+))
+
+CommandInput.displayName = CommandPrimitive.Input.displayName
+
+const CommandList = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+
+CommandList.displayName = CommandPrimitive.List.displayName
+
+const CommandEmpty = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>((props, ref) => (
+
+))
+
+CommandEmpty.displayName = CommandPrimitive.Empty.displayName
+
+const CommandGroup = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+
+CommandGroup.displayName = CommandPrimitive.Group.displayName
+
+const CommandSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+CommandSeparator.displayName = CommandPrimitive.Separator.displayName
+
+const CommandItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+
+CommandItem.displayName = CommandPrimitive.Item.displayName
+
+const CommandShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => {
+ return (
+
+ )
+}
+CommandShortcut.displayName = "CommandShortcut"
+
+export {
+ Command,
+ CommandDialog,
+ CommandInput,
+ CommandList,
+ CommandEmpty,
+ CommandGroup,
+ CommandItem,
+ CommandShortcut,
+ CommandSeparator
+}
diff --git a/components/ui/context-menu.tsx b/components/ui/context-menu.tsx
new file mode 100644
index 0000000000..d504aab074
--- /dev/null
+++ b/components/ui/context-menu.tsx
@@ -0,0 +1,200 @@
+"use client"
+
+import * as React from "react"
+import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
+import { Check, ChevronRight, Circle } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const ContextMenu = ContextMenuPrimitive.Root
+
+const ContextMenuTrigger = ContextMenuPrimitive.Trigger
+
+const ContextMenuGroup = ContextMenuPrimitive.Group
+
+const ContextMenuPortal = ContextMenuPrimitive.Portal
+
+const ContextMenuSub = ContextMenuPrimitive.Sub
+
+const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
+
+const ContextMenuSubTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, children, ...props }, ref) => (
+
+ {children}
+
+
+))
+ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
+
+const ContextMenuSubContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
+
+const ContextMenuContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
+
+const ContextMenuItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, ...props }, ref) => (
+
+))
+ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
+
+const ContextMenuCheckboxItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, checked, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+ContextMenuCheckboxItem.displayName =
+ ContextMenuPrimitive.CheckboxItem.displayName
+
+const ContextMenuRadioItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
+
+const ContextMenuLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, ...props }, ref) => (
+
+))
+ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
+
+const ContextMenuSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
+
+const ContextMenuShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => {
+ return (
+
+ )
+}
+ContextMenuShortcut.displayName = "ContextMenuShortcut"
+
+export {
+ ContextMenu,
+ ContextMenuTrigger,
+ ContextMenuContent,
+ ContextMenuItem,
+ ContextMenuCheckboxItem,
+ ContextMenuRadioItem,
+ ContextMenuLabel,
+ ContextMenuSeparator,
+ ContextMenuShortcut,
+ ContextMenuGroup,
+ ContextMenuPortal,
+ ContextMenuSub,
+ ContextMenuSubContent,
+ ContextMenuSubTrigger,
+ ContextMenuRadioGroup
+}
diff --git a/components/ui/dashboard.tsx b/components/ui/dashboard.tsx
new file mode 100644
index 0000000000..faf2d8aea8
--- /dev/null
+++ b/components/ui/dashboard.tsx
@@ -0,0 +1,131 @@
+"use client"
+
+import { Sidebar } from "@/components/sidebar/sidebar"
+import { SidebarSwitcher } from "@/components/sidebar/sidebar-switcher"
+import { Button } from "@/components/ui/button"
+import { Tabs } from "@/components/ui/tabs"
+import useHotkey from "@/lib/hooks/use-hotkey"
+import { cn } from "@/lib/utils"
+import { ContentType } from "@/types"
+import { IconChevronCompactRight } from "@tabler/icons-react"
+import { usePathname, useRouter, useSearchParams } from "next/navigation"
+import { FC, useState } from "react"
+import { useSelectFileHandler } from "../chat/chat-hooks/use-select-file-handler"
+import { CommandK } from "../utility/command-k"
+
+export const SIDEBAR_WIDTH = 350
+
+interface DashboardProps {
+ children: React.ReactNode
+}
+
+export const Dashboard: FC = ({ children }) => {
+ useHotkey("s", () => setShowSidebar(prevState => !prevState))
+
+ const pathname = usePathname()
+ const router = useRouter()
+ const searchParams = useSearchParams()
+ const tabValue = searchParams.get("tab") || "chats"
+
+ const { handleSelectDeviceFile } = useSelectFileHandler()
+
+ const [contentType, setContentType] = useState(
+ tabValue as ContentType
+ )
+ const [showSidebar, setShowSidebar] = useState(
+ localStorage.getItem("showSidebar") === "true"
+ )
+ const [isDragging, setIsDragging] = useState(false)
+
+ const onFileDrop = (event: React.DragEvent) => {
+ event.preventDefault()
+
+ const files = event.dataTransfer.files
+ const file = files[0]
+
+ handleSelectDeviceFile(file)
+
+ setIsDragging(false)
+ }
+
+ const handleDragEnter = (event: React.DragEvent) => {
+ event.preventDefault()
+ setIsDragging(true)
+ }
+
+ const handleDragLeave = (event: React.DragEvent) => {
+ event.preventDefault()
+ setIsDragging(false)
+ }
+
+ const onDragOver = (event: React.DragEvent) => {
+ event.preventDefault()
+ }
+
+ const handleToggleSidebar = () => {
+ setShowSidebar(prevState => !prevState)
+ localStorage.setItem("showSidebar", String(!showSidebar))
+ }
+
+ return (
+
+
+
+
+
+
+ {showSidebar && (
+ {
+ setContentType(tabValue as ContentType)
+ router.replace(`${pathname}?tab=${tabValue}`)
+ }}
+ >
+
+
+
+
+ )}
+
+
+
+ {isDragging ? (
+
+ drop file here
+
+ ) : (
+ children
+ )}
+
+
+ )
+}
diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx
new file mode 100644
index 0000000000..5c01896f87
--- /dev/null
+++ b/components/ui/dialog.tsx
@@ -0,0 +1,121 @@
+"use client"
+
+import * as DialogPrimitive from "@radix-ui/react-dialog"
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Dialog = DialogPrimitive.Root
+
+const DialogTrigger = DialogPrimitive.Trigger
+
+const DialogPortal = DialogPrimitive.Portal
+
+const DialogClose = DialogPrimitive.Close
+
+const DialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
+
+const DialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+ {children}
+ {/*
+
+ Close
+ */}
+
+
+))
+DialogContent.displayName = DialogPrimitive.Content.displayName
+
+const DialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+DialogHeader.displayName = "DialogHeader"
+
+const DialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+DialogFooter.displayName = "DialogFooter"
+
+const DialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogTitle.displayName = DialogPrimitive.Title.displayName
+
+const DialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogDescription.displayName = DialogPrimitive.Description.displayName
+
+export {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogOverlay,
+ DialogPortal,
+ DialogTitle,
+ DialogTrigger
+}
diff --git a/components/ui/dropdown-menu.tsx b/components/ui/dropdown-menu.tsx
new file mode 100644
index 0000000000..02297f2b6b
--- /dev/null
+++ b/components/ui/dropdown-menu.tsx
@@ -0,0 +1,200 @@
+"use client"
+
+import * as React from "react"
+import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
+import { Check, ChevronRight, Circle } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const DropdownMenu = DropdownMenuPrimitive.Root
+
+const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
+
+const DropdownMenuGroup = DropdownMenuPrimitive.Group
+
+const DropdownMenuPortal = DropdownMenuPrimitive.Portal
+
+const DropdownMenuSub = DropdownMenuPrimitive.Sub
+
+const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
+
+const DropdownMenuSubTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, children, ...props }, ref) => (
+
+ {children}
+
+
+))
+DropdownMenuSubTrigger.displayName =
+ DropdownMenuPrimitive.SubTrigger.displayName
+
+const DropdownMenuSubContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DropdownMenuSubContent.displayName =
+ DropdownMenuPrimitive.SubContent.displayName
+
+const DropdownMenuContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+
+
+))
+DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
+
+const DropdownMenuItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, ...props }, ref) => (
+
+))
+DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
+
+const DropdownMenuCheckboxItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, checked, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+DropdownMenuCheckboxItem.displayName =
+ DropdownMenuPrimitive.CheckboxItem.displayName
+
+const DropdownMenuRadioItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
+
+const DropdownMenuLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, ...props }, ref) => (
+
+))
+DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
+
+const DropdownMenuSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
+
+const DropdownMenuShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => {
+ return (
+
+ )
+}
+DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
+
+export {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuGroup,
+ DropdownMenuPortal,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuRadioGroup
+}
diff --git a/components/ui/file-icon.tsx b/components/ui/file-icon.tsx
new file mode 100644
index 0000000000..94c36777e7
--- /dev/null
+++ b/components/ui/file-icon.tsx
@@ -0,0 +1,39 @@
+import {
+ IconFile,
+ IconFileText,
+ IconFileTypeCsv,
+ IconFileTypeDoc,
+ IconFileTypeDocx,
+ IconFileTypePdf,
+ IconJson,
+ IconMarkdown,
+ IconPhoto
+} from "@tabler/icons-react"
+import { FC } from "react"
+
+interface FileIconProps {
+ type: string
+ size?: number
+}
+
+export const FileIcon: FC = ({ type, size = 32 }) => {
+ if (type.includes("image")) {
+ return
+ } else if (type.includes("pdf")) {
+ return
+ } else if (type.includes("csv")) {
+ return
+ } else if (type.includes("doc")) {
+ return
+ } else if (type.includes("docx")) {
+ return
+ } else if (type.includes("plain")) {
+ return
+ } else if (type.includes("json")) {
+ return
+ } else if (type.includes("markdown")) {
+ return
+ } else {
+ return
+ }
+}
diff --git a/components/ui/file-preview.tsx b/components/ui/file-preview.tsx
new file mode 100644
index 0000000000..4fc37bf52f
--- /dev/null
+++ b/components/ui/file-preview.tsx
@@ -0,0 +1,65 @@
+import { cn } from "@/lib/utils"
+import { Tables } from "@/supabase/types"
+import { ChatFile, MessageImage } from "@/types"
+import { IconFileFilled } from "@tabler/icons-react"
+import Image from "next/image"
+import { FC } from "react"
+import { Dialog, DialogContent } from "./dialog"
+
+interface FilePreviewProps {
+ type: "image" | "file" | "file_item"
+ item: ChatFile | MessageImage | Tables<"file_items">
+ isOpen: boolean
+ onOpenChange: (isOpen: boolean) => void
+}
+
+export const FilePreview: FC = ({
+ type,
+ item,
+ isOpen,
+ onOpenChange
+}) => {
+ return (
+
+ )
+}
diff --git a/components/ui/form.tsx b/components/ui/form.tsx
new file mode 100644
index 0000000000..38cb190f81
--- /dev/null
+++ b/components/ui/form.tsx
@@ -0,0 +1,176 @@
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+import { Slot } from "@radix-ui/react-slot"
+import {
+ Controller,
+ ControllerProps,
+ FieldPath,
+ FieldValues,
+ FormProvider,
+ useFormContext
+} from "react-hook-form"
+
+import { cn } from "@/lib/utils"
+import { Label } from "@/components/ui/label"
+
+const Form = FormProvider
+
+type FormFieldContextValue<
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath
+> = {
+ name: TName
+}
+
+const FormFieldContext = React.createContext(
+ {} as FormFieldContextValue
+)
+
+const FormField = <
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath
+>({
+ ...props
+}: ControllerProps) => {
+ return (
+
+
+
+ )
+}
+
+const useFormField = () => {
+ const fieldContext = React.useContext(FormFieldContext)
+ const itemContext = React.useContext(FormItemContext)
+ const { getFieldState, formState } = useFormContext()
+
+ const fieldState = getFieldState(fieldContext.name, formState)
+
+ if (!fieldContext) {
+ throw new Error("useFormField should be used within ")
+ }
+
+ const { id } = itemContext
+
+ return {
+ id,
+ name: fieldContext.name,
+ formItemId: `${id}-form-item`,
+ formDescriptionId: `${id}-form-item-description`,
+ formMessageId: `${id}-form-item-message`,
+ ...fieldState
+ }
+}
+
+type FormItemContextValue = {
+ id: string
+}
+
+const FormItemContext = React.createContext(
+ {} as FormItemContextValue
+)
+
+const FormItem = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const id = React.useId()
+
+ return (
+
+
+
+ )
+})
+FormItem.displayName = "FormItem"
+
+const FormLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => {
+ const { error, formItemId } = useFormField()
+
+ return (
+
+ )
+})
+FormLabel.displayName = "FormLabel"
+
+const FormControl = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ ...props }, ref) => {
+ const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
+
+ return (
+
+ )
+})
+FormControl.displayName = "FormControl"
+
+const FormDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { formDescriptionId } = useFormField()
+
+ return (
+