@langgraph-js/ui 1.2.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,169 @@
1
+ import { ReactFlow, Background, Controls, Node, Edge, ReactFlowProvider, useNodesState, useEdgesState, useReactFlow, Panel } from "@xyflow/react";
2
+ import "@xyflow/react/dist/style.css";
3
+ import { useCallback, useEffect } from "react";
4
+ import { useChat } from "../chat/context/ChatContext";
5
+ import Dagre from "@dagrejs/dagre";
6
+ import { flattenGraph } from "./flattenGraph";
7
+ import { AssistantGraph } from "@langgraph-js/sdk";
8
+ import "./flow.css";
9
+ const nodeTypes = {
10
+ group: ({ data }: { data: any }) => (
11
+ <div style={{ position: "absolute", bottom: "100%", left: 0 }}>
12
+ <span>{data.name}</span>
13
+ </div>
14
+ ),
15
+ };
16
+
17
+ const transformEdges = (edges: AssistantGraph["edges"], nodes: Node[]): Edge[] => {
18
+ const newEdges = edges.map((edge): Edge => {
19
+ const sourceNode = nodes.find((n) => n.id === edge.source.toString());
20
+ const targetNode = nodes.find((n) => n.id === edge.target.toString());
21
+ const sourceId = sourceNode?.id;
22
+ const targetId = targetNode?.id;
23
+ return {
24
+ id: `${sourceId}=${targetId}`,
25
+ source: sourceId!,
26
+ target: targetId!,
27
+ // type: edge.conditional ? "smoothstep" : "straight",
28
+ animated: edge.conditional,
29
+ label: edge.data,
30
+ style: {
31
+ stroke: edge.conditional ? "#2563eb" : "#64748b",
32
+ },
33
+ };
34
+ });
35
+ if (!newEdges.find((i) => i.target === "__end__")) {
36
+ const end = [...nodes].reverse().find((i) => i.id.endsWith(":__end__"));
37
+ if (end) {
38
+ newEdges.push({
39
+ id: `${end.id}=__end__`,
40
+ source: end.id,
41
+ target: "__end__",
42
+ type: "smoothstep",
43
+ });
44
+ }
45
+ }
46
+ return newEdges;
47
+ };
48
+
49
+ const LayoutFlow = () => {
50
+ const { fitView } = useReactFlow();
51
+ const { graphVisualize, currentNodeName } = useChat();
52
+ const graphData = graphVisualize || { nodes: [], edges: [] };
53
+
54
+ const initialNodes = flattenGraph(graphData.nodes.map((node) => ({ ...node, type: "default" })));
55
+ const initialEdges = transformEdges(graphData.edges, initialNodes);
56
+
57
+ const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
58
+ const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
59
+ const onLayout = useCallback(
60
+ (direction: "TB" | "LR") => {
61
+ const layouted = getLayoutedElements(nodes, edges, { direction });
62
+ setNodes([...layouted.nodes]);
63
+ setEdges([...layouted.edges]);
64
+ fitView();
65
+ },
66
+ [nodes, edges]
67
+ );
68
+
69
+ useEffect(() => {
70
+ if (graphData.nodes.length > 0) {
71
+ const layouted = getLayoutedElements(initialNodes, initialEdges, { direction: "TB" });
72
+ setNodes([...layouted.nodes]);
73
+ setEdges([...layouted.edges]);
74
+ fitView();
75
+ }
76
+ }, [graphData]);
77
+ useEffect(() => {
78
+ const index = nodes.findIndex((i) => i.id.endsWith(currentNodeName));
79
+ if (index !== -1) {
80
+ const newNodes = [...nodes].map((i) => ({ ...i, selected: false }));
81
+ newNodes[index] = { ...newNodes[index], selected: true };
82
+ setNodes(newNodes);
83
+ fitView();
84
+ }
85
+ }, [currentNodeName]);
86
+ return (
87
+ <div style={{ width: "30%", height: "100%", position: "relative", overflow: "hidden" }}>
88
+ <ReactFlow nodes={nodes} edges={edges} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} fitView className="w-full h-full" nodeTypes={nodeTypes}>
89
+ <Background />
90
+ <Controls />
91
+ <Panel position="top-right">
92
+ <button onClick={() => onLayout("TB")}>垂直布局</button>
93
+ <button onClick={() => onLayout("LR")}>水平布局</button>
94
+ </Panel>
95
+ </ReactFlow>
96
+ </div>
97
+ );
98
+ };
99
+
100
+ const getLayoutedElements = (nodes: Node[], edges: Edge[], options: { direction: "TB" | "LR" }) => {
101
+ const g = new Dagre.graphlib.Graph({ compound: true, multigraph: true }).setDefaultEdgeLabel(() => ({}));
102
+ g.setGraph({ rankdir: options.direction, nodesep: 40, ranksep: 40, edgesep: 20 });
103
+
104
+ nodes.forEach((node) => {
105
+ if (node.type === "group") {
106
+ return;
107
+ }
108
+ g.setNode(node.id, {
109
+ ...node,
110
+ width: 128,
111
+ height: 20,
112
+ });
113
+ });
114
+ edges.forEach((edge) => g.setEdge(edge.source, edge.target));
115
+
116
+ Dagre.layout(g);
117
+ const newNodes = nodes.map((node) => {
118
+ const position = g.node(node.id);
119
+ if (!position) return { ...node };
120
+ let x = 0;
121
+ let y = 0;
122
+
123
+ x = position.x + (position?.width ?? 0);
124
+ y = position.y + (position?.height ?? 0);
125
+
126
+ return {
127
+ ...node,
128
+ position: { x, y },
129
+ _p: position,
130
+ width: position.width,
131
+ height: position.height,
132
+ };
133
+ });
134
+
135
+ const children = new Map<string, Node[]>();
136
+ const padding = 15;
137
+ [...newNodes].reverse().forEach((node) => {
138
+ if (node.type === "group") {
139
+ const nodes = children.get(node.id!) || [];
140
+ const minX = Math.min(...nodes.map((i) => i.position.x));
141
+ const minY = Math.min(...nodes.map((i) => i.position.y));
142
+ const maxX = Math.max(...nodes.map((i) => i.position.x + (i.width ?? 0)));
143
+ const maxY = Math.max(...nodes.map((i) => i.position.y + (i.height ?? 0)));
144
+ node.position.x = minX - padding;
145
+ node.position.y = minY - padding;
146
+ node.width = maxX - minX + padding * 2;
147
+ node.height = maxY - minY + padding * 2;
148
+ nodes.forEach((i) => {
149
+ i.position.x = i.position.x - node.position.x;
150
+ i.position.y = i.position.y - node.position.y;
151
+ });
152
+ }
153
+ if (node.parentId) {
154
+ children.set(node.parentId!, [...(children.get(node.parentId!) ?? []), node]);
155
+ }
156
+ });
157
+ return {
158
+ nodes: newNodes,
159
+ edges,
160
+ };
161
+ };
162
+
163
+ export function Graph() {
164
+ return (
165
+ <ReactFlowProvider>
166
+ <LayoutFlow />
167
+ </ReactFlowProvider>
168
+ );
169
+ }
package/vite.config.ts CHANGED
@@ -1,12 +1,17 @@
1
1
  import react from "@vitejs/plugin-react";
