Deploy Script
Deploy PuppyCompanion FastAPI 2025-06-02 09:57:27
b3b7a20
<!DOCTYPE html>
<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>