Add service orchestration and web UI

This commit is contained in:
dwrz
2026-02-13 15:03:02 +00:00
parent 6f0509ff18
commit d5a27c776e
17 changed files with 3890 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
{{ define "body" }}
<body>
{{ template "main" . }}
{{ template "templates" . }}
</body>
{{ end }}

View File

@@ -0,0 +1,41 @@
{{ define "footer/compose" }}
<div class="compose">
<textarea
class="compose__textarea"
id="text-input"
placeholder="Type a message..."
aria-label="Message input"
rows="1"
></textarea>
<div class="compose__attachments" id="attachments"></div>
<div class="compose__actions">
<button
type="button"
class="compose__action-btn"
id="attach"
aria-label="Attach files"
>
<svg class="icon"><use href="/static/icons.svg#attach"></use></svg>
</button>
<input type="file" id="file-input" multiple hidden />
<div class="compose__actions-right">
<button
type="button"
class="compose__action-btn compose__action-btn--record"
id="ptt"
aria-label="Push to talk"
>
<svg class="icon"><use href="/static/icons.svg#mic"></use></svg>
</button>
<button
type="button"
class="compose__action-btn compose__action-btn--send"
id="send"
aria-label="Send message"
>
<svg class="icon"><use href="/static/icons.svg#send"></use></svg>
</button>
</div>
</div>
</div>
{{ end }}

View File

@@ -0,0 +1,6 @@
{{ define "footer" }}
<footer class="footer">
{{ template "footer/compose" . }}
{{ template "footer/toolbar" . }}
</footer>
{{ end }}

View File

@@ -0,0 +1,31 @@
{{ define "footer/toolbar" }}
<div class="footer__toolbar">
<button
type="button"
class="footer__toolbar-btn"
id="reset"
aria-label="Reset conversation"
>
<svg class="icon"><use href="/static/icons.svg#reset"></use></svg>
</button>
<div class="footer__toolbar-spacer"></div>
<select id="model" class="footer__select" aria-label="Model">
<option value="" disabled selected>
Loading...
</option>
</select>
<select id="voice" class="footer__select" aria-label="Voice">
<option value="" disabled selected>
Loading...
</option>
</select>
<button
type="button"
class="footer__toolbar-btn"
id="mute"
aria-label="Mute"
>
<svg class="icon"><use href="/static/icons.svg#volume"></use></svg>
</button>
</div>
{{ end }}

View File

@@ -0,0 +1,11 @@
{{ define "head" }}
<head>
<meta name="author" content="Chimerical LLC">
<meta charset="utf-8">
<meta name="viewport"
content="width=device-width, initial-scale=1.0, shrink-to-fit= no, interactive-widget=resizes-content">
<title>Odidere</title>
<link rel="stylesheet" href="/static/main.css">
<script defer src="/static/main.js"></script>
</head>
{{ end }}

View File

@@ -0,0 +1,5 @@
<!doctype html>
<html lang="en">
{{ template "head" . }}
{{ template "body" . }}
</html>

View File

@@ -0,0 +1,7 @@
{{ define "main" }}
<main class="container">
<section class="chat" id="chat" aria-live="polite">
</section>
{{ template "footer" . }}
</main>
{{ end }}

View File

@@ -0,0 +1,126 @@
{{ define "templates" }}
<template id="tpl-user-message">
<div class="message message--user">
<div class="message__icon" aria-hidden="true">
<svg class="icon"><use href="/static/icons.svg#user"></use></svg>
</div>
<div class="message__body">
<div class="message__content"></div>
<div class="message__actions">
<button
type="button"
class="message__action-btn"
data-action="inspect"
aria-label="Show details"
aria-expanded="false"
>
<svg class="icon"><use href="/static/icons.svg#inspect"></use></svg>
</button>
<button
type="button"
class="message__action-btn"
data-action="copy"
aria-label="Copy to clipboard"
>
<svg class="icon"><use href="/static/icons.svg#copy"></use></svg>
</button>
</div>
<div class="message__debug">
<dl class="message__debug-list"></dl>
</div>
</div>
</div>
</template>
<template id="tpl-assistant-message">
<div class="message message--assistant">
<div class="message__icon" aria-hidden="true">
<svg class="icon"><use href="/static/icons.svg#assistant"></use></svg>
</div>
<div class="message__body">
<div class="message__content"></div>
<div class="message__actions">
<button
type="button"
class="message__action-btn"
data-action="inspect"
aria-label="Show details"
aria-expanded="false"
>
<svg class="icon"><use href="/static/icons.svg#inspect"></use></svg>
</button>
<button
type="button"
class="message__action-btn"
data-action="copy"
aria-label="Copy to clipboard"
>
<svg class="icon"><use href="/static/icons.svg#copy"></use></svg>
</button>
</div>
<div class="message__debug">
<dl class="message__debug-list"></dl>
</div>
</div>
</div>
</template>
<template id="tpl-error-message">
<div class="message message--error message--assistant">
<div class="message__icon" aria-hidden="true">
<svg class="icon"><use href="/static/icons.svg#assistant"></use></svg>
</div>
<div class="message__body">
<div class="message__content"></div>
</div>
</div>
</template>
<template id="tpl-collapsible">
<details class="collapsible">
<summary class="collapsible__summary">
<span class="collapsible__label"></span>
</summary>
<div class="collapsible__content">
<pre></pre>
</div>
</details>
</template>
<template id="tpl-tool-call">
<details class="collapsible collapsible--tool">
<summary class="collapsible__summary">
<svg class="icon"><use href="/static/icons.svg#tool"></use></svg>
<span class="collapsible__label"></span>
</summary>
<div class="collapsible__content">
<div class="collapsible__section">
<div class="collapsible__section-label">Arguments</div>
<pre class="collapsible__pre" data-args></pre>
</div>
<div class="collapsible__section collapsible__section--output" hidden>
<div class="collapsible__section-label">Output</div>
<pre class="collapsible__pre" data-output></pre>
</div>
</div>
</details>
</template>
<template id="tpl-attachment-chip">
<div class="compose__attachment">
<span class="compose__attachment-name"></span>
<button
type="button"
class="compose__attachment-remove"
aria-label="Remove attachment"
>
<svg class="icon"><use href="/static/icons.svg#x-circle"></use></svg>
</button>
</div>
</template>
<template id="tpl-debug-row">
<dt></dt>
<dd></dd>
</template>
{{ end }}

View File

@@ -0,0 +1,43 @@
package templates
import (
"embed"
"fmt"
"html/template"
"io/fs"
"strings"
)
//go:embed static/*
var static embed.FS
const fileType = ".gohtml"
// Parse walks the embedded static directory and parses all .gohtml templates.
func Parse() (*template.Template, error) {
tmpl := template.New("")
parseFS := func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
if strings.Contains(path, fileType) {
if _, err := tmpl.ParseFS(static, path); err != nil {
return fmt.Errorf(
"failed to parse template %s: %w",
path, err,
)
}
}
return nil
}
if err := fs.WalkDir(static, ".", parseFS); err != nil {
return nil, fmt.Errorf("failed to parse templates: %w", err)
}
return tmpl, nil
}