Files
suno-mcp-server/NexusAI/packages/admin/public/index.html
T
OpenCode 8bd2e89907 feat: STT transcripts, model catalog+fuzzy resolve, UI audio player & auto-refresh
Core:
- ModelCatalog: periodic background sync of provider models + fuzzy resolve.
  Misspelled model ids map to the nearest match (cheapest on ties); no match
  raises ModelResolutionError with cheapest-first suggestions for the agent.

mcp-server:
- nexus_stt now writes the transcript to a Markdown file in the media dir so it
  appears in the gallery and is readable in the modal.
- media handler resolves model via catalog before generating; returns a note
  when substituting, or a BAD_REQUEST with available models when nothing fits.
- context wires a ModelCatalog per media provider with auto-sync.

admin:
- /media serves correct Content-Type (incl. text/markdown for transcripts).
- Gallery: audio play button on cards, dedicated audio player in modal,
  text/transcript tiles that fetch and render the .md in the modal.
- Header: auto-refresh toggle (5/10/30s) that refreshes the active tab
  without a full page reload; pauses while a modal is open.
2026-08-18 12:25:42 +07:00

225 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="ru" data-theme="dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>NexusAI Admin</title>
<style>
:root[data-theme="dark"] {
--bg: #0f1115; --panel: #171a21; --panel2: #1f242e; --line: #2a2f3a;
--text: #e6e9ef; --muted: #9aa4b2; --accent: #4f8cff; --ok: #35c46a;
--warn: #f0b429; --err: #f26d6d; --shadow: rgba(0,0,0,.5);
}
:root[data-theme="light"] {
--bg: #f4f6fa; --panel: #ffffff; --panel2: #f0f2f7; --line: #dce1ea;
--text: #1a1f2b; --muted: #5c6675; --accent: #2f6fed; --ok: #1a9e51;
--warn: #b3810f; --err: #d63d3d; --shadow: rgba(0,0,0,.18);
}
* { box-sizing: border-box; }
body { margin: 0; background: var(--bg); color: var(--text);
font: 14px/1.5 system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
transition: background .15s, color .15s; }
header { padding: 14px 20px; border-bottom: 1px solid var(--line);
display: flex; align-items: center; gap: 16px; }
header h1 { font-size: 16px; margin: 0; font-weight: 600; }
.badge { font-size: 12px; color: var(--muted); }
header .spacer { flex: 1; }
#themeToggle { background: var(--panel2); border: 1px solid var(--line); color: var(--text);
border-radius: 8px; padding: 6px 12px; cursor: pointer; font: inherit; }
nav { display: flex; gap: 6px; padding: 12px 20px 0; }
nav button { background: var(--panel); color: var(--muted); border: 1px solid var(--line);
padding: 8px 14px; border-radius: 8px 8px 0 0; cursor: pointer; }
nav button.active { background: var(--panel2); color: var(--text); border-bottom-color: var(--panel2); }
main { padding: 20px; }
.card { background: var(--panel); border: 1px solid var(--line); border-radius: 10px;
padding: 16px; margin-bottom: 16px; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 14px; }
.stat { background: var(--panel2); border: 1px solid var(--line); border-radius: 10px; padding: 14px; }
.stat .n { font-size: 22px; font-weight: 700; }
.stat .l { color: var(--muted); font-size: 12px; }
table { width: 100%; border-collapse: collapse; }
th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--line); font-size: 13px; }
th { color: var(--muted); font-weight: 500; }
input, select { background: var(--panel2); color: var(--text); border: 1px solid var(--line);
border-radius: 8px; padding: 8px 10px; font: inherit; width: 100%; }
label { display: block; font-size: 12px; color: var(--muted); margin: 8px 0 4px; }
button.act { background: var(--accent); color: #fff; border: none; border-radius: 8px;
padding: 8px 14px; cursor: pointer; font: inherit; }
button.ghost { background: transparent; color: var(--text); border: 1px solid var(--line);
border-radius: 8px; padding: 6px 10px; cursor: pointer; }
button.danger { color: var(--err); border-color: var(--err); }
button:disabled { opacity: .45; cursor: not-allowed; }
.row { display: flex; gap: 10px; flex-wrap: wrap; align-items: end; }
.row > div { flex: 1; min-width: 140px; }
.pill { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 11px; }
.pill.on { background: rgba(53,196,106,.15); color: var(--ok); }
.pill.off { background: rgba(242,109,109,.15); color: var(--err); }
.pill.def { background: rgba(79,140,255,.15); color: var(--accent); }
.gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 14px; }
.tile { background: var(--panel2); border: 1px solid var(--line); border-radius: 10px;
overflow: hidden; position: relative; }
.tile .thumb { cursor: pointer; }
.tile img { width: 100%; height: 150px; object-fit: cover; display: block; background: #000; }
.tile .meta { padding: 8px 10px; font-size: 12px; }
.tile .meta .m { color: var(--muted); }
.tile .del { position: absolute; top: 6px; right: 6px; background: var(--panel);
border: 1px solid var(--line); color: var(--err); border-radius: 6px; padding: 2px 8px;
cursor: pointer; font-size: 12px; opacity: .85; }
.tile .del:hover { opacity: 1; }
.pager { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.muted { color: var(--muted); }
.hidden { display: none; }
.toast { position: fixed; right: 16px; bottom: 16px; background: var(--panel2);
border: 1px solid var(--line); padding: 10px 14px; border-radius: 8px; box-shadow: 0 6px 24px var(--shadow); }
audio { width: 100%; }
code { background: var(--panel2); padding: 1px 5px; border-radius: 4px; }
/* Modal */
.modal-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,.7);
display: flex; align-items: center; justify-content: center; padding: 24px; z-index: 50; }
.modal { background: var(--panel); border: 1px solid var(--line); border-radius: 12px;
max-width: 92vw; max-height: 92vh; overflow: auto; box-shadow: 0 20px 60px var(--shadow); }
.modal-head { display: flex; align-items: center; gap: 12px; padding: 12px 16px;
border-bottom: 1px solid var(--line); position: sticky; top: 0; background: var(--panel); }
.modal-head .spacer { flex: 1; }
.modal-body { padding: 16px; }
.modal-body img, .modal-body video { max-width: 88vw; max-height: 72vh; display: block; margin: 0 auto; }
.modal-body audio { width: 60vw; max-width: 600px; }
.kv { color: var(--muted); font-size: 12px; margin-top: 10px; word-break: break-all; }
/* Auto-refresh control */
.autorefresh { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--muted); }
.autorefresh select { width: auto; padding: 4px 6px; }
.autorefresh input[type="checkbox"] { width: auto; }
/* Audio card thumb + play button */
.audio-thumb, .text-thumb { height: 150px; display: flex; align-items: center; justify-content: center;
cursor: pointer; background: linear-gradient(135deg, var(--panel2), var(--panel)); }
.text-thumb.err { background: rgba(242,109,109,.08); }
.play-btn { width: 56px; height: 56px; border-radius: 50%; border: none; cursor: pointer;
background: var(--accent); color: #fff; font-size: 20px; line-height: 1;
box-shadow: 0 6px 18px var(--shadow); transition: transform .1s; }
.play-btn:hover { transform: scale(1.08); }
.doc-icon { font-size: 42px; opacity: .8; }
/* Modal audio player */
.audio-player { display: flex; flex-direction: column; align-items: center; gap: 16px; padding: 20px; }
.audio-visual { font-size: 64px; }
/* Transcript / doc view */
.doc-view { white-space: pre-wrap; word-break: break-word; font: 13px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace;
background: var(--panel2); border: 1px solid var(--line); border-radius: 8px; padding: 16px;
max-width: 80vw; max-height: 70vh; overflow: auto; text-align: left; }
</style>
</head>
<body>
<header>
<h1>NexusAI Admin</h1>
<span class="badge" id="cryptoBadge"></span>
<span class="spacer"></span>
<label class="autorefresh" title="Автообновление активной вкладки">
<input type="checkbox" id="autoRefresh" /> Авто-обновление
<select id="autoRefreshInterval">
<option value="5000">5с</option>
<option value="10000" selected>10с</option>
<option value="30000">30с</option>
</select>
</label>
<button id="themeToggle" title="Переключить тему">🌙 Тёмная</button>
</header>
<nav>
<button data-tab="usage" class="active">Потребление</button>
<button data-tab="gallery">Галерея</button>
<button data-tab="providers">Провайдеры</button>
</nav>
<main>
<section id="tab-usage">
<div class="card">
<div class="row" style="margin-bottom:12px">
<div style="max-width:200px">
<label>Период</label>
<select id="statsRange">
<option value="">Всё время</option>
<option value="1">1 день</option>
<option value="7" selected>7 дней</option>
<option value="30">30 дней</option>
</select>
</div>
</div>
<div class="grid" id="statCards"></div>
</div>
<div class="card">
<h3 style="margin-top:0">По провайдерам</h3>
<table id="byProvider"><thead><tr><th>Провайдер</th><th>Кол-во</th><th>Стоимость, $</th></tr></thead><tbody></tbody></table>
</div>
<div class="card">
<h3 style="margin-top:0">По возможностям</h3>
<table id="byCapability"><thead><tr><th>Возможность</th><th>Кол-во</th><th>Стоимость, $</th></tr></thead><tbody></tbody></table>
</div>
<div class="card">
<h3 style="margin-top:0">По дням</h3>
<table id="byDay"><thead><tr><th>День</th><th>Кол-во</th><th>Стоимость, $</th></tr></thead><tbody></tbody></table>
</div>
</section>
<section id="tab-gallery" class="hidden">
<div class="card">
<div class="row">
<div style="max-width:220px">
<label>Фильтр по возможности</label>
<select id="galleryFilter">
<option value="">Все</option>
<option value="image">image</option>
<option value="tts">tts</option>
<option value="music">music</option>
<option value="video">video</option>
<option value="stt">stt</option>
<option value="vision">vision</option>
<option value="search">search</option>
</select>
</div>
<div style="max-width:180px">
<label>Элементов на странице</label>
<select id="pageSize">
<option value="50" selected>50</option>
<option value="100">100</option>
<option value="150">150</option>
<option value="200">200</option>
</select>
</div>
<div style="max-width:120px"><label>&nbsp;</label><button class="ghost" id="galleryReload">Обновить</button></div>
</div>
<div class="pager" style="margin-top:12px">
<button class="ghost" id="prevPage">← Назад</button>
<span class="muted" id="pageInfo">Страница 1</span>
<button class="ghost" id="nextPage">Вперёд →</button>
</div>
</div>
<div class="gallery" id="gallery"></div>
</section>
<section id="tab-providers" class="hidden">
<div class="card">
<h3 style="margin-top:0">Добавить / обновить провайдера</h3>
<div class="row">
<div><label>Имя</label><input id="pName" placeholder="nordrouter" /></div>
<div><label>Тип</label><select id="pType"></select></div>
<div><label>Base URL (опц.)</label><input id="pBaseUrl" placeholder="https://nordrouter.com" /></div>
</div>
<div class="row">
<div><label>API ключ (опц., шифруется)</label><input id="pKey" type="password" placeholder="sk-..." /></div>
<div style="max-width:140px"><label>По умолчанию</label><select id="pDefault"><option value="false">Нет</option><option value="true">Да</option></select></div>
<div style="max-width:140px"><label>Включён</label><select id="pEnabled"><option value="true">Да</option><option value="false">Нет</option></select></div>
</div>
<div style="margin-top:12px"><button class="act" id="pSave">Сохранить</button></div>
</div>
<div class="card">
<h3 style="margin-top:0">Провайдеры</h3>
<table id="providersTable">
<thead><tr><th>Имя</th><th>Тип</th><th>Ключ</th><th>Статус</th><th>Действия</th></tr></thead>
<tbody></tbody>
</table>
</div>
</section>
</main>
<div id="modalRoot"></div>
<script src="app.js"></script>
</body>
</html>