position ui and owliver update

This commit is contained in:
2026-08-14 01:18:59 +05:30
parent 1c653264b5
commit cccada9bd2
29 changed files with 3718 additions and 1313 deletions

View File

@@ -0,0 +1,217 @@
import React, { useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { ChevronLeft, MoreHorizontal, Pencil, Plus, Trash2 } from 'lucide-react';
import {
Badge, Button, ConfirmModal, DropdownMenu, DropdownMenuContent, DropdownMenuItem,
DropdownMenuTrigger, EmptyState, SearchInput, Surface, Switch, toast,
} from '@/components/ds';
import { usePreferences, useUpdatePreferences } from '@/lib/krowHooks';
import { allSkills, contextIdsForSkill } from '@/lib/skills/registry';
import { removeCustomSkill } from '@/lib/skills/customSkills';
import { ASSISTANT_CONTEXTS } from '@/components/ai-assistant/contexts';
import { AdminPage } from '@/pages/admin/_shell';
/**
* Skills — the management page.
*
* Skills used to be a section at the bottom of the account page, which made
* "add a skill" mean "append another row to a page about something else". They
* are a registry with their own lifecycle: authored, enabled, edited, removed.
* A registry deserves an address, so this is one — reachable from Workspace &
* Skills, where Owliver's capabilities belong, and linkable on its own.
*
* Nothing here is a second skill system. The rows are `allSkills()` — the
* Markdown files on disk plus whatever the account has added — and enabling or
* disabling one writes the same `disabledSkills` preference it always did.
*/
const KIND_LABEL = {
assistant: 'Assistant',
workforce: 'Workforce path',
};
/** One skill, as a row in the management list. */
function SkillRow({ skill, enabled, onToggle, onEdit, onDelete }) {
const pages = contextIdsForSkill(skill)
.map((id) => ASSISTANT_CONTEXTS[id]?.page)
.filter(Boolean);
return (
<div className="grid gap-3 px-4 py-3.5 sm:grid-cols-[minmax(0,1fr)_auto] sm:items-start">
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<p className="text-body-sm font-semibold text-ink-1">{skill.name}</p>
<Badge variant={enabled ? 'success' : 'neutral'} size="sm">
{enabled ? 'Active' : 'Inactive'}
</Badge>
<Badge variant="soft" size="sm">{KIND_LABEL[skill.kind] || 'Assistant'}</Badge>
{skill.custom && <Badge variant="soft" size="sm">Custom</Badge>}
</div>
{skill.description && (
<p className="mt-1 text-caption leading-relaxed text-ink-3">{skill.description}</p>
)}
{/* A workforce path names the capability it governs and the rungs it
defines: on this list those are what change other pages. */}
{skill.kind === 'workforce' && (
<p className="mt-1.5 text-[11px] text-ink-4">
Capability: <span className="text-ink-3">{skill.skillId}</span>
{skill.levels.length > 0 && <> · {skill.levels.map((l) => l.label).join(' → ')}</>}
</p>
)}
{pages.length > 0 && (
<p className="mt-1.5 text-[11px] text-ink-4">
Surfaces: <span className="text-ink-3">{pages.join(', ')}</span>
</p>
)}
<p className="mt-1.5 text-[11px] text-ink-4">
{skill.source === 'account' ? 'Added on this account' : 'Shipped with KROW'}
{' · '}
<span className="font-mono">{skill.path}</span>
</p>
</div>
<div className="flex items-center gap-1.5 sm:justify-end">
<Switch
checked={enabled}
onCheckedChange={(v) => onToggle(skill.id, v)}
aria-label={`${skill.name} — ${enabled ? 'active' : 'inactive'}`}
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
aria-label={`Actions for ${skill.name}`}
className="grid h-8 w-8 place-items-center rounded-lg text-ink-3 transition-colors hover:bg-surface-sunken hover:text-ink-1
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-krow-blue/50"
>
<MoreHorizontal className="h-4 w-4" aria-hidden="true" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuItem onClick={() => onEdit(skill)} className="cursor-pointer">
<Pencil className="mr-2 h-4 w-4" />
{skill.custom ? 'Edit skill' : 'Duplicate & edit'}
</DropdownMenuItem>
{skill.custom && (
<DropdownMenuItem
onClick={() => onDelete(skill)}
className="cursor-pointer text-destructive focus:text-destructive"
>
<Trash2 className="mr-2 h-4 w-4" /> Remove skill
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
);
}
export default function WorkspaceSkills() {
const navigate = useNavigate();
const preferences = usePreferences();
const updatePreferences = useUpdatePreferences();
const [search, setSearch] = useState('');
const [pendingDelete, setPendingDelete] = useState(null);
const disabledSkills = preferences.disabledSkills || [];
const customSkills = preferences.customSkills || [];
const skills = useMemo(() => allSkills(customSkills), [customSkills]);
const filtered = useMemo(() => {
const q = search.trim().toLowerCase();
if (!q) return skills;
return skills.filter((s) => `${s.name} ${s.description} ${s.pages.join(' ')}`.toLowerCase().includes(q));
}, [skills, search]);
const toggleSkill = (id, enabled) => {
const next = enabled
? disabledSkills.filter((s) => s !== id)
: [...new Set([...disabledSkills, id])];
updatePreferences.mutate({ disabledSkills: next });
toast.success(enabled ? 'Skill enabled' : 'Skill disabled');
};
const removeSkill = (skill) => {
updatePreferences.mutate({ customSkills: removeCustomSkill(customSkills, skill.id) });
setPendingDelete(null);
toast.success(`${skill.name} removed`);
};
const activeCount = skills.filter((s) => !disabledSkills.includes(s.id)).length;
return (
<AdminPage
title="Skills"
subtitle="Manage the capabilities Owliver can use across KROW."
actions={
<>
<Button variant="outline" size="sm" shape="rounded" onClick={() => navigate('/admin/workspace')}>
<ChevronLeft aria-hidden="true" /> Workspace
</Button>
<Button size="sm" shape="rounded" onClick={() => navigate('/admin/workspace/skills/new')}>
<Plus aria-hidden="true" /> Add skill
</Button>
</>
}
>
<div className="flex flex-col gap-2 lg:flex-row lg:items-center">
<div className="w-full lg:w-80">
<SearchInput value={search} onChange={setSearch} placeholder="Search skills" size="sm" />
</div>
<p className="text-caption text-ink-4 lg:ml-auto">
{skills.length} registered · {activeCount} active
</p>
</div>
{filtered.length ? (
<Surface
variant="solid"
radius="lg"
padding="none"
elevation="xs"
className="divide-y divide-border overflow-hidden"
>
{filtered.map((skill) => (
<SkillRow
key={skill.id}
skill={skill}
enabled={!disabledSkills.includes(skill.id)}
onToggle={toggleSkill}
onEdit={(s) => navigate(`/admin/workspace/skills/${s.id}`)}
onDelete={setPendingDelete}
/>
))}
</Surface>
) : (
<EmptyState
title="No skills match that search"
description="Try a shorter term, or clear the search to see every registered skill."
action={<Button variant="outline" size="sm" onClick={() => setSearch('')}>Clear search</Button>}
/>
)}
<p className="text-caption leading-relaxed text-ink-4">
Each skill is a Markdown definition that attaches itself to the pages it names.
Definitions shipped with KROW live in the repository and can be switched off here;
skills added on this account are stored with your preferences. Markdown is stored as
data — it is never executed.
</p>
<ConfirmModal
open={Boolean(pendingDelete)}
onOpenChange={(open) => !open && setPendingDelete(null)}
title={`Remove ${pendingDelete?.name || 'skill'}?`}
description="This removes the definition from this account. Skills shipped with KROW are unaffected."
confirmLabel="Remove skill"
tone="destructive"
onConfirm={() => pendingDelete && removeSkill(pendingDelete)}
/>
</AdminPage>
);
}