feat: update Makelore modules and conversations
Some checks failed
Electron E2E / Electron E2E (macos-latest) (push) Has been cancelled
Electron E2E / Electron E2E (ubuntu-latest) (push) Has been cancelled
Electron E2E / Electron E2E (windows-latest) (push) Has been cancelled

This commit is contained in:
inman
2026-07-31 10:08:41 +08:00
parent b8ca3f8eea
commit 80e8386fa6
175 changed files with 8036 additions and 10581 deletions

View File

@@ -0,0 +1,161 @@
import { useEffect, useMemo, useState } from 'react';
import { Search } from 'lucide-react';
import { toast } from 'sonner';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { agentAvatarOptions } from '@/lib/agent-avatars';
import { formatModelRefLabel, type ConfiguredModelOption } from '@/lib/model-options';
import { cn } from '@/lib/utils';
export type AgentCreationSkillOption = {
id: string;
name: string;
description: string;
};
export type AgentCreationInput = {
name: string;
avatarId: string;
model: string;
responsibility: string;
prompt: string;
skillIds: string[];
};
type AgentCreationDialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
modelOptions: ConfiguredModelOption[];
skills?: AgentCreationSkillOption[];
existingAgentNames?: string[];
onCreate: (input: AgentCreationInput) => Promise<void>;
title?: string;
description?: string;
submitLabel?: string;
};
function createInitialInput(modelOptions: ConfiguredModelOption[]): AgentCreationInput {
return {
name: '',
avatarId: agentAvatarOptions[0]?.id ?? 'avatar-01',
model: modelOptions[0]?.modelRef ?? '',
responsibility: '',
prompt: '',
skillIds: [],
};
}
export function AgentCreationDialog({
open,
onOpenChange,
modelOptions,
skills = [],
existingAgentNames = [],
onCreate,
title = '创建项目联系人',
description = '先设置能开始对话所需的基础信息,也可以直接补充提示词和技能。',
submitLabel = '创建并开始对话',
}: AgentCreationDialogProps) {
const [input, setInput] = useState<AgentCreationInput>(() => createInitialInput(modelOptions));
const [creating, setCreating] = useState(false);
const [skillQuery, setSkillQuery] = useState('');
const [avatarPickerOpen, setAvatarPickerOpen] = useState(false);
useEffect(() => {
if (!open) return;
setInput(createInitialInput(modelOptions));
setSkillQuery('');
setAvatarPickerOpen(false);
}, [open, modelOptions]);
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 selectedAvatar = agentAvatarOptions.find((option) => option.id === input.avatarId) ?? agentAvatarOptions[0];
const handleCreate = async () => {
const name = input.name.trim();
const responsibility = input.responsibility.trim();
if (!name || !responsibility || !input.model) {
toast.error('请填写联系人名称、职责,并选择基础模型。');
return;
}
if (existingAgentNames.some((agentName) => agentName.trim() === name)) {
toast.error('联系人名称需要在当前项目内唯一。');
return;
}
setCreating(true);
try {
await onCreate({ ...input, name, responsibility });
onOpenChange(false);
} catch (error) {
toast.error(error instanceof Error ? error.message : String(error));
} finally {
setCreating(false);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-h-[min(820px,calc(100vh-2rem))] overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-1">
<div>
<label htmlFor="agent-create-name" className="text-sm font-semibold">联系人名称 <span className="text-destructive">*</span></label>
<div className="mt-2 flex items-center gap-3">
<button type="button" aria-label="更换头像" title="更换头像" aria-haspopup="dialog" onClick={() => setAvatarPickerOpen(true)} className="motion-press h-10 w-10 shrink-0 overflow-hidden rounded-xl border border-brand bg-brand-soft p-0.5 shadow-soft">
<img src={selectedAvatar?.src} alt={selectedAvatar?.label ?? '当前头像'} className="h-full w-full rounded-lg object-cover [image-rendering:pixelated]" />
</button>
<Input id="agent-create-name" value={input.name} maxLength={30} placeholder="例如:小明" onChange={(event) => setInput((current) => ({ ...current, name: event.target.value }))} className="mt-0 flex-1" />
</div>
</div>
<div>
<label htmlFor="agent-create-model" className="text-sm font-semibold">基础模型 <span className="text-destructive">*</span></label>
<select id="agent-create-model" value={input.model} onChange={(event) => setInput((current) => ({ ...current, model: event.target.value }))} className="mt-2 h-10 w-full rounded-xl border border-border bg-surface-input px-3 text-sm">
<option value="" disabled>请选择已配置模型</option>
{modelOptions.map((option) => <option key={option.modelRef} value={option.modelRef}>{formatModelRefLabel(option.modelRef)} · {option.runtimeProviderKey}</option>)}
</select>
</div>
<div>
<label htmlFor="agent-create-responsibility" className="text-sm font-semibold">职责简述 <span className="text-destructive">*</span></label>
<Input id="agent-create-responsibility" value={input.responsibility} maxLength={200} placeholder="例如:负责把想法拆成清晰的产品计划。" onChange={(event) => setInput((current) => ({ ...current, responsibility: event.target.value }))} className="mt-2" />
</div>
<section className="space-y-4 rounded-lg border border-border/80 bg-surface-subtle p-4">
<h3 className="font-semibold">高级设置(提示词与技能)</h3>
<p className="mt-2 text-xs font-medium text-muted-foreground">基础配置完成后,可以继续补充工作方式和技能绑定。</p>
<Textarea aria-label="Agent 提示词" value={input.prompt} onChange={(event) => setInput((current) => ({ ...current, prompt: event.target.value }))} className="mt-3 min-h-28 bg-background font-mono text-xs leading-5" placeholder="可选:补充这个伙伴的工作方式与边界。" />
<div className="flex items-center justify-between gap-3"><h4 className="text-sm font-semibold">绑定技能</h4><Badge className="border border-border/80 bg-background text-foreground">已选 {input.skillIds.length}</Badge></div>
{skills.length > 0 ? <>
<div className="relative"><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="bg-background pl-9" /></div>
<div className="space-y-2">{filteredSkills.map((skill) => { const checked = input.skillIds.includes(skill.id); return <label key={skill.id} className={cn('flex cursor-pointer items-start gap-2 rounded-md border border-border/80 p-2.5', checked ? 'bg-background' : 'bg-surface-subtle')}><input type="checkbox" aria-label={`绑定技能:${skill.name}`} checked={checked} onChange={() => setInput((current) => ({ ...current, skillIds: checked ? current.skillIds.filter((id) => id !== skill.id) : [...current.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-border/80 p-3 text-center text-xs font-medium text-muted-foreground">没有匹配的技能</p> : null}</div>
</> : <p className="mt-3 rounded-md border border-dashed border-border/80 p-3 text-xs font-medium text-muted-foreground">当前暂无可绑定技能。</p>}
</section>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => onOpenChange(false)} disabled={creating}>取消</Button>
<Button type="button" onClick={() => void handleCreate()} disabled={creating || modelOptions.length === 0}>{creating ? '创建中…' : submitLabel}</Button>
</DialogFooter>
</DialogContent>
<Dialog open={avatarPickerOpen} onOpenChange={setAvatarPickerOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader><DialogTitle>选择头像</DialogTitle><DialogDescription>点击一个头像替换当前头像。</DialogDescription></DialogHeader>
<div className="grid grid-cols-8 gap-2">
{agentAvatarOptions.map((option) => (
<button key={option.id} type="button" aria-label={`选择头像:${option.label}`} aria-pressed={input.avatarId === option.id} onClick={() => { setInput((current) => ({ ...current, avatarId: option.id })); setAvatarPickerOpen(false); }} className={cn('rounded-lg border p-1', input.avatarId === option.id ? 'border-brand bg-brand-soft shadow-soft' : 'border-border bg-background')}>
<img src={option.src} alt="" className="aspect-square w-full rounded object-cover [image-rendering:pixelated]" />
</button>
))}
</div>
</DialogContent>
</Dialog>
</Dialog>
);
}