Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>PlayPulse | Intelligence Platform</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Outfit:wght@400;600;800&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { --bg:#0b0e14; --surface:#151921; --surface2:#1c2333; --border:rgba(255,255,255,0.08); --accent:#3b82f6; --accent-gradient:linear-gradient(135deg,#3b82f6 0%,#2dd4bf 100%); --text:#f1f5f9; --muted:#94a3b8; } | |
| *{box-sizing:border-box;margin:0;padding:0;} | |
| ::-webkit-scrollbar{width:6px;} ::-webkit-scrollbar-track{background:transparent;} ::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.1);border-radius:10px;} | |
| body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);min-height:100vh;display:flex;flex-direction:column;overflow-x:hidden;} | |
| .blob{position:fixed;width:500px;height:500px;background:var(--accent);filter:blur(120px);opacity:0.1;z-index:-1;border-radius:50%;} | |
| .blob-1{top:-100px;right:-100px;} .blob-2{bottom:-100px;left:-100px;background:#2dd4bf;} | |
| header{padding:30px 5%;display:flex;justify-content:space-between;align-items:center;} | |
| .logo{font-family:'Outfit',sans-serif;font-weight:800;font-size:24px;letter-spacing:-1px;color:var(--text);display:flex;align-items:center;gap:10px;} | |
| .logo-icon{width:32px;height:32px;background:var(--accent-gradient);border-radius:8px;display:flex;align-items:center;justify-content:center;} | |
| main{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:60px 5%;max-width:1200px;margin:0 auto;text-align:center;width:100%;} | |
| .hero-tag{background:rgba(59,130,246,0.1);border:1px solid rgba(59,130,246,0.2);color:var(--accent);padding:6px 16px;border-radius:100px;font-size:13px;font-weight:700;margin-bottom:24px;text-transform:uppercase;letter-spacing:1px;} | |
| h1{font-family:'Outfit',sans-serif;font-size:clamp(40px,8vw,72px);font-weight:800;line-height:1.1;margin-bottom:20px;letter-spacing:-2px;} | |
| h1 span{background:var(--accent-gradient);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;} | |
| .sub-hero{color:var(--muted);font-size:clamp(16px,2vw,20px);max-width:600px;margin-bottom:60px;line-height:1.6;} | |
| .cards-container{display:grid;grid-template-columns:repeat(auto-fit,minmax(340px,1fr));gap:30px;width:100%;} | |
| .card{background:var(--surface);border:1px solid var(--border);padding:40px;border-radius:24px;text-align:left;transition:all 0.3s cubic-bezier(0.4,0,0.2,1);cursor:pointer;position:relative;overflow:hidden;display:flex;flex-direction:column;height:100%;} | |
| .card:hover{border-color:rgba(59,130,246,0.5);transform:translateY(-8px);box-shadow:0 20px 40px rgba(0,0,0,0.4);} | |
| .card-icon{width:56px;height:56px;background:rgba(255,255,255,0.03);border:1px solid var(--border);border-radius:16px;display:flex;align-items:center;justify-content:center;margin-bottom:24px;transition:0.3s;} | |
| .card:hover .card-icon{background:var(--accent);color:white;border-color:var(--accent);} | |
| .card h2{font-family:'Outfit',sans-serif;font-size:24px;margin-bottom:12px;font-weight:700;} | |
| .card p{color:var(--muted);line-height:1.6;font-size:15px;margin-bottom:24px;flex:1;} | |
| .badge{position:absolute;top:20px;right:20px;background:rgba(255,255,255,0.05);border:1px solid var(--border);padding:4px 12px;border-radius:100px;font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;} | |
| .badge.active{background:rgba(59,130,246,0.1);color:var(--accent);border-color:rgba(59,130,246,0.2);} | |
| .btn{display:inline-flex;align-items:center;gap:8px;font-weight:700;font-size:14px;color:var(--text);transition:0.3s;} | |
| .card:hover .btn{color:var(--accent);} | |
| footer{padding:40px;text-align:center;color:var(--muted);font-size:13px;} | |
| @media(max-width:768px){h1{font-size:48px;}.cards-container{grid-template-columns:1fr;}} | |
| /* ── Chat styles ── */ | |
| #chat-dialer{position:fixed;bottom:24px;right:24px;width:56px;height:56px;background:var(--accent);border-radius:50%;display:flex;align-items:center;justify-content:center;box-shadow:0 8px 32px rgba(59,130,246,0.4);cursor:pointer;z-index:1000;transition:0.3s cubic-bezier(0.175,0.885,0.32,1.275);border:2px solid rgba(255,255,255,0.1);} | |
| #chat-dialer:hover{transform:scale(1.1) rotate(5deg);box-shadow:0 12px 40px rgba(59,130,246,0.6);} | |
| #chat-dialer svg{width:24px;height:24px;color:white;fill:none;stroke:currentColor;stroke-width:2.5;} | |
| #chat-window{position:fixed;bottom:90px;right:24px;width:420px;height:560px;background:var(--surface);border:1px solid rgba(255,255,255,0.1);border-radius:20px;display:flex;flex-direction:column;box-shadow:0 20px 50px rgba(0,0,0,0.5);z-index:1001;overflow:hidden;transform:translateY(20px) scale(0.95);opacity:0;pointer-events:none;transition:0.3s cubic-bezier(0.4,0,0.2,1);backdrop-filter:blur(20px);} | |
| #chat-window.open{transform:translateY(0) scale(1);opacity:1;pointer-events:auto;} | |
| .chat-header{padding:14px 18px;background:var(--accent);color:white;display:flex;align-items:center;gap:12px;flex-shrink:0;} | |
| .chat-header-info{flex:1;} .chat-header-title{font-weight:800;font-size:15px;} .chat-header-status{font-size:10px;opacity:0.8;display:flex;align-items:center;gap:4px;} .status-dot{width:6px;height:6px;background:#22c55e;border-radius:50%;} | |
| .chat-header-actions{display:flex;gap:8px;align-items:center;} | |
| .chat-clear-btn{background:rgba(255,255,255,0.15);border:none;color:white;font-size:11px;padding:4px 10px;border-radius:8px;cursor:pointer;transition:0.2s;} .chat-clear-btn:hover{background:rgba(255,255,255,0.25);} | |
| .chat-messages{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:12px;background-image:radial-gradient(rgba(255,255,255,0.05) 1px,transparent 1px);background-size:20px 20px;} | |
| .msg-row{display:flex;flex-direction:column;gap:4px;} .msg-row.user{align-items:flex-end;} .msg-row.bot{align-items:flex-start;} | |
| .message{max-width:88%;padding:11px 15px;border-radius:16px;font-size:13px;line-height:1.6;} | |
| .message.user{background:var(--accent);color:white;border-bottom-right-radius:4px;} | |
| .message.bot{background:var(--surface2);color:var(--text);border:1px solid rgba(255,255,255,0.08);border-bottom-left-radius:4px;white-space:pre-wrap;word-break:break-word;} | |
| .msg-section{margin-top:10px;font-weight:700;font-size:11px;color:var(--accent);letter-spacing:0.05em;text-transform:uppercase;} | |
| .msg-item{display:flex;gap:8px;margin-top:5px;} .msg-item-num{font-weight:700;color:var(--accent);min-width:16px;} .msg-bullet{color:var(--accent);min-width:14px;} | |
| .typing-indicator{display:flex;gap:4px;padding:12px 16px;background:var(--surface2);border:1px solid rgba(255,255,255,0.08);border-radius:16px;width:fit-content;} | |
| .dot{width:6px;height:6px;background:#64748b;border-radius:50%;animation:bounce 1.4s infinite;} .dot:nth-child(2){animation-delay:0.2s;} .dot:nth-child(3){animation-delay:0.4s;} | |
| @keyframes bounce{0%,80%,100%{transform:translateY(0)}40%{transform:translateY(-6px)}} | |
| .chat-input-area{padding:14px 16px;background:var(--surface);border-top:1px solid rgba(255,255,255,0.06);display:flex;gap:10px;flex-shrink:0;} | |
| #chat-input{flex:1;background:var(--bg);border:1px solid rgba(255,255,255,0.08);color:white;padding:10px 14px;border-radius:12px;font-size:13px;outline:none;} #chat-input:focus{border-color:var(--accent);} | |
| .btn-send{width:40px;height:40px;background:var(--accent);color:white;border:none;border-radius:10px;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:0.2s;flex-shrink:0;} .btn-send:hover{transform:scale(1.05);} .btn-send svg{width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:2.5;} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="blob blob-1"></div> | |
| <div class="blob blob-2"></div> | |
| <header> | |
| <div class="logo"> | |
| <div class="logo-icon"> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="3"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg> | |
| </div> | |
| PLAYPULSE | |
| </div> | |
| </header> | |
| <main> | |
| <div class="hero-tag">Next-Gen Intelligence</div> | |
| <h1>Extract Insights from <span>Global App Data</span></h1> | |
| <p class="sub-hero">The most powerful tool for analyzing app reviews, sentiment, and developer responses in real-time. Powered by AI chat.</p> | |
| <div class="cards-container"> | |
| <div class="card" onclick="location.href='/scraper'"> | |
| <div class="badge active">Live Now</div> | |
| <div class="card-icon"> | |
| <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16zM21 21l-4.35-4.35"/></svg> | |
| </div> | |
| <h2>Single App Explorer</h2> | |
| <p>Deep-dive into any Play Store app. Extract hundreds of reviews, analyze ratings, and chat with AI to get instant insights.</p> | |
| <div class="btn">Explore Now <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M5 12h14M12 5l7 7-7 7"/></svg></div> | |
| </div> | |
| <div class="card" onclick="location.href='/batch'"> | |
| <div class="badge active">New Mode</div> | |
| <div class="card-icon"> | |
| <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg> | |
| </div> | |
| <h2>Batch Intelligence</h2> | |
| <p>Compare multiple apps side-by-side. Track competitor updates and aggregate sentiment across entire game categories.</p> | |
| <div class="btn">Start Analysis <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M5 12h14M12 5l7 7-7 7"/></svg></div> | |
| </div> | |
| </div> | |
| </main> | |
| <footer>© 2026 PlayPulse Intelligence. Powered by Google Play Scraper Engine.</footer> | |
| <!-- Chat bubble --> | |
| <div id="chat-dialer" onclick="toggleChat()"> | |
| <svg viewBox="0 0 24 24"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg> | |
| </div> | |
| <div id="chat-window"> | |
| <div class="chat-header"> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5"><circle cx="12" cy="12" r="3"/><path d="M12 1v4M12 19v4M4.22 4.22l2.83 2.83M16.95 16.95l2.83 2.83M1 12h4M19 12h4M4.22 19.78l2.83-2.83M16.95 7.05l2.83-2.83"/></svg> | |
| <div class="chat-header-info"> | |
| <div class="chat-header-title">PlayPulse Intelligence</div> | |
| <div class="chat-header-status"><span class="status-dot"></span> Agent Online</div> | |
| </div> | |
| <div class="chat-header-actions"> | |
| <button class="chat-clear-btn" onclick="clearChat()">Clear</button> | |
| <div style="cursor:pointer;opacity:0.7;" onclick="toggleChat()"> | |
| <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="chat-messages" id="chat-messages"> | |
| <div class="msg-row bot"> | |
| <div class="message bot">👋 Welcome to PlayPulse! I can help you understand what tools are available, or answer general app-store questions. Head to <strong>Single Explorer</strong> or <strong>Batch Intelligence</strong> to start analyzing reviews.</div> | |
| </div> | |
| </div> | |
| <div class="chat-input-area"> | |
| <input type="text" id="chat-input" placeholder="Ask a question…" onkeydown="if(event.key==='Enter') sendChatMessage()"> | |
| <button class="btn-send" onclick="sendChatMessage()"> | |
| <svg viewBox="0 0 24 24"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg> | |
| </button> | |
| </div> | |
| </div> | |
| <script> | |
| const SESSION_ID=(()=>{let id=sessionStorage.getItem('pp_sid');if(!id){id='sess_'+Math.random().toString(36).slice(2);sessionStorage.setItem('pp_sid',id);}return id;})(); | |
| function toggleChat(){document.getElementById('chat-window').classList.toggle('open');} | |
| async function clearChat(){ | |
| document.getElementById('chat-messages').innerHTML=`<div class="msg-row bot"><div class="message bot">Chat cleared!</div></div>`; | |
| await fetch('/chat/clear',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({session_id:SESSION_ID})}); | |
| } | |
| async function sendChatMessage(){ | |
| const input=document.getElementById('chat-input');const msg=input.value.trim();if(!msg)return; | |
| appendUserMsg(msg);input.value=''; | |
| const container=document.getElementById('chat-messages'); | |
| const typing=document.createElement('div');typing.className='typing-indicator';typing.innerHTML='<div class="dot"></div><div class="dot"></div><div class="dot"></div>'; | |
| container.appendChild(typing);container.scrollTop=container.scrollHeight; | |
| try{ | |
| const res=await fetch('/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:msg,session_id:SESSION_ID,reviews:[]})}); | |
| const data=await res.json(); | |
| if(typing.parentNode)container.removeChild(typing); | |
| appendBotMsg(data.reply||data.error||'Something went wrong.',null); | |
| }catch(e){if(typing.parentNode)container.removeChild(typing);appendBotMsg('Connection error.',null);} | |
| } | |
| function appendUserMsg(text){const c=document.getElementById('chat-messages');const row=document.createElement('div');row.className='msg-row user';row.innerHTML=`<div class="message user">${escHtml(text)}</div>`;c.appendChild(row);c.scrollTop=c.scrollHeight;} | |
| function appendBotMsg(text,table){const c=document.getElementById('chat-messages');const row=document.createElement('div');row.className='msg-row bot';if(text&&text.trim()){const b=document.createElement('div');b.className='message bot';b.innerHTML=renderMD(text);row.appendChild(b);}c.appendChild(row);c.scrollTop=c.scrollHeight;} | |
| function renderMD(text){ | |
| const lines=text.split('\n');let html='',inList=false; | |
| for(let raw of lines){ | |
| if(/^\*\*[^*]+\*\*:?$/.test(raw.trim())){if(inList){html+='</div>';inList=false;}html+=`<div class="msg-section">${escHtml(raw.trim().replace(/^\*\*/,'').replace(/\*\*:?$/,''))}</div>`;continue;} | |
| const nm=raw.match(/^(\d+)\.\s+(.+)/);if(nm){if(!inList){html+='<div style="margin-top:6px">';inList=true;}html+=`<div class="msg-item"><span class="msg-item-num">${nm[1]}.</span><span>${inlineFmt(nm[2])}</span></div>`;continue;} | |
| const bm=raw.match(/^[•\-\*]\s+(.+)/);if(bm){if(!inList){html+='<div style="margin-top:6px">';inList=true;}html+=`<div class="msg-item"><span class="msg-bullet">•</span><span>${inlineFmt(bm[1])}</span></div>`;continue;} | |
| if(inList&&raw.trim()===''){html+='</div>';inList=false;} | |
| if(raw.trim()===''){html+='<br>';}else{html+=`<span>${inlineFmt(raw)}</span><br>`;} | |
| } | |
| if(inList)html+='</div>';return html; | |
| } | |
| function inlineFmt(t){return escHtml(t).replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>').replace(/_(.+?)_/g,'<em>$1</em>');} | |
| function escHtml(s){return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');} | |
| </script> | |
| </body> | |
| </html> |