@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,1505 @@
|
|
|
1
|
+
import { Button } from '@/components/ui/button';
|
|
2
|
+
import { Input } from '@/components/ui/input';
|
|
3
|
+
import { Textarea } from '@/components/ui/textarea';
|
|
4
|
+
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
5
|
+
import { Badge } from '@/components/ui/badge';
|
|
6
|
+
import {
|
|
7
|
+
Dialog,
|
|
8
|
+
DialogContent,
|
|
9
|
+
DialogDescription,
|
|
10
|
+
DialogFooter,
|
|
11
|
+
DialogHeader,
|
|
12
|
+
DialogTitle,
|
|
13
|
+
} from '@/components/ui/dialog';
|
|
14
|
+
import type { Agent } from '@elizaos/core';
|
|
15
|
+
import { decryptObjectValues, getSalt } from '@elizaos/core';
|
|
16
|
+
import {
|
|
17
|
+
Check,
|
|
18
|
+
CloudUpload,
|
|
19
|
+
Eye,
|
|
20
|
+
EyeOff,
|
|
21
|
+
X,
|
|
22
|
+
AlertCircle,
|
|
23
|
+
FileText,
|
|
24
|
+
Copy,
|
|
25
|
+
Trash2,
|
|
26
|
+
Edit3,
|
|
27
|
+
Globe,
|
|
28
|
+
} from 'lucide-react';
|
|
29
|
+
import {
|
|
30
|
+
useEffect,
|
|
31
|
+
useRef,
|
|
32
|
+
useState,
|
|
33
|
+
useImperativeHandle,
|
|
34
|
+
forwardRef,
|
|
35
|
+
useCallback,
|
|
36
|
+
useMemo,
|
|
37
|
+
} from 'react';
|
|
38
|
+
import { useRequiredSecrets } from '@/hooks/use-plugin-details';
|
|
39
|
+
import { Alert, AlertDescription } from '@/components/ui/alert';
|
|
40
|
+
import { Skeleton } from '@/components/ui/skeleton';
|
|
41
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
42
|
+
import { createElizaClient } from '@/lib/api-client-config';
|
|
43
|
+
|
|
44
|
+
type EnvVariable = {
|
|
45
|
+
name: string;
|
|
46
|
+
value: string;
|
|
47
|
+
isNew?: boolean;
|
|
48
|
+
isModified?: boolean;
|
|
49
|
+
isDeleted?: boolean;
|
|
50
|
+
isRequired?: boolean;
|
|
51
|
+
description?: string;
|
|
52
|
+
example?: string;
|
|
53
|
+
plugin?: string;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
interface SecretPanelProps {
|
|
57
|
+
characterValue: Agent;
|
|
58
|
+
onChange?: (secrets: Record<string, string | null>) => void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface SecretPanelRef {
|
|
62
|
+
getSecrets: () => Record<string, string | null>;
|
|
63
|
+
validateSecrets: () => { isValid: boolean; missingSecrets: string[] };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const SecretPanel = forwardRef<SecretPanelRef, SecretPanelProps>(
|
|
67
|
+
({ characterValue, onChange }, ref) => {
|
|
68
|
+
const [envs, setEnvs] = useState<EnvVariable[]>([]);
|
|
69
|
+
const [name, setName] = useState('');
|
|
70
|
+
const [value, setValue] = useState('');
|
|
71
|
+
const [showPassword, setShowPassword] = useState(false);
|
|
72
|
+
const [editingIndex, setEditingIndex] = useState<number | null>(null);
|
|
73
|
+
const [editedValue, setEditedValue] = useState('');
|
|
74
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
75
|
+
const [deletedKeys, setDeletedKeys] = useState<string[]>([]);
|
|
76
|
+
const [visibleSecrets, setVisibleSecrets] = useState<Set<number>>(new Set());
|
|
77
|
+
const [globalEnvs, setGlobalEnvs] = useState<Record<string, string>>({});
|
|
78
|
+
const [isLoadingGlobalEnvs, setIsLoadingGlobalEnvs] = useState(true);
|
|
79
|
+
|
|
80
|
+
// Raw editor modal state
|
|
81
|
+
const [rawEditorOpen, setRawEditorOpen] = useState(false);
|
|
82
|
+
const [rawEditorContent, setRawEditorContent] = useState('');
|
|
83
|
+
|
|
84
|
+
const lastAgentIdRef = useRef<string | null>(null);
|
|
85
|
+
const lastSecretsRef = useRef<string>('');
|
|
86
|
+
const dropRef = useRef<HTMLDivElement>(null);
|
|
87
|
+
const lastRequiredSecretsKeyRef = useRef<string>('');
|
|
88
|
+
|
|
89
|
+
// Get required secrets based on enabled plugins
|
|
90
|
+
const enabledPlugins = useMemo(() => characterValue?.plugins || [], [characterValue?.plugins]);
|
|
91
|
+
const { requiredSecrets, isLoading: isLoadingSecrets } = useRequiredSecrets(enabledPlugins);
|
|
92
|
+
|
|
93
|
+
// Fetch global environment variables
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
const fetchGlobalEnvs = async () => {
|
|
96
|
+
try {
|
|
97
|
+
setIsLoadingGlobalEnvs(true);
|
|
98
|
+
const elizaClient = createElizaClient();
|
|
99
|
+
const data = await elizaClient.system.getEnvironment();
|
|
100
|
+
setGlobalEnvs(data || {});
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error('Failed to fetch global environment variables:', error);
|
|
103
|
+
setGlobalEnvs({});
|
|
104
|
+
} finally {
|
|
105
|
+
setIsLoadingGlobalEnvs(false);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
fetchGlobalEnvs();
|
|
110
|
+
}, []);
|
|
111
|
+
|
|
112
|
+
// Function to get current secrets
|
|
113
|
+
const getCurrentSecrets = useCallback(() => {
|
|
114
|
+
const currentSecrets: Record<string, string | null> = {};
|
|
115
|
+
|
|
116
|
+
envs.forEach(({ name, value }) => {
|
|
117
|
+
currentSecrets[name] = value;
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
deletedKeys.forEach((key) => {
|
|
121
|
+
currentSecrets[key] = null;
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return currentSecrets;
|
|
125
|
+
}, [envs, deletedKeys]);
|
|
126
|
+
|
|
127
|
+
// Function to validate if all required secrets are provided
|
|
128
|
+
const validateSecrets = useCallback(() => {
|
|
129
|
+
const currentSecrets = getCurrentSecrets();
|
|
130
|
+
|
|
131
|
+
// If we're still loading required secrets, check against current envs marked as required
|
|
132
|
+
const secretsToCheck = isLoadingSecrets
|
|
133
|
+
? envs.filter((env) => env.isRequired)
|
|
134
|
+
: requiredSecrets;
|
|
135
|
+
|
|
136
|
+
const missingSecrets = secretsToCheck
|
|
137
|
+
.filter((secret) => {
|
|
138
|
+
const secretName = typeof secret === 'object' && 'name' in secret ? secret.name : secret;
|
|
139
|
+
const value = currentSecrets[secretName];
|
|
140
|
+
|
|
141
|
+
// Check if the value exists in current secrets
|
|
142
|
+
if (value && value.trim() !== '') {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Check if the value exists in global environment
|
|
147
|
+
if (globalEnvs[secretName] && globalEnvs[secretName].trim() !== '') {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return true;
|
|
152
|
+
})
|
|
153
|
+
.map((secret) => (typeof secret === 'object' && 'name' in secret ? secret.name : secret));
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
isValid: missingSecrets.length === 0,
|
|
157
|
+
missingSecrets,
|
|
158
|
+
};
|
|
159
|
+
}, [getCurrentSecrets, requiredSecrets, isLoadingSecrets, envs, globalEnvs]);
|
|
160
|
+
|
|
161
|
+
// Expose methods to get current secrets state and validate
|
|
162
|
+
useImperativeHandle(
|
|
163
|
+
ref,
|
|
164
|
+
() => ({
|
|
165
|
+
getSecrets: getCurrentSecrets,
|
|
166
|
+
validateSecrets,
|
|
167
|
+
}),
|
|
168
|
+
[getCurrentSecrets, validateSecrets]
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
// Toggle visibility for a specific secret
|
|
172
|
+
const toggleSecretVisibility = (index: number) => {
|
|
173
|
+
setVisibleSecrets((prev) => {
|
|
174
|
+
const newSet = new Set(prev);
|
|
175
|
+
if (newSet.has(index)) {
|
|
176
|
+
newSet.delete(index);
|
|
177
|
+
} else {
|
|
178
|
+
newSet.add(index);
|
|
179
|
+
}
|
|
180
|
+
return newSet;
|
|
181
|
+
});
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// Convert envs to raw editor format - include ALL variables, even empty required ones
|
|
185
|
+
const envsToRawText = useCallback(() => {
|
|
186
|
+
const lines: string[] = [];
|
|
187
|
+
|
|
188
|
+
// Add a header comment
|
|
189
|
+
lines.push('# Environment Variables');
|
|
190
|
+
lines.push('');
|
|
191
|
+
|
|
192
|
+
// Group by required vs optional
|
|
193
|
+
const requiredEnvs = envs.filter((env) => env.isRequired);
|
|
194
|
+
const optionalEnvs = envs.filter(
|
|
195
|
+
(env) => !env.isRequired && env.value && env.value.trim() !== ''
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
// Add required secrets section
|
|
199
|
+
if (requiredEnvs.length > 0) {
|
|
200
|
+
lines.push('# Required Secrets');
|
|
201
|
+
requiredEnvs.forEach((env) => {
|
|
202
|
+
if (env.plugin) {
|
|
203
|
+
lines.push(`# Required by ${env.plugin}`);
|
|
204
|
+
}
|
|
205
|
+
if (env.description) {
|
|
206
|
+
lines.push(`# ${env.description}`);
|
|
207
|
+
}
|
|
208
|
+
lines.push(`${env.name}=${env.value || ''}`);
|
|
209
|
+
lines.push('');
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Add optional secrets section
|
|
214
|
+
if (optionalEnvs.length > 0) {
|
|
215
|
+
lines.push('# Optional Variables');
|
|
216
|
+
optionalEnvs.forEach((env) => {
|
|
217
|
+
lines.push(`${env.name}=${env.value}`);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return lines.join('\n');
|
|
222
|
+
}, [envs]);
|
|
223
|
+
|
|
224
|
+
// Parse raw editor content back to envs
|
|
225
|
+
const parseRawText = useCallback((text: string) => {
|
|
226
|
+
const lines = text.split('\n');
|
|
227
|
+
const parsedEnvs: Record<string, string> = {};
|
|
228
|
+
|
|
229
|
+
for (const line of lines) {
|
|
230
|
+
const trimmedLine = line.trim();
|
|
231
|
+
if (!trimmedLine || trimmedLine.startsWith('#')) continue;
|
|
232
|
+
|
|
233
|
+
const [key, ...rest] = trimmedLine.split('=');
|
|
234
|
+
const val = rest
|
|
235
|
+
.join('=')
|
|
236
|
+
.trim()
|
|
237
|
+
.replace(/^['"]|['"]$/g, '');
|
|
238
|
+
if (key && key.trim()) {
|
|
239
|
+
parsedEnvs[key.trim()] = val;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return parsedEnvs;
|
|
244
|
+
}, []);
|
|
245
|
+
|
|
246
|
+
// Handle raw editor save - this should trigger the parent onChange
|
|
247
|
+
const handleRawEditorSave = () => {
|
|
248
|
+
const parsedEnvs = parseRawText(rawEditorContent);
|
|
249
|
+
|
|
250
|
+
// Create a new envs array with updates from raw editor
|
|
251
|
+
const newEnvs = envs
|
|
252
|
+
.map((env) => {
|
|
253
|
+
if (parsedEnvs.hasOwnProperty(env.name)) {
|
|
254
|
+
const cleanValue = parsedEnvs[env.name].startsWith('process.env.')
|
|
255
|
+
? ''
|
|
256
|
+
: parsedEnvs[env.name];
|
|
257
|
+
return { ...env, value: cleanValue, isModified: env.value !== cleanValue };
|
|
258
|
+
}
|
|
259
|
+
// Keep required secrets even if not in parsed content
|
|
260
|
+
if (env.isRequired) {
|
|
261
|
+
return { ...env, value: '', isModified: env.value !== '' };
|
|
262
|
+
}
|
|
263
|
+
// Remove non-required secrets that aren't in parsed content
|
|
264
|
+
return null;
|
|
265
|
+
})
|
|
266
|
+
.filter(Boolean) as EnvVariable[];
|
|
267
|
+
|
|
268
|
+
// Add new secrets from parsed content
|
|
269
|
+
Object.entries(parsedEnvs).forEach(([key, value]) => {
|
|
270
|
+
const exists = newEnvs.some((env) => env.name === key);
|
|
271
|
+
if (!exists) {
|
|
272
|
+
const cleanValue = value.startsWith('process.env.') ? '' : value;
|
|
273
|
+
const reqSecret = requiredSecrets.find((s) => s.name === key);
|
|
274
|
+
newEnvs.push({
|
|
275
|
+
name: key,
|
|
276
|
+
value: cleanValue,
|
|
277
|
+
isNew: true,
|
|
278
|
+
isModified: false,
|
|
279
|
+
isDeleted: false,
|
|
280
|
+
isRequired: reqSecret?.required || false,
|
|
281
|
+
description: reqSecret?.description,
|
|
282
|
+
example: reqSecret?.example,
|
|
283
|
+
plugin: reqSecret?.plugin,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// Sort: required secrets first, then alphabetically
|
|
289
|
+
newEnvs.sort((a, b) => {
|
|
290
|
+
if (a.isRequired && !b.isRequired) return -1;
|
|
291
|
+
if (!a.isRequired && b.isRequired) return 1;
|
|
292
|
+
return a.name.localeCompare(b.name);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
setEnvs(newEnvs);
|
|
296
|
+
setRawEditorOpen(false);
|
|
297
|
+
|
|
298
|
+
// The useEffect watching envs will trigger onChange to parent
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// Open raw editor modal
|
|
302
|
+
const openRawEditor = () => {
|
|
303
|
+
setRawEditorContent(envsToRawText());
|
|
304
|
+
setRawEditorOpen(true);
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// Copy env content to clipboard
|
|
308
|
+
const copyEnvContent = () => {
|
|
309
|
+
navigator.clipboard.writeText(rawEditorContent);
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// Load initial secrets from characterValue and merge with required secrets
|
|
313
|
+
useEffect(() => {
|
|
314
|
+
// Skip if still loading secrets or global envs
|
|
315
|
+
if (isLoadingSecrets || isLoadingGlobalEnvs) return;
|
|
316
|
+
|
|
317
|
+
// Only reset if we're switching to a different agent or this is the first load
|
|
318
|
+
// or if envs is empty (meaning we haven't initialized yet)
|
|
319
|
+
if (
|
|
320
|
+
characterValue.id !== lastAgentIdRef.current ||
|
|
321
|
+
!lastAgentIdRef.current ||
|
|
322
|
+
envs.length === 0
|
|
323
|
+
) {
|
|
324
|
+
// Decrypt secrets from the server using the core decryption function
|
|
325
|
+
const salt = getSalt();
|
|
326
|
+
const decryptedSecretsRaw = characterValue?.settings?.secrets || {};
|
|
327
|
+
// Ensure we're working with a plain object
|
|
328
|
+
const decryptedSecrets =
|
|
329
|
+
typeof decryptedSecretsRaw === 'object' &&
|
|
330
|
+
!Array.isArray(decryptedSecretsRaw) &&
|
|
331
|
+
decryptedSecretsRaw !== null
|
|
332
|
+
? decryptObjectValues(decryptedSecretsRaw, salt)
|
|
333
|
+
: {};
|
|
334
|
+
|
|
335
|
+
const existingSecrets = Object.entries(decryptedSecrets).map(([name, value]) => {
|
|
336
|
+
// Filter out process.env values - these should not be stored as actual values
|
|
337
|
+
const stringValue = String(value);
|
|
338
|
+
const cleanValue = stringValue.startsWith('process.env.') ? '' : stringValue;
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
name,
|
|
342
|
+
value: cleanValue,
|
|
343
|
+
isNew: false,
|
|
344
|
+
isModified: false,
|
|
345
|
+
isDeleted: false,
|
|
346
|
+
isRequired: false,
|
|
347
|
+
description: undefined,
|
|
348
|
+
example: undefined,
|
|
349
|
+
plugin: undefined,
|
|
350
|
+
};
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// Create a map for quick lookup
|
|
354
|
+
const existingSecretsMap = new Map(existingSecrets.map((s) => [s.name, s]));
|
|
355
|
+
|
|
356
|
+
// Add required secrets that don't exist yet
|
|
357
|
+
const allSecrets: EnvVariable[] = [...existingSecrets];
|
|
358
|
+
|
|
359
|
+
requiredSecrets.forEach((reqSecret) => {
|
|
360
|
+
if (!existingSecretsMap.has(reqSecret.name)) {
|
|
361
|
+
// Check if this secret exists in global environment
|
|
362
|
+
// Don't populate the value - let the UI show it's using global
|
|
363
|
+
allSecrets.push({
|
|
364
|
+
name: reqSecret.name,
|
|
365
|
+
value: '', // Keep empty to show it's using global
|
|
366
|
+
isNew: true,
|
|
367
|
+
isModified: false,
|
|
368
|
+
isDeleted: false,
|
|
369
|
+
isRequired: true,
|
|
370
|
+
description: reqSecret.description,
|
|
371
|
+
example: reqSecret.example,
|
|
372
|
+
plugin: reqSecret.plugin,
|
|
373
|
+
});
|
|
374
|
+
} else {
|
|
375
|
+
// Update existing secret with required metadata
|
|
376
|
+
const existingIndex = allSecrets.findIndex((s) => s.name === reqSecret.name);
|
|
377
|
+
if (existingIndex !== -1) {
|
|
378
|
+
// Keep existing value as is - don't auto-populate from global
|
|
379
|
+
allSecrets[existingIndex] = {
|
|
380
|
+
...allSecrets[existingIndex],
|
|
381
|
+
isRequired: true,
|
|
382
|
+
description: reqSecret.description,
|
|
383
|
+
example: reqSecret.example,
|
|
384
|
+
plugin: reqSecret.plugin,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// Sort: required secrets first, then alphabetically
|
|
391
|
+
allSecrets.sort((a, b) => {
|
|
392
|
+
if (a.isRequired && !b.isRequired) return -1;
|
|
393
|
+
if (!a.isRequired && b.isRequired) return 1;
|
|
394
|
+
return a.name.localeCompare(b.name);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
setEnvs(allSecrets);
|
|
398
|
+
setDeletedKeys([]); // Reset deleted keys when loading new data
|
|
399
|
+
lastAgentIdRef.current = characterValue.id || 'new-agent';
|
|
400
|
+
|
|
401
|
+
// Also reset the required secrets tracking
|
|
402
|
+
const requiredSecretsKey = requiredSecrets
|
|
403
|
+
.map((s) => s.name)
|
|
404
|
+
.sort()
|
|
405
|
+
.join(',');
|
|
406
|
+
lastRequiredSecretsKeyRef.current = requiredSecretsKey;
|
|
407
|
+
}
|
|
408
|
+
}, [
|
|
409
|
+
characterValue.id,
|
|
410
|
+
characterValue.settings?.secrets,
|
|
411
|
+
requiredSecrets,
|
|
412
|
+
isLoadingSecrets,
|
|
413
|
+
globalEnvs,
|
|
414
|
+
isLoadingGlobalEnvs,
|
|
415
|
+
]);
|
|
416
|
+
|
|
417
|
+
// Sync secrets when plugins change (not just when agent changes)
|
|
418
|
+
useEffect(() => {
|
|
419
|
+
// Skip only if still loading secrets
|
|
420
|
+
if (isLoadingSecrets) return;
|
|
421
|
+
|
|
422
|
+
// Create a stable key for comparison
|
|
423
|
+
const requiredSecretsKey = requiredSecrets
|
|
424
|
+
.map((s) => s.name)
|
|
425
|
+
.sort()
|
|
426
|
+
.join(',');
|
|
427
|
+
|
|
428
|
+
// Only update if the required secrets actually changed
|
|
429
|
+
if (requiredSecretsKey === lastRequiredSecretsKeyRef.current) return;
|
|
430
|
+
lastRequiredSecretsKeyRef.current = requiredSecretsKey;
|
|
431
|
+
|
|
432
|
+
// Get current required secret names
|
|
433
|
+
const currentRequiredNames = new Set(requiredSecrets.map((s) => s.name));
|
|
434
|
+
|
|
435
|
+
// Update existing envs
|
|
436
|
+
setEnvs((prevEnvs) => {
|
|
437
|
+
const updatedEnvs = prevEnvs
|
|
438
|
+
.map((env) => {
|
|
439
|
+
// Check if this secret is still required
|
|
440
|
+
const isStillRequired = currentRequiredNames.has(env.name);
|
|
441
|
+
const reqSecret = requiredSecrets.find((s) => s.name === env.name);
|
|
442
|
+
|
|
443
|
+
// If it was required but no longer is, remove it entirely (regardless of value)
|
|
444
|
+
if (env.isRequired && !isStillRequired) {
|
|
445
|
+
// Mark this secret for deletion in parent component immediately
|
|
446
|
+
setTimeout(() => {
|
|
447
|
+
setDeletedKeys((prev) => (prev.includes(env.name) ? prev : [...prev, env.name]));
|
|
448
|
+
}, 0);
|
|
449
|
+
return null; // Mark for removal from envs array
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Update metadata if still required
|
|
453
|
+
if (isStillRequired && reqSecret) {
|
|
454
|
+
return {
|
|
455
|
+
...env,
|
|
456
|
+
isRequired: true,
|
|
457
|
+
description: reqSecret.description,
|
|
458
|
+
example: reqSecret.example,
|
|
459
|
+
plugin: reqSecret.plugin,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Keep non-required envs that have values
|
|
464
|
+
return {
|
|
465
|
+
...env,
|
|
466
|
+
isRequired: isStillRequired,
|
|
467
|
+
description: isStillRequired ? env.description : undefined,
|
|
468
|
+
example: isStillRequired ? env.example : undefined,
|
|
469
|
+
plugin: isStillRequired ? env.plugin : undefined,
|
|
470
|
+
};
|
|
471
|
+
})
|
|
472
|
+
.filter(Boolean) as EnvVariable[]; // Remove nulls
|
|
473
|
+
|
|
474
|
+
// Add new required secrets that don't exist
|
|
475
|
+
const existingNames = new Set(updatedEnvs.map((e) => e.name));
|
|
476
|
+
requiredSecrets.forEach((reqSecret) => {
|
|
477
|
+
if (!existingNames.has(reqSecret.name)) {
|
|
478
|
+
// Don't auto-populate from global - let UI show it's using global
|
|
479
|
+
updatedEnvs.push({
|
|
480
|
+
name: reqSecret.name,
|
|
481
|
+
value: '', // Keep empty to show it's using global
|
|
482
|
+
isNew: true,
|
|
483
|
+
isModified: false,
|
|
484
|
+
isDeleted: false,
|
|
485
|
+
isRequired: true,
|
|
486
|
+
description: reqSecret.description,
|
|
487
|
+
example: reqSecret.example,
|
|
488
|
+
plugin: reqSecret.plugin,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
// Sort: required secrets first, then alphabetically
|
|
494
|
+
updatedEnvs.sort((a, b) => {
|
|
495
|
+
if (a.isRequired && !b.isRequired) return -1;
|
|
496
|
+
if (!a.isRequired && b.isRequired) return 1;
|
|
497
|
+
return a.name.localeCompare(b.name);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
return updatedEnvs;
|
|
501
|
+
});
|
|
502
|
+
}, [requiredSecrets, isLoadingSecrets, globalEnvs]);
|
|
503
|
+
|
|
504
|
+
// Force cleanup of non-required secrets (alternative approach)
|
|
505
|
+
useEffect(() => {
|
|
506
|
+
const currentRequiredNames = new Set(requiredSecrets.map((s) => s.name));
|
|
507
|
+
|
|
508
|
+
setEnvs((prevEnvs) => {
|
|
509
|
+
const cleanedEnvs = prevEnvs.filter((env) => {
|
|
510
|
+
const shouldKeep =
|
|
511
|
+
currentRequiredNames.has(env.name) ||
|
|
512
|
+
(!env.isRequired && env.value && env.value.trim() !== '');
|
|
513
|
+
|
|
514
|
+
if (!shouldKeep && env.isRequired) {
|
|
515
|
+
// Mark for deletion
|
|
516
|
+
setDeletedKeys((prev) => (prev.includes(env.name) ? prev : [...prev, env.name]));
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return shouldKeep;
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
return cleanedEnvs;
|
|
523
|
+
});
|
|
524
|
+
}, [requiredSecrets]);
|
|
525
|
+
|
|
526
|
+
// Notify parent of changes
|
|
527
|
+
useEffect(() => {
|
|
528
|
+
if (!onChange) return;
|
|
529
|
+
|
|
530
|
+
// Create a debounced version to avoid rapid fire updates
|
|
531
|
+
const timeoutId = setTimeout(() => {
|
|
532
|
+
const secrets = getCurrentSecrets();
|
|
533
|
+
|
|
534
|
+
// Also check for secrets that should be removed
|
|
535
|
+
// Get all env names that are currently in the list
|
|
536
|
+
const currentEnvNames = new Set(envs.map((e) => e.name));
|
|
537
|
+
|
|
538
|
+
// Check if there are any secrets in the character settings that are no longer in our envs
|
|
539
|
+
// This happens when a plugin is removed and its required secrets are cleaned up
|
|
540
|
+
const characterSecrets = characterValue?.settings?.secrets || {};
|
|
541
|
+
Object.keys(characterSecrets).forEach((secretName) => {
|
|
542
|
+
if (!currentEnvNames.has(secretName) && !deletedKeys.includes(secretName)) {
|
|
543
|
+
// Mark this secret for deletion
|
|
544
|
+
secrets[secretName] = null;
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
// Only call onChange if secrets actually changed
|
|
549
|
+
// Sort keys to ensure consistent comparison
|
|
550
|
+
const sortedSecrets = Object.keys(secrets)
|
|
551
|
+
.sort()
|
|
552
|
+
.reduce(
|
|
553
|
+
(acc, key) => {
|
|
554
|
+
acc[key] = secrets[key];
|
|
555
|
+
return acc;
|
|
556
|
+
},
|
|
557
|
+
{} as Record<string, string | null>
|
|
558
|
+
);
|
|
559
|
+
const secretsString = JSON.stringify(sortedSecrets);
|
|
560
|
+
|
|
561
|
+
if (secretsString !== lastSecretsRef.current) {
|
|
562
|
+
lastSecretsRef.current = secretsString;
|
|
563
|
+
onChange(secrets);
|
|
564
|
+
}
|
|
565
|
+
}, 100); // 100ms debounce
|
|
566
|
+
|
|
567
|
+
return () => clearTimeout(timeoutId);
|
|
568
|
+
}, [envs, deletedKeys, onChange, characterValue?.settings?.secrets, getCurrentSecrets]);
|
|
569
|
+
|
|
570
|
+
const handleFileUpload = useCallback(
|
|
571
|
+
(file: File) => {
|
|
572
|
+
const reader = new FileReader();
|
|
573
|
+
reader.onload = (event) => {
|
|
574
|
+
const text = event.target?.result as string;
|
|
575
|
+
const lines = text.split('\n');
|
|
576
|
+
|
|
577
|
+
const newEnvs: Record<string, string> = {};
|
|
578
|
+
for (const line of lines) {
|
|
579
|
+
const trimmedLine = line.trim();
|
|
580
|
+
if (!trimmedLine || trimmedLine.startsWith('#')) continue;
|
|
581
|
+
|
|
582
|
+
const [key, ...rest] = trimmedLine.split('=');
|
|
583
|
+
const val = rest
|
|
584
|
+
.join('=')
|
|
585
|
+
.trim()
|
|
586
|
+
.replace(/^['"]|['"]$/g, '');
|
|
587
|
+
if (key) newEnvs[key.trim()] = val;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
setEnvs((prev) => {
|
|
591
|
+
const merged = new Map(
|
|
592
|
+
prev.map(({ name, value, ...rest }) => [name, { value, ...rest }])
|
|
593
|
+
);
|
|
594
|
+
for (const [key, val] of Object.entries(newEnvs)) {
|
|
595
|
+
// Filter out process.env values
|
|
596
|
+
const cleanValue = val.startsWith('process.env.') ? '' : val;
|
|
597
|
+
const existing = merged.get(key);
|
|
598
|
+
if (existing) {
|
|
599
|
+
merged.set(key, { ...existing, value: cleanValue, isModified: true });
|
|
600
|
+
} else {
|
|
601
|
+
const reqSecret = requiredSecrets.find((s) => s.name === key);
|
|
602
|
+
merged.set(key, {
|
|
603
|
+
value: cleanValue,
|
|
604
|
+
isNew: true,
|
|
605
|
+
isModified: false,
|
|
606
|
+
isRequired: reqSecret?.required || false,
|
|
607
|
+
description: reqSecret?.description,
|
|
608
|
+
example: reqSecret?.example,
|
|
609
|
+
plugin: reqSecret?.plugin,
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
const result = Array.from(merged.entries()).map(([name, data]) => ({
|
|
614
|
+
name,
|
|
615
|
+
...data,
|
|
616
|
+
}));
|
|
617
|
+
|
|
618
|
+
// Sort: required secrets first, then alphabetically
|
|
619
|
+
result.sort((a, b) => {
|
|
620
|
+
if (a.isRequired && !b.isRequired) return -1;
|
|
621
|
+
if (!a.isRequired && b.isRequired) return 1;
|
|
622
|
+
return a.name.localeCompare(b.name);
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
return result;
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
if (deletedKeys.length > 0) {
|
|
629
|
+
setDeletedKeys((prev) => prev.filter((key) => !Object.keys(newEnvs).includes(key)));
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
reader.readAsText(file);
|
|
633
|
+
},
|
|
634
|
+
[requiredSecrets, deletedKeys]
|
|
635
|
+
);
|
|
636
|
+
|
|
637
|
+
useEffect(() => {
|
|
638
|
+
const drop = dropRef.current;
|
|
639
|
+
if (!drop) {
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const preventDefaults = (e: Event) => {
|
|
644
|
+
e.preventDefault();
|
|
645
|
+
e.stopPropagation();
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
const highlight = () => {
|
|
649
|
+
setIsDragging(true);
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
const unhighlight = () => {
|
|
653
|
+
setIsDragging(false);
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
const handleDrop = (e: DragEvent) => {
|
|
657
|
+
preventDefaults(e);
|
|
658
|
+
unhighlight();
|
|
659
|
+
|
|
660
|
+
const file = e.dataTransfer?.files?.[0];
|
|
661
|
+
if (file && file.name.endsWith('.env')) {
|
|
662
|
+
handleFileUpload(file);
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
['dragenter', 'dragover'].forEach((event) => drop.addEventListener(event, highlight));
|
|
667
|
+
['dragleave', 'drop'].forEach((event) => drop.addEventListener(event, unhighlight));
|
|
668
|
+
['dragenter', 'dragover', 'dragleave', 'drop'].forEach((event) =>
|
|
669
|
+
drop.addEventListener(event, preventDefaults)
|
|
670
|
+
);
|
|
671
|
+
drop.addEventListener('drop', handleDrop);
|
|
672
|
+
|
|
673
|
+
return () => {
|
|
674
|
+
['dragenter', 'dragover', 'dragleave', 'drop'].forEach((event) =>
|
|
675
|
+
drop.removeEventListener(event, preventDefaults)
|
|
676
|
+
);
|
|
677
|
+
['dragenter', 'dragover'].forEach((event) => drop.removeEventListener(event, highlight));
|
|
678
|
+
['dragleave', 'drop'].forEach((event) => drop.removeEventListener(event, unhighlight));
|
|
679
|
+
drop.removeEventListener('drop', handleDrop);
|
|
680
|
+
};
|
|
681
|
+
}, [handleFileUpload]);
|
|
682
|
+
|
|
683
|
+
const addEnv = () => {
|
|
684
|
+
if (name && value) {
|
|
685
|
+
const exists = envs.some((env) => env.name === name);
|
|
686
|
+
|
|
687
|
+
// Filter out process.env values
|
|
688
|
+
const cleanValue = value.startsWith('process.env.') ? '' : value;
|
|
689
|
+
|
|
690
|
+
if (!exists) {
|
|
691
|
+
if (deletedKeys.includes(name)) {
|
|
692
|
+
setDeletedKeys(deletedKeys.filter((key) => key !== name));
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const reqSecret = requiredSecrets.find((s) => s.name === name);
|
|
696
|
+
const newEnv: EnvVariable = {
|
|
697
|
+
name,
|
|
698
|
+
value: cleanValue,
|
|
699
|
+
isNew: true,
|
|
700
|
+
isRequired: reqSecret?.required || false,
|
|
701
|
+
description: reqSecret?.description,
|
|
702
|
+
example: reqSecret?.example,
|
|
703
|
+
plugin: reqSecret?.plugin,
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
const updatedEnvs = [...envs, newEnv];
|
|
707
|
+
// Sort: required secrets first, then alphabetically
|
|
708
|
+
updatedEnvs.sort((a, b) => {
|
|
709
|
+
if (a.isRequired && !b.isRequired) return -1;
|
|
710
|
+
if (!a.isRequired && b.isRequired) return 1;
|
|
711
|
+
return a.name.localeCompare(b.name);
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
setEnvs(updatedEnvs);
|
|
715
|
+
setName('');
|
|
716
|
+
setValue('');
|
|
717
|
+
} else {
|
|
718
|
+
setEnvs(
|
|
719
|
+
envs.map((env) =>
|
|
720
|
+
env.name === name ? { ...env, value: cleanValue, isModified: true } : env
|
|
721
|
+
)
|
|
722
|
+
);
|
|
723
|
+
setName('');
|
|
724
|
+
setValue('');
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
const startEditing = (index: number) => {
|
|
730
|
+
// Close any other editing
|
|
731
|
+
if (editingIndex !== null && editingIndex !== index) {
|
|
732
|
+
setEditingIndex(null);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
setEditingIndex(index);
|
|
736
|
+
setEditedValue(envs[index].value);
|
|
737
|
+
// Show the secret when editing
|
|
738
|
+
setVisibleSecrets((prev) => new Set(prev).add(index));
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
const saveEdit = (index: number) => {
|
|
742
|
+
const updatedEnvs = [...envs];
|
|
743
|
+
// Filter out process.env values
|
|
744
|
+
const cleanValue = editedValue.startsWith('process.env.') ? '' : editedValue;
|
|
745
|
+
|
|
746
|
+
if (updatedEnvs[index].value !== cleanValue) {
|
|
747
|
+
updatedEnvs[index].value = cleanValue;
|
|
748
|
+
updatedEnvs[index].isModified = true;
|
|
749
|
+
}
|
|
750
|
+
setEnvs(updatedEnvs);
|
|
751
|
+
setEditingIndex(null);
|
|
752
|
+
setEditedValue('');
|
|
753
|
+
// Hide the secret after saving
|
|
754
|
+
setVisibleSecrets((prev) => {
|
|
755
|
+
const newSet = new Set(prev);
|
|
756
|
+
newSet.delete(index);
|
|
757
|
+
return newSet;
|
|
758
|
+
});
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
const cancelEdit = () => {
|
|
762
|
+
const currentIndex = editingIndex;
|
|
763
|
+
setEditingIndex(null);
|
|
764
|
+
setEditedValue('');
|
|
765
|
+
// Hide the secret after canceling
|
|
766
|
+
if (currentIndex !== null) {
|
|
767
|
+
setVisibleSecrets((prev) => {
|
|
768
|
+
const newSet = new Set(prev);
|
|
769
|
+
newSet.delete(currentIndex);
|
|
770
|
+
return newSet;
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
|
|
775
|
+
const removeEnv = (index: number) => {
|
|
776
|
+
const keyToRemove = envs[index].name;
|
|
777
|
+
|
|
778
|
+
setDeletedKeys([...deletedKeys, keyToRemove]);
|
|
779
|
+
|
|
780
|
+
setEnvs(envs.filter((_, i) => i !== index));
|
|
781
|
+
setEditingIndex(null);
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
// Get missing required secrets (considering global envs)
|
|
785
|
+
const missingRequiredSecrets = requiredSecrets.filter((reqSecret) => {
|
|
786
|
+
const env = envs.find((e) => e.name === reqSecret.name);
|
|
787
|
+
const hasLocalValue = env && env.value && env.value.trim() !== '';
|
|
788
|
+
const hasGlobalValue = globalEnvs[reqSecret.name] && globalEnvs[reqSecret.name].trim() !== '';
|
|
789
|
+
return !hasLocalValue && !hasGlobalValue;
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
// Check if a secret exists in global environment
|
|
793
|
+
const isInGlobalEnv = useCallback(
|
|
794
|
+
(secretName: string) => {
|
|
795
|
+
return globalEnvs[secretName] && globalEnvs[secretName].trim() !== '';
|
|
796
|
+
},
|
|
797
|
+
[globalEnvs]
|
|
798
|
+
);
|
|
799
|
+
|
|
800
|
+
// Custom scrollbar styles for input containers - visible scrollbar like Railway
|
|
801
|
+
const scrollbarContainerClass = 'scrollbar-visible';
|
|
802
|
+
|
|
803
|
+
return (
|
|
804
|
+
<div className="w-full h-full flex flex-col">
|
|
805
|
+
{/* Header */}
|
|
806
|
+
<div className="flex items-center justify-between p-4 border-b">
|
|
807
|
+
<div className="flex items-center gap-2">
|
|
808
|
+
<h2 className="text-lg font-semibold">Variables</h2>
|
|
809
|
+
<Badge variant="secondary" className="text-xs">
|
|
810
|
+
{envs.filter((env) => env.value && env.value.trim() !== '').length} Service Variables
|
|
811
|
+
</Badge>
|
|
812
|
+
</div>
|
|
813
|
+
<Button
|
|
814
|
+
type="button"
|
|
815
|
+
variant="outline"
|
|
816
|
+
size="sm"
|
|
817
|
+
onClick={openRawEditor}
|
|
818
|
+
className="flex items-center gap-2"
|
|
819
|
+
>
|
|
820
|
+
<FileText className="w-4 h-4" />
|
|
821
|
+
Raw Editor
|
|
822
|
+
</Button>
|
|
823
|
+
</div>
|
|
824
|
+
|
|
825
|
+
{/* Content */}
|
|
826
|
+
<ScrollArea className="flex-1">
|
|
827
|
+
<div className="p-4 space-y-4">
|
|
828
|
+
{/* Show loading state for required secrets */}
|
|
829
|
+
{(isLoadingSecrets || isLoadingGlobalEnvs) && (
|
|
830
|
+
<div className="space-y-2">
|
|
831
|
+
<Skeleton className="h-4 w-48" />
|
|
832
|
+
<Skeleton className="h-4 w-64" />
|
|
833
|
+
</div>
|
|
834
|
+
)}
|
|
835
|
+
|
|
836
|
+
{/* Show alert if there are missing required secrets */}
|
|
837
|
+
{!isLoadingSecrets && !isLoadingGlobalEnvs && missingRequiredSecrets.length > 0 && (
|
|
838
|
+
<Alert>
|
|
839
|
+
<AlertCircle className="h-4 w-4" />
|
|
840
|
+
<AlertDescription>
|
|
841
|
+
<strong>Missing required secrets:</strong>
|
|
842
|
+
<ul className="mt-2 space-y-1">
|
|
843
|
+
{missingRequiredSecrets.map((secret) => (
|
|
844
|
+
<li key={secret.name} className="text-sm">
|
|
845
|
+
• <code className="font-mono">{secret.name}</code>
|
|
846
|
+
{secret.plugin && (
|
|
847
|
+
<span className="text-muted-foreground ml-1">
|
|
848
|
+
(required by {secret.plugin})
|
|
849
|
+
</span>
|
|
850
|
+
)}
|
|
851
|
+
</li>
|
|
852
|
+
))}
|
|
853
|
+
</ul>
|
|
854
|
+
</AlertDescription>
|
|
855
|
+
</Alert>
|
|
856
|
+
)}
|
|
857
|
+
|
|
858
|
+
{/* Show info about global environment variables */}
|
|
859
|
+
{!isLoadingSecrets &&
|
|
860
|
+
!isLoadingGlobalEnvs &&
|
|
861
|
+
requiredSecrets.some(
|
|
862
|
+
(secret) =>
|
|
863
|
+
isInGlobalEnv(secret.name) && !envs.find((e) => e.name === secret.name)?.value
|
|
864
|
+
) && (
|
|
865
|
+
<Alert className="border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-900/20">
|
|
866
|
+
<Globe className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
|
867
|
+
<AlertDescription className="text-blue-800 dark:text-blue-200">
|
|
868
|
+
<strong>Global environment variables detected:</strong>
|
|
869
|
+
<p className="text-sm mt-1">
|
|
870
|
+
Some required secrets are automatically using values from your global
|
|
871
|
+
environment settings. To override any of these for this specific agent, simply
|
|
872
|
+
enter a new value in the field.
|
|
873
|
+
</p>
|
|
874
|
+
</AlertDescription>
|
|
875
|
+
</Alert>
|
|
876
|
+
)}
|
|
877
|
+
|
|
878
|
+
{/* Add new variable form */}
|
|
879
|
+
<div className="space-y-2">
|
|
880
|
+
{/* Desktop layout */}
|
|
881
|
+
<div className="hidden md:grid md:grid-cols-[minmax(200px,1fr)_2fr_auto] gap-2 items-end">
|
|
882
|
+
<div className="space-y-1">
|
|
883
|
+
<label
|
|
884
|
+
htmlFor="new-secret-name"
|
|
885
|
+
className="text-xs font-medium text-muted-foreground uppercase"
|
|
886
|
+
>
|
|
887
|
+
Name
|
|
888
|
+
</label>
|
|
889
|
+
<Input
|
|
890
|
+
id="new-secret-name"
|
|
891
|
+
placeholder="VARIABLE_NAME"
|
|
892
|
+
value={name}
|
|
893
|
+
onChange={(e) => setName(e.target.value)}
|
|
894
|
+
className="font-mono text-sm"
|
|
895
|
+
onKeyDown={(e) => {
|
|
896
|
+
if (e.key === 'Enter') {
|
|
897
|
+
e.preventDefault();
|
|
898
|
+
addEnv();
|
|
899
|
+
}
|
|
900
|
+
}}
|
|
901
|
+
/>
|
|
902
|
+
</div>
|
|
903
|
+
<div className="space-y-1">
|
|
904
|
+
<label
|
|
905
|
+
htmlFor="new-secret-value"
|
|
906
|
+
className="text-xs font-medium text-muted-foreground uppercase"
|
|
907
|
+
>
|
|
908
|
+
Value
|
|
909
|
+
</label>
|
|
910
|
+
<div className="relative">
|
|
911
|
+
<div className={`rounded border ${scrollbarContainerClass}`}>
|
|
912
|
+
<Input
|
|
913
|
+
id="new-secret-value"
|
|
914
|
+
type={showPassword ? 'text' : 'password'}
|
|
915
|
+
placeholder="Enter value..."
|
|
916
|
+
value={value}
|
|
917
|
+
onChange={(e) => setValue(e.target.value)}
|
|
918
|
+
className="font-mono text-sm pr-10 border-0 focus-visible:ring-0 w-auto min-w-full rounded"
|
|
919
|
+
style={{ minWidth: '100%' }}
|
|
920
|
+
onKeyDown={(e) => {
|
|
921
|
+
if (e.key === 'Enter') {
|
|
922
|
+
e.preventDefault();
|
|
923
|
+
addEnv();
|
|
924
|
+
}
|
|
925
|
+
}}
|
|
926
|
+
/>
|
|
927
|
+
</div>
|
|
928
|
+
<Button
|
|
929
|
+
type="button"
|
|
930
|
+
variant="ghost"
|
|
931
|
+
size="sm"
|
|
932
|
+
className="absolute right-1 top-1/2 -translate-y-1/2 h-7 w-7 p-0"
|
|
933
|
+
onClick={() => setShowPassword(!showPassword)}
|
|
934
|
+
>
|
|
935
|
+
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
|
936
|
+
</Button>
|
|
937
|
+
</div>
|
|
938
|
+
</div>
|
|
939
|
+
<Button type="button" onClick={addEnv} size="sm">
|
|
940
|
+
Add
|
|
941
|
+
</Button>
|
|
942
|
+
</div>
|
|
943
|
+
|
|
944
|
+
{/* Mobile layout */}
|
|
945
|
+
<div className="md:hidden space-y-3">
|
|
946
|
+
<div className="space-y-1">
|
|
947
|
+
<label
|
|
948
|
+
htmlFor="new-secret-name-mobile"
|
|
949
|
+
className="text-xs font-medium text-muted-foreground uppercase"
|
|
950
|
+
>
|
|
951
|
+
Name
|
|
952
|
+
</label>
|
|
953
|
+
<Input
|
|
954
|
+
id="new-secret-name-mobile"
|
|
955
|
+
placeholder="VARIABLE_NAME"
|
|
956
|
+
value={name}
|
|
957
|
+
onChange={(e) => setName(e.target.value)}
|
|
958
|
+
className="font-mono text-sm w-full"
|
|
959
|
+
onKeyDown={(e) => {
|
|
960
|
+
if (e.key === 'Enter') {
|
|
961
|
+
e.preventDefault();
|
|
962
|
+
// Focus on value input on mobile
|
|
963
|
+
document.getElementById('new-secret-value-mobile')?.focus();
|
|
964
|
+
}
|
|
965
|
+
}}
|
|
966
|
+
/>
|
|
967
|
+
</div>
|
|
968
|
+
<div className="space-y-1">
|
|
969
|
+
<label
|
|
970
|
+
htmlFor="new-secret-value-mobile"
|
|
971
|
+
className="text-xs font-medium text-muted-foreground uppercase"
|
|
972
|
+
>
|
|
973
|
+
Value
|
|
974
|
+
</label>
|
|
975
|
+
<div className="relative">
|
|
976
|
+
<div className={`rounded border ${scrollbarContainerClass}`}>
|
|
977
|
+
<Input
|
|
978
|
+
id="new-secret-value-mobile"
|
|
979
|
+
type={showPassword ? 'text' : 'password'}
|
|
980
|
+
placeholder="Enter value..."
|
|
981
|
+
value={value}
|
|
982
|
+
onChange={(e) => setValue(e.target.value)}
|
|
983
|
+
className="font-mono text-sm pr-10 border-0 focus-visible:ring-0 w-auto min-w-full rounded"
|
|
984
|
+
style={{ minWidth: '100%' }}
|
|
985
|
+
onKeyDown={(e) => {
|
|
986
|
+
if (e.key === 'Enter') {
|
|
987
|
+
e.preventDefault();
|
|
988
|
+
addEnv();
|
|
989
|
+
}
|
|
990
|
+
}}
|
|
991
|
+
/>
|
|
992
|
+
</div>
|
|
993
|
+
<Button
|
|
994
|
+
type="button"
|
|
995
|
+
variant="ghost"
|
|
996
|
+
size="sm"
|
|
997
|
+
className="absolute right-1 top-1/2 -translate-y-1/2 h-7 w-7 p-0"
|
|
998
|
+
onClick={() => setShowPassword(!showPassword)}
|
|
999
|
+
>
|
|
1000
|
+
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
|
1001
|
+
</Button>
|
|
1002
|
+
</div>
|
|
1003
|
+
</div>
|
|
1004
|
+
<Button type="button" onClick={addEnv} size="sm" className="w-full">
|
|
1005
|
+
Add Variable
|
|
1006
|
+
</Button>
|
|
1007
|
+
</div>
|
|
1008
|
+
</div>
|
|
1009
|
+
|
|
1010
|
+
{/* Variables Table */}
|
|
1011
|
+
{envs.length > 0 && (
|
|
1012
|
+
<TooltipProvider>
|
|
1013
|
+
{/* Desktop Table Layout */}
|
|
1014
|
+
<div className="hidden md:block border rounded-lg overflow-hidden relative">
|
|
1015
|
+
<div className="bg-muted/50 px-4 py-2 border-b">
|
|
1016
|
+
<div className="grid grid-cols-[minmax(200px,1fr)_2fr_auto] gap-4 text-xs font-medium text-muted-foreground uppercase">
|
|
1017
|
+
<div>Name</div>
|
|
1018
|
+
<div>Value</div>
|
|
1019
|
+
<div className="text-center">Actions</div>
|
|
1020
|
+
</div>
|
|
1021
|
+
</div>
|
|
1022
|
+
|
|
1023
|
+
{/* Scrollable content area for variables */}
|
|
1024
|
+
<div className="max-h-[400px] overflow-y-auto">
|
|
1025
|
+
{envs.map((env, index) => (
|
|
1026
|
+
<div
|
|
1027
|
+
key={`${env.name}-${index}`}
|
|
1028
|
+
className={`grid grid-cols-[minmax(200px,1fr)_2fr_auto] gap-4 items-center px-4 py-3 border-b last:border-b-0 hover:bg-muted/10 transition-colors ${
|
|
1029
|
+
env.isRequired &&
|
|
1030
|
+
(!env.value || env.value.trim() === '') &&
|
|
1031
|
+
!isInGlobalEnv(env.name)
|
|
1032
|
+
? 'bg-red-500/5'
|
|
1033
|
+
: ''
|
|
1034
|
+
}`}
|
|
1035
|
+
>
|
|
1036
|
+
{/* Name Column */}
|
|
1037
|
+
<div className="pr-2">
|
|
1038
|
+
<div className="flex items-start gap-2 flex-wrap">
|
|
1039
|
+
<code className="font-mono text-sm font-medium break-words cursor-default">
|
|
1040
|
+
{env.name}
|
|
1041
|
+
</code>
|
|
1042
|
+
{env.isRequired && (
|
|
1043
|
+
<Badge variant="outline" className="text-xs shrink-0">
|
|
1044
|
+
Required
|
|
1045
|
+
</Badge>
|
|
1046
|
+
)}
|
|
1047
|
+
{isInGlobalEnv(env.name) && (
|
|
1048
|
+
<Tooltip>
|
|
1049
|
+
<TooltipTrigger asChild>
|
|
1050
|
+
<Badge
|
|
1051
|
+
variant="secondary"
|
|
1052
|
+
className="text-xs shrink-0 flex items-center gap-1"
|
|
1053
|
+
>
|
|
1054
|
+
<Globe className="w-3 h-3" />
|
|
1055
|
+
Global
|
|
1056
|
+
</Badge>
|
|
1057
|
+
</TooltipTrigger>
|
|
1058
|
+
<TooltipContent>
|
|
1059
|
+
<p>
|
|
1060
|
+
{env.value
|
|
1061
|
+
? 'Overriding global value'
|
|
1062
|
+
: 'Value pulled from global environment. Add new value to override.'}
|
|
1063
|
+
</p>
|
|
1064
|
+
</TooltipContent>
|
|
1065
|
+
</Tooltip>
|
|
1066
|
+
)}
|
|
1067
|
+
</div>
|
|
1068
|
+
{env.description && (
|
|
1069
|
+
<div className="text-xs text-muted-foreground mt-1 line-clamp-2">
|
|
1070
|
+
{env.description}
|
|
1071
|
+
</div>
|
|
1072
|
+
)}
|
|
1073
|
+
{env.plugin && (
|
|
1074
|
+
<div className="text-xs text-muted-foreground truncate">
|
|
1075
|
+
Required by: {env.plugin}
|
|
1076
|
+
</div>
|
|
1077
|
+
)}
|
|
1078
|
+
</div>
|
|
1079
|
+
|
|
1080
|
+
{/* Value Column */}
|
|
1081
|
+
<div className="min-w-0">
|
|
1082
|
+
{editingIndex === index ? (
|
|
1083
|
+
<div className="flex items-center gap-2">
|
|
1084
|
+
<div className="relative flex-1">
|
|
1085
|
+
<div className={`rounded border ${scrollbarContainerClass}`}>
|
|
1086
|
+
<Input
|
|
1087
|
+
value={editedValue}
|
|
1088
|
+
onChange={(e) => setEditedValue(e.target.value)}
|
|
1089
|
+
className="font-mono text-sm border-0 focus-visible:ring-0 w-auto min-w-full rounded"
|
|
1090
|
+
style={{ minWidth: '100%' }}
|
|
1091
|
+
type={visibleSecrets.has(index) ? 'text' : 'password'}
|
|
1092
|
+
placeholder={
|
|
1093
|
+
isInGlobalEnv(env.name) && !env.value
|
|
1094
|
+
? `Using global (enter to override)`
|
|
1095
|
+
: env.example || 'Enter value...'
|
|
1096
|
+
}
|
|
1097
|
+
autoFocus
|
|
1098
|
+
onKeyDown={(e) => {
|
|
1099
|
+
if (e.key === 'Enter') {
|
|
1100
|
+
e.preventDefault();
|
|
1101
|
+
saveEdit(index);
|
|
1102
|
+
} else if (e.key === 'Escape') {
|
|
1103
|
+
cancelEdit();
|
|
1104
|
+
}
|
|
1105
|
+
}}
|
|
1106
|
+
/>
|
|
1107
|
+
</div>
|
|
1108
|
+
</div>
|
|
1109
|
+
<div className="flex items-center gap-1">
|
|
1110
|
+
<Button
|
|
1111
|
+
type="button"
|
|
1112
|
+
variant="ghost"
|
|
1113
|
+
size="sm"
|
|
1114
|
+
onClick={() => toggleSecretVisibility(index)}
|
|
1115
|
+
className="h-8 w-8 p-0"
|
|
1116
|
+
>
|
|
1117
|
+
{visibleSecrets.has(index) ? (
|
|
1118
|
+
<EyeOff className="w-4 h-4" />
|
|
1119
|
+
) : (
|
|
1120
|
+
<Eye className="w-4 h-4" />
|
|
1121
|
+
)}
|
|
1122
|
+
</Button>
|
|
1123
|
+
<Button
|
|
1124
|
+
type="button"
|
|
1125
|
+
variant="ghost"
|
|
1126
|
+
size="sm"
|
|
1127
|
+
onClick={() => saveEdit(index)}
|
|
1128
|
+
className="h-8 w-8 p-0 text-green-600 hover:text-green-700"
|
|
1129
|
+
>
|
|
1130
|
+
<Check className="w-4 h-4" />
|
|
1131
|
+
</Button>
|
|
1132
|
+
<Button
|
|
1133
|
+
type="button"
|
|
1134
|
+
variant="ghost"
|
|
1135
|
+
size="sm"
|
|
1136
|
+
onClick={cancelEdit}
|
|
1137
|
+
className="h-8 w-8 p-0 text-red-600 hover:text-red-700"
|
|
1138
|
+
>
|
|
1139
|
+
<X className="w-4 h-4" />
|
|
1140
|
+
</Button>
|
|
1141
|
+
</div>
|
|
1142
|
+
</div>
|
|
1143
|
+
) : (
|
|
1144
|
+
<div className="flex items-center gap-2 group">
|
|
1145
|
+
<Tooltip>
|
|
1146
|
+
<TooltipTrigger asChild>
|
|
1147
|
+
<div
|
|
1148
|
+
className={`flex-1 border rounded px-3 py-1 ${scrollbarContainerClass}`}
|
|
1149
|
+
>
|
|
1150
|
+
{visibleSecrets.has(index) ? (
|
|
1151
|
+
<code className="font-mono text-sm whitespace-nowrap block">
|
|
1152
|
+
{env.value || (
|
|
1153
|
+
<span className="text-muted-foreground italic">
|
|
1154
|
+
{isInGlobalEnv(env.name)
|
|
1155
|
+
? 'Using global value'
|
|
1156
|
+
: env.example
|
|
1157
|
+
? `e.g. ${env.example}`
|
|
1158
|
+
: 'Not set'}
|
|
1159
|
+
</span>
|
|
1160
|
+
)}
|
|
1161
|
+
</code>
|
|
1162
|
+
) : env.value ? (
|
|
1163
|
+
<code className="font-mono text-sm text-muted-foreground block">
|
|
1164
|
+
••••••••
|
|
1165
|
+
</code>
|
|
1166
|
+
) : (
|
|
1167
|
+
<span className="text-muted-foreground italic text-sm block">
|
|
1168
|
+
{isInGlobalEnv(env.name) ? 'Using global value' : 'Not set'}
|
|
1169
|
+
</span>
|
|
1170
|
+
)}
|
|
1171
|
+
</div>
|
|
1172
|
+
</TooltipTrigger>
|
|
1173
|
+
{isInGlobalEnv(env.name) && (
|
|
1174
|
+
<TooltipContent className="max-w-xs">
|
|
1175
|
+
<div className="space-y-1">
|
|
1176
|
+
<p className="font-semibold">Global Environment Variable</p>
|
|
1177
|
+
<p className="text-sm">
|
|
1178
|
+
{env.value
|
|
1179
|
+
? "You've entered a custom value for this agent. This overrides the global environment setting. Clear the field to use the global value."
|
|
1180
|
+
: 'This secret is currently using the value from your global environment settings. The actual value is hidden for security. Enter a value here if you want to override it for this agent only.'}
|
|
1181
|
+
</p>
|
|
1182
|
+
</div>
|
|
1183
|
+
</TooltipContent>
|
|
1184
|
+
)}
|
|
1185
|
+
</Tooltip>
|
|
1186
|
+
<Button
|
|
1187
|
+
type="button"
|
|
1188
|
+
variant="ghost"
|
|
1189
|
+
size="sm"
|
|
1190
|
+
onClick={() => toggleSecretVisibility(index)}
|
|
1191
|
+
className="h-8 w-8 p-0 opacity-0 group-hover:opacity-100 transition-opacity"
|
|
1192
|
+
>
|
|
1193
|
+
{visibleSecrets.has(index) ? (
|
|
1194
|
+
<EyeOff className="w-4 h-4" />
|
|
1195
|
+
) : (
|
|
1196
|
+
<Eye className="w-4 h-4" />
|
|
1197
|
+
)}
|
|
1198
|
+
</Button>
|
|
1199
|
+
</div>
|
|
1200
|
+
)}
|
|
1201
|
+
</div>
|
|
1202
|
+
|
|
1203
|
+
{/* Actions Column */}
|
|
1204
|
+
<div className="flex items-center justify-center gap-1">
|
|
1205
|
+
<Button
|
|
1206
|
+
type="button"
|
|
1207
|
+
variant="ghost"
|
|
1208
|
+
size="sm"
|
|
1209
|
+
onClick={() => startEditing(index)}
|
|
1210
|
+
disabled={editingIndex !== null && editingIndex !== index}
|
|
1211
|
+
className="h-8 w-8 p-0"
|
|
1212
|
+
>
|
|
1213
|
+
<Edit3 className="w-4 h-4" />
|
|
1214
|
+
</Button>
|
|
1215
|
+
<Button
|
|
1216
|
+
type="button"
|
|
1217
|
+
variant="ghost"
|
|
1218
|
+
size="sm"
|
|
1219
|
+
onClick={() => removeEnv(index)}
|
|
1220
|
+
disabled={editingIndex !== null}
|
|
1221
|
+
className="h-8 w-8 p-0 text-red-600 hover:text-red-700"
|
|
1222
|
+
>
|
|
1223
|
+
<Trash2 className="w-4 h-4" />
|
|
1224
|
+
</Button>
|
|
1225
|
+
</div>
|
|
1226
|
+
</div>
|
|
1227
|
+
))}
|
|
1228
|
+
</div>
|
|
1229
|
+
</div>
|
|
1230
|
+
|
|
1231
|
+
{/* Mobile Card Layout */}
|
|
1232
|
+
<div className="md:hidden space-y-3">
|
|
1233
|
+
{envs.map((env, index) => (
|
|
1234
|
+
<div
|
|
1235
|
+
key={`${env.name}-${index}-mobile`}
|
|
1236
|
+
className={`border rounded-lg p-4 space-y-3 ${
|
|
1237
|
+
env.isRequired && (!env.value || env.value.trim() === '')
|
|
1238
|
+
? 'border-red-500/50 bg-red-500/5'
|
|
1239
|
+
: ''
|
|
1240
|
+
}`}
|
|
1241
|
+
>
|
|
1242
|
+
{/* Header with name and required badge */}
|
|
1243
|
+
<div className="space-y-1">
|
|
1244
|
+
<div className="flex items-start justify-between gap-2">
|
|
1245
|
+
<div className="flex-1 min-w-0">
|
|
1246
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
1247
|
+
<code className="font-mono text-sm font-medium break-all">
|
|
1248
|
+
{env.name}
|
|
1249
|
+
</code>
|
|
1250
|
+
{env.isRequired && (
|
|
1251
|
+
<Badge variant="outline" className="text-xs">
|
|
1252
|
+
Required
|
|
1253
|
+
</Badge>
|
|
1254
|
+
)}
|
|
1255
|
+
{isInGlobalEnv(env.name) && (
|
|
1256
|
+
<Badge
|
|
1257
|
+
variant="secondary"
|
|
1258
|
+
className="text-xs flex items-center gap-1"
|
|
1259
|
+
>
|
|
1260
|
+
<Globe className="w-3 h-3" />
|
|
1261
|
+
Global
|
|
1262
|
+
</Badge>
|
|
1263
|
+
)}
|
|
1264
|
+
</div>
|
|
1265
|
+
{env.description && (
|
|
1266
|
+
<div className="text-xs text-muted-foreground mt-1">
|
|
1267
|
+
{env.description}
|
|
1268
|
+
</div>
|
|
1269
|
+
)}
|
|
1270
|
+
{env.plugin && (
|
|
1271
|
+
<div className="text-xs text-muted-foreground">
|
|
1272
|
+
Required by: {env.plugin}
|
|
1273
|
+
</div>
|
|
1274
|
+
)}
|
|
1275
|
+
</div>
|
|
1276
|
+
{/* Actions on mobile */}
|
|
1277
|
+
<div className="flex items-center gap-1">
|
|
1278
|
+
<Button
|
|
1279
|
+
type="button"
|
|
1280
|
+
variant="ghost"
|
|
1281
|
+
size="sm"
|
|
1282
|
+
onClick={() => startEditing(index)}
|
|
1283
|
+
disabled={editingIndex !== null && editingIndex !== index}
|
|
1284
|
+
className="h-8 w-8 p-0"
|
|
1285
|
+
>
|
|
1286
|
+
<Edit3 className="w-4 h-4" />
|
|
1287
|
+
</Button>
|
|
1288
|
+
<Button
|
|
1289
|
+
type="button"
|
|
1290
|
+
variant="ghost"
|
|
1291
|
+
size="sm"
|
|
1292
|
+
onClick={() => removeEnv(index)}
|
|
1293
|
+
disabled={editingIndex !== null}
|
|
1294
|
+
className="h-8 w-8 p-0 text-red-600 hover:text-red-700"
|
|
1295
|
+
>
|
|
1296
|
+
<Trash2 className="w-4 h-4" />
|
|
1297
|
+
</Button>
|
|
1298
|
+
</div>
|
|
1299
|
+
</div>
|
|
1300
|
+
</div>
|
|
1301
|
+
|
|
1302
|
+
{/* Value section */}
|
|
1303
|
+
<div className="space-y-1">
|
|
1304
|
+
<div className="text-xs font-medium text-muted-foreground uppercase">
|
|
1305
|
+
Value
|
|
1306
|
+
</div>
|
|
1307
|
+
{editingIndex === index ? (
|
|
1308
|
+
<div className="space-y-2">
|
|
1309
|
+
<div className="relative">
|
|
1310
|
+
<div className={`rounded border ${scrollbarContainerClass}`}>
|
|
1311
|
+
<Input
|
|
1312
|
+
value={editedValue}
|
|
1313
|
+
onChange={(e) => setEditedValue(e.target.value)}
|
|
1314
|
+
className="font-mono text-sm pr-10 border-0 focus-visible:ring-0 w-auto min-w-full rounded"
|
|
1315
|
+
style={{ minWidth: '100%' }}
|
|
1316
|
+
type={visibleSecrets.has(index) ? 'text' : 'password'}
|
|
1317
|
+
placeholder={
|
|
1318
|
+
isInGlobalEnv(env.name) && !env.value
|
|
1319
|
+
? `Using global (enter to override)`
|
|
1320
|
+
: env.example || 'Enter value...'
|
|
1321
|
+
}
|
|
1322
|
+
autoFocus
|
|
1323
|
+
onKeyDown={(e) => {
|
|
1324
|
+
if (e.key === 'Enter') {
|
|
1325
|
+
e.preventDefault();
|
|
1326
|
+
saveEdit(index);
|
|
1327
|
+
} else if (e.key === 'Escape') {
|
|
1328
|
+
cancelEdit();
|
|
1329
|
+
}
|
|
1330
|
+
}}
|
|
1331
|
+
/>
|
|
1332
|
+
</div>
|
|
1333
|
+
<Button
|
|
1334
|
+
type="button"
|
|
1335
|
+
variant="ghost"
|
|
1336
|
+
size="sm"
|
|
1337
|
+
onClick={() => toggleSecretVisibility(index)}
|
|
1338
|
+
className="absolute right-1 top-1/2 -translate-y-1/2 h-7 w-7 p-0"
|
|
1339
|
+
>
|
|
1340
|
+
{visibleSecrets.has(index) ? (
|
|
1341
|
+
<EyeOff className="w-4 h-4" />
|
|
1342
|
+
) : (
|
|
1343
|
+
<Eye className="w-4 h-4" />
|
|
1344
|
+
)}
|
|
1345
|
+
</Button>
|
|
1346
|
+
</div>
|
|
1347
|
+
<div className="flex items-center gap-2">
|
|
1348
|
+
<Button
|
|
1349
|
+
type="button"
|
|
1350
|
+
variant="outline"
|
|
1351
|
+
size="sm"
|
|
1352
|
+
onClick={() => saveEdit(index)}
|
|
1353
|
+
className="flex-1"
|
|
1354
|
+
>
|
|
1355
|
+
<Check className="w-4 h-4 mr-2" />
|
|
1356
|
+
Save
|
|
1357
|
+
</Button>
|
|
1358
|
+
<Button
|
|
1359
|
+
type="button"
|
|
1360
|
+
variant="ghost"
|
|
1361
|
+
size="sm"
|
|
1362
|
+
onClick={cancelEdit}
|
|
1363
|
+
className="flex-1"
|
|
1364
|
+
>
|
|
1365
|
+
<X className="w-4 h-4 mr-2" />
|
|
1366
|
+
Cancel
|
|
1367
|
+
</Button>
|
|
1368
|
+
</div>
|
|
1369
|
+
</div>
|
|
1370
|
+
) : (
|
|
1371
|
+
<div className="flex items-center gap-2">
|
|
1372
|
+
<Tooltip>
|
|
1373
|
+
<TooltipTrigger asChild>
|
|
1374
|
+
<div
|
|
1375
|
+
className={`flex-1 border rounded px-3 py-1 ${scrollbarContainerClass}`}
|
|
1376
|
+
>
|
|
1377
|
+
{visibleSecrets.has(index) ? (
|
|
1378
|
+
<code className="font-mono text-sm whitespace-nowrap block">
|
|
1379
|
+
{env.value || (
|
|
1380
|
+
<span className="text-muted-foreground italic">
|
|
1381
|
+
{isInGlobalEnv(env.name)
|
|
1382
|
+
? 'Using global value'
|
|
1383
|
+
: env.example
|
|
1384
|
+
? `e.g. ${env.example}`
|
|
1385
|
+
: 'Not set'}
|
|
1386
|
+
</span>
|
|
1387
|
+
)}
|
|
1388
|
+
</code>
|
|
1389
|
+
) : env.value ? (
|
|
1390
|
+
<code className="font-mono text-sm text-muted-foreground block">
|
|
1391
|
+
••••••••
|
|
1392
|
+
</code>
|
|
1393
|
+
) : (
|
|
1394
|
+
<span className="text-muted-foreground italic text-sm block">
|
|
1395
|
+
{isInGlobalEnv(env.name) ? 'Using global value' : 'Not set'}
|
|
1396
|
+
</span>
|
|
1397
|
+
)}
|
|
1398
|
+
</div>
|
|
1399
|
+
</TooltipTrigger>
|
|
1400
|
+
{isInGlobalEnv(env.name) && (
|
|
1401
|
+
<TooltipContent className="max-w-xs">
|
|
1402
|
+
<div className="space-y-1">
|
|
1403
|
+
<p className="font-semibold">Global Environment Variable</p>
|
|
1404
|
+
<p className="text-sm">
|
|
1405
|
+
{env.value
|
|
1406
|
+
? "You've entered a custom value for this agent. This overrides the global environment setting. Clear the field to use the global value."
|
|
1407
|
+
: 'This secret is currently using the value from your global environment settings. The actual value is hidden for security. Enter a value here if you want to override it for this agent only.'}
|
|
1408
|
+
</p>
|
|
1409
|
+
</div>
|
|
1410
|
+
</TooltipContent>
|
|
1411
|
+
)}
|
|
1412
|
+
</Tooltip>
|
|
1413
|
+
<Button
|
|
1414
|
+
type="button"
|
|
1415
|
+
variant="ghost"
|
|
1416
|
+
size="sm"
|
|
1417
|
+
onClick={() => toggleSecretVisibility(index)}
|
|
1418
|
+
className="h-8 w-8 p-0"
|
|
1419
|
+
>
|
|
1420
|
+
{visibleSecrets.has(index) ? (
|
|
1421
|
+
<EyeOff className="w-4 h-4" />
|
|
1422
|
+
) : (
|
|
1423
|
+
<Eye className="w-4 h-4" />
|
|
1424
|
+
)}
|
|
1425
|
+
</Button>
|
|
1426
|
+
</div>
|
|
1427
|
+
)}
|
|
1428
|
+
</div>
|
|
1429
|
+
</div>
|
|
1430
|
+
))}
|
|
1431
|
+
</div>
|
|
1432
|
+
</TooltipProvider>
|
|
1433
|
+
)}
|
|
1434
|
+
|
|
1435
|
+
{/* File Upload Area */}
|
|
1436
|
+
<div
|
|
1437
|
+
ref={dropRef}
|
|
1438
|
+
className={`border-2 border-dashed rounded-lg p-6 text-center cursor-pointer transition-colors ${
|
|
1439
|
+
isDragging
|
|
1440
|
+
? 'border-primary bg-primary/5'
|
|
1441
|
+
: 'border-muted-foreground/25 hover:border-muted-foreground/50'
|
|
1442
|
+
}`}
|
|
1443
|
+
onClick={() => document.getElementById('env-upload')?.click()}
|
|
1444
|
+
>
|
|
1445
|
+
<CloudUpload className="w-8 h-8 mx-auto mb-2 text-muted-foreground" />
|
|
1446
|
+
<div className="text-sm text-muted-foreground">
|
|
1447
|
+
Drag & drop <code className="bg-muted px-1 rounded">.env</code> file or click to
|
|
1448
|
+
select
|
|
1449
|
+
</div>
|
|
1450
|
+
<input
|
|
1451
|
+
id="env-upload"
|
|
1452
|
+
type="file"
|
|
1453
|
+
accept="*/*"
|
|
1454
|
+
className="hidden"
|
|
1455
|
+
onChange={(e) => {
|
|
1456
|
+
const file = e.target.files?.[0];
|
|
1457
|
+
if (file && file.name.endsWith('.env')) {
|
|
1458
|
+
handleFileUpload(file);
|
|
1459
|
+
}
|
|
1460
|
+
}}
|
|
1461
|
+
/>
|
|
1462
|
+
</div>
|
|
1463
|
+
</div>
|
|
1464
|
+
</ScrollArea>
|
|
1465
|
+
|
|
1466
|
+
{/* Raw Editor Modal */}
|
|
1467
|
+
<Dialog open={rawEditorOpen} onOpenChange={setRawEditorOpen}>
|
|
1468
|
+
<DialogContent className="max-w-3xl h-[600px] flex flex-col">
|
|
1469
|
+
<DialogHeader>
|
|
1470
|
+
<DialogTitle>Raw Editor</DialogTitle>
|
|
1471
|
+
<DialogDescription>
|
|
1472
|
+
Add, edit, or delete your project variables in .env format
|
|
1473
|
+
</DialogDescription>
|
|
1474
|
+
</DialogHeader>
|
|
1475
|
+
<div className="flex-1 border rounded-lg overflow-hidden">
|
|
1476
|
+
<Textarea
|
|
1477
|
+
value={rawEditorContent}
|
|
1478
|
+
onChange={(e) => setRawEditorContent(e.target.value)}
|
|
1479
|
+
placeholder="# Environment Variables # Required Secrets API_KEY=your_api_key_here # Optional Variables DEBUG=true # Comments are supported"
|
|
1480
|
+
className="w-full h-full resize-none border-0 font-mono text-sm focus-visible:ring-0"
|
|
1481
|
+
/>
|
|
1482
|
+
</div>
|
|
1483
|
+
<DialogFooter>
|
|
1484
|
+
<Button variant="outline" size="sm" onClick={copyEnvContent}>
|
|
1485
|
+
<Copy className="w-4 h-4 mr-2" />
|
|
1486
|
+
Copy ENV
|
|
1487
|
+
</Button>
|
|
1488
|
+
<Button variant="outline" size="sm" onClick={() => setRawEditorOpen(false)}>
|
|
1489
|
+
Cancel
|
|
1490
|
+
</Button>
|
|
1491
|
+
<Button size="sm" onClick={handleRawEditorSave}>
|
|
1492
|
+
Update Variables
|
|
1493
|
+
</Button>
|
|
1494
|
+
</DialogFooter>
|
|
1495
|
+
</DialogContent>
|
|
1496
|
+
</Dialog>
|
|
1497
|
+
</div>
|
|
1498
|
+
);
|
|
1499
|
+
}
|
|
1500
|
+
);
|
|
1501
|
+
|
|
1502
|
+
SecretPanel.displayName = 'SecretPanel';
|
|
1503
|
+
|
|
1504
|
+
// Also provide a default export for backward compatibility
|
|
1505
|
+
export default SecretPanel;
|