DatafetchPro
    May 26, 20265 min read0 views

    How to Export Instagram Comments Easily?

    Need Instagram comment export fast? Try this web scraping and data extraction script for CSV, JSON, and Markdown exports.

    Instagram comments often contain valuable insights, customer feedback, audience questions, and engagement data. Manually copying them takes time, especially on large posts or reels. This is where smart web scraping and data extraction tools become useful.


    The Instagram Comment Exporter Pro script helps collect comments directly from Instagram posts and reels in just a few clicks. It supports CSV, JSON, and Markdown export formats, making it easier to organize data for research, reporting, lead generation, or content analysis.

    The script also detects different Instagram layouts automatically and includes useful details like usernames, likes, replies, timestamps, verified badges, and profile links. A clean floating interface keeps everything simple without interrupting browsing.

    This type of web automation tool is helpful for marketers, researchers, agencies, and creators who regularly analyze engagement or archive social media conversations. Instead of wasting time copying comments manually, the process becomes fast, structured, and reusable.

    // ==UserScript==

    // @name Instagram Comment Exporter Pro

    // @namespace https://github.com/ig-comment-exporter

    // @version 2.0.0

    // @description Export Instagram comments with full stats — CSV, Markdown, JSON. Works across all post view layouts.

    // @author Ali

    // @match https://www.instagram.com/*

    // @grant none

    // @run-at document-idle

    // ==/UserScript==




    (function () {

    'use strict';



    /* ─────────────────────────────────────────────

    CONSTANTS & STATE

    ───────────────────────────────────────────── */

    const ID = 'igce-pro';

    let panelVisible = false;

    let lastExportedData = null;



    /* ─────────────────────────────────────────────

    STYLES

    ───────────────────────────────────────────── */

    function injectStyles() {

    if (document.getElementById(`${ID}-styles`)) return;

    const css = `

    /* ── Floating Trigger Button ── */

    #${ID}-trigger {

    position: fixed;

    bottom: 28px;

    right: 28px;

    z-index: 2147483640;

    width: 52px;

    height: 52px;

    border-radius: 50%;

    background: linear-gradient(135deg, #405de6 0%, #833ab4 40%, #fd1d1d 70%, #fcb045 100%);

    box-shadow: 0 4px 20px rgba(131,58,180,.55);

    border: none;

    cursor: pointer;

    display: flex;

    align-items: center;

    justify-content: center;

    transition: transform .2s ease, box-shadow .2s ease;

    outline: none;

    }

    #${ID}-trigger:hover {

    transform: scale(1.1);

    box-shadow: 0 6px 28px rgba(131,58,180,.75);

    }

    #${ID}-trigger svg { pointer-events: none; }



    /* ── Badge on trigger ── */

    #${ID}-badge {

    position: absolute;

    top: -4px;

    right: -4px;

    background: #ff3b5c;

    color: #fff;

    font-size: 10px;

    font-weight: 700;

    font-family: -apple-system, sans-serif;

    min-width: 18px;

    height: 18px;

    border-radius: 9px;

    display: flex;

    align-items: center;

    justify-content: center;

    padding: 0 4px;

    border: 2px solid #fff;

    box-sizing: border-box;

    opacity: 0;

    transform: scale(0);

    transition: opacity .2s, transform .2s;

    }

    #${ID}-badge.show {

    opacity: 1;

    transform: scale(1);

    }



    /* ── Panel ── */

    #${ID}-panel {

    position: fixed;

    bottom: 92px;

    right: 28px;

    z-index: 2147483639;

    width: 340px;

    background: #fff;

    border-radius: 20px;

    box-shadow: 0 12px 48px rgba(0,0,0,.18), 0 2px 8px rgba(0,0,0,.1);

    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;

    overflow: hidden;

    transform: scale(0.85) translateY(20px);

    transform-origin: bottom right;

    opacity: 0;

    pointer-events: none;

    transition: transform .25s cubic-bezier(.34,1.56,.64,1), opacity .2s ease;

    }

    #${ID}-panel.open {

    transform: scale(1) translateY(0);

    opacity: 1;

    pointer-events: all;

    }



    /* ── Panel Header ── */

    #${ID}-header {

    background: linear-gradient(135deg, #405de6, #833ab4, #fd1d1d);

    padding: 18px 20px 16px;

    color: #fff;

    position: relative;

    }

    #${ID}-header h2 {

    margin: 0 0 2px;

    font-size: 15px;

    font-weight: 700;

    letter-spacing: -.2px;

    }

    #${ID}-header p {

    margin: 0;

    font-size: 12px;

    opacity: .8;

    }

    #${ID}-close {

    position: absolute;

    top: 14px;

    right: 14px;

    background: rgba(255,255,255,.2);

    border: none;

    width: 26px;

    height: 26px;

    border-radius: 50%;

    color: #fff;

    font-size: 14px;

    cursor: pointer;

    display: flex;

    align-items: center;

    justify-content: center;

    transition: background .15s;

    line-height: 1;

    }

    #${ID}-close:hover { background: rgba(255,255,255,.35); }



    /* ── Stats Grid ── */

    #${ID}-stats {

    display: grid;

    grid-template-columns: 1fr 1fr 1fr;

    gap: 1px;

    background: #f0f0f0;

    border-bottom: 1px solid #f0f0f0;

    }

    .${ID}-stat {

    background: #fff;

    padding: 12px 10px;

    text-align: center;

    }

    .${ID}-stat-num {

    font-size: 20px;

    font-weight: 800;

    color: #262626;

    letter-spacing: -.5px;

    line-height: 1.1;

    }

    .${ID}-stat-label {

    font-size: 10px;

    color: #8e8e8e;

    text-transform: uppercase;

    letter-spacing: .4px;

    margin-top: 2px;

    }



    /* ── Scan button ── */

    #${ID}-scan-wrap {

    padding: 14px 16px 10px;

    }

    #${ID}-scan {

    width: 100%;

    padding: 11px;

    border-radius: 12px;

    border: 1.5px dashed #dbdbdb;

    background: #fafafa;

    color: #405de6;

    font-size: 13px;

    font-weight: 600;

    cursor: pointer;

    transition: all .15s;

    display: flex;

    align-items: center;

    justify-content: center;

    gap: 7px;

    font-family: inherit;

    }

    #${ID}-scan:hover {

    background: #f0f0ff;

    border-color: #405de6;

    }

    #${ID}-scan.scanning {

    border-style: solid;

    color: #833ab4;

    animation: ${ID}-pulse 1s infinite;

    }

    @keyframes ${ID}-pulse {

    0%,100% { opacity: 1; }

    50% { opacity: .6; }

    }



    /* ── Progress bar ── */

    #${ID}-progress-wrap {

    padding: 0 16px 10px;

    display: none;

    }

    #${ID}-progress-wrap.show { display: block; }

    #${ID}-progress-bar-bg {

    height: 4px;

    background: #efefef;

    border-radius: 2px;

    overflow: hidden;

    }

    #${ID}-progress-bar {

    height: 100%;

    width: 0%;

    background: linear-gradient(90deg, #405de6, #833ab4);

    border-radius: 2px;

    transition: width .3s ease;

    }

    #${ID}-progress-text {

    font-size: 11px;

    color: #8e8e8e;

    margin-top: 4px;

    text-align: right;

    }



    /* ── Export buttons ── */

    #${ID}-export-wrap {

    padding: 0 16px 16px;

    display: grid;

    grid-template-columns: 1fr 1fr 1fr;

    gap: 8px;

    }

    .${ID}-export-btn {

    padding: 10px 6px;

    border-radius: 12px;

    border: none;

    font-size: 12px;

    font-weight: 700;

    cursor: pointer;

    font-family: inherit;

    display: flex;

    flex-direction: column;

    align-items: center;

    gap: 5px;

    transition: transform .15s, box-shadow .15s, opacity .15s;

    }

    .${ID}-export-btn:disabled { opacity: .4; cursor: default; }

    .${ID}-export-btn:not(:disabled):hover {

    transform: translateY(-2px);

    box-shadow: 0 4px 12px rgba(0,0,0,.15);

    }

    .${ID}-export-btn:not(:disabled):active { transform: translateY(0); }

    .${ID}-btn-csv { background: #e8f5e9; color: #2e7d32; }

    .${ID}-btn-md { background: #ede7f6; color: #4527a0; }

    .${ID}-btn-json { background: #fff3e0; color: #e65100; }

    .${ID}-export-btn svg { width: 20px; height: 20px; }



    /* ── Toast ── */

    #${ID}-toast {

    position: fixed;

    bottom: 90px;

    right: 28px;

    z-index: 2147483641;

    background: #262626;

    color: #fff;

    padding: 10px 16px;

    border-radius: 12px;

    font-family: -apple-system, sans-serif;

    font-size: 13px;

    font-weight: 500;

    box-shadow: 0 4px 20px rgba(0,0,0,.3);

    opacity: 0;

    transform: translateY(8px);

    transition: opacity .2s, transform .2s;

    pointer-events: none;

    max-width: 280px;

    }

    #${ID}-toast.show {

    opacity: 1;

    transform: translateY(0);

    }



    /* ── Comment Preview ── */

    #${ID}-preview {

    max-height: 180px;

    overflow-y: auto;

    margin: 0 16px 16px;

    border: 1px solid #efefef;

    border-radius: 12px;

    background: #fafafa;

    display: none;

    }

    #${ID}-preview.show { display: block; }

    #${ID}-preview::-webkit-scrollbar { width: 4px; }

    #${ID}-preview::-webkit-scrollbar-thumb { background: #dbdbdb; border-radius: 2px; }

    .${ID}-preview-item {

    padding: 8px 12px;

    border-bottom: 1px solid #efefef;

    font-size: 12px;

    line-height: 1.4;

    }

    .${ID}-preview-item:last-child { border-bottom: none; }

    .${ID}-preview-user {

    font-weight: 700;

    color: #262626;

    margin-right: 6px;

    }

    .${ID}-preview-text { color: #444; }

    .${ID}-preview-meta {

    margin-top: 3px;

    font-size: 10px;

    color: #8e8e8e;

    display: flex;

    gap: 8px;

    }



    /* ── Layout detection chip ── */

    #${ID}-layout-chip {

    display: inline-flex;

    align-items: center;

    gap: 4px;

    font-size: 10px;

    font-weight: 600;

    padding: 2px 8px;

    border-radius: 10px;

    background: rgba(255,255,255,.2);

    margin-top: 6px;

    }

    `;

    const el = document.createElement('style');

    el.id = `${ID}-styles`;

    el.textContent = css;

    document.head.appendChild(el);

    }



    /* ─────────────────────────────────────────────

    DOM HELPERS

    ───────────────────────────────────────────── */

    function detectLayout() {

    // Layout 1 & 3: article._aatb (desktop modal with left media)

    if (document.querySelector('article._aatb')) {

    const hasVideo = document.querySelector('article._aatb video');

    return hasVideo ? 'Reel / Video' : 'Photo Post';

    }

    // Layout 2: standalone post page (no article._aatb)

    if (document.querySelector('._aasi._at8n, ._aasi')) {

    return 'Feed / Mobile';

    }

    return 'Unknown';

    }



    function getPostMeta() {

    const meta = { postUrl: location.href, author: '', caption: '', likes: '', datetime: '', layout: detectLayout() };



    // Author — look for profile link with _a6hd or natgeo-style anchor in header

    const authorLink = document.querySelector('header a[role="link"][href*="/"]') ||

    document.querySelector('._aaqw a[role="link"]') ||

    document.querySelector('a.notranslate._a6hd[href]');

    if (authorLink) {

    meta.author = authorLink.textContent.trim() ||

    (authorLink.getAttribute('href') || '').replace(/\//g, '');

    }



    // Caption — class _ap3a or _aaco

    const captionEl = document.querySelector('._ap3a._aaco._aacu._aacx._aad6._aade, h1._ap3a, ._ap3a');

    if (captionEl) meta.caption = captionEl.textContent.trim();



    // Likes

    const likesEl = document.querySelector('span.html-span.x1vvkbs, [role="button"] span.x1vvkbs');

    if (likesEl) meta.likes = likesEl.closest('[role="button"]')?.textContent.trim() || '';



    // datetime from <time>

    const timeEl = document.querySelector('time[datetime]');

    if (timeEl) meta.datetime = timeEl.getAttribute('datetime');



    return meta;

    }



    function scrapeComments() {

    const comments = [];

    const seen = new Set();



    // ── Strategy 1: _a9zj li items (Layout 1 & 3 modal) ──

    const commentItems = document.querySelectorAll('li._a9zj');

    commentItems.forEach(li => {

    const obj = extractFromLi(li, false);

    if (obj && !seen.has(obj.id)) {

    seen.add(obj.id);

    comments.push(obj);



    // Replies inside _a9ym ul

    const repliesUl = li.closest('div')?.querySelectorAll('ul._a9ym li._a9zj, ul._a9ym li._a9zl') || [];

    repliesUl.forEach(rli => {

    const rObj = extractFromLi(rli, true);

    if (rObj && !seen.has(rObj.id)) {

    seen.add(rObj.id);

    comments.push(rObj);

    }

    });

    }

    });



    // ── Strategy 2: Layout 2 flat structure (_aaco _aacu _aacx _aad7) ──

    if (comments.length === 0) {

    document.querySelectorAll('span._ap3a._aaco._aacu._aacx._aad7._aade').forEach((span, i) => {

    const container = span.closest('div') || span.parentElement;

    const userEl = container?.querySelector('a[href*="/"][role="link"]');

    const username = userEl?.textContent.trim() || '';

    const text = span.textContent.trim();

    if (!text || !username) return;



    const timeEl = container?.querySelector('time[datetime]') || container?.parentElement?.querySelector('time[datetime]');

    const datetime = timeEl?.getAttribute('datetime') || '';

    const likesBtn = container?.querySelector('button._a9ze span');

    const likesText = likesBtn?.textContent.trim() || '0 likes';

    const likesCount = parseInt(likesText) || 0;



    const id = `${username}::${text.substring(0, 20)}::${i}`;

    if (!seen.has(id)) {

    seen.add(id);

    comments.push({

    id,

    index: comments.length + 1,

    username,

    profileUrl: `https://www.instagram.com${userEl?.getAttribute('href') || ''}`,

    text,

    likes: likesCount,

    datetime,

    relativeTime: timeEl?.textContent.trim() || '',

    isReply: false,

    replyCount: 0,

    verified: !!container?.querySelector('svg[aria-label="Verified"]'),

    });

    }

    });

    }



    return comments;

    }



    function extractFromLi(li, isReply) {

    try {

    // Username from h3 inside li

    const h3 = li.querySelector('h3 a[role="link"][href], div._a9zr a[role="link"]');

    const username = h3?.textContent.trim() || '';

    const profileUrl = h3 ? `https://www.instagram.com${h3.getAttribute('href') || ''}` : '';



    // Comment text

    const textEl = li.querySelector('span._ap3a._aaco._aacu._aacx._aad7._aade, span._ap3a');

    const text = textEl?.textContent.trim() || '';



    if (!username && !text) return null;



    // Time

    const timeEl = li.querySelector('time[datetime], time._a9ze._a9zf');

    const datetime = timeEl?.getAttribute('datetime') || '';

    const relativeTime = timeEl?.textContent.trim() || '';



    // Likes on this comment

    const likesBtns = li.querySelectorAll('button._a9ze span');

    let likesCount = 0;

    likesBtns.forEach(btn => {

    const m = btn.textContent.match(/^(\d[\d,]*)\s*like/i);

    if (m) likesCount = parseInt(m[1].replace(/,/g, '')) || 0;

    });



    // Reply count

    const replyBtn = li.closest('li')?.nextElementSibling?.querySelector('span._a9yi');

    const replyCount = replyBtn ? (parseInt(replyBtn.textContent.match(/\d+/)?.[0]) || 0) : 0;



    // Verified

    const verified = !!li.querySelector('svg[aria-label="Verified"]');



    const id = `${username}::${text.substring(0, 30)}::${datetime}`;



    return {

    id,

    index: 0, // assigned after

    username: username || '(unknown)',

    profileUrl,

    text,

    likes: likesCount,

    datetime,

    relativeTime,

    isReply,

    replyCount,

    verified,

    };

    } catch (_) {

    return null;

    }

    }



    /* ─────────────────────────────────────────────

    EXPORT FORMATTERS

    ───────────────────────────────────────────── */

    function toCSV(meta, comments) {

    const escape = v => `"${String(v ?? '').replace(/"/g, '""')}"`;

    const header = ['#','Username','Verified','Text','Likes','Datetime','Relative Time','Is Reply','Reply Count','Profile URL'];

    const rows = comments.map(c => [

    c.index, c.username, c.verified ? 'Yes' : 'No',

    c.text, c.likes, c.datetime, c.relativeTime,

    c.isReply ? 'Yes' : 'No', c.replyCount, c.profileUrl

    ].map(escape).join(','));



    const metaBlock = [

    `# Instagram Comment Export`,

    `# Post: ${meta.postUrl}`,

    `# Author: ${meta.author}`,

    `# Layout: ${meta.layout}`,

    `# Likes: ${meta.likes}`,

    `# Datetime: ${meta.datetime}`,

    `# Exported: ${new Date().toISOString()}`,

    `# Total Comments: ${comments.length}`,

    ``,

    ].join('\n');



    return metaBlock + [header.map(escape).join(','), ...rows].join('\n');

    }



    function toMarkdown(meta, comments) {

    const esc = v => String(v ?? '').replace(/\|/g, '\\|');

    const lines = [

    `# 📸 Instagram Comment Export`,

    ``,

    `| Field | Value |`,

    `|-------|-------|`,

    `| **Post URL** | ${meta.postUrl} |`,

    `| **Author** | @${meta.author} |`,

    `| **Layout** | ${meta.layout} |`,

    `| **Likes** | ${meta.likes} |`,

    `| **Post Date** | ${meta.datetime} |`,

    `| **Export Date** | ${new Date().toISOString()} |`,

    `| **Total Comments** | ${comments.length} |`,

    ``,

    `## 💬 Comments`,

    ``,

    `| # | Username | ✓ | Comment | ❤️ Likes | Time | Reply? | Replies |`,

    `|---|----------|---|---------|----------|------|--------|---------|`,

    ...comments.map(c =>

    `| ${c.index} | [@${esc(c.username)}](${c.profileUrl}) | ${c.verified ? '✅' : ''} | ${esc(c.text)} | ${c.likes} | ${esc(c.relativeTime || c.datetime)} | ${c.isReply ? '↩️' : ''} | ${c.replyCount > 0 ? c.replyCount : ''} |`

    ),

    ``,

    `---`,

    `*Exported with Instagram Comment Exporter Pro*`,

    ];

    return lines.join('\n');

    }



    function toJSON(meta, comments) {

    const totalLikes = comments.reduce((s, c) => s + c.likes, 0);

    const verified = comments.filter(c => c.verified).length;

    const replies = comments.filter(c => c.isReply).length;

    const topComment = comments.slice().sort((a, b) => b.likes - a.likes)[0];



    return JSON.stringify({

    exportMeta: {

    tool: 'Instagram Comment Exporter Pro',

    exportedAt: new Date().toISOString(),

    totalComments: comments.length,

    },

    postInfo: {

    url: meta.postUrl,

    author: meta.author,

    caption: meta.caption,

    likes: meta.likes,

    datetime: meta.datetime,

    layout: meta.layout,

    },

    stats: {

    totalComments: comments.length,

    topLevelComments: comments.length - replies,

    replies,

    totalLikesOnComments: totalLikes,

    verifiedCommenters: verified,

    avgLikesPerComment: comments.length ? (totalLikes / comments.length).toFixed(2) : 0,

    topComment: topComment ? { username: topComment.username, text: topComment.text, likes: topComment.likes } : null,

    },

    comments: comments.map(({ id: _id, ...rest }) => rest),

    }, null, 2);

    }



    /* ─────────────────────────────────────────────

    DOWNLOAD

    ───────────────────────────────────────────── */

    function download(content, filename, mime) {

    const blob = new Blob([content], { type: mime });

    const url = URL.createObjectURL(blob);

    const a = document.createElement('a');

    a.href = url;

    a.download = filename;

    document.body.appendChild(a);

    a.click();

    document.body.removeChild(a);

    setTimeout(() => URL.revokeObjectURL(url), 1000);

    }



    function slugDate() {

    return new Date().toISOString().slice(0, 10);

    }



    /* ─────────────────────────────────────────────

    TOAST

    ───────────────────────────────────────────── */

    function showToast(msg, duration = 2800) {

    let toast = document.getElementById(`${ID}-toast`);

    toast.textContent = msg;

    toast.classList.add('show');

    setTimeout(() => toast.classList.remove('show'), duration);

    }



    /* ─────────────────────────────────────────────

    UI BUILD

    ───────────────────────────────────────────── */

    function buildUI() {

    if (document.getElementById(`${ID}-trigger`)) return;



    // Toast

    const toast = document.createElement('div');

    toast.id = `${ID}-toast`;

    document.body.appendChild(toast);



    // Trigger button

    const trigger = document.createElement('button');

    trigger.id = `${ID}-trigger`;

    trigger.title = 'Instagram Comment Exporter Pro';

    trigger.innerHTML = `

    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="white" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">

    <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>

    <line x1="9" y1="10" x2="15" y2="10"/>

    <line x1="9" y1="14" x2="12" y2="14"/>

    </svg>

    <span id="${ID}-badge"></span>

    `;

    document.body.appendChild(trigger);



    // Panel

    const panel = document.createElement('div');

    panel.id = `${ID}-panel`;

    panel.innerHTML = `

    <div id="${ID}-header">

    <h2>Comment Exporter Pro</h2>

    <p>Scrape &amp; export all comments with stats</p>

    <div id="${ID}-layout-chip">

    <svg width="9" height="9" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="12" r="10"/></svg>

    <span id="${ID}-layout-name">Detecting layout…</span>

    </div>

    <button id="${ID}-close" title="Close">✕</button>

    </div>



    <div id="${ID}-stats">

    <div class="${ID}-stat">

    <div class="${ID}-stat-num" id="${ID}-s-count">–</div>

    <div class="${ID}-stat-label">Comments</div>

    </div>

    <div class="${ID}-stat">

    <div class="${ID}-stat-num" id="${ID}-s-likes">–</div>

    <div class="${ID}-stat-label">Total Likes</div>

    </div>

    <div class="${ID}-stat">

    <div class="${ID}-stat-num" id="${ID}-s-replies">–</div>

    <div class="${ID}-stat-label">Replies</div>

    </div>

    </div>



    <div id="${ID}-scan-wrap">

    <button id="${ID}-scan">

    <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">

    <circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>

    </svg>

    Scan Comments

    </button>

    </div>



    <div id="${ID}-progress-wrap">

    <div id="${ID}-progress-bar-bg"><div id="${ID}-progress-bar"></div></div>

    <div id="${ID}-progress-text">Scanning…</div>

    </div>



    <div id="${ID}-preview"></div>



    <div id="${ID}-export-wrap">

    <button class="${ID}-export-btn ${ID}-btn-csv" id="${ID}-dl-csv" disabled>

    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>

    CSV

    </button>

    <button class="${ID}-export-btn ${ID}-btn-md" id="${ID}-dl-md" disabled>

    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg>

    Markdown

    </button>

    <button class="${ID}-export-btn ${ID}-btn-json" id="${ID}-dl-json" disabled>

    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>

    JSON

    </button>

    </div>

    `;

    document.body.appendChild(panel);



    /* ── Event listeners ── */

    trigger.addEventListener('click', () => {

    panelVisible = !panelVisible;

    panel.classList.toggle('open', panelVisible);

    if (panelVisible) refreshLayoutChip();

    });



    document.getElementById(`${ID}-close`).addEventListener('click', () => {

    panelVisible = false;

    panel.classList.remove('open');

    });



    document.getElementById(`${ID}-scan`).addEventListener('click', runScan);



    document.getElementById(`${ID}-dl-csv`).addEventListener('click', () => {

    if (!lastExportedData) return;

    const { meta, comments } = lastExportedData;

    download(toCSV(meta, comments), `ig-comments-${slugDate()}.csv`, 'text/csv');

    showToast(`✅ Exported ${comments.length} comments as CSV`);

    });



    document.getElementById(`${ID}-dl-md`).addEventListener('click', () => {

    if (!lastExportedData) return;

    const { meta, comments } = lastExportedData;

    download(toMarkdown(meta, comments), `ig-comments-${slugDate()}.md`, 'text/markdown');

    showToast(`✅ Exported ${comments.length} comments as Markdown`);

    });



    document.getElementById(`${ID}-dl-json`).addEventListener('click', () => {

    if (!lastExportedData) return;

    const { meta, comments } = lastExportedData;

    download(toJSON(meta, comments), `ig-comments-${slugDate()}.json`, 'application/json');

    showToast(`✅ Exported ${comments.length} comments as JSON`);

    });

    }



    /* ─────────────────────────────────────────────

    LAYOUT CHIP

    ───────────────────────────────────────────── */

    function refreshLayoutChip() {

    const chip = document.getElementById(`${ID}-layout-name`);

    if (chip) chip.textContent = detectLayout();

    }



    /* ─────────────────────────────────────────────

    SCAN LOGIC (with simulated progress)

    ───────────────────────────────────────────── */

    async function runScan() {

    const scanBtn = document.getElementById(`${ID}-scan`);

    const progressWrap = document.getElementById(`${ID}-progress-wrap`);

    const progressBar = document.getElementById(`${ID}-progress-bar`);

    const progressText = document.getElementById(`${ID}-progress-text`);



    scanBtn.textContent = '⏳ Scanning…';

    scanBtn.classList.add('scanning');

    scanBtn.disabled = true;

    progressWrap.classList.add('show');



    // Animate progress

    let pct = 0;

    const tick = setInterval(() => {

    pct = Math.min(pct + Math.random() * 12, 90);

    progressBar.style.width = pct + '%';

    progressText.textContent = `Scanning page… ${Math.round(pct)}%`;

    }, 80);



    await new Promise(r => setTimeout(r, 700));



    let comments = scrapeComments();

    // Assign real index

    comments = comments.map((c, i) => ({ ...c, index: i + 1 }));



    const meta = getPostMeta();

    lastExportedData = { meta, comments };



    clearInterval(tick);

    progressBar.style.width = '100%';

    progressText.textContent = `✅ Found ${comments.length} comments`;



    setTimeout(() => progressWrap.classList.remove('show'), 2000);



    // Update stats

    const totalLikes = comments.reduce((s, c) => s + c.likes, 0);

    const replies = comments.filter(c => c.isReply).length;

    document.getElementById(`${ID}-s-count`).textContent = comments.length;

    document.getElementById(`${ID}-s-likes`).textContent = totalLikes > 999 ? (totalLikes / 1000).toFixed(1) + 'k' : totalLikes;

    document.getElementById(`${ID}-s-replies`).textContent = replies;



    // Badge

    const badge = document.getElementById(`${ID}-badge`);

    badge.textContent = comments.length > 99 ? '99+' : comments.length;

    badge.classList.toggle('show', comments.length > 0);



    // Preview

    renderPreview(comments);



    // Enable export buttons

    ['csv', 'md', 'json'].forEach(f => {

    document.getElementById(`${ID}-dl-${f}`).disabled = comments.length === 0;

    });



    scanBtn.innerHTML = `

    <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">

    <polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/>

    <path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>

    </svg>

    Re-scan (${comments.length} found)

    `;

    scanBtn.classList.remove('scanning');

    scanBtn.disabled = false;



    if (comments.length === 0) {

    showToast('⚠️ No comments found. Try scrolling the comments section first.', 4000);

    } else {

    showToast(`🎉 Found ${comments.length} comments! Ready to export.`);

    }

    }



    function renderPreview(comments) {

    const preview = document.getElementById(`${ID}-preview`);

    if (comments.length === 0) {

    preview.classList.remove('show');

    return;

    }

    preview.classList.add('show');

    const slice = comments.slice(0, 8);

    preview.innerHTML = slice.map(c => `

    <div class="${ID}-preview-item">

    <span class="${ID}-preview-user">${escHtml(c.username)}${c.verified ? ' ✅' : ''}</span>

    <span class="${ID}-preview-text">${escHtml(c.text.slice(0, 80))}${c.text.length > 80 ? '…' : ''}</span>

    <div class="${ID}-preview-meta">

    ${c.likes ? `<span>❤️ ${c.likes}</span>` : ''}

    ${c.relativeTime ? `<span>🕐 ${c.relativeTime}</span>` : ''}

    ${c.isReply ? '<span>↩️ reply</span>' : ''}

    </div>

    </div>

    `).join('') + (comments.length > 8 ? `<div class="${ID}-preview-item" style="text-align:center;color:#8e8e8e">…and ${comments.length - 8} more</div>` : '');

    }



    function escHtml(str) {

    return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');

    }



    /* ─────────────────────────────────────────────

    INIT & PAGE CHANGE OBSERVER

    ───────────────────────────────────────────── */

    function onPageChange() {

    // Reset state when navigating

    lastExportedData = null;

    const badge = document.getElementById(`${ID}-badge`);

    if (badge) badge.classList.remove('show');

    const scanBtn = document.getElementById(`${ID}-scan`);

    if (scanBtn) {

    scanBtn.innerHTML = `

    <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">

    <circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>

    </svg>

    Scan Comments

    `;

    }

    ['csv', 'md', 'json'].forEach(f => {

    const btn = document.getElementById(`${ID}-dl-${f}`);

    if (btn) btn.disabled = true;

    });

    ['count','likes','replies'].forEach(k => {

    const el = document.getElementById(`${ID}-s-${k}`);

    if (el) el.textContent = '–';

    });

    const preview = document.getElementById(`${ID}-preview`);

    if (preview) preview.classList.remove('show');

    refreshLayoutChip();

    }



    function init() {

    injectStyles();

    buildUI();



    // SPA navigation: watch for URL change

    let lastUrl = location.href;

    new MutationObserver(() => {

    if (location.href !== lastUrl) {

    lastUrl = location.href;

    onPageChange();

    }

    }).observe(document.body, { childList: true, subtree: true });

    }



    if (document.readyState === 'loading') {

    document.addEventListener('DOMContentLoaded', init);

    } else {

    init();

    }



    })();
    0