@langgraph-js/ui 5.6.0 → 5.7.1

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 (80) hide show
  1. package/dist/assets/arc-BN7eXDsr.js +1 -0
  2. package/dist/assets/architectureDiagram-VXUJARFQ-BlClJNg_.js +36 -0
  3. package/dist/assets/{blockDiagram-VD42YOAC-JpCtx8Fw.js → blockDiagram-VD42YOAC-B1yxKMBm.js} +4 -4
  4. package/dist/assets/{c4Diagram-YG6GDRKO-Ct0YUG6f.js → c4Diagram-YG6GDRKO-DYnk7yK1.js} +1 -1
  5. package/dist/assets/channel-W05vnIRi.js +1 -0
  6. package/dist/assets/{chunk-4BX2VUAB-O9tuPwJ2.js → chunk-4BX2VUAB-OBKPWQYL.js} +1 -1
  7. package/dist/assets/{chunk-55IACEB6-B1LOZAa5.js → chunk-55IACEB6-gYWv8Zf5.js} +1 -1
  8. package/dist/assets/{chunk-B4BG7PRW-CHB7g7y9.js → chunk-B4BG7PRW-C53GIfTN.js} +1 -1
  9. package/dist/assets/{chunk-DI55MBZ5-BZe7PkXl.js → chunk-DI55MBZ5-VE97R82T.js} +1 -1
  10. package/dist/assets/{chunk-FMBD7UC4-IV4Fy30-.js → chunk-FMBD7UC4-1vi-TjGz.js} +1 -1
  11. package/dist/assets/{chunk-QN33PNHL-DCcmoYki.js → chunk-QN33PNHL-BIwuKx1J.js} +1 -1
  12. package/dist/assets/{chunk-QZHKN3VN-BNYpxk7b.js → chunk-QZHKN3VN-voU2PjMs.js} +1 -1
  13. package/dist/assets/{chunk-TZMSLE5B-BgP3duP5.js → chunk-TZMSLE5B-DqFqeo6j.js} +1 -1
  14. package/dist/assets/classDiagram-2ON5EDUG-BAA0T-HR.js +1 -0
  15. package/dist/assets/classDiagram-v2-WZHVMYZB-BAA0T-HR.js +1 -0
  16. package/dist/assets/clone-BnfE7gLV.js +1 -0
  17. package/dist/assets/{cose-bilkent-S5V4N54A-B_7kmvVh.js → cose-bilkent-S5V4N54A-BG8aPbwR.js} +1 -1
  18. package/dist/assets/{dagre-6UL2VRFP-CAxkUiea.js → dagre-6UL2VRFP-B2SQQldX.js} +2 -2
  19. package/dist/assets/diagram-PSM6KHXK-DPT_FKfu.js +24 -0
  20. package/dist/assets/diagram-QEK2KX5R-BSIuCy1e.js +43 -0
  21. package/dist/assets/{diagram-S2PKOQOG-CZOKfwOI.js → diagram-S2PKOQOG-DAQpMvAq.js} +1 -1
  22. package/dist/assets/{erDiagram-Q2GNP2WA-D9v3M4EU.js → erDiagram-Q2GNP2WA-gKwnjOTf.js} +1 -1
  23. package/dist/assets/{flowDiagram-NV44I4VS-B9qnS72A.js → flowDiagram-NV44I4VS-DYhT5qks.js} +1 -1
  24. package/dist/assets/ganttDiagram-LVOFAZNH-WboqEMaA.js +267 -0
  25. package/dist/assets/{gitGraphDiagram-NY62KEGX-Cm36kSO3.js → gitGraphDiagram-NY62KEGX-BRtqtCxN.js} +1 -1
  26. package/dist/assets/{graph-C2p3i6RT.js → graph-BaHCYR4Y.js} +1 -1
  27. package/dist/assets/index-JVKUnJL6.css +1 -0
  28. package/dist/assets/{index-Cf3jkaWJ.js → index-yx5a2qjD.js} +312 -173
  29. package/dist/assets/{infoDiagram-F6ZHWCRC-DkBxmgNt.js → infoDiagram-F6ZHWCRC-DyYvFs8H.js} +1 -1
  30. package/dist/assets/isUndefined-CuNi_bm9.js +1 -0
  31. package/dist/assets/{journeyDiagram-XKPGCS4Q-CVukoP-j.js → journeyDiagram-XKPGCS4Q-BqtfhCdd.js} +1 -1
  32. package/dist/assets/{kanban-definition-3W4ZIXB7-0-SbO6tS.js → kanban-definition-3W4ZIXB7-CyDmlWOW.js} +4 -4
  33. package/dist/assets/layout-higHf58s.js +1 -0
  34. package/dist/assets/mermaid.core-3MG272Pv.js +197 -0
  35. package/dist/assets/min-BoUtT0GV.js +1 -0
  36. package/dist/assets/{mindmap-definition-VGOIOE7T-CDGdm5yw.js → mindmap-definition-VGOIOE7T-Y0yyOiIO.js} +5 -5
  37. package/dist/assets/pieDiagram-ADFJNKIX-D_nzBY3u.js +30 -0
  38. package/dist/assets/{quadrantDiagram-AYHSOK5B-DY0NltyG.js → quadrantDiagram-AYHSOK5B-CUBZfINn.js} +2 -2
  39. package/dist/assets/{requirementDiagram-UZGBJVZJ-DwhwO5lc.js → requirementDiagram-UZGBJVZJ-kfnK1mwU.js} +1 -1
  40. package/dist/assets/sankeyDiagram-TZEHDZUN-DALezZLf.js +10 -0
  41. package/dist/assets/{sequenceDiagram-WL72ISMW-CpTIKJGg.js → sequenceDiagram-WL72ISMW-Ph3PSHlU.js} +1 -1
  42. package/dist/assets/stateDiagram-FKZM4ZOC-BjIg4oDx.js +1 -0
  43. package/dist/assets/stateDiagram-v2-4FDKWEC3-nRC6pSc8.js +1 -0
  44. package/dist/assets/{timeline-definition-IT6M3QCI-1MkGCcpM.js → timeline-definition-IT6M3QCI-Cra1vVbB.js} +3 -3
  45. package/dist/assets/{treemap-KMMF4GRG-pRPfKbhX.js → treemap-KMMF4GRG-DihM0GUo.js} +1 -1
  46. package/dist/assets/xychartDiagram-PRI3JC2R-PhzqQQLa.js +7 -0
  47. package/dist/index.html +2 -2
  48. package/package.json +3 -2
  49. package/src/chat/Chat.tsx +3 -4
  50. package/src/chat/components/ErrorBoundary.tsx +23 -10
  51. package/src/chat/tools/{show_form.tsx → ask_user_to_fill_form.tsx} +283 -114
  52. package/src/chat/tools/ask_user_with_options.tsx +174 -0
  53. package/src/chat/tools/display_information_card.tsx +131 -0
  54. package/src/chat/tools/image_generation.tsx +140 -0
  55. package/src/chat/tools/index.ts +18 -6
  56. package/src/chat/tools/visualize_data_with_chart.tsx +274 -0
  57. package/src/chat/tools/wait_for_user_to_upload_file.tsx +324 -0
  58. package/dist/assets/arc-BNOCe7pe.js +0 -1
  59. package/dist/assets/architectureDiagram-VXUJARFQ-yf1p-wI_.js +0 -36
  60. package/dist/assets/channel-CizfE02g.js +0 -1
  61. package/dist/assets/classDiagram-2ON5EDUG-CFe5Iz8b.js +0 -1
  62. package/dist/assets/classDiagram-v2-WZHVMYZB-CFe5Iz8b.js +0 -1
  63. package/dist/assets/clone-C4H89Tb1.js +0 -1
  64. package/dist/assets/defaultLocale-C4B-KCzX.js +0 -1
  65. package/dist/assets/diagram-PSM6KHXK-BeM-KUrT.js +0 -24
  66. package/dist/assets/diagram-QEK2KX5R-DDl_pKlU.js +0 -43
  67. package/dist/assets/ganttDiagram-LVOFAZNH-C1Hjglog.js +0 -267
  68. package/dist/assets/index-DLwWU25h.css +0 -1
  69. package/dist/assets/init-Gi6I4Gst.js +0 -1
  70. package/dist/assets/isUndefined-DhAvAp_K.js +0 -1
  71. package/dist/assets/layout-fJUHcXGp.js +0 -1
  72. package/dist/assets/linear-RxTDErCI.js +0 -1
  73. package/dist/assets/mermaid.core-Y2zCEMvG.js +0 -197
  74. package/dist/assets/min-BGYgWBoZ.js +0 -1
  75. package/dist/assets/ordinal-Cboi1Yqb.js +0 -1
  76. package/dist/assets/pieDiagram-ADFJNKIX-EEa08coZ.js +0 -30
  77. package/dist/assets/sankeyDiagram-TZEHDZUN-awXHtkWF.js +0 -10
  78. package/dist/assets/stateDiagram-FKZM4ZOC-vNzQUjNr.js +0 -1
  79. package/dist/assets/stateDiagram-v2-4FDKWEC3-D7uk1o7q.js +0 -1
  80. package/dist/assets/xychartDiagram-PRI3JC2R-DaoAFPdZ.js +0 -7
