-
-
- onChange('showThoughtInProgress')(e.target.checked)
- }
- />
-
- Expand though process by default for generating message
-
+
+ {/* Left panel, showing sections - Desktop version */}
+
+ {SETTING_SECTIONS.map((section, idx) => (
+
setSectionIdx(idx)}
+ dir="auto"
+ >
+ {section.title}
-
-
- onChange('excludeThoughtOnReq')(e.target.checked)
- }
- />
-
- Exclude thought process when sending request to API
- (Recommended for DeepSeek-R1)
-
-
-
-
+ ))}
+
-
-
- Advanced config
-
-
- {/* this button only shows in dev mode, used to import a demo conversation to test message rendering */}
- {isDev && (
-
-
- (debug) Import demo conversation
-
-
- )}
-
-
- onChange('showTokensPerSecond')(e.target.checked)
- }
- />
- Show tokens per second
-
-
-
-
+ {section.title}
+
+ ))}
+
+
+
+
+ {/* Right panel, showing setting fields */}
+
+ {SETTING_SECTIONS[sectionIdx].fields.map((field, idx) => {
+ const key = `${sectionIdx}-${idx}`;
+ if (field.type === SettingInputType.SHORT_INPUT) {
+ return (
+
+ );
+ } else if (field.type === SettingInputType.LONG_INPUT) {
+ return (
+
+ );
+ } else if (field.type === SettingInputType.CHECKBOX) {
+ return (
+
+ );
+ } else if (field.type === SettingInputType.CUSTOM) {
+ return (
+
+ {typeof field.component === 'string'
+ ? field.component
+ : field.component({
+ value: localConfig[field.key],
+ onChange: onChange(field.key),
+ })}
+
+ );
+ }
+ })}
+
+
+ Settings are saved in browser's localStorage
+
+
@@ -285,37 +440,97 @@ export default function SettingDialog({
);
}
-function SettingsModalShortInput({
+function SettingsModalLongInput({
configKey,
- configDefault,
value,
onChange,
label,
}: {
configKey: SettKey;
- configDefault: typeof CONFIG_DEFAULT;
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- value: any;
+ value: string;
onChange: (value: string) => void;
label?: string;
}) {
return (
-
-
-
- {label || configKey}
-
-
- {CONFIG_INFO[configKey] ?? '(no help message available)'}
-
-
-
+ {label || configKey}
+
);
}
+
+function SettingsModalShortInput({
+ configKey,
+ value,
+ onChange,
+ label,
+}: {
+ configKey: SettKey;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ value: any;
+ onChange: (value: string) => void;
+ label?: string;
+}) {
+ const helpMsg = CONFIG_INFO[configKey];
+
+ return (
+ <>
+ {/* on mobile, we simply show the help message here */}
+ {helpMsg && (
+
+
{label || configKey}
+
+
{helpMsg}
+
+ )}
+
+
+
+ {label || configKey}
+
+ {helpMsg && (
+
+ {helpMsg}
+
+ )}
+
+ onChange(e.target.value)}
+ />
+
+ >
+ );
+}
+
+function SettingsModalCheckbox({
+ configKey,
+ value,
+ onChange,
+ label,
+}: {
+ configKey: SettKey;
+ value: boolean;
+ onChange: (value: boolean) => void;
+ label: string;
+}) {
+ return (
+
+ onChange(e.target.checked)}
+ />
+ {label || configKey}
+
+ );
+}
diff --git a/examples/server/webui/src/index.scss b/examples/server/webui/src/index.scss
index 9253b6a02..314b68232 100644
--- a/examples/server/webui/src/index.scss
+++ b/examples/server/webui/src/index.scss
@@ -45,6 +45,9 @@
/* Highlight.js */
[data-color-scheme='light'] {
@include meta.load-css('highlight.js/styles/stackoverflow-light');
+ .dark-color {
+ @apply bg-base-content text-base-100;
+ }
}
[data-color-scheme='dark'] {
@include meta.load-css('highlight.js/styles/stackoverflow-dark');
@@ -52,6 +55,9 @@
[data-color-scheme='auto'] {
@media (prefers-color-scheme: light) {
@include meta.load-css('highlight.js/styles/stackoverflow-light');
+ .dark-color {
+ @apply bg-base-content text-base-100;
+ }
}
@media (prefers-color-scheme: dark) {
@include meta.load-css('highlight.js/styles/stackoverflow-dark');
diff --git a/examples/server/webui/src/utils/app.context.tsx b/examples/server/webui/src/utils/app.context.tsx
index d50f825c3..af6bd885f 100644
--- a/examples/server/webui/src/utils/app.context.tsx
+++ b/examples/server/webui/src/utils/app.context.tsx
@@ -1,5 +1,11 @@
import React, { createContext, useContext, useEffect, useState } from 'react';
-import { APIMessage, Conversation, Message, PendingMessage } from './types';
+import {
+ APIMessage,
+ CanvasData,
+ Conversation,
+ Message,
+ PendingMessage,
+} from './types';
import StorageUtils from './storage';
import {
filterThoughtFromMsgs,
@@ -10,6 +16,7 @@ import { BASE_URL, CONFIG_DEFAULT, isDev } from '../Config';
import { matchPath, useLocation } from 'react-router';
interface AppContextValue {
+ // conversations and messages
viewingConversation: Conversation | null;
pendingMessages: Record
;
isGenerating: (convId: string) => boolean;
@@ -26,8 +33,15 @@ interface AppContextValue {
onChunk?: CallbackGeneratedChunk
) => Promise;
+ // canvas
+ canvasData: CanvasData | null;
+ setCanvasData: (data: CanvasData | null) => void;
+
+ // config
config: typeof CONFIG_DEFAULT;
saveConfig: (config: typeof CONFIG_DEFAULT) => void;
+ showSettings: boolean;
+ setShowSettings: (show: boolean) => void;
}
// for now, this callback is only used for scrolling to the bottom of the chat
@@ -54,8 +68,13 @@ export const AppContextProvider = ({
Record
>({});
const [config, setConfig] = useState(StorageUtils.getConfig());
+ const [canvasData, setCanvasData] = useState(null);
+ const [showSettings, setShowSettings] = useState(false);
+ // handle change when the convId from URL is changed
useEffect(() => {
+ // also reset the canvas data
+ setCanvasData(null);
const handleConversationChange = (changedConvId: string) => {
if (changedConvId !== convId) return;
setViewingConversation(StorageUtils.getOneConversation(convId));
@@ -292,8 +311,12 @@ export const AppContextProvider = ({
sendMessage,
stopGenerating,
replaceMessageAndGenerate,
+ canvasData,
+ setCanvasData,
config,
saveConfig,
+ showSettings,
+ setShowSettings,
}}
>
{children}
diff --git a/examples/server/webui/src/utils/common.tsx b/examples/server/webui/src/utils/common.tsx
new file mode 100644
index 000000000..09b08b5c9
--- /dev/null
+++ b/examples/server/webui/src/utils/common.tsx
@@ -0,0 +1,38 @@
+export const XCloseButton: React.ElementType<
+ React.ClassAttributes &
+ React.HTMLAttributes
+> = ({ className, ...props }) => (
+
+
+
+
+
+);
+
+export const OpenInNewTab = ({
+ href,
+ children,
+}: {
+ href: string;
+ children: string;
+}) => (
+
+ {children}
+
+);
diff --git a/examples/server/webui/src/utils/misc.ts b/examples/server/webui/src/utils/misc.ts
index 0c887ee83..e3153fff5 100644
--- a/examples/server/webui/src/utils/misc.ts
+++ b/examples/server/webui/src/utils/misc.ts
@@ -85,3 +85,6 @@ export function classNames(classes: Record): string {
.map(([key, _]) => key)
.join(' ');
}
+
+export const delay = (ms: number) =>
+ new Promise((resolve) => setTimeout(resolve, ms));
diff --git a/examples/server/webui/src/utils/types.ts b/examples/server/webui/src/utils/types.ts
index d04c6e3dc..7cd12b40a 100644
--- a/examples/server/webui/src/utils/types.ts
+++ b/examples/server/webui/src/utils/types.ts
@@ -23,3 +23,14 @@ export interface Conversation {
export type PendingMessage = Omit & {
content: string | null;
};
+
+export enum CanvasType {
+ PY_INTERPRETER,
+}
+
+export interface CanvasPyInterpreter {
+ type: CanvasType.PY_INTERPRETER;
+ content: string;
+}
+
+export type CanvasData = CanvasPyInterpreter;
diff --git a/examples/server/webui/vite.config.ts b/examples/server/webui/vite.config.ts
index 184fdc6b9..b8a0f03d9 100644
--- a/examples/server/webui/vite.config.ts
+++ b/examples/server/webui/vite.config.ts
@@ -72,5 +72,9 @@ export default defineConfig({
proxy: {
'/v1': 'http://localhost:8080',
},
+ headers: {
+ 'Cross-Origin-Embedder-Policy': 'require-corp',
+ 'Cross-Origin-Opener-Policy': 'same-origin',
+ },
},
});