import type { Express, Request, Response, NextFunction } from "express"; import { createServer, type Server } from "http"; import { storage } from "./storage"; import { setupAuth } from "./auth"; import { generateChatResponse } from "./openai"; import { canUseOpenAI, canUseQwen } from "./fallbackChat"; import { getPersonalityConfig } from "./personalities"; import { generateImage, imageGenerationSchema, isFluxAvailable } from "./flux"; import { generateVideo, videoGenerationSchema, isVideoGenerationAvailable } from "./video"; import OpenAI from "openai"; import { nanoid } from "nanoid"; import { messageSchema, conversationSchema, insertMessageSchema, insertConversationSchema, messageRoleSchema, personalityTypeSchema } from "@shared/schema"; import { z } from "zod"; // Track the current model in use let currentModelStatus = { model: 'openai', isOpenAIAvailable: true, isQwenAvailable: true, lastChecked: new Date() }; // Function to check and update model availability status async function updateModelStatus() { try { const isOpenAIAvailable = await canUseOpenAI(); const isQwenAvailable = await canUseQwen(); // Determine current model based on availability let model = 'unavailable'; if (isOpenAIAvailable) { model = 'openai'; } else if (isQwenAvailable) { model = 'qwen'; } currentModelStatus = { model, isOpenAIAvailable, isQwenAvailable, lastChecked: new Date() }; console.log(`Updated model status: ${model} (OpenAI: ${isOpenAIAvailable}, Qwen: ${isQwenAvailable})`); return currentModelStatus; } catch (error) { console.error("Error updating model status:", error); return currentModelStatus; } } // Initialize model status updateModelStatus(); export async function registerRoutes(app: Express): Promise { // Set up authentication setupAuth(app); // Get all conversations (filtered by user if authenticated) app.get("/api/conversations", async (req: Request, res: Response) => { try { let conversations; // If user is authenticated, only get their conversations if (req.isAuthenticated() && req.user) { const userId = req.user.id; conversations = await storage.getUserConversations(userId); } else { // For unauthenticated users, get only conversations without a userId conversations = await storage.getConversations(); // Filter out conversations that belong to users conversations = conversations.filter(conv => !conv.userId); } res.json(conversations); } catch (error) { console.error("Error fetching conversations:", error); res.status(500).json({ message: "Failed to fetch conversations." }); } }); // Create a new conversation app.post("/api/conversations", async (req: Request, res: Response) => { try { const conversationId = nanoid(); // Generate title based on user's message let title = req.body.title; if (!title || title === "New Conversation") { try { const openaiClient = new OpenAI(); const response = await openaiClient.chat.completions.create({ model: "gpt-3.5-turbo", messages: [ { role: "system", content: "You are a title generator. Create a concise, descriptive title (2-4 words) that summarizes the following message. Respond with just the title." }, { role: "user", content: req.body.firstMessage || "New chat conversation" } ], max_tokens: 15, temperature: 0.6 }); title = response.choices[0].message.content?.trim() || "New Conversation"; } catch (err) { console.error("Error generating AI title:", err); title = "New Conversation"; } } // Include user ID if authenticated const conversationData: any = { id: conversationId, title: title, personality: req.body.personality || "general" }; // Associate conversation with user if authenticated if (req.isAuthenticated() && req.user) { conversationData.userId = req.user.id; } const result = insertConversationSchema.safeParse(conversationData); if (!result.success) { return res.status(400).json({ message: "Invalid conversation data." }); } const conversation = await storage.createConversation(result.data); res.status(201).json(conversation); } catch (error) { console.error("Error creating conversation:", error); res.status(500).json({ message: "Failed to create conversation." }); } }); // Generate AI title for conversation app.post("/api/conversations/:id/generate-title", async (req: Request, res: Response) => { try { const { id } = req.params; // Get the conversation messages const messages = await storage.getMessages(id); if (messages.length < 2) { return res.status(400).json({ message: "Need at least one exchange to generate a title" }); } // Extract the first few messages (user and assistant) to use as context const contextMessages = messages.slice(0, Math.min(4, messages.length)) .map(msg => `${msg.role}: ${msg.content}`).join("\n"); // Generate the title using AI let title; try { // First try using OpenAI const openaiClient = new OpenAI(); const response = await openaiClient.chat.completions.create({ model: "gpt-3.5-turbo", messages: [ { role: "system", content: "You are a helpful assistant that generates short, descriptive titles (max 6 words) for conversations based on their content. Respond with just the title." }, { role: "user", content: `Generate a short, descriptive title (maximum 6 words) for this conversation:\n${contextMessages}` } ], max_tokens: 20, temperature: 0.7 }); title = response.choices[0].message.content?.trim(); // Use fallback if title is undefined or empty if (!title) { title = `Chat ${new Date().toLocaleDateString()}`; } } catch (err) { // Fallback to a generic title console.error("Error generating AI title:", err); title = `Chat ${new Date().toLocaleDateString()}`; } // Update the conversation with the new title const updatedConversation = await storage.updateConversationTitle(id, title as string); if (!updatedConversation) { return res.status(404).json({ message: "Conversation not found" }); } res.json(updatedConversation); } catch (error) { console.error("Error generating title:", error); res.status(500).json({ message: "Failed to generate title." }); } }); // Get messages for a conversation app.get("/api/conversations/:id/messages", async (req: Request, res: Response) => { try { const { id } = req.params; const conversation = await storage.getConversation(id); if (!conversation) { return res.status(404).json({ message: "Conversation not found." }); } // Check ownership if the conversation belongs to a user if (conversation.userId && req.isAuthenticated() && req.user) { // User must be the owner of the conversation if (conversation.userId !== req.user.id) { return res.status(403).json({ message: "You don't have permission to access this conversation." }); } } const messages = await storage.getMessages(id); // If user is authenticated, include their system context if (req.isAuthenticated() && req.user) { const userContext = { role: "system", content: req.user.systemContext || `Chat with ${req.user.username}`, conversationId: id, createdAt: new Date() }; messages.unshift(userContext); } res.json(messages); } catch (error) { console.error("Error fetching messages:", error); res.status(500).json({ message: "Failed to fetch messages." }); } }); // Send a message and get AI response app.post("/api/chat", async (req: Request, res: Response) => { try { // Update model status before processing await updateModelStatus(); // Check if any AI model is available if (currentModelStatus.model === 'unavailable') { return res.status(503).json({ message: "All AI models are currently unavailable. Please check your API keys." }); } // Validate incoming data const result = conversationSchema.safeParse(req.body); if (!result.success) { return res.status(400).json({ message: "Invalid chat data format." }); } const { messages } = result.data; const conversationId = req.body.conversationId || "default"; // Ensure the conversation exists const conversation = await storage.getConversation(conversationId); if (!conversation && conversationId !== "default") { return res.status(404).json({ message: "Conversation not found." }); } // If conversation belongs to a user, check permissions if (conversation && conversation.userId) { // If user is not authenticated or not the owner if (!req.isAuthenticated() || !req.user || conversation.userId !== req.user.id) { return res.status(403).json({ message: "You don't have permission to access this conversation." }); } } // Store user message const userMessage = messages[messages.length - 1]; if (userMessage.role !== "user") { return res.status(400).json({ message: "Last message must be from the user." }); } await storage.createMessage({ content: userMessage.content, role: userMessage.role, conversationId }); // Get user system context if available let userSystemContext: string | undefined = undefined; if (req.isAuthenticated() && req.user && req.user.systemContext) { // If we have a logged-in user, include their system context userSystemContext = req.user.systemContext; console.log("Including user system context in conversation:", userSystemContext ? "Yes" : "None available"); } // Generate AI response with user's system context if available const aiResponse = await generateChatResponse(messages, userSystemContext); // Store AI response const savedMessage = await storage.createMessage({ content: aiResponse, role: "assistant", conversationId }); // Return the AI response with model info res.json({ message: savedMessage, conversationId, modelInfo: { model: currentModelStatus.model, isFallback: currentModelStatus.model !== 'openai' } }); } catch (error: any) { console.error("Chat API error:", error); res.status(500).json({ message: error.message || "Failed to process chat message." }); } }); // Get current model status app.get("/api/model-status", async (_req: Request, res: Response) => { try { // If it's been more than 5 minutes since last check, update status const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000); if (currentModelStatus.lastChecked < fiveMinutesAgo) { await updateModelStatus(); } return res.json(currentModelStatus); } catch (error) { console.error("Error getting model status:", error); return res.status(500).json({ message: "Failed to get model status" }); } }); // Delete a conversation app.delete("/api/conversations/:id", async (req: Request, res: Response) => { try { const { id } = req.params; // Don't allow deleting the default conversation if (id === "default") { return res.status(400).json({ message: "Cannot delete the default conversation" }); } // Check if conversation exists const conversation = await storage.getConversation(id); if (!conversation) { return res.status(404).json({ message: "Conversation not found" }); } // Check ownership if the conversation belongs to a user if (conversation.userId && req.isAuthenticated() && req.user) { // User must be the owner of the conversation if (conversation.userId !== req.user.id) { return res.status(403).json({ message: "You don't have permission to delete this conversation." }); } } // Delete the conversation const success = await storage.deleteConversation(id); if (success) { res.status(200).json({ message: "Conversation deleted successfully" }); } else { res.status(500).json({ message: "Failed to delete conversation" }); } } catch (error) { console.error("Error deleting conversation:", error); res.status(500).json({ message: "Server error deleting conversation" }); } }); // Update conversation title app.patch("/api/conversations/:id/title", async (req: Request, res: Response) => { try { const { id } = req.params; const { title } = req.body; // Validate title if (!title || typeof title !== 'string' || title.trim().length === 0) { return res.status(400).json({ message: "Valid title is required" }); } // Get the conversation const conversation = await storage.getConversation(id); if (!conversation) { return res.status(404).json({ message: "Conversation not found" }); } // Check ownership if the conversation belongs to a user if (conversation.userId && req.isAuthenticated() && req.user) { // User must be the owner of the conversation if (conversation.userId !== req.user.id) { return res.status(403).json({ message: "You don't have permission to update this conversation." }); } } // Update the conversation const updatedConversation = await storage.createConversation({ ...conversation, title: title.trim() }); res.json(updatedConversation); } catch (error) { console.error("Error updating conversation title:", error); res.status(500).json({ message: "Failed to update conversation title" }); } }); // Update conversation personality app.patch("/api/conversations/:id/personality", async (req: Request, res: Response) => { try { const { id } = req.params; const { personality } = req.body; // Validate personality const result = personalityTypeSchema.safeParse(personality); if (!result.success) { return res.status(400).json({ message: "Invalid personality type", validOptions: personalityTypeSchema.options }); } // Get the conversation const conversation = await storage.getConversation(id); if (!conversation) { return res.status(404).json({ message: "Conversation not found" }); } // Check ownership if the conversation belongs to a user if (conversation.userId && req.isAuthenticated() && req.user) { // User must be the owner of the conversation if (conversation.userId !== req.user.id) { return res.status(403).json({ message: "You don't have permission to update this conversation." }); } } // Update the conversation personality const updatedConversation = await storage.updateConversationPersonality(id, result.data); // Return the updated conversation with personality details const personalityConfig = getPersonalityConfig(result.data); res.json({ ...updatedConversation, personalityConfig: { name: personalityConfig.name, description: personalityConfig.description, emoji: personalityConfig.emoji } }); } catch (error) { console.error("Error updating conversation personality:", error); res.status(500).json({ message: "Failed to update conversation personality" }); } }); // Get available personalities app.get("/api/personalities", async (_req: Request, res: Response) => { try { // Get all personality types from the schema const personalityTypes = personalityTypeSchema.options; // Map to include details for each personality const personalities = personalityTypes.map(type => { const config = getPersonalityConfig(type); return { id: type, name: config.name, description: config.description, emoji: config.emoji }; }); res.json(personalities); } catch (error) { console.error("Error fetching personalities:", error); res.status(500).json({ message: "Failed to fetch personalities" }); } }); // Generate image with FLUX.1-dev app.post("/api/generate-image", async (req: Request, res: Response) => { try { // Validate the request body using the schema const result = imageGenerationSchema.safeParse(req.body); if (!result.success) { return res.status(400).json({ message: "Invalid image generation parameters", errors: result.error.format() }); } // Generate the image const imageUrl = await generateImage(result.data); // Return the image URL return res.json({ success: true, imageUrl, params: result.data }); } catch (error: any) { console.error("Error generating image:", error); return res.status(500).json({ success: false, message: error.message || "Failed to generate image" }); } }); // Check FLUX availability app.get("/api/flux-status", async (_req: Request, res: Response) => { try { const isAvailable = await isFluxAvailable(); return res.json({ isAvailable, model: "FLUX.1-dev" }); } catch (error) { console.error("Error checking FLUX availability:", error); return res.status(500).json({ isAvailable: false, message: "Error checking FLUX availability" }); } }); // Generate video using Replicate app.post("/api/generate-video", async (req: Request, res: Response) => { try { // Validate the request body using the schema const result = videoGenerationSchema.safeParse(req.body); if (!result.success) { return res.status(400).json({ message: "Invalid video generation parameters", errors: result.error.format() }); } // Generate the video const videoUrl = await generateVideo(result.data); // Return the video URL return res.json({ success: true, videoUrl, params: result.data }); } catch (error: any) { console.error("Error generating video:", error); return res.status(500).json({ success: false, message: error.message || "Failed to generate video" }); } }); // Check video generation availability app.get("/api/video-status", async (_req: Request, res: Response) => { try { const isAvailable = await isVideoGenerationAvailable(); return res.json({ isAvailable, model: "Wan-AI/Wan2.1-T2V-14B" }); } catch (error) { console.error("Error checking video generation availability:", error); return res.status(500).json({ isAvailable: false, message: "Error checking video generation availability" }); } }); // Health check endpoint app.get("/api/health", (_req: Request, res: Response) => { return res.json({ status: "ok" }); }); const httpServer = createServer(app); return httpServer; }