derricka59 commited on
Commit
5d49b54
·
verified ·
1 Parent(s): 330bd8c

fix my app

Browse files
Files changed (3) hide show
  1. components/modals.js +9 -2
  2. index.html +6 -1248
  3. script.js +934 -39
components/modals.js CHANGED
@@ -26,14 +26,21 @@ class CustomModals extends HTMLElement {
26
  transform: translateY(-2.5rem);
27
  transition: transform 0.25s ease;
28
  }
 
 
 
 
 
 
 
29
  </style>
30
 
31
  <!-- Loading Modal -->
32
  <div id="loading-modal" class="modal">
33
  <div class="modal-content text-center">
34
  <div class="animate-spin h-12 w-12 text-indigo-600 mx-auto">
35
- <svg xmlns="http://www.gstatic.com/firebasejs/11.6.1/firebase-app.js" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
36
- </div>
37
  <p id="loading-message" class="text-sm font-medium text-slate-700 mt-4">Loading...</p>
38
  </div>
39
  </div>
 
26
  transform: translateY(-2.5rem);
27
  transition: transform 0.25s ease;
28
  }
29
+ .modal:not(.opacity-0) {
30
+ opacity: 1;
31
+ pointer-events: auto;
32
+ }
33
+ .modal-content:not(.translate-y-10) {
34
+ transform: translateY(0);
35
+ }
36
  </style>
37
 
38
  <!-- Loading Modal -->
39
  <div id="loading-modal" class="modal">
40
  <div class="modal-content text-center">
41
  <div class="animate-spin h-12 w-12 text-indigo-600 mx-auto">
42
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
43
+ </div>
44
  <p id="loading-message" class="text-sm font-medium text-slate-700 mt-4">Loading...</p>
45
  </div>
46
  </div>
index.html CHANGED
@@ -87,1120 +87,12 @@
87
  <script src="components/sidebar.js"></script>
88
  <script src="components/modals.js"></script>
89
  <script src="script.js"></script>
