Migrate to web/index.html and OpenAI client

- Load index.html from web/index.html instead of templates/static
- Initialize OpenAIClient with an empty key and local endpoint
- Remove the old static/index.html file
This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-10-12 17:41:41 -03:00
parent 386b55bf34
commit e14908fc0b
3 changed files with 426 additions and 164 deletions

View file

@ -686,8 +686,7 @@ async fn set_mode_handler(
#[actix_web::get("/")] #[actix_web::get("/")]
async fn index() -> Result<HttpResponse> { async fn index() -> Result<HttpResponse> {
let html = fs::read_to_string("templates/index.html") let html = fs::read_to_string("web/index.html").unwrap();
.unwrap_or_else(|_| include_str!("../../static/index.html").to_string());
Ok(HttpResponse::Ok().content_type("text/html").body(html)) Ok(HttpResponse::Ok().content_type("text/html").body(html))
} }

View file

@ -105,7 +105,10 @@ async fn main() -> std::io::Result<()> {
}; };
let tool_manager = Arc::new(tools::ToolManager::new()); let tool_manager = Arc::new(tools::ToolManager::new());
let llm_provider = Arc::new(llm::MockLLMProvider::new()); let llm_provider = Arc::new(llm::OpenAIClient::new(
"empty".to_string(),
Some("http://localhost:8081".to_string()),
));
let web_adapter = Arc::new(WebChannelAdapter::new()); let web_adapter = Arc::new(WebChannelAdapter::new());
let voice_adapter = Arc::new(VoiceAdapter::new( let voice_adapter = Arc::new(VoiceAdapter::new(

View file

@ -1,122 +1,162 @@
<!doctype html> <!doctype html>
<html> <html lang="pt-br">
<head> <head>
<title>General Bots</title> <meta charset="utf-8" />
<title>General Bots 2400</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.2/anime.min.js"></script>
<script src="https://unpkg.com/livekit-client@latest/dist/livekit-client.js"></script>
<style> <style>
* { @import url("https://fonts.googleapis.com/css2?family=Orbitron:wght@400;600;800&family=Inter:wght@400;600&display=swap");
:root {
--dante-blue: #000a1f;
--dante-blue2: #001a3d;
--dante-gold: #ffd700;
--dante-gold2: #ffed4e;
--dante-glow: rgba(255, 215, 0, 0.8);
}
html,
body {
height: 100%;
margin: 0; margin: 0;
padding: 0; padding: 0;
box-sizing: border-box; font-family: "Inter", sans-serif;
background: radial-gradient(
circle at 20% 30%,
rgba(0, 48, 135, 0.4),
rgba(0, 26, 77, 0.7)
);
color: #fff;
overflow: hidden;
} }
body {
font-family: .neon-text {
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, color: var(--dante-gold);
sans-serif; text-shadow:
background: #343541; 0 0 15px var(--dante-glow),
color: white; 0 0 30px var(--dante-glow),
height: 100vh; 0 0 60px var(--dante-glow),
display: flex; 0 0 90px rgba(255, 215, 0, 0.5);
font-weight: 700;
letter-spacing: 2px;
} }
.sidebar {
width: 260px; .neon-border {
background: #202123; border: 3px solid var(--dante-gold);
padding: 10px; box-shadow:
display: flex; 0 0 40px var(--dante-glow),
flex-direction: column; 0 0 60px rgba(255, 215, 0, 0.6),
inset 0 0 30px rgba(255, 215, 0, 0.15);
} }
.new-chat {
background: transparent; .glass {
border: 1px solid #4d4d4f; background: rgba(0, 20, 60, 0.4);
color: white; backdrop-filter: blur(20px);
padding: 12px; border: 1px solid rgba(253, 185, 19, 0.2);
border-radius: 6px;
margin-bottom: 10px;
cursor: pointer;
} }
.voice-toggle {
background: #19c37d; .background-animated {
border: 1px solid #19c37d; position: fixed;
color: white; top: 0;
padding: 12px; left: 0;
border-radius: 6px; width: 200%;
margin-bottom: 10px; height: 200%;
cursor: pointer; background: conic-gradient(
from 90deg,
#001a4d,
#003087,
#00509e,
#003087,
#001a4d
);
animation: rotate-bg 20s linear infinite;
opacity: 0.5;
z-index: 0;
filter: blur(120px);
} }
.voice-toggle.recording {
background: #ef4444; @keyframes rotate-bg {
border: 1px solid #ef4444; 0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
} }
.history {
flex: 1; .shine::before {
overflow-y: auto; content: "";
}
.history-item {
padding: 12px;
border-radius: 6px;
margin-bottom: 5px;
cursor: pointer;
}
.history-item:hover {
background: #2a2b32;
}
.main {
flex: 1;
display: flex;
flex-direction: column;
}
.messages {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.message {
max-width: 800px;
margin: 0 auto 20px;
line-height: 1.5;
}
.user-message {
color: #d1d5db;
}
.assistant-message {
color: #ececf1;
}
.voice-message {
color: #19c37d;
font-style: italic;
}
.input-area {
max-width: 800px;
margin: 0 auto 20px;
position: relative;
}
.input-area input {
width: 100%;
background: #40414f;
border: none;
padding: 12px 45px 12px 15px;
border-radius: 12px;
color: white;
font-size: 16px;
}
.input-area button {
position: absolute; position: absolute;
right: 5px; top: 0;
top: 5px; left: -100%;
background: #19c37d; width: 100%;
border: none; height: 100%;
padding: 8px 12px; background: linear-gradient(
border-radius: 6px; 90deg,
color: white; transparent,
cursor: pointer; rgba(253, 185, 19, 0.4),
transparent
);
animation: shine 3s infinite;
} }
@keyframes shine {
0% {
left: -100%;
}
100% {
left: 100%;
}
}
.typing-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--dante-gold);
animation: typing-bounce 1.2s infinite;
box-shadow: 0 0 10px var(--dante-glow);
}
.typing-dot:nth-child(2) {
animation-delay: 0.2s;
}
.typing-dot:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes typing-bounce {
0%,
60%,
100% {
transform: translateY(0);
}
30% {
transform: translateY(-8px);
}
}
::-webkit-scrollbar {
width: 0;
display: none;
}
.voice-status { .voice-status {
text-align: center; text-align: center;
margin: 10px 0; margin: 10px 0;
color: #19c37d; color: #19c37d;
font-family: "Orbitron", monospace;
} }
.pulse { .pulse {
animation: pulse 2s infinite; animation: pulse 2s infinite;
} }
@keyframes pulse { @keyframes pulse {
0% { 0% {
opacity: 1; opacity: 1;
@ -128,40 +168,227 @@
opacity: 1; opacity: 1;
} }
} }
.voice-message {
color: #19c37d;
font-style: italic;
}
.history-item {
padding: 12px;
border-radius: 6px;
margin-bottom: 5px;
cursor: pointer;
color: #d1d5db;
transition: all 0.3s ease;
}
.history-item:hover {
background: rgba(255, 215, 0, 0.1);
}
.sidebar {
width: 260px;
background: rgba(0, 10, 31, 0.8);
padding: 10px;
display: flex;
flex-direction: column;
backdrop-filter: blur(20px);
border-right: 1px solid rgba(255, 215, 0, 0.2);
}
.voice-toggle {
background: rgba(25, 195, 125, 0.2);
border: 1px solid rgba(25, 195, 125, 0.5);
color: #19c37d;
padding: 12px;
border-radius: 6px;
margin-bottom: 10px;
cursor: pointer;
transition: all 0.3s ease;
font-family: "Orbitron", monospace;
font-weight: 600;
}
.voice-toggle.recording {
background: rgba(239, 68, 68, 0.2);
border: 1px solid rgba(239, 68, 68, 0.5);
color: #ef4444;
}
.new-chat {
background: transparent;
border: 1px solid rgba(255, 215, 0, 0.3);
color: var(--dante-gold);
padding: 12px;
border-radius: 6px;
margin-bottom: 10px;
cursor: pointer;
transition: all 0.3s ease;
font-family: "Orbitron", monospace;
font-weight: 600;
}
.new-chat:hover,
.voice-toggle:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
}
.history {
flex: 1;
overflow-y: auto;
margin-top: 10px;
}
.main {
flex: 1;
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
}
.messages {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.message {
max-width: 800px;
margin: 0 auto 20px;
line-height: 1.5;
}
.user-message {
color: #d1d5db;
}
.assistant-message {
color: #ececf1;
}
.input-area {
max-width: 800px;
margin: 0 auto 20px;
position: relative;
}
.input-area input {
width: 100%;
background: rgba(64, 65, 79, 0.5);
border: none;
padding: 12px 45px 12px 15px;
border-radius: 12px;
color: white;
font-size: 16px;
backdrop-filter: blur(10px);
}
.input-area button {
position: absolute;
right: 5px;
top: 5px;
background: rgba(25, 195, 125, 0.3);
border: 1px solid rgba(25, 195, 125, 0.5);
padding: 8px 12px;
border-radius: 6px;
color: #19c37d;
cursor: pointer;
font-family: "Orbitron", monospace;
font-weight: 600;
}
</style> </style>
</head> </head>
<body> <body class="relative overflow-hidden flex">
<div class="background-animated"></div>
<!-- Sidebar -->
<div class="sidebar"> <div class="sidebar">
<button class="new-chat" onclick="createNewSession()"> <button class="new-chat" onclick="createNewSession()">
+ New chat + Novo Chat
</button> </button>
<button <button
class="voice-toggle" class="voice-toggle"
id="voiceToggle" id="voiceToggle"
onclick="toggleVoiceMode()" onclick="toggleVoiceMode()"
> >
🎤 Voice Mode 🎤 Modo Voz
</button> </button>
<div class="history" id="history"></div> <div class="history" id="history"></div>
</div> </div>
<!-- Main Content -->
<div class="main"> <div class="main">
<header
class="glass border-b border-yellow-400/40 relative z-20 flex items-center justify-between px-10 py-6 shadow-2xl"
>
<div class="flex items-center gap-4">
<div
class="w-14 h-14 rounded-2xl neon-border flex items-center justify-center relative shine"
>
<span class="text-3xl font-extrabold neon-text">D</span>
</div>
<h1 class="text-4xl font-extrabold neon-text">
General Bots
</h1>
</div>
<button
id="newChatBtn"
class="px-8 py-3 rounded-xl glass neon-border text-yellow-300 font-semibold hover:scale-105 transition-all"
>
Novo Chat
</button>
</header>
<div class="voice-status" id="voiceStatus" style="display: none"> <div class="voice-status" id="voiceStatus" style="display: none">
<div class="pulse">🎤 Listening... Speak now</div> <div class="pulse">🎤 Ouvindo... Fale agora</div>
</div>
<div class="messages" id="messages"></div>
<div class="input-area">
<input
type="text"
id="messageInput"
placeholder="Type your message or use voice..."
onkeypress="handleKeyPress(event)"
/>
<button onclick="sendMessage()">Send</button>
</div> </div>
<main
id="messages"
class="relative z-10 overflow-y-auto h-[calc(100vh-170px)] px-10 py-8"
>
<div
id="emptyState"
class="text-center pt-40 flex flex-col items-center justify-center"
>
<div
class="w-36 h-36 rounded-3xl neon-border flex items-center justify-center shine mb-6"
>
<span class="text-7xl neon-text font-extrabold">D</span>
</div>
<h2 class="text-4xl neon-text font-bold">
Bem-vindo ao General Bots
</h2>
<p class="text-blue-200 mt-3 opacity-80 text-lg">
Seu assistente de IA avançado
</p>
</div>
</main>
<footer
class="glass border-t border-yellow-400/30 px-10 py-6 relative z-20 backdrop-blur-lg"
>
<div class="flex items-center gap-4">
<input
id="messageInput"
type="text"
placeholder="Fale com General Bots..."
class="flex-1 rounded-2xl px-8 py-4 glass border border-yellow-400/40 text-lg text-white placeholder-yellow-200/40 focus:outline-none focus:border-yellow-300 transition-all"
/>
<button
id="sendBtn"
class="rounded-2xl px-10 py-4 neon-border hover:scale-105 transition-all text-yellow-300 font-bold tracking-wide"
>
Enviar
</button>
</div>
</footer>
</div> </div>
<script src="https://unpkg.com/livekit-client@latest/dist/livekit-client.js"></script>
<script> <script>
let ws = null; let ws = null;
let currentSessionId = null; let currentSessionId = null;
@ -170,6 +397,15 @@
let isVoiceMode = false; let isVoiceMode = false;
let mediaRecorder = null; let mediaRecorder = null;
let audioChunks = []; let audioChunks = [];
let streamingMessageId = null;
const messagesDiv = document.getElementById("messages");
const input = document.getElementById("messageInput");
const sendBtn = document.getElementById("sendBtn");
const newChatBtn = document.getElementById("newChatBtn");
// Initialize
createNewSession();
async function loadSessions() { async function loadSessions() {
const response = await fetch("/api/sessions"); const response = await fetch("/api/sessions");
@ -223,7 +459,7 @@
? "assistant-message" ? "assistant-message"
: "voice-message"; : "voice-message";
addMessage( addMessage(
role === "user" ? "You" : "Assistant", role === "user" ? "Você" : "Assistente",
content, content,
className, className,
); );
@ -240,17 +476,19 @@
if (!response.is_complete) { if (!response.is_complete) {
if (!isStreaming) { if (!isStreaming) {
isStreaming = true; isStreaming = true;
streamingMessageId = "streaming-" + Date.now();
addMessage( addMessage(
"Assistant", "assistant",
response.content, response.content,
"assistant-message",
true, true,
streamingMessageId,
); );
} else { } else {
updateLastMessage(response.content); updateLastMessage(response.content);
} }
} else { } else {
isStreaming = false; isStreaming = false;
streamingMessageId = null;
} }
}; };
@ -260,46 +498,64 @@
} }
function addMessage( function addMessage(
sender, role,
content, content,
className, streaming = false,
isStreaming = false, msgId = null,
) { ) {
const messages = document.getElementById("messages"); const emptyState = document.getElementById("emptyState");
const messageDiv = document.createElement("div"); if (emptyState) emptyState.remove();
messageDiv.className = `message ${className}`;
messageDiv.id = isStreaming ? "streaming-message" : null; const msg = document.createElement("div");
messageDiv.innerHTML = `<strong>${sender}:</strong> ${content}`; msg.className = "mb-8";
messages.appendChild(messageDiv);
messages.scrollTop = messages.scrollHeight; if (role === "user") {
msg.innerHTML = `<div class="flex justify-end"><div class="glass neon-border rounded-2xl px-6 py-4 max-w-3xl text-lg text-yellow-100 font-semibold shadow-2xl">${content}</div></div>`;
} else if (role === "assistant") {
msg.innerHTML = `<div class="flex justify-start"><div class="flex gap-4 max-w-3xl"><div class="w-12 h-12 rounded-xl neon-border flex items-center justify-center flex-shrink-0 shine shadow-2xl"><span class="text-2xl neon-text font-extrabold">D</span></div><div class="glass border-2 border-yellow-400/30 rounded-2xl px-6 py-4 flex-1 text-blue-50 font-medium text-lg shadow-2xl" id="${streaming ? msgId : ""}">${streaming ? "" : content}</div></div></div>`;
} else {
// Voice message
msg.innerHTML = `<div class="flex justify-start"><div class="flex gap-4 max-w-3xl"><div class="w-12 h-12 rounded-xl neon-border flex items-center justify-center flex-shrink-0 shine shadow-2xl"><span class="text-2xl neon-text font-extrabold">D</span></div><div class="glass border-2 border-green-400/30 rounded-2xl px-6 py-4 flex-1 text-green-100 font-medium text-lg shadow-2xl">${content}</div></div></div>`;
}
messagesDiv.appendChild(msg);
gsap.from(msg, {
opacity: 0,
y: 30,
duration: 0.6,
ease: "power3.out",
});
messagesDiv.scrollTop = messagesDiv.scrollHeight;
} }
function updateLastMessage(content) { function updateLastMessage(content) {
const lastMessage = const m = document.getElementById(streamingMessageId);
document.getElementById("streaming-message"); if (m) {
if (lastMessage) { m.textContent += content;
lastMessage.innerHTML = `<strong>Assistant:</strong> ${lastMessage.textContent.replace("Assistant:", "").trim() + content}`; messagesDiv.scrollTop = messagesDiv.scrollHeight;
document.getElementById("messages").scrollTop =
document.getElementById("messages").scrollHeight;
} }
} }
function sendMessage() { function sendMessage() {
const input = document.getElementById("messageInput");
const message = input.value.trim(); const message = input.value.trim();
if (!message || !ws || ws.readyState !== WebSocket.OPEN) return;
if (message && ws && ws.readyState === WebSocket.OPEN) { addMessage("user", message);
addMessage("You", message, "user-message"); ws.send(message);
ws.send(message); input.value = "";
input.value = ""; anime({
} targets: sendBtn,
scale: [1, 0.85, 1],
duration: 300,
easing: "easeInOutQuad",
});
} }
function handleKeyPress(event) { sendBtn.onclick = sendMessage;
if (event.key === "Enter") { input.addEventListener("keypress", (e) => {
sendMessage(); if (e.key === "Enter") sendMessage();
} });
}
newChatBtn.onclick = () => createNewSession();
async function toggleVoiceMode() { async function toggleVoiceMode() {
isVoiceMode = !isVoiceMode; isVoiceMode = !isVoiceMode;
@ -307,12 +563,12 @@
const voiceStatus = document.getElementById("voiceStatus"); const voiceStatus = document.getElementById("voiceStatus");
if (isVoiceMode) { if (isVoiceMode) {
voiceToggle.textContent = "🔴 Stop Voice"; voiceToggle.textContent = "🔴 Parar Voz";
voiceToggle.classList.add("recording"); voiceToggle.classList.add("recording");
voiceStatus.style.display = "block"; voiceStatus.style.display = "block";
await startVoiceSession(); await startVoiceSession();
} else { } else {
voiceToggle.textContent = "🎤 Voice Mode"; voiceToggle.textContent = "🎤 Modo Voz";
voiceToggle.classList.remove("recording"); voiceToggle.classList.remove("recording");
voiceStatus.style.display = "none"; voiceStatus.style.display = "none";
await stopVoiceSession(); await stopVoiceSession();
@ -379,11 +635,7 @@
try { try {
const parsed = JSON.parse(message); const parsed = JSON.parse(message);
if (parsed.type === "voice_response") { if (parsed.type === "voice_response") {
addMessage( addMessage("assistant", parsed.text);
"Assistant",
parsed.text,
"assistant-message",
);
} }
} catch (e) { } catch (e) {
console.log("Voice data:", message); console.log("Voice data:", message);
@ -449,14 +701,14 @@
function simulateVoiceTranscription() { function simulateVoiceTranscription() {
const phrases = [ const phrases = [
"Hello, how can I help you today?", "Olá, como posso ajudá-lo hoje?",
"I understand what you're saying", "Entendo o que você está dizendo",
"That's an interesting point", "Esse é um ponto interessante",
"Let me think about that", "Deixe-me pensar sobre isso",
"I can assist you with that", "Posso ajudá-lo com isso",
"What would you like to know?", "O que você gostaria de saber?",
"That sounds great", "Isso parece ótimo",
"I'm listening to your voice", "Estou ouvindo sua voz",
]; ];
const randomPhrase = const randomPhrase =
@ -475,10 +727,18 @@
); );
} }
addMessage("You", `🎤 ${randomPhrase}`, "voice-message"); addMessage("voice", `🎤 ${randomPhrase}`);
} }
createNewSession(); // Neon text animation
gsap.to(".neon-text", {
textShadow:
"0 0 25px var(--gb-glow),0 0 50px var(--gb-glow),0 0 100px rgba(255,215,0,0.8)",
repeat: -1,
yoyo: true,
duration: 1.8,
ease: "power1.inOut",
});
</script> </script>
</body> </body>
</html> </html>