Spaces:
Sleeping
Sleeping
Upload 2 files
Browse files- templates/batch.html +608 -324
- templates/index.html +0 -0
templates/batch.html
CHANGED
|
@@ -1,383 +1,667 @@
|
|
| 1 |
<!DOCTYPE html>
|
| 2 |
<html lang="en">
|
| 3 |
<head>
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
</head>
|
| 83 |
<body>
|
| 84 |
-
|
| 85 |
<div class="header">
|
| 86 |
-
<a href="/" class="logo">
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
<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>
|
| 92 |
-
<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>
|
| 93 |
-
<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>
|
| 94 |
</nav>
|
| 95 |
<div style="flex:1"></div>
|
| 96 |
-
<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>
|
| 97 |
</div>
|
| 98 |
-
|
| 99 |
<div class="main">
|
| 100 |
-
<aside class="sidebar">
|
| 101 |
-
<div class="
|
| 102 |
-
<div class="
|
| 103 |
-
|
| 104 |
-
<
|
| 105 |
-
<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>
|
| 106 |
</div>
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
<div class="
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
<div class="label">Step 2: Scrape Settings</div>
|
| 119 |
-
<div class="label" style="font-size:10px;margin-top:10px;">Reviews Per App</div>
|
| 120 |
-
<div class="mode-toggle">
|
| 121 |
-
<div class="mode-btn active" id="btn-fixed" onclick="setMode('fixed')">Custom</div>
|
| 122 |
-
<div class="mode-btn" id="btn-all" onclick="setMode('all')">Fetch All</div>
|
| 123 |
-
</div>
|
| 124 |
-
<input type="number" id="reviews_per_app" value="50" min="10" step="10">
|
| 125 |
-
</div>
|
| 126 |
-
<div class="input-group">
|
| 127 |
-
<div class="label">Sort Method</div>
|
| 128 |
-
<select id="sort"><option value="MOST_RELEVANT">Most Relevant</option><option value="NEWEST">Newest</option><option value="RATING">Top Ratings</option></select>
|
| 129 |
-
</div>
|
| 130 |
-
<div class="input-group">
|
| 131 |
-
<div class="label" style="display:flex;justify-content:space-between;align-items:center;">
|
| 132 |
-
<span>Star Rating Filter</span>
|
| 133 |
-
<div style="display:flex;gap:5px;">
|
| 134 |
-
<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>
|
| 135 |
-
<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>
|
| 136 |
</div>
|
|
|
|
|
|
|
| 137 |
</div>
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
<
|
| 142 |
-
<
|
| 143 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
</div>
|
| 145 |
</div>
|
| 146 |
-
<
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
</
|
| 150 |
-
<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>
|
| 151 |
</aside>
|
| 152 |
|
| 153 |
<div class="content">
|
| 154 |
-
<div id="
|
| 155 |
-
<div id="
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
<
|
| 162 |
-
<div class="apps-grid" id="appsGrid"></div>
|
| 163 |
-
</div>
|
| 164 |
-
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;">
|
| 165 |
-
<div class="label">Reviews Comparison</div>
|
| 166 |
-
<div style="font-size:11px;color:var(--muted);" id="resultStats"></div>
|
| 167 |
-
</div>
|
| 168 |
-
<div class="table-container">
|
| 169 |
-
<table id="reviewsTable">
|
| 170 |
-
<thead>
|
| 171 |
-
<tr>
|
| 172 |
-
<th style="width:180px">Application / User</th>
|
| 173 |
-
<th style="width:90px">Score</th>
|
| 174 |
-
<th>Feedback & Developer Response</th>
|
| 175 |
-
<th style="width:100px">Helpful</th>
|
| 176 |
-
<th style="width:110px">Date</th>
|
| 177 |
-
</tr>
|
| 178 |
-
</thead>
|
| 179 |
-
<tbody id="reviewsBody"></tbody>
|
| 180 |
-
</table>
|
| 181 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
</div>
|
|
|
|
| 183 |
</div>
|
| 184 |
<div id="loader" class="loader-overlay hidden">
|
| 185 |
<div class="spinner"></div>
|
| 186 |
-
<p style="color:var(--muted);font-size:
|
| 187 |
</div>
|
| 188 |
</div>
|
| 189 |
</div>
|
| 190 |
|
| 191 |
-
<
|
| 192 |
-
<div id="chat-dialer" onclick="toggleChat()">
|
| 193 |
-
<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>
|
| 194 |
-
</div>
|
| 195 |
-
|
| 196 |
<div id="chat-window">
|
| 197 |
<div class="chat-header">
|
| 198 |
-
<svg width="
|
| 199 |
-
<div class="chat-header-info">
|
| 200 |
-
|
| 201 |
-
<div class="chat-header-status"><span class="status-dot"></span> Agent Online</div>
|
| 202 |
-
</div>
|
| 203 |
-
<div class="chat-header-actions">
|
| 204 |
-
<button class="chat-clear-btn" onclick="clearChat()">Clear</button>
|
| 205 |
-
<div style="cursor:pointer;opacity:0.7;" onclick="toggleChat()">
|
| 206 |
-
<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>
|
| 207 |
-
</div>
|
| 208 |
-
</div>
|
| 209 |
-
</div>
|
| 210 |
-
<div class="chat-messages" id="chat-messages">
|
| 211 |
-
<div class="msg-row bot">
|
| 212 |
-
<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>
|
| 213 |
-
</div>
|
| 214 |
</div>
|
| 215 |
-
<div class="chat-
|
|
|
|
| 216 |
<div class="sug-chip" onclick="fillChat('Compare all apps by rating')">Compare apps</div>
|
| 217 |
<div class="sug-chip" onclick="fillChat('Which app has the most complaints?')">Most complaints</div>
|
| 218 |
-
<div class="sug-chip" onclick="fillChat('Show 1 star reviews in table')">1
|
| 219 |
-
<div class="sug-chip" onclick="fillChat('What are
|
| 220 |
</div>
|
| 221 |
<div class="chat-input-area">
|
| 222 |
-
<input type="text" id="chat-input" placeholder="Ask about
|
| 223 |
-
<button class="btn-send" onclick="sendChatMessage()">
|
| 224 |
-
<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>
|
| 225 |
-
</button>
|
| 226 |
</div>
|
| 227 |
</div>
|
| 228 |
|
| 229 |
<script>
|
| 230 |
-
|
| 231 |
-
|
|
|
|
|
|
|
|
|
|
| 232 |
|
| 233 |
-
|
| 234 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
|
|
|
|
|
|
| 246 |
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
|
| 254 |
-
|
| 255 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
const stars=[...document.querySelectorAll('.star-cb:checked')].map(cb=>parseInt(cb.value));
|
| 261 |
-
if(!stars.length)return alert('Select at least one star rating');
|
| 262 |
-
document.getElementById('welcome').classList.add('hidden');document.getElementById('results').classList.add('hidden');
|
| 263 |
-
document.getElementById('loader').classList.remove('hidden');document.getElementById('go').disabled=true;
|
| 264 |
-
try{
|
| 265 |
-
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})});
|
| 266 |
-
const data=await res.json();if(!res.ok)throw new Error(data.error||'Batch failed');
|
| 267 |
-
currentData=data;render(data);
|
| 268 |
-
}catch(e){alert(e.message);}finally{document.getElementById('loader').classList.add('hidden');document.getElementById('go').disabled=false;}
|
| 269 |
-
}
|
| 270 |
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
const app=data.apps.find(a=>a.appId===r.appId)||{title:r.appTitle};
|
| 279 |
-
const replyHtml = r.replyContent ? `<div class="dev-reply"><span class="dev-reply-label">Developer Reply</span>${r.replyContent}</div>` : '';
|
| 280 |
-
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>`;
|
| 281 |
-
|
| 282 |
-
return `
|
| 283 |
-
<tr>
|
| 284 |
-
<td>
|
| 285 |
-
<div class="app-tag">${app.title}</div>
|
| 286 |
-
<div style="font-size:11px;font-weight:700;color:var(--text);">${r.userName}</div>
|
| 287 |
-
</td>
|
| 288 |
-
<td>
|
| 289 |
-
<div class="score-stars">${'★'.repeat(r.score)}<span style="color:var(--border)">${'★'.repeat(5-r.score)}</span></div>
|
| 290 |
-
</td>
|
| 291 |
-
<td>
|
| 292 |
-
<div class="review-content">${r.content}</div>
|
| 293 |
-
${replyHtml}
|
| 294 |
-
</td>
|
| 295 |
-
<td>${helpfulHtml}</td>
|
| 296 |
-
<td><div style="color:var(--muted);font-size:11px;">${new Date(r.at).toLocaleDateString(undefined, {month:'short', day:'numeric', year:'numeric'})}</div></td>
|
| 297 |
-
</tr>
|
| 298 |
-
`;
|
| 299 |
-
}).join('');
|
| 300 |
-
}
|
| 301 |
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
|
|
|
|
|
|
|
|
|
| 310 |
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
|
|
|
| 315 |
|
| 316 |
-
|
| 317 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 318 |
|
| 319 |
-
|
| 320 |
-
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>`;
|
| 321 |
-
await fetch('/chat/clear',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({session_id:SESSION_ID})});
|
| 322 |
-
}
|
| 323 |
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
const container=document.getElementById('chat-messages');
|
| 328 |
-
const typing=document.createElement('div');typing.className='typing-indicator';typing.innerHTML='<div class="dot"></div><div class="dot"></div><div class="dot"></div>';
|
| 329 |
-
container.appendChild(typing);container.scrollTop=container.scrollHeight;
|
| 330 |
-
try{
|
| 331 |
-
const res=await fetch('/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:msg,session_id:SESSION_ID,reviews:(currentData&¤tData.reviews)?currentData.reviews:[]})});
|
| 332 |
-
const data=await res.json();
|
| 333 |
-
if(typing.parentNode)container.removeChild(typing);
|
| 334 |
-
if(data.error){appendBotMsg('⚠️ '+data.error,null);return;}
|
| 335 |
-
appendBotMsg(data.reply||'',data.table||null);
|
| 336 |
-
if(data.type==='filter'&&data.filters)applyChatFilters(data.filters);
|
| 337 |
-
}catch(e){if(typing.parentNode)container.removeChild(typing);appendBotMsg('Connection error.',null);}
|
| 338 |
-
}
|
| 339 |
|
| 340 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 341 |
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
if(text&&text.trim()){const b=document.createElement('div');b.className='message bot';b.innerHTML=renderMD(text);row.appendChild(b);}
|
| 345 |
-
if(table&&table.rows&&table.rows.length){row.appendChild(buildTable(table));}
|
| 346 |
-
c.appendChild(row);c.scrollTop=c.scrollHeight;
|
| 347 |
-
}
|
| 348 |
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 362 |
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 369 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 370 |
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 380 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
</script>
|
|
|
|
| 382 |
</body>
|
| 383 |
</html>
|
|
|
|
| 1 |
<!DOCTYPE html>
|
| 2 |
<html lang="en">
|
| 3 |
<head>
|
| 4 |
+
<meta charset="UTF-8"/>
|
| 5 |
+
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
| 6 |
+
<title>Batch Intelligence | PlayPulse</title>
|
| 7 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet"/>
|
| 8 |
+
<style>
|
| 9 |
+
:root{--bg:#0b0e14;--surface:#151921;--surface2:#1c2333;--border:#232a35;--accent:#3b82f6;--accent-dim:rgba(59,130,246,0.12);--accent-glow:rgba(59,130,246,0.25);--green:#22c55e;--green-dim:rgba(34,197,94,0.12);--amber:#f59e0b;--amber-dim:rgba(245,158,11,0.12);--text:#f1f5f9;--muted:#64748b;--muted2:#94a3b8;}
|
| 10 |
+
*{box-sizing:border-box;margin:0;padding:0;}
|
| 11 |
+
::-webkit-scrollbar{width:4px;height:4px;}::-webkit-scrollbar-track{background:transparent;}::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.08);border-radius:10px;}::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,0.18);}*{scrollbar-width:thin;scrollbar-color:rgba(255,255,255,0.08) transparent;}
|
| 12 |
+
body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);height:100vh;overflow:hidden;display:flex;flex-direction:column;}
|
| 13 |
+
.header{height:52px;background:var(--surface);border-bottom:1px solid var(--border);display:flex;align-items:center;padding:0 18px;gap:14px;flex-shrink:0;}
|
| 14 |
+
.logo{font-weight:800;font-size:16px;color:var(--accent);display:flex;align-items:center;gap:7px;text-decoration:none;}
|
| 15 |
+
nav{display:flex;gap:3px;margin-left:14px;}
|
| 16 |
+
.nav-link{color:var(--muted2);text-decoration:none;font-size:12px;font-weight:600;padding:5px 10px;border-radius:7px;transition:.15s;}
|
| 17 |
+
.nav-link:hover{color:var(--text);background:var(--surface2);}
|
| 18 |
+
.nav-link.active{color:var(--accent);background:var(--accent-dim);}
|
| 19 |
+
.main{flex:1;display:flex;overflow:hidden;}
|
| 20 |
+
.sidebar{width:300px;min-width:220px;max-width:480px;background:var(--surface);border-right:1px solid var(--border);display:flex;flex-direction:column;flex-shrink:0;overflow:hidden;position:relative;transition:width .25s ease;}
|
| 21 |
+
.sidebar.collapsed{width:36px!important;min-width:36px;}
|
| 22 |
+
.sidebar-inner{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0;}
|
| 23 |
+
.sidebar.collapsed .sidebar-inner{display:none;}
|
| 24 |
+
.resize-handle{position:absolute;right:-4px;top:0;bottom:0;width:8px;cursor:col-resize;z-index:20;}
|
| 25 |
+
.resize-handle::after{content:'';position:absolute;left:3px;top:50%;transform:translateY(-50%);width:2px;height:40px;background:var(--border);border-radius:2px;transition:background .15s,height .15s;}
|
| 26 |
+
.resize-handle:hover::after{background:var(--accent);height:60px;}
|
| 27 |
+
.collapse-btn{position:absolute;right:-15px;top:50%;transform:translateY(-50%);width:26px;height:42px;background:var(--surface);border:1px solid var(--border);border-left:none;border-radius:0 8px 8px 0;display:flex;align-items:center;justify-content:center;cursor:pointer;z-index:25;transition:.2s;color:var(--muted);}
|
| 28 |
+
.collapse-btn:hover{color:var(--accent);border-color:var(--accent);}
|
| 29 |
+
.collapse-btn svg{width:12px;height:12px;fill:none;stroke:currentColor;stroke-width:2.5;transition:transform .25s;}
|
| 30 |
+
.sidebar.collapsed .collapse-btn svg{transform:rotate(180deg);}
|
| 31 |
+
.sidebar-tabs{display:flex;border-bottom:1px solid var(--border);flex-shrink:0;}
|
| 32 |
+
.stab{flex:1;padding:10px 6px;text-align:center;font-size:11px;font-weight:700;color:var(--muted);cursor:pointer;border-bottom:2px solid transparent;transition:.15s;user-select:none;white-space:nowrap;}
|
| 33 |
+
.stab.active{color:var(--accent);border-bottom-color:var(--accent);background:var(--accent-dim);}
|
| 34 |
+
.stab:hover:not(.active){color:var(--text);}
|
| 35 |
+
.spanel{flex:1;display:flex;flex-direction:column;overflow:hidden;padding:12px;}
|
| 36 |
+
.spanel.hidden{display:none;}
|
| 37 |
+
input,select{background:var(--bg);border:1px solid var(--border);color:white;padding:8px 10px;border-radius:7px;font-size:12px;outline:none;width:100%;transition:border-color .15s;}
|
| 38 |
+
input:focus,select:focus{border-color:var(--accent);}
|
| 39 |
+
.find-row{display:flex;gap:7px;margin-bottom:8px;}
|
| 40 |
+
.find-row input{flex:1;}
|
| 41 |
+
.btn-find{background:var(--accent);border:none;color:white;padding:0 13px;border-radius:7px;cursor:pointer;font-weight:700;font-size:12px;white-space:nowrap;transition:.2s;}
|
| 42 |
+
.btn-find:hover{opacity:.85;}
|
| 43 |
+
.btn-find:disabled{opacity:.5;cursor:not-allowed;}
|
| 44 |
+
.search-hint{font-size:10px;color:var(--muted);padding:4px 2px 6px;line-height:1.6;}
|
| 45 |
+
.search-results-list{flex:1;overflow-y:auto;display:flex;flex-direction:column;gap:3px;padding-right:2px;min-height:0;}
|
| 46 |
+
.sr-item{display:flex;align-items:center;gap:8px;padding:7px 8px;background:var(--bg);border-radius:7px;border:1.5px solid var(--border);cursor:grab;user-select:none;transition:border-color .15s,background .12s;}
|
| 47 |
+
.sr-item:hover{border-color:rgba(59,130,246,.5);background:var(--accent-dim);}
|
| 48 |
+
.sr-item.in-queue{border-color:var(--green);opacity:.6;}
|
| 49 |
+
.sr-item.dragging-src{opacity:.25;}
|
| 50 |
+
.sr-item img{width:28px;height:28px;border-radius:6px;object-fit:cover;flex-shrink:0;}
|
| 51 |
+
.sr-info{flex:1;min-width:0;}
|
| 52 |
+
.sr-title{font-size:11px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
| 53 |
+
.sr-dev{font-size:9px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
| 54 |
+
.sr-score{font-size:9px;font-weight:700;color:var(--amber);background:var(--amber-dim);padding:1px 5px;border-radius:4px;flex-shrink:0;}
|
| 55 |
+
.sr-add-btn{width:20px;height:20px;border-radius:5px;background:var(--accent-dim);border:1px solid rgba(59,130,246,.3);display:flex;align-items:center;justify-content:center;cursor:pointer;flex-shrink:0;color:var(--accent);transition:.15s;font-size:14px;font-weight:900;line-height:1;}
|
| 56 |
+
.sr-add-btn:hover{background:var(--accent);color:white;}
|
| 57 |
+
.sr-item.in-queue .sr-add-btn{background:var(--green-dim);border-color:rgba(34,197,94,.3);color:var(--green);}
|
| 58 |
+
.sr-skeleton{display:flex;align-items:center;gap:8px;padding:7px 8px;border-radius:7px;border:1px solid var(--border);}
|
| 59 |
+
.sk-icon{width:28px;height:28px;border-radius:6px;background:var(--surface2);animation:shimmer 1.2s infinite;}
|
| 60 |
+
.sk-lines{flex:1;display:flex;flex-direction:column;gap:5px;}
|
| 61 |
+
.sk-line{height:7px;border-radius:3px;background:var(--surface2);animation:shimmer 1.2s infinite;}
|
| 62 |
+
.sk-line.short{width:55%;}
|
| 63 |
+
@keyframes shimmer{0%,100%{opacity:.35}50%{opacity:.8}}
|
| 64 |
+
.queue-section{display:flex;flex-direction:column;gap:6px;overflow:hidden;flex:1;min-height:0;}
|
| 65 |
+
.queue-box{border:2px dashed var(--border);border-radius:9px;display:flex;flex-direction:column;overflow:hidden;transition:border-color .2s,background .2s;flex:1;min-height:80px;}
|
| 66 |
+
.queue-box.drag-active{border-color:var(--accent);background:rgba(59,130,246,.04);}
|
| 67 |
+
.queue-hdr{padding:7px 10px;background:var(--surface2);border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;flex-shrink:0;}
|
| 68 |
+
.queue-htitle{font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.7px;color:var(--muted2);}
|
| 69 |
+
.queue-empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:var(--muted);gap:5px;padding:14px;text-align:center;}
|
| 70 |
+
.queue-empty p{font-size:11px;line-height:1.5;}
|
| 71 |
+
.queue-list{flex:1;overflow-y:auto;display:flex;flex-direction:column;gap:3px;padding:5px;min-height:0;}
|
| 72 |
+
.q-item{display:flex;align-items:center;gap:7px;padding:6px 7px;background:var(--bg);border-radius:6px;border:1.5px solid var(--border);cursor:grab;user-select:none;transition:border-color .12s,opacity .12s;}
|
| 73 |
+
.q-item:active{cursor:grabbing;}
|
| 74 |
+
.q-item:hover{border-color:rgba(59,130,246,.4);}
|
| 75 |
+
.q-item.drag-over{border-color:var(--accent);background:var(--accent-dim);}
|
| 76 |
+
.q-item.q-dragging{opacity:.3;}
|
| 77 |
+
.q-handle{display:flex;flex-direction:column;gap:2px;color:var(--muted);flex-shrink:0;padding:1px;}
|
| 78 |
+
.q-handle span{display:block;width:10px;height:1.5px;background:currentColor;border-radius:2px;}
|
| 79 |
+
.q-item img{width:22px;height:22px;border-radius:5px;object-fit:cover;flex-shrink:0;}
|
| 80 |
+
.q-info{flex:1;min-width:0;}
|
| 81 |
+
.q-title{font-size:10px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
| 82 |
+
.q-score{font-size:9px;color:var(--amber);}
|
| 83 |
+
.q-rm{width:16px;height:16px;border-radius:4px;background:transparent;border:none;color:var(--muted);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:12px;transition:.15s;flex-shrink:0;line-height:1;}
|
| 84 |
+
.q-rm:hover{color:#ef4444;background:rgba(239,68,68,.1);}
|
| 85 |
+
.quick-btn{font-size:10px;font-weight:700;color:var(--muted2);cursor:pointer;padding:2px 6px;border-radius:5px;border:1px solid var(--border);background:var(--surface2);transition:.15s;}
|
| 86 |
+
.quick-btn:hover{color:white;border-color:var(--accent);}
|
| 87 |
+
.mode-toggle{display:grid;grid-template-columns:1fr 1fr;background:var(--bg);padding:3px;border-radius:8px;border:1px solid var(--border);margin-bottom:6px;}
|
| 88 |
+
.mode-btn{padding:6px;border-radius:5px;text-align:center;cursor:pointer;font-size:11px;font-weight:700;color:var(--muted);transition:.2s;}
|
| 89 |
+
.mode-btn.active{background:var(--surface2);color:white;box-shadow:0 2px 4px rgba(0,0,0,.3);}
|
| 90 |
+
.star-filter-grid{display:flex;flex-direction:column;gap:4px;}
|
| 91 |
+
.star-row{display:flex;align-items:center;gap:8px;padding:6px 9px;border-radius:6px;border:1px solid var(--border);background:var(--bg);cursor:pointer;transition:border-color .15s;user-select:none;}
|
| 92 |
+
.star-row:hover{border-color:var(--accent);}
|
| 93 |
+
.star-row input[type=checkbox]{width:13px;height:13px;accent-color:var(--accent);cursor:pointer;padding:0;border:none;flex-shrink:0;}
|
| 94 |
+
.star-label{display:flex;align-items:center;gap:4px;font-size:11px;font-weight:600;flex:1;}
|
| 95 |
+
.stars-on{color:var(--amber);letter-spacing:-1px;}
|
| 96 |
+
.stars-off{color:var(--border);letter-spacing:-1px;}
|
| 97 |
+
.btn-main{background:var(--accent);color:white;border:none;padding:11px;border-radius:9px;font-weight:800;font-size:12px;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:7px;transition:.2s;width:100%;border-bottom:3px solid rgba(0,0,0,.2);flex-shrink:0;margin-top:10px;}
|
| 98 |
+
.btn-main:hover{transform:translateY(-1px);box-shadow:0 4px 16px var(--accent-glow);}
|
| 99 |
+
.btn-main:disabled{opacity:.5;cursor:not-allowed;transform:none;box-shadow:none;}
|
| 100 |
+
.sec-lbl{font-size:9px;font-weight:800;text-transform:uppercase;color:var(--muted);letter-spacing:.8px;margin-bottom:5px;display:flex;align-items:center;justify-content:space-between;}
|
| 101 |
+
.sec-lbl span{color:var(--accent);font-size:9px;text-transform:none;letter-spacing:0;font-weight:700;}
|
| 102 |
+
.igrp{display:flex;flex-direction:column;gap:5px;margin-bottom:8px;}
|
| 103 |
+
.divider{height:1px;background:var(--border);margin:7px 0;flex-shrink:0;}
|
| 104 |
+
#rubber-band{position:fixed;border:1.5px dashed var(--accent);background:rgba(59,130,246,0.06);border-radius:3px;pointer-events:none;display:none;z-index:9999;}
|
| 105 |
+
.content{flex:1;background:var(--bg);position:relative;display:flex;flex-direction:column;overflow:hidden;min-width:0;}
|
| 106 |
+
.toolbar{display:flex;align-items:center;gap:8px;padding:9px 16px;background:var(--surface);border-bottom:1px solid var(--border);flex-shrink:0;flex-wrap:wrap;}
|
| 107 |
+
.sbox{display:flex;align-items:center;gap:6px;background:var(--bg);border:1px solid var(--border);border-radius:7px;padding:6px 10px;flex:1;max-width:260px;transition:border-color .15s;}
|
| 108 |
+
.sbox:focus-within{border-color:var(--accent);}
|
| 109 |
+
.sbox input{background:transparent;border:none;color:var(--text);font-size:12px;outline:none;width:100%;}
|
| 110 |
+
.sbox svg{color:var(--muted);flex-shrink:0;width:12px;height:12px;fill:none;stroke:currentColor;stroke-width:2.5;}
|
| 111 |
+
.chips-row{display:flex;gap:4px;flex-wrap:wrap;flex:1;min-width:0;}
|
| 112 |
+
.a-chip{display:inline-flex;align-items:center;gap:4px;font-size:10px;font-weight:700;padding:3px 7px;border-radius:14px;background:var(--accent-dim);color:var(--accent);border:1px solid rgba(59,130,246,.3);cursor:pointer;transition:.15s;white-space:nowrap;}
|
| 113 |
+
.a-chip:hover{background:rgba(239,68,68,.1);color:#ef4444;border-color:rgba(239,68,68,.3);}
|
| 114 |
+
.tb-right{display:flex;align-items:center;gap:7px;flex-shrink:0;}
|
| 115 |
+
.res-count{font-size:11px;color:var(--muted);white-space:nowrap;}
|
| 116 |
+
.vs{display:flex;gap:2px;background:var(--bg);padding:3px;border-radius:7px;border:1px solid var(--border);}
|
| 117 |
+
.vb{width:27px;height:25px;display:flex;align-items:center;justify-content:center;border-radius:5px;cursor:pointer;color:var(--muted);transition:.15s;border:none;background:transparent;}
|
| 118 |
+
.vb.active{background:var(--surface2);color:white;}
|
| 119 |
+
.vb svg{width:13px;height:13px;fill:none;stroke:currentColor;stroke-width:2;}
|
| 120 |
+
.btn-exp{background:var(--surface2);border:1px solid var(--border);color:var(--muted2);padding:6px 11px;border-radius:7px;cursor:pointer;font-size:11px;font-weight:700;transition:.15s;display:flex;align-items:center;gap:5px;}
|
| 121 |
+
.btn-exp:hover{border-color:var(--accent);color:var(--text);}
|
| 122 |
+
.btn-exp svg{width:12px;height:12px;fill:none;stroke:currentColor;stroke-width:2.5;}
|
| 123 |
+
.scroll-view{flex:1;overflow-y:auto;padding:16px 18px;display:flex;flex-direction:column;gap:14px;}
|
| 124 |
+
.batch-summary{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:14px 16px;display:flex;flex-direction:column;gap:10px;}
|
| 125 |
+
.apps-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(175px,1fr));gap:8px;}
|
| 126 |
+
.app-mini-card{background:var(--surface2);border:1px solid var(--border);border-radius:9px;padding:9px 11px;display:flex;align-items:center;gap:9px;}
|
| 127 |
+
.app-mini-card img{width:34px;height:34px;border-radius:7px;}
|
| 128 |
+
.app-mini-info{flex:1;min-width:0;}
|
| 129 |
+
.app-mini-title{font-size:11px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
| 130 |
+
.app-mini-score{font-size:10px;color:var(--amber);margin-top:1px;}
|
| 131 |
+
.app-mini-ct{font-size:9px;color:var(--muted);margin-top:1px;}
|
| 132 |
+
.table-container{background:var(--surface);border:1px solid var(--border);border-radius:12px;overflow:hidden;}
|
| 133 |
+
table{width:100%;border-collapse:collapse;font-size:12px;}
|
| 134 |
+
th{text-align:left;background:var(--surface2);padding:9px 13px;color:var(--muted2);font-weight:700;font-size:10px;text-transform:uppercase;border-bottom:1px solid var(--border);letter-spacing:.4px;}
|
| 135 |
+
td{padding:11px 13px;border-bottom:1px solid var(--border);vertical-align:top;}
|
| 136 |
+
tr:last-child td{border-bottom:none;}
|
| 137 |
+
tr:hover td{background:rgba(255,255,255,.012);}
|
| 138 |
+
.app-tag{display:inline-flex;align-items:center;gap:5px;background:var(--accent-dim);color:var(--accent);padding:2px 7px;border-radius:5px;font-weight:700;font-size:10px;margin-bottom:4px;border:1px solid rgba(59,130,246,.2);}
|
| 139 |
+
.score-stars{color:var(--amber);white-space:nowrap;letter-spacing:1px;}
|
| 140 |
+
.rev-content{color:#cbd5e1;line-height:1.55;max-width:460px;word-wrap:break-word;font-size:12px;}
|
| 141 |
+
.dev-reply-cell{margin-top:7px;padding:7px 9px;background:rgba(34,197,94,.05);border-left:2px solid var(--green);border-radius:0 5px 5px 0;font-size:11px;color:var(--muted2);}
|
| 142 |
+
.dev-reply-lbl{font-weight:700;color:var(--green);font-size:9px;text-transform:uppercase;margin-bottom:3px;display:block;}
|
| 143 |
+
.hpill{display:inline-flex;align-items:center;gap:4px;background:var(--surface2);padding:3px 7px;border-radius:9px;font-size:11px;color:var(--muted2);border:1px solid var(--border);}
|
| 144 |
+
.hpill svg{width:10px;height:10px;fill:none;stroke:var(--accent);stroke-width:2.5;}
|
| 145 |
+
.th-wrap{position:relative;}
|
| 146 |
+
.th-inner{display:flex;align-items:center;gap:4px;cursor:pointer;user-select:none;white-space:nowrap;}
|
| 147 |
+
.sa{font-size:10px;color:var(--accent);}
|
| 148 |
+
.fi{width:11px;height:11px;fill:none;stroke:currentColor;stroke-width:2.5;opacity:.3;transition:opacity .15s;flex-shrink:0;}
|
| 149 |
+
.fi.on{opacity:1;stroke:var(--accent);}
|
| 150 |
+
.th-inner:hover .fi{opacity:.65;}
|
| 151 |
+
.filter-dd{position:absolute;top:calc(100% + 3px);left:0;min-width:190px;max-width:250px;background:var(--surface2);border:1px solid var(--border);border-radius:9px;box-shadow:0 12px 36px rgba(0,0,0,.65);z-index:600;overflow:hidden;}
|
| 152 |
+
.fdd-search{padding:6px 7px;border-bottom:1px solid var(--border);}
|
| 153 |
+
.fdd-search input{padding:5px 9px;font-size:11px;border-radius:6px;}
|
| 154 |
+
.fdd-list{max-height:200px;overflow-y:auto;padding:3px;}
|
| 155 |
+
.fdd-opt{display:flex;align-items:center;gap:7px;padding:5px 7px;border-radius:5px;cursor:pointer;font-size:11px;transition:.1s;}
|
| 156 |
+
.fdd-opt:hover{background:var(--accent-dim);}
|
| 157 |
+
.fdd-opt input[type=checkbox]{width:12px;height:12px;accent-color:var(--accent);cursor:pointer;flex-shrink:0;}
|
| 158 |
+
.fdd-opt-lbl{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
| 159 |
+
.fdd-opt-ct{font-size:9px;color:var(--muted);flex-shrink:0;}
|
| 160 |
+
.fdd-acts{display:flex;gap:5px;padding:7px;border-top:1px solid var(--border);}
|
| 161 |
+
.fdd-btn{flex:1;padding:5px;border-radius:6px;border:none;cursor:pointer;font-size:11px;font-weight:700;transition:.15s;}
|
| 162 |
+
.fdd-btn.clr{background:var(--surface);color:var(--muted2);}
|
| 163 |
+
.fdd-btn.clr:hover{color:white;}
|
| 164 |
+
.fdd-btn.apl{background:var(--accent);color:white;}
|
| 165 |
+
.cards-view{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:12px;}
|
| 166 |
+
.rcb{background:var(--surface);border:1px solid var(--border);border-radius:12px;overflow:hidden;transition:border-color .15s;}
|
| 167 |
+
.rcb:hover{border-color:#2d3a4f;}
|
| 168 |
+
.rcb-main{padding:13px 15px;}
|
| 169 |
+
.rcb-header{display:flex;align-items:center;gap:9px;margin-bottom:8px;}
|
| 170 |
+
.rcb-avatar{width:32px;height:32px;border-radius:50%;background:var(--surface2);flex-shrink:0;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:11px;color:var(--muted2);border:1px solid var(--border);overflow:hidden;}
|
| 171 |
+
.rcb-avatar img{width:32px;height:32px;border-radius:50%;object-fit:cover;}
|
| 172 |
+
.rcb-meta{flex:1;min-width:0;}
|
| 173 |
+
.rcb-user{font-weight:700;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
| 174 |
+
.rcb-date{font-size:10px;color:var(--muted);margin-top:1px;}
|
| 175 |
+
.rcb-text{font-size:12px;color:#cbd5e1;line-height:1.6;}
|
| 176 |
+
.rcb-footer{padding:8px 15px;background:var(--bg);border-top:1px solid var(--border);display:flex;gap:5px;flex-wrap:wrap;align-items:center;}
|
| 177 |
+
.mpill{display:inline-flex;align-items:center;gap:3px;font-size:10px;font-weight:600;padding:2px 7px;border-radius:11px;border:1px solid var(--border);color:var(--muted2);}
|
| 178 |
+
.mpill.app{color:var(--accent);border-color:rgba(59,130,246,.25);background:var(--accent-dim);}
|
| 179 |
+
.mpill.replied{color:var(--green);border-color:rgba(34,197,94,.25);background:var(--green-dim);}
|
| 180 |
+
.rcb-dev{margin:0 15px 11px;background:var(--surface2);border:1px solid var(--border);border-left:2.5px solid var(--green);border-radius:6px;padding:8px 10px;}
|
| 181 |
+
.rcb-dev-hdr{font-size:9px;font-weight:700;color:var(--green);margin-bottom:4px;}
|
| 182 |
+
.rcb-dev-text{font-size:11px;color:var(--muted2);line-height:1.5;}
|
| 183 |
+
.loader-overlay{position:absolute;inset:0;background:var(--bg);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px;z-index:10;}
|
| 184 |
+
.spinner{width:34px;height:34px;border:3px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .8s linear infinite;}
|
| 185 |
+
@keyframes spin{to{transform:rotate(360deg);}}
|
| 186 |
+
.hidden{display:none!important;}
|
| 187 |
+
#chat-dialer{position:fixed;bottom:22px;right:22px;width:50px;height:50px;background:var(--accent);border-radius:50%;display:flex;align-items:center;justify-content:center;box-shadow:0 8px 28px rgba(59,130,246,.4);cursor:pointer;z-index:1000;transition:.3s cubic-bezier(.175,.885,.32,1.275);border:2px solid rgba(255,255,255,.1);}
|
| 188 |
+
#chat-dialer:hover{transform:scale(1.1) rotate(5deg);}
|
| 189 |
+
#chat-dialer svg{width:21px;height:21px;color:white;fill:none;stroke:currentColor;stroke-width:2.5;}
|
| 190 |
+
#chat-window{position:fixed;bottom:82px;right:22px;width:390px;height:560px;background:var(--surface);border:1px solid var(--border);border-radius:18px;display:flex;flex-direction:column;box-shadow:0 20px 50px rgba(0,0,0,.5);z-index:1001;overflow:hidden;transform:translateY(20px) scale(.95);opacity:0;pointer-events:none;transition:.3s cubic-bezier(.4,0,.2,1);}
|
| 191 |
+
#chat-window.open{transform:translateY(0) scale(1);opacity:1;pointer-events:auto;}
|
| 192 |
+
.chat-header{padding:12px 16px;background:var(--accent);color:white;display:flex;align-items:center;gap:11px;flex-shrink:0;}
|
| 193 |
+
.chat-header-info{flex:1;}.chat-header-title{font-weight:800;font-size:13px;}.chat-header-status{font-size:10px;opacity:.8;display:flex;align-items:center;gap:4px;}.status-dot{width:5px;height:5px;background:#22c55e;border-radius:50%;}
|
| 194 |
+
.chat-clear-btn{background:rgba(255,255,255,.15);border:none;color:white;font-size:11px;padding:3px 8px;border-radius:6px;cursor:pointer;}
|
| 195 |
+
.chat-clear-btn:hover{background:rgba(255,255,255,.25);}
|
| 196 |
+
.chat-messages{flex:1;overflow-y:auto;padding:12px;display:flex;flex-direction:column;gap:9px;background-image:radial-gradient(var(--border) 1px,transparent 1px);background-size:20px 20px;}
|
| 197 |
+
.msg-row{display:flex;flex-direction:column;gap:4px;}.msg-row.user{align-items:flex-end;}.msg-row.bot{align-items:flex-start;}
|
| 198 |
+
.message{max-width:88%;padding:9px 13px;border-radius:14px;font-size:12px;line-height:1.6;}
|
| 199 |
+
.message.user{background:var(--accent);color:white;border-bottom-right-radius:4px;}
|
| 200 |
+
.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;}
|
| 201 |
+
.msg-section{margin-top:9px;font-weight:700;font-size:10px;color:var(--accent);letter-spacing:.05em;text-transform:uppercase;}
|
| 202 |
+
.msg-item{display:flex;gap:7px;margin-top:4px;}.msg-item-num,.msg-bullet{font-weight:700;color:var(--accent);min-width:14px;}
|
| 203 |
+
.chat-table-wrap{max-width:100%;overflow-x:auto;border:1px solid var(--border);border-radius:9px;background:var(--surface2);margin-top:4px;}
|
| 204 |
+
.chat-table-title{padding:6px 10px;font-size:10px;font-weight:700;color:var(--accent);border-bottom:1px solid var(--border);text-transform:uppercase;}
|
| 205 |
+
.chat-table{width:100%;border-collapse:collapse;font-size:11px;}
|
| 206 |
+
.chat-table th{padding:5px 9px;text-align:left;font-weight:700;font-size:10px;color:var(--muted2);background:var(--bg);border-bottom:1px solid var(--border);white-space:nowrap;}
|
| 207 |
+
.chat-table td{padding:5px 9px;border-bottom:1px solid var(--border);color:var(--text);max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
| 208 |
+
.chat-table tr:last-child td{border-bottom:none;}.chat-table tr:hover td{background:var(--surface);}
|
| 209 |
+
.typing-indicator{display:flex;gap:4px;padding:9px 13px;background:var(--surface2);border:1px solid var(--border);border-radius:13px;width:fit-content;}
|
| 210 |
+
.dot{width:5px;height:5px;background:var(--muted);border-radius:50%;animation:bounce 1.4s infinite;}
|
| 211 |
+
.dot:nth-child(2){animation-delay:.2s;}.dot:nth-child(3){animation-delay:.4s;}
|
| 212 |
+
@keyframes bounce{0%,80%,100%{transform:translateY(0)}40%{transform:translateY(-5px)}}
|
| 213 |
+
.chat-input-area{padding:11px 13px;background:var(--surface);border-top:1px solid var(--border);display:flex;gap:7px;flex-shrink:0;}
|
| 214 |
+
#chat-input{flex:1;background:var(--bg);border:1px solid var(--border);color:white;padding:8px 11px;border-radius:9px;font-size:12px;outline:none;}
|
| 215 |
+
#chat-input:focus{border-color:var(--accent);}
|
| 216 |
+
.btn-send{width:36px;height:36px;background:var(--accent);color:white;border:none;border-radius:8px;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:.2s;flex-shrink:0;}
|
| 217 |
+
.btn-send:hover{transform:scale(1.05);}.btn-send svg{width:15px;height:15px;fill:none;stroke:currentColor;stroke-width:2.5;}
|
| 218 |
+
.chat-suggestions{display:flex;flex-wrap:wrap;gap:4px;padding:0 13px 7px;}
|
| 219 |
+
.sug-chip{font-size:10px;padding:4px 8px;border-radius:16px;background:var(--surface2);border:1px solid var(--border);color:var(--muted2);cursor:pointer;transition:.2s;}
|
| 220 |
+
.sug-chip:hover{border-color:var(--accent);color:var(--accent);}
|
| 221 |
+
</style>
|
| 222 |
</head>
|
| 223 |
<body>
|
| 224 |
+
<div id="rubber-band"></div>
|
| 225 |
<div class="header">
|
| 226 |
+
<a href="/" class="logo"><svg width="19" height="19" 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>PLAYPULSE</a>
|
| 227 |
+
<nav>
|
| 228 |
+
<a href="/" class="nav-link">Home</a>
|
| 229 |
+
<a href="/scraper" class="nav-link">Single Explorer</a>
|
| 230 |
+
<a href="/batch" class="nav-link active">Batch Intelligence</a>
|
|
|
|
|
|
|
|
|
|
| 231 |
</nav>
|
| 232 |
<div style="flex:1"></div>
|
|
|
|
| 233 |
</div>
|
|
|
|
| 234 |
<div class="main">
|
| 235 |
+
<aside class="sidebar" id="sidebarEl">
|
| 236 |
+
<div class="sidebar-inner">
|
| 237 |
+
<div class="sidebar-tabs">
|
| 238 |
+
<div class="stab active" id="tabFind" onclick="switchTab('find')">🔍 Find Apps</div>
|
| 239 |
+
<div class="stab" id="tabQueue" onclick="switchTab('queue')">⚙️ Queue & Run</div>
|
|
|
|
| 240 |
</div>
|
| 241 |
+
|
| 242 |
+
<!-- FIND PANEL -->
|
| 243 |
+
<div class="spanel" id="panelFind" style="gap:0">
|
| 244 |
+
<div style="font-size:10px;color:var(--muted2);font-weight:600;padding:0 0 7px">Search & drag apps to the Queue tab →</div>
|
| 245 |
+
<div class="find-row">
|
| 246 |
+
<input type="text" id="query" placeholder="Search games, apps…" value="Multiplayer Games" onkeydown="if(event.key==='Enter')findApps()">
|
| 247 |
+
<button onclick="findApps()" id="btnFind" class="btn-find">Find</button>
|
| 248 |
+
</div>
|
| 249 |
+
<div style="display:flex;gap:6px;align-items:center;margin-bottom:8px;">
|
| 250 |
+
<span style="font-size:10px;color:var(--muted);white-space:nowrap">Max results</span>
|
| 251 |
+
<input type="number" id="app_count" value="10" min="1" max="50" style="width:60px;padding:5px 8px;font-size:11px;">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
</div>
|
| 253 |
+
<div class="search-hint" id="searchHint">Search for apps above. Click <strong style="color:var(--accent)">+</strong> or drag into the Queue tab to build your batch.</div>
|
| 254 |
+
<div class="search-results-list" id="searchResultsList"></div>
|
| 255 |
</div>
|
| 256 |
+
|
| 257 |
+
<!-- QUEUE + SETTINGS PANEL -->
|
| 258 |
+
<div class="spanel hidden" id="panelQueue" style="gap:0;overflow-y:auto;">
|
| 259 |
+
<div class="sec-lbl">Batch Queue <span id="queueCountLbl">0 apps</span></div>
|
| 260 |
+
<div class="queue-box" id="queueBox" ondragover="qBoxOver(event)" ondrop="qBoxDrop(event)" ondragleave="qBoxLeave(event)">
|
| 261 |
+
<div class="queue-hdr">
|
| 262 |
+
<span class="queue-htitle">Selected Apps — drag to reorder</span>
|
| 263 |
+
<button class="quick-btn" onclick="clearQueue()">Clear all</button>
|
| 264 |
+
</div>
|
| 265 |
+
<div class="queue-empty" id="queueEmpty">
|
| 266 |
+
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/></svg>
|
| 267 |
+
<p>Drag apps from 🔍 Find Apps<br>or click <strong style="color:var(--accent)">+</strong> to add</p>
|
| 268 |
+
</div>
|
| 269 |
+
<div class="queue-list hidden" id="queueList"></div>
|
| 270 |
+
</div>
|
| 271 |
+
<div class="divider"></div>
|
| 272 |
+
<div class="sec-lbl">Scrape Settings</div>
|
| 273 |
+
<div class="igrp">
|
| 274 |
+
<div style="font-size:10px;color:var(--muted2);font-weight:600;">Reviews Per App</div>
|
| 275 |
+
<div class="mode-toggle">
|
| 276 |
+
<div class="mode-btn active" id="btn-fixed" onclick="setMode('fixed')">Custom</div>
|
| 277 |
+
<div class="mode-btn" id="btn-all" onclick="setMode('all')">Fetch All</div>
|
| 278 |
+
</div>
|
| 279 |
+
<input type="number" id="reviews_per_app" value="50" min="10" step="10">
|
| 280 |
+
</div>
|
| 281 |
+
<div class="igrp">
|
| 282 |
+
<div style="font-size:10px;color:var(--muted2);font-weight:600;margin-bottom:4px">Sort Method</div>
|
| 283 |
+
<select id="sort"><option value="MOST_RELEVANT">Most Relevant</option><option value="NEWEST">Newest</option><option value="RATING">Top Ratings</option></select>
|
| 284 |
+
</div>
|
| 285 |
+
<div class="igrp">
|
| 286 |
+
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:5px;">
|
| 287 |
+
<span style="font-size:10px;color:var(--muted2);font-weight:600;">Star Filter</span>
|
| 288 |
+
<div style="display:flex;gap:3px"><button class="quick-btn" onclick="selectAllStars(true)">All</button><button class="quick-btn" onclick="selectAllStars(false)">None</button></div>
|
| 289 |
+
</div>
|
| 290 |
+
<div class="star-filter-grid">
|
| 291 |
+
<label class="star-row"><input type="checkbox" class="star-cb" value="5" checked><span class="star-label"><span class="stars-on">★★★★★</span> 5</span></label>
|
| 292 |
+
<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> 4</span></label>
|
| 293 |
+
<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> 3</span></label>
|
| 294 |
+
<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> 2</span></label>
|
| 295 |
+
<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> 1</span></label>
|
| 296 |
+
</div>
|
| 297 |
+
</div>
|
| 298 |
+
<button class="btn-main" id="go" onclick="runBatch()" disabled>
|
| 299 |
+
<svg width="14" height="14" 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>
|
| 300 |
+
RUN BATCH ANALYSIS
|
| 301 |
+
</button>
|
| 302 |
</div>
|
| 303 |
</div>
|
| 304 |
+
<div class="resize-handle" id="resizeHandle"></div>
|
| 305 |
+
<div class="collapse-btn" id="collapseBtn" onclick="toggleSidebar()">
|
| 306 |
+
<svg viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg>
|
| 307 |
+
</div>
|
|
|
|
| 308 |
</aside>
|
| 309 |
|
| 310 |
<div class="content">
|
| 311 |
+
<div class="toolbar" id="toolbarEl" style="display:none">
|
| 312 |
+
<div class="sbox"><svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg><input type="text" id="globalSearch" placeholder="Search reviews…" oninput="applyAllFilters()"></div>
|
| 313 |
+
<div class="chips-row" id="activeFiltersRow"></div>
|
| 314 |
+
<div class="tb-right">
|
| 315 |
+
<span class="res-count" id="resultStats"></span>
|
| 316 |
+
<div class="vs">
|
| 317 |
+
<button class="vb active" id="vbtnTable" onclick="switchViewMode('table')" title="Table"><svg viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/><line x1="9" y1="9" x2="9" y2="21"/></svg></button>
|
| 318 |
+
<button class="vb" id="vbtnCards" onclick="switchViewMode('cards')" title="Cards"><svg viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/></svg></button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
</div>
|
| 320 |
+
<button class="btn-exp" onclick="downloadCSV()"><svg viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg>Export CSV</button>
|
| 321 |
+
</div>
|
| 322 |
+
</div>
|
| 323 |
+
<div id="dataView" class="scroll-view">
|
| 324 |
+
<div id="welcome" style="display:flex;flex-direction:column;align-items:center;justify-content:center;flex:1;color:var(--muted);gap:14px;text-align:center;padding:40px;">
|
| 325 |
+
<svg width="52" height="52" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/></svg>
|
| 326 |
+
<div><p style="font-size:15px;font-weight:700;color:var(--muted2);margin-bottom:6px">No batch data yet</p>
|
| 327 |
+
<p style="font-size:12px;line-height:1.8">1. Use <strong style="color:var(--text)">Find Apps</strong> to search by genre, name…<br>2. Click <strong style="color:var(--accent)">+</strong> or drag into the queue<br>3. Switch to <strong style="color:var(--text)">Queue & Run</strong> and run</p></div>
|
| 328 |
</div>
|
| 329 |
+
<div id="results" class="hidden" style="display:flex;flex-direction:column;gap:14px;"></div>
|
| 330 |
</div>
|
| 331 |
<div id="loader" class="loader-overlay hidden">
|
| 332 |
<div class="spinner"></div>
|
| 333 |
+
<p style="color:var(--muted);font-size:13px" id="loaderMsg">Running batch analysis…</p>
|
| 334 |
</div>
|
| 335 |
</div>
|
| 336 |
</div>
|
| 337 |
|
| 338 |
+
<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>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 339 |
<div id="chat-window">
|
| 340 |
<div class="chat-header">
|
| 341 |
+
<svg width="17" height="17" 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>
|
| 342 |
+
<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>
|
| 343 |
+
<div style="display:flex;gap:7px;align-items:center"><button class="chat-clear-btn" onclick="clearChat()">Clear</button><div style="cursor:pointer;opacity:.7" onclick="toggleChat()"><svg width="15" height="15" 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>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 344 |
</div>
|
| 345 |
+
<div class="chat-messages" id="chat-messages"><div class="msg-row bot"><div class="message bot">👋 Hi! Search apps, build your queue, run batch — then ask me to compare, find issues, or show tables!</div></div></div>
|
| 346 |
+
<div class="chat-suggestions">
|
| 347 |
<div class="sug-chip" onclick="fillChat('Compare all apps by rating')">Compare apps</div>
|
| 348 |
<div class="sug-chip" onclick="fillChat('Which app has the most complaints?')">Most complaints</div>
|
| 349 |
+
<div class="sug-chip" onclick="fillChat('Show 1 star reviews in table')">1★ table</div>
|
| 350 |
+
<div class="sug-chip" onclick="fillChat('What are common issues?')">Common issues</div>
|
| 351 |
</div>
|
| 352 |
<div class="chat-input-area">
|
| 353 |
+
<input type="text" id="chat-input" placeholder="Ask about batch analysis…" onkeydown="if(event.key==='Enter') sendChatMessage()">
|
| 354 |
+
<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>
|
|
|
|
|
|
|
| 355 |
</div>
|
| 356 |
</div>
|
| 357 |
|
| 358 |
<script>
|
| 359 |
+
// STATE
|
| 360 |
+
let currentData=null,currentMode='fixed',viewMode='table';
|
| 361 |
+
let searchResults=[],queue=[];
|
| 362 |
+
let colFilters={},sortCol=null,sortDir=1,openDd=null;
|
| 363 |
+
let draggedApp=null,qDragSrc=null;
|
| 364 |
|
| 365 |
+
// SIDEBAR TABS
|
| 366 |
+
function switchTab(t){
|
| 367 |
+
document.getElementById('tabFind').classList.toggle('active',t==='find');
|
| 368 |
+
document.getElementById('tabQueue').classList.toggle('active',t==='queue');
|
| 369 |
+
document.getElementById('panelFind').classList.toggle('hidden',t!=='find');
|
| 370 |
+
document.getElementById('panelQueue').classList.toggle('hidden',t!=='queue');
|
| 371 |
+
}
|
| 372 |
|
| 373 |
+
// SIDEBAR COLLAPSE + RESIZE
|
| 374 |
+
let sbCollapsed=false;
|
| 375 |
+
function toggleSidebar(){
|
| 376 |
+
sbCollapsed=!sbCollapsed;
|
| 377 |
+
const sb=document.getElementById('sidebarEl');
|
| 378 |
+
sb.classList.toggle('collapsed',sbCollapsed);
|
| 379 |
+
}
|
| 380 |
+
const rh=document.getElementById('resizeHandle'),sb=document.getElementById('sidebarEl');
|
| 381 |
+
let resizing=false,rsX=0,rsW=0;
|
| 382 |
+
rh.addEventListener('mousedown',e=>{resizing=true;rsX=e.clientX;rsW=sb.offsetWidth;document.body.style.cursor='col-resize';document.body.style.userSelect='none';e.preventDefault();});
|
| 383 |
+
document.addEventListener('mousemove',e=>{if(!resizing)return;const nw=Math.max(220,Math.min(500,rsW+(e.clientX-rsX)));sb.style.width=nw+'px';});
|
| 384 |
+
document.addEventListener('mouseup',()=>{if(resizing){resizing=false;document.body.style.cursor='';document.body.style.userSelect='';}});
|
| 385 |
|
| 386 |
+
// FIND APPS
|
| 387 |
+
async function findApps(){
|
| 388 |
+
const q=document.getElementById('query').value.trim();if(!q)return;
|
| 389 |
+
const btn=document.getElementById('btnFind');btn.disabled=true;btn.textContent='...';
|
| 390 |
+
const hint=document.getElementById('searchHint');
|
| 391 |
+
hint.innerHTML='Searching...';hint.style.display='block';
|
| 392 |
+
const list=document.getElementById('searchResultsList');
|
| 393 |
+
list.innerHTML=Array(6).fill('').map(()=>'<div class="sr-skeleton"><div class="sk-icon"></div><div class="sk-lines"><div class="sk-line"></div><div class="sk-line short"></div></div></div>').join('');
|
| 394 |
+
try{
|
| 395 |
+
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})});
|
| 396 |
+
const data=await res.json();if(!res.ok)throw new Error(data.error||'Discovery failed');
|
| 397 |
+
searchResults=data.results;hint.style.display='none';renderSearchResults();
|
| 398 |
+
}catch(e){list.innerHTML='';hint.textContent='Error: '+e.message;}
|
| 399 |
+
finally{btn.disabled=false;btn.textContent='Find';}
|
| 400 |
+
}
|
| 401 |
|
| 402 |
+
function renderSearchResults(){
|
| 403 |
+
const list=document.getElementById('searchResultsList');
|
| 404 |
+
if(!searchResults.length){list.innerHTML='<div style="text-align:center;padding:20px;color:var(--muted);font-size:11px">No results found</div>';return;}
|
| 405 |
+
const qIds=new Set(queue.map(a=>a.appId));
|
| 406 |
+
list.innerHTML=searchResults.map((a,i)=>{
|
| 407 |
+
const inQ=qIds.has(a.appId);
|
| 408 |
+
return `<div class="sr-item${inQ?' in-queue':''}" data-idx="${i}" draggable="true" ondragstart="srDragStart(event,${i})" ondragend="srDragEnd(event)">
|
| 409 |
+
<img src="${escH(a.icon)}" alt="" onerror="this.src='data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22%3E%3Crect fill=%22%231c2333%22 width=%2224%22 height=%2224%22/%3E%3C/svg%3E'">
|
| 410 |
+
<div class="sr-info"><div class="sr-title">${escH(a.title)}</div><div class="sr-dev">${escH(a.developer||'')}</div></div>
|
| 411 |
+
${a.score>0?`<div class="sr-score">${a.score}☆</div>`:''}
|
| 412 |
+
<div class="sr-add-btn" onclick="toggleInQueue(${i})" title="${inQ?'Remove from queue':'Add to queue'}">${inQ?'✓':'+'}</div>
|
| 413 |
+
</div>`;
|
| 414 |
+
}).join('');
|
| 415 |
+
}
|
| 416 |
|
| 417 |
+
// DRAG FROM SEARCH LIST
|
| 418 |
+
function srDragStart(e,idx){draggedApp=searchResults[idx];e.dataTransfer.effectAllowed='copy';requestAnimationFrame(()=>e.target.classList.add('dragging-src'));}
|
| 419 |
+
function srDragEnd(e){draggedApp=null;document.querySelectorAll('.sr-item').forEach(el=>el.classList.remove('dragging-src'));document.getElementById('queueBox').classList.remove('drag-active');}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 420 |
|
| 421 |
+
// QUEUE DROP ZONE
|
| 422 |
+
function qBoxOver(e){e.preventDefault();e.dataTransfer.dropEffect='copy';document.getElementById('queueBox').classList.add('drag-active');}
|
| 423 |
+
function qBoxLeave(e){if(!e.currentTarget.contains(e.relatedTarget))document.getElementById('queueBox').classList.remove('drag-active');}
|
| 424 |
+
function qBoxDrop(e){
|
| 425 |
+
e.preventDefault();document.getElementById('queueBox').classList.remove('drag-active');
|
| 426 |
+
if(draggedApp){addToQueue(draggedApp);draggedApp=null;}
|
| 427 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 428 |
|
| 429 |
+
function addToQueue(app){
|
| 430 |
+
if(queue.find(a=>a.appId===app.appId))return;
|
| 431 |
+
queue.push({appId:app.appId,title:app.title,icon:app.icon,score:app.score,developer:app.developer});
|
| 432 |
+
renderQueue();renderSearchResults();updateRunBtn();
|
| 433 |
+
}
|
| 434 |
+
function removeFromQueue(appId){queue=queue.filter(a=>a.appId!==appId);renderQueue();renderSearchResults();updateRunBtn();}
|
| 435 |
+
function clearQueue(){queue=[];renderQueue();renderSearchResults();updateRunBtn();}
|
| 436 |
+
function toggleInQueue(idx){
|
| 437 |
+
const app=searchResults[idx];
|
| 438 |
+
if(queue.find(a=>a.appId===app.appId))removeFromQueue(app.appId);else addToQueue(app);
|
| 439 |
+
}
|
| 440 |
|
| 441 |
+
// QUEUE REORDER
|
| 442 |
+
function qiDragStart(e,i){qDragSrc=i;e.dataTransfer.effectAllowed='move';requestAnimationFrame(()=>document.querySelectorAll('.q-item')[i]?.classList.add('q-dragging'));}
|
| 443 |
+
function qiDragOver(e,i){e.preventDefault();document.querySelectorAll('.q-item').forEach((el,j)=>el.classList.toggle('drag-over',j===i&&j!==qDragSrc));}
|
| 444 |
+
function qiDrop(e,i){e.preventDefault();if(qDragSrc===null||qDragSrc===i)return;const m=queue.splice(qDragSrc,1)[0];queue.splice(i,0,m);renderQueue();}
|
| 445 |
+
function qiDragEnd(){qDragSrc=null;document.querySelectorAll('.q-item').forEach(el=>el.classList.remove('q-dragging','drag-over'));}
|
| 446 |
|
| 447 |
+
function renderQueue(){
|
| 448 |
+
const empty=document.getElementById('queueEmpty'),list=document.getElementById('queueList');
|
| 449 |
+
const ct=document.getElementById('queueCountLbl');
|
| 450 |
+
ct.textContent=queue.length?`${queue.length} app${queue.length>1?'s':''}`:'0 apps';
|
| 451 |
+
if(!queue.length){empty.classList.remove('hidden');list.classList.add('hidden');return;}
|
| 452 |
+
empty.classList.add('hidden');list.classList.remove('hidden');
|
| 453 |
+
list.innerHTML=queue.map((a,i)=>`<div class="q-item" draggable="true" ondragstart="qiDragStart(event,${i})" ondragover="qiDragOver(event,${i})" ondrop="qiDrop(event,${i})" ondragend="qiDragEnd()">
|
| 454 |
+
<div class="q-handle"><span></span><span></span><span></span></div>
|
| 455 |
+
<img src="${escH(a.icon)}" alt="" onerror="this.src='data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22%3E%3Crect fill=%22%231c2333%22 width=%2224%22 height=%2224%22/%3E%3C/svg%3E'">
|
| 456 |
+
<div class="q-info"><div class="q-title">${escH(a.title)}</div>${a.score>0?`<div class="q-score">${a.score}☆</div>`:''}</div>
|
| 457 |
+
<button class="q-rm" onclick="removeFromQueue('${a.appId}')" title="Remove">×</button>
|
| 458 |
+
</div>`).join('');
|
| 459 |
+
}
|
| 460 |
|
| 461 |
+
function updateRunBtn(){document.getElementById('go').disabled=queue.length===0;}
|
|
|
|
|
|
|
|
|
|
| 462 |
|
| 463 |
+
// SCRAPE SETTINGS
|
| 464 |
+
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');}
|
| 465 |
+
function selectAllStars(c){document.querySelectorAll('.star-cb').forEach(cb=>cb.checked=c);}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 466 |
|
| 467 |
+
// RUN BATCH
|
| 468 |
+
async function runBatch(){
|
| 469 |
+
if(!queue.length)return alert('Add at least one app to the queue');
|
| 470 |
+
const stars=[...document.querySelectorAll('.star-cb:checked')].map(cb=>parseInt(cb.value));
|
| 471 |
+
if(!stars.length)return alert('Select at least one star rating');
|
| 472 |
+
document.getElementById('welcome').classList.add('hidden');
|
| 473 |
+
document.getElementById('results').classList.add('hidden');
|
| 474 |
+
document.getElementById('loader').classList.remove('hidden');
|
| 475 |
+
document.getElementById('go').disabled=true;
|
| 476 |
+
colFilters={};sortCol=null;
|
| 477 |
+
const msgs=['Connecting...','Fetching app data...','Scraping reviews...','Compiling results...'];
|
| 478 |
+
let mi=0;document.getElementById('loaderMsg').textContent=msgs[0];
|
| 479 |
+
const msgInt=setInterval(()=>{mi=(mi+1)%msgs.length;document.getElementById('loaderMsg').textContent=msgs[mi];},2000);
|
| 480 |
+
try{
|
| 481 |
+
const res=await fetch('/scrape-batch',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({app_ids:queue.map(a=>a.appId),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})});
|
| 482 |
+
const data=await res.json();if(!res.ok)throw new Error(data.error||'Batch failed');
|
| 483 |
+
currentData=data;document.getElementById('toolbarEl').style.display='flex';render(data);
|
| 484 |
+
}catch(e){document.getElementById('welcome').classList.remove('hidden');alert(e.message);}
|
| 485 |
+
finally{clearInterval(msgInt);document.getElementById('loader').classList.add('hidden');document.getElementById('go').disabled=queue.length===0;}
|
| 486 |
+
}
|
| 487 |
|
| 488 |
+
// VIEW MODE
|
| 489 |
+
function switchViewMode(m){viewMode=m;document.getElementById('vbtnTable').classList.toggle('active',m==='table');document.getElementById('vbtnCards').classList.toggle('active',m==='cards');if(currentData)applyAllFilters();}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 490 |
|
| 491 |
+
// FILTER ENGINE
|
| 492 |
+
function filtered(){
|
| 493 |
+
if(!currentData)return[];
|
| 494 |
+
let rv=[...currentData.reviews];
|
| 495 |
+
const q=(document.getElementById('globalSearch')?.value||'').toLowerCase();
|
| 496 |
+
if(q)rv=rv.filter(r=>(r.content||'').toLowerCase().includes(q)||(r.userName||'').toLowerCase().includes(q));
|
| 497 |
+
if(colFilters.app?.size)rv=rv.filter(r=>{const a=currentData.apps.find(x=>x.appId===r.appId);return colFilters.app.has(a?.title||r.appTitle||'');});
|
| 498 |
+
if(colFilters.score?.size)rv=rv.filter(r=>colFilters.score.has(String(r.score)));
|
| 499 |
+
if(colFilters.replied?.size)rv=rv.filter(r=>colFilters.replied.has(r.replyContent?.trim()?'Yes':'No'));
|
| 500 |
+
if(colFilters.date?.size)rv=rv.filter(r=>colFilters.date.has(r.at?String(new Date(r.at).getFullYear()):'Unknown'));
|
| 501 |
+
if(sortCol)rv.sort((a,b)=>{let va,vb;
|
| 502 |
+
if(sortCol==='score'){va=a.score||0;vb=b.score||0;}
|
| 503 |
+
else if(sortCol==='date'){va=a.at||'';vb=b.at||'';}
|
| 504 |
+
else if(sortCol==='helpful'){va=a.thumbsUpCount||0;vb=b.thumbsUpCount||0;}
|
| 505 |
+
else if(sortCol==='app'){va=(currentData.apps.find(x=>x.appId===a.appId)||{}).title||'';vb=(currentData.apps.find(x=>x.appId===b.appId)||{}).title||'';}
|
| 506 |
+
return va<vb?-sortDir:va>vb?sortDir:0;
|
| 507 |
+
});
|
| 508 |
+
return rv;
|
| 509 |
+
}
|
| 510 |
+
function applyAllFilters(){if(!currentData)return;renderResults(currentData,filtered());renderChips();}
|
| 511 |
+
function renderChips(){
|
| 512 |
+
const row=document.getElementById('activeFiltersRow');
|
| 513 |
+
row.innerHTML=Object.entries(colFilters).filter(([,v])=>v?.size).map(([col,vals])=>{
|
| 514 |
+
const vStr=[...vals].slice(0,2).join(', ')+(vals.size>2?` +${vals.size-2}`:'');
|
| 515 |
+
return `<div class="a-chip" onclick="clearF('${col}')" title="Remove">${col.charAt(0).toUpperCase()+col.slice(1)}: ${escH(vStr)} ✕</div>`;
|
| 516 |
+
}).join('');
|
| 517 |
+
}
|
| 518 |
+
function clearF(col){delete colFilters[col];applyAllFilters();}
|
| 519 |
|
| 520 |
+
// COLUMN FILTER DROPDOWN
|
| 521 |
+
function openColFilter(colKey,triggerEl){
|
| 522 |
+
if(openDd){openDd.remove();openDd=null;}
|
| 523 |
+
if(!currentData)return;
|
| 524 |
+
const rv=currentData.reviews;const counts={};
|
| 525 |
+
rv.forEach(r=>{let v;
|
| 526 |
+
if(colKey==='app'){const a=currentData.apps.find(x=>x.appId===r.appId);v=a?.title||r.appTitle||'Unknown';}
|
| 527 |
+
else if(colKey==='score')v=String(r.score);
|
| 528 |
+
else if(colKey==='replied')v=r.replyContent?.trim()?'Yes':'No';
|
| 529 |
+
else if(colKey==='date')v=r.at?String(new Date(r.at).getFullYear()):'Unknown';
|
| 530 |
+
if(v!==undefined)counts[v]=(counts[v]||0)+1;
|
| 531 |
+
});
|
| 532 |
+
const allVals=Object.keys(counts).sort();
|
| 533 |
+
const tempSel=new Set(colFilters[colKey]||allVals);
|
| 534 |
+
const dd=document.createElement('div');dd.className='filter-dd';
|
| 535 |
+
triggerEl.closest('.th-wrap').appendChild(dd);openDd=dd;
|
| 536 |
+
function buildDd(q){
|
| 537 |
+
const show=allVals.filter(v=>v.toLowerCase().includes(q));
|
| 538 |
+
dd.innerHTML=`<div class="fdd-search"><input type="text" placeholder="Search..." id="fddQ_${colKey}" value="${escH(q)}" oninput="rebuildFdd('${colKey}',this.value)"></div>
|
| 539 |
+
<div class="fdd-list">
|
| 540 |
+
<div class="fdd-opt" onclick="fddToggleAll('${colKey}','${q}')"><input type="checkbox" ${show.every(v=>tempSel.has(v))?'checked':''}><span class="fdd-opt-lbl" style="font-weight:700;color:var(--text)">(Select All)</span></div>
|
| 541 |
+
${show.map(v=>`<div class="fdd-opt" onclick="fddToggleVal('${colKey}','${v.replace(/'/g,"\\'")}','${q}')"><input type="checkbox" ${tempSel.has(v)?'checked':''}><span class="fdd-opt-lbl">${escH(v)}</span><span class="fdd-opt-ct">${counts[v]}</span></div>`).join('')}
|
| 542 |
+
</div>
|
| 543 |
+
<div class="fdd-acts"><button class="fdd-btn clr" onclick="fddClear('${colKey}')">Clear</button><button class="fdd-btn apl" onclick="fddApply('${colKey}')">Apply</button></div>`;
|
| 544 |
}
|
| 545 |
+
window['_ts_'+colKey]=tempSel;window['_av_'+colKey]=allVals;
|
| 546 |
+
window.rebuildFdd=(k,q)=>buildDd(q);
|
| 547 |
+
window.fddToggleVal=(k,v,q)=>{window['_ts_'+k].has(v)?window['_ts_'+k].delete(v):window['_ts_'+k].add(v);buildDd(q);};
|
| 548 |
+
window.fddToggleAll=(k,q)=>{const s=window['_ts_'+k];const sv=window['_av_'+k].filter(v=>v.toLowerCase().includes(q));sv.every(v=>s.has(v))?sv.forEach(v=>s.delete(v)):sv.forEach(v=>s.add(v));buildDd(q);};
|
| 549 |
+
window.fddClear=(k)=>{delete colFilters[k];dd.remove();openDd=null;applyAllFilters();};
|
| 550 |
+
window.fddApply=(k)=>{const s=window['_ts_'+k],av=window['_av_'+k];if(s.size===av.length)delete colFilters[k];else colFilters[k]=new Set(s);dd.remove();openDd=null;applyAllFilters();};
|
| 551 |
+
buildDd('');
|
| 552 |
+
}
|
| 553 |
+
document.addEventListener('click',e=>{if(openDd&&!openDd.contains(e.target)&&!e.target.closest('.th-inner')){openDd.remove();openDd=null;}});
|
| 554 |
+
function thClick(key,e){
|
| 555 |
+
const svgEl=e.target.tagName==='svg'||e.target.tagName==='polyline'||e.target.tagName==='path'||e.target.tagName==='polygon'||!!e.target.closest('svg');
|
| 556 |
+
if(svgEl)openColFilter(key,e.currentTarget);
|
| 557 |
+
else{if(sortCol===key)sortDir=-sortDir;else{sortCol=key;sortDir=1;}applyAllFilters();}
|
| 558 |
+
}
|
| 559 |
+
|
| 560 |
+
// RENDER
|
| 561 |
+
function render(data){document.getElementById('welcome').classList.add('hidden');document.getElementById('results').classList.remove('hidden');applyAllFilters();}
|
| 562 |
+
function renderResults(data,rv){
|
| 563 |
+
document.getElementById('resultStats').textContent=`${rv.length.toLocaleString()} reviews`;
|
| 564 |
+
const summaryHTML=`<div class="batch-summary"><div style="font-size:9px;font-weight:800;text-transform:uppercase;color:var(--muted);letter-spacing:.8px;margin-bottom:2px">Comparing Apps</div><div class="apps-grid">${data.apps.map(a=>`<div class="app-mini-card"><img src="${escH(a.icon)}" alt=""><div class="app-mini-info"><div class="app-mini-title">${escH(a.title)}</div><div class="app-mini-score">${(a.score||0).toFixed(1)} ★</div><div class="app-mini-ct">${data.reviews.filter(r=>r.appId===a.appId).length} reviews</div></div></div>`).join('')}</div></div>`;
|
| 565 |
+
const bodyHTML=viewMode==='table'?renderTable(data,rv):renderCards(data,rv);
|
| 566 |
+
document.getElementById('results').innerHTML=summaryHTML+bodyHTML;
|
| 567 |
+
}
|
| 568 |
+
function sa(k){return sortCol===k?(sortDir===1?'↑':'↓'):''}
|
| 569 |
+
function fiSvg(k){const on=colFilters[k]?.size;return `<svg class="fi${on?' on':''}" viewBox="0 0 24 24"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg>`}
|
| 570 |
+
function renderTable(data,rv){
|
| 571 |
+
const cols=[{key:'app',label:'App / User',w:'160px'},{key:'score',label:'Score',w:'85px'},{key:null,label:'Review & Response'},{key:'replied',label:'Reply',w:'68px'},{key:'helpful',label:'Helpful',w:'80px'},{key:'date',label:'Date',w:'100px'}];
|
| 572 |
+
const ths=cols.map(c=>`<th class="th-wrap" ${c.w?`style="width:${c.w}"`:''}>${c.key?`<div class="th-inner" onclick="thClick('${c.key}',event)">${c.label}<span class="sa">${sa(c.key)}</span>${fiSvg(c.key)}</div>`:`<div class="th-inner">${c.label}</div>`}</th>`).join('');
|
| 573 |
+
const rows=rv.map(r=>{
|
| 574 |
+
const app=data.apps.find(a=>a.appId===r.appId)||{title:r.appTitle||''};
|
| 575 |
+
const hasReply=!!(r.replyContent?.trim());
|
| 576 |
+
const stars='★'.repeat(r.score)+`<span style="color:var(--border)">${'★'.repeat(5-r.score)}</span>`;
|
| 577 |
+
const replyHtml=hasReply?`<div class="dev-reply-cell"><span class="dev-reply-lbl">Dev Reply</span>${escH(r.replyContent)}</div>`:'';
|
| 578 |
+
return `<tr><td><div class="app-tag">${escH(app.title)}</div><div style="font-size:11px;font-weight:700">${escH(r.userName||'Anonymous')}</div></td><td><div class="score-stars">${stars}</div><div style="font-size:9px;color:var(--muted);margin-top:3px">${r.score}/5</div></td><td><div class="rev-content">${escH(r.content||'')}</div>${replyHtml}</td><td>${hasReply?'<span style="color:var(--green);font-size:11px;font-weight:700">✓</span>':'<span style="color:var(--muted);font-size:11px">—</span>'}</td><td><div class="hpill"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="10" height="10"><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.3z"/><path d="M7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"/></svg>${r.thumbsUpCount||0}</div></td><td><div style="color:var(--muted);font-size:11px">${fmtDate(r.at)}</div></td></tr>`;
|
| 579 |
+
}).join('');
|
| 580 |
+
return `<div class="table-container"><table><thead><tr>${ths}</tr></thead><tbody>${rows||'<tr><td colspan="6" style="text-align:center;padding:30px;color:var(--muted)">No reviews match the current filters</td></tr>'}</tbody></table></div>`;
|
| 581 |
+
}
|
| 582 |
+
function renderCards(data,rv){
|
| 583 |
+
const cards=rv.map(r=>{
|
| 584 |
+
const app=data.apps.find(a=>a.appId===r.appId)||{title:r.appTitle||''};
|
| 585 |
+
const hasReply=!!(r.replyContent?.trim());
|
| 586 |
+
const initials=(r.userName||'?').trim().split(/\s+/).map(w=>w[0]).join('').slice(0,2).toUpperCase();
|
| 587 |
+
const avatar=r.userImage?`<div class="rcb-avatar"><img src="${r.userImage}" alt="" onerror="this.style.display='none';this.parentElement.textContent='${initials}'"></div>`:`<div class="rcb-avatar">${initials}</div>`;
|
| 588 |
+
const stars=[...Array(5)].map((_,i)=>`<span style="font-size:12px;color:${i<r.score?'var(--amber)':'var(--border)'}">★</span>`).join('');
|
| 589 |
+
const replyHtml=hasReply?`<div class="rcb-dev"><div class="rcb-dev-hdr">💚 Dev Response</div><div class="rcb-dev-text">${escH(r.replyContent)}</div></div>`:'';
|
| 590 |
+
const thumb=r.thumbsUpCount?`<span class="mpill">👍 ${r.thumbsUpCount}</span>`:'';
|
| 591 |
+
return `<div class="rcb"><div class="rcb-main"><div class="rcb-header">${avatar}<div class="rcb-meta"><div class="rcb-user">${escH(r.userName||'Anonymous')}</div><div class="rcb-date">${fmtDate(r.at)}</div></div><div>${stars}</div></div><div class="rcb-text">${escH(r.content||'')}</div></div>${replyHtml}<div class="rcb-footer"><span class="mpill app">${escH(app.title)}</span>${thumb}${hasReply?'<span class="mpill replied">💬 Dev replied</span>':''}</div></div>`;
|
| 592 |
+
}).join('');
|
| 593 |
+
return `<div class="cards-view">${cards||'<div style="grid-column:1/-1;text-align:center;padding:40px;color:var(--muted)">No reviews match filters</div>'}</div>`;
|
| 594 |
+
}
|
| 595 |
|
| 596 |
+
// UTILS
|
| 597 |
+
function fmtDate(iso){if(!iso)return'';return new Date(iso).toLocaleDateString('en-US',{year:'numeric',month:'short',day:'numeric'});}
|
| 598 |
+
function escH(s){return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');}
|
| 599 |
+
function downloadCSV(){
|
| 600 |
+
if(!currentData)return;
|
| 601 |
+
const esc=v=>`"${String(v||'').replace(/"/g,'""')}"`;
|
| 602 |
+
const hdr=['App Name','App ID','User','Score','Date','Content','Thumbs Up','Developer Reply'];
|
| 603 |
+
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(','));
|
| 604 |
+
const blob=new Blob([[hdr.join(','),...rows].join('\n')],{type:'text/csv'});
|
| 605 |
+
const a=Object.assign(document.createElement('a'),{href:URL.createObjectURL(blob),download:`batch_${Date.now()}.csv`});
|
| 606 |
+
a.click();URL.revokeObjectURL(a.href);
|
| 607 |
+
}
|
| 608 |
+
|
| 609 |
+
// CHAT
|
| 610 |
+
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;})();
|
| 611 |
+
function toggleChat(){document.getElementById('chat-window').classList.toggle('open');}
|
| 612 |
+
function fillChat(t){const i=document.getElementById('chat-input');i.value=t;i.focus();}
|
| 613 |
+
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})});}
|
| 614 |
+
async function sendChatMessage(){
|
| 615 |
+
const input=document.getElementById('chat-input'),msg=input.value.trim();if(!msg)return;
|
| 616 |
+
appendUserMsg(msg);input.value='';
|
| 617 |
+
const c=document.getElementById('chat-messages');
|
| 618 |
+
const t=document.createElement('div');t.className='typing-indicator';t.innerHTML='<div class="dot"></div><div class="dot"></div><div class="dot"></div>';
|
| 619 |
+
c.appendChild(t);c.scrollTop=c.scrollHeight;
|
| 620 |
+
try{
|
| 621 |
+
const res=await fetch('/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:msg,session_id:SESSION_ID,reviews:currentData?.reviews||[]})});
|
| 622 |
+
const data=await res.json();if(t.parentNode)c.removeChild(t);
|
| 623 |
+
if(data.error){appendBotMsg('⚠️ '+data.error,null);return;}
|
| 624 |
+
appendBotMsg(data.reply||'',data.table||null);
|
| 625 |
+
if(data.type==='filter'&&data.filters)chatFilter(data.filters);
|
| 626 |
+
}catch(e){if(t.parentNode)c.removeChild(t);appendBotMsg('Connection error.',null);}
|
| 627 |
+
}
|
| 628 |
+
function appendUserMsg(text){const c=document.getElementById('chat-messages'),row=document.createElement('div');row.className='msg-row user';row.innerHTML=`<div class="message user">${escH(text)}</div>`;c.appendChild(row);c.scrollTop=c.scrollHeight;}
|
| 629 |
+
function appendBotMsg(text,table){
|
| 630 |
+
const c=document.getElementById('chat-messages'),row=document.createElement('div');row.className='msg-row bot';
|
| 631 |
+
if(text?.trim()){const b=document.createElement('div');b.className='message bot';b.innerHTML=renderMD(text);row.appendChild(b);}
|
| 632 |
+
if(table?.rows?.length)row.appendChild(buildTable(table));
|
| 633 |
+
c.appendChild(row);c.scrollTop=c.scrollHeight;
|
| 634 |
+
}
|
| 635 |
+
function renderMD(text){
|
| 636 |
+
const lines=text.split('\n');let html='',inList=false;
|
| 637 |
+
for(let raw of lines){
|
| 638 |
+
if(/^\*\*[^*]+\*\*:?$/.test(raw.trim())){if(inList){html+='</div>';inList=false;}html+=`<div class="msg-section">${escH(raw.trim().replace(/^\*\*/,'').replace(/\*\*:?$/,''))}</div>`;continue;}
|
| 639 |
+
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>${inlineF(nm[2])}</span></div>`;continue;}
|
| 640 |
+
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>${inlineF(bm[1])}</span></div>`;continue;}
|
| 641 |
+
if(inList&&!raw.trim()){html+='</div>';inList=false;}
|
| 642 |
+
html+=raw.trim()?`<span>${inlineF(raw)}</span><br>`:'<br>';
|
| 643 |
}
|
| 644 |
+
if(inList)html+='</div>';return html;
|
| 645 |
+
}
|
| 646 |
+
function inlineF(t){return escH(t).replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>').replace(/_(.+?)_/g,'<em style="color:var(--muted2)">$1</em>');}
|
| 647 |
+
function buildTable(td){
|
| 648 |
+
const{title,columns,rows}=td,w=document.createElement('div');w.className='chat-table-wrap';
|
| 649 |
+
let h='';if(title)h+=`<div class="chat-table-title">${escH(title)}</div>`;
|
| 650 |
+
h+='<table class="chat-table"><thead><tr>';for(const c of columns)h+=`<th>${escH(c)}</th>`;h+='</tr></thead><tbody>';
|
| 651 |
+
for(const row of rows){h+='<tr>';for(const c of columns){const v=row[c]??'';h+=`<td title="${escH(String(v))}">${escH(String(v))}</td>`;}h+='</tr>';}
|
| 652 |
+
h+='</tbody></table>';w.innerHTML=h;return w;
|
| 653 |
+
}
|
| 654 |
+
function chatFilter(raw){
|
| 655 |
+
if(!currentData)return;
|
| 656 |
+
try{
|
| 657 |
+
const f=typeof raw==='string'?JSON.parse(raw):raw;let rv=currentData.reviews;
|
| 658 |
+
if(f.stars?.length)rv=rv.filter(r=>f.stars.includes(r.score));
|
| 659 |
+
if(f.app){const q=f.app.toLowerCase();rv=rv.filter(r=>{const a=currentData.apps.find(x=>x.appId===r.appId)||{};return(a.title||'').toLowerCase().includes(q)||(r.appId||'').toLowerCase().includes(q);});}
|
| 660 |
+
if(f.query){const q=f.query.toLowerCase();rv=rv.filter(r=>(r.content||'').toLowerCase().includes(q)||(r.userName||'').toLowerCase().includes(q));}
|
| 661 |
+
renderResults(currentData,rv);
|
| 662 |
+
}catch(e){console.error(e);}
|
| 663 |
+
}
|
| 664 |
</script>
|
| 665 |
+
|
| 666 |
</body>
|
| 667 |
</html>
|
templates/index.html
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|