@langgraph-js/ui 2.0.0 → 2.1.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,517 @@
1
+ import { createUITool, ToolManager } from "@langgraph-js/sdk";
2
+ import Form, { IChangeEvent } from "@rjsf/core";
3
+ import ErrorBoundary from "../components/ErrorBoundary";
4
+ import { FileText, Eye, EyeOff, ChevronDown, X, Plus, Minus } from "lucide-react";
5
+ import { z } from "zod";
6
+ import validator from "@rjsf/validator-ajv8";
7
+ import { useState } from "react";
8
+
9
+ // 自定义文本输入组件
10
+ const CustomTextWidget = (props: any) => {
11
+ const hasMinLength = props.options?.minLength;
12
+ const hasMaxLength = props.options?.maxLength;
13
+ const currentLength = props.value?.length || 0;
14
+
15
+ return (
16
+ <div className="mb-4">
17
+ {props.label && (
18
+ <label className="block text-sm font-medium text-gray-700 mb-1">
19
+ {props.label}
20
+ {props.required && <span className="text-red-500 ml-1">*</span>}
21
+ </label>
22
+ )}
23
+ <input
24
+ type="text"
25
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
26
+ value={props.value || ""}
27
+ onChange={(e) => props.onChange(e.target.value)}
28
+ placeholder={props.placeholder}
29
+ disabled={props.disabled}
30
+ maxLength={hasMaxLength ? props.options.maxLength : undefined}
31
+ />
32
+ {props.description && <p className="text-xs text-gray-500 mt-1">{props.description}</p>}
33
+ {(hasMinLength || hasMaxLength) && (
34
+ <div className="flex justify-end mt-1">
35
+ <span className={`text-xs ${currentLength > (props.options?.maxLength || 0) ? "text-red-500" : "text-gray-400"}`}>
36
+ {hasMinLength && hasMaxLength
37
+ ? `${currentLength}/${props.options.maxLength} (最少${props.options.minLength})`
38
+ : hasMaxLength
39
+ ? `${currentLength}/${props.options.maxLength}`
40
+ : `最少${props.options.minLength}字符`}
41
+ </span>
42
+ </div>
43
+ )}
44
+ </div>
45
+ );
46
+ };
47
+
48
+ // 自定义密码输入组件
49
+ const CustomPasswordWidget = (props: any) => {
50
+ const [showPassword, setShowPassword] = useState(false);
51
+
52
+ return (
53
+ <div className="mb-4">
54
+ {props.label && (
55
+ <label className="block text-sm font-medium text-gray-700 mb-1">
56
+ {props.label}
57
+ {props.required && <span className="text-red-500 ml-1">*</span>}
58
+ </label>
59
+ )}
60
+ <div className="relative">
61
+ <input
62
+ type={showPassword ? "text" : "password"}
63
+ className="w-full px-3 py-2 pr-10 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
64
+ value={props.value || ""}
65
+ onChange={(e) => props.onChange(e.target.value)}
66
+ placeholder={props.placeholder}
67
+ disabled={props.disabled}
68
+ />
69
+ <button type="button" className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600" onClick={() => setShowPassword(!showPassword)}>
70
+ {showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
71
+ </button>
72
+ </div>
73
+ {props.description && <p className="text-xs text-gray-500 mt-1">{props.description}</p>}
74
+ </div>
75
+ );
76
+ };
77
+
78
+ // 自定义文本区域组件
79
+ const CustomTextareaWidget = (props: any) => {
80
+ const hasMinLength = props.options?.minLength;
81
+ const hasMaxLength = props.options?.maxLength;
82
+ const currentLength = props.value?.length || 0;
83
+
84
+ return (
85
+ <div className="mb-4">
86
+ {props.label && (
87
+ <label className="block text-sm font-medium text-gray-700 mb-1">
88
+ {props.label}
89
+ {props.required && <span className="text-red-500 ml-1">*</span>}
90
+ </label>
91
+ )}
92
+ <textarea
93
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors resize-vertical"
94
+ value={props.value || ""}
95
+ onChange={(e) => props.onChange(e.target.value)}
96
+ placeholder={props.placeholder}
97
+ disabled={props.disabled}
98
+ rows={props.options?.rows || 4}
99
+ maxLength={hasMaxLength ? props.options.maxLength : undefined}
100
+ />
101
+ {props.description && <p className="text-xs text-gray-500 mt-1">{props.description}</p>}
102
+ {(hasMinLength || hasMaxLength) && (
103
+ <div className="flex justify-end mt-1">
104
+ <span className={`text-xs ${currentLength > (props.options?.maxLength || 0) ? "text-red-500" : "text-gray-400"}`}>
105
+ {hasMinLength && hasMaxLength
106
+ ? `${currentLength}/${props.options.maxLength} (最少${props.options.minLength})`
107
+ : hasMaxLength
108
+ ? `${currentLength}/${props.options.maxLength}`
109
+ : `最少${props.options.minLength}字符`}
110
+ </span>
111
+ </div>
112
+ )}
113
+ </div>
114
+ );
115
+ };
116
+
117
+ // 自定义选择框组件
118
+ const CustomSelectWidget = (props: any) => {
119
+ return (
120
+ <div className="mb-4">
121
+ {props.label && (
122
+ <label className="block text-sm font-medium text-gray-700 mb-1">
123
+ {props.label}
124
+ {props.required && <span className="text-red-500 ml-1">*</span>}
125
+ </label>
126
+ )}
127
+ <div className="relative">
128
+ <select
129
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors appearance-none"
130
+ value={props.value || ""}
131
+ onChange={(e) => props.onChange(e.target.value)}
132
+ disabled={props.disabled}
133
+ >
134
+ {props.placeholder && (
135
+ <option value="" disabled>
136
+ {props.placeholder}
137
+ </option>
138
+ )}
139
+ {props.options?.enumOptions?.map((option: any, index: number) => (
140
+ <option key={index} value={option.value}>
141
+ {option.label}
142
+ </option>
143
+ ))}
144
+ </select>
145
+ <ChevronDown size={16} className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 pointer-events-none" />
146
+ </div>
147
+ {props.description && <p className="text-xs text-gray-500 mt-1">{props.description}</p>}
148
+ </div>
149
+ );
150
+ };
151
+
152
+ // 自定义多选框组件
153
+ const CustomCheckboxWidget = (props: any) => {
154
+ return (
155
+ <div className="mb-4">
156
+ {props.label && (
157
+ <label className="block text-sm font-medium text-gray-700 mb-2">
158
+ {props.label}
159
+ {props.required && <span className="text-red-500 ml-1">*</span>}
160
+ </label>
161
+ )}
162
+ <div className="space-y-2">
163
+ {props.options?.enumOptions?.map((option: any, index: number) => (
164
+ <label key={index} className="flex items-center">
165
+ <input
166
+ type="checkbox"
167
+ className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500 focus:ring-2"
168
+ checked={props.value?.includes(option.value) || false}
169
+ onChange={(e) => {
170
+ const currentValue = props.value || [];
171
+ if (e.target.checked) {
172
+ props.onChange([...currentValue, option.value]);
173
+ } else {
174
+ props.onChange(currentValue.filter((v: any) => v !== option.value));
175
+ }
176
+ }}
177
+ disabled={props.disabled}
178
+ />
179
+ <span className="ml-2 text-sm text-gray-700">{option.label}</span>
180
+ </label>
181
+ ))}
182
+ </div>
183
+ {props.description && <p className="text-xs text-gray-500 mt-1">{props.description}</p>}
184
+ </div>
185
+ );
186
+ };
187
+
188
+ // 自定义单选按钮组件
189
+ const CustomRadioWidget = (props: any) => {
190
+ return (
191
+ <div className="mb-4">
192
+ {props.label && (
193
+ <label className="block text-sm font-medium text-gray-700 mb-2">
194
+ {props.label}
195
+ {props.required && <span className="text-red-500 ml-1">*</span>}
196
+ </label>
197
+ )}
198
+ <div className="space-y-2">
199
+ {props.options?.enumOptions?.map((option: any, index: number) => (
200
+ <label key={index} className="flex items-center">
201
+ <input
202
+ type="radio"
203
+ name={props.id}
204
+ className="w-4 h-4 text-blue-600 border-gray-300 focus:ring-blue-500 focus:ring-2"
205
+ value={option.value}
206
+ checked={props.value === option.value}
207
+ onChange={(e) => props.onChange(e.target.value)}
208
+ disabled={props.disabled}
209
+ />
210
+ <span className="ml-2 text-sm text-gray-700">{option.label}</span>
211
+ </label>
212
+ ))}
213
+ </div>
214
+ {props.description && <p className="text-xs text-gray-500 mt-1">{props.description}</p>}
215
+ </div>
216
+ );
217
+ };
218
+
219
+ // 自定义数字输入组件
220
+ const CustomNumberWidget = (props: any) => {
221
+ return (
222
+ <div className="mb-4">
223
+ {props.label && (
224
+ <label className="block text-sm font-medium text-gray-700 mb-1">
225
+ {props.label}
226
+ {props.required && <span className="text-red-500 ml-1">*</span>}
227
+ </label>
228
+ )}
229
+ <input
230
+ type="number"
231
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
232
+ value={props.value || ""}
233
+ onChange={(e) => props.onChange(Number(e.target.value))}
234
+ placeholder={props.placeholder}
235
+ disabled={props.disabled}
236
+ min={props.options?.min}
237
+ max={props.options?.max}
238
+ step={props.options?.step}
239
+ />
240
+ {props.description && <p className="text-xs text-gray-500 mt-1">{props.description}</p>}
241
+ </div>
242
+ );
243
+ };
244
+
245
+ // 自定义滑块组件
246
+ const CustomRangeWidget = (props: any) => {
247
+ return (
248
+ <div className="mb-4">
249
+ {props.label && (
250
+ <label className="block text-sm font-medium text-gray-700 mb-2">
251
+ {props.label}
252
+ {props.required && <span className="text-red-500 ml-1">*</span>}
253
+ </label>
254
+ )}
255
+ <div className="px-2">
256
+ <input
257
+ type="range"
258
+ className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider"
259
+ value={props.value || props.options?.min || 0}
260
+ onChange={(e) => props.onChange(Number(e.target.value))}
261
+ disabled={props.disabled}
262
+ min={props.options?.min || 0}
263
+ max={props.options?.max || 100}
264
+ step={props.options?.step || 1}
265
+ />
266
+ <div className="flex justify-between text-xs text-gray-500 mt-1">
267
+ <span>{props.options?.min || 0}</span>
268
+ <span className="font-medium text-blue-600">{props.value}</span>
269
+ <span>{props.options?.max || 100}</span>
270
+ </div>
271
+ </div>
272
+ {props.description && <p className="text-xs text-gray-500 mt-1">{props.description}</p>}
273
+ </div>
274
+ );
275
+ };
276
+
277
+ // 自定义日期选择器组件
278
+ const CustomDateWidget = (props: any) => {
279
+ return (
280
+ <div className="mb-4">
281
+ {props.label && (
282
+ <label className="block text-sm font-medium text-gray-700 mb-1">
283
+ {props.label}
284
+ {props.required && <span className="text-red-500 ml-1">*</span>}
285
+ </label>
286
+ )}
287
+ <input
288
+ type="date"
289
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
290
+ value={props.value || ""}
291
+ onChange={(e) => props.onChange(e.target.value)}
292
+ disabled={props.disabled}
293
+ />
294
+ {props.description && <p className="text-xs text-gray-500 mt-1">{props.description}</p>}
295
+ </div>
296
+ );
297
+ };
298
+
299
+ // 自定义日期时间选择器组件
300
+ const CustomDateTimeWidget = (props: any) => {
301
+ return (
302
+ <div className="mb-4">
303
+ {props.label && (
304
+ <label className="block text-sm font-medium text-gray-700 mb-1">
305
+ {props.label}
306
+ {props.required && <span className="text-red-500 ml-1">*</span>}
307
+ </label>
308
+ )}
309
+ <input
310
+ type="datetime-local"
311
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
312
+ value={props.value || ""}
313
+ onChange={(e) => props.onChange(e.target.value)}
314
+ disabled={props.disabled}
315
+ />
316
+ {props.description && <p className="text-xs text-gray-500 mt-1">{props.description}</p>}
317
+ </div>
318
+ );
319
+ };
320
+
321
+ // 自定义布尔值组件
322
+ const CustomBooleanWidget = (props: any) => {
323
+ return (
324
+ <div className="mb-4">
325
+ {props.label && (
326
+ <label className="flex items-center">
327
+ <input
328
+ type="checkbox"
329
+ className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500 focus:ring-2 mr-2"
330
+ checked={props.value || false}
331
+ onChange={(e) => props.onChange(e.target.checked)}
332
+ disabled={props.disabled}
333
+ />
334
+ <span className="text-sm text-gray-700">
335
+ {props.label}
336
+ {props.required && <span className="text-red-500 ml-1">*</span>}
337
+ </span>
338
+ </label>
339
+ )}
340
+ {props.description && <p className="text-xs text-gray-500 mt-1 ml-6">{props.description}</p>}
341
+ </div>
342
+ );
343
+ };
344
+
345
+ // 自定义文件上传组件
346
+ const CustomFileWidget = (props: any) => {
347
+ const handleFileChange = (e: any) => {
348
+ const files = e.target.files;
349
+ if (props.multiple) {
350
+ // 多文件选择
351
+ props.onChange(files);
352
+ } else {
353
+ // 单文件选择
354
+ props.onChange(files?.[0] || null);
355
+ }
356
+ };
357
+
358
+ return (
359
+ <div className="mb-4">
360
+ {props.label && (
361
+ <label className="block text-sm font-medium text-gray-700 mb-1">
362
+ {props.label}
363
+ {props.required && <span className="text-red-500 ml-1">*</span>}
364
+ </label>
365
+ )}
366
+ <input
367
+ type="file"
368
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors file:mr-3 file:py-1 file:px-3 file:border-0 file:bg-blue-50 file:text-blue-700 file:rounded file:font-medium"
369
+ onChange={handleFileChange}
370
+ disabled={props.disabled}
371
+ multiple={props.multiple}
372
+ accept={props.accept}
373
+ />
374
+ {props.description && <p className="text-xs text-gray-500 mt-1">{props.description}</p>}
375
+ </div>
376
+ );
377
+ };
378
+
379
+ // 自定义数组字段组件
380
+ const CustomArrayField = (props: any) => {
381
+ return (
382
+ <div className="mb-4">
383
+ {props.label && (
384
+ <label className="block text-sm font-medium text-gray-700 mb-2">
385
+ {props.label}
386
+ {props.required && <span className="text-red-500 ml-1">*</span>}
387
+ </label>
388
+ )}
389
+ <div className="space-y-2">
390
+ {props.items.map((item: any, index: number) => (
391
+ <div key={index} className="flex items-start gap-2 p-3 border border-gray-200 rounded-lg">
392
+ <div className="flex-1">{item.children}</div>
393
+ <button type="button" className="p-1 text-red-500 hover:text-red-700 hover:bg-red-50 rounded" onClick={() => props.onDropIndexClick(index)()}>
394
+ <X size={16} />
395
+ </button>
396
+ </div>
397
+ ))}
398
+ <button type="button" className="flex items-center gap-1 px-3 py-1 text-sm bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors" onClick={() => props.onAddClick()}>
399
+ <Plus size={14} />
400
+ 添加项目
401
+ </button>
402
+ </div>
403
+ {props.description && <p className="text-xs text-gray-500 mt-1">{props.description}</p>}
404
+ </div>
405
+ );
406
+ };
407
+
408
+ // 自定义字段模板
409
+ const CustomFieldTemplate = (props: any) => {
410
+ return <div className={`${props.className} mb-4`}>{props.children}</div>;
411
+ };
412
+
413
+ // 自定义对象字段模板
414
+ const CustomObjectFieldTemplate = (props: any) => {
415
+ return (
416
+ <div className="mb-4">
417
+ {props.title && <h3 className="text-sm font-medium text-gray-700 mb-2">{props.title}</h3>}
418
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4 pl-4 border-l-2 border-gray-200">
419
+ {props.properties.map((prop: any) => (
420
+ <div key={prop.name}>{prop.content}</div>
421
+ ))}
422
+ </div>
423
+ {props.description && <p className="text-xs text-gray-500 mt-1">{props.description}</p>}
424
+ </div>
425
+ );
426
+ };
427
+
428
+ // 自定义按钮组件
429
+ const CustomSubmitButton = (props: any) => {
430
+ return (
431
+ <button
432
+ type="submit"
433
+ className="px-4 py-2 rounded-lg bg-blue-500 hover:bg-blue-600 disabled:bg-gray-300 text-white font-medium focus:outline-none focus:ring-2 focus:ring-blue-400 transition-colors disabled:cursor-not-allowed"
434
+ disabled={props.disabled}
435
+ >
436
+ {props.submitText || "提交"}
437
+ </button>
438
+ );
439
+ };
440
+
441
+ export const show_form = createUITool({
442
+ name: "show_form",
443
+ description: "显示动态表单,等待用户填写",
444
+ parameters: {
445
+ schema: z.any(),
446
+ },
447
+ onlyRender: false,
448
+ handler: ToolManager.waitForUIDone,
449
+ render(tool) {
450
+ const data = tool.getInputRepaired();
451
+ const output = tool.getJSONOutputSafe();
452
+ const formSchema = data.schema;
453
+
454
+ const handleSubmit = (formData: any) => {
455
+ console.log("Form submitted:", formData);
456
+ tool.response(formData);
457
+ };
458
+
459
+ // 自定义控件配置
460
+ const customWidgets = {
461
+ text: CustomTextWidget,
462
+ password: CustomPasswordWidget,
463
+ textarea: CustomTextareaWidget,
464
+ select: CustomSelectWidget,
465
+ checkboxes: CustomCheckboxWidget,
466
+ radio: CustomRadioWidget,
467
+ number: CustomNumberWidget,
468
+ range: CustomRangeWidget,
469
+ date: CustomDateWidget,
470
+ datetime: CustomDateTimeWidget,
471
+ file: CustomFileWidget,
472
+ boolean: CustomBooleanWidget,
473
+ };
474
+
475
+ const customFields = {
476
+ array: CustomArrayField,
477
+ };
478
+
479
+ const customTemplates = {
480
+ FieldTemplate: CustomFieldTemplate,
481
+ ObjectFieldTemplate: CustomObjectFieldTemplate,
482
+ ButtonTemplates: {
483
+ SubmitButton: CustomSubmitButton,
484
+ },
485
+ };
486
+
487
+ return (
488
+ <div className="p-4 bg-white rounded-lg border border-gray-200">
489
+ <div className="flex items-center gap-1.5 text-gray-700 mb-3 font-bold">
490
+ <FileText className="w-4 h-4 text-blue-500" />
491
+ <span>AI 表单</span>
492
+ </div>
493
+ <ErrorBoundary>
494
+ <Form
495
+ readonly={tool.state === "done"}
496
+ schema={formSchema}
497
+ formData={output}
498
+ onSubmit={(data: IChangeEvent<any>) => handleSubmit(data.formData)}
499
+ validator={validator}
500
+ onError={(errors) => {
501
+ console.error("表单校验错误:", errors);
502
+ alert("表单填写有误,请检查后再提交。");
503
+ }}
504
+ widgets={customWidgets}
505
+ fields={customFields}
506
+ templates={customTemplates}
507
+ className="custom-form"
508
+ >
509
+ <div className="flex gap-2 mt-4">
510
+ <CustomSubmitButton />
511
+ </div>
512
+ </Form>
513
+ </ErrorBoundary>
514
+ </div>
515
+ );
516
+ },
517
+ });
@@ -83,7 +83,7 @@ const LayoutFlow = () => {
83
83
  }
84
84
  }, [currentNodeName]);
85
85
  return (
86
- <div className="w-1/3 h-full relative overflow-hidden border-l">
86
+ <div className="h-full relative overflow-hidden border-l">
87
87
  <ReactFlow nodes={nodes} edges={edges} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} fitView className="w-full h-full" nodeTypes={nodeTypes}>
88
88
  <Background />
89
89
  <Controls />
package/src/index.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  import "virtual:uno.css";
2
- import "./index.css";
2
+ import "@andypf/json-viewer/dist/iife/index.js";
3
3
  export { default as Chat } from "./chat/Chat";
package/test/App.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import Chat from "../src/chat/Chat";
1
+ import { Chat } from "../src/index";
2
2
  import Login from "../src/login/Login";
3
3
  import { useState } from "react";
4
4
  function App() {