Adds in AI chatting system.
This commit is contained in:
parent
28c054de22
commit
8655eae39c
11 changed files with 1198 additions and 119 deletions
10
src-ssr/middlewares/authMiddleware.js
Normal file
10
src-ssr/middlewares/authMiddleware.js
Normal 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
143
src-ssr/routes/chat.js
Normal 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;
|
|
@ -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
149
src-ssr/utils/gemini.js
Normal 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;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue