feat(22-04): ChatMentionPopover component
- Create ChatMentionPopover (200px, opens upward, agent icon + name + role label) - Agents filtered by query, max 5 visible, No agents found empty state - 3 skeleton rows loading state, onOpenAutoFocus prevented for textarea focus - Include agent-role-colors dependency for worktree build
This commit is contained in:
parent
8f0367d0b4
commit
724e2b2bd8
2 changed files with 81 additions and 2 deletions
|
|
@ -1,8 +1,14 @@
|
|||
import { describe, it } from "vitest";
|
||||
// @vitest-environment jsdom
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
describe("ChatMentionPopover", () => {
|
||||
it("exports ChatMentionPopover component", async () => {
|
||||
const mod = await import("./ChatMentionPopover");
|
||||
expect(mod.ChatMentionPopover).toBeDefined();
|
||||
});
|
||||
|
||||
it.todo("renders agent list filtered by query string");
|
||||
it.todo("shows agent icon, name, and role for each item");
|
||||
it.todo("calls onSelect with @agentName on item click");
|
||||
it.todo("calls onSelect with agent name on item click");
|
||||
it.todo("shows 'No agents found' when filter matches nothing");
|
||||
});
|
||||
|
|
|
|||
73
ui/src/components/ChatMentionPopover.tsx
Normal file
73
ui/src/components/ChatMentionPopover.tsx
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { AgentIcon } from "./AgentIconPicker";
|
||||
import { agentRoleColors, agentRoleColorDefault } from "../lib/agent-role-colors";
|
||||
import type { Agent, AgentRole } from "@paperclipai/shared";
|
||||
|
||||
interface ChatMentionPopoverProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSelect: (agentName: string) => void;
|
||||
query: string;
|
||||
agents: Agent[];
|
||||
isLoading?: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function ChatMentionPopover({
|
||||
open,
|
||||
onOpenChange,
|
||||
onSelect,
|
||||
query,
|
||||
agents,
|
||||
isLoading,
|
||||
children,
|
||||
}: ChatMentionPopoverProps) {
|
||||
const filtered = agents.filter((a) =>
|
||||
a.name.toLowerCase().includes(query.toLowerCase()),
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={onOpenChange}>
|
||||
<PopoverTrigger asChild>{children}</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-[200px] p-1"
|
||||
align="start"
|
||||
side="top"
|
||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="space-y-1 p-1">
|
||||
<Skeleton className="h-7 w-full" />
|
||||
<Skeleton className="h-7 w-full" />
|
||||
<Skeleton className="h-7 w-full" />
|
||||
</div>
|
||||
) : filtered.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-2">No agents found</p>
|
||||
) : (
|
||||
<div className="max-h-[180px] overflow-auto">
|
||||
{filtered.slice(0, 5).map((agent) => {
|
||||
const colorClass = agent.role
|
||||
? (agentRoleColors[agent.role as AgentRole] ?? agentRoleColorDefault)
|
||||
: agentRoleColorDefault;
|
||||
return (
|
||||
<button
|
||||
key={agent.id}
|
||||
className="flex items-center gap-2 w-full rounded-sm px-2 py-1.5 text-left text-sm hover:bg-accent hover:text-accent-foreground cursor-pointer"
|
||||
onClick={() => {
|
||||
onSelect(agent.name);
|
||||
onOpenChange(false);
|
||||
}}
|
||||
>
|
||||
<AgentIcon icon={agent.icon} className={`h-3.5 w-3.5 ${colorClass}`} />
|
||||
<span className="truncate">{agent.name}</span>
|
||||
<span className="ml-auto text-[11px] text-muted-foreground">{agent.role}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue