mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-19 08:51:24 +00:00
[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:
@@ -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'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 ? (
|
||||
|
||||
@@ -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]}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 });
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user