diff --git a/app/api/chat/google/route.ts b/app/api/chat/google/route.ts index ad79139646..cb9820ee88 100644 --- a/app/api/chat/google/route.ts +++ b/app/api/chat/google/route.ts @@ -44,7 +44,6 @@ export async function POST(request: Request) { return new Response(readableStream, { headers: { "Content-Type": "text/plain" } }) - } catch (error: any) { let errorMessage = error.message || "An unexpected error occurred" const errorCode = error.status || 500 diff --git a/components/chat/chat-files-display.tsx b/components/chat/chat-files-display.tsx index a0675053c8..64b6817d45 100644 --- a/components/chat/chat-files-display.tsx +++ b/components/chat/chat-files-display.tsx @@ -2,7 +2,7 @@ import { ChatbotUIContext } from "@/context/context" import { getFileFromStorage } from "@/db/storage/files" import useHotkey from "@/lib/hooks/use-hotkey" import { cn } from "@/lib/utils" -import { ChatFile, MessageImage } from "@/types" +import { ChatFile } from "@/types" import { IconCircleFilled, IconFileFilled, @@ -16,9 +16,8 @@ import { IconX } from "@tabler/icons-react" import Image from "next/image" -import { FC, useContext, useState } from "react" +import { FC, useContext } from "react" import { Button } from "../ui/button" -import { FilePreview } from "../ui/file-preview" import { WithTooltip } from "../ui/with-tooltip" import { ChatRetrievalSettings } from "./chat-retrieval-settings" @@ -40,13 +39,12 @@ export const ChatFilesDisplay: FC = ({}) => { chatImages, setChatImages, setChatFiles, - setUseRetrieval + setUseRetrieval, + setShowFilePreview, + setFilePreviewItem, + setFilePreviewType } = useContext(ChatbotUIContext) - const [selectedFile, setSelectedFile] = useState(null) - const [selectedImage, setSelectedImage] = useState(null) - const [showPreview, setShowPreview] = useState(false) - const messageImages = [ ...newMessageImages.filter( image => @@ -74,30 +72,6 @@ export const ChatFilesDisplay: FC = ({}) => { return showFilesDisplay && combinedMessageFiles.length > 0 ? ( <> - {showPreview && selectedImage && ( - { - setShowPreview(isOpen) - setSelectedImage(null) - }} - /> - )} - - {showPreview && selectedFile && ( - { - setShowPreview(isOpen) - setSelectedFile(null) - }} - /> - )} -
- - {showImagePreview && selectedImage && ( - { - setShowImagePreview(isOpen) - setSelectedImage(null) - }} - /> - )} - - {showFileItemPreview && selectedFileItem && ( - { - setShowFileItemPreview(isOpen) - setSelectedFileItem(null) - }} - /> - )}
) } diff --git a/components/sidebar/right-sidebar.tsx b/components/sidebar/right-sidebar.tsx new file mode 100644 index 0000000000..1bacf3238c --- /dev/null +++ b/components/sidebar/right-sidebar.tsx @@ -0,0 +1,34 @@ +import { ChatbotUIContext } from "@/context/context" +import { IconX } from "@tabler/icons-react" +import { FC, useContext } from "react" +import { Button } from "../ui/button" +import { FilePreview } from "../ui/file-preview" + +interface RightSidebarProps {} + +export const RightSidebar: FC = ({}) => { + const { + showFilePreview, + filePreviewItem, + filePreviewType, + setShowFilePreview + } = useContext(ChatbotUIContext) + + if (!showFilePreview) return null + + return ( +
+
+ +
+ + +
+ ) +} diff --git a/components/ui/dashboard.tsx b/components/ui/dashboard.tsx index b5fbbe9aec..87d2a416c7 100644 --- a/components/ui/dashboard.tsx +++ b/components/ui/dashboard.tsx @@ -1,17 +1,24 @@ "use client" +import { RightSidebar } from "@/components/sidebar/right-sidebar" import { Sidebar } from "@/components/sidebar/sidebar" import { SidebarSwitcher } from "@/components/sidebar/sidebar-switcher" import { Button } from "@/components/ui/button" +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup +} from "@/components/ui/resizable" 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 { FC, useContext, useState } from "react" import { useSelectFileHandler } from "../chat/chat-hooks/use-select-file-handler" import { CommandK } from "../utility/command-k" +import { ChatbotUIContext } from "@/context/context" export const SIDEBAR_WIDTH = 350 @@ -35,6 +42,7 @@ export const Dashboard: FC = ({ children }) => { const [showSidebar, setShowSidebar] = useState( localStorage.getItem("showSidebar") === "true" ) + const { showFilePreview } = useContext(ChatbotUIContext) const [isDragging, setIsDragging] = useState(false) const onFileDrop = (event: React.DragEvent) => { @@ -67,67 +75,90 @@ export const Dashboard: FC = ({ children }) => { localStorage.setItem("showSidebar", String(!showSidebar)) } + const shouldShowSidebar = showSidebar && !showFilePreview + return (
-
- {showSidebar && ( - { - setContentType(tabValue as ContentType) - router.replace(`${pathname}?tab=${tabValue}`) + + +
- + {shouldShowSidebar && ( + { + setContentType(tabValue as ContentType) + router.replace(`${pathname}?tab=${tabValue}`) + }} + > + + + + + )} +
- -
- )} -
- -
- {isDragging ? ( -
- drop file here +
+ {isDragging ? ( +
+ drop file here +
+ ) : ( + children + )} + + {!showFilePreview && ( + + )}
- ) : ( - children + + + {showFilePreview && ( + <> + + + + + )} - - -
+
) } diff --git a/components/ui/file-preview.tsx b/components/ui/file-preview.tsx index 2bfbc70378..788e40f552 100644 --- a/components/ui/file-preview.tsx +++ b/components/ui/file-preview.tsx @@ -1,3 +1,4 @@ +import { MessageMarkdown } from "@/components/messages/message-markdown" import { cn } from "@/lib/utils" import { Tables } from "@/supabase/types" import { ChatFile, MessageImage } from "@/types" @@ -5,64 +6,51 @@ import { IconFileFilled } from "@tabler/icons-react" import Image from "next/image" import { FC } from "react" import { DrawingCanvas } from "../utility/drawing-canvas" -import { Dialog, DialogContent } from "./dialog" interface FilePreviewProps { - type: "image" | "file" | "file_item" - item: ChatFile | MessageImage | Tables<"file_items"> - isOpen: boolean - onOpenChange: (isOpen: boolean) => void + type: "image" | "file" | "file_item" | null + item: ChatFile | MessageImage | Tables<"file_items"> | null } -export const FilePreview: FC = ({ - type, - item, - isOpen, - onOpenChange -}) => { +export const FilePreview: FC = ({ type, item }) => { + if (!type || !item) return null + return ( - - - {(() => { - if (type === "image") { - const imageItem = item as MessageImage + <> + {(() => { + if (type === "image") { + const imageItem = item as MessageImage - return imageItem.file ? ( - - ) : ( - File image - ) - } else if (type === "file_item") { - const fileItem = item as Tables<"file_items"> - return ( -
-
{fileItem.content}
-
- ) - } else if (type === "file") { - return ( -
- -
- ) - } - })()} -
-
+ return imageItem.file ? ( + + ) : ( + File image + ) + } else if (type === "file_item") { + const fileItem = item as Tables<"file_items"> + return ( +
+ +
+ ) + } else if (type === "file") { + return ( +
+ +
+ ) + } + })()} + ) } diff --git a/components/ui/resizable.tsx b/components/ui/resizable.tsx new file mode 100644 index 0000000000..5ef53c615c --- /dev/null +++ b/components/ui/resizable.tsx @@ -0,0 +1,50 @@ +"use client" +import * as React from "react" +import { GripVerticalIcon } from "lucide-react" +import * as ResizablePrimitive from "react-resizable-panels" +import { cn } from "@/lib/utils" +function ResizablePanelGroup({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} +function ResizablePanel({ + ...props +}: React.ComponentProps) { + return +} +function ResizableHandle({ + withHandle, + className, + ...props +}: React.ComponentProps & { + withHandle?: boolean +}) { + return ( + div]:rotate-90", + className + )} + {...props} + > + {withHandle && ( +
+ +
+ )} +
+ ) +} +export { ResizablePanelGroup, ResizablePanel, ResizableHandle } diff --git a/components/utility/global-state.tsx b/components/utility/global-state.tsx index 126babf133..79ea9da89f 100644 --- a/components/utility/global-state.tsx +++ b/components/utility/global-state.tsx @@ -115,6 +115,15 @@ export const GlobalState: FC = ({ children }) => { const [newMessageImages, setNewMessageImages] = useState([]) const [showFilesDisplay, setShowFilesDisplay] = useState(false) + // FILE PREVIEW STORE + const [showFilePreview, setShowFilePreview] = useState(false) + const [filePreviewItem, setFilePreviewItem] = useState< + ChatFile | MessageImage | Tables<"file_items"> | null + >(null) + const [filePreviewType, setFilePreviewType] = useState< + "image" | "file" | "file_item" | null + >(null) + // RETIEVAL STORE const [useRetrieval, setUseRetrieval] = useState(true) const [sourceCount, setSourceCount] = useState(4) @@ -312,6 +321,14 @@ export const GlobalState: FC = ({ children }) => { showFilesDisplay, setShowFilesDisplay, + // FILE PREVIEW STORE + showFilePreview, + setShowFilePreview, + filePreviewItem, + setFilePreviewItem, + filePreviewType, + setFilePreviewType, + // RETRIEVAL STORE useRetrieval, setUseRetrieval, diff --git a/context/context.tsx b/context/context.tsx index 88bb0ec12b..0c6e1aee2d 100644 --- a/context/context.tsx +++ b/context/context.tsx @@ -125,6 +125,18 @@ interface ChatbotUIContext { showFilesDisplay: boolean setShowFilesDisplay: Dispatch> + // FILE PREVIEW STORE + showFilePreview: boolean + setShowFilePreview: Dispatch> + filePreviewItem: ChatFile | MessageImage | Tables<"file_items"> | null + setFilePreviewItem: Dispatch< + SetStateAction | null> + > + filePreviewType: "image" | "file" | "file_item" | null + setFilePreviewType: Dispatch< + SetStateAction<"image" | "file" | "file_item" | null> + > + // RETRIEVAL STORE useRetrieval: boolean setUseRetrieval: Dispatch> @@ -251,6 +263,14 @@ export const ChatbotUIContext = createContext({ showFilesDisplay: false, setShowFilesDisplay: () => {}, + // FILE PREVIEW STORE + showFilePreview: false, + setShowFilePreview: () => {}, + filePreviewItem: null, + setFilePreviewItem: () => {}, + filePreviewType: null, + setFilePreviewType: () => {}, + // RETRIEVAL STORE useRetrieval: false, setUseRetrieval: () => {}, diff --git a/db/files.ts b/db/files.ts index 68ad01e343..0b69625e68 100644 --- a/db/files.ts +++ b/db/files.ts @@ -94,7 +94,10 @@ export const createFile = async ( let validFilename = fileRecord.name.replace(/[^a-z0-9.]/gi, "_").toLowerCase() const extension = file.name.split(".").pop() const extensionIndex = validFilename.lastIndexOf(".") - const baseName = validFilename.substring(0, (extensionIndex < 0) ? undefined : extensionIndex) + const baseName = validFilename.substring( + 0, + extensionIndex < 0 ? undefined : extensionIndex + ) const maxBaseNameLength = 100 - (extension?.length || 0) - 1 if (baseName.length > maxBaseNameLength) { fileRecord.name = baseName.substring(0, maxBaseNameLength) + "." + extension diff --git a/lib/build-prompt.ts b/lib/build-prompt.ts index ddb0e76377..8bda846f9f 100644 --- a/lib/build-prompt.ts +++ b/lib/build-prompt.ts @@ -184,36 +184,35 @@ function buildRetrievalText(fileItems: Tables<"file_items">[]) { } function adaptSingleMessageForGoogleGemini(message: any) { - let adaptedParts = [] let rawParts = [] - if(!Array.isArray(message.content)) { - rawParts.push({type: 'text', text: message.content}) + if (!Array.isArray(message.content)) { + rawParts.push({ type: "text", text: message.content }) } else { rawParts = message.content } - for(let i = 0; i < rawParts.length; i++) { + for (let i = 0; i < rawParts.length; i++) { let rawPart = rawParts[i] - if(rawPart.type == 'text') { - adaptedParts.push({text: rawPart.text}) - } else if(rawPart.type === 'image_url') { + if (rawPart.type == "text") { + adaptedParts.push({ text: rawPart.text }) + } else if (rawPart.type === "image_url") { adaptedParts.push({ inlineData: { data: getBase64FromDataURL(rawPart.image_url.url), - mimeType: getMediaTypeFromDataURL(rawPart.image_url.url), + mimeType: getMediaTypeFromDataURL(rawPart.image_url.url) } }) } } - let role = 'user' - if(["user", "system"].includes(message.role)) { - role = 'user' - } else if(message.role === 'assistant') { - role = 'model' + let role = "user" + if (["user", "system"].includes(message.role)) { + role = "user" + } else if (message.role === "assistant") { + role = "model" } return { @@ -222,29 +221,29 @@ function adaptSingleMessageForGoogleGemini(message: any) { } } -function adaptMessagesForGeminiVision( - messages: any[] -) { +function adaptMessagesForGeminiVision(messages: any[]) { // Gemini Pro Vision cannot process multiple messages // Reformat, using all texts and last visual only const basePrompt = messages[0].parts[0].text const baseRole = messages[0].role - const lastMessage = messages[messages.length-1] - const visualMessageParts = lastMessage.parts; - let visualQueryMessages = [{ - role: "user", - parts: [ - `${baseRole}:\n${basePrompt}\n\nuser:\n${visualMessageParts[0].text}\n\n`, - visualMessageParts.slice(1) - ] - }] + const lastMessage = messages[messages.length - 1] + const visualMessageParts = lastMessage.parts + let visualQueryMessages = [ + { + role: "user", + parts: [ + `${baseRole}:\n${basePrompt}\n\nuser:\n${visualMessageParts[0].text}\n\n`, + visualMessageParts.slice(1) + ] + } + ] return visualQueryMessages } export async function adaptMessagesForGoogleGemini( payload: ChatPayload, - messages: any[] + messages: any[] ) { let geminiMessages = [] for (let i = 0; i < messages.length; i++) { @@ -252,9 +251,8 @@ export async function adaptMessagesForGoogleGemini( geminiMessages.push(adaptedMessage) } - if(payload.chatSettings.model === "gemini-pro-vision") { + if (payload.chatSettings.model === "gemini-pro-vision") { geminiMessages = adaptMessagesForGeminiVision(geminiMessages) } return geminiMessages } - diff --git a/lib/chat-setting-limits.ts b/lib/chat-setting-limits.ts index c802bd657b..1dcde08490 100644 --- a/lib/chat-setting-limits.ts +++ b/lib/chat-setting-limits.ts @@ -47,7 +47,7 @@ export const CHAT_SETTING_LIMITS: Record = { }, // GOOGLE MODELS - + "gemini-1.5-flash": { MIN_TEMPERATURE: 0.0, MAX_TEMPERATURE: 1.0, diff --git a/lib/models/llm/google-llm-list.ts b/lib/models/llm/google-llm-list.ts index d039eda4af..833eb7ec2b 100644 --- a/lib/models/llm/google-llm-list.ts +++ b/lib/models/llm/google-llm-list.ts @@ -44,4 +44,9 @@ const GEMINI_PRO_VISION: LLM = { imageInput: true } -export const GOOGLE_LLM_LIST: LLM[] = [GEMINI_PRO, GEMINI_PRO_VISION, GEMINI_1_5_PRO, GEMINI_1_5_FLASH] +export const GOOGLE_LLM_LIST: LLM[] = [ + GEMINI_PRO, + GEMINI_PRO_VISION, + GEMINI_1_5_PRO, + GEMINI_1_5_FLASH +] diff --git a/package-lock.json b/package-lock.json index 2b03be3258..13dc388ac8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -71,6 +71,7 @@ "react-hook-form": "^7.48.2", "react-i18next": "^14.0.0", "react-markdown": "^9.0.1", + "react-resizable-panels": "^3.0.6", "react-syntax-highlighter": "^15.5.0", "react-textarea-autosize": "^8.5.3", "remark-gfm": "^4.0.0", @@ -14842,6 +14843,55 @@ "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==" }, + "node_modules/playwright": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0.tgz", + "integrity": "sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "playwright-core": "1.55.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0.tgz", + "integrity": "sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/postcss": { "version": "8.4.33", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", @@ -15474,6 +15524,16 @@ } } }, + "node_modules/react-resizable-panels": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-3.0.6.tgz", + "integrity": "sha512-b3qKHQ3MLqOgSS+FRYKapNkJZf5EQzuf6+RLiq1/IlTHw99YrZ2NJZLk4hQIzTnnIkRg2LUqyVinu6YWWpUYew==", + "license": "MIT", + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/react-style-singleton": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", diff --git a/package.json b/package.json index 1ee82b00f3..72ccd949a3 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "react-hook-form": "^7.48.2", "react-i18next": "^14.0.0", "react-markdown": "^9.0.1", + "react-resizable-panels": "^3.0.6", "react-syntax-highlighter": "^15.5.0", "react-textarea-autosize": "^8.5.3", "remark-gfm": "^4.0.0",