Files
Maven/cmd/mavweb/static/index.html
kami 7a95097cc7 feat: web UI redesign with PWA support, voice page, and mobile layout
- Redesign dash/history/notifications/trace pages with unified CSS framework
  (ui.css): cards, badges, scrollable tables, responsive grid, PWA shell.
- Add /voice page with chat-bubble transcript, listening indicator, and
  push-to-talk button for voice interaction.
- Add mavweb.js: client-side JS for live transcript polling, auto-scroll,
  push-to-talk, and service-worker registration.
- Update PWA manifest, icon, and service-worker for installable web app.
- Add WebAuthn credential management (register/list/delete) with modal UI.
- Update main.go: /voice route, WebAuthn handlers, new page navigation.
- Update handlers_test.go for new voice and WebAuthn routes.
2026-07-10 15:49:18 +04:00

158 lines
9.6 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<meta name="theme-color" content="#14110D">
<link rel="manifest" href="/manifest.json">
<link rel=stylesheet href=/ui.css>
<script defer src="/mavweb.js"></script>
<script defer src="/app.js"></script>
<style>
/* Voice page — fits inside the Ethos content area */
.voice{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1.5rem;padding:1rem 0}
/* PTT Button */
#btn{width:140px;height:140px;border-radius:50%;border:3px solid var(--ethos-accent);background:var(--ethos-surface0);color:var(--ethos-accent);font-size:1rem;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all var(--ethos-motion);user-select:none;-webkit-tap-highlight-color:transparent;touch-action:manipulation}
#btn:active,#btn.active{background:rgba(111,191,139,.12);border-color:var(--ethos-green);color:var(--ethos-green);transform:scale(1.05)}
#btn:disabled{opacity:.3;border-color:var(--ethos-border)}
#status{font-size:var(--ethos-fs-sm);color:var(--ethos-muted);min-height:1.2em}
#log{width:100%;max-width:480px;max-height:40vh;overflow-y:auto;font-size:var(--ethos-fs-sm);color:var(--ethos-muted);line-height:1.6;padding:var(--ethos-space-2);border-top:1px solid var(--ethos-border)}
#log .reply{color:var(--ethos-green)}
#log .error{color:var(--ethos-red)}
#log .push{color:var(--ethos-purple)}
/* Cheatsheet */
#cheat{width:100%;max-width:480px;font-size:var(--ethos-fs-sm);border-top:1px solid var(--ethos-border);color:var(--ethos-muted)}
#cheat summary{cursor:pointer;padding:var(--ethos-space-2) 0;color:var(--ethos-accent);letter-spacing:.05em;text-transform:uppercase;font-size:var(--ethos-fs-xs);user-select:none}
#cheat dl{display:grid;grid-template-columns:auto 1fr;gap:var(--ethos-space-1) var(--ethos-space-3);padding:var(--ethos-space-1) 0 var(--ethos-space-3)}
#cheat dt{color:var(--ethos-green);font-weight:500;white-space:nowrap}
#cheat dd{color:var(--ethos-secondary);line-height:1.5}
#cheat dd span{color:var(--ethos-muted)}
.cheat-en{display:revert}
.cheat-ru{display:none}
html[data-lang=ru] .cheat-en{display:none}
html[data-lang=ru] .cheat-ru{display:revert}
/* Lang toggle */
.lang-toggle{display:flex;gap:0;justify-content:center;margin-top:1rem}
.lang-toggle span{font-size:var(--ethos-fs-xs);color:var(--ethos-muted);cursor:pointer;padding:.2rem .5rem;letter-spacing:.1em;text-transform:uppercase;border:1px solid var(--ethos-border);transition:color var(--ethos-motion);user-select:none}
.lang-toggle span:first-child{border-radius:var(--ethos-radius) 0 0 var(--ethos-radius);border-right:none}
.lang-toggle span:last-child{border-radius:0 var(--ethos-radius) var(--ethos-radius) 0;border-left:none}
.lang-toggle span.active{color:var(--ethos-accent);border-color:var(--ethos-accent);background:var(--ethos-accent-dim)}
.lang-toggle span:hover{color:var(--ethos-text)}
</style>
</head>
<body>
<div class=shell>
<header class=topbar>
<div class=breadcrumbs>
<span class=current>Voice</span>
</div>
<div class=topbar-actions>
<div class=search-trigger onclick="window.__openSearch()" role=button tabindex=0>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
Search
<span class=kbd-hint>Ctrl+/</span>
</div>
<button class=icon-btn onclick="window.__openPalette()" title="Command Palette (Ctrl+K)" aria-label="Command Palette">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 7 4 4 7 4"/><polyline points="17 4 20 4 20 7"/><polyline points="20 17 20 20 17 20"/><polyline points="7 20 4 20 4 17"/></svg>
</button>
<span class=conn-status>
<span class="dot online" id=connDot></span>
</span>
</div>
</header>
<div class=shell-body>
<aside class=sidebar>
<div class=sidebar-section>
<div class=sidebar-label>Workspace</div>
<a href=/dash><span class=icon><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/></svg></span><span>Dashboard</span></a>
</div>
<div class=sidebar-section>
<div class=sidebar-label>Infrastructure</div>
<a href=/history><span class=icon><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg></span><span>History</span></a>
</div>
<div class=sidebar-section>
<div class=sidebar-label>Automation</div>
<a href=/trace><span class=icon><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg></span><span>Rule Trace</span></a>
<a href=/notifications><span class=icon><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></svg></span><span>Notifications</span></a>
</div>
<div class=sidebar-section>
<div class=sidebar-label>AI</div>
<a href=/ class=active><span class=icon><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg></span><span>Voice</span></a>
</div>
<div class=sidebar-section>
<div class=sidebar-label>Settings</div>
<a href=/tools><span class=icon><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg></span><span>Tools</span></a>
<a href=/auth/passkey><span class=icon><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg></span><span>Passkey</span></a>
</div>
</aside>
<main class=content>
<div class=voice>
<div id="status">tap &amp; hold to speak</div>
<button id="btn" type="button">&#x1f399;</button>
<div id="log"></div>
<details id="cheat">
<summary>Cheatsheet</summary>
<dl>
<dt>Act</dt>
<dd class="cheat-ru">включи свет · сделай громче</dd>
<dd class="cheat-en">restart nginx · turn on the light</dd>
<dt>Reminder</dt>
<dd class="cheat-ru">напомни через 4 часа размяться</dd>
<dd class="cheat-en">remind me tomorrow at 8am to call the doctor</dd>
<dt>Fact</dt>
<dd class="cheat-ru">выпил кофе · вес 73 кг</dd>
<dd class="cheat-en">ran 3 kilometers</dd>
<dt>Note</dt>
<dd class="cheat-ru">запиши рецепт блинов</dd>
<dd class="cheat-en">note: add a backup job for the db</dd>
<dt>Query</dt>
<dd class="cheat-ru">какой сегодня день · какой прогноз погоды</dd>
<dd class="cheat-en">what version are you · what's the weather</dd>
<dt>System</dt>
<dd class="cheat-ru">включи тихий режим · как загрузка системы</dd>
<dd class="cheat-en">quiet mode on · system load</dd>
</dl>
</details>
<div class=lang-toggle>
<span class="active" data-lang="ru">RU</span>
<span data-lang="en">EN</span>
</div>
</div>
</main>
<aside class=inspector id=inspector>
<div class=inspector-inner>
<div class=inspector-header>
<span id=inspectorTitle>Details</span>
<button class=inspector-close onclick="closeInspector()" aria-label="Close inspector">&times;</button>
</div>
<div class=inspector-body id=inspectorBody></div>
</div>
</aside>
</div>
</div>
<script>
(function() {
var lang = new URLSearchParams(window.location.search).get('lang') || 'ru';
if (lang !== 'en') lang = 'ru';
document.documentElement.setAttribute('data-lang', lang);
document.querySelectorAll('.lang-toggle span').forEach(function(el) {
el.addEventListener('click', function() {
var l = el.dataset.lang;
document.documentElement.setAttribute('data-lang', l);
document.querySelectorAll('.lang-toggle span').forEach(function(b) { b.classList.remove('active'); });
el.classList.add('active');
var url = new URL(window.location);
url.searchParams.set('lang', l);
history.replaceState(null, '', url);
});
});
})();
</script>
</body>
</html>