feat: update Makelore modules and conversations
This commit is contained in:
@@ -1,26 +1,28 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useGSAP } from '@gsap/react';
|
||||
import { gsap } from 'gsap';
|
||||
import { Check, Plus, Search, Settings2, Trash2, Upload, X } from 'lucide-react';
|
||||
import { Check, Plus, Search, Trash2, Upload, X } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Sheet, SheetContent } from '@/components/ui/sheet';
|
||||
import { Sheet, SheetContent, SheetDescription, SheetTitle } from '@/components/ui/sheet';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { AgentCreationDialog, type AgentCreationInput } from '@/components/opencode/AgentCreationDialog';
|
||||
import { hostApiFetch } from '@/lib/host-api';
|
||||
import { buildConfiguredModelOptions, type ConfiguredModelOption } from '@/lib/model-options';
|
||||
import { getSkillDisplayInfo } from '@/lib/skill-display';
|
||||
import { agentAvatarOptions, getAgentAvatarSrc } from '@/lib/agent-avatars';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useOpencodeStore } from '@/stores/opencode';
|
||||
import { useProjectConversationStore } from '@/stores/project-conversations';
|
||||
import { useProjectConfigStore } from '@/stores/project-config';
|
||||
import { useProviderStore } from '@/stores/providers';
|
||||
import { validateAgentNames, YOUTH_PLAIN_LANGUAGE_SKILL_ID, type ProjectAgentConfig, type ProjectConfig } from '../../../shared/project-config';
|
||||
import { validateAgentConfigs, type ProjectAgentConfig, type ProjectConfig } from '../../../shared/project-config';
|
||||
|
||||
const EMPTY_FILES: string[] = [];
|
||||
type DrawerMode = 'models' | 'knowledge' | 'skills' | 'agent' | null;
|
||||
@@ -35,26 +37,53 @@ function prefersReducedMotion() {
|
||||
|| window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
}
|
||||
|
||||
function createCustomAgent(index: number): ProjectAgentConfig {
|
||||
return { id: `custom-agent-${Date.now()}-${index}`, avatarId: 'avatar-01', roleName: '自定义职能', name: '', builtIn: false, enabled: true, model: null, skillIds: [YOUTH_PLAIN_LANGUAGE_SKILL_ID], responsibility: { mission: '', owns: [], boundaries: [], collaborators: [], principles: [] }, prompt: '请描述这个 Agent 在项目中的职责、负责内容、职能边界、协作关系和工作原则。' };
|
||||
function supportsHover() {
|
||||
return typeof window !== 'undefined'
|
||||
&& typeof window.matchMedia === 'function'
|
||||
&& window.matchMedia('(hover: hover) and (pointer: fine)').matches;
|
||||
}
|
||||
|
||||
function isMissingRuntimeSessionError(error: unknown): boolean {
|
||||
const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
||||
return message.includes('404') || message.includes('not found') || message.includes('不存在');
|
||||
}
|
||||
|
||||
function createCustomAgent(index: number, model: string | null): ProjectAgentConfig {
|
||||
const now = new Date().toISOString();
|
||||
return {
|
||||
id: `custom-agent-${Date.now()}-${index}`,
|
||||
avatarId: 'avatar-01',
|
||||
roleName: '项目伙伴',
|
||||
name: '',
|
||||
builtIn: false,
|
||||
enabled: true,
|
||||
model,
|
||||
skillIds: [],
|
||||
responsibility: { mission: '', owns: [], boundaries: [], collaborators: [], principles: [] },
|
||||
prompt: '',
|
||||
archivedAt: null,
|
||||
pinned: false,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
}
|
||||
|
||||
function ResourceCard({ id, icon, title, subtitle, onClick }: { id: string; icon: string; title: string; subtitle: string; onClick: () => void }) {
|
||||
const cardRef = useRef<HTMLButtonElement | null>(null);
|
||||
const { contextSafe } = useGSAP({ scope: cardRef });
|
||||
const animateHover = contextSafe((target: HTMLButtonElement, active: boolean) => {
|
||||
if (prefersReducedMotion()) return;
|
||||
if (prefersReducedMotion() || !supportsHover()) return;
|
||||
gsap.to(target, { y: active ? -4 : 0, scale: active ? 1.015 : 1, duration: 0.22, ease: 'power2.out', overwrite: 'auto' });
|
||||
});
|
||||
return <button ref={cardRef} type="button" data-testid={`resource-card-${id}`} onClick={onClick} onPointerEnter={(event) => animateHover(event.currentTarget, true)} onPointerLeave={(event) => animateHover(event.currentTarget, false)} className="config-motion-resource rounded-lg border-2 border-[#26384D] bg-white p-3 text-left shadow-[3px_3px_0_#26384D]">
|
||||
<div className="flex items-center gap-2"><span className="text-2xl">{icon}</span><p className="text-sm font-black">{title}</p></div><p className="mt-1.5 truncate text-[11px] font-bold text-[#68788B]">{subtitle}</p>
|
||||
return <button ref={cardRef} type="button" data-testid={`resource-card-${id}`} onClick={onClick} onPointerEnter={(event) => animateHover(event.currentTarget, true)} onPointerLeave={(event) => animateHover(event.currentTarget, false)} className="config-motion-resource surface-card motion-press rounded-2xl border border-border/70 bg-background p-3 text-left shadow-none hover:shadow-soft">
|
||||
<div className="flex items-center gap-2"><span className="text-2xl">{icon}</span><p className="text-sm font-semibold">{title}</p></div><p className="mt-1.5 truncate text-[11px] font-medium text-muted-foreground">{subtitle}</p>
|
||||
</button>;
|
||||
}
|
||||
|
||||
function SuperpowersCard({ enabled, onChange }: { enabled: boolean; onChange: (enabled: boolean) => void }) {
|
||||
return <div data-testid="superpowers-card" className="config-motion-resource rounded-lg border-2 border-[#26384D] bg-white p-3 text-left shadow-[3px_3px_0_#26384D]">
|
||||
<div className="flex items-center justify-between gap-2"><div className="flex items-center gap-2"><span className="text-2xl">🧩</span><div><p className="text-sm font-black">Superpowers</p><p className="mt-1 text-[11px] font-bold text-[#68788B]">{enabled ? '已启用' : '已关闭'}</p></div></div><Switch data-testid="superpowers-toggle" aria-label="启用 Superpowers" checked={enabled} onCheckedChange={onChange} /></div>
|
||||
<p className="mt-1.5 text-[11px] font-bold text-[#68788B]">{enabled ? '使用通用开发流程,保存后生效。' : '不加载通用流程,也不会自动调用 brainstorming。'}</p>
|
||||
return <div data-testid="superpowers-card" className="config-motion-resource surface-card rounded-2xl border border-border/70 bg-background p-3 text-left shadow-none">
|
||||
<div className="flex items-center justify-between gap-2"><div className="flex items-center gap-2"><span className="text-2xl">🧩</span><div><p className="text-sm font-semibold">Superpowers</p><p className="mt-1 text-[11px] font-medium text-muted-foreground">{enabled ? '已启用' : '已关闭'}</p></div></div><Switch data-testid="superpowers-toggle" aria-label="启用 Superpowers" checked={enabled} onCheckedChange={onChange} /></div>
|
||||
<p className="mt-1.5 text-[11px] font-medium text-muted-foreground">{enabled ? '使用通用开发流程,保存后生效。' : '不加载通用流程,也不会自动调用 brainstorming。'}</p>
|
||||
</div>;
|
||||
}
|
||||
|
||||
@@ -63,31 +92,33 @@ function DrawerFrame({ title, desc, onClose, children }: { title: string; desc:
|
||||
useGSAP(() => {
|
||||
if (prefersReducedMotion()) return;
|
||||
gsap.timeline({ defaults: { ease: 'power3.out' } })
|
||||
.from('.drawer-motion-header', { x: 18, autoAlpha: 0, duration: 0.36 })
|
||||
.from('.drawer-motion-body > *', { y: 12, autoAlpha: 0, duration: 0.32, stagger: 0.045 }, '-=0.18');
|
||||
// Keep the drawer content render-safe: movement should never make the
|
||||
// existing controls disappear if the entrance timeline is interrupted.
|
||||
.from('.drawer-motion-header', { x: 8, duration: 0.22 })
|
||||
.from('.drawer-motion-body > *', { y: 6, duration: 0.22, stagger: 0.035 }, '-=0.12');
|
||||
}, { scope: drawerRef });
|
||||
return <div ref={drawerRef} className="flex min-h-0 flex-1 flex-col"><div className="drawer-motion-header flex items-start justify-between gap-3 border-b-4 border-[#26384D] bg-[#E9EFF5] px-5 py-4"><div><h2 className="text-lg font-black">{title}</h2><p className="mt-1 text-sm font-bold text-[#68788B]">{desc}</p></div><Button variant="ghost" size="icon" onClick={onClose} aria-label="关闭"><X className="h-4 w-4" /></Button></div><div className="drawer-motion-body min-h-0 flex-1 overflow-auto p-5">{children}</div></div>;
|
||||
return <div ref={drawerRef} className="flex min-h-0 flex-1 flex-col"><div className="glass-surface drawer-motion-header flex items-start justify-between gap-3 border-b border-border/70 bg-background/80 px-5 py-4"><div><SheetTitle className="text-lg font-semibold">{title}</SheetTitle><SheetDescription className="mt-1 text-sm font-medium text-muted-foreground">{desc}</SheetDescription></div><Button variant="ghost" size="icon" onClick={onClose} aria-label="关闭"><X className="h-4 w-4" /></Button></div><div className="drawer-motion-body min-h-0 flex-1 overflow-auto p-5">{children}</div></div>;
|
||||
}
|
||||
|
||||
function AgentCard({ agent, onOpen }: { agent: ProjectAgentConfig; onOpen: () => void }) {
|
||||
const cardRef = useRef<HTMLButtonElement | null>(null);
|
||||
const { contextSafe } = useGSAP({ scope: cardRef });
|
||||
const animateHover = contextSafe((target: HTMLButtonElement, active: boolean) => {
|
||||
if (prefersReducedMotion()) return;
|
||||
if (prefersReducedMotion() || !supportsHover()) return;
|
||||
gsap.to(target, { y: active ? -7 : 0, rotation: active ? -0.4 : 0, zIndex: active ? 2 : 0, duration: 0.25, ease: 'power2.out', overwrite: 'auto' });
|
||||
gsap.to(target.querySelector('.agent-motion-avatar'), { scale: active ? 1.07 : 1, duration: 0.28, ease: 'back.out(1.7)', overwrite: 'auto' });
|
||||
});
|
||||
return <button ref={cardRef} type="button" data-testid={`project-agent-${agent.id}`} onClick={onOpen} onPointerEnter={(event) => animateHover(event.currentTarget, true)} onPointerLeave={(event) => animateHover(event.currentTarget, false)} className="config-motion-agent h-[260px] w-[210px] flex-none rounded-lg border-4 border-[#26384D] bg-white p-4 text-left shadow-[4px_4px_0_#26384D]"><img src={getAgentAvatarSrc(agent.avatarId)} alt="" className="agent-motion-avatar mx-auto h-20 w-20 rounded-lg border-4 border-[#26384D] object-cover [image-rendering:pixelated]" /><div className="mt-4 flex items-center justify-between gap-2"><p className="truncate font-black">{agent.name || '未命名'}</p><Badge className="shrink-0 border-2 border-[#26384D] bg-[#FDE8E1] text-[#26384D]">可用</Badge></div><p className="mt-1 truncate text-xs font-black text-[#68788B]">{agent.roleName}</p><p className="mt-3 line-clamp-3 min-h-12 text-xs text-[#68788B]">{agent.responsibility.mission}</p><p className="mt-2 text-[11px] text-[#68788B]">{agent.skillIds.length} 个技能</p></button>;
|
||||
return <button ref={cardRef} type="button" data-testid={`project-agent-${agent.id}`} onClick={onOpen} onPointerEnter={(event) => animateHover(event.currentTarget, true)} onPointerLeave={(event) => animateHover(event.currentTarget, false)} className="config-motion-agent surface-card motion-press h-[260px] w-[210px] flex-none rounded-2xl border border-border/70 bg-background p-4 text-left shadow-none hover:shadow-soft"><img src={getAgentAvatarSrc(agent.avatarId)} alt="" className="agent-motion-avatar mx-auto h-20 w-20 rounded-xl border border-border object-cover [image-rendering:pixelated]" /><div className="mt-4 flex items-center justify-between gap-2"><p className="truncate font-semibold">{agent.name || '未命名'}</p><Badge className="shrink-0 border border-brand/15 bg-brand-soft text-brand">可用</Badge></div><p className="mt-1 truncate text-xs font-semibold text-muted-foreground">{agent.roleName}</p><p className="mt-3 line-clamp-3 min-h-12 text-xs text-muted-foreground">{agent.responsibility.mission}</p><p className="mt-2 text-[11px] text-muted-foreground">{agent.skillIds.length} 个技能</p></button>;
|
||||
}
|
||||
|
||||
function ModelSelect({ value, models, inheritLabel, label, onChange }: { value: string | null; models: ConfiguredModelOption[]; inheritLabel: string; label: string; onChange: (value: string | null) => void }) {
|
||||
return <select aria-label={label} value={value ?? ''} onChange={(event) => onChange(event.target.value || null)} className="mt-2 h-10 w-full rounded-md border-2 border-[#26384D] bg-white px-3 text-sm font-bold">
|
||||
<option value="">{inheritLabel}</option>
|
||||
function ModelSelect({ value, models, inheritLabel, label, onChange, required = false }: { value: string | null; models: ConfiguredModelOption[]; inheritLabel?: string; label: string; onChange: (value: string | null) => void; required?: boolean }) {
|
||||
return <select aria-label={label} value={value ?? ''} onChange={(event) => onChange(event.target.value || null)} className="mt-2 h-10 w-full rounded-xl border border-border bg-surface-input px-3 text-sm font-medium">
|
||||
{!required ? <option value="">{inheritLabel ?? '使用项目默认模型'}</option> : <option value="" disabled>请选择已配置模型</option>}
|
||||
{models.map((model) => <option key={model.modelRef} value={model.modelRef}>{model.label} · {model.runtimeProviderKey}</option>)}
|
||||
</select>;
|
||||
}
|
||||
|
||||
function AgentEditor({ agent, skills, models, onChange, onClose, onDelete }: { agent: ProjectAgentConfig; skills: SkillInfo[]; models: ConfiguredModelOption[]; onChange: (agent: ProjectAgentConfig) => void; onClose: () => void; onDelete?: () => void }) {
|
||||
function AgentEditor({ agent, skills, models, onChange, onClose, onArchive }: { agent: ProjectAgentConfig; skills: SkillInfo[]; models: ConfiguredModelOption[]; onChange: (agent: ProjectAgentConfig) => void; onClose: () => void; onArchive?: () => void }) {
|
||||
const editorRef = useRef<HTMLDivElement | null>(null);
|
||||
const [skillQuery, setSkillQuery] = useState('');
|
||||
const filteredSkills = useMemo(() => {
|
||||
@@ -97,7 +128,7 @@ function AgentEditor({ agent, skills, models, onChange, onClose, onDelete }: { a
|
||||
}, [skillQuery, skills]);
|
||||
const { contextSafe } = useGSAP(() => {
|
||||
if (prefersReducedMotion()) return;
|
||||
gsap.from('.agent-editor-section', { y: 14, autoAlpha: 0, duration: 0.36, stagger: 0.06, ease: 'power3.out' });
|
||||
gsap.from('.agent-editor-section', { y: 6, duration: 0.22, stagger: 0.035, ease: 'power3.out' });
|
||||
}, { scope: editorRef });
|
||||
const animateSelection = contextSafe((target: HTMLElement) => {
|
||||
if (prefersReducedMotion()) return;
|
||||
@@ -105,28 +136,27 @@ function AgentEditor({ agent, skills, models, onChange, onClose, onDelete }: { a
|
||||
});
|
||||
|
||||
return <div ref={editorRef} data-testid={`project-agent-${agent.id}`} className="space-y-4 pb-20">
|
||||
<section className="agent-editor-section rounded-lg border-2 border-[#26384D] bg-white p-4">
|
||||
<h3 className="font-black">基础设置</h3>
|
||||
<div className="mt-4"><Label>职能名称</Label><Input aria-label="职能名称" value={agent.roleName} readOnly={agent.builtIn} onChange={(event) => onChange({ ...agent, roleName: event.target.value })} className="mt-2 border-2 border-[#26384D] bg-[#EEF3F7] font-black" /></div>
|
||||
<div className="mt-4"><Label>伙伴名字 <span className="text-red-700">*</span></Label><Input aria-label="伙伴名字" required value={agent.name} maxLength={30} placeholder="例如:小明" onChange={(event) => onChange({ ...agent, name: event.target.value })} className="mt-2 border-2 border-[#26384D] bg-white" /></div>
|
||||
<div className="mt-4"><Label>伙伴头像</Label><div className="mt-2 grid grid-cols-4 gap-2">{agentAvatarOptions.map((option) => <button key={option.id} type="button" aria-label={`选择头像:${option.label}`} aria-pressed={agent.avatarId === option.id} onClick={(event) => { onChange({ ...agent, avatarId: option.id }); animateSelection(event.currentTarget); }} className={cn('overflow-hidden rounded-md border-2 border-[#26384D] bg-white p-1', agent.avatarId === option.id && 'bg-[#DCE6EF] shadow-[3px_3px_0_#26384D]')}><img src={option.src} alt="" className="aspect-square w-full rounded object-cover [image-rendering:pixelated]" /></button>)}</div></div>
|
||||
<div className="mt-4"><Label>模型覆盖(可选)</Label><ModelSelect label="模型覆盖(可选)" value={agent.model} models={models} inheritLabel="使用项目默认模型" onChange={(model) => onChange({ ...agent, model })} /></div>
|
||||
<section className="agent-editor-section rounded-lg border border-foreground/15 bg-white p-4">
|
||||
<h3 className="font-semibold">基础设置</h3>
|
||||
<div className="mt-4"><Label>职能名称</Label><Input aria-label="职能名称" value={agent.roleName} readOnly={agent.builtIn} onChange={(event) => onChange({ ...agent, roleName: event.target.value })} className="mt-2 border border-foreground/15 bg-surface-subtle font-semibold" /></div>
|
||||
<div className="mt-4"><Label>伙伴名字 <span className="text-red-700">*</span></Label><Input aria-label="伙伴名字" required value={agent.name} maxLength={30} placeholder="例如:小明" onChange={(event) => onChange({ ...agent, name: event.target.value })} className="mt-2 border border-foreground/15 bg-white" /></div>
|
||||
<div className="mt-4"><Label>伙伴头像 <span className="text-red-700">*</span></Label><div className="mt-2 grid grid-cols-4 gap-2">{agentAvatarOptions.map((option) => <button key={option.id} type="button" aria-label={`选择头像:${option.label}`} aria-pressed={agent.avatarId === option.id} onClick={(event) => { onChange({ ...agent, avatarId: option.id }); animateSelection(event.currentTarget); }} className={cn('overflow-hidden rounded-md border border-foreground/15 bg-white p-1', agent.avatarId === option.id && 'bg-brand-soft shadow-soft')}><img src={option.src} alt="" className="aspect-square w-full rounded object-cover [image-rendering:pixelated]" /></button>)}</div></div>
|
||||
<div className="mt-4"><Label>基础模型 <span className="text-red-700">*</span></Label><ModelSelect required label="基础模型" value={agent.model} models={models} onChange={(model) => onChange({ ...agent, model })} />{models.length === 0 ? <p className="mt-2 text-xs font-medium text-destructive">还没有可用模型,请先到模型管理完成配置。</p> : null}</div>
|
||||
<div className="mt-4"><Label>职责说明 <span className="text-red-700">*</span></Label><Textarea aria-label="职责说明" value={agent.responsibility.mission} maxLength={200} placeholder="例如:负责拆解产品需求、安排实现步骤并检查结果。" onChange={(event) => onChange({ ...agent, responsibility: { ...agent.responsibility, mission: event.target.value } })} className="mt-2 min-h-24 border border-foreground/15 bg-white" /><p className="mt-1 text-xs text-muted-foreground">用一句话告诉孩子:这个联系人主要负责什么。</p></div>
|
||||
</section>
|
||||
|
||||
<section className="agent-editor-section rounded-lg border-4 border-[#26384D] bg-[#EEF3F7] p-4 shadow-[4px_4px_0_#26384D]">
|
||||
<h3 className="font-black">Agent 提示词</h3><p className="mt-1 text-xs font-bold text-[#68788B]">这里定义伙伴的职责、文件范围、边界、协作关系与工作原则。</p>
|
||||
<Textarea aria-label="Agent 提示词" value={agent.prompt} onChange={(event) => onChange({ ...agent, prompt: event.target.value })} className="mt-3 min-h-[320px] border-2 border-[#26384D] bg-white font-mono text-xs leading-5" />
|
||||
</section>
|
||||
<details className="agent-editor-section rounded-lg border border-foreground/15 bg-surface-subtle p-4 shadow-soft">
|
||||
<summary className="cursor-pointer list-none font-semibold">高级设置(提示词与技能)</summary>
|
||||
<p className="mt-2 text-xs font-medium text-muted-foreground">先完成基础联系人配置;提示词和技能可以之后慢慢调整。</p>
|
||||
<Textarea aria-label="Agent 提示词" value={agent.prompt} onChange={(event) => onChange({ ...agent, prompt: event.target.value })} className="mt-3 min-h-[220px] border border-foreground/15 bg-white font-mono text-xs leading-5" placeholder="可选:补充这个联系人的工作方式与边界。" />
|
||||
<div className="mt-5 flex items-center justify-between gap-3"><h3 className="font-semibold">绑定技能</h3><Badge className="border border-foreground/15 bg-accent-soft text-foreground">已选 {agent.skillIds.length}</Badge></div>
|
||||
<div className="relative mt-3"><Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" /><Input aria-label="搜索技能" value={skillQuery} onChange={(event) => setSkillQuery(event.target.value)} placeholder="搜索技能" className="border border-foreground/15 bg-white pl-9" /></div>
|
||||
<div className="mt-3 space-y-2">{filteredSkills.map((skill) => { const checked = agent.skillIds.includes(skill.id); return <label key={skill.id} className={cn('flex cursor-pointer items-start gap-2 rounded-md border border-foreground/15 p-2.5', checked ? 'bg-surface-subtle' : 'bg-white')}><input type="checkbox" aria-label={`绑定技能:${skill.name}`} checked={checked} onChange={() => { onChange({ ...agent, skillIds: checked ? agent.skillIds.filter((id) => id !== skill.id) : [...agent.skillIds, skill.id] }); }} /><span><span className="text-sm font-semibold">{skill.name}</span><span className="mt-0.5 block text-xs text-muted-foreground">{skill.description}</span></span></label>; })}{filteredSkills.length === 0 ? <p className="rounded-md border border-dashed border-foreground/15 p-3 text-center text-xs font-medium text-muted-foreground">没有匹配的技能</p> : null}</div>
|
||||
</details>
|
||||
|
||||
<section className="agent-editor-section rounded-lg border-2 border-[#26384D] bg-white p-4">
|
||||
<div className="flex items-center justify-between gap-3"><h3 className="font-black">绑定技能</h3><Badge className="border-2 border-[#26384D] bg-[#FDE8E1] text-[#26384D]">已选 {agent.skillIds.length}</Badge></div>
|
||||
<div className="relative mt-3"><Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[#68788B]" /><Input aria-label="搜索技能" value={skillQuery} onChange={(event) => setSkillQuery(event.target.value)} placeholder="搜索技能" className="border-2 border-[#26384D] bg-white pl-9" /></div>
|
||||
<div className="mt-3 space-y-2">{filteredSkills.map((skill) => { const required = skill.id === YOUTH_PLAIN_LANGUAGE_SKILL_ID; const checked = required || agent.skillIds.includes(skill.id); return <label key={skill.id} className={cn('flex items-start gap-2 rounded-md border-2 border-[#26384D] p-2.5', required ? 'cursor-not-allowed' : 'cursor-pointer', checked ? 'bg-[#EEF3F7]' : 'bg-white')}><input type="checkbox" aria-label={`绑定技能:${skill.name}`} checked={checked} disabled={required} onChange={(event) => { onChange({ ...agent, skillIds: checked ? agent.skillIds.filter((id) => id !== skill.id) : [...agent.skillIds, skill.id] }); animateSelection(event.currentTarget.closest('label') as HTMLElement); }} /><span><span className="flex items-center gap-2 text-sm font-black">{skill.name}{required ? <Badge className="border border-[#26384D] bg-[#DCE6EF] px-1.5 py-0 text-[10px] text-[#26384D]">所有伙伴必需</Badge> : null}</span><span className="mt-0.5 block text-xs text-[#68788B]">{skill.description}</span></span></label>; })}{filteredSkills.length === 0 ? <p className="rounded-md border-2 border-dashed border-[#26384D] p-3 text-center text-xs font-bold text-[#68788B]">没有匹配的技能</p> : null}</div>
|
||||
</section>
|
||||
|
||||
<div className="agent-editor-section sticky bottom-0 z-20 flex items-center gap-2 rounded-lg border-4 border-[#26384D] bg-[#FFFFFF] p-3 shadow-[0_-4px_0_#26384D]">
|
||||
{onDelete ? <Button type="button" variant="outline" size="icon" aria-label="删除自定义 Agent" className="shrink-0 border-2 border-[#26384D] bg-[#FCE9E5]" onClick={onDelete}><Trash2 className="h-4 w-4" /></Button> : null}
|
||||
<Button type="button" className="flex-1 border-2 border-[#26384D] bg-[#DCE6EF] font-black text-[#26384D]" onClick={onClose}><Check className="mr-2 h-4 w-4" />完成配置</Button>
|
||||
<div className="agent-editor-section sticky bottom-0 z-20 flex items-center gap-2 rounded-lg border border-foreground/15 bg-background p-3 shadow-soft">
|
||||
{onArchive ? <Button type="button" variant="outline" aria-label="归档联系人" className="shrink-0 border border-foreground/15 bg-accent-soft" onClick={onArchive}><Trash2 className="mr-2 h-4 w-4" />归档</Button> : null}
|
||||
<Button type="button" className="flex-1 border border-foreground/15 bg-brand-soft font-semibold text-foreground" onClick={onClose}><Check className="mr-2 h-4 w-4" />完成配置</Button>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
@@ -140,6 +170,13 @@ export function ProjectConfiguration() {
|
||||
const uploadKnowledge = useProjectConfigStore((state) => state.uploadKnowledge);
|
||||
const removeProjectConfig = useProjectConfigStore((state) => state.remove);
|
||||
const removeProject = useOpencodeStore((state) => state.removeProject);
|
||||
const abortSession = useOpencodeStore((state) => state.abortSession);
|
||||
const deleteOpencodeSession = useOpencodeStore((state) => state.deleteSession);
|
||||
const sessionStatuses = useOpencodeStore((state) => state.sessionStatuses);
|
||||
const sendingSessionIds = useOpencodeStore((state) => state.sendingSessionIds);
|
||||
const queuedSessionPrompts = useOpencodeStore((state) => state.queuedSessionPrompts);
|
||||
const loadConversationState = useProjectConversationStore((state) => state.load);
|
||||
const deleteProjectSession = useProjectConversationStore((state) => state.deleteSession);
|
||||
const providerAccounts = useProviderStore((state) => state.accounts);
|
||||
const providerStatuses = useProviderStore((state) => state.statuses);
|
||||
const providerDefaultAccountId = useProviderStore((state) => state.defaultAccountId);
|
||||
@@ -150,9 +187,13 @@ export function ProjectConfiguration() {
|
||||
const [skills, setSkills] = useState<SkillInfo[]>([]);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [createPartnerOpen, setCreatePartnerOpen] = useState(false);
|
||||
const [archiveAgentId, setArchiveAgentId] = useState<string | null>(null);
|
||||
const [deleteAgentId, setDeleteAgentId] = useState<string | null>(null);
|
||||
const pageRef = useRef<HTMLElement | null>(null);
|
||||
const knowledgeInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const loadSkills = useCallback(() => {
|
||||
void hostApiFetch<{ skills?: RuntimeSkillInfo[] }>('/api/opencode/skills')
|
||||
.then((result) => setSkills((result.skills ?? []).map((skill) => ({ id: skill.name, ...getSkillDisplayInfo(skill.name) }))))
|
||||
@@ -160,7 +201,13 @@ export function ProjectConfiguration() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => { if (activeProject) void load(activeProject.id); }, [activeProject, load]);
|
||||
useEffect(() => { if (stored) setDraft({ ...structuredClone(stored), agents: stored.agents.map((agent) => ({ ...agent, enabled: true })) }); }, [stored]);
|
||||
useEffect(() => { if (stored) setDraft(structuredClone(stored)); }, [stored]);
|
||||
useEffect(() => {
|
||||
const requestedAgentId = new URLSearchParams(location.search).get('agent');
|
||||
if (!requestedAgentId || !draft?.agents.some((agent) => agent.id === requestedAgentId)) return;
|
||||
setActiveAgentId(requestedAgentId);
|
||||
setDrawerMode('agent');
|
||||
}, [draft, location.search]);
|
||||
useEffect(() => { loadSkills(); }, [loadSkills]);
|
||||
useEffect(() => {
|
||||
if (drawerMode === 'skills' || drawerMode === 'agent') loadSkills();
|
||||
@@ -171,22 +218,32 @@ export function ProjectConfiguration() {
|
||||
const { contextSafe } = useGSAP(() => {
|
||||
if (!draft || prefersReducedMotion()) return;
|
||||
gsap.timeline({ defaults: { ease: 'power3.out' } })
|
||||
.from('.config-motion-title', { y: 14, autoAlpha: 0, duration: 0.42 })
|
||||
.from('.config-motion-resource', { y: 18, autoAlpha: 0, duration: 0.42, stagger: 0.07 }, '-=0.22')
|
||||
.from('.config-motion-partners', { y: 22, autoAlpha: 0, duration: 0.48 }, '-=0.25')
|
||||
.from('.config-motion-agent', { y: 18, autoAlpha: 0, scale: 0.97, duration: 0.38, stagger: 0.055 }, '-=0.3')
|
||||
.from('.config-motion-actions', { y: 10, autoAlpha: 0, duration: 0.34 }, '-=0.22');
|
||||
// Keep cards in their layout position on first paint. Their deliberate
|
||||
// motion belongs to hover/open/save feedback, not page load.
|
||||
.from('.config-motion-title', { y: 6, duration: 0.22 });
|
||||
}, { scope: pageRef, dependencies: [Boolean(draft)], revertOnUpdate: true });
|
||||
const playSaveConfirmation = contextSafe(() => {
|
||||
if (prefersReducedMotion()) return;
|
||||
gsap.fromTo('.project-save-button', { scale: 0.96 }, { scale: 1, duration: 0.42, ease: 'back.out(2.4)', overwrite: 'auto' });
|
||||
});
|
||||
|
||||
if (!activeProject) return <div className="mx-auto max-w-2xl rounded-lg border-4 border-[#26384D] bg-[#FFFFFF] p-8 text-center shadow-[6px_6px_0_#26384D]"><Settings2 className="mx-auto h-8 w-8" /><h1 className="mt-3 text-xl font-black">请先新建项目</h1></div>;
|
||||
if (!draft) return <div className="p-8 text-center text-sm font-bold">正在读取项目配置…</div>;
|
||||
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>;
|
||||
|
||||
const updateAgent = (next: ProjectAgentConfig) => setDraft((current) => current ? { ...current, agents: current.agents.map((item) => item.id === next.id ? next : item) } : current);
|
||||
const submit = async () => { const enabledAgents = draft.agents.map((agent) => ({ ...agent, enabled: true })); const errors = validateAgentNames(enabledAgents); if (errors.length) { toast.error('伙伴名字必须非空、项目内唯一且不超过 30 个字符'); return; } setSaving(true); try { await save(activeProject.id, { ...draft, agents: enabledAgents, initialized: true }); playSaveConfirmation(); toast.success(draft.initialized ? '项目配置已更新' : '项目初始化完成'); } catch (error) { toast.error(error instanceof Error ? error.message : String(error)); } finally { setSaving(false); } };
|
||||
const updateAgent = (next: ProjectAgentConfig) => setDraft((current) => current ? { ...current, agents: current.agents.map((item) => item.id === next.id ? { ...next, updatedAt: new Date().toISOString() } : item) } : current);
|
||||
const handleCreatePartner = async (input: AgentCreationInput) => {
|
||||
const agent = {
|
||||
...createCustomAgent(draft.agents.length + 1, input.model),
|
||||
name: input.name,
|
||||
avatarId: input.avatarId,
|
||||
model: input.model,
|
||||
prompt: input.prompt,
|
||||
skillIds: input.skillIds,
|
||||
responsibility: { mission: input.responsibility, owns: [], boundaries: [], collaborators: [], principles: [] },
|
||||
};
|
||||
setDraft({ ...draft, agents: [...draft.agents, agent] });
|
||||
};
|
||||
const submit = async () => { const errors = validateAgentConfigs(draft.agents); if (errors.length) { toast.error('请补全联系人名称、头像、基础模型和职责;名称还需要在项目内唯一。'); return; } setSaving(true); try { await save(activeProject.id, { ...draft, initialized: true }); playSaveConfirmation(); toast.success(draft.initialized ? '项目配置已更新' : '项目初始化完成'); } catch (error) { toast.error(error instanceof Error ? error.message : String(error)); } finally { setSaving(false); } };
|
||||
const handleKnowledge = async (file?: File) => { if (!file) return; try { await uploadKnowledge(activeProject.id, file); toast.success(`已上传到 knowledge/${file.name}`); } catch (error) { toast.error(error instanceof Error ? error.message : String(error)); } };
|
||||
const handleDeleteProject = async () => {
|
||||
await removeProject(activeProject.id);
|
||||
@@ -195,24 +252,114 @@ export function ProjectConfiguration() {
|
||||
toast.success(`已移除项目 ${activeProject.name}`);
|
||||
navigate('/project-config');
|
||||
};
|
||||
const handleArchiveAgent = async () => {
|
||||
if (!archiveAgentId) return;
|
||||
const conversationState = await loadConversationState(activeProject.id);
|
||||
const sessionIds = conversationState.sessions
|
||||
.filter((session) => session.agentId === archiveAgentId)
|
||||
.map((session) => session.sessionId)
|
||||
.filter((sessionId) => (
|
||||
Boolean(sendingSessionIds[sessionId])
|
||||
|| sessionStatuses[sessionId]?.type === 'busy'
|
||||
|| sessionStatuses[sessionId]?.type === 'retry'
|
||||
|| (queuedSessionPrompts[sessionId]?.length ?? 0) > 0
|
||||
));
|
||||
await Promise.all(sessionIds.map((sessionId) => abortSession(sessionId)));
|
||||
const archivedAt = new Date().toISOString();
|
||||
setDraft((current) => current ? {
|
||||
...current,
|
||||
agents: current.agents.map((agent) => agent.id === archiveAgentId
|
||||
? { ...agent, archivedAt, updatedAt: archivedAt }
|
||||
: agent),
|
||||
} : current);
|
||||
setArchiveAgentId(null);
|
||||
setDrawerMode(null);
|
||||
};
|
||||
|
||||
return <main ref={pageRef} className="-m-6 min-h-full overflow-auto bg-[#FFFFFF] p-4 text-[#26384D]" data-testid="project-configuration-page">
|
||||
const handleDeleteAgent = async () => {
|
||||
if (!deleteAgentId) return;
|
||||
const target = draft.agents.find((agent) => agent.id === deleteAgentId);
|
||||
if (!target || target.builtIn || !target.archivedAt) return;
|
||||
const conversationState = await loadConversationState(activeProject.id);
|
||||
const sessionIds = conversationState.sessions
|
||||
.filter((session) => session.agentId === target.id)
|
||||
.map((session) => session.sessionId);
|
||||
const runningSessionIds = sessionIds.filter((sessionId) => (
|
||||
Boolean(sendingSessionIds[sessionId])
|
||||
|| sessionStatuses[sessionId]?.type === 'busy'
|
||||
|| sessionStatuses[sessionId]?.type === 'retry'
|
||||
|| (queuedSessionPrompts[sessionId]?.length ?? 0) > 0
|
||||
));
|
||||
await Promise.all(runningSessionIds.map((sessionId) => abortSession(sessionId)));
|
||||
for (const sessionId of sessionIds) {
|
||||
try {
|
||||
await deleteOpencodeSession(sessionId);
|
||||
} catch (error) {
|
||||
if (!isMissingRuntimeSessionError(error)) throw error;
|
||||
}
|
||||
await deleteProjectSession(activeProject.id, sessionId);
|
||||
}
|
||||
const nextDraft = {
|
||||
...draft,
|
||||
agents: draft.agents.filter((agent) => agent.id !== target.id),
|
||||
};
|
||||
await save(activeProject.id, nextDraft);
|
||||
setDraft(nextDraft);
|
||||
setDeleteAgentId(null);
|
||||
if (activeAgentId === target.id) {
|
||||
setActiveAgentId(null);
|
||||
setDrawerMode(null);
|
||||
}
|
||||
toast.success(`已永久删除联系人“${target.name || '未命名联系人'}”`);
|
||||
};
|
||||
|
||||
return <main ref={pageRef} className="-m-6 min-h-full overflow-auto bg-background p-4 text-foreground" data-testid="project-configuration-page">
|
||||
<div className="mx-auto grid max-w-7xl gap-3">
|
||||
<h1 className="config-motion-title text-xl font-black">在这里配置你的项目基础!</h1>
|
||||
{!draft.initialized ? <div className="rounded-lg border-4 border-[#26384D] bg-[#E9EFF5] px-5 py-3 text-sm font-black shadow-[4px_4px_0_#26384D]">为每个预设职能填写伙伴名字并保存后,即可使用项目全部功能。</div> : null}
|
||||
<h1 className="config-motion-title text-xl font-semibold">配置你的项目空间</h1>
|
||||
{!draft.initialized ? <div className="rounded-lg border border-foreground/15 bg-surface-tertiary px-5 py-3 text-sm font-semibold shadow-soft">新项目还没有联系人。点击“新增伙伴”,为它设置名字、头像、基础模型和职责后即可开始对话。</div> : null}
|
||||
<section className="grid gap-3 md:grid-cols-3"><ResourceCard id="models" icon="🧠" title="大脑(大语言模型)" subtitle={draft.defaultModel ? `默认:${draft.defaultModel}` : '使用运行时默认模型'} onClick={() => setDrawerMode('models')} /><ResourceCard id="skills" icon="⚡" title="工具箱(技能)" subtitle={`已安装${skills.length}个`} onClick={() => setDrawerMode('skills')} /><ResourceCard id="knowledge" icon="📓" title="笔记本(知识库)" subtitle={`已上传${knowledge.length}个`} onClick={() => setDrawerMode('knowledge')} /></section>
|
||||
<section className="config-motion-partners rounded-lg border-4 border-[#26384D] bg-[#E9EFF5] p-5 shadow-[6px_6px_0_#26384D]"><div className="mb-4 flex items-center justify-between gap-3"><div><h2 className="text-xl font-black">我的伙伴</h2><p className="mt-1 text-sm font-bold text-[#68788B]">每个 Agent 只属于当前项目,职能由模板预设,伙伴名字由你决定。</p></div><Button className="border-2 border-[#26384D] bg-[#DCE6EF] text-[#26384D]" onClick={() => { const agent = createCustomAgent(draft.agents.length + 1); setDraft({ ...draft, agents: [...draft.agents, agent] }); setActiveAgentId(agent.id); setDrawerMode('agent'); }}><Plus className="mr-2 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.map((agent) => <AgentCard key={agent.id} agent={agent} onOpen={() => { setActiveAgentId(agent.id); setDrawerMode('agent'); }} />)}</div></section>
|
||||
<div className="config-motion-actions sticky bottom-3 flex items-center justify-between gap-3">
|
||||
<Button type="button" variant="outline" onClick={() => setDeleteDialogOpen(true)} className="h-12 border-2 border-[#26384D] bg-[#FCE9E5] px-5 font-black text-[#26384D] shadow-[4px_4px_0_#26384D]"><Trash2 className="mr-2 h-5 w-5" />删除项目</Button>
|
||||
<Button onClick={() => void submit()} disabled={saving} className="project-save-button h-12 border-2 border-[#26384D] bg-[#3A5578] px-6 font-black text-white shadow-[4px_4px_0_#26384D]"><Check className="mr-2 h-5 w-5" />{saving ? '保存中…' : draft.initialized ? '保存项目配置' : '确认并完成初始化'}</Button>
|
||||
<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 className="border border-brand/20 bg-brand-soft text-brand" onClick={() => { if (modelOptions.length === 0) { toast.error('请先在模型管理中配置至少一个可用模型。'); setDrawerMode('models'); return; } setCreatePartnerOpen(true); }}><Plus className="mr-2 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); setDrawerMode('agent'); }} />)}{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) => <div key={agent.id} className="flex items-center gap-1"><Button type="button" variant="outline" className="border-border/70 bg-surface-subtle" onClick={() => updateAgent({ ...agent, archivedAt: null })}>{agent.name || '未命名联系人'} · 恢复</Button>{!agent.builtIn ? <Button type="button" variant="outline" size="icon" aria-label={'永久删除联系人 ' + (agent.name || '未命名联系人')} title="永久删除联系人" className="h-9 w-9 border-border/70 text-muted-foreground hover:bg-accent-soft hover:text-destructive" onClick={() => setDeleteAgentId(agent.id)}><Trash2 className="h-3.5 w-3.5" /></Button> : null}</div>)}</div></div> : null}</section>
|
||||
<div className="glass-surface config-motion-actions sticky bottom-3 flex items-center justify-between gap-3 rounded-2xl border border-border/70 bg-background/85 p-2 shadow-float">
|
||||
<Button type="button" variant="outline" onClick={() => setDeleteDialogOpen(true)} className="h-12 border border-foreground/15 bg-accent-soft px-5 font-semibold text-foreground shadow-soft"><Trash2 className="mr-2 h-5 w-5" />删除项目</Button>
|
||||
<Button onClick={() => void submit()} disabled={saving} className="project-save-button h-12 border border-brand/20 bg-brand px-6 font-semibold text-primary-foreground"><Check className="mr-2 h-5 w-5" />{saving ? '保存中…' : draft.initialized ? '保存项目配置' : '确认并完成初始化'}</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Sheet open={drawerMode !== null} onOpenChange={(open) => !open && setDrawerMode(null)}><SheetContent className="flex w-[min(430px,94vw)] flex-col overflow-hidden border-l-4 border-[#26384D] bg-[#FFFFFF] p-0 text-[#26384D] sm:max-w-none">
|
||||
{drawerMode === 'models' ? <DrawerFrame title="大脑(大语言模型)" desc="设置当前项目的默认模型。" onClose={() => setDrawerMode(null)}><Label>项目默认模型</Label><ModelSelect label="项目默认模型" value={draft.defaultModel} models={modelOptions} inheritLabel="使用运行时默认模型" onChange={(defaultModel) => setDraft({ ...draft, defaultModel })} />{modelOptions.length === 0 ? <p className="mt-3 text-xs font-bold text-[#8a3d2f]">暂无已配置可用模型,请先在模型管理中完成配置。</p> : null}</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-2 border-[#26384D] bg-[#DCE6EF] text-[#26384D]" 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-2 border-[#26384D] bg-white p-3 font-black">{file}</div>) : <div className="rounded-lg border-2 border-dashed border-[#26384D] bg-white p-4 text-sm font-bold">当前还没有知识文件。</div>}</div></DrawerFrame> : null}
|
||||
{drawerMode === 'skills' ? <DrawerFrame title="工具箱(技能)" desc="这里展示已安装技能;绑定请进入伙伴配置。" onClose={() => setDrawerMode(null)}><div className="space-y-3"><SuperpowersCard enabled={draft.superpowersEnabled} onChange={(superpowersEnabled) => setDraft({ ...draft, superpowersEnabled })} />{skills.map((skill) => <div key={skill.id} className="rounded-lg border-2 border-[#26384D] bg-white p-3"><p className="font-black">⚡ {skill.name}</p><p className="mt-2 text-sm text-[#68788B]">{skill.description}</p></div>)}</div></DrawerFrame> : null}
|
||||
{drawerMode === 'agent' && activeAgent ? <DrawerFrame title={`${activeAgent.name || '未命名'} · 伙伴配置`} desc={`${activeAgent.roleName} · 设置名字、完整提示词、模型和技能。`} onClose={() => setDrawerMode(null)}><AgentEditor agent={activeAgent} skills={skills} models={modelOptions} onChange={updateAgent} onClose={() => setDrawerMode(null)} onDelete={activeAgent.builtIn ? undefined : () => { setDraft({ ...draft, agents: draft.agents.filter((agent) => agent.id !== activeAgent.id) }); setDrawerMode(null); }} /></DrawerFrame> : null}
|
||||
<AgentCreationDialog
|
||||
open={createPartnerOpen}
|
||||
onOpenChange={setCreatePartnerOpen}
|
||||
modelOptions={modelOptions}
|
||||
skills={skills}
|
||||
existingAgentNames={draft.agents.map((agent) => agent.name)}
|
||||
onCreate={handleCreatePartner}
|
||||
submitLabel="创建伙伴"
|
||||
/>
|
||||
<Sheet open={drawerMode !== null} onOpenChange={(open) => !open && setDrawerMode(null)}><SheetContent className="glass-surface flex w-[min(430px,94vw)] flex-col overflow-hidden border-l border-border/70 bg-background/90 p-0 text-foreground sm:max-w-none">
|
||||
{drawerMode === 'models' ? <DrawerFrame title="大脑(大语言模型)" desc="设置当前项目的默认模型。" onClose={() => setDrawerMode(null)}><Label>项目默认模型</Label><ModelSelect label="项目默认模型" value={draft.defaultModel} models={modelOptions} inheritLabel="使用运行时默认模型" onChange={(defaultModel) => setDraft({ ...draft, defaultModel })} />{modelOptions.length === 0 ? <p className="mt-3 text-xs font-medium text-destructive">暂无已配置可用模型,请先在模型管理中完成配置。</p> : null}</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' ? <DrawerFrame title="工具箱(技能)" desc="这里展示已安装技能;绑定请进入伙伴配置。" onClose={() => setDrawerMode(null)}><div className="space-y-3"><SuperpowersCard enabled={draft.superpowersEnabled} onChange={(superpowersEnabled) => setDraft({ ...draft, superpowersEnabled })} />{skills.map((skill) => <div key={skill.id} className="rounded-lg border border-foreground/15 bg-white p-3"><p className="font-semibold">⚡ {skill.name}</p><p className="mt-2 text-sm text-muted-foreground">{skill.description}</p></div>)}</div></DrawerFrame> : null}
|
||||
{drawerMode === 'agent' && activeAgent ? <DrawerFrame title={`${activeAgent.name || '未命名'} · 联系人配置`} desc={`${activeAgent.roleName} · 先完成基础设置,高级能力可以之后补充。`} onClose={() => setDrawerMode(null)}><AgentEditor agent={activeAgent} skills={skills} models={modelOptions} onChange={updateAgent} onClose={() => setDrawerMode(null)} onArchive={activeAgent.builtIn ? undefined : () => setArchiveAgentId(activeAgent.id)} /></DrawerFrame> : null}
|
||||
</SheetContent></Sheet>
|
||||
<ConfirmDialog
|
||||
open={Boolean(archiveAgentId)}
|
||||
title={`归档联系人“${draft.agents.find((agent) => agent.id === archiveAgentId)?.name || '未命名联系人'}”?`}
|
||||
message="归档会停止后续使用入口,但会保留联系人和全部会话记录;你可以之后在联系人列表底部恢复。"
|
||||
confirmLabel="确认归档"
|
||||
cancelLabel="取消"
|
||||
onCancel={() => setArchiveAgentId(null)}
|
||||
onConfirm={handleArchiveAgent}
|
||||
onError={(error) => toast.error(error instanceof Error ? error.message : String(error))}
|
||||
/>
|
||||
<ConfirmDialog
|
||||
open={Boolean(deleteAgentId)}
|
||||
title={`永久删除联系人“${draft.agents.find((agent) => agent.id === deleteAgentId)?.name || '未命名联系人'}”?`}
|
||||
message="这会永久删除联系人及其全部会话记录,删除后无法恢复。"
|
||||
confirmLabel="永久删除"
|
||||
cancelLabel="取消"
|
||||
variant="destructive"
|
||||
onCancel={() => setDeleteAgentId(null)}
|
||||
onConfirm={handleDeleteAgent}
|
||||
onError={(error) => toast.error(error instanceof Error ? error.message : String(error))}
|
||||
/>
|
||||
<ConfirmDialog
|
||||
open={deleteDialogOpen}
|
||||
title={`删除项目“${activeProject.name}”?`}
|
||||
|
||||
Reference in New Issue
Block a user