Skip to content
Merged
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
10 changes: 10 additions & 0 deletions src/NemoWorkerContext/NemoWorkerContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { createContext, MutableRefObject, useContext } from "react";
import { NemoWorker } from "../nemoWorker/NemoWorker";

export const NemoWorkerContext = createContext<MutableRefObject<
NemoWorker | undefined
> | null>(null);

export function useNemoWorkerRef() {
return useContext(NemoWorkerContext);
}
17 changes: 17 additions & 0 deletions src/NemoWorkerContext/NemoWorkerContextProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useRef } from "react";
import { NemoWorker } from "../nemoWorker/NemoWorker";
import { NemoWorkerContext } from "./NemoWorkerContext";

interface NemoWorkerProviderProps {
children: React.ReactNode;
}

export function NemoWorkerProvider({ children }: NemoWorkerProviderProps) {
const workerRef = useRef<NemoWorker | undefined>(undefined);

return (
<NemoWorkerContext.Provider value={workerRef}>
{children}
</NemoWorkerContext.Provider>
);
}
97 changes: 97 additions & 0 deletions src/NevBroadcastChannel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { useContext, useEffect } from "react";
import { NemoSessionIdContext } from "./nemoSessionIdContext";

let singletonNevChannel: BroadcastChannel | undefined = undefined;

function initChannel() {
if (!singletonNevChannel) {
singletonNevChannel = new BroadcastChannel("NemoVisualization");
}
}

export type NevBroadcastChannelHandler<RequestPayload, ResponsePayload> = (
payload: RequestPayload,
) => Promise<ResponsePayload>;
type CloseCallback = () => void;

function addNevListener<RequestPayload, ResponsePayload>(
nemoSessionId: string,
eventName: string,
handler: NevBroadcastChannelHandler<RequestPayload, ResponsePayload>,
): CloseCallback {
// create connection to channel if none exist yet
// NOTE: We never close the connection since there is no sensible time to do so.
// During normal operations, once reasoning has been performed, there will always be listeners on the channel.
initChannel();
const bc = singletonNevChannel!;

const onMessage = async (event: MessageEvent) => {
// Don't listen on own messages (which might be filtered by default by the channel, I did not check this...).
if (!!event.data.error || !!event.data.responseType) {
return;
}

// Ignore messages that are not meant for this nemo session.
if (event.data.nemoId !== nemoSessionId) {
return;
}

const id = event.data.id;

if (!event.data.queryType || !event.data.payload) {
bc.postMessage({
nemoId: nemoSessionId,
id,
error: "Expected an object with queryType and payload.",
});
return;
}

const { queryType, payload } = event.data; // data should consist of queryType and payload

// ignore everything but the target eventName
if (queryType !== eventName) {
return;
}

try {
const response = await handler(payload);
// if the handler has a proper response, then we send the response
// (some handler might just perform an action and don't respond)
if (response !== undefined) {
bc.postMessage({
nemoId: nemoSessionId,
id,
responseType: eventName,
payload: response,
});
}
} catch (error) {
console.error(error);
bc.postMessage({
nemoId: nemoSessionId,
id,
error,
});
}
};

bc.addEventListener("message", onMessage);

return () => {
bc.removeEventListener("message", onMessage);
};
}

export function useNevBroadcastChannelListener<RequestPayload, ResponsePayload>(
eventName: string,
handler: NevBroadcastChannelHandler<RequestPayload, ResponsePayload>,
) {
const nemoSessionId = useContext(NemoSessionIdContext);

useEffect(() => {
const removeListener = addNevListener(nemoSessionId, eventName, handler);

return () => removeListener();
}, [nemoSessionId, eventName, handler]);
}
62 changes: 62 additions & 0 deletions src/components/editor/Editor.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,82 @@
import { useCallback, useEffect, useRef } from "react";
import { useAppDispatch, useAppSelector } from "../../store";
import "./Editor.css";
import { programInfoSlice } from "../../store/programInfo";
import { selectDarkMode } from "../../store/preferences/selectors/selectDarkMode";
import { selectProgramText } from "../../store/programInfo/selectors/selectProgramText";
import { LazyMonacoWrapper } from "./LazyMonacoWrapper";
import { DefaultSuspense } from "../DefaultSuspense";
import {
NevBroadcastChannelHandler,
useNevBroadcastChannelListener,
} from "../../NevBroadcastChannel";
import { useNemoWorkerRef } from "../../NemoWorkerContext/NemoWorkerContext";
import { MonacoWrapperImperativeHandle } from "./MonacoWrapper";

export function Editor() {
const dispatch = useAppDispatch();

const darkMode = useAppSelector(selectDarkMode);
const programText = useAppSelector(selectProgramText);

const workerRef = useNemoWorkerRef();

const editorRef = useRef<MonacoWrapperImperativeHandle>(null);

// request notification permission
useEffect(() => {
if (Notification.permission === "default") {
Notification.requestPermission();
}
}, []);

// Jump to line in editor when Nev requests it
const jumpToRuleHandler: NevBroadcastChannelHandler<{ id: number }, string> =
useCallback(
async ({ id: ruleId }) => {
if (!workerRef?.current) {
throw "Cannot process message. Reasoning was not performed.";
}

// get line number from rule id using the Nemo backend
const lineToJumpTo =
await workerRef.current.getLineNumberFromRuleId(ruleId);

if (lineToJumpTo === undefined) {
throw "The target Rule Id was not found in the program.";
}

// briefly change window title
const originalTitle = document.title;
document.title = `[!] ${originalTitle}`;
setTimeout(() => {
document.title = originalTitle;
}, 2000);
Comment on lines +49 to +54

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is this really something we want to do? I'd find that pretty intrusive, when something like a flash message would suffice for this?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ah, well, this happens while the Nev tab is in foreground, doesn't it? Still, perhaps better to just prefix the existing title instead of overriding it completely?

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.

Yes, we were trying to find reliable ways to indicate to a user that they should switch the tab and to highlight which tab is even the relevant one. I'm also not quite happy with what we have but I also do not have a good solution in mind 😅
Prefixing instead of completely changing the title sounds better though. I'll change that :)


// send notification if allowed
if (Notification.permission === "granted") {
const notification = new Notification("Rule Highlighted", {
body: "The rule has been highlighted in the editor.",
});
notification.addEventListener("click", () => window.focus());
}

editorRef.current?.jumpToLine(lineToJumpTo);

// Nev expects some kind of response...
return "Successfully jumped to line";
},
[workerRef],
);
useNevBroadcastChannelListener<{ id: number }, string>(
"jumpToRule",
jumpToRuleHandler,
);

return (
<DefaultSuspense>
<LazyMonacoWrapper
ref={editorRef}
darkMode={darkMode}
programText={programText}
onProgramTextChange={(programText: string) => {
Expand Down
Loading