370 lines
14 KiB
JavaScript
370 lines
14 KiB
JavaScript
document.addEventListener('DOMContentLoaded', () => {
|
|
const chatWindow = document.getElementById('chat-window');
|
|
const chatInput = document.getElementById('chat-input');
|
|
const sendBtn = document.getElementById('send-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 themeSwatches = document.querySelectorAll('.theme-swatch');
|
|
const saveSettingsBtn = document.getElementById('save-settings-btn');
|
|
|
|
let currentMode = 'regular';
|
|
let currentChatId = 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>
|
|
`;
|
|
|
|
// Remove active class from history items
|
|
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('');
|
|
|
|
// Add event listeners to history items
|
|
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;
|
|
|
|
// Update Sidebar Mode UI
|
|
modeItems.forEach(i => {
|
|
if (i.dataset.mode === currentMode) i.classList.add('active');
|
|
else i.classList.remove('active');
|
|
});
|
|
|
|
// Update badge
|
|
const activeModeItem = Array.from(modeItems).find(i => i.dataset.mode === currentMode);
|
|
if (activeModeItem) {
|
|
currentModeBadge.textContent = activeModeItem.querySelector('span').textContent;
|
|
}
|
|
|
|
// Render messages
|
|
chatWindow.innerHTML = '';
|
|
data.messages.forEach(msg => {
|
|
appendMessage(msg.role, msg.content, false); // false = don't animate existing
|
|
|
|
// Special handling for game/app mode launch buttons
|
|
if ((currentMode === 'game' || currentMode === 'app') && msg.role === 'assistant') {
|
|
addLaunchButton(msg.content);
|
|
}
|
|
});
|
|
|
|
// Highlight active history item
|
|
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;
|
|
|
|
// Clear input and disable
|
|
chatInput.value = '';
|
|
chatInput.style.height = 'auto';
|
|
toggleLoading(true);
|
|
|
|
// Append User Message
|
|
appendMessage('user', message);
|
|
|
|
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,
|
|
creativity: creativityRange.value,
|
|
limits_off: limitsToggle.checked ? 1 : 0
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
currentChatId = data.chat_id;
|
|
appendMessage('assistant', data.message);
|
|
|
|
// Special handling for game/app mode
|
|
if (currentMode === 'game' || currentMode === 'app') {
|
|
addLaunchButton(data.message);
|
|
}
|
|
|
|
// If it was a new chat, refresh history to show the title
|
|
if (isNewChat) {
|
|
loadHistory();
|
|
}
|
|
} else {
|
|
appendMessage('assistant', 'Error: ' + (data.error || 'Unknown error'));
|
|
}
|
|
} catch (error) {
|
|
appendMessage('assistant', 'Error: ' + error.message);
|
|
} finally {
|
|
toggleLoading(false);
|
|
}
|
|
}
|
|
|
|
function appendMessage(role, text, animate = true) {
|
|
if (role === 'system') return;
|
|
|
|
// Remove empty state if present
|
|
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;
|
|
}
|
|
|
|
function formatText(text) {
|
|
let formatted = text;
|
|
|
|
// Code blocks: ```[lang]\n[code]```
|
|
formatted = formatted.replace(/```(\w+)?\s*([\s\S]*?)```/g, (match, lang, code) => {
|
|
const safeCode = code.trim().replace(/`/g, '\`');
|
|
return `<div class="code-header d-flex justify-content-between px-3 py-1 bg-dark text-muted small border-bottom border-secondary rounded-top mt-2">
|
|
<span>${lang || 'code'}</span>
|
|
<span class="copy-btn" style="cursor:pointer" onclick="navigator.clipboard.writeText(\"${safeCode}\")"><i class="bi bi-clipboard"></i> Copy</span>
|
|
</div>
|
|
<pre class="bg-dark text-white p-3 rounded-bottom mb-2 overflow-auto" style="font-size: 0.85rem; border: 1px solid #444; border-top:none;"><code>${escapeHtml(code.trim())}</code></pre>`;
|
|
});
|
|
|
|
// Simple line breaks for non-code parts
|
|
if (!formatted.includes('<div class="code-header"')) {
|
|
formatted = formatted.replace(/\n/g, '<br>');
|
|
}
|
|
|
|
return formatted;
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function addLaunchButton(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 btn = document.createElement('button');
|
|
btn.className = 'btn btn-sm btn-success mt-2 d-inline-flex align-items-center gap-2 shadow-sm';
|
|
btn.innerHTML = '<i class="bi bi-rocket-takeoff-fill"></i> Launch Application in New Tab';
|
|
btn.onclick = () => {
|
|
if (!codeToLaunch.toLowerCase().includes('<html')) {
|
|
codeToLaunch = `<!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;}</style></head><body>${codeToLaunch}</body></html>`;
|
|
}
|
|
const blob = new Blob([codeToLaunch], { type: 'text/html' });
|
|
const url = URL.createObjectURL(blob);
|
|
window.open(url, '_blank');
|
|
};
|
|
|
|
chatWindow.lastElementChild.appendChild(btn);
|
|
}
|
|
}
|
|
|
|
function toggleLoading(isLoading) {
|
|
sendBtn.disabled = isLoading;
|
|
chatInput.disabled = isLoading;
|
|
if (isLoading) {
|
|
sendBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status"></span>';
|
|
} else {
|
|
sendBtn.innerHTML = '<i class="bi bi-arrow-up-circle-fill"></i>';
|
|
chatInput.focus();
|
|
}
|
|
}
|
|
|
|
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'
|
|
};
|
|
|
|
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';
|
|
}
|
|
});
|
|
|
|
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);
|
|
}
|
|
|
|
// Set active theme swatch on load
|
|
const currentTheme = document.documentElement.getAttribute('data-theme');
|
|
const activeSwatch = document.querySelector(`.theme-swatch[data-theme="${currentTheme}"]`);
|
|
if (activeSwatch) activeSwatch.classList.add('active');
|
|
|
|
// Initial load
|
|
loadHistory();
|
|
}); |