diff --git a/copilot-widget/README.md b/copilot-widget/README.md index b3f2f75ae..f6238bbcd 100644 --- a/copilot-widget/README.md +++ b/copilot-widget/README.md @@ -1,50 +1,57 @@ -# Copilot widget +# OpenCopilot widget -This is the widget for your copilot, it's what your users will interact with. +This is the widget for OpenCopilot: it's what your users will interact with. -It's a simple react application built to be used in any webpage as a widget, to download the latest build of the widget, go to the actions tab and download the latest build artifact. +It's a simple React application that can be used in any webpage as a widget. To download the latest build of the widget, go to the actions tab in GitHub and download the latest build artifact. ## How to install -1. download the latest build artifact from the actions tab. +1. Download the latest build artifact from the actions tab. -2. extract the zip file. +2. Extract the zip file. -3. copy the `assets/*.js` file to your project. +3. Copy the `assets/*.js` file to your project. -4. reference the js file in your html file as follows: +4. Reference the js file in your HTML file as follows: ```html ``` -5. init the widget. +5. Initialize the widget. ```html ``` -### How to use +## How to use -1. click on the trigger element to open the widget. +1. Click on the trigger element to open the widget. -2. type your message and press enter to send it. +2. Type your message and press enter to send it. OpenCopilot widget diff --git a/copilot-widget/index.html b/copilot-widget/index.html index ed2f1c497..89d31f9ec 100644 --- a/copilot-widget/index.html +++ b/copilot-widget/index.html @@ -14,15 +14,6 @@ href="https://fonts.googleapis.com/css2?family=Noto+Sans+Arabic:wght@300;400;500;600;700&display=swap" rel="stylesheet" /> - - - -
-

- This is an Example on how the widget is fluid and can be placed - anywhere; -

-
- + + + +
+

+ Playground: here you can see how the widget behaves in different + contexts +

