@langgraph-js/ui 5.7.0 → 5.7.2

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.
Files changed (62) hide show
  1. package/dist/assets/{arc-Cxpt6B73.js → arc-DsgW0fW0.js} +1 -1
  2. package/dist/assets/{architectureDiagram-VXUJARFQ-DEy8uu79.js → architectureDiagram-VXUJARFQ-x6jPEiVb.js} +1 -1
  3. package/dist/assets/{blockDiagram-VD42YOAC-yNezEuFm.js → blockDiagram-VD42YOAC-CtC7rOkz.js} +1 -1
  4. package/dist/assets/{c4Diagram-YG6GDRKO-CFr-QKSf.js → c4Diagram-YG6GDRKO-B0Bhovp6.js} +1 -1
  5. package/dist/assets/channel-CYykpNsh.js +1 -0
  6. package/dist/assets/{chunk-4BX2VUAB-DKYgHx9z.js → chunk-4BX2VUAB-7nlfvZIs.js} +1 -1
  7. package/dist/assets/{chunk-55IACEB6-Dp0pEp1k.js → chunk-55IACEB6-CEUhaTkt.js} +1 -1
  8. package/dist/assets/{chunk-B4BG7PRW-B9ne889x.js → chunk-B4BG7PRW-3Je1T-6D.js} +1 -1
  9. package/dist/assets/{chunk-DI55MBZ5-nz3xKTAU.js → chunk-DI55MBZ5-CibwD6Tg.js} +1 -1
  10. package/dist/assets/{chunk-FMBD7UC4-BEqW4wjD.js → chunk-FMBD7UC4-DkqBqoF9.js} +1 -1
  11. package/dist/assets/{chunk-QN33PNHL-DdYfOS87.js → chunk-QN33PNHL-DSoehSAx.js} +1 -1
  12. package/dist/assets/{chunk-QZHKN3VN--fzHnGin.js → chunk-QZHKN3VN-CAcu9F3O.js} +1 -1
  13. package/dist/assets/{chunk-TZMSLE5B-wYv5WbFJ.js → chunk-TZMSLE5B-CS59_8HR.js} +1 -1
  14. package/dist/assets/classDiagram-2ON5EDUG-DEA6c_Xf.js +1 -0
  15. package/dist/assets/classDiagram-v2-WZHVMYZB-DEA6c_Xf.js +1 -0
  16. package/dist/assets/clone-Du_gs0pm.js +1 -0
  17. package/dist/assets/{cose-bilkent-S5V4N54A-Do_6LUNg.js → cose-bilkent-S5V4N54A-CcvTmZ3l.js} +1 -1
  18. package/dist/assets/{dagre-6UL2VRFP-1iQhW5EH.js → dagre-6UL2VRFP-Du0L8eIB.js} +1 -1
  19. package/dist/assets/{diagram-PSM6KHXK-BzAvoA1a.js → diagram-PSM6KHXK-QCoayD06.js} +1 -1
  20. package/dist/assets/{diagram-QEK2KX5R-BruzodAN.js → diagram-QEK2KX5R-DOyrrhCz.js} +1 -1
  21. package/dist/assets/{diagram-S2PKOQOG-DxCLvX73.js → diagram-S2PKOQOG-KjypfMN6.js} +1 -1
  22. package/dist/assets/{erDiagram-Q2GNP2WA-VZD44fiG.js → erDiagram-Q2GNP2WA-BN-9XTVy.js} +1 -1
  23. package/dist/assets/{flowDiagram-NV44I4VS-BW9G6Z5Y.js → flowDiagram-NV44I4VS-BeFTT0TJ.js} +1 -1
  24. package/dist/assets/{ganttDiagram-LVOFAZNH-BE5hhtQW.js → ganttDiagram-LVOFAZNH-1AE61yJv.js} +1 -1
  25. package/dist/assets/{gitGraphDiagram-NY62KEGX-D2xrVqfs.js → gitGraphDiagram-NY62KEGX-aTUWjzWl.js} +1 -1
  26. package/dist/assets/{graph-BOje8BQW.js → graph-uuboXTUX.js} +1 -1
  27. package/dist/assets/index-CKXVIUJM.css +1 -0
  28. package/dist/assets/{index-Dyga3lv6.js → index-DuV4Cs93.js} +139 -134
  29. package/dist/assets/{infoDiagram-F6ZHWCRC-DuvG7CMo.js → infoDiagram-F6ZHWCRC-CQen-ygj.js} +1 -1
  30. package/dist/assets/{isUndefined-Jm66YXc7.js → isUndefined-B1VZkTi-.js} +1 -1
  31. package/dist/assets/{journeyDiagram-XKPGCS4Q-DppY-OqU.js → journeyDiagram-XKPGCS4Q-_d3Y3lJU.js} +1 -1
  32. package/dist/assets/{kanban-definition-3W4ZIXB7-DWFZ7d_a.js → kanban-definition-3W4ZIXB7-Dxarq9GE.js} +1 -1
  33. package/dist/assets/{layout-BrYg1BPZ.js → layout-Cjc7wP1L.js} +1 -1
  34. package/dist/assets/{mermaid.core-Brj2E_sv.js → mermaid.core-BpzyNF0N.js} +5 -5
  35. package/dist/assets/{min-OyaaRTus.js → min-iWVu4DkI.js} +1 -1
  36. package/dist/assets/{mindmap-definition-VGOIOE7T-Ys6gAj49.js → mindmap-definition-VGOIOE7T-CnAMXH16.js} +1 -1
  37. package/dist/assets/{pieDiagram-ADFJNKIX-CDawS5d5.js → pieDiagram-ADFJNKIX-jQmVw15z.js} +1 -1
  38. package/dist/assets/{quadrantDiagram-AYHSOK5B-BwaRQW9j.js → quadrantDiagram-AYHSOK5B-CdR8ErX6.js} +1 -1
  39. package/dist/assets/{requirementDiagram-UZGBJVZJ-xZhlkqne.js → requirementDiagram-UZGBJVZJ-DRe2cRaP.js} +1 -1
  40. package/dist/assets/{sankeyDiagram-TZEHDZUN-CNXqnJNS.js → sankeyDiagram-TZEHDZUN-CCxPdCL6.js} +1 -1
  41. package/dist/assets/{sequenceDiagram-WL72ISMW-DuVqNtcK.js → sequenceDiagram-WL72ISMW-DEqPM-5z.js} +1 -1
  42. package/dist/assets/{stateDiagram-FKZM4ZOC-kjDVpzFt.js → stateDiagram-FKZM4ZOC-BJqbxGAU.js} +1 -1
  43. package/dist/assets/stateDiagram-v2-4FDKWEC3-CILBmBJc.js +1 -0
  44. package/dist/assets/{timeline-definition-IT6M3QCI-BrZan_9K.js → timeline-definition-IT6M3QCI-C3oBdaQs.js} +1 -1
  45. package/dist/assets/{treemap-KMMF4GRG-Bc2NrK0Z.js → treemap-KMMF4GRG-Cg2H1ebp.js} +1 -1
  46. package/dist/assets/{xychartDiagram-PRI3JC2R-Dh5NbHlS.js → xychartDiagram-PRI3JC2R-C0KJkN_M.js} +1 -1
  47. package/dist/index.html +2 -2
  48. package/package.json +4 -4
  49. package/server/graph/debugAgent.ts +10 -7
  50. package/src/artifacts/ArtifactViewer.tsx +42 -11
  51. package/src/chat/Chat.tsx +2 -2
  52. package/src/chat/components/MessageTool.tsx +1 -1
  53. package/src/chat/tools/ask_user_to_fill_form.tsx +136 -45
  54. package/src/chat/tools/create_artifacts.tsx +69 -11
  55. package/src/settings/ArtifactsSettings.tsx +47 -0
  56. package/src/settings/SettingPanel.tsx +6 -0
  57. package/dist/assets/channel-ohUHNNLS.js +0 -1
  58. package/dist/assets/classDiagram-2ON5EDUG-DD3S7Z_T.js +0 -1
  59. package/dist/assets/classDiagram-v2-WZHVMYZB-DD3S7Z_T.js +0 -1
  60. package/dist/assets/clone-BL1RNKEm.js +0 -1
  61. package/dist/assets/index-DKzqRWAz.css +0 -1
  62. package/dist/assets/stateDiagram-v2-4FDKWEC3-Bt6o2Z8q.js +0 -1
