Skip to content

Commit

Permalink
Merge branch 'main' into 188589592-debug-output-2
Browse files Browse the repository at this point in the history
  • Loading branch information
lublagg committed Dec 11, 2024
2 parents f929309 + c130354 commit b3ccd54
Show file tree
Hide file tree
Showing 29 changed files with 657 additions and 209 deletions.
45 changes: 43 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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).
Expand Down
7 changes: 3 additions & 4 deletions cypress/e2e/workspace.test.ts
Original file line number Diff line number Diff line change
@@ -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...");
});
});
31 changes: 0 additions & 31 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions src/app-config-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { createContext } from "react";
import { AppConfigModelType } from "./models/app-config-model";

export const AppConfigContext = createContext<AppConfigModelType | undefined>(undefined);
25 changes: 25 additions & 0 deletions src/app-config-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from "react";
import { AppConfig, isAppMode } 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<AppConfig> = {
mode: isAppMode(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 <AppConfigContext.Provider value={appConfig}>{children}</AppConfigContext.Provider>;
};
30 changes: 15 additions & 15 deletions src/app-config.json
Original file line number Diff line number Diff line change
@@ -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, 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": {
"height": 680,
"width": 380
},
"mockAssistant": false,
"mode": "production"
}
27 changes: 22 additions & 5 deletions src/components/App.test.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,34 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { App } from "./App";
import { mockAppConfig } from "../test-utils/mock-app-config";
import { MockAppConfigProvider } from "../test-utils/app-config-provider";

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(),
}
}));


describe("test load app", () => {
it("renders without crashing", () => {
render(<App />);
render(
<MockAppConfigProvider>
<App />
</MockAppConfigProvider>
);
expect(screen.getByText("Loading...")).toBeDefined();
});
});
85 changes: 68 additions & 17 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,41 @@
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);
const url = new URL(window.location.href);
const params = new URLSearchParams(url.search);
const hasDebugParams = params.has("debug");
const [showDebugLog, setShowDebugLog] = useState(hasDebugParams);

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 = () => {
Expand All @@ -61,15 +60,54 @@ export const App = observer(() => {
setPlaybackSpeed(speed);
};

if (!assistantStore.assistant) {
return <div>Loading...</div>;
}

const handleChatInputSubmit = async (messageText: string) => {
transcriptStore.addMessage(USER_SPEAKER, {content: 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 lose any existing chat history.");
if (!confirmCreate) return;

transcriptStore.clearTranscript();
transcriptStore.addMessage(DAVAI_SPEAKER, {content: 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, {content: GREETING});
appConfig.toggleMockAssistant();
} else {
appConfig.toggleMockAssistant();
}
};

if (!assistantStore.assistant) {
return <div>Loading...</div>;
}

return (
<div className="App">
<header>
Expand Down Expand Up @@ -103,6 +141,7 @@ export const App = observer(() => {
</div>
}
<ChatInputComponent
disabled={!assistantStore.thread && !appConfig.isAssistantMocked}
keyboardShortcutEnabled={keyboardShortcutEnabled}
shortcutKeys={keyboardShortcutKeys}
onSubmit={handleChatInputSubmit}
Expand All @@ -122,6 +161,18 @@ export const App = observer(() => {
onCustomizeShortcut={handleCustomizeShortcut}
onToggleShortcut={handleToggleShortcut}
/>
{appConfig.mode === "development" &&
<>
<hr />
<h2>Developer Options</h2>
<DeveloperOptionsComponent
assistantStore={assistantStore}
onCreateThread={handleCreateThread}
onDeleteThread={handleDeleteThread}
onMockAssistant={handleMockAssistant}
/>
</>
}
</div>
);
});
9 changes: 9 additions & 0 deletions src/components/chat-input.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -57,6 +62,10 @@
&.dictate {
order: 1;
}
&:disabled {
cursor: not-allowed;
opacity: .5;
}
}
}
}
Loading

0 comments on commit b3ccd54

Please sign in to comment.