@finos/legend-lego 2.0.193 → 2.0.194
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/lib/index.css +2 -2
- package/lib/index.css.map +1 -1
- package/lib/legend-ai/LegendAITypes.d.ts +33 -0
- package/lib/legend-ai/LegendAITypes.d.ts.map +1 -1
- package/lib/legend-ai/LegendAITypes.js +39 -1
- package/lib/legend-ai/LegendAITypes.js.map +1 -1
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts +96 -1
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts.map +1 -1
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.js +56 -0
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.js.map +1 -1
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.d.ts.map +1 -1
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.js +6 -0
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.js.map +1 -1
- package/lib/legend-ai/components/LegendAIAnalysisPanel.d.ts +24 -0
- package/lib/legend-ai/components/LegendAIAnalysisPanel.d.ts.map +1 -0
- package/lib/legend-ai/components/LegendAIAnalysisPanel.js +35 -0
- package/lib/legend-ai/components/LegendAIAnalysisPanel.js.map +1 -0
- package/lib/legend-ai/components/LegendAIAnalysisUtils.d.ts +23 -0
- package/lib/legend-ai/components/LegendAIAnalysisUtils.d.ts.map +1 -0
- package/lib/legend-ai/components/LegendAIAnalysisUtils.js +168 -0
- package/lib/legend-ai/components/LegendAIAnalysisUtils.js.map +1 -0
- package/lib/legend-ai/components/LegendAICharts.d.ts +25 -0
- package/lib/legend-ai/components/LegendAICharts.d.ts.map +1 -0
- package/lib/legend-ai/components/LegendAICharts.js +70 -0
- package/lib/legend-ai/components/LegendAICharts.js.map +1 -0
- package/lib/legend-ai/components/LegendAIChat.d.ts +2 -1
- package/lib/legend-ai/components/LegendAIChat.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAIChat.js +14 -10
- package/lib/legend-ai/components/LegendAIChat.js.map +1 -1
- package/lib/legend-ai/index.d.ts +5 -2
- package/lib/legend-ai/index.d.ts.map +1 -1
- package/lib/legend-ai/index.js +5 -2
- package/lib/legend-ai/index.js.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatState.d.ts +12 -5
- package/lib/legend-ai/stores/LegendAIChatState.d.ts.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatState.js +604 -69
- package/lib/legend-ai/stores/LegendAIChatState.js.map +1 -1
- package/package.json +5 -5
- package/src/legend-ai/LegendAITypes.ts +51 -1
- package/src/legend-ai/LegendAI_LegendApplicationPlugin_Extension.ts +169 -0
- package/src/legend-ai/__test-utils__/LegendAITestUtils.ts +9 -0
- package/src/legend-ai/components/LegendAIAnalysisPanel.tsx +102 -0
- package/src/legend-ai/components/LegendAIAnalysisUtils.ts +226 -0
- package/src/legend-ai/components/LegendAICharts.tsx +166 -0
- package/src/legend-ai/components/LegendAIChat.tsx +74 -26
- package/src/legend-ai/index.ts +18 -0
- package/src/legend-ai/stores/LegendAIChatState.ts +1039 -128
- package/tsconfig.json +3 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026-present, Goldman Sachs
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
type LegendAIKeyMetric,
|
|
19
|
+
type LegendAIChartDataPoint,
|
|
20
|
+
LegendAIChartType,
|
|
21
|
+
} from '../LegendAI_LegendApplicationPlugin_Extension.js';
|
|
22
|
+
import type { LegendAIGridData } from '../LegendAITypes.js';
|
|
23
|
+
import { isNonNullable, isNumber, isString } from '@finos/legend-shared';
|
|
24
|
+
|
|
25
|
+
const CHART_PALETTE_COUNT = 10;
|
|
26
|
+
const MAX_CHART_ITEMS = 10;
|
|
27
|
+
const TOP_N_ITEMS = 5;
|
|
28
|
+
const MAX_PROFILE_SAMPLE = 1000;
|
|
29
|
+
const MAX_KEY_METRICS = 4;
|
|
30
|
+
const MAX_METRIC_COLUMNS = 5;
|
|
31
|
+
const MAX_BAR_CHART_ROWS = 20;
|
|
32
|
+
const MAX_PIE_CHART_ROWS = 6;
|
|
33
|
+
|
|
34
|
+
interface ColumnProfile {
|
|
35
|
+
name: string;
|
|
36
|
+
isNumeric: boolean;
|
|
37
|
+
isString: boolean;
|
|
38
|
+
uniqueCount: number;
|
|
39
|
+
values: unknown[];
|
|
40
|
+
numericValues: number[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function profileColumns(gridData: LegendAIGridData): ColumnProfile[] {
|
|
44
|
+
const rows =
|
|
45
|
+
gridData.rowData.length > MAX_PROFILE_SAMPLE
|
|
46
|
+
? gridData.rowData.slice(0, MAX_PROFILE_SAMPLE)
|
|
47
|
+
: gridData.rowData;
|
|
48
|
+
|
|
49
|
+
return gridData.columnDefs.map((col) => {
|
|
50
|
+
const field = col.field ?? col.colId ?? '';
|
|
51
|
+
const values = rows.map((r) => r[field]).filter(isNonNullable);
|
|
52
|
+
const numericValues = values.filter(isNumber);
|
|
53
|
+
const unique = new Set(values.map(String));
|
|
54
|
+
return {
|
|
55
|
+
name: field,
|
|
56
|
+
isNumeric: numericValues.length === values.length && values.length > 0,
|
|
57
|
+
isString: values.length > 0 && values.every(isString),
|
|
58
|
+
uniqueCount: unique.size,
|
|
59
|
+
values,
|
|
60
|
+
numericValues,
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function formatNumber(n: number): string {
|
|
66
|
+
if (Number.isInteger(n) && Math.abs(n) < 1_000_000) {
|
|
67
|
+
return n.toLocaleString();
|
|
68
|
+
}
|
|
69
|
+
if (Math.abs(n) >= 1_000_000) {
|
|
70
|
+
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
71
|
+
}
|
|
72
|
+
if (Math.abs(n) >= 1_000) {
|
|
73
|
+
return `${(n / 1_000).toFixed(1)}K`;
|
|
74
|
+
}
|
|
75
|
+
return n.toFixed(2);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function computeNumericMetrics(
|
|
79
|
+
col: ColumnProfile,
|
|
80
|
+
metrics: LegendAIKeyMetric[],
|
|
81
|
+
): void {
|
|
82
|
+
let sum = 0;
|
|
83
|
+
let min = Number.POSITIVE_INFINITY;
|
|
84
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
85
|
+
for (const n of col.numericValues) {
|
|
86
|
+
sum += n;
|
|
87
|
+
if (n < min) {
|
|
88
|
+
min = n;
|
|
89
|
+
}
|
|
90
|
+
if (n > max) {
|
|
91
|
+
max = n;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const avg = sum / col.numericValues.length;
|
|
95
|
+
|
|
96
|
+
if (col.uniqueCount > 1) {
|
|
97
|
+
metrics.push({
|
|
98
|
+
label: `Avg ${col.name}`,
|
|
99
|
+
value: formatNumber(avg),
|
|
100
|
+
...(min === max
|
|
101
|
+
? {}
|
|
102
|
+
: { detail: `${formatNumber(min)} – ${formatNumber(max)}` }),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (col.uniqueCount > 2 && metrics.length < MAX_METRIC_COLUMNS) {
|
|
107
|
+
metrics.push({
|
|
108
|
+
label: `Total ${col.name}`,
|
|
109
|
+
value: formatNumber(sum),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function computeKeyMetrics(
|
|
115
|
+
gridData: LegendAIGridData,
|
|
116
|
+
): LegendAIKeyMetric[] {
|
|
117
|
+
const profiles = profileColumns(gridData);
|
|
118
|
+
const metrics: LegendAIKeyMetric[] = [];
|
|
119
|
+
const rowCount = gridData.rowData.length;
|
|
120
|
+
|
|
121
|
+
metrics.push({
|
|
122
|
+
label: 'Total Rows',
|
|
123
|
+
value: rowCount.toLocaleString(),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const numericCol = profiles.find(
|
|
127
|
+
(c) => c.isNumeric && c.numericValues.length > 0,
|
|
128
|
+
);
|
|
129
|
+
if (numericCol) {
|
|
130
|
+
computeNumericMetrics(numericCol, metrics);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const stringCol = profiles.find(
|
|
134
|
+
(c) => c.isString && c.uniqueCount > 1 && c.uniqueCount <= rowCount,
|
|
135
|
+
);
|
|
136
|
+
if (stringCol) {
|
|
137
|
+
metrics.push({
|
|
138
|
+
label: `Unique ${stringCol.name}`,
|
|
139
|
+
value: stringCol.uniqueCount.toLocaleString(),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return metrics.slice(0, MAX_KEY_METRICS);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function inferChartType(gridData: LegendAIGridData): LegendAIChartType {
|
|
147
|
+
const profiles = profileColumns(gridData);
|
|
148
|
+
const numericCols = profiles.filter((c) => c.isNumeric);
|
|
149
|
+
const stringCols = profiles.filter((c) => c.isString && c.uniqueCount > 1);
|
|
150
|
+
|
|
151
|
+
if (
|
|
152
|
+
stringCols.length >= 1 &&
|
|
153
|
+
numericCols.length >= 1 &&
|
|
154
|
+
gridData.rowData.length <= MAX_BAR_CHART_ROWS
|
|
155
|
+
) {
|
|
156
|
+
if (gridData.rowData.length <= MAX_PIE_CHART_ROWS) {
|
|
157
|
+
return LegendAIChartType.PIE;
|
|
158
|
+
}
|
|
159
|
+
return LegendAIChartType.BAR;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (numericCols.length >= 1 && gridData.rowData.length > 1) {
|
|
163
|
+
return LegendAIChartType.BAR;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return LegendAIChartType.NONE;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function computeChartData(
|
|
170
|
+
gridData: LegendAIGridData,
|
|
171
|
+
): LegendAIChartDataPoint[] {
|
|
172
|
+
const profiles = profileColumns(gridData);
|
|
173
|
+
const numericCol = profiles.find((c) => c.isNumeric);
|
|
174
|
+
const labelCol = profiles.find((c) => c.isString && c.uniqueCount > 1);
|
|
175
|
+
|
|
176
|
+
if (!numericCol) {
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const field = numericCol.name;
|
|
181
|
+
const labelField = labelCol?.name;
|
|
182
|
+
const rows =
|
|
183
|
+
gridData.rowData.length > MAX_PROFILE_SAMPLE
|
|
184
|
+
? gridData.rowData.slice(0, MAX_PROFILE_SAMPLE)
|
|
185
|
+
: gridData.rowData;
|
|
186
|
+
|
|
187
|
+
const entries = rows
|
|
188
|
+
.map((row) => {
|
|
189
|
+
const rawValue = row[field];
|
|
190
|
+
return {
|
|
191
|
+
label: labelField
|
|
192
|
+
? String(row[labelField] ?? '')
|
|
193
|
+
: String(row[gridData.columnDefs[0]?.field ?? ''] ?? ''),
|
|
194
|
+
value: typeof rawValue === 'number' ? rawValue : 0,
|
|
195
|
+
};
|
|
196
|
+
})
|
|
197
|
+
.filter((e) => e.label.length > 0)
|
|
198
|
+
.sort((a, b) => b.value - a.value)
|
|
199
|
+
.slice(0, MAX_CHART_ITEMS);
|
|
200
|
+
|
|
201
|
+
return entries.map((e, i) => ({
|
|
202
|
+
label: e.label,
|
|
203
|
+
value: e.value,
|
|
204
|
+
colorIndex: i % CHART_PALETTE_COUNT,
|
|
205
|
+
}));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function computeTopItems(
|
|
209
|
+
gridData: LegendAIGridData,
|
|
210
|
+
): LegendAIChartDataPoint[] {
|
|
211
|
+
return computeChartData(gridData).slice(0, TOP_N_ITEMS);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function findNumericColumnName(
|
|
215
|
+
gridData: LegendAIGridData,
|
|
216
|
+
): string | undefined {
|
|
217
|
+
const profiles = profileColumns(gridData);
|
|
218
|
+
const numericCol = profiles.find((c) => c.isNumeric);
|
|
219
|
+
if (!numericCol) {
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
const colDef = gridData.columnDefs.find(
|
|
223
|
+
(c) => (c.field ?? c.colId ?? '') === numericCol.name,
|
|
224
|
+
);
|
|
225
|
+
return colDef?.headerName ?? colDef?.field;
|
|
226
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026-present, Goldman Sachs
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { useMemo } from 'react';
|
|
18
|
+
import type { LegendAIChartDataPoint } from '../LegendAI_LegendApplicationPlugin_Extension.js';
|
|
19
|
+
|
|
20
|
+
const CHART_PALETTE_SIZE = 10;
|
|
21
|
+
const DONUT_SIZE = 160;
|
|
22
|
+
const DONUT_STROKE = 24;
|
|
23
|
+
const DONUT_RADIUS = (DONUT_SIZE - DONUT_STROKE) / 2;
|
|
24
|
+
const DONUT_CIRCUMFERENCE = 2 * Math.PI * DONUT_RADIUS;
|
|
25
|
+
|
|
26
|
+
function getChartColor(index: number): string {
|
|
27
|
+
return `var(--ai-chart-color-${(index % CHART_PALETTE_SIZE) + 1})`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function resolveColor(item: LegendAIChartDataPoint, index: number): string {
|
|
31
|
+
return item.color ?? getChartColor(item.colorIndex ?? index);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const LegendAIBarChart = (props: {
|
|
35
|
+
data: LegendAIChartDataPoint[];
|
|
36
|
+
title?: string;
|
|
37
|
+
}): React.ReactNode => {
|
|
38
|
+
const { data, title } = props;
|
|
39
|
+
|
|
40
|
+
const maxValue = useMemo(
|
|
41
|
+
() => data.reduce((m, d) => Math.max(m, d.value), 0) || 1,
|
|
42
|
+
[data],
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
if (data.length === 0) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div className="legend-ai-chart legend-ai-chart--bar">
|
|
51
|
+
{title !== undefined && title.length > 0 && (
|
|
52
|
+
<div className="legend-ai-chart__title">{title}</div>
|
|
53
|
+
)}
|
|
54
|
+
<div className="legend-ai-chart__bars">
|
|
55
|
+
{data.map((item, idx) => {
|
|
56
|
+
const pct = (item.value / maxValue) * 100;
|
|
57
|
+
return (
|
|
58
|
+
<div key={item.label} className="legend-ai-chart__bar-row">
|
|
59
|
+
<span className="legend-ai-chart__bar-label" title={item.label}>
|
|
60
|
+
{item.label}
|
|
61
|
+
</span>
|
|
62
|
+
<div className="legend-ai-chart__bar-track">
|
|
63
|
+
<div
|
|
64
|
+
className="legend-ai-chart__bar-fill"
|
|
65
|
+
style={{
|
|
66
|
+
width: `${pct}%`,
|
|
67
|
+
backgroundColor: resolveColor(item, idx),
|
|
68
|
+
animationDelay: `${idx * 60}ms`,
|
|
69
|
+
}}
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
<span className="legend-ai-chart__bar-value">
|
|
73
|
+
{Number.isInteger(item.value)
|
|
74
|
+
? item.value.toLocaleString()
|
|
75
|
+
: item.value}
|
|
76
|
+
</span>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
})}
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const LegendAIDonutChart = (props: {
|
|
86
|
+
data: LegendAIChartDataPoint[];
|
|
87
|
+
title?: string;
|
|
88
|
+
}): React.ReactNode => {
|
|
89
|
+
const { data, title } = props;
|
|
90
|
+
|
|
91
|
+
const total = useMemo(
|
|
92
|
+
() => data.reduce((s, d) => s + d.value, 0) || 1,
|
|
93
|
+
[data],
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const segments = useMemo(() => {
|
|
97
|
+
let offset = 0;
|
|
98
|
+
return data.map((item) => {
|
|
99
|
+
const pct = item.value / total;
|
|
100
|
+
const dashLen = pct * DONUT_CIRCUMFERENCE;
|
|
101
|
+
const seg = {
|
|
102
|
+
...item,
|
|
103
|
+
dashLen,
|
|
104
|
+
dashOffset: -offset,
|
|
105
|
+
pct,
|
|
106
|
+
};
|
|
107
|
+
offset += dashLen;
|
|
108
|
+
return seg;
|
|
109
|
+
});
|
|
110
|
+
}, [data, total]);
|
|
111
|
+
|
|
112
|
+
if (data.length === 0) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const center = DONUT_SIZE / 2;
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<div className="legend-ai-chart legend-ai-chart--donut">
|
|
120
|
+
{title !== undefined && title.length > 0 && (
|
|
121
|
+
<div className="legend-ai-chart__title">{title}</div>
|
|
122
|
+
)}
|
|
123
|
+
<div className="legend-ai-chart__donut-wrapper">
|
|
124
|
+
<svg
|
|
125
|
+
viewBox={`0 0 ${DONUT_SIZE} ${DONUT_SIZE}`}
|
|
126
|
+
className="legend-ai-chart__donut-svg"
|
|
127
|
+
>
|
|
128
|
+
{segments.map((seg, idx) => (
|
|
129
|
+
<circle
|
|
130
|
+
key={seg.label}
|
|
131
|
+
cx={center}
|
|
132
|
+
cy={center}
|
|
133
|
+
r={DONUT_RADIUS}
|
|
134
|
+
fill="none"
|
|
135
|
+
stroke={resolveColor(seg, idx)}
|
|
136
|
+
strokeWidth={DONUT_STROKE}
|
|
137
|
+
strokeDasharray={`${seg.dashLen} ${DONUT_CIRCUMFERENCE - seg.dashLen}`}
|
|
138
|
+
strokeDashoffset={seg.dashOffset}
|
|
139
|
+
className="legend-ai-chart__donut-segment"
|
|
140
|
+
/>
|
|
141
|
+
))}
|
|
142
|
+
</svg>
|
|
143
|
+
<div className="legend-ai-chart__donut-center">
|
|
144
|
+
<span className="legend-ai-chart__donut-total">
|
|
145
|
+
{total.toLocaleString()}
|
|
146
|
+
</span>
|
|
147
|
+
<span className="legend-ai-chart__donut-total-label">total</span>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
<div className="legend-ai-chart__legend">
|
|
151
|
+
{data.map((item, idx) => (
|
|
152
|
+
<div key={item.label} className="legend-ai-chart__legend-item">
|
|
153
|
+
<span
|
|
154
|
+
className="legend-ai-chart__legend-dot"
|
|
155
|
+
style={{ backgroundColor: resolveColor(item, idx) }}
|
|
156
|
+
/>
|
|
157
|
+
<span className="legend-ai-chart__legend-label">{item.label}</span>
|
|
158
|
+
<span className="legend-ai-chart__legend-value">
|
|
159
|
+
{item.value.toLocaleString()}
|
|
160
|
+
</span>
|
|
161
|
+
</div>
|
|
162
|
+
))}
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
};
|
|
@@ -14,7 +14,14 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
useMemo,
|
|
19
|
+
useCallback,
|
|
20
|
+
useState,
|
|
21
|
+
useRef,
|
|
22
|
+
useEffect,
|
|
23
|
+
useLayoutEffect,
|
|
24
|
+
} from 'react';
|
|
18
25
|
import {
|
|
19
26
|
SendIcon,
|
|
20
27
|
LoadingIcon,
|
|
@@ -23,6 +30,11 @@ import {
|
|
|
23
30
|
TableIcon,
|
|
24
31
|
CopyIcon,
|
|
25
32
|
RefreshIcon,
|
|
33
|
+
CheckIcon,
|
|
34
|
+
TimesIcon,
|
|
35
|
+
CaretDownIcon,
|
|
36
|
+
CaretRightIcon,
|
|
37
|
+
DotIcon,
|
|
26
38
|
MarkdownTextViewer,
|
|
27
39
|
} from '@finos/legend-art';
|
|
28
40
|
import { noop } from '@finos/legend-shared';
|
|
@@ -172,13 +184,17 @@ export function buildSuggestedQueries(
|
|
|
172
184
|
].slice(0, MAX_SUGGESTED_QUERIES);
|
|
173
185
|
}
|
|
174
186
|
|
|
175
|
-
function renderStepStatusIcon(
|
|
187
|
+
export function renderStepStatusIcon(
|
|
176
188
|
status: LegendAIThinkingStepStatus,
|
|
177
189
|
): React.ReactNode {
|
|
178
190
|
if (status === LegendAIThinkingStepStatus.ACTIVE) {
|
|
179
191
|
return <LoadingIcon isLoading={true} />;
|
|
180
192
|
}
|
|
181
|
-
return status === LegendAIThinkingStepStatus.DONE ?
|
|
193
|
+
return status === LegendAIThinkingStepStatus.DONE ? (
|
|
194
|
+
<CheckIcon />
|
|
195
|
+
) : (
|
|
196
|
+
<TimesIcon />
|
|
197
|
+
);
|
|
182
198
|
}
|
|
183
199
|
|
|
184
200
|
const AssistantMessageView = (props: {
|
|
@@ -186,9 +202,15 @@ const AssistantMessageView = (props: {
|
|
|
186
202
|
isThinkingVisible: boolean;
|
|
187
203
|
onToggleThinking: () => void;
|
|
188
204
|
onSuggestedQueryClick?: (query: string) => void;
|
|
205
|
+
onFallbackAction?: (messageId: string) => void;
|
|
189
206
|
}): React.ReactNode => {
|
|
190
|
-
const {
|
|
191
|
-
|
|
207
|
+
const {
|
|
208
|
+
msg,
|
|
209
|
+
isThinkingVisible,
|
|
210
|
+
onToggleThinking,
|
|
211
|
+
onSuggestedQueryClick,
|
|
212
|
+
onFallbackAction,
|
|
213
|
+
} = props;
|
|
192
214
|
|
|
193
215
|
const [sqlCopied, setSqlCopied] = useState(false);
|
|
194
216
|
const copyTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(
|
|
@@ -233,7 +255,7 @@ const AssistantMessageView = (props: {
|
|
|
233
255
|
onClick={onToggleThinking}
|
|
234
256
|
>
|
|
235
257
|
<span className="legend-ai__thinking-toggle-icon">
|
|
236
|
-
{isThinkingVisible ?
|
|
258
|
+
{isThinkingVisible ? <CaretDownIcon /> : <CaretRightIcon />}
|
|
237
259
|
</span>
|
|
238
260
|
Thought for {msg.thinkingDuration ?? '...'}s
|
|
239
261
|
</button>
|
|
@@ -242,7 +264,7 @@ const AssistantMessageView = (props: {
|
|
|
242
264
|
<div className="legend-ai__thinking-steps">
|
|
243
265
|
{msg.thinkingSteps.map((step) => (
|
|
244
266
|
<div
|
|
245
|
-
key={step.
|
|
267
|
+
key={step.id}
|
|
246
268
|
className={`legend-ai__thinking-step legend-ai__thinking-step--${step.status}`}
|
|
247
269
|
>
|
|
248
270
|
<span className="legend-ai__thinking-step-icon">
|
|
@@ -277,7 +299,7 @@ const AssistantMessageView = (props: {
|
|
|
277
299
|
>
|
|
278
300
|
{sqlCopied ? (
|
|
279
301
|
<span className="legend-ai__sql-copy-btn--copied">
|
|
280
|
-
|
|
302
|
+
<CheckIcon />
|
|
281
303
|
</span>
|
|
282
304
|
) : (
|
|
283
305
|
<CopyIcon />
|
|
@@ -297,6 +319,33 @@ const AssistantMessageView = (props: {
|
|
|
297
319
|
</div>
|
|
298
320
|
)}
|
|
299
321
|
|
|
322
|
+
{msg.error && <div className="legend-ai__exec-error">{msg.error}</div>}
|
|
323
|
+
|
|
324
|
+
{msg.gridData && (
|
|
325
|
+
<div className="legend-ai__results-block">
|
|
326
|
+
<div className="legend-ai__results-header">
|
|
327
|
+
<span className="legend-ai__results-header-icon">
|
|
328
|
+
<TableIcon />
|
|
329
|
+
</span>
|
|
330
|
+
<span>Results</span>
|
|
331
|
+
<span className="legend-ai__results-meta">
|
|
332
|
+
{msg.gridData.rowData.length} row
|
|
333
|
+
{msg.gridData.rowData.length === 1 ? '' : 's'}
|
|
334
|
+
{msg.execTime ? (
|
|
335
|
+
<>
|
|
336
|
+
{' '}
|
|
337
|
+
<DotIcon className="legend-ai__results-meta-dot" />{' '}
|
|
338
|
+
{msg.execTime}s
|
|
339
|
+
</>
|
|
340
|
+
) : (
|
|
341
|
+
''
|
|
342
|
+
)}
|
|
343
|
+
</span>
|
|
344
|
+
</div>
|
|
345
|
+
<LegendAIResultGrid data={msg.gridData} />
|
|
346
|
+
</div>
|
|
347
|
+
)}
|
|
348
|
+
|
|
300
349
|
{msg.textAnswer && (
|
|
301
350
|
<div className="legend-ai__text-answer">
|
|
302
351
|
<MarkdownTextViewer
|
|
@@ -306,8 +355,6 @@ const AssistantMessageView = (props: {
|
|
|
306
355
|
</div>
|
|
307
356
|
)}
|
|
308
357
|
|
|
309
|
-
{msg.error && <div className="legend-ai__exec-error">{msg.error}</div>}
|
|
310
|
-
|
|
311
358
|
{!msg.isProcessing &&
|
|
312
359
|
msg.suggestedQueries.length > 0 &&
|
|
313
360
|
onSuggestedQueryClick && (
|
|
@@ -328,21 +375,19 @@ const AssistantMessageView = (props: {
|
|
|
328
375
|
</div>
|
|
329
376
|
)}
|
|
330
377
|
|
|
331
|
-
{msg.
|
|
332
|
-
<
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
<LegendAIResultGrid data={msg.gridData} />
|
|
345
|
-
</div>
|
|
378
|
+
{msg.fallbackAction && !msg.isProcessing && onFallbackAction && (
|
|
379
|
+
<button
|
|
380
|
+
type="button"
|
|
381
|
+
className="legend-ai__fallback-action-btn"
|
|
382
|
+
onClick={(): void => {
|
|
383
|
+
if (msg.fallbackAction?.actionId) {
|
|
384
|
+
onFallbackAction(msg.id);
|
|
385
|
+
}
|
|
386
|
+
}}
|
|
387
|
+
>
|
|
388
|
+
<SparkleStarsIcon />
|
|
389
|
+
<span>{msg.fallbackAction.label}</span>
|
|
390
|
+
</button>
|
|
346
391
|
)}
|
|
347
392
|
</div>
|
|
348
393
|
</div>
|
|
@@ -376,7 +421,7 @@ export const LegendAIChat = (props: LegendAIChatProps): React.ReactNode => {
|
|
|
376
421
|
const hasMessages = state.messages.length > 0;
|
|
377
422
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
378
423
|
|
|
379
|
-
|
|
424
|
+
useLayoutEffect(() => {
|
|
380
425
|
const el = textareaRef.current;
|
|
381
426
|
if (el) {
|
|
382
427
|
el.style.height = 'auto';
|
|
@@ -453,6 +498,9 @@ export const LegendAIChat = (props: LegendAIChatProps): React.ReactNode => {
|
|
|
453
498
|
msg={msg}
|
|
454
499
|
isThinkingVisible={isThinkingVisible}
|
|
455
500
|
onToggleThinking={(): void => state.toggleThinking(msgIndex)}
|
|
501
|
+
onFallbackAction={(messageId): void =>
|
|
502
|
+
state.runFallbackAction(messageId)
|
|
503
|
+
}
|
|
456
504
|
onSuggestedQueryClick={(q): void =>
|
|
457
505
|
state.askQuestionWithIntent(
|
|
458
506
|
q,
|
package/src/legend-ai/index.ts
CHANGED
|
@@ -23,6 +23,7 @@ export {
|
|
|
23
23
|
isNumericColumn,
|
|
24
24
|
isDateColumn,
|
|
25
25
|
buildSuggestedQueries,
|
|
26
|
+
renderStepStatusIcon,
|
|
26
27
|
} from './components/LegendAIChat.js';
|
|
27
28
|
export { LegendAIErrorBoundary } from './components/LegendAIErrorBoundary.js';
|
|
28
29
|
export {
|
|
@@ -31,6 +32,7 @@ export {
|
|
|
31
32
|
addThinkingStep,
|
|
32
33
|
completeThinkingSteps,
|
|
33
34
|
finishWithThinkingError,
|
|
35
|
+
classifyError,
|
|
34
36
|
buildConversationHistory,
|
|
35
37
|
buildGenerationFailureMessage,
|
|
36
38
|
buildExecutionErrorMessage,
|
|
@@ -41,6 +43,22 @@ export {
|
|
|
41
43
|
processQuestion,
|
|
42
44
|
processQuestionWithIntent,
|
|
43
45
|
handleMetadataQuestion,
|
|
46
|
+
elapsedSeconds,
|
|
47
|
+
createMessagePair,
|
|
48
|
+
analyzeOrchestratorResults,
|
|
44
49
|
type MessageSetter,
|
|
50
|
+
type LegendAIOperationContext,
|
|
45
51
|
} from './stores/LegendAIChatState.js';
|
|
46
52
|
export { LegendAIResultGrid } from './components/LegendAIResultGrid.js';
|
|
53
|
+
export { LegendAIAnalysisPanel } from './components/LegendAIAnalysisPanel.js';
|
|
54
|
+
export {
|
|
55
|
+
LegendAIBarChart,
|
|
56
|
+
LegendAIDonutChart,
|
|
57
|
+
} from './components/LegendAICharts.js';
|
|
58
|
+
export {
|
|
59
|
+
computeKeyMetrics,
|
|
60
|
+
computeChartData,
|
|
61
|
+
inferChartType,
|
|
62
|
+
computeTopItems,
|
|
63
|
+
findNumericColumnName,
|
|
64
|
+
} from './components/LegendAIAnalysisUtils.js';
|