Olya-AI / index.html
hansakafernando's picture
Create index.html
29ea1ed verified
Raw
History Blame Contribute Delete
16.7 kB
<!DOCTYPE html>
<html lang="si">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Olya.ai ✨</title>
<link href="https://fonts.googleapis.com/css2?family=Google+Sans:wght@300;400;500;600&family=Google+Sans+Display:wght@300;400;500&family=Noto+Sans+Sinhala:wght@300;400;500;600&display=swap" rel="stylesheet" />
<script src="https://cdn.tailwindcss.com"></script>
<style>
:root {
--grad-start: #4f86f7;
--grad-end: #9b59f7;
--surface: #ffffff;
--bg: #f8f9fc;
--bubble: #f0f2f7;
--border: #e5e8f0;
--text-primary: #1a1d2e;
--text-secondary: #6b7280;
--text-muted: #9ca3af;
--input-bg: #ffffff;
--shadow-soft: 0 4px 24px rgba(79, 134, 247, 0.08), 0 1px 6px rgba(0,0,0,0.05);
--shadow-input: 0 8px 40px rgba(79, 134, 247, 0.12), 0 2px 12px rgba(0,0,0,0.06);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Google Sans', 'Noto Sans Sinhala', sans-serif;
background: var(--bg);
color: var(--text-primary);
min-height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Subtle background mesh */
body::before {
content: '';
position: fixed;
inset: 0;
background:
radial-gradient(ellipse 60% 40% at 20% 10%, rgba(79,134,247,0.06) 0%, transparent 60%),
radial-gradient(ellipse 50% 50% at 80% 90%, rgba(155,89,247,0.05) 0%, transparent 60%);
pointer-events: none;
z-index: 0;
}
.gradient-text {
background: linear-gradient(135deg, var(--grad-start) 0%, var(--grad-end) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* ── Header ── */
header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: rgba(248, 249, 252, 0.85);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-bottom: 1px solid var(--border);
}
.header-inner {
max-width: 780px;
margin: 0 auto;
padding: 14px 24px;
display: flex;
align-items: center;
justify-content: space-between;
}
.logo-block { display: flex; flex-direction: column; gap: 2px; }
.logo-title {
font-family: 'Google Sans Display', 'Google Sans', sans-serif;
font-size: 1.35rem;
font-weight: 500;
letter-spacing: -0.02em;
line-height: 1;
}
.logo-caption {
font-size: 0.68rem;
color: var(--text-muted);
font-weight: 400;
letter-spacing: 0.01em;
}
.header-badge {
font-size: 0.7rem;
font-weight: 500;
color: var(--text-muted);
background: var(--bubble);
border: 1px solid var(--border);
padding: 4px 10px;
border-radius: 99px;
letter-spacing: 0.02em;
}
/* ── Chat scroll area ── */
#chat-scroll {
flex: 1;
overflow-y: auto;
padding: 90px 24px 140px;
scroll-behavior: smooth;
position: relative;
z-index: 1;
}
#chat-scroll::-webkit-scrollbar { width: 4px; }
#chat-scroll::-webkit-scrollbar-track { background: transparent; }
#chat-scroll::-webkit-scrollbar-thumb { background: var(--border); border-radius: 99px; }
#messages {
max-width: 780px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 28px;
}
/* ── Welcome screen ── */
#welcome {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 80px 24px 40px;
gap: 12px;
animation: fadeUp 0.6s ease both;
}
.welcome-orb {
width: 64px; height: 64px;
border-radius: 50%;
background: linear-gradient(135deg, #4f86f7 0%, #9b59f7 100%);
display: flex; align-items: center; justify-content: center;
font-size: 28px;
box-shadow: 0 8px 32px rgba(79,134,247,0.25);
margin-bottom: 8px;
animation: float 4s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-6px); }
}
.welcome-title {
font-family: 'Google Sans Display', sans-serif;
font-size: 2rem;
font-weight: 400;
letter-spacing: -0.03em;
line-height: 1.2;
}
.welcome-sub {
font-size: 0.92rem;
color: var(--text-secondary);
max-width: 360px;
line-height: 1.6;
}
/* Sinhala sub text */
.welcome-si {
font-family: 'Noto Sans Sinhala', sans-serif;
font-size: 0.88rem;
color: var(--text-muted);
margin-top: 4px;
}
/* Suggestion chips */
.chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
margin-top: 20px;
}
.chip {
font-size: 0.82rem;
font-weight: 400;
color: var(--text-secondary);
background: var(--input-bg);
border: 1px solid var(--border);
border-radius: 99px;
padding: 8px 16px;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
box-shadow: var(--shadow-soft);
}
.chip:hover {
border-color: #4f86f7;
color: #4f86f7;
transform: translateY(-1px);
box-shadow: 0 4px 16px rgba(79,134,247,0.15);
}
.chip.si { font-family: 'Noto Sans Sinhala', sans-serif; font-size: 0.78rem; }
/* ── Messages ── */
.msg-row { display: flex; flex-direction: column; gap: 4px; animation: fadeUp 0.35s ease both; }
@keyframes fadeUp {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
/* User message */
.msg-user {
align-self: flex-end;
max-width: 68%;
}
.msg-user .bubble {
background: var(--bubble);
border-radius: 20px 20px 4px 20px;
padding: 12px 18px;
font-size: 0.93rem;
line-height: 1.6;
color: var(--text-primary);
font-weight: 400;
}
.msg-user .bubble.si { font-family: 'Noto Sans Sinhala', sans-serif; font-size: 0.88rem; }
.msg-user .meta {
text-align: right;
font-size: 0.68rem;
color: var(--text-muted);
padding-right: 6px;
margin-top: 4px;
}
/* Olya (AI) message */
.msg-ai {
align-self: flex-start;
max-width: 85%;
}
.msg-ai .ai-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.ai-avatar {
width: 26px; height: 26px;
border-radius: 50%;
background: linear-gradient(135deg, #4f86f7 0%, #9b59f7 100%);
display: flex; align-items: center; justify-content: center;
font-size: 11px;
flex-shrink: 0;
}
.ai-name {
font-size: 0.78rem;
font-weight: 500;
color: var(--text-secondary);
letter-spacing: 0.01em;
}
.msg-ai .ai-body {
font-size: 0.95rem;
line-height: 1.75;
color: var(--text-primary);
font-weight: 400;
padding-left: 34px;
}
.msg-ai .ai-body.si {
font-family: 'Noto Sans Sinhala', sans-serif;
font-size: 0.9rem;
line-height: 1.9;
white-space: pre-wrap; /* Preserve line breaks from AI */
}
.msg-ai .ai-body strong { font-weight: 600; }
/* Typing indicator */
.typing-dots {
display: inline-flex;
align-items: center;
gap: 4px;
padding-left: 34px;
height: 24px;
}
.typing-dots span {
width: 6px; height: 6px;
border-radius: 50%;
background: linear-gradient(135deg, #4f86f7, #9b59f7);
animation: blink 1.2s ease-in-out infinite;
}
.typing-dots span:nth-child(2) { animation-delay: 0.2s; }
.typing-dots span:nth-child(3) { animation-delay: 0.4s; }
@keyframes blink {
0%, 80%, 100% { opacity: 0.2; transform: scale(0.8); }
40% { opacity: 1; transform: scale(1); }
}
/* ── Input bar ── */
#input-area {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 100;
padding: 16px 24px 28px;
background: linear-gradient(to top, rgba(248,249,252,1) 60%, rgba(248,249,252,0));
}
.input-wrap {
max-width: 780px;
margin: 0 auto;
display: flex;
align-items: flex-end;
gap: 0;
background: var(--input-bg);
border-radius: 28px;
border: 1.5px solid var(--border);
box-shadow: var(--shadow-input);
padding: 8px 8px 8px 20px;
transition: border-color 0.2s, box-shadow 0.2s;
}
.input-wrap:focus-within {
border-color: rgba(79,134,247,0.4);
box-shadow: 0 8px 40px rgba(79,134,247,0.18), 0 2px 12px rgba(0,0,0,0.06);
}
#user-input {
flex: 1;
border: none;
outline: none;
background: transparent;
font-family: 'Google Sans', 'Noto Sans Sinhala', sans-serif;
font-size: 0.92rem;
color: var(--text-primary);
resize: none;
min-height: 28px;
max-height: 160px;
line-height: 1.6;
padding: 4px 0;
}
#user-input::placeholder { color: var(--text-muted); }
.input-actions {
display: flex;
align-items: flex-end;
gap: 4px;
padding-bottom: 2px;
}
.send-btn {
width: 40px; height: 40px;
border-radius: 50%;
background: linear-gradient(135deg, #4f86f7 0%, #9b59f7 100%);
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
flex-shrink: 0;
}
.send-btn:hover {
transform: scale(1.06);
box-shadow: 0 4px 16px rgba(79,134,247,0.4);
}
.send-btn:active { transform: scale(0.97); }
.send-btn svg { width: 18px; height: 18px; fill: white; }
.input-hint {
text-align: center;
font-size: 0.68rem;
color: var(--text-muted);
margin-top: 10px;
}
/* Responsive */
@media (max-width: 600px) {
.msg-user { max-width: 85%; }
.msg-ai { max-width: 95%; }
.welcome-title { font-size: 1.5rem; }
}
</style>
</head>
<body>
<header>
<div class="header-inner">
<div class="logo-block">
<span class="logo-title gradient-text">Olya.ai ✨</span>
<span class="logo-caption">Generated By Hansaka P. Fernando.</span>
</div>
<span class="header-badge">සිංහල AI</span>
</div>
</header>
<div id="chat-scroll">
<div id="messages">
<div id="welcome">
<div class="welcome-orb"></div>
<div class="welcome-title">
Hello, I'm <span class="gradient-text">Olya</span>
</div>
<div class="welcome-sub">Your intelligent Sinhala AI assistant. Ask me anything in Sinhala or English.</div>
<div class="welcome-si">ඔබේ සිංහල AI සහකාරිය. ඕනෑම දෙයක් අසන්න.</div>
<div class="chips">
<div class="chip" onclick="useSuggestion(this)">Explain quantum physics simply</div>
<div class="chip si" onclick="useSuggestion(this)">ශ්‍රී ලංකාවේ ඉතිහාසය කියන්න</div>
<div class="chip" onclick="useSuggestion(this)">Write a short poem</div>
<div class="chip si" onclick="useSuggestion(this)">AI ගැන විස්තර කරන්න</div>
<div class="chip" onclick="useSuggestion(this)">Help me learn Sinhala</div>
<div class="chip si" onclick="useSuggestion(this)">අද කාලගුණය කොහොමද?</div>
</div>
</div>
</div>
</div>
<div id="input-area">
<div class="input-wrap">
<textarea
id="user-input"
rows="1"
placeholder="Message Olya… / Olya ට message කරන්න…"
onkeydown="handleKey(event)"
oninput="autoResize(this)"
></textarea>
<div class="input-actions">
<button class="send-btn" onclick="sendMessage()" title="Send">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
</svg>
</button>
</div>
</div>
<div class="input-hint">Olya can make mistakes. Verify important info.</div>
</div>
<script>
const messagesEl = document.getElementById('messages');
const welcomeEl = document.getElementById('welcome');
const inputEl = document.getElementById('user-input');
let chatStarted = false;
function isSinhala(text) {
return /[\u0D80-\u0DFF]/.test(text);
}
function now() {
return new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
function autoResize(el) {
el.style.height = 'auto';
el.style.height = Math.min(el.scrollHeight, 160) + 'px';
}
function handleKey(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
}
function useSuggestion(chip) {
inputEl.value = chip.textContent;
autoResize(inputEl);
inputEl.focus();
sendMessage();
}
// අලුත් API එකත් එක්ක වැඩ කරන sendMessage function එක
async function sendMessage() {
const text = inputEl.value.trim();
if (!text) return;
/* පළමු පණිවිඩය යැවූ පසු Welcome screen එක සැඟවීම */
if (!chatStarted) {
welcomeEl.style.display = 'none';
chatStarted = true;
}
/* User ගේ පණිවිඩය පෙන්වීම */
const si = isSinhala(text);
const userRow = document.createElement('div');
userRow.className = 'msg-row msg-user';
userRow.innerHTML = `
<div class="bubble${si ? ' si' : ''}">${escHtml(text)}</div>
<div class="meta">${now()}</div>
`;
messagesEl.appendChild(userRow);
inputEl.value = '';
inputEl.style.height = 'auto';
scrollBottom();
/* AI Typing Indicator එක පෙන්වීම */
const typingRow = document.createElement('div');
typingRow.className = 'msg-row msg-ai';
typingRow.id = 'typing';
typingRow.innerHTML = `
<div class="ai-header">
<div class="ai-avatar">✨</div>
<span class="ai-name">Olya</span>
</div>
<div class="typing-dots">
<span></span><span></span><span></span>
</div>
`;
messagesEl.appendChild(typingRow);
scrollBottom();
/* Backend එකට පණිවිඩය යැවීම */
try {
const response = await fetch('/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ message: text })
});
if (!response.ok) {
throw new Error('Network response error');
}
const data = await response.json();
// Typing indicator එක ඉවත් කිරීම
const typingIndicator = document.getElementById('typing');
if (typingIndicator) typingIndicator.remove();
// Olya ගේ පිළිතුර පෙන්වීම
addAiMessage(data.reply, isSinhala(data.reply));
scrollBottom();
} catch (error) {
console.error('Error fetching response:', error);
const typingIndicator = document.getElementById('typing');
if (typingIndicator) typingIndicator.remove();
addAiMessage("කණගාටුයි, සර්වර් එක හා සම්බන්ධ වීමේ දෝෂයක්. කරුණාකර නැවත උත්සාහ කරන්න.", true);
scrollBottom();
}
}
function addAiMessage(text, si = false) {
const row = document.createElement('div');
row.className = 'msg-row msg-ai';
// Convert markdown bold to simple HTML bold for better display
const formattedText = escHtml(text).replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
row.innerHTML = `
<div class="ai-header">
<div class="ai-avatar">✨</div>
<span class="ai-name">Olya &middot; ${now()}</span>
</div>
<div class="ai-body${si ? ' si' : ''}">${formattedText}</div>
`;
messagesEl.appendChild(row);
}
function scrollBottom() {
const sc = document.getElementById('chat-scroll');
sc.scrollTo({ top: sc.scrollHeight, behavior: 'smooth' });
}
function escHtml(s) {
const d = document.createElement('div');
d.appendChild(document.createTextNode(s));
return d.innerHTML;
}
</script>
</body>
</html>