@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,123 @@
1
+ import { useState, useCallback } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import { useToast } from './use-toast';
4
+ import { useQueryClient } from '@tanstack/react-query';
5
+ import { createElizaClient } from '@/lib/api-client-config';
6
+ import type { Agent } from '@elizaos/core';
7
+
8
+ export function useDeleteAgent(targetAgentData: Agent) {
9
+ const [isDeleting, setIsDeleting] = useState(false);
10
+ const { toast } = useToast();
11
+ const navigate = useNavigate();
12
+ const queryClient = useQueryClient();
13
+
14
+ const handleDelete = useCallback(async () => {
15
+ setIsDeleting(true);
16
+ toast({
17
+ title: 'Deleting...',
18
+ description: `Deleting agent "${targetAgentData.name}"`,
19
+ });
20
+
21
+ let responseReceived = false;
22
+ let navigationTimer = null;
23
+
24
+ try {
25
+ navigationTimer = setTimeout(() => {
26
+ if (!responseReceived) {
27
+ queryClient.invalidateQueries({ queryKey: ['agents'] });
28
+ navigate('/');
29
+ toast({
30
+ title: 'Note',
31
+ description: 'Deletion is still processing in the background.',
32
+ });
33
+ }
34
+ }, 8000);
35
+
36
+ const elizaClient = createElizaClient();
37
+
38
+ // Ensure we have a valid ID
39
+ if (!targetAgentData.id) {
40
+ throw new Error('Agent ID is required for deletion');
41
+ }
42
+
43
+ const response = await elizaClient.agents.deleteAgent(targetAgentData.id);
44
+ responseReceived = true;
45
+
46
+ if (navigationTimer) {
47
+ clearTimeout(navigationTimer);
48
+ }
49
+
50
+ // Check if response indicates partial completion (need to verify actual API response type)
51
+ const isPartial = (response as any)?.partial;
52
+
53
+ if (isPartial) {
54
+ toast({
55
+ title: 'Processing',
56
+ description: 'Deletion is still in progress and will complete in the background.',
57
+ });
58
+ } else {
59
+ toast({
60
+ title: 'Success',
61
+ description: 'Agent deleted successfully',
62
+ });
63
+ }
64
+
65
+ queryClient.invalidateQueries({ queryKey: ['agents'] });
66
+ navigate('/');
67
+ } catch (deleteError: any) {
68
+ responseReceived = true;
69
+
70
+ if (navigationTimer) {
71
+ clearTimeout(navigationTimer);
72
+ }
73
+
74
+ const errorMessage = deleteError?.message ?? 'Failed to delete agent';
75
+ const statusCode = deleteError?.statusCode || deleteError?.response?.status;
76
+
77
+ if (
78
+ statusCode === 409 ||
79
+ errorMessage.includes('409') ||
80
+ errorMessage.includes('Conflict') ||
81
+ errorMessage.includes('foreign key constraint') ||
82
+ errorMessage.includes('active references')
83
+ ) {
84
+ toast({
85
+ title: 'Cannot Delete',
86
+ description:
87
+ 'This agent cannot be deleted because it has active references. Try stopping the agent first.',
88
+ variant: 'destructive',
89
+ });
90
+ } else if (
91
+ statusCode === 408 ||
92
+ statusCode === 504 ||
93
+ errorMessage.includes('408') ||
94
+ errorMessage.includes('504') ||
95
+ errorMessage.includes('timeout') ||
96
+ errorMessage.includes('timed out')
97
+ ) {
98
+ toast({
99
+ title: 'Operation Timeout',
100
+ description:
101
+ 'The deletion is taking longer than expected and will continue in the background.',
102
+ variant: 'destructive',
103
+ });
104
+
105
+ queryClient.invalidateQueries({ queryKey: ['agents'] });
106
+ navigate('/');
107
+ } else {
108
+ toast({
109
+ title: 'Error',
110
+ description: errorMessage,
111
+ variant: 'destructive',
112
+ });
113
+ }
114
+ } finally {
115
+ setIsDeleting(false);
116
+ }
117
+ }, [targetAgentData, toast, navigate, queryClient]);
118
+
119
+ return {
120
+ handleDelete,
121
+ isDeleting,
122
+ };
123
+ }
@@ -0,0 +1,198 @@
1
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
2
+ import { createElizaClient } from '@/lib/api-client-config';
3
+ import { useToast } from '@/hooks/use-toast';
4
+
5
+ // Create ElizaClient instance
6
+ const elizaClient = createElizaClient();
7
+ import { type UUID, ChannelType } from '@elizaos/core';
8
+ import type { MessageChannel } from '@/types';
9
+ import { mapApiChannelToClient } from '@/lib/api-type-mappers';
10
+ import clientLogger from '@/lib/logger';
11
+ import { STALE_TIMES } from './use-query-hooks';
12
+ import { getEntityId } from '@/lib/utils';
13
+ import { useNavigate } from 'react-router-dom';
14
+
15
+ /**
16
+ * Hook to get or create a DM channel between current user and target user (agent)
17
+ * This is the original behavior, usually resulting in a single canonical DM channel.
18
+ */
19
+ export function useGetOrCreateDmChannel() {
20
+ const queryClient = useQueryClient();
21
+ const { toast } = useToast();
22
+ const currentUserId = getEntityId();
23
+
24
+ return useMutation({
25
+ mutationFn: async (targetUserId: UUID) => {
26
+ clientLogger.info(
27
+ '[useGetOrCreateDmChannel] Getting or creating canonical DM channel with target:',
28
+ targetUserId
29
+ );
30
+ const elizaClient = createElizaClient();
31
+ const result = await elizaClient.messaging.getOrCreateDmChannel({
32
+ participantIds: [currentUserId, targetUserId],
33
+ });
34
+ return result;
35
+ },
36
+ onSuccess: (data) => {
37
+ clientLogger.info('[useGetOrCreateDmChannel] Canonical DM channel created/found:', data);
38
+ queryClient.invalidateQueries({ queryKey: ['channels'] });
39
+ queryClient.invalidateQueries({ queryKey: ['dmChannels'] });
40
+ const agentId =
41
+ data.metadata?.user1 === currentUserId ? data.metadata?.user2 : data.metadata?.user1;
42
+ if (agentId) {
43
+ queryClient.invalidateQueries({ queryKey: ['dmChannels', agentId, currentUserId] });
44
+ }
45
+ },
46
+ onError: (error) => {
47
+ clientLogger.error(
48
+ '[useGetOrCreateDmChannel] Error creating/finding canonical DM channel:',
49
+ error
50
+ );
51
+ toast({
52
+ title: 'Error',
53
+ description: error instanceof Error ? error.message : 'Failed to process DM channel',
54
+ variant: 'destructive',
55
+ });
56
+ },
57
+ });
58
+ }
59
+
60
+ /**
61
+ * Hook to fetch all DM conversations (channels marked as DMs) for a specific agent.
62
+ */
63
+ export function useDmChannelsForAgent(
64
+ agentId: UUID | undefined,
65
+ serverId: UUID = '00000000-0000-0000-0000-000000000000' as UUID
66
+ ) {
67
+ const currentUserId = getEntityId();
68
+
69
+ return useQuery<MessageChannel[]>({
70
+ queryKey: ['dmChannels', agentId, currentUserId], // This key will be invalidated by useCreateDmChannel
71
+ queryFn: async () => {
72
+ if (!agentId) return [];
73
+ clientLogger.info(
74
+ '[useDmChannelsForAgent] Fetching distinct DM channels for agent:',
75
+ agentId
76
+ );
77
+
78
+ const elizaClient = createElizaClient();
79
+ const result = await elizaClient.messaging.getServerChannels(serverId);
80
+ const apiChannels = result.channels || [];
81
+
82
+ // Map API channels to client type
83
+ const allChannels = apiChannels.map(mapApiChannelToClient);
84
+
85
+ const dmChannels = allChannels.filter((channel) => {
86
+ const metadata = channel.metadata || {};
87
+ const isCorrectType = channel.type === ChannelType.DM;
88
+ const isMarkedAsDm = metadata.isDm === true;
89
+ // Ensure this DM context is specifically associated with the target agentId
90
+ const isForThisAgentContext = metadata.forAgent === agentId;
91
+
92
+ const isParticipant =
93
+ (metadata.user1 === currentUserId && metadata.user2 === agentId) ||
94
+ (metadata.user1 === agentId && metadata.user2 === currentUserId);
95
+
96
+ // Primary filter for new-style distinct DMs
97
+ if (isCorrectType && isMarkedAsDm && isForThisAgentContext && isParticipant) {
98
+ return true;
99
+ }
100
+
101
+ // Fallback for older, canonical DM channels that might be named DM-UserA-UserB
102
+ // This ensures existing single DMs are still listed if they haven't been migrated to new metadata.
103
+ // This part of the filter might become less relevant as new distinct DMs are created.
104
+ if (channel.type === ChannelType.DM && !metadata.isDm && !metadata.forAgent) {
105
+ const channelName = channel.name.toLowerCase();
106
+ // Check if name follows the old convention: DM-currentUserId-agentId or DM-agentId-currentUserId
107
+ const defaultDmName1 = `dm-${currentUserId}-${agentId}`.toLowerCase();
108
+ const defaultDmName2 = `dm-${agentId}-${currentUserId}`.toLowerCase();
109
+ if (channelName === defaultDmName1 || channelName === defaultDmName2) {
110
+ clientLogger.warn(
111
+ '[useDmChannelsForAgent] Matched a canonical DM channel by name convention:',
112
+ channel.id,
113
+ channel.name
114
+ );
115
+ return true;
116
+ }
117
+ }
118
+
119
+ return false;
120
+ });
121
+
122
+ clientLogger.info(
123
+ '[useDmChannelsForAgent] Found distinct DM channels:',
124
+ dmChannels.length,
125
+ dmChannels.map((c) => ({ id: c.id, name: c.name, metadata: c.metadata }))
126
+ );
127
+
128
+ return dmChannels.sort((a, b) => {
129
+ const aTime = new Date(a.updatedAt || a.createdAt).getTime();
130
+ const bTime = new Date(b.updatedAt || b.createdAt).getTime();
131
+ return bTime - aTime;
132
+ });
133
+ },
134
+ enabled: !!agentId,
135
+ staleTime: STALE_TIMES.FREQUENT, // More frequent stale time to catch new chats quickly
136
+ refetchInterval: STALE_TIMES.STANDARD, // Poll less aggressively, rely on invalidation primarily
137
+ });
138
+ }
139
+
140
+ /**
141
+ * Hook to create a new, distinct DM channel (conversation) with an agent.
142
+ */
143
+ export function useCreateDmChannel() {
144
+ const queryClient = useQueryClient();
145
+ const { toast } = useToast();
146
+ const currentUserId = getEntityId();
147
+ const navigate = useNavigate();
148
+
149
+ return useMutation({
150
+ mutationFn: async ({ agentId, channelName }: { agentId: UUID; channelName: string }) => {
151
+ clientLogger.info('[useCreateDmChannel] Creating new distinct DM channel with agent:', {
152
+ agentId,
153
+ channelName,
154
+ });
155
+
156
+ if (!channelName || !channelName.trim()) {
157
+ // This should ideally be caught before calling the mutation, but good to have a check.
158
+ throw new Error('Channel name cannot be empty for a new DM conversation.');
159
+ }
160
+
161
+ const elizaClient = createElizaClient();
162
+ const result = await elizaClient.messaging.createGroupChannel({
163
+ name: channelName.trim(),
164
+ participantIds: [currentUserId, agentId],
165
+ metadata: {
166
+ type: ChannelType.DM, // Set type to DM
167
+ server_id: '00000000-0000-0000-0000-000000000000' as UUID, // Use the default server
168
+ isDm: true, // Mark it as a DM type conversation
169
+ user1: currentUserId, // Explicitly store participants for filtering
170
+ user2: agentId,
171
+ forAgent: agentId, // Critical: associates this DM context with the specific agent
172
+ createdAt: new Date().toISOString(), // Add a creation timestamp in metadata
173
+ },
174
+ });
175
+
176
+ return result; // Direct result from ElizaClient
177
+ },
178
+ onSuccess: (data, variables) => {
179
+ clientLogger.info('[useCreateDmChannel] Distinct DM channel created successfully:', data);
180
+ toast({
181
+ title: 'New Chat Started',
182
+ description: `Conversation "${data.name}" created.`,
183
+ });
184
+ // Invalidate queries to refresh the DM channel list for this agent
185
+ queryClient.invalidateQueries({ queryKey: ['dmChannels', variables.agentId, currentUserId] });
186
+ // Also invalidate general channels list if it might show DMs (though less likely)
187
+ queryClient.invalidateQueries({ queryKey: ['channels'] });
188
+ },
189
+ onError: (error) => {
190
+ clientLogger.error('[useCreateDmChannel] Error creating distinct DM channel:', error);
191
+ toast({
192
+ title: 'Error Creating Chat',
193
+ description: error instanceof Error ? error.message : 'Could not start new chat.',
194
+ variant: 'destructive',
195
+ });
196
+ },
197
+ });
198
+ }
@@ -0,0 +1,83 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { useQuery } from '@tanstack/react-query';
3
+ import { elevenLabsVoiceModels } from '@/config/voice-models';
4
+ import type { VoiceModel } from '@/config/voice-models';
5
+
6
+ // TODO: Move this to a shared config file, or the 11labs plugin once plugin categories are implemented
7
+
8
+ interface ElevenLabsVoice {
9
+ voice_id: string;
10
+ name: string;
11
+ category: string;
12
+ labels?: {
13
+ accent?: string;
14
+ age?: string;
15
+ description?: string;
16
+ gender?: string;
17
+ use_case?: string;
18
+ };
19
+ preview_url?: string;
20
+ }
21
+
22
+ export function useElevenLabsVoices() {
23
+ const [apiKey, setApiKey] = useState<string | null>(null);
24
+
25
+ // Load API key from localStorage or another source
26
+ useEffect(() => {
27
+ const storedKey = localStorage.getItem('ELEVENLABS_API_KEY');
28
+ setApiKey(storedKey);
29
+ }, []);
30
+
31
+ return useQuery({
32
+ queryKey: ['elevenlabs-voices', apiKey],
33
+ queryFn: async () => {
34
+ // If no API key is available, return empty array (no custom voices)
35
+ if (!apiKey) {
36
+ return [];
37
+ }
38
+
39
+ try {
40
+ const response = await fetch('https://api.elevenlabs.io/v2/voices', {
41
+ method: 'GET',
42
+ headers: {
43
+ 'xi-api-key': apiKey,
44
+ },
45
+ });
46
+
47
+ if (!response.ok) {
48
+ console.error('Failed to fetch ElevenLabs voices:', response.statusText);
49
+ return [];
50
+ }
51
+
52
+ const data = await response.json();
53
+
54
+ // Get the IDs of the default voices we already have
55
+ const defaultVoiceIds = elevenLabsVoiceModels.map((v) => v.value);
56
+
57
+ // Filter to only include custom/cloned voices (those not in the default list)
58
+ const customVoices: ElevenLabsVoice[] = data.voices.filter(
59
+ (voice: ElevenLabsVoice) => !defaultVoiceIds.includes(voice.voice_id)
60
+ );
61
+
62
+ // Transform only the custom voices to match our VoiceModel format
63
+ const apiVoices: VoiceModel[] = customVoices.map((voice: ElevenLabsVoice) => ({
64
+ value: voice.voice_id,
65
+ label: `ElevenLabs - ${voice.name} (Custom)`,
66
+ provider: 'elevenlabs',
67
+ gender: voice.labels?.gender === 'female' ? 'female' : 'male',
68
+ language: 'en',
69
+ features: [voice.category || 'professional', voice.labels?.description || 'natural'],
70
+ }));
71
+
72
+ return apiVoices;
73
+ } catch (error) {
74
+ console.error('Error fetching ElevenLabs voices:', error);
75
+ return [];
76
+ }
77
+ },
78
+ // Refresh the data every hour
79
+ staleTime: 60 * 60 * 1000,
80
+ // Don't refetch on window focus
81
+ refetchOnWindowFocus: false,
82
+ });
83
+ }
@@ -0,0 +1,224 @@
1
+ import { useState, useRef, useEffect, useCallback } from 'react';
2
+ import { getContentTypeFromMimeType } from '@elizaos/core';
3
+ import { UUID, Media, ChannelType } from '@elizaos/core';
4
+ import { randomUUID } from '@/lib/utils';
5
+ import { createElizaClient } from '@/lib/api-client-config';
6
+ import { useToast } from '@/hooks/use-toast';
7
+ // Direct error handling
8
+ import clientLogger from '@/lib/logger';
9
+
10
+ export type UploadingFile = {
11
+ file: File;
12
+ id: string;
13
+ isUploading: boolean;
14
+ uploadProgress?: number;
15
+ error?: string;
16
+ blobUrl?: string;
17
+ };
18
+
19
+ interface UseFileUploadProps {
20
+ agentId?: UUID;
21
+ channelId?: UUID;
22
+ chatType: ChannelType.DM | ChannelType.GROUP;
23
+ }
24
+
25
+ export function useFileUpload({ agentId, channelId, chatType }: UseFileUploadProps) {
26
+ const [selectedFiles, setSelectedFiles] = useState<UploadingFile[]>([]);
27
+ const blobUrlsRef = useRef<Set<string>>(new Set());
28
+ const { toast } = useToast();
29
+ const elizaClient = createElizaClient();
30
+
31
+ // Cleanup blob URLs on unmount
32
+ useEffect(() => {
33
+ return () => {
34
+ blobUrlsRef.current.forEach((url) => URL.revokeObjectURL(url));
35
+ blobUrlsRef.current.clear();
36
+ };
37
+ }, []);
38
+
39
+ const handleFileChange = useCallback(
40
+ (e: React.ChangeEvent<HTMLInputElement>) => {
41
+ const files = Array.from(e.target.files || []);
42
+ const validFiles = files.filter(
43
+ (file) =>
44
+ file.type.startsWith('image/') ||
45
+ file.type.startsWith('video/') ||
46
+ file.type.startsWith('audio/') ||
47
+ file.type === 'application/pdf' ||
48
+ file.type === 'application/msword' ||
49
+ file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ||
50
+ file.type === 'application/vnd.ms-excel' ||
51
+ file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
52
+ file.type === 'application/vnd.ms-powerpoint' ||
53
+ file.type ===
54
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation' ||
55
+ file.type.startsWith('text/')
56
+ );
57
+
58
+ const uniqueFiles = validFiles.filter((newFile) => {
59
+ return !selectedFiles.some(
60
+ (existingFile) =>
61
+ existingFile.file.name === newFile.name &&
62
+ existingFile.file.size === newFile.size &&
63
+ existingFile.file.lastModified === newFile.lastModified
64
+ );
65
+ });
66
+
67
+ const newUploadingFiles: UploadingFile[] = uniqueFiles.map((file) => ({
68
+ file,
69
+ id: randomUUID(),
70
+ isUploading: false,
71
+ }));
72
+
73
+ setSelectedFiles((prev) => {
74
+ const combined = [...prev, ...newUploadingFiles];
75
+ return Array.from(
76
+ new Map(combined.map((f) => [`${f.file.name}-${f.file.size}`, f])).values()
77
+ );
78
+ });
79
+ if (e.target) e.target.value = '';
80
+ },
81
+ [selectedFiles]
82
+ );
83
+
84
+ const removeFile = useCallback((fileId: string) => {
85
+ setSelectedFiles((prev) => {
86
+ const file = prev.find((f) => f.id === fileId);
87
+ if (file?.blobUrl) {
88
+ URL.revokeObjectURL(file.blobUrl);
89
+ blobUrlsRef.current.delete(file.blobUrl);
90
+ }
91
+ return prev.filter((f) => f.id !== fileId);
92
+ });
93
+ }, []);
94
+
95
+ const createBlobUrls = useCallback((files: UploadingFile[]): Media[] => {
96
+ const attachmentBlobUrls: string[] = [];
97
+ const optimisticAttachments = files
98
+ .map((sf) => {
99
+ const blobUrl = URL.createObjectURL(sf.file);
100
+ blobUrlsRef.current.add(blobUrl);
101
+ attachmentBlobUrls.push(blobUrl);
102
+ sf.blobUrl = blobUrl;
103
+ return {
104
+ id: sf.id,
105
+ url: blobUrl,
106
+ title: sf.file.name,
107
+ contentType: getContentTypeFromMimeType(sf.file.type),
108
+ isUploading: true,
109
+ };
110
+ })
111
+ .filter((att) => att.contentType !== undefined) as Media[];
112
+
113
+ return optimisticAttachments;
114
+ }, []);
115
+
116
+ const uploadFiles = useCallback(
117
+ async (
118
+ files: UploadingFile[]
119
+ ): Promise<{
120
+ uploaded: Media[];
121
+ failed: Array<{ file: UploadingFile; error: string }>;
122
+ blobUrls: string[];
123
+ }> => {
124
+ if (!files.length) return { uploaded: [], failed: [], blobUrls: [] };
125
+
126
+ const uploadPromises = files.map(async (fileData) => {
127
+ try {
128
+ const uploadResult =
129
+ chatType === ChannelType.DM && agentId
130
+ ? await elizaClient.media.uploadAgentMedia(agentId, {
131
+ file: fileData.file,
132
+ filename: fileData.file.name,
133
+ })
134
+ : await elizaClient.media.uploadChannelMedia(channelId!, fileData.file);
135
+
136
+ return {
137
+ success: true,
138
+ media: {
139
+ id: fileData.id,
140
+ url: uploadResult.url,
141
+ title: fileData.file.name,
142
+ source: 'file_upload',
143
+ contentType: getContentTypeFromMimeType(fileData.file.type),
144
+ } as Media,
145
+ };
146
+ } catch (uploadError) {
147
+ clientLogger.error(`Failed to upload ${fileData.file.name}:`, uploadError);
148
+
149
+ // Direct error handling
150
+ toast({
151
+ title: `Upload Failed: ${fileData.file.name}`,
152
+ description: uploadError instanceof Error ? uploadError.message : 'Upload failed',
153
+ variant: 'destructive',
154
+ });
155
+
156
+ return {
157
+ success: false,
158
+ file: fileData,
159
+ error: uploadError instanceof Error ? uploadError.message : 'Upload failed',
160
+ };
161
+ }
162
+ });
163
+
164
+ const settledUploads = await Promise.allSettled(uploadPromises);
165
+ const uploaded: Media[] = [];
166
+ const failed: Array<{ file: UploadingFile; error: string }> = [];
167
+ const blobUrls: string[] = [];
168
+
169
+ settledUploads.forEach((result, index) => {
170
+ if (result.status === 'fulfilled') {
171
+ if (result.value.success && 'media' in result.value) {
172
+ uploaded.push(result.value.media as Media);
173
+ } else if ('file' in result.value) {
174
+ failed.push(result.value as { file: UploadingFile; error: string });
175
+ }
176
+ } else {
177
+ // Handle rejected promise
178
+ failed.push({
179
+ file: files[index],
180
+ error: result.reason?.message || 'Upload failed',
181
+ });
182
+ }
183
+ });
184
+
185
+ // Collect blob URLs for cleanup
186
+ files.forEach((f) => {
187
+ if (f.blobUrl) blobUrls.push(f.blobUrl);
188
+ });
189
+
190
+ return { uploaded, failed, blobUrls };
191
+ },
192
+ [chatType, agentId, channelId, toast]
193
+ );
194
+
195
+ const cleanupBlobUrls = useCallback((urls: string[]) => {
196
+ urls.forEach((url) => {
197
+ URL.revokeObjectURL(url);
198
+ blobUrlsRef.current.delete(url);
199
+ });
200
+ }, []);
201
+
202
+ const clearFiles = useCallback(() => {
203
+ // Cleanup all blob URLs
204
+ selectedFiles.forEach((file) => {
205
+ if (file.blobUrl) {
206
+ URL.revokeObjectURL(file.blobUrl);
207
+ blobUrlsRef.current.delete(file.blobUrl);
208
+ }
209
+ });
210
+ setSelectedFiles([]);
211
+ }, [selectedFiles]);
212
+
213
+ return {
214
+ selectedFiles,
215
+ setSelectedFiles,
216
+ handleFileChange,
217
+ removeFile,
218
+ createBlobUrls,
219
+ uploadFiles,
220
+ cleanupBlobUrls,
221
+ clearFiles,
222
+ getContentTypeFromMimeType,
223
+ };
224
+ }
@@ -0,0 +1,19 @@
1
+ import * as React from 'react';
2
+
3
+ const MOBILE_BREAKPOINT = 768;
4
+
5
+ export function useIsMobile() {
6
+ const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined);
7
+
8
+ React.useEffect(() => {
9
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
10
+ const onChange = () => {
11
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
12
+ };
13
+ mql.addEventListener('change', onChange);
14
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
15
+ return () => mql.removeEventListener('change', onChange);
16
+ }, []);
17
+
18
+ return !!isMobile;
19
+ }
@@ -0,0 +1,49 @@
1
+ import { useState, useEffect } from 'react';
2
+ import clientLogger from '../lib/logger';
3
+
4
+ // Key for storing onboarding state in localStorage
5
+ const ONBOARDING_COMPLETED_KEY = 'eliza-onboarding-completed';
6
+
7
+ /**
8
+ * Custom hook to manage the onboarding state
9
+ * Tracks whether the user has completed the onboarding tour
10
+ */
11
+ export const useOnboarding = () => {
12
+ // Check if user has completed the onboarding
13
+ const [onboardingCompleted, setOnboardingCompleted] = useState<boolean>(() => {
14
+ try {
15
+ // Try to get the stored value from localStorage
16
+ const storedValue = localStorage.getItem(ONBOARDING_COMPLETED_KEY);
17
+ return storedValue === 'true';
18
+ } catch (error) {
19
+ // If there's an error (e.g. localStorage not available), assume not completed
20
+ clientLogger.error('Error accessing localStorage:', error);
21
+ return false;
22
+ }
23
+ });
24
+
25
+ // Update localStorage when onboardingCompleted changes
26
+ useEffect(() => {
27
+ try {
28
+ localStorage.setItem(ONBOARDING_COMPLETED_KEY, onboardingCompleted.toString());
29
+ } catch (error) {
30
+ clientLogger.error('Error writing to localStorage:', error);
31
+ }
32
+ }, [onboardingCompleted]);
33
+
34
+ // Function to mark onboarding as completed
35
+ const completeOnboarding = () => {
36
+ setOnboardingCompleted(true);
37
+ };
38
+
39
+ // Function to reset onboarding state (for testing)
40
+ const resetOnboarding = () => {
41
+ setOnboardingCompleted(false);
42
+ };
43
+
44
+ return {
45
+ onboardingCompleted,
46
+ completeOnboarding,
47
+ resetOnboarding,
48
+ };
49
+ };