/
MatroskinK
/
Test
Обзор
Документация
Войти
/
MatroskinK
/
Test
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
index.html
1 660 строк
55 KB
MatroskinK
Create: index.html, README.md
10 авг 2026, 12:55
Верифицирован
10 авг 2026, 12:55
cc6bbfe
Код
Авторство
О чём код?
<!doctype html> <html lang="ru"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>AI PC Configurator MVP</title> <style> :root { font-family: Arial, Helvetica, sans-serif; } body { margin: 0; background: #f5f7fa; color: #17233b; } header { background: #111827; color: white; padding: 20px; } header h1 { margin: 0 0 8px; font-size: 28px; } header p { margin: 0 0 12px; color: #d1d5db; } header button { background: #2563eb; color: white; border: 0; border-radius: 8px; padding: 10px 14px; cursor: pointer; } main { padding: 16px; display: grid; gap: 16px; } .panel { background: white; border-radius: 14px; padding: 16px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06); } h2 { margin-top: 0; font-size: 22px; } h3 { margin: 0 0 8px; font-size: 18px; } .controls { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 12px; } label { display: flex; flex-direction: column; gap: 6px; font-size: 14px; font-weight: 600; } input, select { width: 100%; box-sizing: border-box; padding: 9px 10px; border: 1px solid #cbd5e1; border-radius: 9px; background: white; } input[type="range"] { padding: 0; } .value-note { font-size: 13px; color: #475569; font-weight: normal; } .cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 10px; } .card { border: 1px solid #e2e8f0; background: #fcfdff; border-radius: 12px; padding: 12px; } .card .label { color: #64748b; font-size: 13px; margin-bottom: 6px; } .card .value { font-size: 22px; font-weight: 700; } .issue { padding: 9px 11px; border-radius: 9px; margin: 7px 0; font-size: 14px; } .issue.error { background: #fee2e2; color: #7f1d1d; } .issue.warning { background: #fef3c7; color: #78350f; } .issue.info { background: #dbeafe; color: #1e3a8a; } .table-wrap { overflow: auto; } table { width: 100%; border-collapse: collapse; } th, td { border: 1px solid #e2e8f0; padding: 9px; text-align: left; vertical-align: top; font-size: 14px; } th { background: #f8fafc; } .columns { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; } ul { margin: 0; padding-left: 18px; } li { margin: 7px 0; } .novice .advanced { display: none; } @media (max-width: 800px) { .columns { grid-template-columns: 1fr; } } @media print { header button, .controls { display: none; } body { background: white; } .panel { box-shadow: none; border: 1px solid #ddd; } } </style> </head> <body> <header> <h1>Конфигуратор ПК для локальных нейросетей</h1> <p> MVP. Цены статичные, тестовый срез от 10.08.2026. Подбор, дашборд, возможности сборки и рекомендации работают без сервера. </p> <button id="printBtn">Печать / сохранить в PDF</button> </header> <main> <section class="panel"> <h2>Требования</h2> <div class="controls" id="controls"> <label> Уровень пользователя <select id="userLevel"> <option value="novice">Новичок</option> <option value="advanced" selected>Продвинутый</option> <option value="corporate">Корпоративный</option> </select> </label> <label> Бюджет <input type="range" id="budget" min="80000" max="400000" step="5000" value="180000"> <span class="value-note" id="budgetLabel"></span> </label> <label> Основная задача <select id="task"> <option value="llm" selected>Текстовые LLM</option> <option value="image">Генерация изображений</option> <option value="audio">Аудио / речь</option> <option value="mixed">Смешанные задачи</option> </select> </label> <label class="advanced"> Размер LLM, млрд параметров <input type="range" id="modelSize" min="1" max="70" step="1" value="14"> <span class="value-note" id="modelSizeLabel"></span> </label> <label class="advanced"> Контекст, тыс. токенов <input type="range" id="context" min="2" max="128" step="2" value="16"> <span class="value-note" id="contextLabel"></span> </label> <label class="advanced"> Желаемая скорость, tokens/s <input type="range" id="tokens" min="5" max="100" step="1" value="20"> <span class="value-note" id="tokensLabel"></span> </label> <label> Разрешение изображений <select id="resolution"> <option value="512">512x512</option> <option value="768">768x768</option> <option value="1024" selected>1024x1024</option> </select> </label> <label class="advanced"> Batch size <input type="range" id="batch" min="1" max="4" step="1" value="1"> <span class="value-note" id="batchLabel"></span> </label> <label> Операционная система <select id="os"> <option value="windows" selected>Windows</option> <option value="linux">Linux</option> <option value="macos">macOS</option> </select> </label> <label> Уровень шума <select id="noisePref"> <option value="low">Тихий</option> <option value="medium" selected>Средний</option> <option value="high">Максимальная производительность</option> </select> </label> <label> Корпус <select id="caseSize"> <option value="ATX" selected>ATX / просторный</option> <option value="MicroATX">MicroATX / компактнее</option> </select> </label> <label class="advanced"> Лимит энергопотребления, Вт <input type="range" id="powerLimit" min="300" max="1000" step="10" value="700"> <span class="value-note" id="powerLimitLabel"></span> </label> </div> </section> <section class="panel"> <h2>Интерактивный дашборд</h2> <div id="dashboard"></div> </section> <section class="panel"> <h2>Подобранная конфигурация</h2> <div class="table-wrap"> <table id="buildTable"></table> </div> </section> <section class="panel"> <h2>Сильные и слабые стороны сборки</h2> <div id="prosCons"></div> </section> <section class="panel"> <h2>Что можно использовать на этой конфигурации</h2> <div class="table-wrap"> <table id="modelsTable"></table> </div> </section> <section class="panel"> <h2>Рекомендации: дешевле / лучше / дороже</h2> <div id="recommendations" class="cards"></div> </section> </main> <script> const TYPE_LABELS = { cpu: 'Процессор', gpu: 'Видеокарта', motherboard: 'Материнская плата', ram: 'Оперативная память', ssd: 'SSD', psu: 'Блок питания', case: 'Корпус', cooler: 'Охлаждение процессора' }; const CATEGORY_LABELS = { llm: 'LLM', image: 'Изображения', audio: 'Аудио', embedding: 'Embedding', multimodal: 'Мультимодальность', other: 'Другое' }; const SUPPORT_LABELS = { full: 'Полноценно', partial: 'Частично', cpu_offload: 'CPU offload', not_recommended: 'Не рекомендуется', unsupported: 'Не поддерживается', unknown: 'Неизвестно' }; const COMPONENTS = [ { id: 1, type: 'cpu', brand: 'AMD', model: 'Ryzen 5 7500F', name: 'Процессор AMD Ryzen 5 7500F', price: 14990, store: 'DNS', specs: { socket: 'AM5', cores: 6, threads: 12, tdp: 65, ram_type: 'DDR5' }, noise: 20, upgrade: 70, performance: { cpu: 45 } }, { id: 2, type: 'cpu', brand: 'Intel', model: 'Core i5-12400F', name: 'Процессор Intel Core i5-12400F', price: 11990, store: 'Ситилинк', specs: { socket: 'LGA1700', cores: 6, threads: 12, tdp: 65, ram_types: ['DDR4', 'DDR5'] }, noise: 20, upgrade: 65, performance: { cpu: 40 } }, { id: 3, type: 'cpu', brand: 'AMD', model: 'Ryzen 7 7700', name: 'Процессор AMD Ryzen 7 7700', price: 26990, store: 'Регард', specs: { socket: 'AM5', cores: 8, threads: 16, tdp: 65, ram_type: 'DDR5' }, noise: 25, upgrade: 80, performance: { cpu: 60 } }, { id: 4, type: 'gpu', brand: 'NVIDIA', model: 'GeForce RTX 3060 12GB', name: 'Видеокарта NVIDIA GeForce RTX 3060 12GB', price: 29990, store: 'DNS', specs: { vram_gb: 12, mem_bandwidth_gb_s: 360, compute: 'CUDA', length_mm: 280, tdp: 170, min_psu_w: 550, connectors: ['8-pin'] }, noise: 45, upgrade: 60, performance: { llm: 35, image: 40 } }, { id: 5, type: 'gpu', brand: 'NVIDIA', model: 'GeForce RTX 4060 Ti 16GB', name: 'Видеокарта NVIDIA GeForce RTX 4060 Ti 16GB', price: 46990, store: 'Ситилинк', specs: { vram_gb: 16, mem_bandwidth_gb_s: 288, compute: 'CUDA', length_mm: 285, tdp: 165, min_psu_w: 550, connectors: ['8-pin'] }, noise: 40, upgrade: 65, performance: { llm: 42, image: 52 } }, { id: 6, type: 'gpu', brand: 'NVIDIA', model: 'GeForce RTX 4070 Ti Super 16GB', name: 'Видеокарта NVIDIA GeForce RTX 4070 Ti Super 16GB', price: 79990, store: 'Регард', specs: { vram_gb: 16, mem_bandwidth_gb_s: 672, compute: 'CUDA', length_mm: 305, tdp: 285, min_psu_w: 700, connectors: ['12VHPWR'] }, noise: 60, upgrade: 75, performance: { llm: 68, image: 78 } }, { id: 7, type: 'gpu', brand: 'NVIDIA', model: 'GeForce RTX 4090 24GB', name: 'Видеокарта NVIDIA GeForce RTX 4090 24GB', price: 189990, store: 'DNS', specs: { vram_gb: 24, mem_bandwidth_gb_s: 1008, compute: 'CUDA', length_mm: 340, tdp: 450, min_psu_w: 850, connectors: ['12VHPWR'] }, noise: 80, upgrade: 85, performance: { llm: 95, image: 100 } }, { id: 8, type: 'gpu', brand: 'AMD', model: 'Radeon RX 7800 XT 16GB', name: 'Видеокарта AMD Radeon RX 7800 XT 16GB', price: 64990, store: 'ОнлайнТрейд', specs: { vram_gb: 16, mem_bandwidth_gb_s: 624, compute: 'ROCm', length_mm: 305, tdp: 263, min_psu_w: 650, connectors: ['8-pin', '8-pin'] }, noise: 65, upgrade: 60, performance: { llm: 55, image: 60 } }, { id: 9, type: 'motherboard', brand: 'MSI', model: 'PRO B650M-A WIFI', name: 'Материнская плата MSI PRO B650M-A WIFI', price: 13990, store: 'DNS', specs: { socket: 'AM5', ram_type: 'DDR5', form: 'MicroATX', ram_slots: 4, max_ram_gb: 192 }, noise: 10, upgrade: 80 }, { id: 10, type: 'motherboard', brand: 'MSI', model: 'PRO B760M-A DDR4', name: 'Материнская плата MSI PRO B760M-A DDR4', price: 11990, store: 'Ситилинк', specs: { socket: 'LGA1700', ram_type: 'DDR4', form: 'MicroATX', ram_slots: 4, max_ram_gb: 128 }, noise: 10, upgrade: 70 }, { id: 11, type: 'ram', brand: 'Kingston', model: 'Fury Beast DDR5 32GB 6000', name: 'Оперативная память Kingston Fury Beast DDR5 32GB 6000', price: 10990, store: 'Регард', specs: { type: 'DDR5', capacity_gb: 32, slots_used: 2 }, noise: 0, upgrade: 80 }, { id: 12, type: 'ram', brand: 'Kingston', model: 'Fury Beast DDR5 64GB 6000', name: 'Оперативная память Kingston Fury Beast DDR5 64GB 6000', price: 19990, store: 'Регард', specs: { type: 'DDR5', capacity_gb: 64, slots_used: 2 }, noise: 0, upgrade: 60 }, { id: 13, type: 'ssd', brand: 'Kingston', model: 'NV2 1TB', name: 'SSD Kingston NV2 1TB', price: 6490, store: 'DNS', specs: { capacity_gb: 1000, interface: 'NVMe' }, noise: 0, upgrade: 50 }, { id: 14, type: 'ssd', brand: 'Samsung', model: '990 Pro 2TB', name: 'SSD Samsung 990 Pro 2TB', price: 15990, store: 'Ситилинк', specs: { capacity_gb: 2000, interface: 'NVMe' }, noise: 0, upgrade: 70 }, { id: 15, type: 'psu', brand: 'Deepcool', model: 'PK750D 750W', name: 'Блок питания Deepcool PK750D 750W', price: 6490, store: 'ОнлайнТрейд', specs: { wattage: 750, connectors: { pcie_8pin: 3, pcie_12vhpwr: 0 } }, noise: 25, upgrade: 50 }, { id: 16, type: 'psu', brand: 'be quiet!', model: 'Pure Power 12 M 850W', name: 'Блок питания be quiet! Pure Power 12 M 850W', price: 10990, store: 'XCOM-SHOP', specs: { wattage: 850, connectors: { pcie_8pin: 4, pcie_12vhpwr: 1 } }, noise: 15, upgrade: 80 }, { id: 17, type: 'case', brand: 'Deepcool', model: 'CH560 Digital', name: 'Корпус Deepcool CH560 Digital', price: 7990, store: 'DNS', specs: { form_factors: ['ATX', 'MicroATX', 'MiniITX'], max_gpu_length_mm: 380, max_cooler_height_mm: 175 }, noise: 35, upgrade: 75 }, { id: 18, type: 'case', brand: 'Fractal Design', model: 'Pop Mini Air', name: 'Корпус Fractal Design Pop Mini Air', price: 6990, store: 'Ситилинк', specs: { form_factors: ['MicroATX', 'MiniITX'], max_gpu_length_mm: 365, max_cooler_height_mm: 170 }, noise: 30, upgrade: 65 }, { id: 19, type: 'cooler', brand: 'ID-COOLING', model: 'SE-224-XTS', name: 'Кулер ID-COOLING SE-224-XTS', price: 1990, store: 'Регард', specs: { height_mm: 150, tdp_max: 180, sockets: ['AM5', 'LGA1700'] }, noise: 25, upgrade: 50 }, { id: 20, type: 'cooler', brand: 'Deepcool', model: 'AK620', name: 'Кулер Deepcool AK620', price: 3990, store: 'DNS', specs: { height_mm: 160, tdp_max: 260, sockets: ['AM5', 'LGA1700'] }, noise: 35, upgrade: 70 }, { id: 21, type: 'ram', brand: 'Kingston', model: 'Fury Beast DDR4 32GB 3200', name: 'Оперативная память Kingston Fury Beast DDR4 32GB 3200', price: 7990, store: 'Регард', specs: { type: 'DDR4', capacity_gb: 32, slots_used: 2 }, noise: 0, upgrade: 70 } ]; const MODELS = [ { id: 1, name: 'Llama 3.1 8B Instruct', category: 'llm', params_b: 8, frameworks: ['llama.cpp', 'Ollama', 'LM Studio', 'vLLM'], os: ['Windows', 'Linux'], reqs: [ { label: 'Q4, контекст 8K', min_vram: 6, rec_vram: 8, min_ram: 16 }, { label: 'Q8, контекст 8K', min_vram: 9, rec_vram: 12, min_ram: 16 }, { label: 'FP16, контекст 8K', min_vram: 16, rec_vram: 20, min_ram: 32 } ] }, { id: 2, name: 'Qwen 2.5 14B Instruct', category: 'llm', params_b: 14, frameworks: ['llama.cpp', 'Ollama', 'LM Studio', 'vLLM'], os: ['Windows', 'Linux'], reqs: [ { label: 'Q4, контекст 16K', min_vram: 10, rec_vram: 12, min_ram: 32 }, { label: 'Q5, контекст 16K', min_vram: 13, rec_vram: 16, min_ram: 32 } ] }, { id: 3, name: 'DeepSeek-R1-Distill-32B', category: 'llm', params_b: 32, frameworks: ['llama.cpp', 'Ollama', 'vLLM'], os: ['Windows', 'Linux'], reqs: [ { label: 'Q3, контекст 16K', min_vram: 16, rec_vram: 20, min_ram: 64 }, { label: 'Q4, контекст 16K', min_vram: 20, rec_vram: 24, min_ram: 64 } ] }, { id: 4, name: 'Stable Diffusion XL 1.0', category: 'image', params_b: null, frameworks: ['ComfyUI', 'Automatic1111', 'Forge'], os: ['Windows', 'Linux'], reqs: [ { label: '1024x1024, batch 1', min_vram: 8, rec_vram: 12, min_ram: 16 }, { label: '1024x1024, batch 2', min_vram: 12, rec_vram: 16, min_ram: 16 } ] }, { id: 5, name: 'Whisper large-v3', category: 'audio', params_b: 1.55, frameworks: ['faster-whisper', 'whisper.cpp'], os: ['Windows', 'Linux'], reqs: [ { label: 'GPU FP16', min_vram: 4, rec_vram: 6, min_ram: 8 } ] }, { id: 6, name: 'Flux.1 schnell', category: 'image', params_b: null, frameworks: ['ComfyUI'], os: ['Windows', 'Linux'], reqs: [ { label: 'FP8', min_vram: 12, rec_vram: 16, min_ram: 16 }, { label: 'FP16', min_vram: 20, rec_vram: 24, min_ram: 32 } ] } ]; function byId(id) { return COMPONENTS.find(c => c.id === id); } function fmt(n) { return new Intl.NumberFormat('ru-RU').format(Math.round(n)); } function val(id) { return document.getElementById(id).value; } function num(id) { return Number(document.getElementById(id).value); } function getReq() { return { userLevel: val('userLevel'), budget: num('budget'), task: val('task'), modelSize: num('modelSize'), contextK: num('context'), desiredTokens: num('tokens'), resolution: val('resolution'), batch: num('batch'), os: val('os'), noisePref: val('noisePref'), caseSize: val('caseSize'), powerLimit: num('powerLimit') }; } function calcRequired(req) { const llmVRAM = req.modelSize * 0.6 + req.contextK * 0.04 + 1; const imageBase = { '512': 4, '768': 6, '1024': 8 }[req.resolution] || 8; const imageVRAM = imageBase + (req.batch - 1) * 3; const audioVRAM = 4; let vram = 6; if (req.task === 'llm') { vram = llmVRAM; } if (req.task === 'image') { vram = imageVRAM; } if (req.task === 'audio') { vram = audioVRAM; } if (req.task === 'mixed') { vram = Math.max(llmVRAM, imageVRAM, audioVRAM); } vram = Math.ceil(vram); let ram = Math.max(16, Math.ceil(req.modelSize * 1.6)); if (req.task === 'image' && req.batch >= 2) { ram += 8; } if (req.task === 'mixed') { ram = Math.max(ram, 32); } ram = Math.min(128, ram); const gpuBudget = req.budget * 0.55; return { vram, ram, gpuBudget }; } function gpuScore(gpu, req, required) { let score = req.task === 'image' ? gpu.performance.image : gpu.performance.llm; if (req.task === 'mixed') { score = (gpu.performance.llm + gpu.performance.image) / 2; } if (req.task === 'audio') { score = 65; } if (gpu.specs.vram_gb < required.vram) { score *= 0.45; } if (req.os === 'windows' && gpu.specs.compute === 'ROCm') { score *= 0.55; } return score; } function chooseGPU(req, required, mode) { let gpus = COMPONENTS.filter(c => c.type === 'gpu'); let pool = gpus.filter(g => g.specs.vram_gb >= required.vram); if (!pool.length) { pool = [...gpus]; } if (req.os === 'windows') { const cuda = pool.filter(g => g.specs.compute === 'CUDA'); if (cuda.length) { pool = cuda; } } if (mode === 'cheaper') { pool.sort((a, b) => a.price - b.price); } else if (mode === 'expensive') { pool.sort((a, b) => gpuScore(b, req, required) - gpuScore(a, req, required) || b.price - a.price); } else if (mode === 'best') { pool.sort((a, b) => (gpuScore(b, req, required) / b.price) - (gpuScore(a, req, required) / a.price)); } else { pool.sort((a, b) => (gpuScore(b, req, required) / b.price) - (gpuScore(a, req, required) / a.price)); const affordable = pool.filter(g => g.price <= required.gpuBudget); if (affordable.length) { pool = affordable; } } return pool[0] || gpus.sort((a, b) => a.price - b.price)[0]; } function chooseCPU(req, required, gpu) { if (required.ram >= 64 || req.modelSize >= 30) { return byId(3); } if (req.budget < 110000 && gpu.price < 50000) { return byId(2); } return byId(1); } function selectBuild(req, mode = 'normal') { const required = calcRequired(req); const gpu = chooseGPU(req, required, mode); const cpu = chooseCPU(req, required, gpu); const motherboard = cpu.specs.socket === 'AM5' ? byId(9) : byId(10); let ram = motherboard.specs.ram_type === 'DDR5' ? (required.ram >= 64 ? byId(12) : byId(11)) : byId(21); let ssd = (req.modelSize >= 30 || req.task !== 'llm' || req.budget >= 180000) ? byId(14) : byId(13); const estPowerRaw = cpu.specs.tdp + gpu.specs.tdp + 70; let psu = byId(15); if ( (gpu.specs.connectors || []).includes('12VHPWR') || estPowerRaw > 600 || req.noisePref === 'low' || req.budget >= 180000 ) { psu = byId(16); } if (estPowerRaw > psu.specs.wattage * 0.8) { psu = byId(16); } let caseComp = req.caseSize === 'ATX' ? byId(17) : byId(18); if (gpu.specs.length_mm > caseComp.specs.max_gpu_length_mm) { caseComp = byId(17); } let cooler = (cpu.id === 3 || req.noisePref === 'low') ? byId(20) : byId(19); if (cooler.specs.height_mm > caseComp.specs.max_cooler_height_mm) { cooler = byId(19); } const items = [cpu, gpu, motherboard, ram, ssd, psu, caseComp, cooler]; const byType = {}; items.forEach(c => { byType[c.type] = c; }); const analysis = analyzeBuild(items, byType, req, required); return { items, byType, gpu, total: analysis.total, required, analysis }; } function analyzeBuild(items, byType, req, required) { const total = items.reduce((s, c) => s + c.price, 0); const cpu = byType.cpu; const gpu = byType.gpu; const motherboard = byType.motherboard; const ram = byType.ram; const psu = byType.psu; const caseComp = byType['case']; const cooler = byType.cooler; const estPower = cpu.specs.tdp + gpu.specs.tdp + 70; const issues = []; if (cpu.specs.socket !== motherboard.specs.socket) { issues.push({ severity: 'error', text: 'Сокет CPU и материнской платы не совпадает.' }); } if (motherboard.specs.ram_type !== ram.specs.type) { issues.push({ severity: 'error', text: 'Тип оперативной памяти не совместим с материнской платой.' }); } if (!caseComp.specs.form_factors.includes(motherboard.specs.form)) { issues.push({ severity: 'error', text: 'Форм-фактор материнской платы не поддерживается корпусом.' }); } if (gpu.specs.length_mm > caseComp.specs.max_gpu_length_mm) { issues.push({ severity: 'error', text: 'Видеокарта слишком длинная для выбранного корпуса.' }); } if (cooler.specs.height_mm > caseComp.specs.max_cooler_height_mm) { issues.push({ severity: 'error', text: 'Кулер слишком высокий для выбранного корпуса.' }); } if (!cooler.specs.sockets.includes(cpu.specs.socket)) { issues.push({ severity: 'error', text: 'Кулер не поддерживает сокет процессора.' }); } const connectors = gpu.specs.connectors || []; const eightNeeded = connectors.filter(x => x === '8-pin').length; const twelveNeeded = connectors.filter(x => x === '12VHPWR').length; if (eightNeeded > psu.specs.connectors.pcie_8pin) { issues.push({ severity: 'error', text: 'Блоку питания не хватает разъемов 8-pin для видеокарты.' }); } if (twelveNeeded > psu.specs.connectors.pcie_12vhpwr) { issues.push({ severity: 'error', text: 'Для видеокарты нужен разъем 12VHPWR, которого нет у выбранного блока питания.' }); } if (gpu.specs.vram_gb < required.vram) { issues.push({ severity: 'warning', text: `VRAM ${gpu.specs.vram_gb} GB меньше требуемых ~${required.vram} GB. Возможны ограничения или CPU offload.` }); } if (ram.specs.capacity_gb < required.ram) { issues.push({ severity: 'warning', text: `RAM ${ram.specs.capacity_gb} GB меньше рекомендуемых ~${required.ram} GB.` }); } if (estPower > req.powerLimit) { issues.push({ severity: 'warning', text: `Расчетное энергопотребление ${estPower} Вт выше лимита ${req.powerLimit} Вт.` }); } if (estPower > psu.specs.wattage * 0.8) { issues.push({ severity: 'warning', text: 'Блок питания работает близко к пределу.' }); } if (total > req.budget) { issues.push({ severity: 'warning', text: `Итоговая цена ${fmt(total)} ₽ превышает бюджет ${fmt(req.budget)} ₽.` }); } if (req.os === 'windows' && gpu.specs.compute === 'ROCm') { issues.push({ severity: 'warning', text: 'AMD ROCm под Windows может работать хуже, чем CUDA. Для Windows лучше NVIDIA.' }); } if (req.os === 'macos') { issues.push({ severity: 'warning', text: 'В текущей статической базе нет полноценной Apple/Metal-конфигурации. Для macOS нужен отдельный подбор.' }); } if (req.noisePref === 'low' && gpu.noise > 50) { issues.push({ severity: 'info', text: 'Для тихой сборки можно рассмотреть менее мощную или более тихую видеокарту.' }); } let performance = req.task === 'image' ? gpu.performance.image : gpu.performance.llm; if (req.task === 'mixed') { performance = (gpu.performance.llm + gpu.performance.image) / 2; } if (req.task === 'audio') { performance = 70; } if (gpu.specs.vram_gb < required.vram) { performance *= 0.45; } if (req.os === 'windows' && gpu.specs.compute === 'ROCm') { performance *= 0.55; } performance = Math.round(Math.min(100, performance)); const pricePerformance = total ? Math.round(performance / (total / 100000)) : 0; const noiseScore = Math.round((gpu.noise + cooler.noise + caseComp.noise + psu.noise) / 4); const noiseLabel = noiseScore < 28 ? 'Низкий' : noiseScore < 45 ? 'Средний' : 'Высокий'; const ramSlotsLeft = motherboard.specs.ram_slots - ram.specs.slots_used; const upgradeScore = Math.round( ( motherboard.upgrade + psu.upgrade + caseComp.upgrade + Math.min(100, ramSlotsLeft * 30) ) / 4 ); const strengths = []; const weaknesses = []; if (gpu.specs.vram_gb >= required.vram) { strengths.push(`Достаточный VRAM: ${gpu.specs.vram_gb} GB для требований ~${required.vram} GB.`); } else { weaknesses.push(`Недостаточно VRAM: нужно ~${required.vram} GB, установлено ${gpu.specs.vram_gb} GB.`); } if (total <= req.budget) { strengths.push('Сборка укладывается в бюджет.'); } else { weaknesses.push('Сборка выходит за бюджет.'); } if (issues.filter(i => i.severity === 'error').length === 0) { strengths.push('Критических проблем совместимости не найдено.'); } else { weaknesses.push('Есть критические проблемы совместимости.'); } if (ram.specs.capacity_gb >= required.ram) { strengths.push(`RAM ${ram.specs.capacity_gb} GB достаточно для выбранной задачи.`); } else { weaknesses.push(`RAM ${ram.specs.capacity_gb} GB может не хватить, рекомендуется ${required.ram} GB.`); } if (psu.specs.wattage >= estPower * 1.2) { strengths.push('Блок питания имеет запас мощности.'); } else { weaknesses.push('Блок питания близок к пределу по мощности.'); } if (noiseLabel === 'Низкий') { strengths.push('Сборка получилась тихой.'); } else if (req.noisePref === 'low' && noiseLabel !== 'Низкий') { weaknesses.push('Сборка может быть шумнее ожидаемого.'); } if (gpu.specs.compute === 'CUDA') { strengths.push('CUDA — наиболее зрелая экосистема для локальных нейросетей.'); } if (gpu.specs.compute === 'ROCm') { weaknesses.push('ROCm может требовать Linux и дополнительной настройки.'); } if (ramSlotsLeft > 0) { strengths.push(`Есть свободные слоты RAM: ${ramSlotsLeft}.`); } else { weaknesses.push('Свободные слоты RAM отсутствуют.'); } return { total, estPower, issues, performance, pricePerformance, noiseScore, noiseLabel, upgradeScore, strengths, weaknesses }; } function chooseModelReq(model, build, req) { const gpu = build.byType.gpu; if (model.category === 'image' && model.name.includes('Stable Diffusion XL')) { return req.batch >= 2 ? model.reqs[1] : model.reqs[0]; } if (model.name.includes('Flux')) { return gpu.specs.vram_gb >= 20 ? model.reqs[1] : model.reqs[0]; } const recFits = model.reqs.filter(r => r.rec_vram <= gpu.specs.vram_gb); if (recFits.length) { return recFits[recFits.length - 1]; } const minFits = model.reqs.filter(r => r.min_vram <= gpu.specs.vram_gb); if (minFits.length) { return minFits[0]; } return model.reqs[0]; } function getModelSupport(build, req) { const gpu = build.byType.gpu; const ram = build.byType.ram; return MODELS.map(model => { const selected = chooseModelReq(model, build, req); let support = 'unknown'; const osOk = model.os.some(o => o.toLowerCase() === req.os.toLowerCase()); if (!osOk) { support = 'not_recommended'; } else if (gpu.specs.vram_gb >= selected.rec_vram && ram.specs.capacity_gb >= selected.min_ram) { support = 'full'; } else if (gpu.specs.vram_gb >= selected.min_vram) { support = 'partial'; } else if (ram.specs.capacity_gb >= selected.min_ram) { support = 'cpu_offload'; } else { support = 'unsupported'; } if (req.os === 'windows' && gpu.specs.compute === 'ROCm' && support !== 'unsupported') { support = 'partial'; } let speed = '-'; if (model.category === 'llm') { if (support === 'cpu_offload') { speed = '2-5 tok/s (offload)'; } else { const modelGb = model.params_b * 0.6; let tps = Math.round(gpu.specs.mem_bandwidth_gb_s / modelGb * 0.55); if (gpu.specs.vram_gb < selected.rec_vram) { tps = Math.round(tps * 0.7); } speed = `${Math.max(1, Math.min(160, tps))} tok/s`; } } if (model.category === 'image') { if (support !== 'unsupported') { let sec = Math.max(1.5, 22 - gpu.performance.image / 4); if (gpu.specs.vram_gb < selected.rec_vram) { sec *= 1.35; } speed = `${sec.toFixed(1)} с/изобр.`; } } if (model.category === 'audio') { speed = support === 'unsupported' ? '-' : 'быстрее реального времени'; } return { model, selected, support, speed, requirements: `VRAM ${selected.min_vram}-${selected.rec_vram} GB, RAM ${selected.min_ram} GB` }; }); } function updateLabels() { document.getElementById('budgetLabel').textContent = `${fmt(num('budget'))} ₽`; document.getElementById('modelSizeLabel').textContent = `${num('modelSize')}B`; document.getElementById('contextLabel').textContent = `${num('context')}K`; document.getElementById('tokensLabel').textContent = `${num('tokens')} tokens/s`; document.getElementById('batchLabel').textContent = `${num('batch')}`; document.getElementById('powerLimitLabel').textContent = `${num('powerLimit')} Вт`; } function applyUserLevel() { const level = val('userLevel'); const controls = document.getElementById('controls'); controls.classList.toggle('novice', level === 'novice'); } function renderDashboard(build) { const a = build.analysis; const issuesHtml = a.issues.map(i => { return `<div class="issue ${i.severity}">${i.text}</div>`; }).join(''); document.getElementById('dashboard').innerHTML = ` <div class="cards"> <div class="card"> <div class="label">Итоговая цена</div> <div class="value">${fmt(a.total)} ₽</div> </div> <div class="card"> <div class="label">Производительность</div> <div class="value">${a.performance}/100</div> </div> <div class="card"> <div class="label">Цена/производительность</div> <div class="value">${a.pricePerformance}</div> </div> <div class="card"> <div class="label">Энергопотребление</div> <div class="value">${a.estPower} Вт</div> </div> <div class="card"> <div class="label">Шум</div> <div class="value">${a.noiseLabel}</div> </div> <div class="card"> <div class="label">Потенциал апгрейда</div> <div class="value">${a.upgradeScore}/100</div> </div> </div> <div> ${issuesHtml || '<div class="issue info">Замечаний нет.</div>'} </div> `; } function renderBuildTable(build) { const rows = build.items.map(c => { return ` <tr> <td>${TYPE_LABELS[c.type]}</td> <td>${c.name}</td> <td>${fmt(c.price)} ₽</td> <td>${c.store}</td> </tr> `; }).join(''); document.getElementById('buildTable').innerHTML = ` <thead> <tr> <th>Тип</th> <th>Компонент</th> <th>Цена</th> <th>Магазин</th> </tr> </thead> <tbody> ${rows} </tbody> `; } function renderProsCons(build) { const a = build.analysis; document.getElementById('prosCons').innerHTML = ` <div class="columns"> <div> <h3>Сильные стороны</h3> <ul> ${a.strengths.map(s => `<li>${s}</li>`).join('') || '<li>-</li>'} </ul> </div> <div> <h3>Слабые стороны</h3> <ul> ${a.weaknesses.map(s => `<li>${s}</li>`).join('') || '<li>-</li>'} </ul> </div> </div> `; } function renderModels(build, req) { const rows = getModelSupport(build, req).map(item => { return ` <tr> <td>${item.model.name}</td> <td>${CATEGORY_LABELS[item.model.category] || item.model.category}</td> <td>${SUPPORT_LABELS[item.support] || item.support}</td> <td>${item.selected.label}</td> <td>${item.speed}</td> <td>${item.requirements}</td> <td>${item.model.frameworks.join(', ')}</td> <td>${item.model.os.join(', ')}</td> </tr> `; }).join(''); document.getElementById('modelsTable').innerHTML = ` <thead> <tr> <th>Модель</th> <th>Категория</th> <th>Поддержка</th> <th>Режим</th> <th>Ожидаемая скорость</th> <th>Требования</th> <th>Фреймворки</th> <th>ОС</th> </tr> </thead> <tbody> ${rows} </tbody> `; } function renderRecommendations(current, req) { const modes = [ ['cheaper', 'Дешевле'], ['best', 'Лучшая цена/производительность'], ['expensive', 'Дороже и мощнее'] ]; const html = modes.map(([mode, title]) => { const b = selectBuild(req, mode); const delta = b.total - current.total; const deltaPerf = b.analysis.performance - current.analysis.performance; return ` <div class="card"> <h3>${title}</h3> <p> <strong>${fmt(b.total)} ₽</strong> (${delta >= 0 ? '+' : ''}${fmt(delta)} ₽) </p> <p>GPU: ${b.gpu.model}</p> <p>VRAM: ${b.gpu.specs.vram_gb} GB</p> <p> Производительность: ${b.analysis.performance}/100 (${deltaPerf >= 0 ? '+' : ''}${deltaPerf}) </p> <p> ${b.analysis.issues.some(i => i.severity === 'error') ? 'Есть ошибки совместимости' : 'Совместимость без критических ошибок'} </p> </div> `; }).join(''); document.getElementById('recommendations').innerHTML = html; } function render() { updateLabels(); applyUserLevel(); const req = getReq(); const build = selectBuild(req, 'normal'); renderDashboard(build); renderBuildTable(build); renderProsCons(build); renderModels(build, req); renderRecommendations(build, req); } document.querySelectorAll('input, select').forEach(el => { el.addEventListener('input', render); el.addEventListener('change', render); }); document.getElementById('printBtn').addEventListener('click', () => { window.print(); }); render(); </script> </body> </html>