2
2
  import { defineConfig } from "vite";
3
3
  import basicSsl from "@vitejs/plugin-basic-ssl";
4
-
4
+ import path from "node:path";
5
5
  // https://vitejs.dev/config/
6
6
  export default defineConfig(({ mode }) => {
7
7
  const isHttps = mode === "https";
8
8
  return {
9
9
  plugins: [react(), isHttps ? basicSsl() : undefined],
10
+ resolve: {
11
+ alias: {
12
+ "@langgraph-js/sdk": new URL("../langgraph-client/src", import.meta.url).pathname,
13
+ },
14
+ },
10
15
  optimizeDeps: {
11
16
  exclude: ["@langgraph-js/ui", "@langgraph-js/sdk"],
12
17
  },
@@ -1 +0,0 @@
1
- @import"https://unpkg.com/github-markdown-css/github-markdown.css";.chat-container{display:flex;height:100vh;width:100%;position:relative}.chat-sidebar{background-color:#f5f5f5;border-right:1px solid #e0e0e0;display:flex;flex-direction:column}.history-header{padding:16px;border-bottom:1px solid #e0e0e0;display:flex;justify-content:space-between;align-items:center}.history-header h3{margin:0;font-size:16px;color:#333}.close-button{background:none;border:none;font-size:20px;cursor:pointer;color:#666}.history-list{flex:none;width:280px;overflow-y:auto;border-right:1px solid #e0e0e0}.history-item{padding:12px;margin-bottom:8px;background-color:#fff;cursor:pointer;transition:background-color .2s}.history-item:hover{background-color:#e8e8e8}.history-item.active{background-color:#e3f2fd;border-left:3px solid #2196f3}.history-title{font-size:14px;color:#333;margin-bottom:4px}.history-time{font-size:12px;color:#666}.chat-main{overflow:hidden;flex:1;display:flex;flex-direction:column}.chat-header{padding:16px;border-bottom:1px solid #e0e0e0;display:flex;justify-content:flex-end;gap:.5rem}.history-button{padding:8px 16px;background-color:#f0f0f0;border:1px solid #e0e0e0;border-radius:4px;cursor:pointer;font-size:14px;color:#333}.history-button:hover{background-color:#e0e0e0}.chat-messages{flex:1;overflow-y:auto;padding:1rem;display:flex;flex-direction:column;gap:1rem}.message{display:flex;max-width:80%}.message.human{margin-left:auto}.message.ai{margin-right:auto}.message-content{padding:.75rem 1rem;border-radius:8px;border:1px solid #e5e7eb;display:flex;flex-direction:column;gap:.5rem;max-width:100%}.message-text{word-break:break-word;line-height:1.5}.message-image{margin:.5rem 0}.message-image img{max-width:100%;border-radius:4px;box-shadow:0 1px 3px #0000001a}.message-audio{margin:.5rem 0}.message-audio audio{width:100%;max-width:300px}.message-meta{display:flex;align-items:center;gap:.75rem;font-size:.75rem;color:#6b7280;margin-top:.25rem}.message-time{font-family:monospace}.token-info{display:flex;gap:.5rem;align-items:center}.token-item{display:flex;align-items:center;gap:.25rem;background-color:#fff;padding:.125rem .375rem;border-radius:4px;font-family:monospace}.token-emoji{font-size:.875rem}.message.human .message-content{background-color:#3b82f6;color:#fff;border-color:#3b82f6}.message.human .message-meta{color:#fffc}.message.ai .message-content{color:#1f2937}.message.tool{width:100%;max-width:100%}.tool-message{width:100%;border:1px solid #e5e7eb;border-radius:8px;overflow:hidden}.tool-header{display:flex;align-items:center;justify-content:space-between;padding:.5rem 1rem;background-color:#f9fafb;border-bottom:1px solid #e5e7eb;cursor:pointer}.tool-header:hover{background-color:#f3f4f6}.tool-title{font-weight:500;color:#374151}.tool-content{padding:1rem}.tool-input{background-color:#f9fafb;padding:.75rem;border-radius:4px;margin-bottom:.5rem;font-family:monospace;white-space:pre-wrap;word-break:break-all}.tool-output{background-color:#fff;padding:.75rem;border-radius:4px;font-family:monospace;border:1px solid #e5e7eb;margin-bottom:.5rem}.chat-input{border-top:1px solid #e5e7eb;padding:.5rem 1rem 1rem;background-color:#fff}.chat-input-header{display:flex;align-items:center;gap:.5rem;margin-bottom:.5rem}.input-container{display:flex;gap:.5rem}.input-textarea{flex:1;padding:.75rem;border:1px solid #e5e7eb;border-radius:8px;resize:none;font-size:.875rem;line-height:1.25rem}.input-textarea:focus{outline:none;border-color:#3b82f6}.send-button{padding:.5rem 1rem;background-color:#3b82f6;color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:500;transition:all .2s}.send-button:hover{background-color:#2563eb}.send-button:disabled{background-color:#93c5fd;cursor:not-allowed}.send-button.interrupt{background-color:#ef4444}.send-button.interrupt:hover{background-color:#dc2626}.collapsed .tool-content{display:none}.expand-icon{transition:transform .2s}.collapsed .expand-icon{transform:rotate(-90deg)}.loading-indicator{padding:12px 16px;margin:8px 0;background-color:#f5f5f5;border-radius:8px;color:#666;font-size:14px;text-align:center;animation:pulse 1.5s infinite;display:flex;align-items:center;justify-content:center;gap:12px}.interrupt-button{padding:4px 12px;background-color:#ef4444;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:12px;transition:background-color .2s}.interrupt-button:hover{background-color:#dc2626}@keyframes pulse{0%{opacity:.6}50%{opacity:1}to{opacity:.6}}.error-message{padding:12px 16px;margin:8px 0;background-color:#fee2e2;border:1px solid #fecaca;border-radius:8px;color:#dc2626;font-size:14px;text-align:center}.markdown-body p{text-align:left}.edit-params-button{padding:6px 12px;background-color:#f0f0f0;border:1px solid #e0e0e0;border-radius:4px;cursor:pointer;font-size:.8rem;color:#333;white-space:nowrap}.edit-params-button:hover{background-color:#e0e0e0}.file-list{display:flex;gap:.5rem;border-radius:8px;flex:1}.file-list-header{margin-bottom:1rem}.file-upload-button{display:inline-flex;align-items:center;justify-content:center;width:80px;height:80px;color:#929292;background-color:#ebebeb;border-radius:6px;cursor:pointer;transition:all .2s}.file-upload-button svg{width:32px;height:32px}.file-upload-button.empty{width:32px;height:32px}.file-upload-button.empty svg{width:20px;height:20px}.file-list-content{display:flex;flex-wrap:wrap;gap:.5rem}.file-item{position:relative;width:80px;height:80px;border-radius:6px;overflow:hidden}.file-item img{border:1px solid #e5e7eb;overflow:hidden}.file-preview{width:100%;height:100%;object-fit:cover}.file-info{padding:.75rem}.file-name{display:block;font-size:.875rem;color:#374151;margin-bottom:.5rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.file-actions{display:flex;gap:.5rem}.upload-button,.remove-button{flex:1;padding:.375rem .75rem;border:none;border-radius:4px;font-size:.75rem;cursor:pointer;transition:all .2s}.upload-button{color:#000}.upload-button:hover{background-color:#2563eb}.remove-button{position:absolute;top:2px;right:2px;width:20px;height:20px;background-color:#00000080;color:#fff;border:none;border-radius:50%;cursor:pointer;font-size:16px;line-height:1;display:flex;align-items:center;justify-content:center;transition:background-color .2s}.remove-button:hover{background-color:#000000b3}.upload-button:disabled,.remove-button:disabled{opacity:.5;cursor:not-allowed}.json-editor-popup-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background-color:#00000080;display:flex;align-items:center;justify-content:center;z-index:1000}.json-editor-popup-content{background-color:#fff;padding:20px;border-radius:8px;box-shadow:0 2px 10px #0000001a;width:80%;max-width:600px;display:flex;flex-direction:column}.json-editor-popup-content h2{margin-top:0;margin-bottom:15px;font-size:1.5rem;color:#333}.json-editor-popup-content textarea{width:100%;padding:10px;border:1px solid #ccc;border-radius:4px;font-family:monospace;font-size:.9rem;resize:vertical;box-sizing:border-box}.json-editor-popup-content .error-message{color:red;font-size:.8rem;margin-top:5px;margin-bottom:10px}.popup-actions{display:flex;justify-content:flex-end;margin-top:15px;gap:10px}.popup-actions button{padding:8px 16px;border:none;border-radius:4px;cursor:pointer;font-size:.9rem}.popup-actions .save-button{background-color:#3b82f6;color:#fff}.popup-actions .save-button:hover{background-color:#2563eb}.popup-actions .cancel-button{background-color:#e5e7eb;color:#374151}.popup-actions .cancel-button:hover{background-color:#d1d5db}.json-to-message-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background-color:#00000080;display:flex;align-items:center;justify-content:center;z-index:1000}.json-to-message-content{background-color:#fff;border-radius:8px;box-shadow:0 2px 10px #0003;width:90%;max-width:1200px;height:80vh;max-height:800px;display:flex;flex-direction:column;overflow:hidden}.json-to-message-header{display:flex;justify-content:space-between;align-items:center;padding:15px 20px;border-bottom:1px solid #eaeaea}.json-to-message-header h2{margin:0;font-size:1.5rem;color:#333}.close-button{background:none;border:none;font-size:1.5rem;cursor:pointer;color:#666}.close-button:hover{color:#000}.json-to-message-body{display:flex;flex:1;overflow:hidden}.json-editor-pane{flex:1;padding:15px;display:flex;flex-direction:column;border-right:1px solid #eaeaea}.json-editor-pane textarea{flex:1;font-family:Monaco,Menlo,Ubuntu Mono,Consolas,monospace;font-size:.9rem;padding:10px;border:1px solid #ddd;border-radius:4px;resize:none}.error-message{color:#e53e3e;margin-top:10px;font-size:.9rem}.message-preview-pane{flex:1;padding:15px;overflow-y:auto;background-color:#f9f9f9}.preview-container{padding:10px;background-color:#fff;border-radius:4px;min-height:100%}.no-preview{display:flex;align-items:center;justify-content:center;height:100%;color:#666;font-style:italic}.login-container{max-width:600px;margin:2rem auto;padding:2rem;background:#fff;border-radius:8px;box-shadow:0 2px 4px #0000001a}.header-group{display:flex;gap:1rem;align-items:flex-start;padding:1rem;background:#f8f9fa;border-radius:4px;position:relative}.form-group{flex:1}.form-group label{display:block;margin-bottom:.5rem;color:#333;font-weight:500}.form-group input{width:100%;padding:.5rem;border:1px solid #ddd;border-radius:4px;font-size:1rem}.form-group input:focus{outline:none;border-color:#007bff;box-shadow:0 0 0 2px #007bff40}.button-group{display:flex;gap:1rem;margin-top:1rem}button{padding:.5rem 1rem;border:none;border-radius:4px;font-size:1rem;cursor:pointer;transition:background-color .2s}button[type=submit]{background-color:#007bff;color:#fff}button[type=submit]:hover{background-color:#0056b3}button[type=button]{background-color:#6c757d;color:#fff}button[type=button]:hover{background-color:#5a6268}.remove-header{background-color:#dc3545;color:#fff;padding:.25rem .5rem;font-size:.875rem}.remove-header:hover{background-color:#c82333}p{margin-bottom:1.5rem;color:#666;text-align:center}