mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-13 19:17:35 +08:00
webui: replace alert API with standalone GlobalDialog component
This commit is contained in:
45
webui/src/components/GlobalDialog.tsx
Normal file
45
webui/src/components/GlobalDialog.tsx
Normal file
@@ -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 (
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<motion.div className="fixed inset-0 z-[130] bg-black/60 backdrop-blur-sm flex items-center justify-center p-4"
|
||||
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}>
|
||||
<motion.div className="w-full max-w-md rounded-2xl border border-zinc-700 bg-zinc-900 shadow-2xl"
|
||||
initial={{ scale: 0.95, y: 8 }} animate={{ scale: 1, y: 0 }} exit={{ scale: 0.95, y: 8 }}>
|
||||
<div className="px-5 py-4 border-b border-zinc-800">
|
||||
<h3 className="text-sm font-semibold text-zinc-100">{options.title || (kind === 'confirm' ? 'Please confirm' : 'Notice')}</h3>
|
||||
</div>
|
||||
<div className="px-5 py-4 text-sm text-zinc-300 whitespace-pre-wrap">{options.message}</div>
|
||||
<div className="px-5 pb-5 flex items-center justify-end gap-2">
|
||||
{kind === 'confirm' && (
|
||||
<button onClick={onCancel} className="px-3 py-1.5 rounded-lg bg-zinc-800 hover:bg-zinc-700 text-zinc-200 text-sm">{options.cancelText || 'Cancel'}</button>
|
||||
)}
|
||||
<button onClick={onConfirm} className={`px-3 py-1.5 rounded-lg text-sm ${options.danger ? 'bg-red-600 hover:bg-red-500 text-white' : 'bg-indigo-600 hover:bg-indigo-500 text-white'}`}>
|
||||
{options.confirmText || 'OK'}
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
||||
export type { DialogOptions };
|
||||
@@ -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<void>;
|
||||
confirm: (opts: ModalOptions | string) => Promise<boolean>;
|
||||
notify: (opts: DialogOptions | string) => Promise<void>;
|
||||
confirmDialog: (opts: DialogOptions | string) => Promise<boolean>;
|
||||
openModal: (node: React.ReactNode, title?: string) => void;
|
||||
closeModal: () => void;
|
||||
};
|
||||
@@ -24,7 +17,7 @@ const UIContext = createContext<UIContextType | undefined>(undefined);
|
||||
export const UIProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingText, setLoadingText] = useState('Loading...');
|
||||
const [dialog, setDialog] = useState<null | { kind: 'alert' | 'confirm'; options: ModalOptions; resolve: (v: any) => void }>(null);
|
||||
const [dialog, setDialog] = useState<null | { kind: 'notice' | 'confirm'; options: DialogOptions; resolve: (v: any) => void }>(null);
|
||||
const [customModal, setCustomModal] = useState<null | { title?: string; node: React.ReactNode }>(null);
|
||||
|
||||
const value = useMemo<UIContextType>(() => ({
|
||||
@@ -34,11 +27,11 @@ export const UIProvider: React.FC<{ children: React.ReactNode }> = ({ children }
|
||||
setLoading(true);
|
||||
},
|
||||
hideLoading: () => setLoading(false),
|
||||
alert: (opts) => new Promise<void>((resolve) => {
|
||||
notify: (opts) => new Promise<void>((resolve) => {
|
||||
const options = typeof opts === 'string' ? { message: opts } : opts;
|
||||
setDialog({ kind: 'alert', options, resolve });
|
||||
setDialog({ kind: 'notice', options, resolve });
|
||||
}),
|
||||
confirm: (opts) => new Promise<boolean>((resolve) => {
|
||||
confirmDialog: (opts) => new Promise<boolean>((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 }
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<AnimatePresence>
|
||||
{dialog && (
|
||||
<motion.div className="fixed inset-0 z-[130] bg-black/60 backdrop-blur-sm flex items-center justify-center p-4"
|
||||
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}>
|
||||
<motion.div className="w-full max-w-md rounded-2xl border border-zinc-700 bg-zinc-900 shadow-2xl"
|
||||
initial={{ scale: 0.95, y: 8 }} animate={{ scale: 1, y: 0 }} exit={{ scale: 0.95, y: 8 }}>
|
||||
<div className="px-5 py-4 border-b border-zinc-800">
|
||||
<h3 className="text-sm font-semibold text-zinc-100">{dialog.options.title || (dialog.kind === 'confirm' ? 'Please confirm' : 'Notice')}</h3>
|
||||
</div>
|
||||
<div className="px-5 py-4 text-sm text-zinc-300 whitespace-pre-wrap">{dialog.options.message}</div>
|
||||
<div className="px-5 pb-5 flex items-center justify-end gap-2">
|
||||
{dialog.kind === 'confirm' && (
|
||||
<button onClick={() => closeDialog(false)} className="px-3 py-1.5 rounded-lg bg-zinc-800 hover:bg-zinc-700 text-zinc-200 text-sm">{dialog.options.cancelText || 'Cancel'}</button>
|
||||
)}
|
||||
<button onClick={() => closeDialog(true)} className={`px-3 py-1.5 rounded-lg text-sm ${dialog.options.danger ? 'bg-red-600 hover:bg-red-500 text-white' : 'bg-indigo-600 hover:bg-indigo-500 text-white'}`}>
|
||||
{dialog.options.confirmText || 'OK'}
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<GlobalDialog
|
||||
open={!!dialog}
|
||||
kind={(dialog?.kind || 'notice') as 'notice' | 'confirm'}
|
||||
options={dialog?.options || { message: '' }}
|
||||
onConfirm={() => closeDialog(true)}
|
||||
onCancel={() => closeDialog(false)}
|
||||
/>
|
||||
|
||||
<AnimatePresence>
|
||||
{customModal && (
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user