{
  "type": "script",
  "enabled": true,
  "name": "infoblock-panel",
  "id": "c0321417-2a1d-4257-84bc-162944564289",
  "content": "if (!window._ib2CleanEmpty) {\n    window._ib2CleanEmpty = function(html) {\n        return html\n            .replace(/<p>\\s*(<br\\s*\\/?>)?\\s*<\\/p>/gi, '')\n            .replace(/^(\\s*<br\\s*\\/?>)+/gi, '')\n            .replace(/(\\s*<br\\s*\\/?>)+$/gi, '')\n            .replace(/(<br\\s*\\/?>(\\s*)){3,}/gi, '<br>');\n    };\n}\n\n(function() {\n    const $ = window.jQuery;\n\n    const MOD = {\n        promptId: 'ib2_panel_system',\n        cssId: 'ib2-panel-styles',\n        expirationDepth: 8,\n        contextCleanDepth: 3,\n\n        systemPrompt:\n`At the VERY START of every reply, BEFORE any narrative text, output a machine-readable data block inside <ib2_data> tags.\nThis must be the FIRST thing in your message, on the very first line.\n\n<ib2_data>\nSCENE|date=...|weather=...|location=...\nTIMELINE|days=N|since=...|event=...|ago=N\nCHAR|gender=m or f|name=...|role=...|state=...|outfit=...|where=...\nREL|from=...|to=...|reaction=...|mask=...|body=...\n</ib2_data>\n\nThen write your narrative response after.\n\nRules:\n- Place <ib2_data> block at the TOP of your reply, before all other text.\n- One CHAR line per active character in the scene (speaks, acts, or affects scene this beat). Do NOT include {{user}}, only NPCs and {{char}}.\n- Only ONE REL line: from={{char}} to={{user}}. Never write REL from {{user}} to {{char}}.\n- body= field in REL is optional, include only if psychosexual toggle is on. Otherwise end at mask=.\n- TIMELINE.since means how they relate (met, started dating, etc). Omit entire TIMELINE line if not applicable.\n- All values in the same language as the narrative.\n- Do NOT write ---, ***, or any separator line between </ib2_data> and the narrative. Start prose immediately after the closing tag.\n- Keep your existing markdown infoblock in the reply as is. The <ib2_data> is an additional machine-readable mirror.`,\n\n        updatePrompt: '[System Note: Start your reply with <ib2_data>...</ib2_data> block BEFORE any text. Same format. Only {{char}} to {{user}} REL, no reverse. No {{user}} in CHAR lines. No --- or *** after </ib2_data>.]',\n\n        mainRegex: /<ib2_data>([\\s\\S]*?)<\\/ib2_data>/i,\n\n        rawLinePatterns: [\n            /^(SCENE|CHAR|REL|TIMELINE)\\s*\\|/i,\n            /\\|mask=/i,\n            /\\|body=/i,\n            /\\|weather=/i,\n            /\\|outfit=/i,\n            /\\|state=/i,\n            /\\|where=/i,\n            /\\|since=/i,\n        ],\n        pipeKvPattern: /\\|\\w+=/,\n    };\n\n    let TH = null;\n    const _rendering = {};\n    let _contextCleaned = {};\n    const _observers = {};\n\n    function esc(s) { return (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }\n\n    function parseBlock(raw) {\n        const data = { scene: null, timeline: null, chars: [], rels: [] };\n        const lines = raw.split('\\n').map(function(l){ return l.trim(); }).filter(Boolean);\n        for (var i = 0; i < lines.length; i++) {\n            var line = lines[i];\n            var pipeIdx = line.indexOf('|');\n            if (pipeIdx < 0) continue;\n            var type = line.slice(0, pipeIdx).trim().toUpperCase();\n            var rest = line.slice(pipeIdx + 1);\n            var kv = {};\n            rest.split('|').forEach(function(pair) {\n                var eq = pair.indexOf('=');\n                if (eq < 0) return;\n                var k = pair.slice(0, eq).trim().toLowerCase();\n                var v = pair.slice(eq + 1).trim();\n                if (k && v) kv[k] = v;\n            });\n            if (type === 'SCENE')         data.scene = kv;\n            else if (type === 'TIMELINE') data.timeline = kv;\n            else if (type === 'CHAR')     data.chars.push(kv);\n            else if (type === 'REL')      data.rels.push(kv);\n        }\n        return data;\n    }\n\n    function hasData(data) {\n        return data.scene || data.chars.length || data.rels.length;\n    }\n\n    function looksLikeRawData(txt) {\n        if (!txt) return false;\n        for (var i = 0; i < MOD.rawLinePatterns.length; i++) {\n            if (MOD.rawLinePatterns[i].test(txt)) return true;\n        }\n        return false;\n    }\n\n    function looksLikePipeKv(txt) {\n        if (!txt) return false;\n        var pipes = (txt.match(/\\|/g) || []).length;\n        var eqs = (txt.match(/\\w+=/g) || []).length;\n        return pipes >= 2 && eqs >= 2 && MOD.pipeKvPattern.test(txt);\n    }\n\n    function nukeIb2FromDom($target) {\n        if (!$target || !$target.length) return;\n        var html = $target.html();\n        if (!html) return;\n        var orig = html;\n\n        html = html.replace(/(?:<|&lt;)\\s*ib2_data\\s*(?:>|&gt;)([\\s\\S]*?)(?:<|&lt;)\\s*\\/\\s*ib2_data\\s*(?:>|&gt;)/gi, '');\n        html = html.replace(/(?:<|&lt;)\\s*\\/?\\s*ib2_data\\s*(?:>|&gt;)/gi, '');\n        html = html.replace(/^[\\s]*(?:SCENE|CHAR|REL|TIMELINE)\\s*\\|[^\\n<]*$/gmi, '');\n        html = html.replace(/>[\\s]*(?:SCENE|CHAR|REL|TIMELINE)\\s*\\|[^<]*/gi, '>');\n        // strip stray separator lines right after where block was\n        html = html.replace(/<p>\\s*(?:---+|\\*\\*\\*+)\\s*<\\/p>/gi, '');\n        html = html.replace(/<hr\\s*\\/?>/gi, '');\n\n        if (html !== orig) {\n            html = window._ib2CleanEmpty ? window._ib2CleanEmpty(html) : html;\n            $target.html(html.trim());\n        }\n\n        $target.find('p, div, span, li, code, pre, blockquote, em, strong, i, b').each(function() {\n            var $el = $(this);\n            if ($el.closest('.p2-wrap').length) return;\n            var txt = $el.text().trim();\n            if (/ib2_data/i.test(txt) && txt.length < 20) {\n                $el.remove(); return;\n            }\n            if (looksLikeRawData(txt)) {\n                $el.remove(); return;\n            }\n            if ($el.is('blockquote, p') && looksLikePipeKv(txt) && txt.length < 500) {\n                $el.remove(); return;\n            }\n            // remove orphan separator paragraphs\n            if ($el.is('p') && /^(?:---+|\\*\\*\\*+)$/.test(txt)) {\n                $el.remove(); return;\n            }\n        });\n\n        html = $target.html();\n        if (html) {\n            var cleaned = window._ib2CleanEmpty ? window._ib2CleanEmpty(html) : html;\n            if (cleaned !== html) $target.html(cleaned.trim());\n        }\n    }\n\n    function startStreamObserver(messageId) {\n        stopStreamObserver(messageId);\n        var $mes = TH.retrieveDisplayedMessage(messageId);\n        if (!$mes || !$mes.length) return;\n        var $target = $mes.find('.mes_text');\n        if (!$target.length) return;\n        var node = $target[0];\n\n        var observer = new MutationObserver(function() {\n            $target.find('p, div, span, li, code, pre, blockquote, em, strong, i, b').each(function() {\n                var $el = $(this);\n                if ($el.closest('.p2-wrap').length) return;\n                if ($el.data('p2hid')) return;\n                var txt = $el.text().trim();\n                if (/ib2_data/i.test(txt)) {\n                    $el.css('display','none'); $el.data('p2hid', true); return;\n                }\n                if (looksLikeRawData(txt)) {\n                    $el.css('display','none'); $el.data('p2hid', true); return;\n                }\n                if (looksLikePipeKv(txt) && txt.length < 500) {\n                    $el.css('display','none'); $el.data('p2hid', true); return;\n                }\n            });\n\n            var walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, null, false);\n            var n;\n            while (n = walker.nextNode()) {\n                if ($(n).closest('.p2-wrap').length) continue;\n                var t = n.textContent.trim();\n                if (looksLikeRawData(t) || (/ib2_data/i.test(t))) {\n                    if (!n._p2wrap) {\n                        var sp = document.createElement('span');\n                        sp.style.display = 'none';\n                        sp.className = 'p2-stream-hide';\n                        n.parentNode.insertBefore(sp, n);\n                        sp.appendChild(n);\n                        n._p2wrap = true;\n                    }\n                }\n            }\n        });\n\n        observer.observe(node, { childList: true, subtree: true, characterData: true });\n        _observers[messageId] = observer;\n    }\n\n    function stopStreamObserver(messageId) {\n        if (_observers[messageId]) {\n            _observers[messageId].disconnect();\n            delete _observers[messageId];\n        }\n    }\n\n    async function cleanContext() {\n        if (!TH) return;\n        var lastId = TH.getLastMessageId();\n        if (lastId < 0) return;\n        var msgs = TH.getChatMessages('0-' + lastId);\n        if (!msgs) return;\n        var updates = [];\n        for (var i = 0; i < msgs.length; i++) {\n            var msg = msgs[i];\n            if (!msg.message) continue;\n            var age = lastId - msg.message_id;\n            if (age < MOD.contextCleanDepth) continue;\n            if (_contextCleaned[msg.message_id]) continue;\n            var m = msg.message.match(MOD.mainRegex);\n            if (!m) continue;\n            var newMsg = msg.message.replace(MOD.mainRegex, '').trim();\n            if (newMsg !== msg.message) {\n                updates.push({ message_id: msg.message_id, message: newMsg });\n                _contextCleaned[msg.message_id] = true;\n            }\n        }\n        if (updates.length > 0) {\n            await TH.setChatMessages(updates, { refresh: 'none' });\n            console.log('[IB2] context cleaned: ' + updates.length + ' msgs');\n        }\n    }\n\n    function gIcon(g) {\n        g = (g || '').toLowerCase();\n        if (g === 'f' || g === 'ж' || g === 'female') return '\\u2640';\n        if (g === 'm' || g === 'м' || g === 'male')   return '\\u2642';\n        return '\\u25CF';\n    }\n\n    function buildScene(s) {\n        if (!s) return '';\n        var cells = '';\n        if (s.date)     cells += '<div class=\"p2-cell\"><span class=\"p2-lbl\">Дата</span><span class=\"p2-val\">' + esc(s.date) + '</span></div>';\n        if (s.weather)  cells += '<div class=\"p2-cell\"><span class=\"p2-lbl\">Погода</span><span class=\"p2-val\">' + esc(s.weather) + '</span></div>';\n        if (s.location) cells += '<div class=\"p2-cell p2-cell--wide\"><span class=\"p2-lbl\">Место</span><span class=\"p2-val\">' + esc(s.location) + '</span></div>';\n        return '<div class=\"p2-sec p2-scene\">'\n            + '<div class=\"p2-sec-head p2-c-scene\">ВРЕМЯ</div>'\n            + '<div class=\"p2-scene-grid\">' + cells + '</div>'\n            + '</div>';\n    }\n\n    function buildTimeline(t) {\n        if (!t) return '';\n        var parts = [];\n        if (t.days && t.since) parts.push('Дней ' + esc(t.days) + ' \\u2014 ' + esc(t.since));\n        else if (t.days) parts.push('Дней ' + esc(t.days));\n        if (t.event) {\n            var ev = esc(t.event);\n            if (t.ago && t.ago !== '0') ev += ', ' + esc(t.ago) + ' дн. назад';\n            parts.push(ev);\n        }\n        if (!parts.length) return '';\n        return '<div class=\"p2-timeline\">' + parts.map(function(p){ return '<span class=\"p2-tl-row\">' + p + '</span>'; }).join('') + '</div>';\n    }\n\n    function buildChar(c) {\n        var rows = '';\n        if (c.state)  rows += '<div class=\"p2-ck\">Состояние</div><div class=\"p2-cv\">' + esc(c.state) + '</div>';\n        if (c.outfit) rows += '<div class=\"p2-ck\">Одежда</div><div class=\"p2-cv\">' + esc(c.outfit) + '</div>';\n        if (c.where)  rows += '<div class=\"p2-ck\">Где</div><div class=\"p2-cv\">' + esc(c.where) + '</div>';\n        return '<div class=\"p2-char\">'\n            + '<div class=\"p2-char-name\">'\n            +   '<span class=\"p2-gender\">' + gIcon(c.gender) + '</span>'\n            +   '<span class=\"p2-name\">' + esc(c.name || '?') + '</span>'\n            +   (c.role ? '<span class=\"p2-role\">' + esc(c.role) + '</span>' : '')\n            + '</div>'\n            + (rows ? '<div class=\"p2-char-grid\">' + rows + '</div>' : '')\n            + '</div>';\n    }\n\n    function buildRel(r) {\n        var rows = '';\n        if (r.reaction) rows += '<div class=\"p2-ck\">Реакция</div><div class=\"p2-cv p2-italic\">\\u00ab' + esc(r.reaction) + '\\u00bb</div>';\n        if (r.mask)     rows += '<div class=\"p2-ck\">Маска</div><div class=\"p2-cv\">' + esc(r.mask) + '</div>';\n        if (r.body)     rows += '<div class=\"p2-ck\">Тело</div><div class=\"p2-cv p2-body-val\">' + esc(r.body) + '</div>';\n        return '<div class=\"p2-rel\">'\n            + '<div class=\"p2-rel-pair\">'\n            +   '<span class=\"p2-from\">' + esc(r.from || '?') + '</span>'\n            +   '<span class=\"p2-arrow\">\\u2933</span>'\n            +   '<span class=\"p2-to\">' + esc(r.to || '?') + '</span>'\n            + '</div>'\n            + (rows ? '<div class=\"p2-char-grid\">' + rows + '</div>' : '')\n            + '</div>';\n    }\n\n    function buildWidget(data) {\n        var names = data.chars.map(function(c){ return c.name || '?'; }).join(', ');\n        var inner = '';\n        inner += buildScene(data.scene);\n        inner += buildTimeline(data.timeline);\n        if (data.chars.length) {\n            inner += '<div class=\"p2-sec\">'\n                + '<div class=\"p2-sec-head p2-c-status\">СТАТУС</div>'\n                + data.chars.map(buildChar).join('')\n                + '</div>';\n        }\n        if (data.rels.length) {\n            inner += '<div class=\"p2-sec\">'\n                + '<div class=\"p2-sec-head p2-c-rel\">ОТНОШЕНИЕ</div>'\n                + data.rels.map(buildRel).join('')\n                + '</div>';\n        }\n        return '<details class=\"p2-wrap\">'\n            + '<summary class=\"p2-toggle\">'\n            +   '<span class=\"p2-badge\">ИНФО</span>'\n            +   '<span class=\"p2-toggle-names\">' + esc(names) + '</span>'\n            + '</summary>'\n            + '<div class=\"p2-content\">' + inner + '</div>'\n            + '</details>';\n    }\n\n    function getLastState() {\n        var lastId = TH.getLastMessageId();\n        if (lastId < 1) return null;\n        var msgs = TH.getChatMessages('0-' + (lastId - 1));\n        if (!msgs) return null;\n        for (var i = msgs.length - 1; i >= 0; i--) {\n            if (msgs[i].message && MOD.mainRegex.test(msgs[i].message)) return true;\n        }\n        return null;\n    }\n\n    function injectPrompt(has) {\n        TH.injectPrompts([{\n            id: MOD.promptId, position: 'in_chat', depth: 0, role: 'system',\n            content: has ? MOD.updatePrompt : MOD.systemPrompt, should_scan: false,\n        }], { once: true });\n    }\n\n    function renderVisual(messageId) {\n        if (!TH) return;\n        if (_rendering[messageId]) return;\n        _rendering[messageId] = true;\n        setTimeout(function(){ delete _rendering[messageId]; }, 800);\n\n        var msgs = TH.getChatMessages(messageId);\n        if (!msgs || !msgs.length || !msgs[0].message) return;\n        var raw = msgs[0].message;\n        var m = raw.match(MOD.mainRegex);\n        if (!m) return;\n\n        var data = parseBlock(m[1]);\n        if (!hasData(data)) return;\n\n        var $mes = TH.retrieveDisplayedMessage(messageId);\n        if (!$mes || !$mes.length) return;\n        var $target = $mes.find('.mes_text').length ? $mes.find('.mes_text') : $mes;\n        var lastId = TH.getLastMessageId();\n        var expired = (lastId - messageId) >= MOD.expirationDepth;\n\n        stopStreamObserver(messageId);\n\n        $target.find('.p2-wrap').remove();\n        $target.find('.p2-stream-hide').each(function(){\n            var $sh = $(this);\n            $sh.children().unwrap();\n            $sh.remove();\n        });\n\n        nukeIb2FromDom($target);\n\n        if (!expired) {\n            $target.prepend(buildWidget(data));\n        }\n    }\n\n    MOD.css = `\n.p2-wrap {\n    --p2-fg: var(--SmartThemeBodyColor, rgba(255,255,255,0.85));\n    --p2-bg: var(--SmartThemeBlurTintColor, rgba(20,20,20,0.65));\n    --p2-border: var(--SmartThemeBorderColor, rgba(255,255,255,0.10));\n    --p2-quote: var(--SmartThemeQuoteColor, #957C62);\n    --p2-em: var(--SmartThemeEmColor, #669E85);\n    --p2-accent3: #AB274F;\n    display: flex; flex-direction: column;\n    font-family: var(--mainFontFamily, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);\n    border-radius: 10px; overflow: hidden;\n    border: 1px solid color-mix(in srgb, var(--p2-fg) 10%, transparent);\n    background: color-mix(in srgb, var(--p2-fg) 3%, var(--p2-bg));\n    margin-bottom: 12px;\n    animation: p2-slide .4s cubic-bezier(.16,1,.3,1) both;\n    backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);\n}\n@keyframes p2-slide {\n    from { opacity:0; transform:translateY(-6px) }\n    to   { opacity:1; transform:translateY(0) }\n}\n.p2-toggle {\n    display: flex; align-items: center; gap: 10px;\n    padding: 9px 14px; cursor: pointer; user-select: none; list-style: none;\n    background: linear-gradient(135deg,\n        color-mix(in srgb, var(--p2-quote) 12%, transparent),\n        color-mix(in srgb, var(--p2-em) 6%, transparent));\n    transition: background .2s;\n}\n.p2-toggle::-webkit-details-marker { display:none }\n.p2-toggle:hover { background: color-mix(in srgb, var(--p2-fg) 6%, transparent) }\n.p2-badge {\n    font-size: 9px; font-weight: 800; letter-spacing: .22em;\n    color: var(--p2-quote); text-transform: uppercase;\n}\n.p2-toggle-names {\n    flex: 1; text-align: right; font-size: 10px;\n    color: color-mix(in srgb, var(--p2-fg) 45%, transparent);\n    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n}\n.p2-content { padding: 0; }\n.p2-sec {\n    padding: 10px 14px;\n    border-top: 1px solid color-mix(in srgb, var(--p2-fg) 6%, transparent);\n}\n.p2-sec:first-child { border-top: none }\n.p2-sec-head {\n    font-size: 9px; font-weight: 800; letter-spacing: .25em;\n    text-transform: uppercase; margin-bottom: 8px;\n}\n.p2-c-scene  { color: var(--p2-quote) }\n.p2-c-status { color: var(--p2-em) }\n.p2-c-rel    { color: var(--p2-accent3) }\n.p2-scene-grid {\n    display: grid; grid-template-columns: 1fr 1fr; gap: 6px;\n}\n.p2-cell {\n    display: flex; flex-direction: column; gap: 2px;\n    padding: 6px 8px;\n    background: color-mix(in srgb, var(--p2-fg) 3%, transparent);\n    border-radius: 6px;\n}\n.p2-cell--wide { grid-column: 1 / -1 }\n.p2-lbl {\n    font-size: 8px; font-weight: 700; letter-spacing: .14em;\n    text-transform: uppercase;\n    color: color-mix(in srgb, var(--p2-fg) 35%, transparent);\n}\n.p2-val {\n    font-size: 11px;\n    color: color-mix(in srgb, var(--p2-fg) 80%, transparent);\n    line-height: 1.4; overflow-wrap: anywhere;\n}\n.p2-timeline {\n    padding: 8px 14px 10px; font-size: 10px;\n    color: color-mix(in srgb, var(--p2-fg) 45%, transparent);\n    border-top: 1px solid color-mix(in srgb, var(--p2-fg) 6%, transparent);\n    display: flex; flex-direction: column; gap: 4px;\n    font-style: italic; line-height: 1.5;\n}\n.p2-tl-row { display: block; overflow-wrap: anywhere }\n.p2-char {\n    padding: 7px 0;\n    border-top: 1px dashed color-mix(in srgb, var(--p2-border) 60%, transparent);\n}\n.p2-char:first-of-type { border-top: none; padding-top: 2px }\n.p2-char-name {\n    display: flex; align-items: center; gap: 8px; margin-bottom: 5px;\n}\n.p2-gender { font-size: 13px; color: var(--p2-em); line-height: 1 }\n.p2-name {\n    font-size: 12px; font-weight: 700;\n    color: color-mix(in srgb, var(--p2-fg) 90%, transparent);\n}\n.p2-role {\n    font-size: 9px; font-style: italic;\n    color: color-mix(in srgb, var(--p2-fg) 45%, transparent);\n    background: color-mix(in srgb, var(--p2-fg) 5%, transparent);\n    padding: 1px 7px; border-radius: 20px;\n}\n.p2-char-grid {\n    display: grid; grid-template-columns: auto 1fr;\n    column-gap: 10px; row-gap: 3px; align-items: baseline;\n    padding-left: 21px;\n}\n.p2-ck {\n    font-size: 8.5px; font-weight: 600; letter-spacing: .1em;\n    text-transform: uppercase;\n    color: color-mix(in srgb, var(--p2-fg) 32%, transparent);\n    white-space: nowrap;\n}\n.p2-cv {\n    font-size: 10.5px;\n    color: color-mix(in srgb, var(--p2-fg) 72%, transparent);\n    line-height: 1.45; overflow-wrap: anywhere;\n}\n.p2-italic { font-style: italic }\n.p2-body-val { color: color-mix(in srgb, var(--p2-accent3) 80%, var(--p2-fg)) }\n.p2-rel {\n    padding: 7px 0;\n    border-top: 1px dashed color-mix(in srgb, var(--p2-border) 60%, transparent);\n}\n.p2-rel:first-of-type { border-top: none; padding-top: 2px }\n.p2-rel-pair {\n    display: flex; align-items: center; gap: 8px;\n    margin-bottom: 5px; font-size: 11.5px;\n}\n.p2-from { color: var(--p2-em); font-weight: 600 }\n.p2-arrow {\n    font-size: 14px;\n    color: color-mix(in srgb, var(--p2-fg) 30%, transparent);\n}\n.p2-to { color: var(--p2-accent3); font-weight: 600 }\n.p2-stream-hide { display: none !important }\n@media (max-width: 480px) {\n    .p2-scene-grid { grid-template-columns: 1fr }\n    .p2-cell--wide { grid-column: auto }\n    .p2-toggle-names { display: none }\n    .p2-char-grid { padding-left: 0 }\n}\n`;\n\n    function initializeScript() {\n        if (!window.TavernHelper || !window.tavern_events || typeof window.eventOn !== 'function') {\n            setTimeout(initializeScript, 200); return;\n        }\n        TH = window.TavernHelper;\n        var ev = window.tavern_events;\n\n        if (!$('#' + MOD.cssId).length) {\n            $('head').append('<style id=\"' + MOD.cssId + '\">' + MOD.css + '</style>');\n        }\n\n        window.eventOn(ev.CHARACTER_MESSAGE_RENDERED, renderVisual);\n        window.eventOn(ev.USER_MESSAGE_RENDERED, renderVisual);\n        window.eventOn(ev.MESSAGE_EDITED,  function(id){ setTimeout(function(){ renderVisual(id); }, 120); });\n        window.eventOn(ev.MESSAGE_UPDATED, function(id){ setTimeout(function(){ renderVisual(id); }, 120); });\n        window.eventOn(ev.MESSAGE_SWIPED,  function(id){ setTimeout(function(){ renderVisual(id); }, 120); });\n\n        var scan = function() {\n            $('.mes').each(function(){\n                var id = $(this).attr('mesid');\n                if (id) renderVisual(Number(id));\n            });\n        };\n        scan();\n\n        window.eventOn(ev.CHAT_CHANGED, function(){ _contextCleaned = {}; setTimeout(scan, 500); });\n        window.eventOn(ev.MORE_MESSAGES_LOADED, scan);\n\n        window.eventOn(ev.GENERATION_AFTER_COMMANDS, function(){\n            cleanContext();\n            injectPrompt(getLastState());\n        });\n\n        window.eventOn(ev.GENERATION_STARTED, function(){\n            setTimeout(function(){\n                var lastId = TH.getLastMessageId();\n                if (lastId >= 0) startStreamObserver(lastId);\n                startStreamObserver(lastId + 1);\n            }, 300);\n        });\n\n        if (ev.STREAM_TOKEN_RECEIVED) {\n            var _streamObsStarted = {};\n            window.eventOn(ev.STREAM_TOKEN_RECEIVED, function(){\n                var lastId = TH.getLastMessageId();\n                if (!_streamObsStarted[lastId]) {\n                    _streamObsStarted[lastId] = true;\n                    startStreamObserver(lastId);\n                    setTimeout(function(){ delete _streamObsStarted[lastId]; }, 30000);\n                }\n            });\n        }\n\n        window.eventOn(ev.GENERATION_ENDED, function(){\n            setTimeout(function(){\n                for (var k in _observers) stopStreamObserver(k);\n                var $l = $('.mes').last();\n                var id = $l.attr('mesid');\n                if (id) renderVisual(Number(id));\n                cleanContext();\n            }, 400);\n        });\n        window.eventOn(ev.GENERATION_STOPPED, function(){\n            setTimeout(function(){\n                for (var k in _observers) stopStreamObserver(k);\n                var $l = $('.mes').last();\n                var id = $l.attr('mesid');\n                if (id) renderVisual(Number(id));\n            }, 400);\n        });\n\n        console.log('[IB2] \\u2713 infoblock panel v3.1 init OK');\n    }\n\n    $(document).ready(initializeScript);\n})();",
  "info": "Infoblock Panel v3.1 — fix: no ---/*** separator after </ib2_data>, timeline events on separate lines (no mid-dot)",
  "button": {
    "enabled": false,
    "buttons": []
  },
  "data": {}
}