<!DOCTYPE html>
<!--
Author: Twily 2015 - 2021
Website: http://twily.info/
-->
<?php
include('./setup.php');
?>
<html lang="en">
<head>
<title>>_ 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, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
// 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, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
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;">×</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