mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-05-03 15:04:04 +01:00
[Query Copilot] V2 Backend integration (#1584)
* Initial implementetation of backend integration * Added parameters and interfaces moved * Initial client implementation * Additional changes for React FC's * Updated snapshot of Footer * Additional Copilot implementation * Test adjustments and client implementation * Additional test implementations * Naming convetion for functions * Changing {} to any * Additional changes to the type * Additional test changes * Removal of prevention * adding comment * Additional changes to tests * Moving logic based on comments * client implementation along with corrected tests --------- Co-authored-by: Predrag Klepic <v-prklepic@microsoft.com>
This commit is contained in:
parent
8405dbe8da
commit
449118a1bf
@ -1,7 +1,7 @@
|
|||||||
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 { submitFeedback } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
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 { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
@ -10,8 +10,8 @@ import React from "react";
|
|||||||
jest.mock("Utils/UserUtils");
|
jest.mock("Utils/UserUtils");
|
||||||
(getUserEmail as jest.Mock).mockResolvedValue("test@email.com");
|
(getUserEmail as jest.Mock).mockResolvedValue("test@email.com");
|
||||||
|
|
||||||
jest.mock("Explorer/QueryCopilot/QueryCopilotUtilities");
|
jest.mock("Explorer/QueryCopilot/Shared/QueryCopilotClient");
|
||||||
submitFeedback as jest.Mock;
|
SubmitFeedback as jest.Mock;
|
||||||
|
|
||||||
describe("Query Copilot Feedback Modal snapshot test", () => {
|
describe("Query Copilot Feedback Modal snapshot test", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -110,8 +110,8 @@ describe("Query Copilot Feedback Modal snapshot test", () => {
|
|||||||
submitButton.simulate("click");
|
submitButton.simulate("click");
|
||||||
wrapper.setProps({});
|
wrapper.setProps({});
|
||||||
|
|
||||||
expect(submitFeedback).toHaveBeenCalledTimes(1);
|
expect(SubmitFeedback).toHaveBeenCalledTimes(1);
|
||||||
expect(submitFeedback).toHaveBeenCalledWith({
|
expect(SubmitFeedback).toHaveBeenCalledWith({
|
||||||
params: {
|
params: {
|
||||||
likeQuery: false,
|
likeQuery: false,
|
||||||
generatedQuery: "",
|
generatedQuery: "",
|
||||||
|
@ -11,7 +11,7 @@ import {
|
|||||||
TextField,
|
TextField,
|
||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import { submitFeedback } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
import { SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { getUserEmail } from "../../../Utils/UserUtils";
|
import { getUserEmail } from "../../../Utils/UserUtils";
|
||||||
@ -101,7 +101,7 @@ export const QueryCopilotFeedbackModal = ({ explorer }: { explorer: Explorer }):
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
closeFeedbackModal();
|
closeFeedbackModal();
|
||||||
setHideFeedbackModalForLikedQueries(doNotShowAgainChecked);
|
setHideFeedbackModalForLikedQueries(doNotShowAgainChecked);
|
||||||
submitFeedback({
|
SubmitFeedback({
|
||||||
params: { generatedQuery, likeQuery, description, userPrompt, contact },
|
params: { generatedQuery, likeQuery, description, userPrompt, contact },
|
||||||
explorer: explorer,
|
explorer: explorer,
|
||||||
});
|
});
|
||||||
|
@ -30,14 +30,15 @@ import { queryDocumentsPage } from "Common/dataAccess/queryDocumentsPage";
|
|||||||
import { QueryResults } from "Contracts/ViewModels";
|
import { QueryResults } from "Contracts/ViewModels";
|
||||||
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 Explorer from "Explorer/Explorer";
|
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||||
import { SaveQueryPane } from "Explorer/Panes/SaveQueryPane/SaveQueryPane";
|
import { SaveQueryPane } from "Explorer/Panes/SaveQueryPane/SaveQueryPane";
|
||||||
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 { querySampleDocuments, submitFeedback } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
import { querySampleDocuments } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
|
import { SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
|
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 { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
||||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
@ -61,24 +62,12 @@ interface SuggestedPrompt {
|
|||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface QueryCopilotTabProps {
|
|
||||||
explorer: Explorer;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GenerateSQLQueryResponse {
|
|
||||||
apiVersion: string;
|
|
||||||
sql: string;
|
|
||||||
explanation: string;
|
|
||||||
generateStart: string;
|
|
||||||
generateEnd: 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 },
|
||||||
};
|
};
|
||||||
|
|
||||||
export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({ explorer }: QueryCopilotTabProps): JSX.Element => {
|
export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: QueryCopilotProps): JSX.Element => {
|
||||||
const [copilotTeachingBubbleVisible, { toggle: toggleCopilotTeachingBubbleVisible }] = useBoolean(false);
|
const [copilotTeachingBubbleVisible, { toggle: toggleCopilotTeachingBubbleVisible }] = useBoolean(false);
|
||||||
const inputEdited = useRef(false);
|
const inputEdited = useRef(false);
|
||||||
const {
|
const {
|
||||||
@ -554,7 +543,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({ explorer }: Qu
|
|||||||
target="#likeBtn"
|
target="#likeBtn"
|
||||||
onDismiss={() => {
|
onDismiss={() => {
|
||||||
setShowCallout(false);
|
setShowCallout(false);
|
||||||
submitFeedback({
|
SubmitFeedback({
|
||||||
params: {
|
params: {
|
||||||
generatedQuery: generatedQuery,
|
generatedQuery: generatedQuery,
|
||||||
likeQuery: likeQuery,
|
likeQuery: likeQuery,
|
||||||
|
@ -1,14 +1,9 @@
|
|||||||
import { FeedOptions } from "@azure/cosmos";
|
import { FeedOptions } from "@azure/cosmos";
|
||||||
import { QueryCopilotSampleContainerSchema, ShortenedQueryCopilotSampleContainerSchema } from "Common/Constants";
|
|
||||||
import { handleError } from "Common/ErrorHandlingUtils";
|
import { handleError } from "Common/ErrorHandlingUtils";
|
||||||
import { sampleDataClient } from "Common/SampleDataClient";
|
import { sampleDataClient } from "Common/SampleDataClient";
|
||||||
import { createUri } from "Common/UrlUtility";
|
|
||||||
import * as commonUtils from "Common/dataAccess/queryDocuments";
|
import * as commonUtils from "Common/dataAccess/queryDocuments";
|
||||||
import Explorer from "Explorer/Explorer";
|
|
||||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
|
||||||
import DocumentId from "Explorer/Tree/DocumentId";
|
import DocumentId from "Explorer/Tree/DocumentId";
|
||||||
import { userContext } from "UserContext";
|
import { querySampleDocuments, readSampleDocument } from "./QueryCopilotUtilities";
|
||||||
import { querySampleDocuments, readSampleDocument, submitFeedback } from "./QueryCopilotUtilities";
|
|
||||||
|
|
||||||
jest.mock("Explorer/Tree/DocumentId", () => {
|
jest.mock("Explorer/Tree/DocumentId", () => {
|
||||||
return jest.fn().mockImplementation(() => {
|
return jest.fn().mockImplementation(() => {
|
||||||
@ -65,113 +60,6 @@ jest.mock("hooks/useQueryCopilot", () => {
|
|||||||
describe("QueryCopilotUtilities", () => {
|
describe("QueryCopilotUtilities", () => {
|
||||||
beforeEach(() => jest.clearAllMocks());
|
beforeEach(() => jest.clearAllMocks());
|
||||||
|
|
||||||
describe("submitFeedback", () => {
|
|
||||||
const payload = {
|
|
||||||
like: "like",
|
|
||||||
generatedSql: "GeneratedQuery",
|
|
||||||
userPrompt: "UserPrompt",
|
|
||||||
description: "Description",
|
|
||||||
contact: "Contact",
|
|
||||||
containerSchema: userContext.features.enableCopilotFullSchema
|
|
||||||
? QueryCopilotSampleContainerSchema
|
|
||||||
: ShortenedQueryCopilotSampleContainerSchema,
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockStore = useNotebook.getState();
|
|
||||||
beforeEach(() => {
|
|
||||||
mockStore.notebookServerInfo = {
|
|
||||||
notebookServerEndpoint: "mocked-endpoint",
|
|
||||||
authToken: "mocked-token",
|
|
||||||
forwardingId: "mocked-forwarding-id",
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const feedbackUri = userContext.features.enableCopilotPhoenixGateaway
|
|
||||||
? createUri(useNotebook.getState().notebookServerInfo.notebookServerEndpoint, "feedback")
|
|
||||||
: createUri("https://copilotorchestrater.azurewebsites.net/", "feedback");
|
|
||||||
|
|
||||||
it("should call fetch with the payload with like", async () => {
|
|
||||||
const mockFetch = jest.fn().mockResolvedValueOnce({});
|
|
||||||
|
|
||||||
globalThis.fetch = mockFetch;
|
|
||||||
await submitFeedback({
|
|
||||||
params: {
|
|
||||||
likeQuery: true,
|
|
||||||
generatedQuery: "GeneratedQuery",
|
|
||||||
userPrompt: "UserPrompt",
|
|
||||||
description: "Description",
|
|
||||||
contact: "Contact",
|
|
||||||
},
|
|
||||||
explorer: new Explorer(),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(mockFetch).toHaveBeenCalledWith(
|
|
||||||
feedbackUri,
|
|
||||||
expect.objectContaining({
|
|
||||||
headers: expect.objectContaining({
|
|
||||||
"x-ms-correlationid": "mocked-correlation-id",
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const actualBody = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
||||||
expect(actualBody).toEqual(payload);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should call fetch with the payload with unlike and empty parameters", async () => {
|
|
||||||
payload.like = "dislike";
|
|
||||||
payload.description = "";
|
|
||||||
payload.contact = "";
|
|
||||||
const mockFetch = jest.fn().mockResolvedValueOnce({});
|
|
||||||
|
|
||||||
globalThis.fetch = mockFetch;
|
|
||||||
|
|
||||||
await submitFeedback({
|
|
||||||
params: {
|
|
||||||
likeQuery: false,
|
|
||||||
generatedQuery: "GeneratedQuery",
|
|
||||||
userPrompt: "UserPrompt",
|
|
||||||
description: undefined,
|
|
||||||
contact: undefined,
|
|
||||||
},
|
|
||||||
explorer: new Explorer(),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(mockFetch).toHaveBeenCalledWith(
|
|
||||||
feedbackUri,
|
|
||||||
expect.objectContaining({
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"content-type": "application/json",
|
|
||||||
"x-ms-correlationid": "mocked-correlation-id",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const actualBody = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
||||||
expect(actualBody).toEqual(payload);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle errors and call handleError", async () => {
|
|
||||||
globalThis.fetch = jest.fn().mockRejectedValueOnce(new Error("Mock error"));
|
|
||||||
|
|
||||||
await submitFeedback({
|
|
||||||
params: {
|
|
||||||
likeQuery: true,
|
|
||||||
generatedQuery: "GeneratedQuery",
|
|
||||||
userPrompt: "UserPrompt",
|
|
||||||
description: "Description",
|
|
||||||
contact: "Contact",
|
|
||||||
},
|
|
||||||
explorer: new Explorer(),
|
|
||||||
}).catch((error) => {
|
|
||||||
expect(error.message).toEqual("Mock error");
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(handleError).toHaveBeenCalledWith(new Error("Mock error"), expect.any(String));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("querySampleDocuments", () => {
|
describe("querySampleDocuments", () => {
|
||||||
(sampleDataClient as jest.Mock).mockReturnValue({
|
(sampleDataClient as jest.Mock).mockReturnValue({
|
||||||
database: jest.fn().mockReturnValue({
|
database: jest.fn().mockReturnValue({
|
||||||
|
@ -1,73 +1,11 @@
|
|||||||
import { FeedOptions, Item, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
import { FeedOptions, Item, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||||
import {
|
import { QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
|
||||||
QueryCopilotSampleContainerId,
|
|
||||||
QueryCopilotSampleContainerSchema,
|
|
||||||
QueryCopilotSampleDatabaseId,
|
|
||||||
ShortenedQueryCopilotSampleContainerSchema,
|
|
||||||
} from "Common/Constants";
|
|
||||||
import { handleError } from "Common/ErrorHandlingUtils";
|
import { handleError } from "Common/ErrorHandlingUtils";
|
||||||
import { sampleDataClient } from "Common/SampleDataClient";
|
import { sampleDataClient } from "Common/SampleDataClient";
|
||||||
import { createUri } from "Common/UrlUtility";
|
|
||||||
import { getPartitionKeyValue } from "Common/dataAccess/getPartitionKeyValue";
|
import { getPartitionKeyValue } from "Common/dataAccess/getPartitionKeyValue";
|
||||||
import { getCommonQueryOptions } from "Common/dataAccess/queryDocuments";
|
import { getCommonQueryOptions } from "Common/dataAccess/queryDocuments";
|
||||||
import Explorer from "Explorer/Explorer";
|
|
||||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
|
||||||
import DocumentId from "Explorer/Tree/DocumentId";
|
import DocumentId from "Explorer/Tree/DocumentId";
|
||||||
import { userContext } from "UserContext";
|
|
||||||
import { logConsoleProgress } from "Utils/NotificationConsoleUtils";
|
import { logConsoleProgress } from "Utils/NotificationConsoleUtils";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
|
||||||
|
|
||||||
interface FeedbackParams {
|
|
||||||
likeQuery: boolean;
|
|
||||||
generatedQuery: string;
|
|
||||||
userPrompt: string;
|
|
||||||
description?: string;
|
|
||||||
contact?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const submitFeedback = async ({
|
|
||||||
params,
|
|
||||||
explorer,
|
|
||||||
}: {
|
|
||||||
params: FeedbackParams;
|
|
||||||
explorer: Explorer;
|
|
||||||
}): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const { likeQuery, generatedQuery, userPrompt, description, contact } = params;
|
|
||||||
const { correlationId, shouldAllocateContainer, setShouldAllocateContainer } = useQueryCopilot();
|
|
||||||
const payload = {
|
|
||||||
containerSchema: userContext.features.enableCopilotFullSchema
|
|
||||||
? QueryCopilotSampleContainerSchema
|
|
||||||
: ShortenedQueryCopilotSampleContainerSchema,
|
|
||||||
like: likeQuery ? "like" : "dislike",
|
|
||||||
generatedSql: generatedQuery,
|
|
||||||
userPrompt,
|
|
||||||
description: description || "",
|
|
||||||
contact: contact || "",
|
|
||||||
};
|
|
||||||
if (shouldAllocateContainer && userContext.features.enableCopilotPhoenixGateaway) {
|
|
||||||
await explorer.allocateContainer();
|
|
||||||
setShouldAllocateContainer(false);
|
|
||||||
}
|
|
||||||
const serverInfo = useNotebook.getState().notebookServerInfo;
|
|
||||||
const feedbackUri = userContext.features.enableCopilotPhoenixGateaway
|
|
||||||
? createUri(serverInfo.notebookServerEndpoint, "feedback")
|
|
||||||
: createUri("https://copilotorchestrater.azurewebsites.net/", "feedback");
|
|
||||||
const response = await fetch(feedbackUri, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"content-type": "application/json",
|
|
||||||
"x-ms-correlationid": correlationId,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(payload),
|
|
||||||
});
|
|
||||||
if (response.status === 404) {
|
|
||||||
setShouldAllocateContainer(true);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, "copilotSubmitFeedback");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
||||||
|
151
src/Explorer/QueryCopilot/Shared/QueryCopilotClient.test.ts
Normal file
151
src/Explorer/QueryCopilot/Shared/QueryCopilotClient.test.ts
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
import { QueryCopilotSampleContainerSchema, ShortenedQueryCopilotSampleContainerSchema } from "Common/Constants";
|
||||||
|
import { handleError } from "Common/ErrorHandlingUtils";
|
||||||
|
import { createUri } from "Common/UrlUtility";
|
||||||
|
import Explorer from "Explorer/Explorer";
|
||||||
|
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||||
|
import { SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
|
import { userContext } from "UserContext";
|
||||||
|
|
||||||
|
jest.mock("@azure/cosmos", () => ({
|
||||||
|
Constants: {
|
||||||
|
HttpHeaders: {},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("Common/ErrorHandlingUtils", () => ({
|
||||||
|
handleError: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("Common/SampleDataClient");
|
||||||
|
|
||||||
|
jest.mock("node-fetch");
|
||||||
|
|
||||||
|
jest.mock("Explorer/Explorer", () => {
|
||||||
|
class MockExplorer {
|
||||||
|
allocateContainer = jest.fn().mockResolvedValueOnce({});
|
||||||
|
}
|
||||||
|
return MockExplorer;
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock("hooks/useQueryCopilot", () => {
|
||||||
|
const mockQueryCopilotStore = {
|
||||||
|
shouldAllocateContainer: true,
|
||||||
|
setShouldAllocateContainer: jest.fn(),
|
||||||
|
correlationId: "mocked-correlation-id",
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
useQueryCopilot: jest.fn(() => mockQueryCopilotStore),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Query Copilot Client", () => {
|
||||||
|
beforeEach(() => jest.clearAllMocks());
|
||||||
|
|
||||||
|
describe("submitFeedback", () => {
|
||||||
|
const payload = {
|
||||||
|
like: "like",
|
||||||
|
generatedSql: "GeneratedQuery",
|
||||||
|
userPrompt: "UserPrompt",
|
||||||
|
description: "Description",
|
||||||
|
contact: "Contact",
|
||||||
|
containerSchema: userContext.features.enableCopilotFullSchema
|
||||||
|
? QueryCopilotSampleContainerSchema
|
||||||
|
: ShortenedQueryCopilotSampleContainerSchema,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockStore = useNotebook.getState();
|
||||||
|
beforeEach(() => {
|
||||||
|
mockStore.notebookServerInfo = {
|
||||||
|
notebookServerEndpoint: "mocked-endpoint",
|
||||||
|
authToken: "mocked-token",
|
||||||
|
forwardingId: "mocked-forwarding-id",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const feedbackUri = userContext.features.enableCopilotPhoenixGateaway
|
||||||
|
? createUri(useNotebook.getState().notebookServerInfo.notebookServerEndpoint, "feedback")
|
||||||
|
: createUri("https://copilotorchestrater.azurewebsites.net/", "feedback");
|
||||||
|
|
||||||
|
it("should call fetch with the payload with like", async () => {
|
||||||
|
const mockFetch = jest.fn().mockResolvedValueOnce({});
|
||||||
|
|
||||||
|
globalThis.fetch = mockFetch;
|
||||||
|
await SubmitFeedback({
|
||||||
|
params: {
|
||||||
|
likeQuery: true,
|
||||||
|
generatedQuery: "GeneratedQuery",
|
||||||
|
userPrompt: "UserPrompt",
|
||||||
|
description: "Description",
|
||||||
|
contact: "Contact",
|
||||||
|
},
|
||||||
|
explorer: new Explorer(),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockFetch).toHaveBeenCalledWith(
|
||||||
|
feedbackUri,
|
||||||
|
expect.objectContaining({
|
||||||
|
headers: expect.objectContaining({
|
||||||
|
"x-ms-correlationid": "mocked-correlation-id",
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const actualBody = JSON.parse(mockFetch.mock.calls[0][1].body);
|
||||||
|
expect(actualBody).toEqual(payload);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call fetch with the payload with unlike and empty parameters", async () => {
|
||||||
|
payload.like = "dislike";
|
||||||
|
payload.description = "";
|
||||||
|
payload.contact = "";
|
||||||
|
const mockFetch = jest.fn().mockResolvedValueOnce({});
|
||||||
|
|
||||||
|
globalThis.fetch = mockFetch;
|
||||||
|
|
||||||
|
await SubmitFeedback({
|
||||||
|
params: {
|
||||||
|
likeQuery: false,
|
||||||
|
generatedQuery: "GeneratedQuery",
|
||||||
|
userPrompt: "UserPrompt",
|
||||||
|
description: undefined,
|
||||||
|
contact: undefined,
|
||||||
|
},
|
||||||
|
explorer: new Explorer(),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockFetch).toHaveBeenCalledWith(
|
||||||
|
feedbackUri,
|
||||||
|
expect.objectContaining({
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
"x-ms-correlationid": "mocked-correlation-id",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const actualBody = JSON.parse(mockFetch.mock.calls[0][1].body);
|
||||||
|
expect(actualBody).toEqual(payload);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle errors and call handleError", async () => {
|
||||||
|
globalThis.fetch = jest.fn().mockRejectedValueOnce(new Error("Mock error"));
|
||||||
|
|
||||||
|
await SubmitFeedback({
|
||||||
|
params: {
|
||||||
|
likeQuery: true,
|
||||||
|
generatedQuery: "GeneratedQuery",
|
||||||
|
userPrompt: "UserPrompt",
|
||||||
|
description: "Description",
|
||||||
|
contact: "Contact",
|
||||||
|
},
|
||||||
|
explorer: new Explorer(),
|
||||||
|
}).catch((error) => {
|
||||||
|
expect(error.message).toEqual("Mock error");
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(handleError).toHaveBeenCalledWith(new Error("Mock error"), expect.any(String));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
122
src/Explorer/QueryCopilot/Shared/QueryCopilotClient.ts
Normal file
122
src/Explorer/QueryCopilot/Shared/QueryCopilotClient.ts
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import { QueryCopilotSampleContainerSchema, ShortenedQueryCopilotSampleContainerSchema } from "Common/Constants";
|
||||||
|
import { handleError } from "Common/ErrorHandlingUtils";
|
||||||
|
import { createUri } from "Common/UrlUtility";
|
||||||
|
import Explorer from "Explorer/Explorer";
|
||||||
|
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||||
|
import { FeedbackParams, GenerateSQLQueryResponse } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
||||||
|
import { userContext } from "UserContext";
|
||||||
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
|
import { useTabs } from "hooks/useTabs";
|
||||||
|
|
||||||
|
export const SendQueryRequest = async ({
|
||||||
|
userPrompt,
|
||||||
|
explorer,
|
||||||
|
}: {
|
||||||
|
userPrompt: string;
|
||||||
|
explorer: Explorer;
|
||||||
|
}): Promise<void> => {
|
||||||
|
if (userPrompt.trim() !== "") {
|
||||||
|
useQueryCopilot.getState().setIsGeneratingQuery(true);
|
||||||
|
useTabs.getState().setIsTabExecuting(true);
|
||||||
|
useTabs.getState().setIsQueryErrorThrown(false);
|
||||||
|
useQueryCopilot
|
||||||
|
.getState()
|
||||||
|
.setChatMessages([...useQueryCopilot.getState().chatMessages, { source: 0, message: userPrompt }]);
|
||||||
|
try {
|
||||||
|
if (useQueryCopilot.getState().shouldAllocateContainer) {
|
||||||
|
await explorer.allocateContainer();
|
||||||
|
useQueryCopilot.getState().setShouldAllocateContainer(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
useQueryCopilot.getState().refreshCorrelationId();
|
||||||
|
const serverInfo = useNotebook.getState().notebookServerInfo;
|
||||||
|
const queryUri = createUri(serverInfo.notebookServerEndpoint, "generateSQLQuery");
|
||||||
|
const payload = {
|
||||||
|
containerSchema: ShortenedQueryCopilotSampleContainerSchema,
|
||||||
|
userPrompt: userPrompt,
|
||||||
|
};
|
||||||
|
const response = await fetch(queryUri, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
"x-ms-correlationid": useQueryCopilot.getState().correlationId,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
|
||||||
|
const generateSQLQueryResponse: GenerateSQLQueryResponse = await response?.json();
|
||||||
|
if (response.status === 404) {
|
||||||
|
useQueryCopilot.getState().setShouldAllocateContainer(true);
|
||||||
|
}
|
||||||
|
if (response.ok) {
|
||||||
|
if (generateSQLQueryResponse?.sql) {
|
||||||
|
let query = `Here is a query which will help you with provided prompt.\r\n **Prompt:** ${userPrompt}`;
|
||||||
|
query += `\r\n${generateSQLQueryResponse.sql}`;
|
||||||
|
useQueryCopilot
|
||||||
|
.getState()
|
||||||
|
.setChatMessages([
|
||||||
|
...useQueryCopilot.getState().chatMessages,
|
||||||
|
{ source: 1, message: query, explanation: generateSQLQueryResponse.explanation },
|
||||||
|
]);
|
||||||
|
useQueryCopilot.getState().setGeneratedQuery(generateSQLQueryResponse.sql);
|
||||||
|
useQueryCopilot.getState().setGeneratedQueryComments(generateSQLQueryResponse.explanation);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handleError(JSON.stringify(generateSQLQueryResponse), "copilotInternalServerError");
|
||||||
|
useTabs.getState().setIsQueryErrorThrown(true);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, "executeNaturalLanguageQuery");
|
||||||
|
useTabs.getState().setIsQueryErrorThrown(true);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
useQueryCopilot.getState().setUserPrompt("");
|
||||||
|
useQueryCopilot.getState().setIsGeneratingQuery(false);
|
||||||
|
useTabs.getState().setIsTabExecuting(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SubmitFeedback = async ({
|
||||||
|
params,
|
||||||
|
explorer,
|
||||||
|
}: {
|
||||||
|
params: FeedbackParams;
|
||||||
|
explorer: Explorer;
|
||||||
|
}): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const { likeQuery, generatedQuery, userPrompt, description, contact } = params;
|
||||||
|
const { correlationId, shouldAllocateContainer, setShouldAllocateContainer } = useQueryCopilot();
|
||||||
|
const payload = {
|
||||||
|
containerSchema: userContext.features.enableCopilotFullSchema
|
||||||
|
? QueryCopilotSampleContainerSchema
|
||||||
|
: ShortenedQueryCopilotSampleContainerSchema,
|
||||||
|
like: likeQuery ? "like" : "dislike",
|
||||||
|
generatedSql: generatedQuery,
|
||||||
|
userPrompt,
|
||||||
|
description: description || "",
|
||||||
|
contact: contact || "",
|
||||||
|
};
|
||||||
|
if (shouldAllocateContainer && userContext.features.enableCopilotPhoenixGateaway) {
|
||||||
|
await explorer.allocateContainer();
|
||||||
|
setShouldAllocateContainer(false);
|
||||||
|
}
|
||||||
|
const serverInfo = useNotebook.getState().notebookServerInfo;
|
||||||
|
const feedbackUri = userContext.features.enableCopilotPhoenixGateaway
|
||||||
|
? createUri(serverInfo.notebookServerEndpoint, "feedback")
|
||||||
|
: createUri("https://copilotorchestrater.azurewebsites.net/", "feedback");
|
||||||
|
const response = await fetch(feedbackUri, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
"x-ms-correlationid": correlationId,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
if (response.status === 404) {
|
||||||
|
setShouldAllocateContainer(true);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, "copilotSubmitFeedback");
|
||||||
|
}
|
||||||
|
};
|
32
src/Explorer/QueryCopilot/Shared/QueryCopilotInterfaces.ts
Normal file
32
src/Explorer/QueryCopilot/Shared/QueryCopilotInterfaces.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import Explorer from "Explorer/Explorer";
|
||||||
|
|
||||||
|
export interface GenerateSQLQueryResponse {
|
||||||
|
apiVersion: string;
|
||||||
|
sql: string;
|
||||||
|
explanation: string;
|
||||||
|
generateStart: string;
|
||||||
|
generateEnd: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum MessageSource {
|
||||||
|
User,
|
||||||
|
AI,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CopilotMessage {
|
||||||
|
source: MessageSource;
|
||||||
|
message: string;
|
||||||
|
explanation?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FeedbackParams {
|
||||||
|
likeQuery: boolean;
|
||||||
|
generatedQuery: string;
|
||||||
|
userPrompt: string;
|
||||||
|
description?: string;
|
||||||
|
contact?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueryCopilotProps {
|
||||||
|
explorer: Explorer;
|
||||||
|
}
|
@ -1,17 +1,41 @@
|
|||||||
import { IconButton, Image, TextField } from "@fluentui/react";
|
import { IconButton, Image, TextField } from "@fluentui/react";
|
||||||
|
import Explorer from "Explorer/Explorer";
|
||||||
|
import { SendQueryRequest } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
import { shallow } from "enzyme";
|
import { shallow } from "enzyme";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { act } from "react-dom/test-utils";
|
||||||
import { Footer } from "./Footer";
|
import { Footer } from "./Footer";
|
||||||
|
|
||||||
|
jest.mock("@azure/cosmos", () => ({
|
||||||
|
Constants: {
|
||||||
|
HttpHeaders: {},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("Common/SampleDataClient");
|
||||||
|
|
||||||
|
jest.mock("Explorer/Explorer");
|
||||||
|
|
||||||
|
jest.mock("node-fetch");
|
||||||
|
|
||||||
|
jest.mock("Common/ErrorHandlingUtils", () => ({
|
||||||
|
handleError: jest.fn(),
|
||||||
|
getErrorMessage: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("Explorer/QueryCopilot/Shared/QueryCopilotClient");
|
||||||
|
|
||||||
describe("Footer snapshot test", () => {
|
describe("Footer snapshot test", () => {
|
||||||
|
const testMessage = "test message";
|
||||||
|
|
||||||
const initialStoreState = useQueryCopilot.getState();
|
const initialStoreState = useQueryCopilot.getState();
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
useQueryCopilot.setState(initialStoreState, true);
|
useQueryCopilot.setState(initialStoreState, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should open sample prompts on button click", () => {
|
it("should open sample prompts on button click", () => {
|
||||||
const wrapper = shallow(<Footer />);
|
const wrapper = shallow(<Footer explorer={new Explorer()} />);
|
||||||
|
|
||||||
const samplePromptsImage = wrapper.find(Image).first();
|
const samplePromptsImage = wrapper.find(Image).first();
|
||||||
samplePromptsImage.simulate("click", {});
|
samplePromptsImage.simulate("click", {});
|
||||||
@ -21,7 +45,7 @@ describe("Footer snapshot test", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should update user input", () => {
|
it("should update user input", () => {
|
||||||
const wrapper = shallow(<Footer />);
|
const wrapper = shallow(<Footer explorer={new Explorer()} />);
|
||||||
const newInput = "some new input";
|
const newInput = "some new input";
|
||||||
|
|
||||||
const textInput = wrapper.find(TextField).first();
|
const textInput = wrapper.find(TextField).first();
|
||||||
@ -31,22 +55,24 @@ describe("Footer snapshot test", () => {
|
|||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should pass text with enter key", () => {
|
it("should pass text with enter key", async () => {
|
||||||
const testMessage = "test message";
|
|
||||||
useQueryCopilot.getState().setUserPrompt(testMessage);
|
useQueryCopilot.getState().setUserPrompt(testMessage);
|
||||||
const wrapper = shallow(<Footer />);
|
const wrapper = shallow(<Footer explorer={new Explorer()} />);
|
||||||
|
|
||||||
const textInput = wrapper.find(TextField).first();
|
const textInput = wrapper.find(TextField).first();
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
await act(async () => {
|
||||||
textInput.simulate("keydown", { key: "Enter", shiftKey: false, preventDefault: () => {} });
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
textInput.simulate("keydown", { key: "Enter", shiftKey: false, preventDefault: () => {} });
|
||||||
|
});
|
||||||
|
|
||||||
expect(useQueryCopilot.getState().chatMessages).toEqual([testMessage]);
|
await Promise.resolve();
|
||||||
expect(useQueryCopilot.getState().userPrompt).toEqual("");
|
|
||||||
|
expect(SendQueryRequest).toHaveBeenCalledWith({ userPrompt: testMessage, explorer: expect.any(Explorer) });
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not pass text with non enter key", () => {
|
it("should not pass text with non enter key", () => {
|
||||||
const wrapper = shallow(<Footer />);
|
const wrapper = shallow(<Footer explorer={new Explorer()} />);
|
||||||
|
|
||||||
const textInput = wrapper.find(TextField).first();
|
const textInput = wrapper.find(TextField).first();
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
@ -58,7 +84,7 @@ describe("Footer snapshot test", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should not pass if no text", () => {
|
it("should not pass if no text", () => {
|
||||||
const wrapper = shallow(<Footer />);
|
const wrapper = shallow(<Footer explorer={new Explorer()} />);
|
||||||
|
|
||||||
const textInput = wrapper.find(TextField).first();
|
const textInput = wrapper.find(TextField).first();
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
@ -69,16 +95,18 @@ describe("Footer snapshot test", () => {
|
|||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should pass text with icon button", () => {
|
it("should pass text with icon button", async () => {
|
||||||
const testMessage = "test message";
|
|
||||||
useQueryCopilot.getState().setUserPrompt(testMessage);
|
useQueryCopilot.getState().setUserPrompt(testMessage);
|
||||||
const wrapper = shallow(<Footer />);
|
const wrapper = shallow(<Footer explorer={new Explorer()} />);
|
||||||
|
|
||||||
const iconButton = wrapper.find(IconButton).first();
|
const iconButton = wrapper.find(IconButton).first();
|
||||||
iconButton.simulate("click", {});
|
await act(async () => {
|
||||||
|
iconButton.simulate("click", {});
|
||||||
|
});
|
||||||
|
|
||||||
expect(useQueryCopilot.getState().chatMessages).toEqual([testMessage]);
|
await Promise.resolve();
|
||||||
expect(useQueryCopilot.getState().userPrompt).toEqual("");
|
|
||||||
|
expect(SendQueryRequest).toHaveBeenCalledWith({ userPrompt: testMessage, explorer: expect.any(Explorer) });
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
import { IButtonStyles, IconButton, Image, Stack, TextField } from "@fluentui/react";
|
import { IButtonStyles, IconButton, Image, Stack, TextField } from "@fluentui/react";
|
||||||
|
import { SendQueryRequest } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
|
import { QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import HintIcon from "../../../../../images/Hint.svg";
|
import HintIcon from "../../../../../images/Hint.svg";
|
||||||
import { SamplePrompts, SamplePromptsProps } from "../../Shared/SamplePrompts/SamplePrompts";
|
import { SamplePrompts, SamplePromptsProps } from "../../Shared/SamplePrompts/SamplePrompts";
|
||||||
|
|
||||||
export const Footer: React.FC = (): JSX.Element => {
|
export const Footer: React.FC<QueryCopilotProps> = ({ explorer }: QueryCopilotProps): JSX.Element => {
|
||||||
const {
|
const {
|
||||||
userPrompt,
|
userPrompt,
|
||||||
setUserPrompt,
|
setUserPrompt,
|
||||||
chatMessages,
|
|
||||||
setChatMessages,
|
|
||||||
isSamplePromptsOpen,
|
isSamplePromptsOpen,
|
||||||
setIsSamplePromptsOpen,
|
setIsSamplePromptsOpen,
|
||||||
setIsGeneratingQuery,
|
isGeneratingQuery,
|
||||||
} = useQueryCopilot();
|
} = useQueryCopilot();
|
||||||
|
|
||||||
const promptStyles: IButtonStyles = {
|
const promptStyles: IButtonStyles = {
|
||||||
@ -29,16 +29,12 @@ export const Footer: React.FC = (): JSX.Element => {
|
|||||||
const handleEnterKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
const handleEnterKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (event.key === "Enter" && !event.shiftKey) {
|
if (event.key === "Enter" && !event.shiftKey) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
handleSendMessage();
|
startSentMessageProcess();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSendMessage = () => {
|
const startSentMessageProcess = async () => {
|
||||||
if (userPrompt.trim() !== "") {
|
await SendQueryRequest({ userPrompt, explorer });
|
||||||
setChatMessages([...chatMessages, userPrompt]);
|
|
||||||
setUserPrompt("");
|
|
||||||
setIsGeneratingQuery(true);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -66,6 +62,7 @@ export const Footer: React.FC = (): JSX.Element => {
|
|||||||
onKeyDown={handleEnterKeyPress}
|
onKeyDown={handleEnterKeyPress}
|
||||||
multiline
|
multiline
|
||||||
resizable={false}
|
resizable={false}
|
||||||
|
disabled={isGeneratingQuery}
|
||||||
styles={{
|
styles={{
|
||||||
root: {
|
root: {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
@ -79,7 +76,7 @@ export const Footer: React.FC = (): JSX.Element => {
|
|||||||
fieldGroup: { border: "none" },
|
fieldGroup: { border: "none" },
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<IconButton iconProps={{ iconName: "Send" }} onClick={handleSendMessage} />
|
<IconButton disabled={isGeneratingQuery} iconProps={{ iconName: "Send" }} onClick={startSentMessageProcess} />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -49,6 +49,7 @@ exports[`Footer snapshot test should not pass if no text 1`] = `
|
|||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<StyledTextFieldBase
|
<StyledTextFieldBase
|
||||||
|
disabled={false}
|
||||||
multiline={true}
|
multiline={true}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
onKeyDown={[Function]}
|
onKeyDown={[Function]}
|
||||||
@ -73,6 +74,7 @@ exports[`Footer snapshot test should not pass if no text 1`] = `
|
|||||||
value=""
|
value=""
|
||||||
/>
|
/>
|
||||||
<CustomizedIconButton
|
<CustomizedIconButton
|
||||||
|
disabled={false}
|
||||||
iconProps={
|
iconProps={
|
||||||
Object {
|
Object {
|
||||||
"iconName": "Send",
|
"iconName": "Send",
|
||||||
@ -132,6 +134,7 @@ exports[`Footer snapshot test should not pass text with non enter key 1`] = `
|
|||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<StyledTextFieldBase
|
<StyledTextFieldBase
|
||||||
|
disabled={false}
|
||||||
multiline={true}
|
multiline={true}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
onKeyDown={[Function]}
|
onKeyDown={[Function]}
|
||||||
@ -156,6 +159,7 @@ exports[`Footer snapshot test should not pass text with non enter key 1`] = `
|
|||||||
value=""
|
value=""
|
||||||
/>
|
/>
|
||||||
<CustomizedIconButton
|
<CustomizedIconButton
|
||||||
|
disabled={false}
|
||||||
iconProps={
|
iconProps={
|
||||||
Object {
|
Object {
|
||||||
"iconName": "Send",
|
"iconName": "Send",
|
||||||
@ -215,6 +219,7 @@ exports[`Footer snapshot test should open sample prompts on button click 1`] = `
|
|||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<StyledTextFieldBase
|
<StyledTextFieldBase
|
||||||
|
disabled={false}
|
||||||
multiline={true}
|
multiline={true}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
onKeyDown={[Function]}
|
onKeyDown={[Function]}
|
||||||
@ -239,6 +244,7 @@ exports[`Footer snapshot test should open sample prompts on button click 1`] = `
|
|||||||
value=""
|
value=""
|
||||||
/>
|
/>
|
||||||
<CustomizedIconButton
|
<CustomizedIconButton
|
||||||
|
disabled={false}
|
||||||
iconProps={
|
iconProps={
|
||||||
Object {
|
Object {
|
||||||
"iconName": "Send",
|
"iconName": "Send",
|
||||||
@ -298,6 +304,7 @@ exports[`Footer snapshot test should pass text with enter key 1`] = `
|
|||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<StyledTextFieldBase
|
<StyledTextFieldBase
|
||||||
|
disabled={false}
|
||||||
multiline={true}
|
multiline={true}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
onKeyDown={[Function]}
|
onKeyDown={[Function]}
|
||||||
@ -322,6 +329,7 @@ exports[`Footer snapshot test should pass text with enter key 1`] = `
|
|||||||
value="test message"
|
value="test message"
|
||||||
/>
|
/>
|
||||||
<CustomizedIconButton
|
<CustomizedIconButton
|
||||||
|
disabled={false}
|
||||||
iconProps={
|
iconProps={
|
||||||
Object {
|
Object {
|
||||||
"iconName": "Send",
|
"iconName": "Send",
|
||||||
@ -381,6 +389,7 @@ exports[`Footer snapshot test should pass text with icon button 1`] = `
|
|||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<StyledTextFieldBase
|
<StyledTextFieldBase
|
||||||
|
disabled={false}
|
||||||
multiline={true}
|
multiline={true}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
onKeyDown={[Function]}
|
onKeyDown={[Function]}
|
||||||
@ -405,6 +414,7 @@ exports[`Footer snapshot test should pass text with icon button 1`] = `
|
|||||||
value="test message"
|
value="test message"
|
||||||
/>
|
/>
|
||||||
<CustomizedIconButton
|
<CustomizedIconButton
|
||||||
|
disabled={false}
|
||||||
iconProps={
|
iconProps={
|
||||||
Object {
|
Object {
|
||||||
"iconName": "Send",
|
"iconName": "Send",
|
||||||
@ -464,6 +474,7 @@ exports[`Footer snapshot test should update user input 1`] = `
|
|||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<StyledTextFieldBase
|
<StyledTextFieldBase
|
||||||
|
disabled={false}
|
||||||
multiline={true}
|
multiline={true}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
onKeyDown={[Function]}
|
onKeyDown={[Function]}
|
||||||
@ -488,6 +499,7 @@ exports[`Footer snapshot test should update user input 1`] = `
|
|||||||
value=""
|
value=""
|
||||||
/>
|
/>
|
||||||
<CustomizedIconButton
|
<CustomizedIconButton
|
||||||
|
disabled={false}
|
||||||
iconProps={
|
iconProps={
|
||||||
Object {
|
Object {
|
||||||
"iconName": "Send",
|
"iconName": "Send",
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Stack } from "@fluentui/react";
|
import { Stack } from "@fluentui/react";
|
||||||
|
import Explorer from "Explorer/Explorer";
|
||||||
import { SampleBubble } from "Explorer/QueryCopilot/V2/Bubbles/Sample/SampleBubble";
|
import { SampleBubble } from "Explorer/QueryCopilot/V2/Bubbles/Sample/SampleBubble";
|
||||||
import { shallow } from "enzyme";
|
import { shallow } from "enzyme";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
@ -16,7 +17,7 @@ describe("Query Copilot Sidebar snapshot test", () => {
|
|||||||
it("should render and set copilot used flag ", () => {
|
it("should render and set copilot used flag ", () => {
|
||||||
withHooks(() => {
|
withHooks(() => {
|
||||||
useQueryCopilot.getState().setShowCopilotSidebar(true);
|
useQueryCopilot.getState().setShowCopilotSidebar(true);
|
||||||
const wrapper = shallow(<QueryCopilotSidebar />);
|
const wrapper = shallow(<QueryCopilotSidebar explorer={new Explorer()} />);
|
||||||
|
|
||||||
expect(useQueryCopilot.getState().wasCopilotUsed).toBeTruthy();
|
expect(useQueryCopilot.getState().wasCopilotUsed).toBeTruthy();
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
@ -25,7 +26,7 @@ describe("Query Copilot Sidebar snapshot test", () => {
|
|||||||
|
|
||||||
it("should render and not set copilot used flag ", () => {
|
it("should render and not set copilot used flag ", () => {
|
||||||
withHooks(() => {
|
withHooks(() => {
|
||||||
const wrapper = shallow(<QueryCopilotSidebar />);
|
const wrapper = shallow(<QueryCopilotSidebar explorer={new Explorer()} />);
|
||||||
|
|
||||||
expect(useQueryCopilot.getState().wasCopilotUsed).toBeFalsy();
|
expect(useQueryCopilot.getState().wasCopilotUsed).toBeFalsy();
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
@ -34,8 +35,8 @@ describe("Query Copilot Sidebar snapshot test", () => {
|
|||||||
|
|
||||||
it("should render with chat messages", () => {
|
it("should render with chat messages", () => {
|
||||||
const message = "some test message";
|
const message = "some test message";
|
||||||
useQueryCopilot.getState().setChatMessages([message]);
|
useQueryCopilot.getState().setChatMessages([{ source: 0, message: message }]);
|
||||||
const wrapper = shallow(<QueryCopilotSidebar />);
|
const wrapper = shallow(<QueryCopilotSidebar explorer={new Explorer()} />);
|
||||||
|
|
||||||
const messageContainer = wrapper.find(Stack).findWhere((x) => x.text() === message);
|
const messageContainer = wrapper.find(Stack).findWhere((x) => x.text() === message);
|
||||||
|
|
||||||
@ -44,7 +45,7 @@ describe("Query Copilot Sidebar snapshot test", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should render samples without messages", () => {
|
it("should render samples without messages", () => {
|
||||||
const wrapper = shallow(<QueryCopilotSidebar />);
|
const wrapper = shallow(<QueryCopilotSidebar explorer={new Explorer()} />);
|
||||||
|
|
||||||
const sampleBubble = wrapper.find(SampleBubble);
|
const sampleBubble = wrapper.find(SampleBubble);
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Stack } from "@fluentui/react";
|
import { Stack } from "@fluentui/react";
|
||||||
|
import { QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
||||||
import { RetrievingBubble } from "Explorer/QueryCopilot/V2/Bubbles/Retriveing/RetrievingBubble";
|
import { RetrievingBubble } from "Explorer/QueryCopilot/V2/Bubbles/Retriveing/RetrievingBubble";
|
||||||
import { SampleBubble } from "Explorer/QueryCopilot/V2/Bubbles/Sample/SampleBubble";
|
import { SampleBubble } from "Explorer/QueryCopilot/V2/Bubbles/Sample/SampleBubble";
|
||||||
import { WelcomeBubble } from "Explorer/QueryCopilot/V2/Bubbles/Welcome/WelcomeBubble";
|
import { WelcomeBubble } from "Explorer/QueryCopilot/V2/Bubbles/Welcome/WelcomeBubble";
|
||||||
@ -8,8 +9,14 @@ import { useQueryCopilot } from "hooks/useQueryCopilot";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { WelcomeSidebarModal } from "../Modal/WelcomeSidebarModal";
|
import { WelcomeSidebarModal } from "../Modal/WelcomeSidebarModal";
|
||||||
|
|
||||||
export const QueryCopilotSidebar: React.FC = (): JSX.Element => {
|
export const QueryCopilotSidebar: React.FC<QueryCopilotProps> = ({ explorer }: QueryCopilotProps): JSX.Element => {
|
||||||
const { setWasCopilotUsed, showCopilotSidebar, chatMessages } = useQueryCopilot();
|
const {
|
||||||
|
setWasCopilotUsed,
|
||||||
|
showCopilotSidebar,
|
||||||
|
chatMessages,
|
||||||
|
showWelcomeSidebar,
|
||||||
|
isGeneratingQuery,
|
||||||
|
} = useQueryCopilot();
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (showCopilotSidebar) {
|
if (showCopilotSidebar) {
|
||||||
@ -20,37 +27,58 @@ export const QueryCopilotSidebar: React.FC = (): JSX.Element => {
|
|||||||
return (
|
return (
|
||||||
<Stack style={{ width: "100%", height: "100%", backgroundColor: "#FAFAFA" }}>
|
<Stack style={{ width: "100%", height: "100%", backgroundColor: "#FAFAFA" }}>
|
||||||
<Header />
|
<Header />
|
||||||
<WelcomeSidebarModal />
|
{showWelcomeSidebar ? (
|
||||||
<Stack
|
<WelcomeSidebarModal />
|
||||||
style={{
|
) : (
|
||||||
flexGrow: 1,
|
<>
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
overflowY: "auto",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<WelcomeBubble />
|
|
||||||
{chatMessages.map((message, index) => (
|
|
||||||
<Stack
|
<Stack
|
||||||
key={index}
|
|
||||||
horizontalAlign="center"
|
|
||||||
tokens={{ padding: 8, childrenGap: 8 }}
|
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "#E0E7FF",
|
flexGrow: 1,
|
||||||
borderRadius: "8px",
|
display: "flex",
|
||||||
margin: "5px 10px",
|
flexDirection: "column",
|
||||||
textAlign: "start",
|
overflowY: "auto",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{message}
|
<WelcomeBubble />
|
||||||
|
{chatMessages.map((message, index) =>
|
||||||
|
message.source === 0 ? (
|
||||||
|
<Stack
|
||||||
|
key={index}
|
||||||
|
horizontalAlign="center"
|
||||||
|
tokens={{ padding: 8, childrenGap: 8 }}
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#E0E7FF",
|
||||||
|
borderRadius: "8px",
|
||||||
|
margin: "5px 10px",
|
||||||
|
textAlign: "start",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{message.message}
|
||||||
|
</Stack>
|
||||||
|
) : (
|
||||||
|
<Stack
|
||||||
|
key={index}
|
||||||
|
horizontalAlign="center"
|
||||||
|
tokens={{ padding: 8, childrenGap: 8 }}
|
||||||
|
style={{
|
||||||
|
backgroundColor: "white",
|
||||||
|
borderRadius: "8px",
|
||||||
|
margin: "5px 10px",
|
||||||
|
textAlign: "start",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{message.message}
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
|
||||||
|
<RetrievingBubble />
|
||||||
|
|
||||||
|
{chatMessages.length === 0 && !isGeneratingQuery && <SampleBubble />}
|
||||||
</Stack>
|
</Stack>
|
||||||
))}
|
<Footer explorer={explorer} />
|
||||||
|
</>
|
||||||
<RetrievingBubble />
|
)}
|
||||||
|
|
||||||
{chatMessages.length === 0 && <SampleBubble />}
|
|
||||||
</Stack>
|
|
||||||
<Footer />
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -12,21 +12,6 @@ exports[`Query Copilot Sidebar snapshot test should render and not set copilot u
|
|||||||
>
|
>
|
||||||
<Header />
|
<Header />
|
||||||
<WelcomeSidebarModal />
|
<WelcomeSidebarModal />
|
||||||
<Stack
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"display": "flex",
|
|
||||||
"flexDirection": "column",
|
|
||||||
"flexGrow": 1,
|
|
||||||
"overflowY": "auto",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<WelcomeBubble />
|
|
||||||
<RetrievingBubble />
|
|
||||||
<SampleBubble />
|
|
||||||
</Stack>
|
|
||||||
<Footer />
|
|
||||||
</Stack>
|
</Stack>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -42,21 +27,6 @@ exports[`Query Copilot Sidebar snapshot test should render and set copilot used
|
|||||||
>
|
>
|
||||||
<Header />
|
<Header />
|
||||||
<WelcomeSidebarModal />
|
<WelcomeSidebarModal />
|
||||||
<Stack
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"display": "flex",
|
|
||||||
"flexDirection": "column",
|
|
||||||
"flexGrow": 1,
|
|
||||||
"overflowY": "auto",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<WelcomeBubble />
|
|
||||||
<RetrievingBubble />
|
|
||||||
<SampleBubble />
|
|
||||||
</Stack>
|
|
||||||
<Footer />
|
|
||||||
</Stack>
|
</Stack>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -72,21 +42,6 @@ exports[`Query Copilot Sidebar snapshot test should render samples without messa
|
|||||||
>
|
>
|
||||||
<Header />
|
<Header />
|
||||||
<WelcomeSidebarModal />
|
<WelcomeSidebarModal />
|
||||||
<Stack
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"display": "flex",
|
|
||||||
"flexDirection": "column",
|
|
||||||
"flexGrow": 1,
|
|
||||||
"overflowY": "auto",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<WelcomeBubble />
|
|
||||||
<RetrievingBubble />
|
|
||||||
<SampleBubble />
|
|
||||||
</Stack>
|
|
||||||
<Footer />
|
|
||||||
</Stack>
|
</Stack>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -102,39 +57,5 @@ exports[`Query Copilot Sidebar snapshot test should render with chat messages 1`
|
|||||||
>
|
>
|
||||||
<Header />
|
<Header />
|
||||||
<WelcomeSidebarModal />
|
<WelcomeSidebarModal />
|
||||||
<Stack
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"display": "flex",
|
|
||||||
"flexDirection": "column",
|
|
||||||
"flexGrow": 1,
|
|
||||||
"overflowY": "auto",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<WelcomeBubble />
|
|
||||||
<Stack
|
|
||||||
horizontalAlign="center"
|
|
||||||
key="0"
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"backgroundColor": "#E0E7FF",
|
|
||||||
"borderRadius": "8px",
|
|
||||||
"margin": "5px 10px",
|
|
||||||
"textAlign": "start",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tokens={
|
|
||||||
Object {
|
|
||||||
"childrenGap": 8,
|
|
||||||
"padding": 8,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
some test message
|
|
||||||
</Stack>
|
|
||||||
<RetrievingBubble />
|
|
||||||
</Stack>
|
|
||||||
<Footer />
|
|
||||||
</Stack>
|
</Stack>
|
||||||
`;
|
`;
|
||||||
|
@ -426,7 +426,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
|||||||
</div>
|
</div>
|
||||||
{shouldScaleElements && (
|
{shouldScaleElements && (
|
||||||
<div style={{ width: "30%", height: "100%" }}>
|
<div style={{ width: "30%", height: "100%" }}>
|
||||||
<QueryCopilotSidebar />
|
<QueryCopilotSidebar explorer={this.props.collection.container} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +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 { guid } from "Explorer/Tables/Utilities";
|
import { guid } from "Explorer/Tables/Utilities";
|
||||||
import create, { UseStore } from "zustand";
|
import create, { UseStore } from "zustand";
|
||||||
|
|
||||||
@ -30,7 +31,7 @@ export interface QueryCopilotState {
|
|||||||
wasCopilotUsed: boolean;
|
wasCopilotUsed: boolean;
|
||||||
showWelcomeSidebar: boolean;
|
showWelcomeSidebar: boolean;
|
||||||
showCopilotSidebar: boolean;
|
showCopilotSidebar: boolean;
|
||||||
chatMessages: string[];
|
chatMessages: CopilotMessage[];
|
||||||
shouldAllocateContainer: boolean;
|
shouldAllocateContainer: boolean;
|
||||||
shouldIncludeInMessages: boolean;
|
shouldIncludeInMessages: boolean;
|
||||||
|
|
||||||
@ -61,7 +62,7 @@ export interface QueryCopilotState {
|
|||||||
setWasCopilotUsed: (wasCopilotUsed: boolean) => void;
|
setWasCopilotUsed: (wasCopilotUsed: boolean) => void;
|
||||||
setShowWelcomeSidebar: (showWelcomeSidebar: boolean) => void;
|
setShowWelcomeSidebar: (showWelcomeSidebar: boolean) => void;
|
||||||
setShowCopilotSidebar: (showCopilotSidebar: boolean) => void;
|
setShowCopilotSidebar: (showCopilotSidebar: boolean) => void;
|
||||||
setChatMessages: (chatMessages: string[]) => void;
|
setChatMessages: (chatMessages: CopilotMessage[]) => void;
|
||||||
|
|
||||||
setShouldAllocateContainer: (shouldAllocateContainer: boolean) => void;
|
setShouldAllocateContainer: (shouldAllocateContainer: boolean) => void;
|
||||||
setShouldIncludeInMessages: (shouldIncludeInMessages: boolean) => void;
|
setShouldIncludeInMessages: (shouldIncludeInMessages: boolean) => void;
|
||||||
@ -131,7 +132,7 @@ export const useQueryCopilot: QueryCopilotStore = create((set) => ({
|
|||||||
setWasCopilotUsed: (wasCopilotUsed: boolean) => set({ wasCopilotUsed }),
|
setWasCopilotUsed: (wasCopilotUsed: boolean) => set({ wasCopilotUsed }),
|
||||||
setShowWelcomeSidebar: (showWelcomeSidebar: boolean) => set({ showWelcomeSidebar }),
|
setShowWelcomeSidebar: (showWelcomeSidebar: boolean) => set({ showWelcomeSidebar }),
|
||||||
setShowCopilotSidebar: (showCopilotSidebar: boolean) => set({ showCopilotSidebar }),
|
setShowCopilotSidebar: (showCopilotSidebar: boolean) => set({ showCopilotSidebar }),
|
||||||
setChatMessages: (chatMessages: string[]) => set({ chatMessages }),
|
setChatMessages: (chatMessages: CopilotMessage[]) => set({ chatMessages }),
|
||||||
setShouldAllocateContainer: (shouldAllocateContainer: boolean) => set({ shouldAllocateContainer }),
|
setShouldAllocateContainer: (shouldAllocateContainer: boolean) => set({ shouldAllocateContainer }),
|
||||||
setShouldIncludeInMessages: (shouldIncludeInMessages: boolean) => set({ shouldIncludeInMessages }),
|
setShouldIncludeInMessages: (shouldIncludeInMessages: boolean) => set({ shouldIncludeInMessages }),
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user