<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>⚡ LLM Benchmark Pro — Ultimate Testing Suite</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=Inter:wght@300;400;600;700;900&display=swap');
:root {
--neon-green: #00ff88;
--neon-blue: #00d4ff;
--neon-purple: #a855f7;
--neon-red: #ff3366;
--neon-yellow: #ffcc00;
--dark-bg: #0a0e17;
--card-bg: #111827;
--card-border: #1e293b;
}
* { box-sizing: border-box; }
body {
font-family: 'Inter', sans-serif;
background: var(--dark-bg);
color: #e2e8f0;
min-height: 100vh;
}
.font-mono { font-family: 'JetBrains Mono', monospace; }
.glow-green { text-shadow: 0 0 20px rgba(0,255,136,0.5); }
.glow-blue { text-shadow: 0 0 20px rgba(0,212,255,0.5); }
.glow-purple { text-shadow: 0 0 20px rgba(168,85,247,0.5); }
.card {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 16px;
transition: all 0.3s ease;
}
.card:hover { border-color: #334155; box-shadow: 0 0 30px rgba(0,212,255,0.05); }
.btn-primary {
background: linear-gradient(135deg, #00ff88, #00d4ff);
color: #0a0e17;
font-weight: 700;
padding: 10px 24px;
border-radius: 12px;
border: none;
cursor: pointer;
transition: all 0.3s;
font-size: 14px;
}
.btn-primary:hover { transform: translateY(-2px); box-shadow: 0 8px 25px rgba(0,255,136,0.3); }
.btn-primary:disabled { opacity: 0.4; cursor: not-allowed; transform: none; box-shadow: none; }
.btn-danger {
background: linear-gradient(135deg, #ff3366, #ff6b35);
color: white;
font-weight: 700;
padding: 10px 24px;
border-radius: 12px;
border: none;
cursor: pointer;
transition: all 0.3s;
font-size: 14px;
}
.btn-danger:hover { transform: translateY(-2px); box-shadow: 0 8px 25px rgba(255,51,102,0.3); }
.btn-secondary {
background: #1e293b;
color: #94a3b8;
font-weight: 600;
padding: 10px 24px;
border-radius: 12px;
border: 1px solid #334155;
cursor: pointer;
transition: all 0.3s;
font-size: 14px;
}
.btn-secondary:hover { background: #334155; color: #e2e8f0; }
.input-field {
background: #0f172a;
border: 1px solid #1e293b;
border-radius: 12px;
padding: 10px 16px;
color: #e2e8f0;
font-size: 14px;
width: 100%;
transition: border-color 0.3s;
}
.input-field:focus { outline: none; border-color: #00d4ff; box-shadow: 0 0 15px rgba(0,212,255,0.1); }
.progress-bar {
height: 8px;
background: #1e293b;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
border-radius: 4px;
transition: width 0.5s ease;
background: linear-gradient(90deg, #00ff88, #00d4ff);
}
.status-dot {
width: 10px; height: 10px;
border-radius: 50%;
display: inline-block;
}
.status-dot.online { background: #00ff88; box-shadow: 0 0 8px #00ff88; }
.status-dot.offline { background: #ff3366; box-shadow: 0 0 8px #ff3366; }
.status-dot.checking { background: #ffcc00; box-shadow: 0 0 8px #ffcc00; animation: pulse 1s infinite; }
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-in { animation: slideIn 0.4s ease; }
.tab-btn {
padding: 10px 20px;
border-radius: 10px;
cursor: pointer;
font-weight: 600;
font-size: 14px;
border: 1px solid transparent;
background: transparent;
color: #64748b;
transition: all 0.3s;
}
.tab-btn.active {
background: #1e293b;
color: #00d4ff;
border-color: #00d4ff33;
}
.tab-btn:hover:not(.active) { color: #94a3b8; }
.test-result-card {
border-left: 4px solid;
padding: 12px 16px;
margin-bottom: 8px;
border-radius: 0 8px 8px 0;
background: #0f172a;
}
.test-result-card.pass { border-left-color: #00ff88; }
.test-result-card.fail { border-left-color: #ff3366; }
.test-result-card.partial { border-left-color: #ffcc00; }
.scrollbar-thin::-webkit-scrollbar { width: 6px; }
.scrollbar-thin::-webkit-scrollbar-track { background: #0f172a; }
.scrollbar-thin::-webkit-scrollbar-thumb { background: #334155; border-radius: 3px; }
.hero-gradient {
background: linear-gradient(135deg, rgba(0,255,136,0.1), rgba(0,212,255,0.1), rgba(168,85,247,0.1));
border: 1px solid rgba(0,212,255,0.15);
}
.badge {
display: inline-block;
padding: 3px 10px;
border-radius: 20px;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.loop-detect {
background: repeating-linear-gradient(
45deg,
transparent,
transparent 10px,
rgba(255,51,102,0.05) 10px,
rgba(255,51,102,0.05) 20px
);
}
/* Modal styles */
.modal-overlay { background: rgba(0,0,0,0.8); backdrop-filter: blur(4px); }
.diff-del { background: rgba(255,51,102,0.2); color: #ff88a0; text-decoration: line-through; }
.diff-add { background: rgba(0,255,136,0.2); color: #88ffc0; }
.thinking-block {
border-left: 2px solid #a855f7;
}
</style>
<style>*, ::before, ::after{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgb(59 130 246 / 0.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgb(59 130 246 / 0.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/* ! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com */*,::after,::before{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}::after,::before{--tw-content:''}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;font-family:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0px}.bottom-6{bottom:1.5rem}.right-0{right:0px}.right-4{right:1rem}.right-6{right:1.5rem}.top-0{top:0px}.top-4{top:1rem}.left-0{left:0px}.right-3{right:0.75rem}.top-3{top:0.75rem}.z-40{z-index:40}.z-50{z-index:50}.float-right{float:right}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:0.25rem}.mb-2{margin-bottom:0.5rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mt-1{margin-top:0.25rem}.mt-3{margin-top:0.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mr-2{margin-right:0.5rem}.mt-2{margin-top:0.5rem}.mt-0\.5{margin-top:0.125rem}.ml-1{margin-left:0.25rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.h-32{height:8rem}.h-\[600px\]{height:600px}.h-\[800px\]{height:800px}.h-full{height:100%}.max-h-\[500px\]{max-height:500px}.max-h-40{max-height:10rem}.max-h-32{max-height:8rem}.w-32{width:8rem}.w-full{width:100%}.min-w-\[120px\]{min-width:120px}.min-w-\[200px\]{min-width:200px}.max-w-2xl{max-width:42rem}.max-w-7xl{max-width:80rem}.max-w-\[300px\]{max-width:300px}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.translate-y-10{--tw-translate-y:2.5rem;transform:translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes bounce{0%, 100%{transform:translateY(-25%);animation-timing-function:cubic-bezier(0.8,0,1,1)}50%{transform:none;animation-timing-function:cubic-bezier(0,0,0.2,1)}}.animate-bounce{animation:bounce 1s infinite}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite}.cursor-pointer{cursor:pointer}.cursor-help{cursor:help}.select-none{-webkit-user-select:none;user-select:none}.grid-cols-1{grid-template-columns:repeat(1, minmax(0, 1fr))}.grid-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr))}.grid-cols-3{grid-template-columns:repeat(3, minmax(0, 1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:0.25rem}.gap-2{gap:0.5rem}.gap-3{gap:0.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-y-2 > :not([hidden]) ~ :not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0.5rem * var(--tw-space-y-reverse))}.space-y-3 > :not([hidden]) ~ :not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0.75rem * var(--tw-space-y-reverse))}.space-y-4 > :not([hidden]) ~ :not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6 > :not([hidden]) ~ :not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.divide-y > :not([hidden]) ~ :not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.divide-\[\#1e293b\] > :not([hidden]) ~ :not([hidden]){--tw-divide-opacity:1;border-color:rgb(30 41 59 / var(--tw-divide-opacity, 1))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:0.5rem}.rounded-xl{border-radius:0.75rem}.rounded{border-radius:0.25rem}.rounded-br-lg{border-bottom-right-radius:0.5rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-l-4{border-left-width:4px}.border-l-2{border-left-width:2px}.border-t{border-top-width:1px}.border-r{border-right-width:1px}.border-\[\#00d4ff\]{--tw-border-opacity:1;border-color:rgb(0 212 255 / var(--tw-border-opacity, 1))}.border-\[\#1e293b\]{--tw-border-opacity:1;border-color:rgb(30 41 59 / var(--tw-border-opacity, 1))}.border-\[\#334155\]{--tw-border-opacity:1;border-color:rgb(51 65 85 / var(--tw-border-opacity, 1))}.border-\[\#ff3366\]{--tw-border-opacity:1;border-color:rgb(255 51 102 / var(--tw-border-opacity, 1))}.border-\[\#00d4ff33\]{border-color:#00d4ff33}.border-transparent{border-color:transparent}.border-\[\#ff3366\]\/30{border-color:rgb(255 51 102 / 0.3)}.bg-\[\#00d4ff\]{--tw-bg-opacity:1;background-color:rgb(0 212 255 / var(--tw-bg-opacity, 1))}.bg-\[\#0a0e17\]{--tw-bg-opacity:1;background-color:rgb(10 14 23 / var(--tw-bg-opacity, 1))}.bg-\[\#0f172a\]{--tw-bg-opacity:1;background-color:rgb(15 23 42 / var(--tw-bg-opacity, 1))}.bg-\[\#111827\]{--tw-bg-opacity:1;background-color:rgb(17 24 39 / var(--tw-bg-opacity, 1))}.bg-\[\#1e293b\]{--tw-bg-opacity:1;background-color:rgb(30 41 59 / var(--tw-bg-opacity, 1))}.bg-\[\#a855f7\]{--tw-bg-opacity:1;background-color:rgb(168 85 247 / var(--tw-bg-opacity, 1))}.bg-\[var\(--dark-bg\)\]{background-color:var(--dark-bg)}.bg-\[\#00ff8822\]{background-color:#00ff8822}.bg-\[\#ff336622\]{background-color:#ff336622}.bg-\[\#ff3366\]\/10{background-color:rgb(255 51 102 / 0.1)}.bg-black\/50{background-color:rgb(0 0 0 / 0.5)}.bg-\[\#ffcc0022\]{background-color:#ffcc0022}.bg-\[\#ff3366\]\/5{background-color:rgb(255 51 102 / 0.05)}.bg-gradient-to-r{background-image:linear-gradient(to right, var(--tw-gradient-stops))}.from-\[\#00ff88\]{--tw-gradient-from:#00ff88 var(--tw-gradient-from-position);--tw-gradient-to:rgb(0 255 136 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from), var(--tw-gradient-to)}.from-\[\#ff3366\]\/20{--tw-gradient-from:rgb(255 51 102 / 0.2) var(--tw-gradient-from-position);--tw-gradient-to:rgb(255 51 102 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from), var(--tw-gradient-to)}.via-\[\#00d4ff\]{--tw-gradient-to:rgb(0 212 255 / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from), #00d4ff var(--tw-gradient-via-position), var(--tw-gradient-to)}.to-\[\#a855f7\]{--tw-gradient-to:#a855f7 var(--tw-gradient-to-position)}.to-transparent{--tw-gradient-to:transparent var(--tw-gradient-to-position)}.bg-clip-text{-webkit-background-clip:text;background-clip:text}.p-0{padding:0px}.p-2{padding:0.5rem}.p-3{padding:0.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.p-1{padding:0.25rem}.px-3{padding-left:0.75rem;padding-right:0.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-1{padding-top:0.25rem;padding-bottom:0.25rem}.py-1\.5{padding-top:0.375rem;padding-bottom:0.375rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-2{padding-top:0.5rem;padding-bottom:0.5rem}.py-3{padding-top:0.75rem;padding-bottom:0.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.px-2{padding-left:0.5rem;padding-right:0.5rem}.px-1\.5{padding-left:0.375rem;padding-right:0.375rem}.py-0\.5{padding-top:0.125rem;padding-bottom:0.125rem}.px-1{padding-left:0.25rem;padding-right:0.25rem}.pt-2{padding-top:0.5rem}.pt-4{padding-top:1rem}.pl-2{padding-left:0.5rem}.pt-7{padding-top:1.75rem}.pr-4{padding-right:1rem}.pr-8{padding-right:2rem}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace}.font-sans{font-family:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-5xl{font-size:3rem;line-height:1}.text-6xl{font-size:3.75rem;line-height:1}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:0.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:0.75rem;line-height:1rem}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.font-black{font-weight:900}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.uppercase{text-transform:uppercase}.italic{font-style:italic}.leading-none{line-height:1}.tracking-tight{letter-spacing:-0.025em}.tracking-wider{letter-spacing:0.05em}.text-\[\#00d4ff\]{--tw-text-opacity:1;color:rgb(0 212 255 / var(--tw-text-opacity, 1))}.text-\[\#00ff88\]{--tw-text-opacity:1;color:rgb(0 255 136 / var(--tw-text-opacity, 1))}.text-\[\#ff3366\]{--tw-text-opacity:1;color:rgb(255 51 102 / var(--tw-text-opacity, 1))}.text-\[\#ffcc00\]{--tw-text-opacity:1;color:rgb(255 204 0 / var(--tw-text-opacity, 1))}.text-slate-400{--tw-text-opacity:1;color:rgb(148 163 184 / var(--tw-text-opacity, 1))}.text-slate-500{--tw-text-opacity:1;color:rgb(100 116 139 / var(--tw-text-opacity, 1))}.text-transparent{color:transparent}.text-\[\#e2e8f0\]{--tw-text-opacity:1;color:rgb(226 232 240 / var(--tw-text-opacity, 1))}.text-slate-200{--tw-text-opacity:1;color:rgb(226 232 240 / var(--tw-text-opacity, 1))}.text-slate-600{--tw-text-opacity:1;color:rgb(71 85 105 / var(--tw-text-opacity, 1))}.text-slate-300{--tw-text-opacity:1;color:rgb(203 213 225 / var(--tw-text-opacity, 1))}.accent-\[\#00d4ff\]{accent-color:#00d4ff}.opacity-0{opacity:0}.opacity-5{opacity:0.05}.shadow-2xl{--tw-shadow:0 25px 50px -12px rgb(0 0 0 / 0.25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}.shadow-\[0_0_15px_rgba\(0\2c 255\2c 136\2c 0\.3\)\]{--tw-shadow:0 0 15px rgba(0,255,136,0.3);--tw-shadow-colored:0 0 15px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}.shadow-\[0_0_15px_rgba\(255\2c 51\2c 102\2c 0\.3\)\]{--tw-shadow:0 0 15px rgba(255,51,102,0.3);--tw-shadow-colored:0 0 15px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}.shadow-\[0_0_20px_rgba\(255\2c 51\2c 102\2c 0\.1\)\]{--tw-shadow:0 0 20px rgba(255,51,102,0.1);--tw-shadow-colored:0 0 20px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}.shadow-\[0_0_30px_rgba\(0\2c 0\2c 0\2c 0\.5\)\]{--tw-shadow:0 0 30px rgba(0,0,0,0.5);--tw-shadow-colored:0 0 30px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.blur-\[80px\]{--tw-blur:blur(80px);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.drop-shadow-lg{--tw-drop-shadow:drop-shadow(0 10px 8px rgb(0 0 0 / 0.04)) drop-shadow(0 4px 3px rgb(0 0 0 / 0.1));filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms}.transition-colors{transition-property:color, background-color, border-color, fill, stroke, -webkit-text-decoration-color;transition-property:color, background-color, border-color, text-decoration-color, fill, stroke;transition-property:color, background-color, border-color, text-decoration-color, fill, stroke, -webkit-text-decoration-color;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms}.transition{transition-property:color, background-color, border-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-text-decoration-color, -webkit-backdrop-filter;transition-property:color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;transition-property:color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-text-decoration-color, -webkit-backdrop-filter;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms}.duration-300{transition-duration:300ms}.hover\:scale-125:hover{--tw-scale-x:1.25;--tw-scale-y:1.25;transform:translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:border-\[\#00d4ff\]:hover{--tw-border-opacity:1;border-color:rgb(0 212 255 / var(--tw-border-opacity, 1))}.hover\:bg-\[\#111827\]:hover{--tw-bg-opacity:1;background-color:rgb(17 24 39 / var(--tw-bg-opacity, 1))}.hover\:bg-\[\#1e293b\]:hover{--tw-bg-opacity:1;background-color:rgb(30 41 59 / var(--tw-bg-opacity, 1))}.hover\:text-\[\#00ff88\]:hover{--tw-text-opacity:1;color:rgb(0 255 136 / var(--tw-text-opacity, 1))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.hover\:text-\[\#00d4ff\]:hover{--tw-text-opacity:1;color:rgb(0 212 255 / var(--tw-text-opacity, 1))}.hover\:text-\[\#ff3366\]:hover{--tw-text-opacity:1;color:rgb(255 51 102 / var(--tw-text-opacity, 1))}.focus\:border-\[\#00d4ff\]:focus{--tw-border-opacity:1;border-color:rgb(0 212 255 / var(--tw-border-opacity, 1))}@media (min-width: 768px){.md\:col-span-2{grid-column:span 2 / span 2}.md\:col-span-4{grid-column:span 4 / span 4}.md\:grid-cols-6{grid-template-columns:repeat(6, minmax(0, 1fr))}}@media (min-width: 1024px){.lg\:col-span-2{grid-column:span 2 / span 2}.lg\:grid-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3, minmax(0, 1fr))}}</style></head>
<body>
<!-- HEADER -->
<header class="border-b border-[#1e293b] px-6 py-4 sticky top-0 z-40 bg-[var(--dark-bg)] shadow-md">
<div class="max-w-7xl mx-auto flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="text-3xl">⚡</div>
<div>
<h1 class="text-xl font-black tracking-tight">
<span class="bg-gradient-to-r from-[#00ff88] via-[#00d4ff] to-[#a855f7] bg-clip-text text-transparent">LLM Benchmark Pro</span>
</h1>
<p class="text-xs text-slate-500 font-medium" data-i18n="app_subtitle">Ultimate Testing Suite v2.0 by Mr.Vinyl</p>
</div>
</div>
<div class="flex items-center gap-4">
<select id="langSwitcher" onchange="changeLang(this.value)" class="bg-[#1e293b] border border-[#334155] rounded-lg px-3 py-1.5 text-sm text-[#00d4ff] font-bold outline-none cursor-pointer focus:border-[#00d4ff]">
<option value="ru">🇷🇺 RU</option>
<option value="en">🇬🇧 EN</option>
</select>
<div class="flex items-center gap-2 text-sm bg-[#111827] px-4 py-2 rounded-full border border-[#1e293b]">
<span class="status-dot online" id="serverStatus"></span>
<span id="serverStatusText" class="text-slate-400 font-mono text-xs" data-i18n="status_checking">Подключено</span>
</div>
</div>
</div>
</header>
<!-- MAIN -->
<main class="max-w-7xl mx-auto px-6 py-6">
<!-- TABS -->
<div class="flex gap-2 mb-6 flex-wrap bg-[#111827] p-2 rounded-2xl border border-[#1e293b]" id="mainTabs">
<button class="tab-btn flex-1" onclick="switchTab('settings')" data-i18n="tab_settings">⚙️ Настройки</button>
<button class="tab-btn flex-1 active border-[#00d4ff33] text-[#00d4ff]" onclick="switchTab('editor')" data-i18n="tab_editor">✏️ Редактор тестов</button>
<button class="tab-btn flex-1" onclick="switchTab('benchmark')" data-i18n="tab_bench">🚀 Бенчмарк</button>
<button class="tab-btn flex-1" onclick="switchTab('results')" data-i18n="tab_results">📊 Результаты</button>
<button class="tab-btn flex-1" onclick="switchTab('compare')" data-i18n="tab_compare">🔍 Деградация & Сравнение</button>
</div>
<!-- TAB: SETTINGS -->
<div id="tab-settings" class="tab-content animate-in hidden">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Connection -->
<div class="card p-6 relative overflow-hidden">
<div class="absolute top-0 right-0 w-32 h-32 bg-[#00d4ff] opacity-5 blur-[80px]"></div>
<h2 class="text-lg font-bold mb-4 flex items-center gap-2" data-i18n="conn_title">🔌 Подключение к llama.cpp</h2>
<div class="space-y-4">
<div>
<label class="text-sm text-slate-400 mb-1 block" data-i18n="server_url">URL сервера</label>
<input id="serverUrl" class="input-field font-mono" value="http://localhost:9000" placeholder="http://localhost:9000">
</div>
<div>
<label class="text-sm text-slate-400 mb-1 block" data-i18n="api_key">API Key (опционально)</label>
<input id="apiKey" class="input-field font-mono" type="password" placeholder="">
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="text-sm text-slate-400 mb-1 block">Temperature</label>
<input id="temperature" class="input-field font-mono" type="number" value="0.1" min="0" max="2" step="0.1">
</div>
<div>
<label class="text-sm text-slate-400 mb-1 block">Max Tokens</label>
<input id="maxTokens" class="input-field font-mono" type="number" value="512" min="16" max="32768" step="16">
</div>
</div>
<div class="grid grid-cols-3 gap-4">
<div>
<label class="text-sm text-slate-400 mb-1 block">Top P</label>
<input id="topP" class="input-field font-mono" type="number" value="0.95" min="0" max="1" step="0.05">
</div>
<div>
<label class="text-sm text-slate-400 mb-1 block">Top K</label>
<input id="topK" class="input-field font-mono" type="number" value="40" min="0" max="200" step="1">
</div>
<div>
<label class="text-sm text-slate-400 mb-1 block" data-i18n="cot_mode">Мышление (CoT)</label>
<select id="thinkingMode" class="input-field font-mono text-xs p-2">
<option value="auto" data-i18n="cot_auto">Авто</option>
<option value="force" data-i18n="cot_force">Принудительно</option>
<option value="disable" data-i18n="cot_disable">Запретить</option>
</select>
</div>
</div>
<div class="flex gap-3 pt-2">
<button class="btn-primary" onclick="testConnection()" data-i18n="btn_check_conn">🔗 Проверить соединение</button>
<button class="btn-secondary" onclick="saveSettings()" data-i18n="btn_save">💾 Сохранить</button>
</div>
</div>
</div>
<!-- Model Info -->
<div class="card p-6 relative overflow-hidden">
<div class="absolute top-0 right-0 w-32 h-32 bg-[#a855f7] opacity-5 blur-[80px]"></div>
<h2 class="text-lg font-bold mb-4 flex items-center gap-2" data-i18n="model_info">🤖 Информация о модели</h2>
<div class="space-y-4">
<div>
<label class="text-sm text-slate-400 mb-1 block" data-i18n="model_name">Название модели</label>
<input id="modelName" class="input-field font-mono" value="cortiq-auto" placeholder="cortiq-auto">
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="text-sm text-slate-400 mb-1 block" data-i18n="quant_type">Тип квантования</label>
<select id="quantType" class="input-field font-mono text-sm">
<option value="F16">F16 / FP16 (Baseline)</option>
<option value="Q8_0">Q8_0 (8-bit)</option>
<option value="Q6_K">Q6_K</option>
<option value="Q5_K_M">Q5_K_M</option>
<option value="Q4_K_M">Q4_K_M</option>
<option value="Q4_0">Q4_0</option>
<option value="Q3_K_M">Q3_K_M</option>
<option value="IQ2_XXS">IQ2_XXS</option>
<option value="custom" data-i18n="opt_other">Другое</option>
</select>
</div>
<div>
<label class="text-sm text-slate-400 mb-1 block" data-i18n="model_size">Размер (параметры)</label>
<input id="modelSize" class="input-field font-mono" placeholder="7B, 13B...">
</div>
</div>
<div>
<label class="text-sm text-slate-400 mb-1 block" data-i18n="notes">Заметки</label>
<textarea id="modelNotes" class="input-field scrollbar-thin" rows="2" placeholder=""></textarea>
</div>
</div>
</div>
</div>
</div>
<!-- TAB: EDITOR -->
<div id="tab-editor" class="tab-content animate-in">
<div class="flex items-center justify-between mb-4">
<h2 class="text-2xl font-bold" data-i18n="editor_title">📝 Редактор базы тестов</h2>
<div class="flex gap-2">
<button class="btn-secondary text-sm flex items-center gap-1 hover:text-[#00ff88] transition-colors" onclick="exportToStandaloneHtml()">
📦 <span data-i18n="btn_dl_html">Скачать HTML с базой</span>
</button>
<button class="btn-secondary text-sm" onclick="resetToDefaultTests()">🔄 <span data-i18n="btn_reset">Сбросить</span></button>
<button class="btn-primary text-sm" onclick="showAddTestModal()">➕ <span data-i18n="btn_add_test">Добавить тест</span></button>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-6 gap-4 mb-6">
<div class="md:col-span-2 space-y-2" id="categoryFilterList">
<button class="w-full text-left px-4 py-3 rounded-xl border border-transparent transition hover:bg-[#111827] text-slate-400" onclick="setEditorCategory('knowledge')">
<span class="mr-2">🧠</span>
<span class="font-medium">Фактические знания</span>
<span class="float-right text-xs bg-[#0f172a] px-2 py-1 rounded text-slate-500">9</span>
</button>
<button class="w-full text-left px-4 py-3 rounded-xl border border-transparent transition hover:bg-[#111827] text-slate-400" onclick="setEditorCategory('math')">
<span class="mr-2">🔢</span>
<span class="font-medium">Математика</span>
<span class="float-right text-xs bg-[#0f172a] px-2 py-1 rounded text-slate-500">8</span>
</button>
<button class="w-full text-left px-4 py-3 rounded-xl border border-transparent transition hover:bg-[#111827] text-slate-400" onclick="setEditorCategory('logic')">
<span class="mr-2">🧩</span>
<span class="font-medium">Логика</span>
<span class="float-right text-xs bg-[#0f172a] px-2 py-1 rounded text-slate-500">8</span>
</button>
<button class="w-full text-left px-4 py-3 rounded-xl border border-transparent transition hover:bg-[#111827] text-slate-400" onclick="setEditorCategory('code')">
<span class="mr-2">💻</span>
<span class="font-medium">Код</span>
<span class="float-right text-xs bg-[#0f172a] px-2 py-1 rounded text-slate-500">7</span>
</button>
<button class="w-full text-left px-4 py-3 rounded-xl border border-transparent transition hover:bg-[#111827] text-slate-400" onclick="setEditorCategory('tools')">
<span class="mr-2">⚡</span>
<span class="font-medium">Вызовы команд</span>
<span class="float-right text-xs bg-[#0f172a] px-2 py-1 rounded text-slate-500">6</span>
</button>
<button class="w-full text-left px-4 py-3 rounded-xl border border-transparent transition bg-[#1e293b] border-[#00d4ff33] text-[#00d4ff]" onclick="setEditorCategory('antiloop')">
<span class="mr-2">🔄</span>
<span class="font-medium">Анти-зацикливание</span>
<span class="float-right text-xs bg-[#0f172a] px-2 py-1 rounded text-slate-500">5</span>
</button>
</div>
<div class="md:col-span-4 card p-0 overflow-hidden flex flex-col h-[600px]">
<div class="bg-[#1e293b] px-4 py-3 border-b border-[#334155] flex justify-between items-center">
<span class="font-bold text-sm text-[#00d4ff]" id="currentCategoryTitle">🔄 Анти-зацикливание</span>
<span class="text-xs text-slate-400" id="currentCategoryCount">5 тестов</span>
</div>
<div class="flex-1 overflow-y-auto scrollbar-thin p-4 space-y-3" id="testsEditorList">
<div class="card p-4 border-[#334155] hover:border-[#00d4ff] bg-[#0f172a] flex flex-col gap-2">
<div class="flex justify-between items-start gap-4">
<div class="font-mono text-sm text-[#e2e8f0] flex-1 break-words">Сгенерируй длинный Python скрипт для парсинга веб-сайта, который использует BeautifulSoup. Код должен быть уникальным, без повторения одних и тех же строк.</div>
<div class="flex gap-2 flex-shrink-0">
<button class="text-slate-500 hover:text-[#00d4ff]" onclick="editTest(603, 'antiloop')">✏️</button>
<button class="text-slate-500 hover:text-[#ff3366]" onclick="deleteTest(603, 'antiloop')">🗑</button>
</div>
</div>
<div class="text-xs text-slate-400 border-l-2 border-[#334155] pl-2 mt-2">
<span class="text-[#00ff88]">Ожидалось (или ключи):</span> python parser<br>
<span class="text-slate-500">Method: no_repeat | Keys: bs4|beautifulsoup, requests</span>
</div>
</div>
<div class="card p-4 border-[#334155] hover:border-[#00d4ff] bg-[#0f172a] flex flex-col gap-2">
<div class="flex justify-between items-start gap-4">
<div class="font-mono text-sm text-[#e2e8f0] flex-1 break-words">Напиши короткий диалог (на 4-5 реплик) между пользователем и ИИ-помощником, где они обсуждают программирование.</div>
<div class="flex gap-2 flex-shrink-0">
<button class="text-slate-500 hover:text-[#00d4ff]" onclick="editTest(604, 'antiloop')">✏️</button>
<button class="text-slate-500 hover:text-[#ff3366]" onclick="deleteTest(604, 'antiloop')">🗑</button>
</div>
</div>
<div class="text-xs text-slate-400 border-l-2 border-[#334155] pl-2 mt-2">
<span class="text-[#00ff88]">Ожидалось (или ключи):</span> диалог ИИ и человека<br>
<span class="text-slate-500">Method: no_repeat | Keys: n/a</span>
</div>
</div>
<div class="card p-4 border-[#334155] hover:border-[#00d4ff] bg-[#0f172a] flex flex-col gap-2">
<div class="flex justify-between items-start gap-4">
<div class="font-mono text-sm text-[#e2e8f0] flex-1 break-words">Сгенерируй HTML/CSS код для сложной карточки товара с анимациями наведения, тенями и адаптивной версткой. Код должен быть длинным, уникальным и не зацикливаться на одних и тех же стилях.</div>
<div class="flex gap-2 flex-shrink-0">
<button class="text-slate-500 hover:text-[#00d4ff]" onclick="editTest(605, 'antiloop')">✏️</button>
<button class="text-slate-500 hover:text-[#ff3366]" onclick="deleteTest(605, 'antiloop')">🗑</button>
</div>
</div>
<div class="text-xs text-slate-400 border-l-2 border-[#334155] pl-2 mt-2">
<span class="text-[#00ff88]">Ожидалось (или ключи):</span> html css карточка товара<br>
<span class="text-slate-500">Method: no_repeat | Keys: <div, style,="" hover,="" transition<="" span="">
</div,></span></div>
</div>
<div class="card p-4 border-[#334155] hover:border-[#00d4ff] bg-[#0f172a] flex flex-col gap-2">
<div class="flex justify-between items-start gap-4">
<div class="font-mono text-sm text-[#e2e8f0] flex-1 break-words">Назови три разные столицы европейских государств, в названиях которых (на русском языке) вообще нет буквы "А". Напиши только эти три слова через запятую</div>
<div class="flex gap-2 flex-shrink-0">
<button class="text-slate-500 hover:text-[#00d4ff]" onclick="editTest(1778396282738, 'antiloop')">✏️</button>
<button class="text-slate-500 hover:text-[#ff3366]" onclick="deleteTest(1778396282738, 'antiloop')">🗑</button>
</div>
</div>
<div class="text-xs text-slate-400 border-l-2 border-[#334155] pl-2 mt-2">
<span class="text-[#00ff88]">Ожидалось (или ключи):</span> Например: Хельсинки, Осло, Лондон (или Берлин, Мадрид, Киев)<br>
<span class="text-slate-500">Method: contains | Keys: n/a</span>
</div>
</div>
<div class="card p-4 border-[#334155] hover:border-[#00d4ff] bg-[#0f172a] flex flex-col gap-2">
<div class="flex justify-between items-start gap-4">
<div class="font-mono text-sm text-[#e2e8f0] flex-1 break-words">Напиши слово "Токен" ровно 6 раз через пробел. Не используй знаки препинания вообще, даже точку в конце</div>
<div class="flex gap-2 flex-shrink-0">
<button class="text-slate-500 hover:text-[#00d4ff]" onclick="editTest(1778396306898, 'antiloop')">✏️</button>
<button class="text-slate-500 hover:text-[#ff3366]" onclick="deleteTest(1778396306898, 'antiloop')">🗑</button>
</div>
</div>
<div class="text-xs text-slate-400 border-l-2 border-[#334155] pl-2 mt-2">
<span class="text-[#00ff88]">Ожидалось (или ключи):</span> Токен Токен Токен Токен Токен Токен<br>
<span class="text-slate-500">Method: contains | Keys: n/a</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- TAB: BENCHMARK -->
<div id="tab-benchmark" class="tab-content animate-in hidden">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Controls -->
<div class="card p-6">
<h2 class="text-lg font-bold mb-4" data-i18n="bench_ctrl">🎯 Управление тестом</h2>
<div class="space-y-4">
<div>
<label class="text-sm text-slate-400 mb-2 block" data-i18n="select_cats">Выберите категории:</label>
<div class="space-y-2 bg-[#0f172a] p-3 rounded-xl border border-[#1e293b]" id="benchmarkCategoryList">
<label class="flex items-center gap-2 cursor-pointer hover:bg-[#1e293b] p-1 rounded">
<input type="checkbox" id="cat-knowledge" checked="" class="accent-#00ff88">
<span class="text-sm">🧠 Фактические знания</span>
</label>
<label class="flex items-center gap-2 cursor-pointer hover:bg-[#1e293b] p-1 rounded">
<input type="checkbox" id="cat-math" checked="" class="accent-#00d4ff">
<span class="text-sm">🔢 Математика</span>
</label>
<label class="flex items-center gap-2 cursor-pointer hover:bg-[#1e293b] p-1 rounded">
<input type="checkbox" id="cat-logic" checked="" class="accent-#a855f7">
<span class="text-sm">🧩 Логика</span>
</label>
<label class="flex items-center gap-2 cursor-pointer hover:bg-[#1e293b] p-1 rounded">
<input type="checkbox" id="cat-code" checked="" class="accent-#ffcc00">
<span class="text-sm">💻 Код</span>
</label>
<label class="flex items-center gap-2 cursor-pointer hover:bg-[#1e293b] p-1 rounded">
<input type="checkbox" id="cat-tools" checked="" class="accent-#ff6b35">
<span class="text-sm">⚡ Вызовы команд</span>
</label>
<label class="flex items-center gap-2 cursor-pointer hover:bg-[#1e293b] p-1 rounded">
<input type="checkbox" id="cat-antiloop" checked="" class="accent-#ff3366">
<span class="text-sm">🔄 Анти-зацикливание</span>
</label>
</div>
</div>
<div class="flex gap-2">
<button class="btn-primary flex-1 text-lg shadow-[0_0_15px_rgba(0,255,136,0.3)]" id="btnStart" onclick="startBenchmark()">🚀 <span data-i18n="btn_start">ЗАПУСТИТЬ</span></button>
<button class="btn-danger flex-1 text-lg shadow-[0_0_15px_rgba(255,51,102,0.3)] hidden" id="btnStop" onclick="stopBenchmark()">⏹ <span data-i18n="btn_stop">СТОП</span></button>
</div>
</div>
<!-- Progress -->
<div id="progressSection" class="mt-6">
<div class="flex justify-between text-sm mb-2">
<span class="text-slate-400" data-i18n="progress">Прогресс</span>
<span id="progressText" class="font-mono text-[#00d4ff]">33/33</span>
</div>
<div class="progress-bar">
<div class="progress-fill" id="progressFill" style="width: 100%;"></div>
</div>
<div class="mt-3 text-xs text-slate-400 font-mono truncate" id="currentTestName">✅ Завершено</div>
</div>
<!-- Live Score -->
<div id="liveScoreSection" class="mt-6">
<div class="hero-gradient rounded-xl p-4 text-center border border-[#1e293b]">
<div class="text-5xl font-black drop-shadow-lg" id="liveScore">76%</div>
<div class="text-sm text-slate-400 mt-1 uppercase tracking-wider font-bold" data-i18n="score">Оценка</div>
<div class="flex justify-center gap-6 mt-4 text-sm bg-[#0a0e17] py-2 rounded-lg">
<div class="flex flex-col items-center"><span class="text-[#00ff88] font-bold text-lg" id="livePass">25</span> <span class="text-xs text-slate-500">PASS</span></div>
<div class="flex flex-col items-center"><span class="text-[#ff3366] font-bold text-lg" id="liveFail">8</span> <span class="text-xs text-slate-500">FAIL</span></div>
<div class="flex flex-col items-center"><span class="text-[#ffcc00] font-bold text-lg" id="livePartial">0</span> <span class="text-xs text-slate-500">PARTIAL</span></div>
</div>
</div>
</div>
</div>
<!-- Live Log -->
<div class="lg:col-span-2 card p-0 flex flex-col overflow-hidden h-[800px]">
<div class="bg-[#1e293b] px-6 py-4 flex items-center justify-between border-b border-[#334155]">
<h2 class="font-bold" data-i18n="live_log">📋 Лог выполнения (Live)</h2>
<span class="text-xs text-slate-400 font-mono bg-[#0f172a] px-3 py-1 rounded-full border border-[#334155]" id="logCount">33 / ⚡ 54.8 t/s</span>
</div>
<div id="testLog" class="flex-1 overflow-y-auto scrollbar-thin p-4 space-y-3 bg-[#0a0e17]"><div class="flex flex-col items-center justify-center h-full text-slate-500 space-y-4"><div class="text-6xl animate-bounce">🧪</div><p>Выберите категории и нажмите «ЗАПУСТИТЬ»</p></div></div>
</div>
</div>
</div>
<!-- TAB: RESULTS -->
<div id="tab-results" class="tab-content hidden animate-in">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="card p-6"><h2 class="text-lg font-bold mb-4" data-i18n="chart_cats">📊 Результаты по категориям</h2><canvas id="resultsChart" height="250"></canvas></div>
<div class="card p-6"><h2 class="text-lg font-bold mb-4" data-i18n="chart_radar">🎯 Радар компетенций</h2><canvas id="radarChart" height="250"></canvas></div>
</div>
<!-- History -->
<div class="card p-0 mt-6 overflow-hidden">
<div class="bg-[#1e293b] px-6 py-4 flex items-center justify-between border-b border-[#334155]">
<h2 class="font-bold" data-i18n="run_history">📜 История запусков</h2>
<div class="flex gap-2">
<button class="btn-secondary text-xs" onclick="exportResults()">📤 <span data-i18n="btn_exp_json">Экспорт JSON</span></button>
<button class="btn-danger text-xs" onclick="clearHistory()">🗑 <span data-i18n="btn_clr_hist">Очистить всю историю</span></button>
</div>
</div>
<div id="historyList" class="p-4 space-y-3 max-h-[500px] overflow-y-auto scrollbar-thin bg-[#0a0e17]"><div class="text-center text-slate-500 py-8">Нет сохранённых результатов</div></div>
</div>
</div>
<!-- TAB: COMPARE & DEGRADATION -->
<div id="tab-compare" class="tab-content animate-in hidden">
<div class="card p-6 mb-6 border-l-4 border-[#00d4ff]">
<h2 class="text-xl font-bold mb-2" data-i18n="comp_title">🔍 Анализ квантования и Сравнение</h2>
<p class="text-sm text-slate-400 mb-4" data-i18n="comp_desc">Выберите результаты для сравнения (нет ограничений по количеству). Для оценки деградации выберите Baseline (F16/Q8) первым, а квантованную модель (Q4/Q3) второй.</p>
<div id="compareSelect" class="flex flex-wrap gap-2 mb-4 bg-[#0f172a] p-4 rounded-xl border border-[#1e293b]">
<div class="flex items-center justify-between gap-2 px-3 py-2 rounded border border-[#334155] bg-[#0a0e17] hover:border-[#00d4ff] transition-colors">
<label class="flex items-center gap-2 cursor-pointer flex-1">
<input type="checkbox" class="compare-cb accent-[#00d4ff]" value="1778395476855">
<span class="text-sm font-medium text-[#e2e8f0]">Qwen 3.6 35b q4 <span class="text-xs text-[#00d4ff] bg-[#1e293b] px-1 rounded">Q4_K_M</span> <span class="text-xs text-slate-500 font-normal ml-1">10.05.2026</span></span>
</label>
<button onclick="deleteHistoryEntry(1778395476855)" class="text-slate-600 hover:text-[#ff3366] px-1 text-lg leading-none transition-colors" title="Delete">×</button>
</div>
<div class="flex items-center justify-between gap-2 px-3 py-2 rounded border border-[#334155] bg-[#0a0e17] hover:border-[#00d4ff] transition-colors">
<label class="flex items-center gap-2 cursor-pointer flex-1">
<input type="checkbox" class="compare-cb accent-[#00d4ff]" value="1778394952090">
<span class="text-sm font-medium text-[#e2e8f0]">Qwen 3.6 35b q3 <span class="text-xs text-[#00d4ff] bg-[#1e293b] px-1 rounded">Q3_K_M</span> <span class="text-xs text-slate-500 font-normal ml-1">10.05.2026</span></span>
</label>
<button onclick="deleteHistoryEntry(1778394952090)" class="text-slate-600 hover:text-[#ff3366] px-1 text-lg leading-none transition-colors" title="Delete">×</button>
</div>
<div class="flex items-center justify-between gap-2 px-3 py-2 rounded border border-[#334155] bg-[#0a0e17] hover:border-[#00d4ff] transition-colors">
<label class="flex items-center gap-2 cursor-pointer flex-1">
<input type="checkbox" class="compare-cb accent-[#00d4ff]" value="1778394549628">
<span class="text-sm font-medium text-[#e2e8f0]">Qwen 3.6 35b 2m <span class="text-xs text-[#00d4ff] bg-[#1e293b] px-1 rounded">IQ2_XXS</span> <span class="text-xs text-slate-500 font-normal ml-1">10.05.2026</span></span>
</label>
<button onclick="deleteHistoryEntry(1778394549628)" class="text-slate-600 hover:text-[#ff3366] px-1 text-lg leading-none transition-colors" title="Delete">×</button>
</div>
</div>
<button class="btn-primary" onclick="runComparison()">📊 <span data-i18n="btn_run_comp">Запустить Анализ</span></button>
</div>
<div id="comparisonResults" class="space-y-6 hidden">
<!-- High Level Chart -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="card p-6 overflow-hidden"><h2 class="text-lg font-bold mb-4" data-i18n="comp_chart_diff">📊 Разница в баллах</h2><canvas id="compareChart" height="250"></canvas></div>
<div class="card p-6">
<h2 class="text-lg font-bold mb-4" data-i18n="comp_table_total">🏆 Общая таблица</h2>
<div id="compareTable" class="overflow-x-auto text-sm"></div>
</div>
</div>
<!-- Degradation Analysis Section -->
<div class="card p-0 border-[#ff3366] shadow-[0_0_20px_rgba(255,51,102,0.1)]">
<div class="bg-gradient-to-r from-[#ff3366]/20 to-transparent px-6 py-4 border-b border-[#334155]">
<h2 class="font-bold text-lg flex items-center gap-2" data-i18n="deg_title">📉 Отчет о деградации (Где модель стала глупее)</h2>
<p class="text-xs text-slate-400 mt-1" data-i18n="deg_desc">Показаны вопросы, на которые первая выбранная модель ответила лучше, чем последующие.</p>
</div>
<div class="p-4" id="degradationReport"><p class="text-slate-500 text-center py-4">Выберите модели для анализа</p></div>
</div>
<!-- Side-by-side details -->
<div class="card p-0">
<div class="bg-[#1e293b] px-6 py-4 border-b border-[#334155]">
<h2 class="font-bold" data-i18n="comp_detailed">📋 Детальное сравнение по вопросам</h2>
</div>
<div id="compareDetailed" class="p-4 overflow-x-auto"></div>
</div>
</div>
</div>
</main>
<!-- Add/Edit Test Modal -->
<div id="testModal" class="fixed inset-0 modal-overlay z-50 flex items-center justify-center p-4 hidden">
<div class="card p-6 w-full max-w-2xl shadow-2xl relative">
<button onclick="closeTestModal()" class="absolute top-4 right-4 text-slate-400 hover:text-white text-2xl leading-none">×</button>
<h2 class="text-xl font-bold mb-4" id="testModalTitle" data-i18n="modal_add_title">Добавить тест</h2>
<input type="hidden" id="modalTestId" value="">
<div class="space-y-4">
<div>
<label class="text-sm text-slate-400 mb-1 block" data-i18n="category">Категория</label>
<select id="modalCategory" class="input-field"><option value="knowledge">🧠 Фактические знания</option><option value="math">🔢 Математика</option><option value="logic">🧩 Логика</option><option value="code">💻 Код</option><option value="tools">⚡ Вызовы команд</option><option value="antiloop">🔄 Анти-зацикливание</option></select>
</div>
<div>
<label class="text-sm text-slate-400 mb-1 block" data-i18n="modal_q">Вопрос / Промпт</label>
<textarea id="modalQuestion" class="input-field font-mono text-sm scrollbar-thin" rows="3" placeholder=""></textarea>
</div>
<div>
<label class="text-sm text-slate-400 mb-1 block" data-i18n="modal_a">Ожидаемый ответ / Пояснение для вас</label>
<textarea id="modalExpected" class="input-field font-mono text-sm scrollbar-thin" rows="2" placeholder=""></textarea>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="text-sm text-slate-400 mb-1 block" data-i18n="modal_method">Метод проверки</label>
<select id="modalMethod" class="input-field">
<option value="contains" data-i18n="m_contains">Содержит ключевые слова</option>
<option value="exact" data-i18n="m_exact">Точное совпадение</option>
<option value="regex" data-i18n="m_regex">Regex паттерн</option>
<option value="no_repeat" data-i18n="m_no_repeat">Анти-луп (проверка повторений)</option>
</select>
</div>
<div>
<label class="text-sm text-slate-400 mb-1 block" data-i18n="modal_keys">Ключи (через запятую. Разделитель | для синонимов)</label>
<input type="text" id="modalKeys" class="input-field font-mono text-sm" placeholder="word1|synonym1, word2...">
</div>
</div>
<div class="pt-4 flex gap-2 justify-end">
<button class="btn-secondary" onclick="closeTestModal()" data-i18n="btn_cancel">Отмена</button>
<button class="btn-primary" onclick="saveTestFromModal()" data-i18n="btn_save">💾 Сохранить</button>
</div>
</div>
</div>
</div>
<!-- Toast -->
<div id="toast" class="fixed bottom-6 right-6 z-50 transition-all duration-300 transform translate-y-10 opacity-0 hidden">
<div class="card px-5 py-3 flex items-center gap-3 shadow-[0_0_30px_rgba(0,0,0,0.5)] border-[#334155] bg-[#0f172a]">
<span id="toastIcon" class="text-xl">✅</span>
<span id="toastMsg" class="text-sm font-medium">Тест сохранен</span>
</div>
</div>
<script>
// ============================================================
// I18N (TRANSLATIONS)
// ============================================================
const TRANSLATIONS = {
ru: {
app_subtitle: "Ultimate Testing Suite v2.0 by Mr.Vinyl",
status_checking: "Проверка...",
status_online: "Подключено",
status_offline: "Не доступен",
tab_settings: "⚙️ Настройки",
tab_editor: "✏️ Редактор тестов",
tab_bench: "🚀 Бенчмарк",
tab_results: "📊 Результаты",
tab_compare: "🔍 Деградация & Сравнение",
conn_title: "🔌 Подключение к llama.cpp",
server_url: "URL сервера",
api_key: "API Key (опционально)",
cot_mode: "Мышление (CoT)",
cot_auto: "Авто",
cot_force: "Принудительно",
cot_disable: "Запретить",
btn_check_conn: "🔗 Проверить соединение",
btn_save: "💾 Сохранить",
model_info: "🤖 Информация о модели",
model_name: "Название модели",
quant_type: "Тип квантования",
model_size: "Размер (параметры)",
notes: "Заметки",
opt_other: "Другое",
editor_title: "📝 Редактор базы тестов",
btn_dl_html: "Скачать HTML с базой",
btn_reset: "Сбросить",
btn_add_test: "Добавить тест",
msg_select_cat: "Выберите категорию слева",
bench_ctrl: "🎯 Управление тестом",
select_cats: "Выберите категории:",
btn_start: "ЗАПУСТИТЬ",
btn_stop: "СТОП",
progress: "Прогресс",
score: "Оценка",
live_log: "📋 Лог выполнения (Live)",
msg_bench_start: "Выберите категории и нажмите «ЗАПУСТИТЬ»",
chart_cats: "📊 Результаты по категориям",
chart_radar: "🎯 Радар компетенций",
run_history: "📜 История запусков",
btn_exp_json: "Экспорт JSON",
btn_clr_hist: "Очистить всю историю",
msg_empty: "Нет сохранённых результатов",
comp_title: "🔍 Анализ квантования и Сравнение",
comp_desc: "Выберите результаты для сравнения (нет ограничений по количеству). Для оценки деградации выберите Baseline (F16/Q8) первым, а квантованную модель (Q4/Q3) второй.",
btn_run_comp: "Запустить Анализ",
comp_chart_diff: "📊 Разница в баллах",
comp_table_total: "🏆 Общая таблица",
deg_title: "📉 Отчет о деградации (Где модель стала глупее)",
deg_desc: "Показаны вопросы, на которые первая выбранная модель ответила лучше, чем последующие.",
msg_sel_models: "Выберите модели для анализа",
comp_detailed: "📋 Детальное сравнение по вопросам",
modal_add_title: "Добавить тест",
modal_edit_title: "Редактировать тест",
category: "Категория",
modal_q: "Вопрос / Промпт",
modal_a: "Ожидаемый ответ / Пояснение для вас",
modal_method: "Метод проверки",
m_contains: "Содержит ключевые слова",
m_exact: "Точное совпадение",
m_regex: "Regex паттерн",
m_no_repeat: "Анти-луп (проверка повторений)",
modal_keys: "Ключи (через запятую. Разделитель | для синонимов)",
btn_cancel: "Отмена",
cat_knowledge: "Фактические знания",
cat_math: "Математика",
cat_logic: "Логика",
cat_code: "Код",
cat_tools: "Вызовы команд",
cat_antiloop: "Анти-зацикливание",
cot_prompt_force: "\n\nПожалуйста, рассуждай шаг за шагом внутри тегов <think>...</think> перед тем, как дать окончательный ответ.",
cot_prompt_disable: "\n\nОтвечай сразу, кратко и по существу, без промежуточных рассуждений и без использования тегов <think>.",
tests_count: "тестов",
expected_ans: "Ожидалось (или ключи):",
final_ans: "Финальный Ответ",
cot_process: "Процесс рассуждения (CoT):",
err_collapse: "⚠️ Ошибка (Коллапс модели): Модель сгенерировала {n} токенов, но видимый текст отсутствует. Возможно, модель зациклилась или не смогла корректно выполнить вызов.",
err_raw: "Сырой вывод от API (для отладки):",
empty_ans: "Пустой ответ...",
deg_no_deg: "🎉 Деградация не обнаружена! Квантованная модель отвечает так же или лучше.",
deg_base: "✅ {q} (Baseline)",
deg_bad: "❌ {q} (Деградация)",
deg_reason: "Причина: {r}",
msg_all_keys: "Все ключевые слова найдены",
msg_part_keys: "Найдено ключей: {n} из {m}",
msg_no_keys: "Ответ не содержит ожидаемых ключей",
msg_exact: "Точное совпадение",
msg_exact_part: "Содержит точный ответ внутри",
msg_not_match: "Ответ не совпадает",
msg_regex_ok: "Regex совпал",
msg_regex_fail: "Regex не совпал",
msg_loop: "🔄 ЗАЦИКЛИВАНИЕ! Повтор: \"{p}\"",
msg_no_loop: "Чисто. Зацикливаний нет.",
msg_loop_no_keys: "Нет зацикливания, но пропущены ключи",
msg_collapse_short: "Коллапс: сгенерировано {n} пустых/спец. токенов",
msg_empty_short: "Пустой ответ от модели",
msg_no_cond: "Нет условий для проверки",
prompt_sure_reset: "Вы уверены? Все ваши изменения в промптах будут удалены!",
prompt_sure_del: "Удалить тест?",
prompt_sure_clr: "Очистить историю запусков?",
prompt_sure_del_res: "Удалить этот результат теста?",
msg_fill_q_a: "Заполните вопрос и ответ",
msg_saved: "Тест сохранен",
msg_deleted: "Результат удален",
msg_cleared: "Очищено",
msg_select_cat_bench: "Выберите категорию!",
msg_no_tests: "В выбранных категориях нет тестов",
msg_bench_done: "Бенчмарк окончен",
lbl_stopped: "⏹ Остановлено",
lbl_done: "✅ Завершено",
lbl_req: "Запрос...",
col_model: "Модель",
col_quant: "Quant",
col_total: "Общий %",
col_q: "Вопрос"
},
en: {
app_subtitle: "Ultimate Testing Suite v2.0 by Mr.Vinyl",
status_checking: "Checking...",
status_online: "Connected",
status_offline: "Offline",
tab_settings: "⚙️ Settings",
tab_editor: "✏️ Test Editor",
tab_bench: "🚀 Benchmark",
tab_results: "📊 Results",
tab_compare: "🔍 Degradation & Compare",
conn_title: "🔌 llama.cpp Connection",
server_url: "Server URL",
api_key: "API Key (optional)",
cot_mode: "Thinking (CoT)",
cot_auto: "Auto",
cot_force: "Force",
cot_disable: "Disable",
btn_check_conn: "🔗 Check Connection",
btn_save: "💾 Save",
model_info: "🤖 Model Info",
model_name: "Model Name",
quant_type: "Quantization Type",
model_size: "Size (parameters)",
notes: "Notes",
opt_other: "Other",
editor_title: "📝 Test Database Editor",
btn_dl_html: "Download HTML DB",
btn_reset: "Reset",
btn_add_test: "Add Test",
msg_select_cat: "Select category on the left",
bench_ctrl: "🎯 Benchmark Controls",
select_cats: "Select categories:",
btn_start: "START",
btn_stop: "STOP",
progress: "Progress",
score: "Score",
live_log: "📋 Live Execution Log",
msg_bench_start: "Select categories and press «START»",
chart_cats: "📊 Results by Category",
chart_radar: "🎯 Competency Radar",
run_history: "📜 Run History",
btn_exp_json: "Export JSON",
btn_clr_hist: "Clear All History",
msg_empty: "No saved results",
comp_title: "🔍 Quantization Analysis & Comparison",
comp_desc: "Select results to compare (no limit). For degradation analysis, select Baseline (F16/Q8) first, and the quantized model (Q4/Q3) second.",
btn_run_comp: "Run Analysis",
comp_chart_diff: "📊 Score Difference",
comp_table_total: "🏆 Overall Table",
deg_title: "📉 Degradation Report (Where model got dumber)",
deg_desc: "Shows questions where the first selected model answered better than subsequent ones.",
msg_sel_models: "Select models to analyze",
comp_detailed: "📋 Detailed Question Comparison",
modal_add_title: "Add Test",
modal_edit_title: "Edit Test",
category: "Category",
modal_q: "Question / Prompt",
modal_a: "Expected Answer / Note for you",
modal_method: "Validation Method",
m_contains: "Contains Keywords",
m_exact: "Exact Match",
m_regex: "Regex Pattern",
m_no_repeat: "Anti-loop (no repeats)",
modal_keys: "Keys (comma separated. Use | for synonyms)",
btn_cancel: "Cancel",
cat_knowledge: "Factual Knowledge",
cat_math: "Mathematics",
cat_logic: "Logic",
cat_code: "Code",
cat_tools: "Tool Calling",
cat_antiloop: "Anti-looping",
cot_prompt_force: "\n\nPlease reason step by step inside <think>...</think> tags before providing your final answer.",
cot_prompt_disable: "\n\nAnswer immediately, briefly and to the point, without intermediate reasoning and without using <think> tags.",
tests_count: "tests",
expected_ans: "Expected (or keys):",
final_ans: "Final Answer",
cot_process: "Reasoning Process (CoT):",
err_collapse: "⚠️ Error (Model Collapse): Model generated {n} tokens, but visible text is missing. The model might have looped or failed to execute a call properly.",
err_raw: "Raw API output (for debugging):",
empty_ans: "Empty response...",
deg_no_deg: "🎉 No degradation detected! Quantized model answered equally or better.",
deg_base: "✅ {q} (Baseline)",
deg_bad: "❌ {q} (Degradation)",
deg_reason: "Reason: {r}",
msg_all_keys: "All keywords found",
msg_part_keys: "Keys found: {n} of {m}",
msg_no_keys: "Response does not contain expected keys",
msg_exact: "Exact match",
msg_exact_part: "Contains exact answer inside",
msg_not_match: "Response does not match",
msg_regex_ok: "Regex matched",
msg_regex_fail: "Regex failed",
msg_loop: "🔄 LOOP DETECTED! Repeat: \"{p}\"",
msg_no_loop: "Clean. No loops.",
msg_loop_no_keys: "No loop, but keys missing",
msg_collapse_short: "Collapse: {n} empty/spec tokens",
msg_empty_short: "Empty response from model",
msg_no_cond: "No check conditions",
prompt_sure_reset: "Are you sure? All your prompt changes will be lost!",
prompt_sure_del: "Delete test?",
prompt_sure_clr: "Clear run history?",
prompt_sure_del_res: "Delete this test result?",
msg_fill_q_a: "Fill in question and answer",
msg_saved: "Test saved",
msg_deleted: "Result deleted",
msg_cleared: "Cleared",
msg_select_cat_bench: "Select a category!",
msg_no_tests: "No tests in selected categories",
msg_bench_done: "Benchmark finished",
lbl_stopped: "⏹ Stopped",
lbl_done: "✅ Finished",
lbl_req: "Requesting...",
col_model: "Model",
col_quant: "Quant",
col_total: "Total %",
col_q: "Question"
}
};
let currentLang = 'ru';
function t(key, vars = {}) {
let str = TRANSLATIONS[currentLang][key] || TRANSLATIONS['ru'][key] || key;
for (const [k, v] of Object.entries(vars)) {
str = str.replace(`{${k}}`, v);
}
return str;
}
function applyTranslations() {
document.querySelectorAll('[data-i18n]').forEach(el => {
const key = el.getAttribute('data-i18n');
if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
el.placeholder = t(key);
} else {
el.innerHTML = t(key);
}
});
}
function changeLang(lang) {
currentLang = lang;
localStorage.setItem('llm_bench_lang', lang);
// Reload DB tests for specific language
loadDbTests();
applyTranslations();
// Re-render UI pieces that use translations dynamically
renderEditor();
renderBenchmarkCategories();
renderHistory();
if(!document.getElementById('tab-compare').classList.contains('hidden')) renderCompareSelect();
}
// ============================================================
// DEFAULT TESTS DATABASE (Ru / En)
// ============================================================
// === DB_START ===
const DEFAULT_TESTS = {
"ru": {
"knowledge": [
{
"id": 101,
"q": "Какая планета ближе всего к Солнцу? Ответь одним словом.",
"a": "Меркурий",
"method": "contains",
"check": [
"меркурий|mercury"
]
},
{
"id": 102,
"q": "В каком году человек впервые высадился на Луну? Ответь числом.",
"a": "1969",
"method": "contains",
"check": [
"1969"
]
},
{
"id": 103,
"q": "Какой химический элемент обозначается символом Au? Ответь одним словом.",
"a": "Золото",
"method": "contains",
"check": [
"золото|gold"
]
},
{
"id": 104,
"q": "Столица Австралии? Ответь одним словом.",
"a": "Канберра",
"method": "contains",
"check": [
"канберра|canberra"
]
},
{
"id": 105,
"q": "Сколько костей в теле взрослого человека? Ответь числом.",
"a": "206",
"method": "contains",
"check": [
"206"
]
},
{
"id": 106,
"q": "Общая теория относительности Эйнштейна предсказывает искривление чего? Ответь двумя словами.",
"a": "Пространства-времени",
"method": "contains",
"check": [
"пространства|пространство",
"времени|время"
]
},
{
"id": 107,
"q": "Какой фермент в желудке человека отвечает за расщепление белков и активен только в кислой среде?",
"a": "Пепсин",
"method": "contains",
"check": [
"пепсин|pepsin"
]
},
{
"id": 1778395920760,
"q": "Кто был единственным президентом США, который занял этот пост, не будучи избранным ни на должность президента, ни на должность вице-президента?",
"a": "Джеральд Форд",
"method": "contains",
"check": []
},
{
"id": 1778395956273,
"q": "Какой химический элемент чаще всего используется в качестве акцепторной примеси для создания дырочной проводимости (p-типа) в кремниевых полупроводниках?",
"a": "Бор",
"method": "contains",
"check": []
}
],
"math": [
{
"id": 201,
"q": "Сколько будет 15 * 17? Ответь одним числом.",
"a": "255",
"method": "contains",
"check": [
"255"
]
},
{
"id": 202,
"q": "Квадратный корень из 169? Ответь одним числом.",
"a": "13",
"method": "contains",
"check": [
"13"
]
},
{
"id": 203,
"q": "Реши: 3x + 7 = 22. Чему равен x? Ответь одним числом.",
"a": "5",
"method": "contains",
"check": [
"5"
]
},
{
"id": 204,
"q": "Сумма чисел от 1 до 100? Ответь одним числом.",
"a": "5050",
"method": "contains",
"check": [
"5050"
]
},
{
"id": 205,
"q": "Вычисли неопределенный интеграл от 2x по dx. Укажи только основной член без константы C.",
"a": "x^2",
"method": "contains",
"check": [
"x^2|x**2|x в квадрате"
]
},
{
"id": 206,
"q": "В классе 10 мальчиков и 15 девочек. Какова вероятность, что случайно выбранный ученик — мальчик? Ответь в виде десятичной дроби (например, 0.5).",
"a": "0.4",
"method": "contains",
"check": [
"0.4|0,4"
]
},
{
"id": 1778395989887,
"q": "Чему равна последняя цифра числа 7, возведенного в степень 2026?",
"a": "9",
"method": "contains",
"check": []
},
{
"id": 1778396018056,
"q": "Сколько нулей находится в конце числа, которое является результатом вычисления факториала 100 (100!)?",
"a": "24",
"method": "contains",
"check": []
}
],
"logic": [
{
"id": 301,
"q": "Продолжи последовательность: 2, 4, 8, 16, 32, ? Ответь одним числом.",
"a": "64",
"method": "contains",
"check": [
"64"
]
},
{
"id": 302,
"q": "Все розы — цветы. Некоторые цветы быстро вянут. Значит ли это что все розы быстро вянут? Ответь «да» или «нет».",
"a": "Нет",
"method": "contains",
"check": [
"нет|no"
]
},
{
"id": 303,
"q": "В комнате 4 угла. В каждом углу сидит кошка. Напротив каждой кошки сидят 3 кошки. Сколько всего кошек в комнате?",
"a": "4",
"method": "contains",
"check": [
"4|четыре|four"
]
},
{
"id": 304,
"q": "Два отца и два сына делят 3 монеты поровну. Как это возможно? Объясни кратко.",
"a": "Дед, отец и сын",
"method": "contains",
"check": [
"дед|внук|поколени"
]
},
{
"id": 305,
"q": "Врач дал вам 3 таблетки и велел принимать по одной каждые полчаса. Сколько времени займет прием всех таблеток? Ответь в часах (числом).",
"a": "1",
"method": "contains",
"check": [
"1|один|час"
]
},
{
"id": 306,
"q": "Марафонец обогнал бегуна, идущего на втором месте. На каком месте теперь марафонец? Ответьте одним словом.",
"a": "На втором",
"method": "contains",
"check": [
"втором|второе|2|second"
]
},
{
"id": 1778396051504,
"q": "Алиса смотрит на Боба, а Боб смотрит на Чарли. Алиса замужем, а Чарли — нет. Смотрит ли в этой ситуации женатый человек на неженатого? (Ответь только: Да, Нет или Недостаточно данных)",
"a": "Да",
"method": "contains",
"check": []
},
{
"id": 1778396073482,
"q": "Если сегодня понедельник, то какой день недели будет ровно через 2025 дней?",
"a": "Среда",
"method": "contains",
"check": []
}
],
"code": [
{
"id": 401,
"q": "Напиши функцию `is_prime` на Python для проверки, является ли число простым. Можешь добавить краткое объяснение.",
"a": "def is_prime",
"method": "contains",
"check": [
"def",
"is_prime|prime"
]
},
{
"id": 402,
"q": "Напиши SQL-запрос (SELECT), который выбирает всех пользователей из таблицы `users`, возраст которых больше 18 лет.",
"a": "SELECT * FROM users WHERE age > 18",
"method": "contains",
"check": [
"select",
"from",
"users",
"where",
"age",
"18"
]
},
{
"id": 403,
"q": "Какая сложность бинарного поиска? Ответь в формате O(...).",
"a": "O(log n)",
"method": "contains",
"check": [
"o(log n)|o(logn)|log n|logarithmic"
]
},
{
"id": 404,
"q": "Как написать декоратор на Python, который замеряет время выполнения функции? Напиши пример кода с использованием модуля time.",
"a": "import time\ndef timer",
"method": "contains",
"check": [
"def",
"wrapper",
"time"
]
},
{
"id": 405,
"q": "Напиши пример кастомного React хука useDebounce. Объясни, как он работает.",
"a": "function useDebounce",
"method": "contains",
"check": [
"usedebounce",
"useeffect",
"settimeout"
]
},
{
"id": 1778396097943,
"q": "(Python) Что выведет выражение 0.1 + 0.2 == 0.3 в Python?",
"a": "False",
"method": "contains",
"check": []
},
{
"id": 1778396122634,
"q": "(JavaScript) Какое булево значение вернет выражение [] == ![] в консоли браузера?",
"a": "true",
"method": "contains",
"check": []
}
],
"tools": [
{
"id": 501,
"q": "Сгенерируй JSON объект с двумя полями: 'name' (строка 'Иван') и 'age' (число 30).",
"a": "{\"name\": \"Иван\", \"age\": 30}",
"method": "contains",
"check": [
"{",
"}",
"\"name\"",
"\"ivan\"|\"иван\"",
"\"age\"",
"30"
]
},
{
"id": 502,
"q": "Представь, что у тебя есть инструмент `get_weather(city)`. Сгенерируй JSON-объект, чтобы вызвать его для города \"Москва\". Можешь написать пару слов от себя.",
"a": "{\"function\": \"get_weather\", \"args\": {\"city\": \"Москва\"}}",
"method": "contains",
"check": [
"get_weather",
"москва|moscow",
"city"
]
},
{
"id": 503,
"q": "Сгенерируй JSON для вызова API перевода. Отправитель \"Ivan\", получатель \"Anna\", сумма 5000 RUB. Формат: {\"action\": \"transfer\", \"data\": {...}}",
"a": "{\"action\":\"transfer\",\"data\":{\"sender\":\"Ivan\",\"receiver\":\"Anna\",\"amount\":5000,\"currency\":\"RUB\"}}",
"method": "contains",
"check": [
"transfer",
"ivan",
"anna",
"5000",
"rub"
]
},
{
"id": 504,
"q": "Напиши пример JSON, чтобы использовать функцию `search_flights`. Передай туда параметры: origin=\"SVO\", destination=\"DXB\", date=\"2026-10-15\".",
"a": "{\"function\":\"search_flights\",\"args\":{\"origin\":\"SVO\",\"destination\":\"DXB\",\"date\":\"2026-10-15\",\"passengers\":2}}",
"method": "contains",
"check": [
"search_flights",
"svo",
"dxb"
]
},
{
"id": 1778396195083,
"q": "У тебя есть функция get_weather(city: str, country: str). Сгенерируй ТОЛЬКО валидный JSON с аргументами для вызова этой функции, чтобы узнать погоду в Краснодаре. Не пиши никакого текста до или после JSON",
"a": "{\"city\": \"Краснодар\", \"country\": \"Россия\"} (допускается Russia/РФ)",
"method": "contains",
"check": []
},
{
"id": 1778396251186,
"q": "Извлеки аргументы для функции add_expense(item: str, total_price: float). Текст пользователя: \"Взял 3 килограмма яблок, каждый по 150 рублей\". Напиши ответ строго в формате: \"значение1\", значение2",
"a": "\"яблоки\", 450.0 (или \"яблоко\", 450)",
"method": "contains",
"check": []
}
],
"antiloop": [
{
"id": 603,
"q": "Сгенерируй длинный Python скрипт для парсинга веб-сайта, который использует BeautifulSoup. Код должен быть уникальным, без повторения одних и тех же строк.",
"a": "python parser",
"method": "no_repeat",
"check": [
"bs4|beautifulsoup",
"requests"
]
},
{
"id": 604,
"q": "Напиши короткий диалог (на 4-5 реплик) между пользователем и ИИ-помощником, где они обсуждают программирование.",
"a": "диалог ИИ и человека",
"method": "no_repeat",
"check": []
},
{
"id": 605,
"q": "Сгенерируй HTML/CSS код для сложной карточки товара с анимациями наведения, тенями и адаптивной версткой. Код должен быть длинным, уникальным и не зацикливаться на одних и тех же стилях.",
"a": "html css карточка товара",
"method": "no_repeat",
"check": [
"<div",
"style",
"hover",
"transition"
]
},
{
"id": 1778396282738,
"q": "Назови три разные столицы европейских государств, в названиях которых (на русском языке) вообще нет буквы \"А\". Напиши только эти три слова через запятую",
"a": "Например: Хельсинки, Осло, Лондон (или Берлин, Мадрид, Киев)",
"method": "contains",
"check": []
},
{
"id": 1778396306898,
"q": "Напиши слово \"Токен\" ровно 6 раз через пробел. Не используй знаки препинания вообще, даже точку в конце",
"a": "Токен Токен Токен Токен Токен Токен",
"method": "contains",
"check": []
}
]
},
"en": {
"knowledge": [
{
"id": 101,
"q": "Which planet is closest to the Sun? Answer with one word.",
"a": "Mercury",
"method": "contains",
"check": [
"mercury|меркурий"
]
},
{
"id": 102,
"q": "In what year did humans first land on the moon? Answer with a number.",
"a": "1969",
"method": "contains",
"check": [
"1969"
]
},
{
"id": 103,
"q": "Which chemical element is represented by the symbol Au? Answer with one word.",
"a": "Gold",
"method": "contains",
"check": [
"gold"
]
},
{
"id": 104,
"q": "What is the capital of Australia? Answer with one word.",
"a": "Canberra",
"method": "contains",
"check": [
"canberra"
]
},
{
"id": 105,
"q": "How many bones are in the adult human body? Answer with a number.",
"a": "206",
"method": "contains",
"check": [
"206"
]
},
{
"id": 106,
"q": "Einstein's general theory of relativity predicts the curvature of what? Answer with one word (or compound word).",
"a": "Spacetime",
"method": "contains",
"check": [
"spacetime|space-time",
"space",
"time"
]
},
{
"id": 107,
"q": "What enzyme in the human stomach is responsible for breaking down proteins and is only active in an acidic environment?",
"a": "Pepsin",
"method": "contains",
"check": [
"pepsin"
]
}
],
"math": [
{
"id": 201,
"q": "How much is 15 * 17? Answer with a single number.",
"a": "255",
"method": "contains",
"check": [
"255"
]
},
{
"id": 202,
"q": "What is the square root of 169? Answer with a single number.",
"a": "13",
"method": "contains",
"check": [
"13"
]
},
{
"id": 203,
"q": "Solve: 3x + 7 = 22. What is x? Answer with a single number.",
"a": "5",
"method": "contains",
"check": [
"5"
]
},
{
"id": 204,
"q": "What is the sum of numbers from 1 to 100? Answer with a single number.",
"a": "5050",
"method": "contains",
"check": [
"5050"
]
},
{
"id": 205,
"q": "Calculate the indefinite integral of 2x dx. Provide only the main term without the constant C.",
"a": "x^2",
"method": "contains",
"check": [
"x^2|x**2|x squared"
]
},
{
"id": 206,
"q": "A class has 10 boys and 15 girls. What is the probability that a randomly chosen student is a boy? Answer as a decimal (e.g., 0.5).",
"a": "0.4",
"method": "contains",
"check": [
"0.4|0,4"
]
}
],
"logic": [
{
"id": 301,
"q": "Continue the sequence: 2, 4, 8, 16, 32, ? Answer with a single number.",
"a": "64",
"method": "contains",
"check": [
"64"
]
},
{
"id": 302,
"q": "All roses are flowers. Some flowers fade quickly. Does this mean all roses fade quickly? Answer 'yes' or 'no'.",
"a": "No",
"method": "contains",
"check": [
"no|нет"
]
},
{
"id": 303,
"q": "A room has 4 corners. In each corner sits a cat. Opposite each cat sit 3 cats. How many cats are in the room?",
"a": "4",
"method": "contains",
"check": [
"4|four"
]
},
{
"id": 304,
"q": "Two fathers and two sons divide 3 coins equally. How is this possible? Explain briefly.",
"a": "Grandfather, father, and son",
"method": "contains",
"check": [
"grandfather",
"son",
"father",
"generations"
]
},
{
"id": 305,
"q": "A doctor gives you 3 pills and tells you to take one every half hour. How many hours will it take to take all the pills? Answer in hours (number).",
"a": "1",
"method": "contains",
"check": [
"1|one"
]
},
{
"id": 306,
"q": "A marathon runner overtakes the runner in second place. What place is the marathon runner in now? Answer with one word.",
"a": "Second",
"method": "contains",
"check": [
"second|2|втором|второе"
]
}
],
"code": [
{
"id": 401,
"q": "Write a Python function `is_prime` to check if a number is prime. You can add a brief explanation.",
"a": "def is_prime",
"method": "contains",
"check": [
"def",
"is_prime|prime"
]
},
{
"id": 402,
"q": "Write an SQL query (SELECT) that selects all users from the `users` table whose age is greater than 18.",
"a": "SELECT * FROM users WHERE age > 18",
"method": "contains",
"check": [
"select",
"from",
"users",
"where",
"age",
"18"
]
},
{
"id": 403,
"q": "What is the complexity of binary search? Answer in O(...) format.",
"a": "O(log n)",
"method": "contains",
"check": [
"o(log n)|o(logn)|log n|logarithmic"
]
},
{
"id": 404,
"q": "How to write a Python decorator that measures function execution time? Write a code example using the time module.",
"a": "import time\ndef timer",
"method": "contains",
"check": [
"def",
"wrapper",
"time"
]
},
{
"id": 405,
"q": "Write an example of a custom React hook useDebounce. Explain how it works.",
"a": "function useDebounce",
"method": "contains",
"check": [
"usedebounce",
"useeffect",
"settimeout"
]
}
],
"tools": [
{
"id": 501,
"q": "Generate a JSON object with two fields: 'name' (string 'John') and 'age' (number 30).",
"a": "{\"name\": \"John\", \"age\": 30}",
"method": "contains",
"check": [
"{",
"}",
"\"name\"",
"\"john\"",
"\"age\"",
"30"
]
},
{
"id": 502,
"q": "Imagine you have a tool `get_weather(city)`. Generate a JSON object to call it for \"Moscow\". You can say a few words yourself.",
"a": "{\"command\": \"shell_exec\", \"cmd\": \"ls -la\"}",
"method": "contains",
"check": [
"get_weather",
"moscow|москва",
"city"
]
},
{
"id": 503,
"q": "Generate JSON to call translation API. Sender \"Ivan\", receiver \"Anna\", amount 5000 USD. Format: {\"action\": \"transfer\", \"data\": {...}}",
"a": "{\"action\":\"transfer\",\"data\":{\"sender\":\"Ivan\",\"receiver\":\"Anna\",\"amount\":5000,\"currency\":\"USD\"}}",
"method": "contains",
"check": [
"transfer",
"ivan",
"anna",
"5000",
"usd"
]
},
{
"id": 504,
"q": "Write a JSON example to use the `search_flights` function. Pass parameters: origin=\"SVO\", destination=\"DXB\", date=\"2026-10-15\".",
"a": "{\"function\":\"search_flights\",\"args\":{\"origin\":\"JFK\",\"destination\":\"DXB\",\"date\":\"2026-10-15\",\"passengers\":2}}",
"method": "contains",
"check": [
"search_flights",
"svo",
"dxb"
]
}
],
"antiloop": [
{
"id": 601,
"q": "Write a bash (or bat) script to copy files from one folder to another and print a success message. Explain each line.",
"a": "tea instructions",
"method": "contains",
"check": [
"cp|xcopy|robocopy",
"echo"
]
},
{
"id": 602,
"q": "Tell me 3 interesting facts about black holes.",
"a": "5 trees",
"method": "contains",
"check": [
"black",
"hole"
]
},
{
"id": 603,
"q": "Generate a long Python script for parsing a website using BeautifulSoup. The code must be unique, without repeating the same lines.",
"a": "python parser",
"method": "no_repeat",
"check": [
"bs4|beautifulsoup",
"requests"
]
},
{
"id": 604,
"q": "Write a short dialogue (4-5 lines) between a user and an AI assistant where they discuss programming.",
"a": "autumn poem",
"method": "no_repeat",
"check": []
},
{
"id": 605,
"q": "Generate HTML/CSS code for a complex product card with hover animations, shadows, and responsive layout. Code should be long, unique, and not loop on the same styles.",
"a": "html css product card",
"method": "no_repeat",
"check": [
"<div",
"style",
"hover",
"transition"
]
}
]
}
};
// === DB_END ===
// ============================================================
// STATE
// ============================================================
let isRunning = false;
let shouldStop = false;
let currentResults = [];
let charts = {};
let dbTests = {};
let currentEditorCategory = 'knowledge';
const CAT_INFO = {
knowledge: { icon: '🧠', nameKey: 'cat_knowledge' },
math: { icon: '🔢', nameKey: 'cat_math' },
logic: { icon: '🧩', nameKey: 'cat_logic' },
code: { icon: '💻', nameKey: 'cat_code' },
tools: { icon: '⚡', nameKey: 'cat_tools' },
antiloop: { icon: '🔄', nameKey: 'cat_antiloop' }
};
// ============================================================
// DATA MANAGEMENT (Tests & Settings)
// ============================================================
function loadDbTests() {
// Меняем ключ на v2, чтобы принудительно обновить базу у пользователя!
const dbKey = `llm_bench_tests_db_v2_${currentLang}`;
const saved = localStorage.getItem(dbKey);
if (saved) {
try {
dbTests = JSON.parse(saved);
// Fallback migration: If loaded structure is old (not categorized properly), reset it
if (!dbTests['knowledge'] || !Array.isArray(dbTests['knowledge'])) {
throw new Error("Old structure");
}
return;
} catch(e) { console.warn("Resetting DB structure due to error/old format"); }
}
// Clone default for specific lang
dbTests = JSON.parse(JSON.stringify(DEFAULT_TESTS[currentLang]));
saveDbTests();
}
function saveDbTests() {
localStorage.setItem(`llm_bench_tests_db_v2_${currentLang}`, JSON.stringify(dbTests));
}
function resetToDefaultTests() {
if (confirm(t('prompt_sure_reset'))) {
dbTests = JSON.parse(JSON.stringify(DEFAULT_TESTS[currentLang]));
saveDbTests();
renderEditor();
showToast('🔄', t('msg_cleared'));
}
}
// ЭКСПОРТ В HTML С СОХРАНЕНИЕМ ТЕСТОВ
function exportToStandaloneHtml() {
// Клонируем документ, чтобы не испортить текущий интерфейс
const clone = document.documentElement.cloneNode(true);
// Удаляем стили Tailwind, добавленные через JS, чтобы файл генерировал их заново
const tailwindStyle = clone.querySelector('style[id^="tailwindcss"]');
if (tailwindStyle) tailwindStyle.remove();
// Возвращаем UI в первоначальное состояние
clone.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
const tabEditor = clone.querySelector('#tab-editor');
if(tabEditor) tabEditor.classList.remove('hidden');
clone.querySelectorAll('.tab-btn').forEach(el => el.classList.remove('active', 'border-[#00d4ff33]', 'text-[#00d4ff]'));
const editorBtn = clone.querySelectorAll('.tab-btn')[1];
if(editorBtn) editorBtn.classList.add('active', 'border-[#00d4ff33]', 'text-[#00d4ff]');
// Очищаем динамические данные логов и истории
const log = clone.querySelector('#testLog');
if(log) log.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-slate-500 space-y-4"><div class="text-6xl animate-bounce">🧪</div><p>${t('msg_bench_start')}</p></div>`;
const historyList = clone.querySelector('#historyList');
if(historyList) historyList.innerHTML = `<div class="text-center text-slate-500 py-8">${t('msg_empty')}</div>`;
// Очищаем контейнеры с графиками Chart.js
['resultsChart', 'radarChart', 'compareChart'].forEach(id => {
const canvas = clone.querySelector('#' + id);
if (canvas) {
const parent = canvas.parentElement;
const h2 = parent.querySelector('h2');
parent.innerHTML = '';
if (h2) parent.appendChild(h2);
const newCanvas = document.createElement('canvas');
newCanvas.id = id;
newCanvas.setAttribute('height', '250');
parent.appendChild(newCanvas);
}
});
// Очищаем таблицы сравнения
const compareTable = clone.querySelector('#compareTable');
if(compareTable) compareTable.innerHTML = '';
const degradationReport = clone.querySelector('#degradationReport');
if(degradationReport) degradationReport.innerHTML = `<p class="text-slate-500 text-center py-4">${t('msg_sel_models')}</p>`;
const compareDetailed = clone.querySelector('#compareDetailed');
if(compareDetailed) compareDetailed.innerHTML = '';
const compareResults = clone.querySelector('#comparisonResults');
if(compareResults) compareResults.classList.add('hidden');
// Прячем модалки
clone.querySelector('#testModal').classList.add('hidden');
clone.querySelector('#toast').classList.add('hidden', 'translate-y-10', 'opacity-0');
// Формируем чистый HTML
let html = "<!DOCTYPE html>\n<html lang=\"ru\">\n" + clone.innerHTML + "\n</html>";
// Подготавливаем базу тестов к записи
// We need to write the ENTIRE object including both EN and RU tests
// Merge current language user changes back into the overarching default object for export
const exportDb = JSON.parse(JSON.stringify(DEFAULT_TESTS));
exportDb[currentLang] = dbTests;
const testsJson = JSON.stringify(exportDb, null, 2);
// Регулярное выражение для точной замены блока тестов по маркерам
const markerRegex = /(\/\/\s*===\s*DB_START\s*===\s*\nconst\s+DEFAULT_TESTS\s*=\s*)[\s\S]*?(;\n\/\/\s*===\s*DB_END\s*===)/;
// Заменяем оригинальную базу на кастомную
html = html.replace(markerRegex, `$1${testsJson}$2`);
// Запуск скачивания
const blob = new Blob([html], {type: "text/html;charset=utf-8"});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `LLM_Benchmark_Custom_${new Date().toISOString().split('T')[0]}.html`;
a.click();
URL.revokeObjectURL(url);
showToast('📦', 'HTML Generated!');
}
function getSettings() {
const topPEl = document.getElementById('topP');
const topKEl = document.getElementById('topK');
const thinkingModeEl = document.getElementById('thinkingMode');
return {
serverUrl: document.getElementById('serverUrl').value.replace(/\/+$/, ''),
apiKey: document.getElementById('apiKey').value,
temperature: parseFloat(document.getElementById('temperature').value),
maxTokens: parseInt(document.getElementById('maxTokens').value),
topP: topPEl ? parseFloat(topPEl.value) : 0.95,
topK: topKEl ? parseInt(topKEl.value) : 40,
thinkingMode: thinkingModeEl ? thinkingModeEl.value : 'auto',
modelName: document.getElementById('modelName').value,
quantType: document.getElementById('quantType').value,
modelSize: document.getElementById('modelSize').value,
modelNotes: document.getElementById('modelNotes').value
};
}
function saveSettings() {
const s = getSettings();
localStorage.setItem('llm_bench_settings', JSON.stringify(s));
showToast('✅', t('msg_saved'));
}
function loadSettings() {
const saved = localStorage.getItem('llm_bench_settings');
if (saved) {
try {
const s = JSON.parse(saved);
// Force gateway defaults if old llama.cpp URL detected
if (!s.serverUrl || s.serverUrl.includes('8080')) { s.serverUrl = 'http://localhost:9000'; }
if (!s.modelName || s.modelName === 'local-model') { s.modelName = 'cortiq-auto'; }
document.getElementById('serverUrl').value = s.serverUrl || 'http://localhost:9000';
document.getElementById('apiKey').value = s.apiKey || '';
document.getElementById('temperature').value = s.temperature ?? 0.1;
document.getElementById('maxTokens').value = s.maxTokens || 512;
if(document.getElementById('topP')) document.getElementById('topP').value = s.topP ?? 0.95;
if(document.getElementById('topK')) document.getElementById('topK').value = s.topK ?? 40;
if(document.getElementById('thinkingMode')) document.getElementById('thinkingMode').value = s.thinkingMode || 'disable';
document.getElementById('modelName').value = s.modelName || '';
document.getElementById('quantType').value = s.quantType || 'Q4_K_M';
document.getElementById('modelSize').value = s.modelSize || '3B';
document.getElementById('modelNotes').value = s.modelNotes || '';
} catch(e){}
}
}
// ============================================================
// CONNECTION TO LLAMA.CPP
// ============================================================
async function testConnection() {
const statusDot = document.getElementById('serverStatus');
const statusText = document.getElementById('serverStatusText');
statusDot.className = 'status-dot checking';
statusText.textContent = t('status_checking');
const s = getSettings();
try {
const headers = { 'Content-Type': 'application/json' };
if (s.apiKey) headers['Authorization'] = `Bearer ${s.apiKey}`;
const res = await fetch(`${s.serverUrl}/health`, {
method: 'GET',
headers,
signal: AbortSignal.timeout(5000)
});
if (res.ok) {
statusDot.className = 'status-dot online';
statusText.textContent = t('status_online');
showToast('✅', t('status_online'));
return true;
}
} catch(e) {
// Try /v1/models as fallback
try {
const headers = { 'Content-Type': 'application/json' };
if (s.apiKey) headers['Authorization'] = `Bearer ${s.apiKey}`;
const res = await fetch(`${s.serverUrl}/v1/models`, {
method: 'GET',
headers,
signal: AbortSignal.timeout(5000)
});
if (res.ok) {
statusDot.className = 'status-dot online';
statusText.textContent = t('status_online');
showToast('✅', t('status_online'));
return true;
}
} catch(e2) {}
}
statusDot.className = 'status-dot offline';
statusText.textContent = t('status_offline');
showToast('❌', t('status_offline'));
return false;
}
async function checkServer() {
const statusDot = document.getElementById('serverStatus');
const statusText = document.getElementById('serverStatusText');
statusDot.className = 'status-dot checking';
statusText.textContent = t('status_checking');
const s = getSettings();
try {
const headers = {};
if (s.apiKey) headers['Authorization'] = `Bearer ${s.apiKey}`;
const res = await fetch(`${s.serverUrl}/health`, { method: 'GET', headers, signal: AbortSignal.timeout(3000) });
if (res.ok) {
statusDot.className = 'status-dot online';
statusText.textContent = t('status_online');
return;
}
} catch(e) {}
try {
const headers = {};
if (s.apiKey) headers['Authorization'] = `Bearer ${s.apiKey}`;
const res = await fetch(`${s.serverUrl}/v1/models`, { method: 'GET', headers, signal: AbortSignal.timeout(3000) });
if (res.ok) {
statusDot.className = 'status-dot online';
statusText.textContent = t('status_online');
return;
}
} catch(e) {}
statusDot.className = 'status-dot offline';
statusText.textContent = t('status_offline');
}
async function queryModel(prompt, maxTokens) {
const s = getSettings();
const headers = { 'Content-Type': 'application/json' };
if (s.apiKey) headers['Authorization'] = `Bearer ${s.apiKey}`;
// Modify prompt based on thinking mode
let finalPrompt = prompt;
if (s.thinkingMode === 'force') {
finalPrompt += t('cot_prompt_force');
} else if (s.thinkingMode === 'disable') {
finalPrompt += t('cot_prompt_disable');
}
// Try OpenAI-compatible endpoint first
try {
const res = await fetch(`${s.serverUrl}/v1/chat/completions`, {
method: 'POST',
headers,
body: JSON.stringify({
model: s.modelName || "cortiq-auto",
messages: [{ role: "user", content: finalPrompt }],
temperature: s.temperature,
max_tokens: maxTokens || s.maxTokens,
top_p: s.topP,
top_k: s.topK,
stream: false
}),
signal: AbortSignal.timeout(120000)
});
if (res.ok) {
const data = await res.json();
return {
text: data.choices?.[0]?.message?.content || '',
tokens_prompt: data.usage?.prompt_tokens || 0,
tokens_completion: data.usage?.completion_tokens || 0,
time_ms: 0
};
}
} catch(e) {}
// Fallback to /completion endpoint
const res = await fetch(`${s.serverUrl}/completion`, {
method: 'POST',
headers,
body: JSON.stringify({
prompt: finalPrompt,
temperature: s.temperature,
n_predict: maxTokens || s.maxTokens,
top_p: s.topP,
top_k: s.topK,
stream: false
}),
signal: AbortSignal.timeout(120000)
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
return {
text: data.content || '',
tokens_prompt: data.tokens_evaluated || 0,
tokens_completion: data.tokens_predicted || 0,
time_ms: data.timings?.predicted_ms || 0
};
}
// ============================================================
// EVALUATION & ANTI-LOOP (Enhanced)
// ============================================================
function evaluateAnswer(response, test) {
let rawResponse = response.text || '';
let responseText = rawResponse;
// Вырезаем теги <think>...</think> для оценки
const thinkRegex = /<(?:\|)?(?:think|thought)(?:\|)?>([\s\S]*?)(?:<\/(?:\|)?(?:think|thought)(?:\|)?>|$)/i;
const thinkMatch = responseText.match(thinkRegex);
if (thinkMatch) {
responseText = responseText.replace(new RegExp(thinkRegex.source, 'ig'), '').trim();
}
// Если после вырезки мыслей ответ пуст, возможно модель достигла max_tokens
// и не успела дать финал. В таком случае проверим весь сырой текст (включая мысли).
let textToEvaluate = responseText;
if (responseText === '' && thinkMatch) {
textToEvaluate = rawResponse;
}
const resp = textToEvaluate.toLowerCase().trim();
// Если ответ пуст и мыслей нет
if (resp === '' && !thinkMatch) {
if (response.tokens_completion > 5) {
return { status: 'fail', score: 0, detail: t('msg_collapse_short', {n: response.tokens_completion}) };
}
return { status: 'fail', score: 0, detail: t('msg_empty_short') };
}
if (test.method === 'contains') {
if (!test.check || test.check.length === 0) return { status: 'pass', score: 1.0, detail: t('msg_no_cond') };
let matchCount = 0;
test.check.forEach(condition => {
// ИЛИ логика для синонимов, разделенных символом "|"
const options = condition.toLowerCase().split('|').map(s=>s.trim());
if (options.some(opt => resp.includes(opt))) matchCount++;
});
const allMatch = matchCount === test.check.length;
const anyMatch = matchCount > 0;
if (allMatch) return { status: 'pass', score: 1.0, detail: t('msg_all_keys') };
if (anyMatch) return { status: 'partial', score: 0.5, detail: t('msg_part_keys', {n: matchCount, m: test.check.length}) };
return { status: 'fail', score: 0, detail: t('msg_no_keys') };
}
if (test.method === 'exact') {
if (resp === test.a.toLowerCase()) return { status: 'pass', score: 1.0, detail: t('msg_exact') };
if (resp.includes(test.a.toLowerCase())) return { status: 'partial', score: 0.5, detail: t('msg_exact_part') };
return { status: 'fail', score: 0, detail: t('msg_not_match') };
}
if (test.method === 'regex') {
try {
const re = new RegExp(test.check[0], 'i');
if (re.test(textToEvaluate)) return { status: 'pass', score: 1.0, detail: t('msg_regex_ok') };
} catch(e) {}
return { status: 'fail', score: 0, detail: t('msg_regex_fail') };
}
if (test.method === 'no_repeat') {
const loopResult = detectLooping(textToEvaluate);
const hasKeywords = test.check.length === 0 || test.check.some(c => textToEvaluate.toLowerCase().includes(c.toLowerCase()));
if (loopResult.isLooping) {
return { status: 'fail', score: 0, detail: t('msg_loop', {p: loopResult.repeatedPhrase}) };
}
if (hasKeywords) {
return { status: 'pass', score: 1.0, detail: t('msg_no_loop') };
}
return { status: 'partial', score: 0.5, detail: t('msg_loop_no_keys') };
}
return { status: 'fail', score: 0, detail: 'Unknown method' };
}
function detectLooping(text) {
if (!text || text.length < 50) return { isLooping: false, repeatedPhrase: '', uniqueness: 100 };
// 1. Line-block repeating (Crucial for scripts like .bat)
const lines = text.split('\n').map(l => l.trim()).filter(l => l.length > 5);
for (let window = 3; window <= 10; window++) {
let blockCounts = {};
for (let i = 0; i <= lines.length - window; i++) {
const block = lines.slice(i, i + window).join('|');
blockCounts[block] = (blockCounts[block] || 0) + 1;
if (blockCounts[block] >= 3) {
return { isLooping: true, repeatedPhrase: lines.slice(i, i+1).join(' ').substring(0, 50) + '...', count: blockCounts[block] };
}
}
}
// 2. N-gram word repeating
const words = text.split(/\s+/);
let maxRepeat = 0;
let repeatedPhrase = '';
for (let n = 4; n <= 8; n++) {
const ngrams = {};
for (let i = 0; i <= words.length - n; i++) {
const gram = words.slice(i, i + n).join(' ').toLowerCase().replace(/[.,!?;:]/g, '');
if (gram.length < 10) continue;
ngrams[gram] = (ngrams[gram] || 0) + 1;
if (ngrams[gram] > maxRepeat) {
maxRepeat = ngrams[gram];
repeatedPhrase = gram;
}
}
}
if (maxRepeat >= 4) {
return { isLooping: true, repeatedPhrase: repeatedPhrase.substring(0, 50) + '...', count: maxRepeat };
}
return { isLooping: false, repeatedPhrase: '', count: 0 };
}
// ============================================================
// EDITOR LOGIC
// ============================================================
function renderEditor() {
// Render Categories list
const catList = document.getElementById('categoryFilterList');
catList.innerHTML = Object.keys(CAT_INFO).map(c => `
<button class="w-full text-left px-4 py-3 rounded-xl border border-transparent transition ${currentEditorCategory === c ? 'bg-[#1e293b] border-[#00d4ff33] text-[#00d4ff]' : 'hover:bg-[#111827] text-slate-400'}" onclick="setEditorCategory('${c}')">
<span class="mr-2">${CAT_INFO[c].icon}</span>
<span class="font-medium">${t(CAT_INFO[c].nameKey)}</span>
<span class="float-right text-xs bg-[#0f172a] px-2 py-1 rounded text-slate-500">${(dbTests[c]||[]).length}</span>
</button>
`).join('');
// Render Tests for category
const tests = dbTests[currentEditorCategory] || [];
document.getElementById('currentCategoryTitle').textContent = `${CAT_INFO[currentEditorCategory].icon} ${t(CAT_INFO[currentEditorCategory].nameKey)}`;
document.getElementById('currentCategoryCount').textContent = `${tests.length} ${t('tests_count')}`;
const testsList = document.getElementById('testsEditorList');
if (tests.length === 0) {
testsList.innerHTML = `<div class="text-center text-slate-500 py-12">${t('msg_no_tests')}</div>`;
} else {
testsList.innerHTML = tests.map(tData => `
<div class="card p-4 border-[#334155] hover:border-[#00d4ff] bg-[#0f172a] flex flex-col gap-2">
<div class="flex justify-between items-start gap-4">
<div class="font-mono text-sm text-[#e2e8f0] flex-1 break-words">${escapeHtml(tData.q)}</div>
<div class="flex gap-2 flex-shrink-0">
<button class="text-slate-500 hover:text-[#00d4ff]" onclick="editTest(${tData.id}, '${currentEditorCategory}')">✏️</button>
<button class="text-slate-500 hover:text-[#ff3366]" onclick="deleteTest(${tData.id}, '${currentEditorCategory}')">🗑</button>
</div>
</div>
<div class="text-xs text-slate-400 border-l-2 border-[#334155] pl-2 mt-2">
<span class="text-[#00ff88]">${t('expected_ans')}</span> ${escapeHtml(tData.a)}<br>
<span class="text-slate-500">Method: ${tData.method} | Keys: ${(tData.check||[]).join(', ') || 'n/a'}</span>
</div>
</div>
`).join('');
}
}
function setEditorCategory(cat) {
currentEditorCategory = cat;
renderEditor();
}
function renderBenchmarkCategories() {
const container = document.getElementById('benchmarkCategoryList');
if (!container) return;
container.innerHTML = Object.keys(CAT_INFO).map(c => `
<label class="flex items-center gap-2 cursor-pointer hover:bg-[#1e293b] p-1 rounded">
<input type="checkbox" id="cat-${c}" checked class="accent-${c === 'knowledge' ? '#00ff88' : c === 'math' ? '#00d4ff' : c === 'logic' ? '#a855f7' : c === 'code' ? '#ffcc00' : c === 'tools' ? '#ff6b35' : '#ff3366'}">
<span class="text-sm">${CAT_INFO[c].icon} ${t(CAT_INFO[c].nameKey)}</span>
</label>
`).join('');
}
function showAddTestModal() {
document.getElementById('testModalTitle').textContent = t('modal_add_title');
document.getElementById('modalTestId').value = '';
// Populate category select
const catSelect = document.getElementById('modalCategory');
catSelect.innerHTML = Object.keys(CAT_INFO).map(c => `<option value="${c}">${CAT_INFO[c].icon} ${t(CAT_INFO[c].nameKey)}</option>`).join('');
catSelect.value = currentEditorCategory;
document.getElementById('modalQuestion').value = '';
document.getElementById('modalExpected').value = '';
document.getElementById('modalMethod').value = 'contains';
document.getElementById('modalKeys').value = '';
document.getElementById('testModal').classList.remove('hidden');
}
function editTest(id, cat) {
const test = dbTests[cat].find(t => t.id === id);
if(!test) return;
document.getElementById('testModalTitle').textContent = t('modal_edit_title');
document.getElementById('modalTestId').value = id;
const catSelect = document.getElementById('modalCategory');
catSelect.innerHTML = Object.keys(CAT_INFO).map(c => `<option value="${c}">${CAT_INFO[c].icon} ${t(CAT_INFO[c].nameKey)}</option>`).join('');
catSelect.value = cat;
document.getElementById('modalQuestion').value = test.q;
document.getElementById('modalExpected').value = test.a;
document.getElementById('modalMethod').value = test.method;
document.getElementById('modalKeys').value = (test.check||[]).join(', ');
document.getElementById('testModal').classList.remove('hidden');
}
function deleteTest(id, cat) {
if(confirm(t('prompt_sure_del'))) {
dbTests[cat] = dbTests[cat].filter(t => t.id !== id);
saveDbTests();
renderEditor();
}
}
function closeTestModal() {
document.getElementById('testModal').classList.add('hidden');
}
function saveTestFromModal() {
const idStr = document.getElementById('modalTestId').value;
const cat = document.getElementById('modalCategory').value;
const q = document.getElementById('modalQuestion').value.trim();
const a = document.getElementById('modalExpected').value.trim();
const method = document.getElementById('modalMethod').value;
const keysRaw = document.getElementById('modalKeys').value;
if(!q || !a) { showToast('⚠️', t('msg_fill_q_a')); return; }
const check = keysRaw.split(',').map(s=>s.trim()).filter(Boolean);
if (!dbTests[cat]) dbTests[cat] = [];
if (idStr) {
// Edit
const id = parseInt(idStr);
const idx = dbTests[cat].findIndex(t => t.id === id);
if(idx !== -1) {
dbTests[cat][idx] = { id, q, a, method, check };
}
} else {
// Add
dbTests[cat].push({ id: Date.now(), q, a, method, check });
}
saveDbTests();
closeTestModal();
setEditorCategory(cat);
showToast('✅', t('msg_saved'));
}
// ============================================================
// BENCHMARK RUNNER
// ============================================================
async function startBenchmark() {
if (isRunning) return;
const cats = [];
Object.keys(CAT_INFO).forEach(c => {
if(document.getElementById(`cat-${c}`).checked) cats.push(c);
});
if (cats.length === 0) { showToast('⚠️', t('msg_select_cat_bench')); return; }
let testsToRun = [];
cats.forEach(c => {
(dbTests[c]||[]).forEach(testObj => testsToRun.push({...testObj, category: c}));
});
if (testsToRun.length === 0) { showToast('⚠️', t('msg_no_tests')); return; }
const connected = await testConnection();
if (!connected) return;
isRunning = true;
shouldStop = false;
currentResults = [];
document.getElementById('btnStart').classList.add('hidden');
document.getElementById('btnStop').classList.remove('hidden');
document.getElementById('progressSection').classList.remove('hidden');
document.getElementById('liveScoreSection').classList.remove('hidden');
document.getElementById('testLog').innerHTML = '';
let p=0, f=0, pt=0;
for (let i = 0; i < testsToRun.length; i++) {
if (shouldStop) break;
const test = testsToRun[i];
const icon = CAT_INFO[test.category].icon;
document.getElementById('progressText').textContent = `${i + 1}/${testsToRun.length}`;
document.getElementById('progressFill').style.width = `${((i + 1) / testsToRun.length) * 100}%`;
document.getElementById('currentTestName').textContent = `${icon} ${test.q}`;
const logId = `log-${Date.now()}`;
addLogEntry(logId, icon, test.q, t('lbl_req'));
try {
const startTime = performance.now();
const maxT = test.category === 'antiloop' ? 1024 : undefined;
const response = await queryModel(test.q, maxT);
const elapsed = Math.round(performance.now() - startTime);
const evaluation = evaluateAnswer(response, test);
if (evaluation.status === 'pass') p++;
else if (evaluation.status === 'fail') f++;
else pt++;
// Расчет скорости токенов
const timeSec = elapsed / 1000;
const tps = (timeSec > 0 && response.tokens_completion > 0) ? (response.tokens_completion / timeSec).toFixed(1) : 0;
const result = {
category: test.category, question: test.q, expected: test.a,
response: response.text, evaluation,
tokens_completion: response.tokens_completion, time_ms: elapsed, tps: tps
};
currentResults.push(result);
updateLogEntry(logId, evaluation.status, result);
} catch(e) {
f++;
const result = { category: test.category, question: test.q, expected: test.a, response: `Error: ${e.message}`, evaluation: { status: 'fail', score: 0, detail: e.message } };
currentResults.push(result);
updateLogEntry(logId, 'fail', result);
}
const total = p + f + pt;
const validResults = currentResults.filter(r => parseFloat(r.tps) > 0);
const avgTps = validResults.length > 0 ? (validResults.reduce((sum, r) => sum + parseFloat(r.tps), 0) / validResults.length).toFixed(1) : 0;
document.getElementById('liveScore').textContent = `${total>0 ? Math.round(((p+pt*0.5)/total)*100) : 0}%`;
document.getElementById('livePass').textContent = p;
document.getElementById('liveFail').textContent = f;
document.getElementById('livePartial').textContent = pt;
document.getElementById('logCount').textContent = `${total} / ${avgTps > 0 ? ` ⚡ ${avgTps} t/s` : ''}`;
}
if (currentResults.length > 0) saveRunResults(currentResults);
isRunning = false;
document.getElementById('btnStart').classList.remove('hidden');
document.getElementById('btnStop').classList.add('hidden');
document.getElementById('currentTestName').textContent = shouldStop ? t('lbl_stopped') : t('lbl_done');
showToast('🏁', t('msg_bench_done'));
}
function stopBenchmark() { shouldStop = true; }
function addLogEntry(id, icon, q, detail) {
const div = document.createElement('div');
div.id = id;
div.className = 'test-result-card border-[#334155] animate-in';
div.innerHTML = `<div class="flex gap-2"><span class="animate-pulse">⏳</span> <div class="text-sm font-mono flex-1 truncate text-slate-400">${q}</div></div>`;
document.getElementById('testLog').prepend(div);
}
function updateLogEntry(id, status, r) {
const el = document.getElementById(id);
if (!el) return;
const cfg = {
pass: { c: 'pass', i: '✅', b: 'bg-[#00ff8822] text-[#00ff88]' },
fail: { c: 'fail', i: '❌', b: 'bg-[#ff336622] text-[#ff3366]' },
partial: { c: 'partial', i: '◐', b: 'bg-[#ffcc0022] text-[#ffcc00]' }
}[status];
// Проверка наличия тегов <think>
let rawResponse = r.response || '';
const thinkRegex = /<(?:\|)?(?:think|thought)(?:\|)?>([\s\S]*?)(?:<\/(?:\|)?(?:think|thought)(?:\|)?>|$)/i;
const thinkMatch = rawResponse.match(thinkRegex);
let thinkHtml = '';
let finalResponseHtml = '';
if (thinkMatch) {
const thinkContent = escapeHtml(thinkMatch[1].trim());
const stripped = rawResponse.replace(new RegExp(thinkRegex.source, 'ig'), '').trim();
if (stripped === '') {
finalResponseHtml = `<span class="text-[#ffcc00] italic">${t('empty_ans')}</span>`;
} else {
finalResponseHtml = escapeHtml(stripped);
}
thinkHtml = `
<div class="thinking-block bg-[#1e293b]/50 border border-[#334155] p-3 rounded-lg text-xs font-mono text-slate-400 whitespace-pre-wrap max-h-40 overflow-y-auto scrollbar-thin mb-2">
<div class="text-[#a855f7] font-bold mb-1 flex items-center gap-1">
<span>🧠</span> ${t('cot_process')}
</div>
${thinkContent}
</div>
`;
} else {
// ДЕТЕКТОР КОЛЛАПСА
if (rawResponse.trim() === '' && r.tokens_completion > 5) {
let rawDebug = rawResponse;
if (rawDebug === '') rawDebug = '[Empty string returned]';
else rawDebug = JSON.stringify(rawDebug);
finalResponseHtml = `
<span class="text-[#ff3366] font-bold bg-[#ff3366]/10 p-2 rounded block border border-[#ff3366]/30 mb-2">
${t('err_collapse', {n: r.tokens_completion})}
</span>
<div class="mt-2 border-t border-[#334155] pt-2">
<span class="text-xs text-slate-500 mb-1 block">${t('err_raw')}</span>
<div class="bg-black/50 p-2 rounded text-[#ffcc00] font-mono text-[10px] break-all max-h-32 overflow-y-auto scrollbar-thin">
${escapeHtml(rawDebug)}
</div>
</div>`;
} else if (rawResponse.trim() === '') {
finalResponseHtml = `<span class="text-slate-500 italic">${t('empty_ans')}</span>`;
} else {
finalResponseHtml = escapeHtml(rawResponse);
}
}
el.className = `test-result-card ${cfg.c} animate-in`;
el.innerHTML = `
<div class="flex flex-col gap-2">
<div class="text-sm font-medium text-slate-200 flex justify-between items-start gap-4">
<span>${CAT_INFO[r.category]?.icon || ''} ${escapeHtml(r.question)}</span>
<span class="badge ${cfg.b} flex-shrink-0 mt-0.5">${status.toUpperCase()}</span>
</div>
<div class="text-xs text-slate-400">
<span class="text-slate-500">${t('expected_ans')}</span>
<span class="text-[#00ff88]">${escapeHtml(r.expected)}</span>
</div>
<div class="mt-1">
${thinkHtml}
<div class="bg-[#0f172a] border border-[#1e293b] p-3 rounded-lg text-xs font-mono text-[#e2e8f0] break-words whitespace-pre-wrap max-h-40 overflow-y-auto scrollbar-thin relative pt-7">
<div class="absolute top-0 left-0 bg-[#1e293b] text-slate-400 text-[10px] uppercase px-2 py-1 rounded-br-lg select-none font-sans">${t('final_ans')}</div>
${finalResponseHtml}
</div>
</div>
<div class="text-[11px] text-slate-500 flex justify-between mt-1 items-center">
<span class="${status==='fail'?'text-[#ff3366]':status==='pass'?'text-[#00ff88]':'text-[#ffcc00]'}">${cfg.i} ${r.evaluation.detail}</span>
<div class="flex gap-2 items-center">
${r.tps ? `<span class="bg-[#1e293b] px-1.5 py-0.5 rounded text-[#00d4ff] font-bold border border-[#00d4ff33] text-[10px] uppercase tracking-wider">⚡ ${r.tps} t/s</span>` : ''}
<span>${r.tokens_completion||0} tkns • ${r.time_ms||0}ms</span>
</div>
</div>
</div>
`;
}
function saveRunResults(results) {
const s = getSettings();
const history = getHistory();
const total = results.length;
const pass = results.filter(r => r.evaluation.status === 'pass').length;
const partial = results.filter(r => r.evaluation.status === 'partial').length;
const score = Math.round(((pass + partial * 0.5) / total) * 100);
const catScores = {};
[...new Set(results.map(r => r.category))].forEach(cat => {
const cr = results.filter(r => r.category === cat);
const cp = cr.filter(r => r.evaluation.status === 'pass').length;
const cpt = cr.filter(r => r.evaluation.status === 'partial').length;
catScores[cat] = Math.round(((cp + cpt * 0.5) / cr.length) * 100);
});
history.push({
id: Date.now(), date: new Date().toISOString(),
modelName: s.modelName || 'Unknown Model', quantType: s.quantType || 'F16',
totalScore: score, total, pass, partial, fail: total-pass-partial,
catScores, results
});
localStorage.setItem('llm_bench_history', JSON.stringify(history));
}
function getHistory() {
try { return JSON.parse(localStorage.getItem('llm_bench_history') || '[]'); } catch { return []; }
}
function clearHistory() {
if (confirm(t('prompt_sure_clr'))) {
localStorage.removeItem('llm_bench_history');
renderHistory();
renderCompareSelect();
document.getElementById('comparisonResults').classList.add('hidden');
showToast('🗑', t('msg_cleared'));
}
}
function deleteHistoryEntry(id) {
if (confirm(t('prompt_sure_del_res'))) {
let history = getHistory();
history = history.filter(e => e.id !== id);
localStorage.setItem('llm_bench_history', JSON.stringify(history));
renderHistory();
if (!document.getElementById('tab-compare').classList.contains('hidden')) {
renderCompareSelect();
document.getElementById('comparisonResults').classList.add('hidden');
}
showToast('🗑', t('msg_deleted'));
}
}
function exportResults() {
const blob = new Blob([JSON.stringify(getHistory(), null, 2)], { type: 'application/json' });
const a = document.createElement('a'); a.href = URL.createObjectURL(blob);
a.download = `bench_results_${Date.now()}.json`; a.click();
}
function renderHistory() {
const history = getHistory();
const container = document.getElementById('historyList');
if (history.length === 0) { container.innerHTML = `<div class="text-center text-slate-500 py-8">${t('msg_empty')}</div>`; return; }
container.innerHTML = history.slice().reverse().map(e => {
const d = new Date(e.date);
const dateStr = isNaN(d) ? 'Unknown Date' : d.toLocaleString();
return `
<div class="card p-4 hover:border-[#00d4ff] bg-[#0f172a] relative transition-colors">
<div class="cursor-pointer" onclick="switchTab('compare')">
<div class="flex justify-between mb-2 pr-8">
<div>
<div class="font-bold text-sm text-[#e2e8f0]">${escapeHtml(e.modelName || 'Unknown')}</div>
<div class="text-xs bg-[#1e293b] px-2 py-0.5 rounded text-[#00d4ff] inline-block mt-1">${escapeHtml(e.quantType || 'N/A')}</div>
</div>
<div class="text-2xl font-black ${e.totalScore >= 80 ? 'text-[#00ff88]' : e.totalScore >= 50 ? 'text-[#ffcc00]' : 'text-[#ff3366]'}">${e.totalScore}%</div>
</div>
<div class="text-xs text-slate-500 mb-2">${dateStr} • ${e.total} ${t('tests_count')} (✅${e.pass} ◐${e.partial})</div>
</div>
<button onclick="deleteHistoryEntry(${e.id}); event.stopPropagation();" class="absolute top-3 right-3 text-slate-500 hover:text-[#ff3366] p-2 rounded transition-colors text-lg leading-none" title="Delete">×</button>
</div>
`}).join('');
}
function renderCharts() {
const history = getHistory();
if (history.length === 0) return;
const latest = history[history.length - 1];
if (charts.bar) charts.bar.destroy();
charts.bar = new Chart(document.getElementById('resultsChart'), {
type: 'bar',
data: {
labels: Object.keys(latest.catScores).map(c => t(CAT_INFO[c]?.nameKey) || c),
datasets: [{
data: Object.values(latest.catScores),
backgroundColor: Object.values(latest.catScores).map(v => v >= 80 ? 'rgba(0,255,136,0.5)' : v >= 50 ? 'rgba(255,204,0,0.5)' : 'rgba(255,51,102,0.5)'),
borderColor: Object.values(latest.catScores).map(v => v >= 80 ? '#00ff88' : v >= 50 ? '#ffcc00' : '#ff3366'),
borderWidth: 2, borderRadius: 6
}]
},
options: { plugins: { legend: { display: false } }, scales: { y: { max: 100, ticks: { color: '#64748b' } }, x: { ticks: { color: '#94a3b8' } } } }
});
if (charts.radar) charts.radar.destroy();
charts.radar = new Chart(document.getElementById('radarChart'), {
type: 'radar',
data: {
labels: Object.keys(latest.catScores).map(c => t(CAT_INFO[c]?.nameKey) || c),
datasets: [{
label: latest.modelName || 'Unknown', data: Object.values(latest.catScores),
backgroundColor: 'rgba(0,212,255,0.2)', borderColor: '#00d4ff', pointBackgroundColor: '#00d4ff'
}]
},
options: { scales: { r: { max: 100, ticks: { color: '#64748b', backdropColor: 'transparent' }, grid: { color: '#334155' }, pointLabels: { color: '#94a3b8' } } } }
});
}
// ============================================================
// DEGRADATION & COMPARISON
// ============================================================
function renderCompareSelect() {
const history = getHistory();
const c = document.getElementById('compareSelect');
if (history.length === 0) { c.innerHTML = `<span class="text-slate-500 text-sm">${t('msg_empty')}</span>`; return; }
c.innerHTML = history.slice().reverse().map(e => {
const d = new Date(e.date);
const dateStr = isNaN(d) ? '' : d.toLocaleDateString();
return `
<div class="flex items-center justify-between gap-2 px-3 py-2 rounded border border-[#334155] bg-[#0a0e17] hover:border-[#00d4ff] transition-colors">
<label class="flex items-center gap-2 cursor-pointer flex-1">
<input type="checkbox" class="compare-cb accent-[#00d4ff]" value="${e.id}">
<span class="text-sm font-medium text-[#e2e8f0]">${escapeHtml(e.modelName || 'Unknown')} <span class="text-xs text-[#00d4ff] bg-[#1e293b] px-1 rounded">${escapeHtml(e.quantType || 'N/A')}</span> <span class="text-xs text-slate-500 font-normal ml-1">${dateStr}</span></span>
</label>
<button onclick="deleteHistoryEntry(${e.id})" class="text-slate-600 hover:text-[#ff3366] px-1 text-lg leading-none transition-colors" title="Delete">×</button>
</div>
`}).join('');
}
function runComparison() {
const checked = document.querySelectorAll('.compare-cb:checked');
if (checked.length < 2) { showToast('⚠️', 'Select >= 2'); return; }
const history = getHistory();
const selected = Array.from(checked)
.map(cb => history.find(h => h.id === parseInt(cb.value)))
.sort((a,b) => a.id - b.id);
document.getElementById('comparisonResults').classList.remove('hidden');
const colors = ['#00ff88', '#00d4ff', '#a855f7', '#ff6b35', '#facc15', '#ec4899', '#3b82f6', '#14b8a6', '#f43f5e', '#8b5cf6'];
const allCats = [...new Set(selected.flatMap(s => Object.keys(s.catScores)))];
const thinkRegex = /<(?:\|)?(?:think|thought)(?:\|)?>[\s\S]*?(?:<\/(?:\|)?(?:think|thought)(?:\|)?>|$)/ig;
// Chart
if (charts.comp) charts.comp.destroy();
charts.comp = new Chart(document.getElementById('compareChart'), {
type: 'bar',
data: {
labels: allCats.map(c => t(CAT_INFO[c]?.nameKey) || c),
datasets: selected.map((s, i) => ({
label: `${s.modelName || 'Unknown'} [${s.quantType || 'N/A'}]`,
data: allCats.map(c => s.catScores[c] || 0),
backgroundColor: colors[i % colors.length] + '40', borderColor: colors[i % colors.length], borderWidth: 2
}))
},
options: { scales: { y: { max: 100 } } }
});
// Degradation Logic (Compare Baseline [0] vs Quantized [1])
const base = selected[0];
const quant = selected[1];
let degradationHTML = '';
if (base && quant) {
const degraded = [];
base.results.forEach(bRes => {
const qRes = quant.results.find(r => r.question === bRes.question);
if (qRes) {
const bScore = bRes.evaluation.status === 'pass' ? 2 : bRes.evaluation.status === 'partial' ? 1 : 0;
const qScore = qRes.evaluation.status === 'pass' ? 2 : qRes.evaluation.status === 'partial' ? 1 : 0;
if (bScore > qScore) degraded.push({ q: bRes.question, bRes, qRes });
}
});
if (degraded.length === 0) {
degradationHTML = `<div class="text-center py-6 text-[#00ff88]">${t('deg_no_deg')}</div>`;
} else {
degradationHTML = degraded.map(d => {
const bRespFinal = escapeHtml(d.bRes.response.replace(thinkRegex, '').trim());
const qRespFinal = escapeHtml(d.qRes.response.replace(thinkRegex, '').trim());
const degBaseStr = t('deg_base', {q: escapeHtml(base.quantType || 'N/A')});
const degBadStr = t('deg_bad', {q: escapeHtml(quant.quantType || 'N/A')});
return `
<div class="mb-4 bg-[#0a0e17] rounded-xl border border-[#ff3366]/30 overflow-hidden">
<div class="bg-[#1e293b] p-3 border-b border-[#334155] font-mono text-sm text-[#e2e8f0]">
${escapeHtml(d.q)}
</div>
<div class="grid grid-cols-2">
<div class="p-3 border-r border-[#334155]">
<div class="text-xs text-[#00ff88] font-bold mb-2">${degBaseStr}</div>
<div class="text-xs font-mono text-slate-300 whitespace-pre-wrap">${bRespFinal || '<span class="italic text-slate-500">Empty</span>'}</div>
</div>
<div class="p-3 bg-[#ff3366]/5">
<div class="text-xs text-[#ff3366] font-bold mb-2">${degBadStr}</div>
<div class="text-xs font-mono text-slate-300 whitespace-pre-wrap">${qRespFinal || '<span class="italic text-slate-500">Empty</span>'}</div>
<div class="mt-2 text-[10px] text-[#ff3366] italic">${t('deg_reason', {r: d.qRes.evaluation.detail})}</div>
</div>
</div>
</div>
`});
}
}
document.getElementById('degradationReport').innerHTML = degradationHTML;
// Render High Level Table
document.getElementById('compareTable').innerHTML = `
<table class="w-full text-left">
<thead class="border-b border-[#334155] text-slate-400">
<tr>
<th class="py-2 whitespace-nowrap">${t('col_model')}</th>
<th class="py-2 whitespace-nowrap">${t('col_quant')}</th>
<th class="py-2 whitespace-nowrap">${t('col_total')}</th>
${allCats.map(c=>`<th class="py-2 text-center whitespace-nowrap" title="${t(CAT_INFO[c]?.nameKey)}">${CAT_INFO[c]?.icon}</th>`).join('')}
</tr>
</thead>
<tbody class="divide-y divide-[#1e293b]">
${selected.map(s => `
<tr class="hover:bg-[#1e293b]">
<td class="py-2 text-[#e2e8f0] font-medium whitespace-nowrap pr-4">${escapeHtml(s.modelName || 'Unknown')}</td>
<td class="py-2 pr-4"><span class="text-xs text-[#00d4ff] bg-[#0f172a] px-1 rounded whitespace-nowrap">${escapeHtml(s.quantType || 'N/A')}</span></td>
<td class="py-2 font-bold whitespace-nowrap pr-4 ${s.totalScore >= 80?'text-[#00ff88]':'text-[#ffcc00]'}">${s.totalScore}%</td>
${allCats.map(c=>`<td class="py-2 text-center text-slate-400">${s.catScores[c]||0}%</td>`).join('')}
</tr>
`).join('')}
</tbody>
</table>
`;
// Side-by-Side Detailed Comparison Table
const detailedContainer = document.getElementById('compareDetailed');
const allQuestions = [...new Set(selected.flatMap(s => s.results.map(r => r.question)))];
detailedContainer.innerHTML = `
<table class="w-full text-sm text-left">
<thead class="border-b border-[#334155] text-slate-400">
<tr>
<th class="py-3 px-2 whitespace-nowrap">${t('col_q')}</th>
${selected.map((s, i) => `
<th class="text-center py-3 px-4 min-w-[120px]" style="color: ${colors[i % colors.length]}">
${escapeHtml(s.modelName || 'Unknown')}<br>
<span class="text-[10px] text-slate-500">${escapeHtml(s.quantType || 'N/A')}</span>
</th>
`).join('')}
</tr>
</thead>
<tbody class="divide-y divide-[#1e293b]">
${allQuestions.map(q => `
<tr class="hover:bg-[#1e293b] transition-colors">
<td class="py-3 px-2 text-slate-300 min-w-[200px] max-w-[300px] cursor-help" title="${escapeHtml(q)}">
<div class="truncate">${escapeHtml(q)}</div>
</td>
${selected.map(s => {
const r = s.results.find(res => res.question === q);
if (!r) return '<td class="text-center py-3 px-2 text-slate-600">—</td>';
const icon = r.evaluation.status === 'pass' ? '✅' : r.evaluation.status === 'partial' ? '◐' : '❌';
const finalResp = r.response.replace(thinkRegex, '').trim();
const tooltipHtml = `Status: ${r.evaluation.status.toUpperCase()}\nDetail: ${r.evaluation.detail}\n\nFinal Answer:\n${finalResp || '[Empty/Collapse]'}`;
return `
<td class="text-center py-3 px-2 text-lg cursor-help hover:scale-125 transition-transform"
title="${escapeHtml(tooltipHtml)}">
${icon}
</td>
`;
}).join('')}
</tr>
`).join('')}
</tbody>
</table>
`;
}
// ============================================================
// TABS & UTILS
// ============================================================
function switchTab(tab) {
document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
document.querySelectorAll('.tab-btn').forEach(el => el.classList.remove('active', 'border-[#00d4ff33]', 'text-[#00d4ff]'));
document.getElementById(`tab-${tab}`).classList.remove('hidden');
// Find current tab button
const btns = document.querySelectorAll('.tab-btn');
btns.forEach(btn => {
if(btn.getAttribute('onclick') === `switchTab('${tab}')`) {
btn.classList.add('active', 'border-[#00d4ff33]', 'text-[#00d4ff]');
}
});
if (tab === 'editor') renderEditor();
if (tab === 'results') { renderHistory(); setTimeout(renderCharts, 100); }
if (tab === 'compare') renderCompareSelect();
}
function escapeHtml(str) {
if (!str) return '';
const d = document.createElement('div');
d.textContent = str;
return d.innerHTML;
}
function showToast(icon, msg) {
const tToast = document.getElementById('toast');
document.getElementById('toastIcon').textContent = icon;
document.getElementById('toastMsg').textContent = msg;
tToast.classList.remove('hidden', 'translate-y-10', 'opacity-0');
clearTimeout(window._tTmr);
window._tTmr = setTimeout(() => tToast.classList.add('translate-y-10', 'opacity-0'), 3000);
setTimeout(()=> {if(tToast.classList.contains('opacity-0')) tToast.classList.add('hidden')}, 3300);
}
// ============================================================
// INIT
// ============================================================
document.addEventListener('DOMContentLoaded', () => {
const savedLang = localStorage.getItem('llm_bench_lang') || 'ru';
document.getElementById('langSwitcher').value = savedLang;
currentLang = savedLang;
applyTranslations();
loadSettings();
loadDbTests();
checkServer();
renderBenchmarkCategories();
renderEditor();
// Custom styling for scrollbar in textarea
document.querySelectorAll('textarea').forEach(el => el.classList.add('scrollbar-thin'));
});
</script>
</body>
</html>