Skip to content

Commit

Permalink
refactor: adjustments for failing tests, modify config
Browse files Browse the repository at this point in the history
  • Loading branch information
emcelroy committed Dec 20, 2024
1 parent e05ff31 commit c96c115
Show file tree
Hide file tree
Showing 18 changed files with 187 additions and 102 deletions.
1 change: 1 addition & 0 deletions src/app-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"accessibility": {
"keyboardShortcut": "ctrl+?"
},
"assistantId": "asst_xmAX5oxByssXrkBymMbcsVEm",
"dimensions": {
"height": 680,
"width": 380
Expand Down
7 changes: 5 additions & 2 deletions src/components/App.test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import "openai/shims/node";
import React from "react";
import { render, screen } from "@testing-library/react";
import { App } from "./App";
Expand All @@ -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(),
Expand All @@ -29,6 +30,8 @@ describe("test load app", () => {
<App />
</MockAppConfigProvider>
);
expect(screen.getByText("Loading...")).toBeDefined();
expect(screen.getByText("DAVAI")).toBeDefined();
expect(screen.getByTestId("chat-transcript")).toBeDefined();
expect(screen.getByTestId("chat-input")).toBeDefined();
});
});
35 changes: 19 additions & 16 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@ import { observer } from "mobx-react-lite";
import { initializePlugin, selectSelf } from "@concord-consortium/codap-plugin-api";
import { useAppConfigContext } from "../hooks/use-app-config-context";
import { useAssistantStore } from "../hooks/use-assistant-store";
import { useOpenAIContext } from "../hooks/use-open-ai-context";
import { ChatInputComponent } from "./chat-input";
import { ChatTranscriptComponent } from "./chat-transcript";
import { ReadAloudMenu } from "./readaloud-menu";
import { KeyboardShortcutControls } from "./keyboard-shortcut-controls";
import { DAVAI_SPEAKER, defaultAssistantId, GREETING, USER_SPEAKER } from "../constants";
import { DAVAI_SPEAKER, GREETING, USER_SPEAKER } from "../constants";
import { DeveloperOptionsComponent } from "./developer-options";
import { getUrlParam } from "../utils/utils";

