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 MS Clarity Recorder Helper - Raport Błędów
// @namespace http://tampermonkey.net/
// @version 2026-02-17
// @description Widget z raportem wygenerowanych opisów i błędów (np. zbyt krótka sesja)
// @author Paweł Piekarski
// @match https://clarity.microsoft.com/*
// @icon https://clarity.microsoft.com/blog/wp-content/uploads/2025/02/siteIcon.png
// @grant none
// ==/UserScript==
(function() {
'use strict';
const clarityIconSvg = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 4L20 12L4 20V4Z" fill="#1960bf"/>
</svg>`;
let statystyki = { pominiete: 0, sukcesy: 0, oczekiwane: 0 };
// --- LOGIKA WYDOBYWANIA DANYCH ---
function pobierzDostepneNagrania() {
const countEl = document.querySelector('.sessionCount strong');
if (!countEl) return '...';
return countEl.textContent.replace(/\D/g, '');
}
function pobierzUserId(card) {
const badges = Array.from(card.querySelectorAll('div[class*="recordings_cardBadge"]'));
const userBadge = badges.find(el => el.textContent.includes('User ID:'));
if (userBadge) {
const rawText = userBadge.textContent.replace('User ID:', '').trim();
return rawText.split(/\s+/)[0];
}
return '';
}
function startProcess() {
const limit = parseInt(document.getElementById('helper-limit').value) || 0;
const cards = Array.from(document.querySelectorAll('div[data-testid="sessionCard"]')).slice(0, limit || undefined);
statystyki = { pominiete: 0, sukcesy: 0, oczekiwane: cards.length };
let index = 0;
updateStatus(`Analizuję sesje...`);
const procesuj = () => {
if (index < cards.length) {
const card = cards[index];
const btn = card.querySelector('button[data-testid="sessionGenerateInsightsButton"]');
if (!btn || btn.disabled) {
statystyki.pominiete++;
} else {
btn.click();
statystyki.sukcesy++;
}
index++;
updateStatus(`Klikanie: ${index}/${cards.length}`);
setTimeout(procesuj, 800);
} else {
nasluchujOpisy();
}
};
procesuj();
}
const BLAD_AI_TEKST = "I'm sorry, something went wrong";
function czyBladAI(card) {
const content = card.querySelector('div.insightsContent');
return content && content.textContent.includes(BLAD_AI_TEKST);
}
function nasluchujOpisy() {
let proba = 0;
const intervalId = setInterval(() => {
proba++;
const cards = Array.from(document.querySelectorAll('div[data-testid="sessionCard"]')).slice(0, statystyki.oczekiwane);
// Liczymy tylko karty z treścią ORAZ bez komunikatu błędu AI
const aktualneSukcesy = cards.filter(card =>
card.querySelector('div.insightsContent') && !czyBladAI(card)
).length;
// Ile już "odpowiedziało" (sukces lub błąd AI) - do detekcji końca
const juzOdpowiedziane = cards.filter(card => card.querySelector('div.insightsContent')).length;
updateStatus(`Opisy: ${aktualneSukcesy}/${statystyki.oczekiwane}`);
if (juzOdpowiedziane === statystyki.sukcesy || proba >= 40) {
clearInterval(intervalId);
pokazRaport(aktualneSukcesy, juzOdpowiedziane);
}
}, 1000);
}
// --- UI & RAPORT ---
function updateStatus(msg) {
document.getElementById('helper-status').textContent = `Status: ${msg}`;
document.getElementById('helper-report').style.display = 'none';
}
function pokazRaport(finalneSukcesy, juzOdpowiedziane) {
const reportEl = document.getElementById('helper-report');
const statusEl = document.getElementById('helper-status');
statusEl.textContent = `Status: Gotowe (${finalneSukcesy}/${statystyki.oczekiwane})`;
const bledyAI = juzOdpowiedziane - finalneSukcesy;
const linie = [];
if (statystyki.pominiete > 0) {
linie.push(`• ${statystyki.pominiete} pominięto — zbyt krótka sesja`);
}
if (bledyAI > 0) {
linie.push(`• ${bledyAI} błąd generowania AI`);
}
if (linie.length > 0) {
reportEl.innerHTML = linie.join('<br>');
reportEl.style.display = 'block';
}
}
function refreshTotalCount() {
const el = document.getElementById('helper-max-val');
if (el) el.textContent = pobierzDostepneNagrania();
}
const container = document.createElement('div');
container.id = 'clarity-helper-widget';
container.innerHTML = `
<div style="background: #ffffff; border: 1px solid #ced4da; border-radius: 10px; padding: 15px; box-shadow: 0 8px 24px rgba(0,0,0,0.2); width: 220px; font-family: sans-serif; color: #333;">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; border-bottom: 1px solid #f0f0f0; padding-bottom: 8px;">
<div style="display: flex; align-items: center;">
<div style="margin-right: 8px; display: flex; align-items: center;">${clarityIconSvg}</div>
<h3 style="margin: 0; font-size: 14px; color: #000;">Ściągnij opisy nagrań</h3>
</div>
<div style="display: flex; align-items: center;">
<button id="btn-toggle" title="Zwiń / Rozwiń" style="background: none; border: none; cursor: pointer; font-size: 14px; color: #888; line-height: 1; padding: 0 2px;">▼</button>
</div>
</div>
<div id="helper-body">
<label style="font-size: 11px; color: #888; font-weight: bold;">LICZBA NAGRAŃ (MAX: <span id="helper-max-val">...</span>):</label>
<input type="number" id="helper-limit" value="10" style="width: 100%; padding: 6px; margin: 4px 0 12px 0; border: 1px solid #ddd; border-radius: 4px; font-size: 13px; box-sizing: border-box;">
<div id="helper-status" style="font-size: 12px; margin-bottom: 4px; color: #1960bf; font-weight: 500; background: #f0f7ff; padding: 4px 8px; border-radius: 4px;">Status: Gotowy</div>
<div id="helper-report" style="font-size: 10px; color: #d93025; margin-bottom: 12px; padding: 0 8px; display: none; line-height: 1.2;"></div>
<button id="btn-generate" style="width: 100%; background: #1960bf; color: white; border: none; padding: 10px; border-radius: 5px; cursor: pointer; font-weight: bold; margin-bottom: 8px; font-size: 12px;">GENERUJ OPISY</button>
<button id="btn-copy" style="width: 100%; background: #444; color: white; border: none; padding: 10px; border-radius: 5px; cursor: pointer; font-weight: bold; font-size: 12px;">KOPIUJ JSON</button>
<div style="margin-top: 15px; text-align: center; border-top: 1px solid #f0f0f0; padding-top: 12px;">
<a href="https://pawelpiekarski.pl" target="_blank" title="Odwiedź stronę">
<img src="https://pawelpiekarski.pl/wp-content/uploads/2024/02/Logo-pawelpiekarski.pl-v850.png" alt="Paweł Piekarski" style="max-width: 130px; height: auto; opacity: 0.8; transition: opacity 0.2s ease-in-out;" onmouseover="this.style.opacity='1'" onmouseout="this.style.opacity='0.8'">
</a>
</div>
</div>
</div>
`;
Object.assign(container.style, { position: 'fixed', bottom: '20px', right: '20px', zIndex: '10000' });
document.body.appendChild(container);
// --- Toggle minimalizacji ---
let zwiniety = false;
document.getElementById('btn-toggle').addEventListener('click', () => {
zwiniety = !zwiniety;
const body = document.getElementById('helper-body');
const toggleBtn = document.getElementById('btn-toggle');
body.style.display = zwiniety ? 'none' : 'block';
toggleBtn.textContent = zwiniety ? '▲' : '▼';
});
document.getElementById('btn-generate').addEventListener('click', startProcess);
document.getElementById('btn-copy').addEventListener('click', () => {
const limit = parseInt(document.getElementById('helper-limit').value) || 0;
const cards = Array.from(document.querySelectorAll('div[data-testid="sessionCard"]')).slice(0, limit || undefined);
const dane = cards.map(card => ({
userId: pobierzUserId(card),
description: (card.querySelector('div.insightsContent') && !czyBladAI(card))
? card.querySelector('div.insightsContent').textContent.trim()
: 'BRAK OPISU'
}));
navigator.clipboard.writeText(JSON.stringify(dane, null, 2)).then(() => {
const btn = document.getElementById('btn-copy');
btn.textContent = "SKOPIOWANO!";
setTimeout(() => btn.textContent = "KOPIUJ JSON", 2000);
});
});
setInterval(refreshTotalCount, 3000);
refreshTotalCount();
})();