tinygpt / index.html
avi080704's picture
Update index.html
4287a59 verified
Raw
History Blame Contribute Delete
13.5 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TinyGPT</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Bitcount+Prop+Single&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<style>
:root {
--glow: #C56E6E;
--bg: #FFFFFF;
--input: #FFB48F;
--btn: #CF613D;
--slider-green: #31ff49;
--slider-red: #ff4444;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { height: 100%; }
body {
font-family: 'Inter', sans-serif;
color: #333;
min-height: 100vh;
background:
radial-gradient(circle at 50% 42%,
var(--glow) 0%,
rgba(197, 110, 110, 0.55) 22%,
rgba(197, 110, 110, 0.12) 48%,
var(--bg) 72%);
background-color: var(--bg);
display: flex;
align-items: center;
justify-content: center;
overflow-x: hidden;
padding: 24px;
}
.stage {
width: 100%;
max-width: 1177px;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
/* ── Title ── */
h1.title {
font-family: 'Bitcount Prop Single', monospace;
color: #FFFFFF;
font-size: clamp(3rem, 11vw, 6.5rem);
line-height: 1;
letter-spacing: 0.02em;
text-shadow: 7px 9px 10px rgba(255, 0, 0, 0.25);
animation: titleDrop 0.9s cubic-bezier(0.22, 1, 0.36, 1) both;
user-select: none;
}
@keyframes titleDrop {
from { opacity: 0; transform: translateY(-150px); }
to { opacity: 1; transform: translateY(0); }
}
/* ── Subtitle ── */
p.subtitle {
font-family: 'Inter', sans-serif;
font-weight: 600;
color: rgba(255, 255, 255, 0.5);
font-size: clamp(0.95rem, 2.2vw, 1.4rem);
margin-top: 18px;
max-width: 680px;
animation: fadeIn 0.613s ease-out 0.55s both;
}
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes fadeInUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
/* ── Compose group (input + slider + button) ── */
.compose {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
margin-top: 40px;
transition: opacity 0.5s ease, transform 0.5s ease;
}
.compose.hidden {
opacity: 0;
transform: translateY(10px);
pointer-events: none;
}
.input-wrap {
position: relative;
width: 100%;
max-width: 1177px;
animation: fadeInUp 0.6s ease-out 0.9s both;
}
textarea#prompt {
width: 100%;
height: 40px;
min-height: 40px;
max-height: 40px;
resize: none;
border: none;
outline: none;
overflow-y: auto;
scrollbar-width: none;
-ms-overflow-style: none;
border-radius: 45px;
padding: 0 40px;
line-height: 40px;
font-family: 'Inter', sans-serif;
font-size: 1.05rem;
color: #5a2d20;
background: var(--input);
box-shadow: 0 10px 30px rgba(197, 110, 110, 0.25);
transition: box-shadow 0.3s ease, transform 0.3s ease;
white-space: nowrap;
}
textarea#prompt::-webkit-scrollbar { display: none; }
textarea#prompt::placeholder { color: rgba(90, 45, 32, 0.45); }
textarea#prompt:focus {
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.7),
0 12px 38px rgba(207, 97, 61, 0.45);
transform: translateY(-2px);
}
/* ── Slider pinned to input top-right (bare track, no label) ── */
.slider-pod {
position: absolute;
top: -22px;
right: 40px;
display: flex;
align-items: center;
}
.slider-tip {
position: absolute;
bottom: calc(100% + 12px);
right: -24px;
width: 280px;
max-width: calc(100vw - 32px);
background: rgba(40, 20, 18, 0.95);
color: #ffe;
font-size: 0.72rem;
font-weight: 500;
line-height: 1.5;
padding: 12px 14px;
border-radius: 12px;
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.3);
opacity: 0;
transform: translateY(6px);
pointer-events: none;
transition: opacity 0.2s ease, transform 0.2s ease;
z-index: 10;
text-align: left;
}
.slider-tip::after {
content: "";
position: absolute;
top: 100%;
right: 36px;
border: 6px solid transparent;
border-top-color: rgba(40, 20, 18, 0.95);
}
.slider-pod:hover .slider-tip {
opacity: 1;
transform: translateY(0);
}
input[type="range"]#temp {
-webkit-appearance: none;
appearance: none;
width: 150px;
height: 6px;
border-radius: 999px;
background: linear-gradient(to right, var(--slider-green), #e6e84a, var(--slider-red));
outline: none;
cursor: pointer;
}
input[type="range"]#temp::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 14px;
height: 14px;
border-radius: 50%;
background: #fff;
border: 2px solid rgba(0, 0, 0, 0.15);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
transition: transform 0.15s ease;
}
input[type="range"]#temp::-webkit-slider-thumb:active { transform: scale(1.2); }
input[type="range"]#temp::-moz-range-thumb {
width: 14px;
height: 14px;
border-radius: 50%;
background: #fff;
border: 2px solid rgba(0, 0, 0, 0.15);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
}
/* ── Generate button ── */
button#generate {
margin-top: 30px;
background: var(--btn);
color: #FFD04D;
font-family: 'Inter', sans-serif;
font-weight: 700;
font-size: 1.05rem;
border: none;
border-radius: 49px;
padding: 15px 46px;
cursor: pointer;
box-shadow: 0 8px 22px rgba(207, 97, 61, 0.4);
transition: transform 0.15s ease, box-shadow 0.25s ease, background 0.2s ease;
animation: titleDrop 0.9s cubic-bezier(0.22, 1, 0.36, 1) both;
}
button#generate:hover { background: #b9542f; box-shadow: 0 10px 28px rgba(207, 97, 61, 0.55); transform: translateY(-2px); }
button#generate:active { transform: translateY(0) scale(0.98); }
button#generate:disabled { opacity: 0.7; cursor: default; transform: none; }
/* ── Loading ── */
.loading {
margin-top: 40px;
color: rgba(255, 255, 255, 0.75);
font-weight: 600;
font-size: 1.1rem;
display: none;
}
.loading.show { display: block; animation: fadeIn 0.4s ease both; }
.loading .dots span {
animation: blink 1.4s infinite both;
display: inline-block;
}
.loading .dots span:nth-child(2) { animation-delay: 0.2s; }
.loading .dots span:nth-child(3) { animation-delay: 0.4s; }
@keyframes blink { 0%, 80%, 100% { opacity: 0.2; } 40% { opacity: 1; } }
/* ── Story view ── */
.story-view {
width: 100%;
max-width: 720px;
margin-top: 40px;
display: none;
flex-direction: column;
align-items: center;
}
.story-view.show { display: flex; animation: fadeInUp 0.7s ease-out both; }
.story-card {
width: 100%;
background: rgba(255, 255, 255, 0.72);
backdrop-filter: blur(8px);
border-radius: 28px;
padding: 34px 38px;
text-align: left;
line-height: 1.8;
font-size: 1.1rem;
color: #3a2a26;
box-shadow: 0 16px 44px rgba(197, 110, 110, 0.28);
white-space: pre-wrap;
}
.story-meta {
margin-top: 14px;
font-size: 0.78rem;
color: rgba(120, 74, 58, 0.85);
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
justify-content: center;
}
.story-meta .chip {
background: rgba(255, 255, 255, 0.6);
padding: 3px 10px;
border-radius: 999px;
}
button.again {
margin-top: 24px;
background: transparent;
color: var(--btn);
border: 2px solid var(--btn);
border-radius: 49px;
padding: 11px 32px;
font-family: 'Inter', sans-serif;
font-weight: 600;
font-size: 0.95rem;
cursor: pointer;
transition: background 0.2s ease, color 0.2s ease;
}
button.again:hover { background: var(--btn); color: #fff; }
/* ── Responsive ── */
@media (max-width: 900px) {
.story-view { max-width: 100%; }
.story-card { padding: 30px 30px; font-size: 1.05rem; }
}
@media (max-width: 560px) {
body { padding: 16px; align-items: flex-start; padding-top: 8vh; }
.compose { margin-top: 30px; }
.slider-pod { right: 16px; top: -24px; }
input[type="range"]#temp { width: 120px; }
/* keep the single-line input intact: only horizontal padding changes */
textarea#prompt { padding: 0 24px; font-size: 1rem; }
.slider-tip { font-size: 0.7rem; padding: 10px 12px; }
button#generate { padding: 14px 40px; font-size: 1rem; }
.story-card { padding: 24px 22px; border-radius: 22px; font-size: 1rem; }
button.again { padding: 11px 28px; }
}
@media (max-width: 380px) {
h1.title { font-size: clamp(2.4rem, 13vw, 3.2rem); }
input[type="range"]#temp { width: 96px; }
.slider-pod { right: 12px; }
textarea#prompt { padding: 0 18px; }
}
</style>
</head>
<body>
<div class="stage">
<h1 class="title">TinyGPT</h1>
<p class="subtitle" id="subtitle">A Generative Pre-Trained Transformer, that specializes in ending your sentences with stories.<br>ALWAYS</p>
<div class="compose" id="compose">
<div class="input-wrap">
<textarea id="prompt" rows="2"></textarea>
<div class="slider-pod">
<div class="slider-tip">This is the temperature parameter. Higher temperature makes the sampling space larger, more tokens to choose from (lower probability ones). Higher temperature means more creative stories, lower means less and more deterministic.</div>
<input type="range" id="temp" min="0.1" max="1.5" step="0.1" value="0.5" />
</div>
</div>
<button id="generate">Generate</button>
</div>
<div class="loading" id="loading">
Writing your story<span class="dots"><span>.</span><span>.</span><span>.</span></span>
</div>
<div class="story-view" id="story-view">
<div class="story-card" id="story-text"></div>
<div class="story-meta" id="story-meta"></div>
<button class="again" id="again">Write another</button>
</div>
</div>
<script>
const API = (location.protocol === "http:" || location.protocol === "https:")
? location.origin
: "http://localhost:8000";
const promptEl = document.getElementById("prompt");
const tempEl = document.getElementById("temp");
const genBtn = document.getElementById("generate");
const compose = document.getElementById("compose");
const subtitle = document.getElementById("subtitle");
const loading = document.getElementById("loading");
const storyView = document.getElementById("story-view");
const storyText = document.getElementById("story-text");
const storyMeta = document.getElementById("story-meta");
const againBtn = document.getElementById("again");
async function loadTempInfo() {
try {
const r = await fetch(`${API}/temperature-info`);
if (!r.ok) return;
const info = await r.json();
if (info.min != null) tempEl.min = info.min;
if (info.max != null) tempEl.max = info.max;
if (info.step != null) tempEl.step = info.step;
if (info.default != null) tempEl.value = info.default;
} catch {}
}
// Ctrl+Enter to generate
promptEl.addEventListener("keydown", e => {
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) generate();
});
async function generate() {
const prompt = promptEl.value.trim();
if (!prompt) { promptEl.focus(); return; }
genBtn.disabled = true;
compose.classList.add("hidden");
subtitle.style.transition = "opacity 0.5s ease";
subtitle.style.opacity = "0";
loading.classList.add("show");
try {
const res = await fetch(`${API}/generate`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
prompt,
temperature: parseFloat(tempEl.value),
max_new_tokens: 200,
}),
});
const data = await res.json();
loading.classList.remove("show");
if (res.ok) {
showStory(prompt, data);
} else {
showError(data.detail || "Something went wrong.");
}
} catch (e) {
loading.classList.remove("show");
showError("Could not reach the server. Is it running?");
}
}
function showStory(prompt, data) {
storyText.textContent = (data.generated_text || "").trim();
storyMeta.innerHTML = "";
const chips = [];
if (data.temperature_label) chips.push(`${data.temperature_label} · temp ${data.temperature}`);
if (data.response_time_ms != null) chips.push(`${data.response_time_ms} ms`);
chips.forEach(c => {
const s = document.createElement("span");
s.className = "chip";
s.textContent = c;
storyMeta.appendChild(s);
});
storyView.classList.add("show");
}
function showError(msg) {
storyText.textContent = msg;
storyMeta.innerHTML = "";
storyView.classList.add("show");
}
function reset() {
storyView.classList.remove("show");
loading.classList.remove("show");
compose.classList.remove("hidden");
subtitle.style.opacity = "";
genBtn.disabled = false;
promptEl.focus();
}
againBtn.addEventListener("click", reset);
genBtn.addEventListener("click", generate);
loadTempInfo();
</script>
</body>
</html>