@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
@@ -0,0 +1,274 @@
1
+ import { createUITool } from "@langgraph-js/sdk";
2
+ import { z } from "zod";
3
+ import { BarChart3, TrendingUp, PieChart, ScatterChart, AreaChart, Radar, Grid3x3, BarChart } from "lucide-react";
4
+ import {
5
+ LineChart,
6
+ Line,
7
+ BarChart as RechartsBarChart,
8
+ Bar,
9
+ PieChart as RechartsPieChart,
10
+ Pie,
11
+ Cell,
12
+ ScatterChart as RechartsScatterChart,
13
+ Scatter,
14
+ AreaChart as RechartsAreaChart,
15
+ Area,
16
+ RadarChart as RechartsRadarChart,
17
+ PolarGrid,
18
+ PolarAngleAxis,
19
+ PolarRadiusAxis,
20
+ Radar as RechartsRadar,
21
+ XAxis,
22
+ YAxis,
23
+ CartesianGrid,
24
+ Tooltip,
25
+ Legend,
26
+ ResponsiveContainer,
27
+ ComposedChart,
28
+ } from "recharts";
29
+
30
+ const VisualizeDataWithChartSchema = {
31
+ title: z.string().describe("Chart title"),
32
+ description: z.string().optional().describe("Optional chart description"),
33
+ chart_type: z.enum(["line", "bar", "pie", "scatter", "area", "radar", "heatmap", "histogram"]).describe("Type of chart to render"),
34
+ data: z.array(z.record(z.string(), z.any())).describe("Chart data points"),
35
+ x_axis: z
36
+ .object({
37
+ label: z.string().describe("X-axis label"),
38
+ field: z.string().describe("Data field for X-axis"),
39
+ })
40
+ .optional()
41
+ .describe("X-axis configuration"),
42
+ y_axis: z
43
+ .object({
44
+ label: z.string().describe("Y-axis label"),
45
+ field: z.string().describe("Data field for Y-axis"),
46
+ })
47
+ .optional()
48
+ .describe("Y-axis configuration"),
49
+ options: z.record(z.string(), z.any()).optional().describe("Additional chart configuration options"),
50
+ };
51
+
52
+ const getChartIcon = (chartType: string) => {
53
+ switch (chartType) {
54
+ case "line":
55
+ return TrendingUp;
56
+ case "bar":
57
+ return BarChart3;
58
+ case "pie":
59
+ return PieChart;
60
+ case "scatter":
61
+ return ScatterChart;
62
+ case "area":
63
+ return AreaChart;
64
+ case "radar":
65
+ return Radar;
66
+ case "heatmap":
67
+ return Grid3x3;
68
+ case "histogram":
69
+ return BarChart;
70
+ default:
71
+ return BarChart3;
72
+ }
73
+ };
74
+
75
+ const COLORS = ["#3b82f6", "#10b981", "#f59e0b", "#ef4444", "#8b5cf6", "#06b6d4", "#ec4899", "#84cc16"];
76
+
77
+ const renderChart = (chartType: string, data: Record<string, any>[], xAxis?: { label: string; field: string }, yAxis?: { label: string; field: string }, options?: Record<string, any>) => {
78
+ if (!data || data.length === 0) {
79
+ return <div className="text-sm text-gray-500 p-8 text-center">No data available</div>;
80
+ }
81
+
82
+ const commonProps = {
83
+ data,
84
+ margin: { top: 5, right: 30, left: 20, bottom: 5 },
85
+ };
86
+
87
+ switch (chartType) {
88
+ case "line":
89
+ return (
90
+ <ResponsiveContainer width="100%" height={400}>
91
+ <LineChart {...commonProps}>
92
+ <CartesianGrid strokeDasharray="3 3" />
93
+ <XAxis dataKey={xAxis?.field || "name"} label={{ value: xAxis?.label, position: "insideBottom", offset: -5 }} />
94
+ <YAxis label={{ value: yAxis?.label, angle: -90, position: "insideLeft" }} />
95
+ <Tooltip />
96
+ <Legend />
97
+ {yAxis?.field && <Line type="monotone" dataKey={yAxis.field} stroke={COLORS[0]} strokeWidth={2} />}
98
+ </LineChart>
99
+ </ResponsiveContainer>
100
+ );
101
+
102
+ case "bar":
103
+ return (
104
+ <ResponsiveContainer width="100%" height={400}>
105
+ <RechartsBarChart {...commonProps}>
106
+ <CartesianGrid strokeDasharray="3 3" />
107
+ <XAxis dataKey={xAxis?.field || "name"} label={{ value: xAxis?.label, position: "insideBottom", offset: -5 }} />
108
+ <YAxis label={{ value: yAxis?.label, angle: -90, position: "insideLeft" }} />
109
+ <Tooltip />
110
+ <Legend />
111
+ {yAxis?.field && <Bar dataKey={yAxis.field} fill={COLORS[0]} />}
112
+ </RechartsBarChart>
113
+ </ResponsiveContainer>
114
+ );
115
+
116
+ case "pie":
117
+ // Pie chart uses different data structure
118
+ const pieData =
119
+ xAxis?.field && yAxis?.field
120
+ ? data.map((item) => ({
121
+ name: item[xAxis.field],
122
+ value: item[yAxis.field],
123
+ }))
124
+ : data;
125
+ return (
126
+ <ResponsiveContainer width="100%" height={400}>
127
+ <RechartsPieChart>
128
+ <Pie
129
+ data={pieData}
130
+ cx="50%"
131
+ cy="50%"
132
+ labelLine={false}
133
+ label={(props: any) => {
134
+ const { name, percent } = props;
135
+ return `${name}: ${((percent as number) * 100).toFixed(0)}%`;
136
+ }}
137
+ outerRadius={120}
138
+ fill="#8884d8"
139
+ dataKey={yAxis?.field || "value"}
140
+ >
141
+ {pieData.map((entry, index) => (
142
+ <Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
143
+ ))}
144
+ </Pie>
145
+ <Tooltip />
146
+ <Legend />
147
+ </RechartsPieChart>
148
+ </ResponsiveContainer>
149
+ );
150
+
151
+ case "scatter":
152
+ return (
153
+ <ResponsiveContainer width="100%" height={400}>
154
+ <RechartsScatterChart {...commonProps}>
155
+ <CartesianGrid strokeDasharray="3 3" />
156
+ <XAxis type="number" dataKey={xAxis?.field || "x"} name={xAxis?.label || "X"} />
157
+ <YAxis type="number" dataKey={yAxis?.field || "y"} name={yAxis?.label || "Y"} />
158
+ <Tooltip cursor={{ strokeDasharray: "3 3" }} />
159
+ <Scatter dataKey={yAxis?.field || "y"} fill={COLORS[0]} />
160
+ </RechartsScatterChart>
161
+ </ResponsiveContainer>
162
+ );
163
+
164
+ case "area":
165
+ return (
166
+ <ResponsiveContainer width="100%" height={400}>
167
+ <RechartsAreaChart {...commonProps}>
168
+ <CartesianGrid strokeDasharray="3 3" />
169
+ <XAxis dataKey={xAxis?.field || "name"} label={{ value: xAxis?.label, position: "insideBottom", offset: -5 }} />
170
+ <YAxis label={{ value: yAxis?.label, angle: -90, position: "insideLeft" }} />
171
+ <Tooltip />
172
+ <Legend />
173
+ {yAxis?.field && <Area type="monotone" dataKey={yAxis.field} stroke={COLORS[0]} fill={COLORS[0]} fillOpacity={0.6} />}
174
+ </RechartsAreaChart>
175
+ </ResponsiveContainer>
176
+ );
177
+
178
+ case "radar":
179
+ const radarData = data.map((item) => {
180
+ const result: Record<string, any> = {};
181
+ if (xAxis?.field) {
182
+ result[xAxis.field] = item[xAxis.field];
183
+ }
184
+ if (yAxis?.field) {
185
+ result[yAxis.field] = item[yAxis.field];
186
+ }
187
+ // Include all numeric fields
188
+ Object.keys(item).forEach((key) => {
189
+ if (typeof item[key] === "number" && key !== xAxis?.field && key !== yAxis?.field) {
190
+ result[key] = item[key];
191
+ }
192
+ });
193
+ return result;
194
+ });
195
+ return (
196
+ <ResponsiveContainer width="100%" height={400}>
197
+ <RechartsRadarChart data={radarData}>
198
+ <PolarGrid />
199
+ <PolarAngleAxis dataKey={xAxis?.field || "name"} />
200
+ <PolarRadiusAxis />
201
+ <RechartsRadar name={yAxis?.label || "Value"} dataKey={yAxis?.field || "value"} stroke={COLORS[0]} fill={COLORS[0]} fillOpacity={0.6} />
202
+ <Tooltip />
203
+ <Legend />
204
+ </RechartsRadarChart>
205
+ </ResponsiveContainer>
206
+ );
207
+
208
+ case "heatmap":
209
+ // Heatmap is not directly supported by recharts, use a composed chart as approximation
210
+ return (
211
+ <ResponsiveContainer width="100%" height={400}>
212
+ <ComposedChart {...commonProps}>
213
+ <CartesianGrid strokeDasharray="3 3" />
214
+ <XAxis dataKey={xAxis?.field || "name"} label={{ value: xAxis?.label, position: "insideBottom", offset: -5 }} />
215
+ <YAxis label={{ value: yAxis?.label, angle: -90, position: "insideLeft" }} />
216
+ <Tooltip />
217
+ <Legend />
218
+ {yAxis?.field && (
219
+ <Bar dataKey={yAxis.field} fill={COLORS[0]} radius={[4, 4, 0, 0]}>
220
+ {data.map((entry, index) => (
221
+ <Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
222
+ ))}
223
+ </Bar>
224
+ )}
225
+ </ComposedChart>
226
+ </ResponsiveContainer>
227
+ );
228
+
229
+ case "histogram":
230
+ return (
231
+ <ResponsiveContainer width="100%" height={400}>
232
+ <RechartsBarChart {...commonProps}>
233
+ <CartesianGrid strokeDasharray="3 3" />
234
+ <XAxis dataKey={xAxis?.field || "name"} label={{ value: xAxis?.label, position: "insideBottom", offset: -5 }} />
235
+ <YAxis label={{ value: yAxis?.label, angle: -90, position: "insideLeft" }} />
236
+ <Tooltip />
237
+ <Legend />
238
+ {yAxis?.field && <Bar dataKey={yAxis.field} fill={COLORS[0]} />}
239
+ </RechartsBarChart>
240
+ </ResponsiveContainer>
241
+ );
242
+
243
+ default:
244
+ return <div className="text-sm text-gray-500 p-8 text-center">Unsupported chart type: {chartType}</div>;
245
+ }
246
+ };
247
+
248
+ export const visualize_data_with_chart = createUITool({
249
+ name: "visualize_data_with_chart",
250
+ description: "Generate a dynamic chart for data visualization and user review.",
251
+ parameters: VisualizeDataWithChartSchema,
252
+ onlyRender: true,
253
+ render(tool) {
254
+ const data = tool.getInputRepaired();
255
+ const ChartIcon = getChartIcon(data.chart_type || "line");
256
+
257
+ return (
258
+ <div className="w-[70%] my-1 border border-gray-200 bg-white shadow-none rounded-xl overflow-hidden">
259
+ <div className="pb-2 p-3 border-b border-gray-50 bg-gray-50/30">
260
+ <div className="flex items-center gap-2">
261
+ <div className="w-7 h-7 rounded-full bg-blue-50 text-blue-600 flex items-center justify-center">
262
+ <ChartIcon className="w-3.5 h-3.5" />
263
+ </div>
264
+ <h3 className="text-sm font-medium text-gray-900">{data.title}</h3>
265
+ </div>
266
+ </div>
267
+ <div className="p-4">
268
+ {data.description && <div className="text-sm text-gray-600 mb-4">{data.description}</div>}
269
+ <div className="w-full">{renderChart(data.chart_type || "line", (data.data || []) as Record<string, any>[], data.x_axis, data.y_axis, data.options)}</div>
270
+ </div>
271
+ </div>
272
+ );
273
+ },
274
+ });
@@ -0,0 +1,324 @@
1
+ import { createUITool, ToolManager } from "@langgraph-js/sdk";
2
+ import { useState, useRef } from "react";
3
+ import { z } from "zod";
4
+ import { UploadCloud, X, File, CheckCircle, AlertCircle, Loader2 } from "lucide-react";
5
+ import { Button } from "@/components/ui/button";
6
+ import { cn } from "@/lib/utils";
7
+ import { TmpFilesClient } from "@/chat/FileUpload";
8
+
9
+ const WaitForUserToUploadFileSchema = {
10
+ title: z.string().describe("Title for the upload section"),
11
+ description: z.string().optional().describe("Optional description text"),
12
+ accept: z.array(z.string()).optional().describe("Accepted file types (e.g., ['image/*', '.pdf', '.docx'])"),
13
+ multiple: z.boolean().default(false).describe("Allow multiple file uploads"),
14
+ max_size_mb: z.number().positive().optional().describe("Maximum file size in MB"),
15
+ required: z.boolean().default(true).describe("Whether file upload is required"),
16
+ };
17
+
18
+ interface UploadedFileInfo {
19
+ file_name: string;
20
+ file_size: number;
21
+ file_type: string;
22
+ file_url: string;
23
+ }
24
+
25
+ export const wait_for_user_to_upload_file = createUITool({
26
+ name: "wait_for_user_to_upload_file",
27
+ description: "Wait for the user to upload one or more files.",
28
+ parameters: WaitForUserToUploadFileSchema,
29
+ handler: ToolManager.waitForUIDone,
30
+ onlyRender: false,
31
+ render(tool) {
32
+ const data = tool.getInputRepaired();
33
+ const [uploadedFiles, setUploadedFiles] = useState<UploadedFileInfo[]>([]);
34
+ const [uploading, setUploading] = useState(false);
35
+ const [error, setError] = useState<string>("");
36
+ const fileInputRef = useRef<HTMLInputElement>(null);
37
+ const client = new TmpFilesClient();
38
+ const canInteract = tool.state === "interrupted" || tool.state === "loading";
39
+
40
+ const formatFileSize = (bytes: number): string => {
41
+ if (bytes === 0) return "0 Bytes";
42
+ const k = 1024;
43
+ const sizes = ["Bytes", "KB", "MB", "GB"];
44
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
45
+ return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
46
+ };
47
+
48
+ const validateFile = (file: File): string | null => {
49
+ // 检查文件大小
50
+ if (data.max_size_mb) {
51
+ const maxSizeBytes = data.max_size_mb * 1024 * 1024;
52
+ if (file.size > maxSizeBytes) {
53
+ return `文件大小超过限制 (最大 ${data.max_size_mb}MB)`;
54
+ }
55
+ }
56
+
57
+ // 检查文件类型
58
+ if (data.accept && data.accept.length > 0) {
59
+ const fileType = (file.type || "").toLowerCase();
60
+ const fileName = (file.name || "").toLowerCase();
61
+
62
+ const normalize = (v: string) => v.trim().toLowerCase();
63
+
64
+ const matches = data.accept.map(normalize).some((pattern) => {
65
+ // 全放行:*/* 或 *
66
+ if (pattern === "*/*" || pattern === "*") return true;
67
+
68
+ // 扩展名:.pdf / .docx
69
+ if (pattern.startsWith(".")) return fileName.endsWith(pattern);
70
+
71
+ // MIME 前缀:image/* -> image/
72
+ if (pattern.includes("/") && pattern.endsWith("/*")) {
73
+ const prefix = pattern.slice(0, -1); // keep trailing '/'
74
+ return fileType.startsWith(prefix);
75
+ }
76
+
77
+ // 精确 MIME:application/pdf
78
+ if (pattern.includes("/")) return fileType === pattern;
79
+
80
+ // 兜底:不认识的 pattern(不匹配)
81
+ return false;
82
+ });
83
+
84
+ if (!matches) {
85
+ return `不支持的文件类型。支持的类型: ${data.accept.join(", ")}`;
86
+ }
87
+ }
88
+
89
+ return null;
90
+ };
91
+
92
+ const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
93
+ const files = Array.from(event.target.files || []);
94
+ if (files.length === 0) return;
95
+
96
+ setError("");
97
+ setUploading(true);
98
+
99
+ try {
100
+ const uploadPromises = files.map(async (file) => {
101
+ const validationError = validateFile(file);
102
+ if (validationError) {
103
+ throw new Error(validationError);
104
+ }
105
+
106
+ const result = await client.upload(file);
107
+ if (!result.data?.url) {
108
+ throw new Error("上传失败:未返回文件 URL");
109
+ }
110
+
111
+ return {
112
+ file_name: file.name,
113
+ file_size: file.size,
114
+ file_type: file.type || "application/octet-stream",
115
+ file_url: result.data.url,
116
+ };
117
+ });
118
+
119
+ const uploaded = await Promise.all(uploadPromises);
120
+
121
+ if (data.multiple) {
122
+ setUploadedFiles((prev) => [...prev, ...uploaded]);
123
+ } else {
124
+ setUploadedFiles(uploaded);
125
+ }
126
+
127
+ // 如果不需要等待用户确认,自动提交
128
+ if (!data.required || uploaded.length > 0) {
129
+ // 自动提交逻辑可以在这里添加
130
+ }
131
+ } catch (err) {
132
+ setError(err instanceof Error ? err.message : "上传失败");
133
+ } finally {
134
+ setUploading(false);
135
+ if (fileInputRef.current) {
136
+ fileInputRef.current.value = "";
137
+ }
138
+ }
139
+ };
140
+
141
+ const handleRemoveFile = (index: number) => {
142
+ setUploadedFiles((prev) => prev.filter((_, i) => i !== index));
143
+ };
144
+
145
+ const handleSubmit = () => {
146
+ if (data.required && uploadedFiles.length === 0) {
147
+ setError("请至少上传一个文件");
148
+ return;
149
+ }
150
+
151
+ tool.sendResumeData({
152
+ /** @ts-ignore */
153
+ type: "respond",
154
+ message: JSON.stringify(uploadedFiles),
155
+ });
156
+ };
157
+
158
+ const handleReset = () => {
159
+ setUploadedFiles([]);
160
+ setError("");
161
+ if (fileInputRef.current) {
162
+ fileInputRef.current.value = "";
163
+ }
164
+ };
165
+
166
+ const isSubmitDisabled = !canInteract || (data.required && uploadedFiles.length === 0) || uploading;
167
+
168
+ // 完成状态视图
169
+ if (!canInteract && tool.output) {
170
+ const outputFiles = typeof tool.output === "string" ? JSON.parse(tool.output) : tool.output;
171
+ return (
172
+ <div className="flex flex-col gap-2 my-1 p-3 bg-gray-50/50 border border-gray-200 rounded-xl">
173
+ <div className="flex items-center gap-2 text-xs text-gray-500">
174
+ <div className="w-5 h-5 rounded-full bg-gray-100 flex items-center justify-center border border-gray-200">
175
+ <CheckCircle className="w-3 h-3" />
176
+ </div>
177
+ <span className="font-medium">Files Uploaded</span>
178
+
179
+ <Button variant="ghost" size="icon" className="h-7 w-7 text-gray-500 hover:text-blue-600 hover:bg-blue-50 rounded-full" onClick={handleReset} title="Reset upload">
180
+ <X className="h-3.5 w-3.5" />
181
+ </Button>
182
+ </div>
183
+
184
+ <div className="space-y-2">
185
+ <div className="text-sm text-gray-600 pl-1">{data.title}</div>
186
+ {Array.isArray(outputFiles) && outputFiles.length > 0 && (
187
+ <div className="space-y-1.5">
188
+ {outputFiles.map((file: UploadedFileInfo, index: number) => (
189
+ <div key={index} className="flex items-center gap-2 text-sm text-gray-900 bg-white px-3 py-2 rounded-lg border border-gray-200">
190
+ <File className="w-4 h-4 text-blue-500 shrink-0" />
191
+ <div className="flex-1 min-w-0">
192
+ <div className="font-medium truncate">{file.file_name}</div>
193
+ <div className="text-xs text-gray-500">{formatFileSize(file.file_size)}</div>
194
+ </div>
195
+ </div>
196
+ ))}
197
+ </div>
198
+ )}
199
+ </div>
200
+ </div>
201
+ );
202
+ }
203
+
204
+ // 交互状态视图
205
+ return (
206
+ <div className="w-full my-1 border border-gray-200 bg-white shadow-none rounded-xl overflow-hidden">
207
+ <div className="pb-2 p-3 border-b border-gray-50 bg-gray-50/30">
208
+ <div className="flex items-center justify-between">
209
+ <div className="flex items-center gap-2">
210
+ <div className="w-7 h-7 rounded-full bg-blue-50 text-blue-600 flex items-center justify-center">
211
+ <UploadCloud className="w-3.5 h-3.5" />
212
+ </div>
213
+ <h3 className="text-sm font-medium text-gray-900">File Upload Required</h3> {tool.state}
214
+ </div>
215
+ {uploadedFiles.length > 0 && (
216
+ <Button variant="ghost" size="icon" className="h-7 w-7 text-gray-500 hover:text-blue-600 hover:bg-blue-50 rounded-full" onClick={handleReset} title="Reset upload">
217
+ <X className="h-3.5 w-3.5" />
218
+ </Button>
219
+ )}
220
+ </div>
221
+ </div>
222
+ <div className="p-3 space-y-3">
223
+ <div>
224
+ <label
225
+ className={cn(
226
+ "flex flex-col items-center justify-center w-full min-h-[180px] border-2 border-dashed rounded-lg cursor-pointer transition-colors p-6",
227
+ uploading ? "border-blue-300 bg-blue-50/50" : "border-gray-300 bg-gray-50/50 hover:border-blue-400 hover:bg-blue-50/30",
228
+ !canInteract && "opacity-50 cursor-not-allowed"
229
+ )}
230
+ >
231
+ <div className="flex flex-col items-center justify-center w-full space-y-3">
232
+ {uploading ? (
233
+ <>
234
+ <Loader2 className="w-10 h-10 text-blue-600 animate-spin" />
235
+ <p className="text-sm font-medium text-gray-700">上传中...</p>
236
+ </>
237
+ ) : (
238
+ <>
239
+ <UploadCloud className="w-10 h-10 text-gray-400" />
240
+ <div className="text-center space-y-1">
241
+ <p className="text-sm font-medium text-gray-900">{data.title}</p>
242
+ {data.description && <p className="text-xs text-gray-600">{data.description}</p>}
243
+ </div>
244
+ <div className="text-center space-y-0.5">
245
+ <p className="text-sm text-gray-600">
246
+ <span className="font-semibold text-blue-600">点击上传</span> 或拖拽文件到此处
247
+ </p>
248
+ <p className="text-xs text-gray-500">{data.multiple ? "可上传多个文件" : "单个文件"}</p>
249
+ </div>
250
+ {(data.accept && data.accept.length > 0) || data.max_size_mb ? (
251
+ <div className="pt-2 border-t border-gray-200 w-full">
252
+ <div className="flex flex-wrap items-center justify-center gap-x-4 gap-y-1 text-xs text-gray-500">
253
+ {data.accept && data.accept.length > 0 && <span>支持类型: {data.accept.join(", ")}</span>}
254
+ {data.max_size_mb && <span>最大大小: {data.max_size_mb}MB</span>}
255
+ </div>
256
+ </div>
257
+ ) : null}
258
+ </>
259
+ )}
260
+ </div>
261
+ <input
262
+ ref={fileInputRef}
263
+ type="file"
264
+ className="hidden"
265
+ accept={data.accept?.join(",")}
266
+ multiple={data.multiple}
267
+ onChange={handleFileChange}
268
+ disabled={!canInteract || uploading}
269
+ />
270
+ </label>
271
+ </div>
272
+
273
+ {error && (
274
+ <div className="flex items-center gap-2 p-2 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700">
275
+ <AlertCircle className="w-4 h-4 shrink-0" />
276
+ <span>{error}</span>
277
+ </div>
278
+ )}
279
+
280
+ {uploadedFiles.length > 0 && (
281
+ <div className="space-y-2">
282
+ <div className="text-xs font-medium text-gray-700">已上传的文件 ({uploadedFiles.length})</div>
283
+ <div className="space-y-1.5">
284
+ {uploadedFiles.map((file, index) => (
285
+ <div key={index} className="flex items-center gap-2 p-2 bg-gray-50 rounded-lg border border-gray-200">
286
+ <File className="w-4 h-4 text-blue-500 shrink-0" />
287
+ <div className="flex-1 min-w-0">
288
+ <div className="text-sm font-medium text-gray-900 truncate">{file.file_name}</div>
289
+ <div className="text-xs text-gray-500">{formatFileSize(file.file_size)}</div>
290
+ </div>
291
+ {canInteract && (
292
+ <Button
293
+ variant="ghost"
294
+ size="icon"
295
+ className="h-6 w-6 text-gray-400 hover:text-red-600 hover:bg-red-50"
296
+ onClick={() => handleRemoveFile(index)}
297
+ title="Remove file"
298
+ >
299
+ <X className="h-3.5 w-3.5" />
300
+ </Button>
301
+ )}
302
+ </div>
303
+ ))}
304
+ </div>
305
+ </div>
306
+ )}
307
+
308
+ <div className="flex justify-end pt-2">
309
+ <Button
310
+ onClick={handleSubmit}
311
+ disabled={isSubmitDisabled}
312
+ className={cn(
313
+ "px-5 rounded-full transition-all duration-200",
314
+ isSubmitDisabled ? "bg-gray-100 text-gray-400" : "bg-blue-600 hover:bg-blue-700 text-white shadow-md hover:shadow-lg hover:shadow-blue-200"
315
+ )}
316
+ >
317
+ {uploadedFiles.length > 0 ? `Submit ${uploadedFiles.length} File${uploadedFiles.length > 1 ? "s" : ""}` : "Submit"}
318
+ </Button>
319
+ </div>
320
+ </div>
321
+ </div>
322
+ );
323
+ },
324
+ });
@@ -1 +0,0 @@
1
- import{L as ln,M as un,N as X,O,P as N,Q as an,R as y,S as tn,T as $,V as _,W as rn,X as o,Y as sn,Z as on,$ as fn}from"./mermaid.core-Y2zCEMvG.js";function cn(l){return l.innerRadius}function yn(l){return l.outerRadius}function gn(l){return l.startAngle}function dn(l){return l.endAngle}function mn(l){return l&&l.padAngle}function pn(l,h,D,S,v,R,Q,u){var E=D-l,i=S-h,n=Q-v,d=u-R,a=d*E-n*i;if(!(a*a<y))return a=(n*(h-R)-d*(l-v))/a,[l+a*E,h+a*i]}function G(l,h,D,S,v,R,Q){var u=l-D,E=h-S,i=(Q?R:-R)/$(u*u+E*E),n=i*E,d=-i*u,a=l+n,s=h+d,f=D+n,c=S+d,V=(a+f)/2,t=(s+c)/2,m=f-a,g=c-s,A=m*m+g*g,T=v-R,P=a*c-f*s,I=(g<0?-1:1)*$(on(0,T*T*A-P*P)),L=(P*g-m*I)/A,M=(-P*m-g*I)/A,w=(P*g+m*I)/A,p=(-P*m+g*I)/A,x=L-V,e=M-t,r=w-V,W=p-t;return x*x+e*e>r*r+W*W&&(L=w,M=p),{cx:L,cy:M,x01:-n,y01:-d,x11:L*(v/T-1),y11:M*(v/T-1)}}function hn(){var l=cn,h=yn,D=N(0),S=null,v=gn,R=dn,Q=mn,u=null,E=ln(i);function i(){var n,d,a=+l.apply(this,arguments),s=+h.apply(this,arguments),f=v.apply(this,arguments)-an,c=R.apply(this,arguments)-an,V=rn(c-f),t=c>f;if(u||(u=n=E()),s<a&&(d=s,s=a,a=d),!(s>y))u.moveTo(0,0);else if(V>tn-y)u.moveTo(s*X(f),s*O(f)),u.arc(0,0,s,f,c,!t),a>y&&(u.moveTo(a*X(c),a*O(c)),u.arc(0,0,a,c,f,t));else{var m=f,g=c,A=f,T=c,P=V,I=V,L=Q.apply(this,arguments)/2,M=L>y&&(S?+S.apply(this,arguments):$(a*a+s*s)),w=_(rn(s-a)/2,+D.apply(this,arguments)),p=w,x=w,e,r;if(M>y){var W=sn(M/a*O(L)),j=sn(M/s*O(L));(P-=W*2)>y?(W*=t?1:-1,A+=W,T-=W):(P=0,A=T=(f+c)/2),(I-=j*2)>y?(j*=t?1:-1,m+=j,g-=j):(I=0,m=g=(f+c)/2)}var Y=s*X(m),Z=s*O(m),z=a*X(T),B=a*O(T);if(w>y){var C=s*X(g),F=s*O(g),H=a*X(A),J=a*O(A),q;if(V<un)if(q=pn(Y,Z,H,J,C,F,z,B)){var K=Y-q[0],U=Z-q[1],k=C-q[0],b=F-q[1],nn=1/O(fn((K*k+U*b)/($(K*K+U*U)*$(k*k+b*b)))/2),en=$(q[0]*q[0]+q[1]*q[1]);p=_(w,(a-en)/(nn-1)),x=_(w,(s-en)/(nn+1))}else p=x=0}I>y?x>y?(e=G(H,J,Y,Z,s,x,t),r=G(C,F,z,B,s,x,t),u.moveTo(e.cx+e.x01,e.cy+e.y01),x<w?u.arc(e.cx,e.cy,x,o(e.y01,e.x01),o(r.y01,r.x01),!t):(u.arc(e.cx,e.cy,x,o(e.y01,e.x01),o(e.y11,e.x11),!t),u.arc(0,0,s,o(e.cy+e.y11,e.cx+e.x11),o(r.cy+r.y11,r.cx+r.x11),!t),u.arc(r.cx,r.cy,x,o(r.y11,r.x11),o(r.y01,r.x01),!t))):(u.moveTo(Y,Z),u.arc(0,0,s,m,g,!t)):u.moveTo(Y,Z),!(a>y)||!(P>y)?u.lineTo(z,B):p>y?(e=G(z,B,C,F,a,-p,t),r=G(Y,Z,H,J,a,-p,t),u.lineTo(e.cx+e.x01,e.cy+e.y01),p<w?u.arc(e.cx,e.cy,p,o(e.y01,e.x01),o(r.y01,r.x01),!t):(u.arc(e.cx,e.cy,p,o(e.y01,e.x01),o(e.y11,e.x11),!t),u.arc(0,0,a,o(e.cy+e.y11,e.cx+e.x11),o(r.cy+r.y11,r.cx+r.x11),t),u.arc(r.cx,r.cy,p,o(r.y11,r.x11),o(r.y01,r.x01),!t))):u.arc(0,0,a,T,A,t)}if(u.closePath(),n)return u=null,n+""||null}return i.centroid=function(){var n=(+l.apply(this,arguments)+ +h.apply(this,arguments))/2,d=(+v.apply(this,arguments)+ +R.apply(this,arguments))/2-un/2;return[X(d)*n,O(d)*n]},i.innerRadius=function(n){return arguments.length?(l=typeof n=="function"?n:N(+n),i):l},i.outerRadius=function(n){return arguments.length?(h=typeof n=="function"?n:N(+n),i):h},i.cornerRadius=function(n){return arguments.length?(D=typeof n=="function"?n:N(+n),i):D},i.padRadius=function(n){return arguments.length?(S=n==null?null:typeof n=="function"?n:N(+n),i):S},i.startAngle=function(n){return arguments.length?(v=typeof n=="function"?n:N(+n),i):v},i.endAngle=function(n){return arguments.length?(R=typeof n=="function"?n:N(+n),i):R},i.padAngle=function(n){return arguments.length?(Q=typeof n=="function"?n:N(+n),i):Q},i.context=function(n){return arguments.length?(u=n??null,i):u},i}export{hn as d};