@elizaos/client 1.5.5-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +350 -0
  3. package/dist/assets/empty-module-CLMscLYw.js +1 -0
  4. package/dist/assets/main-BBZ_3lkn.css +5999 -0
  5. package/dist/assets/main-C5zNUkXH.js +7 -0
  6. package/dist/assets/main-Dz64ENQg.js +614 -0
  7. package/dist/assets/react-vendor-DM5m98rr.js +545 -0
  8. package/dist/assets/ui-vendor-BQCqNqg0.js +1 -0
  9. package/dist/elizaos-avatar.png +0 -0
  10. package/dist/elizaos-icon.png +0 -0
  11. package/dist/elizaos-logo-light.png +0 -0
  12. package/dist/elizaos.webp +0 -0
  13. package/dist/favicon.ico +0 -0
  14. package/dist/images/agents/agent1.png +0 -0
  15. package/dist/images/agents/agent2.png +0 -0
  16. package/dist/images/agents/agent3.png +0 -0
  17. package/dist/images/agents/agent4.png +0 -0
  18. package/dist/images/agents/agent5.png +0 -0
  19. package/dist/index.html +14 -0
  20. package/index.html +24 -0
  21. package/package.json +159 -0
  22. package/postcss.config.js +3 -0
  23. package/public/elizaos-avatar.png +0 -0
  24. package/public/elizaos-icon.png +0 -0
  25. package/public/elizaos-logo-light.png +0 -0
  26. package/public/elizaos.webp +0 -0
  27. package/public/favicon.ico +0 -0
  28. package/public/images/agents/agent1.png +0 -0
  29. package/public/images/agents/agent2.png +0 -0
  30. package/public/images/agents/agent3.png +0 -0
  31. package/public/images/agents/agent4.png +0 -0
  32. package/public/images/agents/agent5.png +0 -0
  33. package/src/App.tsx +222 -0
  34. package/src/components/AgentDetailsPanel.tsx +147 -0
  35. package/src/components/ChatInputArea.tsx +196 -0
  36. package/src/components/ChatMessageListComponent.tsx +139 -0
  37. package/src/components/actionTool.tsx +186 -0
  38. package/src/components/add-agent-card.tsx +77 -0
  39. package/src/components/agent-action-viewer.tsx +816 -0
  40. package/src/components/agent-avatar-stack.tsx +121 -0
  41. package/src/components/agent-card.cy.tsx +259 -0
  42. package/src/components/agent-card.tsx +177 -0
  43. package/src/components/agent-creator.tsx +142 -0
  44. package/src/components/agent-log-viewer.tsx +645 -0
  45. package/src/components/agent-memory-edit-overlay.tsx +461 -0
  46. package/src/components/agent-memory-viewer.tsx +504 -0
  47. package/src/components/agent-settings.tsx +270 -0
  48. package/src/components/agent-sidebar.tsx +178 -0
  49. package/src/components/api-key-dialog.tsx +113 -0
  50. package/src/components/app-sidebar.tsx +685 -0
  51. package/src/components/array-input.tsx +116 -0
  52. package/src/components/audio-recorder.tsx +292 -0
  53. package/src/components/avatar-panel.tsx +141 -0
  54. package/src/components/character-form.tsx +1138 -0
  55. package/src/components/chat.tsx +1813 -0
  56. package/src/components/combobox.tsx +187 -0
  57. package/src/components/confirmation-dialog.tsx +59 -0
  58. package/src/components/connection-error-banner.tsx +101 -0
  59. package/src/components/connection-status.cy.tsx +73 -0
  60. package/src/components/connection-status.tsx +155 -0
  61. package/src/components/copy-button.tsx +35 -0
  62. package/src/components/delete-button.tsx +24 -0
  63. package/src/components/env-settings.tsx +261 -0
  64. package/src/components/group-card.tsx +160 -0
  65. package/src/components/group-panel.tsx +543 -0
  66. package/src/components/input-copy.tsx +21 -0
  67. package/src/components/logs-page.tsx +41 -0
  68. package/src/components/media-content.tsx +385 -0
  69. package/src/components/memory-graph.tsx +170 -0
  70. package/src/components/missing-secrets-dialog.tsx +72 -0
  71. package/src/components/onboarding-tour.tsx +247 -0
  72. package/src/components/page-title.tsx +8 -0
  73. package/src/components/plugins-panel.tsx +383 -0
  74. package/src/components/profile-card.tsx +66 -0
  75. package/src/components/profile-overlay.tsx +283 -0
  76. package/src/components/retry-button.tsx +28 -0
  77. package/src/components/secret-panel.tsx +1505 -0
  78. package/src/components/server-management.tsx +264 -0
  79. package/src/components/split-button.tsx +148 -0
  80. package/src/components/stop-agent-button.tsx +99 -0
  81. package/src/components/ui/alert-dialog.cy.tsx +333 -0
  82. package/src/components/ui/alert-dialog.tsx +115 -0
  83. package/src/components/ui/alert.tsx +49 -0
  84. package/src/components/ui/avatar.cy.tsx +180 -0
  85. package/src/components/ui/avatar.tsx +57 -0
  86. package/src/components/ui/badge.cy.tsx +146 -0
  87. package/src/components/ui/badge.tsx +43 -0
  88. package/src/components/ui/button.cy.tsx +177 -0
  89. package/src/components/ui/button.tsx +56 -0
  90. package/src/components/ui/card.cy.tsx +160 -0
  91. package/src/components/ui/card.tsx +73 -0
  92. package/src/components/ui/chat/animated-markdown.tsx +59 -0
  93. package/src/components/ui/chat/chat-bubble.tsx +178 -0
  94. package/src/components/ui/chat/chat-container.tsx +51 -0
  95. package/src/components/ui/chat/chat-input.cy.tsx +169 -0
  96. package/src/components/ui/chat/chat-input.tsx +47 -0
  97. package/src/components/ui/chat/chat-message-list.tsx +61 -0
  98. package/src/components/ui/chat/chat-tts-button.tsx +199 -0
  99. package/src/components/ui/chat/code-block.tsx +79 -0
  100. package/src/components/ui/chat/expandable-chat.tsx +131 -0
  101. package/src/components/ui/chat/hooks/useAutoScroll.ts +86 -0
  102. package/src/components/ui/chat/markdown.tsx +209 -0
  103. package/src/components/ui/chat/message-loading.tsx +48 -0
  104. package/src/components/ui/checkbox.cy.tsx +170 -0
  105. package/src/components/ui/checkbox.tsx +30 -0
  106. package/src/components/ui/collapsible.cy.tsx +283 -0
  107. package/src/components/ui/collapsible.tsx +9 -0
  108. package/src/components/ui/command.cy.tsx +313 -0
  109. package/src/components/ui/command.tsx +143 -0
  110. package/src/components/ui/dialog.cy.tsx +279 -0
  111. package/src/components/ui/dialog.tsx +104 -0
  112. package/src/components/ui/dropdown-menu.cy.tsx +273 -0
  113. package/src/components/ui/dropdown-menu.tsx +281 -0
  114. package/src/components/ui/input.cy.tsx +82 -0
  115. package/src/components/ui/input.tsx +27 -0
  116. package/src/components/ui/label.cy.tsx +157 -0
  117. package/src/components/ui/label.tsx +19 -0
  118. package/src/components/ui/resizable.tsx +42 -0
  119. package/src/components/ui/scroll-area.cy.tsx +242 -0
  120. package/src/components/ui/scroll-area.tsx +46 -0
  121. package/src/components/ui/select.cy.tsx +277 -0
  122. package/src/components/ui/select.tsx +155 -0
  123. package/src/components/ui/separator.cy.tsx +145 -0
  124. package/src/components/ui/separator.tsx +29 -0
  125. package/src/components/ui/sheet.cy.tsx +324 -0
  126. package/src/components/ui/sheet.tsx +119 -0
  127. package/src/components/ui/sidebar.tsx +734 -0
  128. package/src/components/ui/skeleton.cy.tsx +149 -0
  129. package/src/components/ui/skeleton.tsx +17 -0
  130. package/src/components/ui/split-button.cy.tsx +274 -0
  131. package/src/components/ui/split-button.tsx +112 -0
  132. package/src/components/ui/switch.tsx +28 -0
  133. package/src/components/ui/tabs.cy.tsx +271 -0
  134. package/src/components/ui/tabs.tsx +53 -0
  135. package/src/components/ui/textarea.cy.tsx +136 -0
  136. package/src/components/ui/textarea.tsx +26 -0
  137. package/src/components/ui/toast.cy.tsx +209 -0
  138. package/src/components/ui/toast.tsx +126 -0
  139. package/src/components/ui/toaster.tsx +29 -0
  140. package/src/components/ui/tooltip.cy.tsx +244 -0
  141. package/src/components/ui/tooltip.tsx +30 -0
  142. package/src/config/agent-templates.ts +349 -0
  143. package/src/config/voice-models.ts +181 -0
  144. package/src/constants.ts +23 -0
  145. package/src/context/AuthContext.tsx +44 -0
  146. package/src/context/ConnectionContext.tsx +194 -0
  147. package/src/entry.tsx +9 -0
  148. package/src/hooks/__tests__/use-agent-tab-state.test.ts +137 -0
  149. package/src/hooks/__tests__/use-agent-update.test.tsx +250 -0
  150. package/src/hooks/__tests__/use-character-convert.test.ts +102 -0
  151. package/src/hooks/__tests__/use-panel-width-state.test.ts +243 -0
  152. package/src/hooks/__tests__/use-sidebar-state.test.ts +117 -0
  153. package/src/hooks/use-agent-management.ts +130 -0
  154. package/src/hooks/use-agent-tab-state.ts +74 -0
  155. package/src/hooks/use-agent-update.ts +469 -0
  156. package/src/hooks/use-character-convert.ts +138 -0
  157. package/src/hooks/use-confirmation.ts +55 -0
  158. package/src/hooks/use-delete-agent.ts +123 -0
  159. package/src/hooks/use-dm-channels.ts +198 -0
  160. package/src/hooks/use-elevenlabs-voices.ts +83 -0
  161. package/src/hooks/use-file-upload.ts +224 -0
  162. package/src/hooks/use-mobile.tsx +19 -0
  163. package/src/hooks/use-onboarding.tsx +49 -0
  164. package/src/hooks/use-panel-width-state.ts +147 -0
  165. package/src/hooks/use-partial-update.ts +288 -0
  166. package/src/hooks/use-plugin-details.ts +462 -0
  167. package/src/hooks/use-plugins.ts +119 -0
  168. package/src/hooks/use-query-hooks.ts +1263 -0
  169. package/src/hooks/use-server-agents.ts +62 -0
  170. package/src/hooks/use-server-version.tsx +47 -0
  171. package/src/hooks/use-sidebar-state.ts +50 -0
  172. package/src/hooks/use-socket-chat.ts +264 -0
  173. package/src/hooks/use-toast.ts +260 -0
  174. package/src/hooks/use-version.tsx +64 -0
  175. package/src/index.css +146 -0
  176. package/src/lib/api-client-config.ts +53 -0
  177. package/src/lib/api-type-mappers.ts +196 -0
  178. package/src/lib/export-utils.ts +123 -0
  179. package/src/lib/logger.ts +19 -0
  180. package/src/lib/media-utils.ts +170 -0
  181. package/src/lib/pca.test.ts +17 -0
  182. package/src/lib/pca.ts +52 -0
  183. package/src/lib/socketio-manager.ts +664 -0
  184. package/src/lib/utils.ts +168 -0
  185. package/src/main.tsx +16 -0
  186. package/src/mocks/empty-module.ts +12 -0
  187. package/src/mocks/node-module.ts +57 -0
  188. package/src/polyfills.ts +37 -0
  189. package/src/routes/agent-detail.tsx +30 -0
  190. package/src/routes/agent-list.tsx +27 -0
  191. package/src/routes/agent-settings.tsx +48 -0
  192. package/src/routes/character-detail.tsx +52 -0
  193. package/src/routes/character-form.tsx +79 -0
  194. package/src/routes/character-list.tsx +38 -0
  195. package/src/routes/chat.tsx +128 -0
  196. package/src/routes/createAgent.tsx +13 -0
  197. package/src/routes/group-new.tsx +50 -0
  198. package/src/routes/group.tsx +29 -0
  199. package/src/routes/home.tsx +218 -0
  200. package/src/routes/not-found.tsx +71 -0
  201. package/src/test/setup.ts +154 -0
  202. package/src/types/crypto-browserify.d.ts +4 -0
  203. package/src/types/index.ts +13 -0
  204. package/src/types/rooms.ts +8 -0
  205. package/src/types.ts +84 -0
  206. package/src/vite-env.d.ts +40 -0
  207. package/tailwind.config.ts +90 -0
  208. package/tsconfig.json +10 -0
  209. package/vite.config.ts +102 -0
