Fix required Paperclip skill rows on agent detail
This commit is contained in:
parent
10d06bc1ca
commit
d77630154a
1 changed files with 69 additions and 29 deletions
|
|
@ -60,7 +60,7 @@ import {
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { AgentIcon, AgentIconPicker } from "../components/AgentIconPicker";
|
import { AgentIcon, AgentIconPicker } from "../components/AgentIconPicker";
|
||||||
import { RunTranscriptView, type TranscriptMode } from "../components/transcript/RunTranscriptView";
|
import { RunTranscriptView, type TranscriptMode } from "../components/transcript/RunTranscriptView";
|
||||||
import { isUuidLike, type Agent, type HeartbeatRun, type HeartbeatRunEvent, type AgentRuntimeState, type LiveEvent } from "@paperclipai/shared";
|
import { isUuidLike, type Agent, type AgentRuntimeState, type AgentSkillSnapshot, type HeartbeatRun, type HeartbeatRunEvent, type LiveEvent } from "@paperclipai/shared";
|
||||||
import { redactHomePathUserSegments, redactHomePathUserSegmentsInValue } from "@paperclipai/adapter-utils";
|
import { redactHomePathUserSegments, redactHomePathUserSegmentsInValue } from "@paperclipai/adapter-utils";
|
||||||
import { agentRouteRef } from "../lib/utils";
|
import { agentRouteRef } from "../lib/utils";
|
||||||
|
|
||||||
|
|
@ -253,6 +253,9 @@ export function AgentDetail() {
|
||||||
const [actionError, setActionError] = useState<string | null>(null);
|
const [actionError, setActionError] = useState<string | null>(null);
|
||||||
const [moreOpen, setMoreOpen] = useState(false);
|
const [moreOpen, setMoreOpen] = useState(false);
|
||||||
const activeView = urlRunId ? "runs" as AgentDetailView : parseAgentDetailView(urlTab ?? null);
|
const activeView = urlRunId ? "runs" as AgentDetailView : parseAgentDetailView(urlTab ?? null);
|
||||||
|
const needsDashboardData = activeView === "dashboard";
|
||||||
|
const needsRunData = activeView === "runs" || Boolean(urlRunId);
|
||||||
|
const shouldLoadHeartbeats = needsDashboardData || needsRunData;
|
||||||
const [configDirty, setConfigDirty] = useState(false);
|
const [configDirty, setConfigDirty] = useState(false);
|
||||||
const [configSaving, setConfigSaving] = useState(false);
|
const [configSaving, setConfigSaving] = useState(false);
|
||||||
const saveConfigActionRef = useRef<(() => void) | null>(null);
|
const saveConfigActionRef = useRef<(() => void) | null>(null);
|
||||||
|
|
@ -282,25 +285,25 @@ export function AgentDetail() {
|
||||||
const { data: runtimeState } = useQuery({
|
const { data: runtimeState } = useQuery({
|
||||||
queryKey: queryKeys.agents.runtimeState(resolvedAgentId ?? routeAgentRef),
|
queryKey: queryKeys.agents.runtimeState(resolvedAgentId ?? routeAgentRef),
|
||||||
queryFn: () => agentsApi.runtimeState(resolvedAgentId!, resolvedCompanyId ?? undefined),
|
queryFn: () => agentsApi.runtimeState(resolvedAgentId!, resolvedCompanyId ?? undefined),
|
||||||
enabled: Boolean(resolvedAgentId),
|
enabled: Boolean(resolvedAgentId) && needsDashboardData,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: heartbeats } = useQuery({
|
const { data: heartbeats } = useQuery({
|
||||||
queryKey: queryKeys.heartbeats(resolvedCompanyId!, agent?.id ?? undefined),
|
queryKey: queryKeys.heartbeats(resolvedCompanyId!, agent?.id ?? undefined),
|
||||||
queryFn: () => heartbeatsApi.list(resolvedCompanyId!, agent?.id ?? undefined),
|
queryFn: () => heartbeatsApi.list(resolvedCompanyId!, agent?.id ?? undefined),
|
||||||
enabled: !!resolvedCompanyId && !!agent?.id,
|
enabled: !!resolvedCompanyId && !!agent?.id && shouldLoadHeartbeats,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: allIssues } = useQuery({
|
const { data: allIssues } = useQuery({
|
||||||
queryKey: queryKeys.issues.list(resolvedCompanyId!),
|
queryKey: queryKeys.issues.list(resolvedCompanyId!),
|
||||||
queryFn: () => issuesApi.list(resolvedCompanyId!),
|
queryFn: () => issuesApi.list(resolvedCompanyId!),
|
||||||
enabled: !!resolvedCompanyId,
|
enabled: !!resolvedCompanyId && needsDashboardData,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: allAgents } = useQuery({
|
const { data: allAgents } = useQuery({
|
||||||
queryKey: queryKeys.agents.list(resolvedCompanyId!),
|
queryKey: queryKeys.agents.list(resolvedCompanyId!),
|
||||||
queryFn: () => agentsApi.list(resolvedCompanyId!),
|
queryFn: () => agentsApi.list(resolvedCompanyId!),
|
||||||
enabled: !!resolvedCompanyId,
|
enabled: !!resolvedCompanyId && needsDashboardData,
|
||||||
});
|
});
|
||||||
|
|
||||||
const assignedIssues = (allIssues ?? [])
|
const assignedIssues = (allIssues ?? [])
|
||||||
|
|
@ -1149,6 +1152,16 @@ function AgentSkillsTab({
|
||||||
agent: Agent;
|
agent: Agent;
|
||||||
companyId?: string;
|
companyId?: string;
|
||||||
}) {
|
}) {
|
||||||
|
type SkillRow = {
|
||||||
|
id: string;
|
||||||
|
slug: string;
|
||||||
|
name: string;
|
||||||
|
description: string | null;
|
||||||
|
detail: string | null;
|
||||||
|
linkTo: string | null;
|
||||||
|
adapterEntry: AgentSkillSnapshot["entries"][number] | null;
|
||||||
|
};
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [skillDraft, setSkillDraft] = useState<string[]>([]);
|
const [skillDraft, setSkillDraft] = useState<string[]>([]);
|
||||||
const [lastSavedSkills, setLastSavedSkills] = useState<string[]>([]);
|
const [lastSavedSkills, setLastSavedSkills] = useState<string[]>([]);
|
||||||
|
|
@ -1210,6 +1223,39 @@ function AgentSkillsTab({
|
||||||
() => new Map((skillSnapshot?.entries ?? []).map((entry) => [entry.name, entry])),
|
() => new Map((skillSnapshot?.entries ?? []).map((entry) => [entry.name, entry])),
|
||||||
[skillSnapshot],
|
[skillSnapshot],
|
||||||
);
|
);
|
||||||
|
const optionalSkillRows = useMemo<SkillRow[]>(
|
||||||
|
() =>
|
||||||
|
(companySkills ?? [])
|
||||||
|
.filter((skill) => !adapterEntryByName.get(skill.slug)?.required)
|
||||||
|
.map((skill) => ({
|
||||||
|
id: skill.id,
|
||||||
|
slug: skill.slug,
|
||||||
|
name: skill.name,
|
||||||
|
description: skill.description,
|
||||||
|
detail: adapterEntryByName.get(skill.slug)?.detail ?? null,
|
||||||
|
linkTo: `/skills/${skill.id}`,
|
||||||
|
adapterEntry: adapterEntryByName.get(skill.slug) ?? null,
|
||||||
|
})),
|
||||||
|
[adapterEntryByName, companySkills],
|
||||||
|
);
|
||||||
|
const requiredSkillRows = useMemo<SkillRow[]>(
|
||||||
|
() =>
|
||||||
|
(skillSnapshot?.entries ?? [])
|
||||||
|
.filter((entry) => entry.required)
|
||||||
|
.map((entry) => {
|
||||||
|
const companySkill = companySkillBySlug.get(entry.name);
|
||||||
|
return {
|
||||||
|
id: companySkill?.id ?? `required:${entry.name}`,
|
||||||
|
slug: entry.name,
|
||||||
|
name: companySkill?.name ?? entry.name,
|
||||||
|
description: companySkill?.description ?? null,
|
||||||
|
detail: entry.detail ?? null,
|
||||||
|
linkTo: companySkill ? `/skills/${companySkill.id}` : null,
|
||||||
|
adapterEntry: entry,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
[companySkillBySlug, skillSnapshot],
|
||||||
|
);
|
||||||
const desiredOnlyMissingSkills = useMemo(
|
const desiredOnlyMissingSkills = useMemo(
|
||||||
() => skillDraft.filter((slug) => !companySkillBySlug.has(slug)),
|
() => skillDraft.filter((slug) => !companySkillBySlug.has(slug)),
|
||||||
[companySkillBySlug, skillDraft],
|
[companySkillBySlug, skillDraft],
|
||||||
|
|
@ -1276,18 +1322,10 @@ function AgentSkillsTab({
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{(() => {
|
{(() => {
|
||||||
const allSkills = companySkills ?? [];
|
const renderSkillRow = (skill: SkillRow) => {
|
||||||
const optionalSkills = allSkills.filter(
|
const adapterEntry = skill.adapterEntry ?? adapterEntryByName.get(skill.slug);
|
||||||
(skill) => !adapterEntryByName.get(skill.slug)?.required,
|
|
||||||
);
|
|
||||||
const requiredSkills = allSkills.filter(
|
|
||||||
(skill) => adapterEntryByName.get(skill.slug)?.required,
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderSkillRow = (skill: (typeof allSkills)[number]) => {
|
|
||||||
const checked = skillDraft.includes(skill.slug);
|
|
||||||
const adapterEntry = adapterEntryByName.get(skill.slug);
|
|
||||||
const required = Boolean(adapterEntry?.required);
|
const required = Boolean(adapterEntry?.required);
|
||||||
|
const checked = required || Boolean(adapterEntry?.desired) || skillDraft.includes(skill.slug);
|
||||||
const disabled = required || skillSnapshot?.mode === "unsupported";
|
const disabled = required || skillSnapshot?.mode === "unsupported";
|
||||||
const checkbox = (
|
const checkbox = (
|
||||||
<input
|
<input
|
||||||
|
|
@ -1330,27 +1368,29 @@ function AgentSkillsTab({
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<span className="truncate font-medium">{skill.name}</span>
|
<span className="truncate font-medium">{skill.name}</span>
|
||||||
<Link
|
{skill.linkTo ? (
|
||||||
to={`/skills/${skill.id}`}
|
<Link
|
||||||
className="shrink-0 text-xs text-muted-foreground no-underline hover:text-foreground"
|
to={skill.linkTo}
|
||||||
>
|
className="shrink-0 text-xs text-muted-foreground no-underline hover:text-foreground"
|
||||||
View
|
>
|
||||||
</Link>
|
View
|
||||||
|
</Link>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
{skill.description && (
|
{skill.description && (
|
||||||
<MarkdownBody className="mt-1 text-xs text-muted-foreground prose-p:my-1 prose-ul:my-1 prose-ol:my-1 prose-li:my-0 [&>*:first-child]:mt-0 [&>*:last-child]:mb-0">
|
<MarkdownBody className="mt-1 text-xs text-muted-foreground prose-p:my-1 prose-ul:my-1 prose-ol:my-1 prose-li:my-0 [&>*:first-child]:mt-0 [&>*:last-child]:mb-0">
|
||||||
{skill.description}
|
{skill.description}
|
||||||
</MarkdownBody>
|
</MarkdownBody>
|
||||||
)}
|
)}
|
||||||
{adapterEntry?.detail && (
|
{skill.detail && (
|
||||||
<p className="mt-1 text-xs text-muted-foreground">{adapterEntry.detail}</p>
|
<p className="mt-1 text-xs text-muted-foreground">{skill.detail}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (allSkills.length === 0) {
|
if (optionalSkillRows.length === 0 && requiredSkillRows.length === 0) {
|
||||||
return (
|
return (
|
||||||
<section className="border-y border-border">
|
<section className="border-y border-border">
|
||||||
<div className="px-3 py-6 text-sm text-muted-foreground">
|
<div className="px-3 py-6 text-sm text-muted-foreground">
|
||||||
|
|
@ -1362,20 +1402,20 @@ function AgentSkillsTab({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{optionalSkills.length > 0 && (
|
{optionalSkillRows.length > 0 && (
|
||||||
<section className="border-y border-border">
|
<section className="border-y border-border">
|
||||||
{optionalSkills.map(renderSkillRow)}
|
{optionalSkillRows.map(renderSkillRow)}
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{requiredSkills.length > 0 && (
|
{requiredSkillRows.length > 0 && (
|
||||||
<section className="border-y border-border">
|
<section className="border-y border-border">
|
||||||
<div className="border-b border-border bg-muted/40 px-3 py-2">
|
<div className="border-b border-border bg-muted/40 px-3 py-2">
|
||||||
<span className="text-xs font-medium text-muted-foreground">
|
<span className="text-xs font-medium text-muted-foreground">
|
||||||
Required by Paperclip
|
Required by Paperclip
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{requiredSkills.map(renderSkillRow)}
|
{requiredSkillRows.map(renderSkillRow)}
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue