From 65d60888e4b290c9b1c3e46796586284c43a5be2 Mon Sep 17 00:00:00 2001 From: czhen <56986964+shczhen@users.noreply.github.com> Date: Tue, 31 Dec 2024 17:50:33 +0800 Subject: [PATCH 1/6] update react flow style --- .../ten_manager/designer_frontend/bun.lockb | Bin 172578 -> 172578 bytes .../components/Popup/CustomNodeConnPopup.tsx | 35 ++++ .../designer_frontend/src/flow/CustomEdge.tsx | 89 ++++++--- .../src/flow/CustomHandle.tsx | 6 +- .../designer_frontend/src/flow/CustomNode.tsx | 180 ++++++++++++++---- .../designer_frontend/src/flow/FlowCanvas.tsx | 79 +++++++- .../designer_frontend/src/flow/reactflow.css | 121 +++++++++++- 7 files changed, 430 insertions(+), 80 deletions(-) create mode 100644 core/src/ten_manager/designer_frontend/src/components/Popup/CustomNodeConnPopup.tsx diff --git a/core/src/ten_manager/designer_frontend/bun.lockb b/core/src/ten_manager/designer_frontend/bun.lockb index 0f2777509f20d2e0221f7fb6cce4cab6bc39520d..9b37abf0c900a64f2e32a0e14c48bb92dda0dffb 100755 GIT binary patch delta 646 zcmXBRT}abW7{~GRI9F~VxXPMygGx#xc5riU>naGsE~<%CX82l684XuWBIs>VRAdz0 zbn2?~CZpBKrWO&DL3o#Vxk; zD8S%4CUFZtc>Xee;un775B@@t3u<6PE$mo}P zWI2O-xPtSzfD8`cAP%7qhs{)8dVN{SN4ScM7{wTlqTe(YBxXNLdCnvX(qTVInJ!Kh z#9uEF^KwDDC2BU5#qH@-yUE<#<#9c}d98XWRVRZTI--N7r!1kSpo*uHiSY@gp6$<^ z$u-P;C*Q6E5p$n*37Aq@yxy=HOQw#DB-2XIm-NnH+iOk*I&~nV+s(JK+zspC|9{}( zrp5Gw()%5I-f$+W15u(=iHSJRP;Iq|hAzkQX_ByR*Wu#)qFfeRkSV(=@>xR0za*nJ Lvf`a3x$*HI)6$9p delta 646 zcmWmBUr1A77{~GVajx7#aFsRZ29;DsoWnLfVpl;Bc2P~FQp3N+l+ke2B!d1eii(WN z8{c+SdXv#=a|_P8$ePUS=pS8F*MWA?W!c~FE2%I=x6JM~QZGLp9Y zGxAcLO%A2D*qfiqgm|`BDYXq-u`Eh0=@(h)c70PsEbBl{I*bNZtb-RmG@>3G;KxR+ zhX-r*WKNm_r`Yf$#&80046x%M`t_Tf>@*Hi>cRnZB7#q}Kk$5yEcprY$mtz(5{XVy zzJt5Ci3v<0jcWYn3cv6Zf3SiY>MjVJaAOTtqY8hS`G-|x2bBIYCyCl2(lCZ74eH*! zG#azKK13SVa1ob~LNAWsFpi=}Pv@mO@Q9efbzDXtMlpipy1pP0=LO<<9V void; +} + +const CustomNodeConnPopup: React.FC = ({ + source, + target, + onClose, +}) => { + return ( + onClose?.()} + initialWidth={DEFAULT_WIDTH} + initialHeight={DEFAULT_HEIGHT} + resizable + > +
+

Source: {source}

+

Target: {target}

+
+
+ ); +}; + +export default CustomNodeConnPopup; diff --git a/core/src/ten_manager/designer_frontend/src/flow/CustomEdge.tsx b/core/src/ten_manager/designer_frontend/src/flow/CustomEdge.tsx index 0f5859a6ee..a8ea8c550f 100644 --- a/core/src/ten_manager/designer_frontend/src/flow/CustomEdge.tsx +++ b/core/src/ten_manager/designer_frontend/src/flow/CustomEdge.tsx @@ -4,7 +4,15 @@ // Licensed under the Apache License, Version 2.0, with certain conditions. // Refer to the "LICENSE" file in the root directory for more information. // -import { EdgeProps, getBezierPath, Edge } from "@xyflow/react"; +import { + type EdgeProps, + getBezierPath, + type Edge, + BaseEdge, + EdgeLabelRenderer, +} from "@xyflow/react"; +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/Button"; export type CustomEdgeType = Edge< { labelOffsetX: number; labelOffsetY: number }, @@ -22,45 +30,74 @@ export function CustomEdge({ id, style, label, + selected, }: EdgeProps) { const [edgePath, labelX, labelY] = getBezierPath({ sourceX, sourceY, + sourcePosition, targetX, targetY, - sourcePosition, targetPosition, }); - // Customize label position offset for left alignment. - // Adjust to move label closer to the left. - const labelOffsetX = data?.labelOffsetX || -50; - // Adjust for vertical positioning. - const labelOffsetY = data?.labelOffsetY || -10; - - // Compute position closer to the source point. - // 25% from source. - const leftLabelX = sourceX + (labelX - sourceX) * 0.25 + labelOffsetX; - const leftLabelY = sourceY + (labelY - sourceY) * 0.25 + labelOffsetY; + // const onEdgeClick = () => { + // console.log("onEdgeClick === ", id, data); + // }; return ( <> - - {label && ( - + {selected && ( + - {label} - + + )} + +
+
+
+
+ C:2 +
+
+ D:1 +
+
+ A:0 +
+
+ V:5 +
+
+
+
+
); } diff --git a/core/src/ten_manager/designer_frontend/src/flow/CustomHandle.tsx b/core/src/ten_manager/designer_frontend/src/flow/CustomHandle.tsx index a78bfe8a8e..0f511b746e 100644 --- a/core/src/ten_manager/designer_frontend/src/flow/CustomHandle.tsx +++ b/core/src/ten_manager/designer_frontend/src/flow/CustomHandle.tsx @@ -27,12 +27,12 @@ const CustomHandle: React.FC = ({ style = {}, }) => { return ( -
+
{/* Render the actual handle */} {/* Render the label */} -
= ({ }} > {label} -
+
*/}
); }; diff --git a/core/src/ten_manager/designer_frontend/src/flow/CustomNode.tsx b/core/src/ten_manager/designer_frontend/src/flow/CustomNode.tsx index 053083a8ce..a3bc2293c7 100644 --- a/core/src/ten_manager/designer_frontend/src/flow/CustomNode.tsx +++ b/core/src/ten_manager/designer_frontend/src/flow/CustomNode.tsx @@ -4,10 +4,18 @@ // Licensed under the Apache License, Version 2.0, with certain conditions. // Refer to the "LICENSE" file in the root directory for more information. // -import { memo } from "react"; +import * as React from "react"; import { Position, NodeProps, Connection, Edge, Node } from "@xyflow/react"; +import { + BlocksIcon as ExtensionIcon, + LogsIcon, + CableIcon, + InfoIcon, +} from "lucide-react"; -import CustomHandle from "./CustomHandle"; +import { cn } from "@/lib/utils"; + +import CustomHandle from "@/flow/CustomHandle"; const onConnect = (params: Connection | Edge) => console.log("Handle onConnect", params); @@ -25,45 +33,139 @@ export type CustomNodeType = Node< export function CustomNode({ data, isConnectable }: NodeProps) { return ( -
-
{data.name}
+ <> +
+
+
{ + console.log("clicked CableIcon === ", data); + if (typeof window !== "undefined") { + console.log("dispatching customNodeAction"); + window.dispatchEvent( + new CustomEvent("customNodeAction", { + detail: { action: "connections", source: data.name }, + }) + ); + } + }} + > + +
+
+ +
+
{ + console.log("clicked LogsIcon === ", data); + }} + > + +
+
+ +
+
{ + console.log("clicked InfoIcon === ", data); + }} + > + +
+
+
+ +
+
+
+
+ +
+
+
{data.name}
+
{data.addon}
+
+
- {/* Render source handles (for outgoing edges) */} - {data.sourceCmds.map((cmd, index) => { - return ( - - ); - })} + {/* Render target handles (for incoming edges) */} + {data.targetCmds.map((cmd, index) => ( + + ))} - {/* Render target handles (for incoming edges) */} - {data.targetCmds.map((cmd, index) => { - return ( - - ); - })} -
+ {/* Render source handles (for outgoing edges) */} + {data.sourceCmds.map((cmd, index) => ( + + ))} +
+
+ ); } -export default memo(CustomNode); +export default React.memo(CustomNode); diff --git a/core/src/ten_manager/designer_frontend/src/flow/FlowCanvas.tsx b/core/src/ten_manager/designer_frontend/src/flow/FlowCanvas.tsx index f21269c4a3..58dbbd0a96 100644 --- a/core/src/ten_manager/designer_frontend/src/flow/FlowCanvas.tsx +++ b/core/src/ten_manager/designer_frontend/src/flow/FlowCanvas.tsx @@ -27,6 +27,7 @@ import NodeContextMenu from "@/flow/ContextMenu/NodeContextMenu"; import EdgeContextMenu from "@/flow/ContextMenu/EdgeContextMenu"; import TerminalPopup, { TerminalData } from "@/components/Popup/TerminalPopup"; import EditorPopup, { EditorData } from "@/components/Popup/EditorPopup"; +import CustomNodeConnPopup from "@/components/Popup/CustomNodeConnPopup"; import { ThemeProviderContext } from "@/components/theme-context"; // Import react-flow style. @@ -62,6 +63,11 @@ const FlowCanvas = forwardRef( edge?: CustomEdgeType; node?: CustomNodeType; }>({ visible: false, x: 0, y: 0 }); + const [connPopup, setConnPopup] = useState<{ + visible: boolean; + source: string; + target?: string; + }>({ visible: false, source: "", target: "" }); const launchTerminal = (data: TerminalData) => { const newPopup = { id: `${data.title}-${Date.now()}`, data }; @@ -100,6 +106,15 @@ const FlowCanvas = forwardRef( setEditorPopups((prev) => prev.filter((popup) => popup.id !== id)); }; + const launchConnPopup = (source: string, target?: string) => { + console.log("launchConnPopup", source, target); + setConnPopup({ visible: true, source, target }); + }; + + const closeConnPopup = () => { + setConnPopup({ visible: false, source: "", target: "" }); + }; + const renderContextMenu = () => { if (contextMenu.type === "node" && contextMenu.node) { return ( @@ -167,8 +182,29 @@ const FlowCanvas = forwardRef( const handleClick = () => { closeContextMenu(); }; + const handleCustomNodeAction = (event: CustomEvent) => { + console.log("handleCustomNodeAction", event); + switch (event.detail.action) { + case "connections": + console.log("handleCustomNodeAction: launching conn popup"); + launchConnPopup(event.detail.source, event.detail.target); + break; + default: + break; + } + }; window.addEventListener("click", handleClick); - return () => window.removeEventListener("click", handleClick); + window.addEventListener( + "customNodeAction", + handleCustomNodeAction as EventListener + ); + return () => { + window.removeEventListener("click", handleClick); + window.removeEventListener( + "customNodeAction", + handleCustomNodeAction as EventListener + ); + }; }, [closeContextMenu]); const { theme } = useContext(ThemeProviderContext); @@ -197,9 +233,39 @@ const FlowCanvas = forwardRef( style={{ width: "100%", height: "100%" }} onNodeContextMenu={clickNodeContextMenu} onEdgeContextMenu={clickEdgeContextMenu} + onEdgeClick={(e, edge) => { + console.log("clicked", e, edge); + }} > - + + + + + + + + + + + + + {renderContextMenu()} @@ -219,6 +285,15 @@ const FlowCanvas = forwardRef( onClose={() => closeEditor(popup.id)} /> ))} + + {connPopup.visible && ( + + )} ); } diff --git a/core/src/ten_manager/designer_frontend/src/flow/reactflow.css b/core/src/ten_manager/designer_frontend/src/flow/reactflow.css index d11e31e06b..e5fee53e98 100644 --- a/core/src/ten_manager/designer_frontend/src/flow/reactflow.css +++ b/core/src/ten_manager/designer_frontend/src/flow/reactflow.css @@ -4,6 +4,10 @@ * Licensed under the Apache License, Version 2.0, with certain conditions. * Refer to the "LICENSE" file in the root directory for more information. */ +.react-flow__panel.react-flow__attribution { + background-color: transparent; +} + .react-flow { --xy-theme-selected: #f57dbd; --xy-theme-hover: #c5c5c5; @@ -46,7 +50,7 @@ 0px 0.51px 1.01px 0px rgba(255, 255, 255, 0.2); } -.react-flow__node { +/* .react-flow__node { box-shadow: var(--xy-node-boxshadow-default); border-radius: var(--xy-node-border-radius-default); background-color: var(--xy-node-background-color-default); @@ -59,18 +63,18 @@ flex-direction: column; border: var(--xy-node-border-default); color: var(--xy-node-color, var(--xy-node-color-default)); -} +} */ -.react-flow__node.selectable:focus { +/* .react-flow__node.selectable:focus { box-shadow: 0px 0px 0px 4px var(--xy-theme-color-focus); border-color: #d9d9d9; -} +} */ -.react-flow__node.selectable:focus:active { +/* .react-flow__node.selectable:focus:active { box-shadow: var(--xy-node-boxshadow-default); -} +} */ -.react-flow__node.selectable:hover, +/* .react-flow__node.selectable:hover, .react-flow__node.draggable:hover { border-color: var(--xy-theme-hover); } @@ -78,17 +82,17 @@ .react-flow__node.selectable.selected { border-color: var(--xy-theme-selected); box-shadow: var(--xy-node-boxshadow-default); -} +} */ .react-flow__node-group { background-color: rgba(207, 182, 255, 0.4); border-color: #9e86ed; } -.react-flow__edge.selectable:hover .react-flow__edge-path, +/* .react-flow__edge.selectable:hover .react-flow__edge-path, .react-flow__edge.selectable.selected .react-flow__edge-path { stroke: var(--xy-theme-edge-hover); -} +} */ .react-flow__handle { background-color: var(--xy-handle-background-color-default); @@ -118,3 +122,100 @@ width: 5px; height: 5px; } + +/* Custom Variables */ +.react-flow { + --bg-color: rgb(17, 17, 17); + --text-color: rgb(243, 244, 246); + /* --node-border-radius: 10px; */ + --node-box-shadow: 10px 0 15px rgba(42, 138, 246, 0.3), + -10px 0 15px rgba(233, 42, 103, 0.3); + /* background-color: var(--bg-color); + color: var(--text-color); */ +} + +/* Custom Styles */ +.react-flow__node-customNode { + @apply rounded-lg flex h-16 min-w-36 font-mono font-medium tracking-tight; + box-shadow: var(--node-box-shadow); +} + +.react-flow__node-customNode .wrapper { + @apply rounded-lg overflow-hidden flex p-0.5 relative flex-grow; +} + +.gradient:before { + content: ""; + position: absolute; + padding-bottom: calc(100% * 1.41421356237); + width: calc(100% * 1.41421356237); + background: conic-gradient( + from -160deg at 50% 50%, + #60a5fa 0deg, + /* Soft blue */ rgba(129, 140, 248, 0.9) 100deg, + /* Indigo */ rgba(192, 132, 252, 0.9) 220deg, + /* Purple */ rgba(96, 165, 250, 0.9) 320deg, + #60a5fa 360deg /* Back to soft blue */ + ); + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + border-radius: 100%; + filter: blur(2px); +} + +.react-flow__node-customNode.selected .wrapper.gradient:before { + content: ""; + background: conic-gradient( + from -160deg at 50% 50%, + rgba(96, 165, 250, 0.9) 0deg, + rgba(129, 140, 248, 0.8) 100deg, + rgba(192, 132, 252, 0.7) 220deg, + rgba(96, 165, 250, 0) 320deg + ); + animation: spinner 4s linear infinite; + transform: translate(-50%, -50%) rotate(0deg); + z-index: -1; + filter: blur(3px); +} + +@keyframes spinner { + 100% { + transform: translate(-50%, -50%) rotate(-360deg); + } +} + +.react-flow__handle { + opacity: 0; +} + +.react-flow__handle.source { + right: -10px; +} + +.react-flow__handle.target { + left: -10px; +} + +.react-flow__node:focus { + outline: none; +} + +.react-flow__edge .react-flow__edge-path { + stroke: url(#edge-gradient); + stroke-width: 2; + stroke-opacity: 0.75; +} + +.react-flow__edge.selected .react-flow__edge-path { + stroke: url(#edge-gradient); + stroke-width: 3; + stroke-opacity: 1; +} + +.react-flow__edge:hover .react-flow__edge-path { + stroke: url(#edge-gradient); + stroke-width: 3; + stroke-opacity: 0.85; + cursor: pointer; +} From e177142932aeebe8cd3cffb5f26bd14b9611cd89 Mon Sep 17 00:00:00 2001 From: czhen <56986964+shczhen@users.noreply.github.com> Date: Thu, 2 Jan 2025 18:04:36 +0800 Subject: [PATCH 2/6] feat: display connection data --- .../ten_manager/designer_frontend/bun.lockb | Bin 172578 -> 174125 bytes .../designer_frontend/package.json | 2 + .../ten_manager/designer_frontend/src/App.tsx | 103 +++++---- .../src/api/endpoints/graphs.ts | 39 ++++ .../components/DataTable/ConnectionTable.tsx | 211 ++++++++++++++++++ .../components/Popup/CustomNodeConnPopup.tsx | 133 ++++++++++- .../src/components/ui/table.tsx | 121 ++++++++++ .../src/components/ui/tabs.tsx | 54 +++++ .../src/context/ReactFlowDataContext.tsx | 71 ++++++ .../designer_frontend/src/flow/CustomEdge.tsx | 92 +++++--- .../src/flow/CustomHandle.tsx | 17 -- .../designer_frontend/src/flow/CustomNode.tsx | 50 +++-- .../designer_frontend/src/flow/FlowCanvas.tsx | 38 ++-- .../designer_frontend/src/flow/graph.ts | 194 +++++++++++++--- .../designer_frontend/src/types/graphs.ts | 30 ++- .../designer_frontend/src/utils/popup.ts | 13 ++ 16 files changed, 1003 insertions(+), 165 deletions(-) create mode 100644 core/src/ten_manager/designer_frontend/src/components/DataTable/ConnectionTable.tsx create mode 100644 core/src/ten_manager/designer_frontend/src/components/ui/table.tsx create mode 100644 core/src/ten_manager/designer_frontend/src/components/ui/tabs.tsx create mode 100644 core/src/ten_manager/designer_frontend/src/context/ReactFlowDataContext.tsx create mode 100644 core/src/ten_manager/designer_frontend/src/utils/popup.ts diff --git a/core/src/ten_manager/designer_frontend/bun.lockb b/core/src/ten_manager/designer_frontend/bun.lockb index 9b37abf0c900a64f2e32a0e14c48bb92dda0dffb..2c5f61bb0cc14b5b413fa21e6ec3a17978c8cae8 100755 GIT binary patch delta 29712 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 diff --git a/core/src/ten_manager/designer_frontend/package.json b/core/src/ten_manager/designer_frontend/package.json index ce11fd77ff..382da3cac2 100644 --- a/core/src/ten_manager/designer_frontend/package.json +++ b/core/src/ten_manager/designer_frontend/package.json @@ -15,6 +15,8 @@ "@radix-ui/react-dropdown-menu": "^2.1.4", "@radix-ui/react-navigation-menu": "^1.2.3", "@radix-ui/react-slot": "^1.1.1", + "@radix-ui/react-tabs": "^1.1.2", + "@tanstack/react-table": "^8.20.6", "@xterm/addon-attach": "^0.11.0", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-unicode11": "^0.8.0", diff --git a/core/src/ten_manager/designer_frontend/src/App.tsx b/core/src/ten_manager/designer_frontend/src/App.tsx index 2c2b20bece..8d3f9e32ef 100644 --- a/core/src/ten_manager/designer_frontend/src/App.tsx +++ b/core/src/ten_manager/designer_frontend/src/App.tsx @@ -35,6 +35,7 @@ import { } from "@/flow/graph"; import Popup from "@/components/Popup/Popup"; import type { IGraph } from "@/types/graphs"; +import { ReactFlowDataContext } from "@/context/ReactFlowDataContext"; const App: React.FC = () => { const [graphs, setGraphs] = useState([]); @@ -69,16 +70,30 @@ const App: React.FC = () => { let initialNodes: CustomNodeType[] = processNodes(backendNodes); - const { initialEdges, nodeSourceCmdMap, nodeTargetCmdMap } = - processConnections(backendConnections); + const { + initialEdges, + nodeSourceCmdMap, + nodeSourceDataMap, + nodeSourceAudioFrameMap, + nodeSourceVideoFrameMap, + nodeTargetCmdMap, + nodeTargetDataMap, + nodeTargetAudioFrameMap, + nodeTargetVideoFrameMap, + } = processConnections(backendConnections); // Write back the cmd information to nodes, so that CustomNode could // generate corresponding handles. - initialNodes = enhanceNodesWithCommands( - initialNodes, + initialNodes = enhanceNodesWithCommands(initialNodes, { nodeSourceCmdMap, - nodeTargetCmdMap - ); + nodeTargetCmdMap, + nodeSourceDataMap, + nodeTargetDataMap, + nodeSourceAudioFrameMap, + nodeSourceVideoFrameMap, + nodeTargetAudioFrameMap, + nodeTargetVideoFrameMap, + }); // Fetch additional addon information for each node. const nodesWithAddonInfo = await fetchAddonInfoForNodes(initialNodes); @@ -136,43 +151,45 @@ const App: React.FC = () => { return ( - - { - setEdges((eds) => addEdge(connection, eds)); - }} - /> - {showGraphSelection && ( - setShowGraphSelection(false)} - resizable={false} - initialWidth={400} - initialHeight={300} - onCollapseToggle={() => {}} - > -

    - {graphs.map((graph) => ( -
  • handleSelectGraph(graph.name)} - > - {graph.name} {graph.auto_start ? "(Auto Start)" : ""} -
  • - ))} -
