diff --git a/images/CopilotCommand.svg b/images/CopilotCommand.svg new file mode 100644 index 000000000..7bcf596a5 --- /dev/null +++ b/images/CopilotCommand.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Explorer/QueryCopilot/Modal/WelcomeModal.tsx b/src/Explorer/QueryCopilot/Modal/WelcomeModal.tsx index 6f0c811ea..da45ff904 100644 --- a/src/Explorer/QueryCopilot/Modal/WelcomeModal.tsx +++ b/src/Explorer/QueryCopilot/Modal/WelcomeModal.tsx @@ -52,7 +52,7 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element => - Welcome to Query Copilot for Azure Cosmos DB (Private Preview) + Welcome to Copilot in Azure Cosmos DB (Private Preview) @@ -61,7 +61,7 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element => - Let Query Copilot do the work for you + Let Copilot do the work for you
diff --git a/src/Explorer/QueryCopilot/Modal/__snapshots__/WelcomeModal.test.tsx.snap b/src/Explorer/QueryCopilot/Modal/__snapshots__/WelcomeModal.test.tsx.snap index 189fd7a2d..349a6479f 100644 --- a/src/Explorer/QueryCopilot/Modal/__snapshots__/WelcomeModal.test.tsx.snap +++ b/src/Explorer/QueryCopilot/Modal/__snapshots__/WelcomeModal.test.tsx.snap @@ -76,7 +76,7 @@ exports[`Query Copilot Welcome Modal snapshot test should render when isOpen is - Welcome to Query Copilot for Azure Cosmos DB (Private Preview) + Welcome to Copilot in Azure Cosmos DB (Private Preview) - Let Query Copilot do the work for you + Let Copilot do the work for you
diff --git a/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx b/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx new file mode 100644 index 000000000..59f5e8d7d --- /dev/null +++ b/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx @@ -0,0 +1,563 @@ +import { + Callout, + CommandBarButton, + DefaultButton, + DirectionalHint, + IButtonStyles, + IconButton, + Image, + Link, + MessageBar, + MessageBarType, + Separator, + Spinner, + Stack, + TeachingBubble, + Text, + TextField, +} from "@fluentui/react"; +import { useBoolean } from "@fluentui/react-hooks"; +import { + ContainerStatusType, + PoolIdType, + QueryCopilotSampleContainerSchema, + ShortenedQueryCopilotSampleContainerSchema, +} from "Common/Constants"; +import { handleError } from "Common/ErrorHandlingUtils"; +import { createUri } from "Common/UrlUtility"; +import { WelcomeModal } from "Explorer/QueryCopilot/Modal/WelcomeModal"; +import { CopyPopup } from "Explorer/QueryCopilot/Popup/CopyPopup"; +import { DeletePopup } from "Explorer/QueryCopilot/Popup/DeletePopup"; +import { SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient"; +import { GenerateSQLQueryResponse, QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces"; +import { SamplePrompts, SamplePromptsProps } from "Explorer/QueryCopilot/Shared/SamplePrompts/SamplePrompts"; +import { userContext } from "UserContext"; +import { useQueryCopilot } from "hooks/useQueryCopilot"; +import React, { useRef, useState } from "react"; +import HintIcon from "../../../images/Hint.svg"; +import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg"; +import RecentIcon from "../../../images/Recent.svg"; +import errorIcon from "../../../images/close-black.svg"; +import { useTabs } from "../../hooks/useTabs"; + +type QueryCopilotPromptProps = QueryCopilotProps & { + toggleCopilot: (toggle: boolean) => void; +}; + +interface SuggestedPrompt { + id: number; + text: string; +} + +const promptStyles: IButtonStyles = { + root: { border: 0, selectors: { ":hover": { outline: "1px dashed #605e5c" } } }, + label: { fontWeight: 400, textAlign: "left", paddingLeft: 8 }, +}; + +export const QueryCopilotPromptbar: React.FC = ({ + explorer, + toggleCopilot, +}: QueryCopilotPromptProps): JSX.Element => { + const [copilotTeachingBubbleVisible, { toggle: toggleCopilotTeachingBubbleVisible }] = useBoolean(false); + const inputEdited = useRef(false); + const { + hideFeedbackModalForLikedQueries, + userPrompt, + setUserPrompt, + generatedQuery, + setGeneratedQuery, + query, + setQuery, + isGeneratingQuery, + setIsGeneratingQuery, + likeQuery, + setLikeQuery, + dislikeQuery, + setDislikeQuery, + showCallout, + setShowCallout, + isSamplePromptsOpen, + setIsSamplePromptsOpen, + showSamplePrompts, + setShowSamplePrompts, + showDeletePopup, + setShowDeletePopup, + showFeedbackBar, + setShowFeedbackBar, + showCopyPopup, + setshowCopyPopup, + showErrorMessageBar, + showInvalidQueryMessageBar, + setShowInvalidQueryMessageBar, + setShowErrorMessageBar, + setGeneratedQueryComments, + setQueryResults, + setErrorMessage, + } = useQueryCopilot(); + + 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); + setTimeout(() => { + setshowCopyPopup(false); + }, 6000); + }; + + const cachedHistoriesString = localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotHistories`); + const cachedHistories = cachedHistoriesString?.split("|"); + const [histories, setHistories] = useState(cachedHistories || []); + const suggestedPrompts: SuggestedPrompt[] = [ + { id: 1, text: 'Show all products that have the word "ultra" in the name or description' }, + { id: 2, text: "What are all of the possible categories for the products, and their counts?" }, + { id: 3, text: 'Show me all products that have been reviewed by someone with a username that contains "bob"' }, + ]; + const [filteredHistories, setFilteredHistories] = useState(histories); + const [filteredSuggestedPrompts, setFilteredSuggestedPrompts] = useState(suggestedPrompts); + + 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); + localStorage.setItem(`${userContext.databaseAccount.id}-queryCopilotHistories`, newHistories.join("|")); + }; + + const resetMessageStates = (): void => { + setShowErrorMessageBar(false); + setShowInvalidQueryMessageBar(false); + setShowFeedbackBar(false); + }; + + const resetQueryResults = (): void => { + setQueryResults(null); + setErrorMessage(""); + }; + + const generateSQLQuery = async (): Promise => { + try { + resetMessageStates(); + setIsGeneratingQuery(true); + setShowDeletePopup(false); + useTabs.getState().setIsTabExecuting(true); + useTabs.getState().setIsQueryErrorThrown(false); + if ( + useQueryCopilot.getState().containerStatus.status !== ContainerStatusType.Active && + !userContext.features.disableCopilotPhoenixGateaway + ) { + await explorer.allocateContainer(PoolIdType.QueryCopilot); + } + const payload = { + containerSchema: userContext.features.enableCopilotFullSchema + ? QueryCopilotSampleContainerSchema + : ShortenedQueryCopilotSampleContainerSchema, + userPrompt: userPrompt, + }; + useQueryCopilot.getState().refreshCorrelationId(); + const serverInfo = useQueryCopilot.getState().notebookServerInfo; + const queryUri = userContext.features.disableCopilotPhoenixGateaway + ? createUri("https://copilotorchestrater.azurewebsites.net/", "generateSQLQuery") + : createUri(serverInfo.notebookServerEndpoint, "generateSQLQuery"); + const response = await fetch(queryUri, { + method: "POST", + headers: { + "content-type": "application/json", + "x-ms-correlationid": useQueryCopilot.getState().correlationId, + }, + body: JSON.stringify(payload), + }); + + const generateSQLQueryResponse: GenerateSQLQueryResponse = await response?.json(); + if (response.ok) { + if (generateSQLQueryResponse?.sql !== "N/A") { + let query = `-- **Prompt:** ${userPrompt}\r\n`; + if (generateSQLQueryResponse.explanation) { + query += `-- **Explanation of query:** ${generateSQLQueryResponse.explanation}\r\n`; + } + query += generateSQLQueryResponse.sql; + setQuery(query); + setGeneratedQuery(generateSQLQueryResponse.sql); + setGeneratedQueryComments(generateSQLQueryResponse.explanation); + setShowFeedbackBar(true); + resetQueryResults(); + } else { + setShowInvalidQueryMessageBar(true); + } + } else { + handleError(JSON.stringify(generateSQLQueryResponse), "copilotInternalServerError"); + useTabs.getState().setIsQueryErrorThrown(true); + setShowErrorMessageBar(true); + } + } catch (error) { + handleError(error, "executeNaturalLanguageQuery"); + useTabs.getState().setIsQueryErrorThrown(true); + setShowErrorMessageBar(true); + throw error; + } finally { + setIsGeneratingQuery(false); + useTabs.getState().setIsTabExecuting(false); + } + }; + + const showTeachingBubble = (): void => { + if (!inputEdited.current) { + setTimeout(() => { + if (!inputEdited.current) { + toggleCopilotTeachingBubbleVisible(); + inputEdited.current = true; + } + }, 30000); + } + }; + + const clearFeedback = () => { + resetButtonState(); + resetQueryResults(); + }; + + const resetButtonState = () => { + setDislikeQuery(false); + setLikeQuery(false); + setShowCallout(false); + }; + + const startGenerateQueryProcess = () => { + updateHistories(); + generateSQLQuery(); + resetButtonState(); + }; + + React.useEffect(() => { + showTeachingBubble(); + useTabs.getState().setIsQueryErrorThrown(false); + }, []); + + return ( + + + + Copilot + { + toggleCopilot(false); + clearFeedback(); + resetMessageStates(); + }} + styles={{ + root: { + marginLeft: "auto !important", + }, + }} + ariaLabel="Close" + /> + + + { + inputEdited.current = true; + setShowSamplePrompts(true); + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + inputEdited.current = true; + startGenerateQueryProcess(); + } + }} + style={{ lineHeight: 30 }} + styles={{ root: { width: "95%" }, fieldGroup: { borderRadius: 6 } }} + disabled={isGeneratingQuery} + autoComplete="off" + /> + {copilotTeachingBubbleVisible && ( + + Write a prompt here and Copilot will generate the query for you. You can also choose from our{" "} + { + setShowSamplePrompts(true); + toggleCopilotTeachingBubbleVisible(); + }} + style={{ color: "white", fontWeight: 600 }} + > + sample prompts + {" "} + or write your own query + + )} + startGenerateQueryProcess()} + /> + {isGeneratingQuery && } + {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; + }} + onRenderIcon={() => } + styles={promptStyles} + > + {history} + + ))} + + )} + {filteredSuggestedPrompts?.length > 0 && ( + + + Suggested Prompts + + {filteredSuggestedPrompts.map((prompt) => ( + { + setUserPrompt(prompt.text); + setShowSamplePrompts(false); + inputEdited.current = true; + }} + onRenderIcon={() => } + styles={promptStyles} + > + {prompt.text} + + ))} + + )} + {(filteredHistories?.length > 0 || filteredSuggestedPrompts?.length > 0) && ( + + + + Learn about{" "} + + writing effective prompts + + + + )} + + + )} + + + + + AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.{" "} + + Read preview terms + + {showErrorMessageBar && ( + + 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 && ( + + Provide feedback on the query generated + {showCallout && !hideFeedbackModalForLikedQueries && ( + { + setShowCallout(false); + SubmitFeedback({ + params: { + generatedQuery: generatedQuery, + likeQuery: likeQuery, + description: "", + userPrompt: userPrompt, + }, + explorer: explorer, + }); + }} + directionalHint={DirectionalHint.topCenter} + > + + Thank you. Need to give{" "} + { + setShowCallout(false); + useQueryCopilot.getState().openFeedbackModal(generatedQuery, true, userPrompt); + }} + > + more feedback? + + + + )} + { + setShowCallout(!likeQuery); + setLikeQuery(!likeQuery); + if (dislikeQuery) { + setDislikeQuery(!dislikeQuery); + } + }} + /> + { + if (!dislikeQuery) { + useQueryCopilot.getState().openFeedbackModal(generatedQuery, false, userPrompt); + setLikeQuery(false); + } + setDislikeQuery(!dislikeQuery); + setShowCallout(false); + }} + /> + + + Copy code + + { + setShowDeletePopup(true); + }} + iconProps={{ iconName: "Delete" }} + style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }} + > + Delete code + + + )} + + {isSamplePromptsOpen && } + {query !== "" && query.trim().length !== 0 && ( + + )} + + + ); +}; diff --git a/src/Explorer/QueryCopilot/QueryCopilotTab.tsx b/src/Explorer/QueryCopilot/QueryCopilotTab.tsx index f5b76b9ef..c7ce41aa6 100644 --- a/src/Explorer/QueryCopilot/QueryCopilotTab.tsx +++ b/src/Explorer/QueryCopilot/QueryCopilotTab.tsx @@ -1,242 +1,28 @@ /* eslint-disable no-console */ -import { - Callout, - CommandBarButton, - DefaultButton, - DirectionalHint, - IButtonStyles, - IconButton, - Image, - Link, - MessageBar, - MessageBarType, - Separator, - Spinner, - Stack, - TeachingBubble, - Text, - TextField, -} from "@fluentui/react"; -import { useBoolean } from "@fluentui/react-hooks"; -import { - ContainerStatusType, - PoolIdType, - QueryCopilotSampleContainerSchema, - ShortenedQueryCopilotSampleContainerSchema, -} from "Common/Constants"; -import { handleError } from "Common/ErrorHandlingUtils"; -import { createUri } from "Common/UrlUtility"; +import { Stack } from "@fluentui/react"; import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent"; import { EditorReact } from "Explorer/Controls/Editor/EditorReact"; import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter"; import { SaveQueryPane } from "Explorer/Panes/SaveQueryPane/SaveQueryPane"; -import { WelcomeModal } from "Explorer/QueryCopilot/Modal/WelcomeModal"; -import { CopyPopup } from "Explorer/QueryCopilot/Popup/CopyPopup"; -import { DeletePopup } from "Explorer/QueryCopilot/Popup/DeletePopup"; -import { OnExecuteQueryClick, SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient"; -import { GenerateSQLQueryResponse, QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces"; +import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar"; +import { OnExecuteQueryClick } from "Explorer/QueryCopilot/Shared/QueryCopilotClient"; +import { QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces"; import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotResults"; -import { SamplePrompts, SamplePromptsProps } from "Explorer/QueryCopilot/Shared/SamplePrompts/SamplePrompts"; import { userContext } from "UserContext"; import { useQueryCopilot } from "hooks/useQueryCopilot"; import { useSidePanel } from "hooks/useSidePanel"; -import React, { useRef, useState } from "react"; +import React, { useState } from "react"; import SplitterLayout from "react-splitter-layout"; +import QueryCommandIcon from "../../../images/CopilotCommand.svg"; import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg"; -import HintIcon from "../../../images/Hint.svg"; -import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg"; -import RecentIcon from "../../../images/Recent.svg"; import SaveQueryIcon from "../../../images/save-cosmos.svg"; -import { useTabs } from "../../hooks/useTabs"; - -interface SuggestedPrompt { - id: number; - text: string; -} - -const promptStyles: IButtonStyles = { - root: { border: 0, selectors: { ":hover": { outline: "1px dashed #605e5c" } } }, - label: { fontWeight: 400, textAlign: "left", paddingLeft: 8 }, -}; +import * as StringUtility from "../../Shared/StringUtility"; export const QueryCopilotTab: React.FC = ({ explorer }: QueryCopilotProps): JSX.Element => { - const [copilotTeachingBubbleVisible, { toggle: toggleCopilotTeachingBubbleVisible }] = useBoolean(false); - const inputEdited = useRef(false); - const { - hideFeedbackModalForLikedQueries, - userPrompt, - setUserPrompt, - generatedQuery, - setGeneratedQuery, - query, - setQuery, - selectedQuery, - setSelectedQuery, - isGeneratingQuery, - setIsGeneratingQuery, - likeQuery, - setLikeQuery, - dislikeQuery, - setDislikeQuery, - showCallout, - setShowCallout, - showSamplePrompts, - setShowSamplePrompts, - isSamplePromptsOpen, - setIsSamplePromptsOpen, - showDeletePopup, - setShowDeletePopup, - showFeedbackBar, - setShowFeedbackBar, - showCopyPopup, - setshowCopyPopup, - showErrorMessageBar, - showInvalidQueryMessageBar, - setShowInvalidQueryMessageBar, - setShowErrorMessageBar, - setGeneratedQueryComments, - setQueryResults, - setErrorMessage, - } = useQueryCopilot(); + const { query, setQuery, selectedQuery, setSelectedQuery, isGeneratingQuery } = useQueryCopilot(); - 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); - setTimeout(() => { - setshowCopyPopup(false); - }, 6000); - }; - - const cachedHistoriesString = localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotHistories`); - const cachedHistories = cachedHistoriesString?.split("|"); - const [histories, setHistories] = useState(cachedHistories || []); - const suggestedPrompts: SuggestedPrompt[] = [ - { id: 1, text: 'Show all products that have the word "ultra" in the name or description' }, - { id: 2, text: "What are all of the possible categories for the products, and their counts?" }, - { id: 3, text: 'Show me all products that have been reviewed by someone with a username that contains "bob"' }, - ]; - const [filteredHistories, setFilteredHistories] = useState(histories); - const [filteredSuggestedPrompts, setFilteredSuggestedPrompts] = useState(suggestedPrompts); - - 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); - localStorage.setItem(`${userContext.databaseAccount.id}-queryCopilotHistories`, newHistories.join("|")); - }; - - const resetMessageStates = (): void => { - setShowErrorMessageBar(false); - setShowInvalidQueryMessageBar(false); - setShowFeedbackBar(false); - }; - - const resetQueryResults = (): void => { - setQueryResults(null); - setErrorMessage(""); - }; - - const generateSQLQuery = async (): Promise => { - try { - resetMessageStates(); - setIsGeneratingQuery(true); - setShowDeletePopup(false); - useTabs.getState().setIsTabExecuting(true); - useTabs.getState().setIsQueryErrorThrown(false); - if ( - useQueryCopilot.getState().containerStatus.status !== ContainerStatusType.Active && - !userContext.features.disableCopilotPhoenixGateaway - ) { - await explorer.allocateContainer(PoolIdType.QueryCopilot); - } - const payload = { - containerSchema: userContext.features.enableCopilotFullSchema - ? QueryCopilotSampleContainerSchema - : ShortenedQueryCopilotSampleContainerSchema, - userPrompt: userPrompt, - }; - useQueryCopilot.getState().refreshCorrelationId(); - const serverInfo = useQueryCopilot.getState().notebookServerInfo; - const queryUri = userContext.features.disableCopilotPhoenixGateaway - ? createUri("https://copilotorchestrater.azurewebsites.net/", "generateSQLQuery") - : createUri(serverInfo.notebookServerEndpoint, "generateSQLQuery"); - const response = await fetch(queryUri, { - method: "POST", - headers: { - "content-type": "application/json", - "x-ms-correlationid": useQueryCopilot.getState().correlationId, - }, - body: JSON.stringify(payload), - }); - - const generateSQLQueryResponse: GenerateSQLQueryResponse = await response?.json(); - if (response.ok) { - if (generateSQLQueryResponse?.sql !== "N/A") { - let query = `-- **Prompt:** ${userPrompt}\r\n`; - if (generateSQLQueryResponse.explanation) { - query += `-- **Explanation of query:** ${generateSQLQueryResponse.explanation}\r\n`; - } - query += generateSQLQueryResponse.sql; - setQuery(query); - setGeneratedQuery(generateSQLQueryResponse.sql); - setGeneratedQueryComments(generateSQLQueryResponse.explanation); - setShowFeedbackBar(true); - resetQueryResults(); - } else { - setShowInvalidQueryMessageBar(true); - } - } else { - handleError(JSON.stringify(generateSQLQueryResponse), "copilotInternalServerError"); - useTabs.getState().setIsQueryErrorThrown(true); - setShowErrorMessageBar(true); - } - } catch (error) { - handleError(error, "executeNaturalLanguageQuery"); - useTabs.getState().setIsQueryErrorThrown(true); - setShowErrorMessageBar(true); - throw error; - } finally { - setIsGeneratingQuery(false); - useTabs.getState().setIsTabExecuting(false); - } - }; + const cachedCopilotToggleStatus = localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`); + const [copilotActive, setCopilotActive] = useState(StringUtility.toBoolean(cachedCopilotToggleStatus)); const getCommandbarButtons = (): CommandButtonComponentProps[] => { const executeQueryBtnLabel = selectedQuery ? "Execute Selection" : "Execute Query"; @@ -261,322 +47,45 @@ export const QueryCopilotTab: React.FC = ({ explorer }: Query disabled: true, }; - // Sample Prompts temporary disabled due current design - // const samplePromptsBtn = { - // iconSrc: SamplePromptsIcon, - // iconAlt: "Sample Prompts", - // onCommandClick: () => setIsSamplePromptsOpen(true), - // commandButtonLabel: "Sample Prompts", - // ariaLabel: "Sample Prompts", - // hasPopup: false, - // }; + const toggleCopilotButton = { + iconSrc: QueryCommandIcon, + iconAlt: "Copilot", + onCommandClick: () => { + toggleCopilot(true); + }, + commandButtonLabel: "Copilot", + ariaLabel: "Copilot", + hasPopup: false, + disabled: copilotActive, + }; - return [executeQueryBtn, saveQueryBtn]; - }; - const showTeachingBubble = (): void => { - if (!inputEdited.current) { - setTimeout(() => { - if (!inputEdited.current) { - toggleCopilotTeachingBubbleVisible(); - inputEdited.current = true; - } - }, 30000); - } - }; - - const clearFeedback = () => { - resetButtonState(); - resetQueryResults(); - }; - - const resetButtonState = () => { - setDislikeQuery(false); - setLikeQuery(false); - setShowCallout(false); - }; - - const startGenerateQueryProcess = () => { - updateHistories(); - generateSQLQuery(); - resetButtonState(); + return [executeQueryBtn, saveQueryBtn, toggleCopilotButton]; }; React.useEffect(() => { useCommandBar.getState().setContextButtons(getCommandbarButtons()); - }, [query, selectedQuery]); + }, [query, selectedQuery, copilotActive]); React.useEffect(() => { - showTeachingBubble(); - useTabs.getState().setIsQueryErrorThrown(false); + return () => { + const commandbarButtons = getCommandbarButtons(); + commandbarButtons.pop(); + commandbarButtons.map((props: CommandButtonComponentProps) => (props.disabled = true)); + useCommandBar.getState().setContextButtons(commandbarButtons); + }; }, []); + const toggleCopilot = (toggle: boolean) => { + setCopilotActive(toggle); + localStorage.setItem(`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`, toggle.toString()); + }; + return ( - +
- - - Copilot - - - { - inputEdited.current = true; - setShowSamplePrompts(true); - }} - onKeyDown={(e) => { - if (e.key === "Enter") { - inputEdited.current = true; - startGenerateQueryProcess(); - } - }} - style={{ lineHeight: 30 }} - styles={{ root: { width: "95%" } }} - disabled={isGeneratingQuery} - autoComplete="off" - /> - {copilotTeachingBubbleVisible && ( - - Write a prompt here and Copilot will generate the query for you. You can also choose from our{" "} - { - setShowSamplePrompts(true); - toggleCopilotTeachingBubbleVisible(); - }} - style={{ color: "white", fontWeight: 600 }} - > - sample prompts - {" "} - or write your own query - - )} - startGenerateQueryProcess()} - /> - {isGeneratingQuery && } - {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; - }} - onRenderIcon={() => } - styles={promptStyles} - > - {history} - - ))} - - )} - {filteredSuggestedPrompts?.length > 0 && ( - - - Suggested Prompts - - {filteredSuggestedPrompts.map((prompt) => ( - { - setUserPrompt(prompt.text); - setShowSamplePrompts(false); - inputEdited.current = true; - }} - onRenderIcon={() => } - styles={promptStyles} - > - {prompt.text} - - ))} - - )} - {(filteredHistories?.length > 0 || filteredSuggestedPrompts?.length > 0) && ( - - - - Learn about{" "} - - writing effective prompts - - - - )} - - - )} - - - - - AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.{" "} - - Read preview terms - - {showErrorMessageBar && ( - - 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 && ( - - Provide feedback on the query generated - {showCallout && !hideFeedbackModalForLikedQueries && ( - { - setShowCallout(false); - SubmitFeedback({ - params: { - generatedQuery: generatedQuery, - likeQuery: likeQuery, - description: "", - userPrompt: userPrompt, - }, - explorer: explorer, - }); - }} - directionalHint={DirectionalHint.topCenter} - > - - Thank you. Need to give{" "} - { - setShowCallout(false); - useQueryCopilot.getState().openFeedbackModal(generatedQuery, true, userPrompt); - }} - > - more feedback? - - - - )} - { - setShowCallout(!likeQuery); - setLikeQuery(!likeQuery); - if (dislikeQuery) { - setDislikeQuery(!dislikeQuery); - } - }} - /> - { - if (!dislikeQuery) { - useQueryCopilot.getState().openFeedbackModal(generatedQuery, false, userPrompt); - setLikeQuery(false); - } - setDislikeQuery(!dislikeQuery); - setShowCallout(false); - }} - /> - - - Copy code - - { - setShowDeletePopup(true); - }} - iconProps={{ iconName: "Delete" }} - style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }} - > - Delete code - - + {copilotActive && ( + )} - = ({ explorer }: Query - - {isSamplePromptsOpen && } - {query !== "" && query.trim().length !== 0 && ( - - )} -
); diff --git a/src/Explorer/QueryCopilot/Shared/QueryCopilotClient.ts b/src/Explorer/QueryCopilot/Shared/QueryCopilotClient.ts index 3074e7ac1..9c9f1de61 100644 --- a/src/Explorer/QueryCopilot/Shared/QueryCopilotClient.ts +++ b/src/Explorer/QueryCopilot/Shared/QueryCopilotClient.ts @@ -21,6 +21,7 @@ import { userContext } from "UserContext"; import { queryPagesUntilContentPresent } from "Utils/QueryUtils"; import { useQueryCopilot } from "hooks/useQueryCopilot"; import { useTabs } from "hooks/useTabs"; +import * as StringUtility from "../../../Shared/StringUtility"; export const SendQueryRequest = async ({ userPrompt, @@ -184,15 +185,20 @@ export const QueryDocumentsPerPage = async ( correlationId: useQueryCopilot.getState().correlationId, }); } catch (error) { + const isCopilotActive = StringUtility.toBoolean( + localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`), + ); const errorMessage = getErrorMessage(error); traceFailure(Action.ExecuteQueryGeneratedFromQueryCopilot, { correlationId: useQueryCopilot.getState().correlationId, errorMessage: errorMessage, }); - useQueryCopilot.getState().setErrorMessage(errorMessage); handleError(errorMessage, "executeQueryCopilotTab"); useTabs.getState().setIsQueryErrorThrown(true); - useQueryCopilot.getState().setShowErrorMessageBar(true); + if (isCopilotActive) { + useQueryCopilot.getState().setErrorMessage(errorMessage); + useQueryCopilot.getState().setShowErrorMessageBar(true); + } } finally { useQueryCopilot.getState().setIsExecuting(false); useTabs.getState().setIsTabExecuting(false); diff --git a/src/Explorer/QueryCopilot/__snapshots__/QueryCopilotTab.test.tsx.snap b/src/Explorer/QueryCopilot/__snapshots__/QueryCopilotTab.test.tsx.snap index 3ef6c1eab..64f5be5a8 100644 --- a/src/Explorer/QueryCopilot/__snapshots__/QueryCopilotTab.test.tsx.snap +++ b/src/Explorer/QueryCopilot/__snapshots__/QueryCopilotTab.test.tsx.snap @@ -5,7 +5,6 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] = className="tab-pane" style={ Object { - "padding": 24, "width": "100%", } } @@ -18,103 +17,6 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] = } } > - - - - Copilot - - - - - - - - - AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it. - - - Read preview terms - - - @@ -142,20 +44,6 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] = - - -
`;