Initial clean commit - unified Lyra stack
This commit is contained in:
270
core/ui/index.html
Normal file
270
core/ui/index.html
Normal 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>
|
||||
Reference in New Issue
Block a user