fixed docker build for webui
added themes and token handling
This commit is contained in:
parent
06ca0f422c
commit
5da6a3bba1
4 changed files with 169 additions and 20 deletions
|
|
@ -3,7 +3,7 @@
|
||||||
# ============================================================
|
# ============================================================
|
||||||
FROM golang:1.26.0-alpine AS builder
|
FROM golang:1.26.0-alpine AS builder
|
||||||
|
|
||||||
RUN apk add --no-cache git make
|
RUN apk add --no-cache git make nodejs npm
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
|
|
@ -13,6 +13,7 @@ RUN go mod download
|
||||||
|
|
||||||
# Copy source and build
|
# Copy source and build
|
||||||
COPY . .
|
COPY . .
|
||||||
|
RUN make ui-build
|
||||||
RUN make build
|
RUN make build
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
@ -25,6 +26,9 @@ RUN apk add --no-cache ca-certificates tzdata curl
|
||||||
# Copy binary
|
# Copy binary
|
||||||
COPY --from=builder /src/build/picoclaw /usr/local/bin/picoclaw
|
COPY --from=builder /src/build/picoclaw /usr/local/bin/picoclaw
|
||||||
|
|
||||||
|
# Copy built Web UI assets
|
||||||
|
COPY --from=builder /src/ui/dist /usr/local/share/picoclaw/ui/dist
|
||||||
|
|
||||||
# Create picoclaw home directory
|
# Create picoclaw home directory
|
||||||
RUN /usr/local/bin/picoclaw onboard
|
RUN /usr/local/bin/picoclaw onboard
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -267,6 +267,8 @@ func (c *WebUIChannel) staticHandler() http.Handler {
|
||||||
|
|
||||||
func (c *WebUIChannel) findUIRoot() string {
|
func (c *WebUIChannel) findUIRoot() string {
|
||||||
candidates := []string{
|
candidates := []string{
|
||||||
|
filepath.Join(string(os.PathSeparator), "usr", "local", "share", "picoclaw", "ui", "dist"),
|
||||||
|
filepath.Join(string(os.PathSeparator), "usr", "share", "picoclaw", "ui", "dist"),
|
||||||
filepath.Join("ui", "dist"),
|
filepath.Join("ui", "dist"),
|
||||||
"ui",
|
"ui",
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,41 @@ import './style.css'
|
||||||
|
|
||||||
const app = document.querySelector('#app')
|
const app = document.querySelector('#app')
|
||||||
|
|
||||||
|
const THEME_STORAGE_KEY = 'picoclaw.theme'
|
||||||
|
|
||||||
|
function getStoredTheme() {
|
||||||
|
const v = localStorage.getItem(THEME_STORAGE_KEY)
|
||||||
|
if (v === 'light' || v === 'dark' || v === 'system') return v
|
||||||
|
return 'system'
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTheme(mode) {
|
||||||
|
const root = document.documentElement
|
||||||
|
if (mode === 'system') {
|
||||||
|
root.removeAttribute('data-theme')
|
||||||
|
} else {
|
||||||
|
root.setAttribute('data-theme', mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let themeMode = getStoredTheme()
|
||||||
|
applyTheme(themeMode)
|
||||||
|
|
||||||
app.innerHTML = `
|
app.innerHTML = `
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
<header class="header">
|
<header class="header">
|
||||||
<div class="title">PicoClaw</div>
|
<div class="title">PicoClaw</div>
|
||||||
<div class="status" id="status">disconnected</div>
|
<div class="header-right">
|
||||||
|
<label class="theme" for="theme">
|
||||||
|
<span class="theme-label">Theme</span>
|
||||||
|
<select class="theme-select" id="theme">
|
||||||
|
<option value="system">System</option>
|
||||||
|
<option value="light">Light</option>
|
||||||
|
<option value="dark">Dark</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<div class="status" id="status">disconnected</div>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="chat" id="chat"></main>
|
<main class="chat" id="chat"></main>
|
||||||
|
|
@ -20,11 +50,38 @@ app.innerHTML = `
|
||||||
|
|
||||||
const chat = document.querySelector('#chat')
|
const chat = document.querySelector('#chat')
|
||||||
const statusEl = document.querySelector('#status')
|
const statusEl = document.querySelector('#status')
|
||||||
|
const themeSelect = document.querySelector('#theme')
|
||||||
const form = document.querySelector('#form')
|
const form = document.querySelector('#form')
|
||||||
const input = document.querySelector('#input')
|
const input = document.querySelector('#input')
|
||||||
|
|
||||||
const chatId = 'browser'
|
const chatId = 'browser'
|
||||||
const pageToken = new URLSearchParams(location.search).get('token') || ''
|
const TOKEN_STORAGE_KEY = 'picoclaw.gateway_token'
|
||||||
|
const urlParams = new URLSearchParams(location.search)
|
||||||
|
const tokenFromUrl = urlParams.get('token') || ''
|
||||||
|
const tokenFromStorage = localStorage.getItem(TOKEN_STORAGE_KEY) || ''
|
||||||
|
let gatewayToken = tokenFromUrl || tokenFromStorage
|
||||||
|
let tokenCameFromUrl = !!tokenFromUrl
|
||||||
|
|
||||||
|
function removeTokenFromUrl() {
|
||||||
|
const p = new URLSearchParams(location.search)
|
||||||
|
if (!p.has('token')) return
|
||||||
|
p.delete('token')
|
||||||
|
const qs = p.toString()
|
||||||
|
const newUrl = `${location.pathname}${qs ? `?${qs}` : ''}${location.hash || ''}`
|
||||||
|
history.replaceState(null, '', newUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
themeSelect.value = themeMode
|
||||||
|
themeSelect.addEventListener('change', () => {
|
||||||
|
themeMode = themeSelect.value
|
||||||
|
localStorage.setItem(THEME_STORAGE_KEY, themeMode)
|
||||||
|
applyTheme(themeMode)
|
||||||
|
})
|
||||||
|
|
||||||
|
const media = window.matchMedia('(prefers-color-scheme: dark)')
|
||||||
|
media.addEventListener('change', () => {
|
||||||
|
if (themeMode === 'system') applyTheme('system')
|
||||||
|
})
|
||||||
|
|
||||||
function addMessage(role, text) {
|
function addMessage(role, text) {
|
||||||
const item = document.createElement('div')
|
const item = document.createElement('div')
|
||||||
|
|
@ -41,7 +98,7 @@ function addMessage(role, text) {
|
||||||
|
|
||||||
function wsUrl() {
|
function wsUrl() {
|
||||||
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'
|
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||||
const tokenPart = pageToken ? `&token=${encodeURIComponent(pageToken)}` : ''
|
const tokenPart = gatewayToken ? `&token=${encodeURIComponent(gatewayToken)}` : ''
|
||||||
return `${proto}//${location.host}/ws?chat_id=${encodeURIComponent(chatId)}${tokenPart}`
|
return `${proto}//${location.host}/ws?chat_id=${encodeURIComponent(chatId)}${tokenPart}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,6 +118,12 @@ function connect() {
|
||||||
|
|
||||||
ws.addEventListener('open', () => {
|
ws.addEventListener('open', () => {
|
||||||
setStatus('connected')
|
setStatus('connected')
|
||||||
|
|
||||||
|
if (tokenCameFromUrl && gatewayToken) {
|
||||||
|
localStorage.setItem(TOKEN_STORAGE_KEY, gatewayToken)
|
||||||
|
removeTokenFromUrl()
|
||||||
|
tokenCameFromUrl = false
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
ws.addEventListener('close', () => {
|
ws.addEventListener('close', () => {
|
||||||
|
|
|
||||||
112
ui/src/style.css
112
ui/src/style.css
|
|
@ -1,11 +1,59 @@
|
||||||
:root {
|
:root {
|
||||||
color-scheme: light dark;
|
color-scheme: light dark;
|
||||||
|
|
||||||
|
--bg: #f6f7fb;
|
||||||
|
--panel: rgba(255, 255, 255, 0.8);
|
||||||
|
--panel-solid: #ffffff;
|
||||||
|
--border: rgba(17, 24, 39, 0.12);
|
||||||
|
--text: #111827;
|
||||||
|
--muted: #6b7280;
|
||||||
|
--accent: #2563eb;
|
||||||
|
--accent-contrast: #ffffff;
|
||||||
|
--input-bg: rgba(255, 255, 255, 0.9);
|
||||||
|
--shadow: 0 1px 2px rgba(17, 24, 39, 0.06), 0 8px 24px rgba(17, 24, 39, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--bg: #0b1020;
|
||||||
|
--panel: rgba(17, 24, 39, 0.72);
|
||||||
|
--panel-solid: #111827;
|
||||||
|
--border: rgba(255, 255, 255, 0.12);
|
||||||
|
--text: rgba(255, 255, 255, 0.92);
|
||||||
|
--muted: rgba(255, 255, 255, 0.62);
|
||||||
|
--accent: #3b82f6;
|
||||||
|
--accent-contrast: #0b1020;
|
||||||
|
--input-bg: rgba(0, 0, 0, 0.25);
|
||||||
|
--shadow: 0 1px 2px rgba(0, 0, 0, 0.35), 0 10px 30px rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="light"] {
|
||||||
|
color-scheme: light;
|
||||||
|
--bg: #f6f7fb;
|
||||||
|
--panel: rgba(255, 255, 255, 0.85);
|
||||||
|
--panel-solid: #ffffff;
|
||||||
|
--border: rgba(17, 24, 39, 0.12);
|
||||||
|
--text: #111827;
|
||||||
|
--muted: #6b7280;
|
||||||
|
--accent: #2563eb;
|
||||||
|
--accent-contrast: #ffffff;
|
||||||
|
--input-bg: rgba(255, 255, 255, 0.92);
|
||||||
|
--shadow: 0 1px 2px rgba(17, 24, 39, 0.06), 0 8px 24px rgba(17, 24, 39, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="dark"] {
|
||||||
|
color-scheme: dark;
|
||||||
--bg: #0b1020;
|
--bg: #0b1020;
|
||||||
--panel: rgba(255, 255, 255, 0.06);
|
--panel: rgba(17, 24, 39, 0.72);
|
||||||
--border: rgba(255, 255, 255, 0.10);
|
--panel-solid: #111827;
|
||||||
|
--border: rgba(255, 255, 255, 0.12);
|
||||||
--text: rgba(255, 255, 255, 0.92);
|
--text: rgba(255, 255, 255, 0.92);
|
||||||
--muted: rgba(255, 255, 255, 0.60);
|
--muted: rgba(255, 255, 255, 0.62);
|
||||||
--accent: #7c5cff;
|
--accent: #3b82f6;
|
||||||
|
--accent-contrast: #0b1020;
|
||||||
|
--input-bg: rgba(0, 0, 0, 0.25);
|
||||||
|
--shadow: 0 1px 2px rgba(0, 0, 0, 0.35), 0 10px 30px rgba(0, 0, 0, 0.45);
|
||||||
}
|
}
|
||||||
|
|
||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; }
|
||||||
|
|
@ -14,9 +62,10 @@ html, body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial;
|
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial;
|
||||||
background: radial-gradient(1200px 800px at 10% 10%, rgba(124, 92, 255, 0.25), transparent 60%),
|
background:
|
||||||
radial-gradient(1000px 600px at 80% 30%, rgba(0, 209, 255, 0.14), transparent 60%),
|
radial-gradient(1000px 600px at 15% 10%, rgba(37, 99, 235, 0.12), transparent 60%),
|
||||||
var(--bg);
|
radial-gradient(900px 500px at 85% 25%, rgba(99, 102, 241, 0.10), transparent 55%),
|
||||||
|
var(--bg);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,6 +88,7 @@ html, body {
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
|
|
@ -55,6 +105,34 @@ html, body {
|
||||||
.status[data-state="connecting"] { color: #faa61a; }
|
.status[data-state="connecting"] { color: #faa61a; }
|
||||||
.status[data-state="disconnected"] { color: #ed4245; }
|
.status[data-state="disconnected"] { color: #ed4245; }
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-select {
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--input-bg);
|
||||||
|
color: var(--text);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-select:focus {
|
||||||
|
border-color: rgba(37, 99, 235, 0.65);
|
||||||
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
.chat {
|
.chat {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
|
@ -62,6 +140,7 @@ html, body {
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg {
|
.msg {
|
||||||
|
|
@ -78,13 +157,13 @@ html, body {
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
background: rgba(255, 255, 255, 0.05);
|
background: var(--panel-solid);
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg.user .bubble {
|
.msg.user .bubble {
|
||||||
background: rgba(124, 92, 255, 0.22);
|
background: rgba(37, 99, 235, 0.12);
|
||||||
border-color: rgba(124, 92, 255, 0.42);
|
border-color: rgba(37, 99, 235, 0.28);
|
||||||
}
|
}
|
||||||
|
|
||||||
.composer {
|
.composer {
|
||||||
|
|
@ -96,6 +175,7 @@ html, body {
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
|
|
@ -103,25 +183,25 @@ html, body {
|
||||||
padding: 12px 12px;
|
padding: 12px 12px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
background: rgba(0, 0, 0, 0.25);
|
background: var(--input-bg);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input:focus {
|
.input:focus {
|
||||||
border-color: rgba(124, 92, 255, 0.8);
|
border-color: rgba(37, 99, 235, 0.65);
|
||||||
box-shadow: 0 0 0 3px rgba(124, 92, 255, 0.25);
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
border: 1px solid rgba(124, 92, 255, 0.6);
|
border: 1px solid rgba(37, 99, 235, 0.45);
|
||||||
background: rgba(124, 92, 255, 0.30);
|
background: rgba(37, 99, 235, 0.14);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn:hover {
|
.btn:hover {
|
||||||
background: rgba(124, 92, 255, 0.42);
|
background: rgba(37, 99, 235, 0.22);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue