Updated references from `redis_client`, `s3_client`, and `custom_conn` to unified names `cache`, `drive`, and `conn` for consistency across modules. Adjusted `add_suggestion_keyword` to use clearer parameter naming and enhanced custom syntax registration for better readability and maintainability.
1910 lines
67 KiB
HTML
1910 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);
|
|
}
|
|
|
|
.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 container = document.getElementById('suggestions-container');
|
|
container.innerHTML = '';
|
|
suggestions.forEach(s => {
|
|
const btn = document.createElement('button');
|
|
btn.textContent = s.text;
|
|
btn.className = 'suggestion-button';
|
|
btn.style.margin = '5px';
|
|
btn.style.padding = '8px 12px';
|
|
btn.style.backgroundColor = '#ffd700';
|
|
btn.style.color = '#0a0e27';
|
|
btn.style.border = 'none';
|
|
btn.style.borderRadius = '6px';
|
|
btn.style.cursor = 'pointer';
|
|
btn.onclick = () => setContext(s.context);
|
|
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;
|
|
|
|
// Set the input field value to the button text
|
|
const input = document.getElementById('messageInput');
|
|
if (input) {
|
|
input.value = buttonText;
|
|
}
|
|
|
|
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: context,
|
|
message_type: 4, // custom type for suggestion click
|
|
is_suggestion: true,
|
|
context_name: context,
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
ws.send(JSON.stringify(suggestionEvent));
|
|
});
|
|
|
|
await pendingContextChange;
|
|
alert(`Contexto alterado para: ${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();
|
|
await triggerStartScript();
|
|
} catch (error) {
|
|
console.error("Failed to initialize auth:", error);
|
|
updateConnectionStatus("disconnected");
|
|
setTimeout(initializeAuth, 3000);
|
|
}
|
|
}
|
|
|
|
async function triggerStartScript() {
|
|
if (!currentSessionId) return;
|
|
try {
|
|
await fetch("/api/start", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
session_id: currentSessionId,
|
|
}),
|
|
});
|
|
} catch (error) {
|
|
console.error("Failed to trigger start script:", error);
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
await triggerStartScript();
|
|
|
|
// 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 suggestion messages
|
|
if (response.message_type === 3) {
|
|
handleSuggestions(response.suggestions);
|
|
return;
|
|
}
|
|
|
|
// 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>
|