@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,116 @@
1
+ import { cn } from '@/lib/utils';
2
+ import { X } from 'lucide-react';
3
+ import { useState } from 'react';
4
+ import { Badge } from './ui/badge';
5
+ import { Button } from './ui/button';
6
+ import { Input } from './ui/input';
7
+ import { Label } from './ui/label';
8
+
9
+ type TagProps = {
10
+ tag: string;
11
+ onRemove: (tag: string) => void;
12
+ };
13
+
14
+ const Tag = ({ tag, onRemove }: TagProps) => (
15
+ <Badge
16
+ variant="outline"
17
+ className="flex items-center gap-1.5 pr-1.5 text-sm py-1 px-2 transition-colors hover:bg-accent hover:text-accent-foreground"
18
+ >
19
+ {tag}
20
+ <Button
21
+ type="button"
22
+ variant="ghost"
23
+ size="icon"
24
+ onClick={() => onRemove(tag)}
25
+ className="hover:bg-accent/20 rounded-full p-0.5 transition-colors h-auto w-auto min-w-0 min-h-0"
26
+ >
27
+ <X className="h-3 w-3" />
28
+ <span className="sr-only">Remove {tag}</span>
29
+ </Button>
30
+ </Badge>
31
+ );
32
+
33
+ type TagListProps = {
34
+ tags: string[];
35
+ onRemove: (tag: string) => void;
36
+ };
37
+
38
+ const TagList = ({ tags, onRemove }: TagListProps) => (
39
+ <div className="flex flex-wrap gap-2 mb-2">
40
+ {tags.map((tag) => (
41
+ <Tag key={tag} tag={tag} onRemove={onRemove} />
42
+ ))}
43
+ </div>
44
+ );
45
+
46
+ type TagInputProps = {
47
+ value: string;
48
+ onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
49
+ onKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void;
50
+ onAdd: () => void;
51
+ };
52
+
53
+ const TagInput = ({ value, onChange, onKeyDown, onAdd }: TagInputProps) => (
54
+ <div className="relative">
55
+ <Input
56
+ value={value}
57
+ onChange={onChange}
58
+ onKeyDown={onKeyDown}
59
+ placeholder="Type and press Enter or click Add..."
60
+ className={cn('bg-background pr-16', !value && 'text-muted-foreground')}
61
+ />
62
+ {value.trim() && (
63
+ <Button
64
+ size="sm"
65
+ onClick={onAdd}
66
+ className="absolute top-1/2 -translate-y-1/2 right-2 h-7 px-3"
67
+ >
68
+ Add
69
+ </Button>
70
+ )}
71
+ </div>
72
+ );
73
+
74
+ type ArrayInputProps = {
75
+ title?: string;
76
+ data: string[];
77
+ onChange: (newData: string[]) => void;
78
+ };
79
+
80
+ export default function ArrayInput({ title, data, onChange }: ArrayInputProps) {
81
+ const [inputValue, setInputValue] = useState('');
82
+
83
+ const addTag = () => {
84
+ const trimmedValue = inputValue.trim();
85
+ if (trimmedValue && !data.includes(trimmedValue)) {
86
+ onChange([...data, trimmedValue]);
87
+ setInputValue('');
88
+ }
89
+ };
90
+
91
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
92
+ if (e.key === 'Enter') {
93
+ e.preventDefault();
94
+ addTag();
95
+ }
96
+ };
97
+
98
+ const removeTag = (tagToRemove: string) => {
99
+ onChange(data.filter((tag) => tag !== tagToRemove));
100
+ };
101
+
102
+ return (
103
+ <div className="space-y-2">
104
+ <Label>{title}</Label>
105
+ <div className="p-2 bg-card rounded border border-input">
106
+ <TagList tags={data} onRemove={removeTag} />
107
+ <TagInput
108
+ value={inputValue}
109
+ onChange={(e) => setInputValue(e.target.value)}
110
+ onKeyDown={handleKeyDown}
111
+ onAdd={addTag}
112
+ />
113
+ </div>
114
+ </div>
115
+ );
116
+ }
@@ -0,0 +1,292 @@
1
+ import { Button } from '@/components/ui/button';
2
+ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
3
+ import { useToast } from '@/hooks/use-toast';
4
+ import { createElizaClient } from '@/lib/api-client-config';
5
+ import { cn } from '@/lib/utils';
6
+ import type { UUID } from '@elizaos/core';
7
+ import { useMutation } from '@tanstack/react-query';
8
+ import { Ellipsis, Mic, Send, Trash } from 'lucide-react';
9
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
10
+
11
+ type Props = {
12
+ agentId: UUID;
13
+ onChange: (newInput: string) => void;
14
+ className?: string;
15
+ timerClassName?: string;
16
+ };
17
+
18
+ type Record = {
19
+ id: number;
20
+ name: string;
21
+ file: string | null;
22
+ };
23
+
24
+ let recorder: MediaRecorder;
25
+ let recordingChunks: BlobPart[] = [];
26
+ let timerTimeout: ReturnType<typeof setTimeout>;
27
+
28
+ // Utility function to pad a number with leading zeros
29
+ const padWithLeadingZeros = (num: number, length: number): string => {
30
+ return String(num).padStart(length, '0');
31
+ };
32
+
33
+ export const AudioRecorder = ({ className, timerClassName, agentId, onChange }: Props) => {
34
+ const { toast } = useToast();
35
+ // States
36
+ const [isRecording, setIsRecording] = useState<boolean>(false);
37
+ const [_, setIsRecordingFinished] = useState<boolean>(false);
38
+ const [timer, setTimer] = useState<number>(0);
39
+ const [currentRecord, setCurrentRecord] = useState<Record>({
40
+ id: -1,
41
+ name: '',
42
+ file: null,
43
+ });
44
+ // Calculate the hours, minutes, and seconds from the timer
45
+ const minutes = Math.floor((timer % 3600) / 60);
46
+ const seconds = timer % 60;
47
+
48
+ const [minuteLeft, minuteRight] = useMemo(
49
+ () => padWithLeadingZeros(minutes, 2).split(''),
50
+ [minutes]
51
+ );
52
+ const [secondLeft, secondRight] = useMemo(
53
+ () => padWithLeadingZeros(seconds, 2).split(''),
54
+ [seconds]
55
+ );
56
+ // Refs
57
+ const mediaRecorderRef = useRef<{
58
+ stream: MediaStream | null;
59
+ analyser: AnalyserNode | null;
60
+ mediaRecorder: MediaRecorder | null;
61
+ audioContext: AudioContext | null;
62
+ }>({
63
+ stream: null,
64
+ analyser: null,
65
+ mediaRecorder: null,
66
+ audioContext: null,
67
+ });
68
+
69
+ const elizaClient = createElizaClient();
70
+ const mutation = useMutation({
71
+ mutationKey: ['whisper'],
72
+ mutationFn: (file: Blob) => elizaClient.audio.transcribe(agentId, { audio: file }),
73
+ onSuccess: (data) => {
74
+ if (data?.text) {
75
+ onChange(data.text);
76
+ }
77
+ },
78
+ onError: (e) => {
79
+ toast({
80
+ variant: 'destructive',
81
+ title: 'Unable to start recording',
82
+ description: e.message,
83
+ });
84
+ console.log(e);
85
+ },
86
+ });
87
+
88
+ function startRecording() {
89
+ if (navigator.mediaDevices?.getUserMedia) {
90
+ navigator.mediaDevices
91
+ .getUserMedia({
92
+ audio: true,
93
+ })
94
+ .then((stream) => {
95
+ setIsRecording(true);
96
+ // ============ Analyzing ============
97
+ const AudioContext = window.AudioContext;
98
+ const audioCtx = new AudioContext();
99
+ const analyser = audioCtx.createAnalyser();
100
+ const source = audioCtx.createMediaStreamSource(stream);
101
+ source.connect(analyser);
102
+ mediaRecorderRef.current = {
103
+ stream,
104
+ analyser,
105
+ mediaRecorder: null,
106
+ audioContext: audioCtx,
107
+ };
108
+
109
+ const mimeType = MediaRecorder.isTypeSupported('audio/mpeg')
110
+ ? 'audio/mpeg'
111
+ : MediaRecorder.isTypeSupported('audio/webm')
112
+ ? 'audio/webm'
113
+ : 'audio/wav';
114
+
115
+ const options = { mimeType };
116
+ mediaRecorderRef.current.mediaRecorder = new MediaRecorder(stream, options);
117
+ mediaRecorderRef.current.mediaRecorder.start();
118
+ recordingChunks = [];
119
+ // ============ Recording ============
120
+ recorder = new MediaRecorder(stream);
121
+ recorder.start();
122
+ recorder.ondataavailable = (e) => {
123
+ recordingChunks.push(e.data);
124
+ };
125
+ })
126
+ .catch((e) => {
127
+ toast({
128
+ variant: 'destructive',
129
+ title: 'Unable to start recording',
130
+ description: e.message,
131
+ });
132
+ console.log(e);
133
+ });
134
+ }
135
+ }
136
+ function stopRecording() {
137
+ recorder.onstop = () => {
138
+ const recordBlob = new Blob(recordingChunks, {
139
+ type: 'audio/wav',
140
+ });
141
+ mutation.mutate(recordBlob);
142
+ setCurrentRecord({
143
+ ...currentRecord,
144
+ file: window.URL.createObjectURL(recordBlob),
145
+ });
146
+ recordingChunks = [];
147
+ };
148
+
149
+ recorder.stop();
150
+
151
+ setIsRecording(false);
152
+ setIsRecordingFinished(true);
153
+ setTimer(0);
154
+ clearTimeout(timerTimeout);
155
+ }
156
+ function resetRecording() {
157
+ const { mediaRecorder, stream, analyser, audioContext } = mediaRecorderRef.current;
158
+
159
+ if (mediaRecorder) {
160
+ mediaRecorder.onstop = () => {
161
+ recordingChunks = [];
162
+ };
163
+ mediaRecorder.stop();
164
+ }
165
+
166
+ // Stop the web audio context and the analyser node
167
+ if (analyser) {
168
+ analyser.disconnect();
169
+ }
170
+ if (stream) {
171
+ for (const track of stream.getTracks()) {
172
+ track.stop();
173
+ }
174
+ }
175
+ if (audioContext) {
176
+ audioContext.close();
177
+ }
178
+ setIsRecording(false);
179
+ setIsRecordingFinished(true);
180
+ setTimer(0);
181
+ clearTimeout(timerTimeout);
182
+ }
183
+ const handleSubmit = () => {
184
+ stopRecording();
185
+ };
186
+
187
+ // Effect to update the timer every second
188
+ useEffect(() => {
189
+ if (isRecording) {
190
+ timerTimeout = setTimeout(() => {
191
+ setTimer(timer + 1);
192
+ }, 1000);
193
+ }
194
+ return () => clearTimeout(timerTimeout);
195
+ }, [isRecording, timer]);
196
+
197
+ if (mutation?.isPending) {
198
+ return (
199
+ <Button variant="ghost" disabled size="icon">
200
+ <Ellipsis className="size-4" />
201
+ </Button>
202
+ );
203
+ }
204
+
205
+ return (
206
+ <div
207
+ className={cn(
208
+ 'flex items-center justify-center gap-2 border-l border-l-transparent border-opacity-0 transition-all duration-300',
209
+ {
210
+ 'border-opacity-100 border-l-border pl-2': isRecording,
211
+ },
212
+ className
213
+ )}
214
+ >
215
+ {isRecording ? (
216
+ <div className="flex gap-1 items-center">
217
+ <div className="bg-red-500 rounded-full h-2.5 w-2.5 animate-pulse" />
218
+ <Timer
219
+ minuteLeft={minuteLeft}
220
+ minuteRight={minuteRight}
221
+ secondLeft={secondLeft}
222
+ secondRight={secondRight}
223
+ timerClassName={timerClassName}
224
+ />
225
+ </div>
226
+ ) : null}
227
+
228
+ <div className="flex items-center gap-2">
229
+ {/* ========== Delete recording button ========== */}
230
+ {isRecording ? (
231
+ <Tooltip>
232
+ <TooltipTrigger asChild>
233
+ <Button onClick={resetRecording} size={'icon'} variant="ghost">
234
+ <Trash className="size-4" />
235
+ </Button>
236
+ </TooltipTrigger>
237
+ <TooltipContent className="m-2">
238
+ <span> Reset recording</span>
239
+ </TooltipContent>
240
+ </Tooltip>
241
+ ) : null}
242
+
243
+ {/* ========== Start and send recording button ========== */}
244
+ <Tooltip>
245
+ <TooltipTrigger asChild>
246
+ {!isRecording ? (
247
+ <Button variant="ghost" size="icon" onClick={() => startRecording()}>
248
+ <Mic className="size-4" />
249
+ <span className="sr-only">Use Microphone</span>
250
+ </Button>
251
+ ) : (
252
+ <Button onClick={handleSubmit} variant="ghost" size="icon">
253
+ <Send className="size-4" />
254
+ </Button>
255
+ )}
256
+ </TooltipTrigger>
257
+ <TooltipContent side="right">
258
+ <span>{!isRecording ? 'Start' : 'Send'} </span>
259
+ </TooltipContent>
260
+ </Tooltip>
261
+ </div>
262
+ </div>
263
+ );
264
+ };
265
+
266
+ const Timer = React.memo(
267
+ ({
268
+ minuteLeft,
269
+ minuteRight,
270
+ secondLeft,
271
+ secondRight,
272
+ timerClassName,
273
+ }: {
274
+ minuteLeft: string;
275
+ minuteRight: string;
276
+ secondLeft: string;
277
+ secondRight: string;
278
+ timerClassName?: string;
279
+ }) => {
280
+ return (
281
+ <div className={cn('text-sm animate-in duration-1000 fade-in-0 select-none', timerClassName)}>
282
+ <p>
283
+ {minuteLeft}
284
+ {minuteRight}:{secondLeft}
285
+ {secondRight}
286
+ </p>
287
+ </div>
288
+ );
289
+ }
290
+ );
291
+
292
+ Timer.displayName = 'Timer';
@@ -0,0 +1,141 @@
1
+ import { Button } from '@/components/ui/button';
2
+ import type { Agent } from '@elizaos/core';
3
+ import { Image as ImageIcon, Upload, X, Info } from 'lucide-react';
4
+ import { useRef, useState, useEffect } from 'react';
5
+ import { compressImage } from '@/lib/utils';
6
+ import { AVATAR_IMAGE_MAX_SIZE } from '@/constants';
7
+
8
+ interface AvatarPanelProps {
9
+ characterValue: Agent;
10
+ setCharacterValue: {
11
+ updateAvatar?: (avatarUrl: string) => void;
12
+ updateSetting?: <T>(path: string, value: T) => void;
13
+ updateField?: <T>(path: string, value: T) => void;
14
+ [key: string]: any;
15
+ };
16
+ }
17
+
18
+ export default function AvatarPanel({ characterValue, setCharacterValue }: AvatarPanelProps) {
19
+ // Extract avatar as string, handling various types
20
+ const getAvatarUrl = () => {
21
+ const avatarSetting = characterValue?.settings?.avatar;
22
+ return typeof avatarSetting === 'string' ? avatarSetting : null;
23
+ };
24
+
25
+ const [avatar, setAvatar] = useState<string | null>(getAvatarUrl());
26
+ const [hasChanged, setHasChanged] = useState(false);
27
+ const fileInputRef = useRef<HTMLInputElement>(null);
28
+
29
+ // Reset the change flag when component initializes or character changes
30
+ useEffect(() => {
31
+ setAvatar(getAvatarUrl());
32
+ setHasChanged(false);
33
+ }, [characterValue.id]);
34
+
35
+ const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
36
+ const file = event.target.files?.[0];
37
+ if (file) {
38
+ try {
39
+ const compressedImage = await compressImage(file);
40
+ setAvatar(compressedImage);
41
+ setHasChanged(true);
42
+
43
+ // Only update when there's a real change
44
+ updateCharacterAvatar(compressedImage);
45
+ } catch (error) {
46
+ console.error('Error compressing image:', error);
47
+ }
48
+ }
49
+ };
50
+
51
+ const handleRemoveAvatar = () => {
52
+ if (avatar) {
53
+ setAvatar(null);
54
+ setHasChanged(true);
55
+ updateCharacterAvatar('');
56
+ }
57
+ };
58
+
59
+ // Centralized update function to avoid code duplication
60
+ const updateCharacterAvatar = (avatarUrl: string) => {
61
+ if (setCharacterValue.updateAvatar) {
62
+ // Use the specialized method for avatar updates when available
63
+ setCharacterValue.updateAvatar(avatarUrl);
64
+ } else if (setCharacterValue.updateSetting) {
65
+ // Use updateSetting as fallback
66
+ setCharacterValue.updateSetting('avatar', avatarUrl);
67
+ } else if (setCharacterValue.updateField) {
68
+ // Last resort - use the generic field update
69
+ setCharacterValue.updateField('settings.avatar', avatarUrl);
70
+ }
71
+ };
72
+
73
+ return (
74
+ <div className="rounded-lg w-full">
75
+ <h2 className="text-xl font-bold mb-4 pb-5 ml-1">Avatar Settings</h2>
76
+
77
+ <div className="flex flex-col items-center gap-4 pb-4 max-w-sm mx-auto">
78
+ {/* Image preview area */}
79
+ {avatar ? (
80
+ <div className="w-64 h-64 mb-2">
81
+ <img
82
+ src={avatar}
83
+ alt="Agent Avatar"
84
+ className="object-cover w-full h-full rounded-lg border"
85
+ />
86
+ </div>
87
+ ) : (
88
+ <div
89
+ className="w-64 h-64 flex items-center justify-center border border-dashed rounded-lg text-gray-500 mb-2 cursor-pointer hover:bg-gray-50 transition-colors"
90
+ onClick={() => fileInputRef.current?.click()}
91
+ >
92
+ <div className="flex flex-col items-center gap-2">
93
+ <ImageIcon className="w-10 h-10" />
94
+ <span className="text-sm">Click to upload</span>
95
+ </div>
96
+ </div>
97
+ )}
98
+
99
+ {/* Controls area */}
100
+ <div className="flex flex-col gap-3 w-64">
101
+ <input
102
+ type="file"
103
+ accept="image/*"
104
+ className="hidden"
105
+ ref={fileInputRef}
106
+ onChange={handleFileUpload}
107
+ />
108
+
109
+ <div className="flex gap-2">
110
+ <Button
111
+ type="button"
112
+ className="flex items-center gap-2 flex-1"
113
+ onClick={() => fileInputRef.current?.click()}
114
+ >
115
+ <Upload className="w-5 h-5" />
116
+ {avatar ? 'Replace' : 'Upload'}
117
+ </Button>
118
+
119
+ {avatar && (
120
+ <Button
121
+ type="button"
122
+ variant="outline"
123
+ className="flex items-center"
124
+ onClick={handleRemoveAvatar}
125
+ >
126
+ <X className="w-5 h-5" />
127
+ </Button>
128
+ )}
129
+ </div>
130
+
131
+ <div className="flex items-center justify-center gap-1 text-xs text-muted-foreground mt-1">
132
+ <Info className="w-3.5 h-3.5" />
133
+ <span>
134
+ Images greater than {AVATAR_IMAGE_MAX_SIZE}x{AVATAR_IMAGE_MAX_SIZE} will be resized
135
+ </span>
136
+ </div>
137
+ </div>
138
+ </div>
139
+ </div>
140
+ );
141
+ }