feat: move plugin center into project configuration
This commit is contained in:
14
src/App.tsx
14
src/App.tsx
@@ -32,7 +32,6 @@ const Chat = lazy(() => import('./pages/Chat').then(({ Chat: component }) => ({
|
||||
const Home = lazy(() => import('./pages/Home').then(({ Home: component }) => ({ default: component })));
|
||||
const PreviewScene = lazy(() => import('./pages/Makelore').then(({ PreviewScene: component }) => ({ default: component })));
|
||||
const ProjectConfiguration = lazy(() => import('./pages/ProjectConfiguration').then(({ ProjectConfiguration: component }) => ({ default: component })));
|
||||
const PluginMarketplace = lazy(() => import('./pages/PluginMarketplace').then(({ PluginMarketplace: component }) => ({ default: component })));
|
||||
const Workbench = lazy(() => import('./pages/Workbench').then(({ Workbench: component }) => ({ default: component })));
|
||||
const ImageCanvas = lazy(() => import('./pages/ImageCanvas').then(({ ImageCanvas: component }) => ({ default: component })));
|
||||
const ImagePromptMuseum = lazy(() => import('./pages/ImagePromptMuseum').then(({ ImagePromptMuseum: component }) => ({ default: component })));
|
||||
@@ -201,6 +200,13 @@ function ModuleSelectionRoute({
|
||||
return <ModuleSelection authRequired={authRequired} />;
|
||||
}
|
||||
|
||||
function LegacyPluginMarketplaceRedirect() {
|
||||
const location = useLocation();
|
||||
const legacyTab = new URLSearchParams(location.search).get('tab');
|
||||
const pluginTab = legacyTab === 'mine' ? 'mine' : 'discover';
|
||||
return <Navigate to={`/project-config?resource=plugins&pluginTab=${pluginTab}`} replace />;
|
||||
}
|
||||
|
||||
function ProjectChatRoute() {
|
||||
const activeProject = useCodingWorkspaceStore((state) => state.activeProject);
|
||||
const loadProjectList = useCodingWorkspaceStore((state) => state.load);
|
||||
@@ -444,9 +450,9 @@ function App() {
|
||||
)}
|
||||
>
|
||||
<Route path="/project-config" element={<ProjectConfiguration />} />
|
||||
<Route path="/project-plugins" element={<Navigate to="/project-config?resource=plugins" replace />} />
|
||||
<Route path="/plugin-marketplace" element={<PluginMarketplace />} />
|
||||
<Route path="/my-plugins" element={<Navigate to="/plugin-marketplace?tab=mine" replace />} />
|
||||
<Route path="/project-plugins" element={<Navigate to="/project-config?resource=plugins&pluginTab=project" replace />} />
|
||||
<Route path="/plugin-marketplace" element={<LegacyPluginMarketplaceRedirect />} />
|
||||
<Route path="/my-plugins" element={<Navigate to="/project-config?resource=plugins&pluginTab=mine" replace />} />
|
||||
<Route path="/makelore-home" element={<Home />} />
|
||||
<Route path="/kangaroo" element={<Navigate to="/project-config" replace />} />
|
||||
<Route path="/subagents" element={<Navigate to="/project-config" replace />} />
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
FolderKanban,
|
||||
LogOut,
|
||||
Plus,
|
||||
Store,
|
||||
Settings as SettingsIcon,
|
||||
UserCircle2,
|
||||
} from 'lucide-react';
|
||||
@@ -586,23 +585,6 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
|
||||
<Plus className="h-4 w-4 shrink-0 text-brand" />
|
||||
{!sidebarCollapsed && '新建项目'}
|
||||
</button>
|
||||
<div className="mt-3 grid gap-1">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="sidebar-nav-plugin-marketplace"
|
||||
aria-current={location.pathname === '/plugin-marketplace' ? 'page' : undefined}
|
||||
aria-label="插件中心"
|
||||
onClick={() => navigate('/plugin-marketplace')}
|
||||
className={cn(
|
||||
'motion-press flex min-h-10 w-full items-center gap-2 rounded-lg px-2 py-2 text-left text-sm font-medium transition-colors duration-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/35',
|
||||
location.pathname === '/plugin-marketplace' ? 'bg-brand-selected text-foreground' : 'text-foreground hover:bg-surface-subtle',
|
||||
sidebarCollapsed && 'justify-center px-0',
|
||||
)}
|
||||
>
|
||||
<Store className="h-4 w-4 shrink-0 text-brand" />
|
||||
{!sidebarCollapsed ? <span>插件中心</span> : null}
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
50
src/components/plugins/PluginServices.tsx
Normal file
50
src/components/plugins/PluginServices.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { MyPlugins } from '@/pages/MyPlugins';
|
||||
import { PluginMarketplaceCatalog } from '@/pages/PluginMarketplace';
|
||||
import { ProjectPlugins } from '@/pages/ProjectPlugins';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { getPluginServiceTab, type PluginServiceTab } from '@/lib/plugin-services';
|
||||
|
||||
type PluginServicesProps = {
|
||||
value: PluginServiceTab;
|
||||
onValueChange(value: PluginServiceTab): void;
|
||||
onProjectPluginsChanged(): void;
|
||||
onManageAgents(): void;
|
||||
onResolveIdentity(): void;
|
||||
};
|
||||
|
||||
export function PluginServices({
|
||||
value,
|
||||
onValueChange,
|
||||
onProjectPluginsChanged,
|
||||
onManageAgents,
|
||||
onResolveIdentity,
|
||||
}: PluginServicesProps) {
|
||||
return (
|
||||
<Tabs
|
||||
data-testid="plugin-services"
|
||||
value={value}
|
||||
onValueChange={(next) => onValueChange(getPluginServiceTab(next))}
|
||||
className="min-w-0"
|
||||
>
|
||||
<TabsList aria-label="插件服务内容" className="grid w-full max-w-lg grid-cols-3">
|
||||
<TabsTrigger value="discover">发现插件</TabsTrigger>
|
||||
<TabsTrigger value="mine">我的插件</TabsTrigger>
|
||||
<TabsTrigger value="project">项目启用</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="discover" className="mt-6">
|
||||
<PluginMarketplaceCatalog embedded />
|
||||
</TabsContent>
|
||||
<TabsContent value="mine" className="mt-6">
|
||||
<MyPlugins embedded />
|
||||
</TabsContent>
|
||||
<TabsContent value="project" className="mt-6">
|
||||
<ProjectPlugins
|
||||
embedded
|
||||
onChanged={onProjectPluginsChanged}
|
||||
onManageAgents={onManageAgents}
|
||||
onResolveIdentity={onResolveIdentity}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
6
src/lib/plugin-services.ts
Normal file
6
src/lib/plugin-services.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export type PluginServiceTab = 'discover' | 'mine' | 'project';
|
||||
|
||||
export function getPluginServiceTab(value: string | null | undefined): PluginServiceTab {
|
||||
if (value === 'discover' || value === 'mine') return value;
|
||||
return 'project';
|
||||
}
|
||||
@@ -62,7 +62,7 @@ export function MyPluginsView(props: MyPluginsViewProps) {
|
||||
{props.state === 'loading' && !props.library ? <p role="status" className="mt-8 text-sm">正在读取我的插件…</p> : null}
|
||||
{props.state === 'error' && !props.library ? <div role="alert" className="mt-8 rounded-2xl bg-warning/10 p-5"><h2 className="font-semibold">无法读取我的插件</h2><p className="mt-2 text-pretty text-sm">{props.error ?? '请稍后重试。'}</p></div> : null}
|
||||
{visibleError && props.library ? <p role="alert" className="mt-4 rounded-xl bg-warning/10 p-3 text-pretty text-sm">{visibleError}</p> : null}
|
||||
{props.library && props.library.items.length === 0 ? <div className="mt-8 rounded-2xl bg-surface-subtle p-6 text-center"><PackageCheck className="mx-auto h-8 w-8 text-muted-foreground" /><h2 className="mt-3 text-balance font-semibold">账号插件库还是空的</h2><p className="mt-1 text-pretty text-sm text-muted-foreground">前往插件中心免费获取需要的插件。</p><Button asChild className="mt-4 min-h-10"><Link to="/plugin-marketplace">浏览插件中心</Link></Button></div> : null}
|
||||
{props.library && props.library.items.length === 0 ? <div className="mt-8 rounded-2xl bg-surface-subtle p-6 text-center"><PackageCheck className="mx-auto h-8 w-8 text-muted-foreground" /><h2 className="mt-3 text-balance font-semibold">账号插件库还是空的</h2><p className="mt-1 text-pretty text-sm text-muted-foreground">前往“发现插件”免费获取需要的插件。</p><Button asChild className="mt-4 min-h-10"><Link to="/project-config?resource=plugins&pluginTab=discover">浏览插件</Link></Button></div> : null}
|
||||
<section aria-label="账号插件库" className="mt-6 space-y-4">
|
||||
{props.library?.items.map((plugin) => {
|
||||
const installed = props.installations[plugin.pluginId];
|
||||
@@ -89,7 +89,7 @@ export function MyPluginsView(props: MyPluginsViewProps) {
|
||||
{!removed && plugin.acquisition !== 'system_included' && plugin.betaVersion && installedChannel !== 'beta' ? <Button type="button" variant="outline" className="min-h-10" disabled={busy || suspended} aria-label={`安装 Beta${plugin.title}`} onClick={() => void props.onInstallBeta(plugin.pluginId)}>安装 Beta</Button> : null}
|
||||
{!removed && plugin.acquisition !== 'system_included' ? <Button type="button" variant="outline" className="min-h-10" disabled={busy} aria-label={`移除${plugin.title}`} onClick={() => void props.onRemove(plugin.pluginId)}><Trash2 className="mr-2 h-4 w-4" />移除</Button> : null}
|
||||
{hasDevicePackage ? <Button type="button" variant="ghost" className="min-h-10" disabled={busy} aria-label={`删除设备上的${plugin.title}`} onClick={() => void props.onUninstall(plugin.pluginId)}>删除设备包</Button> : null}
|
||||
<Button asChild variant="outline" className="min-h-10"><Link to="/project-config?resource=plugins">启用到项目</Link></Button>
|
||||
<Button asChild variant="outline" className="min-h-10"><Link to="/project-config?resource=plugins&pluginTab=project">启用到项目</Link></Button>
|
||||
<Button asChild variant="outline" className="min-h-10"><Link to="/project-config">分配给伙伴</Link></Button>
|
||||
</div>
|
||||
</article>;
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { FormEvent, useEffect, useMemo, useState } from 'react';
|
||||
import { CheckCircle2, Download, Puzzle, Search, ShieldCheck } from 'lucide-react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { MyPlugins } from '@/pages/MyPlugins';
|
||||
import type {
|
||||
MarketplaceCatalogPage,
|
||||
MarketplaceCatalogQuery,
|
||||
@@ -54,7 +51,7 @@ export function PluginMarketplaceView(props: PluginMarketplaceViewProps) {
|
||||
<div>
|
||||
{props.embedded
|
||||
? <h2 className="text-balance text-xl font-semibold tracking-[-0.02em]">发现插件</h2>
|
||||
: <h1 className="text-balance text-3xl font-semibold tracking-[-0.03em]">插件中心</h1>}
|
||||
: <h1 className="text-balance text-3xl font-semibold tracking-[-0.03em]">发现插件</h1>}
|
||||
<p className="mt-2 max-w-2xl text-pretty text-sm text-muted-foreground">浏览 MakeLore 运营发行的插件。免费获取只加入账号插件库,不会自动下载、启用项目或分配给伙伴。</p>
|
||||
</div>
|
||||
{props.catalog ? <p className="tabular-nums rounded-xl bg-surface-subtle px-3 py-2 text-sm" aria-label={`共 ${props.catalog.total} 个插件`}><span className="tabular-nums">{props.catalog.total}</span> 个插件</p> : null}
|
||||
@@ -114,9 +111,7 @@ export function PluginMarketplaceView(props: PluginMarketplaceViewProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export function PluginMarketplace() {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const activeTab = searchParams.get('tab') === 'mine' ? 'mine' : 'discover';
|
||||
export function PluginMarketplaceCatalog({ embedded = false }: { embedded?: boolean } = {}) {
|
||||
const user = useAuthStore((state) => state.user);
|
||||
const accountKey = user?.userId ?? user?.username ?? null;
|
||||
const catalog = usePluginMarketplaceStore((state) => state.catalog);
|
||||
@@ -128,34 +123,11 @@ export function PluginMarketplace() {
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
useEffect(() => {
|
||||
pluginMarketplaceStore.getState().activateAccount(accountKey);
|
||||
if (activeTab === 'discover') void pluginMarketplaceStore.getState().loadCatalog({ limit: 24 }).catch(() => undefined);
|
||||
void pluginMarketplaceStore.getState().loadCatalog({ limit: 24 }).catch(() => undefined);
|
||||
if (accountKey) void pluginMarketplaceStore.getState().loadLibrary().catch(() => undefined);
|
||||
}, [accountKey, activeTab]);
|
||||
}, [accountKey]);
|
||||
const safe = (operation: Promise<void>) => operation.catch((error) => {
|
||||
toast.error(error instanceof Error ? error.message : String(error));
|
||||
});
|
||||
const handleTabChange = (value: string) => {
|
||||
const next = new URLSearchParams(searchParams);
|
||||
if (value === 'mine') next.set('tab', 'mine');
|
||||
else next.delete('tab');
|
||||
setSearchParams(next, { replace: true });
|
||||
};
|
||||
return <main data-testid="plugin-marketplace-page" className="mx-auto w-full max-w-7xl text-foreground">
|
||||
<header>
|
||||
<h1 className="text-balance text-3xl font-semibold tracking-[-0.03em]">插件中心</h1>
|
||||
<p className="mt-2 max-w-3xl text-pretty text-sm text-muted-foreground">在同一处发现插件,并管理账号插件库与本机下载。获取、下载、项目启用和伙伴分配始终是彼此独立的步骤。</p>
|
||||
</header>
|
||||
<Tabs value={activeTab} onValueChange={handleTabChange} className="mt-6">
|
||||
<TabsList aria-label="插件中心内容" className="grid w-full max-w-sm grid-cols-2">
|
||||
<TabsTrigger value="discover">发现插件</TabsTrigger>
|
||||
<TabsTrigger value="mine">我的插件</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="discover" className="mt-6">
|
||||
<PluginMarketplaceView embedded catalog={catalog} detail={selectedId ? details[selectedId] ?? null : null} library={library} catalogState={catalogState} catalogError={catalogError} pending={pending} onSearch={(input) => safe(pluginMarketplaceStore.getState().loadCatalog(input))} onSelect={(pluginId) => { setSelectedId(pluginId); return safe(pluginMarketplaceStore.getState().loadDetail(pluginId)); }} onAcquire={(pluginId) => safe(pluginMarketplaceStore.getState().acquire(pluginId))} />
|
||||
</TabsContent>
|
||||
<TabsContent value="mine" className="mt-6">
|
||||
<MyPlugins embedded />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</main>;
|
||||
return <PluginMarketplaceView embedded={embedded} catalog={catalog} detail={selectedId ? details[selectedId] ?? null : null} library={library} catalogState={catalogState} catalogError={catalogError} pending={pending} onSearch={(input) => safe(pluginMarketplaceStore.getState().loadCatalog(input))} onSelect={(pluginId) => { setSelectedId(pluginId); return safe(pluginMarketplaceStore.getState().loadDetail(pluginId)); }} onAcquire={(pluginId) => safe(pluginMarketplaceStore.getState().acquire(pluginId))} />;
|
||||
}
|
||||
|
||||
@@ -7,13 +7,14 @@ import { Button } from '@/components/ui/button';
|
||||
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
|
||||
import { Sheet, SheetClose, SheetContent, SheetDescription, SheetTitle } from '@/components/ui/sheet';
|
||||
import { AgentCreationDialog, type AgentCreationInput } from '@/components/coding/AgentCreationDialog';
|
||||
import { PluginServices } from '@/components/plugins/PluginServices';
|
||||
import { ProjectPublishAction } from '@/components/works/ProjectPublishAction';
|
||||
import { ProjectPlugins } from '@/pages/ProjectPlugins';
|
||||
import { buildCodingModelOptions, parseCodingModelKey, type CodingModelOption } from '@/lib/coding-model-options';
|
||||
import { getCodingSkills } from '@/lib/coding-product-tools';
|
||||
import { abortCodingConversation } from '@/lib/coding-conversations';
|
||||
import { isCanonicalCodingProjectId } from '@/lib/coding-projects';
|
||||
import { getSkillDisplayInfo } from '@/lib/skill-display';
|
||||
import { getPluginServiceTab, type PluginServiceTab } from '@/lib/plugin-services';
|
||||
import { getAgentAvatarSrc } from '@/lib/agent-avatars';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { codingConversationStore } from '@/stores/coding-conversations';
|
||||
@@ -147,6 +148,7 @@ export function ProjectConfiguration() {
|
||||
const pluginLoadState = useCodingPluginsStore((state) => state.loadState);
|
||||
const [draft, setDraft] = useState<CodingProjectConfig | null>(null);
|
||||
const [drawerMode, setDrawerMode] = useState<DrawerMode>(null);
|
||||
const [pluginServiceTab, setPluginServiceTab] = useState<PluginServiceTab>('project');
|
||||
const [activeSkillId, setActiveSkillId] = useState<string | null>(null);
|
||||
const [activeAgentId, setActiveAgentId] = useState<string | null>(null);
|
||||
const [skills, setSkills] = useState<SkillInfo[]>([]);
|
||||
@@ -190,7 +192,10 @@ export function ProjectConfiguration() {
|
||||
setPartnerDialogOpen(true);
|
||||
}, [draft, location.search]);
|
||||
useEffect(() => {
|
||||
if (new URLSearchParams(location.search).get('resource') === 'plugins') setDrawerMode('plugins');
|
||||
const params = new URLSearchParams(location.search);
|
||||
if (params.get('resource') !== 'plugins') return;
|
||||
setPluginServiceTab(getPluginServiceTab(params.get('pluginTab')));
|
||||
setDrawerMode('plugins');
|
||||
}, [location.search]);
|
||||
useEffect(() => { loadSkills(); }, [loadSkills]);
|
||||
useEffect(() => {
|
||||
@@ -214,6 +219,29 @@ export function ProjectConfiguration() {
|
||||
: `${enabledPluginCount} 个已启用 · ${availablePluginCount} 个可用`;
|
||||
const playSaveConfirmation = () => undefined;
|
||||
const handleBack = () => navigate(-1);
|
||||
const handlePluginServiceTabChange = (tab: PluginServiceTab) => {
|
||||
setPluginServiceTab(tab);
|
||||
const params = new URLSearchParams(location.search);
|
||||
params.set('resource', 'plugins');
|
||||
params.set('pluginTab', tab);
|
||||
navigate({ pathname: '/project-config', search: `?${params.toString()}` }, { replace: true });
|
||||
};
|
||||
const openPluginServices = () => {
|
||||
setPluginServiceTab('project');
|
||||
setDrawerMode('plugins');
|
||||
const params = new URLSearchParams(location.search);
|
||||
params.set('resource', 'plugins');
|
||||
params.set('pluginTab', 'project');
|
||||
navigate({ pathname: '/project-config', search: `?${params.toString()}` }, { replace: true });
|
||||
};
|
||||
const closePluginServices = () => {
|
||||
setDrawerMode(null);
|
||||
const params = new URLSearchParams(location.search);
|
||||
params.delete('resource');
|
||||
params.delete('pluginTab');
|
||||
const search = params.toString();
|
||||
navigate({ pathname: '/project-config', search: search ? `?${search}` : '' }, { replace: true });
|
||||
};
|
||||
|
||||
if (!activeProject) return <main data-testid="project-configuration-empty-state" className="flex min-h-full items-start bg-background p-8 text-foreground"><p className="pt-4 text-xl font-semibold tracking-[-0.02em]">一念成光,万物可创。</p></main>;
|
||||
if (!draft) return <div className="p-8 text-center text-sm font-medium">正在读取项目配置…</div>;
|
||||
@@ -471,7 +499,7 @@ export function ProjectConfiguration() {
|
||||
</form>
|
||||
)}
|
||||
</section>
|
||||
<section className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4"><ResourceCard id="models" icon={<Cpu />} title="可用模型" subtitle={`${modelOptions.length} 个可用模型`} onClick={() => setDrawerMode('models')} /><ResourceCard id="skills" icon={<Blocks />} title="可用技能" subtitle={`${skills.length} 项可用技能`} onClick={() => { setActiveSkillId(null); setDrawerMode('skills'); }} /><ResourceCard id="knowledge" icon={<Files />} title="知识文件" subtitle={`${knowledge.length} 个知识文件`} onClick={() => setDrawerMode('knowledge')} /><ResourceCard id="plugins" icon={<Puzzle />} title="项目插件" subtitle={pluginSummary} onClick={() => setDrawerMode('plugins')} /></section>
|
||||
<section className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4"><ResourceCard id="models" icon={<Cpu />} title="可用模型" subtitle={`${modelOptions.length} 个可用模型`} onClick={() => setDrawerMode('models')} /><ResourceCard id="skills" icon={<Blocks />} title="可用技能" subtitle={`${skills.length} 项可用技能`} onClick={() => { setActiveSkillId(null); setDrawerMode('skills'); }} /><ResourceCard id="knowledge" icon={<Files />} title="知识文件" subtitle={`${knowledge.length} 个知识文件`} onClick={() => setDrawerMode('knowledge')} /><ResourceCard id="plugins" icon={<Puzzle />} title="插件服务" subtitle={pluginSummary} onClick={openPluginServices} /></section>
|
||||
<section id="project-agents-section" className="config-motion-partners surface-card rounded-2xl border border-border/70 bg-background p-5 shadow-soft"><div className="mb-4 flex items-center justify-between gap-3"><div><h2 className="text-xl font-semibold">项目智能体</h2><p className="mt-1 text-sm font-medium text-muted-foreground">智能体配置仅属于当前项目;每个智能体可以拥有多条独立对话。</p></div><Button type="button" size="icon" className="h-8 w-8 rounded-full border border-brand/20 bg-brand-soft text-brand" aria-label="创建智能体" title="创建智能体" onClick={() => { if (modelOptions.length === 0) { toast.error('请先在模型设置中配置至少一个可用模型。'); setDrawerMode('models'); return; } setActiveAgentId(null); setPartnerDialogOpen(true); }}><Plus className="h-4 w-4" /></Button></div><div data-testid="agent-cards-scroll-region" className="-m-2 flex gap-4 overflow-auto p-2">{draft.agents.filter((agent) => !agent.archivedAt).map((agent) => <AgentCard key={agent.id} agent={agent} onOpen={() => { setActiveAgentId(agent.id); setPartnerDialogOpen(true); }} />)}{draft.agents.every((agent) => agent.archivedAt) ? <div className="w-full rounded-xl border border-dashed border-foreground/15 p-6 text-center text-sm font-medium text-muted-foreground">当前项目还没有智能体,点击右上角的 + 创建。</div> : null}</div>{draft.agents.some((agent) => agent.archivedAt) ? <div className="mt-4 border-t border-border/70 pt-4"><p className="text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground">已归档</p><div className="mt-2 flex flex-wrap gap-2">{draft.agents.filter((agent) => agent.archivedAt).map((agent) => <Button key={agent.id} type="button" variant="outline" className="border-border/70 bg-surface-subtle" onClick={() => updateAgent({ ...agent, archivedAt: null })}>{agent.name || '未命名智能体'} · 恢复</Button>)}</div></div> : null}</section>
|
||||
<div data-testid="project-configuration-actions" className="glass-surface config-motion-actions sticky bottom-3 z-10 mt-auto flex shrink-0 flex-col gap-2 rounded-2xl border border-border/70 bg-background/85 p-2 shadow-float sm:flex-row sm:items-start sm:justify-between">
|
||||
<Button type="button" variant="outline" size="sm" onClick={() => setDeleteDialogOpen(true)} className="h-9 w-full border-brand/25 bg-background px-3 font-semibold text-foreground shadow-none hover:border-brand/40 hover:bg-brand-soft sm:w-auto"><Trash2 className="mr-2 h-4 w-4" />删除项目</Button>
|
||||
@@ -502,12 +530,12 @@ export function ProjectConfiguration() {
|
||||
title={activeAgent ? `${activeAgent.name || '未命名智能体'} · 编辑智能体` : '创建项目智能体'}
|
||||
submitLabel={activeAgent ? '保存智能体' : '创建智能体'}
|
||||
/>
|
||||
<Sheet open={drawerMode !== null} onOpenChange={(open) => { if (!open) { setDrawerMode(null); setActiveSkillId(null); } }}><SheetContent className={cn('glass-surface flex flex-col overflow-hidden border-l border-border/70 bg-background/90 p-0 text-foreground sm:max-w-none', drawerMode === 'plugins' ? 'w-[min(920px,96vw)]' : 'w-[min(430px,94vw)]')}>
|
||||
<Sheet open={drawerMode !== null} onOpenChange={(open) => { if (!open) { if (drawerMode === 'plugins') closePluginServices(); else setDrawerMode(null); setActiveSkillId(null); } }}><SheetContent className={cn('glass-surface flex flex-col overflow-hidden border-l border-border/70 bg-background/90 p-0 text-foreground sm:max-w-none', drawerMode === 'plugins' ? 'w-[min(1080px,96vw)]' : 'w-[min(430px,94vw)]')}>
|
||||
{drawerMode === 'models' ? <DrawerFrame title="可用模型" desc="查看已配置的模型;智能体默认模型在智能体配置中设置。" onClose={() => setDrawerMode(null)}><ModelList models={modelOptions} /></DrawerFrame> : null}
|
||||
{drawerMode === 'knowledge' ? <DrawerFrame title="知识文件" desc="文件保存在当前项目的 knowledge/ 目录中。" onClose={() => setDrawerMode(null)}><input ref={knowledgeInputRef} type="file" className="sr-only" onChange={(event) => { void handleKnowledge(event.target.files?.[0]); event.currentTarget.value = ''; }} /><Button className="w-full border border-foreground/15 bg-brand-soft text-foreground" onClick={() => knowledgeInputRef.current?.click()}><Upload className="mr-2 h-4 w-4" />上传知识文件</Button><div className="mt-5 space-y-3">{knowledge.length ? knowledge.map((file) => <div key={file} className="rounded-lg border border-foreground/15 bg-white p-3 font-semibold">{file}</div>) : <div className="rounded-lg border border-dashed border-foreground/15 bg-white p-4 text-sm font-medium">当前还没有知识文件。</div>}</div></DrawerFrame> : null}
|
||||
{drawerMode === 'skills' && activeSkill ? <DrawerFrame title={`${activeSkill.name} · 技能详情`} desc="查看技能目录结构和主文件 SKILL.md。" onClose={() => setActiveSkillId(null)} closeLabel="返回技能列表" closeIcon="back"><SkillDetail skill={activeSkill} /></DrawerFrame> : null}
|
||||
{drawerMode === 'skills' && !activeSkill ? <DrawerFrame title="可用技能" desc="查看技能目录与主文件;在智能体配置中启用所需技能。" onClose={() => setDrawerMode(null)}><div className="space-y-3">{skills.length > 0 ? skills.map((skill) => <button key={skill.id} type="button" data-testid={`skill-card-${skill.id}`} aria-label={`查看技能详情:${skill.name}`} onClick={() => setActiveSkillId(skill.id)} className="w-full rounded-lg border border-foreground/15 bg-white p-3 text-left transition hover:border-brand/40 hover:bg-brand-soft/30"><div className="flex items-start gap-3"><Blocks className="mt-0.5 h-4 w-4 shrink-0 text-brand" /><span className="min-w-0 flex-1"><span className="block font-semibold">{skill.name}</span><span className="mt-1 block text-sm text-muted-foreground">{skill.description}</span><span className="mt-2 block text-[11px] font-medium text-brand">查看目录与 SKILL.md</span></span><ArrowLeft className="mt-0.5 h-4 w-4 shrink-0 rotate-180 text-muted-foreground" aria-hidden="true" /></div></button>) : <div className="rounded-lg border border-dashed border-foreground/15 bg-white p-4 text-sm font-medium text-muted-foreground">当前没有可查看的技能。</div>}</div></DrawerFrame> : null}
|
||||
{drawerMode === 'plugins' ? <DrawerFrame title="项目插件" desc="启用当前项目所需能力;插件提供的 Skill 仍在智能体配置中单独分配。" onClose={() => setDrawerMode(null)}><ProjectPlugins embedded onChanged={loadSkills} onResolveIdentity={() => setDrawerMode(null)} onManageAgents={() => setDrawerMode(null)} /></DrawerFrame> : null}
|
||||
{drawerMode === 'plugins' ? <DrawerFrame title="插件服务" desc="发现与下载插件,并为当前项目单独启用;伙伴 Skill 仍在智能体配置中分配。" onClose={closePluginServices}><PluginServices value={pluginServiceTab} onValueChange={handlePluginServiceTabChange} onProjectPluginsChanged={loadSkills} onResolveIdentity={closePluginServices} onManageAgents={closePluginServices} /></DrawerFrame> : null}
|
||||
</SheetContent></Sheet>
|
||||
<ConfirmDialog
|
||||
open={Boolean(archiveAgentId)}
|
||||
|
||||
Reference in New Issue
Block a user