37686-vm/assets/js/main.js
2026-01-22 04:06:30 +00:00

470 lines
19 KiB
JavaScript

document.addEventListener('DOMContentLoaded', () => {
const chatWindow = document.getElementById('chat-window');
const chatInput = document.getElementById('chat-input');
const sendBtn = document.getElementById('send-btn');
const stopBtn = document.getElementById('stop-btn');
const modeItems = document.querySelectorAll('.mode-item');
const currentModeBadge = document.getElementById('current-mode-badge');
const newChatBtn = document.getElementById('new-chat-btn');
const chatHistoryList = document.getElementById('chat-history');
// Settings elements
const creativityRange = document.getElementById('creativity-range');
const creativityVal = document.getElementById('creativity-val');
const limitsToggle = document.getElementById('limits-toggle');
const modelSelect = document.getElementById('model-select');
const themeSwatches = document.querySelectorAll('.theme-swatch');
const saveSettingsBtn = document.getElementById('save-settings-btn');
// Share elements
const shareModal = new bootstrap.Modal(document.getElementById('shareModal'));
const shareUrlInput = document.getElementById('share-url-input');
const copyShareBtn = document.getElementById('copy-share-btn');
const viewSharedLink = document.getElementById('view-shared-link');
let currentMode = 'regular';
let currentChatId = null;
let abortController = null;
// --- Sidebar & Mode Switching ---
modeItems.forEach(item => {
item.addEventListener('click', () => {
if (item.classList.contains('active')) return;
modeItems.forEach(i => i.classList.remove('active'));
item.classList.add('active');
currentMode = item.dataset.mode;
currentModeBadge.textContent = item.querySelector('span').textContent;
startNewChat();
});
});
function startNewChat() {
currentChatId = null;
chatWindow.innerHTML = "`
<div class=\"text-center my-auto\">
<i class=\"bi bi-stars fs-1 text-primary opacity-50\"></i>
<h4 class=\"mt-3\">New ${currentMode} Chat</h4>
<p class=\"text-muted\">How can I help you in this mode?</p>
</div>
`";
document.querySelectorAll('.history-item').forEach(i => i.classList.remove('active'));
}
newChatBtn.addEventListener('click', startNewChat);
// --- History Loading ---
async function loadHistory() {
try {
const resp = await fetch('api/history.php?action=list');
const data = await resp.json();
if (data.success) {
renderHistory(data.chats);
}
} catch (e) {
console.error('Failed to load history:', e);
}
}
function renderHistory(chats) {
if (!chats || chats.length === 0) {
chatHistoryList.innerHTML = '<div class="text-muted small px-3">No recent chats</div>';
return;
}
chatHistoryList.innerHTML = chats.map(chat => `
<div class="history-item mode-item ${currentChatId == chat.id ? 'active' : ''}" data-id="${chat.id}" data-mode="${chat.mode}">
<i class="bi bi-${getModeIcon(chat.mode)}"></i>
<span class="text-truncate">${escapeHtml(chat.title)}</span>
<i class="bi bi-trash delete-chat ms-auto" data-id="${chat.id}" style="font-size: 0.8rem; opacity: 0.5;"></i>
</div>
`).join('');
chatHistoryList.querySelectorAll('.history-item').forEach(item => {
item.addEventListener('click', (e) => {
if (e.target.classList.contains('delete-chat')) {
deleteChat(e.target.dataset.id);
return;
}
loadChat(item.dataset.id);
});
});
}
function getModeIcon(mode) {
switch(mode) {
case 'coding': return 'code-slash';
case 'game': return 'controller';
case 'app': return 'window';
default: return 'chat-left-dots';
}
}
async function loadChat(chatId) {
if (currentChatId == chatId) return;
chatWindow.innerHTML = '<div class="text-center my-auto"><span class="spinner-border text-primary"></span></div>';
try {
const resp = await fetch(`api/history.php?action=messages&chat_id=${chatId}`);
const data = await resp.json();
if (data.success) {
currentChatId = chatId;
currentMode = data.mode;
modeItems.forEach(i => {
if (i.dataset.mode === currentMode) i.classList.add('active');
else i.classList.remove('active');
});
const activeModeItem = Array.from(modeItems).find(i => i.dataset.mode === currentMode);
if (activeModeItem) {
currentModeBadge.textContent = activeModeItem.querySelector('span').textContent;
}
chatWindow.innerHTML = '';
data.messages.forEach(msg => {
appendMessage(msg.role, msg.content, false);
if ((currentMode === 'game' || currentMode === 'app') && msg.role === 'assistant') {
addActionButtons(msg.content);
}
});
document.querySelectorAll('.history-item').forEach(i => {
if (i.dataset.id == chatId) i.classList.add('active');
else i.classList.remove('active');
});
chatWindow.scrollTop = chatWindow.scrollHeight;
}
} catch (e) {
console.error(e);
chatWindow.innerHTML = '<div class="alert alert-danger m-3">Failed to load chat</div>';
}
}
async function deleteChat(chatId) {
if (!confirm('Are you sure you want to delete this chat?')) return;
try {
const resp = await fetch(`api/history.php?action=delete&chat_id=${chatId}`);
const data = await resp.json();
if (data.success) {
if (currentChatId == chatId) startNewChat();
loadHistory();
}
} catch (e) {
console.error(e);
}
}
// --- Chat Logic ---
async function sendMessage() {
const message = chatInput.value.trim();
if (!message) return;
const isNewChat = !currentChatId;
chatInput.value = '';
chatInput.style.height = 'auto';
toggleLoading(true);
appendMessage('user', message);
abortController = new AbortController();
try {
const response = await fetch('api/chat.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: message,
mode: currentMode,
chat_id: currentChatId,
model: modelSelect.value,
creativity: creativityRange.value,
limits_off: limitsToggle.checked ? 1 : 0
}),
signal: abortController.signal
});
const data = await response.json();
if (data.success) {
currentChatId = data.chat_id;
appendMessage('assistant', data.message);
if (currentMode === 'game' || currentMode === 'app') {
addActionButtons(data.message);
}
if (isNewChat) {
loadHistory();
}
} else {
appendMessage('assistant', 'Error: ' + (data.error || 'Unknown error'));
}
} catch (error) {
if (error.name === 'AbortError') {
appendMessage('assistant', '<i class="bi bi-info-circle me-1"></i> Generation stopped by user.');
} else {
appendMessage('assistant', 'Error: ' + error.message);
}
} finally {
toggleLoading(false);
abortController = null;
}
}
function appendMessage(role, text, animate = true) {
if (role === 'system') return;
const emptyState = chatWindow.querySelector('.my-auto');
if (emptyState) emptyState.remove();
const msgDiv = document.createElement('div');
msgDiv.className = `message message-${role} ${animate ? 'animate-fade-in' : ''}`;
msgDiv.innerHTML = formatText(text);
chatWindow.appendChild(msgDiv);
chatWindow.scrollTop = chatWindow.scrollHeight;
msgDiv.querySelectorAll('.copy-btn').forEach(btn => {
btn.addEventListener('click', () => {
const code = btn.closest('.code-block-wrapper').querySelector('code').textContent;
navigator.clipboard.writeText(code).then(() => {
const original = btn.innerHTML;
btn.innerHTML = '<i class="bi bi-check2"></i> Copied!';
setTimeout(() => btn.innerHTML = original, 2000);
});
});
});
}
function formatText(text) {
if (!text) return '';
const codeBlocks = [];
let formatted = text.replace(/```(\w+)?\s*([\s\S]*?)```/g, (match, lang, code) => {
const id = `CODE_BLOCK_${codeBlocks.length}`;
codeBlocks.push({
id,
lang: lang || 'code',
code: code.trim()
});
return id;
});
formatted = escapeHtml(formatted).replace(/\n/g, '<br>');
codeBlocks.forEach(block => {
const html = `
<div class="code-block-wrapper mt-2 mb-3">
<div class="code-header d-flex justify-content-between px-3 py-1 bg-dark text-muted small border-bottom border-secondary rounded-top">
<span>${block.lang}</span>
<span class="copy-btn" style="cursor:pointer"><i class="bi bi-clipboard"></i> Copy</span>
</div>
<pre class="bg-dark text-white p-3 rounded-bottom mb-0 overflow-auto" style="font-size: 0.85rem; border: 1px solid #444; border-top:none;"><code>${escapeHtml(block.code)}</code></pre>
</div>`;
formatted = formatted.replace(block.id, html);
});
return formatted;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function addActionButtons(content) {
const match = content.match(/```(?:html|xml)?\s*([\s\S]*?)```/i);
let codeToLaunch = match ? match[1] : content;
const hasHtmlTags = /<html|<body|<script|<div|<style/i.test(codeToLaunch);
if (hasHtmlTags) {
const btnContainer = document.createElement('div');
btnContainer.className = 'd-flex flex-wrap gap-2 mt-2 action-buttons';
// Launch Button
const launchBtn = document.createElement('button');
launchBtn.className = 'btn btn-sm btn-success d-inline-flex align-items-center gap-2 shadow-sm';
launchBtn.innerHTML = '<i class="bi bi-rocket-takeoff-fill"></i> Launch';
launchBtn.onclick = () => {
let fullCode = codeToLaunch;
if (!fullCode.toLowerCase().includes('<html')) {
fullCode = `<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>AI Generated App</title><link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"><style>body{padding:20px; font-family: sans-serif;}</style></head><body>${codeToLaunch}</body></html>`;
}
const blob = new Blob([fullCode], { type: 'text/html' });
const url = URL.createObjectURL(blob);
window.open(url, '_blank');
};
// Download Button
const downloadBtn = document.createElement('button');
downloadBtn.className = 'btn btn-sm btn-outline-primary d-inline-flex align-items-center gap-2 shadow-sm';
downloadBtn.innerHTML = '<i class="bi bi-download"></i> Download';
downloadBtn.onclick = () => {
let fullCode = codeToLaunch;
if (!fullCode.toLowerCase().includes('<html')) {
fullCode = `<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>AI Generated App</title><link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"><style>body{padding:20px; font-family: sans-serif;}</style></head><body>${codeToLaunch}</body></html>`;
}
const blob = new Blob([fullCode], { type: 'text/html' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `ai-app-${Date.now()}.html`;
a.click();
URL.revokeObjectURL(url);
};
// Share Button
const shareBtn = document.createElement('button');
shareBtn.className = 'btn btn-sm btn-outline-accent d-inline-flex align-items-center gap-2 shadow-sm';
shareBtn.innerHTML = '<i class="bi bi-share"></i> Share';
shareBtn.onclick = async () => {
shareBtn.disabled = true;
shareBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Sharing...';
try {
const resp = await fetch('api/share.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: codeToLaunch })
});
const data = await resp.json();
if (data.success) {
shareUrlInput.value = data.url;
viewSharedLink.href = data.url;
shareModal.show();
} else {
showToast('Failed to share: ' + (data.error || 'Unknown error'), 'danger');
}
} catch (e) {
console.error(e);
showToast('Error sharing application', 'danger');
} finally {
shareBtn.disabled = false;
shareBtn.innerHTML = '<i class="bi bi-share"></i> Share';
}
};
btnContainer.appendChild(launchBtn);
btnContainer.appendChild(downloadBtn);
btnContainer.appendChild(shareBtn);
chatWindow.lastElementChild.appendChild(btnContainer);
}
}
function toggleLoading(isLoading) {
sendBtn.disabled = isLoading;
chatInput.disabled = isLoading;
if (isLoading) {
sendBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status"></span>';
stopBtn.style.display = 'flex';
} else {
sendBtn.innerHTML = '<i class="bi bi-arrow-up-circle-fill"></i>';
stopBtn.style.display = 'none';
chatInput.focus();
}
}
stopBtn.addEventListener('click', () => {
if (abortController) {
abortController.abort();
}
});
sendBtn.addEventListener('click', sendMessage);
chatInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
chatInput.addEventListener('input', () => {
chatInput.style.height = 'auto';
chatInput.style.height = (chatInput.scrollHeight) + 'px';
});
// --- Settings & Themes ---
creativityRange.addEventListener('input', () => {
creativityVal.textContent = creativityRange.value;
});
themeSwatches.forEach(swatch => {
swatch.addEventListener('click', () => {
const theme = swatch.dataset.theme;
document.documentElement.setAttribute('data-theme', theme);
themeSwatches.forEach(s => s.classList.remove('active'));
swatch.classList.add('active');
});
});
saveSettingsBtn.addEventListener('click', async () => {
const theme = document.documentElement.getAttribute('data-theme');
const settings = {
theme: theme,
creativity: creativityRange.value,
limits_off: limitsToggle.checked ? '1' : '0',
model: modelSelect.value
};
saveSettingsBtn.disabled = true;
saveSettingsBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Saving...';
try {
const resp = await fetch('api/settings.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(settings)
});
const data = await resp.json();
if (data.success) {
const modal = bootstrap.Modal.getInstance(document.getElementById('settingsModal'));
if (modal) modal.hide();
showToast('Settings saved successfully!');
}
} catch (e) {
console.error(e);
showToast('Failed to save settings', 'danger');
} finally {
saveSettingsBtn.disabled = false;
saveSettingsBtn.textContent = 'Save changes';
}
});
copyShareBtn.addEventListener('click', () => {
shareUrlInput.select();
document.execCommand('copy');
const original = copyShareBtn.innerHTML;
copyShareBtn.innerHTML = '<i class="bi bi-check2"></i> Copied!';
setTimeout(() => copyShareBtn.innerHTML = original, 2000);
});
function showToast(message, type = 'success') {
const toast = document.createElement('div');
toast.className = `position-fixed bottom-0 start-50 translate-middle-x mb-4 bg-${type} text-white px-4 py-2 rounded-pill shadow-lg animate-fade-in`;
toast.style.zIndex = '2050';
toast.innerHTML = `<i class="bi bi-${type === 'success' ? 'check-circle' : 'exclamation-triangle'}-fill me-2"></i> ${message}`;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transition = 'opacity 0.5s ease';
setTimeout(() => toast.remove(), 500);
}, 3000);
}
const currentTheme = document.documentElement.getAttribute('data-theme');
const activeSwatch = document.querySelector(`.theme-swatch[data-theme="${currentTheme}"]`);
if (activeSwatch) activeSwatch.classList.add('active');
loadHistory();
});