From 67d3898cc1d8eb8cf3bfe4c84fc7aaa6a8f49178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iurii=20=E1=9A=BE?= Date: Fri, 19 Apr 2024 15:04:49 +0100 Subject: [PATCH] feature: custom agent widget --- agent-widget/package.json | 6 +- agent-widget/src/react/.DS_Store | Bin 0 -> 6148 bytes agent-widget/src/react/agent.scss | 37 ++++++ agent-widget/src/react/agent.tsx | 103 ++++++++++++---- agent-widget/src/react/index.tsx | 7 +- agent-widget/src/react/modal/modal.scss | 157 ++++++++++++++++++++++++ agent-widget/src/react/modal/modal.tsx | 151 +++++++++++++++++++++++ agent-widget/yarn.lock | 42 +++++++ 8 files changed, 478 insertions(+), 25 deletions(-) create mode 100644 agent-widget/src/react/.DS_Store create mode 100644 agent-widget/src/react/modal/modal.scss create mode 100644 agent-widget/src/react/modal/modal.tsx diff --git a/agent-widget/package.json b/agent-widget/package.json index 50b5c4b..f636a16 100644 --- a/agent-widget/package.json +++ b/agent-widget/package.json @@ -1,6 +1,6 @@ { "name": "nvm-agent-demo", - "version": "0.0.2", + "version": "0.0.3", "description": "", "main": "index.js", "scripts": { @@ -66,7 +66,9 @@ "webpack-merge": "^5.10.0" }, "dependencies": { + "@types/react-router-dom": "^5.3.3", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-router-dom": "^6.22.3" } } diff --git a/agent-widget/src/react/.DS_Store b/agent-widget/src/react/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..e414c0e4705fa778e00708d332c06fb4f67439cb GIT binary patch literal 6148 zcmeH~Jx;_h5QS%8ks{G-37v0%wxcwwaDu%6ERmo{lx^sJ0FJ{II18fT47~9Sh%Axl zRtUY3?B|~uC;6n<9ud*?W497nib#WEs@9k>8=jpy3g-n>>l%H3zuMf5!)|7ozd2;@ zejA&+iv+FIW2>-7g-yhwp?E!f7Nx0wh2JBydmy zIJ3=~Q&-j`KmsK2MZorlgkqXQi)-CFP}LLwYS-v$Xj?8}F)1{M7FW!`)I*^js*z%- zhhsd2Uk)v<9xfWmhsKjXlNYT|$NH(di)L5WBtQaF0>|E6+WCKtUuHJRpN6nU0wnOy z2 { - const selector = `script[src='${srcFile}']`; +const InjectScript = React.memo(({ script }: { script: string }) => { + const divRef = useRef(null); - if (document.querySelectorAll(selector).length > 0) { - return; - } + useEffect(() => { + if (divRef.current === null) { + return; + } - const script = document.createElement('script'); - script.src = srcFile; - script.defer = true; + const doc = document.createRange().createContextualFragment(script); - insertBeforeElement.parentNode?.insertBefore(script, insertBeforeElement); -}; + divRef.current.innerHTML = ''; + divRef.current.appendChild(doc); + }, [script]); + + return
; +}); export const Agent = () => { + const [searchParams, setSearchParams] = useSearchParams(); + const [thread, setThread] = useState([]); const [query, setQuery] = useState(''); @@ -31,10 +36,28 @@ export const Agent = () => { const [mustTopUp, setMustTopUp] = useState(false); + const [showModal, setShowModal] = useState(false); + + const [textareaWidgetHtmlCode, setTextareaWidgetHtmlCode] = useState(''); + + const [widgetHtmlCode, setWidgetHtmlCode] = useState(''); + const messageRef = useRef(null); const threadEndRef = useRef(null); + const loadWidget = () => { + setAgentData(null); + + setMustTopUp(false); + + setShowModal(false); + + setThread([]); + + setSearchParams({ html: encodeURIComponent(textareaWidgetHtmlCode) }); + }; + const resetQuery = () => { setQuery(''); messageRef.current?.focus(); @@ -124,22 +147,58 @@ export const Agent = () => { }, [thread]); useEffect(() => { - insertWidgetScriptBefore( - 'https://widgets.testing.nevermined.app/nvm-agent-widget-loader.js', - document.querySelector('.nvm-agent-widget')! - ); + const encodedHtml = searchParams.get('html'); + + if (encodedHtml) { + const decodedHtml = decodeURIComponent(encodedHtml) + setWidgetHtmlCode(decodedHtml); + setTextareaWidgetHtmlCode(decodedHtml); + } + }, [searchParams]); + + useEffect(() => { window.addEventListener('message', handleAgentEvents, false); }, []); return (
-
+ {showModal && ( + { + setShowModal(false); + }} + > +
+ + +
+
+ )} + +
diff --git a/agent-widget/src/react/index.tsx b/agent-widget/src/react/index.tsx index 7ddc24e..189bf4d 100644 --- a/agent-widget/src/react/index.tsx +++ b/agent-widget/src/react/index.tsx @@ -1,5 +1,10 @@ import React from 'react'; import { createRoot } from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; import { Agent } from './agent'; -createRoot(document.getElementById('root')!).render(); +createRoot(document.getElementById('root')!).render( + + + +); diff --git a/agent-widget/src/react/modal/modal.scss b/agent-widget/src/react/modal/modal.scss new file mode 100644 index 0000000..9e5ff0f --- /dev/null +++ b/agent-widget/src/react/modal/modal.scss @@ -0,0 +1,157 @@ +.modal { + z-index: 2147483646; + position: fixed; + inset: 0; + pointer-events: all; + filter: drop-shadow(rgba(0, 0, 0, 0.4) 0px 4px 30px); +} + +.modal-overlay { + z-index: 1; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + opacity: 0; + animation: fade-in 150ms ease-out both; +} + +.modal-container { + --ease: cubic-bezier(0.25, 0.1, 0.25, 1); + --duration: 200ms; + --transition: height var(--duration) var(--ease), + width var(--duration) var(--ease); + z-index: 3; + display: block; + pointer-events: none; + position: absolute; + backface-visibility: hidden; + left: 50%; + top: 50%; + width: 100%; + transform: translate3d(-50%, -50%, 0); + + @media screen and (max-width: 600px) { + max-width: 448px; + margin: 0px auto; + + &:before { + width: 100%; + } + } +} + +.modal-box-container { + z-index: 2; + position: relative; + animation: 150ms ease both; + animation-name: box-out; + + &:before { + content: ''; + position: absolute; + top: 0; + bottom: 0; + left: 50%; + width: var(--width); + height: var(--height); + transform: translateX(-50%); + backface-visibility: hidden; + transition: all 200ms ease; + border-radius: 20px; + } + + &--active { + animation-name: box-in; + } +} + +.modal-inner-container { + position: relative; + height: var(--height); + transition: 0.2s ease height; + > * { + pointer-events: all; + } +} + +.modal-external-content { + margin: 0 auto; + width: fit-content; + padding: 24px; + backface-visibility: hidden; + max-width: 100%; +} + +.modal-content-wrapper { + border-radius: 1.5rem; + background-color: #fff; + overflow: hidden; + filter: drop-shadow(0px 4px 30px rgba(0, 0, 0, 0.4)); +} + +.modal-content-header { + position: relative; + height: 64px; +} + +.modal-close-button-wrapper { + z-index: 3; + position: absolute; + top: 50%; + right: 0; + transform: translate(-50%, -50%); + width: 32px; + height: 32px; +} + +.modal-close-button { + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + padding: 0; + margin: 0; + border-radius: 50%; + border: 1px solid #000; + background: none; + + &:active { + transform: scale(0.9); + } +} + +@keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes box-in { + from { + opacity: 0; + transform: scale(0.97); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes box-out { + from { + opacity: 1; + transform: scale(1); + } + to { + opacity: 0; + transform: scale(0.97); + } +} diff --git a/agent-widget/src/react/modal/modal.tsx b/agent-widget/src/react/modal/modal.tsx new file mode 100644 index 0000000..2d840b0 --- /dev/null +++ b/agent-widget/src/react/modal/modal.tsx @@ -0,0 +1,151 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; + +import './modal.scss'; + +type PortalProps = React.PropsWithChildren<{ selector?: string }>; + +const Portal: React.FC = ({ + selector = '__MODAL__', + children, +}) => { + const ref = useRef(null); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + const selectorPrefixed = `#${selector.replace(/^#/, '')}`; + ref.current = document.querySelector(selectorPrefixed); + + if (!ref.current) { + const div = document.createElement('div'); + div.setAttribute('id', selector); + document.body.append(div); + ref.current = div; + } + + setMounted(true); + }, [selector]); + + if (!ref.current) { + return null; + } + + return mounted ? createPortal(children, ref.current) : null; +}; + +type ModalProps = React.PropsWithChildren<{ + onCloseClick?: () => void; + position?: 'center' | 'top right'; +}>; +type RefElement = HTMLElement; +type RefType = RefElement | null; + +export const Modal: React.FC = ({ onCloseClick, children }) => { + const ref = useRef(null); + const overlayRef = useRef(null); + const [isOpen, setIsOpen] = useState(false); + + const [dimensions, setDimensions] = useState<{ + width: string | undefined; + height: string | undefined; + }>({ + width: undefined, + height: undefined, + }); + + const cssDimensions = { + '--height': dimensions.height, + '--width': dimensions.width, + } as React.CSSProperties; + + const contentRef = useCallback((node: RefElement) => { + if (!node) { + return; + } + + ref.current = node; + updateBounds(node); + setIsOpen(true); + }, []); + + useEffect(() => { + if (ref.current && isOpen) { + updateBounds(ref.current); + } + }, [isOpen, children]); + + const handleOverlayClick = useCallback(() => { + if (overlayRef.current?.contains(ref.current)) { + return; + } + + onCloseClick?.(); + }, []); + + const updateBounds = (node: RefElement) => { + const bounds = { + width: node?.offsetWidth, + height: node?.offsetHeight, + }; + setDimensions({ + width: `${bounds?.width}px`, + height: `${bounds?.height}px`, + }); + }; + + return ( + +
+
} + className="modal-overlay" + onClick={handleOverlayClick} + aria-hidden="true" + /> +
+
+
+
+
void} + > +
+
+
+ +
+
+ {children} +
+
+
+
+
+
+ + ); +}; diff --git a/agent-widget/yarn.lock b/agent-widget/yarn.lock index 3242a2a..fff726c 100644 --- a/agent-widget/yarn.lock +++ b/agent-widget/yarn.lock @@ -1474,6 +1474,11 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== +"@remix-run/router@1.15.3": + version "1.15.3" + resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.15.3.tgz#d2509048d69dbb72d5389a14945339f1430b2d3c" + integrity sha512-Oy8rmScVrVxWZVOpEF57ovlnhpZ8CCPlnIIumVcV9nFdiSIrus99+Lw78ekXyGvVDlIsFJbSfmSovJUhCWYV3w== + "@rushstack/eslint-patch@^1.3.3": version "1.8.0" resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.8.0.tgz#c5545e6a5d2bd5c26b4021c357177a28698c950e" @@ -1679,6 +1684,11 @@ "@types/minimatch" "*" "@types/node" "*" +"@types/history@^4.7.11": + version "4.7.11" + resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64" + integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA== + "@types/html-minifier-terser@^6.0.0": version "6.1.0" resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#4fc33a00c1d0c16987b1a20cf92d20614c55ac35" @@ -1772,6 +1782,23 @@ dependencies: "@types/react" "*" +"@types/react-router-dom@^5.3.3": + version "5.3.3" + resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz#e9d6b4a66fcdbd651a5f106c2656a30088cc1e83" + integrity sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw== + dependencies: + "@types/history" "^4.7.11" + "@types/react" "*" + "@types/react-router" "*" + +"@types/react-router@*": + version "5.1.20" + resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.20.tgz#88eccaa122a82405ef3efbcaaa5dcdd9f021387c" + integrity sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q== + dependencies: + "@types/history" "^4.7.11" + "@types/react" "*" + "@types/react@*", "@types/react@^18.2.72": version "18.2.72" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.72.tgz#3341a6d0746d1c7d8510810319323850c04bd6ed" @@ -5903,6 +5930,21 @@ react-is@^16.13.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-router-dom@^6.22.3: + version "6.22.3" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.22.3.tgz#9781415667fd1361a475146c5826d9f16752a691" + integrity sha512-7ZILI7HjcE+p31oQvwbokjk6OA/bnFxrhJ19n82Ex9Ph8fNAq+Hm/7KchpMGlTgWhUxRHMMCut+vEtNpWpowKw== + dependencies: + "@remix-run/router" "1.15.3" + react-router "6.22.3" + +react-router@6.22.3: + version "6.22.3" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.22.3.tgz#9d9142f35e08be08c736a2082db5f0c9540a885e" + integrity sha512-dr2eb3Mj5zK2YISHK++foM9w4eBnO23eKnZEDs7c880P6oKbrjz/Svg9+nxqtHQK+oMW4OtjZca0RqPglXxguQ== + dependencies: + "@remix-run/router" "1.15.3" + react@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"