csv-charts-ai 1.3.2 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,13 +1,34 @@
1
1
  // package.json
2
2
  var package_default = {
3
3
  name: "csv-charts-ai",
4
- version: "1.3.2",
4
+ version: "1.4.0",
5
+ description: "AI-powered CSV analysis, chart generation, and interactive visualization. Parse CSV, detect anomalies, suggest charts, and ask questions about your data using any LLM provider.",
5
6
  type: "module",
7
+ license: "MIT",
8
+ author: "maxgfr",
9
+ homepage: "https://github.com/maxgfr/csv-ai-analyzer/tree/main/packages/csv-charts-ai#readme",
10
+ bugs: {
11
+ url: "https://github.com/maxgfr/csv-ai-analyzer/issues"
12
+ },
6
13
  repository: {
7
14
  type: "git",
8
15
  url: "https://github.com/maxgfr/csv-ai-analyzer",
9
16
  directory: "packages/csv-charts-ai"
10
17
  },
18
+ keywords: [
19
+ "csv",
20
+ "ai",
21
+ "charts",
22
+ "data-analysis",
23
+ "visualization",
24
+ "llm",
25
+ "openai",
26
+ "anthropic",
27
+ "recharts",
28
+ "tabular-data",
29
+ "anomaly-detection",
30
+ "chart-generation"
31
+ ],
11
32
  main: "./dist/index.js",
12
33
  types: "./dist/index.d.ts",
13
34
  exports: {
@@ -21,7 +42,10 @@ var package_default = {
21
42
  ],
22
43
  scripts: {
23
44
  build: "tsup",
24
- dev: "tsup --watch"
45
+ dev: "tsup --watch",
46
+ test: "vitest run",
47
+ "test:watch": "vitest",
48
+ "test:coverage": "vitest run --coverage"
25
49
  },
26
50
  peerDependencies: {
27
51
  ai: "^5.0.0 || ^6.0.0",
@@ -43,12 +67,14 @@ var package_default = {
43
67
  },
44
68
  devDependencies: {
45
69
  "@types/react": "^19.2.7",
70
+ "@vitest/coverage-v8": "^4.1.0",
46
71
  ai: "^6.0.134",
47
72
  "lucide-react": "^0.556.0",
48
73
  react: "^19.2.4",
49
74
  recharts: "^3.5.1",
50
75
  tsup: "^8.5.0",
51
76
  typescript: "^5.9.3",
77
+ vitest: "^4.1.0",
52
78
  zod: "^4.1.13"
53
79
  },
54
80
  publishConfig: {
@@ -62,6 +88,172 @@ var package_default = {
62
88
  // src/version.ts
63
89
  var VERSION = package_default.version;
64
90
 
91
+ // src/csv-parser.ts
92
+ function parseCSV(csv, options = {}) {
93
+ const { hasHeader = true, skipEmpty = true } = options;
94
+ const normalized = csv.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim();
95
+ if (normalized.length === 0) {
96
+ return { headers: [], rows: [], columns: [], rowCount: 0 };
97
+ }
98
+ const delimiter = options.delimiter ?? detectDelimiter(normalized);
99
+ const allRows = parseRows(normalized, delimiter, skipEmpty);
100
+ if (allRows.length === 0) {
101
+ return { headers: [], rows: [], columns: [], rowCount: 0 };
102
+ }
103
+ const expectedCols = allRows[0].length;
104
+ const normalizedRows = allRows.map((row) => {
105
+ if (row.length < expectedCols) {
106
+ return [...row, ...Array(expectedCols - row.length).fill("")];
107
+ }
108
+ return row.slice(0, expectedCols);
109
+ });
110
+ const headers = hasHeader ? normalizedRows[0] : normalizedRows[0].map((_, i) => `Column ${i + 1}`);
111
+ const dataRows = hasHeader ? normalizedRows.slice(1) : normalizedRows;
112
+ const columns = headers.map((name, index) => ({
113
+ name: name.trim(),
114
+ type: inferColumnType(dataRows, index),
115
+ index
116
+ }));
117
+ return {
118
+ headers: headers.map((h) => h.trim()),
119
+ rows: dataRows,
120
+ columns,
121
+ rowCount: dataRows.length
122
+ };
123
+ }
124
+ function detectDelimiter(csv) {
125
+ const firstLines = csv.split("\n").slice(0, 5);
126
+ const candidates = {
127
+ ",": [],
128
+ ";": [],
129
+ " ": [],
130
+ "|": []
131
+ };
132
+ for (const line of firstLines) {
133
+ let inQuotes = false;
134
+ const counts = { ",": 0, ";": 0, " ": 0, "|": 0 };
135
+ for (const char of line) {
136
+ if (char === '"') {
137
+ inQuotes = !inQuotes;
138
+ } else if (!inQuotes && char in counts) {
139
+ counts[char]++;
140
+ }
141
+ }
142
+ for (const [delim, count] of Object.entries(counts)) {
143
+ candidates[delim].push(count);
144
+ }
145
+ }
146
+ let best = ",";
147
+ let bestScore = -1;
148
+ for (const [delim, counts] of Object.entries(candidates)) {
149
+ if (counts.length === 0 || counts[0] === 0) continue;
150
+ const allSame = counts.every((c) => c === counts[0]);
151
+ const avgCount = counts.reduce((a, b) => a + b, 0) / counts.length;
152
+ const score = (allSame ? 1e3 : 0) + avgCount;
153
+ if (score > bestScore) {
154
+ bestScore = score;
155
+ best = delim;
156
+ }
157
+ }
158
+ return best;
159
+ }
160
+ function parseRows(csv, delimiter, skipEmpty) {
161
+ const rows = [];
162
+ let current = [];
163
+ let field = "";
164
+ let inQuotes = false;
165
+ for (let i = 0; i < csv.length; i++) {
166
+ const char = csv[i];
167
+ if (inQuotes) {
168
+ if (char === '"') {
169
+ if (i + 1 < csv.length && csv[i + 1] === '"') {
170
+ field += '"';
171
+ i++;
172
+ } else {
173
+ inQuotes = false;
174
+ }
175
+ } else {
176
+ field += char;
177
+ }
178
+ } else {
179
+ if (char === '"') {
180
+ inQuotes = true;
181
+ } else if (char === delimiter) {
182
+ current.push(field);
183
+ field = "";
184
+ } else if (char === "\n") {
185
+ current.push(field);
186
+ if (!skipEmpty || current.some((c) => c.trim() !== "")) {
187
+ rows.push(current);
188
+ }
189
+ current = [];
190
+ field = "";
191
+ } else {
192
+ field += char;
193
+ }
194
+ }
195
+ }
196
+ current.push(field);
197
+ if (!skipEmpty || current.some((c) => c.trim() !== "")) {
198
+ rows.push(current);
199
+ }
200
+ return rows;
201
+ }
202
+ var BOOLEAN_VALUES = /* @__PURE__ */ new Set([
203
+ "true",
204
+ "false",
205
+ "yes",
206
+ "no",
207
+ "vrai",
208
+ "faux",
209
+ "oui",
210
+ "non"
211
+ ]);
212
+ var DATE_PATTERNS = [
213
+ /^\d{4}-\d{2}-\d{2}$/,
214
+ // 2024-01-15
215
+ /^\d{2}\/\d{2}\/\d{4}$/,
216
+ // 01/15/2024
217
+ /^\d{2}-\d{2}-\d{4}$/,
218
+ // 01-15-2024
219
+ /^\d{4}\/\d{2}\/\d{2}$/,
220
+ // 2024/01/15
221
+ /^\d{4}-\d{2}-\d{2}T/,
222
+ // ISO 8601
223
+ /^\d{2}\.\d{2}\.\d{4}$/,
224
+ // 15.01.2024
225
+ /^\w{3,9}\s\d{1,2},?\s\d{4}$/
226
+ // Jan 15, 2024 / January 15, 2024
227
+ ];
228
+ function inferColumnType(rows, colIndex) {
229
+ const sampleSize = Math.min(rows.length, 100);
230
+ let numbers = 0;
231
+ let dates = 0;
232
+ let booleans = 0;
233
+ let total = 0;
234
+ for (let i = 0; i < sampleSize; i++) {
235
+ const value = rows[i]?.[colIndex]?.trim() ?? "";
236
+ if (value === "") continue;
237
+ total++;
238
+ if (BOOLEAN_VALUES.has(value.toLowerCase())) {
239
+ booleans++;
240
+ }
241
+ const cleaned = value.replace(/[\s,]/g, "");
242
+ if (cleaned !== "" && !isNaN(Number(cleaned))) {
243
+ numbers++;
244
+ }
245
+ if (DATE_PATTERNS.some((p) => p.test(value))) {
246
+ dates++;
247
+ }
248
+ }
249
+ if (total === 0) return "string";
250
+ const threshold = 0.8;
251
+ if (booleans / total >= threshold && numbers / total < 0.5) return "boolean";
252
+ if (dates / total >= threshold) return "date";
253
+ if (numbers / total >= threshold) return "number";
254
+ return "string";
255
+ }
256
+
65
257
  // src/types.ts
66
258
  var defaultDarkTheme = {
67
259
  colors: [
@@ -363,31 +555,40 @@ function ChartToolbar({
363
555
  onToggleTrendline,
364
556
  onExportCSV,
365
557
  onExportPNG,
366
- onRegenerate
558
+ onRegenerate,
559
+ className,
560
+ unstyled = false
367
561
  }) {
368
562
  const supportsBrush = chartType === "line" || chartType === "area" || chartType === "bar";
369
563
  const supportsTrendline = chartType === "bar" || chartType === "line" || chartType === "area";
370
- return /* @__PURE__ */ jsxs("div", { className: "mb-4 flex flex-wrap items-center gap-2 rounded-xl border border-white/10 bg-white/5 p-3", children: [
564
+ const cls = (defaultCls) => unstyled ? "" : defaultCls;
565
+ const activeCls = (active, activeCls2, inactiveCls) => unstyled ? "" : `flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm transition-colors ${active ? activeCls2 : inactiveCls}`;
566
+ return /* @__PURE__ */ jsxs("div", { className: unstyled ? className ?? "" : `mb-4 flex flex-wrap items-center gap-2 rounded-xl border border-white/10 bg-white/5 p-3 ${className ?? ""}`.trim(), children: [
371
567
  /* @__PURE__ */ jsxs(
372
568
  "button",
373
569
  {
374
570
  onClick: onToggleSort,
375
- className: `flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm transition-colors ${sortOrder !== "none" ? "border border-violet-500/30 bg-violet-500/20 text-violet-400" : "bg-white/5 text-gray-400 hover:bg-white/10 hover:text-white"}`,
571
+ className: activeCls(
572
+ sortOrder !== "none",
573
+ "border border-violet-500/30 bg-violet-500/20 text-violet-400",
574
+ "bg-white/5 text-gray-400 hover:bg-white/10 hover:text-white"
575
+ ),
376
576
  title: "Sort by value",
377
577
  children: [
378
- sortOrder === "asc" ? /* @__PURE__ */ jsx2(SortAsc, { className: "h-4 w-4" }) : sortOrder === "desc" ? /* @__PURE__ */ jsx2(SortDesc, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx2(SortDesc, { className: "h-4 w-4 opacity-50" }),
578
+ sortOrder === "asc" ? /* @__PURE__ */ jsx2(SortAsc, { className: "h-4 w-4" }) : sortOrder === "desc" ? /* @__PURE__ */ jsx2(SortDesc, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx2(SortDesc, { className: cls("h-4 w-4 opacity-50") }),
379
579
  "Sort"
380
580
  ]
381
581
  }
382
582
  ),
383
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
384
- /* @__PURE__ */ jsx2(Filter, { className: "h-4 w-4 text-gray-400" }),
583
+ /* @__PURE__ */ jsxs("div", { className: cls("flex items-center gap-2"), children: [
584
+ /* @__PURE__ */ jsx2(Filter, { className: cls("h-4 w-4 text-gray-400"), "aria-hidden": "true" }),
385
585
  /* @__PURE__ */ jsxs(
386
586
  "select",
387
587
  {
388
588
  value: limitResults,
389
589
  onChange: (e) => onLimitChange(Number(e.target.value)),
390
- className: "rounded-lg border border-white/10 bg-white/5 px-2 py-1.5 text-sm text-gray-300 focus:border-violet-500/50 focus:outline-none",
590
+ "aria-label": "Limit number of results",
591
+ className: cls("rounded-lg border border-white/10 bg-white/5 px-2 py-1.5 text-sm text-gray-300 focus:border-violet-500/50 focus:outline-none"),
391
592
  children: [
392
593
  /* @__PURE__ */ jsx2("option", { value: 10, children: "Top 10" }),
393
594
  /* @__PURE__ */ jsx2("option", { value: 20, children: "Top 20" }),
@@ -402,7 +603,11 @@ function ChartToolbar({
402
603
  "button",
403
604
  {
404
605
  onClick: onToggleBrush,
405
- className: `flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm transition-colors ${showBrush ? "border border-cyan-500/30 bg-cyan-500/20 text-cyan-400" : "bg-white/5 text-gray-400 hover:bg-white/10 hover:text-white"}`,
606
+ className: activeCls(
607
+ showBrush,
608
+ "border border-cyan-500/30 bg-cyan-500/20 text-cyan-400",
609
+ "bg-white/5 text-gray-400 hover:bg-white/10 hover:text-white"
610
+ ),
406
611
  title: "Enable zoom/brush",
407
612
  children: [
408
613
  /* @__PURE__ */ jsx2(RotateCcw, { className: "h-4 w-4" }),
@@ -414,7 +619,11 @@ function ChartToolbar({
414
619
  "button",
415
620
  {
416
621
  onClick: onToggleTrendline,
417
- className: `flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm transition-colors ${showTrendline ? "border border-emerald-500/30 bg-emerald-500/20 text-emerald-400" : "bg-white/5 text-gray-400 hover:bg-white/10 hover:text-white"}`,
622
+ className: activeCls(
623
+ showTrendline,
624
+ "border border-emerald-500/30 bg-emerald-500/20 text-emerald-400",
625
+ "bg-white/5 text-gray-400 hover:bg-white/10 hover:text-white"
626
+ ),
418
627
  title: "Show average line",
419
628
  children: [
420
629
  /* @__PURE__ */ jsx2(TrendingUp, { className: "h-4 w-4" }),
@@ -422,12 +631,12 @@ function ChartToolbar({
422
631
  ]
423
632
  }
424
633
  ),
425
- /* @__PURE__ */ jsx2("div", { className: "flex-1" }),
634
+ /* @__PURE__ */ jsx2("div", { className: cls("flex-1") }),
426
635
  /* @__PURE__ */ jsxs(
427
636
  "button",
428
637
  {
429
638
  onClick: onExportPNG,
430
- className: "flex items-center gap-1.5 rounded-lg bg-white/5 px-3 py-1.5 text-sm text-gray-400 transition-colors hover:bg-white/10 hover:text-white",
639
+ className: cls("flex items-center gap-1.5 rounded-lg bg-white/5 px-3 py-1.5 text-sm text-gray-400 transition-colors hover:bg-white/10 hover:text-white"),
431
640
  title: "Export chart as PNG image",
432
641
  children: [
433
642
  /* @__PURE__ */ jsx2(Image2, { className: "h-4 w-4" }),
@@ -439,7 +648,7 @@ function ChartToolbar({
439
648
  "button",
440
649
  {
441
650
  onClick: onExportCSV,
442
- className: "flex items-center gap-1.5 rounded-lg bg-white/5 px-3 py-1.5 text-sm text-gray-400 transition-colors hover:bg-white/10 hover:text-white",
651
+ className: cls("flex items-center gap-1.5 rounded-lg bg-white/5 px-3 py-1.5 text-sm text-gray-400 transition-colors hover:bg-white/10 hover:text-white"),
443
652
  title: "Export chart data as CSV",
444
653
  children: [
445
654
  /* @__PURE__ */ jsx2(Download, { className: "h-4 w-4" }),
@@ -452,7 +661,7 @@ function ChartToolbar({
452
661
  {
453
662
  onClick: onRegenerate,
454
663
  disabled: isRegenerating,
455
- className: "flex items-center gap-1.5 rounded-lg bg-violet-500/20 px-3 py-1.5 text-sm text-violet-400 transition-colors hover:bg-violet-500/30 disabled:opacity-50",
664
+ className: cls("flex items-center gap-1.5 rounded-lg bg-violet-500/20 px-3 py-1.5 text-sm text-violet-400 transition-colors hover:bg-violet-500/30 disabled:opacity-50"),
456
665
  children: [
457
666
  /* @__PURE__ */ jsx2(
458
667
  RefreshCw,
@@ -469,7 +678,7 @@ function ChartToolbar({
469
678
 
470
679
  // src/SingleChart.tsx
471
680
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
472
- function SingleChart({ data, chart, onRegenerate }) {
681
+ function SingleChart({ data, chart, onRegenerate, className, unstyled = false }) {
473
682
  const theme = useChartTheme();
474
683
  const chartContainerRef = useRef(null);
475
684
  const [isRegenerating, setIsRegenerating] = useState(false);
@@ -851,9 +1060,9 @@ ${rows}`;
851
1060
  }
852
1061
  };
853
1062
  if (processedData.length === 0) {
854
- return /* @__PURE__ */ jsx3("div", { className: "flex h-[300px] flex-col items-center justify-center gap-4 text-gray-400", children: /* @__PURE__ */ jsxs2("div", { className: "text-center", children: [
855
- /* @__PURE__ */ jsx3("p", { className: "mb-1 font-medium text-red-400", children: "Unable to generate this chart" }),
856
- /* @__PURE__ */ jsxs2("p", { className: "mb-4 text-sm text-gray-500", children: [
1063
+ return /* @__PURE__ */ jsx3("div", { className: unstyled ? className ?? "" : `flex h-[300px] flex-col items-center justify-center gap-4 text-gray-400 ${className ?? ""}`.trim(), children: /* @__PURE__ */ jsxs2("div", { className: unstyled ? "" : "text-center", children: [
1064
+ /* @__PURE__ */ jsx3("p", { className: unstyled ? "" : "mb-1 font-medium text-red-400", children: "Unable to generate this chart" }),
1065
+ /* @__PURE__ */ jsxs2("p", { className: unstyled ? "" : "mb-4 text-sm text-gray-500", children: [
857
1066
  "Columns: ",
858
1067
  chart.xAxis,
859
1068
  ", ",
@@ -864,7 +1073,7 @@ ${rows}`;
864
1073
  {
865
1074
  onClick: handleRegenerate,
866
1075
  disabled: isRegenerating,
867
- className: "flex items-center gap-2 rounded-lg bg-violet-600 px-4 py-2 text-sm text-white transition-colors hover:bg-violet-500 disabled:opacity-50",
1076
+ className: unstyled ? "" : "flex items-center gap-2 rounded-lg bg-violet-600 px-4 py-2 text-sm text-white transition-colors hover:bg-violet-500 disabled:opacity-50",
868
1077
  children: [
869
1078
  /* @__PURE__ */ jsx3(
870
1079
  RefreshCw2,
@@ -878,7 +1087,7 @@ ${rows}`;
878
1087
  )
879
1088
  ] }) });
880
1089
  }
881
- return /* @__PURE__ */ jsxs2("div", { children: [
1090
+ return /* @__PURE__ */ jsxs2("div", { className, children: [
882
1091
  /* @__PURE__ */ jsx3(
883
1092
  ChartToolbar,
884
1093
  {
@@ -895,10 +1104,11 @@ ${rows}`;
895
1104
  onToggleTrendline: () => setShowTrendline(!showTrendline),
896
1105
  onExportCSV: handleExportCSV,
897
1106
  onExportPNG: handleExportPNG,
898
- onRegenerate: handleRegenerate
1107
+ onRegenerate: handleRegenerate,
1108
+ unstyled
899
1109
  }
900
1110
  ),
901
- /* @__PURE__ */ jsx3("div", { className: "h-[450px] w-full", ref: chartContainerRef, children: /* @__PURE__ */ jsx3(
1111
+ /* @__PURE__ */ jsx3("div", { className: unstyled ? "" : "h-[450px] w-full", style: unstyled ? { height: 450, width: "100%" } : void 0, ref: chartContainerRef, children: /* @__PURE__ */ jsx3(
902
1112
  ResponsiveContainer,
903
1113
  {
904
1114
  width: "100%",
@@ -915,25 +1125,25 @@ ${rows}`;
915
1125
  },
916
1126
  `chart-${chart.id}-${showBrush ? "brush" : "no-brush"}`
917
1127
  ) }),
918
- /* @__PURE__ */ jsxs2("div", { className: "mt-4 flex flex-wrap gap-2", children: [
919
- /* @__PURE__ */ jsxs2("span", { className: "rounded bg-white/10 px-2 py-0.5 text-xs text-gray-400", children: [
1128
+ /* @__PURE__ */ jsxs2("div", { className: unstyled ? "" : "mt-4 flex flex-wrap gap-2", children: [
1129
+ /* @__PURE__ */ jsxs2("span", { className: unstyled ? "" : "rounded bg-white/10 px-2 py-0.5 text-xs text-gray-400", children: [
920
1130
  "X: ",
921
1131
  chart.xAxis
922
1132
  ] }),
923
- /* @__PURE__ */ jsxs2("span", { className: "rounded bg-white/10 px-2 py-0.5 text-xs text-gray-400", children: [
1133
+ /* @__PURE__ */ jsxs2("span", { className: unstyled ? "" : "rounded bg-white/10 px-2 py-0.5 text-xs text-gray-400", children: [
924
1134
  "Y: ",
925
1135
  chart.yAxis
926
1136
  ] }),
927
- chart.groupBy && /* @__PURE__ */ jsxs2("span", { className: "rounded bg-cyan-500/20 px-2 py-0.5 text-xs text-cyan-300", children: [
1137
+ chart.groupBy && /* @__PURE__ */ jsxs2("span", { className: unstyled ? "" : "rounded bg-cyan-500/20 px-2 py-0.5 text-xs text-cyan-300", children: [
928
1138
  "Group: ",
929
1139
  chart.groupBy
930
1140
  ] }),
931
- chart.aggregation && chart.aggregation !== "none" && /* @__PURE__ */ jsx3("span", { className: "rounded bg-violet-500/20 px-2 py-0.5 text-xs text-violet-300", children: chart.aggregation }),
932
- /* @__PURE__ */ jsxs2("span", { className: "rounded bg-white/10 px-2 py-0.5 text-xs text-gray-400", children: [
1141
+ chart.aggregation && chart.aggregation !== "none" && /* @__PURE__ */ jsx3("span", { className: unstyled ? "" : "rounded bg-violet-500/20 px-2 py-0.5 text-xs text-violet-300", children: chart.aggregation }),
1142
+ /* @__PURE__ */ jsxs2("span", { className: unstyled ? "" : "rounded bg-white/10 px-2 py-0.5 text-xs text-gray-400", children: [
933
1143
  processedData.length,
934
1144
  " items"
935
1145
  ] }),
936
- isMultiSeries && /* @__PURE__ */ jsxs2("span", { className: "rounded bg-emerald-500/20 px-2 py-0.5 text-xs text-emerald-300", children: [
1146
+ isMultiSeries && /* @__PURE__ */ jsxs2("span", { className: unstyled ? "" : "rounded bg-emerald-500/20 px-2 py-0.5 text-xs text-emerald-300", children: [
937
1147
  seriesKeys.length,
938
1148
  " series"
939
1149
  ] })
@@ -954,23 +1164,34 @@ function ChartDisplay({
954
1164
  charts,
955
1165
  onRegenerate,
956
1166
  cardWrapper: CardWrapper = DefaultCard,
957
- theme = defaultDarkTheme
1167
+ theme = defaultDarkTheme,
1168
+ className,
1169
+ chartClassName,
1170
+ titleClassName,
1171
+ descriptionClassName,
1172
+ unstyled = false
958
1173
  }) {
959
1174
  if (charts.length === 0) return null;
960
- return /* @__PURE__ */ jsx4(ChartThemeProvider, { theme, children: /* @__PURE__ */ jsx4("div", { className: "animate-fade-in space-y-6", children: charts.map((chart) => /* @__PURE__ */ jsx4(
1175
+ const containerCls = unstyled ? className ?? "" : `animate-fade-in space-y-6 ${className ?? ""}`.trim();
1176
+ const cardCls = unstyled ? chartClassName ?? "" : `overflow-hidden rounded-2xl border border-white/10 bg-slate-900/50 ${chartClassName ?? ""}`.trim();
1177
+ const titleCls = unstyled ? titleClassName ?? "" : `mb-2 bg-linear-to-r from-violet-400 to-fuchsia-400 bg-clip-text text-xl font-bold text-transparent ${titleClassName ?? ""}`.trim();
1178
+ const descCls = unstyled ? descriptionClassName ?? "" : `mb-4 text-gray-400 ${descriptionClassName ?? ""}`.trim();
1179
+ const innerCls = unstyled ? "" : "p-6";
1180
+ return /* @__PURE__ */ jsx4(ChartThemeProvider, { theme, children: /* @__PURE__ */ jsx4("div", { className: containerCls, children: charts.map((chart) => /* @__PURE__ */ jsx4(
961
1181
  CardWrapper,
962
1182
  {
963
1183
  title: chart.title,
964
- className: "overflow-hidden rounded-2xl border border-white/10 bg-slate-900/50",
965
- children: /* @__PURE__ */ jsxs3("div", { className: "p-6", children: [
966
- /* @__PURE__ */ jsx4("h3", { className: "mb-2 bg-linear-to-r from-violet-400 to-fuchsia-400 bg-clip-text text-xl font-bold text-transparent", children: chart.title }),
967
- /* @__PURE__ */ jsx4("p", { className: "mb-4 text-gray-400", children: chart.description }),
1184
+ className: cardCls,
1185
+ children: /* @__PURE__ */ jsxs3("div", { className: innerCls, children: [
1186
+ /* @__PURE__ */ jsx4("h3", { className: titleCls, children: chart.title }),
1187
+ /* @__PURE__ */ jsx4("p", { className: descCls, children: chart.description }),
968
1188
  /* @__PURE__ */ jsx4(
969
1189
  SingleChart,
970
1190
  {
971
1191
  data,
972
1192
  chart,
973
- onRegenerate
1193
+ onRegenerate,
1194
+ unstyled
974
1195
  }
975
1196
  )
976
1197
  ] })
@@ -1041,6 +1262,10 @@ var SingleChartResponseSchema = z.object({
1041
1262
  chart: ChartSuggestionSchema
1042
1263
  });
1043
1264
  function getAIErrorMessage(error) {
1265
+ if (error instanceof Error && error.name === "ZodError" && "issues" in error) {
1266
+ const issues = error.issues;
1267
+ return `Invalid data: ${issues.map((i) => i.message).join(", ")}`;
1268
+ }
1044
1269
  if (error instanceof Error) {
1045
1270
  const message = error.message.toLowerCase();
1046
1271
  if (message.includes("rate limit") || message.includes("429")) {
@@ -1073,8 +1298,8 @@ function summarizeTabularData(data) {
1073
1298
  if (col.type === "number") {
1074
1299
  const nums = values.map(Number).filter((n) => !isNaN(n));
1075
1300
  if (nums.length > 0) {
1076
- const min = Math.min(...nums);
1077
- const max = Math.max(...nums);
1301
+ const min = nums.reduce((a, b) => b < a ? b : a, nums[0]);
1302
+ const max = nums.reduce((a, b) => b > a ? b : a, nums[0]);
1078
1303
  const avg = nums.reduce((a, b) => a + b, 0) / nums.length;
1079
1304
  lines.push(
1080
1305
  `- ${col.name} (${col.type}): min=${min}, max=${max}, avg=${avg.toFixed(2)}, ${nums.length} values`
@@ -1202,10 +1427,10 @@ function mapChartResult(raw, id) {
1202
1427
  };
1203
1428
  }
1204
1429
  async function suggestCharts(options) {
1205
- const { data, language, temperature = 0.5 } = options;
1430
+ const { data, language, temperature = 0.5, signal } = options;
1206
1431
  const dataSummary = options.dataSummary ?? summarizeTabularData(data);
1207
- TabularDataSchema.parse(data);
1208
1432
  try {
1433
+ TabularDataSchema.parse(data);
1209
1434
  const model = await resolveModel(options.model);
1210
1435
  const { object } = await generateObject({
1211
1436
  model,
@@ -1214,7 +1439,8 @@ async function suggestCharts(options) {
1214
1439
  prompt: `Analyze this CSV data and suggest the best charts:
1215
1440
 
1216
1441
  ${dataSummary}`,
1217
- temperature
1442
+ temperature,
1443
+ ...signal && { abortSignal: signal }
1218
1444
  });
1219
1445
  return object.charts.map(
1220
1446
  (s, i) => mapChartResult(s, `chart-${i}-${Date.now()}`)
@@ -1224,10 +1450,10 @@ ${dataSummary}`,
1224
1450
  }
1225
1451
  }
1226
1452
  async function suggestCustomChart(options) {
1227
- const { data, prompt, language, temperature = 0.5 } = options;
1453
+ const { data, prompt, language, temperature = 0.5, signal } = options;
1228
1454
  const dataSummary = options.dataSummary ?? summarizeTabularData(data);
1229
- TabularDataSchema.parse(data);
1230
1455
  try {
1456
+ TabularDataSchema.parse(data);
1231
1457
  const model = await resolveModel(options.model);
1232
1458
  const { object } = await generateObject({
1233
1459
  model,
@@ -1237,7 +1463,8 @@ async function suggestCustomChart(options) {
1237
1463
  ${dataSummary}
1238
1464
 
1239
1465
  User request: ${prompt}`,
1240
- temperature
1466
+ temperature,
1467
+ ...signal && { abortSignal: signal }
1241
1468
  });
1242
1469
  return mapChartResult(object.chart, `chart-custom-${Date.now()}`);
1243
1470
  } catch (error) {
@@ -1250,7 +1477,8 @@ async function repairChart(options) {
1250
1477
  columns,
1251
1478
  errorContext,
1252
1479
  language,
1253
- temperature = 0.3
1480
+ temperature = 0.3,
1481
+ signal
1254
1482
  } = options;
1255
1483
  try {
1256
1484
  const model = await resolveModel(options.model);
@@ -1264,7 +1492,8 @@ ${JSON.stringify(failedChart, null, 2)}
1264
1492
  Error context: ${errorContext}
1265
1493
 
1266
1494
  Please fix the configuration to use valid columns and aggregation. Available columns: ${columns.join(", ")}`,
1267
- temperature
1495
+ temperature,
1496
+ ...signal && { abortSignal: signal }
1268
1497
  });
1269
1498
  return mapChartResult(object.chart, failedChart.id);
1270
1499
  } catch (error) {
@@ -1318,14 +1547,20 @@ Analyze ALL rows provided. Return empty array if no anomalies found.`;
1318
1547
  var questionSystemPrompt = (language) => `You are a data analysis expert.${language ? ` Respond in ${language}.` : ""}
1319
1548
 
1320
1549
  The user will ask questions about a CSV dataset. Respond clearly and precisely.`;
1550
+ function escapeCSVField(value) {
1551
+ if (value.includes(",") || value.includes('"') || value.includes("\n") || value.includes("\r")) {
1552
+ return `"${value.replace(/"/g, '""')}"`;
1553
+ }
1554
+ return value;
1555
+ }
1321
1556
  function buildSampleCSV(data, maxRows = 50) {
1322
- const header = data.headers.join(",");
1323
- const rows = data.rows.slice(0, maxRows).map((row) => row.join(",")).join("\n");
1557
+ const header = data.headers.map(escapeCSVField).join(",");
1558
+ const rows = data.rows.slice(0, maxRows).map((row) => row.map(escapeCSVField).join(",")).join("\n");
1324
1559
  return `${header}
1325
1560
  ${rows}`;
1326
1561
  }
1327
1562
  async function summarizeData(options) {
1328
- const { data, language, temperature = 0.5 } = options;
1563
+ const { data, language, temperature = 0.5, signal } = options;
1329
1564
  const dataSummary = options.dataSummary ?? summarizeTabularData(data);
1330
1565
  try {
1331
1566
  const model = await resolveModel(options.model);
@@ -1336,7 +1571,8 @@ async function summarizeData(options) {
1336
1571
  prompt: `Here is the data to analyze:
1337
1572
 
1338
1573
  ${dataSummary}`,
1339
- temperature
1574
+ temperature,
1575
+ ...signal && { abortSignal: signal }
1340
1576
  });
1341
1577
  return {
1342
1578
  summary: object.summary,
@@ -1348,7 +1584,7 @@ ${dataSummary}`,
1348
1584
  }
1349
1585
  }
1350
1586
  async function detectAnomalies(options) {
1351
- const { data, maxRows = 50, language, temperature = 0.3 } = options;
1587
+ const { data, maxRows = 50, language, temperature = 0.3, signal } = options;
1352
1588
  const dataSummary = options.dataSummary ?? summarizeTabularData(data);
1353
1589
  const sampleCSV = buildSampleCSV(data, maxRows);
1354
1590
  try {
@@ -1362,7 +1598,8 @@ ${dataSummary}
1362
1598
 
1363
1599
  Data to analyze (CSV format):
1364
1600
  ${sampleCSV}`,
1365
- temperature
1601
+ temperature,
1602
+ ...signal && { abortSignal: signal }
1366
1603
  });
1367
1604
  return object.anomalies;
1368
1605
  } catch (error) {
@@ -1370,7 +1607,7 @@ ${sampleCSV}`,
1370
1607
  }
1371
1608
  }
1372
1609
  async function askAboutData(options) {
1373
- const { data, question, history = [], language, temperature = 0.5 } = options;
1610
+ const { data, question, history = [], language, temperature = 0.5, signal } = options;
1374
1611
  const dataSummary = options.dataSummary ?? summarizeTabularData(data);
1375
1612
  try {
1376
1613
  const model = await resolveModel(options.model);
@@ -1388,7 +1625,8 @@ ${historyText}
1388
1625
  ${dataSummary}
1389
1626
 
1390
1627
  ${contextPrompt}User question: ${question}`,
1391
- temperature
1628
+ temperature,
1629
+ ...signal && { abortSignal: signal }
1392
1630
  });
1393
1631
  return text;
1394
1632
  } catch (error) {
@@ -1402,6 +1640,7 @@ async function streamAskAboutData(options) {
1402
1640
  history = [],
1403
1641
  language,
1404
1642
  temperature = 0.5,
1643
+ signal,
1405
1644
  onChunk,
1406
1645
  onComplete
1407
1646
  } = options;
@@ -1424,6 +1663,7 @@ ${dataSummary}
1424
1663
 
1425
1664
  ${contextPrompt}User question: ${question}`,
1426
1665
  temperature,
1666
+ ...signal && { abortSignal: signal },
1427
1667
  onError: ({ error }) => {
1428
1668
  streamError = error instanceof Error ? error : new Error(String(error));
1429
1669
  }
@@ -1447,17 +1687,58 @@ async function analyzeData(options) {
1447
1687
  const {
1448
1688
  data,
1449
1689
  language,
1690
+ signal,
1450
1691
  detectAnomalies: runAnomalies = true,
1451
1692
  suggestCharts: runCharts = true
1452
1693
  } = options;
1453
1694
  const dataSummary = options.dataSummary ?? summarizeTabularData(data);
1454
1695
  const [summary, anomalies, charts] = await Promise.all([
1455
- summarizeData({ model: options.model, data, dataSummary, language }),
1456
- runAnomalies ? detectAnomalies({ model: options.model, data, dataSummary, language }) : Promise.resolve([]),
1457
- runCharts ? suggestCharts({ model: options.model, data, dataSummary, language }) : Promise.resolve([])
1696
+ summarizeData({ model: options.model, data, dataSummary, language, signal }),
1697
+ runAnomalies ? detectAnomalies({ model: options.model, data, dataSummary, language, signal }) : Promise.resolve([]),
1698
+ runCharts ? suggestCharts({ model: options.model, data, dataSummary, language, signal }) : Promise.resolve([])
1458
1699
  ]);
1459
1700
  return { summary, anomalies, charts };
1460
1701
  }
1702
+ var questionsSystemPrompt = (language) => `You are a data analysis expert.${language ? ` Respond in ${language}.` : ""}
1703
+
1704
+ Given a dataset summary, suggest insightful questions that a user could ask to better understand their data. Focus on:
1705
+ - Trends and patterns
1706
+ - Comparisons and rankings
1707
+ - Correlations between columns
1708
+ - Outliers and anomalies
1709
+ - Actionable business insights
1710
+
1711
+ Each question should be specific to the data columns available.`;
1712
+ var QuestionsResponseSchema = z2.object({
1713
+ questions: z2.array(
1714
+ z2.object({
1715
+ question: z2.string().describe("The question to ask about the data"),
1716
+ category: z2.enum(["trend", "comparison", "correlation", "anomaly", "insight"]).describe("Category of the question")
1717
+ })
1718
+ ).describe("5-8 suggested questions")
1719
+ });
1720
+ async function suggestQuestions(options) {
1721
+ const { data, language, count = 6, temperature = 0.7, signal } = options;
1722
+ const dataSummary = options.dataSummary ?? summarizeTabularData(data);
1723
+ try {
1724
+ const model = await resolveModel(options.model);
1725
+ const { object } = await generateObject2({
1726
+ model,
1727
+ schema: QuestionsResponseSchema,
1728
+ system: questionsSystemPrompt(language),
1729
+ prompt: `Here is the dataset to analyze:
1730
+
1731
+ ${dataSummary}
1732
+
1733
+ Suggest ${count} insightful questions about this data.`,
1734
+ temperature,
1735
+ ...signal && { abortSignal: signal }
1736
+ });
1737
+ return object.questions.slice(0, count);
1738
+ } catch (error) {
1739
+ throw new Error(getAIErrorMessage(error));
1740
+ }
1741
+ }
1461
1742
  export {
1462
1743
  AIConfigSchema,
1463
1744
  COLORS,
@@ -1474,6 +1755,7 @@ export {
1474
1755
  defaultLightTheme,
1475
1756
  detectAnomalies,
1476
1757
  getAIErrorMessage,
1758
+ parseCSV,
1477
1759
  processChartData,
1478
1760
  processChartDataMultiSeries,
1479
1761
  repairChart,
@@ -1481,6 +1763,7 @@ export {
1481
1763
  streamAskAboutData,
1482
1764
  suggestCharts,
1483
1765
  suggestCustomChart,
1766
+ suggestQuestions,
1484
1767
  summarizeData,
1485
1768
  summarizeTabularData,
1486
1769
  useChartTheme