From 444f1b66fd0d0fc0d19b03f3240cd3807c3541d7 Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Tue, 27 Jun 2023 13:43:10 -0700 Subject: [PATCH] Query copilot UI part 3 (#1490) --- images/Hint.svg | 3 + images/Recent.svg | 3 + src/Explorer/QueryCopilot/QueryCopilotTab.tsx | 155 +++++++++++++++--- .../QueryCopilotTab.test.tsx.snap | 14 +- 4 files changed, 153 insertions(+), 22 deletions(-) create mode 100644 images/Hint.svg create mode 100644 images/Recent.svg diff --git a/images/Hint.svg b/images/Hint.svg new file mode 100644 index 000000000..9843bef36 --- /dev/null +++ b/images/Hint.svg @@ -0,0 +1,3 @@ + + + diff --git a/images/Recent.svg b/images/Recent.svg new file mode 100644 index 000000000..5295fb567 --- /dev/null +++ b/images/Recent.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Explorer/QueryCopilot/QueryCopilotTab.tsx b/src/Explorer/QueryCopilot/QueryCopilotTab.tsx index 91d392f0d..1a20e57dc 100644 --- a/src/Explorer/QueryCopilot/QueryCopilotTab.tsx +++ b/src/Explorer/QueryCopilot/QueryCopilotTab.tsx @@ -3,7 +3,9 @@ import { FeedOptions } from "@azure/cosmos"; import { Callout, CommandBarButton, + DefaultButton, DirectionalHint, + IButtonStyles, IconButton, Image, Link, @@ -31,6 +33,7 @@ import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdap import { SaveQueryPane } from "Explorer/Panes/SaveQueryPane/SaveQueryPane"; import { submitFeedback } from "Explorer/QueryCopilot/QueryCopilotUtilities"; import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection"; +import { userContext } from "UserContext"; import { queryPagesUntilContentPresent } from "Utils/QueryUtils"; import { useQueryCopilot } from "hooks/useQueryCopilot"; import { useSidePanel } from "hooks/useSidePanel"; @@ -38,6 +41,8 @@ import React, { useState } from "react"; import SplitterLayout from "react-splitter-layout"; import CopilotIcon from "../../../images/Copilot.svg"; import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg"; +import HintIcon from "../../../images/Hint.svg"; +import RecentIcon from "../../../images/Recent.svg"; import SaveQueryIcon from "../../../images/save-cosmos.svg"; import { useTabs } from "../../hooks/useTabs"; @@ -54,12 +59,17 @@ interface GenerateSQLQueryResponse { generateEnd: string; } +const promptStyles: IButtonStyles = { + root: { border: 0, selectors: { ":hover": { outline: "1px dashed #605e5c" } } }, + label: { fontWeight: 400, textAlign: "left", paddingLeft: 8 }, +}; + export const QueryCopilotTab: React.FC = ({ initialInput, explorer, }: QueryCopilotTabProps): JSX.Element => { const hideFeedbackModalForLikedQueries = useQueryCopilot((state) => state.hideFeedbackModalForLikedQueries); - const [userInput, setUserInput] = useState(initialInput || ""); + const [userPrompt, setUserPrompt] = useState(initialInput || ""); const [generatedQuery, setGeneratedQuery] = useState(""); const [query, setQuery] = useState(""); const [selectedQuery, setSelectedQuery] = useState(""); @@ -67,17 +77,28 @@ export const QueryCopilotTab: React.FC = ({ const [isExecuting, setIsExecuting] = useState(false); const [likeQuery, setLikeQuery] = useState(); const [showCallout, setShowCallout] = useState(false); + const [showSamplePrompts, setShowSamplePrompts] = useState(false); const [queryIterator, setQueryIterator] = useState(); const [queryResults, setQueryResults] = useState(); const [errorMessage, setErrorMessage] = useState(""); + const cachedHistoriesString = localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotHistories`); + const cachedHistories = cachedHistoriesString?.split(","); + const [histories, setHistories] = useState(cachedHistories || []); + + const updateHistories = (): void => { + const newHistories = histories.length < 3 ? [userPrompt, ...histories] : [userPrompt, histories[1], histories[2]]; + setHistories(newHistories); + localStorage.setItem(`${userContext.databaseAccount.id}-queryCopilotHistories`, newHistories.join(",")); + }; + const generateSQLQuery = async (): Promise => { try { setIsGeneratingQuery(true); useTabs.getState().setIsTabExecuting(true); const payload = { containerSchema: QueryCopilotSampleContainerSchema, - userPrompt: userInput, + userPrompt: userPrompt, }; const response = await fetch("https://copilotorchestrater.azurewebsites.net/generateSQLQuery", { method: "POST", @@ -89,10 +110,9 @@ export const QueryCopilotTab: React.FC = ({ const generateSQLQueryResponse: GenerateSQLQueryResponse = await response?.json(); if (generateSQLQueryResponse?.sql) { - let query = `-- ${userInput}\r\n`; + let query = `-- **Prompt:** ${userPrompt}\r\n`; if (generateSQLQueryResponse.explanation) { - query += "-- **Explanation of query**\r\n"; - query += `-- ${generateSQLQueryResponse.explanation}\r\n`; + query += `-- **Explanation of query:** ${generateSQLQueryResponse.explanation}\r\n`; } query += generateSQLQueryResponse.sql; setQuery(query); @@ -169,12 +189,6 @@ export const QueryCopilotTab: React.FC = ({ useCommandBar.getState().setContextButtons(getCommandbarButtons()); }, [query, selectedQuery]); - React.useEffect(() => { - if (initialInput) { - generateSQLQuery(); - } - }, []); - return ( @@ -183,19 +197,122 @@ export const QueryCopilotTab: React.FC = ({ setUserInput(newValue)} + id="naturalLanguageInput" + value={userPrompt} + onChange={(_, newValue) => setUserPrompt(newValue)} style={{ lineHeight: 30 }} - styles={{ root: { width: "90%" } }} + styles={{ root: { width: "95%" } }} disabled={isGeneratingQuery} + onClick={() => setShowSamplePrompts(true)} /> generateSQLQuery()} + onClick={() => { + updateHistories(); + generateSQLQuery(); + }} /> {isGeneratingQuery && } + {showSamplePrompts && ( + setShowSamplePrompts(false)} + directionalHint={DirectionalHint.bottomLeftEdge} + > + + {histories?.length > 0 && ( + + + Recent + + {histories.map((history, i) => ( + { + setUserPrompt(history); + setShowSamplePrompts(false); + }} + onRenderIcon={() => } + styles={promptStyles} + > + {history} + + ))} + + )} + + Suggested Prompts + + { + setUserPrompt("Give me all customers whose names start with C"); + setShowSamplePrompts(false); + }} + onRenderIcon={() => } + styles={promptStyles} + > + Give me all customers whose names start with C + + { + setUserPrompt("Show me all customers"); + setShowSamplePrompts(false); + }} + onRenderIcon={() => } + styles={promptStyles} + > + Show me all customers + + { + setUserPrompt("Show me all customers who bought a bike in 2019"); + setShowSamplePrompts(false); + }} + onRenderIcon={() => } + styles={promptStyles} + > + Show me all customers who bought a bike in 2019 + + + + Learn about{" "} + + writing effective prompts + + + + + )} AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.{" "} @@ -212,7 +329,7 @@ export const QueryCopilotTab: React.FC = ({ target="#likeBtn" onDismiss={() => { setShowCallout(false); - submitFeedback({ generatedQuery, likeQuery, description: "", userPrompt: userInput }); + submitFeedback({ generatedQuery, likeQuery, description: "", userPrompt: userPrompt }); }} directionalHint={DirectionalHint.topCenter} > @@ -221,7 +338,7 @@ export const QueryCopilotTab: React.FC = ({ { setShowCallout(false); - useQueryCopilot.getState().openFeedbackModal(generatedQuery, true, userInput); + useQueryCopilot.getState().openFeedbackModal(generatedQuery, true, userPrompt); }} > more feedback? @@ -244,10 +361,10 @@ export const QueryCopilotTab: React.FC = ({ onClick={() => { setLikeQuery(false); setShowCallout(false); - useQueryCopilot.getState().openFeedbackModal(generatedQuery, false, userInput); + useQueryCopilot.getState().openFeedbackModal(generatedQuery, false, userPrompt); }} /> - + Copy code diff --git a/src/Explorer/QueryCopilot/__snapshots__/QueryCopilotTab.test.tsx.snap b/src/Explorer/QueryCopilot/__snapshots__/QueryCopilotTab.test.tsx.snap index e13ee1254..e4087996b 100644 --- a/src/Explorer/QueryCopilot/__snapshots__/QueryCopilotTab.test.tsx.snap +++ b/src/Explorer/QueryCopilot/__snapshots__/QueryCopilotTab.test.tsx.snap @@ -42,7 +42,9 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] = >