收口 Makelore 客户端变更
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Search } from 'lucide-react';
|
||||
import { Search, Trash2 } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -9,6 +9,7 @@ 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';
|
||||
import type { ProjectAgentConfig } from '../../../shared/project-config';
|
||||
|
||||
export type AgentCreationSkillOption = {
|
||||
id: string;
|
||||
@@ -31,13 +32,26 @@ type AgentCreationDialogProps = {
|
||||
modelOptions: ConfiguredModelOption[];
|
||||
skills?: AgentCreationSkillOption[];
|
||||
existingAgentNames?: string[];
|
||||
onCreate: (input: AgentCreationInput) => Promise<void>;
|
||||
agent?: ProjectAgentConfig | null;
|
||||
onCreate?: (input: AgentCreationInput) => Promise<void>;
|
||||
onUpdate?: (input: AgentCreationInput) => Promise<void>;
|
||||
onArchive?: () => void;
|
||||
title?: string;
|
||||
description?: string;
|
||||
submitLabel?: string;
|
||||
};
|
||||
|
||||
function createInitialInput(modelOptions: ConfiguredModelOption[]): AgentCreationInput {
|
||||
function createInitialInput(modelOptions: ConfiguredModelOption[], agent?: ProjectAgentConfig | null): AgentCreationInput {
|
||||
if (agent) {
|
||||
return {
|
||||
name: agent.name,
|
||||
avatarId: agent.avatarId,
|
||||
model: agent.model ?? '',
|
||||
responsibility: agent.responsibility.mission,
|
||||
prompt: agent.prompt,
|
||||
skillIds: [...agent.skillIds],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: '',
|
||||
avatarId: agentAvatarOptions[0]?.id ?? 'avatar-01',
|
||||
@@ -54,22 +68,25 @@ export function AgentCreationDialog({
|
||||
modelOptions,
|
||||
skills = [],
|
||||
existingAgentNames = [],
|
||||
agent = null,
|
||||
onCreate,
|
||||
title = '创建项目联系人',
|
||||
description = '先设置能开始对话所需的基础信息,也可以直接补充提示词和技能。',
|
||||
submitLabel = '创建并开始对话',
|
||||
onUpdate,
|
||||
onArchive,
|
||||
title,
|
||||
submitLabel,
|
||||
}: AgentCreationDialogProps) {
|
||||
const [input, setInput] = useState<AgentCreationInput>(() => createInitialInput(modelOptions));
|
||||
const [creating, setCreating] = useState(false);
|
||||
const editing = Boolean(agent);
|
||||
const [input, setInput] = useState<AgentCreationInput>(() => createInitialInput(modelOptions, agent));
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [skillQuery, setSkillQuery] = useState('');
|
||||
const [avatarPickerOpen, setAvatarPickerOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setInput(createInitialInput(modelOptions));
|
||||
setInput(createInitialInput(modelOptions, agent));
|
||||
setSkillQuery('');
|
||||
setAvatarPickerOpen(false);
|
||||
}, [open, modelOptions]);
|
||||
}, [agent, modelOptions, open]);
|
||||
|
||||
const filteredSkills = useMemo(() => {
|
||||
const query = skillQuery.trim().toLocaleLowerCase();
|
||||
@@ -78,25 +95,27 @@ export function AgentCreationDialog({
|
||||
}, [skillQuery, skills]);
|
||||
const selectedAvatar = agentAvatarOptions.find((option) => option.id === input.avatarId) ?? agentAvatarOptions[0];
|
||||
|
||||
const handleCreate = async () => {
|
||||
const handleSubmit = async () => {
|
||||
const name = input.name.trim();
|
||||
const responsibility = input.responsibility.trim();
|
||||
if (!name || !responsibility || !input.model) {
|
||||
toast.error('请填写联系人名称、职责,并选择基础模型。');
|
||||
toast.error('请填写伙伴名称、职责,并配置伙伴模型。');
|
||||
return;
|
||||
}
|
||||
if (existingAgentNames.some((agentName) => agentName.trim() === name)) {
|
||||
toast.error('联系人名称需要在当前项目内唯一。');
|
||||
toast.error('伙伴名称需要在当前项目内唯一。');
|
||||
return;
|
||||
}
|
||||
setCreating(true);
|
||||
const submit = editing ? onUpdate : onCreate;
|
||||
if (!submit) return;
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await onCreate({ ...input, name, responsibility });
|
||||
await submit({ ...input, name, responsibility });
|
||||
onOpenChange(false);
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : String(error));
|
||||
} finally {
|
||||
setCreating(false);
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -104,12 +123,12 @@ export function AgentCreationDialog({
|
||||
<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>
|
||||
<DialogTitle>{title ?? (editing ? '维护项目伙伴' : '创建项目伙伴')}</DialogTitle>
|
||||
<DialogDescription>设置伙伴的基础信息,也可以补充提示词和技能。</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>
|
||||
<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]" />
|
||||
@@ -118,7 +137,8 @@ export function AgentCreationDialog({
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="agent-create-model" className="text-sm font-semibold">基础模型 <span className="text-destructive">*</span></label>
|
||||
<label htmlFor="agent-create-model" className="text-sm font-semibold">伙伴模型 <span className="text-destructive">*</span></label>
|
||||
<p className="mt-1 text-xs font-medium text-muted-foreground">当前伙伴的消息固定使用此模型;如需切换,请在这里修改。</p>
|
||||
<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>)}
|
||||
@@ -130,7 +150,6 @@ export function AgentCreationDialog({
|
||||
</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 ? <>
|
||||
@@ -139,9 +158,12 @@ export function AgentCreationDialog({
|
||||
</> : <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 className={cn(agent && onArchive && 'sm:justify-between')}>
|
||||
{agent && onArchive ? <Button type="button" variant="outline" aria-label="归档伙伴" className="border-border/80 bg-accent-soft" onClick={onArchive} disabled={submitting}><Trash2 className="mr-2 h-4 w-4" />归档伙伴</Button> : null}
|
||||
<div className="flex flex-col-reverse gap-2 sm:ml-auto sm:flex-row">
|
||||
<Button type="button" variant="outline" onClick={() => onOpenChange(false)} disabled={submitting}>取消</Button>
|
||||
<Button type="button" onClick={() => void handleSubmit()} disabled={submitting || modelOptions.length === 0}>{submitting ? (editing ? '保存中…' : '创建中…') : (submitLabel ?? (editing ? '保存伙伴' : '创建并开始对话'))}</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
<Dialog open={avatarPickerOpen} onOpenChange={setAvatarPickerOpen}>
|
||||
|
||||
Reference in New Issue
Block a user