OSVauco/static/jason.html
Chris Christiansen 9fcb9c354a
Some checks are pending
Check Python Version Consistency / Check Python Version (push) Waiting to run
feat(core): Fresh initialization - Deploy v3.6.1 Singularity Architecture
2026-09-03 04:03:09 +00:00

473 lines
19 KiB
HTML

<!DOCTYPE html>
<html lang="no">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex, nofollow">
<title>Jason — Vauco Operator</title>
<link rel="manifest" href="/manifest.json">
<style>
:root{
--bg:#0a0a0f;--bg-2:#11131a;--bg-3:#161922;
--border:rgba(255,255,255,0.08);--border-2:rgba(255,255,255,0.14);
--text:#e8eaf0;--text-dim:#8b909c;--text-faint:#5a5f6b;
--accent:#7c5cff;--accent-2:#00d4a8;
--green:#3ecf8e;--amber:#ffb547;--danger:#ff5c7c;
--radius:12px;--radius-lg:18px;
--sans:-apple-system,BlinkMacSystemFont,'Inter',system-ui,sans-serif;
--mono:'SF Mono',ui-monospace,'JetBrains Mono',Menlo,monospace;
}
*{margin:0;padding:0;box-sizing:border-box}
html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--sans);-webkit-font-smoothing:antialiased}
button{font-family:inherit;cursor:pointer;border:none;background:none;color:inherit}
::-webkit-scrollbar{width:8px}
::-webkit-scrollbar-thumb{background:var(--bg-3);border-radius:6px}
/* layout */
.shell{display:flex;height:100vh;overflow:hidden}
/* sidebar */
.sidebar{
width:220px;flex-shrink:0;
background:var(--bg-2);border-right:1px solid var(--border);
display:flex;flex-direction:column;padding:20px 12px;
gap:4px;
}
.brand{display:flex;align-items:center;gap:10px;padding:8px 10px 20px}
.logo{width:28px;height:28px;border-radius:8px;background:linear-gradient(135deg,var(--accent),var(--accent-2));display:flex;align-items:center;justify-content:center;color:#0a0a0f;font-weight:900;font-size:13px;flex-shrink:0}
.brand-name{font-weight:800;font-size:15px;letter-spacing:-0.02em}
.side-label{font-size:10px;text-transform:uppercase;letter-spacing:0.08em;color:var(--text-faint);font-weight:700;padding:12px 10px 4px}
.nav-item{display:flex;align-items:center;gap:9px;padding:8px 10px;border-radius:9px;font-size:13px;color:var(--text-dim);font-weight:500;cursor:pointer;text-decoration:none;transition:background .14s,color .14s}
.nav-item:hover{background:var(--bg-3);color:var(--text)}
.nav-item.active{background:var(--bg-3);color:var(--text)}
.nav-item svg{flex-shrink:0;opacity:.75}
.side-foot{margin-top:auto;padding-top:14px;border-top:1px solid var(--border)}
.me-card{padding:10px 10px;border-radius:9px;background:var(--bg-3);border:1px solid var(--border)}
.me-name{font-size:12px;font-weight:600}
.me-role{font-size:10px;color:var(--text-faint);margin-top:1px;font-family:var(--mono)}
.session-id{font-size:9px;color:var(--text-faint);font-family:var(--mono);margin-top:6px;word-break:break-all}
/* chat area */
.chat-wrap{
flex:1;display:flex;flex-direction:column;overflow:hidden;
background:var(--bg);
}
.chat-head{
display:flex;align-items:center;gap:12px;
padding:16px 24px;border-bottom:1px solid var(--border);
background:rgba(10,10,15,0.9);backdrop-filter:blur(16px);
flex-shrink:0;
}
.jason-avatar{
width:36px;height:36px;border-radius:10px;
background:linear-gradient(135deg,var(--accent) 0%,var(--accent-2) 100%);
display:flex;align-items:center;justify-content:center;
font-size:18px;flex-shrink:0;
}
.jason-info{flex:1}
.jason-name{font-size:15px;font-weight:700;letter-spacing:-0.01em}
.jason-status{font-size:12px;color:var(--text-dim);display:flex;align-items:center;gap:5px;margin-top:1px}
.dot{width:6px;height:6px;border-radius:50%;background:var(--green);flex-shrink:0;animation:pulse 2.4s infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}
.head-actions{display:flex;gap:8px}
.head-btn{
padding:6px 14px;border-radius:8px;font-size:12px;font-weight:600;
background:var(--bg-2);border:1px solid var(--border);color:var(--text-dim);
transition:background .14s,color .14s;
}
.head-btn:hover{background:var(--bg-3);color:var(--text)}
.head-btn.danger:hover{color:var(--danger);border-color:rgba(255,92,124,0.3)}
/* messages */
.messages{
flex:1;overflow-y:auto;
padding:28px 32px;
display:flex;flex-direction:column;gap:20px;
}
.msg{display:flex;gap:12px;max-width:820px;width:100%}
.msg.user{align-self:flex-end;flex-direction:row-reverse}
.msg-avatar{
width:32px;height:32px;border-radius:9px;flex-shrink:0;
display:flex;align-items:center;justify-content:center;font-size:14px;
}
.msg.user .msg-avatar{background:var(--accent);color:#fff;font-weight:700;font-size:12px}
.msg.jason .msg-avatar{background:linear-gradient(135deg,var(--accent),var(--accent-2));font-size:15px}
.msg-body{display:flex;flex-direction:column;gap:4px;min-width:0}
.msg.user .msg-body{align-items:flex-end}
.msg-meta{font-size:11px;color:var(--text-faint);font-family:var(--mono)}
.msg-bubble{
padding:12px 16px;border-radius:14px;
font-size:14px;line-height:1.65;
max-width:700px;word-break:break-word;
white-space:pre-wrap;
}
.msg.user .msg-bubble{
background:var(--accent);
color:#fff;border-radius:14px 4px 14px 14px;
}
.msg.jason .msg-bubble{
background:var(--bg-2);border:1px solid var(--border);
color:var(--text);border-radius:4px 14px 14px 14px;
}
.msg-bubble code{
font-family:var(--mono);font-size:12px;
background:rgba(0,0,0,0.3);padding:1px 5px;border-radius:4px;
}
.msg-bubble pre{
background:rgba(0,0,0,0.35);padding:12px;border-radius:8px;
overflow-x:auto;font-family:var(--mono);font-size:12px;
margin:8px 0;
}
/* typing indicator */
.typing-dots{display:flex;gap:4px;align-items:center;padding:4px 0}
.typing-dots span{
width:6px;height:6px;border-radius:50%;background:var(--text-faint);
animation:bounce 1.2s infinite;
}
.typing-dots span:nth-child(2){animation-delay:.2s}
.typing-dots span:nth-child(3){animation-delay:.4s}
@keyframes bounce{0%,80%,100%{transform:translateY(0)}40%{transform:translateY(-5px)}}
/* input area */
.input-wrap{
padding:16px 24px 20px;
border-top:1px solid var(--border);
background:var(--bg);flex-shrink:0;
}
.input-row{
display:flex;gap:10px;align-items:flex-end;
background:var(--bg-2);border:1px solid var(--border-2);
border-radius:14px;padding:10px 14px;
transition:border-color .2s;
}
.input-row:focus-within{border-color:var(--accent)}
#msg-input{
flex:1;background:none;border:none;color:var(--text);
font-family:var(--sans);font-size:14px;resize:none;
outline:none;min-height:22px;max-height:160px;
line-height:1.55;
}
#msg-input::placeholder{color:var(--text-faint)}
.send-btn{
width:34px;height:34px;border-radius:9px;
background:var(--accent);color:#fff;
display:flex;align-items:center;justify-content:center;
flex-shrink:0;transition:background .15s,opacity .15s;
}
.send-btn:hover{background:#6a4cff}
.send-btn:disabled{opacity:.38;cursor:default}
.input-hint{font-size:11px;color:var(--text-faint);margin-top:7px;padding:0 4px}
/* empty state */
.empty-state{
flex:1;display:flex;flex-direction:column;align-items:center;
justify-content:center;gap:16px;padding:40px;
text-align:center;
}
.empty-icon{font-size:48px}
.empty-title{font-size:20px;font-weight:700;letter-spacing:-0.02em}
.empty-desc{font-size:14px;color:var(--text-dim);max-width:380px;line-height:1.6}
.suggestions{display:flex;flex-wrap:wrap;gap:8px;justify-content:center;margin-top:8px}
.sug-btn{
padding:8px 16px;border-radius:99px;
background:var(--bg-2);border:1px solid var(--border);
font-size:13px;color:var(--text-dim);
transition:background .14s,color .14s,border-color .14s;
}
.sug-btn:hover{background:var(--bg-3);color:var(--text);border-color:var(--border-2)}
/* error banner */
.error-bar{
display:none;background:rgba(255,92,124,0.1);border:1px solid rgba(255,92,124,0.2);
color:var(--danger);font-size:12px;padding:8px 16px;border-radius:8px;
margin:0 24px 8px;text-align:center;
}
.error-bar.show{display:block}
</style>
</head>
<body>
<div class="shell">
<!-- sidebar -->
<nav class="sidebar">
<div class="brand">
<div class="logo">V</div>
<span class="brand-name">Vauco OS</span>
</div>
<div class="side-label">Grensesnitt</div>
<a class="nav-item active" href="/jason">
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><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>
Jason
</a>
<a class="nav-item" href="/opax">
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 21V9"/></svg>
OPAX Hub
</a>
<a class="nav-item" href="/billing" target="_blank">
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
CostGuard
</a>
<div class="side-label">Tilgang</div>
<a class="nav-item" href="https://console.cloud.google.com/run?project=propane-will-491900-m5" target="_blank">
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
Cloud Run
</a>
<a class="nav-item" href="https://console.cloud.google.com/cloud-build/builds?project=propane-will-491900-m5" target="_blank">
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
Cloud Build
</a>
<a class="nav-item" href="https://ssh.cloud.google.com/v2/ssh/projects/propane-will-491900-m5/zones/us-central1-b/instances/osvauco-dev-from-snap" target="_blank">
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>
VM Shell
</a>
<div class="side-foot">
<div class="me-card">
<div class="me-name">Chris Christiansen</div>
<div class="me-role">operator · vauco.no</div>
<div class="session-id" id="sidebar-session">session: laster...</div>
</div>
</div>
</nav>
<!-- chat -->
<div class="chat-wrap">
<div class="chat-head">
<div class="jason-avatar">🤖</div>
<div class="jason-info">
<div class="jason-name">Jason</div>
<div class="jason-status"><span class="dot"></span> Online · ADK + Gemini 2.5 Pro</div>
</div>
<div class="head-actions">
<button class="head-btn" onclick="exportHistory()">⬇ Eksport</button>
<button class="head-btn danger" onclick="clearHistory()">Tøm chat</button>
</div>
</div>
<div class="error-bar" id="error-bar"></div>
<div class="messages" id="messages">
<div class="empty-state" id="empty-state">
<div class="empty-icon">🤖</div>
<div class="empty-title">Jason er klar</div>
<div class="empty-desc">Primærgrensesnitt for Vauco OS. Still spørsmål, gi oppgaver, eller be om status på infrastruktur, builds og agenter.</div>
<div class="suggestions">
<button class="sug-btn" onclick="suggest(this)">Hva er status på siste build?</button>
<button class="sug-btn" onclick="suggest(this)">Oppsummer token-forbruk siste 7 dager</button>
<button class="sug-btn" onclick="suggest(this)">Hvilke oppgaver er aktive i ROADMAP?</button>
<button class="sug-btn" onclick="suggest(this)">Lag en ny Cloud Run-tjeneste</button>
</div>
</div>
</div>
<div class="input-wrap">
<div class="input-row">
<textarea id="msg-input" rows="1" placeholder="Skriv til Jason... (Enter sender, Shift+Enter linjeskift)"
oninput="autoResize(this)" onkeydown="handleKey(event)"></textarea>
<button class="send-btn" id="send-btn" onclick="sendMessage()" disabled>
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
<line x1="22" y1="2" x2="11" y2="13"/>
<polygon points="22 2 15 22 11 13 2 9 22 2"/>
</svg>
</button>
</div>
<div class="input-hint">Jason har tilgang til GCP, GitHub og Vauco-agenter. Historikk lagres lokalt i denne nettleseren.</div>
</div>
</div>
</div>
<script>
// ── Config ────────────────────────────────────────────────────────────────────
const AGENT_URL = '' // tom = samme origin (Cloud Run serverer /run)
const USER_ID = 'chris'
const STORAGE_KEY = 'jason_history_v2'
const SESSION_KEY = 'jason_session_v2'
// ── Session ───────────────────────────────────────────────────────────────────
function getSessionId() {
let sid = sessionStorage.getItem(SESSION_KEY)
if (!sid) {
sid = 'ses-' + Date.now() + '-' + Math.random().toString(36).slice(2,8)
sessionStorage.setItem(SESSION_KEY, sid)
}
return sid
}
const SESSION_ID = getSessionId()
document.getElementById('sidebar-session').textContent = 'session: ' + SESSION_ID.slice(0,18) + '…'
// ── History ───────────────────────────────────────────────────────────────────
function loadHistory() {
try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]') }
catch { return [] }
}
function saveHistory(h) {
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(h.slice(-120))) }
catch {}
}
// ── Render ────────────────────────────────────────────────────────────────────
function fmtTime(ts) {
const d = new Date(ts)
return d.toLocaleTimeString('no-NO', {hour:'2-digit',minute:'2-digit'})
}
function escHtml(s) {
return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
}
function renderMarkdown(text) {
// minimal: code blocks, inline code, bold
let s = escHtml(text)
s = s.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>')
s = s.replace(/`([^`]+)`/g, '<code>$1</code>')
s = s.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
return s
}
function appendMsg(role, text, ts) {
const empty = document.getElementById('empty-state')
if (empty) empty.remove()
const wrap = document.getElementById('messages')
const isUser = role === 'user'
const div = document.createElement('div')
div.className = 'msg ' + (isUser ? 'user' : 'jason')
div.innerHTML = `
<div class="msg-avatar">${isUser ? 'CC' : '🤖'}</div>
<div class="msg-body">
<div class="msg-meta">${isUser ? 'deg' : 'Jason'} · ${fmtTime(ts || Date.now())}</div>
<div class="msg-bubble">${isUser ? escHtml(text) : renderMarkdown(text)}</div>
</div>`
wrap.appendChild(div)
wrap.scrollTop = wrap.scrollHeight
return div
}
function appendTyping() {
const empty = document.getElementById('empty-state')
if (empty) empty.remove()
const wrap = document.getElementById('messages')
const div = document.createElement('div')
div.className = 'msg jason'
div.id = 'typing-indicator'
div.innerHTML = `
<div class="msg-avatar">🤖</div>
<div class="msg-body">
<div class="msg-meta">Jason · skriver...</div>
<div class="msg-bubble"><div class="typing-dots"><span></span><span></span><span></span></div></div>
</div>`
wrap.appendChild(div)
wrap.scrollTop = wrap.scrollHeight
}
function removeTyping() {
const t = document.getElementById('typing-indicator')
if (t) t.remove()
}
// ── Input ─────────────────────────────────────────────────────────────────────
const input = document.getElementById('msg-input')
const sendBtn = document.getElementById('send-btn')
input.addEventListener('input', () => {
sendBtn.disabled = !input.value.trim()
})
function autoResize(el) {
el.style.height = 'auto'
el.style.height = Math.min(el.scrollHeight, 160) + 'px'
}
function handleKey(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
if (!sendBtn.disabled) sendMessage()
}
}
function suggest(btn) {
input.value = btn.textContent.trim()
input.dispatchEvent(new Event('input', { bubbles: true }))
autoResize(input)
sendBtn.disabled = false
sendMessage()
}
// ── Send ──────────────────────────────────────────────────────────────────────
let isSending = false
async function sendMessage() {
if (isSending) return
const text = input.value.trim()
if (!text) return
isSending = true
sendBtn.disabled = true
input.value = ''
input.style.height = 'auto'
hideError()
const ts = Date.now()
const history = loadHistory()
history.push({role:'user', text, ts})
saveHistory(history)
appendMsg('user', text, ts)
appendTyping()
try {
const res = await fetch(AGENT_URL + '/run', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({user_id: USER_ID, session_id: SESSION_ID, message: text})
})
removeTyping()
if (!res.ok) throw new Error('HTTP ' + res.status)
const data = await res.json()
const reply = data.response || '(tomt svar)'
const rts = Date.now()
history.push({role:'jason', text:reply, ts:rts})
saveHistory(history)
appendMsg('jason', reply, rts)
} catch(err) {
removeTyping()
showError('Jason svarte ikke: ' + err.message + ' — sjekk at Cloud Run er oppe.')
} finally {
isSending = false
sendBtn.disabled = !input.value.trim()
input.focus()
}
}
// ── Utilities ─────────────────────────────────────────────────────────────────
function showError(msg) {
const bar = document.getElementById('error-bar')
bar.textContent = msg
bar.classList.add('show')
}
function hideError() {
document.getElementById('error-bar').classList.remove('show')
}
function clearHistory() {
if (!confirm('Tøm hele chat-historikken?')) return
localStorage.removeItem(STORAGE_KEY)
sessionStorage.removeItem(SESSION_KEY)
location.reload()
}
function exportHistory() {
const h = loadHistory()
const lines = h.map(m => `[${new Date(m.ts).toISOString()}] ${m.role.toUpperCase()}: ${m.text}`)
const blob = new Blob([lines.join('\n\n')], {type:'text/plain'})
const a = document.createElement('a')
a.href = URL.createObjectURL(blob)
a.download = 'jason-historikk-' + new Date().toISOString().slice(0,10) + '.txt'
a.click()
}
// ── Restore history on load ───────────────────────────────────────────────────
;(function restoreHistory(){
const h = loadHistory()
if (!h.length) return
h.forEach(m => appendMsg(m.role, m.text, m.ts))
document.getElementById('messages').scrollTop = 99999
})()
input.focus()
</script>
</body>
</html>