WebashalarForML's picture
Upload 6 files
fee4aa3 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Batch Intelligence | PlayPulse</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<style>
:root { --bg:#0b0e14; --surface:#151921; --surface2:#1c2333; --border:#232a35; --accent:#3b82f6; --accent-dim:rgba(59,130,246,0.12); --green:#22c55e; --green-dim:rgba(34,197,94,0.12); --amber:#f59e0b; --text:#f1f5f9; --muted:#64748b; --muted2:#94a3b8; }
* { box-sizing:border-box; margin:0; padding:0; }
::-webkit-scrollbar{width:6px;height:6px;} ::-webkit-scrollbar-track{background:transparent;} ::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.1);border-radius:10px;} ::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,0.2);} *{scrollbar-width:thin;scrollbar-color:rgba(255,255,255,0.1) transparent;}
body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);height:100vh;overflow:hidden;display:flex;flex-direction:column;}
.btn-sm{background:var(--surface2);border:1px solid var(--border);color:white;padding:4px 10px;border-radius:6px;font-size:10px;cursor:pointer;transition:0.2s;} .btn-sm:hover{border-color:var(--accent);}
.header{height:60px;background:var(--surface);border-bottom:1px solid var(--border);display:flex;align-items:center;padding:0 20px;gap:20px;}
.main{flex:1;display:flex;overflow:hidden;}
.sidebar{width:300px;background:var(--surface);border-right:1px solid var(--border);padding:15px;display:flex;flex-direction:column;gap:15px;overflow-y:auto;}
.content{flex:1;background:var(--bg);position:relative;display:flex;flex-direction:column;}
.mode-toggle{display:grid;grid-template-columns:1fr 1fr;background:var(--bg);padding:4px;border-radius:10px;border:1px solid var(--border);margin-bottom:5px;}
.mode-btn{padding:8px;border-radius:7px;text-align:center;cursor:pointer;font-size:11px;font-weight:700;color:var(--muted);transition:0.2s;} .mode-btn.active{background:var(--surface2);color:white;box-shadow:0 2px 4px rgba(0,0,0,0.2);}
.logo{font-weight:800;font-size:18px;color:var(--accent);display:flex;align-items:center;gap:8px;text-decoration:none;}
.input-group{display:flex;flex-direction:column;gap:6px;}
.label{font-size:10px;font-weight:700;text-transform:uppercase;color:var(--muted);letter-spacing:0.5px;}
input,select{background:var(--bg);border:1px solid var(--border);color:white;padding:10px;border-radius:8px;font-size:12px;outline:none;width:100%;} input:focus{border-color:var(--accent);}
.btn-main{background:var(--accent);color:white;border:none;padding:14px;border-radius:10px;font-weight:800;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:8px;transition:0.2s;width:100%;border-bottom:3px solid rgba(0,0,0,0.2);} .btn-main:hover{transform:translateY(-1px);box-shadow:0 4px 12px rgba(59,130,246,0.3);} .btn-main:disabled{opacity:0.5;cursor:not-allowed;}
.scroll-view{flex:1;overflow-y:auto;padding:30px;display:flex;flex-direction:column;gap:25px;}
.batch-summary{background:var(--surface);border:1px solid var(--border);border-radius:16px;padding:20px;display:flex;flex-direction:column;gap:15px;}
.apps-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px;}
.app-mini-card{background:var(--surface2);border:1px solid var(--border);border-radius:10px;padding:10px;display:flex;align-items:center;gap:10px;}
.app-mini-card img{width:32px;height:32px;border-radius:6px;}
.app-mini-info{flex:1;min-width:0;} .app-mini-title{font-size:12px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;} .app-mini-score{font-size:10px;color:var(--amber);}
.table-container{background:var(--surface);border:1px solid var(--border);border-radius:16px;overflow:hidden;}
table{width:100%;border-collapse:collapse;font-size:13px;}
th{text-align:left;background:var(--surface2);padding:12px 16px;color:var(--muted2);font-weight:700;font-size:11px;text-transform:uppercase;border-bottom:1px solid var(--border);}
td{padding:14px 16px;border-bottom:1px solid var(--border);vertical-align:top;} tr:last-child td{border-bottom:none;} tr:hover td{background:rgba(255,255,255,0.02);}
.app-tag{display:inline-flex;align-items:center;gap:6px;background:var(--accent-dim);color:var(--accent);padding:4px 8px;border-radius:6px;font-weight:700;font-size:10px;margin-bottom:6px;border:1px solid rgba(59,130,246,0.2);}
.score-stars{color:var(--amber);white-space:nowrap;}
.review-content{color:#cbd5e1;line-height:1.5;max-width:500px;word-wrap:break-word;}
.dev-reply{margin-top:8px;padding:8px 12px;background:rgba(59,130,246,0.05);border-left:2px solid var(--accent);border-radius:0 6px 6px 0;font-size:11px;color:var(--muted2);}
.dev-reply-label{font-weight:700;color:var(--accent);font-size:9px;text-transform:uppercase;margin-bottom:3px;display:block;}
.helpful-pill{display:inline-flex;align-items:center;gap:4px;background:var(--surface2);padding:4px 8px;border-radius:12px;font-size:10px;color:var(--muted2);border:1px solid var(--border);}
.helpful-pill svg{width:10px;height:10px;color:var(--accent);}
.star-filter-grid{display:flex;flex-direction:column;gap:6px;}
.star-row{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:8px;border:1px solid var(--border);background:var(--bg);cursor:pointer;transition:border-color 0.15s;user-select:none;} .star-row:hover{border-color:var(--accent);}
.star-row input[type="checkbox"]{width:15px;height:15px;accent-color:var(--accent);cursor:pointer;padding:0;border:none;background:transparent;flex-shrink:0;}
.star-label{display:flex;align-items:center;gap:5px;font-size:13px;font-weight:600;flex:1;} .stars-on{color:var(--amber);letter-spacing:-1px;} .stars-off{color:var(--border);letter-spacing:-1px;}
.loader-overlay{position:absolute;inset:0;background:var(--bg);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:15px;z-index:10;}
.spinner{width:40px;height:40px;border:4px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .8s linear infinite;}
@keyframes spin{to{transform:rotate(360deg);}}
.hidden{display:none!important;}
/* ── 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:600px;background:var(--surface);border:1px solid var(--border);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(var(--border) 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 var(--border);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;}
.chat-table-wrap{max-width:100%;overflow-x:auto;border:1px solid var(--border);border-radius:12px;background:var(--surface2);margin-top:4px;}
.chat-table-title{padding:8px 12px;font-size:11px;font-weight:700;color:var(--accent);border-bottom:1px solid var(--border);letter-spacing:0.05em;text-transform:uppercase;}
.chat-table{width:100%;border-collapse:collapse;font-size:12px;}
.chat-table th{padding:7px 12px;text-align:left;font-weight:700;font-size:11px;color:var(--muted2);background:var(--bg);border-bottom:1px solid var(--border);white-space:nowrap;}
.chat-table td{padding:7px 12px;border-bottom:1px solid var(--border);color:var(--text);max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;} .chat-table tr:last-child td{border-bottom:none;} .chat-table tr:hover td{background:var(--surface);}
.typing-indicator{display:flex;gap:4px;padding:12px 16px;background:var(--surface2);border:1px solid var(--border);border-radius:16px;width:fit-content;}
.dot{width:6px;height:6px;background:var(--muted);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 var(--border);display:flex;gap:10px;flex-shrink:0;}
#chat-input{flex:1;background:var(--bg);border:1px solid var(--border);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;}
.chat-suggestions{display:flex;flex-wrap:wrap;gap:6px;padding:0 16px 10px;}
.sug-chip{font-size:11px;padding:5px 10px;border-radius:20px;background:var(--surface2);border:1px solid var(--border);color:var(--muted2);cursor:pointer;transition:0.2s;} .sug-chip:hover{border-color:var(--accent);color:var(--accent);}
</style>
</head>
<body>
<div class="header">
<a href="/" class="logo">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
BATCH INTEL
</a>
<nav style="margin-left:30px;display:flex;gap:20px;">
<a href="/" style="color:var(--muted2);text-decoration:none;font-size:13px;font-weight:600;" onmouseover="this.style.color='var(--text)'" onmouseout="this.style.color='var(--muted2)'">Home</a>
<a href="/scraper" style="color:var(--muted2);text-decoration:none;font-size:13px;font-weight:600;" onmouseover="this.style.color='var(--text)'" onmouseout="this.style.color='var(--muted2)'">Single Explorer</a>
<a href="/batch" style="color:var(--text);text-decoration:none;font-size:13px;font-weight:700;border-bottom:2px solid var(--accent);padding-bottom:4px;">Batch Intelligence</a>
</nav>
<div style="flex:1"></div>
<button onclick="downloadCSV()" style="background:var(--surface2);border:1px solid var(--border);color:white;padding:8px 16px;border-radius:8px;cursor:pointer;font-size:12px;font-weight:700;">Export Combined CSV</button>
</div>
<div class="main">
<aside class="sidebar">
<div class="input-group">
<div class="label">Step 1: Discover Apps</div>
<div style="display:flex;gap:8px;">
<input type="text" id="query" placeholder="e.g. Multiplayer Games..." value="Multiplayer Games" style="flex:1">
<button onclick="findApps()" id="btnFind" style="background:var(--accent);border:none;color:white;padding:0 15px;border-radius:8px;cursor:pointer;font-weight:700;">Find</button>
</div>
</div>
<div class="input-group">
<div class="label">Discovery Limit</div>
<input type="number" id="app_count" value="10" min="1" max="50">
</div>
<div id="selectionArea" class="hidden" style="background:var(--surface2);border:1px solid var(--border);border-radius:12px;padding:10px;display:flex;flex-direction:column;gap:8px;">
<div class="label" style="display:flex;justify-content:space-between;align-items:center;"><span>Select Apps</span><span id="selectedCount" style="color:var(--accent);font-size:9px;">0 selected</span></div>
<div id="appList" style="max-height:160px;overflow-y:auto;overflow-x:hidden;display:flex;flex-direction:column;gap:4px;padding-right:4px;"></div>
<div style="display:flex;gap:5px;"><button onclick="toggleAllApps(true)" class="btn-sm" style="flex:1">All</button><button onclick="toggleAllApps(false)" class="btn-sm" style="flex:1">None</button></div>
</div>
<div class="input-group">
<div class="label">Step 2: Scrape Settings</div>
<div class="label" style="font-size:10px;margin-top:10px;">Reviews Per App</div>
<div class="mode-toggle">
<div class="mode-btn active" id="btn-fixed" onclick="setMode('fixed')">Custom</div>
<div class="mode-btn" id="btn-all" onclick="setMode('all')">Fetch All</div>
</div>
<input type="number" id="reviews_per_app" value="50" min="10" step="10">
</div>
<div class="input-group">
<div class="label">Sort Method</div>
<select id="sort"><option value="MOST_RELEVANT">Most Relevant</option><option value="NEWEST">Newest</option><option value="RATING">Top Ratings</option></select>
</div>
<div class="input-group">
<div class="label" style="display:flex;justify-content:space-between;align-items:center;">
<span>Star Rating Filter</span>
<div style="display:flex;gap:5px;">
<button class="quick-btn" style="font-size:9px;padding:2px 5px;cursor:pointer;background:var(--surface2);border:1px solid var(--border);color:white;border-radius:4px;" onclick="selectAllStars(true)">All</button>
<button class="quick-btn" style="font-size:9px;padding:2px 5px;cursor:pointer;background:var(--surface2);border:1px solid var(--border);color:white;border-radius:4px;" onclick="selectAllStars(false)">None</button>
</div>
</div>
<div class="star-filter-grid">
<label class="star-row"><input type="checkbox" class="star-cb" value="5" checked><span class="star-label"><span class="stars-on">β˜…β˜…β˜…β˜…β˜…</span></span></label>
<label class="star-row"><input type="checkbox" class="star-cb" value="4" checked><span class="star-label"><span class="stars-on">β˜…β˜…β˜…β˜…</span><span class="stars-off">β˜…</span></span></label>
<label class="star-row"><input type="checkbox" class="star-cb" value="3" checked><span class="star-label"><span class="stars-on">β˜…β˜…β˜…</span><span class="stars-off">β˜…β˜…</span></span></label>
<label class="star-row"><input type="checkbox" class="star-cb" value="2" checked><span class="star-label"><span class="stars-on">β˜…β˜…</span><span class="stars-off">β˜…β˜…β˜…</span></span></label>
<label class="star-row"><input type="checkbox" class="star-cb" value="1" checked><span class="star-label"><span class="stars-on">β˜…</span><span class="stars-off">β˜…β˜…β˜…β˜…</span></span></label>
</div>
</div>
<button class="btn-main" id="go" onclick="runBatch()">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><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>
RUN BATCH ANALYSIS
</button>
<div style="background:var(--bg);padding:15px;border-radius:12px;border:1px solid var(--border);font-size:11px;color:var(--muted);line-height:1.6;"><strong style="color:var(--text)">About Batch Mode</strong><br>Search for apps, scrape reviews for each, and compare them side-by-side with AI chat support.</div>
</aside>
<div class="content">
<div id="dataView" class="scroll-view">
<div id="welcome" style="display:flex;flex-direction:column;align-items:center;justify-content:center;flex:1;color:var(--muted);gap:12px">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><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>
<p>Run batch analysis to compare app data</p>
</div>
<div id="results" class="hidden">
<div class="batch-summary">
<div class="label">Comparing These Apps:</div>
<div class="apps-grid" id="appsGrid"></div>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;">
<div class="label">Reviews Comparison</div>
<div style="font-size:11px;color:var(--muted);" id="resultStats"></div>
</div>
<div class="table-container">
<table id="reviewsTable">
<thead>
<tr>
<th style="width:180px">Application / User</th>
<th style="width:90px">Score</th>
<th>Feedback & Developer Response</th>
<th style="width:100px">Helpful</th>
<th style="width:110px">Date</th>
</tr>
</thead>
<tbody id="reviewsBody"></tbody>
</table>
</div>
</div>
</div>
<div id="loader" class="loader-overlay hidden">
<div class="spinner"></div>
<p style="color:var(--muted);font-size:14px" id="loaderMsg">Searching for apps...</p>
</div>
</div>
</div>
<!-- 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">πŸ‘‹ Hi! I'm PlayPulse Intelligence. Run a batch scrape, then ask me to compare apps, find issues, show tables, or analyze sentiment!</div>
</div>
</div>
<div class="chat-suggestions" id="chat-sug">
<div class="sug-chip" onclick="fillChat('Compare all apps by rating')">Compare apps</div>
<div class="sug-chip" onclick="fillChat('Which app has the most complaints?')">Most complaints</div>
<div class="sug-chip" onclick="fillChat('Show 1 star reviews in table')">1β˜… table</div>
<div class="sug-chip" onclick="fillChat('What are the common issues?')">Common issues</div>
</div>
<div class="chat-input-area">
<input type="text" id="chat-input" placeholder="Ask about the batch analysis…" 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>
let currentData=null;
let currentMode='fixed';
function setMode(m){currentMode=m;document.querySelectorAll('.mode-btn').forEach(b=>b.classList.remove('active'));document.getElementById('btn-'+m).classList.add('active');document.getElementById('reviews_per_app').classList.toggle('hidden',m==='all');}
function selectAllStars(check){document.querySelectorAll('.star-cb').forEach(cb=>cb.checked=check);}
let foundApps=[];
async function findApps(){
const q=document.getElementById('query').value.trim();if(!q)return;
const btn=document.getElementById('btnFind');btn.disabled=true;btn.innerText='Searching...';
try{
const res=await fetch('/find-apps',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({query:q,app_count:document.getElementById('app_count').value})});
const data=await res.json();if(!res.ok)throw new Error(data.error||'Discovery failed');
foundApps=data.results;renderAppSelection();
}catch(e){alert(e.message);}finally{btn.disabled=false;btn.innerText='Find';}
}
function renderAppSelection(){
const list=document.getElementById('appList');
document.getElementById('selectionArea').classList.remove('hidden');
list.innerHTML=foundApps.map(a=>`<label style="display:flex;align-items:center;gap:8px;padding:6px;background:var(--bg);border-radius:6px;border:1px solid var(--border);cursor:pointer;min-width:0;"><input type="checkbox" class="app-cb" value="${a.appId}" onchange="updateSelectionCount()" checked style="width:14px;height:14px;margin:0;"><img src="${a.icon}" style="width:20px;height:20px;border-radius:4px;flex-shrink:0;"><div style="flex:1;min-width:0;"><div style="font-size:10px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--text);">${a.title}</div><div style="font-size:9px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${a.developer}</div></div></label>`).join('');
updateSelectionCount();
}
function toggleAllApps(check){document.querySelectorAll('.app-cb').forEach(cb=>cb.checked=check);updateSelectionCount();}
function updateSelectionCount(){const count=document.querySelectorAll('.app-cb:checked').length;document.getElementById('selectedCount').innerText=`${count} selected`;document.getElementById('go').disabled=count===0;}
async function runBatch(){
const selectedAppIds=[...document.querySelectorAll('.app-cb:checked')].map(cb=>cb.value);
if(!selectedAppIds.length)return alert('Select at least one app');
const stars=[...document.querySelectorAll('.star-cb:checked')].map(cb=>parseInt(cb.value));
if(!stars.length)return alert('Select at least one star rating');
document.getElementById('welcome').classList.add('hidden');document.getElementById('results').classList.add('hidden');
document.getElementById('loader').classList.remove('hidden');document.getElementById('go').disabled=true;
try{
const res=await fetch('/scrape-batch',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({app_ids:selectedAppIds,review_count_type:currentMode,reviews_per_app:document.getElementById('reviews_per_app').value,sort_order:document.getElementById('sort').value,star_ratings:stars.length===5?'all':stars})});
const data=await res.json();if(!res.ok)throw new Error(data.error||'Batch failed');
currentData=data;render(data);
}catch(e){alert(e.message);}finally{document.getElementById('loader').classList.add('hidden');document.getElementById('go').disabled=false;}
}
function render(data,customReviews){
const reviews=customReviews||data.reviews;
document.getElementById('results').classList.remove('hidden');
document.getElementById('appsGrid').innerHTML=data.apps.map(a=>`<div class="app-mini-card"><img src="${a.icon}" alt=""><div class="app-mini-info"><div class="app-mini-title">${a.title}</div><div class="app-mini-score">${a.score.toFixed(1)} β˜…</div></div></div>`).join('');
document.getElementById('resultStats').innerText=`Found ${reviews.length} reviews`;
document.getElementById('reviewsBody').innerHTML=reviews.map(r=>{
const app=data.apps.find(a=>a.appId===r.appId)||{title:r.appTitle};
const replyHtml = r.replyContent ? `<div class="dev-reply"><span class="dev-reply-label">Developer Reply</span>${r.replyContent}</div>` : '';
const helpfulHtml = `<div class="helpful-pill"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"/></svg>${r.thumbsUpCount || 0}</div>`;
return `
<tr>
<td>
<div class="app-tag">${app.title}</div>
<div style="font-size:11px;font-weight:700;color:var(--text);">${r.userName}</div>
</td>
<td>
<div class="score-stars">${'β˜…'.repeat(r.score)}<span style="color:var(--border)">${'β˜…'.repeat(5-r.score)}</span></div>
</td>
<td>
<div class="review-content">${r.content}</div>
${replyHtml}
</td>
<td>${helpfulHtml}</td>
<td><div style="color:var(--muted);font-size:11px;">${new Date(r.at).toLocaleDateString(undefined, {month:'short', day:'numeric', year:'numeric'})}</div></td>
</tr>
`;
}).join('');
}
function downloadCSV(){
if(!currentData)return;
const esc=v=>`"${String(v||'').replace(/"/g,'""')}"`;
const hdr=['App Name','App ID','User','Score','Date','Content','Thumbs Up','Developer Reply'];
const rows=currentData.reviews.map(r=>[esc(r.appTitle),esc(r.appId),esc(r.userName),r.score,esc(r.at.slice(0,10)),esc(r.content),r.thumbsUpCount,esc(r.replyContent)].join(','));
const blob=new Blob([[hdr.join(','),...rows].join('\n')],{type:'text/csv'});
const a=Object.assign(document.createElement('a'),{href:URL.createObjectURL(blob),download:`batch_${Date.now()}.csv`});a.click();
}
// ══════════════════════════════════════════════════════════════════════
// CHAT (shared logic identical to index.html)
// ══════════════════════════════════════════════════════════════════════
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');}
function fillChat(t){const i=document.getElementById('chat-input');i.value=t;i.focus();}
async function clearChat(){
document.getElementById('chat-messages').innerHTML=`<div class="msg-row bot"><div class="message bot">Chat cleared. Run a batch scrape then ask me anything!</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:(currentData&&currentData.reviews)?currentData.reviews:[]})});
const data=await res.json();
if(typing.parentNode)container.removeChild(typing);
if(data.error){appendBotMsg('⚠️ '+data.error,null);return;}
appendBotMsg(data.reply||'',data.table||null);
if(data.type==='filter'&&data.filters)applyChatFilters(data.filters);
}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);}
if(table&&table.rows&&table.rows.length){row.appendChild(buildTable(table));}
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 style="color:var(--muted2)">$1</em>');}
function escHtml(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}
function buildTable(td){
const{title,columns,rows}=td;const w=document.createElement('div');w.className='chat-table-wrap';
let h='';if(title)h+=`<div class="chat-table-title">${escHtml(title)}</div>`;
h+='<table class="chat-table"><thead><tr>';for(const c of columns)h+=`<th>${escHtml(c)}</th>`;h+='</tr></thead><tbody>';
for(const row of rows){h+='<tr>';for(const c of columns){const v=row[c]!==undefined?row[c]:'';h+=`<td title="${escHtml(String(v))}">${escHtml(String(v))}</td>`;}h+='</tr>';}
h+='</tbody></table>';w.innerHTML=h;return w;
}
function applyChatFilters(raw){
if(!currentData)return;
try{
const f=typeof raw==='string'?JSON.parse(raw):raw;let filtered=currentData.reviews;
if(f.stars&&f.stars.length)filtered=filtered.filter(r=>f.stars.includes(r.score));
if(f.app){const q=f.app.toLowerCase();filtered=filtered.filter(r=>{const app=currentData.apps.find(a=>a.appId===r.appId)||{title:r.appTitle||""};return(app.title||"").toLowerCase().includes(q)||r.appId.toLowerCase().includes(q);});}
if(f.query){const q=f.query.toLowerCase();filtered=filtered.filter(r=>(r.content||'').toLowerCase().includes(q)||(r.userName||'').toLowerCase().includes(q));}
render(currentData,filtered);
}catch(e){console.error('Filter error',e);}
}
</script>
</body>
</html>