@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,1813 @@
|
|
|
1
|
+
import CopyButton from '@/components/copy-button';
|
|
2
|
+
import DeleteButton from '@/components/delete-button';
|
|
3
|
+
import RetryButton from '@/components/retry-button';
|
|
4
|
+
import MediaContent from '@/components/media-content';
|
|
5
|
+
import ProfileOverlay from '@/components/profile-overlay';
|
|
6
|
+
import { Avatar, AvatarImage } from '@/components/ui/avatar';
|
|
7
|
+
import { Badge } from '@/components/ui/badge';
|
|
8
|
+
import { Button } from '@/components/ui/button';
|
|
9
|
+
import ConfirmationDialog from '@/components/confirmation-dialog';
|
|
10
|
+
import { useConfirmation } from '@/hooks/use-confirmation';
|
|
11
|
+
import { ChatBubbleMessage, ChatBubbleTimestamp } from '@/components/ui/chat/chat-bubble';
|
|
12
|
+
import ChatTtsButton from '@/components/ui/chat/chat-tts-button';
|
|
13
|
+
import { Markdown } from '@/components/ui/chat/markdown';
|
|
14
|
+
import { AnimatedMarkdown } from '@/components/ui/chat/animated-markdown';
|
|
15
|
+
import { useAutoScroll } from '@/components/ui/chat/hooks/useAutoScroll';
|
|
16
|
+
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable';
|
|
17
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
|
18
|
+
import { SplitButton } from '@/components/ui/split-button';
|
|
19
|
+
import { Tool, type ToolPart } from '@/components/actionTool';
|
|
20
|
+
import { CHAT_SOURCE, GROUP_CHAT_SOURCE, USER_NAME } from '@/constants';
|
|
21
|
+
import { useFileUpload } from '@/hooks/use-file-upload';
|
|
22
|
+
import {
|
|
23
|
+
useAgent,
|
|
24
|
+
useAgentsWithDetails,
|
|
25
|
+
useChannelDetails,
|
|
26
|
+
useChannelMessages,
|
|
27
|
+
useChannelParticipants,
|
|
28
|
+
useClearChannelMessages,
|
|
29
|
+
useDeleteChannelMessage,
|
|
30
|
+
type UiMessage,
|
|
31
|
+
} from '@/hooks/use-query-hooks';
|
|
32
|
+
import { useSocketChat } from '@/hooks/use-socket-chat';
|
|
33
|
+
import { useToast } from '@/hooks/use-toast';
|
|
34
|
+
import { createElizaClient } from '@/lib/api-client-config';
|
|
35
|
+
import clientLogger from '@/lib/logger';
|
|
36
|
+
import { parseMediaFromText, removeMediaUrlsFromText, type MediaInfo } from '@/lib/media-utils';
|
|
37
|
+
import {
|
|
38
|
+
cn,
|
|
39
|
+
generateGroupName,
|
|
40
|
+
getAgentAvatar,
|
|
41
|
+
getEntityId,
|
|
42
|
+
moment,
|
|
43
|
+
randomUUID,
|
|
44
|
+
} from '@/lib/utils';
|
|
45
|
+
import type { Agent, Media, UUID } from '@elizaos/core';
|
|
46
|
+
import {
|
|
47
|
+
AgentStatus,
|
|
48
|
+
ChannelType,
|
|
49
|
+
ContentType as CoreContentType,
|
|
50
|
+
validateUuid,
|
|
51
|
+
} from '@elizaos/core';
|
|
52
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
53
|
+
import {
|
|
54
|
+
Trash,
|
|
55
|
+
StopCircle,
|
|
56
|
+
ArrowUpFromLine,
|
|
57
|
+
Edit,
|
|
58
|
+
Eraser,
|
|
59
|
+
Clock,
|
|
60
|
+
ChevronDown,
|
|
61
|
+
Loader2,
|
|
62
|
+
PanelRight,
|
|
63
|
+
PanelRightClose,
|
|
64
|
+
Plus,
|
|
65
|
+
Trash2,
|
|
66
|
+
} from 'lucide-react';
|
|
67
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
68
|
+
import { AgentSidebar } from './agent-sidebar';
|
|
69
|
+
import { ChatInputArea } from './ChatInputArea';
|
|
70
|
+
import { ChatMessageListComponent } from './ChatMessageListComponent';
|
|
71
|
+
import GroupPanel from './group-panel';
|
|
72
|
+
|
|
73
|
+
import {
|
|
74
|
+
DropdownMenu,
|
|
75
|
+
DropdownMenuContent,
|
|
76
|
+
DropdownMenuItem,
|
|
77
|
+
DropdownMenuLabel,
|
|
78
|
+
DropdownMenuSeparator,
|
|
79
|
+
DropdownMenuTrigger,
|
|
80
|
+
} from '@/components/ui/dropdown-menu';
|
|
81
|
+
import { useCreateDmChannel, useDmChannelsForAgent } from '@/hooks/use-dm-channels';
|
|
82
|
+
import { useSidebarState } from '@/hooks/use-sidebar-state';
|
|
83
|
+
import { usePanelWidthState } from '@/hooks/use-panel-width-state';
|
|
84
|
+
import relativeTime from 'dayjs/plugin/relativeTime';
|
|
85
|
+
import type { MessageChannel } from '@/types';
|
|
86
|
+
import { useLocation, useNavigate } from 'react-router-dom';
|
|
87
|
+
import { useAgentManagement } from '@/hooks/use-agent-management';
|
|
88
|
+
import { useDeleteAgent } from '@/hooks/use-delete-agent';
|
|
89
|
+
import { exportCharacterAsJson } from '@/lib/export-utils';
|
|
90
|
+
moment.extend(relativeTime);
|
|
91
|
+
|
|
92
|
+
const DEFAULT_SERVER_ID = '00000000-0000-0000-0000-000000000000' as UUID;
|
|
93
|
+
|
|
94
|
+
// Helper function to convert action message to ToolPart format
|
|
95
|
+
const convertActionMessageToToolPart = (message: UiMessage): ToolPart => {
|
|
96
|
+
const rawMessage = message.rawMessage as any; // Type assertion to access raw message properties
|
|
97
|
+
|
|
98
|
+
// Map actionStatus to ToolPart state
|
|
99
|
+
const mapActionStatusToState = (status: string): ToolPart['state'] => {
|
|
100
|
+
switch (status) {
|
|
101
|
+
case 'pending':
|
|
102
|
+
case 'executing':
|
|
103
|
+
case 'running':
|
|
104
|
+
return 'input-streaming';
|
|
105
|
+
case 'completed':
|
|
106
|
+
case 'success':
|
|
107
|
+
return 'output-available';
|
|
108
|
+
case 'failed':
|
|
109
|
+
case 'error':
|
|
110
|
+
return 'output-error';
|
|
111
|
+
default:
|
|
112
|
+
return 'input-available';
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// Get the primary action name (first action or fallback to message type)
|
|
117
|
+
const actionName = rawMessage.actions?.[0] || rawMessage.action || 'ACTION';
|
|
118
|
+
const actionStatus = rawMessage.actionStatus || 'completed';
|
|
119
|
+
const actionId = rawMessage.actionId;
|
|
120
|
+
|
|
121
|
+
// Create input data from available action properties
|
|
122
|
+
const inputData: Record<string, unknown> = {};
|
|
123
|
+
if (rawMessage.actions) inputData.actions = rawMessage.actions;
|
|
124
|
+
if (rawMessage.action) inputData.action = rawMessage.action;
|
|
125
|
+
if (rawMessage.thought) inputData.thought = rawMessage.thought;
|
|
126
|
+
|
|
127
|
+
// Create output data based on status and content
|
|
128
|
+
const outputData: Record<string, unknown> = {};
|
|
129
|
+
if (rawMessage.text) outputData.result = rawMessage.text;
|
|
130
|
+
if (actionStatus) outputData.status = actionStatus;
|
|
131
|
+
if (rawMessage.thought) outputData.thought = rawMessage.thought;
|
|
132
|
+
if (rawMessage.actionResult) outputData.actionResult = rawMessage.actionResult;
|
|
133
|
+
|
|
134
|
+
// Handle error cases
|
|
135
|
+
const isError = actionStatus === 'failed' || actionStatus === 'error';
|
|
136
|
+
const errorText = isError ? rawMessage.text || 'Action failed' : undefined;
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
type: actionName,
|
|
140
|
+
state: mapActionStatusToState(actionStatus),
|
|
141
|
+
toolCallId: actionId,
|
|
142
|
+
input: Object.keys(inputData).length > 0 ? inputData : undefined,
|
|
143
|
+
output: Object.keys(outputData).length > 0 ? outputData : undefined,
|
|
144
|
+
errorText,
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
interface UnifiedChatViewProps {
|
|
149
|
+
chatType: ChannelType.DM | ChannelType.GROUP;
|
|
150
|
+
contextId: UUID; // agentId for DM, channelId for GROUP
|
|
151
|
+
serverId?: UUID; // Required for GROUP, optional for DM
|
|
152
|
+
initialDmChannelId?: UUID; // New prop for specific DM channel from URL
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Consolidated chat state type
|
|
156
|
+
interface ChatUIState {
|
|
157
|
+
showGroupEditPanel: boolean;
|
|
158
|
+
showProfileOverlay: boolean;
|
|
159
|
+
input: string;
|
|
160
|
+
inputDisabled: boolean;
|
|
161
|
+
selectedGroupAgentId: UUID | null;
|
|
162
|
+
currentDmChannelId: UUID | null;
|
|
163
|
+
isCreatingDM: boolean;
|
|
164
|
+
isMobile: boolean; // Add mobile state
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface ChatLocationState {
|
|
168
|
+
forceNew?: boolean;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Message content component - exported for use in ChatMessageListComponent
|
|
172
|
+
export const MemoizedMessageContent = React.memo(MessageContent, (prevProps, nextProps) => {
|
|
173
|
+
// Only re-render if the message content, animation state, or other key props change
|
|
174
|
+
return (
|
|
175
|
+
prevProps.message.id === nextProps.message.id &&
|
|
176
|
+
prevProps.message.text === nextProps.message.text &&
|
|
177
|
+
prevProps.message.isLoading === nextProps.message.isLoading &&
|
|
178
|
+
prevProps.shouldAnimate === nextProps.shouldAnimate &&
|
|
179
|
+
prevProps.isUser === nextProps.isUser
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
export function MessageContent({
|
|
184
|
+
message,
|
|
185
|
+
agentForTts,
|
|
186
|
+
shouldAnimate,
|
|
187
|
+
onDelete,
|
|
188
|
+
onRetry,
|
|
189
|
+
isUser,
|
|
190
|
+
getAgentInMessage,
|
|
191
|
+
agentAvatarMap,
|
|
192
|
+
chatType,
|
|
193
|
+
}: {
|
|
194
|
+
message: UiMessage;
|
|
195
|
+
agentForTts?: Agent | Partial<Agent> | null;
|
|
196
|
+
shouldAnimate?: boolean;
|
|
197
|
+
onDelete: (id: string) => void;
|
|
198
|
+
onRetry?: (messageText: string) => void;
|
|
199
|
+
isUser: boolean;
|
|
200
|
+
getAgentInMessage?: (agentId: UUID) => Partial<Agent> | undefined;
|
|
201
|
+
agentAvatarMap?: Record<UUID, string | null>;
|
|
202
|
+
chatType?: ChannelType;
|
|
203
|
+
}) {
|
|
204
|
+
const isActionMessage = message.type === 'agent_action' || message.source === 'agent_action';
|
|
205
|
+
return (
|
|
206
|
+
<div className="flex flex-col w-full">
|
|
207
|
+
<ChatBubbleMessage
|
|
208
|
+
isLoading={message.isLoading}
|
|
209
|
+
{...(isUser ? { variant: 'sent' } : {})}
|
|
210
|
+
{...(!message.text && !message.attachments?.length ? { className: 'bg-transparent' } : {})}
|
|
211
|
+
>
|
|
212
|
+
{isActionMessage ? (
|
|
213
|
+
<Tool
|
|
214
|
+
toolPart={convertActionMessageToToolPart(message)}
|
|
215
|
+
defaultOpen={false}
|
|
216
|
+
className="max-w-none"
|
|
217
|
+
/>
|
|
218
|
+
) : (
|
|
219
|
+
<div>
|
|
220
|
+
{(() => {
|
|
221
|
+
if (!message.text) return null;
|
|
222
|
+
|
|
223
|
+
const mediaInfos = parseMediaFromText(message.text);
|
|
224
|
+
const attachmentUrls = new Set(
|
|
225
|
+
message.attachments?.map((att) => att.url).filter(Boolean) || []
|
|
226
|
+
);
|
|
227
|
+
const uniqueMediaInfos = mediaInfos.filter((media) => !attachmentUrls.has(media.url));
|
|
228
|
+
const textWithoutUrls = removeMediaUrlsFromText(message.text, mediaInfos);
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<div className="space-y-3">
|
|
232
|
+
{textWithoutUrls.trim() && (
|
|
233
|
+
<div>
|
|
234
|
+
{isUser ? (
|
|
235
|
+
<Markdown className="prose-sm max-w-none" variant="user">
|
|
236
|
+
{textWithoutUrls}
|
|
237
|
+
</Markdown>
|
|
238
|
+
) : (
|
|
239
|
+
<AnimatedMarkdown
|
|
240
|
+
className="prose-sm max-w-none"
|
|
241
|
+
variant="agent"
|
|
242
|
+
shouldAnimate={shouldAnimate}
|
|
243
|
+
messageId={message.id}
|
|
244
|
+
>
|
|
245
|
+
{textWithoutUrls}
|
|
246
|
+
</AnimatedMarkdown>
|
|
247
|
+
)}
|
|
248
|
+
</div>
|
|
249
|
+
)}
|
|
250
|
+
|
|
251
|
+
{uniqueMediaInfos.length > 0 && (
|
|
252
|
+
<div className="space-y-2">
|
|
253
|
+
{uniqueMediaInfos.map((media, index) => (
|
|
254
|
+
<div key={`${media.url}-${index}`}>
|
|
255
|
+
<MediaContent url={media.url} title="Shared media" />
|
|
256
|
+
</div>
|
|
257
|
+
))}
|
|
258
|
+
</div>
|
|
259
|
+
)}
|
|
260
|
+
</div>
|
|
261
|
+
);
|
|
262
|
+
})()}
|
|
263
|
+
</div>
|
|
264
|
+
)}
|
|
265
|
+
|
|
266
|
+
{message.attachments
|
|
267
|
+
?.filter((attachment) => attachment.url && attachment.url.trim() !== '')
|
|
268
|
+
.map((attachment: Media) => (
|
|
269
|
+
<MediaContent
|
|
270
|
+
key={`${attachment.url}-${attachment.title}`}
|
|
271
|
+
url={attachment.url}
|
|
272
|
+
title={attachment.title || 'Attachment'}
|
|
273
|
+
/>
|
|
274
|
+
))}
|
|
275
|
+
</ChatBubbleMessage>
|
|
276
|
+
|
|
277
|
+
<div className="flex items-center justify-between w-full py-1">
|
|
278
|
+
<div>
|
|
279
|
+
{!isUser && (message.text || message.attachments?.length) && message.createdAt && (
|
|
280
|
+
<ChatBubbleTimestamp
|
|
281
|
+
className="text-muted-foreground"
|
|
282
|
+
timestamp={moment(message.createdAt).format('LT')}
|
|
283
|
+
/>
|
|
284
|
+
)}
|
|
285
|
+
</div>
|
|
286
|
+
<div
|
|
287
|
+
className={`flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity duration-500`}
|
|
288
|
+
>
|
|
289
|
+
{!isUser && message.text && !message.isLoading && agentForTts?.id && (
|
|
290
|
+
<>
|
|
291
|
+
<CopyButton text={message.text} />
|
|
292
|
+
<ChatTtsButton agentId={agentForTts.id} text={message.text} />
|
|
293
|
+
</>
|
|
294
|
+
)}
|
|
295
|
+
{isUser && message.text && !message.isLoading && onRetry && (
|
|
296
|
+
<RetryButton onClick={() => onRetry(message.text || '')} />
|
|
297
|
+
)}
|
|
298
|
+
<DeleteButton onClick={() => onDelete(message.id as string)} />
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export default function Chat({
|
|
306
|
+
chatType,
|
|
307
|
+
contextId,
|
|
308
|
+
serverId,
|
|
309
|
+
initialDmChannelId,
|
|
310
|
+
}: UnifiedChatViewProps) {
|
|
311
|
+
const { toast } = useToast();
|
|
312
|
+
const queryClient = useQueryClient();
|
|
313
|
+
|
|
314
|
+
// Use persistent sidebar state
|
|
315
|
+
const { isVisible: showSidebar, setSidebarVisible, toggleSidebar } = useSidebarState();
|
|
316
|
+
const {
|
|
317
|
+
mainPanelSize,
|
|
318
|
+
sidebarPanelSize,
|
|
319
|
+
isFloatingMode: isFloatingModeFromWidth,
|
|
320
|
+
setMainPanelSize,
|
|
321
|
+
setSidebarPanelSize,
|
|
322
|
+
} = usePanelWidthState();
|
|
323
|
+
|
|
324
|
+
// Consolidate all chat UI state into a single object (excluding showSidebar which is now managed separately)
|
|
325
|
+
const [chatState, setChatState] = useState<ChatUIState>({
|
|
326
|
+
showGroupEditPanel: false,
|
|
327
|
+
showProfileOverlay: false,
|
|
328
|
+
input: '',
|
|
329
|
+
inputDisabled: false,
|
|
330
|
+
selectedGroupAgentId: null,
|
|
331
|
+
currentDmChannelId: null,
|
|
332
|
+
isCreatingDM: false,
|
|
333
|
+
isMobile: false,
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const location = useLocation();
|
|
337
|
+
const state = location.state as ChatLocationState | null;
|
|
338
|
+
const forceNew = state?.forceNew || false;
|
|
339
|
+
|
|
340
|
+
const navigate = useNavigate();
|
|
341
|
+
|
|
342
|
+
const [shouldForceNew, setShouldForceNew] = useState(forceNew);
|
|
343
|
+
|
|
344
|
+
// Determine if we should use floating mode - either from width detection OR mobile
|
|
345
|
+
const isFloatingMode = isFloatingModeFromWidth || chatState.isMobile;
|
|
346
|
+
|
|
347
|
+
// Confirmation dialogs
|
|
348
|
+
const { confirm, isOpen, onOpenChange, onConfirm, options } = useConfirmation();
|
|
349
|
+
|
|
350
|
+
const { stopAgent, isAgentStopping } = useAgentManagement();
|
|
351
|
+
|
|
352
|
+
// Helper to update chat state
|
|
353
|
+
const updateChatState = useCallback((updates: Partial<ChatUIState>) => {
|
|
354
|
+
setChatState((prev) => ({ ...prev, ...updates }));
|
|
355
|
+
}, []);
|
|
356
|
+
|
|
357
|
+
const currentClientEntityId = getEntityId();
|
|
358
|
+
|
|
359
|
+
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
360
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
361
|
+
const formRef = useRef<HTMLFormElement>(null);
|
|
362
|
+
const inputDisabledRef = useRef<boolean>(false);
|
|
363
|
+
const chatTitleRef = useRef<string>('');
|
|
364
|
+
|
|
365
|
+
// For DM, we need agent data. For GROUP, we need channel data
|
|
366
|
+
const { data: agentDataResponse, isLoading: isLoadingAgent } = useAgent(
|
|
367
|
+
chatType === ChannelType.DM ? contextId : undefined,
|
|
368
|
+
{ enabled: chatType === ChannelType.DM }
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
// Convert AgentWithStatus to Agent, ensuring required fields have defaults
|
|
372
|
+
const targetAgentData: Agent | undefined = agentDataResponse?.data
|
|
373
|
+
? ({
|
|
374
|
+
...agentDataResponse.data,
|
|
375
|
+
createdAt: agentDataResponse.data.createdAt || Date.now(),
|
|
376
|
+
updatedAt: agentDataResponse.data.updatedAt || Date.now(),
|
|
377
|
+
} as Agent)
|
|
378
|
+
: undefined;
|
|
379
|
+
|
|
380
|
+
const { handleDelete: handleDeleteAgent, isDeleting: isDeletingAgent } = useDeleteAgent(
|
|
381
|
+
targetAgentData || ({} as Agent) // Provide safe default if undefined
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
// Use the new hooks for DM channel management
|
|
385
|
+
const { data: agentDmChannels = [], isLoading: isLoadingAgentDmChannels } = useDmChannelsForAgent(
|
|
386
|
+
chatType === ChannelType.DM ? contextId : undefined
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
const createDmChannelMutation = useCreateDmChannel();
|
|
390
|
+
|
|
391
|
+
// Group chat specific data
|
|
392
|
+
const { data: channelDetailsData } = useChannelDetails(
|
|
393
|
+
chatType === ChannelType.GROUP ? contextId : undefined
|
|
394
|
+
);
|
|
395
|
+
const { data: participantsData } = useChannelParticipants(
|
|
396
|
+
chatType === ChannelType.GROUP ? contextId : undefined
|
|
397
|
+
);
|
|
398
|
+
const participants = participantsData?.data;
|
|
399
|
+
|
|
400
|
+
const { data: agentsResponse } = useAgentsWithDetails();
|
|
401
|
+
const allAgents = agentsResponse?.agents || [];
|
|
402
|
+
|
|
403
|
+
const latestChannel = agentDmChannels[0]; // adjust sorting if needed
|
|
404
|
+
|
|
405
|
+
// Get the final channel ID for hooks
|
|
406
|
+
const finalChannelIdForHooks: UUID | undefined =
|
|
407
|
+
chatType === ChannelType.DM
|
|
408
|
+
? chatState.currentDmChannelId || undefined
|
|
409
|
+
: contextId || undefined;
|
|
410
|
+
|
|
411
|
+
const finalServerIdForHooks: UUID | undefined = useMemo(() => {
|
|
412
|
+
return chatType === ChannelType.DM ? DEFAULT_SERVER_ID : serverId || undefined;
|
|
413
|
+
}, [chatType, serverId]);
|
|
414
|
+
|
|
415
|
+
const { data: latestChannelMessages = [], isLoading: isLoadingLatestChannelMessages } =
|
|
416
|
+
useChannelMessages(latestChannel?.id, finalServerIdForHooks);
|
|
417
|
+
|
|
418
|
+
const {
|
|
419
|
+
data: messages = [],
|
|
420
|
+
isLoading: isLoadingMessages,
|
|
421
|
+
addMessage,
|
|
422
|
+
updateMessage,
|
|
423
|
+
removeMessage,
|
|
424
|
+
clearMessages,
|
|
425
|
+
} = useChannelMessages(finalChannelIdForHooks, finalServerIdForHooks);
|
|
426
|
+
|
|
427
|
+
// Get agents in the current group
|
|
428
|
+
const groupAgents = useMemo(() => {
|
|
429
|
+
if (chatType !== ChannelType.GROUP || !participants) return [];
|
|
430
|
+
return participants
|
|
431
|
+
.map((pId) => allAgents.find((a) => a.id === pId))
|
|
432
|
+
.filter(Boolean) as Agent[];
|
|
433
|
+
}, [chatType, participants, allAgents]);
|
|
434
|
+
|
|
435
|
+
const agentAvatarMap = useMemo(
|
|
436
|
+
() =>
|
|
437
|
+
allAgents.reduce(
|
|
438
|
+
(acc, agent) => {
|
|
439
|
+
if (agent.id && typeof agent.settings?.avatar === 'string') {
|
|
440
|
+
acc[agent.id] = agent.settings.avatar;
|
|
441
|
+
}
|
|
442
|
+
return acc;
|
|
443
|
+
},
|
|
444
|
+
{} as Record<UUID, string | null>
|
|
445
|
+
),
|
|
446
|
+
[allAgents]
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
const getAgentInMessage = useCallback(
|
|
450
|
+
(agentId: UUID) => {
|
|
451
|
+
return allAgents.find((a) => a.id === agentId);
|
|
452
|
+
},
|
|
453
|
+
[allAgents]
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
const {
|
|
457
|
+
scrollRef,
|
|
458
|
+
contentRef,
|
|
459
|
+
isAtBottom,
|
|
460
|
+
scrollToBottom,
|
|
461
|
+
disableAutoScroll,
|
|
462
|
+
autoScrollEnabled,
|
|
463
|
+
} = useAutoScroll({ smooth: true });
|
|
464
|
+
const prevMessageCountRef = useRef(0);
|
|
465
|
+
const safeScrollToBottom = useCallback(() => {
|
|
466
|
+
if (scrollRef.current) {
|
|
467
|
+
setTimeout(() => scrollToBottom(), 0);
|
|
468
|
+
}
|
|
469
|
+
}, [scrollToBottom, scrollRef]);
|
|
470
|
+
|
|
471
|
+
// Prevent repeated auto-creation of a DM channel when none exist
|
|
472
|
+
const autoCreatedDmRef = useRef(false);
|
|
473
|
+
const autoCreateTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
474
|
+
|
|
475
|
+
// Handle DM channel creation
|
|
476
|
+
const handleNewDmChannel = useCallback(
|
|
477
|
+
async (agentIdForNewChannel: UUID | undefined) => {
|
|
478
|
+
if (!agentIdForNewChannel || chatType !== 'DM') return;
|
|
479
|
+
|
|
480
|
+
if (latestChannel) {
|
|
481
|
+
try {
|
|
482
|
+
const elizaClient = createElizaClient();
|
|
483
|
+
const latestMessages = await elizaClient.messaging.getChannelMessages(latestChannel.id, {
|
|
484
|
+
limit: 30,
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
const hasAutoName = isAutoGeneratedChatName(latestChannel.name);
|
|
488
|
+
const isEmpty = (latestMessages?.messages?.length ?? 0) === 0;
|
|
489
|
+
|
|
490
|
+
if (hasAutoName && isEmpty) {
|
|
491
|
+
const isAlreadyInLatest = chatState.currentDmChannelId === latestChannel.id;
|
|
492
|
+
|
|
493
|
+
if (isAlreadyInLatest) {
|
|
494
|
+
toast({
|
|
495
|
+
title: `Already in a fresh chat`,
|
|
496
|
+
description: `You're already in a new chat with ${targetAgentData?.name || 'the agent'}.`,
|
|
497
|
+
});
|
|
498
|
+
} else {
|
|
499
|
+
updateChatState({ currentDmChannelId: latestChannel.id });
|
|
500
|
+
toast({
|
|
501
|
+
title: `Chat opened`,
|
|
502
|
+
description: `You can now start chatting with ${targetAgentData?.name || 'the agent'}.`,
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
updateChatState({ currentDmChannelId: latestChannel.id });
|
|
507
|
+
return;
|
|
508
|
+
} else {
|
|
509
|
+
clientLogger.info(
|
|
510
|
+
'[Chat] Latest DM channel has messages or customized name, proceeding to create a new one.'
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
} catch (error) {
|
|
514
|
+
clientLogger.error('[Chat] Failed to fetch latest DM channel messages:', error);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const newChatName = `Chat - ${moment().format('MMM D, HH:mm:ss')}`;
|
|
519
|
+
clientLogger.info(
|
|
520
|
+
`[Chat] Creating new distinct DM channel with agent ${agentIdForNewChannel}, name: "${newChatName}"`
|
|
521
|
+
);
|
|
522
|
+
updateChatState({ isCreatingDM: true });
|
|
523
|
+
try {
|
|
524
|
+
// Mark as auto-created so the effect doesn't attempt a duplicate.
|
|
525
|
+
autoCreatedDmRef.current = true;
|
|
526
|
+
|
|
527
|
+
await createDmChannelMutation.mutateAsync({
|
|
528
|
+
agentId: agentIdForNewChannel,
|
|
529
|
+
channelName: newChatName, // Provide a unique name
|
|
530
|
+
});
|
|
531
|
+
updateChatState({ input: '' });
|
|
532
|
+
setTimeout(() => safeScrollToBottom(), 150);
|
|
533
|
+
} catch (error) {
|
|
534
|
+
clientLogger.error('[Chat] Error creating new distinct DM channel:', error);
|
|
535
|
+
// Toast is handled by the mutation hook
|
|
536
|
+
updateChatState({ currentDmChannelId: null, input: '' });
|
|
537
|
+
} finally {
|
|
538
|
+
updateChatState({ isCreatingDM: false });
|
|
539
|
+
toast({
|
|
540
|
+
title: `Chat opened`,
|
|
541
|
+
description: `You can now start chatting with ${targetAgentData?.name || 'the agent'}.`,
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
},
|
|
545
|
+
[chatType, createDmChannelMutation, updateChatState, safeScrollToBottom, latestChannel]
|
|
546
|
+
);
|
|
547
|
+
|
|
548
|
+
// Handle DM channel selection
|
|
549
|
+
const handleSelectDmRoom = useCallback(
|
|
550
|
+
(channelIdToSelect: UUID) => {
|
|
551
|
+
const selectedChannel = agentDmChannels.find((channel) => channel.id === channelIdToSelect);
|
|
552
|
+
if (selectedChannel) {
|
|
553
|
+
clientLogger.info(
|
|
554
|
+
`[Chat] DM Channel selected: ${selectedChannel.name} (Channel ID: ${selectedChannel.id})`
|
|
555
|
+
);
|
|
556
|
+
updateChatState({ currentDmChannelId: selectedChannel.id, input: '' });
|
|
557
|
+
setTimeout(() => safeScrollToBottom(), 150);
|
|
558
|
+
}
|
|
559
|
+
},
|
|
560
|
+
[agentDmChannels, updateChatState, safeScrollToBottom]
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
// Handle DM channel deletion
|
|
564
|
+
const handleDeleteCurrentDmChannel = useCallback(() => {
|
|
565
|
+
if (chatType !== ChannelType.DM || !chatState.currentDmChannelId || !targetAgentData?.id)
|
|
566
|
+
return;
|
|
567
|
+
const channelToDelete = agentDmChannels.find((ch) => ch.id === chatState.currentDmChannelId);
|
|
568
|
+
if (!channelToDelete) return;
|
|
569
|
+
|
|
570
|
+
confirm(
|
|
571
|
+
{
|
|
572
|
+
title: 'Delete Chat',
|
|
573
|
+
description: `Are you sure you want to delete the chat "${channelToDelete.name}" with ${targetAgentData.name}? This action cannot be undone.`,
|
|
574
|
+
confirmText: 'Delete',
|
|
575
|
+
variant: 'destructive',
|
|
576
|
+
},
|
|
577
|
+
async () => {
|
|
578
|
+
clientLogger.info(`[Chat] Deleting DM channel ${channelToDelete.id}`);
|
|
579
|
+
try {
|
|
580
|
+
const elizaClient = createElizaClient();
|
|
581
|
+
await elizaClient.messaging.deleteChannel(channelToDelete.id);
|
|
582
|
+
|
|
583
|
+
// --- Optimistically update the React-Query cache so UI refreshes instantly ---
|
|
584
|
+
queryClient.setQueryData<MessageChannel[] | undefined>(
|
|
585
|
+
['dmChannels', targetAgentData.id, currentClientEntityId],
|
|
586
|
+
(old) => old?.filter((ch) => ch.id !== channelToDelete.id)
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
// Force a refetch to stay in sync with the server
|
|
590
|
+
queryClient.invalidateQueries({
|
|
591
|
+
queryKey: ['dmChannels', targetAgentData.id, currentClientEntityId],
|
|
592
|
+
});
|
|
593
|
+
// Also keep the broader channels cache in sync
|
|
594
|
+
queryClient.invalidateQueries({ queryKey: ['channels'] });
|
|
595
|
+
|
|
596
|
+
toast({ title: 'Chat Deleted', description: `"${channelToDelete.name}" was deleted.` });
|
|
597
|
+
|
|
598
|
+
const remainingChannels =
|
|
599
|
+
(queryClient.getQueryData(['dmChannels', targetAgentData.id, currentClientEntityId]) as
|
|
600
|
+
| MessageChannel[]
|
|
601
|
+
| undefined) || [];
|
|
602
|
+
|
|
603
|
+
if (remainingChannels.length > 0) {
|
|
604
|
+
updateChatState({ currentDmChannelId: remainingChannels[0].id });
|
|
605
|
+
clientLogger.info('[Chat] Switched to DM channel:', remainingChannels[0].id);
|
|
606
|
+
} else {
|
|
607
|
+
clientLogger.info(
|
|
608
|
+
'[Chat] No DM channels left after deletion. Will create a fresh chat once.'
|
|
609
|
+
);
|
|
610
|
+
// Clear the current DM so the effect can handle creating exactly one new chat
|
|
611
|
+
updateChatState({ currentDmChannelId: null });
|
|
612
|
+
// Allow the auto-create logic to run again
|
|
613
|
+
autoCreatedDmRef.current = false;
|
|
614
|
+
await handleNewDmChannel(targetAgentData.id);
|
|
615
|
+
}
|
|
616
|
+
} catch (error) {
|
|
617
|
+
clientLogger.error('[Chat] Error deleting DM channel:', error);
|
|
618
|
+
toast({
|
|
619
|
+
title: 'Error',
|
|
620
|
+
description: 'Could not delete chat. The server might not support this action yet.',
|
|
621
|
+
variant: 'destructive',
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
);
|
|
626
|
+
}, [
|
|
627
|
+
chatType,
|
|
628
|
+
chatState.currentDmChannelId,
|
|
629
|
+
targetAgentData,
|
|
630
|
+
agentDmChannels,
|
|
631
|
+
confirm,
|
|
632
|
+
toast,
|
|
633
|
+
updateChatState,
|
|
634
|
+
handleNewDmChannel,
|
|
635
|
+
queryClient,
|
|
636
|
+
currentClientEntityId,
|
|
637
|
+
]);
|
|
638
|
+
|
|
639
|
+
useEffect(() => {
|
|
640
|
+
if (
|
|
641
|
+
latestChannel &&
|
|
642
|
+
!isLoadingLatestChannelMessages &&
|
|
643
|
+
latestChannelMessages &&
|
|
644
|
+
targetAgentData?.id &&
|
|
645
|
+
shouldForceNew
|
|
646
|
+
) {
|
|
647
|
+
handleNewDmChannel(targetAgentData.id);
|
|
648
|
+
setShouldForceNew(false);
|
|
649
|
+
|
|
650
|
+
navigate(location.pathname, { replace: true });
|
|
651
|
+
}
|
|
652
|
+
}, [
|
|
653
|
+
shouldForceNew,
|
|
654
|
+
setShouldForceNew,
|
|
655
|
+
handleNewDmChannel,
|
|
656
|
+
targetAgentData?.id,
|
|
657
|
+
latestChannel,
|
|
658
|
+
latestChannelMessages,
|
|
659
|
+
isLoadingLatestChannelMessages,
|
|
660
|
+
]);
|
|
661
|
+
|
|
662
|
+
useEffect(() => {
|
|
663
|
+
inputDisabledRef.current = chatState.inputDisabled;
|
|
664
|
+
}, [chatState.inputDisabled]);
|
|
665
|
+
|
|
666
|
+
useEffect(() => {
|
|
667
|
+
const currentChannel = agentDmChannels.find((c) => c.id === chatState.currentDmChannelId);
|
|
668
|
+
if (currentChannel?.name) {
|
|
669
|
+
chatTitleRef.current = currentChannel.name;
|
|
670
|
+
}
|
|
671
|
+
}, [agentDmChannels, chatState.currentDmChannelId]);
|
|
672
|
+
|
|
673
|
+
useEffect(() => {
|
|
674
|
+
if (!isLoadingAgentDmChannels && agentDmChannels.length > 0) {
|
|
675
|
+
clientLogger.info('[Chat] Selecting first available DM channel:', agentDmChannels[0].id);
|
|
676
|
+
updateChatState({ currentDmChannelId: agentDmChannels[0].id });
|
|
677
|
+
}
|
|
678
|
+
}, [agentDmChannels, isLoadingAgentDmChannels, updateChatState]);
|
|
679
|
+
|
|
680
|
+
// Effect to handle initial DM channel selection or creation
|
|
681
|
+
useEffect(() => {
|
|
682
|
+
if (chatType === ChannelType.DM && targetAgentData?.id) {
|
|
683
|
+
// First, check if current channel belongs to the current agent
|
|
684
|
+
// If not, clear it immediately (handles agent switching)
|
|
685
|
+
const currentChannelBelongsToAgent =
|
|
686
|
+
!chatState.currentDmChannelId ||
|
|
687
|
+
agentDmChannels.some((c) => c.id === chatState.currentDmChannelId);
|
|
688
|
+
|
|
689
|
+
if (!currentChannelBelongsToAgent && !isLoadingAgentDmChannels) {
|
|
690
|
+
clientLogger.info(
|
|
691
|
+
`[Chat] Current DM channel ${chatState.currentDmChannelId} doesn't belong to agent ${targetAgentData.id}, clearing it`
|
|
692
|
+
);
|
|
693
|
+
updateChatState({ currentDmChannelId: null });
|
|
694
|
+
return; // Exit early, let the effect run again with cleared state
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
if (
|
|
698
|
+
!isLoadingAgentDmChannels &&
|
|
699
|
+
agentDmChannels.length === 0 &&
|
|
700
|
+
!initialDmChannelId &&
|
|
701
|
+
!autoCreatedDmRef.current &&
|
|
702
|
+
!chatState.isCreatingDM &&
|
|
703
|
+
!createDmChannelMutation.isPending
|
|
704
|
+
) {
|
|
705
|
+
// No channels at all and none expected via URL -> create exactly one
|
|
706
|
+
clientLogger.info('[Chat] No existing DM channels found; auto-creating a fresh one.');
|
|
707
|
+
autoCreatedDmRef.current = true;
|
|
708
|
+
handleNewDmChannel(targetAgentData.id);
|
|
709
|
+
}
|
|
710
|
+
} else if (chatType !== ChannelType.DM && chatState.currentDmChannelId !== null) {
|
|
711
|
+
// Only reset if necessary
|
|
712
|
+
updateChatState({ currentDmChannelId: null });
|
|
713
|
+
}
|
|
714
|
+
}, [
|
|
715
|
+
chatType,
|
|
716
|
+
targetAgentData?.id,
|
|
717
|
+
agentDmChannels,
|
|
718
|
+
isLoadingAgentDmChannels,
|
|
719
|
+
createDmChannelMutation.isPending,
|
|
720
|
+
chatState.isCreatingDM,
|
|
721
|
+
chatState.currentDmChannelId,
|
|
722
|
+
initialDmChannelId,
|
|
723
|
+
updateChatState,
|
|
724
|
+
handleNewDmChannel,
|
|
725
|
+
]);
|
|
726
|
+
|
|
727
|
+
// Cleanup timeout on unmount or when agentDmChannels appears
|
|
728
|
+
useEffect(() => {
|
|
729
|
+
if (agentDmChannels.length > 0 && autoCreateTimeoutRef.current) {
|
|
730
|
+
clearTimeout(autoCreateTimeoutRef.current);
|
|
731
|
+
autoCreateTimeoutRef.current = null;
|
|
732
|
+
}
|
|
733
|
+
return () => {
|
|
734
|
+
if (autoCreateTimeoutRef.current) {
|
|
735
|
+
clearTimeout(autoCreateTimeoutRef.current);
|
|
736
|
+
autoCreateTimeoutRef.current = null;
|
|
737
|
+
}
|
|
738
|
+
};
|
|
739
|
+
}, [agentDmChannels]);
|
|
740
|
+
|
|
741
|
+
// Auto-select single agent in group
|
|
742
|
+
useEffect(() => {
|
|
743
|
+
if (
|
|
744
|
+
chatType === ChannelType.GROUP &&
|
|
745
|
+
groupAgents.length === 1 &&
|
|
746
|
+
!chatState.selectedGroupAgentId
|
|
747
|
+
) {
|
|
748
|
+
updateChatState({
|
|
749
|
+
selectedGroupAgentId: groupAgents[0].id as UUID,
|
|
750
|
+
});
|
|
751
|
+
if (!showSidebar) {
|
|
752
|
+
setSidebarVisible(true);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}, [
|
|
756
|
+
chatType,
|
|
757
|
+
groupAgents,
|
|
758
|
+
chatState.selectedGroupAgentId,
|
|
759
|
+
updateChatState,
|
|
760
|
+
showSidebar,
|
|
761
|
+
setSidebarVisible,
|
|
762
|
+
]);
|
|
763
|
+
|
|
764
|
+
const { mutate: deleteMessageCentral } = useDeleteChannelMessage();
|
|
765
|
+
const { mutate: clearMessagesCentral } = useClearChannelMessages();
|
|
766
|
+
|
|
767
|
+
// Auto-scroll handling
|
|
768
|
+
useEffect(() => {
|
|
769
|
+
const isInitialLoadWithMessages = prevMessageCountRef.current === 0 && messages.length > 0;
|
|
770
|
+
const hasNewMessages =
|
|
771
|
+
messages.length !== prevMessageCountRef.current && prevMessageCountRef.current !== 0;
|
|
772
|
+
|
|
773
|
+
if (isInitialLoadWithMessages) {
|
|
774
|
+
clientLogger.debug('[chat] Initial messages loaded, scrolling to bottom.', {
|
|
775
|
+
count: messages.length,
|
|
776
|
+
});
|
|
777
|
+
safeScrollToBottom();
|
|
778
|
+
} else if (hasNewMessages) {
|
|
779
|
+
if (autoScrollEnabled) {
|
|
780
|
+
clientLogger.debug('[chat] New messages and autoScroll enabled, scrolling.');
|
|
781
|
+
safeScrollToBottom();
|
|
782
|
+
} else {
|
|
783
|
+
clientLogger.debug('[chat] New messages, but autoScroll is disabled (user scrolled up).');
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
prevMessageCountRef.current = messages.length;
|
|
787
|
+
}, [messages, autoScrollEnabled, safeScrollToBottom, finalChannelIdForHooks]);
|
|
788
|
+
|
|
789
|
+
function isAutoGeneratedChatName(name: string | undefined): boolean {
|
|
790
|
+
return !!name?.match(/^Chat - [A-Z][a-z]{2} \d{1,2}, \d{2}:\d{2}(:\d{2})?$/);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
const updateChatTitle = async () => {
|
|
794
|
+
const shouldUpdate: boolean =
|
|
795
|
+
!!chatTitleRef.current &&
|
|
796
|
+
isAutoGeneratedChatName(chatTitleRef.current) &&
|
|
797
|
+
chatType === ChannelType.DM;
|
|
798
|
+
|
|
799
|
+
if (!shouldUpdate) {
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// Guard against undefined channel IDs
|
|
804
|
+
if (!finalChannelIdForHooks || !chatState.currentDmChannelId) {
|
|
805
|
+
clientLogger.warn('Cannot update chat title: missing channel ID');
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const elizaClient = createElizaClient();
|
|
810
|
+
const data = await elizaClient.messaging.generateChannelTitle(
|
|
811
|
+
finalChannelIdForHooks,
|
|
812
|
+
contextId
|
|
813
|
+
);
|
|
814
|
+
|
|
815
|
+
const title = data?.title;
|
|
816
|
+
const participants = await elizaClient.messaging.getChannelParticipants(
|
|
817
|
+
chatState.currentDmChannelId
|
|
818
|
+
);
|
|
819
|
+
if (title && participants) {
|
|
820
|
+
// Handle different possible response formats for participants
|
|
821
|
+
let participantIds = [];
|
|
822
|
+
if (participants && Array.isArray(participants.participants)) {
|
|
823
|
+
participantIds = participants.participants.map((p) => p.userId);
|
|
824
|
+
} else if (participants && Array.isArray(participants)) {
|
|
825
|
+
participantIds = participants.map((p) => p.userId || p.id || p);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
await elizaClient.messaging.updateChannel(finalChannelIdForHooks, {
|
|
829
|
+
name: title,
|
|
830
|
+
participantCentralUserIds: participantIds,
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
const currentUserId = getEntityId();
|
|
834
|
+
queryClient.invalidateQueries({
|
|
835
|
+
queryKey: ['dmChannels', contextId, currentUserId],
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
const { sendMessage, animatedMessageId } = useSocketChat({
|
|
841
|
+
channelId: finalChannelIdForHooks,
|
|
842
|
+
currentUserId: currentClientEntityId,
|
|
843
|
+
contextId,
|
|
844
|
+
chatType,
|
|
845
|
+
allAgents,
|
|
846
|
+
messages,
|
|
847
|
+
onAddMessage: (message: UiMessage) => {
|
|
848
|
+
addMessage(message);
|
|
849
|
+
updateChatTitle();
|
|
850
|
+
if (message.isAgent) safeScrollToBottom();
|
|
851
|
+
},
|
|
852
|
+
onUpdateMessage: (messageId: string, updates: Partial<UiMessage>) => {
|
|
853
|
+
updateMessage(messageId as UUID, updates);
|
|
854
|
+
if (!updates.isLoading && updates.isLoading !== undefined) safeScrollToBottom();
|
|
855
|
+
},
|
|
856
|
+
onDeleteMessage: (messageId: string) => {
|
|
857
|
+
removeMessage(messageId as UUID);
|
|
858
|
+
},
|
|
859
|
+
onClearMessages: () => {
|
|
860
|
+
// Clear the local message list immediately for instant UI response
|
|
861
|
+
clearMessages();
|
|
862
|
+
},
|
|
863
|
+
onInputDisabledChange: (disabled: boolean) => updateChatState({ inputDisabled: disabled }),
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
const {
|
|
867
|
+
selectedFiles,
|
|
868
|
+
handleFileChange,
|
|
869
|
+
removeFile,
|
|
870
|
+
createBlobUrls,
|
|
871
|
+
uploadFiles,
|
|
872
|
+
cleanupBlobUrls,
|
|
873
|
+
clearFiles,
|
|
874
|
+
} = useFileUpload({
|
|
875
|
+
agentId: targetAgentData?.id,
|
|
876
|
+
channelId: finalChannelIdForHooks,
|
|
877
|
+
chatType,
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
// Handlers
|
|
881
|
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
882
|
+
if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
|
|
883
|
+
e.preventDefault();
|
|
884
|
+
handleSendMessage(e as unknown as React.FormEvent<HTMLFormElement>);
|
|
885
|
+
}
|
|
886
|
+
};
|
|
887
|
+
|
|
888
|
+
const handleSendMessage = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
889
|
+
e.preventDefault();
|
|
890
|
+
|
|
891
|
+
// For DM chats, ensure we have a channel before sending
|
|
892
|
+
let channelIdToUse = finalChannelIdForHooks;
|
|
893
|
+
if (chatType === ChannelType.DM && !channelIdToUse && targetAgentData?.id) {
|
|
894
|
+
// If a DM channel is already being (auto) created, abort to prevent duplicate creations.
|
|
895
|
+
if (chatState.isCreatingDM || createDmChannelMutation.isPending) {
|
|
896
|
+
clientLogger.info(
|
|
897
|
+
'[Chat] DM channel creation already in progress; will wait for it to finish instead of creating another.'
|
|
898
|
+
);
|
|
899
|
+
// Early return so the user can try sending again once the channel is ready.
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
clientLogger.info('[Chat] No DM channel selected, creating one before sending message');
|
|
904
|
+
try {
|
|
905
|
+
// Mark as auto-created so the effect doesn't attempt a duplicate.
|
|
906
|
+
autoCreatedDmRef.current = true;
|
|
907
|
+
|
|
908
|
+
const newChannel = await createDmChannelMutation.mutateAsync({
|
|
909
|
+
agentId: targetAgentData.id,
|
|
910
|
+
channelName: `Chat - ${moment().format('MMM D, HH:mm')}`,
|
|
911
|
+
});
|
|
912
|
+
updateChatState({ currentDmChannelId: newChannel.id });
|
|
913
|
+
channelIdToUse = newChannel.id;
|
|
914
|
+
// Wait a moment for state to propagate
|
|
915
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
916
|
+
} catch (error) {
|
|
917
|
+
clientLogger.error('[Chat] Failed to create DM channel before sending message:', error);
|
|
918
|
+
toast({
|
|
919
|
+
title: 'Error',
|
|
920
|
+
description: 'Failed to create chat channel. Please try again.',
|
|
921
|
+
variant: 'destructive',
|
|
922
|
+
});
|
|
923
|
+
updateChatState({ inputDisabled: false });
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
if (
|
|
929
|
+
(!chatState.input.trim() && selectedFiles.length === 0) ||
|
|
930
|
+
inputDisabledRef.current ||
|
|
931
|
+
!channelIdToUse ||
|
|
932
|
+
!finalServerIdForHooks ||
|
|
933
|
+
!currentClientEntityId ||
|
|
934
|
+
(chatType === ChannelType.DM && !targetAgentData?.id)
|
|
935
|
+
)
|
|
936
|
+
return;
|
|
937
|
+
|
|
938
|
+
const tempMessageId = randomUUID() as UUID;
|
|
939
|
+
let messageText = chatState.input.trim();
|
|
940
|
+
const currentInputVal = chatState.input;
|
|
941
|
+
updateChatState({ input: '', inputDisabled: true });
|
|
942
|
+
const currentSelectedFiles = [...selectedFiles];
|
|
943
|
+
clearFiles();
|
|
944
|
+
formRef.current?.reset();
|
|
945
|
+
const optimisticAttachments = createBlobUrls(currentSelectedFiles);
|
|
946
|
+
const optimisticUiMessage: UiMessage = {
|
|
947
|
+
id: tempMessageId,
|
|
948
|
+
text: messageText,
|
|
949
|
+
name: USER_NAME,
|
|
950
|
+
createdAt: Date.now(),
|
|
951
|
+
senderId: currentClientEntityId,
|
|
952
|
+
isAgent: false,
|
|
953
|
+
isLoading: true,
|
|
954
|
+
channelId: channelIdToUse,
|
|
955
|
+
serverId: finalServerIdForHooks,
|
|
956
|
+
source: chatType === ChannelType.DM ? CHAT_SOURCE : GROUP_CHAT_SOURCE,
|
|
957
|
+
attachments: optimisticAttachments,
|
|
958
|
+
};
|
|
959
|
+
if (messageText || currentSelectedFiles.length > 0) addMessage(optimisticUiMessage);
|
|
960
|
+
safeScrollToBottom();
|
|
961
|
+
try {
|
|
962
|
+
let processedUiAttachments: Media[] = [];
|
|
963
|
+
if (currentSelectedFiles.length > 0) {
|
|
964
|
+
const { uploaded, failed, blobUrls } = await uploadFiles(currentSelectedFiles);
|
|
965
|
+
processedUiAttachments = uploaded;
|
|
966
|
+
if (failed.length > 0)
|
|
967
|
+
updateMessage(tempMessageId, {
|
|
968
|
+
attachments: optimisticUiMessage.attachments?.filter(
|
|
969
|
+
(att) => !failed.some((f) => f.file.id === att.id)
|
|
970
|
+
),
|
|
971
|
+
});
|
|
972
|
+
cleanupBlobUrls(blobUrls);
|
|
973
|
+
if (!messageText.trim() && processedUiAttachments.length > 0)
|
|
974
|
+
messageText = `Shared ${processedUiAttachments.length} file(s).`;
|
|
975
|
+
}
|
|
976
|
+
const mediaInfosFromText = parseMediaFromText(currentInputVal);
|
|
977
|
+
const textMediaAttachments: Media[] = mediaInfosFromText.map(
|
|
978
|
+
(media: MediaInfo, index: number): Media => ({
|
|
979
|
+
id: `textmedia-${tempMessageId}-${index}`,
|
|
980
|
+
url: media.url,
|
|
981
|
+
title: media.type === 'image' ? 'Image' : media.type === 'video' ? 'Video' : 'Media Link',
|
|
982
|
+
source: 'user_input_url',
|
|
983
|
+
contentType:
|
|
984
|
+
media.type === 'image'
|
|
985
|
+
? CoreContentType.IMAGE
|
|
986
|
+
: media.type === 'video'
|
|
987
|
+
? CoreContentType.VIDEO
|
|
988
|
+
: undefined,
|
|
989
|
+
})
|
|
990
|
+
);
|
|
991
|
+
const finalAttachments = [...processedUiAttachments, ...textMediaAttachments];
|
|
992
|
+
const finalTextContent =
|
|
993
|
+
messageText || (finalAttachments.length > 0 ? `Shared content.` : '');
|
|
994
|
+
if (!finalTextContent.trim() && finalAttachments.length === 0) {
|
|
995
|
+
updateChatState({ inputDisabled: false });
|
|
996
|
+
removeMessage(tempMessageId);
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
await sendMessage(
|
|
1000
|
+
finalTextContent,
|
|
1001
|
+
finalServerIdForHooks,
|
|
1002
|
+
chatType === ChannelType.DM ? CHAT_SOURCE : GROUP_CHAT_SOURCE,
|
|
1003
|
+
finalAttachments.length > 0 ? finalAttachments : undefined,
|
|
1004
|
+
tempMessageId,
|
|
1005
|
+
undefined,
|
|
1006
|
+
channelIdToUse
|
|
1007
|
+
);
|
|
1008
|
+
} catch (error) {
|
|
1009
|
+
clientLogger.error('Error sending message or uploading files:', error);
|
|
1010
|
+
toast({
|
|
1011
|
+
title: 'Error Sending Message',
|
|
1012
|
+
description: error instanceof Error ? error.message : 'Could not send message.',
|
|
1013
|
+
variant: 'destructive',
|
|
1014
|
+
});
|
|
1015
|
+
updateMessage(tempMessageId, {
|
|
1016
|
+
isLoading: false,
|
|
1017
|
+
text: `${optimisticUiMessage.text || 'Attachment(s)'} (Failed to send)`,
|
|
1018
|
+
});
|
|
1019
|
+
// Re-enable input on error
|
|
1020
|
+
updateChatState({ inputDisabled: false });
|
|
1021
|
+
} finally {
|
|
1022
|
+
// Let the server control input state via control messages
|
|
1023
|
+
// Only focus the input, don't re-enable it
|
|
1024
|
+
inputRef.current?.focus();
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
1027
|
+
|
|
1028
|
+
const handleDeleteMessage = (messageId: string) => {
|
|
1029
|
+
if (!finalChannelIdForHooks || !messageId) return;
|
|
1030
|
+
const validMessageId = validateUuid(messageId);
|
|
1031
|
+
if (validMessageId) {
|
|
1032
|
+
// Immediately remove message from UI for optimistic update
|
|
1033
|
+
removeMessage(messageId as UUID);
|
|
1034
|
+
// Call server mutation to delete on backend
|
|
1035
|
+
deleteMessageCentral({ channelId: finalChannelIdForHooks, messageId: validMessageId });
|
|
1036
|
+
}
|
|
1037
|
+
};
|
|
1038
|
+
|
|
1039
|
+
const handleRetryMessage = async (message: UiMessage) => {
|
|
1040
|
+
if (inputDisabledRef.current || (!message.text?.trim() && message.attachments?.length === 0)) {
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
updateChatState({ inputDisabled: true });
|
|
1044
|
+
const retryMessageId = randomUUID() as UUID;
|
|
1045
|
+
const finalTextContent =
|
|
1046
|
+
message.text?.trim() || `Shared ${message.attachments?.length} file(s).`;
|
|
1047
|
+
|
|
1048
|
+
const optimisticUiMessage: UiMessage = {
|
|
1049
|
+
id: retryMessageId,
|
|
1050
|
+
text: message.text,
|
|
1051
|
+
name: USER_NAME,
|
|
1052
|
+
createdAt: Date.now(),
|
|
1053
|
+
senderId: currentClientEntityId,
|
|
1054
|
+
isAgent: false,
|
|
1055
|
+
isLoading: true,
|
|
1056
|
+
channelId: message.channelId,
|
|
1057
|
+
serverId: finalServerIdForHooks,
|
|
1058
|
+
source: chatType === ChannelType.DM ? CHAT_SOURCE : GROUP_CHAT_SOURCE,
|
|
1059
|
+
attachments: message.attachments,
|
|
1060
|
+
};
|
|
1061
|
+
|
|
1062
|
+
addMessage(optimisticUiMessage);
|
|
1063
|
+
safeScrollToBottom();
|
|
1064
|
+
|
|
1065
|
+
// Guard against undefined IDs
|
|
1066
|
+
if (!finalServerIdForHooks || !finalChannelIdForHooks) {
|
|
1067
|
+
clientLogger.error('Cannot retry message: missing server or channel ID');
|
|
1068
|
+
toast({
|
|
1069
|
+
title: 'Error Sending Message',
|
|
1070
|
+
description: 'Missing required channel information.',
|
|
1071
|
+
variant: 'destructive',
|
|
1072
|
+
});
|
|
1073
|
+
updateChatState({ inputDisabled: false });
|
|
1074
|
+
removeMessage(retryMessageId);
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
try {
|
|
1079
|
+
await sendMessage(
|
|
1080
|
+
finalTextContent,
|
|
1081
|
+
finalServerIdForHooks,
|
|
1082
|
+
chatType === ChannelType.DM ? CHAT_SOURCE : GROUP_CHAT_SOURCE,
|
|
1083
|
+
message.attachments,
|
|
1084
|
+
retryMessageId,
|
|
1085
|
+
undefined,
|
|
1086
|
+
finalChannelIdForHooks
|
|
1087
|
+
);
|
|
1088
|
+
} catch (error) {
|
|
1089
|
+
clientLogger.error('Error sending message or uploading files:', error);
|
|
1090
|
+
toast({
|
|
1091
|
+
title: 'Error Sending Message',
|
|
1092
|
+
description: error instanceof Error ? error.message : 'Could not send message.',
|
|
1093
|
+
variant: 'destructive',
|
|
1094
|
+
});
|
|
1095
|
+
updateMessage(retryMessageId, {
|
|
1096
|
+
isLoading: false,
|
|
1097
|
+
text: `${optimisticUiMessage.text || 'Attachment(s)'} (Failed to send)`,
|
|
1098
|
+
});
|
|
1099
|
+
updateChatState({ inputDisabled: false });
|
|
1100
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
|
|
1103
|
+
const handleClearChat = () => {
|
|
1104
|
+
if (!finalChannelIdForHooks) return;
|
|
1105
|
+
const confirmMessage =
|
|
1106
|
+
chatType === ChannelType.DM
|
|
1107
|
+
? `Clear all messages in this chat with ${targetAgentData?.name}?`
|
|
1108
|
+
: 'Clear all messages in this group chat?';
|
|
1109
|
+
|
|
1110
|
+
confirm(
|
|
1111
|
+
{
|
|
1112
|
+
title: 'Clear Chat',
|
|
1113
|
+
description: `${confirmMessage} This action cannot be undone.`,
|
|
1114
|
+
confirmText: 'Clear',
|
|
1115
|
+
variant: 'destructive',
|
|
1116
|
+
},
|
|
1117
|
+
() => {
|
|
1118
|
+
clearMessagesCentral(finalChannelIdForHooks);
|
|
1119
|
+
}
|
|
1120
|
+
);
|
|
1121
|
+
};
|
|
1122
|
+
|
|
1123
|
+
// Handle mobile detection and window resize
|
|
1124
|
+
useEffect(() => {
|
|
1125
|
+
const checkMobile = () => {
|
|
1126
|
+
const isMobile = window.innerWidth < 768;
|
|
1127
|
+
updateChatState({ isMobile });
|
|
1128
|
+
// Note: Don't auto-hide sidebar on mobile - let floating mode handle it
|
|
1129
|
+
};
|
|
1130
|
+
|
|
1131
|
+
// Initial check
|
|
1132
|
+
checkMobile();
|
|
1133
|
+
|
|
1134
|
+
// Add resize listener
|
|
1135
|
+
window.addEventListener('resize', checkMobile);
|
|
1136
|
+
return () => window.removeEventListener('resize', checkMobile);
|
|
1137
|
+
}, [updateChatState]);
|
|
1138
|
+
|
|
1139
|
+
if (
|
|
1140
|
+
chatType === ChannelType.DM &&
|
|
1141
|
+
(isLoadingAgent || (!targetAgentData && contextId) || isLoadingAgentDmChannels)
|
|
1142
|
+
) {
|
|
1143
|
+
return (
|
|
1144
|
+
<div className="flex items-center justify-center h-full">
|
|
1145
|
+
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
|
1146
|
+
</div>
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
if (
|
|
1151
|
+
!finalChannelIdForHooks ||
|
|
1152
|
+
!finalServerIdForHooks ||
|
|
1153
|
+
(chatType === ChannelType.DM && !targetAgentData)
|
|
1154
|
+
) {
|
|
1155
|
+
return (
|
|
1156
|
+
<div className="flex flex-1 justify-center items-center">
|
|
1157
|
+
<p>Loading chat context...</p>
|
|
1158
|
+
</div>
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
const onDeleteAgent = () => {
|
|
1163
|
+
if (isDeletingAgent) return;
|
|
1164
|
+
confirm(
|
|
1165
|
+
{
|
|
1166
|
+
title: 'Delete Agent',
|
|
1167
|
+
description: `Are you sure you want to delete the agent "${targetAgentData?.name}"? This action cannot be undone.`,
|
|
1168
|
+
confirmText: 'Delete',
|
|
1169
|
+
variant: 'destructive',
|
|
1170
|
+
},
|
|
1171
|
+
handleDeleteAgent
|
|
1172
|
+
);
|
|
1173
|
+
};
|
|
1174
|
+
|
|
1175
|
+
// Chat header
|
|
1176
|
+
const renderChatHeader = () => {
|
|
1177
|
+
if (chatType === ChannelType.DM && targetAgentData) {
|
|
1178
|
+
return (
|
|
1179
|
+
<div className="flex items-center justify-between mb-4 p-3">
|
|
1180
|
+
<div className="flex items-center gap-3 min-w-0 flex-1">
|
|
1181
|
+
<DropdownMenu>
|
|
1182
|
+
<DropdownMenuTrigger asChild>
|
|
1183
|
+
<Button variant="ghost" size="sm" className="py-6 px-2 flex-shrink-0">
|
|
1184
|
+
<div className="relative flex-shrink-0">
|
|
1185
|
+
<Avatar className="size-4 sm:size-10 border rounded-full">
|
|
1186
|
+
<AvatarImage src={getAgentAvatar(targetAgentData)} />
|
|
1187
|
+
</Avatar>
|
|
1188
|
+
{targetAgentData?.status === AgentStatus.ACTIVE ? (
|
|
1189
|
+
<Tooltip>
|
|
1190
|
+
<TooltipTrigger asChild>
|
|
1191
|
+
<span className="absolute bottom-0 right-0 size-2 sm:size-[10px] rounded-full border border-white bg-green-500" />
|
|
1192
|
+
</TooltipTrigger>
|
|
1193
|
+
<TooltipContent side="right">
|
|
1194
|
+
<p>Agent is active</p>
|
|
1195
|
+
</TooltipContent>
|
|
1196
|
+
</Tooltip>
|
|
1197
|
+
) : (
|
|
1198
|
+
<Tooltip>
|
|
1199
|
+
<TooltipTrigger asChild>
|
|
1200
|
+
<span className="absolute bottom-0 right-0 size-2 sm:size-[10px] rounded-full border border-white bg-muted-foreground" />
|
|
1201
|
+
</TooltipTrigger>
|
|
1202
|
+
<TooltipContent side="right">
|
|
1203
|
+
<p>Agent is inactive</p>
|
|
1204
|
+
</TooltipContent>
|
|
1205
|
+
</Tooltip>
|
|
1206
|
+
)}
|
|
1207
|
+
</div>
|
|
1208
|
+
<div>
|
|
1209
|
+
<h2 className="font-semibold text-lg truncate max-w-[80px] sm:max-w-none">
|
|
1210
|
+
{targetAgentData?.name || 'Agent'}
|
|
1211
|
+
</h2>
|
|
1212
|
+
</div>
|
|
1213
|
+
<ChevronDown className="size-4" />
|
|
1214
|
+
</Button>
|
|
1215
|
+
</DropdownMenuTrigger>
|
|
1216
|
+
|
|
1217
|
+
<DropdownMenuContent
|
|
1218
|
+
align="start"
|
|
1219
|
+
side="bottom"
|
|
1220
|
+
className="min-w-36 w-[var(--radix-dropdown-menu-trigger-width)]"
|
|
1221
|
+
>
|
|
1222
|
+
<DropdownMenuItem
|
|
1223
|
+
onClick={() => {
|
|
1224
|
+
exportCharacterAsJson(targetAgentData, toast);
|
|
1225
|
+
}}
|
|
1226
|
+
>
|
|
1227
|
+
<ArrowUpFromLine className="h-4 w-4 mr-2" />
|
|
1228
|
+
Export
|
|
1229
|
+
</DropdownMenuItem>
|
|
1230
|
+
<DropdownMenuSeparator />
|
|
1231
|
+
|
|
1232
|
+
<DropdownMenuItem
|
|
1233
|
+
onClick={() => {
|
|
1234
|
+
if (targetAgentData && !isAgentStopping(targetAgentData.id)) {
|
|
1235
|
+
stopAgent(targetAgentData);
|
|
1236
|
+
}
|
|
1237
|
+
}}
|
|
1238
|
+
>
|
|
1239
|
+
<StopCircle className="h-4 w-4 mr-2" />
|
|
1240
|
+
Stop Agent
|
|
1241
|
+
</DropdownMenuItem>
|
|
1242
|
+
|
|
1243
|
+
<DropdownMenuItem
|
|
1244
|
+
onClick={() => {
|
|
1245
|
+
if (targetAgentData && !isDeletingAgent) {
|
|
1246
|
+
onDeleteAgent();
|
|
1247
|
+
}
|
|
1248
|
+
}}
|
|
1249
|
+
className="text-destructive focus:text-destructive hover:bg-red-50 dark:hover:bg-red-950/50"
|
|
1250
|
+
>
|
|
1251
|
+
<Trash className="h-4 w-4 mr-2" />
|
|
1252
|
+
Delete Agent
|
|
1253
|
+
</DropdownMenuItem>
|
|
1254
|
+
</DropdownMenuContent>
|
|
1255
|
+
</DropdownMenu>
|
|
1256
|
+
</div>
|
|
1257
|
+
|
|
1258
|
+
<div className="flex gap-1 sm:gap-2 items-center flex-shrink-0">
|
|
1259
|
+
{chatType === ChannelType.DM && (
|
|
1260
|
+
<div className="flex items-center gap-2">
|
|
1261
|
+
{agentDmChannels.length > 0 && (
|
|
1262
|
+
<DropdownMenu>
|
|
1263
|
+
<DropdownMenuTrigger asChild>
|
|
1264
|
+
<Button
|
|
1265
|
+
variant="outline"
|
|
1266
|
+
size="sm"
|
|
1267
|
+
className="w-8 h-9 sm:max-w-[300px] sm:w-auto rounded-[12px]"
|
|
1268
|
+
>
|
|
1269
|
+
<Clock className="size-4 flex-shrink-0 text-muted-foreground" />
|
|
1270
|
+
<span className="hidden md:inline truncate text-xs sm:text-sm">
|
|
1271
|
+
{agentDmChannels.find((c) => c.id === chatState.currentDmChannelId)
|
|
1272
|
+
?.name || 'Select Chat'}
|
|
1273
|
+
</span>
|
|
1274
|
+
<ChevronDown className="hidden md:inline-flex size-4 text-muted-foreground" />
|
|
1275
|
+
</Button>
|
|
1276
|
+
</DropdownMenuTrigger>
|
|
1277
|
+
<DropdownMenuContent align="end" className="w-[280px] sm:w-[320px]">
|
|
1278
|
+
<DropdownMenuLabel className="font-medium">
|
|
1279
|
+
Chat History with {targetAgentData.name}
|
|
1280
|
+
</DropdownMenuLabel>
|
|
1281
|
+
<DropdownMenuSeparator />
|
|
1282
|
+
<div className="max-h-[300px] overflow-y-auto">
|
|
1283
|
+
{agentDmChannels.map((channel) => (
|
|
1284
|
+
<DropdownMenuItem
|
|
1285
|
+
key={channel.id}
|
|
1286
|
+
onClick={() => handleSelectDmRoom(channel.id)}
|
|
1287
|
+
className={cn(
|
|
1288
|
+
'cursor-pointer',
|
|
1289
|
+
channel.id === chatState.currentDmChannelId && 'bg-muted'
|
|
1290
|
+
)}
|
|
1291
|
+
>
|
|
1292
|
+
<div className="flex items-center justify-between w-full">
|
|
1293
|
+
<div className="flex flex-col min-w-0 flex-1">
|
|
1294
|
+
<span
|
|
1295
|
+
className={cn(
|
|
1296
|
+
'text-sm truncate',
|
|
1297
|
+
channel.id === chatState.currentDmChannelId && 'font-medium'
|
|
1298
|
+
)}
|
|
1299
|
+
>
|
|
1300
|
+
{channel.name}
|
|
1301
|
+
</span>
|
|
1302
|
+
<span className="text-xs text-muted-foreground">
|
|
1303
|
+
{moment(
|
|
1304
|
+
(typeof channel.metadata?.createdAt === 'string' ||
|
|
1305
|
+
typeof channel.metadata?.createdAt === 'number'
|
|
1306
|
+
? channel.metadata.createdAt
|
|
1307
|
+
: null) ||
|
|
1308
|
+
channel.updatedAt ||
|
|
1309
|
+
channel.createdAt
|
|
1310
|
+
).fromNow()}
|
|
1311
|
+
</span>
|
|
1312
|
+
</div>
|
|
1313
|
+
{channel.id === chatState.currentDmChannelId && (
|
|
1314
|
+
<Badge variant="default" className="text-xs flex-shrink-0 ml-2">
|
|
1315
|
+
Current
|
|
1316
|
+
</Badge>
|
|
1317
|
+
)}
|
|
1318
|
+
</div>
|
|
1319
|
+
</DropdownMenuItem>
|
|
1320
|
+
))}
|
|
1321
|
+
</div>
|
|
1322
|
+
</DropdownMenuContent>
|
|
1323
|
+
</DropdownMenu>
|
|
1324
|
+
)}
|
|
1325
|
+
|
|
1326
|
+
{/* Chat Actions Split Button */}
|
|
1327
|
+
<SplitButton
|
|
1328
|
+
mainAction={{
|
|
1329
|
+
label: chatState.isCreatingDM ? (
|
|
1330
|
+
'Creating...'
|
|
1331
|
+
) : (
|
|
1332
|
+
<>
|
|
1333
|
+
<span className="sm:hidden">New</span>
|
|
1334
|
+
<span className="hidden sm:inline">New Chat</span>
|
|
1335
|
+
</>
|
|
1336
|
+
),
|
|
1337
|
+
onClick: () => handleNewDmChannel(targetAgentData?.id),
|
|
1338
|
+
icon: chatState.isCreatingDM ? (
|
|
1339
|
+
<Loader2 className="size-4 animate-spin" />
|
|
1340
|
+
) : (
|
|
1341
|
+
<Plus className="size-4" />
|
|
1342
|
+
),
|
|
1343
|
+
disabled: chatState.isCreatingDM || isLoadingAgentDmChannels,
|
|
1344
|
+
}}
|
|
1345
|
+
actions={[
|
|
1346
|
+
{
|
|
1347
|
+
label: 'Clear Chat',
|
|
1348
|
+
onClick: handleClearChat,
|
|
1349
|
+
icon: <Eraser className="size-4" />,
|
|
1350
|
+
disabled: !messages || messages.length === 0,
|
|
1351
|
+
},
|
|
1352
|
+
{
|
|
1353
|
+
label: 'Delete Chat',
|
|
1354
|
+
onClick: handleDeleteCurrentDmChannel,
|
|
1355
|
+
icon: <Trash2 className="size-4" />,
|
|
1356
|
+
disabled: !chatState.currentDmChannelId,
|
|
1357
|
+
variant: 'destructive',
|
|
1358
|
+
},
|
|
1359
|
+
]}
|
|
1360
|
+
variant="outline"
|
|
1361
|
+
size="sm"
|
|
1362
|
+
mainButtonClassName="rounded-l-[12px] h-9"
|
|
1363
|
+
dropdownButtonClassName="rounded-r-[12px] h-9"
|
|
1364
|
+
/>
|
|
1365
|
+
|
|
1366
|
+
<Tooltip>
|
|
1367
|
+
<TooltipTrigger asChild>
|
|
1368
|
+
<Button
|
|
1369
|
+
className="w-9 h-9 rounded-[12px]"
|
|
1370
|
+
variant={'outline'}
|
|
1371
|
+
onClick={toggleSidebar}
|
|
1372
|
+
>
|
|
1373
|
+
{showSidebar ? (
|
|
1374
|
+
<PanelRightClose className="size-4" />
|
|
1375
|
+
) : (
|
|
1376
|
+
<PanelRight className="size-4" />
|
|
1377
|
+
)}
|
|
1378
|
+
</Button>
|
|
1379
|
+
</TooltipTrigger>
|
|
1380
|
+
<TooltipContent side="bottom">
|
|
1381
|
+
<p>{showSidebar ? 'Close SidePanel' : 'Open SidePanel'}</p>
|
|
1382
|
+
</TooltipContent>
|
|
1383
|
+
</Tooltip>
|
|
1384
|
+
</div>
|
|
1385
|
+
)}
|
|
1386
|
+
</div>
|
|
1387
|
+
</div>
|
|
1388
|
+
);
|
|
1389
|
+
} else if (chatType === ChannelType.GROUP) {
|
|
1390
|
+
const groupDisplayName = generateGroupName(
|
|
1391
|
+
channelDetailsData?.data || undefined,
|
|
1392
|
+
groupAgents,
|
|
1393
|
+
currentClientEntityId
|
|
1394
|
+
);
|
|
1395
|
+
|
|
1396
|
+
return (
|
|
1397
|
+
<div className="flex flex-col gap-3 mb-4">
|
|
1398
|
+
<div className="flex items-center justify-between p-3 bg-card rounded-lg border">
|
|
1399
|
+
<div className="flex items-center gap-3 min-w-0 flex-1">
|
|
1400
|
+
<h2 className="font-semibold text-lg truncate" title={groupDisplayName}>
|
|
1401
|
+
{groupDisplayName}
|
|
1402
|
+
</h2>
|
|
1403
|
+
</div>
|
|
1404
|
+
<div className="flex gap-1 sm:gap-2 items-center flex-shrink-0">
|
|
1405
|
+
{/* Group Actions Split Button */}
|
|
1406
|
+
<SplitButton
|
|
1407
|
+
mainAction={{
|
|
1408
|
+
label: 'Edit Group',
|
|
1409
|
+
onClick: () => updateChatState({ showGroupEditPanel: true }),
|
|
1410
|
+
icon: <Edit className="size-4" />,
|
|
1411
|
+
}}
|
|
1412
|
+
actions={[
|
|
1413
|
+
{
|
|
1414
|
+
label: 'Clear Messages',
|
|
1415
|
+
onClick: handleClearChat,
|
|
1416
|
+
icon: <Eraser className="size-4" />,
|
|
1417
|
+
disabled: !messages || messages.length === 0,
|
|
1418
|
+
},
|
|
1419
|
+
{
|
|
1420
|
+
label: 'Delete Group',
|
|
1421
|
+
onClick: () => {
|
|
1422
|
+
if (!finalChannelIdForHooks || !finalServerIdForHooks) return;
|
|
1423
|
+
// Capture the channel ID to use in the async callback
|
|
1424
|
+
const channelIdToDelete = finalChannelIdForHooks;
|
|
1425
|
+
confirm(
|
|
1426
|
+
{
|
|
1427
|
+
title: 'Delete Group',
|
|
1428
|
+
description:
|
|
1429
|
+
'Are you sure you want to delete this group? This action cannot be undone.',
|
|
1430
|
+
confirmText: 'Delete',
|
|
1431
|
+
variant: 'destructive',
|
|
1432
|
+
},
|
|
1433
|
+
async () => {
|
|
1434
|
+
try {
|
|
1435
|
+
const elizaClient = createElizaClient();
|
|
1436
|
+
await elizaClient.messaging.deleteChannel(channelIdToDelete);
|
|
1437
|
+
toast({
|
|
1438
|
+
title: 'Group Deleted',
|
|
1439
|
+
description: 'The group has been successfully deleted.',
|
|
1440
|
+
});
|
|
1441
|
+
// Navigate back to home after deletion
|
|
1442
|
+
window.location.href = '/';
|
|
1443
|
+
} catch (error) {
|
|
1444
|
+
clientLogger.error('[Chat] Error deleting group:', error);
|
|
1445
|
+
toast({
|
|
1446
|
+
title: 'Error',
|
|
1447
|
+
description: 'Could not delete group.',
|
|
1448
|
+
variant: 'destructive',
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
);
|
|
1453
|
+
},
|
|
1454
|
+
icon: <Trash2 className="size-4" />,
|
|
1455
|
+
disabled: !finalChannelIdForHooks || !finalServerIdForHooks,
|
|
1456
|
+
variant: 'destructive',
|
|
1457
|
+
},
|
|
1458
|
+
]}
|
|
1459
|
+
variant="outline"
|
|
1460
|
+
size="sm"
|
|
1461
|
+
className="px-2 sm:px-3"
|
|
1462
|
+
/>
|
|
1463
|
+
<Button
|
|
1464
|
+
variant="ghost"
|
|
1465
|
+
size="sm"
|
|
1466
|
+
className="px-2 sm:px-3 h-8 w-8 sm:w-auto"
|
|
1467
|
+
onClick={toggleSidebar}
|
|
1468
|
+
>
|
|
1469
|
+
{showSidebar ? (
|
|
1470
|
+
<PanelRightClose className="h-4 w-4" />
|
|
1471
|
+
) : (
|
|
1472
|
+
<PanelRight className="h-4 w-4" />
|
|
1473
|
+
)}
|
|
1474
|
+
</Button>
|
|
1475
|
+
</div>
|
|
1476
|
+
</div>
|
|
1477
|
+
|
|
1478
|
+
{groupAgents.length > 0 && (
|
|
1479
|
+
<div className="flex items-center gap-2 p-2 bg-card rounded-lg border overflow-x-auto">
|
|
1480
|
+
<span className="text-sm text-muted-foreground whitespace-nowrap flex-shrink-0">
|
|
1481
|
+
Agents:
|
|
1482
|
+
</span>
|
|
1483
|
+
<div className="flex gap-2 min-w-0">
|
|
1484
|
+
<Button
|
|
1485
|
+
variant={!chatState.selectedGroupAgentId ? 'default' : 'ghost'}
|
|
1486
|
+
size="sm"
|
|
1487
|
+
onClick={() => updateChatState({ selectedGroupAgentId: null })}
|
|
1488
|
+
className="flex items-center gap-2 flex-shrink-0"
|
|
1489
|
+
>
|
|
1490
|
+
<span>All</span>
|
|
1491
|
+
</Button>
|
|
1492
|
+
{groupAgents.map((agent) => (
|
|
1493
|
+
<Button
|
|
1494
|
+
key={agent?.id}
|
|
1495
|
+
variant={chatState.selectedGroupAgentId === agent?.id ? 'default' : 'ghost'}
|
|
1496
|
+
size="sm"
|
|
1497
|
+
onClick={() => {
|
|
1498
|
+
updateChatState({
|
|
1499
|
+
selectedGroupAgentId: agent?.id || null,
|
|
1500
|
+
});
|
|
1501
|
+
if (agent?.id && !showSidebar) {
|
|
1502
|
+
setSidebarVisible(true);
|
|
1503
|
+
}
|
|
1504
|
+
}}
|
|
1505
|
+
className="flex items-center gap-2 flex-shrink-0"
|
|
1506
|
+
>
|
|
1507
|
+
<Avatar className="size-5">
|
|
1508
|
+
<AvatarImage src={getAgentAvatar(agent)} />
|
|
1509
|
+
</Avatar>
|
|
1510
|
+
<span className="truncate max-w-[100px] sm:max-w-none">{agent?.name}</span>
|
|
1511
|
+
</Button>
|
|
1512
|
+
))}
|
|
1513
|
+
</div>
|
|
1514
|
+
</div>
|
|
1515
|
+
)}
|
|
1516
|
+
</div>
|
|
1517
|
+
);
|
|
1518
|
+
}
|
|
1519
|
+
return null;
|
|
1520
|
+
};
|
|
1521
|
+
|
|
1522
|
+
return (
|
|
1523
|
+
<>
|
|
1524
|
+
<div className="h-full flex flex-col relative overflow-hidden">
|
|
1525
|
+
{/* Conditional layout based on floating mode */}
|
|
1526
|
+
{isFloatingMode ? (
|
|
1527
|
+
/* Single panel layout for floating mode */
|
|
1528
|
+
<div className="h-full flex flex-col overflow-hidden">
|
|
1529
|
+
<div className="flex-shrink-0 p-2 sm:p-4 pb-0">{renderChatHeader()}</div>
|
|
1530
|
+
|
|
1531
|
+
<div
|
|
1532
|
+
className={cn(
|
|
1533
|
+
'flex flex-col transition-all duration-300 w-full flex-1 min-h-0 overflow-hidden p-2 sm:p-4 pt-0'
|
|
1534
|
+
)}
|
|
1535
|
+
>
|
|
1536
|
+
<div className="flex-1 min-h-0 overflow-hidden px-5 py-2">
|
|
1537
|
+
<ChatMessageListComponent
|
|
1538
|
+
messages={messages}
|
|
1539
|
+
isLoadingMessages={isLoadingMessages}
|
|
1540
|
+
chatType={chatType}
|
|
1541
|
+
currentClientEntityId={currentClientEntityId}
|
|
1542
|
+
targetAgentData={targetAgentData}
|
|
1543
|
+
allAgents={allAgents}
|
|
1544
|
+
animatedMessageId={animatedMessageId}
|
|
1545
|
+
scrollRef={scrollRef as unknown as React.RefObject<HTMLDivElement>}
|
|
1546
|
+
contentRef={contentRef as unknown as React.RefObject<HTMLDivElement>}
|
|
1547
|
+
isAtBottom={isAtBottom}
|
|
1548
|
+
scrollToBottom={scrollToBottom}
|
|
1549
|
+
disableAutoScroll={disableAutoScroll}
|
|
1550
|
+
finalChannelId={finalChannelIdForHooks}
|
|
1551
|
+
getAgentInMessage={getAgentInMessage}
|
|
1552
|
+
agentAvatarMap={agentAvatarMap}
|
|
1553
|
+
onDeleteMessage={handleDeleteMessage}
|
|
1554
|
+
onRetryMessage={(messageText) => {
|
|
1555
|
+
// Ensure we have required IDs before retrying
|
|
1556
|
+
if (!finalChannelIdForHooks) {
|
|
1557
|
+
toast({
|
|
1558
|
+
title: 'Error',
|
|
1559
|
+
description: 'Cannot retry message: missing channel information.',
|
|
1560
|
+
variant: 'destructive',
|
|
1561
|
+
});
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
const message: UiMessage = {
|
|
1565
|
+
id: randomUUID() as UUID,
|
|
1566
|
+
text: messageText,
|
|
1567
|
+
name: USER_NAME,
|
|
1568
|
+
senderId: currentClientEntityId as UUID,
|
|
1569
|
+
isAgent: false,
|
|
1570
|
+
createdAt: Date.now(),
|
|
1571
|
+
channelId: finalChannelIdForHooks,
|
|
1572
|
+
serverId: finalServerIdForHooks,
|
|
1573
|
+
};
|
|
1574
|
+
handleRetryMessage(message);
|
|
1575
|
+
}}
|
|
1576
|
+
selectedGroupAgentId={chatState.selectedGroupAgentId}
|
|
1577
|
+
/>
|
|
1578
|
+
</div>
|
|
1579
|
+
|
|
1580
|
+
<div className="flex-shrink-0">
|
|
1581
|
+
<ChatInputArea
|
|
1582
|
+
input={chatState.input}
|
|
1583
|
+
setInput={(value) => updateChatState({ input: value })}
|
|
1584
|
+
inputDisabled={chatState.inputDisabled}
|
|
1585
|
+
selectedFiles={selectedFiles}
|
|
1586
|
+
removeFile={removeFile}
|
|
1587
|
+
handleFileChange={handleFileChange}
|
|
1588
|
+
handleSendMessage={handleSendMessage}
|
|
1589
|
+
handleKeyDown={handleKeyDown}
|
|
1590
|
+
chatType={chatType}
|
|
1591
|
+
targetAgentData={targetAgentData}
|
|
1592
|
+
formRef={formRef}
|
|
1593
|
+
inputRef={inputRef}
|
|
1594
|
+
fileInputRef={fileInputRef}
|
|
1595
|
+
/>
|
|
1596
|
+
</div>
|
|
1597
|
+
</div>
|
|
1598
|
+
</div>
|
|
1599
|
+
) : (
|
|
1600
|
+
/* Resizable panel layout for desktop mode */
|
|
1601
|
+
<ResizablePanelGroup
|
|
1602
|
+
direction="horizontal"
|
|
1603
|
+
className="h-full flex-1 overflow-hidden"
|
|
1604
|
+
onLayout={(sizes) => {
|
|
1605
|
+
if (sizes.length >= 2 && showSidebar && !chatState.isMobile) {
|
|
1606
|
+
setMainPanelSize(sizes[0]);
|
|
1607
|
+
setSidebarPanelSize(sizes[1]);
|
|
1608
|
+
}
|
|
1609
|
+
}}
|
|
1610
|
+
>
|
|
1611
|
+
<ResizablePanel
|
|
1612
|
+
defaultSize={showSidebar && !chatState.isMobile ? mainPanelSize : 100}
|
|
1613
|
+
minSize={chatState.isMobile ? 100 : 50}
|
|
1614
|
+
>
|
|
1615
|
+
<div className="relative h-full overflow-hidden">
|
|
1616
|
+
{/* Main chat content */}
|
|
1617
|
+
<div className="h-full flex flex-col overflow-hidden">
|
|
1618
|
+
<div className="flex-shrink-0 p-2 sm:p-4 pb-0">{renderChatHeader()}</div>
|
|
1619
|
+
|
|
1620
|
+
<div
|
|
1621
|
+
className={cn(
|
|
1622
|
+
'flex flex-col transition-all duration-300 w-full flex-1 min-h-0 overflow-hidden p-2 sm:p-4 pt-0'
|
|
1623
|
+
)}
|
|
1624
|
+
>
|
|
1625
|
+
<div className="flex-1 min-h-0 overflow-hidden px-5 py-2">
|
|
1626
|
+
<ChatMessageListComponent
|
|
1627
|
+
messages={messages}
|
|
1628
|
+
isLoadingMessages={isLoadingMessages}
|
|
1629
|
+
chatType={chatType}
|
|
1630
|
+
currentClientEntityId={currentClientEntityId}
|
|
1631
|
+
targetAgentData={targetAgentData}
|
|
1632
|
+
allAgents={allAgents}
|
|
1633
|
+
animatedMessageId={animatedMessageId}
|
|
1634
|
+
scrollRef={scrollRef as unknown as React.RefObject<HTMLDivElement>}
|
|
1635
|
+
contentRef={contentRef as unknown as React.RefObject<HTMLDivElement>}
|
|
1636
|
+
isAtBottom={isAtBottom}
|
|
1637
|
+
scrollToBottom={scrollToBottom}
|
|
1638
|
+
disableAutoScroll={disableAutoScroll}
|
|
1639
|
+
finalChannelId={finalChannelIdForHooks}
|
|
1640
|
+
getAgentInMessage={getAgentInMessage}
|
|
1641
|
+
agentAvatarMap={agentAvatarMap}
|
|
1642
|
+
onDeleteMessage={handleDeleteMessage}
|
|
1643
|
+
onRetryMessage={(messageText) => {
|
|
1644
|
+
// Ensure we have required IDs before retrying
|
|
1645
|
+
if (!finalChannelIdForHooks) {
|
|
1646
|
+
toast({
|
|
1647
|
+
title: 'Error',
|
|
1648
|
+
description: 'Cannot retry message: missing channel information.',
|
|
1649
|
+
variant: 'destructive',
|
|
1650
|
+
});
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
const message: UiMessage = {
|
|
1654
|
+
id: randomUUID() as UUID,
|
|
1655
|
+
text: messageText,
|
|
1656
|
+
name: USER_NAME,
|
|
1657
|
+
senderId: currentClientEntityId as UUID,
|
|
1658
|
+
isAgent: false,
|
|
1659
|
+
createdAt: Date.now(),
|
|
1660
|
+
channelId: finalChannelIdForHooks,
|
|
1661
|
+
serverId: finalServerIdForHooks,
|
|
1662
|
+
};
|
|
1663
|
+
handleRetryMessage(message);
|
|
1664
|
+
}}
|
|
1665
|
+
selectedGroupAgentId={chatState.selectedGroupAgentId}
|
|
1666
|
+
/>
|
|
1667
|
+
</div>
|
|
1668
|
+
|
|
1669
|
+
<div className="flex-shrink-0">
|
|
1670
|
+
<ChatInputArea
|
|
1671
|
+
input={chatState.input}
|
|
1672
|
+
setInput={(value) => updateChatState({ input: value })}
|
|
1673
|
+
inputDisabled={chatState.inputDisabled}
|
|
1674
|
+
selectedFiles={selectedFiles}
|
|
1675
|
+
removeFile={removeFile}
|
|
1676
|
+
handleFileChange={handleFileChange}
|
|
1677
|
+
handleSendMessage={handleSendMessage}
|
|
1678
|
+
handleKeyDown={handleKeyDown}
|
|
1679
|
+
chatType={chatType}
|
|
1680
|
+
targetAgentData={targetAgentData}
|
|
1681
|
+
formRef={formRef}
|
|
1682
|
+
inputRef={inputRef}
|
|
1683
|
+
fileInputRef={fileInputRef}
|
|
1684
|
+
/>
|
|
1685
|
+
</div>
|
|
1686
|
+
</div>
|
|
1687
|
+
</div>
|
|
1688
|
+
</div>
|
|
1689
|
+
</ResizablePanel>
|
|
1690
|
+
|
|
1691
|
+
{/* Right panel / sidebar */}
|
|
1692
|
+
{(() => {
|
|
1693
|
+
let sidebarAgentId: UUID | undefined = undefined;
|
|
1694
|
+
let sidebarAgentName: string = 'Agent';
|
|
1695
|
+
let sidebarChannelId: UUID | undefined = undefined;
|
|
1696
|
+
|
|
1697
|
+
if (chatType === ChannelType.DM) {
|
|
1698
|
+
sidebarAgentId = contextId; // This is agentId for DM
|
|
1699
|
+
sidebarAgentName = targetAgentData?.name || 'Agent';
|
|
1700
|
+
sidebarChannelId = chatState.currentDmChannelId || undefined;
|
|
1701
|
+
} else if (chatType === ChannelType.GROUP && chatState.selectedGroupAgentId) {
|
|
1702
|
+
sidebarAgentId = chatState.selectedGroupAgentId;
|
|
1703
|
+
const selectedAgent = allAgents.find(
|
|
1704
|
+
(a) => a.id === chatState.selectedGroupAgentId
|
|
1705
|
+
);
|
|
1706
|
+
sidebarAgentName = selectedAgent?.name || 'Group Member';
|
|
1707
|
+
sidebarChannelId = contextId; // contextId is the channelId for GROUP
|
|
1708
|
+
} else if (chatType === ChannelType.GROUP && !chatState.selectedGroupAgentId) {
|
|
1709
|
+
sidebarAgentName = 'Group';
|
|
1710
|
+
sidebarChannelId = contextId; // contextId is the channelId for GROUP
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
return (
|
|
1714
|
+
showSidebar &&
|
|
1715
|
+
!chatState.isMobile && (
|
|
1716
|
+
<>
|
|
1717
|
+
<ResizableHandle withHandle />
|
|
1718
|
+
<ResizablePanel defaultSize={sidebarPanelSize} minSize={20} maxSize={50}>
|
|
1719
|
+
<AgentSidebar
|
|
1720
|
+
agentId={sidebarAgentId}
|
|
1721
|
+
agentName={sidebarAgentName}
|
|
1722
|
+
channelId={sidebarChannelId}
|
|
1723
|
+
/>
|
|
1724
|
+
</ResizablePanel>
|
|
1725
|
+
</>
|
|
1726
|
+
)
|
|
1727
|
+
);
|
|
1728
|
+
})()}
|
|
1729
|
+
</ResizablePanelGroup>
|
|
1730
|
+
)}
|
|
1731
|
+
|
|
1732
|
+
{/* Floating sidebar overlay for narrow screens */}
|
|
1733
|
+
{(() => {
|
|
1734
|
+
let sidebarAgentId: UUID | undefined = undefined;
|
|
1735
|
+
let sidebarAgentName: string = 'Agent';
|
|
1736
|
+
let sidebarChannelId: UUID | undefined = undefined;
|
|
1737
|
+
|
|
1738
|
+
if (chatType === ChannelType.DM) {
|
|
1739
|
+
sidebarAgentId = contextId; // This is agentId for DM
|
|
1740
|
+
sidebarAgentName = targetAgentData?.name || 'Agent';
|
|
1741
|
+
sidebarChannelId = chatState.currentDmChannelId || undefined;
|
|
1742
|
+
} else if (chatType === ChannelType.GROUP && chatState.selectedGroupAgentId) {
|
|
1743
|
+
sidebarAgentId = chatState.selectedGroupAgentId;
|
|
1744
|
+
const selectedAgent = allAgents.find((a) => a.id === chatState.selectedGroupAgentId);
|
|
1745
|
+
sidebarAgentName = selectedAgent?.name || 'Group Member';
|
|
1746
|
+
sidebarChannelId = contextId; // contextId is the channelId for GROUP
|
|
1747
|
+
} else if (chatType === ChannelType.GROUP && !chatState.selectedGroupAgentId) {
|
|
1748
|
+
sidebarAgentName = 'Group';
|
|
1749
|
+
sidebarChannelId = contextId; // contextId is the channelId for GROUP
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
return (
|
|
1753
|
+
showSidebar &&
|
|
1754
|
+
isFloatingMode && (
|
|
1755
|
+
<div className="absolute inset-0 z-50 bg-background/80 backdrop-blur-sm">
|
|
1756
|
+
<div className="absolute inset-0 bg-background shadow-lg">
|
|
1757
|
+
<div className="h-full flex flex-col">
|
|
1758
|
+
{/* Close button for floating sidebar */}
|
|
1759
|
+
<div className="flex items-center justify-between p-4 border-b">
|
|
1760
|
+
<h3 className="font-semibold text-lg">{sidebarAgentName}</h3>
|
|
1761
|
+
<Button
|
|
1762
|
+
variant="ghost"
|
|
1763
|
+
size="sm"
|
|
1764
|
+
onClick={() => setSidebarVisible(false)}
|
|
1765
|
+
className="h-8 w-8 p-0"
|
|
1766
|
+
>
|
|
1767
|
+
<PanelRightClose className="h-4 w-4" />
|
|
1768
|
+
</Button>
|
|
1769
|
+
</div>
|
|
1770
|
+
<div className="flex-1 overflow-hidden">
|
|
1771
|
+
<AgentSidebar
|
|
1772
|
+
agentId={sidebarAgentId}
|
|
1773
|
+
agentName={sidebarAgentName}
|
|
1774
|
+
channelId={sidebarChannelId}
|
|
1775
|
+
/>
|
|
1776
|
+
</div>
|
|
1777
|
+
</div>
|
|
1778
|
+
</div>
|
|
1779
|
+
</div>
|
|
1780
|
+
)
|
|
1781
|
+
);
|
|
1782
|
+
})()}
|
|
1783
|
+
</div>
|
|
1784
|
+
|
|
1785
|
+
{chatState.showGroupEditPanel && chatType === ChannelType.GROUP && (
|
|
1786
|
+
<GroupPanel
|
|
1787
|
+
onClose={() => updateChatState({ showGroupEditPanel: false })}
|
|
1788
|
+
channelId={contextId}
|
|
1789
|
+
/>
|
|
1790
|
+
)}
|
|
1791
|
+
|
|
1792
|
+
{chatState.showProfileOverlay && chatType === ChannelType.DM && targetAgentData?.id && (
|
|
1793
|
+
<ProfileOverlay
|
|
1794
|
+
isOpen={chatState.showProfileOverlay}
|
|
1795
|
+
onClose={() => updateChatState({ showProfileOverlay: false })}
|
|
1796
|
+
agentId={targetAgentData.id}
|
|
1797
|
+
/>
|
|
1798
|
+
)}
|
|
1799
|
+
|
|
1800
|
+
{/* Confirmation Dialogs */}
|
|
1801
|
+
<ConfirmationDialog
|
|
1802
|
+
open={isOpen}
|
|
1803
|
+
onOpenChange={onOpenChange}
|
|
1804
|
+
title={options?.title || ''}
|
|
1805
|
+
description={options?.description || ''}
|
|
1806
|
+
confirmText={options?.confirmText}
|
|
1807
|
+
cancelText={options?.cancelText}
|
|
1808
|
+
variant={options?.variant}
|
|
1809
|
+
onConfirm={onConfirm}
|
|
1810
|
+
/>
|
|
1811
|
+
</>
|
|
1812
|
+
);
|
|
1813
|
+
}
|