'use client'; import PluginInstalledComponent, { PluginInstalledComponentRef, } from '@/app/home/plugins/components/plugin-installed/PluginInstalledComponent'; import MarketPage from '@/app/home/plugins/components/plugin-market/PluginMarketComponent'; import MCPServerComponent from '@/app/home/plugins/mcp-server/MCPServerComponent'; import MCPFormDialog from '@/app/home/plugins/mcp-server/mcp-form/MCPFormDialog'; import MCPDeleteConfirmDialog from '@/app/home/plugins/mcp-server/mcp-form/MCPDeleteConfirmDialog'; import styles from './plugins.module.css'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Button } from '@/components/ui/button'; import { Card, CardHeader, CardTitle, CardDescription, } from '@/components/ui/card'; import { PlusIcon, ChevronDownIcon, UploadIcon, StoreIcon, Download, Power, Github, ChevronLeft, } from 'lucide-react'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import React, { useState, useRef, useCallback, useEffect } from 'react'; import { httpClient } from '@/app/infra/http/HttpClient'; import { toast } from 'sonner'; import { useTranslation } from 'react-i18next'; import { PluginV4 } from '@/app/infra/entities/plugin'; import { systemInfo } from '@/app/infra/http/HttpClient'; import { ApiRespPluginSystemStatus } from '@/app/infra/entities/api'; enum PluginInstallStatus { WAIT_INPUT = 'wait_input', SELECT_RELEASE = 'select_release', SELECT_ASSET = 'select_asset', ASK_CONFIRM = 'ask_confirm', INSTALLING = 'installing', ERROR = 'error', } interface GithubRelease { id: number; tag_name: string; name: string; published_at: string; prerelease: boolean; draft: boolean; } interface GithubAsset { id: number; name: string; size: number; download_url: string; content_type: string; } export default function PluginConfigPage() { const { t } = useTranslation(); const [activeTab, setActiveTab] = useState('installed'); const [modalOpen, setModalOpen] = useState(false); const [installSource, setInstallSource] = useState('local'); const [installInfo, setInstallInfo] = useState>({}); // eslint-disable-line @typescript-eslint/no-explicit-any const [mcpSSEModalOpen, setMcpSSEModalOpen] = useState(false); const [pluginInstallStatus, setPluginInstallStatus] = useState(PluginInstallStatus.WAIT_INPUT); const [installError, setInstallError] = useState(null); const [githubURL, setGithubURL] = useState(''); const [githubReleases, setGithubReleases] = useState([]); const [selectedRelease, setSelectedRelease] = useState( null, ); const [githubAssets, setGithubAssets] = useState([]); const [selectedAsset, setSelectedAsset] = useState(null); const [githubOwner, setGithubOwner] = useState(''); const [githubRepo, setGithubRepo] = useState(''); const [fetchingReleases, setFetchingReleases] = useState(false); const [fetchingAssets, setFetchingAssets] = useState(false); const [isDragOver, setIsDragOver] = useState(false); const [pluginSystemStatus, setPluginSystemStatus] = useState(null); const [statusLoading, setStatusLoading] = useState(true); const fileInputRef = useRef(null); const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false); const [editingServerName, setEditingServerName] = useState( null, ); const [isEditMode, setIsEditMode] = useState(false); const [refreshKey, setRefreshKey] = useState(0); useEffect(() => { const fetchPluginSystemStatus = async () => { try { setStatusLoading(true); const status = await httpClient.getPluginSystemStatus(); setPluginSystemStatus(status); } catch (error) { console.error('Failed to fetch plugin system status:', error); toast.error(t('plugins.failedToGetStatus')); } finally { setStatusLoading(false); } }; fetchPluginSystemStatus(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); function formatFileSize(bytes: number): string { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i]; } function watchTask(taskId: number) { let alreadySuccess = false; const interval = setInterval(() => { httpClient.getAsyncTask(taskId).then((resp) => { if (resp.runtime.done) { clearInterval(interval); if (resp.runtime.exception) { setInstallError(resp.runtime.exception); setPluginInstallStatus(PluginInstallStatus.ERROR); } else { if (!alreadySuccess) { toast.success(t('plugins.installSuccess')); alreadySuccess = true; } resetGithubState(); setModalOpen(false); pluginInstalledRef.current?.refreshPluginList(); } } }); }, 1000); } const pluginInstalledRef = useRef(null); function resetGithubState() { setGithubURL(''); setGithubReleases([]); setSelectedRelease(null); setGithubAssets([]); setSelectedAsset(null); setGithubOwner(''); setGithubRepo(''); setFetchingReleases(false); setFetchingAssets(false); } async function fetchGithubReleases() { if (!githubURL.trim()) { toast.error(t('plugins.enterRepoUrl')); return; } setFetchingReleases(true); setInstallError(null); try { const result = await httpClient.getGithubReleases(githubURL); setGithubReleases(result.releases); setGithubOwner(result.owner); setGithubRepo(result.repo); if (result.releases.length === 0) { toast.warning(t('plugins.noReleasesFound')); } else { setPluginInstallStatus(PluginInstallStatus.SELECT_RELEASE); } } catch (error: unknown) { console.error('Failed to fetch GitHub releases:', error); const errorMessage = error instanceof Error ? error.message : String(error); setInstallError(errorMessage || t('plugins.fetchReleasesError')); setPluginInstallStatus(PluginInstallStatus.ERROR); } finally { setFetchingReleases(false); } } async function handleReleaseSelect(release: GithubRelease) { setSelectedRelease(release); setFetchingAssets(true); setInstallError(null); try { const result = await httpClient.getGithubReleaseAssets( githubOwner, githubRepo, release.id, ); setGithubAssets(result.assets); if (result.assets.length === 0) { toast.warning(t('plugins.noAssetsFound')); } else { setPluginInstallStatus(PluginInstallStatus.SELECT_ASSET); } } catch (error: unknown) { console.error('Failed to fetch GitHub release assets:', error); const errorMessage = error instanceof Error ? error.message : String(error); setInstallError(errorMessage || t('plugins.fetchAssetsError')); setPluginInstallStatus(PluginInstallStatus.ERROR); } finally { setFetchingAssets(false); } } function handleAssetSelect(asset: GithubAsset) { setSelectedAsset(asset); setPluginInstallStatus(PluginInstallStatus.ASK_CONFIRM); } function handleModalConfirm() { if (installSource === 'github' && selectedAsset && selectedRelease) { installPlugin('github', { asset_url: selectedAsset.download_url, owner: githubOwner, repo: githubRepo, release_tag: selectedRelease.tag_name, }); } else { installPlugin(installSource, installInfo as Record); // eslint-disable-line @typescript-eslint/no-explicit-any } } function installPlugin( installSource: string, installInfo: Record, // eslint-disable-line @typescript-eslint/no-explicit-any ) { setPluginInstallStatus(PluginInstallStatus.INSTALLING); if (installSource === 'github') { httpClient .installPluginFromGithub( installInfo.asset_url, installInfo.owner, installInfo.repo, installInfo.release_tag, ) .then((resp) => { const taskId = resp.task_id; watchTask(taskId); }) .catch((err) => { console.log('error when install plugin:', err); setInstallError(err.message); setPluginInstallStatus(PluginInstallStatus.ERROR); }); } else if (installSource === 'local') { httpClient .installPluginFromLocal(installInfo.file) .then((resp) => { const taskId = resp.task_id; watchTask(taskId); }) .catch((err) => { console.log('error when install plugin:', err); setInstallError(err.message); setPluginInstallStatus(PluginInstallStatus.ERROR); }); } else if (installSource === 'marketplace') { httpClient .installPluginFromMarketplace( installInfo.plugin_author, installInfo.plugin_name, installInfo.plugin_version, ) .then((resp) => { const taskId = resp.task_id; watchTask(taskId); }); } } const validateFileType = (file: File): boolean => { const allowedExtensions = ['.lbpkg', '.zip']; const fileName = file.name.toLowerCase(); return allowedExtensions.some((ext) => fileName.endsWith(ext)); }; const uploadPluginFile = useCallback( async (file: File) => { if (!pluginSystemStatus?.is_enable || !pluginSystemStatus?.is_connected) { toast.error(t('plugins.pluginSystemNotReady')); return; } if (!validateFileType(file)) { toast.error(t('plugins.unsupportedFileType')); return; } setModalOpen(true); setPluginInstallStatus(PluginInstallStatus.INSTALLING); setInstallError(null); installPlugin('local', { file }); }, [t, pluginSystemStatus, installPlugin], ); const handleFileSelect = useCallback(() => { if (fileInputRef.current) { fileInputRef.current.click(); } }, []); const handleFileChange = useCallback( (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (file) { uploadPluginFile(file); } event.target.value = ''; }, [uploadPluginFile], ); const isPluginSystemReady = pluginSystemStatus?.is_enable && pluginSystemStatus?.is_connected; const handleDragOver = useCallback( (event: React.DragEvent) => { event.preventDefault(); if (isPluginSystemReady) { setIsDragOver(true); } }, [isPluginSystemReady], ); const handleDragLeave = useCallback((event: React.DragEvent) => { event.preventDefault(); setIsDragOver(false); }, []); const handleDrop = useCallback( (event: React.DragEvent) => { event.preventDefault(); setIsDragOver(false); if (!isPluginSystemReady) { toast.error(t('plugins.pluginSystemNotReady')); return; } const files = Array.from(event.dataTransfer.files); if (files.length > 0) { uploadPluginFile(files[0]); } }, [uploadPluginFile, isPluginSystemReady, t], ); const renderPluginDisabledState = () => (

{t('plugins.systemDisabled')}

{t('plugins.systemDisabledDesc')}

); const renderPluginConnectionErrorState = () => (

{t('plugins.connectionError')}

{t('plugins.connectionErrorDesc')}

); const renderLoadingState = () => (

{t('plugins.loadingStatus')}

); if (statusLoading) { return renderLoadingState(); } if (!pluginSystemStatus?.is_enable) { return renderPluginDisabledState(); } if (!pluginSystemStatus?.is_connected) { return renderPluginConnectionErrorState(); } return (
{t('plugins.installed')} {systemInfo.enable_marketplace && ( {t('plugins.marketplace')} )} {t('mcp.title')}
{activeTab === 'mcp-servers' ? ( <> { setActiveTab('mcp-servers'); setIsEditMode(false); setEditingServerName(null); setMcpSSEModalOpen(true); }} > {t('mcp.createServer')} ) : ( <> {systemInfo.enable_marketplace && ( { setActiveTab('market'); }} > {t('plugins.marketplace')} )} {t('plugins.uploadLocal')} { setInstallSource('github'); setPluginInstallStatus(PluginInstallStatus.WAIT_INPUT); setInstallError(null); resetGithubState(); setModalOpen(true); }} > {t('plugins.installFromGithub')} )}
{ setInstallSource('marketplace'); setInstallInfo({ plugin_author: plugin.author, plugin_name: plugin.name, plugin_version: plugin.latest_version, }); setPluginInstallStatus(PluginInstallStatus.ASK_CONFIRM); setModalOpen(true); }} /> { setEditingServerName(serverName); setIsEditMode(true); setMcpSSEModalOpen(true); }} />
{ setModalOpen(open); if (!open) { resetGithubState(); setInstallError(null); } }} > {installSource === 'github' ? ( ) : ( )} {t('plugins.installPlugin')} {/* GitHub Install Flow */} {installSource === 'github' && pluginInstallStatus === PluginInstallStatus.WAIT_INPUT && (

{t('plugins.enterRepoUrl')}

setGithubURL(e.target.value)} className="mb-4" /> {fetchingReleases && (

{t('plugins.fetchingReleases')}

)}
)} {installSource === 'github' && pluginInstallStatus === PluginInstallStatus.SELECT_RELEASE && (

{t('plugins.selectRelease')}

{githubReleases.map((release) => ( handleReleaseSelect(release)} >
{release.name || release.tag_name} {t('plugins.releaseTag', { tag: release.tag_name })}{' '} •{' '} {t('plugins.publishedAt', { date: new Date( release.published_at, ).toLocaleDateString(), })}
{release.prerelease && ( {t('plugins.prerelease')} )}
))}
{fetchingAssets && (

{t('plugins.loading')}

)}
)} {installSource === 'github' && pluginInstallStatus === PluginInstallStatus.SELECT_ASSET && (

{t('plugins.selectAsset')}

{selectedRelease && (
{selectedRelease.name || selectedRelease.tag_name}
{selectedRelease.tag_name}
)}
{githubAssets.map((asset) => ( handleAssetSelect(asset)} > {asset.name} {t('plugins.assetSize', { size: formatFileSize(asset.size), })} ))}
)} {/* Marketplace Install Confirm */} {installSource === 'marketplace' && pluginInstallStatus === PluginInstallStatus.ASK_CONFIRM && (

{t('plugins.askConfirm', { name: installInfo.plugin_name, version: installInfo.plugin_version, })}

)} {/* GitHub Install Confirm */} {installSource === 'github' && pluginInstallStatus === PluginInstallStatus.ASK_CONFIRM && (

{t('plugins.confirmInstall')}

{selectedRelease && selectedAsset && (
Repository: {githubOwner}/{githubRepo}
Release: {selectedRelease.tag_name}
File: {selectedAsset.name}
)}
)} {/* Installing State */} {pluginInstallStatus === PluginInstallStatus.INSTALLING && (

{t('plugins.installing')}

)} {/* Error State */} {pluginInstallStatus === PluginInstallStatus.ERROR && (

{t('plugins.installFailed')}

{installError}

)} {pluginInstallStatus === PluginInstallStatus.WAIT_INPUT && installSource === 'github' && ( <> )} {pluginInstallStatus === PluginInstallStatus.ASK_CONFIRM && ( <> )} {pluginInstallStatus === PluginInstallStatus.ERROR && ( )}
{isDragOver && (

{t('plugins.dragToUpload')}

)} { setEditingServerName(null); setIsEditMode(false); setRefreshKey((prev) => prev + 1); }} onDelete={() => { setShowDeleteConfirmModal(true); }} /> { setMcpSSEModalOpen(false); setEditingServerName(null); setIsEditMode(false); setRefreshKey((prev) => prev + 1); }} />
); }