Vineeth Sai
Initial project commit
1604eba
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="color-scheme" content="dark" />
<title>AI Article Summarizer · Qwen + Kokoro</title>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet" />
<style>
:root{
--bg-0:#0a0e16; --bg-1:#0e1420; --bg-2:#111927;
--glass: rgba(255,255,255,.05);
--glass-border: rgba(255,255,255,.1);
--muted: #9aa4bf; --text: #e7ecf8; --text-dim: #b8c2d9;
--accent-1:#6d6aff; --accent-2:#7b5cff; --accent-3:#00d4ff;
--ok:#21d19f; --warn:#ffb84d; --err:#ff6b6b;
--ring: 0 0 0 1px rgba(255,255,255,.08), 0 0 0 4px rgba(124, 58, 237, .15);
--shadow: 0 25px 70px rgba(0,0,0,.5), 0 10px 25px rgba(0,0,0,.4);
--shadow-lg: 0 35px 90px rgba(0,0,0,.6), 0 15px 35px rgba(0,0,0,.5);
--radius-xl:24px; --radius-lg:18px; --radius-md:14px; --radius-sm:10px;
--transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
--transition-slow: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
*{box-sizing:border-box}
html,body{height:100%; scroll-behavior: smooth}
body{
margin:0;
font-family:Inter, system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Helvetica, Arial;
color:var(--text); font-weight: 400; line-height: 1.6;
background:
radial-gradient(1400px 700px at -15% -15%, rgba(109,106,255,.2), transparent 55%),
radial-gradient(1000px 600px at 115% -15%, rgba(0,212,255,.16), transparent 58%),
radial-gradient(1400px 1000px at 50% 115%, rgba(123,92,255,.16), transparent 65%),
linear-gradient(180deg, var(--bg-0) 0%, var(--bg-1) 40%, var(--bg-2) 100%);
background-attachment: fixed;
overflow-y:auto; overflow-x: hidden;
}
/* Enhanced progress bar */
.bar{
position:fixed; inset:0 0 auto 0; height:3px; z-index:9999;
background: linear-gradient(90deg, var(--accent-3), var(--accent-2), var(--accent-1));
background-size:300% 100%; transform:scaleX(0); transform-origin:left;
box-shadow:0 0 20px rgba(0,212,255,.5), 0 0 40px rgba(123,92,255,.3);
transition:transform .25s cubic-bezier(0.4, 0, 0.2, 1);
animation:bar-move 2.5s linear infinite;
}
@keyframes bar-move{0%{background-position:0 0}100%{background-position:300% 0}}
/* Floating particles animation */
.particles{
position: fixed; inset: 0; pointer-events: none; z-index: 1;
background-image:
radial-gradient(2px 2px at 20px 30px, rgba(255,255,255,.1), transparent),
radial-gradient(2px 2px at 40px 70px, rgba(109,106,255,.1), transparent),
radial-gradient(1px 1px at 90px 40px, rgba(0,212,255,.1), transparent),
radial-gradient(1px 1px at 130px 80px, rgba(123,92,255,.1), transparent);
background-repeat: repeat;
background-size: 200px 100px;
animation: float 20s linear infinite;
}
@keyframes float{0%{transform:translateY(0px)}100%{transform:translateY(-100px)}}
.wrap{max-width:1100px; margin:80px auto; padding:0 28px; position: relative; z-index: 2}
/* Enhanced hero section */
.hero{
display:flex; flex-direction:column; align-items:center; gap:18px;
margin-bottom:36px; text-align:center; animation: fadeInUp 0.8s ease-out;
}
@keyframes fadeInUp{0%{opacity:0;transform:translateY(30px)}100%{opacity:1;transform:translateY(0)}}
.hero-badge{
display:inline-flex; align-items:center; gap:12px; padding:10px 16px; border-radius:999px;
background:linear-gradient(180deg, rgba(255,255,255,.08), rgba(255,255,255,.03));
border:1px solid var(--glass-border); backdrop-filter: blur(12px);
box-shadow: var(--shadow); transition: var(--transition);
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse{0%,100%{transform:scale(1)}50%{transform:scale(1.02)}}
.dot{
width:10px;height:10px;border-radius:50%; background:var(--warn);
box-shadow:0 0 0 8px rgba(255,184,77,.15); transition: var(--transition);
animation: dotPulse 1.5s ease-in-out infinite;
}
.dot.ready{
background:var(--ok); box-shadow:0 0 0 8px rgba(33,209,159,.15);
animation: dotReady 0.5s ease-out;
}
@keyframes dotPulse{0%,100%{opacity:1}50%{opacity:0.6}}
@keyframes dotReady{0%{transform:scale(0.8)}100%{transform:scale(1)}}
.hero h1{
font-size: clamp(32px, 5.5vw, 52px); margin:0; font-weight:800;
letter-spacing:-.03em; line-height:1.05; animation: fadeInUp 0.8s 0.2s both;
}
.grad-text{
background: linear-gradient(135deg, #f8faff, #d4e0ff 25%, #a8d8ff 50%, #c8b8ff 75%, #e8d4ff);
-webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent;
background-size: 200% 200%; animation: gradientShift 4s ease-in-out infinite;
}
@keyframes gradientShift{0%,100%{background-position:0% 50%}50%{background-position:100% 50%}}
.hero p{
margin:0; color:var(--text-dim); font-size:16px; font-weight: 300;
animation: fadeInUp 0.8s 0.4s both;
}
/* Enhanced glass panel */
.panel{
position:relative;
background:linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.03));
border:1px solid var(--glass-border); border-radius: var(--radius-xl);
padding:32px; box-shadow: var(--shadow-lg); overflow:hidden;
backdrop-filter: blur(20px); animation: fadeInUp 0.8s 0.6s both;
}
.panel::before{
content:""; position:absolute; inset:-1px; border-radius:inherit; padding:1px;
background:linear-gradient(135deg, rgba(175,134,255,.4) 0%, rgba(0,212,255,.2) 50%, rgba(175,134,255,.4) 100%);
-webkit-mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite:xor; mask-composite: exclude; pointer-events:none;
opacity:.9; animation: borderGlow 3s ease-in-out infinite;
}
@keyframes borderGlow{0%,100%{opacity:.9}50%{opacity:.6}}
/* Enhanced segmented control */
.seg{
display:inline-flex; padding:8px;
background:rgba(0,0,0,.4); border:1px solid rgba(255,255,255,.12);
border-radius:999px; gap:8px; backdrop-filter: blur(8px);
box-shadow: inset 0 2px 8px rgba(0,0,0,.3);
}
.seg button{
border:0; border-radius:999px; padding:12px 18px; color:var(--text);
background:transparent; cursor:pointer; font-weight:600; font-size:14px;
transition: var(--transition); position: relative; overflow: hidden;
}
.seg button::before{
content: ''; position: absolute; inset: 0; border-radius: inherit;
background: linear-gradient(135deg, #7b5cff 0%, #00d4ff 100%);
opacity: 0; transition: var(--transition);
}
.seg button.active{color:#0a0e16; font-weight: 700}
.seg button.active::before{opacity: 1}
.seg button span{position: relative; z-index: 1}
/* Enhanced form elements */
.grid{display:grid; grid-template-columns:1fr auto; gap:16px; align-items:center}
.input, .textarea{
width:100%;
background:linear-gradient(180deg, rgba(0,0,0,.4), rgba(0,0,0,.3));
border:1px solid rgba(255,255,255,.15);
border-radius:18px; padding:16px 20px; color:var(--text); font-size:16px;
outline:none; transition: var(--transition); backdrop-filter: blur(8px);
box-shadow: inset 0 2px 8px rgba(0,0,0,.2);
}
.input::placeholder, .textarea::placeholder{color:#7f8aad; font-weight: 300}
.input:focus, .textarea:focus{
border-color:rgba(0,212,255,.6); box-shadow: var(--ring), inset 0 2px 8px rgba(0,0,0,.2);
transform: translateY(-1px);
}
.textarea{min-height:180px; resize:vertical; line-height: 1.6}
.hint{color:var(--muted); font-size:13px; margin-top:8px; font-weight: 300}
/* Enhanced buttons */
.btn{
position:relative; display:inline-flex; align-items:center; justify-content:center; gap:12px;
padding:16px 24px; border-radius:18px; border:1px solid rgba(255,255,255,.15);
color:#0a0e16; font-weight:700; letter-spacing:.01em; font-size: 15px;
background: linear-gradient(135deg, #7b5cff 0%, #00d4ff 100%);
box-shadow: 0 12px 35px rgba(0,212,255,.4), inset 0 1px 0 rgba(255,255,255,.2);
cursor:pointer; user-select:none; transition: var(--transition); overflow: hidden;
}
.btn::before{
content: ''; position: absolute; inset: 0; border-radius: inherit;
background: linear-gradient(135deg, #8a6bff 0%, #10e4ff 100%);
opacity: 0; transition: var(--transition);
}
.btn:hover{transform: translateY(-2px); box-shadow: 0 15px 45px rgba(0,212,255,.5), inset 0 1px 0 rgba(255,255,255,.2)}
.btn:hover::before{opacity: 1}
.btn:active{transform: translateY(-1px)}
.btn:disabled{opacity:.6; cursor:not-allowed; filter:grayscale(.3); transform: none}
.btn span{position: relative; z-index: 1}
/* Enhanced switch */
.row{display:flex; flex-wrap:wrap; gap:16px; align-items:center; margin-top:18px}
.switch{
display:inline-flex; align-items:center; gap:14px; cursor:pointer; user-select:none;
padding:12px 16px; border-radius:999px;
background:rgba(255,255,255,.05); border:1px solid rgba(255,255,255,.1);
transition: var(--transition); backdrop-filter: blur(8px);
}
.switch:hover{background:rgba(255,255,255,.08); transform: translateY(-1px)}
.switch .track{
width:48px; height:26px; background:rgba(255,255,255,.15); border-radius:999px;
position:relative; transition: var(--transition); box-shadow: inset 0 2px 6px rgba(0,0,0,.3);
}
.switch .thumb{
width:20px; height:20px; border-radius:50%; background:white; position:absolute; top:3px; left:3px;
box-shadow:0 4px 18px rgba(0,0,0,.5), 0 2px 8px rgba(0,0,0,.3);
transition: var(--transition);
}
.switch input{display:none}
.switch input:checked + .track{
background:linear-gradient(90deg, #00d4ff, #7b5cff);
box-shadow: 0 0 20px rgba(0,212,255,.3), inset 0 2px 6px rgba(0,0,0,.2);
}
.switch input:checked + .track .thumb{
left:25px; background:#0a0e16; transform:scale(1.1);
box-shadow:0 6px 20px rgba(0,0,0,.6), 0 2px 8px rgba(0,0,0,.4);
}
/* Enhanced collapsible section */
.collapse{
overflow:hidden; max-height:0; opacity:0; transform: translateY(-8px);
transition: max-height .4s cubic-bezier(0.4, 0, 0.2, 1),
opacity .3s ease, transform .3s ease;
}
.collapse.open{max-height:600px; opacity:1; transform:none}
/* Enhanced voice grid */
.voices{
display:grid; gap:14px; margin-top:16px;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
}
.voice{
position:relative; padding:16px; border-radius:16px;
background:linear-gradient(180deg, rgba(255,255,255,.04), rgba(255,255,255,.02));
border:1px solid rgba(255,255,255,.1);
transition: var(--transition); cursor:pointer; overflow: hidden;
backdrop-filter: blur(8px);
}
.voice::before{
content: ''; position: absolute; inset: 0; border-radius: inherit;
background: linear-gradient(135deg, rgba(0,212,255,.1), rgba(123,92,255,.1));
opacity: 0; transition: var(--transition);
}
.voice:hover{
transform: translateY(-3px);
box-shadow: var(--shadow);
border-color: rgba(0,212,255,.3);
}
.voice:hover::before{opacity: 1}
.voice.selected{
background:linear-gradient(180deg, rgba(0,212,255,.1), rgba(123,92,255,.08));
border-color: rgba(123,92,255,.6);
box-shadow: 0 0 30px rgba(123,92,255,.2);
}
.voice.selected::before{opacity: 1}
.voice .content{position: relative; z-index: 1}
.voice .name{font-weight:700; letter-spacing:.01em; margin-bottom: 8px}
.voice .meta{
color:var(--muted); font-size:13px; display:flex; gap:12px; align-items:center;
flex-wrap: wrap;
}
.voice .badge{
font-size:11px; padding:4px 10px; border-radius:999px;
border:1px solid rgba(255,255,255,.15);
background:rgba(255,255,255,.06); font-weight: 500;
}
/* Enhanced results section */
.results{margin-top:24px}
.chips{display:flex; flex-wrap:wrap; gap:12px}
.chip{
font-size:13px; color:#d4e0ff; padding:10px 14px; border-radius:999px;
border:1px solid rgba(255,255,255,.1);
background:linear-gradient(180deg, rgba(255,255,255,.05), rgba(255,255,255,.02));
backdrop-filter: blur(8px); font-weight: 500;
}
.toolbar{display:flex; gap:12px; flex-wrap:wrap; margin-top:16px}
.tbtn{
display:inline-flex; align-items:center; gap:10px; padding:10px 16px;
border-radius:12px;
background:linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.03));
border:1px solid rgba(255,255,255,.12); color:var(--text); cursor:pointer;
font-size:14px; transition: var(--transition); text-decoration: none;
backdrop-filter: blur(8px); font-weight: 500;
}
.tbtn:hover{
background:linear-gradient(180deg, rgba(255,255,255,.1), rgba(255,255,255,.06));
transform: translateY(-1px);
box-shadow: 0 8px 25px rgba(0,0,0,.2);
}
.tbtn:active{transform: translateY(0)}
/* Enhanced summary display */
.summary{
margin-top:18px;
background:linear-gradient(180deg, rgba(0,0,0,.4), rgba(0,0,0,.3));
border:1px solid rgba(255,255,255,.12); border-radius:18px; padding:24px;
line-height:1.8; font-size:16px; white-space:pre-wrap; min-height:140px;
backdrop-filter: blur(8px); box-shadow: inset 0 2px 8px rgba(0,0,0,.2);
}
/* Enhanced skeleton loading */
.skeleton{
position:relative; overflow:hidden;
background:linear-gradient(90deg, rgba(255,255,255,.04), rgba(255,255,255,.08), rgba(255,255,255,.04));
background-size: 200% 100%; border-radius:12px;
animation: skeletonShimmer 1.5s ease-in-out infinite;
}
@keyframes skeletonShimmer{0%{background-position:-200% 0}100%{background-position:200% 0}}
/* Enhanced messages */
.msg{
margin-top:18px; padding:16px 20px; border-radius:16px;
border:1px solid rgba(255,255,255,.1); display:none; font-size:14px;
backdrop-filter: blur(8px); font-weight: 500;
}
.msg.err{
display:block; color:#ffd8d8;
background:linear-gradient(180deg, rgba(255,107,107,.1), rgba(255,107,107,.05));
border-color: rgba(255,107,107,.3);
}
.msg.ok{
display:block; color:#d9fff4;
background:linear-gradient(180deg, rgba(33,209,159,.1), rgba(33,209,159,.05));
border-color: rgba(33,209,159,.3);
}
/* Enhanced audio section */
.audio{
margin-top:18px; padding:20px;
background:linear-gradient(180deg, rgba(255,255,255,.04), rgba(255,255,255,.02));
border:1px solid rgba(255,255,255,.1); border-radius:18px;
backdrop-filter: blur(8px);
}
audio{
width:100%; height:44px; outline:none; border-radius: 12px;
background: rgba(0,0,0,.3); border: 1px solid rgba(255,255,255,.1);
}
.foot{
margin-top:20px; text-align:center; color:#7f8aad; font-size:13px;
font-weight: 300; opacity: 0.8;
}
/* Keyboard shortcuts tooltip */
.shortcuts{
position: fixed; bottom: 20px; right: 20px; z-index: 1000;
background: rgba(0,0,0,.8); border: 1px solid rgba(255,255,255,.1);
border-radius: 12px; padding: 12px 16px; color: var(--text-dim);
font-size: 12px; backdrop-filter: blur(12px);
opacity: 0; transform: translateY(20px); transition: var(--transition);
pointer-events: none;
}
.shortcuts.show{opacity: 1; transform: translateY(0); pointer-events: auto}
.shortcuts kbd{
background: rgba(255,255,255,.1); padding: 2px 6px; border-radius: 4px;
font-family: monospace; font-size: 11px; margin: 0 2px;
}
/* Responsive design */
@media (max-width:768px){
.wrap{margin: 60px auto; padding: 0 20px}
.panel{padding: 24px}
.grid{grid-template-columns:1fr}
.btn{width:100%; justify-content: center}
.voices{grid-template-columns: 1fr}
.hero h1{font-size: clamp(28px, 8vw, 40px)}
.particles{display: none} /* Reduce animations on mobile */
}
@media (max-width:480px){
.wrap{margin: 40px auto; padding: 0 16px}
.panel{padding: 20px}
.hero{gap: 14px; margin-bottom: 24px}
.row{gap: 12px}
.chips{gap: 8px}
.toolbar{gap: 8px}
}
/* Dark mode enhancements */
@media (prefers-color-scheme: dark) {
:root {
--bg-0: #080c14;
--bg-1: #0c1218;
--bg-2: #0f1825;
}
}
/* Reduced motion support */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
.particles{display: none}
}
/* High contrast mode */
@media (prefers-contrast: high) {
:root {
--glass-border: rgba(255,255,255,.3);
--text: #ffffff;
--muted: #cccccc;
}
}
</style>
</head>
<body>
<div class="particles"></div>
<div class="bar" id="bar"></div>
<div class="wrap">
<header class="hero">
<div class="hero-badge" id="statusBadge">
<span class="dot" id="statusDot"></span>
<span id="statusText">Loading AI models…</span>
</div>
<h1><span class="grad-text">AI Article Summarizer</span></h1>
<p>Qwen3-0.6B summarization · Kokoro neural TTS · smooth, private, fast</p>
</header>
<section class="panel">
<!-- Mode switch -->
<div class="row" style="justify-content:center; margin-bottom:16px">
<div class="seg" role="tablist" aria-label="Input mode">
<button id="modeUrlBtn" class="active" role="tab" aria-selected="true">
<span>🔗 URL</span>
</button>
<button id="modeTextBtn" role="tab" aria-selected="false">
<span>📝 Paste Text</span>
</button>
</div>
</div>
<form id="summarizerForm" autocomplete="on">
<!-- URL mode -->
<div id="urlMode" class="grid">
<input id="articleUrl" class="input" type="url" inputmode="url"
placeholder="Paste an article URL (https://…)"
aria-label="Article URL" />
<button id="submitBtn" class="btn" type="submit">
<span>✨ Summarize</span>
</button>
</div>
<!-- Text mode -->
<div id="textMode" style="display:none; margin-top:16px">
<textarea id="articleText" class="textarea"
placeholder="Paste the article text here…"
aria-label="Article text"></textarea>
<div class="hint">
<span id="charCount">0</span> characters
<span style="margin-left: 12px; opacity: 0.7">
💡 Tip: Press <kbd>Ctrl+Enter</kbd> to submit
</span>
</div>
<div style="margin-top:16px">
<button id="submitBtnText" class="btn" type="submit">
<span>✨ Summarize Text</span>
</button>
</div>
</div>
<div class="row">
<label class="switch" title="Generate audio with Kokoro TTS">
<input id="generateAudio" type="checkbox" />
<span class="track"><span class="thumb"></span></span>
<span>🎵 Text-to-Speech</span>
</label>
<span class="chip">🧠 Qwen3-0.6B</span>
<span class="chip">🎤 Kokoro TTS</span>
<span class="chip">🔒 Private</span>
</div>
<div id="voiceSection" class="collapse" aria-hidden="true">
<div class="voices" id="voiceGrid"></div>
</div>
</form>
<!-- Enhanced loading skeleton -->
<div id="loadingSection" style="display:none; margin-top:24px">
<div style="margin-bottom: 16px">
<div class="skeleton" style="height:20px; width:45%; margin-bottom:12px"></div>
<div class="skeleton" style="height:16px; width:92%; margin-bottom:10px"></div>
<div class="skeleton" style="height:16px; width:88%; margin-bottom:10px"></div>
<div class="skeleton" style="height:16px; width:90%; margin-bottom:10px"></div>
<div class="skeleton" style="height:16px; width:65%"></div>
</div>
<div style="color: var(--muted); font-size: 14px; text-align: center; margin-top: 16px">
🤖 AI is processing your content...
</div>
</div>
<!-- Results -->
<div id="resultSection" class="results" style="display:none">
<div class="chips" id="stats"></div>
<div class="toolbar">
<button class="tbtn" id="copyBtn" type="button" title="Copy summary to clipboard">
📋 Copy summary
</button>
<button class="tbtn" id="shareBtn" type="button" title="Share summary">
🔗 Share
</button>
<a class="tbtn" id="downloadAudioBtn" href="#" download style="display:none" title="Download audio file">
⬇️ Download audio
</a>
</div>
<div id="summaryContent" class="summary" role="region" aria-label="Article summary"></div>
<div id="audioSection" class="audio" style="display:none">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px">
<strong>🎧 Audio Playback</strong>
<span id="duration" style="color:var(--muted); font-size:13px"></span>
</div>
<audio id="audioPlayer" controls preload="none" aria-label="Summary audio"></audio>
</div>
</div>
<div id="errorMessage" class="msg err" role="alert"></div>
<div id="successMessage" class="msg ok" role="status"></div>
</section>
<p class="foot">
💡 Tip: Enable TTS and pick your favorite voice. We'll remember your choice.
<br>Press <kbd>?</kbd> for keyboard shortcuts.
</p>
</div>
<!-- Keyboard shortcuts tooltip -->
<div class="shortcuts" id="shortcutsTooltip">
<div style="margin-bottom: 8px; font-weight: 600">⌨️ Keyboard Shortcuts</div>
<div><kbd>Ctrl</kbd> + <kbd>Enter</kbd> Submit form</div>
<div><kbd>Tab</kbd> Switch input mode</div>
<div><kbd>Space</kbd> Toggle TTS</div>
<div><kbd>C</kbd> Copy summary</div>
<div><kbd>?</kbd> Show/hide shortcuts</div>
</div>
<script>
// ---------------- Enhanced State Management ----------------
let modelsReady = false;
let selectedVoice = localStorage.getItem("voiceId") || "af_heart";
let inputMode = localStorage.getItem("inputMode") || "url";
let isProcessing = false;
const bar = document.getElementById("bar");
const shortcutsTooltip = document.getElementById("shortcutsTooltip");
// --------------- Enhanced Utilities --------------
const $ = (sel) => document.querySelector(sel);
const $$ = (sel) => document.querySelectorAll(sel);
function showBar(active) {
bar.style.transform = active ? "scaleX(1)" : "scaleX(0)";
}
function setStatus(ready, error){
const dot = $("#statusDot");
const text = $("#statusText");
const badge = $("#statusBadge");
if (error){
dot.classList.remove("ready");
text.textContent = "⚠️ " + error;
badge.style.borderColor = "rgba(255,107,107,.45)";
badge.style.background = "linear-gradient(180deg, rgba(255,107,107,.1), rgba(255,107,107,.05))";
return;
}
if (ready){
dot.classList.add("ready");
text.textContent = "✅ Models ready";
badge.style.borderColor = "rgba(33,209,159,.45)";
badge.style.background = "linear-gradient(180deg, rgba(33,209,159,.1), rgba(33,209,159,.05))";
} else {
dot.classList.remove("ready");
text.textContent = "⏳ Loading AI models…";
badge.style.borderColor = "rgba(255,184,77,.45)";
badge.style.background = "linear-gradient(180deg, rgba(255,184,77,.1), rgba(255,184,77,.05))";
}
}
function chip(text, icon = "") {
const span = document.createElement("span");
span.className="chip";
span.textContent = icon + text;
return span;
}
function fmt(x){ return new Intl.NumberFormat().format(x); }
function showMessage(text, type = "ok", duration = 3000) {
const msgEl = type === "ok" ? $("#successMessage") : $("#errorMessage");
msgEl.textContent = text;
msgEl.style.display = "block";
if (duration > 0) {
setTimeout(() => msgEl.style.display = "none", duration);
}
}
// ------------- Enhanced Model Status Poll ---------
async function checkModelStatus(){
try{
const res = await fetch("/status");
const s = await res.json();
modelsReady = !!s.loaded;
setStatus(modelsReady, s.error || null);
if (!modelsReady && !s.error) {
setTimeout(checkModelStatus, 2000);
}
if (modelsReady) {
await loadVoices();
showMessage("🎉 AI models loaded successfully!", "ok", 2000);
}
}catch(e){
console.warn("Status check failed:", e);
setTimeout(checkModelStatus, 3000);
}
}
// ------------- Enhanced Voice Loading -------------
async function loadVoices(){
try{
const res = await fetch("/voices");
const voices = await res.json();
const grid = $("#voiceGrid");
grid.innerHTML = "";
voices.forEach(v=>{
const el = document.createElement("div");
el.className = "voice" + (v.id === selectedVoice ? " selected":"");
el.dataset.voice = v.id;
el.innerHTML = `
<div class="content">
<div class="name">${v.name}</div>
<div class="meta">
<span class="badge">Grade ${v.grade}</span>
<span>${v.description || ""}</span>
</div>
</div>`;
el.addEventListener("click", ()=>{
$$(".voice").forEach(x=>x.classList.remove("selected"));
el.classList.add("selected");
selectedVoice = v.id;
localStorage.setItem("voiceId", selectedVoice);
// Haptic feedback if available
if (navigator.vibrate) navigator.vibrate(50);
});
grid.appendChild(el);
});
}catch(e){
console.warn("Voice loading failed:", e);
}
}
// ------------- Enhanced Collapsible Voices --------
const generateAudio = $("#generateAudio");
const voiceSection = $("#voiceSection");
function toggleVoices(open){
voiceSection.classList.toggle("open", !!open);
voiceSection.setAttribute("aria-hidden", open ? "false" : "true");
localStorage.setItem("ttsEnabled", open);
}
generateAudio.addEventListener("change", e=> {
toggleVoices(e.target.checked);
if (navigator.vibrate) navigator.vibrate(30);
});
// Restore TTS preference
const ttsEnabled = localStorage.getItem("ttsEnabled") === "true";
generateAudio.checked = ttsEnabled;
toggleVoices(ttsEnabled);
// ------------- Enhanced Mode Switching ------------
const urlMode = $("#urlMode");
const textMode = $("#textMode");
const modeUrlBtn = $("#modeUrlBtn");
const modeTextBtn = $("#modeTextBtn");
const urlInput = $("#articleUrl");
const textArea = $("#articleText");
const charCount = $("#charCount");
function setMode(m){
inputMode = m;
localStorage.setItem("inputMode", m);
if (m === "url"){
urlMode.style.display = "grid";
textMode.style.display = "none";
modeUrlBtn.classList.add("active");
modeTextBtn.classList.remove("active");
modeUrlBtn.setAttribute("aria-selected", "true");
modeTextBtn.setAttribute("aria-selected", "false");
setTimeout(() => urlInput.focus(), 100);
} else {
urlMode.style.display = "none";
textMode.style.display = "block";
modeTextBtn.classList.add("active");
modeUrlBtn.classList.remove("active");
modeTextBtn.setAttribute("aria-selected", "true");
modeUrlBtn.setAttribute("aria-selected", "false");
setTimeout(() => textArea.focus(), 100);
}
}
modeUrlBtn.addEventListener("click", ()=> setMode("url"));
modeTextBtn.addEventListener("click", ()=> setMode("text"));
textArea.addEventListener("input", ()=> {
charCount.textContent = (textArea.value || "").length;
});
// ------------- Enhanced Form Submit ----------------
const form = $("#summarizerForm");
const loading = $("#loadingSection");
const result = $("#resultSection");
const errorBox = $("#errorMessage");
const okBox = $("#successMessage");
const submitBtn = $("#submitBtn");
const submitBtnText = $("#submitBtnText");
form.addEventListener("submit", async (e)=>{
e.preventDefault();
if (isProcessing) return;
errorBox.style.display="none";
okBox.style.display="none";
if (!modelsReady){
showMessage("Please wait for the AI models to finish loading.", "err");
return;
}
const url = (urlInput.value || "").trim();
const text = (textArea.value || "").trim();
if (!text && !url){
showMessage("Please paste text or provide a valid URL.", "err");
return;
}
if (inputMode === "url" && !url){
showMessage("Please provide a valid URL or switch to Paste Text.", "err");
return;
}
if (inputMode === "text" && !text){
showMessage("Please paste the article text or switch to URL.", "err");
return;
}
isProcessing = true;
if (submitBtn) submitBtn.disabled = true;
if (submitBtnText) submitBtnText.disabled = true;
showBar(true);
loading.style.display = "block";
result.style.display = "none";
try{
const res = await fetch("/process", {
method: "POST",
headers: {"Content-Type":"application/json"},
body: JSON.stringify({
url, text,
generate_audio: generateAudio.checked,
voice: selectedVoice
})
});
const data = await res.json();
loading.style.display = "none";
isProcessing = false;
if (submitBtn) submitBtn.disabled = false;
if (submitBtnText) submitBtnText.disabled = false;
showBar(false);
if (!data.success){
showMessage(data.error || "Something went wrong.", "err");
return;
}
renderResult(data);
showMessage("✨ Summary generated successfully!", "ok", 2000);
}catch(err){
loading.style.display="none";
isProcessing = false;
if (submitBtn) submitBtn.disabled = false;
if (submitBtnText) submitBtnText.disabled = false;
showBar(false);
showMessage("Network error: " + (err?.message || err), "err");
}
});
// ------------- Enhanced Results Rendering -------------
const stats = $("#stats");
const summaryEl = $("#summaryContent");
const audioWrap = $("#audioSection");
const audioEl = $("#audioPlayer");
const dlBtn = $("#downloadAudioBtn");
const durationLabel = $("#duration");
const copyBtn = $("#copyBtn");
const shareBtn = $("#shareBtn");
function renderResult(r){
stats.innerHTML = "";
stats.appendChild(chip(`📄 ${fmt(r.article_length)}${fmt(r.summary_length)} chars`));
stats.appendChild(chip(`📉 ${r.compression_ratio}% compression`));
stats.appendChild(chip(`🕒 ${r.timestamp}`));
summaryEl.textContent = r.summary || "";
result.style.display = "block";
// Smooth scroll to results
setTimeout(() => {
result.scrollIntoView({ behavior: 'smooth', block: 'start' });
}, 100);
// Handle audio display - show if audio was requested and generated
if (r.audio_file && r.audio_file.trim()) {
audioEl.src = r.audio_file;
audioWrap.style.display = "block";
durationLabel.textContent = `${r.audio_duration || 0}s`;
dlBtn.style.display = "inline-flex";
dlBtn.href = r.audio_file;
dlBtn.download = r.audio_file.split("/").pop() || "summary.wav";
console.log("Audio section displayed:", r.audio_file);
} else if (generateAudio.checked && !r.audio_file) {
// Show message if audio was requested but failed
audioWrap.style.display = "block";
audioWrap.innerHTML = `
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px">
<strong>⚠️ Audio Generation</strong>
</div>
<div style="color: var(--warn); font-size: 14px;">
${r.audio_error || "Audio generation failed. Please try again."}
</div>
`;
dlBtn.style.display = "none";
console.log("Audio error displayed:", r.audio_error);
} else {
audioWrap.style.display = "none";
dlBtn.style.display = "none";
console.log("Audio section hidden - no audio requested or generated");
}
}
// ------------- Enhanced Copy & Share -------------
copyBtn.addEventListener("click", async ()=>{
try{
await navigator.clipboard.writeText(summaryEl.textContent || "");
copyBtn.innerHTML = "✅ Copied";
setTimeout(()=> copyBtn.innerHTML = "📋 Copy summary", 1200);
if (navigator.vibrate) navigator.vibrate(50);
}catch(e){
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = summaryEl.textContent || "";
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
copyBtn.innerHTML = "✅ Copied";
setTimeout(()=> copyBtn.innerHTML = "📋 Copy summary", 1200);
}
});
shareBtn.addEventListener("click", async ()=>{
const summary = summaryEl.textContent || "";
const shareData = {
title: 'AI Article Summary',
text: summary,
url: window.location.href
};
try {
if (navigator.share) {
await navigator.share(shareData);
} else {
// Fallback: copy to clipboard
await navigator.clipboard.writeText(`AI Article Summary:\n\n${summary}\n\nGenerated at: ${window.location.href}`);
shareBtn.innerHTML = "✅ Link copied";
setTimeout(()=> shareBtn.innerHTML = "🔗 Share", 1200);
}
} catch (e) {
console.warn('Sharing failed:', e);
}
});
// ------------- Enhanced Keyboard Shortcuts -------------
let shortcutsVisible = false;
function toggleShortcuts() {
shortcutsVisible = !shortcutsVisible;
shortcutsTooltip.classList.toggle('show', shortcutsVisible);
}
document.addEventListener("keydown", (e) => {
// Ignore if user is typing in input fields
if (e.target.matches('input, textarea')) {
// Allow Ctrl+Enter in textarea
if (e.ctrlKey && e.key === 'Enter') {
e.preventDefault();
form.dispatchEvent(new Event('submit'));
}
return;
}
switch(e.key.toLowerCase()) {
case '?':
e.preventDefault();
toggleShortcuts();
break;
case 'tab':
e.preventDefault();
setMode(inputMode === "url" ? "text" : "url");
break;
case ' ':
e.preventDefault();
generateAudio.checked = !generateAudio.checked;
generateAudio.dispatchEvent(new Event('change'));
break;
case 'c':
if (summaryEl.textContent) {
e.preventDefault();
copyBtn.click();
}
break;
case 'escape':
if (shortcutsVisible) {
e.preventDefault();
toggleShortcuts();
}
break;
}
});
// ------------- Enhanced Auto-paste Detection -------------
window.addEventListener("paste", (e)=>{
if (inputMode === "url" && document.activeElement !== urlInput && !urlInput.value){
const text = (e.clipboardData || window.clipboardData).getData("text");
if (text?.match(/^https?:\/\//)) {
urlInput.value = text;
urlInput.focus();
showMessage("📎 URL pasted automatically", "ok", 1500);
}
}
});
// ------------- Enhanced Initialization -------------
document.addEventListener("DOMContentLoaded", ()=>{
// Initialize status check
checkModelStatus();
// Restore preferences
if (localStorage.getItem("voiceId")) {
selectedVoice = localStorage.getItem("voiceId");
}
// Set initial mode
setMode(inputMode);
charCount.textContent = "0";
// Add loading animation to buttons
[submitBtn, submitBtnText].forEach(btn => {
if (btn) {
btn.addEventListener('click', () => {
if (!btn.disabled) {
btn.style.transform = 'scale(0.98)';
setTimeout(() => btn.style.transform = '', 100);
}
});
}
});
// Enhanced focus management
const focusableElements = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
const modal = document.querySelector('.panel');
const firstFocusableElement = modal.querySelectorAll(focusableElements)[0];
const focusableContent = modal.querySelectorAll(focusableElements);
const lastFocusableElement = focusableContent[focusableContent.length - 1];
document.addEventListener('keydown', function(e) {
if (e.key === 'Tab') {
if (e.shiftKey) {
if (document.activeElement === firstFocusableElement) {
lastFocusableElement.focus();
e.preventDefault();
}
} else {
if (document.activeElement === lastFocusableElement) {
firstFocusableElement.focus();
e.preventDefault();
}
}
}
});
// Performance optimization: Intersection Observer for animations
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.animationPlayState = 'running';
}
});
});
document.querySelectorAll('.hero, .panel').forEach(el => {
observer.observe(el);
});
}
// Service Worker registration for offline support
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').catch(() => {
// Silently fail if no service worker
});
}
});
// ------------- Performance Monitoring -------------
if ('performance' in window) {
window.addEventListener('load', () => {
setTimeout(() => {
const perfData = performance.getEntriesByType('navigation')[0];
if (perfData && perfData.loadEventEnd > 0) {
console.log(`Page loaded in ${Math.round(perfData.loadEventEnd)}ms`);
}
}, 0);
});
}
// ------------- Error Boundary -------------
window.addEventListener('error', (e) => {
console.error('Global error:', e.error);
showMessage('An unexpected error occurred. Please refresh the page.', 'err');
});
window.addEventListener('unhandledrejection', (e) => {
console.error('Unhandled promise rejection:', e.reason);
showMessage('A network error occurred. Please try again.', 'err');
});
</script>
</body>
</html>