@smallwebco/tinypivot-react 1.0.55 → 1.0.58
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 +7 -6
- package/dist/index.cjs +116 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -2
- package/dist/index.d.ts +14 -2
- package/dist/index.js +117 -12
- package/dist/index.js.map +1 -1
- package/package.json +11 -3
package/README.md
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
# @smallwebco/tinypivot-react
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Embed an AI Data Analyst in your React app.** Ask questions in plain English, get instant SQL-powered insights. AI-enabled data grid with pivot tables and charts — all under 40KB gzipped.
|
|
4
4
|
|
|
5
5
|
**[Live Demo](https://tiny-pivot.com)** · **[Buy License](https://tiny-pivot.com/#pricing)**
|
|
6
6
|
|
|
7
7
|
## Why TinyPivot?
|
|
8
8
|
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
9
|
+
- **Embedded AI Data Analyst**: Let users ask questions in plain English — AI generates SQL and returns results instantly
|
|
10
|
+
- **Bring Your Own Key (BYOK)**: Use your own OpenAI, Anthropic, or any LLM API key — full control over costs and privacy
|
|
11
|
+
- **Lightweight**: Under 40KB gzipped vs 500KB+ for AG Grid (10x smaller)
|
|
12
|
+
- **Batteries Included**: Pivot tables, 6 chart types, Excel-like features out of the box
|
|
12
13
|
- **One-Time License**: No subscriptions — pay once, use forever
|
|
13
14
|
|
|
14
15
|
## Installation
|
|
@@ -88,9 +89,9 @@ export default function App() {
|
|
|
88
89
|
| `onExport` | `(payload) => void` | CSV exported |
|
|
89
90
|
| `onCopy` | `(payload) => void` | Cells copied |
|
|
90
91
|
|
|
91
|
-
## AI Data Analyst (Pro)
|
|
92
|
+
## Embedded AI Data Analyst (Pro)
|
|
92
93
|
|
|
93
|
-
|
|
94
|
+
Embed an AI-powered data analyst that lets users explore data using natural language. Ask questions like "What's the return rate by category?" and get instant results — no SQL knowledge required for end users.
|
|
94
95
|
|
|
95
96
|
```tsx
|
|
96
97
|
import { DataGrid } from '@smallwebco/tinypivot-react'
|
package/dist/index.cjs
CHANGED
|
@@ -567,6 +567,82 @@ What would you like to know about this data?`
|
|
|
567
567
|
setIsLoading(false);
|
|
568
568
|
}
|
|
569
569
|
}, [isLoading, schemas, effectiveDataSources, callAIEndpoint, executeQuery, handleDemoResponse, onConversationUpdate, onError]);
|
|
570
|
+
const loadFullData = (0, import_react.useCallback)(async () => {
|
|
571
|
+
const dataSourceId = conversation.dataSourceId;
|
|
572
|
+
if (!dataSourceId) {
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
const dataSource = effectiveDataSources.find((ds) => ds.id === dataSourceId);
|
|
576
|
+
if (!dataSource) {
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
const currentConfig = configRef.current;
|
|
580
|
+
if (currentConfig.dataSourceLoader) {
|
|
581
|
+
try {
|
|
582
|
+
const { data } = await currentConfig.dataSourceLoader(dataSourceId);
|
|
583
|
+
if (data && data.length > 0) {
|
|
584
|
+
return data;
|
|
585
|
+
}
|
|
586
|
+
} catch (err) {
|
|
587
|
+
console.warn("Failed to load full data:", err);
|
|
588
|
+
onError?.({
|
|
589
|
+
message: err instanceof Error ? err.message : "Failed to load full data",
|
|
590
|
+
type: "network"
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
if (currentConfig.queryExecutor) {
|
|
596
|
+
try {
|
|
597
|
+
const result = await currentConfig.queryExecutor(
|
|
598
|
+
`SELECT * FROM ${dataSource.table}`,
|
|
599
|
+
dataSource.table
|
|
600
|
+
);
|
|
601
|
+
if (result.data && result.data.length > 0) {
|
|
602
|
+
return result.data;
|
|
603
|
+
}
|
|
604
|
+
} catch (err) {
|
|
605
|
+
console.warn("Failed to load full data via query:", err);
|
|
606
|
+
onError?.({
|
|
607
|
+
message: err instanceof Error ? err.message : "Failed to load full data",
|
|
608
|
+
type: "network"
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
if (currentConfig.endpoint) {
|
|
614
|
+
try {
|
|
615
|
+
const response = await fetch(currentConfig.endpoint, {
|
|
616
|
+
method: "POST",
|
|
617
|
+
headers: { "Content-Type": "application/json" },
|
|
618
|
+
body: JSON.stringify({
|
|
619
|
+
action: "query",
|
|
620
|
+
sql: `SELECT * FROM ${dataSource.table}`,
|
|
621
|
+
table: dataSource.table
|
|
622
|
+
})
|
|
623
|
+
});
|
|
624
|
+
if (!response.ok) {
|
|
625
|
+
throw new Error(`Failed to load data: ${response.statusText}`);
|
|
626
|
+
}
|
|
627
|
+
const data = await response.json();
|
|
628
|
+
if (data.data && data.data.length > 0) {
|
|
629
|
+
return data.data;
|
|
630
|
+
}
|
|
631
|
+
} catch (err) {
|
|
632
|
+
console.warn("Failed to load full data from endpoint:", err);
|
|
633
|
+
onError?.({
|
|
634
|
+
message: err instanceof Error ? err.message : "Failed to load full data",
|
|
635
|
+
type: "network"
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
if (currentConfig.demoMode) {
|
|
641
|
+
const initialData = (0, import_tinypivot_core.getInitialDemoData)(dataSourceId);
|
|
642
|
+
return initialData || null;
|
|
643
|
+
}
|
|
644
|
+
return null;
|
|
645
|
+
}, [conversation.dataSourceId, effectiveDataSources, onError]);
|
|
570
646
|
const clearConversation = (0, import_react.useCallback)(() => {
|
|
571
647
|
const newConv = (0, import_tinypivot_core.createConversation)(configRef.current.sessionId);
|
|
572
648
|
setConversation(newConv);
|
|
@@ -602,13 +678,15 @@ What would you like to know about this data?`
|
|
|
602
678
|
exportConversation,
|
|
603
679
|
importConversation,
|
|
604
680
|
/** Refresh table list from endpoint */
|
|
605
|
-
fetchTables
|
|
681
|
+
fetchTables,
|
|
682
|
+
/** Load full data for the currently selected data source */
|
|
683
|
+
loadFullData
|
|
606
684
|
};
|
|
607
685
|
}
|
|
608
686
|
|
|
609
687
|
// src/components/AIAnalyst.tsx
|
|
610
688
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
611
|
-
|
|
689
|
+
var AIAnalyst = (0, import_react2.forwardRef)(({
|
|
612
690
|
config,
|
|
613
691
|
theme = "light",
|
|
614
692
|
onDataLoaded,
|
|
@@ -616,7 +694,7 @@ function AIAnalyst({
|
|
|
616
694
|
onQueryExecuted,
|
|
617
695
|
onError,
|
|
618
696
|
onViewResults
|
|
619
|
-
}) {
|
|
697
|
+
}, ref) => {
|
|
620
698
|
const {
|
|
621
699
|
messages,
|
|
622
700
|
hasMessages,
|
|
@@ -629,7 +707,8 @@ function AIAnalyst({
|
|
|
629
707
|
dataSources,
|
|
630
708
|
selectDataSource,
|
|
631
709
|
sendMessage,
|
|
632
|
-
clearConversation
|
|
710
|
+
clearConversation,
|
|
711
|
+
loadFullData
|
|
633
712
|
} = useAIAnalyst({
|
|
634
713
|
config,
|
|
635
714
|
onDataLoaded,
|
|
@@ -637,6 +716,10 @@ function AIAnalyst({
|
|
|
637
716
|
onQueryExecuted,
|
|
638
717
|
onError
|
|
639
718
|
});
|
|
719
|
+
(0, import_react2.useImperativeHandle)(ref, () => ({
|
|
720
|
+
loadFullData,
|
|
721
|
+
selectedDataSource
|
|
722
|
+
}), [loadFullData, selectedDataSource]);
|
|
640
723
|
const [inputText, setInputText] = (0, import_react2.useState)("");
|
|
641
724
|
const [searchQuery, setSearchQuery] = (0, import_react2.useState)("");
|
|
642
725
|
const [selectedMessageId, setSelectedMessageId] = (0, import_react2.useState)(null);
|
|
@@ -1145,7 +1228,7 @@ function AIAnalyst({
|
|
|
1145
1228
|
)
|
|
1146
1229
|
] })
|
|
1147
1230
|
] }) });
|
|
1148
|
-
}
|
|
1231
|
+
});
|
|
1149
1232
|
|
|
1150
1233
|
// src/components/CalculatedFieldModal.tsx
|
|
1151
1234
|
var import_tinypivot_core3 = require("@smallwebco/tinypivot-core");
|
|
@@ -4639,7 +4722,9 @@ function DataGrid({
|
|
|
4639
4722
|
const [showCopyToast, setShowCopyToast] = (0, import_react13.useState)(false);
|
|
4640
4723
|
const [copyToastMessage, setCopyToastMessage] = (0, import_react13.useState)("");
|
|
4641
4724
|
const [viewMode, setViewMode] = (0, import_react13.useState)("grid");
|
|
4725
|
+
const aiAnalystRef = (0, import_react13.useRef)(null);
|
|
4642
4726
|
const [aiLoadedData, setAiLoadedData] = (0, import_react13.useState)(null);
|
|
4727
|
+
const [isLoadingFullData, setIsLoadingFullData] = (0, import_react13.useState)(false);
|
|
4643
4728
|
const displayData = (0, import_react13.useMemo)(() => aiLoadedData || data, [aiLoadedData, data]);
|
|
4644
4729
|
const [_chartConfig, setChartConfig] = (0, import_react13.useState)(null);
|
|
4645
4730
|
const handleChartConfigChange = (0, import_react13.useCallback)((config) => {
|
|
@@ -4997,9 +5082,27 @@ function DataGrid({
|
|
|
4997
5082
|
[isSelecting]
|
|
4998
5083
|
);
|
|
4999
5084
|
const isShowingAIData = aiLoadedData !== null;
|
|
5000
|
-
const resetToFullData = (0, import_react13.useCallback)(() => {
|
|
5001
|
-
|
|
5002
|
-
|
|
5085
|
+
const resetToFullData = (0, import_react13.useCallback)(async () => {
|
|
5086
|
+
if (aiAnalystRef.current?.selectedDataSource) {
|
|
5087
|
+
setIsLoadingFullData(true);
|
|
5088
|
+
try {
|
|
5089
|
+
const fullData = await aiAnalystRef.current.loadFullData();
|
|
5090
|
+
if (fullData && fullData.length > 0) {
|
|
5091
|
+
setAiLoadedData(fullData);
|
|
5092
|
+
} else {
|
|
5093
|
+
setAiLoadedData(null);
|
|
5094
|
+
}
|
|
5095
|
+
} catch (err) {
|
|
5096
|
+
console.warn("Failed to load full data:", err);
|
|
5097
|
+
setAiLoadedData(null);
|
|
5098
|
+
} finally {
|
|
5099
|
+
setIsLoadingFullData(false);
|
|
5100
|
+
}
|
|
5101
|
+
} else {
|
|
5102
|
+
setAiLoadedData(null);
|
|
5103
|
+
}
|
|
5104
|
+
clearAllFilters();
|
|
5105
|
+
}, [clearAllFilters]);
|
|
5003
5106
|
const handleAIDataLoaded = (0, import_react13.useCallback)(
|
|
5004
5107
|
(payload) => {
|
|
5005
5108
|
setAiLoadedData(payload.data);
|
|
@@ -5417,11 +5520,12 @@ function DataGrid({
|
|
|
5417
5520
|
viewMode === "grid" && isShowingAIData && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
|
|
5418
5521
|
"button",
|
|
5419
5522
|
{
|
|
5420
|
-
className:
|
|
5523
|
+
className: `vpg-reset-data-btn${isLoadingFullData ? " vpg-loading-btn" : ""}`,
|
|
5524
|
+
disabled: isLoadingFullData,
|
|
5421
5525
|
title: "Reset to full dataset",
|
|
5422
5526
|
onClick: resetToFullData,
|
|
5423
5527
|
children: [
|
|
5424
|
-
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("svg", { className:
|
|
5528
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("svg", { className: `vpg-icon${isLoadingFullData ? " vpg-spin" : ""}`, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
5425
5529
|
"path",
|
|
5426
5530
|
{
|
|
5427
5531
|
strokeLinecap: "round",
|
|
@@ -5430,7 +5534,7 @@ function DataGrid({
|
|
|
5430
5534
|
d: "M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
|
5431
5535
|
}
|
|
5432
5536
|
) }),
|
|
5433
|
-
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { children: "Full Data" })
|
|
5537
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { children: isLoadingFullData ? "Loading..." : "Full Data" })
|
|
5434
5538
|
]
|
|
5435
5539
|
}
|
|
5436
5540
|
),
|
|
@@ -5498,6 +5602,7 @@ function DataGrid({
|
|
|
5498
5602
|
showAIAnalyst && aiAnalyst && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "vpg-ai-view", style: { display: viewMode === "ai" ? void 0 : "none" }, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
5499
5603
|
AIAnalyst,
|
|
5500
5604
|
{
|
|
5605
|
+
ref: aiAnalystRef,
|
|
5501
5606
|
config: aiAnalyst,
|
|
5502
5607
|
theme: currentTheme,
|
|
5503
5608
|
onDataLoaded: handleAIDataLoaded,
|