diff --git a/tools/server/public/index.html.gz b/tools/server/public/index.html.gz index 8d4e392ff3315..f8e3043421d33 100644 Binary files a/tools/server/public/index.html.gz and b/tools/server/public/index.html.gz differ diff --git a/tools/server/webui/src/App.tsx b/tools/server/webui/src/App.tsx index 1b673bbaa1cce..02f1719d3d2ce 100644 --- a/tools/server/webui/src/App.tsx +++ b/tools/server/webui/src/App.tsx @@ -5,21 +5,24 @@ import { AppContextProvider, useAppContext } from './utils/app.context'; import ChatScreen from './components/ChatScreen'; import SettingDialog from './components/SettingDialog'; import { Toaster } from 'react-hot-toast'; +import { ModalProvider } from './components/ModalProvider'; function App() { return ( - -
- - - }> - } /> - } /> - - - -
-
+ + +
+ + + }> + } /> + } /> + + + +
+
+
); } diff --git a/tools/server/webui/src/components/ModalProvider.tsx b/tools/server/webui/src/components/ModalProvider.tsx new file mode 100644 index 0000000000000..f2ebf8e0a7fa4 --- /dev/null +++ b/tools/server/webui/src/components/ModalProvider.tsx @@ -0,0 +1,151 @@ +import React, { createContext, useState, useContext } from 'react'; + +type ModalContextType = { + showConfirm: (message: string) => Promise; + showPrompt: ( + message: string, + defaultValue?: string + ) => Promise; + showAlert: (message: string) => Promise; +}; +const ModalContext = createContext(null!); + +interface ModalState { + isOpen: boolean; + message: string; + defaultValue?: string; + resolve: ((value: T) => void) | null; +} + +export function ModalProvider({ children }: { children: React.ReactNode }) { + const [confirmState, setConfirmState] = useState>({ + isOpen: false, + message: '', + resolve: null, + }); + const [promptState, setPromptState] = useState< + ModalState + >({ isOpen: false, message: '', resolve: null }); + const [alertState, setAlertState] = useState>({ + isOpen: false, + message: '', + resolve: null, + }); + const inputRef = React.useRef(null); + + const showConfirm = (message: string): Promise => { + return new Promise((resolve) => { + setConfirmState({ isOpen: true, message, resolve }); + }); + }; + + const showPrompt = ( + message: string, + defaultValue?: string + ): Promise => { + return new Promise((resolve) => { + setPromptState({ isOpen: true, message, defaultValue, resolve }); + }); + }; + + const showAlert = (message: string): Promise => { + return new Promise((resolve) => { + setAlertState({ isOpen: true, message, resolve }); + }); + }; + + const handleConfirm = (result: boolean) => { + confirmState.resolve?.(result); + setConfirmState({ isOpen: false, message: '', resolve: null }); + }; + + const handlePrompt = (result?: string) => { + promptState.resolve?.(result); + setPromptState({ isOpen: false, message: '', resolve: null }); + }; + + const handleAlertClose = () => { + alertState.resolve?.(); + setAlertState({ isOpen: false, message: '', resolve: null }); + }; + + return ( + + {children} + + {/* Confirm Modal */} + {confirmState.isOpen && ( + +
+

{confirmState.message}

+
+ + +
+
+
+ )} + + {/* Prompt Modal */} + {promptState.isOpen && ( + +
+

{promptState.message}

+ { + if (e.key === 'Enter') { + handlePrompt((e.target as HTMLInputElement).value); + } + }} + /> +
+ + +
+
+
+ )} + + {/* Alert Modal */} + {alertState.isOpen && ( + +
+

{alertState.message}

+
+ +
+
+
+ )} +
+ ); +} + +export function useModals() { + const context = useContext(ModalContext); + if (!context) throw new Error('useModals must be used within ModalProvider'); + return context; +} diff --git a/tools/server/webui/src/components/SettingDialog.tsx b/tools/server/webui/src/components/SettingDialog.tsx index e4684be7e007c..45a8d73b00592 100644 --- a/tools/server/webui/src/components/SettingDialog.tsx +++ b/tools/server/webui/src/components/SettingDialog.tsx @@ -13,6 +13,7 @@ import { SquaresPlusIcon, } from '@heroicons/react/24/outline'; import { OpenInNewTab } from '../utils/common'; +import { useModals } from './ModalProvider'; type SettKey = keyof typeof CONFIG_DEFAULT; @@ -282,14 +283,15 @@ export default function SettingDialog({ const [localConfig, setLocalConfig] = useState( JSON.parse(JSON.stringify(config)) ); + const { showConfirm, showAlert } = useModals(); - const resetConfig = () => { - if (window.confirm('Are you sure you want to reset all settings?')) { + const resetConfig = async () => { + if (await showConfirm('Are you sure you want to reset all settings?')) { setLocalConfig(CONFIG_DEFAULT); } }; - const handleSave = () => { + const handleSave = async () => { // copy the local config to prevent direct mutation const newConfig: typeof CONFIG_DEFAULT = JSON.parse( JSON.stringify(localConfig) @@ -302,14 +304,14 @@ export default function SettingDialog({ const mustBeNumeric = isNumeric(CONFIG_DEFAULT[key as SettKey]); if (mustBeString) { if (!isString(value)) { - alert(`Value for ${key} must be string`); + await showAlert(`Value for ${key} must be string`); return; } } else if (mustBeNumeric) { const trimmedValue = value.toString().trim(); const numVal = Number(trimmedValue); if (isNaN(numVal) || !isNumeric(numVal) || trimmedValue.length === 0) { - alert(`Value for ${key} must be numeric`); + await showAlert(`Value for ${key} must be numeric`); return; } // force conversion to number @@ -317,7 +319,7 @@ export default function SettingDialog({ newConfig[key] = numVal; } else if (mustBeBoolean) { if (!isBoolean(value)) { - alert(`Value for ${key} must be boolean`); + await showAlert(`Value for ${key} must be boolean`); return; } } else { diff --git a/tools/server/webui/src/components/Sidebar.tsx b/tools/server/webui/src/components/Sidebar.tsx index 8cac52f4c6ddf..a77cb83b45dd7 100644 --- a/tools/server/webui/src/components/Sidebar.tsx +++ b/tools/server/webui/src/components/Sidebar.tsx @@ -14,6 +14,7 @@ import { import { BtnWithTooltips } from '../utils/common'; import { useAppContext } from '../utils/app.context'; import toast from 'react-hot-toast'; +import { useModals } from './ModalProvider'; export default function Sidebar() { const params = useParams(); @@ -38,6 +39,7 @@ export default function Sidebar() { StorageUtils.offConversationChanged(handleConversationChange); }; }, []); + const { showConfirm, showPrompt } = useModals(); const groupedConv = useMemo( () => groupConversationsByDate(conversations), @@ -130,7 +132,7 @@ export default function Sidebar() { onSelect={() => { navigate(`/chat/${conv.id}`); }} - onDelete={() => { + onDelete={async () => { if (isGenerating(conv.id)) { toast.error( 'Cannot delete conversation while generating' @@ -138,7 +140,7 @@ export default function Sidebar() { return; } if ( - window.confirm( + await showConfirm( 'Are you sure to delete this conversation?' ) ) { @@ -167,14 +169,14 @@ export default function Sidebar() { document.body.removeChild(a); URL.revokeObjectURL(url); }} - onRename={() => { + onRename={async () => { if (isGenerating(conv.id)) { toast.error( 'Cannot rename conversation while generating' ); return; } - const newName = window.prompt( + const newName = await showPrompt( 'Enter new name for the conversation', conv.name );