Added new CSS classes for suggestion buttons and container, moving them from message content to footer. Removed inline styles in favor of CSS classes for better maintainability. The suggestions now appear at the top of the footer with consistent styling and hover effects.
1930 lines
67 KiB
HTML
1930 lines
67 KiB
HTML
<!doctype html>
|
|
<html lang="pt-br">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<title>General Bots</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://cdn.jsdelivr.net/npm/livekit-client/dist/livekit-client.umd.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.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-glow: rgba(255, 215, 0, 0.8);
|
|
}
|
|
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: "Inter", sans-serif;
|
|
background: linear-gradient(135deg, #0a0e27 0%, #1a1f3a 100%);
|
|
color: #e0e0e0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.sidebar {
|
|
position: fixed;
|
|
left: -300px;
|
|
top: 0;
|
|
width: 300px;
|
|
height: 100vh;
|
|
background: rgba(10, 14, 39, 0.95);
|
|
border-right: 1px solid rgba(255, 215, 0, 0.3);
|
|
transition: left 0.3s ease;
|
|
z-index: 100;
|
|
overflow-y: auto;
|
|
padding: 20px;
|
|
box-shadow: 5px 0 25px rgba(0, 0, 0, 0.5);
|
|
}
|
|
|
|
.sidebar.open {
|
|
left: 0;
|
|
}
|
|
|
|
.sidebar-toggle {
|
|
position: fixed;
|
|
left: 20px;
|
|
top: 24px;
|
|
opacity: 30%;
|
|
z-index: 101;
|
|
background: rgba(255, 215, 0, 0.2);
|
|
border: 1px solid rgba(255, 215, 0, 0.4);
|
|
color: #ffd700;
|
|
padding: 10px 15px;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
font-size: 20px;
|
|
width: 45px;
|
|
height: 45px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
.sidebar-toggle:hover {
|
|
background: rgba(255, 215, 0, 0.3);
|
|
box-shadow: 0 0 20px rgba(255, 215, 0, 0.5);
|
|
}
|
|
|
|
.new-chat {
|
|
width: 100%;
|
|
padding: 12px;
|
|
background: rgba(255, 215, 0, 0.2);
|
|
border: 1px solid rgba(255, 215, 0, 0.4);
|
|
border-radius: 8px;
|
|
color: #ffd700;
|
|
cursor: pointer;
|
|
margin-top: 60px;
|
|
margin-bottom: 15px;
|
|
transition: all 0.3s ease;
|
|
font-weight: 600;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
.new-chat:hover {
|
|
background: rgba(255, 215, 0, 0.3);
|
|
box-shadow: 0 0 15px rgba(255, 215, 0, 0.4);
|
|
}
|
|
|
|
.voice-toggle {
|
|
width: 100%;
|
|
padding: 12px;
|
|
background: rgba(100, 255, 100, 0.2);
|
|
border: 1px solid rgba(100, 255, 100, 0.3);
|
|
border-radius: 8px;
|
|
color: #90ff90;
|
|
cursor: pointer;
|
|
margin-bottom: 15px;
|
|
transition: all 0.3s ease;
|
|
font-weight: 600;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
.voice-toggle:hover {
|
|
background: rgba(100, 255, 100, 0.3);
|
|
}
|
|
|
|
.voice-toggle.recording {
|
|
background: rgba(255, 100, 100, 0.3);
|
|
border-color: rgba(255, 100, 100, 0.4);
|
|
color: #ff9090;
|
|
}
|
|
|
|
.history-item {
|
|
padding: 10px;
|
|
margin-bottom: 8px;
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
font-size: 14px;
|
|
word-wrap: break-word;
|
|
}
|
|
|
|
.history-item:hover {
|
|
background: rgba(255, 215, 0, 0.2);
|
|
border: 1px solid rgba(255, 215, 0, 0.3);
|
|
}
|
|
|
|
.main {
|
|
margin-left: 0;
|
|
width: 100%;
|
|
height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
transition: margin-left 0.3s ease;
|
|
}
|
|
|
|
.sidebar.open ~ .main {
|
|
margin-left: 300px;
|
|
width: calc(100% - 300px);
|
|
}
|
|
|
|
header {
|
|
background: rgba(10, 14, 39, 0.8);
|
|
backdrop-filter: blur(10px);
|
|
border-bottom: 1px solid rgba(255, 215, 0, 0.3);
|
|
padding: 20px 40px 20px 80px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
.logo {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.logo-icon {
|
|
width: 40px;
|
|
height: 40px;
|
|
background: linear-gradient(135deg, #ffd700, #ffed4e);
|
|
border-radius: 10px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 24px;
|
|
font-weight: 800;
|
|
color: #0a0e27;
|
|
box-shadow: 0 0 20px rgba(255, 215, 0, 0.5);
|
|
}
|
|
|
|
.logo-text {
|
|
font-size: 24px;
|
|
font-weight: 800;
|
|
font-family: "Orbitron", sans-serif;
|
|
color: #ffd700;
|
|
text-shadow: 0 0 20px var(--dante-glow);
|
|
}
|
|
|
|
.neon-text {
|
|
animation: neonPulse 2s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes neonPulse {
|
|
0%,
|
|
100% {
|
|
text-shadow:
|
|
0 0 20px var(--dante-glow),
|
|
0 0 40px var(--dante-glow);
|
|
}
|
|
50% {
|
|
text-shadow:
|
|
0 0 30px var(--dante-glow),
|
|
0 0 60px var(--dante-glow),
|
|
0 0 80px rgba(255, 215, 0, 0.6);
|
|
}
|
|
}
|
|
|
|
#messages {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 40px 20px;
|
|
max-width: 900px;
|
|
margin: 0 auto;
|
|
width: 100%;
|
|
position: relative;
|
|
}
|
|
|
|
#emptyState {
|
|
text-align: center;
|
|
padding-top: 120px;
|
|
}
|
|
|
|
.empty-icon {
|
|
width: 80px;
|
|
height: 80px;
|
|
background: linear-gradient(135deg, #ffd700, #ffed4e);
|
|
border-radius: 20px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 48px;
|
|
font-weight: 800;
|
|
color: #0a0e27;
|
|
margin-bottom: 20px;
|
|
box-shadow: 0 0 30px rgba(255, 215, 0, 0.6);
|
|
}
|
|
|
|
.empty-title {
|
|
font-size: 32px;
|
|
font-weight: 800;
|
|
font-family: "Orbitron", sans-serif;
|
|
color: #ffd700;
|
|
margin-bottom: 10px;
|
|
text-shadow: 0 0 20px var(--dante-glow);
|
|
}
|
|
|
|
.empty-subtitle {
|
|
color: #a0a0ff;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.message-container {
|
|
margin-bottom: 30px;
|
|
opacity: 0;
|
|
transform: translateY(20px);
|
|
}
|
|
|
|
.user-message {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
.user-message-content {
|
|
background: rgba(255, 215, 0, 0.2);
|
|
border: 1px solid rgba(255, 215, 0, 0.4);
|
|
border-radius: 18px;
|
|
padding: 14px 18px;
|
|
max-width: 70%;
|
|
color: #ffd700;
|
|
font-weight: 500;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
.assistant-message {
|
|
display: flex;
|
|
gap: 12px;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.assistant-avatar {
|
|
width: 36px;
|
|
height: 36px;
|
|
background: linear-gradient(135deg, #ffd700, #ffed4e);
|
|
border-radius: 8px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 18px;
|
|
font-weight: 800;
|
|
color: #0a0e27;
|
|
flex-shrink: 0;
|
|
box-shadow: 0 0 15px rgba(255, 215, 0, 0.5);
|
|
}
|
|
|
|
.assistant-message-content {
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border: 1px solid rgba(255, 215, 0, 0.2);
|
|
border-radius: 18px;
|
|
padding: 14px 18px;
|
|
flex: 1;
|
|
color: #e0e0e0;
|
|
line-height: 1.6;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
.thinking-indicator {
|
|
display: flex;
|
|
gap: 12px;
|
|
align-items: center;
|
|
padding: 14px 18px;
|
|
color: #ffd700;
|
|
}
|
|
|
|
.typing-dots {
|
|
display: flex;
|
|
gap: 6px;
|
|
}
|
|
|
|
.typing-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
background: #ffd700;
|
|
border-radius: 50%;
|
|
animation: bounce 1.4s infinite ease-in-out;
|
|
box-shadow: 0 0 10px rgba(255, 215, 0, 0.5);
|
|
}
|
|
|
|
.typing-dot:nth-child(1) {
|
|
animation-delay: -0.32s;
|
|
}
|
|
|
|
.typing-dot:nth-child(2) {
|
|
animation-delay: -0.16s;
|
|
}
|
|
|
|
@keyframes bounce {
|
|
0%,
|
|
80%,
|
|
100% {
|
|
transform: scale(0);
|
|
}
|
|
40% {
|
|
transform: scale(1);
|
|
}
|
|
}
|
|
|
|
footer {
|
|
background: rgba(10, 14, 39, 0.8);
|
|
backdrop-filter: blur(10px);
|
|
border-top: 1px solid rgba(255, 215, 0, 0.3);
|
|
padding: 20px 40px;
|
|
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.3);
|
|
position: relative;
|
|
}
|
|
|
|
.suggestions-container {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
margin-bottom: 10px;
|
|
justify-content: center;
|
|
}
|
|
|
|
.suggestion-button {
|
|
margin: 0 5px;
|
|
padding: 6px 10px;
|
|
background-color: #ffd700;
|
|
color: #0a0e27;
|
|
border: none;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.suggestion-button:hover {
|
|
transform: scale(1.05);
|
|
box-shadow: 0 0 8px rgba(255, 215, 0, 0.6);
|
|
}
|
|
|
|
.input-container {
|
|
display: flex;
|
|
gap: 12px;
|
|
max-width: 900px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
#messageInput {
|
|
flex: 1;
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border: 1px solid rgba(255, 215, 0, 0.2);
|
|
border-radius: 12px;
|
|
padding: 14px 18px;
|
|
color: #e0e0e0;
|
|
font-size: 15px;
|
|
outline: none;
|
|
transition: all 0.3s ease;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
#messageInput:focus {
|
|
border-color: rgba(255, 215, 0, 0.5);
|
|
background: rgba(255, 255, 255, 0.08);
|
|
box-shadow: 0 0 15px rgba(255, 215, 0, 0.3);
|
|
}
|
|
|
|
#messageInput::placeholder {
|
|
color: rgba(255, 215, 0, 0.4);
|
|
}
|
|
|
|
#sendBtn {
|
|
background: linear-gradient(135deg, #ffd700, #ffed4e);
|
|
border: none;
|
|
border-radius: 12px;
|
|
padding: 14px 28px;
|
|
color: #0a0e27;
|
|
font-weight: 700;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
box-shadow: 0 4px 12px rgba(255, 215, 0, 0.4);
|
|
}
|
|
|
|
#sendBtn:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 6px 20px rgba(255, 215, 0, 0.6);
|
|
}
|
|
|
|
#newChatBtn {
|
|
background: rgba(255, 215, 0, 0.2);
|
|
border: 1px solid rgba(255, 215, 0, 0.4);
|
|
border-radius: 10px;
|
|
padding: 10px 20px;
|
|
color: #ffd700;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
#newChatBtn:hover {
|
|
background: rgba(255, 215, 0, 0.3);
|
|
box-shadow: 0 0 15px rgba(255, 215, 0, 0.4);
|
|
}
|
|
|
|
.voice-status {
|
|
background: rgba(100, 255, 100, 0.2);
|
|
border: 1px solid rgba(100, 255, 100, 0.3);
|
|
padding: 12px 20px;
|
|
text-align: center;
|
|
color: #90ff90;
|
|
font-weight: 600;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
.warning-message {
|
|
background: rgba(255, 200, 0, 0.2);
|
|
border: 1px solid rgba(255, 200, 0, 0.3);
|
|
border-radius: 12px;
|
|
padding: 14px 18px;
|
|
margin-bottom: 20px;
|
|
color: #ffd700;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
/* Updated Connection Status - Small Flashing Circle */
|
|
.connection-status {
|
|
position: fixed;
|
|
top: 15px;
|
|
right: 15px;
|
|
width: 16px;
|
|
height: 16px;
|
|
border-radius: 50%;
|
|
z-index: 1000;
|
|
transition: all 0.3s ease;
|
|
box-shadow: 0 0 10px currentColor;
|
|
}
|
|
|
|
.connection-status.connecting {
|
|
background-color: #ffd700;
|
|
animation: connectingPulse 1.5s infinite;
|
|
}
|
|
|
|
.connection-status.connected {
|
|
background-color: #90ee90;
|
|
animation: connectedPulse 2s infinite;
|
|
}
|
|
|
|
.connection-status.disconnected {
|
|
background-color: #ff6b6b;
|
|
animation: none;
|
|
}
|
|
|
|
@keyframes connectingPulse {
|
|
0%,
|
|
100% {
|
|
opacity: 0.6;
|
|
transform: scale(0.8);
|
|
}
|
|
50% {
|
|
opacity: 1;
|
|
transform: scale(1);
|
|
}
|
|
}
|
|
|
|
@keyframes connectedPulse {
|
|
0%,
|
|
100% {
|
|
opacity: 0.8;
|
|
transform: scale(1);
|
|
}
|
|
50% {
|
|
opacity: 1;
|
|
transform: scale(1.1);
|
|
box-shadow: 0 0 15px #90ee90;
|
|
}
|
|
}
|
|
|
|
/* Markdown Styles */
|
|
.markdown-content h1,
|
|
.markdown-content h2,
|
|
.markdown-content h3 {
|
|
margin-top: 20px;
|
|
margin-bottom: 12px;
|
|
font-weight: 700;
|
|
color: #ffd700;
|
|
}
|
|
|
|
.markdown-content h1 {
|
|
font-size: 24px;
|
|
}
|
|
|
|
.markdown-content h2 {
|
|
font-size: 20px;
|
|
}
|
|
|
|
.markdown-content h3 {
|
|
font-size: 18px;
|
|
}
|
|
|
|
.markdown-content p {
|
|
margin-bottom: 12px;
|
|
line-height: 1.7;
|
|
}
|
|
|
|
.markdown-content ul,
|
|
.markdown-content ol {
|
|
margin-bottom: 12px;
|
|
padding-left: 24px;
|
|
}
|
|
|
|
.markdown-content li {
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.markdown-content code {
|
|
background: rgba(255, 215, 0, 0.1);
|
|
padding: 2px 6px;
|
|
border-radius: 4px;
|
|
font-family: "Courier New", monospace;
|
|
font-size: 14px;
|
|
color: #ffd700;
|
|
}
|
|
|
|
.markdown-content pre {
|
|
background: rgba(0, 0, 0, 0.3);
|
|
border: 1px solid rgba(255, 215, 0, 0.2);
|
|
border-radius: 8px;
|
|
padding: 16px;
|
|
overflow-x: auto;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.markdown-content pre code {
|
|
background: none;
|
|
padding: 0;
|
|
color: #e0e0e0;
|
|
}
|
|
|
|
.markdown-content table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.markdown-content table th,
|
|
.markdown-content table td {
|
|
border: 1px solid rgba(255, 215, 0, 0.2);
|
|
padding: 10px;
|
|
text-align: left;
|
|
}
|
|
|
|
.markdown-content table th {
|
|
background: rgba(255, 215, 0, 0.1);
|
|
font-weight: 600;
|
|
color: #ffd700;
|
|
}
|
|
|
|
.markdown-content blockquote {
|
|
border-left: 3px solid rgba(255, 215, 0, 0.5);
|
|
padding-left: 16px;
|
|
margin: 12px 0;
|
|
color: #b0b0ff;
|
|
}
|
|
|
|
.markdown-content strong {
|
|
font-weight: 700;
|
|
color: #ffd700;
|
|
}
|
|
|
|
.markdown-content em {
|
|
font-style: italic;
|
|
color: #b0b0ff;
|
|
}
|
|
|
|
.markdown-content a {
|
|
color: #ffd700;
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.markdown-content a:hover {
|
|
color: #ffed4e;
|
|
}
|
|
|
|
::-webkit-scrollbar {
|
|
width: 8px;
|
|
}
|
|
|
|
::-webkit-scrollbar-track {
|
|
background: rgba(255, 255, 255, 0.05);
|
|
}
|
|
|
|
::-webkit-scrollbar-thumb {
|
|
background: rgba(255, 215, 0, 0.3);
|
|
border-radius: 4px;
|
|
}
|
|
|
|
::-webkit-scrollbar-thumb:hover {
|
|
background: rgba(255, 215, 0, 0.5);
|
|
}
|
|
|
|
/* NEW STYLES FOR IMPROVEMENTS */
|
|
|
|
/* Scroll to bottom button */
|
|
.scroll-to-bottom {
|
|
position: absolute;
|
|
bottom: 20px;
|
|
right: 20px;
|
|
width: 40px;
|
|
height: 40px;
|
|
background: rgba(255, 215, 0, 0.8);
|
|
border: none;
|
|
border-radius: 50%;
|
|
color: #0a0e27;
|
|
font-size: 18px;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
transition: all 0.3s ease;
|
|
z-index: 10;
|
|
}
|
|
|
|
.scroll-to-bottom:hover {
|
|
background: rgba(255, 215, 0, 1);
|
|
transform: scale(1.1);
|
|
}
|
|
|
|
/* Continue button for interrupted responses */
|
|
.continue-button {
|
|
display: inline-block;
|
|
background: rgba(255, 215, 0, 0.2);
|
|
border: 1px solid rgba(255, 215, 0, 0.4);
|
|
border-radius: 8px;
|
|
padding: 8px 16px;
|
|
color: #ffd700;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
margin-top: 10px;
|
|
transition: all 0.3s ease;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.continue-button:hover {
|
|
background: rgba(255, 215, 0, 0.3);
|
|
box-shadow: 0 0 10px rgba(255, 215, 0, 0.4);
|
|
}
|
|
|
|
/* Context usage indicator */
|
|
.context-indicator {
|
|
position: fixed;
|
|
bottom: 90px;
|
|
right: 20px;
|
|
width: 120px;
|
|
background: rgba(10, 14, 39, 0.9);
|
|
border: 1px solid rgba(255, 215, 0, 0.3);
|
|
border-radius: 8px;
|
|
padding: 10px;
|
|
font-size: 12px;
|
|
text-align: center;
|
|
z-index: 100;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
.context-progress {
|
|
height: 6px;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-radius: 3px;
|
|
margin-top: 5px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.context-progress-bar {
|
|
height: 100%;
|
|
background: #90ee90;
|
|
border-radius: 3px;
|
|
transition:
|
|
width 0.3s ease,
|
|
background-color 0.3s ease;
|
|
}
|
|
|
|
.context-progress-bar.warning {
|
|
background: #ffd700;
|
|
}
|
|
|
|
.context-progress-bar.danger {
|
|
background: #ff6b6b;
|
|
}
|
|
|
|
/* Mobile Responsiveness */
|
|
@media (max-width: 768px) {
|
|
.sidebar {
|
|
width: 100%;
|
|
left: -100%;
|
|
}
|
|
|
|
.sidebar.open ~ .main {
|
|
margin-left: 0;
|
|
width: 100%;
|
|
}
|
|
|
|
header {
|
|
padding: 15px 20px 15px 60px;
|
|
}
|
|
|
|
.logo-text {
|
|
font-size: 18px;
|
|
}
|
|
|
|
.logo-icon {
|
|
width: 30px;
|
|
height: 30px;
|
|
font-size: 18px;
|
|
}
|
|
|
|
#newChatBtn {
|
|
padding: 8px 16px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.input-container {
|
|
padding: 0 10px;
|
|
}
|
|
|
|
#messageInput {
|
|
padding: 12px 16px;
|
|
}
|
|
|
|
#sendBtn {
|
|
padding: 12px 20px;
|
|
}
|
|
|
|
.user-message-content,
|
|
.assistant-message-content {
|
|
max-width: 85%;
|
|
}
|
|
|
|
.empty-title {
|
|
font-size: 24px;
|
|
}
|
|
|
|
.empty-icon {
|
|
width: 60px;
|
|
height: 60px;
|
|
font-size: 36px;
|
|
}
|
|
|
|
.connection-status {
|
|
top: 10px;
|
|
right: 10px;
|
|
width: 14px;
|
|
height: 14px;
|
|
}
|
|
|
|
.scroll-to-bottom {
|
|
width: 36px;
|
|
height: 36px;
|
|
font-size: 16px;
|
|
bottom: 15px;
|
|
right: 15px;
|
|
}
|
|
|
|
.context-indicator {
|
|
bottom: 70px;
|
|
right: 15px;
|
|
width: 100px;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 480px) {
|
|
.sidebar-toggle {
|
|
left: 10px;
|
|
top: 15px;
|
|
width: 40px;
|
|
height: 40px;
|
|
}
|
|
|
|
header {
|
|
padding: 10px 15px 10px 50px;
|
|
}
|
|
|
|
.logo-text {
|
|
font-size: 16px;
|
|
}
|
|
|
|
#newChatBtn {
|
|
padding: 6px 12px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.user-message-content,
|
|
.assistant-message-content {
|
|
max-width: 90%;
|
|
}
|
|
|
|
.empty-title {
|
|
font-size: 20px;
|
|
}
|
|
|
|
.scroll-to-bottom {
|
|
width: 32px;
|
|
height: 32px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.context-indicator {
|
|
bottom: 65px;
|
|
right: 10px;
|
|
width: 90px;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="connection-status connecting" id="connectionStatus"></div>
|
|
|
|
<button class="sidebar-toggle" onclick="toggleSidebar()">☰</button>
|
|
|
|
<div class="sidebar" id="sidebar">
|
|
<button class="new-chat" onclick="createNewSession()">
|
|
+ Novo Chat
|
|
</button>
|
|
<button
|
|
class="voice-toggle"
|
|
id="voiceToggle"
|
|
onclick="toggleVoiceMode()"
|
|
>
|
|
🎤 Modo Voz
|
|
</button>
|
|
<div class="history" id="history"></div>
|
|
</div>
|
|
|
|
<div class="main">
|
|
<header>
|
|
<div class="logo">
|
|
<div class="logo-icon">D</div>
|
|
<h1 class="logo-text neon-text">General Bots</h1>
|
|
</div>
|
|
<button id="newChatBtn">Novo Chat</button>
|
|
</header>
|
|
|
|
<div class="voice-status" id="voiceStatus" style="display: none">
|
|
🎤 Ouvindo... Fale agora
|
|
</div>
|
|
|
|
<main id="messages">
|
|
<div id="emptyState">
|
|
<div class="empty-icon">D</div>
|
|
<h2 class="empty-title neon-text">
|
|
Bem-vindo ao General Bots
|
|
</h2>
|
|
<p class="empty-subtitle">Seu assistente de IA avançado</p>
|
|
</div>
|
|
</main>
|
|
|
|
<footer>
|
|
<div class="input-container">
|
|
<input
|
|
id="messageInput"
|
|
type="text"
|
|
placeholder="Fale com General Bots..."
|
|
autofocus
|
|
/>
|
|
<button id="sendBtn">Enviar</button>
|
|
</div>
|
|
<script>
|
|
function handleSuggestions(suggestions) {
|
|
const footer = document.querySelector('footer');
|
|
let container = footer.querySelector('.suggestions-container');
|
|
|
|
if (!container) {
|
|
container = document.createElement('div');
|
|
container.className = 'suggestions-container';
|
|
footer.insertBefore(container, footer.firstChild);
|
|
}
|
|
|
|
container.innerHTML = '';
|
|
suggestions.forEach(s => {
|
|
const btn = document.createElement('button');
|
|
btn.textContent = s.text;
|
|
btn.className = 'suggestion-button';
|
|
btn.onclick = () => {
|
|
setContext(s.context);
|
|
input.value = ''; // Clear input after clicking
|
|
};
|
|
container.appendChild(btn);
|
|
});
|
|
}
|
|
|
|
let pendingContextChange = null;
|
|
|
|
async function setContext(context) {
|
|
try {
|
|
// Get the button text from the clicked suggestion
|
|
const buttonText = event?.target?.textContent || context;
|
|
|
|
// Add the suggestion as a user message to the chat
|
|
addMessage("user", buttonText);
|
|
|
|
// Clear the input field
|
|
const input = document.getElementById('messageInput');
|
|
if (input) {
|
|
input.value = '';
|
|
}
|
|
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
pendingContextChange = new Promise((resolve) => {
|
|
const handler = (event) => {
|
|
const response = JSON.parse(event.data);
|
|
if (response.message_type === 5 && response.context_name === context) {
|
|
ws.removeEventListener('message', handler);
|
|
resolve();
|
|
}
|
|
};
|
|
ws.addEventListener('message', handler);
|
|
|
|
const suggestionEvent = {
|
|
bot_id: currentBotId,
|
|
user_id: currentUserId,
|
|
session_id: currentSessionId,
|
|
channel: "web",
|
|
content: buttonText, // Send button text as content
|
|
message_type: 4, // custom type for suggestion click
|
|
is_suggestion: true,
|
|
context_name: context, // Context to switch to
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
ws.send(JSON.stringify(suggestionEvent));
|
|
});
|
|
|
|
await pendingContextChange;
|
|
// Update UI to show current context
|
|
const contextIndicator = document.getElementById('contextIndicator');
|
|
if (contextIndicator) {
|
|
contextIndicator.style.display = 'block';
|
|
document.getElementById('contextPercentage').textContent = context;
|
|
}
|
|
} else {
|
|
console.warn("WebSocket não está conectado. Tentando reconectar...");
|
|
connectWebSocket();
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to set context:', err);
|
|
}
|
|
}
|
|
|
|
async function sendMessage() {
|
|
if (pendingContextChange) {
|
|
await pendingContextChange;
|
|
pendingContextChange = null;
|
|
}
|
|
const message = input.value.trim();
|
|
if (!message || !ws || ws.readyState !== WebSocket.OPEN) {
|
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
showWarning("Conexão não disponível. Tentando reconectar...");
|
|
connectWebSocket();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (isThinking) {
|
|
hideThinkingIndicator();
|
|
}
|
|
|
|
addMessage("user", message);
|
|
|
|
const messageData = {
|
|
bot_id: currentBotId,
|
|
user_id: currentUserId,
|
|
session_id: currentSessionId,
|
|
channel: "web",
|
|
content: message,
|
|
message_type: 1,
|
|
media_url: null,
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
|
|
ws.send(JSON.stringify(messageData));
|
|
input.value = "";
|
|
input.focus();
|
|
}
|
|
</script>
|
|
</footer>
|
|
</div>
|
|
|
|
<!-- New elements for improvements -->
|
|
<button
|
|
class="scroll-to-bottom"
|
|
id="scrollToBottom"
|
|
style="display: none"
|
|
>
|
|
↓
|
|
</button>
|
|
|
|
<div
|
|
class="context-indicator"
|
|
id="contextIndicator"
|
|
style="display: none"
|
|
>
|
|
<div>Contexto</div>
|
|
<div id="contextPercentage">0%</div>
|
|
<div class="context-progress">
|
|
<div
|
|
class="context-progress-bar"
|
|
id="contextProgressBar"
|
|
style="width: 0%"
|
|
></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let ws = null;
|
|
let currentSessionId = null;
|
|
let currentUserId = null;
|
|
let currentBotId = "default_bot";
|
|
let isStreaming = false;
|
|
let voiceRoom = null;
|
|
let isVoiceMode = false;
|
|
let mediaRecorder = null;
|
|
let audioChunks = [];
|
|
let streamingMessageId = null;
|
|
let isThinking = false;
|
|
let currentStreamingContent = "";
|
|
let hasReceivedInitialMessage = false;
|
|
let reconnectAttempts = 0;
|
|
const maxReconnectAttempts = 5;
|
|
let reconnectTimeout = null;
|
|
let thinkingTimeout = null;
|
|
let lastMessageLength = 0;
|
|
let contextUsage = 0;
|
|
let isUserScrolling = false;
|
|
let autoScrollEnabled = true;
|
|
|
|
const messagesDiv = document.getElementById("messages");
|
|
const input = document.getElementById("messageInput");
|
|
const sendBtn = document.getElementById("sendBtn");
|
|
const newChatBtn = document.getElementById("newChatBtn");
|
|
const connectionStatus =
|
|
document.getElementById("connectionStatus");
|
|
const scrollToBottomBtn = document.getElementById("scrollToBottom");
|
|
const contextIndicator =
|
|
document.getElementById("contextIndicator");
|
|
const contextPercentage =
|
|
document.getElementById("contextPercentage");
|
|
const contextProgressBar =
|
|
document.getElementById("contextProgressBar");
|
|
|
|
marked.setOptions({
|
|
breaks: true,
|
|
gfm: true,
|
|
});
|
|
|
|
function toggleSidebar() {
|
|
document.getElementById("sidebar").classList.toggle("open");
|
|
}
|
|
|
|
function updateConnectionStatus(status) {
|
|
connectionStatus.className = `connection-status ${status}`;
|
|
}
|
|
|
|
function getWebSocketUrl() {
|
|
const protocol =
|
|
window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
// Generate UUIDs if not set yet
|
|
const sessionId = currentSessionId || crypto.randomUUID();
|
|
const userId = currentUserId || crypto.randomUUID();
|
|
return `${protocol}//${window.location.host}/ws?session_id=${sessionId}&user_id=${userId}`;
|
|
}
|
|
|
|
// Auto-focus on input when page loads
|
|
window.addEventListener("load", function () {
|
|
input.focus();
|
|
});
|
|
|
|
// Close sidebar when clicking outside on mobile
|
|
document.addEventListener("click", function (event) {
|
|
const sidebar = document.getElementById("sidebar");
|
|
const sidebarToggle = document.querySelector(".sidebar-toggle");
|
|
|
|
if (
|
|
window.innerWidth <= 768 &&
|
|
sidebar.classList.contains("open") &&
|
|
!sidebar.contains(event.target) &&
|
|
!sidebarToggle.contains(event.target)
|
|
) {
|
|
sidebar.classList.remove("open");
|
|
}
|
|
});
|
|
|
|
// Scroll management
|
|
messagesDiv.addEventListener("scroll", function () {
|
|
// Check if user is scrolling manually
|
|
const isAtBottom =
|
|
messagesDiv.scrollHeight - messagesDiv.scrollTop <=
|
|
messagesDiv.clientHeight + 100;
|
|
|
|
if (!isAtBottom) {
|
|
isUserScrolling = true;
|
|
showScrollToBottomButton();
|
|
} else {
|
|
isUserScrolling = false;
|
|
hideScrollToBottomButton();
|
|
}
|
|
});
|
|
|
|
function scrollToBottom() {
|
|
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
|
isUserScrolling = false;
|
|
hideScrollToBottomButton();
|
|
}
|
|
|
|
function showScrollToBottomButton() {
|
|
scrollToBottomBtn.style.display = "flex";
|
|
}
|
|
|
|
function hideScrollToBottomButton() {
|
|
scrollToBottomBtn.style.display = "none";
|
|
}
|
|
|
|
scrollToBottomBtn.addEventListener("click", scrollToBottom);
|
|
|
|
// Context usage management
|
|
function updateContextUsage(usage) {
|
|
contextUsage = usage;
|
|
const percentage = Math.min(100, Math.round(usage * 100));
|
|
|
|
contextPercentage.textContent = `${percentage}%`;
|
|
contextProgressBar.style.width = `${percentage}%`;
|
|
|
|
// Update color based on usage
|
|
if (percentage >= 90) {
|
|
contextProgressBar.className =
|
|
"context-progress-bar danger";
|
|
} else if (percentage >= 70) {
|
|
contextProgressBar.className =
|
|
"context-progress-bar warning";
|
|
} else {
|
|
contextProgressBar.className = "context-progress-bar";
|
|
}
|
|
|
|
// Show indicator if usage is above 50%
|
|
if (percentage >= 50) {
|
|
contextIndicator.style.display = "block";
|
|
} else {
|
|
contextIndicator.style.display = "none";
|
|
}
|
|
}
|
|
|
|
async function initializeAuth() {
|
|
try {
|
|
updateConnectionStatus("connecting");
|
|
// Extract bot name from URL path (first segment after /
|
|
const pathSegments = window.location.pathname.split('/').filter(s => s);
|
|
const botName = pathSegments.length > 0 ? pathSegments[0] : 'default';
|
|
|
|
const response = await fetch(`/api/auth?bot_name=${encodeURIComponent(botName)}`);
|
|
const authData = await response.json();
|
|
currentUserId = authData.user_id;
|
|
currentSessionId = authData.session_id;
|
|
connectWebSocket();
|
|
loadSessions();
|
|
} catch (error) {
|
|
console.error("Failed to initialize auth:", error);
|
|
updateConnectionStatus("disconnected");
|
|
setTimeout(initializeAuth, 3000);
|
|
}
|
|
}
|
|
|
|
|
|
async function loadSessions() {
|
|
try {
|
|
const response = await fetch("/api/sessions");
|
|
const sessions = await response.json();
|
|
const history = document.getElementById("history");
|
|
history.innerHTML = "";
|
|
} catch (error) {
|
|
console.error("Failed to load sessions:", error);
|
|
}
|
|
}
|
|
|
|
async function createNewSession() {
|
|
try {
|
|
const response = await fetch("/api/sessions", {
|
|
method: "POST",
|
|
});
|
|
const session = await response.json();
|
|
currentSessionId = session.session_id;
|
|
hasReceivedInitialMessage = false;
|
|
connectWebSocket();
|
|
loadSessions();
|
|
document.getElementById("messages").innerHTML = `
|
|
<div id="emptyState">
|
|
<div class="empty-icon">D</div>
|
|
<h2 class="empty-title neon-text">Bem-vindo ao General Bots</h2>
|
|
<p class="empty-subtitle">Seu assistente de IA avançado</p>
|
|
</div>
|
|
`;
|
|
// Reset context usage for new session
|
|
updateContextUsage(0);
|
|
if (isVoiceMode) {
|
|
await startVoiceSession();
|
|
}
|
|
|
|
// Close sidebar on mobile after creating new chat
|
|
if (window.innerWidth <= 768) {
|
|
document
|
|
.getElementById("sidebar")
|
|
.classList.remove("open");
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to create session:", error);
|
|
}
|
|
}
|
|
|
|
function switchSession(sessionId) {
|
|
currentSessionId = sessionId;
|
|
hasReceivedInitialMessage = false;
|
|
loadSessionHistory(sessionId);
|
|
connectWebSocket();
|
|
if (isVoiceMode) {
|
|
startVoiceSession();
|
|
}
|
|
|
|
// Close sidebar on mobile after switching session
|
|
if (window.innerWidth <= 768) {
|
|
document.getElementById("sidebar").classList.remove("open");
|
|
}
|
|
}
|
|
|
|
async function loadSessionHistory(sessionId) {
|
|
try {
|
|
const response = await fetch("/api/sessions/" + sessionId);
|
|
const history = await response.json();
|
|
const messages = document.getElementById("messages");
|
|
messages.innerHTML = "";
|
|
|
|
if (history.length === 0) {
|
|
// Show empty state if no history
|
|
messages.innerHTML = `
|
|
<div id="emptyState">
|
|
<div class="empty-icon">D</div>
|
|
<h2 class="empty-title neon-text">Bem-vindo ao General Bots</h2>
|
|
<p class="empty-subtitle">Seu assistente de IA avançado</p>
|
|
</div>
|
|
`;
|
|
updateContextUsage(0);
|
|
} else {
|
|
// Display existing history
|
|
history.forEach(([role, content]) => {
|
|
addMessage(role, content, false);
|
|
});
|
|
// Estimate context usage based on message count
|
|
updateContextUsage(history.length / 2); // Assuming 20 messages is 100% context
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to load session history:", error);
|
|
}
|
|
}
|
|
|
|
function connectWebSocket() {
|
|
if (ws) {
|
|
ws.close();
|
|
}
|
|
|
|
clearTimeout(reconnectTimeout);
|
|
const wsUrl = getWebSocketUrl();
|
|
ws = new WebSocket(wsUrl);
|
|
|
|
ws.onmessage = function (event) {
|
|
const response = JSON.parse(event.data);
|
|
|
|
// Update current bot_id if provided in the message
|
|
if (response.bot_id) {
|
|
currentBotId = response.bot_id;
|
|
}
|
|
|
|
if (response.message_type === 2) {
|
|
const eventData = JSON.parse(response.content);
|
|
handleEvent(eventData.event, eventData.data);
|
|
return;
|
|
}
|
|
|
|
processMessageContent(response);
|
|
};
|
|
|
|
ws.onopen = function () {
|
|
console.log("Connected to WebSocket");
|
|
updateConnectionStatus("connected");
|
|
reconnectAttempts = 0;
|
|
// Reset the flag when connection is established
|
|
hasReceivedInitialMessage = false;
|
|
};
|
|
|
|
ws.onclose = function (event) {
|
|
console.log(
|
|
"WebSocket disconnected:",
|
|
event.code,
|
|
event.reason,
|
|
);
|
|
updateConnectionStatus("disconnected");
|
|
|
|
// If we were streaming and connection was lost, show continue button
|
|
if (isStreaming) {
|
|
showContinueButton();
|
|
}
|
|
|
|
if (reconnectAttempts < maxReconnectAttempts) {
|
|
reconnectAttempts++;
|
|
const delay = Math.min(1000 * reconnectAttempts, 10000);
|
|
console.log(
|
|
`Reconnecting in ${delay}ms... (attempt ${reconnectAttempts})`,
|
|
);
|
|
|
|
reconnectTimeout = setTimeout(() => {
|
|
updateConnectionStatus("connecting");
|
|
connectWebSocket();
|
|
}, delay);
|
|
} else {
|
|
updateConnectionStatus("disconnected");
|
|
}
|
|
};
|
|
|
|
ws.onerror = function (error) {
|
|
console.error("WebSocket error:", error);
|
|
updateConnectionStatus("disconnected");
|
|
};
|
|
}
|
|
|
|
function processMessageContent(response) {
|
|
// Clear empty state when we receive any message
|
|
const emptyState = document.getElementById("emptyState");
|
|
if (emptyState) {
|
|
emptyState.remove();
|
|
}
|
|
|
|
// Handle context usage if provided
|
|
if (response.context_usage !== undefined) {
|
|
updateContextUsage(response.context_usage);
|
|
}
|
|
|
|
// Handle suggestions if present in any message
|
|
if (response.suggestions && response.suggestions.length > 0) {
|
|
handleSuggestions(response.suggestions);
|
|
}
|
|
|
|
// Handle complete messages
|
|
if (response.is_complete) {
|
|
if (isStreaming) {
|
|
finalizeStreamingMessage();
|
|
isStreaming = false;
|
|
streamingMessageId = null;
|
|
currentStreamingContent = "";
|
|
} else {
|
|
// This is a complete message that wasn't being streamed
|
|
addMessage("assistant", response.content, false);
|
|
}
|
|
} else {
|
|
// Handle streaming messages
|
|
if (!isStreaming) {
|
|
isStreaming = true;
|
|
streamingMessageId = "streaming-" + Date.now();
|
|
currentStreamingContent = response.content || "";
|
|
addMessage(
|
|
"assistant",
|
|
currentStreamingContent,
|
|
true,
|
|
streamingMessageId,
|
|
);
|
|
} else {
|
|
currentStreamingContent += response.content || "";
|
|
updateStreamingMessage(currentStreamingContent);
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleEvent(eventType, eventData) {
|
|
console.log("Event received:", eventType, eventData);
|
|
switch (eventType) {
|
|
case "thinking_start":
|
|
showThinkingIndicator();
|
|
break;
|
|
case "thinking_end":
|
|
hideThinkingIndicator();
|
|
break;
|
|
case "warn":
|
|
showWarning(eventData.message);
|
|
break;
|
|
case "context_usage":
|
|
updateContextUsage(eventData.usage);
|
|
break;
|
|
}
|
|
}
|
|
|
|
function showThinkingIndicator() {
|
|
if (isThinking) return;
|
|
const emptyState = document.getElementById("emptyState");
|
|
if (emptyState) emptyState.remove();
|
|
|
|
const thinkingDiv = document.createElement("div");
|
|
thinkingDiv.id = "thinking-indicator";
|
|
thinkingDiv.className = "message-container";
|
|
thinkingDiv.innerHTML = `
|
|
<div class="assistant-message">
|
|
<div class="assistant-avatar">D</div>
|
|
<div class="thinking-indicator">
|
|
<div class="typing-dots">
|
|
<div class="typing-dot"></div>
|
|
<div class="typing-dot"></div>
|
|
<div class="typing-dot"></div>
|
|
</div>
|
|
<span>Pensando...</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
messagesDiv.appendChild(thinkingDiv);
|
|
|
|
gsap.to(thinkingDiv, {
|
|
opacity: 1,
|
|
y: 0,
|
|
duration: 0.4,
|
|
ease: "power2.out",
|
|
});
|
|
|
|
// Auto-scroll to show thinking indicator
|
|
if (!isUserScrolling) {
|
|
scrollToBottom();
|
|
} else {
|
|
showScrollToBottomButton();
|
|
}
|
|
|
|
// Set timeout to automatically hide thinking indicator after 30 seconds
|
|
// This handles cases where the server restarts and doesn't send thinking_end
|
|
thinkingTimeout = setTimeout(() => {
|
|
if (isThinking) {
|
|
hideThinkingIndicator();
|
|
showWarning(
|
|
"O servidor pode estar ocupado. A resposta está demorando demais.",
|
|
);
|
|
}
|
|
}, 60000);
|
|
|
|
isThinking = true;
|
|
}
|
|
|
|
function hideThinkingIndicator() {
|
|
if (!isThinking) return;
|
|
const thinkingDiv =
|
|
document.getElementById("thinking-indicator");
|
|
if (thinkingDiv) {
|
|
gsap.to(thinkingDiv, {
|
|
opacity: 0,
|
|
duration: 0.2,
|
|
onComplete: () => {
|
|
if (thinkingDiv.parentNode) {
|
|
thinkingDiv.remove();
|
|
}
|
|
},
|
|
});
|
|
}
|
|
// Clear the timeout if thinking ends normally
|
|
if (thinkingTimeout) {
|
|
clearTimeout(thinkingTimeout);
|
|
thinkingTimeout = null;
|
|
}
|
|
isThinking = false;
|
|
}
|
|
|
|
function showWarning(message) {
|
|
const warningDiv = document.createElement("div");
|
|
warningDiv.className = "warning-message";
|
|
warningDiv.innerHTML = `⚠️ ${message}`;
|
|
messagesDiv.appendChild(warningDiv);
|
|
|
|
gsap.from(warningDiv, {
|
|
opacity: 0,
|
|
y: 20,
|
|
duration: 0.4,
|
|
ease: "power2.out",
|
|
});
|
|
|
|
if (!isUserScrolling) {
|
|
scrollToBottom();
|
|
} else {
|
|
showScrollToBottomButton();
|
|
}
|
|
|
|
setTimeout(() => {
|
|
if (warningDiv.parentNode) {
|
|
gsap.to(warningDiv, {
|
|
opacity: 0,
|
|
duration: 0.3,
|
|
onComplete: () => warningDiv.remove(),
|
|
});
|
|
}
|
|
}, 5000);
|
|
}
|
|
|
|
function showContinueButton() {
|
|
const continueDiv = document.createElement("div");
|
|
continueDiv.className = "message-container";
|
|
continueDiv.innerHTML = `
|
|
<div class="assistant-message">
|
|
<div class="assistant-avatar">D</div>
|
|
<div class="assistant-message-content">
|
|
<p>A conexão foi interrompida. Clique em "Continuar" para tentar recuperar a resposta.</p>
|
|
<button class="continue-button" onclick="continueInterruptedResponse()">Continuar</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
messagesDiv.appendChild(continueDiv);
|
|
|
|
gsap.to(continueDiv, {
|
|
opacity: 1,
|
|
y: 0,
|
|
duration: 0.5,
|
|
ease: "power2.out",
|
|
});
|
|
|
|
if (!isUserScrolling) {
|
|
scrollToBottom();
|
|
} else {
|
|
showScrollToBottomButton();
|
|
}
|
|
}
|
|
|
|
function continueInterruptedResponse() {
|
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
connectWebSocket();
|
|
}
|
|
|
|
// Send a continue request to the server
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
const continueData = {
|
|
bot_id: "default_bot",
|
|
user_id: currentUserId,
|
|
session_id: currentSessionId,
|
|
channel: "web",
|
|
content: "continue",
|
|
message_type: 3, // Special message type for continue requests
|
|
media_url: null,
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
|
|
ws.send(JSON.stringify(continueData));
|
|
}
|
|
|
|
// Remove the continue button
|
|
const continueButtons =
|
|
document.querySelectorAll(".continue-button");
|
|
continueButtons.forEach((button) => {
|
|
button.parentElement.parentElement.parentElement.remove();
|
|
});
|
|
}
|
|
|
|
function addMessage(
|
|
role,
|
|
content,
|
|
streaming = false,
|
|
msgId = null,
|
|
) {
|
|
const emptyState = document.getElementById("emptyState");
|
|
if (emptyState) {
|
|
gsap.to(emptyState, {
|
|
opacity: 0,
|
|
y: -20,
|
|
duration: 0.3,
|
|
onComplete: () => emptyState.remove(),
|
|
});
|
|
}
|
|
|
|
const msg = document.createElement("div");
|
|
msg.className = "message-container";
|
|
|
|
if (role === "user") {
|
|
msg.innerHTML = `
|
|
<div class="user-message">
|
|
<div class="user-message-content">${escapeHtml(content)}</div>
|
|
</div>
|
|
`;
|
|
// Update context usage when user sends a message
|
|
updateContextUsage(contextUsage + 0.05); // Simulate 5% increase per message
|
|
} else if (role === "assistant") {
|
|
msg.innerHTML = `
|
|
<div class="assistant-message">
|
|
<div class="assistant-avatar">D</div>
|
|
<div class="assistant-message-content markdown-content" id="${msgId || ""}">
|
|
${streaming ? "" : marked.parse(content)}
|
|
</div>
|
|
</div>
|
|
`;
|
|
// Update context usage when assistant responds
|
|
updateContextUsage(contextUsage + 0.03); // Simulate 3% increase per response
|
|
} else if (role === "voice") {
|
|
msg.innerHTML = `
|
|
<div class="assistant-message">
|
|
<div class="assistant-avatar">🎤</div>
|
|
<div class="assistant-message-content">${content}</div>
|
|
</div>
|
|
`;
|
|
} else {
|
|
msg.innerHTML = `
|
|
<div class="assistant-message">
|
|
<div class="assistant-avatar">D</div>
|
|
<div class="assistant-message-content">${content}</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
messagesDiv.appendChild(msg);
|
|
|
|
gsap.to(msg, {
|
|
opacity: 1,
|
|
y: 0,
|
|
duration: 0.5,
|
|
ease: "power2.out",
|
|
});
|
|
|
|
// Auto-scroll to bottom if user isn't manually scrolling
|
|
if (!isUserScrolling) {
|
|
scrollToBottom();
|
|
} else {
|
|
showScrollToBottomButton();
|
|
}
|
|
}
|
|
|
|
function updateStreamingMessage(content) {
|
|
const msgElement = document.getElementById(streamingMessageId);
|
|
if (msgElement) {
|
|
msgElement.innerHTML = marked.parse(content);
|
|
|
|
// Auto-scroll to bottom if user isn't manually scrolling
|
|
if (!isUserScrolling) {
|
|
scrollToBottom();
|
|
} else {
|
|
showScrollToBottomButton();
|
|
}
|
|
}
|
|
}
|
|
|
|
function finalizeStreamingMessage() {
|
|
const msgElement = document.getElementById(streamingMessageId);
|
|
if (msgElement) {
|
|
msgElement.innerHTML = marked.parse(
|
|
currentStreamingContent,
|
|
);
|
|
msgElement.removeAttribute("id");
|
|
|
|
// Auto-scroll to bottom if user isn't manually scrolling
|
|
if (!isUserScrolling) {
|
|
scrollToBottom();
|
|
} else {
|
|
showScrollToBottomButton();
|
|
}
|
|
}
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement("div");
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function sendMessage() {
|
|
const message = input.value.trim();
|
|
if (!message || !ws || ws.readyState !== WebSocket.OPEN) {
|
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
showWarning(
|
|
"Conexão não disponível. Tentando reconectar...",
|
|
);
|
|
connectWebSocket();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (isThinking) {
|
|
hideThinkingIndicator();
|
|
}
|
|
|
|
addMessage("user", message);
|
|
|
|
const messageData = {
|
|
bot_id: "default_bot",
|
|
user_id: currentUserId,
|
|
session_id: currentSessionId,
|
|
channel: "web",
|
|
content: message,
|
|
message_type: 1,
|
|
media_url: null,
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
|
|
ws.send(JSON.stringify(messageData));
|
|
input.value = "";
|
|
input.focus(); // Keep focus on input after sending
|
|
}
|
|
|
|
sendBtn.onclick = sendMessage;
|
|
input.addEventListener("keypress", (e) => {
|
|
if (e.key === "Enter") sendMessage();
|
|
});
|
|
newChatBtn.onclick = () => createNewSession();
|
|
|
|
async function toggleVoiceMode() {
|
|
isVoiceMode = !isVoiceMode;
|
|
const voiceToggle = document.getElementById("voiceToggle");
|
|
const voiceStatus = document.getElementById("voiceStatus");
|
|
|
|
if (isVoiceMode) {
|
|
voiceToggle.textContent = "🔴 Parar Voz";
|
|
voiceToggle.classList.add("recording");
|
|
voiceStatus.style.display = "block";
|
|
await startVoiceSession();
|
|
} else {
|
|
voiceToggle.textContent = "🎤 Modo Voz";
|
|
voiceToggle.classList.remove("recording");
|
|
voiceStatus.style.display = "none";
|
|
await stopVoiceSession();
|
|
}
|
|
|
|
// Close sidebar on mobile after toggling voice mode
|
|
if (window.innerWidth <= 768) {
|
|
document.getElementById("sidebar").classList.remove("open");
|
|
}
|
|
}
|
|
|
|
async function startVoiceSession() {
|
|
if (!currentSessionId) return;
|
|
try {
|
|
const response = await fetch("/api/voice/start", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
session_id: currentSessionId,
|
|
user_id: currentUserId,
|
|
}),
|
|
});
|
|
const data = await response.json();
|
|
if (data.token) {
|
|
await connectToVoiceRoom(data.token);
|
|
startVoiceRecording();
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to start voice session:", error);
|
|
showWarning("Falha ao iniciar modo de voz");
|
|
}
|
|
}
|
|
|
|
async function stopVoiceSession() {
|
|
if (!currentSessionId) return;
|
|
try {
|
|
await fetch("/api/voice/stop", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
session_id: currentSessionId,
|
|
}),
|
|
});
|
|
if (voiceRoom) {
|
|
voiceRoom.disconnect();
|
|
voiceRoom = null;
|
|
}
|
|
if (mediaRecorder && mediaRecorder.state === "recording") {
|
|
mediaRecorder.stop();
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to stop voice session:", error);
|
|
}
|
|
}
|
|
|
|
async function connectToVoiceRoom(token) {
|
|
try {
|
|
const room = new LiveKitClient.Room();
|
|
// Use o mesmo esquema (ws/wss) do WebSocket principal
|
|
const protocol =
|
|
window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
const voiceUrl = `${protocol}//${window.location.host}/voice`;
|
|
await room.connect(voiceUrl, token);
|
|
voiceRoom = room;
|
|
|
|
room.on("dataReceived", (data) => {
|
|
const decoder = new TextDecoder();
|
|
const message = decoder.decode(data);
|
|
try {
|
|
const parsed = JSON.parse(message);
|
|
if (parsed.type === "voice_response") {
|
|
addMessage("assistant", parsed.text);
|
|
}
|
|
} catch (e) {
|
|
console.log("Voice data:", message);
|
|
}
|
|
});
|
|
|
|
const localTracks = await LiveKitClient.createLocalTracks({
|
|
audio: true,
|
|
video: false,
|
|
});
|
|
for (const track of localTracks) {
|
|
await room.localParticipant.publishTrack(track);
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to connect to voice room:", error);
|
|
showWarning("Falha na conexão de voz");
|
|
}
|
|
}
|
|
|
|
function startVoiceRecording() {
|
|
if (!navigator.mediaDevices) {
|
|
console.log("Media devices not supported");
|
|
return;
|
|
}
|
|
|
|
navigator.mediaDevices
|
|
.getUserMedia({ audio: true })
|
|
.then((stream) => {
|
|
mediaRecorder = new MediaRecorder(stream);
|
|
audioChunks = [];
|
|
|
|
mediaRecorder.ondataavailable = (event) => {
|
|
audioChunks.push(event.data);
|
|
};
|
|
|
|
mediaRecorder.onstop = () => {
|
|
const audioBlob = new Blob(audioChunks, {
|
|
type: "audio/wav",
|
|
});
|
|
simulateVoiceTranscription();
|
|
};
|
|
|
|
mediaRecorder.start();
|
|
setTimeout(() => {
|
|
if (
|
|
mediaRecorder &&
|
|
mediaRecorder.state === "recording"
|
|
) {
|
|
mediaRecorder.stop();
|
|
setTimeout(() => {
|
|
if (isVoiceMode) {
|
|
startVoiceRecording();
|
|
}
|
|
}, 1000);
|
|
}
|
|
}, 5000);
|
|
})
|
|
.catch((error) => {
|
|
console.error("Error accessing microphone:", error);
|
|
showWarning("Erro ao acessar microfone");
|
|
});
|
|
}
|
|
|
|
function simulateVoiceTranscription() {
|
|
const phrases = [
|
|
"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 =
|
|
phrases[Math.floor(Math.random() * phrases.length)];
|
|
|
|
if (voiceRoom) {
|
|
const message = {
|
|
type: "voice_input",
|
|
content: randomPhrase,
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
voiceRoom.localParticipant.publishData(
|
|
new TextEncoder().encode(JSON.stringify(message)),
|
|
LiveKitClient.DataPacketKind.RELIABLE,
|
|
);
|
|
}
|
|
addMessage("voice", `🎤 ${randomPhrase}`);
|
|
}
|
|
|
|
// Inicializar quando a página carregar
|
|
window.addEventListener("load", initializeAuth);
|
|
|
|
// Tentar reconectar quando a página ganhar foco
|
|
window.addEventListener("focus", function () {
|
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
connectWebSocket();
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|