Implement multi-tasking (you can chat with a horny catgirl and chad thundercock at the same time)
This commit is contained in:
@@ -6,7 +6,7 @@ import path from "path";
|
||||
import bodyParser from "body-parser";
|
||||
import Database from "better-sqlite3";
|
||||
import FileStoreFactory from "session-file-store";
|
||||
import { exec } from "child_process";
|
||||
import {exec} from "child_process";
|
||||
|
||||
const modelState = new Map();
|
||||
const app = express();
|
||||
@@ -20,7 +20,7 @@ if (!fs.existsSync("./data")) {
|
||||
|
||||
function scheduleModelUnload(model) {
|
||||
if (!modelState.has(model)) {
|
||||
modelState.set(model, { timer: null, activeRequests: 0 });
|
||||
modelState.set(model, {timer: null, activeRequests: 0});
|
||||
}
|
||||
|
||||
const state = modelState.get(model);
|
||||
@@ -47,37 +47,189 @@ function scheduleModelUnload(model) {
|
||||
|
||||
const db = new Database(DB_PATH);
|
||||
db.pragma("journal_mode = WAL");
|
||||
db.pragma("foreign_keys = ON");
|
||||
db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
username TEXT PRIMARY KEY,
|
||||
password TEXT NOT NULL
|
||||
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
|
||||
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();
|
||||
db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS chat_sessions
|
||||
(
|
||||
id
|
||||
INTEGER
|
||||
PRIMARY
|
||||
KEY
|
||||
AUTOINCREMENT,
|
||||
username
|
||||
TEXT
|
||||
NOT
|
||||
NULL,
|
||||
title
|
||||
TEXT
|
||||
NOT
|
||||
NULL,
|
||||
created_at
|
||||
DATETIME
|
||||
DEFAULT
|
||||
CURRENT_TIMESTAMP,
|
||||
updated_at
|
||||
DATETIME
|
||||
DEFAULT
|
||||
CURRENT_TIMESTAMP
|
||||
)
|
||||
`).run();
|
||||
db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS chat_messages
|
||||
(
|
||||
id
|
||||
INTEGER
|
||||
PRIMARY
|
||||
KEY
|
||||
AUTOINCREMENT,
|
||||
chat_id
|
||||
INTEGER
|
||||
NOT
|
||||
NULL,
|
||||
role
|
||||
TEXT
|
||||
NOT
|
||||
NULL,
|
||||
content
|
||||
TEXT
|
||||
NOT
|
||||
NULL,
|
||||
created_at
|
||||
DATETIME
|
||||
DEFAULT
|
||||
CURRENT_TIMESTAMP,
|
||||
FOREIGN
|
||||
KEY
|
||||
(
|
||||
chat_id
|
||||
) REFERENCES chat_sessions
|
||||
(
|
||||
id
|
||||
) ON DELETE CASCADE
|
||||
)
|
||||
`).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
|
||||
const getLegacyChatsStmt = db.prepare(`
|
||||
SELECT role, content
|
||||
FROM chats
|
||||
WHERE username = ?
|
||||
ORDER BY datetime(created_at)
|
||||
`);
|
||||
const getRecentChatsStmt = db.prepare(`
|
||||
SELECT role, content FROM chats
|
||||
const deleteLegacyChatsStmt = db.prepare("DELETE FROM chats WHERE username = ?");
|
||||
const insertChatSessionStmt = db.prepare(
|
||||
"INSERT INTO chat_sessions (username, title) VALUES (?, ?)"
|
||||
);
|
||||
const listChatSessionsStmt = db.prepare(`
|
||||
SELECT id, title, created_at, updated_at
|
||||
FROM chat_sessions
|
||||
WHERE username = ?
|
||||
ORDER BY datetime(created_at) DESC
|
||||
LIMIT 20
|
||||
ORDER BY datetime(updated_at) DESC
|
||||
`);
|
||||
const deleteChatsStmt = db.prepare("DELETE FROM chats WHERE username = ?");
|
||||
|
||||
const getChatSessionStmt = db.prepare(
|
||||
"SELECT id, title FROM chat_sessions WHERE id = ? AND username = ?"
|
||||
);
|
||||
|
||||
const updateChatSessionTitleStmt = db.prepare(
|
||||
"UPDATE chat_sessions SET title = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND username = ?"
|
||||
);
|
||||
|
||||
const touchChatSessionStmt = db.prepare(
|
||||
"UPDATE chat_sessions SET updated_at = CURRENT_TIMESTAMP WHERE id = ? AND username = ?"
|
||||
);
|
||||
|
||||
const deleteChatSessionStmt = db.prepare(
|
||||
"DELETE FROM chat_sessions WHERE id = ? AND username = ?"
|
||||
);
|
||||
|
||||
const deleteAllChatSessionsStmt = db.prepare(
|
||||
"DELETE FROM chat_sessions WHERE username = ?"
|
||||
);
|
||||
|
||||
const insertChatMessageStmt = db.prepare(
|
||||
"INSERT INTO chat_messages (chat_id, role, content) VALUES (?, ?, ?)"
|
||||
);
|
||||
|
||||
const getChatMessagesStmt = db.prepare(`
|
||||
SELECT role, content
|
||||
FROM chat_messages
|
||||
WHERE chat_id = ?
|
||||
ORDER BY datetime(created_at)
|
||||
`
|
||||
)
|
||||
;
|
||||
const getRecentChatMessagesStmt = db.prepare(`
|
||||
SELECT role, content
|
||||
FROM chat_messages
|
||||
WHERE chat_id = ?
|
||||
ORDER BY datetime(created_at) DESC LIMIT 20
|
||||
`
|
||||
)
|
||||
;
|
||||
|
||||
const deleteChatMessagesStmt = db.prepare(
|
||||
"DELETE FROM chat_messages WHERE chat_id = ?"
|
||||
);
|
||||
|
||||
const migrationNeeded = db.prepare(
|
||||
"SELECT COUNT(*) as count FROM chat_messages"
|
||||
).get();
|
||||
const legacyChatsExist = db.prepare(
|
||||
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'chats'"
|
||||
).get();
|
||||
if (legacyChatsExist && migrationNeeded.count === 0) {
|
||||
const legacyUsers = db.prepare(
|
||||
"SELECT DISTINCT username FROM chats"
|
||||
).all();
|
||||
legacyUsers.forEach(({username}) => {
|
||||
const sessionId = insertChatSessionStmt.run(username, "Imported chat").lastInsertRowid;
|
||||
const legacyMessages = getLegacyChatsStmt.all(username);
|
||||
legacyMessages.forEach(msg => {
|
||||
insertChatMessageStmt.run(sessionId, msg.role, msg.content);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const FileStore = FileStoreFactory(session);
|
||||
app.use(bodyParser.json());
|
||||
@@ -131,24 +283,93 @@ app.get("/models", async (req, res) => {
|
||||
res.json(await response.json());
|
||||
});
|
||||
|
||||
app.get("/chats", requireLogin, (req, res) => {
|
||||
const user = req.session.user;
|
||||
const chats = listChatSessionsStmt.all(user);
|
||||
res.json({chats});
|
||||
});
|
||||
|
||||
app.post("/chats", requireLogin, (req, res) => {
|
||||
const user = req.session.user;
|
||||
const title = (req.body?.title || "New chat").trim() || "New chat";
|
||||
const chatId = insertChatSessionStmt.run(user, title).lastInsertRowid;
|
||||
res.json({chat: {id: chatId, title}});
|
||||
});
|
||||
|
||||
app.put("/chats/:id", requireLogin, (req, res) => {
|
||||
const user = req.session.user;
|
||||
const chatId = Number(req.params.id);
|
||||
const title = (req.body?.title || "").trim();
|
||||
if (!title) {
|
||||
return res.status(400).json({error: "Title is required"});
|
||||
}
|
||||
const result = updateChatSessionTitleStmt.run(title, chatId, user);
|
||||
if (result.changes === 0) {
|
||||
return res.status(404).json({error: "Chat not found"});
|
||||
}
|
||||
res.json({chat: {id: chatId, title}});
|
||||
});
|
||||
|
||||
app.delete("/chats/:id", requireLogin, (req, res) => {
|
||||
const user = req.session.user;
|
||||
const chatId = Number(req.params.id);
|
||||
const result = deleteChatSessionStmt.run(chatId, user);
|
||||
if (result.changes === 0) {
|
||||
return res.status(404).json({error: "Chat not found"});
|
||||
}
|
||||
res.json({message: "Chat deleted"});
|
||||
});
|
||||
|
||||
app.get("/chats/:id/messages", requireLogin, (req, res) => {
|
||||
const user = req.session.user;
|
||||
const chatId = Number(req.params.id);
|
||||
const session = getChatSessionStmt.get(chatId, user);
|
||||
if (!session) {
|
||||
return res.status(404).json({error: "Chat not found"});
|
||||
}
|
||||
const messages = getChatMessagesStmt.all(chatId);
|
||||
res.json({messages});
|
||||
});
|
||||
|
||||
app.post("/chats/:id/clear", requireLogin, (req, res) => {
|
||||
const user = req.session.user;
|
||||
const chatId = Number(req.params.id);
|
||||
const session = getChatSessionStmt.get(chatId, user);
|
||||
if (!session) {
|
||||
return res.status(404).json({error: "Chat not found"});
|
||||
}
|
||||
deleteChatMessagesStmt.run(chatId);
|
||||
touchChatSessionStmt.run(chatId, user);
|
||||
res.json({message: "Chat cleared"});
|
||||
});
|
||||
|
||||
app.post("/chat", requireLogin, async (req, res) => {
|
||||
const user = req.session.user;
|
||||
const { message, model } = req.body;
|
||||
const {message, model, chatId} = req.body;
|
||||
const selectedModel = model || "mistral:latest";
|
||||
const targetChatId = Number(chatId);
|
||||
|
||||
if (!targetChatId) {
|
||||
return res.status(400).json({error: "chatId is required"});
|
||||
}
|
||||
|
||||
const session = getChatSessionStmt.get(targetChatId, user);
|
||||
if (!session) {
|
||||
return res.status(404).json({error: "Chat not found"});
|
||||
}
|
||||
|
||||
if (!modelState.has(selectedModel)) {
|
||||
modelState.set(selectedModel, { activeRequests: 0, timer: null });
|
||||
modelState.set(selectedModel, {activeRequests: 0, timer: null});
|
||||
}
|
||||
const state = modelState.get(selectedModel);
|
||||
state.activeRequests++;
|
||||
|
||||
try {
|
||||
const history = getRecentChatsStmt.all(user);
|
||||
const history = getRecentChatMessagesStmt.all(targetChatId);
|
||||
const conversation = [...history].reverse();
|
||||
conversation.push({ role: "user", content: message });
|
||||
|
||||
insertChatStmt.run(user, "user", message);
|
||||
|
||||
conversation.push({role: "user", content: message});
|
||||
insertChatMessageStmt.run(targetChatId, "user", message);
|
||||
touchChatSessionStmt.run(targetChatId, user);
|
||||
const messagesPayload = conversation.map(m => ({
|
||||
role: m.role === "bot" ? "assistant" : "user",
|
||||
content: m.content
|
||||
@@ -156,7 +377,7 @@ app.post("/chat", requireLogin, async (req, res) => {
|
||||
|
||||
const response = await fetch("https://ai.krishd.ch/api/chat", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({
|
||||
model: selectedModel,
|
||||
messages: messagesPayload,
|
||||
@@ -175,10 +396,10 @@ app.post("/chat", requireLogin, async (req, res) => {
|
||||
let fullReply = "";
|
||||
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
const {value, done} = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
buffer += decoder.decode(value, {stream: true});
|
||||
|
||||
let lines = buffer.split("\n");
|
||||
buffer = lines.pop();
|
||||
@@ -197,15 +418,13 @@ app.post("/chat", requireLogin, async (req, res) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
insertChatStmt.run(user, "bot", fullReply);
|
||||
insertChatMessageStmt.run(targetChatId, "bot", fullReply);
|
||||
touchChatSessionStmt.run(targetChatId, user);
|
||||
console.log("Reply length:", fullReply.length);
|
||||
|
||||
res.json({ reply: fullReply });
|
||||
|
||||
res.json({reply: fullReply});
|
||||
} catch (err) {
|
||||
console.error("AI chat error:", err);
|
||||
res.status(500).json({ error: "AI request failed" });
|
||||
res.status(500).json({error: "AI request failed"});
|
||||
} finally {
|
||||
state.activeRequests--;
|
||||
|
||||
@@ -222,14 +441,8 @@ app.get("/session", (req, res) => {
|
||||
|
||||
app.get("/chat/history", requireLogin, (req, res) => {
|
||||
const user = req.session.user;
|
||||
const history = getChatsStmt.all(user);
|
||||
res.json({history});
|
||||
});
|
||||
|
||||
app.post("/chat/clear", requireLogin, (req, res) => {
|
||||
const user = req.session.user;
|
||||
deleteChatsStmt.run(user);
|
||||
res.json({message: "Chat cleared"});
|
||||
const chats = listChatSessionsStmt.all(user);
|
||||
res.json({history: chats});
|
||||
});
|
||||
|
||||
app.get("/", (req, res) => {
|
||||
|
||||
Reference in New Issue
Block a user