import React, { useCallback, useEffect, useRef, useState } from 'react'; import { Banner, BannerVariant, Button, Icon, IconButton, SpinLoader, TextInput, css, cx, focusRing, palette, spacing, useDarkMode, } from '@mongodb-js/compass-components'; import { DEFAULT_AI_ENTRY_SIZE } from './ai-entry-svg'; import { AIFeedback } from './ai-feedback'; import { AIGuideCue } from './ai-guide-cue'; const containerStyles = css({ display: 'flex', flexDirection: 'column', gap: spacing[1], }); const inputBarContainerStyles = css({ paddingTop: spacing[2], gap: spacing[2], flexGrow: 1, display: 'flex', }); const inputContainerStyles = css({ display: 'flex', flexGrow: 1, position: 'relative', }); const textInputStyles = css({ flexGrow: 1, // Override LeafyGreen input's padding to space for our robot. input: { paddingLeft: spacing[5], paddingRight: spacing[6] * 2 + spacing[2], }, }); const errorSummaryContainer = css({ marginTop: spacing[1], }); const floatingButtonsContainerStyles = css({ position: 'absolute', right: spacing[1], display: 'flex', gap: spacing[2], alignItems: 'center', // Match the whole textbox. height: spacing[4] + spacing[1], }); const successIndicatorDarkModeStyles = css({ color: palette.gray.dark3, backgroundColor: palette.green.base, borderRadius: '50%', }); const successIndicatorLightModeStyles = css({ color: palette.white, backgroundColor: palette.green.dark1, borderRadius: '50%', }); const generateButtonStyles = css({ border: 'none', height: spacing[4] - spacing[1], display: 'flex', fontSize: '12px', borderRadius: spacing[1], }); const generateButtonLightModeStyles = css({ backgroundColor: palette.gray.light2, }); const highlightSize = 14; const buttonHighlightStyles = css({ // Custom button styles. height: `${highlightSize}px`, lineHeight: `${highlightSize}px`, padding: `0px ${spacing[1]}px`, borderRadius: '2px', }); const buttonHighlightDarkModeStyles = css({ backgroundColor: palette.gray.dark1, color: palette.gray.light1, }); const buttonHighlightLightModeStyles = css({ backgroundColor: palette.gray.light1, color: palette.gray.dark1, }); const loaderContainerStyles = css({ padding: spacing[1], display: 'inline-flex', width: DEFAULT_AI_ENTRY_SIZE + spacing[2], justifyContent: 'space-around', }); const buttonResetStyles = css({ margin: 0, padding: 0, border: 'none', background: 'none', cursor: 'pointer', }); const closeAIButtonStyles = css(buttonResetStyles, focusRing, { height: spacing[4] + spacing[1], display: 'flex', alignItems: 'center', padding: `${spacing[1]}px ${spacing[2]}px`, position: 'absolute', }); const aiEntryContainerStyles = css({ display: 'flex', }); const closeText = 'Close AI Helper'; const SubmitArrowSVG = ({ darkMode }: { darkMode?: boolean }) => ( ); type GenerativeAIInputProps = { aiPromptText: string; didSucceed: boolean; errorMessage?: string; errorCode?: string; isFetching?: boolean; placeholder?: string; show: boolean; isAggregationGeneratedFromQuery?: boolean; onResetIsAggregationGeneratedFromQuery?: () => void; onCancelRequest: () => void; onChangeAIPromptText: (text: string) => void; onClose: () => void; onSubmitText: (text: string) => void; onSubmitFeedback?: ( feedback: 'positive' | 'negative', feedbackText: string ) => void; }; function GenerativeAIInput({ aiPromptText, didSucceed, errorMessage, errorCode, isFetching, placeholder = 'Tell Compass what documents to find (e.g. which movies were released in 2000)', show, onCancelRequest, onClose, onChangeAIPromptText, onSubmitFeedback, onSubmitText, isAggregationGeneratedFromQuery = false, onResetIsAggregationGeneratedFromQuery, }: GenerativeAIInputProps) { const promptTextInputRef = useRef(null); const [showSuccess, setShowSuccess] = useState(false); const darkMode = useDarkMode(); const guideCueRef = useRef(null); const onTextInputKeyDown = useCallback( (evt: React.KeyboardEvent) => { if (evt.key === 'Enter') { evt.preventDefault(); if (!aiPromptText) { return; } onSubmitText(aiPromptText); } else if (evt.key === 'Escape') { isFetching ? onCancelRequest() : onClose(); } }, [aiPromptText, onClose, onSubmitText, isFetching, onCancelRequest] ); useEffect(() => { if (didSucceed) { setShowSuccess(true); const timeoutId = setTimeout(() => { setShowSuccess(false); }, 1500); return () => clearTimeout(timeoutId); } }, [didSucceed]); useEffect(() => { if (show) { promptTextInputRef.current?.focus(); } }, [show]); const onCancelRequestRef = useRef(onCancelRequest); onCancelRequestRef.current = onCancelRequest; useEffect(() => { // When unmounting, ensure we cancel any ongoing requests. return () => onCancelRequestRef.current?.(); }, []); if (!show) { return null; } return (
) => onChangeAIPromptText(evt.currentTarget.value) } onKeyDown={onTextInputKeyDown} />
{isFetching ? (
) : showSuccess ? (
) : ( <> {aiPromptText && ( onChangeAIPromptText('')} data-testid="ai-text-clear-prompt" > )} )}
{didSucceed && onSubmitFeedback && ( )}
{errorMessage && (
)}
); } const AIError = ({ errorCode, errorMessage, }: { errorCode?: string; errorMessage: string; }) => { // NOTE: this error is not coming from the HTTP endpoint. if (!errorCode) { return <>{errorMessage}; } // NOTE: this should never happen in Data Explorer as the frontend update would happen // in coordination with the api changes. if (errorCode === 'NOT_SUPPORTED') { return ( <> Sorry, this version of Compass is no longer suitable to generate queries. Please update to the latest version to access all the features. ); } if (errorCode === 'USER_INPUT_TOO_LONG') { return ( <> Looks like your input exceeds the allowed length. Please reduce it and submit your prompt again. ); } if (errorCode === 'PROMPT_TOO_LONG') { // TODO: https://jira.mongodb.org/browse/COMPASS-6866, // the following errors are probably better handled at service or backend level, by retrying the // call without the schema and additional parameters, since users are not // able to fix the issue on their own it cases where the schema is too big. return ( <> Sorry, your collections have too many fields to process. Please try using this feature on a collection with smaller documents. ); } if (errorCode === 'TOO_MANY_REQUESTS') { return ( <> Sorry, we are receiving too many requests in a short period of time. Please wait a few minutes and try again. ); } if (errorCode === 'GATEWAY_TIMEOUT') { return ( <> It took too long to generate your query, please check your connection and try again. If the problem persists, contact our support team. ); } // We received an errorCode that is not actionable (INTERNAL_SERVER_ERROR, QUERY_GENERATION_FAILED), or unknown. return ( <> Sorry, we were unable to generate the query, please try again. If the error persists, try changing your prompt. ); }; export { GenerativeAIInput };