[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
5 changed files with 109 additions and 36 deletions

View File

@@ -47,9 +47,15 @@ import HintIcon from "../../../images/Hint.svg";
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
import RecentIcon from "../../../images/Recent.svg";
import SamplePromptsIcon from "../../../images/SamplePromptsIcon.svg";
import XErrorMessage from "../../../images/X-errorMessage.svg";
import SaveQueryIcon from "../../../images/save-cosmos.svg";
import { useTabs } from "../../hooks/useTabs";
interface SuggestedPrompt {
id: number;
text: string;
}
interface QueryCopilotTabProps {
initialInput: string;
explorer: Explorer;
@@ -89,6 +95,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
const [showDeletePopup, setShowDeletePopup] = useState<boolean>(false);
const [showFeedbackBar, setShowFeedbackBar] = useState<boolean>(false);
const [showCopyPopup, setshowCopyPopup] = useState<boolean>(false);
const [showErrorMessageBar, setShowErrorMessageBar] = useState<boolean>(false);
const sampleProps: SamplePromptsProps = {
isSamplePromptsOpen: isSamplePromptsOpen,
@@ -116,16 +123,40 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
const cachedHistoriesString = localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotHistories`);
const cachedHistories = cachedHistoriesString?.split(",");
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 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<void> => {
try {
setIsGeneratingQuery(true);
useTabs.getState().setIsTabExecuting(true);
useTabs.getState().setIsQueryErrorThrown(false);
const payload = {
containerSchema: QueryCopilotSampleContainerSchema,
userPrompt: userPrompt,
@@ -151,6 +182,8 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
}
} catch (error) {
handleError(error, "executeNaturalLanguageQuery");
useTabs.getState().setIsQueryErrorThrown(true);
setShowErrorMessageBar(true);
throw error;
} finally {
setIsGeneratingQuery(false);
@@ -175,6 +208,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
try {
setIsExecuting(true);
useTabs.getState().setIsTabExecuting(true);
useTabs.getState().setIsQueryErrorThrown(false);
const queryResults: QueryResults = await queryPagesUntilContentPresent(
firstItemIndex,
async (firstItemIndex: number) =>
@@ -187,6 +221,8 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
const errorMessage = getErrorMessage(error);
setErrorMessage(errorMessage);
handleError(errorMessage, "executeQueryCopilotTab");
useTabs.getState().setIsQueryErrorThrown(true);
setShowErrorMessageBar(true);
} finally {
setIsExecuting(false);
useTabs.getState().setIsTabExecuting(false);
@@ -236,19 +272,20 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
<Image src={CopilotIcon} />
<Text style={{ marginLeft: 8, fontWeight: 600, fontSize: 16 }}>Copilot</Text>
</Stack>
<Stack horizontal verticalAlign="center" style={{ marginTop: 16, width: "100%" }}>
<Stack horizontal verticalAlign="center" style={{ marginTop: 16, width: "100%", position: "relative" }}>
<TextField
id="naturalLanguageInput"
value={userPrompt}
onChange={(_, newValue) => setUserPrompt(newValue)}
onChange={handleUserPromptChange}
style={{ lineHeight: 30 }}
styles={{ root: { width: "95%" } }}
disabled={isGeneratingQuery}
autoComplete="off"
onClick={() => setShowSamplePrompts(true)}
/>
<IconButton
iconProps={{ iconName: "Send" }}
disabled={isGeneratingQuery}
disabled={isGeneratingQuery || !userPrompt.trim()}
style={{ marginLeft: 8 }}
onClick={() => {
updateHistories();
@@ -259,14 +296,16 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
{showSamplePrompts && (
<Callout
styles={{ root: { minWidth: 400 } }}
style={{ padding: "8px 0" }}
target="#naturalLanguageInput"
isBeakVisible={false}
onDismiss={() => setShowSamplePrompts(false)}
directionalHintFixed={true}
directionalHint={DirectionalHint.bottomLeftEdge}
alignTargetEdge={true}
gapSpace={4}
>
<Stack>
{histories?.length > 0 && (
{filteredHistories?.length > 0 && (
<Stack>
<Text
style={{
@@ -280,7 +319,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
>
Recent
</Text>
{histories.map((history, i) => (
{filteredHistories.map((history, i) => (
<DefaultButton
key={i}
onClick={() => {
@@ -307,37 +346,27 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
>
Suggested Prompts
</Text>
<DefaultButton
onClick={() => {
setUserPrompt("Give me all customers whose names start with C");
setShowSamplePrompts(false);
{filteredSuggestedPrompts.map((prompt) => (
<DefaultButton
key={prompt.id}
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
style={{
width: "100%",
@@ -355,11 +384,22 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
</Callout>
)}
</Stack>
<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.{" "}
<Link href="" target="_blank">
Read preview terms
</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>
{showFeedbackBar ? (

View File

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

View File

@@ -14,6 +14,7 @@ import ko from "knockout";
import React, { MutableRefObject, useEffect, useRef, useState } from "react";
import loadingIcon from "../../../images/circular_loader_black_16x16.gif";
import errorIcon from "../../../images/close-black.svg";
import errorQuery from "../../../images/error_no_outline.svg";
import { useObservable } from "../../hooks/useObservable";
import { ReactTabKind, useTabs } from "../../hooks/useTabs";
import TabsBase from "./TabsBase";
@@ -116,6 +117,14 @@ function TabNav({ tab, active, tabKind }: { tab?: Tab; active: boolean; tabKind?
{isTabExecuting(tab, tabKind) && (
<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 className="tabNavText">{useObservable(tab?.tabTitle || getReactTabTitle())}</span>
{tabKind !== ReactTabKind.Home && (
@@ -220,6 +229,19 @@ const isTabExecuting = (tab?: Tab, tabKind?: ReactTabKind): boolean => {
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 => {
switch (activeReactTab) {
case ReactTabKind.Connect:

View File

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