|
|
|
<!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>AI Chat with Athspi</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; |
|
} |
|
* { |
|
box-sizing: border-box; |
|
margin: 0; |
|
padding: 0; |
|
} |
|
html, body { |
|
height: 100%; |
|
overflow: hidden; |
|
} |
|
body { |
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; |
|
background-color: var(--background-color); |
|
color: var(--text-color); |
|
display: flex; |
|
flex-direction: column; |
|
height: 100%; |
|
line-height: 1.6; |
|
} |
|
.chat-container { |
|
display: flex; |
|
flex-direction: column; |
|
height: 100%; |
|
width: 100%; |
|
max-width: 800px; |
|
margin: 0 auto; |
|
border-left: 1px solid var(--border-color); |
|
border-right: 1px solid var(--border-color); |
|
} |
|
.chat-area { |
|
flex-grow: 1; |
|
overflow-y: auto; |
|
padding: 20px 16px; |
|
display: flex; |
|
flex-direction: column; |
|
} |
|
.welcome-screen { |
|
text-align: center; |
|
margin: auto; |
|
color: var(--placeholder-color); |
|
padding: 20px; |
|
} |
|
.welcome-screen h2 { |
|
font-size: 1.8rem; |
|
font-weight: 400; |
|
margin-bottom: 10px; |
|
} |
|
.welcome-screen p { |
|
font-size: 1rem; |
|
line-height: 1.5; |
|
} |
|
.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; |
|
} |
|
.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(255, 255, 255, 0.1); |
|
border: none; |
|
border-radius: 50%; |
|
width: 30px; |
|
height: 30px; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
cursor: pointer; |
|
transition: background-color 0.2s; |
|
z-index: 10; |
|
} |
|
.audio-button:hover { |
|
background: rgba(255, 255, 255, 0.2); |
|
} |
|
.audio-button svg { |
|
width: 18px; |
|
height: 18px; |
|
fill: var(--icon-color); |
|
transition: fill 0.2s; |
|
} |
|
.audio-button:hover svg { |
|
fill: white; |
|
} |
|
.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(--send-button-color); |
|
border-radius: 50%; |
|
animation: spin 1s linear infinite; |
|
} |
|
.audio-button.loading .loader { |
|
display: block; |
|
} |
|
.download-button { |
|
position: absolute; |
|
bottom: 6px; |
|
right: 45px; |
|
background: rgba(255, 255, 255, 0.1); |
|
border: none; |
|
border-radius: 50%; |
|
width: 30px; |
|
height: 30px; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
cursor: pointer; |
|
transition: background-color 0.2s; |
|
z-index: 10; |
|
} |
|
.download-button:hover { |
|
background: rgba(255, 255, 255, 0.2); |
|
} |
|
.download-button svg { |
|
width: 18px; |
|
height: 18px; |
|
fill: var(--icon-color); |
|
transition: fill 0.2s; |
|
} |
|
.download-button:hover svg { |
|
fill: #4CAF50; |
|
} |
|
@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); } |
|
.input-area .icon-button { |
|
background: none; |
|
border: none; |
|
padding: 8px; |
|
cursor: pointer; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
transition: opacity 0.2s; |
|
} |
|
.input-area .icon-button:hover { |
|
opacity: 0.8; |
|
} |
|
.input-area .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; } |
|
@keyframes fadeIn { |
|
from { opacity: 0; transform: translateY(10px); } |
|
to { opacity: 1; transform: translateY(0); } |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div class="chat-container"> |
|
<main class="chat-area"> |
|
<div class="welcome-screen"> |
|
<h2>Welcome to Athspi!</h2> |
|
<p>Hello! I'm your friendly AI assistant powered by Gemini. Ask me anything - stories, explanations, or coding help!</p> |
|
<p>For audio responses, try: <em>"Tell me a bedtime story with audio"</em></p> |
|
</div> |
|
</main> |
|
<footer class="input-area"> |
|
<form id="chat-form" class="input-wrapper"> |
|
<textarea id="message-input" placeholder="Ask anything..." rows="1"></textarea> |
|
<button type="submit" id="send-button" class="icon-button send-button disabled"> |
|
<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.querySelector('.chat-area'); |
|
const welcomeScreen = document.querySelector('.welcome-screen'); |
|
const sendButton = document.getElementById('send-button'); |
|
let currentAudio = null; |
|
const adjustTextareaHeight = () => { |
|
messageInput.style.height = 'auto'; |
|
messageInput.style.height = `${messageInput.scrollHeight}px`; |
|
updateSendButtonState(); |
|
}; |
|
const updateSendButtonState = () => { |
|
const isDisabled = messageInput.value.trim() === ''; |
|
sendButton.classList.toggle('disabled', isDisabled); |
|
sendButton.disabled = isDisabled; |
|
}; |
|
const scrollToBottom = () => { |
|
chatArea.scrollTop = chatArea.scrollHeight; |
|
}; |
|
const addMessage = (content, sender, isHTML = false, audioFilename = null) => { |
|
welcomeScreen.classList.add('hidden'); |
|
const messageElement = document.createElement('div'); |
|
messageElement.classList.add('message', `${sender}-message`); |
|
const contentDiv = document.createElement('div'); |
|
if (isHTML) { |
|
contentDiv.classList.add(`${sender}-message-content`); |
|
contentDiv.innerHTML = content; |
|
} else { |
|
contentDiv.textContent = content; |
|
} |
|
messageElement.appendChild(contentDiv); |
|
if (sender === 'ai' && audioFilename) { |
|
|
|
const downloadButton = document.createElement('button'); |
|
downloadButton.classList.add('download-button'); |
|
downloadButton.title = "Download audio"; |
|
downloadButton.innerHTML = ` |
|
<svg viewBox="0 0 24 24"> |
|
<path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/> |
|
</svg> |
|
`; |
|
downloadButton.addEventListener('click', () => { |
|
window.location.href = `/download/${audioFilename}`; |
|
}); |
|
messageElement.appendChild(downloadButton); |
|
|
|
const playButton = document.createElement('button'); |
|
playButton.classList.add('audio-button'); |
|
playButton.dataset.audioUrl = `/static/audio/${audioFilename}`; |
|
playButton.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> |
|
`; |
|
playButton.title = "Listen to response"; |
|
playButton.addEventListener('click', (e) => { |
|
const url = e.currentTarget.dataset.audioUrl; |
|
playAudio(e.currentTarget, url); |
|
}); |
|
messageElement.appendChild(playButton); |
|
|
|
setTimeout(() => playButton.click(), 300); |
|
} |
|
chatArea.appendChild(messageElement); |
|
scrollToBottom(); |
|
return messageElement; |
|
}; |
|
const showTypingIndicator = () => { |
|
const indicatorElement = document.createElement('div'); |
|
indicatorElement.classList.add('message', 'ai-message', 'typing-indicator'); |
|
indicatorElement.innerHTML = '<span></span><span></span><span></span>'; |
|
chatArea.appendChild(indicatorElement); |
|
scrollToBottom(); |
|
return indicatorElement; |
|
}; |
|
const playAudio = async (buttonElement, audioUrl) => { |
|
if (currentAudio) { |
|
currentAudio.pause(); |
|
currentAudio.currentTime = 0; |
|
currentAudio = null; |
|
} |
|
buttonElement.classList.add('loading'); |
|
buttonElement.disabled = true; |
|
try { |
|
if (!audioUrl) throw new Error("No audio available"); |
|
currentAudio = new Audio(audioUrl); |
|
currentAudio.play(); |
|
currentAudio.onended = () => { |
|
buttonElement.classList.remove('loading'); |
|
buttonElement.disabled = false; |
|
currentAudio = null; |
|
}; |
|
currentAudio.onerror = () => { |
|
console.error("Error playing audio"); |
|
buttonElement.classList.remove('loading'); |
|
buttonElement.disabled = false; |
|
currentAudio = null; |
|
}; |
|
} catch (error) { |
|
console.error("Audio error:", error); |
|
alert("Failed to play audio. " + (error.message || "Please try again.")); |
|
buttonElement.classList.remove('loading'); |
|
buttonElement.disabled = false; |
|
} |
|
}; |
|
const getAIResponse = async (userMessage) => { |
|
const indicator = showTypingIndicator(); |
|
try { |
|
const response = await fetch('/chat', { |
|
method: 'POST', |
|
headers: { 'Content-Type': 'application/json' }, |
|
body: JSON.stringify({ message: userMessage }) |
|
}); |
|
const data = await response.json(); |
|
if (data.error) { |
|
throw new Error(data.error); |
|
} |
|
chatArea.removeChild(indicator); |
|
addMessage( |
|
data.response_html, |
|
'ai', |
|
true, |
|
data.audio_filename |
|
); |
|
} catch (error) { |
|
chatArea.removeChild(indicator); |
|
addMessage("Sorry, I encountered an error. Please try again.", 'ai'); |
|
console.error("Error:", error); |
|
} |
|
}; |
|
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); |
|
updateSendButtonState(); |
|
messageInput.focus(); |
|
}); |
|
</script> |
|
</body> |
|
</html> |