GG / NEXUS.html
Mycc's picture
Update NEXUS.html
95c8925 verified
Raw
History Blame Contribute Delete
121 kB
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.5, user-scalable=yes">
<title>Genshin Nexus · BETA Опти</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
:root {
--bg-main: #0a0c12;
--bg-side: #0f1117;
--bg-card: #1a1e2a;
--bg-card-hover: #252b38;
--accent: #8b5cf6;
--accent-glow: rgba(139, 92, 246, 0.5);
--border: #2d3340;
--text-main: #ffffff;
--text-dim: #a0a8c0;
--text-soft: #d1d5db;
--success: #10b981;
--warning: #f59e0b;
--error: #ef4444;
--pyro: #ff6b4a; --pyro-glow: rgba(255, 107, 74, 0.4);
--hydro: #3b82f6; --hydro-glow: rgba(59, 130, 246, 0.4);
--anemo: #34d399; --anemo-glow: rgba(52, 211, 153, 0.4);
--electro: #a855f7; --electro-glow: rgba(168, 85, 247, 0.4);
--dendro: #84cc16; --dendro-glow: rgba(132, 204, 22, 0.4);
--cryo: #38bdf8; --cryo-glow: rgba(56, 189, 248, 0.4);
--geo: #fbbf24; --geo-glow: rgba(251, 191, 36, 0.4);
--radius-xs: 4px;
--radius-sm: 6px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-xl: 16px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Inter', system-ui, sans-serif;
}
body {
background: radial-gradient(circle at 20% 30%, #121624, var(--bg-main));
color: var(--text-main);
display: flex;
height: 100vh;
overflow: hidden;
}
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: var(--bg-side); }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--accent); }
/* Popup Styles */
.blur-popup {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
backdrop-filter: blur(16px) brightness(0.3);
-webkit-backdrop-filter: blur(16px) brightness(0.3);
background: rgba(0, 0, 0, 0.5);
z-index: 3000;
display: none;
align-items: center;
justify-content: center;
}
.blur-popup.active { display: flex; }
.popup-content {
background: linear-gradient(145deg, var(--bg-card), #151a26);
border: 1px solid rgba(255,255,255,0.1);
border-radius: var(--radius-xl);
padding: 28px;
max-width: 600px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 25px 50px -8px rgba(0,0,0,0.6);
position: relative;
}
.popup-close {
position: absolute;
top: 20px;
right: 24px;
font-size: 28px;
cursor: pointer;
color: var(--text-dim);
transition: 0.2s;
}
.popup-close:hover { color: #fff; }
/* Mobile Responsive Design */
@media (max-width: 1024px) {
aside { width: 80px; }
.side-header h1 { font-size: 0; background: none; -webkit-text-fill-color: #fff; }
.side-header h1::after { content: "N"; font-size: 24px; -webkit-text-fill-color: #fff; }
.side-header p { display: none; }
.nav-item span:last-child { display: none; }
.filter-section label { display: none; }
.filter-section select { display: none; }
.filter-section .btn { display: none; }
.filter-section .btn:last-child {
display: block;
width: 100%;
margin-top: 0;
padding: 12px 0;
}
.filter-section .btn:last-child span { display: none; }
.filter-section .btn:last-child::after { content: "📖"; font-size: 20px; }
.stats-strip { gap: 12px; }
.stat-unit b { font-size: 16px; }
.content-area { padding: 16px; }
.view-toggle-btn { padding: 6px 12px; font-size: 12px; }
.data-table th { font-size: 11px; padding: 8px; }
.data-row td { padding: 12px; }
.weapon-badge { font-size: 11px; padding: 4px 10px; }
.role-badge { font-size: 11px; padding: 4px 10px; }
}
@media (max-width: 768px) {
body { flex-direction: column; }
aside {
width: 100%;
height: auto;
border-right: none;
border-bottom: 1px solid var(--border);
}
.nav-group {
display: flex;
padding: 8px;
overflow-x: auto;
}
.nav-item {
flex-shrink: 0;
margin-bottom: 0;
margin-right: 8px;
}
.filter-section { display: none; }
header {
flex-wrap: wrap;
height: auto;
padding: 16px;
}
.stats-strip {
flex-wrap: wrap;
justify-content: center;
}
.content-area { padding: 16px; }
.chars-grid { grid-template-columns: 1fr; }
.team-members { justify-content: center; }
.synergy-card { padding: 20px; }
.team-member { min-width: 90px; padding: 12px; }
.member-icon { width: 56px; height: 56px; font-size: 24px; }
}
/* Sidebar Styles */
aside {
width: 280px;
background: linear-gradient(145deg, var(--bg-side), #0a0c11);
border-right: 1px solid rgba(255,255,255,0.05);
display: flex;
flex-direction: column;
flex-shrink: 0;
box-shadow: 6px 0 24px rgba(0,0,0,0.4);
transition: 0.3s;
overflow-y: auto;
}
.side-header {
padding: 28px 20px;
border-bottom: 1px solid rgba(255,255,255,0.05);
background: radial-gradient(circle at 0% 50%, rgba(139,92,246,0.15), transparent);
}
.side-header h1 {
font-size: 24px;
font-weight: 800;
letter-spacing: -0.5px;
background: linear-gradient(135deg, #fff, var(--accent));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 4px;
}
.side-header p {
color: var(--text-dim);
font-size: 12px;
}
.nav-group {
padding: 20px 16px;
flex-grow: 1;
overflow-y: auto;
}
.nav-item {
padding: 12px 18px;
border-radius: var(--radius-md);
cursor: pointer;
margin-bottom: 6px;
font-size: 15px;
font-weight: 600;
color: var(--text-dim);
transition: 0.2s;
display: flex;
align-items: center;
gap: 14px;
border: 1px solid transparent;
backdrop-filter: blur(4px);
}
.nav-item:hover {
background: rgba(255,255,255,0.06);
color: #fff;
border-color: rgba(255,255,255,0.1);
transform: translateX(4px);
}
.nav-item.active {
background: linear-gradient(135deg, var(--accent), #a78bfa);
color: #fff;
box-shadow: 0 6px 16px var(--accent-glow);
border: none;
}
/* Filter Section */
.filter-section {
padding: 20px;
border-top: 1px solid rgba(255,255,255,0.05);
background: rgba(0,0,0,0.2);
}
.filter-section label {
display: block;
font-size: 11px;
text-transform: uppercase;
color: var(--text-dim);
margin-bottom: 6px;
letter-spacing: 1px;
font-weight: 700;
}
select {
width: 100%;
background: rgba(0,0,0,0.4);
border: 1px solid rgba(255,255,255,0.1);
color: #fff;
padding: 10px 14px;
border-radius: var(--radius-md);
font-size: 13px;
margin-bottom: 16px;
cursor: pointer;
transition: 0.2s;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23a0a8c0' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 14px center;
background-size: 14px;
}
select:hover { border-color: var(--accent); }
/* Main Content */
main {
flex-grow: 1;
display: flex;
flex-direction: column;
overflow: hidden;
background: radial-gradient(circle at 80% 20%, rgba(139,92,246,0.1), transparent 60%);
}
header {
height: 70px;
background: rgba(15, 17, 23, 0.8);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border-bottom: 1px solid rgba(255,255,255,0.05);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
}
.stats-strip { display: flex; gap: 32px; }
.stat-unit { display: flex; flex-direction: column; }
.stat-unit small {
font-size: 11px;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 1px;
font-weight: 600;
}
.stat-unit b {
font-size: 20px;
font-weight: 800;
background: linear-gradient(135deg, #fff, #d6ccff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.content-area {
padding: 24px;
overflow-y: auto;
flex-grow: 1;
position: relative;
}
/* Table Styles */
.data-table {
width: 100%;
border-collapse: separate;
border-spacing: 0 10px;
}
.data-table th {
text-align: left;
padding: 10px 16px;
font-size: 12px;
color: var(--text-soft);
text-transform: uppercase;
letter-spacing: 1px;
font-weight: 700;
opacity: 0.9;
}
.data-row {
border-radius: var(--radius-lg);
transition: 0.2s;
position: relative;
background: rgba(26, 30, 42, 0.7);
backdrop-filter: blur(8px);
box-shadow: 0 10px 24px -6px rgba(0,0,0,0.5);
border: 1px solid rgba(255,255,255,0.05);
}
.data-row td {
padding: 16px;
vertical-align: middle;
}
.data-row:hover {
background: rgba(37, 43, 56, 0.8);
border-color: rgba(139,92,246,0.3);
}
/* Character Grid */
.chars-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: 20px;
}
.char-card {
background: linear-gradient(145deg, rgba(26,30,42,0.9), rgba(16,18,26,0.9));
backdrop-filter: blur(10px);
border-radius: var(--radius-xl);
padding: 24px;
border: 1px solid rgba(255,255,255,0.08);
display: flex;
flex-direction: column;
gap: 16px;
transition: 0.25s;
box-shadow: 0 18px 28px -8px rgba(0,0,0,0.6);
position: relative;
overflow: hidden;
}
.char-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, var(--element-color), var(--element-glow), transparent);
opacity: 0.9;
}
.char-card:hover {
transform: translateY(-4px);
border-color: var(--element-color);
box-shadow: 0 24px 36px -6px var(--element-glow);
}
.char-card-header {
display: flex;
align-items: center;
gap: 16px;
}
.char-element-icon {
width: 60px;
height: 60px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 30px;
border: 2px solid;
box-shadow: 0 0 25px var(--element-glow);
background: rgba(0,0,0,0.4);
backdrop-filter: blur(4px);
object-fit: cover;
}
.char-name {
font-size: 20px;
font-weight: 800;
margin-bottom: 4px;
display: flex;
align-items: center;
gap: 6px;
}
.char-name a {
color: inherit;
text-decoration: none;
transition: 0.2s;
}
.char-name a:hover {
color: var(--accent);
text-decoration: underline;
}
.char-level {
color: var(--text-dim);
font-size: 12px;
background: rgba(0,0,0,0.4);
padding: 4px 10px;
border-radius: 30px;
display: inline-block;
border: 1px solid rgba(255,255,255,0.1);
}
.char-card-stats {
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(0,0,0,0.3);
padding: 14px 18px;
border-radius: var(--radius-lg);
border: 1px solid rgba(255,255,255,0.05);
}
.weapon-badge {
background: rgba(255,255,255,0.08);
padding: 6px 16px;
border-radius: 40px;
font-size: 12px;
display: inline-flex;
align-items: center;
gap: 6px;
border: 1px solid rgba(255,255,255,0.1);
backdrop-filter: blur(4px);
}
.role-badge {
background: linear-gradient(135deg, rgba(139,92,246,0.25), rgba(167,139,250,0.25));
padding: 4px 14px;
border-radius: 30px;
font-size: 12px;
font-weight: 700;
color: #fff;
border: 1px solid rgba(255,255,255,0.15);
backdrop-filter: blur(4px);
}
.char-card-sets {
font-size: 13px;
color: var(--text-soft);
background: rgba(0,0,0,0.2);
padding: 14px 18px;
border-radius: var(--radius-md);
line-height: 1.5;
border: 1px solid rgba(255,255,255,0.03);
}
/* Artifact Styles */
.artifacts-grid {
display: flex;
flex-direction: column;
gap: 16px;
}
.artifact-card {
background: linear-gradient(145deg, rgba(26,30,42,0.9), rgba(16,18,26,0.9));
backdrop-filter: blur(8px);
border-radius: var(--radius-lg);
padding: 20px;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 20px;
border: 1px solid rgba(255,255,255,0.05);
box-shadow: 0 10px 24px -6px rgba(0,0,0,0.5);
transition: 0.2s;
}
.artifact-card:hover {
border-color: var(--accent);
transform: translateX(6px);
background: rgba(37,43,56,0.9);
}
.artifact-mainstat {
font-weight: 700;
color: #ffd966;
background: rgba(255, 217, 102, 0.12);
padding: 6px 18px;
border-radius: 30px;
border: 1px solid rgba(255, 217, 102, 0.3);
backdrop-filter: blur(4px);
font-size: 13px;
}
/* Button Styles */
.ai-btn {
background: linear-gradient(135deg, #8b5cf6, #c084fc);
border: none;
color: white;
padding: 10px 20px;
border-radius: 30px;
font-size: 13px;
cursor: pointer;
font-weight: 700;
display: flex;
align-items: center;
gap: 8px;
white-space: nowrap;
transition: 0.25s;
box-shadow: 0 6px 16px rgba(139,92,246,0.4);
border: 1px solid rgba(255,255,255,0.2);
}
.ai-btn:hover {
transform: scale(1.02);
box-shadow: 0 10px 22px rgba(139,92,246,0.6);
}
.btn {
background: linear-gradient(135deg, var(--accent), #a78bfa);
color: #fff;
border: none;
padding: 10px 22px;
border-radius: 30px;
font-weight: 700;
cursor: pointer;
font-size: 13px;
transition: 0.2s;
box-shadow: 0 5px 14px var(--accent-glow);
border: 1px solid rgba(255,255,255,0.1);
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px var(--accent-glow);
}
.btn-outline {
background: transparent;
border: 1px solid rgba(255,255,255,0.15);
box-shadow: none;
}
.btn-outline:hover {
background: rgba(255,255,255,0.06);
border-color: var(--accent);
}
/* CV Meter */
.cv-meter {
width: 100px;
height: 6px;
background: rgba(255,255,255,0.1);
border-radius: 3px;
overflow: hidden;
margin-top: 6px;
border: 1px solid rgba(255,255,255,0.05);
}
.cv-fill {
height: 100%;
transition: 0.5s;
border-radius: 3px;
}
/* Synergy Card */
.synergy-card {
background: linear-gradient(145deg, rgba(26,30,42,0.9), rgba(16,18,26,0.9));
backdrop-filter: blur(10px);
border-radius: var(--radius-xl);
padding: 28px;
border: 1px solid rgba(255,255,255,0.06);
margin-bottom: 24px;
position: relative;
overflow: hidden;
box-shadow: 0 18px 32px -8px rgba(0,0,0,0.6);
transition: 0.2s;
}
.synergy-card:hover {
border-color: rgba(255,255,255,0.15);
background: rgba(32,38,54,0.9);
}
.team-members {
display: flex;
gap: 20px;
flex-wrap: wrap;
margin: 20px 0;
}
.team-member {
display: flex;
flex-direction: column;
align-items: center;
background: rgba(0,0,0,0.3);
padding: 16px 14px;
border-radius: var(--radius-lg);
min-width: 100px;
backdrop-filter: blur(8px);
border: 1px solid rgba(255,255,255,0.08);
transition: 0.2s;
}
.team-member:hover {
background: rgba(139,92,246,0.15);
border-color: var(--accent);
transform: translateY(-4px);
}
.member-icon {
width: 64px;
height: 64px;
border-radius: 50%;
background: rgba(0,0,0,0.5);
border: 2px solid;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
margin-bottom: 10px;
box-shadow: 0 0 25px var(--element-glow);
object-fit: cover;
}
/* Loader */
.loader {
width: 22px;
height: 22px;
border: 3px solid rgba(255,255,255,0.3);
border-top-color: #fff;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* Empty State */
.empty {
padding: 40px;
text-align: center;
color: var(--text-dim);
background: rgba(0,0,0,0.2);
border-radius: var(--radius-xl);
backdrop-filter: blur(8px);
border: 1px solid rgba(255,255,255,0.05);
}
/* View Toggle */
.view-panel { display: none; }
.view-panel.active { display: block; }
.view-toggle {
display: flex;
gap: 10px;
margin-left: auto;
}
.view-toggle-btn {
background: rgba(0,0,0,0.3);
border: 1px solid rgba(255,255,255,0.08);
padding: 8px 18px;
border-radius: 30px;
font-size: 13px;
cursor: pointer;
color: var(--text-dim);
transition: 0.2s;
backdrop-filter: blur(4px);
}
.view-toggle-btn.active {
background: linear-gradient(135deg, var(--accent), #a78bfa);
color: #fff;
border-color: transparent;
box-shadow: 0 4px 10px var(--accent-glow);
}
/* Assessment Card */
.assessment-card {
background: linear-gradient(145deg, rgba(26,30,42,0.9), rgba(16,18,26,0.9));
backdrop-filter: blur(10px);
border-radius: var(--radius-xl);
padding: 28px;
margin-bottom: 24px;
border-left: 6px solid;
border: 1px solid rgba(255,255,255,0.06);
box-shadow: 0 14px 28px -8px rgba(0,0,0,0.5);
}
/* AI Overlay */
#ai-overlay {
position: fixed;
right: -520px;
top: 0;
width: 520px;
height: 100vh;
background: rgba(15, 17, 23, 0.98);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
border-left: 1px solid rgba(255,255,255,0.1);
z-index: 2000;
transition: 0.3s cubic-bezier(0.2, 0.9, 0.4, 1);
display: flex;
flex-direction: column;
padding: 32px;
box-shadow: -14px 0 40px rgba(0,0,0,0.8);
}
#ai-overlay.open { right: 0; }
#ai-text {
overflow-y: auto;
padding-right: 8px;
height: calc(100vh - 110px);
}
.toc {
background: rgba(139,92,246,0.1);
border-left: 4px solid var(--accent);
padding: 14px 18px;
border-radius: var(--radius-md);
margin-bottom: 20px;
font-size: 13px;
}
.toc a {
color: var(--text-soft);
text-decoration: none;
margin-right: 14px;
display: inline-block;
padding: 4px 0;
}
.toc a:hover {
color: #fff;
text-decoration: underline;
}
/* Settings Section */
.settings-section {
background: rgba(0,0,0,0.2);
border-radius: var(--radius-xl);
padding: 24px;
margin-bottom: 24px;
border: 1px solid rgba(255,255,255,0.05);
backdrop-filter: blur(8px);
}
.settings-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 18px;
padding-bottom: 18px;
border-bottom: 1px solid rgba(255,255,255,0.05);
}
.settings-row:last-child { border-bottom: none; }
.settings-label {
font-size: 14px;
font-weight: 600;
color: var(--text-soft);
}
.settings-description {
font-size: 12px;
color: var(--text-dim);
margin-top: 4px;
}
.provider-tabs {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 20px;
}
.provider-tab {
padding: 8px 16px;
background: rgba(255,255,255,0.05);
border-radius: 30px;
font-size: 13px;
font-weight: 600;
color: var(--text-dim);
cursor: pointer;
border: 1px solid rgba(255,255,255,0.08);
transition: 0.2s;
}
.provider-tab.active {
background: linear-gradient(135deg, var(--accent), #a78bfa);
color: #fff;
border-color: transparent;
}
/* Profile Badge */
.profile-badge {
background: rgba(139,92,246,0.2);
border: 1px solid rgba(139,92,246,0.3);
border-radius: 30px;
padding: 8px 18px;
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
}
/* Teams Grid */
.teams-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
margin-top: 20px;
}
.team-card {
background: rgba(26,30,42,0.7);
backdrop-filter: blur(8px);
border-radius: var(--radius-lg);
padding: 20px;
border: 1px solid rgba(255,255,255,0.05);
}
.team-card-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 14px;
}
.team-card-icons {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.team-card-icon {
width: 44px;
height: 44px;
border-radius: 50%;
background: rgba(0,0,0,0.3);
border: 2px solid;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
object-fit: cover;
}
/* Tooltip Icon */
.tooltip-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
border-radius: 50%;
background: rgba(255,255,255,0.1);
color: var(--text-dim);
font-size: 12px;
cursor: pointer;
margin-left: 6px;
border: 1px solid rgba(255,255,255,0.2);
transition: 0.2s;
}
.tooltip-icon:hover {
background: var(--accent);
color: #fff;
}
/* Copyright Notice */
.copyright-notice {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid rgba(255,255,255,0.1);
font-size: 12px;
color: var(--text-dim);
text-align: center;
}
.copyright-notice a {
color: var(--accent);
text-decoration: none;
}
.copyright-notice a:hover {
text-decoration: underline;
}
/* Resonance Gradient */
.resonance-gradient {
background: linear-gradient(90deg, var(--hydro), var(--pyro), var(--electro));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: bold;
}
</style>
</head>
<body>
<!-- Legend Popup -->
<div id="legendPopup" class="blur-popup">
<div class="popup-content">
<span class="popup-close" onclick="closeLegend()">&times;</span>
<h2 style="font-size: 28px; margin-bottom: 20px; background: linear-gradient(135deg, #fff, var(--accent)); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">📘 Легенда</h2>
<h3 style="margin-bottom: 16px; font-size: 18px;">🎨 Цвета элементов</h3>
<div class="legend-grid" style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px;">
<div style="display: flex; align-items: center; gap: 10px;"><span style="width: 16px; height: 16px; background: var(--pyro); border-radius: 4px;"></span> Пиро 🔥</div>
<div style="display: flex; align-items: center; gap: 10px;"><span style="width: 16px; height: 16px; background: var(--hydro); border-radius: 4px;"></span> Гидро 💧</div>
<div style="display: flex; align-items: center; gap: 10px;"><span style="width: 16px; height: 16px; background: var(--anemo); border-radius: 4px;"></span> Анемо 🍃</div>
<div style="display: flex; align-items: center; gap: 10px;"><span style="width: 16px; height: 16px; background: var(--electro); border-radius: 4px;"></span> Электро ⚡</div>
<div style="display: flex; align-items: center; gap: 10px;"><span style="width: 16px; height: 16px; background: var(--dendro); border-radius: 4px;"></span> Дендро 🌿</div>
<div style="display: flex; align-items: center; gap: 10px;"><span style="width: 16px; height: 16px; background: var(--cryo); border-radius: 4px;"></span> Крио ❄️</div>
<div style="display: flex; align-items: center; gap: 10px;"><span style="width: 16px; height: 16px; background: var(--geo); border-radius: 4px;"></span> Гео 🪨</div>
</div>
<h3 style="margin: 28px 0 16px; font-size: 18px;">📏 Обозначения статов</h3>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 14px;">
<div><strong>Сила атаки</strong> — ATK</div>
<div><strong>Сила атаки%</strong> — ATK%</div>
<div><strong>Защита</strong> — DEF</div>
<div><strong>Защита%</strong> — DEF%</div>
<div><strong>HP</strong> — HP</div>
<div><strong>HP%</strong> — HP%</div>
<div><strong>Мастерство стихий</strong> — EM</div>
<div><strong>Восстановление энергии</strong> — ER</div>
<div><strong>Крит. шанс%</strong> — CR</div>
<div><strong>Крит. урон%</strong> — CD</div>
</div>
<button class="btn" style="margin-top: 28px; width: 100%;" onclick="closeLegend()">Закрыть</button>
</div>
</div>
<!-- CV Popup -->
<div id="cvPopup" class="blur-popup">
<div class="popup-content">
<span class="popup-close" onclick="closeCVPopup()">&times;</span>
<h2 style="font-size: 28px; margin-bottom: 20px; color: var(--accent);">⚔️ Крит-ценность (CV)</h2>
<p style="font-size: 16px; line-height: 1.6; margin-bottom: 20px; color: var(--text-soft);">
<strong>CV = CRIT Rate × 2 + CRIT DMG</strong>
</p>
<p style="margin-bottom: 20px; color: var(--text-dim);">Метрика для оценки качества артефактов. Чем выше, тем лучше.</p>
<div style="background: rgba(0,0,0,0.3); padding: 20px; border-radius: var(--radius-lg);">
<div style="display: flex; align-items: center; gap: 16px; margin-bottom: 12px;">
<div style="width: 20px; height: 20px; background: var(--success); border-radius: 6px;"></div>
<span><strong>Высокий (>200)</strong> — отличный билд</span>
</div>
<div style="display: flex; align-items: center; gap: 16px; margin-bottom: 12px;">
<div style="width: 20px; height: 20px; background: var(--warning); border-radius: 6px;"></div>
<span><strong>Средний (140–200)</strong> — хороший билд</span>
</div>
<div style="display: flex; align-items: center; gap: 16px;">
<div style="width: 20px; height: 20px; background: var(--error); border-radius: 6px;"></div>
<span><strong>Низкий (<140)</strong> — есть куда расти</span>
</div>
</div>
<button class="btn" style="margin-top: 28px; width: 100%;" onclick="closeCVPopup()">Закрыть</button>
</div>
</div>
<!-- Sidebar -->
<aside>
<div class="side-header">
<h1>NEXUS BETA</h1>
<p>анализатор Genshin (Разработка)</p>
</div>
<div class="nav-group">
<div class="nav-item active" data-tab="chars" onclick="switchTab('chars')">👥 <span>Персонажи</span></div>
<div class="nav-item" data-tab="artifacts" onclick="switchTab('artifacts')">💎 <span>Артефакты</span></div>
<div class="nav-item" data-tab="team" onclick="switchTab('team')">⚔️ <span>Отряды</span></div>
<div class="nav-item" data-tab="assessment" onclick="switchTab('assessment')">📊 <span>Оценка</span></div>
<div class="nav-item" data-tab="myteams" onclick="switchTab('myteams')">📁 <span>Мои отряды</span></div>
<div class="nav-item" data-tab="profiles" onclick="switchTab('profiles')">👤 <span>Профили</span></div>
<div class="nav-item" data-tab="settings" onclick="switchTab('settings')">⚙️ <span>Настройки</span></div>
</div>
<div class="filter-section">
<label>Фильтр по стихии</label>
<select id="f-el" onchange="applyFilters()">
<option value="all">Все элементы</option>
<option value="pyro">Пиро</option>
<option value="hydro">Гидро</option>
<option value="anemo">Анемо</option>
<option value="electro">Электро</option>
<option value="dendro">Дендро</option>
<option value="cryo">Крио</option>
<option value="geo">Гео</option>
</select>
<label>Сортировка</label>
<select id="f-sort" onchange="applyFilters()">
<option value="cv">Крит‑ценность (CV)</option>
<option value="lvl">Уровень</option>
<option value="name">Имя</option>
</select>
<button class="btn" style="width:100%; background: linear-gradient(135deg, #ef4444, #b91c1c);" onclick="clearDB()">🗑️ <span>Удалить данные</span></button>
<button class="btn" style="width:100%; margin-top: 14px; background: linear-gradient(135deg, #4b5563, #1f2937);" onclick="openLegend()">📖 <span>Легенда</span></button>
</div>
</aside>
<!-- Main Content -->
<main>
<header>
<div class="stats-strip">
<div class="stat-unit"><small>Персонажи</small><b id="total-c">0</b></div>
<div class="stat-unit"><small>Артефакты</small><b id="total-a">0</b></div>
<div class="stat-unit"><small>Средний CV</small><b id="avg-cv">0</b></div>
</div>
<div style="display: flex; gap: 12px;">
<label class="btn">
📂 <span>Загрузить GOOD</span> <div class="settings-description">ПО для Сбора базы > <a href="https://github.com/konkers/irminsul" target="_blank">irminsul(GitHub)</a></div>
<input type="file" id="fileIn" accept=".json" style="display:none;">
</label>
<button class="btn btn-outline" onclick="showProfileSelector()" style="display:flex; align-items:center; gap:6px;">
👤 <span>Профили</span>
</button>
</div>
</header>
<div class="content-area">
<!-- Characters View -->
<div id="view-chars" class="view-panel active">
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:24px; flex-wrap:wrap; gap:16px;">
<h2 style="font-size:26px; font-weight:800; background: linear-gradient(135deg, #fff, var(--accent)); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">👥 Персонажи</h2>
<div class="view-toggle">
<span class="view-toggle-btn active" data-view="table" onclick="setCharsView('table')">📋 Таблица</span>
<span class="view-toggle-btn" data-view="cards" onclick="setCharsView('cards')">🃏 Карточки</span>
</div>
</div>
<div id="chars-table-container" style="display:block; overflow-x: auto;">
<table class="data-table">
<thead>
<tr>
<th>Персонаж</th>
<th>Оружие / Роль</th>
<th>Экипированные сеты</th>
<th>Крит‑ценность (CV) <span class="tooltip-icon" onclick="openCVPopup()">?</span></th>
<th>✨ AI‑анализ</th>
</tr>
</thead>
<tbody id="char-body"></tbody>
</table>
</div>
<div id="chars-cards-container" style="display:none;" class="chars-grid"></div>
<div id="empty-msg" class="empty" style="display:none;">
📁 Нет данных. Загрузите GOOD‑файл.
</div>
</div>
<!-- Artifacts View -->
<div id="view-artifacts" class="view-panel">
<div style="display:flex; gap:16px; margin-bottom:24px; align-items:center; flex-wrap:wrap;">
<h2 style="font-size:26px; font-weight:800; background: linear-gradient(135deg, #fff, #ffd966); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">💎 Артефакты</h2>
<select id="art-set-filter" onchange="renderArtifacts()" style="width:200px;">
<option value="all">Все сеты</option>
</select>
<select id="art-set-group" onchange="groupArtifacts()" style="width:180px;">
<option value="all">Все группы</option>
<option value="set">По сетам</option>
<option value="slot">По слотам</option>
<option value="rarity">По редкости</option>
<option value="level">По уровню</option>
<option value="cv">По CV</option>
</select>
<select id="art-slot-filter" onchange="renderArtifacts()" style="width:180px;">
<option value="all">Все слоты</option>
<option value="flower">Цветок жизни</option>
<option value="plume">Перо смерти</option>
<option value="sands">Пески времени</option>
<option value="goblet">Кубок пространства</option>
<option value="circlet">Корона разума</option>
</select>
<div class="view-toggle" style="margin-left:auto;">
<span class="view-toggle-btn active" data-view="list" onclick="setArtifactsView('list')">📋 Список</span>
<span class="view-toggle-btn" data-view="grid" onclick="setArtifactsView('grid')">🃏 Карточки</span>
</div>
</div>
<div id="artifacts-list" class="artifacts-grid"></div>
</div>
<!-- Teams View -->
<div id="view-team" class="view-panel">
<div style="display:flex; align-items:center; gap:20px; margin-bottom:24px; flex-wrap:wrap;">
<h2 style="font-size:26px; font-weight:800; background: linear-gradient(135deg, #fff, #fbbf24); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">⚔️ Отряды</h2>
<button class="btn" onclick="aiTeamBuilder()" style="background: linear-gradient(135deg, #8b5cf6, #c084fc); padding: 12px 24px;">
🧠 AI‑подбор
</button>
<label style="display:flex; align-items:center; gap:8px; background:rgba(0,0,0,0.3); padding:8px 20px; border-radius:40px; border:1px solid rgba(255,255,255,0.1);">
<input type="checkbox" id="googleSearchCheckbox" style="width:18px; height:18px; accent-color:var(--accent);">
🔍 Google поиск (Gemini)
</label>
</div>
<div id="team-suggestions" style="display:flex; flex-direction:column; gap:20px;"></div>
<div id="ai-team-result" style="margin-top:24px;"></div>
</div>
<!-- Assessment View -->
<div id="view-assessment" class="view-panel">
<h2 style="font-size:26px; font-weight:800; margin-bottom:24px; background: linear-gradient(135deg, #fff, #10b981); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">📊 Оценка аккаунта</h2>
<div id="assessment-content">Загрузите GOOD‑файл для точной оценки.</div>
</div>
<!-- My Teams View -->
<div id="view-myteams" class="view-panel">
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:24px; flex-wrap:wrap; gap:16px;">
<h2 style="font-size:26px; font-weight:800; background: linear-gradient(135deg, #fff, #f59e0b); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">📁 Мои отряды</h2>
<div style="display:flex; gap:12px;">
<button class="btn" onclick="generateRandomTeam()" style="background: linear-gradient(135deg, #10b981, #059669);">
🎲 Рандомный
</button>
<button class="btn" onclick="saveCurrentTeam()" style="background: linear-gradient(135deg, #8b5cf6, #a78bfa);">
💾 Сохранить
</button>
</div>
</div>
<div id="myteams-content">
<div id="myteams-grid" class="teams-grid"></div>
</div>
</div>
<!-- Profiles View -->
<div id="view-profiles" class="view-panel">
<h2 style="font-size:26px; font-weight:800; margin-bottom:24px; background: linear-gradient(135deg, #fff, #94a3b8); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">👤 Профили GOOD</h2>
<div id="profiles-content">
<div style="display:flex; gap:16px; margin-bottom:24px; flex-wrap:wrap;">
<button class="btn" onclick="createNewProfile()" style="background: linear-gradient(135deg, #8b5cf6, #a78bfa);">
➕ Новый профиль
</button>
<button class="btn btn-outline" onclick="loadProfile(activeProfile)" style="display:flex; align-items:center; gap:6px;">
🔄 Загрузить текущий
</button>
</div>
<div id="profiles-list" style="display:flex; flex-direction:column; gap:14px;"></div>
</div>
</div>
<!-- Settings View -->
<div id="view-settings" class="view-panel">
<h2 style="font-size:26px; font-weight:800; margin-bottom:24px; background: linear-gradient(135deg, #fff, #94a3b8); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">⚙️ Настройки</h2>
<div class="settings-section">
<h3 style="font-size:20px; margin-bottom:20px;">🤖 AI‑провайдер</h3>
<div class="provider-tabs" id="providerTabs">
<div class="provider-tab active" data-provider="huggingface" onclick="setAIProvider('huggingface')">🤗 Hugging Face</div>
<div class="provider-tab" data-provider="openai" onclick="setAIProvider('openai')">⚡ OpenAI</div>
<div class="provider-tab" data-provider="gemini" onclick="setAIProvider('gemini')">🌀 Gemini</div>
<div class="provider-tab" data-provider="deepseek" onclick="setAIProvider('deepseek')">🎯 DeepSeek</div>
<div class="provider-tab" data-provider="custom" onclick="setAIProvider('custom')">🔧 Custom</div>
</div>
<div id="provider-settings">
<!-- Hugging Face -->
<div id="settings-huggingface" style="display:block;">
<div class="settings-row">
<div>
<div class="settings-label">🔑 Hugging Face токен</div>
<div class="settings-description">Получите на <a href="https://huggingface.co/settings/tokens/new?ownUserPermissions=inference.serverless.write&tokenType=fineGrained" target="_blank">huggingface.co/settings/tokens</a></div>
</div>
<input type="password" id="hf-api-key" placeholder="hf_..." value="" style="width:240px; background:rgba(0,0,0,0.3); border:1px solid rgba(255,255,255,0.1); padding:10px 18px; border-radius:30px; color:#fff;">
</div>
<div class="settings-row">
<div>
<div class="settings-label">🧠 Модель Hugging Face</div>
<div class="settings-description">Выберите модель для инференса</div>
</div>
<select id="hf-model" style="width:240px;">
<option value="Qwen/Qwen3-32B">Qwen/Qwen3-32B</option>
<option value="Qwen/Qwen3-4B-Instruct-2507">Qwen3-4B-Instruct</option>
<option value="deepseek-ai/DeepSeek-R1:fastest" selected>DeepSeek-R1:fastest</option>
<option value="deepseek-ai/DeepSeek-R1-0528">DeepSeek-R1-0528</option>
<option value="zai-org/GLM-4.7-Flash">GLM-4.7-Flash</option>
</select>
</div>
</div>
<!-- OpenAI -->
<div id="settings-openai" style="display:none;">
<div class="settings-row">
<div>
<div class="settings-label">🔑 OpenAI API ключ</div>
<div class="settings-description">sk-... с platform.openai.com</div>
</div>
<input type="password" id="openai-api-key" placeholder="sk-..." value="" style="width:240px; background:rgba(0,0,0,0.3); border:1px solid rgba(255,255,255,0.1); padding:10px 18px; border-radius:30px; color:#fff;">
</div>
<div class="settings-row">
<div>
<div class="settings-label">🧠 Модель OpenAI</div>
</div>
<select id="openai-model" style="width:240px;">
<option value="gpt-5-mini-2025-08-07">gpt-5-mini-2025-08-07</option>
<option value="gpt-5-nano-2025-08-07">gpt-5-nano-2025-08-07</option>
<option value="gpt-oss-20b">gpt-oss-20b</option>
</select>
</div>
</div>
<!-- Gemini -->
<div id="settings-gemini" style="display:none;">
<div class="settings-row">
<div>
<div class="settings-label">🔑 Gemini API ключ</div>
<div class="settings-description">AIza... с <a href="https://aistudio.google.com/u/0/api-keys" target="_blank">aistudio.google.com</a></div>
</div>
<input type="password" id="gemini-api-key" placeholder="AIza..." value="" style="width:240px; background:rgba(0,0,0,0.3); border:1px solid rgba(255,255,255,0.1); padding:10px 18px; border-radius:30px; color:#fff;">
</div>
<div class="settings-row">
<div>
<div class="settings-label">🧠 Модель Gemini</div>
</div>
<select id="gemini-model" style="width:240px;">
<option value="gemini-2.5-pro">Gemini 2.5 Pro</option>
<option value="gemini-2.5-flash-lite-preview-09-2025">Gemini 2.0 Flash</option>
<option value="gemini-3-flash-preview:search">Gemini 3 Flash (search)</option>
</select>
</div>
</div>
<!-- DeepSeek -->
<div id="settings-deepseek" style="display:none;">
<div class="settings-row">
<div>
<div class="settings-label">🔑 DeepSeek API ключ</div>
<div class="settings-description">Получите на <a href="https://platform.deepseek.com/api_keys" target="_blank">platform.deepseek.com/api_keys</a></div>
</div>
<input type="password" id="deepseek-api-key" placeholder="sk-..." value="" style="width:240px; background:rgba(0,0,0,0.3); border:1px solid rgba(255,255,255,0.1); padding:10px 18px; border-radius:30px; color:#fff;">
</div>
<div class="settings-row">
<div>
<div class="settings-label">🧠 Модель DeepSeek</div>
</div>
<select id="deepseek-model" style="width:240px;">
<option value="deepseek-chat">DeepSeek Chat</option>
<option value="deepseek-coder">DeepSeek Coder</option>
<option value="deepseek-reasoner">DeepSeek Reasoner</option>
</select>
</div>
</div>
<!-- Custom -->
<div id="settings-custom" style="display:none;">
<div class="settings-row">
<div>
<div class="settings-label">🔑 Custom API ключ</div>
</div>
<input type="password" id="custom-api-key" placeholder="ключ" value="" style="width:240px; background:rgba(0,0,0,0.3); border:1px solid rgba(255,255,255,0.1); padding:10px 18px; border-radius:30px; color:#fff;">
</div>
<div class="settings-row">
<div>
<div class="settings-label">🌐 Custom Base URL</div>
</div>
<input type="text" id="custom-base-url" placeholder="https://..." value="https://router.huggingface.co/v1" style="width:240px; background:rgba(0,0,0,0.3); border:1px solid rgba(255,255,255,0.1); padding:10px 18px; border-radius:30px; color:#fff;">
</div>
<div class="settings-row">
<div>
<div class="settings-label">🧠 Custom Model</div>
</div>
<input type="text" id="custom-model" placeholder="model-name" value="" style="width:240px; background:rgba(0,0,0,0.3); border:1px solid rgba(255,255,255,0.1); padding:10px 18px; border-radius:30px; color:#fff;">
</div>
</div>
</div>
<div style="display:flex; gap:16px; margin-top:20px;">
<button class="btn" onclick="saveAllApiKeys()">💾 Сохранить все ключи</button>
<button class="btn btn-outline" onclick="testCurrentApi()">🔍 Проверить текущий</button>
</div>
<div id="api-test-result" style="margin-top:16px; font-size:13px;"></div>
</div>
<div class="settings-section">
<h3 style="font-size:20px; margin-bottom:20px;">🎨 Интерфейс</h3>
<div class="settings-row">
<div>
<div class="settings-label">🌓 Тема оформления</div>
<div class="settings-description">Стиль отображения</div>
</div>
<select id="theme-selector" onchange="changeTheme(this.value)" style="width:160px;">
<option value="dark">Тёмная</option>
<option value="darker">Угольно-чёрная</option>
<option value="light">Светлая</option>
</select>
</div>
<div class="settings-row">
<div>
<div class="settings-label">🗣️ Язык интерфейса</div>
<div class="settings-description">Только русский</div>
</div>
<select disabled style="width:160px; opacity:0.7;">
<option selected>Русский</option>
</select>
</div>
</div>
<div class="settings-section">
<h3 style="font-size:20px; margin-bottom:20px;">💾 Данные</h3>
<div class="settings-row">
<div>
<div class="settings-label">🗄️ Управление БД</div>
<div class="settings-description">Сброс или экспорт</div>
</div>
<div style="display:flex; gap:12px;">
<button class="btn" style="background:linear-gradient(135deg, #ef4444, #b91c1c);" onclick="clearDB()">♻️ Сбросить</button>
<button class="btn" onclick="exportGOOD()">📤 Экспорт</button>
</div>
</div>
<div class="copyright-notice">
<p>© 2026 Genshin Nexus - фанатский проект. Не связан с miHoYo/Hoyoverse.</p>
<p>Разработано <a href="https://github.com/assBuk" target="_blank">Mycc</a></p>
</div>
</div>
</div>
</div>
</main>
<!-- AI Overlay -->
<div id="ai-overlay">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
<h3 style="font-size:24px; background: linear-gradient(135deg, #fff, var(--accent)); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">✨ AI‑анализ</h3>
<button style="background:none; border:none; color:#fff; font-size:32px; cursor:pointer;" onclick="closeAI()">×</button>
</div>
<div id="ai-toc" class="toc" style="display: none;"></div>
<div id="ai-text" style="font-size:15px; line-height:1.6; color:var(--text-soft); overflow-y: auto; padding-right: 6px;">
💡 Нажмите «✨ Анализ» у персонажа.
</div>
<div id="ai-loader" class="loader" style="margin: 40px auto; display: none;"></div>
</div>
<script>
// ==================== RUSSIAN TRANSLATION ====================
const artifactSetNames = {
'GladiatorsFinale': 'Конец гладиатора',
'WanderersTroupe': 'Странствующий ансамбль',
'NoblesseOblige': 'Церемония древней знати',
'ArchaicPetra': 'Архаичный камень',
'Thundersoother': 'Усмиряющий гром',
'ThunderingFury': 'Громогласный рёв ярости',
'EmblemOfSeveredFate': 'Эмблема рассечённой судьбы',
'PaleFlame': 'Бледный огонь',
'BloodstainedChivalry': 'Рыцарство крови',
'ShimenawasReminiscence': 'Воспоминания Симэнавы',
'HeartOfDepth': 'Сердце глубин',
'ViridescentVenerer': 'Изумрудная тень',
'HuskOfOpulentDreams': 'Кокон сладких грёз',
'OceanHuedClam': 'Моллюск морских красок',
'VermillionHereafter': 'Киноварное загробье',
'EchoesOfAnOffering': 'Отголоски подношения',
'DeepwoodMemories': 'Воспоминания дремучего леса',
'GildedDreams': 'Позолоченные сны',
'DesertPavilionChronicle': 'Хроники Чертогов в пустыне',
'FlowerOfParadiseLost': 'Цветок потерянного рая',
'VourukashasGlow': 'Сияние Вурукаши',
'MarechausseeHunter': 'Охотник Сумеречного двора',
'GoldenTroupe': 'Золотая труппа',
'ObsidianCodex': 'Кодекс Обсидиана',
'ScrollOfTheHeroOfCinderCity': 'Свиток героя Пепельного города',
'SongOfDaysPast': 'Песнь былых дней',
'NighttimeWhispersInTheEchoingWoods': 'Ночной шёпот в Эхо-лесу',
'FragmentOfHarmonicWhimsy': 'Фрагмент гармонической прихоти',
'UnfinishedReverie': 'Неоконченные грёзы',
'FinaleOfTheDeepGalleries': 'Финал глубоких галерей',
'LongNightsOath': 'Клятва долгой ночи',
'NightOfTheSkysUnveiling': 'Ночь раскрытого неба',
'SilkenMoonsSerenade': 'Шёлковая серенада луны',
'ADayCarvedFromRisingWinds': 'День, высеченный из восходящих ветров',
'AubadeOfMorningstarAndMoon': 'Утренняя серенада звезды и луны',
'TenacityOfTheMillelith': 'Стойкость Миллелита',
'CrimsonWitchOfFlames': 'Горящая алая ведьма',
'Lavawalker': 'Ступающий по лаве',
'BlizzardStrayer': 'Заблудший в метели',
'MaidenBeloved': 'Возлюбленная юная дева'
};
const slotNames = {
'flower': '🌺 Цветок жизни',
'plume': '🪶 Перо смерти',
'sands': '⏳ Пески времени',
'goblet': '🏺 Кубок пространства',
'circlet': '👑 Корона разума'
};
const statNames = {
'hp': 'HP',
'hp_': 'HP%',
'atk': '⚔️ Сила атаки',
'atk_': '⚔️ Сила атаки%',
'def': '🛡️ Защита',
'def_': '🛡️ Защита%',
'eleMas': '✨ Мастерство стихий',
'enerRech_': '🔋 Восстановление энергии',
'critRate_': '🎯 Крит. шанс%',
'critDMG_': '💥 Крит. урон%',
'heal_': '💚 Бонус лечения',
'physical_dmg_': '🏹 Физ. урон%',
'pyro_dmg_': '🔥 Пиро урон%',
'hydro_dmg_': '💧 Гидро урон%',
'anemo_dmg_': '🍃 Анемо урон%',
'electro_dmg_': '⚡ Электро урон%',
'dendro_dmg_': '🌿 Дендро урон%',
'cryo_dmg_': '❄️ Крио урон%',
'geo_dmg_': '🪨 Гео урон%'
};
// ==================== LOCAL CHARACTER DATA ====================
const LOCAL_CHARACTERS = [
{ name: "Альбедо", icon: "pic/albedo.png", element: "Geo" },
{ name: "Аль-Хайтам", icon: "pic/alhaitham.png", element: "Dendro" },
{ name: "Элой", icon: "pic/aloy.png", element: "Cryo" },
{ name: "Эмбер", icon: "pic/amber.png", element: "Pyro" },
{ name: "Аратаки Итто", icon: "pic/arataki_itto.png", element: "Geo" },
{ name: "Арлекино", icon: "pic/arlecchino.png", element: "Pyro" },
{ name: "Бай Чжу", icon: "pic/baizhu.png", element: "Dendro" },
{ name: "Барбара", icon: "pic/barbara.png", element: "Hydro" },
{ name: "Бэй Доу", icon: "pic/beidou.png", element: "Electro" },
{ name: "Беннет", icon: "pic/bennett.png", element: "Pyro" },
{ name: "Кандакия", icon: "pic/candace.png", element: "Hydro" },
{ name: "Шарлотта", icon: "pic/charlotte.png", element: "Cryo" },
{ name: "Часка", icon: "pic/chasca.png", element: "Anemo" },
{ name: "Тиори", icon: "pic/chiori.png", element: "Geo" },
{ name: "Чун Юнь", icon: "pic/chongyun.png", element: "Cryo" },
{ name: "Ситлали", icon: "pic/citlali.png", element: "Cryo" },
{ name: "Клоринда", icon: "pic/clorinde.png", element: "Electro" },
{ name: "Коллеи", icon: "pic/collei.png", element: "Dendro" },
{ name: "Сайно", icon: "pic/cyno.png", element: "Electro" },
{ name: "Дэхья", icon: "pic/dehya.png", element: "Pyro" },
{ name: "Дилюк", icon: "pic/diluc.png", element: "Pyro" },
{ name: "Диона", icon: "pic/diona.png", element: "Cryo" },
{ name: "Дори", icon: "pic/dori.png", element: "Electro" },
{ name: "Эмилия", icon: "pic/emilie.png", element: "Dendro" },
{ name: "Эскофье", icon: "pic/escoffier.png", element: "Cryo" },
{ name: "Эола", icon: "pic/eula.png", element: "Cryo" },
{ name: "Фарузан", icon: "pic/faruzan.png", element: "Anemo" },
{ name: "Фишль", icon: "pic/fischl.png", element: "Electro" },
{ name: "Фремине", icon: "pic/freminet.png", element: "Cryo" },
{ name: "Фурина", icon: "pic/furina.png", element: "Hydro" },
{ name: "Га Мин", icon: "pic/gaming.png", element: "Pyro" },
{ name: "Гань Юй", icon: "pic/ganyu.png", element: "Cryo" },
{ name: "Горо", icon: "pic/gorou.png", element: "Geo" },
{ name: "Ху Тао", icon: "pic/hu_tao.png", element: "Pyro" },
{ name: "Ифу", icon: "pic/ifa.png", element: "Anemo" },
{ name: "Иансан", icon: "pic/iansan.png", element: "Electro" },
{ name: "Джинн", icon: "pic/jean.png", element: "Anemo" },
{ name: "Кадзуха", icon: "pic/kaedehara_kazuha.png", element: "Anemo" },
{ name: "Кэйа", icon: "pic/kaeya.png", element: "Cryo" },
{ name: "Качина", icon: "pic/Kachina.webp", element: "Geo" },
{ name: "Аяка", icon: "pic/kamisato_ayaka.png", element: "Cryo" },
{ name: "Аято", icon: "pic/kamisato_ayato.png", element: "Hydro" },
{ name: "Кавэх", icon: "pic/kaveh.png", element: "Dendro" },
{ name: "Кэ Цин", icon: "pic/keqing.png", element: "Electro" },
{ name: "Кинич", icon: "pic/kinich.png", element: "Dendro" },
{ name: "Кирара", icon: "pic/kirara.png", element: "Dendro" },
{ name: "Кли", icon: "pic/klee.png", element: "Pyro" },
{ name: "Сара", icon: "pic/kujou_sara.png", element: "Electro" },
{ name: "Шинобу", icon: "pic/kuki_shinobu.png", element: "Electro" },
{ name: "Лань Янь", icon: "pic/lan_yan.png", element: "Anemo" },
{ name: "Лейла", icon: "pic/layla.png", element: "Cryo" },
{ name: "Лиза", icon: "pic/lisa.png", element: "Electro" },
{ name: "Линнет", icon: "pic/lynette.png", element: "Anemo" },
{ name: "Лини", icon: "pic/lyney.png", element: "Pyro" },
{ name: "Мавуика", icon: "pic/mavuika.png", element: "Pyro" },
{ name: "Мика", icon: "pic/mika.png", element: "Cryo" },
{ name: "Мона", icon: "pic/mona.png", element: "Hydro" },
{ name: "Муалани", icon: "pic/mualani.png", element: "Hydro" },
{ name: "Нахида", icon: "pic/nahida.png", element: "Dendro" },
{ name: "Навия", icon: "pic/navia.png", element: "Geo" },
{ name: "Нёвиллет", icon: "pic/neuvillette.png", element: "Hydro" },
{ name: "Нилу", icon: "pic/nilou.png", element: "Hydro" },
{ name: "Нин Гуан", icon: "pic/ningguang.png", element: "Geo" },
{ name: "Ноэлль", icon: "pic/noelle.png", element: "Geo" },
{ name: "Оророн", icon: "pic/ororon.png", element: "Electro" },
{ name: "Ци Ци", icon: "pic/qiqi.png", element: "Cryo" },
{ name: "Райдэн", icon: "pic/raiden_shogun.png", element: "Electro" },
{ name: "Рэйзор", icon: "pic/razor.png", element: "Electro" },
{ name: "Розалья", icon: "pic/rosaria.png", element: "Cryo" },
{ name: "Кокоми", icon: "pic/sangonomiya_kokomi.png", element: "Hydro" },
{ name: "Саю", icon: "pic/sayu.png", element: "Anemo" },
{ name: "Сетос", icon: "pic/sethos.png", element: "Electro" },
{ name: "Шэнь Хэ", icon: "pic/shenhe.png", element: "Cryo" },
{ name: "Хэйдзо", icon: "pic/shikanoin_heizou.png", element: "Anemo" },
{ name: "Сиджвин", icon: "pic/sigewinne.png", element: "Hydro" },
{ name: "Сахароза", icon: "pic/sucrose.png", element: "Anemo" },
{ name: "Тарталья", icon: "pic/tartaglia.png", element: "Hydro" },
{ name: "Тома", icon: "pic/thoma.png", element: "Pyro" },
{ name: "Тигнари", icon: "pic/tighnari.png", element: "Dendro" },
{ name: "Вареса", icon: "pic/varesa.png", element: "Electro" },
{ name: "Венти", icon: "pic/venti.png", element: "Anemo" },
{ name: "Странник", icon: "pic/wanderer.png", element: "Anemo" },
{ name: "Ризли", icon: "pic/wriothesley.png", element: "Cryo" },
{ name: "Сяо", icon: "pic/xiao.png", element: "Anemo" },
{ name: "Сян Линь", icon: "pic/xiangling.png", element: "Pyro" },
{ name: "Сянь Юнь", icon: "pic/xianyun.png", element: "Anemo" },
{ name: "Шилонен", icon: "pic/xilonen.png", element: "Geo" },
{ name: "Син Цю", icon: "pic/xingqiu.png", element: "Hydro" },
{ name: "Синь Янь", icon: "pic/xinyan.png", element: "Pyro" },
{ name: "Яэ Мико", icon: "pic/yae_miko.png", element: "Electro" },
{ name: "Янь Фэй", icon: "pic/yanfei.png", element: "Pyro" },
{ name: "Яо Яо", icon: "pic/yaoyao.png", element: "Dendro" },
{ name: "Е Лань", icon: "pic/yelan.png", element: "Hydro" },
{ name: "Ёимия", icon: "pic/yoimiya.png", element: "Pyro" },
{ name: "Юнь Цзинь", icon: "pic/yun_jin.png", element: "Geo" },
{ name: "Паймон", icon: "pic/paimon.png", element: "Anemo" },
{ name: "ГГ (Анемо)", icon: "pic/traveler.webp", element: "Anemo" },
{ name: "ГГ (Гео)", icon: "pic/traveler.webp", element: "Geo" },
{ name: "ГГ (Электро)", icon: "pic/traveler.webp", element: "Electro" },
{ name: "ГГ (Дендро)", icon: "pic/traveler.webp", element: "Dendro" },
{ name: "ГГ (Гидро)", icon: "pic/traveler.webp", element: "Hydro" },
{ name: "Мидзуки", icon: "pic/yumemizuki_mizuki.png", element: "Anemo" },
{ name: "Чжун Ли", icon: "pic/zhongli.png", element: "Geo" },
{ name: "Скирк", icon: "pic/skirk.png", element: "Cryo" },
{ name: "Далия", icon: "pic/daliya.png", element: "Hydro" },
{ name: "Инеффа", icon: "pic/ineffa.png", element: "Electro" },
{ name: "Лаума", icon: "pic/lauma_icon.webp", element: "Dendro" },
{ name: "Айно", icon: "pic/aino_icon.webp", element: "Hydro" },
{ name: "Флинс", icon: "pic/flins_icon.webp", element: "Electro" },
{ name: "Нефер", icon: "pic/Nefer.webp", element: "Dendro" },
{ name: "Ягода", icon: "pic/YAgoda.webp", element: "Anemo" },
{ name: "Дурин", icon: "pic/Durin.webp", element: "Pyro" },
{ name: "Иллуги", icon: "pic/Illuga.png", element: "Geo" },
{ name: "Цзы Бай", icon: "pic/zibai.png", element: "Geo" },
{ name: "Коломбина", icon: "pic/Columbina.png", element: "Hydro" }
];
// ==================== KNOWLEDGE BASE ====================
const charElements = {};
const roles = {};
const elementIcons = {
pyro: '🔥', hydro: '💧', anemo: '🍃', electro: '⚡',
dendro: '🌿', cryo: '❄️', geo: '🪨'
};
const roleNames = {
dps: 'Основной урон', subdps: 'Под-урон', support: 'Поддержка',
healer: 'Лекарь', shielder: 'Щитовик', battery: 'Батарея'
};
// Mapping
const nameMapping = {};
LOCAL_CHARACTERS.forEach(c => {
nameMapping[c.name] = c.name;
nameMapping[c.name.replace(/ /g, '')] = c.name;
});
nameMapping['Arlecchino'] = 'Арлекино';
nameMapping['Furina'] = 'Фурина';
nameMapping['Neuvillette'] = 'Нёвиллет';
nameMapping['Kazuha'] = 'Кадзуха';
nameMapping['Zhongli'] = 'Чжун Ли';
nameMapping['Bennett'] = 'Беннет';
nameMapping['Chevreuse'] = 'Шеврёз';
nameMapping['Fischl'] = 'Фишль';
nameMapping['Nahida'] = 'Нахида';
nameMapping['KukiShinobu'] = 'Шинобу';
nameMapping['Wriothesley'] = 'Ризли';
nameMapping['Shenhe'] = 'Шэнь Хэ';
nameMapping['Kokomi'] = 'Кокоми';
nameMapping['Navia'] = 'Навия';
nameMapping['Xiangling'] = 'Сян Линь';
nameMapping['Clorinde'] = 'Клоринда';
nameMapping['RaidenShogun'] = 'Райдэн';
nameMapping['Xingqiu'] = 'Син Цю';
nameMapping['Yelan'] = 'Е Лань';
nameMapping['Chiori'] = 'Тиори';
nameMapping['Xilonen'] = 'Шилонен';
nameMapping['Ororon'] = 'Оророн';
nameMapping['Charlotte'] = 'Шарлотта';
nameMapping['Gaming'] = 'Га Мин';
nameMapping['Lynette'] = 'Линнет';
nameMapping['LanYan'] = 'Лань Янь';
nameMapping['Dehya'] = 'Дэхья';
nameMapping['Thoma'] = 'Тома';
nameMapping['Yanfei'] = 'Янь Фэй';
nameMapping['Sethos'] = 'Сетос';
nameMapping['Ifa'] = 'Ифу';
nameMapping['Dahlia'] = 'Далия';
nameMapping['Iansan'] = 'Иансан';
nameMapping['YumemizukiMizuki'] = 'Мидзуки';
nameMapping['Aino'] = 'Айно';
nameMapping['Jahoda'] = 'Ягода';
nameMapping['Columbina'] = 'Коломбина';
nameMapping['Illuga'] = 'Иллуги';
nameMapping['Traveler'] = 'ГГ (Анемо)';
nameMapping['Skirk'] = 'Скирк';
nameMapping['Kinich'] = 'Кинич';
nameMapping['Emilie'] = 'Эмилия';
nameMapping['Sigewinne'] = 'Сиджвин';
nameMapping['Mualani'] = 'Муалани';
nameMapping['Chasca'] = 'Часка';
nameMapping['Citlali'] = 'Ситлали';
nameMapping['Mavuika'] = 'Мавуика';
nameMapping['Varesa'] = 'Вареса';
const elementMap = {
RaidenShogun: 'electro', Bennett: 'pyro', Nahida: 'dendro', Yelan: 'hydro',
Zhongli: 'geo', Arlecchino: 'pyro', Xiao: 'anemo', Ganyu: 'cryo',
Jean: 'anemo', Diluc: 'pyro', Mona: 'hydro', Fischl: 'electro',
Xiangling: 'pyro', Xingqiu: 'hydro', Sucrose: 'anemo', Noelle: 'geo',
KujouSara: 'electro', KukiShinobu: 'electro', Yaoyao: 'dendro',
Kirara: 'dendro', Collei: 'dendro', Alhaitham: 'dendro', Faruzan: 'anemo',
Chongyun: 'cryo', Diona: 'cryo', Rosaria: 'cryo', Kaeya: 'cryo',
Lisa: 'electro', Amber: 'pyro', Barbara: 'hydro', Sayu: 'anemo',
YunJin: 'geo', Ningguang: 'geo', Gorou: 'geo', Kaveh: 'dendro',
Mika: 'cryo', Freminet: 'cryo', Qiqi: 'cryo', Aloy: 'cryo',
Citlali: 'cryo', Chiori: 'geo', Xilonen: 'geo', Ororon: 'electro',
Charlotte: 'cryo', Gaming: 'pyro', Lynette: 'anemo', LanYan: 'anemo',
Chevreuse: 'pyro', Dehya: 'pyro', Thoma: 'pyro', Yanfei: 'pyro',
Sethos: 'electro', Ifa: 'anemo', Dahlia: 'hydro', Iansan: 'electro',
YumemizukiMizuki: 'anemo', Aino: 'hydro', Manekin: 'anemo', Manekina: 'anemo',
Jahoda: 'anemo', Columbina: 'hydro', Illuga: 'geo', Traveler: 'anemo',
Skirk: 'cryo', Furina: 'hydro', Navia: 'geo', Wriothesley: 'cryo',
Clorinde: 'electro', Sigewinne: 'hydro', Kinich: 'dendro', Emilie: 'dendro',
Neuvillette: 'hydro', Kazuha: 'anemo', Kokomi: 'hydro', Shenhe: 'cryo',
Mualani: 'hydro', Chasca: 'anemo', Mavuika: 'pyro', Varesa: 'electro'
};
// ==================== META TEAMS ====================
const metaTeams = [
{
name: '🌊 Пузырьковый реактор',
fullName: 'Фурина + Нёвиллет',
chars: ['Furina','Neuvillette','Kazuha','Zhongli'],
resonance: 'Гидро резонанс: +25% HP',
buffs: 'Фурина: +~75% урон, Нёвиллет: ДД, Кадзуха: +40% элемента, Чжун Ли: -20% резист',
reactions: 'Пар + Заряжен',
meta: '6.3+',
description: 'Топ-1 мета-отряд. Нёвиллет наносит колоссальный урон заряженными атаками.'
},
{
name: '🔥 Перегрузка Арлекино',
fullName: 'Арлекино + Шеврёз',
chars: ['Arlecchino','Bennett','Chevreuse','Fischl'],
resonance: 'Пиро резонанс: +25% ATK',
buffs: 'Арлекино: мощный ДД, Беннет: +~1000 ATK, Шеврёз: -40% резист',
reactions: 'Перегрузка',
meta: '6.3+',
description: 'Арлекино с Шеврёз дают огромный урон по площади.'
},
{
name: '🌱 Гиперблум',
fullName: 'Нахида + Фурина + Куки',
chars: ['Nahida','Furina','KukiShinobu','Zhongli'],
resonance: 'Дендро +80 EM, Гидро +25% HP',
buffs: 'Нахида: +250 EM, Фурина: бафф, Куки: триггер',
reactions: 'Гиперблум',
meta: '6.3+',
description: 'Стабильный урон через ядра. Куки на EM наносит 30-35к за ядро.'
},
{
name: '❄️ Моно-Крио',
fullName: 'Ризли + Шэнь Хэ',
chars: ['Wriothesley','Shenhe','Kazuha','Kokomi'],
resonance: 'Крио резонанс: +15% CR',
buffs: 'Ризли: ДД, Шэнь Хэ: бафф крио',
reactions: 'Заморозка',
meta: '6.3+',
description: 'Ризли бьёт быстро и сильно, Шэнь Хэ увеличивает крио урон.'
},
{
name: '🪨 Гео-удар',
fullName: 'Навия + Чжун Ли',
chars: ['Navia','Zhongli','Bennett','Xiangling'],
resonance: 'Гео +15% урон, Пиро +25% ATK',
buffs: 'Навия: ДД, Чжун Ли: щит, Беннет: ATK',
reactions: 'Кристалл',
meta: '6.3+',
description: 'Навия наносит огромный урон дробовиком.'
},
{
name: '⚡ Стимуляция',
fullName: 'Клоринда + Нахида',
chars: ['Clorinde','Nahida','Fischl','Zhongli'],
resonance: 'Электро резонанс',
buffs: 'Клоринда: ДД, Нахида: EM',
reactions: 'Стимуляция',
meta: '6.3+',
description: 'Клоринда — тир-1 ДД через обострение.'
}
];
// ==================== GLOBAL VARIABLES ====================
let localData = null;
let dbInstance = null;
// API providers and keys (separate storage)
let currentProvider = localStorage.getItem('nexus_current_provider') || 'huggingface';
let hfApiKey = localStorage.getItem('nexus_hf_key') || '';
let openaiApiKey = localStorage.getItem('nexus_openai_key') || '';
let geminiApiKey = localStorage.getItem('nexus_gemini_key') || '';
let deepseekApiKey = localStorage.getItem('nexus_deepseek_key') || '';
let customApiKey = localStorage.getItem('nexus_custom_key') || '';
let customBaseUrl = localStorage.getItem('nexus_custom_url') || 'https://router.huggingface.co/v1';
let customModel = localStorage.getItem('nexus_custom_model') || '';
let hfModel = localStorage.getItem('nexus_hf_model') || 'deepseek-ai/DeepSeek-R1:fastest';
let openaiModel = localStorage.getItem('nexus_openai_model') || 'gpt-oss-20b';
let geminiModel = localStorage.getItem('nexus_gemini_model') || 'gemini-2.5-flash-lite-preview-09-2025';
let deepseekModel = localStorage.getItem('nexus_deepseek_model') || 'deepseek-chat';
let charsView = localStorage.getItem('nexus_chars_view') || 'table';
let artifactsView = localStorage.getItem('nexus_artifacts_view') || 'list';
// Profiles
let profiles = JSON.parse(localStorage.getItem('nexus_profiles') || '[]');
let activeProfile = localStorage.getItem('nexus_active_profile') || 'default';
// My teams
let myTeams = JSON.parse(localStorage.getItem('nexus_myteams') || '[]');
// ==================== POPUPS ====================
function openLegend() { document.getElementById('legendPopup').classList.add('active'); }
function closeLegend() { document.getElementById('legendPopup').classList.remove('active'); }
function openCVPopup() { document.getElementById('cvPopup').classList.add('active'); }
function closeCVPopup() { document.getElementById('cvPopup').classList.remove('active'); }
// ==================== PROVIDER MANAGEMENT ====================
function setAIProvider(provider) {
currentProvider = provider;
localStorage.setItem('nexus_current_provider', provider);
// Update tabs
document.querySelectorAll('.provider-tab').forEach(tab => {
tab.classList.toggle('active', tab.dataset.provider === provider);
});
// Show correct settings
document.querySelectorAll('#provider-settings > div').forEach(div => div.style.display = 'none');
document.getElementById(`settings-${provider}`).style.display = 'block';
}
// ==================== SAVE ALL KEYS ====================
function saveAllApiKeys() {
hfApiKey = document.getElementById('hf-api-key').value.trim();
localStorage.setItem('nexus_hf_key', hfApiKey);
openaiApiKey = document.getElementById('openai-api-key').value.trim();
localStorage.setItem('nexus_openai_key', openaiApiKey);
geminiApiKey = document.getElementById('gemini-api-key').value.trim();
localStorage.setItem('nexus_gemini_key', geminiApiKey);
deepseekApiKey = document.getElementById('deepseek-api-key').value.trim();
localStorage.setItem('nexus_deepseek_key', deepseekApiKey);
customApiKey = document.getElementById('custom-api-key').value.trim();
localStorage.setItem('nexus_custom_key', customApiKey);
customBaseUrl = document.getElementById('custom-base-url').value.trim();
localStorage.setItem('nexus_custom_url', customBaseUrl);
customModel = document.getElementById('custom-model').value.trim();
localStorage.setItem('nexus_custom_model', customModel);
// Models
hfModel = document.getElementById('hf-model').value;
localStorage.setItem('nexus_hf_model', hfModel);
openaiModel = document.getElementById('openai-model').value;
localStorage.setItem('nexus_openai_model', openaiModel);
geminiModel = document.getElementById('gemini-model').value;
localStorage.setItem('nexus_gemini_model', geminiModel);
deepseekModel = document.getElementById('deepseek-model').value;
localStorage.setItem('nexus_deepseek_model', deepseekModel);
document.getElementById('api-test-result').innerHTML = '✅ Все ключи и настройки сохранены';
}
// ==================== UNIVERSAL AI CALL ====================
async function askAI(prompt, useGoogleSearch = false) {
let apiKey = '', baseURL = '', model = '';
switch(currentProvider) {
case 'huggingface':
apiKey = hfApiKey;
baseURL = 'https://router.huggingface.co/v1';
model = hfModel;
break;
case 'openai':
apiKey = openaiApiKey;
baseURL = 'https://api.openai.com/v1';
model = openaiModel;
break;
case 'gemini':
apiKey = geminiApiKey;
baseURL = 'https://my-openai-gemini-demo.vercel.app/v1';
model = geminiModel;
break;
case 'deepseek':
apiKey = deepseekApiKey;
baseURL = 'https://api.deepseek.com/v1';
model = deepseekModel;
break;
case 'custom':
apiKey = customApiKey;
baseURL = customBaseUrl;
model = customModel || 'gpt-oss-20b';
break;
}
if (!apiKey) throw new Error(`🔑 API ключ для ${currentProvider} не указан`);
// Hugging Face specific format
if (currentProvider === 'huggingface') {
const response = await fetch(`${baseURL}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify({
model: model,
messages: [
{ role: 'system', content: 'Ты - миниакс помощник Genshin Impact. Отвечай строго по шаблону:\n## Название отряда\n**Состав:** персонаж1, персонаж2, персонаж3, персонаж4\n**Резонанс:** ...\n**Реакции:** ...\n**Баффы:** ...\n**Описание:** ...' },
{ role: 'user', content: prompt }
],
stream: false
})
});
if (!response.ok) { const err = await response.json(); throw new Error(err.error?.message || `Ошибка ${response.status}`); }
const data = await response.json();
return data.choices[0].message.content;
}
else if (currentProvider === 'gemini') {
const response = await fetch(`${baseURL}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify({
model: model,
messages: [
{ role: 'system', content: 'Ты - миниакс помощник Genshin Impact. Отвечай строго по шаблону:\n## Название отряда\n**Состав:** персонаж1, персонаж2, персонаж3, персонаж4\n**Резонанс:** ...\n**Реакции:** ...\n**Баффы:** ...\n**Описание:** ...' },
{ role: 'user', content: prompt }
],
temperature: 1
})
});
if (!response.ok) { const err = await response.json(); throw new Error(err.error?.message || `Ошибка ${response.status}`); }
const data = await response.json();
return data.choices[0].message.content;
}
else {
// OpenAI / DeepSeek / Custom
const response = await fetch(`${baseURL}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify({
model: model,
messages: [
{ role: 'system', content: 'Ты - миниакс помощник Genshin Impact. Отвечай строго по шаблону:\n## Название отряда\n**Состав:** персонаж1, персонаж2, персонаж3, персонаж4\n**Резонанс:** ...\n**Реакции:** ...\n**Баффы:** ...\n**Описание:** ...' },
{ role: 'user', content: prompt }
],
temperature: 1
})
});
if (!response.ok) { const err = await response.json(); throw new Error(err.error?.message || `Ошибка ${response.status}`); }
const data = await response.json();
return data.choices[0].message.content;
}
}
async function testCurrentApi() {
const key = currentProvider === 'huggingface' ? hfApiKey :
currentProvider === 'openai' ? openaiApiKey :
currentProvider === 'gemini' ? geminiApiKey :
currentProvider === 'deepseek' ? deepseekApiKey : customApiKey;
if (!key) return alert('🔑 Введите API ключ');
document.getElementById('api-test-result').innerHTML = '⏳ Проверка...';
try {
const result = await askAI('Ответь "OK" одной строкой.', false);
document.getElementById('api-test-result').innerHTML = '✅ Подключение работает, ключ действителен';
} catch (e) {
document.getElementById('api-test-result').innerHTML = `❌ Ошибка: ${e.message}`;
}
}
// ==================== RUSSIAN NAME, ICON, ELEMENT ====================
function getRussianName(engName) {
if (nameMapping[engName]) return nameMapping[engName];
if (nameMapping[engName.replace(/[^a-zA-Z]/g, '')]) return nameMapping[engName.replace(/[^a-zA-Z]/g, '')];
const localChar = LOCAL_CHARACTERS.find(c =>
c.name.toLowerCase().includes(engName.toLowerCase()) ||
engName.toLowerCase().includes(c.name.toLowerCase().replace(/[^a-zа-я]/g, ''))
);
return localChar ? localChar.name : engName;
}
function getCharacterIcon(charName) {
const localChar = LOCAL_CHARACTERS.find(c => c.name === charName);
if (localChar && localChar.icon) return localChar.icon;
for (let [eng, rus] of Object.entries(nameMapping)) {
if (rus === charName || eng === charName) {
const found = LOCAL_CHARACTERS.find(c => c.name === rus);
if (found) return found.icon;
}
}
return `https://placehold.co/64x64/4A4A4A/FFFFFF?text=${charName.charAt(0)}`;
}
function getCharacterElement(charName) {
const localChar = LOCAL_CHARACTERS.find(c => c.name === charName);
if (localChar) return localChar.element.toLowerCase();
for (let [eng, rus] of Object.entries(nameMapping)) {
if (rus === charName) {
const found = LOCAL_CHARACTERS.find(c => c.name === rus);
if (found) return found.element.toLowerCase();
}
}
return 'anemo';
}
// ==================== PARSING AI RESPONSE ====================
function parseAIResponse(text) {
const teams = [];
const sections = text.split(/\n##\s+/);
for (let section of sections) {
if (!section.trim()) continue;
const title = section.split('\n')[0].trim();
let composition = [];
const compMatch = section.match(/\*\*Состав:\*\*\s*(.+)/i);
if (compMatch) composition = compMatch[1].split(',').map(s => s.trim()).slice(0,4);
let resonance = ''; const resMatch = section.match(/\*\*Резонанс:\*\*\s*(.+)/i);
if (resMatch) resonance = resMatch[1].trim();
let reactions = ''; const reactMatch = section.match(/\*\*Реакции:\*\*\s*(.+)/i);
if (reactMatch) reactions = reactMatch[1].trim();
let buffs = ''; const buffMatch = section.match(/\*\*Баффы:\*\*\s*(.+)/i);
if (buffMatch) buffs = buffMatch[1].trim();
let description = ''; const descMatch = section.match(/\*\*Описание:\*\*\s*(.+)/i);
if (descMatch) description = descMatch[1].trim();
teams.push({ title, composition, resonance, reactions, buffs, description });
}
return teams;
}
// ==================== AI TEAM BUILDER ====================
async function aiTeamBuilder() {
if (!localData?.characters?.length) return alert('📁 Сначала загрузите GOOD‑файл');
const resultDiv = document.getElementById('ai-team-result');
resultDiv.innerHTML = '<div style="display:flex; align-items:center; gap:12px;"><div class="loader" style="display:block;"></div> 🧠 AI подбирает отряды...</div>';
const charList = localData.characters.map(c => getRussianName(c.key)).join(', ');
const prompt = `У меня есть персонажи: ${charList}. Составь 10 лучших отряда ЖЕЛАТЕЛЬНО c нужными баффами. Используй строгий шаблон:\n## Название отряда\n**Состав:** персонаж1, персонаж2, персонаж3, персонаж4\n**Резонанс:** ...\n**Реакции:** ...\n**Баффы:** ...\n**Описание:** ...`;
try {
const advice = await askAI(prompt, document.getElementById('googleSearchCheckbox')?.checked || false);
const teams = parseAIResponse(advice);
if (teams.length === 0) { resultDiv.innerHTML = `<div class="empty">❌ Не удалось распарсить ответ AI</div>`; return; }
let html = '<h3 style="color:var(--accent); margin-bottom:24px; font-size:22px;">🧠 AI‑рекомендация отрядов</h3>';
teams.forEach(team => {
let resGlow = 'rgba(139,92,246,0.3)';
if (team.resonance.includes('Пиро')) resGlow = 'rgba(255,107,74,0.4)';
else if (team.resonance.includes('Гидро')) resGlow = 'rgba(59,130,246,0.4)';
else if (team.resonance.includes('Крио')) resGlow = 'rgba(56,189,248,0.4)';
else if (team.resonance.includes('Электро')) resGlow = 'rgba(168,85,247,0.4)';
else if (team.resonance.includes('Дендро')) resGlow = 'rgba(132,204,22,0.4)';
else if (team.resonance.includes('Гео')) resGlow = 'rgba(251,191,36,0.4)';
html += `<div class="synergy-card" style="--res-glow: ${resGlow};">`;
html += `<div style="display:flex; align-items:center; gap:16px; margin-bottom:16px;">
<span style="font-size:32px;">⚔️</span>
<div><h3 style="font-size:22px; font-weight:800;">${team.title}</h3></div>
</div>`;
html += `<div class="team-members">`;
team.composition.forEach(name => {
const rusName = getRussianName(name);
const element = getCharacterElement(rusName);
const elementIcon = elementIcons[element] || '❓';
const elementColor = `var(--${element})`;
const icon = getCharacterIcon(rusName);
html += `<div class="team-member">
<img src="${icon}" class="member-icon" style="border-color:${elementColor}; object-fit:cover;" onerror="this.onerror=null;this.src='https://placehold.co/64x64/4A4A4A/FFFFFF?text=${rusName.charAt(0)}';">
<span style="font-size:14px; font-weight:700;">${rusName}</span>
</div>`;
});
html += `</div>`;
html += `<div style="display:grid; grid-template-columns:repeat(3,1fr); gap:16px; background:rgba(0,0,0,0.25); border-radius:var(--radius-lg); padding:20px;">
<div><span style="color:var(--accent);">🎵 Резонанс:</span> ${team.resonance || '—'}</div>
<div><span style="color:var(--accent);">⚡ Реакции:</span> ${team.reactions || '—'}</div>
<div><span style="color:var(--accent);">✨ Баффы:</span> ${team.buffs || '—'}</div>
</div>`;
if (team.description) html += `<div style="margin-top:16px; color:var(--text-soft); padding:16px; background:rgba(0,0,0,0.15); border-radius:var(--radius-md);">💬 ${team.description}</div>`;
html += `<div style="margin-top:16px;"><button class="btn btn-outline" onclick="saveTeamFromAI('${team.title}', ${JSON.stringify(team.composition)})">💾 Сохранить отряд</button></div>`;
html += `</div>`;
});
resultDiv.innerHTML = html;
} catch (e) {
resultDiv.innerHTML = `<div style="color:var(--error); padding:20px;">❌ Ошибка: ${e.message}</div>`;
}
}
// ==================== SAVE TEAM FUNCTIONS ====================
function saveTeamFromAI(title, composition) {
const team = {
id: Date.now(),
name: title,
characters: composition,
date: new Date().toLocaleDateString('ru-RU')
};
myTeams.push(team);
localStorage.setItem('nexus_myteams', JSON.stringify(myTeams));
renderMyTeams();
alert('✅ Отряд сохранён');
}
function saveCurrentTeam() {
if (!localData?.characters?.length) return alert('📁 Сначала загрузите GOOD‑файл');
const availableChars = localData.characters.map(c => getRussianName(c.key));
if (availableChars.length < 4) return alert('❌ Недостаточно персонажей');
const selectedChars = availableChars.slice(0, 4);
const team = {
id: Date.now(),
name: `Созданный отряд ${new Date().toLocaleDateString('ru-RU')}`,
characters: selectedChars,
date: new Date().toLocaleDateString('ru-RU')
};
myTeams.push(team);
localStorage.setItem('nexus_myteams', JSON.stringify(myTeams));
renderMyTeams();
alert('✅ Отряд сохранён');
}
// ==================== RENDER MY TEAMS ====================
function renderMyTeams() {
const container = document.getElementById('myteams-grid');
if (!container) return;
if (myTeams.length === 0) {
container.innerHTML = '<div class="empty">🎲 Нет сохранённых отрядов. Используйте AI‑подбор.</div>';
return;
}
container.innerHTML = myTeams.map(team => {
return `<div class="team-card">
<div class="team-card-header">
<span style="font-size:22px;">⚔️</span>
<div><h4 style="font-size:17px; font-weight:700;">${team.name}</h4></div>
</div>
<div class="team-card-icons">
${team.characters.map(name => {
const rusName = getRussianName(name);
const element = getCharacterElement(rusName);
const elementColor = `var(--${element})`;
const icon = getCharacterIcon(rusName);
return `<img src="${icon}" class="team-card-icon" style="border-color:${elementColor}; object-fit:cover;" onerror="this.onerror=null;this.src='https://placehold.co/44x44/4A4A4A/FFFFFF?text=${rusName.charAt(0)}';">`;
}).join('')}
</div>
<div style="margin-top:12px; color:var(--text-dim); font-size:12px;">${team.date || ''}</div>
<div style="display:flex; gap:8px; margin-top:12px;">
<button class="btn btn-outline" style="padding:4px 12px; font-size:12px;" onclick="deleteTeam(${team.id})">🗑️</button>
</div>
</div>`;
}).join('');
}
function deleteTeam(id) {
myTeams = myTeams.filter(t => t.id !== id);
localStorage.setItem('nexus_myteams', JSON.stringify(myTeams));
renderMyTeams();
}
function generateRandomTeam() {
if (!localData?.characters?.length) return alert('📁 Сначала загрузите GOOD‑файл');
const availableChars = localData.characters.map(c => getRussianName(c.key));
if (availableChars.length < 4) return alert('❌ Недостаточно персонажей');
const shuffled = [...availableChars].sort(() => 0.5 - Math.random());
const selected = shuffled.slice(0, 4);
const team = {
id: Date.now(),
name: `🎲 Рандомный отряд ${new Date().toLocaleDateString('ru-RU')}`,
characters: selected,
date: new Date().toLocaleDateString('ru-RU')
};
myTeams.push(team);
localStorage.setItem('nexus_myteams', JSON.stringify(myTeams));
renderMyTeams();
alert('✅ Рандомный отряд сохранён');
}
// ==================== PROFILES ====================
function createNewProfile() {
const name = prompt('Название профиля:', `Профиль ${profiles.length + 1}`);
if (!name) return;
const profile = { id: Date.now(), name: name, data: localData ? JSON.parse(JSON.stringify(localData)) : null, date: new Date().toLocaleDateString('ru-RU') };
profiles.push(profile);
localStorage.setItem('nexus_profiles', JSON.stringify(profiles));
renderProfiles();
}
function loadProfile(profileId) {
const profile = profiles.find(p => p.id == profileId);
if (profile && profile.data) {
localData = JSON.parse(JSON.stringify(profile.data));
enrichCharData(); saveToDB(); renderChars(); renderAssessment();
activeProfile = profileId;
localStorage.setItem('nexus_active_profile', profileId);
alert(`✅ Загружен профиль: ${profile.name}`);
}
}
function deleteProfile(profileId) {
if (confirm('Удалить профиль?')) {
profiles = profiles.filter(p => p.id !== profileId);
localStorage.setItem('nexus_profiles', JSON.stringify(profiles));
renderProfiles();
}
}
function renderProfiles() {
const container = document.getElementById('profiles-list');
if (!container) return;
if (profiles.length === 0) { container.innerHTML = '<div class="empty">👤 Нет сохранённых профилей.</div>'; return; }
container.innerHTML = profiles.map(p => {
const isActive = p.id == activeProfile;
return `<div class="profile-badge" style="justify-content:space-between; ${isActive ? 'background:rgba(139,92,246,0.3); border-color:var(--accent);' : ''}">
<div style="display:flex; align-items:center; gap:12px;">
<span style="font-size:20px;">👤</span>
<div><span style="font-weight:700;">${p.name}</span> <span style="font-size:12px;">${p.date || ''}</span></div>
</div>
<div style="display:flex; gap:8px;">
<button class="btn btn-outline" style="padding:6px 12px;" onclick="loadProfile(${p.id})">📂</button>
<button class="btn btn-outline" style="padding:6px 12px;" onclick="deleteProfile(${p.id})">🗑️</button>
</div>
</div>`;
}).join('');
}
function showProfileSelector() { switchTab('profiles'); }
// ==================== MARKDOWN WITH ANCHORS ====================
function renderMarkdownWithAnchors(text) {
if (!text) return { html: '', toc: '' };
let lines = text.split('\n'), toc = [];
let processed = lines.map(line => {
let match = line.match(/^(##+)\s+(.+)/);
if (match) {
let level = match[1].length, title = match[2].trim(), id = title.toLowerCase().replace(/[^\wа-яё\s]/g, '').replace(/\s+/g, '-');
toc.push({ level, title, id });
return `<h${level} id="${id}" style="margin-top:1.5em; color:#c7b9ff;">${title}</h${level}>`;
}
line = line.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
line = line.replace(/\*(.+?)\*/g, '<em>$1</em>');
line = line.replace(/`(.+?)`/g, '<code>$1</code>');
if (line.trim().startsWith('- ')) return '<li>' + line.trim().substring(2) + '</li>';
if (line.trim().startsWith('* ')) return '<li>' + line.trim().substring(2) + '</li>';
return line;
});
let html = '', inList = false;
processed.forEach(line => {
if (line.startsWith('<li>')) { if (!inList) { html += '<ul>'; inList = true; } html += line; }
else { if (inList) { html += '</ul>'; inList = false; } html += line + '<br>'; }
});
if (inList) html += '</ul>';
if (toc.length > 1) {
let tocHtml = '<div class="toc"><strong>📖 Содержание:</strong><div style="display:flex; flex-wrap:wrap; gap:10px; margin-top:8px;">';
toc.forEach(item => { tocHtml += `<a href="#${item.id}" style="color:var(--accent); background:rgba(139,92,246,0.1); padding:4px 12px; border-radius:40px;">${item.title}</a>`; });
tocHtml += '</div></div>';
return { html: tocHtml + html, toc: tocHtml };
}
return { html, toc: '' };
}
// ==================== AI CHARACTER ANALYSIS ====================
async function analyzeCharacter(charKey) {
if (!localData) return alert('📁 Сначала загрузите GOOD‑файл.');
const char = localData.characters.find(c => c.key === charKey);
if (!char) return;
const arts = localData.artifacts.filter(a => a.location === charKey);
const weapon = localData.weapons?.find(w => w.location === charKey) || { key: '—', level: 0, refinement: 0 };
document.getElementById('ai-overlay').classList.add('open');
document.getElementById('ai-text').innerHTML = '<div style="display:flex; align-items:center; gap:16px; padding:40px;"><div class="loader"></div> ⏳ Анализ...</div>';
document.getElementById('ai-toc').style.display = 'none';
const role = roles[charKey] || 'Универсал';
const element = charElements[charKey] || '—';
const rusName = getRussianName(charKey);
const context = `Персонаж: ${rusName} (${charKey}), уровень ${char.level}, созвездие ${char.constellation}, элемент ${element}. Роль: ${role}. Оружие: ${weapon.key} (ур.${weapon.level}, Проб.${weapon.refinement}). Таланты: атака ${char.talent?.auto || 0}, навык ${char.talent?.skill || 0}, взрыв ${char.talent?.burst || 0}. Артефакты (${arts.length}): ${JSON.stringify(arts.map(a => ({ сет: artifactSetNames[a.setKey] || a.setKey, осн: a.mainStatKey, суб: a.substats })))}.
Напиши подробный анализ на русском: 1. Оценка билда (что хорошо/плохо) — 2-3 предложения. 2. 3 конкретных совета по улучшению. 3. Идеальные целевые значения CR, CD, ER, EM. Используй заголовки ## и ###, списки.`;
try {
const advice = await askAI(context, false);
const rendered = renderMarkdownWithAnchors(advice);
document.getElementById('ai-text').innerHTML = rendered.html;
if (rendered.toc) { document.getElementById('ai-toc').innerHTML = rendered.toc; document.getElementById('ai-toc').style.display = 'block'; }
else { document.getElementById('ai-toc').style.display = 'none'; }
document.getElementById('ai-text').scrollTop = 0;
} catch (e) {
document.getElementById('ai-text').innerHTML = `<div style="color:var(--error); padding:24px;">❌ Ошибка: ${e.message}</div>`;
}
}
function closeAI() { document.getElementById('ai-overlay').classList.remove('open'); }
// ==================== NAVIGATION ====================
function switchTab(tabId) {
document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active'));
document.querySelector(`[data-tab="${tabId}"]`).classList.add('active');
document.querySelectorAll('.view-panel').forEach(el => el.classList.remove('active'));
const panel = document.getElementById(`view-${tabId}`);
if (panel) panel.classList.add('active');
if (tabId === 'artifacts' && localData) renderArtifacts();
if (tabId === 'team' && localData) renderTeams();
if (tabId === 'chars' && localData) renderChars();
if (tabId === 'assessment' && localData) renderAssessment();
if (tabId === 'myteams') renderMyTeams();
if (tabId === 'profiles') renderProfiles();
}
// ==================== INDEXEDDB ====================
const dbReq = indexedDB.open("NexusDB", 4);
dbReq.onupgradeneeded = e => { const db = e.target.result; if (!db.objectStoreNames.contains('app')) db.createObjectStore("app", { keyPath: "id" }); };
dbReq.onsuccess = e => { dbInstance = e.target.result; loadFromDB(); };
function loadFromDB() {
if (!dbInstance) return;
const tx = dbInstance.transaction("app", "readonly");
const store = tx.objectStore("app");
const req = store.get("state");
req.onsuccess = () => { if (req.result) { localData = req.result.data; enrichCharData(); renderChars(); renderAssessment(); } };
}
function saveToDB() { if (dbInstance && localData) dbInstance.transaction("app", "readwrite").objectStore("app").put({ id: "state", data: localData }); }
function clearDB() { if (confirm('Удалить все данные?')) { dbInstance.transaction("app", "readwrite").objectStore("app").clear(); localData = null; renderChars(); location.reload(); } }
// ==================== ENRICH CHARACTER DATA ====================
function enrichCharData() {
if (!localData?.characters) return;
localData.characters.forEach(c => {
charElements[c.key] = elementMap[c.key] || 'anemo';
if (!roles[c.key]) {
if (['Bennett','Diona','Zhongli','Kazuha','Sucrose','Gorou','Faruzan','Chevreuse','Mika','Layla','Kirara','Yaoyao','Barbara','Jean','Sayu','Charlotte','Xianyun','KukiShinobu','Citlali','Xilonen','Furina','Kokomi','Shenhe'].includes(c.key))
roles[c.key] = 'support';
else if (['Xingqiu','Yelan','Fischl','Xiangling','Nahida','Alhaitham','Rosaria','Kaeya','Beidou','Collei','KujouSara','YunJin','Candace','Ororon','Thoma','Dehya','Kaveh','Gaming','Chiori','Columbina','Aino','Skirk','Dahlia','Clorinde','Emilie','Kinich','Neuvillette'].includes(c.key))
roles[c.key] = 'subdps';
else roles[c.key] = 'dps';
}
});
}
// ==================== CV CALCULATION ====================
function getCV(art) {
if (!art.substats) return 0;
return art.substats.reduce((acc, s) => {
if (s.key === 'critRate_') return acc + s.value * 2;
if (s.key === 'critDMG_') return acc + s.value;
return acc;
}, 0);
}
// ==================== RENDER CHARACTERS ====================
function renderChars() {
if (!localData) { document.getElementById('empty-msg').style.display = 'block'; return; }
renderTable(); renderCards();
if (charsView === 'table') {
document.getElementById('chars-table-container').style.display = 'block';
document.getElementById('chars-cards-container').style.display = 'none';
} else {
document.getElementById('chars-table-container').style.display = 'none';
document.getElementById('chars-cards-container').style.display = 'grid';
}
document.getElementById('empty-msg').style.display = 'none';
document.getElementById('total-c').innerText = localData.characters?.length || 0;
document.getElementById('total-a').innerText = localData.artifacts?.length || 0;
}
function renderTable() {
const chars = localData.characters || [], arts = localData.artifacts || [], weapons = localData.weapons || [];
let list = chars.map(c => {
const equippedArts = arts.filter(a => a.location === c.key);
const cv = equippedArts.reduce((sum, a) => sum + getCV(a), 0);
const weapon = weapons.find(w => w.location === c.key);
return { ...c, equipped: equippedArts, cv, weapon };
});
const elFilter = document.getElementById('f-el').value;
if (elFilter !== 'all') list = list.filter(c => charElements[c.key] === elFilter);
const sort = document.getElementById('f-sort').value;
if (sort === 'cv') list.sort((a,b) => b.cv - a.cv);
else if (sort === 'lvl') list.sort((a,b) => b.level - a.level);
else if (sort === 'name') list.sort((a,b) => getRussianName(a.key).localeCompare(getRussianName(b.key), 'ru'));
const tbody = document.getElementById('char-body'); tbody.innerHTML = '';
let totalCv = 0;
list.forEach(c => {
totalCv += c.cv;
const tr = document.createElement('tr'); tr.className = 'data-row';
const element = charElements[c.key] || 'anemo';
const elementColor = `var(--${element})`;
const elementIcon = elementIcons[element] || '';
const rusName = getRussianName(c.key);
const icon = getCharacterIcon(rusName);
const setNames = [...new Set(c.equipped.map(a => artifactSetNames[a.setKey] || a.setKey))];
const setsDisplay = setNames.length ? setNames.join(', ') : '—';
let weaponDisplay = '—'; if (c.weapon) weaponDisplay = `${c.weapon.key} 🔹 Проб.${c.weapon.refinement} ур.${c.weapon.level}`;
let roleDisplay = '—'; if (roles[c.key]) roleDisplay = roleNames[roles[c.key]] || roles[c.key];
let cvStatus = '', cvColor = '';
if (c.cv > 200) { cvStatus = 'Высокий'; cvColor = 'var(--success)'; }
else if (c.cv > 140) { cvStatus = 'Средний'; cvColor = 'var(--warning)'; }
else { cvStatus = 'Низкий'; cvColor = 'var(--error)'; }
tr.innerHTML = `
<td><div style="display:flex; align-items:center; gap:12px;">
<img src="${icon}" style="width:44px; height:44px; border-radius:50%; border:2px solid ${elementColor}; object-fit:cover;" onerror="this.onerror=null;this.src='https://placehold.co/44x44/4A4A4A/FFFFFF?text=${rusName.charAt(0)}';">
<div><a href="https://www.google.com/search?q=Genshin+Impact+${encodeURIComponent(rusName)}+гайд" target="_blank" style="font-size:16px; font-weight:700; color:#fff;">${rusName}</a>
<span style="background:rgba(0,0,0,0.4); padding:4px 10px; border-radius:30px; font-size:12px; margin-left:8px;">Ур.${c.level} · С${c.constellation}</span></div>
</div></td>
<td><span class="weapon-badge">⚔️ ${weaponDisplay}</span><br><span class="role-badge" style="margin-top:6px;">🎯 ${roleDisplay}</span></td>
<td><span style="color:var(--text-soft);">${setsDisplay}</span></td>
<td><span style="font-size:18px; font-weight:800;">${c.cv.toFixed(1)}</span>
<div class="cv-meter"><div class="cv-fill" style="width:${Math.min(c.cv/2.2,100)}%; background:${cvColor};"></div></div>
<span style="font-size:11px; color:${cvColor};">${cvStatus}</span>
</td>
<td><button class="ai-btn" onclick="analyzeCharacter('${c.key}')">✨ Анализ</button></td>
`;
tbody.appendChild(tr);
});
document.getElementById('avg-cv').innerText = (totalCv / (list.length || 1)).toFixed(1);
}
function renderCards() {
const chars = localData.characters || [], arts = localData.artifacts || [], weapons = localData.weapons || [];
let list = chars.map(c => {
const equippedArts = arts.filter(a => a.location === c.key);
const cv = equippedArts.reduce((sum, a) => sum + getCV(a), 0);
const weapon = weapons.find(w => w.location === c.key);
return { ...c, equipped: equippedArts, cv, weapon };
});
const elFilter = document.getElementById('f-el').value;
if (elFilter !== 'all') list = list.filter(c => charElements[c.key] === elFilter);
const sort = document.getElementById('f-sort').value;
if (sort === 'cv') list.sort((a,b) => b.cv - a.cv);
else if (sort === 'lvl') list.sort((a,b) => b.level - a.level);
else if (sort === 'name') list.sort((a,b) => getRussianName(a.key).localeCompare(getRussianName(b.key), 'ru'));
const container = document.getElementById('chars-cards-container'); container.innerHTML = '';
list.forEach(c => {
const element = charElements[c.key] || 'anemo';
const elementColor = `var(--${element})`;
const elementGlow = `var(--${element}-glow)`;
const rusName = getRussianName(c.key);
const icon = getCharacterIcon(rusName);
const setNames = [...new Set(c.equipped.map(a => artifactSetNames[a.setKey] || a.setKey))];
const setsDisplay = setNames.length ? setNames.join(', ') : '—';
let weaponDisplay = '—'; if (c.weapon) weaponDisplay = `${c.weapon.key} 🔹 Проб.${c.weapon.refinement}`;
let roleDisplay = '—'; if (roles[c.key]) roleDisplay = roleNames[roles[c.key]] || roles[c.key];
let cvStatus = '', cvColor = '';
if (c.cv > 200) { cvStatus = 'Высокий'; cvColor = 'var(--success)'; }
else if (c.cv > 140) { cvStatus = 'Средний'; cvColor = 'var(--warning)'; }
else { cvStatus = 'Низкий'; cvColor = 'var(--error)'; }
const card = document.createElement('div'); card.className = 'char-card';
card.style.setProperty('--element-color', elementColor);
card.style.setProperty('--element-glow', elementGlow);
card.innerHTML = `
<div class="char-card-header">
<img src="${icon}" class="char-element-icon" style="border-color:${elementColor}; object-fit:cover;" onerror="this.onerror=null;this.src='https://placehold.co/60x60/4A4A4A/FFFFFF?text=${rusName.charAt(0)}';">
<div><div class="char-name"><a href="https://www.google.com/search?q=Genshin+Impact+${encodeURIComponent(rusName)}+гайд" target="_blank">${rusName}</a></div>
<span class="char-level">⭐ Ур.${c.level} · Созвездие ${c.constellation}</span></div>
</div>
<div class="char-card-stats"><span class="weapon-badge">⚔️ ${weaponDisplay}</span><span class="role-badge">🎯 ${roleDisplay}</span></div>
<div class="char-card-sets"><span style="color:var(--text-dim);">⚙️ Сеты:</span> ${setsDisplay}</div>
<div style="display:flex; justify-content:space-between; align-items:center;">
<div><span style="font-size:22px; font-weight:800;">${c.cv.toFixed(1)}</span>
<span style="font-size:12px; color:${cvColor}; margin-left:6px;">${cvStatus}</span>
<div class="cv-meter" style="width:100px;"><div class="cv-fill" style="width:${Math.min(c.cv/2.2,100)}%; background:${cvColor};"></div></div></div>
<button class="ai-btn" onclick="analyzeCharacter('${c.key}')">✨ Анализ</button>
</div>
`;
container.appendChild(card);
});
}
// ==================== ARTIFACTS ====================
function renderArtifacts() {
if (!localData?.artifacts) { document.getElementById('artifacts-list').innerHTML = '<div class="empty">Нет артефактов</div>'; return; }
// Populate set filter dropdown
const setFilter = document.getElementById('art-set-filter');
setFilter.innerHTML = '<option value="all">Все сеты</option>';
const uniqueSets = [...new Set(localData.artifacts.map(a => a.setKey))];
uniqueSets.sort().forEach(setKey => {
const setName = artifactSetNames[setKey] || setKey;
const option = document.createElement('option');
option.value = setKey;
option.textContent = setName;
setFilter.appendChild(option);
});
let filtered = localData.artifacts;
const setFilterValue = document.getElementById('art-set-filter').value;
if (setFilterValue !== 'all') filtered = filtered.filter(a => a.setKey === setFilterValue);
const slotFilter = document.getElementById('art-slot-filter').value;
if (slotFilter !== 'all') filtered = filtered.filter(a => a.slotKey === slotFilter);
const groupBy = document.getElementById('art-set-group').value;
const container = document.getElementById('artifacts-list');
if (groupBy !== 'all') {
// Grouping
let groups = {};
if (groupBy === 'set') {
filtered.forEach(art => { const key = art.setKey; if (!groups[key]) groups[key] = []; groups[key].push(art); });
} else if (groupBy === 'slot') {
filtered.forEach(art => { const key = art.slotKey; if (!groups[key]) groups[key] = []; groups[key].push(art); });
} else if (groupBy === 'rarity') {
filtered.forEach(art => { const key = art.rarity || 5; if (!groups[key]) groups[key] = []; groups[key].push(art); });
} else if (groupBy === 'level') {
filtered.forEach(art => { const key = Math.floor(art.level/4)*4; if (!groups[key]) groups[key] = []; groups[key].push(art); });
} else if (groupBy === 'cv') {
// Group by CV ranges
filtered.forEach(art => {
const cv = getCV(art);
let key;
if (cv > 200) key = 'Высокий (>200)';
else if (cv > 140) key = 'Средний (140-200)';
else key = 'Низкий (<140)';
if (!groups[key]) groups[key] = [];
groups[key].push(art);
});
}
let html = '';
for (let key in groups) {
let groupName = key;
if (groupBy === 'set') groupName = artifactSetNames[key] || key;
if (groupBy === 'slot') groupName = slotNames[key] || key;
if (groupBy === 'rarity') groupName = `${key}★`;
if (groupBy === 'level') groupName = `+${key} и выше`;
if (groupBy === 'cv') groupName = key;
html += `<h3 style="margin:16px 0 8px; color:var(--text-soft);">${groupName} (${groups[key].length})</h3>`;
html += renderArtifactList(groups[key]);
}
container.innerHTML = html;
} else {
container.innerHTML = renderArtifactList(filtered);
}
}
function renderArtifactList(artifacts) {
if (artifactsView === 'list') {
return artifacts.map(art => {
const cv = getCV(art);
const loc = art.location ? getRussianName(art.location) : '❌ не надет';
const setRus = artifactSetNames[art.setKey] || art.setKey;
const slotRus = slotNames[art.slotKey] || art.slotKey;
const mainStatRus = statNames[art.mainStatKey] || art.mainStatKey;
return `<div class="artifact-card" style="flex-direction:column; align-items:stretch;">
<div style="display:flex; justify-content:space-between;"><div><b>${setRus}</b> · ${slotRus}</div><div class="artifact-mainstat">${mainStatRus} +${art.level}</div></div>
<div style="display:flex; gap:10px; flex-wrap:wrap;">${art.substats.map(s => `<span style="background:rgba(255,255,255,0.06); border:1px solid rgba(255,255,255,0.1); padding:6px 14px; border-radius:40px; font-size:12px;">${statNames[s.key] || s.key} +${s.value}</span>`).join('')}</div>
<div style="display:flex; justify-content:space-between; color:var(--text-dim);"><span>CV: <b>${cv.toFixed(1)}</b></span><span>📌 ${loc}</span></div>
</div>`;
}).join('');
} else {
return artifacts.map(art => {
const cv = getCV(art);
const loc = art.location ? getRussianName(art.location) : '❌ не надет';
const setRus = artifactSetNames[art.setKey] || art.setKey;
const slotRus = slotNames[art.slotKey] || art.slotKey;
const mainStatRus = statNames[art.mainStatKey] || art.mainStatKey;
return `<div class="artifact-card">
<div style="min-width:160px;"><b>${setRus}</b><br><span style="color:var(--text-dim);">${slotRus}</span></div>
<div class="artifact-mainstat">${mainStatRus} +${art.level}</div>
<div style="flex:1; display:flex; gap:8px; flex-wrap:wrap;">${art.substats.map(s => `<span style="background:rgba(255,255,255,0.06); border:1px solid rgba(255,255,255,0.1); padding:6px 12px; border-radius:40px; font-size:11px;">${statNames[s.key] || s.key} +${s.value}</span>`).join('')}</div>
<div>CV: <b>${cv.toFixed(1)}</b></div>
<div style="color:var(--text-dim);">📌 ${loc}</div>
</div>`;
}).join('');
}
}
function groupArtifacts() { renderArtifacts(); }
// ==================== TEAMS ====================
function renderTeams() {
if (!localData?.characters) { document.getElementById('team-suggestions').innerHTML = '<div class="empty">Сначала загрузите персонажей.</div>'; return; }
const owned = localData.characters.map(c => c.key);
const container = document.getElementById('team-suggestions');
container.innerHTML = metaTeams.map(t => {
const hasChars = t.chars.map(c => owned.includes(c));
const ownedCount = hasChars.filter(Boolean).length;
const missing = t.chars.filter((c,i) => !hasChars[i]).map(c => getRussianName(c));
let resGlow = 'rgba(139,92,246,0.3)';
if (t.resonance.includes('Пиро')) resGlow = 'rgba(255,107,74,0.4)';
else if (t.resonance.includes('Гидро')) resGlow = 'rgba(59,130,246,0.4)';
else if (t.resonance.includes('Крио')) resGlow = 'rgba(56,189,248,0.4)';
else if (t.resonance.includes('Электро')) resGlow = 'rgba(168,85,247,0.4)';
else if (t.resonance.includes('Дендро')) resGlow = 'rgba(132,204,22,0.4)';
else if (t.resonance.includes('Гео')) resGlow = 'rgba(251,191,36,0.4)';
return `<div class="synergy-card" style="--res-glow: ${resGlow};">
<div style="display:flex; align-items:center; gap:12px;"><span style="font-size:32px;">${t.name.split(' ')[0]}</span>
<div><h3 style="font-size:20px;">${t.fullName}</h3><span style="background:rgba(139,92,246,0.2); padding:4px 12px; border-radius:40px; font-size:12px;">Мета ${t.meta}</span></div></div>
<div style="display:flex; gap:16px; flex-wrap:wrap; margin:16px 0;">
${t.chars.map((c,i) => { const ownedHere = hasChars[i]; const rusName = getRussianName(c);
return `<span style="display:inline-flex; align-items:center; gap:8px; background:${ownedHere ? 'rgba(16,185,129,0.15)' : 'rgba(239,68,68,0.15)'}; border:1px solid ${ownedHere ? 'rgba(16,185,129,0.3)' : 'rgba(239,68,68,0.3)'}; padding:6px 16px; border-radius:40px;">${rusName} ${ownedHere ? '✅' : '❌'}</span>`; }).join('')}
</div>
<div style="display:grid; grid-template-columns:repeat(3,1fr); gap:16px; background:rgba(0,0,0,0.25); border-radius:var(--radius-md); padding:16px;">
<div><span style="color:var(--accent);">🎵 Резонанс:</span> ${t.resonance}</div>
<div><span style="color:var(--accent);">⚡ Реакции:</span> ${t.reactions}</div>
<div><span style="color:var(--accent);">✨ Баффы:</span> ${t.buffs}</div>
</div>
<div style="margin-top:16px; color:var(--text-soft); background:rgba(0,0,0,0.15); padding:16px; border-radius:var(--radius-md);">💬 ${t.description}</div>
<div style="margin-top:16px; display:flex; justify-content:space-between; align-items:center;">
<span>⭐ В наличии: <b>${ownedCount}/${t.chars.length}</b> ${missing.length ? `❌ Нет: ${missing.join(', ')}` : '✅ Полный отряд'}</span>
<button class="btn btn-outline" onclick="saveTeamFromAI('${t.fullName}', ${JSON.stringify(t.chars)})">💾 Сохранить</button>
</div>
</div>`;
}).join('');
}
// ==================== ACCOUNT ASSESSMENT ====================
function renderAssessment() {
if (!localData?.characters) { document.getElementById('assessment-content').innerHTML = '<div class="empty">Загрузите GOOD‑файл.</div>'; return; }
const chars = localData.characters;
const arts = localData.artifacts || [];
const chars80 = chars.filter(c => c.level >= 80).length;
const chars90 = chars.filter(c => c.level >= 90).length;
let totalCV = 0, cvCount = 0;
chars.forEach(c => { const equipped = arts.filter(a => a.location === c.key); const charCV = equipped.reduce((s, a) => s + getCV(a), 0); if (equipped.length > 0) { totalCV += charCV; cvCount++; } });
const avgCV = cvCount > 0 ? totalCV / cvCount : 0;
const keySupports = ['Zhongli', 'Bennett', 'Kazuha', 'Nahida', 'Furina', 'Kokomi', 'Xingqiu', 'Yelan'];
const keyDPS = ['Neuvillette', 'Arlecchino', 'Alhaitham', 'HuTao', 'RaidenShogun', 'Wriothesley', 'Clorinde', 'Navia', 'Ganyu', 'Xiao'];
const ownedSupports = chars.filter(c => keySupports.includes(c.key)).length;
const ownedDPS = chars.filter(c => keyDPS.includes(c.key)).length;
const arts20 = arts.filter(a => a.level >= 20).length;
let abyssScore = 0;
if (chars80 >= 12) abyssScore += 25; else if (chars80 >= 10) abyssScore += 20; else if (chars80 >= 8) abyssScore += 15; else abyssScore += 5;
if (ownedSupports >= 6) abyssScore += 25; else if (ownedSupports >= 4) abyssScore += 20; else if (ownedSupports >= 2) abyssScore += 10;
if (ownedDPS >= 4) abyssScore += 20; else if (ownedDPS >= 2) abyssScore += 15; else if (ownedDPS >= 1) abyssScore += 8;
if (avgCV > 180) abyssScore += 15; else if (avgCV > 160) abyssScore += 12; else if (avgCV > 140) abyssScore += 8; else if (avgCV > 120) abyssScore += 4;
if (arts20 >= 16) abyssScore += 15; else if (arts20 >= 12) abyssScore += 10; else if (arts20 >= 8) abyssScore += 5;
abyssScore = Math.min(100, abyssScore);
let abyssResult = abyssScore >= 90 ? '⭐ 36 ЗВЁЗД' : abyssScore >= 80 ? '✨ 35-36 звёзд' : abyssScore >= 70 ? '⚔️ 33-35 звёзд' : abyssScore >= 60 ? '🛡️ 30-33 звезды' : abyssScore >= 50 ? '⚠️ 27-30 звёзд' : '❌ Менее 27 звёзд';
let mgScore = 0;
const mgDPS = ['Arlecchino', 'Neuvillette', 'Navia', 'Clorinde', 'Wriothesley', 'HuTao', 'Alhaitham'];
const ownedMgDPS = chars.filter(c => mgDPS.includes(c.key)).length;
if (ownedMgDPS >= 2) mgScore += 40; else if (ownedMgDPS >= 1) mgScore += 20;
if (chars90 >= 4) mgScore += 30; else if (chars90 >= 2) mgScore += 20; else mgScore += 10;
if (avgCV > 170) mgScore += 30; else if (avgCV > 150) mgScore += 20; else mgScore += 5;
mgScore = Math.min(100, mgScore);
let mgResult = mgScore >= 80 ? '✅ Мрачный натиск — ПРОЙДЕН' : mgScore >= 65 ? '⚠️ Мрачный натиск — возможно, но сложно' : '❌ Мрачный натиск — НЕ ПРОЙТИ';
const html = `
<div class="assessment-card" style="border-left-color: var(--accent);">
<h3 style="font-size:24px;">📊 Общая статистика</h3>
<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(180px,1fr)); gap:20px; margin-top:20px;">
<div style="background:rgba(0,0,0,0.2); padding:20px; border-radius:var(--radius-lg);"><div style="font-size:14px; color:var(--text-dim);">Персонажи</div><div style="font-size:36px; font-weight:800;">${chars.length}</div></div>
<div style="background:rgba(0,0,0,0.2); padding:20px; border-radius:var(--radius-lg);"><div style="font-size:14px; color:var(--text-dim);">Уровень 80+</div><div style="font-size:36px; font-weight:800;">${chars80}</div><div style="font-size:13px;">90 ур: ${chars90}</div></div>
<div style="background:rgba(0,0,0,0.2); padding:20px; border-radius:var(--radius-lg);"><div style="font-size:14px; color:var(--text-dim);">Средний CV</div><div style="font-size:36px; font-weight:800;">${avgCV.toFixed(1)}</div></div>
<div style="background:rgba(0,0,0,0.2); padding:20px; border-radius:var(--radius-lg);"><div style="font-size:14px; color:var(--text-dim);">Артефакты +20</div><div style="font-size:36px; font-weight:800;">${arts20}</div></div>
</div>
</div>
<div class="assessment-card" style="border-left-color: var(--pyro);">
<div style="display:flex; justify-content:space-between;"><h3 style="font-size:24px;">⚔️ Бездна 12-3</h3><span style="font-size:42px; font-weight:800; color:${abyssScore>=80?'var(--success)':abyssScore>=60?'var(--warning)':'var(--error)'};">${abyssScore}</span></div>
<div style="font-size:22px; font-weight:800; margin:12px 0 4px;">${abyssResult}</div>
<div style="background:rgba(0,0,0,0.2); border-radius:var(--radius-md); padding:16px; margin-top:16px;">
<div>🎯 Топ-саппорты: ${ownedSupports}/8</div>
<div>⚡ Топ-ДД: ${ownedDPS}/10</div>
<div>⭐ CV: ${avgCV > 160 ? 'отличный' : avgCV > 140 ? 'хороший' : avgCV > 120 ? 'средний' : 'низкий'}</div>
</div>
</div>
<div class="assessment-card" style="border-left-color: var(--cryo);">
<div style="display:flex; justify-content:space-between;"><h3 style="font-size:24px;">🌑 Мрачный натиск</h3><span style="font-size:42px; font-weight:800; color:${mgScore>=80?'var(--success)':mgScore>=65?'var(--warning)':'var(--error)'};">${mgScore}</span></div>
<div style="font-size:22px; font-weight:800; margin:12px 0 4px;">${mgResult}</div>
<div style="margin-top:16px; color:var(--text-dim);">💡 Рекомендованы: Арлекино, Нёвиллет, Навия, Клоринда, Ризли</div>
</div>
`;
document.getElementById('assessment-content').innerHTML = html;
}
// ==================== FILTERS, VIEW, THEME ====================
function applyFilters() { renderChars(); }
function setCharsView(view) { charsView = view; localStorage.setItem('nexus_chars_view', view); document.querySelectorAll('#view-chars .view-toggle-btn').forEach(btn => btn.classList.toggle('active', btn.dataset.view === view)); renderChars(); }
function setArtifactsView(view) { artifactsView = view; localStorage.setItem('nexus_artifacts_view', view); document.querySelectorAll('#view-artifacts .view-toggle-btn').forEach(btn => btn.classList.toggle('active', btn.dataset.view === view)); renderArtifacts(); }
function changeTheme(theme) {
if (theme === 'light') {
document.documentElement.style.setProperty('--bg-main', '#f3f4f6');
document.documentElement.style.setProperty('--bg-side', '#e5e7eb');
document.documentElement.style.setProperty('--bg-card', '#ffffff');
document.documentElement.style.setProperty('--text-main', '#111827');
document.documentElement.style.setProperty('--text-dim', '#4b5563');
}
else if (theme === 'darker') {
document.documentElement.style.setProperty('--bg-main', '#030712');
document.documentElement.style.setProperty('--bg-side', '#0a0c12');
document.documentElement.style.setProperty('--bg-card', '#111827');
}
else {
document.documentElement.style.setProperty('--bg-main', '#0a0c12');
document.documentElement.style.setProperty('--bg-side', '#0f1117');
document.documentElement.style.setProperty('--bg-card', '#1a1e2a');
document.documentElement.style.setProperty('--text-main', '#ffffff');
document.documentElement.style.setProperty('--text-dim', '#a0a8c0');
}
}
// ==================== IMPORT GOOD ====================
document.getElementById('fileIn').onchange = e => {
const file = e.target.files[0]; if (!file) return;
const reader = new FileReader();
reader.onload = ev => {
try {
const newData = JSON.parse(ev.target.result);
if (confirm('Сохранить в новый профиль? (Отмена — заменить текущие)')) {
const profileName = prompt('Название профиля:', `Профиль ${profiles.length + 1}`);
if (profileName) { profiles.push({ id: Date.now(), name: profileName, data: newData, date: new Date().toLocaleDateString('ru-RU') }); localStorage.setItem('nexus_profiles', JSON.stringify(profiles)); }
}
localData = newData; enrichCharData(); saveToDB(); renderChars();
if (localData.artifacts) {
const sets = [...new Set(localData.artifacts.map(a => a.setKey))].sort();
const setSelect = document.getElementById('art-set-filter');
setSelect.innerHTML = '<option value="all">Все сеты</option>' + sets.map(s => `<option value="${s}">${artifactSetNames[s] || s}</option>`).join('');
}
switchTab('chars'); renderAssessment(); alert('✅ Данные загружены!');
} catch(ex) { alert('❌ Неверный GOOD‑файл'); }
};
reader.readAsText(file);
};
// ==================== EXPORT ====================
function exportGOOD() { if (!localData) return alert('Нет данных'); const dataStr = JSON.stringify(localData, null, 2); const blob = new Blob([dataStr], {type:'application/json'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'genshin_nexus_export.json'; a.click(); }
// ==================== INITIALIZATION ====================
function initSettings() {
// Load keys into fields
document.getElementById('hf-api-key').value = hfApiKey;
document.getElementById('openai-api-key').value = openaiApiKey;
document.getElementById('gemini-api-key').value = geminiApiKey;
document.getElementById('deepseek-api-key').value = deepseekApiKey;
document.getElementById('custom-api-key').value = customApiKey;
document.getElementById('custom-base-url').value = customBaseUrl;
document.getElementById('custom-model').value = customModel;
document.getElementById('hf-model').value = hfModel;
document.getElementById('openai-model').value = openaiModel;
document.getElementById('gemini-model').value = geminiModel;
document.getElementById('deepseek-model').value = deepseekModel;
// Set active provider
setAIProvider(currentProvider);
// View
document.querySelectorAll('#view-chars .view-toggle-btn').forEach(btn => btn.classList.toggle('active', btn.dataset.view === charsView));
document.querySelectorAll('#view-artifacts .view-toggle-btn').forEach(btn => btn.classList.toggle('active', btn.dataset.view === artifactsView));
renderProfiles(); renderMyTeams();
}
// Global functions
window.switchTab = switchTab;
window.analyzeCharacter = analyzeCharacter;
window.aiTeamBuilder = aiTeamBuilder;
window.closeAI = closeAI;
window.clearDB = clearDB;
window.renderArtifacts = renderArtifacts;
window.renderTeams = renderTeams;
window.saveAllApiKeys = saveAllApiKeys;
window.testCurrentApi = testCurrentApi;
window.setAIProvider = setAIProvider;
window.exportGOOD = exportGOOD;
window.applyFilters = applyFilters;
window.setCharsView = setCharsView;
window.setArtifactsView = setArtifactsView;
window.renderChars = renderChars;
window.openLegend = openLegend;
window.closeLegend = closeLegend;
window.openCVPopup = openCVPopup;
window.closeCVPopup = closeCVPopup;
window.renderAssessment = renderAssessment;
window.changeTheme = changeTheme;
window.generateRandomTeam = generateRandomTeam;
window.saveTeamFromAI = saveTeamFromAI;
window.saveCurrentTeam = saveCurrentTeam;
window.deleteTeam = deleteTeam;
window.renderMyTeams = renderMyTeams;
window.createNewProfile = createNewProfile;
window.loadProfile = loadProfile;
window.deleteProfile = deleteProfile;
window.renderProfiles = renderProfiles;
window.showProfileSelector = showProfileSelector;
window.groupArtifacts = groupArtifacts;
document.addEventListener('DOMContentLoaded', initSettings);
</script>
</body>
</html>