diff --git a/backend/danswer/chat/process_message.py b/backend/danswer/chat/process_message.py index f09d3b27c..2c3a0a332 100644 --- a/backend/danswer/chat/process_message.py +++ b/backend/danswer/chat/process_message.py @@ -694,6 +694,7 @@ def stream_chat_message_objects( except Exception as e: error_msg = str(e) + logger.exception(f"Failed to process chat message: {error_msg}") if "Illegal header value b'Bearer '" in error_msg: diff --git a/backend/ee/danswer/server/auth_check.py b/backend/ee/danswer/server/auth_check.py index d0ba3ffe4..49353abf8 100644 --- a/backend/ee/danswer/server/auth_check.py +++ b/backend/ee/danswer/server/auth_check.py @@ -8,6 +8,7 @@ EE_PUBLIC_ENDPOINT_SPECS = PUBLIC_ENDPOINT_SPECS + [ # needs to be accessible prior to user login ("/enterprise-settings", {"GET"}), ("/enterprise-settings/logo", {"GET"}), + ("/enterprise-settings/logotype", {"GET"}), ("/enterprise-settings/custom-analytics-script", {"GET"}), # oidc ("/auth/oidc/authorize", {"GET"}), diff --git a/backend/ee/danswer/server/enterprise_settings/api.py b/backend/ee/danswer/server/enterprise_settings/api.py index 85f43ca55..736296517 100644 --- a/backend/ee/danswer/server/enterprise_settings/api.py +++ b/backend/ee/danswer/server/enterprise_settings/api.py @@ -12,6 +12,7 @@ from danswer.file_store.file_store import get_default_file_store from ee.danswer.server.enterprise_settings.models import AnalyticsScriptUpload from ee.danswer.server.enterprise_settings.models import EnterpriseSettings from ee.danswer.server.enterprise_settings.store import _LOGO_FILENAME +from ee.danswer.server.enterprise_settings.store import _LOGOTYPE_FILENAME from ee.danswer.server.enterprise_settings.store import load_analytics_script from ee.danswer.server.enterprise_settings.store import load_settings from ee.danswer.server.enterprise_settings.store import store_analytics_script @@ -41,22 +42,38 @@ def fetch_settings() -> EnterpriseSettings: @admin_router.put("/logo") def put_logo( file: UploadFile, + is_logotype: bool = False, db_session: Session = Depends(get_session), _: User | None = Depends(current_admin_user), ) -> None: - upload_logo(file=file, db_session=db_session) + upload_logo(file=file, db_session=db_session, is_logotype=is_logotype) -@basic_router.get("/logo") -def fetch_logo(db_session: Session = Depends(get_session)) -> Response: +def fetch_logo_or_logotype(is_logotype: bool, db_session: Session) -> Response: try: file_store = get_default_file_store(db_session) - file_io = file_store.read_file(_LOGO_FILENAME, mode="b") + filename = _LOGOTYPE_FILENAME if is_logotype else _LOGO_FILENAME + file_io = file_store.read_file(filename, mode="b") # NOTE: specifying "image/jpeg" here, but it still works for pngs # TODO: do this properly return Response(content=file_io.read(), media_type="image/jpeg") except Exception: - raise HTTPException(status_code=404, detail="No logo file found") + raise HTTPException( + status_code=404, + detail=f"No {'logotype' if is_logotype else 'logo'} file found", + ) + + +@basic_router.get("/logotype") +def fetch_logotype(db_session: Session = Depends(get_session)) -> Response: + return fetch_logo_or_logotype(is_logotype=True, db_session=db_session) + + +@basic_router.get("/logo") +def fetch_logo( + is_logotype: bool = False, db_session: Session = Depends(get_session) +) -> Response: + return fetch_logo_or_logotype(is_logotype=is_logotype, db_session=db_session) @admin_router.put("/custom-analytics-script") diff --git a/backend/ee/danswer/server/enterprise_settings/models.py b/backend/ee/danswer/server/enterprise_settings/models.py index c2142c641..c9831d87a 100644 --- a/backend/ee/danswer/server/enterprise_settings/models.py +++ b/backend/ee/danswer/server/enterprise_settings/models.py @@ -8,8 +8,10 @@ class EnterpriseSettings(BaseModel): application_name: str | None = None use_custom_logo: bool = False + use_custom_logotype: bool = False # custom Chat components + custom_lower_disclaimer_content: str | None = None custom_header_content: str | None = None custom_popup_header: str | None = None custom_popup_content: str | None = None diff --git a/backend/ee/danswer/server/enterprise_settings/store.py b/backend/ee/danswer/server/enterprise_settings/store.py index e1418f022..128661cfd 100644 --- a/backend/ee/danswer/server/enterprise_settings/store.py +++ b/backend/ee/danswer/server/enterprise_settings/store.py @@ -63,6 +63,7 @@ def store_analytics_script(analytics_script_upload: AnalyticsScriptUpload) -> No _LOGO_FILENAME = "__logo__" +_LOGOTYPE_FILENAME = "__logotype__" def is_valid_file_type(filename: str) -> bool: @@ -79,8 +80,7 @@ def guess_file_type(filename: str) -> str: def upload_logo( - db_session: Session, - file: UploadFile | str, + db_session: Session, file: UploadFile | str, is_logotype: bool = False ) -> bool: content: IO[Any] @@ -111,7 +111,7 @@ def upload_logo( file_store = get_default_file_store(db_session) file_store.save_file( - file_name=_LOGO_FILENAME, + file_name=_LOGOTYPE_FILENAME if is_logotype else _LOGO_FILENAME, content=content, display_name=display_name, file_origin=FileOrigin.OTHER, diff --git a/web/src/app/admin/settings/interfaces.ts b/web/src/app/admin/settings/interfaces.ts index b6e2b2bd7..910403f1a 100644 --- a/web/src/app/admin/settings/interfaces.ts +++ b/web/src/app/admin/settings/interfaces.ts @@ -8,8 +8,10 @@ export interface Settings { export interface EnterpriseSettings { application_name: string | null; use_custom_logo: boolean; + use_custom_logotype: boolean; // custom Chat components + custom_lower_disclaimer_content: string | null; custom_header_content: string | null; custom_popup_header: string | null; custom_popup_content: string | null; diff --git a/web/src/app/chat/ChatBanner.tsx b/web/src/app/chat/ChatBanner.tsx index 3ce245581..bdd53617f 100644 --- a/web/src/app/chat/ChatBanner.tsx +++ b/web/src/app/chat/ChatBanner.tsx @@ -7,6 +7,7 @@ import remarkGfm from "remark-gfm"; import { Popover } from "@/components/popover/Popover"; import { ChevronDownIcon } from "@/components/icons/icons"; import { Divider } from "@tremor/react"; +import { MinimalMarkdown } from "@/components/chat_search/MinimalMarkdown"; export function ChatBanner() { const settings = useContext(SettingsContext); @@ -79,14 +80,19 @@ export function ChatBanner() { ref={contentRef} className="line-clamp-2 text-center w-full overflow-hidden pr-8" > - {renderMarkdown("")} + -
- {renderMarkdown("")} +
{isOverflowing && ( @@ -104,7 +110,12 @@ export function ChatBanner() { popover={

Banner Content

- {renderMarkdown("max-h-96 overflow-y-auto")} +
} side="bottom" diff --git a/web/src/app/chat/ChatPage.tsx b/web/src/app/chat/ChatPage.tsx index 8f76c4a59..90f56a9d7 100644 --- a/web/src/app/chat/ChatPage.tsx +++ b/web/src/app/chat/ChatPage.tsx @@ -1,4 +1,5 @@ "use client"; +import ReactMarkdown from "react-markdown"; import { redirect, useRouter, useSearchParams } from "next/navigation"; import { @@ -82,6 +83,8 @@ import FixedLogo from "./shared_chat_search/FixedLogo"; import { getSecondsUntilExpiration } from "@/lib/time"; import { SetDefaultModelModal } from "./modal/SetDefaultModelModal"; import { DeleteChatModal } from "./modal/DeleteChatModal"; +import remarkGfm from "remark-gfm"; +import { MinimalMarkdown } from "@/components/chat_search/MinimalMarkdown"; const TEMP_USER_MESSAGE_ID = -1; const TEMP_ASSISTANT_MESSAGE_ID = -2; @@ -289,7 +292,7 @@ export function ChatPage({ } return; } - + clearSelectedDocuments(); setIsFetchingChatMessages(true); const response = await fetch( `/api/chat/get-chat-session/${existingChatSessionId}` @@ -1110,6 +1113,7 @@ export function ChatPage({ // settings are passed in via Context and therefore aren't // available in server-side components const settings = useContext(SettingsContext); + const enterpriseSettings = settings?.enterpriseSettings; if (settings?.settings?.chat_page_enabled === false) { router.push("/search"); } @@ -1694,7 +1698,7 @@ export function ChatPage({ ref={inputRef} className="absolute bottom-0 z-10 w-full" > -
+
{aboveHorizon && (
@@ -1750,7 +1783,7 @@ export function ChatPage({ )}
- + { const [isExpanded, setIsExpanded] = useState(isInitiallyExpanded); - const [isEditing, setIsEditing] = useState(false); + const [isEditing, setIsEditing] = useState(initiallySelected); const [editedFolderName, setEditedFolderName] = useState( folder.folder_name ); @@ -135,6 +134,14 @@ const FolderItem = ({ }; }, []); + const inputRef = useRef(null); + + useEffect(() => { + if (initiallySelected && inputRef.current) { + inputRef.current.focus(); + } + }, [initiallySelected]); + const handleDrop = async (event: React.DragEvent) => { event.preventDefault(); setIsDragOver(false); @@ -170,31 +177,6 @@ const FolderItem = ({ isDragOver ? "bg-hover" : "" }`} > - {showDeleteConfirm && ( -
-

- Are you sure you want to delete {folder.folder_name}? All the - content inside this folder will also be deleted -

-
- - -
-
- )}
setIsHovering(true)} @@ -214,6 +196,7 @@ const FolderItem = ({
{isEditing ? (
-
- -
+ + +
+ } + popover={ +
+

+ Are you sure you want to delete{" "} + {folder.folder_name}? All the content inside + this folder will also be deleted. +

+
+ + +
+
+ } + side="top" + align="center" + /> )} @@ -285,22 +299,25 @@ export const FolderList = ({ folders, currentChatId, openedFolders, + newFolderId, }: { folders: Folder[]; currentChatId?: number; openedFolders?: { [key: number]: boolean }; + newFolderId: number | null; }) => { if (folders.length === 0) { return null; } return ( -
+
{folders.map((folder) => ( { throw new Error("Failed to create folder"); } const data = await response.json(); - return data.folder_id; + return data; } // Function to add a chat session to a folder diff --git a/web/src/app/chat/input/ChatInputBar.tsx b/web/src/app/chat/input/ChatInputBar.tsx index 9c1b64aba..4204b9c5d 100644 --- a/web/src/app/chat/input/ChatInputBar.tsx +++ b/web/src/app/chat/input/ChatInputBar.tsx @@ -268,14 +268,14 @@ export function ChatInputBar({ return (
-
+
@@ -528,6 +528,7 @@ export function ChatInputBar({ mobilePosition="top-right" > void; size?: number; tooltipContent?: React.ReactNode; - options?: { name: string; value: number; onClick?: () => void }[]; flexPriority?: "shrink" | "stiff" | "second"; + toggle?: boolean; } export const ChatInputOption: React.FC = ({ @@ -19,9 +16,9 @@ export const ChatInputOption: React.FC = ({ Icon, // icon: Icon, size = 16, - options, flexPriority, tooltipContent, + toggle, onClick, }) => { const [isDropupVisible, setDropupVisible] = useState(false); @@ -76,7 +73,11 @@ export const ChatInputOption: React.FC = ({ onClick={onClick} > - {name && {name}} +
+ {name && {name}} + {toggle && } +
+ {isTooltipVisible && tooltipContent && (
{ if (scrollableDivRef?.current && !isStreaming) { - if (scrollDist.current < distance) { + if (scrollDist.current < distance - 50) { scrollableDivRef?.current?.scrollBy({ left: 0, top: Math.max(scrollDist.current + 600, 0), diff --git a/web/src/app/chat/sessionSidebar/ChatSessionDisplay.tsx b/web/src/app/chat/sessionSidebar/ChatSessionDisplay.tsx index 77376dc22..8844d9ff4 100644 --- a/web/src/app/chat/sessionSidebar/ChatSessionDisplay.tsx +++ b/web/src/app/chat/sessionSidebar/ChatSessionDisplay.tsx @@ -3,7 +3,11 @@ import { useRouter } from "next/navigation"; import { ChatSession } from "../interfaces"; import { useState, useEffect, useContext } from "react"; -import { deleteChatSession, renameChatSession } from "../lib"; +import { + deleteChatSession, + getChatRetentionInfo, + renameChatSession, +} from "../lib"; import { DeleteChatModal } from "../modal/DeleteChatModal"; import { BasicSelectable } from "@/components/BasicClickable"; import Link from "next/link"; @@ -20,6 +24,8 @@ 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"; +import { WarningCircle } from "@phosphor-icons/react"; +import { CustomTooltip } from "@/components/tooltip/CustomTooltip"; export function ChatSessionDisplay({ chatSession, @@ -41,13 +47,13 @@ export function ChatSessionDisplay({ showDeleteModal?: (chatSession: ChatSession) => void; }) { const router = useRouter(); - const [isDeletionModalVisible, setIsDeletionModalVisible] = useState(false); const [isRenamingChat, setIsRenamingChat] = useState(false); const [isMoreOptionsDropdownOpen, setIsMoreOptionsDropdownOpen] = useState(false); const [isShareModalVisible, setIsShareModalVisible] = useState(false); const [chatName, setChatName] = useState(chatSession.name); const [delayedSkipGradient, setDelayedSkipGradient] = useState(skipGradient); + const settings = useContext(SettingsContext); useEffect(() => { if (skipGradient) { @@ -69,7 +75,15 @@ export function ChatSessionDisplay({ alert("Failed to rename chat session"); } }; - const settings = useContext(SettingsContext); + + if (!settings) { + return <>; + } + + const { daysUntilExpiration, showRetentionWarning } = getChatRetentionInfo( + chatSession, + settings?.settings + ); return ( <> @@ -120,7 +134,7 @@ export function ChatSessionDisplay({ event.preventDefault(); } }} - className="-my-px px-1 mr-2 w-full rounded" + className="-my-px px-1 mr-1 w-full rounded" /> ) : (

@@ -134,10 +148,10 @@ export function ChatSessionDisplay({ {isSelected && (isRenamingChat ? ( -

+
@@ -152,7 +166,25 @@ export function ChatSessionDisplay({
) : ( -
+
+ {!showShareModal && showRetentionWarning && ( + + This chat will expire{" "} + {daysUntilExpiration < 1 + ? "today" + : `in ${daysUntilExpiration} day${daysUntilExpiration !== 1 ? "s" : ""}`} +

+ } + > +
+ +
+
+ )} +
{ @@ -160,7 +192,7 @@ export function ChatSessionDisplay({ !isMoreOptionsDropdownOpen ); }} - className={"-m-1"} + className={"-my-1"} > showDeleteModal(chatSession)} - className={`hover:bg-black/10 p-1 -m-1 rounded ml-2`} + className={`hover:bg-black/10 p-1 -m-1 rounded ml-1`} >
diff --git a/web/src/app/chat/sessionSidebar/HistorySidebar.tsx b/web/src/app/chat/sessionSidebar/HistorySidebar.tsx index 10a4fc052..28bb18531 100644 --- a/web/src/app/chat/sessionSidebar/HistorySidebar.tsx +++ b/web/src/app/chat/sessionSidebar/HistorySidebar.tsx @@ -1,7 +1,13 @@ "use client"; import { FiEdit, FiFolderPlus } from "react-icons/fi"; -import { ForwardedRef, forwardRef, useContext, useEffect } from "react"; +import { + ForwardedRef, + forwardRef, + useContext, + useEffect, + useState, +} from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { ChatSession } from "../interfaces"; @@ -56,6 +62,9 @@ export const HistorySidebar = forwardRef( const router = useRouter(); const { popup, setPopup } = usePopup(); + // For determining intial focus state + const [newFolderId, setNewFolderId] = useState(null); + const currentChatId = currentChatSession?.id; // prevent the NextJS Router cache from causing the chat sidebar to not @@ -69,8 +78,6 @@ export const HistorySidebar = forwardRef( return null; } - const enterpriseSettings = combinedSettings.enterpriseSettings; - const handleNewChat = () => { reset(); const newChatUrl = @@ -133,8 +140,8 @@ export const HistorySidebar = forwardRef( onClick={() => createFolder("New Folder") .then((folderId) => { - console.log(`Folder created with ID: ${folderId}`); router.refresh(); + setNewFolderId(folderId); }) .catch((error) => { console.error("Failed to create folder:", error); @@ -172,6 +179,7 @@ export const HistorySidebar = forwardRef( )}
void; + newFolderId: number | null; showShareModal?: (chatSession: ChatSession) => void; showDeleteModal?: (chatSession: ChatSession) => void; }) { @@ -69,8 +71,10 @@ export function PagesTab({
Chat Folders + {newFolderId ? newFolderId : "Hi"}
null, +}: { + toggleSidebar?: () => void; +}) { const combinedSettings = useContext(SettingsContext); const settings = combinedSettings?.settings; const enterpriseSettings = combinedSettings?.enterpriseSettings; return ( <> -
+
diff --git a/web/src/app/chat/shared_chat_search/FunctionalWrapper.tsx b/web/src/app/chat/shared_chat_search/FunctionalWrapper.tsx index 86fab2eaa..4ef22ef4e 100644 --- a/web/src/app/chat/shared_chat_search/FunctionalWrapper.tsx +++ b/web/src/app/chat/shared_chat_search/FunctionalWrapper.tsx @@ -80,7 +80,10 @@ export default function FunctionalWrapper({ initiallyToggled, content, }: { - content: (toggledSidebar: boolean, toggle: () => void) => ReactNode; + content: ( + toggledSidebar: boolean, + toggle: (toggled?: boolean) => void + ) => ReactNode; initiallyToggled: boolean; }) { const router = useRouter(); @@ -123,11 +126,9 @@ export default function FunctionalWrapper({ const [toggledSidebar, setToggledSidebar] = useState(initiallyToggled); const toggle = (value?: boolean) => { - if (value !== undefined) { - setToggledSidebar(value); - } else { - setToggledSidebar((prevState) => !prevState); - } + setToggledSidebar((toggledSidebar) => + value !== undefined ? value : !toggledSidebar + ); }; return ( diff --git a/web/src/app/ee/admin/whitelabeling/WhitelabelingForm.tsx b/web/src/app/ee/admin/whitelabeling/WhitelabelingForm.tsx index 6b4b7147d..2c8259c0b 100644 --- a/web/src/app/ee/admin/whitelabeling/WhitelabelingForm.tsx +++ b/web/src/app/ee/admin/whitelabeling/WhitelabelingForm.tsx @@ -16,7 +16,8 @@ import { ImageUpload } from "./ImageUpload"; export function WhitelabelingForm() { const router = useRouter(); - const [selectedFile, setSelectedFile] = useState(null); + const [selectedLogo, setSelectedLogo] = useState(null); + const [selectedLogotype, setSelectedLogotype] = useState(null); const settings = useContext(SettingsContext); if (!settings) { @@ -49,27 +50,33 @@ export function WhitelabelingForm() { initialValues={{ application_name: enterpriseSettings?.application_name || null, use_custom_logo: enterpriseSettings?.use_custom_logo || false, + use_custom_logotype: enterpriseSettings?.use_custom_logotype || false, + custom_header_content: enterpriseSettings?.custom_header_content || "", custom_popup_header: enterpriseSettings?.custom_popup_header || "", custom_popup_content: enterpriseSettings?.custom_popup_content || "", + custom_lower_disclaimer_content: + enterpriseSettings?.custom_lower_disclaimer_content || "", }} validationSchema={Yup.object().shape({ application_name: Yup.string().nullable(), use_custom_logo: Yup.boolean().required(), + use_custom_logotype: Yup.boolean().required(), custom_header_content: Yup.string().nullable(), custom_popup_header: Yup.string().nullable(), custom_popup_content: Yup.string().nullable(), + custom_lower_disclaimer_content: Yup.string().nullable(), })} onSubmit={async (values, formikHelpers) => { formikHelpers.setSubmitting(true); - if (selectedFile) { + if (selectedLogo) { values.use_custom_logo = true; const formData = new FormData(); - formData.append("file", selectedFile); - setSelectedFile(null); + formData.append("file", selectedLogo); + setSelectedLogo(null); const response = await fetch( "/api/admin/enterprise-settings/logo", { @@ -85,6 +92,27 @@ export function WhitelabelingForm() { } } + if (selectedLogotype) { + values.use_custom_logotype = true; + + const formData = new FormData(); + formData.append("file", selectedLogotype); + setSelectedLogo(null); + const response = await fetch( + "/api/admin/enterprise-settings/logo?is_logotype=true", + { + method: "PUT", + body: formData, + } + ); + if (!response.ok) { + const errorMsg = (await response.json()).detail; + alert(`Failed to upload logo. ${errorMsg}`); + formikHelpers.setSubmitting(false); + return; + } + } + formikHelpers.setValues(values); await updateEnterpriseSettings(values); }} @@ -138,10 +166,53 @@ export function WhitelabelingForm() { Specify your own logo to replace the standard Danswer logo. )} - +
+ + + {values.use_custom_logotype ? ( +
+ Current Custom Logotype: + logotype + + + + + Override the current custom logotype by uploading a new image + below and clicking the Update button. This logotype is the + text-based logo that will be rendered at the bottom right of + the chat screen. + +
+ ) : ( + Specify your own logotype + )} + @@ -182,6 +253,17 @@ export function WhitelabelingForm() { />
+
+ +
+ diff --git a/web/src/components/chat_search/Header.tsx b/web/src/components/chat_search/Header.tsx index d1fb68ccf..48a0ba42c 100644 --- a/web/src/components/chat_search/Header.tsx +++ b/web/src/components/chat_search/Header.tsx @@ -30,9 +30,6 @@ export default function FunctionalHeader({ setSharingModalVisible?: (value: SetStateAction) => void; toggleSidebar?: () => void; }) { - const combinedSettings = useContext(SettingsContext); - const enterpriseSettings = combinedSettings?.enterpriseSettings; - useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (event.metaKey || event.ctrlKey) { @@ -69,7 +66,7 @@ export default function FunctionalHeader({ }; return (
-
+
= ({ + content, + className = "", +}) => { + return ( + ( + + ), + p: ({ node, ...props }) => ( +

+ ), + }} + remarkPlugins={[remarkGfm]} + > + {content} + + ); +}; diff --git a/web/src/components/chat_search/hooks.ts b/web/src/components/chat_search/hooks.ts index 2e42978a3..f07234e54 100644 --- a/web/src/components/chat_search/hooks.ts +++ b/web/src/components/chat_search/hooks.ts @@ -52,7 +52,12 @@ export const useSidebarVisibility = ({ !isWithinSidebar && !toggledSidebar ) { - setShowDocSidebar(false); + setTimeout(() => { + setShowDocSidebar((showDocSidebar) => { + // Account for possition as point in time of + return !(xPosition.current > sidebarRect.right); + }); + }, 200); } else if (currentXPosition < 100 && !showDocSidebar) { if (!mobile) { setShowDocSidebar(true); diff --git a/web/src/components/header/LogoType.tsx b/web/src/components/header/LogoType.tsx index 5942b132c..b1ea82c45 100644 --- a/web/src/components/header/LogoType.tsx +++ b/web/src/components/header/LogoType.tsx @@ -6,7 +6,7 @@ import { NEXT_PUBLIC_DO_NOT_USE_TOGGLE_OFF_DANSWER_POWERED, NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA, } from "@/lib/constants"; -import { LefToLineIcon, NewChatIcon, RightToLineIcon } from "../icons/icons"; +import { LeftToLineIcon, NewChatIcon, RightToLineIcon } from "../icons/icons"; import { Tooltip } from "../tooltip/Tooltip"; import { pageType } from "@/app/chat/sessionSidebar/types"; import { Logo } from "../Logo"; @@ -50,7 +50,7 @@ export default function LogoType({

)}
{enterpriseSettings && enterpriseSettings.application_name ? ( @@ -100,7 +100,7 @@ export default function LogoType({ {!toggled && !combinedSettings?.isMobile ? ( ) : ( - + )} diff --git a/web/src/components/icons/icons.tsx b/web/src/components/icons/icons.tsx index 709ca9467..70fa5bb6a 100644 --- a/web/src/components/icons/icons.tsx +++ b/web/src/components/icons/icons.tsx @@ -71,6 +71,7 @@ import slackIcon from "../../../public/Slack.png"; import s3Icon from "../../../public/S3.png"; import r2Icon from "../../../public/r2.png"; import salesforceIcon from "../../../public/Salesforce.png"; + import sharepointIcon from "../../../public/Sharepoint.png"; import teamsIcon from "../../../public/Teams.png"; import mediawikiIcon from "../../../public/MediaWiki.svg"; @@ -82,8 +83,7 @@ import cohereIcon from "../../../public/Cohere.svg"; import voyageIcon from "../../../public/Voyage.png"; import googleIcon from "../../../public/Google.webp"; -import { FaRobot, FaSlack } from "react-icons/fa"; -import { IconType } from "react-icons"; +import { FaRobot } from "react-icons/fa"; export interface IconProps { size?: number; @@ -291,7 +291,7 @@ export const AnthropicIcon = ({ ); }; -export const LefToLineIcon = ({ +export const LeftToLineIcon = ({ size = 16, className = defaultTailwindCSS, }: IconProps) => { @@ -2489,3 +2489,95 @@ export const ClosedBookIcon = ({ ); }; + +export const PinIcon = ({ + size = 16, + className = defaultTailwindCSS, +}: IconProps) => { + return ( + + + + ); +}; + +export const TwoRightArrowIcons = ({ + size = 16, + className = defaultTailwindCSS, +}: IconProps) => { + return ( + + + + ); +}; + +export const PlusIcon = ({ + size = 16, + className = defaultTailwindCSS, +}: IconProps) => { + return ( + + + + ); +}; + +export const MinusIcon = ({ + size = 16, + className = defaultTailwindCSS, +}: IconProps) => { + return ( + + + + ); +}; diff --git a/web/src/components/search/SearchSection.tsx b/web/src/components/search/SearchSection.tsx index b41dd4237..eedc74a16 100644 --- a/web/src/components/search/SearchSection.tsx +++ b/web/src/components/search/SearchSection.tsx @@ -644,7 +644,7 @@ export const SearchSection = ({ {
{!settings?.isMobile && diff --git a/web/src/components/search/filtering/Filters.tsx b/web/src/components/search/filtering/Filters.tsx index 1d2f4a2da..feb2b3452 100644 --- a/web/src/components/search/filtering/Filters.tsx +++ b/web/src/components/search/filtering/Filters.tsx @@ -1,7 +1,14 @@ import React, { useState } from "react"; import { DocumentSet, Tag, ValidSources } from "@/lib/types"; import { SourceMetadata } from "@/lib/search/interfaces"; -import { InfoIcon, defaultTailwindCSS } from "../../icons/icons"; +import { + GearIcon, + InfoIcon, + MinusIcon, + PlusCircleIcon, + PlusIcon, + defaultTailwindCSS, +} from "../../icons/icons"; import { HoverPopup } from "../../HoverPopup"; import { FiBook, @@ -75,6 +82,19 @@ export function SourceSelector({ }); }; + let allSourcesSelected = selectedSources.length > 0; + + const toggleAllSources = () => { + if (allSourcesSelected) { + setSelectedSources([]); + } else { + const allSources = listSourceMetadata().filter((source) => + existingSources.includes(source.internalName) + ); + setSelectedSources(allSources); + } + }; + return (
0 && (
- Sources +
+
+

Sources

+ +
+
{listSourceMetadata() .filter((source) => existingSources.includes(source.internalName)) diff --git a/web/src/components/tooltip/CustomTooltip.tsx b/web/src/components/tooltip/CustomTooltip.tsx index 9bedf6a19..c8a18795b 100644 --- a/web/src/components/tooltip/CustomTooltip.tsx +++ b/web/src/components/tooltip/CustomTooltip.tsx @@ -6,6 +6,7 @@ import React, { createContext, useContext, } from "react"; +import { createPortal } from "react-dom"; // Create a context for the tooltip group const TooltipGroupContext = createContext<{ @@ -52,13 +53,14 @@ export const CustomTooltip = ({ light?: boolean; showTick?: boolean; delay?: number; - wrap?: boolean; citation?: boolean; position?: "top" | "bottom"; }) => { const [isVisible, setIsVisible] = useState(false); + const [tooltipPosition, setTooltipPosition] = useState({ top: 0, left: 0 }); const timeoutRef = useRef(null); + const triggerRef = useRef(null); const { groupHovered, setGroupHovered, hoverCountRef } = useContext(TooltipGroupContext); @@ -69,6 +71,7 @@ export const CustomTooltip = ({ timeoutRef.current = setTimeout(() => { setIsVisible(true); setGroupHovered(true); + updateTooltipPosition(); }, showDelay); }; @@ -85,6 +88,16 @@ export const CustomTooltip = ({ }, 100); }; + const updateTooltipPosition = () => { + if (triggerRef.current) { + const rect = triggerRef.current.getBoundingClientRect(); + setTooltipPosition({ + top: position === "top" ? rect.top - 10 : rect.bottom + 10, + left: rect.left + rect.width / 2, + }); + } + }; + useEffect(() => { return () => { if (timeoutRef.current) { @@ -94,47 +107,60 @@ export const CustomTooltip = ({ }, []); return ( - + <> {children} - {isVisible && ( -
- {showTick && ( -
- )} + {isVisible && + createPortal(
- {content} -
-
- )} - + {showTick && ( +
+ )} +
+ {content} +
+
, + document.body + )} + ); }; diff --git a/web/tailwind-themes/tailwind.config.js b/web/tailwind-themes/tailwind.config.js index e43d22043..90be59590 100644 --- a/web/tailwind-themes/tailwind.config.js +++ b/web/tailwind-themes/tailwind.config.js @@ -90,6 +90,9 @@ module.exports = { "background-200": "#e5e5e5", // neutral-200 "background-300": "#d4d4d4", // neutral-300 "background-400": "#a3a3a3", // neutral-400 + "background-500": "#737373", // neutral-400 + "background-600": "#525252", // neutral-400 + "background-700": "#404040", // neutral-400 "background-800": "#262626", // neutral-800 "background-900": "#111827", // gray-900 "background-inverted": "#000000", // black