Greasy Fork is available in English.
Панель администратора Black Russia — шаблоны, автовставка, префиксы, AI Groq
// ==UserScript==
// @name BR Admin Panel Pro v2.0
// @namespace https://vk.ru/club237051164
// @version 2.0.0
// @description Панель администратора Black Russia — шаблоны, автовставка, префиксы, AI Groq
// @author Akzholch1k | Dev: https://vk.ru/brunoverona
// @match https://forum.blackrussia.online/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @connect api.groq.com
// @connect api.telegram.org
// @license Connect
// ==/UserScript==
(function () {
'use strict';
// ══════════════════════════════════════
// КОНФИГ
// ══════════════════════════════════════
const TG_TOKEN = '8572813058:AAEdG21L9oRpvP_nbuiT8l8xDMufJt2s-bo';
const DEV_PASS = 'devpanel';
const GROQ_KEY_DEFAULT = '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';
const GROQ_MODEL = 'llama3-70b-8192';
// ══════════════════════════════════════
// ПРЕФИКСЫ
// ══════════════════════════════════════
const PFX = {
NO_PREFIX: 0,
PIN_PREFIX: 2,
UNACCEPT: 4,
PENDING: 5,
DECIDED: 6,
CLOSE: 7,
ACCEPT: 8,
WATCHED: 9,
COMMAND: 10,
GA: 12,
TEX: 13,
WAIT: 14,
};
const PFX_LABEL = {
0: '— Без префикса',
2: '📌 Закрепить',
4: '❌ Отказано',
5: '🕐 На рассмотрении',
6: '✅ Решено',
7: '🔒 Закрыто',
8: '✅ Одобрено',
9: '👁 Рассмотрено',
10: '👥 Команде проекта',
12: '🔰 GA',
13: '🔧 Тех. специалисту',
14: '⏳ Ожидание',
};
// ══════════════════════════════════════
// ВСТРОЕННЫЕ ШАБЛОНЫ
// {{ greeting }} → время суток
// {{ user.name }} → ник автора темы
// ══════════════════════════════════════
const FLOWER_IMG =
'[CENTER][url=https://postimages.org/][img]https://i.postimg.cc/cCG97p5p/Pics-Art-07-12-03-23-18-1.png[/img][/url][/CENTER]';
const BUILTIN = [
{
id: 'b_pending',
name: '🕐 На рассмотрении',
prefix: PFX.PENDING,
autoPin: true,
borderColor: 'rgb(236,124,38,0.5)',
content:
'[COLOR=#FF00FF][FONT=times new roman][SIZE=4][I][CENTER][ICODE]{{ greeting }}, уважаемый(ая) игрок {{ user.name }}[/ICODE][/CENTER][/I][/SIZE][/FONT][/COLOR]\n' +
FLOWER_IMG + '\n' +
'[B][CENTER][FONT=times new roman][COLOR=#ffffff][ICODE]Ваша заявка принята и находится на рассмотрении.\nМы свяжемся с вами в ближайшее время.[/ICODE][/COLOR][/CENTER][/FONT][/B]\n' +
FLOWER_IMG + '\n' +
'[CENTER][COLOR=#00FF00][ICODE]🕐 На рассмотрении.[/ICODE][/COLOR][/CENTER]',
},
{
id: 'b_accept',
name: '✅ Одобрено',
prefix: PFX.ACCEPT,
autoPin: false,
borderColor: 'rgb(152,251,152,0.5)',
content:
'[COLOR=#FF00FF][FONT=times new roman][SIZE=4][I][CENTER][ICODE]{{ greeting }}, уважаемый(ая) игрок {{ user.name }}[/ICODE][/CENTER][/I][/SIZE][/FONT][/COLOR]\n' +
FLOWER_IMG + '\n' +
'[B][CENTER][FONT=times new roman][COLOR=#ffffff][ICODE]Ваша заявка была рассмотрена и одобрена!\nПоздравляем, следите за дальнейшими инструкциями.[/ICODE][/COLOR][/CENTER][/FONT][/B]\n' +
FLOWER_IMG + '\n' +
'[CENTER][COLOR=#00FF00][ICODE]✅ Одобрено.[/ICODE][/COLOR][/CENTER]',
},
{
id: 'b_unaccept',
name: '❌ Отказано',
prefix: PFX.UNACCEPT,
autoPin: false,
borderColor: 'rgb(255,80,80,0.5)',
content:
'[COLOR=#FF00FF][FONT=times new roman][SIZE=4][I][CENTER][ICODE]{{ greeting }}, уважаемый(ая) игрок {{ user.name }}[/ICODE][/CENTER][/I][/SIZE][/FONT][/COLOR]\n' +
FLOWER_IMG + '\n' +
'[B][CENTER][FONT=times new roman][COLOR=#ffffff][ICODE]К сожалению, ваша заявка была отклонена.\nПричина: несоответствие требованиям.[/ICODE][/COLOR][/CENTER][/FONT][/B]\n' +
FLOWER_IMG + '\n' +
'[CENTER][COLOR=#FF0000][ICODE]❌ Отказано.[/ICODE][/COLOR][/CENTER]',
},
{
id: 'b_close',
name: '🔒 Закрыто',
prefix: PFX.CLOSE,
autoPin: false,
borderColor: 'rgb(150,150,150,0.5)',
content:
'[COLOR=#FF00FF][FONT=times new roman][SIZE=4][I][CENTER][ICODE]{{ greeting }}, уважаемый(ая) игрок {{ user.name }}[/ICODE][/CENTER][/I][/SIZE][/FONT][/COLOR]\n' +
FLOWER_IMG + '\n' +
'[B][CENTER][FONT=times new roman][COLOR=#ffffff][ICODE]Тема закрыта.\nПо вопросам обращайтесь к администрации.[/ICODE][/COLOR][/CENTER][/FONT][/B]\n' +
FLOWER_IMG + '\n' +
'[CENTER][COLOR=#888888][ICODE]🔒 Закрыто.[/ICODE][/COLOR][/CENTER]',
},
{
id: 'b_decided',
name: '✅ Решено',
prefix: PFX.DECIDED,
autoPin: false,
borderColor: 'rgb(46,212,122,0.5)',
content:
'[COLOR=#FF00FF][FONT=times new roman][SIZE=4][I][CENTER][ICODE]{{ greeting }}, уважаемый(ая) игрок {{ user.name }}[/ICODE][/CENTER][/I][/SIZE][/FONT][/COLOR]\n' +
FLOWER_IMG + '\n' +
'[B][CENTER][FONT=times new roman][COLOR=#ffffff][ICODE]Ваш вопрос рассмотрен и решён.[/ICODE][/COLOR][/CENTER][/FONT][/B]\n' +
FLOWER_IMG + '\n' +
'[CENTER][COLOR=#00FF00][ICODE]✅ Решено.[/ICODE][/COLOR][/CENTER]',
},
{
id: 'b_watched',
name: '👁 Рассмотрено',
prefix: PFX.WATCHED,
autoPin: false,
borderColor: 'rgb(100,180,255,0.5)',
content:
'[COLOR=#FF00FF][FONT=times new roman][SIZE=4][I][CENTER][ICODE]{{ greeting }}, уважаемый(ая) игрок {{ user.name }}[/ICODE][/CENTER][/I][/SIZE][/FONT][/COLOR]\n' +
FLOWER_IMG + '\n' +
'[B][CENTER][FONT=times new roman][COLOR=#ffffff][ICODE]Ваше обращение рассмотрено.[/ICODE][/COLOR][/CENTER][/FONT][/B]\n' +
FLOWER_IMG + '\n' +
'[CENTER][COLOR=#64b4ff][ICODE]👁 Рассмотрено.[/ICODE][/COLOR][/CENTER]',
},
{
id: 'b_wait',
name: '⏳ Ожидание',
prefix: PFX.WAIT,
autoPin: false,
borderColor: 'rgb(250,204,21,0.5)',
content:
'[COLOR=#FF00FF][FONT=times new roman][SIZE=4][I][CENTER][ICODE]{{ greeting }}, уважаемый(ая) игрок {{ user.name }}[/ICODE][/CENTER][/I][/SIZE][/FONT][/COLOR]\n' +
FLOWER_IMG + '\n' +
'[B][CENTER][FONT=times new roman][COLOR=#ffffff][ICODE]Ваша заявка ожидает дополнительных действий с вашей стороны.[/ICODE][/COLOR][/CENTER][/FONT][/B]\n' +
FLOWER_IMG + '\n' +
'[CENTER][COLOR=#facc15][ICODE]⏳ Ожидание.[/ICODE][/COLOR][/CENTER]',
},
// ── Биографии ──
{
id: 'b_bio_pending',
name: '📖 Биография — На рассмотрении',
prefix: PFX.PENDING,
autoPin: true,
cat: 'bio',
borderColor: 'rgb(236,124,38,0.5)',
content:
'[COLOR=#FF00FF][FONT=times new roman][SIZE=4][I][CENTER][ICODE]{{ greeting }}, уважаемый(ая) игрок {{ user.name }}[/ICODE][/CENTER][/I][/SIZE][/FONT][/COLOR]\n' +
FLOWER_IMG + '\n' +
'[B][CENTER][FONT=times new roman][COLOR=#ffffff][ICODE]Ваша биография принята на рассмотрение.\nОжидайте ответа куратора.[/ICODE][/COLOR][/CENTER][/FONT][/B]\n' +
FLOWER_IMG + '\n' +
'[CENTER][COLOR=#00FF00][ICODE]🕐 На рассмотрении.[/ICODE][/COLOR][/CENTER]',
},
{
id: 'b_bio_accept',
name: '📖 Биография — Одобрено',
prefix: PFX.ACCEPT,
autoPin: false,
cat: 'bio',
borderColor: 'rgb(152,251,152,0.5)',
content:
'[COLOR=#FF00FF][FONT=times new roman][SIZE=4][I][CENTER][ICODE]{{ greeting }}, уважаемый(ая) игрок {{ user.name }}[/ICODE][/CENTER][/I][/SIZE][/FONT][/COLOR]\n' +
FLOWER_IMG + '\n' +
'[B][CENTER][FONT=times new roman][COLOR=#ffffff][ICODE]Ваша биография проверена и одобрена!\nПоздравляем, она теперь активна.[/ICODE][/COLOR][/CENTER][/FONT][/B]\n' +
FLOWER_IMG + '\n' +
'[CENTER][COLOR=#00FF00][ICODE]✅ Одобрено.[/ICODE][/COLOR][/CENTER]',
},
{
id: 'b_bio_deny',
name: '📖 Биография — Отказано',
prefix: PFX.UNACCEPT,
autoPin: false,
cat: 'bio',
borderColor: 'rgb(255,80,80,0.5)',
content:
'[COLOR=#FF00FF][FONT=times new roman][SIZE=4][I][CENTER][ICODE]{{ greeting }}, уважаемый(ая) игрок {{ user.name }}[/ICODE][/CENTER][/I][/SIZE][/FONT][/COLOR]\n' +
FLOWER_IMG + '\n' +
'[B][CENTER][FONT=times new roman][COLOR=#ffffff][ICODE]Ваша биография отклонена.\nПричина: несоответствие требованиям биографии.[/ICODE][/COLOR][/CENTER][/FONT][/B]\n' +
FLOWER_IMG + '\n' +
'[CENTER][COLOR=#FF0000][ICODE]❌ Отказано.[/ICODE][/COLOR][/CENTER]',
},
];
// ══════════════════════════════════════
// ХРАНИЛИЩЕ
// ══════════════════════════════════════
const S = {
get: (k, d) => { try { return JSON.parse(GM_getValue(k, JSON.stringify(d))); } catch { return d; } },
set: (k, v) => GM_setValue(k, JSON.stringify(v)),
};
let nickname = S.get('nickname', null);
let myTpls = S.get('my_tpls', []);
let logs = S.get('logs', []);
let devOpen = false;
const saveMyTpls = () => S.set('my_tpls', myTpls);
const saveLogs = () => S.set('logs', logs.slice(-300));
function log(action, detail) {
logs.push({ t: new Date().toLocaleString('ru-RU'), u: nickname || '?', action, detail });
saveLogs();
tg(`📋 ${action}\n👤 ${nickname}\n📝 ${detail}`);
}
// ══════════════════════════════════════
// TELEGRAM
// ══════════════════════════════════════
function tg(text) {
const cid = S.get('tg_cid', '');
if (!cid) return;
GM_xmlhttpRequest({
method: 'POST', url: `https://api.telegram.org/bot${TG_TOKEN}/sendMessage`,
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify({ chat_id: cid, text, parse_mode: 'HTML' }),
});
}
// ══════════════════════════════════════
// GROQ AI
// ══════════════════════════════════════
function groqAI(userMsg, sysMsg) {
return new Promise(resolve => {
const key = S.get('groq_key', GROQ_KEY_DEFAULT);
GM_xmlhttpRequest({
method: 'POST',
url: 'https://api.groq.com/openai/v1/chat/completions',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${key}` },
data: JSON.stringify({
model: GROQ_MODEL,
max_tokens: 800,
messages: [
{ role: 'system', content: sysMsg || 'Ты помощник модератора форума Black Russia. Улучши сообщение: сделай вежливым и профессиональным. Верни ТОЛЬКО текст, без пояснений.' },
{ role: 'user', content: userMsg },
],
}),
onload: r => { try { resolve(JSON.parse(r.responseText).choices[0].message.content); } catch { resolve(null); } },
onerror: () => resolve(null),
});
});
}
// ══════════════════════════════════════
// ВСПОМОГАТЕЛЬНЫЕ
// ══════════════════════════════════════
function greeting() {
const h = new Date().getHours();
if (h >= 6 && h < 12) return 'Доброе утро';
if (h >= 12 && h < 17) return 'Добрый день';
if (h >= 17 && h < 22) return 'Добрый вечер';
return 'Доброй ночи';
}
function threadAuthor() {
const el = document.querySelector(
'.message--post:first-child .username, .message-userDetails .username, h1.p-title-value'
);
return el ? el.textContent.trim() : 'игрок';
}
function applyVars(text) {
return text
.replace(/\{\{\s*greeting\s*\}\}/g, greeting())
.replace(/\{\{\s*user\.name\s*\}\}/g, threadAuthor());
}
// ══════════════════════════════════════
// ВСТАВКА В ПОЛЕ ОТВЕТА + ОТПРАВКА
// ══════════════════════════════════════
function insertAndSend(text, prefixId, doSend) {
// 1. Установить префикс
if (prefixId) {
const sel = document.querySelector('select[name="prefix_id"], #ctrl_prefix_id');
if (sel) { sel.value = prefixId; sel.dispatchEvent(new Event('change', { bubbles: true })); }
}
// 2. Найти поле ввода XenForo
const editors = [
document.querySelector('textarea[name="message"]'),
document.querySelector('.fr-element[contenteditable="true"]'),
document.querySelector('[data-xf-init="editor"] .fr-element'),
document.querySelector('.redactor-in'),
].filter(Boolean);
let inserted = false;
for (const ed of editors) {
if (ed.tagName === 'TEXTAREA') {
ed.value = text;
ed.dispatchEvent(new Event('input', { bubbles: true }));
ed.focus();
inserted = true;
} else {
ed.focus();
document.execCommand('selectAll', false, null);
document.execCommand('insertText', false, text);
ed.dispatchEvent(new Event('input', { bubbles: true }));
inserted = true;
}
break;
}
if (!inserted) {
navigator.clipboard.writeText(text).catch(() => {});
toast('📋 Скопировано — вставьте вручную', '#e8a020');
return;
}
toast('✅ Шаблон вставлен!');
// 3. Автоотправка
if (doSend) {
setTimeout(() => {
const btn = document.querySelector(
'button[data-xf-click="submit"], .js-submitButton, button[type="submit"].button--primary'
);
if (btn) btn.click();
}, 300);
}
}
// ══════════════════════════════════════
// ТОСТ
// ══════════════════════════════════════
function toast(msg, bg) {
const el = document.createElement('div');
el.textContent = msg;
Object.assign(el.style, {
position: 'fixed', bottom: '90px', left: '50%', transform: 'translateX(-50%)',
background: bg || '#2ed47a', color: bg ? '#fff' : '#000',
fontWeight: '700', fontSize: '13px', padding: '10px 22px',
borderRadius: '20px', zIndex: '9999999',
boxShadow: '0 4px 20px rgba(0,0,0,0.4)',
fontFamily: 'Nunito, sans-serif',
animation: 'none', opacity: '0', transition: 'opacity .2s',
});
document.body.appendChild(el);
requestAnimationFrame(() => { el.style.opacity = '1'; });
setTimeout(() => { el.style.opacity = '0'; setTimeout(() => el.remove(), 300); }, 2200);
}
// ══════════════════════════════════════
// CSS
// ══════════════════════════════════════
document.head.insertAdjacentHTML('beforeend', `
<style>
@import url('https://fonts.googleapis.com/css2?family=Rajdhani:wght@600;700&family=Nunito:wght@400;600;700&display=swap');
:root{--bg:#0a0c12;--panel:#12151f;--bd:#1e2535;--acc:#e8a020;--red:#ff4f4f;--txt:#d4daf0;--muted:#5a6080;--grn:#2ed47a;--pur:#a855f7;--r:12px;--sh:0 8px 40px rgba(0,0,0,.6)}
#br-ov{position:fixed;inset:0;z-index:999998;background:rgba(0,0,0,.88);backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center}
#br-box{background:var(--panel);border:1px solid var(--bd);border-radius:var(--r);padding:36px 30px;max-width:400px;width:92%;box-shadow:var(--sh);font-family:Nunito,sans-serif;color:var(--txt);text-align:center}
#br-box h2{font-family:Rajdhani,sans-serif;font-size:22px;color:var(--acc);margin:0 0 8px}
#br-box p{font-size:13px;color:var(--muted);margin:0 0 18px}
#br-nick-in{width:100%;box-sizing:border-box;background:var(--bg);border:1px solid var(--bd);border-radius:8px;padding:10px 13px;color:var(--txt);font-size:14px;font-family:Nunito,sans-serif;outline:none;margin-bottom:14px;transition:border .2s}
#br-nick-in:focus{border-color:var(--acc)}
.brow{display:flex;gap:8px}
.brow button{flex:1}
#br-fab{position:fixed;bottom:22px;right:22px;z-index:999997;width:54px;height:54px;border-radius:50%;background:linear-gradient(135deg,var(--acc),#b07010);border:none;cursor:pointer;box-shadow:0 4px 18px rgba(232,160,32,.45);display:flex;align-items:center;justify-content:center;font-size:22px;transition:transform .2s,box-shadow .2s}
#br-fab:hover{transform:scale(1.1);box-shadow:0 6px 26px rgba(232,160,32,.65)}
#br-panel{position:fixed;bottom:86px;right:22px;z-index:999996;width:min(490px,96vw);background:var(--panel);border:1px solid var(--bd);border-radius:var(--r);box-shadow:var(--sh);font-family:Nunito,sans-serif;color:var(--txt);display:none;flex-direction:column;max-height:88vh;overflow:hidden}
#br-panel.open{display:flex}
.brhd{display:flex;align-items:center;justify-content:space-between;padding:13px 17px;border-bottom:1px solid var(--bd);background:linear-gradient(90deg,#0e1120,#12151f)}
.brlogo{font-family:Rajdhani,sans-serif;font-size:17px;font-weight:700;color:var(--acc)}
.brnick{font-size:11px;color:var(--muted)}
.brx{background:none;border:none;color:var(--muted);font-size:20px;cursor:pointer;line-height:1;transition:color .2s}
.brx:hover{color:var(--red)}
.brtabs{display:flex;border-bottom:1px solid var(--bd);background:#0e1120}
.brtab{flex:1;padding:10px 3px;background:none;border:none;border-bottom:2px solid transparent;cursor:pointer;font-size:11px;font-family:Nunito,sans-serif;color:var(--muted);transition:color .2s}
.brtab.on{color:var(--acc);border-bottom-color:var(--acc)}
.brtab:hover{color:var(--txt)}
.brcnt{flex:1;overflow-y:auto;padding:15px}
.brcnt::-webkit-scrollbar{width:4px}
.brcnt::-webkit-scrollbar-thumb{background:var(--bd);border-radius:2px}
.brst{font-family:Rajdhani,sans-serif;font-size:12px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:1px;margin:0 0 10px}
/* фильтры */
.brfilters{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px}
.brfbtn{background:transparent;border:1px solid var(--bd);border-radius:20px;padding:4px 11px;color:var(--muted);font-size:11px;font-family:Nunito,sans-serif;cursor:pointer;transition:all .2s}
.brfbtn.on{background:var(--acc);color:#000;border-color:var(--acc);font-weight:700}
/* карточка шаблона */
.brcard{border-radius:9px;padding:12px 14px;margin-bottom:9px;transition:border-color .2s;border-width:1px;border-style:solid}
.brcard:hover{opacity:.92}
.brcname{font-size:13px;font-weight:700;color:var(--txt);margin-bottom:5px;display:flex;align-items:center;gap:5px;flex-wrap:wrap}
.brpbadge{font-size:10px;padding:2px 7px;border-radius:10px;background:rgba(168,85,247,.15);color:var(--pur);border:1px solid rgba(168,85,247,.3)}
.brpinbadge{font-size:10px;padding:2px 7px;border-radius:10px;background:rgba(250,204,21,.1);color:#facc15;border:1px solid rgba(250,204,21,.3)}
.brcprev{font-size:11px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:9px}
.brbtns{display:flex;gap:6px;flex-wrap:wrap}
.brbtns button{flex:1;min-width:55px}
/* AI результат под карточкой */
.braires{background:rgba(168,85,247,.08);border:1px solid rgba(168,85,247,.25);border-radius:8px;padding:10px;font-size:12px;color:#c084fc;margin-top:8px;display:none;white-space:pre-wrap;word-break:break-word}
/* форма */
.brfg{margin-bottom:11px}
.brfg label{display:block;font-size:12px;color:var(--muted);margin-bottom:3px}
.brin,.brta,.brsel{width:100%;box-sizing:border-box;background:var(--bg);border:1px solid var(--bd);border-radius:8px;padding:9px 12px;color:var(--txt);font-size:13px;font-family:Nunito,sans-serif;outline:none;transition:border .2s}
.brin:focus,.brta:focus,.brsel:focus{border-color:var(--acc)}
.brta{min-height:90px;resize:vertical}
.brsel option{background:#12151f}
/* ai редактор */
.brai-res{background:rgba(46,212,122,.08);border:1px solid rgba(46,212,122,.25);border-radius:8px;padding:11px;font-size:13px;color:var(--grn);margin-top:10px;display:none;white-space:pre-wrap}
.brspin{display:none;align-items:center;gap:7px;font-size:12px;color:var(--muted);margin-top:8px}
.brdot{width:6px;height:6px;border-radius:50%;background:var(--acc);animation:bpulse 1s infinite}
.brdot:nth-child(2){animation-delay:.15s}
.brdot:nth-child(3){animation-delay:.3s}
@keyframes bpulse{0%,100%{opacity:.3;transform:scale(.8)}50%{opacity:1;transform:scale(1.2)}}
/* логи */
.brlog{font-size:11px;padding:7px 10px;border-left:2px solid var(--bd);margin-bottom:5px;color:var(--muted)}
.brlog .bla{color:var(--txt);font-weight:600}
.brlog .blt{float:right;font-size:10px}
/* dev */
.brstat{background:#0e1120;border:1px solid var(--bd);border-radius:8px;padding:13px;display:flex;align-items:center;gap:12px;margin-bottom:8px}
.brsicon{font-size:21px}
.brsval{font-family:Rajdhani,sans-serif;font-size:22px;color:var(--acc);font-weight:700}
.brslbl{font-size:11px;color:var(--muted)}
/* кнопки */
.bb{border:none;border-radius:7px;padding:8px 13px;font-size:12px;font-weight:700;font-family:Nunito,sans-serif;cursor:pointer;transition:opacity .2s,transform .1s}
.bb:active{transform:scale(.97)}
.bb-acc{background:linear-gradient(135deg,var(--acc),#b07010);color:#000}
.bb-grn{background:linear-gradient(135deg,var(--grn),#1a9e55);color:#000}
.bb-pur{background:linear-gradient(135deg,var(--pur),#7c3aed);color:#fff}
.bb-gh{background:transparent;border:1px solid var(--bd)!important;color:var(--txt)}
.bb-gh:hover{border-color:var(--acc)!important;color:var(--acc)}
.bb-red{background:rgba(255,79,79,.15);border:1px solid rgba(255,79,79,.3)!important;color:var(--red)}
.bb-red:hover{background:rgba(255,79,79,.3)}
.bb:hover{opacity:.85}
/* devlock */
#brdl{padding:30px 16px;text-align:center}
#brdl h3{font-family:Rajdhani,sans-serif;font-size:18px;color:var(--acc);margin:0 0 7px}
#brdl p{font-size:12px;color:var(--muted);margin:0 0 14px}
.brcontact{background:#0e1120;border:1px solid var(--bd);border-radius:8px;padding:13px;display:flex;align-items:center;gap:11px;margin-bottom:8px;text-decoration:none;transition:border-color .2s}
.brcontact:hover{border-color:var(--acc)}
@media(max-width:540px){#br-panel{right:8px;left:8px;width:auto;bottom:80px}#br-fab{bottom:14px;right:14px}.brtab{font-size:10px;padding:9px 1px}}
</style>`);
// ══════════════════════════════════════
// HTML
// ══════════════════════════════════════
const pfxOptions = Object.entries(PFX_LABEL)
.map(([k, v]) => `<option value="${k}">${v}</option>`).join('');
document.body.insertAdjacentHTML('beforeend', `
<div id="br-ov" style="display:none">
<div id="br-box">
<h2>⚡ BR Admin Panel</h2>
<p>Введите ваш никнейм на форуме для начала работы</p>
<input id="br-nick-in" type="text" placeholder="Ваш никнейм..." />
<div class="brow">
<button class="bb bb-acc" id="br-ok">✅ Войти</button>
<button class="bb bb-gh" id="br-skip">Пропустить</button>
</div>
</div>
</div>
<button id="br-fab">⚙️</button>
<div id="br-panel">
<div class="brhd">
<div style="display:flex;align-items:center;gap:9px">
<span class="brlogo">⚡ BR Admin</span>
<span class="brnick" id="br-nn"></span>
</div>
<button class="brx" id="br-cls">✕</button>
</div>
<div class="brtabs">
<button class="brtab on" data-t="tpl">📋 Шаблоны</button>
<button class="brtab" data-t="ai">🤖 AI</button>
<button class="brtab" data-t="log">📊 Логи</button>
<button class="brtab" data-t="dev">🔒 Dev</button>
</div>
<div class="brcnt" id="br-cnt"></div>
</div>`);
// ══════════════════════════════════════
// СОГЛАСИЕ
// ══════════════════════════════════════
if (!nickname) document.getElementById('br-ov').style.display = 'flex';
else document.getElementById('br-nn').textContent = `👤 ${nickname}`;
document.getElementById('br-ok').onclick = () => {
const v = document.getElementById('br-nick-in').value.trim();
if (!v) return document.getElementById('br-nick-in').focus();
nickname = v; S.set('nickname', v);
document.getElementById('br-ov').style.display = 'none';
document.getElementById('br-nn').textContent = `👤 ${v}`;
tg(`🟢 Вошёл: <b>${v}</b>\n🌐 ${location.href}`);
log('Вход', v);
};
document.getElementById('br-nick-in').onkeydown = e => { if (e.key === 'Enter') document.getElementById('br-ok').click(); };
document.getElementById('br-skip').onclick = () => { document.getElementById('br-ov').style.display = 'none'; };
// ══════════════════════════════════════
// FAB / ПАНЕЛЬ
// ══════════════════════════════════════
document.getElementById('br-fab').onclick = () => {
const p = document.getElementById('br-panel');
const open = p.classList.toggle('open');
if (open) renderTab('tpl');
};
document.getElementById('br-cls').onclick = () => document.getElementById('br-panel').classList.remove('open');
document.querySelectorAll('.brtab').forEach(t => {
t.onclick = () => {
document.querySelectorAll('.brtab').forEach(x => x.classList.remove('on'));
t.classList.add('on');
renderTab(t.dataset.t);
};
});
function renderTab(t) {
const el = document.getElementById('br-cnt');
if (t === 'tpl') renderTpl(el);
if (t === 'ai') renderAI(el);
if (t === 'log') renderLog(el);
if (t === 'dev') renderDev(el);
}
// ══════════════════════════════════════
// ШАБЛОНЫ
// ══════════════════════════════════════
let filter = 'all';
function renderTpl(el) {
el.innerHTML = `
<p class="brst">Шаблоны — клик вставляет и отправляет</p>
<div class="brfilters">
<button class="brfbtn${filter==='all'?' on':''}" data-f="all">Все</button>
<button class="brfbtn${filter==='main'?' on':''}" data-f="main">Основные</button>
<button class="brfbtn${filter==='bio'?' on':''}" data-f="bio">Биографии</button>
<button class="brfbtn${filter==='my'?' on':''}" data-f="my">Мои</button>
</div>
<div id="br-tlist"></div>
<hr style="border-color:var(--bd);margin:16px 0">
<p class="brst">➕ Добавить шаблон</p>
<div class="brfg"><label>Название</label><input class="brin" id="tn-name" placeholder="Название..."/></div>
<div class="brfg">
<label>Префикс</label>
<select class="brsel" id="tn-pfx">${pfxOptions}</select>
</div>
<div class="brfg">
<label>Автозакреп (PIN)</label>
<select class="brsel" id="tn-pin"><option value="0">Нет</option><option value="1">Да</option></select>
</div>
<div class="brfg"><label>Текст (BBCode)</label><textarea class="brta" id="tn-text" placeholder="Текст..."></textarea></div>
<button class="bb bb-acc" id="tn-add">➕ Добавить</button>
`;
el.querySelectorAll('.brfbtn').forEach(b => {
b.onclick = () => { filter = b.dataset.f; renderTpl(el); };
});
drawCards();
document.getElementById('tn-add').onclick = () => {
const name = document.getElementById('tn-name').value.trim();
const text = document.getElementById('tn-text').value.trim();
const pfx = parseInt(document.getElementById('tn-pfx').value);
const pin = document.getElementById('tn-pin').value === '1';
if (!name || !text) return toast('Заполните название и текст', '#e8a020');
myTpls.push({ id: 'u_' + Date.now(), name, text, prefix: pfx, autoPin: pin, cat: 'my' });
saveMyTpls();
log('Шаблон добавлен', name);
document.getElementById('tn-name').value = '';
document.getElementById('tn-text').value = '';
drawCards();
};
}
function allTpls() {
return [
...BUILTIN.map(t => ({ ...t, _b: true })),
...myTpls.map(t => ({ ...t, _b: false })),
];
}
function getTpl(id) { return allTpls().find(t => t.id === id); }
function drawCards() {
const list = document.getElementById('br-tlist');
if (!list) return;
let tpls = allTpls();
if (filter === 'main') tpls = tpls.filter(t => t._b && t.cat !== 'bio');
else if (filter === 'bio') tpls = tpls.filter(t => t.cat === 'bio');
else if (filter === 'my') tpls = tpls.filter(t => !t._b);
list.innerHTML = '';
if (!tpls.length) { list.innerHTML = '<p style="color:var(--muted);font-size:12px">Нет шаблонов.</p>'; return; }
tpls.forEach(tpl => {
const bc = tpl.borderColor || 'rgba(30,37,53,1)';
const pfxLbl = PFX_LABEL[tpl.prefix] || '';
const prev = (tpl.content || tpl.text || '').replace(/\[.*?\]/g, '').substring(0, 70);
const card = document.createElement('div');
card.className = 'brcard';
card.style.borderColor = bc;
card.style.background = '#0e1120';
card.innerHTML = `
<div class="brcname">
${tpl.name}
${pfxLbl ? `<span class="brpbadge">${pfxLbl}</span>` : ''}
${tpl.autoPin ? `<span class="brpinbadge">📌 автозакреп</span>` : ''}
</div>
<div class="brcprev">${prev}...</div>
<div class="brbtns">
<button class="bb bb-grn" data-ins="${tpl.id}">📨 Вставить и отправить</button>
<button class="bb bb-pur" data-ai="${tpl.id}">✨ AI</button>
<button class="bb bb-gh" data-cp="${tpl.id}">📋 Копировать</button>
${!tpl._b ? `<button class="bb bb-red" data-del="${tpl.id}">🗑</button>` : ''}
</div>
<div class="braires" id="air-${tpl.id}"></div>
`;
list.appendChild(card);
});
// Вставить и отправить
list.querySelectorAll('[data-ins]').forEach(btn => {
btn.onclick = () => {
const tpl = getTpl(btn.dataset.ins);
if (!tpl) return;
const text = applyVars(tpl.content || tpl.text || '');
insertAndSend(text, tpl.autoPin ? PFX.PIN_PREFIX : tpl.prefix, true);
log('Вставлен шаблон', tpl.name);
};
});
// Копировать
list.querySelectorAll('[data-cp]').forEach(btn => {
btn.onclick = () => {
const tpl = getTpl(btn.dataset.cp);
if (!tpl) return;
navigator.clipboard.writeText(applyVars(tpl.content || tpl.text || '')).catch(() => {});
btn.textContent = '✅ Скопировано'; setTimeout(() => { btn.textContent = '📋 Копировать'; }, 1500);
log('Скопирован', tpl.name);
};
});
// AI улучшить
list.querySelectorAll('[data-ai]').forEach(btn => {
btn.onclick = async () => {
const tpl = getTpl(btn.dataset.ai);
const box = document.getElementById('air-' + tpl.id);
if (!box) return;
const orig = btn.textContent; btn.textContent = '⏳'; btn.disabled = true;
box.style.display = 'block'; box.textContent = '🤖 Groq AI улучшает...';
const res = await groqAI(
tpl.content || tpl.text || '',
'Ты помощник модератора форума Black Russia. Улучши текст шаблона: сделай профессиональным и вежливым, сохрани BBCode. Верни ТОЛЬКО текст.'
);
btn.textContent = orig; btn.disabled = false;
if (res) {
box.innerHTML = res.substring(0, 400) + (res.length > 400 ? '...' : '');
const useBtn = document.createElement('button');
useBtn.className = 'bb bb-grn'; useBtn.style.marginTop = '8px'; useBtn.style.fontSize = '11px';
useBtn.textContent = '📨 Вставить улучшенный';
useBtn.onclick = () => { insertAndSend(res, tpl.autoPin ? PFX.PIN_PREFIX : tpl.prefix, true); log('AI шаблон вставлен', tpl.name); };
box.appendChild(document.createElement('br'));
box.appendChild(useBtn);
} else {
box.textContent = '❌ AI недоступен'; box.style.color = 'var(--red)';
}
};
});
// Удалить
list.querySelectorAll('[data-del]').forEach(btn => {
btn.onclick = () => {
if (!confirm('Удалить шаблон?')) return;
const id = btn.dataset.del;
myTpls = myTpls.filter(t => t.id !== id); saveMyTpls(); drawCards();
log('Удалён шаблон', id);
};
});
}
// ══════════════════════════════════════
// AI РЕДАКТОР
// ══════════════════════════════════════
function renderAI(el) {
el.innerHTML = `
<p class="brst">🤖 AI-редактор (Groq Llama 3)</p>
<div style="background:rgba(168,85,247,.1);border:1px solid rgba(168,85,247,.3);border-radius:8px;padding:10px;font-size:12px;color:#c084fc;margin-bottom:12px">
Groq AI улучшит ваш текст и поможет составить ответ. Текст вставляется прямо в поле ответа форума.
</div>
<div class="brfg"><label>Ваш текст</label>
<textarea class="brta" id="ai-in" placeholder="Напишите текст для улучшения через AI..." style="min-height:110px"></textarea>
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<button class="bb bb-pur" id="ai-go">🤖 Улучшить через AI</button>
<button class="bb bb-grn" id="ai-ins" style="display:none">📨 Вставить и отправить</button>
<button class="bb bb-gh" id="ai-cp" style="display:none">📋 Копировать</button>
</div>
<div class="brspin" id="ai-spin">
<div class="brdot"></div><div class="brdot"></div><div class="brdot"></div>
<span>Groq AI обрабатывает...</span>
</div>
<div class="brai-res" id="ai-res"></div>
`;
let lastRes = '';
document.getElementById('ai-go').onclick = async () => {
const txt = document.getElementById('ai-in').value.trim();
if (!txt) return;
const spin = document.getElementById('ai-spin');
const res = document.getElementById('ai-res');
const ins = document.getElementById('ai-ins');
const cp = document.getElementById('ai-cp');
spin.style.display = 'flex'; res.style.display = 'none'; ins.style.display = 'none'; cp.style.display = 'none';
const r = await groqAI(txt);
spin.style.display = 'none';
if (r) {
lastRes = r; res.textContent = r; res.style.display = 'block'; res.style.color = 'var(--grn)';
ins.style.display = 'inline-block'; cp.style.display = 'inline-block';
log('AI улучшение', txt.substring(0, 50));
} else {
res.textContent = '❌ Groq AI недоступен. Проверьте ключ в Dev.';
res.style.color = 'var(--red)'; res.style.display = 'block';
}
};
document.getElementById('ai-ins').onclick = () => { insertAndSend(lastRes, null, true); };
document.getElementById('ai-cp').onclick = () => {
navigator.clipboard.writeText(lastRes).catch(() => {});
document.getElementById('ai-cp').textContent = '✅ Скопировано';
setTimeout(() => { document.getElementById('ai-cp').textContent = '📋 Копировать'; }, 1500);
};
}
// ══════════════════════════════════════
// ЛОГИ
// ══════════════════════════════════════
function renderLog(el) {
el.innerHTML = `
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">
<p class="brst" style="margin:0">История действий</p>
<button class="bb bb-red" id="log-clr" style="padding:5px 10px;font-size:11px">🗑 Очистить</button>
</div>
<div id="br-llist"></div>
`;
document.getElementById('log-clr').onclick = () => {
if (!confirm('Очистить логи?')) return;
logs = []; saveLogs(); renderLog(el);
};
const ll = document.getElementById('br-llist');
if (!logs.length) { ll.innerHTML = '<p style="color:var(--muted);font-size:12px">Логов нет.</p>'; return; }
[...logs].reverse().forEach(l => {
const d = document.createElement('div'); d.className = 'brlog';
d.innerHTML = `<span class="blt">${l.t}</span><div class="bla">${l.action}</div><div>${l.u} — ${l.detail}</div>`;
ll.appendChild(d);
});
}
// ══════════════════════════════════════
// DEV ПАНЕЛЬ
// ══════════════════════════════════════
function renderDev(el) {
if (!devOpen) {
el.innerHTML = `
<div id="brdl">
<h3>🔒 Dev-панель</h3>
<p>Введите пароль разработчика</p>
<div class="brfg"><input class="brin" type="password" id="dp-in" placeholder="Пароль..."/></div>
<button class="bb bb-acc" id="dp-btn">Войти</button>
</div>`;
document.getElementById('dp-btn').onclick = () => {
if (document.getElementById('dp-in').value === DEV_PASS) { devOpen = true; renderDev(el); }
else toast('❌ Неверный пароль', '#ff4f4f');
};
document.getElementById('dp-in').onkeydown = e => { if (e.key === 'Enter') document.getElementById('dp-btn').click(); };
return;
}
const groqKey = S.get('groq_key', GROQ_KEY_DEFAULT);
const tgCid = S.get('tg_cid', '');
el.innerHTML = `
<p class="brst">Статистика</p>
<div class="brstat"><span class="brsicon">📋</span><div><div class="brsval">${BUILTIN.length + myTpls.length}</div><div class="brslbl">Шаблонов всего</div></div></div>
<div class="brstat"><span class="brsicon">📊</span><div><div class="brsval">${logs.length}</div><div class="brslbl">Записей в логах</div></div></div>
<div class="brstat"><span class="brsicon">🤖</span><div>
<div class="brsval" style="color:${groqKey ? 'var(--grn)' : 'var(--red)'}; font-size:15px">${groqKey ? '✅ Groq активен' : '❌ Ключ не задан'}</div>
<div class="brslbl">Статус AI</div>
</div></div>
<hr style="border-color:var(--bd);margin:14px 0">
<p class="brst">Groq AI — API ключ</p>
<div class="brfg"><input class="brin" id="gk-in" type="password" value="${groqKey}" placeholder="gsk_..."/></div>
<div style="display:flex;gap:8px">
<button class="bb bb-acc" id="gk-save">💾 Сохранить</button>
<button class="bb bb-gh" id="gk-test">🧪 Тест</button>
</div>
<div id="gk-st" style="font-size:12px;margin-top:7px"></div>
<hr style="border-color:var(--bd);margin:14px 0">
<p class="brst">Telegram — Chat ID</p>
<div class="brfg"><input class="brin" id="tg-in" value="${tgCid}" placeholder="123456789"/></div>
<button class="bb bb-acc" id="tg-save">💾 Сохранить</button>
<hr style="border-color:var(--bd);margin:14px 0">
<p class="brst">Никнейм</p>
<div class="brfg"><input class="brin" id="nk-in" value="${nickname || ''}" placeholder="Никнейм..."/></div>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<button class="bb bb-acc" id="nk-save">💾 Сохранить</button>
<button class="bb bb-red" id="nk-reset">🗑 Сброс</button>
<button class="bb bb-red" id="all-reset">⚠️ Сброс всего</button>
</div>
<hr style="border-color:var(--bd);margin:14px 0">
<p class="brst">Связь с разработчиком</p>
<a class="brcontact" href="https://vk.ru/brunoverona" target="_blank">
<span style="font-size:20px">💙</span>
<div><div style="font-size:13px;color:var(--txt);font-weight:600">ВКонтакте</div><div style="font-size:11px;color:var(--muted)">vk.ru/brunoverona</div></div>
</a>
`;
document.getElementById('gk-save').onclick = () => {
S.set('groq_key', document.getElementById('gk-in').value.trim());
toast('✅ Groq ключ сохранён!');
};
document.getElementById('gk-test').onclick = async () => {
const st = document.getElementById('gk-st');
st.style.color = 'var(--muted)'; st.textContent = '⏳ Тестирую...';
const r = await groqAI('Ответь одним словом: работает');
if (r) { st.style.color = 'var(--grn)'; st.textContent = '✅ Groq: ' + r.substring(0, 50); }
else { st.style.color = 'var(--red)'; st.textContent = '❌ Не отвечает. Проверьте ключ.'; }
};
document.getElementById('tg-save').onclick = () => {
S.set('tg_cid', document.getElementById('tg-in').value.trim());
toast('✅ Chat ID сохранён!');
tg('✅ BR Admin Panel: Telegram подключён!');
};
document.getElementById('nk-save').onclick = () => {
const v = document.getElementById('nk-in').value.trim();
if (v) { nickname = v; S.set('nickname', v); document.getElementById('br-nn').textContent = `👤 ${v}`; toast('✅ Никнейм обновлён!'); }
};
document.getElementById('nk-reset').onclick = () => {
if (!confirm('Сбросить никнейм?')) return;
S.set('nickname', null); nickname = null;
document.getElementById('br-nn').textContent = '';
toast('Никнейм сброшен', '#888');
};
document.getElementById('all-reset').onclick = () => {
if (!confirm('⚠️ Удалить ВСЕ данные скрипта?')) return;
['nickname','my_tpls','logs','tg_cid','groq_key'].forEach(k => GM_setValue(k, ''));
location.reload();
};
}
})();