Files
AgentCompany/src/renderer/components/skills/SkillAgentToggles.tsx
T
ZacharyZhang-NY e71d2e5529 feat(03-02): add SkillDetail pane, SkillAgentToggles, and assignment query IPC
- 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
2026-03-26 02:01:51 -04:00

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>
);
}