@@ -1,34 +1,52 @@
1
1
  import { createUITool, ToolManager } from "@langgraph-js/sdk";
2
2
  import Form, { IChangeEvent } from "@rjsf/core";
3
3
  import ErrorBoundary from "../components/ErrorBoundary";
4
- import { FileText, Eye, EyeOff, ChevronDown, X, Plus, Minus } from "lucide-react";
4
+ import { FileText, Eye, EyeOff, ChevronDown, X, Plus, CheckCircle, AlertCircle } from "lucide-react";
5
5
  import { z } from "zod";
6
6
  import validator from "@rjsf/validator-ajv8";
7
7
  import { useState } from "react";
8
+ import { Button } from "@/components/ui/button";
9
+ import { cn } from "@/lib/utils";
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
+ };
8
21
 
9
22
  // 自定义文本输入组件
10
23
  const CustomTextWidget = (props: any) => {
11
24
  const hasMinLength = props.options?.minLength;
12
25
  const hasMaxLength = props.options?.maxLength;
13
26
  const currentLength = props.value?.length || 0;
27
+ const hasError = props.rawErrors && props.rawErrors.length > 0;
14
28
 
15
29
  return (
16
30
  <div className="mb-4">
17
31
  {props.label && (
18
- <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">
19
33
  {props.label}
20
34
  {props.required && <span className="text-red-500 ml-1">*</span>}
21
35
  </label>
22
36
  )}
23
37
  <input
24
38
  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"
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
+ )}
26
43
  value={props.value || ""}
