@finos/legend-application-marketplace 0.2.13 → 0.2.15

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.
Files changed (120) hide show
  1. package/lib/__lib__/LegendMarketplaceNavigation.d.ts +2 -0
  2. package/lib/__lib__/LegendMarketplaceNavigation.d.ts.map +1 -1
  3. package/lib/__lib__/LegendMarketplaceNavigation.js +4 -0
  4. package/lib/__lib__/LegendMarketplaceNavigation.js.map +1 -1
  5. package/lib/application/LegendMarketplaceApplicationConfig.d.ts +9 -0
  6. package/lib/application/LegendMarketplaceApplicationConfig.d.ts.map +1 -1
  7. package/lib/application/LegendMarketplaceApplicationConfig.js +40 -22
  8. package/lib/application/LegendMarketplaceApplicationConfig.js.map +1 -1
  9. package/lib/application/LegendMarketplaceWebApplication.d.ts.map +1 -1
  10. package/lib/application/LegendMarketplaceWebApplication.js +4 -1
  11. package/lib/application/LegendMarketplaceWebApplication.js.map +1 -1
  12. package/lib/application/__test-utils__/LegendMarketplaceApplicationTestUtils.d.ts.map +1 -1
  13. package/lib/application/__test-utils__/LegendMarketplaceApplicationTestUtils.js +7 -0
  14. package/lib/application/__test-utils__/LegendMarketplaceApplicationTestUtils.js.map +1 -1
  15. package/lib/application/providers/LegendMarketplaceAIChatStoreProvider.d.ts +21 -0
  16. package/lib/application/providers/LegendMarketplaceAIChatStoreProvider.d.ts.map +1 -0
  17. package/lib/application/providers/LegendMarketplaceAIChatStoreProvider.js +29 -0
  18. package/lib/application/providers/LegendMarketplaceAIChatStoreProvider.js.map +1 -0
  19. package/lib/index.css +2 -2
  20. package/lib/index.css.map +1 -1
  21. package/lib/package.json +1 -1
  22. package/lib/pages/Agents/LegendMarketplaceAgents.d.ts.map +1 -1
  23. package/lib/pages/Agents/LegendMarketplaceAgents.js +49 -17
  24. package/lib/pages/Agents/LegendMarketplaceAgents.js.map +1 -1
  25. package/lib/pages/Agents/MarketplaceAIChatView.d.ts +21 -0
  26. package/lib/pages/Agents/MarketplaceAIChatView.d.ts.map +1 -0
  27. package/lib/pages/Agents/MarketplaceAIChatView.js +141 -0
  28. package/lib/pages/Agents/MarketplaceAIChatView.js.map +1 -0
  29. package/lib/pages/Agents/MarketplaceAIInputBar.d.ts +22 -0
  30. package/lib/pages/Agents/MarketplaceAIInputBar.d.ts.map +1 -0
  31. package/lib/pages/Agents/MarketplaceAIInputBar.js +40 -0
  32. package/lib/pages/Agents/MarketplaceAIInputBar.js.map +1 -0
  33. package/lib/pages/Agents/MarketplaceAIProductAutosuggest.d.ts +25 -0
  34. package/lib/pages/Agents/MarketplaceAIProductAutosuggest.d.ts.map +1 -0
  35. package/lib/pages/Agents/MarketplaceAIProductAutosuggest.js +86 -0
  36. package/lib/pages/Agents/MarketplaceAIProductAutosuggest.js.map +1 -0
  37. package/lib/pages/Agents/MarketplaceAIProductCards.d.ts +23 -0
  38. package/lib/pages/Agents/MarketplaceAIProductCards.d.ts.map +1 -0
  39. package/lib/pages/Agents/MarketplaceAIProductCards.js +20 -0
  40. package/lib/pages/Agents/MarketplaceAIProductCards.js.map +1 -0
  41. package/lib/pages/Agents/MarketplaceAIScopeSelector.d.ts +19 -0
  42. package/lib/pages/Agents/MarketplaceAIScopeSelector.d.ts.map +1 -0
  43. package/lib/pages/Agents/MarketplaceAIScopeSelector.js +46 -0
  44. package/lib/pages/Agents/MarketplaceAIScopeSelector.js.map +1 -0
  45. package/lib/pages/Lakehouse/entitlements/EntitlementsClosedContractsDashboard.d.ts.map +1 -1
  46. package/lib/pages/Lakehouse/entitlements/EntitlementsClosedContractsDashboard.js +74 -63
  47. package/lib/pages/Lakehouse/entitlements/EntitlementsClosedContractsDashboard.js.map +1 -1
  48. package/lib/pages/Lakehouse/entitlements/EntitlementsPendingContractsDashboard.d.ts.map +1 -1
  49. package/lib/pages/Lakehouse/entitlements/EntitlementsPendingContractsDashboard.js +85 -69
  50. package/lib/pages/Lakehouse/entitlements/EntitlementsPendingContractsDashboard.js.map +1 -1
  51. package/lib/pages/Lakehouse/entitlements/EntitlementsPendingTasksDashboard.d.ts.map +1 -1
  52. package/lib/pages/Lakehouse/entitlements/EntitlementsPendingTasksDashboard.js +7 -11
  53. package/lib/pages/Lakehouse/entitlements/EntitlementsPendingTasksDashboard.js.map +1 -1
  54. package/lib/pages/Lakehouse/entitlements/PermitDataAccessRequest.d.ts +17 -0
  55. package/lib/pages/Lakehouse/entitlements/PermitDataAccessRequest.d.ts.map +1 -0
  56. package/lib/pages/Lakehouse/entitlements/PermitDataAccessRequest.js +126 -0
  57. package/lib/pages/Lakehouse/entitlements/PermitDataAccessRequest.js.map +1 -0
  58. package/lib/pages/Lakehouse/entitlements/WorkflowDataAccessRequest.d.ts.map +1 -1
  59. package/lib/pages/Lakehouse/entitlements/WorkflowDataAccessRequest.js +23 -65
  60. package/lib/pages/Lakehouse/entitlements/WorkflowDataAccessRequest.js.map +1 -1
  61. package/lib/pages/Lakehouse/entitlements/showTaskActionAlert.d.ts +29 -0
  62. package/lib/pages/Lakehouse/entitlements/showTaskActionAlert.d.ts.map +1 -0
  63. package/lib/pages/Lakehouse/entitlements/showTaskActionAlert.js +60 -0
  64. package/lib/pages/Lakehouse/entitlements/showTaskActionAlert.js.map +1 -0
  65. package/lib/pages/Lakehouse/searchResults/LegendMarketplaceSearchResults.d.ts.map +1 -1
  66. package/lib/pages/Lakehouse/searchResults/LegendMarketplaceSearchResults.js +3 -8
  67. package/lib/pages/Lakehouse/searchResults/LegendMarketplaceSearchResults.js.map +1 -1
  68. package/lib/stores/LegendMarketplaceBaseStore.d.ts +2 -1
  69. package/lib/stores/LegendMarketplaceBaseStore.d.ts.map +1 -1
  70. package/lib/stores/LegendMarketplaceBaseStore.js +8 -1
  71. package/lib/stores/LegendMarketplaceBaseStore.js.map +1 -1
  72. package/lib/stores/ai/LegendMarketplaceAIChatStore.d.ts +109 -0
  73. package/lib/stores/ai/LegendMarketplaceAIChatStore.d.ts.map +1 -0
  74. package/lib/stores/ai/LegendMarketplaceAIChatStore.js +1106 -0
  75. package/lib/stores/ai/LegendMarketplaceAIChatStore.js.map +1 -0
  76. package/lib/stores/lakehouse/LegendMarketplaceProductViewerStore.d.ts.map +1 -1
  77. package/lib/stores/lakehouse/LegendMarketplaceProductViewerStore.js +3 -2
  78. package/lib/stores/lakehouse/LegendMarketplaceProductViewerStore.js.map +1 -1
  79. package/lib/stores/lakehouse/dataProducts/ProductCardState.d.ts +1 -1
  80. package/lib/stores/lakehouse/dataProducts/ProductCardState.d.ts.map +1 -1
  81. package/lib/stores/lakehouse/dataProducts/ProductCardState.js +1 -2
  82. package/lib/stores/lakehouse/dataProducts/ProductCardState.js.map +1 -1
  83. package/lib/stores/lakehouse/entitlements/EntitlementsDashboardState.d.ts +30 -3
  84. package/lib/stores/lakehouse/entitlements/EntitlementsDashboardState.d.ts.map +1 -1
  85. package/lib/stores/lakehouse/entitlements/EntitlementsDashboardState.js +94 -16
  86. package/lib/stores/lakehouse/entitlements/EntitlementsDashboardState.js.map +1 -1
  87. package/lib/utils/EntitlementsUtils.d.ts +36 -2
  88. package/lib/utils/EntitlementsUtils.d.ts.map +1 -1
  89. package/lib/utils/EntitlementsUtils.js +225 -46
  90. package/lib/utils/EntitlementsUtils.js.map +1 -1
  91. package/lib/utils/SearchUtils.d.ts.map +1 -1
  92. package/lib/utils/SearchUtils.js +7 -4
  93. package/lib/utils/SearchUtils.js.map +1 -1
  94. package/package.json +10 -10
  95. package/src/__lib__/LegendMarketplaceNavigation.ts +11 -0
  96. package/src/application/LegendMarketplaceApplicationConfig.ts +62 -24
  97. package/src/application/LegendMarketplaceWebApplication.tsx +15 -0
  98. package/src/application/__test-utils__/LegendMarketplaceApplicationTestUtils.ts +7 -0
  99. package/src/application/providers/LegendMarketplaceAIChatStoreProvider.tsx +47 -0
  100. package/src/pages/Agents/LegendMarketplaceAgents.tsx +145 -23
  101. package/src/pages/Agents/MarketplaceAIChatView.tsx +555 -0
  102. package/src/pages/Agents/MarketplaceAIInputBar.tsx +91 -0
  103. package/src/pages/Agents/MarketplaceAIProductAutosuggest.tsx +181 -0
  104. package/src/pages/Agents/MarketplaceAIProductCards.tsx +111 -0
  105. package/src/pages/Agents/MarketplaceAIScopeSelector.tsx +84 -0
  106. package/src/pages/Lakehouse/entitlements/EntitlementsClosedContractsDashboard.tsx +131 -136
  107. package/src/pages/Lakehouse/entitlements/EntitlementsPendingContractsDashboard.tsx +158 -165
  108. package/src/pages/Lakehouse/entitlements/EntitlementsPendingTasksDashboard.tsx +12 -17
  109. package/src/pages/Lakehouse/entitlements/PermitDataAccessRequest.tsx +245 -0
  110. package/src/pages/Lakehouse/entitlements/WorkflowDataAccessRequest.tsx +25 -94
  111. package/src/pages/Lakehouse/entitlements/showTaskActionAlert.tsx +101 -0
  112. package/src/pages/Lakehouse/searchResults/LegendMarketplaceSearchResults.tsx +27 -31
  113. package/src/stores/LegendMarketplaceBaseStore.ts +12 -0
  114. package/src/stores/ai/LegendMarketplaceAIChatStore.ts +1720 -0
  115. package/src/stores/lakehouse/LegendMarketplaceProductViewerStore.ts +6 -0
  116. package/src/stores/lakehouse/dataProducts/ProductCardState.ts +3 -4
  117. package/src/stores/lakehouse/entitlements/EntitlementsDashboardState.ts +181 -48
  118. package/src/utils/EntitlementsUtils.tsx +341 -86
  119. package/src/utils/SearchUtils.tsx +7 -4
  120. package/tsconfig.json +9 -0
