271 lines
8.3 KiB
HTML
271 lines
8.3 KiB
HTML
<!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>
|