PageSourceSearch

https://gpt4free.github.io/dist/js/chat.v1.js?v=%7B%7B%20v%20%7D%7D

js gpt4free.github.io collected 2026-10-03 09:15:20 UTC 268,099 bytes, 6,621 lines download raw bytes

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] += `[![${sanitize(message, ' ')}](${img.url})](${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: [![alt](data:image/...))](...)
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}&nbsp;&nbsp;🆕`;
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.