"use client" /** * Plugin Context - Manages plugin state and UI integration */ import React, { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react'; import { useAuth } from './AuthContext'; export interface PluginInfo { id: string; name: string; version: string; description: string; author: string; status: 'enabled' | 'disabled' | 'installed' | 'uninstalled'; loaded: boolean; manifest?: any; health?: any; resource_usage?: any; pages?: PluginPage[]; } export interface PluginPage { name: string; path: string; component: string; title?: string; icon?: string; requiresAuth?: boolean; } export interface PluginConfiguration { plugin_id: string; configuration: Record; schema?: any; last_updated?: string; } export interface AvailablePlugin { id: string; name: string; version: string; description: string; author: string; tags: string[]; category: string; local_status: { installed: boolean; version?: string; status?: string; update_available?: boolean; }; } interface PluginContextType { // State installedPlugins: PluginInfo[]; availablePlugins: AvailablePlugin[]; pluginConfigurations: Record; loading: boolean; error: string | null; // Actions refreshInstalledPlugins: () => Promise; searchAvailablePlugins: (query?: string, tags?: string[], category?: string) => Promise; installPlugin: (pluginId: string, version: string) => Promise; uninstallPlugin: (pluginId: string, keepData?: boolean) => Promise; enablePlugin: (pluginId: string) => Promise; disablePlugin: (pluginId: string) => Promise; loadPlugin: (pluginId: string) => Promise; unloadPlugin: (pluginId: string) => Promise; // Configuration getPluginConfiguration: (pluginId: string) => Promise; savePluginConfiguration: (pluginId: string, config: Record) => Promise; getPluginSchema: (pluginId: string) => Promise; // UI Integration getPluginPages: (pluginId: string) => PluginPage[]; isPluginPageAuthorized: (pluginId: string, pagePath: string) => boolean; getPluginComponent: (pluginId: string, componentName: string) => React.ComponentType | null; } const PluginContext = createContext(undefined); export const usePlugin = () => { const context = useContext(PluginContext); if (context === undefined) { throw new Error('usePlugin must be used within a PluginProvider'); } return context; }; interface PluginProviderProps { children: ReactNode; } export const PluginProvider: React.FC = ({ children }) => { const { user, token } = useAuth(); const [installedPlugins, setInstalledPlugins] = useState([]); const [availablePlugins, setAvailablePlugins] = useState([]); const [pluginConfigurations, setPluginConfigurations] = useState>({}); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); // Plugin component registry const [pluginComponents, setPluginComponents] = useState>>({}); const apiRequest = async (endpoint: string, options: RequestInit = {}) => { if (!token) { throw new Error('Authentication required'); } const response = await fetch(`/api/v1/plugins${endpoint}`, { ...options, headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', ...options.headers, }, }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.detail || `HTTP ${response.status}`); } return response.json(); }; const refreshInstalledPlugins = useCallback(async () => { if (!user || !token) { setError('Authentication required'); return; } try { setLoading(true); setError(null); const data = await apiRequest('/installed'); setInstalledPlugins(data.plugins); // Load configurations for installed plugins for (const plugin of data.plugins) { try { const config = await getPluginConfiguration(plugin.id); if (config) { setPluginConfigurations(prev => ({ ...prev, [plugin.id]: config })); } } catch (e) { console.warn(`Failed to load config for plugin ${plugin.id}:`, e); } } } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load installed plugins'); } finally { setLoading(false); } }, [user, token]); const searchAvailablePlugins = useCallback(async (query = '', tags: string[] = [], category = '') => { if (!user || !token) { setError('Authentication required'); return; } try { setLoading(true); setError(null); const params = new URLSearchParams(); if (query) params.append('query', query); if (tags.length > 0) params.append('tags', tags.join(',')); if (category) params.append('category', category); const data = await apiRequest(`/discover?${params.toString()}`); setAvailablePlugins(data.plugins); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to search plugins'); } finally { setLoading(false); } }, [user, token]); const installPlugin = useCallback(async (pluginId: string, version: string): Promise => { try { setLoading(true); setError(null); await apiRequest('/install', { method: 'POST', body: JSON.stringify({ plugin_id: pluginId, version: version, source: 'repository' }), }); // Refresh plugins after installation await refreshInstalledPlugins(); await searchAvailablePlugins(); // Refresh to update local status return true; } catch (err) { setError(err instanceof Error ? err.message : 'Installation failed'); return false; } finally { setLoading(false); } }, [refreshInstalledPlugins, searchAvailablePlugins]); const uninstallPlugin = async (pluginId: string, keepData = true): Promise => { try { setLoading(true); setError(null); await apiRequest(`/${pluginId}`, { method: 'DELETE', body: JSON.stringify({ keep_data: keepData }), }); // Remove from state setInstalledPlugins(prev => prev.filter(p => p.id !== pluginId)); setPluginConfigurations(prev => { const { [pluginId]: removed, ...rest } = prev; return rest; }); // Unregister components setPluginComponents(prev => { const { [pluginId]: removed, ...rest } = prev; return rest; }); return true; } catch (err) { setError(err instanceof Error ? err.message : 'Uninstallation failed'); return false; } finally { setLoading(false); } }; const enablePlugin = async (pluginId: string): Promise => { try { await apiRequest(`/${pluginId}/enable`, { method: 'POST' }); // Update plugin status setInstalledPlugins(prev => prev.map(p => p.id === pluginId ? { ...p, status: 'enabled' } : p) ); return true; } catch (err) { setError(err instanceof Error ? err.message : 'Enable failed'); return false; } }; const disablePlugin = async (pluginId: string): Promise => { try { await apiRequest(`/${pluginId}/disable`, { method: 'POST' }); // Update plugin status setInstalledPlugins(prev => prev.map(p => p.id === pluginId ? { ...p, status: 'disabled', loaded: false } : p) ); return true; } catch (err) { setError(err instanceof Error ? err.message : 'Disable failed'); return false; } }; const loadPlugin = async (pluginId: string): Promise => { try { await apiRequest(`/${pluginId}/load`, { method: 'POST' }); // Update plugin status setInstalledPlugins(prev => prev.map(p => p.id === pluginId ? { ...p, loaded: true } : p) ); // Load plugin UI components await loadPluginComponents(pluginId); return true; } catch (err) { setError(err instanceof Error ? err.message : 'Load failed'); return false; } }; const unloadPlugin = async (pluginId: string): Promise => { try { await apiRequest(`/${pluginId}/unload`, { method: 'POST' }); // Update plugin status setInstalledPlugins(prev => prev.map(p => p.id === pluginId ? { ...p, loaded: false } : p) ); // Unregister components setPluginComponents(prev => { const { [pluginId]: removed, ...rest } = prev; return rest; }); return true; } catch (err) { setError(err instanceof Error ? err.message : 'Unload failed'); return false; } }; const getPluginConfiguration = async (pluginId: string): Promise => { try { const data = await apiRequest(`/${pluginId}/config`); return data; } catch (err) { console.error(`Failed to get configuration for plugin ${pluginId}:`, err); return null; } }; const savePluginConfiguration = async (pluginId: string, config: Record): Promise => { try { await apiRequest(`/${pluginId}/config`, { method: 'POST', body: JSON.stringify({ configuration: config }), }); // Update local state setPluginConfigurations(prev => ({ ...prev, [pluginId]: { plugin_id: pluginId, configuration: config, last_updated: new Date().toISOString() } })); return true; } catch (err) { setError(err instanceof Error ? err.message : 'Failed to save configuration'); return false; } }; const getPluginSchema = async (pluginId: string): Promise => { try { // Add cache-busting timestamp to force fresh schema fetch const cacheBust = Date.now(); const data = await apiRequest(`/${pluginId}/schema?t=${cacheBust}`); let schema = data.schema; // For certain plugins, we need to populate dynamic options // Find the plugin by ID to get its name const plugin = installedPlugins.find(p => p.id === pluginId); const pluginName = plugin?.name?.toLowerCase(); if (schema && pluginName === 'zammad') { // Populate chatbot options for Zammad try { const chatbotsResponse = await fetch('/api/v1/chatbot/list', { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', } }); if (chatbotsResponse.ok) { const chatbotsData = await chatbotsResponse.json(); const chatbots = chatbotsData.chatbots || []; if (schema.properties?.chatbot_id) { schema.properties.chatbot_id.type = 'select'; schema.properties.chatbot_id.options = chatbots.map((chatbot: any) => ({ value: chatbot.id, label: `${chatbot.name} (${chatbot.chatbot_type})` })); } } } catch (chatbotError) { console.warn('Failed to load chatbots for Zammad configuration:', chatbotError); } // Populate model options for AI settings try { const modelsResponse = await fetch('/api/v1/llm/models', { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', } }); if (modelsResponse.ok) { const modelsData = await modelsResponse.json(); const models = modelsData.data || []; const modelOptions = models.map((model: any) => ({ value: model.id, label: model.id })); // Set model options for AI summarization if (schema.properties?.ai_summarization?.properties?.model) { schema.properties.ai_summarization.properties.model.type = 'select'; schema.properties.ai_summarization.properties.model.options = modelOptions; } // Set model options for draft settings if (schema.properties?.draft_settings?.properties?.model) { schema.properties.draft_settings.properties.model.type = 'select'; schema.properties.draft_settings.properties.model.options = modelOptions; } } } catch (modelError) { console.warn('Failed to load models for Zammad configuration:', modelError); } } if (schema && pluginName === 'signal') { // Populate model options for Signal bot try { const modelsResponse = await fetch('/api/v1/llm/models', { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', } }); if (modelsResponse.ok) { const modelsData = await modelsResponse.json(); const models = modelsData.models || []; if (schema.properties?.model) { schema.properties.model.options = models.map((model: any) => ({ value: model.id, label: model.name || model.id })); } } } catch (modelError) { console.warn('Failed to load models for Signal configuration:', modelError); } } return schema; } catch (err) { console.error(`Failed to get schema for plugin ${pluginId}:`, err); return null; } }; const loadPluginComponents = async (pluginId: string) => { try { // Load plugin UI components dynamically // This would involve loading the plugin's built JavaScript bundle // For now, we'll use a placeholder system const plugin = installedPlugins.find(p => p.id === pluginId); if (!plugin || !plugin.manifest) return; const uiConfig = plugin.manifest.spec?.ui_config; if (!uiConfig) return; // Register placeholder components for now const components: Record = {}; if (uiConfig.pages) { for (const page of uiConfig.pages) { components[page.component] = createPluginComponent(pluginId, page.component); } } setPluginComponents(prev => ({ ...prev, [pluginId]: components })); } catch (err) { console.error(`Failed to load components for plugin ${pluginId}:`, err); } }; const createPluginComponent = (pluginId: string, componentName: string): React.ComponentType => { return () => (

Plugin Component: {componentName}

Plugin: {pluginId}

This is a placeholder for the plugin component that would be loaded dynamically.

); }; const getPluginPages = (pluginId: string): PluginPage[] => { const plugin = installedPlugins.find(p => p.id === pluginId); if (!plugin || !plugin.manifest) return []; const uiConfig = plugin.manifest.spec?.ui_config; return uiConfig?.pages || []; }; const isPluginPageAuthorized = (pluginId: string, pagePath: string): boolean => { // TODO: Implement authorization logic based on user permissions const plugin = installedPlugins.find(p => p.id === pluginId); return plugin?.status === 'enabled' && plugin?.loaded; }; const getPluginComponent = (pluginId: string, componentName: string): React.ComponentType | null => { return pluginComponents[pluginId]?.[componentName] || null; }; // Load installed plugins on mount, but only when authenticated useEffect(() => { if (user && token) { refreshInstalledPlugins(); } else { // Clear plugin data when not authenticated setInstalledPlugins([]); setAvailablePlugins([]); setPluginConfigurations({}); setError(null); } }, [user, token, refreshInstalledPlugins]); const value: PluginContextType = { // State installedPlugins, availablePlugins, pluginConfigurations, loading, error, // Actions refreshInstalledPlugins, searchAvailablePlugins, installPlugin, uninstallPlugin, enablePlugin, disablePlugin, loadPlugin, unloadPlugin, // Configuration getPluginConfiguration, savePluginConfiguration, getPluginSchema, // UI Integration getPluginPages, isPluginPageAuthorized, getPluginComponent, }; return ( {children} ); };