@@ -1,6 +1,6 @@
import { useCallback , useEffect , useMemo , useRef , useState } from 'react' ;
import { useLocation , useNavigate } from 'react-router-dom' ;
import { ArrowLeft , Blocks , Check , Cpu , FileText , Files , FolderOpen , Plus , Trash2 , Upload , X } from 'lucide-react' ;
import { ArrowLeft , Blocks , Bot , Check , Cpu , FileText , Files , FolderOpen , Plus , Trash2 , Upload , X } from 'lucide-react' ;
import { toast } from 'sonner' ;
import { Badge } from '@/components/ui/badge' ;
import { Button } from '@/components/ui/button' ;
@@ -20,10 +20,16 @@ import { useCodingWorkspaceStore } from '@/stores/coding-workspace';
import { useProjectConfigStore } from '@/stores/project-config' ;
import { useProviderStore } from '@/stores/providers' ;
import type { CodingProjectAgent , CodingProjectConfig } from '@/types/coding-project' ;
import type { ProjectIdentityChoice } from '../../../shared/coding-project-contracts' ;
import {
hasReadyCodingProjectAgent ,
isCodingProjectAgentReady ,
isCodingProjectConversationReady ,
type ProjectIdentityChoice ,
} from '../../../shared/coding-project-contracts' ;
const EMPTY_FILES : string [ ] = [ ] ;
type DrawerMode = 'models' | 'knowledge' | 'skills' | null ;
type ExitDestination = '/chat' | '/module-select' | '/models' ;
type ProjectIdentityKind = ProjectIdentityChoice [ 'kind' ] ;
type SkillStructureEntry = { path : string ; type : 'file' | 'directory' } ;
type SkillInfo = {
@@ -93,7 +99,8 @@ function SkillDetail({ skill }: { skill: SkillInfo }) {
}
function AgentCard ( { agent , onOpen } : { agent : CodingProjectAgent ; onOpen : ( ) = > void } ) {
return < button type = "button" data-testid = { ` project-agent- ${ agent . id } ` } onClick = { onOpen } 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 , agent . avatarDataUrl ) } 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 > ;
const ready = isCodingProjectAgentReady ( agent ) ;
return < button type = "button" data-testid = { ` project-agent- ${ agent . id } ` } onClick = { onOpen } 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 , agent . avatarDataUrl ) } 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 = { cn ( 'shrink-0 border' , ready ? 'border-brand/15 bg-brand-soft text-brand' : 'border-border/80 bg-surface-subtle text-muted-foreground' ) } > { ready ? '可用' : '待完成' } < / 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 ModelCard ( { model } : { model : CodingModelOption } ) {
@@ -111,10 +118,20 @@ function ModelCard({ model }: { model: CodingModelOption }) {
</div>;
}
function ModelList({ models }: { models: CodingModelOption[] }) {
function ModelList({
models,
onOpenModelSettings,
}: {
models: CodingModelOption[];
onOpenModelSettings: () => void;
}) {
if (models.length === 0) {
return <div data-testid="project-model-empty-state" className="rounded-xl border border-dashed border-foreground/15 bg-white p-4 text-sm font-medium text-muted-foreground">
当前没有可用模型,请先在模型设置中完成配置。智能体的默认模型在智能体配置中设置。
<p className="text-pretty leading-6">当前没有可用模型。先完成模型配置,再从左侧项目入口回到这里创建智能体。</p>
<Button type="button" className="mt-4 min-h-10 rounded-xl px-4 transition-transform duration-150 ease-out active:scale-[0.96]" onClick={onOpenModelSettings}>
<Cpu className="mr-2 h-4 w-4" aria-hidden="true" />
前往模型设置
</Button>
</div>;
}
return <div data-testid="project-model-list" className="space-y-3">
@@ -153,6 +170,7 @@ export function ProjectConfiguration() {
const [identityError, setIdentityError] = useState<string | null>(null);
const [independentCopyDialogOpen, setIndependentCopyDialogOpen] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [pendingExitDestination, setPendingExitDestination] = useState<ExitDestination | null>(null);
const [partnerDialogOpen, setPartnerDialogOpen] = useState(false);
const [archiveAgentId, setArchiveAgentId] = useState<string | null>(null);
const knowledgeInputRef = useRef<HTMLInputElement | null>(null);
@@ -197,11 +215,47 @@ export function ProjectConfiguration() {
const activeAgent = draft?.agents.find((agent) => agent.id === activeAgentId) ?? null;
const activeSkill = skills.find((skill) => skill.id === activeSkillId) ?? null;
const playSaveConfirmation = () => undefined;
const handleBack = () => navigate(-1);
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 draftHasReadyAgent = hasReadyCodingProjectAgent(draft);
const storedConversationReady = Boolean(stored && isCodingProjectConversationReady(stored));
const hasUnsavedChanges = Boolean(stored && JSON.stringify(stored) !== JSON.stringify(draft));
const backDestination: ExitDestination = storedConversationReady ? '/chat' : '/module-select';
const backLabel = storedConversationReady ? '返回对话' : '稍后设置';
const pendingAgent = draft.agents.find((agent) => (
agent.enabled && !agent.archivedAt && !isCodingProjectAgentReady(agent)
));
const setupActionLabel = modelOptions.length === 0
? '先配置模型'
: pendingAgent
? '完成智能体配置'
: '创建项目智能体';
const archiveAgent = draft.agents.find((agent) => agent.id === archiveAgentId);
const archiveRemovesLastReadyAgent = Boolean(
archiveAgent
&& isCodingProjectAgentReady(archiveAgent)
&& !draft.agents.some((agent) => agent.id !== archiveAgentId && isCodingProjectAgentReady(agent)),
);
const requestNavigation = (destination: ExitDestination) => {
if (hasUnsavedChanges) {
setPendingExitDestination(destination);
return;
}
navigate(destination);
};
const handleBack = () => requestNavigation(backDestination);
const openModelSettings = () => requestNavigation('/models');
const openAgentCreation = () => {
if (modelOptions.length === 0) {
openModelSettings();
return;
}
setActiveAgentId(pendingAgent?.id ?? null);
setPartnerDialogOpen(true);
};
const updateAgent = (next: CodingProjectAgent) => setDraft((current) => current ? { ...current, agents: current.agents.map((item) => item.id === next.id ? { ...next, updatedAt: new Date().toISOString() } : item) } : current);
const handlePartnerDialogOpenChange = (open: boolean) => {
setPartnerDialogOpen(open);
@@ -221,7 +275,22 @@ export function ProjectConfiguration() {
skillIds: input.skillIds,
responsibility: { mission: input.responsibility, owns: [], boundaries: [], collaborators: [], principles: [] },
};
setDraft((current) => current ? { ...curren t, agents: [...curren t.agents, agent] } : current) ;
const nextDraft = { ...draf t, agents: [...draf t.agents, agent] };
if (!draftHasReadyAgent) {
setSaving(true);
try {
const saved = await save(activeProject.id, { ...nextDraft, initialized: true });
setDraft(structuredClone(saved));
await reloadWorkspace();
playSaveConfirmation();
toast.success('项目智能体已创建');
navigate('/chat');
} finally {
setSaving(false);
}
return;
}
setDraft(nextDraft);
};
const handleUpdatePartner = async (input: AgentCreationInput) => {
if (!activeAgentId) return;
@@ -258,12 +327,26 @@ export function ProjectConfiguration() {
toast.error('请补全智能体名称、头像、默认模型和职责说明;名称需在当前项目内唯一。');
return;
}
const conversationReady = hasReadyCodingProjectAgent(draft);
if (!conversationReady && !storedConversationReady) {
openAgentCreation();
return;
}
setSaving(true);
try {
await save(activeProject.id, { ...draft, initialized: true });
const saved = await save(activeProject.id, { ...draft, initialized: conversationReady });
setDraft(structuredClone(saved));
await reloadWorkspace();
playSaveConfirmation();
toast.success(draft.initialized ? '项目配置已更新' : '项目初始化完成');
if (!conversationReady) {
toast.success('项目已保存,创建智能体后即可开始对话');
navigate('/module-select');
} else if (!storedConversationReady) {
toast.success('项目初始化完成');
navigate('/chat');
} else {
toast.success('项目配置已更新');
}
} catch (error) {
toast.error(error instanceof Error ? error.message : String(error));
} finally {
@@ -336,11 +419,36 @@ export function ProjectConfiguration() {
return <main className="-m-5 flex min-h-[calc(100%+2.5rem)] min-h-0 flex-col overflow-auto bg-background p-4 text-foreground sm:-m-6 sm:min-h-[calc(100%+3rem)]" data-testid="project-configuration-page">
<div className="mx-auto flex min-h-0 w-full max-w-7xl flex-1 flex-col gap-3">
<div className="config-motion-title flex items-center gap-2">
<Button type="button" variant="ghost" size="icon" data-testid="project-configuration-back-button" aria-label="返回" title="返回" onClick={handleBack} className="h-8 w-8 shrink-0 text-muted-foreground hover:bg-surface-subtle hover:text-foreground">
<Button type="button" variant="ghost" data-testid="project-configuration-back-button" aria-label={backLabel} title={backLabel} onClick={handleBack} className="min-h-10 shrink-0 rounded-xl px-3 text-muted-foreground transition-transform duration-150 ease-out hover:bg-surface-subtle hover:text-foreground active:scale-[0.96] ">
<ArrowLeft className="h-4 w-4" />
<span className="ml-2 font-semibold">{backLabel}</span>
</Button>
<h1 className="text-xl font-semibold">项目配置</h1>
</div>
{!draftHasReadyAgent ? (
<section data-testid="project-agent-setup-callout" className="surface-card flex flex-col gap-4 rounded-2xl bg-brand-soft/60 p-5 shadow-[0_0_0_1px_rgba(0,0,0,0.06)] sm:flex-row sm:items-center sm:justify-between">
<div className="flex min-w-0 items-start gap-3">
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-background text-brand shadow-[0_0_0_1px_rgba(0,0,0,0.06)]" aria-hidden="true">
<Bot className="h-5 w-5" />
</span>
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<h2 className="text-balance text-lg font-semibold">{draft.agents.length === 0 ? '项目已创建,还差 1 步' : '项目需要一个可用智能体'}</h2>
<Badge className="border border-brand/20 bg-background text-brand">必需</Badge>
</div>
<p className="mt-1 text-pretty text-sm font-medium leading-6 text-muted-foreground">
{modelOptions.length === 0
? '先配置一个可用模型,再创建项目智能体即可开始对话。'
: '创建一个项目智能体并完成名称、默认模型和职责设置,即可开始对话。'}
</p>
</div>
</div>
<Button type="button" data-testid="project-agent-setup-primary" className="min-h-10 shrink-0 rounded-xl px-4 transition-transform duration-150 ease-out active:scale-[0.96]" onClick={openAgentCreation}>
{modelOptions.length === 0 ? <Cpu className="mr-2 h-4 w-4" aria-hidden="true" /> : <Plus className="mr-2 h-4 w-4" aria-hidden="true" />}
{setupActionLabel}
</Button>
</section>
) : null}
<section data-testid="project-identity-card" className="surface-card rounded-2xl border border-border/70 bg-background p-5 shadow-soft">
{draft.projectId ? (
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
@@ -456,7 +564,31 @@ export function ProjectConfiguration() {
)}
</section>
<section className="grid gap-3 md:grid-cols-3"><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')} /></section>
<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>
<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>
<div className="flex flex-wrap items-center gap-2">
<h2 className="text-xl font-semibold">项目智能体</h2>
{!draftHasReadyAgent ? <Badge className="border border-brand/20 bg-brand-soft text-brand">必需</Badge> : null}
</div>
<p className="mt-1 text-pretty text-sm font-medium text-muted-foreground">智能体配置仅属于当前项目;每个智能体可以拥有多条独立对话。</p>
</div>
{draftHasReadyAgent ? (
<Button type="button" size="icon" className="h-10 w-10 rounded-xl border border-brand/20 bg-brand-soft text-brand transition-transform duration-150 ease-out active:scale-[0.96]" aria-label="创建智能体" title="创建智能体" onClick={openAgentCreation}>
<Plus className="h-4 w-4" />
</Button>
) : null}
</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="min-h-10 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>
<div data-testid="project-configuration-primary-actions" className="flex w-full flex-col gap-2 sm:ml-auto sm:w-auto sm:flex-row sm:items-start sm:justify-end">
@@ -469,7 +601,23 @@ export function ProjectConfiguration() {
buttonSize="sm"
/>
) : null}
<Button onClick={() => void submit()} disabled={saving} className="project-save-button h-10 w-full border border-brand/20 bg-brand px-4 font-semibold text-primary-foreground sm:w-auto"><Check className="mr-2 h-4 w-4" />{saving ? '保存中…' : draft.initialized ? '保存项目配置' : '确认并完成初始化'}</Button>
{!draftHasReadyAgent && !storedConversationReady ? (
<Button type="button" onClick={openAgentCreation} disabled={saving} className="project-save-button h-10 w-full border border-brand/20 bg-brand px-4 font-semibold text-primary-foreground transition-transform duration-150 ease-out active:scale-[0.96] sm:w-auto">
{modelOptions.length === 0 ? <Cpu className="mr-2 h-4 w-4" /> : <Plus className="mr-2 h-4 w-4" />}
{setupActionLabel}
</Button>
) : (
<Button onClick={() => void submit()} disabled={saving} className="project-save-button h-10 w-full border border-brand/20 bg-brand px-4 font-semibold text-primary-foreground transition-transform duration-150 ease-out active:scale-[0.96] sm:w-auto">
<Check className="mr-2 h-4 w-4" />
{saving
? '保存中…'
: !draftHasReadyAgent
? '保存并稍后设置'
: storedConversationReady
? '保存项目配置'
: '保存并进入对话'}
</Button>
)}
</div>
</div>
</div>
@@ -484,24 +632,39 @@ export function ProjectConfiguration() {
onUpdate={handleUpdatePartner}
onArchive={activeAgent && !activeAgent.builtIn ? () => setArchiveAgentId(activeAgent.id) : undefined}
title={activeAgent ? ` $ { activeAgent . name || '未命名智能体' } · 编 辑 智 能 体 ` : '创建项目智能体'}
submitLabel={activeAgent ? '保存智能体' : '创建智能体 '}
submitLabel={activeAgent ? '保存智能体' : draftHasReadyAgent ? '创建智能体' : '创建并进入对话 '}
/>
<Sheet open={drawerMode !== null} onOpenChange={(open) => { if (!open) { setDrawerMode(null); setActiveSkillId(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)}><ModelList models={modelOptions} /></DrawerFrame> : null}
{drawerMode === 'models' ? <DrawerFrame title="可用模型" desc="查看已配置的模型;智能体默认模型在智能体配置中设置。" onClose={() => setDrawerMode(null)}><ModelList models={modelOptions} onOpenModelSettings={openModelSettings} /></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}
</SheetContent></Sheet>
<ConfirmDialog
open={Boolean(archiveAgentId)}
title={ ` 归 档 智 能 体 “ $ { draft . agents . find ( ( agent ) = > agent . id === archiveAgentId ) ? . name || '未命名智能体' } ” ? ` }
message="归档后将停止提供新的使用入口,但会保留智能体配置和全部对话记录;你可以之后在智能体列表底部恢复。"
title={ ` 归 档 智 能 体 “ $ { archiveAgent ? . name || '未命名智能体' } ” ? ` }
message={archiveRemovesLastReadyAgent
? '这是当前最后一个可用智能体。归档并保存后,项目将进入待配置状态,无法开始新对话;智能体配置和全部对话记录仍会保留。'
: '归档后将停止提供新的使用入口,但会保留智能体配置和全部对话记录;你可以之后在智能体列表底部恢复。'}
confirmLabel="确认归档"
cancelLabel="取消"
onCancel={() => setArchiveAgentId(null)}
onConfirm={handleArchiveAgent}
onError={(error) => toast.error(error instanceof Error ? error.message : String(error))}
/>
<ConfirmDialog
open={pendingExitDestination !== null}
title="放弃未保存的项目配置?"
message="离开后,本页尚未保存的智能体新增、编辑或归档操作会丢失。"
confirmLabel={pendingExitDestination === '/models' ? '前往模型设置' : backLabel}
cancelLabel="继续编辑"
onCancel={() => setPendingExitDestination(null)}
onConfirm={() => {
const destination = pendingExitDestination;
setPendingExitDestination(null);
if (destination) navigate(destination);
}}
/>
<ConfirmDialog
open={deleteDialogOpen}
title={ ` 删 除 项 目 “ $ { activeProject . name } ” ? ` }