Skip to content

Commit

Permalink
feature: custom agent widget
Browse files Browse the repository at this point in the history
  • Loading branch information
metabacalhau committed Apr 19, 2024
1 parent 122cb12 commit 67d3898
Show file tree
Hide file tree
Showing 8 changed files with 478 additions and 25 deletions.
6 changes: 4 additions & 2 deletions agent-widget/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "nvm-agent-demo",
"version": "0.0.2",
"version": "0.0.3",
"description": "",
"main": "index.js",
"scripts": {
Expand Down Expand Up @@ -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"
}
}
Binary file added agent-widget/src/react/.DS_Store
Binary file not shown.
37 changes: 37 additions & 0 deletions agent-widget/src/react/agent.scss
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ body {

.widget-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}

.chat-panel {
Expand Down Expand Up @@ -144,3 +146,38 @@ ul {
.tooltip:hover .tooltip-text {
visibility: visible;
}

.toggle {
align-items: center;
background: none;
border: none;
width: auto;
margin: 2rem 1rem 2rem;
cursor: pointer;
text-decoration: underline;
font-size: 16px;

&:hover {
text-decoration: none;
}
}

.textarea-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
padding: 1rem 1.5rem 2.5rem;

textarea {
width: 400px;
max-width: 100%;
height: 150px;
}

button {
&:disabled {
cursor: not-allowed;
}
}
}
103 changes: 81 additions & 22 deletions agent-widget/src/react/agent.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import './agent.scss';
import { Modal } from './modal/modal';
import { useParams } from 'react-router-dom';
import { useSearchParams } from '../../node_modules/react-router-dom/dist/index';

const insertWidgetScriptBefore = (
srcFile: string,
insertBeforeElement: HTMLElement
) => {
const selector = `script[src='${srcFile}']`;
const InjectScript = React.memo(({ script }: { script: string }) => {
const divRef = useRef<HTMLDivElement | null>(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 <div ref={divRef} />;
});

export const Agent = () => {
const [searchParams, setSearchParams] = useSearchParams();

const [thread, setThread] = useState<AssistantThread[]>([]);

const [query, setQuery] = useState('');
Expand All @@ -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<HTMLTextAreaElement>(null);

const threadEndRef = useRef<HTMLDivElement>(null);

const loadWidget = () => {
setAgentData(null);

setMustTopUp(false);

setShowModal(false);

setThread([]);

setSearchParams({ html: encodeURIComponent(textareaWidgetHtmlCode) });
};

const resetQuery = () => {
setQuery('');
messageRef.current?.focus();
Expand Down Expand Up @@ -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 (
<div className="agent-container">
<div className="widget-container">
<div
className="nvm-agent-widget"
nvm-did="did:nv:8797caa1baf332316e7d76d53c37619870672062df9727e94a6c714df821f5cf"
nvm-layout="horizontal"
/>
{showModal && (
<Modal
onCloseClick={() => {
setShowModal(false);
}}
>
<div className="textarea-content">
<textarea
value={textareaWidgetHtmlCode}
onChange={(e) =>
setTextareaWidgetHtmlCode(e.currentTarget.value)
}
tabIndex={0}
autoFocus
></textarea>
<button
type="submit"
onClick={() => {
loadWidget();
}}
disabled={!textareaWidgetHtmlCode}
>
Save
</button>
</div>
</Modal>
)}
<button
type="submit"
className="toggle"
onClick={() => setShowModal((prev) => !prev)}
>
Add a widget HTML code
</button>
<InjectScript script={widgetHtmlCode} />
</div>
<div className="chat-panel">
<div className="thread-container">
Expand Down
7 changes: 6 additions & 1 deletion agent-widget/src/react/index.tsx
Original file line number Diff line number Diff line change
@@ -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(<Agent />);
createRoot(document.getElementById('root')!).render(
<BrowserRouter>
<Agent />
</BrowserRouter>
);
157 changes: 157 additions & 0 deletions agent-widget/src/react/modal/modal.scss
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading

0 comments on commit 67d3898

Please sign in to comment.