diff --git a/app/[locale]/share/assistant/[id]/page.tsx b/app/[locale]/share/assistant/[id]/page.tsx deleted file mode 100644 index 777239c348..0000000000 --- a/app/[locale]/share/assistant/[id]/page.tsx +++ /dev/null @@ -1,65 +0,0 @@ -"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/[locale]/share/chat/[id]/page.tsx b/app/[locale]/share/chat/[id]/page.tsx deleted file mode 100644 index 222a918074..0000000000 --- a/app/[locale]/share/chat/[id]/page.tsx +++ /dev/null @@ -1,73 +0,0 @@ -"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/[locale]/share/collection/[id]/page.tsx b/app/[locale]/share/collection/[id]/page.tsx deleted file mode 100644 index 03ea478410..0000000000 --- a/app/[locale]/share/collection/[id]/page.tsx +++ /dev/null @@ -1,75 +0,0 @@ -"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/[locale]/share/file/[id]/page.tsx b/app/[locale]/share/file/[id]/page.tsx deleted file mode 100644 index fb3e107c25..0000000000 --- a/app/[locale]/share/file/[id]/page.tsx +++ /dev/null @@ -1,72 +0,0 @@ -"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/[locale]/share/preset/[id]/page.tsx b/app/[locale]/share/preset/[id]/page.tsx deleted file mode 100644 index 5767ae1ce7..0000000000 --- a/app/[locale]/share/preset/[id]/page.tsx +++ /dev/null @@ -1,22 +0,0 @@ -"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/[locale]/share/prompt/[id]/page.tsx b/app/[locale]/share/prompt/[id]/page.tsx deleted file mode 100644 index fecfa5c153..0000000000 --- a/app/[locale]/share/prompt/[id]/page.tsx +++ /dev/null @@ -1,22 +0,0 @@ -"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/app/api/chat/custom/route.ts b/app/api/chat/custom/route.ts new file mode 100644 index 0000000000..ea35b14832 --- /dev/null +++ b/app/api/chat/custom/route.ts @@ -0,0 +1,69 @@ +import { CHAT_SETTING_LIMITS } from "@/lib/chat-setting-limits" +import { Database } from "@/supabase/types" +import { ChatSettings } from "@/types" +import { createClient } from "@supabase/supabase-js" +import { OpenAIStream, StreamingTextResponse } from "ai" +import { ServerRuntime } from "next" +import OpenAI from "openai" +import { ChatCompletionCreateParamsBase } from "openai/resources/chat/completions.mjs" + +export const runtime: ServerRuntime = "edge" + +export async function POST(request: Request) { + const json = await request.json() + const { chatSettings, messages, customModelId } = json as { + chatSettings: ChatSettings + messages: any[] + customModelId: string + } + + try { + const supabaseAdmin = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.SUPABASE_SERVICE_ROLE_KEY! + ) + + const { data: customModel, error } = await supabaseAdmin + .from("models") + .select("*") + .eq("id", customModelId) + .single() + + if (!customModel) { + throw new Error(error.message) + } + + const custom = new OpenAI({ + apiKey: customModel.api_key || "", + baseURL: customModel.base_url + }) + + const response = await custom.chat.completions.create({ + model: chatSettings.model as ChatCompletionCreateParamsBase["model"], + messages: messages as ChatCompletionCreateParamsBase["messages"], + temperature: chatSettings.temperature, + max_tokens: + CHAT_SETTING_LIMITS[chatSettings.model].MAX_TOKEN_OUTPUT_LENGTH, + stream: true + }) + + const stream = OpenAIStream(response) + + return new StreamingTextResponse(stream) + } catch (error: any) { + let errorMessage = error.message || "An unexpected error occurred" + const errorCode = error.status || 500 + + if (errorMessage.toLowerCase().includes("api key not found")) { + errorMessage = + "Custom API Key not found. Please set it in your profile settings." + } else if (errorMessage.toLowerCase().includes("incorrect api key")) { + errorMessage = + "Custom API Key is incorrect. Please fix it in your profile settings." + } + + return new Response(JSON.stringify({ message: errorMessage }), { + status: errorCode + }) + } +} diff --git a/app/api/chat/perplexity/route.ts b/app/api/chat/perplexity/route.ts index f7f865ab59..5b181c3154 100644 --- a/app/api/chat/perplexity/route.ts +++ b/app/api/chat/perplexity/route.ts @@ -32,10 +32,8 @@ export async function POST(request: Request) { stream: true }) - // Convert the response into a friendly text-stream. const stream = OpenAIStream(response) - // Respond with the stream return new StreamingTextResponse(stream) } catch (error: any) { let errorMessage = error.message || "An unexpected error occurred" diff --git a/components/chat/chat-helpers/index.ts b/components/chat/chat-helpers/index.ts index bb008f680b..82e06a5763 100644 --- a/components/chat/chat-helpers/index.ts +++ b/components/chat/chat-helpers/index.ts @@ -215,13 +215,18 @@ export const handleHostedChat = async ( formattedMessages = await buildFinalMessages(payload, profile, chatImages) } + const apiEndpoint = + provider === "custom" ? "/api/chat/custom" : `/api/chat/${provider}` + + const requestBody = { + chatSettings: payload.chatSettings, + messages: formattedMessages, + customModelId: provider === "custom" ? modelData.hostedId : "" + } + const response = await fetchChatResponse( - `/api/chat/${provider}`, - { - chatSettings: payload.chatSettings, - messages: formattedMessages, - tools: [] - }, + apiEndpoint, + requestBody, true, newAbortController, setIsGenerating, diff --git a/components/chat/chat-hooks/use-chat-handler.tsx b/components/chat/chat-hooks/use-chat-handler.tsx index a8c6b501bd..5b3df1427c 100644 --- a/components/chat/chat-hooks/use-chat-handler.tsx +++ b/components/chat/chat-hooks/use-chat-handler.tsx @@ -3,7 +3,7 @@ import { updateChat } from "@/db/chats" import { deleteMessagesIncludingAndAfter } from "@/db/messages" import { buildFinalMessages } from "@/lib/build-prompt" import { Tables } from "@/supabase/types" -import { ChatMessage, ChatPayload, LLMID } from "@/types" +import { ChatMessage, ChatPayload, LLMID, ModelProvider } from "@/types" import { useRouter } from "next/navigation" import { useContext, useRef } from "react" import { LLM_LIST } from "../../../lib/models/llm/llm-list" @@ -58,7 +58,8 @@ export const useChatHandler = () => { setIsAtPickerOpen, selectedTools, selectedPreset, - setChatSettings + setChatSettings, + models } = useContext(ChatbotUIContext) const chatInputRef = useRef(null) @@ -159,6 +160,14 @@ export const useChatHandler = () => { setAbortController(newAbortController) const modelData = [ + ...models.map(model => ({ + modelId: model.model_id as LLMID, + modelName: model.name, + provider: "custom" as ModelProvider, + hostedId: model.id, + platformLink: "", + imageInput: false + })), ...LLM_LIST, ...availableLocalModels, ...availableOpenRouterModels diff --git a/components/chat/chat-secondary-buttons.tsx b/components/chat/chat-secondary-buttons.tsx index d6c6ed92d5..6fb2d4d4ce 100644 --- a/components/chat/chat-secondary-buttons.tsx +++ b/components/chat/chat-secondary-buttons.tsx @@ -73,9 +73,6 @@ export const ChatSecondaryButtons: FC = ({}) => { /> )} - - {/* TODO */} - {/* */} ) } diff --git a/components/messages/message.tsx b/components/messages/message.tsx index 2d4ecb430d..15254db713 100644 --- a/components/messages/message.tsx +++ b/components/messages/message.tsx @@ -3,7 +3,7 @@ import { ChatbotUIContext } from "@/context/context" import { LLM_LIST } from "@/lib/models/llm/llm-list" import { cn } from "@/lib/utils" import { Tables } from "@/supabase/types" -import { LLM, LLMID, MessageImage } from "@/types" +import { LLM, LLMID, MessageImage, ModelProvider } from "@/types" import { IconBolt, IconCaretDownFilled, @@ -60,7 +60,8 @@ export const Message: FC = ({ chatImages, assistantImages, toolInUse, - files + files, + models } = useContext(ChatbotUIContext) const { handleSendMessage } = useChatHandler() @@ -128,6 +129,14 @@ export const Message: FC = ({ }, [isEditing]) const MODEL_DATA = [ + ...models.map(model => ({ + modelId: model.model_id as LLMID, + modelName: model.name, + provider: "custom" as ModelProvider, + hostedId: model.id, + platformLink: "", + imageInput: false + })), ...LLM_LIST, ...availableLocalModels, ...availableOpenRouterModels diff --git a/components/models/model-select.tsx b/components/models/model-select.tsx index 75f4130cd0..24cc30b647 100644 --- a/components/models/model-select.tsx +++ b/components/models/model-select.tsx @@ -1,5 +1,5 @@ import { ChatbotUIContext } from "@/context/context" -import { LLM, LLMID } from "@/types" +import { LLM, LLMID, ModelProvider } from "@/types" import { IconCheck, IconChevronDown } from "@tabler/icons-react" import { FC, useContext, useEffect, useRef, useState } from "react" import { Button } from "../ui/button" @@ -24,6 +24,7 @@ export const ModelSelect: FC = ({ }) => { const { profile, + models, availableHostedModels, availableLocalModels, availableOpenRouterModels @@ -68,6 +69,14 @@ export const ModelSelect: FC = ({ } const allModels = [ + ...models.map(model => ({ + modelId: model.model_id as LLMID, + modelName: model.name, + provider: "custom" as ModelProvider, + hostedId: model.id, + platformLink: "", + imageInput: false + })), ...availableHostedModels, ...availableLocalModels, ...availableOpenRouterModels diff --git a/components/sharing/add-to-workspace.tsx b/components/sharing/add-to-workspace.tsx deleted file mode 100644 index f5e1143091..0000000000 --- a/components/sharing/add-to-workspace.tsx +++ /dev/null @@ -1,218 +0,0 @@ -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, - setTools - } = 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 - }, - tools: async ( - item: Tables<"tools">, - workspaceId: string, - userId: string - ) => { - // TODO - } - } - - const stateUpdateFunctions = { - chats: setChats, - presets: setPresets, - prompts: setPrompts, - files: setFiles, - collections: setCollections, - assistants: setAssistants, - tools: setTools - } - - 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 deleted file mode 100644 index 02e272a279..0000000000 --- a/components/sharing/share-assistant.tsx +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index 66127500ba..0000000000 --- a/components/sharing/share-chat.tsx +++ /dev/null @@ -1,149 +0,0 @@ -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 deleted file mode 100644 index 42277af35b..0000000000 --- a/components/sharing/share-collection.tsx +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index 1ae04402e7..0000000000 --- a/components/sharing/share-file.tsx +++ /dev/null @@ -1,44 +0,0 @@ -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 deleted file mode 100644 index dc28d480c2..0000000000 --- a/components/sharing/share-header.tsx +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index 18d01ea7dc..0000000000 --- a/components/sharing/share-item.tsx +++ /dev/null @@ -1,203 +0,0 @@ -import Loading from "@/app/[locale]/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 - - case "tools": - 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 deleted file mode 100644 index 4c1a36ac8f..0000000000 --- a/components/sharing/share-message.tsx +++ /dev/null @@ -1,42 +0,0 @@ -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 ( -
-
- {icon} - -
{label}
-
- - -
- ) -} diff --git a/components/sharing/share-page.tsx b/components/sharing/share-page.tsx deleted file mode 100644 index 2c14ecbb91..0000000000 --- a/components/sharing/share-page.tsx +++ /dev/null @@ -1,71 +0,0 @@ -"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 deleted file mode 100644 index 599f587077..0000000000 --- a/components/sharing/share-preset.tsx +++ /dev/null @@ -1,73 +0,0 @@ -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 deleted file mode 100644 index c4b8a1192d..0000000000 --- a/components/sharing/share-prompt.tsx +++ /dev/null @@ -1,26 +0,0 @@ -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 index cf74c02f1b..4be85f0199 100644 --- a/components/sidebar/items/all/sidebar-create-item.tsx +++ b/components/sidebar/items/all/sidebar-create-item.tsx @@ -15,6 +15,7 @@ import { createChat } from "@/db/chats" import { createCollectionFiles } from "@/db/collection-files" import { createCollection } from "@/db/collections" import { createFileBasedOnExtension } from "@/db/files" +import { createModel } from "@/db/models" import { createPreset } from "@/db/presets" import { createPrompt } from "@/db/prompts" import { @@ -54,7 +55,8 @@ export const SidebarCreateItem: FC = ({ setCollections, setAssistants, setAssistantImages, - setTools + setTools, + setModels } = useContext(ChatbotUIContext) const buttonRef = useRef(null) @@ -167,7 +169,8 @@ export const SidebarCreateItem: FC = ({ return updatedAssistant }, - tools: createTool + tools: createTool, + models: createModel } const stateUpdateFunctions = { @@ -177,7 +180,8 @@ export const SidebarCreateItem: FC = ({ files: setFiles, collections: setCollections, assistants: setAssistants, - tools: setTools + tools: setTools, + models: setModels } const handleCreate = async () => { diff --git a/components/sidebar/items/all/sidebar-delete-item.tsx b/components/sidebar/items/all/sidebar-delete-item.tsx index b2d59494b7..5066bdfc8f 100644 --- a/components/sidebar/items/all/sidebar-delete-item.tsx +++ b/components/sidebar/items/all/sidebar-delete-item.tsx @@ -13,6 +13,7 @@ import { deleteAssistant } from "@/db/assistants" import { deleteChat } from "@/db/chats" import { deleteCollection } from "@/db/collections" import { deleteFile } from "@/db/files" +import { deleteModel } from "@/db/models" import { deletePreset } from "@/db/presets" import { deletePrompt } from "@/db/prompts" import { deleteFileFromStorage } from "@/db/storage/files" @@ -37,7 +38,8 @@ export const SidebarDeleteItem: FC = ({ setFiles, setCollections, setAssistants, - setTools + setTools, + setModels } = useContext(ChatbotUIContext) const buttonRef = useRef(null) @@ -69,6 +71,9 @@ export const SidebarDeleteItem: FC = ({ }, tools: async (tool: Tables<"tools">) => { await deleteTool(tool.id) + }, + models: async (model: Tables<"models">) => { + await deleteModel(model.id) } } @@ -79,7 +84,8 @@ export const SidebarDeleteItem: FC = ({ files: setFiles, collections: setCollections, assistants: setAssistants, - tools: setTools + tools: setTools, + models: setModels } const handleDelete = async () => { diff --git a/components/sidebar/items/all/sidebar-display-item.tsx b/components/sidebar/items/all/sidebar-display-item.tsx index daf2b77985..7d4b6bb68e 100644 --- a/components/sidebar/items/all/sidebar-display-item.tsx +++ b/components/sidebar/items/all/sidebar-display-item.tsx @@ -62,7 +62,8 @@ export const SidebarItem: FC = ({ router.push(`/chat/${createdChat.id}`) }, - tools: async (item: any) => {} + tools: async (item: any) => {}, + models: async (item: any) => {} } const handleKeyDown = (e: React.KeyboardEvent) => { diff --git a/components/sidebar/items/all/sidebar-update-item.tsx b/components/sidebar/items/all/sidebar-update-item.tsx index dd44b09c15..93811e5027 100644 --- a/components/sidebar/items/all/sidebar-update-item.tsx +++ b/components/sidebar/items/all/sidebar-update-item.tsx @@ -49,6 +49,12 @@ import { getFileWorkspacesByFileId, updateFile } from "@/db/files" +import { + createModelWorkspaces, + deleteModelWorkspace, + getModelWorkspacesByModelId, + updateModel +} from "@/db/models" import { createPresetWorkspaces, deletePresetWorkspace, @@ -101,7 +107,8 @@ export const SidebarUpdateItem: FC = ({ setFiles, setCollections, setAssistants, - setTools + setTools, + setModels } = useContext(ChatbotUIContext) const buttonRef = useRef(null) @@ -183,7 +190,8 @@ export const SidebarUpdateItem: FC = ({ selectedAssistantTools, setSelectedAssistantTools }, - tools: null + tools: null, + models: null } const fetchDataFunctions = { @@ -212,7 +220,8 @@ export const SidebarUpdateItem: FC = ({ setSelectedAssistantCollections([]) setSelectedAssistantTools([]) }, - tools: null + tools: null, + models: null } const fetchWorkpaceFunctions = { @@ -240,6 +249,10 @@ export const SidebarUpdateItem: FC = ({ tools: async (toolId: string) => { const item = await getToolWorkspacesByToolId(toolId) return item.workspaces + }, + models: async (modelId: string) => { + const item = await getModelWorkspacesByModelId(modelId) + return item.workspaces } } @@ -514,6 +527,20 @@ export const SidebarUpdateItem: FC = ({ ) return updatedTool + }, + models: async (modelId: string, updateState: TablesUpdate<"models">) => { + const updatedModel = await updateModel(modelId, updateState) + + await handleWorkspaceUpdates( + startingWorkspaces, + selectedWorkspaces, + modelId, + deleteModelWorkspace, + createModelWorkspaces as any, + "model_id" + ) + + return updatedModel } } @@ -524,7 +551,8 @@ export const SidebarUpdateItem: FC = ({ files: setFiles, collections: setCollections, assistants: setAssistants, - tools: setTools + tools: setTools, + models: setModels } const handleUpdate = async () => { @@ -590,11 +618,6 @@ export const SidebarUpdateItem: FC = ({ - {/* TODO */} - {/*
- -
*/} -
{workspaces.length > 1 && (
diff --git a/components/sidebar/items/models/create-model.tsx b/components/sidebar/items/models/create-model.tsx new file mode 100644 index 0000000000..a2a238926f --- /dev/null +++ b/components/sidebar/items/models/create-model.tsx @@ -0,0 +1,103 @@ +import { SidebarCreateItem } from "@/components/sidebar/items/all/sidebar-create-item" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { ChatbotUIContext } from "@/context/context" +import { MODEL_NAME_MAX } from "@/db/limits" +import { TablesInsert } from "@/supabase/types" +import { FC, useContext, useState } from "react" + +interface CreateModelProps { + isOpen: boolean + onOpenChange: (isOpen: boolean) => void +} + +export const CreateModel: FC = ({ isOpen, onOpenChange }) => { + const { profile, selectedWorkspace } = useContext(ChatbotUIContext) + + const [isTyping, setIsTyping] = useState(false) + + const [apiKey, setApiKey] = useState("") + const [baseUrl, setBaseUrl] = useState("") + const [description, setDescription] = useState("") + const [modelId, setModelId] = useState("") + const [name, setName] = useState("") + + if (!profile || !selectedWorkspace) return null + + return ( + + } + renderInputs={() => ( + <> +
+
Create a custom model.
+ +
+ Your API *must* be compatible + with the OpenAI SDK. +
+
+ +
+ + + setName(e.target.value)} + maxLength={MODEL_NAME_MAX} + /> +
+ +
+ + + setModelId(e.target.value)} + /> +
+ +
+ + + setBaseUrl(e.target.value)} + /> + +
+ Your API must be compatible with the OpenAI SDK. +
+
+ +
+ + + setApiKey(e.target.value)} + /> +
+ + )} + /> + ) +} diff --git a/components/sidebar/items/models/model-item.tsx b/components/sidebar/items/models/model-item.tsx new file mode 100644 index 0000000000..3576ba1665 --- /dev/null +++ b/components/sidebar/items/models/model-item.tsx @@ -0,0 +1,88 @@ +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { MODEL_NAME_MAX } from "@/db/limits" +import { Tables, TablesUpdate } from "@/supabase/types" +import { IconSparkles } from "@tabler/icons-react" +import { FC, useState } from "react" +import { SidebarItem } from "../all/sidebar-display-item" + +interface ModelItemProps { + model: Tables<"models"> +} + +export const ModelItem: FC = ({ model }) => { + const [isTyping, setIsTyping] = useState(false) + + const [apiKey, setApiKey] = useState(model.api_key) + const [baseUrl, setBaseUrl] = useState(model.base_url) + const [description, setDescription] = useState(model.description) + const [modelId, setModelId] = useState(model.model_id) + const [name, setName] = useState(model.name) + + return ( + } + updateState={ + { + api_key: apiKey, + base_url: baseUrl, + description, + model_id: modelId, + name + } as TablesUpdate<"models"> + } + renderInputs={() => ( + <> +
+ + + setName(e.target.value)} + maxLength={MODEL_NAME_MAX} + /> +
+ +
+ + + setModelId(e.target.value)} + /> +
+ +
+ + + setBaseUrl(e.target.value)} + /> + +
+ Your API must be compatible with the OpenAI SDK. +
+
+ +
+ + + setApiKey(e.target.value)} + /> +
+ + )} + /> + ) +} diff --git a/components/sidebar/sidebar-create-buttons.tsx b/components/sidebar/sidebar-create-buttons.tsx index 9fc871bcdd..934365471f 100644 --- a/components/sidebar/sidebar-create-buttons.tsx +++ b/components/sidebar/sidebar-create-buttons.tsx @@ -8,6 +8,7 @@ import { Button } from "../ui/button" import { CreateAssistant } from "./items/assistants/create-assistant" import { CreateCollection } from "./items/collections/create-collection" import { CreateFile } from "./items/files/create-file" +import { CreateModel } from "./items/models/create-model" import { CreatePreset } from "./items/presets/create-preset" import { CreatePrompt } from "./items/prompts/create-prompt" import { CreateTool } from "./items/tools/create-tool" @@ -31,6 +32,7 @@ export const SidebarCreateButtons: FC = ({ const [isCreatingCollection, setIsCreatingCollection] = useState(false) const [isCreatingAssistant, setIsCreatingAssistant] = useState(false) const [isCreatingTool, setIsCreatingTool] = useState(false) + const [isCreatingModel, setIsCreatingModel] = useState(false) const handleCreateFolder = async () => { if (!profile) return @@ -83,6 +85,11 @@ export const SidebarCreateButtons: FC = ({ setIsCreatingTool(true) } + case "models": + return async () => { + setIsCreatingModel(true) + } + default: break } @@ -138,6 +145,13 @@ export const SidebarCreateButtons: FC = ({ {isCreatingTool && ( )} + + {isCreatingModel && ( + + )}
) } diff --git a/components/sidebar/sidebar-data-list.tsx b/components/sidebar/sidebar-data-list.tsx index 8fce18c2ce..682ffe3a5e 100644 --- a/components/sidebar/sidebar-data-list.tsx +++ b/components/sidebar/sidebar-data-list.tsx @@ -3,6 +3,7 @@ import { updateAssistant } from "@/db/assistants" import { updateChat } from "@/db/chats" import { updateCollection } from "@/db/collections" import { updateFile } from "@/db/files" +import { updateModel } from "@/db/models" import { updatePreset } from "@/db/presets" import { updatePrompt } from "@/db/prompts" import { updateTool } from "@/db/tools" @@ -16,6 +17,7 @@ import { ChatItem } from "./items/chat/chat-item" import { CollectionItem } from "./items/collections/collection-item" import { FileItem } from "./items/files/file-item" import { Folder } from "./items/folders/folder-item" +import { ModelItem } from "./items/models/model-item" import { PresetItem } from "./items/presets/preset-item" import { PromptItem } from "./items/prompts/prompt-item" import { ToolItem } from "./items/tools/tool-item" @@ -38,7 +40,8 @@ export const SidebarDataList: FC = ({ setFiles, setCollections, setAssistants, - setTools + setTools, + setModels } = useContext(ChatbotUIContext) const divRef = useRef(null) @@ -82,6 +85,9 @@ export const SidebarDataList: FC = ({ case "tools": return } /> + case "models": + return } /> + default: return null } @@ -133,7 +139,8 @@ export const SidebarDataList: FC = ({ files: updateFile, collections: updateCollection, assistants: updateAssistant, - tools: updateTool + tools: updateTool, + models: updateModel } const stateUpdateFunctions = { @@ -143,7 +150,8 @@ export const SidebarDataList: FC = ({ files: setFiles, collections: setCollections, assistants: setAssistants, - tools: setTools + tools: setTools, + models: setModels } const updateFolder = async (itemId: string, folderId: string | null) => { diff --git a/components/sidebar/sidebar-switcher.tsx b/components/sidebar/sidebar-switcher.tsx index 8b89028401..6e1caa1838 100644 --- a/components/sidebar/sidebar-switcher.tsx +++ b/components/sidebar/sidebar-switcher.tsx @@ -6,7 +6,8 @@ import { IconFile, IconMessage, IconPencil, - IconRobotFace + IconRobotFace, + IconSparkles } from "@tabler/icons-react" import { FC } from "react" import { TabsList } from "../ui/tabs" @@ -25,32 +26,43 @@ export const SidebarSwitcher: FC = ({ }) => { return (
- + } contentType="chats" onContentTypeChange={onContentTypeChange} /> + } contentType="presets" onContentTypeChange={onContentTypeChange} /> + } contentType="prompts" onContentTypeChange={onContentTypeChange} /> + + } + contentType="models" + onContentTypeChange={onContentTypeChange} + /> + } contentType="files" onContentTypeChange={onContentTypeChange} /> + } contentType="collections" onContentTypeChange={onContentTypeChange} /> + } contentType="assistants" diff --git a/components/sidebar/sidebar.tsx b/components/sidebar/sidebar.tsx index c33f237e45..69a228e6b2 100644 --- a/components/sidebar/sidebar.tsx +++ b/components/sidebar/sidebar.tsx @@ -22,7 +22,8 @@ export const Sidebar: FC = ({ contentType, showSidebar }) => { files, collections, assistants, - tools + tools, + models } = useContext(ChatbotUIContext) const chatFolders = folders.filter(folder => folder.type === "chats") @@ -36,6 +37,7 @@ export const Sidebar: FC = ({ contentType, showSidebar }) => { folder => folder.type === "assistants" ) const toolFolders = folders.filter(folder => folder.type === "tools") + const modelFolders = folders.filter(folder => folder.type === "models") const renderSidebarContent = ( contentType: ContentType, @@ -96,6 +98,9 @@ export const Sidebar: FC = ({ contentType, showSidebar }) => { case "tools": return renderSidebarContent("tools", tools, toolFolders) + case "models": + return renderSidebarContent("models", models, modelFolders) + default: return null } diff --git a/components/utility/global-state.tsx b/components/utility/global-state.tsx index dc7ba4d768..3ce27b0b38 100644 --- a/components/utility/global-state.tsx +++ b/components/utility/global-state.tsx @@ -9,6 +9,7 @@ import { getChatsByWorkspaceId } from "@/db/chats" import { getCollectionWorkspacesByWorkspaceId } from "@/db/collections" import { getFileWorkspacesByWorkspaceId } from "@/db/files" import { getFoldersByWorkspaceId } from "@/db/folders" +import { getModelWorkspacesByWorkspaceId } from "@/db/models" import { getPresetWorkspacesByWorkspaceId } from "@/db/presets" import { getProfileByUserId } from "@/db/profile" import { getPromptWorkspacesByWorkspaceId } from "@/db/prompts" @@ -53,6 +54,7 @@ export const GlobalState: FC = ({ children }) => { const [chats, setChats] = useState[]>([]) const [files, setFiles] = useState[]>([]) const [folders, setFolders] = useState[]>([]) + const [models, setModels] = useState[]>([]) const [presets, setPresets] = useState[]>([]) const [prompts, setPrompts] = useState[]>([]) const [tools, setTools] = useState[]>([]) @@ -291,6 +293,9 @@ export const GlobalState: FC = ({ children }) => { const toolData = await getToolWorkspacesByWorkspaceId(workspaceId) setTools(toolData.tools) + const modelData = await getModelWorkspacesByWorkspaceId(workspaceId) + setModels(modelData.models) + setLoading(false) } @@ -332,6 +337,8 @@ export const GlobalState: FC = ({ children }) => { setFiles, folders, setFolders, + models, + setModels, presets, setPresets, prompts, diff --git a/components/utility/share-menu.tsx b/components/utility/share-menu.tsx deleted file mode 100644 index 3f475f5cdf..0000000000 --- a/components/utility/share-menu.tsx +++ /dev/null @@ -1,152 +0,0 @@ -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 { updateTool } from "@/db/tools" -import { ContentType, DataItemType } from "@/types" -import { IconCheck, IconShare2 } from "@tabler/icons-react" -import { FC, useContext } from "react" -import { toast } from "sonner" -import { Button } from "../ui/button" -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger -} from "../ui/dropdown-menu" - -export type ShareStatus = "private" | "unlisted" | "public" - -interface ShareMenuProps { - item: DataItemType | null - contentType: ContentType - size?: number -} - -export const ShareMenu: FC = ({ - item, - contentType, - size = 28 -}) => { - const { - setChats, - setPresets, - setPrompts, - setFiles, - setCollections, - setAssistants, - setSelectedChat, - setTools - } = useContext(ChatbotUIContext) - - const updateFunctions = { - chats: updateChat, - presets: updatePreset, - prompts: updatePrompt, - files: updateFile, - collections: updateCollection, - assistants: updateAssistant, - tools: updateTool - } - - const stateUpdateFunctions = { - chats: setChats, - presets: setPresets, - prompts: setPrompts, - files: setFiles, - collections: setCollections, - assistants: setAssistants, - tools: setTools - } - - const handleShareChange = async (shareStatus: ShareStatus) => { - if (!item) return null - - const contentName = contentType.slice(0, -1) - - const updateFunction = updateFunctions[contentType] - const setStateFunction = stateUpdateFunctions[contentType] - - if (!updateFunction || !setStateFunction) return - - const updatedItem: any = await updateFunction(item.id, { - ...(item as any), - sharing: shareStatus - }) - - if (navigator.clipboard && shareStatus !== "private") { - navigator.clipboard.writeText( - `${window.location.origin}/share/${contentName}/${item.id}` - ) - } - - if (contentType === "chats") { - setSelectedChat(updatedItem) - } - - setStateFunction((items: any) => - items.map((item: any) => - item.id === updatedItem.id ? updatedItem : item - ) - ) - - if (shareStatus === "private") { - toast.success(`${contentName} is now private`) - } else { - toast.success(`Copied share link to clipboard!`) - } - } - - if (!item) return null - - return ( - - - - - - - Share Menu - - - - handleShareChange("private")} - > - {item.sharing === "private" && ( - - )} -
Private
-
- - handleShareChange("unlisted")} - > - {item.sharing === "unlisted" && ( - - )} -
Unlisted
-
- - handleShareChange("public")} - > - {item.sharing === "public" && ( - - )} -
Public
-
-
-
- ) -} diff --git a/context/context.tsx b/context/context.tsx index 19ea517720..033e63f581 100644 --- a/context/context.tsx +++ b/context/context.tsx @@ -27,6 +27,8 @@ interface ChatbotUIContext { setFiles: Dispatch[]>> folders: Tables<"folders">[] setFolders: Dispatch[]>> + models: Tables<"models">[] + setModels: Dispatch[]>> presets: Tables<"presets">[] setPresets: Dispatch[]>> prompts: Tables<"prompts">[] @@ -143,6 +145,8 @@ export const ChatbotUIContext = createContext({ setFiles: () => {}, folders: [], setFolders: () => {}, + models: [], + setModels: () => {}, presets: [], setPresets: () => {}, prompts: [], diff --git a/db/limits.ts b/db/limits.ts index 74269bba73..4d61f85280 100644 --- a/db/limits.ts +++ b/db/limits.ts @@ -37,3 +37,7 @@ export const ASSISTANT_PROMPT_MAX = 100000 // Tools export const TOOL_NAME_MAX = 100 export const TOOL_DESCRIPTION_MAX = 500 + +// Models +export const MODEL_NAME_MAX = 100 +export const MODEL_DESCRIPTION_MAX = 500 diff --git a/db/models.ts b/db/models.ts new file mode 100644 index 0000000000..252153437b --- /dev/null +++ b/db/models.ts @@ -0,0 +1,177 @@ +import { supabase } from "@/lib/supabase/browser-client" +import { TablesInsert, TablesUpdate } from "@/supabase/types" + +export const getModelById = async (modelId: string) => { + const { data: model, error } = await supabase + .from("models") + .select("*") + .eq("id", modelId) + .single() + + if (!model) { + throw new Error(error.message) + } + + return model +} + +export const getModelWorkspacesByWorkspaceId = async (workspaceId: string) => { + const { data: workspace, error } = await supabase + .from("workspaces") + .select( + ` + id, + name, + models (*) + ` + ) + .eq("id", workspaceId) + .single() + + if (!workspace) { + throw new Error(error.message) + } + + return workspace +} + +export const getModelWorkspacesByModelId = async (modelId: string) => { + const { data: model, error } = await supabase + .from("models") + .select( + ` + id, + name, + workspaces (*) + ` + ) + .eq("id", modelId) + .single() + + if (!model) { + throw new Error(error.message) + } + + return model +} + +export const createModel = async ( + model: TablesInsert<"models">, + workspace_id: string +) => { + const { data: createdModel, error } = await supabase + .from("models") + .insert([model]) + .select("*") + .single() + + if (error) { + throw new Error(error.message) + } + + await createModelWorkspace({ + user_id: model.user_id, + model_id: createdModel.id, + workspace_id: workspace_id + }) + + return createdModel +} + +export const createModels = async ( + models: TablesInsert<"models">[], + workspace_id: string +) => { + const { data: createdModels, error } = await supabase + .from("models") + .insert(models) + .select("*") + + if (error) { + throw new Error(error.message) + } + + await createModelWorkspaces( + createdModels.map(model => ({ + user_id: model.user_id, + model_id: model.id, + workspace_id + })) + ) + + return createdModels +} + +export const createModelWorkspace = async (item: { + user_id: string + model_id: string + workspace_id: string +}) => { + const { data: createdModelWorkspace, error } = await supabase + .from("model_workspaces") + .insert([item]) + .select("*") + .single() + + if (error) { + throw new Error(error.message) + } + + return createdModelWorkspace +} + +export const createModelWorkspaces = async ( + items: { user_id: string; model_id: string; workspace_id: string }[] +) => { + const { data: createdModelWorkspaces, error } = await supabase + .from("model_workspaces") + .insert(items) + .select("*") + + if (error) throw new Error(error.message) + + return createdModelWorkspaces +} + +export const updateModel = async ( + modelId: string, + model: TablesUpdate<"models"> +) => { + const { data: updatedModel, error } = await supabase + .from("models") + .update(model) + .eq("id", modelId) + .select("*") + .single() + + if (error) { + throw new Error(error.message) + } + + return updatedModel +} + +export const deleteModel = async (modelId: string) => { + const { error } = await supabase.from("models").delete().eq("id", modelId) + + if (error) { + throw new Error(error.message) + } + + return true +} + +export const deleteModelWorkspace = async ( + modelId: string, + workspaceId: string +) => { + const { error } = await supabase + .from("model_workspaces") + .delete() + .eq("model_id", modelId) + .eq("workspace_id", workspaceId) + + if (error) throw new Error(error.message) + + return true +} diff --git a/lib/models/fetch-models.ts b/lib/models/fetch-models.ts index 11df24c526..3cb928ca36 100644 --- a/lib/models/fetch-models.ts +++ b/lib/models/fetch-models.ts @@ -40,8 +40,6 @@ export const fetchHostedModels = async (profile: Tables<"profiles">) => { if (Array.isArray(models)) { modelsToAdd.push(...models) - } else { - console.warn(`LLM_LIST_MAP entry for '${provider}' is not an array.`) } } } diff --git a/package.json b/package.json index b4c9267f2e..8cf61f903f 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "format:write": "prettier --write \"{app,lib,db,components,context,types}/**/*.{ts,tsx}\" --cache", "format:check": "prettier --check \"{app,lib,db,components,context,types}**/*.{ts,tsx}\" --cache", "db-reset": "supabase db reset && npm run db-types", - "db-migrate": "supabase migration up", + "db-migrate": "supabase migration up && npm run db-types", "db-types": "supabase gen types typescript --local > supabase/types.ts", "db-pull": "supabase db remote commit", "db-push": "supabase db push", diff --git a/supabase/migrations/20240125194719_add_custom_models.sql b/supabase/migrations/20240125194719_add_custom_models.sql new file mode 100644 index 0000000000..6de0786a1c --- /dev/null +++ b/supabase/migrations/20240125194719_add_custom_models.sql @@ -0,0 +1,92 @@ +--------------- MODELS --------------- + +-- TABLE -- + +CREATE TABLE IF NOT EXISTS models ( + -- ID + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + + -- RELATIONSHIPS + user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + + -- OPTIONAL RELATIONSHIPS + folder_id UUID REFERENCES folders(id) ON DELETE SET NULL, + + -- METADATA + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ, + + -- SHARING + sharing TEXT NOT NULL DEFAULT 'private', + + -- REQUIRED + api_key TEXT NOT NULL CHECK (char_length(api_key) <= 1000), + base_url TEXT NOT NULL CHECK (char_length(base_url) <= 1000), + description TEXT NOT NULL CHECK (char_length(description) <= 500), + model_id TEXT NOT NULL CHECK (char_length(model_id) <= 1000), + name TEXT NOT NULL CHECK (char_length(name) <= 100) +); + +-- INDEXES -- + +CREATE INDEX models_user_id_idx ON models(user_id); + +-- RLS -- + +ALTER TABLE models ENABLE ROW LEVEL SECURITY; + +CREATE POLICY "Allow full access to own models" + ON models + USING (user_id = auth.uid()) + WITH CHECK (user_id = auth.uid()); + +CREATE POLICY "Allow view access to non-private models" + ON models + FOR SELECT + USING (sharing <> 'private'); + +-- TRIGGERS -- + +CREATE TRIGGER update_models_updated_at +BEFORE UPDATE ON models +FOR EACH ROW +EXECUTE PROCEDURE update_updated_at_column(); + +--------------- MODEL WORKSPACES --------------- + +-- TABLE -- + +CREATE TABLE IF NOT EXISTS model_workspaces ( + -- REQUIRED RELATIONSHIPS + user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + model_id UUID NOT NULL REFERENCES models(id) ON DELETE CASCADE, + workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, + + PRIMARY KEY(model_id, workspace_id), + + -- METADATA + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ +); + +-- INDEXES -- + +CREATE INDEX model_workspaces_user_id_idx ON model_workspaces(user_id); +CREATE INDEX model_workspaces_model_id_idx ON model_workspaces(model_id); +CREATE INDEX model_workspaces_workspace_id_idx ON model_workspaces(workspace_id); + +-- RLS -- + +ALTER TABLE model_workspaces ENABLE ROW LEVEL SECURITY; + +CREATE POLICY "Allow full access to own model_workspaces" + ON model_workspaces + USING (user_id = auth.uid()) + WITH CHECK (user_id = auth.uid()); + +-- TRIGGERS -- + +CREATE TRIGGER update_model_workspaces_updated_at +BEFORE UPDATE ON model_workspaces +FOR EACH ROW +EXECUTE PROCEDURE update_updated_at_column(); diff --git a/supabase/types.ts b/supabase/types.ts index df5ead6826..d5952fb395 100644 --- a/supabase/types.ts +++ b/supabase/types.ts @@ -870,6 +870,109 @@ export interface Database { } ] } + model_workspaces: { + Row: { + created_at: string + model_id: string + updated_at: string | null + user_id: string + workspace_id: string + } + Insert: { + created_at?: string + model_id: string + updated_at?: string | null + user_id: string + workspace_id: string + } + Update: { + created_at?: string + model_id?: string + updated_at?: string | null + user_id?: string + workspace_id?: string + } + Relationships: [ + { + foreignKeyName: "model_workspaces_model_id_fkey" + columns: ["model_id"] + isOneToOne: false + referencedRelation: "models" + referencedColumns: ["id"] + }, + { + foreignKeyName: "model_workspaces_user_id_fkey" + columns: ["user_id"] + isOneToOne: false + referencedRelation: "users" + referencedColumns: ["id"] + }, + { + foreignKeyName: "model_workspaces_workspace_id_fkey" + columns: ["workspace_id"] + isOneToOne: false + referencedRelation: "workspaces" + referencedColumns: ["id"] + } + ] + } + models: { + Row: { + api_key: string + base_url: string + created_at: string + description: string + folder_id: string | null + id: string + model_id: string + name: string + sharing: string + updated_at: string | null + user_id: string + } + Insert: { + api_key: string + base_url: string + created_at?: string + description: string + folder_id?: string | null + id?: string + model_id: string + name: string + sharing?: string + updated_at?: string | null + user_id: string + } + Update: { + api_key?: string + base_url?: string + created_at?: string + description?: string + folder_id?: string | null + id?: string + model_id?: string + name?: string + sharing?: string + updated_at?: string | null + user_id?: string + } + Relationships: [ + { + foreignKeyName: "models_folder_id_fkey" + columns: ["folder_id"] + isOneToOne: false + referencedRelation: "folders" + referencedColumns: ["id"] + }, + { + foreignKeyName: "models_user_id_fkey" + columns: ["user_id"] + isOneToOne: false + referencedRelation: "users" + referencedColumns: ["id"] + } + ] + } preset_workspaces: { Row: { created_at: string diff --git a/types/content-type.ts b/types/content-type.ts index 94a2adce7f..804375fa2c 100644 --- a/types/content-type.ts +++ b/types/content-type.ts @@ -6,3 +6,4 @@ export type ContentType = | "collections" | "assistants" | "tools" + | "models" diff --git a/types/models.ts b/types/models.ts index 215ffe0c4d..698c08f0e9 100644 --- a/types/models.ts +++ b/types/models.ts @@ -6,3 +6,4 @@ export type ModelProvider = | "perplexity" | "ollama" | "openrouter" + | "custom" diff --git a/types/sidebar-data.ts b/types/sidebar-data.ts index 240d2af828..1e8b00088f 100644 --- a/types/sidebar-data.ts +++ b/types/sidebar-data.ts @@ -7,6 +7,8 @@ export type DataListType = | Tables<"prompts">[] | Tables<"files">[] | Tables<"assistants">[] + | Tables<"tools">[] + | Tables<"models">[] export type DataItemType = | Tables<"collections"> @@ -15,3 +17,5 @@ export type DataItemType = | Tables<"prompts"> | Tables<"files"> | Tables<"assistants"> + | Tables<"tools"> + | Tables<"models">