mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-05-03 16:30:21 +02:00
Allow re-sizing of document sidebar + make central chat smaller on small screens (#832)
This commit is contained in:
parent
a099f8e296
commit
c7a91b1819
@ -35,6 +35,8 @@ import { buildFilters } from "@/lib/search/utils";
|
||||
import { QA, SearchTypeSelector } from "./modifiers/SearchTypeSelector";
|
||||
import { SelectedDocuments } from "./modifiers/SelectedDocuments";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { ResizableSection } from "@/components/resizable/ResizableSection";
|
||||
import { DanswerInitializingLoader } from "@/components/DanswerInitializingLoader";
|
||||
|
||||
const MAX_INPUT_HEIGHT = 200;
|
||||
|
||||
@ -45,6 +47,7 @@ export const Chat = ({
|
||||
availableSources,
|
||||
availableDocumentSets,
|
||||
availablePersonas,
|
||||
documentSidebarInitialWidth,
|
||||
shouldhideBeforeScroll,
|
||||
}: {
|
||||
existingChatSessionId: number | null;
|
||||
@ -53,6 +56,7 @@ export const Chat = ({
|
||||
availableSources: ValidSources[];
|
||||
availableDocumentSets: DocumentSet[];
|
||||
availablePersonas: Persona[];
|
||||
documentSidebarInitialWidth?: number;
|
||||
shouldhideBeforeScroll?: boolean;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
@ -113,7 +117,6 @@ export const Chat = ({
|
||||
});
|
||||
|
||||
// scroll to bottom initially
|
||||
console.log(shouldhideBeforeScroll);
|
||||
const [hasPerformedInitialScroll, setHasPerformedInitialScroll] = useState(
|
||||
shouldhideBeforeScroll !== true
|
||||
);
|
||||
@ -140,6 +143,35 @@ export const Chat = ({
|
||||
}
|
||||
}, [message]);
|
||||
|
||||
// used for resizing of the document sidebar
|
||||
const masterFlexboxRef = useRef<HTMLDivElement>(null);
|
||||
const [maxDocumentSidebarWidth, setMaxDocumentSidebarWidth] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
const adjustDocumentSidebarWidth = () => {
|
||||
if (masterFlexboxRef.current && document.documentElement.clientWidth) {
|
||||
// numbers below are based on the actual width the center section for different
|
||||
// screen sizes. `1700` corresponds to the custom "3xl" tailwind breakpoint
|
||||
// NOTE: some buffer is needed to account for scroll bars
|
||||
setMaxDocumentSidebarWidth(
|
||||
masterFlexboxRef.current.clientWidth -
|
||||
(document.documentElement.clientWidth > 1700 ? 950 : 810)
|
||||
);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
adjustDocumentSidebarWidth(); // Adjust the width on initial render
|
||||
window.addEventListener("resize", adjustDocumentSidebarWidth); // Add resize event listener
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", adjustDocumentSidebarWidth); // Cleanup the event listener
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!documentSidebarInitialWidth && maxDocumentSidebarWidth) {
|
||||
documentSidebarInitialWidth = Math.min(700, maxDocumentSidebarWidth);
|
||||
}
|
||||
|
||||
const onSubmit = async (messageOverride?: string) => {
|
||||
let currChatSessionId: number;
|
||||
let isNewSession = chatSessionId === null;
|
||||
@ -301,7 +333,7 @@ export const Chat = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex w-full overflow-x-hidden">
|
||||
<div className="flex w-full overflow-x-hidden" ref={masterFlexboxRef}>
|
||||
{popup}
|
||||
{currentFeedback && (
|
||||
<FeedbackModal
|
||||
@ -314,6 +346,8 @@ export const Chat = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
{documentSidebarInitialWidth !== undefined ? (
|
||||
<>
|
||||
<div className="w-full sm:relative">
|
||||
<div
|
||||
className="w-full h-screen flex flex-col overflow-y-auto relative"
|
||||
@ -337,7 +371,7 @@ export const Chat = ({
|
||||
|
||||
{messageHistory.length === 0 && !isStreaming && (
|
||||
<div className="flex justify-center items-center h-full">
|
||||
<div>
|
||||
<div className="px-8 w-searchbar-small 3xl:w-searchbar">
|
||||
<div className="flex">
|
||||
<div className="mx-auto h-[80px] w-[80px]">
|
||||
<Image
|
||||
@ -348,7 +382,7 @@ export const Chat = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-strong p-4">
|
||||
<div className="mx-auto text-2xl font-bold text-strong p-4 w-fit">
|
||||
What are you looking for today?
|
||||
</div>
|
||||
</div>
|
||||
@ -385,8 +419,8 @@ export const Chat = ({
|
||||
i !== messageHistory.length - 1 || !isStreaming
|
||||
}
|
||||
hasDocs={
|
||||
(message.documents && message.documents.length > 0) ===
|
||||
true
|
||||
(message.documents &&
|
||||
message.documents.length > 0) === true
|
||||
}
|
||||
handleFeedback={
|
||||
i === messageHistory.length - 1 && isStreaming
|
||||
@ -492,7 +526,7 @@ export const Chat = ({
|
||||
)} */}
|
||||
|
||||
<div className="flex">
|
||||
<div className="w-searchbar mx-auto px-4 pt-1 flex">
|
||||
<div className="w-searchbar-small 3xl:w-searchbar mx-auto px-4 pt-1 flex">
|
||||
<div className="mr-3">
|
||||
<SearchTypeSelector
|
||||
selectedSearchType={selectedSearchType}
|
||||
@ -501,7 +535,9 @@ export const Chat = ({
|
||||
</div>
|
||||
|
||||
{selectedDocuments.length > 0 ? (
|
||||
<SelectedDocuments selectedDocuments={selectedDocuments} />
|
||||
<SelectedDocuments
|
||||
selectedDocuments={selectedDocuments}
|
||||
/>
|
||||
) : (
|
||||
<ChatFilters
|
||||
{...filterManager}
|
||||
@ -513,7 +549,7 @@ export const Chat = ({
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center py-2 max-w-screen-lg mx-auto mb-2">
|
||||
<div className="w-full shrink relative px-4 w-searchbar mx-auto">
|
||||
<div className="w-full shrink relative px-4 w-searchbar-small 3xl:w-searchbar mx-auto">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
autoFocus
|
||||
@ -557,7 +593,10 @@ export const Chat = ({
|
||||
suppressContentEditableWarning={true}
|
||||
/>
|
||||
<div className="absolute bottom-4 right-10">
|
||||
<div className={"cursor-pointer"} onClick={() => onSubmit()}>
|
||||
<div
|
||||
className={"cursor-pointer"}
|
||||
onClick={() => onSubmit()}
|
||||
>
|
||||
<FiSend
|
||||
size={18}
|
||||
className={
|
||||
@ -573,11 +612,25 @@ export const Chat = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ResizableSection
|
||||
intialWidth={documentSidebarInitialWidth}
|
||||
minWidth={400}
|
||||
maxWidth={maxDocumentSidebarWidth || undefined}
|
||||
>
|
||||
<DocumentSidebar
|
||||
selectedMessage={aiMessage}
|
||||
selectedDocuments={selectedDocuments}
|
||||
setSelectedDocuments={setSelectedDocuments}
|
||||
/>
|
||||
</ResizableSection>
|
||||
</>
|
||||
) : (
|
||||
<div className="mx-auto h-full flex flex-col">
|
||||
<div className="my-auto">
|
||||
<DanswerInitializingLoader />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -15,6 +15,8 @@ import { Persona } from "../admin/personas/interfaces";
|
||||
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
|
||||
import { WelcomeModal } from "@/components/WelcomeModal";
|
||||
import { ApiKeyModal } from "@/components/openai/ApiKeyModal";
|
||||
import { cookies } from "next/headers";
|
||||
import { DOCUMENT_SIDEBAR_WIDTH_COOKIE_NAME } from "@/components/resizable/contants";
|
||||
|
||||
export default async function ChatPage({
|
||||
chatId,
|
||||
@ -175,6 +177,13 @@ export default async function ChatPage({
|
||||
);
|
||||
}
|
||||
|
||||
const documentSidebarCookieInitialWidth = cookies().get(
|
||||
DOCUMENT_SIDEBAR_WIDTH_COOKIE_NAME
|
||||
);
|
||||
const finalDocumentSidebarInitialWidth = documentSidebarCookieInitialWidth
|
||||
? parseInt(documentSidebarCookieInitialWidth.value)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<InstantSSRAutoRefresh />
|
||||
@ -196,6 +205,7 @@ export default async function ChatPage({
|
||||
availableSources={availableSources}
|
||||
availableDocumentSets={documentSets}
|
||||
availablePersonas={personas}
|
||||
documentSidebarInitialWidth={finalDocumentSidebarInitialWidth}
|
||||
shouldhideBeforeScroll={shouldhideBeforeScroll}
|
||||
/>
|
||||
</div>
|
||||
|
@ -45,13 +45,11 @@ export function DocumentSidebar({
|
||||
<div
|
||||
className={`
|
||||
flex-initial
|
||||
w-document-sidebar
|
||||
border-l
|
||||
border-border
|
||||
overflow-y-hidden
|
||||
flex
|
||||
flex-col
|
||||
pt-4
|
||||
w-full
|
||||
`}
|
||||
id="document-sidebar"
|
||||
>
|
||||
@ -117,7 +115,7 @@ export function DocumentSidebar({
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="ml-4">
|
||||
<div className="ml-4 mr-3">
|
||||
<Text>
|
||||
When you run ask a question, the retrieved documents will show up
|
||||
here!
|
||||
|
@ -52,7 +52,7 @@ export const AIMessage = ({
|
||||
const [copyClicked, setCopyClicked] = useState(false);
|
||||
return (
|
||||
<div className={"py-5 px-5 flex -mr-6 w-full"}>
|
||||
<div className="mx-auto w-searchbar relative">
|
||||
<div className="mx-auto w-searchbar-small 3xl:w-searchbar relative">
|
||||
<div className="ml-8">
|
||||
<div className="flex">
|
||||
<div className="p-1 bg-ai rounded-lg h-fit my-auto">
|
||||
@ -67,7 +67,7 @@ export const AIMessage = ({
|
||||
hasDocs &&
|
||||
handleShowRetrieved !== undefined &&
|
||||
isCurrentlyShowingRetrieved !== undefined && (
|
||||
<div className="flex w-message-default absolute ml-8">
|
||||
<div className="flex w-message-small 3xl:w-message-default absolute ml-8">
|
||||
<div className="ml-auto">
|
||||
<ShowHideDocsButton
|
||||
messageId={messageId}
|
||||
@ -79,7 +79,7 @@ export const AIMessage = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="w-message-default break-words mt-1 ml-8">
|
||||
<div className="w-message-small 3xl:w-message-default break-words mt-1 ml-8">
|
||||
{query !== undefined &&
|
||||
handleShowRetrieved !== undefined &&
|
||||
isCurrentlyShowingRetrieved !== undefined && (
|
||||
@ -194,7 +194,7 @@ export const HumanMessage = ({
|
||||
}) => {
|
||||
return (
|
||||
<div className="py-5 px-5 flex -mr-6 w-full">
|
||||
<div className="mx-auto w-searchbar">
|
||||
<div className="mx-auto w-searchbar-small 3xl:w-searchbar">
|
||||
<div className="ml-8">
|
||||
<div className="flex">
|
||||
<div className="p-1 bg-user rounded-lg h-fit">
|
||||
@ -205,8 +205,8 @@ export const HumanMessage = ({
|
||||
|
||||
<div className="font-bold text-emphasis ml-2 my-auto">You</div>
|
||||
</div>
|
||||
<div className="mx-auto mt-1 ml-8 w-full w-message-default flex flex-wrap">
|
||||
<div className="w-full sm:w-full w-message-default break-words">
|
||||
<div className="mx-auto mt-1 ml-8 w-message-small 3xl:w-message-default flex flex-wrap">
|
||||
<div className="w-full sm:w-full w-message-small 3xl:w-message-default break-words">
|
||||
{typeof content === "string" ? (
|
||||
<ReactMarkdown
|
||||
className="prose max-w-full"
|
||||
|
@ -53,6 +53,7 @@ export function ChatSessionDisplay({
|
||||
className="flex my-1"
|
||||
key={chatSession.id}
|
||||
href={`/chat/${chatSession.id}`}
|
||||
scroll={false}
|
||||
>
|
||||
<BasicSelectable fullWidth selected={isSelected}>
|
||||
<div className="flex">
|
||||
|
13
web/src/components/DanswerInitializingLoader.tsx
Normal file
13
web/src/components/DanswerInitializingLoader.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { Bold } from "@tremor/react";
|
||||
import Image from "next/image";
|
||||
|
||||
export function DanswerInitializingLoader() {
|
||||
return (
|
||||
<div className="mx-auto animate-pulse">
|
||||
<div className="h-24 w-24 mx-auto mb-3">
|
||||
<Image src="/logo.png" alt="Logo" width="1419" height="1520" />
|
||||
</div>
|
||||
<Bold>Initializing Danswer</Bold>
|
||||
</div>
|
||||
);
|
||||
}
|
116
web/src/components/resizable/ResizableSection.tsx
Normal file
116
web/src/components/resizable/ResizableSection.tsx
Normal file
@ -0,0 +1,116 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Cookies from "js-cookie";
|
||||
import { DOCUMENT_SIDEBAR_WIDTH_COOKIE_NAME } from "./contants";
|
||||
|
||||
function applyMinAndMax(
|
||||
width: number,
|
||||
minWidth: number | undefined,
|
||||
maxWidth: number | undefined
|
||||
) {
|
||||
let newWidth = width;
|
||||
if (minWidth) {
|
||||
newWidth = Math.max(width, minWidth); // Ensure the width doesn't go below a minimum value
|
||||
}
|
||||
if (maxWidth) {
|
||||
newWidth = Math.min(newWidth, maxWidth); // Ensure the width doesn't exceed a maximum value
|
||||
}
|
||||
return newWidth;
|
||||
}
|
||||
|
||||
export function ResizableSection({
|
||||
children,
|
||||
intialWidth,
|
||||
minWidth,
|
||||
maxWidth,
|
||||
}: {
|
||||
children: JSX.Element;
|
||||
intialWidth: number;
|
||||
minWidth: number;
|
||||
maxWidth?: number;
|
||||
}) {
|
||||
const [width, setWidth] = useState<number>(intialWidth);
|
||||
const [isResizing, setIsResizing] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
const newWidth = applyMinAndMax(width, minWidth, maxWidth);
|
||||
setWidth(newWidth);
|
||||
Cookies.set(DOCUMENT_SIDEBAR_WIDTH_COOKIE_NAME, newWidth.toString(), {
|
||||
path: "/",
|
||||
});
|
||||
}, [minWidth, maxWidth]);
|
||||
|
||||
const startResizing = (mouseDownEvent: React.MouseEvent<HTMLDivElement>) => {
|
||||
setIsResizing(true);
|
||||
|
||||
// Disable text selection
|
||||
document.body.style.userSelect = "none";
|
||||
document.body.style.cursor = "col-resize";
|
||||
|
||||
// Record the initial position of the mouse
|
||||
const startX = mouseDownEvent.clientX;
|
||||
|
||||
const handleMouseMove = (mouseMoveEvent: MouseEvent) => {
|
||||
// Calculate the change in position
|
||||
const delta = mouseMoveEvent.clientX - startX;
|
||||
let newWidth = applyMinAndMax(width - delta, minWidth, maxWidth);
|
||||
setWidth(newWidth);
|
||||
Cookies.set(DOCUMENT_SIDEBAR_WIDTH_COOKIE_NAME, newWidth.toString(), {
|
||||
path: "/",
|
||||
});
|
||||
};
|
||||
const stopResizing = () => {
|
||||
// Re-enable text selection
|
||||
document.body.style.userSelect = "";
|
||||
document.body.style.cursor = "";
|
||||
|
||||
// Remove the event listeners
|
||||
window.removeEventListener("mousemove", handleMouseMove);
|
||||
window.removeEventListener("mouseup", stopResizing);
|
||||
|
||||
setIsResizing(false);
|
||||
};
|
||||
|
||||
window.addEventListener("mousemove", handleMouseMove);
|
||||
window.addEventListener("mouseup", stopResizing);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full">
|
||||
<div
|
||||
className={`
|
||||
-mr-1
|
||||
pr-1
|
||||
z-40
|
||||
h-full
|
||||
`}
|
||||
>
|
||||
<div
|
||||
onMouseDown={startResizing}
|
||||
className={`
|
||||
cursor-col-resize
|
||||
border-l
|
||||
border-border
|
||||
h-full
|
||||
w-full
|
||||
transition-all duration-300 ease-in hover:border-border-strong hover:border-l-2
|
||||
${
|
||||
isResizing
|
||||
? "transition-all duration-300 ease-in border-border-strong border-l-2"
|
||||
: ""
|
||||
}
|
||||
`}
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
style={{ width: `${width}px` }}
|
||||
className={`resize-section h-full flex`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ResizableSection;
|
1
web/src/components/resizable/contants.ts
Normal file
1
web/src/components/resizable/contants.ts
Normal file
@ -0,0 +1 @@
|
||||
export const DOCUMENT_SIDEBAR_WIDTH_COOKIE_NAME = "documentSidebarWidth";
|
@ -16,11 +16,16 @@ module.exports = {
|
||||
transparent: "transparent",
|
||||
current: "currentColor",
|
||||
extend: {
|
||||
screens: {
|
||||
"3xl": "1700px",
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ["var(--font-inter)"],
|
||||
},
|
||||
width: {
|
||||
"message-small": "600px",
|
||||
"message-default": "740px",
|
||||
"searchbar-small": "710px",
|
||||
searchbar: "850px",
|
||||
"document-sidebar": "800px",
|
||||
"document-sidebar-large": "1000px",
|
||||
|
Loading…
x
Reference in New Issue
Block a user