UI Improvements: tooltip, classes icons e icons (#404)

This commit is contained in:
Gabriel Luiz Freitas Almeida 2023-05-30 11:14:05 -03:00 • committed by GitHub
commit 188ae61c79
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 223 additions and 71 deletions

View file

@ -32,6 +32,7 @@
"react-router-dom": "^6.8.1", "react-router-dom": "^6.8.1",
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",
"react-tabs": "^6.0.0", "react-tabs": "^6.0.0",
"react-tooltip": "^5.13.1",
"reactflow": "^11.5.5", "reactflow": "^11.5.5",
"rehype-mathjax": "^4.0.2", "rehype-mathjax": "^4.0.2",
"remark-gfm": "^3.0.1", "remark-gfm": "^3.0.1",
@ -897,6 +898,19 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/@floating-ui/core": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.2.6.tgz",
"integrity": "sha512-EvYTiXet5XqweYGClEmpu3BoxmsQ4hkj3QaYA6qEnigCWffTP3vNRwBReTdrwDwo7OoJ3wM8Uoe9Uk4n+d4hfg=="
},
"node_modules/@floating-ui/dom": {
"version": "1.2.9",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.2.9.tgz",
"integrity": "sha512-sosQxsqgxMNkV3C+3UqTS6LxP7isRLwX8WMepp843Rb3/b0Wz8+MdUkxJksByip3C2WwLugLHN1b4ibn//zKwQ==",
"dependencies": {
"@floating-ui/core": "^1.2.6"
}
},
"node_modules/@headlessui/react": { "node_modules/@headlessui/react": {
"version": "1.7.10", "version": "1.7.10",
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.10.tgz", "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.10.tgz",
@ -2938,6 +2952,11 @@
"resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.4.tgz", "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.4.tgz",
"integrity": "sha512-sbpkOw6z413p+HDGcBENe498WM9woqWHiJxCq7nvmxe9WmrUmqfAcxpIwAiMtM5Q3AhYkzXcNQHqsWq0mND51g==" "integrity": "sha512-sbpkOw6z413p+HDGcBENe498WM9woqWHiJxCq7nvmxe9WmrUmqfAcxpIwAiMtM5Q3AhYkzXcNQHqsWq0mND51g=="
}, },
"node_modules/classnames": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz",
"integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="
},
"node_modules/client-only": { "node_modules/client-only": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
@ -6265,6 +6284,19 @@
"react": "^18.0.0" "react": "^18.0.0"
} }
}, },
"node_modules/react-tooltip": {
"version": "5.13.1",
"resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.13.1.tgz",
"integrity": "sha512-9NstDFdjyy6cIH9zjeT70zXTHlW/TIGCOWQmhkAyqLFeQioLg1FXvb9ec7AxSpn0zyFUkFSLdFYxZRuewti3Aw==",
"dependencies": {
"@floating-ui/dom": "^1.0.0",
"classnames": "^2.3.0"
},
"peerDependencies": {
"react": ">=16.14.0",
"react-dom": ">=16.14.0"
}
},
"node_modules/react-transition-group": { "node_modules/react-transition-group": {
"version": "4.4.5", "version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",

View file

@ -27,6 +27,7 @@
"react-router-dom": "^6.8.1", "react-router-dom": "^6.8.1",
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",
"react-tabs": "^6.0.0", "react-tabs": "^6.0.0",
"react-tooltip": "^5.13.1",
"reactflow": "^11.5.5", "reactflow": "^11.5.5",
"rehype-mathjax": "^4.0.2", "rehype-mathjax": "^4.0.2",
"remark-gfm": "^3.0.1", "remark-gfm": "^3.0.1",

View file

@ -144,6 +144,7 @@ export default function ParameterComponent({
) : left === true && type === "float" ? ( ) : left === true && type === "float" ? (
<FloatComponent <FloatComponent
disabled={disabled} disabled={disabled}
disableCopyPaste={true}
value={data.node.template[name].value ?? ""} value={data.node.template[name].value ?? ""}
onChange={(t) => { onChange={(t) => {
data.node.template[name].value = t; data.node.template[name].value = t;
@ -184,6 +185,7 @@ export default function ParameterComponent({
) : left === true && type === "int" ? ( ) : left === true && type === "int" ? (
<IntComponent <IntComponent
disabled={disabled} disabled={disabled}
disableCopyPaste={true}
value={data.node.template[name].value ?? ""} value={data.node.template[name].value ?? ""}
onChange={(t) => { onChange={(t) => {
data.node.template[name].value = t; data.node.template[name].value = t;

View file

@ -28,6 +28,7 @@ import NodeModal from "../../modals/NodeModal";
import { useCallback } from "react"; import { useCallback } from "react";
import { TabsContext } from "../../contexts/tabsContext"; import { TabsContext } from "../../contexts/tabsContext";
import { debounce } from "../../utils"; import { debounce } from "../../utils";
import TooltipReact from "../../components/ReactTooltipComponent";
import Tooltip from "../../components/TooltipComponent"; import Tooltip from "../../components/TooltipComponent";
export default function GenericNode({ export default function GenericNode({
data, data,
@ -112,50 +113,15 @@ export default function GenericNode({
color: nodeColors[types[data.type]] ?? nodeColors.unknown, color: nodeColors[types[data.type]] ?? nodeColors.unknown,
}} }}
/> />
<Tooltip title={data.type} placement="top"> <div className="ml-2 truncate">
<div className="ml-2 truncate">{data.type}</div> <TooltipReact
</Tooltip> delayShow={1000}
<div> selector={`node-selector-${data.type}`}
<Tooltip htmlContent={data.type}
title={ position="top"
!validationStatus ? (
"Validating..."
) : (
<div className="max-h-96 overflow-auto">
{validationStatus.params.split("\n").map((line, index) => (
<div key={index}>{line}</div>
))}
</div>
)
}
> >
<div className="relative h-5 w-5"> <div className="ml-2 truncate">{data.type}</div>
<CheckCircleIcon </TooltipReact>
className={classNames(
validationStatus && validationStatus.valid
? "text-green-500 opacity-100"
: "animate-spin text-green-500 opacity-0",
"absolute w-5 transition-all duration-200 ease-in-out hover:text-gray-500 hover:dark:text-gray-300"
)}
/>
<ExclamationCircleIcon
className={classNames(
validationStatus && !validationStatus.valid
? "text-red-500 opacity-100"
: "animate-spin text-red-500 opacity-0",
"absolute w-5 transition-all duration-200 ease-in-out hover:text-gray-500 hover:dark:text-gray-600"
)}
/>
<EllipsisHorizontalCircleIcon
className={classNames(
!validationStatus
? "text-yellow-500 opacity-100"
: "animate-spin text-yellow-500 opacity-0",
"absolute w-5 transition-all duration-300 ease-in-out hover:text-gray-500 hover:dark:text-gray-600"
)}
/>
</div>
</Tooltip>
</div> </div>
</div> </div>
<div className="flex gap-3"> <div className="flex gap-3">
@ -183,17 +149,61 @@ export default function GenericNode({
) )
? "" ? ""
: "hidden", : "hidden",
"h-6 w-6 hover:animate-spin dark:text-gray-300" "w-5 h-5 dark:text-gray-300"
)} )}
></Cog6ToothIcon> ></Cog6ToothIcon>
</button> </button>
<button <button
onClick={() => { onClick={() => {
deleteNode(data.id); deleteNode(data.id);
}} }}
> >
<TrashIcon className="h-6 w-6 hover:text-red-500 dark:text-gray-300 dark:hover:text-red-500"></TrashIcon> <TrashIcon className="w-5 h-5 dark:text-gray-300"></TrashIcon>
</button> </button>
<div>
<Tooltip
title={
!validationStatus ? (
"Validating..."
) : (
<div className="max-h-96 overflow-auto">
{validationStatus.params.split("\n").map((line, index) => (
<div key={index}>{line}</div>
))}
</div>
)
}
>
<div className="w-5 h-5 relative top-[3px]">
<div
className={classNames(
validationStatus && validationStatus.valid
? "w-4 h-4 rounded-full bg-green-500 opacity-100"
: "w-4 h-4 rounded-full bg-gray-500 opacity-0 hidden animate-spin",
"absolute w-4 hover:text-gray-500 hover:dark:text-gray-300 transition-all ease-in-out duration-200"
)}
></div>
<div
className={classNames(
validationStatus && !validationStatus.valid
? "w-4 h-4 rounded-full bg-yellow-500 opacity-100"
: "w-4 h-4 rounded-full bg-gray-500 opacity-0 hidden animate-spin",
"absolute w-4 hover:text-gray-500 hover:dark:text-gray-300 transition-all ease-in-out duration-200"
)}
></div>
<div
className={classNames(
!validationStatus
? "w-4 h-4 rounded-full bg-red-500 opacity-100"
: "w-4 h-4 rounded-full bg-gray-500 opacity-0 hidden animate-spin",
"absolute w-4 hover:text-gray-500 hover:dark:text-gray-300 transition-all ease-in-out duration-200"
)}
></div>
</div>
</Tooltip>
</div>
</div> </div>
</div> </div>

View file

@ -0,0 +1,54 @@
"use client";
import type { FC } from "react";
import React from "react";
import { Tooltip as ReactTooltip } from "react-tooltip";
import "react-tooltip/dist/react-tooltip.css";
import { classNames } from "../../utils";
type TooltipProps = {
selector: string;
content?: string;
disabled?: boolean;
htmlContent?: React.ReactNode;
className?: string; // This should use !impornant to override the default styles eg: '!bg-white'
position?: "top" | "right" | "bottom" | "left";
clickable?: boolean;
children: React.ReactNode;
delayShow?: number;
};
const TooltipReact: FC<TooltipProps> = ({
selector,
content,
disabled,
position = "top",
children,
htmlContent,
className,
clickable,
delayShow,
}) => {
return (
<div className="tooltip-container">
{React.cloneElement(children as React.ReactElement, {
"data-tooltip-id": selector,
})}
<ReactTooltip
id={selector}
content={content}
className={classNames(
"!bg-white !text-xs !font-normal !text-gray-700 !shadow-md !opacity-100 z-20",
className
)}
place={position}
clickable={clickable}
isOpen={disabled ? false : undefined}
delayShow={delayShow}
>
{htmlContent && htmlContent}
</ReactTooltip>
</div>
);
};
export default TooltipReact;

View file

@ -5,9 +5,12 @@ import { TabsContext } from "../../contexts/tabsContext";
export default function FloatComponent({ export default function FloatComponent({
value, value,
onChange, onChange,
disableCopyPaste = false,
disabled, disabled,
}: FloatComponentType) { }: FloatComponentType) {
const [myValue, setMyValue] = useState(value ?? ""); const [myValue, setMyValue] = useState(value ?? "");
const { setDisableCopyPaste } = useContext(TabsContext);
useEffect(() => { useEffect(() => {
if (disabled) { if (disabled) {
setMyValue(""); setMyValue("");
@ -15,8 +18,18 @@ export default function FloatComponent({
} }
}, [disabled, onChange]); }, [disabled, onChange]);
return ( return (
<div className={"w-full " + (disabled ? "pointer-events-none cursor-not-allowed" : "")}> <div
className={
"w-full " + (disabled ? "pointer-events-none cursor-not-allowed" : "")
}
>
<input <input
onFocus={() => {
if (disableCopyPaste) setDisableCopyPaste(true);
}}
onBlur={() => {
if (disableCopyPaste) setDisableCopyPaste(false);
}}
type="number" type="number"
value={myValue} value={myValue}
className={ className={

View file

@ -5,9 +5,12 @@ import { TabsContext } from "../../contexts/tabsContext";
export default function IntComponent({ export default function IntComponent({
value, value,
onChange, onChange,
disableCopyPaste = false,
disabled, disabled,
}: FloatComponentType) { }: FloatComponentType) {
const [myValue, setMyValue] = useState(value ?? ""); const [myValue, setMyValue] = useState(value ?? "");
const { setDisableCopyPaste } = useContext(TabsContext);
useEffect(() => { useEffect(() => {
if (disabled) { if (disabled) {
setMyValue(""); setMyValue("");
@ -16,18 +19,32 @@ export default function IntComponent({
}, [disabled, onChange]); }, [disabled, onChange]);
return ( return (
<div <div
className={"w-full " + ( className={
disabled ? "pointer-events-none cursor-not-allowed w-full" : "w-full" "w-full " +
)} (disabled ? "pointer-events-none cursor-not-allowed w-full" : "w-full")
}
> >
<input <input
onFocus={() => {
if (disableCopyPaste) setDisableCopyPaste(true);
}}
onBlur={() => {
if (disableCopyPaste) setDisableCopyPaste(false);
}}
onKeyDown={(event) => { onKeyDown={(event) => {
console.log(event);
if ( if (
event.key !== "Backspace" && event.key !== "Backspace" &&
event.key !== "Enter" && event.key !== "Enter" &&
event.key !== "Delete" && event.key !== "Delete" &&
event.key !== "ArrowLeft" && event.key !== "ArrowLeft" &&
event.key !== "ArrowRight" && event.key !== "ArrowRight" &&
event.key !== "Control" &&
event.key !== "Meta" &&
event.key !== "Shift" &&
event.key !== "c" &&
event.key !== "v" &&
event.key !== "a" &&
!/^[-]?\d*$/.test(event.key) !/^[-]?\d*$/.test(event.key)
) { ) {
event.preventDefault(); event.preventDefault();

View file

@ -96,7 +96,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
cookieObject.flows.forEach((flow) => { cookieObject.flows.forEach((flow) => {
flow.data.edges.forEach((edge) => { flow.data.edges.forEach((edge) => {
edge.className = ""; edge.className = "";
edge.style = { stroke: "#222222" }; edge.style = { stroke: "#555555" };
}); });
flow.data.nodes.forEach((node) => { flow.data.nodes.forEach((node) => {
if (Object.keys(templates[node.data.type]["template"]).length > 0) { if (Object.keys(templates[node.data.type]["template"]).length > 0) {
@ -285,7 +285,12 @@ export function TabsProvider({ children }: { children: ReactNode }) {
sourceHandle, sourceHandle,
targetHandle, targetHandle,
id, id,
className: "animate-pulse", style: { stroke: "inherit" },
className:
targetHandle.split("|")[0] === "Text"
? "stroke-gray-800 dark:stroke-gray-300"
: "stroke-gray-900 dark:stroke-gray-200",
animated: targetHandle.split("|")[0] === "Text",
selected: false, selected: false,
}, },
edges.map((e) => ({ ...e, selected: false })) edges.map((e) => ({ ...e, selected: false }))
@ -301,8 +306,12 @@ export function TabsProvider({ children }: { children: ReactNode }) {
const description = flow?.description ? flow.description : ""; const description = flow?.description ? flow.description : "";
if (data) { if (data) {
data.edges.forEach((edge) => { data.edges.forEach((edge) => {
edge.className = ""; edge.style = { stroke: "inherit" };
edge.style = { stroke: "#222222" }; edge.className =
edge.targetHandle.split("|")[0] === "Text"
? "stroke-gray-800 dark:stroke-gray-300"
: "stroke-gray-900 dark:stroke-gray-200";
edge.animated = edge.targetHandle.split("|")[0] === "Text";
}); });
data.nodes.forEach((node) => { data.nodes.forEach((node) => {
if (Object.keys(templates[node.data.type]["template"]).length > 0) { if (Object.keys(templates[node.data.type]["template"]).length > 0) {

View file

@ -151,13 +151,12 @@ export default function ChatMessage({
) : ( ) : (
<div className="w-full flex items-center"> <div className="w-full flex items-center">
<div className="text-start inline-block px-3 text-sm text-gray-600 dark:text-white"> <div className="text-start inline-block px-3 text-sm text-gray-600 dark:text-white">
<ReactMarkdown <span
remarkPlugins={[remarkGfm, remarkMath]} className="text-gray-600 dark:text-gray-200"
rehypePlugins={[rehypeMathjax]} dangerouslySetInnerHTML={{
className="markdown prose dark:prose-invert text-gray-600 dark:text-gray-200" __html: message.replace(/\n/g, "<br>"),
> }}
{message} ></span>
</ReactMarkdown>
</div> </div>
</div> </div>
)} )}

View file

@ -84,7 +84,7 @@ export default function ExportModal() {
as="h3" as="h3"
className="text-lg font-medium dark:text-white leading-10 text-gray-900" className="text-lg font-medium dark:text-white leading-10 text-gray-900"
> >
Export as Export
</Dialog.Title> </Dialog.Title>
</div> </div>
</div> </div>

View file

@ -124,7 +124,7 @@ export default function ImportModal() {
as="h3" as="h3"
className="text-lg font-medium dark:text-white leading-10 text-gray-900" className="text-lg font-medium dark:text-white leading-10 text-gray-900"
> >
{showExamples ? "Select an example" : "Import from"} {showExamples ? "Select an example" : "Import"}
</Dialog.Title> </Dialog.Title>
</div> </div>
</div> </div>

View file

@ -4,7 +4,7 @@ import { nodeColors, nodeIcons, nodeNames } from "../../../../utils";
import { useContext, useEffect, useState } from "react"; import { useContext, useEffect, useState } from "react";
import { typesContext } from "../../../../contexts/typesContext"; import { typesContext } from "../../../../contexts/typesContext";
import { APIClassType, APIObjectType } from "../../../../types/api"; import { APIClassType, APIObjectType } from "../../../../types/api";
import Tooltip from "../../../../components/TooltipComponent"; import TooltipReact from "../../../../components/ReactTooltipComponent";
export default function ExtraSidebar() { export default function ExtraSidebar() {
const { data } = useContext(typesContext); const { data } = useContext(typesContext);
@ -15,7 +15,9 @@ export default function ExtraSidebar() {
) { ) {
//start drag event //start drag event
var crt = event.currentTarget.cloneNode(true); var crt = event.currentTarget.cloneNode(true);
crt.style.position = "absolute"; crt.style.top = "-500px"; crt.style.right = "-500px"; crt.style.position = "absolute";
crt.style.top = "-500px";
crt.style.right = "-500px";
crt.classList.add("cursor-grabbing"); crt.classList.add("cursor-grabbing");
document.body.appendChild(crt); document.body.appendChild(crt);
event.dataTransfer.setDragImage(crt, 0, 0); event.dataTransfer.setDragImage(crt, 0, 0);
@ -38,8 +40,14 @@ export default function ExtraSidebar() {
{Object.keys(data[d]) {Object.keys(data[d])
.sort() .sort()
.map((t: string, k) => ( .map((t: string, k) => (
<Tooltip title={t.length > 21 ? t : ""} placement="right"> <TooltipReact
<div key={k}> selector={t}
htmlContent={t}
position="right"
delayShow={1500}
key={k}
>
<div key={k} data-tooltip-id={t}>
<div <div
draggable draggable
className={" cursor-grab border-l-8 rounded-l-md"} className={" cursor-grab border-l-8 rounded-l-md"}
@ -54,7 +62,9 @@ export default function ExtraSidebar() {
} }
onDragEnd={() => { onDragEnd={() => {
document.body.removeChild( document.body.removeChild(
document.getElementsByClassName("cursor-grabbing")[0] document.getElementsByClassName(
"cursor-grabbing"
)[0]
); );
}} }}
> >
@ -66,7 +76,7 @@ export default function ExtraSidebar() {
</div> </div>
</div> </div>
</div> </div>
</Tooltip> </TooltipReact>
))} ))}
{Object.keys(data[d]).length === 0 && ( {Object.keys(data[d]).length === 0 && (
<div className="text-gray-400 text-center">Coming soon</div> <div className="text-gray-400 text-center">Coming soon</div>

View file

@ -151,10 +151,11 @@ export default function FlowPage({ flow }: { flow: FlowType }) {
addEdge( addEdge(
{ {
...params, ...params,
style: style: { stroke: "inherit" },
className:
params.targetHandle.split("|")[0] === "Text" params.targetHandle.split("|")[0] === "Text"
? { stroke: "#333333", strokeWidth: 2 } ? "stroke-gray-800 dark:stroke-gray-300"
: { stroke: "#222222" }, : "stroke-gray-900 dark:stroke-gray-200",
animated: params.targetHandle.split("|")[0] === "Text", animated: params.targetHandle.split("|")[0] === "Text",
}, },
eds eds
@ -323,6 +324,7 @@ export default function FlowPage({ flow }: { flow: FlowType }) {
onNodesChange={onNodesChange} onNodesChange={onNodesChange}
onEdgesChange={onEdgesChangeMod} onEdgesChange={onEdgesChangeMod}
onConnect={onConnect} onConnect={onConnect}
disableKeyboardA11y={true}
onLoad={setReactFlowInstance} onLoad={setReactFlowInstance}
onInit={setReactFlowInstance} onInit={setReactFlowInstance}
nodeTypes={nodeTypes} nodeTypes={nodeTypes}
@ -339,6 +341,8 @@ export default function FlowPage({ flow }: { flow: FlowType }) {
onDrop={onDrop} onDrop={onDrop}
onNodesDelete={onDelete} onNodesDelete={onDelete}
onSelectionChange={onSelectionChange} onSelectionChange={onSelectionChange}
nodesDraggable={!disableCopyPaste}
panOnDrag={!disableCopyPaste}
selectNodesOnDrag={false} selectNodesOnDrag={false}
> >
<Background className="dark:bg-gray-900" /> <Background className="dark:bg-gray-900" />

View file

@ -69,6 +69,7 @@ export type DisclosureComponentType = {
export type FloatComponentType = { export type FloatComponentType = {
value: string; value: string;
disabled?: boolean; disabled?: boolean;
disableCopyPaste?: boolean;
onChange: (value: string) => void; onChange: (value: string) => void;
}; };