@finos/legend-lego 2.0.197 → 2.0.198
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/LegendAIDocEnrichment.d.ts +60 -0
- package/lib/legend-ai/LegendAIDocEnrichment.d.ts.map +1 -0
- package/lib/legend-ai/LegendAIDocEnrichment.js +429 -0
- package/lib/legend-ai/LegendAIDocEnrichment.js.map +1 -0
- package/lib/legend-ai/LegendAITypes.d.ts +127 -1
- package/lib/legend-ai/LegendAITypes.d.ts.map +1 -1
- package/lib/legend-ai/LegendAITypes.js +111 -2
- package/lib/legend-ai/LegendAITypes.js.map +1 -1
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts +14 -1
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts.map +1 -1
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.js.map +1 -1
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.d.ts +2 -1
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.d.ts.map +1 -1
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.js +37 -2
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.js.map +1 -1
- package/lib/legend-ai/components/LegendAIAnalysisPanel.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAIAnalysisPanel.js +11 -12
- package/lib/legend-ai/components/LegendAIAnalysisPanel.js.map +1 -1
- package/lib/legend-ai/components/LegendAIAnalysisUtils.d.ts +7 -0
- package/lib/legend-ai/components/LegendAIAnalysisUtils.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAIAnalysisUtils.js +106 -41
- package/lib/legend-ai/components/LegendAIAnalysisUtils.js.map +1 -1
- package/lib/legend-ai/components/LegendAIChat.d.ts +1 -5
- package/lib/legend-ai/components/LegendAIChat.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAIChat.js +168 -109
- package/lib/legend-ai/components/LegendAIChat.js.map +1 -1
- package/lib/legend-ai/components/LegendAIChatHelpers.d.ts +21 -0
- package/lib/legend-ai/components/LegendAIChatHelpers.d.ts.map +1 -0
- package/lib/legend-ai/components/LegendAIChatHelpers.js +85 -0
- package/lib/legend-ai/components/LegendAIChatHelpers.js.map +1 -0
- package/lib/legend-ai/components/LegendAIChatInput.d.ts +21 -0
- package/lib/legend-ai/components/LegendAIChatInput.d.ts.map +1 -0
- package/lib/legend-ai/components/LegendAIChatInput.js +78 -0
- package/lib/legend-ai/components/LegendAIChatInput.js.map +1 -0
- package/lib/legend-ai/components/LegendAIScopeSelector.d.ts +25 -0
- package/lib/legend-ai/components/LegendAIScopeSelector.d.ts.map +1 -0
- package/lib/legend-ai/components/LegendAIScopeSelector.js +85 -0
- package/lib/legend-ai/components/LegendAIScopeSelector.js.map +1 -0
- package/lib/legend-ai/index.d.ts +8 -3
- package/lib/legend-ai/index.d.ts.map +1 -1
- package/lib/legend-ai/index.js +8 -3
- package/lib/legend-ai/index.js.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatProcessors.d.ts +105 -0
- package/lib/legend-ai/stores/LegendAIChatProcessors.d.ts.map +1 -0
- package/lib/legend-ai/stores/LegendAIChatProcessors.js +1482 -0
- package/lib/legend-ai/stores/LegendAIChatProcessors.js.map +1 -0
- package/lib/legend-ai/stores/LegendAIChatState.d.ts +2 -35
- package/lib/legend-ai/stores/LegendAIChatState.d.ts.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatState.js +114 -949
- package/lib/legend-ai/stores/LegendAIChatState.js.map +1 -1
- package/package.json +5 -5
- package/src/legend-ai/LegendAIDocEnrichment.ts +572 -0
- package/src/legend-ai/LegendAITypes.ts +213 -5
- package/src/legend-ai/LegendAI_LegendApplicationPlugin_Extension.ts +25 -0
- package/src/legend-ai/__test-utils__/LegendAITestUtils.ts +55 -1
- package/src/legend-ai/components/LegendAIAnalysisPanel.tsx +14 -34
- package/src/legend-ai/components/LegendAIAnalysisUtils.ts +157 -47
- package/src/legend-ai/components/LegendAIChat.tsx +389 -206
- package/src/legend-ai/components/LegendAIChatHelpers.ts +117 -0
- package/src/legend-ai/components/LegendAIChatInput.tsx +209 -0
- package/src/legend-ai/components/LegendAIScopeSelector.tsx +199 -0
- package/src/legend-ai/index.ts +31 -4
- package/src/legend-ai/stores/LegendAIChatProcessors.ts +2563 -0
- package/src/legend-ai/stores/LegendAIChatState.ts +161 -1697
- package/tsconfig.json +5 -0
|
@@ -14,175 +14,156 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { useMemo, useCallback, useState, useRef, useEffect } from 'react';
|
|
17
18
|
import {
|
|
18
|
-
useMemo,
|
|
19
|
-
useCallback,
|
|
20
|
-
useState,
|
|
21
|
-
useRef,
|
|
22
|
-
useEffect,
|
|
23
|
-
useLayoutEffect,
|
|
24
|
-
} from 'react';
|
|
25
|
-
import {
|
|
26
|
-
SendIcon,
|
|
27
|
-
LoadingIcon,
|
|
28
19
|
SparkleStarsIcon,
|
|
29
20
|
CodeIcon,
|
|
30
21
|
TableIcon,
|
|
31
22
|
CopyIcon,
|
|
32
|
-
RefreshIcon,
|
|
33
23
|
CheckIcon,
|
|
34
24
|
TimesIcon,
|
|
25
|
+
MinusIcon,
|
|
26
|
+
PlusIcon,
|
|
35
27
|
CaretDownIcon,
|
|
36
28
|
CaretRightIcon,
|
|
37
29
|
DotIcon,
|
|
30
|
+
LoadingIcon,
|
|
31
|
+
LikeIcon,
|
|
32
|
+
DislikeIcon,
|
|
33
|
+
ExternalLinkIcon,
|
|
38
34
|
MarkdownTextViewer,
|
|
39
35
|
} from '@finos/legend-art';
|
|
40
36
|
import { noop } from '@finos/legend-shared';
|
|
41
|
-
import { PRIMITIVE_TYPE } from '@finos/legend-graph';
|
|
42
37
|
import {
|
|
43
38
|
type LegendAIChatProps,
|
|
44
39
|
type LegendAIAssistantMessage,
|
|
45
|
-
type
|
|
46
|
-
type
|
|
47
|
-
type
|
|
40
|
+
type LegendAIMessageFeedback,
|
|
41
|
+
type LegendAIThinkingStep,
|
|
42
|
+
type LegendAIScopeItem,
|
|
43
|
+
type LegendAIQuestionIntent,
|
|
44
|
+
LegendAIMessageFeedbackRating,
|
|
48
45
|
LegendAIThinkingStepStatus,
|
|
49
46
|
LegendAIMessageRole,
|
|
50
|
-
|
|
47
|
+
LegendAIErrorType,
|
|
48
|
+
classifyQuestionIntentFast,
|
|
51
49
|
} from '../LegendAITypes.js';
|
|
52
50
|
import { useLegendAIChatState } from '../stores/LegendAIChatState.js';
|
|
53
51
|
import { LegendAIResultGrid } from './LegendAIResultGrid.js';
|
|
52
|
+
import { LegendAIAnalysisPanel } from './LegendAIAnalysisPanel.js';
|
|
53
|
+
import { LegendAIChatInput } from './LegendAIChatInput.js';
|
|
54
|
+
import { buildSuggestedQueries } from './LegendAIChatHelpers.js';
|
|
54
55
|
|
|
55
56
|
export const LEGEND_AI_ANCHOR_ID = 'legend-ai-anchor';
|
|
56
57
|
|
|
57
58
|
const COPY_FEEDBACK_DURATION_MS = 2000;
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const DATE_TYPES = new Set<string>([
|
|
70
|
-
PRIMITIVE_TYPE.DATE,
|
|
71
|
-
PRIMITIVE_TYPE.STRICTDATE,
|
|
72
|
-
PRIMITIVE_TYPE.DATETIME,
|
|
73
|
-
]);
|
|
74
|
-
|
|
75
|
-
export function isStringColumn(c: TDSColumnSchema): boolean {
|
|
76
|
-
return STRING_TYPES.has(c.type ?? '') && !c.name.toLowerCase().includes('id');
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function isNumericColumn(c: TDSColumnSchema): boolean {
|
|
80
|
-
return NUMERIC_TYPES.has(c.type ?? '');
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function isDateColumn(c: TDSColumnSchema): boolean {
|
|
84
|
-
return (
|
|
85
|
-
DATE_TYPES.has(c.type ?? '') ||
|
|
86
|
-
c.name.toLowerCase().includes('date') ||
|
|
87
|
-
c.name.toLowerCase().includes('time')
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function buildDataInsightSuggestions(
|
|
92
|
-
primary: TDSServiceSchema,
|
|
93
|
-
stringCol: TDSColumnSchema | undefined,
|
|
94
|
-
numericCol: TDSColumnSchema | undefined,
|
|
95
|
-
dateCol: TDSColumnSchema | undefined,
|
|
96
|
-
): string[] {
|
|
97
|
-
const result: string[] = [];
|
|
98
|
-
if (stringCol && numericCol) {
|
|
99
|
-
result.push(
|
|
100
|
-
`What are the top ${stringCol.name} values by total ${numericCol.name} in ${primary.title}?`,
|
|
101
|
-
);
|
|
102
|
-
} else if (stringCol) {
|
|
103
|
-
result.push(
|
|
104
|
-
`What are the distinct ${stringCol.name} values in ${primary.title}?`,
|
|
105
|
-
);
|
|
59
|
+
const METADATA_CONTEXT_HEADING = '### Metadata context';
|
|
60
|
+
const QUERY_ANALYSIS_HEADING = '### Query analysis';
|
|
61
|
+
|
|
62
|
+
function toUserFacingThinkingLabel(label: string): string {
|
|
63
|
+
const normalized = label.toLowerCase();
|
|
64
|
+
if (
|
|
65
|
+
normalized.includes('analyzing your question') ||
|
|
66
|
+
normalized.includes('intent is ambiguous')
|
|
67
|
+
) {
|
|
68
|
+
return 'Understanding your request';
|
|
106
69
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
70
|
+
if (
|
|
71
|
+
normalized.includes('building metadata context') ||
|
|
72
|
+
normalized.includes('answering from product metadata')
|
|
73
|
+
) {
|
|
74
|
+
return 'Checking product capabilities and services';
|
|
112
75
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
76
|
+
if (
|
|
77
|
+
normalized.includes('found relevant services') ||
|
|
78
|
+
normalized.includes('selecting best service') ||
|
|
79
|
+
normalized.includes('building context from service schemas') ||
|
|
80
|
+
normalized.includes('preparing data query') ||
|
|
81
|
+
normalized.includes('generating sql query') ||
|
|
82
|
+
normalized.includes('verifying query correctness') ||
|
|
83
|
+
normalized.includes('query corrected') ||
|
|
84
|
+
normalized.includes('max verification attempts reached') ||
|
|
85
|
+
normalized.includes('judge approved a non-sql draft')
|
|
86
|
+
) {
|
|
87
|
+
return 'Trying a data query when helpful';
|
|
116
88
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
89
|
+
if (
|
|
90
|
+
normalized.includes('retrieved ') ||
|
|
91
|
+
normalized.includes('executing') ||
|
|
92
|
+
normalized.includes('analyzing results') ||
|
|
93
|
+
normalized.includes('verifying answer coverage')
|
|
94
|
+
) {
|
|
95
|
+
return 'Summarizing what matters for your question';
|
|
123
96
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (!svcA || !svcB) {
|
|
127
|
-
return [];
|
|
97
|
+
if (normalized.includes('error')) {
|
|
98
|
+
return 'Hit an issue while preparing the answer';
|
|
128
99
|
}
|
|
100
|
+
return label;
|
|
101
|
+
}
|
|
129
102
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
103
|
+
function formatThinkingSteps(
|
|
104
|
+
thinkingSteps: LegendAIThinkingStep[],
|
|
105
|
+
): LegendAIThinkingStep[] {
|
|
106
|
+
const formatted: LegendAIThinkingStep[] = [];
|
|
107
|
+
for (const step of thinkingSteps) {
|
|
108
|
+
const userLabel = toUserFacingThinkingLabel(step.label);
|
|
109
|
+
const last = formatted[formatted.length - 1];
|
|
110
|
+
if (last?.label === userLabel) {
|
|
111
|
+
formatted[formatted.length - 1] = {
|
|
112
|
+
...last,
|
|
113
|
+
status: step.status,
|
|
114
|
+
};
|
|
115
|
+
} else {
|
|
116
|
+
formatted.push({
|
|
117
|
+
...step,
|
|
118
|
+
label: userLabel,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
140
121
|
}
|
|
141
|
-
|
|
142
|
-
return result;
|
|
122
|
+
return formatted;
|
|
143
123
|
}
|
|
144
124
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
];
|
|
152
|
-
|
|
153
|
-
if (services.length === 0) {
|
|
154
|
-
return [
|
|
155
|
-
...suggestions,
|
|
156
|
-
'What access points are available?',
|
|
157
|
-
'Describe the data model and key entities',
|
|
158
|
-
];
|
|
125
|
+
function splitCombinedAnswer(textAnswer: string | null): {
|
|
126
|
+
metadataContext: string | null;
|
|
127
|
+
queryAnalysis: string | null;
|
|
128
|
+
} {
|
|
129
|
+
if (!textAnswer) {
|
|
130
|
+
return { metadataContext: null, queryAnalysis: null };
|
|
159
131
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
return [
|
|
164
|
-
...suggestions,
|
|
165
|
-
'What access points are available and what columns do they have?',
|
|
166
|
-
];
|
|
132
|
+
const metadataIndex = textAnswer.indexOf(METADATA_CONTEXT_HEADING);
|
|
133
|
+
if (metadataIndex < 0) {
|
|
134
|
+
return { metadataContext: null, queryAnalysis: textAnswer };
|
|
167
135
|
}
|
|
168
136
|
|
|
169
|
-
const
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
137
|
+
const metadataStart = metadataIndex + METADATA_CONTEXT_HEADING.length;
|
|
138
|
+
const queryIndex = textAnswer.indexOf(QUERY_ANALYSIS_HEADING, metadataStart);
|
|
139
|
+
|
|
140
|
+
const metadataContext =
|
|
141
|
+
queryIndex >= 0
|
|
142
|
+
? textAnswer.slice(metadataStart, queryIndex).trim()
|
|
143
|
+
: textAnswer.slice(metadataStart).trim();
|
|
144
|
+
const queryAnalysis =
|
|
145
|
+
queryIndex >= 0
|
|
146
|
+
? textAnswer.slice(queryIndex + QUERY_ANALYSIS_HEADING.length).trim() ||
|
|
147
|
+
null
|
|
148
|
+
: null;
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
metadataContext: metadataContext.length > 0 ? metadataContext : null,
|
|
152
|
+
queryAnalysis,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
174
155
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
156
|
+
const AISummaryRenderer = ({ value }: { value: string }): React.ReactNode => (
|
|
157
|
+
<MarkdownTextViewer value={{ value }} className="legend-ai__text-answer-md" />
|
|
158
|
+
);
|
|
178
159
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
160
|
+
const DEFAULT_SCOPES: LegendAIScopeItem[] = [
|
|
161
|
+
{
|
|
162
|
+
id: 'legend-ai-mcp',
|
|
163
|
+
label: 'Legend AI MCP',
|
|
164
|
+
description: 'Model Context Protocol via Marketplace /mcp proxy',
|
|
165
|
+
},
|
|
166
|
+
];
|
|
186
167
|
|
|
187
168
|
export function renderStepStatusIcon(
|
|
188
169
|
status: LegendAIThinkingStepStatus,
|
|
@@ -199,19 +180,36 @@ export function renderStepStatusIcon(
|
|
|
199
180
|
|
|
200
181
|
const AssistantMessageView = (props: {
|
|
201
182
|
msg: LegendAIAssistantMessage;
|
|
183
|
+
questionText: string;
|
|
202
184
|
isThinkingVisible: boolean;
|
|
203
185
|
onToggleThinking: () => void;
|
|
186
|
+
onMessageFeedback?: (
|
|
187
|
+
feedback: LegendAIMessageFeedback,
|
|
188
|
+
) => Promise<void> | void;
|
|
189
|
+
selectedFeedbackRating: LegendAIMessageFeedbackRating | undefined;
|
|
190
|
+
feedbackSubmitting: boolean;
|
|
204
191
|
onSuggestedQueryClick?: (query: string) => void;
|
|
205
192
|
onFallbackAction?: (messageId: string) => void;
|
|
193
|
+
enghubDocUrl?: string;
|
|
194
|
+
enthubRequestAccessUrl?: string;
|
|
206
195
|
}): React.ReactNode => {
|
|
207
196
|
const {
|
|
208
197
|
msg,
|
|
198
|
+
questionText,
|
|
209
199
|
isThinkingVisible,
|
|
210
200
|
onToggleThinking,
|
|
201
|
+
onMessageFeedback,
|
|
202
|
+
selectedFeedbackRating,
|
|
203
|
+
feedbackSubmitting,
|
|
211
204
|
onSuggestedQueryClick,
|
|
212
205
|
onFallbackAction,
|
|
206
|
+
enghubDocUrl,
|
|
207
|
+
enthubRequestAccessUrl,
|
|
213
208
|
} = props;
|
|
214
209
|
|
|
210
|
+
const hasPermissionAccessLinks =
|
|
211
|
+
enghubDocUrl !== undefined || enthubRequestAccessUrl !== undefined;
|
|
212
|
+
|
|
215
213
|
const [sqlCopied, setSqlCopied] = useState(false);
|
|
216
214
|
const copyTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(
|
|
217
215
|
undefined,
|
|
@@ -228,7 +226,7 @@ const AssistantMessageView = (props: {
|
|
|
228
226
|
|
|
229
227
|
const handleCopySql = useCallback(() => {
|
|
230
228
|
if (msg.sql) {
|
|
231
|
-
navigator.clipboard.writeText(msg.sql).catch(noop
|
|
229
|
+
navigator.clipboard.writeText(msg.sql).catch(noop);
|
|
232
230
|
setSqlCopied(true);
|
|
233
231
|
if (copyTimerRef.current !== undefined) {
|
|
234
232
|
clearTimeout(copyTimerRef.current);
|
|
@@ -240,13 +238,48 @@ const AssistantMessageView = (props: {
|
|
|
240
238
|
}
|
|
241
239
|
}, [msg.sql]);
|
|
242
240
|
|
|
241
|
+
const canShowFeedback =
|
|
242
|
+
!msg.isProcessing &&
|
|
243
|
+
(msg.textAnswer !== null || msg.gridData !== null || msg.error !== null);
|
|
244
|
+
const visibleThinkingSteps = formatThinkingSteps(msg.thinkingSteps);
|
|
245
|
+
const { metadataContext, queryAnalysis } = splitCombinedAnswer(
|
|
246
|
+
msg.textAnswer,
|
|
247
|
+
);
|
|
248
|
+
const analysisSummary = (() => {
|
|
249
|
+
if (msg.gridData === null) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
return queryAnalysis ?? (metadataContext === null ? msg.textAnswer : null);
|
|
253
|
+
})();
|
|
254
|
+
const plainAnswer =
|
|
255
|
+
msg.gridData === null ? (metadataContext ?? msg.textAnswer) : null;
|
|
256
|
+
|
|
257
|
+
const submitFeedback = useCallback(
|
|
258
|
+
(rating: LegendAIMessageFeedbackRating): void => {
|
|
259
|
+
const result = onMessageFeedback?.({
|
|
260
|
+
messageId: msg.id,
|
|
261
|
+
rating,
|
|
262
|
+
question: questionText,
|
|
263
|
+
...(msg.textAnswer === null ? {} : { answer: msg.textAnswer }),
|
|
264
|
+
...(msg.sql === null ? {} : { sql: msg.sql }),
|
|
265
|
+
...(msg.gridData === null
|
|
266
|
+
? {}
|
|
267
|
+
: { rowCount: msg.gridData.rowData.length }),
|
|
268
|
+
});
|
|
269
|
+
if (result instanceof Promise) {
|
|
270
|
+
result.catch(noop);
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
[msg, onMessageFeedback, questionText],
|
|
274
|
+
);
|
|
275
|
+
|
|
243
276
|
return (
|
|
244
277
|
<div className="legend-ai__msg legend-ai__msg--assistant">
|
|
245
278
|
<div className="legend-ai__msg-avatar">
|
|
246
279
|
<SparkleStarsIcon />
|
|
247
280
|
</div>
|
|
248
281
|
<div className="legend-ai__msg-content">
|
|
249
|
-
{
|
|
282
|
+
{visibleThinkingSteps.length > 0 && (
|
|
250
283
|
<div className="legend-ai__thinking">
|
|
251
284
|
{!msg.isProcessing && (
|
|
252
285
|
<button
|
|
@@ -262,7 +295,7 @@ const AssistantMessageView = (props: {
|
|
|
262
295
|
)}
|
|
263
296
|
{isThinkingVisible && (
|
|
264
297
|
<div className="legend-ai__thinking-steps">
|
|
265
|
-
{
|
|
298
|
+
{visibleThinkingSteps.map((step) => (
|
|
266
299
|
<div
|
|
267
300
|
key={step.id}
|
|
268
301
|
className={`legend-ai__thinking-step legend-ai__thinking-step--${step.status}`}
|
|
@@ -278,6 +311,15 @@ const AssistantMessageView = (props: {
|
|
|
278
311
|
</div>
|
|
279
312
|
)}
|
|
280
313
|
|
|
314
|
+
{metadataContext && msg.gridData && (
|
|
315
|
+
<div className="legend-ai__inline-answer">
|
|
316
|
+
<MarkdownTextViewer
|
|
317
|
+
value={{ value: metadataContext }}
|
|
318
|
+
className="legend-ai__text-answer-md"
|
|
319
|
+
/>
|
|
320
|
+
</div>
|
|
321
|
+
)}
|
|
322
|
+
|
|
281
323
|
{msg.sql && (
|
|
282
324
|
<div className="legend-ai__sql-block">
|
|
283
325
|
<div className="legend-ai__sql-block-header">
|
|
@@ -319,7 +361,43 @@ const AssistantMessageView = (props: {
|
|
|
319
361
|
</div>
|
|
320
362
|
)}
|
|
321
363
|
|
|
322
|
-
{msg.error &&
|
|
364
|
+
{msg.error && (
|
|
365
|
+
<div className="legend-ai__exec-error">
|
|
366
|
+
{msg.error}
|
|
367
|
+
{msg.errorType === LegendAIErrorType.PERMISSION &&
|
|
368
|
+
hasPermissionAccessLinks && (
|
|
369
|
+
<div className="legend-ai__permission-error-action">
|
|
370
|
+
<span className="legend-ai__permission-error-note">
|
|
371
|
+
Need access?
|
|
372
|
+
</span>
|
|
373
|
+
<div className="legend-ai__permission-error-btns">
|
|
374
|
+
{enghubDocUrl && (
|
|
375
|
+
<a
|
|
376
|
+
className="legend-ai__permission-error-btn"
|
|
377
|
+
href={enghubDocUrl}
|
|
378
|
+
target="_blank"
|
|
379
|
+
rel="noopener noreferrer"
|
|
380
|
+
>
|
|
381
|
+
<ExternalLinkIcon />
|
|
382
|
+
<span>View Documentation</span>
|
|
383
|
+
</a>
|
|
384
|
+
)}
|
|
385
|
+
{enthubRequestAccessUrl && (
|
|
386
|
+
<a
|
|
387
|
+
className="legend-ai__permission-error-btn legend-ai__permission-error-btn--primary"
|
|
388
|
+
href={enthubRequestAccessUrl}
|
|
389
|
+
target="_blank"
|
|
390
|
+
rel="noopener noreferrer"
|
|
391
|
+
>
|
|
392
|
+
<ExternalLinkIcon />
|
|
393
|
+
<span>Request Access</span>
|
|
394
|
+
</a>
|
|
395
|
+
)}
|
|
396
|
+
</div>
|
|
397
|
+
</div>
|
|
398
|
+
)}
|
|
399
|
+
</div>
|
|
400
|
+
)}
|
|
323
401
|
|
|
324
402
|
{msg.gridData && (
|
|
325
403
|
<div className="legend-ai__results-block">
|
|
@@ -346,15 +424,30 @@ const AssistantMessageView = (props: {
|
|
|
346
424
|
</div>
|
|
347
425
|
)}
|
|
348
426
|
|
|
349
|
-
{
|
|
350
|
-
<div className="legend-
|
|
427
|
+
{plainAnswer && (
|
|
428
|
+
<div className="legend-ai__inline-answer">
|
|
351
429
|
<MarkdownTextViewer
|
|
352
|
-
value={{ value:
|
|
430
|
+
value={{ value: plainAnswer }}
|
|
353
431
|
className="legend-ai__text-answer-md"
|
|
354
432
|
/>
|
|
355
433
|
</div>
|
|
356
434
|
)}
|
|
357
435
|
|
|
436
|
+
{analysisSummary && msg.gridData && (
|
|
437
|
+
<LegendAIAnalysisPanel
|
|
438
|
+
gridData={msg.gridData}
|
|
439
|
+
summary={analysisSummary}
|
|
440
|
+
SummaryRenderer={AISummaryRenderer}
|
|
441
|
+
/>
|
|
442
|
+
)}
|
|
443
|
+
|
|
444
|
+
{msg.isProcessing && !msg.isExecuting && msg.gridData && (
|
|
445
|
+
<div className="legend-ai__analyzing">
|
|
446
|
+
<LoadingIcon isLoading={true} />
|
|
447
|
+
<span>Analyzing results...</span>
|
|
448
|
+
</div>
|
|
449
|
+
)}
|
|
450
|
+
|
|
358
451
|
{!msg.isProcessing &&
|
|
359
452
|
msg.suggestedQueries.length > 0 &&
|
|
360
453
|
onSuggestedQueryClick && (
|
|
@@ -389,6 +482,50 @@ const AssistantMessageView = (props: {
|
|
|
389
482
|
<span>{msg.fallbackAction.label}</span>
|
|
390
483
|
</button>
|
|
391
484
|
)}
|
|
485
|
+
|
|
486
|
+
{canShowFeedback && (
|
|
487
|
+
<div className="legend-ai__message-feedback">
|
|
488
|
+
<span className="legend-ai__message-feedback-label">
|
|
489
|
+
Did this answer your question?
|
|
490
|
+
</span>
|
|
491
|
+
<div className="legend-ai__message-feedback-actions">
|
|
492
|
+
<button
|
|
493
|
+
type="button"
|
|
494
|
+
className={`legend-ai__message-feedback-btn${
|
|
495
|
+
selectedFeedbackRating ===
|
|
496
|
+
LegendAIMessageFeedbackRating.THUMBS_UP
|
|
497
|
+
? 'legend-ai__message-feedback-btn--selected'
|
|
498
|
+
: ''
|
|
499
|
+
}`}
|
|
500
|
+
title="Thumbs up"
|
|
501
|
+
aria-label="Thumbs up"
|
|
502
|
+
onClick={(): void =>
|
|
503
|
+
submitFeedback(LegendAIMessageFeedbackRating.THUMBS_UP)
|
|
504
|
+
}
|
|
505
|
+
disabled={feedbackSubmitting}
|
|
506
|
+
>
|
|
507
|
+
<LikeIcon />
|
|
508
|
+
</button>
|
|
509
|
+
<button
|
|
510
|
+
type="button"
|
|
511
|
+
className={`legend-ai__message-feedback-btn${
|
|
512
|
+
selectedFeedbackRating ===
|
|
513
|
+
LegendAIMessageFeedbackRating.THUMBS_DOWN
|
|
514
|
+
? 'legend-ai__message-feedback-btn--selected'
|
|
515
|
+
: ''
|
|
516
|
+
}`}
|
|
517
|
+
title="Thumbs down"
|
|
518
|
+
aria-label="Thumbs down"
|
|
519
|
+
onClick={(): void =>
|
|
520
|
+
submitFeedback(LegendAIMessageFeedbackRating.THUMBS_DOWN)
|
|
521
|
+
}
|
|
522
|
+
disabled={feedbackSubmitting}
|
|
523
|
+
>
|
|
524
|
+
<DislikeIcon />
|
|
525
|
+
</button>
|
|
526
|
+
</div>
|
|
527
|
+
</div>
|
|
528
|
+
)}
|
|
392
529
|
</div>
|
|
393
530
|
</div>
|
|
394
531
|
);
|
|
@@ -404,6 +541,10 @@ export const LegendAIChat = (props: LegendAIChatProps): React.ReactNode => {
|
|
|
404
541
|
plugin,
|
|
405
542
|
dataProductCoordinates,
|
|
406
543
|
pureExecutionContext,
|
|
544
|
+
availableScopes,
|
|
545
|
+
onMessageFeedback,
|
|
546
|
+
onClose,
|
|
547
|
+
onMinimize,
|
|
407
548
|
} = props;
|
|
408
549
|
const state = useLegendAIChatState(
|
|
409
550
|
services,
|
|
@@ -418,16 +559,58 @@ export const LegendAIChat = (props: LegendAIChatProps): React.ReactNode => {
|
|
|
418
559
|
() => buildSuggestedQueries(services, metadata),
|
|
419
560
|
[services, metadata],
|
|
420
561
|
);
|
|
562
|
+
const hasServices = services.length > 0;
|
|
563
|
+
|
|
564
|
+
const inferSuggestedQueryIntent = useCallback(
|
|
565
|
+
(query: string): LegendAIQuestionIntent =>
|
|
566
|
+
classifyQuestionIntentFast(query, hasServices).intent,
|
|
567
|
+
[hasServices],
|
|
568
|
+
);
|
|
421
569
|
const hasMessages = state.messages.length > 0;
|
|
422
|
-
const
|
|
570
|
+
const scopes = availableScopes ?? DEFAULT_SCOPES;
|
|
571
|
+
const [feedbackByMessageId, setFeedbackByMessageId] = useState<
|
|
572
|
+
Map<string, LegendAIMessageFeedbackRating>
|
|
573
|
+
>(new Map());
|
|
574
|
+
const [pendingFeedbackByMessageId, setPendingFeedbackByMessageId] = useState<
|
|
575
|
+
Set<string>
|
|
576
|
+
>(new Set());
|
|
577
|
+
|
|
578
|
+
const handleMessageFeedback = useCallback(
|
|
579
|
+
async (feedback: LegendAIMessageFeedback): Promise<void> => {
|
|
580
|
+
setFeedbackByMessageId((prev) => {
|
|
581
|
+
const next = new Map(prev);
|
|
582
|
+
next.set(feedback.messageId, feedback.rating);
|
|
583
|
+
return next;
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
if (!onMessageFeedback) {
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
423
589
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
590
|
+
setPendingFeedbackByMessageId((prev) => {
|
|
591
|
+
const next = new Set(prev);
|
|
592
|
+
next.add(feedback.messageId);
|
|
593
|
+
return next;
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
try {
|
|
597
|
+
await onMessageFeedback(feedback);
|
|
598
|
+
} catch {
|
|
599
|
+
setFeedbackByMessageId((prev) => {
|
|
600
|
+
const next = new Map(prev);
|
|
601
|
+
next.delete(feedback.messageId);
|
|
602
|
+
return next;
|
|
603
|
+
});
|
|
604
|
+
} finally {
|
|
605
|
+
setPendingFeedbackByMessageId((prev) => {
|
|
606
|
+
const next = new Set(prev);
|
|
607
|
+
next.delete(feedback.messageId);
|
|
608
|
+
return next;
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
},
|
|
612
|
+
[onMessageFeedback],
|
|
613
|
+
);
|
|
431
614
|
|
|
432
615
|
return (
|
|
433
616
|
<div className="legend-ai" id={LEGEND_AI_ANCHOR_ID}>
|
|
@@ -436,18 +619,39 @@ export const LegendAIChat = (props: LegendAIChatProps): React.ReactNode => {
|
|
|
436
619
|
<SparkleStarsIcon />
|
|
437
620
|
</div>
|
|
438
621
|
<div className="legend-ai__title">{title ?? 'Legend AI'}</div>
|
|
439
|
-
|
|
622
|
+
<div className="legend-ai__header-actions">
|
|
440
623
|
<button
|
|
441
624
|
type="button"
|
|
442
|
-
className="legend-
|
|
443
|
-
title="
|
|
444
|
-
aria-label="
|
|
625
|
+
className="legend-ai__header-action"
|
|
626
|
+
title="New chat"
|
|
627
|
+
aria-label="New chat"
|
|
445
628
|
onClick={(): void => state.clearChat()}
|
|
446
629
|
>
|
|
447
|
-
<
|
|
448
|
-
<span>Clear</span>
|
|
630
|
+
<PlusIcon />
|
|
449
631
|
</button>
|
|
450
|
-
|
|
632
|
+
{onMinimize && (
|
|
633
|
+
<button
|
|
634
|
+
type="button"
|
|
635
|
+
className="legend-ai__header-action"
|
|
636
|
+
title="Minimize"
|
|
637
|
+
aria-label="Minimize"
|
|
638
|
+
onClick={onMinimize}
|
|
639
|
+
>
|
|
640
|
+
<MinusIcon />
|
|
641
|
+
</button>
|
|
642
|
+
)}
|
|
643
|
+
{onClose && (
|
|
644
|
+
<button
|
|
645
|
+
type="button"
|
|
646
|
+
className="legend-ai__header-action"
|
|
647
|
+
title="Close"
|
|
648
|
+
aria-label="Close"
|
|
649
|
+
onClick={onClose}
|
|
650
|
+
>
|
|
651
|
+
<TimesIcon />
|
|
652
|
+
</button>
|
|
653
|
+
)}
|
|
654
|
+
</div>
|
|
451
655
|
</div>
|
|
452
656
|
|
|
453
657
|
<div className="legend-ai__conversation" ref={state.conversationRef}>
|
|
@@ -460,23 +664,21 @@ export const LegendAIChat = (props: LegendAIChatProps): React.ReactNode => {
|
|
|
460
664
|
Ask a question about your data
|
|
461
665
|
</div>
|
|
462
666
|
<div className="legend-ai__suggestions">
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
))}
|
|
479
|
-
</div>
|
|
667
|
+
{suggestedQueries.map((q) => (
|
|
668
|
+
<button
|
|
669
|
+
key={q}
|
|
670
|
+
type="button"
|
|
671
|
+
className="legend-ai__suggestion-chip"
|
|
672
|
+
onClick={(): void => {
|
|
673
|
+
state.askQuestionWithIntent(
|
|
674
|
+
q,
|
|
675
|
+
inferSuggestedQueryIntent(q),
|
|
676
|
+
);
|
|
677
|
+
}}
|
|
678
|
+
>
|
|
679
|
+
{q}
|
|
680
|
+
</button>
|
|
681
|
+
))}
|
|
480
682
|
</div>
|
|
481
683
|
</div>
|
|
482
684
|
)}
|
|
@@ -492,59 +694,40 @@ export const LegendAIChat = (props: LegendAIChatProps): React.ReactNode => {
|
|
|
492
694
|
|
|
493
695
|
const isThinkingVisible =
|
|
494
696
|
msg.isProcessing || state.expandedThinking.has(msgIndex);
|
|
697
|
+
const previousMessage =
|
|
698
|
+
msgIndex > 0 ? state.messages[msgIndex - 1] : null;
|
|
699
|
+
const questionText =
|
|
700
|
+
previousMessage?.role === LegendAIMessageRole.USER
|
|
701
|
+
? previousMessage.text
|
|
702
|
+
: '';
|
|
495
703
|
return (
|
|
496
704
|
<AssistantMessageView
|
|
497
705
|
key={msg.id}
|
|
498
706
|
msg={msg}
|
|
707
|
+
questionText={questionText}
|
|
499
708
|
isThinkingVisible={isThinkingVisible}
|
|
500
709
|
onToggleThinking={(): void => state.toggleThinking(msgIndex)}
|
|
710
|
+
onMessageFeedback={handleMessageFeedback}
|
|
711
|
+
selectedFeedbackRating={feedbackByMessageId.get(msg.id)}
|
|
712
|
+
feedbackSubmitting={pendingFeedbackByMessageId.has(msg.id)}
|
|
713
|
+
{...(config.enghubDocUrl === undefined
|
|
714
|
+
? {}
|
|
715
|
+
: { enghubDocUrl: config.enghubDocUrl })}
|
|
716
|
+
{...(config.enthubRequestAccessUrl === undefined
|
|
717
|
+
? {}
|
|
718
|
+
: { enthubRequestAccessUrl: config.enthubRequestAccessUrl })}
|
|
501
719
|
onFallbackAction={(messageId): void =>
|
|
502
720
|
state.runFallbackAction(messageId)
|
|
503
721
|
}
|
|
504
722
|
onSuggestedQueryClick={(q): void =>
|
|
505
|
-
state.askQuestionWithIntent(
|
|
506
|
-
q,
|
|
507
|
-
services.length > 0
|
|
508
|
-
? LegendAIQuestionIntent.DATA_QUERY
|
|
509
|
-
: LegendAIQuestionIntent.ORCHESTRATOR,
|
|
510
|
-
)
|
|
723
|
+
state.askQuestionWithIntent(q, inferSuggestedQueryIntent(q))
|
|
511
724
|
}
|
|
512
725
|
/>
|
|
513
726
|
);
|
|
514
727
|
})}
|
|
515
728
|
</div>
|
|
516
729
|
|
|
517
|
-
<
|
|
518
|
-
<div className="legend-ai__question-wrapper">
|
|
519
|
-
<textarea
|
|
520
|
-
ref={textareaRef}
|
|
521
|
-
className="legend-ai__question"
|
|
522
|
-
placeholder="Ask anything about the data..."
|
|
523
|
-
rows={1}
|
|
524
|
-
spellCheck={false}
|
|
525
|
-
value={state.questionText}
|
|
526
|
-
onChange={(e): void => state.setQuestionText(e.target.value)}
|
|
527
|
-
onKeyDown={(e): void => {
|
|
528
|
-
if (e.key === 'Enter' && !e.shiftKey) {
|
|
529
|
-
e.preventDefault();
|
|
530
|
-
if (!state.isSending && state.questionText.trim()) {
|
|
531
|
-
state.askQuestion();
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
}}
|
|
535
|
-
/>
|
|
536
|
-
<button
|
|
537
|
-
type="button"
|
|
538
|
-
title="Send"
|
|
539
|
-
aria-label="Send"
|
|
540
|
-
className="legend-ai__send-btn"
|
|
541
|
-
disabled={state.isSending || !state.questionText.trim()}
|
|
542
|
-
onClick={(): void => state.askQuestion()}
|
|
543
|
-
>
|
|
544
|
-
{state.isSending ? <LoadingIcon isLoading={true} /> : <SendIcon />}
|
|
545
|
-
</button>
|
|
546
|
-
</div>
|
|
547
|
-
</div>
|
|
730
|
+
<LegendAIChatInput state={state} scopes={scopes} />
|
|
548
731
|
</div>
|
|
549
732
|
);
|
|
550
733
|
};
|