@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,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
|
+
}
|