Add company skill assignment to agent create and hire flows

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
dotta 2026-03-18 13:18:48 -05:00
parent 099c37c4b4
commit 480174367d
12 changed files with 699 additions and 35 deletions

View file

@ -25,6 +25,31 @@ function PayloadField({ label, value }: { label: string; value: unknown }) {
);
}
function SkillList({ values }: { values: unknown }) {
if (!Array.isArray(values)) return null;
const items = values
.filter((value): value is string => typeof value === "string")
.map((value) => value.trim())
.filter(Boolean);
if (items.length === 0) return null;
return (
<div className="flex items-start gap-2">
<span className="text-muted-foreground w-20 sm:w-24 shrink-0 text-xs pt-0.5">Skills</span>
<div className="flex flex-wrap gap-1.5">
{items.map((item) => (
<span
key={item}
className="rounded bg-muted px-1.5 py-0.5 font-mono text-[11px] text-muted-foreground"
>
{item}
</span>
))}
</div>
</div>
);
}
export function HireAgentPayload({ payload }: { payload: Record<string, unknown> }) {
return (
<div className="mt-3 space-y-1.5 text-sm">
@ -49,6 +74,7 @@ export function HireAgentPayload({ payload }: { payload: Record<string, unknown>
</span>
</div>
)}
<SkillList values={payload.desiredSkills} />
</div>
);
}

View file

@ -4,9 +4,11 @@ import { useNavigate, useSearchParams } from "@/lib/router";
import { useCompany } from "../context/CompanyContext";
import { useBreadcrumbs } from "../context/BreadcrumbContext";
import { agentsApi } from "../api/agents";
import { companySkillsApi } from "../api/companySkills";
import { queryKeys } from "../lib/queryKeys";
import { AGENT_ROLES } from "@paperclipai/shared";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
Popover,
PopoverContent,
@ -68,6 +70,7 @@ export function NewAgent() {
const [role, setRole] = useState("general");
const [reportsTo, setReportsTo] = useState("");
const [configValues, setConfigValues] = useState<CreateConfigValues>(defaultCreateValues);
const [selectedSkillKeys, setSelectedSkillKeys] = useState<string[]>([]);
const [roleOpen, setRoleOpen] = useState(false);
const [reportsToOpen, setReportsToOpen] = useState(false);
const [formError, setFormError] = useState<string | null>(null);
@ -91,6 +94,12 @@ export function NewAgent() {
enabled: Boolean(selectedCompanyId),
});
const { data: companySkills } = useQuery({
queryKey: queryKeys.companySkills.list(selectedCompanyId ?? ""),
queryFn: () => companySkillsApi.list(selectedCompanyId!),
enabled: Boolean(selectedCompanyId),
});
const isFirstAgent = !agents || agents.length === 0;
const effectiveRole = isFirstAgent ? "ceo" : role;
@ -174,6 +183,7 @@ export function NewAgent() {
role: effectiveRole,
...(title.trim() ? { title: title.trim() } : {}),
...(reportsTo ? { reportsTo } : {}),
...(selectedSkillKeys.length > 0 ? { desiredSkills: selectedSkillKeys } : {}),
adapterType: configValues.adapterType,
adapterConfig: buildAdapterConfig(),
runtimeConfig: {
@ -190,6 +200,16 @@ export function NewAgent() {
}
const currentReportsTo = (agents ?? []).find((a) => a.id === reportsTo);
const availableSkills = (companySkills ?? []).filter((skill) => !skill.key.startsWith("paperclipai/paperclip/"));
function toggleSkill(key: string, checked: boolean) {
setSelectedSkillKeys((prev) => {
if (checked) {
return prev.includes(key) ? prev : [...prev, key];
}
return prev.filter((value) => value !== key);
});
}
return (
<div className="mx-auto max-w-2xl space-y-6">
@ -311,6 +331,44 @@ export function NewAgent() {
adapterModels={adapterModels}
/>
<div className="border-t border-border px-4 py-4">
<div className="space-y-3">
<div>
<h2 className="text-sm font-medium">Company skills</h2>
<p className="mt-1 text-xs text-muted-foreground">
Optional skills from the company library. Built-in Paperclip runtime skills are added automatically.
</p>
</div>
{availableSkills.length === 0 ? (
<p className="text-xs text-muted-foreground">
No optional company skills installed yet.
</p>
) : (
<div className="space-y-3">
{availableSkills.map((skill) => {
const inputId = `skill-${skill.id}`;
const checked = selectedSkillKeys.includes(skill.key);
return (
<div key={skill.id} className="flex items-start gap-3">
<Checkbox
id={inputId}
checked={checked}
onCheckedChange={(next) => toggleSkill(skill.key, next === true)}
/>
<label htmlFor={inputId} className="grid gap-1 leading-none">
<span className="text-sm font-medium">{skill.name}</span>
<span className="text-xs text-muted-foreground">
{skill.description ?? skill.key}
</span>
</label>
</div>
);
})}
</div>
)}
</div>
</div>
{/* Footer */}
<div className="border-t border-border px-4 py-3">
{isFirstAgent && (