diff --git a/webui/src/App.tsx b/webui/src/App.tsx index 4051485..78b9ccc 100644 --- a/webui/src/App.tsx +++ b/webui/src/App.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { BrowserRouter, Routes, Route } from 'react-router-dom'; import { AppProvider } from './context/AppContext'; +import { UIProvider } from './context/UIContext'; import Layout from './components/Layout'; import Dashboard from './pages/Dashboard'; import Chat from './pages/Chat'; @@ -14,20 +15,22 @@ import Memory from './pages/Memory'; export default function App() { return ( - - - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - + + + + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + ); } diff --git a/webui/src/context/UIContext.tsx b/webui/src/context/UIContext.tsx new file mode 100644 index 0000000..5f73629 --- /dev/null +++ b/webui/src/context/UIContext.tsx @@ -0,0 +1,117 @@ +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; +}; + +type UIContextType = { + loading: boolean; + showLoading: (text?: string) => void; + hideLoading: () => void; + alert: (opts: ModalOptions | string) => Promise; + confirm: (opts: ModalOptions | string) => Promise; + openModal: (node: React.ReactNode, title?: string) => void; + closeModal: () => void; +}; + +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 [customModal, setCustomModal] = useState(null); + + const value = useMemo(() => ({ + loading, + showLoading: (text?: string) => { + setLoadingText(text || 'Loading...'); + setLoading(true); + }, + hideLoading: () => setLoading(false), + alert: (opts) => new Promise((resolve) => { + const options = typeof opts === 'string' ? { message: opts } : opts; + setDialog({ kind: 'alert', options, resolve }); + }), + confirm: (opts) => new Promise((resolve) => { + const options = typeof opts === 'string' ? { message: opts } : opts; + setDialog({ kind: 'confirm', options, resolve }); + }), + openModal: (node, title) => setCustomModal({ node, title }), + closeModal: () => setCustomModal(null), + }), [loading]); + + const closeDialog = (result: boolean) => { + if (!dialog) return; + dialog.resolve(dialog.kind === 'alert' ? undefined : result); + setDialog(null); + }; + + return ( + + {children} + + + {loading && ( + +
+
+
{loadingText}
+
+ + )} + + + + {dialog && ( + + +
+

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

+
+
{dialog.options.message}
+
+ {dialog.kind === 'confirm' && ( + + )} + +
+
+
+ )} +
+ + + {customModal && ( + + +
+

{customModal.title || 'Modal'}

+ +
+
{customModal.node}
+
+
+ )} +
+ + ); +}; + +export const useUI = () => { + const ctx = useContext(UIContext); + if (!ctx) throw new Error('useUI must be used within UIProvider'); + return ctx; +}; diff --git a/webui/src/pages/Skills.tsx b/webui/src/pages/Skills.tsx index e5fa31a..5f1144d 100644 --- a/webui/src/pages/Skills.tsx +++ b/webui/src/pages/Skills.tsx @@ -3,6 +3,7 @@ import { Plus, RefreshCw, Trash2, Edit2, Zap, X, FileText, Save } from 'lucide-r import { motion, AnimatePresence } from 'motion/react'; import { useTranslation } from 'react-i18next'; import { useAppContext } from '../context/AppContext'; +import { useUI } from '../context/UIContext'; import { Skill } from '../types'; const initialSkillForm: Omit = { @@ -15,6 +16,7 @@ const initialSkillForm: Omit = { const Skills: React.FC = () => { const { t } = useTranslation(); const { skills, refreshSkills, q } = useAppContext(); + const ui = useUI(); const [installName, setInstallName] = useState(''); const qp = (k: string, v: string) => `${q}${q ? '&' : '?'}${k}=${encodeURIComponent(v)}`; const [isModalOpen, setIsModalOpen] = useState(false); @@ -28,7 +30,7 @@ const Skills: React.FC = () => { const [fileContent, setFileContent] = useState(''); async function deleteSkill(id: string) { - if (!confirm('Are you sure you want to delete this skill?')) return; + if (!await ui.confirm({ 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(); @@ -46,7 +48,7 @@ const Skills: React.FC = () => { body: JSON.stringify({ action: 'install', name }), }); if (!r.ok) { - alert(await r.text()); + await ui.alert({ title: 'Request Failed', message: await r.text() }); return; } setInstallName(''); @@ -66,10 +68,10 @@ const Skills: React.FC = () => { setIsModalOpen(false); await refreshSkills(); } else { - alert(await r.text()); + await ui.alert({ title: 'Request Failed', message: await r.text() }); } } catch (e) { - alert(String(e)); + await ui.alert({ title: 'Error', message: String(e) }); } } @@ -78,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) { - alert(await r.text()); + await ui.alert({ title: 'Request Failed', message: await r.text() }); return; } const j = await r.json(); @@ -96,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) { - alert(await r.text()); + await ui.alert({ title: 'Request Failed', message: await r.text() }); return; } const j = await r.json(); @@ -112,10 +114,10 @@ const Skills: React.FC = () => { body: JSON.stringify({ action: 'write_file', id: activeSkill, file: activeFile, content: fileContent }), }); if (!r.ok) { - alert(await r.text()); + await ui.alert({ title: 'Request Failed', message: await r.text() }); return; } - alert('Saved'); + await ui.alert({ title: 'Saved', message: 'Skill file saved successfully.' }); } return (