288 lines
8.4 KiB
HTML
288 lines
8.4 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=320, height=240">
|
|
<title>GB Embedded</title>
|
|
<style>
|
|
/* Optimized for LCD displays: 320x240, 128x64, 16x2 character displays */
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
:root {
|
|
--bg: #000;
|
|
--fg: #0f0;
|
|
--border: #0a0;
|
|
--font-size: 14px;
|
|
--line-height: 1.2;
|
|
}
|
|
|
|
/* High contrast mode for outdoor/industrial displays */
|
|
.high-contrast {
|
|
--bg: #000;
|
|
--fg: #fff;
|
|
--border: #fff;
|
|
}
|
|
|
|
/* E-ink mode */
|
|
.eink {
|
|
--bg: #fff;
|
|
--fg: #000;
|
|
--border: #000;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Courier New', monospace;
|
|
background: var(--bg);
|
|
color: var(--fg);
|
|
font-size: var(--font-size);
|
|
line-height: var(--line-height);
|
|
height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Header: 1 line */
|
|
header {
|
|
padding: 2px 4px;
|
|
border-bottom: 1px solid var(--border);
|
|
font-weight: bold;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
height: 20px;
|
|
}
|
|
|
|
.status {
|
|
font-size: 10px;
|
|
}
|
|
|
|
/* Messages area */
|
|
#messages {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 4px;
|
|
}
|
|
|
|
.msg {
|
|
margin-bottom: 4px;
|
|
word-wrap: break-word;
|
|
}
|
|
|
|
.msg-user {
|
|
text-align: right;
|
|
}
|
|
|
|
.msg-user::before {
|
|
content: "> ";
|
|
}
|
|
|
|
.msg-bot::before {
|
|
content: "< ";
|
|
}
|
|
|
|
/* Input area */
|
|
footer {
|
|
border-top: 1px solid var(--border);
|
|
padding: 2px 4px;
|
|
display: flex;
|
|
gap: 4px;
|
|
height: 24px;
|
|
}
|
|
|
|
#input {
|
|
flex: 1;
|
|
background: var(--bg);
|
|
color: var(--fg);
|
|
border: 1px solid var(--border);
|
|
font-family: inherit;
|
|
font-size: var(--font-size);
|
|
padding: 0 4px;
|
|
}
|
|
|
|
#input:focus {
|
|
outline: none;
|
|
border-color: var(--fg);
|
|
}
|
|
|
|
button {
|
|
background: var(--bg);
|
|
color: var(--fg);
|
|
border: 1px solid var(--border);
|
|
font-family: inherit;
|
|
font-size: var(--font-size);
|
|
padding: 0 8px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
button:active {
|
|
background: var(--fg);
|
|
color: var(--bg);
|
|
}
|
|
|
|
/* Scrollbar minimal */
|
|
::-webkit-scrollbar { width: 4px; }
|
|
::-webkit-scrollbar-track { background: var(--bg); }
|
|
::-webkit-scrollbar-thumb { background: var(--border); }
|
|
|
|
/* Loading indicator */
|
|
.loading::after {
|
|
content: "...";
|
|
animation: dots 1s infinite;
|
|
}
|
|
|
|
@keyframes dots {
|
|
0%, 33% { content: "."; }
|
|
34%, 66% { content: ".."; }
|
|
67%, 100% { content: "..."; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<span>GB</span>
|
|
<span class="status" id="status">--</span>
|
|
</header>
|
|
|
|
<div id="messages"></div>
|
|
|
|
<footer>
|
|
<input type="text" id="input" placeholder=">" autofocus>
|
|
<button id="send">OK</button>
|
|
</footer>
|
|
|
|
<script>
|
|
// Configuration
|
|
const CONFIG = {
|
|
serverUrl: window.BOTSERVER_URL || 'http://localhost:8088',
|
|
maxMessages: 10, // Keep memory low
|
|
maxMsgLen: 100, // Truncate long messages
|
|
};
|
|
|
|
// State
|
|
let ws = null;
|
|
let sessionId = null;
|
|
let userId = null;
|
|
|
|
// DOM
|
|
const messagesEl = document.getElementById('messages');
|
|
const inputEl = document.getElementById('input');
|
|
const statusEl = document.getElementById('status');
|
|
const sendBtn = document.getElementById('send');
|
|
|
|
// Truncate text for small displays
|
|
function truncate(text, len) {
|
|
return text.length > len ? text.substring(0, len - 3) + '...' : text;
|
|
}
|
|
|
|
// Add message to display
|
|
function addMsg(type, text) {
|
|
const div = document.createElement('div');
|
|
div.className = `msg msg-${type}`;
|
|
div.textContent = truncate(text, CONFIG.maxMsgLen);
|
|
messagesEl.appendChild(div);
|
|
|
|
// Remove old messages to save memory
|
|
while (messagesEl.children.length > CONFIG.maxMessages) {
|
|
messagesEl.removeChild(messagesEl.firstChild);
|
|
}
|
|
|
|
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
}
|
|
|
|
// Update status
|
|
function setStatus(s) {
|
|
statusEl.textContent = s;
|
|
}
|
|
|
|
// Connect to server
|
|
async function connect() {
|
|
setStatus('...');
|
|
|
|
try {
|
|
// Get auth
|
|
const res = await fetch(`${CONFIG.serverUrl}/api/auth?bot_name=default`);
|
|
const auth = await res.json();
|
|
sessionId = auth.session_id;
|
|
userId = auth.user_id;
|
|
|
|
// Connect WebSocket
|
|
const wsUrl = CONFIG.serverUrl.replace('http', 'ws');
|
|
ws = new WebSocket(`${wsUrl}/ws?session_id=${sessionId}&user_id=${userId}`);
|
|
|
|
ws.onopen = () => {
|
|
setStatus('OK');
|
|
addMsg('bot', 'Ready');
|
|
};
|
|
|
|
ws.onclose = () => {
|
|
setStatus('--');
|
|
setTimeout(connect, 3000);
|
|
};
|
|
|
|
ws.onerror = () => {
|
|
setStatus('ERR');
|
|
};
|
|
|
|
ws.onmessage = (e) => {
|
|
try {
|
|
const data = JSON.parse(e.data);
|
|
if (data.content && data.is_complete) {
|
|
addMsg('bot', data.content);
|
|
} else if (data.content) {
|
|
// Streaming - show partial
|
|
const last = messagesEl.lastChild;
|
|
if (last && last.classList.contains('msg-bot') && last.classList.contains('loading')) {
|
|
last.textContent = truncate(data.content, CONFIG.maxMsgLen);
|
|
} else {
|
|
const div = document.createElement('div');
|
|
div.className = 'msg msg-bot loading';
|
|
div.textContent = truncate(data.content, CONFIG.maxMsgLen);
|
|
messagesEl.appendChild(div);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
// Ignore parse errors
|
|
}
|
|
};
|
|
} catch (err) {
|
|
setStatus('ERR');
|
|
setTimeout(connect, 5000);
|
|
}
|
|
}
|
|
|
|
// Send message
|
|
function send() {
|
|
const text = inputEl.value.trim();
|
|
if (!text || !ws || ws.readyState !== WebSocket.OPEN) return;
|
|
|
|
addMsg('user', text);
|
|
|
|
ws.send(JSON.stringify({
|
|
bot_id: 'default_bot',
|
|
user_id: userId,
|
|
session_id: sessionId,
|
|
channel: 'embedded',
|
|
content: text,
|
|
message_type: 1, // USER
|
|
timestamp: new Date().toISOString()
|
|
}));
|
|
|
|
inputEl.value = '';
|
|
}
|
|
|
|
// Event listeners
|
|
sendBtn.onclick = send;
|
|
inputEl.onkeypress = (e) => { if (e.key === 'Enter') send(); };
|
|
|
|
// Keyboard navigation for devices without touch
|
|
document.onkeydown = (e) => {
|
|
if (e.key === 'Escape') {
|
|
inputEl.value = '';
|
|
}
|
|
};
|
|
|
|
// Start
|
|
connect();
|
|
</script>
|
|
</body>
|
|
</html>
|