- - )} + + + { + setEdges((eds) => addEdge(connection, eds)); + }} + /> + {showGraphSelection && ( + setShowGraphSelection(false)} + resizable={false} + initialWidth={400} + initialHeight={300} + onCollapseToggle={() => {}} + > +
    + {graphs.map((graph) => ( +
  • handleSelectGraph(graph.name)} + > + {graph.name} {graph.auto_start ? "(Auto Start)" : ""} +
  • + ))} +
+
+ )} +
); }; diff --git a/core/src/ten_manager/designer_frontend/src/api/endpoints/graphs.ts b/core/src/ten_manager/designer_frontend/src/api/endpoints/graphs.ts index 0b4ec5a67a..4f7e9fe3c8 100644 --- a/core/src/ten_manager/designer_frontend/src/api/endpoints/graphs.ts +++ b/core/src/ten_manager/designer_frontend/src/api/endpoints/graphs.ts @@ -53,6 +53,45 @@ export const ENDPOINT_GRAPHS = { }) ) .optional(), + data: z + .array( + z.object({ + name: z.string(), + dest: z.array( + z.object({ + app: z.string(), + extension: z.string(), + }) + ), + }) + ) + .optional(), + audio_frame: z + .array( + z.object({ + name: z.string(), + dest: z.array( + z.object({ + app: z.string(), + extension: z.string(), + }) + ), + }) + ) + .optional(), + video_frame: z + .array( + z.object({ + name: z.string(), + dest: z.array( + z.object({ + app: z.string(), + extension: z.string(), + }) + ), + }) + ) + .optional(), }) ) ), diff --git a/core/src/ten_manager/designer_frontend/src/components/DataTable/ConnectionTable.tsx b/core/src/ten_manager/designer_frontend/src/components/DataTable/ConnectionTable.tsx new file mode 100644 index 0000000000..31a37e8dd8 --- /dev/null +++ b/core/src/ten_manager/designer_frontend/src/components/DataTable/ConnectionTable.tsx @@ -0,0 +1,211 @@ +import { + ColumnDef, + flexRender, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table"; +import { BlocksIcon, ArrowBigRightDashIcon } from "lucide-react"; + +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Badge } from "@/components/ui/Badge"; +import { Button } from "@/components/ui/Button"; +import { cn } from "@/lib/utils"; +import { dispatchCustomNodeActionPopup } from "@/utils/popup"; + +import { EConnectionType } from "@/types/graphs"; + +export type TConnection = { + id: string; + source: string; + target: string; + type?: EConnectionType; +}; + +// eslint-disable-next-line react-refresh/only-export-components +export const commonConnectionColumns: ColumnDef[] = [ + { + accessorKey: "id", + header: () =>
No.
, + cell: ({ row }) => { + const index = row.index + 1; + return
{index}
; + }, + }, + { + accessorKey: "type", + header: "Type", + cell: ({ row }) => { + const type = row.getValue("type") as EConnectionType; + if (!type) return null; + return ; + }, + }, +]; + +// eslint-disable-next-line react-refresh/only-export-components +export const connectionColumns: ColumnDef[] = [ + ...commonConnectionColumns, + { + accessorKey: "source", + header: "Source", + }, + { + accessorKey: "target", + header: "Target", + }, +]; + +// eslint-disable-next-line react-refresh/only-export-components +export const extensionConnectionColumns1: ColumnDef[] = [ + ...commonConnectionColumns, + { + accessorKey: "downstream", + header: "Downstream", + cell: ({ row }) => { + const downstream = row.getValue("downstream") as string; + if (!downstream) return null; + return ( +
+ + + +
+ ); + }, + }, +]; + +// eslint-disable-next-line react-refresh/only-export-components +export const extensionConnectionColumns2: ColumnDef[] = [ + ...commonConnectionColumns, + { + accessorKey: "upstream", + header: "Upstream", + cell: ({ row }) => { + const upstream = row.getValue("upstream") as string; + if (!upstream) return null; + return ( +
+ + + +
+ ); + }, + }, +]; + +interface DataTableProps { + columns: ColumnDef[]; + data: TData[]; +} + +export function DataTable({ + columns, + data, + className, +}: DataTableProps & { className?: string }) { + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + }); + + return ( +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+ ); +} + +// eslint-disable-next-line react-refresh/only-export-components +export const connectionTypeBadgeStyle = { + [EConnectionType.CMD]: "bg-blue-100 text-blue-800 border-blue-200", + [EConnectionType.DATA]: "bg-green-100 text-green-800 border-green-200", + [EConnectionType.AUDIO_FRAME]: + "bg-purple-100 text-purple-800 border-purple-200", + [EConnectionType.VIDEO_FRAME]: "bg-red-100 text-red-800 border-red-200", +}; + +export function ConnectionTypeWithBadge({ + type, + className, +}: { + type: EConnectionType; + className?: string; +}) { + return ( + + {type} + + ); +} diff --git a/core/src/ten_manager/designer_frontend/src/components/Popup/CustomNodeConnPopup.tsx b/core/src/ten_manager/designer_frontend/src/components/Popup/CustomNodeConnPopup.tsx index c51843f0ac..ecf6b20f98 100644 --- a/core/src/ten_manager/designer_frontend/src/components/Popup/CustomNodeConnPopup.tsx +++ b/core/src/ten_manager/designer_frontend/src/components/Popup/CustomNodeConnPopup.tsx @@ -1,6 +1,18 @@ import * as React from "react"; +import { BlocksIcon, ArrowBigRightDashIcon } from "lucide-react"; + import Popup from "@/components/Popup/Popup"; +import { Button } from "@/components/ui/Button"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { ReactFlowDataContext } from "@/context/ReactFlowDataContext"; +import { + DataTable as ConnectionDataTable, + connectionColumns, + extensionConnectionColumns1, + extensionConnectionColumns2, +} from "@/components/DataTable/ConnectionTable"; +import { dispatchCustomNodeActionPopup } from "@/utils/popup"; const DEFAULT_WIDTH = 800; const DEFAULT_HEIGHT = 400; @@ -16,20 +28,133 @@ const CustomNodeConnPopup: React.FC = ({ target, onClose, }) => { + const titleMemo = React.useMemo(() => { + if (source && !target) { + return `Node[${source}] Connection`; + } + if (source && target) { + return `Connection Details`; + } + return "Connection"; + }, [source, target]); + return ( onClose?.()} initialWidth={DEFAULT_WIDTH} initialHeight={DEFAULT_HEIGHT} resizable > -
-

