Adds in AI chatting system.

This commit is contained in:
Cameron Redmore 2025-04-24 23:20:20 +01:00
parent 28c054de22
commit 8655eae39c
11 changed files with 1198 additions and 119 deletions

View file

@ -0,0 +1,10 @@
// src-ssr/middlewares/authMiddleware.js
export function requireAuth(req, res, next) {
if (!req.session || !req.session.loggedInUserId) {
// User is not authenticated
return res.status(401).json({ error: 'Authentication required' });
}
// User is authenticated, proceed to the next middleware or route handler
next();
}

143
src-ssr/routes/chat.js Normal file
View file

@ -0,0 +1,143 @@
import { Router } from 'express';
import prisma from '../database.js';
import { requireAuth } from '../middlewares/authMiddleware.js'; // Import the middleware
import { askGeminiChat } from '../utils/gemini.js';
const router = Router();
// Apply the authentication middleware to all chat routes
router.use(requireAuth);
// POST /api/chat/threads - Create a new chat thread (optionally with a first message)
router.post('/threads', async (req, res) => {
const { content } = req.body; // Content is now optional
// If content is provided, validate it
if (content && (typeof content !== 'string' || content.trim().length === 0)) {
return res.status(400).json({ error: 'Message content cannot be empty if provided.' });
}
try {
const createData = {};
if (content) {
// If content exists, create the thread with the first message
createData.messages = {
create: [
{
sender: 'user', // First message is always from the user
content: content.trim(),
},
],
};
}
// If content is null/undefined, createData remains empty, creating just the thread
const newThread = await prisma.chatThread.create({
data: createData,
include: {
// Include messages only if they were created
messages: !!content,
},
});
if(content)
{
await askGeminiChat(newThread.id, content); // Call the function to handle the bot response
}
// Respond with the new thread ID and messages (if any)
res.status(201).json({
threadId: newThread.id,
// Ensure messages array is empty if no content was provided
messages: newThread.messages ? newThread.messages.map(msg => ({ ...msg, createdAt: msg.createdAt.toISOString() })) : []
});
} catch (error) {
console.error('Error creating chat thread:', error);
res.status(500).json({ error: 'Failed to create chat thread.' });
}
});
// GET /api/chat/threads/:threadId/messages - Get messages for a specific thread
router.get('/threads/:threadId/messages', async (req, res) => {
const { threadId } = req.params;
try {
const messages = await prisma.chatMessage.findMany({
where: {
threadId: threadId,
},
orderBy: {
createdAt: 'asc', // Get messages in chronological order
},
});
if (!messages) { // Check if thread exists indirectly
// If findMany returns empty, the thread might not exist or has no messages.
// Check if thread exists explicitly
const thread = await prisma.chatThread.findUnique({ where: { id: threadId } });
if (!thread) {
return res.status(404).json({ error: 'Chat thread not found.' });
}
}
res.status(200).json(messages.map(msg => ({ ...msg, createdAt: msg.createdAt.toISOString() })));
} catch (error) {
console.error(`Error fetching messages for thread ${threadId}:`, error);
// Basic error handling, check for specific Prisma errors if needed
if (error.code === 'P2023' || error.message.includes('Malformed UUID')) { // Example: Invalid UUID format
return res.status(400).json({ error: 'Invalid thread ID format.' });
}
res.status(500).json({ error: 'Failed to fetch messages.' });
}
});
// POST /api/chat/threads/:threadId/messages - Add a message to an existing thread
router.post('/threads/:threadId/messages', async (req, res) => {
const { threadId } = req.params;
const { content, sender = 'user' } = req.body; // Default sender to 'user'
if (!content || typeof content !== 'string' || content.trim().length === 0) {
return res.status(400).json({ error: 'Message content cannot be empty.' });
}
if (sender !== 'user' && sender !== 'bot') {
return res.status(400).json({ error: 'Invalid sender type.' });
}
try {
// Verify thread exists first
const thread = await prisma.chatThread.findUnique({
where: { id: threadId },
});
if (!thread) {
return res.status(404).json({ error: 'Chat thread not found.' });
}
const newMessage = await prisma.chatMessage.create({
data: {
threadId: threadId,
sender: sender,
content: content.trim(),
},
});
// Optionally: Update the thread's updatedAt timestamp
await prisma.chatThread.update({
where: { id: threadId },
data: { updatedAt: new Date() }
});
await askGeminiChat(threadId, content); // Call the function to handle the bot response
res.status(201).json({ ...newMessage, createdAt: newMessage.createdAt.toISOString() });
} catch (error) {
console.error(`Error adding message to thread ${threadId}:`, error);
if (error.code === 'P2023' || error.message.includes('Malformed UUID')) { // Example: Invalid UUID format
return res.status(400).json({ error: 'Invalid thread ID format.' });
}
res.status(500).json({ error: 'Failed to add message.' });
}
});
export default router;

