diff --git a/src/assets/SvgIcons/IconExternalLink.tsx b/src/assets/SvgIcons/IconExternalLink.tsx new file mode 100644 index 0000000..6dff1e8 --- /dev/null +++ b/src/assets/SvgIcons/IconExternalLink.tsx @@ -0,0 +1,20 @@ +import SvgIcon from "src/components/shared/SvgIcon"; + +const IconExternalLink = (props: any) => { + const size = props?.size || 16; + return ( + + + {/* */} + + + + ); +}; + +export default IconExternalLink; diff --git a/src/components/shared/Buttons/Button.tsx b/src/components/shared/Buttons/Button.tsx index 64a84e3..ce8138c 100644 --- a/src/components/shared/Buttons/Button.tsx +++ b/src/components/shared/Buttons/Button.tsx @@ -37,7 +37,7 @@ const Button = ({
{rest.children} @@ -45,7 +45,8 @@ const Button = ({
diff --git a/src/components/shared/Layout/AppLayout.tsx b/src/components/shared/Layout/AppLayout.tsx index 4449d59..a5e427c 100644 --- a/src/components/shared/Layout/AppLayout.tsx +++ b/src/components/shared/Layout/AppLayout.tsx @@ -6,6 +6,7 @@ import Header from "src/widgets/copilot/components/Header"; import { addInlineStyle } from "src/addStyles"; import style from "./appLayout.scss?inline"; import SideNavbar from "./SideNavbar"; +import SecondaryDrawer from "./SecondaryDrawer"; addInlineStyle(style); @@ -21,7 +22,7 @@ const CHAT_WINDOW_WIDTH = 760; const generateParentContainerClass = ( isInline: boolean, isFullScreen: boolean, - isFocusMode: boolean + isFocusMode: boolean, ) => { if (!isInline) { if (isFocusMode) return "gooey-focused-popup"; @@ -67,8 +68,8 @@ const AppLayout = ({ children }: Props) => { generateParentContainerClass( layoutController!.isInline, config?.mode === "fullscreen", - layoutController!.isFocusMode - ) + layoutController!.isFocusMode, + ), )} >
@@ -85,6 +86,7 @@ const AppLayout = ({ children }: Props) => { <>{children}
+
); diff --git a/src/components/shared/Layout/SecondaryDrawer.tsx b/src/components/shared/Layout/SecondaryDrawer.tsx new file mode 100644 index 0000000..d5d3613 --- /dev/null +++ b/src/components/shared/Layout/SecondaryDrawer.tsx @@ -0,0 +1,144 @@ +import { useSystemContext } from "src/contexts/hooks"; +import clsx from "clsx"; +import { useEffect, useRef, useState } from "react"; + +const MIN_DRAWER_WIDTH = 300; +const MAX_DRAWER_WIDTH = 800; +const RESIZE_HANDLE_WIDTH = 5; + +const SecondaryDrawer = () => { + const { layoutController } = useSystemContext(); + const drawerRef = useRef(null); + const [isResizing, setIsResizing] = useState(false); + const [drawerWidth, setDrawerWidth] = useState(window.innerWidth * 0.65); + + useEffect(() => { + const sideBarElement = drawerRef.current; + + if (!sideBarElement || !layoutController?.isSecondaryDrawerOpen) return; + + if (layoutController?.isMobile) { + sideBarElement.style.width = "100%"; + sideBarElement.style.position = "absolute !important"; + } else { + if (layoutController?.isSecondaryDrawerOpen) { + sideBarElement.style.width = `${drawerWidth}px`; + sideBarElement.style.position = "relative !important"; + } + } + }, [ + layoutController?.isMobile, + layoutController?.isSecondaryDrawerOpen, + drawerWidth, + ]); + + const handleMouseDown = (e: React.MouseEvent) => { + if (layoutController?.isMobile) return; + setIsResizing(true); + e.preventDefault(); + }; + + useEffect(() => { + const handleMouseMove = (e: MouseEvent) => { + if (!isResizing) return; + + const container = drawerRef.current?.parentElement; + if (!container) return; + + const containerRect = container.getBoundingClientRect(); + const newWidth = containerRect.right - e.clientX; + + const constrainedWidth = Math.min( + Math.max(newWidth, MIN_DRAWER_WIDTH), + Math.max(MAX_DRAWER_WIDTH, containerRect.width * 0.8), + ); + + setDrawerWidth(constrainedWidth); + }; + + const handleMouseUp = () => { + setIsResizing(false); + }; + + if (isResizing) { + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + document.body.style.userSelect = "none"; + document.body.style.cursor = "ew-resize"; + } else { + document.body.style.userSelect = ""; + document.body.style.cursor = ""; + } + + return () => { + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + document.body.style.userSelect = ""; + document.body.style.cursor = ""; + }; + }, [isResizing]); + + return ( +
+ {/* Content Container */} +
+ {layoutController?.secondaryDrawerContent?.()} +
+ + {/* Resize System */} + {!layoutController?.isMobile && ( +
+
+
+ )} + + {/* Full-screen overlay during resize */} + {isResizing && ( +
+ )} +
+ ); +}; + +export default SecondaryDrawer; \ No newline at end of file diff --git a/src/components/shared/Layout/SideNavbar.tsx b/src/components/shared/Layout/SideNavbar.tsx index 93120ea..3f55168 100644 --- a/src/components/shared/Layout/SideNavbar.tsx +++ b/src/components/shared/Layout/SideNavbar.tsx @@ -113,7 +113,7 @@ const SideNavbar = () => { }} className={clsx( "b-rt-1 h-100 overflow-x-hidden top-0 left-0 bg-grey d-flex flex-col", - layoutController?.isMobile ? "pos-absolute" : "pos-relative" + layoutController?.isMobile ? "pos-absolute" : "pos-relative", )} >
{
@@ -242,7 +247,7 @@ const SideNavbar = () => { .sort( (a: Conversation, b: Conversation) => new Date(b.timestamp as string).getTime() - - new Date(a.timestamp as string).getTime() + new Date(a.timestamp as string).getTime(), ) .map((conversation: Conversation) => { return ( @@ -256,6 +261,8 @@ const SideNavbar = () => { setActiveConversation(conversation); if (layoutController?.isMobile) layoutController?.toggleSidebar(); + if (layoutController?.isSecondaryDrawerOpen) + layoutController?.toggleSecondaryDrawer(null); }} /> diff --git a/src/components/shared/Link.tsx b/src/components/shared/Link.tsx index d295277..15e181b 100644 --- a/src/components/shared/Link.tsx +++ b/src/components/shared/Link.tsx @@ -1,6 +1,24 @@ +import { useSystemContext } from "src/contexts/hooks"; +import { FullSourcePreview } from "src/widgets/copilot/components/Messages/Sources"; + const Link = (props: any) => { + const { layoutController } = useSystemContext(); + + const handleClick = () => { + layoutController?.toggleSecondaryDrawer?.(() => ( + + )); + }; + return ( - + handleClick()} + style={{ color: props.configColor }} + className="gooey-link cr-pointer" + > {props.children} ); diff --git a/src/contexts/SystemContext.tsx b/src/contexts/SystemContext.tsx index 9c2530c..3ac8248 100644 --- a/src/contexts/SystemContext.tsx +++ b/src/contexts/SystemContext.tsx @@ -3,17 +3,33 @@ import { CopilotConfigType } from "./types"; import useDeviceWidth from "src/hooks/useDeviceWidth"; // eslint-disable-next-line react-refresh/only-export-components -const toggleSidebarStyles = (isSidebarOpen: boolean) => { - const sideBarElement: HTMLElement | null | undefined = - gooeyShadowRoot?.querySelector("#gooey-side-navbar"); - if (!sideBarElement) return; - // set width to 0px if sidebar is closed - if (!isSidebarOpen) { - sideBarElement.style.width = "260px"; - sideBarElement.style.transition = "width ease-in-out 0.2s"; +const toggleSidebarStyles = ( + isSidebarOpen: boolean, + sidebarName: "left" | "right" = "left", + isMobile: boolean = false, +) => { + if (sidebarName === "right") { + const sideBarElement: HTMLElement | null | undefined = + gooeyShadowRoot?.querySelector("#gooey-right-bar"); + if (!sideBarElement) return; + // set width to 0px if sidebar is closed + if (!isSidebarOpen) { + sideBarElement.style.width = isMobile ? "100%" : "65vw"; + } else { + sideBarElement.style.width = "0px"; + } } else { - sideBarElement.style.width = "0px"; - sideBarElement.style.transition = "width ease-in-out 0.2s"; + const sideBarElement: HTMLElement | null | undefined = + gooeyShadowRoot?.querySelector("#gooey-side-navbar"); + if (!sideBarElement) return; + // set width to 0px if sidebar is closed + if (!isSidebarOpen) { + sideBarElement.style.width = "260px"; + sideBarElement.style.transition = "width ease-in-out 0.2s"; + } else { + sideBarElement.style.width = "0px"; + sideBarElement.style.transition = "width ease-in-out 0.2s"; + } } }; @@ -21,6 +37,7 @@ interface LayoutController extends LayoutStateType { toggleOpenClose: () => void; toggleSidebar: () => void; toggleFocusMode: () => void; + toggleSecondaryDrawer: (data: Record | null) => void; setState: (state: any) => void; } @@ -31,6 +48,8 @@ type LayoutStateType = { isMobile: boolean; isSidebarOpen: boolean; + isSecondaryDrawerOpen: boolean; + secondaryDrawerContent: () => ReactNode | null; showCloseButton: boolean; showSidebarButton: boolean; showFocusModeButton: boolean; @@ -68,6 +87,8 @@ const SystemContextProvider = ({ ? true : config?.enableConversations, isMobile: false, + isSecondaryDrawerOpen: false, + secondaryDrawerContent: () => null, }); const forceHideSidebar = !layoutState?.showNewConversationButton; const [isMobile, isMobileWindow] = useDeviceWidth("mobile", [ @@ -135,6 +156,31 @@ const SystemContextProvider = ({ } }); }, + toggleSecondaryDrawer: (data = null) => { + setLayoutState((prev: any) => { + const triggerSidebar = + data && prev.isSidebarOpen && !prev.isSecondaryDrawerOpen; + if (triggerSidebar) toggleSidebarStyles(prev.isSidebarOpen); + if ((data && !prev.isSecondaryDrawerOpen) || !data) + // open / close secondary drawer + toggleSidebarStyles( + prev.isSecondaryDrawerOpen, + "right", + prev.isMobile, + ); + return { + ...prev, + isSecondaryDrawerOpen: data ? true : false, + secondaryDrawerContent: data, + isSidebarOpen: triggerSidebar + ? !prev.isSidebarOpen + : prev.isSidebarOpen, + showSidebarButton: triggerSidebar + ? prev.isSidebarOpen + : prev.showSidebarButton, + }; + }); + }, setState: (state: any) => { setLayoutState((prev) => ({ ...prev, @@ -143,7 +189,7 @@ const SystemContextProvider = ({ }, ...layoutState, }), - [setLayoutState, forceHideSidebar, layoutState] + [setLayoutState, forceHideSidebar, layoutState], ); useEffect(() => { diff --git a/src/css/App.css b/src/css/App.css index aa7004f..a79da12 100644 --- a/src/css/App.css +++ b/src/css/App.css @@ -56,6 +56,20 @@ ul { text-decoration-thickness: 2px; } +.gooey-embed-container .gooey-link { + color: inherit; + text-decoration: underline; + text-decoration-color: rgba(0, 0, 0, 0.45); + text-decoration-thickness: 0.6px; + text-underline-offset: 0.15em; +} + +.gooey-embed-container .gooey-link:hover { + color: inherit; + text-decoration-color: black; + text-decoration-thickness: 2px; +} + div:focus-visible { outline: none; } diff --git a/src/css/_extra.scss b/src/css/_extra.scss index 1cbd303..e23f8f3 100644 --- a/src/css/_extra.scss +++ b/src/css/_extra.scss @@ -119,6 +119,9 @@ .b-rt-1 { border-right: 1px solid $border-color; } +.b-lt-1 { + border-left: 1px solid $border-color; +} .b-none { border: none !important; } @@ -171,6 +174,9 @@ .left-0 { left: 0; } +.right-0 { + right: 0; +} .h-header { height: 56px; } diff --git a/src/widgets/copilot/components/Header/index.tsx b/src/widgets/copilot/components/Header/index.tsx index 66bab21..17bc627 100644 --- a/src/widgets/copilot/components/Header/index.tsx +++ b/src/widgets/copilot/components/Header/index.tsx @@ -18,7 +18,7 @@ const Header = ({ onEditClick }: HeaderProps) => { const { messages }: any = useMessagesContext(); const { layoutController, config }: SystemContextType = useSystemContext(); const isEmpty = !messages?.size; - const botName = config?.branding?.name; + const branding = config?.branding; return (
@@ -71,17 +71,33 @@ const Header = ({ onEditClick }: HeaderProps) => { )}
-

- {botName} -

+
+ bot-avatar +
+

{branding?.name}

+
{layoutController?.showNewConversationButton && ( diff --git a/src/widgets/copilot/components/Loader/index.tsx b/src/widgets/copilot/components/Loader/index.tsx index ecfca18..99e6a14 100644 --- a/src/widgets/copilot/components/Loader/index.tsx +++ b/src/widgets/copilot/components/Loader/index.tsx @@ -17,8 +17,9 @@ const ResponseLoader = (props: any) => { if (!props.show) return null; return (
- - + + +
); }; diff --git a/src/widgets/copilot/components/Messages/IncomingMsg.tsx b/src/widgets/copilot/components/Messages/IncomingMsg.tsx index 079cc07..2ad317f 100644 --- a/src/widgets/copilot/components/Messages/IncomingMsg.tsx +++ b/src/widgets/copilot/components/Messages/IncomingMsg.tsx @@ -10,10 +10,10 @@ import Button from "src/components/shared/Buttons/Button"; import { memo } from "react"; addInlineStyle(style); -export const BotMessageLayout = () => { +export const BotMessageLayout = (props: Record) => { const branding = useSystemContext().config?.branding; return ( -
+
{branding?.photoUrl && (
{ />
)} -

{branding?.name}

+
{props.children}
); }; @@ -54,7 +54,7 @@ const FeedbackButtons = ({ data, onFeedbackClick }: any) => { > {getFeedbackButtonIcon(button.id, button.isPressed)} - ) + ), )}
); @@ -77,31 +77,43 @@ const IncomingMsg = memo( const parsedElements = formatTextResponse( props.data, props?.linkColor, - props?.showSources + props?.showSources, ); if (!parsedElements) return ; return (
- -
- {parsedElements} -
+ +
+ {parsedElements} +
+
{!isStreaming && !videoTrack && audioTrack && ( -
- +
+
)} {!isStreaming && videoTrack && (
- +
)} {!isStreaming && props?.data?.buttons && ( @@ -113,7 +125,7 @@ const IncomingMsg = memo(
); - } + }, ); export default IncomingMsg; diff --git a/src/widgets/copilot/components/Messages/OutgoingMsg.tsx b/src/widgets/copilot/components/Messages/OutgoingMsg.tsx index 71ed882..fd75190 100644 --- a/src/widgets/copilot/components/Messages/OutgoingMsg.tsx +++ b/src/widgets/copilot/components/Messages/OutgoingMsg.tsx @@ -1,7 +1,6 @@ import { addInlineStyle } from "src/addStyles"; import style from "./outgoing.scss?inline"; import { memo } from "react"; -import IconUserCircle from "src/assets/SvgIcons/IconUserCircle"; import clsx from "clsx"; addInlineStyle(style); @@ -9,10 +8,6 @@ const OutgoingMsg = memo((props: any) => { const { input_prompt = "", input_audio = "", input_images = [] } = props.data; return (
-
- -

You

-
{input_images.length > 0 && input_images.map((url: string) => ( @@ -21,7 +16,7 @@ const OutgoingMsg = memo((props: any) => { alt={url} className={clsx( "outgoingMsg-image b-1 br-large", - input_prompt && "gmb-4" + input_prompt && "gmb-4", )} /> diff --git a/src/widgets/copilot/components/Messages/Sources.tsx b/src/widgets/copilot/components/Messages/Sources.tsx index cc35be0..44dd0f2 100644 --- a/src/widgets/copilot/components/Messages/Sources.tsx +++ b/src/widgets/copilot/components/Messages/Sources.tsx @@ -1,57 +1,162 @@ import clsx from "clsx"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { extractFileDetails, extractMainDomain, fetchUrlMeta, findSourceIcon, + getEmbedUrl, truncateMiddle, } from "./helpers"; import { useSystemContext } from "src/contexts/hooks"; +import IconButton from "src/components/shared/Buttons/IconButton"; +import IconExternalLink from "src/assets/SvgIcons/IconExternalLink"; +import IconClose from "src/assets/SvgIcons/IconClose"; -const SourcesCard = (props: any) => { - const { data, index, onClick } = props; - const { getTempStoreValue, setTempStoreValue }: any = useSystemContext(); +export const FullSourcePreview = (props: any) => { + const { data, layoutController, metaData } = props; + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + // fade loader for src reset + setIsLoading(true); + setTimeout(() => { + setIsLoading(false); + }, 0); + }, [data.url]); + + const embedUrl = getEmbedUrl(data.url); + if (!data || !data?.url) return null; + const ExtensionIcon: any = findSourceIcon( + metaData?.content_type, + metaData?.redirect_urls[0] || data?.url, + 24, + ); + + if (isLoading) return null; + return ( +
+
+
+ {ExtensionIcon || !metaData?.logo ? ( + + ) : ( + {data?.title} + )} +

+ {data?.title} +

+ window.open(data?.url, "_ablank")} + variant="text-alt" + className="gml-4" + > + + +
+ layoutController?.toggleSecondaryDrawer(null)} + variant="text-alt" + className="gp-6" + > + + +
+