|
|
|
|
|
import { initializeApp } from 'https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js'; |
|
|
import { getAuth, onAuthStateChanged, signInAnonymously, signInWithCustomToken, signOut, GoogleAuthProvider, GithubAuthProvider, signInWithPopup, updateProfile } from 'https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js'; |
|
|
import { getFirestore, doc, getDoc, setDoc, deleteDoc } from 'https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js'; |
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', async () => { |
|
|
try { |
|
|
console.log('App initialized'); |
|
|
|
|
|
const firebaseConfig = JSON.parse(typeof __firebase_config !== 'undefined' ? __firebase_config : '{}'); |
|
|
const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id'; |
|
|
|
|
|
const app = initializeApp(firebaseConfig); |
|
|
const db = getFirestore(app); |
|
|
const auth = getAuth(app); |
|
|
|
|
|
|
|
|
|
|
|
console.log("Firebase Initialized. Waiting for auth..."); |
|
|
|
|
|
|
|
|
let userId, agentConfigDocRef; |
|
|
let isProfileSidebarOpen = false; |
|
|
let testChatHistory = []; |
|
|
|
|
|
const defaultAppState = { |
|
|
agentName: "Creative Writer", |
|
|
description: "A helpful assistant for Python code.", |
|
|
instructions: "You are a helpful AI assistant...", |
|
|
modelConfig: { |
|
|
selectedModel: 'llama3-70b', |
|
|
apiKeys: { 'qhy-pro': '', 'gemini-2.5': '' } |
|
|
}, |
|
|
knowledge: { |
|
|
prioritize: false, |
|
|
urls: ['https://qhy.sync/docs/main'] |
|
|
}, |
|
|
capabilities: { |
|
|
codeInterpreter: false, |
|
|
imageGenerator: true |
|
|
}, |
|
|
prompts: [ |
|
|
{ title: "Explain a concept", message: "\"Explain quantum computing...\"" }, |
|
|
{ title: "Write some code", message: "\"Write a Python function...\"" } |
|
|
], |
|
|
bio: "", |
|
|
displayName: "", |
|
|
photoURL: "" |
|
|
}; |
|
|
|
|
|
let appState = { ...defaultAppState }; |
|
|
|
|
|
|
|
|
let agentNameInput, agentDescriptionInput, agentInstructionsInput, |
|
|
listenInstructionsBtn, ttsPlayer, |
|
|
modelConfigContainer, knowledgeListContainer, knowledgeEmptyState, |
|
|
knowledgeUrlInput, addKnowledgeUrlBtn, prioritizeKnowledgeToggle, |
|
|
codeInterpreterToggle, testCodeInterpreterBtn, |
|
|
imageGeneratorToggle, testImageGenBtn, |
|
|
promptListContainer, promptEmptyState, addPromptBtn, |
|
|
saveChangesBtn, deleteAgentBtn, suggestNameBtn, |
|
|
suggestDescriptionBtn, generateInstructionsBtn, suggestPromptsBtn, |
|
|
loadingModal, loadingMessage, promptModal, promptModalTitle, |
|
|
promptModalCloseBtn, promptModalCancelBtn, promptModalForm, |
|
|
promptModalSaveBtn, promptModalEditIndex, promptModalTitleInput, |
|
|
promptModalMessageInput, deleteAgentModal, deleteAgentCancelBtn, |
|
|
deleteAgentConfirmBtn, |
|
|
|
|
|
menuBtn, profileToggleBtn, profileHeaderIcon, profileHeaderImg, |
|
|
profileSidebar, sidebarOverlay, profileSidebarCloseBtn, |
|
|
profileImgPreview, profileImgUrlInput, profileDisplayNameInput, |
|
|
profileBioInput, suggestBioBtn, saveProfileBtn, googleSigninBtn, |
|
|
githubSigninBtn, signOutBtn, socialLoginContainer, signOutContainer, |
|
|
geminiSettingsModal, geminiSettingsCloseBtn, geminiSettingsSaveBtn, |
|
|
|
|
|
testAgentBtn, testAgentModal, testAgentCloseBtn, testAgentHistory, |
|
|
testAgentForm, testAgentInput, |
|
|
|
|
|
imageGenModal, imageGenCloseBtn, imageGenForm, imageGenInput, |
|
|
imageGenResultContainer, imageGenSpinner, imageGenResult, imageGenError, |
|
|
|
|
|
codeTestModal, codeTestCloseBtn, codeTestGenerateBtn, |
|
|
codeTestResultContainer, codeTestSpinner, codeTestResult; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function selectDOMElements() { |
|
|
|
|
|
const waitForElements = setInterval(() => { |
|
|
|
|
|
const mainContent = document.getElementById('main-content'); |
|
|
const sidebar = document.querySelector('custom-sidebar'); |
|
|
const navbar = document.querySelector('custom-navbar'); |
|
|
const modals = document.querySelector('custom-modals'); |
|
|
|
|
|
if (mainContent) { |
|
|
|
|
|
agentNameInput = document.getElementById('agent-name'); |
|
|
agentDescriptionInput = document.getElementById('agent-description'); |
|
|
agentInstructionsInput = document.getElementById('agent-instructions'); |
|
|
listenInstructionsBtn = document.getElementById('listen-instructions-btn'); |
|
|
modelConfigContainer = document.getElementById('model-config-container'); |
|
|
knowledgeListContainer = document.getElementById('knowledge-list-container'); |
|
|
knowledgeEmptyState = document.getElementById('knowledge-empty-state'); |
|
|
knowledgeUrlInput = document.getElementById('knowledge-url-input'); |
|
|
addKnowledgeUrlBtn = document.getElementById('add-knowledge-url-btn'); |
|
|
prioritizeKnowledgeToggle = document.getElementById('prioritize-knowledge'); |
|
|
codeInterpreterToggle = document.getElementById('code-interpreter'); |
|
|
testCodeInterpreterBtn = document.getElementById('test-code-interpreter-btn'); |
|
|
imageGeneratorToggle = document.getElementById('image-generator'); |
|
|
testImageGenBtn = document.getElementById('test-image-gen-btn'); |
|
|
promptListContainer = document.getElementById('prompt-list-container'); |
|
|
promptEmptyState = document.getElementById('prompt-empty-state'); |
|
|
addPromptBtn = document.getElementById('add-prompt-btn'); |
|
|
saveChangesBtn = document.getElementById('save-changes-btn'); |
|
|
deleteAgentBtn = document.getElementById('delete-agent-btn'); |
|
|
suggestNameBtn = document.getElementById('suggest-name-btn'); |
|
|
suggestDescriptionBtn = document.getElementById('suggest-description-btn'); |
|
|
generateInstructionsBtn = document.getElementById('generate-instructions-btn'); |
|
|
suggestPromptsBtn = document.getElementById('suggest-prompts-btn'); |
|
|
|
|
|
|
|
|
testAgentBtn = document.getElementById('test-agent-btn'); |
|
|
testAgentHistory = document.getElementById('test-agent-history'); |
|
|
testAgentForm = document.getElementById('test-agent-form'); |
|
|
testAgentInput = document.getElementById('test-agent-input'); |
|
|
testAgentModal = document.getElementById('test-agent-modal'); |
|
|
testAgentCloseBtn = document.getElementById('test-agent-close-btn'); |
|
|
|
|
|
|
|
|
imageGenModal = document.getElementById('image-gen-modal'); |
|
|
imageGenCloseBtn = document.getElementById('image-gen-close-btn'); |
|
|
imageGenForm = document.getElementById('image-gen-form'); |
|
|
imageGenInput = document.getElementById('image-gen-input'); |
|
|
imageGenResultContainer = document.getElementById('image-gen-result-container'); |
|
|
imageGenSpinner = document.getElementById('image-gen-spinner'); |
|
|
imageGenResult = document.getElementById('image-gen-result'); |
|
|
imageGenError = document.getElementById('image-gen-error'); |
|
|
|
|
|
|
|
|
codeTestModal = document.getElementById('code-test-modal'); |
|
|
codeTestCloseBtn = document.getElementById('code-test-close-btn'); |
|
|
codeTestGenerateBtn = document.getElementById('code-test-generate-btn'); |
|
|
codeTestResultContainer = document.getElementById('code-test-result-container'); |
|
|
codeTestSpinner = document.getElementById('code-test-spinner'); |
|
|
codeTestResult = document.getElementById('code-test-result'); |
|
|
|
|
|
|
|
|
loadingModal = document.getElementById('loading-modal'); |
|
|
loadingMessage = document.getElementById('loading-message'); |
|
|
promptModal = document.getElementById('prompt-modal'); |
|
|
promptModalTitle = document.getElementById('prompt-modal-title'); |
|
|
promptModalCloseBtn = document.getElementById('prompt-modal-close-btn'); |
|
|
promptModalCancelBtn = document.getElementById('prompt-modal-cancel-btn'); |
|
|
promptModalForm = document.getElementById('prompt-modal-form'); |
|
|
promptModalSaveBtn = document.getElementById('prompt-modal-save-btn'); |
|
|
promptModalEditIndex = document.getElementById('prompt-modal-edit-index'); |
|
|
promptModalTitleInput = document.getElementById('prompt-modal-title-input'); |
|
|
promptModalMessageInput = document.getElementById('prompt-modal-message-input'); |
|
|
deleteAgentModal = document.getElementById('delete-agent-modal'); |
|
|
deleteAgentCancelBtn = document.getElementById('delete-agent-cancel-btn'); |
|
|
deleteAgentConfirmBtn = document.getElementById('delete-agent-confirm-btn'); |
|
|
geminiSettingsModal = document.getElementById('gemini-settings-modal'); |
|
|
geminiSettingsCloseBtn = document.getElementById('gemini-settings-close-btn'); |
|
|
geminiSettingsSaveBtn = document.getElementById('gemini-settings-save-btn'); |
|
|
} |
|
|
|
|
|
if (navbar && navbar.shadowRoot) { |
|
|
menuBtn = navbar.shadowRoot.querySelector('#menu-btn'); |
|
|
profileToggleBtn = navbar.shadowRoot.querySelector('#profile-toggle-btn'); |
|
|
profileHeaderIcon = navbar.shadowRoot.querySelector('#profile-header-icon'); |
|
|
profileHeaderImg = navbar.shadowRoot.querySelector('#profile-header-img'); |
|
|
} |
|
|
|
|
|
if (sidebar && sidebar.shadowRoot) { |
|
|
profileSidebar = sidebar.shadowRoot.querySelector('#profile-sidebar'); |
|
|
sidebarOverlay = sidebar.shadowRoot.querySelector('#sidebar-overlay'); |
|
|
profileSidebarCloseBtn = sidebar.shadowRoot.querySelector('#profile-sidebar-close-btn'); |
|
|
profileImgPreview = sidebar.shadowRoot.querySelector('#profile-img-preview'); |
|
|
profileImgUrlInput = sidebar.shadowRoot.querySelector('#profile-img-url-input'); |
|
|
profileDisplayNameInput = sidebar.shadowRoot.querySelector('#profile-display-name-input'); |
|
|
profileBioInput = sidebar.shadowRoot.querySelector('#profile-bio-input'); |
|
|
suggestBioBtn = sidebar.shadowRoot.querySelector('#suggest-bio-btn'); |
|
|
saveProfileBtn = sidebar.shadowRoot.querySelector('#save-profile-btn'); |
|
|
googleSigninBtn = sidebar.shadowRoot.querySelector('#google-signin-btn'); |
|
|
githubSigninBtn = sidebar.shadowRoot.querySelector('#github-signin-btn'); |
|
|
signOutBtn = sidebar.shadowRoot.querySelector('#sign-out-btn'); |
|
|
socialLoginContainer = sidebar.shadowRoot.querySelector('#social-login-container'); |
|
|
signOutContainer = sidebar.shadowRoot.querySelector('#sign-out-container'); |
|
|
} |
|
|
|
|
|
|
|
|
ttsPlayer = document.getElementById('tts-player'); |
|
|
|
|
|
if (agentNameInput && menuBtn && profileToggleBtn) { |
|
|
clearInterval(waitForElements); |
|
|
console.log('DOM elements ready'); |
|
|
attachEventListeners(); |
|
|
initializeAppUI(); |
|
|
} |
|
|
}, 100); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function initializeAppUI() { |
|
|
if (!agentNameInput) { |
|
|
console.warn("DOM elements not ready, skipping UI init."); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
agentNameInput.value = appState.agentName; |
|
|
agentDescriptionInput.value = appState.description; |
|
|
agentInstructionsInput.value = appState.instructions; |
|
|
prioritizeKnowledgeToggle.checked = appState.knowledge.prioritize; |
|
|
|
|
|
|
|
|
codeInterpreterToggle.checked = appState.capabilities.codeInterpreter; |
|
|
if (appState.capabilities.codeInterpreter) { |
|
|
testCodeInterpreterBtn.disabled = false; |
|
|
testCodeInterpreterBtn.classList.remove('btn-disabled', 'text-slate-400'); |
|
|
testCodeInterpreterBtn.classList.add('text-indigo-600', 'hover:bg-indigo-50'); |
|
|
} else { |
|
|
testCodeInterpreterBtn.disabled = true; |
|
|
testCodeInterpreterBtn.classList.add('btn-disabled', 'text-slate-400'); |
|
|
testCodeInterpreterBtn.classList.remove('text-indigo-600', 'hover:bg-indigo-50'); |
|
|
} |
|
|
|
|
|
|
|
|
imageGeneratorToggle.checked = appState.capabilities.imageGenerator; |
|
|
if (appState.capabilities.imageGenerator) { |
|
|
testImageGenBtn.disabled = false; |
|
|
testImageGenBtn.classList.remove('btn-disabled', 'text-slate-400'); |
|
|
testImageGenBtn.classList.add('text-indigo-600', 'hover:bg-indigo-50'); |
|
|
} else { |
|
|
testImageGenBtn.disabled = true; |
|
|
testImageGenBtn.classList.add('btn-disabled', 'text-slate-400'); |
|
|
testImageGenBtn.classList.remove('text-indigo-600', 'hover:bg-indigo-50'); |
|
|
} |
|
|
|
|
|
if (appState.modelConfig?.selectedModel) { |
|
|
const modelInput = document.querySelector(`input[name="model-selection"][value="${appState.modelConfig.selectedModel}"]`); |
|
|
if (modelInput) modelInput.checked = true; |
|
|
} |
|
|
document.getElementById('api-key-qhy-pro-input').value = appState.modelConfig?.apiKeys?.['qhy-pro'] || ''; |
|
|
document.getElementById('api-key-gemini-2.5-input').value = appState.modelConfig?.apiKeys?.['gemini-2.5'] || ''; |
|
|
|
|
|
renderModelConfig(); |
|
|
renderKnowledgeList(); |
|
|
renderPromptList(); |
|
|
|
|
|
|
|
|
const currentUser = auth.currentUser; |
|
|
const displayName = currentUser?.displayName || appState.displayName || ''; |
|
|
const photoURL = currentUser?.photoURL || appState.photoURL || ''; |
|
|
const bio = appState.bio || ''; |
|
|
|
|
|
if (profileDisplayNameInput) profileDisplayNameInput.value = displayName; |
|
|
if (profileImgUrlInput) profileImgUrlInput.value = photoURL; |
|
|
if (profileBioInput) profileBioInput.value = bio; |
|
|
|
|
|
|
|
|
updateImagePreviews(photoURL); |
|
|
|
|
|
|
|
|
if (currentUser && saveProfileBtn) { |
|
|
if (currentUser.isAnonymous) { |
|
|
if (socialLoginContainer) socialLoginContainer.classList.remove('hidden'); |
|
|
if (signOutContainer) signOutContainer.classList.add('hidden'); |
|
|
saveProfileBtn.classList.add('btn-disabled'); |
|
|
saveProfileBtn.disabled = true; |
|
|
saveProfileBtn.title = "Sign in to save profile"; |
|
|
} else { |
|
|
if (socialLoginContainer) socialLoginContainer.classList.add('hidden'); |
|
|
if (signOutContainer) signOutContainer.classList.remove('hidden'); |
|
|
saveProfileBtn.classList.remove('btn-disabled'); |
|
|
saveProfileBtn.disabled = false; |
|
|
saveProfileBtn.title = ""; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
function updateImagePreviews(url) { |
|
|
const validUrl = url && (url.startsWith('http') || url.startsWith('data:image')); |
|
|
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>`; |
|
|
|
|
|
if (profileHeaderImg && profileHeaderIcon) { |
|
|
if (validUrl) { |
|
|
profileHeaderImg.src = url; |
|
|
profileHeaderImg.classList.remove('hidden'); |
|
|
profileHeaderIcon.classList.add('hidden'); |
|
|
} else { |
|
|
profileHeaderImg.classList.add('hidden'); |
|
|
profileHeaderIcon.classList.remove('hidden'); |
|
|
} |
|
|
} |
|
|
|
|
|
if (profileImgPreview) { |
|
|
if (validUrl) { |
|
|
profileImgPreview.src = url; |
|
|
profileImgPreview.classList.remove('bg-slate-200', 'text-slate-400'); |
|
|
profileImgPreview.innerHTML = ''; |
|
|
} else { |
|
|
profileImgPreview.src = ''; |
|
|
profileImgPreview.classList.add('bg-slate-200', 'text-slate-400'); |
|
|
profileImgPreview.innerHTML = userIconSvg; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
function renderKnowledgeList() { |
|
|
if (!knowledgeListContainer) return; |
|
|
|
|
|
const items = knowledgeListContainer.querySelectorAll('.knowledge-item'); |
|
|
items.forEach(item => item.remove()); |
|
|
const urls = appState.knowledge?.urls || []; |
|
|
|
|
|
if (knowledgeEmptyState) { |
|
|
if (urls.length === 0) { |
|
|
knowledgeEmptyState.classList.remove('hidden'); |
|
|
} else { |
|
|
knowledgeEmptyState.classList.add('hidden'); |
|
|
} |
|
|
} |
|
|
|
|
|
urls.forEach((url, index) => { |
|
|
const div = document.createElement('div'); |
|
|
div.className = 'knowledge-item flex items-center justify-between rounded-md border border-slate-200 bg-white p-3'; |
|
|
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>`; |
|
|
knowledgeListContainer.appendChild(div); |
|
|
}); |
|
|
} |
|
|
|
|
|
function renderPromptList() { |
|
|
if (!promptListContainer) return; |
|
|
|
|
|
const items = promptListContainer.querySelectorAll('.prompt-item'); |
|
|
items.forEach(item => item.remove()); |
|
|
const prompts = appState.prompts || []; |
|
|
|
|
|
if (promptEmptyState) { |
|
|
if (prompts.length === 0) { |
|
|
promptEmptyState.classList.remove('hidden'); |
|
|
} else { |
|
|
promptEmptyState.classList.add('hidden'); |
|
|
} |
|
|
} |
|
|
|
|
|
prompts.forEach((prompt, index) => { |
|
|
const div = document.createElement('div'); |
|
|
div.className = 'prompt-item rounded-md border border-slate-200 p-4'; |
|
|
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>`; |
|
|
promptListContainer.appendChild(div); |
|
|
}); |
|
|
} |
|
|
|
|
|
function renderModelConfig() { |
|
|
const qhyProEl = document.getElementById('api-key-qhy-pro'); |
|
|
const geminiEl = document.getElementById('api-key-gemini-2.5'); |
|
|
|
|
|
if (qhyProEl) qhyProEl.classList.add('hidden'); |
|
|
if (geminiEl) geminiEl.classList.add('hidden'); |
|
|
|
|
|
const selected = appState.modelConfig?.selectedModel; |
|
|
if (selected === 'qhy-pro' && qhyProEl) { |
|
|
qhyProEl.classList.remove('hidden'); |
|
|
} else if (selected === 'gemini-2.5' && geminiEl) { |
|
|
geminiEl.classList.remove('hidden'); |
|
|
} |
|
|
} |
|
|
|
|
|
function renderTestChatMessage(sender, text) { |
|
|
if (!testAgentHistory) return; |
|
|
|
|
|
const messageDiv = document.createElement('div'); |
|
|
messageDiv.className = `chat-message ${sender} mb-2`; |
|
|
let html = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>'); |
|
|
html = html.replace(/`(.*?)`/g, '<code class="bg-slate-200 px-1 py-0.5 rounded text-sm">$1</code>'); |
|
|
html = html.replace(/\n/g, '<br>'); |
|
|
messageDiv.innerHTML = html; |
|
|
testAgentHistory.appendChild(messageDiv); |
|
|
testAgentHistory.scrollTop = testAgentHistory.scrollHeight; |
|
|
} |
|
|
|
|
|
function setChatTyping(isTyping) { |
|
|
if (!testAgentHistory) return; |
|
|
|
|
|
let typingEl = document.getElementById('typing-indicator'); |
|
|
if (isTyping) { |
|
|
if (!typingEl) { |
|
|
typingEl = document.createElement('div'); |
|
|
typingEl.id = 'typing-indicator'; |
|
|
typingEl.className = 'chat-message model typing'; |
|
|
typingEl.textContent = 'typing...'; |
|
|
testAgentHistory.appendChild(typingEl); |
|
|
testAgentHistory.scrollTop = testAgentHistory.scrollHeight; |
|
|
} |
|
|
} else { |
|
|
if (typingEl) { |
|
|
typingEl.remove(); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function showModal(modalEl) { |
|
|
if (!modalEl) return; |
|
|
modalEl.classList.remove('opacity-0', 'pointer-events-none'); |
|
|
const content = modalEl.querySelector('.modal-content'); |
|
|
if (content) content.classList.remove('-translate-y-10'); |
|
|
} |
|
|
|
|
|
function hideModal(modalEl) { |
|
|
if (!modalEl) return; |
|
|
modalEl.classList.add('opacity-0', 'pointer-events-none'); |
|
|
const content = modalEl.querySelector('.modal-content'); |
|
|
if (content) content.classList.add('-translate-y-10'); |
|
|
} |
|
|
|
|
|
function showLoading(message = "Generating...") { |
|
|
if (loadingMessage) loadingMessage.textContent = message; |
|
|
if (loadingModal) showModal(loadingModal); |
|
|
} |
|
|
|
|
|
function hideLoading() { |
|
|
if (loadingModal) hideModal(loadingModal); |
|
|
} |
|
|
|
|
|
function showPromptModal(mode = 'add', index = -1) { |
|
|
if (!promptModal || !promptModalTitle || !promptModalSaveBtn || |
|
|
!promptModalEditIndex || !promptModalTitleInput || !promptModalMessageInput) return; |
|
|
|
|
|
if (mode === 'edit' && index > -1) { |
|
|
const prompt = appState.prompts[index]; |
|
|
promptModalTitle.textContent = 'Edit Prompt'; |
|
|
promptModalSaveBtn.textContent = 'Save Changes'; |
|
|
promptModalEditIndex.value = index; |
|
|
promptModalTitleInput.value = prompt.title; |
|
|
promptModalMessageInput.value = prompt.message; |
|
|
} else { |
|
|
promptModalTitle.textContent = 'Add New Prompt'; |
|
|
promptModalSaveBtn.textContent = 'Save Prompt'; |
|
|
if (promptModalForm) promptModalForm.reset(); |
|
|
promptModalEditIndex.value = -1; |
|
|
} |
|
|
showModal(promptModal); |
|
|
} |
|
|
|
|
|
function toggleProfileSidebar() { |
|
|
isProfileSidebarOpen = !isProfileSidebarOpen; |
|
|
const sidebar = document.querySelector('custom-sidebar'); |
|
|
if (sidebar && sidebar.shadowRoot) { |
|
|
const profileSidebar = sidebar.shadowRoot.querySelector('#profile-sidebar'); |
|
|
const sidebarOverlay = sidebar.shadowRoot.querySelector('#sidebar-overlay'); |
|
|
|
|
|
if (profileSidebar && sidebarOverlay) { |
|
|
if (isProfileSidebarOpen) { |
|
|
profileSidebar.classList.add('open'); |
|
|
sidebarOverlay.classList.remove('opacity-0', 'pointer-events-none'); |
|
|
} else { |
|
|
profileSidebar.classList.remove('open'); |
|
|
sidebarOverlay.classList.add('opacity-0', 'pointer-events-none'); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
function openTestAgentModal() { |
|
|
testChatHistory = []; |
|
|
if (testAgentHistory) testAgentHistory.innerHTML = ''; |
|
|
renderTestChatMessage('model', 'Hi! I\'m ready to test. My instructions are set. Ask me anything.'); |
|
|
if (testAgentModal) showModal(testAgentModal); |
|
|
} |
|
|
|
|
|
function openImageGenModal() { |
|
|
if (imageGenInput) imageGenInput.value = ''; |
|
|
if (imageGenResult) { |
|
|
imageGenResult.src = ''; |
|
|
imageGenResult.alt = ''; |
|
|
} |
|
|
if (imageGenError) imageGenError.textContent = ''; |
|
|
if (imageGenResultContainer) imageGenResultContainer.classList.remove('loading'); |
|
|
if (imageGenResult) imageGenResult.classList.add('hidden'); |
|
|
if (imageGenError) imageGenError.classList.add('hidden'); |
|
|
if (imageGenModal) showModal(imageGenModal); |
|
|
} |
|
|
|
|
|
|
|
|
function openCodeTestModal() { |
|
|
if (codeTestResultContainer) codeTestResultContainer.classList.remove('loading'); |
|
|
if (codeTestResult) codeTestResult.textContent = '// Click "Generate Example" to see a code snippet...'; |
|
|
if (codeTestSpinner) codeTestSpinner.style.display = 'none'; |
|
|
if (codeTestResult) codeTestResult.style.display = 'block'; |
|
|
if (codeTestModal) showModal(codeTestModal); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function callGeminiApi(payload, maxRetries = 5) { |
|
|
const apiKey = ""; |
|
|
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${apiKey}`; |
|
|
let attempt = 0, delay = 1000; |
|
|
|
|
|
while (attempt < maxRetries) { |
|
|
try { |
|
|
const response = await fetch(apiUrl, { |
|
|
method: 'POST', |
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
body: JSON.stringify(payload) |
|
|
}); |
|
|
|
|
|
if (response.status === 429 || response.status >= 500) { |
|
|
throw new Error(`API Error: ${response.status}`); |
|
|
} |
|
|
|
|
|
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}`); |
|
|
} |
|
|
|
|
|
const result = await response.json(); |
|
|
const candidate = result.candidates?.[0]; |
|
|
|
|
|
if (!candidate) throw new Error("Invalid API response: No candidates found."); |
|
|
|
|
|
if (payload.generationConfig?.responseMimeType === "application/json") { |
|
|
const text = candidate.content?.parts?.[0]?.text; |
|
|
if (!text) throw new Error("Invalid API response: No text part found for JSON."); |
|
|
return text; |
|
|
} |
|
|
|
|
|
const text = candidate.content?.parts?.[0]?.text; |
|
|
if (typeof text !== 'string') throw new Error("Invalid API response: No text content found."); |
|
|
return text; |
|
|
} catch (error) { |
|
|
console.warn(`Gemini API call attempt ${attempt + 1} failed: ${error.message}`); |
|
|
attempt++; |
|
|
if (attempt >= maxRetries) throw new Error(`Gemini API failed after ${maxRetries} attempts: ${error.message}`); |
|
|
await new Promise(resolve => setTimeout(resolve, delay)); |
|
|
delay *= 2; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async function callImagenApi(userPrompt, maxRetries = 5) { |
|
|
const apiKey = ""; |
|
|
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/imagen-3.0-generate-002:predict?key=${apiKey}`; |
|
|
|
|
|
const payload = { |
|
|
instances: { prompt: userPrompt }, |
|
|
parameters: { "sampleCount": 1 } |
|
|
}; |
|
|
|
|
|
let attempt = 0, delay = 1000; |
|
|
while (attempt < maxRetries) { |
|
|
try { |
|
|
const response = await fetch(apiUrl, { |
|
|
method: 'POST', |
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
body: JSON.stringify(payload) |
|
|
}); |
|
|
|
|
|
if (response.status === 429 || response.status >= 500) { |
|
|
throw new Error(`API Error: ${response.status}`); |
|
|
} |
|
|
|
|
|
if (!response.ok) { |
|
|
const errorResult = await response.json(); |
|
|
console.error("Imagen API Error:", errorResult); |
|
|
throw new Error(`Imagen API Error: ${errorResult.error?.message || response.statusText}`); |
|
|
} |
|
|
|
|
|
const result = await response.json(); |
|
|
|
|
|
if (result.predictions && result.predictions.length > 0 && result.predictions[0].bytesBase64Encoded) { |
|
|
return `data:image/png;base64,${result.predictions[0].bytesBase64Encoded}`; |
|
|
} else { |
|
|
throw new Error("Invalid API response: No image data found."); |
|
|
} |
|
|
|
|
|
} catch (error) { |
|
|
console.warn(`Imagen API call attempt ${attempt + 1} failed: ${error.message}`); |
|
|
attempt++; |
|
|
if (attempt >= maxRetries) { |
|
|
throw new Error(`Imagen API failed after ${maxRetries} attempts: ${error.message}`); |
|
|
} |
|
|
await new Promise(resolve => setTimeout(resolve, delay)); |
|
|
delay *= 2; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async function callTtsApi(textToSpeak, maxRetries = 5) { |
|
|
const apiKey = ""; |
|
|
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-tts:generateContent?key=${apiKey}`; |
|
|
|
|
|
const payload = { |
|
|
contents: [{ |
|
|
parts: [{ text: textToSpeak }] |
|
|
}], |
|
|
generationConfig: { |
|
|
responseModalities: ["AUDIO"], |
|
|
speechConfig: { |
|
|
voiceConfig: { |
|
|
prebuiltVoiceConfig: { voiceName: "Kore" } |
|
|
} |
|
|
} |
|
|
}, |
|
|
model: "gemini-2.5-flash-preview-tts" |
|
|
}; |
|
|
|
|
|
let attempt = 0, delay = 1000; |
|
|
while (attempt < maxRetries) { |
|
|
try { |
|
|
const response = await fetch(apiUrl, { |
|
|
method: 'POST', |
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
body: JSON.stringify(payload) |
|
|
}); |
|
|
|
|
|
if (response.status === 429 || response.status >= 500) throw new Error(`API Error: ${response.status}`); |
|
|
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}`); |
|
|
} |
|
|
|
|
|
const result = await response.json(); |
|
|
const part = result?.candidates?.[0]?.content?.parts?.[0]; |
|
|
const audioData = part?.inlineData?.data; |
|
|
const mimeType = part?.inlineData?.mimeType; |
|
|
|
|
|
if (audioData && mimeType && mimeType.startsWith("audio/")) { |
|
|
const sampleRate = parseInt(mimeType.match(/rate=(\d+)/)[1], 10) || 24000; |
|
|
return { audioData, sampleRate }; |
|
|
} else { |
|
|
throw new Error("Invalid API response: No audio data found."); |
|
|
} |
|
|
} catch (error) { |
|
|
console.warn(`TTS API call attempt ${attempt + 1} failed: ${error.message}`); |
|
|
attempt++; |
|
|
if (attempt >= maxRetries) throw new Error(`TTS API failed after ${maxRetries} attempts: ${error.message}`); |
|
|
await new Promise(resolve => setTimeout(resolve, delay)); |
|
|
delay *= 2; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function base64ToArrayBuffer(base64) { |
|
|
const binaryString = window.atob(base64); |
|
|
const len = binaryString.length; |
|
|
const bytes = new Uint8Array(len); |
|
|
for (let i = 0; i < len; i++) { |
|
|
bytes[i] = binaryString.charCodeAt(i); |
|
|
} |
|
|
return bytes.buffer; |
|
|
} |
|
|
|
|
|
|
|
|
function pcmToWav(pcmData, sampleRate) { |
|
|
const numChannels = 1; |
|
|
const bitsPerSample = 16; |
|
|
const byteRate = sampleRate * numChannels * (bitsPerSample / 8); |
|
|
const blockAlign = numChannels * (bitsPerSample / 8); |
|
|
const dataSize = pcmData.byteLength; |
|
|
const fileSize = 36 + dataSize; |
|
|
|
|
|
const buffer = new ArrayBuffer(44 + dataSize); |
|
|
const view = new DataView(buffer); |
|
|
|
|
|
|
|
|
writeString(view, 0, 'RIFF'); |
|
|
view.setUint32(4, fileSize, true); |
|
|
writeString(view, 8, 'WAVE'); |
|
|
|
|
|
|
|
|
writeString(view, 12, 'fmt '); |
|
|
view.setUint32(16, 16, true); |
|
|
view.setUint16(20, 1, true); |
|
|
view.setUint16(22, numChannels, true); |
|
|
view.setUint32(24, sampleRate, true); |
|
|
view.setUint32(28, byteRate, true); |
|
|
view.setUint16(32, blockAlign, true); |
|
|
view.setUint16(34, bitsPerSample, true); |
|
|
|
|
|
|
|
|
writeString(view, 36, 'data'); |
|
|
view.setUint32(40, dataSize, true); |
|
|
|
|
|
|
|
|
new Uint8Array(buffer, 44).set(new Uint8Array(pcmData)); |
|
|
|
|
|
return new Blob([buffer], { type: 'audio/wav' }); |
|
|
} |
|
|
|
|
|
|
|
|
function writeString(view, offset, string) { |
|
|
for (let i = 0; i < string.length; i++) { |
|
|
view.setUint8(offset + i, string.charCodeAt(i)); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function loadAgentConfig() { |
|
|
if (!agentConfigDocRef) return; |
|
|
try { |
|
|
const docSnap = await getDoc(agentConfigDocRef); |
|
|
if (docSnap.exists()) { |
|
|
console.log("Loaded data from Firestore:", docSnap.data()); |
|
|
appState = { ...defaultAppState, ...docSnap.data() }; |
|
|
} else { |
|
|
console.log("No saved config found, using default."); |
|
|
appState = { ...defaultAppState }; |
|
|
} |
|
|
} catch (error) { |
|
|
console.error("Error loading agent config:", error); |
|
|
appState = { ...defaultAppState }; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async function signInWithGoogle() { |
|
|
const provider = new GoogleAuthProvider(); |
|
|
try { |
|
|
await signInWithPopup(auth, provider); |
|
|
toggleProfileSidebar(); |
|
|
} catch (error) { |
|
|
console.error("Google Sign-In Error:", error); |
|
|
} |
|
|
} |
|
|
|
|
|
async function signInWithGitHub() { |
|
|
const provider = new GithubAuthProvider(); |
|
|
try { |
|
|
await signInWithPopup(auth, provider); |
|
|
toggleProfileSidebar(); |
|
|
} catch (error) { |
|
|
console.error("GitHub Sign-In Error:", error); |
|
|
} |
|
|
} |
|
|
|
|
|
async function handleSignOut() { |
|
|
try { |
|
|
await signOut(auth); |
|
|
toggleProfileSidebar(); |
|
|
} catch (error) { |
|
|
console.error("Sign-Out Error:", error); |
|
|
} |
|
|
} |
|
|
|
|
|
async function handleSaveProfile() { |
|
|
const currentUser = auth.currentUser; |
|
|
if (!currentUser || currentUser.isAnonymous || |
|
|
!profileDisplayNameInput || !profileImgUrlInput || !profileBioInput || |
|
|
!saveProfileBtn || !agentConfigDocRef) return; |
|
|
|
|
|
const displayName = profileDisplayNameInput.value.trim(); |
|
|
const photoURL = profileImgUrlInput.value.trim(); |
|
|
const bio = profileBioInput.value.trim(); |
|
|
|
|
|
saveProfileBtn.textContent = "Saving..."; |
|
|
saveProfileBtn.disabled = true; |
|
|
saveProfileBtn.classList.add('btn-disabled'); |
|
|
|
|
|
try { |
|
|
await updateProfile(currentUser, { displayName, photoURL }); |
|
|
const profileData = { displayName, photoURL, bio }; |
|
|
await setDoc(agentConfigDocRef, profileData, { merge: true }); |
|
|
appState.displayName = displayName; |
|
|
appState.photoURL = photoURL; |
|
|
appState.bio = bio; |
|
|
updateImagePreviews(photoURL); |
|
|
saveProfileBtn.textContent = "Profile Saved!"; |
|
|
saveProfileBtn.classList.add('bg-green-600'); |
|
|
|
|
|
setTimeout(() => { |
|
|
saveProfileBtn.textContent = "Save Profile"; |
|
|
saveProfileBtn.disabled = false; |
|
|
saveProfileBtn.classList.remove('btn-disabled', 'bg-green-600'); |
|
|
}, 2000); |
|
|
} catch (error) { |
|
|
console.error("Error saving profile:", error); |
|
|
saveProfileBtn.textContent = "Save Failed"; |
|
|
saveProfileBtn.classList.add('bg-red-600'); |
|
|
|
|
|
setTimeout(() => { |
|
|
saveProfileBtn.textContent = "Save Profile"; |
|
|
saveProfileBtn.disabled = false; |
|
|
saveProfileBtn.classList.remove('btn-disabled', 'bg-red-600'); |
|
|
}, 2000); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
async function handleSuggestBio() { |
|
|
if (!profileDisplayNameInput || !profileBioInput) return; |
|
|
|
|
|
const agentRole = profileDisplayNameInput.value.trim(); |
|
|
if (!agentRole) { |
|
|
profileBioInput.placeholder = "Please enter a Display Name first, then click β¨ Suggest."; |
|
|
return; |
|
|
} |
|
|
|
|
|
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.`; |
|
|
const payload = { |
|
|
contents: [{ parts: [{ text: userQuery }] }], |
|
|
systemInstruction: { parts: [{ text: "You are a professional profile writer. You return only the bio text." }] } |
|
|
}; |
|
|
|
|
|
showLoading("Generating bio..."); |
|
|
try { |
|
|
const generatedText = await callGeminiApi(payload); |
|
|
const cleanText = generatedText.replace(/["*]/g, "").trim(); |
|
|
profileBioInput.value = cleanText; |
|
|
} catch (error) { |
|
|
console.error(error); |
|
|
profileBioInput.value = `Error: ${error.message}`; |
|
|
} finally { |
|
|
hideLoading(); |
|
|
} |
|
|
} |
|
|
|
|
|
async function handleTestAgentSend(e) { |
|
|
e.preventDefault(); |
|
|
if (!testAgentInput || !testAgentHistory) return; |
|
|
|
|
|
const userMessage = testAgentInput.value.trim(); |
|
|
if (!userMessage) return; |
|
|
|
|
|
testAgentInput.value = ''; |
|
|
renderTestChatMessage('user', userMessage); |
|
|
setChatTyping(true); |
|
|
testChatHistory.push({ role: "user", parts: [{ text: userMessage }] }); |
|
|
|
|
|
let finalSystemInstructions = appState.instructions || "You are a helpful assistant."; |
|
|
let payloadTools = []; |
|
|
|
|
|
if (appState.knowledge.urls.length > 0 && appState.knowledge.prioritize) { |
|
|
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.`; |
|
|
payloadTools.push({ "google_search": {} }); |
|
|
} |
|
|
|
|
|
const payload = { |
|
|
contents: testChatHistory, |
|
|
systemInstruction: { parts: [{ text: finalSystemInstructions }] } |
|
|
}; |
|
|
|
|
|
if (payloadTools.length > 0) { |
|
|
payload.tools = payloadTools; |
|
|
} |
|
|
|
|
|
try { |
|
|
const modelResponse = await callGeminiApi(payload); |
|
|
testChatHistory.push({ role: "model", parts: [{ text: modelResponse }] }); |
|
|
setChatTyping(false); |
|
|
renderTestChatMessage('model', modelResponse); |
|
|
} catch (error) { |
|
|
console.error("Test Agent Chat Error:", error); |
|
|
setChatTyping(false); |
|
|
renderTestChatMessage('model', `Sorry, an error occurred: ${error.message}`); |
|
|
} |
|
|
} |
|
|
|
|
|
async function handleImageGenTest(e) { |
|
|
e.preventDefault(); |
|
|
if (!imageGenInput || !imageGenResultContainer || !imageGenError || |
|
|
!imageGenResult || !imageGenSpinner) return; |
|
|
|
|
|
const userPrompt = imageGenInput.value.trim(); |
|
|
if (!userPrompt) return; |
|
|
|
|
|
imageGenResultContainer.classList.add('loading'); |
|
|
imageGenError.textContent = ''; |
|
|
imageGenError.classList.add('hidden'); |
|
|
imageGenResult.classList.add('hidden'); |
|
|
|
|
|
try { |
|
|
const imageUrl = await callImagenApi(userPrompt); |
|
|
imageGenResult.src = imageUrl; |
|
|
imageGenResult.alt = userPrompt; |
|
|
imageGenResult.classList.remove('hidden'); |
|
|
} catch (error) { |
|
|
console.error("Image Gen Test Error:", error); |
|
|
imageGenError.textContent = `Error: ${error.message}`; |
|
|
imageGenError.classList.remove('hidden'); |
|
|
} finally { |
|
|
imageGenResultContainer.classList.remove('loading'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async function handleListenInstructions() { |
|
|
if (!agentInstructionsInput || !listenInstructionsBtn || !ttsPlayer) return; |
|
|
|
|
|
const text = agentInstructionsInput.value.trim(); |
|
|
if (!text) return; |
|
|
|
|
|
const icon = listenInstructionsBtn.querySelector('svg'); |
|
|
listenInstructionsBtn.disabled = true; |
|
|
if (icon) icon.classList.add('spinner'); |
|
|
|
|
|
try { |
|
|
const { audioData, sampleRate } = await callTtsApi(text); |
|
|
const pcmBuffer = base64ToArrayBuffer(audioData); |
|
|
const wavBlob = pcmToWav(pcmBuffer, sampleRate); |
|
|
const audioUrl = URL.createObjectURL(wavBlob); |
|
|
|
|
|
ttsPlayer.src = audioUrl; |
|
|
ttsPlayer.play(); |
|
|
|
|
|
} catch (error) { |
|
|
console.error("TTS Error:", error); |
|
|
} finally { |
|
|
listenInstructionsBtn.disabled = false; |
|
|
if (icon) icon.classList.remove('spinner'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async function handleCodeInterpreterTest() { |
|
|
if (!codeTestResultContainer || !codeTestSpinner || !codeTestResult) return; |
|
|
|
|
|
codeTestResultContainer.classList.add('loading'); |
|
|
codeTestSpinner.style.display = 'flex'; |
|
|
codeTestResult.style.display = 'none'; |
|
|
|
|
|
const agentRole = appState.agentName.trim() || "a helpful assistant"; |
|
|
const userQuery = `You are a code interpreter. Your agent role is "${agentRole}". |
|
|
Generate a simple Python code snippet that demonstrates this role. |
|
|
Return ONLY the Python code, inside \`\`\`python ... \`\`\` tags.`; |
|
|
|
|
|
const payload = { |
|
|
contents: [{ parts: [{ text: userQuery }] }], |
|
|
systemInstruction: { parts: [{ text: "You are a helpful code generation assistant. You only return code." }] } |
|
|
}; |
|
|
|
|
|
try { |
|
|
const generatedText = await callGeminiApi(payload); |
|
|
|
|
|
const cleanCode = generatedText.replace(/^ |