Instrukcja użycia→ Ściągnij wtyczkę Tampermonkey do przeglądarki
→ W opcjach wtyczki włącz opcję „Allow User Scripts” (dzięki temu będzie można korzystać z własnych skryptów)
→ Wklej w Tampermonkey poniższy skrypt widgetu
→ Wejdź do Microsoft Clarity, do sekcji nagrań, i pobieraj opisy
Skrypt widgeta
// ==UserScript==
// @name Clarity Insighter
// @namespace https://pawelpiekarski.pl/
// @version 3.6
// @description Widget do generowania opisów sesji w MS Clarity — trwały schowek opisów per ID nagrania (bez ponownego generowania, także po odświeżeniu), „Doładuj listę” generuje wszystko czego brakuje, filtry (ID nagrań, kraj, urządzenie, przeglądarka, referrer, czas/kliki/strony, URL), kopiowanie prompta (ICE + zasięg → ⭐) — opcjonalnie tylko nagrania z załadowanej listy
// @author Paweł Piekarski
// @match https://clarity.microsoft.com/*
// @icon https://clarity.microsoft.com/blog/wp-content/uploads/2025/02/siteIcon.png
// @grant none
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
const WID = 'clarity-helper';
const BLAD_AI_TEKST = "I'm sorry, something went wrong";
const LS_OPTS = 'clarityHelper.ustawienia5';
const LS_POS = 'clarityHelper.pos';
const LS_SCHOWEK = 'clarityHelper.schowek.'; // + ID projektu Clarity
const URZADZENIA = ['mobile', 'desktop', 'tablet'];
const MULTI = ['kraj', 'przegladarka', 'urzadzenie', 'referrer'];
const MAX_PROB = 3; // ile razy klikać „Generate” dla jednego nagrania (gdy AI zwraca błąd)
const PRZERWA_PO_BLEDZIE = 5000;
const OCZEKIWANIE_MS = 120000; // kliknięte nagranie bez odpowiedzi — po tym czasie wolno kliknąć ponownie
const PROMPT_WERSJA = 2; // podbicie wersji jednorazowo podmienia zapisany prompt na nowy domyślny
const DOMYSLNY_PROMPT = `## ROLA
Jesteś senior analitykiem UX/CRO. Oceniasz nagrania sesji z Microsoft Clarity i zamieniasz je w priorytetyzowaną listę hipotez do wdrożenia/testów. Opierasz się wyłącznie na danych z INPUT — niczego nie dopowiadasz ani nie zmyślasz.
## DANE
INPUT to tablica JSON. Każdy obiekt = jedno nagranie:
- userId — ID nagrania (to ono trafia do kolumny „ID nagrań”)
- description — opis sesji wygenerowany przez Clarity (główne źródło obserwacji)
- entry, exit, referrer, device, browser, country, duration, clicks, pages — kontekst sesji
N = liczba obiektów w tablicy.
## ZADANIE
1. Przeczytaj wszystkie opisy i pogrupuj nagrania we wzorce zachowań / problemy. Jedno nagranie może należeć do kilku wzorców. Pomiń zachowania neutralne, z których nie wynika żaden wniosek biznesowy.
2. Każdy wzorzec oceń w 4 wymiarach (tylko do obliczeń — nie pokazuj ich w tabeli):
- Impact 1–3 — wpływ na konwersję/przychód po usunięciu problemu (3 = blokuje zakup/lead),
- Confidence 1–3 — na ile opisy jednoznacznie potwierdzają problem (3 = powtarzalny i jasno opisany),
- Ease 1–3 — łatwość wdrożenia (3 = drobna zmiana treści/UI, 1 = przebudowa procesu lub integracji),
- Zasięg 1–3 — liczba nagrań: 1 = 1 nagranie, 2 = 2–3 nagrania, 3 = co najmniej 4 nagrania lub ≥ 20% N.
3. Punkty = Impact + Confidence + Ease + Zasięg (zakres 4–12) → Score:
- ⭐⭐⭐ = 10–12 pkt (zrobić w pierwszej kolejności)
- ⭐⭐ = 7–9 pkt
- ⭐ = 4–6 pkt
4. Posortuj wiersze: Score malejąco, przy równym Score — więcej nagrań wyżej.
## FORMAT ODPOWIEDZI
Zwróć WYŁĄCZNIE tabelę Markdown — bez wstępu, legendy i podsumowania — z kolumnami:
| Obserwacja | Hipoteza (Rekomendacja) | Score | ID nagrań |
- Obserwacja — co konkretnie robią użytkownicy (1–2 zdania, z kontekstem strony/urządzenia, jeśli istotny); na końcu liczba nagrań w nawiasie, np. (4 nagr.).
- Hipoteza (Rekomendacja) — w formie: „Jeśli [zmiana], to [oczekiwany efekt], ponieważ [co widać w nagraniach].”
- Score — wyłącznie gwiazdki: ⭐⭐⭐, ⭐⭐ albo ⭐.
- ID nagrań — WSZYSTKIE userId przypisane do wzorca, dokładnie tak jak w INPUT, oddzielone przecinkiem i spacją, np.: 1r41zk2, 14n3xyi, 724ia7. Bez cudzysłowów, nawiasów, formatowania kodu i skrótów typu „…” — ta kolumna jest kopiowana 1:1 do filtra, więc nie pomijaj żadnego ID.
## PRZYKŁAD ODPOWIEDZI
| Obserwacja | Hipoteza (Rekomendacja) | Score | ID nagrań |
|---|---|---|---|
| Porzucenie checkoutu na etapie Dostawa i Płatność mimo wypełnionych danych (4 nagr.) | Jeśli uprościmy formularz i dodamy pasek postępu, to więcej osób dokończy zamówienie, ponieważ użytkownicy przewijają formularz w górę i w dół, po czym wychodzą. | ⭐⭐⭐ | 4fyolj, cctsli, 1hzq5lm, 14n3xyi |
| Błędy JS przy wyborze wariantu i galerii blokują dodanie do koszyka (3 nagr.) | Jeśli naprawimy skrypty wariantów i galerii, to wzrośnie liczba dodań do koszyka, ponieważ kliknięcia w warianty nie dają reakcji, a użytkownicy klikają wielokrotnie. | ⭐⭐⭐ | 1r41zk2, 14n3xyi, 724ia7 |
| Wahanie przy wyborze rozmiaru — częste przełączanie wariantów (2 nagr.) | Jeśli umieścimy tabelę rozmiarów bezpośrednio przy wyborze wariantu, to skróci się czas decyzji, ponieważ użytkownicy przełączają rozmiary bez dodania do koszyka. | ⭐⭐ | 13amh0c, 1hzq5lm |
| Długie oczekiwanie przy logowaniu przez Google (1 nagr.) | Jeśli skrócimy czas autoryzacji Google, to mniej osób porzuci logowanie, ponieważ użytkownik czekał na ekranie logowania, po czym opuścił stronę. | ⭐ | 1ff24f8 |
## INPUT
`;
/* ═══════════════════ THEME ═══════════════════ */
const C = {
bg: '#111827', hover: '#1e293b', input: '#1f2937',
accent: '#3b82f6', accentDk: '#1e3a5f',
green: '#22c55e', red: '#ef4444', orange: '#f97316',
text: '#e5e7eb', dim: '#6b7280', border: '#374151',
};
const escHtml = s => String(s ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
/* ═══════════════════ STAN ═══════════════════ */
const stan = {
aktywny: false, stop: false, przewijanie: false, watcherId: null,
dodane: 0, // ile opisów trafiło do schowka w bieżącym przebiegu
proby: new Map(), // id → liczba kliknięć „Generate” w bieżącym przebiegu
oczekujace: new Map(), // id → czas kliknięcia (czeka na odpowiedź AI)
sygnaturaSelectow: ''
};
let widget = null;
let collapsed = false;
// Domyślnie brak filtrów — widoczne wszystkie nagrania
const DOMYSLNE = {
minCzas: 0, minKlik: 0, minStron: 0, ids: '',
kraj: [], przegladarka: [], urzadzenie: [], referrer: [], url: '', exitUrl: '',
ukrywajKarty: true, tylkoZListy: true, collapsed: false, promptOpen: false,
prompt: DOMYSLNY_PROMPT, promptWersja: PROMPT_WERSJA
};
let u = wczytajUstawienia();
function wczytajUstawienia() {
try {
const zapisane = localStorage.getItem(LS_OPTS);
const surowe = JSON.parse(zapisane || '{}');
const z = { ...DOMYSLNE, ...surowe };
delete z.limit; // usunięte w 3.5 — zawsze generujemy wszystko
for (const k of MULTI) if (!Array.isArray(z[k])) z[k] = z[k] ? [z[k]] : [];
if (typeof z.ids !== 'string') z.ids = '';
// nowa wersja prompta → jednorazowa podmiana zapisanego prompta
if (surowe.promptWersja !== PROMPT_WERSJA || !z.prompt || !z.prompt.trim()) {
z.prompt = DOMYSLNY_PROMPT;
z.promptWersja = PROMPT_WERSJA;
}
return z;
} catch { return { ...DOMYSLNE }; }
}
function zapiszUstawienia() {
try { localStorage.setItem(LS_OPTS, JSON.stringify(u)); } catch { /* ignoruj */ }
}
/* ═══════════════════ WYDOBYWANIE DANYCH Z KART ═══════════════════ */
const pobierzKarty = () => Array.from(document.querySelectorAll('div[data-testid="sessionCard"]'));
const maOpis = card => !!card.querySelector('div.insightsContent');
function czyBladAI(card) {
const el = card.querySelector('div.insightsContent');
return !!el && el.textContent.includes(BLAD_AI_TEKST);
}
const maPoprawnyOpis = card => maOpis(card) && !czyBladAI(card);
const przyciskGeneruj = card => card.querySelector('button[data-testid="sessionGenerateInsightsButton"]');
const tekstOpisu = card => card.querySelector('div.insightsContent')?.textContent.trim() || '';
function pobierzDostepneNagrania() {
const el = document.querySelector('.sessionCount strong');
return el ? el.textContent.replace(/\D/g, '') : '…';
}
function parsujCzas(str) {
if (!str) return 0;
const p = str.trim().split(':').map(n => parseInt(n, 10) || 0);
if (p.length === 3) return p[0] * 3600 + p[1] * 60 + p[2];
if (p.length === 2) return p[0] * 60 + p[1];
return p[0] || 0;
}
function sciezkaUrl(url) {
if (!url) return '';
try { return new URL(url).pathname; } catch { return url.split('?')[0].split('#')[0]; }
}
const BEZPOSREDNIE = '(bezpośrednie)';
function hostReferrera(url) {
if (!url || !url.trim()) return BEZPOSREDNIE;
try { return new URL(url).hostname.replace(/^www\./, ''); }
catch { return url.replace(/^https?:\/\//, '').replace(/^www\./, '').split('/')[0] || BEZPOSREDNIE; }
}
function usunParametryUrl(url) {
if (!url) return url;
try { const p = new URL(url); return p.origin + p.pathname; }
catch { return url.split('?')[0].split('#')[0]; }
}
const metaCache = new WeakMap();
function pobierzMetadaneSesji(card) {
const cached = metaCache.get(card);
if (cached && cached._opisFlag === maOpis(card)) return cached;
const meta = { entry: '', exit: '', referrer: '', duration: '', clicks: '', pages: '', date: '',
userId: '', country: '', browser: '', device: '', other: [] };
const rows = Array.from(card.querySelectorAll('div[class*="recordings_cardRow"]'));
rows.forEach(row => {
const strong = row.querySelector('strong');
const span = row.querySelector('span[class*="oneLineText"]');
if (!strong || !span) return;
const label = strong.textContent.trim().replace(':', '');
if (label === 'Entry') meta.entry = row.getAttribute('title') || span.textContent.trim();
if (label === 'Exit') meta.exit = row.getAttribute('title') || span.textContent.trim();
});
const refRow = rows.find(r => r.querySelector('strong')?.textContent.includes('Referrer'));
if (refRow) {
const zTitle = refRow.hasAttribute('title') ? refRow : refRow.querySelector('[title]');
meta.referrer = zTitle?.getAttribute('title') || refRow.querySelector('span')?.textContent.trim() || '';
}
const multiRow = card.querySelector('div[class*="recordings_multiValuedRow"]');
if (multiRow) {
const strongs = multiRow.querySelectorAll('strong');
const spans = multiRow.querySelectorAll('span');
strongs.forEach((s, i) => {
const label = s.textContent.trim().replace(':', '').toLowerCase();
const val = spans[i] ? spans[i].textContent.trim() : '';
if (label === 'duration') meta.duration = val;
if (label === 'clicks') meta.clicks = val;
if (label === 'pages') meta.pages = val;
});
}
meta.durationSec = parsujCzas(meta.duration);
meta.clicksNum = parseInt(meta.clicks, 10) || 0;
meta.pagesNum = parseInt(meta.pages, 10) || 0;
const dateSection = card.querySelector('div[class*="recordings_cardDateSection"]');
if (dateSection) meta.date = Array.from(dateSection.querySelectorAll('span')).map(s => s.textContent.trim()).join(' ');
// Badge'e: wiersz 1 = [User ID, Kraj], wiersz 2 = [Przeglądarka/aplikacja, Urządzenie]
card.querySelectorAll('div[class*="recordings_cardBadgeRow"]').forEach((row, ri) => {
Array.from(row.querySelectorAll('div[class*="recordings_cardBadge__"]')).map(x => x.textContent.trim()).filter(Boolean).forEach(txt => {
if (txt.startsWith('User ID:')) { meta.userId = txt.replace('User ID:', '').trim().split(/\s+/)[0]; return; }
if (URZADZENIA.includes(txt.toLowerCase())) { meta.device = txt; return; }
if (ri === 0 && !meta.country) meta.country = txt;
else if (ri === 1 && !meta.browser) meta.browser = txt;
else meta.other.push(txt);
});
});
meta.badges = [meta.country, meta.browser, meta.device, ...meta.other].filter(Boolean);
meta.entryPath = sciezkaUrl(meta.entry);
meta.exitPath = sciezkaUrl(meta.exit);
meta.referrerHost = hostReferrera(meta.referrer);
meta.userIdLc = (meta.userId || '').toLowerCase();
meta.klucz = [meta.userId, meta.date, meta.duration, meta.entryPath].join('|'); // stabilny identyfikator sesji
meta._opisFlag = maOpis(card);
metaCache.set(card, meta);
return meta;
}
/* ═══════════════════ FILTRY ═══════════════════ */
// "1r41zk2, 14n3xyi\n724ia7" / z backtickami, cudzysłowami, średnikami… → ['1r41zk2','14n3xyi','724ia7']
function parsujIds(txt) {
return [...new Set(String(txt || '').toLowerCase()
.split(/[\s,;|`"'()[\]{}<>]+/)
.map(s => s.replace(/^[#.:…]+|[#.:…]+$/g, ''))
.filter(s => /^[a-z0-9_-]+$/.test(s)))];
}
function odczytajFiltry() {
const ids = parsujIds(u.ids);
return {
ids, idsSet: new Set(ids),
minCzas: u.minCzas, minKlik: u.minKlik, minStron: u.minStron,
kraj: u.kraj, przegladarka: u.przegladarka, urzadzenie: u.urzadzenie, referrer: u.referrer,
url: (u.url || '').trim().toLowerCase(),
exitUrl: (u.exitUrl || '').trim().toLowerCase()
};
}
function pasujeDoFiltrow(meta, f) {
if (f.ids.length && !f.idsSet.has(meta.userIdLc)) return false;
if (meta.durationSec < f.minCzas) return false;
if (meta.clicksNum < f.minKlik) return false;
if (meta.pagesNum < f.minStron) return false;
if (f.kraj.length && !f.kraj.includes(meta.country)) return false;
if (f.przegladarka.length && !f.przegladarka.includes(meta.browser)) return false;
if (f.urzadzenie.length && !f.urzadzenie.includes(meta.device)) return false;
if (f.referrer.length && !f.referrer.includes(meta.referrerHost)) return false;
if (f.url && !(meta.entry || '').toLowerCase().includes(f.url)) return false;
if (f.exitUrl && !(meta.exit || '').toLowerCase().includes(f.exitUrl)) return false;
return true;
}
const czyFiltryAktywne = f => f.ids.length > 0 || f.minCzas > 0 || f.minKlik > 0 || f.minStron > 0 || f.kraj.length > 0 || f.przegladarka.length > 0 || f.urzadzenie.length > 0 || f.referrer.length > 0 || !!f.url || !!f.exitUrl;
function opisFiltrow(f) {
const k = [];
if (f.ids.length) k.push(`ID nagrań: ${f.ids.length}`);
if (f.minCzas) k.push(`czas ≥ ${f.minCzas}s`);
if (f.minKlik) k.push(`kliknięcia ≥ ${f.minKlik}`);
if (f.minStron) k.push(`strony ≥ ${f.minStron}`);
if (f.kraj.length) k.push(`kraj: ${f.kraj.join('/')}`);
if (f.przegladarka.length) k.push(`przeglądarka: ${f.przegladarka.join('/')}`);
if (f.urzadzenie.length) k.push(`urządzenie: ${f.urzadzenie.join('/')}`);
if (f.referrer.length) k.push(`referrer: ${f.referrer.join('/')}`);
if (f.url) k.push(`entry URL zawiera: "${f.url}"`);
if (f.exitUrl) k.push(`exit URL zawiera: "${f.exitUrl}"`);
return k.join(', ');
}
function policz(metas, pole) {
const m = new Map();
metas.forEach(x => { const v = x[pole]; if (v) m.set(v, (m.get(v) || 0) + 1); });
return [...m.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0], 'pl'));
}
/* ═══════════════════ MULTISELECT ═══════════════════ */
const MS_MAP = { 'f-kraj': ['kraj', 'country'], 'f-przegladarka': ['przegladarka', 'browser'], 'f-urzadzenie': ['urzadzenie', 'device'], 'f-referrer': ['referrer', 'referrerHost'] };
const msDane = {};
function msEtykieta(id) {
const klucz = MS_MAP[id][0];
const wyb = u[klucz];
const box = document.getElementById(id);
if (!box) return;
const lab = box.querySelector('.h-ms-label');
const n = (msDane[id] || []).reduce((a, [, c]) => a + c, 0);
lab.textContent = wyb.length ? `${wyb.join(', ')} (${wyb.length})` : `wszystkie (${n})`;
lab.title = wyb.join(', ');
box.classList.toggle('h-input--active', wyb.length > 0);
}
function msRenderujListe(id) {
const box = document.getElementById(id);
const list = box.querySelector('.h-ms-list');
const klucz = MS_MAP[id][0];
const wpisy = msDane[id] || [];
const brakujace = u[klucz].filter(v => !wpisy.some(([w]) => w === v)).map(v => [v, 0]);
list.innerHTML = [...wpisy, ...brakujace].map(([v, n]) =>
`<div class="h-ms-item${u[klucz].includes(v) ? ' on' : ''}" data-v="${escHtml(v)}"><div class="h-chk">✓</div><span>${escHtml(v)}</span><em>${n}</em></div>`
).join('') || `<div class="h-ms-empty">brak danych</div>`;
}
function odswiezSelecty(metas) {
const dane = {};
for (const id of Object.keys(MS_MAP)) dane[id] = policz(metas, MS_MAP[id][1]);
const sciezki = policz(metas, 'entryPath');
const sciezkiExit = policz(metas, 'exitPath');
const sygnatura = JSON.stringify([dane, sciezki, sciezkiExit]);
if (sygnatura === stan.sygnaturaSelectow) return;
stan.sygnaturaSelectow = sygnatura;
for (const id of Object.keys(MS_MAP)) {
msDane[id] = dane[id];
const box = document.getElementById(id);
if (!box) continue;
if (!box.classList.contains('open')) msRenderujListe(id);
msEtykieta(id);
}
const dl = document.getElementById('f-url-list');
if (dl) dl.innerHTML = sciezki.map(([v, n]) => `<option value="${escHtml(v)}">${n} sesji</option>`).join('');
const dlx = document.getElementById('f-exit-list');
if (dlx) dlx.innerHTML = sciezkiExit.map(([v, n]) => `<option value="${escHtml(v)}">${n} sesji</option>`).join('');
const max = pole => metas.reduce((a, m) => Math.max(a, m[pole] || 0), 0);
const ph = (id, v) => { const el = document.getElementById(id); if (el) el.placeholder = `max ${v}`; };
ph('f-czas', max('durationSec') + ' s'); ph('f-klik', max('clicksNum')); ph('f-strony', max('pagesNum'));
}
function msInit(id) {
const box = document.getElementById(id);
const klucz = MS_MAP[id][0];
box.querySelector('.h-ms-btn').addEventListener('click', e => {
e.stopPropagation();
const otwarty = box.classList.contains('open');
document.querySelectorAll(`#${WID} .h-ms.open`).forEach(b => b.classList.remove('open'));
if (!otwarty) { msRenderujListe(id); box.classList.add('open'); }
});
box.querySelector('.h-ms-list').addEventListener('click', e => {
const item = e.target.closest('.h-ms-item'); if (!item) return;
e.stopPropagation();
const v = item.dataset.v;
const i = u[klucz].indexOf(v);
if (i >= 0) u[klucz].splice(i, 1); else u[klucz].push(v);
item.classList.toggle('on', i < 0);
zapiszUstawienia(); msEtykieta(id); refreshLiczniki();
});
box.querySelector('.h-ms-clear').addEventListener('click', e => {
e.stopPropagation();
u[klucz] = []; zapiszUstawienia(); msRenderujListe(id); msEtykieta(id); refreshLiczniki();
});
msEtykieta(id);
}
/* ═══════════════════ SCHOWEK (trwały, per projekt, klucz = ID nagrania) ═══════════════════ */
// Każdy poprawny opis trafia do localStorage pod ID nagrania. Nagranie, które jest w schowku,
// nigdy nie jest generowane ponownie — także po odświeżeniu strony i w innej karcie przeglądarki.
let schowekCache = {}, schowekProjekt = null;
const idSesji = meta => meta.userIdLc || meta.klucz;
function idProjektu() {
const m = location.pathname.match(/\/projects\/view\/([^/]+)/);
return m ? m[1] : 'default';
}
function schowek() {
const p = idProjektu();
if (schowekProjekt !== p) {
schowekProjekt = p;
try { schowekCache = JSON.parse(localStorage.getItem(LS_SCHOWEK + p) || '{}') || {}; }
catch { schowekCache = {}; }
}
return schowekCache;
}
function zapiszSchowek() {
try { localStorage.setItem(LS_SCHOWEK + schowekProjekt, JSON.stringify(schowekCache)); }
catch { ustawStatus('Brak miejsca w localStorage — schowek nie zapisany', 'warn'); }
}
// inna karta zapisała schowek → wczytaj go na nowo przy następnym odczycie
window.addEventListener('storage', e => { if (e.key && e.key.startsWith(LS_SCHOWEK)) schowekProjekt = null; });
function wpisZMeta(meta, opis) {
return {
userId: meta.userId, userIdLc: meta.userIdLc, date: meta.date,
duration: meta.duration, durationSec: meta.durationSec, clicksNum: meta.clicksNum, pagesNum: meta.pagesNum,
country: meta.country, browser: meta.browser, device: meta.device, referrerHost: meta.referrerHost,
entry: meta.entry, exit: meta.exit, entryPath: meta.entryPath, exitPath: meta.exitPath,
opis, ts: Date.now()
};
}
// Przepisuje do schowka wszystkie poprawne opisy widoczne na kartach. Zwraca liczbę nowych wpisów.
function synchronizujSchowek() {
const s = schowek();
let nowe = 0, zmiana = false;
for (const card of pobierzKarty()) {
if (!maPoprawnyOpis(card)) continue;
const meta = pobierzMetadaneSesji(card);
const id = idSesji(meta);
const opis = tekstOpisu(card);
if (s[id] && s[id].opis === opis) continue;
if (!s[id]) nowe++;
s[id] = wpisZMeta(meta, opis);
zmiana = true;
}
if (zmiana) zapiszSchowek();
if (stan.aktywny) stan.dodane += nowe;
return nowe;
}
function wyzerujSchowek() {
if (stan.aktywny) return;
const n = Object.keys(schowek()).length;
if (!n) { ustawStatus('Schowek jest pusty', 'warn'); return; }
if (!confirm(`Usunąć ${n} opisów ze schowka dla tego projektu?\n\nOpisy widoczne teraz na załadowanych kartach od razu wrócą do schowka (są już wygenerowane).`)) return;
schowekCache = {};
zapiszSchowek();
stan.oczekujace.clear();
ustawStatus(`Wyzerowano schowek (−${n})`, 'ok');
refreshLiczniki();
}
/* ═══════════════════ GENEROWANIE: „Doładuj listę” (zawsze na maksa) ═══════════════════ */
// Pętla: generuj opisy dla załadowanych nagrań spoza schowka → przewiń listę po kolejne → powtarzaj,
// aż lista się skończy, znajdą się wszystkie ID z filtra albo użytkownik kliknie „Zatrzymaj”.
const czekaj = ms => new Promise(r => setTimeout(r, ms));
const liczbaOczekujacych = () => [...stan.oczekujace.values()].filter(ts => Date.now() - ts < OCZEKIWANIE_MS).length;
function znajdzKartyPoKluczu(klucze) {
const mapa = new Map();
for (const card of pobierzKarty()) {
const k = pobierzMetadaneSesji(card).klucz;
if (klucze.has(k)) mapa.set(k, card);
}
return mapa;
}
function kandydaciDoGenerowania() {
const f = odczytajFiltry();
const s = schowek();
const widziane = new Set();
const lista = [];
let pominiete = 0, odfiltrowane = 0;
for (const card of pobierzKarty()) {
const meta = pobierzMetadaneSesji(card);
const id = idSesji(meta);
if (s[id] || widziane.has(id) || maPoprawnyOpis(card)) continue; // już jest — nie generujemy drugi raz
if (!pasujeDoFiltrow(meta, f)) { odfiltrowane++; continue; }
const btn = przyciskGeneruj(card);
if (!btn || btn.disabled) { pominiete++; continue; }
const ts = stan.oczekujace.get(id);
if (ts && Date.now() - ts < OCZEKIWANIE_MS) continue; // kliknięte, czeka na odpowiedź
if ((stan.proby.get(id) || 0) >= MAX_PROB) continue; // AI odmawia — odpuszczamy
widziane.add(id);
lista.push({ id, klucz: meta.klucz });
}
return { lista, pominiete, odfiltrowane };
}
function wszystkieIdZnalezione() {
const f = odczytajFiltry();
if (!f.ids.length) return false;
const s = schowek();
const zaladowane = new Set(pobierzKarty().map(c => pobierzMetadaneSesji(c).userIdLc));
return f.ids.every(id => s[id] || zaladowane.has(id));
}
async function startProcess() {
if (stan.aktywny) { stan.stop = true; ustawStatus('Zatrzymywanie…', 'warn'); odswiezPrzyciski(); return; }
stan.aktywny = true; stan.stop = false; stan.dodane = 0; stan.proby = new Map();
odswiezPrzyciski();
let pustePrzewiniecia = 0, powod = '';
try {
while (!stan.stop) {
synchronizujSchowek();
const { lista } = kandydaciDoGenerowania();
if (lista.length) {
const w = await runda(lista);
if (stan.stop) break;
if (w.klikniete > 0) {
if (w.bledy) { ustawStatus(`${w.bledy} błąd AI → ponawiam za ${PRZERWA_PO_BLEDZIE / 1000} s…`, 'warn'); await czekaj(PRZERWA_PO_BLEDZIE); }
continue;
}
}
if (wszystkieIdZnalezione()) { powod = 'znaleziono wszystkie ID z filtra'; break; }
const nowe = await przewinListe();
if (nowe > 0) pustePrzewiniecia = 0;
else if (++pustePrzewiniecia >= 2) { powod = 'koniec listy'; break; }
}
} finally {
zakoncz(powod);
}
}
// Klika „Generate” po kolei, potem czeka na opisy. Zwraca { klikniete, bledy }.
function runda(lista) {
return new Promise(resolve => {
const klikniete = [];
let i = 0;
const klik = () => {
if (stan.stop || i >= lista.length) { obserwuj(klikniete, resolve); return; }
const w = lista[i++];
let kliknieto = false;
if (!schowek()[w.id]) { // ponowne sprawdzenie tuż przed kliknięciem (np. inna karta przeglądarki)
const card = znajdzKartyPoKluczu(new Set([w.klucz])).get(w.klucz);
const btn = card ? przyciskGeneruj(card) : null;
if (card && !maPoprawnyOpis(card) && btn && !btn.disabled) {
w.staryOpis = maOpis(card) ? tekstOpisu(card) : null;
btn.click();
kliknieto = true;
stan.oczekujace.set(w.id, Date.now());
stan.proby.set(w.id, (stan.proby.get(w.id) || 0) + 1);
klikniete.push(w);
}
}
ustawStatus(`Klikam ${i}/${lista.length} · +${stan.dodane} w schowku`);
odswiezPrzyciski();
setTimeout(klik, kliknieto ? 700 + Math.random() * 500 : 0);
};
klik();
});
}
function obserwuj(klikniete, resolve) {
if (!klikniete.length) { resolve({ klikniete: 0, bledy: 0 }); return; }
const klucze = new Set(klikniete.map(w => w.klucz));
const start = Date.now();
const maxMs = Math.min(300, Math.max(60, klikniete.length * 10)) * 1000;
let ostatniPostep = start, ostatnieGotowe = -1;
clearInterval(stan.watcherId);
stan.watcherId = setInterval(() => {
synchronizujSchowek();
const mapa = znajdzKartyPoKluczu(klucze);
let gotowe = 0, bledy = 0, zniknelo = 0;
for (const w of klikniete) {
const card = mapa.get(w.klucz);
if (!card) { zniknelo++; continue; }
if (!maOpis(card)) continue;
if (w.staryOpis != null && tekstOpisu(card) === w.staryOpis) continue; // stary błąd jeszcze nie podmieniony
gotowe++;
stan.oczekujace.delete(w.id);
if (czyBladAI(card)) bledy++;
}
if (gotowe !== ostatnieGotowe) { ostatnieGotowe = gotowe; ostatniPostep = Date.now(); }
const sek = Math.round((Date.now() - start) / 1000);
ustawStatus(`Generuję ${gotowe}/${klikniete.length} · +${stan.dodane} w schowku · ${sek}s`);
odswiezPrzyciski();
const koniec = gotowe + zniknelo >= klikniete.length
|| Date.now() - start >= maxMs
|| stan.stop
|| (gotowe > 0 && Date.now() - ostatniPostep >= 45000);
if (koniec) {
clearInterval(stan.watcherId); stan.watcherId = null;
resolve({ klikniete: klikniete.length, bledy });
}
}, 1000);
}
function zakoncz(powod) {
clearInterval(stan.watcherId); stan.watcherId = null;
const przerwane = stan.stop;
stan.aktywny = false; stan.stop = false; stan.przewijanie = false;
synchronizujSchowek();
const s = schowek();
const nieudane = [...stan.proby.entries()].filter(([id, n]) => n >= MAX_PROB && !s[id]).length;
stan.proby = new Map();
const { pominiete, odfiltrowane } = kandydaciDoGenerowania();
ustawStatus(`${przerwane ? 'Zatrzymano' : 'Gotowe'} · +${stan.dodane} do schowka (razem: ${Object.keys(s).length})`, przerwane ? 'warn' : 'ok');
const linie = [];
if (powod) linie.push(powod);
if (odfiltrowane) linie.push(`${odfiltrowane} odfiltrowano`);
if (pominiete) linie.push(`${pominiete} zbyt krótkich (brak przycisku)`);
if (nieudane) linie.push(`${nieudane} z błędem AI po ${MAX_PROB} próbach`);
const rep = document.getElementById('h-report');
rep.textContent = linie.join(' · ');
rep.style.display = linie.length ? 'block' : 'none';
refreshLiczniki();
}
// Przewija listę Clarity, żeby doładowała kolejne nagrania. Zwraca liczbę nowych kart.
function przewinListe() {
return new Promise(resolve => {
const karty = pobierzKarty();
if (!karty.length) { resolve(0); return; }
stan.przewijanie = true;
karty.forEach(c => c.classList.remove('ch-hidden')); // ukryte karty nie wyzwalają infinite scroll
const ostatnia = karty[karty.length - 1];
ostatnia.scrollIntoView({ block: 'end' });
let el = ostatnia.parentElement;
while (el && el !== document.body) {
if (el.scrollHeight > el.clientHeight + 5) el.scrollTop = el.scrollHeight;
el = el.parentElement;
}
window.scrollTo(0, document.body.scrollHeight);
const przed = karty.length;
ustawStatus(`Przewijam po kolejne nagrania (załadowane: ${przed}) · +${stan.dodane} w schowku`);
setTimeout(() => {
stan.przewijanie = false;
const po = pobierzKarty().length;
refreshLiczniki();
resolve(po - przed);
}, 2500);
});
}
/* ═══════════════════ EKSPORT: PROMPT + INPUT (JSON) ═══════════════════ */
function wykryjDomeneSerwisu(dane) {
for (const d of dane) { if (d.entry) { try { return new URL(d.entry).hostname; } catch { /* skrócony */ } } }
return null;
}
function anonimizujTekst(tekst, domena) {
if (!domena || !tekst) return tekst;
const bezWww = domena.replace(/^www\./, '');
const wzorce = [...new Set(['www.' + bezWww, bezWww, domena])].sort((a, b) => b.length - a.length);
let wynik = tekst;
for (const w of wzorce) wynik = wynik.replace(new RegExp(w.replace(/\./g, '\\.'), 'gi'), 'firma.pl');
return wynik;
}
// Wpisy ze schowka do eksportu: z opisem, zgodne z filtrami i — gdy włączona opcja
// „tylko z załadowanej listy” — wyłącznie nagrania obecne teraz na liście Clarity
// (bez wpisów z wcześniejszych przebiegów / innych zakresów dat).
function wpisyDoEksportu() {
const f = odczytajFiltry();
const naLiscie = u.tylkoZListy
? new Set(pobierzKarty().map(c => idSesji(pobierzMetadaneSesji(c))))
: null;
return Object.entries(schowek())
.filter(([id, e]) => e.opis && pasujeDoFiltrow(e, f) && (!naLiscie || naLiscie.has(id)))
.map(([, e]) => e);
}
// Źródłem dla prompta jest schowek (przetrwa odświeżenie), zawężony filtrami
function pobierzDaneSesji() {
synchronizujSchowek();
return wpisyDoEksportu();
}
function zbudujInputJson(dane) {
const domena = wykryjDomeneSerwisu(dane);
const anon = t => anonimizujTekst(t, domena);
return dane.map(d => ({
userId: d.userId,
date: d.date,
duration: d.duration,
clicks: d.clicksNum,
pages: d.pagesNum,
country: d.country,
browser: d.browser,
device: d.device,
referrer: d.referrerHost,
entry: anon(usunParametryUrl(d.entry)),
exit: anon(usunParametryUrl(d.exit)),
description: anon(d.opis)
}));
}
async function kopiujPrompt() {
const dane = pobierzDaneSesji();
if (!dane.length) { flashBtn('btn-copy', '✕ brak opisów do skopiowania', C.red); return; }
const json = JSON.stringify(zbudujInputJson(dane), null, 2);
const f = odczytajFiltry();
const zakres = [];
if (u.tylkoZListy) zakres.push('tylko nagrania z załadowanej listy');
if (czyFiltryAktywne(f)) zakres.push('filtr: ' + opisFiltrow(f));
const naglowek = `<!-- nagrań w INPUT (N): ${dane.length}${zakres.length ? '; ' + zakres.join('; ') : ''} -->\n`;
const prompt = (u.prompt || DOMYSLNY_PROMPT).replace(/\s*$/, '') + '\n' + naglowek + json + '\n';
try {
await navigator.clipboard.writeText(prompt);
flashBtn('btn-copy', `✓ skopiowano prompt + ${dane.length} opisów`, C.green);
} catch { flashBtn('btn-copy', '✕ błąd schowka', C.red); }
}
function flashBtn(id, text, color) {
const btn = document.getElementById(id);
const orig = btn.dataset.label || btn.textContent;
btn.dataset.label = orig;
btn.textContent = text;
btn.style.background = color; btn.style.borderColor = color; btn.style.color = '#fff';
setTimeout(() => { btn.textContent = orig; btn.style.background = ''; btn.style.borderColor = ''; btn.style.color = ''; }, 2200);
}
/* ═══════════════════ UI ═══════════════════ */
function ustawStatus(msg, typ) {
const el = document.getElementById('h-status');
el.textContent = msg;
el.className = 'h-status' + (typ ? ' h-status--' + typ : '');
if (typ !== 'ok') document.getElementById('h-report').style.display = 'none';
}
// Przycisk „Doładuj listę (X)” / „Zatrzymaj (X)” — X = ile opisów zostało do wygenerowania
function odswiezPrzyciski() {
const btn = document.getElementById('btn-load');
const rst = document.getElementById('btn-wipe');
if (!btn) return;
const x = kandydaciDoGenerowania().lista.length + (stan.aktywny ? liczbaOczekujacych() : 0);
btn.textContent = stan.aktywny
? (stan.stop ? '⏳ Zatrzymywanie…' : `⏹ Zatrzymaj (${x})`)
: `⟳ Doładuj listę (${x})`;
btn.classList.toggle('h-btn--stop', stan.aktywny);
btn.title = stan.aktywny
? 'Zatrzymuje generowanie (to, co już jest w schowku, zostaje)'
: `Generuje opisy dla wszystkich nagrań pasujących do filtrów, których nie ma w schowku (${x} na załadowanej liście), i przewija listę po kolejne`;
rst.disabled = stan.aktywny;
}
// Filtruje listę Clarity: karty spoza filtra dostają .ch-hidden (display:none), karty w schowku — .ch-stored
function zastosujFiltrNaListe() {
const karty = pobierzKarty();
const metas = karty.map(pobierzMetadaneSesji);
const f = odczytajFiltry();
const aktywne = czyFiltryAktywne(f);
const s = schowek();
let pasuje = 0;
karty.forEach((card, i) => {
const ok = !aktywne || pasujeDoFiltrow(metas[i], f);
if (ok) pasuje++;
card.classList.toggle('ch-hidden', !stan.przewijanie && u.ukrywajKarty && aktywne && !ok);
card.classList.toggle('ch-stored', !!s[idSesji(metas[i])]);
});
return { karty, metas, aktywne, pasuje, f };
}
function odswiezInfoIds(metas, f) {
const el = document.getElementById('h-ids-info');
if (!el) return;
if (!f.ids.length) { el.style.display = 'none'; return; }
const s = schowek();
const zaladowane = new Set(metas.map(m => m.userIdLc));
const wSchowku = f.ids.filter(id => s[id]).length;
const brak = f.ids.filter(id => !zaladowane.has(id) && !s[id]);
el.innerHTML = `znaleziono <b>${f.ids.length - brak.length}/${f.ids.length}</b> · w schowku <b>${wSchowku}</b>`
+ (brak.length ? ` · brak: <span style="color:${C.orange}">${escHtml(brak.join(', '))}</span> — „Doładuj listę” je wyszuka` : '');
el.style.display = 'block';
}
function refreshLiczniki() {
if (!widget) return;
synchronizujSchowek();
const { karty, metas, aktywne, pasuje, f } = zastosujFiltrNaListe();
const wpisy = Object.values(schowek());
const doEksportu = wpisyDoEksportu().length;
const ukryte = u.ukrywajKarty && aktywne ? karty.length - pasuje : 0;
document.getElementById('h-badge').textContent = karty.length ? `${karty.length} / ${pobierzDostepneNagrania()}` : '—';
document.getElementById('h-info').innerHTML =
`załadowane <b>${karty.length}</b> · pasuje <b style="color:${C.accent}">${pasuje}</b>${ukryte ? ` · ukryte <b>${ukryte}</b>` : ''}`
+ ` · w schowku <b style="color:${C.green}">${wpisy.length}</b>${aktywne || u.tylkoZListy ? ` (do eksportu ${doEksportu})` : ''}`;
document.getElementById('h-filter-cnt').textContent = aktywne ? `${pasuje}/${karty.length}` : '';
const cb = document.getElementById('btn-copy');
if (!cb.dataset.label || cb.textContent === cb.dataset.label) {
cb.textContent = `⎘ Kopiuj prompt + INPUT (${doEksportu})`;
cb.dataset.label = cb.textContent;
}
odswiezPrzyciski();
odswiezInfoIds(metas, f);
// listy w filtrach budujemy z załadowanych kart + schowka (po odświeżeniu schowek ma więcej nagrań niż lista)
const naLiscie = new Set(metas.map(idSesji));
odswiezSelecty([...metas, ...wpisy.filter(e => !naLiscie.has(e.userIdLc))]);
}
function buildWidget() {
collapsed = !!u.collapsed;
widget = document.createElement('div');
widget.id = WID;
widget.innerHTML = `
<style>
#${WID}{position:fixed;bottom:16px;right:16px;z-index:2147483646;font-family:'Google Sans',system-ui,-apple-system,sans-serif;width:340px;border-radius:14px;overflow:hidden;box-shadow:0 10px 36px rgba(0,0,0,.55);border:1px solid ${C.border};background:${C.bg};color:${C.text};user-select:none;display:flex;flex-direction:column}
#${WID} *{box-sizing:border-box}
div[data-testid="sessionCard"].ch-hidden{display:none!important}
div[data-testid="sessionCard"].ch-stored{box-shadow:inset 4px 0 0 ${C.green}!important}
#${WID} .h-hdr{background:linear-gradient(135deg,${C.accentDk},${C.bg});padding:10px 14px;display:flex;align-items:center;gap:8px;cursor:move;border-bottom:1px solid ${C.border}}
#${WID} .h-logo{width:22px;height:22px;flex-shrink:0;display:flex;align-items:center;justify-content:center}
#${WID} .h-title{flex:1;font-size:13px;font-weight:600;color:#fff;letter-spacing:.3px}
#${WID} .h-badge{font-size:10px;background:${C.border};color:${C.dim};padding:2px 8px;border-radius:10px;white-space:nowrap}
#${WID} .h-hdr-btn{background:none;border:none;color:#fff;font-size:18px;cursor:pointer;padding:0 4px;line-height:1;opacity:.6}
#${WID} .h-hdr-btn:hover{opacity:1}
#${WID} .h-wrap{overflow:hidden;transition:max-height .25s ease}
#${WID} .h-wrap.open{max-height:92vh}#${WID} .h-wrap.closed{max-height:0!important}
#${WID} .h-panel{padding:10px 14px;max-height:calc(100vh - 170px);overflow-y:auto}
#${WID} .h-panel::-webkit-scrollbar{width:5px}#${WID} .h-panel::-webkit-scrollbar-thumb{background:${C.border};border-radius:3px}
#${WID} .h-label{font-size:10px;color:${C.dim};font-weight:700;text-transform:uppercase;letter-spacing:.5px;margin:0 0 4px;display:flex;align-items:center;gap:6px}
#${WID} .h-label .h-cnt{color:${C.accent};text-transform:none;letter-spacing:0}
#${WID} .h-link{margin-left:auto;color:${C.dim};font-weight:600;text-transform:none;letter-spacing:0;cursor:pointer;border:none;background:none;font-size:10px;font-family:inherit}
#${WID} .h-link:hover{color:${C.red}}
#${WID} .h-grid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px;margin-bottom:8px}
#${WID} .h-grid2{display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:8px}
#${WID} .h-field{display:flex;flex-direction:column;gap:3px}
#${WID} .h-field label{font-size:9px;color:${C.dim};font-weight:700;text-transform:uppercase;letter-spacing:.4px}
#${WID} .h-input{width:100%;background:${C.input};border:1px solid #4b5563;color:${C.text};padding:6px 8px;border-radius:8px;font-size:11px;outline:none;font-family:inherit;min-width:0}
#${WID} .h-input:focus{border-color:${C.accent}}
#${WID} .h-input::placeholder{color:#4b5563}
#${WID} .h-input--active{border-color:${C.accent};background:${C.accentDk}}
#${WID} textarea.h-input{resize:vertical;min-height:120px;font-size:10px;line-height:1.35;user-select:text;font-family:ui-monospace,Menlo,Consolas,monospace}
#${WID} .h-ids-info{font-size:10px;color:${C.dim};line-height:1.35;word-break:break-all;display:none}
#${WID} .h-ids-info b{color:${C.text}}
#${WID} .h-ms{position:relative}
#${WID} .h-ms-btn{display:flex;align-items:center;gap:6px;cursor:pointer;text-align:left}
#${WID} .h-ms-label{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
#${WID} .h-ms-btn::after{content:'';width:10px;height:6px;flex-shrink:0;background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%236b7280' stroke-width='1.5' fill='none'/%3E%3C/svg%3E") no-repeat center}
#${WID} .h-ms-drop{display:none;position:absolute;left:0;right:0;top:calc(100% + 4px);z-index:5;background:${C.bg};border:1px solid ${C.accent};border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,.5);overflow:hidden}
#${WID} .h-ms.open .h-ms-drop{display:block}
#${WID} .h-ms-list{max-height:180px;overflow-y:auto;padding:4px 0}
#${WID} .h-ms-item{display:flex;align-items:center;gap:7px;padding:5px 8px;font-size:11px;color:${C.text};cursor:pointer}
#${WID} .h-ms-item:hover{background:${C.hover}}
#${WID} .h-ms-item span{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
#${WID} .h-ms-item em{font-style:normal;color:${C.dim};font-size:10px}
#${WID} .h-ms-item.on .h-chk{background:${C.accent};border-color:${C.accent};color:#fff}
#${WID} .h-ms-empty{padding:8px;font-size:10px;color:${C.dim};text-align:center}
#${WID} .h-ms-clear{display:block;width:100%;background:${C.hover};border:none;border-top:1px solid ${C.border};color:${C.dim};font-size:10px;padding:5px;cursor:pointer;font-family:inherit}
#${WID} .h-ms-clear:hover{color:${C.red}}
#${WID} .h-sep{border:none;border-top:1px solid ${C.border};margin:10px 0}
#${WID} .h-opt{display:flex;align-items:center;gap:7px;cursor:pointer;padding:3px 0;font-size:11px;color:${C.dim}}
#${WID} .h-opt:hover{color:${C.text}}
#${WID} .h-chk{width:14px;height:14px;border-radius:4px;border:2px solid ${C.border};display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:9px;color:transparent;transition:all .12s}
#${WID} .h-opt.on .h-chk{background:${C.accent};border-color:${C.accent};color:#fff}
#${WID} .h-info{font-size:10px;color:${C.dim};margin:6px 0 8px;line-height:1.4}
#${WID} .h-info b{color:${C.text};font-weight:600}
#${WID} .h-status{font-size:11px;font-weight:500;color:${C.text};background:${C.hover};border:1px solid ${C.border};padding:6px 10px;border-radius:8px}
#${WID} .h-status--ok{border-color:${C.green};color:${C.green}}
#${WID} .h-status--warn{border-color:${C.orange};color:${C.orange}}
#${WID} .h-report{font-size:10px;color:${C.orange};margin-top:4px;padding:0 4px;line-height:1.4;display:none}
#${WID} .h-bar{border-top:1px solid ${C.border};padding:10px 12px;background:${C.hover};display:grid;grid-template-columns:1fr 1fr;gap:6px}
#${WID} .h-btn{font-size:12px;font-weight:700;padding:9px 8px;width:100%;border-radius:8px;cursor:pointer;border:1px solid ${C.border};background:${C.bg};color:${C.text};transition:all .12s;text-align:center;white-space:nowrap;font-family:inherit;overflow:hidden;text-overflow:ellipsis}
#${WID} .h-btn:hover{border-color:${C.accent}}
#${WID} .h-btn:active{transform:scale(.98)}
#${WID} .h-btn--primary{background:${C.accent};border-color:${C.accent};color:#fff}
#${WID} .h-btn--primary:hover{background:#2563eb}
#${WID} .h-btn--stop{background:${C.red}!important;border-color:${C.red}!important}
#${WID} .h-btn--danger{background:${C.bg};border-color:#7f1d1d;color:#fca5a5}
#${WID} .h-btn--danger:hover{border-color:${C.red};color:#fff}
#${WID} .h-btn:disabled{opacity:.4;cursor:not-allowed;transform:none}
#${WID} .h-btn--dark{background:#000;border-color:#000;color:${C.text};grid-column:1 / -1}
#${WID} .h-btn--dark:hover{border-color:${C.dim}}
#${WID} details.h-prompt summary{font-size:10px;color:${C.dim};font-weight:700;text-transform:uppercase;letter-spacing:.5px;cursor:pointer;user-select:none;margin:8px 0 4px;list-style:none;display:flex;align-items:center;gap:6px}
#${WID} details.h-prompt summary::before{content:'▸';font-size:11px}
#${WID} details.h-prompt[open] summary::before{content:'▾'}
#${WID} details.h-prompt summary .h-link{margin-left:auto}
</style>
<div class="h-hdr" id="h-hdr">
<div class="h-logo"><svg width="22" height="22" viewBox="0 0 192 192" xmlns="http://www.w3.org/2000/svg"><rect width="192" height="192" rx="24" fill="#1960bf"/><path d="M56 48L144 96L56 144V48Z" fill="#fff"/></svg></div>
<span class="h-title">Clarity Insighter</span>
<span class="h-badge" id="h-badge" title="załadowane / dostępne">—</span>
<button class="h-hdr-btn" id="h-col">${collapsed ? '▸' : '▾'}</button>
</div>
<div class="h-wrap ${collapsed ? 'closed' : 'open'}" id="h-wrap">
<div class="h-panel">
<div class="h-label">Filtry nagrań <span class="h-cnt" id="h-filter-cnt"></span><button class="h-link" id="btn-reset">✕ wyczyść</button></div>
<div class="h-field" style="margin-bottom:8px">
<label title="Wklej kolumnę „ID nagrań” z tabeli LLM — separator: przecinek, spacja, średnik lub nowa linia">ID nagrań (po przecinku)</label>
<input type="text" id="f-ids" class="h-input" value="${escHtml(u.ids)}" placeholder="np. 1r41zk2, 14n3xyi, 724ia7" spellcheck="false" autocomplete="off">
<div class="h-ids-info" id="h-ids-info"></div>
</div>
<div class="h-grid">
<div class="h-field"><label title="Minimalny czas trwania sesji, w sekundach (np. 60 = 1 min)">Czas ≥ (sek.)</label><input type="number" id="f-czas" class="h-input" min="0" value="${u.minCzas || ''}"></div>
<div class="h-field"><label>Kliki ≥</label><input type="number" id="f-klik" class="h-input" min="0" value="${u.minKlik || ''}"></div>
<div class="h-field"><label>Strony ≥</label><input type="number" id="f-strony" class="h-input" min="0" value="${u.minStron || ''}"></div>
</div>
<div class="h-grid2">
${ms('f-kraj', 'Kraj')}
${ms('f-urzadzenie', 'Urządzenie')}
</div>
<div class="h-grid2">
${ms('f-przegladarka', 'Przeglądarka / aplikacja')}
${ms('f-referrer', 'Referrer', 'Domena strony, z której użytkownik przyszedł')}
</div>
<div class="h-field" style="margin-bottom:8px"><label>Entry URL zawiera</label><input type="text" id="f-url" class="h-input" list="f-url-list" value="${escHtml(u.url)}" placeholder="np. /produkt/ — lista podpowiada ścieżki"><datalist id="f-url-list"></datalist></div>
<div class="h-field"><label>Exit URL zawiera</label><input type="text" id="f-exit" class="h-input" list="f-exit-list" value="${escHtml(u.exitUrl)}" placeholder="np. /koszyk/ — lista podpowiada ścieżki"><datalist id="f-exit-list"></datalist></div>
<hr class="h-sep">
<div class="h-opt" data-opt="ukrywajKarty" title="Karty niepasujące do filtrów znikają z listy po lewej"><div class="h-chk">✓</div><span>Ukrywaj niepasujące nagrania na liście Clarity</span></div>
<div class="h-opt" data-opt="tylkoZListy" title="Do prompta trafiają tylko nagrania obecne na załadowanej liście Clarity (bez wpisów ze schowka z wcześniejszych przebiegów / innych zakresów dat)"><div class="h-chk">✓</div><span>Kopiuj tylko nagrania z załadowanej listy</span></div>
<details class="h-prompt" id="h-prompt" ${u.promptOpen ? 'open' : ''}>
<summary>Prompt (edytowalny) <button class="h-link" id="btn-prompt-reset" title="Przywróć domyślny prompt">↺ domyślny</button></summary>
<textarea id="f-prompt" class="h-input" spellcheck="false">${escHtml(u.prompt)}</textarea>
<div class="h-info" style="margin-top:4px">Pod sekcją <b>## INPUT</b> zostanie doklejony JSON z opisami sesji pasujących do filtrów.</div>
</details>
<div class="h-info" id="h-info">…</div>
<div class="h-status" id="h-status">Gotowy</div>
<div class="h-report" id="h-report"></div>
</div>
<div class="h-bar">
<button class="h-btn h-btn--primary" id="btn-load">⟳ Doładuj listę</button>
<button class="h-btn h-btn--danger" id="btn-wipe" title="Usuwa wszystkie opisy ze schowka tego projektu">🗑 Wyzeruj listę</button>
<button class="h-btn h-btn--dark" id="btn-copy" title="Kopiuje prompt z sekcją ## INPUT wypełnioną JSON-em opisów ze schowka (wg filtrów i opcji „tylko z załadowanej listy”)">⎘ Kopiuj prompt + INPUT</button>
</div>
</div>`;
document.body.appendChild(widget);
widget.querySelectorAll('.h-opt').forEach(el => {
el.classList.toggle('on', !!u[el.dataset.opt]);
el.addEventListener('click', () => {
u[el.dataset.opt] = !u[el.dataset.opt];
el.classList.toggle('on', u[el.dataset.opt]);
zapiszUstawienia(); refreshLiczniki();
});
});
const powiaz = (id, klucz, parse) => {
const el = document.getElementById(id);
const zastosuj = () => {
u[klucz] = parse ? parse(el.value) : el.value;
el.classList.toggle('h-input--active', !!u[klucz] && klucz !== 'prompt');
zapiszUstawienia(); refreshLiczniki();
};
el.addEventListener('input', zastosuj);
el.addEventListener('change', zastosuj);
el.classList.toggle('h-input--active', !!u[klucz] && klucz !== 'prompt');
};
const num = v => parseInt(v, 10) || 0;
powiaz('f-ids', 'ids');
powiaz('f-czas', 'minCzas', num); powiaz('f-klik', 'minKlik', num); powiaz('f-strony', 'minStron', num);
powiaz('f-url', 'url');
powiaz('f-exit', 'exitUrl');
powiaz('f-prompt', 'prompt');
Object.keys(MS_MAP).forEach(msInit);
document.addEventListener('click', () => document.querySelectorAll(`#${WID} .h-ms.open`).forEach(b => b.classList.remove('open')));
// Pole input gubi znaki nowej linii przy wklejaniu (ID sklejałyby się) — zamieniamy je na przecinki
document.getElementById('f-ids').addEventListener('paste', e => {
const t = e.clipboardData?.getData('text');
if (!t) return;
e.preventDefault();
const el = e.target;
el.setRangeText(t.trim().replace(/\s*[\r\n]+\s*/g, ', '), el.selectionStart, el.selectionEnd, 'end');
el.dispatchEvent(new Event('input'));
});
document.getElementById('h-prompt').addEventListener('toggle', e => {
u.promptOpen = e.target.open; zapiszUstawienia();
setTimeout(dopasujDoEkranu, 50);
});
document.getElementById('btn-prompt-reset').addEventListener('click', e => {
e.preventDefault(); e.stopPropagation();
u.prompt = DOMYSLNY_PROMPT; u.promptWersja = PROMPT_WERSJA;
document.getElementById('f-prompt').value = DOMYSLNY_PROMPT; zapiszUstawienia();
});
// „wyczyść” = zero filtrów (wszystkie nagrania widoczne)
document.getElementById('btn-reset').addEventListener('click', () => {
Object.assign(u, { ids: '', minCzas: 0, minKlik: 0, minStron: 0, kraj: [], przegladarka: [], urzadzenie: [], referrer: [], url: '', exitUrl: '' });
const ustaw = (id, v) => { const el = document.getElementById(id); el.value = v || ''; el.classList.toggle('h-input--active', !!v); };
ustaw('f-ids', ''); ustaw('f-czas', 0); ustaw('f-klik', 0); ustaw('f-strony', 0); ustaw('f-url', ''); ustaw('f-exit', '');
Object.keys(MS_MAP).forEach(id => { msRenderujListe(id); msEtykieta(id); });
zapiszUstawienia(); refreshLiczniki();
});
const colBtn = document.getElementById('h-col');
colBtn.addEventListener('click', () => {
zakotwicz(); // przy dolnej krawędzi zakotwicz od dołu → panel rozwija się w górę
collapsed = !collapsed; u.collapsed = collapsed; zapiszUstawienia();
document.getElementById('h-wrap').classList.toggle('open', !collapsed);
document.getElementById('h-wrap').classList.toggle('closed', collapsed);
colBtn.textContent = collapsed ? '▸' : '▾';
setTimeout(() => { dopasujDoEkranu(); savePos(); }, 300);
});
document.getElementById('btn-load').addEventListener('click', startProcess);
document.getElementById('btn-wipe').addEventListener('click', wyzerujSchowek);
document.getElementById('btn-copy').addEventListener('click', kopiujPrompt);
makeDraggable(widget, document.getElementById('h-hdr'));
restorePos();
window.addEventListener('resize', () => { zakotwicz(); dopasujDoEkranu(); });
refreshLiczniki();
}
function ms(id, label, title = '') {
const t = title ? ` title="${escHtml(title)}"` : '';
return `<div class="h-field"><label${t}>${label}</label><div class="h-ms" id="${id}"><div class="h-input h-ms-btn"><span class="h-ms-label">wszystkie</span></div><div class="h-ms-drop"><div class="h-ms-list"></div><button class="h-ms-clear">wyczyść</button></div></div></div>`;
}
/* ═══════════════════ DRAG / POZYCJA ═══════════════════ */
// Widget w dolnej połowie ekranu jest kotwiczony od dołu (bottom), w górnej — od góry (top).
// Dzięki temu przy dolnej krawędzi panel rozwija się w górę, a przy górnej — w dół.
const clamp = (v, min, max) => Math.min(Math.max(min, v), Math.max(min, max));
const czyZDolu = () => !widget.style.top || widget.style.top === 'auto';
function zakotwicz() {
if (!widget) return;
const r = widget.getBoundingClientRect();
const vw = window.innerWidth, vh = window.innerHeight;
widget.style.left = clamp(r.left, 0, vw - r.width) + 'px';
widget.style.right = 'auto';
if (r.top + r.height / 2 > vh / 2) {
widget.style.top = 'auto';
widget.style.bottom = clamp(vh - r.bottom, 0, vh - r.height) + 'px';
} else {
widget.style.bottom = 'auto';
widget.style.top = clamp(r.top, 0, vh - r.height) + 'px';
}
}
// Po rozwinięciu: jeśli widget wystaje poza ekran, przesuń go tak, żeby się zmieścił
function dopasujDoEkranu() {
if (!widget) return;
const r = widget.getBoundingClientRect();
const vh = window.innerHeight, m = 8;
if (czyZDolu()) {
if (r.top < m) {
const b = parseFloat(getComputedStyle(widget).bottom) || 0;
widget.style.bottom = Math.max(0, b + r.top - m) + 'px';
}
} else if (r.bottom > vh - m) {
const t = parseFloat(widget.style.top) || 0;
widget.style.top = Math.max(0, t - (r.bottom - vh + m)) + 'px';
}
}
function savePos() {
const s = widget.style;
if (!s.left) return; // pozycja domyślna (prawy dolny róg) — nic do zapisania
const p = czyZDolu()
? { left: parseFloat(s.left), bottom: parseFloat(getComputedStyle(widget).bottom) || 0 }
: { left: parseFloat(s.left), top: parseFloat(s.top) || 0 };
try { localStorage.setItem(LS_POS, JSON.stringify(p)); } catch { /* ignoruj */ }
}
function restorePos() {
try {
const p = JSON.parse(localStorage.getItem(LS_POS) || 'null');
if (!p) return;
widget.style.left = clamp(p.left, 0, window.innerWidth - 60) + 'px';
widget.style.right = 'auto';
if (typeof p.bottom === 'number') {
widget.style.top = 'auto';
widget.style.bottom = clamp(p.bottom, 0, window.innerHeight - 60) + 'px';
} else {
widget.style.bottom = 'auto';
widget.style.top = clamp(p.top, 0, window.innerHeight - 60) + 'px';
}
zakotwicz(); // stary format {left, top} → przelicz na właściwą kotwicę
dopasujDoEkranu();
} catch { /* ignoruj */ }
}
function makeDraggable(el, handle) {
handle.addEventListener('mousedown', e => {
if (e.target.closest('button')) return;
e.preventDefault();
const sx = e.clientX, sy = e.clientY, r = el.getBoundingClientRect();
const mv = ev => {
el.style.left = (r.left + ev.clientX - sx) + 'px';
el.style.top = (r.top + ev.clientY - sy) + 'px';
el.style.right = 'auto'; el.style.bottom = 'auto';
};
const up = () => {
document.removeEventListener('mousemove', mv);
document.removeEventListener('mouseup', up);
zakotwicz(); savePos();
};
document.addEventListener('mousemove', mv);
document.addEventListener('mouseup', up);
});
}
/* ═══════════════════ INIT ═══════════════════ */
let moTimer = null;
function obserwujListe() {
const mo = new MutationObserver(muts => {
// interesują nas tylko zmiany poza widgetem
if (!muts.some(m => !widget.contains(m.target))) return;
clearTimeout(moTimer);
moTimer = setTimeout(() => { zastosujFiltrNaListe(); }, 60);
});
mo.observe(document.body, { childList: true, subtree: true });
}
function init() {
buildWidget();
obserwujListe();
window.addEventListener('beforeunload', e => { if (stan.aktywny) { e.preventDefault(); e.returnValue = ''; } });
setInterval(refreshLiczniki, 3000);
}
if (document.readyState === 'complete') setTimeout(init, 1000);
else window.addEventListener('load', () => setTimeout(init, 1000));
})();

