Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improve group panel #3996

Merged
merged 2 commits into from
Mar 19, 2025
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions packages/client/src/components/combobox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { useState, useRef, useEffect } from 'react';
import { Input } from '@/components/ui/input';
import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { ChevronDown, X } from 'lucide-react';
import { formatAgentName } from '@/lib/utils';

interface Option {
icon: string;
label: string;
}

interface MultiSelectComboboxProps {
options: Option[];
className?: string;
onSelect?: (selected: Option[]) => void;
}

export default function MultiSelectCombobox({
options = [],
className = '',
onSelect,
}: MultiSelectComboboxProps) {
const [selected, setSelected] = useState<Option[]>([]);
const [isOpen, setIsOpen] = useState(false);
const comboboxRef = useRef<HTMLDivElement>(null);

useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (comboboxRef.current && !comboboxRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};

document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);

const toggleSelection = (option: Option) => {
setSelected((prev) => {
const newSelection = prev.some((item) => item.label === option.label)
? prev.filter((item) => item.label !== option.label)
: [...prev, option];
if (onSelect) onSelect(newSelection);
return newSelection;
});
};

const removeSelection = (option: Option) => {
setSelected((prev) => {
const newSelection = prev.filter((item) => item.label !== option.label);
if (onSelect) onSelect(newSelection);
return newSelection;
});
};

const removeExtraSelections = () => {
setSelected((prev) => {
const newSelection = prev.slice(0, 3); // Keep only the first 3
if (onSelect) onSelect(newSelection);
return newSelection;
});
};

return (
<div className={`relative w-80 bg-muted ${className}`} ref={comboboxRef}>
<div
className="flex items-center gap-2 border border-gray-300 p-2 rounded cursor-pointer"
onClick={() => setIsOpen(!isOpen)}
>
<div className="flex flex-wrap gap-1 w-full">
{selected.length > 0 ? (
<>
{selected.slice(0, 3).map((item, index) => (
<Badge key={index} className="flex items-center gap-1 px-2">
{item.label}
<X
size={12}
className="cursor-pointer"
onClick={(e) => {
e.stopPropagation();
removeSelection(item);
}}
/>
</Badge>
))}
{selected.length > 3 && (
<Badge
className="px-2 cursor-pointer"
onClick={(e) => {
e.stopPropagation();
removeExtraSelections();
}}
>
+{selected.length - 3} more
</Badge>
)}
</>
) : (
<span className="text-gray-500">Select options...</span>
)}
</div>
<ChevronDown size={16} />
</div>
{isOpen && (
<Card className="absolute left-0 mt-2 w-full p-2 shadow-md border border-gray-500 rounded z-40 max-h-60 overflow-y-auto">
{options.map((option, index) => (
<div
key={index}
className={`flex items-center gap-2 p-2 cursor-pointer rounded ${
selected.some((item) => item.label === option.label) ? 'bg-muted' : 'bg-card'
}`}
onClick={() => toggleSelection(option)}
>
<div className="bg-gray-500 rounded-full w-4 h-4 flex justify-center items-center overflow-hidden text-xs">
{option.icon ? (
<img src={option.icon} alt={option.label} className="w-full h-full" />
) : (
formatAgentName(option.label)
)}
</div>
{option.label}
</div>
))}
</Card>
)}
</div>
);
}
73 changes: 18 additions & 55 deletions packages/client/src/components/group-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import { Card, CardContent } from './ui/card';
import { formatAgentName } from '@/lib/utils';
import { Button } from './ui/button';
import { ImageIcon, Loader2, X } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import { Input } from './ui/input';
import { Switch } from './ui/switch-button';
import { apiClient } from '@/lib/api';
import { useQueryClient } from '@tanstack/react-query';
import { useNavigate } from 'react-router';
import { type Agent, AgentStatus } from '@elizaos/core';
import { UUID } from 'crypto';
import { GROUP_CHAT_SOURCE } from '@/constants';
import { useRooms } from '@/hooks/use-query-hooks';
import MultiSelectCombobox from './combobox';

