/** * 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 && ( setIsThinkingVisible(!isThinkingVisible)} > {isThinkingVisible ? : } Thought for {msg.thinkingDuration ?? '...'}s )} {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 )} {sqlCopied ? ( ) : ( )} {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 && ( onFallbackAction(msg.id)} > {msg.fallbackAction.label} )} {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 && ( {isOpeningDataCube ? ( ) : ( )} Open in DataCube )} {store.supportsPython && ( {isPythonOpen ? : } Python code )} )} {store.supportsPython && isPythonOpen && ( {pythonEntry?.status === LegendAIPythonCodeStatus.LOADING && ( Generating Python code... )} {pythonEntry?.status === LegendAIPythonCodeStatus.ERROR && ( {pythonEntry.error} )} {pythonEntry?.status === LegendAIPythonCodeStatus.READY && ( <> Python {pythonCopied ? ( ) : ( )} {pythonEntry.code} {pythonEntry.notebookUrl !== undefined && ( Launch Notebook )} > )} )} {!msg.isProcessing && msg.suggestedQueries.length > 0 && onSuggestedQueryClick && ( Follow-up questions: {msg.suggestedQueries.map((q) => ( onSuggestedQueryClick(q)} > {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.deselectProduct(); }} > )} store.clearChat()} > Clear chat {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) => ( { store.setQuestionText(q); dispatchQuery(q); }} > {q} ))} )} )} ); }, );
{msg.sql}
{pythonEntry.code}
Ask anything about your data. I'll find the right data product and query it for you.