2023-06-06 19:43:53 +01:00
|
|
|
/* eslint-disable no-console */
|
|
|
|
import { FeedOptions } from "@azure/cosmos";
|
2023-06-16 08:25:23 +01:00
|
|
|
import {
|
|
|
|
Callout,
|
|
|
|
CommandBarButton,
|
2023-06-27 21:43:10 +01:00
|
|
|
DefaultButton,
|
2023-06-16 08:25:23 +01:00
|
|
|
DirectionalHint,
|
2023-06-27 21:43:10 +01:00
|
|
|
IButtonStyles,
|
2023-06-16 08:25:23 +01:00
|
|
|
IconButton,
|
|
|
|
Image,
|
|
|
|
Link,
|
|
|
|
Separator,
|
|
|
|
Spinner,
|
|
|
|
Stack,
|
|
|
|
Text,
|
|
|
|
TextField,
|
|
|
|
} from "@fluentui/react";
|
|
|
|
import {
|
|
|
|
QueryCopilotSampleContainerId,
|
|
|
|
QueryCopilotSampleContainerSchema,
|
|
|
|
QueryCopilotSampleDatabaseId,
|
|
|
|
} from "Common/Constants";
|
2023-06-06 19:43:53 +01:00
|
|
|
import { getErrorMessage, handleError } from "Common/ErrorHandlingUtils";
|
|
|
|
import { shouldEnableCrossPartitionKey } from "Common/HeadersUtility";
|
|
|
|
import { MinimalQueryIterator } from "Common/IteratorUtilities";
|
|
|
|
import { queryDocuments } from "Common/dataAccess/queryDocuments";
|
|
|
|
import { queryDocumentsPage } from "Common/dataAccess/queryDocumentsPage";
|
|
|
|
import { QueryResults } from "Contracts/ViewModels";
|
|
|
|
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
|
|
|
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
|
|
|
import Explorer from "Explorer/Explorer";
|
|
|
|
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
|
|
|
import { SaveQueryPane } from "Explorer/Panes/SaveQueryPane/SaveQueryPane";
|
2023-06-16 08:25:23 +01:00
|
|
|
import { submitFeedback } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
2023-06-06 19:43:53 +01:00
|
|
|
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
2023-06-27 21:43:10 +01:00
|
|
|
import { userContext } from "UserContext";
|
2023-06-06 19:43:53 +01:00
|
|
|
import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
|
2023-06-16 08:25:23 +01:00
|
|
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
2023-06-06 19:43:53 +01:00
|
|
|
import { useSidePanel } from "hooks/useSidePanel";
|
|
|
|
import React, { useState } from "react";
|
|
|
|
import SplitterLayout from "react-splitter-layout";
|
|
|
|
import CopilotIcon from "../../../images/Copilot.svg";
|
|
|
|
import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
|
2023-06-27 21:43:10 +01:00
|
|
|
import HintIcon from "../../../images/Hint.svg";
|
|
|
|
import RecentIcon from "../../../images/Recent.svg";
|
2023-06-06 19:43:53 +01:00
|
|
|
import SaveQueryIcon from "../../../images/save-cosmos.svg";
|
2023-06-26 08:13:30 +01:00
|
|
|
import { useTabs } from "../../hooks/useTabs";
|
2023-06-06 19:43:53 +01:00
|
|
|
|
|
|
|
interface QueryCopilotTabProps {
|
|
|
|
initialInput: string;
|
|
|
|
explorer: Explorer;
|
|
|
|
}
|
|
|
|
|
2023-06-16 08:25:23 +01:00
|
|
|
interface GenerateSQLQueryResponse {
|
|
|
|
apiVersion: string;
|
|
|
|
sql: string;
|
|
|
|
explanation: string;
|
|
|
|
generateStart: string;
|
|
|
|
generateEnd: string;
|
|
|
|
}
|
|
|
|
|
2023-06-27 21:43:10 +01:00
|
|
|
const promptStyles: IButtonStyles = {
|
|
|
|
root: { border: 0, selectors: { ":hover": { outline: "1px dashed #605e5c" } } },
|
|
|
|
label: { fontWeight: 400, textAlign: "left", paddingLeft: 8 },
|
|
|
|
};
|
|
|
|
|
2023-06-06 19:43:53 +01:00
|
|
|
export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
|
|
|
|
initialInput,
|
|
|
|
explorer,
|
|
|
|
}: QueryCopilotTabProps): JSX.Element => {
|
2023-06-16 08:25:23 +01:00
|
|
|
const hideFeedbackModalForLikedQueries = useQueryCopilot((state) => state.hideFeedbackModalForLikedQueries);
|
2023-06-27 21:43:10 +01:00
|
|
|
const [userPrompt, setUserPrompt] = useState<string>(initialInput || "");
|
2023-06-16 08:25:23 +01:00
|
|
|
const [generatedQuery, setGeneratedQuery] = useState<string>("");
|
2023-06-06 19:43:53 +01:00
|
|
|
const [query, setQuery] = useState<string>("");
|
|
|
|
const [selectedQuery, setSelectedQuery] = useState<string>("");
|
2023-06-16 08:25:23 +01:00
|
|
|
const [isGeneratingQuery, setIsGeneratingQuery] = useState<boolean>(false);
|
2023-06-06 19:43:53 +01:00
|
|
|
const [isExecuting, setIsExecuting] = useState<boolean>(false);
|
2023-06-16 08:25:23 +01:00
|
|
|
const [likeQuery, setLikeQuery] = useState<boolean>();
|
|
|
|
const [showCallout, setShowCallout] = useState<boolean>(false);
|
2023-06-27 21:43:10 +01:00
|
|
|
const [showSamplePrompts, setShowSamplePrompts] = useState<boolean>(false);
|
2023-06-06 19:43:53 +01:00
|
|
|
const [queryIterator, setQueryIterator] = useState<MinimalQueryIterator>();
|
|
|
|
const [queryResults, setQueryResults] = useState<QueryResults>();
|
|
|
|
const [errorMessage, setErrorMessage] = useState<string>("");
|
|
|
|
|
2023-06-27 21:43:10 +01:00
|
|
|
const cachedHistoriesString = localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotHistories`);
|
|
|
|
const cachedHistories = cachedHistoriesString?.split(",");
|
|
|
|
const [histories, setHistories] = useState<string[]>(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(","));
|
|
|
|
};
|
|
|
|
|
2023-06-16 08:25:23 +01:00
|
|
|
const generateSQLQuery = async (): Promise<void> => {
|
|
|
|
try {
|
|
|
|
setIsGeneratingQuery(true);
|
2023-06-26 08:13:30 +01:00
|
|
|
useTabs.getState().setIsTabExecuting(true);
|
2023-06-16 08:25:23 +01:00
|
|
|
const payload = {
|
|
|
|
containerSchema: QueryCopilotSampleContainerSchema,
|
2023-06-27 21:43:10 +01:00
|
|
|
userPrompt: userPrompt,
|
2023-06-16 08:25:23 +01:00
|
|
|
};
|
|
|
|
const response = await fetch("https://copilotorchestrater.azurewebsites.net/generateSQLQuery", {
|
|
|
|
method: "POST",
|
|
|
|
headers: {
|
|
|
|
"content-type": "application/json",
|
|
|
|
},
|
|
|
|
body: JSON.stringify(payload),
|
|
|
|
});
|
|
|
|
|
|
|
|
const generateSQLQueryResponse: GenerateSQLQueryResponse = await response?.json();
|
|
|
|
if (generateSQLQueryResponse?.sql) {
|
2023-06-27 21:43:10 +01:00
|
|
|
let query = `-- **Prompt:** ${userPrompt}\r\n`;
|
2023-06-16 08:25:23 +01:00
|
|
|
if (generateSQLQueryResponse.explanation) {
|
2023-06-27 21:43:10 +01:00
|
|
|
query += `-- **Explanation of query:** ${generateSQLQueryResponse.explanation}\r\n`;
|
2023-06-16 08:25:23 +01:00
|
|
|
}
|
|
|
|
query += generateSQLQueryResponse.sql;
|
|
|
|
setQuery(query);
|
|
|
|
setGeneratedQuery(generateSQLQueryResponse.sql);
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
handleError(error, "executeNaturalLanguageQuery");
|
|
|
|
throw error;
|
|
|
|
} finally {
|
|
|
|
setIsGeneratingQuery(false);
|
2023-06-26 08:13:30 +01:00
|
|
|
useTabs.getState().setIsTabExecuting(false);
|
2023-06-06 19:43:53 +01:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const onExecuteQueryClick = async (): Promise<void> => {
|
|
|
|
const queryToExecute = selectedQuery || query;
|
|
|
|
const queryIterator = queryDocuments(QueryCopilotSampleDatabaseId, QueryCopilotSampleContainerId, queryToExecute, {
|
|
|
|
enableCrossPartitionQuery: shouldEnableCrossPartitionKey(),
|
|
|
|
} as FeedOptions);
|
|
|
|
setQueryIterator(queryIterator);
|
|
|
|
|
|
|
|
setTimeout(async () => {
|
|
|
|
await queryDocumentsPerPage(0, queryIterator);
|
|
|
|
}, 100);
|
|
|
|
};
|
|
|
|
|
|
|
|
const queryDocumentsPerPage = async (firstItemIndex: number, queryIterator: MinimalQueryIterator): Promise<void> => {
|
|
|
|
try {
|
|
|
|
setIsExecuting(true);
|
2023-06-26 08:13:30 +01:00
|
|
|
useTabs.getState().setIsTabExecuting(true);
|
2023-06-06 19:43:53 +01:00
|
|
|
const queryResults: QueryResults = await queryPagesUntilContentPresent(
|
|
|
|
firstItemIndex,
|
|
|
|
async (firstItemIndex: number) =>
|
|
|
|
queryDocumentsPage(QueryCopilotSampleContainerId, queryIterator, firstItemIndex)
|
|
|
|
);
|
|
|
|
|
|
|
|
setQueryResults(queryResults);
|
|
|
|
setErrorMessage("");
|
|
|
|
} catch (error) {
|
|
|
|
const errorMessage = getErrorMessage(error);
|
|
|
|
setErrorMessage(errorMessage);
|
|
|
|
handleError(errorMessage, "executeQueryCopilotTab");
|
|
|
|
} finally {
|
|
|
|
setIsExecuting(false);
|
2023-06-26 08:13:30 +01:00
|
|
|
useTabs.getState().setIsTabExecuting(false);
|
2023-06-06 19:43:53 +01:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const getCommandbarButtons = (): CommandButtonComponentProps[] => {
|
|
|
|
const executeQueryBtnLabel = selectedQuery ? "Execute Selection" : "Execute Query";
|
|
|
|
const executeQueryBtn = {
|
|
|
|
iconSrc: ExecuteQueryIcon,
|
|
|
|
iconAlt: executeQueryBtnLabel,
|
|
|
|
onCommandClick: () => onExecuteQueryClick(),
|
|
|
|
commandButtonLabel: executeQueryBtnLabel,
|
|
|
|
ariaLabel: executeQueryBtnLabel,
|
|
|
|
hasPopup: false,
|
|
|
|
};
|
|
|
|
|
|
|
|
const saveQueryBtn = {
|
|
|
|
iconSrc: SaveQueryIcon,
|
|
|
|
iconAlt: "Save Query",
|
|
|
|
onCommandClick: () =>
|
|
|
|
useSidePanel.getState().openSidePanel("Save Query", <SaveQueryPane explorer={explorer} queryToSave={query} />),
|
|
|
|
commandButtonLabel: "Save Query",
|
|
|
|
ariaLabel: "Save Query",
|
|
|
|
hasPopup: false,
|
|
|
|
};
|
|
|
|
|
|
|
|
return [executeQueryBtn, saveQueryBtn];
|
|
|
|
};
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
useCommandBar.getState().setContextButtons(getCommandbarButtons());
|
2023-06-16 08:25:23 +01:00
|
|
|
}, [query, selectedQuery]);
|
|
|
|
|
2023-06-06 19:43:53 +01:00
|
|
|
return (
|
|
|
|
<Stack className="tab-pane" style={{ padding: 24, width: "100%", height: "100%" }}>
|
|
|
|
<Stack horizontal verticalAlign="center">
|
|
|
|
<Image src={CopilotIcon} />
|
|
|
|
<Text style={{ marginLeft: 8, fontWeight: 600, fontSize: 16 }}>Copilot</Text>
|
|
|
|
</Stack>
|
|
|
|
<Stack horizontal verticalAlign="center" style={{ marginTop: 16, width: "100%" }}>
|
|
|
|
<TextField
|
2023-06-27 21:43:10 +01:00
|
|
|
id="naturalLanguageInput"
|
|
|
|
value={userPrompt}
|
|
|
|
onChange={(_, newValue) => setUserPrompt(newValue)}
|
2023-06-06 19:43:53 +01:00
|
|
|
style={{ lineHeight: 30 }}
|
2023-06-27 21:43:10 +01:00
|
|
|
styles={{ root: { width: "95%" } }}
|
2023-06-16 08:25:23 +01:00
|
|
|
disabled={isGeneratingQuery}
|
2023-06-27 21:43:10 +01:00
|
|
|
onClick={() => setShowSamplePrompts(true)}
|
2023-06-06 19:43:53 +01:00
|
|
|
/>
|
|
|
|
<IconButton
|
|
|
|
iconProps={{ iconName: "Send" }}
|
2023-06-16 08:25:23 +01:00
|
|
|
disabled={isGeneratingQuery}
|
2023-06-06 19:43:53 +01:00
|
|
|
style={{ marginLeft: 8 }}
|
2023-06-27 21:43:10 +01:00
|
|
|
onClick={() => {
|
|
|
|
updateHistories();
|
|
|
|
generateSQLQuery();
|
|
|
|
}}
|
2023-06-06 19:43:53 +01:00
|
|
|
/>
|
2023-06-16 08:25:23 +01:00
|
|
|
{isGeneratingQuery && <Spinner style={{ marginLeft: 8 }} />}
|
2023-06-27 21:43:10 +01:00
|
|
|
{showSamplePrompts && (
|
|
|
|
<Callout
|
|
|
|
styles={{ root: { minWidth: 400 } }}
|
|
|
|
style={{ padding: "8px 0" }}
|
|
|
|
target="#naturalLanguageInput"
|
|
|
|
isBeakVisible={false}
|
|
|
|
onDismiss={() => setShowSamplePrompts(false)}
|
|
|
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
|
|
|
>
|
|
|
|
<Stack>
|
|
|
|
{histories?.length > 0 && (
|
|
|
|
<Stack>
|
|
|
|
<Text
|
|
|
|
style={{
|
|
|
|
width: "100%",
|
|
|
|
fontSize: 14,
|
|
|
|
fontWeight: 600,
|
|
|
|
color: "#0078D4",
|
|
|
|
marginLeft: 16,
|
|
|
|
padding: "4px 0",
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
Recent
|
|
|
|
</Text>
|
|
|
|
{histories.map((history, i) => (
|
|
|
|
<DefaultButton
|
|
|
|
key={i}
|
|
|
|
onClick={() => {
|
|
|
|
setUserPrompt(history);
|
|
|
|
setShowSamplePrompts(false);
|
|
|
|
}}
|
|
|
|
onRenderIcon={() => <Image src={RecentIcon} />}
|
|
|
|
styles={promptStyles}
|
|
|
|
>
|
|
|
|
{history}
|
|
|
|
</DefaultButton>
|
|
|
|
))}
|
|
|
|
</Stack>
|
|
|
|
)}
|
|
|
|
<Text
|
|
|
|
style={{
|
|
|
|
width: "100%",
|
|
|
|
fontSize: 14,
|
|
|
|
fontWeight: 600,
|
|
|
|
color: "#0078D4",
|
|
|
|
marginLeft: 16,
|
|
|
|
padding: "4px 0",
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
Suggested Prompts
|
|
|
|
</Text>
|
|
|
|
<DefaultButton
|
|
|
|
onClick={() => {
|
|
|
|
setUserPrompt("Give me all customers whose names start with C");
|
|
|
|
setShowSamplePrompts(false);
|
|
|
|
}}
|
|
|
|
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%",
|
|
|
|
fontSize: 14,
|
|
|
|
marginLeft: 16,
|
|
|
|
padding: "4px 0",
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
Learn about{" "}
|
|
|
|
<Link target="_blank" href="">
|
|
|
|
writing effective prompts
|
|
|
|
</Link>
|
|
|
|
</Text>
|
|
|
|
</Stack>
|
|
|
|
</Callout>
|
|
|
|
)}
|
2023-06-06 19:43:53 +01:00
|
|
|
</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>
|
|
|
|
</Text>
|
|
|
|
|
2023-06-16 08:25:23 +01:00
|
|
|
<Stack style={{ backgroundColor: "#FFF8F0", padding: "2px 8px" }} horizontal verticalAlign="center">
|
|
|
|
<Text style={{ fontWeight: 600, fontSize: 12 }}>Provide feedback on the query generated</Text>
|
|
|
|
{showCallout && !hideFeedbackModalForLikedQueries && (
|
|
|
|
<Callout
|
|
|
|
style={{ padding: 8 }}
|
|
|
|
target="#likeBtn"
|
|
|
|
onDismiss={() => {
|
|
|
|
setShowCallout(false);
|
2023-06-27 21:43:10 +01:00
|
|
|
submitFeedback({ generatedQuery, likeQuery, description: "", userPrompt: userPrompt });
|
2023-06-16 08:25:23 +01:00
|
|
|
}}
|
|
|
|
directionalHint={DirectionalHint.topCenter}
|
|
|
|
>
|
|
|
|
<Text>
|
|
|
|
Thank you. Need to give{" "}
|
|
|
|
<Link
|
|
|
|
onClick={() => {
|
|
|
|
setShowCallout(false);
|
2023-06-27 21:43:10 +01:00
|
|
|
useQueryCopilot.getState().openFeedbackModal(generatedQuery, true, userPrompt);
|
2023-06-16 08:25:23 +01:00
|
|
|
}}
|
|
|
|
>
|
|
|
|
more feedback?
|
|
|
|
</Link>
|
|
|
|
</Text>
|
|
|
|
</Callout>
|
|
|
|
)}
|
|
|
|
<IconButton
|
|
|
|
id="likeBtn"
|
|
|
|
style={{ marginLeft: 20 }}
|
|
|
|
iconProps={{ iconName: likeQuery === true ? "LikeSolid" : "Like" }}
|
|
|
|
onClick={() => {
|
|
|
|
setLikeQuery(true);
|
|
|
|
setShowCallout(true);
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
<IconButton
|
|
|
|
style={{ margin: "0 10px" }}
|
|
|
|
iconProps={{ iconName: likeQuery === false ? "DislikeSolid" : "Dislike" }}
|
|
|
|
onClick={() => {
|
|
|
|
setLikeQuery(false);
|
|
|
|
setShowCallout(false);
|
2023-06-27 21:43:10 +01:00
|
|
|
useQueryCopilot.getState().openFeedbackModal(generatedQuery, false, userPrompt);
|
2023-06-16 08:25:23 +01:00
|
|
|
}}
|
|
|
|
/>
|
2023-06-27 21:43:10 +01:00
|
|
|
<Separator vertical styles={{ root: { selectors: { "::before": { background: "#E1DFDD" } } } }} />
|
2023-06-16 08:25:23 +01:00
|
|
|
<CommandBarButton iconProps={{ iconName: "Copy" }} style={{ margin: "0 10px", backgroundColor: "#FFF8F0" }}>
|
|
|
|
Copy code
|
|
|
|
</CommandBarButton>
|
|
|
|
<CommandBarButton iconProps={{ iconName: "Delete" }} style={{ backgroundColor: "#FFF8F0" }}>
|
|
|
|
Delete code
|
|
|
|
</CommandBarButton>
|
|
|
|
</Stack>
|
2023-06-06 19:43:53 +01:00
|
|
|
<Stack className="tabPaneContentContainer">
|
|
|
|
<SplitterLayout vertical={true} primaryIndex={0} primaryMinSize={100} secondaryMinSize={200}>
|
|
|
|
<EditorReact
|
|
|
|
language={"sql"}
|
|
|
|
content={query}
|
|
|
|
isReadOnly={false}
|
|
|
|
ariaLabel={"Editing Query"}
|
|
|
|
lineNumbers={"on"}
|
|
|
|
onContentChanged={(newQuery: string) => setQuery(newQuery)}
|
|
|
|
onContentSelected={(selectedQuery: string) => setSelectedQuery(selectedQuery)}
|
|
|
|
/>
|
|
|
|
<QueryResultSection
|
|
|
|
isMongoDB={false}
|
|
|
|
queryEditorContent={selectedQuery || query}
|
|
|
|
error={errorMessage}
|
|
|
|
queryResults={queryResults}
|
|
|
|
isExecuting={isExecuting}
|
|
|
|
executeQueryDocumentsPage={(firstItemIndex: number) => queryDocumentsPerPage(firstItemIndex, queryIterator)}
|
|
|
|
/>
|
|
|
|
</SplitterLayout>
|
|
|
|
</Stack>
|
|
|
|
</Stack>
|
|
|
|
);
|
|
|
|
};
|