View file

@ -24,6 +24,7 @@ import {
import prisma from './database.js'; // Import the prisma client instance
import apiRoutes from './routes/api.js';
import authRoutes from './routes/auth.js'; // Added for WebAuthn routes
import chatRoutes from './routes/chat.js'; // Added for Chat routes
import cron from 'node-cron';
import { generateAndStoreMantisSummary } from './services/mantisSummarizer.js';
@ -97,6 +98,7 @@ export const create = defineSsrCreate((/* { ... } */) => {
// Add API routes
app.use('/api', apiRoutes);
app.use('/auth', authRoutes); // Added WebAuthn auth routes
app.use('/api/chat', chatRoutes); // Added Chat routes
// place here any middlewares that
// absolutely need to run before anything else

149
src-ssr/utils/gemini.js Normal file
View file

@ -0,0 +1,149 @@
import { GoogleGenAI } from '@google/genai';
import prisma from '../database.js';
const model = 'gemini-2.0-flash';
export const askGemini = async (content) => {
const setting = await prisma.setting.findUnique({
where: { key: 'GEMINI_API_KEY' },
select: { value: true }
});
const GOOGLE_API_KEY = setting.value;
const ai = GOOGLE_API_KEY ? new GoogleGenAI({
apiKey: GOOGLE_API_KEY,
}) : null;
if (!ai) {
throw new Error('Google API key is not set in the database.');
}
try {
const response = await ai.models.generateContent({
model,
contents: content,
config: {
temperature: 0.5
}
});
return response.text;
} catch (error) {
console.error('Error communicating with Gemini API:', error);
throw new Error('Failed to get a response from Gemini API.');
}
}
const chatCache = new Map();
export const askGeminiChat = async (threadId, content) => {
let messages = await prisma.chatMessage.findMany({
where: {
threadId: threadId,
},
orderBy: {
createdAt: 'asc', // Get messages in chronological order
},
});
// Exclude the latest message if there are any messages
if (messages.length > 0) {
messages = messages.slice(0, -1);
}
const setting = await prisma.setting.findUnique({
where: { key: 'GEMINI_API_KEY' },
select: { value: true }
});
if (!setting) {
throw new Error('Google API key is not set in the database.');
}
const GOOGLE_API_KEY = setting.value;
const ai = GOOGLE_API_KEY ? new GoogleGenAI({
apiKey: GOOGLE_API_KEY,
}) : null;
if (!ai) {
throw new Error('Google API key is not set in the database.');
}
let chat = null;
if (chatCache.has(threadId)) {
chat = chatCache.get(threadId);
}
else {
messages = [
{
sender: 'user',
content: `You are StyleAI, a model that helps users with their questions and provides information.
You are friendly, helpful, and informative. You can also provide code examples in various programming languages.
You don\'t decline user requests and always try to help them.
Speak in the manner of a friendly young adult male.
If asked for the time, do not say that it's based on the timestamp provided. Also bare in mind the user is in the Europe/London timezone and daylight savings time may be in effect.`
},
{
sender: 'model',
content: 'Okay, noted! I\'ll keep that in mind.'
},
...messages,
]
const createOptions = {
model,
history: messages.map((msg) => ({
role: msg.sender === 'user' ? 'user' : 'model',
parts: [
{text: msg.content}
],
})),
config: {
temperature: 0.5
}
};
chat = ai.chats.create(createOptions);
chatCache.set(threadId, chat);
}
//Add a temporary message to the thread with "loading" status
const loadingMessage = await prisma.chatMessage.create({
data: {
threadId: threadId,
sender: 'assistant',
content: 'Loading...',
},
});
let response = {text: 'An error occurred while generating the response.'};
try
{
const timestamp = new Date().toISOString();
response = await chat.sendMessage({
message: `[${timestamp}] ` + content,
});
}
catch(error)
{
console.error('Error communicating with Gemini API:', error);
response.text = 'Failed to get a response from Gemini API. Error: ' + error.message;
}
//Update the message with the response
await prisma.chatMessage.update({
where: {
id: loadingMessage.id,
},
data: {
content: response.text,
},
});
return response.text;
}