/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable no-console */ import { Callout, CommandBarButton, DefaultButton, DirectionalHint, IButtonStyles, IconButton, Image, Link, MessageBar, MessageBarType, ProgressIndicator, Separator, Stack, TeachingBubble, Text, TextField, } from "@fluentui/react"; import { HttpStatusCodes, NormalizedEventKey } from "Common/Constants"; import { handleError } from "Common/ErrorHandlingUtils"; import QueryError, { QueryErrorSeverity } from "Common/QueryError"; import { createUri } from "Common/UrlUtility"; import { CopyPopup } from "Explorer/QueryCopilot/Popup/CopyPopup"; import { DeletePopup } from "Explorer/QueryCopilot/Popup/DeletePopup"; import { SuggestedPrompt, getSampleDatabaseSuggestedPrompts, getSuggestedPrompts, readPromptHistory, savePromptHistory, } from "Explorer/QueryCopilot/QueryCopilotUtilities"; import { SubmitFeedback, allocatePhoenixContainer } from "Explorer/QueryCopilot/Shared/QueryCopilotClient"; import { GenerateSQLQueryResponse, QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces"; import { SamplePrompts, SamplePromptsProps } from "Explorer/QueryCopilot/Shared/SamplePrompts/SamplePrompts"; import { Action } from "Shared/Telemetry/TelemetryConstants"; import { userContext } from "UserContext"; import { useQueryCopilot } from "hooks/useQueryCopilot"; import React, { useMemo, useRef, useState } from "react"; import HintIcon from "../../../images/Hint.svg"; import RecentIcon from "../../../images/Recent.svg"; import errorIcon from "../../../images/close-black.svg"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { useTabs } from "../../hooks/useTabs"; import { useCopilotStore } from "../QueryCopilot/QueryCopilotContext"; import { useSelectedNode } from "../useSelectedNode"; type QueryCopilotPromptProps = QueryCopilotProps & { databaseId: string; containerId: string; toggleCopilot: (toggle: boolean) => void; }; const promptStyles: IButtonStyles = { root: { border: 0, selectors: { ":hover": { outline: "1px dashed #605e5c" } } }, label: { fontWeight: 400, textAlign: "left", paddingLeft: 8, overflow: "hidden", whiteSpace: "nowrap", textOverflow: "ellipsis", }, textContainer: { overflow: "hidden" }, }; export const QueryCopilotPromptbar: React.FC = ({ explorer, toggleCopilot, databaseId, containerId, }: QueryCopilotPromptProps): JSX.Element => { const [copilotTeachingBubbleVisible, setCopilotTeachingBubbleVisible] = useState(false); const inputEdited = useRef(false); const itemRefs = useRef([]); const searchInputRef = useRef(null); const copyQueryRef = useRef(null); const { openFeedbackModal, hideFeedbackModalForLikedQueries, userPrompt, setUserPrompt, generatedQuery, setGeneratedQuery, query, setQuery, isGeneratingQuery, setIsGeneratingQuery, likeQuery, setLikeQuery, dislikeQuery, setDislikeQuery, showCallout, setShowCallout, isSamplePromptsOpen, setIsSamplePromptsOpen, showSamplePrompts, setShowSamplePrompts, showPromptTeachingBubble, setShowPromptTeachingBubble, showDeletePopup, setShowDeletePopup, showFeedbackBar, setShowFeedbackBar, showCopyPopup, setshowCopyPopup, showErrorMessageBar, showInvalidQueryMessageBar, setShowInvalidQueryMessageBar, setShowErrorMessageBar, setGeneratedQueryComments, setQueryResults, setErrors, errors, } = useCopilotStore(); const [focusedIndex, setFocusedIndex] = useState(-1); const sampleProps: SamplePromptsProps = { isSamplePromptsOpen: isSamplePromptsOpen, setIsSamplePromptsOpen: setIsSamplePromptsOpen, setTextBox: setUserPrompt, }; const copyGeneratedCode = () => { if (!query) { return; } const queryElement = document.createElement("textarea"); queryElement.value = query; document.body.appendChild(queryElement); queryElement.select(); document.execCommand("copy"); document.body.removeChild(queryElement); setshowCopyPopup(true); copyQueryRef.current.focus(); setTimeout(() => { setshowCopyPopup(false); }, 6000); }; const isSampleCopilotActive = useSelectedNode.getState().isQueryCopilotCollectionSelected(); const [histories, setHistories] = useState(() => readPromptHistory(userContext.databaseAccount)); const suggestedPrompts: SuggestedPrompt[] = isSampleCopilotActive ? getSampleDatabaseSuggestedPrompts() : getSuggestedPrompts(); const [filteredHistories, setFilteredHistories] = useState(histories); const [filteredSuggestedPrompts, setFilteredSuggestedPrompts] = useState(suggestedPrompts); const { UpArrow, DownArrow, Enter } = NormalizedEventKey; const handleUserPromptChange = (event: React.ChangeEvent) => { inputEdited.current = true; const { value } = event.target; setUserPrompt(value); // Filter history prompts const filteredHistory = histories.filter((history) => history.toLowerCase().includes(value.toLowerCase())); setFilteredHistories(filteredHistory); // Filter suggested prompts const filteredSuggested = suggestedPrompts.filter((prompt) => prompt.text.toLowerCase().includes(value.toLowerCase()), ); setFilteredSuggestedPrompts(filteredSuggested); }; const updateHistories = (): void => { const formattedUserPrompt = userPrompt.replace(/\s+/g, " ").trim(); const existingHistories = histories.map((history) => history.replace(/\s+/g, " ").trim()); const updatedHistories = existingHistories.filter( (history) => history.toLowerCase() !== formattedUserPrompt.toLowerCase(), ); const newHistories = [formattedUserPrompt, ...updatedHistories.slice(0, 2)]; setHistories(newHistories); savePromptHistory(userContext.databaseAccount, newHistories); }; const resetMessageStates = (): void => { setShowErrorMessageBar(false); setShowInvalidQueryMessageBar(false); setShowFeedbackBar(false); }; const resetQueryResults = (): void => { setQueryResults(null); setErrors([]); }; const generateSQLQuery = async (): Promise => { try { resetMessageStates(); setIsGeneratingQuery(true); setShowDeletePopup(false); useTabs.getState().setIsTabExecuting(true); useTabs.getState().setIsQueryErrorThrown(false); const mode: string = isSampleCopilotActive ? "Sample" : "User"; await allocatePhoenixContainer({ explorer, databaseId, containerId, mode }); const payload = { userPrompt: userPrompt, }; useQueryCopilot.getState().refreshCorrelationId(); const serverInfo = useQueryCopilot.getState().notebookServerInfo; const queryUri = userContext.features.disableCopilotPhoenixGateaway ? createUri("https://copilotorchestrater.azurewebsites.net/", "generateSQLQuery") : createUri(serverInfo.notebookServerEndpoint, "public/generateSQLQuery"); const response = await fetch(queryUri, { method: "POST", headers: { "content-type": "application/json", "x-ms-correlationid": useQueryCopilot.getState().correlationId, Authorization: `token ${useQueryCopilot.getState().notebookServerInfo.authToken}`, }, body: JSON.stringify(payload), }); const generateSQLQueryResponse: GenerateSQLQueryResponse = await response?.json(); if (response.ok) { if (generateSQLQueryResponse?.sql !== "N/A") { const queryExplanation = `-- **Explanation of query:** ${ generateSQLQueryResponse.explanation ? generateSQLQueryResponse.explanation : "N/A" }\r\n`; const currentGeneratedQuery = queryExplanation + generateSQLQueryResponse.sql; const lastQuery = generatedQuery && query ? `${query}\r\n` : ""; setQuery(`${lastQuery}${currentGeneratedQuery}`); setGeneratedQuery(generateSQLQueryResponse.sql); setGeneratedQueryComments(generateSQLQueryResponse.explanation); setShowFeedbackBar(true); resetQueryResults(); TelemetryProcessor.traceSuccess(Action.QueryGenerationFromCopilotPrompt, { databaseName: databaseId, collectionId: containerId, copilotLatency: Date.parse(generateSQLQueryResponse?.generateEnd) - Date.parse(generateSQLQueryResponse?.generateStart), responseCode: response.status, }); } else { setShowInvalidQueryMessageBar(true); TelemetryProcessor.traceFailure(Action.QueryGenerationFromCopilotPrompt, { databaseName: databaseId, collectionId: containerId, responseCode: response.status, }); } } else if (response?.status === HttpStatusCodes.TooManyRequests) { handleError(JSON.stringify(generateSQLQueryResponse), "copilotTooManyRequestError"); useTabs.getState().setIsQueryErrorThrown(true); setShowErrorMessageBar(true); setErrors([ new QueryError( "Ratelimit exceeded 5 per 1 minute. Please try again after sometime", QueryErrorSeverity.Error, ), ]); TelemetryProcessor.traceFailure(Action.QueryGenerationFromCopilotPrompt, { databaseName: databaseId, collectionId: containerId, responseCode: response.status, }); } else { handleError(JSON.stringify(generateSQLQueryResponse), "copilotInternalServerError"); useTabs.getState().setIsQueryErrorThrown(true); setShowErrorMessageBar(true); TelemetryProcessor.traceFailure(Action.QueryGenerationFromCopilotPrompt, { databaseName: databaseId, collectionId: containerId, responseCode: response.status, }); } } catch (error) { handleError(error, "executeNaturalLanguageQuery"); useTabs.getState().setIsQueryErrorThrown(true); setShowErrorMessageBar(true); throw error; } finally { setIsGeneratingQuery(false); useTabs.getState().setIsTabExecuting(false); } }; const toggleCopilotTeachingBubbleVisible = (visible: boolean): void => { setCopilotTeachingBubbleVisible(visible); setShowPromptTeachingBubble(visible); }; const clearFeedback = () => { resetButtonState(); resetQueryResults(); }; const resetButtonState = () => { setDislikeQuery(false); setLikeQuery(false); setShowCallout(false); }; const startGenerateQueryProcess = () => { updateHistories(); generateSQLQuery(); resetButtonState(); }; const getAriaLabel = () => { if (isGeneratingQuery === null) { return " "; } else if (isGeneratingQuery) { return "Content is loading"; } else { return "Content is updated"; } }; const openSamplePrompts = () => { inputEdited.current = true; setShowSamplePrompts(true); }; const totalSuggestions = useMemo( () => [...filteredSuggestedPrompts, ...filteredHistories], [filteredSuggestedPrompts, filteredHistories], ); const handleKeyDownForInput = (event: React.KeyboardEvent) => { if (event.key === DownArrow) { setFocusedIndex(0); itemRefs.current[0]?.current?.focus(); } else if (event.key === Enter && userPrompt) { inputEdited.current = true; startGenerateQueryProcess(); } }; const handleKeyDownForItem = (event: React.KeyboardEvent) => { if (event.key === UpArrow && focusedIndex > 0) { itemRefs.current[focusedIndex - 1].current?.focus(); setFocusedIndex((prevIndex) => prevIndex - 1); } else if (event.key === DownArrow && focusedIndex < totalSuggestions.length - 1) { itemRefs.current[focusedIndex + 1].current?.focus(); setFocusedIndex((prevIndex) => prevIndex + 1); } }; React.useEffect(() => { itemRefs.current = totalSuggestions.map(() => React.createRef()); }, [totalSuggestions]); React.useEffect(() => { useTabs.getState().setIsQueryErrorThrown(false); }, []); return ( setShowSamplePrompts(true)} elementRef={searchInputRef} onKeyDown={handleKeyDownForInput} style={{ lineHeight: 30 }} styles={{ root: { width: "100%" }, suffix: { background: "none", padding: 0 }, fieldGroup: { borderRadius: 4, borderColor: "#D1D1D1", "::after": { border: "inherit", borderWidth: 2, borderBottomColor: "#464FEB", borderRadius: 4, }, }, }} disabled={isGeneratingQuery} autoComplete="list" aria-expanded={showSamplePrompts} placeholder="Ask a question in natural language and we’ll generate the query for you." aria-labelledby="copilot-textfield-label" onRenderSuffix={() => { return ( startGenerateQueryProcess()} aria-label="Send" /> ); }} /> {showPromptTeachingBubble && copilotTeachingBubbleVisible && ( toggleCopilotTeachingBubbleVisible(false)} hasSmallHeadline={true} headline="Write a prompt" > Write a prompt here and Query Advisor will generate the query for you. You can also choose from our{" "} { setShowSamplePrompts(true); toggleCopilotTeachingBubbleVisible(false); }} style={{ color: "white", fontWeight: 600 }} > sample prompts {" "} or write your own query )} {showSamplePrompts && ( setShowSamplePrompts(false)} directionalHintFixed={true} directionalHint={DirectionalHint.bottomLeftEdge} alignTargetEdge={true} gapSpace={4} > {filteredHistories?.length > 0 && ( Recent {filteredHistories.map((history, i) => ( { setUserPrompt(history); setShowSamplePrompts(false); inputEdited.current = true; }} elementRef={itemRefs.current[i]} onKeyDown={handleKeyDownForItem} onRenderIcon={() => } styles={promptStyles} > {history} ))} )} {filteredSuggestedPrompts?.length > 0 && ( Suggested Prompts {filteredSuggestedPrompts.map((prompt, index) => ( { setUserPrompt(prompt.text); setShowSamplePrompts(false); inputEdited.current = true; }} onKeyDown={handleKeyDownForItem} onRenderIcon={() => } styles={promptStyles} > {prompt.text} ))} )} {(filteredHistories?.length > 0 || filteredSuggestedPrompts?.length > 0) && ( Learn about{" "} writing effective prompts )} )} {!isGeneratingQuery && ( {!showFeedbackBar && ( AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.{" "} Read preview terms {showErrorMessageBar && ( {errors.length > 0 ? errors[0].message : "We ran into an error and were not able to execute query."} )} {showInvalidQueryMessageBar && ( We were unable to generate a query based upon the prompt provided. Please modify the prompt and try again. For examples of how to write a good prompt, please read this article. {" "} Our content guidelines can be found here. )} )} {showFeedbackBar && ( {userContext.feedbackPolicies?.policyAllowFeedback && ( Provide feedback {showCallout && !hideFeedbackModalForLikedQueries && ( { setShowCallout(false); SubmitFeedback({ params: { generatedQuery: generatedQuery, likeQuery: likeQuery, description: "", userPrompt: userPrompt, }, explorer, databaseId, containerId, mode: isSampleCopilotActive ? "Sample" : "User", }); }} directionalHint={DirectionalHint.topCenter} > Thank you. Need to give{" "} { setShowCallout(false); openFeedbackModal(generatedQuery, true, userPrompt); }} > more feedback? )} { setShowCallout(!likeQuery); setLikeQuery(!likeQuery); if (likeQuery === true) { document.getElementById("likeStatus").innerHTML = "Unpressed"; } if (likeQuery === false) { document.getElementById("likeStatus").innerHTML = "Liked"; } if (dislikeQuery) { setDislikeQuery(!dislikeQuery); } }} /> { let toggleStatusValue = "Unpressed"; if (!dislikeQuery) { openFeedbackModal(generatedQuery, false, userPrompt); setLikeQuery(false); toggleStatusValue = "Disliked"; } setDislikeQuery(!dislikeQuery); setShowCallout(false); document.getElementById("likeStatus").innerHTML = toggleStatusValue; }} /> )} Copy code { setShowDeletePopup(true); }} iconProps={{ iconName: "Delete" }} style={{ fontSize: 12, transition: "background-color 0.3s ease", height: "100%" }} styles={{ root: { backgroundColor: "inherit", }, }} > Clear editor )} )} {isGeneratingQuery && ( )} { toggleCopilot(false); clearFeedback(); resetMessageStates(); }} ariaLabel="Close" title="Close copilot" /> {isSamplePromptsOpen && } {query !== "" && query.trim().length !== 0 && ( )} ); };