diff --git a/webui/src/components/GlobalDialog.tsx b/webui/src/components/GlobalDialog.tsx new file mode 100644 index 0000000..5e33f22 --- /dev/null +++ b/webui/src/components/GlobalDialog.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { AnimatePresence, motion } from 'motion/react'; + +type DialogOptions = { + title?: string; + message: string; + confirmText?: string; + cancelText?: string; + danger?: boolean; +}; + +export const GlobalDialog: React.FC<{ + open: boolean; + kind: 'notice' | 'confirm'; + options: DialogOptions; + onConfirm: () => void; + onCancel: () => void; +}> = ({ open, kind, options, onConfirm, onCancel }) => { + return ( + + {open && ( + + +
+

{options.title || (kind === 'confirm' ? 'Please confirm' : 'Notice')}

+
+
{options.message}
+
+ {kind === 'confirm' && ( + + )} + +
+
+
+ )} +
+ ); +}; + +export type { DialogOptions }; diff --git a/webui/src/context/UIContext.tsx b/webui/src/context/UIContext.tsx index 5f73629..2678888 100644 --- a/webui/src/context/UIContext.tsx +++ b/webui/src/context/UIContext.tsx @@ -1,20 +1,13 @@ import React, { createContext, useContext, useMemo, useState } from 'react'; import { AnimatePresence, motion } from 'motion/react'; - -type ModalOptions = { - title?: string; - message: string; - confirmText?: string; - cancelText?: string; - danger?: boolean; -}; +import { GlobalDialog, DialogOptions } from '../components/GlobalDialog'; type UIContextType = { loading: boolean; showLoading: (text?: string) => void; hideLoading: () => void; - alert: (opts: ModalOptions | string) => Promise; - confirm: (opts: ModalOptions | string) => Promise; + notify: (opts: DialogOptions | string) => Promise; + confirmDialog: (opts: DialogOptions | string) => Promise; openModal: (node: React.ReactNode, title?: string) => void; closeModal: () => void; }; @@ -24,7 +17,7 @@ const UIContext = createContext(undefined); export const UIProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [loading, setLoading] = useState(false); const [loadingText, setLoadingText] = useState('Loading...'); - const [dialog, setDialog] = useState void }>(null); + const [dialog, setDialog] = useState void }>(null); const [customModal, setCustomModal] = useState(null); const value = useMemo(() => ({ @@ -34,11 +27,11 @@ export const UIProvider: React.FC<{ children: React.ReactNode }> = ({ children } setLoading(true); }, hideLoading: () => setLoading(false), - alert: (opts) => new Promise((resolve) => { + notify: (opts) => new Promise((resolve) => { const options = typeof opts === 'string' ? { message: opts } : opts; - setDialog({ kind: 'alert', options, resolve }); + setDialog({ kind: 'notice', options, resolve }); }), - confirm: (opts) => new Promise((resolve) => { + confirmDialog: (opts) => new Promise((resolve) => { const options = typeof opts === 'string' ? { message: opts } : opts; setDialog({ kind: 'confirm', options, resolve }); }), @@ -48,7 +41,7 @@ export const UIProvider: React.FC<{ children: React.ReactNode }> = ({ children } const closeDialog = (result: boolean) => { if (!dialog) return; - dialog.resolve(dialog.kind === 'alert' ? undefined : result); + dialog.resolve(dialog.kind === 'notice' ? undefined : result); setDialog(null); }; @@ -68,28 +61,13 @@ export const UIProvider: React.FC<{ children: React.ReactNode }> = ({ children } )} - - {dialog && ( - - -
-

{dialog.options.title || (dialog.kind === 'confirm' ? 'Please confirm' : 'Notice')}

-
-
{dialog.options.message}
-
- {dialog.kind === 'confirm' && ( - - )} - -
-
-
- )} -
+ closeDialog(true)} + onCancel={() => closeDialog(false)} + /> {customModal && ( diff --git a/webui/src/pages/Skills.tsx b/webui/src/pages/Skills.tsx index 5f1144d..d929818 100644 --- a/webui/src/pages/Skills.tsx +++ b/webui/src/pages/Skills.tsx @@ -30,7 +30,7 @@ const Skills: React.FC = () => { const [fileContent, setFileContent] = useState(''); async function deleteSkill(id: string) { - if (!await ui.confirm({ title: 'Delete Skill', message: 'Are you sure you want to delete this skill?', danger: true, confirmText: 'Delete' })) return; + if (!await ui.confirmDialog({ title: 'Delete Skill', message: 'Are you sure you want to delete this skill?', danger: true, confirmText: 'Delete' })) return; try { await fetch(`/webui/api/skills${qp('id', id)}`, { method: 'DELETE' }); await refreshSkills(); @@ -48,7 +48,7 @@ const Skills: React.FC = () => { body: JSON.stringify({ action: 'install', name }), }); if (!r.ok) { - await ui.alert({ title: 'Request Failed', message: await r.text() }); + await ui.notify({ title: 'Request Failed', message: await r.text() }); return; } setInstallName(''); @@ -68,10 +68,10 @@ const Skills: React.FC = () => { setIsModalOpen(false); await refreshSkills(); } else { - await ui.alert({ title: 'Request Failed', message: await r.text() }); + await ui.notify({ title: 'Request Failed', message: await r.text() }); } } catch (e) { - await ui.alert({ title: 'Error', message: String(e) }); + await ui.notify({ title: 'Error', message: String(e) }); } } @@ -80,7 +80,7 @@ const Skills: React.FC = () => { setIsFileModalOpen(true); const r = await fetch(`/webui/api/skills${q ? `${q}&id=${encodeURIComponent(skillId)}&files=1` : `?id=${encodeURIComponent(skillId)}&files=1`}`); if (!r.ok) { - await ui.alert({ title: 'Request Failed', message: await r.text() }); + await ui.notify({ title: 'Request Failed', message: await r.text() }); return; } const j = await r.json(); @@ -98,7 +98,7 @@ const Skills: React.FC = () => { const url = `/webui/api/skills${q ? `${q}&id=${encodeURIComponent(skillId)}&file=${encodeURIComponent(file)}` : `?id=${encodeURIComponent(skillId)}&file=${encodeURIComponent(file)}`}`; const r = await fetch(url); if (!r.ok) { - await ui.alert({ title: 'Request Failed', message: await r.text() }); + await ui.notify({ title: 'Request Failed', message: await r.text() }); return; } const j = await r.json(); @@ -114,10 +114,10 @@ const Skills: React.FC = () => { body: JSON.stringify({ action: 'write_file', id: activeSkill, file: activeFile, content: fileContent }), }); if (!r.ok) { - await ui.alert({ title: 'Request Failed', message: await r.text() }); + await ui.notify({ title: 'Request Failed', message: await r.text() }); return; } - await ui.alert({ title: 'Saved', message: 'Skill file saved successfully.' }); + await ui.notify({ title: 'Saved', message: 'Skill file saved successfully.' }); } return (