update owliver agent
This commit is contained in:
@@ -7,14 +7,18 @@ import {
|
||||
import { cn } from '@/lib/utils';
|
||||
import { agentIconFor } from '@/components/agents/icons';
|
||||
import OwliverAvatar from '@/components/krow/OwliverAvatar';
|
||||
import { usePreferences } from '@/lib/krowHooks';
|
||||
import { usePreferences, useUpdatePreferences } from '@/lib/krowHooks';
|
||||
import { reportSave } from '@/lib/skills/saveFeedback';
|
||||
import { useAgents } from '@/lib/agents/useAgents';
|
||||
import { agentTemplate } from '@/lib/agents/customAgents';
|
||||
import { agentFieldsFromSource, applyAgentFields } from '@/lib/agents/agentFields';
|
||||
import { validateAgentSource } from '@/lib/agents/registry';
|
||||
import { AgentConfigure } from '@/components/agents/AgentConfigure';
|
||||
import { AgentSkillWorkspace } from '@/components/agents/skills/AgentSkillWorkspace';
|
||||
import { AgentTestPanel } from '@/components/agents/AgentTestPanel';
|
||||
import { AgentInsightsPanel } from '@/components/agents/AgentInsightsPanel';
|
||||
import { useAssistantPanel } from '@/components/ai-assistant';
|
||||
import { boardCatalog, skillCatalog } from '@/lib/skills/catalog';
|
||||
|
||||
/**
|
||||
* One agent, configured.
|
||||
@@ -31,6 +35,44 @@ import { AgentInsightsPanel } from '@/components/agents/AgentInsightsPanel';
|
||||
|
||||
const STATUS_TONE = { published: 'success', draft: 'neutral', archived: 'warning' };
|
||||
|
||||
/**
|
||||
* What Owliver says when a skill joins or leaves an agent.
|
||||
*
|
||||
* Composed from the definition itself — its name, its own description, and the
|
||||
* questions it declares — so the sentence cannot promise a capability the skill
|
||||
* does not have. Nothing is generated: this is the workspace stating a fact in
|
||||
* the conversation, and the chips under it are the skill's own suggestions, so
|
||||
* the next step is a question that skill was written to answer.
|
||||
*/
|
||||
function skillNotice({ entry, id, agentName, attached }) {
|
||||
const name = entry?.name || id;
|
||||
const on = agentName ? `**${agentName}**` : 'this agent';
|
||||
|
||||
if (!attached) {
|
||||
return {
|
||||
text: `**${name}** has been removed from ${on}. It no longer answers here.`,
|
||||
followUp: null,
|
||||
};
|
||||
}
|
||||
|
||||
const opening = `**${name}** is now available to ${on}.`;
|
||||
const what = entry?.description ? ` ${entry.description}` : '';
|
||||
const invitation = entry?.suggestions?.length
|
||||
? ' Ask me one of these, or anything else it covers.'
|
||||
: ' Ask me about it whenever you need it.';
|
||||
|
||||
return {
|
||||
text: `${opening}${what}${invitation}`,
|
||||
followUp: entry?.suggestions?.length
|
||||
? entry.suggestions.slice(0, 3).map((s) => ({
|
||||
label: s.label,
|
||||
prompt: s.prompt,
|
||||
...(s.capability ? { capability: s.capability } : null),
|
||||
}))
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
export default function AdminAgentDetail() {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
@@ -53,6 +95,50 @@ export default function AdminAgentDetail() {
|
||||
const [fields, setFields] = useState(() => (baseSource ? agentFieldsFromSource(baseSource) : null));
|
||||
const [dirty, setDirty] = useState(false);
|
||||
const [view, setView] = useState('configure');
|
||||
/* The skill currently being written, so its card can say so and a second
|
||||
click cannot race the first. */
|
||||
const [pendingSkill, setPendingSkill] = useState(/** @type {string|null} */ (null));
|
||||
|
||||
/* The same catalog the workspace renders, read here so the sentence Owliver
|
||||
says about a skill comes from the definition rather than from the card. */
|
||||
const customSkills = useMemo(() => preferences.customSkills || [], [preferences.customSkills]);
|
||||
const catalog = useMemo(() => skillCatalog(customSkills), [customSkills]);
|
||||
|
||||
/* Owliver's existing panel, on this page already. Attaching a skill states a
|
||||
fact in that conversation — it never opens a second one. */
|
||||
const { announce, ask, test } = useAssistantPanel();
|
||||
|
||||
/**
|
||||
* Trying a capability, in the panel already on this page.
|
||||
*
|
||||
* `scope` runs the turn against the surface the capability actually answers
|
||||
* on — see `useConversation.send`. Nothing is written: this is the whole of
|
||||
* "test", and the agent is unchanged until Enable is pressed.
|
||||
*/
|
||||
const onTestCapability = useCallback(({ question, scope, capability, trace }) => {
|
||||
ask({ question, scope, capability, trace });
|
||||
}, [ask]);
|
||||
|
||||
/* Board skills are governed by the account's `disabledSkills` — the one list
|
||||
`SkillSurface` actually reads. Written through the same `updatePreferences`
|
||||
the Skills page uses, so there is no second persistence for it. */
|
||||
const updatePreferences = useUpdatePreferences();
|
||||
const disabledSkills = useMemo(
|
||||
() => preferences.disabledSkills || [],
|
||||
[preferences.disabledSkills]
|
||||
);
|
||||
|
||||
const onToggleBoardSkill = useCallback((skillId, enabled) => {
|
||||
const entry = boardCatalog(customSkills).find((e) => e.id === skillId);
|
||||
const next = enabled
|
||||
? disabledSkills.filter((x) => x !== skillId)
|
||||
: [...new Set([...disabledSkills, skillId])];
|
||||
|
||||
updatePreferences.mutate(
|
||||
{ disabledSkills: next },
|
||||
reportSave(`${entry?.name || skillId} ${enabled ? 'enabled' : 'disabled'}`)
|
||||
);
|
||||
}, [customSkills, disabledSkills, updatePreferences]);
|
||||
|
||||
/* Reload when the address changes, but never overwrite an edit in progress. */
|
||||
useEffect(() => {
|
||||
@@ -86,6 +172,82 @@ export default function AdminAgentDetail() {
|
||||
return result.agent;
|
||||
}, [fields, baseSource, save, creating, navigate]);
|
||||
|
||||
/**
|
||||
* Attaching and detaching a skill.
|
||||
*
|
||||
* The one write path for `fields.skills`, and deliberately not the same one
|
||||
* the rest of the form uses. Every other field is a *draft* until Save — a
|
||||
* half-typed name should never reach Owliver. A skill is not like that: the
|
||||
* whole point of the catalog is that the agent gains the capability there and
|
||||
* then, and the tree, the card and Owliver all say so immediately.
|
||||
*
|
||||
* So it is optimistic, through the existing `save`, with one honest exception
|
||||
* and one honest revert:
|
||||
*
|
||||
* - **A draft the author is still editing is left a draft.** With unsaved
|
||||
* changes on screen — or a brand-new agent with no definition yet — a
|
||||
* write here would silently persist a half-finished name along with the
|
||||
* skill. The change joins the draft instead, and the header already says
|
||||
* the agent has unsaved changes.
|
||||
* - **A refused write is put back.** The list is restored to exactly what it
|
||||
* was and the reason is shown, rather than leaving a tree claiming a skill
|
||||
* the stored definition does not carry.
|
||||
*/
|
||||
const onToggleSkill = useCallback(async (skillId) => {
|
||||
if (!fields || pendingSkill) return;
|
||||
|
||||
const attached = !fields.skills.includes(skillId);
|
||||
const previous = fields.skills;
|
||||
const nextSkills = attached
|
||||
/* Never a duplicate: the id is added only when it is absent, so clicking
|
||||
Add twice cannot list the same skill twice. */
|
||||
? [...previous, skillId]
|
||||
: previous.filter((x) => x !== skillId);
|
||||
|
||||
const nextFields = { ...fields, skills: nextSkills };
|
||||
const notice = skillNotice({
|
||||
entry: catalog.find((e) => e.id === skillId) || null,
|
||||
id: skillId,
|
||||
agentName: fields.name,
|
||||
attached,
|
||||
});
|
||||
|
||||
/* A draft stays a draft — see above. */
|
||||
if (creating || dirty || !agent) {
|
||||
setFields(nextFields);
|
||||
setDirty(true);
|
||||
announce(notice);
|
||||
return;
|
||||
}
|
||||
|
||||
setFields(nextFields);
|
||||
setPendingSkill(skillId);
|
||||
|
||||
const composed = applyAgentFields(baseSource, nextFields);
|
||||
const problem = validateAgentSource(composed);
|
||||
if (problem) {
|
||||
setFields((current) => ({ ...current, skills: previous }));
|
||||
setPendingSkill(null);
|
||||
toast.error(problem);
|
||||
return;
|
||||
}
|
||||
|
||||
const skillName = catalog.find((e) => e.id === skillId)?.name || skillId;
|
||||
const result = await save(composed, {
|
||||
message: attached ? `${skillName} attached` : `${skillName} removed`,
|
||||
});
|
||||
|
||||
setPendingSkill(null);
|
||||
|
||||
if (!result.ok) {
|
||||
setFields((current) => ({ ...current, skills: previous }));
|
||||
toast.error(result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
announce(notice);
|
||||
}, [fields, pendingSkill, creating, dirty, agent, catalog, baseSource, save, announce]);
|
||||
|
||||
const onPublish = useCallback(async () => {
|
||||
const stored = dirty ? await persist() : agent;
|
||||
if (!stored) return;
|
||||
@@ -230,7 +392,9 @@ export default function AdminAgentDetail() {
|
||||
)}
|
||||
|
||||
<Tabs
|
||||
className="mt-3 mb-1"
|
||||
/* Four tabs do not fit a 320px screen and the tablist does not wrap,
|
||||
so the strip scrolls rather than the page. */
|
||||
className="mt-3 mb-1 -mx-0.5 flex-nowrap overflow-x-auto px-0.5 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||||
variant="underline"
|
||||
size="sm"
|
||||
ariaLabel="Agent views"
|
||||
@@ -238,6 +402,11 @@ export default function AdminAgentDetail() {
|
||||
onChange={setView}
|
||||
tabs={[
|
||||
{ value: 'configure', label: 'Configure' },
|
||||
/* Skills earn their own view rather than a section inside
|
||||
Configure: composing an agent is a workspace — tree, catalog and
|
||||
Owliver at once — and it cannot be that inside a column of form
|
||||
fields. */
|
||||
{ value: 'skills', label: 'Skills', count: fields.skills.length },
|
||||
{ value: 'test', label: 'Test' },
|
||||
{ value: 'insights', label: 'Insights' },
|
||||
]}
|
||||
@@ -248,15 +417,31 @@ export default function AdminAgentDetail() {
|
||||
<AgentConfigure
|
||||
fields={fields}
|
||||
agents={agents}
|
||||
customSkills={preferences.customSkills || []}
|
||||
customSkills={customSkills}
|
||||
onChange={onChange}
|
||||
onOpenSkills={() => setView('skills')}
|
||||
scopeLocked={shipped}
|
||||
/>
|
||||
)}
|
||||
{view === 'skills' && (
|
||||
<AgentSkillWorkspace
|
||||
fields={fields}
|
||||
customSkills={customSkills}
|
||||
onToggleSkill={onToggleSkill}
|
||||
disabledSkills={disabledSkills}
|
||||
onToggleBoardSkill={onToggleBoardSkill}
|
||||
onTestCapability={onTestCapability}
|
||||
test={test}
|
||||
busy={saving || Boolean(pendingSkill)}
|
||||
pendingId={pendingSkill}
|
||||
dirty={dirty || creating}
|
||||
/>
|
||||
)}
|
||||
{view === 'test' && (
|
||||
<AgentTestPanel
|
||||
fields={fields}
|
||||
dirty={dirty}
|
||||
customSkills={preferences.customSkills || []}
|
||||
customSkills={customSkills}
|
||||
/>
|
||||
)}
|
||||
{view === 'insights' && (
|
||||
|
||||
@@ -198,7 +198,7 @@ export default function AdminSkillDevelopment() {
|
||||
actions={
|
||||
<>
|
||||
<Button variant="outline" size="sm" shape="rounded" onClick={() => navigate('/admin/workspace')}>
|
||||
<ChevronLeft aria-hidden="true" /> Workspace
|
||||
<ChevronLeft aria-hidden="true" /> Agent Registry
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" shape="rounded" onClick={() => navigate('/admin/university')}>
|
||||
Manage training
|
||||
|
||||
@@ -1,35 +1,39 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
LayoutTemplate, Sparkles,
|
||||
BookOpen, ArrowRight
|
||||
} from 'lucide-react';
|
||||
import { ArrowRight, BookOpen, Sparkles } from 'lucide-react';
|
||||
import { usePreferences } from '@/lib/krowHooks';
|
||||
import { aiAgentSkills, allSkills, skillsWithFacet } from '@/lib/skills/registry';
|
||||
import { aiAgentSkills, allSkills } from '@/lib/skills/registry';
|
||||
import { allAgents } from '@/lib/agents/registry';
|
||||
import { AdminPage } from '@/components/admin/PageShell';
|
||||
import OwliverAvatar from '@/components/krow/OwliverAvatar';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
/**
|
||||
* Workspace & Skills — AI agent capability governance.
|
||||
* Workspace — the way in to AI configuration, and only that.
|
||||
*
|
||||
* This page governs what *Owliver* can do. It used to govern workforce training
|
||||
* as well — Training Paths and Skill Progression sat in the right-hand column —
|
||||
* and that conflated two unrelated things under one word. A "skill" here is a
|
||||
* capability the assistant gains; a "skill" in KROW Forge is something a person
|
||||
* learns, proves and is verified in. They have different owners, different
|
||||
* lifecycles and different audiences, and putting them side by side made the page
|
||||
* read as though enabling a Bartending path taught Owliver to tend bar.
|
||||
* This page used to offer three destinations side by side: Owliver Agents,
|
||||
* Owliver Skills, and Board Skills. Each was a real thing with a real page
|
||||
* behind it, and together they asked the reader a question the product should
|
||||
* never have asked — *which of these three am I supposed to open?* Nothing on
|
||||
* screen answered it, because the answer is structural: skills are configured
|
||||
* **on an agent**, and an agent is the only thing here anyone actually manages.
|
||||
*
|
||||
* Workforce training was not removed, only returned to where it belongs: the
|
||||
* definitions, the routes and the pages are untouched, and KROW Forge
|
||||
* (`/admin/university`) and Skill Development still read them. See
|
||||
* `workforceTrainingSkills` in the registry for the other half of the split.
|
||||
* So there is one destination now. Agents. Everything else is reached through
|
||||
* the agent it belongs to:
|
||||
*
|
||||
* Workspace → Agents → Configure → Skills → Owliver | Board
|
||||
*
|
||||
* Nothing was deleted to achieve that. `/admin/workspace/skills` still exists
|
||||
* and still authors definitions — it is where the agent editor's own links go —
|
||||
* it simply stopped being advertised as a peer of the thing that uses it.
|
||||
*
|
||||
* The counts are the same registry reads as before, minus the one that named an
|
||||
* implementation detail: "Skill Kinds" told a recruiter that the product has two
|
||||
* internal facets, which is true and is not their problem.
|
||||
*/
|
||||
|
||||
export default function AdminWorkspace() {
|
||||
const preferences = usePreferences();
|
||||
|
||||
/* Read through the agent registry, never by counting skills — one registry
|
||||
answers "how many agents", exactly as the skill registry answers its own. */
|
||||
const agentCount = useMemo(
|
||||
@@ -53,220 +57,112 @@ export default function AdminWorkspace() {
|
||||
const disabledSkills = preferences.disabledSkills || [];
|
||||
const activeSkills = skills.filter((s) => !disabledSkills.includes(s.id)).length;
|
||||
|
||||
/* The two kinds of agent skill — the same two lists the management page is
|
||||
split into, read here from the same filtered registry so the landing page and
|
||||
the manager can never disagree about what is registered. */
|
||||
const owliverSkills = useMemo(() => skillsWithFacet(skills, 'owliver'), [skills]);
|
||||
const boardSkills = useMemo(() => skillsWithFacet(skills, 'ui'), [skills]);
|
||||
const owliverCount = owliverSkills.length;
|
||||
const boardCount = boardSkills.length;
|
||||
const metrics = [
|
||||
{ label: 'Agents', value: agentCount },
|
||||
{ label: 'Skills', value: skills.length },
|
||||
{ label: 'Active', value: activeSkills },
|
||||
];
|
||||
|
||||
return (
|
||||
<AdminPage
|
||||
title="Workspace"
|
||||
subtitle="Govern the AI agent capabilities that extend Owliver and KROW pages."
|
||||
title="Agent Registry"
|
||||
subtitle="Manage how Owliver behaves across Krow."
|
||||
>
|
||||
<div className="space-y-6 w-full">
|
||||
{/* ── 1. Hero Banner (Full Width) ───────────────────────────────── */}
|
||||
<div className="relative overflow-hidden rounded-3xl bg-gradient-to-r from-[#0838E0] via-[#1E40AF] to-[#0284C7] p-6 sm:p-8 text-white shadow-xl w-full">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,_var(--tw-gradient-stops))] from-white/15 via-transparent to-transparent pointer-events-none" />
|
||||
<div className="relative z-10 flex flex-col md:flex-row md:items-center justify-between gap-6">
|
||||
<div className="space-y-2">
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/20 backdrop-blur-md text-white text-[12px] font-semibold border border-white/30">
|
||||
<Sparkles className="w-3.5 h-3.5 text-sky-300" />
|
||||
AI Capability Governance
|
||||
<div className="w-full space-y-6">
|
||||
{/* ── Hero ───────────────────────────────────────────────────────
|
||||
The same banner, saying three plain numbers instead of two figures
|
||||
and a word only this codebase uses. */}
|
||||
<div className="relative w-full overflow-hidden rounded-3xl bg-gradient-to-r from-[#0838E0] via-[#1E40AF] to-[#0284C7] p-6 text-white shadow-xl sm:p-8">
|
||||
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,_var(--tw-gradient-stops))] from-white/15 via-transparent to-transparent" />
|
||||
<div className="relative z-10 flex flex-col justify-between gap-6 md:flex-row md:items-center">
|
||||
<div className="min-w-0 space-y-2">
|
||||
<div className="inline-flex items-center gap-2 rounded-full border border-white/30 bg-white/20 px-3 py-1 text-[12px] font-semibold text-white backdrop-blur-md">
|
||||
<Sparkles className="h-3.5 w-3.5 text-sky-300" />
|
||||
AI Agent Registry
|
||||
</div>
|
||||
<h1 className="text-2xl sm:text-3xl font-bold tracking-tight text-white">AI Agent Capabilities</h1>
|
||||
<p className="text-[14px] text-blue-100 font-normal max-w-2xl">
|
||||
Govern what Owliver can understand, analyze and execute. Workforce training
|
||||
paths and talent progression are managed in KROW Forge.
|
||||
<h1 className="text-2xl font-bold tracking-tight text-white sm:text-3xl">
|
||||
AI Agent Registry
|
||||
</h1>
|
||||
<p className="max-w-2xl text-[14px] font-normal text-blue-100">
|
||||
Manage the agents that power Owliver across Krow. Each agent decides which
|
||||
pages it answers on and which skills it may use.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-3 sm:flex-nowrap sm:shrink-0">
|
||||
<div className="flex-1 sm:flex-none bg-white/10 backdrop-blur-md rounded-2xl p-4 border border-white/15 min-w-[130px] text-center">
|
||||
<p className="text-[11px] uppercase tracking-wider text-blue-200 font-semibold">Registered Skills</p>
|
||||
<p className="text-2xl font-bold text-white mt-0.5">{skills.length}</p>
|
||||
<span className="text-[10px] text-emerald-300 font-medium">{activeSkills} Active</span>
|
||||
</div>
|
||||
{/* Was "Training Paths". Replaced rather than dropped: the slot is
|
||||
useful, and the split of agent skills across the two lists is
|
||||
what this page actually governs. */}
|
||||
<div className="flex-1 sm:flex-none bg-white/10 backdrop-blur-md rounded-2xl p-4 border border-white/15 min-w-[130px] text-center">
|
||||
<p className="text-[11px] uppercase tracking-wider text-blue-200 font-semibold">Skill Kinds</p>
|
||||
<p className="text-2xl font-bold text-sky-300 mt-0.5">{owliverCount + boardCount}</p>
|
||||
<span className="text-[10px] text-blue-200">{owliverCount} Owliver · {boardCount} Board</span>
|
||||
</div>
|
||||
{metrics.map((metric) => (
|
||||
<div
|
||||
key={metric.label}
|
||||
className="min-w-[100px] flex-1 rounded-2xl border border-white/15 bg-white/10 p-4 text-center backdrop-blur-md sm:flex-none"
|
||||
>
|
||||
<p className="text-[11px] font-semibold uppercase tracking-wider text-blue-200">
|
||||
{metric.label}
|
||||
</p>
|
||||
<p className="mt-0.5 text-2xl font-bold tabular-nums text-white">{metric.value}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Agents ─────────────────────────────────────────────────────
|
||||
Above the two skill columns because it is the layer over them: an
|
||||
agent decides which of these skills Owliver may use on a page. */}
|
||||
<div className="bg-white rounded-2xl border border-[#E0E4EA] p-6 shadow-xs space-y-4">
|
||||
<div className="flex items-center justify-between border-b border-[#E0E4EA] pb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<OwliverAvatar className="w-6 h-6" rounded="rounded-md" />
|
||||
<h2 className="text-[16px] font-bold text-[#101820]">Owliver Agents</h2>
|
||||
{/* ── The one destination ────────────────────────────────────────
|
||||
Skills are not a card beside this one. They are configured on an
|
||||
agent, and this is the door to the agents. */}
|
||||
<div className="space-y-4 rounded-2xl border border-[#E0E4EA] bg-white p-6 shadow-xs">
|
||||
<div className="flex items-center justify-between gap-3 border-b border-[#E0E4EA] pb-3">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<OwliverAvatar className="h-6 w-6" rounded="rounded-md" />
|
||||
<h2 className="truncate text-[16px] font-bold text-[#101820]">Agents</h2>
|
||||
</div>
|
||||
<span className="text-[12px] font-semibold text-[#0838E0] bg-blue-50 px-3 py-1 rounded-full border border-blue-200/60">
|
||||
<span className="shrink-0 rounded-full border border-blue-200/60 bg-blue-50 px-3 py-1 text-[12px] font-semibold text-[#0838E0]">
|
||||
{agentCount} Registered
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p className="text-[13px] text-[#687386]">
|
||||
An agent is how Owliver answers on a page: which skills it may use, what it knows, and
|
||||
how much work an answer is worth. Agents narrow what a page offers — they never widen it.
|
||||
An agent is how Owliver answers on a page: which skills it may use, what it knows,
|
||||
and how much work an answer is worth. Agents narrow what a page offers — they never
|
||||
widen it.
|
||||
</p>
|
||||
|
||||
{/* The hierarchy, said once, so the reader knows where skills live
|
||||
before they click rather than after. */}
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 rounded-xl border border-[#E0E4EA] bg-[#F8F8F8]/60 px-3.5 py-2.5 text-[12px] text-[#687386]">
|
||||
<span className="font-semibold text-[#101820]">Agents</span>
|
||||
<ArrowRight className="h-3 w-3 shrink-0" aria-hidden="true" />
|
||||
<span>Configure</span>
|
||||
<ArrowRight className="h-3 w-3 shrink-0" aria-hidden="true" />
|
||||
<span>Skills</span>
|
||||
<ArrowRight className="h-3 w-3 shrink-0" aria-hidden="true" />
|
||||
<span className="font-medium text-[#101820]">Owliver Skills</span>
|
||||
<span aria-hidden="true">/</span>
|
||||
<span className="font-medium text-[#101820]">Board Skills</span>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
to="/admin/workspace/agents"
|
||||
className="w-full inline-flex items-center justify-center gap-2 px-5 py-3 rounded-xl bg-[#0838E0] hover:bg-[#062BAF] text-white text-[13px] font-semibold transition-all shadow-md shadow-blue-500/20 hover:scale-[1.01]"
|
||||
className="inline-flex w-full items-center justify-center gap-2 rounded-xl bg-[#0838E0] px-5 py-3 text-[13px] font-semibold text-white shadow-md shadow-blue-500/20 transition-all hover:scale-[1.01] hover:bg-[#062BAF]"
|
||||
>
|
||||
<Sparkles className="w-4 h-4" />
|
||||
<Sparkles className="h-4 w-4" />
|
||||
Manage Agents
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* ── 2. Full Width 2-Column Grid ───────────────────────────────── */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 w-full">
|
||||
{/* ── Left Column: Owliver AI Capabilities ───────────────────── */}
|
||||
<div className="space-y-6">
|
||||
<div className="bg-white rounded-2xl border border-[#E0E4EA] p-6 shadow-xs space-y-5">
|
||||
<div className="flex items-center justify-between border-b border-[#E0E4EA] pb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<OwliverAvatar className="w-6 h-6" rounded="rounded-md" />
|
||||
<h2 className="text-[16px] font-bold text-[#101820]">Owliver Skills</h2>
|
||||
</div>
|
||||
<span className="text-[12px] font-semibold text-[#0838E0] bg-blue-50 px-3 py-1 rounded-full border border-blue-200/60">
|
||||
{owliverCount} Registered
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p className="text-[13px] text-[#687386]">
|
||||
Owliver skills teach the assistant what it can be asked on a page and how to answer —
|
||||
screening, matching and contextual analysis. They define what the AI understands, not
|
||||
what a candidate learns.
|
||||
</p>
|
||||
|
||||
{/* Skills Sample List */}
|
||||
<div className="space-y-3">
|
||||
{owliverSkills.slice(0, 4).map((s) => {
|
||||
const isActive = !disabledSkills.includes(s.id);
|
||||
return (
|
||||
<div key={s.id} className="p-3.5 rounded-xl border border-[#E0E4EA] bg-[#F8F8F8]/50 flex items-center justify-between gap-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-[13px] font-bold text-[#101820]">{s.name || s.id}</p>
|
||||
{s.description && <p className="text-[12px] text-[#687386] truncate mt-0.5">{s.description}</p>}
|
||||
</div>
|
||||
<span className={cn(
|
||||
'px-2.5 py-0.5 rounded-full text-[11px] font-semibold shrink-0',
|
||||
isActive ? 'bg-emerald-50 text-[#16A765] border border-emerald-200' : 'bg-gray-100 text-gray-500'
|
||||
)}>
|
||||
{isActive ? 'Active' : 'Disabled'}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* CTA Button */}
|
||||
<div className="pt-2">
|
||||
<Link
|
||||
to="/admin/workspace/skills?tab=owliver"
|
||||
className="w-full inline-flex items-center justify-center gap-2 px-5 py-3 rounded-xl bg-[#0838E0] hover:bg-[#062BAF] text-white text-[13px] font-semibold transition-all shadow-md shadow-blue-500/20 hover:scale-[1.01]"
|
||||
>
|
||||
<Sparkles className="w-4 h-4" />
|
||||
Manage Owliver Skills
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Right Column: Board Skills ─────────────────────────────────
|
||||
The other kind of AI agent skill. This column previously held
|
||||
Workforce Training Paths and Skill Progression Tracking, both of
|
||||
which are KROW Forge concerns — see the note at the top of the file.
|
||||
Board skills belong here because, like Owliver skills, they are
|
||||
capabilities the product gains rather than something a person
|
||||
learns. */}
|
||||
<div className="space-y-6">
|
||||
<div className="bg-white rounded-2xl border border-[#E0E4EA] p-6 shadow-xs space-y-5">
|
||||
<div className="flex items-center justify-between border-b border-[#E0E4EA] pb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<LayoutTemplate className="w-5 h-5 text-[#0838E0]" />
|
||||
<h2 className="text-[16px] font-bold text-[#101820]">Board Skills</h2>
|
||||
</div>
|
||||
<span className="text-[12px] font-semibold text-[#0838E0] bg-blue-50 px-3 py-1 rounded-full border border-blue-200/60">
|
||||
{boardCount} Registered
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p className="text-[13px] text-[#687386]">
|
||||
Board skills add dynamic sections to KROW pages, read from real page data. They
|
||||
extend what a page shows, not what a candidate learns.
|
||||
</p>
|
||||
|
||||
{/* Sample list, from the same filtered registry read as the left
|
||||
column — so the two columns can never disagree about what is
|
||||
registered. */}
|
||||
<div className="space-y-3">
|
||||
{boardSkills.slice(0, 4).map((s) => {
|
||||
const isActive = !disabledSkills.includes(s.id);
|
||||
return (
|
||||
<div key={s.id} className="p-3.5 rounded-xl border border-[#E0E4EA] bg-[#F8F8F8]/50 flex items-center justify-between gap-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-[13px] font-bold text-[#101820]">{s.name || s.id}</p>
|
||||
{s.description && <p className="text-[12px] text-[#687386] truncate mt-0.5">{s.description}</p>}
|
||||
</div>
|
||||
<span className={cn(
|
||||
'px-2.5 py-0.5 rounded-full text-[11px] font-semibold shrink-0',
|
||||
isActive ? 'bg-emerald-50 text-[#16A765] border border-emerald-200' : 'bg-gray-100 text-gray-500'
|
||||
)}>
|
||||
{isActive ? 'Active' : 'Disabled'}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{boardSkills.length === 0 && (
|
||||
<p className="text-[13px] text-[#687386] py-2">
|
||||
No Board skills registered yet. A Board skill declares a section, where it
|
||||
sits and what it reads.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="pt-2">
|
||||
<Link
|
||||
to="/admin/workspace/skills?tab=ui"
|
||||
className="w-full inline-flex items-center justify-center gap-2 px-5 py-3 rounded-xl border border-[#E0E4EA] bg-white hover:bg-[#F8F8F8] text-[#101820] text-[13px] font-semibold transition-all shadow-xs"
|
||||
>
|
||||
<LayoutTemplate className="w-4 h-4 text-[#0838E0]" />
|
||||
Manage Board Skills
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── 3. Unified Registry Architecture Banner ───────────────────── */}
|
||||
<div className="flex items-start gap-3.5 p-5 rounded-2xl bg-blue-50/60 border border-blue-200/50 text-[#0838E0] text-[13px] leading-relaxed">
|
||||
<BookOpen className="w-5 h-5 shrink-0 text-[#0838E0] mt-0.5" />
|
||||
<div>
|
||||
{/* ── The one distinction worth keeping ──────────────────────────
|
||||
Not a third destination: a standing fact about a word that means two
|
||||
unrelated things in this product, and the reason KROW Forge is not
|
||||
on this page. */}
|
||||
<div className="flex items-start gap-3.5 rounded-2xl border border-blue-200/50 bg-blue-50/60 p-5 text-[13px] leading-relaxed text-[#0838E0]">
|
||||
<BookOpen className="mt-0.5 h-5 w-5 shrink-0 text-[#0838E0]" />
|
||||
<div className="min-w-0">
|
||||
<p className="font-bold text-[#101820]">AI skills and workforce training are different things</p>
|
||||
<p className="text-[#687386] mt-0.5">
|
||||
<p className="mt-0.5 text-[#687386]">
|
||||
This page governs <span className="font-semibold">AI agent skills</span> — what Owliver
|
||||
understands and what a Board skill draws on a page. <span className="font-semibold">Workforce
|
||||
training</span> — training paths, proving challenges, skill verification and talent
|
||||
understands, and what a Board skill draws on a page. <span className="font-semibold">Workforce
|
||||
training</span> — training paths, proving challenges, verification and talent
|
||||
progression — is what a person learns, and it is managed in{' '}
|
||||
<Link to="/admin/university" className="font-semibold underline hover:no-underline">KROW Forge</Link>.
|
||||
Both are Markdown in one registry, read through separate queries so neither appears in the
|
||||
other’s surface.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -469,7 +469,7 @@ export default function AdminWorkspaceAgents() {
|
||||
className="hidden md:inline-flex items-center gap-1.5 rounded-lg border border-border/70 bg-surface px-3 py-1.5 text-body-sm text-ink-3 transition-colors hover:border-krow-blue/40 hover:text-krow-blue hover:bg-surface-subtle"
|
||||
>
|
||||
<span>Skills managed in</span>
|
||||
<span className="font-semibold text-krow-blue">Workspace → Skills</span>
|
||||
<span className="font-semibold text-krow-blue">Agent Registry → Skills</span>
|
||||
</Link>
|
||||
|
||||
<SegmentedToggle
|
||||
|
||||
@@ -504,7 +504,7 @@ export default function WorkspaceSkills() {
|
||||
actions={
|
||||
<>
|
||||
<Button variant="outline" size="sm" shape="rounded" onClick={() => navigate('/admin/workspace')}>
|
||||
<ChevronLeft aria-hidden="true" /> Workspace
|
||||
<ChevronLeft aria-hidden="true" /> Agent Registry
|
||||
</Button>
|
||||
{/* Account definitions live in this browser's storage and nowhere
|
||||
else. Import and export are what make that a place rather than a
|
||||
|
||||
Reference in New Issue
Block a user