Spaces:
Running
Running
<html lang="id"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Character AI Chat</title> | |
<style> | |
* { | |
margin: 0; | |
padding: 0; | |
box-sizing: border-box; | |
} | |
body { | |
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |
background: url('background.png'), linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
background-size: cover; | |
background-attachment: fixed; | |
height: 100vh; | |
overflow: hidden; | |
} | |
.chat-container { | |
height: 100vh; | |
display: flex; | |
flex-direction: column; | |
max-width: 100%; | |
margin: 0 auto; | |
background: rgba(255, 255, 255, 0.95); | |
backdrop-filter: blur(10px); | |
} | |
/* Header */ | |
.chat-header { | |
background: #075e54; | |
color: white; | |
padding: 10px 16px; | |
display: flex; | |
align-items: center; | |
position: sticky; | |
top: 0; | |
z-index: 100; | |
box-shadow: 0 2px 8px rgba(0,0,0,0.1); | |
} | |
.avatar { | |
width: 40px; | |
height: 40px; | |
border-radius: 50%; | |
margin-right: 12px; | |
background: url('avatar.png'), linear-gradient(45deg, #25d366, #128c7e); | |
background-size: cover; | |
background-position: center; | |
} | |
.header-info { | |
flex: 1; | |
} | |
.char-name { | |
font-size: 16px; | |
font-weight: 600; | |
margin-bottom: 2px; | |
} | |
.status { | |
font-size: 13px; | |
opacity: 0.8; | |
color: #dcf8c6; | |
} | |
.header-actions { | |
display: flex; | |
align-items: center; | |
gap: 20px; | |
} | |
.three-dots { | |
cursor: pointer; | |
padding: 8px; | |
border-radius: 50%; | |
transition: background 0.2s; | |
} | |
.three-dots:hover { | |
background: rgba(255,255,255,0.1); | |
} | |
/* Settings Popup */ | |
.settings-popup { | |
display: none; | |
position: absolute; | |
top: 60px; | |
right: 16px; | |
background: white; | |
border-radius: 12px; | |
box-shadow: 0 8px 32px rgba(0,0,0,0.2); | |
min-width: 280px; | |
z-index: 1000; | |
padding: 8px 0; | |
} | |
.settings-popup.show { | |
display: block; | |
animation: popupIn 0.2s ease-out; | |
} | |
@keyframes popupIn { | |
from { opacity: 0; transform: translateY(-10px) scale(0.95); } | |
to { opacity: 1; transform: translateY(0) scale(1); } | |
} | |
.settings-section { | |
padding: 12px 20px; | |
border-bottom: 1px solid #eee; | |
} | |
.settings-section:last-child { | |
border-bottom: none; | |
} | |
.settings-label { | |
font-size: 14px; | |
font-weight: 600; | |
color: #333; | |
margin-bottom: 8px; | |
} | |
.model-select, .input-field { | |
width: 100%; | |
padding: 8px 12px; | |
border: 1px solid #ddd; | |
border-radius: 8px; | |
font-size: 14px; | |
background: #f8f9fa; | |
} | |
.input-field { | |
margin-top: 4px; | |
} | |
/* Chat Body */ | |
.chat-body { | |
flex: 1; | |
overflow-y: auto; | |
padding: 20px 16px; | |
background: url('background.png'), | |
radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.3), transparent), | |
radial-gradient(circle at 80% 20%, rgba(255, 119, 198, 0.3), transparent), | |
linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
background-size: cover, 400px 400px, 400px 400px, cover; | |
background-attachment: fixed; | |
} | |
/* Message Bubbles */ | |
.message { | |
display: flex; | |
margin-bottom: 12px; | |
animation: messageSlide 0.3s ease-out; | |
} | |
@keyframes messageSlide { | |
from { opacity: 0; transform: translateY(20px); } | |
to { opacity: 1; transform: translateY(0); } | |
} | |
.message.user { | |
justify-content: flex-end; | |
} | |
.message.char { | |
justify-content: flex-start; | |
} | |
.message-avatar { | |
width: 32px; | |
height: 32px; | |
border-radius: 50%; | |
margin: 0 8px; | |
background: url('avatar.png'), linear-gradient(45deg, #25d366, #128c7e); | |
background-size: cover; | |
background-position: center; | |
align-self: flex-end; | |
} | |
.message.user .message-avatar { | |
background: linear-gradient(45deg, #0084ff, #00a0ff); | |
} | |
.bubble { | |
max-width: 70%; | |
padding: 12px 16px; | |
border-radius: 18px; | |
position: relative; | |
word-wrap: break-word; | |
backdrop-filter: blur(10px); | |
} | |
.message.user .bubble { | |
background: linear-gradient(135deg, #0084ff, #00a0ff); | |
color: white; | |
border-bottom-right-radius: 6px; | |
} | |
.message.char .bubble { | |
background: rgba(255, 255, 255, 0.95); | |
color: #333; | |
border-bottom-left-radius: 6px; | |
box-shadow: 0 2px 8px rgba(0,0,0,0.1); | |
} | |
.message-text { | |
font-size: 16px; | |
line-height: 1.4; | |
margin-bottom: 6px; | |
} | |
.message-meta { | |
display: flex; | |
align-items: center; | |
justify-content: flex-end; | |
gap: 4px; | |
font-size: 12px; | |
opacity: 0.7; | |
} | |
.timestamp { | |
color: inherit; | |
} | |
.checkmarks { | |
color: #4fc3f7; | |
font-weight: bold; | |
} | |
.message.char .checkmarks { | |
display: none; | |
} | |
/* Typing Indicator */ | |
.typing-indicator { | |
display: none; | |
align-items: center; | |
margin-bottom: 12px; | |
} | |
.typing-indicator.show { | |
display: flex; | |
} | |
.typing-dots { | |
background: rgba(255, 255, 255, 0.95); | |
border-radius: 18px; | |
padding: 12px 16px; | |
margin-left: 48px; | |
box-shadow: 0 2px 8px rgba(0,0,0,0.1); | |
} | |
.typing-dots span { | |
display: inline-block; | |
width: 8px; | |
height: 8px; | |
border-radius: 50%; | |
background: #999; | |
margin-right: 4px; | |
animation: typing 2s infinite; | |
} | |
.typing-dots span:nth-child(2) { animation-delay: 0.2s; } | |
.typing-dots span:nth-child(3) { animation-delay: 0.4s; } | |
@keyframes typing { | |
0%, 60%, 100% { transform: translateY(0); opacity: 0.4; } | |
30% { transform: translateY(-10px); opacity: 1; } | |
} | |
/* Footer */ | |
.chat-footer { | |
background: rgba(255, 255, 255, 0.95); | |
backdrop-filter: blur(10px); | |
padding: 12px 16px; | |
border-top: 1px solid rgba(0,0,0,0.1); | |
position: sticky; | |
bottom: 0; | |
} | |
.input-container { | |
display: flex; | |
align-items: flex-end; | |
gap: 8px; | |
} | |
.emoji-btn { | |
background: none; | |
border: none; | |
font-size: 24px; | |
cursor: pointer; | |
padding: 8px; | |
border-radius: 50%; | |
transition: background 0.2s; | |
} | |
.emoji-btn:hover { | |
background: rgba(0,0,0,0.05); | |
} | |
.message-input { | |
flex: 1; | |
min-height: 40px; | |
max-height: 120px; | |
padding: 10px 16px; | |
border: 1px solid #ddd; | |
border-radius: 20px; | |
font-size: 16px; | |
font-family: inherit; | |
resize: none; | |
outline: none; | |
background: white; | |
transition: border-color 0.2s; | |
} | |
.message-input:focus { | |
border-color: #075e54; | |
} | |
.send-btn { | |
background: #075e54; | |
color: white; | |
border: none; | |
width: 40px; | |
height: 40px; | |
border-radius: 50%; | |
cursor: pointer; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
transition: all 0.2s; | |
font-size: 18px; | |
} | |
.send-btn:hover { | |
background: #128c7e; | |
transform: scale(1.05); | |
} | |
.send-btn:disabled { | |
background: #ccc; | |
cursor: not-allowed; | |
transform: none; | |
} | |
/* Emoji Picker */ | |
.emoji-picker { | |
display: none; | |
position: absolute; | |
bottom: 70px; | |
left: 16px; | |
background: white; | |
border-radius: 12px; | |
box-shadow: 0 8px 32px rgba(0,0,0,0.2); | |
padding: 16px; | |
max-width: 300px; | |
z-index: 1000; | |
} | |
.emoji-picker.show { | |
display: block; | |
animation: popupIn 0.2s ease-out; | |
} | |
.emoji-grid { | |
display: grid; | |
grid-template-columns: repeat(8, 1fr); | |
gap: 8px; | |
max-height: 200px; | |
overflow-y: auto; | |
} | |
.emoji-item { | |
background: none; | |
border: none; | |
font-size: 24px; | |
cursor: pointer; | |
padding: 8px; | |
border-radius: 8px; | |
transition: background 0.2s; | |
} | |
.emoji-item:hover { | |
background: #f0f0f0; | |
} | |
/* Responsive */ | |
@media (max-width: 768px) { | |
.chat-container { | |
height: 100vh; | |
} | |
.bubble { | |
max-width: 85%; | |
} | |
.settings-popup { | |
right: 8px; | |
min-width: 260px; | |
} | |
} | |
/* Scrollbar Styling */ | |
.chat-body::-webkit-scrollbar { | |
width: 6px; | |
} | |
.chat-body::-webkit-scrollbar-track { | |
background: transparent; | |
} | |
.chat-body::-webkit-scrollbar-thumb { | |
background: rgba(0,0,0,0.2); | |
border-radius: 3px; | |
} | |
.chat-body::-webkit-scrollbar-thumb:hover { | |
background: rgba(0,0,0,0.3); | |
} | |
</style> | |
</head> | |
<body> | |
<div class="chat-container"> | |
<!-- Header --> | |
<div class="chat-header"> | |
<div class="avatar"></div> | |
<div class="header-info"> | |
<div class="char-name" id="charName">Sayang</div> | |
<div class="status" id="status">online</div> | |
</div> | |
<div class="header-actions"> | |
<div class="three-dots" onclick="toggleSettings()"> | |
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> | |
<circle cx="12" cy="5" r="2"/> | |
<circle cx="12" cy="12" r="2"/> | |
<circle cx="12" cy="19" r="2"/> | |
</svg> | |
</div> | |
</div> | |
</div> | |
<!-- Settings Popup --> | |
<div class="settings-popup" id="settingsPopup"> | |
<div class="settings-section"> | |
<div class="settings-label">Model AI</div> | |
<select class="model-select" id="modelSelect"> | |
<option value="distil-gpt-2">DistilGPT-2 ⚡</option> | |
<option value="gpt-2-tinny">GPT-2 Tinny ⚡</option> | |
<option value="bert-tinny">BERT Tinny 🎭</option> | |
<option value="distilbert-base-uncased">DistilBERT 🎭</option> | |
<option value="albert-base-v2">ALBERT Base 🎭</option> | |
<option value="electra-small">ELECTRA Small 🎭</option> | |
<option value="t5-small">T5 Small 🔄</option> | |
<option value="gpt-2">GPT-2 Standard</option> | |
<option value="tinny-llama">Tinny Llama</option> | |
<option value="pythia">Pythia</option> | |
<option value="gpt-neo">GPT-Neo</option> | |
</select> | |
</div> | |
<div class="settings-section"> | |
<div class="settings-label">Karakter</div> | |
<input type="text" class="input-field" id="charNameInput" placeholder="Nama karakter" value="Sayang"> | |
<input type="text" class="input-field" id="userNameInput" placeholder="Nama kamu" value="Kamu"> | |
</div> | |
<div class="settings-section"> | |
<div class="settings-label">Situasi & Lokasi</div> | |
<input type="text" class="input-field" id="situationInput" placeholder="Situasi" value="Santai"> | |
<input type="text" class="input-field" id="locationInput" placeholder="Lokasi" value="Ruang tamu"> | |
</div> | |
<div class="settings-section"> | |
<div class="settings-label">Panjang Pesan</div> | |
<input type="range" class="input-field" id="maxLengthRange" min="50" max="300" value="150"> | |
<div style="font-size: 12px; color: #666; margin-top: 4px;"> | |
<span id="maxLengthValue">150</span> karakter maksimal | |
</div> | |
</div> | |
</div> | |
<!-- Chat Body --> | |
<div class="chat-body" id="chatBody"> | |
<!-- Welcome Message --> | |
<div class="message char"> | |
<div class="message-avatar"></div> | |
<div class="bubble"> | |
<div class="message-text">Hai! Aku siap ngobrol sama kamu nih. Mau bahas apa hari ini? 😊</div> | |
<div class="message-meta"> | |
<span class="timestamp" id="welcome-time"></span> | |
</div> | |
</div> | |
</div> | |
<!-- Typing Indicator --> | |
<div class="typing-indicator" id="typingIndicator"> | |
<div class="message-avatar"></div> | |
<div class="typing-dots"> | |
<span></span> | |
<span></span> | |
<span></span> | |
</div> | |
</div> | |
</div> | |
<!-- Footer --> | |
<div class="chat-footer"> | |
<!-- Emoji Picker --> | |
<div class="emoji-picker" id="emojiPicker"> | |
<div class="emoji-grid" id="emojiGrid"></div> | |
</div> | |
<div class="input-container"> | |
<button class="emoji-btn" onclick="toggleEmojiPicker()">😊</button> | |
<textarea class="message-input" id="messageInput" placeholder="Ketik pesan..." rows="1"></textarea> | |
<button class="send-btn" id="sendBtn" onclick="sendMessage()"> | |
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> | |
<path d="M2,21L23,12L2,3V10L17,12L2,14V21Z"/> | |
</svg> | |
</button> | |
</div> | |
</div> | |
</div> | |
<script> | |
// Configuration | |
const API_BASE = window.location.origin; | |
let isTyping = false; | |
let currentSettings = { | |
model: 'distil-gpt-2', | |
charName: 'Sayang', | |
userName: 'Kamu', | |
situation: 'Santai', | |
location: 'Ruang tamu', | |
maxLength: 150 | |
}; | |
// Emoji list | |
const emojis = [ | |
'😊', '😃', '😄', '😁', '😆', '😅', '😂', '🤣', | |
'😉', '😊', '😇', '🥰', '😍', '🤩', '😘', '😗', | |
'😚', '😙', '😋', '😛', '😜', '🤪', '😝', '🤑', | |
'🤗', '🤭', '🤫', '🤔', '🤐', '🤨', '😐', '😑', | |
'😶', '😏', '😒', '🙄', '😬', '🤥', '😔', '😪', | |
'🤤', '😴', '😷', '🤒', '🤕', '🤢', '🤮', '🤧', | |
'🥵', '🥶', '🥴', '😵', '🤯', '🤠', '🥳', '😎', | |
'🤓', '🧐', '😕', '😟', '🙁', '☹️', '😮', '😯', | |
'😲', '😳', '🥺', '😦', '😧', '😨', '😰', '😥', | |
'😢', '😭', '😱', '😖', '😣', '😞', '😓', '😩', | |
'😫', '🥱', '😤', '😡', '😠', '🤬', '😈', '👿', | |
'💀', '☠️', '💩', '🤡', '👹', '👺', '👻', '👽' | |
]; | |
// Initialize | |
document.addEventListener('DOMContentLoaded', function() { | |
initializeApp(); | |
loadEmojiPicker(); | |
setupEventListeners(); | |
setWelcomeTime(); | |
}); | |
function initializeApp() { | |
// Load settings from elements | |
document.getElementById('charName').textContent = currentSettings.charName; | |
document.getElementById('charNameInput').value = currentSettings.charName; | |
document.getElementById('userNameInput').value = currentSettings.userName; | |
document.getElementById('situationInput').value = currentSettings.situation; | |
document.getElementById('locationInput').value = currentSettings.location; | |
document.getElementById('maxLengthRange').value = currentSettings.maxLength; | |
document.getElementById('maxLengthValue').textContent = currentSettings.maxLength; | |
document.getElementById('modelSelect').value = currentSettings.model; | |
} | |
function setupEventListeners() { | |
// Message input | |
const messageInput = document.getElementById('messageInput'); | |
messageInput.addEventListener('keydown', function(e) { | |
if (e.key === 'Enter' && !e.shiftKey) { | |
e.preventDefault(); | |
sendMessage(); | |
} | |
}); | |
messageInput.addEventListener('input', function() { | |
this.style.height = 'auto'; | |
this.style.height = Math.min(this.scrollHeight, 120) + 'px'; | |
}); | |
// Settings inputs | |
document.getElementById('charNameInput').addEventListener('input', updateCharName); | |
document.getElementById('maxLengthRange').addEventListener('input', updateMaxLength); | |
// Model select | |
document.getElementById('modelSelect').addEventListener('change', function() { | |
currentSettings.model = this.value; | |
}); | |
// Click outside to close popups | |
document.addEventListener('click', function(e) { | |
if (!e.target.closest('.settings-popup') && !e.target.closest('.three-dots')) { | |
document.getElementById('settingsPopup').classList.remove('show'); | |
} | |
if (!e.target.closest('.emoji-picker') && !e.target.closest('.emoji-btn')) { | |
document.getElementById('emojiPicker').classList.remove('show'); | |
} | |
}); | |
} | |
function setWelcomeTime() { | |
const now = new Date(); | |
const timeString = now.toLocaleTimeString('id-ID', { | |
hour: '2-digit', | |
minute: '2-digit' | |
}); | |
document.getElementById('welcome-time').textContent = timeString; | |
} | |
function updateCharName() { | |
const newName = document.getElementById('charNameInput').value || 'Sayang'; | |
currentSettings.charName = newName; | |
document.getElementById('charName').textContent = newName; | |
} | |
function updateMaxLength() { | |
const value = document.getElementById('maxLengthRange').value; | |
currentSettings.maxLength = parseInt(value); | |
document.getElementById('maxLengthValue').textContent = value; | |
} | |
function toggleSettings() { | |
const popup = document.getElementById('settingsPopup'); | |
popup.classList.toggle('show'); | |
} | |
function toggleEmojiPicker() { | |
const picker = document.getElementById('emojiPicker'); | |
picker.classList.toggle('show'); | |
} | |
function loadEmojiPicker() { | |
const grid = document.getElementById('emojiGrid'); | |
emojis.forEach(emoji => { | |
const button = document.createElement('button'); | |
button.className = 'emoji-item'; | |
button.textContent = emoji; | |
button.onclick = () => insertEmoji(emoji); | |
grid.appendChild(button); | |
}); | |
} | |
function insertEmoji(emoji) { | |
const input = document.getElementById('messageInput'); | |
const start = input.selectionStart; | |
const end = input.selectionEnd; | |
const text = input.value; | |
input.value = text.substring(0, start) + emoji + text.substring(end); | |
input.selectionStart = input.selectionEnd = start + emoji.length; | |
input.focus(); | |
document.getElementById('emojiPicker').classList.remove('show'); | |
} | |
function getCurrentTime() { | |
const now = new Date(); | |
return now.toLocaleTimeString('id-ID', { | |
hour: '2-digit', | |
minute: '2-digit' | |
}); | |
} | |
function addMessage(text, isUser = false, showTime = true) { | |
const chatBody = document.getElementById('chatBody'); | |
const messageDiv = document.createElement('div'); | |
messageDiv.className = `message ${isUser ? 'user' : 'char'}`; | |
const time = showTime ? getCurrentTime() : ''; | |
const checkmarks = isUser ? '<span class="checkmarks">✓✓</span>' : ''; | |
messageDiv.innerHTML = ` | |
<div class="message-avatar"></div> | |
<div class="bubble"> | |
<div class="message-text">${text}</div> | |
<div class="message-meta"> | |
<span class="timestamp">${time}</span> | |
${checkmarks} | |
</div> | |
</div> | |
`; | |
chatBody.appendChild(messageDiv); | |
chatBody.scrollTop = chatBody.scrollHeight; | |
} | |
function showTyping() { | |
if (isTyping) return; | |
isTyping = true; | |
document.getElementById('status').textContent = 'mengetik...'; | |
document.getElementById('typingIndicator').classList.add('show'); | |
const chatBody = document.getElementById('chatBody'); | |
chatBody.scrollTop = chatBody.scrollHeight; | |
} | |
function hideTyping() { | |
if (!isTyping) return; | |
isTyping = false; | |
document.getElementById('status').textContent = 'online'; | |
document.getElementById('typingIndicator').classList.remove('show'); | |
} | |
async function sendMessage() { | |
const input = document.getElementById('messageInput'); | |
const message = input.value.trim(); | |
if (!message) return; | |
// Update settings from inputs | |
currentSettings.charName = document.getElementById('charNameInput').value || 'Sayang'; | |
currentSettings.userName = document.getElementById('userNameInput').value || 'Kamu'; | |
currentSettings.situation = document.getElementById('situationInput').value || 'Santai'; | |
currentSettings.location = document.getElementById('locationInput').value || 'Ruang tamu'; | |
// Add user message | |
addMessage(message, true); | |
input.value = ''; | |
input.style.height = 'auto'; | |
// Show typing | |
showTyping(); | |
// Disable send button | |
const sendBtn = document.getElementById('sendBtn'); | |
sendBtn.disabled = true; | |
try { | |
const response = await fetch(`${API_BASE}/chat`, { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
}, | |
body: JSON.stringify({ | |
message: message, | |
model: currentSettings.model, | |
situation: currentSettings.situation, | |
location: currentSettings.location, | |
char_name: currentSettings.charName, | |
user_name: currentSettings.userName, | |
max_length: currentSettings.maxLength | |
}) | |
}); | |
const data = await response.json(); | |
// Simulate typing delay | |
await new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 2000)); | |
hideTyping(); | |
if (data.status === 'success') { | |
addMessage(data.response); | |
} else { | |
addMessage('Maaf, ada masalah dengan sistem. Coba lagi ya! 😅'); | |
} | |
} catch (error) { | |
hideTyping(); | |
console.error('Error:', error); | |
addMessage('Ups, koneksi bermasalah. Coba lagi nanti ya! 🔄'); | |
} | |
// Re-enable send button | |
sendBtn.disabled = false; | |
} | |
// Handle online/offline status | |
window.addEventListener('online', function() { | |
document.getElementById('status').textContent = 'online'; | |
}); | |
window.addEventListener('offline', function() { | |
if (!isTyping) { | |
document.getElementById('status').textContent = 'offline'; | |
} | |
}); | |
</script> | |
</body> | |
</html> |