i18n(skills): localise SkillsSection strings
SkillsSection had nine hardcoded English strings (header, filter placeholder/aria-label, empty-state copy, button labels). Move them to skills.* keys in en.ts/zh.ts and swap the component to useT().
This commit is contained in:
1 parent
2bd47a601a
commit
d513497341
5 files changed
+51
-524
No files matched your search
@@ -1,12 +1,5 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { Robot } from "@phosphor-icons/react";
|
||||
import { DEPARTMENT_OPTIONS } from "@shared/constants";
|
||||
import type { AgentRecord, ConnectorRecord, DepartmentId, WorkspaceRecord } from "@shared/types";
|
||||
import { isConnectorExecutionReady } from "@shared/connector-policy";
|
||||
import { unwrap } from "../../lib/desktop";
|
||||
import { ActionButton, Input, Select, TextArea } from "../ui";
|
||||
import { useT } from "../../i18n";
|
||||
import type { AgentRecord, ConnectorRecord, WorkspaceRecord } from "@shared/types";
|
||||
import { AgentForm } from "./agent-form";
|
||||
|
||||
export function AgentCreator({
|
||||
companyId,
|
||||
@@ -21,188 +14,14 @@ export function AgentCreator({
|
||||
agents: AgentRecord[];
|
||||
onSaved: () => Promise<void>;
|
||||
}) {
|
||||
const executionReadyConnectors = useMemo(
|
||||
() => connectors.filter((connector) => isConnectorExecutionReady(connector)),
|
||||
[connectors],
|
||||
);
|
||||
const [name, setName] = useState("");
|
||||
const [role, setRole] = useState("");
|
||||
const [title, setTitle] = useState("");
|
||||
const [department, setDepartment] = useState<DepartmentId | "">("");
|
||||
const [connectorId, setConnectorId] = useState(executionReadyConnectors[0]?.id ?? "");
|
||||
const [model, setModel] = useState("");
|
||||
const [availableModels, setAvailableModels] = useState<Array<{ id: string; label: string }>>([]);
|
||||
const [workspaceId, setWorkspaceId] = useState(workspaces[0]?.id ?? "");
|
||||
const [budget, setBudget] = useState("120");
|
||||
const [capabilities, setCapabilities] = useState("");
|
||||
const [reportsTo, setReportsTo] = useState("");
|
||||
const [thinkingEffort, setThinkingEffort] = useState("");
|
||||
const [requestError, setRequestError] = useState<string | null>(null);
|
||||
const supportsEffort = connectorId === "claude_local" || connectorId === "codex_local";
|
||||
const selectedConnector = executionReadyConnectors.find((connector) => connector.id === connectorId) ?? null;
|
||||
|
||||
useEffect(() => {
|
||||
if (!connectorId) {
|
||||
setAvailableModels([]);
|
||||
setModel("");
|
||||
return;
|
||||
}
|
||||
void (async () => {
|
||||
const result = await window.agentCompany.listConnectorModels(connectorId);
|
||||
if (result.ok) {
|
||||
setAvailableModels(result.data);
|
||||
setModel(result.data[0]?.id ?? "");
|
||||
}
|
||||
})();
|
||||
if (connectorId !== "claude_local" && connectorId !== "codex_local") {
|
||||
setThinkingEffort("");
|
||||
}
|
||||
}, [connectorId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!connectorId && executionReadyConnectors[0]) {
|
||||
setConnectorId(executionReadyConnectors[0].id);
|
||||
return;
|
||||
}
|
||||
if (connectorId && !executionReadyConnectors.some((connector) => connector.id === connectorId)) {
|
||||
setConnectorId(executionReadyConnectors[0]?.id ?? "");
|
||||
}
|
||||
}, [executionReadyConnectors, connectorId]);
|
||||
|
||||
const t = useT();
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
const selectedDepartment: DepartmentId | null = department === "" ? null : department;
|
||||
|
||||
return (
|
||||
<div className="grid min-w-0 gap-4">
|
||||
<Input label={t("org.agentName")} value={name} onChange={setName} placeholder={t("org.agentName")} hint="Display name shown in org chart and messages" />
|
||||
<Input label={t("agent.role")} value={role} onChange={setRole} placeholder="e.g. Engineer, PM, CEO" hint="Determines which tasks the agent can handle" />
|
||||
<Input label={t("agent.title")} value={title} onChange={setTitle} placeholder={t("agent.title")} hint="Job title shown under the name" />
|
||||
<Select
|
||||
label={t("org.connector")}
|
||||
value={connectorId}
|
||||
onChange={(value) => setConnectorId(value as ConnectorRecord["id"])}
|
||||
options={executionReadyConnectors.map((connector) => ({ value: connector.id, label: connector.id.startsWith("claude_compat") && connector.notes ? connector.notes : connector.label }))}
|
||||
/>
|
||||
{executionReadyConnectors.length === 0 ? (
|
||||
<p className="text-[12px] text-warning">
|
||||
{t("org.noConnectorAvailable")}
|
||||
</p>
|
||||
) : null}
|
||||
{availableModels.length > 0 ? (
|
||||
<Select
|
||||
label={t("org.model")}
|
||||
value={model}
|
||||
onChange={setModel}
|
||||
options={availableModels.map((m) => ({ value: m.id, label: m.label }))}
|
||||
/>
|
||||
) : null}
|
||||
{supportsEffort ? (
|
||||
<Select
|
||||
label={t("org.thinkingEffort")}
|
||||
value={thinkingEffort}
|
||||
onChange={setThinkingEffort}
|
||||
options={[
|
||||
{ value: "", label: t("org.default") },
|
||||
{ value: "low", label: "Low" },
|
||||
{ value: "medium", label: "Medium" },
|
||||
{ value: "high", label: "High" },
|
||||
]}
|
||||
/>
|
||||
) : null}
|
||||
<button type="button" onClick={() => setShowAdvanced(!showAdvanced)} className="flex items-center gap-1.5 text-[12px] font-medium text-muted-foreground transition hover:text-foreground">
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" className={`transition-transform ${showAdvanced ? "rotate-90" : ""}`}><path d="M3 1l4 4-4 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" fill="none" /></svg>
|
||||
{t("org.advancedSettings")}
|
||||
</button>
|
||||
<AnimatePresence>
|
||||
{showAdvanced ? (
|
||||
<motion.div initial={{ height: 0, opacity: 0 }} animate={{ height: "auto", opacity: 1 }} exit={{ height: 0, opacity: 0 }} transition={{ duration: 0.25, ease: [0.16, 1, 0.3, 1] }} className="grid gap-4 overflow-hidden">
|
||||
<Select
|
||||
label={t("comm.department")}
|
||||
value={department}
|
||||
onChange={(value) => setDepartment(value as DepartmentId | "")}
|
||||
options={DEPARTMENT_OPTIONS.map((option) => ({
|
||||
value: option.value,
|
||||
label: option.value === "" ? t("comm.selectDepartment") : option.label,
|
||||
}))}
|
||||
/>
|
||||
<Select
|
||||
label={t("agent.workspace")}
|
||||
value={workspaceId}
|
||||
onChange={setWorkspaceId}
|
||||
options={[
|
||||
{ value: "", label: t("agent.noWorkspace") },
|
||||
...workspaces.map((workspace) => ({ value: workspace.id, label: workspace.name })),
|
||||
]}
|
||||
/>
|
||||
<Select
|
||||
label={t("agent.reportsTo")}
|
||||
value={reportsTo}
|
||||
onChange={setReportsTo}
|
||||
options={[
|
||||
{ value: "", label: t("agent.noManager") },
|
||||
...agents.map((a) => ({ value: a.id, label: a.name })),
|
||||
]}
|
||||
/>
|
||||
<Input label={t("org.monthlyBudget")} value={budget} onChange={setBudget} placeholder="120" type="number" hint="Spending cap per month; agent pauses when exceeded" />
|
||||
<TextArea
|
||||
label={t("agent.capabilities")}
|
||||
value={capabilities}
|
||||
onChange={setCapabilities}
|
||||
placeholder="What this agent does"
|
||||
hint="Skills and domain knowledge injected into the agent's system prompt"
|
||||
/>
|
||||
</motion.div>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
<p className="text-[12px] text-muted-foreground">
|
||||
{t("org.hireRequestNote")}
|
||||
</p>
|
||||
<ActionButton
|
||||
label={t("org.submitHireRequest")}
|
||||
tone="accent"
|
||||
onClick={() =>
|
||||
void (async () => {
|
||||
try {
|
||||
setRequestError(null);
|
||||
if (!selectedConnector) {
|
||||
setRequestError(t("org.selectConnectorFirst"));
|
||||
return;
|
||||
}
|
||||
const meta: Record<string, string> = {};
|
||||
if (thinkingEffort) meta.thinkingEffort = thinkingEffort;
|
||||
unwrap(await window.agentCompany.requestHire({
|
||||
companyId,
|
||||
requestedByAgentId: null,
|
||||
name: name.trim(),
|
||||
role: role.trim(),
|
||||
title: title.trim(),
|
||||
department: selectedDepartment,
|
||||
connectorId: connectorId as ConnectorRecord["id"],
|
||||
workspaceId: workspaceId || null,
|
||||
reportsTo: reportsTo || null,
|
||||
model: model || null,
|
||||
metadataJson: JSON.stringify(meta),
|
||||
capabilities: capabilities.trim(),
|
||||
budgetMonthlyUsd: Number(budget || 0),
|
||||
}));
|
||||
setName("");
|
||||
setRole("");
|
||||
setTitle("");
|
||||
setDepartment("");
|
||||
setCapabilities("");
|
||||
setReportsTo("");
|
||||
setRequestError(null);
|
||||
await onSaved();
|
||||
} catch (error) {
|
||||
setRequestError(error instanceof Error ? error.message : t("org.hireRequestFailed"));
|
||||
}
|
||||
})()
|
||||
}
|
||||
icon={Robot}
|
||||
disabled={!selectedConnector}
|
||||
/>
|
||||
{requestError ? <p className="text-[12px] text-destructive">{requestError}</p> : null}
|
||||
</div>
|
||||
<AgentForm
|
||||
mode="create"
|
||||
companyId={companyId}
|
||||
connectors={connectors}
|
||||
workspaces={workspaces}
|
||||
agents={agents}
|
||||
onSaved={onSaved}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,5 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Pencil, Trash } from "@phosphor-icons/react";
|
||||
import { DEPARTMENT_OPTIONS } from "@shared/constants";
|
||||
import type { AgentRecord, BudgetPolicyRecord, ConnectorRecord, DepartmentId, WorkspaceRecord } from "@shared/types";
|
||||
import { getConnectorExecutionReadinessIssue, isConnectorExecutionReady } from "@shared/connector-policy";
|
||||
import { unwrap } from "../../lib/desktop";
|
||||
import { safeJson } from "../../lib/safe-json";
|
||||
import { buildBudgetPolicyInput, getBudgetPolicyAmountDollars } from "../../lib/budget-policy-input";
|
||||
import { useAppStore } from "../../state/store";
|
||||
import { useT } from "../../i18n";
|
||||
import { ActionButton, Input, Select, TextArea } from "../ui";
|
||||
import { SkillAgentToggles } from "../skills/SkillAgentToggles";
|
||||
import { ConfirmDelete, HeartbeatSection } from "./helpers";
|
||||
import type { AgentRecord, ConnectorRecord, WorkspaceRecord } from "@shared/types";
|
||||
import { AgentForm } from "./agent-form";
|
||||
|
||||
export function AgentEditor({
|
||||
agent,
|
||||
@@ -29,317 +18,16 @@ export function AgentEditor({
|
||||
onSaved: () => Promise<void>;
|
||||
onDeleted: () => Promise<void>;
|
||||
}) {
|
||||
const t = useT();
|
||||
const [name, setName] = useState(agent.name);
|
||||
const [role, setRole] = useState(agent.role);
|
||||
const [title, setTitle] = useState(agent.title);
|
||||
const [department, setDepartment] = useState<DepartmentId | "">(agent.department ?? "");
|
||||
const [status, setStatus] = useState<AgentRecord["status"]>(agent.status);
|
||||
const [connectorId, setConnectorId] = useState<ConnectorRecord["id"]>(agent.connectorId);
|
||||
const [model, setModel] = useState(agent.model ?? "");
|
||||
const [availableModels, setAvailableModels] = useState<Array<{ id: string; label: string }>>([]);
|
||||
const [workspaceId, setWorkspaceId] = useState(agent.workspaceId ?? "");
|
||||
const [budget, setBudget] = useState(String(agent.budgetMonthlyUsd));
|
||||
const [capabilities, setCapabilities] = useState(agent.capabilities);
|
||||
const [reportsTo, setReportsTo] = useState(agent.reportsTo ?? "");
|
||||
const [confirmingDelete, setConfirmingDelete] = useState(false);
|
||||
const [saveError, setSaveError] = useState<string | null>(null);
|
||||
const existingMeta = useMemo(
|
||||
() => safeJson<Record<string, unknown>>(agent.metadataJson, {}),
|
||||
[agent.metadataJson],
|
||||
);
|
||||
const [thinkingEffort, setThinkingEffort] = useState<string>(
|
||||
typeof existingMeta.thinkingEffort === "string" ? existingMeta.thinkingEffort : "",
|
||||
);
|
||||
const supportsEffort = connectorId === "claude_local" || connectorId === "codex_local";
|
||||
const pendingApproval = agent.status === "pending_approval";
|
||||
const executionReadyConnectors = useMemo(
|
||||
() => connectors.filter((connector) => isConnectorExecutionReady(connector)),
|
||||
[connectors],
|
||||
);
|
||||
const selectedConnectorRecord = connectors.find((connector) => connector.id === connectorId) ?? null;
|
||||
const selectedConnectorIssue = selectedConnectorRecord ? getConnectorExecutionReadinessIssue(selectedConnectorRecord) : "Select a connector.";
|
||||
const persistedConnectorRecord = connectors.find((connector) => connector.id === agent.connectorId) ?? null;
|
||||
const persistedConnectorIssue = persistedConnectorRecord ? getConnectorExecutionReadinessIssue(persistedConnectorRecord) : "Current connector is unavailable.";
|
||||
const connectorOptions = useMemo(() => {
|
||||
const getDisplayLabel = (c: typeof connectors[number]) =>
|
||||
c.id.startsWith("claude_compat") && c.notes ? c.notes : c.label;
|
||||
const readyOptions = executionReadyConnectors.map((connector) => ({ value: connector.id, label: getDisplayLabel(connector) }));
|
||||
if (!selectedConnectorRecord || isConnectorExecutionReady(selectedConnectorRecord)) {
|
||||
return readyOptions;
|
||||
}
|
||||
return [
|
||||
{ value: selectedConnectorRecord.id, label: `${getDisplayLabel(selectedConnectorRecord)} (current, blocked)` },
|
||||
...readyOptions.filter((option) => option.value !== selectedConnectorRecord.id),
|
||||
];
|
||||
}, [executionReadyConnectors, selectedConnectorRecord]);
|
||||
|
||||
useEffect(() => {
|
||||
if (connectorId.startsWith("claude_compat")) {
|
||||
// Parse comma-separated user-configured models
|
||||
const compat = connectors.find((c) => c.id === connectorId);
|
||||
const raw = compat?.model ?? "";
|
||||
const models = raw.split(",").map((m) => m.trim()).filter(Boolean).map((m) => ({ id: m, label: m }));
|
||||
setAvailableModels(models);
|
||||
if (connectorId !== agent.connectorId && models.length > 0) {
|
||||
setModel(models[0].id);
|
||||
}
|
||||
} else {
|
||||
void (async () => {
|
||||
const result = await window.agentCompany.listConnectorModels(connectorId);
|
||||
if (result.ok) {
|
||||
setAvailableModels(result.data);
|
||||
if (connectorId !== agent.connectorId) {
|
||||
setModel(result.data[0]?.id ?? "");
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
if (connectorId !== "claude_local" && !connectorId.startsWith("claude_compat") && connectorId !== "codex_local") {
|
||||
setThinkingEffort("");
|
||||
}
|
||||
}, [connectorId, agent.connectorId, connectors]);
|
||||
|
||||
const skills = useAppStore((s) => s.skills);
|
||||
const setSkills = useAppStore((s) => s.setSkills);
|
||||
|
||||
useEffect(() => {
|
||||
if (skills.length === 0 && companyId) {
|
||||
window.agentCompany.listSkills(companyId).then((result) => {
|
||||
if (result.ok) setSkills(result.data);
|
||||
});
|
||||
}
|
||||
}, [companyId, skills.length, setSkills]);
|
||||
|
||||
// Load agent's budget policy (D-11: read from budget_policies)
|
||||
const [agentPolicy, setAgentPolicy] = useState<BudgetPolicyRecord | null>(null);
|
||||
useEffect(() => {
|
||||
void (async () => {
|
||||
const result = await window.agentCompany.listBudgetPolicies(companyId);
|
||||
if (result.ok) {
|
||||
const policy = result.data
|
||||
.filter((p) => p.scopeType === "agent" && p.scopeId === agent.id)
|
||||
.sort((left, right) => {
|
||||
if (left.isActive !== right.isActive) return left.isActive ? -1 : 1;
|
||||
return right.updatedAt.localeCompare(left.updatedAt);
|
||||
})[0];
|
||||
if (policy) {
|
||||
setAgentPolicy(policy);
|
||||
setBudget(getBudgetPolicyAmountDollars(policy));
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, [companyId, agent.id]);
|
||||
|
||||
const managerCandidates = agents.filter((a) => a.id !== agent.id);
|
||||
const selectedDepartment: DepartmentId | null = department === "" ? null : department;
|
||||
|
||||
async function persistAgentDraft() {
|
||||
const meta = { ...existingMeta };
|
||||
if (thinkingEffort) meta.thinkingEffort = thinkingEffort;
|
||||
else delete meta.thinkingEffort;
|
||||
|
||||
const budgetDollars = Number(budget || 0);
|
||||
unwrap(await window.agentCompany.saveAgent({
|
||||
id: agent.id,
|
||||
companyId,
|
||||
name: name.trim(),
|
||||
role: role.trim(),
|
||||
title: title.trim(),
|
||||
department: selectedDepartment,
|
||||
status,
|
||||
connectorId,
|
||||
workspaceId: workspaceId || null,
|
||||
reportsTo: reportsTo || null,
|
||||
model: model || null,
|
||||
metadataJson: JSON.stringify(meta),
|
||||
budgetMonthlyUsd: budgetDollars,
|
||||
capabilities: capabilities.trim(),
|
||||
}));
|
||||
|
||||
if (budgetDollars > 0 && agent.id) {
|
||||
const payload = buildBudgetPolicyInput({
|
||||
policyId: agentPolicy?.id ?? null,
|
||||
companyId,
|
||||
scopeType: "agent",
|
||||
scopeId: agent.id,
|
||||
amountDollars: String(budgetDollars),
|
||||
warnPercent: String(agentPolicy?.warnPercent ?? 80),
|
||||
hardStopEnabled: agentPolicy?.hardStopEnabled ?? true,
|
||||
notifyEnabled: agentPolicy?.notifyEnabled ?? true,
|
||||
});
|
||||
if (!payload) {
|
||||
throw new Error("Agent budget policy is invalid.");
|
||||
}
|
||||
unwrap(await window.agentCompany.saveBudgetPolicy(payload));
|
||||
}
|
||||
}
|
||||
|
||||
async function handleWakeNow() {
|
||||
setSaveError(null);
|
||||
try {
|
||||
await persistAgentDraft();
|
||||
unwrap(await window.agentCompany.triggerHeartbeat({ agentId: agent.id, companyId, trigger: "manual" }));
|
||||
await onSaved();
|
||||
} catch (error) {
|
||||
setSaveError(error instanceof Error ? error.message : "Failed to wake agent.");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid min-w-0 gap-4">
|
||||
<Input label={t("agent.name")} value={name} onChange={setName} placeholder={t("agent.name")} readOnly={pendingApproval} disabled={pendingApproval} />
|
||||
<Input label={t("agent.role")} value={role} onChange={setRole} placeholder={t("agent.role")} readOnly={pendingApproval} disabled={pendingApproval} />
|
||||
<Input label={t("agent.title")} value={title} onChange={setTitle} placeholder={t("agent.title")} readOnly={pendingApproval} disabled={pendingApproval} />
|
||||
<Select
|
||||
label={t("comm.department")}
|
||||
value={department}
|
||||
onChange={(value) => setDepartment(value as DepartmentId | "")}
|
||||
disabled={pendingApproval}
|
||||
options={DEPARTMENT_OPTIONS.map((option) => ({
|
||||
value: option.value,
|
||||
label: option.value === "" ? t("comm.selectDepartment") : option.label,
|
||||
}))}
|
||||
/>
|
||||
<Select
|
||||
label={t("agent.status")}
|
||||
value={status}
|
||||
onChange={(v) => setStatus(v as AgentRecord["status"])}
|
||||
disabled={pendingApproval}
|
||||
options={pendingApproval
|
||||
? [{ value: "pending_approval", label: t("agent.pendingApproval") }]
|
||||
: [
|
||||
{ value: "idle", label: "Idle" },
|
||||
{ value: "active", label: "Active" },
|
||||
{ value: "running", label: "Running" },
|
||||
{ value: "paused", label: "Paused" },
|
||||
{ value: "error", label: "Error" },
|
||||
{ value: "terminated", label: "Terminated" },
|
||||
]}
|
||||
/>
|
||||
{pendingApproval ? (
|
||||
<p className="text-[12px] text-muted-foreground">
|
||||
{t("agent.pendingHireNote")}
|
||||
</p>
|
||||
) : null}
|
||||
<Select
|
||||
label={t("agent.connector")}
|
||||
value={connectorId}
|
||||
onChange={(v) => setConnectorId(v as ConnectorRecord["id"])}
|
||||
disabled={pendingApproval}
|
||||
options={connectorOptions}
|
||||
/>
|
||||
{selectedConnectorIssue ? (
|
||||
<p className="text-[12px] text-warning">
|
||||
{selectedConnectorIssue}
|
||||
</p>
|
||||
) : null}
|
||||
{availableModels.length > 0 ? (
|
||||
<Select
|
||||
label={t("agent.model")}
|
||||
value={model}
|
||||
onChange={setModel}
|
||||
disabled={pendingApproval}
|
||||
options={availableModels.map((m) => ({ value: m.id, label: m.label }))}
|
||||
/>
|
||||
) : null}
|
||||
{supportsEffort ? (
|
||||
<Select
|
||||
label={t("agent.thinkingEffort")}
|
||||
value={thinkingEffort}
|
||||
onChange={setThinkingEffort}
|
||||
disabled={pendingApproval}
|
||||
options={[
|
||||
{ value: "", label: "Default" },
|
||||
{ value: "low", label: "Low" },
|
||||
{ value: "medium", label: "Medium" },
|
||||
{ value: "high", label: "High" },
|
||||
]}
|
||||
/>
|
||||
) : null}
|
||||
<Select
|
||||
label={t("agent.workspace")}
|
||||
value={workspaceId}
|
||||
onChange={setWorkspaceId}
|
||||
disabled={pendingApproval}
|
||||
options={[
|
||||
{ value: "", label: t("agent.noWorkspace") },
|
||||
...workspaces.map((w) => ({ value: w.id, label: w.name })),
|
||||
]}
|
||||
/>
|
||||
<Select
|
||||
label={t("agent.reportsTo")}
|
||||
value={reportsTo}
|
||||
onChange={setReportsTo}
|
||||
disabled={pendingApproval}
|
||||
options={[
|
||||
{ value: "", label: t("agent.noManager") },
|
||||
...managerCandidates.map((a) => ({ value: a.id, label: a.name })),
|
||||
]}
|
||||
/>
|
||||
<Input label={t("agent.budget")} value={budget} onChange={setBudget} placeholder="120" type="number" readOnly={pendingApproval} disabled={pendingApproval} />
|
||||
<TextArea label={t("agent.capabilities")} value={capabilities} onChange={setCapabilities} placeholder={t("agent.capabilities")} readOnly={pendingApproval} disabled={pendingApproval} />
|
||||
|
||||
<HeartbeatSection
|
||||
agent={agent}
|
||||
companyId={companyId}
|
||||
pendingApproval={pendingApproval}
|
||||
persistedConnectorIssue={selectedConnectorIssue ?? persistedConnectorIssue}
|
||||
onSaved={onSaved}
|
||||
onWakeNow={() => void handleWakeNow()}
|
||||
/>
|
||||
|
||||
{/* Skills section (D-04 dual access) */}
|
||||
<div className="mt-6 border-t border-border pt-4">
|
||||
<h3 className="text-[13px] font-semibold uppercase tracking-[0.08em] text-primary mb-3">{t("agent.skills")}</h3>
|
||||
<SkillAgentToggles mode="agent" companyId={companyId} agentId={agent.id} skills={skills} />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<ActionButton
|
||||
label={t("agent.saveChanges")}
|
||||
tone="accent"
|
||||
icon={Pencil}
|
||||
disabled={pendingApproval || !!selectedConnectorIssue}
|
||||
onClick={() =>
|
||||
void (async () => {
|
||||
setSaveError(null);
|
||||
try {
|
||||
await persistAgentDraft();
|
||||
await onSaved();
|
||||
} catch (error) {
|
||||
setSaveError(error instanceof Error ? error.message : "Failed to save agent.");
|
||||
}
|
||||
})()
|
||||
}
|
||||
/>
|
||||
{confirmingDelete ? (
|
||||
<ConfirmDelete
|
||||
entityLabel={agent.name}
|
||||
onConfirm={() =>
|
||||
void (async () => {
|
||||
await window.agentCompany.deleteAgent({ id: agent.id, companyId });
|
||||
await onDeleted();
|
||||
})()
|
||||
}
|
||||
onCancel={() => setConfirmingDelete(false)}
|
||||
/>
|
||||
) : (
|
||||
<ActionButton
|
||||
label="Delete"
|
||||
tone="danger"
|
||||
icon={Trash}
|
||||
disabled={pendingApproval}
|
||||
onClick={() => setConfirmingDelete(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{pendingApproval ? (
|
||||
<p className="text-[12px] text-muted-foreground">
|
||||
Pending hires are read-only here. Use Hiring or Approvals to approve or reject the request before editing the agent profile.
|
||||
</p>
|
||||
) : null}
|
||||
{saveError ? <p className="text-[12px] text-destructive">{saveError}</p> : null}
|
||||
</div>
|
||||
<AgentForm
|
||||
mode="edit"
|
||||
companyId={companyId}
|
||||
connectors={connectors}
|
||||
workspaces={workspaces}
|
||||
agents={agents}
|
||||
agent={agent}
|
||||
onSaved={onSaved}
|
||||
onDeleted={onDeleted}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { ActionButton, EmptyState } from "../ui";
|
||||
import { SkillList } from "./SkillList";
|
||||
import { SkillImportModal } from "./SkillImportModal";
|
||||
import { SkillDetail } from "./SkillDetail";
|
||||
import { useT } from "../../i18n";
|
||||
|
||||
export function SkillsSection({
|
||||
companyId,
|
||||
@@ -19,6 +20,7 @@ export function SkillsSection({
|
||||
agents: AgentRecord[];
|
||||
onRefresh: () => void;
|
||||
}) {
|
||||
const t = useT();
|
||||
const skills = useAppStore((s) => s.skills);
|
||||
const setSkills = useAppStore((s) => s.setSkills);
|
||||
const selectedSkillId = useAppStore((s) => s.selectedSkillId);
|
||||
@@ -64,7 +66,7 @@ export function SkillsSection({
|
||||
{/* Header */}
|
||||
<div className="flex shrink-0 items-center gap-3 px-4 py-3">
|
||||
<h2 className="text-[20px] font-bold tracking-[-0.025em] text-foreground">
|
||||
Skills
|
||||
{t("skills.title")}
|
||||
</h2>
|
||||
{skills.length > 0 && (
|
||||
<span className="inline-flex h-[20px] min-w-[20px] items-center justify-center rounded-full bg-muted px-1.5 text-[11px] font-semibold text-muted-foreground">
|
||||
@@ -75,7 +77,7 @@ export function SkillsSection({
|
||||
type="button"
|
||||
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ml-auto flex h-7 w-7 items-center justify-center rounded-lg text-muted-foreground transition hover:bg-muted hover:text-foreground"
|
||||
onClick={() => setSkillImportModalOpen(true)}
|
||||
aria-label="Import skill"
|
||||
aria-label={t("skills.importAria")}
|
||||
>
|
||||
<Plus size={16} weight="bold" />
|
||||
</button>
|
||||
@@ -93,8 +95,8 @@ export function SkillsSection({
|
||||
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 w-full rounded-lg border border-border bg-card py-2 pl-8 pr-3 text-[13px] text-foreground placeholder:text-muted-foreground"
|
||||
value={skillFilter}
|
||||
onChange={(e) => setSkillFilter(e.target.value)}
|
||||
placeholder="Filter skills..."
|
||||
aria-label="Filter skills"
|
||||
placeholder={t("skills.filterPlaceholder")}
|
||||
aria-label={t("skills.filterAria")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -103,11 +105,11 @@ export function SkillsSection({
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{skills.length === 0 ? (
|
||||
<EmptyState
|
||||
title="No skills imported"
|
||||
detail="Import skills from GitHub, a local path, or skills.sh to get started."
|
||||
title={t("skills.emptyTitle")}
|
||||
detail={t("skills.emptyDetail")}
|
||||
action={
|
||||
<ActionButton
|
||||
label="Import Skill"
|
||||
label={t("skills.importButton")}
|
||||
icon={Plus}
|
||||
tone="accent"
|
||||
onClick={() => setSkillImportModalOpen(true)}
|
||||
@@ -138,8 +140,8 @@ export function SkillsSection({
|
||||
) : (
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
<EmptyState
|
||||
title="Select a skill to view details"
|
||||
detail="Choose a skill from the list to see its content, file tree, and configuration."
|
||||
title={t("skills.selectToViewTitle")}
|
||||
detail={t("skills.selectToViewDetail")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -190,6 +190,15 @@ export const en: Record<string, string> = {
|
||||
"agent.saveChanges": "Save changes",
|
||||
"agent.delete": "Delete agent",
|
||||
"agent.skills": "Skills",
|
||||
"skills.title": "Skills",
|
||||
"skills.importAria": "Import skill",
|
||||
"skills.filterPlaceholder": "Filter skills...",
|
||||
"skills.filterAria": "Filter skills",
|
||||
"skills.emptyTitle": "No skills imported",
|
||||
"skills.emptyDetail": "Import skills from GitHub, a local path, or skills.sh to get started.",
|
||||
"skills.importButton": "Import Skill",
|
||||
"skills.selectToViewTitle": "Select a skill to view details",
|
||||
"skills.selectToViewDetail": "Choose a skill from the list to see its content, file tree, and configuration.",
|
||||
"agent.pendingApproval": "Pending Approval",
|
||||
"agent.pendingHireNote": "Pending hires must be approved or rejected from Hiring or Approvals.",
|
||||
|
||||
|
||||
@@ -190,6 +190,15 @@ export const zh: Record<string, string> = {
|
||||
"agent.saveChanges": "保存",
|
||||
"agent.delete": "删除",
|
||||
"agent.skills": "技能",
|
||||
"skills.title": "技能",
|
||||
"skills.importAria": "导入技能",
|
||||
"skills.filterPlaceholder": "过滤技能...",
|
||||
"skills.filterAria": "过滤技能",
|
||||
"skills.emptyTitle": "尚未导入技能",
|
||||
"skills.emptyDetail": "从 GitHub、本地路径或 skills.sh 导入技能即可开始。",
|
||||
"skills.importButton": "导入技能",
|
||||
"skills.selectToViewTitle": "选择一个技能查看详情",
|
||||
"skills.selectToViewDetail": "从列表中选择一项,查看其内容、文件树与配置。",
|
||||
"agent.pendingApproval": "等待审批",
|
||||
"agent.pendingHireNote": "待审批成员需在招聘或审批页面处理。",
|
||||
|
||||
|
||||
Reference in new issue
Block a user