@elizaos/client 1.5.5-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +350 -0
  3. package/dist/assets/empty-module-CLMscLYw.js +1 -0
  4. package/dist/assets/main-BBZ_3lkn.css +5999 -0
  5. package/dist/assets/main-C5zNUkXH.js +7 -0
  6. package/dist/assets/main-Dz64ENQg.js +614 -0
  7. package/dist/assets/react-vendor-DM5m98rr.js +545 -0
  8. package/dist/assets/ui-vendor-BQCqNqg0.js +1 -0
  9. package/dist/elizaos-avatar.png +0 -0
  10. package/dist/elizaos-icon.png +0 -0
  11. package/dist/elizaos-logo-light.png +0 -0
  12. package/dist/elizaos.webp +0 -0
  13. package/dist/favicon.ico +0 -0
  14. package/dist/images/agents/agent1.png +0 -0
  15. package/dist/images/agents/agent2.png +0 -0
  16. package/dist/images/agents/agent3.png +0 -0
  17. package/dist/images/agents/agent4.png +0 -0
  18. package/dist/images/agents/agent5.png +0 -0
  19. package/dist/index.html +14 -0
  20. package/index.html +24 -0
  21. package/package.json +159 -0
  22. package/postcss.config.js +3 -0
  23. package/public/elizaos-avatar.png +0 -0
  24. package/public/elizaos-icon.png +0 -0
  25. package/public/elizaos-logo-light.png +0 -0
  26. package/public/elizaos.webp +0 -0
  27. package/public/favicon.ico +0 -0
  28. package/public/images/agents/agent1.png +0 -0
  29. package/public/images/agents/agent2.png +0 -0
  30. package/public/images/agents/agent3.png +0 -0
  31. package/public/images/agents/agent4.png +0 -0
  32. package/public/images/agents/agent5.png +0 -0
  33. package/src/App.tsx +222 -0
  34. package/src/components/AgentDetailsPanel.tsx +147 -0
  35. package/src/components/ChatInputArea.tsx +196 -0
  36. package/src/components/ChatMessageListComponent.tsx +139 -0
  37. package/src/components/actionTool.tsx +186 -0
  38. package/src/components/add-agent-card.tsx +77 -0
  39. package/src/components/agent-action-viewer.tsx +816 -0
  40. package/src/components/agent-avatar-stack.tsx +121 -0
  41. package/src/components/agent-card.cy.tsx +259 -0
  42. package/src/components/agent-card.tsx +177 -0
  43. package/src/components/agent-creator.tsx +142 -0
  44. package/src/components/agent-log-viewer.tsx +645 -0
  45. package/src/components/agent-memory-edit-overlay.tsx +461 -0
  46. package/src/components/agent-memory-viewer.tsx +504 -0
  47. package/src/components/agent-settings.tsx +270 -0
  48. package/src/components/agent-sidebar.tsx +178 -0
  49. package/src/components/api-key-dialog.tsx +113 -0
  50. package/src/components/app-sidebar.tsx +685 -0
  51. package/src/components/array-input.tsx +116 -0
  52. package/src/components/audio-recorder.tsx +292 -0
  53. package/src/components/avatar-panel.tsx +141 -0
  54. package/src/components/character-form.tsx +1138 -0
  55. package/src/components/chat.tsx +1813 -0
  56. package/src/components/combobox.tsx +187 -0
  57. package/src/components/confirmation-dialog.tsx +59 -0
  58. package/src/components/connection-error-banner.tsx +101 -0
  59. package/src/components/connection-status.cy.tsx +73 -0
  60. package/src/components/connection-status.tsx +155 -0
  61. package/src/components/copy-button.tsx +35 -0
  62. package/src/components/delete-button.tsx +24 -0
  63. package/src/components/env-settings.tsx +261 -0
  64. package/src/components/group-card.tsx +160 -0
  65. package/src/components/group-panel.tsx +543 -0
  66. package/src/components/input-copy.tsx +21 -0
  67. package/src/components/logs-page.tsx +41 -0
  68. package/src/components/media-content.tsx +385 -0
  69. package/src/components/memory-graph.tsx +170 -0
  70. package/src/components/missing-secrets-dialog.tsx +72 -0
  71. package/src/components/onboarding-tour.tsx +247 -0
  72. package/src/components/page-title.tsx +8 -0
  73. package/src/components/plugins-panel.tsx +383 -0
  74. package/src/components/profile-card.tsx +66 -0
  75. package/src/components/profile-overlay.tsx +283 -0
  76. package/src/components/retry-button.tsx +28 -0
  77. package/src/components/secret-panel.tsx +1505 -0
  78. package/src/components/server-management.tsx +264 -0
  79. package/src/components/split-button.tsx +148 -0
  80. package/src/components/stop-agent-button.tsx +99 -0
  81. package/src/components/ui/alert-dialog.cy.tsx +333 -0
  82. package/src/components/ui/alert-dialog.tsx +115 -0
  83. package/src/components/ui/alert.tsx +49 -0
  84. package/src/components/ui/avatar.cy.tsx +180 -0
  85. package/src/components/ui/avatar.tsx +57 -0
  86. package/src/components/ui/badge.cy.tsx +146 -0
  87. package/src/components/ui/badge.tsx +43 -0
  88. package/src/components/ui/button.cy.tsx +177 -0
  89. package/src/components/ui/button.tsx +56 -0
  90. package/src/components/ui/card.cy.tsx +160 -0
  91. package/src/components/ui/card.tsx +73 -0
  92. package/src/components/ui/chat/animated-markdown.tsx +59 -0
  93. package/src/components/ui/chat/chat-bubble.tsx +178 -0
  94. package/src/components/ui/chat/chat-container.tsx +51 -0
  95. package/src/components/ui/chat/chat-input.cy.tsx +169 -0
  96. package/src/components/ui/chat/chat-input.tsx +47 -0
  97. package/src/components/ui/chat/chat-message-list.tsx +61 -0
  98. package/src/components/ui/chat/chat-tts-button.tsx +199 -0
  99. package/src/components/ui/chat/code-block.tsx +79 -0
  100. package/src/components/ui/chat/expandable-chat.tsx +131 -0
  101. package/src/components/ui/chat/hooks/useAutoScroll.ts +86 -0
  102. package/src/components/ui/chat/markdown.tsx +209 -0
  103. package/src/components/ui/chat/message-loading.tsx +48 -0
  104. package/src/components/ui/checkbox.cy.tsx +170 -0
  105. package/src/components/ui/checkbox.tsx +30 -0
  106. package/src/components/ui/collapsible.cy.tsx +283 -0
  107. package/src/components/ui/collapsible.tsx +9 -0
  108. package/src/components/ui/command.cy.tsx +313 -0
  109. package/src/components/ui/command.tsx +143 -0
  110. package/src/components/ui/dialog.cy.tsx +279 -0
  111. package/src/components/ui/dialog.tsx +104 -0
  112. package/src/components/ui/dropdown-menu.cy.tsx +273 -0
  113. package/src/components/ui/dropdown-menu.tsx +281 -0
  114. package/src/components/ui/input.cy.tsx +82 -0
  115. package/src/components/ui/input.tsx +27 -0
  116. package/src/components/ui/label.cy.tsx +157 -0
  117. package/src/components/ui/label.tsx +19 -0
  118. package/src/components/ui/resizable.tsx +42 -0
  119. package/src/components/ui/scroll-area.cy.tsx +242 -0
  120. package/src/components/ui/scroll-area.tsx +46 -0
  121. package/src/components/ui/select.cy.tsx +277 -0
  122. package/src/components/ui/select.tsx +155 -0
  123. package/src/components/ui/separator.cy.tsx +145 -0
  124. package/src/components/ui/separator.tsx +29 -0
  125. package/src/components/ui/sheet.cy.tsx +324 -0
  126. package/src/components/ui/sheet.tsx +119 -0
  127. package/src/components/ui/sidebar.tsx +734 -0
  128. package/src/components/ui/skeleton.cy.tsx +149 -0
  129. package/src/components/ui/skeleton.tsx +17 -0
  130. package/src/components/ui/split-button.cy.tsx +274 -0
  131. package/src/components/ui/split-button.tsx +112 -0
  132. package/src/components/ui/switch.tsx +28 -0
  133. package/src/components/ui/tabs.cy.tsx +271 -0
  134. package/src/components/ui/tabs.tsx +53 -0
  135. package/src/components/ui/textarea.cy.tsx +136 -0
  136. package/src/components/ui/textarea.tsx +26 -0
  137. package/src/components/ui/toast.cy.tsx +209 -0
  138. package/src/components/ui/toast.tsx +126 -0
  139. package/src/components/ui/toaster.tsx +29 -0
  140. package/src/components/ui/tooltip.cy.tsx +244 -0
  141. package/src/components/ui/tooltip.tsx +30 -0
  142. package/src/config/agent-templates.ts +349 -0
  143. package/src/config/voice-models.ts +181 -0
  144. package/src/constants.ts +23 -0
  145. package/src/context/AuthContext.tsx +44 -0
  146. package/src/context/ConnectionContext.tsx +194 -0
  147. package/src/entry.tsx +9 -0
  148. package/src/hooks/__tests__/use-agent-tab-state.test.ts +137 -0
  149. package/src/hooks/__tests__/use-agent-update.test.tsx +250 -0
  150. package/src/hooks/__tests__/use-character-convert.test.ts +102 -0
  151. package/src/hooks/__tests__/use-panel-width-state.test.ts +243 -0
  152. package/src/hooks/__tests__/use-sidebar-state.test.ts +117 -0
  153. package/src/hooks/use-agent-management.ts +130 -0
  154. package/src/hooks/use-agent-tab-state.ts +74 -0
  155. package/src/hooks/use-agent-update.ts +469 -0
  156. package/src/hooks/use-character-convert.ts +138 -0
  157. package/src/hooks/use-confirmation.ts +55 -0
  158. package/src/hooks/use-delete-agent.ts +123 -0
  159. package/src/hooks/use-dm-channels.ts +198 -0
  160. package/src/hooks/use-elevenlabs-voices.ts +83 -0
  161. package/src/hooks/use-file-upload.ts +224 -0
  162. package/src/hooks/use-mobile.tsx +19 -0
  163. package/src/hooks/use-onboarding.tsx +49 -0
  164. package/src/hooks/use-panel-width-state.ts +147 -0
  165. package/src/hooks/use-partial-update.ts +288 -0
  166. package/src/hooks/use-plugin-details.ts +462 -0
  167. package/src/hooks/use-plugins.ts +119 -0
  168. package/src/hooks/use-query-hooks.ts +1263 -0
  169. package/src/hooks/use-server-agents.ts +62 -0
  170. package/src/hooks/use-server-version.tsx +47 -0
  171. package/src/hooks/use-sidebar-state.ts +50 -0
  172. package/src/hooks/use-socket-chat.ts +264 -0
  173. package/src/hooks/use-toast.ts +260 -0
  174. package/src/hooks/use-version.tsx +64 -0
  175. package/src/index.css +146 -0
  176. package/src/lib/api-client-config.ts +53 -0
  177. package/src/lib/api-type-mappers.ts +196 -0
  178. package/src/lib/export-utils.ts +123 -0
  179. package/src/lib/logger.ts +19 -0
  180. package/src/lib/media-utils.ts +170 -0
  181. package/src/lib/pca.test.ts +17 -0
  182. package/src/lib/pca.ts +52 -0
  183. package/src/lib/socketio-manager.ts +664 -0
  184. package/src/lib/utils.ts +168 -0
  185. package/src/main.tsx +16 -0
  186. package/src/mocks/empty-module.ts +12 -0
  187. package/src/mocks/node-module.ts +57 -0
  188. package/src/polyfills.ts +37 -0
  189. package/src/routes/agent-detail.tsx +30 -0
  190. package/src/routes/agent-list.tsx +27 -0
  191. package/src/routes/agent-settings.tsx +48 -0
  192. package/src/routes/character-detail.tsx +52 -0
  193. package/src/routes/character-form.tsx +79 -0
  194. package/src/routes/character-list.tsx +38 -0
  195. package/src/routes/chat.tsx +128 -0
  196. package/src/routes/createAgent.tsx +13 -0
  197. package/src/routes/group-new.tsx +50 -0
  198. package/src/routes/group.tsx +29 -0
  199. package/src/routes/home.tsx +218 -0
  200. package/src/routes/not-found.tsx +71 -0
  201. package/src/test/setup.ts +154 -0
  202. package/src/types/crypto-browserify.d.ts +4 -0
  203. package/src/types/index.ts +13 -0
  204. package/src/types/rooms.ts +8 -0
  205. package/src/types.ts +84 -0
  206. package/src/vite-env.d.ts +40 -0
  207. package/tailwind.config.ts +90 -0
  208. package/tsconfig.json +10 -0
  209. package/vite.config.ts +102 -0
