Spaces:
Running
Running
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<Server> { | |
// 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; | |
} | |