@@ -8,29 +8,45 @@ import { useState } from "react";
8
8
  import { Button } from "@/components/ui/button";
9
9
  import { cn } from "@/lib/utils";
10
10
 
11
+ // 错误信息显示组件
12
+ const ErrorMessage = ({ errors }: { errors?: string[] }) => {
13
+ if (!errors || errors.length === 0) return null;
14
+ return (
15
+ <div className="flex items-start gap-1.5 mt-1">
16
+ <AlertCircle className="w-3.5 h-3.5 text-red-500 shrink-0 mt-0.5" />
17
+ <span className="text-xs text-red-600">{errors[0]}</span>
18
+ </div>
19
+ );
20
+ };
21
+
11
22
  // 自定义文本输入组件
12
23
  const CustomTextWidget = (props: any) => {
13
24
  const hasMinLength = props.options?.minLength;
14
25
  const hasMaxLength = props.options?.maxLength;
15
26
  const currentLength = props.value?.length || 0;
27
+ const hasError = props.rawErrors && props.rawErrors.length > 0;
16
28
 
17
29
  return (
18
30
  <div className="mb-4">
19
31
  {props.label && (
20
- <label className="block text-sm font-medium text-gray-700 mb-1">
32
+ <label className="block text-sm font-medium text-gray-900 mb-1">
21
33
  {props.label}
22
34
  {props.required && <span className="text-red-500 ml-1">*</span>}
23
35
  </label>
24
36
  )}
25
37
  <input
26
38
  type="text"
27
- 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"
39
+ className={cn(
40
+ "w-full rounded-lg border bg-gray-50/50 p-2 text-sm text-gray-900 placeholder:text-gray-400 focus:outline-none focus:ring-2 transition-all",
41
+ hasError ? "border-red-500 focus:ring-red-100 focus:border-red-500" : "border-gray-200 focus:ring-blue-100 focus:border-blue-400"
42
+ )}
28
43
  value={props.value || ""}
29
44
  onChange={(e) => props.onChange(e.target.value)}
30
45
  placeholder={props.placeholder}
31
46
  disabled={props.disabled}
32
47
  maxLength={hasMaxLength ? props.options.maxLength : undefined}
33
48
  />
49
+ <ErrorMessage errors={props.rawErrors} />
34
50
  {props.description && <p className="text-xs text-gray-500 mt-1">{props.description}</p>}
35
51
  {(hasMinLength || hasMaxLength) && (
36
52
  <div className="flex justify-end mt-1">
@@ -50,11 +66,12 @@ const CustomTextWidget = (props: any) => {
50
66
  // 自定义密码输入组件
51
67
  const CustomPasswordWidget = (props: any) => {
52
68
  const [showPassword, setShowPassword] = useState(false);
69
+ const hasError = props.rawErrors && props.rawErrors.length > 0;
53
70
 
54
71
  return (
55
72
  <div className="mb-4">
56
73
  {props.label && (
57
- <label className="block text-sm font-medium text-gray-700 mb-1">
74
+ <label className="block text-sm font-medium text-gray-900 mb-1">
58
75
  {props.label}
59
76
  {props.required && <span className="text-red-500 ml-1">*</span>}
60
77
  </label>
@@ -62,7 +79,10 @@ const CustomPasswordWidget = (props: any) => {
62
79
  <div className="relative">
63
80
  <input
64
81
  type={showPassword ? "text" : "password"}
65
- 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"
82
+ className={cn(
83
+ "w-full rounded-lg border bg-gray-50/50 p-2 pr-10 text-sm text-gray-900 placeholder:text-gray-400 focus:outline-none focus:ring-2 transition-all",
84
+ hasError ? "border-red-500 focus:ring-red-100 focus:border-red-500" : "border-gray-200 focus:ring-blue-100 focus:border-blue-400"
85
+ )}
66
86
  value={props.value || ""}
67
87
  onChange={(e) => props.onChange(e.target.value)}
68
88
  placeholder={props.placeholder}
@@ -72,6 +92,7 @@ const CustomPasswordWidget = (props: any) => {
72
92
  {showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
73
93
  </button>
74
94
  </div>
95
+ <ErrorMessage errors={props.rawErrors} />
75
96
  {props.description && <p className="text-xs text-gray-500 mt-1">{props.description}</p>}
76
97
  </div>
77
98
  );
@@ -82,17 +103,21 @@ const CustomTextareaWidget = (props: any) => {
82
103
  const hasMinLength = props.options?.minLength;
83
104
  const hasMaxLength = props.options?.maxLength;
84
105
  const currentLength = props.value?.length || 0;
106
+ const hasError = props.rawErrors && props.rawErrors.length > 0;
85
107
 
86
108
  return (
87
109
  <div className="mb-4">
88
110
  {props.label && (
89
- <label className="block text-sm font-medium text-gray-700 mb-1">
111
+ <label className="block text-sm font-medium text-gray-900 mb-1">
90
112
  {props.label}
91
113
  {props.required && <span className="text-red-500 ml-1">*</span>}
92
114
  </label>
93
115
  )}
94
116
  <textarea
95
- 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"
117
+ className={cn(
118
+ "w-full rounded-lg border bg-gray-50/50 p-2 text-sm text-gray-900 placeholder:text-gray-400 focus:outline-none focus:ring-2 transition-all resize-none",
119
+ hasError ? "border-red-500 focus:ring-red-100 focus:border-red-500" : "border-gray-200 focus:ring-blue-100 focus:border-blue-400"
120
+ )}
96
121
  value={props.value || ""}
97
122
  onChange={(e) => props.onChange(e.target.value)}
98
123
  placeholder={props.placeholder}
@@ -100,6 +125,7 @@ const CustomTextareaWidget = (props: any) => {
100
125
  rows={props.options?.rows || 4}
101
126
  maxLength={hasMaxLength ? props.options.maxLength : undefined}
102
127
  />
128
+ <ErrorMessage errors={props.rawErrors} />
103
129
  {props.description && <p className="text-xs text-gray-500 mt-1">{props.description}</p>}
104
130
  {(hasMinLength || hasMaxLength) && (
105
131
  <div className="flex justify-end mt-1">
@@ -118,17 +144,22 @@ const CustomTextareaWidget = (props: any) => {
118
144
 
119
145
  // 自定义选择框组件
120
146
  const CustomSelectWidget = (props: any) => {
147
+ const hasError = props.rawErrors && props.rawErrors.length > 0;
148
+
121
149
  return (
122
150
  <div className="mb-4">
123
151
  {props.label && (
124
- <label className="block text-sm font-medium text-gray-700 mb-1">
152
+ <label className="block text-sm font-medium text-gray-900 mb-1">
125
153
  {props.label}
126
154
  {props.required && <span className="text-red-500 ml-1">*</span>}
127
155
  </label>
128
156
  )}
129
157
  <div className="relative">
130
158
  <select
131
- 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"
159
+ className={cn(
160
+ "w-full rounded-lg border bg-gray-50/50 p-2 pr-10 text-sm text-gray-900 focus:outline-none focus:ring-2 transition-all appearance-none",
161
+ hasError ? "border-red-500 focus:ring-red-100 focus:border-red-500" : "border-gray-200 focus:ring-blue-100 focus:border-blue-400"
162
+ )}
132
163
  value={props.value || ""}
133
164
  onChange={(e) => props.onChange(e.target.value)}
134
165
  disabled={props.disabled}
@@ -146,6 +177,7 @@ const CustomSelectWidget = (props: any) => {
146
177
  </select>
147
178
  <ChevronDown size={16} className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 pointer-events-none" />
148
179
  </div>
180
+ <ErrorMessage errors={props.rawErrors} />
149
181
  {props.description && <p className="text-xs text-gray-500 mt-1">{props.description}</p>}
150
182
  </div>
151
183
  );
@@ -153,15 +185,17 @@ const CustomSelectWidget = (props: any) => {
153
185
 
154
186
  // 自定义多选框组件
155
187
  const CustomCheckboxWidget = (props: any) => {
188
+ const hasError = props.rawErrors && props.rawErrors.length > 0;
189
+
156
190
  return (
157
191
  <div className="mb-4">
158
192
  {props.label && (
159
- <label className="block text-sm font-medium text-gray-700 mb-2">
193
+ <label className="block text-sm font-medium text-gray-900 mb-2">
160
194
  {props.label}
161
195
  {props.required && <span className="text-red-500 ml-1">*</span>}
162
196
  </label>
163
197
  )}
164
- <div className="space-y-2">
198
+ <div className={cn("space-y-2", hasError && "p-2 border border-red-200 rounded-lg bg-red-50/30")}>
165
199
  {props.options?.enumOptions?.map((option: any, index: number) => (
166
200
  <label key={index} className="flex items-center">
167
201
  <input
@@ -182,6 +216,7 @@ const CustomCheckboxWidget = (props: any) => {
182
216
  </label>
183
217
  ))}
184
218
  </div>
219
+ <ErrorMessage errors={props.rawErrors} />
185
220
  {props.description && <p className="text-xs text-gray-500 mt-1">{props.description}</p>}
186
221
  </div>
187
222
  );
@@ -189,15 +224,17 @@ const CustomCheckboxWidget = (props: any) => {
189
224
 
190
225
  // 自定义单选按钮组件
191
226
  const CustomRadioWidget = (props: any) => {
227
+ const hasError = props.rawErrors && props.rawErrors.length > 0;
228
+
192
229
  return (
193
230
  <div className="mb-4">
194
231
  {props.label && (
195
- <label className="block text-sm font-medium text-gray-700 mb-2">
232
+ <label className="block text-sm font-medium text-gray-900 mb-2">
196
233
  {props.label}
197
234
  {props.required && <span className="text-red-500 ml-1">*</span>}
198
235
  </label>
199
236
  )}
200
- <div className="space-y-2">
237
+ <div className={cn("space-y-2", hasError && "p-2 border border-red-200 rounded-lg bg-red-50/30")}>
201
238
  {props.options?.enumOptions?.map((option: any, index: number) => (
202
239
  <label key={index} className="flex items-center">
203
240
  <input
@@ -213,6 +250,7 @@ const CustomRadioWidget = (props: any) => {
213
250
  </label>
214
251
  ))}
215
252
  </div>
253
+ <ErrorMessage errors={props.rawErrors} />
216
254
  {props.description && <p className="text-xs text-gray-500 mt-1">{props.description}</p>}
217
255
  </div>
218
256
  );
@@ -220,17 +258,22 @@ const CustomRadioWidget = (props: any) => {
220
258
 
221
259
  // 自定义数字输入组件
222
260
  const CustomNumberWidget = (props: any) => {
261
+ const hasError = props.rawErrors && props.rawErrors.length > 0;
262
+
223
263
  return (
224
264
  <div className="mb-4">
225
265
  {props.label && (
226
- <label className="block text-sm font-medium text-gray-700 mb-1">
266
+ <label className="block text-sm font-medium text-gray-900 mb-1">
227
267
  {props.label}
228
268
  {props.required && <span className="text-red-500 ml-1">*</span>}
229
269
  </label>
230
270
  )}
231
271
  <input
232
272
  type="number"
233
- 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"
273
+ className={cn(
274
+ "w-full rounded-lg border bg-gray-50/50 p-2 text-sm text-gray-900 placeholder:text-gray-400 focus:outline-none focus:ring-2 transition-all",
275
+ hasError ? "border-red-500 focus:ring-red-100 focus:border-red-500" : "border-gray-200 focus:ring-blue-100 focus:border-blue-400"
276
+ )}
234
277
  value={props.value || ""}
235
278
  onChange={(e) => props.onChange(Number(e.target.value))}
236
279
  placeholder={props.placeholder}
@@ -239,6 +282,7 @@ const CustomNumberWidget = (props: any) => {
239
282
  max={props.options?.max}
240
283
  step={props.options?.step}
241
284
  />
285
+ <ErrorMessage errors={props.rawErrors} />
242
286
  {props.description && <p className="text-xs text-gray-500 mt-1">{props.description}</p>}
243
287
  </div>
244
288
  );
@@ -246,15 +290,17 @@ const CustomNumberWidget = (props: any) => {
246
290
 
247
291
  // 自定义滑块组件
248
292
  const CustomRangeWidget = (props: any) => {
293
+ const hasError = props.rawErrors && props.rawErrors.length > 0;
294
+
249
295
  return (
250
296
  <div className="mb-4">
251
297
  {props.label && (
252
- <label className="block text-sm font-medium text-gray-700 mb-2">
298
+ <label className="block text-sm font-medium text-gray-900 mb-2">
253
299
  {props.label}
254
300
  {props.required && <span className="text-red-500 ml-1">*</span>}
255
301
  </label>
256
302
  )}
257
- <div className="px-2">
303
+ <div className={cn("px-2", hasError && "p-2 border border-red-200 rounded-lg bg-red-50/30")}>
258
304
  <input
259
305
  type="range"
260
306
  className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider"
@@ -271,6 +317,7 @@ const CustomRangeWidget = (props: any) => {
271
317
  <span>{props.options?.max || 100}</span>
272
318
  </div>
273
319
  </div>
320
+ <ErrorMessage errors={props.rawErrors} />
274
321
  {props.description && <p className="text-xs text-gray-500 mt-1">{props.description}</p>}
275
322
  </div>
276
323
  );
@@ -278,21 +325,27 @@ const CustomRangeWidget = (props: any) => {
278
325
 
279
326
  // 自定义日期选择器组件
280
327
  const CustomDateWidget = (props: any) => {
328
+ const hasError = props.rawErrors && props.rawErrors.length > 0;
329
+
281
330
  return (
282
331
  <div className="mb-4">
283
332
  {props.label && (
284
- <label className="block text-sm font-medium text-gray-700 mb-1">
333
+ <label className="block text-sm font-medium text-gray-900 mb-1">
285
334
  {props.label}
286
335
  {props.required && <span className="text-red-500 ml-1">*</span>}
287
336
  </label>
288
337
  )}
289
338
  <input
290
339
  type="date"
291
- 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"
340
+ className={cn(
341
+ "w-full rounded-lg border bg-gray-50/50 p-2 text-sm text-gray-900 focus:outline-none focus:ring-2 transition-all",
342
+ hasError ? "border-red-500 focus:ring-red-100 focus:border-red-500" : "border-gray-200 focus:ring-blue-100 focus:border-blue-400"
343
+ )}
292
344
  value={props.value || ""}
293
345
  onChange={(e) => props.onChange(e.target.value)}
294
346
  disabled={props.disabled}
295
347
  />
348
+ <ErrorMessage errors={props.rawErrors} />
296
349
  {props.description && <p className="text-xs text-gray-500 mt-1">{props.description}</p>}
297
350
  </div>
298
351
  );
@@ -300,21 +353,27 @@ const CustomDateWidget = (props: any) => {
300
353
 
301
354
  // 自定义日期时间选择器组件
302
355
  const CustomDateTimeWidget = (props: any) => {
356
+ const hasError = props.rawErrors && props.rawErrors.length > 0;
357
+
303
358
  return (
304
359
  <div className="mb-4">
305
360
  {props.label && (
306
- <label className="block text-sm font-medium text-gray-700 mb-1">
361
+ <label className="block text-sm font-medium text-gray-900 mb-1">
307
362
  {props.label}
308
363
  {props.required && <span className="text-red-500 ml-1">*</span>}
309
364
  </label>
310
365
  )}
311
366
  <input
312
367
  type="datetime-local"
313
- 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"
368
+ className={cn(
369
+ "w-full rounded-lg border bg-gray-50/50 p-2 text-sm text-gray-900 focus:outline-none focus:ring-2 transition-all",
370
+ hasError ? "border-red-500 focus:ring-red-100 focus:border-red-500" : "border-gray-200 focus:ring-blue-100 focus:border-blue-400"
371
+ )}
314
372
  value={props.value || ""}
315
373
  onChange={(e) => props.onChange(e.target.value)}
316
374
  disabled={props.disabled}
317
375
  />
376
+ <ErrorMessage errors={props.rawErrors} />
318
377
  {props.description && <p className="text-xs text-gray-500 mt-1">{props.description}</p>}
319
378
  </div>
320
379
  );
@@ -322,23 +381,28 @@ const CustomDateTimeWidget = (props: any) => {
322
381
 
323
382
  // 自定义布尔值组件
324
383
  const CustomBooleanWidget = (props: any) => {
384
+ const hasError = props.rawErrors && props.rawErrors.length > 0;
385
+
325
386
  return (
326
387
  <div className="mb-4">
327
- {props.label && (
328
- <label className="flex items-center">
329
- <input
330
- type="checkbox"
331
- className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500 focus:ring-2 mr-2"
332
- checked={props.value || false}
333
- onChange={(e) => props.onChange(e.target.checked)}
334
- disabled={props.disabled}
335
- />
336
- <span className="text-sm text-gray-700">
337
- {props.label}
338
- {props.required && <span className="text-red-500 ml-1">*</span>}
339
- </span>
340
- </label>
341
- )}
388
+ <div className={cn(hasError && "p-2 border border-red-200 rounded-lg bg-red-50/30")}>
389
+ {props.label && (
390
+ <label className="flex items-center">
391
+ <input
392
+ type="checkbox"
393
+ className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500 focus:ring-2 mr-2"
394
+ checked={props.value || false}
395
+ onChange={(e) => props.onChange(e.target.checked)}
396
+ disabled={props.disabled}
397
+ />
398
+ <span className="text-sm text-gray-700">
399
+ {props.label}
400
+ {props.required && <span className="text-red-500 ml-1">*</span>}
401
+ </span>
402
+ </label>
403
+ )}
404
+ </div>
405
+ <ErrorMessage errors={props.rawErrors} />
342
406
  {props.description && <p className="text-xs text-gray-500 mt-1 ml-6">{props.description}</p>}
343
407
  </div>
344
408
  );
@@ -346,6 +410,7 @@ const CustomBooleanWidget = (props: any) => {
346
410
 
347
411
  // 自定义文件上传组件
348
412
  const CustomFileWidget = (props: any) => {
413
+ const hasError = props.rawErrors && props.rawErrors.length > 0;
349
414
  const handleFileChange = (e: any) => {
350
415
  const files = e.target.files;
351
416
  if (props.multiple) {
@@ -360,19 +425,23 @@ const CustomFileWidget = (props: any) => {
360
425
  return (
361
426
  <div className="mb-4">
362
427
  {props.label && (
363
- <label className="block text-sm font-medium text-gray-700 mb-1">
428
+ <label className="block text-sm font-medium text-gray-900 mb-1">
364
429
  {props.label}
365
430
  {props.required && <span className="text-red-500 ml-1">*</span>}
366
431
  </label>
367
432
  )}
368
433
  <input
369
434
  type="file"
370
- 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"
435
+ className={cn(
436
+ "w-full rounded-lg border bg-gray-50/50 p-2 text-sm text-gray-900 focus:outline-none focus:ring-2 transition-all 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",
437
+ hasError ? "border-red-500 focus:ring-red-100 focus:border-red-500" : "border-gray-200 focus:ring-blue-100 focus:border-blue-400"
438
+ )}
371
439
  onChange={handleFileChange}
372
440
  disabled={props.disabled}
373
441
  multiple={props.multiple}
374
442
  accept={props.accept}
375
443
  />
444
+ <ErrorMessage errors={props.rawErrors} />
376
445
  {props.description && <p className="text-xs text-gray-500 mt-1">{props.description}</p>}
377
446
  </div>
378
447
  );
@@ -380,15 +449,17 @@ const CustomFileWidget = (props: any) => {
380
449
 
381
450
  // 自定义数组字段组件
382
451
  const CustomArrayField = (props: any) => {
452
+ const hasError = props.rawErrors && props.rawErrors.length > 0;
453
+
383
454
  return (
384
455
  <div className="mb-4">
385
456
  {props.label && (
386
- <label className="block text-sm font-medium text-gray-700 mb-2">
457
+ <label className="block text-sm font-medium text-gray-900 mb-2">
387
458
  {props.label}
388
459
  {props.required && <span className="text-red-500 ml-1">*</span>}
389
460
  </label>
390
461
  )}
391
- <div className="space-y-2">
462
+ <div className={cn("space-y-2", hasError && "p-2 border border-red-200 rounded-lg bg-red-50/30")}>
392
463
  {props.items.map((item: any, index: number) => (
393
464
  <div key={index} className="flex items-start gap-2 p-3 border border-gray-200 rounded-lg">
394
465
  <div className="flex-1">{item.children}</div>
@@ -402,6 +473,7 @@ const CustomArrayField = (props: any) => {
402
473
  添加项目
403
474
  </button>
404
475
  </div>
476
+ <ErrorMessage errors={props.rawErrors} />
405
477
  {props.description && <p className="text-xs text-gray-500 mt-1">{props.description}</p>}
406
478
  </div>
407
479
  );
@@ -412,12 +484,12 @@ const CustomFieldTemplate = (props: any) => {
412
484
  return <div className={`${props.className} mb-4`}>{props.children}</div>;
413
485
  };
414
486
 
415
- // 自定义对象字段模板
487
+ // 自定义对象字段模板 - 默认竖向排列
416
488
  const CustomObjectFieldTemplate = (props: any) => {
417
489
  return (
418
490
  <div className="mb-4">
419
491
  {props.title && <h3 className="text-sm font-medium text-gray-700 mb-2">{props.title}</h3>}
420
- <div className="grid grid-cols-1 md:grid-cols-3 gap-4 pl-4 border-l-2 border-gray-200">
492
+ <div className="space-y-3 pl-4 border-l-2 border-gray-200">
421
493
  {props.properties.map((prop: any) => (
422
494
  <div key={prop.name}>{prop.content}</div>
423
495
  ))}
@@ -460,7 +532,7 @@ export const ask_user_to_fill_form = createUITool({
460
532
  const data = tool.getInputRepaired();
461
533
  const output = tool.getJSONOutputSafe();
462
534
  const formSchema = data?.schema || {};
463
- const canInteract = tool.state === "interrupted";
535
+ const canInteract = tool.state === "interrupted" || tool.state === "loading";
464
536
 
465
537
  const handleSubmit = (formData: any) => {
466
538
  try {
@@ -543,8 +615,8 @@ export const ask_user_to_fill_form = createUITool({
543
615
  </div>
544
616
  </div>
545
617
  </div>
546
- <div className="p-4 space-y-3">
547
- {data?.description && <div className="text-sm text-gray-600">{data.description}</div>}
618
+ <div className="p-3 space-y-3">
619
+ {data?.description && <div className="text-sm font-medium text-gray-900">{data.description}</div>}
548
620
  <ErrorBoundary>
549
621
  <Form
550
622
  readonly={!canInteract}
@@ -578,6 +650,25 @@ export const ask_user_to_fill_form = createUITool({
578
650
  } catch (error) {
579
651
  // 外层错误边界,捕获所有未处理的错误
580
652
  console.error("表单组件渲染错误:", error);
653
+ const errorMessage = error instanceof Error ? error.message : "表单配置有误,请检查 schema 格式是否正确。";
654
+
655
+ // 如果可以交互,直接将错误响应到远端
656
+ const canInteract = tool.state === "interrupted" || tool.state === "loading";
657
+ if (canInteract) {
658
+ try {
659
+ tool.sendResumeData({
660
+ /** @ts-ignore */
661
+ type: "respond",
662
+ message: JSON.stringify({
663
+ error: "表单加载失败",
664
+ message: errorMessage,
665
+ }),
666
+ });
667
+ } catch (sendError) {
668
+ console.error("发送错误响应失败:", sendError);
669
+ }
670
+ }
671
+
581
672
  return (
582
673
  <div className="w-full my-1 border border-red-200 bg-red-50 rounded-xl overflow-hidden">
583
674
  <div className="p-4">
@@ -585,7 +676,7 @@ export const ask_user_to_fill_form = createUITool({
585
676
  <AlertCircle className="w-5 h-5 text-red-600" />
586
677
  <h3 className="text-sm font-medium text-red-900">表单加载失败</h3>
587
678
  </div>
588
- <p className="text-xs text-red-700">表单配置有误,请检查 schema 格式是否正确。</p>
679
+ <p className="text-xs text-red-700">{errorMessage}</p>
589
680
  </div>
590
681
  </div>
591
682
  );
@@ -1,6 +1,7 @@
1
- import { ArtifactCommand, createUITool, ToolRenderData } from "@langgraph-js/sdk";
2
- import { FileIcon } from "lucide-react";
1
+ import { ArtifactCommand, createUITool } from "@langgraph-js/sdk";
2
+ import { FileIcon, Code2, Eye, Sparkles, Terminal } from "lucide-react";
3
3
  import { useChat } from "@langgraph-js/sdk/react";
4
+ import { motion } from "motion/react";
4
5
 
5
6
  export const create_artifacts = createUITool({
6
7
  name: "create_artifacts",
@@ -8,26 +9,83 @@ export const create_artifacts = createUITool({
8
9
  parameters: {},
9
10
  onlyRender: true,
10
11
  render(tool: any) {
11
- const data: ArtifactCommand = tool.getInputRepaired();
12
+ const data: Partial<ArtifactCommand> = tool.getInputRepaired();
12
13
  const { setCurrentArtifactById, setShowArtifact } = useChat();
13
14
 
14
15
  const toggleExpand = () => {
16
+ if (!data.id) return;
15
17
  setCurrentArtifactById(data.id, tool.message.id!);
16
18
  setShowArtifact(true);
17
19
  };
18
20
 
21
+ // 根据文件扩展名获取合适的图标
22
+ const getFileIcon = (filename: string = "") => {
23
+ const ext = filename.split(".").pop()?.toLowerCase();
24
+ switch (ext) {
25
+ case "tsx":
26
+ case "jsx":
27
+ case "ts":
28
+ case "js":
29
+ return <Code2 className="w-5 h-5 text-blue-500" />;
30
+ case "py":
31
+ return <Terminal className="w-5 h-5 text-green-500" />;
32
+ case "html":
33
+ case "css":
34
+ return <Code2 className="w-5 h-5 text-orange-500" />;
35
+ default:
36
+ return <FileIcon className="w-5 h-5 text-gray-500" />;
37
+ }
38
+ };
39
+
19
40
  return (
20
- <div className="p-4 space-y-4">
21
- <div className="text-sm text-gray-500">创建文件: {data.title}</div>
22
- <div className="border rounded-lg p-2 hover:bg-gray-50">
23
- <div className="flex items-center justify-between select-none cursor-pointer" onClick={toggleExpand}>
24
- <div className="flex items-center gap-2">
25
- <FileIcon className="w-4 h-4" />
26
- <span className="font-xs">{data.title}</span> version: {data.id}
41
+ <motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="p-2 max-w-md">
42
+ {/* Artifact 卡片 */}
43
+ <div
44
+ className="group relative overflow-hidden bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl cursor-pointer transition-all duration-300 hover:shadow-2xl hover:shadow-indigo-500/10 dark:hover:shadow-indigo-500/20 hover:border-indigo-300 dark:hover:border-indigo-700/50"
45
+ onClick={toggleExpand}
46
+ >
47
+ {/* 背景光晕效果 */}
48
+ <div className="absolute -top-24 -right-24 w-48 h-48 bg-indigo-500/5 dark:bg-indigo-500/10 rounded-full blur-3xl group-hover:bg-indigo-500/10 dark:group-hover:bg-indigo-500/20 transition-colors duration-500" />
49
+
50
+ <div className="p-4 relative flex items-center gap-4">
51
+ {/* 图标容器 */}
52
+ <div className="relative shrink-0">
53
+ <div className="p-3 rounded-xl bg-gray-50 dark:bg-gray-800 group-hover:bg-white dark:group-hover:bg-gray-700 shadow-sm group-hover:shadow-md transition-all duration-300">
54
+ {getFileIcon(data.title)}
55
+ </div>
56
+ <div className="absolute -top-1 -right-1 p-1 rounded-full bg-indigo-500 shadow-lg shadow-indigo-500/50">
57
+ <Sparkles className="w-2 h-2 text-white" />
58
+ </div>
59
+ </div>
60
+
61
+ {/* 文字内容 */}
62
+ <div className="flex-1 min-w-0 space-y-1.5">
63
+ <h5 className="text-sm font-bold text-gray-900 dark:text-gray-100 truncate group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition-colors">{data.title}</h5>
64
+ <div className="flex flex-wrap items-center gap-2">
65
+ <span className="inline-block max-w-[150px] truncate text-[10px] uppercase tracking-wider font-bold px-1.5 py-0.5 rounded-md bg-indigo-50 dark:bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 border border-indigo-100/50 dark:border-indigo-500/20 whitespace-nowrap">
66
+ ID: {data.id}
67
+ </span>
68
+ <span className="flex items-center gap-1 text-[11px] text-gray-400 dark:text-gray-500 font-medium whitespace-nowrap shrink-0">
69
+ <span className="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse" />
70
+ 可交互
71
+ </span>
72
+ </div>
27
73
  </div>
74
+
75
+ {/* 操作按钮 */}
76
+ <div className="shrink-0">
77
+ <div className="p-2.5 rounded-xl bg-gray-50 dark:bg-gray-800 group-hover:bg-indigo-500 group-hover:text-white group-hover:shadow-lg group-hover:shadow-indigo-500/30 transition-all duration-300">
78
+ <Eye className="w-4 h-4 group-hover:scale-110 transition-transform" />
79
+ </div>
80
+ </div>
81
+ </div>
82
+
83
+ {/* 悬停时的底部进度条式装饰 */}
84
+ <div className="h-0.5 w-full bg-transparent overflow-hidden">
85
+ <motion.div initial={{ x: "-100%" }} whileHover={{ x: "0%" }} className="h-full bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500" transition={{ duration: 0.4 }} />
28
86
  </div>
29
87
  </div>
30
- </div>
88
+ </motion.div>
31
89
  );
32
90
  },
33
91
  });
@@ -0,0 +1,47 @@
1
+ import React from "react";
2
+ import SettingFormBase from "./SettingFormBase";
3
+
4
+ interface ArtifactsSettingsData {
5
+ artifactsUrl: string;
6
+ }
7
+
8
+ const initialArtifactsSettings: ArtifactsSettingsData = {
9
+ artifactsUrl: "https://langgraph-artifacts.netlify.app/",
10
+ };
11
+
12
+ const ArtifactsSettings: React.FC = () => {
13
+ return (
14
+ <SettingFormBase settingKey="artifactsUrl" initialData={initialArtifactsSettings}>
15
+ {(formData, handleChange) => (
16
+ <form className="space-y-6">
17
+ <div className="bg-blue-50 dark:bg-blue-900/20 p-4 rounded-lg border border-blue-200 dark:border-blue-800">
18
+ <h4 className="text-sm font-medium text-blue-900 dark:text-blue-100 mb-2">Artifacts 设置</h4>
19
+ <p className="text-sm text-blue-800 dark:text-blue-200 leading-relaxed">
20
+ 配置 artifacts 查看器的 URL。这个 URL 指向用于显示 artifacts 的 Web 组件。
21
+ </p>
22
+ </div>
23
+
24
+ <div>
25
+ <label htmlFor="artifactsUrl" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
26
+ Artifacts URL
27
+ </label>
28
+ <input
29
+ type="url"
30
+ id="artifactsUrl"
31
+ name="artifactsUrl"
32
+ value={formData.artifactsUrl}
33
+ onChange={handleChange}
34
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
35
+ placeholder="https://langgraph-artifacts.netlify.app/"
36
+ />
37
+ <p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
38
+ 输入有效的 URL 地址,用于加载 artifacts 查看器组件。
39
+ </p>
40
+ </div>
41
+ </form>
42
+ )}
43
+ </SettingFormBase>
44
+ );
45
+ };
46
+
47
+ export default ArtifactsSettings;