diff --git a/core/src/ten_manager/designer_frontend/bun.lockb b/core/src/ten_manager/designer_frontend/bun.lockb index 0f2777509f..689d24a7a8 100755 Binary files a/core/src/ten_manager/designer_frontend/bun.lockb and b/core/src/ten_manager/designer_frontend/bun.lockb differ 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/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 new file mode 100644 index 0000000000..c098998021 --- /dev/null +++ b/core/src/ten_manager/designer_frontend/src/components/DataTable/ConnectionTable.tsx @@ -0,0 +1,323 @@ +// +// 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 { + ColumnDef, + ColumnFiltersState, + SortingState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table"; +import { + BlocksIcon, + ArrowBigRightDashIcon, + MoreHorizontal, + ArrowUpDown, + ArrowUpIcon, + ArrowDownIcon, +} from "lucide-react"; +import { toast } from "sonner"; + +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} 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"; + +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: ({ column }) => { + return ( + + ); + }, + 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", + }, + { + 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 +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 [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() + )} + + ))} + + )) + ) : ( + + + 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 new file mode 100644 index 0000000000..63d349ce00 --- /dev/null +++ b/core/src/ten_manager/designer_frontend/src/components/Popup/CustomNodeConnPopup.tsx @@ -0,0 +1,166 @@ +// +// 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"; + +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; + +export interface CustomNodeConnPopupProps { + source: string; + target?: string; + onClose?: () => void; +} + +const CustomNodeConnPopup: React.FC = ({ + source, + 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 && 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/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/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 new file mode 100644 index 0000000000..7885253cba --- /dev/null +++ b/core/src/ten_manager/designer_frontend/src/components/ui/Input.tsx @@ -0,0 +1,29 @@ +// +// 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 { cn } from "@/lib/utils"; + +const Input = React.forwardRef>( + ({ className, type, ...props }, ref) => { + return ( + + ); + } +); +Input.displayName = "Input"; + +export { Input }; 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 new file mode 100644 index 0000000000..aebdad347b --- /dev/null +++ b/core/src/ten_manager/designer_frontend/src/components/ui/Table.tsx @@ -0,0 +1,127 @@ +// +// 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 { 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..1ed9b8735e --- /dev/null +++ b/core/src/ten_manager/designer_frontend/src/components/ui/Tabs.tsx @@ -0,0 +1,60 @@ +// +// 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"; + +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..1890077510 --- /dev/null +++ b/core/src/ten_manager/designer_frontend/src/context/ReactFlowDataContext.tsx @@ -0,0 +1,74 @@ +// +// 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"; +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); + + const edge = edges.find((e) => e.id === edgeId); + if (!edge) return null; + + 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 0f5859a6ee..fd75afec9b 100644 --- a/core/src/ten_manager/designer_frontend/src/flow/CustomEdge.tsx +++ b/core/src/ten_manager/designer_frontend/src/flow/CustomEdge.tsx @@ -4,10 +4,26 @@ // 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, + getEdgeCenter, +} from "@xyflow/react"; +import { cn } from "@/lib/utils"; + +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" >; @@ -18,49 +34,90 @@ 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, targetX, targetY, - sourcePosition, targetPosition, }); + const [edgeCenterX, edgeCenterY] = getEdgeCenter({ + sourceX, + sourceY, + targetX, + targetY, + }); - // 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; + const { connectionCounts } = useCDAVInfoByEdgeId(id) ?? {}; - // 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 = (e: React.MouseEvent) => { + e.stopPropagation(); + dispatchCustomNodeActionPopup("connections", source, target); + }; return ( <> - - {label && ( - + {selected && ( + - {label} - + + )} + +
onEdgeClick(event)} + > +
+
+ 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 a78bfe8a8e..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 053083a8ce..09a0ed264d 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 { dispatchCustomNodeActionPopup } from "@/utils/popup"; +import CustomHandle from "@/flow/CustomHandle"; const onConnect = (params: Connection | Edge) => console.log("Handle onConnect", params); @@ -18,6 +26,12 @@ 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" @@ -25,45 +39,154 @@ export type CustomNodeType = Node< export function CustomNode({ data, isConnectable }: NodeProps) { return ( -
-
{data.name}
+ <> +
+
+
{ + dispatchCustomNodeActionPopup("connections", data.name); + }} + > + +
+
- {/* Render source handles (for outgoing edges) */} - {data.sourceCmds.map((cmd, index) => { - return ( +
+
{ + console.log("clicked LogsIcon === ", data); + }} + > + +
+
+ +
+
{ + console.log("clicked InfoIcon === ", data); + }} + > + +
+
+
+ +
+
+
+
+ +
+
+
{data.name}
+
{data.addon}
+
+
+ + {/* 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..21f2c28d6d 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,13 @@ const FlowCanvas = forwardRef( edge?: CustomEdgeType; node?: CustomNodeType; }>({ visible: false, x: 0, y: 0 }); + const [connPopups, setConnPopups] = useState< + { + id: string; + source: string; + target?: string; + }[] + >([]); const launchTerminal = (data: TerminalData) => { const newPopup = { id: `${data.title}-${Date.now()}`, data }; @@ -100,6 +108,22 @@ const FlowCanvas = forwardRef( setEditorPopups((prev) => prev.filter((popup) => popup.id !== id)); }; + const launchConnPopup = (source: string, target?: string) => { + 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 = (id: string) => { + setConnPopups((prev) => prev.filter((popup) => popup.id !== id)); + }; + const renderContextMenu = () => { if (contextMenu.type === "node" && contextMenu.node) { return ( @@ -167,8 +191,27 @@ const FlowCanvas = forwardRef( const handleClick = () => { closeContextMenu(); }; + const handleCustomNodeAction = (event: CustomEvent) => { + switch (event.detail.action) { + case "connections": + 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 +240,39 @@ const FlowCanvas = forwardRef( style={{ width: "100%", height: "100%" }} onNodeContextMenu={clickNodeContextMenu} onEdgeContextMenu={clickEdgeContextMenu} + // onEdgeClick={(e, edge) => { + // console.log("clicked", e, edge); + // }} > - + + + + + + + + + + + + + {renderContextMenu()} @@ -219,6 +292,15 @@ const FlowCanvas = forwardRef( onClose={() => closeEditor(popup.id)} /> ))} + + {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/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; +} 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..718545f20c --- /dev/null +++ b/core/src/ten_manager/designer_frontend/src/utils/popup.ts @@ -0,0 +1,19 @@ +// +// 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, + target?: string +) => { + if (typeof window !== "undefined") { + window.dispatchEvent( + new CustomEvent("customNodeAction", { + detail: { action, source, target }, + }) + ); + } +};