/** * Copyright (c) 2026-present, Goldman Sachs * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { useRef, useEffect, useCallback, useState } from 'react'; import { observer } from 'mobx-react-lite'; import { flowResult } from 'mobx'; import { LoadingIcon, SparkleStarsIcon, CodeIcon, TableIcon, CopyIcon, RefreshIcon, TimesIcon, CheckIcon, CaretDownIcon, CaretRightIcon, DotIcon, MarkdownTextViewer, ExternalLinkIcon, JupyterIcon, } from '@finos/legend-art'; import { noop } from '@finos/legend-shared'; import { type LegendAIAssistantMessage, LegendAIMessageRole, LegendAIErrorType, LegendAIPythonCodeStatus, LegendAIResultGrid, LegendAIAnalysisPanel, renderStepStatusIcon, LAKEHOUSE_ENV_PROD, COVERAGE_NAME_PROD, COVERAGE_NAME_SANDBOX, } from '@finos/legend-lego/legend-ai'; import { useAuth } from 'react-oidc-context'; import { useLegendMarketplaceAIChatStore } from '../../application/providers/LegendMarketplaceAIChatStoreProvider.js'; import { MarketplaceAIChatStage } from '../../stores/ai/LegendMarketplaceAIChatStore.js'; import { MarketplaceAIProductCards } from './MarketplaceAIProductCards.js'; import { MarketplaceAIProductAutosuggest } from './MarketplaceAIProductAutosuggest.js'; import { MarketplaceAIInputBar } from './MarketplaceAIInputBar.js'; const COPY_FEEDBACK_DURATION_MS = 2000; const AISummaryRenderer = ({ value }: { value: string }): React.ReactNode => ( ); // Copies text to the clipboard and flags the button as copied for a moment. const useCopyFeedback = (): [boolean, (text: string) => void] => { const [copied, setCopied] = useState(false); const timerRef = useRef | undefined>(undefined); useEffect( () => () => { if (timerRef.current !== undefined) { clearTimeout(timerRef.current); } }, [], ); const copy = useCallback((text: string) => { navigator.clipboard .writeText(text) .then(() => { setCopied(true); if (timerRef.current !== undefined) { clearTimeout(timerRef.current); } timerRef.current = setTimeout(() => { setCopied(false); timerRef.current = undefined; }, COPY_FEEDBACK_DURATION_MS); }) .catch(noop()); }, []); return [copied, copy]; }; const AssistantMessageView = observer( (props: { msg: LegendAIAssistantMessage; onSuggestedQueryClick?: (query: string) => void; onFallbackAction?: (messageId: string) => void; }): React.ReactNode => { const { msg, onSuggestedQueryClick, onFallbackAction } = props; const store = useLegendMarketplaceAIChatStore(); const { enghubDocUrl, enthubRequestAccessUrl, lakehouseEnvironment } = store.config; const isProd = lakehouseEnvironment === LAKEHOUSE_ENV_PROD; const hasAccessLinks = enghubDocUrl !== undefined || enthubRequestAccessUrl !== undefined; const [isThinkingVisible, setIsThinkingVisible] = useState( msg.isProcessing, ); const [sqlCopied, copySql] = useCopyFeedback(); const [pythonCopied, copyPython] = useCopyFeedback(); const [isPythonOpen, setIsPythonOpen] = useState(false); const [isOpeningDataCube, setIsOpeningDataCube] = useState(false); const pythonEntry = store.pythonCodeByMessageId.get(msg.id); useEffect(() => { setIsThinkingVisible(msg.isProcessing); }, [msg.isProcessing]); const handleCopySql = useCallback(() => { if (msg.sql) { copySql(msg.sql); store.logCopySql(); } }, [msg.sql, store, copySql]); const handleTogglePython = useCallback(() => { const opening = !isPythonOpen; setIsPythonOpen(opening); const status = store.pythonCodeByMessageId.get(msg.id)?.status; if ( opening && status !== LegendAIPythonCodeStatus.LOADING && status !== LegendAIPythonCodeStatus.READY ) { flowResult(store.generatePythonCode(msg.id)).catch(noop()); } }, [isPythonOpen, store, msg.id]); const handleCopyPython = useCallback(() => { const entry = store.pythonCodeByMessageId.get(msg.id); if (entry?.status === LegendAIPythonCodeStatus.READY && entry.code) { copyPython(entry.code); store.logCopyPython(); } }, [store, msg.id, copyPython]); const handleOpenInDataCube = useCallback(() => { setIsOpeningDataCube(true); flowResult(store.openInDataCube(msg.id)) .catch(noop()) .finally(() => setIsOpeningDataCube(false)); }, [store, msg.id]); return (
{msg.thinkingSteps.length > 0 && (
{!msg.isProcessing && ( )} {isThinkingVisible && (
{msg.thinkingSteps.map((step) => (
{renderStepStatusIcon(step.status)} {step.label}
))}
)}
)} {msg.dataContext && (
)} {msg.sql && (
{msg.gridData === null ? 'Show the query I tried' : 'Query'}
Generated Query {msg.sqlGenTime && ( {msg.sqlGenTime}s )}
{msg.sql}
)} {msg.isExecuting && (
Executing query...
)} {msg.textAnswer && !msg.gridData && (
)} {msg.error && (
{msg.error} {msg.errorType === LegendAIErrorType.PERMISSION && hasAccessLinks && (
Select coverage:{' '} {isProd ? COVERAGE_NAME_PROD : COVERAGE_NAME_SANDBOX}
{enghubDocUrl && ( View Documentation )} {enthubRequestAccessUrl && ( Request Access )}
)} {msg.errorType === LegendAIErrorType.NETWORK && (
Please check your network connection and try again.
)}
)} {msg.fallbackAction && !msg.isProcessing && onFallbackAction && ( )} {msg.gridData && (
Results {msg.gridData.rowData.length} row {msg.gridData.rowData.length === 1 ? '' : 's'} {msg.execTime ? ( <> {' '} {' '} {msg.execTime}s ) : ( '' )}
)} {msg.textAnswer && msg.gridData && ( )} {msg.isProcessing && !msg.isExecuting && msg.gridData && (
Analyzing results...
)} {msg.sql && !msg.isProcessing && msg.gridData !== null && (store.supportsDataCube || store.supportsPython) && (
{store.supportsDataCube && ( )} {store.supportsPython && ( )}
)} {store.supportsPython && isPythonOpen && (
{pythonEntry?.status === LegendAIPythonCodeStatus.LOADING && (
Generating Python code...
)} {pythonEntry?.status === LegendAIPythonCodeStatus.ERROR && (
{pythonEntry.error}
)} {pythonEntry?.status === LegendAIPythonCodeStatus.READY && ( <>
Python
                      {pythonEntry.code}
                    
{pythonEntry.notebookUrl !== undefined && ( )} )}
)} {!msg.isProcessing && msg.suggestedQueries.length > 0 && onSuggestedQueryClick && (
Follow-up questions: {msg.suggestedQueries.map((q) => ( ))}
)}
); }, ); export const MarketplaceAIChatView = observer( (props: { initialQuery?: string }): React.ReactNode => { const { initialQuery } = props; const store = useLegendMarketplaceAIChatStore(); const auth = useAuth(); const tokenRef = useRef(auth.user?.access_token); tokenRef.current = auth.user?.access_token; const conversationRef = useRef(null); const hasMessages = store.messages.length > 0; const initialQuerySubmitted = useRef(false); const lastMessage = store.messages.at(-1); const lastMessageStepCount = lastMessage?.role === LegendAIMessageRole.ASSISTANT ? lastMessage.thinkingSteps.length : 0; const isLastMessageProcessing = lastMessage?.role === LegendAIMessageRole.ASSISTANT && lastMessage.isProcessing; useEffect(() => { store.setTokenProvider(() => tokenRef.current); return (): void => store.setTokenProvider(() => undefined); }, [store]); useEffect(() => { const el = conversationRef.current; if (el) { el.scrollTop = el.scrollHeight; } }, [store.messages.length, lastMessageStepCount, isLastMessageProcessing]); const dispatchQuery = useCallback( (text: string): void => { if (store.selectedProduct) { flowResult(store.askFollowUp(text)).catch(noop()); } else { flowResult(store.submitQuery(text)).catch(noop()); } }, [store], ); useEffect(() => { if ( initialQuery && initialQuery.trim().length > 0 && !initialQuerySubmitted.current && store.isEnabled ) { initialQuerySubmitted.current = true; store.setQuestionText(initialQuery); flowResult(store.submitQuery(initialQuery)).catch(noop()); } }, [initialQuery, store, store.isEnabled]); const handleSubmit = useCallback((): void => { if (!store.questionText.trim() || store.isSending) { return; } dispatchQuery(store.questionText); }, [store, dispatchQuery]); const handleFallbackAction = useCallback( (messageId: string): void => { flowResult(store.runOrchestratorFallback(messageId)).catch(noop()); }, [store], ); const handleSuggestedQueryClick = useCallback( (query: string): void => { store.logSuggestedQueryClicked(); store.setQuestionText(query); dispatchQuery(query); }, [store, dispatchQuery], ); if (!store.isEnabled) { return (
Legend AI is not configured. Please contact your administrator.
); } return (
{hasMessages ? ( <>
{store.selectedProduct && (
Scoped to:{' '} {store.selectedProduct.dataProductTitle ?? 'Data Product'}
)}
{store.messages.map((msg) => { if (msg.role === LegendAIMessageRole.USER) { return (
{msg.text}
); } return ( ); })} {store.stage === MarketplaceAIChatStage.PRODUCT_SELECTION && store.suggestedProducts.length > 0 && ( 0 ? { scoredCandidates: store.scoredCandidates, } : {})} onSelect={(product): void => { store.selectDataProduct(product); dispatchQuery(store.lastUserMessageText); }} /> )} {store.stage === MarketplaceAIChatStage.PRODUCT_SELECTION && (
Don't see the right product? Search for it:
{ store.selectAutosuggestProduct(result); dispatchQuery(store.lastUserMessageText); }} className="marketplace-ai-chat__product-search-autosuggest" />
)}
0 ? `Ask about ${store.selectedProduct?.dataProductTitle ?? store.scopeProducts[0]?.name ?? 'this data product'}...` : 'Ask a follow-up...' } onSubmit={handleSubmit} />
) : (

Legend Marketplace AI

Ask anything about your data. I'll find the right data product and query it for you.

{store.welcomeSuggestedQueries.length > 0 && (
{store.welcomeSuggestedQueries.map((q) => ( ))}
)}
)}
); }, );