From b102121ee1de420cff75a4b5c30ebdcac7a326a0 Mon Sep 17 00:00:00 2001 From: Ethan McElroy Date: Wed, 4 Dec 2024 17:04:46 -0500 Subject: [PATCH 1/8] chore: remove unnecessary aria labels --- src/components/chat-transcript.test.tsx | 2 -- src/components/chat-transcript.tsx | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/chat-transcript.test.tsx b/src/components/chat-transcript.test.tsx index a3ee6a3..23dce7e 100644 --- a/src/components/chat-transcript.test.tsx +++ b/src/components/chat-transcript.test.tsx @@ -33,11 +33,9 @@ describe("test chat transcript component", () => { expect(message).toHaveAttribute("aria-label", labelContent); const speaker = within(message).getByTestId("chat-message-speaker"); - expect(speaker).toHaveAttribute("aria-label", "speaker"); expect(speaker).toHaveTextContent(chatTranscript.messages[index].speaker); const content = within(message).getByTestId("chat-message-content"); - expect(content).toHaveAttribute("aria-label", "message"); expect(content).toHaveTextContent(chatTranscript.messages[index].content); }); }); diff --git a/src/components/chat-transcript.tsx b/src/components/chat-transcript.tsx index bccd300..1b1d919 100644 --- a/src/components/chat-transcript.tsx +++ b/src/components/chat-transcript.tsx @@ -37,10 +37,10 @@ export const ChatTranscriptComponent = ({chatTranscript}: IProps) => { key={message.timestamp} role="listitem" > -

+

{message.speaker}

-

+

{message.content}

From d6feb50d97bf7f052d6177e65308d35ad23fac04 Mon Sep 17 00:00:00 2001 From: Ethan McElroy Date: Mon, 9 Dec 2024 18:48:29 -0500 Subject: [PATCH 2/8] feat: add configuration, developer options --- cypress/e2e/workspace.test.ts | 7 +- src/app-config-context.ts | 12 ++++ src/app-config-provider.tsx | 25 +++++++ src/app-config.json | 30 ++++---- src/components/App.test.tsx | 36 ++++++++-- src/components/App.tsx | 85 ++++++++++++++++++----- src/components/chat-input.scss | 9 +++ src/components/chat-input.tsx | 5 +- src/components/developer-options.scss | 38 ++++++++++ src/components/developer-options.test.tsx | 83 ++++++++++++++++++++++ src/components/developer-options.tsx | 45 ++++++++++++ src/constants.ts | 2 + src/hooks/use-app-config-context.ts | 11 +++ src/hooks/use-assistant-store.ts | 21 ++++++ src/hooks/use-chat-transcript-store.ts | 20 ++++++ src/index.tsx | 7 +- src/models/app-config-model.ts | 32 +++++++++ src/models/assistant-model.ts | 69 ++++++++++++++---- src/models/chat-transcript-model.ts | 5 -- src/test-utils/app-config-provider.tsx | 13 ++++ src/test-utils/mock-app-config.ts | 19 +++++ src/types.ts | 23 ++++++ src/utils/utils.ts | 5 ++ 23 files changed, 539 insertions(+), 63 deletions(-) create mode 100644 src/app-config-context.ts create mode 100644 src/app-config-provider.tsx create mode 100644 src/components/developer-options.scss create mode 100644 src/components/developer-options.test.tsx create mode 100644 src/components/developer-options.tsx create mode 100644 src/hooks/use-app-config-context.ts create mode 100644 src/hooks/use-assistant-store.ts create mode 100644 src/hooks/use-chat-transcript-store.ts create mode 100644 src/models/app-config-model.ts create mode 100644 src/test-utils/app-config-provider.tsx create mode 100644 src/test-utils/mock-app-config.ts diff --git a/cypress/e2e/workspace.test.ts b/cypress/e2e/workspace.test.ts index 7e3eb11..2124c68 100644 --- a/cypress/e2e/workspace.test.ts +++ b/cypress/e2e/workspace.test.ts @@ -1,7 +1,6 @@ -import { AppElements as ae } from "../support/elements/app-elements"; - context("Test the overall app", () => { - beforeEach(() => { - cy.visit(""); + it("renders without crashing", () => { + cy.visit("/"); + cy.get("body").should("contain", "Loading..."); }); }); diff --git a/src/app-config-context.ts b/src/app-config-context.ts new file mode 100644 index 0000000..2a647e1 --- /dev/null +++ b/src/app-config-context.ts @@ -0,0 +1,12 @@ +import { createContext, useContext } from "react"; +import { AppConfigModelType } from "./models/app-config-model"; + +export const AppConfigContext = createContext(undefined); + +export const useAppConfigContext = (): AppConfigModelType => { + const context = useContext(AppConfigContext); + if (!context) { + throw new Error("useAppConfig must be used within a AppConfigContext.Provider"); + } + return context; +}; diff --git a/src/app-config-provider.tsx b/src/app-config-provider.tsx new file mode 100644 index 0000000..ec37276 --- /dev/null +++ b/src/app-config-provider.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { AppConfig, isMode } from "./types"; +import appConfigJson from "./app-config.json"; +import { AppConfigModel, AppConfigModelSnapshot } from "./models/app-config-model"; +import { getUrlParam } from "./utils/utils"; +import { AppConfigContext } from "./app-config-context"; + +export const loadAppConfig = (): AppConfig => { + const defaultConfig = appConfigJson as AppConfig; + const urlParamMode = getUrlParam("mode"); + const configOverrides: Partial = { + mode: isMode(urlParamMode) ? urlParamMode : defaultConfig.mode + }; + + return { + ...defaultConfig, + ...configOverrides, + }; +}; + +export const AppConfigProvider = ({ children }: { children: React.ReactNode }) => { + const appConfigSnapshot = loadAppConfig() as AppConfigModelSnapshot; + const appConfig = AppConfigModel.create(appConfigSnapshot); + return {children}; +}; diff --git a/src/app-config.json b/src/app-config.json index 2072454..c0a1600 100644 --- a/src/app-config.json +++ b/src/app-config.json @@ -1,17 +1,17 @@ { - "config": { - "accessibility": { - "keyboard_shortcut": "ctrl+?" - }, - "assistant": { - "existing_assistant_id": "asst_Af8jrKYOFP4MxA9nse61yFBq", - "instructions": "You are DAVAI, an 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.", - "model": "gpt-4o-mini", - "use_existing": true - }, - "dimensions": { - "height": 680, - "width": 380 - } - } + "accessibility": { + "keyboardShortcut": "ctrl+?" + }, + "assistant": { + "assistantId": "asst_Af8jrKYOFP4MxA9nse61yFBq", + "instructions": "You are DAVAI, an 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.", + "model": "gpt-4o-mini", + "useExisting": true + }, + "dimensions": { + "height": 680, + "width": 380 + }, + "mockAssistant": false, + "mode": "production" } diff --git a/src/components/App.test.tsx b/src/components/App.test.tsx index 254f204..66aacac 100755 --- a/src/components/App.test.tsx +++ b/src/components/App.test.tsx @@ -1,17 +1,43 @@ import React from "react"; import { render, screen } from "@testing-library/react"; import { App } from "./App"; +import { AppConfigContext } from "../app-config-context"; +import { AppConfigModel } from "../models/app-config-model"; +import { mockAppConfig } from "../test-utils/mock-app-config"; -jest.mock("../models/assistant-model", () => ({ - assistantStore: { - assistant: null, +jest.mock("../hooks/use-assistant-store", () => ({ + useAssistantStore: jest.fn(() => ({ initialize: jest.fn(), - }, + transcriptStore: { + messages: [], + addMessage: jest.fn(), + }, + })), })); +jest.mock("../models/app-config-model", () => ({ + AppConfigModel: { + create: jest.fn(() => (mockAppConfig)), + initialize: jest.fn(), + } +})); + +const MockAppConfigProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const mockAppConfigValue = AppConfigModel.create(mockAppConfig); + return ( + + {children} + + ); +}; + describe("test load app", () => { it("renders without crashing", () => { - render(); + render( + + + + ); expect(screen.getByText("Loading...")).toBeDefined(); }); }); diff --git a/src/components/App.tsx b/src/components/App.tsx index 745af63..97961b6 100755 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1,38 +1,37 @@ import React, { useEffect, useState } from "react"; import { observer } from "mobx-react-lite"; import { initializePlugin, selectSelf } from "@concord-consortium/codap-plugin-api"; -import appConfigJson from "../app-config.json"; -import { assistantStore } from "../models/assistant-model"; -import { transcriptStore } from "../models/chat-transcript-model"; +import { useAppConfigContext } from "../hooks/use-app-config-context"; +import { useAssistantStore } from "../hooks/use-assistant-store"; import { ChatInputComponent } from "./chat-input"; import { ChatTranscriptComponent } from "./chat-transcript"; import { ReadAloudMenu } from "./readaloud-menu"; import { KeyboardShortcutControls } from "./keyboard-shortcut-controls"; -import { USER_SPEAKER } from "../constants"; +import { DAVAI_SPEAKER, GREETING, USER_SPEAKER } from "../constants"; +import { DeveloperOptionsComponent } from "./developer-options"; import "./App.scss"; -const appConfig = appConfigJson.config; - const kPluginName = "DAVAI"; const kVersion = "0.0.1"; -const kInitialDimensions = { - width: appConfig.dimensions.width, - height: appConfig.dimensions.height, -}; export const App = observer(() => { + const appConfig = useAppConfigContext(); + const assistantStore = useAssistantStore(); + const transcriptStore = assistantStore.transcriptStore; + const dimensions = { width: appConfig.dimensions.width, height: appConfig.dimensions.height }; const [readAloudEnabled, setReadAloudEnabled] = useState(false); const [playbackSpeed, setPlaybackSpeed] = useState(1); const isShortcutEnabled = JSON.parse(localStorage.getItem("keyboardShortcutEnabled") || "true"); const [keyboardShortcutEnabled, setKeyboardShortcutEnabled] = useState(isShortcutEnabled); - const shortcutKeys = localStorage.getItem("keyboardShortcutKeys") || appConfig.accessibility.keyboard_shortcut; + const shortcutKeys = localStorage.getItem("keyboardShortcutKeys") || appConfig.accessibility.keyboardShortcut; const [keyboardShortcutKeys, setKeyboardShortcutKeys] = useState(shortcutKeys); useEffect(() => { - initializePlugin({pluginName: kPluginName, version: kVersion, dimensions: kInitialDimensions}); + initializePlugin({pluginName: kPluginName, version: kVersion, dimensions}); selectSelf(); assistantStore.initialize(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const handleFocusShortcut = () => { @@ -57,15 +56,54 @@ export const App = observer(() => { setPlaybackSpeed(speed); }; - if (!assistantStore.assistant) { - return
Loading...
; - } - const handleChatInputSubmit = async (messageText: string) => { transcriptStore.addMessage(USER_SPEAKER, messageText); - assistantStore.handleMessageSubmit(messageText); + + if (appConfig.isAssistantMocked) { + assistantStore.handleMessageSubmitMockAssistant(); + } else { + assistantStore.handleMessageSubmit(messageText); + } + + }; + + const handleCreateThread = async () => { + const confirmCreate = window.confirm("Are you sure you want to create a new thread? If you do, you will not be able to continue this chat and will lose its history."); + if (!confirmCreate) return; + + transcriptStore.clearTranscript(); + transcriptStore.addMessage(DAVAI_SPEAKER, GREETING); + + await assistantStore.createThread(); }; + const handleDeleteThread = async () => { + const confirmDelete = window.confirm("Are you sure you want to delete the current thread? If you do, you will not be able to continue this chat."); + if (!confirmDelete) return false; + + await assistantStore.deleteThread(); + 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; + + transcriptStore.clearTranscript(); + transcriptStore.addMessage(DAVAI_SPEAKER, GREETING); + appConfig.toggleMockAssistant(); + } else { + appConfig.toggleMockAssistant(); + } + }; + + if (!assistantStore.assistant) { + return
Loading...
; + } + return (
@@ -76,6 +114,7 @@ export const App = observer(() => {
{ onCustomizeShortcut={handleCustomizeShortcut} onToggleShortcut={handleToggleShortcut} /> + {appConfig.mode === "development" && + <> +
+

Developer Options

+ + + }
); }); diff --git a/src/components/chat-input.scss b/src/components/chat-input.scss index fee34f4..b331b7e 100644 --- a/src/components/chat-input.scss +++ b/src/components/chat-input.scss @@ -18,6 +18,11 @@ flex: 1 0 calc(100% - 22px); // full width of container minus 10px padding and 1px border on either side height: 75px; padding: 8px 10px; + + &:disabled { + cursor: not-allowed; + opacity: .5; + } } .buttons-container { @@ -57,6 +62,10 @@ &.dictate { order: 1; } + &:disabled { + cursor: not-allowed; + opacity: .5; + } } } } \ No newline at end of file diff --git a/src/components/chat-input.tsx b/src/components/chat-input.tsx index 7016f7d..a205e21 100644 --- a/src/components/chat-input.tsx +++ b/src/components/chat-input.tsx @@ -5,13 +5,14 @@ import { isInputElement, isShortcutPressed } from "../utils/utils"; import "./chat-input.scss"; interface IProps { + disabled?: boolean; keyboardShortcutEnabled: boolean; shortcutKeys: string; onKeyboardShortcut: () => void; onSubmit: (messageText: string) => void; } -export const ChatInputComponent = ({keyboardShortcutEnabled, shortcutKeys, onKeyboardShortcut, onSubmit}: IProps) => { +export const ChatInputComponent = ({disabled, keyboardShortcutEnabled, shortcutKeys, onKeyboardShortcut, onSubmit}: IProps) => { const textAreaRef = useRef(null); // const [browserSupportsDictation, setBrowserSupportsDictation] = useState(false); // const [dictationEnabled, setDictationEnabled] = useState(false); @@ -156,6 +157,7 @@ export const ChatInputComponent = ({keyboardShortcutEnabled, shortcutKeys, onKey aria-describedby={showError ? "input-error" : undefined} aria-invalid={showError} data-testid="chat-input-textarea" + disabled={disabled} id="chat-input" placeholder={"Ask DAVAI about the data"} ref={textAreaRef} @@ -178,6 +180,7 @@ export const ChatInputComponent = ({keyboardShortcutEnabled, shortcutKeys, onKey + + + ); +}); diff --git a/src/constants.ts b/src/constants.ts index 36249d9..2acb1a1 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,2 +1,4 @@ export const DAVAI_SPEAKER = "DAVAI"; export const USER_SPEAKER = "User"; + +export const GREETING = `Hello! I'm DAVAI, your Data Analysis through Voice and Artificial Intelligence partner.`; diff --git a/src/hooks/use-app-config-context.ts b/src/hooks/use-app-config-context.ts new file mode 100644 index 0000000..cac532d --- /dev/null +++ b/src/hooks/use-app-config-context.ts @@ -0,0 +1,11 @@ +import { useContext } from "react"; +import { AppConfigModelType } from "../models/app-config-model"; +import { AppConfigContext } from "../app-config-context"; + +export const useAppConfigContext = (): AppConfigModelType => { + const context = useContext(AppConfigContext); + if (!context) { + throw new Error("useAppConfigContext must be used within a AppConfigContext.Provider"); + } + return context; +}; diff --git a/src/hooks/use-assistant-store.ts b/src/hooks/use-assistant-store.ts new file mode 100644 index 0000000..abe6695 --- /dev/null +++ b/src/hooks/use-assistant-store.ts @@ -0,0 +1,21 @@ +import { useMemo } from "react"; +import { useAppConfigContext } from "./use-app-config-context"; +import { AssistantModel } from "../models/assistant-model"; +import { useChatTranscriptStore } from "./use-chat-transcript-store"; + +export const useAssistantStore = () => { + const appConfig = useAppConfigContext(); + const transcriptStore = useChatTranscriptStore(); + const { assistantId, instructions, model, useExisting } = appConfig.assistant; + const assistantStore = useMemo(() => { + return AssistantModel.create({ + assistantId, + model, + instructions, + transcriptStore, + useExisting, + }); + }, [assistantId, instructions, model, transcriptStore, useExisting]); + + return assistantStore; +}; diff --git a/src/hooks/use-chat-transcript-store.ts b/src/hooks/use-chat-transcript-store.ts new file mode 100644 index 0000000..f83ed88 --- /dev/null +++ b/src/hooks/use-chat-transcript-store.ts @@ -0,0 +1,20 @@ +import { useMemo } from "react"; +import { ChatTranscriptModel } from "../models/chat-transcript-model"; +import { timeStamp } from "../utils/utils"; +import { DAVAI_SPEAKER } from "../constants"; + +export const useChatTranscriptStore = () => { + const chatTranscriptStore = useMemo(() => { + return ChatTranscriptModel.create({ + messages: [ + { + speaker: DAVAI_SPEAKER, + content: "Hello! I'm DAVAI, your Data Analysis through Voice and Artificial Intelligence partner.", + timestamp: timeStamp(), + }, + ], + }); + }, []); + + return chatTranscriptStore; +}; diff --git a/src/index.tsx b/src/index.tsx index 051716a..67b2594 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,6 +1,7 @@ import React from "react"; import { createRoot } from "react-dom/client"; import { App } from "./components/App"; +import { AppConfigProvider } from "./app-config-provider"; import "./index.scss"; @@ -12,5 +13,9 @@ if (container) { window.focus(); }); - root.render(); + root.render( + + + + ); } diff --git a/src/models/app-config-model.ts b/src/models/app-config-model.ts new file mode 100644 index 0000000..09706f2 --- /dev/null +++ b/src/models/app-config-model.ts @@ -0,0 +1,32 @@ +import { types, Instance, SnapshotIn } from "mobx-state-tree"; +import { Mode } from "../types"; + +export const AppConfigModel = types.model("AppConfigModel", { + accessibility: types.model({ + keyboardShortcut: types.string, + }), + assistant: types.model({ + assistantId: types.string, + instructions: types.string, + model: types.string, + useExisting: types.boolean, + }), + dimensions: types.model({ + width: types.number, + height: types.number, + }), + mockAssistant: types.maybe(types.boolean), + mode: types.enumeration("Mode", ["development", "production", "test"]), +}) +.volatile((self) => ({ + isAssistantMocked: self.mode === "development" && self.mockAssistant, +})) +.actions((self) => ({ + toggleMockAssistant() { + self.mockAssistant = !self.mockAssistant; + self.isAssistantMocked = self.mode === "development" && self.mockAssistant; + }, +})); + +export interface AppConfigModelSnapshot extends SnapshotIn {} +export interface AppConfigModelType extends Instance {} diff --git a/src/models/assistant-model.ts b/src/models/assistant-model.ts index b350ca5..b25b986 100644 --- a/src/models/assistant-model.ts +++ b/src/models/assistant-model.ts @@ -1,11 +1,10 @@ -import { types, flow } from "mobx-state-tree"; +import { types, flow, Instance } from "mobx-state-tree"; import { getTools, initLlmConnection } from "../utils/llm-utils"; -import { ChatTranscriptModel, transcriptStore } from "./chat-transcript-model"; +import { ChatTranscriptModel } from "./chat-transcript-model"; import { Message } from "openai/resources/beta/threads/messages"; import { getAttributeList, getDataContext } from "@concord-consortium/codap-plugin-api"; import { DAVAI_SPEAKER } from "../constants"; import { createGraph } from "../utils/codap-utils"; -import appConfigJson from "../app-config.json"; export const AssistantModel = types .model("AssistantModel", { @@ -15,8 +14,19 @@ export const AssistantModel = types model: types.string, thread: types.maybe(types.frozen()), transcriptStore: ChatTranscriptModel, - useExistingAssistant: true + useExisting: true, }) + .actions((self) => ({ + handleMessageSubmitMockAssistant() { + // Use a brief delay to prevent duplicate timestamp-based keys. + setTimeout(() => { + self.transcriptStore.addMessage( + DAVAI_SPEAKER, + "I'm just a mock assistant and can't process that request." + ); + }, 1000); + } + })) .actions((self) => { const davai = initLlmConnection(); @@ -24,11 +34,11 @@ export const AssistantModel = types try { const tools = getTools(); - const davaiAssistant = self.useExistingAssistant && self.assistantId + const davaiAssistant = self.useExisting && self.assistantId ? yield davai.beta.assistants.retrieve(self.assistantId) : yield davai.beta.assistants.create({instructions: self.instructions, model: self.model, tools }); - if (!self.useExistingAssistant) { + if (!self.useExisting) { self.assistantId = davaiAssistant.id; } self.assistant = davaiAssistant; @@ -126,14 +136,43 @@ export const AssistantModel = types } }); - return { initialize, handleMessageSubmit }; + const createThread = flow(function* () { + try { + const newThread = yield davai.beta.threads.create(); + self.thread = newThread; + } catch (err) { + console.error("Error creating thread:", err); + } + }); + + const deleteThread = flow(function* () { + try { + if (!self.thread) { + console.warn("No thread to delete."); + return; + } + + const threadId = self.thread.id; + const response = yield fetch(`${process.env.REACT_APP_OPENAI_BASE_URL}threads/${threadId}`, { + method: "DELETE", + headers: { + Authorization: `Bearer ${process.env.REACT_APP_OPENAI_API_KEY}`, + "OpenAI-Beta": "assistants=v2", + "Content-Type": "application/json", + }, + }); + + if (response.ok) { + self.thread = undefined; + } else { + console.warn("Failed to delete thread, unexpected response:", response.status); + } + } catch (err) { + console.error("Error deleting thread:", err); + } + }); + + return { createThread, deleteThread, initialize, handleMessageSubmit }; }); -const assistant = appConfigJson.config.assistant; -export const assistantStore = AssistantModel.create({ - assistantId: assistant.existing_assistant_id, - model: assistant.model, - instructions: assistant.instructions, - transcriptStore, - useExistingAssistant: assistant.use_existing -}); +export interface AssistantModelType extends Instance {} diff --git a/src/models/chat-transcript-model.ts b/src/models/chat-transcript-model.ts index f335fa9..6e0b101 100644 --- a/src/models/chat-transcript-model.ts +++ b/src/models/chat-transcript-model.ts @@ -1,6 +1,5 @@ import { Instance, types } from "mobx-state-tree"; import { timeStamp } from "../utils/utils"; -import { DAVAI_SPEAKER } from "../constants"; const MessageModel = types.model("MessageModel", { speaker: types.string, @@ -26,7 +25,3 @@ export const ChatTranscriptModel = types })); export interface ChatTranscriptModelType extends Instance {} -export const transcriptStore = ChatTranscriptModel.create({ - messages: [{speaker: DAVAI_SPEAKER, content: "Hello! I'm DAVAI, your Data Analysis through Voice and Artificial Intelligence partner.", timestamp: timeStamp() - }] -}); diff --git a/src/test-utils/app-config-provider.tsx b/src/test-utils/app-config-provider.tsx new file mode 100644 index 0000000..8603949 --- /dev/null +++ b/src/test-utils/app-config-provider.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import { AppConfigContext } from "../app-config-context"; +import { AppConfigModel } from "../models/app-config-model"; +import { mockAppConfig } from "./mock-app-config"; + +export const MockAppConfigProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const mockAppConfigValue = AppConfigModel.create(mockAppConfig); + return ( + + {children} + + ); +}; diff --git a/src/test-utils/mock-app-config.ts b/src/test-utils/mock-app-config.ts new file mode 100644 index 0000000..4cf0ccb --- /dev/null +++ b/src/test-utils/mock-app-config.ts @@ -0,0 +1,19 @@ +import { Mode } from "../types"; + +export const mockAppConfig = { + accessibility: { + keyboardShortcut: "ctrl+?" + }, + assistant: { + assistantId: "asst_abc123", + instructions: "You are just a test AI. Don't do anything fancy.", + model: "test-model", + useExisting: true + }, + dimensions: { + height: 680, + width: 380 + }, + mockAssistant: false, + mode: "test" as Mode, +}; diff --git a/src/types.ts b/src/types.ts index 64ace2d..dfa894c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,26 @@ +export type Mode = "development" | "production" | "test"; +export const isMode = (value: unknown): value is Mode => { + return value === "development" || value === "production" || value === "test"; +}; + +export type AppConfig = { + accessibility: { + keyboardShortcut: string; + }; + assistant: { + assistantId: string; + instructions: string; + model: string; + useExisting: boolean; + }; + dimensions: { + height: number; + width: number; + }; + mockAssistant?: boolean; + mode: Mode; +}; + export type ChatMessage = { content: string; speaker: string; diff --git a/src/utils/utils.ts b/src/utils/utils.ts index b725dc8..3bb57aa 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -18,6 +18,11 @@ export const isInputElement = (activeElement: Element | null) => { } }; +export const getUrlParam = (paramName: string) => { + const urlParams = new URLSearchParams(window.location.search); + return urlParams.get(paramName); +}; + export const keyMap: Record = { "ShiftLeft": { shifted: "Shift", unshifted: "Shift" }, "ShiftRight": { shifted: "Shift", unshifted: "Shift" }, From dcc156c629e7a27fcc6990371eef440d1c41b866 Mon Sep 17 00:00:00 2001 From: Ethan McElroy Date: Mon, 9 Dec 2024 22:45:04 -0500 Subject: [PATCH 3/8] chore: minor refactoring, clean up --- src/app-config-provider.tsx | 4 ++-- src/components/App.tsx | 2 +- src/hooks/use-chat-transcript-store.ts | 4 ++-- src/models/app-config-model.ts | 4 ++-- src/test-utils/mock-app-config.ts | 4 ++-- src/types.ts | 7 ++++--- 6 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/app-config-provider.tsx b/src/app-config-provider.tsx index ec37276..b6b10d2 100644 --- a/src/app-config-provider.tsx +++ b/src/app-config-provider.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { AppConfig, isMode } from "./types"; +import { AppConfig, isAppMode } from "./types"; import appConfigJson from "./app-config.json"; import { AppConfigModel, AppConfigModelSnapshot } from "./models/app-config-model"; import { getUrlParam } from "./utils/utils"; @@ -9,7 +9,7 @@ export const loadAppConfig = (): AppConfig => { const defaultConfig = appConfigJson as AppConfig; const urlParamMode = getUrlParam("mode"); const configOverrides: Partial = { - mode: isMode(urlParamMode) ? urlParamMode : defaultConfig.mode + mode: isAppMode(urlParamMode) ? urlParamMode : defaultConfig.mode }; return { diff --git a/src/components/App.tsx b/src/components/App.tsx index 97961b6..6455c67 100755 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -68,7 +68,7 @@ export const App = observer(() => { }; const handleCreateThread = async () => { - const confirmCreate = window.confirm("Are you sure you want to create a new thread? If you do, you will not be able to continue this chat and will lose its history."); + const confirmCreate = window.confirm("Are you sure you want to create a new thread? If you do, you will lose any existing chat history."); if (!confirmCreate) return; transcriptStore.clearTranscript(); diff --git a/src/hooks/use-chat-transcript-store.ts b/src/hooks/use-chat-transcript-store.ts index f83ed88..70fa1e7 100644 --- a/src/hooks/use-chat-transcript-store.ts +++ b/src/hooks/use-chat-transcript-store.ts @@ -1,7 +1,7 @@ import { useMemo } from "react"; import { ChatTranscriptModel } from "../models/chat-transcript-model"; import { timeStamp } from "../utils/utils"; -import { DAVAI_SPEAKER } from "../constants"; +import { DAVAI_SPEAKER, GREETING } from "../constants"; export const useChatTranscriptStore = () => { const chatTranscriptStore = useMemo(() => { @@ -9,7 +9,7 @@ export const useChatTranscriptStore = () => { messages: [ { speaker: DAVAI_SPEAKER, - content: "Hello! I'm DAVAI, your Data Analysis through Voice and Artificial Intelligence partner.", + content: GREETING, timestamp: timeStamp(), }, ], diff --git a/src/models/app-config-model.ts b/src/models/app-config-model.ts index 09706f2..4527078 100644 --- a/src/models/app-config-model.ts +++ b/src/models/app-config-model.ts @@ -1,5 +1,5 @@ import { types, Instance, SnapshotIn } from "mobx-state-tree"; -import { Mode } from "../types"; +import { AppMode, AppModeValues } from "../types"; export const AppConfigModel = types.model("AppConfigModel", { accessibility: types.model({ @@ -16,7 +16,7 @@ export const AppConfigModel = types.model("AppConfigModel", { height: types.number, }), mockAssistant: types.maybe(types.boolean), - mode: types.enumeration("Mode", ["development", "production", "test"]), + mode: types.enumeration("Mode", AppModeValues), }) .volatile((self) => ({ isAssistantMocked: self.mode === "development" && self.mockAssistant, diff --git a/src/test-utils/mock-app-config.ts b/src/test-utils/mock-app-config.ts index 4cf0ccb..dfaef1f 100644 --- a/src/test-utils/mock-app-config.ts +++ b/src/test-utils/mock-app-config.ts @@ -1,4 +1,4 @@ -import { Mode } from "../types"; +import { AppMode } from "../types"; export const mockAppConfig = { accessibility: { @@ -15,5 +15,5 @@ export const mockAppConfig = { width: 380 }, mockAssistant: false, - mode: "test" as Mode, + mode: "test" as AppMode, }; diff --git a/src/types.ts b/src/types.ts index dfa894c..bc3de0a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,6 @@ -export type Mode = "development" | "production" | "test"; -export const isMode = (value: unknown): value is Mode => { +export const AppModeValues = ["development", "production", "test"] as const; +export type AppMode = typeof AppModeValues[number]; +export const isAppMode = (value: unknown): value is AppMode => { return value === "development" || value === "production" || value === "test"; }; @@ -18,7 +19,7 @@ export type AppConfig = { width: number; }; mockAssistant?: boolean; - mode: Mode; + mode: AppMode; }; export type ChatMessage = { From fb72646a3c6eb14f2e68f606d871e28a5d895d42 Mon Sep 17 00:00:00 2001 From: Ethan McElroy Date: Mon, 9 Dec 2024 22:45:28 -0500 Subject: [PATCH 4/8] chore: clean up tests --- src/components/App.test.tsx | 11 +---------- src/components/developer-options.test.tsx | 4 ++-- src/components/keyboard-shortcut-controls.test.tsx | 4 ++-- 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/src/components/App.test.tsx b/src/components/App.test.tsx index 66aacac..aa769f7 100755 --- a/src/components/App.test.tsx +++ b/src/components/App.test.tsx @@ -1,9 +1,8 @@ import React from "react"; import { render, screen } from "@testing-library/react"; import { App } from "./App"; -import { AppConfigContext } from "../app-config-context"; -import { AppConfigModel } from "../models/app-config-model"; import { mockAppConfig } from "../test-utils/mock-app-config"; +import { MockAppConfigProvider } from "../test-utils/app-config-provider"; jest.mock("../hooks/use-assistant-store", () => ({ useAssistantStore: jest.fn(() => ({ @@ -22,14 +21,6 @@ jest.mock("../models/app-config-model", () => ({ } })); -const MockAppConfigProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const mockAppConfigValue = AppConfigModel.create(mockAppConfig); - return ( - - {children} - - ); -}; describe("test load app", () => { it("renders without crashing", () => { diff --git a/src/components/developer-options.test.tsx b/src/components/developer-options.test.tsx index 4f81285..bf73763 100644 --- a/src/components/developer-options.test.tsx +++ b/src/components/developer-options.test.tsx @@ -18,7 +18,7 @@ const mockTranscriptStore = ChatTranscriptModel.create({ ], }); -const assistantStore = AssistantModel.create({ +const mockAssistantStore = AssistantModel.create({ assistant: {}, assistantId: "asst_abc123", instructions: "This is just a test", @@ -44,7 +44,7 @@ describe("test developer options component", () => { return ( { expect(button).toHaveTextContent("Disable Shortcut"); }); - it.skip("renders a form for customizing the keyboard shortcut", async () => { + it("renders a form for customizing the keyboard shortcut", async () => { render(); const form = screen.getByTestId("custom-keyboard-shortcut-form"); const input = within(form).getByTestId("custom-keyboard-shortcut"); @@ -60,7 +60,7 @@ describe("test keyboard shortcut controls component", () => { expect(dismissButton).toHaveTextContent("dismiss"); }); - it.skip("shows an error message if the custom keyboard shortcut input is empty", () => { + it("shows an error message if the custom keyboard shortcut input is empty", () => { render(); const form = screen.getByTestId("custom-keyboard-shortcut-form"); const input = within(form).getByTestId("custom-keyboard-shortcut"); From c57379dce4f9151cb5add032d5623904151269e3 Mon Sep 17 00:00:00 2001 From: Ethan McElroy Date: Mon, 9 Dec 2024 22:53:32 -0500 Subject: [PATCH 5/8] chore: refactor isAppMode --- src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types.ts b/src/types.ts index bc3de0a..560d15e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,7 +1,7 @@ export const AppModeValues = ["development", "production", "test"] as const; export type AppMode = typeof AppModeValues[number]; export const isAppMode = (value: unknown): value is AppMode => { - return value === "development" || value === "production" || value === "test"; + return AppModeValues.includes(value as AppMode); }; export type AppConfig = { From 83107c06bb97300fd859f8ecad2f35173bb9ee71 Mon Sep 17 00:00:00 2001 From: Ethan McElroy Date: Tue, 10 Dec 2024 18:13:20 -0500 Subject: [PATCH 6/8] chore: code review suggestions --- README.md | 45 ++++++++++++++- src/app-config-context.ts | 10 +--- src/app-config.json | 4 +- src/components/developer-options.test.tsx | 2 +- src/hooks/use-assistant-store.ts | 6 +- src/models/app-config-model.ts | 23 +++++++- src/models/assistant-model.ts | 68 ++++++++++++----------- src/models/chat-transcript-model.ts | 7 +++ src/types.ts | 2 +- src/utils/anthropic-utils.ts | 52 ----------------- 10 files changed, 117 insertions(+), 102 deletions(-) delete mode 100644 src/utils/anthropic-utils.ts diff --git a/README.md b/README.md index 43542e4..6819bcd 100755 --- a/README.md +++ b/README.md @@ -10,9 +10,9 @@ ## Testing the plugin in CODAP -Currently there is no trivial way to load a plugin running on a local server with `http` into the online CODAP, which forces `https`. One simple solution is to download the latest `build_[...].zip` file from https://codap.concord.org/releases/zips/, extract it to a folder and run it locally. If CODAP is running on port 8080, and this project is running by default on 3000, you can go to +Currently there is no trivial way to load a plugin running on a local server with `http` into the online CODAP, which forces `https`. One simple solution is to download the latest `build_[...].zip` file from https://codap.concord.org/releases/zips/, extract it to a folder and run it locally. If CODAP is running on port 8080, and this project is running by default on 8081, you can go to -http://127.0.0.1:8080/static/dg/en/cert/index.html?di=http://localhost:3000 +http://127.0.0.1:8080/static/dg/en/cert/index.html?di=http://localhost:8081 to see the plugin running in CODAP. @@ -57,6 +57,47 @@ Instead, it will copy all the configuration files and the transitive dependencie You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. +## Configuration Settings + +Configuration settings control various aspects of the application's behavior and appearance. Access to the configuration settings is provided by `AppConfigContext` via the `useAppConfigContext` hook. + +Default configuration setting values are defined in the `app-config.json` file. Currently, only the `mode` setting can be overridden by URL parameter (e.g. `?mode=development`). Support for overriding some of the other settings with URL parameters may be added in the future. + +### Accessibility + +- **`accessibility`** (Object) + 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. + +### Dimensions + +- **`dimensions`** (Object) + Dimensions of the application's component within CODAP: + - **`width`** (number): The width of the application (in pixels). + - **`height`** (number): The height of the application (in pixels). + +### Mock Assistant + +- **`mockAssistant`** (boolean) + A flag indicating whether to mock AI interactions. + +### Mode + +- **`mode`** (string) + The mode in which the application runs. Possible values: + - `"development"`: Enables additional UI for debugging and artifact maintenance. + - `"production"`: Standard runtime mode for end users. + - `"test"`: Specialized mode for automated testing. + ## Learn More You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). diff --git a/src/app-config-context.ts b/src/app-config-context.ts index 2a647e1..58a185a 100644 --- a/src/app-config-context.ts +++ b/src/app-config-context.ts @@ -1,12 +1,4 @@ -import { createContext, useContext } from "react"; +import { createContext } from "react"; import { AppConfigModelType } from "./models/app-config-model"; export const AppConfigContext = createContext(undefined); - -export const useAppConfigContext = (): AppConfigModelType => { - const context = useContext(AppConfigContext); - if (!context) { - throw new Error("useAppConfig must be used within a AppConfigContext.Provider"); - } - return context; -}; diff --git a/src/app-config.json b/src/app-config.json index c0a1600..4a76c6c 100644 --- a/src/app-config.json +++ b/src/app-config.json @@ -4,8 +4,8 @@ }, "assistant": { "assistantId": "asst_Af8jrKYOFP4MxA9nse61yFBq", - "instructions": "You are DAVAI, an 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.", - "model": "gpt-4o-mini", + "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 }, "dimensions": { diff --git a/src/components/developer-options.test.tsx b/src/components/developer-options.test.tsx index bf73763..add5b9c 100644 --- a/src/components/developer-options.test.tsx +++ b/src/components/developer-options.test.tsx @@ -22,7 +22,7 @@ const mockAssistantStore = AssistantModel.create({ assistant: {}, assistantId: "asst_abc123", instructions: "This is just a test", - model: "model-1", + modelName: "test-model", thread: {}, transcriptStore: mockTranscriptStore, useExistingAssistant: true, diff --git a/src/hooks/use-assistant-store.ts b/src/hooks/use-assistant-store.ts index abe6695..eb4b94c 100644 --- a/src/hooks/use-assistant-store.ts +++ b/src/hooks/use-assistant-store.ts @@ -6,16 +6,16 @@ import { useChatTranscriptStore } from "./use-chat-transcript-store"; export const useAssistantStore = () => { const appConfig = useAppConfigContext(); const transcriptStore = useChatTranscriptStore(); - const { assistantId, instructions, model, useExisting } = appConfig.assistant; + const { assistantId, instructions, modelName, useExisting } = appConfig.assistant; const assistantStore = useMemo(() => { return AssistantModel.create({ assistantId, - model, + modelName, instructions, transcriptStore, useExisting, }); - }, [assistantId, instructions, model, transcriptStore, useExisting]); + }, [assistantId, instructions, modelName, transcriptStore, useExisting]); return assistantStore; }; diff --git a/src/models/app-config-model.ts b/src/models/app-config-model.ts index 4527078..be3a81e 100644 --- a/src/models/app-config-model.ts +++ b/src/models/app-config-model.ts @@ -1,6 +1,27 @@ import { types, Instance, SnapshotIn } from "mobx-state-tree"; import { AppMode, AppModeValues } from "../types"; +/** + * AppConfigModel encapsulates the application's configuration settings. + * It includes properties and methods for managing accessibility, AI assistant settings, and the application's mode. + * + * @property {Object} accessibility - Settings related to accessibility in the UI. + * @property {string} accessibility.keyboardShortcut - Custom keystroke for placing focus in the main text input field (e.g., `ctrl+?`). + * + * @property {Object} assistant - Settings to configure the AI assistant. + * @property {string} assistant.assistantId - The unique ID of an existing assistant to use. + * @property {string} assistant.instructions - Instructions to use when creating new assistants (e.g., `You are helpful data analysis partner.`). + * @property {string} assistant.modelName - The name of the model the assistant should use (e.g., `gpt-4o-mini`). + * @property {boolean} assistant.useExisting - Whether to use an existing assistant. + * + * @property {Object} dimensions - Dimensions of the application's component within CODAP. + * @property {number} dimensions.width - The width of the application (in pixels). + * @property {number} dimensions.height - The height of the application (in pixels). + * + * @property {boolean|null} mockAssistant - A flag indicating whether to mock AI interactions. (optional). + * + * @property {"development"|"production"|"test"} mode - The mode in which the application runs. + */ export const AppConfigModel = types.model("AppConfigModel", { accessibility: types.model({ keyboardShortcut: types.string, @@ -8,7 +29,7 @@ export const AppConfigModel = types.model("AppConfigModel", { assistant: types.model({ assistantId: types.string, instructions: types.string, - model: types.string, + modelName: types.string, useExisting: types.boolean, }), dimensions: types.model({ diff --git a/src/models/assistant-model.ts b/src/models/assistant-model.ts index b25b986..bd1023c 100644 --- a/src/models/assistant-model.ts +++ b/src/models/assistant-model.ts @@ -1,17 +1,32 @@ import { types, flow, Instance } from "mobx-state-tree"; -import { getTools, initLlmConnection } from "../utils/llm-utils"; -import { ChatTranscriptModel } from "./chat-transcript-model"; import { Message } from "openai/resources/beta/threads/messages"; import { getAttributeList, getDataContext } from "@concord-consortium/codap-plugin-api"; +import { getTools, initLlmConnection } from "../utils/llm-utils"; +import { ChatTranscriptModel } from "./chat-transcript-model"; import { DAVAI_SPEAKER } from "../constants"; import { createGraph } from "../utils/codap-utils"; +/** + * AssistantModel encapsulates the AI assistant and its interactions with the user. + * It includes properties and methods for configuring the assistant, handling chat interactions, and maintaining the assistant's + * thread and transcript. + * + * @property {Object|null} assistant - The assistant object, or `null` if not initialized. + * @property {string} assistantId - The unique ID of the assistant being used. + * @property {string} instructions - Instructions provided when creating or configuring a new assistant. + * @property {string} modelName - The identifier for the assistant's model (e.g., "gpt-4o-mini"). + * @property {Object|null} apiConnection - The API connection object for interacting with the assistant, or `null` if not connected. + * @property {Object|null} thread - The assistant's thread used for the current chat, or `null` if no thread is active. + * @property {ChatTranscriptModel} transcriptStore - The assistant's chat transcript store for recording and managing chat messages. + * @property {boolean} useExisting - A flag indicating whether to use an existing assistant (`true`) or create a new one (`false`). + */ export const AssistantModel = types .model("AssistantModel", { assistant: types.maybe(types.frozen()), assistantId: types.string, instructions: types.string, - model: types.string, + modelName: types.string, + apiConnection: types.maybe(types.frozen()), thread: types.maybe(types.frozen()), transcriptStore: ChatTranscriptModel, useExisting: true, @@ -27,22 +42,25 @@ export const AssistantModel = types }, 1000); } })) + .actions((self) => ({ + afterCreate(){ + self.apiConnection = initLlmConnection(); + } + })) .actions((self) => { - const davai = initLlmConnection(); - const initialize = flow(function* () { try { const tools = getTools(); const davaiAssistant = self.useExisting && self.assistantId - ? yield davai.beta.assistants.retrieve(self.assistantId) - : yield davai.beta.assistants.create({instructions: self.instructions, model: self.model, tools }); + ? yield self.apiConnection.beta.assistants.retrieve(self.assistantId) + : yield self.apiConnection.beta.assistants.create({instructions: self.instructions, model: self.modelName, tools }); if (!self.useExisting) { self.assistantId = davaiAssistant.id; } self.assistant = davaiAssistant; - self.thread = yield davai.beta.threads.create(); + self.thread = yield self.apiConnection.beta.threads.create(); } catch (err) { console.error("Failed to initialize assistant:", err); } @@ -50,7 +68,7 @@ export const AssistantModel = types const handleMessageSubmit = flow(function* (messageText) { try { - yield davai.beta.threads.messages.create(self.thread.id, { + yield self.apiConnection.beta.threads.messages.create(self.thread.id, { role: "user", content: messageText, }); @@ -63,14 +81,14 @@ export const AssistantModel = types const startRun = flow(function* () { try { - const run = yield davai.beta.threads.runs.create(self.thread.id, { + const run = yield self.apiConnection.beta.threads.runs.create(self.thread.id, { assistant_id: self.assistant.id, }); // Wait for run completion and handle responses - let runState = yield davai.beta.threads.runs.retrieve(self.thread.id, run.id); + let runState = yield self.apiConnection.beta.threads.runs.retrieve(self.thread.id, run.id); while (runState.status !== "completed" && runState.status !== "requires_action") { - runState = yield davai.beta.threads.runs.retrieve(self.thread.id, run.id); + runState = yield self.apiConnection.beta.threads.runs.retrieve(self.thread.id, run.id); } if (runState.status === "requires_action") { @@ -78,7 +96,7 @@ export const AssistantModel = types } // Get the last assistant message from the messages array - const messages = yield davai.beta.threads.messages.list(self.thread.id); + const messages = yield self.apiConnection.beta.threads.messages.list(self.thread.id); const lastMessageForRun = messages.data.filter( (msg: Message) => msg.run_id === run.id && msg.role === "assistant" ).pop(); @@ -96,13 +114,13 @@ export const AssistantModel = types try { const toolOutputs = runState.required_action?.submit_tool_outputs.tool_calls ? yield Promise.all( - runState.required_action.submit_tool_outputs.tool_calls.map(async (toolCall: any) => { + runState.required_action.submit_tool_outputs.tool_calls.map(flow(function* (toolCall: any) { if (toolCall.function.name === "get_attributes") { const { dataset } = JSON.parse(toolCall.function.arguments); // getting the root collection won't always work. what if a user wants the attributes // in the Mammals dataset but there is a hierarchy? - const rootCollection = (await getDataContext(dataset)).values.collections[0]; - const attributeList = await getAttributeList(dataset, rootCollection.name); + const rootCollection = (yield getDataContext(dataset)).values.collections[0]; + const attributeList = yield getAttributeList(dataset, rootCollection.name); return { tool_call_id: toolCall.id, output: JSON.stringify(attributeList) }; } else { const { dataset, name, xAttribute, yAttribute } = JSON.parse(toolCall.function.arguments); @@ -110,26 +128,14 @@ export const AssistantModel = types return { tool_call_id: toolCall.id, output: "Graph created." }; } }) - ) + )) : []; if (toolOutputs) { - davai.beta.threads.runs.submitToolOutputsStream( + yield self.apiConnection.beta.threads.runs.submitToolOutputs( self.thread.id, runId, { tool_outputs: toolOutputs } ); - const threadMessageList = yield davai.beta.threads.messages.list(self.thread.id); - const threadMessages = threadMessageList.data.map((msg: any) => ({ - role: msg.role, - content: msg.content[0].text.value, - })); - - yield davai.chat.completions.create({ - model: "gpt-4o-mini", - messages: [ - ...threadMessages - ], - }); } } catch (err) { console.error(err); @@ -138,7 +144,7 @@ export const AssistantModel = types const createThread = flow(function* () { try { - const newThread = yield davai.beta.threads.create(); + const newThread = yield self.apiConnection.beta.threads.create(); self.thread = newThread; } catch (err) { console.error("Error creating thread:", err); diff --git a/src/models/chat-transcript-model.ts b/src/models/chat-transcript-model.ts index 6e0b101..8697290 100644 --- a/src/models/chat-transcript-model.ts +++ b/src/models/chat-transcript-model.ts @@ -7,6 +7,13 @@ const MessageModel = types.model("MessageModel", { timestamp: types.string, }); +/** + * ChatTranscriptModel encapsulates the transcript of a chat between an AI assistant and the user. + * It includes properties for adding messages and clearing the transcript. + * + * @property {Array} messages - An array of messages in the chat transcript. + * Each message includes details about the speaker, content, and timestamp. + */ export const ChatTranscriptModel = types .model("ChatTranscriptModel", { messages: types.array(MessageModel), diff --git a/src/types.ts b/src/types.ts index 560d15e..e15a8e2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -11,7 +11,7 @@ export type AppConfig = { assistant: { assistantId: string; instructions: string; - model: string; + modelName: string; useExisting: boolean; }; dimensions: { diff --git a/src/utils/anthropic-utils.ts b/src/utils/anthropic-utils.ts deleted file mode 100644 index 3bee8d9..0000000 --- a/src/utils/anthropic-utils.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Anthropic } from "@anthropic-ai/sdk"; - -export const newAnthropic = () => { - return new Anthropic({ - apiKey: process.env.REACT_APP_ANTHROPIC_API_KEY, - }); -}; - -export const anthropicTools = [ - { - name: "get_attributes", - description: "Get the current weather in a given location", - input_schema: { - type: "object", - properties: { - dataset: { - type: "string", - description: "The specified dataset containing attributes" - } - }, - required: [ - "dataset" - ] - } - }, - { - name: "create_graph", - description: "Create a graph tile in CODAP", - input_schema: { - type: "object", - properties: { - name: { - type: "string", - description: "A name for the graph" - }, - xAttribute: { - type: "string", - description: "The x-axis attribute" - }, - yAttribute: { - type: "string", - description: "The y-axis attribute" - } - }, - required: [ - "name", - "xAttribute", - "yAttribute" - ] - } - } -]; From 034c4e1a444f055458e5448b63f62dc5c0e604e5 Mon Sep 17 00:00:00 2001 From: Ethan McElroy Date: Tue, 10 Dec 2024 18:22:13 -0500 Subject: [PATCH 7/8] chore: remove anthropic module --- package-lock.json | 31 ------------------------------- package.json | 1 - 2 files changed, 32 deletions(-) diff --git a/package-lock.json b/package-lock.json index e6521af..4968a73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "0.0.1", "license": "MIT", "dependencies": { - "@anthropic-ai/sdk": "^0.32.1", "@types/dom-speech-recognition": "^0.0.4", "dotenv": "^16.4.5", "mobx-react-lite": "^4.0.7", @@ -100,36 +99,6 @@ "node": ">=6.0.0" } }, - "node_modules/@anthropic-ai/sdk": { - "version": "0.32.1", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.32.1.tgz", - "integrity": "sha512-U9JwTrDvdQ9iWuABVsMLj8nJVwAyQz6QXvgLsVhryhCEPkLsbcP/MXxm+jYcAwLoV8ESbaTTjnD4kuAFa+Hyjg==", - "license": "MIT", - "dependencies": { - "@types/node": "^18.11.18", - "@types/node-fetch": "^2.6.4", - "abort-controller": "^3.0.0", - "agentkeepalive": "^4.2.1", - "form-data-encoder": "1.7.2", - "formdata-node": "^4.3.2", - "node-fetch": "^2.6.7" - } - }, - "node_modules/@anthropic-ai/sdk/node_modules/@types/node": { - "version": "18.19.66", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.66.tgz", - "integrity": "sha512-14HmtUdGxFUalGRfLLn9Gc1oNWvWh5zNbsyOLo5JV6WARSeN1QcEBKRnZm9QqNfrutgsl/hY4eJW63aZ44aBCg==", - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@anthropic-ai/sdk/node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "license": "MIT" - }, "node_modules/@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", diff --git a/package.json b/package.json index 95bc4b1..78c7c70 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,6 @@ "webpack-dev-server": "^4.15.0" }, "dependencies": { - "@anthropic-ai/sdk": "^0.32.1", "@types/dom-speech-recognition": "^0.0.4", "dotenv": "^16.4.5", "mobx-react-lite": "^4.0.7", From 68b42cd5bde108d32d9f08d67cd28b797ab51c9a Mon Sep 17 00:00:00 2001 From: Ethan McElroy Date: Wed, 11 Dec 2024 08:53:57 -0500 Subject: [PATCH 8/8] chore: utility function for thread deletion --- src/models/assistant-model.ts | 10 ++-------- src/utils/openai-utils.ts | 13 +++++++++++++ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/models/assistant-model.ts b/src/models/assistant-model.ts index bd1023c..896ba77 100644 --- a/src/models/assistant-model.ts +++ b/src/models/assistant-model.ts @@ -5,6 +5,7 @@ import { getTools, initLlmConnection } from "../utils/llm-utils"; import { ChatTranscriptModel } from "./chat-transcript-model"; import { DAVAI_SPEAKER } from "../constants"; import { createGraph } from "../utils/codap-utils"; +import { requestThreadDeletion } from "../utils/openai-utils"; /** * AssistantModel encapsulates the AI assistant and its interactions with the user. @@ -159,14 +160,7 @@ export const AssistantModel = types } const threadId = self.thread.id; - const response = yield fetch(`${process.env.REACT_APP_OPENAI_BASE_URL}threads/${threadId}`, { - method: "DELETE", - headers: { - Authorization: `Bearer ${process.env.REACT_APP_OPENAI_API_KEY}`, - "OpenAI-Beta": "assistants=v2", - "Content-Type": "application/json", - }, - }); + const response = yield requestThreadDeletion(threadId); if (response.ok) { self.thread = undefined; diff --git a/src/utils/openai-utils.ts b/src/utils/openai-utils.ts index 674c1eb..d016be6 100644 --- a/src/utils/openai-utils.ts +++ b/src/utils/openai-utils.ts @@ -70,3 +70,16 @@ export const openAiTools: AssistantTool[] = [ } } ]; + +export const requestThreadDeletion = async (threadId: string): Promise => { + const response = await fetch(`${process.env.REACT_APP_OPENAI_BASE_URL}threads/${threadId}`, { + method: "DELETE", + headers: { + Authorization: `Bearer ${process.env.REACT_APP_OPENAI_API_KEY}`, + "OpenAI-Beta": "assistants=v2", + "Content-Type": "application/json", + }, + }); + + return response; +};