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.
1003 lines
32 KiB
HTML
Executable File
1003 lines
32 KiB
HTML
Executable File
<!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>🤖 Chat com IA</h1>
|
||
<div class="btn-group">
|
||
<button class="btn btn-about" onclick="showAbout()">ℹ Sobre</button>
|
||
<button class="btn btn-help" onclick="showHelpModal()">❓ Ajuda</button>
|
||
<button class="btn btn-reset" onclick="resetChat()">↻ Reiniciar</button>
|
||
<button class="btn btn-clear" onclick="clearChat()">🗑 Apagar</button>
|
||
<button class="btn btn-end" onclick="endChat()">⏹ Encerrar</button>
|
||
</div>
|
||
</header>
|
||
|
||
<div id="chat">
|
||
<div class="msg msg-system">⚡ 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">😊</button>
|
||
<button type="button" class="picker-btn" id="uploadBtn" title="Anexar arquivo/imagem">📎</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">✎ Editor de Arquivo</h2>
|
||
<button class="modal-close" id="editorClose" aria-label="Fechar">×</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">✓ 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: '⚡' };
|
||
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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||
}
|
||
|
||
// ═══ 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}">×</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>📎 ${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">ℹ Sobre</h2><hr style="border-color:#2a2a4a" /><p>Chat com IA — Flask + OpenRouter</p><p style="color:#94a3b8">Versão 1.0</p><hr style="border-color:#2a2a4a" /><p>Sistema de chat com inteligê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ções</li><li>Upload de imagens e arquivos</li><li>Seletor de emojis com categorias</li><li>Persistência diá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ç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">❓ 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>
|