Skip to content

Commit c7e0a20

Browse files
igardevigardevngxson
authored
webui : Replace alert and confirm with custom modals. (#13711)
* Replace alert and confirm with custom modals. This is needed as Webview in VS Code doesn't permit alert and confirm for security reasons. * use Modal Provider to simplify the use of confirm and alert modals. * Increase the z index of the modal dialogs. * Update index.html.gz * also add showPrompt * rebuild --------- Co-authored-by: igardev <ivailo.gardev@akros.ch> Co-authored-by: Xuan Son Nguyen <son@huggingface.co>
1 parent 3f55f78 commit c7e0a20

File tree

5 files changed

+180
-22
lines changed

5 files changed

+180
-22
lines changed

tools/server/public/index.html.gz

676 Bytes
Binary file not shown.

tools/server/webui/src/App.tsx

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,24 @@ import { AppContextProvider, useAppContext } from './utils/app.context';
55
import ChatScreen from './components/ChatScreen';
66
import SettingDialog from './components/SettingDialog';
77
import { Toaster } from 'react-hot-toast';
8+
import { ModalProvider } from './components/ModalProvider';
89

910
function App() {
1011
return (
11-
<HashRouter>
12-
<div className="flex flex-row drawer lg:drawer-open">
13-
<AppContextProvider>
14-
<Routes>
15-
<Route element={<AppLayout />}>
16-
<Route path="/chat/:convId" element={<ChatScreen />} />
17-
<Route path="*" element={<ChatScreen />} />
18-
</Route>
19-
</Routes>
20-
</AppContextProvider>
21-
</div>
22-
</HashRouter>
12+
<ModalProvider>
13+
<HashRouter>
14+
<div className="flex flex-row drawer lg:drawer-open">
15+
<AppContextProvider>
16+
<Routes>
17+
<Route element={<AppLayout />}>
18+
<Route path="/chat/:convId" element={<ChatScreen />} />
19+
<Route path="*" element={<ChatScreen />} />
20+
</Route>
21+
</Routes>
22+
</AppContextProvider>
23+
</div>
24+
</HashRouter>
25+
</ModalProvider>
2326
);
2427
}
2528

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import React, { createContext, useState, useContext } from 'react';
2+
3+
type ModalContextType = {
4+
showConfirm: (message: string) => Promise<boolean>;
5+
showPrompt: (
6+
message: string,
7+
defaultValue?: string
8+
) => Promise<string | undefined>;
9+
showAlert: (message: string) => Promise<void>;
10+
};
11+
const ModalContext = createContext<ModalContextType>(null!);
12+
13+
interface ModalState<T> {
14+
isOpen: boolean;
15+
message: string;
16+
defaultValue?: string;
17+
resolve: ((value: T) => void) | null;
18+
}
19+
20+
export function ModalProvider({ children }: { children: React.ReactNode }) {
21+
const [confirmState, setConfirmState] = useState<ModalState<boolean>>({
22+
isOpen: false,
23+
message: '',
24+
resolve: null,
25+
});
26+
const [promptState, setPromptState] = useState<
27+
ModalState<string | undefined>
28+
>({ isOpen: false, message: '', resolve: null });
29+
const [alertState, setAlertState] = useState<ModalState<void>>({
30+
isOpen: false,
31+
message: '',
32+
resolve: null,
33+
});
34+
const inputRef = React.useRef<HTMLInputElement>(null);
35+
36+
const showConfirm = (message: string): Promise<boolean> => {
37+
return new Promise((resolve) => {
38+
setConfirmState({ isOpen: true, message, resolve });
39+
});
40+
};
41+
42+
const showPrompt = (
43+
message: string,
44+
defaultValue?: string
45+
): Promise<string | undefined> => {
46+
return new Promise((resolve) => {
47+
setPromptState({ isOpen: true, message, defaultValue, resolve });
48+
});
49+
};
50+
51+
const showAlert = (message: string): Promise<void> => {
52+
return new Promise((resolve) => {
53+
setAlertState({ isOpen: true, message, resolve });
54+
});
55+
};
56+
57+
const handleConfirm = (result: boolean) => {
58+
confirmState.resolve?.(result);
59+
setConfirmState({ isOpen: false, message: '', resolve: null });
60+
};
61+
62+
const handlePrompt = (result?: string) => {
63+
promptState.resolve?.(result);
64+
setPromptState({ isOpen: false, message: '', resolve: null });
65+
};
66+
67+
const handleAlertClose = () => {
68+
alertState.resolve?.();
69+
setAlertState({ isOpen: false, message: '', resolve: null });
70+
};
71+
72+
return (
73+
<ModalContext.Provider value={{ showConfirm, showPrompt, showAlert }}>
74+
{children}
75+
76+
{/* Confirm Modal */}
77+
{confirmState.isOpen && (
78+
<dialog className="modal modal-open z-[1100]">
79+
<div className="modal-box">
80+
<h3 className="font-bold text-lg">{confirmState.message}</h3>
81+
<div className="modal-action">
82+
<button
83+
className="btn btn-ghost"
84+
onClick={() => handleConfirm(false)}
85+
>
86+
Cancel
87+
</button>
88+
<button
89+
className="btn btn-error"
90+
onClick={() => handleConfirm(true)}
91+
>
92+
Confirm
93+
</button>
94+
</div>
95+
</div>
96+
</dialog>
97+
)}
98+
99+
{/* Prompt Modal */}
100+
{promptState.isOpen && (
101+
<dialog className="modal modal-open z-[1100]">
102+
<div className="modal-box">
103+
<h3 className="font-bold text-lg">{promptState.message}</h3>
104+
<input
105+
type="text"
106+
className="input input-bordered w-full mt-2"
107+
defaultValue={promptState.defaultValue}
108+
ref={inputRef}
109+
onKeyDown={(e) => {
110+
if (e.key === 'Enter') {
111+
handlePrompt((e.target as HTMLInputElement).value);
112+
}
113+
}}
114+
/>
115+
<div className="modal-action">
116+
<button className="btn btn-ghost" onClick={() => handlePrompt()}>
117+
Cancel
118+
</button>
119+
<button
120+
className="btn btn-primary"
121+
onClick={() => handlePrompt(inputRef.current?.value)}
122+
>
123+
Submit
124+
</button>
125+
</div>
126+
</div>
127+
</dialog>
128+
)}
129+
130+
{/* Alert Modal */}
131+
{alertState.isOpen && (
132+
<dialog className="modal modal-open z-[1100]">
133+
<div className="modal-box">
134+
<h3 className="font-bold text-lg">{alertState.message}</h3>
135+
<div className="modal-action">
136+
<button className="btn" onClick={handleAlertClose}>
137+
OK
138+
</button>
139+
</div>
140+
</div>
141+
</dialog>
142+
)}
143+
</ModalContext.Provider>
144+
);
145+
}
146+
147+
export function useModals() {
148+
const context = useContext(ModalContext);
149+
if (!context) throw new Error('useModals must be used within ModalProvider');
150+
return context;
151+
}

tools/server/webui/src/components/SettingDialog.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
SquaresPlusIcon,
1414
} from '@heroicons/react/24/outline';
1515
import { OpenInNewTab } from '../utils/common';
16+
import { useModals } from './ModalProvider';
1617

