Major changes: - Config-driven adapters: all channels (Slack, Discord, Telegram, WebChat, Webhooks) controlled via config.json with enabled flags and token auto-detection, no CLI flags required - Runtime engine field: runtime.engine selects opencode/claude from config - Interactive install script: 8-phase setup wizard with AI runtime detection/installation, token setup, identity file personalization (personality presets), aetheel CLI command, background service (launchd/systemd) - Live runtime switching: /engine, /model, /provider commands hot-swap the AI runtime from chat without restart, changes persisted to config.json - Usage tracking: per-request cost extraction from Claude Code JSON output, cumulative stats via /usage command - Auto-failover: rate limit detection on both runtimes, automatic switch to other engine on quota errors with user notification - Chat commands work without / prefix (Slack intercepts / in channels), commands: engine, model, provider, config, usage, reload, cron, subagents, status, help - /config set for editing config.json from chat with dotted key notation - Security audit saved to docs/security-audit.md - Full command reference in docs/commands.md - Future changes doc with NanoClaw agent teams analysis - Logo added to README and WebChat UI - README fully rewritten with all features documented
221 lines
7.6 KiB
HTML
221 lines
7.6 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Aetheel Chat</title>
|
|
<link rel="icon" type="image/jpeg" href="/logo.jpeg">
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background: #1a1a2e;
|
|
color: #e0e0e0;
|
|
height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
#header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 12px 20px;
|
|
background: #16213e;
|
|
border-bottom: 1px solid #0f3460;
|
|
flex-shrink: 0;
|
|
}
|
|
#header .title { font-size: 1.1rem; font-weight: 600; }
|
|
#status {
|
|
font-size: 0.8rem;
|
|
padding: 3px 10px;
|
|
border-radius: 12px;
|
|
background: #333;
|
|
color: #aaa;
|
|
}
|
|
#status.connected { background: #0a3d2a; color: #4ade80; }
|
|
#status.disconnected { background: #3d0a0a; color: #f87171; }
|
|
#messages {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 16px 20px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
.msg {
|
|
max-width: 80%;
|
|
padding: 10px 14px;
|
|
border-radius: 12px;
|
|
line-height: 1.5;
|
|
word-wrap: break-word;
|
|
font-size: 0.95rem;
|
|
}
|
|
.msg.user {
|
|
align-self: flex-end;
|
|
background: #0f3460;
|
|
color: #e0e0e0;
|
|
border-bottom-right-radius: 4px;
|
|
}
|
|
.msg.ai {
|
|
align-self: flex-start;
|
|
background: #222244;
|
|
color: #d0d0d0;
|
|
border-bottom-left-radius: 4px;
|
|
}
|
|
.msg.ai strong { color: #93c5fd; }
|
|
.msg.ai code {
|
|
background: #1a1a2e;
|
|
padding: 1px 5px;
|
|
border-radius: 4px;
|
|
font-size: 0.88em;
|
|
font-family: 'Fira Code', 'Consolas', monospace;
|
|
}
|
|
.msg.ai pre {
|
|
background: #111128;
|
|
padding: 10px 12px;
|
|
border-radius: 6px;
|
|
overflow-x: auto;
|
|
margin: 6px 0;
|
|
font-size: 0.88em;
|
|
font-family: 'Fira Code', 'Consolas', monospace;
|
|
line-height: 1.4;
|
|
}
|
|
.msg.ai pre code { background: none; padding: 0; }
|
|
.msg.ai ul, .msg.ai ol { padding-left: 20px; margin: 4px 0; }
|
|
#input-area {
|
|
display: flex;
|
|
gap: 8px;
|
|
padding: 12px 20px;
|
|
background: #16213e;
|
|
border-top: 1px solid #0f3460;
|
|
flex-shrink: 0;
|
|
}
|
|
#input {
|
|
flex: 1;
|
|
padding: 10px 14px;
|
|
border: 1px solid #0f3460;
|
|
border-radius: 8px;
|
|
background: #1a1a2e;
|
|
color: #e0e0e0;
|
|
font-size: 0.95rem;
|
|
outline: none;
|
|
}
|
|
#input:focus { border-color: #3b82f6; }
|
|
#send {
|
|
padding: 10px 20px;
|
|
border: none;
|
|
border-radius: 8px;
|
|
background: #3b82f6;
|
|
color: #fff;
|
|
font-size: 0.95rem;
|
|
cursor: pointer;
|
|
font-weight: 500;
|
|
}
|
|
#send:hover { background: #2563eb; }
|
|
#send:disabled { background: #333; cursor: not-allowed; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="header">
|
|
<div style="display:flex;align-items:center;gap:8px">
|
|
<img src="/logo.jpeg" alt="Aetheel" width="28" height="28" style="border-radius:6px">
|
|
<span class="title">Aetheel</span>
|
|
</div>
|
|
<span id="status">connecting…</span>
|
|
</div>
|
|
<div id="messages" role="log" aria-live="polite"></div>
|
|
<div id="input-area">
|
|
<input type="text" id="input" placeholder="Type a message…" autofocus
|
|
aria-label="Chat message input">
|
|
<button id="send" aria-label="Send message">Send</button>
|
|
</div>
|
|
<script>
|
|
(function () {
|
|
var messagesEl = document.getElementById('messages');
|
|
var inputEl = document.getElementById('input');
|
|
var sendBtn = document.getElementById('send');
|
|
var statusEl = document.getElementById('status');
|
|
var ws = null;
|
|
var reconnectTimer = null;
|
|
|
|
function renderMarkdown(text) {
|
|
var html = text
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>');
|
|
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, function (_, lang, code) {
|
|
return '<pre><code>' + code.trimEnd() + '</code></pre>';
|
|
});
|
|
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
|
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
|
|
html = html.replace(/^[\-\*] (.+)$/gm, '<li>$1</li>');
|
|
html = html.replace(/(<li>[\s\S]*?<\/li>\n?)+/g, function (m) {
|
|
return '<ul>' + m + '</ul>';
|
|
});
|
|
html = html.replace(/\n/g, '<br>');
|
|
html = html.replace(/<pre><code>([\s\S]*?)<\/code><\/pre>/g, function (_, c) {
|
|
return '<pre><code>' + c.replace(/<br>/g, '\n') + '</code></pre>';
|
|
});
|
|
html = html.replace(/<ul>([\s\S]*?)<\/ul>/g, function (_, items) {
|
|
return '<ul>' + items.replace(/<br>/g, '') + '</ul>';
|
|
});
|
|
return html;
|
|
}
|
|
|
|
function addMessage(text, type) {
|
|
var div = document.createElement('div');
|
|
div.className = 'msg ' + type;
|
|
div.setAttribute('role', 'article');
|
|
if (type === 'ai') {
|
|
div.innerHTML = renderMarkdown(text);
|
|
} else {
|
|
div.textContent = text;
|
|
}
|
|
messagesEl.appendChild(div);
|
|
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
}
|
|
|
|
function setStatus(state) {
|
|
statusEl.className = state;
|
|
var labels = { connected: 'connected', disconnected: 'disconnected', '': 'connecting\u2026' };
|
|
statusEl.textContent = labels[state] || state;
|
|
}
|
|
|
|
function connect() {
|
|
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return;
|
|
setStatus('');
|
|
var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
ws = new WebSocket(proto + '//' + location.host + '/ws');
|
|
ws.onopen = function () {
|
|
setStatus('connected');
|
|
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
|
|
};
|
|
ws.onmessage = function (event) { addMessage(event.data, 'ai'); };
|
|
ws.onclose = function () {
|
|
setStatus('disconnected');
|
|
ws = null;
|
|
reconnectTimer = setTimeout(connect, 3000);
|
|
};
|
|
ws.onerror = function () { ws.close(); };
|
|
}
|
|
|
|
function send() {
|
|
var text = inputEl.value.trim();
|
|
if (!text || !ws || ws.readyState !== WebSocket.OPEN) return;
|
|
addMessage(text, 'user');
|
|
ws.send(text);
|
|
inputEl.value = '';
|
|
inputEl.focus();
|
|
}
|
|
|
|
sendBtn.addEventListener('click', send);
|
|
inputEl.addEventListener('keydown', function (e) {
|
|
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); }
|
|
});
|
|
|
|
connect();
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html> |