Copilot user db (#1672)

* Implement copilot for user database

* Fix minor bugs

* fix bugs

* Add user database copilot

* Add placeholder text on copilot

* Add AFEC adn killswitch

* Add new v2 sampledatabase endpoint

* Add telemetry

* fix telemetry bug

* Add query edited telemetry

* add authorization header

* Add back to the staging env for phoenix

* point to stage for phoenix

* Preview commit for test env

* Preview link for staging

* change the staging url

* fix lint, unit tests

* fix lint, unit tests

* fix formatting
This commit is contained in:
sunghyunkang1111 2023-11-09 11:55:25 -06:00 committed by GitHub
parent a5e1b37ba6
commit 0e124f4881
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 911 additions and 252 deletions

View File

@ -171,6 +171,7 @@ export class Areas {
public static Tab: string = "Tab"; public static Tab: string = "Tab";
public static ShareDialog: string = "Share Access Dialog"; public static ShareDialog: string = "Share Access Dialog";
public static Notebook: string = "Notebook"; public static Notebook: string = "Notebook";
public static Copilot: string = "Copilot";
} }
export class HttpHeaders { export class HttpHeaders {

View File

@ -1,3 +1,4 @@
import { JunoEndpoints } from "Common/Constants";
import { import {
allowedAadEndpoints, allowedAadEndpoints,
allowedArcadiaEndpoints, allowedArcadiaEndpoints,
@ -78,7 +79,7 @@ let configContext: Readonly<ConfigContext> = {
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: "dev.azuresynapse.net", ARCADIA_LIVY_ENDPOINT_DNS_ZONE: "dev.azuresynapse.net",
GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1189306 GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1189306
GITHUB_TEST_ENV_CLIENT_ID: "b63fc8cbf87fd3c6e2eb", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1777772 GITHUB_TEST_ENV_CLIENT_ID: "b63fc8cbf87fd3c6e2eb", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1777772
JUNO_ENDPOINT: "https://tools.cosmos.azure.com", JUNO_ENDPOINT: JunoEndpoints.Prod,
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com", BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
isTerminalEnabled: false, isTerminalEnabled: false,
isPhoenixEnabled: false, isPhoenixEnabled: false,

View File

@ -457,8 +457,11 @@ export interface ContainerInfo {
} }
export interface IProvisionData { export interface IProvisionData {
cosmosEndpoint: string; cosmosEndpoint?: string;
poolId: string; poolId: string;
databaseId?: string;
containerId?: string;
mode?: string;
} }
export interface IContainerData { export interface IContainerData {
@ -601,3 +604,14 @@ export enum PhoenixErrorType {
PhoenixFlightFallback = "PhoenixFlightFallback", PhoenixFlightFallback = "PhoenixFlightFallback",
UserMissingPermissionsError = "UserMissingPermissionsError", UserMissingPermissionsError = "UserMissingPermissionsError",
} }
export interface CopilotEnabledConfiguration {
isEnabled: boolean;
}
export interface FeatureRegistration {
name: string;
properties: {
state: string;
};
}

View File

@ -48,7 +48,7 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
public componentDidUpdate(previous: EditorReactProps) { public componentDidUpdate(previous: EditorReactProps) {
if (this.props.content !== previous.content) { if (this.props.content !== previous.content) {
this.editor.setValue(this.props.content); this.editor?.setValue(this.props.content);
} }
} }
@ -111,7 +111,7 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
this.rootNode.innerHTML = ""; this.rootNode.innerHTML = "";
const monaco = await loadMonaco(); const monaco = await loadMonaco();
createCallback(monaco.editor.create(this.rootNode, options)); createCallback(monaco?.editor?.create(this.rootNode, options));
if (this.rootNode.innerHTML) { if (this.rootNode.innerHTML) {
this.setState({ this.setState({

View File

@ -3,6 +3,7 @@ import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
import { sendMessage } from "Common/MessageHandler"; import { sendMessage } from "Common/MessageHandler";
import { Platform, configContext } from "ConfigContext"; import { Platform, configContext } from "ConfigContext";
import { MessageTypes } from "Contracts/ExplorerContracts"; import { MessageTypes } from "Contracts/ExplorerContracts";
import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { IGalleryItem } from "Juno/JunoClient"; import { IGalleryItem } from "Juno/JunoClient";
import { requestDatabaseResourceTokens } from "Platform/Fabric/FabricUtil"; import { requestDatabaseResourceTokens } from "Platform/Fabric/FabricUtil";
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation"; import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation";
@ -92,7 +93,7 @@ export default class Explorer {
}; };
private static readonly MaxNbDatabasesToAutoExpand = 5; private static readonly MaxNbDatabasesToAutoExpand = 5;
private phoenixClient: PhoenixClient; public phoenixClient: PhoenixClient;
constructor() { constructor() {
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, { const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
dataExplorerArea: Constants.Areas.ResourceTree, dataExplorerArea: Constants.Areas.ResourceTree,
@ -411,7 +412,7 @@ export default class Explorer {
this._isInitializingNotebooks = false; this._isInitializingNotebooks = false;
} }
public async allocateContainer(poolId: PoolIdType): Promise<void> { public async allocateContainer(poolId: PoolIdType, mode?: string): Promise<void> {
const shouldUseNotebookStates = poolId === PoolIdType.DefaultPoolId ? true : false; const shouldUseNotebookStates = poolId === PoolIdType.DefaultPoolId ? true : false;
const notebookServerInfo = shouldUseNotebookStates const notebookServerInfo = shouldUseNotebookStates
? useNotebook.getState().notebookServerInfo ? useNotebook.getState().notebookServerInfo
@ -425,10 +426,6 @@ export default class Explorer {
(notebookServerInfo === undefined || (notebookServerInfo === undefined ||
(notebookServerInfo && notebookServerInfo.notebookServerEndpoint === undefined)) (notebookServerInfo && notebookServerInfo.notebookServerEndpoint === undefined))
) { ) {
const provisionData: IProvisionData = {
cosmosEndpoint: userContext?.databaseAccount?.properties?.documentEndpoint,
poolId: shouldUseNotebookStates ? undefined : poolId,
};
const connectionStatus: ContainerConnectionInfo = { const connectionStatus: ContainerConnectionInfo = {
status: ConnectionStatusType.Connecting, status: ConnectionStatusType.Connecting,
}; };
@ -436,14 +433,26 @@ export default class Explorer {
shouldUseNotebookStates && useNotebook.getState().setConnectionInfo(connectionStatus); shouldUseNotebookStates && useNotebook.getState().setConnectionInfo(connectionStatus);
let connectionInfo; let connectionInfo;
let provisionData: IProvisionData;
try { try {
TelemetryProcessor.traceStart(Action.PhoenixConnection, { TelemetryProcessor.traceStart(Action.PhoenixConnection, {
dataExplorerArea: Areas.Notebook, dataExplorerArea: Areas.Notebook,
}); });
shouldUseNotebookStates if (shouldUseNotebookStates) {
? useNotebook.getState().setIsAllocating(true) useNotebook.getState().setIsAllocating(true);
: useQueryCopilot.getState().setIsAllocatingContainer(true); provisionData = {
cosmosEndpoint: userContext?.databaseAccount?.properties?.documentEndpoint,
poolId: undefined,
};
} else {
useQueryCopilot.getState().setIsAllocatingContainer(true);
provisionData = {
poolId: poolId,
databaseId: useTabs.getState().activeTab.collection.databaseId,
containerId: useTabs.getState().activeTab.collection.id(),
mode: mode,
};
}
connectionInfo = await this.phoenixClient.allocateContainer(provisionData); connectionInfo = await this.phoenixClient.allocateContainer(provisionData);
if (!connectionInfo?.data?.phoenixServiceUrl) { if (!connectionInfo?.data?.phoenixServiceUrl) {
throw new Error(`PhoenixServiceUrl is invalid!`); throw new Error(`PhoenixServiceUrl is invalid!`);
@ -459,19 +468,21 @@ export default class Explorer {
error: getErrorMessage(error), error: getErrorMessage(error),
errorStack: getErrorStack(error), errorStack: getErrorStack(error),
}); });
connectionStatus.status = ConnectionStatusType.Failed; if (shouldUseNotebookStates) {
shouldUseNotebookStates connectionStatus.status = ConnectionStatusType.Failed;
? useNotebook.getState().resetContainerConnection(connectionStatus) shouldUseNotebookStates
: useQueryCopilot.getState().resetContainerConnection(); ? useNotebook.getState().resetContainerConnection(connectionStatus)
if (error?.status === HttpStatusCodes.Forbidden && error.message) { : useQueryCopilot.getState().resetContainerConnection();
useDialog.getState().showOkModalDialog("Connection Failed", `${error.message}`); if (error?.status === HttpStatusCodes.Forbidden && error.message) {
} else { useDialog.getState().showOkModalDialog("Connection Failed", `${error.message}`);
useDialog } else {
.getState() useDialog
.showOkModalDialog( .getState()
"Connection Failed", .showOkModalDialog(
"We are unable to connect to the temporary workspace. Please try again in a few minutes. If the error persists, file a support ticket.", "Connection Failed",
); "We are unable to connect to the temporary workspace. Please try again in a few minutes. If the error persists, file a support ticket.",
);
}
} }
throw error; throw error;
} finally { } finally {
@ -485,11 +496,11 @@ export default class Explorer {
} }
} }
private async setNotebookInfo( public async setNotebookInfo(
shouldUseNotebookStates: boolean, shouldUseNotebookStates: boolean,
connectionInfo: IResponse<IPhoenixServiceInfo>, connectionInfo: IResponse<IPhoenixServiceInfo>,
connectionStatus: DataModels.ContainerConnectionInfo, connectionStatus: DataModels.ContainerConnectionInfo,
) { ): Promise<void> {
const containerData = { const containerData = {
forwardingId: connectionInfo.data.forwardingId, forwardingId: connectionInfo.data.forwardingId,
dbAccountName: userContext.databaseAccount.name, dbAccountName: userContext.databaseAccount.name,
@ -510,6 +521,7 @@ export default class Explorer {
shouldUseNotebookStates shouldUseNotebookStates
? useNotebook.getState().setNotebookServerInfo(noteBookServerInfo) ? useNotebook.getState().setNotebookServerInfo(noteBookServerInfo)
: useQueryCopilot.getState().setNotebookServerInfo(noteBookServerInfo); : useQueryCopilot.getState().setNotebookServerInfo(noteBookServerInfo);
shouldUseNotebookStates && shouldUseNotebookStates &&
this.notebookManager?.notebookClient this.notebookManager?.notebookClient
.getMemoryUsage() .getMemoryUsage()
@ -1372,6 +1384,16 @@ export default class Explorer {
await this.refreshSampleData(); await this.refreshSampleData();
} }
public async configureCopilot(): Promise<void> {
if (userContext.apiType !== "SQL") {
return;
}
const copilotEnabled = await getCopilotEnabled();
const copilotUserDBEnabled = await isCopilotFeatureRegistered(userContext.subscriptionId);
useQueryCopilot.getState().setCopilotEnabled(copilotEnabled);
useQueryCopilot.getState().setCopilotUserDBEnabled(copilotUserDBEnabled);
}
public async refreshSampleData(): Promise<void> { public async refreshSampleData(): Promise<void> {
if (!userContext.sampleDataConnectionInfo) { if (!userContext.sampleDataConnectionInfo) {
return; return;

View File

@ -1,6 +1,3 @@
import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
import { ReactTabKind, useTabs } from "hooks/useTabs";
import * as React from "react"; import * as React from "react";
import AddCollectionIcon from "../../../../images/AddCollection.svg"; import AddCollectionIcon from "../../../../images/AddCollection.svg";
import AddDatabaseIcon from "../../../../images/AddDatabase.svg"; import AddDatabaseIcon from "../../../../images/AddDatabase.svg";
@ -337,13 +334,8 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
iconSrc: AddSqlQueryIcon, iconSrc: AddSqlQueryIcon,
iconAlt: label, iconAlt: label,
onCommandClick: () => { onCommandClick: () => {
if (useSelectedNode.getState().isQueryCopilotCollectionSelected()) { const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot); selectedCollection && selectedCollection.onNewQueryClick(selectedCollection);
traceOpen(Action.OpenQueryCopilotFromNewQuery, { apiType: userContext.apiType });
} else {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection);
}
}, },
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,

View File

@ -6,6 +6,7 @@ import {
IDropdownOption, IDropdownOption,
IDropdownStyles, IDropdownStyles,
} from "@fluentui/react"; } from "@fluentui/react";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import * as React from "react"; import * as React from "react";
import _ from "underscore"; import _ from "underscore";
import ChevronDownIcon from "../../../../images/Chevron_down.svg"; import ChevronDownIcon from "../../../../images/Chevron_down.svg";
@ -57,7 +58,11 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
}, },
onClick: (ev?: React.MouseEvent<HTMLElement, MouseEvent> | React.KeyboardEvent<HTMLElement>) => { onClick: (ev?: React.MouseEvent<HTMLElement, MouseEvent> | React.KeyboardEvent<HTMLElement>) => {
btn.onCommandClick(ev); btn.onCommandClick(ev);
TelemetryProcessor.trace(Action.ClickCommandBarButton, ActionModifiers.Mark, { label }); let copilotEnabled = false;
if (useQueryCopilot.getState().copilotEnabled && useQueryCopilot.getState().copilotUserDBEnabled) {
copilotEnabled = useQueryCopilot.getState().copilotEnabledforExecution;
}
TelemetryProcessor.trace(Action.ClickCommandBarButton, ActionModifiers.Mark, { label, copilotEnabled });
}, },
key: `${btn.commandButtonLabel}${index}`, key: `${btn.commandButtonLabel}${index}`,
text: label, text: label,

View File

@ -1,10 +1,10 @@
import { Checkbox, ChoiceGroup, DefaultButton, IconButton, PrimaryButton, TextField } from "@fluentui/react"; import { Checkbox, ChoiceGroup, DefaultButton, IconButton, PrimaryButton, TextField } from "@fluentui/react";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal"; import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext";
import { SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient"; import { SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { getUserEmail } from "Utils/UserUtils"; import { getUserEmail } from "Utils/UserUtils";
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react"; import React from "react";
jest.mock("Utils/UserUtils"); jest.mock("Utils/UserUtils");
@ -13,21 +13,49 @@ jest.mock("Utils/UserUtils");
jest.mock("Explorer/QueryCopilot/Shared/QueryCopilotClient"); jest.mock("Explorer/QueryCopilot/Shared/QueryCopilotClient");
SubmitFeedback as jest.Mock; SubmitFeedback as jest.Mock;
jest.mock("Explorer/QueryCopilot/QueryCopilotContext");
const mockUseCopilotStore = useCopilotStore as jest.Mock;
const mockReturnValue = {
generatedQuery: "test query",
userPrompt: "test prompt",
likeQuery: false,
showFeedbackModal: false,
closeFeedbackModal: jest.fn,
setHideFeedbackModalForLikedQueries: jest.fn,
};
describe("Query Copilot Feedback Modal snapshot test", () => { describe("Query Copilot Feedback Modal snapshot test", () => {
beforeEach(() => { beforeEach(() => {
mockUseCopilotStore.mockReturnValue(mockReturnValue);
jest.clearAllMocks(); jest.clearAllMocks();
}); });
it("shoud render and match snapshot", () => { it("shoud render and match snapshot", () => {
useQueryCopilot.getState().openFeedbackModal("test query", false, "test prompt"); mockUseCopilotStore.mockReturnValue({
...mockReturnValue,
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />); showFeedbackModal: true,
});
const wrapper = shallow(
<QueryCopilotFeedbackModal
explorer={new Explorer()}
databaseId="CopilotUserDb"
containerId="CopilotUserContainer"
mode="User"
/>,
);
expect(wrapper.props().isOpen).toBeTruthy(); expect(wrapper.props().isOpen).toBeTruthy();
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
it("should close on cancel click", () => { it("should close on cancel click", () => {
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />); const wrapper = shallow(
<QueryCopilotFeedbackModal
explorer={new Explorer()}
databaseId="CopilotUserDb"
containerId="CopilotUserContainer"
mode="User"
/>,
);
const cancelButton = wrapper.find(IconButton); const cancelButton = wrapper.find(IconButton);
cancelButton.simulate("click"); cancelButton.simulate("click");
@ -38,7 +66,14 @@ describe("Query Copilot Feedback Modal snapshot test", () => {
}); });
it("should get user unput", () => { it("should get user unput", () => {
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />); const wrapper = shallow(
<QueryCopilotFeedbackModal
explorer={new Explorer()}
databaseId="CopilotUserDb"
containerId="CopilotUserContainer"
mode="User"
/>,
);
const testUserInput = "test user input"; const testUserInput = "test user input";
const userInput = wrapper.find(TextField).first(); const userInput = wrapper.find(TextField).first();
@ -49,7 +84,14 @@ describe("Query Copilot Feedback Modal snapshot test", () => {
}); });
it("should record user contact choice no", () => { it("should record user contact choice no", () => {
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />); const wrapper = shallow(
<QueryCopilotFeedbackModal
explorer={new Explorer()}
databaseId="CopilotUserDb"
containerId="CopilotUserContainer"
mode="User"
/>,
);
const contactAllowed = wrapper.find(ChoiceGroup); const contactAllowed = wrapper.find(ChoiceGroup);
contactAllowed.simulate("change", {}, { key: "no" }); contactAllowed.simulate("change", {}, { key: "no" });
@ -60,7 +102,14 @@ describe("Query Copilot Feedback Modal snapshot test", () => {
}); });
it("should record user contact choice yes", () => { it("should record user contact choice yes", () => {
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />); const wrapper = shallow(
<QueryCopilotFeedbackModal
explorer={new Explorer()}
databaseId="CopilotUserDb"
containerId="CopilotUserContainer"
mode="User"
/>,
);
const contactAllowed = wrapper.find(ChoiceGroup); const contactAllowed = wrapper.find(ChoiceGroup);
contactAllowed.simulate("change", {}, { key: "yes" }); contactAllowed.simulate("change", {}, { key: "yes" });
@ -71,7 +120,14 @@ describe("Query Copilot Feedback Modal snapshot test", () => {
}); });
it("should not render dont show again button", () => { it("should not render dont show again button", () => {
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />); const wrapper = shallow(
<QueryCopilotFeedbackModal
explorer={new Explorer()}
databaseId="CopilotUserDb"
containerId="CopilotUserContainer"
mode="User"
/>,
);
const dontShowAgain = wrapper.find(Checkbox); const dontShowAgain = wrapper.find(Checkbox);
@ -80,8 +136,19 @@ describe("Query Copilot Feedback Modal snapshot test", () => {
}); });
it("should render dont show again button and check it ", () => { it("should render dont show again button and check it ", () => {
useQueryCopilot.getState().openFeedbackModal("test query", true, "test prompt"); mockUseCopilotStore.mockReturnValue({
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />); ...mockReturnValue,
showFeedbackModal: true,
likeQuery: true,
});
const wrapper = shallow(
<QueryCopilotFeedbackModal
explorer={new Explorer()}
databaseId="CopilotUserDb"
containerId="CopilotUserContainer"
mode="User"
/>,
);
const dontShowAgain = wrapper.find(Checkbox); const dontShowAgain = wrapper.find(Checkbox);
dontShowAgain.simulate("change", {}, true); dontShowAgain.simulate("change", {}, true);
@ -92,7 +159,14 @@ describe("Query Copilot Feedback Modal snapshot test", () => {
}); });
it("should cancel submission", () => { it("should cancel submission", () => {
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />); const wrapper = shallow(
<QueryCopilotFeedbackModal
explorer={new Explorer()}
databaseId="CopilotUserDb"
containerId="CopilotUserContainer"
mode="User"
/>,
);
const cancelButton = wrapper.find(DefaultButton); const cancelButton = wrapper.find(DefaultButton);
cancelButton.simulate("click"); cancelButton.simulate("click");
@ -104,7 +178,14 @@ describe("Query Copilot Feedback Modal snapshot test", () => {
it("should not submit submission if required description field is null", () => { it("should not submit submission if required description field is null", () => {
const explorer = new Explorer(); const explorer = new Explorer();
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={explorer} />); const wrapper = shallow(
<QueryCopilotFeedbackModal
explorer={explorer}
databaseId="CopilotUserDb"
containerId="CopilotUserContainer"
mode="User"
/>,
);
const submitButton = wrapper.find(PrimaryButton); const submitButton = wrapper.find(PrimaryButton);
submitButton.simulate("click"); submitButton.simulate("click");
@ -114,9 +195,15 @@ describe("Query Copilot Feedback Modal snapshot test", () => {
}); });
it("should submit submission", () => { it("should submit submission", () => {
useQueryCopilot.getState().openFeedbackModal("test query", false, "test prompt");
const explorer = new Explorer(); const explorer = new Explorer();
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={explorer} />); const wrapper = shallow(
<QueryCopilotFeedbackModal
explorer={explorer}
databaseId="CopilotUserDb"
containerId="CopilotUserContainer"
mode="User"
/>,
);
const submitButton = wrapper.find("form"); const submitButton = wrapper.find("form");
submitButton.simulate("submit"); submitButton.simulate("submit");
@ -124,6 +211,9 @@ describe("Query Copilot Feedback Modal snapshot test", () => {
expect(SubmitFeedback).toHaveBeenCalledTimes(1); expect(SubmitFeedback).toHaveBeenCalledTimes(1);
expect(SubmitFeedback).toHaveBeenCalledWith({ expect(SubmitFeedback).toHaveBeenCalledWith({
containerId: "CopilotUserContainer",
databaseId: "CopilotUserDb",
mode: "User",
params: { params: {
likeQuery: false, likeQuery: false,
generatedQuery: "test query", generatedQuery: "test query",

View File

@ -11,12 +11,22 @@ import {
TextField, TextField,
} from "@fluentui/react"; } from "@fluentui/react";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext";
import { SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient"; import { SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react"; import React from "react";
import { getUserEmail } from "../../../Utils/UserUtils"; import { getUserEmail } from "../../../Utils/UserUtils";
export const QueryCopilotFeedbackModal = ({ explorer }: { explorer: Explorer }): JSX.Element => { export const QueryCopilotFeedbackModal = ({
explorer,
databaseId,
containerId,
mode,
}: {
explorer: Explorer;
databaseId: string;
containerId: string;
mode: string;
}): JSX.Element => {
const { const {
generatedQuery, generatedQuery,
userPrompt, userPrompt,
@ -24,7 +34,7 @@ export const QueryCopilotFeedbackModal = ({ explorer }: { explorer: Explorer }):
showFeedbackModal, showFeedbackModal,
closeFeedbackModal, closeFeedbackModal,
setHideFeedbackModalForLikedQueries, setHideFeedbackModalForLikedQueries,
} = useQueryCopilot(); } = useCopilotStore();
const [isContactAllowed, setIsContactAllowed] = React.useState<boolean>(false); const [isContactAllowed, setIsContactAllowed] = React.useState<boolean>(false);
const [description, setDescription] = React.useState<string>(""); const [description, setDescription] = React.useState<string>("");
const [doNotShowAgainChecked, setDoNotShowAgainChecked] = React.useState<boolean>(false); const [doNotShowAgainChecked, setDoNotShowAgainChecked] = React.useState<boolean>(false);
@ -35,7 +45,10 @@ export const QueryCopilotFeedbackModal = ({ explorer }: { explorer: Explorer }):
setHideFeedbackModalForLikedQueries(doNotShowAgainChecked); setHideFeedbackModalForLikedQueries(doNotShowAgainChecked);
SubmitFeedback({ SubmitFeedback({
params: { generatedQuery, likeQuery, description, userPrompt, contact }, params: { generatedQuery, likeQuery, description, userPrompt, contact },
explorer: explorer, explorer,
databaseId,
containerId,
mode: mode,
}); });
}; };

View File

@ -1,7 +1,6 @@
import { IconButton, Image, Link, Modal, PrimaryButton, Stack, StackItem, Text } from "@fluentui/react"; import { IconButton, Image, Link, Modal, PrimaryButton, Stack, StackItem, Text } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks"; import { useBoolean } from "@fluentui/react-hooks";
import React from "react"; import React from "react";
import Database from "../../../../images/CopilotDatabase.svg";
import Flash from "../../../../images/CopilotFlash.svg"; import Flash from "../../../../images/CopilotFlash.svg";
import Thumb from "../../../../images/CopilotThumb.svg"; import Thumb from "../../../../images/CopilotThumb.svg";
import CoplilotWelcomeIllustration from "../../../../images/CopliotWelcomeIllustration.svg"; import CoplilotWelcomeIllustration from "../../../../images/CopliotWelcomeIllustration.svg";
@ -23,8 +22,10 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
onDismiss={hideModal} onDismiss={hideModal}
isBlocking={false} isBlocking={false}
styles={{ styles={{
scrollableContent: { main: {
minHeight: 680, maxHeight: 530,
borderRadius: 10,
overflow: "hidden",
}, },
}} }}
> >
@ -52,7 +53,7 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
</Stack> </Stack>
<Stack horizontalAlign="center"> <Stack horizontalAlign="center">
<Stack.Item align="center" style={{ textAlign: "center" }}> <Stack.Item align="center" style={{ textAlign: "center" }}>
<Text className="title bold">Welcome to Copilot in Azure Cosmos DB (Private Preview)</Text> <Text className="title bold">Welcome to Copilot in Azure Cosmos DB</Text>
</Stack.Item> </Stack.Item>
<Stack.Item align="center" className="text"> <Stack.Item align="center" className="text">
<Stack horizontal> <Stack horizontal>
@ -69,7 +70,7 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
<Text> <Text>
Ask Copilot to generate a query by describing the query in your words. Ask Copilot to generate a query by describing the query in your words.
<br /> <br />
<Link target="_blank" href="https://aka.ms/cdb-copilot-learn-more"> <Link target="_blank" href="https://aka.ms/CopilotInAzureCDBDocs">
Learn more Learn more
</Link> </Link>
</Text> </Text>
@ -87,31 +88,11 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
</StackItem> </StackItem>
</Stack> </Stack>
<Text> <Text>
AI-generated content can have mistakes. Make sure its accurate and appropriate before using it. AI-generated content can have mistakes. Make sure it is accurate and appropriate before executing the
query.
<br /> <br />
<Link target="_blank" href="https://aka.ms/cdb-copilot-preview-terms"> <Link target="_blank" href="https://aka.ms/cdb-copilot-preview-terms">
Read preview terms Read our preview terms here
</Link>
</Text>
</Stack.Item>
<Stack.Item align="center" className="text">
<Stack horizontal>
<StackItem align="start" className="imageTextPadding">
<Image src={Database} />
</StackItem>
<StackItem align="start">
<Text className="bold">
Query Copilot works on a sample database.
<br />
</Text>
</StackItem>
</Stack>
<Text>
While in Private Preview, Query Copilot is setup to work on sample database we have configured for you
at no cost.
<br />
<Link target="_blank" href="https://aka.ms/cdb-copilot-learn-more">
Learn more
</Link> </Link>
</Text> </Text>
</Stack.Item> </Stack.Item>

View File

@ -291,21 +291,6 @@ exports[`Query Copilot Feedback Modal snapshot test should cancel submission 1`]
for more information. for more information.
</Text> </Text>
<StyledCheckboxBase
checked={false}
label="Don't show me this next time"
onChange={[Function]}
styles={
Object {
"label": Object {
"paddingLeft": 0,
},
"root": Object {
"marginBottom": 14,
},
}
}
/>
<Stack <Stack
horizontal={true} horizontal={true}
horizontalAlign="end" horizontalAlign="end"

View File

@ -8,8 +8,10 @@ exports[`Query Copilot Welcome Modal snapshot test should render when isOpen is
onDismiss={[Function]} onDismiss={[Function]}
styles={ styles={
Object { Object {
"scrollableContent": Object { "main": Object {
"minHeight": 680, "borderRadius": 10,
"maxHeight": 530,
"overflow": "hidden",
}, },
} }
} }
@ -76,7 +78,7 @@ exports[`Query Copilot Welcome Modal snapshot test should render when isOpen is
<Text <Text
className="title bold" className="title bold"
> >
Welcome to Copilot in Azure Cosmos DB (Private Preview) Welcome to Copilot in Azure Cosmos DB
</Text> </Text>
</StackItem> </StackItem>
<StackItem <StackItem
@ -109,7 +111,7 @@ exports[`Query Copilot Welcome Modal snapshot test should render when isOpen is
Ask Copilot to generate a query by describing the query in your words. Ask Copilot to generate a query by describing the query in your words.
<br /> <br />
<StyledLinkBase <StyledLinkBase
href="https://aka.ms/cdb-copilot-learn-more" href="https://aka.ms/CopilotInAzureCDBDocs"
target="_blank" target="_blank"
> >
Learn more Learn more
@ -143,50 +145,13 @@ exports[`Query Copilot Welcome Modal snapshot test should render when isOpen is
</StackItem> </StackItem>
</Stack> </Stack>
<Text> <Text>
AI-generated content can have mistakes. Make sure its accurate and appropriate before using it. AI-generated content can have mistakes. Make sure it is accurate and appropriate before executing the query.
<br /> <br />
<StyledLinkBase <StyledLinkBase
href="https://aka.ms/cdb-copilot-preview-terms" href="https://aka.ms/cdb-copilot-preview-terms"
target="_blank" target="_blank"
> >
Read preview terms Read our preview terms here
</StyledLinkBase>
</Text>
</StackItem>
<StackItem
align="center"
className="text"
>
<Stack
horizontal={true}
>
<StackItem
align="start"
className="imageTextPadding"
>
<Image
src={Object {}}
/>
</StackItem>
<StackItem
align="start"
>
<Text
className="bold"
>
Query Copilot works on a sample database.
<br />
</Text>
</StackItem>
</Stack>
<Text>
While in Private Preview, Query Copilot is setup to work on sample database we have configured for you at no cost.
<br />
<StyledLinkBase
href="https://aka.ms/cdb-copilot-learn-more"
target="_blank"
>
Learn more
</StyledLinkBase> </StyledLinkBase>
</Text> </Text>
</StackItem> </StackItem>

View File

@ -0,0 +1,135 @@
import { MinimalQueryIterator } from "Common/IteratorUtilities";
import { QueryResults } from "Contracts/ViewModels";
import { CopilotMessage } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
import { guid } from "Explorer/Tables/Utilities";
import { QueryCopilotState } from "hooks/useQueryCopilot";
import React, { createContext, useContext, useState } from "react";
import create from "zustand";
const context = createContext(null);
const useCopilotStore = (): Partial<QueryCopilotState> => useContext(context);
const CopilotProvider = ({ children }: { children: React.ReactNode }): JSX.Element => {
const [useStore] = useState(() =>
create((set, get) => ({
generatedQuery: "",
likeQuery: false,
userPrompt: "",
showFeedbackModal: false,
hideFeedbackModalForLikedQueries: false,
correlationId: "",
query: "SELECT * FROM c",
selectedQuery: "",
isGeneratingQuery: false,
isGeneratingExplanation: false,
isExecuting: false,
dislikeQuery: undefined,
showCallout: false,
showSamplePrompts: false,
queryIterator: undefined,
queryResults: undefined,
errorMessage: "",
isSamplePromptsOpen: false,
showDeletePopup: false,
showFeedbackBar: false,
showCopyPopup: false,
showErrorMessageBar: false,
showInvalidQueryMessageBar: false,
generatedQueryComments: "",
wasCopilotUsed: false,
showWelcomeSidebar: true,
showCopilotSidebar: false,
chatMessages: [],
shouldIncludeInMessages: true,
showExplanationBubble: false,
isAllocatingContainer: false,
openFeedbackModal: (generatedQuery: string, likeQuery: boolean, userPrompt: string) =>
set({ generatedQuery, likeQuery, userPrompt, showFeedbackModal: true }),
closeFeedbackModal: () => set({ showFeedbackModal: false }),
setHideFeedbackModalForLikedQueries: (hideFeedbackModalForLikedQueries: boolean) =>
set({ hideFeedbackModalForLikedQueries }),
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 }),
setIsGeneratingExplanation: (isGeneratingExplanation: boolean) => set({ isGeneratingExplanation }),
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 }),
setShowInvalidQueryMessageBar: (showInvalidQueryMessageBar: boolean) => set({ showInvalidQueryMessageBar }),
setGeneratedQueryComments: (generatedQueryComments: string) => set({ generatedQueryComments }),
setWasCopilotUsed: (wasCopilotUsed: boolean) => set({ wasCopilotUsed }),
setShowWelcomeSidebar: (showWelcomeSidebar: boolean) => set({ showWelcomeSidebar }),
setShowCopilotSidebar: (showCopilotSidebar: boolean) => set({ showCopilotSidebar }),
setChatMessages: (chatMessages: CopilotMessage[]) => set({ chatMessages }),
setShouldIncludeInMessages: (shouldIncludeInMessages: boolean) => set({ shouldIncludeInMessages }),
setShowExplanationBubble: (showExplanationBubble: boolean) => set({ showExplanationBubble }),
getState: () => {
return get();
},
resetQueryCopilotStates: () => {
set((state) => ({
...state,
generatedQuery: "",
likeQuery: false,
userPrompt: "",
showFeedbackModal: false,
hideFeedbackModalForLikedQueries: false,
correlationId: "",
query: "SELECT * FROM c",
selectedQuery: "",
isGeneratingQuery: false,
isGeneratingExplanation: false,
isExecuting: false,
dislikeQuery: undefined,
showCallout: false,
showSamplePrompts: false,
queryIterator: undefined,
queryResults: undefined,
errorMessage: "",
isSamplePromptsOpen: false,
showDeletePopup: false,
showFeedbackBar: false,
showCopyPopup: false,
showErrorMessageBar: false,
showInvalidQueryMessageBar: false,
generatedQueryComments: "",
wasCopilotUsed: false,
showCopilotSidebar: false,
chatMessages: [],
shouldIncludeInMessages: true,
showExplanationBubble: false,
notebookServerInfo: {
notebookServerEndpoint: undefined,
authToken: undefined,
forwardingId: undefined,
},
containerStatus: {
status: undefined,
durationLeftInMinutes: undefined,
phoenixServerInfo: undefined,
},
isAllocatingContainer: false,
}));
},
})),
);
return <context.Provider value={useStore()}>{children}</context.Provider>;
};
export { CopilotProvider, useCopilotStore };

View File

@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable no-console */
import { import {
Callout, Callout,
CommandBarButton, CommandBarButton,
@ -17,20 +19,20 @@ import {
TextField, TextField,
} from "@fluentui/react"; } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks"; import { useBoolean } from "@fluentui/react-hooks";
import {
ContainerStatusType,
PoolIdType,
QueryCopilotSampleContainerSchema,
ShortenedQueryCopilotSampleContainerSchema,
} from "Common/Constants";
import { handleError } from "Common/ErrorHandlingUtils"; import { handleError } from "Common/ErrorHandlingUtils";
import { createUri } from "Common/UrlUtility"; import { createUri } from "Common/UrlUtility";
import { WelcomeModal } from "Explorer/QueryCopilot/Modal/WelcomeModal"; import { WelcomeModal } from "Explorer/QueryCopilot/Modal/WelcomeModal";
import { CopyPopup } from "Explorer/QueryCopilot/Popup/CopyPopup"; import { CopyPopup } from "Explorer/QueryCopilot/Popup/CopyPopup";
import { DeletePopup } from "Explorer/QueryCopilot/Popup/DeletePopup"; import { DeletePopup } from "Explorer/QueryCopilot/Popup/DeletePopup";
import { SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient"; import {
SuggestedPrompt,
getSampleDatabaseSuggestedPrompts,
getSuggestedPrompts,
} from "Explorer/QueryCopilot/QueryCopilotUtilities";
import { SubmitFeedback, allocatePhoenixContainer } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { GenerateSQLQueryResponse, QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces"; import { GenerateSQLQueryResponse, QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
import { SamplePrompts, SamplePromptsProps } from "Explorer/QueryCopilot/Shared/SamplePrompts/SamplePrompts"; import { SamplePrompts, SamplePromptsProps } from "Explorer/QueryCopilot/Shared/SamplePrompts/SamplePrompts";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { useQueryCopilot } from "hooks/useQueryCopilot"; import { useQueryCopilot } from "hooks/useQueryCopilot";
import React, { useRef, useState } from "react"; import React, { useRef, useState } from "react";
@ -38,17 +40,17 @@ 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 errorIcon from "../../../images/close-black.svg"; import errorIcon from "../../../images/close-black.svg";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { useTabs } from "../../hooks/useTabs"; import { useTabs } from "../../hooks/useTabs";
import { useCopilotStore } from "../QueryCopilot/QueryCopilotContext";
import { useSelectedNode } from "../useSelectedNode";
type QueryCopilotPromptProps = QueryCopilotProps & { type QueryCopilotPromptProps = QueryCopilotProps & {
databaseId: string;
containerId: string;
toggleCopilot: (toggle: boolean) => void; toggleCopilot: (toggle: boolean) => void;
}; };
interface SuggestedPrompt {
id: number;
text: string;
}
const promptStyles: IButtonStyles = { const promptStyles: IButtonStyles = {
root: { border: 0, selectors: { ":hover": { outline: "1px dashed #605e5c" } } }, root: { border: 0, selectors: { ":hover": { outline: "1px dashed #605e5c" } } },
label: { fontWeight: 400, textAlign: "left", paddingLeft: 8 }, label: { fontWeight: 400, textAlign: "left", paddingLeft: 8 },
@ -57,10 +59,13 @@ const promptStyles: IButtonStyles = {
export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
explorer, explorer,
toggleCopilot, toggleCopilot,
databaseId,
containerId,
}: QueryCopilotPromptProps): JSX.Element => { }: QueryCopilotPromptProps): JSX.Element => {
const [copilotTeachingBubbleVisible, { toggle: toggleCopilotTeachingBubbleVisible }] = useBoolean(false); const [copilotTeachingBubbleVisible, { toggle: toggleCopilotTeachingBubbleVisible }] = useBoolean(false);
const inputEdited = useRef(false); const inputEdited = useRef(false);
const { const {
openFeedbackModal,
hideFeedbackModalForLikedQueries, hideFeedbackModalForLikedQueries,
userPrompt, userPrompt,
setUserPrompt, setUserPrompt,
@ -93,7 +98,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
setGeneratedQueryComments, setGeneratedQueryComments,
setQueryResults, setQueryResults,
setErrorMessage, setErrorMessage,
} = useQueryCopilot(); } = useCopilotStore();
const sampleProps: SamplePromptsProps = { const sampleProps: SamplePromptsProps = {
isSamplePromptsOpen: isSamplePromptsOpen, isSamplePromptsOpen: isSamplePromptsOpen,
@ -118,14 +123,13 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
}, 6000); }, 6000);
}; };
const isSampleCopilotActive = useSelectedNode.getState().isQueryCopilotCollectionSelected();
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[] = [ const suggestedPrompts: SuggestedPrompt[] = isSampleCopilotActive
{ id: 1, text: 'Show all products that have the word "ultra" in the name or description' }, ? getSampleDatabaseSuggestedPrompts()
{ id: 2, text: "What are all of the possible categories for the products, and their counts?" }, : getSuggestedPrompts();
{ id: 3, text: 'Show me all products that have been reviewed by someone with a username that contains "bob"' },
];
const [filteredHistories, setFilteredHistories] = useState<string[]>(histories); const [filteredHistories, setFilteredHistories] = useState<string[]>(histories);
const [filteredSuggestedPrompts, setFilteredSuggestedPrompts] = useState<SuggestedPrompt[]>(suggestedPrompts); const [filteredSuggestedPrompts, setFilteredSuggestedPrompts] = useState<SuggestedPrompt[]>(suggestedPrompts);
@ -176,16 +180,11 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
setShowDeletePopup(false); setShowDeletePopup(false);
useTabs.getState().setIsTabExecuting(true); useTabs.getState().setIsTabExecuting(true);
useTabs.getState().setIsQueryErrorThrown(false); useTabs.getState().setIsQueryErrorThrown(false);
if ( const mode: string = isSampleCopilotActive ? "Sample" : "User";
useQueryCopilot.getState().containerStatus.status !== ContainerStatusType.Active &&
!userContext.features.disableCopilotPhoenixGateaway await allocatePhoenixContainer({ explorer, databaseId, containerId, mode });
) {
await explorer.allocateContainer(PoolIdType.QueryCopilot);
}
const payload = { const payload = {
containerSchema: userContext.features.enableCopilotFullSchema
? QueryCopilotSampleContainerSchema
: ShortenedQueryCopilotSampleContainerSchema,
userPrompt: userPrompt, userPrompt: userPrompt,
}; };
useQueryCopilot.getState().refreshCorrelationId(); useQueryCopilot.getState().refreshCorrelationId();
@ -198,6 +197,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
headers: { headers: {
"content-type": "application/json", "content-type": "application/json",
"x-ms-correlationid": useQueryCopilot.getState().correlationId, "x-ms-correlationid": useQueryCopilot.getState().correlationId,
Authorization: `token ${useQueryCopilot.getState().notebookServerInfo.authToken}`,
}, },
body: JSON.stringify(payload), body: JSON.stringify(payload),
}); });
@ -215,13 +215,30 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
setGeneratedQueryComments(generateSQLQueryResponse.explanation); setGeneratedQueryComments(generateSQLQueryResponse.explanation);
setShowFeedbackBar(true); setShowFeedbackBar(true);
resetQueryResults(); resetQueryResults();
TelemetryProcessor.traceSuccess(Action.QueryGenerationFromCopilotPrompt, {
databaseName: databaseId,
collectionId: containerId,
copilotLatency:
Date.parse(generateSQLQueryResponse?.generateEnd) - Date.parse(generateSQLQueryResponse?.generateStart),
responseCode: response.status,
});
} else { } else {
setShowInvalidQueryMessageBar(true); setShowInvalidQueryMessageBar(true);
TelemetryProcessor.traceFailure(Action.QueryGenerationFromCopilotPrompt, {
databaseName: databaseId,
collectionId: containerId,
responseCode: response.status,
});
} }
} else { } else {
handleError(JSON.stringify(generateSQLQueryResponse), "copilotInternalServerError"); handleError(JSON.stringify(generateSQLQueryResponse), "copilotInternalServerError");
useTabs.getState().setIsQueryErrorThrown(true); useTabs.getState().setIsQueryErrorThrown(true);
setShowErrorMessageBar(true); setShowErrorMessageBar(true);
TelemetryProcessor.traceFailure(Action.QueryGenerationFromCopilotPrompt, {
databaseName: databaseId,
collectionId: containerId,
responseCode: response.status,
});
} }
} catch (error) { } catch (error) {
handleError(error, "executeNaturalLanguageQuery"); handleError(error, "executeNaturalLanguageQuery");
@ -310,6 +327,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
styles={{ root: { width: "95%" }, fieldGroup: { borderRadius: 6 } }} styles={{ root: { width: "95%" }, fieldGroup: { borderRadius: 6 } }}
disabled={isGeneratingQuery} disabled={isGeneratingQuery}
autoComplete="off" autoComplete="off"
placeholder="Ask a question in natural language and well generate the query for you."
/> />
{copilotTeachingBubbleVisible && ( {copilotTeachingBubbleVisible && (
<TeachingBubble <TeachingBubble
@ -489,7 +507,10 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
description: "", description: "",
userPrompt: userPrompt, userPrompt: userPrompt,
}, },
explorer: explorer, explorer,
databaseId,
containerId,
mode: isSampleCopilotActive ? "Sample" : "User",
}); });
}} }}
directionalHint={DirectionalHint.topCenter} directionalHint={DirectionalHint.topCenter}
@ -499,7 +520,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
<Link <Link
onClick={() => { onClick={() => {
setShowCallout(false); setShowCallout(false);
useQueryCopilot.getState().openFeedbackModal(generatedQuery, true, userPrompt); openFeedbackModal(generatedQuery, true, userPrompt);
}} }}
> >
more feedback? more feedback?
@ -524,7 +545,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
iconProps={{ iconName: dislikeQuery === true ? "DislikeSolid" : "Dislike" }} iconProps={{ iconName: dislikeQuery === true ? "DislikeSolid" : "Dislike" }}
onClick={() => { onClick={() => {
if (!dislikeQuery) { if (!dislikeQuery) {
useQueryCopilot.getState().openFeedbackModal(generatedQuery, false, userPrompt); openFeedbackModal(generatedQuery, false, userPrompt);
setLikeQuery(false); setLikeQuery(false);
} }
setDislikeQuery(!dislikeQuery); setDislikeQuery(!dislikeQuery);

View File

@ -1,5 +1,6 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
import { Stack } from "@fluentui/react"; import { Stack } from "@fluentui/react";
import { QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
import { EditorReact } from "Explorer/Controls/Editor/EditorReact"; import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter"; import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
@ -11,6 +12,7 @@ import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotRe
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { useQueryCopilot } from "hooks/useQueryCopilot"; import { useQueryCopilot } from "hooks/useQueryCopilot";
import { useSidePanel } from "hooks/useSidePanel"; import { useSidePanel } from "hooks/useSidePanel";
import { ReactTabKind, TabsState, useTabs } from "hooks/useTabs";
import React, { useState } from "react"; import React, { useState } from "react";
import SplitterLayout from "react-splitter-layout"; import SplitterLayout from "react-splitter-layout";
import QueryCommandIcon from "../../../images/CopilotCommand.svg"; import QueryCommandIcon from "../../../images/CopilotCommand.svg";
@ -28,13 +30,14 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
? StringUtility.toBoolean(cachedCopilotToggleStatus) ? StringUtility.toBoolean(cachedCopilotToggleStatus)
: true; : true;
const [copilotActive, setCopilotActive] = useState<boolean>(copilotInitialActive); const [copilotActive, setCopilotActive] = useState<boolean>(copilotInitialActive);
const [tabActive, setTabActive] = useState<boolean>(true);
const getCommandbarButtons = (): CommandButtonComponentProps[] => { const getCommandbarButtons = (): CommandButtonComponentProps[] => {
const executeQueryBtnLabel = selectedQuery ? "Execute Selection" : "Execute Query"; const executeQueryBtnLabel = selectedQuery ? "Execute Selection" : "Execute Query";
const executeQueryBtn = { const executeQueryBtn = {
iconSrc: ExecuteQueryIcon, iconSrc: ExecuteQueryIcon,
iconAlt: executeQueryBtnLabel, iconAlt: executeQueryBtnLabel,
onCommandClick: () => OnExecuteQueryClick(), onCommandClick: () => OnExecuteQueryClick(useQueryCopilot),
commandButtonLabel: executeQueryBtnLabel, commandButtonLabel: executeQueryBtnLabel,
ariaLabel: executeQueryBtnLabel, ariaLabel: executeQueryBtnLabel,
hasPopup: false, hasPopup: false,
@ -73,10 +76,13 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
React.useEffect(() => { React.useEffect(() => {
return () => { return () => {
const commandbarButtons = getCommandbarButtons(); useTabs.subscribe((state: TabsState) => {
commandbarButtons.pop(); if (state.activeReactTab === ReactTabKind.QueryCopilot) {
commandbarButtons.map((props: CommandButtonComponentProps) => (props.disabled = true)); setTabActive(true);
useCommandBar.getState().setContextButtons(commandbarButtons); } else {
setTabActive(false);
}
});
}; };
}, []); }, []);
@ -88,8 +94,13 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
return ( return (
<Stack className="tab-pane" style={{ width: "100%" }}> <Stack className="tab-pane" style={{ width: "100%" }}>
<div style={isGeneratingQuery ? { height: "100%" } : { overflowY: "auto", height: "100%" }}> <div style={isGeneratingQuery ? { height: "100%" } : { overflowY: "auto", height: "100%" }}>
{copilotActive && ( {tabActive && copilotActive && (
<QueryCopilotPromptbar explorer={explorer} toggleCopilot={toggleCopilot}></QueryCopilotPromptbar> <QueryCopilotPromptbar
explorer={explorer}
toggleCopilot={toggleCopilot}
databaseId={QueryCopilotSampleDatabaseId}
containerId={QueryCopilotSampleContainerId}
></QueryCopilotPromptbar>
)} )}
<Stack className="tabPaneContentContainer"> <Stack className="tabPaneContentContainer">
<SplitterLayout percentage={true} vertical={true} primaryIndex={0} primaryMinSize={30} secondaryMinSize={70}> <SplitterLayout percentage={true} vertical={true} primaryIndex={0} primaryMinSize={30} secondaryMinSize={70}>

View File

@ -7,6 +7,11 @@ import { getCommonQueryOptions } from "Common/dataAccess/queryDocuments";
import DocumentId from "Explorer/Tree/DocumentId"; import DocumentId from "Explorer/Tree/DocumentId";
import { logConsoleProgress } from "Utils/NotificationConsoleUtils"; import { logConsoleProgress } from "Utils/NotificationConsoleUtils";
export interface SuggestedPrompt {
id: number;
text: string;
}
export const querySampleDocuments = (query: string, options: FeedOptions): QueryIterator<ItemDefinition & Resource> => { export const querySampleDocuments = (query: string, options: FeedOptions): QueryIterator<ItemDefinition & Resource> => {
options = getCommonQueryOptions(options); options = getCommonQueryOptions(options);
return sampleDataClient() return sampleDataClient()
@ -33,3 +38,19 @@ export const readSampleDocument = async (documentId: DocumentId): Promise<Item>
clearMessage(); clearMessage();
} }
}; };
export const getSampleDatabaseSuggestedPrompts = (): SuggestedPrompt[] => {
return [
{ id: 1, text: 'Show all products that have the word "ultra" in the name or description' },
{ id: 2, text: "What are all of the possible categories for the products, and their counts?" },
{ id: 3, text: 'Show me all products that have been reviewed by someone with a username that contains "bob"' },
];
};
export const getSuggestedPrompts = (): SuggestedPrompt[] => {
return [
{ id: 1, text: "Show the first 10 items" },
{ id: 2, text: 'Count all the items in my data as "numItems"' },
{ id: 3, text: "Find the oldest item added to my collection" },
];
};

View File

@ -1,4 +1,3 @@
import { QueryCopilotSampleContainerSchema, ShortenedQueryCopilotSampleContainerSchema } from "Common/Constants";
import { handleError } from "Common/ErrorHandlingUtils"; import { handleError } from "Common/ErrorHandlingUtils";
import { createUri } from "Common/UrlUtility"; import { createUri } from "Common/UrlUtility";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
@ -37,9 +36,6 @@ describe("Query Copilot Client", () => {
userPrompt: "UserPrompt", userPrompt: "UserPrompt",
description: "Description", description: "Description",
contact: "Contact", contact: "Contact",
containerSchema: userContext.features.enableCopilotFullSchema
? QueryCopilotSampleContainerSchema
: ShortenedQueryCopilotSampleContainerSchema,
}; };
const mockStore = useQueryCopilot.getState(); const mockStore = useQueryCopilot.getState();
@ -59,6 +55,9 @@ describe("Query Copilot Client", () => {
globalThis.fetch = mockFetch; globalThis.fetch = mockFetch;
await SubmitFeedback({ await SubmitFeedback({
databaseId: "test",
containerId: "test",
mode: "User",
params: { params: {
likeQuery: true, likeQuery: true,
generatedQuery: "GeneratedQuery", generatedQuery: "GeneratedQuery",
@ -91,6 +90,9 @@ describe("Query Copilot Client", () => {
globalThis.fetch = mockFetch; globalThis.fetch = mockFetch;
await SubmitFeedback({ await SubmitFeedback({
databaseId: "test",
containerId: "test",
mode: "User",
params: { params: {
likeQuery: false, likeQuery: false,
generatedQuery: "GeneratedQuery", generatedQuery: "GeneratedQuery",
@ -108,6 +110,7 @@ describe("Query Copilot Client", () => {
headers: { headers: {
"content-type": "application/json", "content-type": "application/json",
"x-ms-correlationid": "mocked-correlation-id", "x-ms-correlationid": "mocked-correlation-id",
Authorization: "token mocked-token",
}, },
}), }),
); );
@ -120,6 +123,9 @@ describe("Query Copilot Client", () => {
globalThis.fetch = jest.fn().mockRejectedValueOnce(new Error("Mock error")); globalThis.fetch = jest.fn().mockRejectedValueOnce(new Error("Mock error"));
await SubmitFeedback({ await SubmitFeedback({
databaseId: "test",
containerId: "test",
mode: "User",
params: { params: {
likeQuery: true, likeQuery: true,
generatedQuery: "GeneratedQuery", generatedQuery: "GeneratedQuery",

View File

@ -1,28 +1,174 @@
import { FeedOptions } from "@azure/cosmos"; import { FeedOptions } from "@azure/cosmos";
import { import {
Areas,
ConnectionStatusType,
ContainerStatusType, ContainerStatusType,
HttpStatusCodes,
PoolIdType, PoolIdType,
QueryCopilotSampleContainerId, QueryCopilotSampleContainerId,
QueryCopilotSampleContainerSchema, QueryCopilotSampleContainerSchema,
ShortenedQueryCopilotSampleContainerSchema, ShortenedQueryCopilotSampleContainerSchema,
} from "Common/Constants"; } from "Common/Constants";
import { getErrorMessage, handleError } from "Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack, handleError } from "Common/ErrorHandlingUtils";
import { shouldEnableCrossPartitionKey } from "Common/HeadersUtility"; import { shouldEnableCrossPartitionKey } from "Common/HeadersUtility";
import { MinimalQueryIterator } from "Common/IteratorUtilities"; import { MinimalQueryIterator } from "Common/IteratorUtilities";
import { createUri } from "Common/UrlUtility"; import { createUri } from "Common/UrlUtility";
import { queryDocumentsPage } from "Common/dataAccess/queryDocumentsPage"; import { queryDocumentsPage } from "Common/dataAccess/queryDocumentsPage";
import { QueryResults } from "Contracts/ViewModels"; import { configContext } from "ConfigContext";
import {
ContainerConnectionInfo,
CopilotEnabledConfiguration,
FeatureRegistration,
IProvisionData,
} from "Contracts/DataModels";
import { AuthorizationTokenHeaderMetadata, QueryResults } from "Contracts/ViewModels";
import { useDialog } from "Explorer/Controls/Dialog";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import { querySampleDocuments } from "Explorer/QueryCopilot/QueryCopilotUtilities"; import { querySampleDocuments } from "Explorer/QueryCopilot/QueryCopilotUtilities";
import { FeedbackParams, GenerateSQLQueryResponse } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces"; import { FeedbackParams, GenerateSQLQueryResponse } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
import { Action } from "Shared/Telemetry/TelemetryConstants"; import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor"; import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { getAuthorizationHeader } from "Utils/AuthorizationUtils";
import { queryPagesUntilContentPresent } from "Utils/QueryUtils"; import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
import { useQueryCopilot } from "hooks/useQueryCopilot"; import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
import { useTabs } from "hooks/useTabs"; import { useTabs } from "hooks/useTabs";
import * as StringUtility from "../../../Shared/StringUtility"; import * as StringUtility from "../../../Shared/StringUtility";
export const isCopilotFeatureRegistered = async (subscriptionId: string): Promise<boolean> => {
const api_version = "2021-07-01";
const url = `${configContext.ARM_ENDPOINT}/subscriptions/${subscriptionId}/providers/Microsoft.Features/featureProviders/Microsoft.DocumentDB/subscriptionFeatureRegistrations/CopilotInAzureCDB?api-version=${api_version}`;
const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
const headers = { [authorizationHeader.header]: authorizationHeader.token };
let response;
try {
response = await window.fetch(url, {
headers,
});
} catch (error) {
return false;
}
if (!response?.ok) {
return false;
}
const featureRegistration = (await response?.json()) as FeatureRegistration;
return featureRegistration?.properties?.state === "Registered";
};
export const getCopilotEnabled = async (): Promise<boolean> => {
const url = `${configContext.BACKEND_ENDPOINT}/api/portalsettings/querycopilot`;
const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
const headers = { [authorizationHeader.header]: authorizationHeader.token };
let response;
try {
response = await window.fetch(url, {
headers,
});
} catch (error) {
return false;
}
if (!response?.ok) {
throw new Error(await response?.text());
}
const copilotPortalConfiguration = (await response?.json()) as CopilotEnabledConfiguration;
return copilotPortalConfiguration?.isEnabled;
};
export const allocatePhoenixContainer = async ({
explorer,
databaseId,
containerId,
mode,
}: {
explorer: Explorer;
databaseId: string;
containerId: string;
mode: string;
}): Promise<void> => {
try {
if (
useQueryCopilot.getState().containerStatus.status !== ContainerStatusType.Active &&
!userContext.features.disableCopilotPhoenixGateaway
) {
await explorer.allocateContainer(PoolIdType.QueryCopilot, mode);
} else {
const currentAllocatedSchemaInfo = useQueryCopilot.getState().schemaAllocationInfo;
if (
currentAllocatedSchemaInfo.databaseId !== databaseId ||
currentAllocatedSchemaInfo.containerId !== containerId
) {
await resetPhoenixContainerSchema({ explorer, databaseId, containerId, mode });
}
}
useQueryCopilot.getState().setSchemaAllocationInfo({
databaseId,
containerId,
});
} catch (error) {
traceFailure(Action.PhoenixConnection, {
dataExplorerArea: Areas.Copilot,
status: error.status,
error: getErrorMessage(error),
errorStack: getErrorStack(error),
});
useQueryCopilot.getState().resetContainerConnection();
if (error?.status === HttpStatusCodes.Forbidden && error.message) {
useDialog.getState().showOkModalDialog("Connection Failed", `${error.message}`);
} else {
useDialog
.getState()
.showOkModalDialog(
"Connection Failed",
"We are unable to connect to the temporary workspace. Please try again in a few minutes. If the error persists, file a support ticket.",
);
}
} finally {
useTabs.getState().setIsTabExecuting(false);
}
};
export const resetPhoenixContainerSchema = async ({
explorer,
databaseId,
containerId,
mode,
}: {
explorer: Explorer;
databaseId: string;
containerId: string;
mode: string;
}): Promise<void> => {
try {
const provisionData: IProvisionData = {
poolId: PoolIdType.QueryCopilot,
databaseId: databaseId,
containerId: containerId,
mode: mode,
};
const connectionInfo = await explorer.phoenixClient.allocateContainer(provisionData);
const connectionStatus: ContainerConnectionInfo = {
status: ConnectionStatusType.Connecting,
};
await explorer.setNotebookInfo(false, connectionInfo, connectionStatus);
} catch (error) {
traceFailure(Action.PhoenixConnection, {
dataExplorerArea: Areas.Copilot,
status: error.status,
error: getErrorMessage(error),
errorStack: getErrorStack(error),
});
throw error;
}
};
export const SendQueryRequest = async ({ export const SendQueryRequest = async ({
userPrompt, userPrompt,
explorer, explorer,
@ -106,16 +252,19 @@ export const SendQueryRequest = async ({
export const SubmitFeedback = async ({ export const SubmitFeedback = async ({
params, params,
explorer, explorer,
databaseId,
containerId,
mode,
}: { }: {
params: FeedbackParams; params: FeedbackParams;
explorer: Explorer; explorer: Explorer;
databaseId: string;
containerId: string;
mode: string;
}): Promise<void> => { }): Promise<void> => {
try { try {
const { likeQuery, generatedQuery, userPrompt, description, contact } = params; const { likeQuery, generatedQuery, userPrompt, description, contact } = params;
const payload = { const payload = {
containerSchema: userContext.features.enableCopilotFullSchema
? QueryCopilotSampleContainerSchema
: ShortenedQueryCopilotSampleContainerSchema,
like: likeQuery ? "like" : "dislike", like: likeQuery ? "like" : "dislike",
generatedSql: generatedQuery, generatedSql: generatedQuery,
userPrompt, userPrompt,
@ -126,7 +275,7 @@ export const SubmitFeedback = async ({
useQueryCopilot.getState().containerStatus.status !== ContainerStatusType.Active && useQueryCopilot.getState().containerStatus.status !== ContainerStatusType.Active &&
!userContext.features.disableCopilotPhoenixGateaway !userContext.features.disableCopilotPhoenixGateaway
) { ) {
await explorer.allocateContainer(PoolIdType.QueryCopilot); await allocatePhoenixContainer({ explorer, databaseId, containerId, mode });
} }
const serverInfo = useQueryCopilot.getState().notebookServerInfo; const serverInfo = useQueryCopilot.getState().notebookServerInfo;
const feedbackUri = userContext.features.disableCopilotPhoenixGateaway const feedbackUri = userContext.features.disableCopilotPhoenixGateaway
@ -137,6 +286,7 @@ export const SubmitFeedback = async ({
headers: { headers: {
"content-type": "application/json", "content-type": "application/json",
"x-ms-correlationid": useQueryCopilot.getState().correlationId, "x-ms-correlationid": useQueryCopilot.getState().correlationId,
Authorization: `token ${useQueryCopilot.getState().notebookServerInfo.authToken}`,
}, },
body: JSON.stringify(payload), body: JSON.stringify(payload),
}); });
@ -145,7 +295,7 @@ export const SubmitFeedback = async ({
} }
}; };
export const OnExecuteQueryClick = async (): Promise<void> => { export const OnExecuteQueryClick = async (useQueryCopilot: Partial<QueryCopilotState>): Promise<void> => {
traceStart(Action.ExecuteQueryGeneratedFromQueryCopilot, { traceStart(Action.ExecuteQueryGeneratedFromQueryCopilot, {
correlationId: useQueryCopilot.getState().correlationId, correlationId: useQueryCopilot.getState().correlationId,
userPrompt: useQueryCopilot.getState().userPrompt, userPrompt: useQueryCopilot.getState().userPrompt,
@ -160,13 +310,14 @@ export const OnExecuteQueryClick = async (): Promise<void> => {
useQueryCopilot.getState().setQueryIterator(queryIterator); useQueryCopilot.getState().setQueryIterator(queryIterator);
setTimeout(async () => { setTimeout(async () => {
await QueryDocumentsPerPage(0, queryIterator); await QueryDocumentsPerPage(0, queryIterator, useQueryCopilot);
}, 100); }, 100);
}; };
export const QueryDocumentsPerPage = async ( export const QueryDocumentsPerPage = async (
firstItemIndex: number, firstItemIndex: number,
queryIterator: MinimalQueryIterator, queryIterator: MinimalQueryIterator,
useQueryCopilot: Partial<QueryCopilotState>,
): Promise<void> => { ): Promise<void> => {
try { try {
useQueryCopilot.getState().setIsExecuting(true); useQueryCopilot.getState().setIsExecuting(true);

View File

@ -32,3 +32,8 @@ export interface FeedbackParams {
export interface QueryCopilotProps { export interface QueryCopilotProps {
explorer: Explorer; explorer: Explorer;
} }
export interface CopilotSchemaAllocationInfo {
databaseId: string;
containerId: string;
}

View File

@ -12,7 +12,7 @@ export const QueryCopilotResults: React.FC = (): JSX.Element => {
queryResults={useQueryCopilot.getState().queryResults} queryResults={useQueryCopilot.getState().queryResults}
isExecuting={useQueryCopilot.getState().isExecuting} isExecuting={useQueryCopilot.getState().isExecuting}
executeQueryDocumentsPage={(firstItemIndex: number) => executeQueryDocumentsPage={(firstItemIndex: number) =>
QueryDocumentsPerPage(firstItemIndex, useQueryCopilot.getState().queryIterator) QueryDocumentsPerPage(firstItemIndex, useQueryCopilot.getState().queryIterator, useQueryCopilot)
} }
/> />
); );

View File

@ -18,6 +18,8 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] =
} }
> >
<QueryCopilotPromptbar <QueryCopilotPromptbar
containerId="SampleContainer"
databaseId="CopilotSampleDb"
explorer={ explorer={
Explorer { Explorer {
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,

View File

@ -19,6 +19,7 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor"; import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
import { useCarousel } from "hooks/useCarousel"; import { useCarousel } from "hooks/useCarousel";
import { usePostgres } from "hooks/usePostgres"; import { usePostgres } from "hooks/usePostgres";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import { ReactTabKind, useTabs } from "hooks/useTabs"; import { ReactTabKind, useTabs } from "hooks/useTabs";
import * as React from "react"; import * as React from "react";
import ConnectIcon from "../../../images/Connect_color.svg"; import ConnectIcon from "../../../images/Connect_color.svg";
@ -104,6 +105,12 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
(state) => state.sampleDataResourceTokenCollection, (state) => state.sampleDataResourceTokenCollection,
), ),
}, },
{
dispose: useQueryCopilot.subscribe(
() => this.setState({}),
(state) => state.copilotEnabled,
),
},
); );
} }
@ -114,9 +121,9 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
private getSplashScreenButtons = (): JSX.Element => { private getSplashScreenButtons = (): JSX.Element => {
if ( if (
useDatabases.getState().sampleDataResourceTokenCollection && userContext.apiType === "SQL" &&
userContext.features.enableCopilot && useQueryCopilot.getState().copilotEnabled &&
userContext.apiType === "SQL" useDatabases.getState().sampleDataResourceTokenCollection
) { ) {
return ( return (
<Stack style={{ width: "66%", cursor: "pointer", margin: "40px auto" }} tokens={{ childrenGap: 16 }}> <Stack style={{ width: "66%", cursor: "pointer", margin: "40px auto" }} tokens={{ childrenGap: 16 }}>

View File

@ -1,10 +1,10 @@
import { ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos"; import { ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities"; import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
import { QueryConstants } from "Shared/Constants";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import * as ko from "knockout"; import * as ko from "knockout";
import Q from "q"; import Q from "q";
import { format } from "react-string-format"; import { format } from "react-string-format";
import { QueryConstants } from "Shared/Constants";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import DeleteDocumentIcon from "../../../images/DeleteDocument.svg"; import DeleteDocumentIcon from "../../../images/DeleteDocument.svg";
import NewDocumentIcon from "../../../images/NewDocument.svg"; import NewDocumentIcon from "../../../images/NewDocument.svg";
import UploadIcon from "../../../images/Upload_16x16.svg"; import UploadIcon from "../../../images/Upload_16x16.svg";
@ -331,6 +331,7 @@ export default class DocumentsTab extends TabsBase {
this.showPartitionKey = this._shouldShowPartitionKey(); this.showPartitionKey = this._shouldShowPartitionKey();
this._isQueryCopilotSampleContainer = this._isQueryCopilotSampleContainer =
this.collection?.isSampleCollection &&
this.collection?.databaseId === QueryCopilotSampleDatabaseId && this.collection?.databaseId === QueryCopilotSampleDatabaseId &&
this.collection?.id() === QueryCopilotSampleContainerId; this.collection?.id() === QueryCopilotSampleContainerId;
} }

View File

@ -1,11 +1,16 @@
import { CopilotProvider } from "Explorer/QueryCopilot/QueryCopilotContext";
import { userContext } from "UserContext";
import React from "react"; import React from "react";
import * as DataModels from "../../../Contracts/DataModels"; import * as DataModels from "../../../Contracts/DataModels";
import type { QueryTabOptions } from "../../../Contracts/ViewModels"; import type { QueryTabOptions } from "../../../Contracts/ViewModels";
import { useTabs } from "../../../hooks/useTabs"; import { useTabs } from "../../../hooks/useTabs";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { IQueryTabComponentProps, ITabAccessor } from "../../Tabs/QueryTab/QueryTabComponent"; import QueryTabComponent, {
IQueryTabComponentProps,
ITabAccessor,
QueryTabFunctionComponent,
} from "../../Tabs/QueryTab/QueryTabComponent";
import TabsBase from "../TabsBase"; import TabsBase from "../TabsBase";
import QueryTabComponent from "./QueryTabComponent";
export interface IQueryTabProps { export interface IQueryTabProps {
container: Explorer; container: Explorer;
@ -40,7 +45,13 @@ export class NewQueryTab extends TabsBase {
} }
public render(): JSX.Element { public render(): JSX.Element {
return <QueryTabComponent {...this.iQueryTabComponentProps} />; return userContext.apiType === "SQL" ? (
<CopilotProvider>
<QueryTabFunctionComponent {...this.iQueryTabComponentProps} />
</CopilotProvider>
) : (
<QueryTabComponent {...this.iQueryTabComponentProps} />
);
} }
public onTabClick(): void { public onTabClick(): void {

View File

@ -1,6 +1,16 @@
import { fireEvent, render } from "@testing-library/react"; import { fireEvent, render } from "@testing-library/react";
import QueryTabComponent, { IQueryTabComponentProps } from "Explorer/Tabs/QueryTab/QueryTabComponent"; import { CollectionTabKind } from "Contracts/ViewModels";
import { CopilotProvider } from "Explorer/QueryCopilot/QueryCopilotContext";
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
import QueryTabComponent, {
IQueryTabComponentProps,
QueryTabFunctionComponent,
} from "Explorer/Tabs/QueryTab/QueryTabComponent";
import TabsBase from "Explorer/Tabs/TabsBase";
import { updateUserContext, userContext } from "UserContext";
import { mount } from "enzyme";
import { useQueryCopilot } from "hooks/useQueryCopilot"; import { useQueryCopilot } from "hooks/useQueryCopilot";
import { useTabs } from "hooks/useTabs";
import React from "react"; import React from "react";
jest.mock("Explorer/Controls/Editor/EditorReact"); jest.mock("Explorer/Controls/Editor/EditorReact");
@ -11,9 +21,15 @@ describe("QueryTabComponent", () => {
mockStore.showCopilotSidebar = false; mockStore.showCopilotSidebar = false;
mockStore.setShowCopilotSidebar = jest.fn(); mockStore.setShowCopilotSidebar = jest.fn();
}); });
beforeEach(() => jest.clearAllMocks()); afterEach(() => jest.clearAllMocks());
it("should launch Copilot when ALT+C is pressed", () => { it("should launch conversational Copilot when ALT+C is pressed and when copilot version is 3", () => {
updateUserContext({
features: {
...userContext.features,
copilotVersion: "v3.0",
},
});
const propsMock: Readonly<IQueryTabComponentProps> = { const propsMock: Readonly<IQueryTabComponentProps> = {
collection: { databaseId: "CopilotSampleDb" }, collection: { databaseId: "CopilotSampleDb" },
onTabAccessor: () => jest.fn(), onTabAccessor: () => jest.fn(),
@ -31,4 +47,32 @@ describe("QueryTabComponent", () => {
expect(mockStore.setShowCopilotSidebar).toHaveBeenCalledWith(true); expect(mockStore.setShowCopilotSidebar).toHaveBeenCalledWith(true);
}); });
it("copilot should be enabled by default when tab is active", () => {
useQueryCopilot.getState().setCopilotEnabled(true);
useQueryCopilot.getState().setCopilotUserDBEnabled(true);
const activeTab = new TabsBase({
tabKind: CollectionTabKind.Query,
title: "Query",
tabPath: "",
});
activeTab.tabId = "mockTabId";
useTabs.getState().activeTab = activeTab;
const propsMock: Readonly<IQueryTabComponentProps> = {
collection: { databaseId: "CopilotUserDb", id: () => "CopilotUserContainer" },
onTabAccessor: () => jest.fn(),
isExecutionError: false,
tabId: "mockTabId",
tabsBaseInstance: {
updateNavbarWithTabsButtons: () => jest.fn(),
},
} as unknown as IQueryTabComponentProps;
const container = mount(
<CopilotProvider>
<QueryTabFunctionComponent {...propsMock} />
</CopilotProvider>,
);
expect(container.find(QueryCopilotPromptbar).exists()).toBe(true);
});
}); });

View File

@ -1,21 +1,29 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable no-console */
import { FeedOptions } from "@azure/cosmos"; import { FeedOptions } from "@azure/cosmos";
import { useDialog } from "Explorer/Controls/Dialog"; import { useDialog } from "Explorer/Controls/Dialog";
import { OnExecuteQueryClick } from "Explorer/QueryCopilot/Shared/QueryCopilotClient"; import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotResults"; import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext";
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
import { OnExecuteQueryClick, QueryDocumentsPerPage } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar"; import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar";
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection"; import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
import { useSelectedNode } from "Explorer/useSelectedNode";
import { QueryConstants } from "Shared/Constants"; import { QueryConstants } from "Shared/Constants";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot"; import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
import { TabsState, useTabs } from "hooks/useTabs";
import React, { Fragment } from "react"; import React, { Fragment } from "react";
import SplitterLayout from "react-splitter-layout"; import SplitterLayout from "react-splitter-layout";
import "react-splitter-layout/lib/index.css"; import "react-splitter-layout/lib/index.css";
import { format } from "react-string-format"; import { format } from "react-string-format";
import QueryCommandIcon from "../../../../images/CopilotCommand.svg";
import LaunchCopilot from "../../../../images/CopilotTabIcon.svg"; import LaunchCopilot from "../../../../images/CopilotTabIcon.svg";
import CancelQueryIcon from "../../../../images/Entity_cancel.svg"; import CancelQueryIcon from "../../../../images/Entity_cancel.svg";
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg"; import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
import SaveQueryIcon from "../../../../images/save-cosmos.svg"; import SaveQueryIcon from "../../../../images/save-cosmos.svg";
import { NormalizedEventKey, QueryCopilotSampleDatabaseId } from "../../../Common/Constants"; import { NormalizedEventKey } from "../../../Common/Constants";
import { getErrorMessage } from "../../../Common/ErrorHandlingUtils"; import { getErrorMessage } from "../../../Common/ErrorHandlingUtils";
import * as HeadersUtility from "../../../Common/HeadersUtility"; import * as HeadersUtility from "../../../Common/HeadersUtility";
import { MinimalQueryIterator } from "../../../Common/IteratorUtilities"; import { MinimalQueryIterator } from "../../../Common/IteratorUtilities";
@ -24,6 +32,8 @@ import { queryDocuments } from "../../../Common/dataAccess/queryDocuments";
import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage"; import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage";
import * as DataModels from "../../../Contracts/DataModels"; import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import * as StringUtility from "../../../Shared/StringUtility";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../../UserContext"; import { userContext } from "../../../UserContext";
import * as QueryUtils from "../../../Utils/QueryUtils"; import * as QueryUtils from "../../../Utils/QueryUtils";
import { useSidePanel } from "../../../hooks/useSidePanel"; import { useSidePanel } from "../../../hooks/useSidePanel";
@ -72,6 +82,9 @@ export interface IQueryTabComponentProps {
isPreferredApiMongoDB?: boolean; isPreferredApiMongoDB?: boolean;
monacoEditorSetting?: string; monacoEditorSetting?: string;
viewModelcollection?: ViewModels.Collection; viewModelcollection?: ViewModels.Collection;
copilotEnabled?: boolean;
isSampleCopilotActive?: boolean;
copilotStore?: Partial<QueryCopilotState>;
} }
interface IQueryTabStates { interface IQueryTabStates {
@ -85,8 +98,24 @@ interface IQueryTabStates {
showCopilotSidebar: boolean; showCopilotSidebar: boolean;
queryCopilotGeneratedQuery: string; queryCopilotGeneratedQuery: string;
cancelQueryTimeoutID: NodeJS.Timeout; cancelQueryTimeoutID: NodeJS.Timeout;
copilotActive: boolean;
currentTabActive: boolean;
} }
export const QueryTabFunctionComponent = (props: IQueryTabComponentProps): any => {
const copilotStore = useCopilotStore();
const isSampleCopilotActive = useSelectedNode.getState().isQueryCopilotCollectionSelected();
const queryTabProps = {
...props,
copilotEnabled:
useQueryCopilot().copilotEnabled &&
(useQueryCopilot().copilotUserDBEnabled || (isSampleCopilotActive && !!userContext.sampleDataConnectionInfo)),
isSampleCopilotActive: isSampleCopilotActive,
copilotStore: copilotStore,
};
return <QueryTabComponent {...queryTabProps}></QueryTabComponent>;
};
export default class QueryTabComponent extends React.Component<IQueryTabComponentProps, IQueryTabStates> { export default class QueryTabComponent extends React.Component<IQueryTabComponentProps, IQueryTabStates> {
public queryEditorId: string; public queryEditorId: string;
public executeQueryButton: Button; public executeQueryButton: Button;
@ -113,12 +142,14 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
showCopilotSidebar: useQueryCopilot.getState().showCopilotSidebar, showCopilotSidebar: useQueryCopilot.getState().showCopilotSidebar,
queryCopilotGeneratedQuery: useQueryCopilot.getState().query, queryCopilotGeneratedQuery: useQueryCopilot.getState().query,
cancelQueryTimeoutID: undefined, cancelQueryTimeoutID: undefined,
copilotActive: this._queryCopilotActive(),
currentTabActive: true,
}; };
this.isCloseClicked = false; this.isCloseClicked = false;
this.splitterId = this.props.tabId + "_splitter"; this.splitterId = this.props.tabId + "_splitter";
this.queryEditorId = `queryeditor${this.props.tabId}`; this.queryEditorId = `queryeditor${this.props.tabId}`;
this.isPreferredApiMongoDB = this.props.isPreferredApiMongoDB; this.isPreferredApiMongoDB = this.props.isPreferredApiMongoDB;
this.isCopilotTabActive = QueryCopilotSampleDatabaseId === this.props.collection.databaseId; this.isCopilotTabActive = userContext.features.copilotVersion === "v3.0";
this.executeQueryButton = { this.executeQueryButton = {
enabled: !!this.state.sqlQueryEditorContent && this.state.sqlQueryEditorContent.length > 0, enabled: !!this.state.sqlQueryEditorContent && this.state.sqlQueryEditorContent.length > 0,
visible: true, visible: true,
@ -143,6 +174,19 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
}); });
} }
private _queryCopilotActive(): boolean {
if (this.props.copilotEnabled) {
const cachedCopilotToggleStatus: string = localStorage.getItem(
`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`,
);
const copilotInitialActive: boolean = cachedCopilotToggleStatus
? StringUtility.toBoolean(cachedCopilotToggleStatus)
: true;
return copilotInitialActive;
}
return false;
}
public onCloseClick(isClicked: boolean): void { public onCloseClick(isClicked: boolean): void {
this.isCloseClicked = isClicked; this.isCloseClicked = isClicked;
if (useQueryCopilot.getState().wasCopilotUsed && this.isCopilotTabActive) { if (useQueryCopilot.getState().wasCopilotUsed && this.isCopilotTabActive) {
@ -167,6 +211,16 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
setTimeout(async () => { setTimeout(async () => {
await this._executeQueryDocumentsPage(0); await this._executeQueryDocumentsPage(0);
}, 100); }, 100);
if (this.state.copilotActive) {
const query = this.state.sqlQueryEditorContent.split("\r\n")?.pop();
const isqueryEdited = this.props.copilotStore.generatedQuery && this.props.copilotStore.generatedQuery !== query;
if (isqueryEdited) {
TelemetryProcessor.traceMark(Action.QueryEdited, {
databaseName: this.props.collection.databaseId,
collectionId: this.props.collection.id(),
});
}
}
}; };
public onSaveQueryClick = (): void => { public onSaveQueryClick = (): void => {
@ -326,7 +380,9 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
buttons.push({ buttons.push({
iconSrc: ExecuteQueryIcon, iconSrc: ExecuteQueryIcon,
iconAlt: label, iconAlt: label,
onCommandClick: this.isCopilotTabActive ? () => OnExecuteQueryClick() : this.onExecuteQueryClick, onCommandClick: this.props.isSampleCopilotActive
? () => OnExecuteQueryClick(this.props.copilotStore)
: this.onExecuteQueryClick,
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,
hasPopup: false, hasPopup: false,
@ -380,6 +436,20 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
buttons.push(launchCopilotButton); buttons.push(launchCopilotButton);
} }
if (this.props.copilotEnabled) {
const toggleCopilotButton = {
iconSrc: QueryCommandIcon,
iconAlt: "Copilot",
onCommandClick: () => {
this._toggleCopilot(!this.state.copilotActive);
},
commandButtonLabel: "Copilot",
ariaLabel: "Copilot",
hasPopup: false,
};
buttons.push(toggleCopilotButton);
}
if (!this.props.isPreferredApiMongoDB && this.state.isExecuting) { if (!this.props.isPreferredApiMongoDB && this.state.isExecuting) {
const label = "Cancel query"; const label = "Cancel query";
buttons.push({ buttons.push({
@ -395,11 +465,31 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
return buttons; return buttons;
} }
private _toggleCopilot = (active: boolean) => {
this.setState({ copilotActive: active });
useQueryCopilot.getState().setCopilotEnabledforExecution(active);
localStorage.setItem(`${userContext.databaseAccount?.id}-queryCopilotToggleStatus`, active.toString());
TelemetryProcessor.traceSuccess(active ? Action.ActivateQueryCopilot : Action.DeactivateQueryCopilot, {
databaseName: this.props.collection.databaseId,
collectionId: this.props.collection.id(),
});
};
componentDidUpdate = (_prevProps: IQueryTabComponentProps, prevState: IQueryTabStates): void => {
if (prevState.copilotActive !== this.state.copilotActive) {
useCommandBar.getState().setContextButtons(this.getTabsButtons());
}
};
public onChangeContent(newContent: string): void { public onChangeContent(newContent: string): void {
this.setState({ this.setState({
sqlQueryEditorContent: newContent, sqlQueryEditorContent: newContent,
queryCopilotGeneratedQuery: "", queryCopilotGeneratedQuery: "",
}); });
if (this.state.copilotActive) {
this.props.copilotStore?.setQuery(newContent);
}
if (this.isPreferredApiMongoDB) { if (this.isPreferredApiMongoDB) {
if (newContent.length > 0) { if (newContent.length > 0) {
this.executeQueryButton = { this.executeQueryButton = {
@ -434,6 +524,10 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
: useQueryCopilot.getState().setSelectedQuery(""); : useQueryCopilot.getState().setSelectedQuery("");
} }
if (this.state.copilotActive) {
this.props.copilotStore?.setSelectedQuery(selectedContent);
}
useCommandBar.getState().setContextButtons(this.getTabsButtons()); useCommandBar.getState().setContextButtons(this.getTabsButtons());
} }
@ -442,6 +536,10 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
return this.state.queryCopilotGeneratedQuery; return this.state.queryCopilotGeneratedQuery;
} }
if (this.state.copilotActive) {
return this.props.copilotStore?.query;
}
return this.state.sqlQueryEditorContent; return this.state.sqlQueryEditorContent;
} }
@ -452,12 +550,15 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
private unsubscribeCopilotSidebar: () => void; private unsubscribeCopilotSidebar: () => void;
componentDidMount(): void { componentDidMount(): void {
this.unsubscribeCopilotSidebar = useQueryCopilot.subscribe((state: QueryCopilotState) => { useTabs.subscribe((state: TabsState) => {
if (this.state.showCopilotSidebar !== state.showCopilotSidebar) { if (this.state.currentTabActive && state.activeTab?.tabId !== this.props.tabId) {
this.setState({ showCopilotSidebar: state.showCopilotSidebar }); this.setState({
} currentTabActive: false,
if (this.state.queryCopilotGeneratedQuery !== state.query) { });
this.setState({ queryCopilotGeneratedQuery: state.query }); } else if (!this.state.currentTabActive && state.activeTab?.tabId === this.props.tabId) {
this.setState({
currentTabActive: true,
});
} }
}); });
@ -466,7 +567,6 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
} }
componentWillUnmount(): void { componentWillUnmount(): void {
this.unsubscribeCopilotSidebar();
document.removeEventListener("keydown", this.handleCopilotKeyDown); document.removeEventListener("keydown", this.handleCopilotKeyDown);
} }
@ -474,6 +574,14 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
return ( return (
<Fragment> <Fragment>
<div className="tab-pane" id={this.props.tabId} role="tabpanel"> <div className="tab-pane" id={this.props.tabId} role="tabpanel">
{this.props.copilotEnabled && this.state.currentTabActive && this.state.copilotActive && (
<QueryCopilotPromptbar
explorer={this.props.collection.container}
toggleCopilot={this._toggleCopilot}
databaseId={this.props.collection.databaseId}
containerId={this.props.collection.id()}
></QueryCopilotPromptbar>
)}
<div className="tabPaneContentContainer"> <div className="tabPaneContentContainer">
<SplitterLayout vertical={true} primaryIndex={0} primaryMinSize={100} secondaryMinSize={200}> <SplitterLayout vertical={true} primaryIndex={0} primaryMinSize={100} secondaryMinSize={200}>
<Fragment> <Fragment>
@ -482,6 +590,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
language={"sql"} language={"sql"}
content={this.setEditorContent()} content={this.setEditorContent()}
isReadOnly={false} isReadOnly={false}
wordWrap={"on"}
ariaLabel={"Editing Query"} ariaLabel={"Editing Query"}
lineNumbers={"on"} lineNumbers={"on"}
onContentChanged={(newContent: string) => this.onChangeContent(newContent)} onContentChanged={(newContent: string) => this.onChangeContent(newContent)}
@ -489,8 +598,21 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
/> />
</div> </div>
</Fragment> </Fragment>
{this.isCopilotTabActive ? ( {this.props.isSampleCopilotActive ? (
<QueryCopilotResults /> <QueryResultSection
isMongoDB={this.props.isPreferredApiMongoDB}
queryEditorContent={this.state.sqlQueryEditorContent}
error={this.props.copilotStore?.errorMessage}
queryResults={this.props.copilotStore?.queryResults}
isExecuting={this.props.copilotStore?.isExecuting}
executeQueryDocumentsPage={(firstItemIndex: number) =>
QueryDocumentsPerPage(
firstItemIndex,
this.props.copilotStore.queryIterator,
this.props.copilotStore,
)
}
/>
) : ( ) : (
<QueryResultSection <QueryResultSection
isMongoDB={this.props.isPreferredApiMongoDB} isMongoDB={this.props.isPreferredApiMongoDB}
@ -506,6 +628,14 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
</SplitterLayout> </SplitterLayout>
</div> </div>
</div> </div>
{this.props.copilotEnabled && this.props.copilotStore?.showFeedbackModal && (
<QueryCopilotFeedbackModal
explorer={this.props.collection.container}
databaseId={this.props.collection.databaseId}
containerId={this.props.collection.id()}
mode={this.props.isSampleCopilotActive ? "Sample" : "User"}
/>
)}
</Fragment> </Fragment>
); );
} }

View File

@ -11,7 +11,6 @@ import { QuickstartTab } from "Explorer/Tabs/QuickstartTab";
import { VcoreMongoConnectTab } from "Explorer/Tabs/VCoreMongoConnectTab"; import { VcoreMongoConnectTab } from "Explorer/Tabs/VCoreMongoConnectTab";
import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab"; import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab";
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";
@ -170,7 +169,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(); // 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)}
@ -256,6 +255,7 @@ const isQueryErrorThrown = (tab?: Tab, tabKind?: ReactTabKind): boolean => {
}; };
const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): JSX.Element => { const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): JSX.Element => {
// eslint-disable-next-line no-console
switch (activeReactTab) { switch (activeReactTab) {
case ReactTabKind.Connect: case ReactTabKind.Connect:
return userContext.apiType === "VCoreMongo" ? ( return userContext.apiType === "VCoreMongo" ? (

View File

@ -3,15 +3,15 @@ import * as Constants from "../../Common/Constants";
import * as ThemeUtility from "../../Common/ThemeUtility"; import * as ThemeUtility from "../../Common/ThemeUtility";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { useNotificationConsole } from "../../hooks/useNotificationConsole";
import { useTabs } from "../../hooks/useTabs";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { useNotificationConsole } from "../../hooks/useNotificationConsole";
import { useTabs } from "../../hooks/useTabs";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
import { useSelectedNode } from "../useSelectedNode";
import { WaitsForTemplateViewModel } from "../WaitsForTemplateViewModel"; import { WaitsForTemplateViewModel } from "../WaitsForTemplateViewModel";
import { useSelectedNode } from "../useSelectedNode";
// TODO: Use specific actions for logging telemetry data // TODO: Use specific actions for logging telemetry data
export default class TabsBase extends WaitsForTemplateViewModel { export default class TabsBase extends WaitsForTemplateViewModel {
private static id = 0; private static id = 0;

View File

@ -177,8 +177,8 @@ export default class Collection implements ViewModels.Collection {
this.children.subscribe(() => { this.children.subscribe(() => {
// update the database in zustand store // update the database in zustand store
const database = this.getDatabase(); const database = this.getDatabase();
database.collections( database?.collections(
database.collections()?.map((collection) => { database?.collections()?.map((collection) => {
if (collection.id() === this.id()) { if (collection.id() === this.id()) {
return this; return this;
} }

View File

@ -156,6 +156,6 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
} }
public getDatabase(): ViewModels.Database { public getDatabase(): ViewModels.Database {
return useDatabases.getState().findDatabaseWithId(this.databaseId); return useDatabases.getState().findDatabaseWithId(this.databaseId, this.isSampleCollection);
} }
} }

View File

@ -1,6 +1,7 @@
import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react"; import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react";
import { SampleDataTree } from "Explorer/Tree/SampleDataTree"; import { SampleDataTree } from "Explorer/Tree/SampleDataTree";
import { getItemName } from "Utils/APITypeUtils"; import { getItemName } from "Utils/APITypeUtils";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import * as React from "react"; import * as React from "react";
import shallow from "zustand/shallow"; import shallow from "zustand/shallow";
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
@ -767,7 +768,8 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
}; };
const dataRootNode = buildDataTree(); const dataRootNode = buildDataTree();
const isSampleDataEnabled = userContext.sampleDataConnectionInfo && userContext.apiType === "SQL"; const isSampleDataEnabled =
useQueryCopilot().copilotEnabled && userContext.sampleDataConnectionInfo && userContext.apiType === "SQL";
const sampleDataResourceTokenCollection = useDatabases((state) => state.sampleDataResourceTokenCollection); const sampleDataResourceTokenCollection = useDatabases((state) => state.sampleDataResourceTokenCollection);
return ( return (

View File

@ -14,7 +14,7 @@ interface DatabasesState {
deleteDatabase: (database: ViewModels.Database) => void; deleteDatabase: (database: ViewModels.Database) => void;
clearDatabases: () => void; clearDatabases: () => void;
isSaveQueryEnabled: () => boolean; isSaveQueryEnabled: () => boolean;
findDatabaseWithId: (databaseId: string) => ViewModels.Database; findDatabaseWithId: (databaseId: string, isSampleDatabase?: boolean) => ViewModels.Database;
isLastNonEmptyDatabase: () => boolean; isLastNonEmptyDatabase: () => boolean;
findCollection: (databaseId: string, collectionId: string) => ViewModels.Collection; findCollection: (databaseId: string, collectionId: string) => ViewModels.Collection;
isLastCollection: () => boolean; isLastCollection: () => boolean;
@ -33,7 +33,7 @@ export const useDatabases: UseStore<DatabasesState> = create((set, get) => ({
updateDatabase: (updatedDatabase: ViewModels.Database) => updateDatabase: (updatedDatabase: ViewModels.Database) =>
set((state) => { set((state) => {
const updatedDatabases = state.databases.map((database: ViewModels.Database) => { const updatedDatabases = state.databases.map((database: ViewModels.Database) => {
if (database.id() === updatedDatabase.id()) { if (database?.id() === updatedDatabase?.id()) {
return updatedDatabase; return updatedDatabase;
} }
@ -67,7 +67,9 @@ export const useDatabases: UseStore<DatabasesState> = create((set, get) => ({
} }
return true; return true;
}, },
findDatabaseWithId: (databaseId: string) => get().databases.find((db) => databaseId === db.id()), findDatabaseWithId: (databaseId: string, isSampleDatabase?: boolean) => {
return get().databases.find((db) => databaseId === db.id() && db.isSampleDB === isSampleDatabase);
},
isLastNonEmptyDatabase: () => { isLastNonEmptyDatabase: () => {
const databases = get().databases; const databases = get().databases;
return databases.length === 1 && (databases[0].collections()?.length > 0 || !!databases[0].offer()); return databases.length === 1 && (databases[0].collections()?.length > 0 || !!databases[0].offer());

View File

@ -1,13 +1,13 @@
// CSS Dependencies // CSS Dependencies
import { initializeIcons, loadTheme } from "@fluentui/react"; import { initializeIcons, loadTheme } from "@fluentui/react";
import "bootstrap/dist/css/bootstrap.css";
import { QuickstartCarousel } from "Explorer/Quickstart/QuickstartCarousel"; import { QuickstartCarousel } from "Explorer/Quickstart/QuickstartCarousel";
import { MongoQuickstartTutorial } from "Explorer/Quickstart/Tutorials/MongoQuickstartTutorial"; import { MongoQuickstartTutorial } from "Explorer/Quickstart/Tutorials/MongoQuickstartTutorial";
import { SQLQuickstartTutorial } from "Explorer/Quickstart/Tutorials/SQLQuickstartTutorial"; import { SQLQuickstartTutorial } from "Explorer/Quickstart/Tutorials/SQLQuickstartTutorial";
import { userContext } from "UserContext";
import "bootstrap/dist/css/bootstrap.css";
import { useCarousel } from "hooks/useCarousel"; import { useCarousel } from "hooks/useCarousel";
import React, { useState } from "react"; import React, { useState } from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { userContext } from "UserContext";
import "../externals/jquery-ui.min.css"; import "../externals/jquery-ui.min.css";
import "../externals/jquery-ui.min.js"; import "../externals/jquery-ui.min.js";
import "../externals/jquery-ui.structure.min.css"; import "../externals/jquery-ui.structure.min.css";
@ -18,21 +18,19 @@ import "../externals/jquery.typeahead.min.js";
// Image Dependencies // Image Dependencies
import { Platform } from "ConfigContext"; import { Platform } from "ConfigContext";
import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel"; import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel";
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import "../images/CosmosDB_rgb_ui_lighttheme.ico"; import "../images/CosmosDB_rgb_ui_lighttheme.ico";
import "../images/favicon.ico";
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg"; import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
import "../images/favicon.ico";
import "../less/TableStyles/CustomizeColumns.less";
import "../less/TableStyles/EntityEditor.less";
import "../less/TableStyles/fulldatatables.less";
import "../less/TableStyles/queryBuilder.less";
import "../less/documentDB.less"; import "../less/documentDB.less";
import "../less/forms.less"; import "../less/forms.less";
import "../less/infobox.less"; import "../less/infobox.less";
import "../less/menus.less"; import "../less/menus.less";
import "../less/messagebox.less"; import "../less/messagebox.less";
import "../less/resourceTree.less"; import "../less/resourceTree.less";
import "../less/TableStyles/CustomizeColumns.less";
import "../less/TableStyles/EntityEditor.less";
import "../less/TableStyles/fulldatatables.less";
import "../less/TableStyles/queryBuilder.less";
import "../less/tree.less"; import "../less/tree.less";
import { CollapsedResourceTree } from "./Common/CollapsedResourceTree"; import { CollapsedResourceTree } from "./Common/CollapsedResourceTree";
import { ResourceTreeContainer } from "./Common/ResourceTreeContainer"; import { ResourceTreeContainer } from "./Common/ResourceTreeContainer";
@ -55,11 +53,11 @@ import "./Explorer/Panes/PanelComponent.less";
import { SidePanel } from "./Explorer/Panes/PanelContainerComponent"; import { SidePanel } from "./Explorer/Panes/PanelContainerComponent";
import "./Explorer/SplashScreen/SplashScreen.less"; import "./Explorer/SplashScreen/SplashScreen.less";
import { Tabs } from "./Explorer/Tabs/Tabs"; import { Tabs } from "./Explorer/Tabs/Tabs";
import { useConfig } from "./hooks/useConfig";
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
import "./Libs/jquery"; import "./Libs/jquery";
import { appThemeFabric } from "./Platform/Fabric/FabricTheme"; import { appThemeFabric } from "./Platform/Fabric/FabricTheme";
import "./Shared/appInsights"; import "./Shared/appInsights";
import { useConfig } from "./hooks/useConfig";
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
initializeIcons(); initializeIcons();
@ -67,7 +65,6 @@ const App: React.FunctionComponent = () => {
const [isLeftPaneExpanded, setIsLeftPaneExpanded] = useState<boolean>(true); const [isLeftPaneExpanded, setIsLeftPaneExpanded] = useState<boolean>(true);
const isCarouselOpen = useCarousel((state) => state.shouldOpen); const isCarouselOpen = useCarousel((state) => state.shouldOpen);
const isCopilotCarouselOpen = useCarousel((state) => state.showCopilotCarousel); const isCopilotCarouselOpen = useCarousel((state) => state.showCopilotCarousel);
const shouldShowModal = useQueryCopilot((state) => state.showFeedbackModal);
const config = useConfig(); const config = useConfig();
if (config?.platform === Platform.Fabric) { if (config?.platform === Platform.Fabric) {
@ -136,7 +133,6 @@ const App: React.FunctionComponent = () => {
{<SQLQuickstartTutorial />} {<SQLQuickstartTutorial />}
{<MongoQuickstartTutorial />} {<MongoQuickstartTutorial />}
{<QueryCopilotCarousel isOpen={isCopilotCarouselOpen} explorer={explorer} />} {<QueryCopilotCarousel isOpen={isCopilotCarouselOpen} explorer={explorer} />}
{shouldShowModal && <QueryCopilotFeedbackModal explorer={explorer} />}
</div> </div>
); );
}; };

View File

@ -112,7 +112,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
enableLegacyMongoShellV2Debug: "true" === get("enablelegacymongoshellv2debug"), enableLegacyMongoShellV2Debug: "true" === get("enablelegacymongoshellv2debug"),
loadLegacyMongoShellFromBE: "true" === get("loadlegacymongoshellfrombe"), loadLegacyMongoShellFromBE: "true" === get("loadlegacymongoshellfrombe"),
enableCopilot: "true" === get("enablecopilot", "true"), enableCopilot: "true" === get("enablecopilot", "true"),
copilotVersion: get("copilotversion") ?? "v1.0", copilotVersion: get("copilotversion") ?? "v2.0",
disableCopilotPhoenixGateaway: "true" === get("disablecopilotphoenixgateaway"), disableCopilotPhoenixGateaway: "true" === get("disablecopilotphoenixgateaway"),
enableCopilotFullSchema: "true" === get("enablecopilotfullschema", "true"), enableCopilotFullSchema: "true" === get("enablecopilotfullschema", "true"),
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"), copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),

View File

@ -133,6 +133,10 @@ export enum Action {
CompleteUITour, CompleteUITour,
OpenQueryCopilotFromSplashScreen, OpenQueryCopilotFromSplashScreen,
OpenQueryCopilotFromNewQuery, OpenQueryCopilotFromNewQuery,
ActivateQueryCopilot,
DeactivateQueryCopilot,
QueryGenerationFromCopilotPrompt,
QueryEdited,
ExecuteQueryGeneratedFromQueryCopilot, ExecuteQueryGeneratedFromQueryCopilot,
} }

View File

@ -7,6 +7,7 @@ import {
scheduleRefreshDatabaseResourceToken, scheduleRefreshDatabaseResourceToken,
} from "Platform/Fabric/FabricUtil"; } from "Platform/Fabric/FabricUtil";
import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility"; import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import { ReactTabKind, useTabs } from "hooks/useTabs"; import { ReactTabKind, useTabs } from "hooks/useTabs";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
@ -79,7 +80,7 @@ export function useKnockoutExplorer(platform: Platform): Explorer {
if (explorer) { if (explorer) {
applyExplorerBindings(explorer); applyExplorerBindings(explorer);
if (userContext.features.enableCopilot) { if (userContext.features.enableCopilot) {
updateContextForSampleData(explorer); updateContextForCopilot(explorer).then(() => updateContextForSampleData(explorer));
} }
} }
}, [explorer]); }, [explorer]);
@ -554,12 +555,23 @@ interface PortalMessage {
inputs?: DataExplorerInputsFrame; inputs?: DataExplorerInputsFrame;
} }
async function updateContextForCopilot(explorer: Explorer): Promise<void> {
await explorer.configureCopilot();
}
async function updateContextForSampleData(explorer: Explorer): Promise<void> { async function updateContextForSampleData(explorer: Explorer): Promise<void> {
if (!userContext.features.enableCopilot) { const copilotEnabled =
userContext.apiType === "SQL" && userContext.features.enableCopilot && useQueryCopilot.getState().copilotEnabled;
if (!copilotEnabled) {
return; return;
} }
const url = createUri(`${configContext.BACKEND_ENDPOINT}`, `/api/tokens/sampledataconnection`); const sampleDatabaseEndpoint = useQueryCopilot.getState().copilotUserDBEnabled
? `/api/tokens/sampledataconnection/v2`
: `/api/tokens/sampledataconnection`;
const url = createUri(`${configContext.BACKEND_ENDPOINT}`, sampleDatabaseEndpoint);
const authorizationHeader = getAuthorizationHeader(); const authorizationHeader = getAuthorizationHeader();
const headers = { [authorizationHeader.header]: authorizationHeader.token }; const headers = { [authorizationHeader.header]: authorizationHeader.token };

View File

@ -1,6 +1,6 @@
import { MinimalQueryIterator } from "Common/IteratorUtilities"; import { MinimalQueryIterator } from "Common/IteratorUtilities";
import { QueryResults } from "Contracts/ViewModels"; import { QueryResults } from "Contracts/ViewModels";
import { CopilotMessage } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces"; import { CopilotMessage, CopilotSchemaAllocationInfo } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
import { guid } from "Explorer/Tables/Utilities"; import { guid } from "Explorer/Tables/Utilities";
import { useTabs } from "hooks/useTabs"; import { useTabs } from "hooks/useTabs";
import create, { UseStore } from "zustand"; import create, { UseStore } from "zustand";
@ -8,6 +8,8 @@ import * as DataModels from "../Contracts/DataModels";
import { ContainerInfo } from "../Contracts/DataModels"; import { ContainerInfo } from "../Contracts/DataModels";
export interface QueryCopilotState { export interface QueryCopilotState {
copilotEnabled: boolean;
copilotUserDBEnabled: boolean;
generatedQuery: string; generatedQuery: string;
likeQuery: boolean; likeQuery: boolean;
userPrompt: string; userPrompt: string;
@ -40,8 +42,14 @@ export interface QueryCopilotState {
showExplanationBubble: boolean; showExplanationBubble: boolean;
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo; notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
containerStatus: ContainerInfo; containerStatus: ContainerInfo;
schemaAllocationInfo: CopilotSchemaAllocationInfo;
isAllocatingContainer: boolean; isAllocatingContainer: boolean;
copilotEnabledforExecution: boolean;
getState?: () => QueryCopilotState;
setCopilotEnabled: (copilotEnabled: boolean) => void;
setCopilotUserDBEnabled: (copilotUserDBEnabled: boolean) => void;
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;
@ -76,6 +84,8 @@ export interface QueryCopilotState {
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void; setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void;
setContainerStatus: (containerStatus: ContainerInfo) => void; setContainerStatus: (containerStatus: ContainerInfo) => void;
setIsAllocatingContainer: (isAllocatingContainer: boolean) => void; setIsAllocatingContainer: (isAllocatingContainer: boolean) => void;
setSchemaAllocationInfo: (schemaAllocationInfo: CopilotSchemaAllocationInfo) => void;
setCopilotEnabledforExecution: (copilotEnabledforExecution: boolean) => void;
resetContainerConnection: () => void; resetContainerConnection: () => void;
resetQueryCopilotStates: () => void; resetQueryCopilotStates: () => void;
@ -84,6 +94,8 @@ export interface QueryCopilotState {
type QueryCopilotStore = UseStore<QueryCopilotState>; type QueryCopilotStore = UseStore<QueryCopilotState>;
export const useQueryCopilot: QueryCopilotStore = create((set) => ({ export const useQueryCopilot: QueryCopilotStore = create((set) => ({
copilotEnabled: false,
copilotUserDBEnabled: false,
generatedQuery: "", generatedQuery: "",
likeQuery: false, likeQuery: false,
userPrompt: "", userPrompt: "",
@ -124,8 +136,15 @@ export const useQueryCopilot: QueryCopilotStore = create((set) => ({
durationLeftInMinutes: undefined, durationLeftInMinutes: undefined,
phoenixServerInfo: undefined, phoenixServerInfo: undefined,
}, },
schemaAllocationInfo: {
databaseId: undefined,
containerId: undefined,
},
isAllocatingContainer: false, isAllocatingContainer: false,
copilotEnabledforExecution: false,
setCopilotEnabled: (copilotEnabled: boolean) => set({ copilotEnabled }),
setCopilotUserDBEnabled: (copilotUserDBEnabled: boolean) => set({ copilotUserDBEnabled }),
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({ showFeedbackModal: false }), closeFeedbackModal: () => set({ showFeedbackModal: false }),
@ -163,6 +182,8 @@ export const useQueryCopilot: QueryCopilotStore = create((set) => ({
set({ notebookServerInfo }), set({ notebookServerInfo }),
setContainerStatus: (containerStatus: ContainerInfo) => set({ containerStatus }), setContainerStatus: (containerStatus: ContainerInfo) => set({ containerStatus }),
setIsAllocatingContainer: (isAllocatingContainer: boolean) => set({ isAllocatingContainer }), setIsAllocatingContainer: (isAllocatingContainer: boolean) => set({ isAllocatingContainer }),
setSchemaAllocationInfo: (schemaAllocationInfo: CopilotSchemaAllocationInfo) => set({ schemaAllocationInfo }),
setCopilotEnabledforExecution: (copilotEnabledforExecution: boolean) => set({ copilotEnabledforExecution }),
resetContainerConnection: (): void => { resetContainerConnection: (): void => {
useTabs.getState().closeAllNotebookTabs(true); useTabs.getState().closeAllNotebookTabs(true);
@ -173,6 +194,10 @@ export const useQueryCopilot: QueryCopilotStore = create((set) => ({
durationLeftInMinutes: undefined, durationLeftInMinutes: undefined,
phoenixServerInfo: undefined, phoenixServerInfo: undefined,
}); });
useQueryCopilot.getState().setSchemaAllocationInfo({
databaseId: undefined,
containerId: undefined,
});
}, },
resetQueryCopilotStates: () => { resetQueryCopilotStates: () => {
@ -217,6 +242,10 @@ export const useQueryCopilot: QueryCopilotStore = create((set) => ({
durationLeftInMinutes: undefined, durationLeftInMinutes: undefined,
phoenixServerInfo: undefined, phoenixServerInfo: undefined,
}, },
schemaAllocationInfo: {
databaseId: undefined,
containerId: undefined,
},
isAllocatingContainer: false, isAllocatingContainer: false,
})); }));
}, },

View File

@ -5,7 +5,7 @@ import NotebookTabV2 from "../Explorer/Tabs/NotebookV2Tab";
import TabsBase from "../Explorer/Tabs/TabsBase"; import TabsBase from "../Explorer/Tabs/TabsBase";
import { Platform, configContext } from "./../ConfigContext"; import { Platform, configContext } from "./../ConfigContext";
interface TabsState { export interface TabsState {
openedTabs: TabsBase[]; openedTabs: TabsBase[];
openedReactTabs: ReactTabKind[]; openedReactTabs: ReactTabKind[];
activeTab: TabsBase | undefined; activeTab: TabsBase | undefined;