Update to v0.9.1 #1
@@ -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" });
|
||||
}
|
||||
});
|
||||
|
||||
// -----------------------------------------------------
|
||||
|
||||
3
core/relay/sessions/sess-6rxu7eia.meta.json
Normal file
3
core/relay/sessions/sess-6rxu7eia.meta.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"name": "My Coding Session"
|
||||
}
|
||||
18
core/relay/sessions/sess-dnm44wyb.json
Normal file
18
core/relay/sessions/sess-dnm44wyb.json
Normal 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?"
|
||||
}
|
||||
]
|
||||
1
core/relay/sessions/sess-dnm44wyb.meta.json
Normal file
1
core/relay/sessions/sess-dnm44wyb.meta.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
1
core/relay/sessions/sess-l08ndm60.meta.json
Normal file
1
core/relay/sessions/sess-l08ndm60.meta.json
Normal file
@@ -0,0 +1 @@
|
||||
{"name":"Session 2"}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user