chore(integration): snapshot local main worktree
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { ArrowLeft, Check, FileText, FolderOpen, Plus, Trash2, Upload, Wrench, X } from 'lucide-react';
|
||||
import { ArrowLeft, Blocks, 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';
|
||||
@@ -30,7 +30,7 @@ function createCustomAgent(index: number, modelKey: string): CodingProjectAgent
|
||||
return {
|
||||
id: `custom-agent-${Date.now()}-${index}`,
|
||||
avatarId: 'avatar-01',
|
||||
roleName: '项目伙伴',
|
||||
roleName: '项目智能体',
|
||||
name: '',
|
||||
builtIn: false,
|
||||
enabled: true,
|
||||
@@ -48,7 +48,7 @@ function createCustomAgent(index: number, modelKey: string): CodingProjectAgent
|
||||
|
||||
function ResourceCard({ id, icon, title, subtitle, onClick }: { id: string; icon: React.ReactNode; title: string; subtitle: string; onClick: () => void }) {
|
||||
return <button type="button" data-testid={`resource-card-${id}`} onClick={onClick} 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>
|
||||
<div className="flex items-center gap-2"><span className="flex h-8 w-8 items-center justify-center rounded-lg bg-surface-subtle text-brand [&>svg]:h-4 [&>svg]:w-4">{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>;
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ function DrawerFrame({ title, desc, onClose, children, closeLabel = '关闭', cl
|
||||
function SkillDetail({ skill }: { skill: SkillInfo }) {
|
||||
return <div data-testid="skill-detail-view" className="space-y-4">
|
||||
<section data-testid="skill-structure" className="rounded-xl border border-foreground/15 bg-surface-subtle p-4">
|
||||
<div className="flex items-center gap-2"><FolderOpen className="h-4 w-4 text-brand" /><h3 className="font-semibold">Skill 内结构</h3></div>
|
||||
<div className="flex items-center gap-2"><FolderOpen className="h-4 w-4 text-brand" /><h3 className="font-semibold">技能目录结构</h3></div>
|
||||
<p className="mt-1 truncate text-[11px] font-medium text-muted-foreground" title={skill.location}>{skill.location}</p>
|
||||
<div className="mt-3 space-y-1 rounded-lg border border-foreground/10 bg-white p-2">
|
||||
{skill.entries.length > 0 ? skill.entries.map((entry) => {
|
||||
@@ -86,7 +86,7 @@ function AgentCard({ agent, onOpen }: { agent: CodingProjectAgent; onOpen: () =>
|
||||
function ModelCard({ model }: { model: CodingModelOption }) {
|
||||
return <div data-testid={`model-card-${model.key.replace(/[^a-zA-Z0-9_-]+/gu, '-')}`} className="rounded-xl border border-foreground/15 bg-white p-3">
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-brand-soft text-lg" aria-hidden="true">🧠</span>
|
||||
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-brand-soft text-brand" aria-hidden="true"><Cpu className="h-4 w-4" /></span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<p className="min-w-0 truncate font-semibold" title={model.label}>{model.label}</p>
|
||||
@@ -101,11 +101,11 @@ function ModelCard({ model }: { model: CodingModelOption }) {
|
||||
function ModelList({ models }: { models: CodingModelOption[] }) {
|
||||
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">
|
||||
当前没有可用模型,请先在模型管理中完成配置;模型切换请进入伙伴配置。
|
||||
当前没有可用模型,请先在模型设置中完成配置。智能体的默认模型在智能体配置中设置。
|
||||
</div>;
|
||||
}
|
||||
return <div data-testid="project-model-list" className="space-y-3">
|
||||
<p className="text-xs font-medium leading-5 text-muted-foreground">以下模型已完成配置。项目只展示模型,实际使用模型由每个伙伴单独配置。</p>
|
||||
<p className="text-xs font-medium leading-5 text-muted-foreground">此处仅展示已经配置的模型。每个智能体可单独设置新对话使用的默认模型。</p>
|
||||
<div className="space-y-2">
|
||||
{models.map((model) => <ModelCard key={model.key} model={model} />)}
|
||||
</div>
|
||||
@@ -182,7 +182,7 @@ export function ProjectConfiguration() {
|
||||
};
|
||||
const handleCreatePartner = async (input: AgentCreationInput) => {
|
||||
const selectedModel = parseCodingModelKey(input.model);
|
||||
if (!selectedModel) throw new Error('伙伴模型无效');
|
||||
if (!selectedModel) throw new Error('默认模型无效');
|
||||
const agent = {
|
||||
...createCustomAgent(draft.agents.length + 1, input.model),
|
||||
name: input.name,
|
||||
@@ -199,7 +199,7 @@ export function ProjectConfiguration() {
|
||||
const handleUpdatePartner = async (input: AgentCreationInput) => {
|
||||
if (!activeAgentId) return;
|
||||
const selectedModel = parseCodingModelKey(input.model);
|
||||
if (!selectedModel) throw new Error('伙伴模型无效');
|
||||
if (!selectedModel) throw new Error('默认模型无效');
|
||||
setDraft((current) => current ? {
|
||||
...current,
|
||||
agents: current.agents.map((agent) => agent.id === activeAgentId ? {
|
||||
@@ -228,7 +228,7 @@ export function ProjectConfiguration() {
|
||||
|| !agent.responsibility.mission.trim()
|
||||
)) || new Set(names).size !== draft.agents.length;
|
||||
if (invalid) {
|
||||
toast.error('请补全伙伴名称、头像、伙伴模型和职责;名称还需要在项目内唯一。');
|
||||
toast.error('请补全智能体名称、头像、默认模型和职责说明;名称需在当前项目内唯一。');
|
||||
return;
|
||||
}
|
||||
setSaving(true);
|
||||
@@ -279,10 +279,10 @@ export function ProjectConfiguration() {
|
||||
<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">
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<h1 className="text-xl font-semibold">配置你的项目空间</h1>
|
||||
<h1 className="text-xl font-semibold">项目配置</h1>
|
||||
</div>
|
||||
<section className="grid gap-3 md:grid-cols-3"><ResourceCard id="models" icon="🧠" title="大脑(大语言模型)" subtitle={`已配置${modelOptions.length}个`} onClick={() => setDrawerMode('models')} /><ResourceCard id="skills" icon={<Wrench className="h-5 w-5" />} title="工具箱(技能)" subtitle={`已安装${skills.length}个`} onClick={() => { setActiveSkillId(null); setDrawerMode('skills'); }} /><ResourceCard id="knowledge" icon="📓" 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="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>
|
||||
<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">
|
||||
@@ -309,19 +309,19 @@ export function ProjectConfiguration() {
|
||||
onCreate={handleCreatePartner}
|
||||
onUpdate={handleUpdatePartner}
|
||||
onArchive={activeAgent && !activeAgent.builtIn ? () => setArchiveAgentId(activeAgent.id) : undefined}
|
||||
title={activeAgent ? `${activeAgent.name || '未命名伙伴'} · 伙伴维护` : '创建项目伙伴'}
|
||||
submitLabel={activeAgent ? '保存伙伴' : '创建伙伴'}
|
||||
title={activeAgent ? `${activeAgent.name || '未命名智能体'} · 编辑智能体` : '创建项目智能体'}
|
||||
submitLabel={activeAgent ? '保存智能体' : '创建智能体'}
|
||||
/>
|
||||
<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 === '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 内结构,再阅读主文件 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"><Wrench 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}
|
||||
{drawerMode === 'models' ? <DrawerFrame title="可用模型" desc="查看已配置的模型;智能体默认模型在智能体配置中设置。" onClose={() => setDrawerMode(null)}><ModelList models={modelOptions} /></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={`归档智能体“${draft.agents.find((agent) => agent.id === archiveAgentId)?.name || '未命名智能体'}”?`}
|
||||
message="归档后将停止提供新的使用入口,但会保留智能体配置和全部对话记录;你可以之后在智能体列表底部恢复。"
|
||||
confirmLabel="确认归档"
|
||||
cancelLabel="取消"
|
||||
onCancel={() => setArchiveAgentId(null)}
|
||||
|
||||
Reference in New Issue
Block a user