mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-09-18 19:43:26 +02:00
Add initial mobile support (#1962)
This commit is contained in:
@@ -19,4 +19,5 @@ export interface CombinedSettings {
|
||||
settings: Settings;
|
||||
enterpriseSettings: EnterpriseSettings | null;
|
||||
customAnalyticsScript: string | null;
|
||||
isMobile?: boolean;
|
||||
}
|
||||
|
@@ -7,12 +7,13 @@ import { Folder } from "@/app/chat/folders/interfaces";
|
||||
import { User } from "@/lib/types";
|
||||
import Cookies from "js-cookie";
|
||||
import { SIDEBAR_TOGGLED_COOKIE_NAME } from "@/components/resizable/constants";
|
||||
import { ReactNode, useEffect, useRef, useState } from "react";
|
||||
import { ReactNode, useContext, useEffect, useRef, useState } from "react";
|
||||
import { useSidebarVisibility } from "@/components/chat_search/hooks";
|
||||
import FunctionalHeader from "@/components/chat_search/Header";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { pageType } from "../chat/sessionSidebar/types";
|
||||
import FixedLogo from "../chat/shared_chat_search/FixedLogo";
|
||||
import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||
|
||||
interface SidebarWrapperProps<T extends object> {
|
||||
chatSessions?: ChatSession[];
|
||||
@@ -55,11 +56,13 @@ export default function SidebarWrapper<T extends object>({
|
||||
|
||||
const sidebarElementRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const settings = useContext(SettingsContext);
|
||||
useSidebarVisibility({
|
||||
toggledSidebar,
|
||||
sidebarElementRef,
|
||||
showDocSidebar,
|
||||
setShowDocSidebar,
|
||||
mobile: settings?.isMobile,
|
||||
});
|
||||
|
||||
const innerSidebarElementRef = useRef<HTMLDivElement>(null);
|
||||
@@ -90,9 +93,7 @@ export default function SidebarWrapper<T extends object>({
|
||||
flex-none
|
||||
fixed
|
||||
left-0
|
||||
z-20
|
||||
overflow-y-hidden
|
||||
sidebar
|
||||
z-30
|
||||
bg-background-100
|
||||
h-screen
|
||||
transition-all
|
||||
@@ -119,10 +120,10 @@ export default function SidebarWrapper<T extends object>({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute left-0 w-full top-0">
|
||||
<div className="absolute h-svh left-0 w-full top-0">
|
||||
<FunctionalHeader
|
||||
toggleSidebar={toggleSidebar}
|
||||
page="assistants"
|
||||
showSidebar={showDocSidebar}
|
||||
user={headerProps.user}
|
||||
/>
|
||||
<div className="w-full flex">
|
||||
@@ -138,6 +139,7 @@ export default function SidebarWrapper<T extends object>({
|
||||
ease-in-out
|
||||
${toggledSidebar ? "w-[250px]" : "w-[0px]"}`}
|
||||
/>
|
||||
|
||||
<div className="mt-4 w-full max-w-3xl mx-auto">
|
||||
{content(contentProps)}
|
||||
</div>
|
||||
|
@@ -47,7 +47,7 @@ export function AssistantTools({
|
||||
hovered?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="relative text-xs flex text-subtle">
|
||||
<div className="relative text-xs overflow-x-hidden flex text-subtle">
|
||||
<span
|
||||
className={`${assistant.tools.length > 0 && "py-1"} ${!list ? "font-semibold" : "text-subtle text-sm"}`}
|
||||
>
|
||||
|
@@ -287,7 +287,7 @@ export function AssistantsList({ user, assistants }: AssistantsListProps) {
|
||||
return (
|
||||
<>
|
||||
{popup}
|
||||
<div className="mx-auto w-searchbar-xs 2xl:w-searchbar-sm 3xl:w-searchbar">
|
||||
<div className="mx-auto mobile:w-[90%] desktop:w-searchbar-xs 2xl:w-searchbar-sm 3xl:w-searchbar">
|
||||
<AssistantsPageTitle>My Assistants</AssistantsPageTitle>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 mt-3">
|
||||
|
@@ -38,7 +38,7 @@ export function ChatIntro({
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-center items-center h-full">
|
||||
<div className="w-message-xs 2xl:w-message-sm 3xl:w-message">
|
||||
<div className="mobile:w-[90%] mobile:px-4 w-message-xs 2xl:w-message-sm 3xl:w-message">
|
||||
<div className="flex">
|
||||
<div className="mx-auto">
|
||||
<div className="m-auto text-3xl font-strong font-bold text-strong w-fit">
|
||||
|
@@ -85,7 +85,7 @@ export function ChatPage({
|
||||
defaultSelectedAssistantId,
|
||||
toggledSidebar,
|
||||
}: {
|
||||
toggle: () => void;
|
||||
toggle: (toggled?: boolean) => void;
|
||||
documentSidebarInitialWidth?: number;
|
||||
defaultSelectedAssistantId?: number;
|
||||
toggledSidebar: boolean;
|
||||
@@ -524,15 +524,6 @@ export function ChatPage({
|
||||
const distance = 500; // distance that should "engage" the scroll
|
||||
const debounce = 100; // time for debouncing
|
||||
|
||||
useScrollonStream({
|
||||
isStreaming,
|
||||
scrollableDivRef,
|
||||
scrollDist,
|
||||
endDivRef,
|
||||
distance,
|
||||
debounce,
|
||||
});
|
||||
|
||||
const [hasPerformedInitialScroll, setHasPerformedInitialScroll] = useState(
|
||||
existingChatSessionId === null
|
||||
);
|
||||
@@ -644,7 +635,6 @@ export function ChatPage({
|
||||
alternativeAssistantOverride?: Persona | null;
|
||||
} = {}) => {
|
||||
setAlternativeGeneratingAssistant(alternativeAssistantOverride);
|
||||
|
||||
clientScrollToBottom();
|
||||
let currChatSessionId: number;
|
||||
let isNewSession = chatSessionIdRef.current === null;
|
||||
@@ -1059,6 +1049,10 @@ export function ChatPage({
|
||||
|
||||
toggle();
|
||||
};
|
||||
const removeToggle = () => {
|
||||
setShowDocSidebar(false);
|
||||
toggle(false);
|
||||
};
|
||||
|
||||
const sidebarElementRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -1067,6 +1061,17 @@ export function ChatPage({
|
||||
sidebarElementRef,
|
||||
showDocSidebar,
|
||||
setShowDocSidebar,
|
||||
setToggled: removeToggle,
|
||||
mobile: settings?.isMobile,
|
||||
});
|
||||
|
||||
useScrollonStream({
|
||||
isStreaming,
|
||||
scrollableDivRef,
|
||||
scrollDist,
|
||||
endDivRef,
|
||||
distance,
|
||||
debounce,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -1119,20 +1124,48 @@ export function ChatPage({
|
||||
<>
|
||||
<HealthCheckBanner secondsUntilExpiration={secondsUntilExpiration} />
|
||||
<InstantSSRAutoRefresh />
|
||||
|
||||
{/* ChatPopup is a custom popup that displays a admin-specified message on initial user visit.
|
||||
Only used in the EE version of the app. */}
|
||||
<ChatPopup />
|
||||
|
||||
<div className="flex relative bg-background text-default ">
|
||||
{currentFeedback && (
|
||||
<FeedbackModal
|
||||
feedbackType={currentFeedback[0]}
|
||||
onClose={() => setCurrentFeedback(null)}
|
||||
onSubmit={({ message, predefinedFeedback }) => {
|
||||
onFeedback(
|
||||
currentFeedback[1],
|
||||
currentFeedback[0],
|
||||
message,
|
||||
predefinedFeedback
|
||||
);
|
||||
setCurrentFeedback(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{sharingModalVisible && chatSessionIdRef.current !== null && (
|
||||
<ShareChatSessionModal
|
||||
chatSessionId={chatSessionIdRef.current}
|
||||
existingSharedStatus={chatSessionSharedStatus}
|
||||
onClose={() => setSharingModalVisible(false)}
|
||||
onShare={(shared) =>
|
||||
setChatSessionSharedStatus(
|
||||
shared
|
||||
? ChatSessionSharedStatus.Public
|
||||
: ChatSessionSharedStatus.Private
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<div className="fixed inset-0 flex flex-col text-default">
|
||||
<div className="h-[100dvh] overflow-y-hidden">
|
||||
<div className="w-full">
|
||||
<div
|
||||
ref={sidebarElementRef}
|
||||
className={`
|
||||
flex-none
|
||||
absolute
|
||||
fixed
|
||||
left-0
|
||||
z-20
|
||||
sidebar
|
||||
z-30
|
||||
bg-background-100
|
||||
h-screen
|
||||
transition-all
|
||||
@@ -1150,48 +1183,24 @@ export function ChatPage({
|
||||
page="chat"
|
||||
ref={innerSidebarElementRef}
|
||||
toggleSidebar={toggleSidebar}
|
||||
toggled={toggledSidebar}
|
||||
toggled={toggledSidebar && !settings?.isMobile}
|
||||
existingChats={chatSessions}
|
||||
currentChatSession={selectedChatSession}
|
||||
folders={folders}
|
||||
openedFolders={openedFolders}
|
||||
removeToggle={removeToggle}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div ref={masterFlexboxRef} className="flex w-full overflow-x-hidden">
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={masterFlexboxRef}
|
||||
className="flex h-full w-full overflow-x-hidden"
|
||||
>
|
||||
{popup}
|
||||
{currentFeedback && (
|
||||
<FeedbackModal
|
||||
feedbackType={currentFeedback[0]}
|
||||
onClose={() => setCurrentFeedback(null)}
|
||||
onSubmit={({ message, predefinedFeedback }) => {
|
||||
onFeedback(
|
||||
currentFeedback[1],
|
||||
currentFeedback[0],
|
||||
message,
|
||||
predefinedFeedback
|
||||
);
|
||||
setCurrentFeedback(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{sharingModalVisible && chatSessionIdRef.current !== null && (
|
||||
<ShareChatSessionModal
|
||||
chatSessionId={chatSessionIdRef.current}
|
||||
existingSharedStatus={chatSessionSharedStatus}
|
||||
onClose={() => setSharingModalVisible(false)}
|
||||
onShare={(shared) =>
|
||||
setChatSessionSharedStatus(
|
||||
shared
|
||||
? ChatSessionSharedStatus.Public
|
||||
: ChatSessionSharedStatus.Private
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex h-[calc(100dvh)] flex-col w-full">
|
||||
<div className="flex h-full flex-col w-full">
|
||||
{liveAssistant && (
|
||||
<FunctionalHeader
|
||||
page="chat"
|
||||
@@ -1200,7 +1209,7 @@ export function ChatPage({
|
||||
? setSharingModalVisible
|
||||
: undefined
|
||||
}
|
||||
showSidebar={showDocSidebar}
|
||||
toggleSidebar={toggleSidebar}
|
||||
user={user}
|
||||
currentChatSession={selectedChatSession}
|
||||
/>
|
||||
@@ -1226,6 +1235,7 @@ export function ChatPage({
|
||||
<Dropzone onDrop={handleImageUpload} noClick>
|
||||
{({ getRootProps }) => (
|
||||
<div className="flex h-full w-full">
|
||||
{!settings?.isMobile && (
|
||||
<div
|
||||
style={{ transition: "width 0.30s ease-out" }}
|
||||
className={`
|
||||
@@ -1240,8 +1250,10 @@ export function ChatPage({
|
||||
${toggledSidebar ? "w-[250px]" : "w-[0px]"}
|
||||
`}
|
||||
></div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`h-full w-full relative flex-auto transition-margin duration-300 overflow-x-auto pb-[140px]`}
|
||||
className={`h-full w-full relative flex-auto transition-margin duration-300 overflow-x-auto mobile:pb-12 desktop:pb-[140px]`}
|
||||
{...getRootProps()}
|
||||
>
|
||||
{/* <input {...getInputProps()} /> */}
|
||||
@@ -1263,12 +1275,13 @@ export function ChatPage({
|
||||
<div
|
||||
className={
|
||||
"mt-4 -ml-4 w-full mx-auto " +
|
||||
"absolute top-12 left-0 " +
|
||||
"absolute mobile:top-0 desktop:top-12 left-0" +
|
||||
(hasPerformedInitialScroll ? "" : "invisible")
|
||||
}
|
||||
>
|
||||
{messageHistory.map((message, i) => {
|
||||
const messageMap = completeMessageDetail.messageMap;
|
||||
const messageMap =
|
||||
completeMessageDetail.messageMap;
|
||||
const messageReactComponentKey = `${i}-${completeMessageDetail.sessionId}`;
|
||||
if (message.type === "user") {
|
||||
const parentMessage = message.parentMessageId
|
||||
@@ -1375,7 +1388,8 @@ export function ChatPage({
|
||||
message
|
||||
)}
|
||||
toolCall={
|
||||
message.toolCalls && message.toolCalls[0]
|
||||
message.toolCalls &&
|
||||
message.toolCalls[0]
|
||||
}
|
||||
isComplete={
|
||||
i !== messageHistory.length - 1 ||
|
||||
@@ -1409,7 +1423,8 @@ export function ChatPage({
|
||||
}
|
||||
|
||||
if (
|
||||
previousMessage.messageId === null
|
||||
previousMessage.messageId ===
|
||||
null
|
||||
) {
|
||||
setPopup({
|
||||
type: "error",
|
||||
@@ -1524,10 +1539,6 @@ export function ChatPage({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Some padding at the bottom so the search bar has space at the bottom to not cover the last message*/}
|
||||
<div ref={endPaddingRef} className="h-[95px]" />
|
||||
<div ref={endDivRef}></div>
|
||||
|
||||
{currentPersona &&
|
||||
currentPersona.starter_messages &&
|
||||
currentPersona.starter_messages.length > 0 &&
|
||||
@@ -1566,6 +1577,8 @@ export function ChatPage({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* Some padding at the bottom so the search bar has space at the bottom to not cover the last message*/}
|
||||
<div ref={endPaddingRef} className="h-[95px]" />
|
||||
<div ref={endDivRef} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -1627,9 +1640,12 @@ export function ChatPage({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<FixedLogo />
|
||||
</div>
|
||||
</div>
|
||||
<DocumentSidebar
|
||||
initialWidth={390}
|
||||
initialWidth={350}
|
||||
ref={innerSidebarElementRef}
|
||||
closeSidebar={() => setDocumentSelection(false)}
|
||||
selectedMessage={aiMessage}
|
||||
@@ -1641,9 +1657,6 @@ export function ChatPage({
|
||||
isLoading={isFetchingChatMessages}
|
||||
isOpen={documentSelection}
|
||||
/>
|
||||
</div>
|
||||
<FixedLogo />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@@ -2,44 +2,10 @@ import { DanswerDocument } from "@/lib/search/interfaces";
|
||||
import { Divider, Text } from "@tremor/react";
|
||||
import { ChatDocumentDisplay } from "./ChatDocumentDisplay";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { FiAlertTriangle, FiFileText } from "react-icons/fi";
|
||||
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 { 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 (
|
||||
<div
|
||||
className={`w-full mt-3 flex text-lg text-emphasis font-medium flex mb-3.5 font-bold flex items-end`}
|
||||
>
|
||||
<div className="flex mt-auto justify-between w-full">
|
||||
<p className="flex">
|
||||
{icon({ className: "my-auto mr-1" })}
|
||||
{name}
|
||||
</p>
|
||||
{closeHeader && (
|
||||
<button onClick={() => closeHeader()}>
|
||||
<TbLayoutSidebarLeftExpand size={24} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface DocumentSidebarProps {
|
||||
closeSidebar: () => void;
|
||||
selectedMessage: Message | null;
|
||||
@@ -71,8 +37,6 @@ export const DocumentSidebar = forwardRef<HTMLDivElement, DocumentSidebarProps>(
|
||||
) => {
|
||||
const { popup, setPopup } = usePopup();
|
||||
|
||||
const selectedMessageRetrievalType = selectedMessage?.retrievalType || null;
|
||||
|
||||
const selectedDocumentIds =
|
||||
selectedDocuments?.map((document) => document.document_id) || [];
|
||||
|
||||
@@ -88,9 +52,7 @@ export const DocumentSidebar = forwardRef<HTMLDivElement, DocumentSidebarProps>(
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`fixed inset-0 transition-opacity duration-300 z-50 bg-black/80 ${
|
||||
isOpen ? "opacity-100" : "opacity-0 pointer-events-none"
|
||||
}`}
|
||||
className={`fixed inset-0 transition-opacity duration-300 z-50 bg-black/80 ${isOpen ? "opacity-100" : "opacity-0 pointer-events-none"}`}
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
closeSidebar();
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import React, { useContext, useEffect, useRef, useState } from "react";
|
||||
import { FiPlusCircle, FiPlus, FiInfo, FiX } from "react-icons/fi";
|
||||
import { ChatInputOption } from "./ChatInputOption";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
@@ -29,6 +29,7 @@ import { DanswerDocument } from "@/lib/search/interfaces";
|
||||
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
|
||||
import { Tooltip } from "@/components/tooltip/Tooltip";
|
||||
import { Hoverable } from "@/components/Hoverable";
|
||||
import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||
const MAX_INPUT_HEIGHT = 200;
|
||||
|
||||
export function ChatInputBar({
|
||||
@@ -103,6 +104,7 @@ export function ChatInputBar({
|
||||
}
|
||||
}
|
||||
};
|
||||
const settings = useContext(SettingsContext);
|
||||
|
||||
const { llmProviders } = useChatContext();
|
||||
const [_, llmName] = getFinalLLM(llmProviders, selectedAssistant, null);
|
||||
@@ -213,9 +215,8 @@ export function ChatInputBar({
|
||||
className="
|
||||
w-[90%]
|
||||
shrink
|
||||
bg-background
|
||||
relative
|
||||
px-4
|
||||
desktop:px-4
|
||||
max-w-searchbar-max
|
||||
mx-auto
|
||||
"
|
||||
@@ -390,7 +391,7 @@ export function ChatInputBar({
|
||||
style={{ scrollbarWidth: "thin" }}
|
||||
role="textarea"
|
||||
aria-multiline
|
||||
placeholder="Send a message or @ to tag an assistant..."
|
||||
placeholder={`Send a message ${!settings?.isMobile ? "or @ to tag an assistant..." : ""}`}
|
||||
value={message}
|
||||
onKeyDown={(event) => {
|
||||
if (
|
||||
@@ -420,7 +421,9 @@ export function ChatInputBar({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
flexPriority="shrink"
|
||||
position="top"
|
||||
mobilePosition="top-right"
|
||||
>
|
||||
<ChatInputOption
|
||||
flexPriority="shrink"
|
||||
@@ -450,16 +453,21 @@ export function ChatInputBar({
|
||||
/>
|
||||
)}
|
||||
position="top"
|
||||
// flexPriority="second"
|
||||
>
|
||||
<ChatInputOption
|
||||
flexPriority="second"
|
||||
name={getDisplayNameForModel(
|
||||
name={
|
||||
settings?.isMobile
|
||||
? undefined
|
||||
: getDisplayNameForModel(
|
||||
llmOverrideManager.llmOverride.modelName ||
|
||||
(selectedAssistant
|
||||
? selectedAssistant.llm_model_version_override ||
|
||||
llmName
|
||||
: llmName)
|
||||
)}
|
||||
)
|
||||
}
|
||||
Icon={CpuIconSkeleton}
|
||||
/>
|
||||
</Popup>
|
||||
@@ -484,7 +492,7 @@ export function ChatInputBar({
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute bottom-2.5 right-10">
|
||||
<div className="absolute bottom-2.5 mobile:right-4 desktop:right-10">
|
||||
<div
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
|
@@ -5,7 +5,7 @@ import { Popover } from "../../../components/popover/Popover";
|
||||
import { IconProps } from "@/components/icons/icons";
|
||||
|
||||
interface ChatInputOptionProps {
|
||||
name: string;
|
||||
name?: string;
|
||||
Icon: ({ size, className }: IconProps) => JSX.Element;
|
||||
onClick?: () => void;
|
||||
size?: number;
|
||||
@@ -61,7 +61,7 @@ export const ChatInputOption: React.FC<ChatInputOptionProps> = ({
|
||||
rounded-md
|
||||
${
|
||||
flexPriority === "shrink" &&
|
||||
"flex-shrink-[10000] flex-grow-0 flex-basis-auto min-w-[30px] whitespace-nowrap overflow-hidden"
|
||||
"flex-shrink-100 flex-grow-0 flex-basis-auto min-w-[30px] whitespace-nowrap overflow-hidden"
|
||||
}
|
||||
${
|
||||
flexPriority === "second" &&
|
||||
@@ -76,7 +76,7 @@ export const ChatInputOption: React.FC<ChatInputOptionProps> = ({
|
||||
onClick={onClick}
|
||||
>
|
||||
<Icon size={size} className="flex-none" />
|
||||
<span className="text-sm break-all line-clamp-1">{name}</span>
|
||||
{name && <span className="text-sm break-all line-clamp-1">{name}</span>}
|
||||
{isTooltipVisible && tooltipContent && (
|
||||
<div
|
||||
className="absolute z-10 p-2 text-sm text-white bg-black rounded shadow-lg"
|
||||
|
@@ -610,6 +610,7 @@ export async function useScrollonStream({
|
||||
endDivRef: RefObject<HTMLDivElement>;
|
||||
distance: number;
|
||||
debounce: number;
|
||||
mobile?: boolean;
|
||||
}) {
|
||||
const preventScrollInterference = useRef<boolean>(false);
|
||||
const preventScroll = useRef<boolean>(false);
|
||||
|
@@ -13,7 +13,7 @@ import {
|
||||
FiGlobe,
|
||||
} from "react-icons/fi";
|
||||
import { FeedbackType } from "../types";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useContext, useEffect, useRef, useState } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import {
|
||||
DanswerDocument,
|
||||
@@ -58,6 +58,7 @@ import { ValidSources } from "@/lib/types";
|
||||
import { Tooltip } from "@/components/tooltip/Tooltip";
|
||||
import { useMouseTracking } from "./hooks";
|
||||
import { InternetSearchIcon } from "@/components/InternetSearchIcon";
|
||||
import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||
|
||||
const TOOLS_WITH_CUSTOM_HANDLING = [
|
||||
SEARCH_TOOL_NAME,
|
||||
@@ -162,6 +163,7 @@ export const AIMessage = ({
|
||||
|
||||
const { isHovering, trackedElementRef, hoverElementRef } = useMouseTracking();
|
||||
|
||||
const settings = useContext(SettingsContext);
|
||||
// this is needed to give Prism a chance to load
|
||||
if (!isReady) {
|
||||
return <div />;
|
||||
@@ -226,7 +228,7 @@ export const AIMessage = ({
|
||||
return (
|
||||
<div ref={trackedElementRef} className={"py-5 px-2 lg:px-5 relative flex "}>
|
||||
<div className="mx-auto w-[90%] max-w-message-max">
|
||||
<div className="xl:ml-8">
|
||||
<div className=" mobile:ml-4 xl:ml-8">
|
||||
<div className="flex">
|
||||
<AssistantIcon
|
||||
size="small"
|
||||
@@ -430,7 +432,8 @@ export const AIMessage = ({
|
||||
<div className="mt-2 -mx-8 w-full mb-4 flex relative">
|
||||
<div className="w-full">
|
||||
<div className="px-8 flex gap-x-2">
|
||||
{filteredDocs.length > 0 &&
|
||||
{!settings?.isMobile &&
|
||||
filteredDocs.length > 0 &&
|
||||
filteredDocs.slice(0, 2).map((doc, ind) => (
|
||||
<div
|
||||
key={doc.document_id}
|
||||
@@ -530,10 +533,10 @@ export const AIMessage = ({
|
||||
<div
|
||||
ref={hoverElementRef}
|
||||
className={`
|
||||
absolute -bottom-2
|
||||
invisible ${isHovering && "!visible"}
|
||||
opacity-0 ${isHovering && "!opacity-100"}
|
||||
translate-y-2 ${isHovering && "!translate-y-0"}
|
||||
absolute -bottom-4
|
||||
invisible ${(isHovering || settings?.isMobile) && "!visible"}
|
||||
opacity-0 ${(isHovering || settings?.isMobile) && "!opacity-100"}
|
||||
translate-y-2 ${(isHovering || settings?.isMobile) && "!translate-y-0"}
|
||||
transition-transform duration-300 ease-in-out
|
||||
flex md:flex-row gap-x-0.5 bg-background-125/40 p-1.5 rounded-lg
|
||||
`}
|
||||
|
@@ -28,9 +28,12 @@ export function SkippedSearch({
|
||||
}) {
|
||||
return (
|
||||
<div className="flex text-sm !pt-0 p-1">
|
||||
<FiBook className="my-auto mr-2" size={14} />
|
||||
<FiBook className="my-auto flex-none mr-2" size={14} />
|
||||
<div className="my-auto cursor-default">
|
||||
<span className="mobile:hidden">
|
||||
The AI decided this query didn't need a search
|
||||
</span>
|
||||
<span className="desktop:hidden">No search</span>
|
||||
</div>
|
||||
|
||||
<div className="ml-auto my-auto" onClick={handleForceSearch}>
|
||||
|
@@ -43,15 +43,14 @@ export default async function Page({
|
||||
|
||||
return (
|
||||
<>
|
||||
<InstantSSRAutoRefresh />
|
||||
{shouldShowWelcomeModal && <WelcomeModal user={user} />}
|
||||
{/* <InstantSSRAutoRefresh /> */}
|
||||
{/* {shouldShowWelcomeModal && <WelcomeModal user={user} />}
|
||||
{!shouldShowWelcomeModal && !shouldDisplaySourcesIncompleteModal && (
|
||||
<ApiKeyModal user={user} />
|
||||
)}
|
||||
{shouldDisplaySourcesIncompleteModal && (
|
||||
<NoCompleteSourcesModal ccPairs={ccPairs} />
|
||||
)}
|
||||
|
||||
)} */}
|
||||
<ChatProvider
|
||||
value={{
|
||||
user,
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ChatSession } from "../interfaces";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useContext } from "react";
|
||||
import { deleteChatSession, renameChatSession } from "../lib";
|
||||
import { DeleteChatModal } from "../modal/DeleteChatModal";
|
||||
import { BasicSelectable } from "@/components/BasicClickable";
|
||||
@@ -19,12 +19,14 @@ import { DefaultDropdownElement } from "@/components/Dropdown";
|
||||
import { Popover } from "@/components/popover/Popover";
|
||||
import { ShareChatSessionModal } from "../modal/ShareChatSessionModal";
|
||||
import { CHAT_SESSION_ID_KEY, FOLDER_ID_KEY } from "@/lib/drag/constants";
|
||||
import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||
|
||||
export function ChatSessionDisplay({
|
||||
chatSession,
|
||||
search,
|
||||
isSelected,
|
||||
skipGradient,
|
||||
closeSidebar,
|
||||
}: {
|
||||
chatSession: ChatSession;
|
||||
isSelected: boolean;
|
||||
@@ -32,6 +34,7 @@ export function ChatSessionDisplay({
|
||||
// needed when the parent is trying to apply some background effect
|
||||
// if not set, the gradient will still be applied and cause weirdness
|
||||
skipGradient?: boolean;
|
||||
closeSidebar?: () => void;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const [isDeletionModalVisible, setIsDeletionModalVisible] = useState(false);
|
||||
@@ -62,6 +65,7 @@ export function ChatSessionDisplay({
|
||||
alert("Failed to rename chat session");
|
||||
}
|
||||
};
|
||||
const settings = useContext(SettingsContext);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -92,6 +96,11 @@ export function ChatSessionDisplay({
|
||||
<Link
|
||||
className="flex my-1 group relative"
|
||||
key={chatSession.id}
|
||||
onClick={() => {
|
||||
if (settings?.isMobile && closeSidebar) {
|
||||
closeSidebar();
|
||||
}
|
||||
}}
|
||||
href={
|
||||
search
|
||||
? `/search?searchId=${chatSession.id}`
|
||||
|
@@ -48,6 +48,7 @@ interface HistorySidebarProps {
|
||||
openedFolders?: { [key: number]: boolean };
|
||||
toggleSidebar?: () => void;
|
||||
toggled?: boolean;
|
||||
removeToggle?: () => void;
|
||||
}
|
||||
|
||||
export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
|
||||
@@ -60,6 +61,7 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
|
||||
folders,
|
||||
openedFolders,
|
||||
toggleSidebar,
|
||||
removeToggle,
|
||||
},
|
||||
ref: ForwardedRef<HTMLDivElement>
|
||||
) => {
|
||||
@@ -101,11 +103,11 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
|
||||
transition-transform`}
|
||||
>
|
||||
<div className="max-w-full ml-3 mr-3 mt-2 flex flex gap-x-1 items-center my-auto text-text-700 text-xl">
|
||||
<div className="mr-1 invisible mb-auto h-6 w-6">
|
||||
<div className="mr-1 desktop:invisible mb-auto h-6 w-6">
|
||||
<Logo height={24} width={24} />
|
||||
</div>
|
||||
|
||||
<div className="invisible">
|
||||
<div className="desktop:invisible">
|
||||
{enterpriseSettings && enterpriseSettings.application_name ? (
|
||||
<div>
|
||||
<HeaderTitle>
|
||||
@@ -119,13 +121,18 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
|
||||
<HeaderTitle>Danswer</HeaderTitle>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{toggleSidebar && (
|
||||
<Tooltip
|
||||
delayDuration={0}
|
||||
content={toggled ? `Unpin sidebar` : "Pin sidebar"}
|
||||
>
|
||||
<button className="my-auto ml-auto" onClick={toggleSidebar}>
|
||||
{!toggled ? <RightToLineIcon /> : <LefToLineIcon />}
|
||||
{!toggled && !combinedSettings.isMobile ? (
|
||||
<RightToLineIcon />
|
||||
) : (
|
||||
<LefToLineIcon />
|
||||
)}
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
@@ -182,6 +189,7 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
|
||||
<div className="border-b border-border pb-4 mx-3" />
|
||||
|
||||
<PagesTab
|
||||
closeSidebar={removeToggle}
|
||||
page={page}
|
||||
existingChats={existingChats}
|
||||
currentChatId={currentChatId}
|
||||
|
@@ -16,12 +16,14 @@ export function PagesTab({
|
||||
currentChatId,
|
||||
folders,
|
||||
openedFolders,
|
||||
closeSidebar,
|
||||
}: {
|
||||
page: pageType;
|
||||
existingChats?: ChatSession[];
|
||||
currentChatId?: number;
|
||||
folders?: Folder[];
|
||||
openedFolders?: { [key: number]: boolean };
|
||||
closeSidebar?: () => void;
|
||||
}) {
|
||||
const groupedChatSessions = existingChats
|
||||
? groupSessionsByDateRange(existingChats)
|
||||
@@ -58,7 +60,7 @@ export function PagesTab({
|
||||
const isHistoryEmpty = !existingChats || existingChats.length === 0;
|
||||
|
||||
return (
|
||||
<div className="mb-1 ml-3 relative miniscroll overflow-y-auto h-full">
|
||||
<div className="mb-1 ml-3 relative miniscroll mobile:pb-40 overflow-y-auto h-full">
|
||||
{folders && folders.length > 0 && (
|
||||
<div className="py-2 border-b border-border">
|
||||
<div className="text-xs text-subtle flex pb-0.5 mb-1.5 mt-2 font-bold">
|
||||
@@ -115,6 +117,7 @@ export function PagesTab({
|
||||
return (
|
||||
<div key={`${chat.id}-${chat.name}`}>
|
||||
<ChatSessionDisplay
|
||||
closeSidebar={closeSidebar}
|
||||
search={page == "search"}
|
||||
chatSession={chat}
|
||||
isSelected={isSelected}
|
||||
|
@@ -14,8 +14,8 @@ export default function FixedLogo() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="absolute flex z-40 left-2.5 top-2">
|
||||
<div className="max-w-[200px] flex items-center gap-x-1 my-auto">
|
||||
<div className="fixed pointer-events-none flex z-40 left-2.5 top-2">
|
||||
<div className="max-w-[200px] mobile:hidden flex items-center gap-x-1 my-auto">
|
||||
<div className="flex-none my-auto">
|
||||
<Logo height={24} width={24} />
|
||||
</div>
|
||||
@@ -33,7 +33,7 @@ export default function FixedLogo() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute left-2.5 bottom-4">
|
||||
<div className="mobile:hidden fixed left-2.5 bottom-4">
|
||||
<FiSidebar />
|
||||
</div>
|
||||
</>
|
||||
|
@@ -10,6 +10,7 @@ const ToggleSwitch = () => {
|
||||
const commandSymbol = KeyboardSymbol();
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const settings = useContext(SettingsContext);
|
||||
|
||||
const [activeTab, setActiveTab] = useState(() => {
|
||||
return pathname == "/search" ? "search" : "chat";
|
||||
@@ -27,13 +28,17 @@ const ToggleSwitch = () => {
|
||||
const handleTabChange = (tab: string) => {
|
||||
setActiveTab(tab);
|
||||
localStorage.setItem("activeTab", tab);
|
||||
if (settings?.isMobile) {
|
||||
window.location.href = tab;
|
||||
} else {
|
||||
router.push(tab === "search" ? "/search" : "/chat");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-gray-100 flex rounded-full p-1">
|
||||
<div className="bg-gray-100 mobile:mt-8 flex rounded-full p-1">
|
||||
<div
|
||||
className={`absolute top-1 bottom-1 ${
|
||||
className={`absolute mobile:mt-8 top-1 bottom-1 ${
|
||||
activeTab === "chat" ? "w-[45%]" : "w-[50%]"
|
||||
} bg-white rounded-full shadow ${
|
||||
isInitialLoad ? "" : "transition-transform duration-300 ease-in-out"
|
||||
@@ -116,15 +121,19 @@ export default function FunctionalWrapper({
|
||||
|
||||
const [toggledSidebar, setToggledSidebar] = useState(initiallyToggled);
|
||||
|
||||
const toggle = () => {
|
||||
setToggledSidebar((toggledSidebar) => !toggledSidebar);
|
||||
const toggle = (value?: boolean) => {
|
||||
if (value !== undefined) {
|
||||
setToggledSidebar(value);
|
||||
} else {
|
||||
setToggledSidebar((prevState) => !prevState);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{(!settings ||
|
||||
(settings.search_page_enabled && settings.chat_page_enabled)) && (
|
||||
<div className="z-[40] flex fixed top-4 left-1/2 transform -translate-x-1/2">
|
||||
<div className="mobile:hidden z-30 flex fixed top-4 left-1/2 transform -translate-x-1/2">
|
||||
<div
|
||||
style={{ transition: "width 0.30s ease-out" }}
|
||||
className={`flex-none overflow-y-hidden bg-background-100 transition-all bg-opacity-80 duration-300 ease-in-out h-full
|
||||
@@ -136,7 +145,7 @@ export default function FunctionalWrapper({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="absolute left-0 top-0 w-full h-full">
|
||||
<div className="overscroll-y-contain overflow-y-scroll overscroll-contain left-0 top-0 w-full h-svh">
|
||||
{content(toggledSidebar, toggle)}
|
||||
</div>
|
||||
</>
|
||||
|
@@ -111,6 +111,10 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.prevent-scroll {
|
||||
overscroll-behavior-y: none;
|
||||
}
|
||||
|
||||
body {
|
||||
overscroll-behavior-y: none;
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@ import { SettingsProvider } from "@/components/settings/SettingsProvider";
|
||||
import { Metadata } from "next";
|
||||
import { buildClientUrl } from "@/lib/utilsSS";
|
||||
import { Inter } from "next/font/google";
|
||||
import Head from "next/head";
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
@@ -41,6 +42,13 @@ export default async function RootLayout({
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<Head>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0, interactive-widget=resizes-content"
|
||||
/>
|
||||
</Head>
|
||||
|
||||
{CUSTOM_ANALYTICS_ENABLED && combinedSettings.customAnalyticsScript && (
|
||||
<head>
|
||||
<script
|
||||
@@ -51,7 +59,9 @@ export default async function RootLayout({
|
||||
/>
|
||||
</head>
|
||||
)}
|
||||
<body
|
||||
|
||||
<body className="relative">
|
||||
<div
|
||||
className={`${inter.variable} font-sans text-default bg-background ${
|
||||
// TODO: remove this once proper dark mode exists
|
||||
process.env.THEME_IS_DARK?.toLowerCase() === "true" ? "dark" : ""
|
||||
@@ -60,6 +70,7 @@ export default async function RootLayout({
|
||||
<SettingsProvider settings={combinedSettings}>
|
||||
{children}
|
||||
</SettingsProvider>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
@@ -187,7 +187,6 @@ export default async function Home() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header user={user} />
|
||||
<HealthCheckBanner secondsUntilExpiration={secondsUntilExpiration} />
|
||||
{shouldShowWelcomeModal && <WelcomeModal user={user} />}
|
||||
|
||||
|
@@ -54,7 +54,7 @@ export function UserDropdown({
|
||||
onClick={() => setUserInfoVisible(!userInfoVisible)}
|
||||
className="flex cursor-pointer"
|
||||
>
|
||||
<div className="my-auto bg-background-strong ring-2 ring-transparent group-hover:ring-background-300/50 transition-ring duration-150 rounded-lg inline-block flex-none px-2 text-base font-normal">
|
||||
<div className="my-auto bg-background-strong ring-2 ring-transparent group-hover:ring-background-300/50 transition-ring duration-150 rounded-lg inline-block flex-none px-2 text-base ">
|
||||
{user && user.email ? user.email[0].toUpperCase() : "A"}
|
||||
</div>
|
||||
</div>
|
||||
@@ -84,9 +84,10 @@ export function UserDropdown({
|
||||
<>
|
||||
<Link
|
||||
href="/admin/indexing/status"
|
||||
className="flex py-3 px-4 cursor-pointer rounded hover:bg-hover-light"
|
||||
className="flex py-3 px-4 cursor-pointer !
|
||||
rounded hover:bg-hover-light"
|
||||
>
|
||||
<LightSettingsIcon className="h-5 w-5 text-text-200est0 my-auto mr-2" />
|
||||
<LightSettingsIcon className="h-5 w-5 my-auto mr-2" />
|
||||
Admin Panel
|
||||
</Link>
|
||||
</>
|
||||
|
@@ -18,20 +18,19 @@ import { SettingsContext } from "../settings/SettingsProvider";
|
||||
import { pageType } from "@/app/chat/sessionSidebar/types";
|
||||
|
||||
export default function FunctionalHeader({
|
||||
showSidebar,
|
||||
user,
|
||||
page,
|
||||
currentChatSession,
|
||||
setSharingModalVisible,
|
||||
toggleSidebar,
|
||||
}: {
|
||||
page: pageType;
|
||||
showSidebar: boolean;
|
||||
user: User | null;
|
||||
currentChatSession?: ChatSession | null | undefined;
|
||||
setSharingModalVisible?: (value: SetStateAction<boolean>) => void;
|
||||
toggleSidebar: () => void;
|
||||
}) {
|
||||
const combinedSettings = useContext(SettingsContext);
|
||||
const settings = combinedSettings?.settings;
|
||||
const enterpriseSettings = combinedSettings?.enterpriseSettings;
|
||||
|
||||
const commandSymbol = KeyboardSymbol();
|
||||
@@ -60,12 +59,15 @@ export default function FunctionalHeader({
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<div className="pb-6 left-0 sticky top-0 z-10 w-full relative flex">
|
||||
<div className="pb-6 left-0 sticky top-0 z-20 w-full relative flex">
|
||||
<div className="mt-2 mx-4 text-text-700 flex w-full">
|
||||
<div className="absolute z-[100] my-auto flex items-center text-xl font-bold">
|
||||
<div className="pt-[2px] invisible mb-auto">
|
||||
<button
|
||||
onClick={() => toggleSidebar()}
|
||||
className="pt-[2px] desktop:invisible mb-auto"
|
||||
>
|
||||
<FiSidebar size={20} />
|
||||
</div>
|
||||
</button>
|
||||
<div className="invisible break-words inline-block w-fit ml-2 text-text-700 text-xl">
|
||||
<div className="max-w-[200px]">
|
||||
{enterpriseSettings && enterpriseSettings.application_name ? (
|
||||
@@ -86,7 +88,7 @@ export default function FunctionalHeader({
|
||||
{page == "chat" && (
|
||||
<Tooltip delayDuration={1000} content={`${commandSymbol}U`}>
|
||||
<Link
|
||||
className="my-auto"
|
||||
className="mobile:hidden my-auto"
|
||||
href={
|
||||
`/${page}` +
|
||||
(NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA &&
|
||||
@@ -107,15 +109,29 @@ export default function FunctionalHeader({
|
||||
{setSharingModalVisible && (
|
||||
<div
|
||||
onClick={() => setSharingModalVisible(true)}
|
||||
className="my-auto rounded cursor-pointer hover:bg-hover-light"
|
||||
className="mobile:hidden my-auto rounded cursor-pointer hover:bg-hover-light"
|
||||
>
|
||||
<FiShare2 size="18" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex my-auto">
|
||||
<div className="mobile:hidden flex my-auto">
|
||||
<UserDropdown user={user} />
|
||||
</div>
|
||||
<Link
|
||||
className="desktop:hidden my-auto"
|
||||
href={
|
||||
`/${page}` +
|
||||
(NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA &&
|
||||
currentChatSession
|
||||
? `?assistantId=${currentChatSession.persona_id}`
|
||||
: "")
|
||||
}
|
||||
>
|
||||
<div className="cursor-pointer ml-2 flex-none text-text-700 hover:text-text-600 transition-colors duration-300">
|
||||
<NewChatIcon size={20} />
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@@ -5,18 +5,22 @@ interface UseSidebarVisibilityProps {
|
||||
sidebarElementRef: React.RefObject<HTMLElement>;
|
||||
showDocSidebar: boolean;
|
||||
setShowDocSidebar: Dispatch<SetStateAction<boolean>>;
|
||||
mobile?: boolean;
|
||||
setToggled?: () => void;
|
||||
}
|
||||
|
||||
export const useSidebarVisibility = ({
|
||||
toggledSidebar,
|
||||
sidebarElementRef,
|
||||
setShowDocSidebar,
|
||||
setToggled,
|
||||
showDocSidebar,
|
||||
mobile,
|
||||
}: UseSidebarVisibilityProps) => {
|
||||
const xPosition = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
const handleEvent = (event: MouseEvent) => {
|
||||
const currentXPosition = event.clientX;
|
||||
xPosition.current = currentXPosition;
|
||||
|
||||
@@ -28,11 +32,20 @@ export const useSidebarVisibility = ({
|
||||
currentXPosition <= sidebarRect.right &&
|
||||
event.clientY >= sidebarRect.top &&
|
||||
event.clientY <= sidebarRect.bottom;
|
||||
|
||||
const sidebarStyle = window.getComputedStyle(sidebarElementRef.current);
|
||||
const isVisible = sidebarStyle.opacity !== "0";
|
||||
if (isWithinSidebar && isVisible) {
|
||||
if (!mobile) {
|
||||
setShowDocSidebar(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (mobile && !isWithinSidebar && setToggled) {
|
||||
setToggled();
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
currentXPosition > 100 &&
|
||||
showDocSidebar &&
|
||||
@@ -41,17 +54,19 @@ export const useSidebarVisibility = ({
|
||||
) {
|
||||
setShowDocSidebar(false);
|
||||
} else if (currentXPosition < 100 && !showDocSidebar) {
|
||||
if (!mobile) {
|
||||
setShowDocSidebar(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
document.addEventListener("mousemove", handleEvent);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mousemove", handleEvent);
|
||||
};
|
||||
}, [showDocSidebar, toggledSidebar, sidebarElementRef]);
|
||||
}, [showDocSidebar, toggledSidebar, sidebarElementRef, mobile]);
|
||||
|
||||
return { showDocSidebar };
|
||||
};
|
||||
|
@@ -3,7 +3,6 @@
|
||||
import { User } from "@/lib/types";
|
||||
import Link from "next/link";
|
||||
import React, { useContext } from "react";
|
||||
import { FiMessageSquare, FiSearch } from "react-icons/fi";
|
||||
import { HeaderWrapper } from "./HeaderWrapper";
|
||||
import { SettingsContext } from "../settings/SettingsProvider";
|
||||
import { UserDropdown } from "../UserDropdown";
|
||||
|
@@ -6,4 +6,5 @@
|
||||
|
||||
.PopoverContent[data-side="top"] {
|
||||
transform: translateY(var(--radix-popover-trigger-height) px);
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
@@ -1,4 +1,12 @@
|
||||
import React, { useState, useRef, useEffect, ReactNode } from "react";
|
||||
"use client";
|
||||
import React, {
|
||||
useState,
|
||||
useRef,
|
||||
useEffect,
|
||||
ReactNode,
|
||||
useContext,
|
||||
} from "react";
|
||||
import { SettingsContext } from "../settings/SettingsProvider";
|
||||
|
||||
interface PopupProps {
|
||||
children: JSX.Element;
|
||||
@@ -6,9 +14,11 @@ interface PopupProps {
|
||||
close: () => void,
|
||||
ref?: React.RefObject<HTMLDivElement>
|
||||
) => ReactNode;
|
||||
position?: "top" | "bottom" | "left" | "right";
|
||||
position?: "top" | "bottom" | "left" | "right" | "top-right";
|
||||
removePadding?: boolean;
|
||||
tab?: boolean;
|
||||
flexPriority?: "shrink" | "stiff" | "second";
|
||||
mobilePosition?: "top" | "bottom" | "left" | "right" | "top-right";
|
||||
}
|
||||
|
||||
const Popup: React.FC<PopupProps> = ({
|
||||
@@ -17,6 +27,8 @@ const Popup: React.FC<PopupProps> = ({
|
||||
tab,
|
||||
removePadding,
|
||||
position = "top",
|
||||
flexPriority,
|
||||
mobilePosition,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const triggerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -51,6 +63,8 @@ const Popup: React.FC<PopupProps> = ({
|
||||
};
|
||||
}, []);
|
||||
|
||||
const settings = useContext(SettingsContext);
|
||||
|
||||
const getPopupStyle = (): React.CSSProperties => {
|
||||
if (!triggerRef.current) return {};
|
||||
|
||||
@@ -60,7 +74,11 @@ const Popup: React.FC<PopupProps> = ({
|
||||
zIndex: 10,
|
||||
};
|
||||
|
||||
switch (position) {
|
||||
const currentPosition = settings?.isMobile
|
||||
? mobilePosition || position
|
||||
: position;
|
||||
|
||||
switch (currentPosition) {
|
||||
case "bottom":
|
||||
popupStyle.top = `${triggerRect.height + 5}px`;
|
||||
popupStyle.left = "50%";
|
||||
@@ -76,9 +94,14 @@ const Popup: React.FC<PopupProps> = ({
|
||||
popupStyle.left = `${triggerRect.width + 5}px`;
|
||||
popupStyle.transform = "translateY(-50%)";
|
||||
break;
|
||||
case "top-right":
|
||||
popupStyle.bottom = `${triggerRect.height + 5}px`;
|
||||
popupStyle.right = "0";
|
||||
popupStyle.right = "auto";
|
||||
break;
|
||||
default: // top
|
||||
popupStyle.bottom = `${triggerRect.height + 5}px`;
|
||||
popupStyle.left = `${triggerRect.width + 50}px`;
|
||||
popupStyle.left = "50%";
|
||||
popupStyle.transform = "translateX(-50%)";
|
||||
}
|
||||
|
||||
@@ -86,7 +109,19 @@ const Popup: React.FC<PopupProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative inline-block">
|
||||
<div
|
||||
className={`relative inline-block
|
||||
${
|
||||
flexPriority === "shrink" &&
|
||||
"flex-shrink-100 flex-grow-0 flex-basis-auto min-w-[30px] whitespace-nowrap "
|
||||
}
|
||||
${
|
||||
flexPriority === "second" &&
|
||||
"flex-shrink flex-basis-0 min-w-[30px] whitespace-nowrap "
|
||||
}
|
||||
${flexPriority === "stiff" && "flex-none whitespace-nowrap "}
|
||||
`}
|
||||
>
|
||||
<div ref={triggerRef} onClick={togglePopup} className="cursor-pointer">
|
||||
{children}
|
||||
</div>
|
||||
@@ -96,7 +131,7 @@ const Popup: React.FC<PopupProps> = ({
|
||||
ref={popupRef}
|
||||
className={`absolute bg-white border border-gray-200 rounded-lg shadow-lg ${
|
||||
!removePadding && "p-4"
|
||||
} ${tab ? " w-[400px] " : "min-w-[400px]"}`}
|
||||
} ${!settings?.isMobile ? (tab ? "w-[400px] " : "min-w-[400px]") : "w-[250px]"}`}
|
||||
style={getPopupStyle()}
|
||||
>
|
||||
{content(closePopup, contentRef)}
|
||||
|
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
DanswerDocument,
|
||||
DocumentRelevance,
|
||||
@@ -5,7 +7,7 @@ import {
|
||||
SearchDanswerDocument,
|
||||
} from "@/lib/search/interfaces";
|
||||
import { DocumentFeedbackBlock } from "./DocumentFeedbackBlock";
|
||||
import { useState } from "react";
|
||||
import { useContext, useState } from "react";
|
||||
import { PopupSpec } from "../admin/connectors/Popup";
|
||||
import { DocumentUpdatedAtBadge } from "./DocumentUpdatedAtBadge";
|
||||
import { SourceIcon } from "../SourceIcon";
|
||||
@@ -15,6 +17,7 @@ import { BookIcon, CheckmarkIcon, LightBulbIcon, XIcon } from "../icons/icons";
|
||||
import { FaStar } from "react-icons/fa";
|
||||
import { FiTag } from "react-icons/fi";
|
||||
import { DISABLE_AGENTIC_SEARCH } from "@/lib/constants";
|
||||
import { SettingsContext } from "../settings/SettingsProvider";
|
||||
|
||||
export const buildDocumentSummaryDisplay = (
|
||||
matchHighlights: string[],
|
||||
@@ -178,11 +181,12 @@ export const DocumentDisplay = ({
|
||||
const [alternativeToggled, setAlternativeToggled] = useState(false);
|
||||
const relevance_explanation =
|
||||
document.relevance_explanation ?? additional_relevance?.content;
|
||||
const settings = useContext(SettingsContext);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={document.semantic_identifier}
|
||||
className={`text-sm border-b border-border transition-all duration-500
|
||||
className={`text-sm mobile:ml-4 border-b border-border transition-all duration-500
|
||||
${hide ? "transform translate-x-full opacity-0" : ""}
|
||||
${!hide ? "pt-3" : "border-transparent"} relative`}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
@@ -234,7 +238,7 @@ export const DocumentDisplay = ({
|
||||
|
||||
{(contentEnriched || additional_relevance) &&
|
||||
relevance_explanation &&
|
||||
(isHovered || alternativeToggled) && (
|
||||
(isHovered || alternativeToggled || settings?.isMobile) && (
|
||||
<button
|
||||
onClick={() =>
|
||||
setAlternativeToggled(
|
||||
@@ -242,7 +246,9 @@ export const DocumentDisplay = ({
|
||||
)
|
||||
}
|
||||
>
|
||||
<LightBulbIcon className="text-blue-400 h-4 w-4 cursor-pointer" />
|
||||
<LightBulbIcon
|
||||
className={`${settings?.isMobile && alternativeToggled ? "text-green-600" : "text-blue-600"} h-4 w-4 cursor-pointer`}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -286,7 +292,7 @@ export const AgenticDocumentDisplay = ({
|
||||
return (
|
||||
<div
|
||||
key={document.semantic_identifier}
|
||||
className={`text-sm border-b border-border transition-all duration-500
|
||||
className={`text-sm mobile:ml-4 border-b border-border transition-all duration-500
|
||||
${!hide ? "transform translate-x-full opacity-0" : ""}
|
||||
${hide ? "py-3" : "border-transparent"} relative`}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import React, { KeyboardEvent, ChangeEvent } from "react";
|
||||
import React, { KeyboardEvent, ChangeEvent, useContext } from "react";
|
||||
import { searchState } from "./SearchSection";
|
||||
|
||||
import { MagnifyingGlass } from "@phosphor-icons/react";
|
||||
@@ -17,6 +17,7 @@ import { SendIcon } from "../icons/icons";
|
||||
import { Divider } from "@tremor/react";
|
||||
import { CustomTooltip } from "../tooltip/CustomTooltip";
|
||||
import KeyboardSymbol from "@/lib/browserUtilities";
|
||||
import { SettingsContext } from "../settings/SettingsProvider";
|
||||
|
||||
export const AnimatedToggle = ({
|
||||
isOn,
|
||||
@@ -139,6 +140,8 @@ export const FullSearchBar = ({
|
||||
}
|
||||
};
|
||||
|
||||
const settings = useContext(SettingsContext);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="
|
||||
@@ -198,13 +201,17 @@ export const FullSearchBar = ({
|
||||
|
||||
{searchState == "reading" && (
|
||||
<div key={"Reading"} className="mr-auto relative inline-block">
|
||||
<span className="loading-text">Reading Documents...</span>
|
||||
<span className="loading-text">
|
||||
Reading{settings?.isMobile ? "" : " Documents"}...
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{searchState == "analyzing" && (
|
||||
<div key={"Generating"} className="mr-auto relative inline-block">
|
||||
<span className="loading-text">Generating Analysis...</span>
|
||||
<span className="loading-text">
|
||||
Generating{settings?.isMobile ? "" : " Analysis"}...
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
@@ -23,7 +23,7 @@ import { useFilters, useObjectState } from "@/lib/hooks";
|
||||
import { questionValidationStreamed } from "@/lib/search/streamingQuestionValidation";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { computeAvailableFilters } from "@/lib/filters";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { redirect, useRouter, useSearchParams } from "next/navigation";
|
||||
import { SettingsContext } from "../settings/SettingsProvider";
|
||||
import { HistorySidebar } from "@/app/chat/sessionSidebar/HistorySidebar";
|
||||
import { ChatSession, SearchSession } from "@/app/chat/interfaces";
|
||||
@@ -339,7 +339,6 @@ export const SearchSection = ({
|
||||
if ((overrideMessage || query) == "") {
|
||||
return;
|
||||
}
|
||||
setSearchResponse;
|
||||
setAgenticResults(agentic!);
|
||||
resetInput();
|
||||
setContentEnriched(false);
|
||||
@@ -456,6 +455,12 @@ export const SearchSection = ({
|
||||
};
|
||||
}, [router]);
|
||||
|
||||
useEffect(() => {
|
||||
if (settings?.isMobile) {
|
||||
router.push("/chat");
|
||||
}
|
||||
}, [settings?.isMobile, router]);
|
||||
|
||||
const handleTransitionEnd = (e: React.TransitionEvent<HTMLDivElement>) => {
|
||||
if (e.propertyName === "opacity" && !firstSearch) {
|
||||
const target = e.target as HTMLDivElement;
|
||||
@@ -474,20 +479,19 @@ export const SearchSection = ({
|
||||
sidebarElementRef,
|
||||
showDocSidebar,
|
||||
setShowDocSidebar,
|
||||
mobile: settings?.isMobile,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex relative w-full pr-[8px] h-full text-default overflow-x-hidden">
|
||||
<div className="flex relative w-full pr-[8px] h-full text-default">
|
||||
<div
|
||||
ref={sidebarElementRef}
|
||||
className={`
|
||||
flex-none
|
||||
fixed -8
|
||||
fixed
|
||||
left-0
|
||||
z-20
|
||||
overflow-y-hidden
|
||||
sidebar
|
||||
z-30
|
||||
bg-background-100
|
||||
h-screen
|
||||
transition-all
|
||||
@@ -514,7 +518,7 @@ export const SearchSection = ({
|
||||
|
||||
<div className="absolute left-0 w-full top-0">
|
||||
<FunctionalHeader
|
||||
showSidebar={showDocSidebar}
|
||||
toggleSidebar={toggleSidebar}
|
||||
page="search"
|
||||
user={user}
|
||||
/>
|
||||
@@ -535,9 +539,10 @@ export const SearchSection = ({
|
||||
/>
|
||||
|
||||
{
|
||||
<div className="px-24 w-full pt-10 relative max-w-[2000px] xl:max-w-[1430px] mx-auto">
|
||||
<div className="absolute z-10 top-12 left-0 hidden 2xl:block w-52 3xl:w-64">
|
||||
{(ccPairs.length > 0 || documentSets.length > 0) && (
|
||||
<div className="desktop:px-24 w-full pt-10 relative max-w-[2000px] xl:max-w-[1430px] mx-auto">
|
||||
<div className="absolute z-10 mobile:px-4 mobile:max-w-searchbar-max mobile:w-[90%] top-12 desktop:left-0 hidden 2xl:block mobile:left-1/2 mobile:transform mobile:-translate-x-1/2 desktop:w-52 3xl:w-64">
|
||||
{!settings?.isMobile &&
|
||||
(ccPairs.length > 0 || documentSets.length > 0) && (
|
||||
<SourceSelector
|
||||
{...filterManager}
|
||||
showDocSidebar={showDocSidebar || toggledSidebar}
|
||||
@@ -549,6 +554,28 @@ export const SearchSection = ({
|
||||
</div>
|
||||
<div className="absolute left-0 hidden 2xl:block w-52 3xl:w-64"></div>
|
||||
<div className="max-w-searchbar-max w-[90%] mx-auto">
|
||||
{settings?.isMobile && (
|
||||
<div className="mt-6">
|
||||
{!(agenticResults && isFetching) || disabledAgentic ? (
|
||||
<SearchResultsDisplay
|
||||
disabledAgentic={disabledAgentic}
|
||||
contentEnriched={contentEnriched}
|
||||
comments={comments}
|
||||
sweep={sweep}
|
||||
agenticResults={agenticResults && !disabledAgentic}
|
||||
performSweep={performSweep}
|
||||
searchResponse={searchResponse}
|
||||
isFetching={isFetching}
|
||||
defaultOverrides={defaultOverrides}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`mobile:fixed mobile:left-1/2 mobile:transform mobile:-translate-x-1/2 mobile:max-w-search-bar-max mobile:w-[90%] mobile:z-100 mobile:bottom-12`}
|
||||
>
|
||||
<div
|
||||
className={`transition-all duration-500 ease-in-out overflow-hidden
|
||||
${
|
||||
@@ -568,9 +595,10 @@ export const SearchSection = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FullSearchBar
|
||||
toggleAgentic={disabledAgentic ? undefined : toggleAgentic}
|
||||
toggleAgentic={
|
||||
disabledAgentic ? undefined : toggleAgentic
|
||||
}
|
||||
agentic={agentic}
|
||||
searchState={searchState}
|
||||
query={query}
|
||||
@@ -580,7 +608,9 @@ export const SearchSection = ({
|
||||
await onSearch({ agentic, offset: 0 });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!settings?.isMobile && (
|
||||
<div className="mt-6">
|
||||
{!(agenticResults && isFetching) || disabledAgentic ? (
|
||||
<SearchResultsDisplay
|
||||
@@ -598,6 +628,7 @@ export const SearchSection = ({
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { CombinedSettings } from "@/app/admin/settings/interfaces";
|
||||
import { createContext } from "react";
|
||||
import { createContext, useEffect, useState } from "react";
|
||||
|
||||
export const SettingsContext = createContext<CombinedSettings | null>(null);
|
||||
|
||||
@@ -12,8 +12,20 @@ export function SettingsProvider({
|
||||
children: React.ReactNode | JSX.Element;
|
||||
settings: CombinedSettings;
|
||||
}) {
|
||||
const [isMobile, setIsMobile] = useState<boolean | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
const checkMobile = () => {
|
||||
setIsMobile(window.innerWidth < 768);
|
||||
};
|
||||
|
||||
checkMobile();
|
||||
window.addEventListener("resize", checkMobile);
|
||||
return () => window.removeEventListener("resize", checkMobile);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SettingsContext.Provider value={settings}>
|
||||
<SettingsContext.Provider value={{ ...settings, isMobile }}>
|
||||
{children}
|
||||
</SettingsContext.Provider>
|
||||
);
|
||||
|
@@ -36,6 +36,7 @@ export interface CombinedSettings {
|
||||
settings: Settings;
|
||||
enterpriseSettings: EnterpriseSettings | null;
|
||||
customAnalyticsScript: string | null;
|
||||
isMobile?: boolean;
|
||||
}
|
||||
|
||||
let cachedSettings: CombinedSettings;
|
||||
|
@@ -45,6 +45,8 @@ module.exports = {
|
||||
"2xl": "1420px",
|
||||
"3xl": "1700px",
|
||||
"4xl": "2000px",
|
||||
mobile: { max: "767px" },
|
||||
desktop: "768px",
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ["var(--font-inter)"],
|
||||
|
Reference in New Issue
Block a user