27
44
  onChange={(e) => props.onChange(e.target.value)}
28
45
  placeholder={props.placeholder}
29
46
  disabled={props.disabled}
30
47
  maxLength={hasMaxLength ? props.options.maxLength : undefined}
31
48
  />
49
+ <ErrorMessage errors={props.rawErrors} />
32
50
  {props.description && <p className="text-xs text-gray-500 mt-1">{props.description}</p>}
33
51
  {(hasMinLength || hasMaxLength) && (
34
52
  <div className="flex justify-end mt-1">
@@ -48,11 +66,12 @@ const CustomTextWidget = (props: any) => {
48
66
  // 自定义密码输入组件
49
67
  const CustomPasswordWidget = (props: any) => {
50
68
  const [showPassword, setShowPassword] = useState(false);
69
+ const hasError = props.rawErrors && props.rawErrors.length > 0;
51
70
 
52
71
  return (
53
72
  <div className="mb-4">
54
73
  {props.label && (
55
- <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">
56
75
  {props.label}
57
76
  {props.required && <span className="text-red-500 ml-1">*</span>}
58
77
  </label>
@@ -60,7 +79,10 @@ const CustomPasswordWidget = (props: any) => {
60
79
  <div className="relative">
61
80
  <input
62
81
  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"
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
+ )}
64
86
  value={props.value || ""}
65
87
  onChange={(e) => props.onChange(e.target.value)}
66
88
  placeholder={props.placeholder}
@@ -70,6 +92,7 @@ const CustomPasswordWidget = (props: any) => {
70
92
  {showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
71
93
  </button>
72
94
  </div>
95
+ <ErrorMessage errors={props.rawErrors} />
73
96
  {props.description && <p className="text-xs text-gray-500 mt-1">{props.description}</p>}
74
97
  </div>
75
98
  );
@@ -80,17 +103,21 @@ const CustomTextareaWidget = (props: any) => {
80
103
  const hasMinLength = props.options?.minLength;
81
104
  const hasMaxLength = props.options?.maxLength;
82
105
  const currentLength = props.value?.length || 0;
106
+ const hasError = props.rawErrors && props.rawErrors.length > 0;
83
107
 
84
108
  return (
85
109
  <div className="mb-4">
86
110
  {props.label && (
87
- <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">
88
112
  {props.label}
89
113
  {props.required && <span className="text-red-500 ml-1">*</span>}
90
114
  </label>
91
115
  )}
92
116
  <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"
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
+ )}
94
121
  value={props.value || ""}
95
122
  onChange={(e) => props.onChange(e.target.value)}
96
123
  placeholder={props.placeholder}
@@ -98,6 +125,7 @@ const CustomTextareaWidget = (props: any) => {
98
125
  rows={props.options?.rows || 4}
99
126
  maxLength={hasMaxLength ? props.options.maxLength : undefined}
100
127
  />
128
+ <ErrorMessage errors={props.rawErrors} />
101
129
  {props.description && <p className="text-xs text-gray-500 mt-1">{props.description}</p>}
102
130
  {(hasMinLength || hasMaxLength) && (
103
131
  <div className="flex justify-end mt-1">
@@ -116,17 +144,22 @@ const CustomTextareaWidget = (props: any) => {
116
144
 
117
145
  // 自定义选择框组件
118
146
  const CustomSelectWidget = (props: any) => {
147
+ const hasError = props.rawErrors && props.rawErrors.length > 0;
148
+
119
149
  return (
120
150
  <div className="mb-4">
121
151
  {props.label && (
122
- <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">
123
153
  {props.label}
124
154
  {props.required && <span className="text-red-500 ml-1">*</span>}
125
155
  </label>
126
156
  )}
127
157
  <div className="relative">
128
158
  <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"
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
+ )}
130
163
  value={props.value || ""}
131
164
  onChange={(e) => props.onChange(e.target.value)}
132
165
  disabled={props.disabled}
@@ -144,6 +177,7 @@ const CustomSelectWidget = (props: any) => {
144
177
  </select>
145
178
  <ChevronDown size={16} className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 pointer-events-none" />
146
179
  </div>
180
+ <ErrorMessage errors={props.rawErrors} />
147
181
  {props.description && <p className="text-xs text-gray-500 mt-1">{props.description}</p>}
148
182
  </div>
149
183
  );
@@ -151,15 +185,17 @@ const CustomSelectWidget = (props: any) => {
151
185
 
152
186
  // 自定义多选框组件
153
187
  const CustomCheckboxWidget = (props: any) => {
188
+ const hasError = props.rawErrors && props.rawErrors.length > 0;
189
+
154
190
  return (
155
191
  <div className="mb-4">
156
192
  {props.label && (
157
- <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">
158
194
  {props.label}
159
195
  {props.required && <span className="text-red-500 ml-1">*</span>}
160
196
  </label>
161
197
  )}
162
- <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")}>
163
199
  {props.options?.enumOptions?.map((option: any, index: number) => (
164
200
  <label key={index} className="flex items-center">
165
201
  <input
@@ -180,6 +216,7 @@ const CustomCheckboxWidget = (props: any) => {
180
216
  </label>
181
217
  ))}
182
218
  </div>
219
+ <ErrorMessage errors={props.rawErrors} />
183
220
  {props.description && <p className="text-xs text-gray-500 mt-1">{props.description}</p>}
184
221
  </div>
185
222
  );
@@ -187,15 +224,17 @@ const CustomCheckboxWidget = (props: any) => {
187
224
 
188
225
  // 自定义单选按钮组件
189
226
  const CustomRadioWidget = (props: any) => {
227
+ const hasError = props.rawErrors && props.rawErrors.length > 0;
228
+
190
229
  return (
191
230
  <div className="mb-4">
192
231
  {props.label && (
193
- <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">
194
233
  {props.label}
195
234
  {props.required && <span className="text-red-500 ml-1">*</span>}
196
235
  </label>
197
236
  )}
198
- <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")}>
199
238
  {props.options?.enumOptions?.map((option: any, index: number) => (
200
239
  <label key={index} className="flex items-center">
201
240
  <input
@@ -211,6 +250,7 @@ const CustomRadioWidget = (props: any) => {
211
250
  </label>
212
251
  ))}
213
252
  </div>
253
+ <ErrorMessage errors={props.rawErrors} />
214
254
  {props.description && <p className="text-xs text-gray-500 mt-1">{props.description}</p>}
215
255
  </div>
216
256
  );
@@ -218,17 +258,22 @@ const CustomRadioWidget = (props: any) => {
218
258
 
219
259
  // 自定义数字输入组件
220
260
  const CustomNumberWidget = (props: any) => {
261
+ const hasError = props.rawErrors && props.rawErrors.length > 0;
262
+
221
263
  return (
222
264
  <div className="mb-4">
223
265
  {props.label && (
224
- <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">
225
267
  {props.label}
226
268
  {props.required && <span className="text-red-500 ml-1">*</span>}
227
269
  </label>
228
270
  )}
229
271
  <input
230
272
  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"
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
+ )}
232
277
  value={props.value || ""}
233
278
  onChange={(e) => props.onChange(Number(e.target.value))}
234
279
  placeholder={props.placeholder}
@@ -237,6 +282,7 @@ const CustomNumberWidget = (props: any) => {
237
282
  max={props.options?.max}
238
283
  step={props.options?.step}
239
284
  />
285
+ <ErrorMessage errors={props.rawErrors} />
240
286
  {props.description && <p className="text-xs text-gray-500 mt-1">{props.description}</p>}
241
287
  </div>
242
288
  );
@@ -244,15 +290,17 @@ const CustomNumberWidget = (props: any) => {
244
290
 
245
291
  // 自定义滑块组件
246
292
  const CustomRangeWidget = (props: any) => {
293
+ const hasError = props.rawErrors && props.rawErrors.length > 0;
294
+
247
295
  return (
248
296
  <div className="mb-4">
249
297
  {props.label && (
250
- <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">
251
299
  {props.label}
252
300
  {props.required && <span className="text-red-500 ml-1">*</span>}
253
301
  </label>
254
302
  )}
255
- <div className="px-2">
303
+ <div className={cn("px-2", hasError && "p-2 border border-red-200 rounded-lg bg-red-50/30")}>
256
304
  <input
257
305
  type="range"
258
306
  className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider"
@@ -269,6 +317,7 @@ const CustomRangeWidget = (props: any) => {
269
317
  <span>{props.options?.max || 100}</span>
270
318
  </div>
271
319
  </div>
320
+ <ErrorMessage errors={props.rawErrors} />
272
321
  {props.description && <p className="text-xs text-gray-500 mt-1">{props.description}</p>}
273
322
  </div>
274
323
  );
@@ -276,21 +325,27 @@ const CustomRangeWidget = (props: any) => {
276
325
 
277
326
  // 自定义日期选择器组件
278
327
  const CustomDateWidget = (props: any) => {
328
+ const hasError = props.rawErrors && props.rawErrors.length > 0;
329
+
279
330
  return (
280
331
  <div className="mb-4">
281
332
  {props.label && (
282
- <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">
283
334
  {props.label}
284
335
  {props.required && <span className="text-red-500 ml-1">*</span>}
285
336
  </label>
286
337
  )}
287
338
  <input
288
339
  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"
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
+ )}
290
344
  value={props.value || ""}
291
345
  onChange={(e) => props.onChange(e.target.value)}
292
346
  disabled={props.disabled}
293
347
  />
348
+ <ErrorMessage errors={props.rawErrors} />
294
349
  {props.description && <p className="text-xs text-gray-500 mt-1">{props.description}</p>}
295
350
  </div>
296
351
  );
@@ -298,21 +353,27 @@ const CustomDateWidget = (props: any) => {
298
353
 
299
354
  // 自定义日期时间选择器组件
300
355
  const CustomDateTimeWidget = (props: any) => {
356
+ const hasError = props.rawErrors && props.rawErrors.length > 0;
357
+
301
358
  return (
302
359
  <div className="mb-4">
303
360
  {props.label && (
304
- <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">
305
362
  {props.label}
306
363
  {props.required && <span className="text-red-500 ml-1">*</span>}
307
364
  </label>
308
365
  )}
309
366
  <input
310
367
  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"
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
+ )}
312
372
  value={props.value || ""}
313
373
  onChange={(e) => props.onChange(e.target.value)}
314
374
  disabled={props.disabled}
315
375
  />
376
+ <ErrorMessage errors={props.rawErrors} />
316
377
  {props.description && <p className="text-xs text-gray-500 mt-1">{props.description}</p>}
317
378
  </div>
318
379
  );
@@ -320,23 +381,28 @@ const CustomDateTimeWidget = (props: any) => {
320
381
 
321
382
  // 自定义布尔值组件
322
383
  const CustomBooleanWidget = (props: any) => {
384
+ const hasError = props.rawErrors && props.rawErrors.length > 0;
385
+
323
386
  return (
324
387
  <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
- )}
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} />
340
406
  {props.description && <p className="text-xs text-gray-500 mt-1 ml-6">{props.description}</p>}
341
407
  </div>
342
408
  );
@@ -344,6 +410,7 @@ const CustomBooleanWidget = (props: any) => {
344
410
 
345
411
  // 自定义文件上传组件
346
412
  const CustomFileWidget = (props: any) => {
413
+ const hasError = props.rawErrors && props.rawErrors.length > 0;
347
414
  const handleFileChange = (e: any) => {
348
415
  const files = e.target.files;
349
416
  if (props.multiple) {
@@ -358,19 +425,23 @@ const CustomFileWidget = (props: any) => {
358
425
  return (
359
426
  <div className="mb-4">
360
427
  {props.label && (
361
- <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">
362
429
  {props.label}
363
430
  {props.required && <span className="text-red-500 ml-1">*</span>}
364
431
  </label>
365
432
  )}
366
433
  <input
367
434
  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"
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
+ )}
369
439
  onChange={handleFileChange}
370
440
  disabled={props.disabled}
371
441
  multiple={props.multiple}
372
442
  accept={props.accept}
373
443
  />
444
+ <ErrorMessage errors={props.rawErrors} />
374
445
  {props.description && <p className="text-xs text-gray-500 mt-1">{props.description}</p>}
375
446
  </div>
376
447
  );
@@ -378,15 +449,17 @@ const CustomFileWidget = (props: any) => {
378
449
 
379
450
  // 自定义数组字段组件
380
451
  const CustomArrayField = (props: any) => {
452
+ const hasError = props.rawErrors && props.rawErrors.length > 0;
453
+
381
454
  return (
382
455
  <div className="mb-4">
383
456
  {props.label && (
384
- <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">
385
458
  {props.label}
386
459
  {props.required && <span className="text-red-500 ml-1">*</span>}
387
460
  </label>
388
461
  )}
389
- <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")}>
390
463
  {props.items.map((item: any, index: number) => (
391
464
  <div key={index} className="flex items-start gap-2 p-3 border border-gray-200 rounded-lg">
392
465
  <div className="flex-1">{item.children}</div>
@@ -400,6 +473,7 @@ const CustomArrayField = (props: any) => {
400
473
  添加项目
401
474
  </button>
402
475
  </div>
476
+ <ErrorMessage errors={props.rawErrors} />
403
477
  {props.description && <p className="text-xs text-gray-500 mt-1">{props.description}</p>}
404
478
  </div>
405
479
  );
@@ -410,12 +484,12 @@ const CustomFieldTemplate = (props: any) => {
410
484
  return <div className={`${props.className} mb-4`}>{props.children}</div>;
411
485
  };
412
486
 
413
- // 自定义对象字段模板
487
+ // 自定义对象字段模板 - 默认竖向排列
414
488
  const CustomObjectFieldTemplate = (props: any) => {
415
489
  return (
416
490
  <div className="mb-4">
417
491
  {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">
492
+ <div className="space-y-3 pl-4 border-l-2 border-gray-200">
419
493
  {props.properties.map((prop: any) => (
420
494
  <div key={prop.name}>{prop.content}</div>
421
495
  ))}
@@ -428,89 +502,184 @@ const CustomObjectFieldTemplate = (props: any) => {
428
502
  // 自定义按钮组件
429
503
  const CustomSubmitButton = (props: any) => {
430
504
  return (
431
- <button
505
+ <Button
432
506
  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
507
  disabled={props.disabled}
508
+ className={cn(
509
+ "px-5 rounded-full transition-all duration-200",
510
+ props.disabled ? "bg-gray-100 text-gray-400" : "bg-blue-600 hover:bg-blue-700 text-white shadow-md hover:shadow-lg hover:shadow-blue-200"
511
+ )}
435
512
  >
436
- {props.submitText || "提交"}
437
- </button>
513
+ {props.submitText || "提交表单"}
514
+ </Button>
438
515
  );
439
516
  };
440
517
 
441
- export const show_form = createUITool({
442
- name: "show_form",
443
- description: "显示动态表单,等待用户填写",
444
- parameters: {
445
- schema: z.any(),
446
- },
518
+ const AskUserToFillFormSchema = {
519
+ title: z.string().describe("Form title"),
520
+ description: z.string().optional().describe("Optional form description"),
521
+ schema: z.record(z.string(), z.any()).describe("JSON Schema for the form (react-jsonschema-form compatible)"),
522
+ };
523
+
524
+ export const ask_user_to_fill_form = createUITool({
525
+ name: "ask_user_to_fill_form",
526
+ description: "Present a form to the user for filling out. Schema follows react-jsonschema-form format.",
527
+ parameters: AskUserToFillFormSchema,
447
528
  onlyRender: false,
448
529
  handler: ToolManager.waitForUIDone,
449
530
  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
- return (
487
- <div className="p-4 bg-white rounded-lg border border-gray-200">
488
- <div className="flex items-center gap-1.5 text-gray-700 mb-3 font-bold">
489
- <FileText className="w-4 h-4 text-blue-500" />
490
- <span>AI 表单</span>
491
- </div>
531
+ try {
532
+ const data = tool.getInputRepaired();
533
+ const output = tool.getJSONOutputSafe();
534
+ const formSchema = data?.schema || {};
535
+ const canInteract = tool.state === "interrupted" || tool.state === "loading";
536
+
537
+ const handleSubmit = (formData: any) => {
538
+ try {
539
+ tool.sendResumeData({
540
+ /** @ts-ignore */
541
+ type: "respond",
542
+ message: JSON.stringify(formData),
543
+ });
544
+ } catch (error) {
545
+ console.error("提交表单数据时出错:", error);
546
+ }
547
+ };
548
+
549
+ // 自定义控件配置
550
+ const customWidgets = {
551
+ text: CustomTextWidget,
552
+ password: CustomPasswordWidget,
553
+ textarea: CustomTextareaWidget,
554
+ select: CustomSelectWidget,
555
+ checkboxes: CustomCheckboxWidget,
556
+ radio: CustomRadioWidget,
557
+ number: CustomNumberWidget,
558
+ range: CustomRangeWidget,
559
+ date: CustomDateWidget,
560
+ datetime: CustomDateTimeWidget,
561
+ file: CustomFileWidget,
562
+ boolean: CustomBooleanWidget,
563
+ };
564
+
565
+ const customFields = {
566
+ array: CustomArrayField,
567
+ };
568
+
569
+ const customTemplates = {
570
+ FieldTemplate: CustomFieldTemplate,
571
+ ObjectFieldTemplate: CustomObjectFieldTemplate,
572
+ ButtonTemplates: {
573
+ SubmitButton: CustomSubmitButton,
574
+ },
575
+ };
576
+
577
+ // 完成状态视图
578
+ if (!canInteract && tool.output) {
579
+ let submittedData;
580
+ try {
581
+ submittedData = typeof tool.output === "string" ? JSON.parse(tool.output) : tool.output;
582
+ } catch (error) {
583
+ submittedData = tool.output;
584
+ }
585
+ return (
586
+ <div className="flex flex-col gap-2 my-1 p-3 bg-gray-50/50 border border-gray-200 rounded-xl">
587
+ <div className="flex items-center gap-2 text-xs text-gray-500">
588
+ <div className="w-5 h-5 rounded-full bg-gray-100 flex items-center justify-center border border-gray-200">
589
+ <CheckCircle className="w-3 h-3" />
590
+ </div>
591
+ <span className="font-medium">Form Submitted</span>
592
+ </div>
593
+
594
+ <div className="space-y-2">
595
+ <div className="text-sm text-gray-600 pl-1">{data?.title || "表单"}</div>
596
+ <div className="text-sm text-gray-900 bg-white px-3 py-2.5 rounded-xl border border-gray-200">
597
+ <pre className="text-xs whitespace-pre-wrap break-all">{JSON.stringify(submittedData, null, 2)}</pre>
598
+ </div>
599
+ </div>
600
+ </div>
601
+ );
602
+ }
603
+
604
+ // 交互状态视图
605
+ return (
492
606
  <ErrorBoundary>
493
- <Form
494
- readonly={tool.state === "done"}
495
- schema={formSchema || {}}
496
- formData={output}
497
- onSubmit={(data: IChangeEvent<any>) => handleSubmit(data.formData)}
498
- validator={validator}
499
- onError={(errors) => {
500
- console.error("表单校验错误:", errors);
501
- alert("表单填写有误,请检查后再提交。");
502
- }}
503
- widgets={customWidgets}
504
- fields={customFields}
505
- templates={customTemplates}
506
- className="custom-form"
507
- >
508
- <div className="flex gap-2 mt-4">
509
- <CustomSubmitButton />
607
+ <div className="w-[70%] my-1 border border-gray-200 bg-white shadow-none rounded-xl overflow-hidden">
608
+ <div className="pb-2 p-3 border-b border-gray-50 bg-gray-50/30">
609
+ <div className="flex items-center justify-between">
610
+ <div className="flex items-center gap-2">
611
+ <div className="w-7 h-7 rounded-full bg-blue-50 text-blue-600 flex items-center justify-center">
612
+ <FileText className="w-3.5 h-3.5" />
613
+ </div>
614
+ <h3 className="text-sm font-medium text-gray-900">{data?.title || "表单"}</h3>
615
+ </div>
616
+ </div>
510
617
  </div>
511
- </Form>
618
+ <div className="p-3 space-y-3">
619
+ {data?.description && <div className="text-sm font-medium text-gray-900">{data.description}</div>}
620
+ <ErrorBoundary>
621
+ <Form
622
+ readonly={!canInteract}
623
+ schema={formSchema}
624
+ formData={output}
625
+ onSubmit={(formData: IChangeEvent<any>) => {
626
+ try {
627
+ handleSubmit(formData.formData);
628
+ } catch (error) {
629
+ console.error("表单提交处理错误:", error);
630
+ }
631
+ }}
632
+ validator={validator}
633
+ onError={(errors) => {
634
+ console.error("表单校验错误:", errors);
635
+ }}
636
+ widgets={customWidgets}
637
+ fields={customFields}
638
+ templates={customTemplates}
639
+ className="custom-form"
640
+ >
641
+ <div className="flex justify-end pt-2">
642
+ <CustomSubmitButton disabled={!canInteract} />
643
+ </div>
644
+ </Form>
645
+ </ErrorBoundary>
646
+ </div>
647
+ </div>
512
648
  </ErrorBoundary>
513
- </div>
514
- );
649
+ );
650
+ } catch (error) {
651
+ // 外层错误边界,捕获所有未处理的错误
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
+
672
+ return (
673
+ <div className="w-full my-1 border border-red-200 bg-red-50 rounded-xl overflow-hidden">
674
+ <div className="p-4">
675
+ <div className="flex items-center gap-2 mb-2">
676
+ <AlertCircle className="w-5 h-5 text-red-600" />
677
+ <h3 className="text-sm font-medium text-red-900">表单加载失败</h3>
678
+ </div>
679
+ <p className="text-xs text-red-700">{errorMessage}</p>
680
+ </div>
681
+ </div>
682
+ );
683
+ }
515
684
  },
516
685
  });