mirror of
https://github.com/karpathy/nanochat.git
synced 2026-05-19 14:17:36 +00:00
- Tablet (700px): shrink illustrations, hide steam, wider message bubbles - Mobile (480px): smaller hero text, compact input bar, stacked footer, tighter spacing, scaled-down toran, properly sized illustrations - Small phones (360px): further reduced hero/illustration sizes - Safe area insets for notched phones Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
921 lines
42 KiB
HTML
921 lines
42 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
|
<title>समोसा चाट — samosaChaat</title>
|
|
<link rel="icon" type="image/svg+xml" href="/logo.svg">
|
|
<link href="https://fonts.googleapis.com/css2?family=Baloo+2:wght@400;600;700;800&family=Caveat:wght@400;600;700&family=Great+Vibes&display=swap" rel="stylesheet">
|
|
<style>
|
|
:root {
|
|
color-scheme: light;
|
|
--gold: #e8a838;
|
|
--brown: #8b4d0a;
|
|
--cream: #fff8e7;
|
|
--green-chutney: #2d8a4e;
|
|
--red-chutney: #c0392b;
|
|
--warm-grey: #d4c4a0;
|
|
--light-cream: #fffdf5;
|
|
}
|
|
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
html, body { height: 100%; }
|
|
|
|
body {
|
|
font-family: 'Caveat', cursive, sans-serif;
|
|
background-color: #ffffff;
|
|
color: #111827;
|
|
min-height: 100dvh;
|
|
overflow-x: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
/* ===== NAVBAR ===== */
|
|
.landing-nav {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
padding: 1.1rem 2.2rem;
|
|
position: relative;
|
|
z-index: 10;
|
|
flex-shrink: 0;
|
|
}
|
|
.nav-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.6rem;
|
|
}
|
|
.nav-left .home-icon { cursor: pointer; transition: transform 0.2s; }
|
|
.nav-left .home-icon:hover { transform: scale(1.08); }
|
|
.nav-brand {
|
|
font-family: 'Caveat', cursive;
|
|
font-size: 1.35rem;
|
|
font-weight: 600;
|
|
color: #333;
|
|
text-decoration: none;
|
|
position: relative;
|
|
}
|
|
.nav-brand::after {
|
|
content: '';
|
|
position: absolute;
|
|
bottom: -2px; left: 0;
|
|
width: 100%; height: 1.5px;
|
|
background: #555;
|
|
border-radius: 1px;
|
|
transform: rotate(-0.5deg);
|
|
}
|
|
.nav-right {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1.2rem;
|
|
font-family: 'Caveat', cursive;
|
|
font-size: 1.05rem;
|
|
color: #555;
|
|
padding-top: 0.3rem;
|
|
}
|
|
.nav-right a { color: #555; text-decoration: none; transition: color 0.2s; }
|
|
.nav-right a:hover { color: var(--brown); }
|
|
.inference-badge {
|
|
font-family: 'Caveat', cursive;
|
|
font-size: 0.9rem;
|
|
padding: 2px 10px;
|
|
border-radius: 12px;
|
|
border: 1px solid var(--warm-grey);
|
|
color: var(--brown);
|
|
background: var(--light-cream);
|
|
display: none;
|
|
}
|
|
.inference-badge.visible { display: inline; }
|
|
.inference-badge.local {
|
|
border-color: var(--green-chutney);
|
|
color: var(--green-chutney);
|
|
background: #eafaf0;
|
|
}
|
|
|
|
/* --- Toran (center navbar) --- */
|
|
.toran-wrap {
|
|
position: absolute;
|
|
left: 50%;
|
|
top: 0;
|
|
transform: translateX(-50%);
|
|
transform-origin: top center;
|
|
animation: pendulum 3s ease-in-out infinite alternate;
|
|
z-index: 5;
|
|
}
|
|
|
|
/* ===== LANDING ELEMENTS (fade/slide out on chat) ===== */
|
|
.hero {
|
|
text-align: center;
|
|
padding: 1.5rem 1rem 0;
|
|
flex-shrink: 0;
|
|
transition: opacity 0.5s ease, transform 0.5s ease;
|
|
}
|
|
.hero-hindi {
|
|
font-family: 'Baloo 2', cursive;
|
|
font-weight: 800;
|
|
font-size: clamp(3rem, 7vw, 5.5rem);
|
|
color: #1a1a1a;
|
|
line-height: 1.1;
|
|
transform: rotate(-1deg);
|
|
margin-bottom: -0.35em;
|
|
position: relative;
|
|
z-index: 2;
|
|
}
|
|
.hero-english {
|
|
font-family: 'Great Vibes', cursive;
|
|
font-weight: 400;
|
|
font-size: clamp(2rem, 5vw, 3.8rem);
|
|
color: rgba(30,30,30,0.55);
|
|
line-height: 1;
|
|
transform: rotate(0.5deg);
|
|
position: relative;
|
|
z-index: 1;
|
|
margin-top: -0.1em;
|
|
}
|
|
|
|
/* --- Illustrations (absolute positioned, slide off-screen) --- */
|
|
.illust-left {
|
|
position: fixed;
|
|
bottom: 5%;
|
|
left: 5%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
transition: transform 0.7s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.5s ease;
|
|
z-index: 5;
|
|
}
|
|
.illust-left .samosa-svg {
|
|
animation: float 2.5s ease-in-out infinite;
|
|
}
|
|
.explore-tag {
|
|
font-family: 'Caveat', cursive;
|
|
font-size: 1.15rem;
|
|
color: #5a3e1b;
|
|
background: #f5edd6;
|
|
padding: 3px 18px;
|
|
border: 1px solid #d4c4a0;
|
|
transform: rotate(-3deg);
|
|
box-shadow: 1px 2px 5px rgba(0,0,0,0.08);
|
|
margin-top: 6px;
|
|
display: inline-block;
|
|
border-radius: 2px;
|
|
}
|
|
.illust-right {
|
|
position: fixed;
|
|
bottom: 5%;
|
|
right: 5%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
transition: transform 0.7s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.5s ease;
|
|
z-index: 5;
|
|
}
|
|
.illust-right .kettle-wrap {
|
|
animation: wobble 3s ease-in-out infinite;
|
|
position: relative;
|
|
}
|
|
.kettle-steam {
|
|
position: absolute;
|
|
top: -40px;
|
|
right: -10px;
|
|
pointer-events: none;
|
|
}
|
|
.steam-wisp { transform-box: fill-box; transform-origin: bottom center; }
|
|
.steam-w1 { animation: steamFloat 2.8s ease-in-out infinite; }
|
|
.steam-w2 { animation: steamFloat 2.8s ease-in-out infinite 0.7s; }
|
|
.steam-w3 { animation: steamFloat 2.8s ease-in-out infinite 1.4s; }
|
|
.chai-label {
|
|
font-family: 'Caveat', cursive;
|
|
font-size: 1.15rem;
|
|
color: #5a3e1b;
|
|
background: #f5edd6;
|
|
padding: 3px 18px;
|
|
border: 1px solid #d4c4a0;
|
|
transform: rotate(2deg);
|
|
box-shadow: 1px 2px 5px rgba(0,0,0,0.08);
|
|
margin-top: 6px;
|
|
display: inline-block;
|
|
border-radius: 2px;
|
|
}
|
|
|
|
/* Slide-out states */
|
|
.illust-left.slide-out { transform: translateX(-120vw); opacity: 0; }
|
|
.illust-right.slide-out { transform: translateX(120vw); opacity: 0; }
|
|
.hero.fade-out { opacity: 0; transform: translateY(-30px); pointer-events: none; }
|
|
.doodle.fade-out { opacity: 0 !important; }
|
|
.landing-footer.fade-out { opacity: 0; pointer-events: none; }
|
|
|
|
/* --- Ambient doodles --- */
|
|
.doodle {
|
|
position: fixed;
|
|
pointer-events: none;
|
|
opacity: 0.35;
|
|
z-index: 0;
|
|
transition: opacity 0.5s ease;
|
|
}
|
|
.doodle-1 { top: 18%; right: 6%; transform: rotate(25deg); }
|
|
.doodle-2 { bottom: 28%; left: 3%; transform: rotate(-15deg); }
|
|
.doodle-3 { top: 45%; right: 3%; transform: rotate(140deg); }
|
|
.doodle-4 { bottom: 18%; left: 42%; transform: rotate(10deg); }
|
|
.doodle-5 { top: 32%; left: 7%; transform: rotate(-40deg); }
|
|
|
|
/* --- Footer --- */
|
|
.landing-footer {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 0.8rem 2.2rem;
|
|
font-family: 'Caveat', cursive;
|
|
font-size: 0.95rem;
|
|
color: #999;
|
|
flex-shrink: 0;
|
|
transition: opacity 0.5s ease;
|
|
margin-top: auto;
|
|
}
|
|
.landing-footer a { color: #999; text-decoration: none; transition: color 0.2s; }
|
|
.landing-footer a:hover { color: #666; }
|
|
|
|
/* ===== ANIMATIONS ===== */
|
|
@keyframes pendulum {
|
|
0% { transform: translateX(-50%) rotate(-4deg); }
|
|
100% { transform: translateX(-50%) rotate(4deg); }
|
|
}
|
|
@keyframes float {
|
|
0%, 100% { transform: translateY(0); }
|
|
50% { transform: translateY(-8px); }
|
|
}
|
|
@keyframes wobble {
|
|
0%, 100% { transform: rotate(-1.5deg); }
|
|
50% { transform: rotate(1.5deg); }
|
|
}
|
|
@keyframes steamFloat {
|
|
0% { opacity: 0; transform: translateY(4px) scaleX(0.8); }
|
|
35% { opacity: 0.5; }
|
|
70% { opacity: 0.35; }
|
|
100% { opacity: 0; transform: translateY(-18px) scaleX(1.4); }
|
|
}
|
|
|
|
/* ===== CHAT AREA ===== */
|
|
.chat-container {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
background-color: #ffffff;
|
|
display: none;
|
|
}
|
|
.chat-container.active { display: block; }
|
|
.chat-wrapper {
|
|
max-width: 48rem;
|
|
margin: 0 auto;
|
|
padding: 1.5rem 1.5rem 2rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
/* --- Download banner --- */
|
|
.download-banner {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
padding: 0.5rem 1.5rem;
|
|
background: var(--light-cream);
|
|
border-bottom: 1px solid #f0e8d8;
|
|
font-family: 'Caveat', cursive;
|
|
font-size: 1rem;
|
|
color: var(--brown);
|
|
}
|
|
.progress-track {
|
|
flex: 1; height: 6px; background: #efe6d0; border-radius: 3px; overflow: hidden; max-width: 220px;
|
|
}
|
|
.progress-fill {
|
|
height: 100%; width: 0%;
|
|
background: linear-gradient(90deg, var(--gold), #d4940a);
|
|
border-radius: 3px; transition: width 0.3s ease;
|
|
}
|
|
.download-cancel {
|
|
background: none; border: none; font-family: 'Caveat', cursive;
|
|
font-size: 0.95rem; color: var(--red-chutney); cursor: pointer; padding: 2px 6px;
|
|
}
|
|
.download-cancel:hover { text-decoration: underline; }
|
|
.download-done { color: var(--green-chutney); font-weight: 600; }
|
|
|
|
/* --- Messages --- */
|
|
.message {
|
|
display: flex;
|
|
justify-content: flex-start;
|
|
margin-bottom: 0.5rem;
|
|
color: #0d0d0d;
|
|
}
|
|
.message.assistant { justify-content: flex-start; }
|
|
.message.user { justify-content: flex-end; }
|
|
|
|
.message-content {
|
|
white-space: pre-wrap;
|
|
line-height: 1.65;
|
|
max-width: 100%;
|
|
font-family: ui-sans-serif, -apple-system, system-ui, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
font-size: 1rem;
|
|
}
|
|
.message.assistant .message-content {
|
|
background: transparent; border: none; cursor: pointer;
|
|
border-radius: 0.5rem; padding: 0.5rem; margin-left: -0.5rem;
|
|
transition: background-color 0.2s ease;
|
|
}
|
|
.message.assistant .message-content:hover { background-color: var(--light-cream); }
|
|
.message.user .message-content {
|
|
background-color: var(--cream); border: 1px solid #f0e0b8;
|
|
border-radius: 1.25rem; padding: 0.8rem 1rem; max-width: 65%;
|
|
cursor: pointer; transition: background-color 0.2s ease, border-color 0.2s ease;
|
|
}
|
|
.message.user .message-content:hover { background-color: #fff0d0; border-color: var(--gold); }
|
|
.message.console .message-content {
|
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'Courier New', monospace;
|
|
font-size: 0.875rem; background-color: var(--light-cream); border: 1px solid #f0e8d8;
|
|
padding: 0.75rem 1rem; color: #5a3e1b; max-width: 80%; border-radius: 0.75rem;
|
|
}
|
|
.error-message {
|
|
background-color: #fee2e2; border: 1px solid #fecaca; color: #b91c1c;
|
|
padding: 0.75rem 1rem; border-radius: 0.75rem; margin-top: 0.5rem;
|
|
}
|
|
|
|
/* --- Steam typing indicator --- */
|
|
.steam-typing-wrap { display: inline-flex; align-items: flex-end; height: 28px; padding: 0 4px; }
|
|
.steam-typing-wrap .st {
|
|
width: 3px; border-radius: 2px; background: var(--warm-grey);
|
|
margin: 0 3px; animation: steamType 1.6s ease-in-out infinite; transform-origin: bottom;
|
|
}
|
|
.steam-typing-wrap .st:nth-child(1) { height: 14px; animation-delay: 0s; }
|
|
.steam-typing-wrap .st:nth-child(2) { height: 18px; animation-delay: 0.25s; }
|
|
.steam-typing-wrap .st:nth-child(3) { height: 12px; animation-delay: 0.5s; }
|
|
.steam-typing-wrap .st:nth-child(4) { height: 16px; animation-delay: 0.75s; }
|
|
@keyframes steamType {
|
|
0%, 100% { opacity: 0.25; transform: scaleY(0.6); }
|
|
50% { opacity: 0.8; transform: scaleY(1); }
|
|
}
|
|
|
|
/* --- Input --- */
|
|
.input-container {
|
|
background-color: #ffffff;
|
|
padding: 1rem;
|
|
padding-bottom: calc(1rem + env(safe-area-inset-bottom));
|
|
flex-shrink: 0;
|
|
z-index: 10;
|
|
}
|
|
/* In landing mode, push input to vertically center with illustrations */
|
|
.input-container.landing-mode {
|
|
margin-top: auto;
|
|
margin-bottom: 0;
|
|
}
|
|
.input-wrapper {
|
|
max-width: 48rem; margin: 0 auto; display: flex; gap: 0.75rem; align-items: flex-end;
|
|
}
|
|
.chat-input {
|
|
flex: 1; padding: 0.8rem 1rem;
|
|
border: 1px solid var(--warm-grey); border-radius: 0.75rem;
|
|
background-color: #ffffff; color: #111827;
|
|
font-family: ui-sans-serif, -apple-system, system-ui, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
font-size: 1rem; line-height: 1.5; resize: none; outline: none;
|
|
min-height: 54px; max-height: 200px;
|
|
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
|
}
|
|
.chat-input::placeholder { color: #b8a88a; }
|
|
.chat-input:focus { border-color: var(--gold); box-shadow: 0 0 0 3px rgba(232, 168, 56, 0.15); }
|
|
.send-button {
|
|
flex-shrink: 0; padding: 0; width: 54px; height: 54px;
|
|
border: 1px solid var(--gold); border-radius: 0.75rem;
|
|
background-color: var(--gold); color: #ffffff;
|
|
display: flex; align-items: center; justify-content: center;
|
|
cursor: pointer; transition: background-color 0.2s ease, border-color 0.2s ease;
|
|
}
|
|
.send-button:hover:not(:disabled) { background-color: #d4940a; border-color: #d4940a; }
|
|
.send-button:disabled {
|
|
cursor: not-allowed; border-color: #e0d5c0; background-color: #efe6d0; color: #c4b090;
|
|
}
|
|
.new-conversation-btn {
|
|
width: 32px; height: 32px; padding: 0;
|
|
border: 1px solid #e0d5c0; border-radius: 0.5rem;
|
|
background-color: #ffffff; color: #8b7355;
|
|
cursor: pointer; display: none; align-items: center; justify-content: center;
|
|
transition: all 0.2s ease;
|
|
}
|
|
.new-conversation-btn.visible { display: flex; }
|
|
.new-conversation-btn:hover { background-color: var(--cream); border-color: var(--warm-grey); color: var(--brown); }
|
|
|
|
/* ===== RESPONSIVE ===== */
|
|
/* --- Tablet --- */
|
|
@media (max-width: 700px) {
|
|
.landing-nav { padding: 0.8rem 1rem; }
|
|
.nav-right .social-text { display: none; }
|
|
.hero-hindi { font-size: 2.5rem; }
|
|
.hero-english { font-size: 1.6rem; }
|
|
.illust-left { left: 2%; }
|
|
.illust-right { right: 2%; }
|
|
.illust-left svg { width: 130px; height: auto; }
|
|
.illust-right svg.kettle-svg { width: 130px; height: auto; }
|
|
.kettle-steam { display: none; }
|
|
.doodle { display: none; }
|
|
.landing-footer { padding: 0.6rem 1rem; font-size: 0.85rem; }
|
|
.chat-wrapper { padding: 1.5rem 1rem 2rem; }
|
|
.message.user .message-content { max-width: 80%; }
|
|
}
|
|
|
|
/* --- Mobile --- */
|
|
@media (max-width: 480px) {
|
|
.landing-nav { padding: 0.6rem 0.8rem; }
|
|
.nav-brand { font-size: 1.15rem; }
|
|
.toran-wrap svg { width: 36px; height: 75px; }
|
|
.hero { padding: 1rem 0.5rem 0; }
|
|
.hero-hindi { font-size: 1.8rem; }
|
|
.hero-english { font-size: 1.2rem; }
|
|
.illust-left { left: 1%; bottom: 8%; }
|
|
.illust-right { right: 1%; bottom: 8%; }
|
|
.illust-left svg { width: 90px; }
|
|
.illust-right svg.kettle-svg { width: 90px; }
|
|
.explore-tag, .chai-label { font-size: 0.95rem; padding: 2px 12px; }
|
|
.input-container { padding: 0.6rem 0.5rem; padding-bottom: calc(0.6rem + env(safe-area-inset-bottom)); }
|
|
.input-wrapper { gap: 0.5rem; }
|
|
.chat-input { min-height: 46px; font-size: 0.95rem; padding: 0.6rem 0.8rem; }
|
|
.send-button { width: 46px; height: 46px; }
|
|
.chat-wrapper { padding: 1rem 0.75rem 1.5rem; }
|
|
.message.user .message-content { max-width: 85%; padding: 0.6rem 0.8rem; font-size: 0.95rem; }
|
|
.message.assistant .message-content { font-size: 0.95rem; }
|
|
.landing-footer {
|
|
flex-direction: column;
|
|
gap: 0.3rem;
|
|
text-align: center;
|
|
padding: 0.6rem 0.8rem;
|
|
font-size: 0.8rem;
|
|
}
|
|
.new-conversation-btn { width: 28px; height: 28px; }
|
|
.inference-badge { font-size: 0.8rem; padding: 1px 8px; }
|
|
}
|
|
|
|
/* --- Small phones --- */
|
|
@media (max-width: 360px) {
|
|
.hero-hindi { font-size: 1.5rem; }
|
|
.hero-english { font-size: 1rem; }
|
|
.illust-left svg { width: 70px; }
|
|
.illust-right svg.kettle-svg { width: 70px; }
|
|
.explore-tag, .chai-label { font-size: 0.85rem; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- Navbar (always visible) -->
|
|
<nav class="landing-nav">
|
|
<div class="nav-left">
|
|
<svg class="home-icon" onclick="resetToLanding()" viewBox="0 0 30 30" width="30" height="30" fill="none" stroke="#444" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M4,16 L15.2,5 L26,16"/>
|
|
<path d="M7,14.5 L7,26 L23,26 L23,14.5"/>
|
|
<rect x="12" y="19" width="6" height="7" rx="0.5"/>
|
|
<rect x="8.5" y="16" width="3" height="2.8" rx="0.3"/>
|
|
<rect x="18.5" y="16" width="3" height="2.8" rx="0.3"/>
|
|
</svg>
|
|
<a class="nav-brand" href="#">samosaChaat</a>
|
|
</div>
|
|
|
|
<!-- Toran -->
|
|
<div class="toran-wrap">
|
|
<svg viewBox="0 0 50 105" width="48" height="100" fill="none">
|
|
<line x1="25" y1="0" x2="25" y2="88" stroke="#555" stroke-width="1"/>
|
|
<path d="M23,16 C18,22 13,34 14,46 C14,49 16,50 18,47 C20,43 22,28 23,20Z" fill="#2E8B57" stroke="#1a6b3a" stroke-width="0.4"/>
|
|
<path d="M23,16 L22,11" stroke="#5a7c4f" stroke-width="1.4" stroke-linecap="round"/>
|
|
<path d="M27,20 C32,26 37,38 36,50 C36,53 34,54 32,51 C30,47 28,32 27,24Z" fill="#34A85A" stroke="#228B44" stroke-width="0.4"/>
|
|
<path d="M27,20 L28,15" stroke="#5a7c4f" stroke-width="1.4" stroke-linecap="round"/>
|
|
<path d="M23,35 C17,42 13,52 15,61 C15,64 17,64 19,62 C21,58 22,45 23,39Z" fill="#2E8B57" stroke="#1a6b3a" stroke-width="0.4"/>
|
|
<path d="M23,35 L22,31" stroke="#5a7c4f" stroke-width="1.3" stroke-linecap="round"/>
|
|
<path d="M27,42 C31,47 34,54 33,61 C33,63 31,63 30,61 C29,58 28,49 27,45Z" fill="#34A85A" stroke="#228B44" stroke-width="0.4"/>
|
|
<path d="M27,42 L28,39" stroke="#5a7c4f" stroke-width="1.2" stroke-linecap="round"/>
|
|
<!-- Lemon -->
|
|
<ellipse cx="25" cy="82" rx="12" ry="10.5" fill="#F4D03F"/>
|
|
<ellipse cx="25" cy="82" rx="9" ry="8" fill="#F7DC6F" opacity="0.5"/>
|
|
<ellipse cx="23" cy="80" rx="5" ry="4" fill="#FAEA7A" opacity="0.35"/>
|
|
<path d="M25,71 Q26,68 25,66" stroke="#C5A828" stroke-width="1.5" stroke-linecap="round" fill="none"/>
|
|
<circle cx="25" cy="65.5" r="1.5" fill="#8B9A46"/>
|
|
</svg>
|
|
</div>
|
|
|
|
<div class="nav-right">
|
|
<span id="inferenceMode" class="inference-badge">Cloud</span>
|
|
<button id="newConvBtn" class="new-conversation-btn" onclick="newConversation()" title="New Conversation (Ctrl+Shift+N)">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M12 5v14"></path><path d="M5 12h14"></path>
|
|
</svg>
|
|
</button>
|
|
<a href="https://instagram.com/samosachaat.art" class="social-text" target="_blank" rel="noopener">@samosachaat</a>
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- Download banner (hidden until WebGPU engine calls API) -->
|
|
<div id="downloadBanner" class="download-banner" style="display:none">
|
|
<span id="downloadText">Downloading model for faster local inference...</span>
|
|
<div class="progress-track"><div id="progressFill" class="progress-fill"></div></div>
|
|
<span id="downloadPercent"></span>
|
|
<button id="downloadCancel" class="download-cancel" onclick="window.samosaChaat._cancelDownload && window.samosaChaat._cancelDownload()">Cancel</button>
|
|
</div>
|
|
|
|
<!-- Hero (fades out on chat) -->
|
|
<section class="hero" id="heroSection">
|
|
<h1 class="hero-hindi">समोसा चाट</h1>
|
|
<h2 class="hero-english">samosaChaat</h2>
|
|
</section>
|
|
|
|
<!-- Chat messages (hidden initially, grows on chat) -->
|
|
<div class="chat-container" id="chatContainer">
|
|
<div class="chat-wrapper" id="chatWrapper"></div>
|
|
</div>
|
|
|
|
<!-- Samosa illustration (left, slides out) -->
|
|
<div class="illust-left" id="samosaIllust">
|
|
<svg class="samosa-svg" viewBox="0 0 220 195" width="200" height="175">
|
|
<defs>
|
|
<linearGradient id="samosaG" x1="30%" y1="0%" x2="80%" y2="100%">
|
|
<stop offset="0%" stop-color="#edb44c"/><stop offset="35%" stop-color="#d4940a"/>
|
|
<stop offset="75%" stop-color="#b87a08"/><stop offset="100%" stop-color="#8b5e0a"/>
|
|
</linearGradient>
|
|
<linearGradient id="samosaHighlight" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
<stop offset="0%" stop-color="#f5d080" stop-opacity="0.5"/>
|
|
<stop offset="100%" stop-color="#d4940a" stop-opacity="0"/>
|
|
</linearGradient>
|
|
<filter id="samosaSh"><feDropShadow dx="1" dy="3" stdDeviation="3" flood-color="#00000015"/></filter>
|
|
</defs>
|
|
<ellipse cx="108" cy="178" rx="58" ry="7" fill="#00000008"/>
|
|
<path d="M108,18 C88,18 38,72 28,128 C24,150 35,168 55,170 L162,170 C182,168 192,150 188,128 C178,72 128,18 108,18Z" fill="url(#samosaG)" filter="url(#samosaSh)"/>
|
|
<path d="M108,18 C88,18 38,72 28,128 C24,150 35,168 55,170 L162,170 C182,168 192,150 188,128 C178,72 128,18 108,18Z" fill="url(#samosaHighlight)"/>
|
|
<path d="M108,28 Q102,65 92,105 Q87,128 82,148" stroke="#a06808" stroke-width="1.2" fill="none" opacity="0.35"/>
|
|
<path d="M108,28 Q114,65 124,105 Q129,128 134,148" stroke="#a06808" stroke-width="1.2" fill="none" opacity="0.35"/>
|
|
<path d="M90,50 Q95,47 100,50 Q105,53 110,50 Q115,47 120,50 Q125,53 128,50" stroke="#8b5e0a" stroke-width="1" fill="none" opacity="0.3"/>
|
|
<path d="M55,170 Q80,164 108,166 Q136,168 162,170" stroke="#8b5e0a" stroke-width="1.5" fill="none" opacity="0.4"/>
|
|
<path d="M88,50 Q94,68 96,90" stroke="#f5d080" stroke-width="2.5" fill="none" opacity="0.35" stroke-linecap="round"/>
|
|
<ellipse cx="185" cy="158" rx="14" ry="9" fill="#2d8a4e" opacity="0.65"/>
|
|
<ellipse cx="183" cy="156" rx="9" ry="5.5" fill="#3aa85e" opacity="0.45"/>
|
|
<circle cx="60" cy="175" r="2" fill="#c4940a" opacity="0.3"/>
|
|
<circle cx="165" cy="176" r="1.5" fill="#c4940a" opacity="0.25"/>
|
|
<circle cx="145" cy="180" r="1.8" fill="#b87a08" opacity="0.2"/>
|
|
</svg>
|
|
<span class="explore-tag">Samosa</span>
|
|
</div>
|
|
|
|
<!-- Chai Kettle (right, slides out) -->
|
|
<div class="illust-right" id="kettleIllust">
|
|
<div class="kettle-wrap">
|
|
<svg class="kettle-steam" viewBox="0 0 60 55" width="60" height="55">
|
|
<g class="steam-w1"><path d="M15,48 Q10,36 18,26 Q26,16 18,4" stroke="#aaa" stroke-width="2" fill="none" stroke-linecap="round" opacity="0.4"/></g>
|
|
<g class="steam-w2"><path d="M30,48 Q36,36 28,26 Q20,16 28,4" stroke="#bbb" stroke-width="1.8" fill="none" stroke-linecap="round" opacity="0.3"/></g>
|
|
<g class="steam-w3"><path d="M45,48 Q40,36 46,28 Q52,18 44,6" stroke="#aaa" stroke-width="1.5" fill="none" stroke-linecap="round" opacity="0.3"/></g>
|
|
</svg>
|
|
<svg class="kettle-svg" viewBox="0 0 200 185" width="180" height="165">
|
|
<defs>
|
|
<linearGradient id="kettleG" x1="20%" y1="0%" x2="80%" y2="100%">
|
|
<stop offset="0%" stop-color="#d4a543"/><stop offset="45%" stop-color="#b8862a"/>
|
|
<stop offset="100%" stop-color="#8b6914"/>
|
|
</linearGradient>
|
|
<radialGradient id="kettleHL" cx="35%" cy="38%">
|
|
<stop offset="0%" stop-color="#e8c860" stop-opacity="0.4"/>
|
|
<stop offset="100%" stop-color="#d4a543" stop-opacity="0"/>
|
|
</radialGradient>
|
|
<filter id="kettleSh"><feDropShadow dx="1" dy="3" stdDeviation="3" flood-color="#00000012"/></filter>
|
|
</defs>
|
|
<ellipse cx="95" cy="178" rx="55" ry="6" fill="#00000008"/>
|
|
<path d="M28,118 C28,155 55,175 98,175 C141,175 168,155 168,118 C168,92 152,78 142,73 L54,73 C44,78 28,92 28,118Z" fill="url(#kettleG)" filter="url(#kettleSh)"/>
|
|
<path d="M28,118 C28,155 55,175 98,175 C141,175 168,155 168,118 C168,92 152,78 142,73 L54,73 C44,78 28,92 28,118Z" fill="url(#kettleHL)"/>
|
|
<rect x="58" y="54" width="80" height="19" rx="3" fill="#a07828"/>
|
|
<path d="M54,57 Q98,42 142,57" fill="#a07828" stroke="#8b6914" stroke-width="0.8"/>
|
|
<circle cx="98" cy="44" r="5" fill="#8b6914" stroke="#705510" stroke-width="0.8"/>
|
|
<path d="M56,52 Q38,18 98,10 Q158,18 140,52" stroke="#8b6914" stroke-width="4.5" fill="none" stroke-linecap="round"/>
|
|
<path d="M56,52 Q38,18 98,10 Q158,18 140,52" stroke="#c4a040" stroke-width="1.5" fill="none" stroke-linecap="round" opacity="0.3"/>
|
|
<path d="M163,98 Q178,90 182,76 Q184,70 180,66" stroke="#a07828" stroke-width="6" fill="none" stroke-linecap="round"/>
|
|
<path d="M163,98 Q178,90 182,76 Q184,70 180,66" stroke="#c4a040" stroke-width="2" fill="none" stroke-linecap="round" opacity="0.25"/>
|
|
<ellipse cx="98" cy="112" rx="66" ry="2.5" fill="#c4a040" opacity="0.25"/>
|
|
<ellipse cx="98" cy="128" rx="64" ry="2" fill="#c4a040" opacity="0.18"/>
|
|
<path d="M58,103 Q63,98 68,103 Q73,108 78,103" stroke="#705510" stroke-width="0.7" fill="none" opacity="0.25"/>
|
|
<path d="M118,103 Q123,98 128,103 Q133,108 138,103" stroke="#705510" stroke-width="0.7" fill="none" opacity="0.25"/>
|
|
<path d="M55,95 Q60,110 62,130" stroke="#e8c860" stroke-width="3" fill="none" opacity="0.2" stroke-linecap="round"/>
|
|
</svg>
|
|
</div>
|
|
<span class="chai-label">Chai</span>
|
|
</div>
|
|
|
|
<!-- Ambient doodles -->
|
|
<div class="doodle doodle-1"><svg viewBox="0 0 28 14" width="32" height="16"><path d="M2,7 C2,3 8,1 14,2 C20,3 26,7 26,10 C26,12 24,13 22,12 C18,10 10,5 6,6 C4,6.5 2,9 2,7Z" fill="#34A85A" opacity="0.7"/></svg></div>
|
|
<div class="doodle doodle-2"><svg viewBox="0 0 18 18" width="20" height="20"><circle cx="9" cy="9" r="7" fill="#F4D03F" opacity="0.6"/><circle cx="9" cy="9" r="5" fill="#F7DC6F" opacity="0.35"/></svg></div>
|
|
<div class="doodle doodle-3"><svg viewBox="0 0 24 12" width="28" height="14"><path d="M2,6 C2,3 6,1 12,2 C18,3 22,6 22,9 C22,11 20,11 18,10 C14,8 8,4 4,5 C3,5.5 2,7.5 2,6Z" fill="#2E8B57" opacity="0.6"/></svg></div>
|
|
<div class="doodle doodle-4"><svg viewBox="0 0 16 16" width="16" height="16"><circle cx="8" cy="8" r="6" fill="#F4D03F" opacity="0.45"/></svg></div>
|
|
<div class="doodle doodle-5"><svg viewBox="0 0 22 11" width="24" height="12"><path d="M2,5 C2,2 6,1 11,2 C16,3 20,5 20,8 C20,10 18,10 16,9 C12,7 8,3 4,4.5 C3,5 2,7 2,5Z" fill="#34A85A" opacity="0.55"/></svg></div>
|
|
|
|
<!-- Input -->
|
|
<div class="input-container landing-mode" id="inputContainer">
|
|
<div class="input-wrapper">
|
|
<textarea id="chatInput" class="chat-input" placeholder="Ask samosaChaat anything..." rows="1" onkeydown="handleKeyDown(event)"></textarea>
|
|
<button id="sendButton" class="send-button" onclick="sendMessage()" disabled>
|
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M22 2L11 13"></path><path d="M22 2l-7 20-4-9-9-4 20-7z"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Footer (always at very bottom) -->
|
|
<footer class="landing-footer" id="landingFooter">
|
|
<span>© 2026 samosachaat.art</span>
|
|
<span style="font-size:0.85rem; color:#bbb;">Built on <a href="https://github.com/karpathy/nanochat" target="_blank" rel="noopener" style="color:#b8a88a;">nanochat</a> by Andrej Karpathy</span>
|
|
<a href="#">Terms and Policies</a>
|
|
</footer>
|
|
|
|
<script>
|
|
// ================================================================
|
|
// STATE
|
|
// ================================================================
|
|
const API_URL = '';
|
|
const chatContainer = document.getElementById('chatContainer');
|
|
const chatWrapper = document.getElementById('chatWrapper');
|
|
const chatInput = document.getElementById('chatInput');
|
|
const sendButton = document.getElementById('sendButton');
|
|
|
|
let messages = [];
|
|
let isGenerating = false;
|
|
let isChatMode = false;
|
|
let currentTemperature = 0.8;
|
|
let currentTopK = 50;
|
|
|
|
// ================================================================
|
|
// TRANSITION: Landing → Chat
|
|
// ================================================================
|
|
function enterChatMode() {
|
|
if (isChatMode) return;
|
|
isChatMode = true;
|
|
|
|
// Slide samosa left, kettle right
|
|
document.getElementById('samosaIllust').classList.add('slide-out');
|
|
document.getElementById('kettleIllust').classList.add('slide-out');
|
|
|
|
// Fade out hero, doodles, footer
|
|
document.getElementById('heroSection').classList.add('fade-out');
|
|
document.getElementById('landingFooter').classList.add('fade-out');
|
|
document.querySelectorAll('.doodle').forEach(d => d.classList.add('fade-out'));
|
|
|
|
// Show chat area and new conversation button
|
|
chatContainer.classList.add('active');
|
|
document.getElementById('newConvBtn').classList.add('visible');
|
|
document.getElementById('inputContainer').classList.remove('landing-mode');
|
|
|
|
// Show inference badge
|
|
document.getElementById('inferenceMode').classList.add('visible');
|
|
}
|
|
|
|
function resetToLanding() {
|
|
if (!isChatMode) return;
|
|
isChatMode = false;
|
|
|
|
// Slide illustrations back
|
|
document.getElementById('samosaIllust').classList.remove('slide-out');
|
|
document.getElementById('kettleIllust').classList.remove('slide-out');
|
|
|
|
// Fade in hero, doodles, footer
|
|
document.getElementById('heroSection').classList.remove('fade-out');
|
|
document.getElementById('landingFooter').classList.remove('fade-out');
|
|
document.querySelectorAll('.doodle').forEach(d => d.classList.remove('fade-out'));
|
|
|
|
// Hide chat area
|
|
chatContainer.classList.remove('active');
|
|
document.getElementById('newConvBtn').classList.remove('visible');
|
|
document.getElementById('inferenceMode').classList.remove('visible');
|
|
document.getElementById('inputContainer').classList.add('landing-mode');
|
|
|
|
// Reset chat
|
|
messages = [];
|
|
chatWrapper.innerHTML = '';
|
|
chatInput.value = '';
|
|
chatInput.style.height = 'auto';
|
|
sendButton.disabled = false;
|
|
isGenerating = false;
|
|
chatInput.focus();
|
|
}
|
|
|
|
// ================================================================
|
|
// CHAT FUNCTIONALITY
|
|
// ================================================================
|
|
chatInput.addEventListener('input', function() {
|
|
this.style.height = 'auto';
|
|
this.style.height = Math.min(this.scrollHeight, 200) + 'px';
|
|
sendButton.disabled = !this.value.trim() || isGenerating;
|
|
});
|
|
|
|
function handleKeyDown(event) {
|
|
if (event.key === 'Enter' && !event.shiftKey) {
|
|
event.preventDefault();
|
|
sendMessage();
|
|
}
|
|
}
|
|
|
|
document.addEventListener('keydown', function(event) {
|
|
if (event.ctrlKey && event.shiftKey && event.key === 'N') {
|
|
event.preventDefault();
|
|
if (!isGenerating) newConversation();
|
|
}
|
|
});
|
|
|
|
function newConversation() {
|
|
messages = [];
|
|
chatWrapper.innerHTML = '';
|
|
chatInput.value = '';
|
|
chatInput.style.height = 'auto';
|
|
sendButton.disabled = false;
|
|
isGenerating = false;
|
|
chatInput.focus();
|
|
}
|
|
|
|
function addMessage(role, content, messageIndex = null) {
|
|
const messageDiv = document.createElement('div');
|
|
messageDiv.className = `message ${role}`;
|
|
const contentDiv = document.createElement('div');
|
|
contentDiv.className = 'message-content';
|
|
contentDiv.textContent = content;
|
|
|
|
if (role === 'user' && messageIndex !== null) {
|
|
contentDiv.setAttribute('data-message-index', messageIndex);
|
|
contentDiv.setAttribute('title', 'Click to edit and restart from here');
|
|
contentDiv.addEventListener('click', function() { if (!isGenerating) editMessage(messageIndex); });
|
|
}
|
|
if (role === 'assistant' && messageIndex !== null) {
|
|
contentDiv.setAttribute('data-message-index', messageIndex);
|
|
contentDiv.setAttribute('title', 'Click to regenerate this response');
|
|
contentDiv.addEventListener('click', function() { if (!isGenerating) regenerateMessage(messageIndex); });
|
|
}
|
|
|
|
messageDiv.appendChild(contentDiv);
|
|
chatWrapper.appendChild(messageDiv);
|
|
chatContainer.scrollTop = chatContainer.scrollHeight;
|
|
return contentDiv;
|
|
}
|
|
|
|
function editMessage(messageIndex) {
|
|
if (messageIndex < 0 || messageIndex >= messages.length) return;
|
|
if (messages[messageIndex].role !== 'user') return;
|
|
chatInput.value = messages[messageIndex].content;
|
|
chatInput.style.height = 'auto';
|
|
chatInput.style.height = Math.min(chatInput.scrollHeight, 200) + 'px';
|
|
messages = messages.slice(0, messageIndex);
|
|
const allMessages = chatWrapper.querySelectorAll('.message');
|
|
for (let i = messageIndex; i < allMessages.length; i++) allMessages[i].remove();
|
|
sendButton.disabled = false;
|
|
chatInput.focus();
|
|
}
|
|
|
|
async function generateAssistantResponse() {
|
|
isGenerating = true;
|
|
sendButton.disabled = true;
|
|
const assistantContent = addMessage('assistant', '');
|
|
assistantContent.innerHTML = '<div class="steam-typing-wrap"><div class="st"></div><div class="st"></div><div class="st"></div><div class="st"></div></div>';
|
|
|
|
try {
|
|
if (window.samosaChaat._mode === 'local' && window.samosaChaat.generateLocal) {
|
|
let fullResponse = '';
|
|
assistantContent.textContent = '';
|
|
for await (const token of window.samosaChaat.generateLocal(messages)) {
|
|
fullResponse += token;
|
|
assistantContent.textContent = fullResponse;
|
|
chatContainer.scrollTop = chatContainer.scrollHeight;
|
|
}
|
|
const idx = messages.length;
|
|
messages.push({ role: 'assistant', content: fullResponse });
|
|
assistantContent.setAttribute('data-message-index', idx);
|
|
assistantContent.setAttribute('title', 'Click to regenerate this response');
|
|
assistantContent.addEventListener('click', function() { if (!isGenerating) regenerateMessage(idx); });
|
|
} else {
|
|
const response = await fetch(`${API_URL}/chat/completions`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ messages, temperature: currentTemperature, top_k: currentTopK, max_tokens: 512 }),
|
|
});
|
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
|
const reader = response.body.getReader();
|
|
const decoder = new TextDecoder();
|
|
let fullResponse = '';
|
|
assistantContent.textContent = '';
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
for (const line of decoder.decode(value).split('\n')) {
|
|
if (line.startsWith('data: ')) {
|
|
try {
|
|
const data = JSON.parse(line.slice(6));
|
|
if (data.token) { fullResponse += data.token; assistantContent.textContent = fullResponse; chatContainer.scrollTop = chatContainer.scrollHeight; }
|
|
} catch (e) {}
|
|
}
|
|
}
|
|
}
|
|
const idx = messages.length;
|
|
messages.push({ role: 'assistant', content: fullResponse });
|
|
assistantContent.setAttribute('data-message-index', idx);
|
|
assistantContent.setAttribute('title', 'Click to regenerate this response');
|
|
assistantContent.addEventListener('click', function() { if (!isGenerating) regenerateMessage(idx); });
|
|
}
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
assistantContent.innerHTML = `<div class="error-message">Error: ${error.message}</div>`;
|
|
} finally {
|
|
isGenerating = false;
|
|
sendButton.disabled = !chatInput.value.trim();
|
|
}
|
|
}
|
|
|
|
async function regenerateMessage(messageIndex) {
|
|
if (messageIndex < 0 || messageIndex >= messages.length) return;
|
|
if (messages[messageIndex].role !== 'assistant') return;
|
|
messages = messages.slice(0, messageIndex);
|
|
const allMessages = chatWrapper.querySelectorAll('.message');
|
|
for (let i = messageIndex; i < allMessages.length; i++) allMessages[i].remove();
|
|
await generateAssistantResponse();
|
|
}
|
|
|
|
function handleSlashCommand(command) {
|
|
const parts = command.trim().split(/\s+/);
|
|
const cmd = parts[0].toLowerCase();
|
|
const arg = parts[1];
|
|
if (cmd === '/temperature') {
|
|
if (arg === undefined) { addMessage('console', `Current temperature: ${currentTemperature}`); }
|
|
else { const t = parseFloat(arg); if (isNaN(t)||t<0||t>2) addMessage('console','Invalid temperature. Must be between 0.0 and 2.0'); else { currentTemperature=t; addMessage('console',`Temperature set to ${t}`); } }
|
|
return true;
|
|
} else if (cmd === '/topk') {
|
|
if (arg === undefined) { addMessage('console', `Current top-k: ${currentTopK}`); }
|
|
else { const k = parseInt(arg); if (isNaN(k)||k<1||k>200) addMessage('console','Invalid top-k. Must be between 1 and 200'); else { currentTopK=k; addMessage('console',`Top-k set to ${k}`); } }
|
|
return true;
|
|
} else if (cmd === '/clear') { newConversation(); return true; }
|
|
else if (cmd === '/help') {
|
|
addMessage('console', 'Available commands:\n/temperature [value] - Get/set temperature (0.0-2.0)\n/topk [value] - Get/set top-k (1-200)\n/clear - Clear conversation\n/help - Show this help');
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
async function sendMessage() {
|
|
const message = chatInput.value.trim();
|
|
if (!message || isGenerating) return;
|
|
|
|
// Enter chat mode on first message
|
|
if (!isChatMode) enterChatMode();
|
|
|
|
if (message.startsWith('/')) {
|
|
chatInput.value = ''; chatInput.style.height = 'auto';
|
|
handleSlashCommand(message);
|
|
return;
|
|
}
|
|
|
|
chatInput.value = ''; chatInput.style.height = 'auto';
|
|
const userMessageIndex = messages.length;
|
|
messages.push({ role: 'user', content: message });
|
|
addMessage('user', message, userMessageIndex);
|
|
await generateAssistantResponse();
|
|
}
|
|
|
|
sendButton.disabled = false;
|
|
|
|
// ================================================================
|
|
// WEBGPU INTEGRATION HOOKS
|
|
// ================================================================
|
|
window.samosaChaat = {
|
|
_mode: 'cloud',
|
|
setInferenceMode(mode) {
|
|
this._mode = mode;
|
|
const badge = document.getElementById('inferenceMode');
|
|
if (mode === 'local') { badge.textContent = 'Local GPU'; badge.classList.add('local'); }
|
|
else { badge.textContent = 'Cloud'; badge.classList.remove('local'); }
|
|
},
|
|
updateDownloadProgress(loaded, total) {
|
|
const banner = document.getElementById('downloadBanner');
|
|
const fill = document.getElementById('progressFill');
|
|
const pct = document.getElementById('downloadPercent');
|
|
const text = document.getElementById('downloadText');
|
|
banner.style.display = 'flex';
|
|
const percent = Math.round((loaded / total) * 100);
|
|
fill.style.width = percent + '%';
|
|
text.textContent = `Downloading model... ${(loaded/1048576).toFixed(0)}/${(total/1048576).toFixed(0)} MB`;
|
|
pct.textContent = `(${percent}%)`;
|
|
},
|
|
onModelReady() {
|
|
const banner = document.getElementById('downloadBanner');
|
|
document.getElementById('progressFill').style.width = '100%';
|
|
document.getElementById('downloadPercent').textContent = '';
|
|
document.getElementById('downloadText').innerHTML = '<span class="download-done">Model loaded on your GPU — responses are now local!</span>';
|
|
document.getElementById('downloadCancel').style.display = 'none';
|
|
this.setInferenceMode('local');
|
|
setTimeout(() => { banner.style.display = 'none'; }, 4000);
|
|
},
|
|
generateLocal: null,
|
|
_cancelDownload: null
|
|
};
|
|
|
|
// Health check
|
|
fetch(`${API_URL}/health`)
|
|
.then(r => r.json())
|
|
.then(data => console.log('Engine status:', data))
|
|
.catch(error => console.error('Engine not available:', error));
|
|
|
|
chatInput.focus();
|
|
</script>
|
|
</body>
|
|
</html>
|