From 3ac8a15677b33f4abfdff705e8fa2c268ad87b6c Mon Sep 17 00:00:00 2001 From: Alex Hancock Date: Tue, 25 Feb 2025 16:37:51 -0500 Subject: [PATCH] feat: wip on ConfigProvider and integration in settings_v2 --- ui/desktop/src/components/ConfigContext.tsx | 72 ++++++++++++ .../settings_v2/ExtensionsSection.tsx | 96 +++++++++++++++ .../components/settings_v2/SettingsView.tsx | 95 ++------------- ui/desktop/src/config/ConfigManager.tsx | 107 ----------------- ui/desktop/src/config/api.ts | 98 ---------------- ui/desktop/src/hooks/useConfig.ts | 111 ------------------ ui/desktop/src/renderer.tsx | 17 +-- 7 files changed, 185 insertions(+), 411 deletions(-) create mode 100644 ui/desktop/src/components/ConfigContext.tsx create mode 100644 ui/desktop/src/components/settings_v2/ExtensionsSection.tsx delete mode 100644 ui/desktop/src/config/ConfigManager.tsx delete mode 100644 ui/desktop/src/config/api.ts delete mode 100644 ui/desktop/src/hooks/useConfig.ts diff --git a/ui/desktop/src/components/ConfigContext.tsx b/ui/desktop/src/components/ConfigContext.tsx new file mode 100644 index 000000000..f6a9638b2 --- /dev/null +++ b/ui/desktop/src/components/ConfigContext.tsx @@ -0,0 +1,72 @@ +import React, { createContext, useContext, useState, useEffect } from 'react'; +import { Config } from '../api/config'; + +interface ConfigContextType { + config: Record; + upsert: (key: string, value: any, isSecret?: boolean) => Promise; + read: (key: string) => Promise; + remove: (key: string) => Promise; + addExtension: (name: string, config: any) => Promise; + removeExtension: (name: string) => Promise; +} + +interface ConfigProviderProps { + children: React.ReactNode; +} + +const ConfigContext = createContext(undefined); + +export const ConfigProvider: React.FC = ({ children }) => { + const [config, setConfig] = useState>({}); + + useEffect(() => { + // Load all configuration data on mount + (async () => { + const initialConfig = await Config.readAll(); + setConfig(initialConfig || {}); + })(); + }, []); + + const reloadConfig = async () => { + const newConfig = await Config.readAll(); + setConfig(newConfig || {}); + }; + + const upsert = async (key: string, value: any, isSecret?: boolean) => { + await Config.upsert(key, value, isSecret); + await reloadConfig(); + }; + + const read = async (key: string) => { + return Config.read(key); + }; + + const remove = async (key: string) => { + await Config.remove(key); + await reloadConfig(); + }; + + const addExtension = async (name: string, config: any) => { + await Config.addExtension(name, config); + await reloadConfig(); + }; + + const removeExtension = async (name: string) => { + await Config.removeExtension(name); + await reloadConfig(); + }; + + return ( + + {children} + + ); +}; + +export const useConfig = () => { + const context = useContext(ConfigContext); + if (context === undefined) { + throw new Error('useConfig must be used within a ConfigProvider'); + } + return context; +}; diff --git a/ui/desktop/src/components/settings_v2/ExtensionsSection.tsx b/ui/desktop/src/components/settings_v2/ExtensionsSection.tsx new file mode 100644 index 000000000..27d906b9c --- /dev/null +++ b/ui/desktop/src/components/settings_v2/ExtensionsSection.tsx @@ -0,0 +1,96 @@ +import React, { useState } from 'react'; +import { Button } from '../ui/button'; +import { Switch } from '../ui/switch'; +import { Plus } from 'lucide-react'; +import { Gear } from '../icons/Gear'; +import { GPSIcon } from '../ui/icons'; + +interface ExtensionItem { + id: string; + title: string; + subtitle: string; + enabled: boolean; + canConfigure?: boolean; +} + +const extensionItems: ExtensionItem[] = [ + { + id: 'dev', + title: 'Developer Tools', + subtitle: 'Code editing and shell access', + enabled: true, + canConfigure: true, + }, + { + id: 'browser', + title: 'Web Browser', + subtitle: 'Internet access and web automation', + enabled: false, + canConfigure: true, + }, +]; + +export default function ExtensionsSection() { + const [extensions, setExtensions] = useState(extensionItems); + + const handleExtensionToggle = (id: string) => { + setExtensions( + extensions.map((extension) => ({ + ...extension, + enabled: extension.id === id ? !extension.enabled : extension.enabled, + })) + ); + }; + + return ( +
+
+

Extensions

+
+
+

+ These extensions use the Model Context Protocol (MCP). They can expand Goose's + capabilities using three main components: Prompts, Resources, and Tools. +

+
+ {extensions.map((extension, index) => ( + +
+
+

{extension.title}

+

{extension.subtitle}

+
+
+ {extension.canConfigure && ( + + )} + handleExtensionToggle(extension.id)} + className="bg-[#393838] [&_span[data-state]]:bg-white" + /> +
+
+ {index < extensions.length - 1 &&
} + + ))} +
+
+ + +
+
+
+ ); +} diff --git a/ui/desktop/src/components/settings_v2/SettingsView.tsx b/ui/desktop/src/components/settings_v2/SettingsView.tsx index b7b874cb7..67f492b45 100644 --- a/ui/desktop/src/components/settings_v2/SettingsView.tsx +++ b/ui/desktop/src/components/settings_v2/SettingsView.tsx @@ -2,11 +2,11 @@ import React from 'react'; import { ScrollArea } from '../ui/scroll-area'; import BackButton from '../ui/BackButton'; import type { View } from '../../App'; +import { useConfig } from '../ConfigContext'; import { Button } from '../ui/button'; -import { Switch } from '../ui/switch'; import { Plus } from 'lucide-react'; import { Gear } from '../icons/Gear'; -import { GPSIcon } from '../ui/icons'; +import ExtensionsSection from './ExtensionsSection'; interface ModelOption { id: string; @@ -15,14 +15,6 @@ interface ModelOption { selected: boolean; } -interface ExtensionItem { - id: string; - title: string; - subtitle: string; - enabled: boolean; - canConfigure?: boolean; -} - // Mock data - replace with actual data source const defaultModelOptions: ModelOption[] = [ { @@ -39,23 +31,6 @@ const defaultModelOptions: ModelOption[] = [ }, ]; -const extensionItems: ExtensionItem[] = [ - { - id: 'dev', - title: 'Developer Tools', - subtitle: 'Code editing and shell access', - enabled: true, - canConfigure: true, - }, - { - id: 'browser', - title: 'Web Browser', - subtitle: 'Internet access and web automation', - enabled: false, - canConfigure: true, - }, -]; - export type SettingsViewOptions = { extensionId?: string; showEnvVars?: boolean; @@ -71,7 +46,10 @@ export default function SettingsView({ viewOptions: SettingsViewOptions; }) { const [modelOptions, setModelOptions] = React.useState(defaultModelOptions); - const [extensions, setExtensions] = React.useState(extensionItems); + + const { config } = useConfig(); + + console.log(config); const handleModelSelect = (selectedId: string) => { setModelOptions( @@ -82,15 +60,6 @@ export default function SettingsView({ ); }; - const handleExtensionToggle = (id: string) => { - setExtensions( - extensions.map((extension) => ({ - ...extension, - enabled: extension.id === id ? !extension.enabled : extension.enabled, - })) - ); - }; - return (
@@ -146,57 +115,7 @@ export default function SettingsView({ {/* Extensions Section */} -
-
-

Extensions

-
-
-

- These extensions use the Model Context Protocol (MCP). They can expand Goose's - capabilities using three main components: Prompts, Resources, and Tools. -

-
- {extensions.map((extension, index) => ( - -
-
-

{extension.title}

-

{extension.subtitle}

-
-
- {extension.canConfigure && ( - - )} - handleExtensionToggle(extension.id)} - className="bg-[#393838] [&_span[data-state]]:bg-white" - /> -
-
- {index < extensions.length - 1 &&
} - - ))} -
-
- - -
-
-
+
diff --git a/ui/desktop/src/config/ConfigManager.tsx b/ui/desktop/src/config/ConfigManager.tsx deleted file mode 100644 index ba87186a8..000000000 --- a/ui/desktop/src/config/ConfigManager.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { ConfigAPI, ConfigResponse } from './api'; - -export const ConfigManager: React.FC = () => { - const [config, setConfig] = useState(null); - const [error, setError] = useState(null); - const [newKey, setNewKey] = useState(''); - const [newValue, setNewValue] = useState(''); - - useEffect(() => { - loadConfig(); - }, []); - - const loadConfig = async () => { - try { - const data = await ConfigAPI.readAllConfig(); - setConfig(data); - setError(null); - } catch (err) { - setError('Failed to load configuration'); - console.error(err); - } - }; - - const handleUpsert = async () => { - try { - await ConfigAPI.upsertConfig({ - key: newKey, - value: newValue as any, // You might want to add proper parsing here - }); - await loadConfig(); - setNewKey(''); - setNewValue(''); - setError(null); - } catch (err) { - setError('Failed to update configuration'); - console.error(err); - } - }; - - const handleRemove = async (key: string) => { - try { - await ConfigAPI.removeConfig(key); - await loadConfig(); - setError(null); - } catch (err) { - setError('Failed to remove configuration'); - console.error(err); - } - }; - - return ( -
-

Configuration Manager

- - {error && ( -
- {error} -
- )} - -
-

Add/Update Configuration

-
- setNewKey(e.target.value)} - placeholder="Key" - className="border p-2 rounded" - /> - setNewValue(e.target.value)} - placeholder="Value" - className="border p-2 rounded" - /> - -
-
- -
-

Current Configuration

- {config && ( -
- {Object.entries(config.config).map(([key, value]) => ( -
-
- {key}: {JSON.stringify(value)} -
- -
- ))} -
- )} -
-
- ); -}; diff --git a/ui/desktop/src/config/api.ts b/ui/desktop/src/config/api.ts deleted file mode 100644 index d4f4ea3c8..000000000 --- a/ui/desktop/src/config/api.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { Value } from 'yaml'; - -export interface UpsertConfigQuery { - key: string; - value: Value; - isSecret?: boolean; -} - -export interface ConfigKeyQuery { - key: string; -} - -export interface ExtensionQuery { - name: string; - config: Value; -} - -export interface ConfigResponse { - config: Record; -} - -const API_BASE = 'http://localhost:3000'; // Update this with your actual API base URL - -export class ConfigAPI { - static async readAllConfig(): Promise { - const response = await fetch(`${API_BASE}/config`); - if (!response.ok) { - throw new Error('Failed to fetch config'); - } - return response.json(); - } - - static async upsertConfig(query: UpsertConfigQuery): Promise { - const params = new URLSearchParams({ - key: query.key, - value: JSON.stringify(query.value), - ...(query.isSecret && { is_secret: String(query.isSecret) }), - }); - - const response = await fetch(`${API_BASE}/config/upsert?${params}`, { - method: 'POST', - }); - - if (!response.ok) { - throw new Error('Failed to upsert config'); - } - return response.text(); - } - - static async removeConfig(key: string): Promise { - const params = new URLSearchParams({ key }); - const response = await fetch(`${API_BASE}/config/remove?${params}`, { - method: 'DELETE', - }); - - if (!response.ok) { - throw new Error('Failed to remove config'); - } - return response.text(); - } - - static async readConfig(key: string): Promise { - const params = new URLSearchParams({ key }); - const response = await fetch(`${API_BASE}/config/read?${params}`); - - if (!response.ok) { - throw new Error('Failed to read config'); - } - return response.json(); - } - - static async addExtension(extension: ExtensionQuery): Promise { - const response = await fetch(`${API_BASE}/config/extension`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(extension), - }); - - if (!response.ok) { - throw new Error('Failed to add extension'); - } - return response.text(); - } - - static async removeExtension(name: string): Promise { - const params = new URLSearchParams({ key: name }); - const response = await fetch(`${API_BASE}/config/extension?${params}`, { - method: 'DELETE', - }); - - if (!response.ok) { - throw new Error('Failed to remove extension'); - } - return response.text(); - } -} diff --git a/ui/desktop/src/hooks/useConfig.ts b/ui/desktop/src/hooks/useConfig.ts deleted file mode 100644 index 83012ca19..000000000 --- a/ui/desktop/src/hooks/useConfig.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { useState, useCallback } from 'react'; -import { Config } from '../api/config'; -import { toast } from 'react-toastify'; - -export interface UseConfigOptions { - onError?: (error: Error) => void; - showToasts?: boolean; -} - -export function useConfig(options: UseConfigOptions = {}) { - const { onError, showToasts = true } = options; - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const handleError = useCallback( - (error: Error, message: string) => { - setError(error); - if (showToasts) { - toast.error(message); - } - if (onError) { - onError(error); - } - }, - [onError, showToasts] - ); - - const loadConfigs = useCallback(async () => { - try { - setLoading(true); - setError(null); - const configs = await Config.readAll(); - return configs; - } catch (err) { - const error = err instanceof Error ? err : new Error('Failed to load configurations'); - handleError(error, 'Failed to load configurations'); - return {}; - } finally { - setLoading(false); - } - }, [handleError]); - - const addConfig = useCallback( - async (key: string, value: any) => { - try { - setLoading(true); - setError(null); - await Config.upsert(key, value); - if (showToasts) { - toast.success(`Successfully added configuration: ${key}`); - } - return true; - } catch (err) { - const error = err instanceof Error ? err : new Error('Failed to add configuration'); - handleError(error, `Failed to add configuration: ${key}`); - return false; - } finally { - setLoading(false); - } - }, - [handleError, showToasts] - ); - - const removeConfig = useCallback( - async (key: string) => { - try { - setLoading(true); - setError(null); - await Config.remove(key); - if (showToasts) { - toast.success(`Successfully removed configuration: ${key}`); - } - return true; - } catch (err) { - const error = err instanceof Error ? err : new Error('Failed to remove configuration'); - handleError(error, `Failed to remove configuration: ${key}`); - return false; - } finally { - setLoading(false); - } - }, - [handleError, showToasts] - ); - - const readConfig = useCallback( - async (key: string) => { - try { - setLoading(true); - setError(null); - const value = await Config.read(key); - return value; - } catch (err) { - const error = err instanceof Error ? err : new Error('Failed to read configuration'); - handleError(error, `Failed to read configuration: ${key}`); - return null; - } finally { - setLoading(false); - } - }, - [handleError] - ); - - return { - loading, - error, - loadConfigs, - addConfig, - removeConfig, - readConfig, - }; -} diff --git a/ui/desktop/src/renderer.tsx b/ui/desktop/src/renderer.tsx index 8ac19a843..085bccbfa 100644 --- a/ui/desktop/src/renderer.tsx +++ b/ui/desktop/src/renderer.tsx @@ -2,6 +2,7 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; import { ModelProvider } from './components/settings/models/ModelContext'; +import { ConfigProvider } from './components/ConfigContext'; import { ErrorBoundary } from './components/ErrorBoundary'; import { ActiveKeysProvider } from './components/settings/api_keys/ActiveKeysContext'; import { patchConsoleLogging } from './utils'; @@ -10,12 +11,14 @@ patchConsoleLogging(); ReactDOM.createRoot(document.getElementById('root')!).render( - - - - - - - + + + + + + + + + );