diff --git a/agenthub/codeact_agent/codeact_agent.py b/agenthub/codeact_agent/codeact_agent.py
index 59b8ba131c1..3154db5dafb 100644
--- a/agenthub/codeact_agent/codeact_agent.py
+++ b/agenthub/codeact_agent/codeact_agent.py
@@ -122,6 +122,12 @@ def step(self, state: State) -> Action:
self.messages.append({'role': 'user', 'content': content})
elif isinstance(obs, IPythonRunCellObservation):
content = 'OBSERVATION:\n' + obs.content
+ # replace base64 images with a placeholder
+ splited = content.split('\n')
+ for i, line in enumerate(splited):
+ if '![image](data:image/png;base64,' in line:
+ splited[i] = '![image](data:image/png;base64, ...) already displayed to user'
+ content = '\n'.join(splited)
self.messages.append({'role': 'user', 'content': content})
else:
raise NotImplementedError(
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 231da53a4bc..e4fe6f11616 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -26,6 +26,7 @@
"react": "^18.2.0",
"react-accessible-treeview": "^2.8.3",
"react-dom": "^18.2.0",
+ "react-highlight": "^0.15.0",
"react-hot-toast": "^2.4.1",
"react-i18next": "^14.1.0",
"react-icons": "^5.0.1",
@@ -44,6 +45,7 @@
"@types/node": "^18.0.0 ",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
+ "@types/react-highlight": "^0.12.8",
"@types/react-syntax-highlighter": "^15.5.11",
"@typescript-eslint/eslint-plugin": "^7.4.0",
"@typescript-eslint/parser": "^7.0.0",
@@ -4878,6 +4880,15 @@
"@types/react": "*"
}
},
+ "node_modules/@types/react-highlight": {
+ "version": "0.12.8",
+ "resolved": "https://registry.npmjs.org/@types/react-highlight/-/react-highlight-0.12.8.tgz",
+ "integrity": "sha512-V7O7zwXUw8WSPd//YUO8sz489J/EeobJljASGhP0rClrvq+1Y1qWEpToGu+Pp7YuChxhAXSgkLkrOYpZX5A62g==",
+ "dev": true,
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
"node_modules/@types/react-syntax-highlighter": {
"version": "15.5.11",
"resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.11.tgz",
@@ -12799,6 +12810,14 @@
"react": "^18.2.0"
}
},
+ "node_modules/react-highlight": {
+ "version": "0.15.0",
+ "resolved": "https://registry.npmjs.org/react-highlight/-/react-highlight-0.15.0.tgz",
+ "integrity": "sha512-5uV/b/N4Z421GSVVe05fz+OfTsJtFzx/fJBdafZyw4LS70XjIZwgEx3Lrkfc01W/RzZ2Dtfb0DApoaJFAIKBtA==",
+ "dependencies": {
+ "highlight.js": "^10.5.0"
+ }
+ },
"node_modules/react-hot-toast": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 154310d5a80..56f2d04f155 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -25,6 +25,7 @@
"react": "^18.2.0",
"react-accessible-treeview": "^2.8.3",
"react-dom": "^18.2.0",
+ "react-highlight": "^0.15.0",
"react-hot-toast": "^2.4.1",
"react-i18next": "^14.1.0",
"react-icons": "^5.0.1",
@@ -64,6 +65,7 @@
"@types/node": "^18.0.0 ",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
+ "@types/react-highlight": "^0.12.8",
"@types/react-syntax-highlighter": "^15.5.11",
"@typescript-eslint/eslint-plugin": "^7.4.0",
"@typescript-eslint/parser": "^7.0.0",
diff --git a/frontend/src/components/Jupyter.tsx b/frontend/src/components/Jupyter.tsx
new file mode 100644
index 00000000000..22b27e80c02
--- /dev/null
+++ b/frontend/src/components/Jupyter.tsx
@@ -0,0 +1,77 @@
+import React from "react";
+import { useSelector } from "react-redux";
+import SyntaxHighlighter from "react-syntax-highlighter";
+import Markdown from "react-markdown";
+import { atomOneDark } from "react-syntax-highlighter/dist/esm/styles/hljs";
+import { RootState } from "#/store";
+import { Cell } from "#/state/jupyterSlice";
+
+interface IJupyterCell {
+ cell: Cell;
+}
+
+function JupyterCell({ cell }: IJupyterCell): JSX.Element {
+ const code = cell.content;
+
+ if (cell.type === "input") {
+ return (
+
+
EXECUTE
+
+
+ {code}
+
+
+
+ );
+ }
+ return (
+
+
STDOUT/STDERR
+
+ {/* split code by newline and render each line as a plaintext, except it starts with `![image]` so we render it as markdown */}
+ {code.split("\n").map((line, index) => {
+ if (line.startsWith("![image](data:image/png;base64,")) {
+ // add new line before and after the image
+ return (
+
+ value}>
+ {line}
+
+
+
+ );
+ }
+ return (
+
+
+ {line}
+
+
+
+ );
+ })}
+
+
+ );
+}
+
+function Jupyter(): JSX.Element {
+ const { cells } = useSelector((state: RootState) => state.jupyter);
+
+ return (
+
+ {cells.map((cell, index) => (
+
+ ))}
+
+ );
+}
+
+export default Jupyter;
diff --git a/frontend/src/components/Workspace.tsx b/frontend/src/components/Workspace.tsx
index 7d12b64d0f2..b291e18c0bc 100644
--- a/frontend/src/components/Workspace.tsx
+++ b/frontend/src/components/Workspace.tsx
@@ -12,6 +12,7 @@ import { AllTabs, TabOption, TabType } from "#/types/TabOption";
import Browser from "./Browser";
import CodeEditor from "./CodeEditor";
import Planner from "./Planner";
+import Jupyter from "./Jupyter";
function Workspace() {
const { t } = useTranslation();
@@ -20,12 +21,13 @@ function Workspace() {
const screenshotSrc = useSelector(
(state: RootState) => state.browser.screenshotSrc,
);
-
+ const jupyterCells = useSelector((state: RootState) => state.jupyter.cells);
const [activeTab, setActiveTab] = useState(TabOption.CODE);
const [changes, setChanges] = useState>({
[TabOption.PLANNER]: false,
[TabOption.CODE]: false,
[TabOption.BROWSER]: false,
+ [TabOption.JUPYTER]: false,
});
const tabData = useMemo(
@@ -45,6 +47,11 @@ function Workspace() {
icon: ,
component: ,
},
+ [TabOption.JUPYTER]: {
+ name: t(I18nKey.WORKSPACE$JUPYTER_TAB_LABEL),
+ icon: ,
+ component: ,
+ },
}),
[t],
);
@@ -73,6 +80,14 @@ function Workspace() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [screenshotSrc]);
+ useEffect(() => {
+ if (activeTab !== TabOption.JUPYTER && jupyterCells.length > 0) {
+ // FIXME: This is a temporary solution to show the jupyter tab when the first cell is added
+ // Only need to show the tab only when a cell is added
+ setChanges((prev) => ({ ...prev, [TabOption.JUPYTER]: true }));
+ }
+ }, [jupyterCells]);
+
return (
{
getPlan().then((fetchedPlan) => store.dispatch(setPlan(fetchedPlan)));
diff --git a/frontend/src/services/observations.ts b/frontend/src/services/observations.ts
index c5a5cbab7bb..956fc906697 100644
--- a/frontend/src/services/observations.ts
+++ b/frontend/src/services/observations.ts
@@ -3,6 +3,7 @@ import { setUrl, setScreenshotSrc } from "#/state/browserSlice";
import store from "#/store";
import { ObservationMessage } from "#/types/Message";
import { appendOutput } from "#/state/commandSlice";
+import { appendJupyterOutput } from "#/state/jupyterSlice";
import ObservationType from "#/types/ObservationType";
export function handleObservationMessage(message: ObservationMessage) {
@@ -12,7 +13,7 @@ export function handleObservationMessage(message: ObservationMessage) {
break;
case ObservationType.RUN_IPYTHON:
// FIXME: render this as markdown
- store.dispatch(appendOutput(message.content));
+ store.dispatch(appendJupyterOutput(message.content));
break;
case ObservationType.BROWSE:
if (message.extras?.screenshot) {
diff --git a/frontend/src/state/jupyterSlice.ts b/frontend/src/state/jupyterSlice.ts
new file mode 100644
index 00000000000..241a8df6f95
--- /dev/null
+++ b/frontend/src/state/jupyterSlice.ts
@@ -0,0 +1,27 @@
+import { createSlice } from "@reduxjs/toolkit";
+
+export type Cell = {
+ content: string;
+ type: "input" | "output";
+};
+
+const initialCells: Cell[] = [];
+
+export const cellSlice = createSlice({
+ name: "cell",
+ initialState: {
+ cells: initialCells,
+ },
+ reducers: {
+ appendJupyterInput: (state, action) => {
+ state.cells.push({ content: action.payload, type: "input" });
+ },
+ appendJupyterOutput: (state, action) => {
+ state.cells.push({ content: action.payload, type: "output" });
+ },
+ },
+});
+
+export const { appendJupyterInput, appendJupyterOutput } = cellSlice.actions;
+
+export default cellSlice.reducer;
diff --git a/frontend/src/store.ts b/frontend/src/store.ts
index 05ea2fe509a..f675f929ec5 100644
--- a/frontend/src/store.ts
+++ b/frontend/src/store.ts
@@ -7,6 +7,7 @@ import commandReducer from "./state/commandSlice";
import errorsReducer from "./state/errorsSlice";
import planReducer from "./state/planSlice";
import taskReducer from "./state/taskSlice";
+import jupyterReducer from "./state/jupyterSlice";
export const rootReducer = combineReducers({
browser: browserReducer,
@@ -17,6 +18,7 @@ export const rootReducer = combineReducers({
errors: errorsReducer,
plan: planReducer,
agent: agentReducer,
+ jupyter: jupyterReducer,
});
const store = configureStore({
diff --git a/frontend/src/types/TabOption.tsx b/frontend/src/types/TabOption.tsx
index b114c0da285..a732acb2c74 100644
--- a/frontend/src/types/TabOption.tsx
+++ b/frontend/src/types/TabOption.tsx
@@ -2,10 +2,20 @@ enum TabOption {
PLANNER = "planner",
CODE = "code",
BROWSER = "browser",
+ JUPYTER = "jupyter",
}
-type TabType = TabOption.PLANNER | TabOption.CODE | TabOption.BROWSER;
+type TabType =
+ | TabOption.PLANNER
+ | TabOption.CODE
+ | TabOption.BROWSER
+ | TabOption.JUPYTER;
-const AllTabs = [TabOption.CODE, TabOption.BROWSER, TabOption.PLANNER];
+const AllTabs = [
+ TabOption.CODE,
+ TabOption.BROWSER,
+ TabOption.PLANNER,
+ TabOption.JUPYTER,
+];
export { AllTabs, TabOption, type TabType };
diff --git a/opendevin/sandbox/plugins/jupyter/execute_server b/opendevin/sandbox/plugins/jupyter/execute_server
index 828ac520e8b..c7212e907a0 100755
--- a/opendevin/sandbox/plugins/jupyter/execute_server
+++ b/opendevin/sandbox/plugins/jupyter/execute_server
@@ -187,8 +187,7 @@ class JupyterKernel:
outputs.append(msg['content']['data']['text/plain'])
if 'image/png' in msg['content']['data']:
# use markdone to display image (in case of large image)
- # outputs.append(f"\n
\n")
- outputs.append(f"![image](data:image/png;base64,{msg['content']['data']['image/png']})")
+ outputs.append(f"\n![image](data:image/png;base64,{msg['content']['data']['image/png']})\n")
elif msg_type == 'execute_reply':
execution_done = True