@langgraph-js/ui 1.0.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,192 @@
1
+ import React from "react";
2
+ import { useChat } from "../context/ChatContext";
3
+
4
+ interface HistoryListProps {
5
+ onClose: () => void;
6
+ formatTime: (date: Date) => string;
7
+ }
8
+
9
+ const HistoryList: React.FC<HistoryListProps> = ({ onClose, formatTime }) => {
10
+ const { historyList, currentChatId, refreshHistoryList, createNewChat, deleteHistoryChat, toHistoryChat } = useChat();
11
+
12
+ return (
13
+ <div className="history-list">
14
+ <div className="history-header">
15
+ <div className="header-left">
16
+ <h3>历史记录</h3>
17
+ <button className="refresh-button" onClick={refreshHistoryList} title="刷新列表">
18
+ 🔁
19
+ </button>
20
+ </div>
21
+ <button className="close-button" onClick={onClose} title="关闭">
22
+
23
+ </button>
24
+ </div>
25
+ <div className="history-content">
26
+ <div
27
+ className="history-items"
28
+ onClick={() => {
29
+ createNewChat();
30
+ }}
31
+ >
32
+ <div className="history-item">
33
+ <div className="history-title"> New Chat</div>
34
+ </div>
35
+ </div>
36
+ {historyList.length === 0 ? (
37
+ <div className="empty-history">暂无历史记录</div>
38
+ ) : (
39
+ <div className="history-items">
40
+ {historyList
41
+ .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
42
+ .map((thread) => (
43
+ <div className={`history-item ${thread.thread_id === currentChatId ? "active" : ""}`} key={thread.thread_id}>
44
+ <div className="history-info">
45
+ <div className="history-title">{thread?.values?.messages?.[0]?.content as string}</div>
46
+ <div className="history-meta">
47
+ <span className="history-time">{formatTime(new Date(thread.created_at))}</span>
48
+ <span className="history-status">{thread.status}</span>
49
+ </div>
50
+ </div>
51
+ <div className="history-actions">
52
+ <button
53
+ className="action-button"
54
+ onClick={() => {
55
+ toHistoryChat(thread);
56
+ }}
57
+ title="恢复对话"
58
+ >
59
+
60
+ </button>
61
+ <button
62
+ className="action-button"
63
+ onClick={async () => {
64
+ await deleteHistoryChat(thread);
65
+ }}
66
+ title="删除对话"
67
+ >
68
+
69
+ </button>
70
+ </div>
71
+ </div>
72
+ ))}
73
+ </div>
74
+ )}
75
+ </div>
76
+ <style>{`
77
+ .history-list {
78
+ background: #fff;
79
+ border-radius: 8px;
80
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
81
+ height: 100%;
82
+ display: flex;
83
+ flex-direction: column;
84
+ }
85
+
86
+ .history-header {
87
+ padding: 16px;
88
+ border-bottom: 1px solid #eee;
89
+ display: flex;
90
+ justify-content: space-between;
91
+ align-items: center;
92
+ }
93
+
94
+ .header-left {
95
+ display: flex;
96
+ align-items: center;
97
+ gap: 12px;
98
+ }
99
+
100
+ .history-header h3 {
101
+ margin: 0;
102
+ font-size: 18px;
103
+ color: #333;
104
+ }
105
+
106
+ .history-content {
107
+ flex: 1;
108
+ overflow-y: auto;
109
+ padding: 16px;
110
+ }
111
+
112
+ .history-items {
113
+ display: flex;
114
+ flex-direction: column;
115
+ gap: 12px;
116
+ }
117
+
118
+ .history-item {
119
+ display: flex;
120
+ justify-content: space-between;
121
+ align-items: center;
122
+ padding: 12px;
123
+ border-radius: 6px;
124
+ background: #f8f9fa;
125
+ transition: all 0.2s ease;
126
+ }
127
+
128
+ .history-item:hover {
129
+ background: #f0f2f5;
130
+ }
131
+
132
+ .history-item.active {
133
+ background: #e6f7ff;
134
+ border: 1px solid #91d5ff;
135
+ }
136
+
137
+ .history-info {
138
+ flex: 1;
139
+ min-width: 0;
140
+ }
141
+
142
+ .history-title {
143
+ font-size: 14px;
144
+ color: #333;
145
+ margin-bottom: 4px;
146
+ white-space: nowrap;
147
+ overflow: hidden;
148
+ text-overflow: ellipsis;
149
+ }
150
+
151
+ .history-meta {
152
+ display: flex;
153
+ gap: 12px;
154
+ color: #666;
155
+ font-size: 12px;
156
+ }
157
+
158
+ .history-actions {
159
+ display: flex;
160
+ gap: 8px;
161
+ margin-left: 12px;
162
+ }
163
+
164
+ .action-button, .close-button, .refresh-button {
165
+ background: none;
166
+ border: none;
167
+ cursor: pointer;
168
+ padding: 6px;
169
+ font-size: 16px;
170
+ border-radius: 4px;
171
+ transition: all 0.2s ease;
172
+ display: flex;
173
+ align-items: center;
174
+ justify-content: center;
175
+ }
176
+
177
+ .action-button:hover, .close-button:hover, .refresh-button:hover {
178
+ background: rgba(0, 0, 0, 0.05);
179
+ transform: scale(1.1);
180
+ }
181
+
182
+ .empty-history {
183
+ text-align: center;
184
+ color: #999;
185
+ padding: 32px 0;
186
+ }
187
+ `}</style>
188
+ </div>
189
+ );
190
+ };
191
+
192
+ export default HistoryList;
@@ -0,0 +1,20 @@
1
+ import React from "react";
2
+ import { RenderMessage } from "@langgraph-js/sdk";
3
+ import { UsageMetadata } from "./UsageMetadata";
4
+ import { getMessageContent } from "@langgraph-js/sdk";
5
+ interface MessageAIProps {
6
+ message: RenderMessage;
7
+ }
8
+
9
+ const MessageAI: React.FC<MessageAIProps> = ({ message }) => {
10
+ return (
11
+ <div className="message ai">
12
+ <div className="message-content">
13
+ <div className="message-text">{getMessageContent(message.content)}</div>
14
+ {message.usage_metadata && <UsageMetadata usage_metadata={message.usage_metadata} spend_time={message.spend_time} />}
15
+ </div>
16
+ </div>
17
+ );
18
+ };
19
+
20
+ export default MessageAI;
@@ -0,0 +1,15 @@
1
+ import React from "react";
2
+
3
+ interface MessageHumanProps {
4
+ content: any;
5
+ }
6
+
7
+ const MessageHuman: React.FC<MessageHumanProps> = ({ content }) => {
8
+ return (
9
+ <div className="message human">
10
+ <div className="message-content">{typeof content === "string" ? content : JSON.stringify(content)}</div>
11
+ </div>
12
+ );
13
+ };
14
+
15
+ export default MessageHuman;
@@ -0,0 +1,47 @@
1
+ import React from "react";
2
+ import { LangGraphClient, RenderMessage, ToolMessage } from "@langgraph-js/sdk";
3
+ import { UsageMetadata } from "./UsageMetadata";
4
+ interface MessageToolProps {
5
+ message: ToolMessage & RenderMessage;
6
+ client: LangGraphClient;
7
+ getMessageContent: (content: any) => string;
8
+ formatTokens: (tokens: number) => string;
9
+ isCollapsed: boolean;
10
+ onToggleCollapse: () => void;
11
+ }
12
+
13
+ const MessageTool: React.FC<MessageToolProps> = ({ message, client, getMessageContent, formatTokens, isCollapsed, onToggleCollapse }) => {
14
+ console.log(message)
15
+ return (
16
+ <div className="message tool">
17
+ {message.name === "ask_user" && !message.additional_kwargs?.done && (
18
+ <div>
19
+ <div>询问 {message.tool_input}</div>
20
+ <input
21
+ type="text"
22
+ onKeyDown={(e) => {
23
+ if (e.key === "Enter") {
24
+ client.doneFEToolWaiting(message.id!, (e.target as any).value);
25
+ }
26
+ }}
27
+ />
28
+ </div>
29
+ )}
30
+ <div className="tool-message">
31
+ <div className="tool-header" onClick={onToggleCollapse}>
32
+ <div className="tool-title">{message.name}</div>
33
+ </div>
34
+
35
+ {!isCollapsed && (
36
+ <div className="tool-content">
37
+ <div className="tool-input">{message.tool_input}</div>
38
+ <div className="tool-output">{getMessageContent(message.content)}</div>
39
+ {message.usage_metadata && <UsageMetadata usage_metadata={message.usage_metadata} spend_time={message.spend_time} />}
40
+ </div>
41
+ )}
42
+ </div>
43
+ </div>
44
+ );
45
+ };
46
+
47
+ export default MessageTool;
@@ -0,0 +1,34 @@
1
+ interface UsageMetadataProps {
2
+ usage_metadata: Partial<{
3
+ input_tokens: number;
4
+ output_tokens: number;
5
+ total_tokens: number;
6
+ }>;
7
+ spend_time?: number;
8
+ }
9
+
10
+ export const UsageMetadata: React.FC<UsageMetadataProps> = ({ usage_metadata, spend_time }) => {
11
+ const formatTokens = (tokens: number) => {
12
+ return tokens.toString();
13
+ };
14
+
15
+ return (
16
+ <div className="message-meta">
17
+ <div className="token-info">
18
+ <span className="token-item">
19
+ <span className="token-emoji">📥</span>
20
+ {formatTokens(usage_metadata.input_tokens || 0)}
21
+ </span>
22
+ <span className="token-item">
23
+ <span className="token-emoji">📤</span>
24
+ {formatTokens(usage_metadata.output_tokens || 0)}
25
+ </span>
26
+ <span className="token-item">
27
+ <span className="token-emoji">📊</span>
28
+ {formatTokens(usage_metadata.total_tokens || 0)}
29
+ </span>
30
+ </div>
31
+ <span className="message-time">{spend_time ? `${(spend_time / 1000).toFixed(2)}s` : ""}</span>
32
+ </div>
33
+ );
34
+ };
@@ -0,0 +1,29 @@
1
+ import React, { createContext, useContext, useState, useCallback, ReactNode, useEffect } from "react";
2
+ type ChatContextType = UnionStore<typeof globalChatStore>;
3
+
4
+ const ChatContext = createContext<ChatContextType | undefined>(undefined);
5
+
6
+ export const useChat = () => {
7
+ const context = useContext(ChatContext);
8
+ if (!context) {
9
+ throw new Error("useChat must be used within a ChatProvider");
10
+ }
11
+ return context;
12
+ };
13
+
14
+ interface ChatProviderProps {
15
+ children: ReactNode;
16
+ }
17
+ import { globalChatStore } from "../store";
18
+ import { UnionStore, useUnionStore } from "@langgraph-js/sdk";
19
+ import { useStore } from "@nanostores/react";
20
+ export const ChatProvider: React.FC<ChatProviderProps> = ({ children }) => {
21
+ const store = useUnionStore(globalChatStore, useStore);
22
+ useEffect(() => {
23
+ store.initClient().then((res) => {
24
+ store.refreshHistoryList();
25
+ });
26
+ }, [store.currentAgent]);
27
+
28
+ return <ChatContext.Provider value={store}>{children}</ChatContext.Provider>;
29
+ };
@@ -0,0 +1,24 @@
1
+ import { createChatStore } from "@langgraph-js/sdk";
2
+ const F =
3
+ localStorage.getItem("withCredentials") === "true"
4
+ ? (url: string, options: RequestInit) => {
5
+ options.credentials = "include";
6
+ return fetch(url, options);
7
+ }
8
+ : fetch;
9
+ export const globalChatStore = createChatStore(
10
+ "agent",
11
+ {
12
+ apiUrl: localStorage.getItem("apiUrl") || "http://localhost:8123",
13
+ defaultHeaders: JSON.parse(localStorage.getItem("code") || "{}"),
14
+ callerOptions: {
15
+ // 携带 cookie 的写法
16
+ fetch: F,
17
+ },
18
+ },
19
+ {
20
+ onInit(client) {
21
+ client.tools.bindTools([]);
22
+ },
23
+ }
24
+ );
@@ -0,0 +1,33 @@
1
+ import { createFETool, ToolManager } from "@langgraph-js/sdk";
2
+
3
+ // 文件操作工具
4
+ export const fileTool = createFETool({
5
+ name: "file_operation",
6
+ description: "执行文件操作,包括读取和写入",
7
+ parameters: [
8
+ {
9
+ name: "filePath",
10
+ type: "string",
11
+ description: "文件的完整路径",
12
+ },
13
+ ],
14
+ returnDirect: true,
15
+ callbackMessage: () => [{ type: "ai", content: "工作完成" }],
16
+ async handler(args) {
17
+ await new Promise((resolve) => setTimeout(resolve, 3000));
18
+ return [{ type: "text", text: "执行文件操作 " + args.filePath }];
19
+ },
20
+ });
21
+
22
+ export const askUserTool = createFETool({
23
+ name: "ask_user",
24
+ description: "询问用户",
25
+ parameters: [
26
+ {
27
+ name: "question",
28
+ type: "string",
29
+ description: "问题",
30
+ },
31
+ ],
32
+ handler: ToolManager.waitForUIDone,
33
+ });
@@ -0,0 +1,16 @@
1
+ export interface Message {
2
+ content: string;
3
+ role: string;
4
+ name?: string;
5
+ metadata?: {
6
+ graph_id?: string;
7
+ };
8
+ thread_id?: string;
9
+ usage_metadata?: {
10
+ input_tokens: number;
11
+ output_tokens: number;
12
+ total_tokens: number;
13
+ };
14
+ spend_time?: number;
15
+ tool_input?: string;
16
+ }
@@ -0,0 +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;
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { default as Chat } from "./chat/Chat";
@@ -0,0 +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
+ }
@@ -0,0 +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;
package/test/App.tsx ADDED
@@ -0,0 +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;
package/test/main.tsx ADDED
@@ -0,0 +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 />);
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
package/tsconfig.json ADDED
@@ -0,0 +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
+ }