mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-25 03:27:15 +00:00
bc3199bf29
- Bot adapter selector: show adapter icon in trigger and dropdown items - Knowledge engine selector: show plugin icon derived from plugin_id - Pipeline binding selector: show pipeline emoji in trigger and dropdown items - Knowledge base selectors (single/multi): show KB emoji in all views - Sidebar bot entries: show green/gray status dot on adapter icon for enable/disable state - Sidebar plugin list: sync after install/uninstall from all entry points (PluginInstalledComponent, plugins page, marketplace page) - Pipeline form: add cursor-pointer to left-side tab list buttons - Clean up unused onBotDeleted prop from BotForm
197 lines
6.0 KiB
TypeScript
197 lines
6.0 KiB
TypeScript
'use client';
|
|
|
|
import MarketPage from '@/app/home/plugins/components/plugin-market/PluginMarketComponent';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogFooter,
|
|
} from '@/components/ui/dialog';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Download } from 'lucide-react';
|
|
import React, { useState, useCallback } from 'react';
|
|
import { httpClient } from '@/app/infra/http/HttpClient';
|
|
import { systemInfo } from '@/app/infra/http/HttpClient';
|
|
import { toast } from 'sonner';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { PluginV4 } from '@/app/infra/entities/plugin';
|
|
import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext';
|
|
|
|
enum PluginInstallStatus {
|
|
ASK_CONFIRM = 'ask_confirm',
|
|
INSTALLING = 'installing',
|
|
ERROR = 'error',
|
|
}
|
|
|
|
export default function MarketplacePage() {
|
|
const { t } = useTranslation();
|
|
|
|
if (!systemInfo?.enable_marketplace) {
|
|
return (
|
|
<div className="flex flex-col items-center justify-center h-[60vh] text-center">
|
|
<p className="text-muted-foreground">{t('plugins.marketplace')}</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return <MarketplaceContent />;
|
|
}
|
|
|
|
function MarketplaceContent() {
|
|
const { t } = useTranslation();
|
|
const { refreshPlugins } = useSidebarData();
|
|
const [modalOpen, setModalOpen] = useState(false);
|
|
const [installInfo, setInstallInfo] = useState<Record<string, string>>({});
|
|
const [pluginInstallStatus, setPluginInstallStatus] =
|
|
useState<PluginInstallStatus>(PluginInstallStatus.ASK_CONFIRM);
|
|
const [installError, setInstallError] = useState<string | null>(null);
|
|
|
|
async function checkExtensionsLimit(): Promise<boolean> {
|
|
const maxExtensions = systemInfo.limitation?.max_extensions ?? -1;
|
|
if (maxExtensions < 0) return true;
|
|
try {
|
|
const [pluginsResp, mcpResp] = await Promise.all([
|
|
httpClient.getPlugins(),
|
|
httpClient.getMCPServers(),
|
|
]);
|
|
const total =
|
|
(pluginsResp.plugins?.length ?? 0) + (mcpResp.servers?.length ?? 0);
|
|
if (total >= maxExtensions) {
|
|
toast.error(
|
|
t('limitation.maxExtensionsReached', { max: maxExtensions }),
|
|
);
|
|
return false;
|
|
}
|
|
} catch {
|
|
// If we can't check, let backend handle it
|
|
}
|
|
return true;
|
|
}
|
|
|
|
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;
|
|
}
|
|
setModalOpen(false);
|
|
refreshPlugins();
|
|
}
|
|
}
|
|
});
|
|
}, 1000);
|
|
}
|
|
|
|
const handleInstallPlugin = useCallback(
|
|
async (plugin: PluginV4) => {
|
|
if (!(await checkExtensionsLimit())) return;
|
|
setInstallInfo({
|
|
plugin_author: plugin.author,
|
|
plugin_name: plugin.name,
|
|
plugin_version: plugin.latest_version,
|
|
});
|
|
setPluginInstallStatus(PluginInstallStatus.ASK_CONFIRM);
|
|
setInstallError(null);
|
|
setModalOpen(true);
|
|
},
|
|
[t],
|
|
);
|
|
|
|
function handleModalConfirm() {
|
|
setPluginInstallStatus(PluginInstallStatus.INSTALLING);
|
|
httpClient
|
|
.installPluginFromMarketplace(
|
|
installInfo.plugin_author,
|
|
installInfo.plugin_name,
|
|
installInfo.plugin_version,
|
|
)
|
|
.then((resp) => {
|
|
const taskId = resp.task_id;
|
|
watchTask(taskId);
|
|
})
|
|
.catch((err) => {
|
|
setInstallError(err.msg);
|
|
setPluginInstallStatus(PluginInstallStatus.ERROR);
|
|
});
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<div className="h-full overflow-y-auto">
|
|
<MarketPage installPlugin={handleInstallPlugin} />
|
|
</div>
|
|
|
|
<Dialog
|
|
open={modalOpen}
|
|
onOpenChange={(open) => {
|
|
setModalOpen(open);
|
|
if (!open) {
|
|
setInstallError(null);
|
|
}
|
|
}}
|
|
>
|
|
<DialogContent className="w-[500px] max-h-[80vh] p-6 bg-white dark:bg-[#1a1a1e] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle className="flex items-center gap-4">
|
|
<Download className="size-6" />
|
|
<span>{t('plugins.installPlugin')}</span>
|
|
</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
{pluginInstallStatus === PluginInstallStatus.ASK_CONFIRM && (
|
|
<div className="mt-4">
|
|
<p className="mb-2">
|
|
{t('plugins.askConfirm', {
|
|
name: installInfo.plugin_name,
|
|
version: installInfo.plugin_version,
|
|
})}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{pluginInstallStatus === PluginInstallStatus.INSTALLING && (
|
|
<div className="mt-4">
|
|
<p className="mb-2">{t('plugins.installing')}</p>
|
|
</div>
|
|
)}
|
|
|
|
{pluginInstallStatus === PluginInstallStatus.ERROR && (
|
|
<div className="mt-4">
|
|
<p className="mb-2">{t('plugins.installFailed')}</p>
|
|
<p className="mb-2 text-red-500">{installError}</p>
|
|
</div>
|
|
)}
|
|
|
|
<DialogFooter>
|
|
{pluginInstallStatus === PluginInstallStatus.ASK_CONFIRM && (
|
|
<>
|
|
<Button variant="outline" onClick={() => setModalOpen(false)}>
|
|
{t('common.cancel')}
|
|
</Button>
|
|
<Button onClick={handleModalConfirm}>
|
|
{t('common.confirm')}
|
|
</Button>
|
|
</>
|
|
)}
|
|
{pluginInstallStatus === PluginInstallStatus.ERROR && (
|
|
<Button variant="default" onClick={() => setModalOpen(false)}>
|
|
{t('common.close')}
|
|
</Button>
|
|
)}
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</>
|
|
);
|
|
}
|