@@ -0,0 +1,555 @@
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 { useRef, useEffect, useCallback, useState } from 'react';
18
+ import { observer } from 'mobx-react-lite';
19
+ import { flowResult } from 'mobx';
20
+ import {
21
+ LoadingIcon,
22
+ SparkleStarsIcon,
23
+ CodeIcon,
24
+ TableIcon,
25
+ CopyIcon,
26
+ RefreshIcon,
27
+ TimesIcon,
28
+ CheckIcon,
29
+ CaretDownIcon,
30
+ CaretRightIcon,
31
+ DotIcon,
32
+ MarkdownTextViewer,
33
+ ExternalLinkIcon,
34
+ } from '@finos/legend-art';
35
+ import { noop } from '@finos/legend-shared';
36
+ import {
37
+ type LegendAIAssistantMessage,
38
+ LegendAIMessageRole,
39
+ LegendAIErrorType,
40
+ LegendAIResultGrid,
41
+ LegendAIAnalysisPanel,
42
+ renderStepStatusIcon,
43
+ LAKEHOUSE_ENV_PROD,
44
+ COVERAGE_NAME_PROD,
45
+ COVERAGE_NAME_SANDBOX,
46
+ } from '@finos/legend-lego/legend-ai';
47
+ import { useLegendMarketplaceAIChatStore } from '../../application/providers/LegendMarketplaceAIChatStoreProvider.js';
48
+ import { MarketplaceAIChatStage } from '../../stores/ai/LegendMarketplaceAIChatStore.js';
49
+ import { MarketplaceAIProductCards } from './MarketplaceAIProductCards.js';
50
+ import { MarketplaceAIProductAutosuggest } from './MarketplaceAIProductAutosuggest.js';
51
+ import { MarketplaceAIInputBar } from './MarketplaceAIInputBar.js';
52
+
53
+ const COPY_FEEDBACK_DURATION_MS = 2000;
54
+
55
+ const AISummaryRenderer = ({ value }: { value: string }): React.ReactNode => (
56
+ <MarkdownTextViewer value={{ value }} className="legend-ai__text-answer-md" />
57
+ );
58
+
59
+ const AssistantMessageView = observer(
60
+ (props: {
61
+ msg: LegendAIAssistantMessage;
62
+ onSuggestedQueryClick?: (query: string) => void;
63
+ onFallbackAction?: (messageId: string, actionId: string) => void;
64
+ }): React.ReactNode => {
65
+ const { msg, onSuggestedQueryClick, onFallbackAction } = props;
66
+ const store = useLegendMarketplaceAIChatStore();
67
+ const { enghubDocUrl, enthubRequestAccessUrl, lakehouseEnvironment } =
68
+ store.config;
69
+ const isProd = lakehouseEnvironment === LAKEHOUSE_ENV_PROD;
70
+ const hasAccessLinks =
71
+ enghubDocUrl !== undefined || enthubRequestAccessUrl !== undefined;
72
+ const [isThinkingVisible, setIsThinkingVisible] = useState(
73
+ msg.isProcessing,
74
+ );
75
+ const [sqlCopied, setSqlCopied] = useState(false);
76
+ const copyTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(
77
+ undefined,
78
+ );
79
+
80
+ useEffect(
81
+ () => () => {
82
+ if (copyTimerRef.current !== undefined) {
83
+ clearTimeout(copyTimerRef.current);
84
+ }
85
+ },
86
+ [],
87
+ );
88
+
89
+ useEffect(() => {
90
+ setIsThinkingVisible(msg.isProcessing);
91
+ }, [msg.isProcessing]);
92
+
93
+ const handleCopySql = useCallback(() => {
94
+ if (msg.sql) {
95
+ navigator.clipboard.writeText(msg.sql).catch(noop());
96
+ setSqlCopied(true);
97
+ if (copyTimerRef.current !== undefined) {
98
+ clearTimeout(copyTimerRef.current);
99
+ }
100
+ copyTimerRef.current = setTimeout(() => {
101
+ setSqlCopied(false);
102
+ copyTimerRef.current = undefined;
103
+ }, COPY_FEEDBACK_DURATION_MS);
104
+ }
105
+ }, [msg.sql]);
106
+
107
+ return (
108
+ <div className="legend-ai__msg legend-ai__msg--assistant">
109
+ <div className="legend-ai__msg-avatar">
110
+ <SparkleStarsIcon />
111
+ </div>
112
+ <div className="legend-ai__msg-content">
113
+ {msg.thinkingSteps.length > 0 && (
114
+ <div className="legend-ai__thinking">
115
+ {!msg.isProcessing && (
116
+ <button
117
+ type="button"
118
+ className="legend-ai__thinking-toggle"
119
+ onClick={(): void => setIsThinkingVisible(!isThinkingVisible)}
120
+ >
121
+ <span className="legend-ai__thinking-toggle-icon">
122
+ {isThinkingVisible ? <CaretDownIcon /> : <CaretRightIcon />}
123
+ </span>
124
+ Thought for {msg.thinkingDuration ?? '...'}s
125
+ </button>
126
+ )}
127
+ {isThinkingVisible && (
128
+ <div className="legend-ai__thinking-steps">
129
+ {msg.thinkingSteps.map((step) => (
130
+ <div
131
+ key={step.id}
132
+ className={`legend-ai__thinking-step legend-ai__thinking-step--${step.status}`}
133
+ >
134
+ <span className="legend-ai__thinking-step-icon">
135
+ {renderStepStatusIcon(step.status)}
136
+ </span>
137
+ <span>{step.label}</span>
138
+ </div>
139
+ ))}
140
+ </div>
141
+ )}
142
+ </div>
143
+ )}
144
+
145
+ {msg.dataContext && (
146
+ <div className="legend-ai__data-context">
147
+ <MarkdownTextViewer
148
+ value={{ value: msg.dataContext }}
149
+ className="legend-ai__text-answer-md"
150
+ />
151
+ </div>
152
+ )}
153
+
154
+ {msg.sql && (
155
+ <div className="legend-ai__sql-block">
156
+ <div className="legend-ai__sql-block-header">
157
+ <span className="legend-ai__sql-block-header-icon">
158
+ <CodeIcon />
159
+ </span>
160
+ <span>Generated Query</span>
161
+ {msg.sqlGenTime && (
162
+ <span className="legend-ai__sql-block-time">
163
+ {msg.sqlGenTime}s
164
+ </span>
165
+ )}
166
+ <button
167
+ type="button"
168
+ className="legend-ai__sql-copy-btn"
169
+ title="Copy query"
170
+ aria-label="Copy query"
171
+ onClick={handleCopySql}
172
+ >
173
+ {sqlCopied ? (
174
+ <span className="legend-ai__sql-copy-btn--copied">
175
+ <CheckIcon />
176
+ </span>
177
+ ) : (
178
+ <CopyIcon />
179
+ )}
180
+ </button>
181
+ </div>
182
+ <div className="legend-ai__sql-scroll">
183
+ <pre className="legend-ai__sql-display">{msg.sql}</pre>
184
+ </div>
185
+ </div>
186
+ )}
187
+
188
+ {msg.isExecuting && (
189
+ <div className="legend-ai__executing">
190
+ <LoadingIcon isLoading={true} />
191
+ <span>Executing query...</span>
192
+ </div>
193
+ )}
194
+
195
+ {msg.textAnswer && !msg.gridData && (
196
+ <div className="legend-ai__text-answer">
197
+ <MarkdownTextViewer
198
+ value={{ value: msg.textAnswer }}
199
+ className="legend-ai__text-answer-md"
200
+ />
201
+ </div>
202
+ )}
203
+
204
+ {msg.error && (
205
+ <div className="legend-ai__exec-error">
206
+ {msg.error}
207
+ {msg.errorType === LegendAIErrorType.PERMISSION &&
208
+ hasAccessLinks && (
209
+ <div className="legend-ai__permission-error-action">
210
+ <span className="legend-ai__permission-error-note">
211
+ Select coverage:{' '}
212
+ <strong>
213
+ {isProd ? COVERAGE_NAME_PROD : COVERAGE_NAME_SANDBOX}
214
+ </strong>
215
+ </span>
216
+ <div className="legend-ai__permission-error-btns">
217
+ {enghubDocUrl && (
218
+ <a
219
+ className="legend-ai__permission-error-btn"
220
+ href={enghubDocUrl}
221
+ target="_blank"
222
+ rel="noopener noreferrer"
223
+ >
224
+ <ExternalLinkIcon />
225
+ <span>View Documentation</span>
226
+ </a>
227
+ )}
228
+ {enthubRequestAccessUrl && (
229
+ <a
230
+ className="legend-ai__permission-error-btn legend-ai__permission-error-btn--primary"
231
+ href={enthubRequestAccessUrl}
232
+ target="_blank"
233
+ rel="noopener noreferrer"
234
+ >
235
+ <ExternalLinkIcon />
236
+ <span>Request Access</span>
237
+ </a>
238
+ )}
239
+ </div>
240
+ </div>
241
+ )}
242
+ {msg.errorType === LegendAIErrorType.NETWORK && (
243
+ <div className="legend-ai__permission-error-action">
244
+ <span className="legend-ai__permission-error-note">
245
+ Please check your network connection and try again.
246
+ </span>
247
+ </div>
248
+ )}
249
+ </div>
250
+ )}
251
+
252
+ {msg.fallbackAction && !msg.isProcessing && onFallbackAction && (
253
+ <button
254
+ type="button"
255
+ className="legend-ai__fallback-action-btn"
256
+ onClick={(): void => {
257
+ const actionId = msg.fallbackAction?.actionId;
258
+ if (actionId) {
259
+ onFallbackAction(msg.id, actionId);
260
+ }
261
+ }}
262
+ >
263
+ <SparkleStarsIcon />
264
+ <span>{msg.fallbackAction.label}</span>
265
+ </button>
266
+ )}
267
+
268
+ {msg.gridData && (
269
+ <div className="legend-ai__results-block">
270
+ <div className="legend-ai__results-header">
271
+ <span className="legend-ai__results-header-icon">
272
+ <TableIcon />
273
+ </span>
274
+ <span>Results</span>
275
+ <span className="legend-ai__results-meta">
276
+ {msg.gridData.rowData.length} row
277
+ {msg.gridData.rowData.length === 1 ? '' : 's'}
278
+ {msg.execTime ? (
279
+ <>
280
+ {' '}
281
+ <DotIcon className="legend-ai__results-meta-dot" />{' '}
282
+ {msg.execTime}s
283
+ </>
284
+ ) : (
285
+ ''
286
+ )}
287
+ </span>
288
+ </div>
289
+ <LegendAIResultGrid data={msg.gridData} />
290
+ </div>
291
+ )}
292
+
293
+ {msg.textAnswer && msg.gridData && (
294
+ <LegendAIAnalysisPanel
295
+ gridData={msg.gridData}
296
+ summary={msg.textAnswer}
297
+ SummaryRenderer={AISummaryRenderer}
298
+ />
299
+ )}
300
+
301
+ {msg.isProcessing && !msg.isExecuting && msg.gridData && (
302
+ <div className="legend-ai__analyzing">
303
+ <LoadingIcon isLoading={true} />
304
+ <span>Analyzing results...</span>
305
+ </div>
306
+ )}
307
+
308
+ {!msg.isProcessing &&
309
+ msg.suggestedQueries.length > 0 &&
310
+ onSuggestedQueryClick && (
311
+ <div className="legend-ai__follow-up-suggestions">
312
+ <span className="legend-ai__follow-up-label">
313
+ Follow-up questions:
314
+ </span>
315
+ {msg.suggestedQueries.map((q) => (
316
+ <button
317
+ key={q}
318
+ type="button"
319
+ className="legend-ai__follow-up-btn"
320
+ onClick={(): void => onSuggestedQueryClick(q)}
321
+ >
322
+ {q}
323
+ </button>
324
+ ))}
325
+ </div>
326
+ )}
327
+ </div>
328
+ </div>
329
+ );
330
+ },
331
+ );
332
+
333
+ export const MarketplaceAIChatView = observer(
334
+ (props: { initialQuery?: string }): React.ReactNode => {
335
+ const { initialQuery } = props;
336
+ const store = useLegendMarketplaceAIChatStore();
337
+ const conversationRef = useRef<HTMLDivElement>(null);
338
+ const hasMessages = store.messages.length > 0;
339
+ const initialQuerySubmitted = useRef(false);
340
+
341
+ useEffect(() => {
342
+ const el = conversationRef.current;
343
+ if (el) {
344
+ el.scrollTop = el.scrollHeight;
345
+ }
346
+ }, [store.messages.length]);
347
+
348
+ const dispatchQuery = useCallback(
349
+ (text: string): void => {
350
+ if (store.selectedProduct) {
351
+ flowResult(store.askFollowUp(text)).catch(noop());
352
+ } else {
353
+ flowResult(store.submitQuery(text)).catch(noop());
354
+ }
355
+ },
356
+ [store],
357
+ );
358
+
359
+ useEffect(() => {
360
+ if (
361
+ initialQuery &&
362
+ initialQuery.trim().length > 0 &&
363
+ !initialQuerySubmitted.current &&
364
+ store.isEnabled
365
+ ) {
366
+ initialQuerySubmitted.current = true;
367
+ store.setQuestionText(initialQuery);
368
+ flowResult(store.submitQuery(initialQuery)).catch(noop());
369
+ }
370
+ }, [initialQuery, store]);
371
+
372
+ const handleSubmit = useCallback((): void => {
373
+ if (!store.questionText.trim() || store.isSending) {
374
+ return;
375
+ }
376
+ dispatchQuery(store.questionText);
377
+ }, [store, dispatchQuery]);
378
+
379
+ const handleFallbackAction = useCallback(
380
+ (messageId: string, _actionId: string): void => {
381
+ flowResult(store.runOrchestratorFallback(messageId)).catch(noop());
382
+ },
383
+ [store],
384
+ );
385
+
386
+ const handleSuggestedQueryClick = useCallback(
387
+ (query: string): void => {
388
+ store.setQuestionText(query);
389
+ dispatchQuery(query);
390
+ },
391
+ [store, dispatchQuery],
392
+ );
393
+
394
+ if (!store.isEnabled) {
395
+ return (
396
+ <div className="marketplace-ai-chat marketplace-ai-chat--disabled">
397
+ <div className="marketplace-ai-chat__empty">
398
+ <div className="marketplace-ai-chat__empty-text">
399
+ Legend AI is not configured. Please contact your administrator.
400
+ </div>
401
+ </div>
402
+ </div>
403
+ );
404
+ }
405
+
406
+ return (
407
+ <div className="marketplace-ai-chat">
408
+ {hasMessages ? (
409
+ <>
410
+ <div className="marketplace-ai-chat__header">
411
+ {store.selectedProduct && (
412
+ <div className="marketplace-ai-chat__product-pill">
413
+ <SparkleStarsIcon />
414
+ <span className="marketplace-ai-chat__product-pill-text">
415
+ Scoped to:{' '}
416
+ <strong>
417
+ {store.selectedProduct.dataProductTitle ?? 'Data Product'}
418
+ </strong>
419
+ </span>
420
+ <button
421
+ type="button"
422
+ className="marketplace-ai-chat__product-pill-dismiss"
423
+ title="Remove product scope"
424
+ aria-label="Remove product scope"
425
+ onClick={(): void => {
426
+ store.deselectProduct();
427
+ }}
428
+ >
429
+ <TimesIcon />
430
+ </button>
431
+ </div>
432
+ )}
433
+ <button
434
+ type="button"
435
+ className="marketplace-ai-chat__clear-btn"
436
+ title="Clear chat"
437
+ aria-label="Clear chat"
438
+ onClick={(): void => store.clearChat()}
439
+ >
440
+ <RefreshIcon />
441
+ <span>Clear chat</span>
442
+ </button>
443
+ </div>
444
+ <div
445
+ className="marketplace-ai-chat__messages"
446
+ ref={conversationRef}
447
+ >
448
+ {store.messages.map((msg) => {
449
+ if (msg.role === LegendAIMessageRole.USER) {
450
+ return (
451
+ <div
452
+ key={msg.id}
453
+ className="marketplace-ai-chat__msg marketplace-ai-chat__msg--user"
454
+ >
455
+ <div className="marketplace-ai-chat__msg-bubble">
456
+ {msg.text}
457
+ </div>
458
+ </div>
459
+ );
460
+ }
461
+
462
+ return (
463
+ <AssistantMessageView
464
+ key={msg.id}
465
+ msg={msg}
466
+ onSuggestedQueryClick={handleSuggestedQueryClick}
467
+ onFallbackAction={handleFallbackAction}
468
+ />
469
+ );
470
+ })}
471
+
472
+ {store.stage === MarketplaceAIChatStage.PRODUCT_SELECTION &&
473
+ store.suggestedProducts.length > 0 && (
474
+ <MarketplaceAIProductCards
475
+ products={store.suggestedProducts}
476
+ {...(store.scoredCandidates.length > 0
477
+ ? {
478
+ scoredCandidates: store.scoredCandidates,
479
+ }
480
+ : {})}
481
+ onSelect={(product): void => {
482
+ store.selectDataProduct(product);
483
+ dispatchQuery(store.lastUserMessageText);
484
+ }}
485
+ />
486
+ )}
487
+
488
+ {store.stage === MarketplaceAIChatStage.PRODUCT_SELECTION && (
489
+ <div className="marketplace-ai-chat__product-search">
490
+ <div className="marketplace-ai-chat__product-search-label">
491
+ Don&apos;t see the right product? Search for it:
492
+ </div>
493
+ <MarketplaceAIProductAutosuggest
494
+ onSelect={(result): void => {
495
+ store.selectAutosuggestProduct(result);
496
+ dispatchQuery(store.lastUserMessageText);
497
+ }}
498
+ className="marketplace-ai-chat__product-search-autosuggest"
499
+ />
500
+ </div>
501
+ )}
502
+ </div>
503
+
504
+ <div className="marketplace-ai-chat__input-bar">
505
+ <MarketplaceAIInputBar
506
+ placeholder={
507
+ store.selectedProduct || store.scopeProducts.length > 0
508
+ ? `Ask about ${store.selectedProduct?.dataProductTitle ?? store.scopeProducts[0]?.name ?? 'this data product'}...`
509
+ : 'Ask a follow-up...'
510
+ }
511
+ onSubmit={handleSubmit}
512
+ />
513
+ </div>
514
+ </>
515
+ ) : (
516
+ <div className="marketplace-ai-chat__welcome">
517
+ <div className="marketplace-ai-chat__welcome-spacer" />
518
+ <div className="marketplace-ai-chat__welcome-icon">
519
+ <SparkleStarsIcon />
520
+ </div>
521
+ <h1 className="marketplace-ai-chat__welcome-title">
522
+ Legend Marketplace AI
523
+ </h1>
524
+ <p className="marketplace-ai-chat__welcome-subtitle">
525
+ Ask anything about your data. I&apos;ll find the right data
526
+ product and query it for you.
527
+ </p>
528
+ <div className="marketplace-ai-chat__welcome-input">
529
+ <MarketplaceAIInputBar
530
+ placeholder="Ask anything about your data..."
531
+ onSubmit={handleSubmit}
532
+ />
533
+ </div>
534
+ <div className="marketplace-ai-chat__suggestions">
535
+ {store.welcomeSuggestedQueries.map((q) => (
536
+ <button
537
+ key={q}
538
+ type="button"
539
+ className="marketplace-ai-chat__suggestion"
540
+ onClick={(): void => {
541
+ store.setQuestionText(q);
542
+ dispatchQuery(q);
543
+ }}
544
+ >
545
+ {q}
546
+ </button>
547
+ ))}
548
+ </div>
549
+ <div className="marketplace-ai-chat__welcome-spacer-bottom" />
550
+ </div>
551
+ )}
552
+ </div>
553
+ );
554
+ },
555
+ );
@@ -0,0 +1,91 @@
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 { useRef, useLayoutEffect } from 'react';
18
+ import { observer } from 'mobx-react-lite';
19
+ import { SendIcon, LoadingIcon, TimesIcon } from '@finos/legend-art';
20
+ import { useLegendMarketplaceAIChatStore } from '../../application/providers/LegendMarketplaceAIChatStoreProvider.js';
21
+ import { MarketplaceAIScopeSelector } from './MarketplaceAIScopeSelector.js';
22
+
23
+ export const MarketplaceAIInputBar = observer(
24
+ (props: { placeholder: string; onSubmit: () => void }): React.ReactNode => {
25
+ const { placeholder, onSubmit } = props;
26
+ const store = useLegendMarketplaceAIChatStore();
27
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
28
+
29
+ useLayoutEffect(() => {
30
+ const el = textareaRef.current;
31
+ if (el) {
32
+ el.style.height = 'auto';
33
+ el.style.height = `${el.scrollHeight}px`;
34
+ }
35
+ }, [store.questionText]);
36
+
37
+ return (
38
+ <>
39
+ {store.scopeProducts.length > 0 && (
40
+ <div className="ai-scope-selector__pills">
41
+ {store.scopeProducts.map((sp, idx) => (
42
+ <div
43
+ key={`${sp.coordinates.group_id}:${sp.coordinates.artifact_id}:${sp.coordinates.version}`}
44
+ className="ai-scope-selector__pill"
45
+ >
46
+ <span className="ai-scope-selector__pill-text">{sp.name}</span>
47
+ <button
48
+ type="button"
49
+ className="ai-scope-selector__pill-dismiss"
50
+ title="Remove"
51
+ aria-label={`Remove ${sp.name}`}
52
+ onClick={(): void => store.removeScopeProduct(idx)}
53
+ >
54
+ <TimesIcon />
55
+ </button>
56
+ </div>
57
+ ))}
58
+ </div>
59
+ )}
60
+ <div className="marketplace-ai-chat__input-row">
61
+ <MarketplaceAIScopeSelector />
62
+ <textarea
63
+ ref={textareaRef}
64
+ className="marketplace-ai-chat__textarea"
65
+ placeholder={placeholder}
66
+ rows={1}
67
+ spellCheck={false}
68
+ value={store.questionText}
69
+ onChange={(e): void => store.setQuestionText(e.target.value)}
70
+ onKeyDown={(e): void => {
71
+ if (e.key === 'Enter' && !e.shiftKey) {
72
+ e.preventDefault();
73
+ onSubmit();
74
+ }
75
+ }}
76
+ />
77
+ <button
78
+ type="button"
79
+ title="Send"
80
+ aria-label="Send"
81
+ className="marketplace-ai-chat__send-btn"
82
+ disabled={store.isSending || !store.questionText.trim()}
83
+ onClick={onSubmit}
84
+ >
85
+ {store.isSending ? <LoadingIcon isLoading={true} /> : <SendIcon />}
86
+ </button>
87
+ </div>
88
+ </>
89
+ );
90
+ },
91
+ );