mirror of
https://github.com/hexastack/hexabot
synced 2025-06-26 18:27:28 +00:00
feat: highlight triggered or errored block (frontend)
This commit is contained in:
parent
ae95de5d1a
commit
b3845b5f67
@ -8,11 +8,12 @@
|
||||
|
||||
import { debounce } from "@mui/material";
|
||||
import createEngine, { DiagramModel } from "@projectstorm/react-diagrams";
|
||||
import { useRouter } from "next/router";
|
||||
import * as React from "react";
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
import { useCreate } from "@/hooks/crud/useCreate";
|
||||
import { EntityType } from "@/services/types";
|
||||
import { EntityType, RouterType } from "@/services/types";
|
||||
import { IBlock } from "@/types/block.types";
|
||||
import {
|
||||
BlockPorts,
|
||||
@ -20,6 +21,7 @@ import {
|
||||
IVisualEditorContext,
|
||||
VisualEditorContextProps,
|
||||
} from "@/types/visual-editor.types";
|
||||
import { useSubscribe } from "@/websocket/socket-hooks";
|
||||
|
||||
import { ZOOM_LEVEL } from "../constants";
|
||||
import { AdvancedLinkFactory } from "../v2/AdvancedLink/AdvancedLinkFactory";
|
||||
@ -42,6 +44,8 @@ const addNode = (block: IBlock) => {
|
||||
patterns: (block?.patterns || [""]) as any,
|
||||
message: (block?.message || [""]) as any,
|
||||
starts_conversation: !!block?.starts_conversation,
|
||||
_hasErrored: false,
|
||||
_isHighlighted: false,
|
||||
});
|
||||
|
||||
node.setPosition(block.position.x, block.position.y);
|
||||
@ -252,6 +256,7 @@ const VisualEditorProvider: React.FC<VisualEditorContextProps> = ({
|
||||
children,
|
||||
}) => {
|
||||
const [selectedCategoryId, setSelectedCategoryId] = React.useState("");
|
||||
const router = useRouter();
|
||||
const { mutate: createBlock } = useCreate(EntityType.BLOCK);
|
||||
const createNode = (payload: any) => {
|
||||
payload.position = payload.position || getCentroid();
|
||||
@ -267,6 +272,130 @@ const VisualEditorProvider: React.FC<VisualEditorContextProps> = ({
|
||||
});
|
||||
};
|
||||
|
||||
async function removeHighlights() {
|
||||
return new Promise((resolve) => {
|
||||
if (!engine) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nodes = engine.getModel().getNodes() as NodeModel[];
|
||||
|
||||
nodes.forEach((node) => {
|
||||
if (node.isHighlighted()) {
|
||||
node.setHighlighted(false);
|
||||
node.setSelected(false);
|
||||
}
|
||||
if (node.hasErrored()) {
|
||||
node.setHasErrored(false);
|
||||
}
|
||||
});
|
||||
|
||||
engine.repaintCanvas();
|
||||
resolve(true);
|
||||
});
|
||||
}
|
||||
function isBlockVisibleOnCanvas(block: NodeModel): boolean {
|
||||
const zoom = engine.getModel().getZoomLevel() / 100;
|
||||
const canvas = engine.getCanvas();
|
||||
const canvasRect = canvas?.getBoundingClientRect();
|
||||
|
||||
if (!canvasRect) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const offsetX = engine.getModel().getOffsetX();
|
||||
const offsetY = engine.getModel().getOffsetY();
|
||||
const blockX = block.getX() * zoom + offsetX;
|
||||
const blockY = block.getY() * zoom + offsetY;
|
||||
const blockScreenX = blockX + (BLOCK_WIDTH * zoom) / 2;
|
||||
const blockScreenY = blockY + (BLOCK_HEIGHT * zoom) / 2;
|
||||
|
||||
return (
|
||||
blockScreenX > 0 &&
|
||||
blockScreenX < canvasRect.width &&
|
||||
blockScreenY > 0 &&
|
||||
blockScreenY < canvasRect.height
|
||||
);
|
||||
}
|
||||
|
||||
function centerBlockInView(block: NodeModel) {
|
||||
const zoom = engine.getModel().getZoomLevel() / 100;
|
||||
const canvasRect = engine.getCanvas()?.getBoundingClientRect();
|
||||
|
||||
if (!canvasRect) {
|
||||
return;
|
||||
}
|
||||
|
||||
const centerX = canvasRect.width / 2;
|
||||
const centerY = canvasRect.height / 2;
|
||||
const offsetX = centerX - (block.getX() + BLOCK_WIDTH / 2) * zoom;
|
||||
const offsetY = centerY - (block.getY() + BLOCK_HEIGHT / 2) * zoom;
|
||||
|
||||
engine.getModel().setOffset(offsetX, offsetY);
|
||||
}
|
||||
|
||||
async function redirectToTriggeredFlow(
|
||||
currentFlow: string,
|
||||
triggeredFlow: string,
|
||||
) {
|
||||
if (currentFlow !== triggeredFlow) {
|
||||
setSelectedCategoryId(triggeredFlow);
|
||||
}
|
||||
|
||||
const triggeredFlowUrl = `/${RouterType.VISUAL_EDITOR}/flows/${triggeredFlow}`;
|
||||
|
||||
if (window.location.href !== triggeredFlowUrl) {
|
||||
await router.push(triggeredFlow);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleHighlightFlow(payload: any) {
|
||||
await removeHighlights();
|
||||
|
||||
await redirectToTriggeredFlow(selectedCategoryId, payload.flowId);
|
||||
|
||||
setTimeout(() => {
|
||||
const block = engine?.getModel().getNode(payload.blockId) as NodeModel;
|
||||
|
||||
if (!block) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isBlockVisibleOnCanvas(block)) {
|
||||
centerBlockInView(block);
|
||||
}
|
||||
|
||||
block.setSelected(true);
|
||||
block.setHighlighted(true);
|
||||
|
||||
engine.repaintCanvas();
|
||||
}, 200);
|
||||
}
|
||||
|
||||
async function handleHighlightErroredBlock(payload: any) {
|
||||
await removeHighlights();
|
||||
|
||||
await redirectToTriggeredFlow(selectedCategoryId, payload.flowId);
|
||||
setTimeout(() => {
|
||||
const block = engine?.getModel().getNode(payload.blockId) as NodeModel;
|
||||
|
||||
if (!block) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isBlockVisibleOnCanvas(block)) {
|
||||
centerBlockInView(block);
|
||||
}
|
||||
|
||||
block.setHasErrored(true);
|
||||
|
||||
engine.repaintCanvas();
|
||||
}, 220);
|
||||
}
|
||||
useSubscribe("highlight:flow", handleHighlightFlow);
|
||||
|
||||
useSubscribe("highlight:error", handleHighlightErroredBlock);
|
||||
|
||||
return (
|
||||
<VisualEditorContext.Provider
|
||||
value={{
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@ -8,8 +8,8 @@
|
||||
|
||||
import { BaseModelOptions } from "@projectstorm/react-canvas-core";
|
||||
import {
|
||||
DefaultNodeModel as StormNodeModel,
|
||||
DefaultPortModel,
|
||||
DefaultNodeModel as StormNodeModel,
|
||||
} from "@projectstorm/react-diagrams";
|
||||
|
||||
import { BlockPorts } from "@/types/visual-editor.types";
|
||||
@ -24,6 +24,8 @@ export interface NodeModelOptions extends BaseModelOptions {
|
||||
patterns?: string[];
|
||||
message?: string[];
|
||||
starts_conversation?: boolean;
|
||||
_isHighlighted: boolean;
|
||||
_hasErrored: boolean;
|
||||
}
|
||||
|
||||
export class NodeModel extends StormNodeModel {
|
||||
@ -36,8 +38,12 @@ export class NodeModel extends StormNodeModel {
|
||||
patterns: string[];
|
||||
message: string[];
|
||||
starts_conversation?: boolean;
|
||||
_isHighlighted: boolean = false;
|
||||
_hasErrored: boolean = false;
|
||||
|
||||
constructor(options: NodeModelOptions = {}) {
|
||||
constructor(
|
||||
options: NodeModelOptions = { _isHighlighted: false, _hasErrored: false },
|
||||
) {
|
||||
super({
|
||||
...options,
|
||||
type: "ts-custom-node",
|
||||
@ -74,6 +80,21 @@ export class NodeModel extends StormNodeModel {
|
||||
}),
|
||||
);
|
||||
}
|
||||
setHighlighted(isHighlighted: boolean) {
|
||||
this._isHighlighted = isHighlighted;
|
||||
}
|
||||
|
||||
isHighlighted(): boolean {
|
||||
return this._isHighlighted;
|
||||
}
|
||||
|
||||
setHasErrored(hasErrored: boolean) {
|
||||
this._hasErrored = hasErrored;
|
||||
this.fireEvent({ hasErrored }, "stateChanged");
|
||||
}
|
||||
hasErrored(): boolean {
|
||||
return this._hasErrored;
|
||||
}
|
||||
|
||||
serialize() {
|
||||
return {
|
||||
|
@ -14,6 +14,7 @@ import MenuRoundedIcon from "@mui/icons-material/MenuRounded";
|
||||
import PlayArrowRoundedIcon from "@mui/icons-material/PlayArrowRounded";
|
||||
import ReplyIcon from "@mui/icons-material/Reply";
|
||||
import { Chip, styled } from "@mui/material";
|
||||
import { ListenerHandle } from "@projectstorm/react-canvas-core";
|
||||
import { DiagramEngine, PortWidget } from "@projectstorm/react-diagrams-core";
|
||||
import clsx from "clsx";
|
||||
import * as React from "react";
|
||||
@ -242,6 +243,7 @@ class NodeWidget extends React.Component<
|
||||
NodeWidgetProps & WithTranslation,
|
||||
NodeWidgetState
|
||||
> {
|
||||
listener: ListenerHandle | undefined;
|
||||
config: {
|
||||
type: TBlock;
|
||||
color: string;
|
||||
@ -253,15 +255,30 @@ class NodeWidget extends React.Component<
|
||||
this.config = getBlockConfig(this.props.node.message as any);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.listener = this.props.node.registerListener({
|
||||
stateChanged: () => this.forceUpdate(),
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.listener) {
|
||||
this.listener.deregister();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { t, i18n, tReady } = this.props;
|
||||
const selectedStyling = clsx(
|
||||
"custom-node",
|
||||
this.props.node.isSelected() ? "selected" : "",
|
||||
this.props.node.isHighlighted() ? "high-lighted" : "",
|
||||
this.props.node.hasErrored() ? "high-light-error" : "",
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"custom-node",
|
||||
this.props.node.isSelected() ? "selected" : "",
|
||||
)}
|
||||
className={selectedStyling}
|
||||
style={{
|
||||
border: `1px solid ${this.config.color}`,
|
||||
}}
|
||||
|
@ -34,7 +34,7 @@
|
||||
}
|
||||
|
||||
.start-point {
|
||||
color: #FFF;
|
||||
color: #fff;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
@ -183,7 +183,7 @@
|
||||
margin-top: 5px;
|
||||
border-radius: 100%;
|
||||
box-shadow: 0 0 8px #0003;
|
||||
transition: all .4s ease 0s;
|
||||
transition: all 0.4s ease 0s;
|
||||
}
|
||||
.circle-out-porters {
|
||||
position: absolute;
|
||||
@ -194,11 +194,11 @@
|
||||
.circle-porter-in {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%) scale(.75);
|
||||
transform: translateY(-50%) scale(0.75);
|
||||
left: -12px;
|
||||
}
|
||||
.circle-porter-out {
|
||||
transform: scale(.75);
|
||||
transform: scale(0.75);
|
||||
right: -12px;
|
||||
}
|
||||
|
||||
@ -211,3 +211,61 @@
|
||||
cursor: grab;
|
||||
transform: translateY(-50%) scale(1.1);
|
||||
}
|
||||
|
||||
.node:has(.high-lighted) {
|
||||
box-shadow: 0 0 14px 6px rgba(0, 153, 255, 0.6);
|
||||
z-index: 10;
|
||||
transform: scale(1.04);
|
||||
cursor: grab;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
border-radius: 10px;
|
||||
animation: highlightPulse 0.8s ease-out infinite;
|
||||
}
|
||||
|
||||
@keyframes highlightPulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 rgba(0, 153, 255, 0.2);
|
||||
transform: scale(1);
|
||||
}
|
||||
20% {
|
||||
box-shadow: 0 0 30px 12px rgba(0, 153, 255, 1);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
60% {
|
||||
box-shadow: 0 0 20px 6px rgba(0, 153, 255, 0.7);
|
||||
transform: scale(1.06);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 10px 4px rgba(0, 153, 255, 0.6);
|
||||
transform: scale(1.04);
|
||||
}
|
||||
}
|
||||
|
||||
.node:has(.high-light-error) {
|
||||
box-shadow: 0 0 14px 6px rgba(255, 0, 0, 0.6);
|
||||
z-index: 10;
|
||||
transform: scale(1.04);
|
||||
cursor: grab;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
border-radius: 10px;
|
||||
animation: highlightPulseError 0.8s ease-out infinite;
|
||||
}
|
||||
|
||||
@keyframes highlightPulseError {
|
||||
0% {
|
||||
box-shadow: 0 0 0 rgba(255, 0, 0, 0.2);
|
||||
transform: scale(1);
|
||||
}
|
||||
20% {
|
||||
box-shadow: 0 0 30px 12px rgba(255, 0, 0, 1);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
60% {
|
||||
box-shadow: 0 0 20px 6px rgba(255, 0, 0, 0.7);
|
||||
transform: scale(1.06);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 10px 4px rgba(255, 0, 0, 0.6);
|
||||
transform: scale(1.04);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@ -41,6 +41,7 @@ export const SocketProvider = (props: PropsWithChildren) => {
|
||||
const [connected, setConnected] = useState(false);
|
||||
const { toast } = useToast();
|
||||
const { user } = useAuth();
|
||||
// todo: fix we aren't sending auth token
|
||||
const socket = useMemo(() => new SocketIoClient(apiUrl), [apiUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
|
Loading…
Reference in New Issue
Block a user