Projects / ui.html
Shreyansh234's picture
Create ui.html
a9e06a5 verified
Raw
History Blame Contribute Delete
9.34 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>AI Text + TTS</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Segoe UI', sans-serif;
background: #0f1117;
color: #e0e0e0;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 40px 16px;
}
h1 {
font-size: 1.8rem;
font-weight: 700;
color: #fff;
margin-bottom: 6px;
}
.subtitle {
color: #888;
font-size: 0.95rem;
margin-bottom: 32px;
text-align: center;
}
.card {
background: #1a1d27;
border: 1px solid #2a2d3e;
border-radius: 14px;
padding: 28px;
width: 100%;
max-width: 720px;
margin-bottom: 20px;
}
label {
display: block;
font-size: 0.85rem;
color: #aaa;
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 0.05em;
}
textarea {
width: 100%;
background: #0f1117;
border: 1px solid #2a2d3e;
border-radius: 8px;
color: #e0e0e0;
font-size: 1rem;
padding: 12px 14px;
resize: vertical;
min-height: 90px;
outline: none;
transition: border-color 0.2s;
}
textarea:focus { border-color: #ff7043; }
.btn-row {
display: flex;
gap: 12px;
margin-top: 16px;
flex-wrap: wrap;
}
button {
flex: 1;
padding: 12px 20px;
border: none;
border-radius: 8px;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: opacity 0.2s, transform 0.1s;
}
button:active { transform: scale(0.97); }
button:disabled { opacity: 0.4; cursor: not-allowed; }
#btn-text { background: #3a86ff; color: #fff; }
#btn-tts { background: #ff7043; color: #fff; }
#btn-both { background: #8b5cf6; color: #fff; }
#btn-clear { background: #2a2d3e; color: #aaa; flex: 0 0 auto; padding: 12px 16px; }
.result-card {
background: #1a1d27;
border: 1px solid #2a2d3e;
border-radius: 14px;
padding: 24px;
width: 100%;
max-width: 720px;
margin-bottom: 20px;
display: none;
}
.result-card.visible { display: block; }
.result-label {
font-size: 0.8rem;
color: #888;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 10px;
}
.answer-text {
font-size: 1.05rem;
line-height: 1.6;
color: #e0e0e0;
white-space: pre-wrap;
}
audio {
width: 100%;
margin-top: 14px;
border-radius: 8px;
accent-color: #ff7043;
}
.spinner {
display: inline-block;
width: 16px; height: 16px;
border: 2px solid rgba(255,255,255,0.3);
border-top-color: #fff;
border-radius: 50%;
animation: spin 0.7s linear infinite;
vertical-align: middle;
margin-right: 6px;
}
@keyframes spin { to { transform: rotate(360deg); } }
.api-box {
background: #1a1d27;
border: 1px solid #2a2d3e;
border-radius: 14px;
padding: 24px;
width: 100%;
max-width: 720px;
}
.api-box h2 {
font-size: 1rem;
color: #aaa;
margin-bottom: 14px;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.endpoint {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
background: #0f1117;
border-radius: 8px;
margin-bottom: 8px;
font-size: 0.88rem;
}
.method {
font-weight: 700;
font-size: 0.78rem;
padding: 3px 8px;
border-radius: 5px;
flex-shrink: 0;
}
.method.post { background: #ff704322; color: #ff7043; }
.method.get { background: #3a86ff22; color: #3a86ff; }
.path { color: #e0e0e0; font-family: monospace; }
.desc { color: #666; font-size: 0.82rem; margin-left: auto; }
.error-msg {
color: #ff5555;
font-size: 0.9rem;
margin-top: 10px;
display: none;
}
</style>
</head>
<body>
<h1>🎙️ AI Text + Text-to-Speech</h1>
<p class="subtitle">SmolLM2 generates answers · Kokoro converts them to speech</p>
<!-- Input card -->
<div class="card">
<label>Your Question</label>
<textarea id="question" placeholder="e.g. Explain zero-shot classification in simple words."></textarea>
<p class="error-msg" id="error-msg">Something went wrong. Please try again.</p>
<div class="btn-row">
<button id="btn-text" onclick="query('text')">📝 Text Only</button>
<button id="btn-tts" onclick="query('tts')">🔊 Text + Audio</button>
<button id="btn-both" onclick="query('both')">⚡ Full JSON</button>
<button id="btn-clear" onclick="clearAll()"></button>
</div>
</div>
<!-- Result card -->
<div class="result-card" id="result-card">
<div class="result-label">Response</div>
<div class="answer-text" id="answer-text"></div>
<audio id="audio-player" controls style="display:none;"></audio>
</div>
<!-- API reference -->
<div class="api-box">
<h2>API Endpoints</h2>
<div class="endpoint">
<span class="method get">GET</span>
<span class="path">/health</span>
<span class="desc">Model status</span>
</div>
<div class="endpoint">
<span class="method post">POST</span>
<span class="path">/api/text</span>
<span class="desc">JSON → text answer</span>
</div>
<div class="endpoint">
<span class="method post">POST</span>
<span class="path">/api/tts</span>
<span class="desc">JSON → WAV audio stream</span>
</div>
<div class="endpoint">
<span class="method post">POST</span>
<span class="path">/api/both</span>
<span class="desc">JSON → text + base64 audio</span>
</div>
</div>
<script>
const buttons = ['btn-text','btn-tts','btn-both'];
function setLoading(label) {
buttons.forEach(id => {
const b = document.getElementById(id);
b.disabled = true;
});
document.getElementById('error-msg').style.display = 'none';
const btn = document.getElementById('btn-' + label.replace('both','both').replace('text','text').replace('tts','tts'));
btn.innerHTML = `<span class="spinner"></span> Generating…`;
}
function resetButtons() {
document.getElementById('btn-text').innerHTML = '📝 Text Only';
document.getElementById('btn-tts').innerHTML = '🔊 Text + Audio';
document.getElementById('btn-both').innerHTML = '⚡ Full JSON';
buttons.forEach(id => document.getElementById(id).disabled = false);
}
function showError(msg) {
const el = document.getElementById('error-msg');
el.textContent = msg || 'Something went wrong.';
el.style.display = 'block';
}
function clearAll() {
document.getElementById('question').value = '';
document.getElementById('result-card').classList.remove('visible');
document.getElementById('answer-text').textContent = '';
document.getElementById('audio-player').style.display = 'none';
document.getElementById('error-msg').style.display = 'none';
resetButtons();
}
async function query(mode) {
const question = document.getElementById('question').value.trim();
if (!question) { showError('Please enter a question first.'); return; }
setLoading(mode);
const body = JSON.stringify({ question, voice: 'af_heart' });
const headers = { 'Content-Type': 'application/json' };
try {
if (mode === 'text') {
const res = await fetch('/api/text', { method: 'POST', headers, body });
if (!res.ok) throw new Error(await res.text());
const data = await res.json();
showResult(data.answer, null);
} else if (mode === 'tts') {
const res = await fetch('/api/tts', { method: 'POST', headers, body });
if (!res.ok) throw new Error(await res.text());
const answer = res.headers.get('X-Answer-Text') || '(see audio)';
const blob = await res.blob();
const url = URL.createObjectURL(blob);
showResult(answer, url);
} else if (mode === 'both') {
const res = await fetch('/api/both', { method: 'POST', headers, body });
if (!res.ok) throw new Error(await res.text());
const data = await res.json();
let audioUrl = null;
if (data.audio_base64) {
const bytes = Uint8Array.from(atob(data.audio_base64), c => c.charCodeAt(0));
const blob = new Blob([bytes], { type: 'audio/wav' });
audioUrl = URL.createObjectURL(blob);
}
showResult(data.answer, audioUrl);
}
} catch (err) {
showError('Error: ' + err.message);
} finally {
resetButtons();
}
}
function showResult(text, audioUrl) {
document.getElementById('answer-text').textContent = text;
const player = document.getElementById('audio-player');
if (audioUrl) {
player.src = audioUrl;
player.style.display = 'block';
player.play();
} else {
player.style.display = 'none';
}
document.getElementById('result-card').classList.add('visible');
}
</script>
</body>
</html>