interface GroupPanel {
agents: Agent[] | undefined;
@@ -21,7 +20,7 @@ interface GroupPanel {

export default function GroupPanel({ onClose, agents, groupId }: GroupPanel) {
const [chatName, setChatName] = useState(``);
const [selectedAgents, setSelectedAgents] = useState<{ [key: string]: boolean }>({});
const [selectedAgents, setSelectedAgents] = useState<Agent[]>([]);
const [creating, setCreating] = useState(false);
const [deleting, setDeleting] = useState(false);

@@ -44,13 +43,6 @@ export default function GroupPanel({ onClose, agents, groupId }: GroupPanel) {
}
}, [groupId]);

const toggleAgentSelection = (agentId: string) => {
setSelectedAgents((prev) => ({
...prev,
[agentId]: !prev[agentId],
}));
};

const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
@@ -119,7 +111,7 @@ export default function GroupPanel({ onClose, agents, groupId }: GroupPanel) {
{!avatar && <ImageIcon className="w-6 h-6 text-white" />}
</div>

<CardContent className="w-full flex grow flex-col items-center overflow-y-auto">
<CardContent className="w-full flex grow flex-col items-center">
<div className="rounded-md w-full mb-3">
<div className="flex h-full">
<div className="p-6 flex flex-col gap-4 w-full">
@@ -133,42 +125,19 @@ export default function GroupPanel({ onClose, agents, groupId }: GroupPanel) {
/>
</div>
<div className="font-light">Invite Agents</div>
<div className="overflow-scroll">
<div className="flex flex-col gap-4 pt-3">
{agents
<MultiSelectCombobox
options={
agents
?.filter((agent) => agent.status === AgentStatus.ACTIVE)
.map((agent) => {
return (
<div key={agent.id} className="bg-muted rounded-sm h-16">
<div className="flex w-full h-full justify-between items-center">
<div className="flex gap-2 items-center h-full w-full p-4">
<div className="bg-card rounded-full w-12 h-12 flex justify-center items-center overflow-hidden">
{agent && agent.settings?.avatar ? (
<img
src={agent.settings.avatar}
alt="Agent Avatar"
className="w-full h-full object-contain"
/>
) : (
formatAgentName(agent.name)
)}
</div>
<div className="flex flex-col justify-center items-center ml-2">
<div className="text-lg">{agent.name}</div>
</div>
</div>
<div className="mr-6">
<Switch
checked={selectedAgents[agent.id as UUID]}
onChange={() => toggleAgentSelection(agent.id as UUID)}
/>
</div>
</div>
</div>
);
})}
</div>
</div>
.map((agent) => ({
icon: agent.settings?.avatar || '',
label: agent.name,
id: agent.id,
})) || []
}
onSelect={(selected) => setSelectedAgents(selected)}
className="w-full"
/>
</div>
</div>
</div>
@@ -183,11 +152,7 @@ export default function GroupPanel({ onClose, agents, groupId }: GroupPanel) {
}
setCreating(true);
try {
const selectedAgentIds = Object.keys(selectedAgents).filter(
(agentId) => selectedAgents[agentId]
);

if (selectedAgentIds.length > 0) {
if (selectedAgents.length > 0) {
if (groupId) {
try {
await apiClient.deleteGroupChat(groupId);
@@ -196,13 +161,11 @@ export default function GroupPanel({ onClose, agents, groupId }: GroupPanel) {
}
}
await apiClient.createGroupChat(
selectedAgentIds,
selectedAgents.map((agent) => agent.id),
chatName,
serverId,
GROUP_CHAT_SOURCE,
{
thumbnail: avatar,
}
{ thumbnail: avatar }
);
}
} catch (error) {
30 changes: 0 additions & 30 deletions packages/client/src/components/ui/switch-button.tsx

This file was deleted.