feat(mavweb): /ecosystem page consuming Nexus/Praxis/Hexis + shell fixes
Add a read-only /ecosystem page that consumes the sibling services' JSON APIs (Nexus entities, Praxis attention, Hexis capabilities), fetched concurrently with honest per-panel error states. Siblings stay headless — mavweb is their human surface (arch §16). Wired via mavweb -nexus/-praxis/-hexis flags; mavweb joins the ecosystem compose network. Fix mobile horizontal overflow across all pages: .content is a flex child with default min-width:auto, so it refused to shrink below the tables' intrinsic width. min-width:0 lets wide tables pan inside .scroll instead of dragging the page sideways. Verified via CDP geometry check (scrollWidth === clientWidth at 430px). Also includes in-progress Ethos UI redesign, ecosystem deploy compose, and planning docs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
<h1>Dashboard</h1>
|
||||
<section class=card>
|
||||
<h2 class=card-title>
|
||||
<span class="dot dot-ok" style="margin-right:4px"></span>
|
||||
<span class="dot dot-ok" style="margin-right:6px"></span>
|
||||
presence
|
||||
</h2>
|
||||
<p>
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// The three sibling services Maven coordinates are headless JSON APIs (no web UI
|
||||
// of their own — arch §16: mavweb IS their human surface). This page is that
|
||||
// surface: a read-only observability panel that GETs each sibling's list
|
||||
// endpoints and renders them. Never mutates — enabling/executing stays on the
|
||||
// authed /tools + voice paths, never here.
|
||||
|
||||
// ecoURLs holds the sibling base URLs; empty ⇒ that panel shows "not configured".
|
||||
type ecoURLs struct {
|
||||
nexus, praxis, hexis string
|
||||
}
|
||||
|
||||
var ecoClient = &http.Client{Timeout: 6 * time.Second}
|
||||
|
||||
// getEco GETs path off base and decodes the JSON array into out. Returns a
|
||||
// human error string (empty on success) — honest per-panel state, no spinner.
|
||||
func getEco(ctx context.Context, base, path string, out any) string {
|
||||
if base == "" {
|
||||
return "not configured"
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, base+path, nil)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
resp, err := ecoClient.Do(req)
|
||||
if err != nil {
|
||||
return "unreachable"
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return fmt.Sprintf("http %d", resp.StatusCode)
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(out); err != nil {
|
||||
return "bad json"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Minimal projections of each sibling's list payload — only the columns the
|
||||
// panel shows. Extra JSON fields are ignored.
|
||||
type ecoEntity struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
DisplayName string `json:"display_name"`
|
||||
State string `json:"state"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type ecoItem struct {
|
||||
ID string `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
Title string `json:"title"`
|
||||
State string `json:"state"`
|
||||
Importance int `json:"importance"`
|
||||
LastSeenAt time.Time `json:"last_seen_at"`
|
||||
}
|
||||
|
||||
type ecoCap struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Provider string `json:"provider"`
|
||||
Operation string `json:"operation"`
|
||||
Risk string `json:"risk"`
|
||||
ReadOnly bool `json:"read_only"`
|
||||
}
|
||||
|
||||
type ecoPanel[T any] struct {
|
||||
Rows []T
|
||||
Err string
|
||||
}
|
||||
|
||||
type ecoData struct {
|
||||
Nexus ecoPanel[ecoEntity]
|
||||
Praxis ecoPanel[ecoItem]
|
||||
Hexis ecoPanel[ecoCap]
|
||||
}
|
||||
|
||||
func handleEcosystem(w http.ResponseWriter, r *http.Request, urls ecoURLs) {
|
||||
ctx := r.Context()
|
||||
var d ecoData
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(3)
|
||||
go func() { defer wg.Done(); d.Nexus.Err = getEco(ctx, urls.nexus, "/api/v1/entities?limit=50", &d.Nexus.Rows) }()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
d.Praxis.Err = getEco(ctx, urls.praxis, "/api/v1/items?limit=50", &d.Praxis.Rows)
|
||||
}()
|
||||
go func() { defer wg.Done(); d.Hexis.Err = getEco(ctx, urls.hexis, "/api/v1/capabilities", &d.Hexis.Rows) }()
|
||||
wg.Wait()
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
if err := ecosystemTmpl.Execute(w, d); err != nil {
|
||||
log.Printf("ecosystem render: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
{{template "shellTop" "ecosystem"}}
|
||||
<h1>Ecosystem</h1>
|
||||
<p class=hint>The sibling services Maven coordinates — read-only. Identity, attention, capabilities. Enabling & execution live on the authed surfaces, never here.</p>
|
||||
|
||||
<section class=card>
|
||||
<div class=section-header>
|
||||
<h2>Nexus <span class=card-sub>identity</span></h2>
|
||||
</div>
|
||||
{{with .Nexus}}
|
||||
{{if .Err}}<div class=empty>nexus — {{.Err}}</div>
|
||||
{{else if not .Rows}}<div class=empty>no entities yet.</div>
|
||||
{{else}}<div class=scroll><table class=mono><tr><th>id<th>type<th>name<th>state<th>updated</tr>
|
||||
{{range .Rows}}<tr><td class=key>{{.ID}}<td><span class=badge>{{.Type}}</span><td class=en>{{.DisplayName}}<td>{{.State}}<td>{{ago .UpdatedAt}}</tr>{{end}}
|
||||
</table></div>{{end}}
|
||||
{{end}}
|
||||
</section>
|
||||
|
||||
<section class=card>
|
||||
<div class=section-header>
|
||||
<h2>Praxis <span class=card-sub>attention</span></h2>
|
||||
</div>
|
||||
{{with .Praxis}}
|
||||
{{if .Err}}<div class=empty>praxis — {{.Err}}</div>
|
||||
{{else if not .Rows}}<div class=empty>nothing needs attention.</div>
|
||||
{{else}}<div class=scroll><table class=mono><tr><th>imp<th>kind<th>title<th>state<th>last seen</tr>
|
||||
{{range .Rows}}<tr><td>{{.Importance}}<td><span class=badge>{{.Kind}}</span><td class=en>{{.Title}}<td>{{.State}}<td>{{ago .LastSeenAt}}</tr>{{end}}
|
||||
</table></div>{{end}}
|
||||
{{end}}
|
||||
</section>
|
||||
|
||||
<section class=card>
|
||||
<div class=section-header>
|
||||
<h2>Hexis <span class=card-sub>capabilities</span></h2>
|
||||
</div>
|
||||
{{with .Hexis}}
|
||||
{{if .Err}}<div class=empty>hexis — {{.Err}}</div>
|
||||
{{else if not .Rows}}<div class=empty>no capabilities registered.</div>
|
||||
{{else}}<div class=scroll><table class=mono><tr><th>id<th>name<th>provider<th>operation<th>risk<th>access</tr>
|
||||
{{range .Rows}}<tr><td class=key>{{.ID}}<td class=en>{{.Name}}<td><span class=badge>{{.Provider}}</span><td>{{.Operation}}<td>{{.Risk}}<td>{{if .ReadOnly}}<span class="badge badge-ok">read-only</span>{{else}}<span class="badge badge-warn">mutating</span>{{end}}</tr>{{end}}
|
||||
</table></div>{{end}}
|
||||
{{end}}
|
||||
</section>
|
||||
|
||||
{{template "shellBottom"}}
|
||||
<script>
|
||||
setInterval(() => fetch('/ecosystem').then(r => r.text()).then(html => {
|
||||
const d = new DOMParser().parseFromString(html, 'text/html');
|
||||
document.querySelectorAll('main.content section.card').forEach((old, i) => {
|
||||
const nu = d.querySelectorAll('main.content section.card')[i];
|
||||
if (nu) old.replaceWith(nu);
|
||||
});
|
||||
}), 15000);
|
||||
</script>
|
||||
</html>
|
||||
@@ -17,7 +17,7 @@
|
||||
</table></div>
|
||||
{{else}}
|
||||
<div class=empty>
|
||||
<svg class=icon width="24" height="24" 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>
|
||||
<svg class=icon width="24" height="24"><use href="/ethos-icons.svg#i-clock"/></svg>
|
||||
<div>no facts recorded yet</div>
|
||||
<div class=hint>facts appear here when maven observes or records events</div>
|
||||
</div>
|
||||
|
||||
+51
-34
@@ -60,6 +60,9 @@ var remindersHTML string
|
||||
//go:embed voice.html
|
||||
var voiceHTML string
|
||||
|
||||
//go:embed ecosystem.html
|
||||
var ecosystemHTML string
|
||||
|
||||
// ── Ethos Workstation Shell ──
|
||||
//
|
||||
// Two template pieces that wrap every page:
|
||||
@@ -94,6 +97,12 @@ var sidebarSections = []struct {
|
||||
{Label: "Routines", URL: "/routines", Key: "routines"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Label: "Ecosystem",
|
||||
Pages: []struct{ Label, URL, Key string }{
|
||||
{Label: "Siblings", URL: "/ecosystem", Key: "ecosystem"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Label: "AI",
|
||||
Pages: []struct{ Label, URL, Key string }{
|
||||
@@ -145,31 +154,33 @@ func sidebarHTML(active string) template.HTML {
|
||||
return template.HTML(b.String())
|
||||
}
|
||||
|
||||
// pageIcon returns a lucide-style inline SVG icon path for the given page.
|
||||
// pageIcon returns an ethos-icons.svg <use> reference for the given page.
|
||||
func pageIcon(key string) string {
|
||||
switch key {
|
||||
case "dash":
|
||||
return `<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>`
|
||||
return `<svg class=icon width="14" height="14"><use href="/ethos-icons.svg#i-grid"/></svg>`
|
||||
case "history":
|
||||
return `<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>`
|
||||
return `<svg class=icon width="14" height="14"><use href="/ethos-icons.svg#i-clock"/></svg>`
|
||||
case "trace":
|
||||
return `<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>`
|
||||
return `<svg class=icon width="14" height="14"><use href="/ethos-icons.svg#i-wave"/></svg>`
|
||||
case "notifications":
|
||||
return `<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>`
|
||||
return `<svg class=icon width="14" height="14"><use href="/ethos-icons.svg#i-bell"/></svg>`
|
||||
case "reminders":
|
||||
return `<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="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>`
|
||||
return `<svg class=icon width="14" height="14"><use href="/ethos-icons.svg#i-calendar"/></svg>`
|
||||
case "routines":
|
||||
return `<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>`
|
||||
return `<svg class=icon width="14" height="14"><use href="/ethos-icons.svg#i-repeat"/></svg>`
|
||||
case "chat":
|
||||
return `<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="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>`
|
||||
return `<svg class=icon width="14" height="14"><use href="/ethos-icons.svg#i-message"/></svg>`
|
||||
case "voice":
|
||||
return `<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>`
|
||||
return `<svg class=icon width="14" height="14"><use href="/ethos-icons.svg#i-mic"/></svg>`
|
||||
case "ecosystem":
|
||||
return `<svg class=icon width="14" height="14"><use href="/ethos-icons.svg#i-grid"/></svg>`
|
||||
case "tools":
|
||||
return `<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>`
|
||||
return `<svg class=icon width="14" height="14"><use href="/ethos-icons.svg#i-settings"/></svg>`
|
||||
case "passkey":
|
||||
return `<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>`
|
||||
return `<svg class=icon width="14" height="14"><use href="/ethos-icons.svg#i-lock"/></svg>`
|
||||
default:
|
||||
return `<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="1"/></svg>`
|
||||
return `<svg class=icon width="14" height="14"><use href="/ethos-icons.svg#i-search"/></svg>`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,6 +203,8 @@ func pageTitle(key string) string {
|
||||
return "Chat"
|
||||
case "voice":
|
||||
return "Voice"
|
||||
case "ecosystem":
|
||||
return "Ecosystem"
|
||||
case "tools":
|
||||
return "Tools"
|
||||
case "passkey":
|
||||
@@ -209,19 +222,19 @@ const shellTopHTML = `{{define "shellTop"}}<!doctype html><meta charset=utf-8>
|
||||
<link rel=manifest href=/manifest.json>
|
||||
<title>maven · {{pageTitle .}}</title>
|
||||
<link rel=stylesheet href=/ui.css>
|
||||
<div class=shell>
|
||||
<div class=shell data-app=maven>
|
||||
<header class=topbar>
|
||||
<div class=breadcrumbs>
|
||||
<span class=current>{{pageTitle .}}</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>
|
||||
<svg class=icon width="13" height="13"><use href="/ethos-icons.svg#i-search"/></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>
|
||||
<svg class=icon width="15" height="15"><use href="/ethos-icons.svg#i-grid"/></svg>
|
||||
</button>
|
||||
<span class=conn-status>
|
||||
<span class="dot online" id=connDot></span>
|
||||
@@ -274,6 +287,10 @@ func shellFuncs() template.FuncMap {
|
||||
// CoreAPI, never writes — the store IS the audit trail, this just shows it.
|
||||
var dashTmpl = template.Must(template.New("dash").Funcs(shellFuncs()).Parse(shellTopHTML + dashHTML + shellBottomHTML))
|
||||
|
||||
// ecosystemTmpl — read-only view of the Nexus/Praxis/Hexis siblings, whose only
|
||||
// human surface is here (they ship no web UI of their own).
|
||||
var ecosystemTmpl = template.Must(template.New("ecosystem").Funcs(shellFuncs()).Parse(shellTopHTML + ecosystemHTML + shellBottomHTML))
|
||||
|
||||
func noCache(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
@@ -298,6 +315,9 @@ func main() {
|
||||
pkOrigin := flag.String("webauthn-origin", "", "WebAuthn origin URL (e.g. https://maven.kvmx.ru)")
|
||||
pkRPID := flag.String("webauthn-rpid", "", "WebAuthn RP ID (e.g. maven.kvmx.ru)")
|
||||
pkFile := flag.String("passkey-file", "./passkeys.json", "path to WebAuthn credential store (JSON)")
|
||||
nexusURL := flag.String("nexus", "", "Nexus base URL for the /ecosystem panel (empty = not configured)")
|
||||
praxisURL := flag.String("praxis", "", "Praxis base URL for the /ecosystem panel (empty = not configured)")
|
||||
hexisURL := flag.String("hexis", "", "Hexis base URL for the /ecosystem panel (empty = not configured)")
|
||||
flag.Parse()
|
||||
|
||||
var core ipc.CoreAPI
|
||||
@@ -363,6 +383,10 @@ func main() {
|
||||
mux.HandleFunc("/routines", func(w http.ResponseWriter, r *http.Request) {
|
||||
handleRoutines(w, r, core)
|
||||
})
|
||||
ecoURLsCfg := ecoURLs{nexus: *nexusURL, praxis: *praxisURL, hexis: *hexisURL}
|
||||
mux.HandleFunc("/ecosystem", func(w http.ResponseWriter, r *http.Request) {
|
||||
handleEcosystem(w, r, ecoURLsCfg)
|
||||
})
|
||||
// ----- passkey (WebAuthn) endpoints -----
|
||||
// Wired when both -core and a configured origin are present. The origin
|
||||
// must match the browser's view of mavweb (e.g. https://maven.kvmx.ru).
|
||||
@@ -601,13 +625,13 @@ const toolsHTML = `{{template "shellTop" "tools"}}
|
||||
<input type=text name=cmd class=input-wide placeholder="systemctl restart" required>
|
||||
<label><input type=checkbox name=destructive> destructive</label>
|
||||
<button class=btn>enable</button></form>
|
||||
<form method=post action=/tools style=display:inline>
|
||||
<form method=post action=/tools class=inline-form>
|
||||
<input type=hidden name=name value="{{.Name}}">
|
||||
<input type=hidden name=action value=dismiss>
|
||||
<button class="btn btn-muted">dismiss</button></form></td>
|
||||
</tr>{{end}}</table></div>
|
||||
{{else}}<div class=empty style=padding:var(--ethos-space-5)>
|
||||
<svg class=icon width="20" height="20" 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>
|
||||
{{else}}<div class=empty>
|
||||
<svg class=icon width="20" height="20"><use href="/ethos-icons.svg#i-search"/></svg>
|
||||
<div>no proposed tools</div>
|
||||
<div class=hint>maven will propose tools here when she needs help running an action</div>
|
||||
</div>{{end}}
|
||||
@@ -617,13 +641,13 @@ const toolsHTML = `{{template "shellTop" "tools"}}
|
||||
{{if .Enabled}}<div class=scroll><table><tr><th>name</th><th>scope</th><th>command</th><th></th><th></th></tr>
|
||||
{{range .Enabled}}<tr><td><code>{{.Name}}</code></td><td><span class=badge>{{.Scope}}</span></td><td><code>{{join .Cmd " "}}</code></td>
|
||||
<td>{{if .Destructive}}<span class=red>destructive</span>{{end}}</td>
|
||||
<td><form method=post action=/tools style=display:inline>
|
||||
<td><form method=post action=/tools class=inline-form>
|
||||
<input type=hidden name=name value="{{.Name}}">
|
||||
<input type=hidden name=scope value="{{.Scope}}">
|
||||
<input type=hidden name=action value=disable>
|
||||
<button class=btn>disable</button></form></td></tr>{{end}}</table></div>
|
||||
{{else}}<div class=empty style=padding:var(--ethos-space-5)>
|
||||
<svg class=icon width="20" height="20" 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>
|
||||
{{else}}<div class=empty>
|
||||
<svg class=icon width="20" height="20"><use href="/ethos-icons.svg#i-settings"/></svg>
|
||||
<div>no tools enabled</div>
|
||||
<div class=hint>enable proposed tools above, or ask maven to configure one</div>
|
||||
</div>{{end}}
|
||||
@@ -641,14 +665,14 @@ const routinesHTML = `{{template "shellTop" "routines"}}
|
||||
{{range .Proposed}}<tr>
|
||||
<td><code>{{.Action}}</code></td><td><code>{{.Object}}</code></td><td>{{.IntervalDays}} days</td>
|
||||
<td>
|
||||
<form method=post action=/routines style=display:inline>
|
||||
<form method=post action=/routines class=inline-form>
|
||||
<input type=hidden name=id value="{{.ID}}">
|
||||
<input type=hidden name=action value=dismiss>
|
||||
<button class="btn btn-muted">dismiss</button></form>
|
||||
</td>
|
||||
</tr>{{end}}</table></div>
|
||||
{{else}}<div class=empty style=padding:var(--ethos-space-5)>
|
||||
<svg class=icon width="20" height="20" 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>
|
||||
{{else}}<div class=empty>
|
||||
<svg class=icon width="20" height="20"><use href="/ethos-icons.svg#i-wave"/></svg>
|
||||
<div>no proposed routines</div>
|
||||
<div class=hint>maven will propose routines here when she detects a recurring pattern</div>
|
||||
</div>{{end}}
|
||||
@@ -1054,12 +1078,12 @@ const chatPageHTML = `{{template "shellTop" "chat"}}
|
||||
<h1>Chat</h1>
|
||||
<section class=card>
|
||||
{{if .Error}}<div class="msg msg-err">{{.Error}}</div>{{end}}
|
||||
<div class=scroll style="max-height:60vh;overflow-y:auto;margin-bottom:var(--ethos-space-4)" id=chatHistory>
|
||||
<div class="scroll chat-scroll" id=chatHistory>
|
||||
{{range .Messages}}
|
||||
<div class="chat-msg {{.Role}}"><strong>{{if eq .Role "user"}}you{{else}}maven{{end}}:</strong> {{.Text}}</div>
|
||||
{{else}}
|
||||
<div class=empty style=padding:var(--ethos-space-5)>
|
||||
<svg class=icon width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
||||
<div class=empty>
|
||||
<svg class=icon width="20" height="20"><use href="/ethos-icons.svg#i-message"/></svg>
|
||||
<div>start a conversation</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -1073,13 +1097,6 @@ const chatPageHTML = `{{template "shellTop" "chat"}}
|
||||
var ch = document.getElementById('chatHistory');
|
||||
if(ch) ch.scrollTop = ch.scrollHeight;
|
||||
</script>
|
||||
<style>
|
||||
.chat-msg { padding: var(--ethos-space-2) var(--ethos-space-3); border-radius: var(--ethos-radius-m); margin-bottom: var(--ethos-space-1); }
|
||||
.chat-msg.user { background: var(--ethos-bg-subtle); }
|
||||
.chat-msg.assistant { background: var(--ethos-bg-elevated); }
|
||||
.chat-form { display:flex; gap:var(--ethos-space-2); }
|
||||
.chat-form input { flex:1; }
|
||||
</style>
|
||||
{{template "shellBottom"}}`
|
||||
|
||||
// handleChatPage renders the chat conversation page.
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<td class=text-max>{{.Message}}</td>
|
||||
</tr>{{end}}</table></div>
|
||||
{{else}}<div class=empty>
|
||||
<svg class=icon width="24" height="24" 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>
|
||||
<svg class=icon width="24" height="24"><use href="/ethos-icons.svg#i-bell"/></svg>
|
||||
<div>no notifications yet</div>
|
||||
<div class=hint>check back later or ask maven a question</div>
|
||||
</div>{{end}}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<td class=text-max>{{.Payload}}</td>
|
||||
</tr>{{end}}</table></div>
|
||||
{{else}}<div class=empty>
|
||||
<svg class=icon width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
|
||||
<svg class=icon width="24" height="24"><use href="/ethos-icons.svg#i-calendar"/></svg>
|
||||
<div>no reminders yet</div>
|
||||
<div class=hint>ask maven to remind you of something</div>
|
||||
</div>{{end}}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
ETHOS — icon set
|
||||
One hand, one grid. Every icon: viewBox 0 0 24 24, stroke="currentColor",
|
||||
stroke-width 1.7, round caps/joins, fill="none". Transport glyphs (play,
|
||||
pause, prev, next, more) are the only filled exceptions.
|
||||
|
||||
USE: <svg class="icon" width="22" height="22"><use href="/ethos-icons.svg#i-search"/></svg>
|
||||
color + width/height come from the consumer; the icon inherits them.
|
||||
|
||||
EXTEND: add a new <symbol id="i-NAME"> on the same 24px grid, same stroke,
|
||||
same corner feel. Match the existing hand — do not import lucide or
|
||||
any other pack. Keep ids prefixed i- and kebab-cased.
|
||||
-->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
|
||||
|
||||
<!-- ── app mark / motif seeds ── -->
|
||||
<symbol id="i-wave" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round">
|
||||
<path d="M2 12h1M6 8v8M10 4v16M14 7v10M18 5v14M21 11v2"/>
|
||||
</symbol>
|
||||
<symbol id="i-grid" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round">
|
||||
<rect x="4" y="4" width="7" height="7" rx="1.5"/><rect x="13" y="4" width="7" height="7" rx="1.5"/>
|
||||
<rect x="4" y="13" width="7" height="7" rx="1.5"/><rect x="13" y="13" width="7" height="7" rx="1.5"/>
|
||||
</symbol>
|
||||
|
||||
<!-- ── navigation ── -->
|
||||
<symbol id="i-home" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M4 11l8-6 8 6M6 10v9h12v-9"/>
|
||||
</symbol>
|
||||
<symbol id="i-listen" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M4 18V9l14-3v9"/><circle cx="6" cy="18" r="2.4"/><circle cx="18" cy="15" r="2.4"/>
|
||||
</symbol>
|
||||
<symbol id="i-library" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round">
|
||||
<rect x="4" y="4" width="6" height="16" rx="1.5"/><rect x="14" y="4" width="6" height="16" rx="1.5"/>
|
||||
</symbol>
|
||||
<symbol id="i-album" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">
|
||||
<circle cx="12" cy="12" r="8.5"/><circle cx="12" cy="12" r="2"/>
|
||||
</symbol>
|
||||
<symbol id="i-artist" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="8" r="4"/><path d="M5 20c0-3.5 3.1-6 7-6s7 2.5 7 6"/>
|
||||
</symbol>
|
||||
<symbol id="i-queue" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M4 7h11M4 12h11M4 17h7M18 9v8"/><circle cx="18" cy="18.5" r="1.6"/>
|
||||
</symbol>
|
||||
<symbol id="i-folder" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round">
|
||||
<path d="M4 7a1 1 0 011-1h4.5l2 2H19a1 1 0 011 1v8a1 1 0 01-1 1H5a1 1 0 01-1-1z"/>
|
||||
</symbol>
|
||||
<symbol id="i-file" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round">
|
||||
<path d="M14 3H7a1 1 0 00-1 1v16a1 1 0 001 1h10a1 1 0 001-1V8z"/><path d="M14 3v5h5"/>
|
||||
</symbol>
|
||||
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="3.2"/><path d="M12 2v3M12 19v3M4.2 4.2l2.1 2.1M17.7 17.7l2.1 2.1M2 12h3M19 12h3M4.2 19.8l2.1-2.1M17.7 6.3l2.1-2.1"/>
|
||||
</symbol>
|
||||
|
||||
<!-- ── transport (filled) ── -->
|
||||
<symbol id="i-play" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></symbol>
|
||||
<symbol id="i-pause" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5h3v14H8zM13 5h3v14h-3z"/></symbol>
|
||||
<symbol id="i-prev" viewBox="0 0 24 24" fill="currentColor"><path d="M6 6h2v12H6z"/><path d="M20 6v12l-9-6z"/></symbol>
|
||||
<symbol id="i-next" viewBox="0 0 24 24" fill="currentColor"><path d="M16 6h2v12h-2z"/><path d="M6 6v12l9-6z"/></symbol>
|
||||
<symbol id="i-shuffle" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M16 4h4v4M20 4l-6 6M4 20l16-16M16 20h4v-4M14 14l6 6M4 4l4 4"/>
|
||||
</symbol>
|
||||
<symbol id="i-repeat" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M17 2l3 3-3 3M20 5H8a4 4 0 00-4 4v1M7 22l-3-3 3-3M4 19h12a4 4 0 004-4v-1"/>
|
||||
</symbol>
|
||||
|
||||
<!-- ── actions / status ── -->
|
||||
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round">
|
||||
<circle cx="11" cy="11" r="7"/><path d="M20 20l-3.5-3.5"/>
|
||||
</symbol>
|
||||
<symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></symbol>
|
||||
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></symbol>
|
||||
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12l4.5 4.5L20 7"/></symbol>
|
||||
<symbol id="i-chevron-left" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M15 6l-6 6 6 6"/></symbol>
|
||||
<symbol id="i-chevron-right" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 6l6 6-6 6"/></symbol>
|
||||
<symbol id="i-more" viewBox="0 0 24 24" fill="currentColor"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></symbol>
|
||||
<symbol id="i-bell" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M6 9a6 6 0 0112 0c0 5 2 6 2 6H4s2-1 2-6M10 21h4"/>
|
||||
</symbol>
|
||||
<symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 4v11M8 11l4 4 4-4M5 20h14"/>
|
||||
</symbol>
|
||||
<symbol id="i-upload" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 15V4M8 8l4-4 4 4M5 20h14"/>
|
||||
</symbol>
|
||||
|
||||
<!-- ── terminal / code (tmux-view, drive) ── -->
|
||||
<symbol id="i-terminal" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M4 4l8 8-8 8M16 19h4"/>
|
||||
</symbol>
|
||||
<symbol id="i-refresh" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 4v6h-6M3 20v-6h6"/>
|
||||
<path d="M20.5 10a8.5 8.5 0 00-16 3M3.5 14a8.5 8.5 0 0016-3"/>
|
||||
</symbol>
|
||||
<symbol id="i-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round">
|
||||
<rect x="9" y="9" width="12" height="12" rx="1.5"/>
|
||||
<path d="M15 9V6a1 1 0 00-1-1H5a1 1 0 00-1 1v9a1 1 0 001 1h3"/>
|
||||
</symbol>
|
||||
<symbol id="i-send" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 12h16M13 6l6 6-6 6"/>
|
||||
</symbol>
|
||||
|
||||
<!-- ── maven-specific ── -->
|
||||
<symbol id="i-clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 3"/>
|
||||
</symbol>
|
||||
<symbol id="i-calendar" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="4" y="5" width="16" height="16" rx="1.5"/><path d="M4 10h16M8 3v3M16 3v3"/>
|
||||
<path d="M10 15l2 2 4-4"/>
|
||||
</symbol>
|
||||
<symbol id="i-mic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="10" y="2" width="4" height="12" rx="2"/><path d="M6 11a6 6 0 0012 0M12 19v4M9 23h6"/>
|
||||
</symbol>
|
||||
<symbol id="i-message" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M4 4h16a2 2 0 012 2v12a2 2 0 01-2 2H8l-4 4V6a2 2 0 012-2z"/>
|
||||
</symbol>
|
||||
<symbol id="i-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round">
|
||||
<rect x="6" y="11" width="12" height="10" rx="1.5"/><path d="M9 11V7a3 3 0 016 0v4"/>
|
||||
</symbol>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.4 KiB |
@@ -47,15 +47,15 @@
|
||||
let paletteEl = null;
|
||||
let paletteInput = null;
|
||||
|
||||
// Page entries for the command palette
|
||||
// Page entries for the command palette — uses ethos-icons.svg sprite
|
||||
const palettePages = [
|
||||
{ label: 'Dashboard', url: '/dash', 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>' },
|
||||
{ label: 'History', url: '/history', 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>' },
|
||||
{ label: 'Rule Trace', url: '/trace', 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>' },
|
||||
{ label: 'Notifications', url: '/notifications', 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>' },
|
||||
{ label: 'Voice', url: '/', 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>' },
|
||||
{ label: 'Tools', url: '/tools', 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>' },
|
||||
{ label: 'Passkey', url: '/auth/passkey', 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>' },
|
||||
{ label: 'Dashboard', url: '/dash', icon: '<svg width="14" height="14"><use href="/ethos-icons.svg#i-grid"/></svg>' },
|
||||
{ label: 'History', url: '/history', icon: '<svg width="14" height="14"><use href="/ethos-icons.svg#i-clock"/></svg>' },
|
||||
{ label: 'Rule Trace', url: '/trace', icon: '<svg width="14" height="14"><use href="/ethos-icons.svg#i-wave"/></svg>' },
|
||||
{ label: 'Notifications', url: '/notifications', icon: '<svg width="14" height="14"><use href="/ethos-icons.svg#i-bell"/></svg>' },
|
||||
{ label: 'Voice', url: '/', icon: '<svg width="14" height="14"><use href="/ethos-icons.svg#i-mic"/></svg>' },
|
||||
{ label: 'Tools', url: '/tools', icon: '<svg width="14" height="14"><use href="/ethos-icons.svg#i-settings"/></svg>' },
|
||||
{ label: 'Passkey', url: '/auth/passkey', icon: '<svg width="14" height="14"><use href="/ethos-icons.svg#i-lock"/></svg>' },
|
||||
];
|
||||
|
||||
function buildPalette() {
|
||||
@@ -65,7 +65,7 @@
|
||||
paletteEl.innerHTML =
|
||||
'<div class=cmd-palette>' +
|
||||
'<div class=cmd-palette-input>' +
|
||||
'<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="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>' +
|
||||
'<svg width="14" height="14"><use href="/ethos-icons.svg#i-search"/></svg>' +
|
||||
'<input type=text placeholder="Go to page or run action…" spellcheck=false autofocus>' +
|
||||
'</div>' +
|
||||
'<div class=cmd-palette-list id=paletteList></div>' +
|
||||
|
||||
+492
-374
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{{template "shellTop" "trace"}}
|
||||
<h1>Rule Trace</h1>
|
||||
<div class=hint style=margin-bottom:var(--ethos-space-4)>{{.Now | ago}} — winner: <strong>{{if .Winner}}{{.Winner}}{{else}}nothing fired{{end}}</strong></div>
|
||||
<div class="hint mb-4">{{.Now | ago}} — winner: <strong>{{if .Winner}}{{.Winner}}{{else}}nothing fired{{end}}</strong></div>
|
||||
<div class=scroll><table class=mono>
|
||||
<tr><th>rule<th>sev<th>predicate<th>gate<th>blocked by<th>detail<th>selected<th>lost to</tr>
|
||||
{{range .Rules}}<tr>
|
||||
|
||||
+24
-49
@@ -1,61 +1,36 @@
|
||||
{{template "shellTop" "voice"}}
|
||||
<style>
|
||||
.voice{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1.5rem;padding:1rem 0}
|
||||
#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)}
|
||||
#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{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>
|
||||
<div class=voice>
|
||||
<div id="status">tap & hold to speak</div>
|
||||
<button id="btn" type="button">🎙</button>
|
||||
<div id="log"></div>
|
||||
<details id="cheat">
|
||||
<div class=voice-page>
|
||||
<div class=voice-status id=status>tap & hold to speak</div>
|
||||
<button class=voice-btn id=btn type=button>
|
||||
<svg width="36" height="36"><use href="/ethos-icons.svg#i-mic"/></svg>
|
||||
</button>
|
||||
<div class=voice-log id=log></div>
|
||||
<details class=voice-cheat 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>
|
||||
<dd class=ru>включи свет · сделай громче</dd>
|
||||
<dd class=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>
|
||||
<dd class=ru>напомни через 4 часа размяться</dd>
|
||||
<dd class=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>
|
||||
<dd class=ru>выпил кофе · вес 73 кг</dd>
|
||||
<dd class=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>
|
||||
<dd class=ru>запиши рецепт блинов</dd>
|
||||
<dd class=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>
|
||||
<dd class=ru>какой сегодня день · какой прогноз погоды</dd>
|
||||
<dd class=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>
|
||||
<dd class=ru>включи тихий режим · как загрузка системы</dd>
|
||||
<dd class=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 class=voice-lang>
|
||||
<span class="active" data-lang=ru>RU</span>
|
||||
<span data-lang=en>EN</span>
|
||||
</div>
|
||||
</div>
|
||||
<script defer src="/app.js"></script>
|
||||
@@ -64,11 +39,11 @@ html[data-lang=ru] .cheat-ru{display:revert}
|
||||
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) {
|
||||
document.querySelectorAll('.voice-lang 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'); });
|
||||
document.querySelectorAll('.voice-lang span').forEach(function(b) { b.classList.remove('active'); });
|
||||
el.classList.add('active');
|
||||
var url = new URL(window.location);
|
||||
url.searchParams.set('lang', l);
|
||||
|
||||
Reference in New Issue
Block a user