Created
December 14, 2025 07:40
-
-
Save intari/73ca7a0295e39b415516bfd75ac809df to your computer and use it in GitHub Desktop.
Простой анализатор текста на эмоции
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name LLM Analyzer Emotional analyizer | |
| // @namespace vikari-anatra | |
| // @version 0.9 | |
| // @description Extract main text via Readability.js and analyze with LLM. Modes: dual(default)=JSON+local pretty, pretty, json. Copy + toggle. | |
| // @match http*://*/* | |
| // @grant GM_xmlhttpRequest | |
| // @grant GM_registerMenuCommand | |
| // @grant GM_getValue | |
| // @grant GM_setValue | |
| // @require https://cdn.jsdelivr.net/npm/@mozilla/readability@0.5.0/Readability.js | |
| // @connect litellm.viorsan.com | |
| // ==/UserScript== | |
| (function () { | |
| 'use strict'; | |
| const DEFAULTS = Object.freeze({ | |
| endpoint: 'https://litellm.viorsan.com/v1/chat/completions', | |
| model: 'gpt-5-nano', | |
| apiKey: '', | |
| // Modes: 'dual' | 'pretty' | 'json' | |
| mode: 'dual', | |
| temperature: 0.2, | |
| maxChars: 60000, | |
| // Robust JSON | |
| jsonRetryCount: 2, | |
| jsonRetryTempBoost: 0.0, // usually keep 0 | |
| traceUserId: 'tampermonkey', | |
| generationName: 'emo-prop-score', | |
| debug: false | |
| }); | |
| // --- GM APIs from window (no /* global GM_* */) --- | |
| const gm = (typeof window !== 'undefined' && window) ? window : {}; | |
| const GM_xmlhttpRequest = gm.GM_xmlhttpRequest; | |
| const GM_registerMenuCommand = gm.GM_registerMenuCommand; | |
| const GM_getValue = gm.GM_getValue; | |
| const GM_setValue = gm.GM_setValue; | |
| if (!GM_xmlhttpRequest || !GM_registerMenuCommand || !GM_getValue || !GM_setValue) return; | |
| function dbg(cfg, ...args) { | |
| if (!cfg || !cfg.debug) return; | |
| // eslint-disable-next-line no-console | |
| console.log('[LLM Analyzer]', ...args); | |
| } | |
| /* ===================== PROMPTS ===================== */ | |
| function buildPromptPretty(text) { | |
| return ` | |
| Системные инструкции: | |
| Ты — аналитик текста. Работай строго по шагам алгоритма. Не добавляй внешние факты. Опирайся только на предоставленный текст. | |
| Формат ответа соблюдай. | |
| Шкалы: | |
| 1) Эмоциональная насыщенность (0..10): 0 — физика плазмы; 10 — максимум агитации. | |
| 2) Пропаганда (0..10): 0 — нейтрально; 10 — предельная пропаганда. | |
| Шаги: | |
| 1) Статус мира: реальный современный / реальный исторический / художественный или альтернативный / неясно. | |
| 2) Конфликт: явный реальный / социальный-идеологический / локальный / отсутствует. | |
| 2.5) Направленность текста: внутрь мира / наружу / обе. | |
| 3) Нормы: современные человеческие; если заданы иные — указать. | |
| 4) Пропаганда: НЕ считать автоматически художественность/односторонность/жанр/документ стороны. Считать: подмена анализа, давление, дегуманизация, мобилизация/оправдание, образ врага, символическое насилие. | |
| 4.5) Доп. проверки: | |
| Эмоции: плотность оценочной лексики; асимметрия эмоций; имплицитные эмоции (угрозы/неизбежность/нормализация насилия). | |
| Пропаганда: нормализация крайних действий; моральная бинарность; смещение агентности; инверсия причин-следствий. | |
| 5) Если художественный: оценивать как наш мир; отметить влияние художественной формы. | |
| Формат ответа: | |
| 1) Мир: ... | |
| 2) Конфликт: ... | |
| 2.5) Направленность: ... | |
| 3) Эмоциональная насыщенность: <0..10> (пункты) | |
| 4) Пропаганда: <0..10> (пункты) | |
| 5) Пристрастность автора: low/medium/high (почему) | |
| 6) Влияние статуса мира: ... | |
| 7) Доп. проверки: перечисли сработавшие | |
| 8) Уверенность: low/medium/high (почему) | |
| Текст: | |
| """${text}""" | |
| `.trim(); | |
| } | |
| function buildPromptJson(text) { | |
| return ` | |
| Системные инструкции: | |
| Верни только валидный JSON (без Markdown и комментариев). Не добавляй внешние факты, опирайся только на текст. | |
| КАЛИБРОВКА ШКАЛ: | |
| Эмоции 0..10: | |
| 0: техничка/наука/инструкции ("физика плазмы") без общественно-политического контекста. | |
| 1-2: нейтральная новость/справка. | |
| 3-4: публицистичность/акценты без агрессии. | |
| 5-6: сильное риторическое давление. | |
| 7-8: агрессивная обвинительная подача. | |
| 9-10: агитация/мобилизация максимум. | |
| Пропаганда 0..10: | |
| 0: почти только тех/научное или крайне сухая справка без риторики. | |
| 1-2: нейтральный отчёт/новость о политике без дегуманизации и мобилизации. | |
| 3-4: заметный фрейминг, но без дегуманизации. | |
| 5-6: явное оправдание/обвинение + давление. | |
| 7-8: устойчивый образ врага, дегуманизация. | |
| 9-10: предельная агитация. | |
| ОСОБОЕ ПРАВИЛО: | |
| Если текст относится к политике/обществу/конфликту (не техничка): | |
| - emotional_intensity минимум 1. | |
| - propaganda_intensity: 0 только если почти техническая сводка/документ без риторики; иначе минимум 1. | |
| Если ставишь 0 — заполни zero_justification. | |
| Направленность текста (text_direction): | |
| inward | outward | both. | |
| Доп. проверки (обязательные флаги): | |
| Эмоции: emotional_density_high, emotional_asymmetry, implicit_emotional_pressure | |
| Пропаганда: normalization_of_extreme_actions, moral_binarization, agency_shift, cause_effect_inversion | |
| Верни JSON строго по схеме: | |
| { | |
| "world_status": "real_modern|real_historical|fictional_or_alt|unclear", | |
| "world_status_rationale": ["...","..."], | |
| "conflict_type": "real_conflict|social_ideological|local_personal|none", | |
| "conflict_rationale": ["...","..."], | |
| "text_direction": "inward|outward|both", | |
| "text_direction_rationale": ["...","..."], | |
| "emotional_intensity": 0.0, | |
| "emotional_reasons": ["...","...","..."], | |
| "propaganda_intensity": 0.0, | |
| "propaganda_reasons": ["...","...","..."], | |
| "propaganda_markers": { | |
| "analysis_replaced_by_assertion": true/false, | |
| "emotional_pressure": true/false, | |
| "dehumanization": true/false, | |
| "mobilizing_or_justifying": true/false, | |
| "enemy_image": true/false, | |
| "symbolic_violence": true/false | |
| }, | |
| "additional_checks": { | |
| "emotional_density_high": true/false, | |
| "emotional_asymmetry": true/false, | |
| "implicit_emotional_pressure": true/false, | |
| "normalization_of_extreme_actions": true/false, | |
| "moral_binarization": true/false, | |
| "agency_shift": true/false, | |
| "cause_effect_inversion": true/false | |
| }, | |
| "author_bias": "low|medium|high", | |
| "bias_notes": ["..."], | |
| "effect_of_world_status_on_score": ["..."], | |
| "effect_of_artistic_form": ["none_or_not_applicable" or "raises" or "lowers", "notes..."], | |
| "confidence": "low|medium|high", | |
| "confidence_notes": ["..."], | |
| "zero_justification": "" | |
| } | |
| Текст: | |
| """${text}""" | |
| `.trim(); | |
| } | |
| /* ===================== SETTINGS ===================== */ | |
| function getNumberSetting(key, fallback) { | |
| const v = Number(GM_getValue(key, fallback)); | |
| return Number.isFinite(v) ? v : fallback; | |
| } | |
| function getStringSetting(key, fallback) { | |
| const v = GM_getValue(key, fallback); | |
| return (typeof v === 'string' && v.trim()) ? v.trim() : fallback; | |
| } | |
| function getBoolSetting(key, fallback) { | |
| const v = GM_getValue(key, fallback); | |
| if (v === true || v === false) return v; | |
| if (v === 'true') return true; | |
| if (v === 'false') return false; | |
| return fallback; | |
| } | |
| function getSettings() { | |
| const mode = getStringSetting('mode', DEFAULTS.mode); | |
| return { | |
| endpoint: getStringSetting('endpoint', DEFAULTS.endpoint), | |
| model: getStringSetting('model', DEFAULTS.model), | |
| apiKey: getStringSetting('apiKey', DEFAULTS.apiKey), | |
| mode: ['dual', 'pretty', 'json'].includes(mode) ? mode : DEFAULTS.mode, | |
| temperature: getNumberSetting('temperature', DEFAULTS.temperature), | |
| maxChars: getNumberSetting('maxChars', DEFAULTS.maxChars), | |
| jsonRetryCount: Math.max(0, Math.floor(getNumberSetting('jsonRetryCount', DEFAULTS.jsonRetryCount))), | |
| jsonRetryTempBoost: getNumberSetting('jsonRetryTempBoost', DEFAULTS.jsonRetryTempBoost), | |
| traceUserId: getStringSetting('traceUserId', DEFAULTS.traceUserId), | |
| generationName: getStringSetting('generationName', DEFAULTS.generationName), | |
| debug: getBoolSetting('debug', DEFAULTS.debug) | |
| }; | |
| } | |
| function openSettings() { | |
| const cfg = getSettings(); | |
| // eslint-disable-next-line no-restricted-globals | |
| const apiKey = prompt('API key (вставь без "Bearer "):', cfg.apiKey || ''); | |
| if (apiKey !== null) GM_setValue('apiKey', apiKey.trim()); | |
| // eslint-disable-next-line no-restricted-globals | |
| const model = prompt('Model:', cfg.model); | |
| if (model !== null) GM_setValue('model', model.trim()); | |
| // eslint-disable-next-line no-restricted-globals | |
| const temperature = prompt('Temperature (0..2):', String(cfg.temperature)); | |
| if (temperature !== null && temperature.trim() !== '') GM_setValue('temperature', Number(temperature)); | |
| // eslint-disable-next-line no-restricted-globals | |
| const mode = prompt('Mode: dual | pretty | json', cfg.mode); | |
| if (mode !== null && ['dual', 'pretty', 'json'].includes(mode.trim())) GM_setValue('mode', mode.trim()); | |
| // eslint-disable-next-line no-restricted-globals | |
| const retry = prompt('JSON retries if invalid (0..5):', String(cfg.jsonRetryCount)); | |
| if (retry !== null && retry.trim() !== '') GM_setValue('jsonRetryCount', Number(retry)); | |
| // eslint-disable-next-line no-restricted-globals | |
| const debug = prompt('Debug logs? 0/1', cfg.debug ? '1' : '0'); | |
| if (debug !== null) GM_setValue('debug', debug.trim() === '1'); | |
| toast('Settings saved'); | |
| } | |
| function toggleMode() { | |
| const cfg = getSettings(); | |
| const order = ['dual', 'pretty', 'json']; | |
| const idx = Math.max(0, order.indexOf(cfg.mode)); | |
| const next = order[(idx + 1) % order.length]; | |
| GM_setValue('mode', next); | |
| toast(`Mode: ${next.toUpperCase()}`); | |
| } | |
| /* ===================== TEXT EXTRACTION ===================== */ | |
| function safeHost() { | |
| try { | |
| return (typeof location !== 'undefined' && location.hostname) ? location.hostname : 'unknown'; | |
| } catch (_) { | |
| return 'unknown'; | |
| } | |
| } | |
| function extractMainText(cfg) { | |
| if (!document || !document.body) return ''; | |
| try { | |
| const clone = document.cloneNode(true); | |
| clone.querySelectorAll('script, style, noscript').forEach((e) => e.remove()); | |
| const ReaderCtor = (typeof window !== 'undefined' && window.Readability) ? window.Readability : null; | |
| if (ReaderCtor) { | |
| const reader = new ReaderCtor(clone, { charThreshold: 400 }); | |
| const article = reader.parse(); | |
| const content = (article && typeof article.textContent === 'string') ? article.textContent.trim() : ''; | |
| if (content.length > 300) return content; | |
| } | |
| } catch (e) { | |
| dbg(cfg, 'Readability failed', e); | |
| } | |
| const fallback = (document.body && typeof document.body.innerText === 'string') ? document.body.innerText.trim() : ''; | |
| return fallback; | |
| } | |
| function getTextForAnalysis(cfg) { | |
| try { | |
| const sel = (window.getSelection && window.getSelection()) | |
| ? String(window.getSelection().toString() || '').trim() | |
| : ''; | |
| if (sel.length > 200) return sel; | |
| } catch (e) { | |
| dbg(cfg, 'Selection read failed', e); | |
| } | |
| return extractMainText(cfg); | |
| } | |
| /* ===================== UI ===================== */ | |
| function escapeHtml(str) { | |
| return String(str).replace(/[&<>"']/g, (m) => ({ | |
| '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' | |
| }[m])); | |
| } | |
| function btnStyle() { | |
| return 'background:#2b2b2b;border:1px solid #555;color:#eee;border-radius:10px;padding:6px 10px;cursor:pointer;'; | |
| } | |
| function toast(msg) { | |
| const id = '__llm_toast'; | |
| const old = document.getElementById(id); | |
| if (old) old.remove(); | |
| const el = document.createElement('div'); | |
| el.id = id; | |
| el.textContent = msg; | |
| el.style = 'position:fixed;left:16px;bottom:16px;z-index:999999;background:rgba(20,20,20,0.95);color:#eee;border:1px solid #444;border-radius:12px;padding:10px 12px;font:13px/1.2 system-ui;box-shadow:0 10px 30px rgba(0,0,0,.45);'; | |
| document.body.appendChild(el); | |
| setTimeout(() => el.remove(), 1800); | |
| } | |
| async function copyToClipboard(text, cfg) { | |
| try { | |
| if (navigator.clipboard && navigator.clipboard.writeText) { | |
| await navigator.clipboard.writeText(String(text)); | |
| return true; | |
| } | |
| } catch (e) { | |
| dbg(cfg, 'Clipboard API failed', e); | |
| } | |
| try { | |
| const ta = document.createElement('textarea'); | |
| ta.value = String(text); | |
| ta.style.position = 'fixed'; | |
| ta.style.left = '-9999px'; | |
| document.body.appendChild(ta); | |
| ta.focus(); | |
| ta.select(); | |
| const ok = document.execCommand('copy'); | |
| ta.remove(); | |
| return ok; | |
| } catch (e) { | |
| dbg(cfg, 'execCommand copy failed', e); | |
| return false; | |
| } | |
| } | |
| function showOverlay(cfg, title, content, metaLine) { | |
| const old = document.getElementById('__llm_overlay'); | |
| if (old) old.remove(); | |
| const wrap = document.createElement('div'); | |
| wrap.id = '__llm_overlay'; | |
| wrap.style = 'position:fixed;top:16px;right:16px;z-index:999999;width:min(920px,calc(100vw - 32px));max-height:calc(100vh - 32px);background:rgba(18,18,18,0.97);color:#eee;border-radius:14px;box-shadow:0 20px 50px rgba(0,0,0,.6);padding:14px;overflow:auto;font:13px/1.45 system-ui;'; | |
| wrap.innerHTML = ` | |
| <div style="display:flex;justify-content:space-between;align-items:center;gap:10px;margin-bottom:10px;"> | |
| <div style="display:flex;flex-direction:column;gap:2px;min-width:0;"> | |
| <strong style="font-size:14px;">${escapeHtml(title)}</strong> | |
| <div style="opacity:.75;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${escapeHtml(metaLine || '')}</div> | |
| </div> | |
| <div style="display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end;"> | |
| <button id="__llm_copy" style="${btnStyle()}">Copy</button> | |
| <button id="__llm_mode" style="${btnStyle()}">Mode: ${escapeHtml(String(cfg.mode || '').toUpperCase())}</button> | |
| <button id="__llm_close" style="${btnStyle()}">Close</button> | |
| </div> | |
| </div> | |
| <pre style="white-space:pre-wrap;margin:0;word-break:break-word;">${escapeHtml(content || '')}</pre> | |
| `; | |
| document.body.appendChild(wrap); | |
| const closeBtn = document.getElementById('__llm_close'); | |
| const modeBtn = document.getElementById('__llm_mode'); | |
| const copyBtn = document.getElementById('__llm_copy'); | |
| if (closeBtn) closeBtn.onclick = () => wrap.remove(); | |
| if (modeBtn) modeBtn.onclick = () => { toggleMode(); wrap.remove(); }; | |
| if (copyBtn) { | |
| copyBtn.onclick = async () => { | |
| const ok = await copyToClipboard(content || '', cfg); | |
| toast(ok ? 'Copied ✅' : 'Copy failed ❌'); | |
| }; | |
| } | |
| } | |
| /* ===================== LLM + DUAL LOCAL ===================== */ | |
| function buildMetadata(cfg, stage) { | |
| const host = safeHost(); | |
| const now = Date.now(); | |
| const rid = Math.random().toString(36).slice(2); | |
| return { | |
| generation_name: cfg.generationName, | |
| generation_id: `gen-${now}-${stage || 'single'}`, | |
| trace_id: `trace-${host}-${rid}`, | |
| trace_user_id: cfg.traceUserId, | |
| stage: stage || 'single' | |
| }; | |
| } | |
| function callLLM(cfg, prompt, stage, temperatureOverride) { | |
| const temp = Number.isFinite(temperatureOverride) ? temperatureOverride : cfg.temperature; | |
| const payload = { | |
| model: cfg.model, | |
| messages: [{ role: 'user', content: prompt }], | |
| temperature: temp, | |
| metadata: buildMetadata(cfg, stage) | |
| }; | |
| return new Promise((resolve, reject) => { | |
| GM_xmlhttpRequest({ | |
| method: 'POST', | |
| url: cfg.endpoint, | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Authorization': `Bearer ${cfg.apiKey}` | |
| }, | |
| data: JSON.stringify(payload), | |
| onload: (resp) => { | |
| try { | |
| const data = JSON.parse(resp.responseText); | |
| const out = data?.choices?.[0]?.message?.content ?? ''; | |
| resolve({ ok: true, raw: out, full: data, status: resp.status }); | |
| } catch (_) { | |
| reject(new Error(`Bad wrapper JSON from endpoint (HTTP ${resp.status})`)); | |
| } | |
| }, | |
| onerror: () => reject(new Error('Network/CORS error')) | |
| }); | |
| }); | |
| } | |
| function tryParseJsonStrict(s) { | |
| const t = String(s || '').trim(); | |
| if (!t.startsWith('{') || !t.endsWith('}')) return null; | |
| try { return JSON.parse(t); } catch (_) { return null; } | |
| } | |
| function prettyJson(obj) { | |
| try { return JSON.stringify(obj, null, 2); } catch (_) { return String(obj); } | |
| } | |
| function mapWorldStatus(ws) { | |
| const m = { | |
| real_modern: 'реальный современный мир', | |
| real_historical: 'реальный, но исторический', | |
| fictional_or_alt: 'художественный / альтернативный / иной мир', | |
| unclear: 'статус мира неясен' | |
| }; | |
| return m[ws] || String(ws || 'неизвестно'); | |
| } | |
| function mapConflict(ct) { | |
| const m = { | |
| real_conflict: 'явный реальный конфликт', | |
| social_ideological: 'социальный / идеологический конфликт', | |
| local_personal: 'локальный конфликт персонажей', | |
| none: 'конфликт отсутствует' | |
| }; | |
| return m[ct] || String(ct || 'неизвестно'); | |
| } | |
| function mapDirection(d) { | |
| const m = { | |
| inward: 'внутрь мира (для персонажей/участников)', | |
| outward: 'наружу (для читателя/наблюдателя)', | |
| both: 'одновременно внутрь и наружу' | |
| }; | |
| return m[d] || String(d || 'неизвестно'); | |
| } | |
| function bullets(arr, max = 7) { | |
| if (!Array.isArray(arr) || arr.length === 0) return '—'; | |
| return arr.slice(0, max).map((x) => `- ${String(x)}`).join('\n'); | |
| } | |
| function listTrueChecks(checks) { | |
| if (!checks || typeof checks !== 'object') return []; | |
| return Object.entries(checks) | |
| .filter(([, v]) => v === true) | |
| .map(([k]) => k); | |
| } | |
| function humanizeCheckKey(k) { | |
| const map = { | |
| emotional_density_high: 'плотность оценочной/эмоциональной лексики высокая', | |
| emotional_asymmetry: 'асимметрия эмоций (в основном на одну сторону)', | |
| implicit_emotional_pressure: 'имплицитное эмоциональное давление (угрозы/неизбежность/нормализация насилия)', | |
| normalization_of_extreme_actions: 'нормализация крайних действий как естественных/неизбежных', | |
| moral_binarization: 'моральная бинарность (упрощение до “хорошие/плохие”)', | |
| agency_shift: 'смещение агентности (кто субъект, кто “фактор/проблема”)', | |
| cause_effect_inversion: 'инверсия причин и следствий' | |
| }; | |
| return map[k] || k; | |
| } | |
| function clamp01to10(n) { | |
| const x = Number(n); | |
| if (!Number.isFinite(x)) return null; | |
| if (x < 0) return 0; | |
| if (x > 10) return 10; | |
| return x; | |
| } | |
| function buildLocalPrettyFromJson(j) { | |
| const ws = mapWorldStatus(j.world_status); | |
| const ct = mapConflict(j.conflict_type); | |
| const td = mapDirection(j.text_direction); | |
| const emotional = clamp01to10(j.emotional_intensity); | |
| const propaganda = clamp01to10(j.propaganda_intensity); | |
| const checks = listTrueChecks(j.additional_checks).map(humanizeCheckKey); | |
| const checksText = checks.length ? checks.map((x) => `- ${x}`).join('\n') : '—'; | |
| const worldWhy = bullets(j.world_status_rationale, 4); | |
| const conflictWhy = bullets(j.conflict_rationale, 4); | |
| const dirWhy = bullets(j.text_direction_rationale, 4); | |
| const bias = String(j.author_bias || 'unknown'); | |
| const biasNotes = bullets(j.bias_notes, 4); | |
| const worldEffect = bullets(j.effect_of_world_status_on_score, 4); | |
| const artEffect = Array.isArray(j.effect_of_artistic_form) | |
| ? `- ${String(j.effect_of_artistic_form[0] || '')}\n- ${String(j.effect_of_artistic_form[1] || '')}`.trim() | |
| : '—'; | |
| const conf = String(j.confidence || 'unknown'); | |
| const confNotes = bullets(j.confidence_notes, 4); | |
| const zeroJust = String(j.zero_justification || '').trim(); | |
| const zeroLine = zeroJust ? `\nПримечание о нулях: ${zeroJust}\n` : ''; | |
| return [ | |
| `1) Мир: ${ws}`, | |
| `${worldWhy !== '—' ? worldWhy : ''}`.trim(), | |
| `\n2) Конфликт: ${ct}`, | |
| `${conflictWhy !== '—' ? conflictWhy : ''}`.trim(), | |
| `\n2.5) Направленность: ${td}`, | |
| `${dirWhy !== '—' ? dirWhy : ''}`.trim(), | |
| `\n3) Эмоциональная насыщенность: ${emotional !== null ? emotional : '—'}`, | |
| bullets(j.emotional_reasons, 7), | |
| `\n4) Пропагандистская направленность: ${propaganda !== null ? propaganda : '—'}`, | |
| bullets(j.propaganda_reasons, 7), | |
| `\n4.5) Дополнительные проверки (сработали):`, | |
| checksText, | |
| `\n5) Пристрастность автора: ${bias}`, | |
| biasNotes, | |
| `\n6) Влияние статуса мира на итог:`, | |
| worldEffect, | |
| `\n7) Художественная форма:`, | |
| artEffect, | |
| `\n8) Уверенность: ${conf}`, | |
| confNotes, | |
| zeroLine | |
| ].filter(Boolean).join('\n').replace(/\n{3,}/g, '\n\n').trim(); | |
| } | |
| async function runDual(cfg, text) { | |
| let attempts = 0; | |
| let jsonObj = null; | |
| let lastRaw = ''; | |
| while (attempts <= cfg.jsonRetryCount) { | |
| const temp = cfg.temperature + (attempts > 0 ? cfg.jsonRetryTempBoost : 0); | |
| const res = await callLLM(cfg, buildPromptJson(text), 'json', temp); | |
| lastRaw = res.raw || ''; | |
| jsonObj = tryParseJsonStrict(lastRaw); | |
| if (jsonObj) break; | |
| attempts += 1; | |
| dbg(cfg, 'JSON invalid, retry', attempts, lastRaw.slice(0, 200)); | |
| } | |
| if (!jsonObj) { | |
| return `DUAL MODE FAILED: could not parse JSON after ${cfg.jsonRetryCount + 1} attempts.\n\nRaw output:\n${lastRaw}`; | |
| } | |
| const pretty = buildLocalPrettyFromJson(jsonObj); | |
| const jsonBlock = prettyJson(jsonObj); | |
| return `${pretty}\n\n---\nJSON (source of truth):\n${jsonBlock}\n`; | |
| } | |
| async function runSinglePretty(cfg, text) { | |
| const res = await callLLM(cfg, buildPromptPretty(text), 'pretty'); | |
| return (res.raw || '').trim(); | |
| } | |
| async function runSingleJson(cfg, text) { | |
| const res = await callLLM(cfg, buildPromptJson(text), 'json'); | |
| return (res.raw || '').trim(); | |
| } | |
| /* ===================== MAIN ===================== */ | |
| async function runAnalysis() { | |
| const cfg = getSettings(); | |
| if (!cfg.apiKey) { | |
| toast('API key not set: open Settings'); | |
| return; | |
| } | |
| const rawText = getTextForAnalysis(cfg); | |
| if (!rawText || rawText.length < 200) { | |
| showOverlay(cfg, 'LLM Analyzer', 'Недостаточно текста для анализа (слишком мало контента/выделения).', `${cfg.model} | temp=${cfg.temperature}`); | |
| return; | |
| } | |
| const text = rawText.slice(0, cfg.maxChars); | |
| showOverlay(cfg, 'LLM Analyzer', 'Анализирую…', `${cfg.model} | temp=${cfg.temperature} | mode=${cfg.mode} | chars=${text.length}`); | |
| try { | |
| let out = ''; | |
| if (cfg.mode === 'pretty') out = await runSinglePretty(cfg, text); | |
| else if (cfg.mode === 'json') out = await runSingleJson(cfg, text); | |
| else out = await runDual(cfg, text); | |
| showOverlay(cfg, 'LLM Analyzer — Result', out, `${cfg.model} | temp=${cfg.temperature} | ${safeHost()} | mode=${cfg.mode}`); | |
| } catch (e) { | |
| showOverlay(cfg, 'LLM Analyzer — Error', String(e && e.message ? e.message : e), `${cfg.model} | temp=${cfg.temperature} | mode=${cfg.mode}`); | |
| } | |
| } | |
| /* ===================== HOTKEYS / MENU ===================== */ | |
| GM_registerMenuCommand('LLM Analyzer: Run (Alt+L)', runAnalysis); | |
| GM_registerMenuCommand('LLM Analyzer: Toggle Mode (Alt+M)', toggleMode); | |
| GM_registerMenuCommand('LLM Analyzer: Settings', openSettings); | |
| window.addEventListener('keydown', (e) => { | |
| if (!e || !e.altKey) return; | |
| const k = String(e.key || '').toLowerCase(); | |
| if (k === 'l') runAnalysis(); | |
| if (k === 'm') toggleMode(); | |
| }); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment