Merge pull request #2 from Fretuks/save-chats-and-characters-to-database
Persist chat data in SQLite database
This commit is contained in:
@@ -9,6 +9,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"body-parser": "^1.20.2",
|
"body-parser": "^1.20.2",
|
||||||
|
"better-sqlite3": "^9.5.0",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"express-session": "^1.17.3",
|
"express-session": "^1.17.3",
|
||||||
"session-file-store": "^1.5.0"
|
"session-file-store": "^1.5.0"
|
||||||
|
|||||||
@@ -1,16 +1,53 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import session from "express-session";
|
import session from "express-session";
|
||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import bodyParser from "body-parser";
|
import bodyParser from "body-parser";
|
||||||
|
import Database from "better-sqlite3";
|
||||||
|
import FileStoreFactory from "session-file-store";
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = 3000;
|
const PORT = 3000;
|
||||||
const DATA_PATH = "./data/users.json";
|
const DB_PATH = "./data/app.db";
|
||||||
const CHAT_PATH = "./data/chats.json";
|
|
||||||
|
|
||||||
import FileStoreFactory from "session-file-store";
|
if (!fs.existsSync("./data")) {
|
||||||
|
fs.mkdirSync("./data", {recursive: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = new Database(DB_PATH);
|
||||||
|
db.pragma("journal_mode = WAL");
|
||||||
|
db.prepare(`
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
username TEXT PRIMARY KEY,
|
||||||
|
password TEXT NOT NULL
|
||||||
|
)
|
||||||
|
`).run();
|
||||||
|
db.prepare(`
|
||||||
|
CREATE TABLE IF NOT EXISTS chats (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
username TEXT NOT NULL,
|
||||||
|
role TEXT NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
`).run();
|
||||||
|
|
||||||
|
const insertUserStmt = db.prepare("INSERT INTO users (username, password) VALUES (?, ?)");
|
||||||
|
const getUserStmt = db.prepare("SELECT username, password FROM users WHERE username = ?");
|
||||||
|
const insertChatStmt = db.prepare("INSERT INTO chats (username, role, content) VALUES (?, ?, ?)");
|
||||||
|
const getChatsStmt = db.prepare(`
|
||||||
|
SELECT role, content FROM chats
|
||||||
|
WHERE username = ?
|
||||||
|
ORDER BY datetime(created_at)
|
||||||
|
`);
|
||||||
|
const getRecentChatsStmt = db.prepare(`
|
||||||
|
SELECT role, content FROM chats
|
||||||
|
WHERE username = ?
|
||||||
|
ORDER BY datetime(created_at) DESC
|
||||||
|
LIMIT 20
|
||||||
|
`);
|
||||||
|
const deleteChatsStmt = db.prepare("DELETE FROM chats WHERE username = ?");
|
||||||
|
|
||||||
const FileStore = FileStoreFactory(session);
|
const FileStore = FileStoreFactory(session);
|
||||||
app.use(bodyParser.json());
|
app.use(bodyParser.json());
|
||||||
@@ -29,25 +66,20 @@ app.use(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
let chats = JSON.parse(fs.readFileSync(CHAT_PATH));
|
|
||||||
const saveChats = () => fs.writeFileSync(CHAT_PATH, JSON.stringify(chats, null, 2));
|
|
||||||
|
|
||||||
if (!fs.existsSync(DATA_PATH)) fs.writeFileSync(DATA_PATH, "{}");
|
|
||||||
const users = JSON.parse(fs.readFileSync(DATA_PATH));
|
|
||||||
const saveUsers = () => fs.writeFileSync(DATA_PATH, JSON.stringify(users, null, 2));
|
|
||||||
|
|
||||||
app.post("/register", async (req, res) => {
|
app.post("/register", async (req, res) => {
|
||||||
const {username, password} = req.body;
|
const {username, password} = req.body;
|
||||||
if (users[username]) return res.status(400).json({error: "User already exists"});
|
const existingUser = getUserStmt.get(username);
|
||||||
|
|
||||||
|
if (existingUser) return res.status(400).json({error: "User already exists"});
|
||||||
|
|
||||||
const hashed = await bcrypt.hash(password, 10);
|
const hashed = await bcrypt.hash(password, 10);
|
||||||
users[username] = {password: hashed};
|
insertUserStmt.run(username, hashed);
|
||||||
saveUsers();
|
|
||||||
res.json({message: "Registration successful"});
|
res.json({message: "Registration successful"});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/login", async (req, res) => {
|
app.post("/login", async (req, res) => {
|
||||||
const {username, password} = req.body;
|
const {username, password} = req.body;
|
||||||
const user = users[username];
|
const user = getUserStmt.get(username);
|
||||||
if (!user) return res.status(400).json({error: "User not found"});
|
if (!user) return res.status(400).json({error: "User not found"});
|
||||||
const valid = await bcrypt.compare(password, user.password);
|
const valid = await bcrypt.compare(password, user.password);
|
||||||
if (!valid) return res.status(403).json({error: "Invalid password"});
|
if (!valid) return res.status(403).json({error: "Invalid password"});
|
||||||
@@ -67,7 +99,7 @@ const requireLogin = (req, res, next) => {
|
|||||||
app.get("/models", async (req, res) => {
|
app.get("/models", async (req, res) => {
|
||||||
const response = await fetch("https://ai.krishd.ch/api/tags");
|
const response = await fetch("https://ai.krishd.ch/api/tags");
|
||||||
res.json(await response.json());
|
res.json(await response.json());
|
||||||
})
|
});
|
||||||
|
|
||||||
app.post("/chat", requireLogin, async (req, res) => {
|
app.post("/chat", requireLogin, async (req, res) => {
|
||||||
const user = req.session.user;
|
const user = req.session.user;
|
||||||
@@ -75,11 +107,13 @@ app.post("/chat", requireLogin, async (req, res) => {
|
|||||||
const selectedModel = model || "mistral:latest";
|
const selectedModel = model || "mistral:latest";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!chats[user]) chats[user] = [];
|
const history = getRecentChatsStmt.all(user);
|
||||||
chats[user].push({ role: "user", content: message });
|
const conversation = [...history].reverse();
|
||||||
if (chats[user].length > 20) chats[user] = chats[user].slice(-20);
|
conversation.push({ role: "user", content: message });
|
||||||
|
|
||||||
const messagesPayload = chats[user].map(m => ({
|
insertChatStmt.run(user, "user", message);
|
||||||
|
|
||||||
|
const messagesPayload = conversation.map(m => ({
|
||||||
role: m.role === "bot" ? "assistant" : "user",
|
role: m.role === "bot" ? "assistant" : "user",
|
||||||
content: m.content
|
content: m.content
|
||||||
}));
|
}));
|
||||||
@@ -111,7 +145,7 @@ app.post("/chat", requireLogin, async (req, res) => {
|
|||||||
buffer += decoder.decode(value, { stream: true });
|
buffer += decoder.decode(value, { stream: true });
|
||||||
|
|
||||||
let lines = buffer.split("\n");
|
let lines = buffer.split("\n");
|
||||||
buffer = lines.pop(); // keep incomplete JSON
|
buffer = lines.pop();
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (!line.trim()) continue;
|
if (!line.trim()) continue;
|
||||||
@@ -128,8 +162,7 @@ app.post("/chat", requireLogin, async (req, res) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
chats[user].push({ role: "bot", content: fullReply });
|
insertChatStmt.run(user, "bot", fullReply);
|
||||||
saveChats();
|
|
||||||
console.log("Reply length:", fullReply.length);
|
console.log("Reply length:", fullReply.length);
|
||||||
|
|
||||||
res.json({ reply: fullReply });
|
res.json({ reply: fullReply });
|
||||||
@@ -140,19 +173,6 @@ app.post("/chat", requireLogin, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
async function checkSession() {
|
|
||||||
const res = await get('/session');
|
|
||||||
if (res.user) {
|
|
||||||
authDiv.style.display = "none";
|
|
||||||
chatDiv.style.display = "grid";
|
|
||||||
loadChatHistory();
|
|
||||||
msgInput.focus();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
app.get("/session", (req, res) => {
|
app.get("/session", (req, res) => {
|
||||||
if (req.session.user) res.json({user: req.session.user});
|
if (req.session.user) res.json({user: req.session.user});
|
||||||
else res.json({user: null});
|
else res.json({user: null});
|
||||||
@@ -160,13 +180,13 @@ app.get("/session", (req, res) => {
|
|||||||
|
|
||||||
app.get("/chat/history", requireLogin, (req, res) => {
|
app.get("/chat/history", requireLogin, (req, res) => {
|
||||||
const user = req.session.user;
|
const user = req.session.user;
|
||||||
res.json({history: chats[user] || []});
|
const history = getChatsStmt.all(user);
|
||||||
|
res.json({history});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/chat/clear", requireLogin, (req, res) => {
|
app.post("/chat/clear", requireLogin, (req, res) => {
|
||||||
const user = req.session.user;
|
const user = req.session.user;
|
||||||
chats[user] = [];
|
deleteChatsStmt.run(user);
|
||||||
saveChats();
|
|
||||||
res.json({message: "Chat cleared"});
|
res.json({message: "Chat cleared"});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -174,4 +194,4 @@ app.get("/", (req, res) => {
|
|||||||
res.sendFile(path.resolve("public/index.html"));
|
res.sendFile(path.resolve("public/index.html"));
|
||||||
});
|
});
|
||||||
|
|
||||||
app.listen(PORT, () => console.log(`Server running at http://localhost:${PORT}`));
|
app.listen(PORT, () => console.log(`Server running at http://localhost:${PORT}`));
|
||||||
|
|||||||
Reference in New Issue
Block a user