--- /dev/null
+import React, { createContext, useState, useContext } from 'react';
+
+type ModalContextType = {
+ showConfirm: (message: string) => Promise<boolean>;
+ showPrompt: (
+ message: string,
+ defaultValue?: string
+ ) => Promise<string | undefined>;
+ showAlert: (message: string) => Promise<void>;
+};
+const ModalContext = createContext<ModalContextType>(null!);
+
+interface ModalState<T> {
+ isOpen: boolean;
+ message: string;
+ defaultValue?: string;
+ resolve: ((value: T) => void) | null;
+}
+
+export function ModalProvider({ children }: { children: React.ReactNode }) {
+ const [confirmState, setConfirmState] = useState<ModalState<boolean>>({
+ isOpen: false,
+ message: '',
+ resolve: null,
+ });
+ const [promptState, setPromptState] = useState<
+ ModalState<string | undefined>
+ >({ isOpen: false, message: '', resolve: null });
+ const [alertState, setAlertState] = useState<ModalState<void>>({
+ isOpen: false,
+ message: '',
+ resolve: null,
+ });
+ const inputRef = React.useRef<HTMLInputElement>(null);
+
+ const showConfirm = (message: string): Promise<boolean> => {
+ return new Promise((resolve) => {
+ setConfirmState({ isOpen: true, message, resolve });
+ });
+ };
+
+ const showPrompt = (
+ message: string,
+ defaultValue?: string
+ ): Promise<string | undefined> => {
+ return new Promise((resolve) => {
+ setPromptState({ isOpen: true, message, defaultValue, resolve });
+ });
+ };
+
+ const showAlert = (message: string): Promise<void> => {
+ 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 (
+ <ModalContext.Provider value={{ showConfirm, showPrompt, showAlert }}>
+ {children}
+
+ {/* Confirm Modal */}
+ {confirmState.isOpen && (
+ <dialog className="modal modal-open z-[1100]">
+ <div className="modal-box">
+ <h3 className="font-bold text-lg">{confirmState.message}</h3>
+ <div className="modal-action">
+ <button
+ className="btn btn-ghost"
+ onClick={() => handleConfirm(false)}
+ >
+ Cancel
+ </button>
+ <button
+ className="btn btn-error"
+ onClick={() => handleConfirm(true)}
+ >
+ Confirm
+ </button>
+ </div>
+ </div>
+ </dialog>
+ )}
+
+ {/* Prompt Modal */}
+ {promptState.isOpen && (
+ <dialog className="modal modal-open z-[1100]">
+ <div className="modal-box">
+ <h3 className="font-bold text-lg">{promptState.message}</h3>
+ <input
+ type="text"
+ className="input input-bordered w-full mt-2"
+ defaultValue={promptState.defaultValue}
+ ref={inputRef}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') {
+ handlePrompt((e.target as HTMLInputElement).value);
+ }
+ }}
+ />
+ <div className="modal-action">
+ <button className="btn btn-ghost" onClick={() => handlePrompt()}>
+ Cancel
+ </button>
+ <button
+ className="btn btn-primary"
+ onClick={() => handlePrompt(inputRef.current?.value)}
+ >
+ Submit
+ </button>
+ </div>
+ </div>
+ </dialog>
+ )}
+
+ {/* Alert Modal */}
+ {alertState.isOpen && (
+ <dialog className="modal modal-open z-[1100]">
+ <div className="modal-box">
+ <h3 className="font-bold text-lg">{alertState.message}</h3>
+ <div className="modal-action">
+ <button className="btn" onClick={handleAlertClose}>
+ OK
+ </button>
+ </div>
+ </div>
+ </dialog>
+ )}
+ </ModalContext.Provider>
+ );
+}
+
+export function useModals() {
+ const context = useContext(ModalContext);
+ if (!context) throw new Error('useModals must be used within ModalProvider');
+ return context;
+}
SquaresPlusIcon,
} from '@heroicons/react/24/outline';
import { OpenInNewTab } from '../utils/common';
+import { useModals } from './ModalProvider';
type SettKey = keyof typeof CONFIG_DEFAULT;
const [localConfig, setLocalConfig] = useState<typeof CONFIG_DEFAULT>(
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)
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
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 {
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();
StorageUtils.offConversationChanged(handleConversationChange);
};
}, []);
+ const { showConfirm, showPrompt } = useModals();
const groupedConv = useMemo(
() => groupConversationsByDate(conversations),
onSelect={() => {
navigate(`/chat/${conv.id}`);
}}
- onDelete={() => {
+ onDelete={async () => {
if (isGenerating(conv.id)) {
toast.error(
'Cannot delete conversation while generating'
return;
}
if (
- window.confirm(
+ await showConfirm(
'Are you sure to delete this conversation?'
)
) {
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
);