Play-Scrapper / templates /batch.html
WebashalarForML's picture
Upload 2 files
88b4fa3 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
<title>Batch Intelligence | PlayPulse</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet"/>
<style>
:root{--bg:#0b0e14;--surface:#151921;--surface2:#1c2333;--border:#232a35;--accent:#3b82f6;--accent-dim:rgba(59,130,246,0.12);--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')">&#128269; Find Apps</div>
<div class="stab" id="tabQueue" onclick="switchTab('queue')">&#9881;&#65039; Queue &amp; 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 &amp; drag apps to the Queue tab &#8594;</div>
<div class="find-row">
<input type="text" id="query" placeholder="Search games, apps&#8230;" 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 &#8212; 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 &#128269; 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">&#9733;&#9733;&#9733;&#9733;&#9733;</span>&nbsp;5</span></label>
<label class="star-row"><input type="checkbox" class="star-cb" value="4" checked><span class="star-label"><span class="stars-on">&#9733;&#9733;&#9733;&#9733;</span><span class="stars-off">&#9733;</span>&nbsp;4</span></label>
<label class="star-row"><input type="checkbox" class="star-cb" value="3" checked><span class="star-label"><span class="stars-on">&#9733;&#9733;&#9733;</span><span class="stars-off">&#9733;&#9733;</span>&nbsp;3</span></label>
<label class="star-row"><input type="checkbox" class="star-cb" value="2" checked><span class="star-label"><span class="stars-on">&#9733;&#9733;</span><span class="stars-off">&#9733;&#9733;&#9733;</span>&nbsp;2</span></label>
<label class="star-row"><input type="checkbox" class="star-cb" value="1" checked><span class="star-label"><span class="stars-on">&#9733;</span><span class="stars-off">&#9733;&#9733;&#9733;&#9733;</span>&nbsp;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&#8230;" 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&#8230;<br>2. Click <strong style="color:var(--accent)">+</strong> or drag into the queue<br>3. Switch to <strong style="color:var(--text)">Queue &amp; 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&#8230;</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">&#128075; Hi! Search apps, build your queue, run batch &#8212; 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&#9733; 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&#8230;" 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}&star;</div>`:''}
<div class="sr-add-btn" onclick="toggleInQueue(${i})" title="${inQ?'Remove from queue':'Add to queue'}">${inQ?'&#10003;':'+'}</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}&star;</div>`:''}</div>
<button class="q-rm" onclick="removeFromQueue('${a.appId}')" title="Remove">&times;</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)} &#10005;</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)} &#9733;</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?'&#8593;':'&#8595;'):''}
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='&#9733;'.repeat(r.score)+`<span style="color:var(--border)">${'&#9733;'.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">&#10003;</span>':'<span style="color:var(--muted);font-size:11px">&#8212;</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)'}">&#9733;</span>`).join('');
const replyHtml=hasReply?`<div class="rcb-dev"><div class="rcb-dev-hdr">&#128154; Dev Response</div><div class="rcb-dev-text">${escH(r.replyContent)}</div></div>`:'';
const thumb=r.thumbsUpCount?`<span class="mpill">&#128077; ${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">&#128172; 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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}
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>