WebashalarForML commited on
Commit
88b4fa3
·
verified ·
1 Parent(s): cbf6fc5

Upload 2 files

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