Skip to content

Commit 483a3bc

Browse files
committed
add python code interpreter
1 parent 422e53e commit 483a3bc

File tree

12 files changed

+406
-85
lines changed

12 files changed

+406
-85
lines changed

examples/server/webui/src/App.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { HashRouter, Outlet, Route, Routes } from 'react-router';
22
import Header from './components/Header';
33
import Sidebar from './components/Sidebar';
4-
import { AppContextProvider } from './utils/app.context';
4+
import { AppContextProvider, useAppContext } from './utils/app.context';
55
import ChatScreen from './components/ChatScreen';
6+
import SettingDialog from './components/SettingDialog';
67

78
function App() {
89
return (
@@ -22,13 +23,18 @@ function App() {
2223
}
2324

2425
function AppLayout() {
26+
const { showSettings, setShowSettings } = useAppContext();
2527
return (
2628
<>
2729
<Sidebar />
28-
<div className="chat-screen drawer-content grow flex flex-col h-screen w-screen mx-auto px-4">
30+
<div className="drawer-content grow flex flex-col h-screen w-screen mx-auto px-4 overflow-auto">
2931
<Header />
3032
<Outlet />
3133
</div>
34+
{<SettingDialog
35+
show={showSettings}
36+
onClose={() => setShowSettings(false)}
37+
/>}
3238
</>
3339
);
3440
}

examples/server/webui/src/Config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export const BASE_URL = new URL('.', document.baseURI).href
1010

1111
export const CONFIG_DEFAULT = {
1212
// Note: in order not to introduce breaking changes, please keep the same data type (number, string, etc) if you want to change the default value. Do not use null or undefined for default value.
13+
// Do not use nested objects, keep it single level. Prefix the key if you need to group them.
1314
apiKey: '',
1415
systemMessage: 'You are a helpful assistant.',
1516
showTokensPerSecond: false,
@@ -36,6 +37,8 @@ export const CONFIG_DEFAULT = {
3637
dry_penalty_last_n: -1,
3738
max_tokens: -1,
3839
custom: '', // custom json-stringified object
40+
// experimental features
41+
pyIntepreterEnabled: false,
3942
};
4043
export const CONFIG_INFO: Record<string, string> = {
4144
apiKey: 'Set the API Key if you are using --api-key option for the server.',
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { useEffect, useState } from 'react';
2+
import { useAppContext } from '../utils/app.context';
3+
import { XCloseButton } from '../utils/common';
4+
import { delay } from '../utils/misc';
5+
import StorageUtils from '../utils/storage';
6+
import { CanvasType } from '../utils/types';
7+
import { PlayIcon } from '@heroicons/react/24/outline';
8+
9+
const PyodideWrapper = {
10+
load: async function () {
11+
// load pyodide from CDN
12+
// @ts-expect-error experimental pyodide
13+
if (window.addedScriptPyodide) return;
14+
// @ts-expect-error experimental pyodide
15+
window.addedScriptPyodide = true;
16+
const scriptElem = document.createElement('script');
17+
scriptElem.src = 'https://cdn.jsdelivr.net/pyodide/v0.27.2/full/pyodide.js';
18+
document.body.appendChild(scriptElem);
19+
},
20+
21+
run: async function (code: string) {
22+
PyodideWrapper.load();
23+
24+
// wait for pyodide to be loaded
25+
// @ts-expect-error experimental pyodide
26+
while (!window.loadPyodide) {
27+
await delay(100);
28+
}
29+
const stdOutAndErr: string[] = [];
30+
// @ts-expect-error experimental pyodide
31+
const pyodide = await window.loadPyodide({
32+
stdout: (data: string) => stdOutAndErr.push(data),
33+
stderr: (data: string) => stdOutAndErr.push(data),
34+
});
35+
const result = await pyodide.runPythonAsync(code);
36+
if (result) {
37+
stdOutAndErr.push(result.toString());
38+
}
39+
return stdOutAndErr.join('');
40+
},
41+
};
42+
43+
if (StorageUtils.getConfig().pyIntepreterEnabled) {
44+
PyodideWrapper.load();
45+
}
46+
47+
export default function CanvasPyInterpreter() {
48+
const { canvasData, setCanvasData } = useAppContext();
49+
50+
const [running, setRunning] = useState(false);
51+
const [output, setOutput] = useState('');
52+
53+
const runCode = async () => {
54+
const code = canvasData?.content;
55+
if (!code) return;
56+
setRunning(true);
57+
setOutput('Running...');
58+
const out = await PyodideWrapper.run(code);
59+
setOutput(out);
60+
setRunning(false);
61+
};
62+
63+
// run code on mount
64+
useEffect(() => {
65+
runCode();
66+
// eslint-disable-next-line react-hooks/exhaustive-deps
67+
}, []);
68+
69+
if (canvasData?.type !== CanvasType.PY_INTERPRETER) {
70+
return null;
71+
}
72+
73+
return (
74+
<div className="card bg-base-200 w-full h-full shadow-xl">
75+
<div className="card-body">
76+
<div className="flex justify-between items-center mb-4">
77+
<span className="text-lg font-bold">Pyodide</span>
78+
<XCloseButton
79+
className="bg-base-100"
80+
onClick={() => setCanvasData(null)}
81+
/>
82+
</div>
83+
<div className="grid grid-rows-3 gap-4 h-full">
84+
<textarea
85+
className="textarea textarea-bordered w-full h-full font-mono"
86+
value={canvasData.content}
87+
onChange={(e) =>
88+
setCanvasData({
89+
...canvasData,
90+
content: e.target.value,
91+
})
92+
}
93+
></textarea>
94+
<div className="font-mono flex flex-col row-span-2">
95+
<div className="flex items-center mb-2">
96+
<button
97+
className="btn btn-sm bg-base-100"
98+
onClick={runCode}
99+
disabled={running}
100+
>
101+
<PlayIcon className="h-6 w-6" />{' '}
102+
{running ? 'Running...' : 'Run'}
103+
</button>
104+
</div>
105+
<pre className="bg-slate-900 rounded-md grow text-gray-200 p-3">
106+
{output}
107+
</pre>
108+
</div>
109+
</div>
110+
</div>
111+
</div>
112+
);
113+
}

examples/server/webui/src/components/ChatMessage.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,11 +149,17 @@ export default function ChatMessage({
149149
)}
150150
</summary>
151151
<div className="collapse-content">
152-
<MarkdownDisplay content={thought} />
152+
<MarkdownDisplay
153+
content={thought}
154+
isGenerating={isPending}
155+
/>
153156
</div>
154157
</details>
155158
)}
156-
<MarkdownDisplay content={content} />
159+
<MarkdownDisplay
160+
content={content}
161+
isGenerating={isPending}
162+
/>
157163
</div>
158164
</>
159165
)}

