Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,13 @@
"@sentry/react": "^10.5.0",
"@sourceacademy/autocomplete": "github:source-academy/autocomplete#e669d9ed98753350a3c8433a92985227eb789663",
"@sourceacademy/c-slang": "^1.0.21",
"@sourceacademy/common-tabs": "file:../plugins/src/common/tabs",
"@sourceacademy/conductor": "https://github.com/source-academy/conductor.git#0.4.0",
"@sourceacademy/language-directory": "https://github.com/source-academy/language-directory.git#0.0.6",
"@sourceacademy/plugin-directory": "https://github.com/source-academy/plugin-directory.git#0.0.2",
"@sourceacademy/sharedb-ace": "2.1.1",
"@sourceacademy/sling-client": "^0.1.0",
"@sourceacademy/web-test": "file:..\\plugins\\src\\web\\test",
"@szhsin/react-menu": "^4.0.0",
"@tanstack/react-query": "^5.100.14",
"@tanstack/react-table": "^8.9.3",
Expand Down
14 changes: 9 additions & 5 deletions src/commons/assessmentWorkspace/AssessmentWorkspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ import SideContentToneMatrix from '../sideContent/content/SideContentToneMatrix'
import type { SideContentProps } from '../sideContent/SideContent';
import { changeSideContentHeight } from '../sideContent/SideContentActions';
import { useSideContent } from '../sideContent/SideContentHelper';
import { type SideContentTab, SideContentType } from '../sideContent/SideContentTypes';
import {
type SideContentTab,
type SideContentTabId,
SideContentType,
} from '../sideContent/SideContentTypes';
import Constants from '../utils/Constants';
import { useResponsive, useTypedSelector } from '../utils/Hooks';
import { assessmentTypeLink } from '../utils/ParamParseHelper';
Expand Down Expand Up @@ -666,8 +670,8 @@ function AssessmentWorkspace(props: AssessmentWorkspaceProps) {
}

