1const chatBody = document.getElementById(`chatBody`); 2const userInput = document.getElementById("userInput"); 3const codeButton = document.querySelector(".code"); 4const box_conversations = document.querySelector(`#box_conversations, .top`); 5const stop_generating = document.querySelector(`.stop_generating`); 6const regenerate_button = document.querySelector(`.regenerate`); 7const sidebar = document.querySelector(".sidebar"); 8const sidebar_buttons = document.querySelectorAll(".mobile-sidebar-toggle"); 9const sendButton = document.getElementById("sendButton"); 10const addButton = document.getElementById("addButton"); 11const imageInput = document.querySelector(".image-label"); 12const mediaSelect = document.querySelector(".media-select"); 13const imageSelect = document.getElementById("image"); 14const cameraInput = document.getElementById("camera"); 15const audioButton = document.querySelector(".capture-audio"); 16const linkButton = document.querySelector(".add-link"); 17const fileInput = document.getElementById("file"); 18const microLabel = document.querySelector(".micro-label"); 19const inputCount = document.getElementById("input-count").querySelector(".text"); 20const providerSelect = document.getElementById("provider"); 21const modelSelect = document.getElementById("model"); 22const modelSearch = document.getElementById("model-search"); 23const modelSelector = document.querySelector(".model-selector"); 24const modelSuggestions = document.getElementById('model-suggestions'); 25const chatPrompt = document.getElementById("chatPrompt"); 26const settings = document.querySelector(".settings"); 27const settingsContent = settings.querySelector(".settings-content") || settings.querySelector(".paper"); 28const chat = document.querySelector(".chat-container"); 29const album = document.querySelector(".images"); 30const searchButton = document.getElementById("search"); 31const paperclip = document.querySelector(".user-input .fa-paperclip"); 32const userInputHeight = document.getElementById("userInput-height"); 33const hide_systemPrompt = document.getElementById("hide-systemPrompt") 34const slide_systemPrompt_icon = document.querySelector(".slide-header i"); 35 36const optionElementsSelector = ".settings input, .settings textarea, .chat-body input, #model, #provider"; 37 38const translationSnipptes = [ 39 "with", "**An error occurred:**", "Private Conversation", "New Conversation", "Regenerate", "Continue", 40 "Hello! How can I assist you today?", "words", "chars", "tokens", "{0} total tokens", 41 "{0} Messages were imported", "{0} File(s) uploaded successfully", 42 "{0} Conversations/Settings were imported successfully", 43 "No content found", "Files are loaded successfully", 44 "Importing conversations...", "New version:", "Providers API key", "Providers (Enable/Disable)", 45 "Get API key", "Uploading files...", "Invalid link", "Loading...", "Live Providers", "Custom Providers", 46 "Search Off", "Search On", "Recognition On", "Recognition Off", "Delete Conversation", 47 "Favorite Models:", "Stop Recording", "Record Audio", "Upload Audio", "No Title", "1 Copy", 48 "Delete all conversations?", "Error Occurred", "Remaining:", "Balance:", "Reasoning", "Credits:", 49 "Login", "Login to", "Enable", "Invalid API key", "Waiting for tool response..." 50]; 51 52let providers = [ 53 {"name": "ApiAirforce", "label": "Api.Airforce", "login_url": "https://panel.api.airforce/dashboard", "active_by_default": true}, 54 {"name": "HuggingFace", "login_url": "https://huggingface.co/settings/tokens", "active_by_default": true}, 55 {"name": "HuggingFaceMedia", "parent": "HuggingFace", "active_by_default": true}, 56 {"name": "PollinationsAI", "label": "Pollinations AI", "login_url": "https://enter.pollinations.ai", "active_by_default": true}, 57 {"name": "PuterJS", "label": "Puter.js", "login_url": "https://discord.gg/qXA4Wf4Fsm", "active_by_default": true}, 58]; 59 60const modelTags = { 61 image: "ð¨", 62 "image-edit": "ð¨", 63 vision: "ð", 64 audio: "ð§", 65 video: "ð¥", 66 paid_only: "ð°", 67 free: "ð", 68} 69 70document.addEventListener("DOMContentLoaded", (event) => {
71 translationSnipptes.forEach((text) => framework.translate(text)); 72 73 // Listen for user tier updates from API responses 74 window.addEventListener('userTierUpdate', (event) => { 75 const userInfo = event.detail; 76 const infoBar = document.getElementById('user-tier-info'); 77 const tierText = document.getElementById('user-tier-text'); 78 const maxTokensText = document.getElementById('max-tokens-text'); 79 const maxRequestsText = document.getElementById('max-requests-text'); 80 const tierLimitsRow = document.getElementById('tier-limits-row'); 81 82 if (infoBar && (userInfo.tier || userInfo.remainingTokens !== null || userInfo.remainingRequests !== null)) { 83 if (userInfo.tier) { 84 infoBar.setAttribute('data-tier', userInfo.tier); 85 // Only update tier text if user is not logged in (keep username if logged in) 86 const sidebarLogoutBtn = document.getElementById('sidebar-logout-btn'); 87 if (sidebarLogoutBtn && sidebarLogoutBtn.classList.contains('hidden')) { 88 if (tierText) tierText.textContent = userInfo.tier; 89 } 90 } 91 if (maxTokensText && (userInfo.remainingTokens !== null || userInfo.limitTokens !== null)) { 92 const remaining = userInfo.remainingTokens !== null ? formatNumber(userInfo.remainingTokens) : '-'; 93 const limit = userInfo.limitTokens !== null ? formatNumber(userInfo.limitTokens) : '-'; 94 maxTokensText.innerHTML = `<i class="fa-solid fa-coins" aria-hidden="true"></i> ${remaining}/${limit}`; 95 maxTokensText.title = `Tokens: ${remaining} remaining of ${limit}`; 96 if (tierLimitsRow) tierLimitsRow.classList.remove('hidden'); 97 } 98 if (maxRequestsText && (userInfo.remainingRequests !== null || userInfo.limitRequests !== null)) { 99 const remaining = userInfo.remainingRequests !== null ? userInfo.remainingRequests : '-'; 100 const limit = userInfo.limitRequests !== null ? userInfo.limitRequests : '-'; 101 maxRequestsText.innerHTML = `<i class="fa-solid fa-list" aria-hidden="true"></i> ${remaining}/${limit}`; 102 maxRequestsText.title = `Requests: ${remaining} remaining of ${limit}`; 103 if (tierLimitsRow) tierLimitsRow.classList.remove('hidden'); 104 } 105 } 106 }); 107 108 function formatNumber(num) { 109 if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M'; 110 if (num >= 1000) return (num / 1000).toFixed(0) + 'K'; 111 return (Math.round(num * 10) / 10).toString(); 112 } 113 114 // Settings tabs functionality 115 const settingsTabs = document.querySelectorAll('.settings-tab'); 116 const tabContents = document.querySelectorAll('.settings-tab-content'); 117
118 settingsTabs.forEach(tab => { 119 tab.addEventListener('click', () => { 120 const targetTab = tab.dataset.tab; 121 122 // Update active tab button 123 settingsTabs.forEach(t => { 124 t.classList.remove('active'); 125 t.setAttribute('aria-selected', 'false'); 126 }); 127 tab.classList.add('active'); 128 tab.setAttribute('aria-selected', 'true'); 129 130 // Update active tab content 131 tabContents.forEach(content => { 132 content.classList.remove('active'); 133 }); 134 const targetContent = document.getElementById(`tab-${targetTab}`); 135 if (targetContent) { 136 targetContent.classList.add('active'); 137 } 138 139 // Save active tab to appStorage 140 appStorage.setItem('settings-active-tab', targetTab); 141 }); 142 }); 143 144 // Restore last active tab 145 const savedTab = appStorage.getItem('settings-active-tab'); 146 if (savedTab) { 147 const tabButton = document.querySelector(`.settings-tab[data-tab="${savedTab}"]`); 148 if (tabButton) { 149 tabButton.click(); 150 } 151 } 152 153 // Load voice models for the voice select dropdown 154 loadVoiceModels(); 155}); 156 157let provider_storage = {}; 158let message_storage = {}; 159let content_alt_storage = {}; 160let content_data_storage = {}; 161let controller_storage = {}; 162let content_storage = {}; 163let error_storage = {}; 164let synthesize_storage = {}; 165let title_storage = {}; 166let parameters_storage = {}; 167let finish_storage = {}; 168let usage_storage = {}; 169let continue_storage = {}; 170let reasoning_storage = {}; 171let variant_storage = {}; 172let debug_response_counter = {} 173let title_ids_storage = {}; 174let image_storage = {}; 175let headers_storage = {}; 176let wakeLock = null; 177let countTokensEnabled = true; 178let suggestions = null; 179let tool_calls_storage = {}; 180let startup_questions = []; 181let lastUpdated = null; 182let mediaRecorder = null; 183let stopRecognition = ()=>{}; 184let providerModelSignal = null; 185let searchModels = {}; 186let client = null; 187let voicePreviewAudio = null; 188 189appStorage = window.localStorage || { 190 setItem: (key, value) => self[key] = value, 191 getItem: (key) => self[key], 192 removeItem: (key) => delete self[key], 193 length: 0 194} 195 196// Load voice models from API and populate voice select dropdown 197async function loadVoiceModels() { 198 const voiceSelect = document.getElementById('voice'); 199 if (!voiceSelect) return; 200 201 try { 202 const response = await fetch('https://g4f.space/api/audio/models'); 203 if (!response.ok) { 204 throw new Error('Failed to fetch voice models'); 205 } 206 const data = await response.json(); 207 208 // Clear existing options 209 voiceSelect.innerHTML = ''; 210 211 // Populate with voice models 212 if (data.data && Array.isArray(data.data)) { 213 data.data.forEach(voice => { 214 const option = document.createElement('option'); 215 option.selected = voice.id === "gpt-audio" ? true : false; 216 option.value = voice.id === "gpt-audio" ? "" : voice.id; 217 option.textContent = voice.id === "gpt-audio" ? "Default (gemini)" : voice.name || voice.id; 218 voiceSelect.appendChild(option); 219 }); 220 } else if (Array.isArray(data)) { 221 data.forEach(voice => { 222 const option = document.createElement('option'); 223 option.value = typeof voice === 'string' ? voice : voice.name || voice.id; 224 option.textContent = typeof voice === 'string' ? voice : voice.name || voice.id; 225 voiceSelect.appendChild(option); 226 }); 227 } 228 229 // Restore saved voice selection 230 const savedVoice = appStorage.getItem('voice'); 231 if (savedVoice) { 232 voiceSelect.value = savedVoice; 233 } 234 235 // Add change event listener to play preview and save selection 236 voiceSelect.addEventListener('change', async (event) => { 237 const selectedVoice = event.target.value; 238 appStorage.setItem('voice', selectedVoice); 239 240 if (selectedVoice) { 241 playVoicePreview(selectedVoice); 242 } 243 }); 244 } catch (error) { 245 console.error('Error loading voice models:', error); 246 voiceSelect.innerHTML = '<option value="">Failed to load voices</option>'; 247 } 248} 249 250// Play a preview of the selected voice 251async function playVoicePreview(voice) { 252 // Stop any currently playing preview 253 if (voicePreviewAudio) { 254 voicePreviewAudio.pause(); 255 voicePreviewAudio = null; 256 } 257 258 const previewText = 'Hello, how are you?'; 259 const audioUrl = `https://g4f.space/ai/audio/${encodeURIComponent(previewText)}?voice=${encodeURIComponent(voice)}`; 260 const response = await fetch(audioUrl, { 261 headers: appStorage.getItem("session_token") ? { 262 'Authorization': `Bearer ${appStorage.getItem("session_token")}` 263 } : {} 264 }); 265 const object = await response.blob(); 266 voicePreviewAudio = new Audio(URL.createObjectURL(object)); 267 voicePreviewAudio.play().catch(error => { 268 console.error('Error playing voice preview:', error); 269 }); 270} 271 272function render_reasoning(reasoning, final = false) { 273 const inner_text = reasoning.text ? `<div class="reasoning_text${final ? " final hidden" : ""}"> 274 ${renderer(reasoning.text)} 275 </div>` : ""; 276 return `<div class="reasoning_body"> 277 <div class="reasoning_title"> 278 <strong>${reasoning.label ? reasoning.label : framework.translate('Reasoning') + ' <i class="brain">ð§ </i>'}: </strong> 279 ${typeof reasoning.status === 'string' ? framework.escape(reasoning.status) : '<i class="fas fa-spinner fa-spin"></i>'} 280 </div> 281 ${inner_text} 282 </div>`; 283} 284 285function render_reasoning_text(reasoning) { 286 return `${reasoning.label ? reasoning.label : framework.translate('Reasoning') + ' ð§ '}: ${reasoning.status}\n\n${reasoning.text}\n\n`; 287} 288 289function filter_message(text) { 290 if (Array.isArray(text) || !text) { 291 return text; 292 } 293 // Remove images from text 294 return filter_message_content(text.replaceAll( 295 /!\[.*?\]\(.*?\)/gm, "" 296 )) 297} 298 299function filter_message_content(text) { 300 if (Array.isArray(text) || !text) { 301 return text; 302 } 303 return text.replace(/ \[aborted\]$/g, "").replace(/ \[error\]$/g, "") 304} 305 306function fallback_clipboard (text) { 307 var textBox = document.createElement("textarea"); 308 textBox.value = text; 309 textBox.style.top = "0"; 310 textBox.style.left = "0"; 311 textBox.style.position = "fixed"; 312 document.body.appendChild(textBox); 313 textBox.focus(); 314 textBox.select(); 315 try { 316 var success = document.execCommand('copy'); 317 var msg = success ? 'succeeded' : 'failed'; 318 console.log('Clipboard Fallback: Copying text command ' + msg); 319 } catch (e) { 320 console.error('Clipboard Fallback: Unable to copy', e); 321 } 322 document.body.removeChild(textBox); 323} 324 325const iframe_container = document.querySelector(".hljs-iframe-container"); 326const iframe = document.querySelector(".hljs-iframe"); 327const iframe_close = Object.assign(document.createElement("button"), { 328 className: "hljs-iframe-close", 329 innerHTML: '<i class="fa-regular fa-x"></i>', 330}); 331iframe_close.onclick = () => { 332 iframe_container.classList.add("hidden"); 333 iframe.src = ""; 334} 335iframe_container.appendChild(iframe_close); 336 337class HtmlRenderPlugin { 338 constructor(options = {}) { 339 self.hook = options.hook; 340 self.callback = options.callback 341 } 342 "after:highlightElement"({ 343 el, 344 text 345 }) { 346 if (!el.classList.contains("language-html") && !el.classList.contains("language-svg")) { 347 return; 348 } 349 let button = Object.assign(document.createElement("button"), { 350 innerHTML: '<i class="fa-regular fa-folder-open"></i>', 351 className: "hljs-iframe-button", 352 }); 353 el.parentElement.appendChild(button); 354 button.onclick = async () => { 355 let newText = text; 356 if (hook && typeof hook === "function") { 357 newText = hook(text, el) || text 358 } 359 const mimeType = el.classList.contains("language-svg") ? "image/svg+xml" : "text/html"; 360 iframe.src = `data:${mimeType};charset=utf-8,${encodeURIComponent(newText)}`; 361 iframe_container.classList.remove("hidden"); 362 if (typeof callback === "function") return callback(newText, el); 363 } 364 } 365} 366let typesetPromise = Promise.resolve(); 367let hljs_loaded = false;
368const highlight = (container) => { 369 if (window.hljs) { 370 if (window.hljs && !hljs_loaded) { 371 hljs.addPlugin(new HtmlRenderPlugin()); 372 if (typeof CopyButtonPlugin === 'function') { 373 hljs.addPlugin(new CopyButtonPlugin()); 374 hljs_loaded = true; 375 } 376 } 377 container.querySelectorAll('code:not(.hljs').forEach((el) => { 378 if (el.className != "hljs") { 379 hljs.highlightElement(el); 380 } 381 }); 382 } 383 if (window.MathJax && window.MathJax.typesetPromise) { 384 typesetPromise = typesetPromise.then( 385 () => MathJax.typesetPromise([container]) 386 ).catch( 387 (err) => console.log('Typeset failed: ' + err.message) 388 ); 389 } 390} 391 392const get_message_el = (el) => { 393 let message_el = el; 394 while(!(message_el.classList.contains('message')) && message_el.parentElement) { 395 message_el = message_el.parentElement; 396 } 397 if (message_el.classList.contains('message')) { 398 return message_el; 399 } 400} 401 402function generateUUID() { 403 if (crypto.randomUUID) { 404 return crypto.randomUUID(); 405 } 406 return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { 407 const r = crypto.getRandomValues(new Uint8Array(1))[0] % 16; 408 return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); 409 }); 410} 411 412function register_message_images() { 413 chatBody.querySelectorAll(".message .fa-clipboard").forEach(async (el) => { 414 if (el.dataset.click) { 415 return 416 } 417 el.dataset.click = true; 418 el.addEventListener("click", async () => { 419 let message_el = get_message_el(el); 420 let response = await fetch(message_el.dataset.object_url); 421 let copyText = await response.text(); 422 423 try { 424 if (!navigator.clipboard) { 425 throw new Error("navigator.clipboard: Clipboard API unavailable."); 426 } 427 await navigator.clipboard.writeText(copyText); 428 showNotification("Text copied to clipboard"); 429 } catch (e) { 430 console.error(e); 431 console.error("Clipboard API writeText() failed! Fallback to document.exec(\"copy\")..."); 432 try { 433 fallback_clipboard(copyText); 434 showNotification("Text copied to clipboard"); 435 } catch (fallbackError) { 436 console.error("Fallback clipboard also failed:", fallbackError); 437 showNotification("Failed to copy text", "error"); 438 } 439 } 440 el.classList.add("clicked"); 441 setTimeout(() => el.classList.remove("clicked"), 1000); 442 }); 443 }); 444} 445 446function showToast(message, type = 'info', duration = 2000) { 447 showNotification(message, type, duration); 448 // duration currently controlled by showNotification animation, but we keep param compatibility. 449} 450 451function showOAuthCodePrompt(userCode, verificationUri) { 452 const existingPrompt = document.getElementById('oauth-code-prompt'); 453 if (existingPrompt) existingPrompt.remove(); 454 455 const prompt = document.createElement('div'); 456 prompt.id = 'oauth-code-prompt'; 457 prompt.style.position = 'fixed'; 458 prompt.style.bottom = '20px'; 459 prompt.style.left = '20px'; 460 prompt.style.zIndex = '10000'; 461 prompt.style.backgroundColor = '#111'; 462 prompt.style.color = '#fff'; 463 prompt.style.padding = '12px'; 464 prompt.style.borderRadius = '8px'; 465 prompt.style.boxShadow = '0 8px 20px rgba(0,0,0,0.5)'; 466 prompt.style.minWidth = '300px'; 467 468 prompt.innerHTML = ` 469 <div style="font-weight:700; margin-bottom:8px;">GitHub Copilot Login</div> 470 <div style="margin-bottom:6px;">Enter this code at GitHub:</div> 471 <div id="oauth-user-code" style="font-size:1.2rem; font-weight:700; letter-spacing:0.1em; background:#222; padding:8px; border-radius:4px; word-break:break-all;">${framework.escape(userCode)}</div> 472 <div style="display:flex; gap:6px; margin-top:8px;"> 473 <button id="oauth-copy-code" style="flex:1; padding:8px; background:#2563eb; border:none;
473 color:#fff; border-radius:4px; cursor:pointer;">Copy code</button> 474 <button id="oauth-open-url" style="flex:1; padding:8px; background:#059669; border:none; color:#fff; border-radius:4px; cursor:pointer;">Open GitHub</button> 475 </div> 476 <div style="text-align:right; margin-top:8px;"><button id="oauth-close" style="color:#aaa; background:transparent; border:none; cursor:pointer;">Close</button></div> 477 `; 478 479 document.body.appendChild(prompt); 480 481 prompt.querySelector('#oauth-copy-code').addEventListener('click', async () => { 482 try { 483 await navigator.clipboard.writeText(userCode); 484 showNotification('Code copied to clipboard', 'success'); 485 } catch (copyErr) { 486 showNotification('Copy failed', 'error'); 487 } 488 }); 489 490 prompt.querySelector('#oauth-open-url').addEventListener('click', () => { 491 window.open(verificationUri, '_blank'); 492 }); 493 494 prompt.querySelector('#oauth-close').addEventListener('click', () => { 495 prompt.remove(); 496 }); 497} 498 499function showNotification(message, type = 'success', duration = 2000) { 500 // Check if notification container exists, create if not 501 let container = document.getElementById('notification-container'); 502 if (!container) { 503 container = document.createElement('div'); 504 container.id = 'notification-container'; 505 container.style.position = 'fixed'; 506 container.style.bottom = '20px'; 507 container.style.right = '20px'; 508 container.style.zIndex = '9999'; 509 document.body.appendChild(container); 510 } 511 512 const notification = document.createElement('div'); 513 notification.className = `notification ${type}`; 514 notification.textContent = message; 515 notification.style.padding = '10px 20px'; 516 notification.style.marginTop = '10px'; 517 notification.style.borderRadius = '4px'; 518 notification.style.backgroundColor = type === 'success' ? '#4CAF50' : (type === 'info' ? '#2196F3' : '#F44336'); 519 notification.style.color = 'white'; 520 notification.style.opacity = '0'; 521 notification.style.transform = 'translateY(20px)'; 522 notification.style.transition = 'opacity 0.3s, transform 0.3s'; 523 524 container.appendChild(notification); 525 526 // Show notification with animation 527 setTimeout(() => { 528 notification.style.opacity = '1'; 529 notification.style.transform = 'translateY(0)'; 530 531 // Hide and remove after delay 532 setTimeout(() => { 533 notification.style.opacity = '0'; 534 notification.style.transform = 'translateY(20px)'; 535 536 setTimeout(() => { 537 container.removeChild(notification); 538 if (container.children.length === 0) { 539 document.body.removeChild(container); 540 } 541 }, 300); 542 }, duration); 543 }, 10); 544} 545 546async function showErrorPopup(errorMessage) { 547 // Only show popup occasionally (30% chance or first time) 548 const HOUR_IN_MS = 3600000; // 1 hour in milliseconds 549 const SHOW_PROBABILITY = 0.3; // 30% chance to show 550 551 const lastShown = appStorage.getItem('errorPopupLastShown'); 552 const now = Date.now(); 553 554 // Show if: never shown before OR (more than 1 hour since last shown AND random chance) 555 const isFirstTime = !lastShown; 556 const hasEnoughTimePassed = lastShown && (now - parseInt(lastShown) > HOUR_IN_MS); 557 const shouldShow = isFirstTime || (hasEnoughTimePassed && Math.random() < SHOW_PROBABILITY); 558 559 if (!shouldShow) { 560 return; // Don't show popup this time 561 } 562 563 // Mark as shown 564 appStorage.setItem('errorPopupLastShown', now.toString()); 565 566 // Remove any existing error popup 567 const existingOverlay = document.querySelector('.error-popup-overlay'); 568 const existingPopup = document.querySelector('.error-popup'); 569 if (existingOverlay) existingOverlay.remove(); 570 if (existingPopup) existingPopup.remove(); 571 572 // Create overlay 573 const overlay = document.createElement('div'); 574 overlay.className = 'error-popup-overlay'; 575 overlay.addEventListener('click', () => closeErrorPopup()); 576 577 // Fetch popup content from HTML file 578 let hintsHtml = ''; 579 try { 580 const response = await fetch('/chat/error-popup.html'); 581 if (response.ok) { 582 hintsHtml = await response.text(); 583 } else { 584 // Fallback if fetch fails 585 console.warn('Failed to load error popup HTML, using fallback'); 586 hintsHtml = generateFallbackHints(); 587 } 588 } catch (error) { 589 console.warn('Error fetching popup HTML:', error); 590 hintsHtml = generateFallbackHints(); 591 } 592 593 let translatedResponse; 594 if (!navigator.language.startsWith('en')) { 595 translatedResponse = framework.query(`Translate this document to (${navigator.language}):\n\`\`\`html\n${hintsHtml}\`\`\``) 596 } 597 598 // Create popup 599 const popup = document.createElement('div'); 600 popup.className = 'error-popup'; 601 const hintsTemplate = hintsHtml=>` 602 <div class="error-popup-header"> 603 <h3>â ï¸ ${framework.translate('Error Occurred')}</h3> 604 <button class="error-popup-close" aria-label="Close">Ã</button> 605 </div> 606 <div class="error-popup-body"> 607 <div class="error-popup-message"></div> 608 ${hintsHtml} 609 </div> 610 `; 611 const updateErrorMessage = ()=>{ 612 // Safely set error message text content to prevent XSS 613 const messageDiv = popup.querySelector('.error-popup-message'); 614 messageDiv.textContent = errorMessage; 615 616 // Add close button event 617 const closeBtn = popup.querySelector('.error-popup-close'); 618 closeBtn.addEventListener('click', () => closeErrorPopup()); 619 } 620 popup.innerHTML = hintsTemplate(hintsHtml); 621 updateErrorMessage(); 622 623 if (translatedResponse) 624 translatedResponse.then(r=>r.text()) 625 .then(t=>framework.filterMarkdown(t, 'html', t)) 626 .then(t=>window.sanitizeHtml(t, framework.sanitizedConfig())) 627 .then(t=>(popup.innerHTML=hintsTemplate(t)) && updateErrorMessage()) 628 629 // Add to document 630 document.body.appendChild(overlay);
631 document.body.appendChild(popup); 632 633 // Show with animation 634 setTimeout(() => { 635 overlay.classList.add('show'); 636 popup.classList.add('show'); 637 }, 10); 638} 639 640function generateFallbackHints() { 641 return ``; 642} 643 644function closeErrorPopup() { 645 const overlay = document.querySelector('.error-popup-overlay'); 646 const popup = document.querySelector('.error-popup'); 647 648 if (overlay) { 649 overlay.classList.remove('show'); 650 setTimeout(() => overlay.remove(), 300); 651 } 652 653 if (popup) { 654 popup.classList.remove('show'); 655 setTimeout(() => popup.remove(), 300); 656 } 657} 658 659const register_message_buttons = async () => { 660 chatBody.querySelectorAll(".message .content .provider").forEach(async (el) => { 661 if (el.dataset.click) { 662 return 663 } 664 el.dataset.click = true; 665 const provider_link = el.querySelector("a"); 666 provider_link?.addEventListener("click", async (event) => { 667 event.preventDefault(); 668 await load_provider_parameters(el.dataset.provider); 669 const provider_forms_container = document.querySelector(".provider_forms"); 670 provider_forms_container.querySelectorAll("form").forEach(form => form.classList.add("hidden")); 671 const provider_form = provider_forms_container.querySelector(`#${el.dataset.provider.replaceAll(':', '-')}-form`); 672 if (provider_form) { 673 provider_form.classList.remove("hidden"); 674 provider_forms_container.classList.remove("hidden"); 675 chat.classList.add("hidden"); 676 } 677 return false; 678 }); 679 }); 680 681 chatBody.querySelectorAll(".message .fa-xmark, .message .delete-message").forEach(async (el) => { 682 if (el.dataset.click) { 683 return 684 } 685 el.dataset.click = true; 686 el.addEventListener("click", async () => { 687 const message_el = get_message_el(el); 688 if (message_el) { 689 if ("index" in message_el.dataset) { 690 await remove_message(window.conversation_id, message_el.dataset.index); 691 chatBody.removeChild(message_el); 692 } 693 } 694 await safe_load_conversation(window.conversation_id); 695 }); 696 }); 697 698 chatBody.querySelectorAll(".message .fa-clipboard, .message .copy-to-clipboard").forEach(async (el) => { 699 if (el.dataset.click) { 700 return 701 } 702 el.dataset.click = true; 703 el.addEventListener("click", async () => { 704 let message_el = get_message_el(el); 705 let response = await fetch(message_el.dataset.object_url); 706 let copyText = await response.text(); 707 try { 708 if (!navigator.clipboard) { 709 throw new Error("navigator.clipboard: Clipboard API unavailable."); 710 } 711 await navigator.clipboard.writeText(copyText); 712 } catch (e) { 713 console.error(e); 714 console.error("Clipboard API writeText() failed! Fallback to document.exec(\"copy\")..."); 715 fallback_clipboard(copyText); 716 } 717 el.classList.add("clicked"); 718 setTimeout(() => el.classList.remove("clicked"), 1000); 719 const startText = el.innerText; 720 if (startText) { 721 el.innerText = framework.translate("Copied") 722 setTimeout(() => el.innerText = startText, 1000); 723 } 724 }); 725 }) 726 727 chatBody.querySelectorAll(".message .fa-file-export").forEach(async (el) => { 728 if (el.dataset.click) { 729 return 730 } 731 el.dataset.click = true; 732 el.addEventListener("click", async () => { 733 const elem = window.document.createElement('a'); 734 let filename = `chat ${new Date().toLocaleString()}.txt`.replaceAll(":", "-"); 735 const conversation = await get_conversation(window.conversation_id); 736 let buffer = ""; 737 conversation.items.forEach(message => { 738 if (message.reasoning) { 739 buffer += render_reasoning_text(message.reasoning); 740 } 741 buffer += `${message.role == 'user' ? 'User' : 'Assistant'}: ${message.content.trim()}\n\n`; 742 }); 743 var download = document.getElementById("download"); 744 download.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(buffer.trim())); 745 download.setAttribute("download", filename); 746 download.click(); 747 el.classList.add("clicked"); 748 setTimeout(() => el.classList.remove("clicked"), 1000); 749 }); 750 }) 751 752 chatBody.querySelectorAll(".message .fa-volume-high, .message .volume-high").forEach(async (el) => { 753 if (el.dataset.click) { 754 return 755 } 756 el.dataset.click = true; 757 el.addEventListener("click", async () => { 758 const message_el = get_message_el(el); 759 let audio; 760 if (message_el.dataset.synthesize_url) { 761 el.classList.add("active"); 762 if (message_el.dataset.synthesize_url.startsWith("https://g4f.space/ai/audio/")) { 763 const response = await fetch(message_el.dataset.synthesize_url, { 764 headers: appStorage.getItem("session_token") ? { 765 'Authorization': `Bearer ${appStorage.getItem("session_token")}` 766 } : {} 767 }); 768 window.captureUserTierHeaders?.(response.headers); 769 const object = await response.blob(); 770 message_el.dataset.synthesize_url = URL.createObjectURL(object); 771 } 772 setTimeout(()=>el.classList.remove("active"), 2000); 773 const media_player = document.querySelector(".media-player"); 774 if (!media_player.classList.contains("show")) { 775 media_player.classList.add("show"); 776 audio = new Audio(message_el.dataset.synthesize_url); 777 audio.controls = true; 778 media_player.appendChild(audio); 779 } else { 780 audio = media_player.querySelector("audio"); 781 audio.src = message_el.dataset.synthesize_url; 782 } 783 audio.play(); 784 return; 785 } 786 }); 787 }); 788 789 chatBody.querySelectorAll(".message .regenerate_button").forEach(async (el) => { 790 if (el.dataset.click) { 791 return 792 } 793 el.dataset.click = true; 794 el.addEventListener("click", async () => { 795 const message_el = get_message_el(el); 796 el.classList.add("clicked"); 797 setTimeout(() => el.classList.remove("clicked"), 1000); 798 await ask_gpt(get_message_id(), message_el.dataset.index); 799 }); 800 }); 801 802 chatBody.querySelectorAll(".message .continue_button").forEach(async (el) => { 803 if (el.dataset.click) { 804 return 805 } 806 el.dataset.click = true; 807 el.addEventListener("click", async () => { 808 if (!el.disabled) { 809 el.disabled = true; 810 const message_el = get_message_el(el); 811 el.classList.add("clicked"); 812 setTimeout(() => {el.classList.remove("clicked"); el.disabled = false}, 1000); 813 await ask_gpt(get_message_id(), message_el.dataset.index, false, null, null, "continue"); 814 } 815 }); 816 }); 817 818 chatBody.querySelectorAll(".message .fa-whatsapp").forEach(async (el) => { 819 if (el.dataset.click) { 820 return 821 } 822 el.dataset.click = true; 823 el.addEventListener("click", async () => { 824 const text = get_message_el(el).innerText; 825 window.open(`https://wa.me/?text=${encodeURIComponent(text)}`, '_blank'); 826 }); 827 }); 828 829 chatBody.querySelectorAll(".message .fa-print").forEach(async (el) => { 830 if (el.dataset.click) { 831 return 832 } 833 el.dataset.click = true; 834 el.addEventListener("click", async () => { 835 const message_el = get_message_el(el); 836 el.classList.add("clicked"); 837 chatBody.scrollTop = 0; 838 message_el.classList.add("print"); 839 setTimeout(() => { 840 el.classList.remove("clicked"); 841 message_el.classList.remove("print"); 842 }, 1000); 843 window.print() 844 }); 845 }); 846 847 chatBody.querySelectorAll(".message .fa-qrcode").forEach(async (el) => { 848 if (el.dataset.click) { 849 return 850 } 851 el.dataset.click = true; 852 el.addEventListener("click", async () => { 853 iframe.src = 'qrcode.html' + (window.conversation_id ? `#${window.conversation_id}` : ''); 854 iframe_container.classList.remove("hidden"); 855 }); 856 }); 857 858 chatBody.querySelectorAll(".message .reasoning_title").forEach(async (el) => { 859 if (el.dataset.click) { 860 return 861 } 862 el.dataset.click = true; 863 el.addEventListener("click", async () => { 864 let text_el = el.parentElement.querySelector(".reasoning_text"); 865 if (text_el) { 866 text_el.classList.toggle("hidden"); 867 } 868 }); 869 }); 870} 871 872const new_conversation = async (private = false) => { 873 if (window.location.hash) { 874 await clear_conversation(); 875 add_url_to_history(private ? "#private" : window.location.pathname); 876 } 877 window.conversation_id = private ? null : generateUUID(); 878 document.title = window.title || document.title; 879 document.querySelector(".chat-top-panel .convo-title").innerText = private ? framework.translate("Private Conversation") : framework.translate("New Conversation"); 880 881 suggestions = null; 882 if (chatPrompt) { 883 chatPrompt.value = document.getElementById("systemPrompt")?.value; 884 } 885 load_conversations(); 886 hide_sidebar(true); 887 say_hello(); 888 render_startup_questions(); 889}; 890 891const delete_conversations = async () => {
892 if (!confirm(framework.translate("Delete all conversations?"))) { 893 return; 894 } 895 // Delete all conversations 896 const { store, done } = await withStore('readwrite'); 897 store.clear(); 898 899 hide_sidebar(); 900 await new_conversation(); 901 return done; 902}; 903 904const handle_ask = async (do_ask_gpt = true, message = null) => { 905 await scroll_to_bottom(); 906 907 if (!message) { 908 message = userInput.value; 909 if (!message) { 910 return; 911 } 912 userInput.value = ""; 913 await count_input() 914 } 915 916 // Is message a url? 917 const expression = /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/gi; 918 const regex = new RegExp(expression); 919 if (!Array.isArray(message) && message.match(regex)) { 920 paperclip.classList.add("blink"); 921 const blob = new Blob([JSON.stringify([{url: message}])], { type: 'application/json' }); 922 const file = new File([blob], 'downloads.json', { type: 'application/json' }); // Create File object 923 let formData = new FormData(); 924 formData.append('files', file); // Append as a file 925 const bucket_id = generateUUID(); 926 await fetch(`${framework.backendUrl}/backend-api/v2/files/${bucket_id}`, { 927 method: 'POST', 928 body: formData 929 }); 930 connectToSSE(`${framework.backendUrl}/backend-api/v2/files/${bucket_id}/stream`, false, bucket_id); //Retrieve and refine 931 return; 932 } 933 if (!Array.isArray(message)) { 934 message = message.trim(); 935 if (!message.length) { 936 return; 937 } 938 } 939 940 await add_conversation(window.conversation_id); 941 let message_index = await add_message(window.conversation_id, "user", message); 942 let message_id = get_message_id(); 943 944 const message_el = document.createElement("div"); 945 message_el.classList.add("message"); 946 message_el.dataset.index = message_index; 947 message_el.innerHTML = ` 948 <div class="user"> 949 ${user_image} 950 <i class="fa-solid fa-xmark"></i> 951 <i class="fa-regular fa-phone-arrow-up-right"></i> 952 </div> 953 <div class="content"> 954 <div class="content_inner"> 955 ${renderer(message)} 956 </div> 957 <div class="count"> 958 ${countTokensEnabled ? count_words_and_tokens(message, get_selected_model()) : ""} 959 </div> 960 </div> 961 `; 962 chatBody.appendChild(message_el); 963 highlight(message_el); 964 if (do_ask_gpt) { 965 const all_pinned = document.querySelectorAll("#pin_container button.pinned") 966 if (all_pinned.length > 0) { 967 all_pinned.forEach((el, idx) => ask_gpt( 968 idx == 0 ? message_id : get_message_id(), 969 -1, 970 idx != 0, 971 el.dataset.provider, 972 el.dataset.model 973 )); 974 } else { 975 await ask_gpt(message_id, -1, false, null, null, "next", message); 976 } 977 } else { 978 await safe_load_conversation(window.conversation_id); 979 await load_conversations(); 980 } 981}; 982 983async function safe_remove_cancel_button() { 984 for (let key in controller_storage) { 985 if (!controller_storage[key].signal.aborted) { 986 return; 987 } 988 } 989 stop_generating.classList.add("stop_generating-hidden"); 990 if (wakeLock) { 991 wakeLock.release(); 992 wakeLock = null; 993 } 994} 995 996regenerate_button.addEventListener("click", async () => { 997 regenerate_button.classList.add("regenerate-hidden"); 998 setTimeout(()=>regenerate_button.classList.remove("regenerate-hidden"), 3000); 999 const all_pinned = document.querySelectorAll("#pin_container button.pinned") 1000 if (all_pinned.length > 0) { 1001 all_pinned.forEach((el) => ask_gpt(get_message_id(), -1, true, el.dataset.provider, el.dataset.model, "variant")); 1002 } else { 1003 await ask_gpt(get_message_id(), -1, true, null, null, "variant"); 1004 } 1005}); 1006 1007stop_generating.addEventListener("click", async () => { 1008 regenerate_button.classList.remove("regenerate-hidden"); 1009 stop_generating.classList.add("stop_generating-hidden"); 1010 let key; 1011 for (key in controller_storage) { 1012 if (!controller_storage[key].signal.aborted) { 1013 console.log(`aborted ${window.conversation_id} #${key}`); 1014 try { 1015 controller_storage[key].abort(); 1016 } finally { 1017 let message = message_storage[key]; 1018 if (message) { 1019 content_storage[key].inner.innerHTML += " [aborted]"; 1020 message_storage[key] += " [aborted]"; 1021 } 1022 } 1023 } 1024 } 1025 await safe_load_conversation(window.conversation_id); 1026}); 1027 1028document.querySelector(".media-player .fa-x").addEventListener("click", ()=>{ 1029 const media_player = document.querySelector(".media-player"); 1030 media_player.classList.remove("show"); 1031 const audio = document.querySelector(".media-player audio"); 1032 media_player.removeChild(audio); 1033}); 1034 1035document.getElementById("close_provider_forms").addEventListener("click", async () => { 1036 const provider_forms = document.querySelector(".provider_forms"); 1037 provider_forms.classList.add("hidden"); 1038 chat.classList.remove("hidden"); 1039}); 1040 1041const prepare_messages = (messages, message_index = -1, do_continue = false, do_filter = true) => { 1042 messages = [ ...messages ] 1043 if (message_index != null) { 1044 console.debug("Messages Index:", message_index); 1045 1046 // Removes messages after selected 1047 if (message_index >= 0) { 1048 messages = messages.filter((_, index) => message_index >= index); 1049 } 1050 // Removes none user messages at end 1051 if (!do_continue) { 1052 let last_message; 1053 while (last_message = messages.pop()) { 1054 if (last_message["role"] == "user") { 1055 messages.push(last_message); 1056 break; 1057 } 1058 } 1059 console.debug("Messages filtered:", messages); 1060 } 1061 } 1062 // Combine assistant messages 1063 // let last_message; 1064 // let new_messages = [];
1065 // messages.forEach((message) => { 1066 // message_copy = { ...message }; 1067 // if (last_message) { 1068 // if (last_message["role"] == message["role"] && message["role"] == "assistant") { 1069 // message_copy["content"] = last_message["content"] + message_copy["content"]; 1070 // new_messages.pop(); 1071 // } 1072 // } 1073 // last_message = message_copy; 1074 // new_messages.push(last_message); 1075 // }); 1076 // messages = new_messages; 1077 // console.log(2, messages); 1078 1079 // Insert system prompt as first message 1080 let last_steps_messages = []; 1081 if (document.getElementById('globalPrompt')?.value) { 1082 last_steps_messages.push({ 1083 "role": "system", 1084 "content": document.getElementById('globalPrompt').value 1085 }); 1086 } 1087 if (chatPrompt?.value) { 1088 last_steps_messages.push({ 1089 "role": "system", 1090 "content": chatPrompt.value 1091 }); 1092 } 1093 1094 // Remove history, only add new user messages 1095 // The message_index is null on count total tokens 1096 if (!do_continue && document.getElementById('history')?.checked && do_filter && message_index != null) { 1097 let filtered_messages = []; 1098 while (last_message = messages.pop()) { 1099 if (last_message["role"] == "user") { 1100 filtered_messages.push(last_message); 1101 } else { 1102 break; 1103 } 1104 } 1105 messages = filtered_messages.reverse(); 1106 if (last_message) { 1107 console.debug("History removed:", messages) 1108 } 1109 } 1110 1111 messages.forEach((new_message, i) => { 1112 // Copy message first 1113 new_message = { ...new_message }; 1114 // Include last message, if do_continue 1115 if (i + 1 == messages.length && do_continue) { 1116 delete new_message.regenerate; 1117 } 1118 // Include only not regenerated messages 1119 if (new_message) { 1120 // Remove generated images from content 1121 if (new_message.content) { 1122 new_message.content = filter_message(new_message.content); 1123 } 1124 // Remove internal fields 1125 new_message = {role: new_message.role, content: new_message.content}; 1126 // Append message to new messages 1127 if (do_filter && !new_message.regenerate) { 1128 last_steps_messages.push(new_message) 1129 } else if (!do_filter) { 1130 last_steps_messages.push(new_message) 1131 } 1132 } 1133 }); 1134 1135 // Remove multiple assistant messages 1136 let has_assistant = false; 1137 let final_messages = []; 1138 for (let new_message of last_steps_messages.reverse()) { 1139 if (new_message.role == "assistant") { 1140 if (has_assistant) { 1141 continue; 1142 } 1143 has_assistant = true; 1144 } 1145 final_messages.push(new_message); 1146 } 1147 final_messages = final_messages.reverse(); 1148 1149 console.debug("Final messages:", final_messages) 1150 1151 return final_messages; 1152} 1153 1154async function load_provider_parameters(provider) { 1155 let form_id = `${provider.replaceAll(':', '-')}-form`; 1156 if (!parameters_storage[provider]) { 1157 parameters_storage[provider] = JSON.parse(appStorage.getItem(form_id)); 1158 } 1159 if (!parameters_storage[provider]) { 1160 parameters_storage[provider] = {"provider": provider, "model": "", "messages": [{"role": "system", "content": ""}, {"role": "user", "content": ""}], "stream": true, "timeout": 0, "response_format": {"type": "json_object"}, "max_tokens": 4096, "stop": ["stop1", "stop2"], "media": [["data:image/jpeg;base64,...", "filename.jpg"]], "temperature": 1, "presence_penalty": 1, "top_p": 1, "frequency_penalty": 1} 1161 } 1162 if (parameters_storage[provider]) { 1163 let provider_forms = document.querySelector(".provider_forms"); 1164 let form_el = document.createElement("form"); 1165 form_el.id = form_id; 1166 form_el.classList.add("hidden"); 1167 appStorage.setItem(form_el.id, JSON.stringify(parameters_storage[provider])); 1168 let old_form = document.getElementById(form_id); 1169 if (old_form) { 1170 old_form.remove(); 1171 }
1172 Object.entries(parameters_storage[provider]).forEach(([key, value]) => { 1173 let el_id = `${provider}-${key}`; 1174 let saved_value = appStorage.getItem(el_id); 1175 let input_el; 1176 let field_el; 1177 if (typeof value == "boolean") { 1178 field_el = document.createElement("div"); 1179 field_el.classList.add("field"); 1180 if (saved_value) { 1181 field_el.classList.add("saved"); 1182 saved_value = saved_value == "true"; 1183 } else { 1184 saved_value = value; 1185 } 1186 field_el.innerHTML = `<span class="label">${key}:</span> 1187 <input type="checkbox" id="${el_id}" name="${key}"> 1188 <label for="${el_id}" class="toogle" title=""></label> 1189 <i class="fa-solid fa-xmark"></i>`; 1190 form_el.appendChild(field_el); 1191 input_el = field_el.querySelector("input"); 1192 input_el.checked = saved_value; 1193 input_el.dataset.checked = value ? "true" : "false"; 1194 input_el.onchange = () => { 1195 field_el.classList.add("saved"); 1196 appStorage.setItem(el_id, input_el.checked ? "true" : "false"); 1197 } 1198 } else if (typeof value == "string" || typeof value == "object"|| typeof value == "number") { 1199 field_el = document.createElement("div"); 1200 field_el.classList.add("field"); 1201 field_el.classList.add("box"); 1202 if (typeof value == "object" && value != null) { 1203 value = JSON.stringify(value, null, 4); 1204 } 1205 if (saved_value) { 1206 field_el.classList.add("saved"); 1207 } else { 1208 saved_value = value; 1209 } 1210 let placeholder; 1211 if (["api_key", "proof_token"].includes(key)) { 1212 placeholder = saved_value && saved_value.length >= 22 ? (saved_value.substring(0, 12) + "*".repeat(12) + saved_value.substring(saved_value.length-12)) : value; 1213 } else { 1214 placeholder = value == null ? "null" : value; 1215 } 1216 field_el.innerHTML = `<label for="${el_id}" title="">${key}:</label>`; 1217 if (Number.isInteger(value)) { 1218 max = key === "n" ? 10 : value == 42 || value >= 4096 ? 8192 : value >= 100 ? 4096 : value > 1 ? 100 : value === 0 ? 600 : 2; 1219 step = value >= 1024 ? 8 : value > 1 ? 1 : value > 0 ? 0.1 : 1; 1220 field_el.innerHTML += `<input type="range" id="${el_id}" name="${key}" value="${framework.escape(value)}" class="slider" min="0" max="${max}" step="${step}"/><output>${framework.escape(value)}</output>`; 1221 field_el.innerHTML += `<i class="fa-solid fa-xmark"></i>`; 1222 } else if (typeof value == "number") { 1223 field_el.innerHTML += `<input type="range" id="${el_id}" name="${key}" value="${framework.escape(value)}" class="slider" min="0" max="2" step="0.1"/><output>${framework.escape(value)}</output>`; 1224 field_el.innerHTML += `<i class="fa-solid fa-xmark"></i>`; 1225 } else { 1226 field_el.innerHTML += `<textarea id="${el_id}" name="${key}"></textarea>`; 1227 field_el.innerHTML += `<i class="fa-solid fa-xmark"></i>`; 1228 input_el = field_el.querySelector("textarea"); 1229 if (value != null) { 1230 input_el.dataset.text = value; 1231 } 1232 input_el.placeholder = placeholder; 1233 if (!["api_key", "proof_token"].includes(key)) { 1234 input_el.value = saved_value; 1235 } else { 1236 input_el.dataset.saved_value = saved_value; 1237 } 1238 input_el.oninput = () => { 1239 field_el.classList.add("saved"); 1240 appStorage.setItem(el_id, input_el.value); 1241 input_el.dataset.saved_value = input_el.value; 1242 }; 1243 input_el.onfocus = () => { 1244 if (input_el.dataset.saved_value) { 1245 input_el.value = input_el.dataset.saved_value; 1246 } else if (["api_key", "proof_token"].includes(key)) { 1247 input_el.value = input_el.dataset.text; 1248 } 1249 input_el.style.height = (input_el.scrollHeight) + "px"; 1250 } 1251 input_el.onblur = () => { 1252 input_el.style.removeProperty("height"); 1253 if (["api_key", "proof_token"].includes(key)) { 1254 input_el.value = ""; 1255 } 1256 } 1257 } 1258 if (!input_el) { 1259 input_el = field_el.querySelector("input"); 1260 input_el.dataset.value = value; 1261 input_el.value = saved_value; 1262 input_el.nextElementSibling.value = input_el.value; 1263 input_el.oninput = () => { 1264 input_el.nextElementSibling.value = input_el.value; 1265 field_el.classList.add("saved"); 1266 appStorage.setItem(input_el.id, input_el.value); 1267 }; 1268 } 1269 } 1270 form_el.appendChild(field_el); 1271 let xmark_el = field_el.querySelector(".fa-xmark"); 1272 xmark_el.onclick = () => { 1273 if (input_el.dataset.checked) { 1274 input_el.checked = input_el.dataset.checked == "true"; 1275 } else if (input_el.dataset.value) { 1276 input_el.value = input_el.dataset.value; 1277 input_el.nextElementSibling.value = input_el.dataset.value; 1278 } else if (input_el.dataset.text) { 1279 input_el.value = input_el.dataset.text; 1280 } 1281 delete input_el.dataset.saved_value; 1282 appStorage.removeItem(el_id); 1283 field_el.classList.remove("saved"); 1284 } 1285 }); 1286 provider_forms.appendChild(form_el); 1287 } 1288} 1289 1290async function add_message_chunk(message, message_id, provider, finish_message=null) { 1291 content_map = content_storage[message_id]; 1292 if (message.type == "conversation") { 1293 const conversation = await get_conversation(window.conversation_id); 1294 if (!conversation.data) { 1295 conversation.data = {}; 1296 } 1297 for (const [key, value] of Object.entries(message.conversation)) { 1298 conversation.data[key] = value; 1299 } 1300 await save_conversation(update_conversation(conversation)); 1301 } else if (message.type == "auth") { 1302 error_storage[message_id] = message.message 1303 content_map.inner.innerHTML += framework.markdown(`${framework.translate('**An error occurred:**')} ${message.message}`); 1304 1305 // Show error popup with partner hints for auth errors 1306 await showErrorPopup(message.message); 1307 1308 let provider = provider_storage[message_id]?.name; 1309 let configEl = document.querySelector(`.settings .${provider}-api_key`); 1310 if (configEl) { 1311 configEl = configEl.parentElement.cloneNode(true); 1312 content_map.content.appendChild(configEl); 1313 await register_settings_storage(); 1314 } 1315 } else if (message.type == "provider") { 1316 provider_storage[message_id] = message.provider; 1317 let provider_el = content_map.content.querySelector('.provider'); 1318 provider_el.innerHTML = ` 1319 <a href="${message.provider.url}" target="_blank"> 1320 ${message.provider.label ? message.provider.label : message.provider.name} 1321 </a> 1322 ${message.provider.model ? ' ' + framework.translate('with') + ' ' + message.provider.model : ''} 1323 `; 1324 } else if (message.type == "message") { 1325 console.error(message.message) 1326 await api("log", {...message, provider: provider_storage[message_id]}); 1327 } else if (message.type == "error") { 1328 const error_message = message.message || message.error; 1329 error_storage[message_id] = error_message; 1330 console.error(error_message); 1331 content_map.inner.innerHTML += framework.markdown(`${framework.translate('**An error occurred:**')} ${error_message}`); 1332 1333 // Show error popup with partner hints 1334 await showErrorPopup(error_message); 1335 1336 if (finish_message) { 1337 await finish_message(); 1338 } 1339 let p = document.createElement("p"); 1340 p.innerText = error_message; 1341 logContent.appendChild(p); 1342 await api("log", {...message, provider: provider_storage[message_id]}); 1343 } else if (message.type == "preview") { 1344 let img; 1345 if (img = content_map.inner.querySelector("img")) { 1346 if (img.complete) { 1347 const backup = img.src; 1348 img.src = message.urls; 1349 img.onerror = () => img.src = backup; 1350 } 1351 } else { 1352 content_map.inner.innerHTML = framework.markdown(message.preview + ' <span class="cursor"></span>'); 1353 await register_message_images(); 1354 } 1355 } else if (message.type == "content") { 1356 if (message.content) { 1357 if (!message_storage[message_id]) { 1358 content_map.inner.innerHTML = '<pre><span class="cursor"></span>
1358<pre><br>'; 1359 content_map.innerPre = content_map.inner.querySelector("pre"); 1360 } 1361 message_storage[message_id] += message.content; 1362 if (message.data) { 1363 content_data_storage[message_id] = message.data; 1364 } 1365 } 1366 if (message.urls) { 1367 content_alt_storage[message_id] = message.alt; 1368 const div = document.createElement("div"); 1369 div.innerHTML = framework.markdown(message.content); 1370 content_map.inner.appendChild(div); 1371 let cursorDiv = content_map.inner.querySelector(".cursor"); 1372 if (cursorDiv) cursorDiv.parentNode.removeChild(cursorDiv); 1373 } else if (message.content) { 1374 const contentInnerPre = content_map.innerPre || content_map.inner; 1375 if (Math.floor(Math.random() * 100) === 0) { 1376 setTimeout(() => { 1377 contentInnerPre.innerHTML = contentInnerPre.innerHTML; // Trigger re-render to prevent freezing on long messages 1378 }, 100); 1379 } 1380 try { 1381 let lastChild = contentInnerPre.querySelector(".cursor") || contentInnerPre.lastChild; 1382 if (appStorage.getItem("simulateTyping") === "false") { 1383 contentInnerPre.insertBefore(document.createTextNode(message.content), lastChild); 1384 } else { 1385 let firstLine = true; 1386 for (line of message.content.split("\n")) { 1387 if (firstLine) { 1388 firstLine = false; 1389 } else { 1390 contentInnerPre.insertBefore(document.createElement("br"), lastChild); 1391 } 1392 if (line.length > 0) { 1393 let firstToken = true; 1394 for (token of line.split(' ')) { 1395 if (token) { 1396 await new Promise(resolve => setTimeout(resolve, (Math.random() * (20 - 40) + 20))) 1397 } 1398 if (firstToken) { 1399 firstToken = false; 1400 } else { 1401 token = ' ' + token 1402 } 1403 contentInnerPre.insertBefore(document.createTextNode(token), lastChild); 1404 } 1405 } 1406 }; 1407 } 1408 } catch (e) { 1409 add_error("Error updating content:", e); 1410 } 1411 } 1412 } else if (message.type == "log") { 1413 let p = document.createElement("p"); 1414 p.innerText = message.log; 1415 logContent.appendChild(p); 1416 } else if (message.type == "synthesize") { 1417 synthesize_storage[message_id] = message.synthesize; 1418 } else if (message.type == "title") { 1419 title_storage[message_id] = message.title; 1420 } else if (message.type == "login") { 1421 content_map.inner.innerHTML = framework.markdown(message.login + ' <span class="cursor"></span>'); 1422 } else if (message.type == "finish") { 1423 finish_storage[message_id] = message.finish; 1424 } else if (message.type == "variant") { 1425 variant_storage[message_id] = message.variant; 1426 } else if (message.type == "continue") { 1427 continue_storage[message_id] = message; 1428 } else if (message.type == "usage") { 1429 usage_storage[message_id] = message.usage; 1430 if (headers_storage[message_id]) { 1431 window.captureUserTierHeaders?.(new Headers(headers_storage[message_id]), message.usage); 1432 delete headers_storage[message_id]; 1433 } 1434 } else if (message.type == "reasoning") { 1435 if (!reasoning_storage[message_id]) { 1436 reasoning_storage[message_id] = message; 1437 reasoning_storage[message_id].text = ""; 1438 if (message.is_thinking && message_storage[message_id]) { 1439 reasoning_storage[message_id].text = message_storage[message_id]; 1440 message_storage[message_id] = ""; 1441 } 1442 } else if (typeof message.status !== 'undefined') { 1443 reasoning_storage[message_id].status = message.status; 1444 } if (message.label) { 1445 reasoning_storage[message_id].label = message.label; 1446 } if (message.token) { 1447 reasoning_storage[message_id].text += message.token; 1448 } 1449 if (message.status || message.token || message.label) { 1450 const reasoning_body = content_map.inner; 1451 reasoning_body.innerHTML = render_reasoning(reasoning_storage[message_id]); 1452 } 1453 } else if (message.type == "parameters") { 1454 if (!parameters_storage[provider]) { 1455 parameters_storage[provider] = {}; 1456 }
1457 Object.entries(message.parameters).forEach(([key, value]) => { 1458 parameters_storage[provider][key] = value; 1459 }); 1460 } else if (message.type == "suggestions") { 1461 suggestions = message.suggestions; 1462 } else if (message.type == "tool_calls") { 1463 // Handle tool calls and show spinner 1464 if (message.tool_calls) { 1465 if (!tool_calls_storage[message_id]) { 1466 tool_calls_storage[message_id] = []; 1467 } 1468 for (const toolCall of message.tool_calls) { 1469 if (typeof toolCall.index === 'undefined') { 1470 toolCall.index = toolCall.function.index || 0; 1471 } 1472 if (!tool_calls_storage[message_id][toolCall.index]) { 1473 tool_calls_storage[message_id][toolCall.index] = toolCall 1474 tool_calls_storage[message_id][toolCall.index].function.arguments = tool_calls_storage[message_id][toolCall.index].function.arguments || ''; 1475 } else if (toolCall.function?.arguments) { 1476 tool_calls_storage[message_id][toolCall.index].function.arguments += toolCall.function.arguments; 1477 } 1478 } 1479 // Show spinner/loading indicator in the message 1480 if (content_storage[message_id] && content_storage[message_id].inner) { 1481 let spinner = content_storage[message_id].inner.querySelector('.tool-call-spinner'); 1482 if (!spinner) { 1483 spinner = document.createElement('div'); 1484 spinner.className = 'tool-call-spinner'; 1485 spinner.innerHTML = `<span>${framework.translate('Waiting for tool response...')}</span>`; 1486 content_storage[message_id].inner.appendChild(spinner); 1487 } 1488 } 1489 } 1490 } else if (["request", "response"].includes(message.type)) { 1491 debug_response_counter[message_id] = (debug_response_counter[message_id] || 0) + (message.type == "response" ? 1 : 0); 1492 logRequestResponse(message, message_id, debug_response_counter[message_id]); 1493 } else if (message.type == "headers") { 1494 headers_storage[message_id] = message.headers; 1495 } 1496} 1497 1498function add_sources(data, message_id) { 1499 console.debug("Adding sources for message", message_id, data); 1500 const blockquote = document.createElement("blockquote"); 1501 if (data.webSearchQueries) { 1502 suggestions = data.webSearchQueries; 1503 } 1504 if (data.groundingChunks) { 1505 const links = data.groundingChunks.map((chunk, index) => { 1506 return `<p>[${index}] <a target="_blank" href="${chunk.web.uri}">${chunk.web.title}</a></p>`; 1507 }).join(""); 1508 blockquote.innerHTML = links; 1509 } 1510 if (data.citations) { 1511 const links = data.citations.map((citation, index) => { 1512 return `<p>[${index+1}] <a target="_blank" href="${citation}">${citation.replace("https://www.", "").replace("https://", "")}</a></p>`; 1513 }).join(""); 1514 blockquote.innerHTML = links; 1515 } 1516 if (data.sources) { 1517 const links = data.sources.map((source, index) => { 1518 return `<p>[${index}] <a target="_blank" href="${source.link}">${source.title}</a></p>`; 1519 }).join(""); 1520 blockquote.innerHTML = links; 1521 } 1522 if (blockquote.innerHTML) { 1523 message_storage[message_id] += blockquote.outerHTML; 1524 content_storage[message_id].inner.innerHTML += blockquote.outerHTML; 1525 } 1526} 1527 1528function renderer(text) { 1529 if (appStorage.getItem("renderMarkdown") == "false") { 1530 return `<pre>${framework.escape(text)}</pre>`; 1531 } 1532 return framework.markdown(text); 1533} 1534 1535function is_stopped() { 1536 if (stop_generating.classList.contains('stop_generating-hidden')) { 1537 return true; 1538 } 1539 return false; 1540} 1541 1542const requestWakeLock = async () => { 1543 try { 1544 wakeLock = await navigator.wakeLock.request('screen'); 1545 } 1546 catch(err) { 1547 console.error(err); 1548 } 1549}; 1550 1551async function play_last_message(response = null) { 1552 const last_message = Array.from(document.querySelectorAll(".message")).at(-1); 1553 const last_content = last_message ? last_message.querySelector(".content_inner") : null; 1554 const last_media = last_message ? last_content.querySelector("audio, iframe, img") : null; 1555 if (last_media) { 1556 if (last_media.tagName == "IFRAME") { 1557 if (YT) { 1558 async function onPlayerReady(event) { 1559 event.target.setVolume(100); 1560 event.target.playVideo(); 1561 } 1562 player = new YT.Player(last_media, { 1563 events: { 1564 'onReady': onPlayerReady, 1565 } 1566 }); 1567 } 1568 } else if (last_media.tagName == "AUDIO") { 1569 if (response) { 1570 if (response.choices && response.choices[0].message?.audio?.data) { 1571 response = `data:audio/mpeg;base64,${response.choices[0].message.audio.data}`; 1572 } 1573 last_media.src = response; 1574 } 1575 last_media.play(); 1576 } else { 1577 // width = last_media.parentElement.dataset.width || last_media.naturalWi
1577dth; 1578 // height = last_media.parentElement.dataset.height || last_media.naturalHeight; 1579 // if (width > 0 && height > 0) { 1580 // last_message.querySelector(".count").childNodes[0].nodeValue = `(width: ${width}px, height: ${height}px)`; 1581 // } 1582 } 1583 return true; 1584 } 1585 return false; 1586} 1587 1588const toBase64 = file => new Promise((resolve, reject) => { 1589 const reader = new FileReader(); 1590 reader.readAsDataURL(file); 1591 reader.onload = () => resolve(reader.result); 1592 reader.onerror = reject; 1593}); 1594const toUrl = async (file)=>{ 1595 if (file instanceof File) { 1596 return await toBase64(file); 1597 } 1598 return file.url ? file.url : file; 1599} 1600 1601function getExtraBody(provider) { 1602 const extraBody = {}; 1603 for (el of document.getElementById(`${provider.replaceAll(':', '-')}-form`)?.querySelectorAll(".saved input, .saved textarea") || []) { 1604 let value; 1605 if (el.type == "checkbox") { 1606 value = el.checked; 1607 } else { 1608 value = el.value; 1609 try { 1610 value = JSON.parse(value); 1611 } catch (e) {} 1612 } 1613 extraBody[el.name] = value; 1614 }; 1615 return extraBody; 1616} 1617 1618const ask_gpt = async (message_id, message_index = -1, regenerate = false, provider = null, model = null, action = null, message = null) => { 1619 if (!model && !provider) { 1620 model = get_selected_model(); 1621 provider = providerSelect?.value; 1622 } 1623 const is_youtube = provider == "YouTube"; 1624 let conversation = await get_conversation(window.conversation_id); 1625 if (!conversation) { 1626 return; 1627 } 1628 await requestWakeLock(); 1629 let messages = prepare_messages(conversation.items, is_youtube ? -1 : message_index, action=="continue" || is_youtube); 1630 message_storage[message_id] = ""; 1631 stop_generating.classList.remove("stop_generating-hidden"); 1632 1633 chatBody.querySelectorAll('.suggestions').forEach((suggestions_el) => suggestions_el.remove()); 1634 if (countTokensEnabled) { 1635 let count_total = chatBody.querySelector('.count_total'); 1636 count_total ? count_total.parentElement.removeChild(count_total) : null; 1637 } 1638 1639 const message_el = document.createElement("div"); 1640 message_el.classList.add("message"); 1641 if (message_index != -1 || regenerate) { 1642 message_el.classList.add("regenerate"); 1643 } 1644 message_el.innerHTML = ` 1645 <div class="assistant"> 1646 ${gpt_image} 1647 <i class="fa-solid fa-xmark"></i> 1648 <i class="fa-regular fa-phone-arrow-down-left"></i> 1649 </div> 1650 <div class="content"> 1651 <div class="provider" data-provider="${provider}"></div> 1652 <div class="content_inner"><span class="cursor"></span></div> 1653 <div class="count"></div> 1654 </div> 1655 `; 1656 if (message_index == -1) { 1657 chatBody.appendChild(message_el); 1658 } else { 1659 const parent_message = chatBody.querySelector(`.message[data-index="${message_index}"]`); 1660 if (!parent_message) { 1661 return; 1662 } 1663 parent_message.after(message_el); 1664 } 1665 1666 let content_el = message_el.querySelector('.content'); 1667 let content_map = content_storage[message_id] = { 1668 container: message_el, 1669 content: content_el, 1670 inner: content_el.querySelector('.content_inner'), 1671 count: content_el.querySelector('.count'), 1672 message_index: message_index, 1673 } 1674 async function finish_message() { 1675 let final_message = null; 1676 if (!error_storage[message_id] && message_storage[message_id]) { 1677 content_map.inner.innerHTML = renderer(message_storage[message_id]); 1678 highlight(content_map.inner); 1679 } 1680 // Handle tool calls if any 1681 if (tool_calls_storage[message_id] && tool_calls_storage[message_id].length > 0 && mcpClient) { 1682 const tool_calls = tool_calls_storage[message_id]; 1683 delete tool_calls_storage[message_id]; 1684 await handleToolCalls(tool_calls, messages, model, provider, message_id, finish_message); 1685 // Remove spinner/loading indicator after tool call is handled 1686 if (content_storage[message_id] && content_storage[message_id].inner) { 1687 let spinner = content_storage[message_id].inner.querySelector('.tool-call-spinner'); 1688 if (spinner) spinner.remove(); 1689 } 1690 } 1691 if (message_storage[message_id] || reasoning_storage[message_id]?.status || reasoning_storage[message_id]?.text) { 1692 const message_provider = message_id in provider_storage ? provider_storage[message_id] : null; 1693 let usage = {}; 1694 if (usage_storage[message_id]) { 1695 usage = usage_storage[message_id]; 1696 } 1697 // Calculate usage if we don't have it jet 1698 if (countTokensEnabled && !usage.prompt_tokens && window.GPTTokenizer_cl100k_base) { 1699 const prompt_token_model = model?.startsWith("gpt-3") ? "gpt-3.5-turbo" : "gpt-4" 1700 let prompt_tokens = 0; 1701 if (content_alt_storage[message_id]) { 1702 prompt_tokens = count_tokens(content_alt_storage[message_id], content_alt_storage[message_id]); 1703 } else { 1704 const filtered = messages.filter((item)=>!Array.isArray(item.content) && item.content); 1705 prompt_tokens = GPTTokenizer_cl100k_base?.encodeChat(filtered, prompt_token_model).length; 1706 } 1707 const completion_tokens = count_tokens(message_provider?.model, message_storage[message_id]) 1708 + (reasoning_storage[message_id] ? count_tokens(message_provider?.model, reasoning_storage[message_id].text) : 0); 1709 usage = { 1710 ...usage, 1711 prompt_tokens: prompt_tokens, 1712 completion_tokens: completion_tokens, 1713 total_tokens: prompt_tokens + completion_tokens 1714 } 1715 } 1716 // It is not regenerated, if it is the first response to a new question 1717 if (regenerate && message_index == -1) { 1718 let conversation = await get_conversation(window.conversation_id); 1719 regenerate = conversation.items[conversation.items.length-1]["role"] != "user"; 1720 } 1721 // Create final message content 1722 final_message = message_storage[message_id] 1723 + (error_storage[message_id] ? " [error]" : "") 1724 + (stop_generating.classList.contains('stop_generating-hidden') ? " [aborted]" : "") 1725 if (reasoning_storage[message_id] && !reasoning_storage[message_id].status) { 1726 reasoning_storage[message_id].status = ""; 1727 } 1728 // Save message in local storage 1729 message_index = await add_message( 1730 window.conversation_id, 1731 "assistant", 1732 final_message, 1733 message_provider, 1734 message_index, 1735 synthesize_storage[message_id], 1736 regenerate && provider != "YouTube", 1737 title_storage[message_id], 1738 finish_storage[message_id], 1739 usage, 1740 reasoning_storage[message_id], 1741 action=="continue" 1742 ); 1743 delete reasoning_storage[message_id]; 1744 delete synthesize_storage[message_id]; 1745 delete title_storage[message_id]; 1746 delete finish_storage[message_id]; 1747 if (variant_storage[message_id]) { 1748 message_index = await add_message( 1749 window.conversation_id, 1750 "assistant", 1751 variant_storage[message_id], 1752 {...message_provider, modelLabel: message_provider.variantLabel, modelUrl: message_provider.variantUrl}, 1753 message_index, 1754 null, 1755 true 1756 ); 1757 delete variant_storage[message_id]; 1758 } 1759 // Send usage to the server 1760 if (!usage_storage[message_id] || !usage_storage[message_id].prompt_tokens) { 1761 usage = { 1762 model: message_provider?.model, 1763 provider: message_provider?.name, 1764 ...usage 1765 }; 1766 const user = appStorage.getItem("user"); 1767 if (user) { 1768 usage = {user: user, ...usage}; 1769 } 1770 api("usage", usage); 1771 } 1772 delete usage_storage[message_id]; 1773 } 1774 // Update controller storage 1775 if (controller_storage[message_id]) { 1776 delete controller_storage[message_id]; 1777 } 1778 // Reload conversation if no error 1779 if (message_storage[message_id] && !document.body.classList.contains("screen-reader")) { 1780 try { 1781 if(await safe_load_conversation(window.conversation_id)) { 1782 // Play last message async 1783 if(!await play_last_message(content_data_storage[message_id])) { 1784 if (action === "next" && final_message) { 1785 load_follow_up_questions(messages, final_message); 1786 } 1787 } 1788 delete content_data_storage[message_id]; 1789 if (client) { 1790 loadClientModels(); 1791 } else { 1792 refreshModels(providerSelect?.value); 1793 } 1794 } 1795 } catch (e) { 1796 add_error("Failed to load the conversation:", e); 1797 } 1798 } 1799 delete message_storage[message_id]; 1800 let cursorDiv = message_el.querySelector(".cursor"); 1801 if (cursorDiv) cursorDiv.parentNode.removeChild(cursorDiv); 1802 await safe_remove_cancel_button(); 1803 await register_message_images(); 1804 await register_message_buttons(); 1805 await load_conversations(); 1806 regenerate_button.classList.remove("regenerate-hidden"); 1807 } 1808 const media = []; 1809 if (mediaRecorder && mediaRecorder.wavBlob) { 1810 const data = await toBase64(mediaRecorder.wavBlob); 1811 media.push({ 1812 "type": "input_audio", 1813 "input_audio": { 1814 "data": data.split(",")[1], 1815 "format": "wav" 1816 } 1817 }); 1818 } 1819 if (client) {
1820 for (const file of Object.values(image_storage)) { 1821 media.push({ 1822 "type": "image_url", 1823 "image_url": { 1824 "url": await toUrl(file) 1825 } 1826 }); 1827 } 1828 // Helper function to solve bucket content 1829 const solveBucketContent = async (item) => { 1830 // Check if this is a media bucket (has url with /media/ path) 1831 if (item.bucket_id && item.url && item.url.includes('/media/')) { 1832 // For media buckets, add as image_url when schema is https 1833 if (window.location.protocol === 'https:') { 1834 return { 1835 type: "image_url", 1836 image_url: { 1837 url: item.url 1838 } 1839 }; 1840 } 1841 // For non-https, skip media content 1842 return null; 1843 } 1844 // Check if this is a text bucket (has bucket_id but no media url) 1845 if (item.bucket_id && !item.text) { 1846 // Fetch plain text content from backend 1847 try { 1848 const response = await fetch(`${framework.backendUrl}/backend-api/v2/files/${item.bucket_id}`); 1849 if (response.ok) { 1850 const text = await response.text(); 1851 return { 1852 type: "text", 1853 text: text 1854 }; 1855 } 1856 } catch (e) { 1857 console.error("Failed to fetch bucket content:", e); 1858 } 1859 return null; 1860 } 1861 // Regular text content 1862 return { 1863 type: "text", 1864 text: item.text || "" 1865 }; 1866 }; 1867 // Process messages with async bucket resolution 1868 messages = await Promise.all(messages.map(async (message) => { 1869 if (Array.isArray(message.content)) { 1870 const resolvedContent = await Promise.all(message.content.map(solveBucketContent)); 1871 return { 1872 role: message.role, 1873 content: resolvedContent.filter(item => item !== null) 1874 }; 1875 } 1876 return { 1877 role: message.role, 1878 content: message.content 1879 }; 1880 })); 1881 } 1882 if (messages.length > 0) { 1883 const last_message = messages[messages.length - 1]; 1884 if (!message) { 1885 message = last_message?.content; 1886 } 1887 if (last_message.content && media.length > 0) { 1888 last_message.content = [ 1889 ...(Array.isArray(last_message.content) ? last_message.content : [{type: "text", text: last_message.content}]), 1890 ...media 1891 ]; 1892 } else { 1893 last_message.content = media.length > 0 ? media : last_message.content; 1894 } 1895 } else { 1896 messages = [{ 1897 role: "user", 1898 content: media.length > 0 ? media : message || "" 1899 }]; 1900 } 1901 if (client) { 1902 const selectedOption = modelSelect.options[modelSelect.selectedIndex]; 1903 const providerSelectOption = providerSelect.options[providerSelect.selectedIndex]; 1904 const selectedModel = get_selected_model() || client.defaultModel; 1905 const modelType = selectedOption?.dataset.type || 'chat'; 1906 const modelSeed = selectedOption?.dataset.seed; 1907 let providerLabel = providerSelectOption?.dataset.label || provider; 1908 const isAudio = selectedOption?.dataset.audio == "true"; 1909 try { 1910 // Conditionally call the correct client method based on model type. 1911 if (['image', 'image-edit', 'video'].includes(modelType)) { 1912 const method = ['image', 'video'].includes(modelType) ? 'generate' : 'edit'; 1913 // Handle image generation 1914 const image = image_storage ? Object.values(image_storage)[0] : null; 1915 const isAutomaticOrientation = appStorage.getItem("automaticOrientation") != "false"; 1916 const imageHeight = isAutomaticOrientation ? (window.innerHeight > window.innerWidth ? 832 : 480) : undefined; 1917 const imageWidth = isAutomaticOrientation ? (window.innerHeight > window.innerWidth ? 480 : 832) : undefined; 1918 const response = await client.images[method]({ 1919 model: selectedModel, 1920 prompt: message, 1921 ...(modelSeed && regenerate ? { seed: Math.floor(Date.now() / 1000) } : {}), 1922 ...(!modelSeed ? { response_format: 'b64_json' } : {}), 1923 ...(image && image.url ? { image: image.url } : {}), 1924 height: imageHeight, 1925 width: imageWidth, 1926 }); 1927 if (!response.data) { 1928 throw new Error(framework.translate("No image URL returned from the API.")); 1929 } 1930 response.data.forEach(img => { 1931 if (img.b64_json) { 1932 const mimeType = modelType === 'video' ? 'video/mp4' : 'image/png'; 1933 img.url = `data:${mimeType};base64,${img.b64_json}`; 1934 } 1935 if (modelType === 'video') { 1936 message_storage[message_id] += `<video controls src="${img.url}"></video>`; 1937 } else { 1938 message_storage[message_id] += `[](${img.url.startsWith('data:') ? '' : img.url})` 1939 } 1940 }); 1941 } else if (isAudio) { 1942 // Handle audio generation
1943 const response = await client.chat.completions.create({ 1944 model: selectedModel, 1945 messages, 1946 }); 1947 message_storage[message_id] = response.choices[0].message.content; 1948 if (response.usage) { 1949 add_message_chunk({type: "usage", usage: response.usage}, message_id); 1950 } 1951 if (response.choices && response.choices[0].message.audio) { 1952 const audio = response.choices[0].message.audio; 1953 message_storage[message_id] = message_storage[message_id] || `<audio controls></audio>\n\n\n${audio.transcript}`; 1954 content_data_storage[message_id] = `data:audio/mpeg;base64,${audio.data}`; 1955 } 1956 } else { 1957 if (framework.backendUrl && searchButton.classList.contains("active") && provider != "CachedSearch") { 1958 let query = message.split(":"); 1959 query = query.length > 1 ? query[1].trim() : message; 1960 query = query.split("\n")[0].trim(); 1961 const searchUrl = `${framework.backendUrl}/backend-api/v2/create?provider=CachedSearch&prompt=${encodeURIComponent(query)}`; 1962 const response = await fetch(searchUrl); 1963 if (response.ok) { 1964 const result = await response.text(); 1965 if (result) { 1966 const new_message = `<details><summary>${framework.translate("Web search:")} ${query}</summary>\n\n\n${result}</details>`; 1967 await add_message(window.conversation_id, "user", new_message); 1968 await safe_load_conversation(window.conversation_id); 1969 messages = messages.slice(0, -1).concat([{role: "user", content: new_message}, messages.slice(-1)[0]]); 1970 } 1971 } 1972 } 1973 1974 controller_storage[message_id] = new AbortController(); 1975 1976 // Get MCP tools if available 1977 const mcpTools = mcpClient && mcpClient.selectedTools.length > 0 1978 ? mcpClient.getSelectedToolsForAPI() 1979 : undefined; 1980 1981 // Handle chat completion (existing logic) 1982 const body = { 1983 model: selectedModel, 1984 messages, 1985 stream: true, 1986 signal: controller_storage[message_id].signal, 1987 ...(mcpTools && mcpTools.length > 0 ? { tools: mcpTools } : {}), 1988 ...(conversation.data ? { conversation: conversation.data[provider] } : {}), 1989 ...getExtraBody(provider) 1990 }; 1991 const response = await client.chat.completions.create(body); 1992 1993 add_message_chunk({type: "provider", provider: {name: provider, model: selectedModel, label: providerLabel}}, message_id); 1994 1995 if (!body.stream) { 1996 if (response.usage) { 1997 add_message_chunk({type: "usage", usage: response.usage}, message_id); 1998 } 1999 if (response.model) { 2000 let provider; 2001 if (response.server && response.provider) { 2002 provider = `custom:${response.server}`; 2003 } else if (response.provider) { 2004 provider = response.provider || provider; 2005 } 2006 add_message_chunk({type: "provider", provider: {name: provider, model: response.model, label: response.provider, server: response.server}}, message_id); 2007 } 2008 if (response.error) { 2009 add_message_chunk({type: "error", ...response.error}, message_id, null, finish_message); 2010 return; 2011 } 2012 if (response.conversation) { 2013 const conversation = await get_conversation(window.conversation_id); 2014 if (!conversation.data) { 2015 conversation.data = {}; 2016 } 2017 conversation.data[provider] = response.conversation; 2018 await save_conversation(update_conversation(conversation)); 2019 } 2020 if (response.choices) { 2021 const choice = response.choices[0]; 2022 if (choice.reasoning || choice.reasoning_content) { 2023 await add_message_chunk({type: "reasoning", token: choice.reasoning || choice.reasoning_content}, message_id); 2024 } 2025 if (choice.content) { 2026 await add_message_chunk({type: "content", content: choice.content}, message_id); 2027 } 2028 } 2029 await finish_message(); 2030 return; 2031 } 2032 2033 let hasModel = false;
2034 let sources = null; 2035 2036 for await (const chunk of response) { 2037 if (chunk.usage) { 2038 add_message_chunk({type: "usage", usage: chunk.usage}, message_id); 2039 } 2040 if (chunk.model && !hasModel) { 2041 hasModel = true; 2042 if (chunk.server && chunk.provider) { 2043 provider = `custom:${chunk.server}`; 2044 providerLabel = chunk.provider; 2045 } else if (chunk.provider) { 2046 provider = chunk.provider || provider; 2047 } 2048 add_message_chunk({type: "provider", provider: {name: provider, model: chunk.model, label: providerLabel, server: chunk.server}}, message_id); 2049 } 2050 if (chunk.error) { 2051 add_message_chunk({type: "error", ...chunk.error}, message_id, null, finish_message); 2052 return; 2053 } 2054 if (chunk.conversation) { 2055 const conversation = await get_conversation(window.conversation_id); 2056 if (!conversation.data) { 2057 conversation.data = {}; 2058 } 2059 conversation.data[provider] = chunk.conversation; 2060 await save_conversation(update_conversation(conversation)); 2061 } 2062 if (chunk.choices) { 2063 const choice = chunk.choices[0]; 2064 if (choice?.groundingMetadata?.groundingChunks) { 2065 sources = choice.groundingMetadata; 2066 } 2067 // Handle tool calls 2068 if (choice?.delta?.tool_calls) { 2069 await add_message_chunk({type: "tool_calls", tool_calls: choice.delta.tool_calls}, message_id); 2070 } 2071 if (choice?.delta?.reasoning || choice?.delta?.reasoning_content) { 2072 await add_message_chunk({type: "reasoning", token: choice.delta.reasoning || choice.delta.reasoning_content}, message_id); 2073 } 2074 if (choice?.delta?.content) { 2075 const delta = choice?.delta?.content || ''; 2076 await add_message_chunk({type: "content", content: delta}, message_id); 2077 } 2078 } 2079 if (chunk.citations) { 2080 sources = {citations: chunk.citations}; 2081 } 2082 if (chunk.sources) { 2083 sources = {sources: chunk.sources}; 2084 } 2085 } 2086 if (sources) { 2087 add_sources(sources, message_id); 2088 } 2089 if (tool_calls_storage[message_id] && tool_calls_storage[message_id].length > 0 && mcpClient) { 2090 const toolCalls = tool_calls_storage[message_id]; 2091 delete tool_calls_storage[message_id]; 2092 await handleToolCalls(toolCalls, messages, selectedModel, provider, message_id, finish_message); 2093 } 2094 } 2095 await finish_message(); 2096 } catch (err) { 2097 add_error(err, true); 2098 safe_remove_cancel_button(); 2099 error_storage[message_id] = `${err.message || err}`; 2100 content_map.inner.innerHTML += framework.markdown(`${framework.translate('**An error occurred:**')} ${error_storage[message_id]}`); 2101 await finish_message(); 2102 } 2103 return; 2104 } 2105 try { 2106 const apiKey = get_api_key_by_provider(provider); 2107 const downloadMedia = document.getElementById("download_media")?.checked; 2108 let apiBase; 2109 if (provider == "Custom") { 2110 apiBase = appStorage.getItem("Custom-api_base"); 2111 } 2112 const ignored = Array.from(settings.querySelectorAll("input.provider:not(:checked)")).map((el)=>el.value); 2113 const extraBody = getExtraBody(provider); 2114 const isAutomaticOrientation = appStorage.getItem("automaticOrientation") != "false"; 2115 const aspectRatio = isAutomaticOrientation ? (window.innerHeight > window.innerWidth ? "9:16" : "16:9") : null; 2116 let conversationData = null; 2117 if (provider == "AnyProvider") { 2118 conversationData = conversation.data; 2119 } else if (provider && conversation.data && provider in conversation.data) { 2120 conversationData = conversation.data[provider]; 2121 } 2122 controller_storage[message_id] = new AbortController(); 2123 // Get MCP tools if available 2124 const mcpTools = mcpClient && mcpClient.selectedTools.length > 0 2125 ? mcpClient.getSelectedToolsForAPI() 2126 : undefined; 2127 await api("conversation", { 2128 id: message_id, 2129 conversation_id: window.conversation_id, 2130 conversation: conversationData, 2131 model: model, 2132 web_search: searchButton.classList.contains("active"), 2133 provider: provider, 2134 messages: messages, 2135 action: action, 2136 download_media: downloadMedia, 2137 debug_mode: appStorage.getItem("debugMode") == "true", 2138 api_key: apiKey, 2139 base_url: apiBase, 2140 ignored: ignored, 2141 aspect_ratio: aspectRatio, 2142 ...(mcpTools && mcpTools.length > 0 ? { tools: mcpTools } : {}), 2143 ...extraBody 2144 }, Object.values(image_storage), message_id, finish_message); 2145 } catch (e) { 2146 add_error(e, true); 2147 } 2148}; 2149 2150async function scroll_to_bottom() { 2151 if (document.body.classList.contains("screen-reader")) { 2152 return; // Skip enhancements for screen readers 2153 } 2154 window.scrollTo(0, 0); 2155 chatBody.scrollTop = chatBody.scrollHeight; 2156} 2157 2158let autoScrollEnabled = true; 2159 2160setInterval(() => { 2161 // Auto-scroll if enabled 2162 if (autoScrollEnabled) { 2163 chatBody.scrollTop = chatBody.scrollHeight; 2164 } 2165}, 200); 2166 2167chatBody.addEventListener('scroll', () => { 2168 const atBottom = chatBody.scrollTop + chatBody.clientHeight >= chatBody.scrollHeight - 40; 2169 autoScrollEnabled = atBottom && chatBody.clientHeight > 0; 2170}); 2171 2172const clear_conversations = async () => { 2173 const elements = box_conversations.childNodes; 2174 let index = elements.length; 2175 2176 if (index > 0) { 2177 while (index--) { 2178 const element = elements[index]; 2179 if ( 2180 element.nodeType === Node.ELEMENT_NODE && 2181 element.tagName.toLowerCase() !== `button` 2182 ) { 2183 box_conversations.removeChild(element); 2184 } 2185 } 2186 } 2187}; 2188 2189const clear_conversation = async () => { 2190 let messages = chatBody.getElementsByTagName(`div`); 2191 2192 while (messages.length > 0) { 2193 chatBody.removeChild(messages[0]); 2194 } 2195}; 2196 2197var illegalRe = /[\/\?<>\\:\*\|":]/g; 2198var controlRe = /[\x00-\x1f\x80-\x9f]/g; 2199var reservedRe = /^\.+$/; 2200var windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i; 2201 2202function sanitize(input, replacement) { 2203 var sanitized = input 2204 .replace(illegalRe, replacement) 2205 .replace(controlRe, replacement) 2206 .replace(reservedRe, replacement) 2207 .replace(windowsReservedRe, replacement); 2208 return sanitized.replaceAll(/\/|#|\s{2,}/g, replacement).trim(); 2209} 2210 2211async function set_conversation_title(conversation_id, title) { 2212 conversation = await get_conversation(conversation_id) 2213 conversation.new_title = title; 2214 delete conversation.share; 2215 const new_id = sanitize(title, " "); 2216 const new_conv = await get_conversation(new_id); 2217 if (new_id && !new_conv) { 2218 await delete_conversation(conversation_id); 2219 title_ids_storage[conversation_id] = new_id; 2220 conversation.backup = conversation.backup || conversation.id; 2221 conversation.id = new_id; 2222 add_url_to_history(`#${new_id}`); 2223 } 2224 await save_conversation(conversation); 2225} 2226 2227const show_option = async (conversation_id) => { 2228 const conv = document.getElementById(`conv-${conversation_id}`); 2229 const choi = document.getElementById(`cho-${conversation_id}`); 2230 2231 conv.style.display = "none"; 2232 choi.style.display = "block"; 2233 2234 const el = document.getElementById(`convo-${conversation_id}`);
2235 const trash_el = el.querySelector(".fa-trash"); 2236 const title_el = el.querySelector("span.convo-title"); 2237 if (title_el) { 2238 const left_el = el.querySelector(".left"); 2239 const input_el = document.createElement("input"); 2240 input_el.value = title_el.innerText; 2241 input_el.classList.add("convo-title"); 2242 input_el.onclick = (e) => e.stopPropagation() 2243 input_el.onfocus = () => trash_el.style.display = "none"; 2244 input_el.onchange = () => set_conversation_title(conversation_id, input_el.value); 2245 input_el.onblur = () => set_conversation_title(conversation_id, input_el.value); 2246 left_el.removeChild(title_el); 2247 left_el.appendChild(input_el); 2248 } 2249}; 2250 2251const hide_option = async (conversation_id) => { 2252 const conv = document.getElementById(`conv-${conversation_id}`); 2253 const choi = document.getElementById(`cho-${conversation_id}`); 2254 2255 conv.style.display = "block"; 2256 choi.style.display = "none"; 2257 2258 const el = document.getElementById(`convo-${conversation_id}`); 2259 el.querySelector(".fa-trash").style.display = ""; 2260 const input_el = el.querySelector("input.convo-title"); 2261 if (input_el) { 2262 const left_el = el.querySelector(".left"); 2263 const span_el = document.createElement("span"); 2264 span_el.innerText = input_el.value; 2265 span_el.classList.add("convo-title"); 2266 left_el.removeChild(input_el); 2267 left_el.appendChild(span_el); 2268 } 2269}; 2270 2271const on_delete_conversation = async (conversation_id) => { 2272 const conversation = await get_conversation(conversation_id); 2273 for (const message of conversation.items) { 2274 if (Array.isArray(message.content)) { 2275 for (const item of message.content) { 2276 if (item.bucket_id) { 2277 await framework.delete(item.bucket_id); 2278 } 2279 } 2280 } 2281 } 2282 if (conversation.share) { 2283 await framework.delete(conversation.id); 2284 } 2285 2286 const { store, done } = await withStore('readwrite'); 2287 store.delete(conversation.id); 2288 if (window.conversation_id == conversation_id) { 2289 await new_conversation(); 2290 } 2291 2292 await load_conversations(); 2293 return done; 2294}; 2295 2296const on_star_conversation = async (conversation_id, target) => { 2297 const conversation = await get_conversation(conversation_id); 2298 if (conversation.star) { 2299 target.classList.remove("active"); 2300 } else { 2301 target.classList.add("active"); 2302 } 2303 await save_conversation(update_conversation({ 2304 ...conversation, 2305 star: !conversation.star 2306 })); 2307 await load_conversations(); 2308}; 2309 2310const on_preset_conversation = async (conversation_id) => { 2311 const conversation = await get_conversation(conversation_id); 2312 delete conversation.data; 2313 delete conversation.share; 2314 delete conversation.star; 2315 conversation.id = generateUUID(); 2316 conversation.items = conversation.items.slice(0, 2); 2317 conversation.title = `${framework.translate("1 Copy").split(" ").pop()}: ${conversation.title || framework.translate("No Title")}`; 2318 await save_conversation(update_conversation(conversation)); 2319 await set_conversation(conversation.id); 2320} 2321 2322const set_conversation = async (conversation_id) => { 2323 if (title_ids_storage[conversation_id]) { 2324 conversation_id = title_ids_storage[conversation_id]; 2325 } 2326 add_url_to_history(`#${conversation_id}`); 2327 window.conversation_id = conversation_id; 2328 2329 suggestions = null; 2330 await clear_conversation(); 2331 await load_conversation(await get_conversation(conversation_id)); 2332 play_last_message(); 2333 load_conversations(); 2334 hide_sidebar(true); 2335}; 2336 2337function merge_messages(message1, message2) { 2338 if (Array.isArray(message2) || !message1) { 2339 return message2; 2340 } 2341 let newContent = message2; 2342 // Remove start tokens 2343 if (newContent.startsWith("```")) { 2344 const index = newContent.indexOf("\n"); 2345 if (index != -1) { 2346 newContent = newContent.substring(index); 2347 } 2348 } else if (newContent.startsWith("...")) { 2349 newContent = " " + newContent.substring(3); 2350 } else if (newContent.startsWith(message1)) { 2351 newContent = newContent.substring(message1.length); 2352 } else { 2353 // Remove duplicate lines 2354 let lines = message1.trim().split("\n"); 2355 let lastLine = lines[lines.length - 1]; 2356 let foundLastLine = newContent.indexOf(lastLine + "\n"); 2357 if (foundLastLine != -1) { 2358 foundLastLine += 1; 2359 } else { 2360 foundLastLine = newContent.indexOf(lastLine); 2361 } 2362 if (foundLastLine != -1) { 2363 newContent = newContent.substring(foundLastLine + lastLine.length); 2364 } // Remove duplicate words 2365 else if (newContent.indexOf(" ") > 0) { 2366 let words = message1.trim().split(" "); 2367 let lastWord = words[words.length - 1]; 2368 if (newContent.startsWith(lastWord)) { 2369 newContent = newContent.substring(lastWord.length); 2370 } 2371 } 2372 } 2373 return message1 + newContent; 2374} 2375 2376// console.log(merge_messages("Hello", "Hello,\nhow are you?")); 2377// console.log(merge_messages("Hello", "Hello, how are you?")); 2378// console.log(merge_messages("Hello", "Hello,\nhow are you?")); 2379// console.log(merge_messages("Hello,\n", "Hello,\nhow are you?")); 2380// console.log(merge_messages("Hello,\n", "how are you?")); 2381// console.log(merge_messages("1 != 2", "1 != 2;")); 2382// console.log(merge_messages("1 != 2", "```python\n1 != 2;")); 2383// console.log(merge_messages("1 != 2;\n1 != 3;\n", "1 != 2;\n1 != 3;\n")); 2384 2385const load_conversation = async (conversation, append = false) => {
2386 console.log("Loading conversation...", conversation ? conversation.id : "new", append ? "(append)" : ""); 2387 if (!conversation) { 2388 return; 2389 } 2390 lastUpdated = conversation.updated; 2391 let messages = conversation?.items || []; 2392 console.debug("Conversation:", conversation.id) 2393 2394 let conversation_title = conversation.new_title || conversation.title; 2395 title = conversation_title ? `${conversation_title} - G4F` : window.title; 2396 if (title) { 2397 document.title = title; 2398 } 2399 const chatHeader = document.querySelector(".chat-top-panel .convo-title"); 2400 if (conversation.share) { 2401 chatHeader.innerHTML = '<i class="fa-solid fa-qrcode"></i> ' + framework.escape(conversation_title); 2402 } else if (window.conversation_id) { 2403 chatHeader.innerText = conversation_title; 2404 } 2405 2406 if (chatPrompt) { 2407 chatPrompt.value = conversation.system || ""; 2408 } 2409 2410 let elements = []; 2411 let last_model = null; 2412 let providers = []; 2413 let buffer = ""; 2414 let completion_tokens = 0; 2415 2416 if (!append) { 2417 chatBody.innerHTML = ""; 2418 } 2419 2420 messages.forEach((item, i) => { 2421 if (item.continue) { 2422 elements.pop(); 2423 } else { 2424 buffer = ""; 2425 } 2426 buffer = filter_message_content(buffer); 2427 new_content = filter_message_content(item.content); 2428 buffer = merge_messages(buffer, new_content); 2429 last_model = item.provider?.model; 2430 providers.push(item.provider?.name); 2431 let next_i = parseInt(i) + 1; 2432 let next_provider = item.provider ? item.provider : (messages.length > next_i ? messages[next_i].provider : null); 2433 let provider_label = item.provider?.label ? item.provider.label : item.provider?.name; 2434 let provider_link = item.provider?.name ? `<a href="${item.provider.modelUrl || item.provider.url || ('#' + item.provider.server) || ''}" target="_blank">${provider_label}</a>` : ""; 2435 let provider = provider_link ? ` 2436 <div class="provider" data-provider="${item.provider.name}"> 2437 ${provider_link} 2438 ${item.provider.model ? ' ' + framework.translate('with') + ' ' + (item.provider.modelLabel || item.provider.model) : ''} 2439 </div> 2440 ` : ""; 2441 let synthesize_url = ""; 2442 let synthesize_params; 2443 let synthesize_provider; 2444 let text = Array.isArray(buffer) && buffer.length ? buffer[0].text : buffer; 2445 if (!text) { 2446 text = item.reasoning ? item.reasoning.text : ""; 2447 } 2448 if (text) { 2449 if (!framework.backendUrl || appStorage.getItem("voice")) { 2450 // synthesize_params = (new URLSearchParams({input: filter_message(text), voice: appStorage.getItem("voice") || "alloy"})).toString(); 2451 // synthesize_url = `https://www.openai.fm/api/generate?${synthesize_params}`; 2452 synthesize_url = `https://g4f.space/ai/audio/${encodeURIComponent(filter_message(text))}?voice=${encodeURIComponent(appStorage.getItem("voice") || "alloy")}`; 2453 } else { 2454 if (item.synthesize) { 2455 synthesize_params = item.synthesize.data 2456 synthesize_provider = item.synthesize.provider; 2457 } else { 2458 synthesize_params = {text: filter_message(text)} 2459 synthesize_provider = "Gemini"; 2460 } 2461 synthesize_params = (new URLSearchParams(synthesize_params)).toString(); 2462 synthesize_url = `${framework.backendUrl}/backend-api/v2/synthesize/${synthesize_provider}?${synthesize_params}`; 2463 } 2464 } 2465 const file = new File([text], 'message.md', {type: 'text/plain'}); 2466 const objectUrl = URL.createObjectURL(file); 2467 2468 let add_buttons = []; 2469 // Find buttons to add 2470 actions = ["variant"] 2471 // Add continue button if possible 2472 if (buffer && item.role == "assistant" && !Array.isArray(buffer)) { 2473 let reason = "stop"; 2474 // Read finish reason from conversation 2475 if (item.finish && item.finish.reason) { 2476 reason = item.finish.reason; 2477 } 2478 let lines = buffer.trim().split("\n"); 2479 let lastLine = lines[lines.length - 1]; 2480 // Has a stop or error token at the end 2481 if (lastLine.endsWith("[aborted]") || lastLine.endsWith("[error]")) { 2482 reason = "error"; 2483 // Has an even number of start or end code tags 2484 } else if (reason == "stop" && buffer.split("```").length - 1 % 2 === 1) { 2485 reason = "length"; 2486 } 2487 if (reason != "stop") { 2488 actions.push("continue") 2489 } 2490 } 2491 2492 if (document.body.classList.contains("screen-reader")) { 2493 add_buttons.push(` 2494 <div role="group" aria-label="Message controls"> 2495 <button class="delete-message" aria-label="Delete message">Delete Message</button> 2496 <button class="volume-high" aria-label="Play audio" aria-pressed="false" title="Play audio">Play Audio</button>
2497 <button class="copy-to-clipboard" aria-label="Copy message to clipboard">Copy</button> 2498 </div> 2499 `); 2500 } else { 2501 add_buttons.push(`<button class="options_button"> 2502 <div> 2503 <span><i class="fa-solid fa-qrcode"></i></span> 2504 <span><i class="fa-brands fa-whatsapp"></i></span> 2505 <span><i class="fa-solid fa-volume-high"></i></i></span> 2506 <span><i class="fa-solid fa-print"></i></span> 2507 <span><i class="fa-solid fa-file-export"></i></span> 2508 <span><i class="fa-regular fa-clipboard"></i></span> 2509 </div> 2510 <i class="fa-solid fa-plus"></i> 2511 </button>`); 2512 } 2513 2514 if (actions.includes("variant")) { 2515 add_buttons.push(`<button class="regenerate_button"> 2516 <span>${framework.translate('Regenerate')}</span> 2517 <i class="fa-solid fa-rotate"></i> 2518 </button>`); 2519 } 2520 if (actions.includes("continue")) { 2521 if (messages.length >= i - 1) { 2522 add_buttons.push(`<button class="continue_button"> 2523 <span>${framework.translate('Continue')}</span> 2524 <i class="fa-solid fa-wand-magic-sparkles"></i> 2525 </button>`); 2526 } 2527 } 2528 2529 countTokensEnabled = appStorage.getItem("countTokens") != "false"; 2530 let next_usage; 2531 let prompt_tokens; 2532 if (countTokensEnabled) { 2533 if (!item.continue) { 2534 completion_tokens = 0; 2535 } 2536 completion_tokens += item.usage?.completion_tokens ? item.usage.completion_tokens : 0; 2537 next_usage = messages.length > next_i ? messages[next_i].usage : null; 2538 prompt_tokens = next_usage?.prompt_tokens ? next_usage?.prompt_tokens : 0 2539 } 2540 2541 const messageElement = ` 2542 <div class="message${item.regenerate ? " regenerate": ""}" data-index="${i}" data-object_url="${objectUrl}" data-synthesize_url="${synthesize_url}"> 2543 <div class="${item.role}"> 2544 ${item.role == "assistant" ? gpt_image : user_image} 2545 <i class="fa-solid fa-xmark"></i> 2546 ${item.role == "assistant" 2547 ? `<i class="fa-regular fa-phone-arrow-down-left"></i>` 2548 : `<i class="fa-regular fa-phone-arrow-up-right"></i>` 2549 } 2550 </div> 2551 <div class="content"> 2552 ${provider} 2553 <div class="content_inner"> 2554 ${item.reasoning ? render_reasoning(item.reasoning, true): ""} 2555 ${renderer(buffer)} 2556 </div> 2557 <div class="count"> 2558 ${countTokensEnabled ? count_words_and_tokens( 2559 item.reasoning ? item.reasoning.text + text : text, 2560 next_provider?.model, completion_tokens, prompt_tokens 2561 ) : ""} 2562 ${add_buttons.join("")} 2563 </div> 2564 </div> 2565 </div> 2566 `; 2567 const letter = document.createElement("div"); 2568 letter.innerHTML = messageElement; 2569 chatBody.appendChild(letter.firstElementChild); 2570 }); 2571 2572 chatBody.querySelectorAll("video").forEach((el) => { 2573 el.onloadedmetadata = () => { 2574 if (el.videoWidth > 0) { 2575 el.muted = true; 2576 el.onclick = () => el.click(); 2577 el.onmouseover = () => { 2578 el.loop = true; 2579 el.play() 2580 }; 2581 el.onmouseleave = () => { 2582 el.loop = false; 2583 }; 2584 el.ontouchstart = () => { 2585 el.loop = true; 2586 el.play(); 2587 }; 2588 el.ontouchend = () => { 2589 el.loop = false; 2590 }; 2591 } else { 2592 el.style.width = "300px"; 2593 el.style.height = "40px"; 2594 } 2595 } 2596 }); 2597 2598 if (suggestions) { 2599 if (!Array.isArray(suggestions)) { 2600 suggestions = [suggestions]; 2601 } 2602 suggestions_el = document.createElement("div"); 2603 suggestions_el.classList.add("suggestions"); 2604 suggestions.forEach((suggestion)=> { 2605 const el = document.createElement("button"); 2606 el.classList.add("suggestion"); 2607 el.innerHTML = `<span>${framework.escape(suggestion)}</span> <i class="fa-solid fa-turn-up"></i>`; 2608 el.onclick = async () => { 2609 suggestions = null; 2610 suggestions_el = chatBody.querySelector('.suggestions'); 2611 suggestions_el ? suggestions_el.remove() : null; 2612 await handle_ask(true, suggestion); 2613 } 2614 suggestions_el.appendChild(el); 2615 }); 2616 chatBody.querySelectorAll('.suggestions').forEach((suggestions_el) => suggestions_el.remove()); 2617 chatBody.appendChild(suggestions_el); 2618 } else if (countTokensEnabled && window.GPTTokenizer_cl100k_base) { 2619 let filtered = prepare_messages(messages, null, true, false); 2620 filtered = filtered.filter((item)=>!Array.isArray(item.content) && item.content); 2621 if (filtered.length > 0) { 2622 last_model = last_model?.startsWith("gpt-3") ? "gpt-3.5-turbo" : "gpt-4" 2623 let count_total = GPTTokenizer_cl100k_base?.encodeChat(filtered, last_model).length 2624 if (count_total > 0) { 2625 const count_total_el = document.createElement("div"); 2626 count_total_el.classList.add("count_total"); 2627 count_total_el.innerText = framework.translate("{0} total tokens").replace("{0}", count_total); 2628 chatBody.appendChild(count_total_el); 2629 } 2630 } 2631 } 2632 2633 await register_message_buttons(); 2634 highlight(chatBody); 2635 regenerate_button.classList.remove("regenerate-hidden"); 2636 chatBody.scrollTo({ top: chatBody.scrollHeight, behavior: "smooth" }); 2637}; 2638 2639async function safe_load_conversation(conversation_id) { 2640 let is_running = false 2641 for (const key in controller_storage) { 2642 if (!controller_storage[key].signal.aborted) { 2643 is_running = true; 2644 break 2645 } 2646 } 2647 if (!is_running) { 2648 await load_conversation(await get_conversation(conversation_id)); 2649 return true; 2650 } 2651 return false;
2652} 2653 2654function update_conversation(conversation) { 2655 conversation.updated = Date.now(); 2656 return conversation; 2657} 2658 2659async function get_messages(conversation_id) { 2660 const conversation = await get_conversation(conversation_id); 2661 return conversation?.items || []; 2662} 2663 2664async function add_conversation(conversation_id) { 2665 if (!conversation_id) { 2666 privateConversation = { 2667 id: conversation_id, 2668 title: "", 2669 added: Date.now(), 2670 system: chatPrompt?.value, 2671 items: [], 2672 } 2673 return; 2674 } 2675 if (!await get_conversation(conversation_id)) { 2676 await save_conversation(update_conversation({ 2677 id: conversation_id, 2678 title: "", 2679 added: Date.now(), 2680 system: chatPrompt?.value, 2681 items: [], 2682 })); 2683 } 2684 add_url_to_history(`#${conversation_id}`); 2685} 2686 2687async function save_system_message() { 2688 if (!window.conversation_id) { 2689 return; 2690 } 2691 const conversation = await get_conversation(window.conversation_id); 2692 if (conversation) { 2693 conversation.system = chatPrompt?.value; 2694 await save_conversation(update_conversation(conversation)); 2695 } 2696} 2697 2698const remove_message = async (conversation_id, index) => { 2699 const conversation = await get_conversation(conversation_id); 2700 const old_message = conversation.items[index]; 2701 let new_items = []; 2702 for (i in conversation.items) { 2703 if (i == index - 1) { 2704 if (!conversation.items[index]?.regenerate) { 2705 delete conversation.items[i]["regenerate"]; 2706 } 2707 } 2708 if (i != index) { 2709 new_items.push(conversation.items[i]) 2710 } 2711 } 2712 conversation.items = new_items; 2713 const data = update_conversation(conversation); 2714 await save_conversation(data); 2715 if (conversation.share) { 2716 const url = `${framework.backendUrl}/backend-api/v2/chat/${conversation.id}`; 2717 await fetch(url, { 2718 method: 'POST', 2719 headers: {'content-type': 'application/json'}, 2720 body: JSON.stringify(data), 2721 }); 2722 } 2723 if (Array.isArray(old_message.content)) { 2724 for (const item of old_message.content) { 2725 if (item.bucket_id) { 2726 await framework.delete(item.bucket_id); 2727 } 2728 } 2729 } 2730}; 2731 2732const get_message = async (conversation_id, index) => { 2733 const messages = await get_messages(conversation_id); 2734 if (index in messages) 2735 return messages[index]["content"]; 2736}; 2737 2738const add_message = async ( 2739 conversation_id, role, content, 2740 provider = null, 2741 message_index = -1, 2742 synthesize_data = null, 2743 regenerate = false, 2744 title = null, 2745 finish = null, 2746 usage = null, 2747 reasoning = null, 2748 do_continue = false 2749) => { 2750 const conversation = await get_conversation(conversation_id); 2751 if (!conversation) { 2752 return; 2753 } 2754 if (title) { 2755 conversation.title = title; 2756 } else if (!conversation.title && !Array.isArray(content)) { 2757 let new_value = content.trim(); 2758 let new_lenght = new_value.indexOf("\n"); 2759 new_lenght = new_lenght > 200 || new_lenght < 0 ? 200 : new_lenght; 2760 conversation.title = new_value.substring(0, new_lenght); 2761 } 2762 const new_message = { 2763 role: role, 2764 content: content, 2765 provider: provider, 2766 }; 2767 if (synthesize_data) { 2768 new_message.synthesize = synthesize_data; 2769 } 2770 if (regenerate) { 2771 new_message.regenerate = true; 2772 } 2773 if (finish) { 2774 new_message.finish = finish; 2775 } 2776 if (usage) { 2777 new_message.usage = usage; 2778 } 2779 if (reasoning) { 2780 new_message.reasoning = reasoning; 2781 } 2782 if (do_continue) { 2783 new_message.continue = true; 2784 } 2785 if (message_index == -1) { 2786 conversation.items.push(new_message); 2787 } else { 2788 const new_messages = []; 2789 conversation.items.forEach((item, index)=>{ 2790 new_messages.push(item); 2791 if (index == message_index) { 2792 new_messages.push(new_message); 2793 } 2794 }); 2795 conversation.items = new_messages; 2796 } 2797 data = update_conversation(conversation); 2798 await save_conversation(data); 2799 if (conversation.share) { 2800 const url = `${framework.backendUrl}/backend-api/v2/chat/${conversation.id}`; 2801 fetch(url, { 2802 method: 'POST', 2803 headers: {'content-type': 'application/json'}, 2804 body: JSON.stringify(data), 2805 }); 2806 } 2807 if (message_index == -1) { 2808 return conversation.items.length - 1; 2809 } else { 2810 return message_index + 1; 2811 } 2812}; 2813 2814const toLocaleDateString = (date) => { 2815 date = new Date(date);
2816 return date.toLocaleString('en-GB', {dateStyle: 'short', timeStyle: 'short', monthStyle: 'short'}).replace("/" + date.getFullYear(), ""); 2817} 2818 2819const load_conversations = async () => { 2820 let conversations = await list_conversations(); 2821 conversations.sort((a, b) => (b.updated || b.added) - (a.updated || a.added)); 2822 await clear_conversations(); 2823 conversations.forEach((conversation) => { 2824 // const length = conversation.items.map((item) => ( 2825 // !item.content.toLowerCase().includes("hello") && 2826 // !item.content.toLowerCase().includes("hi") && 2827 // item.content 2828 // ) ? 1 : 0).reduce((a,b)=>a+b, 0); 2829 // if (!length) { 2830 // appStorage.removeItem(`conversation:${conversation.id}`); 2831 // return; 2832 // } 2833 const shareIcon = conversation.share ? '<i class="fa-solid fa-qrcode"></i>': ''; 2834 const starIcon = conversation.star ? `<i onclick="on_preset_conversation('${conversation.id}', this); return false;" class="fa-solid fa-star" style="margin-right: 12px;"></i>`: ''; 2835 let convo = document.createElement("div"); 2836 convo.classList.add("convo"); 2837 convo.id = `convo-${conversation.id}`; 2838 let choise; 2839 if (document.body.classList.contains("screen-reader")) { 2840 choise = `<button onclick="on_delete_conversation('${conversation.id}')" class="delete"> 2841 ${framework.translate('Delete Conversation')} 2842 </button>`; 2843 } else { 2844 choise = ` 2845 <i onclick="show_option('${conversation.id}')" class="fa-solid fa-ellipsis-vertical" id="conv-${conversation.id}"></i> 2846 <div id="cho-${conversation.id}" class="choise" style="display:none;"> 2847 <i onclick="on_star_conversation('${conversation.id}', this)" class="fa-solid fa-star ${conversation.star ? 'active' : ''}"></i> 2848 <i onclick="on_delete_conversation('${conversation.id}')" class="fa-solid fa-trash"></i> 2849 <i onclick="hide_option('${conversation.id}')" class="fa-regular fa-x"></i> 2850 </div> 2851 `; 2852 } 2853 convo.innerHTML = ` 2854 <a class="left" href="#${conversation.id}" onclick="set_conversation('${conversation.id}'); return false;"> 2855 <i class="fa-regular fa-comments"></i> 2856 <span class="datetime">${conversation.updated ? toLocaleDateString(conversation.updated) : ""}</span> 2857 <span class="convo-title">${shareIcon} ${framework.escape(conversation.new_title ? conversation.new_title : conversation.title)}</span> 2858 ${starIcon} 2859 </a> 2860 ${choise} 2861 `; 2862 box_conversations.appendChild(convo); 2863 }); 2864}; 2865 2866const hide_input = document.querySelector(".chat-toolbar .hide-input"); 2867hide_input.addEventListener("click", async (e) => { 2868 const icon = hide_input.querySelector("i"); 2869 const func = icon.classList.contains("fa-angles-down") ? "add" : "remove"; 2870 const remv = icon.classList.contains("fa-angles-down") ? "remove" : "add"; 2871 icon.classList[func]("fa-angles-up"); 2872 icon.classList[remv]("fa-angles-down"); 2873 document.querySelector(".chat-footer .user-input").classList[func]("hidden"); 2874 document.querySelector(".chat-footer .buttons").classList[func]("hidden"); 2875}); 2876 2877function get_message_id() { 2878 random_bytes = (Math.floor(Math.random() * 1338377565) + 2956589730).toString( 2879 2 2880 ); 2881 unix = Math.floor(Date.now() / 1000).toString(2); 2882 2883 return BigInt(`0b${unix}${random_bytes}`).toString(); 2884}; 2885 2886async function hide_sidebar(remove_shown=false) { 2887 if (remove_shown && window.innerWidth < 640) { // Only apply on mobile 2888 sidebar.classList.remove("shown"); 2889 } 2890 settings.classList.add("hidden"); 2891 chat.classList.remove("hidden"); 2892 logStorage.classList.add("hidden"); 2893 await hide_settings(); 2894 if (window.location.hash.endsWith("#menu") || window.location.hash.endsWith("#settings")) { 2895 history.back(); 2896 } 2897} 2898 2899async function hide_settings() { 2900 settings.classList.add("hidden"); 2901 let provider_forms = document.querySelectorAll(".provider_forms from");
2902 Array.from(provider_forms).forEach((form) => form.classList.add("hidden")); 2903} 2904 2905sidebar_buttons.forEach((el) => el.addEventListener("click", async () => { 2906 // Animate sidebar buttons 2907 sidebar_buttons.forEach((el) => { 2908 el.classList.toggle("rotated"); 2909 }); 2910 // For desktop 2911 if (window.innerWidth >= 640) { 2912 // Toggle between shown and minimized only 2913 if (sidebar.classList.contains("shown")) { 2914 // Change from shown to minimized 2915 sidebar.classList.remove("shown"); 2916 sidebar.classList.add("minimized"); 2917 } else { 2918 // Change from minimized to shown 2919 sidebar.classList.remove("minimized"); 2920 sidebar.classList.add("shown"); 2921 } 2922 } 2923 // For mobile 2924 else { 2925 if (sidebar.classList.contains("shown")) { 2926 // Hide sidebar on mobile 2927 sidebar.classList.remove("shown"); 2928 } else { 2929 // Show sidebar on mobile 2930 sidebar.classList.add("shown"); 2931 } 2932 } 2933})); 2934 2935function add_url_to_history(url) { 2936 if (!window?.pywebview) { 2937 try { 2938 history.pushState({}, null, url); 2939 } catch (e) { 2940 console.error(e); 2941 } 2942 } 2943} 2944 2945async function show_menu() { 2946 sidebar.classList.add("shown"); 2947 sidebar.classList.remove("minimized"); 2948 await hide_settings(); 2949 add_url_to_history("#menu"); 2950} 2951 2952function open_settings() { 2953 if (settings.classList.contains("hidden")) { 2954 chat.classList.add("hidden"); 2955 sidebar.classList.remove("shown"); 2956 settings.classList.remove("hidden"); 2957 add_url_to_history("#settings"); 2958 } else { 2959 settings.classList.add("hidden"); 2960 chat.classList.remove("hidden"); 2961 add_url_to_history(window.conversation_id ? `#${window.conversation_id}` : window.location.search); 2962 } 2963 logStorage.classList.add("hidden"); 2964} 2965 2966const register_settings_storage = async () => { 2967 const optionElements = document.querySelectorAll(optionElementsSelector); 2968 optionElements.forEach((element) => { 2969 const storageKey = element.dataset.storageKey || element.id; 2970 if (element.type == "textarea") { 2971 element.addEventListener('input', async (event) => { 2972 appStorage.setItem(storageKey, element.value); 2973 }); 2974 } else { 2975 element.addEventListener('change', async (event) => { 2976 switch (element.type) { 2977 case "checkbox": 2978 appStorage.setItem(storageKey, element.checked); 2979 break; 2980 case "select-one": 2981 appStorage.setItem(storageKey, element.value); 2982 break; 2983 case "url": 2984 case "text": 2985 case "number": 2986 appStorage.setItem(storageKey, element.value); 2987 break; 2988 default: 2989 console.warn("Unresolved element type"); 2990 } 2991 }); 2992 } 2993 if (element.id.endsWith("-api_key")) { 2994 element.addEventListener('focus', async (event) => { 2995 if (element.dataset.value) { 2996 element.value = element.dataset.value 2997 } 2998 }); 2999 element.addEventListener('blur', async (event) => { 3000 element.dataset.value = element.value; 3001 if (element.value) { 3002 element.placeholder = element.value && element.value.length >= 22 ? (element.value.substring(0, 12)+"*".repeat(12)+element.value.substring(element.value.length-12)) : "*".repeat(element.value.length); 3003 } else if (element.placeholder != "api_key") { 3004 element.placeholder = ""; 3005 } 3006 element.value = "" 3007 }); 3008 } 3009 // Handle Custom-api_base changes to update custom provider dropdown 3010 if (element.id === "Custom-api_base") { 3011 element.addEventListener('input', async (event) => { 3012 updateCustomProviderOption(element.value); 3013 }); 3014 element.addEventListener('change', async (event) => { 3015 updateCustomProviderOption(element.value); 3016 }); 3017 } 3018 }); 3019} 3020 3021function updateCustomProviderOption(apiBaseValue) { 3022 const customOptgroup = document.getElementById("custom-providers-optgroup"); 3023 if (!customOptgroup) return; 3024 3025 const existingOption = customOptgroup.querySelector('option[value="Custom"]'); 3026 3027 if (apiBaseValue && apiBaseValue.trim()) { 3028 if (!existingOption) { 3029 const customOption = document.createElement("option"); 3030 customOption.value = "Custom"; 3031 customOption.dataset.live = "true"; 3032 customOption.dataset.custom = "true"; 3033 customOption.text = "Custom Provider ð§"; 3034 customOptgroup.appendChild(customOption); 3035 } 3036 } else { 3037 if (existingOption) { 3038 existingOption.remove(); 3039 } 3040 } 3041} 3042 3043async function loadCustomProvidersFromAPI(customOptgroup, providersContainer = null) { 3044 if (!customOptgroup) { 3045 customOptgroup = document.getElementById("custom-providers-optgroup"); 3046 } 3047 if (!customOptgroup) return; 3048 3049 try { 3050 let privateData; 3051 if (appStorage.getItem("session_token")) { 3052 const url = "https://g4f.space/custom/api/servers"; 3053 const resp = await fetch(url, { 3054 headers: {'Authorization': `Bearer ${appStorage.getItem("session_token") || ""}`} 3055 }); 3056 if (resp.status === 401) { 3057 appStorage.removeItem("session_token"); 3058 } 3059 privateData = await resp.json(); 3060 }
3061 const publicUrl = "https://g4f.space/custom/api/servers/public"; 3062 const publicResp = await fetch(publicUrl); 3063 let data = await publicResp.json(); 3064 data = data.servers; 3065 if (privateData) { 3066 if (privateData.servers) { 3067 data = data.concat(privateData.servers.filter(server=>!server.is_public)); 3068 } 3069 } 3070 // Store servers globally for client creation 3071 window.customServers = data; 3072 3073 data.forEach(server => { 3074 // Check if this server already exists in dropdown 3075 const existingOption = customOptgroup.querySelector(`option[data-server-id="${server.id}"]`); 3076 if (!existingOption) { 3077 const option = document.createElement("option"); 3078 option.value = `custom:${server.id}`; 3079 option.dataset.live = "true"; 3080 option.dataset.custom = "true"; 3081 option.dataset.serverId = server.id; 3082 option.dataset.baseUrl = server.base_url; 3083 option.dataset.label = server.label; 3084 3085 // Build label with model count if available 3086 let label = server.label || server.id; 3087 if (server.allowed_models && server.allowed_models.length > 0) { 3088 label += ` (${server.allowed_models.length} models)`; 3089 } 3090 option.text = `${label} ð`; 3091 3092 customOptgroup.appendChild(option); 3093 } 3094 3095 // Add to providers toggle list if container provided 3096 if (providersContainer) { 3097 const toggleContent = providersContainer.querySelector(".collapsible-content"); 3098 if (toggleContent && !toggleContent.querySelector(`#ProviderCustom${server.id}`)) { 3099 const providerItem = document.createElement("div"); 3100 providerItem.classList.add("provider-item", "custom-server-item"); 3101 const isEnabled = appStorage.getItem(`enableCustomServer_${server.id}`) !== "false"; 3102 providerItem.innerHTML = ` 3103 <span class="label">${server.label || server.id} ð</span> 3104 <input id="ProviderCustom${server.id}" type="checkbox" name="ProviderCustom${server.id}" value="custom:${server.id}" class="provider custom-server" data-server-id="${server.id}" ${isEnabled ? 'checked="checked"' : ''}/> 3105 <label for="ProviderCustom${server.id}" class="toogle" title="Enable or disable this custom server"></label> 3106 `; 3107 providerItem.querySelector("input").addEventListener("change", (event) => { 3108 appStorage.setItem(`enableCustomServer_${server.id}`, event.target.checked ? "true" : "false"); 3109 const option = customOptgroup.querySelector(`option[data-server-id="${server.id}"]`); 3110 if (option) { 3111 option.disabled = !event.target.checked; 3112 } 3113 }); 3114 toggleContent.appendChild(providerItem); 3115 } 3116 } 3117 }); 3118 } catch (e) { 3119 console.debug("Failed to load custom providers from API:", e); 3120 } 3121} 3122 3123async function load_settings(provider_options) { 3124 await register_settings_storage(); 3125 await load_settings_storage(); 3126 3127 Object.entries(provider_options).forEach( 3128 ([provider_name, option]) => load_provider_option(option.querySelector("input"), provider_name) 3129 ); 3130} 3131 3132const load_settings_storage = async () => { 3133 const optionElements = document.querySelectorAll(optionElementsSelector); 3134 optionElements.forEach((element) => { 3135 const storageKey = element.dataset.storageKey || element.id; 3136 let value = appStorage.getItem(storageKey); 3137 if (value == null && element.dataset.value) { 3138 value = element.dataset.value; 3139 } 3140 if (value) { 3141 switch (element.type) { 3142 case "checkbox": 3143 element.checked = value === "true"; 3144 break; 3145 case "select-one": 3146 element.value = value; 3147 break; 3148 case "url": 3149 case "text": 3150 case "number": 3151 case "textarea":
3152 if (element.id.endsWith("-api_key")) { 3153 element.placeholder = value && value.length >= 22 ? (value.substring(0, 12)+"*".repeat(12)+value.substring(value.length-12)) : "*".repeat(value ? value.length : 0); 3154 element.dataset.value = value; 3155 } else { 3156 element.value = value == null ? element.dataset.value : value; 3157 } 3158 break; 3159 default: 3160 console.warn("`Unresolved element type:", element.type); 3161 } 3162 } 3163 }); 3164} 3165 3166const say_hello = async () => { 3167 tokens = framework.translate(`Hello! How can I assist you today?`).split(" ").map((token) => token + " "); 3168 3169 let to_modify = document.querySelector(`.welcome-message`); 3170 if (!to_modify) { 3171 const message_container = document.createElement("div"); 3172 message_container.innerHTML = ` 3173 <div class="message"> 3174 <div class="assistant"> 3175 ${gpt_image} 3176 <i class="fa-regular fa-phone-arrow-down-left"></i> 3177 </div> 3178 <div class="content"> 3179 <p class=" welcome-message"></p> 3180 </div> 3181 </div> 3182 `; 3183 chatBody.appendChild(message_container.firstElementChild); 3184 } else { 3185 to_modify.textContent = ""; 3186 } 3187 3188 to_modify = document.querySelector(`.welcome-message`); 3189 for (token of tokens) { 3190 await new Promise(resolve => setTimeout(resolve, (Math.random() * (100 - 200) + 100))) 3191 to_modify.textContent += token; 3192 } 3193} 3194 3195function count_tokens(model, text, prompt_tokens = 0) { 3196 if (!text) { 3197 return 0; 3198 } 3199 if (model) { 3200 if (window.llamaTokenizer) 3201 if (model.startsWith("llama") || model.startsWith("codellama")) { 3202 return llamaTokenizer.encode(text).length; 3203 } 3204 if (window.mistralTokenizer) 3205 if (model.startsWith("mistral") || model.startsWith("mixtral")) { 3206 return mistralTokenizer.encode(text).length; 3207 } 3208 } 3209 if (window.GPTTokenizer_cl100k_base && (model?.startsWith("gpt-3") || model == "gpt-4")) { 3210 model = model?.startsWith("gpt-3") ? "gpt-3.5-turbo" : "gpt-4" 3211 return GPTTokenizer_cl100k_base?.encode(text, model).length; 3212 } else if (window.GPTTokenizer_o200k_base) { 3213 return GPTTokenizer_o200k_base?.encode(text, model).length; 3214 } else { 3215 return prompt_tokens; 3216 } 3217} 3218 3219function count_words(text) { 3220 return text.trim().match(/[\w\u4E00-\u9FA5]+/gu)?.length || 0; 3221} 3222 3223function count_chars(text) { 3224 return text.match(/[^\s\p{P}]/gu)?.length || 0; 3225} 3226 3227function calculateBase64Size(base64String) { 3228 // Remove any whitespace that might be in the base64 string 3229 const cleanBase64 = base64String.replace(/\s/g, ''); 3230 // Each base64 character represents 6 bits, and padding is accounted for 3231 const padding = (cleanBase64.match(/=/g) || []).length; 3232 const sizeInBytes = Math.floor((cleanBase64.length * 3) / 4) - padding; 3233 return sizeInBytes; 3234} 3235 3236function get_media_size(text) { 3237 if (Array.isArray(text) || !text) { 3238 return null; 3239 } 3240 3241 // Check for base64-encoded image in markdown format: [)](...) 3242 const imageMarkdownMatch = text.match(/!\[.*?\]\(data:image\/[^;]+;base64,([A-Za-z0-9+/=]+)\)/); 3243 if (imageMarkdownMatch && imageMarkdownMatch[1]) { 3244 return calculateBase64Size(imageMarkdownMatch[1]); 3245 } 3246 3247 // Check for base64-encoded media in video/audio tags: <video controls src="data:..."></video> 3248 const mediaTagMatch = text.match(/<(?:video|audio)[^>]*src="data:[^;]+;base64,([A-Za-z0-9+/=]+)"/); 3249 if (mediaTagMatch && mediaTagMatch[1]) { 3250 return calculateBase64Size(mediaTagMatch[1]); 3251 } 3252 3253 return null; 3254} 3255 3256function count_words_and_tokens(text, model, completion_tokens, prompt_tokens) { 3257 if (Array.isArray(text) || !text) { 3258 return ""; 3259 } 3260 3261 // Check if the message contains media (image/video) 3262 const mediaSize = get_media_size(text); 3263 if (mediaSize !== null) { 3264 // Show size instead of word/token count for media responses 3265 return `(${formatFileSize(mediaSize)})`; 3266 } 3267 3268 text = filter_message(text); 3269 return `(${count_words(text)} ${framework.translate('words')}, ${count_chars(text)} ${framework.translate('chars')}, ${completion_tokens ? completion_tokens : count_tokens(model, text, prompt_tokens)} ${framework.translate('tokens')})`; 3270} 3271function renderLargeMessage(container, content, chunkSize = 50) { 3272 if (content.length <= chunkSize * 100) { 3273 container.innerHTML = content; 3274 return; 3275 } 3276 3277 // Split content into chunks 3278 const lines = content.split("\n"); 3279 const chunks = []; 3280 const buffer = []; 3281 for (let i = 0; i < lines.length; i += 1) { 3282 buffer.push(lines[i]); 3283 if (buffer.length >= chunkSize || i === lines.length - 1) { 3284 chunks.push(buffer.join("\n")); 3285 buffer.length = 0; // Clear the buffer 3286 } 3287 } 3288 3289 // Render chunks progressively 3290 let index = 0; 3291 container.innerHTML = chunks[0]; 3292 3293 const renderNextChunk = () => { 3294 index++; 3295 if (index < chunks.length) { 3296 const tempDiv = document.createElement('div'); 3297 tempDiv.innerHTML = chunks[index]; 3298 while (tempDiv.firstChild) { 3299 container.appendChild(tempDiv.firstChild); 3300 } 3301 setTimeout(renderNextChunk, 10); 3302 } 3303 }; 3304 3305 setTimeout(renderNextChunk, 10); 3306} 3307 3308let countFocus = userInput; 3309const count_input = async () => { 3310 if (countTokensEnabled && countFocus.value) { 3311 if (window.matchMedia("(pointer:coarse)")) { 3312 inputCount.innerText = `(${count_tokens(get_selected_model(), countFocus.value)} tokens)`; 3313 } else { 3314 inputCount.innerText = count_words_and_tokens(countFocus.value, get_selected_model()); 3315 } 3316 } else { 3317 inputCount.innerText = ""; 3318 } 3319}; 3320userInput.addEventListener("keyup", count_input); 3321chatPrompt.addEventListener("keyup", count_input); 3322chatPrompt.addEventListener("focus", function() { 3323 countFocus = chatPrompt; 3324 count_input(); 3325}); 3326chatPrompt.addEventListener("input", function() { 3327 countFocus = userInput; 3328 count_input(); 3329}); 3330window.addEventListener("hashchange", async (event) => { 3331 iframe_container.classList.add("hidden"); 3332 iframe.src = ""; 3333 const locationHash = window.location.hash.substring(1); 3334 3335 if (locationHash == "login") { 3336 window.location.href='https://g4f.dev/members?redirect='+encodeURIComponent(location.href.split('#')[0])+'&conversation='+encodeURIComponent(window.conversation_id); 3337 return; 3338 } 3339 if (locationHash == "menu" || locationHash == "settings") { 3340 if (locationHash == "settings") { 3341 open_settings(); 3342 } 3343 return; 3344 } 3345 hide_sidebar(true); 3346 if (locationHash && locationHash != "new") { 3347 window.conversation_id = locationHash; 3348 set_conversation(locationHash); 3349 } else { 3350 window.conversation_id = generateUUID(); 3351 new_conversation(); 3352 } 3353}); 3354function render_startup_questions() { 3355 if (!Array.isArray(startup_questions) || !startup_questions.length) { 3356 return; 3357 } 3358 try { 3359 const used_startup_questions = startup_questions.sort(() => .5 - Math.random()).slice(0, 4); 3360 const suggestions_el = document.createElement("div"); 3361 suggestions_el.classList.add("suggestions"); 3362 used_startup_questions.forEach((suggestion)=> { 3363 const el = document.createElement("button"); 3364 el.classList.add("suggestion"); 3365 el.innerHTML = `<span>${framework.escape(suggestion)}</span> <i class="fa-solid fa-turn-up"></i>`; 3366 el.onclick = async () => { 3367 startup_questions = startup_questions.filter((q) => q != suggestion); 3368 await handle_ask(true, suggestion); 3369 } 3370 suggestions_el.appendChild(el); 3371 }); 3372 chatBody.querySelectorAll('.suggestions').forEach((suggestions_el) => suggestions_el.remove()); 3373 chatBody.appendChild(suggestions_el); 3374 } catch (e) { 3375 add_error("Failed to render startup questions:", e); 3376 } 3377} 3378async function load_startup_questions() { 3379 let prompt = `Generate a JSON-formatted list of engaging and diverse questions I can ask you at the start of a new conversation. 3380Example: 3381\`\`\`json 3382{ 3383 "q": [ 3384 "ð¤ What are the latest advancements in AI?", 3385 "ð¾âï¸ Can you help me plan a trip to Japan?", 3386 "ð¥ð What are some healthy meal ideas?" 3387 ] 3388} 3389\`\`\``; 3390 if (appStorage.getItem(framework.translationKey) && navigator.language.startsWith("en") == false) { 3391 prompt += `\nRespond in ${navigator.language}.`; 3392 } 3393 try { 3394 const response = await framework.query(prompt, {json: true, seed: Math.floor(Date.now() / 1000 / 3600 / 24)}); 3395 if (!response.ok) { 3396 throw new Error(`HTTP ${response.status}: ${await response.text()}`); 3397 } 3398 startup_questions = await response.json() 3399 startup_questions = startup_questions.q || startup_questions.questions || startup_questions; 3400 } catch (e) { 3401 add_error("Failed to parse startup questions:", e); 3402 } 3403} 3404load_startup_questions(); 3405async function load_follow_up_questions(messages, new_response) { 3406 if (appStorage.getItem("aiFeatures") !== "true") { 3407 return; 3408 } 3409 if (suggestions) { 3410 return; 3411 } 3412 messages = messages.filter((msg) => !Array.isArray(msg.content) && msg.content && msg.role === "user"); 3413 let prompt = `Suggest 3-4 follow-up questions that sound like they come from the user. 3414 Use first-person language and reflect the user's intent, curiosity, or goals. 3415 Stay relevant, avoid generic questions, and help deepen the conversation naturally. 3416 Generate a short conversation title with emojis. Keep it natural and relevant. Return as JSON with "questions" and "title" keys.`; 3417 prompt += ` 3418\`\`\`json 3419{ 3420 "title": "⨠ð§ The Next Steps", 3421 "q": [ 3422 "ð ï¸ Can you help me brainstorm ideas for a weekend project?", 3423 "ð©ðª What are some interesting facts about Germany I might not know?", 3424 "ð How do I stay productive when working late at night?", 3425 "ð What are some relaxing things to do before bed?" 3426 ] 3427} 3428\`\`\``; 3429 if (appStorage.getItem(framework.translationKey) && navigator.language.startsWith("en") == false) { 3430 prompt += `\n\nRespond in language ${navigator.language}.`; 3431 } 3432 const new_messages = [{role: "assistant", content: new_response}, {role: "user", content: prompt}]; 3433 console.log("Loading follow up questions with messages:", new_messages); 3434 try { 3435 const response = await fetch("https://g4f.space/ai/?json=true", { 3436 method: "POST", 3437 headers: { 3438 "Content-Type": "application/json", 3439 ...appStorage.getItem("session_token") ? {"Authorization": `Bearer ${appStorage.getItem("session_token")}`} : {} 3440 }, 3441 body: JSON.stringify({ 3442 messages: messages.concat(new_messages) 3443 })}); 3444 if (!response.ok) { 3445 throw new Error(`HTTP ${response.status}: ${await response.text()}`); 3446 } 3447 window.captureUserTierHeaders?.(response.headers); 3448 const follow_up_questions = await response.json() 3449 suggestions = follow_up_questions.q || follow_up_questions.questions || follow_up_questions; 3450 const conversation = await get_conversation(window.conversation_id); 3451 conversation.title = follow_up_questions.title || conversation.title || ""; 3452 await save_conversation(conversation); 3453 await load_conversations(); 3454 await safe_load_conversation(window.conversation_id); 3455 } catch (e) { 3456 add_error("Failed to parse follow up questions:", e); 3457 } 3458} 3459window.addEventListener('DOMContentLoaded', async function () { 3460 await on_load(); 3461 await on_api(); 3462 3463 if (window.conversation_id) { 3464 let conversation = await get_conversation(window.conversation_id); 3465 if (conversation && !conversation.share) { 3466 await load_conversation(conversation); 3467 await play_last_message(); 3468 return; 3469 } 3470 const response = await fetch(`${framework.backendUrl}/backend-api/v2/chat/${window.conversation_id}`, { 3471 headers: {'accept': 'application/json'}, 3472 }); 3473 if (!response.ok) { 3474 return await load_conversation(conversation); 3475 } 3476 conversation = await response.json(); 3477 if (conversation.id == window.conversation_id) { 3478 await save_conversation(conversation); 3479 await load_conversations(); 3480 } 3481 await load_conversation(window.conversation_id); 3482 } 3483 3484 // Set default sidebar state based on screen size 3485 if (window.innerWidth >= 640) { // 40em = 640px 3486 sidebar.classList.add("shown"); 3487 sidebar.classList.remove("minimized"); 3488 } else { 3489 sidebar.classList.remove("shown"); 3490 } 3491 // Ensure sidebar is shown by default on desktop 3492 if (window.innerWidth >= 640) { // 40em = 640px 3493 sidebar.classList.add("shown"); 3494 sidebar.classList.remove("minimized"); 3495 } 3496 3497}); 3498 3499let refreshOnHidden = true; 3500document.addEventListener("visibilitychange", () => { 3501 refreshOnHidden = !document.hidden; 3502}); 3503setInterval(async () => { 3504 if (!refreshOnHidden || !window.conversation_id) { 3505 return; 3506 }
3507 let conversation = await get_conversation(window.conversation_id); 3508 if (!conversation || !conversation.share) { 3509 return 3510 } 3511 refreshOnHidden = false; 3512 const now = Math.floor(Date.now() / 1000); 3513 const response = await fetch(`${framework.backendUrl}/backend-api/v2/chat/${conversation.id}?now=${now - now % 5}`, { 3514 headers: { 3515 'accept': 'application/json', 3516 'if-none-match': conversation.updated, 3517 }, 3518 }); 3519 refreshOnHidden = true; 3520 if (response.status == 200) { 3521 const new_conversation = await response.json(); 3522 if (conversation.id == window.conversation_id && new_conversation.updated != conversation.updated) { 3523 conversation = new_conversation; 3524 await save_conversation(conversation); 3525 } 3526 } 3527 if (lastUpdated != conversation.updated) { 3528 await load_conversations(); 3529 await load_conversation(conversation); 3530 } 3531}, 5000); 3532 3533window.addEventListener('pywebviewready', async function() { 3534 await on_api(); 3535}); 3536 3537window.addEventListener("load", (event) => { 3538 if (!window.location.hash.substring(1)) { 3539 render_startup_questions(); 3540 } 3541}); 3542 3543async function on_load() { 3544 translationSnipptes.forEach((snippet)=>this.framework.translate(snippet)); 3545 count_input(); 3546 const locationHash = window.location.hash.substring(1); 3547 if (locationHash === "login") { 3548 window.location.href='https://g4f.dev/members?redirect='+encodeURIComponent(location.href.split('#')[0])+'&conversation='+encodeURIComponent(window.conversation_id); 3549 return; 3550 } 3551 if (locationHash === "settings") { 3552 open_settings(); 3553 await load_conversations(); 3554 return; 3555 } 3556 let isNewConversation = locationHash === "" || ["new", "private"].includes(locationHash); 3557 if (!isNewConversation && !locationHash.startsWith("session=") && locationHash !== "menu") { 3558 window.conversation_id = locationHash; 3559 } else { 3560 window.conversation_id = generateUUID(); 3561 } 3562 chatPrompt.value = document.getElementById("systemPrompt")?.value || ""; 3563 let chatParams = new URLSearchParams(window.location.search); 3564 if (chatParams.get("prompt")) { 3565 userInput.value = chatParams.get("prompt"); 3566 userInput.focus(); 3567 window.history.replaceState({}, document.title, window.location.pathname + window.location.hash); 3568 } 3569 if (isNewConversation) { 3570 await new_conversation(locationHash === "private"); 3571 } else { 3572 await load_conversations(); 3573 } 3574 // Ensure sidebar is shown by default on desktop 3575 if (window.innerWidth >= 640) { 3576 sidebar.classList.add("shown"); 3577 sidebar.classList.remove("minimized"); 3578 } 3579} 3580 3581const load_provider_option = (input, provider_name) => { 3582 if (input.checked) { 3583 providerSelect.querySelectorAll(`option[value="${provider_name}"]:not([data-live="true"])`).forEach( 3584 (el) => el.removeAttribute("disabled") 3585 ); 3586 providerSelect.querySelectorAll(`option[data-parent="${provider_name}"]:not([data-live="true"])`).forEach( 3587 (el) => el.removeAttribute("disabled") 3588 ); 3589 settings.querySelector(`.field.box:has(label[for="${provider_name}-api_key"])`)?.classList.remove("hidden"); 3590 console.log(`.field.box:has(label[for="${provider_name}-api_key"])`) 3591 settings.querySelector(`.field.box:has(label[for="${provider_name}-api_base"])`)?.classList.remove("hidden"); 3592 } else { 3593 providerSelect.querySelectorAll(`option[value="${provider_name}"]:not([data-live="true"])`).forEach( 3594 (el) => el.setAttribute("disabled", "disabled") 3595 ); 3596 providerSelect.querySelectorAll(`option[data-parent="${provider_name}"]:not([data-live="true"])`).forEach( 3597 (el) => el.setAttribute("disabled", "disabled") 3598 ); 3599 } 3600}; 3601 3602async function load_providers(providers, provider_options, providersListContainer, providersToggleContainer) { 3603 providersToggleContainer = providersToggleContainer || settingsContent; 3604 providers.sort((a, b) => a.label.localeCompare(b.label)); 3605 const optGroupCore = document.createElement("optgroup"); 3606 optGroupCore.label = "Core Providers";
3607 providers.forEach((provider) => { 3608 if (provider.hf_space) { 3609 return; 3610 } 3611 let option = document.createElement("option"); 3612 option.value = provider.name; 3613 option.dataset.label = provider.label; 3614 option.text = provider.label 3615 + (window.getModelTags ? getModelTags(provider) : "") 3616 + (provider.hf_space ? " ð¤" : "") 3617 + (provider.nodriver ? " ð" : "") 3618 + (!provider.nodriver && provider.auth ? " ð" : "") 3619 + (provider.live > 0 ? " ð¢" : "") 3620 if (provider.parent) 3621 option.dataset.parent = provider.parent; 3622 optGroupCore.appendChild(option); 3623 }); 3624 providerSelect.appendChild(optGroupCore); 3625 providerSelect.selectedIndex = 0; 3626 if (!document.body.classList.contains("screen-reader")) { 3627 let providersContainer = document.createElement("div"); 3628 providersContainer.classList.add("field", "collapsible"); 3629 providersContainer.innerHTML = ` 3630 <div class="collapsible-header"> 3631 <span class="label">${framework.translate('Providers (Enable/Disable)')}</span> 3632 <i class="fa-solid fa-chevron-down"></i> 3633 </div> 3634 <div class="collapsible-content hidden"></div> 3635 `; 3636 providersToggleContainer.appendChild(providersContainer); 3637 3638 providers.forEach((provider) => { 3639 if (!provider.parent || provider.name == "PuterJS") { 3640 const name = provider.parent || provider.name; 3641 let option = document.createElement("div"); 3642 option.classList.add("provider-item"); 3643 let api_key = appStorage.getItem(`${name}-api_key`); 3644 option.innerHTML = ` 3645 <span class="label">${framework.translate("Enable")} ${provider.label}</span> 3646 <input id="Provider${name}" type="checkbox" name="Provider${name}" value="${name}" class="provider" ${(provider.active_by_default || api_key) ? 'checked="checked"' : ''}/> 3647 <label for="Provider${name}" class="toogle" title="Remove provider from dropdown"></label> 3648 `; 3649 option.querySelector("input").addEventListener("change", (event) => load_provider_option(event.target, name)); 3650 providersContainer.querySelector(".collapsible-content").appendChild(option); 3651 provider_options[name] = option; 3652 } 3653 }); 3654 3655 providersContainer.querySelector(".collapsible-header").addEventListener('click', (e) => { 3656 providersContainer.querySelector(".collapsible-content").classList.toggle('hidden'); 3657 providersContainer.querySelector(".collapsible-header").classList.toggle('active'); 3658 }); 3659 3660 // Add Live Providers toggle 3661 let liveProvidersToggle = document.createElement("div"); 3662 liveProvidersToggle.classList.add("provider-item"); 3663 const liveEnabled = appStorage.getItem("enableLiveProviders") !== "false"; 3664 liveProvidersToggle.innerHTML = ` 3665 <span class="label">Enable Live Providers</span> 3666 <input id="enableLiveProviders" type="checkbox" name="enableLiveProviders" value="live" class="provider-toggle" ${liveEnabled ? 'checked="checked"' : ''}/> 3667 <label for="enableLiveProviders" class="toogle" title="Enable or disable all live providers in dropdown"></label> 3668 `; 3669 liveProvidersToggle.querySelector("input").addEventListener("change", (event) => { 3670 appStorage.setItem("enableLiveProviders", event.target.checked ? "true" : "false"); 3671 const optgroup = document.getElementById("live-providers-optgroup"); 3672 if (optgroup) { 3673 optgroup.disabled = !event.target.checked; 3674 } 3675 }); 3676 providersContainer.querySelector(".collapsible-content").insertBefore(liveProvidersToggle, providersContainer.querySelector(".collapsible-content").firstChild); 3677 3678 // Add Custom Providers toggle 3679 let customProvidersToggle = document.createElement("div"); 3680 customProvidersToggle.classList.add("provider-item"); 3681 const customEnabled = appStorage.getItem("enableCustomProviders") !== "fal
3681se"; 3682 customProvidersToggle.innerHTML = ` 3683 <span class="label">Enable Custom Providers</span> 3684 <input id="enableCustomProviders" type="checkbox" name="enableCustomProviders" value="custom" class="provider-toggle" ${customEnabled ? 'checked="checked"' : ''}/> 3685 <label for="enableCustomProviders" class="toogle" title="Enable or disable custom providers in dropdown"></label> 3686 `; 3687 customProvidersToggle.querySelector("input").addEventListener("change", (event) => { 3688 appStorage.setItem("enableCustomProviders", event.target.checked ? "true" : "false"); 3689 const optgroup = document.getElementById("custom-providers-optgroup"); 3690 if (optgroup) { 3691 optgroup.disabled = !event.target.checked; 3692 } 3693 }); 3694 providersContainer.querySelector(".collapsible-content").insertBefore(customProvidersToggle, providersContainer.querySelector(".collapsible-content").firstChild.nextSibling); 3695 } 3696 load_provider_login_urls(providersListContainer, providers); 3697 await load_settings(provider_options);
3698 loadModels(providers); 3699} 3700function load_provider_login_urls(providersListContainer, providers = []) { 3701 for (const provider of providers) { 3702 if (provider.parent || provider.name == "AnyProvider") { 3703 continue; 3704 } 3705 let childs = providers.filter((p) => p.parent == provider.name).map((p) => p.name); 3706 let providerBox = document.createElement("div"); 3707 providerBox.classList.add("field", "box"); 3708 if (!provider.active_by_default || appStorage.getItem(`Provider${provider.name}`) === "false") { 3709 providerBox.classList.add("hidden"); 3710 } 3711 let isChecked = false; 3712 async function checkStatus() { 3713 setTimeout(async () => { 3714 if (isChecked) { 3715 return; 3716 } 3717 isChecked = true; 3718 const label = providerBox.querySelector('label'); 3719 if (!label) { 3720 return; 3721 } 3722 label.textContent = label.textContent.replaceAll(" â ", "") + " ð"; 3723 const quota = await get_quota(provider.name); 3724 label.textContent = label.textContent.replaceAll(" ð", "").replaceAll(" â ", "") 3725 if (quota) { 3726 label.textContent += " â "; 3727 } 3728 }, Math.random() * 100); 3729 } 3730 providerBox.addEventListener('mouseenter', checkStatus); 3731 const label = provider.label || provider.name; 3732 childs = childs.map((child) => `${child}-api_key`).join(" "); 3733 const login_provider = provider.name.replace("AI", "").replace("Api", "").toLowerCase(); 3734 let oauthButton = ""; 3735 3736 // Add OAuth button for providers that support it (server-side endpoint) 3737 if (provider.login) { 3738 oauthButton = `<button class="oauth-btn" data-provider="${provider.name}" data-login-url="/backend-api/v2/oauth/${provider.name}" title="${framework.translate("Login to")} ${framework.escape(label)}">${framework.translate('Login')}</button>`; 3739 } 3740 3741 const apiKeyLink = ["PollinationsAI", "HuggingFace", "ApiAirforce"].includes(provider.name) 3742 ? `<a href="https://g4f.dev/members?provider=${login_provider}&redirect=${encodeURIComponent(window.location.href.split("#")[0])}" title="${framework.translate("Login to")} ${framework.escape(label)}">${framework.translate('Login')}</a>` 3743 : (provider.login_url ? `<a href="${framework.escape(provider.login_url)}" target="_blank" title="${framework.translate("Login to")} ${framework.escape(label)}">${framework.translate('Get API key')}</a>` : ""); 3744 const inputId = `${provider.name}-api_key`; 3745 const storageKey = provider.name == "PuterJS" ? "puter.auth.token" : inputId; 3746 providerBox.innerHTML = ` 3747 <label for="${inputId}" class="label" title="">${framework.escape(label)}:</label> 3748 ` + (oauthButton || (apiKeyLink ? ` 3749 <input type="text" id="${inputId}" name="${provider.name}[api_key]" class="${childs}" placeholder="api_key" autocomplete="off" data-storage-key="${storageKey}"/> 3750 ` + apiKeyLink : "")); 3751 3752 if (provider.name == "PuterJS") { 3753 const link = providerBox.querySelector("a"); 3754 link.textContent = framework.translate("Login"); 3755 link.addEventListener("click", async (event) => { 3756 event.preventDefault(); 3757 await (new window.Puter()).signIn().then((res) => { 3758 console.log('PuterJS signed in:', res); 3759 providerBox.querySelector("input").value = res.token; 3760 appStorage.setItem(storageKey, res.token); 3761 }); 3762 }); 3763 } 3764 3765 providerBox.addEventListener("click", () => { 3766 isChecked = false; 3767 setTimeout(checkStatus, 100); 3768 }); 3769 3770 // Add OAuth button event listener 3771 if (oauthButton) { 3772 providerBox.querySelector(".oauth-btn").addEventListener("click", async (event) => { 3773 const provider = event.target.dataset.provider; 3774 event.target.disabled = true; 3775 event.target.textContent = "Authenticating..."; 3776 try { 3777 const loginUrl = event.target.dataset.loginUrl || `/backend-api/v2/oauth/${provider}`; 3778 const response = await fetch(loginUrl, { 3779 method: "POST", 3780 headers: { 3781 "Content-Type": "application/json" 3782 }, 3783 body: JSON.stringify({ action: "start" }) 3784 }); 3785 const result = await response.json(); 3786 3787 if (result.status === "pending" && result.user_code && result.verification_uri) { 3788 showOAuthCodePrompt(result.user_code, result.verification_uri); 3789 showToast("GitHub Copilot authorization started. Click Open GitHub and enter the code.", "info", 10000); 3790 3791 // Poll for completion 3792 let pollResult; 3793 const maxPollAttempts = 45; 3794 let pollAttempts = 0; 3795 while (pollAttempts < maxPollAttempts) { 3796 pollAttempts += 1; 3797 await new Promise(resolve => setTimeout(resolve, result.interval ? result.interval * 1000 : 5000)); 3798 const pollResponse = await fetch(loginUrl, { 3799 method: "POST", 3800 headers: { "Content-Type": "application/json" }, 3801 body: JSON.stringify({ action: "poll", device_code: result.device_code }) 3802 }); 3803 pollResult = await pollResponse.json(); 3804 3805 if (pollResult.status === "success") { 3806 showToast("OAuth authentication successful!", "success"); 3807 await load_providers(providers, {}, providersListContainer, null); 3808 break; 3809 } 3810 if (pollResult.status !== "pending") { 3811 showToast(`OAuth failed: ${pollResult.error?.message || pollResult.message || "Unknown error"}`, "error"); 3812 break; 3813 } 3814 } 3815 3816 if (pollAttempts >= maxPollAttempts) { 3817 showToast("OAuth poll timed out. Please retry.", "error"); 3818 } 3819 3820 } else if (result.status === "success") { 3821 showToast("OAuth authentication successful!", "success"); 3822 await load_providers(providers, {}, providersListContainer, null); 3823 } else { 3824 showToast(`OAuth failed: ${result.error?.message || result.message || "Unknown error"}`, "error"); 3825 } 3826 } catch (error) { 3827 showToast(`OAuth error: ${error.message}`, "error"); 3828 } finally { 3829 event.target.disabled = false;
3830 event.target.textContent = framework.translate('Login'); 3831 } 3832 }); 3833 } 3834 providersListContainer.querySelector(".collapsible-content").appendChild(providerBox); 3835 } 3836} 3837async function on_api() { 3838 load_version(); 3839 let prompt_lock = false; 3840 userInput.addEventListener("keydown", async (evt) => { 3841 if (prompt_lock) return; 3842 // If not mobile and not shift enter 3843 let do_enter = userInput.value.endsWith("\n\n\n\n"); 3844 if (do_enter || !window.matchMedia("(pointer:coarse)").matches && evt.keyCode === 13 && !evt.shiftKey) { 3845 evt.preventDefault(); 3846 console.log("pressed enter"); 3847 prompt_lock = true; 3848 setTimeout(()=>prompt_lock=false, 3000); 3849 await handle_ask(!do_enter); 3850 } 3851 }); 3852 let timeoutBlur = null; 3853 userInput.addEventListener("focus", async (evt) => { 3854 userInput.style.height = userInputHeight?.value + "px"; 3855 }); 3856 userInput.addEventListener("blur", async (evt) => { 3857 timeoutBlur = setTimeout(() => userInput.style.height = "", 200); 3858 }); 3859 codeButton?.addEventListener("click", async () => { 3860 clearTimeout(timeoutBlur); 3861 insertBackticksInTextarea(userInput); 3862 }); 3863 sendButton.addEventListener(`click`, async () => { 3864 console.log("clicked send"); 3865 if (prompt_lock) return; 3866 prompt_lock = true; 3867 setTimeout(()=>prompt_lock=false, 3000); 3868 stopRecognition(); 3869 await handle_ask(); 3870 }); 3871 addButton.addEventListener(`click`, async () => { 3872 stopRecognition(); 3873 await handle_ask(false); 3874 }); 3875 userInput.addEventListener(`click`, async () => { 3876 stopRecognition(); 3877 }); 3878 3879 // Get the Providers tab containers (or fall back to settingsContent for backward compatibility) 3880 const providersApiKeysContainer = document.getElementById("providers-api-keys-container") || settingsContent; 3881 const providersToggleContainer = document.getElementById("providers-toggle-container") || settingsContent; 3882 3883 let providersListContainer = document.createElement("div"); 3884 providersListContainer.classList.add("field", "collapsible"); 3885 providersListContainer.innerHTML = ` 3886 <div class="collapsible-header"> 3887 <span class="label">${framework.translate('Providers API key')}</span> 3888 <i class="fa-solid fa-chevron-down"></i> 3889 </div> 3890 <div class="collapsible-content api-key hidden"></div> 3891 `; 3892 providersApiKeysContainer.appendChild(providersListContainer); 3893 3894 providersListContainer.querySelector(".collapsible-header").addEventListener('click', (e) => { 3895 providersListContainer.querySelector(".collapsible-content").classList.toggle('hidden'); 3896 providersListContainer.querySelector(".collapsible-header").classList.toggle('active'); 3897 }); 3898 if (providerSelect) { 3899 // Add Live Providers optgroup 3900 const optgroup = document.createElement("optgroup"); 3901 optgroup.id = "live-providers-optgroup"; 3902 optgroup.label = framework.translate('Live Providers'); 3903 const liveProvidersEnabled = appStorage.getItem("enableLiveProviders") !== "false"; 3904 if (!liveProvidersEnabled) { 3905 optgroup.disabled = true; 3906 } 3907 providerSelect.appendChild(optgroup); 3908 async function updateLiveProviderOptions() { 3909 try { 3910 Object.entries(await window.loadProviders()).forEach(([name, config]) => { 3911 if (name === "custom") { 3912 return; // Skip custom here, will be added separately 3913 } 3914 if (["together", "huggingface", "typegpt"].includes(name) && !appStorage.getItem(window.providerLocalStorage[name])) { 3915 return; 3916 } 3917 let option = document.createElement("option"); 3918 if (name === config.defaultModel) { 3919 option.selected = true; 3920 } 3921 option.value = name; 3922 option.dataset.live = "true"; 3923 option.text = (config.label || name) + (config.tags ? ` ${config.tags} ð¢` : " ð¢"); 3924 optgroup.appendChild(option); 3925 }); 3926 providerSelect.value = "default"; 3927 } catch(e) { 3928 add_error(e, true); 3929 } 3930 } 3931 3932 // Add Custom Providers optgroup 3933 const customOptgroup = document.createElement("optgroup"); 3934 customOptgroup.id = "custom-providers-optgroup"; 3935 customOptgroup.label = framework.translate('Custom Providers'); 3936 const customProvidersEnabled = appStorage.getItem("enableCustomProviders") !== "fal
3936se"; 3937 if (!customProvidersEnabled) { 3938 customOptgroup.disabled = true; 3939 } 3940 providerSelect.appendChild(customOptgroup); 3941 async function loadCustomProvidersSelect() { 3942 try { 3943 // Add Custom provider if configured (local custom provider) 3944 if (appStorage.getItem("Custom-api_base")) { 3945 const customOption = document.createElement("option"); 3946 customOption.value = "custom"; 3947 customOption.dataset.live = "true"; 3948 customOption.dataset.custom = "true"; 3949 customOption.text = "Custom Provider ð§"; 3950 customOptgroup.appendChild(customOption); 3951 } 3952 // Load custom providers from API and add to toggle list 3953 await loadCustomProvidersFromAPI(document.getElementById("custom-providers-optgroup")); 3954 } catch(e) { 3955 add_error(e, true); 3956 } 3957 } 3958 3959 // Add PA Providers optgroup 3960 const paOptgroup = document.createElement("optgroup"); 3961 paOptgroup.id = "pa-providers-optgroup"; 3962 paOptgroup.label = framework.translate('PA Providers'); 3963 providerSelect.appendChild(paOptgroup); 3964 3965 await Promise.all([updateLiveProviderOptions(), loadCustomProvidersSelect(), loadPaProviderSelect(paOptgroup)]); 3966 3967 let provider_options = []; 3968 await api("providers").then(async (providers) => { 3969 await load_providers(providers, provider_options, providersListContainer, providersToggleContainer); 3970 loadProviderModels(appStorage.getItem("provider")); 3971 }).catch(async (e)=>{ 3972 console.log(e) 3973 providerSelect.querySelectorAll("option:not([data-live])").forEach((el)=>el.remove()); 3974 await load_provider_login_urls(providersListContainer, providers); 3975 await load_settings(provider_options); 3976 await loadProviderModels(appStorage.getItem("provider")); 3977 }); 3978 3979 set_favorite_providers(); 3980 } else { 3981 await load_provider_login_urls(providersListContainer, providers); 3982 await load_settings({}); 3983 await initClient(); 3984 } 3985 3986 const update_systemPrompt_icon = (checked) => { 3987 slide_systemPrompt_icon.classList[checked ? "remove": "add"]("fa-angles-up"); 3988 slide_systemPrompt_icon.classList[checked ? "add": "remove"]("fa-angles-down"); 3989 chatPrompt.classList[checked ? "add": "remove"]("hidden"); 3990 }; 3991 if (appStorage.getItem("hide-systemPrompt") == "true") { 3992 update_systemPrompt_icon(true); 3993 } 3994 slide_systemPrompt_icon.addEventListener("click", ()=>{ 3995 update_systemPrompt_icon(slide_systemPrompt_icon.classList.contains("fa-angles-up")); 3996 }); 3997 hide_systemPrompt ? hide_systemPrompt.addEventListener('change', async (event) => { 3998 update_systemPrompt_icon(event.target.checked); 3999 }) : null; 4000 const darkMode = document.getElementById("darkMode"); 4001 if (darkMode) { 4002 darkMode.addEventListener('change', async (event) => { 4003 if (event.target.checked) { 4004 document.body.classList.remove("white"); 4005 } else { 4006 document.body.classList.add("white"); 4007 } 4008 }); 4009 } 4010 const liquid = document.getElementById("liquid"); 4011 if (liquid) { 4012 liquid.addEventListener('change', async (event) => { 4013 if (event.target.checked) { 4014 document.body.classList.add("liquid"); 4015 } else { 4016 document.body.classList.remove("liquid"); 4017 } 4018 }); 4019 } 4020 const disableAnimations = document.getElementById("disableAnimations"); 4021 if (disableAnimations) { 4022 disableAnimations.addEventListener('change', async (event) => { 4023 if (event.target.checked) { 4024 document.body.classList.add("no-animations"); 4025 } else { 4026 document.body.classList.remove("no-animations"); 4027 } 4028 }); 4029 } 4030 4031 document.getElementById('recognition-language').placeholder = await get_recognition_language(); 4032} 4033 4034async function load_version() { 4035 let new_version = document.querySelector(".new_version"); 4036 if (new_version) return; 4037 let text = "version ~ " 4038 api("version").then((versions)=>{ 4039 window.title = 'G4F - ' + versions["version"]; 4040 if (document.title == "G4F Chat") { 4041 document.title = window.title; 4042 } 4043 if (versions["latest_version"] && versions["version"] != ver
4043sions["latest_version"]) { 4044 let release_url = 'https://github.com/xtekky/gpt4free/releases/latest'; 4045 let title = `${framework.translate('New version:')} ${versions["latest_version"]}`; 4046 text += `<a href="${release_url}" target="_blank" title="${title}">${versions["version"]}</a> ð`; 4047 new_version = document.createElement("div"); 4048 new_version.classList.add("new_version"); 4049 const link = `<a href="${release_url}" target="_blank" title="${title}">v${versions["latest_version"]}</a>`; 4050 new_version.innerHTML = `G4F ${link} ð`; 4051 new_version.addEventListener("click", ()=>new_version.parentElement.removeChild(new_version)); 4052 document.body.appendChild(new_version); 4053 } else { 4054 text += versions["version"]; 4055 } 4056 document.getElementById("version_text").innerHTML = text 4057 }).catch((e)=>{ 4058 console.error("Error loading version:", e); 4059 fetch("https://api.github.com/repos/xtekky/gpt4free/releases/latest").then((response)=>response.json()).then((data)=>{ 4060 document.getElementById("version_text").innerText = text + data.tag_name; 4061 }); 4062 }); 4063 setTimeout(load_version, 1000 * 60 * 60); // 1 hour 4064} 4065 4066function renderMediaSelect() { 4067 const oldImages = mediaSelect.querySelectorAll("a:has(img)"); 4068 oldImages.forEach((el)=>el.remove()); 4069 Object.entries(image_storage).forEach(async ([object_url, file]) => { 4070 const bucket_id = generateUUID(); 4071 const link = document.createElement("a"); 4072 link.title = file.name; 4073 const img = document.createElement("img"); 4074 img.src = object_url; 4075 img.onclick = async () => { 4076 link.remove(); 4077 delete image_storage[object_url]; 4078 await framework.delete(item.bucket_id); 4079 } 4080 img.onload = () => { 4081 link.title += `\n${img.naturalWidth}x${img.naturalHeight}`; 4082 }; 4083 img.onerror = () => { 4084 img.remove(); 4085 delete image_storage[object_url]; 4086 } 4087 link.appendChild(img); 4088 mediaSelect.appendChild(link); 4089 if (file instanceof File && window.location.protocol == "https:") { 4090 const formData = new FormData(); 4091 formData.append('files', file); 4092 const response = await fetch(framework.backendUrl + "/backend-api/v2/files/" + bucket_id, { 4093 method: 'POST', 4094 body: formData 4095 }); 4096 const result = await response.json() 4097 if (result.media) { 4098 const media = []; 4099 result.media.forEach((part)=> { 4100 part = part.name ? part : {name: part}; 4101 let url = framework.backendUrl ? framework.backendUrl : window.location.origin; 4102 url = `${url}/files/${bucket_id}/media/${part.name}`; 4103 delete image_storage[object_url]; 4104 object_url = url.replaceAll("/media/", "/thumbnail/"); 4105 image_storage[object_url] = {bucket_id: bucket_id, url: url, ...part}; 4106 }); 4107 } 4108 } 4109 }); 4110} 4111 4112imageInput ? imageInput.onclick = () => mediaSelect.classList.toggle("hidden") : null; 4113 4114mediaSelect.querySelector(".close").onclick = () => { 4115 if (Object.values(image_storage).length) { 4116 Object.entries(image_storage).forEach(async ([object_url, file]) => { 4117 if (file instanceof File) { 4118 URL.revokeObjectURL(object_url) 4119 } else if (file.bucket_id) { 4120 await framework.delete(file.bucket_id); 4121 } 4122 }); 4123 image_storage = {}; 4124 renderMediaSelect(); 4125 } else { 4126 mediaSelect.classList.add("hidden"); 4127 } 4128} 4129 4130[imageSelect, cameraInput].filter(el=>el).forEach((el) => { 4131 el.addEventListener('change', async () => { 4132 if (el.files.length) { 4133 Array.from(el.files).forEach((file) => { 4134 image_storage[URL.createObjectURL(file)] = file; 4135 }); 4136 el.value = ""; 4137 renderMediaSelect(); 4138 } 4139 }); 4140}); 4141 4142async function upload_audio(blob) { 4143 const loadingIndicator = document.createElement('div'); 4144 loadingIndicator.className = 'file-upload-loading'; 4145 loadingIndicator.innerHTML = ` 4146 <div class="upload-spinner"></div> 4147 <p>${framework.translate("Uploading audio...")}</p> 4148 `; 4149 document.body.appendChild(loadingIndicator); 4150 try { 4151 const formData = new FormData(); 4152 formData.append('files', blob); 4153 const bucket_id = generateUUID(); 4154 const response = await fetch(framework.backendUrl + "/backend-api/v2/files/" + bucket_id, { 4155 method: 'POST', 4156 body: formData, 4157 headers: { 4158 "x-recognition-language": await get_recognition_language() 4159 } 4160 }); 4161 if (!response.ok) { 4162 inputCount.innerText = framework.translate("Error uploading audio"); 4163 return; 4164 } 4165 const result = await response.json() 4166 if (result.media) { 4167 const media = [];
4168 result.media.forEach((part)=> { 4169 part = part.name ? part : {name: part}; 4170 const url = `${framework.backendUrl}/files/${bucket_id}/media/${part.name}`; 4171 media.push({bucket_id: bucket_id, url: url, ...part}); 4172 }); 4173 await handle_ask(false, media); 4174 } 4175 } finally { 4176 document.body.removeChild(loadingIndicator); 4177 } 4178} 4179 4180audioButton.addEventListener('click', async (event) => { 4181 const i = audioButton.querySelector("i"); 4182 const t = audioButton.querySelector("*"); 4183 if (mediaRecorder) { 4184 i.classList.remove("fa-stop"); 4185 i.classList.add("fa-microphone"); 4186 mediaRecorder.stop(); 4187 t.innerText = framework.translate("Upload Audio"); 4188 if(mediaRecorder.stream) { 4189 mediaRecorder.stream.getTracks().forEach(track => track.stop()); 4190 } 4191 if (mediaRecorder.wavBlob) { 4192 if (modelSelect.selectedIndex >= 0 && modelSelect.options[modelSelect.selectedIndex].dataset.audio) { 4193 await add_conversation(window.conversation_id); 4194 provider = get_selected_provider(); 4195 model = get_selected_model(); 4196 await ask_gpt(get_message_id(), -1, false, provider, model, "next"); 4197 } else { 4198 await upload_audio(mediaRecorder.wavBlob); 4199 } 4200 t.innerText = framework.translate("Record Audio"); 4201 } 4202 mediaRecorder = null; 4203 return; 4204 } 4205 4206 i.classList.remove("fa-microphone"); 4207 i.classList.add("fa-stop"); 4208 t.innerText = framework.translate("Stop Recording"); 4209 4210 try { 4211 stream = await navigator.mediaDevices.getUserMedia({ 4212 audio: true 4213 }); 4214 4215 //if (modelSelect.selectedIndex && modelSelect.options[modelSelect.selectedIndex].dataset.audio) { 4216 mediaRecorder = new Recorder(stream); 4217 mediaRecorder.start(); 4218 return; 4219 //} 4220 4221 if (!MediaRecorder.isTypeSupported('audio/webm')) { 4222 console.warn('audio/webm is not supported'); 4223 } 4224 mediaRecorder = new MediaRecorder(stream, { 4225 mimeType: 'audio/webm', 4226 }); 4227 4228 mediaRecorder.addEventListener('dataavailable', async event => { 4229 await upload_audio(event.data); 4230 t.innerText = framework.translate("Record Audio"); 4231 }); 4232 4233 mediaRecorder.start() 4234 } catch (err) { 4235 console.error('Error accessing microphone:', err); 4236 i.classList.remove("fa-stop"); 4237 i.classList.add("fa-microphone"); 4238 t.innerText = framework.translate("Record Audio"); 4239 if(mediaRecorder?.stream) { 4240 mediaRecorder.stream.getTracks().forEach(track => track.stop()); 4241 } 4242 mediaRecorder = null; 4243 } 4244}); 4245 4246linkButton.addEventListener('click', async (event) => { 4247 const i = audioButton.querySelector("i"); 4248 const link = prompt("Please enter a link"); 4249 if (!link) { 4250 return; 4251 } 4252 if (link.startsWith("http") === false) { 4253 inputCount.innerText = framework.translate("Invalid link"); 4254 return; 4255 } 4256 image_storage[link] = link; 4257 renderMediaSelect(); 4258}); 4259 4260fileInput.addEventListener('click', async (event) => { 4261 fileInput.value = ''; 4262}); 4263 4264cameraInput?.addEventListener("click", (e) => { 4265 if (window?.pywebview) { 4266 e.preventDefault(); 4267 pywebview.api.take_picture(); 4268 } 4269}); 4270 4271imageSelect?.addEventListener("click", (e) => { 4272 if (window?.pywebview) { 4273 e.preventDefault(); 4274 pywebview.api.choose_image(); 4275 } 4276}); 4277 4278async function upload_cookies() { 4279 const file = fileInput.files[0]; 4280 const formData = new FormData(); 4281 formData.append('file', file); 4282 response = await fetch(framework.backendUrl + "/backend-api/v2/upload_cookies", { 4283 method: 'POST', 4284 body: formData, 4285 }); 4286 if (response.status == 200) { 4287 inputCount.innerText = framework.translate("{0} File(s) uploaded successfully").replace('{0}', file.name); 4288 } 4289 fileInput.value = ""; 4290} 4291 4292function formatFileSize(bytes) { 4293 const units = ['B', 'KB', 'MB', 'GB']; 4294 let unitIndex = 0; 4295 while (bytes >= 1024 && unitIndex < units.length - 1) { 4296 bytes /= 1024; 4297 unitIndex++; 4298 } 4299 return `${bytes.toFixed(2)} ${units[unitIndex]}`; 4300} 4301 4302function connectToSSE(url, do_refine, bucket_id) { 4303 const eventSource = new EventSource(url); 4304 eventSource.onmessage = async (event) => { 4305 const data = JSON.parse(event.data); 4306 if (data.error) { 4307 inputCount.innerText = `${framework.translate('Error:')} ${data.error.message}`; 4308 paperclip.classList.remove("blink"); 4309 fileInput.value = ""; 4310 } else if (data.action == "load") { 4311 inputCount.innerText = `${framework.translate('Read data:')} ${formatFileSize(data.size)}`; 4312 } else if (data.action == "refine") { 4313 inputCount.innerText = `${framework.translate('Refine data:')} ${formatFileSize(data.size)}`; 4314 } else if (data.action == "download") { 4315 inputCount.innerText = `${framework.translate('Download:')} ${data.count} files`; 4316 } else if (data.action == "done") { 4317 if (do_refine) { 4318 connectToSSE(`${framework.backendUrl}/backend-api/v2/files/${encodeURIComponent(bucket_id)}?refine_chunks_with_spacy=true`, false, bucket_id); 4319 return; 4320 } 4321 fileInput.value = ""; 4322 paperclip.classList.remove("blink"); 4323 if (!data.size) { 4324 inputCount.innerText = framework.translate("No content found"); 4325 return 4326 } 4327 appStorage.setItem(`bucket:${bucket_id}`, data.size); 4328 inputCount.innerText = framework.translate("Files are loaded successfully"); 4329 4330 const url = `${framework.backendUrl}/backend-api/v2/files/${encodeURIComponent(bucket_id)}`; 4331 const media = [{bucket_id: bucket_id, url: url}]; 4332 await handle_ask(false, media); 4333 } 4334 }; 4335 eventSource.onerror = (event) => { 4336 eventSource.close(); 4337 paperclip.classList.remove("blink"); 4338 } 4339} 4340 4341async function upload_files(fileInput) { 4342 try { 4343 const bucket_id = generateUUID(); 4344 paperclip.classList.add("blink"); 4345 4346 const formData = new FormData();
4347 Array.from(fileInput.files).forEach(file => { 4348 formData.append('files', file); 4349 }); 4350 const response = await fetch(framework.backendUrl + "/backend-api/v2/files/" + bucket_id, { 4351 method: 'POST', 4352 body: formData 4353 }); 4354 const result = await response.json() 4355 const count = result.files.length + result.media.length; 4356 inputCount.innerText = framework.translate('{0} File(s) uploaded successfully').replace('{0}', count); 4357 if (result.files.length > 0) { 4358 let do_refine = document.getElementById("refine")?.checked; 4359 connectToSSE(`${framework.backendUrl}/backend-api/v2/files/${bucket_id}/stream`, do_refine, bucket_id); 4360 } else { 4361 paperclip.classList.remove("blink"); 4362 fileInput.value = ""; 4363 } 4364 if (result.media) { 4365 const media = []; 4366 result.media.forEach((part)=> { 4367 part = part.name ? part : {name: part}; 4368 const url = `${framework.backendUrl}/files/${bucket_id}/media/${part.name}`; 4369 media.push({bucket_id: bucket_id, url: url, ...part}); 4370 }); 4371 await handle_ask(false, media); 4372 } 4373 } catch(e) { 4374 add_error(e, true); 4375 } 4376} 4377 4378fileInput.addEventListener('change', async (event) => { 4379 if (fileInput.files.length) { 4380 type = fileInput.files[0].name.split('.').pop() 4381 if (type == "har") { 4382 return await upload_cookies(); 4383 } else if (type != "json") { 4384 await upload_files(fileInput); 4385 } 4386 fileInput.dataset.type = type 4387 if (type == "json") { 4388 const reader = new FileReader(); 4389 reader.addEventListener('load', async (event) => { 4390 const data = JSON.parse(event.target.result); 4391 if (data.options && "g4f" in data.options) { 4392 let count = 0; 4393 Object.keys(data).forEach(async key => { 4394 if (key == "options") { 4395 Object.keys(data[key]).forEach(keyOption => { 4396 appStorage.setItem(keyOption, data[key][keyOption]); 4397 count += 1; 4398 }); 4399 } else if (!appStorage.getItem(key)) { 4400 if (key.startsWith("conversation:")) { 4401 await save_conversation(data[key]); 4402 count += 1; 4403 } else { 4404 appStorage.setItem(key, data[key]); 4405 } 4406 } 4407 }); 4408 await load_conversations(); 4409 await load_settings_storage(); 4410 fileInput.value = ""; 4411 inputCount.innerText = framework.translate('{0} Conversations/Settings were imported successfully').replace('{0}', count); 4412 } else { 4413 is_cookie_file = data.api_key; 4414 if (Array.isArray(data)) { 4415 data.forEach((item) => { 4416 if (item.domain && item.name && item.value) { 4417 is_cookie_file = true; 4418 } 4419 }); 4420 } 4421 if (is_cookie_file) { 4422 await upload_cookies(); 4423 } else { 4424 await upload_files(fileInput); 4425 } 4426 } 4427 }); 4428 reader.readAsText(fileInput.files[0]); 4429 } 4430 } 4431}); 4432 4433if (!window.matchMedia("(pointer:coarse)").matches) { 4434 document.getElementById("image").setAttribute("multiple", "multiple"); 4435} 4436 4437chatPrompt?.addEventListener("input", async () => { 4438 await save_system_message(); 4439}); 4440 4441function get_selected_model() { 4442 let model = null; 4443 if (modelSearch && modelSearch.value) { 4444 return modelSearch.value; 4445 } else if (modelSelect.selectedIndex >= 0) { 4446 model = modelSelect.options[modelSelect.selectedIndex]; 4447 } 4448 return model?.value ? model.value : null; 4449} 4450 4451async function api(ressource, args=null, files=null, message_id=null, finish_message=null) { 4452 if (window?.pywebview) { 4453 if (args !== null) { 4454 if (ressource == "conversation") { 4455 return pywebview.api[`get_${ressource}`](args, message_id); 4456 } 4457 if (ressource == "models") { 4458 ressource = "provider_models"; 4459 } 4460 return pywebview.api[`get_${ressource}`](args); 4461 } 4462 return pywebview.api[`get_${ressource}`](); 4463 } 4464 let headers = {}; 4465 let user = appStorage.getItem("user"); 4466 if (user) { 4467 headers['x-user'] = user; 4468 } 4469 let url = `${framework.backendUrl}/backend-api/v2/${ressource}`; 4470 let response; 4471 if (ressource == "models" && args) { 4472 if (providerModelSignal) { 4473 providerModelSignal.abort(); 4474 } 4475 providerModelSignal = new AbortController(); 4476 4477 api_key = get_api_key_by_provider(args); 4478 if (api_key) { 4479 headers['x-api-key'] = api_key; 4480 } 4481 api_base = args == "Custom" ? document.getElementById(`${args}-api_base`).value : null; 4482 if (api_base) { 4483 headers['x-api-base'] = api_base; 4484 } 4485 const ignored = Array.from(settings.querySelectorAll("input.provider:not(:checked)")).map((el)=>el.value); 4486 if (ignored) {
4487 headers['x-ignored'] = ignored.join(" "); 4488 } 4489 url = `${framework.backendUrl}/backend-api/v2/${ressource}/${args}`; 4490 headers['content-type'] = 'application/json'; 4491 response = await fetch(url, { 4492 method: 'GET', 4493 headers: headers, 4494 signal: providerModelSignal.signal, 4495 }); 4496 } else if (ressource == "conversation") { 4497 // Route PA providers to their dedicated backend endpoint 4498 if (args && args.provider && String(args.provider).startsWith("pa:")) { 4499 args = { ...args, provider: args.provider.slice(3) }; 4500 url = `${framework.backendUrl}/pa/backend-api/v2/conversation`; 4501 } 4502 let body = JSON.stringify(args); 4503 headers = { 4504 accept: 'text/event-stream', 4505 ...await framework.getHeaders() 4506 }; 4507 if (files.length > 0) { 4508 const formData = new FormData(); 4509 for (const file of files) { 4510 if (file instanceof File) { 4511 formData.append('files', file) 4512 } else { 4513 formData.append('media_url', file.url ? file.url : file) 4514 } 4515 } 4516 formData.append('json', body); 4517 body = formData; 4518 } else { 4519 headers['content-type'] = 'application/json'; 4520 } 4521 response = await fetch(url, { 4522 method: 'POST', 4523 signal: controller_storage[message_id].signal, 4524 headers: headers, 4525 body: body, 4526 }); 4527 // On Ratelimit 4528 if (response.status == 429) { 4529 const body = await response.text(); 4530 const title = body.match(/<title>([^<]+?)<\/title>/)[1]; 4531 const message = body.match(/<p>([^<]+?)<\/p>/)[1]; 4532 error_storage[message_id] = `**${title}**\n${message}`; 4533 await finish_message(); 4534 return; 4535 } else { 4536 try { 4537 await read_response(response, message_id, args.provider || null, finish_message); 4538 } catch (e) { 4539 console.error(e); 4540 if (continue_storage[message_id]) { 4541 delete continue_storage[message_id]; 4542 await api("conversation", args, files, message_id, finish_message) 4543 } 4544 } 4545 await finish_message(); 4546 return; 4547 } 4548 } else if (args) { 4549 if (ressource == "log" || ressource == "usage") { 4550 if (ressource == "log" && !document.getElementById("reportError").checked) { 4551 return; 4552 } 4553 } 4554 headers['content-type'] = 'application/json'; 4555 response = await fetch(url, { 4556 method: 'POST', 4557 headers: headers, 4558 body: JSON.stringify(args), 4559 }); 4560 } 4561 if (!response) { 4562 response = await fetch(url, {headers: headers}); 4563 } 4564 if (response.status != 200) { 4565 console.error(response); 4566 } 4567 return await response.json(); 4568} 4569 4570async function read_response(response, message_id, provider, finish_message) { 4571 const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); 4572 let buffer = ""; 4573 let currentEvent = null; 4574 let currentData = null; 4575 4576 while (true) { 4577 const { value, done } = await reader.read(); 4578 if (done) { 4579 break; 4580 } 4581 buffer += value; 4582 const lines = buffer.split("\n"); 4583 // Keep the last incomplete line in the buffer 4584 buffer = lines.pop(); 4585 4586 for (const line of lines) { 4587 if (line.startsWith("event: ")) { 4588 currentEvent = line.substring(7).trim(); 4589 } else if (line.startsWith("data: ")) { 4590 currentData = line.substring(6); 4591 } else if (line === "" && currentData !== null) { 4592 // Empty line marks end of SSE event 4593 try { 4594 const data = JSON.parse(currentData); 4595 await add_message_chunk(data, message_id, provider, finish_message); 4596 } catch (e) { 4597 console.error("Failed to parse SSE data:", e, currentData); 4598 } 4599 currentEvent = null; 4600 currentData = null; 4601 } else if (line && !line.startsWith("event:") && !line.startsWith("data:")) { 4602 // Fallback for legacy JSON-only format (no SSE prefix) 4603 try { 4604 const data = JSON.parse(line); 4605 await add_message_chunk(data, message_id, provider, finish_message); 4606 } catch { 4607 // Ignore parse errors for incomplete lines 4608 } 4609 } 4610 } 4611 } 4612} 4613 4614function get_api_key_by_provider(provider, single=false) { 4615 let api_key = null; 4616 if (provider.startsWith("pa:")) { 4617 return appStorage.getItem(`pa:${provider.slice(3)}-api_key`) || appStorage.getItem("session_token"); 4618 } 4619 if (provider) { 4620 if (provider === "custom:srv_ml2kr1wn9b1fb453079a") { 4621 return appStorage.getItem("DeepInfra-api_key") || appStorage.getItem("session_token"); 4622 } 4623 if (provider === "custom:srv_mkomfko63371049b6da6") { 4624 return appStorage.getItem("ApiAirforce-api_key") || appStorage.getItem("session_token"); 4625 } 4626 if (["Azure"].includes(provider)) { 4627 return appStorage.getItem("session_token"); 4628 } 4629 if (["custom"].includes(provider)) { 4630 return appStorage.getItem("Custom-api_key"); 4631 } 4632 if (provider.startsWith("custom:")) { 4633 return appStorage.getItem("session_token"); 4634 } 4635 if (!single && provider === "AnyProvider") { 4636 return { 4637 "PollinationsAI": get_api_key_by_provider("PollinationsAI"), 4638 "HuggingFace": appStorage.getItem("HuggingFace-api_key"), 4639 "Together": appStorage.getItem("Together-api_key"), 4640 "GeminiPro": get_api_key_by_provider("GeminiPro"), 4641 "OpenRouter": appStorage.getItem("OpenRouter-api_key"),
4642 "OpenRouterFree": get_api_key_by_provider("OpenRouterFree"), 4643 "Groq": get_api_key_by_provider("Groq"), 4644 "DeepInfra": appStorage.getItem("DeepInfra-api_key"), 4645 "Replicate": appStorage.getItem("Replicate-api_key"), 4646 "PuterJS": appStorage.getItem("puter.auth.token"), 4647 "Azure": get_api_key_by_provider("Azure"), 4648 "Nvidia": get_api_key_by_provider("Nvidia"), 4649 "Ollama": get_api_key_by_provider("Ollama"), 4650 } 4651 } 4652 api_key = document.querySelector(`.${provider}-api_key`)?.id || null; 4653 if (api_key == null) { 4654 api_key = document.getElementById(`${provider}-api_key`)?.id || null; 4655 } 4656 if (api_key) { 4657 api_key = appStorage.getItem(api_key); 4658 } 4659 if (!api_key && provider.startsWith("Puter")) { 4660 return appStorage.getItem("puter.auth.token"); 4661 } 4662 if (!api_key && ["GeminiPro", "Ollama", "Nvidia", "OpenRouterFree", "PollinationsAI", "Groq"].includes(provider)) { 4663 return appStorage.getItem("session_token"); 4664 } 4665 } 4666 return api_key; 4667} 4668 4669function setFavoriteModels(provider, defaultModel) { 4670 const optgroup = document.createElement('optgroup'); 4671 optgroup.label = framework.translate("Favorite Models:"); 4672 const favorites = JSON.parse(appStorage.getItem("favorites") || "{}"); 4673 let selected = favorites[provider]; 4674 if (!selected) { 4675 selected = {}; 4676 if (defaultModel) { 4677 selected[defaultModel] = 0; 4678 } 4679 } 4680 Object.keys(selected).forEach((key) => { 4681 const option = document.createElement('option'); 4682 option.value = key; 4683 option.text = key; 4684 const value_option = modelSelect.querySelector(`option[value="${key}"]`) 4685 if (value_option) { 4686 const option = value_option.cloneNode(true); 4687 if (typeof option.dataset.remaining === 'undefined' || option.dataset.remaining > 0) { 4688 option.selected = true; 4689 } 4690 optgroup.appendChild(option); 4691 if (optgroup.childElementCount > 5) { 4692 delete selected[optgroup.firstChild.value]; 4693 optgroup.removeChild(optgroup.firstChild); 4694 } 4695 } 4696 }); 4697 favorites[provider] = selected; 4698 appStorage.setItem("favorites", JSON.stringify(favorites)); 4699 modelSelect.appendChild(optgroup); 4700} 4701 4702function set_favorite_providers() { 4703 const optgroup = document.createElement('optgroup'); 4704 optgroup.label = framework.translate("Favorite Providers:"); 4705 let favorites = JSON.parse(appStorage.getItem("favorite_providers") || "null"); 4706 if (!favorites) { 4707 favorites = {}; 4708 favorites[providerSelect.value] = 0; 4709 } 4710 Object.keys(favorites).forEach((key) => { 4711 const value_option = providerSelect.querySelector(`option[value="${key}"]`) 4712 if (value_option) { 4713 const option = value_option.cloneNode(true); 4714 optgroup.appendChild(option); 4715 } 4716 }); 4717 providerSelect.appendChild(optgroup); 4718} 4719 4720function setQuotaInfo(models, quota) { 4721 if (!quota) { 4722 return; 4723 } 4724 let defaultModel = null; 4725 models.forEach((model) => { 4726 let percent; 4727 if (quota.buckets) { 4728 if (!["gemini-3-pro-preview"].includes(defaultModel)) { 4729 defaultModel = null; // Use last model with enough quota as default instead of the first one 4730 } 4731 percent = (quota.buckets.filter((bucket) => bucket.modelId == model.id).pop()?.remainingFraction || 0) * 100; 4732 model.label = `${model.label} (${framework.translate("Remaining:")} ${percent}%)`; 4733 } else if (quota.models) { 4734 percent = (quota.models[model.id]?.quotaInfo?.remainingFraction || 0) * 100; 4735 model.label = `${model.label} (${framework.translate("Remaining:")} ${percent}%)`; 4736 } else if (quota.quota_snapshots) { 4737 function isPremium(model) { 4738 return model.includes("claude") || model.includes("gemini") || (model != "gpt-5-mini" && model.includes("gpt-5")) || model.includes("grok"); 4739 } 4740 if (isPremium(model.id)) { 4741 percent = Math.max(0, quota.quota_snapshots?.premium_interactions?.percent_remaining || 0); 4742 model.label = `${model.label} (${framework.translate("Remaining:")} ${percent}%)`; 4743 } else { 4744 percent = Math.max(0, quota.quota_snapshots?.chat?.percent_remaining || 0); 4745 model.label = `${model.label} (${framework.translate("Remaining:")} ${percent}%)`; 4746 } 4747 } else { 4748 return; 4749 } 4750 if (percent !== undefined && percent < 10) { 4751 model.label += ` â ï¸`; 4752 } else { 4753 model.label += ` â `; 4754 } 4755 model.remaining_percent = percent; 4756 if (!defaultModel && percent >= 10) { 4757 defaultModel = model.id;
4758 models.forEach((model) => delete model.default); 4759 model.default = true; 4760 } 4761 }); 4762 if (quota && quota.hasOwnProperty("balance")) { 4763 let creditsInfo = `${framework.translate("Balance:")} ${quota.balance.toFixed(2).replace(".00", "")} Pollen`; 4764 if (quota.balance > 0) { 4765 creditsInfo += " â "; 4766 } else { 4767 creditsInfo += " â ï¸"; 4768 } 4769 if (models.length > 10) { 4770 models.unshift({id: "credits_info", label: creditsInfo, disabled: true}); 4771 } 4772 } 4773 if (quota.credits) { 4774 const percent = (quota.credits.remaining / quota.credits.total) * 100; 4775 let creditsInfo = `${framework.translate("Credits:")} ${quota.credits.remaining}, ${framework.translate("Remaining:")} ${percent.toFixed(2)}%`; 4776 if (percent >= 10) { 4777 creditsInfo += " â "; 4778 } else { 4779 creditsInfo += " â ï¸"; 4780 } 4781 models.unshift({id: "credits_info", label: creditsInfo, disabled: true}); 4782 if (models.length > 10) { 4783 models.push({id: "credits_info", label: creditsInfo, disabled: true}); 4784 } 4785 } 4786 if (quota.session_usage) { 4787 models.push({id: "session_usage", label: `${framework.translate("Session usage:")} ${quota.session_usage.used_percent}%` + (quota.session_usage.used_percent > 90 ? " â ï¸" : " â "), disabled: true}); 4788 } 4789 if (quota.weekly_usage) { 4790 models.push({id: "weekly_usage", label: `${framework.translate("Weekly usage:")} ${quota.weekly_usage.used_percent}%` + (quota.weekly_usage.used_percent > 90 ? " â ï¸" : " â "), disabled: true}); 4791 } 4792 if (quota.allowanceInfo?.remaining) { 4793 const percent = (quota.allowanceInfo.remaining / quota.allowanceInfo.monthUsageAllowance) * 100; 4794 const total = (quota?.allowanceInfo?.remaining || 0) / 1e8; 4795 const creditsInfo = `${framework.translate("Credits:")} ${total.toFixed(2)}$, ${framework.translate("Remaining:")} ${percent.toFixed(2)}%` + (percent > 10 ? " â " : " â ï¸"); 4796 models.unshift({id: "credits_info", label: creditsInfo, disabled: true}); 4797 } 4798 if (!defaultModel && client && client.defaultModel) { 4799 defaultModel = client.defaultModel; 4800 models.forEach((model) => { 4801 if ((model.model || model.id) == defaultModel) { 4802 model.default = true; 4803 } else { 4804 delete model.default; 4805 } 4806 }); 4807 } 4808} 4809function setProviderModels(models, provider, quota=null) { 4810 modelSelect.innerHTML = ''; 4811 const option = providerSelect.options[providerSelect.selectedIndex]; 4812 if (option) option.text = option.text.replaceAll(" ð¢", "") + (quota ? " ð¢" : ""); 4813 function addOptions(group, models, search) { 4814 if (quota) { 4815 setQuotaInfo(models, quota); 4816 } 4817 models.forEach((model, i) => { 4818 if (!model.models) { 4819 let option = document.createElement('option'); 4820 option.dataset.label = model.label || model.id || model; 4821 if (window.convertModel && model.id) convertModel(model); 4822 option.value = model.id || model; 4823 option.text = model.label || model.id || model; 4824 if (model.audio) { 4825 option.dataset.audio = "true"; 4826 } 4827 if (model.remaining_percent !== undefined) { 4828 option.dataset.remaining = model.remaining_percent; 4829 } 4830 group.appendChild(option); 4831 if (model.default) { 4832 option.selected = true; 4833 } 4834 if (model.disabled) { 4835 option.disabled = true; 4836 } 4837 } else { 4838 let optgroup = document.createElement('optgroup'); 4839 optgroup.label = model.group; 4840 addOptions(optgroup, model.models, search); 4841 if (optgroup.childElementCount == 0) { 4842 return; 4843 } 4844 modelSelect.appendChild(optgroup); 4845 } 4846 }); 4847 } 4848 if (Array.isArray(models)) { 4849 addOptions(modelSelect, models, search); 4850 if (models.length > 2) { 4851 const defaultModel = models.map(m => m.models?.find(m => m.default) || m).find(m => m.default)?.id; 4852 setFavoriteModels(provider, defaultModel); 4853 } 4854 } 4855} 4856async function get_quota(provider) { 4857 if (!provider || provider == "AnyProvider") { 4858 return; 4859 } 4860 const url = `${framework.backendUrl}/backend-api/v2/quota/${provider}`; 4861 const api_key = get_api_key_by_provider(provider, true); 4862 const response = await fetch(url, { method: 'GET', headers: api_key ? {"x-api-key": api_key} : {} }); 4863 let data; 4864 try { 4865 data = await response.json(); 4866 } catch (e) { 4867 add_error(e, true); 4868 return; 4869 } 4870 if (response.status == 401 || (data.error && data.error.code == 401)) { 4871 let input = document.querySelector(`.${provider}-api_key`); 4872 if (!input) { 4873 input = document.getElementById(`${provider}-api_key`); 4874 } 4875 console.warn("Unauthorized access for provider:", provider); 4876 if (input) { 4877 input.value = ""; 4878 input.dataset.value = ""; 4879 appStorage.removeItem(input.id); 4880 input.placeholder = framework.translate("Invalid API key"); 4881 } 4882 } 4883 return response.ok ? data : undefined; 4884} 4885async function refreshModels(provider) {
4886 // PA providers expose models via the pa providers list, not the models API 4887 if (provider && String(provider).startsWith("pa:")) { 4888 const paId = provider.slice(3); 4889 const paEntry = window._paProviders && window._paProviders.find(p => p.id === paId); 4890 console.log("PA provider entry for provider:", provider, paEntry); 4891 if (paEntry && Array.isArray(paEntry.models) && paEntry.models.length > 0) { 4892 const models = paEntry.models.map(m => ({ name: m, model: m })); 4893 console.log("Setting PA provider models for provider:", provider, models); 4894 setProviderModels(models, provider); 4895 } 4896 return; 4897 } 4898 let models = appStorage.getItem(`${provider}:models`); 4899 if (models) { 4900 models = JSON.parse(models); 4901 setProviderModels(models, provider); 4902 } 4903 const [new_models, quota] = await Promise.all([api('models', provider), get_quota(provider)]); 4904 if (new_models) { 4905 setProviderModels(new_models, provider, quota); 4906 appStorage.setItem(`${provider}:models`, JSON.stringify(new_models)); 4907 } 4908} 4909async function loadProviderModels(provider=null) { 4910 const isLoading = !!provider; 4911 if (!provider) { 4912 provider = providerSelect?.value; 4913 } 4914 if (!provider) { 4915 modelSelect.classList.add("hidden"); 4916 return; 4917 } 4918 modelSelect.innerHTML = ''; 4919 modelSelect.name = `model[${provider}]`; 4920 modelSelect.classList.remove("hidden"); 4921 if (!isLoading && ["PuterJS", "puter"].includes(provider) && !appStorage.getItem("puter.auth.token") && window.Puter) { 4922 try { 4923 await (new window.Puter()).signIn().then((res) => { 4924 console.log('PuterJS signed in:', res); 4925 }); 4926 } catch (error) { 4927 add_error(error, true); 4928 } 4929 } 4930 if (await initClient()) { 4931 return; 4932 } 4933 console.log("Loading models for provider:", provider); 4934 await refreshModels(provider); 4935}; 4936if (providerSelect) { 4937 providerSelect.addEventListener("change", async () => { 4938 await loadProviderModels() 4939 const favorites = appStorage.getItem("favorite_providers") ? JSON.parse(appStorage.getItem("favorite_providers")) : {}; 4940 const selected = providerSelect.options[providerSelect.selectedIndex]; 4941 console.log("Selected provider:", providerSelect.value, selected); 4942 if (!favorites[providerSelect.value]) { 4943 const option = selected.cloneNode(true); 4944 const optgroup = providerSelect.querySelector('optgroup:last-child'); 4945 if (optgroup) { 4946 optgroup.appendChild(option); 4947 if (optgroup.childElementCount > 5) { 4948 delete favorites[optgroup.firstChild.value]; 4949 optgroup.removeChild(optgroup.firstChild); 4950 } 4951 } 4952 } 4953 const selected_values = favorites[providerSelect.value] ? favorites[providerSelect.value] + 1 : 1; 4954 delete favorites[providerSelect.value]; 4955 favorites[providerSelect.value] = selected_values; 4956 appStorage.setItem("favorite_providers", JSON.stringify(favorites)); 4957 }); 4958} 4959modelSelect.addEventListener("change", () => { 4960 const favorites = appStorage.getItem("favorites") ? JSON.parse(appStorage.getItem("favorites")) : {}; 4961 const selected = favorites[providerSelect?.value] || {}; 4962 const selectedOption = modelSelect.options[modelSelect.selectedIndex]; 4963 console.log("Selected model:", modelSelect.value, selectedOption); 4964 if (!selected[modelSelect.value]) { 4965 const option = selectedOption.cloneNode(true); 4966 const optgroup = modelSelect.querySelector('optgroup:last-child'); 4967 if (optgroup) { 4968 optgroup.appendChild(option); 4969 if (optgroup.childElementCount > 5) { 4970 delete selected[optgroup.firstChild.value]; 4971 optgroup.removeChild(optgroup.firstChild); 4972 } 4973 } 4974 } 4975 const selected_values = selected[modelSelect.value] ? selected[modelSelect.value] + 1 : 1; 4976 delete selected[modelSelect.value]; 4977 selected[modelSelect.value] = selected_values; 4978 favorites[providerSelect?.value] = selected; 4979 appStorage.setItem("favorites", JSON.stringify(favorites)); 4980}); 4981document.getElementById("model_edit")?.addEventListener("click", () => { 4982 if (!modelSelector.classList.contains("hidden")) { 4983 providerSelect.classList.remove("hidden"); 4984 modelSelect.classList.remove("hidden"); 4985 modelSelector.classList.add("hidden"); 4986 modelSearch.value = ""; 4987 return; 4988 } 4989 providerSelect.classList.add("hidden"); 4990 modelSelect.classList.add("hidden"); 4991 modelSelector.classList.remove("hidden"); 4992 modelSearch.focus() 4993}); 4994modelSearch?.addEventListener('input', function() { 4995 const searchTerm = this.value.toLowerCase(); 4996 modelSuggestions.innerHTML = ''; 4997 4998 if (!searchTerm) return; 4999 5000 let matches = []; 5001 5002 // Search across all models 5003 for (const [provider, modelList] of Object.entries(searchModels)) { 5004 if (!Array.isArray(modelList)) continue; 5005 modelList.forEach(model => { 5006 if (model.models) {
5007 model.models.forEach(subModel => { 5008 if (subModel.model.toLowerCase().includes(searchTerm)) { 5009 matches.push({ provider, model: subModel }); 5010 } 5011 }); 5012 } else if ((model.id || model).toLowerCase().includes(searchTerm)) { 5013 matches.push({ provider, model }); 5014 } 5015 }); 5016 } 5017 5018 // Display matches 5019 matches.forEach(match => { 5020 const div = document.createElement('div'); 5021 div.className = 'suggestion-item'; 5022 div.innerHTML = ` 5023 <strong>${match.model.id || match.model}</strong> 5024 <span class="provider-tag">${match.provider}</span> 5025 `; 5026 div.addEventListener('click', async () => { 5027 modelSearch.value = ""; 5028 providerSelect.value = match.provider; 5029 await loadProviderModels(); 5030 modelSelect.value = match.model.id || match.model; 5031 modelSelector.classList.add("hidden"); 5032 providerSelect.classList.remove("hidden"); 5033 modelSelect.classList.remove("hidden"); 5034 modelSuggestions.innerHTML = ''; 5035 console.log(`Selected model: ${match.model}`); 5036 }); 5037 modelSuggestions.appendChild(div); 5038 }); 5039}); 5040async function loadModels(providers) { 5041 searchModels = await api('models'); 5042} 5043 5044// Close dropdown when clicking outside 5045if (modelSuggestions) 5046document.addEventListener('click', (e) => { 5047 if (e.target !== modelSearch) { 5048 modelSuggestions.innerHTML = ''; 5049 } 5050}); 5051 5052document.getElementById("pin").addEventListener("click", async () => { 5053 add_pinned(providerSelect?.value, get_selected_model()); 5054}); 5055 5056(async () => { 5057 JSON.parse(appStorage.getItem("pinned") || "[]").forEach((el) => { 5058 add_pinned(el.provider, el.model, false); 5059 }); 5060})(); 5061 5062function add_pinned(selected_provider, selected_model, save=true) { 5063 if (save) { 5064 const all_pinned_saved = JSON.parse(appStorage.getItem("pinned") || "[]"); 5065 appStorage.setItem("pinned", JSON.stringify([{ 5066 provider: selected_provider?.value || selected_provider, 5067 model: selected_model?.value || selected_model, 5068 }, ...all_pinned_saved])); 5069 } 5070 const pinned = document.createElement("button"); 5071 pinned.classList.add("pinned"); 5072 if (selected_provider) pinned.dataset.provider = selected_provider.value || selected_provider; 5073 if (selected_model) pinned.dataset.model = selected_model.value || selected_model; 5074 pinned.innerHTML = ` 5075 <span> 5076 ${selected_provider && selected_provider.dataset ? selected_provider.dataset.label || selected_provider.text : selected_provider} 5077 ${selected_provider && selected_model ? "/" : ""} 5078 ${selected_model && selected_model.dataset ? selected_model.dataset.label || selected_model.text : selected_model} 5079 </span> 5080 <i class="fa-regular fa-circle-xmark"></i>`; 5081 pinned.addEventListener("click", () => { 5082 pin_container.removeChild(pinned); 5083 let all_pinned = JSON.parse(appStorage.getItem("pinned") || "[]"); 5084 all_pinned = all_pinned.filter((el) => { 5085 return el.provider != pinned.dataset.provider || el.model != pinned.dataset.model; 5086 }); 5087 appStorage.setItem("pinned", JSON.stringify(all_pinned)); 5088 }); 5089 all_pinned = pin_container.querySelectorAll(".pinned"); 5090 while (all_pinned.length > 4) { 5091 pin_container.removeChild(all_pinned[0]) 5092 all_pinned = pin_container.querySelectorAll(".pinned"); 5093 } 5094 pin_container.appendChild(pinned); 5095} 5096 5097searchButton.addEventListener("click", async () => { 5098 setTimeout(() => userInput.focus(), 100); 5099 searchButton.classList.toggle("active"); 5100 (searchButton.querySelector("*")).innerText = (searchButton.classList.contains("active") ? framework.translate("Search On") : framework.translate("Search Off")); 5101}); 5102 5103async function save_storage(settings=false) { 5104 let filename = `${settings ? 'settings' : 'chat'} ${new Date().toLocaleString()}.json`.replaceAll(":", "-"); 5105 let data = {"options": {"g4f": ""}}; 5106 if (!settings) { 5107 const conversations = await list_conversations(); 5108 conversations.forEach((conversation) => { 5109 data[`conversation:${conversation.id}`] = conversation; 5110 }); 5111 } 5112 for (let i = 0; i < appStorage.length; i++) { 5113 let key = appStorage.key(i); 5114 let item = appStorage.getItem(key); 5115 if (key.startsWith("conversation:")) { 5116 if (!settings) { 5117 data[key] = JSON.parse(item); 5118 } 5119 } else if (key.startsWith("bucket:")) { 5120 if (!settings) { 5121 data[key] = item; 5122 } 5123 } else if (settings && !key.endsWith("-form") && !key.endsWith("user")) { 5124 data["options"][key] = item; 5125 } 5126 } 5127 data = JSON.stringify(data, null, 4); 5128 const blob = new Blob([data], {type: 'application/json'}); 5129 const elem = window.document.createElement('a'); 5130 elem.href = window.URL.createObjectURL(blob); 5131 elem.download = filename; 5132 document.body.appendChild(elem); 5133 elem.click(); 5134 document.body.removeChild(elem); 5135} 5136 5137async function get_recognition_language() { 5138 const lang = document.getElementById("recognition-language")?.value; 5139 if (lang) { 5140 return lang; 5141 } 5142 if (navigator.language == "en") { 5143 return "en-US"; 5144 } 5145 let locale = navigator.language; 5146 if (!locale.includes("-")) { 5147 locale = appStorage.getItem(navigator.language); 5148 if (locale) { 5149 return locale; 5150 } 5151 try {
5152 const prompt = 'Response the full locale in JSON. Example: {"locale": "en-US"} Language: ' + navigator.language 5153 response = await framework.query(prompt, true); 5154 locale = (await response.json()).locale || navigator.language; 5155 if (locale.includes("-")) { 5156 appStorage.setItem(navigator.language, locale); 5157 } 5158 } catch (e) { 5159 add_error(e, true); 5160 } 5161 } 5162 return locale; 5163} 5164 5165const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; 5166if (SpeechRecognition) { 5167 const mircoIcon = microLabel.querySelector("i"); 5168 mircoIcon.classList.add("fa-microphone"); 5169 mircoIcon.classList.remove("fa-microphone-slash"); 5170 5171 const recognition = new SpeechRecognition(); 5172 recognition.continuous = true; 5173 recognition.interimResults = true; 5174 recognition.maxAlternatives = 1; 5175 5176 let startValue; 5177 let buffer; 5178 let lastDebounceTranscript; 5179 recognition.onstart = function() { 5180 startValue = userInput.value; 5181 lastDebounceTranscript = ""; 5182 userInput.readOnly = true; 5183 buffer = ""; 5184 }; 5185 recognition.onend = function() { 5186 if (buffer) { 5187 userInput.value += `${startValue ? startValue + "\n" : ""}${buffer}`; 5188 buffer = ""; 5189 count_input(); 5190 } 5191 if (microLabel.classList.contains("recognition")) { 5192 recognition.start(); 5193 } else { 5194 userInput.readOnly = false; 5195 userInput.focus(); 5196 } 5197 }; 5198 recognition.onresult = function(event) { 5199 if (!event.results) { 5200 return; 5201 } 5202 let result = event.results[event.resultIndex]; 5203 let isFinal = result.isFinal && (result[0].confidence > 0); 5204 let transcript = result[0].transcript; 5205 if (isFinal) { 5206 if(transcript == lastDebounceTranscript) { 5207 return; 5208 } 5209 lastDebounceTranscript = transcript; 5210 } 5211 if (transcript) { 5212 inputCount.innerText = transcript; 5213 if (isFinal) { 5214 buffer = `${buffer ? buffer + "\n" : ""}${transcript.trim()}`; 5215 } 5216 } 5217 }; 5218 5219 stopRecognition = ()=>{ 5220 if (microLabel.classList.contains("recognition")) { 5221 microLabel.classList.remove("recognition"); 5222 recognition.stop(); 5223 count_input(); 5224 return true; 5225 } 5226 return false; 5227 } 5228 5229 microLabel.addEventListener("click", async (e) => { 5230 if (!stopRecognition()) { 5231 microLabel.classList.add("recognition"); 5232 microLabel.querySelector("*").innerText = framework.translate("Recognition On"); 5233 recognition.lang = await get_recognition_language(); 5234 recognition.start(); 5235 } else { 5236 microLabel.querySelector("*").innerText = framework.translate("Recognition Off"); 5237 } 5238 }); 5239} 5240 5241function showLog() { 5242 logStorage.classList.remove("hidden"); 5243 settings.classList.add("hidden"); 5244 logContent.scrollTop = logContent.scrollHeight; 5245 chat.classList.add("hidden"); 5246} 5247 5248function hideLog() { 5249 logStorage.classList.add("hidden"); 5250 chat.classList.remove("hidden"); 5251} 5252 5253function logRequestResponse(event, messageId, count=0) { 5254 const eventType = event.response ? "response" : "request"; 5255 let details = document.createElement("details"); 5256 let summary = document.createElement("summary"); 5257 summary.textContent = `${eventType[0].toUpperCase() + eventType.slice(1)} ${messageId} #${count}`; 5258 details.appendChild(summary); 5259 let pre = document.createElement("pre"); 5260 let code = document.createElement("code"); 5261 if (typeof event.response === 'string' || event.response instanceof String) { 5262 code.classList.add("language-plaintext"); 5263 code.textContent = event.response; 5264 } else { 5265 code.classList.add("language-json"); 5266 code.textContent = JSON.stringify(event.response || event.request, null, 2); 5267 } 5268 pre.appendChild(code) 5269 details.appendChild(pre); 5270 const detailsList = logContent.getElementsByTagName('details'); 5271 if (detailsList.length >= 100) { 5272 logContent.removeChild(detailsList[0]); 5273 } 5274 5275 logContent.appendChild(details); 5276 if (window.hljs) { 5277 hljs.highlightElement(code); 5278 } 5279} 5280 5281// Mobile Experience Enhancements 5282 5283// Create overlay element for sidebar 5284function createSidebarOverlay() {
5285 const overlay = document.createElement('div'); 5286 overlay.className = 'sidebar-overlay'; 5287 overlay.addEventListener('click', () => { 5288 sidebar.classList.remove('shown'); 5289 overlay.classList.remove('active'); 5290 }); 5291 document.body.appendChild(overlay); 5292 return overlay; 5293} 5294 5295// Initialize mobile enhancements 5296function initMobileEnhancements() { 5297 const overlay = createSidebarOverlay(); 5298 5299 // Enhance sidebar toggle behavior 5300 sidebar_buttons.forEach((el) => { 5301 el.removeEventListener('click', null); 5302 el.addEventListener('click', () => { 5303 if (window.innerWidth < 640) { 5304 if (sidebar.classList.contains('shown')) { 5305 sidebar.classList.remove('shown'); 5306 overlay.classList.remove('active'); 5307 } else { 5308 sidebar.classList.add('shown'); 5309 overlay.classList.add('active'); 5310 } 5311 } else { 5312 // Desktop behavior remains the same 5313 if (sidebar.classList.contains('shown')) { 5314 sidebar.classList.remove('shown'); 5315 sidebar.classList.add('minimized'); 5316 } else { 5317 sidebar.classList.remove('minimized'); 5318 sidebar.classList.add('shown'); 5319 } 5320 } 5321 }); 5322 }); 5323 5324 // Add swipe gesture support 5325 let touchStartX = 0; 5326 let touchEndX = 0; 5327 5328 document.addEventListener('touchstart', e => { 5329 touchStartX = e.changedTouches[0].screenX; 5330 }, { passive: true }); 5331 5332 document.addEventListener('touchend', e => { 5333 touchEndX = e.changedTouches[0].screenX; 5334 handleSwipeGesture(); 5335 }, { passive: true }); 5336 5337 function handleSwipeGesture() { 5338 const swipeThreshold = 100; 5339 5340 // Right swipe (from left edge) - open sidebar 5341 if (touchEndX - touchStartX > swipeThreshold && touchStartX < 30) { 5342 sidebar.classList.add('shown'); 5343 overlay.classList.add('active'); 5344 } 5345 5346 // Left swipe - close sidebar 5347 if (touchStartX - touchEndX > swipeThreshold && sidebar.classList.contains('shown')) { 5348 sidebar.classList.remove('shown'); 5349 overlay.classList.remove('active'); 5350 } 5351 } 5352 5353 // Double tap to scroll to bottom 5354// let lastTap = 0; 5355// chatBody.addEventListener('touchend', e => { 5356// const currentTime = new Date().getTime(); 5357// const tapLength = currentTime - lastTap; 5358 5359// if (tapLength < 300 && tapLength > 0) { 5360// // Double tap detected 5361// scroll_to_bottom(); 5362// e.preventDefault(); 5363// } 5364 5365// lastTap = currentTime; 5366// }); 5367 5368 // Improve file input experience on mobile 5369 const fileLabels = document.querySelectorAll('.file-label');
5370 fileLabels.forEach(label => { 5371 label.addEventListener('touchstart', () => { 5372 label.classList.add('active-touch'); 5373 }); 5374 5375 label.addEventListener('touchend', () => { 5376 setTimeout(() => { 5377 label.classList.remove('active-touch'); 5378 }, 200); 5379 }); 5380 }); 5381} 5382 5383// Call this function after the DOM is loaded 5384window.addEventListener('load', () => { 5385 if (window.matchMedia('(max-width: 640px)').matches || window.matchMedia('(pointer: coarse)').matches) { 5386 initMobileEnhancements(); 5387 } 5388}); 5389 5390// Handle orientation changes 5391window.addEventListener('orientationchange', () => { 5392 // Adjust UI based on new orientation 5393 setTimeout(() => { 5394 document.querySelector(".container").style.maxHeight = window.innerHeight + "px"; 5395 5396 // Adjust media content display 5397 // adjustMediaContentForOrientation(); 5398 }, 200); 5399}); 5400 5401// // Adaptive Media Content Display 5402 5403// // Function to adjust media content based on screen size and orientation 5404// function adjustMediaContentForOrientation() { 5405// const isLandscape = window.innerWidth > window.innerHeight; 5406// const mediaElements = document.querySelectorAll('.message .content img, .message .content video'); 5407 5408// mediaElements.forEach(media => { 5409// // Reset styles first 5410// media.style.maxWidth = ''; 5411// media.style.maxHeight = ''; 5412 5413// // Get natural dimensions 5414// const naturalWidth = media.naturalWidth || media.videoWidth || 400; 5415// const naturalHeight = media.naturalHeight || media.videoHeight || 300;
5416// const aspectRatio = naturalWidth / naturalHeight; 5417 5418// if (isLandscape) { 5419// // In landscape, prioritize height 5420// media.style.maxHeight = '70vh'; 5421// media.style.maxWidth = '90vw'; 5422// } else { 5423// // In portrait, limit width more strictly 5424// media.style.maxWidth = '95vw'; 5425// media.style.maxHeight = '50vh'; 5426// } 5427 5428// // Add special class for better display 5429// media.classList.add('adaptive-media'); 5430// }); 5431// } 5432 5433// // Function to enhance image viewing experience 5434// function enhanceMobileImageViewing() { 5435// // Improve image tap behavior 5436// document.addEventListener('click', e => { 5437// const target = e.target; 5438 5439// // Check if clicked element is an image in a message 5440// if (target.tagName === 'IMG' && target.closest('.message')) { 5441// // Don't apply to avatar images 5442// if (target.alt === 'your avatar') return; 5443 5444// // Toggle fullscreen-like view 5445// if (target.classList.contains('expanded-view')) { 5446// target.classList.remove('expanded-view'); 5447// } else { 5448// // Remove expanded view from any other images 5449// document.querySelectorAll('.expanded-view').forEach(img => { 5450// img.classList.remove('expanded-view'); 5451// }); 5452 5453// target.classList.add('expanded-view'); 5454// } 5455// } else if (!target.closest('img.expanded-view')) { 5456// // Close expanded view when clicking elsewhere 5457// document.querySelectorAll('.expanded-view').forEach(img => { 5458// img.classList.remove('expanded-view'); 5459// }); 5460// } 5461// }); 5462// } 5463 5464// // Register these functions to run after content is loaded 5465// function registerMediaEnhancements() { 5466// // Run initially 5467// adjustMediaContentForOrientation(); 5468// enhanceMobileImageViewing(); 5469 5470// // Also run when new messages are added 5471// const originalRegisterMessageImages = register_message_images; 5472// register_message_images = function() { 5473// originalRegisterMessageImages(); 5474// adjustMediaContentForOrientation(); 5475// }; 5476 5477// // And when window is resized 5478// window.addEventListener('resize', adjustMediaContentForOrientation); 5479// } 5480 5481// Add this to the window load event 5482// window.addEventListener('load', registerMediaEnhancements); 5483 5484// Mobile Experience Initialization 5485 5486// Function to check if device is mobile 5487function isMobileDevice() { 5488 return window.matchMedia('(max-width: 640px)').matches || 5489 window.matchMedia('(pointer: coarse)').matches; 5490} 5491 5492// Function to apply mobile-specific enhancements 5493function applyMobileEnhancements() { 5494 if (document.body.classList.contains("screen-reader")) { 5495 return; // Skip enhancements for screen readers 5496 } 5497 5498 // Hotfix for mobile 5499 document.querySelector(".container").style.maxHeight = window.innerHeight + "px"; 5500 5501 // Add mobile class to body for CSS targeting 5502 document.body.classList.add('mobile-device'); 5503 5504 // Adjust height for mobile browsers (handles address bar) 5505 function setMobileHeight() { 5506 document.querySelector(".container").style.maxHeight = window.innerHeight + "px"; 5507 document.querySelector(".container").style.height = window.innerHeight + "px"; 5508 } 5509 5510 setMobileHeight(); 5511 window.addEventListener('resize', setMobileHeight); 5512 5513 // Improve scroll behavior 5514 const chatBody = document.getElementById('chatBody'); 5515 chatBody.style.overscrollBehavior = 'contain'; 5516 5517 // Enhance touch feedback for all interactive elements 5518 const touchElements = document.querySelectorAll('button, .file-label, .micro-label, select, .convo'); 5519 touchElements.forEach(el => { 5520 el.addEventListener('touchstart', () => { 5521 el.classList.add('active-touch'); 5522 }, { passive: true }); 5523 5524 el.addEventListener('touchend', () => { 5525 setTimeout(() => { 5526 el.classList.remove('active-touch'); 5527 }, 200); 5528 }, { passive: true }); 5529 }); 5530 5531 // Optimize input field behavior 5532 const userInput = document.getElementById('userInput'); 5533 userInput.addEventListener('focus', () => { 5534 // Small delay to ensure keyboard is open 5535 setTimeout(() => { 5536 window.scrollTo(0, 0); 5537 document.body.scrollTop = 0; 5538 }, 300); 5539 }); 5540 5541 // Show/hide floating action button based on scroll position 5542 let lastScrollTop = 0; 5543 const floatingButton = document.querySelector('.new_convo_icon.mobile-only'); 5544 if (floatingButton) { 5545 chatBody.addEventListener('scroll', () => { 5546 const st = chatBody.scrollTop; 5547 if (st > lastScrollTop && st > 100) { 5548 // Scrolling down - hide button 5549 floatingButton.style.transform = 'translateY(80px)'; 5550 } else { 5551 // Scrolling up - show button 5552 floatingButton.style.transform = 'translateY(0)'; 5553 } 5554 lastScrollTop = st; 5555 }, { passive: true }); 5556 } 5557} 5558 5559// Initialize mobile enhancements if on mobile device 5560document.addEventListener('DOMContentLoaded', () => {
5561 if (isMobileDevice()) { 5562 applyMobileEnhancements(); 5563 initMobileEnhancements(); // From previous code 5564 } 5565 5566 // Add CSS class based on orientation 5567 function updateOrientationClass() { 5568 if (window.innerWidth > window.innerHeight) { 5569 document.body.classList.add('landscape'); 5570 document.body.classList.remove('portrait'); 5571 } else { 5572 document.body.classList.add('portrait'); 5573 document.body.classList.remove('landscape'); 5574 } 5575 } 5576 5577 updateOrientationClass(); 5578 window.addEventListener('resize', updateOrientationClass); 5579 window.addEventListener('orientationchange', updateOrientationClass); 5580}); 5581 5582// Create drag-and-drop zones 5583function setupDragAndDrop() { 5584 const dropZone = document.createElement('div'); 5585 dropZone.className = 'file-drop-zone hidden'; 5586 dropZone.innerHTML = ` 5587 <div class="file-drop-content"> 5588 <i class="fa-solid fa-cloud-arrow-up"></i> 5589 <p>Drop files here to upload</p> 5590 </div> 5591 `; 5592 document.querySelector('.container').appendChild(dropZone); 5593 5594 // Add CSS for drop zone 5595 const dropZoneStyles = document.createElement('style'); 5596 dropZoneStyles.textContent = ` 5597 .file-drop-zone { 5598 position: fixed; 5599 top: 0; 5600 left: 0; 5601 width: 100%; 5602 height: 100%; 5603 background-color: rgba(0, 0, 0, 0.7); 5604 z-index: 2000; 5605 display: flex; 5606 align-items: center; 5607 justify-content: center; 5608 pointer-events: none; 5609 opacity: 0; 5610 transition: opacity 0.3s ease; 5611 } 5612 5613 .file-drop-zone.active { 5614 opacity: 1; 5615 pointer-events: auto; 5616 } 5617 5618 .file-drop-zone.drag-over { 5619 background-color: rgba(139, 61, 255, 0.3); 5620 } 5621 5622 .file-drop-content { 5623 background-color: var(--blur-bg); 5624 border: 2px dashed var(--accent); 5625 border-radius: var(--border-radius-1); 5626 padding: 40px; 5627 text-align: center; 5628 color: var(--colour-3); 5629 max-width: 80%; 5630 } 5631 5632 .file-drop-content i { 5633 font-size: 48px; 5634 margin-bottom: 20px; 5635 color: var(--accent); 5636 } 5637 5638 .file-drop-content p { 5639 font-size: 18px; 5640 margin: 0; 5641 } 5642 5643 /* Add highlight to chat area when dragging */ 5644 .chat-body.drag-highlight { 5645 border: 2px dashed var(--accent); 5646 background-color: rgba(139, 61, 255, 0.1); 5647 } 5648 `; 5649 document.head.appendChild(dropZoneStyles); 5650 5651 // Handle drag and drop events 5652 const handleDragOver = (e) => { 5653 e.preventDefault(); 5654 e.stopPropagation(); 5655 dropZone.classList.add('active'); 5656 dropZone.classList.add('drag-over'); 5657 }; 5658 5659 const handleDragLeave = (e) => { 5660 e.preventDefault(); 5661 e.stopPropagation(); 5662 dropZone.classList.remove('drag-over'); 5663 5664 // Check if the drag left the document 5665 const rect = dropZone.getBoundingClientRect(); 5666 const x = e.clientX; 5667 const y = e.clientY; 5668 5669 if ( 5670 x < rect.left || 5671 x >= rect.right || 5672 y < rect.top || 5673 y >= rect.bottom 5674 ) { 5675 dropZone.classList.remove('active'); 5676 } 5677 }; 5678 5679 const handleDrop = async (e) => { 5680 e.preventDefault(); 5681 e.stopPropagation(); 5682 5683 dropZone.classList.remove('active'); 5684 dropZone.classList.remove('drag-over'); 5685 chatBody.classList.remove('drag-highlight'); 5686 5687 if (e.dataTransfer.files.length > 0) { 5688 // Handle image files 5689 const imageFiles = Array.from(e.dataTransfer.files).filter(file => 5690 file.type.startsWith('image/') 5691 ); 5692 5693 if (imageFiles.length > 0) { 5694 imageFiles.forEach(file => { 5695 image_storage[URL.createObjectURL(file)] = file; 5696 }); 5697 renderMediaSelect(); 5698 mediaSelect.classList.remove('hidden'); 5699 } 5700 5701 // Handle other files 5702 const otherFiles = Array.from(e.dataTransfer.files).filter(file => 5703 !file.type.startsWith('image/') 5704 ); 5705 5706 if (otherFiles.length > 0) { 5707 // Create a new FileList-like object 5708 const dataTransfer = new DataTransfer();
5709 otherFiles.forEach(file => dataTransfer.items.add(file)); 5710 5711 // Set the files to the file input 5712 fileInput.files = dataTransfer.files; 5713 5714 // Trigger the change event 5715 const event = new Event('change', { bubbles: true }); 5716 fileInput.dispatchEvent(event); 5717 } 5718 } 5719 }; 5720 5721 // Add event listeners to document 5722 document.addEventListener('dragenter', (e) => { 5723 e.preventDefault(); 5724 e.stopPropagation(); 5725 dropZone.classList.add('active'); 5726 chatBody.classList.add('drag-highlight'); 5727 }); 5728 5729 document.addEventListener('dragover', handleDragOver); 5730 document.addEventListener('dragleave', handleDragLeave); 5731 document.addEventListener('drop', handleDrop); 5732 5733 // Add specific handling for chat body 5734 chatBody.addEventListener('dragover', (e) => { 5735 e.preventDefault(); 5736 e.stopPropagation(); 5737 chatBody.classList.add('drag-highlight'); 5738 }); 5739 5740 chatBody.addEventListener('dragleave', (e) => { 5741 e.preventDefault(); 5742 e.stopPropagation(); 5743 chatBody.classList.remove('drag-highlight'); 5744 }); 5745 5746 // NEW: Add a click handler to hide the drop zone when clicked outside 5747 dropZone.addEventListener('click', (e) => { 5748 if (e.target === dropZone) { 5749 dropZone.classList.remove('active'); 5750 dropZone.classList.remove('drag-over'); 5751 chatBody.classList.remove('drag-highlight'); 5752 } 5753 }); 5754 5755 // NEW: Add a global escape key handler to hide the drop zone 5756 document.addEventListener('keydown', (e) => { 5757 if (e.key === 'Escape') { 5758 dropZone.classList.remove('active'); 5759 dropZone.classList.remove('drag-over'); 5760 chatBody.classList.remove('drag-highlight'); 5761 } 5762 }); 5763 5764 // NEW: Force hide drop zone when window loses focus 5765 window.addEventListener('blur', () => { 5766 dropZone.classList.remove('active'); 5767 dropZone.classList.remove('drag-over'); 5768 chatBody.classList.remove('drag-highlight'); 5769 }); 5770 5771 // NEW: Add a safety cleanup function that runs periodically 5772 setInterval(() => { 5773 // If no drag is happening but the zone is still active, hide it 5774 if (!document.querySelector('.drag-highlight') && dropZone.classList.contains('active')) { 5775 dropZone.classList.remove('active'); 5776 dropZone.classList.remove('drag-over'); 5777 } 5778 }, 2000); 5779} 5780 5781// Initialize drag and drop 5782setupDragAndDrop(); 5783 5784// Enhance the existing file upload functionality 5785function enhanceFileUpload() { 5786 // Add visual feedback when files are being processed 5787 const originalUploadFiles = upload_files; 5788 upload_files = async function(fileInput) { 5789 // Show loading indicator 5790 const loadingIndicator = document.createElement('div'); 5791 loadingIndicator.className = 'file-upload-loading'; 5792 loadingIndicator.innerHTML = ` 5793 <div class="upload-spinner"></div> 5794 <p>${framework.translate("Uploading files...")}</p> 5795 `; 5796 document.body.appendChild(loadingIndicator); 5797 5798 try { 5799 await originalUploadFiles(fileInput); 5800 } finally { 5801 // Remove loading indicator 5802 document.body.removeChild(loadingIndicator); 5803 } 5804 }; 5805 5806 // Add CSS for loading indicator 5807 const loadingStyles = document.createElement('style'); 5808 loadingStyles.textContent = ` 5809 .file-upload-loading { 5810 position: fixed; 5811 bottom: 20px; 5812 right: 20px; 5813 background-color: var(--blur-bg); 5814 border-radius: var(--border-radius-1); 5815 padding: 15px; 5816 display: flex; 5817 align-items: center; 5818 gap: 10px; 5819 z-index: 1000; 5820 box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); 5821 } 5822 5823 .upload-spinner { 5824 width: 20px; 5825 height: 20px; 5826 border: 2px solid var(--colour-3); 5827 border-top-color: var(--accent); 5828 border-radius: 50%; 5829 animation: spinner 0.8s linear infinite; 5830 } 5831 5832 .file-upload-loading p { 5833 margin: 0; 5834 color: var(--colour-3); 5835 } 5836 `; 5837 document.head.appendChild(loadingStyles); 5838} 5839 5840enhanceFileUpload(); 5841 5842function isLive() { 5843 if (!providerSelect) { 5844 return true; 5845 } 5846 return providerSelect.options[providerSelect.selectedIndex]?.dataset?.live; 5847} 5848 5849async function initClient() { 5850 if (!isLive()) { 5851 client = null; 5852 return; 5853 } 5854 let messageId = null;
5855 let count = 0; 5856 function logCallback(event) { 5857 if (event.request) { 5858 messageId = generateUUID(); 5859 count = 0; 5860 } 5861 if (event.response || event.request) { 5862 logRequestResponse(event, messageId, count); 5863 count += 1; 5864 } 5865 } 5866 const provider = providerSelect?.value; 5867 const apiKey = get_api_key_by_provider(provider); 5868 const options = apiKey ? { apiKey } : {}; 5869 if (appStorage.getItem("debugMode") == "true") { 5870 options.logCallback = logCallback; 5871 } 5872 try { 5873 // Handle custom providers with custom:server_id format 5874 client = await window.createClient(provider, options); 5875 } catch (error) { 5876 console.error('Failed to create client:', error); 5877 return; 5878 } 5879 await loadClientModels(); 5880 return true; 5881} 5882 5883async function loadClientModels() { 5884 modelSelect.innerHTML = `<option value="" disabled selected>${framework.translate("Loading...")}</option>`; 5885 try { 5886 const [models, quota] = await Promise.all([client.models.list(), client.getQuota().catch(() => undefined)]); 5887 setQuotaInfo(models, quota); 5888 modelSelect.innerHTML = ''; 5889 models.forEach(model => { 5890 if (window.isValidModel && !isValidModel(model)) { 5891 return; 5892 } 5893 const opt = document.createElement('option'); 5894 opt.value = model.id; 5895 opt.text = model.label; 5896 if (model.type) { 5897 opt.dataset.type = model.type; 5898 } 5899 if (model.audio) { 5900 opt.dataset.audio = model.audio; 5901 } 5902 if (model.remaining_percent !== undefined) { 5903 opt.dataset.remaining = model.remaining_percent; 5904 } 5905 if (model.default) { 5906 opt.selected = true; 5907 } 5908 if (model.disabled) { 5909 opt.disabled = true; 5910 } 5911 modelSelect.appendChild(opt); 5912 }); 5913 if (models.length > 2) { 5914 setFavoriteModels(providerSelect?.value, client.defaultModel || models[0].id); 5915 } 5916 } catch (err) { 5917 console.error('Model load failed:', err); 5918 modelSelect.innerHTML = ""; 5919 } 5920} 5921 5922// Import old conversations from appStorage into IndexedDB 5923async function import_from_appStorage() { 5924 const prefix = 'conversation:'; 5925 const keys = Object.keys(appStorage).filter(k => k.startsWith(prefix)); 5926 5927 for (const key of keys) { 5928 try { 5929 const json = appStorage.getItem(key); 5930 if (!json) continue; 5931 const conv = JSON.parse(json); 5932 // Use the id from conversation, if missing fallback to key after prefix 5933 conv.id = conv.id || key.substring(prefix.length); 5934 conv.updated = conv.updated || Date.now(); 5935 await save_conversation(conv); 5936 appStorage.removeItem(key); // Optionally clear old storage 5937 } catch (e) { 5938 console.warn(`Skipping appStorage item ${key} due to error`, e); 5939 } 5940 } 5941} 5942 5943import_from_appStorage(); 5944 5945/** 5946 * Insert or wrap text with Markdown triple backâticks (```). 5947 * 5948 * @param {HTMLTextAreaElement|HTMLInputElement} el â The <textarea> (or <input type="text">). 5949 */ 5950function insertBackticksInTextarea(el) { 5951 // Modern browsers expose selectionStart / selectionEnd. 5952 const start = el.selectionStart; 5953 const end = el.selectionEnd; 5954 const value = el.value; 5955 5956 // ---------- CASE 1: a range is selected â wrap it ---------- 5957 if (start !== end) { 5958 const selected = value.slice(start, end); 5959 const before = value.slice(0, start); 5960 const after = value.slice(end); 5961 5962 // Wrap the selected text: ```selected``` 5963 const newText = `${before}\`\`\`\n${selected}\n\`\`\`${after}`; 5964 5965 // Replace and bring focus back to the textarea 5966 el.value = newText; 5967 // Keep the wrapped text selected (optional) 5968 el.setSelectionRange(start, start + newText.length - after.length); 5969 el.focus(); 5970 return; 5971 } 5972 5973 // ---------- CASE 2: nothing selected â insert empty block ---------- 5974 // Insert a block like: 5975 // ``` 5976 // | 5977 // ``` 5978 // where | is the new caret position.
5979 const fence = "```\n\n```"; 5980 const before = value.slice(0, start); 5981 const after = value.slice(start); 5982 5983 const newValue = before + fence + after; 5984 el.value = newValue; 5985 5986 // Put caret between the two newline characters (i.e. inside the empty block) 5987 const caretPos = start + 4; // after the opening ```\n 5988 el.setSelectionRange(caretPos, caretPos); 5989 el.focus(); 5990} 5991 5992// ============================================================ 5993// MCP (Model Context Protocol) Integration 5994// ============================================================ 5995 5996let mcpClient = null; 5997 5998// Initialize MCP client when page loads 5999document.addEventListener('DOMContentLoaded', () => { 6000 if (typeof MCPClient !== 'undefined') { 6001 mcpClient = new MCPClient(); 6002 try { 6003 initializeMCPUI(); 6004 } catch(e) { 6005 add_error(e) 6006 } 6007 } 6008}); 6009 6010function initializeMCPUI() { 6011 // Add default server if none exist 6012 if (mcpClient.servers.length === 0) { 6013 mcpClient.addServer({ name: 'Default', url: 'https://mcp.g4f.space' }); 6014 } 6015 6016 // Render servers list 6017 renderMCPServers(); 6018 6019 // Render tools list 6020 renderMCPTools(); 6021 6022 // Initial refresh of tools 6023 refreshMCPTools(); 6024 6025 // Add server button 6026 document.getElementById('add-mcp-server-btn')?.addEventListener('click', showAddServerDialog); 6027 6028 // Refresh tools button 6029 document.getElementById('refresh-mcp-tools-btn')?.addEventListener('click', refreshMCPTools); 6030 6031 // PA providers 6032 document.getElementById('refresh-pa-providers-btn')?.addEventListener('click', loadPaProviders); 6033 loadPaProviders(); 6034} 6035 6036function renderMCPServers() { 6037 const container = document.getElementById('mcp-servers-list'); 6038 if (!container || !mcpClient) return; 6039 6040 const servers = mcpClient.servers; 6041 6042 if (servers.length === 0) { 6043 container.innerHTML = '<div class="mcp-empty">No MCP servers configured. Click + to add one.</div>'; 6044 return; 6045 } 6046 6047 container.innerHTML = servers.map(server => ` 6048 <div class="mcp-server-item" data-server-id="${server.id}"> 6049 <div class="mcp-server-info"> 6050 <input type="checkbox" 6051 id="mcp-server-${server.id}" 6052 ${server.enabled ? 'checked' : ''} 6053 onchange="toggleMCPServer('${server.id}')"> 6054 <label for="mcp-server-${server.id}" class="mcp-server-name">${escapeHtml(server.name)}</label> 6055 <span class="mcp-server-url">${escapeHtml(server.url)}</span> 6056 </div> 6057 <button type="button" 6058 class="mcp-remove-btn" 6059 onclick="removeMCPServer('${server.id}')" 6060 aria-label="Remove server"> 6061 <i class="fa-solid fa-trash"></i> 6062 </button> 6063 </div> 6064 `).join(''); 6065} 6066 6067function renderMCPTools() { 6068 const container = document.getElementById('mcp-tools-list'); 6069 if (!container || !mcpClient) return; 6070 6071 const tools = mcpClient.getAllTools(); 6072 6073 if (tools.length === 0) { 6074 container.innerHTML = '<div class="mcp-empty">No tools available. Add an MCP server and refresh.</div>'; 6075 return; 6076 } 6077 6078 // Group tools by server 6079 const toolsByServer = {}; 6080 tools.forEach(tool => { 6081 if (!toolsByServer[tool.serverName]) { 6082 toolsByServer[tool.serverName] = []; 6083 } 6084 toolsByServer[tool.serverName].push(tool); 6085 }); 6086 6087 container.innerHTML = Object.entries(toolsByServer).map(([serverName, serverTools]) => ` 6088 <div class="mcp-server-tools"> 6089 <div class="mcp-server-group-title">${escapeHtml(serverName)}</div> 6090 ${serverTools.map(tool => ` 6091 <div class="mcp-tool-item"> 6092 <input type="checkbox" 6093 id="mcp-tool-${tool.toolId}" 6094 ${mcpClient.isToolSelected(tool.toolId) ? 'checked' : ''} 6095 onchange="toggleMCPTool('${tool.toolId}')"> 6096 <label for="mcp-tool-${tool.toolId}">
6097 <span class="mcp-tool-name">${escapeHtml(tool.name)}</span> 6098 ${tool.description ? `<span class="mcp-tool-desc">${escapeHtml(tool.description)}</span>` : ''} 6099 </label> 6100 </div> 6101 `).join('')} 6102 </div> 6103 `).join(''); 6104} 6105 6106function showAddServerDialog() { 6107 const name = prompt('Enter MCP server name:'); 6108 if (!name) return; 6109 6110 const url = prompt('Enter MCP server URL (e.g., http://localhost:3000):'); 6111 if (!url) return; 6112 6113 try { 6114 mcpClient.addServer({ name, url }); 6115 renderMCPServers(); 6116 refreshMCPTools(); 6117 } catch (error) { 6118 alert('Error adding server: ' + error.message); 6119 } 6120} 6121 6122function removeMCPServer(serverId) { 6123 if (!confirm('Remove this MCP server?')) return; 6124 6125 mcpClient.removeServer(serverId); 6126 renderMCPServers(); 6127 renderMCPTools(); 6128} 6129 6130function toggleMCPServer(serverId) { 6131 mcpClient.toggleServer(serverId); 6132 renderMCPServers(); 6133 renderMCPTools(); 6134} 6135 6136function toggleMCPTool(toolId) { 6137 mcpClient.toggleToolSelection(toolId); 6138} 6139 6140async function refreshMCPTools() { 6141 const button = document.getElementById('refresh-mcp-tools-btn'); 6142 if (button) { 6143 button.disabled = true; 6144 button.innerHTML = '<i class="fa-solid fa-sync fa-spin"></i>'; 6145 } 6146 6147 try { 6148 await mcpClient.fetchAllTools(); 6149 renderMCPTools(); 6150 } catch (error) { 6151 alert('Error refreshing tools: ' + error.message); 6152 } finally { 6153 if (button) { 6154 button.disabled = false; 6155 button.innerHTML = '<i class="fa-solid fa-sync"></i>'; 6156 } 6157 } 6158} 6159 6160function escapeHtml(text) { 6161 const div = document.createElement('div'); 6162 div.textContent = text; 6163 return div.innerHTML; 6164} 6165 6166// ============================================================ 6167// PA Providers listing 6168// ============================================================ 6169 6170/** 6171 * Derive the base URL for PA endpoints. 6172 * Uses the first enabled MCP server URL (strips /mcp suffix) or falls back 6173 * to window.location.origin. 6174 */ 6175function getPaBaseUrl() { 6176 if (typeof mcpClient !== 'undefined' && mcpClient.servers.length > 0) { 6177 const first = mcpClient.servers.find(s => s.enabled) || mcpClient.servers[0]; 6178 // MCP server URLs end with /mcp â strip that to get the base 6179 return first.url.replace(/\/mcp$/, ''); 6180 } 6181 return window.location.origin; 6182} 6183 6184async function fetchPaProviders() { 6185 const res = await fetch(`${framework.backendUrl}/pa/providers`, { headers: { 'Accept': 'application/json' } }); 6186 if (!res.ok) throw new Error(`HTTP ${res.status}`); 6187 return await res.json(); 6188} 6189 6190async function loadPaProviderSelect(optgroup) { 6191 optgroup = optgroup || document.getElementById('pa-providers-optgroup'); 6192 if (!optgroup) return; 6193 try { 6194 window._paProviders = window._paProviders || await fetchPaProviders(); 6195 // Remove stale options 6196 optgroup.innerHTML = ''; 6197 window._paProviders.forEach(p => { 6198 const opt = document.createElement('option'); 6199 opt.value = `pa:${p.id}`; 6200 opt.dataset.pa = 'true'; 6201 opt.dataset.paId = p.id; 6202 opt.dataset.label = p.label || p.id; 6203 const modelHint = Array.isArray(p.models) && p.models.length > 0 ? ` (${p.models.length} model${p.models.length > 1 ? 's' : ''})` : ''; 6204 opt.text = `${p.label || p.id}${modelHint} ð`; 6205 optgroup.appendChild(opt); 6206 }); 6207 } catch (e) { 6208 console.debug('Failed to load PA providers into select:', e); 6209 } 6210} 6211 6212async function loadPaProviders() { 6213 const btn = document.getElementById('refresh-pa-providers-btn'); 6214 if (btn) { 6215 btn.disabled = true; 6216 btn.innerHTML = '<i class="fa-solid fa-sync fa-spin"></i>'; 6217 } 6218 try { 6219 const providers = await fetchPaProviders(); 6220 window._paProviders = providers; 6221 renderPaProviders(providers); 6222 // Also refresh the select dropdown 6223 await loadPaProviderSelect(); 6224 } catch (err) { 6225 const container = document.getElementById('pa-providers-l
6225ist'); 6226 if (container) container.innerHTML = `<div class="mcp-empty">Failed to load PA providers: ${escapeHtml(String(err))}</div>`; 6227 } finally { 6228 if (btn) { 6229 btn.disabled = false; 6230 btn.innerHTML = '<i class="fa-solid fa-sync"></i>'; 6231 } 6232 } 6233} 6234 6235function renderPaProviders(providers) { 6236 const container = document.getElementById('pa-providers-list'); 6237 if (!container) return; 6238 if (!providers || providers.length === 0) { 6239 container.innerHTML = '<div class="mcp-empty">No PA providers found. Add <code>.pa.py</code> files to <code>~/.g4f/workspace</code> and refresh.</div>'; 6240 return; 6241 } 6242 container.innerHTML = providers.map(p => { 6243 const models = Array.isArray(p.models) ? p.models.join(', ') : ''; 6244 const url = p.url ? `<a href="${escapeHtml(p.url)}" target="_blank" rel="noopener noreferrer" class="mcp-server-url">${escapeHtml(p.url)}</a>` : ''; 6245 return `<div class="mcp-tool-item"> 6246 <div> 6247 <span class="mcp-tool-name">${escapeHtml(p.label || p.id)}</span> 6248 ${url} 6249 ${models ? `<span class="mcp-tool-desc">${escapeHtml(models)}</span>` : ''} 6250 </div> 6251 </div>`; 6252 }).join(''); 6253} 6254 6255document.getElementById('refresh-pa-providers-btn')?.addEventListener('click', loadPaProviders); 6256 6257/** 6258 * Handle tool calls from assistant 6259 */ 6260async function handleToolCalls(toolCalls, messages, model, provider, message_id, finish_message=()=>{}) { 6261 try { 6262 // Display tool calls in the chat 6263 for (const toolCall of toolCalls) { 6264 const toolName = toolCall.function.name; 6265 const toolArgs = toolCall.function.arguments; 6266 6267 const toolMessage = `\nð§ **Tool Call:** \`${toolName}\`\n\`\`\`json\n${typeof toolArgs === 'string' ? toolArgs : JSON.stringify(toolArgs, null, 2)}\n\`\`\``; 6268 await add_message_chunk({type: "reasoning", token: toolMessage}, message_id); 6269 } 6270 6271 // Execute tool calls 6272 const toolResults = await mcpClient.executeToolCalls(toolCalls); 6273 6274 // Display tool results 6275 for (const result of toolResults) { 6276 const resultMessage = `\nâ **Tool Result:** \`${result.name}\`\n\`\`\`json\n${result.content}\n\`\`\`\n`; 6277 await add_message_chunk({type: "reasoning", token: resultMessage}, message_id); 6278 } 6279 6280 // Add tool results to messages and continue conversation 6281 const updatedMessages = [...messages, { 6282 role: 'assistant', 6283 content: '', 6284 tool_calls: toolCalls.map(tc => ({ 6285 id: tc.id, 6286 type: 'function', 6287 function: { 6288 name: tc.function.name, 6289 arguments: tc.function.arguments 6290 } 6291 })) 6292 }, ...toolResults]; 6293 6294 6295 // Make another API call with tool results 6296 controller_storage[message_id] = new AbortController(); 6297 if (client) { 6298 const stream = await client.chat.completions.create({ 6299 model: model, 6300 messages: updatedMessages, 6301 stream: true, 6302 signal: controller_storage[message_id].signal 6303 }); 6304 6305 for await (const chunk of stream) { 6306 if (chunk.error) { 6307 add_message_chunk({type: "error", ...chunk.error}, message_id); 6308 return; 6309 } 6310 if (chunk.choices) { 6311 const choice = chunk.choices[0]; 6312 if (choice?.delta?.reasoning || choice?.delta?.reasoning_content) { 6313 await add_message_chunk({type: "reasoning", token: choice.delta.reasoning || choice.delta.reasoning_content}, message_id); 6314 } 6315 if (choice?.delta?.content) { 6316 const delta = choice?.delta?.content || ''; 6317 if (delta) { 6318 await add_message_chunk({type: "content", content: delta}, message_id); 6319 } 6320 } 6321 } 6322 } 6323 } else { 6324 const apiKey = get_api_key_by_provider(provider); 6325 const downloadMedia = document.getElementById("download_media")?.checked; 6326 let apiBase; 6327 if (provider == "Custom") { 6328 apiBase = appStorage.getItem("Custom-api_base"); 6329 } 6330 const ignored = Array.from(settings.querySelectorAll("input.provider:not(:checked)")).map((el)=>el.value); 6331 await api("conversation", { 6332 id: message_id, 6333 conversation_id: window.conversation_id, 6334 model: model, 6335 provider: provider, 6336 messages: updatedMessages, 6337 action: "next", 6338 download_media: downloadMedia, 6339 debug_mode: appStorage.getItem("debugMode") == "true", 6340 api_key: apiKey,
6341 api_base: apiBase, 6342 ignored: ignored 6343 }, [], message_id, finish_message); 6344 } 6345 } catch (error) { 6346 console.error('Error handling tool calls:', error); 6347 const errorMessage = `\nâ **Tool Execution Error:** ${error.message}`; 6348 await add_message_chunk({type: "reasoning", token: errorMessage}, message_id); 6349 } 6350} 6351 6352// Cloud Sync Functions 6353const CLOUD_SYNC_API = "https://auth.gpt4free.workers.dev/members/api"; 6354 6355async function checkCloudSyncSession() { 6356 const token = appStorage.getItem("session_token"); 6357 if (!token) { 6358 showCloudSyncLogin(); 6359 return; 6360 } 6361 try { 6362 const url = token.startsWith("g4f_") ? `${CLOUD_SYNC_API}/keys/validate` : `${CLOUD_SYNC_API}/user`; 6363 const response = await fetch(url, { 6364 headers: { "Authorization": `Bearer ${token}` } 6365 }); 6366 if (response.ok) { 6367 const data = await response.json(); 6368 if (data.user || data.username) { 6369 showCloudSyncLoggedIn(data.user || {name: data.username, tier: data.tier}); 6370 return; 6371 } else { 6372 appStorage.removeItem("session_token"); 6373 showCloudSyncLogin(); 6374 return; 6375 } 6376 } else { 6377 appStorage.removeItem("session_token"); 6378 showCloudSyncLogin(); 6379 return; 6380 } 6381 } catch (e) { 6382 console.error("Cloud sync session check failed:", e); 6383 // Keep the token but show login section on error (network issue) 6384 showCloudSyncLogin(); 6385 } 6386} 6387 6388function showCloudSyncLogin() { 6389 const loginSection = document.getElementById("cloudSyncLogin"); 6390 const syncSection = document.getElementById("cloudSyncSection"); 6391 if (loginSection) loginSection.style.display = "block"; 6392 if (syncSection) syncSection.style.display = "none"; 6393 6394 // Update sidebar login/logout buttons 6395 const sidebarLoginBtn = document.getElementById("sidebar-login-btn"); 6396 const sidebarLogoutBtn = document.getElementById("sidebar-logout-btn"); 6397 const tierText = document.getElementById("user-tier-text"); 6398 const tierLimitsRow = document.getElementById("tier-limits-row"); 6399 if (sidebarLoginBtn) sidebarLoginBtn.classList.remove("hidden"); 6400 if (sidebarLogoutBtn) sidebarLogoutBtn.classList.add("hidden"); 6401 if (tierText) tierText.textContent = "Guest"; 6402 if (tierLimitsRow) tierLimitsRow.classList.add("hidden"); 6403} 6404 6405function showCloudSyncLoggedIn(user) { 6406 const loginSection = document.getElementById("cloudSyncLogin"); 6407 const syncSection = document.getElementById("cloudSyncSection"); 6408 const userEl = document.getElementById("cloudSyncUser"); 6409 if (loginSection) loginSection.style.display = "none"; 6410 if (syncSection) syncSection.style.display = "block"; 6411 if (userEl) userEl.textContent = user.name || user.email || "User"; 6412 6413 // Update sidebar login/logout buttons 6414 const sidebarLoginBtn = document.getElementById("sidebar-login-btn"); 6415 const sidebarLogoutBtn = document.getElementById("sidebar-logout-btn"); 6416 const tierText = document.getElementById("user-tier-text"); 6417 const tierLimitsRow = document.getElementById("tier-limits-row"); 6418 const infoBar = document.getElementById("user-tier-info"); 6419 if (sidebarLoginBtn) sidebarLoginBtn.classList.add("hidden"); 6420 if (sidebarLogoutBtn) sidebarLogoutBtn.classList.remove("hidden"); 6421 if (tierText) tierText.textContent = user.name || user.email || "User"; 6422 if (tierLimitsRow) tierLimitsRow.classList.remove("hidden"); 6423 if (infoBar && user.tier) { 6424 infoBar.setAttribute("data-tier", user.tier); 6425 } 6426} 6427 6428function handleCloudSyncCallback() { 6429 // Check hash fragment for provider-specific API keys or session tokens 6430 const hashStr = window.location.hash ? decodeURIComponent(window.location.hash.substring(1)) : ""; 6431 const hashParams = new URLSearchParams(hashStr); 6432 const token = hashParams.get("session"); 6433 const userParam = hashParams.get("user"); 6434 const openSettings = hashParams.has("settings"); 6435 6436 // Handle provider API keys from URL hash (set by members page after OAuth) 6437 if (token) { 6438 const location_url = window.location.href.split("#")[0] + (hashParams.get("conversation") ? `#${hashParams.get("conversation")}` : ""); 6439 window.history.replaceState({}, document.title, location_url); 6440 appStorage.setItem("session_token", token); 6441 6442 // Parse and use user info if provided 6443 if (userParam) { 6444 try { 6445 const user = JSON.parse(decodeURIComponent(userParam)); 6446 // Also store provider-specific API key if included in user info 6447 if (user.pollinations?.api_key) { 6448 appStorage.setItem("PollinationsAI-api_key", user.pollinations.api_key); 6449 } 6450 if (user.provider === "huggingface" && user.access_token) { 6451 appStorage.setItem("HuggingFace-api_key", user.access_token); 6452 } 6453 if (user.airforce?.access_token) { 6454 appStorage.setItem("ApiAirforce-api_key", user.airforce.access_token); 6455 } 6456 showCloudSyncLoggedIn(user); 6457 } catch (e) { 6458 console.error("Failed to parse user data:", e); 6459 } 6460 } 6461 6462 // Open settings to cloud sync tab if requested 6463 if (openSettings) { 6464 setTimeout(() => { 6465 open_settings(); 6466 const cloudSyncTab = document.querySelector(`.settings-tab[data-tab="${hashParams.get("tab")}"]`); 6467 if (cloudSyncTab) cloudSyncTab.click(); 6468 }, 100); 6469 } 6470 6471 checkCloudSyncSession(); 6472 } 6473} 6474 6475async function cloudSyncLogout() { 6476 const token = appStorage.getItem("session_token"); 6477 if (token) { 6478 try { 6479 await fetch(`${CLOUD_SYNC_API}/logout`, { 6480 method: "POST", 6481 headers: { "Authorization": `Bearer ${token}` } 6482 }); 6483 } catch (e) { 6484 console.error("Logout failed:", e); 6485 } 6486 } 6487 appStorage.removeItem("session_token"); 6488 showCloudSyncLogin(); 6489} 6490 6491// Helper function to show/hide cloud sync loading indicator 6492function showCloudSyncLoading(message) { 6493 let loadingEl = document.getElementById("cloudSyncLoading"); 6494 if (!loadingEl) { 6495 loadingEl = document.createElement("div"); 6496 loadingEl.id = "cloudSyncLoading"; 6497 loadingEl.className = "file-upload-loading"; 6498 document.body.appendChild(loadingEl); 6499 } 6500 loadingEl.innerHTML = ` 6501 <div class="upload-spinner"></div> 6502 <p>${framework.translate(message)}</p> 6503 `; 6504 loadingEl.style.display = "flex"; 6505} 6506 6507function hideCloudSyncLoading() { 6508 const loadingEl = document.getElementById("cloudSyncLoading"); 6509 if (loadingEl) { 6510 loadingEl.style.display = "none"; 6511 } 6512} 6513 6514async function syncConversationsToCloud() { 6515 const token = appStorage.getItem("session_token"); 6516 if (!token) { 6517 cloudSyncLoginRedirect(); 6518 return; 6519 } 6520 showCloudSyncLoading("Uploading conversations..."); 6521 try { 6522 const conversations = await list_conversations(); 6523 if (!conversations || conversations.length === 0) { 6524 hideCloudSyncLoading(); 6525 alert("No conversations to sync."); 6526 return; 6527 } 6528 const response = await fetch(`${CLOUD_SYNC_API}/conversations/sync`, { 6529 method: "POST", 6530 headers: { 6531 "Authorization": `Bearer ${token}`, 6532 "Content-Type": "application/json" 6533 }, 6534 body: JSON.stringify({ conversations }) 6535 }); 6536 hideCloudSyncLoading(); 6537 if (response.ok) { 6538 const data = await response.json(); 6539 console.log("Conversations synced to cloud:", data); 6540 alert(`${conversations.length} conversations uploaded to cloud successfully!`); 6541 } else { 6542 const error = await response.json(); 6543 throw new Error(error.error || "Sync failed"); 6544 } 6545 } catch (e) { 6546 hideCloudSyncLoading(); 6547 console.error("Cloud sync upload failed:", e); 6548 alert("Failed to upload conversations to cloud: " + e.message); 6549 } 6550} 6551 6552async function syncConversationsFromCloud() { 6553 const token = appStorage.getItem("session_token"); 6554 if (!token) { 6555 cloudSyncLoginRedirect(); 6556 return; 6557 } 6558 showCloudSyncLoading("Downloading conversations..."); 6559 try { 6560 const response = await fetch(`${CLOUD_SYNC_API}/conversations`, { 6561 headers: { "Authorization": `Bearer ${token}` } 6562 }); 6563 if (response.ok) { 6564 const data = await response.json(); 6565 if (data.conversations && data.conversations.length > 0) { 6566 for (const conv of data.conversations) { 6567 // Remove cloud-specific fields before saving locally 6568 delete conv.synced_at; 6569 delete conv.user_id; 6570 await save_conversation(conv); 6571 } 6572 await load_conversations(); 6573 hideCloudSyncLoading(); 6574 console.log("Conversations synced from cloud"); 6575 alert(`Downloaded ${data.conversations.length} conversations from cloud!`); 6576 } else { 6577 hideCloudSyncLoading();
6578 alert("No conversations found in cloud."); 6579 } 6580 } else { 6581 const error = await response.json(); 6582 throw new Error(error.error || "Sync failed"); 6583 } 6584 } catch (e) { 6585 hideCloudSyncLoading(); 6586 console.error("Cloud sync download failed:", e); 6587 alert("Failed to download conversations from cloud: " + e.message); 6588 } 6589} 6590 6591// Initialize cloud sync on page load 6592handleCloudSyncCallback(); 6593checkCloudSyncSession(); 6594 6595// Redirect to members login page 6596function cloudSyncLoginRedirect(provider = "airforce") { 6597 const returnUrl = encodeURIComponent(window.location.href.split("#")[0]); 6598 const conversation = window.conversation_id ? `&conversation=${encodeURIComponent(window.conversation_id)}` : ""; 6599 const providerParam = provider ? `&provider=${encodeURIComponent(provider)}` : ""; 6600 window.location.href = `https://g4f.dev/members.html?redirect=${returnUrl}${conversation}${providerParam}`; 6601} 6602 6603// Cloud Sync button event listeners 6604const cloudSyncLoginBtn = document.getElementById("cloudSyncLoginBtn"); 6605const cloudSyncUploadBtn = document.getElementById("cloudSyncUpload"); 6606const cloudSyncDownloadBtn = document.getElementById("cloudSyncDownload"); 6607const cloudSyncLogoutBtn = document.getElementById("cloudSyncLogoutBtn"); 6608 6609if (cloudSyncLoginBtn) cloudSyncLoginBtn.addEventListener("click", () => cloudSyncLoginRedirect(true)); 6610if (cloudSyncUploadBtn) cloudSyncUploadBtn.addEventListener("click", syncConversationsToCloud); 6611if (cloudSyncDownloadBtn) cloudSyncDownloadBtn.addEventListener("click", syncConversationsFromCloud); 6612if (cloudSyncLogoutBtn) cloudSyncLogoutBtn.addEventListener("click", cloudSyncLogout); 6613 6614// Expose functions to global scope 6615window.toggleMCPServer = toggleMCPServer; 6616window.removeMCPServer = removeMCPServer; 6617window.toggleMCPTool = toggleMCPTool; 6618window.cloudSyncLoginRedirect = cloudSyncLoginRedirect; 6619window.syncConversationsToCloud = syncConversationsToCloud; 6620window.syncConversationsFromCloud = syncConversationsFromCloud; 6621window.cloudSyncLogout = cloudSyncLogout;
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.