Initial clean commit - unified Lyra stack

This commit is contained in:
serversdwn
2025-11-16 03:17:32 -05:00
commit 94fb091e59
270 changed files with 74200 additions and 0 deletions

270
core/ui/index.html Normal file
View File

@@ -0,0 +1,270 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Lyra Core Chat</title>
<link rel="stylesheet" href="style.css" />
<!-- PWA -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<link rel="manifest" href="manifest.json" />
</head>
<body>
<div id="chat">
<!-- Model selector -->
<div id="model-select">
<label for="model">Model:</label>
<select id="model">
<option value="gpt-4o-mini">GPT-4o-mini (OpenAI)</option>
<option value="ollama:nollama/mythomax-l2-13b:Q5_K_S">Ollama MythoMax (3090)</option>
</select>
<div id="theme-toggle">
<button id="toggleThemeBtn">🌙 Dark Mode</button>
</div>
</div>
<!-- Session selector -->
<div id="session-select">
<label for="sessions">Session:</label>
<select id="sessions"></select>
<button id="newSessionBtn"> New</button>
<button id="renameSessionBtn">✏️ Rename</button>
</div>
<!-- Status -->
<div id="status">
<span id="status-dot"></span>
<span id="status-text">Checking Relay...</span>
</div>
<!-- Chat messages -->
<div id="messages"></div>
<!-- Input box -->
<div id="input">
<input id="userInput" type="text" placeholder="Type a message..." autofocus />
<button id="sendBtn">Send</button>
</div>
</div>
<script>
const RELAY_BASE = "http://10.0.0.40:7078";
const API_URL = `${RELAY_BASE}/v1/chat/completions`;
function generateSessionId() {
return "sess-" + Math.random().toString(36).substring(2, 10);
}
let history = [];
let currentSession = localStorage.getItem("currentSession") || null;
let sessions = JSON.parse(localStorage.getItem("sessions") || "[]");
function saveSessions() {
localStorage.setItem("sessions", JSON.stringify(sessions));
localStorage.setItem("currentSession", currentSession);
}
function renderSessions() {
const select = document.getElementById("sessions");
select.innerHTML = "";
sessions.forEach(s => {
const opt = document.createElement("option");
opt.value = s.id;
opt.textContent = s.name;
if (s.id === currentSession) opt.selected = true;
select.appendChild(opt);
});
}
function getSessionName(id) {
const s = sessions.find(s => s.id === id);
return s ? s.name : id;
}
async function loadSession(id) {
try {
const res = await fetch(`${RELAY_BASE}/sessions/${id}`);
const data = await res.json();
history = Array.isArray(data) ? data : [];
const messagesEl = document.getElementById("messages");
messagesEl.innerHTML = "";
history.forEach(m => addMessage(m.role, m.content));
addMessage("system", `📂 Loaded session: ${getSessionName(id)}${history.length} message(s)`);
} catch (e) {
addMessage("system", `Failed to load session: ${e.message}`);
}
}
async function saveSession() {
if (!currentSession) return;
try {
await fetch(`${RELAY_BASE}/sessions/${currentSession}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(history)
});
} catch (e) {
addMessage("system", `Failed to save session: ${e.message}`);
}
}
async function sendMessage() {
const inputEl = document.getElementById("userInput");
const msg = inputEl.value.trim();
if (!msg) return;
inputEl.value = "";
addMessage("user", msg);
history.push({ role: "user", content: msg });
await saveSession(); // ✅ persist both user + assistant messages
const model = document.getElementById("model").value;
// make sure we always include a stable user_id
let userId = localStorage.getItem("userId");
if (!userId) {
userId = "brian"; // use whatever ID you seeded Mem0 with
localStorage.setItem("userId", userId);
}
const body = {
model: model,
messages: history,
sessionId: currentSession
};
try {
const resp = await fetch(API_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body)
});
const data = await resp.json();
const reply = data.choices?.[0]?.message?.content || "(no reply)";
addMessage("assistant", reply);
history.push({ role: "assistant", content: reply });
await saveSession();
} catch (err) {
addMessage("system", "Error: " + err.message);
}
}
function addMessage(role, text) {
const messagesEl = document.getElementById("messages");
const msgDiv = document.createElement("div");
msgDiv.className = `msg ${role}`;
msgDiv.textContent = text;
messagesEl.appendChild(msgDiv);
// only auto-scroll if user is near bottom
const threshold = 120;
const isNearBottom = messagesEl.scrollHeight - messagesEl.scrollTop - messagesEl.clientHeight < threshold;
if (isNearBottom) {
messagesEl.scrollTo({ top: messagesEl.scrollHeight, behavior: "smooth" });
}
}
async function checkHealth() {
try {
const resp = await fetch(API_URL.replace("/v1/chat/completions", "/_health"));
if (resp.ok) {
document.getElementById("status-dot").className = "dot ok";
document.getElementById("status-text").textContent = "Relay Online";
} else {
throw new Error("Bad status");
}
} catch (err) {
document.getElementById("status-dot").className = "dot fail";
document.getElementById("status-text").textContent = "Relay Offline";
}
}
document.addEventListener("DOMContentLoaded", () => {
// Dark mode toggle
const btn = document.getElementById("toggleThemeBtn");
btn.addEventListener("click", () => {
document.body.classList.toggle("dark");
const isDark = document.body.classList.contains("dark");
btn.textContent = isDark ? "☀️ Light Mode" : "🌙 Dark Mode";
localStorage.setItem("theme", isDark ? "dark" : "light");
});
if (localStorage.getItem("theme") === "dark") {
document.body.classList.add("dark");
btn.textContent = "☀️ Light Mode";
}
// Sessions
// Populate dropdown initially
renderSessions();
// Ensure we have at least one session
if (!currentSession) {
const id = generateSessionId();
const name = "default";
sessions.push({ id, name });
currentSession = id;
saveSessions();
renderSessions();
}
// Load current session history (if it exists on Relay)
loadSession(currentSession);
// Switch session
document.getElementById("sessions").addEventListener("change", async e => {
currentSession = e.target.value;
history = [];
saveSessions();
addMessage("system", `Switched to session: ${getSessionName(currentSession)}`);
await loadSession(currentSession); // ✅ load the chat history from Relay
});
// Create new session
document.getElementById("newSessionBtn").addEventListener("click", () => {
const name = prompt("Enter new session name:");
if (!name) return;
const id = generateSessionId();
sessions.push({ id, name });
currentSession = id;
history = [];
saveSessions();
renderSessions();
addMessage("system", `Created session: ${name}`);
});
// Rename session
document.getElementById("renameSessionBtn").addEventListener("click", () => {
const session = sessions.find(s => s.id === currentSession);
if (!session) return;
const newName = prompt("Rename session:", session.name);
if (!newName) return;
session.name = newName;
saveSessions();
renderSessions();
addMessage("system", `Session renamed to: ${newName}`);
});
// Health check
checkHealth();
setInterval(checkHealth, 10000);
// Input events
document.getElementById("sendBtn").addEventListener("click", sendMessage);
document.getElementById("userInput").addEventListener("keypress", e => {
if (e.key === "Enter") sendMessage();
});
});
</script>
</body>
</html>