Spaces:
Runtime error
Runtime error
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width,initial-scale=1" /> | |
| <title>Summarizer β AI Hub</title> | |
| <meta name="description" content="SummarizeAI β fast, private text summarizer." /> | |
| <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&display=swap" rel="stylesheet"> | |
| <style> | |
| :root{ | |
| --bg:#fbfdff; | |
| --muted:#57706d; | |
| --ink:#0b2320; | |
| --accent: linear-gradient(90deg,#9fd7ff,#89e0c9); | |
| --card-radius:12px; | |
| --maxw:900px; | |
| --ease:cubic-bezier(.2,.9,.3,1); | |
| } | |
| *{box-sizing:border-box} | |
| body{margin:0;font-family:Poppins,system-ui,-apple-system,Segoe UI,Roboto,Arial;background:linear-gradient(180deg,#fbfdff,#fffaf8);color:var(--ink);-webkit-font-smoothing:antialiased} | |
| .page{max-width:var(--maxw);margin:32px auto;padding:20px} | |
| header{display:flex;align-items:center;justify-content:space-between;gap:12px} | |
| .brand{display:flex;gap:12px;align-items:center} | |
| .logo{width:44px;height:44px;border-radius:10px;background:linear-gradient(135deg,#9fd7ff,#c9f0eb);display:flex;align-items:center;justify-content:center;color:#043033;font-weight:700} | |
| h1{margin:0;font-size:1.05rem} | |
| .sub{color:var(--muted);font-size:0.92rem;margin-top:6px} | |
| .card{margin-top:18px;background:linear-gradient(180deg,#ffffff,#fbfbff);border-radius:var(--card-radius);padding:18px;border:1px solid rgba(10,12,12,0.04);box-shadow:0 20px 40px rgba(10,18,16,0.04)} | |
| .controls{display:flex;gap:10px;align-items:center;justify-content:space-between;flex-wrap:wrap} | |
| .left-controls{display:flex;gap:8px;align-items:center} | |
| .select, .btn, .small-btn, .checkbox { | |
| border-radius:10px;padding:10px 12px;border:1px solid rgba(10,12,12,0.04);background:transparent;color:inherit;font-weight:600; | |
| } | |
| .btn{background:linear-gradient(90deg,#9fd7ff,#89e0c9);color:#043033;cursor:pointer;border:0} | |
| .small-btn{font-size:0.9rem;padding:8px 10px;cursor:pointer} | |
| .input-row{display:flex;gap:12px;margin-top:12px;align-items:flex-start} | |
| textarea#text-input{flex:1;min-height:260px;resize:vertical;padding:14px;border-radius:10px;border:1px solid rgba(10,12,12,0.04);background:transparent;color:inherit;font-size:1rem;line-height:1.5} | |
| .side{width:260px;min-width:220px} | |
| .meta{color:var(--muted);font-size:0.9rem;margin-top:8px} | |
| .word-count{color:var(--muted);font-size:0.9rem;margin-top:8px} | |
| #loader{display:none;margin:12px auto;border-radius:50%;width:36px;height:36px;border:4px solid rgba(0,0,0,0.06);border-top-color:#89e0c9;animation:spin 1s linear infinite} | |
| @keyframes spin{to{transform:rotate(360deg)}} | |
| .summary-box{margin-top:16px;background:linear-gradient(180deg,#ffffff,#fbfbff);padding:14px;border-radius:10px;border:1px solid rgba(10,12,12,0.04)} | |
| .summary-actions{display:flex;gap:8px;justify-content:flex-end;margin-top:8px} | |
| .summary-text{white-space:pre-wrap;font-size:1rem;color:var(--ink)} | |
| .history{margin-top:14px;display:flex;gap:8px;flex-wrap:wrap} | |
| .pill{background:rgba(0,0,0,0.02);padding:8px 10px;border-radius:999px;border:1px solid rgba(0,0,0,0.03);font-size:0.9rem;color:var(--muted);cursor:pointer} | |
| .msg{margin-top:10px;padding:10px;border-radius:8px;font-weight:600} | |
| .error{background:linear-gradient(180deg, rgba(255,40,60,0.06), rgba(255,40,60,0.02));color:#b80000;border:1px solid rgba(255,40,60,0.06)} | |
| @media (max-width:860px){.input-row{flex-direction:column}.side{width:100%}} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="page" role="main"> | |
| <header> | |
| <div class="brand"> | |
| <div class="logo">AI</div> | |
| <div> | |
| <h1>Summarizer</h1> | |
| <div class="sub">Fast, private summaries β paste or send demo text from the hub.</div> | |
| </div> | |
| </div> | |
| <div> | |
| <button id="themeToggle" class="small-btn">Toggle</button> | |
| </div> | |
| </header> | |
| <div class="card" aria-live="polite"> | |
| <div class="controls"> | |
| <div class="left-controls"> | |
| <label> | |
| <select id="length" class="select" aria-label="Summary length"> | |
| <option value="short">Short</option> | |
| <option value="medium" selected>Medium</option> | |
| <option value="long">Long</option> | |
| </select> | |
| </label> | |
| <label> | |
| <select id="tone" class="select" aria-label="Tone"> | |
| <option value="neutral" selected>Neutral</option> | |
| <option value="formal">Formal</option> | |
| <option value="casual">Casual</option> | |
| <option value="bullet">Bulleted</option> | |
| </select> | |
| </label> | |
| <label style="display:flex;align-items:center;gap:8px"> | |
| <input id="aiChoose" type="checkbox" class="checkbox" /> <span style="font-weight:700">AI choose settings</span> | |
| </label> | |
| <button id="exampleBtn" class="small-btn" title="Insert example">Example</button> | |
| <button id="clearBtn" class="small-btn" title="Clear input">Clear</button> | |
| </div> | |
| <div style="display:flex;gap:8px;align-items:center"> | |
| <button id="summarize-btn" class="btn">Summarize</button> | |
| </div> | |
| </div> | |
| <div class="input-row"> | |
| <textarea id="text-input" placeholder="Paste your article, meeting notes, or long text here..." aria-label="Text input"></textarea> | |
| <aside class="side" aria-hidden> | |
| <div class="meta">Quick tips</div> | |
| <div style="color:var(--muted);margin-top:8px">β’ Best with 50β1500 words. Use AI choose for the assistant to pick length & tone.</div> | |
| <div class="word-count" id="word-count">Words: 0</div> | |
| <div style="margin-top:12px" id="wordWarning" class="msg" style="display:none"></div> | |
| <div style="margin-top:14px;font-size:0.95rem;color:var(--muted)">Saved history</div> | |
| <div class="history" id="history"></div> | |
| </aside> | |
| </div> | |
| <div id="loader" role="status" aria-hidden="true"></div> | |
| <div id="error-box" class="msg error" style="display:none"></div> | |
| <div id="summary-area" style="display:none"> | |
| <div class="summary-box" role="region" aria-label="Summary result"> | |
| <div class="summary-text" id="summary-text"></div> | |
| <div class="summary-actions"> | |
| <button id="copy-btn" class="small-btn">Copy</button> | |
| <button id="download-btn" class="small-btn">Download .txt</button> | |
| <button id="save-btn" class="small-btn">Save</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| /* Config */ | |
| const API_ENDPOINT = '/summarize'; | |
| const textInput = document.getElementById('text-input'); | |
| const summarizeBtn = document.getElementById('summarize-btn'); | |
| const loader = document.getElementById('loader'); | |
| const summaryArea = document.getElementById('summary-area'); | |
| const summaryText = document.getElementById('summary-text'); | |
| const copyBtn = document.getElementById('copy-btn'); | |
| const downloadBtn = document.getElementById('download-btn'); | |
| const saveBtn = document.getElementById('save-btn'); | |
| const wordCountEl = document.getElementById('word-count'); | |
| const wordWarning = document.getElementById('wordWarning'); | |
| const errorBox = document.getElementById('error-box'); | |
| const historyEl = document.getElementById('history'); | |
| const lengthSelect = document.getElementById('length'); | |
| const toneSelect = document.getElementById('tone'); | |
| const aiChoose = document.getElementById('aiChoose'); | |
| let currentSummary = ''; | |
| /* Word count */ | |
| function updateWordCount(){ | |
| const words = textInput.value.trim().split(/\s+/).filter(Boolean).length; | |
| wordCountEl.textContent = `Words: ${words}`; | |
| if(words < 10){ | |
| wordWarning.textContent = 'Please enter at least 10 words to get a good summary.'; | |
| wordWarning.style.display = 'block'; | |
| } else { | |
| wordWarning.style.display = 'none'; | |
| } | |
| } | |
| textInput.addEventListener('input', updateWordCount); | |
| updateWordCount(); | |
| /* Small UI helpers */ | |
| function showLoader(on=true){ | |
| loader.style.display = on ? 'block' : 'none'; | |
| } | |
| function showError(msg){ | |
| errorBox.textContent = msg; | |
| errorBox.style.display = 'block'; | |
| } | |
| function clearError(){ | |
| errorBox.style.display = 'none'; | |
| } | |
| /* Fetch summary */ | |
| async function fetchSummary(text){ | |
| const body = { | |
| text, | |
| length: aiChoose.checked ? "auto" : lengthSelect.value, | |
| tone: aiChoose.checked ? "auto" : toneSelect.value | |
| }; | |
| showError(''); clearError(); | |
| summaryArea.style.display = 'none'; | |
| showLoader(true); | |
| summarizeBtn.disabled = true; | |
| try{ | |
| const res = await fetch(API_ENDPOINT, { | |
| method: 'POST', | |
| headers: {'Content-Type':'application/json'}, | |
| body: JSON.stringify(body) | |
| }); | |
| if(!res.ok) throw new Error(`Server error: ${res.status}`); | |
| const data = await res.json(); | |
| currentSummary = data.summary || data?.result || ''; | |
| summaryText.textContent = currentSummary; | |
| summaryArea.style.display = 'block'; | |
| // save lightweight meta if available | |
| if(data.meta) { | |
| const m = document.createElement('div'); | |
| m.textContent = `Settings: ${data.meta.length_choice || ''} Β· ${data.meta.tone || ''} Β· ${data.meta.time_seconds || 0}s`; | |
| m.style.color = 'var(--muted)'; | |
| m.style.fontSize = '0.85rem'; | |
| m.style.marginTop = '8px'; | |
| if(!summaryArea.querySelector('.meta-info')) { m.className = 'meta-info'; summaryArea.querySelector('.summary-box').appendChild(m); } | |
| } | |
| }catch(err){ | |
| showError(err.message || 'Failed to summarize. Try again later.'); | |
| }finally{ | |
| showLoader(false); | |
| summarizeBtn.disabled = false; | |
| } | |
| } | |
| /* Buttons */ | |
| document.getElementById('exampleBtn').addEventListener('click', ()=> { | |
| textInput.value = "OpenAI launched a new model today that focuses on making text summarization far more accurate for long-form content. Engineers and researchers praised the model's efficiency in tests."; | |
| updateWordCount(); | |
| }); | |
| document.getElementById('clearBtn').addEventListener('click', ()=> { textInput.value=''; updateWordCount(); }); | |
| summarizeBtn.addEventListener('click', (e)=> { | |
| e.preventDefault(); | |
| const txt = textInput.value.trim(); | |
| if(!txt || txt.split(/\s+/).filter(Boolean).length < 10){ | |
| wordWarning.style.display = 'block'; | |
| return; | |
| } | |
| fetchSummary(txt); | |
| }); | |
| copyBtn.addEventListener('click', ()=> { | |
| if(!currentSummary) return; | |
| navigator.clipboard.writeText(currentSummary).then(()=> { | |
| copyBtn.textContent = 'Copied!'; | |
| setTimeout(()=> copyBtn.textContent = 'Copy',1400); | |
| }); | |
| }); | |
| downloadBtn.addEventListener('click', ()=> { | |
| if(!currentSummary) return; | |
| const blob = new Blob([currentSummary], {type:'text/plain;charset=utf-8'}); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; a.download = 'summary.txt'; document.body.appendChild(a); a.click(); a.remove(); | |
| URL.revokeObjectURL(url); | |
| }); | |
| saveBtn.addEventListener('click', ()=> { | |
| if(!currentSummary) return; | |
| const item = {text: textInput.value, summary: currentSummary, time: Date.now()}; | |
| const list = JSON.parse(localStorage.getItem('sa_history') || '[]'); | |
| list.unshift(item); | |
| localStorage.setItem('sa_history', JSON.stringify(list.slice(0,20))); | |
| renderHistory(); | |
| }); | |
| /* History */ | |
| function renderHistory(){ | |
| const list = JSON.parse(localStorage.getItem('sa_history') || '[]'); | |
| historyEl.innerHTML = ''; | |
| list.forEach((it, idx)=> { | |
| const pill = document.createElement('button'); | |
| pill.className = 'pill'; | |
| pill.textContent = new Date(it.time).toLocaleString(); | |
| pill.title = (it.summary || '').slice(0,200); | |
| pill.addEventListener('click', ()=> { | |
| textInput.value = it.text; updateWordCount(); | |
| summaryText.textContent = it.summary; summaryArea.style.display = 'block'; | |
| }); | |
| historyEl.appendChild(pill); | |
| }); | |
| } | |
| renderHistory(); | |
| /* Accept prefill from hub (postMessage) */ | |
| window.addEventListener('message', (ev)=> { | |
| try{ | |
| const d = ev.data || {}; | |
| if(d && d.type === 'summarizer:prefill' && typeof d.text === 'string'){ | |
| textInput.value = d.text; | |
| updateWordCount(); | |
| } | |
| }catch(e){} | |
| }, false); | |
| /* Support ?text= param */ | |
| (function prefillFromQuery(){ | |
| try{ | |
| const params = new URLSearchParams(location.search); | |
| const t = params.get('text'); | |
| if(t){ textInput.value = t; updateWordCount(); } | |
| }catch(e){} | |
| })(); | |
| /* Theme toggle (minimal) */ | |
| document.getElementById('themeToggle').addEventListener('click', ()=> { | |
| document.body.classList.toggle('light'); | |
| }); | |
| /* accessibility: Ctrl/Cmd+Enter to submit */ | |
| textInput.addEventListener('keydown', (e)=> { if(e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); summarizeBtn.click(); }}); | |
| </script> | |
| </body> | |
| </html> | |