update owliver agent
This commit is contained in:
227
src/components/agents/skills/BoardSkillList.jsx
Normal file
227
src/components/agents/skills/BoardSkillList.jsx
Normal file
@@ -0,0 +1,227 @@
|
||||
import * as React from 'react';
|
||||
import { LayoutTemplate, Pencil, Plus, SearchX } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Alert, Button, SearchInput, Switch } from '@/components/ds';
|
||||
|
||||
/**
|
||||
* Board skills, under the agent that stands beside them.
|
||||
*
|
||||
* The second half of one Skills section — the same heading, a tab away from the
|
||||
* Owliver catalog — so nobody has to decide between two top-level destinations
|
||||
* ever again. What it is *not* is a second copy of that catalog with the word
|
||||
* Board on it, and the reason is worth stating because it is the one thing here
|
||||
* that could quietly become a lie:
|
||||
*
|
||||
* **A Board skill is not carried by an agent.** `SkillSurface` renders a
|
||||
* section from the page it names and the account's `disabledSkills`; it does not
|
||||
* read `agent.skills` and has no agent in scope. Writing a Board skill id into
|
||||
* `agent.skills` would therefore record an assignment nothing honours — a card
|
||||
* that says "Added" and a product that behaves identically either way.
|
||||
*
|
||||
* So the relationship shown is the real one: these are the sections the pages
|
||||
* *this agent answers on* will draw, and the control offered is the one that
|
||||
* actually governs them — the workspace switch every surface already reads. It
|
||||
* is labelled as workspace-wide, because it is.
|
||||
*/
|
||||
|
||||
/** One Board skill: what it draws, where, and whether it is switched on. */
|
||||
function BoardCard({ entry, enabled, onToggle, onSurfaces, busy }) {
|
||||
const switchId = `board-${entry.id}`;
|
||||
|
||||
return (
|
||||
<li className="min-w-0 h-full">
|
||||
<article
|
||||
className={cn(
|
||||
`group relative flex h-full min-w-0 flex-col justify-between rounded-2xl border bg-surface p-5 text-left
|
||||
shadow-xs transition-all duration-300 ease-out hover:-translate-y-1 hover:shadow-lg overflow-hidden`,
|
||||
enabled ? 'border-krow-blue/40 hover:border-krow-blue' : 'border-border/90 hover:border-krow-blue/40'
|
||||
)}
|
||||
>
|
||||
{/* Soft Background Highlight on Hover */}
|
||||
<div
|
||||
className={cn(
|
||||
'absolute inset-x-0 top-0 h-20 bg-gradient-to-b from-krow-blue-tint/40 to-transparent pointer-events-none transition-opacity duration-300',
|
||||
enabled ? 'opacity-70' : 'opacity-0 group-hover:opacity-100'
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="relative z-10">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<span
|
||||
className={cn(
|
||||
'flex h-6 w-6 shrink-0 items-center justify-center rounded-lg border shadow-2xs transition-colors',
|
||||
enabled
|
||||
? 'border-krow-blue/30 bg-krow-blue-tint text-krow-blue'
|
||||
: 'border-border bg-surface-subtle text-ink-3 group-hover:text-krow-blue group-hover:border-krow-blue/30 group-hover:bg-krow-blue-tint/50'
|
||||
)}
|
||||
>
|
||||
<LayoutTemplate className="h-3.5 w-3.5" />
|
||||
</span>
|
||||
<span className="truncate text-[11px] font-bold uppercase tracking-wider text-ink-3">
|
||||
Board Skill
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<span className={cn('text-[11px] font-semibold', enabled ? 'text-emerald-600' : 'text-ink-4')}>
|
||||
{enabled ? 'Active' : 'Disabled'}
|
||||
</span>
|
||||
<Switch
|
||||
id={switchId}
|
||||
checked={enabled}
|
||||
disabled={busy}
|
||||
onCheckedChange={(next) => onToggle(entry.id, next)}
|
||||
aria-label={`${entry.name} — ${enabled ? 'on' : 'off'} for this workspace`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="font-heading text-body-lg font-bold leading-snug text-ink-1 group-hover:text-krow-blue transition-colors mt-3.5">
|
||||
{entry.name}
|
||||
</h3>
|
||||
|
||||
{entry.description && (
|
||||
<p className="mt-1.5 text-body-sm leading-relaxed text-ink-3 line-clamp-2 min-h-[2.5rem]">
|
||||
{entry.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* What it actually draws. */}
|
||||
{entry.sections.length > 0 && (
|
||||
<ul className="mt-3 space-y-1.5">
|
||||
{entry.sections.map((section, i) => (
|
||||
<li
|
||||
key={section.id || `${section.page}-${i}`}
|
||||
className="flex flex-wrap items-center gap-x-1.5 gap-y-0.5 rounded-md bg-surface-subtle px-2 py-1 text-[11px] text-ink-3 border border-border/60"
|
||||
>
|
||||
<span className="font-medium text-ink-2">{section.title}</span>
|
||||
<span className="text-ink-4">·</span>
|
||||
<span>{section.typeLabel}</span>
|
||||
<span className="text-ink-4">·</span>
|
||||
<span className={cn(onSurfaces.has(section.page) ? 'font-semibold text-krow-blue' : 'text-ink-3')}>
|
||||
{section.pageLabel}
|
||||
</span>
|
||||
<span className="text-ink-4">/ {section.placementLabel}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 mt-4 flex items-center justify-between gap-2 border-t border-border/60 pt-3 font-medium">
|
||||
<span className="text-caption text-ink-4">
|
||||
{entry.surfaces.some((s) => onSurfaces.has(s.id))
|
||||
? 'Active on this agent\'s pages'
|
||||
: 'Draws across workspace'}
|
||||
</span>
|
||||
<Link
|
||||
to={`/admin/workspace/skills/${entry.id}`}
|
||||
className="inline-flex items-center gap-1 text-caption font-semibold text-krow-blue hover:text-krow-blue-dark transition-colors"
|
||||
>
|
||||
<Pencil className="h-3 w-3" aria-hidden="true" /> Edit
|
||||
</Link>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
/** @param {any} props */
|
||||
export function BoardSkillList({
|
||||
entries, agentPages = [], disabledIds, onToggle, busy = false, query, onQueryChange, total,
|
||||
}) {
|
||||
const searchId = React.useId();
|
||||
const onSurfaces = React.useMemo(() => new Set(agentPages), [agentPages]);
|
||||
|
||||
/* The ones that meet this agent first */
|
||||
const ordered = React.useMemo(() => {
|
||||
const here = [];
|
||||
const elsewhere = [];
|
||||
for (const e of entries) {
|
||||
(e.surfaces.some((s) => onSurfaces.has(s.id)) ? here : elsewhere).push(e);
|
||||
}
|
||||
return [...here, ...elsewhere];
|
||||
}, [entries, onSurfaces]);
|
||||
|
||||
return (
|
||||
<section aria-label="Board skills" className="flex min-w-0 flex-col gap-4">
|
||||
<Alert tone="info" title="Board skills belong to a page, not to an agent">
|
||||
They draw sections on KROW pages and are switched on for the whole workspace —
|
||||
so a change here affects every agent answering on that page, not just this one.
|
||||
</Alert>
|
||||
|
||||
{total > 0 && (
|
||||
<div>
|
||||
<label htmlFor={searchId} className="sr-only">Search board skills</label>
|
||||
<SearchInput
|
||||
id={searchId}
|
||||
value={query}
|
||||
onChange={onQueryChange}
|
||||
placeholder="Search board skills, sections or pages..."
|
||||
debounce={0}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{total > 0 && (
|
||||
<div className="flex flex-wrap items-center justify-between gap-x-3 gap-y-1.5 pt-1">
|
||||
<p className="text-xs font-medium text-ink-3" aria-live="polite">
|
||||
<span className="font-semibold text-ink-1">{ordered.length}</span> of {total} board skill{total === 1 ? '' : 's'}
|
||||
</p>
|
||||
<Link
|
||||
to="/admin/workspace/skills/new"
|
||||
className={`inline-flex items-center gap-1.5 rounded-lg border border-border/80 bg-surface px-3 py-1.5
|
||||
text-xs font-semibold text-krow-blue shadow-2xs transition-all
|
||||
hover:bg-krow-blue-tint/60 hover:border-krow-blue/40 focus-visible:outline-none
|
||||
focus-visible:ring-2 focus-visible:ring-krow-blue/40`}
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" aria-hidden="true" /> New board skill
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{ordered.length > 0 ? (
|
||||
<ul className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-4.5">
|
||||
{ordered.map((entry) => (
|
||||
<BoardCard
|
||||
key={entry.id}
|
||||
entry={entry}
|
||||
enabled={!disabledIds.has(entry.id)}
|
||||
onToggle={onToggle}
|
||||
onSurfaces={onSurfaces}
|
||||
busy={busy}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<div className="rounded-2xl border border-dashed border-border bg-surface-subtle/40 px-4 py-12 text-center">
|
||||
{total > 0 ? (
|
||||
<>
|
||||
<SearchX className="mx-auto h-6 w-6 text-ink-4" aria-hidden="true" />
|
||||
<p className="mt-2.5 text-sm font-semibold text-ink-1">No board skills match that</p>
|
||||
<p className="mt-1 text-xs text-ink-3">Try a different word.</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<LayoutTemplate className="mx-auto h-6 w-6 text-ink-4" aria-hidden="true" />
|
||||
<p className="mt-2.5 text-sm font-semibold text-ink-1">No board skills yet</p>
|
||||
<p className="mx-auto mt-1 max-w-[46ch] text-xs leading-relaxed text-ink-3">
|
||||
A board skill declares a section, where it sits on a page, and what it reads.
|
||||
Add one and the page it names renders it.
|
||||
</p>
|
||||
<Button asChild variant="outline" size="sm" className="mt-3.5">
|
||||
<Link to="/admin/workspace/skills/new">
|
||||
<Plus className="h-3.5 w-3.5" /> Add a board skill
|
||||
</Link>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default BoardSkillList;
|
||||
Reference in New Issue
Block a user