[Query Copilot] Maintain Query Copilot state when switching tabs (#1559)

* Sample implementation for saving states

* Maintaining Query Copilot state

* Fixing failed PR checks

* Additional changes based on checks

* snapshots updated

* Changes based on merging previous PR

* test mock changed

* Fixing minor bug for close button

* Destruct of queryCopilotState

* passing only function in Tabs component

* Maintaining copilot state with zustand store

* additional test changes

* test snapshot updated

---------

Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
This commit is contained in:
Predrag Klepic 2023-08-03 09:23:31 +02:00 committed by GitHub
parent c873fed7aa
commit fa55d528ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 163 additions and 38 deletions

View File

@ -5,9 +5,7 @@ import { QueryCopilotTab } from "./QueryCopilotTab";
describe("Query copilot tab snapshot test", () => { describe("Query copilot tab snapshot test", () => {
it("should render with initial input", () => { it("should render with initial input", () => {
const wrapper = shallow( const wrapper = shallow(<QueryCopilotTab explorer={new Explorer()} />);
<QueryCopilotTab initialInput="Write a query to return all records in this table" explorer={new Explorer()} />
);
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
}); });

View File

@ -56,7 +56,6 @@ interface SuggestedPrompt {
} }
interface QueryCopilotTabProps { interface QueryCopilotTabProps {
initialInput: string;
explorer: Explorer; explorer: Explorer;
} }
@ -73,32 +72,50 @@ const promptStyles: IButtonStyles = {
label: { fontWeight: 400, textAlign: "left", paddingLeft: 8 }, label: { fontWeight: 400, textAlign: "left", paddingLeft: 8 },
}; };
export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({ explorer }: QueryCopilotTabProps): JSX.Element => {
initialInput,
explorer,
}: QueryCopilotTabProps): JSX.Element => {
const hideFeedbackModalForLikedQueries = useQueryCopilot((state) => state.hideFeedbackModalForLikedQueries);
const [userPrompt, setUserPrompt] = useState<string>(initialInput || "");
const [generatedQuery, setGeneratedQuery] = useState<string>("");
const [generatedQueryComments, setGeneratedQueryComments] = useState<string>("");
const [query, setQuery] = useState<string>("");
const [selectedQuery, setSelectedQuery] = useState<string>("");
const [isGeneratingQuery, setIsGeneratingQuery] = useState<boolean>(false);
const [isExecuting, setIsExecuting] = useState<boolean>(false);
const [likeQuery, setLikeQuery] = useState<boolean>();
const [dislikeQuery, setDislikeQuery] = useState<boolean>();
const [showCallout, setShowCallout] = useState<boolean>(false);
const [showSamplePrompts, setShowSamplePrompts] = useState<boolean>(false);
const [queryIterator, setQueryIterator] = useState<MinimalQueryIterator>();
const [queryResults, setQueryResults] = useState<QueryResults>();
const [errorMessage, setErrorMessage] = useState<string>("");
const [copilotTeachingBubbleVisible, { toggle: toggleCopilotTeachingBubbleVisible }] = useBoolean(false); const [copilotTeachingBubbleVisible, { toggle: toggleCopilotTeachingBubbleVisible }] = useBoolean(false);
const inputEdited = useRef(false); const inputEdited = useRef(false);
const [isSamplePromptsOpen, setIsSamplePromptsOpen] = useState<boolean>(false); const {
const [showDeletePopup, setShowDeletePopup] = useState<boolean>(false); hideFeedbackModalForLikedQueries,
const [showFeedbackBar, setShowFeedbackBar] = useState<boolean>(false); userPrompt,
const [showCopyPopup, setshowCopyPopup] = useState<boolean>(false); setUserPrompt,
const [showErrorMessageBar, setShowErrorMessageBar] = useState<boolean>(false); generatedQuery,
setGeneratedQuery,
query,
setQuery,
selectedQuery,
setSelectedQuery,
isGeneratingQuery,
setIsGeneratingQuery,
isExecuting,
setIsExecuting,
likeQuery,
setLikeQuery,
dislikeQuery,
setDislikeQuery,
showCallout,
setShowCallout,
showSamplePrompts,
setShowSamplePrompts,
queryIterator,
setQueryIterator,
queryResults,
setQueryResults,
errorMessage,
setErrorMessage,
isSamplePromptsOpen,
setIsSamplePromptsOpen,
showDeletePopup,
setShowDeletePopup,
showFeedbackBar,
setShowFeedbackBar,
showCopyPopup,
setshowCopyPopup,
showErrorMessageBar,
setShowErrorMessageBar,
generatedQueryComments,
setGeneratedQueryComments,
} = useQueryCopilot();
const sampleProps: SamplePromptsProps = { const sampleProps: SamplePromptsProps = {
isSamplePromptsOpen: isSamplePromptsOpen, isSamplePromptsOpen: isSamplePromptsOpen,
@ -301,9 +318,10 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
return [executeQueryBtn, saveQueryBtn]; return [executeQueryBtn, saveQueryBtn];
}; };
const showTeachingBubble = (): void => { const showTeachingBubble = (): void => {
if (!inputEdited.current) { const shouldShowTeachingBubble = !inputEdited.current && userPrompt.trim() === "";
if (shouldShowTeachingBubble) {
setTimeout(() => { setTimeout(() => {
if (!inputEdited.current) { if (shouldShowTeachingBubble) {
toggleCopilotTeachingBubbleVisible(); toggleCopilotTeachingBubbleVisible();
inputEdited.current = true; inputEdited.current = true;
} }
@ -328,9 +346,6 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
}, [query, selectedQuery]); }, [query, selectedQuery]);
React.useEffect(() => { React.useEffect(() => {
if (initialInput) {
generateSQLQuery();
}
showTeachingBubble(); showTeachingBubble();
useTabs.getState().setIsQueryErrorThrown(false); useTabs.getState().setIsQueryErrorThrown(false);
}, []); }, []);
@ -517,7 +532,12 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
target="#likeBtn" target="#likeBtn"
onDismiss={() => { onDismiss={() => {
setShowCallout(false); setShowCallout(false);
submitFeedback({ generatedQuery, likeQuery, description: "", userPrompt: userPrompt }); submitFeedback({
generatedQuery: generatedQuery,
likeQuery: likeQuery,
description: "",
userPrompt: userPrompt,
});
}} }}
directionalHint={DirectionalHint.topCenter} directionalHint={DirectionalHint.topCenter}
> >

View File

@ -67,10 +67,10 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] =
}, },
} }
} }
value="Write a query to return all records in this table" value=""
/> />
<CustomizedIconButton <CustomizedIconButton
disabled={false} disabled={true}
iconProps={ iconProps={
Object { Object {
"iconName": "Send", "iconName": "Send",

View File

@ -9,6 +9,7 @@ import { ConnectTab } from "Explorer/Tabs/ConnectTab";
import { PostgresConnectTab } from "Explorer/Tabs/PostgresConnectTab"; import { PostgresConnectTab } from "Explorer/Tabs/PostgresConnectTab";
import { QuickstartTab } from "Explorer/Tabs/QuickstartTab"; import { QuickstartTab } from "Explorer/Tabs/QuickstartTab";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import { useTeachingBubble } from "hooks/useTeachingBubble"; import { useTeachingBubble } from "hooks/useTeachingBubble";
import ko from "knockout"; import ko from "knockout";
import React, { MutableRefObject, useEffect, useRef, useState } from "react"; import React, { MutableRefObject, useEffect, useRef, useState } from "react";
@ -158,6 +159,7 @@ const CloseButton = ({
onClick={(event: React.MouseEvent<HTMLSpanElement, MouseEvent>) => { onClick={(event: React.MouseEvent<HTMLSpanElement, MouseEvent>) => {
event.stopPropagation(); event.stopPropagation();
tab ? tab.onCloseTabButtonClick() : useTabs.getState().closeReactTab(tabKind); tab ? tab.onCloseTabButtonClick() : useTabs.getState().closeReactTab(tabKind);
tabKind === ReactTabKind.QueryCopilot && useQueryCopilot.getState().resetQueryCopilotStates();
}} }}
tabIndex={active ? 0 : undefined} tabIndex={active ? 0 : undefined}
onKeyPress={({ nativeEvent: e }) => tab.onKeyPressClose(undefined, e)} onKeyPress={({ nativeEvent: e }) => tab.onKeyPressClose(undefined, e)}
@ -251,7 +253,7 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
case ReactTabKind.Quickstart: case ReactTabKind.Quickstart:
return <QuickstartTab explorer={explorer} />; return <QuickstartTab explorer={explorer} />;
case ReactTabKind.QueryCopilot: case ReactTabKind.QueryCopilot:
return <QueryCopilotTab initialInput={useTabs.getState().queryCopilotTabInitialInput} explorer={explorer} />; return <QueryCopilotTab explorer={explorer} />;
default: default:
throw Error(`Unsupported tab kind ${ReactTabKind[activeReactTab]}`); throw Error(`Unsupported tab kind ${ReactTabKind[activeReactTab]}`);
} }

View File

@ -1,3 +1,5 @@
import { MinimalQueryIterator } from "Common/IteratorUtilities";
import { QueryResults } from "Contracts/ViewModels";
import { guid } from "Explorer/Tables/Utilities"; import { guid } from "Explorer/Tables/Utilities";
import create, { UseStore } from "zustand"; import create, { UseStore } from "zustand";
@ -8,23 +10,126 @@ interface QueryCopilotState {
showFeedbackModal: boolean; showFeedbackModal: boolean;
hideFeedbackModalForLikedQueries: boolean; hideFeedbackModalForLikedQueries: boolean;
correlationId: string; correlationId: string;
query: string;
selectedQuery: string;
isGeneratingQuery: boolean;
isExecuting: boolean;
dislikeQuery: boolean | undefined;
showCallout: boolean;
showSamplePrompts: boolean;
queryIterator: MinimalQueryIterator | undefined;
queryResults: QueryResults | undefined;
errorMessage: string;
isSamplePromptsOpen: boolean;
showDeletePopup: boolean;
showFeedbackBar: boolean;
showCopyPopup: boolean;
showErrorMessageBar: boolean;
generatedQueryComments: string;
openFeedbackModal: (generatedQuery: string, likeQuery: boolean, userPrompt: string) => void; openFeedbackModal: (generatedQuery: string, likeQuery: boolean, userPrompt: string) => void;
closeFeedbackModal: () => void; closeFeedbackModal: () => void;
setHideFeedbackModalForLikedQueries: (hideFeedbackModalForLikedQueries: boolean) => void; setHideFeedbackModalForLikedQueries: (hideFeedbackModalForLikedQueries: boolean) => void;
refreshCorrelationId: () => void; refreshCorrelationId: () => void;
setUserPrompt: (userPrompt: string) => void;
setQuery: (query: string) => void;
setGeneratedQuery: (generatedQuery: string) => void;
setSelectedQuery: (selectedQuery: string) => void;
setIsGeneratingQuery: (isGeneratingQuery: boolean) => void;
setIsExecuting: (isExecuting: boolean) => void;
setLikeQuery: (likeQuery: boolean) => void;
setDislikeQuery: (dislikeQuery: boolean | undefined) => void;
setShowCallout: (showCallout: boolean) => void;
setShowSamplePrompts: (showSamplePrompts: boolean) => void;
setQueryIterator: (queryIterator: MinimalQueryIterator | undefined) => void;
setQueryResults: (queryResults: QueryResults | undefined) => void;
setErrorMessage: (errorMessage: string) => void;
setIsSamplePromptsOpen: (isSamplePromptsOpen: boolean) => void;
setShowDeletePopup: (showDeletePopup: boolean) => void;
setShowFeedbackBar: (showFeedbackBar: boolean) => void;
setshowCopyPopup: (showCopyPopup: boolean) => void;
setShowErrorMessageBar: (showErrorMessageBar: boolean) => void;
setGeneratedQueryComments: (generatedQueryComments: string) => void;
resetQueryCopilotStates: () => void;
} }
export const useQueryCopilot: UseStore<QueryCopilotState> = create((set) => ({ type QueryCopilotStore = UseStore<QueryCopilotState>;
export const useQueryCopilot: QueryCopilotStore = create((set) => ({
generatedQuery: "", generatedQuery: "",
likeQuery: false, likeQuery: false,
userPrompt: "", userPrompt: "",
showFeedbackModal: false, showFeedbackModal: false,
hideFeedbackModalForLikedQueries: false, hideFeedbackModalForLikedQueries: false,
correlationId: "", correlationId: "",
query: "",
selectedQuery: "",
isGeneratingQuery: false,
isExecuting: false,
dislikeQuery: undefined,
showCallout: false,
showSamplePrompts: false,
queryIterator: undefined,
queryResults: undefined,
errorMessage: "",
isSamplePromptsOpen: false,
showDeletePopup: false,
showFeedbackBar: false,
showCopyPopup: false,
showErrorMessageBar: false,
generatedQueryComments: "",
openFeedbackModal: (generatedQuery: string, likeQuery: boolean, userPrompt: string) => openFeedbackModal: (generatedQuery: string, likeQuery: boolean, userPrompt: string) =>
set({ generatedQuery, likeQuery, userPrompt, showFeedbackModal: true }), set({ generatedQuery, likeQuery, userPrompt, showFeedbackModal: true }),
closeFeedbackModal: () => set({ generatedQuery: "", likeQuery: false, userPrompt: "", showFeedbackModal: false }), closeFeedbackModal: () => set({ generatedQuery: "", likeQuery: false, userPrompt: "", showFeedbackModal: false }),
setHideFeedbackModalForLikedQueries: (hideFeedbackModalForLikedQueries: boolean) => setHideFeedbackModalForLikedQueries: (hideFeedbackModalForLikedQueries: boolean) =>
set({ hideFeedbackModalForLikedQueries }), set({ hideFeedbackModalForLikedQueries }),
refreshCorrelationId: () => set({ correlationId: guid() }), refreshCorrelationId: () => set({ correlationId: guid() }),
setUserPrompt: (userPrompt: string) => set({ userPrompt }),
setQuery: (query: string) => set({ query }),
setGeneratedQuery: (generatedQuery: string) => set({ generatedQuery }),
setSelectedQuery: (selectedQuery: string) => set({ selectedQuery }),
setIsGeneratingQuery: (isGeneratingQuery: boolean) => set({ isGeneratingQuery }),
setIsExecuting: (isExecuting: boolean) => set({ isExecuting }),
setLikeQuery: (likeQuery: boolean) => set({ likeQuery }),
setDislikeQuery: (dislikeQuery: boolean | undefined) => set({ dislikeQuery }),
setShowCallout: (showCallout: boolean) => set({ showCallout }),
setShowSamplePrompts: (showSamplePrompts: boolean) => set({ showSamplePrompts }),
setQueryIterator: (queryIterator: MinimalQueryIterator | undefined) => set({ queryIterator }),
setQueryResults: (queryResults: QueryResults | undefined) => set({ queryResults }),
setErrorMessage: (errorMessage: string) => set({ errorMessage }),
setIsSamplePromptsOpen: (isSamplePromptsOpen: boolean) => set({ isSamplePromptsOpen }),
setShowDeletePopup: (showDeletePopup: boolean) => set({ showDeletePopup }),
setShowFeedbackBar: (showFeedbackBar: boolean) => set({ showFeedbackBar }),
setshowCopyPopup: (showCopyPopup: boolean) => set({ showCopyPopup }),
setShowErrorMessageBar: (showErrorMessageBar: boolean) => set({ showErrorMessageBar }),
setGeneratedQueryComments: (generatedQueryComments: string) => set({ generatedQueryComments }),
resetQueryCopilotStates: () => {
set((state) => ({
...state,
generatedQuery: "",
likeQuery: false,
userPrompt: "",
showFeedbackModal: false,
hideFeedbackModalForLikedQueries: false,
correlationId: "",
query: "",
selectedQuery: "",
isGeneratingQuery: false,
isExecuting: false,
dislikeQuery: undefined,
showCallout: false,
showSamplePrompts: false,
queryIterator: undefined,
queryResults: undefined,
errorMessage: "",
isSamplePromptsOpen: false,
showDeletePopup: false,
showFeedbackBar: false,
showCopyPopup: false,
showErrorMessageBar: false,
generatedQueryComments: "",
}));
},
})); }));