Spaces:
Sleeping
Sleeping
File size: 58,540 Bytes
3e89456 88b4fa3 3e89456 88b4fa3 3e89456 88b4fa3 3e89456 88b4fa3 47df4ee 88b4fa3 3e89456 88b4fa3 3e89456 88b4fa3 3e89456 88b4fa3 3e89456 88b4fa3 3e89456 88b4fa3 3e89456 88b4fa3 3e89456 88b4fa3 3e89456 88b4fa3 fee4aa3 88b4fa3 fee4aa3 88b4fa3 fee4aa3 88b4fa3 fee4aa3 88b4fa3 fee4aa3 47df4ee fee4aa3 88b4fa3 fee4aa3 88b4fa3 fee4aa3 88b4fa3 47df4ee 88b4fa3 47df4ee 88b4fa3 fee4aa3 88b4fa3 47df4ee 88b4fa3 3e89456 88b4fa3 fee4aa3 88b4fa3 fee4aa3 88b4fa3 3e89456 88b4fa3 3e89456 88b4fa3 fee4aa3 88b4fa3 fee4aa3 88b4fa3 fee4aa3 88b4fa3 fee4aa3 88b4fa3 fee4aa3 88b4fa3 fee4aa3 88b4fa3 fee4aa3 88b4fa3 fee4aa3 88b4fa3 3e89456 fee4aa3 | 1 2 3 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 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 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 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 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 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 | <!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);--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;}
*{box-sizing:border-box;margin:0;padding:0;}
::-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;}
body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);height:100vh;overflow:hidden;display:flex;flex-direction:column;}
.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;}
.logo{font-weight:800;font-size:16px;color:var(--accent);display:flex;align-items:center;gap:7px;text-decoration:none;}
nav{display:flex;gap:3px;margin-left:14px;}
.nav-link{color:var(--muted2);text-decoration:none;font-size:12px;font-weight:600;padding:5px 10px;border-radius:7px;transition:.15s;}
.nav-link:hover{color:var(--text);background:var(--surface2);}
.nav-link.active{color:var(--accent);background:var(--accent-dim);}
.main{flex:1;display:flex;overflow:hidden;}
.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;}
.sidebar.collapsed{width:36px!important;min-width:36px;}
.sidebar-inner{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0;}
.sidebar.collapsed .sidebar-inner{display:none;}
.resize-handle{position:absolute;right:-4px;top:0;bottom:0;width:8px;cursor:col-resize;z-index:20;}
.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;}
.resize-handle:hover::after{background:var(--accent);height:60px;}
.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);}
.collapse-btn:hover{color:var(--accent);border-color:var(--accent);}
.collapse-btn svg{width:12px;height:12px;fill:none;stroke:currentColor;stroke-width:2.5;transition:transform .25s;}
.sidebar.collapsed .collapse-btn svg{transform:rotate(180deg);}
.sidebar-tabs{display:flex;border-bottom:1px solid var(--border);flex-shrink:0;}
.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;}
.stab.active{color:var(--accent);border-bottom-color:var(--accent);background:var(--accent-dim);}
.stab:hover:not(.active){color:var(--text);}
.spanel{flex:1;display:flex;flex-direction:column;overflow:hidden;padding:12px;}
.spanel.hidden{display:none;}
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;}
input:focus,select:focus{border-color:var(--accent);}
.find-row{display:flex;gap:7px;margin-bottom:8px;}
.find-row input{flex:1;}
.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;}
.btn-find:hover{opacity:.85;}
.btn-find:disabled{opacity:.5;cursor:not-allowed;}
.search-hint{font-size:10px;color:var(--muted);padding:4px 2px 6px;line-height:1.6;}
.search-results-list{flex:1;overflow-y:auto;display:flex;flex-direction:column;gap:3px;padding-right:2px;min-height:0;}
.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;}
.sr-item:hover{border-color:rgba(59,130,246,.5);background:var(--accent-dim);}
.sr-item.in-queue{border-color:var(--green);opacity:.6;}
.sr-item.dragging-src{opacity:.25;}
.sr-item img{width:28px;height:28px;border-radius:6px;object-fit:cover;flex-shrink:0;}
.sr-info{flex:1;min-width:0;}
.sr-title{font-size:11px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.sr-dev{font-size:9px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.sr-score{font-size:9px;font-weight:700;color:var(--amber);background:var(--amber-dim);padding:1px 5px;border-radius:4px;flex-shrink:0;}
.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;}
.sr-add-btn:hover{background:var(--accent);color:white;}
.sr-item.in-queue .sr-add-btn{background:var(--green-dim);border-color:rgba(34,197,94,.3);color:var(--green);}
.sr-skeleton{display:flex;align-items:center;gap:8px;padding:7px 8px;border-radius:7px;border:1px solid var(--border);}
.sk-icon{width:28px;height:28px;border-radius:6px;background:var(--surface2);animation:shimmer 1.2s infinite;}
.sk-lines{flex:1;display:flex;flex-direction:column;gap:5px;}
.sk-line{height:7px;border-radius:3px;background:var(--surface2);animation:shimmer 1.2s infinite;}
.sk-line.short{width:55%;}
@keyframes shimmer{0%,100%{opacity:.35}50%{opacity:.8}}
.queue-section{display:flex;flex-direction:column;gap:6px;overflow:hidden;flex:1;min-height:0;}
.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;}
.queue-box.drag-active{border-color:var(--accent);background:rgba(59,130,246,.04);}
.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;}
.queue-htitle{font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.7px;color:var(--muted2);}
.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;}
.queue-empty p{font-size:11px;line-height:1.5;}
.queue-list{flex:1;overflow-y:auto;display:flex;flex-direction:column;gap:3px;padding:5px;min-height:0;}
.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;}
.q-item:active{cursor:grabbing;}
.q-item:hover{border-color:rgba(59,130,246,.4);}
.q-item.drag-over{border-color:var(--accent);background:var(--accent-dim);}
.q-item.q-dragging{opacity:.3;}
.q-handle{display:flex;flex-direction:column;gap:2px;color:var(--muted);flex-shrink:0;padding:1px;}
.q-handle span{display:block;width:10px;height:1.5px;background:currentColor;border-radius:2px;}
.q-item img{width:22px;height:22px;border-radius:5px;object-fit:cover;flex-shrink:0;}
.q-info{flex:1;min-width:0;}
.q-title{font-size:10px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.q-score{font-size:9px;color:var(--amber);}
.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;}
.q-rm:hover{color:#ef4444;background:rgba(239,68,68,.1);}
.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;}
.quick-btn:hover{color:white;border-color:var(--accent);}
.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;}
.mode-btn{padding:6px;border-radius:5px;text-align:center;cursor:pointer;font-size:11px;font-weight:700;color:var(--muted);transition:.2s;}
.mode-btn.active{background:var(--surface2);color:white;box-shadow:0 2px 4px rgba(0,0,0,.3);}
.star-filter-grid{display:flex;flex-direction:column;gap:4px;}
.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;}
.star-row:hover{border-color:var(--accent);}
.star-row input[type=checkbox]{width:13px;height:13px;accent-color:var(--accent);cursor:pointer;padding:0;border:none;flex-shrink:0;}
.star-label{display:flex;align-items:center;gap:4px;font-size:11px;font-weight:600;flex:1;}
.stars-on{color:var(--amber);letter-spacing:-1px;}
.stars-off{color:var(--border);letter-spacing:-1px;}
.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;}
.btn-main:hover{transform:translateY(-1px);box-shadow:0 4px 16px var(--accent-glow);}
.btn-main:disabled{opacity:.5;cursor:not-allowed;transform:none;box-shadow:none;}
.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;}
.sec-lbl span{color:var(--accent);font-size:9px;text-transform:none;letter-spacing:0;font-weight:700;}
.igrp{display:flex;flex-direction:column;gap:5px;margin-bottom:8px;}
.divider{height:1px;background:var(--border);margin:7px 0;flex-shrink:0;}
#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;}
.content{flex:1;background:var(--bg);position:relative;display:flex;flex-direction:column;overflow:hidden;min-width:0;}
.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;}
.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;}
.sbox:focus-within{border-color:var(--accent);}
.sbox input{background:transparent;border:none;color:var(--text);font-size:12px;outline:none;width:100%;}
.sbox svg{color:var(--muted);flex-shrink:0;width:12px;height:12px;fill:none;stroke:currentColor;stroke-width:2.5;}
.chips-row{display:flex;gap:4px;flex-wrap:wrap;flex:1;min-width:0;}
.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;}
.a-chip:hover{background:rgba(239,68,68,.1);color:#ef4444;border-color:rgba(239,68,68,.3);}
.tb-right{display:flex;align-items:center;gap:7px;flex-shrink:0;}
.res-count{font-size:11px;color:var(--muted);white-space:nowrap;}
.vs{display:flex;gap:2px;background:var(--bg);padding:3px;border-radius:7px;border:1px solid var(--border);}
.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;}
.vb.active{background:var(--surface2);color:white;}
.vb svg{width:13px;height:13px;fill:none;stroke:currentColor;stroke-width:2;}
.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;}
.btn-exp:hover{border-color:var(--accent);color:var(--text);}
.btn-exp svg{width:12px;height:12px;fill:none;stroke:currentColor;stroke-width:2.5;}
.scroll-view{flex:1;overflow-y:auto;padding:16px 18px;display:flex;flex-direction:column;gap:14px;}
.batch-summary{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:14px 16px;display:flex;flex-direction:column;gap:10px;}
.apps-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(175px,1fr));gap:8px;}
.app-mini-card{background:var(--surface2);border:1px solid var(--border);border-radius:9px;padding:9px 11px;display:flex;align-items:center;gap:9px;}
.app-mini-card img{width:34px;height:34px;border-radius:7px;}
.app-mini-info{flex:1;min-width:0;}
.app-mini-title{font-size:11px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.app-mini-score{font-size:10px;color:var(--amber);margin-top:1px;}
.app-mini-ct{font-size:9px;color:var(--muted);margin-top:1px;}
.table-container{background:var(--surface);border:1px solid var(--border);border-radius:12px;overflow:hidden;}
table{width:100%;border-collapse:collapse;font-size:12px;}
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;}
td{padding:11px 13px;border-bottom:1px solid var(--border);vertical-align:top;}
tr:last-child td{border-bottom:none;}
tr:hover td{background:rgba(255,255,255,.012);}
.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);}
.score-stars{color:var(--amber);white-space:nowrap;letter-spacing:1px;}
.rev-content{color:#cbd5e1;line-height:1.55;max-width:460px;word-wrap:break-word;font-size:12px;}
.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);}
.dev-reply-lbl{font-weight:700;color:var(--green);font-size:9px;text-transform:uppercase;margin-bottom:3px;display:block;}
.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);}
.hpill svg{width:10px;height:10px;fill:none;stroke:var(--accent);stroke-width:2.5;}
.th-wrap{position:relative;}
.th-inner{display:flex;align-items:center;gap:4px;cursor:pointer;user-select:none;white-space:nowrap;}
.sa{font-size:10px;color:var(--accent);}
.fi{width:11px;height:11px;fill:none;stroke:currentColor;stroke-width:2.5;opacity:.3;transition:opacity .15s;flex-shrink:0;}
.fi.on{opacity:1;stroke:var(--accent);}
.th-inner:hover .fi{opacity:.65;}
.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;}
.fdd-search{padding:6px 7px;border-bottom:1px solid var(--border);}
.fdd-search input{padding:5px 9px;font-size:11px;border-radius:6px;}
.fdd-list{max-height:200px;overflow-y:auto;padding:3px;}
.fdd-opt{display:flex;align-items:center;gap:7px;padding:5px 7px;border-radius:5px;cursor:pointer;font-size:11px;transition:.1s;}
.fdd-opt:hover{background:var(--accent-dim);}
.fdd-opt input[type=checkbox]{width:12px;height:12px;accent-color:var(--accent);cursor:pointer;flex-shrink:0;}
.fdd-opt-lbl{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.fdd-opt-ct{font-size:9px;color:var(--muted);flex-shrink:0;}
.fdd-acts{display:flex;gap:5px;padding:7px;border-top:1px solid var(--border);}
.fdd-btn{flex:1;padding:5px;border-radius:6px;border:none;cursor:pointer;font-size:11px;font-weight:700;transition:.15s;}
.fdd-btn.clr{background:var(--surface);color:var(--muted2);}
.fdd-btn.clr:hover{color:white;}
.fdd-btn.apl{background:var(--accent);color:white;}
.cards-view{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:12px;}
.rcb{background:var(--surface);border:1px solid var(--border);border-radius:12px;overflow:hidden;transition:border-color .15s;}
.rcb:hover{border-color:#2d3a4f;}
.rcb-main{padding:13px 15px;}
.rcb-header{display:flex;align-items:center;gap:9px;margin-bottom:8px;}
.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;}
.rcb-avatar img{width:32px;height:32px;border-radius:50%;object-fit:cover;}
.rcb-meta{flex:1;min-width:0;}
.rcb-user{font-weight:700;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.rcb-date{font-size:10px;color:var(--muted);margin-top:1px;}
.rcb-text{font-size:12px;color:#cbd5e1;line-height:1.6;}
.rcb-footer{padding:8px 15px;background:var(--bg);border-top:1px solid var(--border);display:flex;gap:5px;flex-wrap:wrap;align-items:center;}
.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);}
.mpill.app{color:var(--accent);border-color:rgba(59,130,246,.25);background:var(--accent-dim);}
.mpill.replied{color:var(--green);border-color:rgba(34,197,94,.25);background:var(--green-dim);}
.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;}
.rcb-dev-hdr{font-size:9px;font-weight:700;color:var(--green);margin-bottom:4px;}
.rcb-dev-text{font-size:11px;color:var(--muted2);line-height:1.5;}
.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;}
.spinner{width:34px;height:34px;border:3px 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-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);}
#chat-dialer:hover{transform:scale(1.1) rotate(5deg);}
#chat-dialer svg{width:21px;height:21px;color:white;fill:none;stroke:currentColor;stroke-width:2.5;}
#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);}
#chat-window.open{transform:translateY(0) scale(1);opacity:1;pointer-events:auto;}
.chat-header{padding:12px 16px;background:var(--accent);color:white;display:flex;align-items:center;gap:11px;flex-shrink:0;}
.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%;}
.chat-clear-btn{background:rgba(255,255,255,.15);border:none;color:white;font-size:11px;padding:3px 8px;border-radius:6px;cursor:pointer;}
.chat-clear-btn:hover{background:rgba(255,255,255,.25);}
.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;}
.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:9px 13px;border-radius:14px;font-size:12px;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:9px;font-weight:700;font-size:10px;color:var(--accent);letter-spacing:.05em;text-transform:uppercase;}
.msg-item{display:flex;gap:7px;margin-top:4px;}.msg-item-num,.msg-bullet{font-weight:700;color:var(--accent);min-width:14px;}
.chat-table-wrap{max-width:100%;overflow-x:auto;border:1px solid var(--border);border-radius:9px;background:var(--surface2);margin-top:4px;}
.chat-table-title{padding:6px 10px;font-size:10px;font-weight:700;color:var(--accent);border-bottom:1px solid var(--border);text-transform:uppercase;}
.chat-table{width:100%;border-collapse:collapse;font-size:11px;}
.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;}
.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;}
.chat-table tr:last-child td{border-bottom:none;}.chat-table tr:hover td{background:var(--surface);}
.typing-indicator{display:flex;gap:4px;padding:9px 13px;background:var(--surface2);border:1px solid var(--border);border-radius:13px;width:fit-content;}
.dot{width:5px;height:5px;background:var(--muted);border-radius:50%;animation:bounce 1.4s infinite;}
.dot:nth-child(2){animation-delay:.2s;}.dot:nth-child(3){animation-delay:.4s;}
@keyframes bounce{0%,80%,100%{transform:translateY(0)}40%{transform:translateY(-5px)}}
.chat-input-area{padding:11px 13px;background:var(--surface);border-top:1px solid var(--border);display:flex;gap:7px;flex-shrink:0;}
#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;}
#chat-input:focus{border-color:var(--accent);}
.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;}
.btn-send:hover{transform:scale(1.05);}.btn-send svg{width:15px;height:15px;fill:none;stroke:currentColor;stroke-width:2.5;}
.chat-suggestions{display:flex;flex-wrap:wrap;gap:4px;padding:0 13px 7px;}
.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;}
.sug-chip:hover{border-color:var(--accent);color:var(--accent);}
</style>
</head>
<body>
<div id="rubber-band"></div>
<div class="header">
<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>
<nav>
<a href="/" class="nav-link">Home</a>
<a href="/scraper" class="nav-link">Single Explorer</a>
<a href="/batch" class="nav-link active">Batch Intelligence</a>
</nav>
<div style="flex:1"></div>
</div>
<div class="main">
<aside class="sidebar" id="sidebarEl">
<div class="sidebar-inner">
<div class="sidebar-tabs">
<div class="stab active" id="tabFind" onclick="switchTab('find')">🔍 Find Apps</div>
<div class="stab" id="tabQueue" onclick="switchTab('queue')">⚙️ Queue & Run</div>
</div>
<!-- FIND PANEL -->
<div class="spanel" id="panelFind" style="gap:0">
<div style="font-size:10px;color:var(--muted2);font-weight:600;padding:0 0 7px">Search & drag apps to the Queue tab →</div>
<div class="find-row">
<input type="text" id="query" placeholder="Search games, apps…" value="Multiplayer Games" onkeydown="if(event.key==='Enter')findApps()">
<button onclick="findApps()" id="btnFind" class="btn-find">Find</button>
</div>
<div style="display:flex;gap:6px;align-items:center;margin-bottom:8px;">
<span style="font-size:10px;color:var(--muted);white-space:nowrap">Max results</span>
<input type="number" id="app_count" value="10" min="1" max="50" style="width:60px;padding:5px 8px;font-size:11px;">
</div>
<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>
<div class="search-results-list" id="searchResultsList"></div>
</div>
<!-- QUEUE + SETTINGS PANEL -->
<div class="spanel hidden" id="panelQueue" style="gap:0;overflow-y:auto;">
<div class="sec-lbl">Batch Queue <span id="queueCountLbl">0 apps</span></div>
<div class="queue-box" id="queueBox" ondragover="qBoxOver(event)" ondrop="qBoxDrop(event)" ondragleave="qBoxLeave(event)">
<div class="queue-hdr">
<span class="queue-htitle">Selected Apps — drag to reorder</span>
<button class="quick-btn" onclick="clearQueue()">Clear all</button>
</div>
<div class="queue-empty" id="queueEmpty">
<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>
<p>Drag apps from 🔍 Find Apps<br>or click <strong style="color:var(--accent)">+</strong> to add</p>
</div>
<div class="queue-list hidden" id="queueList"></div>
</div>
<div class="divider"></div>
<div class="sec-lbl">Scrape Settings</div>
<div class="igrp">
<div style="font-size:10px;color:var(--muted2);font-weight:600;">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="igrp">
<div style="font-size:10px;color:var(--muted2);font-weight:600;margin-bottom:4px">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="igrp">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:5px;">
<span style="font-size:10px;color:var(--muted2);font-weight:600;">Star Filter</span>
<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>
</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> 5</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> 4</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> 3</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> 2</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> 1</span></label>
</div>
</div>
<button class="btn-main" id="go" onclick="runBatch()" disabled>
<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>
RUN BATCH ANALYSIS
</button>
</div>
</div>
<div class="resize-handle" id="resizeHandle"></div>
<div class="collapse-btn" id="collapseBtn" onclick="toggleSidebar()">
<svg viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg>
</div>
</aside>
<div class="content">
<div class="toolbar" id="toolbarEl" style="display:none">
<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>
<div class="chips-row" id="activeFiltersRow"></div>
<div class="tb-right">
<span class="res-count" id="resultStats"></span>
<div class="vs">
<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>
<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>
</div>
<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>
</div>
</div>
<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:14px;text-align:center;padding:40px;">
<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>
<div><p style="font-size:15px;font-weight:700;color:var(--muted2);margin-bottom:6px">No batch data yet</p>
<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>
</div>
<div id="results" class="hidden" style="display:flex;flex-direction:column;gap:14px;"></div>
</div>
<div id="loader" class="loader-overlay hidden">
<div class="spinner"></div>
<p style="color:var(--muted);font-size:13px" id="loaderMsg">Running batch analysis…</p>
</div>
</div>
</div>
<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="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>
<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 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>
</div>
<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>
<div class="chat-suggestions">
<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 common issues?')">Common issues</div>
</div>
<div class="chat-input-area">
<input type="text" id="chat-input" placeholder="Ask about 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>
// STATE
let currentData=null,currentMode='fixed',viewMode='table';
let searchResults=[],queue=[];
let colFilters={},sortCol=null,sortDir=1,openDd=null;
let draggedApp=null,qDragSrc=null;
// SIDEBAR TABS
function switchTab(t){
document.getElementById('tabFind').classList.toggle('active',t==='find');
document.getElementById('tabQueue').classList.toggle('active',t==='queue');
document.getElementById('panelFind').classList.toggle('hidden',t!=='find');
document.getElementById('panelQueue').classList.toggle('hidden',t!=='queue');
}
// SIDEBAR COLLAPSE + RESIZE
let sbCollapsed=false;
function toggleSidebar(){
sbCollapsed=!sbCollapsed;
const sb=document.getElementById('sidebarEl');
sb.classList.toggle('collapsed',sbCollapsed);
}
const rh=document.getElementById('resizeHandle'),sb=document.getElementById('sidebarEl');
let resizing=false,rsX=0,rsW=0;
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();});
document.addEventListener('mousemove',e=>{if(!resizing)return;const nw=Math.max(220,Math.min(500,rsW+(e.clientX-rsX)));sb.style.width=nw+'px';});
document.addEventListener('mouseup',()=>{if(resizing){resizing=false;document.body.style.cursor='';document.body.style.userSelect='';}});
// FIND APPS
async function findApps(){
const q=document.getElementById('query').value.trim();if(!q)return;
const btn=document.getElementById('btnFind');btn.disabled=true;btn.textContent='...';
const hint=document.getElementById('searchHint');
hint.innerHTML='Searching...';hint.style.display='block';
const list=document.getElementById('searchResultsList');
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('');
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');
searchResults=data.results;hint.style.display='none';renderSearchResults();
}catch(e){list.innerHTML='';hint.textContent='Error: '+e.message;}
finally{btn.disabled=false;btn.textContent='Find';}
}
function renderSearchResults(){
const list=document.getElementById('searchResultsList');
if(!searchResults.length){list.innerHTML='<div style="text-align:center;padding:20px;color:var(--muted);font-size:11px">No results found</div>';return;}
const qIds=new Set(queue.map(a=>a.appId));
list.innerHTML=searchResults.map((a,i)=>{
const inQ=qIds.has(a.appId);
return `<div class="sr-item${inQ?' in-queue':''}" data-idx="${i}" draggable="true" ondragstart="srDragStart(event,${i})" ondragend="srDragEnd(event)">
<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'">
<div class="sr-info"><div class="sr-title">${escH(a.title)}</div><div class="sr-dev">${escH(a.developer||'')}</div></div>
${a.score>0?`<div class="sr-score">${a.score}☆</div>`:''}
<div class="sr-add-btn" onclick="toggleInQueue(${i})" title="${inQ?'Remove from queue':'Add to queue'}">${inQ?'✓':'+'}</div>
</div>`;
}).join('');
}
// DRAG FROM SEARCH LIST
function srDragStart(e,idx){draggedApp=searchResults[idx];e.dataTransfer.effectAllowed='copy';requestAnimationFrame(()=>e.target.classList.add('dragging-src'));}
function srDragEnd(e){draggedApp=null;document.querySelectorAll('.sr-item').forEach(el=>el.classList.remove('dragging-src'));document.getElementById('queueBox').classList.remove('drag-active');}
// QUEUE DROP ZONE
function qBoxOver(e){e.preventDefault();e.dataTransfer.dropEffect='copy';document.getElementById('queueBox').classList.add('drag-active');}
function qBoxLeave(e){if(!e.currentTarget.contains(e.relatedTarget))document.getElementById('queueBox').classList.remove('drag-active');}
function qBoxDrop(e){
e.preventDefault();document.getElementById('queueBox').classList.remove('drag-active');
if(draggedApp){addToQueue(draggedApp);draggedApp=null;}
}
function addToQueue(app){
if(queue.find(a=>a.appId===app.appId))return;
queue.push({appId:app.appId,title:app.title,icon:app.icon,score:app.score,developer:app.developer});
renderQueue();renderSearchResults();updateRunBtn();
}
function removeFromQueue(appId){queue=queue.filter(a=>a.appId!==appId);renderQueue();renderSearchResults();updateRunBtn();}
function clearQueue(){queue=[];renderQueue();renderSearchResults();updateRunBtn();}
function toggleInQueue(idx){
const app=searchResults[idx];
if(queue.find(a=>a.appId===app.appId))removeFromQueue(app.appId);else addToQueue(app);
}
// QUEUE REORDER
function qiDragStart(e,i){qDragSrc=i;e.dataTransfer.effectAllowed='move';requestAnimationFrame(()=>document.querySelectorAll('.q-item')[i]?.classList.add('q-dragging'));}
function qiDragOver(e,i){e.preventDefault();document.querySelectorAll('.q-item').forEach((el,j)=>el.classList.toggle('drag-over',j===i&&j!==qDragSrc));}
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();}
function qiDragEnd(){qDragSrc=null;document.querySelectorAll('.q-item').forEach(el=>el.classList.remove('q-dragging','drag-over'));}
function renderQueue(){
const empty=document.getElementById('queueEmpty'),list=document.getElementById('queueList');
const ct=document.getElementById('queueCountLbl');
ct.textContent=queue.length?`${queue.length} app${queue.length>1?'s':''}`:'0 apps';
if(!queue.length){empty.classList.remove('hidden');list.classList.add('hidden');return;}
empty.classList.add('hidden');list.classList.remove('hidden');
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()">
<div class="q-handle"><span></span><span></span><span></span></div>
<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'">
<div class="q-info"><div class="q-title">${escH(a.title)}</div>${a.score>0?`<div class="q-score">${a.score}☆</div>`:''}</div>
<button class="q-rm" onclick="removeFromQueue('${a.appId}')" title="Remove">×</button>
</div>`).join('');
}
function updateRunBtn(){document.getElementById('go').disabled=queue.length===0;}
// SCRAPE SETTINGS
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(c){document.querySelectorAll('.star-cb').forEach(cb=>cb.checked=c);}
// RUN BATCH
async function runBatch(){
if(!queue.length)return alert('Add at least one app to the queue');
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;
colFilters={};sortCol=null;
const msgs=['Connecting...','Fetching app data...','Scraping reviews...','Compiling results...'];
let mi=0;document.getElementById('loaderMsg').textContent=msgs[0];
const msgInt=setInterval(()=>{mi=(mi+1)%msgs.length;document.getElementById('loaderMsg').textContent=msgs[mi];},2000);
try{
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})});
const data=await res.json();if(!res.ok)throw new Error(data.error||'Batch failed');
currentData=data;document.getElementById('toolbarEl').style.display='flex';render(data);
}catch(e){document.getElementById('welcome').classList.remove('hidden');alert(e.message);}
finally{clearInterval(msgInt);document.getElementById('loader').classList.add('hidden');document.getElementById('go').disabled=queue.length===0;}
}
// VIEW MODE
function switchViewMode(m){viewMode=m;document.getElementById('vbtnTable').classList.toggle('active',m==='table');document.getElementById('vbtnCards').classList.toggle('active',m==='cards');if(currentData)applyAllFilters();}
// FILTER ENGINE
function filtered(){
if(!currentData)return[];
let rv=[...currentData.reviews];
const q=(document.getElementById('globalSearch')?.value||'').toLowerCase();
if(q)rv=rv.filter(r=>(r.content||'').toLowerCase().includes(q)||(r.userName||'').toLowerCase().includes(q));
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||'');});
if(colFilters.score?.size)rv=rv.filter(r=>colFilters.score.has(String(r.score)));
if(colFilters.replied?.size)rv=rv.filter(r=>colFilters.replied.has(r.replyContent?.trim()?'Yes':'No'));
if(colFilters.date?.size)rv=rv.filter(r=>colFilters.date.has(r.at?String(new Date(r.at).getFullYear()):'Unknown'));
if(sortCol)rv.sort((a,b)=>{let va,vb;
if(sortCol==='score'){va=a.score||0;vb=b.score||0;}
else if(sortCol==='date'){va=a.at||'';vb=b.at||'';}
else if(sortCol==='helpful'){va=a.thumbsUpCount||0;vb=b.thumbsUpCount||0;}
else if(sortCol==='app'){va=(currentData.apps.find(x=>x.appId===a.appId)||{}).title||'';vb=(currentData.apps.find(x=>x.appId===b.appId)||{}).title||'';}
return va<vb?-sortDir:va>vb?sortDir:0;
});
return rv;
}
function applyAllFilters(){if(!currentData)return;renderResults(currentData,filtered());renderChips();}
function renderChips(){
const row=document.getElementById('activeFiltersRow');
row.innerHTML=Object.entries(colFilters).filter(([,v])=>v?.size).map(([col,vals])=>{
const vStr=[...vals].slice(0,2).join(', ')+(vals.size>2?` +${vals.size-2}`:'');
return `<div class="a-chip" onclick="clearF('${col}')" title="Remove">${col.charAt(0).toUpperCase()+col.slice(1)}: ${escH(vStr)} ✕</div>`;
}).join('');
}
function clearF(col){delete colFilters[col];applyAllFilters();}
// COLUMN FILTER DROPDOWN
function openColFilter(colKey,triggerEl){
if(openDd){openDd.remove();openDd=null;}
if(!currentData)return;
const rv=currentData.reviews;const counts={};
rv.forEach(r=>{let v;
if(colKey==='app'){const a=currentData.apps.find(x=>x.appId===r.appId);v=a?.title||r.appTitle||'Unknown';}
else if(colKey==='score')v=String(r.score);
else if(colKey==='replied')v=r.replyContent?.trim()?'Yes':'No';
else if(colKey==='date')v=r.at?String(new Date(r.at).getFullYear()):'Unknown';
if(v!==undefined)counts[v]=(counts[v]||0)+1;
});
const allVals=Object.keys(counts).sort();
const tempSel=new Set(colFilters[colKey]||allVals);
const dd=document.createElement('div');dd.className='filter-dd';
triggerEl.closest('.th-wrap').appendChild(dd);openDd=dd;
function buildDd(q){
const show=allVals.filter(v=>v.toLowerCase().includes(q));
dd.innerHTML=`<div class="fdd-search"><input type="text" placeholder="Search..." id="fddQ_${colKey}" value="${escH(q)}" oninput="rebuildFdd('${colKey}',this.value)"></div>
<div class="fdd-list">
<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>
${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('')}
</div>
<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>`;
}
window['_ts_'+colKey]=tempSel;window['_av_'+colKey]=allVals;
window.rebuildFdd=(k,q)=>buildDd(q);
window.fddToggleVal=(k,v,q)=>{window['_ts_'+k].has(v)?window['_ts_'+k].delete(v):window['_ts_'+k].add(v);buildDd(q);};
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);};
window.fddClear=(k)=>{delete colFilters[k];dd.remove();openDd=null;applyAllFilters();};
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();};
buildDd('');
}
document.addEventListener('click',e=>{if(openDd&&!openDd.contains(e.target)&&!e.target.closest('.th-inner')){openDd.remove();openDd=null;}});
function thClick(key,e){
const svgEl=e.target.tagName==='svg'||e.target.tagName==='polyline'||e.target.tagName==='path'||e.target.tagName==='polygon'||!!e.target.closest('svg');
if(svgEl)openColFilter(key,e.currentTarget);
else{if(sortCol===key)sortDir=-sortDir;else{sortCol=key;sortDir=1;}applyAllFilters();}
}
// RENDER
function render(data){document.getElementById('welcome').classList.add('hidden');document.getElementById('results').classList.remove('hidden');applyAllFilters();}
function renderResults(data,rv){
document.getElementById('resultStats').textContent=`${rv.length.toLocaleString()} reviews`;
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>`;
const bodyHTML=viewMode==='table'?renderTable(data,rv):renderCards(data,rv);
document.getElementById('results').innerHTML=summaryHTML+bodyHTML;
}
function sa(k){return sortCol===k?(sortDir===1?'↑':'↓'):''}
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>`}
function renderTable(data,rv){
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'}];
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('');
const rows=rv.map(r=>{
const app=data.apps.find(a=>a.appId===r.appId)||{title:r.appTitle||''};
const hasReply=!!(r.replyContent?.trim());
const stars='★'.repeat(r.score)+`<span style="color:var(--border)">${'★'.repeat(5-r.score)}</span>`;
const replyHtml=hasReply?`<div class="dev-reply-cell"><span class="dev-reply-lbl">Dev Reply</span>${escH(r.replyContent)}</div>`:'';
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>`;
}).join('');
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>`;
}
function renderCards(data,rv){
const cards=rv.map(r=>{
const app=data.apps.find(a=>a.appId===r.appId)||{title:r.appTitle||''};
const hasReply=!!(r.replyContent?.trim());
const initials=(r.userName||'?').trim().split(/\s+/).map(w=>w[0]).join('').slice(0,2).toUpperCase();
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>`;
const stars=[...Array(5)].map((_,i)=>`<span style="font-size:12px;color:${i<r.score?'var(--amber)':'var(--border)'}">★</span>`).join('');
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>`:'';
const thumb=r.thumbsUpCount?`<span class="mpill">👍 ${r.thumbsUpCount}</span>`:'';
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>`;
}).join('');
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>`;
}
// UTILS
function fmtDate(iso){if(!iso)return'';return new Date(iso).toLocaleDateString('en-US',{year:'numeric',month:'short',day:'numeric'});}
function escH(s){return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');}
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();URL.revokeObjectURL(a.href);
}
// CHAT
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!</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'),msg=input.value.trim();if(!msg)return;
appendUserMsg(msg);input.value='';
const c=document.getElementById('chat-messages');
const t=document.createElement('div');t.className='typing-indicator';t.innerHTML='<div class="dot"></div><div class="dot"></div><div class="dot"></div>';
c.appendChild(t);c.scrollTop=c.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?.reviews||[]})});
const data=await res.json();if(t.parentNode)c.removeChild(t);
if(data.error){appendBotMsg('⚠️ '+data.error,null);return;}
appendBotMsg(data.reply||'',data.table||null);
if(data.type==='filter'&&data.filters)chatFilter(data.filters);
}catch(e){if(t.parentNode)c.removeChild(t);appendBotMsg('Connection error.',null);}
}
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;}
function appendBotMsg(text,table){
const c=document.getElementById('chat-messages'),row=document.createElement('div');row.className='msg-row bot';
if(text?.trim()){const b=document.createElement('div');b.className='message bot';b.innerHTML=renderMD(text);row.appendChild(b);}
if(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">${escH(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>${inlineF(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>${inlineF(bm[1])}</span></div>`;continue;}
if(inList&&!raw.trim()){html+='</div>';inList=false;}
html+=raw.trim()?`<span>${inlineF(raw)}</span><br>`:'<br>';
}
if(inList)html+='</div>';return html;
}
function inlineF(t){return escH(t).replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>').replace(/_(.+?)_/g,'<em style="color:var(--muted2)">$1</em>');}
function buildTable(td){
const{title,columns,rows}=td,w=document.createElement('div');w.className='chat-table-wrap';
let h='';if(title)h+=`<div class="chat-table-title">${escH(title)}</div>`;
h+='<table class="chat-table"><thead><tr>';for(const c of columns)h+=`<th>${escH(c)}</th>`;h+='</tr></thead><tbody>';
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>';}
h+='</tbody></table>';w.innerHTML=h;return w;
}
function chatFilter(raw){
if(!currentData)return;
try{
const f=typeof raw==='string'?JSON.parse(raw):raw;let rv=currentData.reviews;
if(f.stars?.length)rv=rv.filter(r=>f.stars.includes(r.score));
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);});}
if(f.query){const q=f.query.toLowerCase();rv=rv.filter(r=>(r.content||'').toLowerCase().includes(q)||(r.userName||'').toLowerCase().includes(q));}
renderResults(currentData,rv);
}catch(e){console.error(e);}
}
</script>
</body>
</html> |