mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-19 06:30:10 +08:00
fix: plugins related wip
This commit is contained in:
parent
bd4a979f62
commit
468aebd632
@ -1,6 +1,7 @@
|
||||
import { Button, Chip, Skeleton, Table, TableBody, TableCell, TableColumn, TableHeader, TableRow } from '@heroui/react'
|
||||
import type { InstalledPlugin } from '@renderer/types/plugin'
|
||||
import { Trash2 } from 'lucide-react'
|
||||
import type { TableProps } from 'antd'
|
||||
import { Button, Skeleton, Table as AntTable, Tag } from 'antd'
|
||||
import { Dot, Trash2 } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -33,10 +34,10 @@ export const InstalledPluginsList: FC<InstalledPluginsListProps> = ({ plugins, o
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-12 w-full rounded-lg" />
|
||||
<Skeleton className="h-12 w-full rounded-lg" />
|
||||
<Skeleton className="h-12 w-full rounded-lg" />
|
||||
<div className="flex flex-col space-y-2">
|
||||
<Skeleton.Input active className="w-full" size={'large'} style={{ width: '100%' }} />
|
||||
<Skeleton.Input active className="w-full" size={'large'} style={{ width: '100%' }} />
|
||||
<Skeleton.Input active className="w-full" size={'large'} style={{ width: '100%' }} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -50,50 +51,59 @@ export const InstalledPluginsList: FC<InstalledPluginsListProps> = ({ plugins, o
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Table aria-label="Installed plugins table" removeWrapper>
|
||||
<TableHeader>
|
||||
<TableColumn>{t('plugins.name')}</TableColumn>
|
||||
<TableColumn>{t('plugins.type')}</TableColumn>
|
||||
<TableColumn>{t('plugins.category')}</TableColumn>
|
||||
<TableColumn align="end">{t('plugins.actions')}</TableColumn>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{plugins.map((plugin) => (
|
||||
<TableRow key={plugin.filename}>
|
||||
<TableCell>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-semibold text-small">{plugin.metadata.name}</span>
|
||||
{plugin.metadata.description && (
|
||||
<span className="line-clamp-1 text-default-400 text-tiny">{plugin.metadata.description}</span>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip size="sm" variant="flat" color={plugin.type === 'agent' ? 'primary' : 'secondary'}>
|
||||
{plugin.type}
|
||||
</Chip>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip size="sm" variant="dot">
|
||||
{plugin.metadata.category}
|
||||
</Chip>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
size="sm"
|
||||
color="danger"
|
||||
variant="light"
|
||||
isIconOnly
|
||||
onPress={() => handleUninstall(plugin)}
|
||||
isLoading={uninstallingPlugin === plugin.filename}
|
||||
isDisabled={loading}>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)
|
||||
const columns: TableProps<InstalledPlugin>['columns'] = [
|
||||
{
|
||||
title: t('plugins.name'),
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
render: (_: any, plugin: InstalledPlugin) => (
|
||||
<div className="flex flex-col">
|
||||
<span className="font-semibold text-small">{plugin.metadata.name}</span>
|
||||
{plugin.metadata.description && (
|
||||
<span className="line-clamp-1 text-default-400 text-tiny">{plugin.metadata.description}</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('plugins.type'),
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
render: (type: string) => <Tag color={type === 'agent' ? 'magenta' : 'purple'}>{type}</Tag>
|
||||
},
|
||||
{
|
||||
title: t('plugins.category'),
|
||||
dataIndex: 'category',
|
||||
key: 'category',
|
||||
render: (_: any, plugin: InstalledPlugin) => (
|
||||
<Tag
|
||||
icon={<Dot size={14} strokeWidth={8} />}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: '2px'
|
||||
}}>
|
||||
{plugin.metadata.category}
|
||||
</Tag>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('plugins.actions'),
|
||||
key: 'actions',
|
||||
align: 'right' as const,
|
||||
render: (_: any, plugin: InstalledPlugin) => (
|
||||
<Button
|
||||
danger
|
||||
type="text"
|
||||
onClick={() => handleUninstall(plugin)}
|
||||
loading={uninstallingPlugin === plugin.filename}
|
||||
disabled={loading}
|
||||
icon={<Trash2 className="h-4 w-4" />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
return <AntTable columns={columns} dataSource={plugins} />
|
||||
}
|
||||
|
||||
@ -1,16 +1,6 @@
|
||||
import {
|
||||
Button,
|
||||
Chip,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
Spinner,
|
||||
Textarea
|
||||
} from '@heroui/react'
|
||||
import type { PluginMetadata } from '@renderer/types/plugin'
|
||||
import { Download, Edit, Save, Trash2, X } from 'lucide-react'
|
||||
import { Button as AntButton, Input, Modal as AntdModal, Spin, Tag } from 'antd'
|
||||
import { Dot, Download, Edit, Save, Trash2, X } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
@ -121,200 +111,203 @@ export const PluginDetailModal: FC<PluginDetailModalProps> = ({
|
||||
if (!plugin) return null
|
||||
|
||||
const modalContent = (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
size="2xl"
|
||||
scrollBehavior="inside"
|
||||
classNames={{
|
||||
wrapper: 'z-[9999]'
|
||||
}}>
|
||||
<ModalContent>
|
||||
<ModalHeader className="flex flex-col gap-1">
|
||||
<AntdModal
|
||||
centered
|
||||
open={isOpen}
|
||||
onCancel={onClose}
|
||||
styles={{
|
||||
body: {
|
||||
maxHeight: '60vh',
|
||||
overflowY: 'auto'
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
width: '70%'
|
||||
}}
|
||||
title={
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="font-bold text-xl">{plugin.name}</h2>
|
||||
<Chip size="sm" variant="solid" color={plugin.type === 'agent' ? 'primary' : 'secondary'}>
|
||||
{plugin.type}
|
||||
</Chip>
|
||||
<Tag color={plugin.type === 'agent' ? 'magenta' : 'purple'}>{plugin.type}</Tag>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Chip size="sm" variant="dot" color="default">
|
||||
<Tag
|
||||
icon={<Dot size={14} strokeWidth={8} />}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: '2px'
|
||||
}}>
|
||||
{plugin.category}
|
||||
</Chip>
|
||||
{plugin.version && (
|
||||
<Chip size="sm" variant="bordered">
|
||||
v{plugin.version}
|
||||
</Chip>
|
||||
)}
|
||||
</Tag>
|
||||
{plugin.version && <Tag>v{plugin.version}</Tag>}
|
||||
</div>
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{/* Description */}
|
||||
{plugin.description && (
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-2 font-semibold text-small">Description</h3>
|
||||
<p className="text-default-600 text-small">{plugin.description}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Author */}
|
||||
{plugin.author && (
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-2 font-semibold text-small">Author</h3>
|
||||
<p className="text-default-600 text-small">{plugin.author}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tools (for agents) */}
|
||||
{plugin.tools && plugin.tools.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-2 font-semibold text-small">Tools</h3>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{plugin.tools.map((tool) => (
|
||||
<Chip key={tool} size="sm" variant="flat">
|
||||
{tool}
|
||||
</Chip>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Allowed Tools (for commands) */}
|
||||
{plugin.allowed_tools && plugin.allowed_tools.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-2 font-semibold text-small">Allowed Tools</h3>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{plugin.allowed_tools.map((tool) => (
|
||||
<Chip key={tool} size="sm" variant="flat">
|
||||
{tool}
|
||||
</Chip>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tags */}
|
||||
{plugin.tags && plugin.tags.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-2 font-semibold text-small">Tags</h3>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{plugin.tags.map((tag) => (
|
||||
<Chip key={tag} size="sm" variant="bordered">
|
||||
{tag}
|
||||
</Chip>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Metadata */}
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-2 font-semibold text-small">Metadata</h3>
|
||||
<div className="space-y-1 text-small">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-default-500">File:</span>
|
||||
<span className="font-mono text-default-600 text-tiny">{plugin.filename}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-default-500">Size:</span>
|
||||
<span className="text-default-600">{(plugin.size / 1024).toFixed(2)} KB</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-default-500">Source:</span>
|
||||
<span className="font-mono text-default-600 text-tiny">{plugin.sourcePath}</span>
|
||||
</div>
|
||||
{plugin.installedAt && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-default-500">Installed:</span>
|
||||
<span className="text-default-600">{new Date(plugin.installedAt).toLocaleString()}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="mb-4">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<h3 className="font-semibold text-small">Content</h3>
|
||||
{installed && !contentLoading && !contentError && (
|
||||
<div className="flex gap-2">
|
||||
{isEditing ? (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
color="danger"
|
||||
startContent={<X className="h-3 w-3" />}
|
||||
onPress={handleCancelEdit}
|
||||
isDisabled={saving}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
color="primary"
|
||||
startContent={saving ? <Spinner size="sm" color="current" /> : <Save className="h-3 w-3" />}
|
||||
onPress={handleSave}
|
||||
isDisabled={saving}>
|
||||
{saving ? 'Saving...' : 'Save'}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button size="sm" variant="flat" startContent={<Edit className="h-3 w-3" />} onPress={handleEdit}>
|
||||
Edit
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{contentLoading ? (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<Spinner size="sm" />
|
||||
</div>
|
||||
) : contentError ? (
|
||||
<div className="rounded-md bg-danger-50 p-3 text-danger text-small">{contentError}</div>
|
||||
) : isEditing ? (
|
||||
<Textarea
|
||||
value={editedContent}
|
||||
onValueChange={setEditedContent}
|
||||
minRows={20}
|
||||
classNames={{
|
||||
input: 'font-mono text-tiny'
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<pre className="max-h-96 overflow-auto whitespace-pre-wrap rounded-md bg-default-100 p-3 font-mono text-tiny">
|
||||
{content}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button variant="light" onPress={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
footer={
|
||||
<div className="flex flex-row justify-end gap-4">
|
||||
<AntButton type="text" onClick={onClose}>
|
||||
{t('common.close')}
|
||||
</AntButton>
|
||||
{installed ? (
|
||||
<Button
|
||||
color="danger"
|
||||
variant="flat"
|
||||
startContent={loading ? <Spinner size="sm" color="current" /> : <Trash2 className="h-4 w-4" />}
|
||||
onPress={onUninstall}
|
||||
isDisabled={loading}>
|
||||
<AntButton
|
||||
danger
|
||||
variant="filled"
|
||||
icon={loading ? <Spin size="small" /> : <Trash2 className="h-4 w-4" />}
|
||||
iconPosition={'start'}
|
||||
onClick={onUninstall}
|
||||
disabled={loading}>
|
||||
{loading ? t('plugins.uninstalling') : t('plugins.uninstall')}
|
||||
</Button>
|
||||
</AntButton>
|
||||
) : (
|
||||
<Button
|
||||
<AntButton
|
||||
color="primary"
|
||||
startContent={loading ? <Spinner size="sm" color="current" /> : <Download className="h-4 w-4" />}
|
||||
onPress={onInstall}
|
||||
isDisabled={loading}>
|
||||
variant="solid"
|
||||
icon={loading ? <Spin size="small" /> : <Download className="h-4 w-4" />}
|
||||
iconPosition={'start'}
|
||||
onClick={onInstall}
|
||||
disabled={loading}>
|
||||
{loading ? t('plugins.installing') : t('plugins.install')}
|
||||
</Button>
|
||||
</AntButton>
|
||||
)}
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</div>
|
||||
}>
|
||||
<div>
|
||||
{/* Description */}
|
||||
{plugin.description && (
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-2 font-semibold text-small">Description</h3>
|
||||
<p className="text-default-600 text-small">{plugin.description}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Author */}
|
||||
{plugin.author && (
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-2 font-semibold text-small">Author</h3>
|
||||
<p className="text-default-600 text-small">{plugin.author}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tools (for agents) */}
|
||||
{plugin.tools && plugin.tools.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-2 font-semibold text-small">Tools</h3>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{plugin.tools.map((tool) => (
|
||||
<Tag key={tool}>{tool}</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Allowed Tools (for commands) */}
|
||||
{plugin.allowed_tools && plugin.allowed_tools.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-2 font-semibold text-small">Allowed Tools</h3>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{plugin.allowed_tools.map((tool) => (
|
||||
<Tag key={tool}>{tool}</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tags */}
|
||||
{plugin.tags && plugin.tags.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-2 font-semibold text-small">Tags</h3>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{plugin.tags.map((tag) => (
|
||||
<Tag key={tag}>{tag}</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Metadata */}
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-2 font-semibold text-small">Metadata</h3>
|
||||
<div className="space-y-1 text-small">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-default-500">File:</span>
|
||||
<span className="font-mono text-default-600 text-tiny">{plugin.filename}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-default-500">Size:</span>
|
||||
<span className="text-default-600">{(plugin.size / 1024).toFixed(2)} KB</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-default-500">Source:</span>
|
||||
<span className="font-mono text-default-600 text-tiny">{plugin.sourcePath}</span>
|
||||
</div>
|
||||
{plugin.installedAt && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-default-500">Installed:</span>
|
||||
<span className="text-default-600">{new Date(plugin.installedAt).toLocaleString()}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="mb-4">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<h3 className="font-semibold text-small">Content</h3>
|
||||
{installed && !contentLoading && !contentError && (
|
||||
<div className="flex gap-2">
|
||||
{isEditing ? (
|
||||
<>
|
||||
<AntButton
|
||||
danger
|
||||
variant="filled"
|
||||
icon={<X className="h-3 w-3" />}
|
||||
iconPosition="start"
|
||||
onClick={handleCancelEdit}
|
||||
disabled={saving}>
|
||||
{t('common.cancel')}
|
||||
</AntButton>
|
||||
<AntButton
|
||||
color="primary"
|
||||
variant="filled"
|
||||
icon={saving ? <Spin size="small" /> : <Save className="h-3 w-3" />}
|
||||
onClick={handleSave}
|
||||
disabled={saving}>
|
||||
{t('common.save')}
|
||||
</AntButton>
|
||||
</>
|
||||
) : (
|
||||
<AntButton variant="filled" icon={<Edit className="h-3 w-3" />} onClick={handleEdit}>
|
||||
{t('common.edit')}
|
||||
</AntButton>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{contentLoading ? (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<Spin size="small" />
|
||||
</div>
|
||||
) : contentError ? (
|
||||
<div className="rounded-md bg-danger-50 p-3 text-danger text-small">{contentError}</div>
|
||||
) : isEditing ? (
|
||||
<Input.TextArea
|
||||
value={editedContent}
|
||||
onChange={(e) => setEditedContent(e.target.value)}
|
||||
autoSize={{ minRows: 20 }}
|
||||
classNames={{
|
||||
textarea: 'font-mono text-tiny'
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<pre className="max-h-96 overflow-auto whitespace-pre-wrap rounded-md bg-default-100 p-3 font-mono text-tiny">
|
||||
{content}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</AntdModal>
|
||||
)
|
||||
|
||||
return createPortal(modalContent, document.body)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user