Files
marcocosta10000 e1c24bd2e6 refactor(scripts): simplifica lógica de inicialização e adapta encerramento de chat
Simplifica o script de inicialização removendo detecção complexa de venv e
lógica de fallback, mantendo apenas o fluxo essencial de verificação e
instalação de dependências. Adapta a interface de encerramento de chat para
não aguardar resposta do servidor, considerando que o mesmo é desligado
após o término da conversa.
2026-10-11 19:16:21 -03:00

1003 lines
32 KiB
HTML
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Chat com IA - by syspanel.com.br</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', Arial, sans-serif;
background: #0f0f1a;
color: #e0e0e0;
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
header {
background: #1a1a2e;
padding: 14px 24px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #2a2a4a;
box-shadow: 0 2px 8px rgba(0,0,0,0.4);
}
header h1 {
font-size: 1.3rem;
color: #a78bfa;
font-weight: 600;
letter-spacing: 0.5px;
}
.btn-group { display: flex; gap: 8px; }
.btn {
padding: 7px 16px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 0.82rem;
font-weight: 500;
transition: background 0.2s, transform 0.1s;
}
.btn:hover { transform: translateY(-1px); }
.btn:active { transform: translateY(0); }
.btn-reset { background: #3b3b6b; color: #c4c4ff; }
.btn-reset:hover { background: #4a4a80; }
.btn-clear { background: #7f1d1d; color: #fca5a5; }
.btn-clear:hover { background: #991b1b; }
.btn-end { background: #6b21a8; color: #e9d5ff; }
.btn-end:hover { background: #7c3aed; }
.btn-help { background: #1e3a5f; color: #93c5fd; }
.btn-help:hover { background: #1e4976; }
.btn-about { background: #2a2a4a; color: #a5b4fc; }
.btn-about:hover { background: #3a3a5a; }
#chat {
flex: 1;
overflow-y: auto;
padding: 20px 24px;
display: flex;
flex-direction: column;
gap: 14px;
}
.msg {
max-width: 80%;
padding: 11px 16px;
border-radius: 12px;
line-height: 1.55;
font-size: 0.95rem;
word-wrap: break-word;
white-space: pre-wrap;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.msg-user {
align-self: flex-end;
background: #7c3aed;
color: #fff;
border-bottom-right-radius: 2px;
}
.msg-ai {
align-self: flex-start;
background: #1e293b;
color: #e2e8f0;
border-bottom-left-radius: 2px;
border: 1px solid #2a3a5a;
}
.msg-system {
align-self: center;
background: rgba(99,102,241,0.12);
color: #a5b4fc;
font-size: 0.82rem;
max-width: 90%;
text-align: center;
border: 1px solid rgba(99,102,241,0.25);
border-radius: 10px;
padding: 8px 14px;
}
.msg-command {
align-self: flex-start;
background: #1a1a2e;
color: #c084fc;
font-family: 'Courier New', monospace;
font-size: 0.82rem;
border: 1px solid #2a3a5a;
border-left: 3px solid #7c3aed;
border-radius: 6px;
padding: 10px 14px;
}
.cmd-link {
color: #7c3aed;
cursor: pointer;
text-decoration: underline;
transition: color 0.15s;
}
.cmd-link:hover { color: #a78bfa; }
.msg-label {
display: block;
font-size: 0.72rem;
margin-bottom: 4px;
opacity: 0.7;
letter-spacing: 0.3px;
}
.msg-user .msg-label { color: #e9d5ff; }
.msg-ai .msg-label { color: #94a3b8; }
#loading {
display: none;
align-self: flex-start;
padding: 11px 16px;
background: #1e293b;
border-radius: 12px;
border-bottom-left-radius: 2px;
border: 1px solid #2a3a5a;
color: #94a3b8;
font-size: 0.88rem;
}
#loading .spinner {
display: inline-block;
width: 14px; height: 14px;
border: 2px solid #4a5568;
border-top-color: #a78bfa;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-right: 8px;
vertical-align: middle;
}
@keyframes spin { to { transform: rotate(360deg); } }
footer {
background: #1a1a2e;
padding: 14px 20px;
border-top: 1px solid #2a2a4a;
}
#form { display: flex; gap: 10px; }
#input {
flex: 1;
padding: 11px 16px;
border-radius: 8px;
border: 1px solid #2a3a5a;
background: #0f0f1a;
color: #e0e0e0;
font-size: 0.95rem;
outline: none;
transition: border-color 0.2s;
}
#input:focus { border-color: #7c3aed; }
#input::placeholder { color: #555; }
#send {
padding: 11px 22px;
background: #7c3aed;
color: #fff;
border: none;
border-radius: 8px;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
#send:hover { background: #6d28d9; }
#send:disabled { background: #3b3b6b; cursor: not-allowed; }
#form { display: flex; gap: 10px; align-items: center; }
.picker-btn {
background: none;
border: none;
font-size: 1.2rem;
cursor: pointer;
padding: 6px 10px;
border-radius: 6px;
color: #94a3b8;
transition: background 0.15s;
font-family: 'Twemoji Mozilla', 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', 'EmojiOne Color', sans-serif;
}
.picker-btn:hover { background: #2a2a4a; color: #e2e8f0; }
#uploadInput { display: none; }
#emojiPicker {
display: none;
position: absolute;
bottom: 56px;
right: 0;
background: #1a1a2e;
border: 1px solid #2a2a4a;
border-radius: 12px;
z-index: 100;
box-shadow: 0 4px 24px rgba(0,0,0,0.6);
width: 300px;
max-height: 300px;
overflow: hidden;
flex-direction: column;
}
#emojiPicker.open { display: flex; }
#emojiPicker .picker-search {
width: 100%;
padding: 8px 10px;
background: #0f0f1a;
border: none;
border-bottom: 1px solid #2a3a5a;
color: #e0e0e0;
font-size: 0.82rem;
outline: none;
}
#emojiPicker .picker-search:focus { border-color: #7c3aed; }
#emojiPicker .picker-tabs {
display: flex;
gap: 0;
border-bottom: 1px solid #2a2a4a;
overflow-x: auto;
padding: 0 4px;
}
#emojiPicker .picker-tabs button {
background: none;
border: none;
font-size: 0.85rem;
padding: 4px 8px;
cursor: pointer;
color: #94a3b8;
white-space: nowrap;
font-family: 'Twemoji Mozilla', 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', sans-serif;
}
#emojiPicker .picker-tabs button:hover { color: #e2e8f0; background: #2a2a4a; }
#emojiPicker .picker-tabs button.active { color: #a78bfa; border-bottom: 2px solid #7c3aed; }
#emojiPicker .picker-grid {
display: flex;
flex-wrap: wrap;
gap: 2px;
padding: 6px;
overflow-y: auto;
flex: 1;
min-height: 140px;
max-height: 200px;
}
#emojiPicker .picker-grid button {
background: none;
border: none;
font-size: 1.4rem;
padding: 3px 5px;
cursor: pointer;
border-radius: 4px;
line-height: 1.2;
transition: background 0.1s;
font-family: 'Twemoji Mozilla', 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', 'EmojiOne Color', sans-serif;
}
#emojiPicker .picker-grid button:hover { background: #2a2a4a; }
.msg img.uploaded-img {
max-width: 100%;
border-radius: 8px;
margin: 4px 0;
}
.msg a.download-link {
display: inline-block;
background: #2a3a5a;
color: #a5b4fc;
padding: 6px 12px;
border-radius: 6px;
text-decoration: none;
font-size: 0.82rem;
}
.msg a.download-link:hover { background: #3b4a6a; }
#attachments {
display: flex;
flex-wrap: wrap;
gap: 6px;
padding: 6px 0;
}
.attach-chip {
background: #2a2a4a;
border: 1px solid #3b3b6b;
border-radius: 6px;
padding: 3px 8px;
font-size: 0.78rem;
display: inline-flex;
align-items: center;
gap: 4px;
}
.attach-chip .remove-attach {
cursor: pointer;
color: #f87171;
font-size: 0.85rem;
line-height: 1;
}
.attach-chip .remove-attach:hover { color: #ef4444; }
/* --- Modal --- */
.modal-overlay { display: none; position: fixed; top: 0; right: 0; bottom: 0; left: 0; background: rgba(0,0,0,0.75); z-index: 9999; justify-content: center; align-items: center; }
.modal {
background: #1a1a2e;
border: 1px solid #2a2a4a;
border-radius: 14px;
width: 90%;
max-width: 720px;
max-height: 85vh;
display: flex;
flex-direction: column;
box-shadow: 0 8px 40px rgba(0,0,0,0.6);
animation: modalIn 0.25s ease;
}
@keyframes modalIn {
from { opacity: 0; transform: scale(0.95) translateY(10px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 20px;
border-bottom: 1px solid #2a2a4a;
}
.modal-header h2 {
color: #a78bfa;
font-size: 1.05rem;
font-weight: 600;
}
.modal-close {
background: none;
border: none;
color: #94a3b8;
font-size: 1.3rem;
cursor: pointer;
padding: 2px 6px;
border-radius: 4px;
}
.modal-close:hover { color: #fff; background: #2a2a4a; }
.modal-body {
flex: 1;
overflow-y: auto;
padding: 16px 20px;
}
.modal-body textarea {
width: 100%;
height: 360px;
background: #0f0f1a;
color: #e2e8f0;
border: 1px solid #2a3a5a;
border-radius: 8px;
padding: 12px 14px;
font-family: 'Courier New', monospace;
font-size: 0.88rem;
line-height: 1.6;
resize: vertical;
outline: none;
}
.modal-body textarea:focus { border-color: #7c3aed; }
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 12px 20px;
border-top: 1px solid #2a2a4a;
}
.btn-save {
background: #7c3aed;
color: #fff;
border: none;
padding: 8px 20px;
border-radius: 6px;
cursor: pointer;
font-size: 0.88rem;
font-weight: 600;
}
.btn-save:hover { background: #6d28d9; }
.btn-cancel {
background: #3b3b6b;
color: #c4c4ff;
border: none;
padding: 8px 20px;
border-radius: 6px;
cursor: pointer;
font-size: 0.88rem;
}
.btn-cancel:hover { background: #4a4a80; }
</style>
</head>
<body>
<header>
<h1>&#129302; Chat com IA</h1>
<div class="btn-group">
<button class="btn btn-about" onclick="showAbout()">&#8505; Sobre</button>
<button class="btn btn-help" onclick="showHelpModal()">&#10067; Ajuda</button>
<button class="btn btn-reset" onclick="resetChat()">&#8635; Reiniciar</button>
<button class="btn btn-clear" onclick="clearChat()">&#128465; Apagar</button>
<button class="btn btn-end" onclick="endChat()">&#9209; Encerrar</button>
</div>
</header>
<div id="chat">
<div class="msg msg-system">&#9889; Seja bem-vindo! Digite uma mensagem ou use <b>/help</b> para ver os comandos disponíveis.</div>
</div>
<div id="loading"><span class="spinner"></span>A IA está pensando...</div>
<footer>
<form id="form">
<button type="button" class="picker-btn" id="emojiBtn" title="Inserir emoji">&#128522;</button>
<button type="button" class="picker-btn" id="uploadBtn" title="Anexar arquivo/imagem">&#128206;</button>
<input type="file" id="uploadInput" accept=".png,.jpg,.jpeg,.gif,.webp,.svg,.pdf,.txt,.csv,.zip,.doc,.docx" />
<div id="attachments"></div>
<input type="text" id="input" placeholder="Digite sua mensagem ou /help para comandos..." autocomplete="off" />
<button type="submit" id="send">Enviar</button>
</form>
<div id="emojiPicker"></div>
</footer>
<!-- Modal Editor -->
<div class="modal-overlay" id="editorOverlay">
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="editorTitle">
<div class="modal-header">
<h2 id="editorTitle">&#9998; Editor de Arquivo</h2>
<button class="modal-close" id="editorClose" aria-label="Fechar">&times;</button>
</div>
<div class="modal-body">
<textarea id="editorArea" spellcheck="false"></textarea>
</div>
<div class="modal-footer">
<button class="btn-cancel" id="editorCancel">Cancelar</button>
<button class="btn-save" id="editorSave">&#10003; Salvar</button>
</div>
</div>
</div>
<script>
const chatEl = document.getElementById('chat');
const loading = document.getElementById('loading');
const input = document.getElementById('input');
const sendBtn = document.getElementById('send');
/* ── Modal ── */
const overlay = document.getElementById('editorOverlay');
const editorArea = document.getElementById('editorArea');
const editorTitle = document.getElementById('editorTitle');
let currentFilename = '';
function openModal(filename, customTitle) {
currentFilename = filename;
const titles = {
perfil_ia: 'Editor — perfil_ia.json',
perfil_user: 'Editor — perfil_user.json',
contexto: 'Editor — contexto.json',
config: 'Editor — config.json',
proibidos: 'Editor — proibidos.json',
};
editorTitle.textContent = customTitle || (titles[filename] || 'Editor — ' + filename);
overlay.style.display = 'flex';
editorArea.focus();
}
function closeModal() {
overlay.style.display = 'none';
currentFilename = '';
editorArea.value = '';
}
document.getElementById('editorClose').addEventListener('click', closeModal);
document.getElementById('editorCancel').addEventListener('click', closeModal);
overlay.addEventListener('click', (e) => { if (e.target === overlay) closeModal(); });
document.getElementById('editorSave').addEventListener('click', async () => {
try {
const res = await fetch('/save_file/' + currentFilename, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: editorArea.value }),
});
const data = await res.json();
if (data.error) {
appendMessage('command', 'Erro ao salvar: ' + data.error);
} else {
appendMessage('command', '✔ ' + data.filename + ' salvo com sucesso!');
}
closeModal();
input.focus();
} catch (err) {
appendMessage('command', 'Erro de conexão ao salvar.');
closeModal();
input.focus();
}
});
/* ── Chat helpers ── */
function scrollToBottom() {
const el = chatEl;
el.scrollTo(0, el.scrollHeight);
// fallback for older browsers
el.scrollTop = el.scrollHeight;
}
function appendMessage(role, text) {
const div = document.createElement('div');
div.className = 'msg msg-' + role;
const labels = { user: 'Você', ai: 'IA', system: '', command: '&#9889;' };
const label = labels[role] || '';
let html;
if (role === 'command') {
html = label
? '<span class="msg-label">' + label + '</span>' + renderClickableCommands(escapeHtml(text))
: renderClickableCommands(escapeHtml(text));
} else {
html = label
? '<span class="msg-label">' + label + '</span>' + escapeHtml(text)
: escapeHtml(text);
}
div.innerHTML = html;
chatEl.appendChild(div);
scrollToBottom();
}
function renderClickableCommands(str) {
return str.replace(/(\/[a-z_]+)/g, '<a href="#" class="cmd-link" data-cmd="$1">$1</a>');
}
function escapeHtml(str) {
return str.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// ═══ Emoji picker (categorizado) ═══
const EMOJI_CATEGORIES = [
{ label: 'Rostos', emojis: ['😀','😁','😂','🤣','😃','😄','😅','😆','😊','😋','😌','😍','😘','😗','😙','😚','😜','😝','😛','😏','😒','😳','😪','😴','😔','😕','😣','😖','😫','😩','😤','😠','😭','😢','😰','😱','😨','😵','😷'] },
{ label: 'Mãos', emojis: ['👍','👎','👏','👐','✌️','🤞','🖕','🤟','💪','👊','🤝','🙏','💅'] },
{ label: 'Corações', emojis: ['❤️','🧡','💛','💚','💙','💜','🖤','🤍','🤎','💔','💕','💖','💗','💘','💝','💞','💓','❣️'] },
{ label: 'Animais', emojis: ['🐶','🐱','🐭','🐹','🐰','🦊','🐻','🐼','🐨','🐯','🦁','🐮','🐷','🐸','🐵','🐔','🐧','🐦','🐤','🦆','🦅','🦉','🦇','🐺','🐗','🐴','🦄','🐝','🐛','🦋','🐌','🐞','🐜','🦟','🐢','🐍','🦎','🐙','🦑','🦐','🦞','🦀','🐟','🐠','🐡','🐬','🐳','🐋','🦈','🐊','🐘','🦒','🐪','🦘','🐇'] },
{ label: 'Comida', emojis: ['🍎','🍏','🍐','🍑','🍒','🍓','🍉','🍊','🍋','🍌','🍍','🥝','🍇','🥑','🍆','🍅','🌽','🥕','🧅','🥦','🍄','🥩','🍗','🍖','🍔','🍟','🍕','🌭','🥪','🥙','🥗','🍝','🍜','🍛','🍣','🍤','🥟','🍦','🍧','🍨','🍩','🍪','🎂','🍰','🧁','🍫','🍬','🍭','🍿','🥤','☕','🍺','🍷','🍸'] },
{ label: 'Natureza', emojis: ['🌸','🌹','🌺','🌻','🌼','🌽','🌾','🌿','🌳','🌲','🌴','🍀','🌵','🌱','🌿','☘️','🍁','🍂','🍃','🌍','🌎','🌏','🌟','⭐','🌙','☀️','🌈','⚡','🔥','💧','❄️','🌊','🌋','⛰️','🏔️','🏝️'] },
{ label: 'Objetos', emojis: ['📱','💻','🖥️','⌨️','🖨️','📷','📹','📸','🔍','🔬','🔭','📡','🛰️','⏰','⌚','📱','📞','📟','🔋','💡','🔦','🔌','🔑','🔒','🧲','⚙️','🔧','🔨','🛠️','🔩','📎','📌','✂️','🗑️','🪣','🧹','🪥','🪒','💄','👓','🕶️','💍','👑','🎒','👝','👜','💼','🎒','🧳','☂️','🧤','🧣','👔','👗','👘','🧥','🥻','🩱'] },
{ label: 'Transporte', emojis: ['🚗','🚕','🚙','🚌','🚎','🏎️','🚓','🚑','🚒','🚐','🛻','🚚','🚛','🚜','🏍️','🛵','🛴','🚲','🛹','🚟','🚃','🚂','🛩️','✈️','🚀','🛸','🚁','🪂','⛵','🚤','🛥️','🛳️'] },
{ label: 'Símbolos', emojis: ['💯','✔️','✖️','➕','➖','➗','♾️','❓','❔','❗','❕','‼️','⁉️','🔴','🟠','🟡','🟢','🔵','🟣','🟤','⚫','⚪','🔘','🛑','⭕','🔺','🔻','🔶','🔷','🔸','🔹','💠','🔵','🟢','🟡','🟠','🔴'] },
{ label: 'Celebração', emojis: ['🎉','🎊','🎈','🎁','🎂','🎀','🎄','🎃','🎆','🎇','🎨','🎭','🎪','🎤','🎧','🎵','🎶','🎸','🎺','🎻','🥳','🎲','♟️','🃏','🀄️','🎴'] },
];
const EMOJI_ALL = EMOJI_CATEGORIES.flatMap(c => c.emojis);
let recentEmojis = [];
const emojiBtn = document.getElementById('emojiBtn');
const emojiPicker = document.getElementById('emojiPicker');
// Build picker UI
emojiPicker.innerHTML = `
<input class="picker-search" id="emojiSearch" placeholder="Buscar emoji..." />
<div class="picker-tabs" id="emojiTabs"></div>
<div class="picker-grid" id="emojiGrid"></div>
`;
const emojiSearch = emojiPicker.querySelector('#emojiSearch');
const emojiTabs = emojiPicker.querySelector('#emojiTabs');
const emojiGrid = emojiPicker.querySelector('#emojiGrid');
let activeCat = 0;
function renderCategory(idx) {
activeCat = idx;
const cat = EMOJI_CATEGORIES[idx];
emojiGrid.innerHTML = '';
// recent emojis shown first
let shown = [...new Set(recentEmojis)];
if (idx > 0) shown = [];
shown.forEach(ch => {
const b = document.createElement('button');
b.textContent = ch;
b.title = ch;
emojiGrid.appendChild(b);
});
if (shown.length > 0) {
const sep = document.createElement('div');
sep.style.cssText = 'width:100%;height:1px;background:#2a2a4a;margin:2px 0';
emojiGrid.appendChild(sep);
}
cat.emojis.forEach(ch => {
const b = document.createElement('button');
b.textContent = ch;
b.title = ch;
emojiGrid.appendChild(b);
});
// update tab active
Array.from(emojiTabs.children).forEach((tb, i) => {
tb.className = i === idx ? 'active' : '';
});
}
function filterEmojis(q) {
emojiGrid.innerHTML = '';
const ql = q.toLowerCase();
const results = EMOJI_ALL.filter(ch => {
const desc = ch.codePointAt(0).toString(16);
return ch.includes(q) || desc.includes(ql);
});
if (results.length === 0) {
emojiGrid.innerHTML = '<div style="color:#6b7280;padding:20px;text-align:center">Nenhum emoji encontrado</div>';
return;
}
results.forEach(ch => {
const b = document.createElement('button');
b.textContent = ch;
b.title = ch;
emojiGrid.appendChild(b);
});
}
// Build tabs
EMOJI_CATEGORIES.forEach((cat, i) => {
const tb = document.createElement('button');
tb.textContent = cat.label;
tb.addEventListener('click', () => { renderCategory(i); emojiSearch.value = ''; });
emojiTabs.appendChild(tb);
});
renderCategory(0);
emojiSearch.addEventListener('input', () => {
const q = emojiSearch.value.trim();
if (q) filterEmojis(q);
else renderCategory(activeCat);
});
emojiGrid.addEventListener('click', (e) => {
if (e.target.tagName === 'BUTTON') {
const ch = e.target.textContent;
const start = input.selectionStart || input.value.length;
input.value = input.value.slice(0, start) + ch + input.value.slice(input.selectionEnd || start);
input.focus();
emojiPicker.classList.remove('open');
// track recent
recentEmojis = [ch, ...recentEmojis.filter(x => x !== ch)].slice(0, 12);
}
});
emojiBtn.addEventListener('click', () => {
emojiPicker.classList.toggle('open');
if (emojiPicker.classList.contains('open')) {
renderCategory(activeCat);
emojiSearch.value = '';
setTimeout(() => emojiSearch.focus(), 50);
}
});
document.addEventListener('click', (e) => {
if (!e.target.closest('#emojiPicker') && e.target !== emojiBtn) {
emojiPicker.classList.remove('open');
}
});
// ═══ File upload ═══
const uploadBtn = document.getElementById('uploadBtn');
const uploadInput = document.getElementById('uploadInput');
const attachContainer = document.getElementById('attachments');
let pendingAttachments = []; // {filename, url, data_url, is_image}
uploadBtn.addEventListener('click', () => uploadInput.click());
function renderAttachments() {
attachContainer.innerHTML = '';
pendingAttachments.forEach((a, i) => {
const chip = document.createElement('span');
chip.className = 'attach-chip';
const icon = a.is_image ? '🖼️' : '📎';
chip.innerHTML = `${icon} ${a.filename} <span class="remove-attach" data-idx="${i}">&times;</span>`;
attachContainer.appendChild(chip);
});
}
uploadInput.addEventListener('change', async () => {
const file = uploadInput.files[0];
if (!file) return;
uploadInput.value = '';
try {
const formData = new FormData();
formData.append('file', file);
const res = await fetch('/upload', { method: 'POST', body: formData });
const data = await res.json();
if (data.error) {
appendMessage('command', 'Erro upload: ' + data.error);
return;
}
pendingAttachments.push({
filename: data.filename,
url: data.url,
data_url: data.is_image ? data.data_url : null,
is_image: data.is_image,
});
renderAttachments();
input.focus();
} catch (err) {
appendMessage('command', 'Erro ao fazer upload.');
}
});
// remove attachment chip
document.addEventListener('click', (e) => {
const rm = e.target.closest('.remove-attach');
if (rm) {
const idx = parseInt(rm.getAttribute('data-idx'), 10);
if (!Number.isNaN(idx)) {
pendingAttachments.splice(idx, 1);
renderAttachments();
}
}
});
document.addEventListener('click', function(e) {
const target = e.target;
if (target.classList.contains('cmd-link')) {
e.preventDefault();
const cmd = target.getAttribute('data-cmd');
input.value = cmd;
input.focus();
input.setSelectionRange(cmd.length, cmd.length);
}
});
function setLoading(on) {
loading.style.display = on ? 'block' : 'none';
scrollToBottom();
}
/* ── Send ── */
async function sendMessage(e) {
e.preventDefault();
const msg = input.value.trim();
if (!msg && pendingAttachments.length === 0) return false;
const hasAttach = pendingAttachments.length > 0;
const payload = { message: msg || (hasAttach ? '[anexo]' : '') };
// Build user message display with attachments
let userHtml = '<span class="msg-label">Você</span>';
for (const a of pendingAttachments) {
const tag = a.is_image
? `<img src="${a.url}" class="uploaded-img" alt="${a.filename}" />`
: `<a href="${a.url}" class="download-link" download>&#128206; ${a.filename}</a>`;
userHtml += tag + ' ';
}
if (msg) {
userHtml += escapeHtml(msg);
}
if (userHtml !== '<span class="msg-label">Você</span>') {
const div = document.createElement('div');
div.className = 'msg msg-user';
div.innerHTML = userHtml;
chatEl.appendChild(div);
scrollToBottom();
}
if (hasAttach) {
const img = pendingAttachments.find(a => a.is_image && a.data_url);
if (img) payload.image_url = img.data_url;
const names = pendingAttachments.map(a => a.filename).join(', ');
payload.message = (msg || '') + (msg ? ' ' : '') + '[arquivo: ' + names + ']';
}
pendingAttachments = [];
renderAttachments();
input.value = '';
sendBtn.disabled = true;
setLoading(true);
try {
const res = await fetch('/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const data = await res.json();
setLoading(false);
sendBtn.disabled = false;
if (data.error) {
appendMessage('system', 'Erro: ' + data.error);
} else {
if (data.cmd_reply) {
appendMessage('command', data.reply);
} else if (data.reply) {
appendMessage('ai', data.reply);
}
if (data.end) {
input.value = '';
input.disabled = true;
sendBtn.disabled = true;
}
if (data.optimizing) {
(async () => {
try {
const optRes = await fetch('/optimize', { method: 'POST' });
const optData = await optRes.json();
if (optData.error) {
appendMessage('command', 'Erro: ' + optData.error);
} else {
appendMessage('command', optData.reply);
}
} catch (err) {
appendMessage('command', 'Erro de conexão ao otimizar.');
}
})();
}
if (data.open_editor) {
(async () => {
try {
const fileRes = await fetch('/get_file/' + data.filename);
const fileData = await fileRes.json();
if (fileData.content !== undefined) {
editorArea.value = fileData.content;
openModal(data.filename, data.modalTitle);
} else {
appendMessage('command', 'Erro ao carregar arquivo.');
}
} catch (err) {
appendMessage('command', 'Erro ao carregar arquivo.');
}
})();
}
}
} catch (err) {
setLoading(false);
sendBtn.disabled = false;
appendMessage('system', 'Erro de conexão com o servidor.');
}
return false;
}
/* ── Button shortcuts ── */
async function showHelp() {
appendMessage('user', '/help');
try {
const res = await fetch('/help');
const data = await res.json();
if (data.reply) appendMessage('command', data.reply);
} catch (err) {
appendMessage('system', 'Erro ao obter ajuda.');
}
input.focus();
}
function modalOpen(id) {
const el = document.getElementById(id);
if (el) el.style.display = 'flex';
}
function modalClose(id) {
const el = document.getElementById(id);
if (el) el.style.display = 'none';
}
// Sobre modal
function showAbout() {
const m = document.createElement('div');
m.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.8);z-index:9999;display:flex;justify-content:center;align-items:center;';
const b = document.createElement('div');
b.style.cssText = 'background:#1a1a2e;border:1px solid #2a2a4a;border-radius:14px;padding:24px;max-width:480px;color:#e2e8f0;line-height:1.6;box-shadow:0 8px 40px rgba(0,0,0,0.6);';
b.innerHTML = '<h2 style="color:#a78bfa;margin-top:0">&#8505; Sobre</h2><hr style="border-color:#2a2a4a" /><p>Chat com IA &mdash; Flask + OpenRouter</p><p style="color:#94a3b8">Vers&atilde;o 1.0</p><hr style="border-color:#2a2a4a" /><p>Sistema de chat com intelig&ecirc;ncia artificial via API OpenRouter.ai.</p><p><strong>Funcionalidades:</strong></p><ul style="color:#c4c4ff"><li>Chat em tempo real com IA</li><li>Editor de perfis, contexto e proibi&ccedil;&otilde;es</li><li>Upload de imagens e arquivos</li><li>Seletor de emojis com categorias</li><li>Persist&ecirc;ncia di&aacute;ria</li></ul><hr style="border-color:#2a2a4a" /><p style="font-size:0.82rem">Desenvolvido por <strong>SysPanel.com.br</strong><br />contato@syspanel.com.br<br />Licen&ccedil;a: GNU GPL v3</p><button id="aboutCloseBtn" style="background:#3b3b6b;color:#c4c4ff;border:none;padding:8px 20px;border-radius:6px;cursor:pointer;font-size:0.88rem;margin-top:12px">Fechar</button>';
m.appendChild(b);
document.body.appendChild(m);
document.getElementById('aboutCloseBtn').addEventListener('click', () => document.body.removeChild(m));
m.addEventListener('click', (e) => { if (e.target === m) document.body.removeChild(m); });
}
// Ajuda modal
async function showHelpModal() {
let content = 'Carregando...';
try {
const res = await fetch('/help');
const data = await res.json();
content = data.reply || 'Nenhum dado.';
} catch (err) {
content = 'Erro ao carregar ajuda.';
}
const m = document.createElement('div');
m.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.8);z-index:9999;display:flex;justify-content:center;align-items:center;';
const b = document.createElement('div');
b.style.cssText = 'background:#1a1a2e;border:1px solid #2a2a4a;border-radius:14px;padding:20px;max-width:600px;max-height:80vh;overflow-y:auto;color:#e2e8f0;box-shadow:0 8px 40px rgba(0,0,0,0.6);';
b.innerHTML = '<h2 style="color:#a78bfa;margin-top:0">&#10067; Ajuda</h2><hr style="border-color:#2a2a4a" /><pre style="color:#c4c4ff;font-size:0.82rem;line-height:1.6;white-space:pre-wrap">' + escapeHtml(content) + '</pre><button id="helpCloseBtn" style="background:#3b3b6b;color:#c4c4ff;border:none;padding:8px 20px;border-radius:6px;cursor:pointer;font-size:0.88rem;margin-top:12px">Fechar</button>';
m.appendChild(b);
document.body.appendChild(m);
document.getElementById('helpCloseBtn').addEventListener('click', () => document.body.removeChild(m));
m.addEventListener('click', (e) => { if (e.target === m) document.body.removeChild(m); });
}
async function resetChat() {
appendMessage('user', '/reiniciar');
try {
const res = await fetch('/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: '/reiniciar' }),
});
const data = await res.json();
chatEl.innerHTML = '';
appendMessage('command', data.reply || 'Chat reiniciado.');
} catch (err) {
appendMessage('system', 'Erro ao reiniciar.');
}
input.focus();
}
async function clearChat() {
appendMessage('user', '/clear');
try {
const res = await fetch('/clear', { method: 'POST' });
const data = await res.json();
chatEl.innerHTML = '';
appendMessage('command', 'Histórico apagado. Inicie uma nova conversa!');
} catch (err) {
appendMessage('system', 'Erro ao apagar histórico.');
}
input.focus();
}
async function endChat() {
appendMessage('user', '/encerrar');
try {
await fetch('/end', { method: 'POST' });
} catch (err) {
// servidor encerrado - esperado
}
chatEl.innerHTML = '';
appendMessage('ai', 'Chat encerrado. Recarregue a página ou inicie o servidor novamente.');
input.value = '';
input.disabled = true;
sendBtn.disabled = true;
}
input.focus();
// Load today's conversation history on startup
(async function loadHistory() {
try {
const res = await fetch('/history');
const data = await res.json();
if (data.history && data.history.length > 0) {
chatEl.innerHTML = '';
for (const msg of data.history) {
const role = msg.role === 'user' ? 'user' : 'ai';
appendMessage(role, msg.content);
}
scrollToBottom();
}
} catch (err) {
// history unavailable, start fresh
}
})();
document.getElementById('form').addEventListener('submit', function(e) {
e.preventDefault();
sendMessage(e);
});
input.focus();
</script>
</body>
</html>