Spaces:
Sleeping
Sleeping
| <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 · ${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> |