Makelore 2.0 initial clean snapshot
This commit is contained in:
230
src/pages/ProjectConfiguration/index.tsx
Normal file
230
src/pages/ProjectConfiguration/index.tsx
Normal file
@@ -0,0 +1,230 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { 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 { 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 { Switch } from '@/components/ui/switch';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
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 { 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';
|
||||
|
||||
const EMPTY_FILES: string[] = [];
|
||||
type DrawerMode = 'models' | 'knowledge' | 'skills' | 'agent' | null;
|
||||
type RuntimeSkillInfo = { name: string; description?: string; location?: string };
|
||||
type SkillInfo = { id: string; name: string; description: string };
|
||||
|
||||
gsap.registerPlugin(useGSAP);
|
||||
|
||||
function prefersReducedMotion() {
|
||||
return typeof window === 'undefined'
|
||||
|| typeof window.matchMedia !== 'function'
|
||||
|| 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 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;
|
||||
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>
|
||||
</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>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function DrawerFrame({ title, desc, onClose, children }: { title: string; desc: string; onClose: () => void; children: React.ReactNode }) {
|
||||
const drawerRef = useRef<HTMLDivElement | null>(null);
|
||||
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');
|
||||
}, { 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>;
|
||||
}
|
||||
|
||||
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;
|
||||
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>;
|
||||
}
|
||||
|
||||
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>
|
||||
{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 }) {
|
||||
const editorRef = useRef<HTMLDivElement | null>(null);
|
||||
const [skillQuery, setSkillQuery] = useState('');
|
||||
const filteredSkills = useMemo(() => {
|
||||
const query = skillQuery.trim().toLocaleLowerCase();
|
||||
if (!query) return skills;
|
||||
return skills.filter((skill) => `${skill.name} ${skill.description ?? ''}`.toLocaleLowerCase().includes(query));
|
||||
}, [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' });
|
||||
}, { scope: editorRef });
|
||||
const animateSelection = contextSafe((target: HTMLElement) => {
|
||||
if (prefersReducedMotion()) return;
|
||||
gsap.fromTo(target, { scale: 0.96 }, { scale: 1, duration: 0.34, ease: 'back.out(2.2)', overwrite: 'auto' });
|
||||
});
|
||||
|
||||
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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</div>;
|
||||
}
|
||||
|
||||
export function ProjectConfiguration() {
|
||||
const activeProject = useOpencodeStore((state) => state.activeProject);
|
||||
const stored = useProjectConfigStore((state) => activeProject ? state.configsByProjectId[activeProject.id] : undefined);
|
||||
const knowledge = useProjectConfigStore((state) => activeProject ? state.knowledgeByProjectId[activeProject.id] ?? EMPTY_FILES : EMPTY_FILES);
|
||||
const load = useProjectConfigStore((state) => state.load);
|
||||
const save = useProjectConfigStore((state) => state.save);
|
||||
const uploadKnowledge = useProjectConfigStore((state) => state.uploadKnowledge);
|
||||
const removeProjectConfig = useProjectConfigStore((state) => state.remove);
|
||||
const removeProject = useOpencodeStore((state) => state.removeProject);
|
||||
const providerAccounts = useProviderStore((state) => state.accounts);
|
||||
const providerStatuses = useProviderStore((state) => state.statuses);
|
||||
const providerDefaultAccountId = useProviderStore((state) => state.defaultAccountId);
|
||||
const refreshProviderSnapshot = useProviderStore((state) => state.refreshProviderSnapshot);
|
||||
const [draft, setDraft] = useState<ProjectConfig | null>(null);
|
||||
const [drawerMode, setDrawerMode] = useState<DrawerMode>(null);
|
||||
const [activeAgentId, setActiveAgentId] = useState<string | null>(null);
|
||||
const [skills, setSkills] = useState<SkillInfo[]>([]);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const pageRef = useRef<HTMLElement | null>(null);
|
||||
const knowledgeInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const navigate = useNavigate();
|
||||
const loadSkills = useCallback(() => {
|
||||
void hostApiFetch<{ skills?: RuntimeSkillInfo[] }>('/api/opencode/skills')
|
||||
.then((result) => setSkills((result.skills ?? []).map((skill) => ({ id: skill.name, ...getSkillDisplayInfo(skill.name) }))))
|
||||
.catch(() => setSkills([]));
|
||||
}, []);
|
||||
|
||||
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(() => { loadSkills(); }, [loadSkills]);
|
||||
useEffect(() => {
|
||||
if (drawerMode === 'skills' || drawerMode === 'agent') loadSkills();
|
||||
}, [drawerMode, loadSkills]);
|
||||
useEffect(() => { void refreshProviderSnapshot(); }, [refreshProviderSnapshot]);
|
||||
const modelOptions = useMemo(() => buildConfiguredModelOptions(providerAccounts, providerStatuses, providerDefaultAccountId), [providerAccounts, providerStatuses, providerDefaultAccountId]);
|
||||
const activeAgent = draft?.agents.find((agent) => agent.id === activeAgentId) ?? null;
|
||||
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');
|
||||
}, { 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>;
|
||||
|
||||
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 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);
|
||||
removeProjectConfig(activeProject.id);
|
||||
setDeleteDialogOpen(false);
|
||||
toast.success(`已移除项目 ${activeProject.name}`);
|
||||
navigate('/project-config');
|
||||
};
|
||||
|
||||
return <main ref={pageRef} className="-m-6 min-h-full overflow-auto bg-[#FFFFFF] p-4 text-[#26384D]" 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}
|
||||
<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>
|
||||
</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}
|
||||
</SheetContent></Sheet>
|
||||
<ConfirmDialog
|
||||
open={deleteDialogOpen}
|
||||
title={`删除项目“${activeProject.name}”?`}
|
||||
message="项目会从 Makelore 的项目列表中移除,但不会删除磁盘中的项目文件。此操作需要你明确确认。"
|
||||
confirmLabel="确认删除项目"
|
||||
cancelLabel="取消"
|
||||
variant="destructive"
|
||||
onCancel={() => setDeleteDialogOpen(false)}
|
||||
onConfirm={handleDeleteProject}
|
||||
onError={(error) => toast.error(error instanceof Error ? error.message : String(error))}
|
||||
/>
|
||||
</main>;
|
||||
}
|
||||
|
||||
export default ProjectConfiguration;
|
||||
Reference in New Issue
Block a user