~firefoxphpai-chat
12 items • Download ./*

..
ajax.php
ana-ai-chat.png
atom-one-dark.min.css
emoji.json
favicon.ico
highlight.min.js
index.php
note.txt
readme.txt
setup.php
setup_and_launch.py
worker.php


ai-chatindex.php
70 KB• 15•  8 hours ago•  DownloadRawClose
8 hours ago•  15

{}
<!DOCTYPE html>
<!--

    Author:        Twily  2015 - 2021
    Website:       http://twily.info/

-->
<?php
include('./setup.php');
?>
<html lang="en">
<head>
    <title>&gt;_ AI Terminal</title>
    <meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
    <link id="favicon" rel="shortcut icon" href="./favicon.ico?v-2" />

    <link rel="stylesheet" href="./atom-one-dark.min.css?v=11.9.0">
    <script type="text/javascript" src="./highlight.min.js?v=11.9.0"></script>
    <style type="text/css">
        html, body {
            width: 100%; height: 100%; margin: 0; padding: 0; overflow: hidden;
            font-family: "Droid Sans", "Liberation Sans", "DejaVu Sans", "Segoe UI", Sans;
            font-size: 11pt; background: #17181A; color: /*#AAABAD*/ #C7C9CE; box-sizing: border-box;
        }
        *, *:before, *:after { box-sizing: inherit; }
        *:focus { outline: none !important; }
        
        .app-layout { display: flex; width: 100%; height: 100%; }
        
        /* Sidebar styling */
        #sidebar { width: 280px; background: #111113; border-right: 1px solid #606163; display: flex; flex-direction: column; transition: width 0.2s ease; }
        #sidebar.collapsed { width: 0; overflow: hidden; border: none; }
        
        .sidebar-tabs { display: flex; background: #0a0b0d; border-bottom: 1px solid #333; }
        .tab-btn { flex: 1; padding: 10px; background: none; border: none; color: #777; cursor: pointer; font-weight: bold; font-size: 9pt; }
        .tab-btn.active { color: #fff; /*background: #111113; border-bottom: 2px solid #EA75BC*/; }
        #btn-tab-chats.active    { background: #170d17; border-bottom: 2px solid #EA75BC }
        #btn-tab-settings.active { background: #101020; border-bottom: 2px solid #4A90E2 }
        
        .tab-pane { display: none; flex: 1; overflow-y: auto; padding: 10px; flex-direction: column; gap: 8px; }
        .tab-pane.active { display: flex; }
        
        .sidebar-header { padding: 4px 0; border: none; display: flex; justify-content: space-between; align-items: center; }
        .chat-list { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 5px; }
        .chat-item { padding: 8px 10px; background: #1c1d21; border: 1px solid #333; cursor: pointer; font-size: 10pt; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; user-select: none; min-height: 34px; }
        .chat-item:hover, .chat-item.active { border-color: #EA75BC; background: #25262c; }
        
        .setting-group { display: flex; flex-direction: column; gap: 4px; margin-bottom: 12px; font-size: 10pt; }
        .setting-group label { color: #888; }
        .setting-help { font-size: 9.5pt; color: #606163; margin-top: 4px; line-height: 1.3; }
        
        /* Generating indicator in sidebar */
        .chat-item.generating {
            border-color: #4a90e2 /*!important*/;
            background: #25262c /*!important*/;
            position: relative;
        }
        .chat-item-generating-dots {
            float: right;
            font-weight: bold;
            color: #4a90e2;
            animation: blink 1.4s infinite both;
        }

        /* Main pane */
        .main-pane { flex: 1; display: flex; flex-direction: column; padding: 10px; gap: 10px; height: 100%; }
        .top-bar { display: flex; gap: 10px; align-items: center; background: #111113; padding: 10px; border: 1px solid #606163; }
        .chat-box { flex: 1; background: #111113; border: 1px solid #606163; overflow-y: auto; padding: 15px; display: flex; flex-direction: column; gap: 12px; box-shadow: inset 0 0 2px 2px #000; }
        
        /* Input & Vibe Bar */
        .input-container { display: flex; flex-direction: column; gap: 5px; background: #111113; border: 1px solid #606163; padding: 8px; }
        .vibe-bar { display: flex; gap: 8px; align-items: center; padding-bottom: 5px; border-bottom: 1px solid #222; font-size: 10pt; }
        .emoji-btn { background: #1c1d21; border: 1px solid #333; color: #fff; padding: 2px 6px; cursor: pointer; border-radius: 3px; user-select: none; }
        .emoji-btn:hover { background: #25262c; border-color: #EA75BC; }
        .file-upload-lbl { font-size: 9pt; color: #aaa; cursor: pointer; background: #1c1d21; border: 1px solid #333; padding: 2px 8px; border-radius: 3px; user-select: none; }
        .file-upload-lbl:hover { border-color: #EA75BC; color: #fff; }

        /* Emoji Picker Modal */
        #emoji-modal {
            display: none; position: absolute; bottom: 90px; left: 10px; width: 320px; height: 220px;
            background: #111113; border: 1px solid #606163; border-radius: 4px; box-shadow: 0 4px 12px rgba(0,0,0,0.5);
            z-index: 99; flex-direction: column; overflow: hidden; user-select: none; 
        }
        #emoji-modal.open { display: flex; }
        .emoji-modal-header { padding: 6px 10px; background: #1a1b1e; border-bottom: 1px solid #333; font-size: 9pt; display: flex; justify-content: space-between; align-items: center; }
        .emoji-grid { flex: 1; overflow-y: auto; padding: 8px; display: grid; grid-template-columns: repeat(8, 1fr); gap: 4px; font-size: 14pt; text-align: center; }
        .emoji-cell { cursor: pointer; padding: 4px; border-radius: 3px; }
        .emoji-cell:hover { background: #25262c; }

        /* Lightbox Overlay */
        #image-lightbox {
            display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
            background: rgba(0, 0, 0, 0.88); z-index: 999; justify-content: center; align-items: center; padding: 30px;
            box-sizing: border-box;
        }
        #image-lightbox.open { display: flex; }
        
        #lightbox-img {
            display: block;
            object-fit: contain;
            border: 2px solid #606163;
            box-shadow: 0 0 25px rgba(0,0,0,0.9);
            cursor: pointer;
            /* Dimensions will be dynamically injected via JS scaling */
        }

        .input-area { display: flex; gap: 10px; }
        textarea { background: #000; color: #fff; border: 1px solid #666; resize: none; padding: 10px; font-family: inherit; font-size: inherit; flex: 1; height: 82px; }
        
        select, input[type="text"], button { background: #222; color: #fff; border: 1px solid #666; padding: 6px 12px; cursor: pointer; font-weight: bold; }
        button:hover { background: #333; border-color: #EA75BC; }
        
        .message { padding: 10px; border-left: 3px solid #606163; background: #1a1b1e; word-break: break-word; position: relative; }
        .message.user { border-left-color: #EA75BC; background: #1e1a1e; color: #AAABAD; }
        .message.ai { border-left-color: #4a90e2; line-height: 1.5;/* color: #e0e0e0;*/ }
        /* Headings inside AI messages */
        .message.ai h1, .message.ai h2, .message.ai h3 {
            margin: 3px 0 3px 0;
            color: #fff;
            font-weight: 600;
            border-bottom: 1px solid #333;
            padding-bottom: 4px;
        }
        
        .message.ai h1 { font-size: 13pt; }
        .message.ai h2 { font-size: 12pt; }
        .message.ai h3 { font-size: 11pt; }

        .message.ai h4 {
            margin: 10px 0 4px 0;
            color: #d1d5db;
            font-size: 10pt;
            font-weight: 600;
        }
        
        .message.ai ol {
            margin: 6px 0 6px 20px;
            padding: 0;
        }
        
        /* Paragraphs & Lists */
        .message.ai p {
            margin: 4px 0; /* Reduced from 6px to keep vertical flow tight */
        }
        
        .message.ai ul, .message.ai ol {
            margin: 4px 0 4px 20px;
            padding: 0;
        }
        
        .message.ai li {
            margin-bottom: 2px;
        }

        /* Style for inline state markers like *UI:* or *Action:* */
        .message.ai em {
            color: #61afef;
            font-style: normal;
            font-weight: 600;
        }
        
        /* Inline code or technical highlights outside pre blocks */
        .message.ai code:not(pre code) {
            background: rgba(255, 255, 255, 0.08);
            padding: 2px 4px;
            border-radius: 3px;
            font-family: monospace;
            font-size: 90%;
            color: #e5c07b;
        }
        
        /* Styled Blockquotes */
        .message.ai blockquote {
            border-left: 3px solid #61afef;
            margin: 8px 0;
            padding-left: 10px;
            color: #abb2bf;
            background: rgba(97, 175, 239, 0.05);
        }
        .message.system-error { border-left-color: #e74c3c; color: #e74c3c; }
        .message-actions {
            display: none;
            position: absolute;
            top: 4px;
            right: 8px;
            background: rgba(30, 30, 36, 0.85);
            border: 1px solid #444;
            border-radius: 4px;
            padding: 2px 4px;
            gap: 4px;
        }
        
        .message:hover .message-actions { display: flex; }
        
        .message-actions button {
            background: transparent;
            border: none;
            color: #ccc;
            font-size: 8pt;
            cursor: pointer;
            padding: 2px 6px;
            border-radius: 2px;
        }
        
        .message-actions button:hover { background: #444; color: #fff; }
		
		.smlnk {visibility: hidden; }
		.smlnk a:link, .smlnk a:visited { color: #715f71; text-decoration: none; }
		.smlnk a:hover, .smlnk a:active { color: #bdadbd; }
		.message:hover .smlnk { visibility: visible; }
		
		.attachment { color: #4f4f4f; font-size: 10pt; }
        
        pre { background: #0b0c0e !important; padding: 10px; border: 1px solid #333; overflow-x: auto; white-space: pre-wrap; word-break: break-all; max-width: 100%; position: relative; }
        code { font-family: monospace; white-space: pre-wrap; word-break: break-all; }
        img.generated-art { max-width: 400px; border: 1px solid #444; margin-top: 8px; }

        .code-copy-btn {
            position: absolute;
            top: 6px;
            right: 6px;
            background: rgba(40, 40, 48, 0.9);
            color: #bbb;
            border: 1px solid #555;
            border-radius: 3px;
            padding: 2px 6px;
            font-size: 7.5pt;
            cursor: pointer;
            z-index: 10;
            user-select: none;
        }
        .code-copy-btn:hover { background: #555; color: #fff; }

        .typing-dots span { animation: blink 1.4s infinite both; font-size: 16pt; line-height: 5px; }
        .typing-dots span:nth-child(2) { animation-delay: .2s; }
        .typing-dots span:nth-child(3) { animation-delay: .4s; }
        @keyframes blink { 0% { opacity: .2; } 20% { opacity: 1; } 100% { opacity: .2; } }
    </style>
    <script type="text/javascript">
        var $ = function(id) { return document.getElementById(id); };
        var currentChatId = null;

        var BACKEND_IP = "<?php echo $BACKEND_IP; ?>";
        var BACKEND_AI_PORT = <?php echo $BACKEND_AI_PORT; ?>;
        var BACKEND_IMG_PORT = <?php echo $BACKEND_IMG_PORT; ?>;

        function b64Enc(str) {
            return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function(match, p1) {
                return String.fromCharCode(parseInt(p1, 16));
            }));
        }

        function sendAjax(action, dataObj, cb) {
            var xhr = new XMLHttpRequest();
            xhr.open("POST", "./ajax.php", true);
            xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
            
            xhr.onreadystatechange = function() {
                if (xhr.readyState === 4 && xhr.status === 200) {
                    //try {
                        const rawData = xhr.responseText;
                        //console.log(rawData); // debug raw json
                        const cleanData = rawData.trim().replace(/^\uFEFF/, '');
                        
                        //try {
                        //  JSON.parse(rawString);
                        //} catch (e) {
                        //  console.error("Error Message:", e.message);
                        //
                        //  // Extract the position if provided in the error message
                        //  const posMatch = e.message.match(/position (\d+)/);
                        //  if (posMatch) {
                        //    const pos = parseInt(posMatch[1], 10);
                        //    console.log("Problem context:", rawString.substring(Math.max(0, pos - 20), pos + 20));
                        //  }
                        //}
                        const res = JSON.parse(cleanData);
                        if (typeof cb === 'function') cb(res);
                    //} catch(e) {
                    //    console.error("Invalid JSON response", xhr.responseText); // issue can mask other error culprits
                    //}
                }
            };
            xhr.send("action=" + action + "&payload=" + b64Enc(JSON.stringify(dataObj)));
        }

        function toggleSidebar() { $('sidebar').classList.toggle('collapsed'); }

        function switchTab(tabName) {
            document.querySelectorAll('.tab-pane').forEach(el => el.classList.remove('active'));
            document.querySelectorAll('.tab-btn').forEach(el => el.classList.remove('active'));
            $('pane-' + tabName).classList.add('active');$('btn-tab-' + tabName).classList.add('active');
            if(tabName === 'settings') loadSdModels();
        }

        var primaryActive=false;
        function loadChatList() {
            sendAjax('load_history_list', {}, function(res) {
                if (res.status === 'success') {
                    // Sort chats by time descending (newest/most recent first)
                    // may be sorted on php already fixed time
                    //console.log(res);
                    //res.chats.sort(function(a, b) {
                    //    return (b.time || 0) - (a.time || 0);
                    //});
                    //primaryActive=false; // do not reset here as new chat doesnt exist instantly

                    var listContainer = $('chat-list');
                    listContainer.innerHTML = '';
					var found=false;
                    res.chats.forEach(function(chat) {
                        var div = document.createElement('div');
                        var isGenerating = (chat.status === 'generating');
                        var isThinking = (chat.status === 'thinking');
                        
                        let divclass="chat-item";
                        if(chat.id==currentChatId) {
                            divclass+=" active"; // pink border
							
							found = true;

                            if(isGenerating || isThinking) {
                                primaryActive=true;
                            } else {
								primaryActive=false;
							}
                        } else if(isGenerating || isThinking) {
                            divclass+=" generating"; // blue border
                        }
                        div.className=divclass;
                        
                        // Set base display title
                        var titleText = chat.title || chat.id;
                        div.setAttribute('data-name',titleText);
                        
                        if (isGenerating || isThinking) { // loading dots in chat list
                            div.innerHTML = titleText + '<span class="chat-item-generating-dots">...</span>';
                            //addChatListLoading();
                        } else {
                            // clears and generating state visual
                            div.innerText = titleText;
                        }
                        
                        div.onclick = function() { switchChat(chat.id); };
                        listContainer.appendChild(div);

                        // add live view for any device
                        if(currentChatId==chat.id && !$('loading-bubble')) {
                            if(isGenerating) {
                                // If it's an image generation state, ensure it boots up with the Imagine tag
                                //var isImgGen = res.is_image_generating || true; // Or check your active session flag
                                var isImgGen = true;
                                appendMessage('loading', '', [], false, true, isImgGen);
                            } else if(isThinking) {
                                appendMessage('loading', '', [], false, true, false);
                            }
                        }
                    });
					
					//if(!found) {
					//	primaryActive remains its potential submitquery state?
					//}
                }
            });
        }

        function loadCurrentSdModel() {
            sendAjax('get_current_sd_model', {}, function(res) {
                if (res.status === 'success' && res.current_model) {
                    var select = $('sd-model-select');
                    select.value = res.current_model;
                }
            });
        }

        function loadSdModels() {
            sendAjax('get_sd_models', {}, function(res) {
                if (res.status === 'success') {
                    var select = $('sd-model-select');
                    select.innerHTML = '';
                    res.models.forEach(function(m) {
                        if(m.model_name.toLowerCase().includes('flux')) return;
                        var opt = document.createElement('option');
                        opt.value = m.title;
                        opt.innerText = m.model_name;
                        select.appendChild(opt);
                    });

                    loadCurrentSdModel();
                }
            });
        }

        function updateSdModel() {
            var val = $('sd-model-select').value;
            sendAjax('set_sd_model', { title: val }, function(res) {
                alert('Stable Diffusion Checkpoint switched successfully!');
            });
        }
		
		function nl2br(str) {
			return str.replace(/\n/g,'<br>');
		}

        // Global state maps in outer scope
        var activePollers = {}; // Stores timeout IDs keyed by chatId
        var chatDrafts = {};    // Stores un-sent textarea input drafts keyed by chatId
        
        // 1. Hook up chat switching to save and restore drafts
        function switchChat(chatId,sTB=true) {
            var chatBox = $('chat-box');
            if (currentChatId) {
                chatDrafts[currentChatId] = $('user-input').value;
            }
        
            currentChatId = chatId;
            $('user-input').value = chatDrafts[currentChatId] || '';

            let curscroll=chatBox.scrollTop;
            sendAjax('load_chat', { chat_id: chatId }, function(res) {
                //console.log(res);
                if (res.status === 'success') {
                    chatBox.innerHTML = '';

                    ltime=Date.now();

                    let msgIdx=0;
                    res.messages.forEach(function(msg) {
                        let attachment=[];
                        if(msg.hasOwnProperty('attachment')) {
                            if(typeof msg.attachment==="string") {
                                if(msg.attachment) {
									attachment.push(msg.attachment.substr(msg.attachment.lastIndexOf("/")+1));
                                }
                            } else if(Array.isArray(msg.attachment) && msg.attachment.length>0) { // array type is "object"
                                for(let i=0;i<msg.attachment.length;i++) {
                                    if(msg.attachment[i]) {
										attachment.push(msg.attachment[i].substr(msg.attachment[i].lastIndexOf("/")+1));
									}
                                }
                            }
                        }

                        appendMessage(
                            msg.role === 'user' ? 'user' : 'ai',
                            msg.content,
							attachment,
                            msg.is_image || false,
                            true,
                            false, //isGeneratingImage
                            msgIdx,
                            msg.is_upscaled || false // Pass optional key here!
                        );
                        msgIdx++;
                    });
        
                    $('chat-title-input').value = res.title || chatId;
        
                    // RESTORE GENERATING STATE IF REFRESHED/SWITCHED MID-STREAM
                    if (res.chat_status === 'generating') {
                        if (!$('loading-bubble')) {
                            // If it's an image generation state, ensure it boots up with the Imagine tag
                            //var isImgGen = res.is_image_generating || true; // Or check your active session flag
                            var isImgGen = true;
                            appendMessage('loading', '', [], false, true, isImgGen);
                        }
                        pollImageProgress(currentChatId);
                    } else if(res.chat_status === 'thinking') {
                        if (!$('loading-bubble')) {
                            appendMessage('loading', '', [], false, true, false);
                        }
                    }

                    if(sTB) {
                        scrollToBottom(chatBox,true);
                    } else {
                        setTimeout(function() { chatBox.scrollTop=curscroll; },100);
                    }
                    loadChatList();
                }
            });
        }

        var kATT = 3;
        var kATC = 0;
        var kAT;

        var totalQueue=0;
        function keepAlive() {
            clearTimeout(kAT);
        
            if (kATC === 0) {
                // Run background history sync / queue state check
                loadChatList();

                // Fetch queue status to calculate position and update indicators
                sendAjax('check_queue_status', { chat_id: currentChatId }, function(res) {
                    if (res.status === 'success' && res.queue) {
                        let pendingJobs = res.queue.filter(j => j.status === 'pending' || j.status === 'processing');
                        totalQueue=pendingJobs.length;

                        if (currentChatId) {
                            let myIndex = pendingJobs.findIndex(j => j.chat_id === currentChatId);
                            let loader = $('loading-bubble');
                            if (loader) {
                                if (myIndex > 0) {
                                    loader.querySelector('.typing-dots').innerText = "Queued position: #" + myIndex;
                                }
                            }

                            // Check if current chat job completed in background
                            let curChatCompleted = res.queue.find(j => j.chat_id === currentChatId && j.status === 'completed');
                            if (curChatCompleted && loader) {
                                switchChat(currentChatId, true); // Refresh chat to pull completed content
                            } else if(loader && !primaryActive) {
                                switchChat(currentChatId, true); // Refresh for other device if in feed
                            }
                        }
                    }
                });
        
                kATC = kATT;
            } else {
                kATC--;
            }
        
            kAT = setTimeout(keepAlive, 1000);
        }

        var sTB;
        function scrollToBottom(box,f=false) {
            clearTimeout(sTB);

            let cont=true;
            if(!f) {
                cont=false;
                //
                let chatheight=box.clientHeight;
                let maxscroll=box.scrollHeight-chatheight;
                let curscroll=box.scrollTop;

                //if(curscroll>maxscroll-100 || curscroll==0) {
                if(curscroll>maxscroll-(chatheight*.5)) {
                    cont=true;
                }
            }

            if(cont) {
                sTB=setTimeout(function() {
                    box.scrollTop = box.scrollHeight;
                },100);
            }
        }
        
        function newChat() {
            currentChatId = 'chat-' + Math.floor(Date.now() / 1000);
            $('chat-box').innerHTML = '<div class="message ai"><strong>SYSTEM:</strong><br>New session initialized.</div>';
            $('chat-title-input').value = "New Chat";
            loadChatList();
        }

        function deleteCurrentChat() {
            if (!currentChatId) return;
            if (confirm("Are you sure you want to delete this chat session?")) {
                sendAjax('delete_chat', { chat_id: currentChatId }, function(res) {
                    if (res.status === 'success') {
                        newChat();
                    }
                });
            }
        }

        function updateChatTitle() {
            var newTitle = $('chat-title-input').value.trim();
            if (!currentChatId || !newTitle) return;
            sendAjax('rename_chat', { chat_id: currentChatId, title: newTitle }, function(res) {
                if (res.status === 'success') {
                    loadChatList();
                }
            });
        }

        function insertEmoji(emoji) {
            var input = $('user-input');
            input.value += emoji;
            input.focus();
        }

		function showmore(midx=-1) {
			let ccid = currentChatId;
			
			sendAjax('show_more', { chat_id: currentChatId, message_index: midx }, function(res) {
                if (res.status === 'success') {
                    //alert(res.message);
					let msg=res.message;
					let idx=res.message_index;
					let elm_message=document.getElementsByClassName('message')[idx];
					let elm_msg=elm_message.getElementsByClassName('msg')[0];
					elm_msg.innerHTML = nl2br(msg);
					
                } else {
                    alert("Error: could not find message!");
                }
            });
        }

        var ltime=0;
        var ldtime=0;
        function imgready() {
            ldtime=Date.now()-ltime;

            if(ldtime<1000) {
                scrollToBottom($('chat-box'),true);
            }
        }

        function appendMessage(sender, text, attachment = [], isImage = false, sTB = true, isGeneratingImage = false, msgIndex = null, isUpscaled = false) {
            var chatBox = $('chat-box');
            var msgDiv = document.createElement('div');
            msgDiv.className = "message " + sender;

            // --- START: Feature 4 Truncation Implementation ---
            let displayContent = '';
			let smlnk = '';
            const MAX_MESSAGE_LENGTH = 300; // Define a reasonable limit for readability
            let isTruncated = false;

            if (text.length > MAX_MESSAGE_LENGTH && sender == "user") {
                displayContent = text.substring(0, MAX_MESSAGE_LENGTH) + '...';
				smlnk = "<span class=\"smlnk\"><a href=\"javascript:showmore("+msgIndex+");\" target=\"_self\">Show more</a></span>";
                isTruncated = true;
            }
            // --- END: Feature 4 Truncation Implementation ---

            // Build Action Toolbar, injecting msgIndex safely if available
			var copyBtnHtml = (!isImage)?'<button onclick="copyMessageText(this)" title="Copy text">Copy</button>':'';
			var downloadBtnHtml = (isImage)?'<button onclick="downloadImage(this)" title="Download image">Download</button>':'';
            var deleteBtnHtml = (msgIndex !== null) ? '<button onclick="deleteMessage(\'' + currentChatId + '\', ' + msgIndex + ')" title="Delete message">Del</button>' : '';
            // Add Upscale button only if it's an image and NOT already upscaled or upload?
            //var upscaleBtnHtml = (isImage && !isUpload && !isUpscaled && msgIndex !== null)
            var upscaleBtnHtml = (isImage && !isUpscaled && msgIndex !== null)
                ? '<button onclick="triggerUpscale(\'' + currentChatId + '\', ' + msgIndex + ', this)" title="Upscale 2x with SD">Upscale</button>'
                : '';

            var actionsHtml = '<div class="message-actions">' +
							  copyBtnHtml +
							  downloadBtnHtml +
                              upscaleBtnHtml +
                              deleteBtnHtml +
                              '</div>';

            if (isImage) {
                var fullSrc = text;
                var previewSrc = text.replace('/generated/', '/generated_prev/').replace('.png', '.jpg');
                var labelPrefix = isUpscaled ? 'Imagine Upscaled:' : 'Imagine:';
                
                msgDiv.innerHTML = actionsHtml + '<strong>' + labelPrefix + '</strong><br>' +
                                   '<img class="generated-art" src="' + previewSrc + '" ' +
                                   'onerror="this.src=\'' + fullSrc + '\'" ' +
                                   'onclick="openLightbox(this.getAttribute(\'data-src\'));" ' +
                                   'onload="imgready();" ' +
								   'data-src="' + fullSrc + '" ' +
                                   'title="Click to view full-res PNG" ' +
                                   'style="max-width:512px; width:100%; height:auto; cursor:pointer; border:1px solid #444; border-radius:4px; margin-top:6px; display:block;">';
            } else if (sender === 'loading') {
                msgDiv.className = "message ai";
                msgDiv.id = "loading-bubble";
                
                // Check if this loading bubble is for an image generation or standard AI chat text
                if (isGeneratingImage) {
                    msgDiv.innerHTML = '<strong>Imagine:</strong><br><div class="typing-dots">Initializing render...</div>';
                    pollImageProgress(currentChatId);
                } else {
                    msgDiv.innerHTML = '<strong>AI:</strong><br><div class="typing-dots"><span>.</span><span>.</span><span>.</span></div>';
                }
            } else {				
				let att='';
				if(attachment.length>0) {
					att="<span class=\"attachment\">[Attachment:";
					for(let i=0;i<attachment.length;i++) {
						if(attachment[i]) {
							att+=" "+attachment[i];
						}
					}
					att+="]</span><br>";
				}
				
                if(isTruncated) {
                    msgDiv.innerHTML = actionsHtml + '<strong>' + (sender === 'user' ? 'You:' : 'AI:') + '</strong><br>'+att+'<span class="msg">' + nl2br(displayContent) + smlnk + '</span>';
                } else {
                    if(sender === 'user') {
                        msgDiv.innerHTML = actionsHtml + '<strong>' + 'You:' + '</strong><br>'+att+'<span class="msg">' + nl2br(text) + '</span>';
                    } else {
                        msgDiv.innerHTML = actionsHtml + '<strong>' + 'AI:' + '</strong><br>'+att+'<span class="msg">' + formatText(text) + '</span>';
                    }
                }
            }
        
            chatBox.appendChild(msgDiv);

            // Only highlight code blocks that haven't been processed yet
            document.querySelectorAll('pre code:not([data-highlighted])').forEach((block) => {
                hljs.highlightElement(block);
            });

            // Inside appendMessage, right after code highlighting:
            document.querySelectorAll('pre:not([data-copy-wired])').forEach((pre) => {
                pre.setAttribute('data-copy-wired', 'true');
            
                var copyBtn = document.createElement('button');
                copyBtn.innerText = 'Copy';
                copyBtn.className = 'code-copy-btn';
                copyBtn.title = 'Copy code snippet';
            
                copyBtn.onclick = function() {
                    var codeEl = pre.querySelector('code');
                    var textToCopy = codeEl ? codeEl.innerText : pre.innerText;
            
                    // Use our robust fallback/clipboard handler
                    if (navigator.clipboard && navigator.clipboard.writeText) {
                        navigator.clipboard.writeText(textToCopy).then(function() {
                            showCopiedFeedback(copyBtn);
                        }).catch(function() {
                            fallbackCopyText(textToCopy, copyBtn);
                        });
                    } else {
                        fallbackCopyText(textToCopy, copyBtn);
                    }
                };
            
                pre.appendChild(copyBtn);
            });
            
            if (sTB) scrollToBottom(chatBox,true);
        }

        function removeLoading() {
            var loader = $('loading-bubble');
            if (loader) loader.remove();
        }

        // fancy format ai standard
		function formatText(text,ebr=true) {
			if (!text) return '';

			// 1. Extract and protect fenced code blocks first
			var codeBlocks = [];
			var protectedText = text.replace(/```([\s\S]*?)```/g, function(match) {
				var placeholder = '___CODE_BLOCK_' + codeBlocks.length + '___';
				codeBlocks.push(match);
				return placeholder;
			});

			// Escape HTML on surrounding text
			var safe = protectedText
				.replace(/&/g, "&amp;")
				.replace(/</g, "&lt;")
				.replace(/>/g, "&gt;");

			// 2. Extract and protect inline code (`code`) BEFORE bold/italic
			// Restricts to single lines so an unclosed backtick never bleeds across paragraphs
			var inlineCodes = [];
			safe = safe.replace(/`([^`\n]+)`/g, function(match, code) {
				var placeholder = '___INLINE_CODE_' + inlineCodes.length + '___';
				inlineCodes.push('<code>' + code + '</code>');
				return placeholder;
			});

			// Headers (#, ##, ###, ####)
			safe = safe.replace(/^#### (.*$)/gim, '<h4>\$1</h4>');
			safe = safe.replace(/^### (.*$)/gim, '<h3>\$1</h3>');
			safe = safe.replace(/^## (.*$)/gim, '<h2>\$1</h2>');
			safe = safe.replace(/^# (.*$)/gim, '<h1>\$1</h1>');

			// Bold & Italic (line-restricted so unclosed * never bleeds down the page)
			safe = safe.replace(/\*\*([^\*\n]+)\*\*/g, '<strong>\$1</strong>');
			safe = safe.replace(/\*([^\*\n]+)\*/g, '<em>\$1</em>');

			// Catch floating state labels like "UI:*" or "Action:*"
			safe = safe.replace(/(UI|Action|Status):\*/gi, '<em>\$1:</em>');

			// Line breaks, bullet lists, numbered lists, and horizontal rules
			var lines = safe.split('\n');
			var inUl = false;
			var inOl = false;
			var result = '';

			lines.forEach(function(line) {
				var trimmed = line.trim();

				// Horizontal rule (remove the backslash before $)
				if (/^(-{3,}|\*{3,}|_{3,})$/.test(trimmed)) {
					if (inUl) { inUl = false; result += '</ul>'; }
					if (inOl) { inOl = false; result += '</ol>'; }
					result += '<br>'; // optional: '<hr>' or '<br'>
					return;
				}

				// Skip empty or dangling bullet dashes (- )
				if (trimmed === '-' || trimmed === '*') return;

				// Bullet list
				if (trimmed.startsWith('- ') || trimmed.startsWith('* ')) {
					if (inOl) { inOl = false; result += '</ol>'; }
					if (!inUl) { inUl = true; result += '<ul>'; }
					result += '<li>' + trimmed.substring(2) + '</li>';
					return;
				}

				// Numbered list
				if (/^\d+\.\s/.test(trimmed)) {
					if (inUl) { inUl = false; result += '</ul>'; }
					if (!inOl) { inOl = true; result += '<ol>'; }
					result += '<li>' + trimmed.replace(/^\d+\.\s*/, '') + '</li>';
					return;
				}

				// Close any open lists before normal lines or headings
				if (inUl) { inUl = false; result += '</ul>'; }
				if (inOl) { inOl = false; result += '</ol>'; }

				// Block elements (headings or code block placeholders) don't need trailing <br>
				if (/^<h[1-4]>/.test(trimmed) || /^___CODE_BLOCK_\d+___\$/.test(trimmed)) {
					result += trimmed;
				} else if (trimmed === '') {
					// Collapse multiple blank lines into a single break
					if (!result.endsWith('<br>') && result !== '' && ebr) {
						result += '<br>';
					}
				} else if(ebr) {
					result += line + '<br>';
				} else {
					result += line;
				}
			});

			if (inUl) result += '</ul>';
			if (inOl) result += '</ol>';

			// Clean up trailing <br> at the very end
			result = result.replace(/(<br>)+\$/, '');

			// 3. Restore inline code safely
			inlineCodes.forEach(function(html, index) {
				result = result.replace('___INLINE_CODE_' + index + '___', function() {
					return html;
				});
			});

			// 4. Restore fenced code blocks safely
			codeBlocks.forEach(function(block, index) {
				var innerMatch = block.match(/```([a-zA-Z0-9_-]*)\n([\s\S]*?)```/);
				var lang = innerMatch && innerMatch[1] ? innerMatch[1] : '';
				var codeContent = innerMatch ? innerMatch[2] : block.replace(/```/g, '').trim();

				var safeCode = codeContent
					.replace(/&/g, "&amp;")
					.replace(/</g, "&lt;")
					.replace(/>/g, "&gt;");

				var replacement = '<pre><code' + (lang ? ' class="language-' + lang + '"' : '') + '>' + safeCode + '</code></pre>';
				result = result.replace('___CODE_BLOCK_' + index + '___', function() {
					return replacement;
				});
			});

			return result;
		}

        let selectedFileObjects = [];

        function submitQuery() {
            var loader = $('loading-bubble');
            if(loader || primaryActive) {
                alert("This chat already has primary task in progress!");
                return;
            }
            var input = $('user-input');
            var val = input.value.trim();
            if (!val && selectedFileObjects.length==0) return; // Allow sending file with or without text prompt
            if (!currentChatId) currentChatId = 'chat-' + Math.floor(Date.now() / 1000);
            
            // Display preview text locally
            var displayVal = "";
			displayVal = nl2br(val);
			
			const attachment = selectedFileObjects.map(f => f.name);
            appendMessage('user', displayVal, attachment, false, true);
            input.value = '';
            
            var textModel = $('text-model-select').value;
			var visionModel = $('vision-model-select').value;
            
            // Check if it's an image generation command
            if (val.startsWith('/imagine ')) {
                var imgPrompt = val.replace('/imagine ', '');

                var resVal = $('sd-resolution').value.split('x');
                var baseWidth = parseInt(resVal[0]);
                var baseHeight = parseInt(resVal[1]);
                var is2x = $('sd-multiplier-2x').checked;

                var width = is2x ? baseWidth * 2 : baseWidth;
                var height = is2x ? baseHeight * 2 : baseHeight;
                var steps = parseInt($('sd-steps').value);

                // Pack into FormData to support optional image attachments for img2img
                var formData = new FormData();
                formData.append('action', 'generate_image');
                formData.append('prompt', imgPrompt);
                formData.append('chat_id', currentChatId);
                formData.append('width', width);
                formData.append('height', height);
                formData.append('steps', steps);

                if (selectedFileObjects.length > 0) {
                    selectedFileObjects.forEach(function(file) {
                        formData.append('attachment[]', file);
                    });
                }

                var xhr = new XMLHttpRequest();
                xhr.open('POST', 'ajax.php', true);
                xhr.onload = function() {
                    if (activePollers[currentChatId]) {
                        clearTimeout(activePollers[currentChatId].timer);
                        delete activePollers[currentChatId];
                    }
                    if (xhr.status === 200) {
                        try {
                            var res = JSON.parse(xhr.responseText);
                            if (res.status === 'success') {
                                appendMessage('loading', '', [], false, true, true);
								
								primaryActive=true;
                            } else {
                                removeLoading();
                                appendMessage('system-error', 'Error generating image: ' + (res.message || 'Unknown error'), [], false, true);
                                loadChatList();
                            }
                        } catch(e) {
                            removeLoading();
                            appendMessage('system-error', 'Invalid JSON response from server.', [], false, true);
                        }
                    } else {
                        removeLoading();
                        appendMessage('system-error', 'HTTP Error: ' + xhr.status, [], false, true);
                    }
                };
                xhr.send(formData);

                // Start polling immediately to catch live frames & %
                pollImageProgress(currentChatId);

                // Update sidebar generator badge indicator
                addChatListLoading();
            } else {
                // Standard chat or multimodal file analysis request using FormData
                var formData = new FormData();
                formData.append('action', 'chat');
                formData.append('chat_id', currentChatId);
                formData.append('prompt', val);
                formData.append('model', textModel);
				formData.append('vmodel', visionModel);
				
                if (selectedFileObjects.length > 0) {
                    selectedFileObjects.forEach(function(file) {
                        formData.append('attachment[]', file);
                    });
                }
                
                var xhr = new XMLHttpRequest();
                xhr.open('POST', 'ajax.php', true);
                xhr.onload = function() {
                    removeLoading();
                    if (xhr.status === 200) {
                        try {
							//console.log(xhr.responseText);
                            var res = JSON.parse(xhr.responseText);
							
                            if (res.status === 'success') {
                                appendMessage('loading', '', [], false, true);
								
								primaryActive=true;
                            } else {
                                appendMessage('system-error', 'Backend Error: ' + res.message, [], false, true);
                            }
                        } catch(e) {
                            appendMessage('system-error', 'Invalid JSON response from server.', [], false, true);
                        }
                    } else {
                        appendMessage('system-error', 'HTTP Error: ' + xhr.status, [], false, true);
                    }
                };
                xhr.send(formData);

                addChatListLoading();
            }
            clearAttachedFile(); // Clear file input & preview UI
        }

        function addChatListLoading() {
            // loadChatList clears and resets if generating
            let div = $('chat-list').getElementsByClassName("active")[0];
            if (div) {
                let titleText = div.getAttribute('data-name') || div.innerText;
                div.innerHTML = titleText + '<span class="chat-item-generating-dots">...</span>';
            }
        }

        function copyMessageText(btn) {
            var msgDiv = btn.closest('.message');
            var clone = msgDiv.cloneNode(true);
            var actions = clone.querySelector('.message-actions');
            if (actions) actions.remove();
        
            //var textToCopy = clone.innerText.replace(/^(You:|AI:|Imagine:)\s*/, '').trim();
			var textToCopy = clone.getElementsByClassName('msg')[0].innerText.replace(/^(You:|AI:|Imagine:)\s*/, '').trim();
			
            // Modern clipboard API with HTTP LAN fallback
            if (navigator.clipboard && navigator.clipboard.writeText) {
                navigator.clipboard.writeText(textToCopy).then(function() {
                    showCopiedFeedback(btn);
                }).catch(function() {
                    fallbackCopyText(textToCopy, btn);
                });
            } else {
                fallbackCopyText(textToCopy, btn);
            }
        }
		
		async function downloadImage(btn) {
			var msgDiv = btn.closest('.message');
            var imageUrl=msgDiv.getElementsByClassName('generated-art')[0].getAttribute('data-src');
			var fileName = imageUrl.substr(imageUrl.lastIndexOf('/')+1);
			
			try {
				// 1. Fetch the image data
				const response = await fetch(imageUrl);
				const blob = await response.blob();

				// 2. Create a temporary local URL for the Blob
				const blobUrl = URL.createObjectURL(blob);

				// 3. Create a hidden <a> tag and trigger the download
				const link = document.createElement('a');
				link.href = blobUrl;
				link.download = fileName;
				document.body.appendChild(link);
				link.click();

				// 4. Clean up the DOM and memory
				document.body.removeChild(link);
				URL.revokeObjectURL(blobUrl);
			  } catch (error) {
				console.error('Failed to download image:', error);
			  }
		}
        
        function fallbackCopyText(text, btn) {
            var textArea = document.createElement("textarea");
            textArea.value = text;
            textArea.style.position = "fixed";  // Avoid scrolling to bottom
            textArea.style.opacity = "0";
            document.body.appendChild(textArea);
            textArea.focus();
            textArea.select();
        
            try {
                var successful = document.execCommand('copy');
                if (successful) {
                    showCopiedFeedback(btn);
                } else {
                    alert('Failed to copy text.');
                }
            } catch (err) {
                alert('Browser does not support copying from unsecure LAN contexts.');
            }
        
            document.body.removeChild(textArea);
        }
        
        function showCopiedFeedback(btn) {
            var originalText = btn.innerText;
            btn.innerText = 'Copied!';
            setTimeout(function() { btn.innerText = originalText; }, 1500);
        }
        
        function deleteMessage(chatId, index) {
            if (!confirm("Delete this message?")) return;
            sendAjax('delete_message', { chat_id: chatId, message_index: index }, function(res) {
                if (res.status === 'success') {
                    switchChat(chatId,false); // Reload chat ledger view cleanly
                } else {
                    alert("Error deleting message: " + res.message);
                }
            });
        }

        function triggerUpscaleDebug() {
            sendAjax('upscale_debug', { }, function(res) {
                alert('output should be in ./upscalers_debug.json');
            });
        }

        function triggerUpscale(chatId, messageIndex, btnEl) {
            if (btnEl.classList.contains('loading')) return;
            //--if (!confirm("Upscale this image 2x using Stable Diffusion?")) return;

            btnEl.classList.add('loading');
            var originalText = btnEl.innerText;
            btnEl.innerText = 'Upscaling...';
            btnEl.disabled = true;

            sendAjax('upscale_image', { chat_id: chatId, message_index: messageIndex }, function(res) {
                if (res.status === 'success') {
                    // Keep button locked as Upscaling until keepAlive/load_chat refreshes it
                } else {
                    alert("Upscale failed: " + (res.message || "Unknown error"));
                    btnEl.classList.remove('loading');
                    btnEl.innerText = originalText;
                    btnEl.disabled = false;
                }
            });
        }

        function handleMultipleFileSelected(input) {
            if (input.files && input.files.length > 0) {
                // Store the array of File objects
                selectedFileObjects = Array.from(input.files);

                // Update the visible preview bar
                $('fileNameDisplay').innerText = selectedFileObjects.map(f => f.name).join(', ');
                $('filePreviewContainer').style.display = 'flex';
            } else {
                $('filePreviewContainer').style.display = 'none';
            }
        }
        
        function clearAttachedFile() {
            selectedFileObjects = [];
            $('chatFileInput').value = '';
            $('filePreviewContainer').style.display = 'none';
        }

        // Keep track of active XHR requests per chat to allow cancellation
        var activeXhrRequests = {};
        
        function cancelGeneration(chatId) {
            if (activeXhrRequests[chatId]) {
                activeXhrRequests[chatId].abort();
                delete activeXhrRequests[chatId];
            }
            if (activePollers[chatId]) {
                clearTimeout(activePollers[chatId].timer);
                delete activePollers[chatId];
            }
            removeLoading();
            appendMessage('system-error', 'Generation cancelled by user.', [], false, true);
            
            // Notify backend to clear any hanging generating state
            sendAjax('cancel_chat_state', { chat_id: chatId });
        }

        function triggerImagineMode() {
            var input = $('user-input');
            if (!input.value.startsWith('/imagine ')) {
                input.value = '/imagine ' + input.value;
            }
            input.focus();
        }

        var pollActive=false;
        function pollImageProgress(chatId) {
            //console.log("poll pull"+Date.now());
            if (activePollers[chatId] && activePollers[chatId]['timer']) {
                clearTimeout(activePollers[chatId]['timer']);
            }
        
            var pollFn = function() {
                var xhr = new XMLHttpRequest();
                xhr.open("GET", "http://" + BACKEND_IP + ":" + BACKEND_IMG_PORT + "/sdapi/v1/progress?skip_current_image=false", true);
                xhr.onreadystatechange = function() {
                    if (xhr.readyState === 4) {
                        if (xhr.status === 200) {
                            try {
                                var res = JSON.parse(xhr.responseText);
                                var pct = Math.round(res.progress * 100);
        
                                if (!activePollers[chatId]) return;
                                if (pct < activePollers[chatId]['lpct'] && activePollers[chatId]['lpct'] > 50) {
                                    pct = 100;
                                }
                                activePollers[chatId]['lpct'] = pct;
        
                                // Force live update if we are looking at this exact chat
                                if (currentChatId === chatId) {
                                    requestAnimationFrame(function() {
                                        //console.log("poll inner"+Date.now());
                                        var loader = $('loading-bubble');
                                        if (loader && pct<100) {
                                            var progressText = pct > 1 ? ('Generating... ' + pct + '%') : 'Rendering image...';
                                            var liveImgHtml = '';
											let sTB=false;
                                            if (res.current_image) {
												if(!activePollers[chatId]['prev']) {
													sTB=true;
													activePollers[chatId]['prev']=true;
												}
                                                liveImgHtml = '<img src="data:image/png;base64,' + res.current_image + '" onclick="openLightbox(this.src)" style="max-width:256px; border:1px solid #444; margin-top:6px; display:block; cursor:pointer;">';
                                            } else if(!activePollers[chatId]['init']) {
												sTB=true;
												activePollers[chatId]['init']=true;
											}

                                            loader.innerHTML = '<strong>Imagine:</strong><br><div class="typing-dots">' + progressText + '</div>' +
                                                               liveImgHtml;// + // cancel not been updated since queue system
                                                               //'<button onclick="cancelGeneration(\'' + chatId + '\')" style="margin-top:6px; font-size:8pt; background:#311; border-color:#833; padding:2px 6px;">Cancel</button>';

                                            var chatBox = $('chat-box');
                                            if (sTB && chatBox) scrollToBottom(chatBox);
                                        }
                                    });
                                }

                               // Completion check based on actual Forge progress values
                                if (pct >= 100 || res.progress >= 0.99 || (res.state && (res.state.interrupted || res.state.skipped)) || (pct >= 50 && !primaryActive)) {
                                    if (activePollers[chatId] && activePollers[chatId]['timer']) {
                                        clearTimeout(activePollers[chatId]['timer']);
                                    }
                                    delete activePollers[chatId];
                                } else {
                                    activePollers[chatId]['timer'] = setTimeout(pollFn, 600); // Poll every 600ms for smooth live updates
                                }
                            } catch(e) {
                                activePollers[chatId]['timer'] = setTimeout(pollFn, 1000);
                            }
                        } else {
                            activePollers[chatId]['timer'] = setTimeout(pollFn, 1500);
                        }
                    }
                };
                xhr.send();
            };
        
            if (!activePollers[chatId]) {
                activePollers[chatId] = { lpct: 0, timer: null, prev: false, init: false };
                pollActive=false;
            }
            activePollers[chatId]['timer'] = setTimeout(pollFn, 200);
        }

        var cachedEmojiData = [];

        function initEmojiPicker() {
            var grid = $('emoji-grid');
            if (cachedEmojiData.length > 0) {
                renderEmojiGrid(cachedEmojiData);
                return;
            }
        
            fetch('./emoji.json')
                .then(response => response.json())
                .then(data => {
                    cachedEmojiData = data;
                    renderEmojiGrid(cachedEmojiData);
                })
                .catch(err => {
                    console.error("Failed to load local ./emoji.json", err);
                });
        }
        
        function renderEmojiGrid(items) {
            var grid = $('emoji-grid');
            grid.innerHTML = '';
            items.forEach(item => {
                var span = document.createElement('span');
                span.className = 'emoji-cell';
                var charStr = item.char || item.emoji || '';
                span.innerText = charStr;
                span.title = item.name || '';
                span.onclick = function() {
                    insertEmoji(charStr);
                    $('emoji-modal').classList.remove('open');
                };
                grid.appendChild(span);
            });
        }
        
        function filterEmojis(query) {
            var q = query.toLowerCase().trim();
            if (!q) {
                renderEmojiGrid(cachedEmojiData);
                return;
            }
            var filtered = cachedEmojiData.filter(item => {
                var name = (item.name || '').toLowerCase();
                var cat = (item.category || '').toLowerCase();
                var group = (item.group || '').toLowerCase();
                var sub = (item.subgroup || '').toLowerCase();
                var codes = (item.codes || '').toLowerCase();
                return name.includes(q) || cat.includes(q) || group.includes(q) || sub.includes(q) || codes.includes(q);
            });
            renderEmojiGrid(filtered);
        }

        var currentLightboxSrc = '';

        function openLightbox(imgSrc) {
            currentLightboxSrc = imgSrc;
            var lbImg = $('lightbox-img');
            lbImg.src = imgSrc;
            
            // Create an invisible image object to read native natural width/height
            var tempImg = new Image();
            tempImg.onload = function() {
                scaleLightboxImage(tempImg.naturalWidth, tempImg.naturalHeight);
                $('image-lightbox').classList.add('open');
            };
            tempImg.src = imgSrc;
        }
        
        function scaleLightboxImage(natWidth, natHeight) {
            var lbImg = $('lightbox-img');
            
            // Get available window viewport dimensions with a 60px total margin buffer
            var SCR_W = window.innerWidth - 60;
            var SCR_H = window.innerHeight - 60;
            
            var imgAspect = natWidth / natHeight;
            var scrAspect = SCR_W / SCR_H;
            
            var finalW, finalH;
            
            if (imgAspect > scrAspect) {
                // Image is wider relative to screen
                finalW = SCR_W;
                finalH = SCR_W / imgAspect;
            } else {
                // Image is taller relative to screen
                finalH = SCR_H;
                finalW = SCR_H * imgAspect;
            }
            
            // Apply calculated dimensions to force scaling up or down to fit viewport
            lbImg.style.width = finalW + 'px';
            lbImg.style.height = finalH + 'px';
        }
        
        // Window resize handler with debounce timeout check
        var resizeTimer;
        window.addEventListener('resize', function() {
            if (!$('image-lightbox').classList.contains('open')) return;
            clearTimeout(resizeTimer);
            resizeTimer = setTimeout(function() {
                if (currentLightboxSrc) {
                    var tempImg = new Image();
                    tempImg.onload = function() {
                        scaleLightboxImage(tempImg.naturalWidth, tempImg.naturalHeight);
                    };
                    tempImg.src = currentLightboxSrc;
                }
            }, 100);
        });
        
        function closeLightbox(e) {
            if (e.target.id === 'image-lightbox') {
                $('image-lightbox').classList.remove('open');
                currentLightboxSrc = '';
            }
        }
        
        function openInNewTab(e) {
            e.stopPropagation();
            window.open($('lightbox-img').src, '_blank');
        }

        // Clear search input and reset filter when closing modal
        function closeEmojiModal() {
            $('emoji-modal').classList.remove('open');
            var searchInput = $('emoji-search');
            if (searchInput) {
                searchInput.value = '';
                renderEmojiGrid(cachedEmojiData); // Reset grid back to full set
            }
        }

        function enhancePrompt() {
            if(totalQueue>0) {
                alert("Heavy load: enhance is currently not available!");
                return;
            }
            var input = $('user-input');
            var rawPrompt = input.value.trim();
            if (!rawPrompt) {
                rawPrompt = "a cinematic masterpiece landscape scene"; // Fallback if empty
            }
        
            // Strip out /imagine prefix temporarily if present so the LLM focuses purely on descriptive tags
            var cleanPrompt = rawPrompt.replace(/^\/imagine\s*/i, '');
            input.value = "Enhancing prompt...";
        
            var textModel = $('text-model-select') ? $('text-model-select').value : 'gemma4-uncensored';

            // Construct request to local Ollama instance
            var payload = {
                model: textModel,
                prompt: "Rewrite the following short idea into a rich, highly detailed Stable Diffusion prompt with descriptive visual tokens, lighting details, and art style tags. Output ONLY the final prompt text without conversational filler: " + cleanPrompt,
                stream: false
            };
        
            var xhr = new XMLHttpRequest();
            xhr.open("POST", "http://" + BACKEND_IP + ":" + BACKEND_AI_PORT + "/api/generate", true);
            xhr.setRequestHeader("Content-Type", "application/json");
            xhr.onreadystatechange = function() {
                if (xhr.readyState === 4) {
                    if (xhr.status === 200) {
                        try {
                            var res = JSON.parse(xhr.responseText);
                            var enhancedTextFull = res.response ? res.response.trim() : cleanPrompt;
                            // Restore /imagine prefix if it was there
                            var enhancedText=enhancedTextFull.substr(enhancedTextFull.lastIndexOf('<channel|>')+10);
                            input.value = '/imagine ' + enhancedText;
                        } catch(e) {
                            input.value = '/imagine ' + cleanPrompt;
                        }
                    } else {
                        input.value = '/imagine ' + cleanPrompt;
                    }
                    input.focus();
                }
            };
            xhr.send(JSON.stringify(payload));
        }
        
        // Close modal when clicking outside
        window.onclick = function(event) {
            var modal = $('emoji-modal');
            if (!modal.contains(event.target) && event.target.id !== 'toggle-emoji-picker') {
                closeEmojiModal();
            }
        };

        window.onload = function() {
            currentChatId = 'chat-' + Math.floor(Date.now() / 1000);
            keepAlive();
            
            $('toggle-sidebar').onclick = toggleSidebar;
            $('btn-tab-chats').onclick = function() { switchTab('chats'); };
            $('btn-tab-settings').onclick = function() { switchTab('settings'); };$('new-chat-btn').onclick = newChat;
            $('delete-chat-btn').onclick = deleteCurrentChat;
            $('send-btn').onclick = submitQuery;
            $('update-model-btn').onclick = updateSdModel;
            $('chat-title-input').onchange = updateChatTitle;
            
            $('user-input').onkeydown = function(e) {
                if (e.keyCode === 13 && !e.shiftKey) {
                    e.preventDefault();
                    submitQuery();
                }
            };

            $('toggle-emoji-picker').onclick = function(e) {
                e.stopPropagation();
                var modal = $('emoji-modal');
                //var btn = $('toggle-emoji-picker');
                var btn2 = $('imagine-button');
                
                if (!modal.classList.contains('open')) {
                    var rect = btn2.getBoundingClientRect();
                    // Position modal left edge aligned with button left, or offset it
                    modal.style.left = ((rect.left + rect.width) - 320) + 'px';
                    modal.style.bottom = (window.innerHeight - rect.top + 8) + 'px';
                    modal.classList.add('open');
                    initEmojiPicker();
                } else {
                    closeEmojiModal();
                }
            };
        };
    </script>
</head>
<body>
    <div class="app-layout">
        <!-- Sidebar -->
        <div id="sidebar">
            <div class="sidebar-tabs">
                <button id="btn-tab-chats" class="tab-btn active">Chats</button>
                <button id="btn-tab-settings" class="tab-btn">Settings</button>
            </div>
            
            <!-- Chats Tab Pane -->
            <div id="pane-chats" class="tab-pane active">
                <div class="sidebar-header">
                    <span style="font-size:9pt; color:#888;">HISTORY</span>
                    <button id="new-chat-btn" style="padding: 2px 8px; font-size: 9pt;">+ New</button>
                </div>
                <div id="chat-list" class="chat-list"></div>
            </div>
            
            <!-- Settings Tab Pane -->
            <div id="pane-settings" class="tab-pane">
                <div class="setting-group">
                    <label>Text Model (Ollama):</label>
                    <select id="text-model-select">
                        <option value="gemma4-uncensored">gemma4-uncensored</option>
                        <!--<option value="qwen3">Qwen 2.5 (7B Instruct)</option>
                        <option value="twil-8b">TwIL-8B Reason Model</option>
                        <option value="liquid-1.7b">Liquid-1.7B Local Agent</option>-->
                    </select>
					<br />
					<label>Vision Model (Ollama):</label>
                    <select id="vision-model-select">
                        <option value="qwen2.5-vision">qwen2.5-vision</option>
                        <!--<option value="llama3.2-vision">llama3.2-vision</option>-->
                    </select>
                </div>
                <div class="setting-group">
                    <label>Active SD Checkpoint:</label>
                    <select id="sd-model-select"></select>
                    <button id="update-model-btn" style="margin-top: 5px; font-size: 9pt;">Apply SD Model</button>
                </div>
                <div class="setting-group" style="margin-top: 15px;">
                    <label>Image Resolution:</label>
                    <select id="sd-resolution" style="width:100%; background:#1e1e24; color:#fff; border:1px solid #444; padding:4px; margin-bottom:6px;">
                        <option value="512x512" selected>512 x 512 (Standard)</option>
                        <option value="768x512">768 x 512 (Landscape)</option>
                        <option value="512x768">512 x 768 (Portrait)</option>
                        <option value="768x768">768 x 768 (Square Large)</option>
                    </select>
                    
                    <!-- 2x Multiplier Toggle -->
                    <label style="font-size: 8.5pt; color: #aaa; display: flex; align-items: center; cursor: pointer; margin-bottom: 8px;">
                        <input type="checkbox" id="sd-multiplier-2x" style="margin-right: 6px;"> 2x Scale Multiplier (Double dimensions)
                    </label>
                
                    <label>Generation Steps:</label>
                    <select id="sd-steps" style="width:100%; background:#1e1e24; color:#fff; border:1px solid #444; padding:4px;">
                        <option value="20" selected>20 Steps (Fast)</option>
                        <option value="30">30 Steps (Balanced)</option>
                        <option value="50">50 Steps (Detailed)</option>
                    </select>
                </div>
                
                <div class="setting-group" style="margin-top: 15px;">
                    <label>Image Generation Help:</label>
                    <div class="setting-help">Type <code>/imagine [prompt]</code> directly in the chat box to trigger Stable Diffusion image generation.</div>
                </div>
            </div>
        </div>
        
        <!-- Main Pane -->
        <div class="main-pane">
            <div class="top-bar">
                <button id="toggle-sidebar">☰</button>
                <input type="text" id="chat-title-input" value="New Chat" style="width: 250px;" title="Click to rename list title">
                <button id="delete-chat-btn" style="background: #502; border-color: #834; margin-left: auto;">Delete</button>
            </div>
            
            <div id="chat-box" class="chat-box">
                <div class="message ai">
                    <strong>SYSTEM:</strong><br>Terminal initialized. Ready.
                </div>
            </div>
            
            <!-- Vibe & Input Container -->
            <div class="vibe-bar">
                <span style="font-size:8.5pt; color:#666;">VIBE:</span>
                <button class="emoji-btn" onclick="insertEmoji(' x)')"> x)</button>
                <button class="emoji-btn" onclick="insertEmoji(' ✨')"> ✨</button>
                <button class="emoji-btn" onclick="insertEmoji(' 🚀')"> 🚀</button>
                <button class="emoji-btn" onclick="insertEmoji(' 💡')"> 💡</button>
                <button class="emoji-btn" id="toggle-emoji-picker" style="background:#25262c; border-color:#EA75BC;">😊 All</button>
                <button class="emoji-btn" id="imagine-button" onclick="triggerImagineMode()" style="background:#25262c; border-color:#4a90e2;" title="Quick Image Generation Mode">🖼️ Img</button>
                
                <!-- New Prompt Enhancer Button -->
                <button class="emoji-btn" onclick="enhancePrompt()" style="background:#25262c; border-color:#9b59b6;" title="Expand prompt into detailed descriptive tags using local LLM">🪄 Enhance</button>

                <!-- Hidden file input + Styled Label -->
                <input type="file" id="chatFileInput" multiple style="display: none;" onchange="handleMultipleFileSelected(this)">
                <label for="chatFileInput" class="file-upload-lbl" style="margin-left: auto;" title="Upload image or text file" ontouchup="handleMultipleFileSelected($('chatFileInput'))">📂 Upload File (+)</label>
            </div>

            <!-- Dynamic File Preview Bar (Place just above or inside your prompt area) -->
            <div id="filePreviewContainer" style="display: none; padding: 4px 8px; background: #1c1d21; border-top: 1px solid #333; font-size: 8.5pt; color: #61afef; align-items: center; gap: 8px;">
                <span>Attached: <strong id="fileNameDisplay"></strong></span>
                <button onclick="clearAttachedFile()" style="background: none; border: none; color: #e06c75; cursor: pointer; font-weight: bold;" title="Remove file">✕ Remove</button>
            </div>
            
            <!-- Popup Emoji Drawer Modal -->
            <div id="emoji-modal">
                <div class="emoji-modal-header" style="flex-direction: column; gap: 6px; align-items: stretch;">
                    <div style="display: flex; justify-content: space-between; align-items: center;">
                        <span style="font-weight:bold;">Select Emoji</span>
                        <button onclick="closeEmojiModal()" style="background:none; border:none; color:#aaa; cursor:pointer; font-weight:bold; font-size:11pt;">&times;</button>
                    </div>
                    <!-- Search filter input -->
                    <input type="text" id="emoji-search" placeholder="Search emojis (e.g. cat, smile, fire)..." oninput="filterEmojis(this.value)" style="background:#000; color:#fff; border:1px solid #444; padding:4px 8px; font-size:9pt; width:100%;">
                </div>
                <div id="emoji-grid" class="emoji-grid"></div>
            </div>
                <div class="input-area">
                    <textarea id="user-input" placeholder="Type your prompt here... (Shift+Enter for new line)"></textarea>
                    <button id="send-btn">SEND</button>
                </div>
            </div>
        </div>
    </div>

<!-- Lightbox Modal Container -->
<div id="image-lightbox" onclick="closeLightbox(event)">
    <img id="lightbox-img" src="" title="Click image to open in new tab" onclick="openInNewTab(event)">
</div>
</body>
</html>

Top
©twily.info 2013 - 2026
twily at twily dot info



3 816 607 visits
... ^ v