Expand All @@ -35,12 +34,13 @@ export const App = observer(() => {
useEffect(() => {
initializePlugin({pluginName: kPluginName, version: kVersion, dimensions});
selectSelf();
if (!isDevMode) {
assistantStore.initializeAssistant(defaultAssistantId);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

useEffect(() => {
assistantStore.initializeAssistant();
}, [assistantStore, appConfig.assistantId]);

const handleFocusShortcut = () => {
selectSelf();
};
Expand Down Expand Up @@ -92,19 +92,22 @@ 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;
}

appConfig.setMockAssistant(false);
appConfig.setAssistantId(id);
};

return (
Expand Down Expand Up @@ -141,7 +144,7 @@ export const App = observer(() => {
</div>
}
<ChatInputComponent
disabled={!assistantStore.assistant || !appConfig.isAssistantMocked}
disabled={!assistantStore.thread && !appConfig.isAssistantMocked}
keyboardShortcutEnabled={keyboardShortcutEnabled}
shortcutKeys={keyboardShortcutKeys}
onSubmit={handleChatInputSubmit}
Expand Down Expand Up @@ -169,7 +172,7 @@ export const App = observer(() => {
assistantStore={assistantStore}
onCreateThread={handleCreateThread}
onDeleteThread={handleDeleteThread}
onMockAssistant={handleMockAssistant}
onSelectAssistant={handleSelectAssistant}
/>
</>
}
Expand Down
7 changes: 6 additions & 1 deletion src/components/developer-options.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
48 changes: 28 additions & 20 deletions src/components/developer-options.test.tsx
Original file line number Diff line number Diff line change
@@ -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: [
Expand All @@ -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", () => ({
Expand All @@ -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 (
<MockAppConfigProvider>
<DeveloperOptionsComponent
assistantStore={mockAssistantStore}
onCreateThread={onCreateThread}
onDeleteThread={onDeleteThread}
onMockAssistant={onMockAssistant}
/>
<MockOpenAiConnectionProvider>
<DeveloperOptionsComponent
assistantStore={mockAssistantStore}
onCreateThread={onCreateThread}
onDeleteThread={onDeleteThread}
onSelectAssistant={onSelectAssistant}
/>
</MockOpenAiConnectionProvider>
</MockAppConfigProvider>
);
};

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(<WrapperComponent />);

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();
Expand Down
75 changes: 38 additions & 37 deletions src/components/developer-options.tsx
Original file line number Diff line number Diff line change
@@ -1,69 +1,70 @@
import React, { SyntheticEvent, useEffect, useState } 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";
import { useOpenAIContext } from "../hooks/use-open-ai-context";

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 [assistantOptions, setAssistantOptioms] = useState<string[]>();
const selectedAssistant = assistantStore.assistantId ? assistantStore.assistantId : "mock";
const [assistantOptions, setAssistantOptions] = useState<Map<string, string>>();

useEffect(() => {
const fetchAssistants = async () => {
try {
const res = await apiConnection.beta.assistants.list();
const assistantIds = res.data.map(asst => asst.id);
setAssistantOptioms(assistantIds);
} catch (err) {
console.error(err);
}
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 handleSetSelectedAssistant = (e: React.ChangeEvent<HTMLSelectElement>) => {
const id = e.target.value;
assistantStore.initializeAssistant(id);
onSelectAssistant(id);
};

return (
<div className="developer-options" data-testid="developer-options">
<label htmlFor="mock-assistant-checkbox" data-testid="mock-assistant-checkbox-label">
<select
value={assistantStore.assistantId ? assistantStore.assistantId : "default"}
onChange={handleSetSelectedAssistant}
>
<option value="default" disabled>
-- Select an assistant --
</option>
{assistantOptions?.map((id) => {
return (
<option aria-selected={assistantStore.assistantId === id} key={id}>
{id}
</option>
);
})}
</select>
<input
checked={appConfig.isAssistantMocked}
data-testid="mock-assistant-checkbox"
id="mock-assistant-checkbox"
type="checkbox"
onChange={onMockAssistant}
/>
Use Mock Assistant
<label htmlFor="assistant-select" data-testid="assistant-select-label">
Select an Assistant
</label>
<select
id="assistant-select"
data-testid="assistant-select"
value={selectedAssistant}
onChange={handleSetSelectedAssistant}
>
<option value="mock">Mock Assistant</option>
{Array.from(assistantOptions?.entries() || []).map(([assistantId, assistantName]) => (
<option
aria-selected={assistantStore.assistantId === assistantId}
key={assistantId}
value={assistantId}
>
{assistantName}
</option>
))}
</select>
<button
data-testid="delete-thread-button"
disabled={!assistantStore.assistant || !assistantStore.thread}
Expand Down
1 change: 0 additions & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,3 @@ export const USER_SPEAKER = "User";

export const GREETING = `Hello! I'm DAVAI, your Data Analysis through Voice and Artificial Intelligence partner.`;

export const defaultAssistantId = "asst_xmAX5oxByssXrkBymMbcsVEm";
4 changes: 3 additions & 1 deletion src/contexts/app-config-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import { AppConfigContext } from "./app-config-context";
export const loadAppConfig = (): AppConfig => {
const defaultConfig = appConfigJson as AppConfig;
const urlParamMode = getUrlParam("mode");
const assistantId = getUrlParam("assistantId");
const configOverrides: Partial<AppConfig> = {
mode: isAppMode(urlParamMode) ? urlParamMode : defaultConfig.mode
mode: isAppMode(urlParamMode) ? urlParamMode : defaultConfig.mode,
assistantId: assistantId || defaultConfig.assistantId,
};

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@ export const OpenAIConnectionProvider = ({ children }: {children: React.ReactNod
<OpenAIConnectionContext.Provider value={apiConnection}>
{children}
</OpenAIConnectionContext.Provider>
)
}
);
};
29 changes: 24 additions & 5 deletions src/hooks/use-assistant-store.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,33 @@
import { useMemo } from "react";
import { AssistantModel } from "../models/assistant-model";
import { useChatTranscriptStore } from "./use-chat-transcript-store";
import { useOpenAIContext } from "./use-open-ai-context";
import { useOpenAIContext } from "./use-openai-context";
import { useAppConfigContext } from "./use-app-config-context";
import { ChatTranscriptModel } from "../models/chat-transcript-model";
import { DAVAI_SPEAKER, GREETING } from "../constants";
import { timeStamp } from "../utils/utils";

export const useAssistantStore = () => {
const apiConnection = useOpenAIContext();
const transcriptStore = useChatTranscriptStore();
const appConfig = useAppConfigContext();
const assistantId = appConfig.assistantId;
const assistantStore = useMemo(() => {
return AssistantModel.create({transcriptStore, apiConnection});
}, [transcriptStore, apiConnection]);
const newTranscriptStore = ChatTranscriptModel.create({
messages: [
{
speaker: DAVAI_SPEAKER,
messageContent: { content: GREETING },
timestamp: timeStamp(),
id: "initial-message",
},
],
});

return AssistantModel.create({
apiConnection,
assistantId,
transcriptStore: newTranscriptStore
});
}, [apiConnection, assistantId]);

return assistantStore;
};
Loading

0 comments on commit c96c115

Please sign in to comment.