@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,1138 @@
|
|
|
1
|
+
import ArrayInput from '@/components/array-input';
|
|
2
|
+
import { Button } from '@/components/ui/button';
|
|
3
|
+
import { Input } from '@/components/ui/input';
|
|
4
|
+
import { Label } from '@/components/ui/label';
|
|
5
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
6
|
+
import { Textarea } from '@/components/ui/textarea';
|
|
7
|
+
import { AVATAR_IMAGE_MAX_SIZE, FIELD_REQUIREMENT_TYPE, FIELD_REQUIREMENTS } from '@/constants';
|
|
8
|
+
import { useToast } from '@/hooks/use-toast';
|
|
9
|
+
import { exportCharacterAsJson } from '@/lib/export-utils';
|
|
10
|
+
import { compressImage } from '@/lib/utils';
|
|
11
|
+
import type { Agent, Character } from '@elizaos/core';
|
|
12
|
+
import type React from 'react';
|
|
13
|
+
import {
|
|
14
|
+
type FormEvent,
|
|
15
|
+
type ReactNode,
|
|
16
|
+
useState,
|
|
17
|
+
useMemo,
|
|
18
|
+
useCallback,
|
|
19
|
+
useRef,
|
|
20
|
+
useEffect,
|
|
21
|
+
} from 'react';
|
|
22
|
+
import {
|
|
23
|
+
Select,
|
|
24
|
+
SelectContent,
|
|
25
|
+
SelectItem,
|
|
26
|
+
SelectTrigger,
|
|
27
|
+
SelectValue,
|
|
28
|
+
SelectGroup,
|
|
29
|
+
SelectLabel,
|
|
30
|
+
SelectSeparator,
|
|
31
|
+
} from '@/components/ui/select';
|
|
32
|
+
import {
|
|
33
|
+
getAllVoiceModels,
|
|
34
|
+
getVoiceModelByValue,
|
|
35
|
+
providerPluginMap,
|
|
36
|
+
openAIVoiceModels,
|
|
37
|
+
elevenLabsVoiceModels,
|
|
38
|
+
} from '../config/voice-models';
|
|
39
|
+
import { useElevenLabsVoices } from '@/hooks/use-elevenlabs-voices';
|
|
40
|
+
import {
|
|
41
|
+
Trash,
|
|
42
|
+
Loader2,
|
|
43
|
+
RotateCcw,
|
|
44
|
+
ArrowDownToLine,
|
|
45
|
+
ArrowUpFromLine,
|
|
46
|
+
Save,
|
|
47
|
+
StopCircle,
|
|
48
|
+
MoreVertical,
|
|
49
|
+
} from 'lucide-react';
|
|
50
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
51
|
+
import {
|
|
52
|
+
DropdownMenu,
|
|
53
|
+
DropdownMenuContent,
|
|
54
|
+
DropdownMenuItem,
|
|
55
|
+
DropdownMenuSeparator,
|
|
56
|
+
DropdownMenuTrigger,
|
|
57
|
+
} from '@/components/ui/dropdown-menu';
|
|
58
|
+
import { agentTemplates, getTemplateById } from '@/config/agent-templates';
|
|
59
|
+
import { cn } from '@/lib/utils';
|
|
60
|
+
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
|
61
|
+
|
|
62
|
+
import type { SecretPanelRef } from './secret-panel';
|
|
63
|
+
import { MissingSecretsDialog } from './missing-secrets-dialog';
|
|
64
|
+
import { useRequiredSecrets } from '@/hooks/use-plugin-details';
|
|
65
|
+
import { createElizaClient } from '@/lib/api-client-config';
|
|
66
|
+
import { V1Character, useConvertCharacter } from '@/hooks/use-character-convert';
|
|
67
|
+
|
|
68
|
+
export type InputField = {
|
|
69
|
+
name: string;
|
|
70
|
+
title: string;
|
|
71
|
+
description?: string;
|
|
72
|
+
fieldType: 'text' | 'textarea' | 'email' | 'url' | 'checkbox' | 'select';
|
|
73
|
+
getValue: (agent: Agent) => string;
|
|
74
|
+
options?: { value: string; label: string }[];
|
|
75
|
+
tooltip?: string;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export type ArrayField = {
|
|
79
|
+
path: string;
|
|
80
|
+
title: string;
|
|
81
|
+
description?: string;
|
|
82
|
+
getData: (agent: Agent) => string[];
|
|
83
|
+
tooltip?: string;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
enum SECTION_TYPE {
|
|
87
|
+
INPUT = 'input',
|
|
88
|
+
ARRAY = 'array',
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
type customComponent = {
|
|
92
|
+
name: string;
|
|
93
|
+
component: ReactNode;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export type CharacterFormProps = {
|
|
97
|
+
title: string;
|
|
98
|
+
description: string;
|
|
99
|
+
onSubmit: (character: Agent) => Promise<void>;
|
|
100
|
+
onDelete?: () => void;
|
|
101
|
+
onReset?: () => void;
|
|
102
|
+
onStopAgent?: () => void;
|
|
103
|
+
isAgent?: boolean;
|
|
104
|
+
isDeleting?: boolean;
|
|
105
|
+
isStopping?: boolean;
|
|
106
|
+
customComponents?: customComponent[];
|
|
107
|
+
characterValue: Agent;
|
|
108
|
+
setCharacterValue: {
|
|
109
|
+
updateField: <T>(path: string, value: T) => void;
|
|
110
|
+
addArrayItem?: <T>(path: string, item: T) => void;
|
|
111
|
+
removeArrayItem?: (path: string, index: number) => void;
|
|
112
|
+
updateSetting?: (path: string, value: any) => void;
|
|
113
|
+
importAgent?: (value: Character) => void;
|
|
114
|
+
[key: string]: any;
|
|
115
|
+
};
|
|
116
|
+
onTemplateChange?: () => void;
|
|
117
|
+
secretPanelRef?: React.RefObject<SecretPanelRef | null>;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// Custom hook to detect container width and determine if labels should be shown
|
|
121
|
+
const useContainerWidth = (threshold: number = 768) => {
|
|
122
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
123
|
+
const [showLabels, setShowLabels] = useState(true);
|
|
124
|
+
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
|
|
125
|
+
const currentWidthRef = useRef<number>(0);
|
|
126
|
+
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
const container = containerRef.current;
|
|
129
|
+
if (!container) return;
|
|
130
|
+
|
|
131
|
+
// Debounced resize handler
|
|
132
|
+
const handleResize = (width: number) => {
|
|
133
|
+
// Clear existing timer
|
|
134
|
+
if (debounceTimerRef.current) {
|
|
135
|
+
clearTimeout(debounceTimerRef.current);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Set new timer
|
|
139
|
+
debounceTimerRef.current = setTimeout(() => {
|
|
140
|
+
const shouldShowLabels = width >= threshold;
|
|
141
|
+
// Only update if the value actually changes
|
|
142
|
+
setShowLabels((prev) => {
|
|
143
|
+
if (prev !== shouldShowLabels) {
|
|
144
|
+
return shouldShowLabels;
|
|
145
|
+
}
|
|
146
|
+
return prev;
|
|
147
|
+
});
|
|
148
|
+
currentWidthRef.current = width;
|
|
149
|
+
}, 150); // 150ms debounce
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const resizeObserver = new ResizeObserver((entries) => {
|
|
153
|
+
for (const entry of entries) {
|
|
154
|
+
const { width } = entry.contentRect;
|
|
155
|
+
// Only trigger if width actually changed significantly (more than 5px)
|
|
156
|
+
if (Math.abs(width - currentWidthRef.current) > 5) {
|
|
157
|
+
handleResize(width);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
resizeObserver.observe(container);
|
|
163
|
+
|
|
164
|
+
// Initial check with delay to avoid race conditions
|
|
165
|
+
setTimeout(() => {
|
|
166
|
+
const { width } = container.getBoundingClientRect();
|
|
167
|
+
currentWidthRef.current = width;
|
|
168
|
+
setShowLabels(width >= threshold);
|
|
169
|
+
}, 0);
|
|
170
|
+
|
|
171
|
+
// Cleanup
|
|
172
|
+
return () => {
|
|
173
|
+
resizeObserver.disconnect();
|
|
174
|
+
if (debounceTimerRef.current) {
|
|
175
|
+
clearTimeout(debounceTimerRef.current);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}, [threshold]);
|
|
179
|
+
|
|
180
|
+
return { containerRef, showLabels };
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export default function CharacterForm({
|
|
184
|
+
characterValue,
|
|
185
|
+
setCharacterValue,
|
|
186
|
+
title,
|
|
187
|
+
description,
|
|
188
|
+
onSubmit,
|
|
189
|
+
onDelete,
|
|
190
|
+
onReset,
|
|
191
|
+
onStopAgent,
|
|
192
|
+
isDeleting = false,
|
|
193
|
+
isStopping = false,
|
|
194
|
+
customComponents = [],
|
|
195
|
+
onTemplateChange,
|
|
196
|
+
secretPanelRef,
|
|
197
|
+
}: CharacterFormProps) {
|
|
198
|
+
const { toast } = useToast();
|
|
199
|
+
const { data: elevenlabsVoices, isLoading: isLoadingVoices } = useElevenLabsVoices();
|
|
200
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
201
|
+
const [selectedTemplate, setSelectedTemplate] = useState<string>('none');
|
|
202
|
+
const [activeTab, setActiveTab] = useState('basic');
|
|
203
|
+
const tabsContainerRef = useRef<HTMLDivElement>(null);
|
|
204
|
+
const [showLeftScroll, setShowLeftScroll] = useState(false);
|
|
205
|
+
const [showRightScroll, setShowRightScroll] = useState(false);
|
|
206
|
+
const [showMissingSecretsDialog, setShowMissingSecretsDialog] = useState(false);
|
|
207
|
+
const [pendingSubmit, setPendingSubmit] = useState<Agent | null>(null);
|
|
208
|
+
const [globalEnvs, setGlobalEnvs] = useState<Record<string, string>>({});
|
|
209
|
+
|
|
210
|
+
// Get required secrets based on enabled plugins
|
|
211
|
+
const enabledPlugins = useMemo(() => characterValue?.plugins || [], [characterValue?.plugins]);
|
|
212
|
+
const { requiredSecrets } = useRequiredSecrets(enabledPlugins);
|
|
213
|
+
|
|
214
|
+
const { convertCharacter } = useConvertCharacter();
|
|
215
|
+
|
|
216
|
+
// Fetch global environment variables
|
|
217
|
+
useEffect(() => {
|
|
218
|
+
const fetchGlobalEnvs = async () => {
|
|
219
|
+
try {
|
|
220
|
+
const elizaClient = createElizaClient();
|
|
221
|
+
const data = await elizaClient.system.getEnvironment();
|
|
222
|
+
setGlobalEnvs(data || {});
|
|
223
|
+
} catch (error) {
|
|
224
|
+
console.error('Failed to fetch global environment variables:', error);
|
|
225
|
+
setGlobalEnvs({});
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
fetchGlobalEnvs();
|
|
230
|
+
}, []);
|
|
231
|
+
|
|
232
|
+
// Use the custom hook to detect container width
|
|
233
|
+
const { containerRef, showLabels } = useContainerWidth(640); // Adjust threshold as needed
|
|
234
|
+
|
|
235
|
+
// Check if tabs need scroll buttons
|
|
236
|
+
const checkScrollButtons = useCallback(() => {
|
|
237
|
+
const container = tabsContainerRef.current;
|
|
238
|
+
if (!container) return;
|
|
239
|
+
|
|
240
|
+
const { scrollLeft, scrollWidth, clientWidth } = container;
|
|
241
|
+
setShowLeftScroll(scrollLeft > 0);
|
|
242
|
+
setShowRightScroll(scrollLeft + clientWidth < scrollWidth - 1);
|
|
243
|
+
}, []);
|
|
244
|
+
|
|
245
|
+
useEffect(() => {
|
|
246
|
+
const container = tabsContainerRef.current;
|
|
247
|
+
if (!container) return;
|
|
248
|
+
|
|
249
|
+
checkScrollButtons();
|
|
250
|
+
container.addEventListener('scroll', checkScrollButtons);
|
|
251
|
+
window.addEventListener('resize', checkScrollButtons);
|
|
252
|
+
|
|
253
|
+
return () => {
|
|
254
|
+
container.removeEventListener('scroll', checkScrollButtons);
|
|
255
|
+
window.removeEventListener('resize', checkScrollButtons);
|
|
256
|
+
};
|
|
257
|
+
}, [checkScrollButtons, customComponents.length]);
|
|
258
|
+
|
|
259
|
+
const scrollTabs = (direction: 'left' | 'right') => {
|
|
260
|
+
const container = tabsContainerRef.current;
|
|
261
|
+
if (!container) return;
|
|
262
|
+
|
|
263
|
+
const scrollAmount = container.clientWidth * 0.8;
|
|
264
|
+
container.scrollBy({
|
|
265
|
+
left: direction === 'left' ? -scrollAmount : scrollAmount,
|
|
266
|
+
behavior: 'smooth',
|
|
267
|
+
});
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
// Get all voice models, using the dynamic ElevenLabs voices when available
|
|
271
|
+
const allVoiceModels = useMemo(() => {
|
|
272
|
+
const staticModels = getAllVoiceModels();
|
|
273
|
+
|
|
274
|
+
// If we have dynamically loaded ElevenLabs voices, replace the static ones
|
|
275
|
+
if (elevenlabsVoices && !isLoadingVoices) {
|
|
276
|
+
// Filter out the static ElevenLabs voices
|
|
277
|
+
const nonElevenLabsModels = staticModels.filter((model) => model.provider !== 'elevenlabs');
|
|
278
|
+
// Return combined models with dynamic ElevenLabs voices
|
|
279
|
+
return [...nonElevenLabsModels, ...elevenlabsVoices];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Otherwise return the static models
|
|
283
|
+
return staticModels;
|
|
284
|
+
}, [elevenlabsVoices, isLoadingVoices]);
|
|
285
|
+
|
|
286
|
+
// Define form schema with dynamic voice model options
|
|
287
|
+
const AGENT_FORM_SCHEMA = useMemo(
|
|
288
|
+
() => [
|
|
289
|
+
{
|
|
290
|
+
sectionTitle: 'Basic Info',
|
|
291
|
+
sectionValue: 'basic',
|
|
292
|
+
sectionType: SECTION_TYPE.INPUT,
|
|
293
|
+
fields: [
|
|
294
|
+
{
|
|
295
|
+
title: 'Name',
|
|
296
|
+
name: 'name',
|
|
297
|
+
description: 'The primary identifier for this agent',
|
|
298
|
+
fieldType: 'text',
|
|
299
|
+
getValue: (char) => char.name || '',
|
|
300
|
+
tooltip:
|
|
301
|
+
'Display name that will be visible to users. Required for identification purposes.',
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
title: 'Username',
|
|
305
|
+
name: 'username',
|
|
306
|
+
description: 'Used in URLs and API endpoints',
|
|
307
|
+
fieldType: 'text',
|
|
308
|
+
getValue: (char) => char.username || '',
|
|
309
|
+
tooltip: 'Unique identifier for your agent. Used in APIs/URLs and Rooms.',
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
title: 'System',
|
|
313
|
+
name: 'system',
|
|
314
|
+
description: 'System prompt defining agent behavior',
|
|
315
|
+
fieldType: 'textarea',
|
|
316
|
+
getValue: (char) => char.system || '',
|
|
317
|
+
tooltip:
|
|
318
|
+
'Instructions for the AI model that establish core behavior patterns and personality traits.',
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
title: 'Voice Model',
|
|
322
|
+
name: 'settings.voice.model',
|
|
323
|
+
description: 'Voice model for audio synthesis',
|
|
324
|
+
fieldType: 'select',
|
|
325
|
+
getValue: (char) => (char.settings as Record<string, any>)?.voice?.model || '',
|
|
326
|
+
options: allVoiceModels.map((model) => ({
|
|
327
|
+
value: model.value,
|
|
328
|
+
label: model.label,
|
|
329
|
+
})),
|
|
330
|
+
tooltip: "Select a voice that aligns with the agent's intended persona.",
|
|
331
|
+
},
|
|
332
|
+
] as InputField[],
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
sectionTitle: 'Content',
|
|
336
|
+
sectionValue: 'content',
|
|
337
|
+
sectionType: SECTION_TYPE.ARRAY,
|
|
338
|
+
fields: [
|
|
339
|
+
{
|
|
340
|
+
title: 'Bio',
|
|
341
|
+
description: 'Bio data for this agent',
|
|
342
|
+
path: 'bio',
|
|
343
|
+
getData: (char) => (Array.isArray(char.bio) ? char.bio : []),
|
|
344
|
+
tooltip: "Biographical details that establish the agent's background and context.",
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
title: 'Topics',
|
|
348
|
+
description: 'Topics this agent can talk about',
|
|
349
|
+
path: 'topics',
|
|
350
|
+
getData: (char) => char.topics || [],
|
|
351
|
+
tooltip: 'Subject domains the agent can discuss with confidence.',
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
title: 'Adjectives',
|
|
355
|
+
description: 'Descriptive personality traits',
|
|
356
|
+
path: 'adjectives',
|
|
357
|
+
getData: (char) => char.adjectives || [],
|
|
358
|
+
tooltip: "Key personality attributes that define the agent's character.",
|
|
359
|
+
},
|
|
360
|
+
] as ArrayField[],
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
sectionTitle: 'Style',
|
|
364
|
+
sectionValue: 'style',
|
|
365
|
+
sectionType: SECTION_TYPE.ARRAY,
|
|
366
|
+
fields: [
|
|
367
|
+
{
|
|
368
|
+
title: 'All Styles',
|
|
369
|
+
description: 'Writing style for all content types',
|
|
370
|
+
path: 'style.all',
|
|
371
|
+
getData: (char) => char.style?.all || [],
|
|
372
|
+
tooltip: 'Core writing style guidelines applied across all content formats.',
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
title: 'Chat Style',
|
|
376
|
+
description: 'Style specific to chat interactions',
|
|
377
|
+
path: 'style.chat',
|
|
378
|
+
getData: (char) => char.style?.chat || [],
|
|
379
|
+
tooltip: 'Writing style specific to conversational exchanges.',
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
title: 'Post Style',
|
|
383
|
+
description: 'Style for long-form content',
|
|
384
|
+
path: 'style.post',
|
|
385
|
+
getData: (char) => char.style?.post || [],
|
|
386
|
+
tooltip: 'Writing style for structured content such as articles or posts.',
|
|
387
|
+
},
|
|
388
|
+
] as ArrayField[],
|
|
389
|
+
},
|
|
390
|
+
],
|
|
391
|
+
[allVoiceModels]
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
395
|
+
const { name, value, type } = e.target;
|
|
396
|
+
const checked = (e.target as HTMLInputElement).checked;
|
|
397
|
+
|
|
398
|
+
if (type === 'checkbox') {
|
|
399
|
+
setCharacterValue.updateField(name, checked);
|
|
400
|
+
} else if (name.startsWith('settings.')) {
|
|
401
|
+
// Handle nested settings fields like settings.voice.model
|
|
402
|
+
const path = name.substring(9); // Remove 'settings.' prefix
|
|
403
|
+
|
|
404
|
+
if (setCharacterValue.updateSetting) {
|
|
405
|
+
// Use the specialized method if available
|
|
406
|
+
setCharacterValue.updateSetting(path, value);
|
|
407
|
+
} else {
|
|
408
|
+
// Fall back to generic updateField
|
|
409
|
+
setCharacterValue.updateField(name, value);
|
|
410
|
+
}
|
|
411
|
+
} else {
|
|
412
|
+
setCharacterValue.updateField(name, value);
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
const handleVoiceModelChange = (value: string, name: string) => {
|
|
417
|
+
if (name.startsWith('settings.')) {
|
|
418
|
+
const path = name.substring(9); // Remove 'settings.' prefix
|
|
419
|
+
|
|
420
|
+
if (setCharacterValue.updateSetting) {
|
|
421
|
+
// Use the specialized method if available
|
|
422
|
+
setCharacterValue.updateSetting(path, value);
|
|
423
|
+
|
|
424
|
+
// Handle voice model change and required plugins
|
|
425
|
+
if (path === 'voice.model' && value) {
|
|
426
|
+
const voiceModel = getVoiceModelByValue(value);
|
|
427
|
+
if (voiceModel) {
|
|
428
|
+
const currentPlugins = Array.isArray(characterValue.plugins)
|
|
429
|
+
? [...characterValue.plugins]
|
|
430
|
+
: [];
|
|
431
|
+
const previousVoiceModel = getVoiceModelByValue(
|
|
432
|
+
(characterValue.settings as Record<string, any>)?.voice?.model
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
// Get the required plugin for the new voice model
|
|
436
|
+
const requiredPlugin = providerPluginMap[voiceModel.provider];
|
|
437
|
+
|
|
438
|
+
// Add the required plugin for the selected voice model
|
|
439
|
+
const newPlugins = [...currentPlugins];
|
|
440
|
+
if (requiredPlugin && !currentPlugins.includes(requiredPlugin)) {
|
|
441
|
+
newPlugins.push(requiredPlugin);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Update the plugins
|
|
445
|
+
if (setCharacterValue.setPlugins) {
|
|
446
|
+
setCharacterValue.setPlugins(newPlugins);
|
|
447
|
+
} else if (setCharacterValue.updateField) {
|
|
448
|
+
setCharacterValue.updateField('plugins', newPlugins);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Show toast notification
|
|
452
|
+
if (previousVoiceModel?.provider !== voiceModel.provider) {
|
|
453
|
+
toast({
|
|
454
|
+
title: 'Plugin Updated',
|
|
455
|
+
description: `${requiredPlugin} plugin has been set for the selected voice model.`,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
} else {
|
|
461
|
+
// Fall back to generic updateField
|
|
462
|
+
setCharacterValue.updateField(name, value);
|
|
463
|
+
}
|
|
464
|
+
} else {
|
|
465
|
+
setCharacterValue.updateField(name, value);
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
const updateArray = (path: string, newData: string[]) => {
|
|
470
|
+
// If the path is a simple field name
|
|
471
|
+
if (!path.includes('.')) {
|
|
472
|
+
setCharacterValue.updateField(path, newData);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Handle nested paths (e.g. style.all)
|
|
477
|
+
const parts = path.split('.');
|
|
478
|
+
if (parts.length === 2 && parts[0] === 'style') {
|
|
479
|
+
// For style arrays, use the setStyleArray method if available
|
|
480
|
+
if (setCharacterValue.setStyleArray) {
|
|
481
|
+
setCharacterValue.setStyleArray(parts[1] as 'all' | 'chat' | 'post', newData);
|
|
482
|
+
} else {
|
|
483
|
+
setCharacterValue.updateField(path, newData);
|
|
484
|
+
}
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Default case - just update the field
|
|
489
|
+
setCharacterValue.updateField(path, newData);
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
const ensureAvatarSize = async (char: Agent): Promise<Agent> => {
|
|
493
|
+
if (char.settings?.avatar) {
|
|
494
|
+
const img = new Image();
|
|
495
|
+
img.src = char.settings.avatar as string;
|
|
496
|
+
await new Promise((resolve) => (img.onload = resolve));
|
|
497
|
+
|
|
498
|
+
if (img.width > AVATAR_IMAGE_MAX_SIZE || img.height > AVATAR_IMAGE_MAX_SIZE) {
|
|
499
|
+
const response = await fetch(char.settings.avatar as string);
|
|
500
|
+
const blob = await response.blob();
|
|
501
|
+
const file = new File([blob], 'avatar.jpg', { type: blob.type });
|
|
502
|
+
const compressedImage = await compressImage(file);
|
|
503
|
+
return {
|
|
504
|
+
...char,
|
|
505
|
+
settings: {
|
|
506
|
+
...char.settings,
|
|
507
|
+
avatar: compressedImage,
|
|
508
|
+
},
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return char;
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
const handleFormSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
|
516
|
+
e.preventDefault();
|
|
517
|
+
setIsSubmitting(true);
|
|
518
|
+
|
|
519
|
+
try {
|
|
520
|
+
const updatedCharacter = await ensureAvatarSize(characterValue);
|
|
521
|
+
|
|
522
|
+
// Validate required secrets
|
|
523
|
+
let missingSecrets: string[] = [];
|
|
524
|
+
|
|
525
|
+
// If secret panel is mounted, use it for validation (has most up-to-date data)
|
|
526
|
+
if (secretPanelRef?.current) {
|
|
527
|
+
const secretValidation = secretPanelRef.current.validateSecrets();
|
|
528
|
+
missingSecrets = secretValidation.missingSecrets;
|
|
529
|
+
} else {
|
|
530
|
+
// Secret panel not mounted - validate based on current character settings
|
|
531
|
+
const secretsObj = updatedCharacter.settings?.secrets;
|
|
532
|
+
const currentSecrets =
|
|
533
|
+
secretsObj && typeof secretsObj === 'object' && !Array.isArray(secretsObj)
|
|
534
|
+
? (secretsObj as Record<string, any>)
|
|
535
|
+
: {};
|
|
536
|
+
|
|
537
|
+
missingSecrets = requiredSecrets
|
|
538
|
+
.filter((secret) => {
|
|
539
|
+
const value = currentSecrets[secret.name];
|
|
540
|
+
// Check agent-specific secret
|
|
541
|
+
if (value && typeof value === 'string' && value.trim() !== '') {
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
// Check global environment
|
|
545
|
+
const globalValue = globalEnvs[secret.name];
|
|
546
|
+
if (globalValue && globalValue.trim() !== '') {
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
return true;
|
|
550
|
+
})
|
|
551
|
+
.map((secret) => secret.name);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (missingSecrets.length > 0) {
|
|
555
|
+
// Show the warning dialog
|
|
556
|
+
setIsSubmitting(false);
|
|
557
|
+
setPendingSubmit(updatedCharacter);
|
|
558
|
+
setShowMissingSecretsDialog(true);
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
await onSubmit(updatedCharacter);
|
|
563
|
+
} catch (error) {
|
|
564
|
+
toast({
|
|
565
|
+
title: 'Error',
|
|
566
|
+
description: error instanceof Error ? error.message : 'Failed to update agent',
|
|
567
|
+
variant: 'destructive',
|
|
568
|
+
});
|
|
569
|
+
} finally {
|
|
570
|
+
setIsSubmitting(false);
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
// Handle confirmation from missing secrets dialog
|
|
575
|
+
const handleConfirmSaveWithMissingSecrets = async () => {
|
|
576
|
+
setShowMissingSecretsDialog(false);
|
|
577
|
+
if (pendingSubmit) {
|
|
578
|
+
setIsSubmitting(true);
|
|
579
|
+
try {
|
|
580
|
+
await onSubmit(pendingSubmit);
|
|
581
|
+
} catch (error) {
|
|
582
|
+
toast({
|
|
583
|
+
title: 'Error',
|
|
584
|
+
description: error instanceof Error ? error.message : 'Failed to update agent',
|
|
585
|
+
variant: 'destructive',
|
|
586
|
+
});
|
|
587
|
+
} finally {
|
|
588
|
+
setIsSubmitting(false);
|
|
589
|
+
setPendingSubmit(null);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
// Handle cancellation from missing secrets dialog
|
|
595
|
+
const handleCancelSaveWithMissingSecrets = () => {
|
|
596
|
+
setShowMissingSecretsDialog(false);
|
|
597
|
+
setPendingSubmit(null);
|
|
598
|
+
// Switch to the Secret tab to show the user what's missing
|
|
599
|
+
setActiveTab('custom-Secret');
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
const renderInputField = (field: InputField) => (
|
|
603
|
+
<div
|
|
604
|
+
key={field.name}
|
|
605
|
+
className={`w-full ${field.name === 'name' ? 'agent-form-name' : ''} ${field.name === 'system' ? 'agent-form-system-prompt' : ''}`}
|
|
606
|
+
>
|
|
607
|
+
<Label htmlFor={field.name} className="text-sm font-normal block mb-2">
|
|
608
|
+
{field.title}
|
|
609
|
+
{field.name in FIELD_REQUIREMENTS &&
|
|
610
|
+
(FIELD_REQUIREMENTS as Record<string, FIELD_REQUIREMENT_TYPE>)[field.name] ===
|
|
611
|
+
FIELD_REQUIREMENT_TYPE.REQUIRED && <span className="text-destructive ml-1">*</span>}
|
|
612
|
+
</Label>
|
|
613
|
+
|
|
614
|
+
{field.fieldType === 'textarea' ? (
|
|
615
|
+
<Textarea
|
|
616
|
+
id={field.name}
|
|
617
|
+
name={field.name}
|
|
618
|
+
value={field.getValue(characterValue)}
|
|
619
|
+
onChange={handleChange}
|
|
620
|
+
className="min-h-[120px] resize-y"
|
|
621
|
+
/>
|
|
622
|
+
) : field.fieldType === 'checkbox' ? (
|
|
623
|
+
<Input
|
|
624
|
+
id={field.name}
|
|
625
|
+
name={field.name}
|
|
626
|
+
type="checkbox"
|
|
627
|
+
checked={(characterValue as Record<string, any>)[field.name] === 'true'}
|
|
628
|
+
onChange={handleChange}
|
|
629
|
+
/>
|
|
630
|
+
) : field.fieldType === 'select' ? (
|
|
631
|
+
<Select
|
|
632
|
+
name={field.name}
|
|
633
|
+
value={field.getValue(characterValue)}
|
|
634
|
+
onValueChange={(value) => handleVoiceModelChange(value, field.name)}
|
|
635
|
+
>
|
|
636
|
+
<SelectTrigger>
|
|
637
|
+
<SelectValue
|
|
638
|
+
placeholder={
|
|
639
|
+
field.name.includes('voice.model') && isLoadingVoices
|
|
640
|
+
? 'Loading voice models...'
|
|
641
|
+
: 'Select a voice model'
|
|
642
|
+
}
|
|
643
|
+
/>
|
|
644
|
+
</SelectTrigger>
|
|
645
|
+
<SelectContent>
|
|
646
|
+
{field.name === 'settings.voice.model' ? (
|
|
647
|
+
<>
|
|
648
|
+
<SelectGroup>
|
|
649
|
+
<SelectItem value="none">No Voice</SelectItem>
|
|
650
|
+
</SelectGroup>
|
|
651
|
+
|
|
652
|
+
<SelectSeparator />
|
|
653
|
+
|
|
654
|
+
<SelectGroup>
|
|
655
|
+
<SelectLabel>OpenAI Voices</SelectLabel>
|
|
656
|
+
{openAIVoiceModels.map((model) => (
|
|
657
|
+
<SelectItem key={model.value} value={model.value}>
|
|
658
|
+
{model.label.replace('OpenAI - ', '')}
|
|
659
|
+
</SelectItem>
|
|
660
|
+
))}
|
|
661
|
+
</SelectGroup>
|
|
662
|
+
|
|
663
|
+
<SelectSeparator />
|
|
664
|
+
|
|
665
|
+
<SelectGroup>
|
|
666
|
+
<SelectLabel>ElevenLabs Voices</SelectLabel>
|
|
667
|
+
{/* Show default ElevenLabs voices from config */}
|
|
668
|
+
{elevenLabsVoiceModels.map((model) => (
|
|
669
|
+
<SelectItem key={model.value} value={model.value}>
|
|
670
|
+
{model.label.replace('ElevenLabs - ', '')}
|
|
671
|
+
</SelectItem>
|
|
672
|
+
))}
|
|
673
|
+
{/* Show custom ElevenLabs voices if available */}
|
|
674
|
+
{elevenlabsVoices &&
|
|
675
|
+
elevenlabsVoices.length > 0 &&
|
|
676
|
+
elevenlabsVoices.map((voice) => (
|
|
677
|
+
<SelectItem key={voice.value} value={voice.value}>
|
|
678
|
+
{voice.label.replace('ElevenLabs - ', '')}
|
|
679
|
+
</SelectItem>
|
|
680
|
+
))}
|
|
681
|
+
</SelectGroup>
|
|
682
|
+
</>
|
|
683
|
+
) : (
|
|
684
|
+
field.options?.map((option) => (
|
|
685
|
+
<SelectItem key={option.value} value={option.value}>
|
|
686
|
+
{option.label}
|
|
687
|
+
</SelectItem>
|
|
688
|
+
))
|
|
689
|
+
)}
|
|
690
|
+
</SelectContent>
|
|
691
|
+
</Select>
|
|
692
|
+
) : (
|
|
693
|
+
<Input
|
|
694
|
+
id={field.name}
|
|
695
|
+
name={field.name}
|
|
696
|
+
type={field.fieldType}
|
|
697
|
+
value={field.getValue(characterValue)}
|
|
698
|
+
onChange={handleChange}
|
|
699
|
+
/>
|
|
700
|
+
)}
|
|
701
|
+
|
|
702
|
+
{field.description && (
|
|
703
|
+
<p className="text-xs text-muted-foreground mt-1">{field.description}</p>
|
|
704
|
+
)}
|
|
705
|
+
</div>
|
|
706
|
+
);
|
|
707
|
+
|
|
708
|
+
const renderArrayField = (field: ArrayField) => (
|
|
709
|
+
<div key={field.path} className="w-full">
|
|
710
|
+
<Label htmlFor={field.path} className="text-sm font-normal block mb-2">
|
|
711
|
+
{field.title}
|
|
712
|
+
{field.path in FIELD_REQUIREMENTS &&
|
|
713
|
+
(FIELD_REQUIREMENTS as Record<string, FIELD_REQUIREMENT_TYPE>)[field.path] ===
|
|
714
|
+
FIELD_REQUIREMENT_TYPE.REQUIRED && <span className="text-destructive ml-1">*</span>}
|
|
715
|
+
</Label>
|
|
716
|
+
|
|
717
|
+
<ArrayInput
|
|
718
|
+
data={field.getData(characterValue)}
|
|
719
|
+
onChange={(newData) => updateArray(field.path, newData)}
|
|
720
|
+
/>
|
|
721
|
+
|
|
722
|
+
{field.description && (
|
|
723
|
+
<p className="text-xs text-muted-foreground mt-1">{field.description}</p>
|
|
724
|
+
)}
|
|
725
|
+
</div>
|
|
726
|
+
);
|
|
727
|
+
|
|
728
|
+
const handleExportJSON = () => {
|
|
729
|
+
exportCharacterAsJson(characterValue, toast);
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
function isV1Character(char: unknown): char is V1Character {
|
|
733
|
+
return (
|
|
734
|
+
typeof char === 'object' &&
|
|
735
|
+
char !== null &&
|
|
736
|
+
('lore' in char || 'clients' in char || 'modelProvider' in char)
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const handleImportJSON = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
741
|
+
const file = event.target.files?.[0];
|
|
742
|
+
if (!file) return;
|
|
743
|
+
|
|
744
|
+
try {
|
|
745
|
+
const text = await file.text();
|
|
746
|
+
let json: Character = JSON.parse(text);
|
|
747
|
+
|
|
748
|
+
if (isV1Character(json)) {
|
|
749
|
+
json = convertCharacter(json);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Check for required fields using FIELD_REQUIREMENTS
|
|
753
|
+
const missingFields = (
|
|
754
|
+
Object.keys(FIELD_REQUIREMENTS) as Array<keyof typeof FIELD_REQUIREMENTS>
|
|
755
|
+
).filter((field) => {
|
|
756
|
+
if (FIELD_REQUIREMENTS[field] !== FIELD_REQUIREMENT_TYPE.REQUIRED) return false;
|
|
757
|
+
|
|
758
|
+
// Handle nested fields like style.all
|
|
759
|
+
const parts = field.split('.');
|
|
760
|
+
let current: any = json;
|
|
761
|
+
|
|
762
|
+
for (const part of parts) {
|
|
763
|
+
current = current?.[part];
|
|
764
|
+
if (current === undefined) return true; // field missing
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
return false;
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
if (missingFields.length > 0) {
|
|
771
|
+
toast({
|
|
772
|
+
title: 'Import Failed',
|
|
773
|
+
description: `Missing required fields: ${missingFields.join(', ')}`,
|
|
774
|
+
variant: 'destructive',
|
|
775
|
+
});
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
if (setCharacterValue.importAgent) {
|
|
780
|
+
setCharacterValue.importAgent(json);
|
|
781
|
+
} else {
|
|
782
|
+
console.warn('Missing importAgent method');
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
toast({
|
|
786
|
+
title: 'Agent Imported',
|
|
787
|
+
description: 'Agent data has been successfully loaded.',
|
|
788
|
+
});
|
|
789
|
+
} catch (error) {
|
|
790
|
+
toast({
|
|
791
|
+
title: 'Import Failed',
|
|
792
|
+
description: error instanceof Error ? error.message : 'Invalid JSON file',
|
|
793
|
+
variant: 'destructive',
|
|
794
|
+
});
|
|
795
|
+
} finally {
|
|
796
|
+
event.target.value = '';
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
|
|
800
|
+
// File input ref for import functionality
|
|
801
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
802
|
+
|
|
803
|
+
const handleImportClick = () => {
|
|
804
|
+
fileInputRef.current?.click();
|
|
805
|
+
};
|
|
806
|
+
|
|
807
|
+
// Define stop/delete options (only if both are available)
|
|
808
|
+
const stopDeleteOptions = useMemo(() => {
|
|
809
|
+
const options = [];
|
|
810
|
+
|
|
811
|
+
if (onStopAgent) {
|
|
812
|
+
options.push({
|
|
813
|
+
label: 'Stop Agent',
|
|
814
|
+
description: 'Stop running',
|
|
815
|
+
onClick: onStopAgent,
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
if (onDelete) {
|
|
820
|
+
options.push({
|
|
821
|
+
label: 'Delete Agent',
|
|
822
|
+
description: 'Delete permanently',
|
|
823
|
+
onClick: () => onDelete(),
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
return options;
|
|
828
|
+
}, [onStopAgent, onDelete]);
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Handle template selection
|
|
832
|
+
*/
|
|
833
|
+
const handleTemplateChange = useCallback(
|
|
834
|
+
(templateId: string) => {
|
|
835
|
+
setSelectedTemplate(templateId);
|
|
836
|
+
|
|
837
|
+
// If "None" is selected, reset to blank form if reset function is available
|
|
838
|
+
if (templateId === 'none' && onReset) {
|
|
839
|
+
onReset();
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Get the template data
|
|
844
|
+
const template = getTemplateById(templateId);
|
|
845
|
+
if (template && setCharacterValue.importAgent) {
|
|
846
|
+
// Use the importAgent function to set all template values at once
|
|
847
|
+
setCharacterValue.importAgent(template.template as Character);
|
|
848
|
+
// Notify parent of template change
|
|
849
|
+
onTemplateChange?.();
|
|
850
|
+
}
|
|
851
|
+
},
|
|
852
|
+
[onReset, setCharacterValue, onTemplateChange]
|
|
853
|
+
);
|
|
854
|
+
|
|
855
|
+
// Create all tabs data with better short labels
|
|
856
|
+
const allTabs = [
|
|
857
|
+
...AGENT_FORM_SCHEMA.map((section) => ({
|
|
858
|
+
value: section.sectionValue,
|
|
859
|
+
label: section.sectionTitle,
|
|
860
|
+
shortLabel: section.sectionTitle.split(' ')[0], // Use first word for mobile
|
|
861
|
+
})),
|
|
862
|
+
...customComponents.map((component) => ({
|
|
863
|
+
value: `custom-${component.name}`,
|
|
864
|
+
label: component.name,
|
|
865
|
+
shortLabel: (component as any).shortLabel || component.name.split(' ')[0], // Use first word
|
|
866
|
+
})),
|
|
867
|
+
];
|
|
868
|
+
|
|
869
|
+
return (
|
|
870
|
+
<div ref={containerRef} className="w-full max-w-full mx-auto p-6 sm:p-8 h-full overflow-y-auto">
|
|
871
|
+
{(title || description) && (
|
|
872
|
+
<div className="mb-8">
|
|
873
|
+
{title && <h1 className="text-2xl font-semibold mb-2">{title}</h1>}
|
|
874
|
+
{description && (
|
|
875
|
+
<p className="text-sm text-muted-foreground whitespace-pre-line">{description}</p>
|
|
876
|
+
)}
|
|
877
|
+
</div>
|
|
878
|
+
)}
|
|
879
|
+
|
|
880
|
+
{/* Template Selector */}
|
|
881
|
+
<div className="mb-8">
|
|
882
|
+
<Label htmlFor="template-selector" className="text-sm">
|
|
883
|
+
Start with a template
|
|
884
|
+
</Label>
|
|
885
|
+
<Select value={selectedTemplate} onValueChange={handleTemplateChange}>
|
|
886
|
+
<SelectTrigger className="w-full mt-1">
|
|
887
|
+
<SelectValue placeholder="None (blank start)" />
|
|
888
|
+
</SelectTrigger>
|
|
889
|
+
<SelectContent>
|
|
890
|
+
{agentTemplates.map((template) => (
|
|
891
|
+
<SelectItem key={template.id} value={template.id}>
|
|
892
|
+
<TooltipProvider>
|
|
893
|
+
<Tooltip>
|
|
894
|
+
<TooltipTrigger asChild>
|
|
895
|
+
<span className="w-full text-left">{template.label}</span>
|
|
896
|
+
</TooltipTrigger>
|
|
897
|
+
<TooltipContent side="right">
|
|
898
|
+
<p className="max-w-xs">{template.description}</p>
|
|
899
|
+
</TooltipContent>
|
|
900
|
+
</Tooltip>
|
|
901
|
+
</TooltipProvider>
|
|
902
|
+
</SelectItem>
|
|
903
|
+
))}
|
|
904
|
+
</SelectContent>
|
|
905
|
+
</Select>
|
|
906
|
+
</div>
|
|
907
|
+
|
|
908
|
+
<form onSubmit={handleFormSubmit}>
|
|
909
|
+
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
|
910
|
+
<div className="relative mb-4">
|
|
911
|
+
{/* Scroll button left */}
|
|
912
|
+
{showLeftScroll && (
|
|
913
|
+
<Button
|
|
914
|
+
type="button"
|
|
915
|
+
variant="ghost"
|
|
916
|
+
size="sm"
|
|
917
|
+
className="absolute left-0 top-1/2 -translate-y-1/2 z-10 h-8 w-8 p-0 bg-background/80 backdrop-blur-sm shadow-md"
|
|
918
|
+
onClick={() => scrollTabs('left')}
|
|
919
|
+
>
|
|
920
|
+
<ChevronLeft className="h-4 w-4" />
|
|
921
|
+
</Button>
|
|
922
|
+
)}
|
|
923
|
+
|
|
924
|
+
{/* Tabs container */}
|
|
925
|
+
<div ref={tabsContainerRef} className="overflow-x-auto scrollbar-hide">
|
|
926
|
+
<TabsList className="inline-flex h-10 items-center justify-start text-muted-foreground w-full">
|
|
927
|
+
{allTabs.map((tab) => (
|
|
928
|
+
<TabsTrigger
|
|
929
|
+
key={tab.value}
|
|
930
|
+
value={tab.value}
|
|
931
|
+
className={cn(
|
|
932
|
+
'whitespace-nowrap',
|
|
933
|
+
!showLabels && 'px-2 text-xs' // Smaller padding and text on mobile
|
|
934
|
+
)}
|
|
935
|
+
>
|
|
936
|
+
{showLabels ? tab.label : tab.shortLabel}
|
|
937
|
+
</TabsTrigger>
|
|
938
|
+
))}
|
|
939
|
+
</TabsList>
|
|
940
|
+
</div>
|
|
941
|
+
|
|
942
|
+
{/* Scroll button right */}
|
|
943
|
+
{showRightScroll && (
|
|
944
|
+
<Button
|
|
945
|
+
type="button"
|
|
946
|
+
variant="ghost"
|
|
947
|
+
size="sm"
|
|
948
|
+
className="absolute right-0 top-1/2 -translate-y-1/2 z-10 h-8 w-8 p-0 bg-background/80 backdrop-blur-sm shadow-md"
|
|
949
|
+
onClick={() => scrollTabs('right')}
|
|
950
|
+
>
|
|
951
|
+
<ChevronRight className="h-4 w-4" />
|
|
952
|
+
</Button>
|
|
953
|
+
)}
|
|
954
|
+
</div>
|
|
955
|
+
|
|
956
|
+
<div className="max-h-[100vh] overflow-y-auto pb-4 px-1">
|
|
957
|
+
{AGENT_FORM_SCHEMA.map((section) => (
|
|
958
|
+
<TabsContent
|
|
959
|
+
key={section.sectionValue}
|
|
960
|
+
value={section.sectionValue}
|
|
961
|
+
className="space-y-6 mt-0 focus:outline-none"
|
|
962
|
+
>
|
|
963
|
+
{section.sectionType === SECTION_TYPE.INPUT
|
|
964
|
+
? (section.fields as InputField[]).map(renderInputField)
|
|
965
|
+
: (section.fields as ArrayField[]).map(renderArrayField)}
|
|
966
|
+
</TabsContent>
|
|
967
|
+
))}
|
|
968
|
+
{customComponents.map((component) => (
|
|
969
|
+
<TabsContent
|
|
970
|
+
key={`custom-${component.name}`}
|
|
971
|
+
value={`custom-${component.name}`}
|
|
972
|
+
className="mt-0 focus:outline-none"
|
|
973
|
+
>
|
|
974
|
+
<div className="h-full">{component.component}</div>
|
|
975
|
+
</TabsContent>
|
|
976
|
+
))}
|
|
977
|
+
</div>
|
|
978
|
+
</Tabs>
|
|
979
|
+
|
|
980
|
+
<div className="flex items-center justify-between w-full mt-6">
|
|
981
|
+
{/* Three-dot menu button on the left */}
|
|
982
|
+
<DropdownMenu>
|
|
983
|
+
<DropdownMenuTrigger asChild>
|
|
984
|
+
<Button type="button" variant="outline" size="icon" className="flex-shrink-0 p-2.5">
|
|
985
|
+
<MoreVertical className="h-4 w-4" />
|
|
986
|
+
<span className="sr-only">More options</span>
|
|
987
|
+
</Button>
|
|
988
|
+
</DropdownMenuTrigger>
|
|
989
|
+
<DropdownMenuContent align="start" side="top">
|
|
990
|
+
<DropdownMenuItem onClick={handleImportClick}>
|
|
991
|
+
<ArrowDownToLine className="h-4 w-4 mr-2" />
|
|
992
|
+
Import
|
|
993
|
+
</DropdownMenuItem>
|
|
994
|
+
<DropdownMenuItem onClick={handleExportJSON}>
|
|
995
|
+
<ArrowUpFromLine className="h-4 w-4 mr-2" />
|
|
996
|
+
Export
|
|
997
|
+
</DropdownMenuItem>
|
|
998
|
+
{stopDeleteOptions.length > 0 && <DropdownMenuSeparator />}
|
|
999
|
+
{stopDeleteOptions.length > 0 &&
|
|
1000
|
+
stopDeleteOptions.map((option) => {
|
|
1001
|
+
const isStopAction = option.label === 'Stop Agent';
|
|
1002
|
+
const isDeleteAction = option.label === 'Delete Agent';
|
|
1003
|
+
const isLoading = isStopAction ? isStopping : isDeleteAction ? isDeleting : false;
|
|
1004
|
+
|
|
1005
|
+
return (
|
|
1006
|
+
<DropdownMenuItem
|
|
1007
|
+
key={option.label}
|
|
1008
|
+
onClick={option.onClick}
|
|
1009
|
+
disabled={isLoading}
|
|
1010
|
+
className={
|
|
1011
|
+
isDeleteAction
|
|
1012
|
+
? 'text-destructive focus:text-destructive hover:bg-red-50 dark:hover:bg-red-950/50'
|
|
1013
|
+
: ''
|
|
1014
|
+
}
|
|
1015
|
+
>
|
|
1016
|
+
{isLoading ? (
|
|
1017
|
+
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
|
1018
|
+
) : isStopAction ? (
|
|
1019
|
+
<StopCircle className="h-4 w-4 mr-2" />
|
|
1020
|
+
) : (
|
|
1021
|
+
<Trash className="h-4 w-4 mr-2" />
|
|
1022
|
+
)}
|
|
1023
|
+
{isLoading ? `${isStopAction ? 'Stopping' : 'Deleting'}...` : option.label}
|
|
1024
|
+
</DropdownMenuItem>
|
|
1025
|
+
);
|
|
1026
|
+
})}
|
|
1027
|
+
</DropdownMenuContent>
|
|
1028
|
+
</DropdownMenu>
|
|
1029
|
+
|
|
1030
|
+
{/* Reset and Save buttons on the right with gap */}
|
|
1031
|
+
<div className="flex items-center gap-3">
|
|
1032
|
+
<TooltipProvider>
|
|
1033
|
+
<Tooltip>
|
|
1034
|
+
<TooltipTrigger asChild>
|
|
1035
|
+
<Button
|
|
1036
|
+
type="button"
|
|
1037
|
+
variant="outline"
|
|
1038
|
+
onClick={() => {
|
|
1039
|
+
onReset?.();
|
|
1040
|
+
}}
|
|
1041
|
+
>
|
|
1042
|
+
<RotateCcw className="h-4 w-4" />
|
|
1043
|
+
<span className="ml-2">Reset</span>
|
|
1044
|
+
</Button>
|
|
1045
|
+
</TooltipTrigger>
|
|
1046
|
+
<TooltipContent>
|
|
1047
|
+
<p>Reset all form fields to their original values</p>
|
|
1048
|
+
</TooltipContent>
|
|
1049
|
+
</Tooltip>
|
|
1050
|
+
</TooltipProvider>
|
|
1051
|
+
|
|
1052
|
+
<TooltipProvider>
|
|
1053
|
+
<Tooltip>
|
|
1054
|
+
<TooltipTrigger asChild>
|
|
1055
|
+
<Button type="submit" disabled={isSubmitting} className="agent-form-submit">
|
|
1056
|
+
{isSubmitting ? (
|
|
1057
|
+
<>
|
|
1058
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
1059
|
+
<span className="ml-2">Saving...</span>
|
|
1060
|
+
</>
|
|
1061
|
+
) : (
|
|
1062
|
+
<>
|
|
1063
|
+
<Save className="h-4 w-4" />
|
|
1064
|
+
<span className="ml-2">Save</span>
|
|
1065
|
+
</>
|
|
1066
|
+
)}
|
|
1067
|
+
</Button>
|
|
1068
|
+
</TooltipTrigger>
|
|
1069
|
+
<TooltipContent>
|
|
1070
|
+
<p>Save all changes to the agent configuration</p>
|
|
1071
|
+
</TooltipContent>
|
|
1072
|
+
</Tooltip>
|
|
1073
|
+
</TooltipProvider>
|
|
1074
|
+
</div>
|
|
1075
|
+
|
|
1076
|
+
{/* Hidden file input for import */}
|
|
1077
|
+
<input
|
|
1078
|
+
ref={fileInputRef}
|
|
1079
|
+
type="file"
|
|
1080
|
+
accept=".json"
|
|
1081
|
+
onChange={handleImportJSON}
|
|
1082
|
+
className="hidden"
|
|
1083
|
+
/>
|
|
1084
|
+
</div>
|
|
1085
|
+
</form>
|
|
1086
|
+
|
|
1087
|
+
{/* Missing Secrets Warning Dialog */}
|
|
1088
|
+
<MissingSecretsDialog
|
|
1089
|
+
open={showMissingSecretsDialog}
|
|
1090
|
+
onOpenChange={setShowMissingSecretsDialog}
|
|
1091
|
+
missingSecrets={(() => {
|
|
1092
|
+
let missingSecretNames: string[] = [];
|
|
1093
|
+
|
|
1094
|
+
// If secret panel is mounted, use it (has most up-to-date data)
|
|
1095
|
+
if (secretPanelRef?.current) {
|
|
1096
|
+
const validation = secretPanelRef.current.validateSecrets();
|
|
1097
|
+
missingSecretNames = validation.missingSecrets;
|
|
1098
|
+
} else {
|
|
1099
|
+
// Secret panel not mounted - calculate based on character value
|
|
1100
|
+
const secretsObj = characterValue.settings?.secrets;
|
|
1101
|
+
const currentSecrets =
|
|
1102
|
+
secretsObj && typeof secretsObj === 'object' && !Array.isArray(secretsObj)
|
|
1103
|
+
? (secretsObj as Record<string, any>)
|
|
1104
|
+
: {};
|
|
1105
|
+
|
|
1106
|
+
missingSecretNames = requiredSecrets
|
|
1107
|
+
.filter((secret) => {
|
|
1108
|
+
const value = currentSecrets[secret.name];
|
|
1109
|
+
// Check agent-specific secret
|
|
1110
|
+
if (value && typeof value === 'string' && value.trim() !== '') {
|
|
1111
|
+
return false;
|
|
1112
|
+
}
|
|
1113
|
+
// Check global environment
|
|
1114
|
+
const globalValue = globalEnvs[secret.name];
|
|
1115
|
+
if (globalValue && globalValue.trim() !== '') {
|
|
1116
|
+
return false;
|
|
1117
|
+
}
|
|
1118
|
+
return true;
|
|
1119
|
+
})
|
|
1120
|
+
.map((secret) => secret.name);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// Map secret names to full details
|
|
1124
|
+
return missingSecretNames.map((secretName) => {
|
|
1125
|
+
const reqSecret = requiredSecrets.find((s) => s.name === secretName);
|
|
1126
|
+
return {
|
|
1127
|
+
name: secretName,
|
|
1128
|
+
plugin: reqSecret?.plugin,
|
|
1129
|
+
description: reqSecret?.description,
|
|
1130
|
+
};
|
|
1131
|
+
});
|
|
1132
|
+
})()}
|
|
1133
|
+
onConfirm={handleConfirmSaveWithMissingSecrets}
|
|
1134
|
+
onCancel={handleCancelSaveWithMissingSecrets}
|
|
1135
|
+
/>
|
|
1136
|
+
</div>
|
|
1137
|
+
);
|
|
1138
|
+
}
|