- Add listSkillAgents and listAgentSkills IPC channels with Zod schemas - Create SkillDetail component with metadata, file viewer, update check, delete - Create SkillAgentToggles with discriminated union for skill/agent-centric modes - Wire SkillDetail into SkillsSection right column replacing placeholder - Remove duplicate skill method declarations from DesktopApi type
114 lines
4.2 KiB
TypeScript
114 lines
4.2 KiB
TypeScript
import { useCallback, useEffect, useState } from "react";
|
|
import type { AgentRecord, SkillRecord } from "@shared/types";
|
|
import { useAppStore } from "../../state/store";
|
|
|
|
type SkillAgentTogglesProps =
|
|
| { mode: "skill"; companyId: string; skillId: string; agents: AgentRecord[] }
|
|
| { mode: "agent"; companyId: string; agentId: string; skills: SkillRecord[] };
|
|
|
|
export function SkillAgentToggles(props: SkillAgentTogglesProps) {
|
|
const { companyId, mode } = props;
|
|
const addToast = useAppStore((s) => s.addToast);
|
|
const [assignedIds, setAssignedIds] = useState<Set<string>>(new Set());
|
|
const [togglingId, setTogglingId] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
async function load() {
|
|
if (mode === "skill") {
|
|
const result = await window.agentCompany.listSkillAgents(companyId, props.skillId);
|
|
if (!cancelled && result.ok) setAssignedIds(new Set(result.data));
|
|
} else {
|
|
const result = await window.agentCompany.listAgentSkills(companyId, props.agentId);
|
|
if (!cancelled && result.ok) setAssignedIds(new Set(result.data));
|
|
}
|
|
}
|
|
void load();
|
|
return () => { cancelled = true; };
|
|
}, [mode, companyId, mode === "skill" ? props.skillId : props.agentId]);
|
|
|
|
const handleToggle = useCallback(
|
|
async (targetId: string) => {
|
|
setTogglingId(targetId);
|
|
const isAssigned = assignedIds.has(targetId);
|
|
const agentId = mode === "skill" ? targetId : props.agentId;
|
|
const skillId = mode === "skill" ? props.skillId : targetId;
|
|
|
|
const result = isAssigned
|
|
? await window.agentCompany.removeSkillFromAgent(companyId, agentId, skillId)
|
|
: await window.agentCompany.assignSkillToAgent(companyId, agentId, skillId);
|
|
|
|
if (result.ok) {
|
|
setAssignedIds((prev) => {
|
|
const next = new Set(prev);
|
|
if (isAssigned) next.delete(targetId);
|
|
else next.add(targetId);
|
|
return next;
|
|
});
|
|
} else {
|
|
addToast(result.error.message, "danger");
|
|
}
|
|
setTogglingId(null);
|
|
},
|
|
[assignedIds, mode, companyId, addToast,
|
|
mode === "skill" ? props.skillId : "",
|
|
mode === "agent" ? props.agentId : ""],
|
|
);
|
|
|
|
const items: Array<{ id: string; name: string; detail?: string }> =
|
|
mode === "skill"
|
|
? props.agents.map((a) => ({ id: a.id, name: a.name, detail: a.role }))
|
|
: props.skills.map((s) => ({ id: s.id, name: s.name, detail: s.description ?? undefined }));
|
|
|
|
if (items.length === 0) {
|
|
return (
|
|
<p className="px-3 py-4 text-center text-[13px] text-[color:var(--muted)]">
|
|
{mode === "skill" ? "No agents in this company" : "No skills imported yet"}
|
|
</p>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
{items.map((item) => {
|
|
const isAssigned = assignedIds.has(item.id);
|
|
const isToggling = togglingId === item.id;
|
|
return (
|
|
<button
|
|
key={item.id}
|
|
type="button"
|
|
disabled={isToggling}
|
|
className="flex w-full items-center gap-3 border-b border-[color:var(--line)] px-3 py-2 text-left transition hover:bg-[color:var(--panel-soft)] disabled:opacity-60"
|
|
onClick={() => void handleToggle(item.id)}
|
|
>
|
|
{/* Toggle indicator */}
|
|
<span
|
|
className={`flex h-[18px] w-[32px] shrink-0 items-center rounded-full p-[2px] transition-colors ${
|
|
isAssigned
|
|
? "bg-[color:var(--accent)]"
|
|
: "bg-[color:var(--line)]"
|
|
}`}
|
|
>
|
|
<span
|
|
className={`block h-[14px] w-[14px] rounded-full bg-white shadow-sm transition-transform ${
|
|
isAssigned ? "translate-x-[14px]" : "translate-x-0"
|
|
}`}
|
|
/>
|
|
</span>
|
|
<div className="min-w-0 flex-1">
|
|
<div className="truncate text-[13px] font-medium text-[color:var(--text)]">
|
|
{item.name}
|
|
</div>
|
|
{item.detail ? (
|
|
<div className="truncate text-[12px] text-[color:var(--muted)]">
|
|
{item.detail}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|