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/README.md +277 -41
- package/dist/index.d.ts +117 -5
- package/dist/index.js +341 -58
- package/dist/index.js.map +1 -1
- package/package.json +28 -2
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.
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
965
|
-
children: /* @__PURE__ */ jsxs3("div", { className:
|
|
966
|
-
/* @__PURE__ */ jsx4("h3", { className:
|
|
967
|
-
/* @__PURE__ */ jsx4("p", { className:
|
|
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 =
|
|
1077
|
-
const max =
|
|
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
|