COGNILINE-APP-v1.0 / index.html
GABASSI's picture
Update index.html
63c483a verified
<!DOCTYPE html>
<html lang="pt">
<head>
<meta charset="UTF-8">
<title>COGNILINE OMNI</title>
<!-- Bibliotecas Estáveis -->
<script src="https://unpkg.com/react@18/umd/react.production.min.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js" crossorigin></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<!-- KaTeX -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
body { background: #09090b; color: #e4e4e7; font-family: 'Segoe UI', sans-serif; height: 100vh; overflow: hidden; margin: 0; }
.sidebar-anim { transition: width 0.4s ease, opacity 0.3s; }
.cloud-zone { border: 2px dashed #52525b; background: #27272a; cursor: pointer; transition: 0.3s; }
.cloud-zone:hover { border-color: #a1a1aa; background: #3f3f46; }
.chat-text { overflow-wrap: break-word; }
.chat-text > *:last-child { margin-bottom: 0 !important; }
p:empty { display: none !important; }
.katex-display { background: #18181b; padding: 1rem; border-radius: 8px; overflow-x: auto; margin: 0.8rem 0; border: 1px solid #3f3f46; }
.chat-text .katex-display:last-child { margin-bottom: 0 !important; }
.mic-active { animation: pulse 1.5s infinite; background: #ef4444 !important; color: #fff !important; border-color: #ef4444 !important; }
@keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(239,68,68,0.7); } 70% { box-shadow: 0 0 0 10px rgba(239,68,68,0); } 100% { box-shadow: 0 0 0 0 rgba(239,68,68,0); } }
.img-prev { width: 32px; height: 32px; border-radius: 6px; object-fit: cover; border: 1px solid #3f3f46; }
.msg-img { max-width: 200px; border-radius: 10px; margin-bottom: 10px; border: 1px solid #ffffff20; }
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-thumb { background: #52525b; border-radius: 10px; }
::-webkit-scrollbar-track { background: #18181b; }
#loader { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: #09090b; display: flex; flex-direction: column; justify-content: center; align-items: center; z-index: 50; }
#error-msg { display: none; color: #ef4444; margin-top: 20px; font-family: monospace; max-width: 80%; text-align: center; }
.btn-reset { margin-top: 15px; padding: 10px 20px; background: #2563eb; color: white; border: none; border-radius: 6px; cursor: pointer; display: none; }
</style>
</head>
<body>
<div id="loader">
<h2 style="font-size: 24px; font-weight: bold; color: white; letter-spacing: 2px;">COGNILINE</h2>
<p id="loading-text" style="color: #71717a; margin-top: 10px;">A iniciar interface...</p>
<div id="error-msg"></div>
<button id="reset-btn" class="btn-reset" onclick="localStorage.clear(); window.location.reload()">⚠️ LIMPAR DADOS E REINICIAR</button>
</div>
<div id="root" class="h-screen" style="display:none"></div>
<script>
window.onerror = function(msg, src, line, col, error) {
document.getElementById('loading-text').innerText = "Erro Crítico Detetado.";
document.getElementById('error-msg').style.display = 'block';
document.getElementById('error-msg').innerHTML = `${msg} (Linha ${line})`;
document.getElementById('reset-btn').style.display = 'block';
};
setTimeout(() => {
if (document.getElementById('root').style.display === 'none') {
document.getElementById('loading-text').innerText = "O carregamento está lento.";
document.getElementById('reset-btn').style.display = 'block';
}
}, 8000);
</script>
<script type="text/babel">
// Mostra o app e esconde o loader assim que o React inicia
document.getElementById('root').style.display = 'block';
document.getElementById('loader').style.display = 'none';
const { useState, useEffect, useRef } = React;
const LOGO = "LOGO_B64";
const Msg = ({ t, role, img }) => {
const r = useRef();
useEffect(() => {
if (r.current) {
try {
let txt = t || "";
// Limpeza segura de delimitadores
txt = txt.split("\\[").join("$$").split("\\]").join("$$").split("\\(").join("$").split("\\)").join("$");
r.current.innerHTML = marked.parse(txt);
renderMathInElement(r.current, { delimiters: [{ left: "$$", right: "$$", display: true }, { left: "$", right: "$", display: false }], throwOnError: false });
} catch(e) { r.current.innerText = t; }
}
}, [t]);
const speak = () => {
window.speechSynthesis.cancel();
const u = new SpeechSynthesisUtterance(t.replace(/[\$\*#]/g, ''));
const vs = window.speechSynthesis.getVoices();
const m = vs.find(v => (v.name.includes("Daniel") || v.name.includes("Male") || v.name.includes("Google")) && v.lang.includes("pt"));
if (m) u.voice = m;
u.lang = 'pt-PT'; u.rate = 1.1;
window.speechSynthesis.speak(u);
};
return (
<div className="msg-box">
{img && <img src={`data:image/jpeg;base64,${img}`} className="msg-img" />}
<div ref={r} className="chat-text space-y-2" />
{role === 'ai' && (
<div className="mt-2 pt-2 border-t border-white/10 opacity-70 hover:opacity-100 transition flex gap-2">
<button onClick={speak} className="text-[10px] flex items-center gap-1 cursor-pointer bg-white/5 px-2 py-1 rounded hover:bg-white/10">
<i data-lucide="volume-2" className="w-3 h-3"></i> Ouvir
</button>
</div>
)}
</div>
);
};
function App() {
// Histórico seguro
const [hist, setHist] = useState(() => {
try { return JSON.parse(localStorage.getItem("cgl_v106") || "[]") } catch { return [] }
});
const [cur, setCur] = useState(null);
const [mode, setMode] = useState("OFFLINE");
const [load, setLoad] = useState(false);
const [chat, setChat] = useState([]);
// --- CORREÇÃO: Nome da variável padronizado para 'input' ---
const [input, setInput] = useState("");
const [files, setFiles] = useState(null);
const [sb, setSb] = useState(true);
const [mic, setMic] = useState(false);
const [web, setWeb] = useState(false);
const [img, setImg] = useState(null);
const [prev, setPrev] = useState(null);
const box = useRef(null);
useEffect(() => { localStorage.setItem("cgl_v106", JSON.stringify(hist)) }, [hist]);
useEffect(() => {
if (window.lucide) window.lucide.createIcons();
if (box.current) box.current.scrollTop = box.current.scrollHeight;
}, [chat, load, sb]);
useEffect(() => { window.speechSynthesis.getVoices() }, []);
const togMic = () => {
if (!('webkitSpeechRecognition' in window)) return alert("Sem voz.");
if (mic) { window.rec.stop(); setMic(false); return; }
const R = window.webkitSpeechRecognition; window.rec = new R(); window.rec.lang = 'pt-BR';
window.rec.onstart = () => setMic(true);
// CORREÇÃO: Usando 'setInput'
window.rec.onresult = (e) => setInput(p => (p ? p + " " + e.results[0][0].transcript : e.results[0][0].transcript));
window.rec.onend = () => setMic(false); window.rec.start();
};
const handleImg = (e) => {
const f = e.target.files[0];
if (f) { setImg(f); const r = new FileReader(); r.onloadend = () => { setPrev(r.result) }; r.readAsDataURL(f); }
};
const boot = async (uF) => {
setLoad(true); const fd = new FormData();
if (uF && files) for (let f of files) fd.append("files", f);
try {
const r = await fetch("/api/init", { method: "POST", body: fd });
const d = await r.json();
if (d.status === "ok") {
setMode(d.mode); setChat([{ role: 'ai', t: d.message }]);
const id = Date.now();
const nC = { id, title: uF ? "Docs" : "Novo Chat", messages: [{ role: 'ai', t: d.message }], mode: d.mode, time: new Date().toLocaleTimeString('pt-PT', { hour: '2-digit', minute: '2-digit' }) };
setHist([nC, ...hist]); setCur(id); setFiles(null);
} else alert(d.message);
} catch (e) { alert("Erro Rede"); } setLoad(false);
};
const loadChat = (id) => { const t = hist.find(c => c.id === id); if (t) { setCur(id); setChat(t.messages); setMode(t.mode); } };
const send = async () => {
// CORREÇÃO: Usando 'input'
if ((!input.trim() && !img) || load || mode === "OFFLINE") return;
const ut = input; setInput("");
const nCh = [...chat, { role: 'user', t: ut + (img ? " [Imagem]" : ""), img: prev ? prev.split(',')[1] : null }];
setChat(nCh); setLoad(true);
let b64 = null; if (img) b64 = prev.split(',')[1];
setImg(null); setPrev(null);
try {
const r = await fetch("/api/ask", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query: ut, web_search: web, image_base64: b64, generate_title: nCh.filter(x => x.role === 'user').length === 1 }) });
const d = await r.json();
const aiM = { role: 'ai', t: d.ans };
const final = [...nCh, aiM];
setChat(final);
if (cur) setHist(p => p.map(c => c.id === cur ? { ...c, messages: final, title: d.title || c.title, time: new Date().toLocaleTimeString('pt-PT', { hour: '2-digit', minute: '2-digit' }) } : c));
} catch (e) { setChat(p => [...p, { role: 'ai', t: "Erro Rede" }]); } setLoad(false);
};
return (
<div className="flex h-full w-full bg-zinc-950">
<aside className={`sidebar-anim ${sb ? 'w-80' : 'w-0'} bg-zinc-900 border-r border-zinc-800 flex flex-col overflow-hidden z-20`}>
<div className="p-6 flex flex-col gap-6 min-w-[320px]">
<div className="flex items-center gap-3"><img src={LOGO} className="h-10 w-10 rounded-full border-2 border-zinc-500 shadow-lg grayscale" onError={(e) => e.target.style.display = 'none'} /><h1 className="font-bold text-white text-lg">COGNILINE</h1></div>
<div className="flex flex-col gap-4">
<button onClick={() => boot(false)} className="w-full py-3 bg-zinc-800 hover:bg-zinc-700 rounded-full text-xs font-bold text-white border border-zinc-700 flex justify-center gap-2">Chat Livre (Nova Sessão)</button>
<div className="cloud-zone rounded-[2rem] p-5 flex flex-col items-center gap-2 relative">
<input type="file" multiple onChange={e => setFiles(e.target.files)} className="absolute inset-0 opacity-0 cursor-pointer" />
<i data-lucide="cloud-upload" className="w-7 h-7 text-zinc-400"></i><span className="text-[9px] font-bold text-zinc-500">{files ? files.length + " PDFs" : "Carregar Manuais"}</span>
</div>
<button onClick={() => boot(true)} disabled={!files} className="w-full py-2.5 bg-zinc-100 hover:bg-white text-black rounded-full text-xs font-bold shadow-lg disabled:opacity-30">ATIVAR BASE</button>
</div>
<div className="flex-1 overflow-y-auto flex flex-col gap-2"><h2 className="text-[9px] font-black text-zinc-600 uppercase border-b border-zinc-800 pb-1">Histórico</h2>{hist.map(c => (<div key={c.id} onClick={() => loadChat(c.id)} className={`p-3 rounded-2xl border cursor-pointer ${c.id === cur ? 'bg-zinc-800 border-zinc-600' : 'bg-zinc-900/50 border-zinc-800'}`}><div className="flex justify-between"><h3 className="text-[10px] font-bold text-zinc-200 truncate flex-1">{c.title}</h3><span className="text-[7px] text-zinc-500 font-bold">{c.mode}</span></div><div className="mt-1 text-[8px] text-zinc-500">{c.time}</div></div>))}</div>
</div>
</aside>
<main className="flex-1 flex flex-col relative h-full bg-zinc-950">
<button onClick={() => setSb(!sb)} className="absolute top-6 left-6 z-30 p-2 bg-zinc-800 text-zinc-400 rounded-xl shadow-xl border border-zinc-700 hover:text-white"><i data-lucide={sb ? "panel-left-close" : "panel-left-open"} className="w-5 h-5"></i></button>
<div ref={box} className="absolute inset-0 overflow-y-auto p-8 pt-20 pb-40 space-y-8 scroll-smooth">
{chat.length === 0 && <div className="h-full flex flex-col items-center justify-center opacity-10 grayscale"><i data-lucide="cpu" className="w-32 h-32 mb-4"></i><p className="font-black text-4xl text-zinc-600">COGNILINE OMNI</p></div>}
{chat.map((m, i) => (<div key={i} className={`flex w-full ${m.role === 'user' ? 'justify-end' : 'justify-start'}`}><div className={`max-w-[85%] p-6 text-sm shadow-2xl rounded-[2rem] ${m.role === 'user' ? 'bg-zinc-700 text-white border border-zinc-600' : 'bg-zinc-900 text-zinc-300 border border-zinc-800'}`}><Msg t={m.t} role={m.role} img={m.img} /></div></div>))}
{load && <div className="ml-10 text-zinc-500 text-[10px] font-black animate-pulse flex items-center gap-2">PROCESSANDO...</div>}
</div>
<div className="absolute bottom-0 left-0 w-full p-8 bg-zinc-950/95 border-t border-zinc-900 backdrop-blur-md z-20">
<div className="max-w-4xl mx-auto flex gap-3 items-center bg-zinc-900 border border-zinc-800 p-3 pl-5 pr-5 rounded-full shadow-2xl focus-within:ring-1 ring-zinc-500/50 transition-all">
<button onClick={() => setWeb(!web)} className={`w-8 h-8 rounded-full flex items-center justify-center transition ${web ? 'text-blue-400' : 'text-zinc-600 hover:text-white'}`} title="Web"><i data-lucide="globe" className="w-4 h-4"></i></button>
<div className="relative group">
<input type="file" accept="image/*" onChange={handleImg} className="absolute inset-0 w-8 h-8 opacity-0 cursor-pointer" />
<button className={`w-8 h-8 rounded-full flex items-center justify-center transition ${prev ? 'text-blue-400' : 'text-zinc-600 hover:text-white'}`}><i data-lucide="image" className="w-4 h-4"></i></button>
</div>
<button onClick={togMic} className={`w-10 h-10 rounded-full flex items-center justify-center transition ${mic ? 'mic-active' : 'text-zinc-500 hover:text-white hover:bg-zinc-800'}`}><i data-lucide={mic ? "mic-off" : "mic"} className="w-5 h-5"></i></button>
{/* CORREÇÃO AQUI: Usando 'input' e 'setInput' consistentemente */}
<input
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={e => e.key === 'Enter' && send()}
disabled={mode === "OFFLINE" || load}
className="flex-1 bg-transparent text-white outline-none placeholder-zinc-600"
placeholder={web ? "Pesquisar na Web..." : "Escreva ou fale..."}
/>
<button onClick={send} className="bg-zinc-100 hover:bg-white text-black w-10 h-10 rounded-full flex items-center justify-center shadow-lg active:scale-95 transition-all"><i data-lucide="send-horizontal" className="w-5 h-5"></i></button>
</div>
</div>
</main>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>