{
  "type": "script",
  "enabled": true,
  "name": "infoboard",
  "id": "8ee3aa20-783f-4c00-8bbd-fd9e2ba05ab7",
  "content": "if (!window.renriCleanEmpty) {\n    window.renriCleanEmpty = 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}\nif (!window.renriHideInbd) {\n    window.renriHideInbd = function($target) {\n        if (!$target || !$target.length) return;\n        let html = $target.html();\n        if (!html) return;\n        const orig = html;\n        html = html.replace(/\\[INBD:[^\\]]*\\]/gi, '');\n        if (html !== orig) {\n            html = window.renriCleanEmpty ? window.renriCleanEmpty(html) : html;\n            $target.html(html.trim());\n        }\n    };\n}\n\n(function() {\n    const $ = window.jQuery;\n\n    const MOD = {\n        promptId: 'renri_infoboard_system',\n        systemPrompt: 'End your messages with a information block STRICTLY in this exact format:\\n[INBD: Name | Appearance & outfit detail | posture/activity | character state | NPC present | a short literary line fitting the current scene OR a message from the characters to us | recap ]\\nCRITICAL: You MUST fill ALL fields in the [INBD:] block. If any information is unavailable or not applicable, write \"N/A\" in that field. Never skip or merge fields.\\nGuidelines:\\nRecap: Naturally recap what just happened in plain novel-outline style.\\n- Normal scenes: ~60 words\\n- NSFW scenes: ~30 words\\nKeep it concise and fluent like fiction outline.\\nLanguage for placeholders must match the language of the narrative.\\nFill out a block for each main character in the scene. The AVATAR block will always come first, leave recap block N/A for them.',\n        updatePrompt: '[System Note: Continue with INFOBOARD block(s) at the END of your message. Same [INBD: ...] format for each character.]',\n        cssId: 'renri-infoboard-styles',\n        expirationDepth: 7,\n    };\n\n    let TH = null;\n    const _rendering = {};\n\n    function esc(s) {\n        return (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');\n    }\n\n    function isNA(s) {\n        return !s || /^\\s*n\\/a\\s*$/i.test((s||'').trim());\n    }\n\n    function buildCard(m) {\n        var name  = (m[1]||'').trim();\n        var recap = (m[7]||'').trim();\n\n        var fieldDefs = [\n            { key: 'Appearance', val: (m[2]||'').trim(), isQuote: false },\n            { key: 'Posture',    val: (m[3]||'').trim(), isQuote: false },\n            { key: 'State',      val: (m[4]||'').trim(), isQuote: false },\n            { key: 'NPC',        val: (m[5]||'').trim(), isQuote: false },\n            { key: 'Quote',      val: (m[6]||'').trim(), isQuote: true  },\n        ];\n\n        var rows = '';\n        for (var j = 0; j < fieldDefs.length; j++) {\n            var fd = fieldDefs[j];\n            if (isNA(fd.val)) continue;\n            var valHtml = fd.isQuote\n                ? '<div class=\"rib-quote-val\">' + esc(fd.val) + '</div>'\n                : '<div class=\"rib-val\">' + esc(fd.val) + '</div>';\n            rows += '<div class=\"rib-key\">' + fd.key + '</div>' + valHtml;\n        }\n\n        var recapHtml = '';\n        if (!isNA(recap)) {\n            recapHtml = '<div class=\"rib-recap\">' +\n                '<div class=\"rib-recap-label\">Recap</div>' +\n                '<div class=\"rib-recap-text\">' + esc(recap) + '</div>' +\n                '</div>';\n        }\n\n        return '<div class=\"rib-card\">' +\n            '<div class=\"rib-name\">' + esc(name) + '</div>' +\n            '<div class=\"rib-fields\">' + rows + '</div>' +\n            recapHtml +\n            '</div>';\n    }\n\n    function buildBoard(matches) {\n        var names = matches.map(function(m){ return (m[1]||'').trim(); }).join(', ');\n        var cards = matches.map(buildCard).join('');\n        return '<details class=\"rib-wrap\">' +\n            '<summary class=\"rib-toggle\">' +\n            '<span class=\"rib-toggle-icon\">\\u25b6</span>' +\n            '<span class=\"rib-toggle-label\">Infoboard</span>' +\n            '<span class=\"rib-toggle-names\">' + esc(names) + '</span>' +\n            '</summary>' +\n            '<div class=\"rib-cards\">' + cards + '</div>' +\n            '</details>';\n    }\n\n    function parseBlocks(raw) {\n        var re = /\\[INBD:\\s*([^|\\]]+)\\|([^|\\]]*)\\|([^|\\]]*)\\|([^|\\]]*)\\|([^|\\]]*)\\|([^|\\]]*)\\|([^\\]]*)\\]/gi;\n        var matches = [];\n        var m;\n        while ((m = re.exec(raw)) !== null) {\n            matches.push(m);\n        }\n        return matches;\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) continue;\n            if (parseBlocks(msgs[i].message).length > 0) return true;\n        }\n        return null;\n    }\n\n    function injectPrompt(hasHistory) {\n        var content = hasHistory ? MOD.updatePrompt : MOD.systemPrompt;\n        TH.injectPrompts([{ id: MOD.promptId, position: 'in_chat', depth: 0, role: 'system', content: content, should_scan: false }], { 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 matches = parseBlocks(raw);\n        if (!matches.length) 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 isExpired = (lastId - messageId) >= MOD.expirationDepth;\n\n        $target.find('.rib-wrap').remove();\n        if (window.renriHideInbd) window.renriHideInbd($target);\n        $target.find('p, div, li').each(function() {\n            var $el = $(this);\n            if ($el.closest('.rib-wrap').length) return;\n            if (/\\[INBD:/i.test($el.text())) $el.remove();\n        });\n        var html = $target.html();\n        html = window.renriCleanEmpty ? window.renriCleanEmpty(html) : html;\n        $target.html(html.trim());\n\n        if (!isExpired) {\n            $target.append(buildBoard(matches));\n        }\n    }\n\n    function initializeScript() {\n        if (!window.TavernHelper || !window.tavern_events || typeof window.eventOn !== 'function') {\n            setTimeout(initializeScript, 200);\n            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); }, 100); });\n        window.eventOn(ev.MESSAGE_UPDATED, function(id){ setTimeout(function(){ renderVisual(id); }, 100); });\n        window.eventOn(ev.MESSAGE_SWIPED,  function(id){ setTimeout(function(){ renderVisual(id); }, 100); });\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(){ setTimeout(scan, 500); });\n        window.eventOn(ev.MORE_MESSAGES_LOADED, scan);\n        window.eventOn(ev.GENERATION_AFTER_COMMANDS, function(){ injectPrompt(getLastState()); });\n        window.eventOn(ev.GENERATION_ENDED,   function(){ setTimeout(function(){ var $l=$('.mes').last(); var id=$l.attr('mesid'); if(id) renderVisual(Number(id)); }, 400); });\n        window.eventOn(ev.GENERATION_STOPPED, function(){ setTimeout(function(){ var $l=$('.mes').last(); var id=$l.attr('mesid'); if(id) renderVisual(Number(id)); }, 400); });\n\n        console.log('[RenriInfoboard] init OK');\n    }\n\n    MOD.css = \"@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;700&display=swap');.rib-wrap{display:flex;flex-direction:column;gap:0;font-family:'JetBrains Mono','Courier New',monospace;animation:rib-appear 0.35s cubic-bezier(0.16,1,0.3,1) both}@keyframes rib-appear{from{opacity:0;transform:translateY(-5px)}to{opacity:1;transform:translateY(0)}}.rib-toggle{display:flex;align-items:center;gap:8px;padding:7px 12px;background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.09);cursor:pointer;user-select:none;list-style:none;transition:background 0.15s}.rib-toggle::-webkit-details-marker{display:none}.rib-toggle:hover{background:rgba(255,255,255,0.05)}.rib-toggle-icon{font-size:9px;color:rgba(255,255,255,0.25);transition:transform 0.2s;flex-shrink:0}details[open] .rib-toggle-icon{transform:rotate(90deg)}.rib-toggle-label{font-size:9px;font-weight:500;letter-spacing:0.12em;text-transform:uppercase;color:rgba(255,255,255,0.35);flex:1}.rib-toggle-names{font-size:9px;font-weight:400;color:rgba(255,255,255,0.2);letter-spacing:0.05em}.rib-cards{display:flex;flex-direction:column;border:1px solid rgba(255,255,255,0.09);border-top:none;overflow:hidden;background:rgba(255,255,255,0.02)}.rib-card{padding:10px 12px 11px;border-bottom:1px solid rgba(255,255,255,0.05);position:relative}.rib-card:last-child{border-bottom:none}.rib-name{font-size:11px;font-weight:700;color:rgba(255,255,255,0.80);letter-spacing:0.04em;margin-bottom:8px;display:flex;align-items:center;gap:6px}.rib-name::after{content:'';flex:1;height:1px;background:rgba(255,255,255,0.06)}.rib-fields{display:grid;grid-template-columns:auto 1fr;column-gap:10px;row-gap:4px;align-items:baseline}.rib-key{font-size:8px;font-weight:500;letter-spacing:0.1em;text-transform:uppercase;color:rgba(255,255,255,0.22);white-space:nowrap;padding-top:1px}.rib-val{font-size:10px;font-weight:300;color:rgba(255,255,255,0.62);line-height:1.45;overflow-wrap:anywhere}.rib-quote-val{font-size:10px;font-weight:300;color:rgba(255,255,255,0.40);font-style:italic;line-height:1.45;overflow-wrap:anywhere}.rib-recap{margin-top:8px;padding:7px 9px;background:rgba(255,255,255,0.025);border-left:2px solid rgba(255,255,255,0.08)}.rib-recap-label{font-size:8px;font-weight:500;letter-spacing:0.1em;text-transform:uppercase;color:rgba(255,255,255,0.18);margin-bottom:4px}.rib-recap-text{font-size:10px;font-weight:300;color:rgba(255,255,255,0.45);line-height:1.6;overflow-wrap:anywhere}\";\n    $(document).ready(initializeScript);\n})();",
  "info": "Infoboard: character status cards — collapsible, N/A fields hidden, multi-character",
  "button": {
    "enabled": false,
    "buttons": []
  },
  "data": {}
}