1718
type SettKey = keyof typeof CONFIG_DEFAULT;
1819

@@ -282,14 +283,15 @@ export default function SettingDialog({
282283
const [localConfig, setLocalConfig] = useState<typeof CONFIG_DEFAULT>(
283284
JSON.parse(JSON.stringify(config))
284285
);
286+
const { showConfirm, showAlert } = useModals();
285287

286-
const resetConfig = () => {
287-
if (window.confirm('Are you sure you want to reset all settings?')) {
288+
const resetConfig = async () => {
289+
if (await showConfirm('Are you sure you want to reset all settings?')) {
288290
setLocalConfig(CONFIG_DEFAULT);
289291
}
290292
};
291293

292-
const handleSave = () => {
294+
const handleSave = async () => {
293295
// copy the local config to prevent direct mutation
294296
const newConfig: typeof CONFIG_DEFAULT = JSON.parse(
295297
JSON.stringify(localConfig)
@@ -302,22 +304,22 @@ export default function SettingDialog({
302304
const mustBeNumeric = isNumeric(CONFIG_DEFAULT[key as SettKey]);
303305
if (mustBeString) {
304306
if (!isString(value)) {
305-
alert(`Value for ${key} must be string`);
307+
await showAlert(`Value for ${key} must be string`);
306308
return;
307309
}
308310
} else if (mustBeNumeric) {
309311
const trimmedValue = value.toString().trim();
310312
const numVal = Number(trimmedValue);
311313
if (isNaN(numVal) || !isNumeric(numVal) || trimmedValue.length === 0) {
312-
alert(`Value for ${key} must be numeric`);
314+
await showAlert(`Value for ${key} must be numeric`);
313315
return;
314316
}
315317
// force conversion to number
316318
// @ts-expect-error this is safe
317319
newConfig[key] = numVal;
318320
} else if (mustBeBoolean) {
319321
if (!isBoolean(value)) {
320-
alert(`Value for ${key} must be boolean`);
322+
await showAlert(`Value for ${key} must be boolean`);
321323
return;
322324
}
323325
} else {

tools/server/webui/src/components/Sidebar.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
import { BtnWithTooltips } from '../utils/common';
1515
import { useAppContext } from '../utils/app.context';
1616
import toast from 'react-hot-toast';
17+
import { useModals } from './ModalProvider';
1718

1819
export default function Sidebar() {
1920
const params = useParams();
@@ -38,6 +39,7 @@ export default function Sidebar() {
3839
StorageUtils.offConversationChanged(handleConversationChange);
3940
};
4041
}, []);
42+
const { showConfirm, showPrompt } = useModals();
4143

4244
const groupedConv = useMemo(
4345
() => groupConversationsByDate(conversations),
@@ -130,15 +132,15 @@ export default function Sidebar() {
130132
onSelect={() => {
131133
navigate(`/chat/${conv.id}`);
132134
}}
133-
onDelete={() => {
135+
onDelete={async () => {
134136
if (isGenerating(conv.id)) {
135137
toast.error(
136138
'Cannot delete conversation while generating'
137139
);
138140
return;
139141
}
140142
if (
141-
window.confirm(
143+
await showConfirm(
142144
'Are you sure to delete this conversation?'
143145
)
144146
) {
@@ -167,14 +169,14 @@ export default function Sidebar() {
167169
document.body.removeChild(a);
168170
URL.revokeObjectURL(url);
169171
}}
170-
onRename={() => {
172+
onRename={async () => {
171173
if (isGenerating(conv.id)) {
172174
toast.error(
173175
'Cannot rename conversation while generating'
174176
);
175177
return;
176178
}
177-
const newName = window.prompt(
179+
const newName = await showPrompt(
178180
'Enter new name for the conversation',
179181
conv.name
180182
);

0 commit comments

Comments
 (0)