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("/")]
async fn index() -> Result<HttpResponse> {
let html = fs::read_to_string("templates/index.html")
.unwrap_or_else(|_| include_str!("../../static/index.html").to_string());
let html = fs::read_to_string("web/index.html").unwrap();
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 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 voice_adapter = Arc::new(VoiceAdapter::new(

View file

@ -1,122 +1,162 @@
<!doctype html>
<html>
<html lang="pt-br">
<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>
* {
@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;
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:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
sans-serif;
background: #343541;
color: white;
height: 100vh;
display: flex;
.neon-text {
color: var(--dante-gold);
text-shadow:
0 0 15px var(--dante-glow),
0 0 30px var(--dante-glow),
0 0 60px var(--dante-glow),
0 0 90px rgba(255, 215, 0, 0.5);
font-weight: 700;
letter-spacing: 2px;
}
.sidebar {
width: 260px;
background: #202123;
padding: 10px;
display: flex;
flex-direction: column;
.neon-border {
border: 3px solid var(--dante-gold);
box-shadow:
0 0 40px var(--dante-glow),
0 0 60px rgba(255, 215, 0, 0.6),
inset 0 0 30px rgba(255, 215, 0, 0.15);
}
.new-chat {
background: transparent;
border: 1px solid #4d4d4f;
color: white;
padding: 12px;
border-radius: 6px;
margin-bottom: 10px;
cursor: pointer;
.glass {
background: rgba(0, 20, 60, 0.4);
backdrop-filter: blur(20px);
border: 1px solid rgba(253, 185, 19, 0.2);
}
.voice-toggle {
background: #19c37d;
border: 1px solid #19c37d;
color: white;
padding: 12px;
border-radius: 6px;
margin-bottom: 10px;
cursor: pointer;
.background-animated {
position: fixed;
top: 0;
left: 0;
width: 200%;
height: 200%;
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;
border: 1px solid #ef4444;
@keyframes rotate-bg {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.history {
flex: 1;
overflow-y: auto;
}
.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 {
.shine::before {
content: "";
position: absolute;
right: 5px;
top: 5px;
background: #19c37d;
border: none;
padding: 8px 12px;
border-radius: 6px;
color: white;
cursor: pointer;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
transparent,
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 {
text-align: center;
margin: 10px 0;
color: #19c37d;
font-family: "Orbitron", monospace;
}
.pulse {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
opacity: 1;
@ -128,40 +168,227 @@
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>
</head>
<body>
<body class="relative overflow-hidden flex">
<div class="background-animated"></div>
<!-- Sidebar -->
<div class="sidebar">
<button class="new-chat" onclick="createNewSession()">
+ New chat
+ Novo Chat
</button>
<button
class="voice-toggle"
id="voiceToggle"
onclick="toggleVoiceMode()"
>
🎤 Voice Mode
🎤 Modo Voz
</button>
<div class="history" id="history"></div>
</div>
<!-- Main Content -->
<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="pulse">🎤 Listening... Speak now</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 class="pulse">🎤 Ouvindo... Fale agora</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>
<script src="https://unpkg.com/livekit-client@latest/dist/livekit-client.js"></script>
<script>
let ws = null;
let currentSessionId = null;
@ -170,6 +397,15 @@
let isVoiceMode = false;
let mediaRecorder = null;
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() {
const response = await fetch("/api/sessions");
@ -223,7 +459,7 @@
? "assistant-message"
: "voice-message";
addMessage(
role === "user" ? "You" : "Assistant",
role === "user" ? "Você" : "Assistente",
content,
className,
);
@ -240,17 +476,19 @@
if (!response.is_complete) {
if (!isStreaming) {
isStreaming = true;
streamingMessageId = "streaming-" + Date.now();
addMessage(
"Assistant",
"assistant",
response.content,
"assistant-message",
true,
streamingMessageId,
);
} else {
updateLastMessage(response.content);
}
} else {
isStreaming = false;
streamingMessageId = null;
}
};
@ -260,46 +498,64 @@
}
function addMessage(
sender,
role,
content,
className,
isStreaming = false,
streaming = false,
msgId = null,
) {
const messages = document.getElementById("messages");
const messageDiv = document.createElement("div");
messageDiv.className = `message ${className}`;
messageDiv.id = isStreaming ? "streaming-message" : null;
messageDiv.innerHTML = `<strong>${sender}:</strong> ${content}`;
messages.appendChild(messageDiv);
messages.scrollTop = messages.scrollHeight;
const emptyState = document.getElementById("emptyState");
if (emptyState) emptyState.remove();
const msg = document.createElement("div");
msg.className = "mb-8";
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) {
const lastMessage =
document.getElementById("streaming-message");
if (lastMessage) {
lastMessage.innerHTML = `<strong>Assistant:</strong> ${lastMessage.textContent.replace("Assistant:", "").trim() + content}`;
document.getElementById("messages").scrollTop =
document.getElementById("messages").scrollHeight;
const m = document.getElementById(streamingMessageId);
if (m) {
m.textContent += content;
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
}
function sendMessage() {
const input = document.getElementById("messageInput");
const message = input.value.trim();
if (message && ws && ws.readyState === WebSocket.OPEN) {
addMessage("You", message, "user-message");
ws.send(message);
input.value = "";
}
if (!message || !ws || ws.readyState !== WebSocket.OPEN) return;
addMessage("user", message);
ws.send(message);
input.value = "";
anime({
targets: sendBtn,
scale: [1, 0.85, 1],
duration: 300,
easing: "easeInOutQuad",
});
}
function handleKeyPress(event) {
if (event.key === "Enter") {
sendMessage();
}
}
sendBtn.onclick = sendMessage;
input.addEventListener("keypress", (e) => {
if (e.key === "Enter") sendMessage();
});
newChatBtn.onclick = () => createNewSession();
async function toggleVoiceMode() {
isVoiceMode = !isVoiceMode;
@ -307,12 +563,12 @@
const voiceStatus = document.getElementById("voiceStatus");
if (isVoiceMode) {
voiceToggle.textContent = "🔴 Stop Voice";
voiceToggle.textContent = "🔴 Parar Voz";
voiceToggle.classList.add("recording");
voiceStatus.style.display = "block";
await startVoiceSession();
} else {
voiceToggle.textContent = "🎤 Voice Mode";
voiceToggle.textContent = "🎤 Modo Voz";
voiceToggle.classList.remove("recording");
voiceStatus.style.display = "none";
await stopVoiceSession();
@ -379,11 +635,7 @@
try {
const parsed = JSON.parse(message);
if (parsed.type === "voice_response") {
addMessage(
"Assistant",
parsed.text,
"assistant-message",
);
addMessage("assistant", parsed.text);
}
} catch (e) {
console.log("Voice data:", message);
@ -449,14 +701,14 @@
function simulateVoiceTranscription() {
const phrases = [
"Hello, how can I help you today?",
"I understand what you're saying",
"That's an interesting point",
"Let me think about that",
"I can assist you with that",
"What would you like to know?",
"That sounds great",
"I'm listening to your voice",
"Olá, como posso ajudá-lo hoje?",
"Entendo o que você está dizendo",
"Esse é um ponto interessante",
"Deixe-me pensar sobre isso",
"Posso ajudá-lo com isso",
"O que você gostaria de saber?",
"Isso parece ótimo",
"Estou ouvindo sua voz",
];
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>
</body>
</html>