feat: Complete AI Chat Agent, Admin Settings (MCP/AI), and Translations
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
This commit is contained in:
+167
-25
@@ -5,11 +5,13 @@ import { insertLabelSchema, insertTaskSchema, insertNoteSchema, insertXpEventSch
|
||||
import { z } from "zod";
|
||||
import { EmailService } from "./email.js";
|
||||
import { mcpServer } from "./mcp";
|
||||
import { AiService } from "./ai.js";
|
||||
import { AiService, DEFAULT_SYSTEM_PROMPT } from "./ai.js";
|
||||
import { RecurrenceService } from "./services/recurrence.js";
|
||||
import { GamificationService } from "./gamification.js";
|
||||
|
||||
const emailService = new EmailService(storage);
|
||||
const aiService = new AiService(storage);
|
||||
const recurrenceService = new RecurrenceService(storage);
|
||||
const gamificationService = new GamificationService(storage);
|
||||
|
||||
import { setupAuth, hashPassword, comparePassword } from "./auth.js";
|
||||
@@ -59,13 +61,25 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
|
||||
req.login(adminUser, (err) => {
|
||||
if (err) return res.status(500).json({ error: "Login failed after setup" });
|
||||
return res.json(adminUser);
|
||||
req.session.save(() => {
|
||||
return res.json(adminUser);
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: "Failed to create admin user" });
|
||||
}
|
||||
});
|
||||
|
||||
// Check if AI is configured (for UI empty states)
|
||||
app.get("/api/ai/status", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
const key = await storage.getSystemSettings("ai_api_key");
|
||||
const provider = await storage.getSystemSettings("ai_provider");
|
||||
// Consider configured if we have a key OR if provider is 'ollama'
|
||||
const isConfigured = (provider === 'ollama') || (!!key && key.length > 0);
|
||||
res.json({ configured: isConfigured, provider });
|
||||
});
|
||||
|
||||
// --- Password Reset Routes ---
|
||||
app.post("/api/auth/forgot-password", async (req, res) => {
|
||||
try {
|
||||
@@ -256,16 +270,37 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
settings[key] = val || ""; // Return empty string if undefined for inputs
|
||||
}
|
||||
}
|
||||
|
||||
// Inject System Prompt with Default Fallback
|
||||
const storedPrompt = await storage.getSystemSettings("ai_system_prompt");
|
||||
settings["ai_system_prompt"] = storedPrompt || DEFAULT_SYSTEM_PROMPT;
|
||||
|
||||
res.json(settings);
|
||||
});
|
||||
|
||||
app.post("/api/admin/settings", isAdmin, async (req, res) => {
|
||||
const updates = req.body;
|
||||
const currentPrompt = await storage.getSystemSettings("ai_system_prompt");
|
||||
|
||||
// Check for System Prompt changes & Log
|
||||
if (updates.ai_system_prompt && updates.ai_system_prompt !== currentPrompt) {
|
||||
await storage.createAuditLog({
|
||||
userId: (req.user as User).id,
|
||||
action: "UPDATE",
|
||||
entityType: "SYSTEM_SETTINGS",
|
||||
entityId: "ai_system_prompt",
|
||||
details: { message: "AI System Prompt updated" },
|
||||
source: "ADMIN"
|
||||
});
|
||||
}
|
||||
|
||||
const keys = [
|
||||
"registration_enabled",
|
||||
"smtp_host", "smtp_port", "smtp_user", "smtp_pass", "smtp_from", "smtp_secure",
|
||||
"ai_provider", "ai_api_key", "ai_model", "ai_base_url",
|
||||
"ai_provider", "ai_api_key", "ai_model", "ai_base_url", "ai_system_prompt",
|
||||
"mcp_enabled", "mcp_port"
|
||||
];
|
||||
|
||||
for (const key of keys) {
|
||||
if (req.body[key] !== undefined) {
|
||||
await storage.setSystemSettings(key, String(req.body[key]));
|
||||
@@ -508,28 +543,36 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
if (conv.userId !== user.id) return res.sendStatus(403);
|
||||
|
||||
// Store User Message
|
||||
await storage.addMessage({
|
||||
const userMessage = await storage.addMessage({
|
||||
conversationId,
|
||||
role: 'user',
|
||||
content
|
||||
});
|
||||
|
||||
// Fetch history for context
|
||||
const dbMessages = await storage.getMessages(conversationId);
|
||||
// Convert to format expected by AiService (role, content)
|
||||
const history = dbMessages.map(m => ({ role: m.role as 'user' | 'assistant' | 'system', content: m.content }));
|
||||
// Send immediate response to client (Session Aware: request completes)
|
||||
res.json(userMessage);
|
||||
|
||||
// Build User Context (Tasks etc)
|
||||
const tasks = await storage.getTasksForUser(user.id);
|
||||
const activeTasks = tasks.filter(t => t.status !== 'done');
|
||||
const completedTasks = tasks.filter(t => t.status === 'done');
|
||||
// Background Processing (Fire & Forget)
|
||||
(async () => {
|
||||
try {
|
||||
// Fetch history for context
|
||||
const dbMessages = await storage.getMessages(conversationId);
|
||||
// Convert to format expected by AiService (role, content)
|
||||
const history = dbMessages.map(m => ({ role: m.role as 'user' | 'assistant' | 'system', content: m.content }));
|
||||
|
||||
const context = `
|
||||
// Build User Context (Tasks etc)
|
||||
const tasks = await storage.getTasksForUser(user.id);
|
||||
const activeTasks = tasks.filter(t => t.status !== 'done');
|
||||
const completedTasks = tasks.filter(t => t.status === 'done');
|
||||
|
||||
const currentClientTime = clientTime || new Date().toLocaleString();
|
||||
|
||||
const context = `
|
||||
User Context:
|
||||
- User ID: ${user.id}
|
||||
- Username: ${user.username}
|
||||
- XP: ${user.xp} (Level ${user.level})
|
||||
- Current Date/Time: ${clientTime || new Date().toLocaleString()}
|
||||
- Current Date/Time: ${currentClientTime}
|
||||
|
||||
Task Summary:
|
||||
- Total Active Tasks: ${activeTasks.length}
|
||||
@@ -542,23 +585,54 @@ Recent Active Tasks:
|
||||
${activeTasks.slice(0, 5).map(t => `- [${t.priority}] ${t.title}`).join('\n')}
|
||||
`;
|
||||
|
||||
// Call AI Service
|
||||
const responseContent = await aiService.chat(history, user, context);
|
||||
// Call AI Service
|
||||
const responseContent = await aiService.chat(history, user, context);
|
||||
|
||||
// Store AI Response
|
||||
const botMessage = await storage.addMessage({
|
||||
conversationId,
|
||||
role: 'assistant',
|
||||
content: responseContent
|
||||
});
|
||||
// Store AI Response
|
||||
await storage.addMessage({
|
||||
conversationId,
|
||||
role: 'assistant',
|
||||
content: responseContent
|
||||
});
|
||||
} catch (bgError) {
|
||||
console.error("Background AI Processing Error:", bgError);
|
||||
// Optional: Add a system message saying it failed?
|
||||
}
|
||||
})();
|
||||
|
||||
res.json(botMessage);
|
||||
} catch (e: any) {
|
||||
console.error("AI Route Error:", e);
|
||||
res.status(500).json({ error: e.message || "Failed to generate AI response" });
|
||||
}
|
||||
});
|
||||
|
||||
// Analyze completed task for follow-up
|
||||
app.post("/api/ai/analyze-completion", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
const user = req.user as User;
|
||||
if (!user.aiEnabled) return res.json({ needed: false }); // Silently fail/return false
|
||||
|
||||
try {
|
||||
const { taskId } = req.body;
|
||||
const task = await storage.getTask(taskId);
|
||||
if (!task) return res.status(404).json({ error: "Task not found" });
|
||||
|
||||
// Build context
|
||||
const context = `
|
||||
User Context:
|
||||
- User ID: ${user.id}
|
||||
- Username: ${user.username}
|
||||
- Current Date/Time: ${new Date().toLocaleString()}
|
||||
`;
|
||||
|
||||
const analysis = await aiService.analyzeTaskCompletion(task, context);
|
||||
res.json(analysis);
|
||||
} catch (e: any) {
|
||||
console.error("Analysis Error:", e);
|
||||
res.status(500).json({ error: "Failed to analyze task" });
|
||||
}
|
||||
});
|
||||
|
||||
// Edit message and regenerate (Regenerate Response)
|
||||
app.put("/api/ai/chat/:messageId", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
@@ -1402,12 +1476,80 @@ ${activeTasksWithLabels.slice(0, 5).map(t => `- [${t.priority}] [${t.label}] ${t
|
||||
app.patch("/api/goals/:id", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
try {
|
||||
const updated = await storage.updateGoal(req.params.id, req.body);
|
||||
const updated = await storage.updateTask(req.params.id, req.body);
|
||||
|
||||
// Check for Recurrence if task is marked done
|
||||
if (updated && updated.status === 'done' && updated.isRecurring && req.body.status === 'done') {
|
||||
// Fire and forget, or await? Await to ensure it happens.
|
||||
console.log(`[Recurrence] Checking recurrence for task ${updated.id}`);
|
||||
try {
|
||||
const nextTask = await recurrenceService.handleTaskCompletion(updated);
|
||||
if (nextTask) {
|
||||
console.log(`[Recurrence] Created next task: ${nextTask.id} (${nextTask.title})`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[Recurrence] Error creating next task:", err);
|
||||
}
|
||||
}
|
||||
|
||||
res.json(updated);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: "Failed to update goal" });
|
||||
res.status(500).json({ error: "Failed to update task" });
|
||||
}
|
||||
});
|
||||
|
||||
// Export User Data
|
||||
app.post("/api/user/export", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
try {
|
||||
const user = req.user as User;
|
||||
const { includeTasks, includeLabels, includeSettings } = req.body;
|
||||
|
||||
const exportData: any = {
|
||||
exportedAt: new Date(),
|
||||
user: { username: user.username, email: user.email }
|
||||
};
|
||||
|
||||
if (includeTasks) {
|
||||
exportData.tasks = await storage.getTasksForUser(user.id);
|
||||
}
|
||||
|
||||
if (includeLabels) {
|
||||
exportData.labels = await storage.getLabels(user.id);
|
||||
}
|
||||
|
||||
if (includeSettings && user.role === 'admin') {
|
||||
// Only admins can request system settings dump, though arguably they might mean "User Settings"
|
||||
// The prompt said "system settings". I will include system settings if they are admin.
|
||||
// But maybe they meant user preferences? The prompt specifically said "system settings".
|
||||
const allSettings: Record<string, string> = {};
|
||||
const keys = [
|
||||
"registration_enabled", "smtp_host", "smtp_port", "smtp_user", "smtp_from",
|
||||
"ai_provider", "ai_model", "ai_base_url", "ai_system_prompt", "mcp_enabled"
|
||||
];
|
||||
for (const key of keys) {
|
||||
const val = await storage.getSystemSettings(key);
|
||||
if (val) allSettings[key] = val;
|
||||
}
|
||||
exportData.systemSettings = allSettings;
|
||||
}
|
||||
|
||||
await storage.createAuditLog({
|
||||
userId: user.id,
|
||||
action: "EXPORT_DATA",
|
||||
entityType: "USER_DATA",
|
||||
entityId: user.id.toString(),
|
||||
source: "USER",
|
||||
details: { includeTasks, includeLabels, includeSettings }
|
||||
});
|
||||
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="taskflow_export_${user.username}.json"`);
|
||||
res.json(exportData);
|
||||
} catch (e) {
|
||||
console.error("Export Failed:", e);
|
||||
res.status(500).json({ error: "Failed to export data" });
|
||||
}
|
||||
}); // End of routes
|
||||
return httpServer;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user