收口 Makelore 客户端变更
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
FileCode2,
|
||||
FileText,
|
||||
FolderKanban,
|
||||
FolderOpen,
|
||||
Layers3,
|
||||
Lock,
|
||||
Mic,
|
||||
@@ -22,11 +23,13 @@ import {
|
||||
Square,
|
||||
Upload,
|
||||
Volume2,
|
||||
Wrench,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { DisclosureContent } from '@/components/ui/disclosure';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Select } from '@/components/ui/select';
|
||||
@@ -98,6 +101,8 @@ type Skill = {
|
||||
appliesTo?: string[];
|
||||
statusLabel: string;
|
||||
location?: string;
|
||||
content?: string;
|
||||
entries?: SkillStructureEntry[];
|
||||
};
|
||||
|
||||
type OpencodeSkillInfo = {
|
||||
@@ -105,8 +110,11 @@ type OpencodeSkillInfo = {
|
||||
description?: string;
|
||||
location: string;
|
||||
content: string;
|
||||
entries?: SkillStructureEntry[];
|
||||
};
|
||||
|
||||
type SkillStructureEntry = { path: string; type: 'file' | 'directory' };
|
||||
|
||||
type KnowledgeFile = {
|
||||
id: string;
|
||||
title: string;
|
||||
@@ -394,6 +402,8 @@ function mapOpencodeSkillToDisplaySkill(skill: OpencodeSkillInfo): Skill {
|
||||
appliesTo: [],
|
||||
statusLabel: 'opencode 已安装',
|
||||
location: skill.location,
|
||||
content: skill.content,
|
||||
entries: skill.entries,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -407,7 +417,13 @@ function mergeInstalledSkills(runtimeSkills: Skill[]): Skill[] {
|
||||
const bundledSkills = courseSkills.map((skill) => {
|
||||
const runtimeSkill = runtimeById.get(skill.id);
|
||||
return runtimeSkill
|
||||
? { ...skill, location: runtimeSkill.location, statusLabel: runtimeSkill.statusLabel }
|
||||
? {
|
||||
...skill,
|
||||
location: runtimeSkill.location,
|
||||
statusLabel: runtimeSkill.statusLabel,
|
||||
content: runtimeSkill.content,
|
||||
entries: runtimeSkill.entries,
|
||||
}
|
||||
: skill;
|
||||
});
|
||||
const knownIds = new Set(bundledSkills.map((skill) => skill.id));
|
||||
@@ -729,7 +745,7 @@ function ResourceCard({
|
||||
onClick,
|
||||
}: {
|
||||
id: string;
|
||||
icon: string;
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
locked?: boolean;
|
||||
@@ -758,11 +774,15 @@ function DrawerFrame({
|
||||
desc,
|
||||
children,
|
||||
onClose,
|
||||
closeLabel = '关闭',
|
||||
closeIcon = 'close',
|
||||
}: {
|
||||
title: string;
|
||||
desc?: string;
|
||||
children: React.ReactNode;
|
||||
onClose: () => void;
|
||||
closeLabel?: string;
|
||||
closeIcon?: 'close' | 'back';
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
@@ -772,8 +792,8 @@ function DrawerFrame({
|
||||
<SheetTitle className="text-foreground">{title}</SheetTitle>
|
||||
{desc ? <SheetDescription className="mt-1 text-muted-foreground">{desc}</SheetDescription> : null}
|
||||
</div>
|
||||
<Button type="button" variant="outline" size="sm" className={whiteControlClassName} onClick={onClose}>
|
||||
×
|
||||
<Button type="button" variant="outline" size="sm" className={whiteControlClassName} onClick={onClose} aria-label={closeLabel}>
|
||||
{closeIcon === 'back' ? <ArrowLeft className="h-4 w-4" /> : '×'}
|
||||
</Button>
|
||||
</div>
|
||||
</SheetHeader>
|
||||
@@ -784,9 +804,35 @@ function DrawerFrame({
|
||||
);
|
||||
}
|
||||
|
||||
function SkillDetail({ skill }: { skill: Skill }) {
|
||||
const entries = skill.entries ?? [{ path: 'SKILL.md', type: 'file' as const }];
|
||||
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-tertiary p-4">
|
||||
<div className="flex items-center gap-2"><FolderOpen className="h-4 w-4 text-brand" /><h3 className="font-semibold">Skill 内结构</h3></div>
|
||||
<p className="mt-1 truncate text-[11px] font-medium text-muted-foreground" title={skill.location}>{skill.location ?? '运行时 Skill 目录'}</p>
|
||||
<div className="mt-3 space-y-1 rounded-lg border border-foreground/10 bg-white p-2">
|
||||
{entries.map((entry) => {
|
||||
const depth = entry.path.split('/').length - 1;
|
||||
return <div key={`${entry.type}:${entry.path}`} className="flex items-center gap-2 rounded-md py-1 text-xs font-medium text-foreground" style={{ paddingLeft: `${8 + depth * 14}px` }}>
|
||||
{entry.type === 'directory' ? <FolderOpen className="h-3.5 w-3.5 shrink-0 text-brand" /> : <FileText className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />}
|
||||
<code className="truncate">{entry.type === 'directory' ? `${entry.path}/` : entry.path}</code>
|
||||
</div>;
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
<section data-testid="skill-main-file" className="rounded-xl border border-foreground/15 bg-white p-4">
|
||||
<div className="flex items-center gap-2"><FileText className="h-4 w-4 text-brand" /><h3 className="font-semibold">SKILL.md</h3></div>
|
||||
<pre className="mt-3 max-h-[min(58vh,680px)] overflow-auto whitespace-pre-wrap break-words rounded-lg border border-foreground/10 bg-surface-tertiary p-3 font-mono text-[11px] leading-5 text-foreground">{skill.content || 'SKILL.md 暂无内容。'}</pre>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CharacterScene() {
|
||||
const navigate = useNavigate();
|
||||
const [activeId, setActiveId] = useState<string | null>(null);
|
||||
const [activeSkillId, setActiveSkillId] = useState<string | null>(null);
|
||||
const [drawerMode, setDrawerMode] = useState<CharacterDrawerMode>(null);
|
||||
const [projectDialogOpen, setProjectDialogOpen] = useState(false);
|
||||
const partnerSubagents = usePartnerProfilesStore((state) => state.subagents);
|
||||
@@ -856,6 +902,7 @@ export function CharacterScene() {
|
||||
() => normalizeSelectedSkillIds(draftSkillIds, installedSkills),
|
||||
[draftSkillIds, installedSkills],
|
||||
);
|
||||
const activeSkill = installedSkills.find((skill) => skill.id === activeSkillId) ?? null;
|
||||
|
||||
useEffect(() => {
|
||||
const handleOpenProjectDialog = () => setProjectDialogOpen(true);
|
||||
@@ -1184,10 +1231,13 @@ export function CharacterScene() {
|
||||
/>
|
||||
<ResourceCard
|
||||
id="skills"
|
||||
icon="⚡"
|
||||
icon={<Wrench className="h-10 w-10 text-brand" />}
|
||||
title="技能库"
|
||||
subtitle={installedSkillsLoading ? '正在读取技能' : `已安装${installedSkills.length}个`}
|
||||
onClick={() => setDrawerMode('skills')}
|
||||
onClick={() => {
|
||||
setActiveSkillId(null);
|
||||
setDrawerMode('skills');
|
||||
}}
|
||||
/>
|
||||
<ResourceCard
|
||||
id="tools"
|
||||
@@ -1261,7 +1311,12 @@ export function CharacterScene() {
|
||||
|
||||
</div>
|
||||
|
||||
<Sheet open={drawerMode !== null} onOpenChange={(open) => !open && setDrawerMode(null)}>
|
||||
<Sheet open={drawerMode !== null} onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setDrawerMode(null);
|
||||
setActiveSkillId(null);
|
||||
}
|
||||
}}>
|
||||
<SheetContent className="flex w-[min(430px,94vw)] flex-col overflow-hidden border-l border-foreground/15 bg-background p-0 text-foreground sm:max-w-none">
|
||||
{drawerMode === 'models' ? (
|
||||
<DrawerFrame title="LLM模型管理" desc="管理伙伴可以使用的电子心脏。" onClose={() => setDrawerMode(null)}>
|
||||
@@ -1407,8 +1462,14 @@ export function CharacterScene() {
|
||||
</DrawerFrame>
|
||||
) : null}
|
||||
|
||||
{drawerMode === 'skills' ? (
|
||||
<DrawerFrame title="可用技能库" desc="这里展示已安装技能;绑定请进入伙伴配置。" onClose={() => setDrawerMode(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="mb-4 flex items-center justify-between rounded-lg border border-foreground/15 bg-surface-tertiary px-3 py-2">
|
||||
<span className="text-sm font-semibold text-foreground">技能列表</span>
|
||||
<Badge className="border border-foreground/15 bg-white text-foreground">{installedSkills.length} 个技能</Badge>
|
||||
@@ -1420,21 +1481,28 @@ export function CharacterScene() {
|
||||
) : null}
|
||||
<div className="space-y-3">
|
||||
{installedSkills.map((skill) => (
|
||||
<div
|
||||
<button
|
||||
key={skill.id}
|
||||
type="button"
|
||||
data-testid={`skill-library-card-${skill.id}`}
|
||||
className="rounded-lg border border-foreground/15 bg-white p-3"
|
||||
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 justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="font-semibold">⚡ {skill.name}</p>
|
||||
<p className="flex items-center gap-2 font-semibold"><Wrench className="h-4 w-4 shrink-0 text-brand" />{skill.name}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge className="shrink-0 border border-foreground/15 bg-brand-soft text-foreground">
|
||||
{skill.statusLabel}
|
||||
</Badge>
|
||||
<ArrowLeft className="h-4 w-4 shrink-0 rotate-180 text-muted-foreground" aria-hidden="true" />
|
||||
</div>
|
||||
<Badge className="shrink-0 border border-foreground/15 bg-brand-soft text-foreground">
|
||||
{skill.statusLabel}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="mt-3 text-sm font-medium leading-6 text-muted-foreground">{skill.description}</p>
|
||||
</div>
|
||||
<p className="mt-2 text-[11px] font-medium text-brand">查看结构与 SKILL.md</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</DrawerFrame>
|
||||
@@ -1853,16 +1921,15 @@ export function ChatScene() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMemoryOpen(!memoryOpen)}
|
||||
aria-expanded={memoryOpen}
|
||||
className="mt-5 w-fit rounded-full border border-foreground/15 bg-surface-tertiary px-3 py-1 text-xs font-semibold shadow-soft"
|
||||
>
|
||||
{memoryNotice.title}
|
||||
</button>
|
||||
{memoryOpen ? (
|
||||
<div className="mt-3 max-w-xl rounded-lg border border-foreground/15 bg-background p-4 shadow-soft">
|
||||
<p className="font-semibold">本次更新</p>
|
||||
<p className="mt-2 text-sm">{memoryNotice.detail}</p>
|
||||
</div>
|
||||
) : null}
|
||||
<DisclosureContent open={memoryOpen} className="mt-3" innerClassName="max-w-xl rounded-lg border border-foreground/15 bg-background p-4 shadow-soft">
|
||||
<p className="font-semibold">本次更新</p>
|
||||
<p className="mt-2 text-sm">{memoryNotice.detail}</p>
|
||||
</DisclosureContent>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -1922,33 +1989,32 @@ export function ChatScene() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpandedAgentDeliverables((current) => ({ ...current, [agent.id]: !expanded }))}
|
||||
aria-expanded={expanded}
|
||||
className="mt-3 flex w-full items-center justify-between rounded-md border border-foreground/15 bg-background px-3 py-2 text-xs font-semibold text-foreground"
|
||||
>
|
||||
<span>交付物({agentDeliverables.length})</span>
|
||||
<span>{expanded ? '收起' : '展开'}</span>
|
||||
</button>
|
||||
{expanded ? (
|
||||
<div className="mt-2 space-y-2">
|
||||
{agentDeliverables.length > 0 ? agentDeliverables.map((deliverable) => (
|
||||
<button
|
||||
key={deliverable.id}
|
||||
type="button"
|
||||
onClick={() => navigate('/deliverables')}
|
||||
className="flex w-full items-center gap-2 rounded-md border border-foreground/15 bg-white p-2 text-left text-foreground"
|
||||
>
|
||||
<DeliverableIcon type={deliverable.type} />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-xs font-semibold">{deliverable.title}</p>
|
||||
<p className="text-[11px] text-muted-foreground">{deliverable.createdAt}</p>
|
||||
</div>
|
||||
</button>
|
||||
)) : (
|
||||
<div className="rounded-md border border-dashed border-foreground/15 bg-white p-3 text-xs font-medium text-muted-foreground">
|
||||
这个Agent还没有交付物。
|
||||
<DisclosureContent open={expanded} className="mt-2" innerClassName="space-y-2">
|
||||
{agentDeliverables.length > 0 ? agentDeliverables.map((deliverable) => (
|
||||
<button
|
||||
key={deliverable.id}
|
||||
type="button"
|
||||
onClick={() => navigate('/deliverables')}
|
||||
className="flex w-full items-center gap-2 rounded-md border border-foreground/15 bg-white p-2 text-left text-foreground"
|
||||
>
|
||||
<DeliverableIcon type={deliverable.type} />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-xs font-semibold">{deliverable.title}</p>
|
||||
<p className="text-[11px] text-muted-foreground">{deliverable.createdAt}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</button>
|
||||
)) : (
|
||||
<div className="rounded-md border border-dashed border-foreground/15 bg-white p-3 text-xs font-medium text-muted-foreground">
|
||||
这个Agent还没有交付物。
|
||||
</div>
|
||||
)}
|
||||
</DisclosureContent>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user