|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> |
|
<title>AstroChat - AI Assistant</title> |
|
<style> |
|
:root { |
|
--background-color: #212121; |
|
--input-area-color: #212121; |
|
--user-bubble-color: #303030; |
|
--ai-bubble-color: #2a2a2a; |
|
--text-color: #e0e0e0; |
|
--placeholder-color: #888888; |
|
--icon-color: #b0b0b0; |
|
--send-button-color: #4CAF50; |
|
--border-color: #333333; |
|
--code-bg-color: #1e1e1e; |
|
--code-text-color: #f8f8f2; |
|
--link-color: #64b5f6; |
|
--quote-color: #4CAF50; |
|
--audio-color: #00b4d8; |
|
--error-color: #f44336; |
|
} |
|
|
|
* { |
|
box-sizing: border-box; |
|
margin: 0; |
|
padding: 0; |
|
} |
|
|
|
html, body { |
|
height: 100%; |
|
overflow: hidden; |
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; |
|
} |
|
|
|
body { |
|
background-color: var(--background-color); |
|
color: var(--text-color); |
|
display: flex; |
|
flex-direction: column; |
|
line-height: 1.6; |
|
} |
|
|
|
.chat-container { |
|
display: flex; |
|
flex-direction: column; |
|
height: 100vh; |
|
max-width: 800px; |
|
margin: 0 auto; |
|
width: 100%; |
|
} |
|
|
|
.chat-area { |
|
flex: 1; |
|
overflow-y: auto; |
|
padding: 20px 16px; |
|
scroll-behavior: smooth; |
|
} |
|
|
|
.welcome-screen { |
|
text-align: center; |
|
margin: auto; |
|
color: var(--placeholder-color); |
|
padding: 20px; |
|
animation: fadeIn 1s ease-out; |
|
} |
|
|
|
.welcome-screen h2 { |
|
font-size: 1.8rem; |
|
font-weight: 400; |
|
margin-bottom: 10px; |
|
} |
|
|
|
.welcome-screen p { |
|
font-size: 1rem; |
|
line-height: 1.5; |
|
margin-bottom: 8px; |
|
} |
|
|
|
.welcome-screen.hidden { |
|
display: none; |
|
} |
|
|
|
.message { |
|
max-width: 85%; |
|
padding: 12px 16px; |
|
border-radius: 18px; |
|
margin-bottom: 12px; |
|
word-wrap: break-word; |
|
line-height: 1.6; |
|
font-size: 1rem; |
|
position: relative; |
|
animation: fadeIn 0.3s ease-out; |
|
opacity: 0; |
|
animation-fill-mode: forwards; |
|
} |
|
|
|
@keyframes fadeIn { |
|
from { opacity: 0; transform: translateY(10px); } |
|
to { opacity: 1; transform: translateY(0); } |
|
} |
|
|
|
.user-message { |
|
background-color: var(--user-bubble-color); |
|
align-self: flex-end; |
|
border-bottom-right-radius: 4px; |
|
} |
|
|
|
.ai-message { |
|
background-color: var(--ai-bubble-color); |
|
align-self: flex-start; |
|
border-bottom-left-radius: 4px; |
|
padding-bottom: 30px; |
|
} |
|
|
|
.ai-message-content { |
|
overflow: hidden; |
|
} |
|
|
|
.ai-message-content p { |
|
margin-bottom: 12px; |
|
} |
|
|
|
.ai-message-content p:last-child { |
|
margin-bottom: 0; |
|
} |
|
|
|
.ai-message-content strong { |
|
color: #ffffff; |
|
font-weight: 600; |
|
} |
|
|
|
.ai-message-content em { |
|
color: #d1d1d1; |
|
font-style: italic; |
|
} |
|
|
|
.ai-message-content a { |
|
color: var(--link-color); |
|
text-decoration: none; |
|
word-break: break-all; |
|
} |
|
|
|
.ai-message-content a:hover { |
|
text-decoration: underline; |
|
} |
|
|
|
.ai-message-content ul, |
|
.ai-message-content ol { |
|
padding-left: 24px; |
|
margin-bottom: 12px; |
|
} |
|
|
|
.ai-message-content li { |
|
margin-bottom: 6px; |
|
} |
|
|
|
.ai-message-content blockquote { |
|
border-left: 3px solid var(--quote-color); |
|
padding-left: 12px; |
|
margin-left: 0; |
|
color: #bdbdbd; |
|
margin-bottom: 12px; |
|
} |
|
|
|
.ai-message-content pre { |
|
background-color: var(--code-bg-color); |
|
border-radius: 6px; |
|
padding: 12px; |
|
overflow-x: auto; |
|
margin-bottom: 12px; |
|
} |
|
|
|
.ai-message-content code { |
|
font-family: 'Courier New', Courier, monospace; |
|
background-color: var(--code-bg-color); |
|
padding: 2px 4px; |
|
border-radius: 3px; |
|
color: var(--code-text-color); |
|
font-size: 0.9em; |
|
white-space: pre-wrap; |
|
} |
|
|
|
.ai-message-content .code-block code { |
|
padding: 0; |
|
background-color: transparent; |
|
color: inherit; |
|
} |
|
|
|
.audio-button { |
|
position: absolute; |
|
bottom: 6px; |
|
right: 8px; |
|
background: rgba(0, 180, 216, 0.1); |
|
border: none; |
|
border-radius: 50%; |
|
width: 30px; |
|
height: 30px; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
cursor: pointer; |
|
transition: all 0.2s; |
|
z-index: 10; |
|
} |
|
|
|
.audio-button:hover { |
|
background: rgba(0, 180, 216, 0.2); |
|
transform: scale(1.1); |
|
} |
|
|
|
.audio-button.playing { |
|
background: rgba(0, 180, 216, 0.3); |
|
box-shadow: 0 0 0 2px var(--audio-color); |
|
} |
|
|
|
.audio-button.playing svg { |
|
fill: var(--audio-color); |
|
} |
|
|
|
.audio-button.loading .speaker-icon { |
|
display: none; |
|
} |
|
|
|
.audio-button .loader { |
|
display: none; |
|
width: 18px; |
|
height: 18px; |
|
border: 2px solid #f3f3f3; |
|
border-top: 2px solid var(--audio-color); |
|
border-radius: 50%; |
|
animation: spin 1s linear infinite; |
|
} |
|
|
|
.audio-button.loading .loader { |
|
display: block; |
|
} |
|
|
|
@keyframes spin { |
|
0% { transform: rotate(0deg); } |
|
100% { transform: rotate(360deg); } |
|
} |
|
|
|
.typing-indicator { |
|
display: flex; |
|
align-items: center; |
|
padding: 12px 16px; |
|
} |
|
|
|
.typing-indicator span { |
|
height: 8px; |
|
width: 8px; |
|
background-color: var(--icon-color); |
|
border-radius: 50%; |
|
display: inline-block; |
|
margin: 0 2px; |
|
animation: bounce 1.4s infinite both; |
|
} |
|
|
|
.typing-indicator span:nth-child(2) { |
|
animation-delay: 0.2s; |
|
} |
|
|
|
.typing-indicator span:nth-child(3) { |
|
animation-delay: 0.4s; |
|
} |
|
|
|
@keyframes bounce { |
|
0%, 80%, 100% { transform: scale(0); } |
|
40% { transform: scale(1.0); } |
|
} |
|
|
|
.input-area { |
|
display: flex; |
|
align-items: flex-end; |
|
padding: 10px 16px; |
|
border-top: 1px solid var(--border-color); |
|
background-color: var(--input-area-color); |
|
flex-shrink: 0; |
|
} |
|
|
|
.input-wrapper { |
|
display: flex; |
|
align-items: center; |
|
width: 100%; |
|
background-color: var(--user-bubble-color); |
|
border-radius: 24px; |
|
padding: 4px; |
|
} |
|
|
|
.input-area textarea { |
|
flex-grow: 1; |
|
border: none; |
|
background: transparent; |
|
color: var(--text-color); |
|
resize: none; |
|
font-size: 1rem; |
|
line-height: 1.5; |
|
max-height: 120px; |
|
overflow-y: auto; |
|
padding: 8px 12px; |
|
font-family: inherit; |
|
outline: none; |
|
} |
|
|
|
.input-area textarea::placeholder { |
|
color: var(--placeholder-color); |
|
} |
|
|
|
.icon-button { |
|
background: none; |
|
border: none; |
|
padding: 8px; |
|
cursor: pointer; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
transition: opacity 0.2s; |
|
} |
|
|
|
.icon-button:hover { |
|
opacity: 0.8; |
|
} |
|
|
|
.icon-button svg { |
|
width: 24px; |
|
height: 24px; |
|
fill: var(--icon-color); |
|
} |
|
|
|
.send-button { |
|
background-color: var(--send-button-color); |
|
border-radius: 50%; |
|
padding: 8px; |
|
transition: background-color 0.2s, opacity 0.2s; |
|
} |
|
|
|
.send-button.disabled { |
|
background-color: #555; |
|
cursor: not-allowed; |
|
opacity: 0.6; |
|
} |
|
|
|
.send-button.disabled svg { |
|
fill: #888; |
|
} |
|
|
|
.send-button svg { |
|
fill: white; |
|
width: 24px; |
|
height: 24px; |
|
} |
|
|
|
.error-message { |
|
color: var(--error-color); |
|
padding: 8px 16px; |
|
text-align: center; |
|
font-size: 0.9rem; |
|
animation: fadeIn 0.3s ease-out; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div class="chat-container"> |
|
<main class="chat-area" id="chat-area"> |
|
<div class="welcome-screen"> |
|
<h2>Welcome to AstroChat!</h2> |
|
<p>Hello! I'm your AI assistant powered by Gemini. I can help with explanations, ideas, and even read responses aloud.</p> |
|
<p>Try saying: <em>"Explain quantum computing"</em> or <em>"Read this poem aloud"</em></p> |
|
</div> |
|
</main> |
|
|
|
<footer class="input-area"> |
|
<form id="chat-form" class="input-wrapper"> |
|
<button type="button" class="icon-button" id="attach-button" aria-label="Attach file"> |
|
<svg viewBox="0 0 24 24"><path d="M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5a2.5 2.5 0 0 1 5 0v10.5c0 .83-.67 1.5-1.5 1.5s-1.5-.67-1.5-1.5V6H10v9.5a2.5 2.5 0 0 0 5 0V5c-1.38 0-2.5 1.12-2.5 2.5v10.5c0 1.38-1.12 2.5-2.5 2.5s-2.5-1.12-2.5-2.5V5a4 4 0 0 1 8 0v11.5c-1.1 0-2-.9-2-2V6h-1.5z"></path></svg> |
|
</button> |
|
<textarea id="message-input" placeholder="Ask anything..." rows="1" aria-label="Message input"></textarea> |
|
<button type="submit" id="send-button" class="icon-button send-button disabled" aria-label="Send message"> |
|
<svg viewBox="0 0 24 24"><path d="M2 21l21-9L2 3v7l15 2-15 2v7z"></path></svg> |
|
</button> |
|
</form> |
|
</footer> |
|
</div> |
|
|
|
<script> |
|
document.addEventListener('DOMContentLoaded', () => { |
|
const chatForm = document.getElementById('chat-form'); |
|
const messageInput = document.getElementById('message-input'); |
|
const chatArea = document.getElementById('chat-area'); |
|
const welcomeScreen = document.querySelector('.welcome-screen'); |
|
const sendButton = document.getElementById('send-button'); |
|
const attachButton = document.getElementById('attach-button'); |
|
|
|
|
|
const audioPlayer = { |
|
currentAudio: null, |
|
currentButton: null, |
|
isPlaying: false, |
|
|
|
play: async function(buttonElement, text) { |
|
|
|
this.stop(); |
|
|
|
|
|
buttonElement.classList.add('loading'); |
|
buttonElement.disabled = true; |
|
|
|
try { |
|
|
|
const response = await fetch('/generate-audio', { |
|
method: 'POST', |
|
headers: { 'Content-Type': 'application/json' }, |
|
body: JSON.stringify({ text }) |
|
}); |
|
|
|
if (!response.ok) { |
|
throw new Error(await response.text()); |
|
} |
|
|
|
const data = await response.json(); |
|
|
|
|
|
this.currentAudio = new Audio(data.audio_url); |
|
this.currentButton = buttonElement; |
|
|
|
|
|
this.currentAudio.onplay = () => { |
|
this.isPlaying = true; |
|
buttonElement.classList.remove('loading'); |
|
buttonElement.classList.add('playing'); |
|
}; |
|
|
|
this.currentAudio.onended = () => { |
|
this.reset(); |
|
}; |
|
|
|
this.currentAudio.onerror = () => { |
|
this.reset(); |
|
showError("Audio playback failed"); |
|
}; |
|
|
|
|
|
this.currentAudio.play(); |
|
|
|
} catch (error) { |
|
console.error("Audio error:", error); |
|
buttonElement.classList.remove('loading', 'playing'); |
|
showError("Couldn't generate audio"); |
|
} |
|
}, |
|
|
|
stop: function() { |
|
if (this.currentAudio) { |
|
this.currentAudio.pause(); |
|
this.currentAudio.currentTime = 0; |
|
} |
|
if (this.currentButton) { |
|
this.currentButton.classList.remove('playing'); |
|
} |
|
this.reset(); |
|
}, |
|
|
|
reset: function() { |
|
this.isPlaying = false; |
|
this.currentAudio = null; |
|
if (this.currentButton) { |
|
this.currentButton.classList.remove('loading', 'playing'); |
|
this.currentButton.disabled = false; |
|
} |
|
this.currentButton = null; |
|
} |
|
}; |
|
|
|
|
|
function adjustTextareaHeight() { |
|
messageInput.style.height = 'auto'; |
|
messageInput.style.height = `${messageInput.scrollHeight}px`; |
|
updateSendButtonState(); |
|
} |
|
|
|
function updateSendButtonState() { |
|
const isDisabled = messageInput.value.trim() === ''; |
|
sendButton.classList.toggle('disabled', isDisabled); |
|
sendButton.disabled = isDisabled; |
|
} |
|
|
|
function scrollToBottom() { |
|
chatArea.scrollTop = chatArea.scrollHeight; |
|
} |
|
|
|
function showError(message) { |
|
const existingError = document.querySelector('.error-message'); |
|
if (existingError) { |
|
existingError.remove(); |
|
} |
|
|
|
const errorElement = document.createElement('div'); |
|
errorElement.className = 'error-message'; |
|
errorElement.textContent = message; |
|
chatArea.appendChild(errorElement); |
|
scrollToBottom(); |
|
|
|
setTimeout(() => { |
|
errorElement.remove(); |
|
}, 5000); |
|
} |
|
|
|
|
|
function addMessage(content, sender, isHTML = false, plainText = '') { |
|
welcomeScreen.classList.add('hidden'); |
|
|
|
const messageElement = document.createElement('div'); |
|
messageElement.className = `message ${sender}-message`; |
|
|
|
const contentDiv = document.createElement('div'); |
|
if (isHTML) { |
|
contentDiv.className = `${sender}-message-content`; |
|
contentDiv.innerHTML = content; |
|
} else { |
|
contentDiv.textContent = content; |
|
} |
|
|
|
messageElement.appendChild(contentDiv); |
|
|
|
|
|
if (sender === 'ai' && plainText) { |
|
const audioButton = document.createElement('button'); |
|
audioButton.className = 'audio-button'; |
|
audioButton.title = "Play audio"; |
|
audioButton.innerHTML = ` |
|
<svg class="speaker-icon" viewBox="0 0 24 24"> |
|
<path d="M3 10v4c0 .55.45 1 1 1h3.5l5.5 5V5L7.5 9H4c-.55 0-1 .45-1 1zm14 0h-1.5c-2.31 0-4.22-1.74-4.47-4H10v12h1.5v-2.22c.25-2.26 2.16-4.22 4.47-4.22H17c1.1 0 2 .9 2 2s-.9 2-2 2h-1.5v2H17c2.21 0 4-1.79 4-4s-1.79-4-4-4z"/> |
|
</svg> |
|
<div class="loader"></div> |
|
`; |
|
|
|
audioButton.addEventListener('click', () => { |
|
audioPlayer.play(audioButton, plainText); |
|
}); |
|
|
|
messageElement.appendChild(audioButton); |
|
} |
|
|
|
chatArea.appendChild(messageElement); |
|
scrollToBottom(); |
|
return messageElement; |
|
} |
|
|
|
function showTypingIndicator() { |
|
const indicator = document.createElement('div'); |
|
indicator.className = 'message ai-message typing-indicator'; |
|
indicator.innerHTML = '<span></span><span></span><span></span>'; |
|
chatArea.appendChild(indicator); |
|
scrollToBottom(); |
|
return indicator; |
|
} |
|
|
|
|
|
async function getAIResponse(userMessage) { |
|
const indicator = showTypingIndicator(); |
|
|
|
try { |
|
const response = await fetch('/chat', { |
|
method: 'POST', |
|
headers: { 'Content-Type': 'application/json' }, |
|
body: JSON.stringify({ message: userMessage }) |
|
}); |
|
|
|
if (!response.ok) { |
|
throw new Error(await response.text()); |
|
} |
|
|
|
const data = await response.json(); |
|
chatArea.removeChild(indicator); |
|
|
|
const messageElement = addMessage( |
|
data.response_html, |
|
'ai', |
|
true, |
|
data.response_text |
|
); |
|
|
|
|
|
if (data.audio_requested) { |
|
const audioButton = messageElement.querySelector('.audio-button'); |
|
if (audioButton) { |
|
setTimeout(() => { |
|
audioPlayer.play(audioButton, data.response_text); |
|
}, 300); |
|
} |
|
} |
|
|
|
} catch (error) { |
|
console.error("Chat error:", error); |
|
chatArea.removeChild(indicator); |
|
showError("Failed to get response from AI"); |
|
} |
|
} |
|
|
|
|
|
chatForm.addEventListener('submit', async (e) => { |
|
e.preventDefault(); |
|
const message = messageInput.value.trim(); |
|
|
|
if (message) { |
|
addMessage(message, 'user'); |
|
messageInput.value = ''; |
|
adjustTextareaHeight(); |
|
|
|
await getAIResponse(message); |
|
} |
|
}); |
|
|
|
messageInput.addEventListener('keydown', (e) => { |
|
if (e.key === 'Enter' && !e.shiftKey) { |
|
e.preventDefault(); |
|
chatForm.dispatchEvent(new Event('submit')); |
|
} |
|
}); |
|
|
|
messageInput.addEventListener('input', adjustTextareaHeight); |
|
|
|
attachButton.addEventListener('click', () => { |
|
|
|
showError("File attachments coming soon!"); |
|
messageInput.focus(); |
|
}); |
|
|
|
|
|
updateSendButtonState(); |
|
messageInput.focus(); |
|
|
|
|
|
if (!localStorage.getItem('chatVisited')) { |
|
localStorage.setItem('chatVisited', 'true'); |
|
setTimeout(() => { |
|
addMessage( |
|
"Pro tip: You can say 'read aloud' to hear my responses!", |
|
'ai' |
|
); |
|
}, 3000); |
|
} |
|
}); |
|
</script> |
|
</body> |
|
</html> |