Source: {source}

-

Target: {target}

+
+ {source && target && ( + + )} + {source && !target && }
); }; export default CustomNodeConnPopup; + +function EdgeInfoContent(props: { source: string; target: string }) { + const { source, target } = props; + + const { edges } = React.useContext(ReactFlowDataContext); + + const [, rowsMemo] = React.useMemo(() => { + const relatedEdges = edges.filter( + (e) => e.source === source && e.target === target + ); + const rows = relatedEdges.map((e) => ({ + id: e.id, + type: e.data?.connectionType, + source: e.source, + target: e.target, + })); + return [relatedEdges, rows]; + }, [edges, source, target]); + + return ( + <> +
+ + + +
+ + + ); +} + +function CustomNodeConnPopupContent(props: { source: string }) { + const { source } = props; + + const [flowDirection, setFlowDirection] = React.useState< + "upstream" | "downstream" + >("upstream"); + + const { edges } = React.useContext(ReactFlowDataContext); + + const [rowsMemo] = React.useMemo(() => { + const relatedEdges = edges.filter((e) => + flowDirection === "upstream" ? e.target === source : e.source === source + ); + const rows = relatedEdges.map((e) => ({ + id: e.id, + type: e.data?.connectionType, + upstream: flowDirection === "upstream" ? e.source : e.target, + downstream: flowDirection === "upstream" ? e.source : e.target, + })); + return [rows, relatedEdges]; + }, [flowDirection, edges, source]); + + return ( + <> + + setFlowDirection(value as "upstream" | "downstream") + } + className="w-[400px]" + > + + Upstream + Downstream + + + ({ + ...row, + source: row.upstream, + target: row.downstream, + }))} + className="overflow-y-auto" + /> + + ); +} diff --git a/core/src/ten_manager/designer_frontend/src/components/ui/table.tsx b/core/src/ten_manager/designer_frontend/src/components/ui/table.tsx new file mode 100644 index 0000000000..0f714d296e --- /dev/null +++ b/core/src/ten_manager/designer_frontend/src/components/ui/table.tsx @@ -0,0 +1,121 @@ +/* eslint-disable max-len */ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+ + +)); +Table.displayName = "Table"; + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableHeader.displayName = "TableHeader"; + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableBody.displayName = "TableBody"; + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0", + className + )} + {...props} + /> +)); +TableFooter.displayName = "TableFooter"; + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableRow.displayName = "TableRow"; + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
[role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> +)); +TableHead.displayName = "TableHead"; + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + [role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> +)); +TableCell.displayName = "TableCell"; + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +TableCaption.displayName = "TableCaption"; + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +}; diff --git a/core/src/ten_manager/designer_frontend/src/components/ui/tabs.tsx b/core/src/ten_manager/designer_frontend/src/components/ui/tabs.tsx new file mode 100644 index 0000000000..988f2bdf3a --- /dev/null +++ b/core/src/ten_manager/designer_frontend/src/components/ui/tabs.tsx @@ -0,0 +1,54 @@ +/* eslint-disable max-len */ +import * as React from "react"; +import * as TabsPrimitive from "@radix-ui/react-tabs"; + +import { cn } from "@/lib/utils"; + +const Tabs = TabsPrimitive.Root; + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsList.displayName = TabsPrimitive.List.displayName; + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsContent.displayName = TabsPrimitive.Content.displayName; + +export { Tabs, TabsList, TabsTrigger, TabsContent }; diff --git a/core/src/ten_manager/designer_frontend/src/context/ReactFlowDataContext.tsx b/core/src/ten_manager/designer_frontend/src/context/ReactFlowDataContext.tsx new file mode 100644 index 0000000000..9f15a15538 --- /dev/null +++ b/core/src/ten_manager/designer_frontend/src/context/ReactFlowDataContext.tsx @@ -0,0 +1,71 @@ +import * as React from "react"; + +import type { CustomNodeType } from "@/flow/CustomNode"; +import type { CustomEdgeType } from "@/flow/CustomEdge"; +import { EConnectionType } from "@/types/graphs"; + +export type TReactFlowDataContext = { + nodes: CustomNodeType[]; + edges: CustomEdgeType[]; +}; + +export const ReactFlowDataContext = React.createContext({ + nodes: [], + edges: [], +}); + +// --- hooks --- +export const useCDAVInfoByEdgeId = (edgeId: string) => { + const { nodes, edges } = React.useContext(ReactFlowDataContext); + console.log("[useCDAVInfoByEdgeId] nodes === ", nodes); + console.log("[useCDAVInfoByEdgeId] edges === ", edges); + const edge = edges.find((e) => e.id === edgeId); + if (!edge) return null; + + console.log("[useCDAVInfoByEdgeId] edge === ", edge); + + const source = nodes.find((n) => n.id === edge.source); + const target = nodes.find((n) => n.id === edge.target); + if (!source || !target) return null; + + // Find all edges between the same source and target nodes + const relatedEdges = edges.filter( + (e) => e.source === edge.source && e.target === edge.target + ); + + // Count connections between source and target nodes + let cmdCount = 0; + let dataCount = 0; + let audioCount = 0; + let videoCount = 0; + + // Count all connection types from related edges + relatedEdges.forEach((e) => { + switch (e.data?.connectionType) { + case EConnectionType.CMD: + cmdCount++; + break; + case EConnectionType.DATA: + dataCount++; + break; + case EConnectionType.AUDIO_FRAME: + audioCount++; + break; + case EConnectionType.VIDEO_FRAME: + videoCount++; + break; + } + }); + + return { + source, + target, + relatedEdges, + connectionCounts: { + cmd: cmdCount, + data: dataCount, + audio: audioCount, + video: videoCount, + }, + }; +}; diff --git a/core/src/ten_manager/designer_frontend/src/flow/CustomEdge.tsx b/core/src/ten_manager/designer_frontend/src/flow/CustomEdge.tsx index a8ea8c550f..fd75afec9b 100644 --- a/core/src/ten_manager/designer_frontend/src/flow/CustomEdge.tsx +++ b/core/src/ten_manager/designer_frontend/src/flow/CustomEdge.tsx @@ -9,13 +9,21 @@ import { getBezierPath, type Edge, BaseEdge, - EdgeLabelRenderer, + getEdgeCenter, } from "@xyflow/react"; import { cn } from "@/lib/utils"; -import { Button } from "@/components/ui/Button"; + +import { useCDAVInfoByEdgeId } from "@/context/ReactFlowDataContext"; +import { dispatchCustomNodeActionPopup } from "@/utils/popup"; + +import { type EConnectionType } from "@/types/graphs"; export type CustomEdgeType = Edge< - { labelOffsetX: number; labelOffsetY: number }, + { + labelOffsetX: number; + labelOffsetY: number; + connectionType: EConnectionType; + }, "customEdge" >; @@ -26,13 +34,13 @@ export function CustomEdge({ targetY, sourcePosition, targetPosition, - data, id, style, - label, selected, + source, + target, }: EdgeProps) { - const [edgePath, labelX, labelY] = getBezierPath({ + const [edgePath] = getBezierPath({ sourceX, sourceY, sourcePosition, @@ -40,10 +48,19 @@ export function CustomEdge({ targetY, targetPosition, }); + const [edgeCenterX, edgeCenterY] = getEdgeCenter({ + sourceX, + sourceY, + targetX, + targetY, + }); + + const { connectionCounts } = useCDAVInfoByEdgeId(id) ?? {}; - // const onEdgeClick = () => { - // console.log("onEdgeClick === ", id, data); - // }; + const onEdgeClick = (e: React.MouseEvent) => { + e.stopPropagation(); + dispatchCustomNodeActionPopup("connections", source, target); + }; return ( <> @@ -66,38 +83,41 @@ export function CustomEdge({ /> )} - +
onEdgeClick(event)} > -
-
-
- C:2 -
-
- D:1 -
-
- A:0 -
-
- V:5 -
+
+
+ C: + {connectionCounts?.cmd ?? 0} +
+
+ D: + {connectionCounts?.data ?? 0} +
+
+ A: + {connectionCounts?.audio ?? 0} +
+
+ V: + {connectionCounts?.video ?? 0}
- + ); } diff --git a/core/src/ten_manager/designer_frontend/src/flow/CustomHandle.tsx b/core/src/ten_manager/designer_frontend/src/flow/CustomHandle.tsx index 0f511b746e..22ce896ed8 100644 --- a/core/src/ten_manager/designer_frontend/src/flow/CustomHandle.tsx +++ b/core/src/ten_manager/designer_frontend/src/flow/CustomHandle.tsx @@ -21,29 +21,12 @@ const CustomHandle: React.FC = ({ id, type, position, - label, - labelOffsetX = 0, // Default label position offset - labelOffsetY = -0, style = {}, }) => { return (
{/* Render the actual handle */} - - {/* Render the label */} - {/*
- {label} -
*/}
); }; diff --git a/core/src/ten_manager/designer_frontend/src/flow/CustomNode.tsx b/core/src/ten_manager/designer_frontend/src/flow/CustomNode.tsx index a3bc2293c7..4c9332f1f6 100644 --- a/core/src/ten_manager/designer_frontend/src/flow/CustomNode.tsx +++ b/core/src/ten_manager/designer_frontend/src/flow/CustomNode.tsx @@ -14,7 +14,7 @@ import { } from "lucide-react"; import { cn } from "@/lib/utils"; - +import { dispatchCustomNodeActionPopup } from "@/utils/popup"; import CustomHandle from "@/flow/CustomHandle"; const onConnect = (params: Connection | Edge) => @@ -26,12 +26,20 @@ export type CustomNodeType = Node< addon: string; sourceCmds: string[]; targetCmds: string[]; + sourceData: string[]; + targetData: string[]; + sourceAudioFrame: string[]; + targetAudioFrame: string[]; + sourceVideoFrame: string[]; + targetVideoFrame: string[]; url?: string; }, "customNode" >; export function CustomNode({ data, isConnectable }: NodeProps) { + console.log("CustomNode data === ", data); + return ( <>
) { )} onClick={() => { console.log("clicked CableIcon === ", data); - if (typeof window !== "undefined") { - console.log("dispatching customNodeAction"); - window.dispatchEvent( - new CustomEvent("customNodeAction", { - detail: { action: "connections", source: data.name }, - }) - ); - } + dispatchCustomNodeActionPopup("connections", data.name); }} > @@ -133,7 +134,7 @@ export function CustomNode({ data, isConnectable }: NodeProps) {
{/* Render target handles (for incoming edges) */} - {data.targetCmds.map((cmd, index) => ( + {/* {data.targetCmds.map((cmd, index) => ( ) { isConnectable={isConnectable} onConnect={onConnect} /> - ))} + ))} */} + {/* Render source handles (for outgoing edges) */} - {data.sourceCmds.map((cmd, index) => ( + {/* {data.sourceCmds.map((cmd, index) => ( ) { // style={{ top: index * 20, background: "#555" }} isConnectable={isConnectable} /> - ))} + ))} */} +
diff --git a/core/src/ten_manager/designer_frontend/src/flow/FlowCanvas.tsx b/core/src/ten_manager/designer_frontend/src/flow/FlowCanvas.tsx index 58dbbd0a96..30fdab6cb4 100644 --- a/core/src/ten_manager/designer_frontend/src/flow/FlowCanvas.tsx +++ b/core/src/ten_manager/designer_frontend/src/flow/FlowCanvas.tsx @@ -63,11 +63,13 @@ const FlowCanvas = forwardRef( edge?: CustomEdgeType; node?: CustomNodeType; }>({ visible: false, x: 0, y: 0 }); - const [connPopup, setConnPopup] = useState<{ - visible: boolean; - source: string; - target?: string; - }>({ visible: false, source: "", target: "" }); + const [connPopups, setConnPopups] = useState< + { + id: string; + source: string; + target?: string; + }[] + >([]); const launchTerminal = (data: TerminalData) => { const newPopup = { id: `${data.title}-${Date.now()}`, data }; @@ -108,11 +110,19 @@ const FlowCanvas = forwardRef( const launchConnPopup = (source: string, target?: string) => { console.log("launchConnPopup", source, target); - setConnPopup({ visible: true, source, target }); + setConnPopups((prev) => { + const existingPopup = prev.find( + (popup) => popup.source === source && popup.target === target + ); + if (existingPopup) { + return prev; + } + return [...prev, { source, target, id: `${source}-${target ?? ""}` }]; + }); }; - const closeConnPopup = () => { - setConnPopup({ visible: false, source: "", target: "" }); + const closeConnPopup = (id: string) => { + setConnPopups((prev) => prev.filter((popup) => popup.id !== id)); }; const renderContextMenu = () => { @@ -286,14 +296,14 @@ const FlowCanvas = forwardRef( /> ))} - {connPopup.visible && ( + {connPopups.map((popup) => ( closeConnPopup(popup.id)} /> - )} + ))} ); } diff --git a/core/src/ten_manager/designer_frontend/src/flow/graph.ts b/core/src/ten_manager/designer_frontend/src/flow/graph.ts index ed6fea928d..e6d093f475 100644 --- a/core/src/ten_manager/designer_frontend/src/flow/graph.ts +++ b/core/src/ten_manager/designer_frontend/src/flow/graph.ts @@ -11,7 +11,11 @@ import { CustomNodeType } from "@/flow/CustomNode"; import { CustomEdgeType } from "@/flow/CustomEdge"; import { getExtensionAddonByName } from "@/api/services/addons"; import type { IExtensionAddon } from "@/types/addons"; -import type { IBackendNode, IBackendConnection } from "@/types/graphs"; +import { + type IBackendNode, + type IBackendConnection, + EConnectionType, +} from "@/types/graphs"; const NODE_WIDTH = 172; const NODE_HEIGHT = 36; @@ -61,6 +65,12 @@ export const processNodes = ( addon: n.addon, sourceCmds: [], targetCmds: [], + sourceData: [], + targetData: [], + sourceAudioFrame: [], + targetAudioFrame: [], + sourceVideoFrame: [], + targetVideoFrame: [], }, })); }; @@ -70,52 +80,115 @@ export const processConnections = ( ): { initialEdges: CustomEdgeType[]; nodeSourceCmdMap: Record>; + nodeSourceDataMap: Record>; + nodeSourceAudioFrameMap: Record>; + nodeSourceVideoFrameMap: Record>; nodeTargetCmdMap: Record>; + nodeTargetDataMap: Record>; + nodeTargetAudioFrameMap: Record>; + nodeTargetVideoFrameMap: Record>; } => { const initialEdges: CustomEdgeType[] = []; const nodeSourceCmdMap: Record> = {}; + const nodeSourceDataMap: Record> = {}; + const nodeSourceAudioFrameMap: Record> = {}; + const nodeSourceVideoFrameMap: Record> = {}; const nodeTargetCmdMap: Record> = {}; + const nodeTargetDataMap: Record> = {}; + const nodeTargetAudioFrameMap: Record> = {}; + const nodeTargetVideoFrameMap: Record> = {}; backendConnections.forEach((c) => { const sourceNodeId = c.extension; - if (c.cmd) { - c.cmd.forEach((cmdItem) => { - cmdItem.dest.forEach((d) => { - const targetNodeId = d.extension; - const edgeId = - `edge-${sourceNodeId}-` + `${cmdItem.name}-${targetNodeId}`; - const cmdName = cmdItem.name; - - // Record the cmd name of the source node. - if (!nodeSourceCmdMap[sourceNodeId]) { - nodeSourceCmdMap[sourceNodeId] = new Set(); - } - nodeSourceCmdMap[sourceNodeId].add(cmdName); - - // Record the cmd name of the target node. - if (!nodeTargetCmdMap[targetNodeId]) { - nodeTargetCmdMap[targetNodeId] = new Set(); - } - nodeTargetCmdMap[targetNodeId].add(cmdName); - - initialEdges.push({ - id: edgeId, - source: sourceNodeId, - target: targetNodeId, - type: "customEdge", - label: cmdName, - sourceHandle: `source-${cmdName}`, - targetHandle: `target-${cmdName}`, - markerEnd: { - type: MarkerType.ArrowClosed, - }, + const types = [ + EConnectionType.CMD, + EConnectionType.DATA, + EConnectionType.AUDIO_FRAME, + EConnectionType.VIDEO_FRAME, + ]; + types.forEach((type) => { + if (c[type as keyof IBackendConnection]) { + ( + c[type as keyof IBackendConnection] as Array<{ + name: string; + dest: Array<{ extension: string }>; + }> + ).forEach((item) => { + item.dest.forEach((d) => { + const targetNodeId = d.extension; + const edgeId = `edge-${sourceNodeId}-${item.name}-${targetNodeId}`; + const itemName = item.name; + + // Record the item name in the appropriate source map based on type + let sourceMap: Record>; + let targetMap: Record>; + switch (type) { + case EConnectionType.CMD: + sourceMap = nodeSourceCmdMap; + targetMap = nodeTargetCmdMap; + break; + case EConnectionType.DATA: + sourceMap = nodeSourceDataMap; + targetMap = nodeTargetDataMap; + break; + case EConnectionType.AUDIO_FRAME: + sourceMap = nodeSourceAudioFrameMap; + targetMap = nodeTargetAudioFrameMap; + break; + case EConnectionType.VIDEO_FRAME: + sourceMap = nodeSourceVideoFrameMap; + targetMap = nodeTargetVideoFrameMap; + break; + default: + sourceMap = nodeSourceCmdMap; + targetMap = nodeTargetCmdMap; + } + + if (!sourceMap[sourceNodeId]) { + sourceMap[sourceNodeId] = new Set(); + } + sourceMap[sourceNodeId].add(itemName); + + // Record the item name in the appropriate target map + if (!targetMap[targetNodeId]) { + targetMap[targetNodeId] = new Set(); + } + targetMap[targetNodeId].add(itemName); + + initialEdges.push({ + id: edgeId, + source: sourceNodeId, + target: targetNodeId, + data: { + connectionType: type, + labelOffsetX: 0, + labelOffsetY: 0, + }, + type: "customEdge", + label: itemName, + sourceHandle: `source-${sourceNodeId}`, + targetHandle: `target-${targetNodeId}`, + markerEnd: { + type: MarkerType.ArrowClosed, + }, + }); }); }); - }); - } + } + }); }); - return { initialEdges, nodeSourceCmdMap, nodeTargetCmdMap }; + return { + initialEdges, + nodeSourceCmdMap, + nodeSourceDataMap, + nodeSourceAudioFrameMap, + nodeSourceVideoFrameMap, + nodeTargetCmdMap, + nodeTargetDataMap, + nodeTargetAudioFrameMap, + nodeTargetVideoFrameMap, + }; }; export const fetchAddonInfoForNodes = async ( @@ -148,9 +221,28 @@ export const fetchAddonInfoForNodes = async ( export const enhanceNodesWithCommands = ( nodes: CustomNodeType[], - nodeSourceCmdMap: Record>, - nodeTargetCmdMap: Record> + options: { + nodeSourceCmdMap: Record>; + nodeTargetCmdMap: Record>; + nodeSourceDataMap: Record>; + nodeSourceAudioFrameMap: Record>; + nodeSourceVideoFrameMap: Record>; + nodeTargetDataMap: Record>; + nodeTargetAudioFrameMap: Record>; + nodeTargetVideoFrameMap: Record>; + } ): CustomNodeType[] => { + const { + nodeSourceCmdMap = {}, + nodeTargetCmdMap = {}, + nodeSourceDataMap = {}, + nodeSourceAudioFrameMap = {}, + nodeSourceVideoFrameMap = {}, + nodeTargetDataMap = {}, + nodeTargetAudioFrameMap = {}, + nodeTargetVideoFrameMap = {}, + } = options; + return nodes.map((node) => { const sourceCmds = nodeSourceCmdMap[node.id] ? Array.from(nodeSourceCmdMap[node.id]) @@ -158,6 +250,28 @@ export const enhanceNodesWithCommands = ( const targetCmds = nodeTargetCmdMap[node.id] ? Array.from(nodeTargetCmdMap[node.id]) : []; + + const sourceData = nodeSourceDataMap[node.id] + ? Array.from(nodeSourceDataMap[node.id]) + : []; + const targetData = nodeTargetDataMap[node.id] + ? Array.from(nodeTargetDataMap[node.id]) + : []; + + const sourceAudioFrame = nodeSourceAudioFrameMap[node.id] + ? Array.from(nodeSourceAudioFrameMap[node.id]) + : []; + const targetAudioFrame = nodeTargetAudioFrameMap[node.id] + ? Array.from(nodeTargetAudioFrameMap[node.id]) + : []; + + const sourceVideoFrame = nodeSourceVideoFrameMap[node.id] + ? Array.from(nodeSourceVideoFrameMap[node.id]) + : []; + const targetVideoFrame = nodeTargetVideoFrameMap[node.id] + ? Array.from(nodeTargetVideoFrameMap[node.id]) + : []; + return { ...node, type: "customNode", @@ -166,6 +280,12 @@ export const enhanceNodesWithCommands = ( label: node.data.name || `${node.id}`, sourceCmds, targetCmds, + sourceData, + targetData, + sourceAudioFrame, + targetAudioFrame, + sourceVideoFrame, + targetVideoFrame, }, }; }); diff --git a/core/src/ten_manager/designer_frontend/src/types/graphs.ts b/core/src/ten_manager/designer_frontend/src/types/graphs.ts index 7be1287a14..fdc2ec8eef 100644 --- a/core/src/ten_manager/designer_frontend/src/types/graphs.ts +++ b/core/src/ten_manager/designer_frontend/src/types/graphs.ts @@ -13,10 +13,38 @@ export interface IBackendNode { api?: unknown; } +export enum EConnectionType { + CMD = "cmd", + DATA = "data", + AUDIO_FRAME = "audio_frame", + VIDEO_FRAME = "video_frame", +} + export interface IBackendConnection { app: string; extension: string; - cmd?: { + [EConnectionType.CMD]?: { + name: string; + dest: { + app: string; + extension: string; + }[]; + }[]; + [EConnectionType.DATA]?: { + name: string; + dest: { + app: string; + extension: string; + }[]; + }[]; + [EConnectionType.AUDIO_FRAME]?: { + name: string; + dest: { + app: string; + extension: string; + }[]; + }[]; + [EConnectionType.VIDEO_FRAME]?: { name: string; dest: { app: string; diff --git a/core/src/ten_manager/designer_frontend/src/utils/popup.ts b/core/src/ten_manager/designer_frontend/src/utils/popup.ts new file mode 100644 index 0000000000..79b4a77192 --- /dev/null +++ b/core/src/ten_manager/designer_frontend/src/utils/popup.ts @@ -0,0 +1,13 @@ +export const dispatchCustomNodeActionPopup = ( + action: string, + source: string, + target?: string +) => { + if (typeof window !== "undefined") { + window.dispatchEvent( + new CustomEvent("customNodeAction", { + detail: { action, source, target }, + }) + ); + } +}; From 10c1e30fb778f78cab64ea7721542efccee1e096 Mon Sep 17 00:00:00 2001 From: czhen <56986964+shczhen@users.noreply.github.com> Date: Thu, 2 Jan 2025 18:11:02 +0800 Subject: [PATCH 3/6] chore: rm unexpected console log --- .../src/components/Popup/EditorPopup.tsx | 1 - .../src/context/ReactFlowDataContext.tsx | 5 +---- .../designer_frontend/src/flow/CustomNode.tsx | 3 --- .../designer_frontend/src/flow/FlowCanvas.tsx | 9 +++------ 4 files changed, 4 insertions(+), 14 deletions(-) diff --git a/core/src/ten_manager/designer_frontend/src/components/Popup/EditorPopup.tsx b/core/src/ten_manager/designer_frontend/src/components/Popup/EditorPopup.tsx index 7213824b40..8c1d59cf5a 100644 --- a/core/src/ten_manager/designer_frontend/src/components/Popup/EditorPopup.tsx +++ b/core/src/ten_manager/designer_frontend/src/components/Popup/EditorPopup.tsx @@ -90,7 +90,6 @@ const EditorPopup: React.FC = ({ data, onClose }) => { const saveFile = async (content: string) => { try { await putFileContent(data.url, { content }); - console.log("File saved successfully"); toast.success("File saved successfully"); // We can add UI prompts, such as displaying a success notification. } catch (error: unknown) { diff --git a/core/src/ten_manager/designer_frontend/src/context/ReactFlowDataContext.tsx b/core/src/ten_manager/designer_frontend/src/context/ReactFlowDataContext.tsx index 9f15a15538..c291962b00 100644 --- a/core/src/ten_manager/designer_frontend/src/context/ReactFlowDataContext.tsx +++ b/core/src/ten_manager/designer_frontend/src/context/ReactFlowDataContext.tsx @@ -17,13 +17,10 @@ export const ReactFlowDataContext = React.createContext({ // --- hooks --- export const useCDAVInfoByEdgeId = (edgeId: string) => { const { nodes, edges } = React.useContext(ReactFlowDataContext); - console.log("[useCDAVInfoByEdgeId] nodes === ", nodes); - console.log("[useCDAVInfoByEdgeId] edges === ", edges); + const edge = edges.find((e) => e.id === edgeId); if (!edge) return null; - console.log("[useCDAVInfoByEdgeId] edge === ", edge); - const source = nodes.find((n) => n.id === edge.source); const target = nodes.find((n) => n.id === edge.target); if (!source || !target) return null; diff --git a/core/src/ten_manager/designer_frontend/src/flow/CustomNode.tsx b/core/src/ten_manager/designer_frontend/src/flow/CustomNode.tsx index 4c9332f1f6..09a0ed264d 100644 --- a/core/src/ten_manager/designer_frontend/src/flow/CustomNode.tsx +++ b/core/src/ten_manager/designer_frontend/src/flow/CustomNode.tsx @@ -38,8 +38,6 @@ export type CustomNodeType = Node< >; export function CustomNode({ data, isConnectable }: NodeProps) { - console.log("CustomNode data === ", data); - return ( <>
) { "cursor-pointer" )} onClick={() => { - console.log("clicked CableIcon === ", data); dispatchCustomNodeActionPopup("connections", data.name); }} > diff --git a/core/src/ten_manager/designer_frontend/src/flow/FlowCanvas.tsx b/core/src/ten_manager/designer_frontend/src/flow/FlowCanvas.tsx index 30fdab6cb4..21f2c28d6d 100644 --- a/core/src/ten_manager/designer_frontend/src/flow/FlowCanvas.tsx +++ b/core/src/ten_manager/designer_frontend/src/flow/FlowCanvas.tsx @@ -109,7 +109,6 @@ const FlowCanvas = forwardRef( }; const launchConnPopup = (source: string, target?: string) => { - console.log("launchConnPopup", source, target); setConnPopups((prev) => { const existingPopup = prev.find( (popup) => popup.source === source && popup.target === target @@ -193,10 +192,8 @@ const FlowCanvas = forwardRef( closeContextMenu(); }; const handleCustomNodeAction = (event: CustomEvent) => { - console.log("handleCustomNodeAction", event); switch (event.detail.action) { case "connections": - console.log("handleCustomNodeAction: launching conn popup"); launchConnPopup(event.detail.source, event.detail.target); break; default: @@ -243,9 +240,9 @@ const FlowCanvas = forwardRef( style={{ width: "100%", height: "100%" }} onNodeContextMenu={clickNodeContextMenu} onEdgeContextMenu={clickEdgeContextMenu} - onEdgeClick={(e, edge) => { - console.log("clicked", e, edge); - }} + // onEdgeClick={(e, edge) => { + // console.log("clicked", e, edge); + // }} > From bd99a67b5d69443eb053e404e09d6a8a2cd84f57 Mon Sep 17 00:00:00 2001 From: czhen <56986964+shczhen@users.noreply.github.com> Date: Tue, 7 Jan 2025 16:04:08 +0800 Subject: [PATCH 4/6] chore: update data table --- .../src/components/AppBar/AppBar.tsx | 11 +- .../src/components/AppBar/AppStatus.tsx | 40 ++++ .../components/DataTable/ConnectionTable.tsx | 196 ++++++++++++++---- .../src/components/ui/input.tsx | 23 ++ 4 files changed, 224 insertions(+), 46 deletions(-) create mode 100644 core/src/ten_manager/designer_frontend/src/components/AppBar/AppStatus.tsx create mode 100644 core/src/ten_manager/designer_frontend/src/components/ui/input.tsx diff --git a/core/src/ten_manager/designer_frontend/src/components/AppBar/AppBar.tsx b/core/src/ten_manager/designer_frontend/src/components/AppBar/AppBar.tsx index 1c51461bda..d0baaa09f0 100644 --- a/core/src/ten_manager/designer_frontend/src/components/AppBar/AppBar.tsx +++ b/core/src/ten_manager/designer_frontend/src/components/AppBar/AppBar.tsx @@ -16,6 +16,7 @@ import { Badge } from "@/components/ui/Badge"; import { FileMenu } from "@/components/AppBar/FileMenu"; import { EditMenu } from "@/components/AppBar/EditMenu"; import { HelpMenu } from "@/components/AppBar/HelpMenu"; +import { AppStatus } from "@/components/AppBar/AppStatus"; import { cn } from "@/lib/utils"; interface AppBarProps { @@ -67,8 +68,16 @@ const AppBar: React.FC = ({ + {/* Middle part is the status bar. */} + + {/* Right part is the logo. */} -
+
+
+ {status === EAppStatus.SAVED && } + {status === EAppStatus.UNSAVED && ( + + )} + {status === EAppStatus.EDITING && ( + + )} + {status} +
+ {status === EAppStatus.EDITING && ( +
+ +
+ )} +
+ ); +} diff --git a/core/src/ten_manager/designer_frontend/src/components/DataTable/ConnectionTable.tsx b/core/src/ten_manager/designer_frontend/src/components/DataTable/ConnectionTable.tsx index 31a37e8dd8..4dddc002dd 100644 --- a/core/src/ten_manager/designer_frontend/src/components/DataTable/ConnectionTable.tsx +++ b/core/src/ten_manager/designer_frontend/src/components/DataTable/ConnectionTable.tsx @@ -1,10 +1,24 @@ +import * as React from "react"; + import { ColumnDef, + ColumnFiltersState, + SortingState, flexRender, getCoreRowModel, + getFilteredRowModel, + getSortedRowModel, useReactTable, } from "@tanstack/react-table"; -import { BlocksIcon, ArrowBigRightDashIcon } from "lucide-react"; +import { + BlocksIcon, + ArrowBigRightDashIcon, + MoreHorizontal, + ArrowUpDown, + ArrowUpIcon, + ArrowDownIcon, +} from "lucide-react"; +import { toast } from "sonner"; import { Table, @@ -16,6 +30,14 @@ import { } from "@/components/ui/table"; import { Badge } from "@/components/ui/Badge"; import { Button } from "@/components/ui/Button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/DropdownMenu"; import { cn } from "@/lib/utils"; import { dispatchCustomNodeActionPopup } from "@/utils/popup"; @@ -40,7 +62,23 @@ export const commonConnectionColumns: ColumnDef[] = [ }, { accessorKey: "type", - header: "Type", + header: ({ column }) => { + return ( + + ); + }, cell: ({ row }) => { const type = row.getValue("type") as EConnectionType; if (!type) return null; @@ -60,6 +98,53 @@ export const connectionColumns: ColumnDef[] = [ accessorKey: "target", header: "Target", }, + { + id: "actions", + header: "Actions", + cell: ({ row }) => { + const connection = row.original; + const { id, source, target, type } = connection; + + return ( + + + + + + Actions + { + toast.info("View Details", { + description: + "Source: " + + source + + ", Target: " + + target + + ", Type: " + + type, + }); + }} + > + View Details + + + { + toast.info("Delete", { + description: "Delete connection: " + id, + }); + }} + > + Delete + + + + ); + }, + }, ]; // eslint-disable-next-line react-refresh/only-export-components @@ -130,57 +215,78 @@ export function DataTable({ data, className, }: DataTableProps & { className?: string }) { + const [sorting, setSorting] = React.useState([]); + const [columnFilters, setColumnFilters] = React.useState( + [] + ); + const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel(), + onSortingChange: setSorting, + getSortedRowModel: getSortedRowModel(), + onColumnFiltersChange: setColumnFilters, + getFilteredRowModel: getFilteredRowModel(), + state: { + sorting, + columnFilters, + }, }); return ( -
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ); - })} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} + <> +
+
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ); + })} - )) - ) : ( - - - No results. - - - )} - -
-
+ ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+ ); } diff --git a/core/src/ten_manager/designer_frontend/src/components/ui/input.tsx b/core/src/ten_manager/designer_frontend/src/components/ui/input.tsx new file mode 100644 index 0000000000..b4a64bb189 --- /dev/null +++ b/core/src/ten_manager/designer_frontend/src/components/ui/input.tsx @@ -0,0 +1,23 @@ +/* eslint-disable max-len */ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const Input = React.forwardRef>( + ({ className, type, ...props }, ref) => { + return ( + + ); + } +); +Input.displayName = "Input"; + +export { Input }; From d24d9f427a35f5dc412cde1c1da70fec0bff9358 Mon Sep 17 00:00:00 2001 From: czhen <56986964+shczhen@users.noreply.github.com> Date: Tue, 7 Jan 2025 16:28:38 +0800 Subject: [PATCH 5/6] chore: rename components --- .../src/components/DataTable/ConnectionTable.tsx | 2 +- .../src/components/Popup/CustomNodeConnPopup.tsx | 2 +- .../src/components/ui/{input.tsx => Input.tsx} | 0 .../src/components/ui/{table.tsx => Table.tsx} | 0 .../designer_frontend/src/components/ui/{tabs.tsx => Tabs.tsx} | 0 5 files changed, 2 insertions(+), 2 deletions(-) rename core/src/ten_manager/designer_frontend/src/components/ui/{input.tsx => Input.tsx} (100%) rename core/src/ten_manager/designer_frontend/src/components/ui/{table.tsx => Table.tsx} (100%) rename core/src/ten_manager/designer_frontend/src/components/ui/{tabs.tsx => Tabs.tsx} (100%) diff --git a/core/src/ten_manager/designer_frontend/src/components/DataTable/ConnectionTable.tsx b/core/src/ten_manager/designer_frontend/src/components/DataTable/ConnectionTable.tsx index 4dddc002dd..a5cfaf5a4b 100644 --- a/core/src/ten_manager/designer_frontend/src/components/DataTable/ConnectionTable.tsx +++ b/core/src/ten_manager/designer_frontend/src/components/DataTable/ConnectionTable.tsx @@ -27,7 +27,7 @@ import { TableHead, TableHeader, TableRow, -} from "@/components/ui/table"; +} from "@/components/ui/Table"; import { Badge } from "@/components/ui/Badge"; import { Button } from "@/components/ui/Button"; import { diff --git a/core/src/ten_manager/designer_frontend/src/components/Popup/CustomNodeConnPopup.tsx b/core/src/ten_manager/designer_frontend/src/components/Popup/CustomNodeConnPopup.tsx index ecf6b20f98..5afc0d5e34 100644 --- a/core/src/ten_manager/designer_frontend/src/components/Popup/CustomNodeConnPopup.tsx +++ b/core/src/ten_manager/designer_frontend/src/components/Popup/CustomNodeConnPopup.tsx @@ -4,7 +4,7 @@ import { BlocksIcon, ArrowBigRightDashIcon } from "lucide-react"; import Popup from "@/components/Popup/Popup"; import { Button } from "@/components/ui/Button"; -import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/Tabs"; import { ReactFlowDataContext } from "@/context/ReactFlowDataContext"; import { DataTable as ConnectionDataTable, diff --git a/core/src/ten_manager/designer_frontend/src/components/ui/input.tsx b/core/src/ten_manager/designer_frontend/src/components/ui/Input.tsx similarity index 100% rename from core/src/ten_manager/designer_frontend/src/components/ui/input.tsx rename to core/src/ten_manager/designer_frontend/src/components/ui/Input.tsx diff --git a/core/src/ten_manager/designer_frontend/src/components/ui/table.tsx b/core/src/ten_manager/designer_frontend/src/components/ui/Table.tsx similarity index 100% rename from core/src/ten_manager/designer_frontend/src/components/ui/table.tsx rename to core/src/ten_manager/designer_frontend/src/components/ui/Table.tsx diff --git a/core/src/ten_manager/designer_frontend/src/components/ui/tabs.tsx b/core/src/ten_manager/designer_frontend/src/components/ui/Tabs.tsx similarity index 100% rename from core/src/ten_manager/designer_frontend/src/components/ui/tabs.tsx rename to core/src/ten_manager/designer_frontend/src/components/ui/Tabs.tsx From 193f7e9910487e8919aca4f0b383ecc13d4743fd Mon Sep 17 00:00:00 2001 From: Hu Yueh-Wei Date: Tue, 7 Jan 2025 17:28:30 +0800 Subject: [PATCH 6/6] fix: refine codes --- .../ten_manager/designer_frontend/bun.lockb | Bin 174125 -> 174125 bytes .../src/components/AppBar/AppStatus.tsx | 6 ++++++ .../components/DataTable/ConnectionTable.tsx | 6 ++++++ .../components/Popup/CustomNodeConnPopup.tsx | 6 ++++++ .../src/components/ui/Badge.tsx | 6 ++++++ .../src/components/ui/Button.tsx | 6 ++++++ .../src/components/ui/Dialog.tsx | 6 ++++++ .../src/components/ui/DropdownMenu.tsx | 6 ++++++ .../src/components/ui/Input.tsx | 6 ++++++ .../src/components/ui/NavigationMenu.tsx | 6 ++++++ .../src/components/ui/Sonner.tsx | 6 ++++++ .../src/components/ui/Table.tsx | 6 ++++++ .../src/components/ui/Tabs.tsx | 6 ++++++ .../src/context/ReactFlowDataContext.tsx | 6 ++++++ .../designer_frontend/src/utils/popup.ts | 6 ++++++ 15 files changed, 84 insertions(+) diff --git a/core/src/ten_manager/designer_frontend/bun.lockb b/core/src/ten_manager/designer_frontend/bun.lockb index 2c5f61bb0cc14b5b413fa21e6ec3a17978c8cae8..689d24a7a8ea914b1497c1aaada41d5a6d58ccad 100755 GIT binary patch delta 931 zcmXxgTS(JU9KiAaPt#yJ=eFi5m(|SKG6z%3Fjmk*FFgdAbY-@rP$;OV=s{bzm!gZ{ z!;zA%NTfJ-b#%dsAPOQ^QDo4=EQgg^K1I|+-#@LxK0CkL|LpuXkTMLU3@769EaaZj z{;l((X?(OCtk%C+7qYcn;ypJjDsYx%?ts(Mf=rB5{t%1_lD z7Ki!&>H2B2kf$PHDK+ZTi{V7M3WUY3-ZO42BsZc6CN&e5g(by=H88^hD{3`092I}V z3XZKqEq-yyX?l~;-|rjwB&PH_zT*cb=zY=DnuvV0y&z5_g;#iuH_8_id;Tcl2Ti%6 zQdW`Xu`ztcMe3ax#uGfpeLPTssBEyTVx%4`@rRfHEh-Zir)L>ATmm<$upBN_q8dw4 z0Vm3HRenOsR8LG6EjYzNClN$DITv%a3G3=NFGy3^UZs9KO z;SnB-rbZHCQ*{Y(cz1KD793>y0QR93ZTLieg8eFc5`DO%Iuc@c?jYQXE!c_e*n~!G zMkq&>4N1xRE*?Kdeu#ZP#)&hyhJJ)_7N_AyE84IJO?b9jskk! noOups5Tn+MZHSQXW!D|S;biZSOq*tl-|O*uYLb_qN>}CbC)DvJquv)I}f^L>ED2?=4Ldje?4bE@t+2QeM02)i&UQX6 zuVTUV7H#^vCYr{r8SMr$yKZJyvIg}dCaz%p^kPlBVNT7YUuS0BW>!Q$sX}q7kwMiM zm!Izc8yg$Vfbmq3dZ3F}ebB{WchHx@376WeOQnkFQZ~m=A3&AL7}=OEKF><}I;>{1 zI;_Gki|R^9xzZC-P(w-aRea$Sz9NqijA9I3JlU=6DcRy&LtBrv_{|Cbi1H*P?ElV^ zNlc-DZx})zIlNQRqQ_fp1@JG zp&bR>WiE?Hc&wrs@%nc$+>O21hyB=w?bv~sT9J|EJ4YGzu)|sEN$N4&Gp6wbS8xe! zXvblM(TMl_?%A;7FTGI5sS}!N=#%P=AL)m2j;s@B5W!8}aTS-5qVGj9>-JC+WUW-g VDV)ys_Q_PqEMnuz-g+rL`F~}Cl0*Oi diff --git a/core/src/ten_manager/designer_frontend/src/components/AppBar/AppStatus.tsx b/core/src/ten_manager/designer_frontend/src/components/AppBar/AppStatus.tsx index 05e824a099..1ebf38f98a 100644 --- a/core/src/ten_manager/designer_frontend/src/components/AppBar/AppStatus.tsx +++ b/core/src/ten_manager/designer_frontend/src/components/AppBar/AppStatus.tsx @@ -1,3 +1,9 @@ +// +// Copyright © 2025 Agora +// This file is part of TEN Framework, an open source project. +// Licensed under the Apache License, Version 2.0, with certain conditions. +// Refer to the "LICENSE" file in the root directory for more information. +// import { SaveIcon, FilePenLineIcon, SaveOffIcon } from "lucide-react"; import { Button } from "@/components/ui/Button"; diff --git a/core/src/ten_manager/designer_frontend/src/components/DataTable/ConnectionTable.tsx b/core/src/ten_manager/designer_frontend/src/components/DataTable/ConnectionTable.tsx index a5cfaf5a4b..c098998021 100644 --- a/core/src/ten_manager/designer_frontend/src/components/DataTable/ConnectionTable.tsx +++ b/core/src/ten_manager/designer_frontend/src/components/DataTable/ConnectionTable.tsx @@ -1,3 +1,9 @@ +// +// Copyright © 2025 Agora +// This file is part of TEN Framework, an open source project. +// Licensed under the Apache License, Version 2.0, with certain conditions. +// Refer to the "LICENSE" file in the root directory for more information. +// import * as React from "react"; import { diff --git a/core/src/ten_manager/designer_frontend/src/components/Popup/CustomNodeConnPopup.tsx b/core/src/ten_manager/designer_frontend/src/components/Popup/CustomNodeConnPopup.tsx index 5afc0d5e34..63d349ce00 100644 --- a/core/src/ten_manager/designer_frontend/src/components/Popup/CustomNodeConnPopup.tsx +++ b/core/src/ten_manager/designer_frontend/src/components/Popup/CustomNodeConnPopup.tsx @@ -1,3 +1,9 @@ +// +// Copyright © 2025 Agora +// This file is part of TEN Framework, an open source project. +// Licensed under the Apache License, Version 2.0, with certain conditions. +// Refer to the "LICENSE" file in the root directory for more information. +// import * as React from "react"; import { BlocksIcon, ArrowBigRightDashIcon } from "lucide-react"; diff --git a/core/src/ten_manager/designer_frontend/src/components/ui/Badge.tsx b/core/src/ten_manager/designer_frontend/src/components/ui/Badge.tsx index e8a9d91dbb..ff1ea0201b 100644 --- a/core/src/ten_manager/designer_frontend/src/components/ui/Badge.tsx +++ b/core/src/ten_manager/designer_frontend/src/components/ui/Badge.tsx @@ -1,3 +1,9 @@ +// +// Copyright © 2025 Agora +// This file is part of TEN Framework, an open source project. +// Licensed under the Apache License, Version 2.0, with certain conditions. +// Refer to the "LICENSE" file in the root directory for more information. +// /* eslint-disable max-len */ /* eslint-disable react-refresh/only-export-components */ import * as React from "react"; diff --git a/core/src/ten_manager/designer_frontend/src/components/ui/Button.tsx b/core/src/ten_manager/designer_frontend/src/components/ui/Button.tsx index a98e8374e2..19d999c863 100644 --- a/core/src/ten_manager/designer_frontend/src/components/ui/Button.tsx +++ b/core/src/ten_manager/designer_frontend/src/components/ui/Button.tsx @@ -1,3 +1,9 @@ +// +// Copyright © 2025 Agora +// This file is part of TEN Framework, an open source project. +// Licensed under the Apache License, Version 2.0, with certain conditions. +// Refer to the "LICENSE" file in the root directory for more information. +// /* eslint-disable max-len */ /* eslint-disable react-refresh/only-export-components */ import * as React from "react"; diff --git a/core/src/ten_manager/designer_frontend/src/components/ui/Dialog.tsx b/core/src/ten_manager/designer_frontend/src/components/ui/Dialog.tsx index 2d9f6b7dc5..d47cf2e5eb 100644 --- a/core/src/ten_manager/designer_frontend/src/components/ui/Dialog.tsx +++ b/core/src/ten_manager/designer_frontend/src/components/ui/Dialog.tsx @@ -1,3 +1,9 @@ +// +// Copyright © 2025 Agora +// This file is part of TEN Framework, an open source project. +// Licensed under the Apache License, Version 2.0, with certain conditions. +// Refer to the "LICENSE" file in the root directory for more information. +// /* eslint-disable max-len */ import * as React from "react"; import * as DialogPrimitive from "@radix-ui/react-dialog"; diff --git a/core/src/ten_manager/designer_frontend/src/components/ui/DropdownMenu.tsx b/core/src/ten_manager/designer_frontend/src/components/ui/DropdownMenu.tsx index f031b86603..19363ea2b2 100644 --- a/core/src/ten_manager/designer_frontend/src/components/ui/DropdownMenu.tsx +++ b/core/src/ten_manager/designer_frontend/src/components/ui/DropdownMenu.tsx @@ -1,3 +1,9 @@ +// +// Copyright © 2025 Agora +// This file is part of TEN Framework, an open source project. +// Licensed under the Apache License, Version 2.0, with certain conditions. +// Refer to the "LICENSE" file in the root directory for more information. +// /* eslint-disable max-len */ import * as React from "react"; import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; diff --git a/core/src/ten_manager/designer_frontend/src/components/ui/Input.tsx b/core/src/ten_manager/designer_frontend/src/components/ui/Input.tsx index b4a64bb189..7885253cba 100644 --- a/core/src/ten_manager/designer_frontend/src/components/ui/Input.tsx +++ b/core/src/ten_manager/designer_frontend/src/components/ui/Input.tsx @@ -1,3 +1,9 @@ +// +// Copyright © 2025 Agora +// This file is part of TEN Framework, an open source project. +// Licensed under the Apache License, Version 2.0, with certain conditions. +// Refer to the "LICENSE" file in the root directory for more information. +// /* eslint-disable max-len */ import * as React from "react"; diff --git a/core/src/ten_manager/designer_frontend/src/components/ui/NavigationMenu.tsx b/core/src/ten_manager/designer_frontend/src/components/ui/NavigationMenu.tsx index f9332a1e85..142b2ae76e 100644 --- a/core/src/ten_manager/designer_frontend/src/components/ui/NavigationMenu.tsx +++ b/core/src/ten_manager/designer_frontend/src/components/ui/NavigationMenu.tsx @@ -1,3 +1,9 @@ +// +// Copyright © 2025 Agora +// This file is part of TEN Framework, an open source project. +// Licensed under the Apache License, Version 2.0, with certain conditions. +// Refer to the "LICENSE" file in the root directory for more information. +// /* eslint-disable max-len */ /* eslint-disable react-refresh/only-export-components */ import * as React from "react"; diff --git a/core/src/ten_manager/designer_frontend/src/components/ui/Sonner.tsx b/core/src/ten_manager/designer_frontend/src/components/ui/Sonner.tsx index 10e908244d..ae04bb7c9d 100644 --- a/core/src/ten_manager/designer_frontend/src/components/ui/Sonner.tsx +++ b/core/src/ten_manager/designer_frontend/src/components/ui/Sonner.tsx @@ -1,3 +1,9 @@ +// +// Copyright © 2025 Agora +// This file is part of TEN Framework, an open source project. +// Licensed under the Apache License, Version 2.0, with certain conditions. +// Refer to the "LICENSE" file in the root directory for more information. +// /* eslint-disable max-len */ import { useTheme } from "next-themes"; import { Toaster as Sonner } from "sonner"; diff --git a/core/src/ten_manager/designer_frontend/src/components/ui/Table.tsx b/core/src/ten_manager/designer_frontend/src/components/ui/Table.tsx index 0f714d296e..aebdad347b 100644 --- a/core/src/ten_manager/designer_frontend/src/components/ui/Table.tsx +++ b/core/src/ten_manager/designer_frontend/src/components/ui/Table.tsx @@ -1,3 +1,9 @@ +// +// Copyright © 2025 Agora +// This file is part of TEN Framework, an open source project. +// Licensed under the Apache License, Version 2.0, with certain conditions. +// Refer to the "LICENSE" file in the root directory for more information. +// /* eslint-disable max-len */ import * as React from "react"; diff --git a/core/src/ten_manager/designer_frontend/src/components/ui/Tabs.tsx b/core/src/ten_manager/designer_frontend/src/components/ui/Tabs.tsx index 988f2bdf3a..1ed9b8735e 100644 --- a/core/src/ten_manager/designer_frontend/src/components/ui/Tabs.tsx +++ b/core/src/ten_manager/designer_frontend/src/components/ui/Tabs.tsx @@ -1,3 +1,9 @@ +// +// Copyright © 2025 Agora +// This file is part of TEN Framework, an open source project. +// Licensed under the Apache License, Version 2.0, with certain conditions. +// Refer to the "LICENSE" file in the root directory for more information. +// /* eslint-disable max-len */ import * as React from "react"; import * as TabsPrimitive from "@radix-ui/react-tabs"; diff --git a/core/src/ten_manager/designer_frontend/src/context/ReactFlowDataContext.tsx b/core/src/ten_manager/designer_frontend/src/context/ReactFlowDataContext.tsx index c291962b00..1890077510 100644 --- a/core/src/ten_manager/designer_frontend/src/context/ReactFlowDataContext.tsx +++ b/core/src/ten_manager/designer_frontend/src/context/ReactFlowDataContext.tsx @@ -1,3 +1,9 @@ +// +// Copyright © 2025 Agora +// This file is part of TEN Framework, an open source project. +// Licensed under the Apache License, Version 2.0, with certain conditions. +// Refer to the "LICENSE" file in the root directory for more information. +// import * as React from "react"; import type { CustomNodeType } from "@/flow/CustomNode"; diff --git a/core/src/ten_manager/designer_frontend/src/utils/popup.ts b/core/src/ten_manager/designer_frontend/src/utils/popup.ts index 79b4a77192..718545f20c 100644 --- a/core/src/ten_manager/designer_frontend/src/utils/popup.ts +++ b/core/src/ten_manager/designer_frontend/src/utils/popup.ts @@ -1,3 +1,9 @@ +// +// Copyright © 2025 Agora +// This file is part of TEN Framework, an open source project. +// Licensed under the Apache License, Version 2.0, with certain conditions. +// Refer to the "LICENSE" file in the root directory for more information. +// export const dispatchCustomNodeActionPopup = ( action: string, source: string,