mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-16 09:26:07 +00:00
360 lines
12 KiB
TypeScript
360 lines
12 KiB
TypeScript
'use client';
|
|
import PluginInstalledComponent, {
|
|
PluginInstalledComponentRef,
|
|
} from '@/app/home/plugins/plugin-installed/PluginInstalledComponent';
|
|
import MarketPage from '@/app/home/plugins/plugin-market/PluginMarketComponent';
|
|
// import PluginSortDialog from '@/app/home/plugins/plugin-sort/PluginSortDialog';
|
|
import styles from './plugins.module.css';
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
PlusIcon,
|
|
ChevronDownIcon,
|
|
UploadIcon,
|
|
StoreIcon,
|
|
Download,
|
|
} 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 { useState, useRef, useCallback } 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';
|
|
|
|
enum PluginInstallStatus {
|
|
WAIT_INPUT = 'wait_input',
|
|
ASK_CONFIRM = 'ask_confirm',
|
|
INSTALLING = 'installing',
|
|
ERROR = 'error',
|
|
}
|
|
|
|
export default function PluginConfigPage() {
|
|
const { t } = useTranslation();
|
|
const [modalOpen, setModalOpen] = useState(false);
|
|
// const [sortModalOpen, setSortModalOpen] = useState(false);
|
|
const [activeTab, setActiveTab] = useState('installed');
|
|
const [installSource, setInstallSource] = useState<string>('local');
|
|
const [installInfo, setInstallInfo] = useState<Record<string, any>>({}); // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
const [pluginInstallStatus, setPluginInstallStatus] =
|
|
useState<PluginInstallStatus>(PluginInstallStatus.WAIT_INPUT);
|
|
const [installError, setInstallError] = useState<string | null>(null);
|
|
const [githubURL, setGithubURL] = useState('');
|
|
const [isDragOver, setIsDragOver] = useState(false);
|
|
const pluginInstalledRef = useRef<PluginInstalledComponentRef>(null);
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
function watchTask(taskId: number) {
|
|
let alreadySuccess = false;
|
|
console.log('taskId:', taskId);
|
|
|
|
// 每秒拉取一次任务状态
|
|
const interval = setInterval(() => {
|
|
httpClient.getAsyncTask(taskId).then((resp) => {
|
|
console.log('task status:', resp);
|
|
if (resp.runtime.done) {
|
|
clearInterval(interval);
|
|
if (resp.runtime.exception) {
|
|
setInstallError(resp.runtime.exception);
|
|
setPluginInstallStatus(PluginInstallStatus.ERROR);
|
|
} else {
|
|
// success
|
|
if (!alreadySuccess) {
|
|
toast.success(t('plugins.installSuccess'));
|
|
alreadySuccess = true;
|
|
}
|
|
setGithubURL('');
|
|
setModalOpen(false);
|
|
pluginInstalledRef.current?.refreshPluginList();
|
|
}
|
|
}
|
|
});
|
|
}, 1000);
|
|
}
|
|
|
|
function handleModalConfirm() {
|
|
installPlugin(installSource, installInfo as Record<string, any>); // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
}
|
|
|
|
function installPlugin(
|
|
installSource: string,
|
|
installInfo: Record<string, any>, // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
) {
|
|
setPluginInstallStatus(PluginInstallStatus.INSTALLING);
|
|
if (installSource === 'github') {
|
|
httpClient
|
|
.installPluginFromGithub(installInfo.url)
|
|
.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 (!validateFileType(file)) {
|
|
toast.error(t('plugins.unsupportedFileType'));
|
|
return;
|
|
}
|
|
|
|
setModalOpen(true);
|
|
setPluginInstallStatus(PluginInstallStatus.INSTALLING);
|
|
setInstallError(null);
|
|
installPlugin('local', { file });
|
|
},
|
|
[t],
|
|
);
|
|
|
|
const handleFileSelect = useCallback(() => {
|
|
if (fileInputRef.current) {
|
|
fileInputRef.current.click();
|
|
}
|
|
}, []);
|
|
|
|
const handleFileChange = useCallback(
|
|
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = event.target.files?.[0];
|
|
if (file) {
|
|
uploadPluginFile(file);
|
|
}
|
|
// 清空input值,以便可以重复选择同一个文件
|
|
event.target.value = '';
|
|
},
|
|
[uploadPluginFile],
|
|
);
|
|
|
|
const handleDragOver = useCallback((event: React.DragEvent) => {
|
|
event.preventDefault();
|
|
setIsDragOver(true);
|
|
}, []);
|
|
|
|
const handleDragLeave = useCallback((event: React.DragEvent) => {
|
|
event.preventDefault();
|
|
setIsDragOver(false);
|
|
}, []);
|
|
|
|
const handleDrop = useCallback(
|
|
(event: React.DragEvent) => {
|
|
event.preventDefault();
|
|
setIsDragOver(false);
|
|
|
|
const files = Array.from(event.dataTransfer.files);
|
|
if (files.length > 0) {
|
|
uploadPluginFile(files[0]);
|
|
}
|
|
},
|
|
[uploadPluginFile],
|
|
);
|
|
|
|
return (
|
|
<div
|
|
className={`${styles.pageContainer} ${isDragOver ? 'bg-blue-50' : ''}`}
|
|
onDragOver={handleDragOver}
|
|
onDragLeave={handleDragLeave}
|
|
onDrop={handleDrop}
|
|
>
|
|
<input
|
|
ref={fileInputRef}
|
|
type="file"
|
|
accept=".lbpkg,.zip"
|
|
onChange={handleFileChange}
|
|
style={{ display: 'none' }}
|
|
/>
|
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
|
<div className="flex flex-row justify-between items-center px-[0.8rem]">
|
|
<TabsList className="shadow-md py-5 bg-[#f0f0f0]">
|
|
<TabsTrigger value="installed" className="px-6 py-4 cursor-pointer">
|
|
{t('plugins.installed')}
|
|
</TabsTrigger>
|
|
<TabsTrigger value="market" className="px-6 py-4 cursor-pointer">
|
|
{t('plugins.marketplace')}
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<div className="flex flex-row justify-end items-center">
|
|
{/* <Button
|
|
variant="outline"
|
|
className="px-6 py-4 cursor-pointer mr-2"
|
|
onClick={() => {
|
|
// setSortModalOpen(true);
|
|
}}
|
|
>
|
|
{t('plugins.arrange')}
|
|
</Button> */}
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="default" className="px-6 py-4 cursor-pointer">
|
|
<PlusIcon className="w-4 h-4" />
|
|
{t('plugins.install')}
|
|
<ChevronDownIcon className="ml-2 w-4 h-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem onClick={handleFileSelect}>
|
|
<UploadIcon className="w-4 h-4" />
|
|
{t('plugins.uploadLocal')}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
onClick={() => {
|
|
setActiveTab('market');
|
|
}}
|
|
>
|
|
<StoreIcon className="w-4 h-4" />
|
|
{t('plugins.marketplace')}
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
</div>
|
|
<TabsContent value="installed">
|
|
<PluginInstalledComponent ref={pluginInstalledRef} />
|
|
</TabsContent>
|
|
<TabsContent value="market">
|
|
<MarketPage
|
|
installPlugin={(plugin: PluginV4) => {
|
|
setInstallSource('marketplace');
|
|
setInstallInfo({
|
|
plugin_author: plugin.author,
|
|
plugin_name: plugin.name,
|
|
plugin_version: plugin.latest_version,
|
|
});
|
|
setPluginInstallStatus(PluginInstallStatus.ASK_CONFIRM);
|
|
setModalOpen(true);
|
|
}}
|
|
/>
|
|
</TabsContent>
|
|
</Tabs>
|
|
|
|
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
|
|
<DialogContent className="w-[500px] p-6">
|
|
<DialogHeader>
|
|
<DialogTitle className="flex items-center gap-4">
|
|
<Download className="size-6" />
|
|
<span>{t('plugins.installPlugin')}</span>
|
|
</DialogTitle>
|
|
</DialogHeader>
|
|
{pluginInstallStatus === PluginInstallStatus.WAIT_INPUT && (
|
|
<div className="mt-4">
|
|
<p className="mb-2">{t('plugins.onlySupportGithub')}</p>
|
|
<Input
|
|
placeholder={t('plugins.enterGithubLink')}
|
|
value={githubURL}
|
|
onChange={(e) => setGithubURL(e.target.value)}
|
|
className="mb-4"
|
|
/>
|
|
</div>
|
|
)}
|
|
{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.WAIT_INPUT ||
|
|
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>
|
|
|
|
{/* 拖拽提示覆盖层 */}
|
|
{isDragOver && (
|
|
<div className="fixed inset-0 bg-gray-500 bg-opacity-50 flex items-center justify-center z-50 pointer-events-none">
|
|
<div className="bg-white rounded-lg p-8 shadow-lg border-2 border-dashed border-gray-500">
|
|
<div className="text-center">
|
|
<UploadIcon className="mx-auto h-12 w-12 text-gray-500 mb-4" />
|
|
<p className="text-lg font-medium text-gray-700">
|
|
{t('plugins.dragToUpload')}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* <PluginSortDialog
|
|
open={sortModalOpen}
|
|
onOpenChange={setSortModalOpen}
|
|
onSortComplete={() => {
|
|
pluginInstalledRef.current?.refreshPluginList();
|
|
}}
|
|
/> */}
|
|
</div>
|
|
);
|
|
}
|