@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.
- package/LICENSE +21 -0
- package/README.md +350 -0
- package/dist/assets/empty-module-CLMscLYw.js +1 -0
- package/dist/assets/main-BBZ_3lkn.css +5999 -0
- package/dist/assets/main-C5zNUkXH.js +7 -0
- package/dist/assets/main-Dz64ENQg.js +614 -0
- package/dist/assets/react-vendor-DM5m98rr.js +545 -0
- package/dist/assets/ui-vendor-BQCqNqg0.js +1 -0
- package/dist/elizaos-avatar.png +0 -0
- package/dist/elizaos-icon.png +0 -0
- package/dist/elizaos-logo-light.png +0 -0
- package/dist/elizaos.webp +0 -0
- package/dist/favicon.ico +0 -0
- package/dist/images/agents/agent1.png +0 -0
- package/dist/images/agents/agent2.png +0 -0
- package/dist/images/agents/agent3.png +0 -0
- package/dist/images/agents/agent4.png +0 -0
- package/dist/images/agents/agent5.png +0 -0
- package/dist/index.html +14 -0
- package/index.html +24 -0
- package/package.json +159 -0
- package/postcss.config.js +3 -0
- package/public/elizaos-avatar.png +0 -0
- package/public/elizaos-icon.png +0 -0
- package/public/elizaos-logo-light.png +0 -0
- package/public/elizaos.webp +0 -0
- package/public/favicon.ico +0 -0
- package/public/images/agents/agent1.png +0 -0
- package/public/images/agents/agent2.png +0 -0
- package/public/images/agents/agent3.png +0 -0
- package/public/images/agents/agent4.png +0 -0
- package/public/images/agents/agent5.png +0 -0
- package/src/App.tsx +222 -0
- package/src/components/AgentDetailsPanel.tsx +147 -0
- package/src/components/ChatInputArea.tsx +196 -0
- package/src/components/ChatMessageListComponent.tsx +139 -0
- package/src/components/actionTool.tsx +186 -0
- package/src/components/add-agent-card.tsx +77 -0
- package/src/components/agent-action-viewer.tsx +816 -0
- package/src/components/agent-avatar-stack.tsx +121 -0
- package/src/components/agent-card.cy.tsx +259 -0
- package/src/components/agent-card.tsx +177 -0
- package/src/components/agent-creator.tsx +142 -0
- package/src/components/agent-log-viewer.tsx +645 -0
- package/src/components/agent-memory-edit-overlay.tsx +461 -0
- package/src/components/agent-memory-viewer.tsx +504 -0
- package/src/components/agent-settings.tsx +270 -0
- package/src/components/agent-sidebar.tsx +178 -0
- package/src/components/api-key-dialog.tsx +113 -0
- package/src/components/app-sidebar.tsx +685 -0
- package/src/components/array-input.tsx +116 -0
- package/src/components/audio-recorder.tsx +292 -0
- package/src/components/avatar-panel.tsx +141 -0
- package/src/components/character-form.tsx +1138 -0
- package/src/components/chat.tsx +1813 -0
- package/src/components/combobox.tsx +187 -0
- package/src/components/confirmation-dialog.tsx +59 -0
- package/src/components/connection-error-banner.tsx +101 -0
- package/src/components/connection-status.cy.tsx +73 -0
- package/src/components/connection-status.tsx +155 -0
- package/src/components/copy-button.tsx +35 -0
- package/src/components/delete-button.tsx +24 -0
- package/src/components/env-settings.tsx +261 -0
- package/src/components/group-card.tsx +160 -0
- package/src/components/group-panel.tsx +543 -0
- package/src/components/input-copy.tsx +21 -0
- package/src/components/logs-page.tsx +41 -0
- package/src/components/media-content.tsx +385 -0
- package/src/components/memory-graph.tsx +170 -0
- package/src/components/missing-secrets-dialog.tsx +72 -0
- package/src/components/onboarding-tour.tsx +247 -0
- package/src/components/page-title.tsx +8 -0
- package/src/components/plugins-panel.tsx +383 -0
- package/src/components/profile-card.tsx +66 -0
- package/src/components/profile-overlay.tsx +283 -0
- package/src/components/retry-button.tsx +28 -0
- package/src/components/secret-panel.tsx +1505 -0
- package/src/components/server-management.tsx +264 -0
- package/src/components/split-button.tsx +148 -0
- package/src/components/stop-agent-button.tsx +99 -0
- package/src/components/ui/alert-dialog.cy.tsx +333 -0
- package/src/components/ui/alert-dialog.tsx +115 -0
- package/src/components/ui/alert.tsx +49 -0
- package/src/components/ui/avatar.cy.tsx +180 -0
- package/src/components/ui/avatar.tsx +57 -0
- package/src/components/ui/badge.cy.tsx +146 -0
- package/src/components/ui/badge.tsx +43 -0
- package/src/components/ui/button.cy.tsx +177 -0
- package/src/components/ui/button.tsx +56 -0
- package/src/components/ui/card.cy.tsx +160 -0
- package/src/components/ui/card.tsx +73 -0
- package/src/components/ui/chat/animated-markdown.tsx +59 -0
- package/src/components/ui/chat/chat-bubble.tsx +178 -0
- package/src/components/ui/chat/chat-container.tsx +51 -0
- package/src/components/ui/chat/chat-input.cy.tsx +169 -0
- package/src/components/ui/chat/chat-input.tsx +47 -0
- package/src/components/ui/chat/chat-message-list.tsx +61 -0
- package/src/components/ui/chat/chat-tts-button.tsx +199 -0
- package/src/components/ui/chat/code-block.tsx +79 -0
- package/src/components/ui/chat/expandable-chat.tsx +131 -0
- package/src/components/ui/chat/hooks/useAutoScroll.ts +86 -0
- package/src/components/ui/chat/markdown.tsx +209 -0
- package/src/components/ui/chat/message-loading.tsx +48 -0
- package/src/components/ui/checkbox.cy.tsx +170 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/collapsible.cy.tsx +283 -0
- package/src/components/ui/collapsible.tsx +9 -0
- package/src/components/ui/command.cy.tsx +313 -0
- package/src/components/ui/command.tsx +143 -0
- package/src/components/ui/dialog.cy.tsx +279 -0
- package/src/components/ui/dialog.tsx +104 -0
- package/src/components/ui/dropdown-menu.cy.tsx +273 -0
- package/src/components/ui/dropdown-menu.tsx +281 -0
- package/src/components/ui/input.cy.tsx +82 -0
- package/src/components/ui/input.tsx +27 -0
- package/src/components/ui/label.cy.tsx +157 -0
- package/src/components/ui/label.tsx +19 -0
- package/src/components/ui/resizable.tsx +42 -0
- package/src/components/ui/scroll-area.cy.tsx +242 -0
- package/src/components/ui/scroll-area.tsx +46 -0
- package/src/components/ui/select.cy.tsx +277 -0
- package/src/components/ui/select.tsx +155 -0
- package/src/components/ui/separator.cy.tsx +145 -0
- package/src/components/ui/separator.tsx +29 -0
- package/src/components/ui/sheet.cy.tsx +324 -0
- package/src/components/ui/sheet.tsx +119 -0
- package/src/components/ui/sidebar.tsx +734 -0
- package/src/components/ui/skeleton.cy.tsx +149 -0
- package/src/components/ui/skeleton.tsx +17 -0
- package/src/components/ui/split-button.cy.tsx +274 -0
- package/src/components/ui/split-button.tsx +112 -0
- package/src/components/ui/switch.tsx +28 -0
- package/src/components/ui/tabs.cy.tsx +271 -0
- package/src/components/ui/tabs.tsx +53 -0
- package/src/components/ui/textarea.cy.tsx +136 -0
- package/src/components/ui/textarea.tsx +26 -0
- package/src/components/ui/toast.cy.tsx +209 -0
- package/src/components/ui/toast.tsx +126 -0
- package/src/components/ui/toaster.tsx +29 -0
- package/src/components/ui/tooltip.cy.tsx +244 -0
- package/src/components/ui/tooltip.tsx +30 -0
- package/src/config/agent-templates.ts +349 -0
- package/src/config/voice-models.ts +181 -0
- package/src/constants.ts +23 -0
- package/src/context/AuthContext.tsx +44 -0
- package/src/context/ConnectionContext.tsx +194 -0
- package/src/entry.tsx +9 -0
- package/src/hooks/__tests__/use-agent-tab-state.test.ts +137 -0
- package/src/hooks/__tests__/use-agent-update.test.tsx +250 -0
- package/src/hooks/__tests__/use-character-convert.test.ts +102 -0
- package/src/hooks/__tests__/use-panel-width-state.test.ts +243 -0
- package/src/hooks/__tests__/use-sidebar-state.test.ts +117 -0
- package/src/hooks/use-agent-management.ts +130 -0
- package/src/hooks/use-agent-tab-state.ts +74 -0
- package/src/hooks/use-agent-update.ts +469 -0
- package/src/hooks/use-character-convert.ts +138 -0
- package/src/hooks/use-confirmation.ts +55 -0
- package/src/hooks/use-delete-agent.ts +123 -0
- package/src/hooks/use-dm-channels.ts +198 -0
- package/src/hooks/use-elevenlabs-voices.ts +83 -0
- package/src/hooks/use-file-upload.ts +224 -0
- package/src/hooks/use-mobile.tsx +19 -0
- package/src/hooks/use-onboarding.tsx +49 -0
- package/src/hooks/use-panel-width-state.ts +147 -0
- package/src/hooks/use-partial-update.ts +288 -0
- package/src/hooks/use-plugin-details.ts +462 -0
- package/src/hooks/use-plugins.ts +119 -0
- package/src/hooks/use-query-hooks.ts +1263 -0
- package/src/hooks/use-server-agents.ts +62 -0
- package/src/hooks/use-server-version.tsx +47 -0
- package/src/hooks/use-sidebar-state.ts +50 -0
- package/src/hooks/use-socket-chat.ts +264 -0
- package/src/hooks/use-toast.ts +260 -0
- package/src/hooks/use-version.tsx +64 -0
- package/src/index.css +146 -0
- package/src/lib/api-client-config.ts +53 -0
- package/src/lib/api-type-mappers.ts +196 -0
- package/src/lib/export-utils.ts +123 -0
- package/src/lib/logger.ts +19 -0
- package/src/lib/media-utils.ts +170 -0
- package/src/lib/pca.test.ts +17 -0
- package/src/lib/pca.ts +52 -0
- package/src/lib/socketio-manager.ts +664 -0
- package/src/lib/utils.ts +168 -0
- package/src/main.tsx +16 -0
- package/src/mocks/empty-module.ts +12 -0
- package/src/mocks/node-module.ts +57 -0
- package/src/polyfills.ts +37 -0
- package/src/routes/agent-detail.tsx +30 -0
- package/src/routes/agent-list.tsx +27 -0
- package/src/routes/agent-settings.tsx +48 -0
- package/src/routes/character-detail.tsx +52 -0
- package/src/routes/character-form.tsx +79 -0
- package/src/routes/character-list.tsx +38 -0
- package/src/routes/chat.tsx +128 -0
- package/src/routes/createAgent.tsx +13 -0
- package/src/routes/group-new.tsx +50 -0
- package/src/routes/group.tsx +29 -0
- package/src/routes/home.tsx +218 -0
- package/src/routes/not-found.tsx +71 -0
- package/src/test/setup.ts +154 -0
- package/src/types/crypto-browserify.d.ts +4 -0
- package/src/types/index.ts +13 -0
- package/src/types/rooms.ts +8 -0
- package/src/types.ts +84 -0
- package/src/vite-env.d.ts +40 -0
- package/tailwind.config.ts +90 -0
- package/tsconfig.json +10 -0
- 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
|
+
};
|