const onChangeTabs = (
newTabId: SideContentType,
prevTabId: SideContentType,
newTabId: SideContentTabId,
prevTabId: SideContentTabId,
event: React.MouseEvent<HTMLElement>,
) => {
if (newTabId === prevTabId) {
Expand Down Expand Up @@ -879,8 +883,8 @@ function AssessmentWorkspace(props: AssessmentWorkspaceProps) {

const mobileSideContentProps: (q: number) => MobileSideContentProps = (questionId: number) => {
const onChangeTabs = (
newTabId: SideContentType,
prevTabId: SideContentType,
newTabId: SideContentTabId,
prevTabId: SideContentTabId,
event: React.MouseEvent<HTMLElement>,
) => {
if (newTabId === prevTabId) {
Expand Down
12 changes: 8 additions & 4 deletions src/commons/mobileWorkspace/MobileWorkspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import McqChooser, { type McqChooserProps } from '../mcqChooser/McqChooser';
import { Prompt } from '../ReactRouterPrompt';
import type { ReplProps } from '../repl/Repl';
import type { SideBarTab } from '../sideBar/SideBar';
import { type SideContentTab, SideContentType } from '../sideContent/SideContentTypes';
import {
type SideContentTab,
type SideContentTabId,
SideContentType,
} from '../sideContent/SideContentTypes';
import DraggableRepl from './DraggableRepl';
import MobileKeyboard from './MobileKeyboard';
import MobileSideContent, {
Expand Down Expand Up @@ -169,7 +173,7 @@ function MobileWorkspace(props: MobileWorkspaceProps) {

const handleEditorEval = props.editorContainerProps?.handleEditorEval;
const handleTabChangeForRepl = useCallback(
(newTabId: SideContentType, prevTabId: SideContentType) => {
(newTabId: SideContentTabId, prevTabId: SideContentTabId) => {
// Evaluate program upon pressing the run tab.
if (newTabId === SideContentType.mobileEditorRun) {
handleEditorEval?.();
Expand Down Expand Up @@ -210,8 +214,8 @@ function MobileWorkspace(props: MobileWorkspaceProps) {
const onChange = props.mobileSideContentProps.onChange;
const onSideContentTabChange = useCallback(
(
newTabId: SideContentType,
prevTabId: SideContentType,
newTabId: SideContentTabId,
prevTabId: SideContentTabId,
event: React.MouseEvent<HTMLElement>,
) => {
onChange(newTabId, prevTabId, event);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
type ChangeTabsCallback,
type SideContentLocation,
type SideContentTab,
SideContentType,
type SideContentTabId,
} from '../../sideContent/SideContentTypes';
import { propsAreEqual } from '../../utils/MemoizeHelper';
import MobileControlBar from './MobileControlBar';
Expand Down Expand Up @@ -66,7 +66,7 @@ function MobileSideContent({
* renderedPanels is not memoized since a change in selectedTabId (when changing tabs)
* would force React.useMemo to recompute the nullary function anyway
*/
const renderedPanels = (dynamicTabs: SideContentTab[], selectedTabId?: SideContentType) => {
const renderedPanels = (dynamicTabs: SideContentTab[], selectedTabId?: SideContentTabId) => {
// TODO: Fix the CSS of all the panels (e.g. subst_visualizer)
const renderPanel = (tab: SideContentTab, workspaceLocation?: SideContentLocation) => {
if (!tab.body) return;
Expand Down
4 changes: 2 additions & 2 deletions src/commons/sagas/AchievementSaga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type { Tokens } from '../application/types/SessionTypes';
import { combineSagaHandlers } from '../redux/utils';
import SideContentActions from '../sideContent/SideContentActions';
import { getLocation } from '../sideContent/SideContentHelper';
import { SideContentType } from '../sideContent/SideContentTypes';
import { type SideContentTabId, SideContentType } from '../sideContent/SideContentTypes';
import { actions } from '../utils/ActionsHelper';
import Constants from '../utils/Constants';
import { selectTokens } from './BackendSaga';
Expand Down Expand Up @@ -145,7 +145,7 @@ const AchievementSaga = combineSagaHandlers({
(state: OverallState) => state.session.enableAchievements,
);
if (workspaceLocation !== undefined && eventNames.find(e => e === EventType.ERROR)) {
const selectedTab: SideContentType | undefined = yield select((state: OverallState) => {
const selectedTab: SideContentTabId | undefined = yield select((state: OverallState) => {
const [loc] = getLocation(workspaceLocation);
return state.sideContent[loc].selectedTab;
});
Expand Down
7 changes: 4 additions & 3 deletions src/commons/sagas/SideContentSaga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { getLocation } from '../sideContent/SideContentHelper';
import {
type SideContentLocation,
type SideContentManagerState,
type SideContentTabId,
SideContentType,
} from '../sideContent/SideContentTypes';
import WorkspaceActions from '../workspace/WorkspaceActions';
Expand All @@ -25,7 +26,7 @@ const isVisitSideContent = (
const selectSelectedTab = (
state: any,
workspaceLocation: SideContentLocation,
): SideContentType | undefined => {
): SideContentTabId | undefined => {
const sideContentState = (state.sideContent ?? state) as SideContentManagerState;
const [location] = getLocation(workspaceLocation);
return sideContentState[location]?.selectedTab;
Expand All @@ -38,7 +39,7 @@ const SideContentSaga = combineSagaHandlers({
// When a program finishes evaluation, we clear all alerts,
// So we must wait until after and all module tabs have been spawned
// to process any kind of alerts that were raised by non-module side content
const selectedTab: SideContentType | undefined = yield select((state: any) =>
const selectedTab: SideContentTabId | undefined = yield select((state: any) =>
selectSelectedTab(state, workspaceLocation),
);

Expand Down Expand Up @@ -72,7 +73,7 @@ const SideContentSaga = combineSagaHandlers({
return;
}

const selectedTabAfterWait: SideContentType | undefined = yield select((state: any) =>
const selectedTabAfterWait: SideContentTabId | undefined = yield select((state: any) =>
selectSelectedTab(state, workspaceLocation),
);

Expand Down
4 changes: 3 additions & 1 deletion src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { selectConductorEnable } from '../../../../features/conductor/flagConduc
import LanguageDirectoryActions from '../../../../features/directory/LanguageDirectoryActions';
import { type OverallState } from '../../../application/ApplicationTypes';
import { visitSideContent } from '../../../sideContent/SideContentActions';
import sideContentManager from '../../../sideContent/SideContentManager';
import { SideContentType } from '../../../sideContent/SideContentTypes';
import { actions } from '../../../utils/ActionsHelper';
import DisplayBufferService from '../../../utils/DisplayBufferService';
Expand Down Expand Up @@ -548,7 +549,7 @@ export function* evalCodeConductorSaga(
// Reuse a preloaded conductor instance when available.
const { hostPlugin, conduit }: { hostPlugin: BrowserHostPlugin; conduit: IConduit } = yield call(
getPreparedConductorSaga,
{ files, consume: true },
{ files, consume: true, workspaceLocation },
);

// Begin evaluation
Expand All @@ -574,6 +575,7 @@ export function* evalCodeConductorSaga(
}
yield cancel(statusTask);
yield call([conduit, 'terminate']);
yield call([sideContentManager, sideContentManager.clearTabs]);
yield cancel(stdoutTask);
Comment on lines 577 to 579

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Clearing the tabs immediately when the evaluation finishes or is terminated will cause any dynamically loaded tabs (such as visualization or interactive game tabs) to disappear as soon as the program stops running.

To allow users to interact with and view the tabs after the program execution completes, the tabs should persist. Instead of clearing them here at the end of the evaluation, you should clear them at the beginning of evalCodeConductorSaga when a new execution is initiated.

Suggested change
yield call([conduit, 'terminate']);
yield call([sideContentManager, sideContentManager.clearTabs]);
yield cancel(stdoutTask);
yield call([conduit, 'terminate']);
yield cancel(stdoutTask);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That isn't true, it doesn't immediately vanish. It does reload when the run button is pressed, so maybe finding a way to persistently get run the Conductor across evaluations is something to consider

yield cancel(resultTask);
yield cancel(errorTask);
Expand Down
51 changes: 48 additions & 3 deletions src/commons/sagas/helpers/conductorEvaluatorCache.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import type { IConduit } from '@sourceacademy/conductor/conduit';
import { PluginType } from '@sourceacademy/plugin-directory';
import type { SagaIterator } from 'redux-saga';
import { call } from 'redux-saga/effects';
import { call, select } from 'redux-saga/effects';
import { requireProvider } from 'src/commons/sideContent/SideContentHelper';
import { registry } from 'src/features/conductor/Registry';
import { selectDirectoryPluginUrl } from 'src/features/directory/flagDirectoryPluginUrl';

import type { BrowserHostPlugin } from '../../../features/conductor/BrowserHostPlugin';
import { createConductor } from '../../../features/conductor/createConductor';
import type { OverallState } from '../../application/ApplicationTypes';
import sideContentManager from '../../sideContent/SideContentManager';
import type { SideContentLocation } from '../../sideContent/SideContentTypes';

type PreparedConductor = {
path: string;
Expand All @@ -16,13 +23,25 @@ type PreparedConductor = {
type GetPreparedConductorOptions = {
files?: Record<string, string>;
consume?: boolean;
workspaceLocation?: SideContentLocation;
};

let preparedConductorPath: string | null = null;
let preparedConductor: PreparedConductor | null = null;
let loadingConductorPath: string | null = null;
let loadingConductorPromise: Promise<PreparedConductor> | null = null;
let currentEvaluatorPath: string | null = null;
let currentPluginDirectoryUrl: string | null = null;
let currentPluginMap: OverallState['pluginDirectory']['pluginMap'] = {};

function getWebPluginLocation(pluginName: string): string | undefined {
return currentPluginMap[pluginName]?.resolutions[PluginType.WEB];
}

function* updatePluginDirectorySnapshotSaga(): SagaIterator {
currentPluginMap = yield select((state: OverallState) => state.pluginDirectory.pluginMap);
currentPluginDirectoryUrl = yield select(selectDirectoryPluginUrl);
}

async function fetchEvaluatorObjectUrl(path: string): Promise<string> {
const evaluatorResponse = await fetch(path);
Expand All @@ -40,6 +59,7 @@ async function terminatePreparedConductor(conductor: PreparedConductor | null) {
}

await conductor.conduit.terminate();
sideContentManager.clearTabs();
URL.revokeObjectURL(conductor.evaluatorUrl);
Comment thread
AaravMalani marked this conversation as resolved.
}

Expand All @@ -61,8 +81,28 @@ async function createPreparedConductor(path: string): Promise<PreparedConductor>
const { hostPlugin, conduit } = createConductor(
evaluatorUrl,
async (fileName: string) => currentFiles[fileName],
(_pluginName: string) => {
// TODO: implement dynamic plugin loading
async (pluginName: string) => {
if (registry.has(pluginName)) {
const pluginClass = registry.get(pluginName)!;
conduit.registerPlugin(pluginClass, sideContentManager);
return;
}

let pluginClassLocation = getWebPluginLocation(pluginName);
if (!pluginClassLocation) {
console.warn(`No web plugin resolution found for "${pluginName}" in the plugin directory.`);
return;
}
if (!pluginClassLocation.startsWith('http')) {
pluginClassLocation = new URL(
pluginClassLocation,
currentPluginDirectoryUrl || document.baseURI,
).toString();
}
await import(/* webpackIgnore: true */ pluginClassLocation)
.then(tab => tab.default(requireProvider))
.then(plugin => conduit.registerPlugin(plugin, sideContentManager))
.catch(error => console.error(`Unable to load external plugin "${pluginName}".`, error));
},
);

Expand Down Expand Up @@ -109,6 +149,7 @@ export function* preloadConductorEvaluatorSaga(path?: string): SagaIterator {
return;
}

yield call(updatePluginDirectorySnapshotSaga);
currentEvaluatorPath = path;
yield call(ensurePreparedConductorSaga, path);
}
Expand All @@ -125,6 +166,10 @@ export function* getPreparedConductorSaga(
}

const path = currentEvaluatorPath;
yield call(updatePluginDirectorySnapshotSaga);
if (options?.workspaceLocation) {
sideContentManager.setWorkspaceLocation(options.workspaceLocation);
}
const prepared: PreparedConductor = yield call(ensurePreparedConductorSaga, path);
const files = options?.files;
const consume = options?.consume ?? false;
Expand Down
6 changes: 3 additions & 3 deletions src/commons/sideContent/SideContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type {
ChangeTabsCallback,
SideContentLocation,
SideContentTab,
SideContentType,
SideContentTabId,
} from './SideContentTypes';

export type SideContentProps = {
Expand All @@ -19,8 +19,8 @@ export type SideContentProps = {
afterDynamicTabs: SideContentTab[];
};
onChange?: ChangeTabsCallback;
selectedTabId?: SideContentType;
defaultTab?: SideContentType;
selectedTabId?: SideContentTabId;
defaultTab?: SideContentTabId;
workspaceLocation: SideContentLocation;
};

Expand Down
12 changes: 6 additions & 6 deletions src/commons/sideContent/SideContentActions.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import { createActions } from '../redux/utils';
import type { DebuggerContext, WorkspaceLocation } from '../workspace/WorkspaceTypes';
import type { SideContentLocation, SideContentType } from './SideContentTypes';
import type { SideContentLocation, SideContentTabId } from './SideContentTypes';

const SideContentActions = createActions('sideContent', {
beginAlertSideContent: (id: SideContentType, workspaceLocation: SideContentLocation) => ({
beginAlertSideContent: (id: SideContentTabId, workspaceLocation: SideContentLocation) => ({
id,
workspaceLocation,
}),
endAlertSideContent: (id: SideContentType, workspaceLocation: SideContentLocation) => ({
endAlertSideContent: (id: SideContentTabId, workspaceLocation: SideContentLocation) => ({
id,
workspaceLocation,
}),
visitSideContent: (
newId: SideContentType,
prevId: SideContentType | undefined,
newId: SideContentTabId,
prevId: SideContentTabId | undefined,
workspaceLocation: SideContentLocation,
) => ({ newId, prevId, workspaceLocation }),
removeSideContentAlert: (id: SideContentType, workspaceLocation: SideContentLocation) => ({
removeSideContentAlert: (id: SideContentTabId, workspaceLocation: SideContentLocation) => ({
id,
workspaceLocation,
}),
Expand Down
11 changes: 7 additions & 4 deletions src/commons/sideContent/SideContentHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ import type {
SideContentLocation,
SideContentState,
SideContentTab,
SideContentTabId,
} from './SideContentTypes';
import { SideContentType } from './SideContentTypes';

const requireProvider = (x: string) => {
export const requireProvider = (x: string) => {
const exports = {
react: React,
'react/jsx-runtime': JSXRuntime,
Expand All @@ -41,7 +42,9 @@ const requireProvider = (x: string) => {
return exports[x as keyof typeof exports] as any;
};

type RawTab = (provider: ReturnType<typeof requireProvider>) => { default: ModuleSideContent };
export type RawTab = (provider: ReturnType<typeof requireProvider>) => {
default: ModuleSideContent;
};

/**
* Returns an array of SideContentTabs to be spawned
Expand Down Expand Up @@ -72,14 +75,14 @@ export const getTabId = (tab: SideContentTab) =>
export const generateTabAlert = (shouldAlert: boolean) =>
`side-content-tooltip${shouldAlert ? ' side-content-tab-alert' : ''}`;

export const useSideContent = (location: SideContentLocation, defaultTab?: SideContentType) => {
export const useSideContent = (location: SideContentLocation, defaultTab?: SideContentTabId) => {
const [workspaceLocation] = getLocation(location);
const { alerts, dynamicTabs, selectedTab, height }: SideContentState = useTypedSelector(
state => state.sideContent[workspaceLocation],
);
const dispatch = useDispatch();
const setSelectedTab = useCallback(
(newId: SideContentType) => {
(newId: SideContentTabId) => {
if (
(selectedTab === SideContentType.substVisualizer ||
selectedTab === SideContentType.cseMachine) &&
Expand Down
Loading