Spaces:
Sleeping
Sleeping
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>PuppyCompanion - AI Assistant</title> | |
<!-- Marked.js for Markdown rendering --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/4.3.0/marked.min.js"></script> | |
<style> | |
* { | |
margin: 0; | |
padding: 0; | |
box-sizing: border-box; | |
} | |
body { | |
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; | |
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 50%, #cbd5e1 100%); | |
height: 100vh; | |
overflow: hidden; | |
} | |
.container { | |
display: flex; | |
height: 100vh; | |
max-width: 1400px; | |
margin: 0 auto; | |
background: rgba(255, 255, 255, 0.1); | |
backdrop-filter: blur(10px); | |
border-radius: 20px; | |
overflow: hidden; | |
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); | |
gap: 15px; | |
padding: 15px; | |
} | |
/* Mobile Interface (Left) */ | |
.mobile-interface { | |
flex: 1; | |
max-width: 400px; | |
background: linear-gradient(to bottom, #1e1e1e, #2d2d2d); | |
display: flex; | |
flex-direction: column; | |
position: relative; | |
border-radius: 20px; | |
overflow: hidden; | |
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); | |
} | |
.mobile-header { | |
background: linear-gradient(135deg, #FF8A65 0%, #FFAB91 50%, #FFCCBC 100%); | |
padding: 20px; | |
text-align: center; | |
border-bottom: 1px solid rgba(255, 255, 255, 0.1); | |
} | |
.mobile-header h1 { | |
color: #ffffff; | |
font-size: 1.5rem; | |
font-weight: 600; | |
margin-bottom: 5px; | |
} | |
.mobile-header p { | |
color: #a0a0a0; | |
font-size: 0.9rem; | |
} | |
.chat-container { | |
flex: 1; | |
display: flex; | |
flex-direction: column; | |
padding: 20px; | |
overflow: hidden; | |
} | |
.messages { | |
flex: 1; | |
overflow-y: auto; | |
margin-bottom: 20px; | |
padding-right: 5px; | |
} | |
.messages::-webkit-scrollbar { | |
width: 4px; | |
} | |
.messages::-webkit-scrollbar-track { | |
background: rgba(255, 255, 255, 0.1); | |
border-radius: 2px; | |
} | |
.messages::-webkit-scrollbar-thumb { | |
background: rgba(255, 255, 255, 0.3); | |
border-radius: 2px; | |
} | |
.message { | |
margin-bottom: 15px; | |
animation: slideIn 0.3s ease-out; | |
} | |
@keyframes slideIn { | |
from { | |
opacity: 0; | |
transform: translateY(20px); | |
} | |
to { | |
opacity: 1; | |
transform: translateY(0); | |
} | |
} | |
.message.user { | |
text-align: right; | |
} | |
.message.bot { | |
text-align: left; | |
} | |
.message-content { | |
display: inline-block; | |
max-width: 85%; | |
padding: 12px 16px; | |
border-radius: 18px; | |
word-wrap: break-word; | |
line-height: 1.4; | |
} | |
.message.user .message-content { | |
background: linear-gradient(135deg, #FF7043, #FF8A65); | |
color: white; | |
} | |
.message.bot .message-content { | |
background: rgba(255, 255, 255, 0.1); | |
color: #ffffff; | |
border: 1px solid rgba(255, 255, 255, 0.2); | |
} | |
/* Markdown styling for bot messages */ | |
.message.bot .message-content h1, | |
.message.bot .message-content h2, | |
.message.bot .message-content h3 { | |
color: #ffffff; | |
margin: 8px 0 4px 0; | |
font-weight: 600; | |
} | |
.message.bot .message-content h1 { | |
font-size: 1.2em; | |
} | |
.message.bot .message-content h2 { | |
font-size: 1.1em; | |
} | |
.message.bot .message-content h3 { | |
font-size: 1em; | |
} | |
.message.bot .message-content ul, | |
.message.bot .message-content ol { | |
margin: 8px 0; | |
padding-left: 20px; | |
} | |
.message.bot .message-content li { | |
margin: 2px 0; | |
} | |
.message.bot .message-content p { | |
margin: 6px 0; | |
line-height: 1.4; | |
} | |
.message.bot .message-content strong { | |
color: #ffffff; | |
font-weight: 600; | |
} | |
.message.bot .message-content em { | |
font-style: italic; | |
color: rgba(255, 255, 255, 0.9); | |
} | |
.message.bot .message-content code { | |
background: rgba(0, 0, 0, 0.3); | |
padding: 2px 4px; | |
border-radius: 3px; | |
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; | |
font-size: 0.9em; | |
} | |
.message.bot .message-content blockquote { | |
border-left: 3px solid rgba(255, 255, 255, 0.3); | |
margin: 8px 0; | |
padding-left: 12px; | |
font-style: italic; | |
color: rgba(255, 255, 255, 0.8); | |
} | |
.input-container { | |
display: flex; | |
gap: 10px; | |
padding: 15px; | |
background: rgba(255, 255, 255, 0.05); | |
border-radius: 25px; | |
border: 1px solid rgba(255, 255, 255, 0.1); | |
} | |
.input-container input { | |
flex: 1; | |
background: transparent; | |
border: none; | |
color: white; | |
font-size: 16px; | |
outline: none; | |
padding: 10px 15px; | |
} | |
.input-container input::placeholder { | |
color: rgba(255, 255, 255, 0.6); | |
} | |
.input-container button { | |
background: linear-gradient(135deg, #FF7043, #FF8A65); | |
border: none; | |
color: white; | |
padding: 10px 20px; | |
border-radius: 20px; | |
cursor: pointer; | |
font-weight: 600; | |
transition: all 0.3s ease; | |
} | |
.input-container button:hover { | |
transform: scale(1.05); | |
box-shadow: 0 5px 15px rgba(255, 112, 67, 0.4); | |
} | |
.input-container button:disabled { | |
opacity: 0.6; | |
cursor: not-allowed; | |
transform: none; | |
} | |
.loading { | |
display: none; | |
text-align: center; | |
color: rgba(255, 255, 255, 0.7); | |
font-style: italic; | |
margin: 10px 0; | |
} | |
.loading.show { | |
display: block; | |
} | |
/* Debug Terminal (Right) */ | |
.debug-terminal { | |
flex: 1; | |
background: #1a1a1a; | |
display: flex; | |
flex-direction: column; | |
border-radius: 20px; | |
overflow: hidden; | |
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); | |
} | |
.terminal-header { | |
background: #2d2d2d; | |
padding: 15px 20px; | |
border-bottom: 1px solid #404040; | |
display: flex; | |
align-items: center; | |
} | |
.terminal-title { | |
color: #ffffff; | |
font-weight: 600; | |
} | |
.terminal-content { | |
flex: 1; | |
overflow-y: auto; | |
padding: 20px; | |
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; | |
font-size: 13px; | |
line-height: 1.5; | |
} | |
.terminal-content::-webkit-scrollbar { | |
width: 8px; | |
} | |
.terminal-content::-webkit-scrollbar-track { | |
background: #2d2d2d; | |
} | |
.terminal-content::-webkit-scrollbar-thumb { | |
background: #555; | |
border-radius: 4px; | |
} | |
.log-item { | |
margin-bottom: 8px; | |
padding: 8px 12px; | |
border-radius: 6px; | |
border-left: 3px solid #666; | |
font-size: 12px; | |
animation: logSlide 0.3s ease-out; | |
} | |
@keyframes logSlide { | |
from { | |
opacity: 0; | |
transform: translateX(-20px); | |
} | |
to { | |
opacity: 1; | |
transform: translateX(0); | |
} | |
} | |
.log-content { | |
display: flex; | |
align-items: flex-start; | |
gap: 8px; | |
} | |
.log-timestamp { | |
flex-shrink: 0; | |
color: #888; | |
font-size: 11px; | |
min-width: 60px; | |
} | |
.log-message { | |
flex: 1; | |
word-wrap: break-word; | |
} | |
/* Status indicator */ | |
.status-indicator { | |
position: fixed; | |
top: 20px; | |
right: 20px; | |
padding: 8px 16px; | |
background: rgba(0, 0, 0, 0.8); | |
color: white; | |
border-radius: 20px; | |
font-size: 12px; | |
z-index: 1000; | |
transition: all 0.3s ease; | |
} | |
.status-indicator.connected { | |
background: rgba(39, 174, 96, 0.9); | |
} | |
.status-indicator.disconnected { | |
background: rgba(231, 76, 60, 0.9); | |
} | |
/* Responsive */ | |
@media (max-width: 768px) { | |
.container { | |
flex-direction: column; | |
border-radius: 0; | |
} | |
.mobile-interface { | |
max-width: none; | |
border-radius: 0; | |
} | |
.debug-terminal { | |
border-radius: 0; | |
height: 40vh; | |
} | |
} | |
/* Smooth transitions */ | |
* { | |
transition: background-color 0.3s ease; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="status-indicator" id="status">Connecting...</div> | |
<div class="container"> | |
<!-- Mobile Interface --> | |
<div class="mobile-interface"> | |
<div class="mobile-header"> | |
<h1>🐶 PuppyCompanion</h1> | |
<p>Your AI assistant for puppy care</p> | |
</div> | |
<div class="chat-container"> | |
<div class="messages" id="messages"> | |
<div class="message bot"> | |
<div class="message-content"> | |
Hello! I'm your AI assistant specialized in puppy care and training. Ask me anything about your furry friend! 🐾 | |
</div> | |
</div> | |
</div> | |
<div class="loading" id="loading"> | |
<div>Thinking...</div> | |
</div> | |
<div class="input-container"> | |
<input type="text" id="messageInput" placeholder="Ask me about your puppy" maxlength="500"> | |
<button id="sendButton" onclick="sendMessage()">Send</button> | |
</div> | |
</div> | |
</div> | |
<!-- Debug Terminal --> | |
<div class="debug-terminal"> | |
<div class="terminal-header"> | |
<div class="terminal-title">Debug Console</div> | |
</div> | |
<div class="terminal-content" id="logContainer"> | |
<div class="log-item" style="border-left-color: #10b981; background: rgba(16, 185, 129, 0.1);"> | |
<div class="log-content"> | |
<span class="log-timestamp">00:00:00</span> | |
<span class="log-message" style="color: #10b981;">System ready - Connecting to backend...</span> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<script> | |
let ws = null; | |
let isConnected = false; | |
function connectWebSocket() { | |
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; | |
const wsUrl = `${protocol}//${window.location.host}/ws`; | |
ws = new WebSocket(wsUrl); | |
ws.onopen = function(event) { | |
isConnected = true; | |
updateStatus('Connected', 'connected'); | |
addLogMessage(getCurrentTime(), 'WebSocket connected to debug console', 'success'); | |
}; | |
ws.onmessage = function(event) { | |
const data = JSON.parse(event.data); | |
addLogMessage(data.timestamp, data.message, data.type); | |
}; | |
ws.onclose = function(event) { | |
isConnected = false; | |
updateStatus('Disconnected', 'disconnected'); | |
addLogMessage(getCurrentTime(), 'WebSocket connection closed', 'error'); | |
// Reconnect after 3 seconds | |
setTimeout(connectWebSocket, 3000); | |
}; | |
ws.onerror = function(error) { | |
addLogMessage(getCurrentTime(), 'WebSocket error occurred', 'error'); | |
}; | |
} | |
function updateStatus(text, className) { | |
const status = document.getElementById('status'); | |
status.textContent = text; | |
status.className = `status-indicator ${className}`; | |
} | |
function getCurrentTime() { | |
return new Date().toLocaleTimeString('en-US', { hour12: false }); | |
} | |
function addLogMessage(timestamp, message, type) { | |
const logContainer = document.getElementById('logContainer'); | |
const logItem = document.createElement('div'); | |
logItem.className = 'log-item'; | |
let icon, color, bgColor; | |
switch(type) { | |
case 'success': | |
icon = '✅'; | |
color = '#10b981'; | |
bgColor = 'rgba(16, 185, 129, 0.1)'; | |
break; | |
case 'error': | |
icon = '❌'; | |
color = '#ef4444'; | |
bgColor = 'rgba(239, 68, 68, 0.1)'; | |
break; | |
case 'warning': | |
icon = '⚠️'; | |
color = '#f59e0b'; | |
bgColor = 'rgba(245, 158, 11, 0.1)'; | |
break; | |
case 'tool': | |
icon = '🔧'; | |
color = '#3b82f6'; | |
bgColor = 'rgba(59, 130, 246, 0.1)'; | |
break; | |
case 'source': | |
icon = '📄'; | |
color = '#8b5cf6'; | |
bgColor = 'rgba(139, 92, 246, 0.1)'; | |
break; | |
case 'chunk': | |
icon = '📝'; | |
color = '#06b6d4'; | |
bgColor = 'rgba(6, 182, 212, 0.1)'; | |
break; | |
default: | |
icon = 'ℹ️'; | |
color = '#6b7280'; | |
bgColor = 'rgba(107, 114, 128, 0.1)'; | |
} | |
logItem.style.borderLeft = `3px solid ${color}`; | |
logItem.style.backgroundColor = bgColor; | |
logItem.innerHTML = ` | |
<div class="log-content"> | |
<span class="log-icon">${icon}</span> | |
<span class="log-timestamp">${timestamp}</span> | |
<span class="log-message" style="color: ${color};">${message}</span> | |
</div> | |
`; | |
logContainer.appendChild(logItem); | |
logContainer.scrollTop = logContainer.scrollHeight; | |
} | |
function addMessage(content, isUser = false) { | |
const messages = document.getElementById('messages'); | |
const messageDiv = document.createElement('div'); | |
messageDiv.className = `message ${isUser ? 'user' : 'bot'}`; | |
const contentDiv = document.createElement('div'); | |
contentDiv.className = 'message-content'; | |
if (isUser) { | |
// User messages as plain text | |
contentDiv.textContent = content; | |
} else { | |
// Bot messages rendered as Markdown | |
try { | |
contentDiv.innerHTML = marked.parse(content); | |
} catch (error) { | |
// Fallback to plain text if Markdown parsing fails | |
contentDiv.textContent = content; | |
} | |
} | |
messageDiv.appendChild(contentDiv); | |
messages.appendChild(messageDiv); | |
messages.scrollTop = messages.scrollHeight; | |
} | |
async function sendMessage() { | |
const input = document.getElementById('messageInput'); | |
const sendButton = document.getElementById('sendButton'); | |
const loading = document.getElementById('loading'); | |
const question = input.value.trim(); | |
if (!question) return; | |
// Add user message | |
addMessage(question, true); | |
// Clear input and disable form | |
input.value = ''; | |
sendButton.disabled = true; | |
loading.classList.add('show'); | |
try { | |
const response = await fetch('/chat', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
}, | |
body: JSON.stringify({ question: question }) | |
}); | |
if (!response.ok) { | |
throw new Error(`HTTP error! status: ${response.status}`); | |
} | |
const data = await response.json(); | |
// Add bot response | |
addMessage(data.response); | |
} catch (error) { | |
console.error('Error:', error); | |
addMessage('Sorry, there was an error processing your question. Please try again.'); | |
} finally { | |
sendButton.disabled = false; | |
loading.classList.remove('show'); | |
input.focus(); | |
} | |
} | |
// Event listeners | |
document.getElementById('messageInput').addEventListener('keypress', function(e) { | |
if (e.key === 'Enter') { | |
sendMessage(); | |
} | |
}); | |
// Initialize | |
document.addEventListener('DOMContentLoaded', function() { | |
// Check if marked.js loaded and configure it | |
if (typeof marked !== 'undefined') { | |
marked.setOptions({ | |
breaks: true, // Convert \n to <br> | |
gfm: true, // GitHub Flavored Markdown | |
sanitize: false, // Allow HTML (we trust our backend) | |
smartLists: true, // Better list handling | |
smartypants: true // Smart quotes and dashes | |
}); | |
} else { | |
console.warn('Marked.js not loaded, falling back to plain text'); | |
} | |
connectWebSocket(); | |
document.getElementById('messageInput').focus(); | |
}); | |
</script> | |
</body> | |
</html> |