90
-
91
- <script>
92
- // --- App State & Firebase Globals ---
93
- let app, db, auth, userId, agentConfigDocRef;
94
- let isProfileSidebarOpen = false;
95
- let testChatHistory = []; // State for test chat
96
-
97
- const defaultAppState = {
98
- agentName: "Creative Writer",
99
- description: "A helpful assistant for Python code.",
100
- instructions: "You are a helpful AI assistant...",
101
- modelConfig: {
102
- selectedModel: 'llama3-70b',
103
- apiKeys: { 'qhy-pro': '', 'gemini-2.5': '' }
104
- },
105
- knowledge: {
106
- prioritize: false,
107
- urls: ['https://qhy.sync/docs/main']
108
- },
109
- capabilities: {
110
- codeInterpreter: false,
111
- imageGenerator: true
112
- },
113
- prompts: [
114
- { title: "Explain a concept", message: "\"Explain quantum computing...\"" },
115
- { title: "Write some code", message: "\"Write a Python function...\"" }
116
- ],
117
- bio: "",
118
- displayName: "",
119
- photoURL: ""
120
- };
121
-
122
- let appState = JSON.parse(JSON.stringify(defaultAppState));
123
- // --- DOM Selectors (will be assigned in selectDOMElements) ---
124
- let agentNameInput, agentDescriptionInput, agentInstructionsInput,
125
- listenInstructionsBtn, ttsPlayer, // NEW
126
- modelConfigContainer, knowledgeListContainer, knowledgeEmptyState,
127
- knowledgeUrlInput, addKnowledgeUrlBtn, prioritizeKnowledgeToggle,
128
- codeInterpreterToggle, testCodeInterpreterBtn, // MODIFIED
129
- imageGeneratorToggle, testImageGenBtn,
130
- promptListContainer, promptEmptyState, addPromptBtn,
131
- saveChangesBtn, deleteAgentBtn, suggestNameBtn,
132
- suggestDescriptionBtn, generateInstructionsBtn, suggestPromptsBtn,
133
- loadingModal, loadingMessage, promptModal, promptModalTitle,
134
- promptModalCloseBtn, promptModalCancelBtn, promptModalForm,
135
- promptModalSaveBtn, promptModalEditIndex, promptModalTitleInput,
136
- promptModalMessageInput, deleteAgentModal, deleteAgentCancelBtn,
137
- deleteAgentConfirmBtn,
138
- // UI Elements
139
- menuBtn, profileToggleBtn, profileHeaderIcon, profileHeaderImg,
140
- profileSidebar, sidebarOverlay, profileSidebarCloseBtn,
141
- profileImgPreview, profileImgUrlInput, profileDisplayNameInput,
142
- profileBioInput, suggestBioBtn, saveProfileBtn, googleSigninBtn,
143
- githubSigninBtn, signOutBtn, socialLoginContainer, signOutContainer,
144
- geminiSettingsModal, geminiSettingsCloseBtn, geminiSettingsSaveBtn,
145
- // Test Agent Elements
146
- testAgentBtn, testAgentModal, testAgentCloseBtn, testAgentHistory,
147
- testAgentForm, testAgentInput,
148
- // Image Gen Modal Elements
149
- imageGenModal, imageGenCloseBtn, imageGenForm, imageGenInput,
150
- imageGenResultContainer, imageGenSpinner, imageGenResult, imageGenError,
151
- // NEW: Code Test Modal Elements
152
- codeTestModal, codeTestCloseBtn, codeTestGenerateBtn,
153
- codeTestResultContainer, codeTestSpinner, codeTestResult;
154
- /**
155
- * Assign all DOM elements to their variables.
156
- */
157
- function selectDOMElements() {
158
- // Existing Elements
159
- agentNameInput = document.getElementById('agent-name');
160
- agentDescriptionInput = document.getElementById('agent-description');
161
- agentInstructionsInput = document.getElementById('agent-instructions');
162
- listenInstructionsBtn = document.getElementById('listen-instructions-btn'); // NEW
163
- ttsPlayer = document.getElementById('tts-player'); // NEW
164
- modelConfigContainer = document.getElementById('model-config-container');
165
- knowledgeListContainer = document.getElementById('knowledge-list-container');
166
- knowledgeEmptyState = document.getElementById('knowledge-empty-state');
167
- knowledgeUrlInput = document.getElementById('knowledge-url-input');
168
- addKnowledgeUrlBtn = document.getElementById('add-knowledge-url-btn');
169
- prioritizeKnowledgeToggle = document.getElementById('prioritize-knowledge');
170
- codeInterpreterToggle = document.getElementById('code-interpreter');
171
- testCodeInterpreterBtn = document.getElementById('test-code-interpreter-btn'); // NEW
172
- imageGeneratorToggle = document.getElementById('image-generator');
173
- testImageGenBtn = document.getElementById('test-image-gen-btn');
174
- promptListContainer = document.getElementById('prompt-list-container');
175
- promptEmptyState = document.getElementById('prompt-empty-state');
176
- addPromptBtn = document.getElementById('add-prompt-btn');
177
- saveChangesBtn = document.getElementById('save-changes-btn');
178
- deleteAgentBtn = document.getElementById('delete-agent-btn');
179
- suggestNameBtn = document.getElementById('suggest-name-btn');
180
- suggestDescriptionBtn = document.getElementById('suggest-description-btn');
181
- generateInstructionsBtn = document.getElementById('generate-instructions-btn');
182
- suggestPromptsBtn = document.getElementById('suggest-prompts-btn');
183
- loadingModal = document.getElementById('loading-modal');
184
- loadingMessage = document.getElementById('loading-message');
185
- promptModal = document.getElementById('prompt-modal');
186
- promptModalTitle = document.getElementById('prompt-modal-title');
187
- promptModalCloseBtn = document.getElementById('prompt-modal-close-btn');
188
- promptModalCancelBtn = document.getElementById('prompt-modal-cancel-btn');
189
- promptModalForm = document.getElementById('prompt-modal-form');
190
- promptModalSaveBtn = document.getElementById('prompt-modal-save-btn');
191
- promptModalEditIndex = document.getElementById('prompt-modal-edit-index');
192
- promptModalTitleInput = document.getElementById('prompt-modal-title-input');
193
- promptModalMessageInput = document.getElementById('prompt-modal-message-input');
194
- deleteAgentModal = document.getElementById('delete-agent-modal');
195
- deleteAgentCancelBtn = document.getElementById('delete-agent-cancel-btn');
196
- deleteAgentConfirmBtn = document.getElementById('delete-agent-confirm-btn');
197
-
198
- // Profile & Menu Elements
199
- menuBtn = document.getElementById('menu-btn');
200
- profileToggleBtn = document.getElementById('profile-toggle-btn');
201
- profileHeaderIcon = document.getElementById('profile-header-icon');
202
- profileHeaderImg = document.getElementById('profile-header-img');
203
- profileSidebar = document.getElementById('profile-sidebar');
204
- sidebarOverlay = document.getElementById('sidebar-overlay');
205
- profileSidebarCloseBtn = document.getElementById('profile-sidebar-close-btn');
206
- profileImgPreview = document.getElementById('profile-img-preview');
207
- profileImgUrlInput = document.getElementById('profile-img-url-input');
208
- profileDisplayNameInput = document.getElementById('profile-display-name-input');
209
- profileBioInput = document.getElementById('profile-bio-input');
210
- suggestBioBtn = document.getElementById('suggest-bio-btn');
211
- saveProfileBtn = document.getElementById('save-profile-btn');
212
- googleSigninBtn = document.getElementById('google-signin-btn');
213
- githubSigninBtn = document.getElementById('github-signin-btn');
214
- signOutBtn = document.getElementById('sign-out-btn');
215
- socialLoginContainer = document.getElementById('social-login-container');
216
- signOutContainer = document.getElementById('sign-out-container');
217
- geminiSettingsModal = document.getElementById('gemini-settings-modal');
218
- geminiSettingsCloseBtn = document.getElementById('gemini-settings-close-btn');
219
- geminiSettingsSaveBtn = document.getElementById('gemini-settings-save-btn');
220
-
221
- // Test Agent Elements
222
- testAgentBtn = document.getElementById('test-agent-btn');
223
- testAgentModal = document.getElementById('test-agent-modal');
224
- testAgentCloseBtn = document.getElementById('test-agent-close-btn');
225
- testAgentHistory = document.getElementById('test-agent-history');
226
- testAgentForm = document.getElementById('test-agent-form');
227
- testAgentInput = document.getElementById('test-agent-input');
228
-
229
- // Image Gen Modal Elements
230
- imageGenModal = document.getElementById('image-gen-modal');
231
- imageGenCloseBtn = document.getElementById('image-gen-close-btn');
232
- imageGenForm = document.getElementById('image-gen-form');
233
- imageGenInput = document.getElementById('image-gen-input');
234
- imageGenResultContainer = document.getElementById('image-gen-result-container');
235
- imageGenSpinner = document.getElementById('image-gen-spinner');
236
- imageGenResult = document.getElementById('image-gen-result');
237
- imageGenError = document.getElementById('image-gen-error');
238
-
239
- // NEW: Code Test Modal Elements
240
- codeTestModal = document.getElementById('code-test-modal');
241
- codeTestCloseBtn = document.getElementById('code-test-close-btn');
242
- codeTestGenerateBtn = document.getElementById('code-test-generate-btn');
243
- codeTestResultContainer = document.getElementById('code-test-result-container');
244
- codeTestSpinner = document.getElementById('code-test-spinner');
245
- codeTestResult = document.getElementById('code-test-result');
246
- }
247
-
248
-
249
- // --- Render Functions ---
250
-
251
- /** Initializes the UI from the current appState */
252
- function initializeAppUI() {
253
- if (!agentNameInput) {
254
- console.warn("DOM elements not ready, skipping UI init.");
255
- return; // Guard clause
256
- }
257
-
258
- // 1. Populate Agent Config Form
259
- agentNameInput.value = appState.agentName;
260
- agentDescriptionInput.value = appState.description;
261
- agentInstructionsInput.value = appState.instructions;
262
- prioritizeKnowledgeToggle.checked = appState.knowledge.prioritize;
263
-
264
- // Handle Code Interpreter Toggle and Test Button
265
- codeInterpreterToggle.checked = appState.capabilities.codeInterpreter;
266
- if (appState.capabilities.codeInterpreter) {
267
- testCodeInterpreterBtn.disabled = false;
268
- testCodeInterpreterBtn.classList.remove('btn-disabled', 'text-slate-400');
269
- testCodeInterpreterBtn.classList.add('text-indigo-600', 'hover:bg-indigo-50');
270
- } else {
271
- testCodeInterpreterBtn.disabled = true;
272
- testCodeInterpreterBtn.classList.add('btn-disabled', 'text-slate-400');
273
- testCodeInterpreterBtn.classList.remove('text-indigo-600', 'hover:bg-indigo-50');
274
- }
275
-
276
- // Handle Image Generator Toggle and Test Button
277
- imageGeneratorToggle.checked = appState.capabilities.imageGenerator;
278
- if (appState.capabilities.imageGenerator) {
279
- testImageGenBtn.disabled = false;
280
- testImageGenBtn.classList.remove('btn-disabled', 'text-slate-400');
281
- testImageGenBtn.classList.add('text-indigo-600', 'hover:bg-indigo-50');
282
- } else {
283
- testImageGenBtn.disabled = true;
284
- testImageGenBtn.classList.add('btn-disabled', 'text-slate-400');
285
- testImageGenBtn.classList.remove('text-indigo-600', 'hover:bg-indigo-50');
286
- }
287
-
288
-
289
- if (appState.modelConfig?.selectedModel) {
290
- const modelInput = document.querySelector(`input[name="model-selection"][value="${appState.modelConfig.selectedModel}"]`);
291
- if (modelInput) modelInput.checked = true;
292
- }
293
- document.getElementById('api-key-qhy-pro-input').value = appState.modelConfig?.apiKeys?.['qhy-pro'] || '';
294
- document.getElementById('api-key-gemini-2.5-input').value = appState.modelConfig?.apiKeys?.['gemini-2.5'] || '';
295
-
296
- renderModelConfig();
297
- renderKnowledgeList();
298
- renderPromptList();
299
-
300
- // 2. Populate Profile Sidebar
301
- const currentUser = auth.currentUser;
302
- const displayName = currentUser?.displayName || appState.displayName || '';
303
- const photoURL = currentUser?.photoURL || appState.photoURL || '';
304
- const bio = appState.bio || '';
305
-
306
- profileDisplayNameInput.value = displayName;
307
- profileImgUrlInput.value = photoURL;
308
- profileBioInput.value = bio;
309
-
310
- // Update image previews
311
- updateImagePreviews(photoURL);
312
-
313
- // 3. Update Auth-dependent UI
314
- if (currentUser) {
315
- if (currentUser.isAnonymous) {
316
- socialLoginContainer.classList.remove('hidden');
317
- signOutContainer.classList.add('hidden');
318
- saveProfileBtn.classList.add('btn-disabled');
319
- saveProfileBtn.disabled = true;
320
- saveProfileBtn.title = "Sign in to save profile";
321
- } else {
322
- socialLoginContainer.classList.add('hidden');
323
- signOutContainer.classList.remove('hidden');
324
- saveProfileBtn.classList.remove('btn-disabled');
325
- saveProfileBtn.disabled = false;
326
- saveProfileBtn.title = "";
327
- }
328
- }
329
- }
330
- // ... (other render functions: updateImagePreviews, renderKnowledgeList, etc. - unchanged) ...
331
- function updateImagePreviews(url) {
332
- const validUrl = url && (url.startsWith('http') || url.startsWith('data:image'));
333
- const userIconSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>`;
334
- if (validUrl) {
335
- profileHeaderImg.src = url;
336
- profileHeaderImg.classList.remove('hidden');
337
- profileHeaderIcon.classList.add('hidden');
338
- } else {
339
- profileHeaderImg.classList.add('hidden');
340
- profileHeaderIcon.classList.remove('hidden');
341
- }
342
- if (validUrl) {
343
- profileImgPreview.src = url;
344
- profileImgPreview.classList.remove('bg-slate-200', 'text-slate-400');
345
- profileImgPreview.innerHTML = '';
346
- } else {
347
- profileImgPreview.src = '';
348
- profileImgPreview.classList.add('bg-slate-200', 'text-slate-400');
349
- profileImgPreview.innerHTML = userIconSvg;
350
- }
351
- }
352
- function renderKnowledgeList() {
353
- if (!knowledgeListContainer) return;
354
- const items = knowledgeListContainer.querySelectorAll('.knowledge-item');
355
- items.forEach(item => item.remove());
356
- const urls = appState.knowledge?.urls || [];
357
- if (urls.length === 0) {
358
- if (knowledgeEmptyState) knowledgeEmptyState.classList.remove('hidden');
359
- } else {
360
- if (knowledgeEmptyState) knowledgeEmptyState.classList.add('hidden');
361
- urls.forEach((url, index) => {
362
- const div = document.createElement('div');
363
- div.className = 'knowledge-item flex items-center justify-between rounded-md border border-slate-200 bg-white p-3';
364
- div.innerHTML = `<span class="text-sm text-slate-800 truncate" title="${url}">${url}</span><button class="ml-4 flex-shrink-0 p-1 text-slate-400 hover:text-red-600 rounded-full hover:bg-red-50" data-index="${index}"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="pointer-events-none"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg></button>`;
365
- knowledgeListContainer.appendChild(div);
366
- });
367
- }
368
- }
369
- function renderPromptList() {
370
- if (!promptListContainer) return;
371
- const items = promptListContainer.querySelectorAll('.prompt-item');
372
- items.forEach(item => item.remove());
373
- const prompts = appState.prompts || [];
374
- if (prompts.length === 0) {
375
- if (promptEmptyState) promptEmptyState.classList.remove('hidden');
376
- } else {
377
- if (promptEmptyState) promptEmptyState.classList.add('hidden');
378
- prompts.forEach((prompt, index) => {
379
- const div = document.createElement('div');
380
- div.className = 'prompt-item rounded-md border border-slate-200 p-4';
381
- div.innerHTML = `<div class="flex justify-between items-center mb-2"><p class="text-sm font-semibold text-slate-800">${prompt.title}</p><div><button class="edit-prompt-btn p-1 text-slate-400 hover:text-slate-700 rounded-full hover:bg-slate-100" data-index="${index}"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="pointer-events-none"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg></button><button class="delete-prompt-btn ml-1 p-1 text-slate-400 hover:text-red-600 rounded-full hover:bg-red-50" data-index="${index}"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="pointer-events-none"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg></button></div></div><p class="text-sm text-slate-600 bg-slate-50 p-3 rounded">${prompt.message}</p>`;
382
- promptListContainer.appendChild(div);
383
- });
384
- }
385
- }
386
- function renderModelConfig() {
387
- const qhyProEl = document.getElementById('api-key-qhy-pro');
388
- const geminiEl = document.getElementById('api-key-gemini-2.5');
389
- if (!qhyProEl || !geminiEl) return;
390
-
391
- qhyProEl.classList.add('hidden');
392
- geminiEl.classList.add('hidden');
393
- const selected = appState.modelConfig?.selectedModel;
394
- if (selected === 'qhy-pro') {
395
- qhyProEl.classList.remove('hidden');
396
- } else if (selected === 'gemini-2.5') {
397
- geminiEl.classList.remove('hidden');
398
- }
399
- }
400
- function renderTestChatMessage(sender, text) {
401
- const messageDiv = document.createElement('div');
402
- messageDiv.className = `chat-message ${sender} mb-2`;
403
- let html = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
404
- html = html.replace(/`(.*?)`/g, '<code class="bg-slate-200 px-1 py-0.5 rounded text-sm">$1</code>');
405
- html = html.replace(/\n/g, '<br>');
406
- messageDiv.innerHTML = html;
407
- testAgentHistory.appendChild(messageDiv);
408
- testAgentHistory.scrollTop = testAgentHistory.scrollHeight;
409
- }
410
- function setChatTyping(isTyping) {
411
- let typingEl = document.getElementById('typing-indicator');
412
- if (isTyping) {
413
- if (!typingEl) {
414
- typingEl = document.createElement('div');
415
- typingEl.id = 'typing-indicator';
416
- typingEl.className = 'chat-message model typing';
417
- typingEl.textContent = 'typing...';
418
- testAgentHistory.appendChild(typingEl);
419
- testAgentHistory.scrollTop = testAgentHistory.scrollHeight;
420
- }
421
- } else {
422
- if (typingEl) {
423
- typingEl.remove();
424
- }
425
- }
426
- }
427
-
428
- // --- Modal & Sidebar Functions ---
429
-
430
- function showModal(modalEl) {
431
- modalEl.classList.remove('opacity-0', 'pointer-events-none');
432
- modalEl.querySelector('.modal-content').classList.remove('-translate-y-10');
433
- }
434
- function hideModal(modalEl) {
435
- modalEl.classList.add('opacity-0', 'pointer-events-none');
436
- modalEl.querySelector('.modal-content').classList.add('-translate-y-10');
437
- }
438
- function showLoading(message = "Generating...") {
439
- loadingMessage.textContent = message;
440
- loadingModal.classList.remove('opacity-0', 'pointer-events-none');
441
- }
442
- function hideLoading() {
443
- loadingModal.classList.add('opacity-0', 'pointer-events-none');
444
- }
445
- function showPromptModal(mode = 'add', index = -1) {
446
- if (mode === 'edit' && index > -1) {
447
- const prompt = appState.prompts[index];
448
- promptModalTitle.textContent = 'Edit Prompt';
449
- promptModalSaveBtn.textContent = 'Save Changes';
450
- promptModalEditIndex.value = index;
451
- promptModalTitleInput.value = prompt.title;
452
- promptModalMessageInput.value = prompt.message;
453
- } else {
454
- promptModalTitle.textContent = 'Add New Prompt';
455
- promptModalSaveBtn.textContent = 'Save Prompt';
456
- promptModalForm.reset();
457
- promptModalEditIndex.value = -1;
458
- }
459
- showModal(promptModal);
460
- }
461
-
462
- function toggleProfileSidebar() {
463
- isProfileSidebarOpen = !isProfileSidebarOpen;
464
- if (isProfileSidebarOpen) {
465
- profileSidebar.classList.add('open');
466
- sidebarOverlay.classList.remove('opacity-0', 'pointer-events-none');
467
- } else {
468
- profileSidebar.classList.remove('open');
469
- sidebarOverlay.classList.add('opacity-0', 'pointer-events-none');
470
- }
471
- }
472
-
473
- function openTestAgentModal() {
474
- testChatHistory = []; // Reset history
475
- testAgentHistory.innerHTML = ''; // Clear UI
476
- renderTestChatMessage('model', 'Hi! I\'m ready to test. My instructions are set. Ask me anything.');
477
- showModal(testAgentModal);
478
- }
479
-
480
- function openImageGenModal() {
481
- imageGenInput.value = '';
482
- imageGenResult.src = '';
483
- imageGenResult.alt = '';
484
- imageGenError.textContent = '';
485
- imageGenResultContainer.classList.remove('loading');
486
- imageGenResult.classList.add('hidden');
487
- imageGenError.classList.add('hidden');
488
- showModal(imageGenModal);
489
- }
490
-
491
- // NEW: Open Code Test Modal
492
- function openCodeTestModal() {
493
- codeTestResultContainer.classList.remove('loading');
494
- codeTestResult.textContent = '// Click "Generate Example" to see a code snippet...';
495
- codeTestSpinner.style.display = 'none';
496
- codeTestResult.style.display = 'block';
497
- showModal(codeTestModal);
498
- }
499
- // --- Gemini API Callers ---
500
-
501
- // Text Generation
502
- async function callGeminiApi(payload, maxRetries = 5) {
503
- const apiKey = "";
504
- const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${apiKey}`;
505
- let attempt = 0, delay = 1000;
506
- while (attempt < maxRetries) {
507
- try {
508
- const response = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
509
- if (response.status === 429 || response.status >= 500) throw new Error(`API Error: ${response.status}`);
510
- if (!response.ok) { const errorResult = await response.json(); console.error("Gemini API Error:", errorResult); throw new Error(`Gemini API Error: ${errorResult.error?.message || response.statusText}`); }
511
- const result = await response.json();
512
- const candidate = result.candidates?.[0];
513
- if (!candidate) throw new Error("Invalid API response: No candidates found.");
514
- if (payload.generationConfig?.responseMimeType === "application/json") {
515
- const text = candidate.content?.parts?.[0]?.text;
516
- if (!text) throw new Error("Invalid API response: No text part found for JSON.");
517
- return text;
518
- }
519
- const text = candidate.content?.parts?.[0]?.text;
520
- if (typeof text !== 'string') throw new Error("Invalid API response: No text content found.");
521
- return text;
522
- } catch (error) {
523
- console.warn(`Gemini API call attempt ${attempt + 1} failed: ${error.message}`);
524
- attempt++;
525
- if (attempt >= maxRetries) throw new Error(`Gemini API failed after ${maxRetries} attempts: ${error.message}`);
526
- await new Promise(resolve => setTimeout(resolve, delay));
527
- delay *= 2;
528
- }
529
- }
530
- }
531
- // Image Generation
532
- async function callImagenApi(userPrompt, maxRetries = 5) {
533
- const apiKey = ""; // API key is handled by the environment
534
- const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/imagen-3.0-generate-002:predict?key=${apiKey}`;
535
-
536
- const payload = {
537
- instances: { prompt: userPrompt },
538
- parameters: { "sampleCount": 1 }
539
- };
540
-
541
- let attempt = 0, delay = 1000;
542
- while (attempt < maxRetries) {
543
- try {
544
- const response = await fetch(apiUrl, {
545
- method: 'POST',
546
- headers: { 'Content-Type': 'application/json' },
547
- body: JSON.stringify(payload)
548
- });
549
-
550
- if (response.status === 429 || response.status >= 500) {
551
- throw new Error(`API Error: ${response.status}`);
552
- }
553
- if (!response.ok) {
554
- const errorResult = await response.json();
555
- console.error("Imagen API Error:", errorResult);
556
- throw new Error(`Imagen API Error: ${errorResult.error?.message || response.statusText}`);
557
- }
558
-
559
- const result = await response.json();
560
-
561
- if (result.predictions && result.predictions.length > 0 && result.predictions[0].bytesBase64Encoded) {
562
- return `data:image/png;base64,${result.predictions[0].bytesBase64Encoded}`;
563
- } else {
564
- throw new Error("Invalid API response: No image data found.");
565
- }
566
-
567
- } catch (error) {
568
- console.warn(`Imagen API call attempt ${attempt + 1} failed: ${error.message}`);
569
- attempt++;
570
- if (attempt >= maxRetries) {
571
- throw new Error(`Imagen API failed after ${maxRetries} attempts: ${error.message}`);
572
- }
573
- await new Promise(resolve => setTimeout(resolve, delay));
574
- delay *= 2;
575
- }
576
- }
577
- }
578
- // NEW: TTS Generation
579
- async function callTtsApi(textToSpeak, maxRetries = 5) {
580
- const apiKey = "";
581
- const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-tts:generateContent?key=${apiKey}`;
582
-
583
- const payload = {
584
- contents: [{
585
- parts: [{ text: textToSpeak }]
586
- }],
587
- generationConfig: {
588
- responseModalities: ["AUDIO"],
589
- speechConfig: {
590
- voiceConfig: {
591
- prebuiltVoiceConfig: { voiceName: "Kore" } // Using 'Kore' voice
592
- }
593
- }
594
- },
595
- model: "gemini-2.5-flash-preview-tts"
596
- };
597
-
598
- let attempt = 0, delay = 1000;
599
- while (attempt < maxRetries) {
600
- try {
601
- const response = await fetch(apiUrl, {
602
- method: 'POST',
603
- headers: { 'Content-Type': 'application/json' },
604
- body: JSON.stringify(payload)
605
- });
606
-
607
- if (response.status === 429 || response.status >= 500) throw new Error(`API Error: ${response.status}`);
608
- if (!response.ok) { const errorResult = await response.json(); console.error("TTS API Error:", errorResult); throw new Error(`TTS API Error: ${errorResult.error?.message || response.statusText}`); }
609
-
610
- const result = await response.json();
611
- const part = result?.candidates?.[0]?.content?.parts?.[0];
612
- const audioData = part?.inlineData?.data;
613
- const mimeType = part?.inlineData?.mimeType;
614
-
615
- if (audioData && mimeType && mimeType.startsWith("audio/")) {
616
- const sampleRate = parseInt(mimeType.match(/rate=(\d+)/)[1], 10) || 24000;
617
- return { audioData, sampleRate };
618
- } else {
619
- throw new Error("Invalid API response: No audio data found.");
620
- }
621
- } catch (error) {
622
- console.warn(`TTS API call attempt ${attempt + 1} failed: ${error.message}`);
623
- attempt++;
624
- if (attempt >= maxRetries) throw new Error(`TTS API failed after ${maxRetries} attempts: ${error.message}`);
625
- await new Promise(resolve => setTimeout(resolve, delay));
626
- delay *= 2;
627
- }
628
- }
629
- }
630
-
631
- // --- TTS Audio Helpers ---
632
-
633
- // NEW: Decodes base64 string to ArrayBuffer
634
- function base64ToArrayBuffer(base64) {
635
- const binaryString = window.atob(base64);
636
- const len = binaryString.length;
637
- const bytes = new Uint8Array(len);
638
- for (let i = 0; i < len; i++) {
639
- bytes[i] = binaryString.charCodeAt(i);
640
- }
641
- return bytes.buffer;
642
- }
643
-
644
- // NEW: Wraps raw PCM data in a WAV header
645
- function pcmToWav(pcmData, sampleRate) {
646
- const numChannels = 1;
647
- const bitsPerSample = 16;
648
- const byteRate = sampleRate * numChannels * (bitsPerSample / 8);
649
- const blockAlign = numChannels * (bitsPerSample / 8);
650
- const dataSize = pcmData.byteLength;
651
- const fileSize = 36 + dataSize;
652
-
653
- const buffer = new ArrayBuffer(44 + dataSize);
654
- const view = new DataView(buffer);
655
-
656
- // RIFF header
657
- writeString(view, 0, 'RIFF');
658
- view.setUint32(4, fileSize, true);
659
- writeString(view, 8, 'WAVE');
660
-
661
- // fmt chunk
662
- writeString(view, 12, 'fmt ');
663
- view.setUint32(16, 16, true); // chunk size
664
- view.setUint16(20, 1, true); // audio format (1 = PCM)
665
- view.setUint16(22, numChannels, true);
666
- view.setUint32(24, sampleRate, true);
667
- view.setUint32(28, byteRate, true);
668
- view.setUint16(32, blockAlign, true);
669
- view.setUint16(34, bitsPerSample, true);
670
-
671
- // data chunk
672
- writeString(view, 36, 'data');
673
- view.setUint32(40, dataSize, true);
674
-
675
- // Write PCM data
676
- new Uint8Array(buffer, 44).set(new Uint8Array(pcmData));
677
-
678
- return new Blob([buffer], { type: 'audio/wav' });
679
- }
680
-
681
- // NEW: Helper for pcmToWav
682
- function writeString(view, offset, string) {
683
- for (let i = 0; i < string.length; i++) {
684
- view.setUint8(offset + i, string.charCodeAt(i));
685
- }
686
- }
687
-
688
- /**
689
- * Loads the agent configuration from Firestore.
690
- */
691
- async function loadAgentConfig() {
692
- if (!agentConfigDocRef) return;
693
- try {
694
- const docSnap = await getDoc(agentConfigDocRef);
695
- if (docSnap.exists()) {
696
- console.log("Loaded data from Firestore:", docSnap.data());
697
- appState = { ...defaultAppState, ...docSnap.data() };
698
- } else {
699
- console.log("No saved config found, using default.");
700
- appState = { ...defaultAppState };
701
- }
702
- } catch (error) {
703
- console.error("Error loading agent config:", error);
704
- appState = { ...defaultAppState };
705
- }
706
- }
707
-
708
- // --- Auth Functions (unchanged) ---
709
- async function signInWithGoogle() {
710
- const provider = new GoogleAuthProvider();
711
- try {
712
- await signInWithPopup(auth, provider);
713
- toggleProfileSidebar();
714
- } catch (error) { console.error("Google Sign-In Error:", error); }
715
- }
716
- async function signInWithGitHub() {
717
- const provider = new GithubAuthProvider();
718
- try {
719
- await signInWithPopup(auth, provider);
720
- toggleProfileSidebar();
721
- } catch (error) { console.error("GitHub Sign-In Error:", error); }
722
- }
723
- async function handleSignOut() {
724
- try {
725
- await signOut(auth);
726
- toggleProfileSidebar();
727
- } catch (error) { console.error("Sign-Out Error:", error); }
728
- }
729
- async function handleSaveProfile() {
730
- const currentUser = auth.currentUser;
731
- if (!currentUser || currentUser.isAnonymous) return;
732
- const displayName = profileDisplayNameInput.value.trim();
733
- const photoURL = profileImgUrlInput.value.trim();
734
- const bio = profileBioInput.value.trim();
735
- saveProfileBtn.textContent = "Saving...";
736
- saveProfileBtn.disabled = true;
737
- saveProfileBtn.classList.add('btn-disabled');
738
- try {
739
- await updateProfile(currentUser, { displayName, photoURL });
740
- const profileData = { displayName, photoURL, bio };
741
- await setDoc(agentConfigDocRef, profileData, { merge: true });
742
- appState.displayName = displayName;
743
- appState.photoURL = photoURL;
744
- appState.bio = bio;
745
- updateImagePreviews(photoURL);
746
- saveProfileBtn.textContent = "Profile Saved!";
747
- saveProfileBtn.classList.add('bg-green-600');
748
- setTimeout(() => {
749
- saveProfileBtn.textContent = "Save Profile";
750
- saveProfileBtn.disabled = false;
751
- saveProfileBtn.classList.remove('btn-disabled', 'bg-green-600');
752
- }, 2000);
753
- } catch (error) {
754
- console.error("Error saving profile:", error);
755
- saveProfileBtn.textContent = "Save Failed";
756
- saveProfileBtn.classList.add('bg-red-600');
757
- setTimeout(() => {
758
- saveProfileBtn.textContent = "Save Profile";
759
- saveProfileBtn.disabled = false;
760
- saveProfileBtn.classList.remove('btn-disabled', 'bg-red-600');
761
- }, 2000);
762
- }
763
- }
764
-
765
- // --- AI Feature Handlers ---
766
-
767
- async function handleSuggestBio() {
768
- const agentRole = profileDisplayNameInput.value.trim();
769
- if (!agentRole) {
770
- profileBioInput.placeholder = "Please enter a Display Name first, then click ✨ Suggest.";
771
- return;
772
- }
773
- const userQuery = `Based on the following name or role, write a short, professional bio (1-2 sentences). Role: "${agentRole}" Return ONLY the bio text, with no preamble or quotes.`;
774
- const payload = {
775
- contents: [{ parts: [{ text: userQuery }] }],
776
- systemInstruction: { parts: [{ text: "You are a professional profile writer. You return only the bio text." }] }
777
- };
778
- showLoading("Generating bio...");
779
- try {
780
- const generatedText = await callGeminiApi(payload);
781
- const cleanText = generatedText.replace(/["*]/g, "").trim();
782
- profileBioInput.value = cleanText;
783
- } catch (error) {
784
- console.error(error);
785
- profileBioInput.value = `Error: ${error.message}`;
786
- } finally {
787
- hideLoading();
788
- }
789
- }
790
-
791
- async function handleTestAgentSend(e) {
792
- e.preventDefault();
793
- const userMessage = testAgentInput.value.trim();
794
- if (!userMessage) return;
795
- testAgentInput.value = '';
796
- renderTestChatMessage('user', userMessage);
797
- setChatTyping(true);
798
- testChatHistory.push({ role: "user", parts: [{ text: userMessage }] });
799
- let finalSystemInstructions = appState.instructions || "You are a helpful assistant.";
800
- let payloadTools = [];
801
- if (appState.knowledge.urls.length > 0 && appState.knowledge.prioritize) {
802
- finalSystemInstructions += `\n\nCRITICAL KNOWLEDGE: You MUST use the provided Google Search tool to find information to answer the user's query. You must prioritize information from these specific websites: ${appState.knowledge.urls.join(', ')}. If the answer cannot be found in those sources, you should state that.`;
803
- payloadTools.push({ "google_search": {} });
804
- }
805
- const payload = {
806
- contents: testChatHistory,
807
- systemInstruction: { parts: [{ text: finalSystemInstructions }] }
808
- };
809
- if (payloadTools.length > 0) {
810
- payload.tools = payloadTools;
811
- }
812
- try {
813
- const modelResponse = await callGeminiApi(payload);
814
- testChatHistory.push({ role: "model", parts: [{ text: modelResponse }] });
815
- setChatTyping(false);
816
- renderTestChatMessage('model', modelResponse);
817
- } catch (error) {
818
- console.error("Test Agent Chat Error:", error);
819
- setChatTyping(false);
820
- renderTestChatMessage('model', `Sorry, an error occurred: ${error.message}`);
821
- }
822
- }
823
-
824
- async function handleImageGenTest(e) {
825
- e.preventDefault();
826
- const userPrompt = imageGenInput.value.trim();
827
- if (!userPrompt) return;
828
-
829
- imageGenResultContainer.classList.add('loading');
830
- imageGenError.textContent = '';
831
- imageGenError.classList.add('hidden');
832
- imageGenResult.classList.add('hidden');
833
-
834
- try {
835
- const imageUrl = await callImagenApi(userPrompt);
836
- imageGenResult.src = imageUrl;
837
- imageGenResult.alt = userPrompt;
838
- imageGenResult.classList.remove('hidden');
839
- } catch (error) {
840
- console.error("Image Gen Test Error:", error);
841
- imageGenError.textContent = `Error: ${error.message}`;
842
- imageGenError.classList.remove('hidden');
843
- } finally {
844
- imageGenResultContainer.classList.remove('loading');
845
- }
846
- }
847
-
848
- // NEW: Handle TTS for instructions
849
- async function handleListenInstructions() {
850
- const text = agentInstructionsInput.value.trim();
851
- if (!text) return;
852
-
853
- const icon = listenInstructionsBtn.querySelector('svg');
854
- listenInstructionsBtn.disabled = true;
855
- icon.classList.add('spinner');
856
-
857
- try {
858
- const { audioData, sampleRate } = await callTtsApi(text);
859
- const pcmBuffer = base64ToArrayBuffer(audioData);
860
- const wavBlob = pcmToWav(pcmBuffer, sampleRate);
861
- const audioUrl = URL.createObjectURL(wavBlob);
862
-
863
- ttsPlayer.src = audioUrl;
864
- ttsPlayer.play();
865
-
866
- } catch (error) {
867
- console.error("TTS Error:", error);
868
- } finally {
869
- listenInstructionsBtn.disabled = false;
870
- icon.classList.remove('spinner');
871
- }
872
- }
873
-
874
- // NEW: Handle Code Interpreter Test
875
- async function handleCodeInterpreterTest() {
876
- codeTestResultContainer.classList.add('loading');
877
- codeTestSpinner.style.display = 'flex';
878
- codeTestResult.style.display = 'none';
879
-
880
- const agentRole = appState.agentName.trim() || "a helpful assistant";
881
- const userQuery = `You are a code interpreter. Your agent role is "${agentRole}".
882
- Generate a simple Python code snippet that demonstrates this role.
883
- Return ONLY the Python code, inside \`\`\`python ... \`\`\` tags.`;
884
- const payload = {
885
- contents: [{ parts: [{ text: userQuery }] }],
886
- systemInstruction: { parts: [{ text: "You are a helpful code generation assistant. You only return code." }] }
887
- };
888
-
889
- try {
890
- const generatedText = await callGeminiApi(payload);
891
- // Clean up markdown fences
892
- const cleanCode = generatedText.replace(/^```python\n|```$/g, "").trim();
893
- codeTestResult.textContent = cleanCode;
894
- } catch (error) {
895
- console.error("Code Gen Test Error:", error);
896
- codeTestResult.textContent = `# Error generating code: ${error.message}`;
897
- } finally {
898
- codeTestResultContainer.classList.remove('loading');
899
- codeTestSpinner.style.display = 'none';
900
- codeTestResult.style.display = 'block';
901
- }
902
- }
903
-
904
-
905
- /**
906
- * Attaches all application event listeners.
907
- */
908
- function attachEventListeners() {
909
- // Agent Config Listeners
910
- agentNameInput.addEventListener('input', (e) => appState.agentName = e.target.value);
911
- agentDescriptionInput.addEventListener('input', (e) => appState.description = e.target.value);
912
- agentInstructionsInput.addEventListener('input', (e) => appState.instructions = e.target.value);
913
- listenInstructionsBtn.addEventListener('click', handleListenInstructions); // NEW
914
- prioritizeKnowledgeToggle.addEventListener('change', (e) => appState.knowledge.prioritize = e.target.checked);
915
-
916
- // Code Interpreter Toggle
917
- codeInterpreterToggle.addEventListener('change', (e) => { // MODIFIED
918
- appState.capabilities.codeInterpreter = e.target.checked;
919
- if (e.target.checked) {
920
- testCodeInterpreterBtn.disabled = false;
921
- testCodeInterpreterBtn.classList.remove('btn-disabled', 'text-slate-400');
922
- testCodeInterpreterBtn.classList.add('text-indigo-600', 'hover:bg-indigo-50');
923
- } else {
924
- testCodeInterpreterBtn.disabled = true;
925
- testCodeInterpreterBtn.classList.add('btn-disabled', 'text-slate-400');
926
- testCodeInterpreterBtn.classList.remove('text-indigo-600', 'hover:bg-indigo-50');
927
- }
928
- });
929
-
930
- // Image Gen Toggle
931
- imageGeneratorToggle.addEventListener('change', (e) => {
932
- appState.capabilities.imageGenerator = e.target.checked;
933
- if (e.target.checked) {
934
- testImageGenBtn.disabled = false;
935
- testImageGenBtn.classList.remove('btn-disabled', 'text-slate-400');
936
- testImageGenBtn.classList.add('text-indigo-600', 'hover:bg-indigo-50');
937
- } else {
938
- testImageGenBtn.disabled = true;
939
- testImageGenBtn.classList.add('btn-disabled', 'text-slate-400');
940
- testImageGenBtn.classList.remove('text-indigo-600', 'hover:bg-indigo-50');
941
- }
942
- });
943
-
944
- // ... (rest of listeners: model config, knowledge, prompts - unchanged) ...
945
- modelConfigContainer.addEventListener('change', (e) => {
946
- if (e.target.name === 'model-selection') {
947
- appState.modelConfig.selectedModel = e.target.value;
948
- renderModelConfig();
949
- }
950
- });
951
- modelConfigContainer.addEventListener('input', (e) => {
952
- if (e.target.id === 'api-key-qhy-pro-input') appState.modelConfig.apiKeys['qhy-pro'] = e.target.value;
953
- else if (e.target.id === 'api-key-gemini-2.5-input') appState.modelConfig.apiKeys['gemini-2.5'] = e.target.value;
954
- });
955
- addKnowledgeUrlBtn.addEventListener('click', () => {
956
- const url = knowledgeUrlInput.value.trim();
957
- if (url) {
958
- try {
959
- new URL(url);
960
- if (!appState.knowledge.urls.includes(url)) {
961
- appState.knowledge.urls.push(url);
962
- renderKnowledgeList();
963
- knowledgeUrlInput.value = '';
964
- }
965
- } catch (error) { console.warn("Invalid URL."); }
966
- }
967
- });
968
- knowledgeListContainer.addEventListener('click', (e) => {
969
- if (e.target.closest('button')) {
970
- const index = parseInt(e.target.closest('button').dataset.index, 10);
971
- appState.knowledge.urls.splice(index, 1);
972
- renderKnowledgeList();
973
- }
974
- });
975
- promptListContainer.addEventListener('click', (e) => {
976
- const deleteBtn = e.target.closest('.delete-prompt-btn');
977
- if (deleteBtn) {
978
- const index = parseInt(deleteBtn.dataset.index, 10);
979
- appState.prompts.splice(index, 1);
980
- renderPromptList();
981
- }
982
- const editBtn = e.target.closest('.edit-prompt-btn');
983
- if (editBtn) {
984
- const index = parseInt(editBtn.dataset.index, 10);
985
- showPromptModal('edit', index);
986
- }
987
- });
988
- addPromptBtn.addEventListener('click', () => showPromptModal('add'));
989
- promptModalCloseBtn.addEventListener('click', () => hideModal(promptModal));
990
- promptModalCancelBtn.addEventListener('click', () => hideModal(promptModal));
991
- promptModalForm.addEventListener('submit', (e) => {
992
- e.preventDefault();
993
- const title = promptModalTitleInput.value.trim();
994
- const message = promptModalMessageInput.value.trim();
995
- const editIndex = parseInt(promptModalEditIndex.value, 10);
996
- if (title && message) {
997
- if (editIndex > -1) appState.prompts[editIndex] = { title, message };
998
- else appState.prompts.push({ title, message });
999
- renderPromptList();
1000
- hideModal(promptModal);
1001
- }
1002
- });
1003
-
1004
- // Gemini API Buttons (Agent Config)
1005
- suggestNameBtn.addEventListener('click', async () => {
1006
- const agentDescription = appState.description.trim();
1007
- if (!agentDescription) { console.warn("Please enter a Description first."); return; }
1008
- const userQuery = `Based on the agent description: "${agentDescription}", suggest one creative agent name. Return ONLY the name.`;
1009
- const payload = { contents: [{ parts: [{ text: userQuery }] }], systemInstruction: { parts: [{ text: "You are an expert marketer." }] } };
1010
- showLoading("Suggesting name...");
1011
- try {
1012
- const generatedText = await callGeminiApi(payload);
1013
- const cleanText = generatedText.replace(/["*]/g, "").trim();
1014
- agentNameInput.value = cleanText;
1015
- appState.agentName = cleanText;
1016
- } catch (error) { console.error(error); } finally { hideLoading(); }
1017
- });
1018
- suggestDescriptionBtn.addEventListener('click', async () => {
1019
- const agentRole = appState.agentName.trim();
1020
- if (!agentRole) { console.warn("Please enter an Agent Name first."); return; }
1021
- const userQuery = `Based on the agent name: "${agentRole}", write a concise one-sentence description. Return ONLY the description.`;
1022
- const payload = { contents: [{ parts: [{ text: userQuery }] }], systemInstruction: { parts: [{ text: "You are a concise technical writer." }] } };
1023
- showLoading("Suggesting description...");
1024
- try {
1025
- const generatedText = await callGeminiApi(payload);
1026
- const cleanText = generatedText.replace(/["*]/g, "").trim();
1027
- agentDescriptionInput.value = cleanText;
1028
- appState.description = cleanText;
1029
- } catch (error) { console.error(error); } finally { hideLoading(); }
1030
- });
1031
- generateInstructionsBtn.addEventListener('click', async () => {
1032
- const agentRole = appState.agentName.trim();
1033
- if (!agentRole) { console.warn("Please enter an Agent Name first."); return; }
1034
- const userQuery = `Write a detailed system prompt for an AI agent with the role: "${agentRole}". Include a persona, rules, and guidelines. Return ONLY the prompt text.`;
1035
- const payload = { contents: [{ parts: [{ text: userQuery }] }], systemInstruction: { parts: [{ text: "You are a world-class prompt engineer." }] } };
1036
- showLoading("Generating instructions...");
1037
- try {
1038
- const generatedText = await callGeminiApi(payload);
1039
- const cleanText = generatedText.replace(/^(```(markdown|text)?\n)|(```)$/g, "").trim();
1040
- agentInstructionsInput.value = cleanText;
1041
- appState.instructions = cleanText;
1042
- } catch (error) { console.error(error); } finally { hideLoading(); }
1043
- });
1044
- suggestPromptsBtn.addEventListener('click', async () => {
1045
- const { agentName, description } = appState;
1046
- if (!agentName || !description) { console.warn("Please enter an Agent Name and Description first."); return; }
1047
- const userQuery = `Generate 3 unique suggested prompts for an agent named "${agentName}" described as "${description}". Return ONLY the JSON array.`;
1048
- const promptSchema = { type: "ARRAY", items: { type: "OBJECT", properties: { "title": { "type": "STRING" }, "message": { "type": "STRING" } }, required: ["title", "message"] } };
1049
- const payload = { contents: [{ parts: [{ text: userQuery }] }], systemInstruction: { parts: [{ text: "You generate example prompts as a JSON array." }] }, generationConfig: { responseMimeType: "application/json", responseSchema: promptSchema } };
1050
- showLoading("Suggesting prompts...");
1051
- try {
1052
- const jsonText = await callGeminiApi(payload);
1053
- const newPrompts = JSON.parse(jsonText);
1054
- if (Array.isArray(newPrompts) && newPrompts.length > 0) {
1055
- newPrompts.forEach(p => { if (!appState.prompts.some(ap => ap.title === p.title)) appState.prompts.push(p); });
1056
- renderPromptList();
1057
- }
1058
- } catch (error) { console.error(error); } finally { hideLoading(); }
1059
- });
1060
-
1061
- // Save/Delete Agent
1062
- deleteAgentBtn.addEventListener('click', () => showModal(deleteAgentModal));
1063
- deleteAgentCancelBtn.addEventListener('click', () => hideModal(deleteAgentModal));
1064
- deleteAgentConfirmBtn.addEventListener('click', async () => {
1065
- try {
1066
- if (agentConfigDocRef) await deleteDoc(agentConfigDocRef);
1067
- const { bio, displayName, photoURL } = appState;
1068
- appState = { ...defaultAppState, bio, displayName, photoURL };
1069
- initializeAppUI();
1070
- } catch (error) { console.error("Error deleting agent:", error); } finally { hideModal(deleteAgentModal); }
1071
- });
1072
- saveChangesBtn.addEventListener('click', async () => {
1073
- if (!agentConfigDocRef) return;
1074
- saveChangesBtn.textContent = "Saving...";
1075
- saveChangesBtn.disabled = true;
1076
- saveChangesBtn.classList.add('btn-disabled');
1077
- try {
1078
- await setDoc(agentConfigDocRef, appState);
1079
- saveChangesBtn.textContent = "Changes Saved!";
1080
- saveChangesBtn.classList.add('bg-green-600');
1081
- setTimeout(() => {
1082
- saveChangesBtn.textContent = "Save Changes";
1083
- saveChangesBtn.disabled = false;
1084
- saveChangesBtn.classList.remove('btn-disabled', 'bg-green-600');
1085
- }, 2000);
1086
- } catch (error) {
1087
- console.error("Error saving to Firestore:", error);
1088
- saveChangesBtn.textContent = "Save Failed";
1089
- saveChangesBtn.classList.add('bg-red-600');
1090
- setTimeout(() => {
1091
- saveChangesBtn.textContent = "Save Changes";
1092
- saveChangesBtn.disabled = false;
1093
- saveChangesBtn.classList.remove('btn-disabled', 'bg-red-600');
1094
- }, 2000);
1095
- }
1096
- });
1097
-
1098
- // New UI Listeners
1099
- menuBtn.addEventListener('click', () => showModal(geminiSettingsModal));
1100
- geminiSettingsCloseBtn.addEventListener('click', () => hideModal(geminiSettingsModal));
1101
- geminiSettingsSaveBtn.addEventListener('click', () => {
1102
- console.log("Gemini settings saved (placeholder).");
1103
- hideModal(geminiSettingsModal);
1104
- });
1105
- profileToggleBtn.addEventListener('click', toggleProfileSidebar);
1106
- profileSidebarCloseBtn.addEventListener('click', toggleProfileSidebar);
1107
- sidebarOverlay.addEventListener('click', toggleProfileSidebar);
1108
-
1109
- // Profile Sidebar Listeners
1110
- profileImgUrlInput.addEventListener('input', (e) => updateImagePreviews(e.target.value));
1111
- suggestBioBtn.addEventListener('click', handleSuggestBio);
1112
- saveProfileBtn.addEventListener('click', handleSaveProfile);
1113
- googleSigninBtn.addEventListener('click', signInWithGoogle);
1114
- githubSigninBtn.addEventListener('click', signInWithGitHub);
1115
- signOutBtn.addEventListener('click', handleSignOut);
1116
-
1117
- // Test Agent Listeners
1118
- testAgentBtn.addEventListener('click', openTestAgentModal);
1119
- testAgentCloseBtn.addEventListener('click', () => hideModal(testAgentModal));
1120
- testAgentForm.addEventListener('submit', handleTestAgentSend);
1121
-
1122
- // Image Gen Test Listeners
1123
- testImageGenBtn.addEventListener('click', openImageGenModal);
1124
- imageGenCloseBtn.addEventListener('click', () => hideModal(imageGenModal));
1125
- imageGenForm.addEventListener('submit', handleImageGenTest);
1126
-
1127
- // NEW: Code Test Listeners
1128
- testCodeInterpreterBtn.addEventListener('click', openCodeTestModal);
1129
- codeTestCloseBtn.addEventListener('click', () => hideModal(codeTestModal));
1130
- codeTestGenerateBtn.addEventListener('click', handleCodeInterpreterTest);
1131
- }
1132
-
1133
- // --- App Entry Point ---
1134
- document.addEventListener('DOMContentLoaded', () => {
1135
- selectDOMElements();
1136
- attachEventListeners();
1137
-
1138
- const firebaseConfig = JSON.parse(typeof __firebase_config !== 'undefined' ? __firebase_config : '{}');
1139
- const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id';
1140
-
1141
- try {
1142
- app = initializeApp(firebaseConfig);
1143
- db = getFirestore(app);
1144
- auth = getAuth(app);
1145
- setLogLevel('Debug');
1146
- console.log("Firebase Initialized. Waiting for auth...");
1147
-
1148
- onAuthStateChanged(auth, async (user) => {
1149
- if (user) {
1150
- userId = user.uid;
1151
- console.log(`User authenticated. UserID: ${userId}, Anonymous: ${user.isAnonymous}`);
1152
- agentConfigDocRef = doc(db, 'artifacts', appId, 'users', userId, 'qhySyncAgent', 'config');
1153
-
1154
- await loadAgentConfig();
1155
- initializeAppUI(); // Now this will update the UI with data
1156
-
1157
- } else {
1158
- console.log("User signed out. Attempting anonymous sign-in...");
1159
- try {
1160
- if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) {
1161
- await signInWithCustomToken(auth, __initial_auth_token);
1162
- } else {
1163
- await signInAnonymously(auth);
1164
- }
1165
- } catch (error) {
1166
- console.error("Error during anonymous sign-in:", error);
1167
- }
1168
- }
1169
- });
1170
- } catch (error) {
1171
- console.error("Failed to initialize Firebase:", error);
1172
- document.body.innerHTML = `<div class="p-8 text-center text-red-600"><h1 class="text-2xl font-bold">Application Error</h1><p class="mt-2">Could not connect to services.</p></div>`;
1173
- }
1174
- });
1175
-
1176
- </script>
1177
  </head>
1178
  <body class="bg-slate-100 text-slate-900 antialiased">
1179
-
1180
- <!-- NEW: Hidden Audio Player for TTS -->
1181
- <audio id="tts-player" class="hidden"></audio>
1182
-
1183
- <!-- Header -->
1184
- <header class="bg-white shadow-sm sticky top-0 z-30">
1185
- <div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
1186
- <div class="flex justify-between items-center h-16">
1187
- <button id="menu-btn" class="p-2 rounded-full text-slate-500 hover:text-slate-800 hover:bg-slate-100">
1188
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>
1189
- </button>
1190
- <h1 class="text-xl font-semibold text-slate-800 hidden sm:block">
1191
- Agent Configuration
1192
- </h1>
1193
- <button id="profile-toggle-btn" class="p-1.5 rounded-full text-slate-500 hover:text-slate-800 hover:bg-slate-100">
1194
- <svg id="profile-header-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>
1195
- <img id="profile-header-img" src="" alt="Profile" class="w-6 h-6 rounded-full hidden object-cover">
1196
- </button>
1197
- </div>
1198
- </div>
1199
- </header>
1200
-
1201
- <!-- Main Content Area -->
1202
- <main class="max-w-3xl mx-auto p-4 sm:p-6 lg:p-8 mt-6">
1203
- <div class="space-y-8">
1204
  <!-- Card: Agent Name -->
1205
  <div class="bg-white shadow-md rounded-lg overflow-hidden">
1206
  <div class="px-6 py-5 flex justify-between items-center">
@@ -1344,13 +236,8 @@ Return ONLY the Python code, inside \`\`\`python ... \`\`\` tags.`;
1344
  </div>
1345
  </div>
1346
  </main>
1347
-
1348
- <!-- Sidebar Overlay -->
1349
- <div id="sidebar-overlay" class="fixed inset-0 z-30 bg-black bg-opacity-50 opacity-0 pointer-events-none"></div>
1350
-
1351
- <!-- Profile Sidebar -->
1352
- <aside id="profile-sidebar" class="fixed top-0 right-0 z-40 w-full max-w-sm h-full bg-white shadow-xl overflow-y-auto">
1353
- <div class="flex flex-col h-full">
1354
  <!-- Sidebar Header -->
1355
  <div class="flex items-center justify-between px-6 py-5 border-b border-slate-200">
1356
  <h3 class="text-lg font-semibold text-slate-900">Profile & Settings</h3>
@@ -1416,134 +303,5 @@ Return ONLY the Python code, inside \`\`\`python ... \`\`\` tags.`;
1416
  </div>
1417
  </div>
1418
  </aside>
1419
-
1420
- <!-- Modals -->
1421
- <!-- API Loading -->
1422
- <div id="loading-modal" class="modal fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-75 backdrop-blur-sm opacity-0 pointer-events-none">
1423
- <div class="modal-content bg-white w-full max-w-xs rounded-lg shadow-xl p-6 text-center">
1424
- <svg class="animate-spin h-12 w-12 text-indigo-600 mx-auto" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
1425
- <p id="loading-message" class="text-sm font-medium text-slate-700 mt-4">Generating...</p>
1426
- </div>
1427
- </div>
1428
- <!-- Add/Edit Prompt -->
1429
- <div id="prompt-modal" class="modal fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 backdrop-blur-sm opacity-0 pointer-events-none">
1430
- <div class="modal-content bg-white w-full max-w-lg rounded-lg shadow-xl p-6 transform -translate-y-10">
1431
- <div class="flex justify-between items-center mb-4"><h3 id="prompt-modal-title" class="text-lg font-semibold text-slate-900">Add New Prompt</h3><button id="prompt-modal-close-btn" class="p-1 rounded-full text-slate-400 hover:bg-slate-100"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button></div>
1432
- <form id="prompt-modal-form"><input type="hidden" id="prompt-modal-edit-index"><div class="space-y-4"><div><label for="prompt-modal-title-input" class="block text-sm font-medium text-slate-700">Title</label><input type="text" id="prompt-modal-title-input" placeholder="e.g., Explain a concept" class="mt-1 block w-full rounded-md border-slate-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"></div><div><label for="prompt-modal-message-input" class="block text-sm font-medium text-slate-700">Message</label><textarea id="prompt-modal-message-input" rows="3" class="mt-1 block w-full rounded-md border-slate-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" placeholder="e.g., &quot;Explain quantum...&quot;"></textarea></div></div><div class="mt-6 flex justify-end space-x-3"><button id="prompt-modal-cancel-btn" type="button" class="rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50">Cancel</button><button id="prompt-modal-save-btn" type="submit" class="inline-flex justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700">Save Prompt</button></div></form>
1433
- </div>
1434
- </div>
1435
- <!-- Confirm Delete Agent -->
1436
- <div id="delete-agent-modal" class="modal fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 backdrop-blur-sm opacity-0 pointer-events-none">
1437
- <div class="modal-content bg-white w-full max-w-md rounded-lg shadow-xl p-6 transform -translate-y-10">
1438
- <div class="flex items-start"><div class="flex-shrink-0 h-12 w-12 flex items-center justify-center rounded-full bg-red-100 sm:mx-0"><svg class="h-6 w-6 text-red-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" /></svg></div><div class="ml-4 text-left"><h3 class="text-lg font-semibold text-slate-900">Delete Agent</h3><div class="mt-2"><p class="text-sm text-slate-500">Are you sure you want to delete this agent? This action cannot be undone.</p></div></div></div>
1439
- <div class="mt-6 flex justify-end space-x-3"><button id="delete-agent-cancel-btn" type="button" class="rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50">Cancel</button><button id="delete-agent-confirm-btn" type="button" class="inline-flex justify-center rounded-md border border-transparent bg-red-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-red-700">Delete</button></div>
1440
- </div>
1441
- </div>
1442
-
1443
- <!-- Gemini Settings Modal -->
1444
- <div id="gemini-settings-modal" class="modal fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 backdrop-blur-sm opacity-0 pointer-events-none">
1445
- <div class="modal-content bg-white w-full max-w-lg rounded-lg shadow-xl p-6 transform -translate-y-10">
1446
- <div class="flex justify-between items-center mb-4"><h3 class="text-lg font-semibold text-slate-900">Gemini Settings</h3><button id="gemini-settings-close-btn" class="p-1 rounded-full text-slate-400 hover:bg-slate-100"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button></div>
1447
- <div class="space-y-4">
1448
- <p class="text-sm text-slate-600">Configure advanced settings for the Gemini API features (e.g., suggestion generation).</p>
1449
- <div>
1450
- <label for="gemini-safety-level" class="block text-sm font-medium text-slate-700">Suggestion Safety Level</label>
1451
- <select id="gemini-safety-level" class="mt-1 block w-full rounded-md border-slate-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
1452
- <option value="BLOCK_MEDIUM_AND_ABOVE">Block Medium and Above (Default)</option>
1453
- <option value="BLOCK_ONLY_HIGH">Block Only High</option>
1454
- <option value="BLOCK_NONE">Block None</option>
1455
- </select>
1456
- </div>
1457
- </div>
1458
- <div class="mt-6 flex justify-end">
1459
- <button id="gemini-settings-save-btn" type="button" class="inline-flex justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700">Save Settings</button>
1460
- </div>
1461
- </div>
1462
- </div>
1463
-
1464
- <!-- Test Agent Modal -->
1465
- <div id="test-agent-modal" class="modal fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 backdrop-blur-sm opacity-0 pointer-events-none">
1466
- <div class="modal-content bg-white w-full max-w-lg rounded-lg shadow-xl flex flex-col transform -translate-y-10" style="height: 70vh; max-height: 500px;">
1467
- <div class="flex justify-between items-center p-4 border-b border-slate-200">
1468
- <h3 class="text-lg font-semibold text-slate-900">Test Agent</h3>
1469
- <button id="test-agent-close-btn" class="p-1 rounded-full text-slate-400 hover:bg-slate-100">
1470
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
1471
- </button>
1472
- </div>
1473
- <div id="test-agent-history" class="flex-1 p-4 space-y-4 overflow-y-auto bg-slate-50">
1474
- <!-- Chat messages injected here -->
1475
- </div>
1476
- <form id="test-agent-form" class="p-4 border-t border-slate-200">
1477
- <div class="flex items-center space-x-2">
1478
- <input type="text" id="test-agent-input" placeholder="Message your agent..." class="block w-full rounded-md border-slate-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
1479
- <button type="submit" class="inline-flex items-center justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700">
1480
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>
1481
- </button>
1482
- </div>
1483
- </form>
1484
- </div>
1485
- </div>
1486
-
1487
- <!-- Image Gen Test Modal -->
1488
- <div id="image-gen-modal" class="modal fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 backdrop-blur-sm opacity-0 pointer-events-none">
1489
- <div class="modal-content bg-white w-full max-w-lg rounded-lg shadow-xl flex flex-col transform -translate-y-10">
1490
- <div class="flex justify-between items-center p-4 border-b border-slate-200">
1491
- <h3 class="text-lg font-semibold text-slate-900">✨ Test Image Generator</h3>
1492
- <button id="image-gen-close-btn" class="p-1 rounded-full text-slate-400 hover:bg-slate-100">
1493
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
1494
- </button>
1495
- </div>
1496
- <div id="image-gen-result-container" class="flex-1 p-4 flex items-center justify-center bg-slate-50 min-h-[300px]">
1497
- <div id="image-gen-spinner" class="flex flex-col items-center justify-center text-slate-500">
1498
- <svg class="animate-spin h-12 w-12 text-indigo-600 mx-auto" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
1499
- <p class="text-sm font-medium mt-2">Generating image...</p>
1500
- </div>
1501
- <img id="image-gen-result" src="" alt="" class="max-w-full max-h-full h-auto w-auto object-contain rounded-lg shadow-md hidden">
1502
- <p id="image-gen-error" class="text-sm text-red-600 p-4 bg-red-50 rounded-md hidden"></p>
1503
- </div>
1504
- <form id="image-gen-form" class="p-4 border-t border-slate-200">
1505
- <div class="flex items-center space-x-2">
1506
- <input type="text" id="image-gen-input" placeholder="e.g., A red panda in a space helmet" class="block w-full rounded-md border-slate-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
1507
- <button type="submit" class="inline-flex items-center justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700">
1508
- Generate
1509
- </button>
1510
- </div>
1511
- </form>
1512
- </div>
1513
- </div>
1514
-
1515
- <!-- NEW: Code Interpreter Test Modal -->
1516
- <div id="code-test-modal" class="modal fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 backdrop-blur-sm opacity-0 pointer-events-none">
1517
- <div class="modal-content bg-white w-full max-w-lg rounded-lg shadow-xl flex flex-col transform -translate-y-10">
1518
- <!-- Modal Header -->
1519
- <div class="flex justify-between items-center p-4 border-b border-slate-200">
1520
- <h3 class="text-lg font-semibold text-slate-900">✨ Test Code Interpreter</h3>
1521
- <button id="code-test-close-btn" class="p-1 rounded-full text-slate-400 hover:bg-slate-100">
1522
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
1523
- </button>
1524
- </div>
1525
-
1526
- <!-- Code Result Area -->
1527
- <div id="code-test-result-container" class="flex-1 p-4 flex items-center justify-center bg-slate-900 min-h-[300px]">
1528
- <!-- Spinner -->
1529
- <div id="code-test-spinner" class="flex flex-col items-center justify-center text-slate-400" style="display: none;">
1530
- <svg class="animate-spin h-12 w-12 text-indigo-500 mx-auto" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
1531
- <p class="text-sm font-medium mt-2">Generating code snippet...</p>
1532
- </div>
1533
- <!-- Result -->
1534
- <pre id="code-test-result" class="w-full h-full text-sm text-slate-100 overflow-auto p-4 bg-transparent rounded-md"></pre>
1535
- </div>
1536
-
1537
- <!-- Generate Button -->
1538
- <div class="p-4 border-t border-slate-200 flex justify-end">
1539
- <button id="code-test-generate-btn" type="button" class="inline-flex items-center justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700">
1540
- Generate Example
1541
- </button>
1542
- </div>
1543
- </div>
1544
- </div>
1545
-
1546
  </body>
1547
  </html>
1548
-
1549
-
 
87
  <script src="components/sidebar.js"></script>
88
  <script src="components/modals.js"></script>
89
  <script src="script.js"></script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  </head>
91
  <body class="bg-slate-100 text-slate-900 antialiased">
92
+ <custom-navbar></custom-navbar>
93
+ <!-- Main Content Area -->
94
+ <main class="max-w-3xl mx-auto p-4 sm:p-6 lg:p-8 mt-6" id="main-content">
95
+ <div class="space-y-8">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  <!-- Card: Agent Name -->
97
  <div class="bg-white shadow-md rounded-lg overflow-hidden">
98
  <div class="px-6 py-5 flex justify-between items-center">
 
236
  </div>
237
  </div>
238
  </main>
239
+ <custom-sidebar></custom-sidebar>
240
+ <div class="flex flex-col h-full">
 
 
 
 
 
241
  <!-- Sidebar Header -->
242
  <div class="flex items-center justify-between px-6 py-5 border-b border-slate-200">
243
  <h3 class="text-lg font-semibold text-slate-900">Profile & Settings</h3>
 
303
  </div>
304
  </div>
305
  </aside>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
  </body>
307
  </html>
 
 
script.js CHANGED
@@ -1,3 +1,7 @@
 
 
 
 
1
 
2
  // Main application logic
3
  document.addEventListener('DOMContentLoaded', async () => {
@@ -10,51 +14,942 @@ document.addEventListener('DOMContentLoaded', async () => {
10
  const app = initializeApp(firebaseConfig);
11
  const db = getFirestore(app);
12
  const auth = getAuth(app);
13
- setLogLevel('Debug');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
- onAuthStateChanged(auth, async (user) => {
16
- if (user) {
17
- console.log(`User authenticated. UserID: ${user.uid}, Anonymous: ${user.isAnonymous}`);
18
- const agentConfigDocRef = doc(db, 'artifacts', appId, 'users', user.uid, 'qhySyncAgent', 'config');
19
- // Load and initialize UI here
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  } else {
21
- console.log("User signed out. Attempting anonymous sign-in...");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  try {
23
- if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) {
24
- await signInWithCustomToken(auth, __initial_auth_token);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  } else {
26
- await signInAnonymously(auth);
27
  }
 
28
  } catch (error) {
29
- console.error("Error during anonymous sign-in:", error);
 
 
 
 
 
 
30
  }
31
  }
32
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
- } catch (error) {
35
- console.error("Failed to initialize Firebase:", error);
36
- document.body.innerHTML = `<div class="p-8 text-center text-red-600"><h1 class="text-2xl font-bold">Application Error</h1><p class="mt-2">Could not connect to services.</p></div>`;
37
- }
38
- });
39
- // API call functions
40
- async function callGeminiApi(payload) {
41
- try {
42
- // API implementation
43
- console.log('Calling Gemini API with payload:', payload);
44
- // ...
45
- } catch (error) {
46
- console.error('API Error:', error);
47
- throw error;
48
- }
49
- }
50
-
51
- // Other utility functions
52
- function showModal(modalEl) {
53
- modalEl.classList.remove('opacity-0', 'pointer-events-none');
54
- modalEl.querySelector('.modal-content').classList.remove('-translate-y-10');
55
- }
56
-
57
- function hideModal(modalEl) {
58
- modalEl.classList.add('opacity-0', 'pointer-events-none');
59
- modalEl.querySelector('.modal-content').classList.add('-translate-y-10');
60
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Import Firebase functions
2
+ import { initializeApp } from 'https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js';
3
+ import { getAuth, onAuthStateChanged, signInAnonymously, signInWithCustomToken, signOut, GoogleAuthProvider, GithubAuthProvider, signInWithPopup, updateProfile } from 'https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js';
4
+ import { getFirestore, doc, getDoc, setDoc, deleteDoc } from 'https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js';
5
 
6
  // Main application logic
7
  document.addEventListener('DOMContentLoaded', async () => {
 
14
  const app = initializeApp(firebaseConfig);
15
  const db = getFirestore(app);
16
  const auth = getAuth(app);
17
+
18
+ // Set log level for debugging
19
+ // Note: setLogLevel is not available in the modular SDK, using console.log instead
20
+ console.log("Firebase Initialized. Waiting for auth...");
21
+
22
+ // --- App State & Firebase Globals ---
23
+ let userId, agentConfigDocRef;
24
+ let isProfileSidebarOpen = false;
25
+ let testChatHistory = []; // State for test chat
26
+
27
+ const defaultAppState = {
28
+ agentName: "Creative Writer",
29
+ description: "A helpful assistant for Python code.",
30
+ instructions: "You are a helpful AI assistant...",
31
+ modelConfig: {
32
+ selectedModel: 'llama3-70b',
33
+ apiKeys: { 'qhy-pro': '', 'gemini-2.5': '' }
34
+ },
35
+ knowledge: {
36
+ prioritize: false,
37
+ urls: ['https://qhy.sync/docs/main']
38
+ },
39
+ capabilities: {
40
+ codeInterpreter: false,
41
+ imageGenerator: true
42
+ },
43
+ prompts: [
44
+ { title: "Explain a concept", message: "\"Explain quantum computing...\"" },
45
+ { title: "Write some code", message: "\"Write a Python function...\"" }
46
+ ],
47
+ bio: "",
48
+ displayName: "",
49
+ photoURL: ""
50
+ };
51
+
52
+ let appState = { ...defaultAppState };
53
+
54
+ // --- DOM Selectors ---
55
+ let agentNameInput, agentDescriptionInput, agentInstructionsInput,
56
+ listenInstructionsBtn, ttsPlayer,
57
+ modelConfigContainer, knowledgeListContainer, knowledgeEmptyState,
58
+ knowledgeUrlInput, addKnowledgeUrlBtn, prioritizeKnowledgeToggle,
59
+ codeInterpreterToggle, testCodeInterpreterBtn,
60
+ imageGeneratorToggle, testImageGenBtn,
61
+ promptListContainer, promptEmptyState, addPromptBtn,
62
+ saveChangesBtn, deleteAgentBtn, suggestNameBtn,
63
+ suggestDescriptionBtn, generateInstructionsBtn, suggestPromptsBtn,
64
+ loadingModal, loadingMessage, promptModal, promptModalTitle,
65
+ promptModalCloseBtn, promptModalCancelBtn, promptModalForm,
66
+ promptModalSaveBtn, promptModalEditIndex, promptModalTitleInput,
67
+ promptModalMessageInput, deleteAgentModal, deleteAgentCancelBtn,
68
+ deleteAgentConfirmBtn,
69
+ // UI Elements
70
+ menuBtn, profileToggleBtn, profileHeaderIcon, profileHeaderImg,
71
+ profileSidebar, sidebarOverlay, profileSidebarCloseBtn,
72
+ profileImgPreview, profileImgUrlInput, profileDisplayNameInput,
73
+ profileBioInput, suggestBioBtn, saveProfileBtn, googleSigninBtn,
74
+ githubSigninBtn, signOutBtn, socialLoginContainer, signOutContainer,
75
+ geminiSettingsModal, geminiSettingsCloseBtn, geminiSettingsSaveBtn,
76
+ // Test Agent Elements
77
+ testAgentBtn, testAgentModal, testAgentCloseBtn, testAgentHistory,
78
+ testAgentForm, testAgentInput,
79
+ // Image Gen Modal Elements
80
+ imageGenModal, imageGenCloseBtn, imageGenForm, imageGenInput,
81
+ imageGenResultContainer, imageGenSpinner, imageGenResult, imageGenError,
82
+ // NEW: Code Test Modal Elements
83
+ codeTestModal, codeTestCloseBtn, codeTestGenerateBtn,
84
+ codeTestResultContainer, codeTestSpinner, codeTestResult;
85
+
86
+ /**
87
+ * Assign all DOM elements to their variables.
88
+ */
89
+ function selectDOMElements() {
90
+ // Wait for custom elements to be fully loaded
91
+ const waitForElements = setInterval(() => {
92
+ // Try to get elements from main content and custom components
93
+ const mainContent = document.getElementById('main-content');
94
+ const sidebar = document.querySelector('custom-sidebar');
95
+ const navbar = document.querySelector('custom-navbar');
96
+ const modals = document.querySelector('custom-modals');
97
+
98
+ if (mainContent) {
99
+ // Existing Elements
100
+ agentNameInput = document.getElementById('agent-name');
101
+ agentDescriptionInput = document.getElementById('agent-description');
102
+ agentInstructionsInput = document.getElementById('agent-instructions');
103
+ listenInstructionsBtn = document.getElementById('listen-instructions-btn');
104
+ modelConfigContainer = document.getElementById('model-config-container');
105
+ knowledgeListContainer = document.getElementById('knowledge-list-container');
106
+ knowledgeEmptyState = document.getElementById('knowledge-empty-state');
107
+ knowledgeUrlInput = document.getElementById('knowledge-url-input');
108
+ addKnowledgeUrlBtn = document.getElementById('add-knowledge-url-btn');
109
+ prioritizeKnowledgeToggle = document.getElementById('prioritize-knowledge');
110
+ codeInterpreterToggle = document.getElementById('code-interpreter');
111
+ testCodeInterpreterBtn = document.getElementById('test-code-interpreter-btn');
112
+ imageGeneratorToggle = document.getElementById('image-generator');
113
+ testImageGenBtn = document.getElementById('test-image-gen-btn');
114
+ promptListContainer = document.getElementById('prompt-list-container');
115
+ promptEmptyState = document.getElementById('prompt-empty-state');
116
+ addPromptBtn = document.getElementById('add-prompt-btn');
117
+ saveChangesBtn = document.getElementById('save-changes-btn');
118
+ deleteAgentBtn = document.getElementById('delete-agent-btn');
119
+ suggestNameBtn = document.getElementById('suggest-name-btn');
120
+ suggestDescriptionBtn = document.getElementById('suggest-description-btn');
121
+ generateInstructionsBtn = document.getElementById('generate-instructions-btn');
122
+ suggestPromptsBtn = document.getElementById('suggest-prompts-btn');
123
+
124
+ // Test Agent Elements
125
+ testAgentBtn = document.getElementById('test-agent-btn');
126
+ testAgentHistory = document.getElementById('test-agent-history');
127
+ testAgentForm = document.getElementById('test-agent-form');
128
+ testAgentInput = document.getElementById('test-agent-input');
129
+ testAgentModal = document.getElementById('test-agent-modal');
130
+ testAgentCloseBtn = document.getElementById('test-agent-close-btn');
131
+
132
+ // Image Gen Modal Elements
133
+ imageGenModal = document.getElementById('image-gen-modal');
134
+ imageGenCloseBtn = document.getElementById('image-gen-close-btn');
135
+ imageGenForm = document.getElementById('image-gen-form');
136
+ imageGenInput = document.getElementById('image-gen-input');
137
+ imageGenResultContainer = document.getElementById('image-gen-result-container');
138
+ imageGenSpinner = document.getElementById('image-gen-spinner');
139
+ imageGenResult = document.getElementById('image-gen-result');
140
+ imageGenError = document.getElementById('image-gen-error');
141
+
142
+ // NEW: Code Test Modal Elements
143
+ codeTestModal = document.getElementById('code-test-modal');
144
+ codeTestCloseBtn = document.getElementById('code-test-close-btn');
145
+ codeTestGenerateBtn = document.getElementById('code-test-generate-btn');
146
+ codeTestResultContainer = document.getElementById('code-test-result-container');
147
+ codeTestSpinner = document.getElementById('code-test-spinner');
148
+ codeTestResult = document.getElementById('code-test-result');
149
+
150
+ // Modals
151
+ loadingModal = document.getElementById('loading-modal');
152
+ loadingMessage = document.getElementById('loading-message');
153
+ promptModal = document.getElementById('prompt-modal');
154
+ promptModalTitle = document.getElementById('prompt-modal-title');
155
+ promptModalCloseBtn = document.getElementById('prompt-modal-close-btn');
156
+ promptModalCancelBtn = document.getElementById('prompt-modal-cancel-btn');
157
+ promptModalForm = document.getElementById('prompt-modal-form');
158
+ promptModalSaveBtn = document.getElementById('prompt-modal-save-btn');
159
+ promptModalEditIndex = document.getElementById('prompt-modal-edit-index');
160
+ promptModalTitleInput = document.getElementById('prompt-modal-title-input');
161
+ promptModalMessageInput = document.getElementById('prompt-modal-message-input');
162
+ deleteAgentModal = document.getElementById('delete-agent-modal');
163
+ deleteAgentCancelBtn = document.getElementById('delete-agent-cancel-btn');
164
+ deleteAgentConfirmBtn = document.getElementById('delete-agent-confirm-btn');
165
+ geminiSettingsModal = document.getElementById('gemini-settings-modal');
166
+ geminiSettingsCloseBtn = document.getElementById('gemini-settings-close-btn');
167
+ geminiSettingsSaveBtn = document.getElementById('gemini-settings-save-btn');
168
+ }
169
+
170
+ if (navbar && navbar.shadowRoot) {
171
+ menuBtn = navbar.shadowRoot.querySelector('#menu-btn');
172
+ profileToggleBtn = navbar.shadowRoot.querySelector('#profile-toggle-btn');
173
+ profileHeaderIcon = navbar.shadowRoot.querySelector('#profile-header-icon');
174
+ profileHeaderImg = navbar.shadowRoot.querySelector('#profile-header-img');
175
+ }
176
+
177
+ if (sidebar && sidebar.shadowRoot) {
178
+ profileSidebar = sidebar.shadowRoot.querySelector('#profile-sidebar');
179
+ sidebarOverlay = sidebar.shadowRoot.querySelector('#sidebar-overlay');
180
+ profileSidebarCloseBtn = sidebar.shadowRoot.querySelector('#profile-sidebar-close-btn');
181
+ profileImgPreview = sidebar.shadowRoot.querySelector('#profile-img-preview');
182
+ profileImgUrlInput = sidebar.shadowRoot.querySelector('#profile-img-url-input');
183
+ profileDisplayNameInput = sidebar.shadowRoot.querySelector('#profile-display-name-input');
184
+ profileBioInput = sidebar.shadowRoot.querySelector('#profile-bio-input');
185
+ suggestBioBtn = sidebar.shadowRoot.querySelector('#suggest-bio-btn');
186
+ saveProfileBtn = sidebar.shadowRoot.querySelector('#save-profile-btn');
187
+ googleSigninBtn = sidebar.shadowRoot.querySelector('#google-signin-btn');
188
+ githubSigninBtn = sidebar.shadowRoot.querySelector('#github-signin-btn');
189
+ signOutBtn = sidebar.shadowRoot.querySelector('#sign-out-btn');
190
+ socialLoginContainer = sidebar.shadowRoot.querySelector('#social-login-container');
191
+ signOutContainer = sidebar.shadowRoot.querySelector('#sign-out-container');
192
+ }
193
+
194
+ // Audio player
195
+ ttsPlayer = document.getElementById('tts-player');
196
+
197
+ if (agentNameInput && menuBtn && profileToggleBtn) {
198
+ clearInterval(waitForElements);
199
+ console.log('DOM elements ready');
200
+ attachEventListeners();
201
+ initializeAppUI();
202
+ }
203
+ }, 100);
204
+ }
205
+
206
+ // --- Render Functions ---
207
+
208
+ /** Initializes the UI from the current appState */
209
+ function initializeAppUI() {
210
+ if (!agentNameInput) {
211
+ console.warn("DOM elements not ready, skipping UI init.");
212
+ return; // Guard clause
213
+ }
214
+
215
+ // 1. Populate Agent Config Form
216
+ agentNameInput.value = appState.agentName;
217
+ agentDescriptionInput.value = appState.description;
218
+ agentInstructionsInput.value = appState.instructions;
219
+ prioritizeKnowledgeToggle.checked = appState.knowledge.prioritize;
220
+
221
+ // Handle Code Interpreter Toggle and Test Button
222
+ codeInterpreterToggle.checked = appState.capabilities.codeInterpreter;
223
+ if (appState.capabilities.codeInterpreter) {
224
+ testCodeInterpreterBtn.disabled = false;
225
+ testCodeInterpreterBtn.classList.remove('btn-disabled', 'text-slate-400');
226
+ testCodeInterpreterBtn.classList.add('text-indigo-600', 'hover:bg-indigo-50');
227
+ } else {
228
+ testCodeInterpreterBtn.disabled = true;
229
+ testCodeInterpreterBtn.classList.add('btn-disabled', 'text-slate-400');
230
+ testCodeInterpreterBtn.classList.remove('text-indigo-600', 'hover:bg-indigo-50');
231
+ }
232
+
233
+ // Handle Image Generator Toggle and Test Button
234
+ imageGeneratorToggle.checked = appState.capabilities.imageGenerator;
235
+ if (appState.capabilities.imageGenerator) {
236
+ testImageGenBtn.disabled = false;
237
+ testImageGenBtn.classList.remove('btn-disabled', 'text-slate-400');
238
+ testImageGenBtn.classList.add('text-indigo-600', 'hover:bg-indigo-50');
239
+ } else {
240
+ testImageGenBtn.disabled = true;
241
+ testImageGenBtn.classList.add('btn-disabled', 'text-slate-400');
242
+ testImageGenBtn.classList.remove('text-indigo-600', 'hover:bg-indigo-50');
243
+ }
244
+
245
+ if (appState.modelConfig?.selectedModel) {
246
+ const modelInput = document.querySelector(`input[name="model-selection"][value="${appState.modelConfig.selectedModel}"]`);
247
+ if (modelInput) modelInput.checked = true;
248
+ }
249
+ document.getElementById('api-key-qhy-pro-input').value = appState.modelConfig?.apiKeys?.['qhy-pro'] || '';
250
+ document.getElementById('api-key-gemini-2.5-input').value = appState.modelConfig?.apiKeys?.['gemini-2.5'] || '';
251
+
252
+ renderModelConfig();
253
+ renderKnowledgeList();
254
+ renderPromptList();
255
+
256
+ // 2. Populate Profile Sidebar
257
+ const currentUser = auth.currentUser;
258
+ const displayName = currentUser?.displayName || appState.displayName || '';
259
+ const photoURL = currentUser?.photoURL || appState.photoURL || '';
260
+ const bio = appState.bio || '';
261
+
262
+ if (profileDisplayNameInput) profileDisplayNameInput.value = displayName;
263
+ if (profileImgUrlInput) profileImgUrlInput.value = photoURL;
264
+ if (profileBioInput) profileBioInput.value = bio;
265
+
266
+ // Update image previews
267
+ updateImagePreviews(photoURL);
268
+
269
+ // 3. Update Auth-dependent UI
270
+ if (currentUser && saveProfileBtn) {
271
+ if (currentUser.isAnonymous) {
272
+ if (socialLoginContainer) socialLoginContainer.classList.remove('hidden');
273
+ if (signOutContainer) signOutContainer.classList.add('hidden');
274
+ saveProfileBtn.classList.add('btn-disabled');
275
+ saveProfileBtn.disabled = true;
276
+ saveProfileBtn.title = "Sign in to save profile";
277
+ } else {
278
+ if (socialLoginContainer) socialLoginContainer.classList.add('hidden');
279
+ if (signOutContainer) signOutContainer.classList.remove('hidden');
280
+ saveProfileBtn.classList.remove('btn-disabled');
281
+ saveProfileBtn.disabled = false;
282
+ saveProfileBtn.title = "";
283
+ }
284
+ }
285
+ }
286
+
287
+ function updateImagePreviews(url) {
288
+ const validUrl = url && (url.startsWith('http') || url.startsWith('data:image'));
289
+ const userIconSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>`;
290
+
291
+ if (profileHeaderImg && profileHeaderIcon) {
292
+ if (validUrl) {
293
+ profileHeaderImg.src = url;
294
+ profileHeaderImg.classList.remove('hidden');
295
+ profileHeaderIcon.classList.add('hidden');
296
+ } else {
297
+ profileHeaderImg.classList.add('hidden');
298
+ profileHeaderIcon.classList.remove('hidden');
299
+ }
300
+ }
301
+
302
+ if (profileImgPreview) {
303
+ if (validUrl) {
304
+ profileImgPreview.src = url;
305
+ profileImgPreview.classList.remove('bg-slate-200', 'text-slate-400');
306
+ profileImgPreview.innerHTML = '';
307
+ } else {
308
+ profileImgPreview.src = '';
309
+ profileImgPreview.classList.add('bg-slate-200', 'text-slate-400');
310
+ profileImgPreview.innerHTML = userIconSvg;
311
+ }
312
+ }
313
+ }
314
+
315
+ function renderKnowledgeList() {
316
+ if (!knowledgeListContainer) return;
317
+
318
+ const items = knowledgeListContainer.querySelectorAll('.knowledge-item');
319
+ items.forEach(item => item.remove());
320
+ const urls = appState.knowledge?.urls || [];
321
+
322
+ if (knowledgeEmptyState) {
323
+ if (urls.length === 0) {
324
+ knowledgeEmptyState.classList.remove('hidden');
325
+ } else {
326
+ knowledgeEmptyState.classList.add('hidden');
327
+ }
328
+ }
329
+
330
+ urls.forEach((url, index) => {
331
+ const div = document.createElement('div');
332
+ div.className = 'knowledge-item flex items-center justify-between rounded-md border border-slate-200 bg-white p-3';
333
+ div.innerHTML = `<span class="text-sm text-slate-800 truncate" title="${url}">${url}</span><button class="ml-4 flex-shrink-0 p-1 text-slate-400 hover:text-red-600 rounded-full hover:bg-red-50" data-index="${index}"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="pointer-events-none"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg></button>`;
334
+ knowledgeListContainer.appendChild(div);
335
+ });
336
+ }
337
+
338
+ function renderPromptList() {
339
+ if (!promptListContainer) return;
340
+
341
+ const items = promptListContainer.querySelectorAll('.prompt-item');
342
+ items.forEach(item => item.remove());
343
+ const prompts = appState.prompts || [];
344
+
345
+ if (promptEmptyState) {
346
+ if (prompts.length === 0) {
347
+ promptEmptyState.classList.remove('hidden');
348
+ } else {
349
+ promptEmptyState.classList.add('hidden');
350
+ }
351
+ }
352
+
353
+ prompts.forEach((prompt, index) => {
354
+ const div = document.createElement('div');
355
+ div.className = 'prompt-item rounded-md border border-slate-200 p-4';
356
+ div.innerHTML = `<div class="flex justify-between items-center mb-2"><p class="text-sm font-semibold text-slate-800">${prompt.title}</p><div><button class="edit-prompt-btn p-1 text-slate-400 hover:text-slate-700 rounded-full hover:bg-slate-100" data-index="${index}"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="pointer-events-none"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg></button><button class="delete-prompt-btn ml-1 p-1 text-slate-400 hover:text-red-600 rounded-full hover:bg-red-50" data-index="${index}"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="pointer-events-none"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg></button></div></div><p class="text-sm text-slate-600 bg-slate-50 p-3 rounded">${prompt.message}</p>`;
357
+ promptListContainer.appendChild(div);
358
+ });
359
+ }
360
+
361
+ function renderModelConfig() {
362
+ const qhyProEl = document.getElementById('api-key-qhy-pro');
363
+ const geminiEl = document.getElementById('api-key-gemini-2.5');
364
+
365
+ if (qhyProEl) qhyProEl.classList.add('hidden');
366
+ if (geminiEl) geminiEl.classList.add('hidden');
367
+
368
+ const selected = appState.modelConfig?.selectedModel;
369
+ if (selected === 'qhy-pro' && qhyProEl) {
370
+ qhyProEl.classList.remove('hidden');
371
+ } else if (selected === 'gemini-2.5' && geminiEl) {
372
+ geminiEl.classList.remove('hidden');
373
+ }
374
+ }
375
+
376
+ function renderTestChatMessage(sender, text) {
377
+ if (!testAgentHistory) return;
378
+
379
+ const messageDiv = document.createElement('div');
380
+ messageDiv.className = `chat-message ${sender} mb-2`;
381
+ let html = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
382
+ html = html.replace(/`(.*?)`/g, '<code class="bg-slate-200 px-1 py-0.5 rounded text-sm">$1</code>');
383
+ html = html.replace(/\n/g, '<br>');
384
+ messageDiv.innerHTML = html;
385
+ testAgentHistory.appendChild(messageDiv);
386
+ testAgentHistory.scrollTop = testAgentHistory.scrollHeight;
387
+ }
388
+
389
+ function setChatTyping(isTyping) {
390
+ if (!testAgentHistory) return;
391
+
392
+ let typingEl = document.getElementById('typing-indicator');
393
+ if (isTyping) {
394
+ if (!typingEl) {
395
+ typingEl = document.createElement('div');
396
+ typingEl.id = 'typing-indicator';
397
+ typingEl.className = 'chat-message model typing';
398
+ typingEl.textContent = 'typing...';
399
+ testAgentHistory.appendChild(typingEl);
400
+ testAgentHistory.scrollTop = testAgentHistory.scrollHeight;
401
+ }
402
+ } else {
403
+ if (typingEl) {
404
+ typingEl.remove();
405
+ }
406
+ }
407
+ }
408
 
409
+ // --- Modal & Sidebar Functions ---
410
+
411
+ function showModal(modalEl) {
412
+ if (!modalEl) return;
413
+ modalEl.classList.remove('opacity-0', 'pointer-events-none');
414
+ const content = modalEl.querySelector('.modal-content');
415
+ if (content) content.classList.remove('-translate-y-10');
416
+ }
417
+
418
+ function hideModal(modalEl) {
419
+ if (!modalEl) return;
420
+ modalEl.classList.add('opacity-0', 'pointer-events-none');
421
+ const content = modalEl.querySelector('.modal-content');
422
+ if (content) content.classList.add('-translate-y-10');
423
+ }
424
+
425
+ function showLoading(message = "Generating...") {
426
+ if (loadingMessage) loadingMessage.textContent = message;
427
+ if (loadingModal) showModal(loadingModal);
428
+ }
429
+
430
+ function hideLoading() {
431
+ if (loadingModal) hideModal(loadingModal);
432
+ }
433
+
434
+ function showPromptModal(mode = 'add', index = -1) {
435
+ if (!promptModal || !promptModalTitle || !promptModalSaveBtn ||
436
+ !promptModalEditIndex || !promptModalTitleInput || !promptModalMessageInput) return;
437
+
438
+ if (mode === 'edit' && index > -1) {
439
+ const prompt = appState.prompts[index];
440
+ promptModalTitle.textContent = 'Edit Prompt';
441
+ promptModalSaveBtn.textContent = 'Save Changes';
442
+ promptModalEditIndex.value = index;
443
+ promptModalTitleInput.value = prompt.title;
444
+ promptModalMessageInput.value = prompt.message;
445
  } else {
446
+ promptModalTitle.textContent = 'Add New Prompt';
447
+ promptModalSaveBtn.textContent = 'Save Prompt';
448
+ if (promptModalForm) promptModalForm.reset();
449
+ promptModalEditIndex.value = -1;
450
+ }
451
+ showModal(promptModal);
452
+ }
453
+
454
+ function toggleProfileSidebar() {
455
+ isProfileSidebarOpen = !isProfileSidebarOpen;
456
+ const sidebar = document.querySelector('custom-sidebar');
457
+ if (sidebar && sidebar.shadowRoot) {
458
+ const profileSidebar = sidebar.shadowRoot.querySelector('#profile-sidebar');
459
+ const sidebarOverlay = sidebar.shadowRoot.querySelector('#sidebar-overlay');
460
+
461
+ if (profileSidebar && sidebarOverlay) {
462
+ if (isProfileSidebarOpen) {
463
+ profileSidebar.classList.add('open');
464
+ sidebarOverlay.classList.remove('opacity-0', 'pointer-events-none');
465
+ } else {
466
+ profileSidebar.classList.remove('open');
467
+ sidebarOverlay.classList.add('opacity-0', 'pointer-events-none');
468
+ }
469
+ }
470
+ }
471
+ }
472
+
473
+ function openTestAgentModal() {
474
+ testChatHistory = []; // Reset history
475
+ if (testAgentHistory) testAgentHistory.innerHTML = ''; // Clear UI
476
+ renderTestChatMessage('model', 'Hi! I\'m ready to test. My instructions are set. Ask me anything.');
477
+ if (testAgentModal) showModal(testAgentModal);
478
+ }
479
+
480
+ function openImageGenModal() {
481
+ if (imageGenInput) imageGenInput.value = '';
482
+ if (imageGenResult) {
483
+ imageGenResult.src = '';
484
+ imageGenResult.alt = '';
485
+ }
486
+ if (imageGenError) imageGenError.textContent = '';
487
+ if (imageGenResultContainer) imageGenResultContainer.classList.remove('loading');
488
+ if (imageGenResult) imageGenResult.classList.add('hidden');
489
+ if (imageGenError) imageGenError.classList.add('hidden');
490
+ if (imageGenModal) showModal(imageGenModal);
491
+ }
492
+
493
+ // NEW: Open Code Test Modal
494
+ function openCodeTestModal() {
495
+ if (codeTestResultContainer) codeTestResultContainer.classList.remove('loading');
496
+ if (codeTestResult) codeTestResult.textContent = '// Click "Generate Example" to see a code snippet...';
497
+ if (codeTestSpinner) codeTestSpinner.style.display = 'none';
498
+ if (codeTestResult) codeTestResult.style.display = 'block';
499
+ if (codeTestModal) showModal(codeTestModal);
500
+ }
501
+
502
+ // --- Gemini API Callers ---
503
+
504
+ // Text Generation
505
+ async function callGeminiApi(payload, maxRetries = 5) {
506
+ const apiKey = ""; // In a real app, this would come from environment variables
507
+ const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${apiKey}`;
508
+ let attempt = 0, delay = 1000;
509
+
510
+ while (attempt < maxRetries) {
511
  try {
512
+ const response = await fetch(apiUrl, {
513
+ method: 'POST',
514
+ headers: { 'Content-Type': 'application/json' },
515
+ body: JSON.stringify(payload)
516
+ });
517
+
518
+ if (response.status === 429 || response.status >= 500) {
519
+ throw new Error(`API Error: ${response.status}`);
520
+ }
521
+
522
+ if (!response.ok) {
523
+ const errorResult = await response.json();
524
+ console.error("Gemini API Error:", errorResult);
525
+ throw new Error(`Gemini API Error: ${errorResult.error?.message || response.statusText}`);
526
+ }
527
+
528
+ const result = await response.json();
529
+ const candidate = result.candidates?.[0];
530
+
531
+ if (!candidate) throw new Error("Invalid API response: No candidates found.");
532
+
533
+ if (payload.generationConfig?.responseMimeType === "application/json") {
534
+ const text = candidate.content?.parts?.[0]?.text;
535
+ if (!text) throw new Error("Invalid API response: No text part found for JSON.");
536
+ return text;
537
+ }
538
+
539
+ const text = candidate.content?.parts?.[0]?.text;
540
+ if (typeof text !== 'string') throw new Error("Invalid API response: No text content found.");
541
+ return text;
542
+ } catch (error) {
543
+ console.warn(`Gemini API call attempt ${attempt + 1} failed: ${error.message}`);
544
+ attempt++;
545
+ if (attempt >= maxRetries) throw new Error(`Gemini API failed after ${maxRetries} attempts: ${error.message}`);
546
+ await new Promise(resolve => setTimeout(resolve, delay));
547
+ delay *= 2;
548
+ }
549
+ }
550
+ }
551
+
552
+ // Image Generation
553
+ async function callImagenApi(userPrompt, maxRetries = 5) {
554
+ const apiKey = ""; // API key is handled by the environment
555
+ const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/imagen-3.0-generate-002:predict?key=${apiKey}`;
556
+
557
+ const payload = {
558
+ instances: { prompt: userPrompt },
559
+ parameters: { "sampleCount": 1 }
560
+ };
561
+
562
+ let attempt = 0, delay = 1000;
563
+ while (attempt < maxRetries) {
564
+ try {
565
+ const response = await fetch(apiUrl, {
566
+ method: 'POST',
567
+ headers: { 'Content-Type': 'application/json' },
568
+ body: JSON.stringify(payload)
569
+ });
570
+
571
+ if (response.status === 429 || response.status >= 500) {
572
+ throw new Error(`API Error: ${response.status}`);
573
+ }
574
+
575
+ if (!response.ok) {
576
+ const errorResult = await response.json();
577
+ console.error("Imagen API Error:", errorResult);
578
+ throw new Error(`Imagen API Error: ${errorResult.error?.message || response.statusText}`);
579
+ }
580
+
581
+ const result = await response.json();
582
+
583
+ if (result.predictions && result.predictions.length > 0 && result.predictions[0].bytesBase64Encoded) {
584
+ return `data:image/png;base64,${result.predictions[0].bytesBase64Encoded}`;
585
  } else {
586
+ throw new Error("Invalid API response: No image data found.");
587
  }
588
+
589
  } catch (error) {
590
+ console.warn(`Imagen API call attempt ${attempt + 1} failed: ${error.message}`);
591
+ attempt++;
592
+ if (attempt >= maxRetries) {
593
+ throw new Error(`Imagen API failed after ${maxRetries} attempts: ${error.message}`);
594
+ }
595
+ await new Promise(resolve => setTimeout(resolve, delay));
596
+ delay *= 2;
597
  }
598
  }
599
+ }
600
+
601
+ // NEW: TTS Generation
602
+ async function callTtsApi(textToSpeak, maxRetries = 5) {
603
+ const apiKey = "";
604
+ const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-tts:generateContent?key=${apiKey}`;
605
+
606
+ const payload = {
607
+ contents: [{
608
+ parts: [{ text: textToSpeak }]
609
+ }],
610
+ generationConfig: {
611
+ responseModalities: ["AUDIO"],
612
+ speechConfig: {
613
+ voiceConfig: {
614
+ prebuiltVoiceConfig: { voiceName: "Kore" } // Using 'Kore' voice
615
+ }
616
+ }
617
+ },
618
+ model: "gemini-2.5-flash-preview-tts"
619
+ };
620
 
621
+ let attempt = 0, delay = 1000;
622
+ while (attempt < maxRetries) {
623
+ try {
624
+ const response = await fetch(apiUrl, {
625
+ method: 'POST',
626
+ headers: { 'Content-Type': 'application/json' },
627
+ body: JSON.stringify(payload)
628
+ });
629
+
630
+ if (response.status === 429 || response.status >= 500) throw new Error(`API Error: ${response.status}`);
631
+ if (!response.ok) {
632
+ const errorResult = await response.json();
633
+ console.error("TTS API Error:", errorResult);
634
+ throw new Error(`TTS API Error: ${errorResult.error?.message || response.statusText}`);
635
+ }
636
+
637
+ const result = await response.json();
638
+ const part = result?.candidates?.[0]?.content?.parts?.[0];
639
+ const audioData = part?.inlineData?.data;
640
+ const mimeType = part?.inlineData?.mimeType;
641
+
642
+ if (audioData && mimeType && mimeType.startsWith("audio/")) {
643
+ const sampleRate = parseInt(mimeType.match(/rate=(\d+)/)[1], 10) || 24000;
644
+ return { audioData, sampleRate };
645
+ } else {
646
+ throw new Error("Invalid API response: No audio data found.");
647
+ }
648
+ } catch (error) {
649
+ console.warn(`TTS API call attempt ${attempt + 1} failed: ${error.message}`);
650
+ attempt++;
651
+ if (attempt >= maxRetries) throw new Error(`TTS API failed after ${maxRetries} attempts: ${error.message}`);
652
+ await new Promise(resolve => setTimeout(resolve, delay));
653
+ delay *= 2;
654
+ }
655
+ }
656
+ }
657
+
658
+ // --- TTS Audio Helpers ---
659
+
660
+ // NEW: Decodes base64 string to ArrayBuffer
661
+ function base64ToArrayBuffer(base64) {
662
+ const binaryString = window.atob(base64);
663
+ const len = binaryString.length;
664
+ const bytes = new Uint8Array(len);
665
+ for (let i = 0; i < len; i++) {
666
+ bytes[i] = binaryString.charCodeAt(i);
667
+ }
668
+ return bytes.buffer;
669
+ }
670
+
671
+ // NEW: Wraps raw PCM data in a WAV header
672
+ function pcmToWav(pcmData, sampleRate) {
673
+ const numChannels = 1;
674
+ const bitsPerSample = 16;
675
+ const byteRate = sampleRate * numChannels * (bitsPerSample / 8);
676
+ const blockAlign = numChannels * (bitsPerSample / 8);
677
+ const dataSize = pcmData.byteLength;
678
+ const fileSize = 36 + dataSize;
679
+
680
+ const buffer = new ArrayBuffer(44 + dataSize);
681
+ const view = new DataView(buffer);
682
+
683
+ // RIFF header
684
+ writeString(view, 0, 'RIFF');
685
+ view.setUint32(4, fileSize, true);
686
+ writeString(view, 8, 'WAVE');
687
+
688
+ // fmt chunk
689
+ writeString(view, 12, 'fmt ');
690
+ view.setUint32(16, 16, true); // chunk size
691
+ view.setUint16(20, 1, true); // audio format (1 = PCM)
692
+ view.setUint16(22, numChannels, true);
693
+ view.setUint32(24, sampleRate, true);
694
+ view.setUint32(28, byteRate, true);
695
+ view.setUint16(32, blockAlign, true);
696
+ view.setUint16(34, bitsPerSample, true);
697
+
698
+ // data chunk
699
+ writeString(view, 36, 'data');
700
+ view.setUint32(40, dataSize, true);
701
+
702
+ // Write PCM data
703
+ new Uint8Array(buffer, 44).set(new Uint8Array(pcmData));
704
+
705
+ return new Blob([buffer], { type: 'audio/wav' });
706
+ }
707
+
708
+ // NEW: Helper for pcmToWav
709
+ function writeString(view, offset, string) {
710
+ for (let i = 0; i < string.length; i++) {
711
+ view.setUint8(offset + i, string.charCodeAt(i));
712
+ }
713
+ }
714
+
715
+ /**
716
+ * Loads the agent configuration from Firestore.
717
+ */
718
+ async function loadAgentConfig() {
719
+ if (!agentConfigDocRef) return;
720
+ try {
721
+ const docSnap = await getDoc(agentConfigDocRef);
722
+ if (docSnap.exists()) {
723
+ console.log("Loaded data from Firestore:", docSnap.data());
724
+ appState = { ...defaultAppState, ...docSnap.data() };
725
+ } else {
726
+ console.log("No saved config found, using default.");
727
+ appState = { ...defaultAppState };
728
+ }
729
+ } catch (error) {
730
+ console.error("Error loading agent config:", error);
731
+ appState = { ...defaultAppState };
732
+ }
733
+ }
734
+
735
+ // --- Auth Functions ---
736
+ async function signInWithGoogle() {
737
+ const provider = new GoogleAuthProvider();
738
+ try {
739
+ await signInWithPopup(auth, provider);
740
+ toggleProfileSidebar();
741
+ } catch (error) {
742
+ console.error("Google Sign-In Error:", error);
743
+ }
744
+ }
745
+
746
+ async function signInWithGitHub() {
747
+ const provider = new GithubAuthProvider();
748
+ try {
749
+ await signInWithPopup(auth, provider);
750
+ toggleProfileSidebar();
751
+ } catch (error) {
752
+ console.error("GitHub Sign-In Error:", error);
753
+ }
754
+ }
755
+
756
+ async function handleSignOut() {
757
+ try {
758
+ await signOut(auth);
759
+ toggleProfileSidebar();
760
+ } catch (error) {
761
+ console.error("Sign-Out Error:", error);
762
+ }
763
+ }
764
+
765
+ async function handleSaveProfile() {
766
+ const currentUser = auth.currentUser;
767
+ if (!currentUser || currentUser.isAnonymous ||
768
+ !profileDisplayNameInput || !profileImgUrlInput || !profileBioInput ||
769
+ !saveProfileBtn || !agentConfigDocRef) return;
770
+
771
+ const displayName = profileDisplayNameInput.value.trim();
772
+ const photoURL = profileImgUrlInput.value.trim();
773
+ const bio = profileBioInput.value.trim();
774
+
775
+ saveProfileBtn.textContent = "Saving...";
776
+ saveProfileBtn.disabled = true;
777
+ saveProfileBtn.classList.add('btn-disabled');
778
+
779
+ try {
780
+ await updateProfile(currentUser, { displayName, photoURL });
781
+ const profileData = { displayName, photoURL, bio };
782
+ await setDoc(agentConfigDocRef, profileData, { merge: true });
783
+ appState.displayName = displayName;
784
+ appState.photoURL = photoURL;
785
+ appState.bio = bio;
786
+ updateImagePreviews(photoURL);
787
+ saveProfileBtn.textContent = "Profile Saved!";
788
+ saveProfileBtn.classList.add('bg-green-600');
789
+
790
+ setTimeout(() => {
791
+ saveProfileBtn.textContent = "Save Profile";
792
+ saveProfileBtn.disabled = false;
793
+ saveProfileBtn.classList.remove('btn-disabled', 'bg-green-600');
794
+ }, 2000);
795
+ } catch (error) {
796
+ console.error("Error saving profile:", error);
797
+ saveProfileBtn.textContent = "Save Failed";
798
+ saveProfileBtn.classList.add('bg-red-600');
799
+
800
+ setTimeout(() => {
801
+ saveProfileBtn.textContent = "Save Profile";
802
+ saveProfileBtn.disabled = false;
803
+ saveProfileBtn.classList.remove('btn-disabled', 'bg-red-600');
804
+ }, 2000);
805
+ }
806
+ }
807
+
808
+ // --- AI Feature Handlers ---
809
+
810
+ async function handleSuggestBio() {
811
+ if (!profileDisplayNameInput || !profileBioInput) return;
812
+
813
+ const agentRole = profileDisplayNameInput.value.trim();
814
+ if (!agentRole) {
815
+ profileBioInput.placeholder = "Please enter a Display Name first, then click ✨ Suggest.";
816
+ return;
817
+ }
818
+
819
+ const userQuery = `Based on the following name or role, write a short, professional bio (1-2 sentences). Role: "${agentRole}" Return ONLY the bio text, with no preamble or quotes.`;
820
+ const payload = {
821
+ contents: [{ parts: [{ text: userQuery }] }],
822
+ systemInstruction: { parts: [{ text: "You are a professional profile writer. You return only the bio text." }] }
823
+ };
824
+
825
+ showLoading("Generating bio...");
826
+ try {
827
+ const generatedText = await callGeminiApi(payload);
828
+ const cleanText = generatedText.replace(/["*]/g, "").trim();
829
+ profileBioInput.value = cleanText;
830
+ } catch (error) {
831
+ console.error(error);
832
+ profileBioInput.value = `Error: ${error.message}`;
833
+ } finally {
834
+ hideLoading();
835
+ }
836
+ }
837
+
838
+ async function handleTestAgentSend(e) {
839
+ e.preventDefault();
840
+ if (!testAgentInput || !testAgentHistory) return;
841
+
842
+ const userMessage = testAgentInput.value.trim();
843
+ if (!userMessage) return;
844
+
845
+ testAgentInput.value = '';
846
+ renderTestChatMessage('user', userMessage);
847
+ setChatTyping(true);
848
+ testChatHistory.push({ role: "user", parts: [{ text: userMessage }] });
849
+
850
+ let finalSystemInstructions = appState.instructions || "You are a helpful assistant.";
851
+ let payloadTools = [];
852
+
853
+ if (appState.knowledge.urls.length > 0 && appState.knowledge.prioritize) {
854
+ finalSystemInstructions += `\n\nCRITICAL KNOWLEDGE: You MUST use the provided Google Search tool to find information to answer the user's query. You must prioritize information from these specific websites: ${appState.knowledge.urls.join(', ')}. If the answer cannot be found in those sources, you should state that.`;
855
+ payloadTools.push({ "google_search": {} });
856
+ }
857
+
858
+ const payload = {
859
+ contents: testChatHistory,
860
+ systemInstruction: { parts: [{ text: finalSystemInstructions }] }
861
+ };
862
+
863
+ if (payloadTools.length > 0) {
864
+ payload.tools = payloadTools;
865
+ }
866
+
867
+ try {
868
+ const modelResponse = await callGeminiApi(payload);
869
+ testChatHistory.push({ role: "model", parts: [{ text: modelResponse }] });
870
+ setChatTyping(false);
871
+ renderTestChatMessage('model', modelResponse);
872
+ } catch (error) {
873
+ console.error("Test Agent Chat Error:", error);
874
+ setChatTyping(false);
875
+ renderTestChatMessage('model', `Sorry, an error occurred: ${error.message}`);
876
+ }
877
+ }
878
+
879
+ async function handleImageGenTest(e) {
880
+ e.preventDefault();
881
+ if (!imageGenInput || !imageGenResultContainer || !imageGenError ||
882
+ !imageGenResult || !imageGenSpinner) return;
883
+
884
+ const userPrompt = imageGenInput.value.trim();
885
+ if (!userPrompt) return;
886
+
887
+ imageGenResultContainer.classList.add('loading');
888
+ imageGenError.textContent = '';
889
+ imageGenError.classList.add('hidden');
890
+ imageGenResult.classList.add('hidden');
891
+
892
+ try {
893
+ const imageUrl = await callImagenApi(userPrompt);
894
+ imageGenResult.src = imageUrl;
895
+ imageGenResult.alt = userPrompt;
896
+ imageGenResult.classList.remove('hidden');
897
+ } catch (error) {
898
+ console.error("Image Gen Test Error:", error);
899
+ imageGenError.textContent = `Error: ${error.message}`;
900
+ imageGenError.classList.remove('hidden');
901
+ } finally {
902
+ imageGenResultContainer.classList.remove('loading');
903
+ }
904
+ }
905
+
906
+ // NEW: Handle TTS for instructions
907
+ async function handleListenInstructions() {
908
+ if (!agentInstructionsInput || !listenInstructionsBtn || !ttsPlayer) return;
909
+
910
+ const text = agentInstructionsInput.value.trim();
911
+ if (!text) return;
912
+
913
+ const icon = listenInstructionsBtn.querySelector('svg');
914
+ listenInstructionsBtn.disabled = true;
915
+ if (icon) icon.classList.add('spinner');
916
+
917
+ try {
918
+ const { audioData, sampleRate } = await callTtsApi(text);
919
+ const pcmBuffer = base64ToArrayBuffer(audioData);
920
+ const wavBlob = pcmToWav(pcmBuffer, sampleRate);
921
+ const audioUrl = URL.createObjectURL(wavBlob);
922
+
923
+ ttsPlayer.src = audioUrl;
924
+ ttsPlayer.play();
925
+
926
+ } catch (error) {
927
+ console.error("TTS Error:", error);
928
+ } finally {
929
+ listenInstructionsBtn.disabled = false;
930
+ if (icon) icon.classList.remove('spinner');
931
+ }
932
+ }
933
+
934
+ // NEW: Handle Code Interpreter Test
935
+ async function handleCodeInterpreterTest() {
936
+ if (!codeTestResultContainer || !codeTestSpinner || !codeTestResult) return;
937
+
938
+ codeTestResultContainer.classList.add('loading');
939
+ codeTestSpinner.style.display = 'flex';
940
+ codeTestResult.style.display = 'none';
941
+
942
+ const agentRole = appState.agentName.trim() || "a helpful assistant";
943
+ const userQuery = `You are a code interpreter. Your agent role is "${agentRole}".
944
+ Generate a simple Python code snippet that demonstrates this role.
945
+ Return ONLY the Python code, inside \`\`\`python ... \`\`\` tags.`;
946
+
947
+ const payload = {
948
+ contents: [{ parts: [{ text: userQuery }] }],
949
+ systemInstruction: { parts: [{ text: "You are a helpful code generation assistant. You only return code." }] }
950
+ };
951
+
952
+ try {
953
+ const generatedText = await callGeminiApi(payload);
954
+ // Clean up markdown fences
955
+ const cleanCode = generatedText.replace(/^