From b701660f880634ff9babb2f4bf49a19666647293 Mon Sep 17 00:00:00 2001 From: Timothy Carambat <rambat1010@gmail.com> Date: Wed, 13 Nov 2024 11:11:13 -0800 Subject: [PATCH] Frontend performance improvements (#2627) * Frontend performance improvements * test docker build --- .github/workflows/dev-build.yaml | 2 +- frontend/src/components/Footer/index.jsx | 35 +-- .../Documents/Directory/FileRow/index.jsx | 26 +- .../Documents/Directory/index.jsx | 279 ++++++++++-------- .../WorkspaceFileRow/index.jsx | 50 +--- .../Documents/WorkspaceDirectory/index.jsx | 55 ++++ .../src/components/SettingsButton/index.jsx | 13 +- .../ChatHistory/Citation/index.jsx | 26 +- .../Actions/ActionMenu/index.jsx | 7 - .../Actions/EditMessage/index.jsx | 7 - .../Actions/TTSButton/asyncTts.jsx | 7 - .../Actions/TTSButton/native.jsx | 7 - .../Actions/TTSButton/piperTTS.jsx | 7 - .../HistoricalMessage/Actions/index.jsx | 24 +- .../ChatContainer/ChatTooltips/index.jsx | 66 +++++ .../WorkspaceChat/ChatContainer/index.jsx | 2 + .../src/components/WorkspaceChat/index.jsx | 4 +- .../src/pages/Admin/Agents/Badges/default.jsx | 10 +- frontend/src/pages/Admin/Agents/index.jsx | 76 +++-- .../BrowserExtensionApiKeyRow/index.jsx | 17 +- .../BrowserExtensionApiKey/index.jsx | 13 + 21 files changed, 391 insertions(+), 342 deletions(-) create mode 100644 frontend/src/components/WorkspaceChat/ChatContainer/ChatTooltips/index.jsx diff --git a/.github/workflows/dev-build.yaml b/.github/workflows/dev-build.yaml index 86e10bbcd..e7acd4e53 100644 --- a/.github/workflows/dev-build.yaml +++ b/.github/workflows/dev-build.yaml @@ -6,7 +6,7 @@ concurrency: on: push: - branches: ['agent-skill-plugins'] # put your current branch to create a build. Core team only. + branches: ['2602-page-load-speed'] # put your current branch to create a build. Core team only. paths-ignore: - '**.md' - 'cloud-deployments/*' diff --git a/frontend/src/components/Footer/index.jsx b/frontend/src/components/Footer/index.jsx index 2dfccc3f9..095aeba91 100644 --- a/frontend/src/components/Footer/index.jsx +++ b/frontend/src/components/Footer/index.jsx @@ -15,7 +15,6 @@ import React, { useEffect, useState } from "react"; import SettingsButton from "../SettingsButton"; import { isMobile } from "react-device-detect"; import { Tooltip } from "react-tooltip"; -import { v4 } from "uuid"; export const MAX_ICONS = 3; export const ICON_COMPONENTS = { @@ -49,40 +48,40 @@ export default function Footer() { return ( <div className="flex justify-center mb-2"> <div className="flex space-x-4"> - <ToolTipWrapper id="open-github"> + <div className="flex w-fit"> <a href={paths.github()} target="_blank" rel="noreferrer" className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border" aria-label="Find us on Github" - data-tooltip-id="open-github" + data-tooltip-id="footer-item" data-tooltip-content="View source code on Github" > <GithubLogo weight="fill" className="h-5 w-5 " /> </a> - </ToolTipWrapper> - <ToolTipWrapper id="open-documentation"> + </div> + <div className="flex w-fit"> <a href={paths.docs()} target="_blank" rel="noreferrer" className="w-fit transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border" aria-label="Docs" - data-tooltip-id="open-documentation" + data-tooltip-id="footer-item" data-tooltip-content="Open AnythingLLM help docs" > <BookOpen weight="fill" className="h-5 w-5 " /> </a> - </ToolTipWrapper> - <ToolTipWrapper id="open-discord"> + </div> + <div className="flex w-fit"> <a href={paths.discord()} target="_blank" rel="noreferrer" className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border" aria-label="Join our Discord server" - data-tooltip-id="open-discord" + data-tooltip-id="footer-item" data-tooltip-content="Join the AnythingLLM Discord" > <DiscordLogo @@ -90,9 +89,15 @@ export default function Footer() { className="h-5 w-5 stroke-slate-200 group-hover:stroke-slate-200" /> </a> - </ToolTipWrapper> + </div> {!isMobile && <SettingsButton />} </div> + <Tooltip + id="footer-item" + place="top" + delayShow={300} + className="tooltip !text-xs z-99" + /> </div> ); } @@ -119,16 +124,8 @@ export default function Footer() { ))} {!isMobile && <SettingsButton />} </div> - </div> - ); -} - -export function ToolTipWrapper({ id = v4(), children }) { - return ( - <div className="flex w-fit"> - {children} <Tooltip - id={id} + id="footer-item" place="top" delayShow={300} className="tooltip !text-xs z-99" diff --git a/frontend/src/components/Modals/ManageWorkspace/Documents/Directory/FileRow/index.jsx b/frontend/src/components/Modals/ManageWorkspace/Documents/Directory/FileRow/index.jsx index fc3546c17..663c73d56 100644 --- a/frontend/src/components/Modals/ManageWorkspace/Documents/Directory/FileRow/index.jsx +++ b/frontend/src/components/Modals/ManageWorkspace/Documents/Directory/FileRow/index.jsx @@ -5,7 +5,6 @@ import { middleTruncate, } from "@/utils/directories"; import { File } from "@phosphor-icons/react"; -import { Tooltip } from "react-tooltip"; export default function FileRow({ item, selected, toggleSelection }) { return ( @@ -16,8 +15,13 @@ export default function FileRow({ item, selected, toggleSelection }) { }`} > <div - data-tooltip-id={`directory-item-${item.url}`} + data-tooltip-id={`directory-item`} className="col-span-10 w-fit flex gap-x-[4px] items-center relative" + data-tooltip-content={JSON.stringify({ + title: item.title, + date: formatDate(item?.published), + extension: getFileExtension(item.url).toUpperCase(), + })} > <div className="shrink-0 w-3 h-3 rounded border-[1px] border-white flex justify-center items-center cursor-pointer" @@ -42,24 +46,6 @@ export default function FileRow({ item, selected, toggleSelection }) { </div> )} </div> - <Tooltip - id={`directory-item-${item.url}`} - place="bottom" - delayShow={800} - className="tooltip invert z-99" - > - <div className="text-xs "> - <p className="text-white">{item.title}</p> - <div className="flex mt-1 gap-x-2"> - <p className=""> - Date: <b>{formatDate(item?.published)}</b> - </p> - <p className=""> - Type: <b>{getFileExtension(item.url).toUpperCase()}</b> - </p> - </div> - </div> - </Tooltip> </tr> ); } diff --git a/frontend/src/components/Modals/ManageWorkspace/Documents/Directory/index.jsx b/frontend/src/components/Modals/ManageWorkspace/Documents/Directory/index.jsx index d1c5eba78..d87ae66c8 100644 --- a/frontend/src/components/Modals/ManageWorkspace/Documents/Directory/index.jsx +++ b/frontend/src/components/Modals/ManageWorkspace/Documents/Directory/index.jsx @@ -13,6 +13,8 @@ import NewFolderModal from "./NewFolderModal"; import debounce from "lodash.debounce"; import { filterFileSearchResults } from "./utils"; import ContextMenu from "./ContextMenu"; +import { Tooltip } from "react-tooltip"; +import { safeJsonParse } from "@/utils/request"; function Directory({ files, @@ -188,140 +190,175 @@ function Directory({ }; return ( - <div className="px-8 pb-8" onContextMenu={handleContextMenu}> - <div className="flex flex-col gap-y-6"> - <div className="flex items-center justify-between w-[560px] px-5 relative"> - <h3 className="text-white text-base font-bold">My Documents</h3> - <div className="relative"> - <input - type="search" - placeholder="Search for document" - onChange={handleSearch} - className="search-input bg-zinc-900 text-white placeholder-white/40 text-sm rounded-lg pl-9 pr-2.5 py-2 w-[250px] h-[32px]" - /> - <MagnifyingGlass - size={14} - className="absolute left-3 top-1/2 transform -translate-y-1/2 text-white" - weight="bold" - /> - </div> - <button - className="flex items-center gap-x-2 cursor-pointer px-[14px] py-[7px] -mr-[14px] rounded-lg hover:bg-[#222628]/60 z-20 relative" - onClick={openFolderModal} - > - <Plus size={18} weight="bold" color="#D3D4D4" /> - <div className="text-[#D3D4D4] text-xs font-bold leading-[18px]"> - New Folder + <> + <div className="px-8 pb-8" onContextMenu={handleContextMenu}> + <div className="flex flex-col gap-y-6"> + <div className="flex items-center justify-between w-[560px] px-5 relative"> + <h3 className="text-white text-base font-bold">My Documents</h3> + <div className="relative"> + <input + type="search" + placeholder="Search for document" + onChange={handleSearch} + className="search-input bg-zinc-900 text-white placeholder-white/40 text-sm rounded-lg pl-9 pr-2.5 py-2 w-[250px] h-[32px]" + /> + <MagnifyingGlass + size={14} + className="absolute left-3 top-1/2 transform -translate-y-1/2 text-white" + weight="bold" + /> </div> - </button> - </div> - - <div className="relative w-[560px] h-[310px] bg-zinc-900 rounded-2xl overflow-hidden"> - <div className="absolute top-0 left-0 right-0 z-10 rounded-t-2xl text-white/80 text-xs grid grid-cols-12 py-2 px-8 border-b border-white/20 shadow-lg bg-zinc-900"> - <p className="col-span-6">Name</p> + <button + className="flex items-center gap-x-2 cursor-pointer px-[14px] py-[7px] -mr-[14px] rounded-lg hover:bg-[#222628]/60 z-20 relative" + onClick={openFolderModal} + > + <Plus size={18} weight="bold" color="#D3D4D4" /> + <div className="text-[#D3D4D4] text-xs font-bold leading-[18px]"> + New Folder + </div> + </button> </div> - <div className="overflow-y-auto h-full pt-8"> - {loading ? ( - <div className="w-full h-full flex items-center justify-center flex-col gap-y-5"> - <PreLoader /> - <p className="text-white/80 text-sm font-semibold animate-pulse text-center w-1/3"> - {loadingMessage} - </p> - </div> - ) : filteredFiles.length > 0 ? ( - filteredFiles.map( - (item, index) => - item.type === "folder" && ( - <FolderRow - key={index} - item={item} - selected={isSelected( - item.id, - item.type === "folder" ? item : null + <div className="relative w-[560px] h-[310px] bg-zinc-900 rounded-2xl overflow-hidden"> + <div className="absolute top-0 left-0 right-0 z-10 rounded-t-2xl text-white/80 text-xs grid grid-cols-12 py-2 px-8 border-b border-white/20 shadow-lg bg-zinc-900"> + <p className="col-span-6">Name</p> + </div> + + <div className="overflow-y-auto h-full pt-8"> + {loading ? ( + <div className="w-full h-full flex items-center justify-center flex-col gap-y-5"> + <PreLoader /> + <p className="text-white/80 text-sm font-semibold animate-pulse text-center w-1/3"> + {loadingMessage} + </p> + </div> + ) : filteredFiles.length > 0 ? ( + filteredFiles.map( + (item, index) => + item.type === "folder" && ( + <FolderRow + key={index} + item={item} + selected={isSelected( + item.id, + item.type === "folder" ? item : null + )} + onRowClick={() => toggleSelection(item)} + toggleSelection={toggleSelection} + isSelected={isSelected} + autoExpanded={index === 0} + /> + ) + ) + ) : ( + <div className="w-full h-full flex items-center justify-center"> + <p className="text-white text-opacity-40 text-sm font-medium"> + No Documents + </p> + </div> + )} + </div> + {amountSelected !== 0 && ( + <div className="absolute bottom-[12px] left-0 right-0 flex justify-center pointer-events-none"> + <div className="mx-auto bg-white/40 rounded-lg py-1 px-2 pointer-events-auto"> + <div className="flex flex-row items-center gap-x-2"> + <button + onClick={moveToWorkspace} + onMouseEnter={() => setHighlightWorkspace(true)} + onMouseLeave={() => setHighlightWorkspace(false)} + className="border-none text-sm font-semibold bg-white h-[30px] px-2.5 rounded-lg hover:text-white hover:bg-neutral-800/80" + > + Move to Workspace + </button> + <div className="relative"> + <button + onClick={() => + setShowFolderSelection(!showFolderSelection) + } + className="border-none text-sm font-semibold bg-white h-[32px] w-[32px] rounded-lg text-dark-text hover:bg-neutral-800/80 flex justify-center items-center group" + > + <MoveToFolderIcon className="text-dark-text group-hover:text-white" /> + </button> + {showFolderSelection && ( + <FolderSelectionPopup + folders={files.items.filter( + (item) => item.type === "folder" + )} + onSelect={moveToFolder} + onClose={() => setShowFolderSelection(false)} + /> )} - onRowClick={() => toggleSelection(item)} - toggleSelection={toggleSelection} - isSelected={isSelected} - autoExpanded={index === 0} - /> - ) - ) - ) : ( - <div className="w-full h-full flex items-center justify-center"> - <p className="text-white text-opacity-40 text-sm font-medium"> - No Documents - </p> - </div> - )} - </div> - {amountSelected !== 0 && ( - <div className="absolute bottom-[12px] left-0 right-0 flex justify-center pointer-events-none"> - <div className="mx-auto bg-white/40 rounded-lg py-1 px-2 pointer-events-auto"> - <div className="flex flex-row items-center gap-x-2"> - <button - onClick={moveToWorkspace} - onMouseEnter={() => setHighlightWorkspace(true)} - onMouseLeave={() => setHighlightWorkspace(false)} - className="border-none text-sm font-semibold bg-white h-[30px] px-2.5 rounded-lg hover:text-white hover:bg-neutral-800/80" - > - Move to Workspace - </button> - <div className="relative"> + </div> <button - onClick={() => - setShowFolderSelection(!showFolderSelection) - } - className="border-none text-sm font-semibold bg-white h-[32px] w-[32px] rounded-lg text-dark-text hover:bg-neutral-800/80 flex justify-center items-center group" + onClick={deleteFiles} + className="border-none text-sm font-semibold bg-white h-[32px] w-[32px] rounded-lg text-dark-text hover:text-white hover:bg-neutral-800/80 flex justify-center items-center" > - <MoveToFolderIcon className="text-dark-text group-hover:text-white" /> + <Trash size={18} weight="bold" /> </button> - {showFolderSelection && ( - <FolderSelectionPopup - folders={files.items.filter( - (item) => item.type === "folder" - )} - onSelect={moveToFolder} - onClose={() => setShowFolderSelection(false)} - /> - )} </div> - <button - onClick={deleteFiles} - className="border-none text-sm font-semibold bg-white h-[32px] w-[32px] rounded-lg text-dark-text hover:text-white hover:bg-neutral-800/80 flex justify-center items-center" - > - <Trash size={18} weight="bold" /> - </button> </div> </div> - </div> - )} - </div> + )} + </div> - <UploadFile - workspace={workspace} - fetchKeys={fetchKeys} - setLoading={setLoading} - setLoadingMessage={setLoadingMessage} - /> - </div> - {isFolderModalOpen && ( - <div className="bg-black/60 backdrop-blur-sm fixed top-0 left-0 outline-none w-screen h-screen flex items-center justify-center z-30"> - <NewFolderModal - closeModal={closeFolderModal} - files={files} - setFiles={setFiles} + <UploadFile + workspace={workspace} + fetchKeys={fetchKeys} + setLoading={setLoading} + setLoadingMessage={setLoadingMessage} /> </div> - )} - <ContextMenu - contextMenu={contextMenu} - closeContextMenu={closeContextMenu} - files={files} - selectedItems={selectedItems} - setSelectedItems={setSelectedItems} - /> - </div> + {isFolderModalOpen && ( + <div className="bg-black/60 backdrop-blur-sm fixed top-0 left-0 outline-none w-screen h-screen flex items-center justify-center z-30"> + <NewFolderModal + closeModal={closeFolderModal} + files={files} + setFiles={setFiles} + /> + </div> + )} + <ContextMenu + contextMenu={contextMenu} + closeContextMenu={closeContextMenu} + files={files} + selectedItems={selectedItems} + setSelectedItems={setSelectedItems} + /> + </div> + <DirectoryTooltips /> + </> + ); +} + +/** + * Tooltips for the directory components. Renders when the directory is shown + * or updated so that tooltips are attached as the items are changed. + */ +function DirectoryTooltips() { + return ( + <Tooltip + id="directory-item" + place="bottom" + delayShow={800} + className="tooltip invert z-99" + render={({ content }) => { + const data = safeJsonParse(content, null); + if (!data) return null; + return ( + <div className="text-xs"> + <p className="text-white">{data.title}</p> + <div className="flex mt-1 gap-x-2"> + <p className=""> + Date: <b>{data.date}</b> + </p> + <p className=""> + Type: <b>{data.extension}</b> + </p> + </div> + </div> + ); + }} + /> ); } diff --git a/frontend/src/components/Modals/ManageWorkspace/Documents/WorkspaceDirectory/WorkspaceFileRow/index.jsx b/frontend/src/components/Modals/ManageWorkspace/Documents/WorkspaceDirectory/WorkspaceFileRow/index.jsx index 505c4c22c..496702fc2 100644 --- a/frontend/src/components/Modals/ManageWorkspace/Documents/WorkspaceDirectory/WorkspaceFileRow/index.jsx +++ b/frontend/src/components/Modals/ManageWorkspace/Documents/WorkspaceDirectory/WorkspaceFileRow/index.jsx @@ -8,7 +8,6 @@ import { ArrowUUpLeft, Eye, File, PushPin } from "@phosphor-icons/react"; import Workspace from "@/models/workspace"; import showToast from "@/utils/toast"; import System from "@/models/system"; -import { Tooltip } from "react-tooltip"; export default function WorkspaceFileRow({ item, @@ -64,7 +63,12 @@ export default function WorkspaceFileRow({ > <div className="col-span-10 w-fit flex gap-x-[2px] items-center relative" - data-tooltip-id={`ws-directory-item-${item.url}`} + data-tooltip-id="ws-directory-item" + data-tooltip-content={JSON.stringify({ + title: item.title, + date: formatDate(item?.published), + extension: getFileExtension(item.url).toUpperCase(), + })} > <div className="shrink-0 w-3 h-3"> {!disableSelection ? ( @@ -106,24 +110,6 @@ export default function WorkspaceFileRow({ </div> )} </div> - <Tooltip - id={`ws-directory-item-${item.url}`} - place="bottom" - delayShow={800} - className="tooltip invert z-99" - > - <div className="text-xs "> - <p className="text-white">{item.title}</p> - <div className="flex mt-1 gap-x-2"> - <p className=""> - Date: <b>{formatDate(item?.published)}</b> - </p> - <p className=""> - Type: <b>{getFileExtension(item.url).toUpperCase()}</b> - </p> - </div> - </div> - </Tooltip> </div> ); } @@ -175,7 +161,7 @@ const PinItemToWorkspace = memo(({ workspace, docPath, item }) => { className="flex gap-x-2 items-center hover:bg-main-gradient p-[2px] rounded ml-2" > <PushPin - data-tooltip-id={`pin-${item.id}`} + data-tooltip-id="pin-document" data-tooltip-content={ pinned ? "Un-Pin from workspace" : "Pin to workspace" } @@ -184,12 +170,6 @@ const PinItemToWorkspace = memo(({ workspace, docPath, item }) => { weight={hover || pinned ? "fill" : "regular"} className="outline-none text-base font-bold flex-shrink-0 cursor-pointer" /> - <Tooltip - id={`pin-${item.id}`} - place="bottom" - delayShow={300} - className="tooltip invert !text-xs" - /> </div> ); }); @@ -247,7 +227,7 @@ const WatchForChanges = memo(({ workspace, docPath, item }) => { className="flex gap-x-2 items-center hover:bg-main-gradient p-[2px] rounded ml-2" > <Eye - data-tooltip-id={`watch-changes-${item.id}`} + data-tooltip-id="watch-changes" data-tooltip-content={ watched ? "Stop watching for changes" : "Watch document for changes" } @@ -256,12 +236,6 @@ const WatchForChanges = memo(({ workspace, docPath, item }) => { weight={hover || watched ? "fill" : "regular"} className="outline-none text-base font-bold flex-shrink-0 cursor-pointer" /> - <Tooltip - id={`watch-changes-${item.id}`} - place="bottom" - delayShow={300} - className="tooltip invert !text-xs" - /> </div> ); }); @@ -270,17 +244,11 @@ const RemoveItemFromWorkspace = ({ item, onClick }) => { return ( <div> <ArrowUUpLeft - data-tooltip-id={`remove-${item.id}`} + data-tooltip-id="remove-document" data-tooltip-content="Remove document from workspace" onClick={onClick} className="text-base font-bold w-4 h-4 ml-2 flex-shrink-0 cursor-pointer" /> - <Tooltip - id={`remove-${item.id}`} - place="bottom" - delayShow={300} - className="tooltip invert !text-xs" - /> </div> ); }; diff --git a/frontend/src/components/Modals/ManageWorkspace/Documents/WorkspaceDirectory/index.jsx b/frontend/src/components/Modals/ManageWorkspace/Documents/WorkspaceDirectory/index.jsx index e9864f12c..b54f29cc1 100644 --- a/frontend/src/components/Modals/ManageWorkspace/Documents/WorkspaceDirectory/index.jsx +++ b/frontend/src/components/Modals/ManageWorkspace/Documents/WorkspaceDirectory/index.jsx @@ -8,6 +8,8 @@ import { SEEN_DOC_PIN_ALERT, SEEN_WATCH_ALERT } from "@/utils/constants"; import paths from "@/utils/paths"; import { Link } from "react-router-dom"; import Workspace from "@/models/workspace"; +import { Tooltip } from "react-tooltip"; +import { safeJsonParse } from "@/utils/request"; function WorkspaceDirectory({ workspace, @@ -241,6 +243,7 @@ function WorkspaceDirectory({ </div> <PinAlert /> <DocumentWatchAlert /> + <WorkspaceDocumentTooltips /> </> ); } @@ -396,4 +399,56 @@ function RenderFileRows({ files, movedItems, children }) { }); } +/** + * Tooltips for the workspace directory components. Renders when the workspace directory is shown + * or updated so that tooltips are attached as the items are changed. + */ +function WorkspaceDocumentTooltips() { + return ( + <> + <Tooltip + id="ws-directory-item" + place="bottom" + delayShow={800} + className="tooltip invert z-99" + render={({ content }) => { + const data = safeJsonParse(content, null); + if (!data) return null; + return ( + <div className="text-xs"> + <p className="text-white">{data.title}</p> + <div className="flex mt-1 gap-x-2"> + <p className=""> + Date: <b>{data.date}</b> + </p> + <p className=""> + Type: <b>{data.extension}</b> + </p> + </div> + </div> + ); + }} + /> + <Tooltip + id="watch-changes" + place="bottom" + delayShow={300} + className="tooltip invert !text-xs" + /> + <Tooltip + id="pin-document" + place="bottom" + delayShow={300} + className="tooltip invert !text-xs" + /> + <Tooltip + id="remove-document" + place="bottom" + delayShow={300} + className="tooltip invert !text-xs" + /> + </> + ); +} + export default memo(WorkspaceDirectory); diff --git a/frontend/src/components/SettingsButton/index.jsx b/frontend/src/components/SettingsButton/index.jsx index f53e675f1..6ce4fad15 100644 --- a/frontend/src/components/SettingsButton/index.jsx +++ b/frontend/src/components/SettingsButton/index.jsx @@ -3,7 +3,6 @@ import paths from "@/utils/paths"; import { ArrowUUpLeft, Wrench } from "@phosphor-icons/react"; import { Link } from "react-router-dom"; import { useMatch } from "react-router-dom"; -import { ToolTipWrapper } from "../Footer"; export default function SettingsButton() { const isInSettings = !!useMatch("/settings/*"); @@ -13,30 +12,30 @@ export default function SettingsButton() { if (isInSettings) return ( - <ToolTipWrapper id="go-home"> + <div className="flex w-fit"> <Link to={paths.home()} className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border" aria-label="Home" - data-tooltip-id="go-home" + data-tooltip-id="footer-item" data-tooltip-content="Back to workspaces" > <ArrowUUpLeft className="h-5 w-5" weight="fill" /> </Link> - </ToolTipWrapper> + </div> ); return ( - <ToolTipWrapper id="open-settings"> + <div className="flex w-fit"> <Link to={paths.settings.appearance()} className="transition-all duration-300 p-2 rounded-full text-white bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border" aria-label="Settings" - data-tooltip-id="open-settings" + data-tooltip-id="footer-item" data-tooltip-content="Open settings" > <Wrench className="h-5 w-5" weight="fill" /> </Link> - </ToolTipWrapper> + </div> ); } diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx index 1f726b781..6f81b17c5 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx @@ -15,7 +15,6 @@ import { YoutubeLogo, } from "@phosphor-icons/react"; import ConfluenceLogo from "@/media/dataConnectors/confluence.png"; -import { Tooltip } from "react-tooltip"; import { toPercentString } from "@/utils/numbers"; function combineLikeSources(sources) { @@ -176,23 +175,16 @@ function CitationDetailModal({ source, onClose }) { </p> {!!score && ( - <> - <div className="w-full flex items-center text-xs text-white/60 gap-x-2 cursor-default"> - <div - data-tooltip-id="similarity-score" - data-tooltip-content={`This is the semantic similarity score of this chunk of text compared to your query calculated by the vector database.`} - className="flex items-center gap-x-1" - > - <Info size={14} /> - <p>{toPercentString(score)} match</p> - </div> + <div className="w-full flex items-center text-xs text-white/60 gap-x-2 cursor-default"> + <div + data-tooltip-id="similarity-score" + data-tooltip-content={`This is the semantic similarity score of this chunk of text compared to your query calculated by the vector database.`} + className="flex items-center gap-x-1" + > + <Info size={14} /> + <p>{toPercentString(score)} match</p> </div> - <Tooltip - id="similarity-score" - place="top" - delayShow={100} - /> - </> + </div> )} </div> {[...Array(3)].map((_, idx) => ( diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/ActionMenu/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/ActionMenu/index.jsx index de7df30f8..acd0c4fa5 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/ActionMenu/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/ActionMenu/index.jsx @@ -1,6 +1,5 @@ import React, { useState, useEffect, useRef } from "react"; import { Trash, DotsThreeVertical, TreeView } from "@phosphor-icons/react"; -import { Tooltip } from "react-tooltip"; function ActionMenu({ chatId, forkThread, isEditing, role }) { const [open, setOpen] = useState(false); @@ -37,12 +36,6 @@ function ActionMenu({ chatId, forkThread, isEditing, role }) { return ( <div className="mt-2 -ml-0.5 relative" ref={menuRef}> - <Tooltip - id="action-menu" - place="top" - delayShow={300} - className="tooltip !text-xs" - /> <button onClick={toggleMenu} className="border-none text-zinc-300 hover:text-zinc-100 transition-colors duration-200" diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/EditMessage/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/EditMessage/index.jsx index 811de87d6..c99dd5b92 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/EditMessage/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/EditMessage/index.jsx @@ -1,7 +1,6 @@ import { AI_BACKGROUND_COLOR, USER_BACKGROUND_COLOR } from "@/utils/constants"; import { Pencil } from "@phosphor-icons/react"; import { useState, useEffect, useRef } from "react"; -import { Tooltip } from "react-tooltip"; const EDIT_EVENT = "toggle-message-edit"; @@ -55,12 +54,6 @@ export function EditMessageAction({ chatId = null, role, isEditing }) { > <Pencil size={21} className="mb-1" /> </button> - <Tooltip - id="edit-input-text" - place="bottom" - delayShow={300} - className="tooltip !text-xs" - /> </div> ); } diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/TTSButton/asyncTts.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/TTSButton/asyncTts.jsx index 1947f0057..3fa99174f 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/TTSButton/asyncTts.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/TTSButton/asyncTts.jsx @@ -1,6 +1,5 @@ import { useEffect, useState, useRef } from "react"; import { SpeakerHigh, PauseCircle, CircleNotch } from "@phosphor-icons/react"; -import { Tooltip } from "react-tooltip"; import Workspace from "@/models/workspace"; import showToast from "@/utils/toast"; @@ -83,12 +82,6 @@ export default function AsyncTTSMessage({ slug, chatId }) { controls={false} /> </button> - <Tooltip - id="message-to-speech" - place="bottom" - delayShow={300} - className="tooltip !text-xs" - /> </div> ); } diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/TTSButton/native.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/TTSButton/native.jsx index 5f3bd3f69..daa9ebda9 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/TTSButton/native.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/TTSButton/native.jsx @@ -1,6 +1,5 @@ import React, { useEffect, useState } from "react"; import { SpeakerHigh, PauseCircle } from "@phosphor-icons/react"; -import { Tooltip } from "react-tooltip"; export default function NativeTTSMessage({ message }) { const [speaking, setSpeaking] = useState(false); @@ -50,12 +49,6 @@ export default function NativeTTSMessage({ message }) { <SpeakerHigh size={18} className="mb-1" /> )} </button> - <Tooltip - id="message-to-speech" - place="bottom" - delayShow={300} - className="tooltip !text-xs" - /> </div> ); } diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/TTSButton/piperTTS.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/TTSButton/piperTTS.jsx index f8431a3bd..1ec39874c 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/TTSButton/piperTTS.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/TTSButton/piperTTS.jsx @@ -1,6 +1,5 @@ import { useEffect, useState, useRef } from "react"; import { SpeakerHigh, PauseCircle, CircleNotch } from "@phosphor-icons/react"; -import { Tooltip } from "react-tooltip"; import PiperTTSClient from "@/utils/piperTTS"; export default function PiperTTS({ voiceId = null, message }) { @@ -80,12 +79,6 @@ export default function PiperTTS({ voiceId = null, message }) { controls={false} /> </button> - <Tooltip - id="message-to-speech" - place="bottom" - delayShow={300} - className="tooltip !text-xs" - /> </div> ); } diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/index.jsx index e383b4a03..e396fd11b 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/index.jsx @@ -1,7 +1,6 @@ import React, { memo, useState } from "react"; import useCopyText from "@/hooks/useCopyText"; import { Check, ThumbsUp, ArrowsClockwise, Copy } from "@phosphor-icons/react"; -import { Tooltip } from "react-tooltip"; import Workspace from "@/models/workspace"; import { EditMessageAction } from "./EditMessage"; import ActionMenu from "./ActionMenu"; @@ -46,7 +45,7 @@ const Actions = ({ <FeedbackButton isSelected={selectedFeedback === true} handleFeedback={() => handleFeedback(true)} - tooltipId={`${chatId}-thumbs-up`} + tooltipId="feedback-button" tooltipContent="Good response" IconComponent={ThumbsUp} /> @@ -66,7 +65,6 @@ const Actions = ({ function FeedbackButton({ isSelected, handleFeedback, - tooltipId, tooltipContent, IconComponent, }) { @@ -74,7 +72,7 @@ function FeedbackButton({ <div className="mt-3 relative"> <button onClick={handleFeedback} - data-tooltip-id={tooltipId} + data-tooltip-id="feedback-button" data-tooltip-content={tooltipContent} className="text-zinc-300" aria-label={tooltipContent} @@ -85,12 +83,6 @@ function FeedbackButton({ weight={isSelected ? "fill" : "regular"} /> </button> - <Tooltip - id={tooltipId} - place="bottom" - delayShow={300} - className="tooltip !text-xs" - /> </div> ); } @@ -114,12 +106,6 @@ function CopyMessage({ message }) { <Copy size={20} className="mb-1" /> )} </button> - <Tooltip - id="copy-assistant-text" - place="bottom" - delayShow={300} - className="tooltip !text-xs" - /> </div> </> ); @@ -138,12 +124,6 @@ function RegenerateMessage({ regenerateMessage, chatId }) { > <ArrowsClockwise size={20} className="mb-1" weight="fill" /> </button> - <Tooltip - id="regenerate-assistant-text" - place="bottom" - delayShow={300} - className="tooltip !text-xs" - /> </div> ); } diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatTooltips/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatTooltips/index.jsx new file mode 100644 index 000000000..99571ba33 --- /dev/null +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatTooltips/index.jsx @@ -0,0 +1,66 @@ +import { Tooltip } from "react-tooltip"; + +/** + * Set the tooltips for the chat container in bulk. + * Why do this? + * + * React-tooltip rendering on _each_ chat will attach an event listener to the body. + * This will add up if we have many chats open resulting in the browser crashing + * so we batch them together in a single component that renders at the top most level with + * a static id the content can change, but this prevents the React-tooltip library from adding + * hundreds of event listeners to the DOM. + * + * In general, anywhere we have iterative rendering the Tooltip should be rendered at the highest level to prevent + * hundreds of event listeners from being added to the DOM in the worst case scenario. + * @returns + */ +export function ChatTooltips() { + return ( + <> + <Tooltip + id="message-to-speech" + place="bottom" + delayShow={300} + className="tooltip !text-xs" + /> + <Tooltip + id="regenerate-assistant-text" + place="bottom" + delayShow={300} + className="tooltip !text-xs" + /> + <Tooltip + id="copy-assistant-text" + place="bottom" + delayShow={300} + className="tooltip !text-xs" + /> + <Tooltip + id="feedback-button" + place="bottom" + delayShow={300} + className="tooltip !text-xs" + /> + <Tooltip + id="action-menu" + place="top" + delayShow={300} + className="tooltip !text-xs" + /> + <Tooltip + id="edit-input-text" + place="bottom" + delayShow={300} + className="tooltip !text-xs" + /> + <Tooltip + id="similarity-score" + place="top" + delayShow={100} + // z-[99] to ensure it renders above the chat history + // as the citation modal is z-indexed above the chat history + className="tooltip !text-xs z-[99]" + /> + </> + ); +} diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx index 4f09c0b44..6b2a51fcd 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx @@ -17,6 +17,7 @@ import DnDFileUploaderWrapper from "./DnDWrapper"; import SpeechRecognition, { useSpeechRecognition, } from "react-speech-recognition"; +import { ChatTooltips } from "./ChatTooltips"; export default function ChatContainer({ workspace, knownHistory = [] }) { const { threadSlug = null } = useParams(); @@ -284,6 +285,7 @@ export default function ChatContainer({ workspace, knownHistory = [] }) { attachments={files} /> </DnDFileUploaderWrapper> + <ChatTooltips /> </div> ); } diff --git a/frontend/src/components/WorkspaceChat/index.jsx b/frontend/src/components/WorkspaceChat/index.jsx index c37ecb0c8..61dc43df5 100644 --- a/frontend/src/components/WorkspaceChat/index.jsx +++ b/frontend/src/components/WorkspaceChat/index.jsx @@ -5,9 +5,7 @@ import ChatContainer from "./ChatContainer"; import paths from "@/utils/paths"; import ModalWrapper from "../ModalWrapper"; import { useParams } from "react-router-dom"; -import DnDFileUploaderWrapper, { - DnDFileUploaderProvider, -} from "./ChatContainer/DnDWrapper"; +import { DnDFileUploaderProvider } from "./ChatContainer/DnDWrapper"; export default function WorkspaceChat({ loading, workspace }) { const { threadSlug = null } = useParams(); diff --git a/frontend/src/pages/Admin/Agents/Badges/default.jsx b/frontend/src/pages/Admin/Agents/Badges/default.jsx index 238155508..13a37abad 100644 --- a/frontend/src/pages/Admin/Agents/Badges/default.jsx +++ b/frontend/src/pages/Admin/Agents/Badges/default.jsx @@ -1,11 +1,9 @@ -import { Tooltip } from "react-tooltip"; - export function DefaultBadge({ title }) { return ( <> <span className="w-fit" - data-tooltip-id={`default-skill-${title}`} + data-tooltip-id="default-skill" data-tooltip-content="This skill is enabled by default and cannot be turned off." > <div className="flex items-center gap-x-1 w-fit rounded-full bg-[#F4FFD0]/10 px-2.5 py-0.5 text-sm font-medium text-sky-400 shadow-sm cursor-pointer"> @@ -14,12 +12,6 @@ export function DefaultBadge({ title }) { </div> </div> </span> - <Tooltip - id={`default-skill-${title}`} - place="bottom" - delayShow={300} - className="tooltip !text-xs" - /> </> ); } diff --git a/frontend/src/pages/Admin/Agents/index.jsx b/frontend/src/pages/Admin/Agents/index.jsx index 99c093d5c..8f758647b 100644 --- a/frontend/src/pages/Admin/Agents/index.jsx +++ b/frontend/src/pages/Admin/Agents/index.jsx @@ -12,6 +12,7 @@ import { defaultSkills, configurableSkills } from "./skills"; import { DefaultBadge } from "./Badges/default"; import ImportedSkillList from "./Imported/SkillList"; import ImportedSkillConfig from "./Imported/ImportedSkillConfig"; +import { Tooltip } from "react-tooltip"; export default function AdminAgents() { const [hasChanges, setHasChanges] = useState(false); @@ -361,38 +362,49 @@ function SkillList({ if (skills.length === 0) return null; return ( - <div - className={`bg-white/5 text-white rounded-xl ${ - isMobile ? "w-full" : "min-w-[360px] w-fit" - }`} - > - {Object.entries(skills).map(([skill, settings], index) => ( - <div - key={skill} - className={`py-3 px-4 flex items-center justify-between ${ - index === 0 ? "rounded-t-xl" : "" - } ${ - index === Object.keys(skills).length - 1 - ? "rounded-b-xl" - : "border-b border-white/10" - } cursor-pointer transition-all duration-300 hover:bg-white/5 ${ - selectedSkill === skill ? "bg-white/10" : "" - }`} - onClick={() => handleClick?.(skill)} - > - <div className="text-sm font-light">{settings.title}</div> - <div className="flex items-center gap-x-2"> - {isDefault ? ( - <DefaultBadge title={skill} /> - ) : ( - <div className="text-sm text-white/60 font-medium"> - {activeSkills.includes(skill) ? "On" : "Off"} - </div> - )} - <CaretRight size={14} weight="bold" className="text-white/80" /> + <> + <div + className={`bg-white/5 text-white rounded-xl ${ + isMobile ? "w-full" : "min-w-[360px] w-fit" + }`} + > + {Object.entries(skills).map(([skill, settings], index) => ( + <div + key={skill} + className={`py-3 px-4 flex items-center justify-between ${ + index === 0 ? "rounded-t-xl" : "" + } ${ + index === Object.keys(skills).length - 1 + ? "rounded-b-xl" + : "border-b border-white/10" + } cursor-pointer transition-all duration-300 hover:bg-white/5 ${ + selectedSkill === skill ? "bg-white/10" : "" + }`} + onClick={() => handleClick?.(skill)} + > + <div className="text-sm font-light">{settings.title}</div> + <div className="flex items-center gap-x-2"> + {isDefault ? ( + <DefaultBadge title={skill} /> + ) : ( + <div className="text-sm text-white/60 font-medium"> + {activeSkills.includes(skill) ? "On" : "Off"} + </div> + )} + <CaretRight size={14} weight="bold" className="text-white/80" /> + </div> </div> - </div> - ))} - </div> + ))} + </div> + {/* Tooltip for default skills - only render when skill list is passed isDefault */} + {isDefault && ( + <Tooltip + id="default-skill" + place="bottom" + delayShow={300} + className="tooltip !text-xs" + /> + )} + </> ); } diff --git a/frontend/src/pages/GeneralSettings/BrowserExtensionApiKey/BrowserExtensionApiKeyRow/index.jsx b/frontend/src/pages/GeneralSettings/BrowserExtensionApiKey/BrowserExtensionApiKeyRow/index.jsx index e43996a41..8017aa4b2 100644 --- a/frontend/src/pages/GeneralSettings/BrowserExtensionApiKey/BrowserExtensionApiKeyRow/index.jsx +++ b/frontend/src/pages/GeneralSettings/BrowserExtensionApiKey/BrowserExtensionApiKeyRow/index.jsx @@ -3,7 +3,6 @@ import BrowserExtensionApiKey from "@/models/browserExtensionApiKey"; import showToast from "@/utils/toast"; import { Trash, Copy, Check, Plug } from "@phosphor-icons/react"; import { POPUP_BROWSER_EXTENSION_EVENT } from "@/utils/constants"; -import { Tooltip } from "react-tooltip"; export default function BrowserExtensionApiKeyRow({ apiKey, @@ -66,7 +65,7 @@ export default function BrowserExtensionApiKeyRow({ <div className="flex items-center space-x-2"> <button onClick={handleCopy} - data-tooltip-id={`copy-connection-text-${apiKey.id}`} + data-tooltip-id="copy-connection-text" data-tooltip-content="Copy connection string" className="text-white hover:text-white/80 transition-colors duration-200 p-1 rounded" > @@ -75,27 +74,15 @@ export default function BrowserExtensionApiKeyRow({ ) : ( <Copy className="h-5 w-5" /> )} - <Tooltip - id={`copy-connection-text-${apiKey.id}`} - place="bottom" - delayShow={300} - className="allm-tooltip !allm-text-xs" - /> </button> <button onClick={handleConnect} - data-tooltip-id={`auto-connection-${apiKey.id}`} + data-tooltip-id="auto-connection" data-tooltip-content="Automatically connect to extension" className="text-white hover:text-white/80 transition-colors duration-200 p-1 rounded" > <Plug className="h-5 w-5" /> - <Tooltip - id={`auto-connection-${apiKey.id}`} - place="bottom" - delayShow={300} - className="allm-tooltip !allm-text-xs" - /> </button> </div> </td> diff --git a/frontend/src/pages/GeneralSettings/BrowserExtensionApiKey/index.jsx b/frontend/src/pages/GeneralSettings/BrowserExtensionApiKey/index.jsx index 1bcb7c13f..2641aa64a 100644 --- a/frontend/src/pages/GeneralSettings/BrowserExtensionApiKey/index.jsx +++ b/frontend/src/pages/GeneralSettings/BrowserExtensionApiKey/index.jsx @@ -11,6 +11,7 @@ import NewBrowserExtensionApiKeyModal from "./NewBrowserExtensionApiKeyModal"; import ModalWrapper from "@/components/ModalWrapper"; import { useModal } from "@/hooks/useModal"; import { fullApiUrl } from "@/utils/constants"; +import { Tooltip } from "react-tooltip"; export default function BrowserExtensionApiKeys() { const [loading, setLoading] = useState(true); @@ -128,6 +129,18 @@ export default function BrowserExtensionApiKeys() { isMultiUser={isMultiUser} /> </ModalWrapper> + <Tooltip + id="auto-connection" + place="bottom" + delayShow={300} + className="allm-tooltip !allm-text-xs" + /> + <Tooltip + id="copy-connection-text" + place="bottom" + delayShow={300} + className="allm-tooltip !allm-text-xs" + /> </div> ); } -- GitLab