[Query Copilot] Polishing UI of Query Copilot (#1513)

* Implementation of filtering suggestion and history

* Error message if query is not received

* Exclamation mark on fail and button disabled

* Changed from hook to const for suggestions

* Test snapshots and formatting updated

* Fix based on comment

---------

Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
This commit is contained in:
Predrag Klepic 2023-07-03 22:49:40 +02:00 committed by GitHub
parent ceed162491
commit 3a961870d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 109 additions and 36 deletions

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 0C8.73438 0 9.44271 0.0963542 10.125 0.289062C10.8073 0.476562 11.4427 0.744792 12.0312 1.09375C12.625 1.44271 13.1641 1.86198 13.6484 2.35156C14.138 2.83594 14.5573 3.375 14.9062 3.96875C15.2552 4.55729 15.5234 5.19271 15.7109 5.875C15.9036 6.55729 16 7.26562 16 8C16 8.73438 15.9036 9.44271 15.7109 10.125C15.5234 10.8073 15.2552 11.4453 14.9062 12.0391C14.5573 12.6276 14.138 13.1667 13.6484 13.6562C13.1641 14.1406 12.625 14.5573 12.0312 14.9062C11.4427 15.2552 10.8073 15.526 10.125 15.7188C9.44271 15.9062 8.73438 16 8 16C7.26562 16 6.55729 15.9062 5.875 15.7188C5.19271 15.526 4.55469 15.2552 3.96094 14.9062C3.3724 14.5573 2.83333 14.1406 2.34375 13.6562C1.85938 13.1667 1.44271 12.6276 1.09375 12.0391C0.744792 11.4453 0.473958 10.8073 0.28125 10.125C0.09375 9.44271 0 8.73438 0 8C0 7.26562 0.09375 6.55729 0.28125 5.875C0.473958 5.19271 0.744792 4.55729 1.09375 3.96875C1.44271 3.375 1.85938 2.83594 2.34375 2.35156C2.83333 1.86198 3.3724 1.44271 3.96094 1.09375C4.55469 0.744792 5.19271 0.476562 5.875 0.289062C6.55729 0.0963542 7.26562 0 8 0ZM8 15C8.64583 15 9.26562 14.9167 9.85938 14.75C10.4583 14.5833 11.0156 14.349 11.5312 14.0469C12.0521 13.7396 12.5234 13.375 12.9453 12.9531C13.3724 12.526 13.737 12.0547 14.0391 11.5391C14.3464 11.0182 14.5833 10.4609 14.75 9.86719C14.9167 9.26823 15 8.64583 15 8C15 7.35417 14.9167 6.73438 14.75 6.14062C14.5833 5.54167 14.3464 4.98438 14.0391 4.46875C13.737 3.94792 13.3724 3.47656 12.9453 3.05469C12.5234 2.6276 12.0521 2.26302 11.5312 1.96094C11.0156 1.65365 10.4583 1.41667 9.85938 1.25C9.26562 1.08333 8.64583 1 8 1C7.35417 1 6.73177 1.08333 6.13281 1.25C5.53906 1.41667 4.98177 1.65365 4.46094 1.96094C3.94531 2.26302 3.47396 2.6276 3.04688 3.05469C2.625 3.47656 2.26042 3.94792 1.95312 4.46875C1.65104 4.98438 1.41667 5.54167 1.25 6.14062C1.08333 6.73438 1 7.35417 1 8C1 8.64583 1.08333 9.26823 1.25 9.86719C1.41667 10.4609 1.65104 11.0182 1.95312 11.5391C2.26042 12.0547 2.625 12.526 3.04688 12.9531C3.47396 13.375 3.94531 13.7396 4.46094 14.0469C4.98177 14.349 5.53906 14.5833 6.13281 14.75C6.73177 14.9167 7.35417 15 8 15ZM11.4609 5.24219L8.71094 8L11.4609 10.7578L10.7578 11.4609L8 8.71094L5.24219 11.4609L4.53906 10.7578L7.28906 8L4.53906 5.24219L5.24219 4.53906L8 7.28906L10.7578 4.53906L11.4609 5.24219Z" fill="#E00B1C"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -47,9 +47,15 @@ import HintIcon from "../../../images/Hint.svg";
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg"; import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
import RecentIcon from "../../../images/Recent.svg"; import RecentIcon from "../../../images/Recent.svg";
import SamplePromptsIcon from "../../../images/SamplePromptsIcon.svg"; import SamplePromptsIcon from "../../../images/SamplePromptsIcon.svg";
import XErrorMessage from "../../../images/X-errorMessage.svg";
import SaveQueryIcon from "../../../images/save-cosmos.svg"; import SaveQueryIcon from "../../../images/save-cosmos.svg";
import { useTabs } from "../../hooks/useTabs"; import { useTabs } from "../../hooks/useTabs";
interface SuggestedPrompt {
id: number;
text: string;
}
interface QueryCopilotTabProps { interface QueryCopilotTabProps {
initialInput: string; initialInput: string;
explorer: Explorer; explorer: Explorer;
@ -89,6 +95,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
const [showDeletePopup, setShowDeletePopup] = useState<boolean>(false); const [showDeletePopup, setShowDeletePopup] = useState<boolean>(false);
const [showFeedbackBar, setShowFeedbackBar] = useState<boolean>(false); const [showFeedbackBar, setShowFeedbackBar] = useState<boolean>(false);
const [showCopyPopup, setshowCopyPopup] = useState<boolean>(false); const [showCopyPopup, setshowCopyPopup] = useState<boolean>(false);
const [showErrorMessageBar, setShowErrorMessageBar] = useState<boolean>(false);
const sampleProps: SamplePromptsProps = { const sampleProps: SamplePromptsProps = {
isSamplePromptsOpen: isSamplePromptsOpen, isSamplePromptsOpen: isSamplePromptsOpen,
@ -116,16 +123,40 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
const cachedHistoriesString = localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotHistories`); const cachedHistoriesString = localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotHistories`);
const cachedHistories = cachedHistoriesString?.split(","); const cachedHistories = cachedHistoriesString?.split(",");
const [histories, setHistories] = useState<string[]>(cachedHistories || []); const [histories, setHistories] = useState<string[]>(cachedHistories || []);
const suggestedPrompts: SuggestedPrompt[] = [
{ id: 1, text: "Give me all customers whose names start with C" },
{ id: 2, text: "Show me all customers" },
{ id: 3, text: "Show me all customers who bought a bike in 2019" },
];
const [filteredHistories, setFilteredHistories] = useState<string[]>(histories);
const [filteredSuggestedPrompts, setFilteredSuggestedPrompts] = useState<SuggestedPrompt[]>(suggestedPrompts);
const handleUserPromptChange = (event: React.ChangeEvent<HTMLInputElement>) => {
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 updateHistories = (): void => {
const newHistories = histories.length < 3 ? [userPrompt, ...histories] : [userPrompt, histories[1], histories[2]]; const newHistories = histories.length < 3 ? [userPrompt, ...histories] : [userPrompt, histories[1], histories[2]];
setHistories(newHistories); setHistories(newHistories);
localStorage.setItem(`${userContext.databaseAccount.id}-queryCopilotHistories`, newHistories.join(",")); localStorage.setItem(`${userContext.databaseAccount.id}-queryCopilotHistories`, newHistories.join(","));
}; };
const generateSQLQuery = async (): Promise<void> => { const generateSQLQuery = async (): Promise<void> => {
try { try {
setIsGeneratingQuery(true); setIsGeneratingQuery(true);
useTabs.getState().setIsTabExecuting(true); useTabs.getState().setIsTabExecuting(true);
useTabs.getState().setIsQueryErrorThrown(false);
const payload = { const payload = {
containerSchema: QueryCopilotSampleContainerSchema, containerSchema: QueryCopilotSampleContainerSchema,
userPrompt: userPrompt, userPrompt: userPrompt,
@ -151,6 +182,8 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
} }
} catch (error) { } catch (error) {
handleError(error, "executeNaturalLanguageQuery"); handleError(error, "executeNaturalLanguageQuery");
useTabs.getState().setIsQueryErrorThrown(true);
setShowErrorMessageBar(true);
throw error; throw error;
} finally { } finally {
setIsGeneratingQuery(false); setIsGeneratingQuery(false);
@ -175,6 +208,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
try { try {
setIsExecuting(true); setIsExecuting(true);
useTabs.getState().setIsTabExecuting(true); useTabs.getState().setIsTabExecuting(true);
useTabs.getState().setIsQueryErrorThrown(false);
const queryResults: QueryResults = await queryPagesUntilContentPresent( const queryResults: QueryResults = await queryPagesUntilContentPresent(
firstItemIndex, firstItemIndex,
async (firstItemIndex: number) => async (firstItemIndex: number) =>
@ -187,6 +221,8 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
setErrorMessage(errorMessage); setErrorMessage(errorMessage);
handleError(errorMessage, "executeQueryCopilotTab"); handleError(errorMessage, "executeQueryCopilotTab");
useTabs.getState().setIsQueryErrorThrown(true);
setShowErrorMessageBar(true);
} finally { } finally {
setIsExecuting(false); setIsExecuting(false);
useTabs.getState().setIsTabExecuting(false); useTabs.getState().setIsTabExecuting(false);
@ -236,19 +272,20 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
<Image src={CopilotIcon} /> <Image src={CopilotIcon} />
<Text style={{ marginLeft: 8, fontWeight: 600, fontSize: 16 }}>Copilot</Text> <Text style={{ marginLeft: 8, fontWeight: 600, fontSize: 16 }}>Copilot</Text>
</Stack> </Stack>
<Stack horizontal verticalAlign="center" style={{ marginTop: 16, width: "100%" }}> <Stack horizontal verticalAlign="center" style={{ marginTop: 16, width: "100%", position: "relative" }}>
<TextField <TextField
id="naturalLanguageInput" id="naturalLanguageInput"
value={userPrompt} value={userPrompt}
onChange={(_, newValue) => setUserPrompt(newValue)} onChange={handleUserPromptChange}
style={{ lineHeight: 30 }} style={{ lineHeight: 30 }}
styles={{ root: { width: "95%" } }} styles={{ root: { width: "95%" } }}
disabled={isGeneratingQuery} disabled={isGeneratingQuery}
autoComplete="off"
onClick={() => setShowSamplePrompts(true)} onClick={() => setShowSamplePrompts(true)}
/> />
<IconButton <IconButton
iconProps={{ iconName: "Send" }} iconProps={{ iconName: "Send" }}
disabled={isGeneratingQuery} disabled={isGeneratingQuery || !userPrompt.trim()}
style={{ marginLeft: 8 }} style={{ marginLeft: 8 }}
onClick={() => { onClick={() => {
updateHistories(); updateHistories();
@ -259,14 +296,16 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
{showSamplePrompts && ( {showSamplePrompts && (
<Callout <Callout
styles={{ root: { minWidth: 400 } }} styles={{ root: { minWidth: 400 } }}
style={{ padding: "8px 0" }}
target="#naturalLanguageInput" target="#naturalLanguageInput"
isBeakVisible={false} isBeakVisible={false}
onDismiss={() => setShowSamplePrompts(false)} onDismiss={() => setShowSamplePrompts(false)}
directionalHintFixed={true}
directionalHint={DirectionalHint.bottomLeftEdge} directionalHint={DirectionalHint.bottomLeftEdge}
alignTargetEdge={true}
gapSpace={4}
> >
<Stack> <Stack>
{histories?.length > 0 && ( {filteredHistories?.length > 0 && (
<Stack> <Stack>
<Text <Text
style={{ style={{
@ -280,7 +319,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
> >
Recent Recent
</Text> </Text>
{histories.map((history, i) => ( {filteredHistories.map((history, i) => (
<DefaultButton <DefaultButton
key={i} key={i}
onClick={() => { onClick={() => {
@ -307,37 +346,27 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
> >
Suggested Prompts Suggested Prompts
</Text> </Text>
<DefaultButton {filteredSuggestedPrompts.map((prompt) => (
onClick={() => { <DefaultButton
setUserPrompt("Give me all customers whose names start with C"); key={prompt.id}
setShowSamplePrompts(false); onClick={() => {
setUserPrompt(prompt.text);
setShowSamplePrompts(false);
}}
onRenderIcon={() => <Image src={HintIcon} />}
styles={promptStyles}
>
{prompt.text}
</DefaultButton>
))}
<Separator
styles={{
root: {
selectors: { "::before": { background: "#E1DFDD" } },
padding: 0,
},
}} }}
onRenderIcon={() => <Image src={HintIcon} />} />
styles={promptStyles}
>
Give me all customers whose names start with C
</DefaultButton>
<DefaultButton
onClick={() => {
setUserPrompt("Show me all customers");
setShowSamplePrompts(false);
}}
onRenderIcon={() => <Image src={HintIcon} />}
styles={promptStyles}
>
Show me all customers
</DefaultButton>
<DefaultButton
onClick={() => {
setUserPrompt("Show me all customers who bought a bike in 2019");
setShowSamplePrompts(false);
}}
onRenderIcon={() => <Image src={HintIcon} />}
styles={promptStyles}
>
Show me all customers who bought a bike in 2019
</DefaultButton>
<Separator styles={{ root: { selectors: { "::before": { background: "#E1DFDD" } }, padding: 0 } }} />
<Text <Text
style={{ style={{
width: "100%", width: "100%",
@ -355,11 +384,22 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
</Callout> </Callout>
)} )}
</Stack> </Stack>
<Text style={{ marginTop: 8, marginBottom: 24, fontSize: 12 }}> <Text style={{ marginTop: 8, marginBottom: 24, fontSize: 12 }}>
AI-generated content can have mistakes. Make sure it&apos;s accurate and appropriate before using it.{" "} AI-generated content can have mistakes. Make sure it&apos;s accurate and appropriate before using it.{" "}
<Link href="" target="_blank"> <Link href="" target="_blank">
Read preview terms Read preview terms
</Link> </Link>
{showErrorMessageBar ? (
<Stack style={{ backgroundColor: "#FEF0F1", padding: "4px 8px" }} horizontal verticalAlign="center">
<Image src={XErrorMessage} style={{ marginRight: "8px" }} />
<Text style={{ fontSize: 12 }}>
We ran into an error and were not able to execute query. Please try again after sometime
</Text>
</Stack>
) : (
<></>
)}
</Text> </Text>
{showFeedbackBar ? ( {showFeedbackBar ? (

View File

@ -35,12 +35,14 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] =
style={ style={
Object { Object {
"marginTop": 16, "marginTop": 16,
"position": "relative",
"width": "100%", "width": "100%",
} }
} }
verticalAlign="center" verticalAlign="center"
> >
<StyledTextFieldBase <StyledTextFieldBase
autoComplete="off"
disabled={false} disabled={false}
id="naturalLanguageInput" id="naturalLanguageInput"
onChange={[Function]} onChange={[Function]}

View File

@ -14,6 +14,7 @@ import ko from "knockout";
import React, { MutableRefObject, useEffect, useRef, useState } from "react"; import React, { MutableRefObject, useEffect, useRef, useState } from "react";
import loadingIcon from "../../../images/circular_loader_black_16x16.gif"; import loadingIcon from "../../../images/circular_loader_black_16x16.gif";
import errorIcon from "../../../images/close-black.svg"; import errorIcon from "../../../images/close-black.svg";
import errorQuery from "../../../images/error_no_outline.svg";
import { useObservable } from "../../hooks/useObservable"; import { useObservable } from "../../hooks/useObservable";
import { ReactTabKind, useTabs } from "../../hooks/useTabs"; import { ReactTabKind, useTabs } from "../../hooks/useTabs";
import TabsBase from "./TabsBase"; import TabsBase from "./TabsBase";
@ -116,6 +117,14 @@ function TabNav({ tab, active, tabKind }: { tab?: Tab; active: boolean; tabKind?
{isTabExecuting(tab, tabKind) && ( {isTabExecuting(tab, tabKind) && (
<img className="loadingIcon" title="Loading" src={loadingIcon} alt="Loading" /> <img className="loadingIcon" title="Loading" src={loadingIcon} alt="Loading" />
)} )}
{isQueryErrorThrown(tab, tabKind) && (
<img
src={errorQuery}
title="Error"
alt="Error"
style={{ marginTop: 4, marginLeft: 4, width: 10, height: 11 }}
/>
)}
</span> </span>
<span className="tabNavText">{useObservable(tab?.tabTitle || getReactTabTitle())}</span> <span className="tabNavText">{useObservable(tab?.tabTitle || getReactTabTitle())}</span>
{tabKind !== ReactTabKind.Home && ( {tabKind !== ReactTabKind.Home && (
@ -220,6 +229,19 @@ const isTabExecuting = (tab?: Tab, tabKind?: ReactTabKind): boolean => {
return false; return false;
}; };
const isQueryErrorThrown = (tab?: Tab, tabKind?: ReactTabKind): boolean => {
if (
!tab?.isExecuting &&
tabKind !== undefined &&
tabKind !== ReactTabKind.Home &&
useTabs.getState()?.isQueryErrorThrown &&
!useTabs.getState()?.isTabExecuting
) {
return true;
}
return false;
};
const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): JSX.Element => { const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): JSX.Element => {
switch (activeReactTab) { switch (activeReactTab) {
case ReactTabKind.Connect: case ReactTabKind.Connect:

View File

@ -12,6 +12,7 @@ interface TabsState {
networkSettingsWarning: string; networkSettingsWarning: string;
queryCopilotTabInitialInput: string; queryCopilotTabInitialInput: string;
isTabExecuting: boolean; isTabExecuting: boolean;
isQueryErrorThrown: boolean;
activateTab: (tab: TabsBase) => void; activateTab: (tab: TabsBase) => void;
activateNewTab: (tab: TabsBase) => void; activateNewTab: (tab: TabsBase) => void;
activateReactTab: (tabkind: ReactTabKind) => void; activateReactTab: (tabkind: ReactTabKind) => void;
@ -26,6 +27,7 @@ interface TabsState {
setNetworkSettingsWarning: (warningMessage: string) => void; setNetworkSettingsWarning: (warningMessage: string) => void;
setQueryCopilotTabInitialInput: (input: string) => void; setQueryCopilotTabInitialInput: (input: string) => void;
setIsTabExecuting: (state: boolean) => void; setIsTabExecuting: (state: boolean) => void;
setIsQueryErrorThrown: (state: boolean) => void;
} }
export enum ReactTabKind { export enum ReactTabKind {
@ -43,6 +45,7 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
networkSettingsWarning: "", networkSettingsWarning: "",
queryCopilotTabInitialInput: "", queryCopilotTabInitialInput: "",
isTabExecuting: false, isTabExecuting: false,
isQueryErrorThrown: false,
activateTab: (tab: TabsBase): void => { activateTab: (tab: TabsBase): void => {
if (get().openedTabs.some((openedTab) => openedTab.tabId === tab.tabId)) { if (get().openedTabs.some((openedTab) => openedTab.tabId === tab.tabId)) {
set({ activeTab: tab, activeReactTab: undefined }); set({ activeTab: tab, activeReactTab: undefined });
@ -157,4 +160,7 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
setIsTabExecuting: (state: boolean) => { setIsTabExecuting: (state: boolean) => {
set({ isTabExecuting: state }); set({ isTabExecuting: state });
}, },
setIsQueryErrorThrown: (state: boolean) => {
set({ isQueryErrorThrown: state });
},
})); }));