Add embedded UI interface for LCD/keyboard devices (IoT, Raspberry Pi, ESP32)
This commit is contained in:
parent
a63d0bdd34
commit
d929cfb525
1 changed files with 288 additions and 0 deletions
288
ui/embedded/index.html
Normal file
288
ui/embedded/index.html
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
<!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>
|
||||
Loading…
Add table
Reference in a new issue