@@ -0,0 +1,543 @@
1
+ import { ChannelType, validateUuid } from '@elizaos/core';
2
+ import { Separator } from '@/components/ui/separator';
3
+ import { GROUP_CHAT_SOURCE } from '@/constants';
4
+ import { useAgentsWithDetails, useChannels } from '@/hooks/use-query-hooks';
5
+ import { createElizaClient } from '@/lib/api-client-config';
6
+ import { type Agent, AgentStatus, type UUID } from '@elizaos/core';
7
+ import { useQueryClient, useQuery, useMutation, type UseQueryResult } from '@tanstack/react-query';
8
+ import { Loader2, Trash, X } from 'lucide-react';
9
+ import { useEffect, useState, useMemo, useCallback, useRef } from 'react';
10
+ import { useNavigate } from 'react-router';
11
+ import MultiSelectCombobox from './combobox';
12
+ import { Button } from './ui/button';
13
+ import { Card, CardContent, CardFooter, CardHeader, CardTitle } from './ui/card';
14
+ import { Input } from './ui/input';
15
+ import { useToast } from '@/hooks/use-toast';
16
+ import clientLogger from '@/lib/logger';
17
+ import { useConfirmation } from '@/hooks/use-confirmation';
18
+ import ConfirmationDialog from './confirmation-dialog';
19
+
20
+ const DEFAULT_SERVER_ID = '00000000-0000-0000-0000-000000000000' as UUID;
21
+
22
+ // This Option type must precisely match what MultiSelectCombobox.tsx expects for its props.
23
+ // Based on MultiSelectCombobox.tsx: { icon: string; label: string; id?: string; }
24
+ interface ComboboxOption {
25
+ icon: string;
26
+ label: string;
27
+ id?: string; // We will always provide agent.id here, which is UUID (string)
28
+ }
29
+
30
+ interface GroupPanelProps {
31
+ onClose: () => void;
32
+ channelId?: UUID;
33
+ }
34
+
35
+ interface ChannelParticipantsResponse {
36
+ success: boolean;
37
+ data?: UUID[];
38
+ error?: { message?: string; code?: number | string };
39
+ }
40
+
41
+ type SelectableAgent = Agent & { id: UUID; name: string };
42
+
43
+ function isAgentSelectable(agent: Partial<Agent>): agent is SelectableAgent {
44
+ return (
45
+ !!agent.id &&
46
+ !!validateUuid(agent.id) &&
47
+ typeof agent.name === 'string' &&
48
+ agent.name.trim() !== ''
49
+ );
50
+ }
51
+
52
+ export default function GroupPanel({ onClose, channelId }: GroupPanelProps) {
53
+ const [chatName, setChatName] = useState('');
54
+ const [selectedAgents, setSelectedAgents] = useState<SelectableAgent[]>([]);
55
+ const [initialChatName, setInitialChatName] = useState('');
56
+ const [initialSelectedAgentIds, setInitialSelectedAgentIds] = useState<UUID[]>([]);
57
+ const serverId = DEFAULT_SERVER_ID;
58
+ const initializedRef = useRef(false);
59
+ const lastChannelIdRef = useRef(channelId);
60
+ const agentsInitializedRef = useRef(false);
61
+
62
+ const { data: channelsData } = useChannels(channelId ? serverId : undefined, {
63
+ enabled: !!channelId,
64
+ });
65
+ const {
66
+ data: agentsData,
67
+ isLoading: isLoadingAgents,
68
+ isError: isErrorAgents,
69
+ } = useAgentsWithDetails();
70
+
71
+ const allAvailableSelectableAgents = useMemo(() => {
72
+ return (agentsData?.agents || []).filter(isAgentSelectable);
73
+ }, [agentsData]);
74
+
75
+ const queryClient = useQueryClient();
76
+ const navigate = useNavigate();
77
+ const { toast } = useToast();
78
+
79
+ const { confirm, isOpen: confirmOpen, onOpenChange, onConfirm, options } = useConfirmation();
80
+
81
+ // Force fetch participants immediately when component mounts with channelId
82
+ useEffect(() => {
83
+ if (channelId) {
84
+ queryClient.invalidateQueries({ queryKey: ['channelParticipants', channelId] });
85
+ queryClient.refetchQueries({ queryKey: ['channelParticipants', channelId] });
86
+ }
87
+ }, [channelId, queryClient]);
88
+
89
+ // Create group mutation
90
+ const createGroupMutation = useMutation({
91
+ mutationFn: async ({ name, participantIds }: { name: string; participantIds: UUID[] }) => {
92
+ const elizaClient = createElizaClient();
93
+ return await elizaClient.messaging.createGroupChannel({
94
+ name,
95
+ participantIds: participantIds,
96
+ metadata: {
97
+ type: ChannelType.GROUP,
98
+ server_id: serverId,
99
+ source: GROUP_CHAT_SOURCE,
100
+ },
101
+ });
102
+ },
103
+ onSuccess: (response) => {
104
+ if (response) {
105
+ toast({ title: 'Success', description: 'Group created successfully.' });
106
+ queryClient.invalidateQueries({ queryKey: ['channels', serverId] });
107
+ queryClient.invalidateQueries({ queryKey: ['channels'] });
108
+ onClose();
109
+ setTimeout(() => {
110
+ navigate(`/group/${response.id}?serverId=${serverId}`);
111
+ }, 100);
112
+ }
113
+ },
114
+ onError: (error) => {
115
+ clientLogger.error('Failed to create group', error);
116
+ const errorMsg = error instanceof Error ? error.message : 'Failed to create group.';
117
+ toast({ title: 'Error', description: errorMsg, variant: 'destructive' });
118
+ },
119
+ });
120
+
121
+ // Update group mutation
122
+ const updateGroupMutation = useMutation({
123
+ mutationFn: async ({ name, participantIds }: { name: string; participantIds: UUID[] }) => {
124
+ if (!channelId) throw new Error('Channel ID is required for update');
125
+ const elizaClient = createElizaClient();
126
+ return await elizaClient.messaging.updateChannel(channelId, {
127
+ name,
128
+ participantCentralUserIds: participantIds,
129
+ });
130
+ },
131
+ onSuccess: () => {
132
+ toast({ title: 'Group Updated', description: 'Group details updated successfully.' });
133
+ queryClient.invalidateQueries({ queryKey: ['channels', serverId] });
134
+ queryClient.invalidateQueries({ queryKey: ['channels'] });
135
+ onClose();
136
+ setTimeout(() => {
137
+ navigate(`/group/${channelId}?serverId=${serverId}`);
138
+ }, 100);
139
+ },
140
+ onError: (error) => {
141
+ clientLogger.error('Failed to update group', error);
142
+ console.error('Group update error details:', error);
143
+ const errorMsg = error instanceof Error ? error.message : 'Failed to update group.';
144
+ toast({ title: 'Error', description: errorMsg, variant: 'destructive' });
145
+ },
146
+ });
147
+
148
+ // Delete group mutation
149
+ const deleteGroupMutation = useMutation({
150
+ mutationFn: async () => {
151
+ if (!channelId) throw new Error('Channel ID is required for delete');
152
+ const elizaClient = createElizaClient();
153
+ return await elizaClient.messaging.deleteChannel(channelId);
154
+ },
155
+ onSuccess: () => {
156
+ toast({ title: 'Group Deleted', description: 'The group has been successfully deleted.' });
157
+ queryClient.invalidateQueries({ queryKey: ['channels', serverId] });
158
+ queryClient.invalidateQueries({ queryKey: ['channels'] });
159
+ navigate('/');
160
+ onClose();
161
+ },
162
+ onError: (error) => {
163
+ clientLogger.error('Failed to delete channel', error);
164
+ const errorMsg = error instanceof Error ? error.message : 'Could not delete group.';
165
+ if (typeof error === 'object' && error !== null && (error as any).statusCode === 404) {
166
+ toast({
167
+ title: 'Error Deleting Group',
168
+ description: 'Delete operation not found on server.',
169
+ variant: 'destructive',
170
+ });
171
+ } else {
172
+ toast({ title: 'Error Deleting Group', description: errorMsg, variant: 'destructive' });
173
+ }
174
+ },
175
+ });
176
+
177
+ const {
178
+ data: channelParticipantsApiResponse,
179
+ isLoading: isLoadingChannelParticipants,
180
+ isError: isErrorChannelParticipants,
181
+ error: errorChannelParticipants,
182
+ }: UseQueryResult<ChannelParticipantsResponse, Error> = useQuery({
183
+ queryKey: ['channelParticipants', channelId],
184
+ queryFn: async () => {
185
+ if (!channelId) return { success: true, data: [] };
186
+ try {
187
+ const elizaClient = createElizaClient();
188
+ const result = await elizaClient.messaging.getChannelParticipants(channelId);
189
+
190
+ // Handle different possible response formats
191
+ let participants = [];
192
+ if (result && Array.isArray(result.participants)) {
193
+ participants = result.participants.map((participant) => participant.userId);
194
+ } else if (result && Array.isArray(result)) {
195
+ // If result is directly an array
196
+ participants = result.map(
197
+ (participant) => participant.userId || participant.id || participant
198
+ );
199
+ }
200
+
201
+ return { success: true, data: participants };
202
+ } catch (error) {
203
+ console.error('[GroupPanel] Error fetching channel participants:', error);
204
+ return {
205
+ success: false,
206
+ error: { message: error instanceof Error ? error.message : 'Unknown error' },
207
+ };
208
+ }
209
+ },
210
+ enabled: !!channelId,
211
+ retry: false,
212
+ refetchOnMount: true,
213
+ refetchOnWindowFocus: false,
214
+ staleTime: 0, // Always fetch fresh data
215
+ gcTime: 0, // Don't cache results
216
+ });
217
+
218
+ // Separate effect for initializing chat name when channel loads
219
+ useEffect(() => {
220
+ if (channelId && channelsData?.data?.channels) {
221
+ const channelDetails = channelsData.data.channels.find((ch) => ch.id === channelId);
222
+ if (!initializedRef.current || lastChannelIdRef.current !== channelId) {
223
+ const initialName = channelDetails?.name || '';
224
+ setChatName(initialName);
225
+ setInitialChatName(initialName);
226
+ initializedRef.current = true;
227
+ lastChannelIdRef.current = channelId;
228
+ agentsInitializedRef.current = false; // Reset agents initialization for new channel
229
+ }
230
+ } else if (!channelId) {
231
+ // Reset for create mode
232
+ initializedRef.current = false;
233
+ setChatName('');
234
+ setSelectedAgents([]);
235
+ setInitialChatName('');
236
+ setInitialSelectedAgentIds([]);
237
+ }
238
+ }, [channelId, channelsData]);
239
+
240
+ // Separate effect for handling participants
241
+ useEffect(() => {
242
+ if (isLoadingAgents) return;
243
+ if (channelId && isLoadingChannelParticipants) return;
244
+ if (!channelId) {
245
+ // Reset for create mode
246
+ agentsInitializedRef.current = false;
247
+ return;
248
+ }
249
+
250
+ // Only initialize once per channel
251
+ if (agentsInitializedRef.current && lastChannelIdRef.current === channelId) return;
252
+
253
+ if (isErrorChannelParticipants) {
254
+ toast({
255
+ title: 'Error',
256
+ description: `Could not load group participants: ${errorChannelParticipants?.message || 'Unknown error'}`,
257
+ variant: 'destructive',
258
+ });
259
+ setSelectedAgents([]);
260
+ setInitialSelectedAgentIds([]);
261
+ agentsInitializedRef.current = true;
262
+ return;
263
+ }
264
+
265
+ if (channelParticipantsApiResponse?.success && channelParticipantsApiResponse.data) {
266
+ const participantIds = channelParticipantsApiResponse.data;
267
+ const newSelected = allAvailableSelectableAgents.filter((agent) =>
268
+ participantIds.includes(agent.id)
269
+ );
270
+
271
+ setSelectedAgents(newSelected);
272
+ setInitialSelectedAgentIds(newSelected.map((a) => a.id));
273
+ agentsInitializedRef.current = true;
274
+ } else if (channelParticipantsApiResponse && !channelParticipantsApiResponse.success) {
275
+ toast({
276
+ title: 'Error',
277
+ description: `Could not load group participants: ${channelParticipantsApiResponse.error?.message || 'Server error'}`,
278
+ variant: 'destructive',
279
+ });
280
+ setSelectedAgents([]);
281
+ setInitialSelectedAgentIds([]);
282
+ agentsInitializedRef.current = true;
283
+ } else {
284
+ setSelectedAgents([]);
285
+ setInitialSelectedAgentIds([]);
286
+ agentsInitializedRef.current = true;
287
+ }
288
+ }, [
289
+ channelId,
290
+ isLoadingAgents,
291
+ isLoadingChannelParticipants,
292
+ channelParticipantsApiResponse,
293
+ allAvailableSelectableAgents,
294
+ isErrorChannelParticipants,
295
+ errorChannelParticipants,
296
+ toast,
297
+ ]);
298
+
299
+ const comboboxOptions: ComboboxOption[] = useMemo(() => {
300
+ if (isLoadingAgents || isErrorAgents) return [];
301
+ return allAvailableSelectableAgents.map((agent) => ({
302
+ id: agent.id,
303
+ label: `${agent.name}${agent.status === AgentStatus.INACTIVE ? ' (Inactive)' : ''}`,
304
+ icon: typeof agent.settings?.avatar === 'string' ? agent.settings.avatar : '', // Ensure icon is always a string
305
+ }));
306
+ }, [allAvailableSelectableAgents, isLoadingAgents, isErrorAgents]);
307
+
308
+ const STABLE_EMPTY_COMBOBOX_OPTIONS_ARRAY = useMemo(() => [], []);
309
+
310
+ const initialSelectedComboboxOptions: ComboboxOption[] = useMemo(() => {
311
+ if (isLoadingAgents) return STABLE_EMPTY_COMBOBOX_OPTIONS_ARRAY;
312
+ if (!channelId) return STABLE_EMPTY_COMBOBOX_OPTIONS_ARRAY; // Create mode
313
+
314
+ // In edit mode, wait for agents to be initialized before determining selection
315
+ if (channelId && !agentsInitializedRef.current) return STABLE_EMPTY_COMBOBOX_OPTIONS_ARRAY;
316
+
317
+ const options = selectedAgents.map((agent) => ({
318
+ id: agent.id,
319
+ label: `${agent.name}${agent.status === AgentStatus.INACTIVE ? ' (Inactive)' : ''}`,
320
+ icon: typeof agent.settings?.avatar === 'string' ? agent.settings.avatar : '',
321
+ }));
322
+
323
+ return options;
324
+ }, [channelId, selectedAgents, isLoadingAgents, STABLE_EMPTY_COMBOBOX_OPTIONS_ARRAY]);
325
+
326
+ const handleSelectAgents = useCallback(
327
+ (selectedOptions: ComboboxOption[]) => {
328
+ const newSelectedAgentObjects = allAvailableSelectableAgents.filter((agent) =>
329
+ selectedOptions.some((option) => option.id === agent.id)
330
+ );
331
+ setSelectedAgents(newSelectedAgentObjects);
332
+ },
333
+ [allAvailableSelectableAgents]
334
+ );
335
+
336
+ const handleDeleteGroup = useCallback(async () => {
337
+ if (!channelId) return;
338
+ const channel = channelsData?.data?.channels.find((ch) => ch.id === channelId);
339
+ confirm(
340
+ {
341
+ title: 'Delete Group',
342
+ description: `Are you sure you want to permanently delete the group chat "${channel?.name || chatName || 'this group'}"? This action cannot be undone.`,
343
+ confirmText: 'Delete',
344
+ variant: 'destructive',
345
+ },
346
+ () => {
347
+ deleteGroupMutation.mutate();
348
+ }
349
+ );
350
+ }, [channelId, chatName, channelsData, deleteGroupMutation]);
351
+
352
+ // Check if form has changed
353
+ const hasFormChanged = useMemo(() => {
354
+ if (!channelId) return true; // Always allow creation
355
+
356
+ const nameChanged = chatName.trim() !== initialChatName.trim();
357
+ const currentAgentIds = selectedAgents.map((a) => a.id).sort();
358
+ const initialAgentIds = initialSelectedAgentIds.sort();
359
+ const agentsChanged = JSON.stringify(currentAgentIds) !== JSON.stringify(initialAgentIds);
360
+
361
+ return nameChanged || agentsChanged;
362
+ }, [channelId, chatName, initialChatName, selectedAgents, initialSelectedAgentIds]);
363
+
364
+ const handleCreateOrUpdateGroup = useCallback(async () => {
365
+ // For create mode, require at least one agent
366
+ if (!channelId && selectedAgents.length === 0) {
367
+ toast({
368
+ title: 'Validation Error',
369
+ description: 'Please select at least one agent for the group.',
370
+ variant: 'destructive',
371
+ });
372
+ return;
373
+ }
374
+
375
+ const proceed = () => {
376
+ const participantIds = selectedAgents.map((agent) => agent.id);
377
+ const finalName =
378
+ chatName.trim() ||
379
+ (selectedAgents.length > 0
380
+ ? selectedAgents.map((agent) => agent.name).join(', ')
381
+ : 'Empty Group');
382
+
383
+ if (!channelId) {
384
+ createGroupMutation.mutate({ name: finalName, participantIds });
385
+ } else {
386
+ updateGroupMutation.mutate({ name: finalName, participantIds });
387
+ }
388
+ };
389
+
390
+ // For edit mode, warn if removing all agents but allow it
391
+ if (channelId && selectedAgents.length === 0) {
392
+ confirm(
393
+ {
394
+ title: 'Remove All Agents?',
395
+ description:
396
+ 'Are you sure you want to remove all agents from this group? This will leave the group with no participants.',
397
+ confirmText: 'Remove All',
398
+ variant: 'destructive',
399
+ },
400
+ () => {
401
+ proceed();
402
+ }
403
+ );
404
+ return;
405
+ }
406
+
407
+ proceed();
408
+ }, [channelId, chatName, selectedAgents, createGroupMutation, updateGroupMutation, toast]);
409
+
410
+ // Use the exact same logic as "unsaved changes" detection for update button
411
+ const isSubmitDisabled = channelId
412
+ ? // Edit mode - disable if no changes OR loading (allow agent removal)
413
+ !hasFormChanged ||
414
+ createGroupMutation.isPending ||
415
+ updateGroupMutation.isPending ||
416
+ deleteGroupMutation.isPending
417
+ : // Create mode - disable if no agents OR loading
418
+ selectedAgents.length === 0 ||
419
+ createGroupMutation.isPending ||
420
+ updateGroupMutation.isPending ||
421
+ deleteGroupMutation.isPending;
422
+
423
+ return (
424
+ <>
425
+ <div
426
+ className="fixed inset-0 bg-black bg-opacity-80 flex items-center justify-center z-50"
427
+ onClick={onClose}
428
+ >
429
+ <Card className="w-[80%] max-w-2xl" onClick={(e) => e.stopPropagation()}>
430
+ <CardHeader className="flex flex-row items-center justify-between space-y-0">
431
+ <CardTitle className="text-xl font-semibold">
432
+ {channelId ? 'Edit Group Chat' : 'Create Group Chat'}
433
+ </CardTitle>
434
+ <Button variant="ghost" size="sm" onClick={onClose}>
435
+ <X className="h-4 w-4" />
436
+ </Button>
437
+ </CardHeader>
438
+
439
+ <Separator />
440
+
441
+ <CardContent className="pt-4">
442
+ <div className="flex flex-col gap-4 w-full">
443
+ <div className="flex flex-col gap-2 w-full">
444
+ <label htmlFor="chat-name" className="text-sm font-medium">
445
+ Chat Name (Optional)
446
+ </label>
447
+ <Input
448
+ id="chat-name"
449
+ value={chatName}
450
+ onChange={(e) => setChatName(e.target.value)}
451
+ className="w-full bg-background text-foreground"
452
+ placeholder="Leave blank to auto-generate from participants"
453
+ disabled={
454
+ createGroupMutation.isPending ||
455
+ updateGroupMutation.isPending ||
456
+ deleteGroupMutation.isPending
457
+ }
458
+ />
459
+ </div>
460
+
461
+ <div className="flex flex-col gap-2 w-full">
462
+ <label htmlFor="invite-agents" className="text-sm font-medium">
463
+ Select Agents{' '}
464
+ {!channelId && <span className="text-muted-foreground">(Required)</span>}
465
+ </label>
466
+ {isLoadingAgents ? (
467
+ <div className="flex items-center justify-center p-4">
468
+ <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
469
+ <span className="ml-2 text-muted-foreground">Loading agents...</span>
470
+ </div>
471
+ ) : isErrorAgents ? (
472
+ <div className="flex items-center justify-center p-4 text-red-500">
473
+ Error loading agents. Please try again later.
474
+ </div>
475
+ ) : (
476
+ <MultiSelectCombobox
477
+ options={comboboxOptions}
478
+ onSelect={handleSelectAgents}
479
+ className="w-full"
480
+ initialSelected={initialSelectedComboboxOptions}
481
+ key={`group-panel-combobox-${channelId || 'create'}-${allAvailableSelectableAgents.length}`}
482
+ />
483
+ )}
484
+ {selectedAgents.length > 0 && (
485
+ <p className="text-sm text-muted-foreground">
486
+ {selectedAgents.length} agent{selectedAgents.length > 1 ? 's' : ''} selected
487
+ </p>
488
+ )}
489
+ {channelId && hasFormChanged && (
490
+ <div className="text-sm text-blue-500 mt-1">You have unsaved changes</div>
491
+ )}
492
+ </div>
493
+ </div>
494
+ </CardContent>
495
+
496
+ <CardFooter className="flex justify-between pt-4">
497
+ {channelId && (
498
+ <Button
499
+ variant="destructive"
500
+ onClick={handleDeleteGroup}
501
+ disabled={
502
+ deleteGroupMutation.isPending ||
503
+ createGroupMutation.isPending ||
504
+ updateGroupMutation.isPending ||
505
+ isLoadingAgents
506
+ }
507
+ >
508
+ {deleteGroupMutation.isPending ? (
509
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
510
+ ) : (
511
+ <Trash className="mr-2 h-4 w-4" />
512
+ )}
513
+ Delete Group
514
+ </Button>
515
+ )}
516
+
517
+ <Button
518
+ variant="default"
519
+ className={channelId ? '' : 'w-full'}
520
+ onClick={handleCreateOrUpdateGroup}
521
+ disabled={isSubmitDisabled}
522
+ >
523
+ {(createGroupMutation.isPending || updateGroupMutation.isPending) && (
524
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
525
+ )}
526
+ {channelId ? 'Update Group' : 'Create Group'}
527
+ </Button>
528
+ </CardFooter>
529
+ </Card>
530
+ </div>
531
+ <ConfirmationDialog
532
+ open={confirmOpen}
533
+ onOpenChange={onOpenChange}
534
+ title={options?.title || ''}
535
+ description={options?.description || ''}
536
+ confirmText={options?.confirmText}
537
+ cancelText={options?.cancelText}
538
+ variant={options?.variant}
539
+ onConfirm={onConfirm}
540
+ />
541
+ </>
542
+ );
543
+ }
@@ -0,0 +1,21 @@
1
+ import { Input } from './ui/input';
2
+ import { Label } from './ui/label';
3
+
4
+ export default function InputCopy({
5
+ title,
6
+ name,
7
+ value,
8
+ onChange,
9
+ }: {
10
+ title: string;
11
+ name: string;
12
+ value: string | number | undefined;
13
+ onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
14
+ }) {
15
+ return (
16
+ <div className="space-y-2">
17
+ <Label>{title}</Label>
18
+ <Input name={name} value={value} onChange={onChange} />
19
+ </div>
20
+ );
21
+ }
@@ -0,0 +1,41 @@
1
+ import { useState } from 'react';
2
+ import { AgentLogViewer } from './agent-log-viewer';
3
+ import { Button } from './ui/button';
4
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
5
+ import { Label } from './ui/label';
6
+
7
+ export default function LogsPage() {
8
+ const [logLevel, setLogLevel] = useState('all');
9
+
10
+ return (
11
+ <div className="container mx-auto p-6 h-full flex flex-col">
12
+ <div className="flex items-center justify-between mb-6">
13
+ <h1 className="text-2xl font-bold">System Logs</h1>
14
+ <div className="flex items-center gap-4">
15
+ <div className="flex items-center gap-2">
16
+ <Label htmlFor="log-level">Log Level</Label>
17
+ <Select value={logLevel} onValueChange={setLogLevel}>
18
+ <SelectTrigger id="log-level" className="w-32">
19
+ <SelectValue placeholder="Select level" />
20
+ </SelectTrigger>
21
+ <SelectContent>
22
+ <SelectItem value="all">All</SelectItem>
23
+ <SelectItem value="info">Info</SelectItem>
24
+ <SelectItem value="warn">Warning</SelectItem>
25
+ <SelectItem value="error">Error</SelectItem>
26
+ <SelectItem value="debug">Debug</SelectItem>
27
+ </SelectContent>
28
+ </Select>
29
+ </div>
30
+ <Button variant="outline" onClick={() => window.location.reload()}>
31
+ Refresh
32
+ </Button>
33
+ </div>
34
+ </div>
35
+
36
+ <div className="flex-1 border rounded-lg overflow-hidden">
37
+ <AgentLogViewer level={logLevel as any} />
38
+ </div>
39
+ </div>
40
+ );
41
+ }