@langgraph-js/ui 1.2.0 → 1.3.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.
- package/LICENSE +201 -201
- package/README.md +6 -6
- package/cli.mjs +36 -36
- package/dist/assets/index-88Ds6iYK.js +214 -0
- package/dist/assets/index-_qsGvf6N.css +1 -0
- package/dist/index.html +14 -14
- package/index.html +22 -22
- package/package.json +10 -9
- package/src/chat/Chat.tsx +169 -164
- package/src/chat/FileUpload/index.ts +105 -105
- package/src/chat/chat.css +403 -403
- package/src/chat/components/FileList.css +128 -128
- package/src/chat/components/FileList.tsx +73 -73
- package/src/chat/components/HistoryList.tsx +192 -192
- package/src/chat/components/JsonEditorPopup.css +80 -80
- package/src/chat/components/JsonEditorPopup.tsx +56 -56
- package/src/chat/components/JsonToMessage/JsonToMessage.css +104 -0
- package/src/chat/components/JsonToMessage/JsonToMessage.tsx +114 -0
- package/src/chat/components/JsonToMessage/JsonToMessageButton.tsx +27 -0
- package/src/chat/components/JsonToMessage/index.tsx +5 -0
- package/src/chat/components/MessageAI.tsx +24 -24
- package/src/chat/components/MessageBox.tsx +39 -0
- package/src/chat/components/MessageHuman.tsx +55 -55
- package/src/chat/components/MessageTool.tsx +46 -46
- package/src/chat/components/UsageMetadata.tsx +40 -40
- package/src/chat/context/ChatContext.tsx +32 -29
- package/src/chat/context/ExtraParamsContext.tsx +41 -41
- package/src/chat/store/index.ts +38 -24
- package/src/chat/tools.ts +33 -33
- package/src/chat/types.ts +16 -16
- package/src/graph/GraphPanel.tsx +5 -0
- package/src/graph/flattenGraph.ts +45 -0
- package/src/graph/flow.css +176 -0
- package/src/graph/index.tsx +161 -0
- package/src/hooks/useLocalStorage.ts +27 -27
- package/src/index.ts +1 -1
- package/src/login/Login.css +93 -93
- package/src/login/Login.tsx +92 -92
- package/test/App.tsx +9 -9
- package/test/main.tsx +5 -5
- package/test/vite-env.d.ts +1 -1
- package/tsconfig.json +21 -21
- package/tsconfig.node.json +9 -9
- package/vite.config.ts +22 -17
- package/dist/assets/index-BHPbGlnP.js +0 -192
- package/dist/assets/index-Du4LMUX2.css +0 -1
|
@@ -0,0 +1,161 @@
|
|
|
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 } = 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
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div style={{ width: "30%", height: "100%", position: "relative", overflow: "hidden" }}>
|
|
80
|
+
<ReactFlow nodes={nodes} edges={edges} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} fitView className="w-full h-full" nodeTypes={nodeTypes}>
|
|
81
|
+
<Background />
|
|
82
|
+
<Controls />
|
|
83
|
+
<Panel position="top-right">
|
|
84
|
+
<button onClick={() => onLayout("TB")}>垂直布局</button>
|
|
85
|
+
<button onClick={() => onLayout("LR")}>水平布局</button>
|
|
86
|
+
</Panel>
|
|
87
|
+
</ReactFlow>
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const getLayoutedElements = (nodes: Node[], edges: Edge[], options: { direction: "TB" | "LR" }) => {
|
|
93
|
+
const g = new Dagre.graphlib.Graph({ compound: true, multigraph: true }).setDefaultEdgeLabel(() => ({}));
|
|
94
|
+
g.setGraph({ rankdir: options.direction, nodesep: 40, ranksep: 40, edgesep: 20 });
|
|
95
|
+
|
|
96
|
+
nodes.forEach((node) => {
|
|
97
|
+
if (node.type === "group") {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
g.setNode(node.id, {
|
|
101
|
+
...node,
|
|
102
|
+
width: 128,
|
|
103
|
+
height: 20,
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
edges.forEach((edge) => g.setEdge(edge.source, edge.target));
|
|
107
|
+
|
|
108
|
+
Dagre.layout(g);
|
|
109
|
+
const newNodes = nodes.map((node) => {
|
|
110
|
+
const position = g.node(node.id);
|
|
111
|
+
if (!position) return { ...node };
|
|
112
|
+
let x = 0;
|
|
113
|
+
let y = 0;
|
|
114
|
+
|
|
115
|
+
x = position.x + (position?.width ?? 0);
|
|
116
|
+
y = position.y + (position?.height ?? 0);
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
...node,
|
|
120
|
+
position: { x, y },
|
|
121
|
+
_p: position,
|
|
122
|
+
width: position.width,
|
|
123
|
+
height: position.height,
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const children = new Map<string, Node[]>();
|
|
128
|
+
const padding = 15;
|
|
129
|
+
[...newNodes].reverse().forEach((node) => {
|
|
130
|
+
if (node.type === "group") {
|
|
131
|
+
const nodes = children.get(node.id!) || [];
|
|
132
|
+
const minX = Math.min(...nodes.map((i) => i.position.x));
|
|
133
|
+
const minY = Math.min(...nodes.map((i) => i.position.y));
|
|
134
|
+
const maxX = Math.max(...nodes.map((i) => i.position.x + (i.width ?? 0)));
|
|
135
|
+
const maxY = Math.max(...nodes.map((i) => i.position.y + (i.height ?? 0)));
|
|
136
|
+
node.position.x = minX - padding;
|
|
137
|
+
node.position.y = minY - padding;
|
|
138
|
+
node.width = maxX - minX + padding * 2;
|
|
139
|
+
node.height = maxY - minY + padding * 2;
|
|
140
|
+
nodes.forEach((i) => {
|
|
141
|
+
i.position.x = i.position.x - node.position.x;
|
|
142
|
+
i.position.y = i.position.y - node.position.y;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
if (node.parentId) {
|
|
146
|
+
children.set(node.parentId!, [...(children.get(node.parentId!) ?? []), node]);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
return {
|
|
150
|
+
nodes: newNodes,
|
|
151
|
+
edges,
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export function Graph() {
|
|
156
|
+
return (
|
|
157
|
+
<ReactFlowProvider>
|
|
158
|
+
<LayoutFlow />
|
|
159
|
+
</ReactFlowProvider>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
import { useState, useEffect } from "react";
|
|
2
|
-
|
|
3
|
-
function useLocalStorage<T>(key: string, initialValue: T) {
|
|
4
|
-
// 从 localStorage 获取初始值
|
|
5
|
-
const [storedValue, setStoredValue] = useState<T>(() => {
|
|
6
|
-
try {
|
|
7
|
-
const item = window.localStorage.getItem(key);
|
|
8
|
-
return item ? JSON.parse(item) : initialValue;
|
|
9
|
-
} catch (error) {
|
|
10
|
-
console.error("Error reading from localStorage:", error);
|
|
11
|
-
return initialValue;
|
|
12
|
-
}
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
// 当值改变时,自动同步到 localStorage
|
|
16
|
-
useEffect(() => {
|
|
17
|
-
try {
|
|
18
|
-
window.localStorage.setItem(key, JSON.stringify(storedValue));
|
|
19
|
-
} catch (error) {
|
|
20
|
-
console.error("Error writing to localStorage:", error);
|
|
21
|
-
}
|
|
22
|
-
}, [key, storedValue]);
|
|
23
|
-
|
|
24
|
-
return [storedValue, setStoredValue] as const;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export default useLocalStorage;
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
function useLocalStorage<T>(key: string, initialValue: T) {
|
|
4
|
+
// 从 localStorage 获取初始值
|
|
5
|
+
const [storedValue, setStoredValue] = useState<T>(() => {
|
|
6
|
+
try {
|
|
7
|
+
const item = window.localStorage.getItem(key);
|
|
8
|
+
return item ? JSON.parse(item) : initialValue;
|
|
9
|
+
} catch (error) {
|
|
10
|
+
console.error("Error reading from localStorage:", error);
|
|
11
|
+
return initialValue;
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// 当值改变时,自动同步到 localStorage
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
try {
|
|
18
|
+
window.localStorage.setItem(key, JSON.stringify(storedValue));
|
|
19
|
+
} catch (error) {
|
|
20
|
+
console.error("Error writing to localStorage:", error);
|
|
21
|
+
}
|
|
22
|
+
}, [key, storedValue]);
|
|
23
|
+
|
|
24
|
+
return [storedValue, setStoredValue] as const;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default useLocalStorage;
|
package/src/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { default as Chat } from "./chat/Chat";
|
|
1
|
+
export { default as Chat } from "./chat/Chat";
|
package/src/login/Login.css
CHANGED
|
@@ -1,93 +1,93 @@
|
|
|
1
|
-
.login-container {
|
|
2
|
-
max-width: 600px;
|
|
3
|
-
margin: 2rem auto;
|
|
4
|
-
padding: 2rem;
|
|
5
|
-
background: #fff;
|
|
6
|
-
border-radius: 8px;
|
|
7
|
-
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
.header-group {
|
|
11
|
-
display: flex;
|
|
12
|
-
gap: 1rem;
|
|
13
|
-
align-items: flex-start;
|
|
14
|
-
padding: 1rem;
|
|
15
|
-
background: #f8f9fa;
|
|
16
|
-
border-radius: 4px;
|
|
17
|
-
position: relative;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
.form-group {
|
|
21
|
-
flex: 1;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
.form-group label {
|
|
25
|
-
display: block;
|
|
26
|
-
margin-bottom: 0.5rem;
|
|
27
|
-
color: #333;
|
|
28
|
-
font-weight: 500;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
.form-group input {
|
|
32
|
-
width: 100%;
|
|
33
|
-
padding: 0.5rem;
|
|
34
|
-
border: 1px solid #ddd;
|
|
35
|
-
border-radius: 4px;
|
|
36
|
-
font-size: 1rem;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
.form-group input:focus {
|
|
40
|
-
outline: none;
|
|
41
|
-
border-color: #007bff;
|
|
42
|
-
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
.button-group {
|
|
46
|
-
display: flex;
|
|
47
|
-
gap: 1rem;
|
|
48
|
-
margin-top: 1rem;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
button {
|
|
52
|
-
padding: 0.5rem 1rem;
|
|
53
|
-
border: none;
|
|
54
|
-
border-radius: 4px;
|
|
55
|
-
font-size: 1rem;
|
|
56
|
-
cursor: pointer;
|
|
57
|
-
transition: background-color 0.2s;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
button[type="submit"] {
|
|
61
|
-
background-color: #007bff;
|
|
62
|
-
color: white;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
button[type="submit"]:hover {
|
|
66
|
-
background-color: #0056b3;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
button[type="button"] {
|
|
70
|
-
background-color: #6c757d;
|
|
71
|
-
color: white;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
button[type="button"]:hover {
|
|
75
|
-
background-color: #5a6268;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
.remove-header {
|
|
79
|
-
background-color: #dc3545;
|
|
80
|
-
color: white;
|
|
81
|
-
padding: 0.25rem 0.5rem;
|
|
82
|
-
font-size: 0.875rem;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
.remove-header:hover {
|
|
86
|
-
background-color: #c82333;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
p {
|
|
90
|
-
margin-bottom: 1.5rem;
|
|
91
|
-
color: #666;
|
|
92
|
-
text-align: center;
|
|
93
|
-
}
|
|
1
|
+
.login-container {
|
|
2
|
+
max-width: 600px;
|
|
3
|
+
margin: 2rem auto;
|
|
4
|
+
padding: 2rem;
|
|
5
|
+
background: #fff;
|
|
6
|
+
border-radius: 8px;
|
|
7
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.header-group {
|
|
11
|
+
display: flex;
|
|
12
|
+
gap: 1rem;
|
|
13
|
+
align-items: flex-start;
|
|
14
|
+
padding: 1rem;
|
|
15
|
+
background: #f8f9fa;
|
|
16
|
+
border-radius: 4px;
|
|
17
|
+
position: relative;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.form-group {
|
|
21
|
+
flex: 1;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.form-group label {
|
|
25
|
+
display: block;
|
|
26
|
+
margin-bottom: 0.5rem;
|
|
27
|
+
color: #333;
|
|
28
|
+
font-weight: 500;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.form-group input {
|
|
32
|
+
width: 100%;
|
|
33
|
+
padding: 0.5rem;
|
|
34
|
+
border: 1px solid #ddd;
|
|
35
|
+
border-radius: 4px;
|
|
36
|
+
font-size: 1rem;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.form-group input:focus {
|
|
40
|
+
outline: none;
|
|
41
|
+
border-color: #007bff;
|
|
42
|
+
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.button-group {
|
|
46
|
+
display: flex;
|
|
47
|
+
gap: 1rem;
|
|
48
|
+
margin-top: 1rem;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
button {
|
|
52
|
+
padding: 0.5rem 1rem;
|
|
53
|
+
border: none;
|
|
54
|
+
border-radius: 4px;
|
|
55
|
+
font-size: 1rem;
|
|
56
|
+
cursor: pointer;
|
|
57
|
+
transition: background-color 0.2s;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
button[type="submit"] {
|
|
61
|
+
background-color: #007bff;
|
|
62
|
+
color: white;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
button[type="submit"]:hover {
|
|
66
|
+
background-color: #0056b3;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
button[type="button"] {
|
|
70
|
+
background-color: #6c757d;
|
|
71
|
+
color: white;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
button[type="button"]:hover {
|
|
75
|
+
background-color: #5a6268;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.remove-header {
|
|
79
|
+
background-color: #dc3545;
|
|
80
|
+
color: white;
|
|
81
|
+
padding: 0.25rem 0.5rem;
|
|
82
|
+
font-size: 0.875rem;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.remove-header:hover {
|
|
86
|
+
background-color: #c82333;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
p {
|
|
90
|
+
margin-bottom: 1.5rem;
|
|
91
|
+
color: #666;
|
|
92
|
+
text-align: center;
|
|
93
|
+
}
|
package/src/login/Login.tsx
CHANGED
|
@@ -1,92 +1,92 @@
|
|
|
1
|
-
import React, { useState } from "react";
|
|
2
|
-
import "./Login.css";
|
|
3
|
-
|
|
4
|
-
interface HeaderConfig {
|
|
5
|
-
key: string;
|
|
6
|
-
value: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const Login: React.FC = () => {
|
|
10
|
-
const [headers, setHeaders] = useState<HeaderConfig[]>([{ key: "authorization", value: "" }]);
|
|
11
|
-
const [withCredentials, setWithCredentials] = useState<boolean>(localStorage.getItem("withCredentials") === "true");
|
|
12
|
-
const [apiUrl, setApiUrl] = useState<string>(localStorage.getItem("apiUrl") || "");
|
|
13
|
-
|
|
14
|
-
const addHeader = () => {
|
|
15
|
-
setHeaders([...headers, { key: "", value: "" }]);
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
const removeHeader = (index: number) => {
|
|
19
|
-
setHeaders(headers.filter((_, i) => i !== index));
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const updateHeader = (index: number, field: "key" | "value", value: string) => {
|
|
23
|
-
const newHeaders = [...headers];
|
|
24
|
-
newHeaders[index][field] = value;
|
|
25
|
-
setHeaders(newHeaders);
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const handleLogin = () => {
|
|
29
|
-
const headerObject = Object.fromEntries(headers.map((k) => [k.key, k.value]));
|
|
30
|
-
|
|
31
|
-
localStorage.setItem("code", JSON.stringify(headerObject));
|
|
32
|
-
localStorage.setItem("withCredentials", JSON.stringify(withCredentials));
|
|
33
|
-
localStorage.setItem("apiUrl", apiUrl);
|
|
34
|
-
location.reload();
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
return (
|
|
38
|
-
<div className="login-container">
|
|
39
|
-
<form
|
|
40
|
-
onSubmit={(e) => {
|
|
41
|
-
e.preventDefault();
|
|
42
|
-
handleLogin();
|
|
43
|
-
}}
|
|
44
|
-
>
|
|
45
|
-
<h2>LangGraph UI</h2>
|
|
46
|
-
<p>登录,自定义请求头配置</p>
|
|
47
|
-
|
|
48
|
-
<div className="form-group api-url-group">
|
|
49
|
-
<label htmlFor="api-url">API URL</label>
|
|
50
|
-
<input type="text" id="api-url" value={apiUrl} onChange={(e) => setApiUrl(e.target.value)} placeholder="例如: http://localhost:8123" />
|
|
51
|
-
</div>
|
|
52
|
-
|
|
53
|
-
{headers.map((header, index) => (
|
|
54
|
-
<div key={index} className="header-group">
|
|
55
|
-
<div className="form-group">
|
|
56
|
-
<input type="text" id={`header-key-${index}`} value={header.key} onChange={(e) => updateHeader(index, "key", e.target.value)} placeholder="例如: authorization" required />
|
|
57
|
-
</div>
|
|
58
|
-
<div className="form-group">
|
|
59
|
-
<input
|
|
60
|
-
type="text"
|
|
61
|
-
id={`header-value-${index}`}
|
|
62
|
-
value={header.value}
|
|
63
|
-
onChange={(e) => updateHeader(index, "value", e.target.value)}
|
|
64
|
-
placeholder="例如: Bearer token;无则填 1"
|
|
65
|
-
required
|
|
66
|
-
/>
|
|
67
|
-
</div>
|
|
68
|
-
{index > 0 && (
|
|
69
|
-
<button type="button" className="remove-header" onClick={() => removeHeader(index)}>
|
|
70
|
-
删除
|
|
71
|
-
</button>
|
|
72
|
-
)}
|
|
73
|
-
</div>
|
|
74
|
-
))}
|
|
75
|
-
<div className="with-credentials-option">
|
|
76
|
-
<label>
|
|
77
|
-
<input type="checkbox" checked={withCredentials} onChange={(e) => setWithCredentials(e.target.checked)} />
|
|
78
|
-
启用 withCredentials(跨域请求时发送 Cookie)
|
|
79
|
-
</label>
|
|
80
|
-
</div>
|
|
81
|
-
<div className="button-group">
|
|
82
|
-
<button type="button" onClick={addHeader}>
|
|
83
|
-
添加请求头
|
|
84
|
-
</button>
|
|
85
|
-
<button type="submit">保存配置</button>
|
|
86
|
-
</div>
|
|
87
|
-
</form>
|
|
88
|
-
</div>
|
|
89
|
-
);
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
export default Login;
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import "./Login.css";
|
|
3
|
+
|
|
4
|
+
interface HeaderConfig {
|
|
5
|
+
key: string;
|
|
6
|
+
value: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const Login: React.FC = () => {
|
|
10
|
+
const [headers, setHeaders] = useState<HeaderConfig[]>([{ key: "authorization", value: "" }]);
|
|
11
|
+
const [withCredentials, setWithCredentials] = useState<boolean>(localStorage.getItem("withCredentials") === "true");
|
|
12
|
+
const [apiUrl, setApiUrl] = useState<string>(localStorage.getItem("apiUrl") || "");
|
|
13
|
+
|
|
14
|
+
const addHeader = () => {
|
|
15
|
+
setHeaders([...headers, { key: "", value: "" }]);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const removeHeader = (index: number) => {
|
|
19
|
+
setHeaders(headers.filter((_, i) => i !== index));
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const updateHeader = (index: number, field: "key" | "value", value: string) => {
|
|
23
|
+
const newHeaders = [...headers];
|
|
24
|
+
newHeaders[index][field] = value;
|
|
25
|
+
setHeaders(newHeaders);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const handleLogin = () => {
|
|
29
|
+
const headerObject = Object.fromEntries(headers.map((k) => [k.key, k.value]));
|
|
30
|
+
|
|
31
|
+
localStorage.setItem("code", JSON.stringify(headerObject));
|
|
32
|
+
localStorage.setItem("withCredentials", JSON.stringify(withCredentials));
|
|
33
|
+
localStorage.setItem("apiUrl", apiUrl);
|
|
34
|
+
location.reload();
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className="login-container">
|
|
39
|
+
<form
|
|
40
|
+
onSubmit={(e) => {
|
|
41
|
+
e.preventDefault();
|
|
42
|
+
handleLogin();
|
|
43
|
+
}}
|
|
44
|
+
>
|
|
45
|
+
<h2>LangGraph UI</h2>
|
|
46
|
+
<p>登录,自定义请求头配置</p>
|
|
47
|
+
|
|
48
|
+
<div className="form-group api-url-group">
|
|
49
|
+
<label htmlFor="api-url">API URL</label>
|
|
50
|
+
<input type="text" id="api-url" value={apiUrl} onChange={(e) => setApiUrl(e.target.value)} placeholder="例如: http://localhost:8123" />
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
{headers.map((header, index) => (
|
|
54
|
+
<div key={index} className="header-group">
|
|
55
|
+
<div className="form-group">
|
|
56
|
+
<input type="text" id={`header-key-${index}`} value={header.key} onChange={(e) => updateHeader(index, "key", e.target.value)} placeholder="例如: authorization" required />
|
|
57
|
+
</div>
|
|
58
|
+
<div className="form-group">
|
|
59
|
+
<input
|
|
60
|
+
type="text"
|
|
61
|
+
id={`header-value-${index}`}
|
|
62
|
+
value={header.value}
|
|
63
|
+
onChange={(e) => updateHeader(index, "value", e.target.value)}
|
|
64
|
+
placeholder="例如: Bearer token;无则填 1"
|
|
65
|
+
required
|
|
66
|
+
/>
|
|
67
|
+
</div>
|
|
68
|
+
{index > 0 && (
|
|
69
|
+
<button type="button" className="remove-header" onClick={() => removeHeader(index)}>
|
|
70
|
+
删除
|
|
71
|
+
</button>
|
|
72
|
+
)}
|
|
73
|
+
</div>
|
|
74
|
+
))}
|
|
75
|
+
<div className="with-credentials-option">
|
|
76
|
+
<label>
|
|
77
|
+
<input type="checkbox" checked={withCredentials} onChange={(e) => setWithCredentials(e.target.checked)} />
|
|
78
|
+
启用 withCredentials(跨域请求时发送 Cookie)
|
|
79
|
+
</label>
|
|
80
|
+
</div>
|
|
81
|
+
<div className="button-group">
|
|
82
|
+
<button type="button" onClick={addHeader}>
|
|
83
|
+
添加请求头
|
|
84
|
+
</button>
|
|
85
|
+
<button type="submit">保存配置</button>
|
|
86
|
+
</div>
|
|
87
|
+
</form>
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export default Login;
|
package/test/App.tsx
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import Chat from "../src/chat/Chat";
|
|
2
|
-
import Login from "../src/login/Login";
|
|
3
|
-
import { useState } from "react";
|
|
4
|
-
function App() {
|
|
5
|
-
const [isLogin, setIsLogin] = useState(localStorage.getItem("code"));
|
|
6
|
-
return <>{isLogin ? <Chat /> : <Login></Login>}</>;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export default App;
|
|
1
|
+
import Chat from "../src/chat/Chat";
|
|
2
|
+
import Login from "../src/login/Login";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
function App() {
|
|
5
|
+
const [isLogin, setIsLogin] = useState(localStorage.getItem("code"));
|
|
6
|
+
return <>{isLogin ? <Chat /> : <Login></Login>}</>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default App;
|
package/test/main.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import ReactDOM from "react-dom/client";
|
|
3
|
-
import App from "./App";
|
|
4
|
-
|
|
5
|
-
ReactDOM.createRoot(document.getElementById("root")!).render(<App />);
|
|
1
|
+
import React from "react";
|
|
2
|
+
import ReactDOM from "react-dom/client";
|
|
3
|
+
import App from "./App";
|
|
4
|
+
|
|
5
|
+
ReactDOM.createRoot(document.getElementById("root")!).render(<App />);
|
package/test/vite-env.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
/// <reference types="vite/client" />
|
|
1
|
+
/// <reference types="vite/client" />
|
package/tsconfig.json
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ESNext",
|
|
4
|
-
"useDefineForClassFields": true,
|
|
5
|
-
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
|
6
|
-
"allowJs": false,
|
|
7
|
-
"skipLibCheck": true,
|
|
8
|
-
"esModuleInterop": false,
|
|
9
|
-
"allowSyntheticDefaultImports": true,
|
|
10
|
-
"strict": true,
|
|
11
|
-
"forceConsistentCasingInFileNames": true,
|
|
12
|
-
"module": "ESNext",
|
|
13
|
-
"moduleResolution": "Node",
|
|
14
|
-
"resolveJsonModule": true,
|
|
15
|
-
"isolatedModules": true,
|
|
16
|
-
"noEmit": true,
|
|
17
|
-
"jsx": "react-jsx"
|
|
18
|
-
},
|
|
19
|
-
"include": ["test", "test/**/*.tsx"],
|
|
20
|
-
"references": [{ "path": "./tsconfig.node.json" }]
|
|
21
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
|
6
|
+
"allowJs": false,
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"esModuleInterop": false,
|
|
9
|
+
"allowSyntheticDefaultImports": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"module": "ESNext",
|
|
13
|
+
"moduleResolution": "Node",
|
|
14
|
+
"resolveJsonModule": true,
|
|
15
|
+
"isolatedModules": true,
|
|
16
|
+
"noEmit": true,
|
|
17
|
+
"jsx": "react-jsx"
|
|
18
|
+
},
|
|
19
|
+
"include": ["test", "test/**/*.tsx"],
|
|
20
|
+
"references": [{ "path": "./tsconfig.node.json" }]
|
|
21
|
+
}
|