@@ -0,0 +1,121 @@
1
+ import { UUID } from '@elizaos/core';
2
+ import { Avatar, AvatarImage } from './ui/avatar';
3
+ import { formatAgentName } from '@/lib/utils';
4
+ import { useState } from 'react';
5
+ import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
6
+
7
+ interface AgentAvatarStackProps {
8
+ agentIds: UUID[];
9
+ agentNames: string[];
10
+ agentAvatars: Record<string, string | null>;
11
+ size?: 'sm' | 'md' | 'lg';
12
+ maxStack?: number;
13
+ showExtraTooltip?: boolean;
14
+ }
15
+
16
+ export default function AgentAvatarStack({
17
+ agentIds,
18
+ agentNames,
19
+ agentAvatars,
20
+ size = 'md',
21
+ maxStack = 2,
22
+ showExtraTooltip = false,
23
+ }: AgentAvatarStackProps) {
24
+ const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
25
+ const displayAgents = agentIds.slice(0, maxStack);
26
+ const isMultiple = displayAgents.length > 1;
27
+ const hiddenCount = agentIds.length - maxStack;
28
+ const showExtra = showExtraTooltip && agentIds.length > maxStack;
29
+
30
+ const baseSize = size === 'sm' ? 24 : size === 'lg' ? 40 : 32;
31
+ const avatarSizeClass = isMultiple
32
+ ? size === 'sm'
33
+ ? 'size-6'
34
+ : size === 'lg'
35
+ ? 'size-10'
36
+ : 'size-8'
37
+ : size === 'sm'
38
+ ? 'size-6'
39
+ : size === 'lg'
40
+ ? 'size-10'
41
+ : 'size-8';
42
+
43
+ const visibleCount = showExtra ? maxStack + 1 : maxStack;
44
+ const overlapFactor = showExtraTooltip ? 1 : 0.6;
45
+ const avatarOffset = Math.floor(baseSize * (overlapFactor / visibleCount));
46
+
47
+ const getAvatarContent = (agentId: UUID, index: number) => {
48
+ const avatarSrc = agentAvatars[agentId] || '/elizaos-icon.png';
49
+ return agentAvatars[agentId] ? (
50
+ <AvatarImage src={avatarSrc} alt="Agent avatar" />
51
+ ) : (
52
+ <div className="rounded-full bg-gray-600 w-full h-full flex-shrink-0 flex items-center justify-center">
53
+ {formatAgentName(agentNames[index])}
54
+ </div>
55
+ );
56
+ };
57
+
58
+ const handleMouseEnter = (index: number) => {
59
+ if (showExtraTooltip) {
60
+ setHoveredIndex(index);
61
+ }
62
+ };
63
+
64
+ const handleMouseLeave = () => {
65
+ setHoveredIndex(null);
66
+ };
67
+
68
+ return (
69
+ <div
70
+ className="relative flex items-center text-xs"
71
+ style={{ height: baseSize, width: baseSize }}
72
+ >
73
+ {displayAgents.length === 1 ? (
74
+ <Avatar className={`${avatarSizeClass} rounded-full overflow-hidden`}>
75
+ {getAvatarContent(displayAgents[0], 0)}
76
+ </Avatar>
77
+ ) : (
78
+ <>
79
+ {displayAgents.map((agentId, index) => (
80
+ <Avatar
81
+ onMouseEnter={() => handleMouseEnter(index)}
82
+ onMouseLeave={() => handleMouseLeave()}
83
+ key={agentId}
84
+ className={`${avatarSizeClass} rounded-full overflow-hidden absolute border border-2 border-card`}
85
+ style={{
86
+ zIndex: hoveredIndex === index ? agentIds.length + 1 : index,
87
+ left: `${(index - (visibleCount - 1) / 2) * avatarOffset}px`,
88
+ top: `${(index - (visibleCount - 1) / 2) * avatarOffset}px`,
89
+ }}
90
+ >
91
+ {getAvatarContent(agentId, index)}
92
+ </Avatar>
93
+ ))}
94
+ {showExtra && (
95
+ <Tooltip>
96
+ <TooltipTrigger asChild>
97
+ <div
98
+ className={`${avatarSizeClass} rounded-full bg-gray-500 text-foreground/60 flex items-center justify-center absolute border border-2 border-card`}
99
+ style={{
100
+ zIndex: displayAgents.length,
101
+ left: `${(displayAgents.length - (visibleCount - 1) / 2) * avatarOffset}px`,
102
+ top: `${(displayAgents.length - (visibleCount - 1) / 2) * avatarOffset}px`,
103
+ }}
104
+ >
105
+ +{hiddenCount}
106
+ </div>
107
+ </TooltipTrigger>
108
+ <TooltipContent side="bottom">
109
+ <div className="flex flex-col">
110
+ {agentNames.slice(maxStack).map((name, index) => (
111
+ <span key={index}>{name}</span>
112
+ ))}
113
+ </div>
114
+ </TooltipContent>
115
+ </Tooltip>
116
+ )}
117
+ </>
118
+ )}
119
+ </div>
120
+ );
121
+ }
@@ -0,0 +1,259 @@
1
+ /// <reference types="cypress" />
2
+ /// <reference path="../../cypress/support/types.d.ts" />
3
+
4
+ import React from 'react';
5
+ import type { AgentWithStatus } from '@/types';
6
+ import { AgentStatus } from '@elizaos/core';
7
+
8
+ // Create a minimal test component that represents AgentCard functionality
9
+ const TestAgentCard: React.FC<{
10
+ agent: Partial<AgentWithStatus>;
11
+ onChat: (agent: Partial<AgentWithStatus>) => void;
12
+ }> = ({ agent, onChat }) => {
13
+ if (!agent || !agent.id) {
14
+ return <div data-testid="agent-card-error">Agent data not available.</div>;
15
+ }
16
+
17
+ const agentName = agent.name || 'Unnamed Agent';
18
+ const isActive = agent.status === AgentStatus.ACTIVE;
19
+
20
+ return (
21
+ <div data-testid="agent-card" className="agent-card">
22
+ <div data-testid="agent-name">{agentName}</div>
23
+ <div
24
+ data-testid="status-indicator"
25
+ className={`status-dot ${isActive ? 'active' : 'inactive'}`}
26
+ />
27
+ <div data-testid="agent-status">
28
+ {agent.status === AgentStatus.ACTIVE
29
+ ? 'active'
30
+ : agent.status === AgentStatus.INACTIVE
31
+ ? 'inactive'
32
+ : 'unknown'}
33
+ </div>
34
+ {agent.settings?.avatar && (
35
+ <img
36
+ data-testid="agent-avatar"
37
+ src={
38
+ typeof agent.settings.avatar === 'string' ? agent.settings.avatar : '/elizaos-icon.png'
39
+ }
40
+ alt={agentName}
41
+ />
42
+ )}
43
+ {!agent.settings?.avatar && (
44
+ <div data-testid="agent-initials">{agentName.substring(0, 2).toUpperCase()}</div>
45
+ )}
46
+ {isActive ? (
47
+ <button data-testid="chat-button" onClick={() => onChat(agent)}>
48
+ Chat
49
+ </button>
50
+ ) : (
51
+ <button data-testid="start-button">Start</button>
52
+ )}
53
+ <button data-testid="card-button" onClick={() => onChat(agent)} className="card-clickable">
54
+ Card Click
55
+ </button>
56
+ </div>
57
+ );
58
+ };
59
+
60
+ describe('AgentCard Component', () => {
61
+ const mockAgent: Partial<AgentWithStatus> = {
62
+ id: '12345678-1234-1234-1234-123456789012',
63
+ name: 'Test Agent',
64
+ username: 'testagent',
65
+ status: AgentStatus.INACTIVE,
66
+ settings: {
67
+ avatar: 'https://example.com/avatar.png',
68
+ },
69
+ bio: 'Test agent bio',
70
+ enabled: true,
71
+ createdAt: Date.now(),
72
+ updatedAt: Date.now(),
73
+ };
74
+
75
+ const activeAgent: Partial<AgentWithStatus> = {
76
+ ...mockAgent,
77
+ status: AgentStatus.ACTIVE,
78
+ };
79
+
80
+ it('renders agent information correctly', () => {
81
+ const onChat = cy.stub();
82
+
83
+ cy.mount(<TestAgentCard agent={mockAgent} onChat={onChat} />);
84
+
85
+ // Check agent name is displayed
86
+ cy.get('[data-testid="agent-name"]').should('contain.text', 'Test Agent');
87
+
88
+ // Check status indicator exists
89
+ cy.get('[data-testid="status-indicator"]').should('exist');
90
+
91
+ // Check agent card exists
92
+ cy.get('[data-testid="agent-card"]').should('exist');
93
+ });
94
+
95
+ it('displays active agent correctly', () => {
96
+ const onChat = cy.stub();
97
+
98
+ cy.mount(<TestAgentCard agent={activeAgent} onChat={onChat} />);
99
+
100
+ // Status should show active
101
+ cy.get('[data-testid="agent-status"]').should('contain.text', 'active');
102
+
103
+ // Chat button should be visible
104
+ cy.get('[data-testid="chat-button"]').should('exist');
105
+ });
106
+
107
+ it('handles missing agent data gracefully', () => {
108
+ const onChat = cy.stub();
109
+
110
+ cy.mount(<TestAgentCard agent={{}} onChat={onChat} />);
111
+
112
+ // Should show error message
113
+ cy.get('[data-testid="agent-card-error"]').should('contain.text', 'Agent data not available');
114
+ });
115
+
116
+ it('shows start button for inactive agents', () => {
117
+ const onChat = cy.stub();
118
+
119
+ cy.mount(<TestAgentCard agent={mockAgent} onChat={onChat} />);
120
+
121
+ // Start button should be visible
122
+ cy.get('[data-testid="start-button"]').should('contain.text', 'Start');
123
+ });
124
+
125
+ it('shows chat button for active agents', () => {
126
+ const onChat = cy.stub();
127
+
128
+ cy.mount(<TestAgentCard agent={activeAgent} onChat={onChat} />);
129
+
130
+ // Chat button should be visible
131
+ cy.get('[data-testid="chat-button"]').should('contain.text', 'Chat');
132
+ });
133
+
134
+ it('handles chat button click for active agents', () => {
135
+ const onChat = cy.stub();
136
+
137
+ cy.mount(<TestAgentCard agent={activeAgent} onChat={onChat} />);
138
+
139
+ // Click chat button
140
+ cy.get('[data-testid="chat-button"]').click();
141
+
142
+ // Verify onChat was called
143
+ cy.wrap(onChat).should('have.been.calledWith', activeAgent);
144
+ });
145
+
146
+ it('navigates when card is clicked', () => {
147
+ const onChat = cy.stub();
148
+
149
+ cy.mount(<TestAgentCard agent={mockAgent} onChat={onChat} />);
150
+
151
+ // Click the card button
152
+ cy.get('[data-testid="card-button"]').click();
153
+
154
+ // Verify onChat was called
155
+ cy.wrap(onChat).should('have.been.calledWith', mockAgent);
156
+ });
157
+
158
+ it('shows agent avatar when provided', () => {
159
+ const onChat = cy.stub();
160
+
161
+ cy.mount(<TestAgentCard agent={mockAgent} onChat={onChat} />);
162
+
163
+ // Should show avatar
164
+ cy.get('[data-testid="agent-avatar"]').should('exist');
165
+ cy.get('[data-testid="agent-avatar"]').should(
166
+ 'have.attr',
167
+ 'src',
168
+ 'https://example.com/avatar.png'
169
+ );
170
+ });
171
+
172
+ it('shows initials fallback when no avatar', () => {
173
+ const onChat = cy.stub();
174
+ const agentWithoutAvatar = {
175
+ ...mockAgent,
176
+ settings: {},
177
+ };
178
+
179
+ cy.mount(<TestAgentCard agent={agentWithoutAvatar} onChat={onChat} />);
180
+
181
+ // Should show initials fallback
182
+ cy.get('[data-testid="agent-initials"]').should('contain.text', 'TE');
183
+ });
184
+
185
+ it('displays agent status correctly', () => {
186
+ const onChat = cy.stub();
187
+
188
+ cy.mount(<TestAgentCard agent={mockAgent} onChat={onChat} />);
189
+
190
+ // Check status is displayed
191
+ cy.get('[data-testid="agent-status"]').should('contain.text', 'inactive');
192
+ });
193
+
194
+ it('handles different button states', () => {
195
+ const onChat = cy.stub();
196
+
197
+ // Test inactive agent - should show start button
198
+ cy.mount(<TestAgentCard agent={mockAgent} onChat={onChat} />);
199
+ cy.get('[data-testid="start-button"]').should('exist');
200
+
201
+ // Test active agent - should show chat button
202
+ cy.mount(<TestAgentCard agent={activeAgent} onChat={onChat} />);
203
+ cy.get('[data-testid="chat-button"]').should('exist');
204
+ });
205
+
206
+ it('handles long agent names', () => {
207
+ const onChat = cy.stub();
208
+ const longNameAgent = {
209
+ ...mockAgent,
210
+ name: 'This is a very long agent name that should be displayed',
211
+ };
212
+
213
+ cy.mount(<TestAgentCard agent={longNameAgent} onChat={onChat} />);
214
+
215
+ // Check that name is displayed
216
+ cy.get('[data-testid="agent-name"]').should('contain.text', 'This is a very long agent name');
217
+ });
218
+
219
+ it('renders with proper structure', () => {
220
+ const onChat = cy.stub();
221
+
222
+ cy.mount(<TestAgentCard agent={mockAgent} onChat={onChat} />);
223
+
224
+ // Check basic structure elements exist
225
+ cy.get('[data-testid="agent-card"]').should('exist');
226
+ cy.get('[data-testid="agent-name"]').should('exist');
227
+ cy.get('[data-testid="status-indicator"]').should('exist');
228
+ cy.get('[data-testid="agent-status"]').should('exist');
229
+ });
230
+
231
+ it('handles button interactions', () => {
232
+ const onChat = cy.stub();
233
+
234
+ cy.mount(<TestAgentCard agent={activeAgent} onChat={onChat} />);
235
+
236
+ // Click chat button
237
+ cy.get('[data-testid="chat-button"]').click();
238
+ cy.wrap(onChat).should('have.been.calledWith', activeAgent);
239
+
240
+ // Click card button
241
+ cy.get('[data-testid="card-button"]').click();
242
+ cy.wrap(onChat).should('have.been.calledWith', activeAgent);
243
+ });
244
+
245
+ it('shows appropriate content for different agent states', () => {
246
+ const onChat = cy.stub();
247
+
248
+ // Test with different agent configurations
249
+ const testCases = [
250
+ { agent: mockAgent, expectedButton: 'start-button' },
251
+ { agent: activeAgent, expectedButton: 'chat-button' },
252
+ ];
253
+
254
+ testCases.forEach(({ agent, expectedButton }) => {
255
+ cy.mount(<TestAgentCard agent={agent} onChat={onChat} />);
256
+ cy.get(`[data-testid="${expectedButton}"]`).should('exist');
257
+ });
258
+ });
259
+ });
@@ -0,0 +1,177 @@
1
+ import React from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import { Card, CardContent } from '@/components/ui/card';
4
+ import { Button } from '@/components/ui/button';
5
+ import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
6
+ import { Switch } from '@/components/ui/switch';
7
+ import { formatAgentName, cn, getAgentAvatar } from '@/lib/utils';
8
+ import type { Agent } from '@elizaos/core';
9
+ import { AgentStatus as CoreAgentStatus } from '@elizaos/core';
10
+ import { Settings } from 'lucide-react';
11
+ import { useAgentManagement } from '@/hooks/use-agent-management';
12
+ import type { AgentWithStatus } from '@/types';
13
+ import clientLogger from '@/lib/logger';
14
+
15
+ interface AgentCardProps {
16
+ agent: Partial<AgentWithStatus>;
17
+ onChat: (forceNew: boolean) => void;
18
+ }
19
+
20
+ const AgentCard: React.FC<AgentCardProps> = ({ agent, onChat }) => {
21
+ const navigate = useNavigate();
22
+ const { startAgent, stopAgent, isAgentStarting, isAgentStopping } = useAgentManagement();
23
+
24
+ if (!agent || !agent.id) {
25
+ clientLogger.error('[AgentCard] Agent data or ID is missing', { agent });
26
+ return (
27
+ <Card className="p-4 min-h-[100px] flex items-center justify-center text-muted-foreground">
28
+ Agent data not available.
29
+ </Card>
30
+ );
31
+ }
32
+
33
+ const agentIdForNav = agent.id;
34
+ const agentName = agent.name || 'Unnamed Agent';
35
+
36
+ const description = Array.isArray(agent.bio)
37
+ ? agent.bio.filter(Boolean).join(' ').trim()
38
+ : (typeof agent.bio === 'string' && agent.bio.trim()) ||
39
+ 'Engages with all types of questions and conversations';
40
+ const isActive = agent.status === CoreAgentStatus.ACTIVE;
41
+ const isStarting = isAgentStarting(agent.id);
42
+ const isStopping = isAgentStopping(agent.id);
43
+
44
+ const agentForMutation: Agent = {
45
+ id: agent.id!,
46
+ name: agentName,
47
+ username: agent.username || agentName,
48
+ bio: agent.bio || '',
49
+ messageExamples: agent.messageExamples || [],
50
+ postExamples: agent.postExamples || [],
51
+ topics: agent.topics || [],
52
+ adjectives: agent.adjectives || [],
53
+ knowledge: agent.knowledge || [],
54
+ plugins: agent.plugins || [],
55
+ settings: agent.settings || {},
56
+ secrets: agent.secrets || {},
57
+ style: agent.style || {},
58
+ system: agent.system || undefined,
59
+ templates: agent.templates || {},
60
+ enabled: typeof agent.enabled === 'boolean' ? agent.enabled : true,
61
+ status: agent.status || CoreAgentStatus.INACTIVE,
62
+ createdAt: typeof agent.createdAt === 'number' ? agent.createdAt : Date.now(),
63
+ updatedAt: typeof agent.updatedAt === 'number' ? agent.updatedAt : Date.now(),
64
+ };
65
+
66
+ const handleStart = () => {
67
+ startAgent(agentForMutation);
68
+ };
69
+
70
+ const handleStop = () => {
71
+ stopAgent(agentForMutation);
72
+ };
73
+
74
+ const handleNewChat = (forceNew: boolean = false) => {
75
+ onChat(forceNew);
76
+ };
77
+
78
+ const handleSettings = () => {
79
+ navigate(`/settings/${agentIdForNav}`);
80
+ };
81
+
82
+ const handleToggle = () => {
83
+ if (isActive) {
84
+ handleStop();
85
+ } else {
86
+ handleStart();
87
+ }
88
+ };
89
+
90
+ return (
91
+ <Card
92
+ className={cn(
93
+ 'w-full transition-all bg-card border border-border/50 rounded-sm hover:bg-card/50 cursor-pointer',
94
+ isActive ? '' : 'opacity-75'
95
+ )}
96
+ data-testid="agent-card"
97
+ onClick={(e) => {
98
+ e.stopPropagation();
99
+ handleNewChat();
100
+ }}
101
+ >
102
+ <CardContent className="p-0 relative h-full">
103
+ {/* Toggle Switch - positioned absolutely in top-right */}
104
+ <div className="absolute top-3 right-3">
105
+ <Switch
106
+ checked={isActive}
107
+ onCheckedChange={(checked) => {
108
+ if (checked !== isActive) {
109
+ handleToggle();
110
+ }
111
+ }}
112
+ onClick={(e) => e.stopPropagation()}
113
+ aria-label={`Toggle ${agentName}`}
114
+ disabled={isStarting || isStopping}
115
+ className={cn(
116
+ isActive
117
+ ? 'data-[state=checked]:!bg-green-600'
118
+ : 'data-[state=unchecked]:!bg-gray-500/80'
119
+ )}
120
+ />
121
+ </div>
122
+
123
+ <div className="flex flex-col justify-between h-full">
124
+ <div className="flex items-center gap-4 p-2 h-[90%]">
125
+ {/* Avatar */}
126
+ <Avatar className="h-16 w-16 flex-shrink-0 rounded-sm">
127
+ <AvatarImage src={getAgentAvatar(agent)} alt={agentName} />
128
+ <AvatarFallback className="text-lg font-medium rounded-sm">
129
+ {formatAgentName(agentName)}
130
+ </AvatarFallback>
131
+ </Avatar>
132
+
133
+ {/* Content - Name and Description */}
134
+ <div className="flex-1 min-w-0">
135
+ <h3 className="font-semibold text-xl mb-1 truncate" title={agentName}>
136
+ {agentName}
137
+ </h3>
138
+ <p className="text-sm text-muted-foreground line-clamp-2 leading-relaxed">
139
+ {description}
140
+ </p>
141
+ </div>
142
+ </div>
143
+ <div className="border-t border-muted" />
144
+ <div className="flex items-center justify-between py-1 px-2">
145
+ {/* Settings button */}
146
+ <Button
147
+ variant="ghost"
148
+ size="sm"
149
+ onClick={(e) => {
150
+ e.stopPropagation();
151
+ handleSettings();
152
+ }}
153
+ className="h-8 w-8 p-0 hover:bg-muted/50 cursor-pointer"
154
+ >
155
+ <Settings className="h-4 w-4 text-muted-foreground" />
156
+ </Button>
157
+
158
+ {/* New Chat button - ghost variant */}
159
+ <Button
160
+ variant="outline"
161
+ size="sm"
162
+ onClick={(e) => {
163
+ e.stopPropagation();
164
+ handleNewChat(true);
165
+ }}
166
+ className="h-8 px-2 rounded-sm bg-muted hover:bg-muted/50 cursor-pointer"
167
+ >
168
+ New Chat
169
+ </Button>
170
+ </div>
171
+ </div>
172
+ </CardContent>
173
+ </Card>
174
+ );
175
+ };
176
+
177
+ export default AgentCard;
@@ -0,0 +1,142 @@
1
+ import CharacterForm from '@/components/character-form';
2
+ import { useToast } from '@/hooks/use-toast';
3
+ import { createElizaClient } from '@/lib/api-client-config';
4
+ import type { Agent } from '@elizaos/core';
5
+ import { useQueryClient } from '@tanstack/react-query';
6
+ import { useState, useRef } from 'react';
7
+ import { useNavigate } from 'react-router-dom';
8
+ import AvatarPanel from './avatar-panel';
9
+ import PluginsPanel from './plugins-panel';
10
+ import { SecretPanel, type SecretPanelRef } from './secret-panel';
11
+ import { useAgentUpdate } from '@/hooks/use-agent-update';
12
+ import { getTemplateById } from '@/config/agent-templates';
13
+
14
+ // Define a partial agent for initialization from the "none" template
15
+ const defaultCharacter: Partial<Agent> = getTemplateById('none')?.template || {
16
+ name: '',
17
+ username: '',
18
+ system: '',
19
+ bio: [] as string[],
20
+ topics: [] as string[],
21
+ adjectives: [] as string[],
22
+ plugins: ['@elizaos/plugin-sql', '@elizaos/plugin-openai', '@elizaos/plugin-bootstrap'],
23
+ settings: { secrets: {} },
24
+ };
25
+
26
+ export default function AgentCreator() {
27
+ const navigate = useNavigate();
28
+ const { toast } = useToast();
29
+ const queryClient = useQueryClient();
30
+ const [initialCharacter] = useState<Partial<Agent>>({
31
+ ...defaultCharacter,
32
+ });
33
+ const secretPanelRef = useRef<SecretPanelRef>(null);
34
+ const [currentSecrets, setCurrentSecrets] = useState<Record<string, string | null>>({});
35
+
36
+ // Use agent update hook for proper handling of nested fields
37
+ const agentState = useAgentUpdate(initialCharacter as Agent);
38
+
39
+ const ensureRequiredFields = (character: Agent): Agent => {
40
+ return {
41
+ ...character,
42
+ bio: character.bio ?? [],
43
+ messageExamples: character.messageExamples ?? [],
44
+ postExamples: character.postExamples ?? [],
45
+ topics: character.topics ?? [],
46
+ adjectives: character.adjectives ?? [],
47
+ plugins: character.plugins ?? [],
48
+ style: {
49
+ all: character.style?.all ?? [],
50
+ chat: character.style?.chat ?? [],
51
+ post: character.style?.post ?? [],
52
+ },
53
+ settings: character.settings ?? { secrets: {} },
54
+ };
55
+ };
56
+
57
+ const handleSubmit = async (character: Agent) => {
58
+ try {
59
+ const completeCharacter = ensureRequiredFields(character);
60
+
61
+ // Get secrets from state (or ref as fallback)
62
+ const secrets = currentSecrets || secretPanelRef.current?.getSecrets() || {};
63
+ if (secrets && Object.keys(secrets).length > 0) {
64
+ // Add secrets to the character settings
65
+ completeCharacter.settings = {
66
+ ...completeCharacter.settings,
67
+ secrets,
68
+ };
69
+ }
70
+
71
+ const elizaClient = createElizaClient();
72
+ await elizaClient.agents.createAgent({ agent: completeCharacter });
73
+
74
+ // Invalidate the characters query to refresh the characters list
75
+ queryClient.invalidateQueries({ queryKey: ['characters'] });
76
+
77
+ toast({
78
+ title: 'Success',
79
+ description: 'Agent created successfully!',
80
+ });
81
+ queryClient.invalidateQueries({ queryKey: ['agents'] });
82
+ navigate('/');
83
+ } catch (error) {
84
+ toast({
85
+ title: 'Error',
86
+ description: error instanceof Error ? error.message : 'Failed to create agent',
87
+ variant: 'destructive',
88
+ });
89
+ }
90
+ };
91
+
92
+ return (
93
+ <div className="h-full w-full">
94
+ <CharacterForm
95
+ characterValue={agentState.agent}
96
+ setCharacterValue={agentState}
97
+ title="Create Agent"
98
+ description="Configure your AI agent's behavior and capabilities."
99
+ onSubmit={handleSubmit}
100
+ onReset={() => {
101
+ agentState.reset();
102
+ setCurrentSecrets({});
103
+ }}
104
+ onDelete={() => {
105
+ navigate('/');
106
+ }}
107
+ onTemplateChange={() => {
108
+ setCurrentSecrets({});
109
+ }}
110
+ isAgent={true}
111
+ secretPanelRef={secretPanelRef}
112
+ customComponents={[
113
+ {
114
+ name: 'Plugins',
115
+ component: (
116
+ <PluginsPanel characterValue={agentState.agent} setCharacterValue={agentState} />
117
+ ),
118
+ },
119
+ {
120
+ name: 'Secret',
121
+ component: (
122
+ <SecretPanel
123
+ characterValue={agentState.agent}
124
+ ref={secretPanelRef}
125
+ onChange={(secrets) => {
126
+ // Only update local state, don't update agent state to avoid circular updates
127
+ setCurrentSecrets(secrets);
128
+ }}
129
+ />
130
+ ),
131
+ },
132
+ {
133
+ name: 'Avatar',
134
+ component: (
135
+ <AvatarPanel characterValue={agentState.agent} setCharacterValue={agentState} />
136
+ ),
137
+ },
138
+ ]}
139
+ />
140
+ </div>
141
+ );
142
+ }