+
+ -
some text here, this is a sample text
-
+ - - diff --git a/copilot-widget/lib/CopilotWidget.tsx b/copilot-widget/lib/CopilotWidget.tsx index ea01545e1..3295f7650 100644 --- a/copilot-widget/lib/CopilotWidget.tsx +++ b/copilot-widget/lib/CopilotWidget.tsx @@ -2,19 +2,20 @@ import { useEffect, useRef } from "react"; import { useWidgetState } from "./contexts/WidgetState"; import cn from "./utils/cn"; import ChatScreen from "./screens/ChatScreen"; -import { IS_SERVER } from "./utils/is_server"; +import { isServer } from "./utils/isServer.ts"; import { MessageCircle } from "lucide-react"; function useTrigger(selector?: string, toggle?: () => void) { const trigger = useRef( - !selector ? null : IS_SERVER ? null : document.querySelector(selector) + !selector ? null : isServer ? null : document.querySelector(selector) ).current; useEffect(() => { if (!selector) { return; } - if (trigger && !IS_SERVER) { + + if (trigger && !isServer) { trigger.addEventListener("click", () => toggle?.()); return () => trigger.removeEventListener("click", () => toggle?.()); } else { @@ -25,8 +26,8 @@ function useTrigger(selector?: string, toggle?: () => void) { }, [selector, toggle, trigger]); } -const TRIGGER_BOTTOM = "20px"; -const TRIGGER_RIGHT = "20px"; +const OFFSET_BOTTOM = "20px"; +const OFFSET_RIGHT = "20px"; export function CopilotWidget({ triggerSelector, @@ -38,6 +39,7 @@ export function CopilotWidget({ const [open, toggle] = useWidgetState(); useTrigger(triggerSelector, toggle); const SHOULD_RENDER_IN_THE_RIGHT_CORNER = !triggerSelector && __isEmbedded; + return ( <>
@@ -69,8 +71,8 @@ export function CopilotWidget({
+ +
+ + + ); +} export default function ChatHeader() { const [, , SetState] = useWidgetState(); const { data } = useInitialData(); - const { get } = useLang(); + const config = useConfigData(); + return (

- {data?.bot_name || "opencopilot"} + {data?.bot_name || "OpenCopilot"}

-
- - +
+ {config?.warnBeforeClose === false ? ( + - -
- -
+ + ) : ( + + )}
diff --git a/copilot-widget/lib/components/ChatInputFooter.tsx b/copilot-widget/lib/components/ChatInputFooter.tsx index 3b969fe9d..8cf5ccd1c 100644 --- a/copilot-widget/lib/components/ChatInputFooter.tsx +++ b/copilot-widget/lib/components/ChatInputFooter.tsx @@ -3,7 +3,7 @@ import { SendHorizonal, AlertTriangle, RotateCcw } from "lucide-react"; import { useChat } from "../contexts/Controller"; import { useRef, useState } from "react"; import { getId, isEmpty } from "@lib/utils/utils"; -import now from "@lib/utils/timenow"; +import { now } from "@lib/utils/time"; import { useDocumentDirection } from "@lib/hooks/useDocumentDirection"; import { VoiceRecorder } from "./VoiceRecorder"; import { useInitialData } from "@lib/hooks/useInitialData"; diff --git a/copilot-widget/lib/components/Dialog.tsx b/copilot-widget/lib/components/Dialog.tsx index f3ad9fd84..ac4f4eeed 100644 --- a/copilot-widget/lib/components/Dialog.tsx +++ b/copilot-widget/lib/components/Dialog.tsx @@ -1,4 +1,4 @@ -import { createSafeContext } from "@lib/contexts/create-safe-context"; +import { createSafeContext } from "@lib/contexts/createSafeContext"; import cn from "@lib/utils/cn"; import { ElementRef, @@ -71,6 +71,7 @@ const DialogContent = forwardRef< ComponentPropsWithoutRef<"div"> >(({ className, ...props }, ref) => { const { open } = useDialog(); + return ( open && ( @@ -113,6 +114,7 @@ const DialogHeader = forwardRef< ComponentPropsWithoutRef<"div"> >(({ className, ...props }, ref) => { const { open } = useDialog(); + return (
); } -function UserIcon() { +function UserAvatar() { const config = useConfigData(); + + if (config?.user?.avatarUrl) { + return ( + + ); + } + + return ( +
+ + + +
+ ); +} + +function User() { + const config = useConfigData(); + return ( -
- - - -
+
); @@ -52,7 +75,10 @@ export function BotTextMessage({ }) { const { messages, lastMessageToVote } = useChat(); const isLast = getLast(messages)?.id === id; + const config = useConfigData(); + if (isEmpty(message)) return null; + return (
@@ -70,12 +96,11 @@ export function BotTextMessage({
+ {isLast && (
- Bot - {lastMessageToVote && isLast && ( - - )} + {config?.bot?.name ?? "Bot"} + {lastMessageToVote && }
)}
@@ -95,7 +120,7 @@ export function UserMessage({ dir="auto" className="w-full overflow-x-auto shrink-0 max-w-full last-of-type:mb-10 bg-accent p-2 flex gap-3 items-center" > - +
@@ -132,16 +157,17 @@ export function BotMessageLoading({ displayText }: { displayText: string }) { ); } -export function BotMessageError({ message }: { message?: FailedMessage }) { +export function BotMessageError() { const { displayText } = useTypeWriter({ text: "Error sending the message.", every: 0.001, }); + return (
-
+
-
{displayText}
+
{displayText}
); diff --git a/copilot-widget/lib/components/VoiceRecorder.tsx b/copilot-widget/lib/components/VoiceRecorder.tsx index 6bcb94d18..78e49651a 100644 --- a/copilot-widget/lib/components/VoiceRecorder.tsx +++ b/copilot-widget/lib/components/VoiceRecorder.tsx @@ -1,7 +1,7 @@ -import { Square, MicIcon } from "lucide-react"; +import { MicIcon, Square } from "lucide-react"; import { Tooltip, TooltipContent, TooltipTrigger } from "./ToolTip"; import { useAxiosInstance } from "@lib/contexts/axiosInstance"; -import now from "@lib/utils/timenow"; +import { now } from "@lib/utils/time"; import { useEffect } from "react"; import useAudioRecorder from "@lib/hooks/useAudioRecord"; import { useLang } from "@lib/contexts/LocalesProvider"; diff --git a/copilot-widget/lib/components/Vote.tsx b/copilot-widget/lib/components/Vote.tsx index b096b25c0..cf1c9a78d 100644 --- a/copilot-widget/lib/components/Vote.tsx +++ b/copilot-widget/lib/components/Vote.tsx @@ -13,7 +13,7 @@ export function Vote({ messageId }: { messageId: number }) { const userVoted = isUpvoted || isDownvoted; const { get } = useLang(); return ( -
+
{userVoted ? ( {get("thank-you")} diff --git a/copilot-widget/lib/contexts/ConfigData.tsx b/copilot-widget/lib/contexts/ConfigData.tsx index 620c644e6..ebe183233 100644 --- a/copilot-widget/lib/contexts/ConfigData.tsx +++ b/copilot-widget/lib/contexts/ConfigData.tsx @@ -1,17 +1,14 @@ import type { Options } from "@lib/types"; import type { ReactNode } from "react"; -import { createSafeContext } from "./create-safe-context"; +import { createSafeContext } from "./createSafeContext"; export type ConfigDataContextType = Omit< Options, - 'containerProps' | 'triggerSelector' + "containerProps" | "triggerSelector" >; -const [ - useConfigData, - ConfigDataSafeProvider, - -] = createSafeContext(); +const [useConfigData, ConfigDataSafeProvider] = + createSafeContext(); export default function ConfigDataProvider({ children, @@ -21,11 +18,9 @@ export default function ConfigDataProvider({ children: ReactNode; }) { return ( - - {children} - + {children} ); } -// eslint-disable-next-line react-refresh/only-export-components -export { useConfigData } \ No newline at end of file +// eslint-disable-next-line react-refresh/only-export-components +export { useConfigData }; diff --git a/copilot-widget/lib/contexts/Controller.tsx b/copilot-widget/lib/contexts/Controller.tsx index d3ade2811..ea55748de 100644 --- a/copilot-widget/lib/contexts/Controller.tsx +++ b/copilot-widget/lib/contexts/Controller.tsx @@ -5,14 +5,14 @@ import React, { useMemo, useState, } from "react"; -import now from "../utils/timenow"; +import { now } from "../utils/time"; import { useConfigData } from "./ConfigData"; import { Message } from "@lib/types"; import { getId } from "@lib/utils/utils"; import io from "socket.io-client"; import { useSessionId } from "@lib/hooks/useSessionId"; import { BotResponse, UserMessage } from "@lib/types/messageTypes"; -import { createSafeContext } from "./create-safe-context"; +import { createSafeContext } from "./createSafeContext"; import { useWidgetState } from "./WidgetState"; export type FailedMessage = { @@ -87,7 +87,7 @@ const ChatProvider: React.FC<{ children: ReactNode }> = ({ children }) => { from: "user", session_id: sessionId, headers: config.headers ?? {}, - bot_token: config.token + bot_token: config.token, }; socket.emit("send_chat", userMessage); diff --git a/copilot-widget/lib/contexts/LocalesProvider.tsx b/copilot-widget/lib/contexts/LocalesProvider.tsx index 392963ee8..526b1e76c 100644 --- a/copilot-widget/lib/contexts/LocalesProvider.tsx +++ b/copilot-widget/lib/contexts/LocalesProvider.tsx @@ -1,5 +1,5 @@ import { type LangType, getStr } from "@lib/locales"; -import { createSafeContext } from "./create-safe-context"; +import { createSafeContext } from "./createSafeContext"; import { useConfigData } from "./ConfigData"; const [useLang, SafeLanguageProvider] = createSafeContext<{ diff --git a/copilot-widget/lib/contexts/WidgetState.tsx b/copilot-widget/lib/contexts/WidgetState.tsx index b614831e9..070b9c3b9 100644 --- a/copilot-widget/lib/contexts/WidgetState.tsx +++ b/copilot-widget/lib/contexts/WidgetState.tsx @@ -1,20 +1,20 @@ import type { ReactNode } from "react"; import useToggle from "../hooks/useToggle"; import { useConfigData } from "./ConfigData"; -import { createSafeContext } from "./create-safe-context"; +import { createSafeContext } from "./createSafeContext"; -type StateContextType = ReturnType +type StateContextType = ReturnType; -const [ - useWidgetState, - WidgetStateSafeProvider, -] = createSafeContext() +const [useWidgetState, WidgetStateSafeProvider] = + createSafeContext(); export default function WidgetState({ children }: { children: ReactNode }) { - const { defaultOpen } = useConfigData() - const data = useToggle(defaultOpen ?? false) - return {children}; + const { defaultOpen } = useConfigData(); + const data = useToggle(defaultOpen ?? false); + return ( + {children} + ); } // eslint-disable-next-line react-refresh/only-export-components -export { useWidgetState } \ No newline at end of file +export { useWidgetState }; diff --git a/copilot-widget/lib/contexts/axiosInstance.tsx b/copilot-widget/lib/contexts/axiosInstance.tsx index 4dda6ad9e..5becd4601 100644 --- a/copilot-widget/lib/contexts/axiosInstance.tsx +++ b/copilot-widget/lib/contexts/axiosInstance.tsx @@ -3,20 +3,18 @@ import { ReactNode, useMemo } from "react"; import { useConfigData } from "./ConfigData"; import { useSessionId } from "@lib/hooks/useSessionId"; import { createAxiosInstance } from "@lib/data/chat"; -import { createSafeContext } from "./create-safe-context"; +import { createSafeContext } from "./createSafeContext"; interface AxiosInstanceProps { axiosInstance: AxiosInstance; } -const [ - useAxiosInstance, - AxiosSafeProvider, -] = createSafeContext(); +const [useAxiosInstance, AxiosSafeProvider] = + createSafeContext(); function AxiosProvider({ children }: { children: ReactNode }) { const config = useConfigData(); - const { sessionId } = useSessionId(config?.token || 'defaultToken'); + const { sessionId } = useSessionId(config?.token || "defaultToken"); const axiosInstance: AxiosInstance = useMemo(() => { return createAxiosInstance({ botToken: config?.token, @@ -26,11 +24,9 @@ function AxiosProvider({ children }: { children: ReactNode }) { }, [config, sessionId]); return ( - - {children} - + {children} ); } // eslint-disable-next-line react-refresh/only-export-components -export { useAxiosInstance, AxiosProvider }; \ No newline at end of file +export { useAxiosInstance, AxiosProvider }; diff --git a/copilot-widget/lib/contexts/create-safe-context.ts b/copilot-widget/lib/contexts/create-safe-context.ts deleted file mode 100644 index e5a31d8b7..000000000 --- a/copilot-widget/lib/contexts/create-safe-context.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { createContext, useContext } from "react"; - -export function createSafeContext(init?: TDdata) { - const context = createContext(init); - const useSafeContext = () => { - const ctx = useContext(context); - if (ctx === undefined) { - throw new Error("useSafeContext must be used within a Provider"); - } - return ctx; - }; - return [useSafeContext, context.Provider] as const; -} \ No newline at end of file diff --git a/copilot-widget/lib/contexts/createSafeContext.ts b/copilot-widget/lib/contexts/createSafeContext.ts new file mode 100644 index 000000000..8333caca1 --- /dev/null +++ b/copilot-widget/lib/contexts/createSafeContext.ts @@ -0,0 +1,13 @@ +import { createContext, useContext } from "react"; + +export function createSafeContext(init?: TDdata) { + const context = createContext(init); + const useSafeContext = () => { + const ctx = useContext(context); + if (ctx === undefined) { + throw new Error("useSafeContext must be used within a Provider"); + } + return ctx; + }; + return [useSafeContext, context.Provider] as const; +} diff --git a/copilot-widget/lib/data/chat.ts b/copilot-widget/lib/data/chat.ts index 5970e38a9..1b61606c1 100644 --- a/copilot-widget/lib/data/chat.ts +++ b/copilot-widget/lib/data/chat.ts @@ -27,6 +27,7 @@ export function createAxiosInstance({ }); return instance; } + type HistoryMessage = { chatbot_id: string; created_at: string; @@ -64,6 +65,7 @@ export function historyToMessages(history?: HistoryMessage[]): Message[] { } return $messages; } + export async function getInitialData(instance: AxiosInstance) { const { data } = await instance.get<{ bot_name: string; diff --git a/copilot-widget/lib/hooks/useCopy.ts b/copilot-widget/lib/hooks/useCopy.ts deleted file mode 100644 index d0d6888a8..000000000 --- a/copilot-widget/lib/hooks/useCopy.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { useState, useEffect } from "react"; - -type CopyFn = (text: any) => Promise; - -export function useCopyToClipboard(): [boolean, CopyFn] { - const [copied, setCopied] = useState(false); - const copy: CopyFn = async (text) => { - if (!navigator?.clipboard) { - console.warn("Clipboard not supported"); - return false; - } - - // Try to save to clipboard then update the state if it worked - try { - await navigator.clipboard.writeText(text); - setCopied(true); - return true; - } catch (error) { - console.warn("Copy failed", error); - setCopied(false); - return false; - } - }; - - useEffect(() => { - if (copied) { - const timeout = setTimeout(() => { - setCopied(false); - }, 5000); - - return () => clearTimeout(timeout); - } - }, [copied]); - - return [copied, copy]; -} diff --git a/copilot-widget/lib/hooks/useDocumentDirection.ts b/copilot-widget/lib/hooks/useDocumentDirection.ts index 98e4e7ae9..2947ff6a0 100644 --- a/copilot-widget/lib/hooks/useDocumentDirection.ts +++ b/copilot-widget/lib/hooks/useDocumentDirection.ts @@ -1,19 +1,21 @@ -import { useEffect, useState } from "react" +import { useEffect, useState } from "react"; -type Dir = "rtl" | "ltr" +type Dir = "rtl" | "ltr"; export function useDocumentDirection() { - const [direction, setDirection] = useState(getComputedStyle(document.body).direction as Dir) - useEffect(() => { - const observer = new MutationObserver(() => { - setDirection(getComputedStyle(document.body).direction as Dir) - }) - observer.observe(document.head, { attributes: true }) - return () => observer.disconnect() - }, []) - return { - rtl: direction === "rtl", - ltr: direction === "ltr", - direction - } -} \ No newline at end of file + const [direction, setDirection] = useState( + getComputedStyle(document.body).direction as Dir + ); + useEffect(() => { + const observer = new MutationObserver(() => { + setDirection(getComputedStyle(document.body).direction as Dir); + }); + observer.observe(document.head, { attributes: true }); + return () => observer.disconnect(); + }, []); + return { + rtl: direction === "rtl", + ltr: direction === "ltr", + direction, + }; +} diff --git a/copilot-widget/lib/hooks/useInitialData.tsx b/copilot-widget/lib/hooks/useInitialData.ts similarity index 80% rename from copilot-widget/lib/hooks/useInitialData.tsx rename to copilot-widget/lib/hooks/useInitialData.ts index 2d965d118..36a289a7f 100644 --- a/copilot-widget/lib/hooks/useInitialData.tsx +++ b/copilot-widget/lib/hooks/useInitialData.ts @@ -4,5 +4,8 @@ import useSWR from "swr"; export function useInitialData() { const { axiosInstance } = useAxiosInstance(); - return useSWR("initialData", () => getInitialData(axiosInstance), {revalidateIfStale: false, revalidateOnFocus: false}); + return useSWR("initialData", () => getInitialData(axiosInstance), { + revalidateIfStale: false, + revalidateOnFocus: false, + }); } diff --git a/copilot-widget/lib/hooks/useScroll.ts b/copilot-widget/lib/hooks/useScroll.ts deleted file mode 100644 index 214cf35d8..000000000 --- a/copilot-widget/lib/hooks/useScroll.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { useState, useEffect, RefObject } from 'react'; - -interface ScrollPosition { - x: number; - y: number; -} - -const useScroll = (targetRef: RefObject): ScrollPosition => { - const [scrollPosition, setScrollPosition] = useState({ - x: 0, - y: 0, - }); - - useEffect(() => { - const handleScroll = () => { - if (targetRef.current) { - const { scrollWidth, scrollHeight, clientWidth, clientHeight } = targetRef.current; - - setScrollPosition({ - x: (targetRef.current.scrollLeft / (scrollWidth - clientWidth)), - y: (targetRef.current.scrollTop / (scrollHeight - clientHeight)), - }); - } - }; - - const targetElement = targetRef.current; - - if (targetElement) { - targetElement.addEventListener('scroll', handleScroll); - } - - return () => { - if (targetElement) { - targetElement.removeEventListener('scroll', handleScroll); - } - }; - }, [targetRef]); - - return scrollPosition; -}; - -export default useScroll; diff --git a/copilot-widget/lib/hooks/useScrollTo.ts b/copilot-widget/lib/hooks/useScrollTo.ts index f78fbe41f..9277060e6 100644 --- a/copilot-widget/lib/hooks/useScrollTo.ts +++ b/copilot-widget/lib/hooks/useScrollTo.ts @@ -1,22 +1,22 @@ -import { RefObject } from 'react'; +import { RefObject } from "react"; -const useScrollToPercentage = (elementRef: RefObject -): [(percentageX: number, percentageY: number) => void - ] => { - const scrollToPercentage = (percentageX: number, percentageY: number) => { - if (elementRef.current) { - const { scrollWidth, scrollHeight } = elementRef.current; - const maxScrollX = scrollWidth - elementRef.current.clientWidth; - const maxScrollY = scrollHeight - elementRef.current.clientHeight; +const useScrollToPercentage = ( + elementRef: RefObject +): [(percentageX: number, percentageY: number) => void] => { + const scrollToPercentage = (percentageX: number, percentageY: number) => { + if (elementRef.current) { + const { scrollWidth, scrollHeight } = elementRef.current; + const maxScrollX = scrollWidth - elementRef.current.clientWidth; + const maxScrollY = scrollHeight - elementRef.current.clientHeight; - const scrollToX = (percentageX / 100) * maxScrollX; - const scrollToY = (percentageY / 100) * maxScrollY; + const scrollToX = (percentageX / 100) * maxScrollX; + const scrollToY = (percentageY / 100) * maxScrollY; - elementRef.current.scrollTo(scrollToX, scrollToY); - } - }; + elementRef.current.scrollTo(scrollToX, scrollToY); + } + }; - return [scrollToPercentage]; + return [scrollToPercentage]; }; export default useScrollToPercentage; diff --git a/copilot-widget/lib/hooks/useSessionId.ts b/copilot-widget/lib/hooks/useSessionId.ts index 2bb44ab2a..60a2fd498 100644 --- a/copilot-widget/lib/hooks/useSessionId.ts +++ b/copilot-widget/lib/hooks/useSessionId.ts @@ -2,13 +2,15 @@ import { useRef } from "react"; // the session id will be copilotId:uniqueId function randomString(length = 10) { - return Math.random().toString(36).substring(2, length + 2); + return Math.random() + .toString(36) + .substring(2, length + 2); } export function useSessionId(copilotToken: string) { const sessionId = useRef(copilotToken + "|" + randomString()).current; const setSessionId = (copilotToken: string) => { // copilotToken - } + }; return { sessionId, setSessionId }; } diff --git a/copilot-widget/lib/hooks/useTextRotator.ts b/copilot-widget/lib/hooks/useTextRotator.ts deleted file mode 100644 index a07ee9084..000000000 --- a/copilot-widget/lib/hooks/useTextRotator.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { useEffect, useState } from "react"; - -export function useTextRotator({ texts, intervalInSeconds }: { texts: string[], intervalInSeconds: number }) { - const [textObjects, setTextObjects] = useState(texts.map((text, index) => ({ - text, - isVisible: index === 0, - order: index - }))); - - useEffect(() => { - let interval: NodeJS.Timeout; - if (textObjects.length > 1) { - interval = setInterval(() => { - setTextObjects((prev) => { - const next = [...prev]; - const currentVisibleIndex = next.findIndex(({ isVisible }) => isVisible); - const nextIndex = currentVisibleIndex + 1; - if (nextIndex < next.length) { - next[currentVisibleIndex].isVisible = false; - next[nextIndex].isVisible = true; - } else { - clearInterval(interval); - } - return next; - }) - }, intervalInSeconds * 1000); - } - return () => { - if (interval) { - clearInterval(interval); - } - } - }, [texts, intervalInSeconds, textObjects]); - - return textObjects -}; \ No newline at end of file diff --git a/copilot-widget/lib/hooks/useToggle.ts b/copilot-widget/lib/hooks/useToggle.ts index a22ed1978..9733419c1 100644 --- a/copilot-widget/lib/hooks/useToggle.ts +++ b/copilot-widget/lib/hooks/useToggle.ts @@ -1,11 +1,10 @@ -import { Dispatch, SetStateAction, useCallback, useState } from 'react' +import { Dispatch, SetStateAction, useCallback, useState } from "react"; export default function useToggle( - defaultValue?: boolean, + defaultValue?: boolean ): [boolean, () => void, Dispatch>] { - const [value, setValue] = useState(!!defaultValue) + const [value, setValue] = useState(!!defaultValue); + const toggle = useCallback(() => setValue((x) => !x), []); - const toggle = useCallback(() => setValue(x => !x), []) - - return [value, toggle, setValue] -} \ No newline at end of file + return [value, toggle, setValue]; +} diff --git a/copilot-widget/lib/hooks/useTypeWriter.ts b/copilot-widget/lib/hooks/useTypeWriter.ts index 9a5f7bbdf..90999f11f 100644 --- a/copilot-widget/lib/hooks/useTypeWriter.ts +++ b/copilot-widget/lib/hooks/useTypeWriter.ts @@ -1,34 +1,42 @@ import { useEffect, useState } from "react"; +type useTypeWriterOpts = { + text: string; + every?: number; + onFinish?: () => void; + shouldStart?: boolean; +}; +export default function useTypeWriter({ + text, + every, + onFinish, + shouldStart = true, +}: useTypeWriterOpts) { + const [displayText, setDisplayText] = useState(""); + const [currentIndex, setCurrentIndex] = useState(-1); + const [isComplete, setIsComplete] = useState(false); + const timeout = every || 0.00001; -type useTypeWriterOpts = { text: string, every?: number, onFinish?: () => void, shouldStart?: boolean } + useEffect(() => { + if (shouldStart) { + if (currentIndex < text.length + 1) { + const timer = setInterval(() => { + setDisplayText(text.substring(0, currentIndex + 1)); + setCurrentIndex((prevIndex) => prevIndex + 1); + }, timeout); + return () => { + clearInterval(timer); + }; + } else { + setIsComplete(true); -export default function useTypeWriter({ text, every, onFinish, shouldStart = true }: useTypeWriterOpts) { - const [displayText, setDisplayText] = useState(""); - const [currentIndex, setCurrentIndex] = useState(-1); - const [isComplete, setIsComplete] = useState(false); - const timeout = every || 0.00001; - - useEffect(() => { - if (shouldStart) { - if (currentIndex < text.length + 1) { - const timer = setInterval(() => { - setDisplayText(text.substring(0, currentIndex + 1)); - setCurrentIndex((prevIndex) => prevIndex + 1); - }, timeout); - - return () => { - clearInterval(timer); - }; - } else { - setIsComplete(true); - if (typeof onFinish === "function") { - onFinish(); - } - } + if (typeof onFinish === "function") { + onFinish(); } - }, [text, currentIndex, timeout, onFinish, shouldStart]); - return { displayText, isComplete, text } -} \ No newline at end of file + } + } + }, [text, currentIndex, timeout, onFinish, shouldStart]); + return { displayText, isComplete, text }; +} diff --git a/copilot-widget/lib/index.ts b/copilot-widget/lib/index.ts index b4b9a054a..67e34e6f3 100644 --- a/copilot-widget/lib/index.ts +++ b/copilot-widget/lib/index.ts @@ -1,6 +1,4 @@ import Root from "./Root"; import { CopilotWidget } from "./CopilotWidget"; -export { - Root, - CopilotWidget, -} \ No newline at end of file + +export { Root, CopilotWidget }; diff --git a/copilot-widget/lib/locales/ar.locale.ts b/copilot-widget/lib/locales/ar.locale.ts index b65b91840..db021f896 100644 --- a/copilot-widget/lib/locales/ar.locale.ts +++ b/copilot-widget/lib/locales/ar.locale.ts @@ -1,6 +1,6 @@ -import { Locale } from "./types"; +import { TranslatableMessages } from "./types"; -const arLocale: Locale = { +export const arLocale: TranslatableMessages = { ok: "حسنا", agree: "موافق", cancel: "إلغاء", @@ -11,5 +11,3 @@ const arLocale: Locale = { recording: "تسجيل", "thank-you": "شكرا", }; - -export default arLocale; diff --git a/copilot-widget/lib/locales/en.locale.ts b/copilot-widget/lib/locales/en.locale.ts index 70a2b1a13..b88512d75 100644 --- a/copilot-widget/lib/locales/en.locale.ts +++ b/copilot-widget/lib/locales/en.locale.ts @@ -1,15 +1,13 @@ -import { Locale } from "./types"; +import { TranslatableMessages } from "./types"; -const enLocale: Locale = { +export const enLocale: TranslatableMessages = { ok: "Ok", agree: "Agree", cancel: "Cancel", - "yes-exit": "Yes, Exit", - "yes-reset": "Yes, Reset", - "no-cancel": "No, Cancel", + "yes-exit": "Yes, exit", + "yes-reset": "Yes, reset", + "no-cancel": "No, cancel", "are-you-sure": "Are you sure?", recording: "Recording", "thank-you": "Thank you", }; - -export default enLocale; diff --git a/copilot-widget/lib/locales/helper.ts b/copilot-widget/lib/locales/helper.ts index 3496c04fa..53d65fb26 100644 --- a/copilot-widget/lib/locales/helper.ts +++ b/copilot-widget/lib/locales/helper.ts @@ -1,9 +1,11 @@ -import enLocale from "./en.locale"; -import arLocale from "./ar.locale"; +import { enLocale } from "./en.locale"; +import { arLocale } from "./ar.locale"; +import { nlLocale } from "./nl.locale.ts"; const locales = { en: enLocale, ar: arLocale, + nl: nlLocale, }; export type LangType = keyof typeof locales; diff --git a/copilot-widget/lib/locales/nl.locale.ts b/copilot-widget/lib/locales/nl.locale.ts new file mode 100644 index 000000000..41a50315d --- /dev/null +++ b/copilot-widget/lib/locales/nl.locale.ts @@ -0,0 +1,13 @@ +import { TranslatableMessages } from "./types"; + +export const nlLocale: TranslatableMessages = { + ok: "Ok", + agree: "Akkoord", + cancel: "Annuleren", + "yes-exit": "Beëindigen", + "yes-reset": "Reset chat", + "no-cancel": "Annuleren", + "are-you-sure": "Wil je de chat beëindigen?", + recording: "Aan het opnemen...", + "thank-you": "Thanks!", +}; diff --git a/copilot-widget/lib/locales/types.ts b/copilot-widget/lib/locales/types.ts index 8b04a9686..0252f8697 100644 --- a/copilot-widget/lib/locales/types.ts +++ b/copilot-widget/lib/locales/types.ts @@ -1,3 +1,3 @@ -export type Locale = { +export type TranslatableMessages = { [key: string]: string; }; \ No newline at end of file diff --git a/copilot-widget/lib/screens/ChatScreen.tsx b/copilot-widget/lib/screens/ChatScreen.tsx index c641e5edb..75deae97b 100644 --- a/copilot-widget/lib/screens/ChatScreen.tsx +++ b/copilot-widget/lib/screens/ChatScreen.tsx @@ -10,7 +10,7 @@ import useScrollToPercentage from "../hooks/useScrollTo"; import ChatInputFooter from "../components/ChatInputFooter"; import { ChatProvider, useChat } from "../contexts/Controller"; import { useConfigData } from "../contexts/ConfigData"; -import { Map } from "../utils/Map"; +import { Map } from "../utils/map"; function ChatScreen() { const scrollElementRef = useRef(null); @@ -18,6 +18,7 @@ function ChatScreen() { const { messages, loading, failedMessage, conversationInfo } = useChat(); const config = useConfigData(); const initialMessage = config?.initialMessage; + useEffect(() => { setPos(0, 100); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -43,16 +44,15 @@ function ChatScreen() { fallback={
} data={messages} render={(message, index) => { - if (message.from === "bot") { - if (message.type === "text") - return ( - - ); + if (message.from === "bot" && message.type === "text") { + return ( + + ); } else if (message.from === "user") { return ( )} - {failedMessage && } + {failedMessage && }
diff --git a/copilot-widget/lib/types/index.ts b/copilot-widget/lib/types/index.ts index 0aff2d149..4a78d685c 100644 --- a/copilot-widget/lib/types/index.ts +++ b/copilot-widget/lib/types/index.ts @@ -1,2 +1,2 @@ -export { type Message } from './messageTypes'; -export { type Options } from './options'; \ No newline at end of file +export { type Message } from "./messageTypes"; +export { type Options } from "./options"; diff --git a/copilot-widget/lib/types/messageTypes.ts b/copilot-widget/lib/types/messageTypes.ts index a7461e481..331bb19ef 100644 --- a/copilot-widget/lib/types/messageTypes.ts +++ b/copilot-widget/lib/types/messageTypes.ts @@ -1,5 +1,5 @@ -// original shape type TS = Date | number; + export type BotResponse = { id: string | number; timestamp: TS; @@ -8,12 +8,13 @@ export type BotResponse = { response: { text: string; }; -} +}; + export type UserMessage = { id: string | number; timestamp: TS; from: "user"; content: string; -} +}; export type Message = BotResponse | UserMessage; diff --git a/copilot-widget/lib/types/options.ts b/copilot-widget/lib/types/options.ts index 94433a1b4..8e41e0c67 100644 --- a/copilot-widget/lib/types/options.ts +++ b/copilot-widget/lib/types/options.ts @@ -9,11 +9,17 @@ export type Options = { socketUrl: string; defaultOpen?: boolean; language?: LangType; + warnBeforeClose?: boolean; containerProps?: React.DetailedHTMLProps< React.HTMLAttributes, HTMLDivElement >; user?: { name?: string; + avatarUrl?: string; + }; + bot?: { + name?: string; + avatarUrl?: string; }; }; diff --git a/copilot-widget/lib/utils/cn.ts b/copilot-widget/lib/utils/cn.ts index 8b3f873ed..dbc16d640 100644 --- a/copilot-widget/lib/utils/cn.ts +++ b/copilot-widget/lib/utils/cn.ts @@ -1,6 +1,6 @@ -import { type ClassValue, clsx } from "clsx" -import { twMerge } from "tailwind-merge" - +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; + export default function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) -} \ No newline at end of file + return twMerge(clsx(inputs)); +} diff --git a/copilot-widget/lib/utils/formatTime.ts b/copilot-widget/lib/utils/formatTime.ts deleted file mode 100644 index cfd98a3ae..000000000 --- a/copilot-widget/lib/utils/formatTime.ts +++ /dev/null @@ -1,9 +0,0 @@ -export default function formatTimeFromTimestamp(timestamp: number | Date): string { - const date = new Date(timestamp); // Multiply by 1000 to convert seconds to milliseconds - const hours = date.getHours(); - const minutes = String(date.getMinutes()).padStart(2, '0'); - const period = hours >= 12 ? 'PM' : 'AM'; - const formattedHours = (hours % 12) || 12; // Convert hours from 24-hour to 12-hour format - - return `${formattedHours}:${minutes} ${period}`; -} \ No newline at end of file diff --git a/copilot-widget/lib/utils/isServer.ts b/copilot-widget/lib/utils/isServer.ts new file mode 100644 index 000000000..ca617a8ce --- /dev/null +++ b/copilot-widget/lib/utils/isServer.ts @@ -0,0 +1 @@ +export const isServer = typeof window === "undefined"; diff --git a/copilot-widget/lib/utils/is_server.ts b/copilot-widget/lib/utils/is_server.ts deleted file mode 100644 index 5b2577bc2..000000000 --- a/copilot-widget/lib/utils/is_server.ts +++ /dev/null @@ -1 +0,0 @@ -export const IS_SERVER = typeof window === "undefined"; diff --git a/copilot-widget/lib/utils/Map.tsx b/copilot-widget/lib/utils/map.ts similarity index 100% rename from copilot-widget/lib/utils/Map.tsx rename to copilot-widget/lib/utils/map.ts diff --git a/copilot-widget/lib/utils/time.ts b/copilot-widget/lib/utils/time.ts new file mode 100644 index 000000000..b9f5d9e2d --- /dev/null +++ b/copilot-widget/lib/utils/time.ts @@ -0,0 +1,13 @@ +export function now(): number { + return Date.now(); +} + +export function formatTimeFromTimestamp(timestamp: number | Date): string { + const date = new Date(timestamp); // Multiply by 1000 to convert seconds to milliseconds + const hours = date.getHours(); + const minutes = String(date.getMinutes()).padStart(2, "0"); + const period = hours >= 12 ? "PM" : "AM"; + const formattedHours = hours % 12 || 12; // Convert hours from 24-hour to 12-hour format + + return `${formattedHours}:${minutes} ${period}`; +} diff --git a/copilot-widget/lib/utils/timenow.ts b/copilot-widget/lib/utils/timenow.ts deleted file mode 100644 index 720adfdc7..000000000 --- a/copilot-widget/lib/utils/timenow.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default function now():number { - return Date.now() -} \ No newline at end of file diff --git a/copilot-widget/lib/utils/utils.ts b/copilot-widget/lib/utils/utils.ts index af8d720c0..186230fe4 100644 --- a/copilot-widget/lib/utils/utils.ts +++ b/copilot-widget/lib/utils/utils.ts @@ -21,4 +21,5 @@ function getId(): string { function getLast(arr: T[]): T | undefined { return arr[arr.length - 1]; } + export { isEmpty, getId, getLast }; diff --git a/copilot-widget/src/utils.ts b/copilot-widget/src/utils.ts index dc842afee..e672420b2 100644 --- a/copilot-widget/src/utils.ts +++ b/copilot-widget/src/utils.ts @@ -1,32 +1,34 @@ import type { CSSProperties } from "react"; + function getOrCreateRootById(id: string): HTMLElement { - let root = document.getElementById(id); - if (!root) { - root = document.createElement('div'); - root.id = id; - document.body.appendChild(root); - } - return root; + let root = document.getElementById(id); + + if (!root) { + root = document.createElement("div"); + root.id = id; + document.body.appendChild(root); + } + return root; } function styleTheRoot(root: HTMLElement, styles?: CSSProperties): HTMLElement { - // user may style the root from outside, so we need to preserve it and merge with our styles but our styles should have low priority. - Object.assign(root.style, styles); - return root; + // user may style the root from outside, so we need to preserve it and merge with our styles but our styles should have low priority. + Object.assign(root.style, styles); + + return root; } export function composeRoot(id: string, fluid?: boolean): HTMLElement { - const baseStyles: CSSProperties = { - isolation: 'isolate', - unicodeBidi: 'bidi-override', - fontVariantNumeric: 'tabular-nums', - } + const baseStyles: CSSProperties = { + isolation: "isolate", + unicodeBidi: "bidi-override", + fontVariantNumeric: "tabular-nums", + }; + const fluidStyles: CSSProperties = { + width: "100%", + height: "100%", + }; + const styles = fluid ? { ...baseStyles, ...fluidStyles } : baseStyles; - const fluidStyles: CSSProperties = { - width: '100%', - height: '100%', - } - - const styles = fluid ? { ...baseStyles, ...fluidStyles } : baseStyles; - return styleTheRoot(getOrCreateRootById(id), styles); -} \ No newline at end of file + return styleTheRoot(getOrCreateRootById(id), styles); +} diff --git a/copilot-widget/styles/index.css b/copilot-widget/styles/index.css index e815b8722..33df50dab 100644 --- a/copilot-widget/styles/index.css +++ b/copilot-widget/styles/index.css @@ -8,6 +8,7 @@ svg { vector-effect: non-scaling-stroke; stroke-width: 1.5; } + @keyframes fade-in-bottom { 0% { transform: translateY(50px); @@ -60,11 +61,6 @@ svg { animation: fade-in 1.2s cubic-bezier(0.39, 0.575, 0.565, 1) both; } -.PopoverContent { - transform-origin: var(--radix-popover-content-transform-origin); - animation: scaleIn 0.5s ease-out; -} - @keyframes scaleIn { from { opacity: 0; @@ -77,19 +73,6 @@ svg { } } -.PopoverContent { - animation-duration: 0.6s; - animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); -} - -.PopoverContent[data-side="top"] { - animation-name: slideUp; -} - -.PopoverContent[data-side="bottom"] { - animation-name: slideDown; -} - @keyframes slideDown { from { opacity: 0; @@ -147,9 +130,11 @@ svg { .scale-out-br { animation: scale-out-br 0.5s ease-in-out forwards; } + .no-scrollbar::-webkit-scrollbar { display: none; } + @keyframes text-blur-out { 0% { filter: blur(0.01);