sessions improved, v0.7.0

This commit is contained in:
serversdwn
2025-12-21 15:50:52 -05:00
parent 01d4811717
commit b4613ac30c
7 changed files with 424 additions and 41 deletions

View File

@@ -4,9 +4,17 @@
import express from "express";
import dotenv from "dotenv";
import cors from "cors";
import fs from "fs/promises";
import path from "path";
import { fileURLToPath } from "url";
dotenv.config();
// ES module __dirname workaround
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const SESSIONS_DIR = path.join(__dirname, "sessions");
const app = express();
app.use(cors());
app.use(express.json());
@@ -173,20 +181,182 @@ app.post("/chat", async (req, res) => {
// -----------------------------------------------------
// SESSION ENDPOINTS (for UI)
// -----------------------------------------------------
// In-memory session storage (could be replaced with a database)
const sessions = new Map();
// Helper functions for session persistence
async function ensureSessionsDir() {
try {
await fs.mkdir(SESSIONS_DIR, { recursive: true });
} catch (err) {
console.error("Failed to create sessions directory:", err);
}
}
app.get("/sessions/:id", (req, res) => {
async function loadSession(sessionId) {
try {
const sessionPath = path.join(SESSIONS_DIR, `${sessionId}.json`);
const data = await fs.readFile(sessionPath, "utf-8");
return JSON.parse(data);
} catch (err) {
// File doesn't exist or is invalid - return empty array
return [];
}
}
async function saveSession(sessionId, history, metadata = {}) {
try {
await ensureSessionsDir();
const sessionPath = path.join(SESSIONS_DIR, `${sessionId}.json`);
const metadataPath = path.join(SESSIONS_DIR, `${sessionId}.meta.json`);
// Save history
await fs.writeFile(sessionPath, JSON.stringify(history, null, 2), "utf-8");
// Save metadata (name, etc.)
await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
return true;
} catch (err) {
console.error(`Failed to save session ${sessionId}:`, err);
return false;
}
}
async function loadSessionMetadata(sessionId) {
try {
const metadataPath = path.join(SESSIONS_DIR, `${sessionId}.meta.json`);
const data = await fs.readFile(metadataPath, "utf-8");
return JSON.parse(data);
} catch (err) {
// No metadata file, return default
return { name: sessionId };
}
}
async function saveSessionMetadata(sessionId, metadata) {
try {
await ensureSessionsDir();
const metadataPath = path.join(SESSIONS_DIR, `${sessionId}.meta.json`);
await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
return true;
} catch (err) {
console.error(`Failed to save metadata for ${sessionId}:`, err);
return false;
}
}
async function listSessions() {
try {
await ensureSessionsDir();
const files = await fs.readdir(SESSIONS_DIR);
const sessions = [];
for (const file of files) {
if (file.endsWith(".json") && !file.endsWith(".meta.json")) {
const sessionId = file.replace(".json", "");
const sessionPath = path.join(SESSIONS_DIR, file);
const stats = await fs.stat(sessionPath);
// Try to read the session to get message count
let messageCount = 0;
try {
const data = await fs.readFile(sessionPath, "utf-8");
const history = JSON.parse(data);
messageCount = history.length;
} catch (e) {
// Invalid JSON, skip
}
// Load metadata (name)
const metadata = await loadSessionMetadata(sessionId);
sessions.push({
id: sessionId,
name: metadata.name || sessionId,
lastModified: stats.mtime,
messageCount
});
}
}
// Sort by last modified (newest first)
sessions.sort((a, b) => b.lastModified - a.lastModified);
return sessions;
} catch (err) {
console.error("Failed to list sessions:", err);
return [];
}
}
async function deleteSession(sessionId) {
try {
const sessionPath = path.join(SESSIONS_DIR, `${sessionId}.json`);
const metadataPath = path.join(SESSIONS_DIR, `${sessionId}.meta.json`);
// Delete session file
await fs.unlink(sessionPath);
// Delete metadata file (if exists)
try {
await fs.unlink(metadataPath);
} catch (e) {
// Metadata file doesn't exist, that's ok
}
return true;
} catch (err) {
console.error(`Failed to delete session ${sessionId}:`, err);
return false;
}
}
// GET /sessions - List all sessions
app.get("/sessions", async (req, res) => {
const sessions = await listSessions();
res.json(sessions);
});
// GET /sessions/:id - Get specific session history
app.get("/sessions/:id", async (req, res) => {
const sessionId = req.params.id;
const history = sessions.get(sessionId) || [];
const history = await loadSession(sessionId);
res.json(history);
});
app.post("/sessions/:id", (req, res) => {
// POST /sessions/:id - Save session history
app.post("/sessions/:id", async (req, res) => {
const sessionId = req.params.id;
const history = req.body;
sessions.set(sessionId, history);
res.json({ ok: true, saved: history.length });
const success = await saveSession(sessionId, history);
if (success) {
res.json({ ok: true, saved: history.length });
} else {
res.status(500).json({ error: "Failed to save session" });
}
});
// PATCH /sessions/:id/metadata - Update session metadata (name, etc.)
app.patch("/sessions/:id/metadata", async (req, res) => {
const sessionId = req.params.id;
const metadata = req.body;
const success = await saveSessionMetadata(sessionId, metadata);
if (success) {
res.json({ ok: true, metadata });
} else {
res.status(500).json({ error: "Failed to update metadata" });
}
});
// DELETE /sessions/:id - Delete a session
app.delete("/sessions/:id", async (req, res) => {
const sessionId = req.params.id;
const success = await deleteSession(sessionId);
if (success) {
res.json({ ok: true, deleted: sessionId });
} else {
res.status(500).json({ error: "Failed to delete session" });
}
});
// -----------------------------------------------------

View File

@@ -0,0 +1,3 @@
{
"name": "My Coding Session"
}

View File

@@ -0,0 +1,18 @@
[
{
"role": "user",
"content": "Hello! this is a new test session. Do you know who i am?"
},
{
"role": "assistant",
"content": "Hi! In this session, I don't have information about your previous interactions. You can tell me who you are or any other details you'd like to share. How can I assist you today?"
},
{
"role": "user",
"content": "sure im brian! i am designing you... you are a robot!"
},
{
"role": "assistant",
"content": "Hello Brian! Nice to meet you. As an AI, I don't have physical design capabilities, but I'm here to help with any information or tasks you need. How can I assist you in your design process?"
}
]

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1 @@
{"name":"Session 2"}

View File

@@ -81,6 +81,14 @@
</label>
</div>
</div>
<div class="settings-section" style="margin-top: 24px;">
<h4>Session Management</h4>
<p class="settings-desc">Manage your saved chat sessions:</p>
<div id="sessionList" class="session-list">
<p style="color: var(--text-fade); font-size: 0.85rem;">Loading sessions...</p>
</div>
</div>
</div>
<div class="modal-footer">
<button id="saveSettingsBtn" class="primary-btn">Save</button>
@@ -99,21 +107,28 @@
let history = [];
let currentSession = localStorage.getItem("currentSession") || null;
let sessions = JSON.parse(localStorage.getItem("sessions") || "[]");
let sessions = []; // Now loaded from server
function saveSessions() {
localStorage.setItem("sessions", JSON.stringify(sessions));
localStorage.setItem("currentSession", currentSession);
async function loadSessionsFromServer() {
try {
const resp = await fetch(`${RELAY_BASE}/sessions`);
const serverSessions = await resp.json();
sessions = serverSessions;
return sessions;
} catch (e) {
console.error("Failed to load sessions from server:", e);
return [];
}
}
function renderSessions() {
async 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;
opt.textContent = s.name || s.id;
if (s.id === currentSession) opt.selected = true;
select.appendChild(opt);
});
@@ -121,7 +136,21 @@
function getSessionName(id) {
const s = sessions.find(s => s.id === id);
return s ? s.name : id;
return s ? (s.name || s.id) : id;
}
async function saveSessionMetadata(sessionId, name) {
try {
await fetch(`${RELAY_BASE}/sessions/${sessionId}/metadata`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name })
});
return true;
} catch (e) {
console.error("Failed to save session metadata:", e);
return false;
}
}
async function loadSession(id) {
@@ -258,55 +287,75 @@
localStorage.setItem("theme", isDark ? "dark" : "light");
});
// 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();
}
// Sessions - Load from server
(async () => {
await loadSessionsFromServer();
await renderSessions();
// Load current session history (if it exists on Relay)
loadSession(currentSession);
// Ensure we have at least one session
if (sessions.length === 0) {
const id = generateSessionId();
const name = "default";
currentSession = id;
history = [];
await saveSession(); // Create empty session on server
await saveSessionMetadata(id, name);
await loadSessionsFromServer();
await renderSessions();
localStorage.setItem("currentSession", currentSession);
} else {
// If no current session or current session doesn't exist, use first one
if (!currentSession || !sessions.find(s => s.id === currentSession)) {
currentSession = sessions[0].id;
localStorage.setItem("currentSession", currentSession);
}
}
// Load current session history
if (currentSession) {
await loadSession(currentSession);
}
})();
// Switch session
document.getElementById("sessions").addEventListener("change", async e => {
currentSession = e.target.value;
history = [];
saveSessions();
localStorage.setItem("currentSession", currentSession);
addMessage("system", `Switched to session: ${getSessionName(currentSession)}`);
await loadSession(currentSession); // ✅ load the chat history from Relay
await loadSession(currentSession);
});
// Create new session
document.getElementById("newSessionBtn").addEventListener("click", () => {
document.getElementById("newSessionBtn").addEventListener("click", async () => {
const name = prompt("Enter new session name:");
if (!name) return;
const id = generateSessionId();
sessions.push({ id, name });
currentSession = id;
history = [];
saveSessions();
renderSessions();
localStorage.setItem("currentSession", currentSession);
// Create session on server
await saveSession();
await saveSessionMetadata(id, name);
await loadSessionsFromServer();
await renderSessions();
addMessage("system", `Created session: ${name}`);
});
// Rename session
document.getElementById("renameSessionBtn").addEventListener("click", () => {
document.getElementById("renameSessionBtn").addEventListener("click", async () => {
const session = sessions.find(s => s.id === currentSession);
if (!session) return;
const newName = prompt("Rename session:", session.name);
const newName = prompt("Rename session:", session.name || currentSession);
if (!newName) return;
session.name = newName;
saveSessions();
renderSessions();
// Update metadata on server
await saveSessionMetadata(currentSession, newName);
await loadSessionsFromServer();
await renderSessions();
addMessage("system", `Session renamed to: ${newName}`);
});
@@ -335,9 +384,90 @@
document.querySelector(`input[name="backend"][value="${savedBackend}"]`).checked = true;
}
// Show modal
// Session management functions
async function loadSessionList() {
try {
// Reload from server to get latest
await loadSessionsFromServer();
const sessionListEl = document.getElementById("sessionList");
if (sessions.length === 0) {
sessionListEl.innerHTML = '<p style="color: var(--text-fade); font-size: 0.85rem;">No saved sessions found</p>';
return;
}
sessionListEl.innerHTML = "";
sessions.forEach(sess => {
const sessionItem = document.createElement("div");
sessionItem.className = "session-item";
const sessionInfo = document.createElement("div");
sessionInfo.className = "session-info";
const sessionName = sess.name || sess.id;
const lastModified = new Date(sess.lastModified).toLocaleString();
sessionInfo.innerHTML = `
<strong>${sessionName}</strong>
<small>${sess.messageCount} messages • ${lastModified}</small>
`;
const deleteBtn = document.createElement("button");
deleteBtn.className = "session-delete-btn";
deleteBtn.textContent = "🗑️";
deleteBtn.title = "Delete session";
deleteBtn.onclick = async () => {
if (!confirm(`Delete session "${sessionName}"?`)) return;
try {
await fetch(`${RELAY_BASE}/sessions/${sess.id}`, { method: "DELETE" });
// Reload sessions from server
await loadSessionsFromServer();
// If we deleted the current session, switch to another or create new
if (currentSession === sess.id) {
if (sessions.length > 0) {
currentSession = sessions[0].id;
localStorage.setItem("currentSession", currentSession);
history = [];
await loadSession(currentSession);
} else {
const id = generateSessionId();
const name = "default";
currentSession = id;
localStorage.setItem("currentSession", currentSession);
history = [];
await saveSession();
await saveSessionMetadata(id, name);
await loadSessionsFromServer();
}
}
// Refresh both the dropdown and the settings list
await renderSessions();
await loadSessionList();
addMessage("system", `Deleted session: ${sessionName}`);
} catch (e) {
alert("Failed to delete session: " + e.message);
}
};
sessionItem.appendChild(sessionInfo);
sessionItem.appendChild(deleteBtn);
sessionListEl.appendChild(sessionItem);
});
} catch (e) {
const sessionListEl = document.getElementById("sessionList");
sessionListEl.innerHTML = '<p style="color: #ff3333; font-size: 0.85rem;">Failed to load sessions</p>';
}
}
// Show modal and load session list
settingsBtn.addEventListener("click", () => {
settingsModal.classList.add("show");
loadSessionList(); // Refresh session list when opening settings
});
// Hide modal functions

View File

@@ -362,3 +362,63 @@ select:hover {
background: #ff7a33;
box-shadow: var(--accent-glow);
}
/* Session List */
.session-list {
display: flex;
flex-direction: column;
gap: 8px;
max-height: 300px;
overflow-y: auto;
}
.session-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
border: 1px solid rgba(255,102,0,0.3);
border-radius: 6px;
background: rgba(255,102,0,0.05);
transition: all 0.2s;
}
.session-item:hover {
border-color: var(--accent);
background: rgba(255,102,0,0.1);
}
.session-info {
display: flex;
flex-direction: column;
gap: 4px;
flex: 1;
}
.session-info strong {
color: var(--text-main);
font-size: 0.95rem;
}
.session-info small {
color: var(--text-fade);
font-size: 0.75rem;
}
.session-delete-btn {
background: transparent;
border: 1px solid rgba(255,102,0,0.5);
color: var(--accent);
padding: 6px 10px;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
transition: all 0.2s;
}
.session-delete-btn:hover {
background: rgba(255,0,0,0.2);
border-color: #ff3333;
color: #ff3333;
box-shadow: 0 0 8px rgba(255,0,0,0.3);
}