diff --git a/web/src/app/chat/ChatIntro.tsx b/web/src/app/chat/ChatIntro.tsx index 021224ff7..d44be4da5 100644 --- a/web/src/app/chat/ChatIntro.tsx +++ b/web/src/app/chat/ChatIntro.tsx @@ -7,6 +7,7 @@ import { FiBookmark, FiCpu, FiInfo, FiX, FiZoomIn } from "react-icons/fi"; import { HoverPopup } from "@/components/HoverPopup"; import { Modal } from "@/components/Modal"; import { useState } from "react"; +import { FaCaretDown, FaCaretRight } from "react-icons/fa"; import { Logo } from "@/components/Logo"; const MAX_PERSONAS_TO_DISPLAY = 4; @@ -37,6 +38,8 @@ export function ChatIntro({ }) { const availableSourceMetadata = getSourceMetadataForSources(availableSources); + const [displaySources, setDisplaySources] = useState(false); + return ( <>
@@ -90,12 +93,13 @@ export function ChatIntro({
)} + {availableSources.length > 0 && ( -
+

Connected Sources:{" "}

-
+
{availableSourceMetadata.map((sourceMetadata) => ( ( + documentSidebarInitialWidth || parseInt(SIDEBAR_WIDTH_CONST) + ); + + const updateSidebarWidth = (newWidth: number) => { + setUsedSidebarWidth(newWidth); + if (sidebarElementRef.current && innerSidebarElementRef.current) { + sidebarElementRef.current.style.transition = ""; + sidebarElementRef.current.style.width = `${newWidth}px`; + innerSidebarElementRef.current.style.width = `${newWidth}px`; + } + }; + const [chatSessionId, setChatSessionId] = useState( existingChatSessionId ); @@ -472,6 +488,7 @@ export function ChatPage({ } } }; + useEffect(() => { adjustDocumentSidebarWidth(); // Adjust the width on initial render window.addEventListener("resize", adjustDocumentSidebarWidth); // Add resize event listener @@ -865,12 +882,26 @@ export function ChatPage({ router.push("/search"); } + const [showDocSidebar, setShowDocSidebar] = useState(true); // State to track if sidebar is open + + const toggleSidebar = () => { + if (sidebarElementRef.current) { + sidebarElementRef.current.style.transition = "width 0.3s ease-in-out"; + + sidebarElementRef.current.style.width = showDocSidebar + ? "0px" + : `${usedSidebarWidth}px`; + } + + setShowDocSidebar((showDocSidebar) => !showDocSidebar); // Toggle the state which will in turn toggle the class + }; + const retrievalDisabled = !personaIncludesRetrieval(livePersona); + const sidebarElementRef = useRef(null); + const innerSidebarElementRef = useRef(null); + return ( <> - {/*
-
-
*/} @@ -886,7 +917,7 @@ export function ChatPage({ openedFolders={openedFolders} /> -
+
{popup} {currentFeedback && ( {/* */} @@ -963,7 +997,7 @@ export function ChatPage({ />
-
+
{chatSessionId !== null && (
setSharingModalVisible(true)} @@ -979,8 +1013,16 @@ export function ChatPage({
)} -
+
+ {!retrievalDisabled && !showDocSidebar && ( + + )}
@@ -1047,7 +1089,6 @@ export function ChatPage({ newCompleteMessageMap ); setSelectedMessageForDocDisplay(messageId); - // set message as latest so we can edit this message // and so it sticks around on page reload setMessageAsLatest(messageId); @@ -1076,9 +1117,7 @@ export function ChatPage({ citedDocuments={getCitedDocumentsFromMessage( message )} - toolCall={ - message.toolCalls && message.toolCalls[0] - } + toolCall={message?.toolCalls?.[0]} isComplete={ i !== messageHistory.length - 1 || !isStreaming @@ -1273,21 +1312,31 @@ export function ChatPage({
{!retrievalDisabled ? ( - - - + + toggleSidebar()} + selectedMessage={aiMessage} + selectedDocuments={selectedDocuments} + toggleDocumentSelection={toggleDocumentSelection} + clearSelectedDocuments={clearSelectedDocuments} + selectedDocumentTokens={selectedDocumentTokens} + maxTokens={maxTokens} + isLoading={isFetchingChatMessages} + /> + +
) : // Another option is to use a div with the width set to the initial width, so that the // chat section appears in the same place as before //
diff --git a/web/src/app/chat/ChatPersonaSelector.tsx b/web/src/app/chat/ChatPersonaSelector.tsx index 33ef61ef1..55cb8086f 100644 --- a/web/src/app/chat/ChatPersonaSelector.tsx +++ b/web/src/app/chat/ChatPersonaSelector.tsx @@ -130,8 +130,8 @@ export function ChatPersonaSelector({
} > -
-
+
+
{currentlySelectedPersona?.name || "Default"}
diff --git a/web/src/app/chat/documentSidebar/DocumentSidebar.tsx b/web/src/app/chat/documentSidebar/DocumentSidebar.tsx index c5913d406..aa962a1f9 100644 --- a/web/src/app/chat/documentSidebar/DocumentSidebar.tsx +++ b/web/src/app/chat/documentSidebar/DocumentSidebar.tsx @@ -7,33 +7,41 @@ import { SelectedDocumentDisplay } from "./SelectedDocumentDisplay"; import { removeDuplicateDocs } from "@/lib/documentUtils"; import { BasicSelectable } from "@/components/BasicClickable"; import { Message, RetrievalType } from "../interfaces"; +import { SIDEBAR_WIDTH } from "@/lib/constants"; import { HoverPopup } from "@/components/HoverPopup"; -import { HEADER_PADDING } from "@/lib/constants"; +import { TbLayoutSidebarLeftExpand } from "react-icons/tb"; +import { ForwardedRef, forwardRef } from "react"; function SectionHeader({ name, icon, + closeHeader, }: { name: string; icon: React.FC<{ className: string }>; + closeHeader?: () => void; }) { return ( -
- {icon({ className: "my-auto mr-1" })} - {name} +
+
+

+ {icon({ className: "my-auto mr-1" })} + {name} +

+ {closeHeader && ( + + )} +
); } -export function DocumentSidebar({ - selectedMessage, - selectedDocuments, - toggleDocumentSelection, - clearSelectedDocuments, - selectedDocumentTokens, - maxTokens, - isLoading, -}: { +interface DocumentSidebarProps { + closeSidebar: () => void; selectedMessage: Message | null; selectedDocuments: DanswerDocument[] | null; toggleDocumentSelection: (document: DanswerDocument) => void; @@ -41,174 +49,203 @@ export function DocumentSidebar({ selectedDocumentTokens: number; maxTokens: number; isLoading: boolean; -}) { - const { popup, setPopup } = usePopup(); + initialWidth: number; +} - const selectedMessageRetrievalType = selectedMessage?.retrievalType || null; +export const DocumentSidebar = forwardRef( + ( + { + closeSidebar, + selectedMessage, + selectedDocuments, + toggleDocumentSelection, + clearSelectedDocuments, + selectedDocumentTokens, + maxTokens, + isLoading, + initialWidth, + }, + ref: ForwardedRef + ) => { + const { popup, setPopup } = usePopup(); - const selectedDocumentIds = - selectedDocuments?.map((document) => document.document_id) || []; + const selectedMessageRetrievalType = selectedMessage?.retrievalType || null; - const currentDocuments = selectedMessage?.documents || null; - const dedupedDocuments = removeDuplicateDocs(currentDocuments || []); + const selectedDocumentIds = + selectedDocuments?.map((document) => document.document_id) || []; - // NOTE: do not allow selection if less than 75 tokens are left - // this is to prevent the case where they are able to select the doc - // but it basically is unused since it's truncated right at the very - // start of the document (since title + metadata + misc overhead) takes up - // space - const tokenLimitReached = selectedDocumentTokens > maxTokens - 75; - return ( -
- {popup} + const currentDocuments = selectedMessage?.documents || null; + const dedupedDocuments = removeDuplicateDocs(currentDocuments || []); -
-
- -
+ // NOTE: do not allow selection if less than 75 tokens are left + // this is to prevent the case where they are able to select the doc + // but it basically is unused since it's truncated right at the very + // start of the document (since title + metadata + misc overhead) takes up + // space + const tokenLimitReached = selectedDocumentTokens > maxTokens - 75; - {currentDocuments ? ( -
-
- {dedupedDocuments.length > 0 ? ( - dedupedDocuments.map((document, ind) => ( -
- { - toggleDocumentSelection( - dedupedDocuments.find( - (document) => document.document_id === documentId - )! - ); - }} - tokenLimitReached={tokenLimitReached} - /> -
- )) - ) : ( -
- No documents found for the query. -
- )} + return ( +
+
+ {popup} + +
+
+
-
- ) : ( - !isLoading && ( -
- - When you run ask a question, the retrieved documents will show - up here! - -
- ) - )} -
-
-
-
- - - {tokenLimitReached && ( -
-
- - } - popupContent={ - - Over LLM context length by:{" "} - {selectedDocumentTokens - maxTokens} tokens -
-
- {selectedDocuments && selectedDocuments.length > 0 && ( - <> - Truncating: " - - { - selectedDocuments[selectedDocuments.length - 1] - .semantic_identifier - } - - " - - )} -
- } - direction="left" - /> + {currentDocuments ? ( +
+
+ {dedupedDocuments.length > 0 ? ( + dedupedDocuments.map((document, ind) => ( +
+ { + toggleDocumentSelection( + dedupedDocuments.find( + (document) => + document.document_id === documentId + )! + ); + }} + tokenLimitReached={tokenLimitReached} + /> +
+ )) + ) : ( +
+ No documents found for the query. +
+ )}
+ ) : ( + !isLoading && ( +
+ + When you run ask a question, the retrieved documents will + show up here! + +
+ ) )}
- {selectedDocuments && selectedDocuments.length > 0 && ( -
- De-Select All +
+
+
+ + {tokenLimitReached && ( +
+
+ + } + popupContent={ + + Over LLM context length by:{" "} + {selectedDocumentTokens - maxTokens} tokens +
+
+ {selectedDocuments && + selectedDocuments.length > 0 && ( + <> + Truncating: " + + { + selectedDocuments[ + selectedDocuments.length - 1 + ].semantic_identifier + } + + " + + )} +
+ } + direction="left" + /> +
+
+ )} +
+ {selectedDocuments && selectedDocuments.length > 0 && ( +
+ + De-Select All + +
+ )}
- )} -
- {selectedDocuments && selectedDocuments.length > 0 ? ( -
- {selectedDocuments.map((document) => ( - { - toggleDocumentSelection( - dedupedDocuments.find( - (document) => document.document_id === documentId - )! - ); - }} - /> - ))} + {selectedDocuments && selectedDocuments.length > 0 ? ( +
+ {selectedDocuments.map((document) => ( + { + toggleDocumentSelection( + dedupedDocuments.find( + (document) => document.document_id === documentId + )! + ); + }} + /> + ))} +
+ ) : ( + !isLoading && ( + + Select documents from the retrieved documents section to chat + specifically with them! + + ) + )}
- ) : ( - !isLoading && ( - - Select documents from the retrieved documents section to chat - specifically with them! - - ) - )} +
-
- ); -} + ); + } +); + +DocumentSidebar.displayName = "DocumentSidebar"; diff --git a/web/src/app/chat/files/documents/DocumentPreview.tsx b/web/src/app/chat/files/documents/DocumentPreview.tsx index a38e36872..7e584f34d 100644 --- a/web/src/app/chat/files/documents/DocumentPreview.tsx +++ b/web/src/app/chat/files/documents/DocumentPreview.tsx @@ -26,7 +26,7 @@ export function DocumentPreview({ flex items-center p-2 - bg-hover-light + bg-hover border border-border rounded-md diff --git a/web/src/app/chat/input/ChatInputBar.tsx b/web/src/app/chat/input/ChatInputBar.tsx index 278acfabb..984fa0726 100644 --- a/web/src/app/chat/input/ChatInputBar.tsx +++ b/web/src/app/chat/input/ChatInputBar.tsx @@ -84,9 +84,10 @@ export function ChatInputBar({ flex flex-col border - border-border + border-border-medium rounded-lg - bg-background + overflow-hidden + bg-background-weak [&:has(textarea:focus)]::ring-1 [&:has(textarea:focus)]::ring-black " @@ -118,13 +119,14 @@ export function ChatInputBar({ shrink resize-none border-0 - bg-transparent + bg-background-weak ${ textAreaRef.current && textAreaRef.current.scrollHeight > MAX_INPUT_HEIGHT ? "overflow-y-auto mt-2" : "" } + overflow-hidden whitespace-normal break-word overscroll-contain @@ -157,7 +159,7 @@ export function ChatInputBar({ }} suppressContentEditableWarning={true} /> -
+
{ setEditedContent(e.target.value); - e.target.style.height = "auto"; e.target.style.height = `${e.target.scrollHeight}px`; }} onKeyDown={(e) => { @@ -514,6 +514,10 @@ export const HumanMessage = ({ setEditedContent(content); setIsEditing(false); } + // Submit edit if "Command Enter" is pressed, like in ChatGPT + if (e.key === "Enter" && e.metaKey) { + handleEditSubmit(); + } }} // ref={(textarea) => { // if (textarea) { diff --git a/web/src/app/chat/sessionSidebar/ChatSessionDisplay.tsx b/web/src/app/chat/sessionSidebar/ChatSessionDisplay.tsx index 99084baa6..f7c0f3279 100644 --- a/web/src/app/chat/sessionSidebar/ChatSessionDisplay.tsx +++ b/web/src/app/chat/sessionSidebar/ChatSessionDisplay.tsx @@ -198,7 +198,7 @@ export function ChatSessionDisplay({
)} {!isSelected && !delayedSkipGradient && ( -
+
)} diff --git a/web/src/app/chat/sessionSidebar/ChatSidebar.tsx b/web/src/app/chat/sessionSidebar/ChatSidebar.tsx index 089aaad4c..9b0d94020 100644 --- a/web/src/app/chat/sessionSidebar/ChatSidebar.tsx +++ b/web/src/app/chat/sessionSidebar/ChatSidebar.tsx @@ -7,10 +7,12 @@ import Image from "next/image"; import { useRouter } from "next/navigation"; import { BasicClickable, BasicSelectable } from "@/components/BasicClickable"; import { ChatSession } from "../interfaces"; + import { - NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA, NEXT_PUBLIC_DO_NOT_USE_TOGGLE_OFF_DANSWER_POWERED, + NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA, } from "@/lib/constants"; + import { ChatTab } from "./ChatTab"; import { Folder } from "../folders/interfaces"; import { createFolder } from "../folders/FolderManagement"; @@ -56,8 +58,10 @@ export const ChatSidebar = ({ {popup}
{children} @@ -65,11 +66,13 @@ export function BasicSelectable({ selected, hasBorder, fullWidth = false, + padding = true, }: { children: string | JSX.Element; selected: boolean; hasBorder?: boolean; fullWidth?: boolean; + padding?: boolean; }) { return (
+
setUserInfoVisible(!userInfoVisible)} className="flex cursor-pointer" > -
+
{user && user.email ? user.email[0].toUpperCase() : "A"}
diff --git a/web/src/components/admin/Layout.tsx b/web/src/components/admin/Layout.tsx index 561d1a31d..27e446483 100644 --- a/web/src/components/admin/Layout.tsx +++ b/web/src/components/admin/Layout.tsx @@ -69,7 +69,7 @@ export async function Layout({ children }: { children: React.ReactNode }) {
-
+
{collection.items.map((item) => ( - diff --git a/web/src/components/resizable/ResizableSection.tsx b/web/src/components/resizable/ResizableSection.tsx index 81e2c6ea0..64921ab4e 100644 --- a/web/src/components/resizable/ResizableSection.tsx +++ b/web/src/components/resizable/ResizableSection.tsx @@ -20,11 +20,13 @@ function applyMinAndMax( } export function ResizableSection({ + updateSidebarWidth, children, intialWidth, minWidth, maxWidth, }: { + updateSidebarWidth?: (newWidth: number) => void; children: JSX.Element; intialWidth: number; minWidth: number; @@ -56,6 +58,9 @@ export function ResizableSection({ const delta = mouseMoveEvent.clientX - startX; let newWidth = applyMinAndMax(width - delta, minWidth, maxWidth); setWidth(newWidth); + if (updateSidebarWidth) { + updateSidebarWidth(newWidth); + } Cookies.set(DOCUMENT_SIDEBAR_WIDTH_COOKIE_NAME, newWidth.toString(), { path: "/", }); diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts index fc3b187c4..b5a31394b 100644 --- a/web/src/lib/constants.ts +++ b/web/src/lib/constants.ts @@ -1,6 +1,9 @@ export type AuthType = "disabled" | "basic" | "google_oauth" | "oidc" | "saml"; export const HOST_URL = process.env.WEB_DOMAIN || "http://127.0.0.1:3000"; +export const HEADER_HEIGHT = "h-16"; +export const SUB_HEADER = "h-12"; + export const INTERNAL_URL = process.env.INTERNAL_URL || "http://127.0.0.1:8080"; export const NEXT_PUBLIC_DISABLE_STREAMING = process.env.NEXT_PUBLIC_DISABLE_STREAMING?.toLowerCase() === "true"; @@ -20,7 +23,8 @@ export const GOOGLE_DRIVE_AUTH_IS_ADMIN_COOKIE_NAME = export const SEARCH_TYPE_COOKIE_NAME = "search_type"; -export const HEADER_PADDING = `pt-[64px]`; +export const SIDEBAR_WIDTH_CONST = "350px"; +export const SIDEBAR_WIDTH = `w-[350px]`; export const LOGOUT_DISABLED = process.env.NEXT_PUBLIC_DISABLE_LOGOUT?.toLowerCase() === "true"; @@ -31,6 +35,12 @@ export const LOGOUT_DISABLED = // it will not be accurate (will always be false). export const SERVER_SIDE_ONLY__PAID_ENTERPRISE_FEATURES_ENABLED = process.env.ENABLE_PAID_ENTERPRISE_EDITION_FEATURES?.toLowerCase() === "true"; +// NOTE: since this is a `NEXT_PUBLIC_` variable, it will be set at +// build-time +// TODO: consider moving this to an API call so that the api_server +// can be the single source of truth +export const EE_ENABLED = + process.env.NEXT_PUBLIC_ENABLE_PAID_EE_FEATURES?.toLowerCase() === "true"; export const CUSTOM_ANALYTICS_ENABLED = process.env.CUSTOM_ANALYTICS_SECRET_KEY ? true diff --git a/web/tailwind-themes/tailwind.config.js b/web/tailwind-themes/tailwind.config.js index 12fc3f339..8986a5357 100644 --- a/web/tailwind-themes/tailwind.config.js +++ b/web/tailwind-themes/tailwind.config.js @@ -43,6 +43,8 @@ module.exports = { "background-strong": "#eaecef", "background-search": "#ffffff", "background-custom-header": "#f3f4f6", + "background-inverted": "#000000", + "background-weak": "#f3f4f6", // gray-100 // text or icons link: "#3b82f6", // blue-500 @@ -60,6 +62,7 @@ module.exports = { // borders border: "#e5e7eb", // gray-200 "border-light": "#f3f4f6", // gray-100 + "border-medium": "#d1d5db", // gray-300 "border-strong": "#9ca3af", // gray-400 // hover @@ -73,13 +76,6 @@ module.exports = { text: "#fef9c3", // yellow-100 }, - // bubbles in chat for each "user" - user: "#fb7185", // yellow-400 - ai: "#60a5fa", // blue-400 - - // for display documents - document: "#ec4899", // pink-500 - // scrollbar scrollbar: { track: "#f9fafb", @@ -92,6 +88,13 @@ module.exports = { }, }, + // bubbles in chat for each "user" + user: "#fb7185", // yellow-400 + ai: "#60a5fa", // blue-400 + + // for display documents + document: "#ec4899", // pink-500 + // light mode tremor: { brand: {