@finos/legend-application-marketplace
Version:
Legend Marketplace application core
141 lines • 15.6 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
/**
* 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, } from '@finos/legend-art';
import { noop } from '@finos/legend-shared';
import { LegendAIMessageRole, LegendAIErrorType, LegendAIResultGrid, LegendAIAnalysisPanel, renderStepStatusIcon, LAKEHOUSE_ENV_PROD, COVERAGE_NAME_PROD, COVERAGE_NAME_SANDBOX, } from '@finos/legend-lego/legend-ai';
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 }) => (_jsx(MarkdownTextViewer, { value: { value }, className: "legend-ai__text-answer-md" }));
const AssistantMessageView = observer((props) => {
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, setSqlCopied] = useState(false);
const copyTimerRef = useRef(undefined);
useEffect(() => () => {
if (copyTimerRef.current !== undefined) {
clearTimeout(copyTimerRef.current);
}
}, []);
useEffect(() => {
setIsThinkingVisible(msg.isProcessing);
}, [msg.isProcessing]);
const handleCopySql = useCallback(() => {
if (msg.sql) {
navigator.clipboard.writeText(msg.sql).catch(noop());
setSqlCopied(true);
if (copyTimerRef.current !== undefined) {
clearTimeout(copyTimerRef.current);
}
copyTimerRef.current = setTimeout(() => {
setSqlCopied(false);
copyTimerRef.current = undefined;
}, COPY_FEEDBACK_DURATION_MS);
}
}, [msg.sql]);
return (_jsxs("div", { className: "legend-ai__msg legend-ai__msg--assistant", children: [_jsx("div", { className: "legend-ai__msg-avatar", children: _jsx(SparkleStarsIcon, {}) }), _jsxs("div", { className: "legend-ai__msg-content", children: [msg.thinkingSteps.length > 0 && (_jsxs("div", { className: "legend-ai__thinking", children: [!msg.isProcessing && (_jsxs("button", { type: "button", className: "legend-ai__thinking-toggle", onClick: () => setIsThinkingVisible(!isThinkingVisible), children: [_jsx("span", { className: "legend-ai__thinking-toggle-icon", children: isThinkingVisible ? _jsx(CaretDownIcon, {}) : _jsx(CaretRightIcon, {}) }), "Thought for ", msg.thinkingDuration ?? '...', "s"] })), isThinkingVisible && (_jsx("div", { className: "legend-ai__thinking-steps", children: msg.thinkingSteps.map((step) => (_jsxs("div", { className: `legend-ai__thinking-step legend-ai__thinking-step--${step.status}`, children: [_jsx("span", { className: "legend-ai__thinking-step-icon", children: renderStepStatusIcon(step.status) }), _jsx("span", { children: step.label })] }, step.id))) }))] })), msg.dataContext && (_jsx("div", { className: "legend-ai__data-context", children: _jsx(MarkdownTextViewer, { value: { value: msg.dataContext }, className: "legend-ai__text-answer-md" }) })), msg.sql && (_jsxs("div", { className: "legend-ai__sql-block", children: [_jsxs("div", { className: "legend-ai__sql-block-header", children: [_jsx("span", { className: "legend-ai__sql-block-header-icon", children: _jsx(CodeIcon, {}) }), _jsx("span", { children: "Generated Query" }), msg.sqlGenTime && (_jsxs("span", { className: "legend-ai__sql-block-time", children: [msg.sqlGenTime, "s"] })), _jsx("button", { type: "button", className: "legend-ai__sql-copy-btn", title: "Copy query", "aria-label": "Copy query", onClick: handleCopySql, children: sqlCopied ? (_jsx("span", { className: "legend-ai__sql-copy-btn--copied", children: _jsx(CheckIcon, {}) })) : (_jsx(CopyIcon, {})) })] }), _jsx("div", { className: "legend-ai__sql-scroll", children: _jsx("pre", { className: "legend-ai__sql-display", children: msg.sql }) })] })), msg.isExecuting && (_jsxs("div", { className: "legend-ai__executing", children: [_jsx(LoadingIcon, { isLoading: true }), _jsx("span", { children: "Executing query..." })] })), msg.textAnswer && !msg.gridData && (_jsx("div", { className: "legend-ai__text-answer", children: _jsx(MarkdownTextViewer, { value: { value: msg.textAnswer }, className: "legend-ai__text-answer-md" }) })), msg.error && (_jsxs("div", { className: "legend-ai__exec-error", children: [msg.error, msg.errorType === LegendAIErrorType.PERMISSION &&
hasAccessLinks && (_jsxs("div", { className: "legend-ai__permission-error-action", children: [_jsxs("span", { className: "legend-ai__permission-error-note", children: ["Select coverage:", ' ', _jsx("strong", { children: isProd ? COVERAGE_NAME_PROD : COVERAGE_NAME_SANDBOX })] }), _jsxs("div", { className: "legend-ai__permission-error-btns", children: [enghubDocUrl && (_jsxs("a", { className: "legend-ai__permission-error-btn", href: enghubDocUrl, target: "_blank", rel: "noopener noreferrer", children: [_jsx(ExternalLinkIcon, {}), _jsx("span", { children: "View Documentation" })] })), enthubRequestAccessUrl && (_jsxs("a", { className: "legend-ai__permission-error-btn legend-ai__permission-error-btn--primary", href: enthubRequestAccessUrl, target: "_blank", rel: "noopener noreferrer", children: [_jsx(ExternalLinkIcon, {}), _jsx("span", { children: "Request Access" })] }))] })] })), msg.errorType === LegendAIErrorType.NETWORK && (_jsx("div", { className: "legend-ai__permission-error-action", children: _jsx("span", { className: "legend-ai__permission-error-note", children: "Please check your network connection and try again." }) }))] })), msg.fallbackAction && !msg.isProcessing && onFallbackAction && (_jsxs("button", { type: "button", className: "legend-ai__fallback-action-btn", onClick: () => {
const actionId = msg.fallbackAction?.actionId;
if (actionId) {
onFallbackAction(msg.id, actionId);
}
}, children: [_jsx(SparkleStarsIcon, {}), _jsx("span", { children: msg.fallbackAction.label })] })), msg.gridData && (_jsxs("div", { className: "legend-ai__results-block", children: [_jsxs("div", { className: "legend-ai__results-header", children: [_jsx("span", { className: "legend-ai__results-header-icon", children: _jsx(TableIcon, {}) }), _jsx("span", { children: "Results" }), _jsxs("span", { className: "legend-ai__results-meta", children: [msg.gridData.rowData.length, " row", msg.gridData.rowData.length === 1 ? '' : 's', msg.execTime ? (_jsxs(_Fragment, { children: [' ', _jsx(DotIcon, { className: "legend-ai__results-meta-dot" }), ' ', msg.execTime, "s"] })) : ('')] })] }), _jsx(LegendAIResultGrid, { data: msg.gridData })] })), msg.textAnswer && msg.gridData && (_jsx(LegendAIAnalysisPanel, { gridData: msg.gridData, summary: msg.textAnswer, SummaryRenderer: AISummaryRenderer })), msg.isProcessing && !msg.isExecuting && msg.gridData && (_jsxs("div", { className: "legend-ai__analyzing", children: [_jsx(LoadingIcon, { isLoading: true }), _jsx("span", { children: "Analyzing results..." })] })), !msg.isProcessing &&
msg.suggestedQueries.length > 0 &&
onSuggestedQueryClick && (_jsxs("div", { className: "legend-ai__follow-up-suggestions", children: [_jsx("span", { className: "legend-ai__follow-up-label", children: "Follow-up questions:" }), msg.suggestedQueries.map((q) => (_jsx("button", { type: "button", className: "legend-ai__follow-up-btn", onClick: () => onSuggestedQueryClick(q), children: q }, q)))] }))] })] }));
});
export const MarketplaceAIChatView = observer((props) => {
const { initialQuery } = props;
const store = useLegendMarketplaceAIChatStore();
const conversationRef = useRef(null);
const hasMessages = store.messages.length > 0;
const initialQuerySubmitted = useRef(false);
useEffect(() => {
const el = conversationRef.current;
if (el) {
el.scrollTop = el.scrollHeight;
}
}, [store.messages.length]);
const dispatchQuery = useCallback((text) => {
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]);
const handleSubmit = useCallback(() => {
if (!store.questionText.trim() || store.isSending) {
return;
}
dispatchQuery(store.questionText);
}, [store, dispatchQuery]);
const handleFallbackAction = useCallback((messageId, _actionId) => {
flowResult(store.runOrchestratorFallback(messageId)).catch(noop());
}, [store]);
const handleSuggestedQueryClick = useCallback((query) => {
store.setQuestionText(query);
dispatchQuery(query);
}, [store, dispatchQuery]);
if (!store.isEnabled) {
return (_jsx("div", { className: "marketplace-ai-chat marketplace-ai-chat--disabled", children: _jsx("div", { className: "marketplace-ai-chat__empty", children: _jsx("div", { className: "marketplace-ai-chat__empty-text", children: "Legend AI is not configured. Please contact your administrator." }) }) }));
}
return (_jsx("div", { className: "marketplace-ai-chat", children: hasMessages ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "marketplace-ai-chat__header", children: [store.selectedProduct && (_jsxs("div", { className: "marketplace-ai-chat__product-pill", children: [_jsx(SparkleStarsIcon, {}), _jsxs("span", { className: "marketplace-ai-chat__product-pill-text", children: ["Scoped to:", ' ', _jsx("strong", { children: store.selectedProduct.dataProductTitle ?? 'Data Product' })] }), _jsx("button", { type: "button", className: "marketplace-ai-chat__product-pill-dismiss", title: "Remove product scope", "aria-label": "Remove product scope", onClick: () => {
store.deselectProduct();
}, children: _jsx(TimesIcon, {}) })] })), _jsxs("button", { type: "button", className: "marketplace-ai-chat__clear-btn", title: "Clear chat", "aria-label": "Clear chat", onClick: () => store.clearChat(), children: [_jsx(RefreshIcon, {}), _jsx("span", { children: "Clear chat" })] })] }), _jsxs("div", { className: "marketplace-ai-chat__messages", ref: conversationRef, children: [store.messages.map((msg) => {
if (msg.role === LegendAIMessageRole.USER) {
return (_jsx("div", { className: "marketplace-ai-chat__msg marketplace-ai-chat__msg--user", children: _jsx("div", { className: "marketplace-ai-chat__msg-bubble", children: msg.text }) }, msg.id));
}
return (_jsx(AssistantMessageView, { msg: msg, onSuggestedQueryClick: handleSuggestedQueryClick, onFallbackAction: handleFallbackAction }, msg.id));
}), store.stage === MarketplaceAIChatStage.PRODUCT_SELECTION &&
store.suggestedProducts.length > 0 && (_jsx(MarketplaceAIProductCards, { products: store.suggestedProducts, ...(store.scoredCandidates.length > 0
? {
scoredCandidates: store.scoredCandidates,
}
: {}), onSelect: (product) => {
store.selectDataProduct(product);
dispatchQuery(store.lastUserMessageText);
} })), store.stage === MarketplaceAIChatStage.PRODUCT_SELECTION && (_jsxs("div", { className: "marketplace-ai-chat__product-search", children: [_jsx("div", { className: "marketplace-ai-chat__product-search-label", children: "Don't see the right product? Search for it:" }), _jsx(MarketplaceAIProductAutosuggest, { onSelect: (result) => {
store.selectAutosuggestProduct(result);
dispatchQuery(store.lastUserMessageText);
}, className: "marketplace-ai-chat__product-search-autosuggest" })] }))] }), _jsx("div", { className: "marketplace-ai-chat__input-bar", children: _jsx(MarketplaceAIInputBar, { placeholder: store.selectedProduct || store.scopeProducts.length > 0
? `Ask about ${store.selectedProduct?.dataProductTitle ?? store.scopeProducts[0]?.name ?? 'this data product'}...`
: 'Ask a follow-up...', onSubmit: handleSubmit }) })] })) : (_jsxs("div", { className: "marketplace-ai-chat__welcome", children: [_jsx("div", { className: "marketplace-ai-chat__welcome-spacer" }), _jsx("div", { className: "marketplace-ai-chat__welcome-icon", children: _jsx(SparkleStarsIcon, {}) }), _jsx("h1", { className: "marketplace-ai-chat__welcome-title", children: "Legend Marketplace AI" }), _jsx("p", { className: "marketplace-ai-chat__welcome-subtitle", children: "Ask anything about your data. I'll find the right data product and query it for you." }), _jsx("div", { className: "marketplace-ai-chat__welcome-input", children: _jsx(MarketplaceAIInputBar, { placeholder: "Ask anything about your data...", onSubmit: handleSubmit }) }), _jsx("div", { className: "marketplace-ai-chat__suggestions", children: store.welcomeSuggestedQueries.map((q) => (_jsx("button", { type: "button", className: "marketplace-ai-chat__suggestion", onClick: () => {
store.setQuestionText(q);
dispatchQuery(q);
}, children: q }, q))) }), _jsx("div", { className: "marketplace-ai-chat__welcome-spacer-bottom" })] })) }));
});
//# sourceMappingURL=MarketplaceAIChatView.js.map