From 4548710f2f04d53d7b45b4d4353f34b01e062f31 Mon Sep 17 00:00:00 2001 From: lily-de <119957291+lily-de@users.noreply.github.com> Date: Sun, 26 Jan 2025 11:15:40 -0500 Subject: [PATCH] feat: confirmation dialog for deeplinks (#783) --- ui/desktop/src/App.tsx | 93 ++++++++++++++++--- .../components/settings/extensions/utils.tsx | 14 +++ ui/desktop/src/components/ui/BaseModal.tsx | 37 ++++++++ .../src/components/ui/ConfirmationModal.tsx | 50 ++++++++++ .../src/{extensions.ts => extensions.tsx} | 0 5 files changed, 179 insertions(+), 15 deletions(-) create mode 100644 ui/desktop/src/components/settings/extensions/utils.tsx create mode 100644 ui/desktop/src/components/ui/BaseModal.tsx create mode 100644 ui/desktop/src/components/ui/ConfirmationModal.tsx rename ui/desktop/src/{extensions.ts => extensions.tsx} (100%) diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index 7619dc66..ce2bec60 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -4,23 +4,74 @@ import { useNavigate } from 'react-router-dom'; import LauncherWindow from './LauncherWindow'; import ChatWindow from './ChatWindow'; import ErrorScreen from './components/ErrorScreen'; +import { ConfirmationModal } from './components/ui/ConfirmationModal'; import 'react-toastify/dist/ReactToastify.css'; import { ToastContainer } from 'react-toastify'; import { ModelProvider } from './components/settings/models/ModelContext'; import { ActiveKeysProvider } from './components/settings/api_keys/ActiveKeysContext'; +import { extractExtensionName } from './components/settings/extensions/utils'; export default function App() { const [fatalError, setFatalError] = useState(null); + const [modalVisible, setModalVisible] = useState(false); + const [pendingLink, setPendingLink] = useState(null); + const [modalMessage, setModalMessage] = useState(''); + const [isInstalling, setIsInstalling] = useState(false); // Track installation progress const searchParams = new URLSearchParams(window.location.search); const isLauncher = searchParams.get('window') === 'launcher'; const navigate = useNavigate(); + // Utility function to extract the command from the link + function extractCommand(link: string): string { + const url = new URL(link); + const cmd = url.searchParams.get('cmd') || 'Unknown Command'; + const args = url.searchParams.getAll('arg').map(decodeURIComponent); + return `${cmd} ${args.join(' ')}`.trim(); // Combine the command and arguments + } + useEffect(() => { - window.electron.on('add-extension', (_, link) => { + const handleAddExtension = (_, link: string) => { + const command = extractCommand(link); // Extract and format the command + const extName = extractExtensionName(link); window.electron.logInfo(`Adding extension from deep link ${link}`); - addExtensionFromDeepLink(link, navigate); - }); - }, [navigate]); + setPendingLink(link); // Save the link for later use + setModalMessage( + `Are you sure you want to install the ${extName} extension?\n\nCommand: ${command}` + ); // Display command + setModalVisible(true); // Show confirmation modal + }; + + window.electron.on('add-extension', handleAddExtension); + + return () => { + // Clean up the event listener when the component unmounts + window.electron.off('add-extension', handleAddExtension); + }; + }, []); + + const handleConfirm = async () => { + if (pendingLink && !isInstalling) { + setIsInstalling(true); // Disable further attempts + console.log('Confirming installation for link:', pendingLink); + + try { + await addExtensionFromDeepLink(pendingLink, navigate); // Proceed with adding the extension + } catch (error) { + console.error('Failed to add extension:', error); + } finally { + // Always reset states + setModalVisible(false); + setPendingLink(null); + setIsInstalling(false); + } + } + }; + + const handleCancel = () => { + console.log('Cancelled extension installation.'); + setModalVisible(false); + setPendingLink(null); // Clear the link if the user cancels + }; useEffect(() => { const handleFatalError = (_: any, errorMessage: string) => { @@ -40,17 +91,29 @@ export default function App() { } return ( - - - {isLauncher ? : } - + {modalVisible && ( + - - + )} + + + {isLauncher ? : } + + + + ); } diff --git a/ui/desktop/src/components/settings/extensions/utils.tsx b/ui/desktop/src/components/settings/extensions/utils.tsx new file mode 100644 index 00000000..39603d11 --- /dev/null +++ b/ui/desktop/src/components/settings/extensions/utils.tsx @@ -0,0 +1,14 @@ +export function extractCommand(link: string): string { + const url = new URL(link); + const cmd = url.searchParams.get('cmd') || 'Unknown Command'; + const args = url.searchParams.getAll('arg').map(decodeURIComponent); + + // Combine the command and its arguments into a reviewable format + return `${cmd} ${args.join(' ')}`.trim(); +} + +export function extractExtensionName(link: string): string { + const url = new URL(link); + const name = url.searchParams.get('name'); + return name ? decodeURIComponent(name) : 'Unknown Extension'; +} diff --git a/ui/desktop/src/components/ui/BaseModal.tsx b/ui/desktop/src/components/ui/BaseModal.tsx new file mode 100644 index 00000000..93ab8873 --- /dev/null +++ b/ui/desktop/src/components/ui/BaseModal.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { Card } from './card'; + +export function BaseModal({ + isOpen, + title, + children, + actions, + onClose, +}: { + isOpen: boolean; + title: string; + children: React.ReactNode; + actions: React.ReactNode; // Buttons for actions + onClose: () => void; +}) { + if (!isOpen) return null; + + return ( +
+ +
+ {/* Header */} +
+

{title}

+
+ + {/* Content */} + {children &&
{children}
} + + {/* Actions */} +
{actions}
+
+
+
+ ); +} diff --git a/ui/desktop/src/components/ui/ConfirmationModal.tsx b/ui/desktop/src/components/ui/ConfirmationModal.tsx new file mode 100644 index 00000000..84d35f4a --- /dev/null +++ b/ui/desktop/src/components/ui/ConfirmationModal.tsx @@ -0,0 +1,50 @@ +import { BaseModal } from './BaseModal'; +import React from 'react'; + +export function ConfirmationModal({ + isOpen, + title, + message, + onConfirm, + onCancel, + confirmLabel = 'Yes', + cancelLabel = 'No', + isSubmitting = false, +}: { + isOpen: boolean; + title: string; + message: string; + onConfirm: () => void; + onCancel: () => void; + confirmLabel?: string; + cancelLabel?: string; + isSubmitting?: boolean; // To handle debounce state +}) { + return ( + + + + + } + > +

{message}

+
+ ); +} diff --git a/ui/desktop/src/extensions.ts b/ui/desktop/src/extensions.tsx similarity index 100% rename from ui/desktop/src/extensions.ts rename to ui/desktop/src/extensions.tsx