examples/server/webui/src/components/ChatScreen.tsx

Lines changed: 77 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import StorageUtils from '../utils/storage';
44
import { useNavigate } from 'react-router';
55
import ChatMessage from './ChatMessage';
66
import { PendingMessage } from '../utils/types';
7+
import { classNames } from '../utils/misc';
8+
import CanvasPyInterpreter from './CanvasPyInterpreter';
79

810
export default function ChatScreen() {
911
const {
@@ -12,6 +14,7 @@ export default function ChatScreen() {
1214
isGenerating,
1315
stopGenerating,
1416
pendingMessages,
17+
canvasData,
1518
} = useAppContext();
1619
const [inputMsg, setInputMsg] = useState('');
1720
const containerRef = useRef<HTMLDivElement>(null);
@@ -59,65 +62,82 @@ export default function ChatScreen() {
5962
};
6063

6164
return (
62-
<>
63-
{/* chat messages */}
64-
<div
65-
id="messages-list"
66-
className="flex flex-col grow overflow-y-auto"
67-
ref={containerRef}
68-
>
69-
<div className="mt-auto flex justify-center">
70-
{/* placeholder to shift the message to the bottom */}
71-
{viewingConversation ? '' : 'Send a message to start'}
72-
</div>
73-
{viewingConversation?.messages.map((msg) => (
74-
<ChatMessage key={msg.id} msg={msg} scrollToBottom={scrollToBottom} />
75-
))}
65+
<div
66+
className={classNames({
67+
'grid gap-8 grow': true,
68+
'grid-cols-2': !!canvasData,
69+
'grid-cols-1': !canvasData,
70+
})}
71+
>
72+
<div className="flex flex-col w-full max-w-[900px] mx-auto">
73+
{/* chat messages */}
74+
<div
75+
id="messages-list"
76+
className="flex flex-col grow overflow-y-auto"
77+
ref={containerRef}
78+
>
79+
<div className="mt-auto flex justify-center">
80+
{/* placeholder to shift the message to the bottom */}
81+
{viewingConversation ? '' : 'Send a message to start'}
82+
</div>
83+
{viewingConversation?.messages.map((msg) => (
84+
<ChatMessage
85+
key={msg.id}
86+
msg={msg}
87+
scrollToBottom={scrollToBottom}
88+
/>
89+
))}
7690

77-
{pendingMsg && (
78-
<ChatMessage
79-
msg={pendingMsg}
80-
scrollToBottom={scrollToBottom}
81-
isPending
82-
id="pending-msg"
83-
/>
84-
)}
85-
</div>
91+
{pendingMsg && (
92+
<ChatMessage
93+
msg={pendingMsg}
94+
scrollToBottom={scrollToBottom}
95+
isPending
96+
id="pending-msg"
97+
/>
98+
)}
99+
</div>
86100

87-
{/* chat input */}
88-
<div className="flex flex-row items-center mt-8 mb-6">
89-
<textarea
90-
className="textarea textarea-bordered w-full"
91-
placeholder="Type a message (Shift+Enter to add a new line)"
92-
value={inputMsg}
93-
onChange={(e) => setInputMsg(e.target.value)}
94-
onKeyDown={(e) => {
95-
if (e.key === 'Enter' && e.shiftKey) return;
96-
if (e.key === 'Enter' && !e.shiftKey) {
97-
e.preventDefault();
98-
sendNewMessage();
99-
}
100-
}}
101-
id="msg-input"
102-
dir="auto"
103-
></textarea>
104-
{isGenerating(currConvId) ? (
105-
<button
106-
className="btn btn-neutral ml-2"
107-
onClick={() => stopGenerating(currConvId)}
108-
>
109-
Stop
110-
</button>
111-
) : (
112-
<button
113-
className="btn btn-primary ml-2"
114-
onClick={sendNewMessage}
115-
disabled={inputMsg.trim().length === 0}
116-
>
117-
Send
118-
</button>
119-
)}
101+
{/* chat input */}
102+
<div className="flex flex-row items-center pt-8 pb-6 sticky bottom-0 bg-base-100">
103+
<textarea
104+
className="textarea textarea-bordered w-full"
105+
placeholder="Type a message (Shift+Enter to add a new line)"
106+
value={inputMsg}
107+
onChange={(e) => setInputMsg(e.target.value)}
108+
onKeyDown={(e) => {
109+
if (e.key === 'Enter' && e.shiftKey) return;
110+
if (e.key === 'Enter' && !e.shiftKey) {
111+
e.preventDefault();
112+
sendNewMessage();
113+
}
114+
}}
115+
id="msg-input"
116+
dir="auto"
117+
></textarea>
118+
{isGenerating(currConvId) ? (
119+
<button
120+
className="btn btn-neutral ml-2"
121+
onClick={() => stopGenerating(currConvId)}
122+
>
123+
Stop
124+
</button>
125+
) : (
126+
<button
127+
className="btn btn-primary ml-2"
128+
onClick={sendNewMessage}
129+
disabled={inputMsg.trim().length === 0}
130+
>
131+
Send
132+
</button>
133+
)}
134+
</div>
120135
</div>
121-
</>
136+
{canvasData && (
137+
<div className="w-full sticky top-[8em] h-[calc(100vh-9em)]">
138+
<CanvasPyInterpreter />
139+
</div>
140+
)}
141+
</div>
122142
);
123143
}

examples/server/webui/src/components/Header.tsx

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@ import { classNames } from '../utils/misc';
55
import daisyuiThemes from 'daisyui/src/theming/themes';
66
import { THEMES } from '../Config';
77
import { useNavigate } from 'react-router';
8-
import SettingDialog from './SettingDialog';
98

109
export default function Header() {
1110
const navigate = useNavigate();
1211
const [selectedTheme, setSelectedTheme] = useState(StorageUtils.getTheme());
13-
const [showSettingDialog, setShowSettingDialog] = useState(false);
12+
const { setShowSettings } = useAppContext();
1413

1514
const setTheme = (theme: string) => {
1615
StorageUtils.setTheme(theme);
@@ -54,7 +53,7 @@ export default function Header() {
5453
};
5554

5655
return (
57-
<div className="flex flex-row items-center mt-6 mb-6">
56+
<div className="flex flex-row items-center pt-6 pb-6 sticky top-0 z-10 bg-base-100">
5857
{/* open sidebar button */}
5958
<label htmlFor="toggle-drawer" className="btn btn-ghost lg:hidden">
6059
<svg
@@ -109,7 +108,7 @@ export default function Header() {
109108
</ul>
110109
</div>
111110
<div className="tooltip tooltip-bottom" data-tip="Settings">
112-
<button className="btn" onClick={() => setShowSettingDialog(true)}>
111+
<button className="btn" onClick={() => setShowSettings(true)}>
113112
{/* settings button */}
114113
<svg
115114
xmlns="http://www.w3.org/2000/svg"
@@ -172,11 +171,6 @@ export default function Header() {
172171
</div>
173172
</div>
174173
</div>
175-
176-
<SettingDialog
177-
show={showSettingDialog}
178-
onClose={() => setShowSettingDialog(false)}
179-
/>
180174
</div>
181175
);
182176
}

0 commit comments

Comments
 (0)