diff --git a/README.md b/README.md index 6819bcd..f33b1e6 100755 --- a/README.md +++ b/README.md @@ -69,14 +69,10 @@ Default configuration setting values are defined in the `app-config.json` file. Settings related to accessibility in the UI: - **`keyboardShortcut`** (string): Custom keystroke for placing focus in the main text input field (e.g., `ctrl+?`). -### Assistant - -- **`assistant`** (Object) - Settings to configure the AI assistant: - - **`assistantId`** (string): The unique ID of an existing assistant to use. - - **`instructions`** (string): Instructions to use when creating new assistants (e.g., `You are helpful data analysis partner.`). - - **`modelName`** (string): The name of the model the assistant should use (e.g., `gpt-4o-mini`). - - **`useExisting`** (boolean): Whether to use an existing assistant. +### AssistantId + +- **`assistantId`** (string) + The unique ID of an existing assistant to use, or "mock" for a mocked assistant. ### Dimensions diff --git a/cypress/e2e/workspace.test.ts b/cypress/e2e/workspace.test.ts index 2124c68..105ec1e 100644 --- a/cypress/e2e/workspace.test.ts +++ b/cypress/e2e/workspace.test.ts @@ -1,6 +1,8 @@ context("Test the overall app", () => { it("renders without crashing", () => { cy.visit("/"); - cy.get("body").should("contain", "Loading..."); + cy.get("body").should("contain", "DAVAI"); + cy.get("[data-testid=chat-transcript]").should("exist"); + cy.get("[data-testid=chat-input]").should("exist"); }); }); diff --git a/src/app-config.json b/src/app-config.json index 2c22176..231c9ac 100644 --- a/src/app-config.json +++ b/src/app-config.json @@ -2,12 +2,7 @@ "accessibility": { "keyboardShortcut": "ctrl+?" }, - "assistant": { - "assistantId": "asst_xmAX5oxByssXrkBymMbcsVEm", - "instructions": "You are DAVAI, a Data Analysis through Voice and Artificial Intelligence partner. You are an intermediary for a user who is blind who wants to interact with data tables in a data analysis app named CODAP.", - "modelName": "gpt-4o-mini", - "useExisting": true - }, + "assistantId": "asst_xmAX5oxByssXrkBymMbcsVEm", "dimensions": { "height": 680, "width": 380 diff --git a/src/components/App.test.tsx b/src/components/App.test.tsx index aa769f7..406d534 100755 --- a/src/components/App.test.tsx +++ b/src/components/App.test.tsx @@ -1,3 +1,4 @@ +import "openai/shims/node"; import React from "react"; import { render, screen } from "@testing-library/react"; import { App } from "./App"; @@ -6,7 +7,7 @@ import { MockAppConfigProvider } from "../test-utils/app-config-provider"; jest.mock("../hooks/use-assistant-store", () => ({ useAssistantStore: jest.fn(() => ({ - initialize: jest.fn(), + initializeAssistant: jest.fn(), transcriptStore: { messages: [], addMessage: jest.fn(), @@ -29,6 +30,8 @@ describe("test load app", () => { ); - expect(screen.getByText("Loading...")).toBeDefined(); + expect(screen.getByText("DAVAI")).toBeDefined(); + expect(screen.getByTestId("chat-transcript")).toBeDefined(); + expect(screen.getByTestId("chat-input")).toBeDefined(); }); }); diff --git a/src/components/App.tsx b/src/components/App.tsx index e35f2d6..3e1af09 100755 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -34,10 +34,13 @@ export const App = observer(() => { useEffect(() => { initializePlugin({pluginName: kPluginName, version: kVersion, dimensions}); selectSelf(); - assistantStore.initialize(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + useEffect(() => { + assistantStore.initializeAssistant(); + }, [assistantStore, appConfig.assistantId]); + const handleFocusShortcut = () => { selectSelf(); }; @@ -89,24 +92,23 @@ export const App = observer(() => { return true; }; - const handleMockAssistant = async () => { - if (!appConfig.isAssistantMocked) { - // If we switch to a mocked assistant, we delete the current thread and clear the transcript. - // First make sure the user is OK with that. - const threadDeleted = await handleDeleteThread(); - if (!threadDeleted) return; + const handleSelectAssistant = async (id: string) => { + // If we switch assistants, we delete the current thread and clear the transcript. + // First make sure the user is OK with that. + const threadDeleted = await handleDeleteThread(); + if (!threadDeleted) return; + if (id === "mock") { transcriptStore.clearTranscript(); transcriptStore.addMessage(DAVAI_SPEAKER, {content: GREETING}); - appConfig.toggleMockAssistant(); - } else { - appConfig.toggleMockAssistant(); + appConfig.setMockAssistant(true); + appConfig.setAssistantId(id); + return; } - }; - if (!assistantStore.assistant) { - return
Loading...
; - } + appConfig.setMockAssistant(false); + appConfig.setAssistantId(id); + }; return (
@@ -170,7 +172,7 @@ export const App = observer(() => { assistantStore={assistantStore} onCreateThread={handleCreateThread} onDeleteThread={handleDeleteThread} - onMockAssistant={handleMockAssistant} + onSelectAssistant={handleSelectAssistant} /> } diff --git a/src/components/developer-options.scss b/src/components/developer-options.scss index e841343..0b6932a 100644 --- a/src/components/developer-options.scss +++ b/src/components/developer-options.scss @@ -30,9 +30,14 @@ font-size: .75rem; font-weight: normal; line-height: 1.4; - margin: 0 0 0 5px; + margin: 0 10px; padding: 0; user-select: none; white-space: nowrap; } + + select { + margin: 0 10px 10px; + padding: 7px 10px; + } } \ No newline at end of file diff --git a/src/components/developer-options.test.tsx b/src/components/developer-options.test.tsx index 9c1d053..f45d55a 100644 --- a/src/components/developer-options.test.tsx +++ b/src/components/developer-options.test.tsx @@ -1,12 +1,13 @@ import "openai/shims/node"; import React from "react"; -import { fireEvent, render, screen } from "@testing-library/react"; +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { DeveloperOptionsComponent } from "./developer-options"; import { AssistantModel } from "../models/assistant-model"; import { ChatTranscriptModel } from "../models/chat-transcript-model"; import { MockAppConfigProvider } from "../test-utils/app-config-provider"; import { mockAppConfig } from "../test-utils/mock-app-config"; +import { MockOpenAiConnectionProvider } from "../test-utils/openai-connection-provider"; const mockTranscriptStore = ChatTranscriptModel.create({ messages: [ @@ -20,13 +21,14 @@ const mockTranscriptStore = ChatTranscriptModel.create({ }); const mockAssistantStore = AssistantModel.create({ + apiConnection: { + apiKey: "abc123", + dangerouslyAllowBrowser: true + }, assistant: {}, assistantId: "asst_abc123", - instructions: "This is just a test", - modelName: "test-model", thread: {}, transcriptStore: mockTranscriptStore, - useExistingAssistant: true, }); jest.mock("../models/app-config-model", () => ({ @@ -39,35 +41,41 @@ jest.mock("../models/app-config-model", () => ({ describe("test developer options component", () => { const onCreateThread = jest.fn(); const onDeleteThread = jest.fn(); - const onMockAssistant = jest.fn(); + const onSelectAssistant = jest.fn(); const WrapperComponent = () => { return ( - + + + ); }; - it("renders a developer options component with mock assistant checkbox and thread buttons", () => { + it("renders a developer options component with mock assistant checkbox and thread buttons", async () => { render(); const developerOptions = screen.getByTestId("developer-options"); expect(developerOptions).toBeInTheDocument(); - const mockAssistantCheckbox = screen.getByTestId("mock-assistant-checkbox"); - expect(mockAssistantCheckbox).toBeInTheDocument(); - expect(mockAssistantCheckbox).toHaveAttribute("type", "checkbox"); - expect(mockAssistantCheckbox).toHaveProperty("checked", false); - const mockAssistantCheckboxLabel = screen.getByTestId("mock-assistant-checkbox-label"); - expect(mockAssistantCheckboxLabel).toHaveTextContent("Use Mock Assistant"); - fireEvent.click(mockAssistantCheckbox); - expect(onMockAssistant).toHaveBeenCalledTimes(1); + const selectAssistantOptionLabel = screen.getByTestId("assistant-select-label"); + expect(selectAssistantOptionLabel).toHaveTextContent("Select an Assistant"); + const selectAssistantOption = screen.getByTestId("assistant-select"); + expect(selectAssistantOption).toBeInTheDocument(); + await waitFor(() => { + expect(selectAssistantOption).toHaveValue("asst_abc123"); + }); + await waitFor(() => { + expect(selectAssistantOption).toHaveTextContent("Jest Mock Assistant"); + }); + fireEvent.change(selectAssistantOption, { target: { value: "mock" } }); + expect(onSelectAssistant).toHaveBeenCalledTimes(1); const deleteThreadButton = screen.getByTestId("delete-thread-button"); expect(deleteThreadButton).toBeInTheDocument(); diff --git a/src/components/developer-options.tsx b/src/components/developer-options.tsx index 67c4fe6..d7fac1f 100644 --- a/src/components/developer-options.tsx +++ b/src/components/developer-options.tsx @@ -1,7 +1,9 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; +import { OpenAI } from "openai"; import { observer } from "mobx-react-lite"; import { AssistantModelType } from "../models/assistant-model"; import { useAppConfigContext } from "../hooks/use-app-config-context"; +import { useOpenAIContext } from "../hooks/use-openai-context"; import "./developer-options.scss"; @@ -9,33 +11,70 @@ interface IProps { assistantStore: AssistantModelType; onCreateThread: () => void; onDeleteThread: () => void; - onMockAssistant: () => void; + onSelectAssistant: (id: string) => void; } -export const DeveloperOptionsComponent = observer(function DeveloperOptions({assistantStore, onCreateThread, onDeleteThread, onMockAssistant}: IProps) { +export const DeveloperOptionsComponent = observer(function DeveloperOptions({assistantStore, onCreateThread, onDeleteThread, onSelectAssistant}: IProps) { const appConfig = useAppConfigContext(); + const apiConnection = useOpenAIContext(); + const selectedAssistant = assistantStore.assistantId ? assistantStore.assistantId : "mock"; + const [assistantOptions, setAssistantOptions] = useState>(); + + useEffect(() => { + const fetchAssistants = async () => { + try { + const res = await apiConnection.beta.assistants.list(); + const assistants = new Map(); + res.data.map((assistant: OpenAI.Beta.Assistant) => { + const assistantName = assistant.name || assistant.id; + assistants.set(assistant.id, assistantName); + }); + setAssistantOptions(assistants); + } catch (err) { + console.error(err); + } + }; + + fetchAssistants(); + }, [apiConnection.beta.assistants]); + + const handleSelectAssistant = (e: React.ChangeEvent) => { + const id = e.target.value; + onSelectAssistant(id); + }; + return (
-