chore(repo): reinitialize repository
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { usePolling } from "../hooks/usePolling";
|
||||
import { fetchRoles } from "../api/client";
|
||||
import type { RoleInfo } from "../types";
|
||||
import AsyncPageState from "./ui/AsyncPageState";
|
||||
import Button from "./ui/Button";
|
||||
import Chip from "./ui/Chip";
|
||||
import RoleBadge from "./RoleBadge";
|
||||
import PageHero from "./ui/PageHero";
|
||||
import PageSectionCard from "./ui/PageSectionCard";
|
||||
import SummaryStat from "./ui/SummaryStat";
|
||||
import StatusDot from "./ui/StatusDot";
|
||||
import RoleEditor from "./RoleEditor";
|
||||
import { useI18n } from "../i18n";
|
||||
import { roleListChangeKey } from "../utils/pollingKeys";
|
||||
|
||||
interface RoleStatusProps {
|
||||
workspace: string;
|
||||
}
|
||||
|
||||
function getRoleState(role: RoleInfo, copy: ReturnType<typeof useI18n>["copy"]) {
|
||||
if (role.pending > 0) {
|
||||
return {
|
||||
label: copy.roleStatus.states.attention,
|
||||
dotClassName: "bg-[color:var(--app-accent-warm)]",
|
||||
textClassName: "text-[color:var(--app-accent-warm)]",
|
||||
};
|
||||
}
|
||||
|
||||
if (role.session) {
|
||||
return {
|
||||
label: copy.roleStatus.states.active,
|
||||
dotClassName: "app-dot-success",
|
||||
textClassName: "app-text-success",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
label: copy.roleStatus.states.idle,
|
||||
dotClassName: "app-dot-idle",
|
||||
textClassName: "app-text-idle",
|
||||
};
|
||||
}
|
||||
|
||||
function getRolePriority(role: RoleInfo) {
|
||||
return (role.pending > 0 ? 2 : 0) + (role.session ? 1 : 0);
|
||||
}
|
||||
|
||||
export default function RoleStatus({ workspace }: RoleStatusProps) {
|
||||
const { copy, formatAbsoluteDateTime, formatRelativeTime } = useI18n();
|
||||
const fetcher = useCallback(() => fetchRoles(workspace), [workspace]);
|
||||
const { data, loading, error, refresh } = usePolling<RoleInfo[]>(
|
||||
fetcher,
|
||||
5000,
|
||||
{ getChangeKey: roleListChangeKey },
|
||||
);
|
||||
|
||||
// Editor state: string = editing role name, undefined = closed
|
||||
const [editorTarget, setEditorTarget] = useState<string | undefined>();
|
||||
|
||||
const sortedRoles = useMemo(() => {
|
||||
if (!data) return [];
|
||||
return [...data].sort((left, right) => {
|
||||
if (left.sort_order !== right.sort_order) {
|
||||
return left.sort_order - right.sort_order;
|
||||
}
|
||||
const priorityDiff = getRolePriority(right) - getRolePriority(left);
|
||||
if (priorityDiff !== 0) {
|
||||
return priorityDiff;
|
||||
}
|
||||
return left.name.localeCompare(right.name);
|
||||
});
|
||||
}, [data]);
|
||||
|
||||
const activeSessions = data?.filter((role) => role.session).length ?? 0;
|
||||
const attentionCount = data?.filter((role) => role.pending > 0).length ?? 0;
|
||||
const idleCount = data ? data.length - activeSessions : 0;
|
||||
const waitingMessages = data?.reduce((sum, role) => sum + role.pending, 0) ?? 0;
|
||||
|
||||
return (
|
||||
<AsyncPageState
|
||||
workspace={workspace}
|
||||
workspaceSubject={copy.roleStatus.workspaceSubject}
|
||||
loading={loading}
|
||||
loadingMode="initial"
|
||||
hasData={Boolean(data && data.length > 0)}
|
||||
error={error}
|
||||
loadingEyebrow={copy.roleStatus.eyebrow}
|
||||
loadingTitle={copy.roleStatus.loadingTitle}
|
||||
errorEyebrow={copy.roleStatus.eyebrow}
|
||||
errorTitle={copy.roleStatus.errorTitle}
|
||||
emptyEyebrow={copy.roleStatus.eyebrow}
|
||||
emptyTitle={copy.roleStatus.emptyTitle}
|
||||
emptyDetail={copy.roleStatus.emptyDetail}
|
||||
retryLabel={copy.common.retry}
|
||||
onRetry={refresh}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<PageHero
|
||||
eyebrow={copy.roleStatus.eyebrow}
|
||||
title={copy.roleStatus.heroTitle}
|
||||
stats={
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:min-w-[24rem]">
|
||||
<SummaryStat
|
||||
label={copy.roleStatus.summary.activeAgents}
|
||||
value={activeSessions}
|
||||
detail={copy.roleStatus.summary.activeAgentsDetail}
|
||||
/>
|
||||
<SummaryStat
|
||||
label={copy.roleStatus.summary.needsAttention}
|
||||
value={attentionCount}
|
||||
detail={copy.roleStatus.summary.needsAttentionDetail}
|
||||
tone="attention"
|
||||
/>
|
||||
<SummaryStat
|
||||
label={copy.roleStatus.summary.waitingItems}
|
||||
value={waitingMessages}
|
||||
detail={copy.roleStatus.summary.waitingItemsDetail}
|
||||
/>
|
||||
<SummaryStat
|
||||
label={copy.roleStatus.summary.notStarted}
|
||||
value={idleCount}
|
||||
detail={copy.roleStatus.summary.notStartedDetail}
|
||||
tone="muted"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageSectionCard
|
||||
title={copy.roleStatus.listTitle}
|
||||
detail={copy.roleStatus.listDetail}
|
||||
action={<Chip size="sm">{sortedRoles.length}</Chip>}
|
||||
className="rounded-[26px]"
|
||||
headerClassName="px-4 py-4 sm:px-5"
|
||||
bodyClassName="grid gap-3 p-4 sm:p-5 md:grid-cols-2"
|
||||
>
|
||||
{sortedRoles.map((role) => {
|
||||
const state = getRoleState(role, copy);
|
||||
|
||||
return (
|
||||
<article
|
||||
key={role.name}
|
||||
className="app-panel-muted flex h-full flex-col rounded-2xl border border-[color:var(--app-divider-soft)] p-4"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<RoleBadge role={role.name} size="md" />
|
||||
<StatusDot className={`h-2 w-2 ${state.dotClassName}`} />
|
||||
<span className={`app-caption-medium ${state.textClassName}`}>
|
||||
{state.label}
|
||||
</span>
|
||||
{role.pending > 0 && (
|
||||
<Chip tone="attention" className="app-caption-medium">
|
||||
{copy.roleStatus.states.waiting(role.pending)}
|
||||
</Chip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p
|
||||
className="app-text-soft mt-3 text-sm"
|
||||
title={role.description}
|
||||
dir="auto"
|
||||
>
|
||||
{copy.roleStatus.conciseDescription[role.name as keyof typeof copy.roleStatus.conciseDescription] ?? role.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
onClick={() => setEditorTarget(role.name)}
|
||||
>
|
||||
{copy.common.edit}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{role.session?.last_message ? (
|
||||
<p
|
||||
className="app-text-faint mt-3 line-clamp-3 break-words text-xs leading-6"
|
||||
title={role.session.last_message}
|
||||
dir="auto"
|
||||
>
|
||||
{role.session.last_message}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<div className="mt-auto flex items-end justify-between gap-3 pt-4">
|
||||
<div className="min-w-0">
|
||||
<div className="app-text-faint app-caption">{copy.roleStatus.lastActive}</div>
|
||||
{role.session ? (
|
||||
<div
|
||||
className="app-text-primary mt-1 text-sm"
|
||||
title={formatAbsoluteDateTime(role.session.last_used_at)}
|
||||
>
|
||||
{formatRelativeTime(role.session.last_used_at)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="app-text-idle mt-1 text-sm">{copy.common.notStarted}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="app-text-faint text-right text-[11px] font-medium uppercase tracking-[0.16em]">
|
||||
#{role.sort_order}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</PageSectionCard>
|
||||
|
||||
{/* Role Editor slide-out panel */}
|
||||
{editorTarget !== undefined && (
|
||||
<RoleEditor
|
||||
workspace={workspace}
|
||||
roleName={editorTarget}
|
||||
onClose={() => setEditorTarget(undefined)}
|
||||
onSaved={() => {
|
||||
setEditorTarget(undefined);
|
||||
refresh();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</AsyncPageState>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user